Files
HomeAgent/site/index.html
JianFeeeee 631963fdc7 feat(site): 总起页改为「真正的 AgentOS 长这样」+ 修页脚状态栏不可点
## 总起页:从对比改为展示(用户要求)

前几版是对照表(「别人说 X → 我们要做到 Y」)。用户指出重点不是对比,
而是**把自己作为「真正的 AgentOS 样例」展示出来**。改为四张并列卡片,
每条给「机制 + 可测数字」,不做比较。

标题「真正的 AgentOS,长这样」;四题:隔离 / 调度 / 通信 / 资源 ——
操作系统躲不开的四道题,逐题给答案。

## 文案改了第三轮(用户指「读着太难受」)

按「短句、有节奏、不堆从句」重写四段正文。举一例:

  旧:插件不是进程内的一个库,是内核 spawn 的独立进程。崩了就把它的
      工具、钩子、通道一并摘掉,其余照跑。
  新:插件不在内核里,是另一个进程。崩了就把它注册的东西一并摘掉,其余照跑。

## 修页脚「状态」栏不可点(用户报的 bug)

三行原为 `<span class="muted">` 死文本,点不动。改为链接:

- 最新发布:v1.3.x 线   → /releases
- main 在研:1.4.0      → blob/main/internal/meta/meta.go(版本号的实际来源)
- 许可:AGPL-3.0-only   → blob/main/LICENSE

## 一处自查纠正

我一度在卡片里写「插件崩溃 → 恢复 <1s」,那是照抄 README 的旧说法。
查源码 `dynamic_proc.go` 发现退避实为 **1s / 2s / 3s**(`procRestartBackoff=1s`,
5 分钟内崩 3 次 `procMaxRestarts=3` 即停手等人),**<1s 不成立**。
改为如实写明退避序列与停手机上阈值。README 那句待另开一轮核实。

## 验证

- 双主题:4 卡片、控制台错误 0、横向溢出 0
- 一滑一页仍生效(6 次滑动偏差恒为 72px = scroll-padding-top)
- JS 禁用 24/24 可见;reduce 下 snap 自动关闭
- 390/768/1440 溢出均 0
- 页脚 9 个 gitcode 目标逐个对照 origin/main 的树:**全部存在**
  (不只看 HTTP 200 —— gitcode 对错误路径也返回 200,此前踩过)
2026-09-20 10:11:02 +08:00

2519 lines
112 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>HomeAgent — 常驻型个人 Agent 框架</title>
<meta name="description" content="HomeAgent 是一个持续运行的个人智能 Agent 框架:内核零 IO所有 IO 由插件承载、三层记忆Context → Document → Graph、输入调度两类别 + 四级中断)与驻留式子 agent。" />
<link rel="icon" type="image/svg+xml" href="assets/logo.svg" />
<meta property="og:title" content="HomeAgent — 常驻型个人 Agent 框架" />
<meta property="og:description" content="内核零 IO · 三层记忆 · 四级中断调度 · 驻留式子 agent" />
<meta property="og:type" content="website" />
<script>
/* 在首次绘制前定主题,避免浅/深闪一下。
优先级localStorage 手动选择 > 系统偏好。 */
(function () {
try {
var saved = localStorage.getItem("ha-theme");
var sys = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches
? "light" : "dark";
document.documentElement.setAttribute("data-theme", saved || sys);
} catch (e) {
document.documentElement.setAttribute("data-theme", "dark");
}
})();
</script>
<style>
/* ── 设计令牌 ──
品牌色两主题共用(蓝/青/金 = 三层记忆);语义色按主题覆盖。
默认跟随系统,可用导航栏按钮手动切换(存 localStorage。 */
:root {
--brand-blue: #3b82f6; /* Context 层 */
--brand-cyan: #06b6d4; /* Document 层 */
--brand-gold: #f59e0b; /* Graph / Kernel */
--brand-violet: #8b5cf6;
--brand-sky: #38bdf8;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-pill: 999px;
--font-sans: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
--ease-back: cubic-bezier(0.34, 1.56, 0.64, 1);
--dur: 300ms;
/* 深色(默认) */
--bg: #070b16;
--bg-soft: rgba(17, 24, 44, 0.72);
--bg-card: rgba(17, 24, 44, 0.58);
--bg-card-hi: rgba(23, 32, 58, 0.72);
--border: rgba(255, 255, 255, 0.09);
--border-hi: rgba(59, 130, 246, 0.38);
--text: #eef1f8;
--text-dim: #a7b0c4;
--text-muted: #77809a;
--code-bg: rgba(6, 10, 22, 0.85);
--code-fg: #cdd3f5;
--nav-bg: rgba(7, 11, 22, 0.72);
--aurora-a: rgba(59, 130, 246, 0.22);
--aurora-b: rgba(6, 182, 212, 0.18);
--aurora-c: rgba(139, 92, 246, 0.16);
--aurora-d: rgba(245, 158, 11, 0.10);
--particle: 148, 190, 255;
--grid-line: rgba(255, 255, 255, 0.035);
--spot: rgba(255, 255, 255, 0.07);
--hover: rgba(255, 255, 255, 0.06);
--sheen: rgba(255, 255, 255, 0.38);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.28);
--shadow-lg: 0 12px 36px rgba(0, 0, 0, 0.38);
--shadow-glow: 0 0 0 1px rgba(59, 130, 246, 0.22),
0 8px 32px rgba(59, 130, 246, 0.18);
}
/* 浅色:同一套语义变量,另一组取值 */
:root[data-theme="light"] {
--bg: #f4f7fc;
--bg-soft: rgba(255, 255, 255, 0.82);
--bg-card: rgba(255, 255, 255, 0.72);
--bg-card-hi: rgba(255, 255, 255, 0.9);
--border: rgba(15, 23, 42, 0.10);
--border-hi: rgba(37, 99, 235, 0.45);
--text: #0d1526;
--text-dim: #48546c;
--text-muted: #78849a;
--code-bg: #f1f5fb;
--code-fg: #1e293b;
--nav-bg: rgba(244, 247, 252, 0.78);
--aurora-a: rgba(59, 130, 246, 0.22);
--aurora-b: rgba(6, 182, 212, 0.18);
--aurora-c: rgba(139, 92, 246, 0.14);
--aurora-d: rgba(245, 158, 11, 0.14);
--particle: 37, 99, 235;
--grid-line: rgba(15, 23, 42, 0.045);
--spot: rgba(15, 23, 42, 0.05);
--hover: rgba(15, 23, 42, 0.05);
--sheen: rgba(15, 23, 42, 0.16);
--shadow-md: 0 4px 16px rgba(15, 23, 42, 0.08);
--shadow-lg: 0 12px 36px rgba(15, 23, 42, 0.12);
--shadow-glow: 0 0 0 1px rgba(37, 99, 235, 0.16),
0 8px 28px rgba(37, 99, 235, 0.14);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
/* 防横向溢出:装饰层(极光/粒子)永远不该产生滚动条。
实测 canvas 曾因按 innerWidth 铺开而多出 15px故这里兜一层。 */
body { overflow-x: clip; }
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
line-height: 1.75;
-webkit-font-smoothing: antialiased;
}
/* 背景光晕:蓝/青/金,呼应三层记忆 */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
background:
radial-gradient(60rem 40rem at 15% -10%, rgba(59, 130, 246, 0.16), transparent 60%),
radial-gradient(50rem 35rem at 85% 5%, rgba(6, 182, 212, 0.12), transparent 60%),
radial-gradient(45rem 30rem at 50% 100%, rgba(245, 158, 11, 0.08), transparent 60%);
pointer-events: none;
}
.wrap { width: min(1120px, 92vw); margin: 0 auto; }
/* ── 顶部导航 ── */
header {
position: sticky;
top: 0;
z-index: 50;
backdrop-filter: blur(14px);
background: var(--nav-bg);
border-bottom: 1px solid var(--border);
}
nav {
display: flex;
align-items: center;
gap: 1.25rem;
padding: 0.85rem 0;
}
nav .brand {
display: flex;
align-items: center;
gap: 0.6rem;
font-weight: 700;
letter-spacing: 0.01em;
margin-right: auto;
}
nav .brand img { width: 30px; height: 30px; border-radius: 8px; }
nav a.link {
color: var(--text-dim);
text-decoration: none;
font-size: 0.92rem;
padding: 0.35rem 0.2rem;
border-bottom: 2px solid transparent;
transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
nav a.link:hover { color: var(--text); border-bottom-color: var(--brand-blue); }
nav .en { color: var(--text-muted); font-size: 0.85rem; }
@media (max-width: 760px) {
nav { flex-wrap: wrap; gap: 0.6rem; }
nav .hide-sm { display: none; }
}
/* ── Hero ── */
.hero { padding: 6rem 0 4.5rem; }
.hero-grid { display: grid; grid-template-columns: 1fr; }
.hero-copy { max-width: 46rem; }
@media (max-width: 860px) {
.hero { padding: 3rem 0 2rem; }
.hero-grid { gap: 2rem; text-align: center; }
.hero-copy { margin: 0 auto; }
.hero .cta { justify-content: center; }
}
.tagline {
display: inline-block;
font-size: 0.8rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand-sky);
border: 1px solid rgba(56, 189, 248, 0.3);
background: rgba(56, 189, 248, 0.08);
padding: 0.3rem 0.7rem;
border-radius: var(--radius-pill);
margin-bottom: 1.1rem;
}
h1 {
font-size: clamp(2.1rem, 5vw, 3.2rem);
line-height: 1.2;
margin: 0 0 1rem;
letter-spacing: -0.02em;
}
h1 .grad {
background: linear-gradient(120deg, var(--brand-blue), var(--brand-cyan) 50%, var(--brand-gold));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lede {
font-size: 1.08rem;
color: var(--text-dim);
max-width: 40rem;
margin: 0 0 1.6rem;
}
@media (max-width: 860px) { .lede { margin-inline: auto; } }
.cta { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: center; }
.btn {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.62rem 1.1rem;
border-radius: var(--radius-md);
font-size: 0.94rem;
font-weight: 600;
text-decoration: none;
border: 1px solid transparent;
transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease),
background var(--dur) var(--ease);
}
.btn:hover { transform: translateY(-1px); }
.btn-primary {
background: linear-gradient(135deg, var(--brand-blue), var(--brand-cyan));
color: #04121f;
box-shadow: var(--shadow-glow);
}
.btn-ghost {
background: var(--bg-soft);
color: var(--text);
border-color: var(--border);
}
.btn-ghost:hover { background: var(--hover); }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
}
/* ── 通用区块 ── */
section { padding: 3.6rem 0; }
section > .wrap > h2 {
font-size: clamp(1.4rem, 3vw, 1.85rem);
margin: 0 0 0.5rem;
letter-spacing: -0.01em;
}
section > .wrap > .sub {
color: var(--text-dim);
margin: 0 0 2rem;
max-width: 46rem;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 1.4rem;
transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease),
box-shadow var(--dur) var(--ease);
}
.card:hover {
border-color: rgba(59, 130, 246, 0.35);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.card h3 { margin: 0 0 0.5rem; font-size: 1.05rem; }
.card p { margin: 0; color: var(--text-dim); font-size: 0.93rem; }
.card .icon {
width: 34px; height: 34px;
border-radius: var(--radius-sm);
display: grid; place-items: center;
margin-bottom: 0.8rem;
font-size: 1.05rem;
}
.grid-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.1rem; }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.1rem; }
@media (max-width: 860px) {
.grid-2, .grid-3 { grid-template-columns: 1fr; }
}
/* ── 整屏翻页 ──
用 mandatory一滑一页。前提是**每节都必须塞得进一屏** ——
mandatory 下比视口高的节,超出部分会永久滚不到。
为此 features 的 5 条设计决定已各自独立成节(见 stageB2
architecture 的流程图也单独成节。
实测 proximity 不满足需求:滑 500px 落在 500离最近边界 395px
距离太远不触发吸附 —— 那样只是"有时粘一下",不是翻页。 */
html {
scroll-snap-type: y mandatory;
scroll-padding-top: 4.5rem;
}
section {
min-height: calc(100svh - 4.5rem);
display: flex;
flex-direction: column;
justify-content: center;
scroll-snap-align: start;
}
section > .wrap { width: min(1120px, 92vw); margin: 0 auto; }
/* 吸附容器不能有 transform 祖先:会让 snap 失效(实测过)。
故 .wrap 上不加任何位移类动画,入场只做透明度。 */
@media (prefers-reduced-motion: reduce) {
html { scroll-snap-type: none; }
}
/* 窄屏/矮屏放宽:手机可视高度小且地址栏收缩会变,强制一屏反而难受 */
@media (max-width: 860px), (max-height: 620px) {
html { scroll-snap-type: none; }
section { min-height: 0; }
}
/* ── 总起页:四项 OS 本职 ──
用四张并列的卡片而不是对照表:用户要的是「展示我们做到了什么」,
不是「和别人比」。每张卡固定给「机制 + 可测数字」两段。 */
.duties {
margin-top: 2.2rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.duty {
position: relative;
padding: 1.35rem 1.4rem 1.5rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
transition: border-color 0.3s var(--ease), transform 0.3s var(--ease-back);
}
.duty:hover { border-color: var(--border-hi); transform: translateY(-3px); }
.duty-k {
position: absolute;
top: 1.15rem; right: 1.25rem;
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.12em;
color: var(--brand-cyan);
opacity: 0.75;
}
.duty h3 {
margin: 0 0 0.6rem;
font-size: 1.06rem;
letter-spacing: -0.01em;
}
.duty p {
margin: 0 0 0.85rem;
font-size: 0.9rem;
line-height: 1.75;
color: var(--text-dim);
}
.duty p strong { color: var(--brand-cyan); font-weight: 700; }
.duty-ev {
display: inline-block;
font-family: var(--font-mono);
font-size: 0.72rem;
line-height: 1.5;
color: var(--text-muted);
padding-top: 0.7rem;
border-top: 1px solid var(--border);
width: 100%;
}
@media (max-width: 860px) {
.duties { grid-template-columns: 1fr; gap: 0.8rem; }
}
/* ── 五个决定的索引(总起页)── */
.idx {
list-style: none;
margin: 2.2rem 0 0;
padding: 0;
display: grid;
gap: 0.5rem;
}
.idx li {
display: flex;
align-items: center;
gap: 0.85rem;
padding: 0.7rem 1rem;
font-size: 0.95rem;
color: var(--text-dim);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
transition: border-color .25s var(--ease), color .25s var(--ease), transform .25s var(--ease);
}
.idx li:hover { border-color: var(--border-hi); color: var(--text); transform: translateX(3px); }
.idx li span {
flex: 0 0 auto;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--brand-cyan);
}
/* ── 交错行:文/图左右互换 ──
为什么用 grid 而不是 flex 的 order 翻转order 只改视觉次序,
键盘 Tab 与读屏仍按 DOM —— 这里 DOM 始终「标题在前」,
翻的只是视觉,所以两者都正确。 */
.alt {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 3.2rem;
align-items: center;
margin-top: 3.4rem;
}
.alt.flip .alt-copy { order: 2; }
.alt.flip .alt-visual { order: 1; }
.alt-n {
display: inline-block;
font-family: var(--font-mono);
font-size: 0.75rem;
letter-spacing: 0.08em;
color: var(--brand-cyan);
border: 1px solid var(--border);
border-radius: var(--radius-pill);
padding: 0.1rem 0.55rem;
margin-bottom: 0.7rem;
}
.alt-copy h3 {
font-size: clamp(1.15rem, 2vw, 1.5rem);
margin: 0 0 0.7rem;
letter-spacing: -0.01em;
}
.alt-copy p { margin: 0; color: var(--text-dim); line-height: 1.8; }
.alt-visual { min-width: 0; }
.alt-visual svg { width: 100%; height: auto; display: block; }
.dv-note {
margin: 0.6rem 0 0;
text-align: center;
font-size: 0.75rem;
color: var(--text-muted);
}
@media (max-width: 860px) {
.alt { grid-template-columns: 1fr; gap: 1.6rem; margin-top: 2.6rem; }
.alt.flip .alt-copy, .alt.flip .alt-visual { order: 0; }
}
/* ── 示意图图元 ── */
.dv-bound { fill: none; stroke: var(--border); stroke-width: 1.5; stroke-dasharray: 6 5; }
.dv-box { fill: var(--bg-card); stroke: var(--border-hi); stroke-width: 1.4; }
.dv-accent { fill: rgba(6, 182, 212, 0.10); stroke: var(--brand-cyan); }
.dv-run { fill: rgba(59, 130, 246, 0.10); stroke: var(--brand-blue); }
.dv-l4 { fill: rgba(139, 92, 246, 0.13); stroke: var(--brand-violet); }
.dv-l3 { fill: rgba(59, 130, 246, 0.11); stroke: var(--brand-blue); }
.dv-l2 { fill: rgba(6, 182, 212, 0.10); stroke: var(--brand-cyan); }
.dv-l1 { fill: rgba(245, 158, 11, 0.10); stroke: var(--brand-gold); }
.dv-t { fill: var(--text); font-size: 15px; font-weight: 700; text-anchor: middle; }
.dv-ts { fill: var(--text); font-size: 12.5px; font-weight: 600; text-anchor: middle; }
.dv-s { fill: var(--text-muted); font-size: 10.5px; text-anchor: middle; }
.dv-lab { fill: var(--text-dim); font-size: 11px; text-anchor: middle; }
.dv-labs { fill: var(--text-muted); font-size: 9.5px; text-anchor: middle; }
.dv-cap { fill: var(--text-muted); font-size: 10px; }
.dv-seg { fill: rgba(119,128,154,.07); stroke: var(--border-hi); stroke-width: 1.4; stroke-dasharray: 5 4; }
.dv-hd { fill: rgba(139,92,246,.12); stroke: var(--brand-violet); }
.dv-ar { fill: rgba(6,182,212,.09); stroke: var(--border-hi); }
.dv-off { fill: none; stroke: var(--border); stroke-width: 1.2; stroke-dasharray: 4 4; }
.dv-offt { fill: var(--text-muted); font-size: 11.5px; text-anchor: middle; text-decoration: line-through; }
.dv-no path { stroke: var(--brand-gold); stroke-width: 1.6; opacity: .85; }
.dv-line { fill: none; stroke: var(--border-hi); stroke-width: 1.4; }
.dv-dash { stroke-dasharray: 4 4; }
.dv-pk { fill: none; stroke: var(--brand-gold); stroke-width: 2; }
.dv-arrow { fill: none; stroke: var(--brand-cyan); stroke-width: 1.6; stroke-dasharray: 4 4; }
.dv-x { fill: var(--text-muted); font-size: 10px; text-anchor: middle; }
/* ── 三层记忆:用品牌三色 ── */
.layers { display: grid; gap: 0.9rem; }
.layer {
display: grid;
grid-template-columns: 4px 1fr;
gap: 1.1rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: border-color var(--dur) var(--ease);
}
.layer:hover { border-color: var(--border); }
.layer .bar { border-radius: 0; }
.layer .body { padding: 1.05rem 1.2rem 1.05rem 0; }
.layer h3 { margin: 0 0 0.35rem; font-size: 1.02rem; }
.layer h3 .en { color: var(--text-muted); font-weight: 400; font-size: 0.85rem; margin-left: 0.4rem; }
.layer p { margin: 0; color: var(--text-dim); font-size: 0.92rem; }
.l-ctx .bar { background: var(--brand-blue); }
.l-doc .bar { background: var(--brand-cyan); }
.l-gph .bar { background: var(--brand-gold); }
.l-ctx h3 span.mark { color: var(--brand-blue); }
.l-doc h3 span.mark { color: var(--brand-cyan); }
.l-gph h3 span.mark { color: var(--brand-gold); }
/* ── 消息流程图 ──
不用 ASCII那只能表达"上下关系",工具循环与分支都画不出来。
语义化结构 + CSS深浅主题跟着走窄屏也能重排。 */
.flow {
position: relative;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 1.6rem 1.5rem 1.3rem;
}
/* 注:这里原来有一条贯穿全图的 ::before 轴线。
但节点背景是半透明令牌(--bg-card-hi轴线会直接透出来——
实测在「处理管道」里穿过整个编号列表,看着像画错了。
连接线 .lnk 本身就是那条主轴,不需要再来一条。 */
/* ── 节点 ── */
.n {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: 1.9rem auto;
grid-template-areas: "ico t" "ico s";
align-items: center;
gap: 0 0.7rem;
width: 100%;
max-width: 34rem;
margin: 0 auto;
padding: 0.6rem 1.1rem 0.6rem 0.75rem;
background: var(--bg-card-hi);
border: 1px solid var(--border-hi);
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
}
.n-ico {
grid-area: ico;
display: grid;
place-items: center;
width: 1.9rem; height: 1.9rem;
border-radius: 50%;
font-size: 0.85rem;
background: rgba(59, 130, 246, 0.14);
color: var(--brand-blue);
}
.n-t {
grid-area: t;
font-weight: 700;
font-size: 0.95rem;
color: var(--text);
line-height: 1.35;
}
.n-s {
grid-area: s;
font-size: 0.78rem;
color: var(--text-muted);
font-family: var(--font-mono);
line-height: 1.45;
}
/* 各类节点用颜色区分:与三层记忆的三色一致 */
.n-in .n-ico { background: rgba(119, 128, 154, 0.16); color: var(--text-dim); }
.n-sched .n-ico { background: rgba(56, 189, 248, 0.16); color: var(--brand-sky); }
.n-pipe { border-color: rgba(6, 182, 212, 0.45); }
.n-pipe .n-ico { background: rgba(6, 182, 212, 0.16); color: var(--brand-cyan); }
.n-mem { border-color: rgba(245, 158, 11, 0.45); }
.n-mem .n-ico { background: rgba(245, 158, 11, 0.16); color: var(--brand-gold); }
.n-out .n-ico { background: rgba(59, 130, 246, 0.16); color: var(--brand-blue); }
/* ── 连接线:中轴上的一小段,带流动感 ── */
.lnk {
position: relative;
height: 1.15rem;
width: 2px;
margin: 0 auto;
background: var(--border-hi);
}
.lnk::before {
/* 静态箭头reduce 下动画停了,方向也要看得出来 */
content: "";
position: absolute;
left: 50%; bottom: -1px;
width: 7px; height: 7px;
margin-left: -3.5px;
border-right: 2px solid var(--border-hi);
border-bottom: 2px solid var(--border-hi);
transform: rotate(45deg);
}
.lnk::after {
/* 下行的光点 */
content: "";
position: absolute;
left: 50%; top: 0;
width: 6px; height: 6px;
margin-left: -3px;
border-radius: 50%;
background: var(--brand-sky);
box-shadow: 0 0 8px var(--brand-sky);
animation: dotDown 2.4s var(--ease) infinite;
animation-delay: var(--d, 0s);
}
@keyframes dotDown {
0% { top: 0; opacity: 0; }
18% { opacity: 1; }
82% { opacity: 1; }
100% { top: 100%; opacity: 0; }
}
/* ── 调度分支 ── */
.br {
position: relative;
z-index: 1;
list-style: none;
max-width: 34rem;
margin: 0.55rem auto 0;
padding: 0;
display: grid;
gap: 0.28rem;
}
.br li {
position: relative;
font-size: 0.78rem;
color: var(--text-dim);
padding: 0.3rem 0.7rem 0.3rem 1.6rem;
border-left: 2px solid var(--border);
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
background: rgba(119, 128, 154, 0.06);
}
.br li::before {
content: "└";
position: absolute;
left: 0.42rem;
color: var(--text-muted);
font-family: var(--font-mono);
}
.br li b { color: var(--text); font-weight: 600; }
.br li i { font-style: normal; font-weight: 600; }
.br-que i, .br-que b { color: var(--text-dim); }
.br-pre { border-left-color: rgba(245, 158, 11, 0.55); }
.br-pre i { color: var(--brand-gold); }
.br-off { border-left-color: rgba(139, 92, 246, 0.55); }
.br-off i { color: var(--brand-violet); }
/* ── 工具循环 ── */
.loop {
position: relative;
z-index: 1;
max-width: 34rem;
margin: 0.5rem auto 0;
padding: 1.7rem 1rem 1.9rem;
border: 1px dashed rgba(6, 182, 212, 0.4);
border-radius: var(--radius-md);
background: rgba(6, 182, 212, 0.045);
}
.loop-tag {
position: absolute;
top: -0.68rem; left: 1rem;
padding: 0.12rem 0.55rem;
font-size: 0.7rem;
font-family: var(--font-mono);
color: var(--brand-cyan);
background: var(--bg);
border: 1px solid rgba(6, 182, 212, 0.4);
border-radius: var(--radius-pill);
}
.seq {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.34rem;
}
.seq-item {
display: grid;
grid-template-columns: 1.5rem 1fr;
gap: 0.6rem;
align-items: baseline;
}
.seq-n {
display: grid;
place-items: center;
width: 1.5rem; height: 1.5rem;
border-radius: 50%;
font-size: 0.7rem;
font-family: var(--font-mono);
color: var(--brand-cyan);
background: rgba(6, 182, 212, 0.14);
border: 1px solid rgba(6, 182, 212, 0.3);
}
.seq-b { font-size: 0.82rem; color: var(--text); line-height: 1.5; }
.seq-b b { font-weight: 600; }
.seq-b code { font-size: 0.78rem; }
.seq-b em {
display: block;
font-style: normal;
font-size: 0.74rem;
color: var(--text-muted);
}
/* 循环回边:内嵌徽标。
曾试过从框底绕出去的弧线,但它会压到下一条连接线,看着像画错了。 */
.loop-back {
position: absolute;
right: 0.7rem; bottom: 0.7rem;
display: flex;
align-items: center;
gap: 0.34rem;
padding: 0.2rem 0.6rem;
font-size: 0.7rem;
font-family: var(--font-mono);
color: var(--brand-cyan);
background: rgba(6, 182, 212, 0.12);
border: 1px solid rgba(6, 182, 212, 0.35);
border-radius: var(--radius-pill);
white-space: nowrap;
pointer-events: none;
}
.loop-back::before {
content: "↻";
font-size: 0.85rem;
line-height: 1;
}
.flow-note {
position: relative;
z-index: 1;
margin: 1.1rem auto 0;
max-width: 34rem;
text-align: center;
font-size: 0.76rem;
color: var(--text-muted);
line-height: 1.6;
}
.flow-note code { font-size: 0.74rem; }
/* 窄屏:中轴线收边,节点撑满 */
@media (max-width: 620px) {
.flow { padding: 1.2rem 1rem 1.1rem; }
.flow::before { display: none; }
.n { width: auto; max-width: none; }
.br, .flow-note { max-width: none; }
.loop { max-width: none; padding: 2.2rem 0.8rem 2.4rem; }
.lnk { height: 1.6rem; }
}
/* ── 代码块 ── */
.code {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 1rem 1.15rem;
font-family: var(--font-mono);
font-size: 0.86rem;
color: var(--code-fg);
overflow-x: auto;
line-height: 1.7;
/* ❗必须显式声明HTML 源码里的换行默认会被折叠成一个空格,
实测不写这行整段命令会挤成一行“make build build-cli # 启动…
./build/homed” 全连在一起)。 */
white-space: pre;
}
.code .c { color: var(--text-muted); }
.code .s { color: #7cb342; }
:root:not([data-theme="light"]) .code .s { color: #c3e88d; }
/* ── 数字 ── */
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
@media (max-width: 860px) { .stats { grid-template-columns: repeat(2, 1fr); } }
.stat {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 1.2rem 1rem;
text-align: center;
}
.stat .n {
font-size: 1.7rem;
font-weight: 700;
letter-spacing: -0.02em;
background: linear-gradient(120deg, var(--brand-blue), var(--brand-cyan));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.stat .l { color: var(--text-muted); font-size: 0.86rem; }
/* ── 插件徽章 ── */
.badges { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.badge {
font-family: var(--font-mono);
font-size: 0.8rem;
padding: 0.32rem 0.65rem;
border-radius: var(--radius-pill);
background: var(--hover);
border: 1px solid var(--border);
color: var(--text-dim);
transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}
.badge:hover { border-color: rgba(6, 182, 212, 0.45); color: var(--text); }
/* ── 组件角色 ── */
.comp { display: grid; gap: 0.7rem; }
.comp .row {
display: grid;
grid-template-columns: minmax(9rem, auto) 1fr;
gap: 1rem;
padding: 0.75rem 1rem;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: 0.9rem;
align-items: baseline;
}
.comp .row code {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--brand-sky);
}
.comp .row span { color: var(--text-dim); }
@media (max-width: 760px) {
.comp .row { grid-template-columns: 1fr; gap: 0.25rem; }
}
/* ── FAQ ── */
details.faq {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 0.95rem 1.15rem;
margin-bottom: 0.7rem;
}
details.faq summary {
cursor: pointer;
font-weight: 600;
font-size: 0.96rem;
list-style: none;
}
details.faq summary::-webkit-details-marker { display: none; }
details.faq summary::before { content: " "; color: var(--brand-cyan); }
details.faq[open] summary::before { content: " "; }
details.faq p {
margin: 0.7rem 0 0;
color: var(--text-dim);
font-size: 0.92rem;
}
/* ── 页脚 ── */
footer {
border-top: 1px solid var(--border);
padding: 2.4rem 0 3rem;
color: var(--text-muted);
font-size: 0.88rem;
}
footer .cols {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.6rem;
margin-bottom: 1.8rem;
}
@media (max-width: 860px) { footer .cols { grid-template-columns: repeat(2, 1fr); } }
footer h4 { color: var(--text); font-size: 0.9rem; margin: 0 0 0.6rem; }
footer ul { list-style: none; padding: 0; margin: 0; }
footer li { margin-bottom: 0.35rem; }
footer a { color: var(--text-dim); text-decoration: none; }
footer a:hover { color: var(--brand-cyan); }
footer .note {
border-top: 1px solid var(--border);
padding-top: 1.3rem;
line-height: 1.7;
}
a.plain { color: var(--brand-sky); text-decoration: none; }
a.plain:hover { text-decoration: underline; }
.mono { font-family: var(--font-mono); }
.muted { color: var(--text-muted); }
/* 滚入淡入:**默认可见**JS 就绪后才接管。
为什么这样写:若默认 opacity:0则 JS 未执行/抛错/被禁用时整页内容
永久不可见实测30 个 .reveal 里 26 个停在 opacity:0
所以动画必须由 JS 主动加 .js-reveal 才启用 —— 渐进增强,不是降级。 */
.js-reveal {
opacity: 0;
transform: translateY(14px);
transition: opacity 600ms var(--ease), transform 600ms var(--ease);
}
.js-reveal.in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
.js-reveal { opacity: 1; transform: none; transition: none; }
}
/* ── 插件:徽章可点,下方显示详情 ── */
.badges {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
.badge {
/* 重置 button 默认样式 */
appearance: none;
font: inherit;
font-family: var(--font-mono);
font-size: 0.8rem;
padding: 0.34rem 0.7rem;
border-radius: var(--radius-pill);
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-dim);
cursor: pointer;
transition: border-color 0.22s var(--ease), color 0.22s var(--ease),
background 0.22s var(--ease), transform 0.22s var(--ease-back),
box-shadow 0.22s var(--ease);
}
.badge:hover {
border-color: var(--border-hi);
color: var(--text);
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.badge:focus-visible {
outline: 2px solid var(--brand-sky);
outline-offset: 2px;
}
/* 选中态:淡底 + 主色描边实心渐变太重20 个齐亮像一堵墙) */
.badge[aria-pressed="true"] {
/* 回退:不支持 color-mix 的浏览器给淡青底 */
background: rgba(6, 182, 212, 0.14);
background: color-mix(in srgb, var(--brand-cyan) 12%, var(--bg-card));
border-color: var(--brand-cyan);
color: var(--text);
}
/* 小圆点提示可点 */
.badge::after {
content: "";
display: inline-block;
width: 4px; height: 4px;
margin-left: 0.4rem;
border-radius: 50%;
background: currentColor;
opacity: 0.45;
vertical-align: middle;
transition: opacity 0.22s var(--ease);
}
.badge[aria-pressed="true"]::after { opacity: 1; }
/* 「展开全部」:虚线边框,与具体插件徽章区分 */
.badge.muted {
font-style: italic;
color: var(--text-muted);
border-style: dashed;
}
.badge.muted::after { display: none; }
.pdetail {
margin-top: 1.2rem;
display: grid;
gap: 0.85rem;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.phint {
grid-column: 1 / -1;
margin: 0;
text-align: center;
color: var(--text-muted);
font-size: 0.88rem;
padding: 0.85rem 0;
border: 1px dashed var(--border);
border-radius: var(--radius-md);
}
.pcard {
display: none;
position: relative;
background: var(--bg-card);
border: 1px solid var(--border-hi);
border-radius: var(--radius-lg);
padding: 1rem 1.15rem;
box-shadow: var(--shadow-md);
}
.pcard.show {
display: block;
animation: pcardIn 0.4s var(--ease-back);
}
@keyframes pcardIn {
from { opacity: 0; transform: translateY(10px) scale(0.98); }
to { opacity: 1; transform: none; }
}
.pcard-head {
display: flex;
align-items: baseline;
gap: 0.6rem;
margin-bottom: 0.5rem;
}
.pcard-head code {
font-size: 0.95rem;
font-weight: 700;
color: var(--brand-cyan);
}
.pcard-head .ver {
font-size: 0.75rem;
color: var(--text-muted);
font-family: var(--font-mono);
margin-left: auto;
}
.pcard p {
margin: 0;
font-size: 0.88rem;
color: var(--text-dim);
line-height: 1.65;
}
/* 源码链接:卡片底部一行,点进对应源码仓路径 */
.psrc {
display: flex;
align-items: center;
gap: 0.4rem;
margin-top: 0.75rem;
padding-top: 0.65rem;
border-top: 1px dashed var(--border);
font-size: 0.76rem;
font-family: var(--font-mono);
color: var(--text-muted);
text-decoration: none;
transition: color 0.2s var(--ease);
}
a.psrc:hover { color: var(--brand-cyan); }
a.psrc:focus-visible {
outline: 2px solid var(--brand-sky);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
.psrc-r {
flex: 0 0 auto;
padding: 0.06rem 0.4rem;
border-radius: var(--radius-sm);
font-size: 0.7rem;
background: rgba(119, 128, 154, 0.16);
color: var(--text-dim);
}
.psrc-p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* editdoc卡片写 v2.0.0,公开仓只有 v1.0.0 —— 显式标出,不静默指过去 */
.psrc-warn .psrc-r { background: rgba(245, 158, 11, 0.18); color: var(--brand-gold); }
.psrc-note {
margin: 0.4rem 0 0;
font-size: 0.72rem;
line-height: 1.55;
color: var(--brand-gold);
}
.psrc-out {
margin-left: auto;
opacity: 0.55;
transition: transform 0.2s var(--ease), opacity 0.2s var(--ease);
}
a.psrc:hover .psrc-out { opacity: 1; transform: translate(2px, -2px); }
/* 无公开地址:不可点,视觉上也要看得出来 */
.psrc-none {
cursor: default;
opacity: 0.75;
font-style: italic;
}
/* 全部展开时的网格 */
.pdetail.all {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.pdetail.all .pcard { display: block; }
.pdetail.all .phint { display: none; }
/* ── 统计卡:补一行小字,避免数字孤零零 ── */
.stat .sm {
display: block;
margin-top: 0.25rem;
font-size: 0.72rem;
color: var(--text-muted);
letter-spacing: 0.01em;
}
.stat .l { line-height: 1.5; }
/* ── 版面精致化:统一节奏与呼吸 ── */
section { padding: 4.5rem 0; }
@media (max-width: 860px) { section { padding: 3rem 0; } }
h2 { margin: 0 0 0.7rem; letter-spacing: -0.01em; }
.sub { max-width: 44rem; }
/* 卡片内边距与标题间距收敛一档 */
.card { padding: 1.4rem 1.5rem; }
.card h3 { margin: 0 0 0.55rem; }
.card p { font-size: 0.9rem; }
/* 组件表:行高收敛、代码列定宽对齐 */
.comp .row { padding: 0.65rem 1rem; }
.comp .row code { min-width: 7.5rem; }
/* 统计卡内边距 */
.stat { padding: 1.25rem 1rem; }
.stat .n { margin-bottom: 0.15rem; }
/* 三层记忆卡收紧 */
.layer .body { padding: 1rem 1.2rem 1rem 0; }
.layer h3 { margin-bottom: 0.3rem; }
.layer p { margin: 0; }
/* 分隔:章节之间给一条极淡的线,强化节奏 */
section + section > .wrap > h2,
section + section > .wrap > .sub {
/* 占位:保持选择器有效,视觉由间距承担 */
}
/* 备案号:公开站点合规要求,需始终可见 */
.beian {
margin: 1rem 0 0;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
font-size: 0.78rem;
color: var(--text-muted);
}
.beian a {
color: var(--text-muted);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.beian a:hover {
color: var(--text-dim);
border-bottom-color: var(--border-hi);
}
.beian:focus-within a { outline: none; }
.beian a:focus-visible {
outline: 2px solid var(--brand-sky);
outline-offset: 2px;
border-radius: 2px;
}
.beian-sep { opacity: 0.45; }
/* 打印:不显示装饰与折叠 */
@media print {
.aurora, #fx, #progress, .badges, .pdetail { display: none !important; }
body { background: #fff; color: #000; }
}
/* ═══════════════════════════════════════════════════
动效层。全部遵守 prefers-reduced-motion见文件末脚本
原则:**内容默认可读**动效只是增强JS 挂了也不影响阅读。
═══════════════════════════════════════════════════ */
/* ── 极光背景:四团光斑各自漂移 ── */
.aurora {
position: fixed;
inset: -20%;
z-index: -2;
pointer-events: none;
filter: blur(38px);
opacity: 0.9;
}
.aurora span {
position: absolute;
display: block;
border-radius: 50%;
will-change: transform;
}
.aurora .a1 {
width: 46vw; height: 46vw; left: 2%; top: -6%;
background: radial-gradient(circle at 35% 35%, var(--aurora-a), transparent 68%);
animation: drift1 26s ease-in-out infinite;
}
.aurora .a2 {
width: 40vw; height: 40vw; right: -4%; top: 4%;
background: radial-gradient(circle at 60% 40%, var(--aurora-b), transparent 68%);
animation: drift2 32s ease-in-out infinite;
}
.aurora .a3 {
width: 44vw; height: 44vw; left: 22%; bottom: -12%;
background: radial-gradient(circle at 50% 50%, var(--aurora-c), transparent 68%);
animation: drift3 38s ease-in-out infinite;
}
.aurora .a4 {
width: 30vw; height: 30vw; right: 14%; bottom: 6%;
background: radial-gradient(circle at 50% 50%, var(--aurora-d), transparent 68%);
animation: drift4 30s ease-in-out infinite;
}
@keyframes drift1 {
0%,100% { transform: translate3d(0,0,0) scale(1); }
33% { transform: translate3d(6vw,4vh,0) scale(1.12); }
66% { transform: translate3d(-3vw,7vh,0) scale(0.94); }
}
@keyframes drift2 {
0%,100% { transform: translate3d(0,0,0) scale(1); }
40% { transform: translate3d(-7vw,5vh,0) scale(1.1); }
70% { transform: translate3d(-2vw,-4vh,0) scale(0.92); }
}
@keyframes drift3 {
0%,100% { transform: translate3d(0,0,0) scale(1); }
50% { transform: translate3d(5vw,-6vh,0) scale(1.15); }
}
@keyframes drift4 {
0%,100% { transform: translate3d(0,0,0) scale(1); }
45% { transform: translate3d(-5vw,-3vh,0) scale(1.08); }
}
/* ── 细网格:给「工程感」打底 ── */
body::after {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: linear-gradient(var(--grid-line) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 25%, transparent 78%);
-webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 25%, transparent 78%);
}
/* ── 粒子画布 ── */
#fx {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
opacity: 0.75;
}
/* ── 顶部滚动进度条(三色渐变) ── */
#progress {
position: fixed;
top: 0; left: 0;
height: 2px;
width: 0%;
z-index: 100;
background: linear-gradient(90deg, var(--brand-blue), var(--brand-cyan) 55%, var(--brand-gold));
box-shadow: 0 0 12px rgba(56, 189, 248, 0.55);
transition: width 90ms linear;
}
/* ── 主题切换按钮 ── */
.theme-btn {
display: inline-grid;
place-items: center;
width: 34px; height: 34px;
border-radius: var(--radius-pill);
border: 1px solid var(--border);
background: var(--bg-soft);
color: var(--text-dim);
cursor: pointer;
padding: 0;
transition: color var(--dur) var(--ease), border-color var(--dur) var(--ease),
transform var(--dur) var(--ease), background var(--dur) var(--ease);
}
.theme-btn:hover {
color: var(--text);
border-color: var(--border-hi);
transform: rotate(-18deg) scale(1.06);
}
.theme-btn svg { width: 16px; height: 16px; display: block; }
.theme-btn .i-moon { display: none; }
:root[data-theme="light"] .theme-btn .i-moon { display: block; }
:root[data-theme="light"] .theme-btn .i-sun { display: none; }
/* ── 标题与文字的入场:分块上浮 ── */
.js-fx .rise {
opacity: 0;
transform: translateY(22px);
transition: opacity 0.75s var(--ease), transform 0.75s var(--ease);
transition-delay: var(--d, 0ms);
}
.js-fx .rise.in { opacity: 1; transform: none; }
.js-fx .rise-zoom {
opacity: 0; transform: scale(0.93) translateY(14px);
transition: opacity 0.8s var(--ease), transform 0.8s var(--ease-back);
transition-delay: var(--d, 0ms);
}
.js-fx .rise-zoom.in { opacity: 1; transform: none; }
/* ── Hero 大标题:渐变流动 + 描边光 ── */
h1 .grad {
background: linear-gradient(110deg, var(--brand-blue), var(--brand-cyan) 32%,
var(--brand-violet) 58%, var(--brand-gold) 82%, var(--brand-cyan));
background-size: 280% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: gradFlow 9s ease infinite;
}
@keyframes gradFlow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* 光标扫过标题时的光泽 */
h1 {
position: relative;
text-shadow: 0 0 40px rgba(56, 189, 248, 0.10);
}
/* ── 按钮:光泽扫过 + 悬浮抬升 ── */
.btn { position: relative; overflow: hidden; }
.btn::after {
content: "";
position: absolute;
top: 0; left: -120%;
width: 60%; height: 100%;
background: linear-gradient(100deg, transparent, var(--sheen), transparent);
transform: skewX(-18deg);
transition: left 0.75s var(--ease);
pointer-events: none;
}
.btn:hover::after { left: 130%; }
.btn-primary {
background-size: 180% 100%;
animation: gradFlow 7s ease infinite;
}
.btn-primary:hover { box-shadow: 0 10px 30px rgba(6, 182, 212, 0.42); }
/* ── 卡片:跟随光标的聚光 + 3D 微倾 ── */
.card, .layer, .stat, .comp .row, details.faq {
position: relative;
transform-style: preserve-3d;
}
.card::before, .stat::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
background: radial-gradient(
260px circle at var(--mx, 50%) var(--my, 50%),
var(--spot), transparent 62%);
opacity: 0;
transition: opacity var(--dur) var(--ease);
pointer-events: none;
}
.card:hover::before, .stat:hover::before { opacity: 1; }
.card:hover, .stat:hover {
border-color: var(--border-hi);
box-shadow: var(--shadow-md);
}
/* 图标在悬浮时轻转 */
.card .icon { transition: transform 0.5s var(--ease-back); }
.card:hover .icon { transform: rotate(-8deg) scale(1.12); }
/* ── 三层记忆色条:入场时自上而下「灌注」 ── */
.layer .bar {
transform: scaleY(0);
transform-origin: top center;
transition: transform 0.9s var(--ease) 0.15s;
box-shadow: 0 0 18px currentColor;
}
.layer.in .bar { transform: scaleY(1); }
.layer.l-ctx .bar { color: var(--brand-blue); }
.layer.l-doc .bar { color: var(--brand-cyan); }
.layer.l-gph .bar { color: var(--brand-gold); }
/* ── 流程图的「数据流动」──
光带扫描会盖住节点,改成让节点自己的图标呼吸。 */
.js-fx .flow .n-ico {
animation: nodePulse 4.5s ease-in-out infinite;
}
.js-fx .flow .n:nth-of-type(1) .n-ico { animation-delay: 0s; }
.js-fx .flow .n .n-ico { animation-delay: 0.9s; }
.js-fx .flow .n-mem .n-ico { animation-delay: 1.8s; }
@keyframes nodePulse {
0%, 100% { box-shadow: 0 0 0 0 transparent; }
50% { box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.10); }
}
/* 序号依次点亮,跟着流程走 */
.js-fx .flow .seq-n {
animation: seqPulse 4.5s ease-in-out infinite;
}
.js-fx .flow .seq-item:nth-child(1) .seq-n { animation-delay: 0s; }
.js-fx .flow .seq-item:nth-child(2) .seq-n { animation-delay: 0.35s; }
.js-fx .flow .seq-item:nth-child(3) .seq-n { animation-delay: 0.7s; }
.js-fx .flow .seq-item:nth-child(4) .seq-n { animation-delay: 1.05s; }
.js-fx .flow .seq-item:nth-child(5) .seq-n { animation-delay: 1.4s; }
.js-fx .flow .seq-item:nth-child(6) .seq-n { animation-delay: 1.75s; }
@keyframes seqPulse {
0%, 70%, 100% { border-color: rgba(6, 182, 212, 0.3); color: var(--brand-cyan); }
12% { border-color: var(--brand-cyan); color: var(--text); }
}
/* ── 数字卡片:数值滚动到位 ── */
.stat .n { font-variant-numeric: tabular-nums; }
/* ── 插件徽章:错落浮现 ── */
.js-fx .badge {
opacity: 0;
transform: translateY(10px) scale(0.94);
transition: opacity 0.5s var(--ease), transform 0.5s var(--ease-back);
}
.js-fx .badges.in .badge { opacity: 1; transform: none; }
/* ── 分节标题:渐变下划线展开 ── */
section > .wrap > h2 { position: relative; }
section > .wrap > h2::after {
content: "";
display: block;
width: 62px;
height: 3px;
margin-top: 0.6rem;
border-radius: 3px;
background: linear-gradient(90deg, var(--brand-blue), var(--brand-cyan), var(--brand-gold));
transform: scaleX(0);
transform-origin: left center;
transition: transform 0.9s var(--ease) 0.2s;
}
section > .wrap > h2.in::after { transform: scaleX(1); }
/* ── FAQ 展开:内容淡入 ── */
details.faq[open] p { animation: fadeUp 0.45s var(--ease); }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: none; }
}
/* ── 导航链接:下划线滑入 ── */
nav a.link { position: relative; }
nav a.link::after {
content: "";
position: absolute;
left: 0; bottom: -2px;
width: 100%; height: 2px;
background: linear-gradient(90deg, var(--brand-blue), var(--brand-cyan));
transform: scaleX(0);
transform-origin: right center;
transition: transform var(--dur) var(--ease);
}
nav a.link:hover::after { transform: scaleX(1); transform-origin: left center; }
/* ── 浅色下的微调:光晕别太亮、网格别太淡 ── */
:root[data-theme="light"] .aurora { opacity: 0.62; }
:root[data-theme="light"] #fx { opacity: 0.5; }
:root[data-theme="light"] nav .en,
:root[data-theme="light"] nav a.link { color: var(--text-dim); }
/* 浅色下代码块的光条更淡 */
:root[data-theme="light"] .flow::after {
background: linear-gradient(to bottom, transparent,
rgba(37, 99, 235, 0.07) 45%, rgba(37, 99, 235, 0.12) 50%, transparent);
}
/* ── 统一降级:减少动态效果时全部静止 ── */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
.js-fx .rise, .js-fx .rise-zoom, .js-fx .badge { opacity: 1; transform: none; }
.js-fx .flow .n-ico, .js-fx .flow .seq-n, .lnk::after { animation: none; }
.lnk::after { opacity: 0; }
.layer .bar { transform: scaleY(1); }
section > .wrap > h2::after { transform: scaleX(1); }
#progress { display: none; }
#fx { display: none; }
}
</style>
</head>
<body>
<div id="progress"></div>
<div class="aurora" aria-hidden="true">
<span class="a1"></span><span class="a2"></span>
<span class="a3"></span><span class="a4"></span>
</div>
<canvas id="fx" aria-hidden="true"></canvas>
<header>
<div class="wrap">
<nav>
<span class="brand">
<img src="assets/logo.svg" alt="HomeAgent logo" />
HomeAgent
</span>
<a class="link hide-sm" href="#features">特性</a>
<a class="link hide-sm" href="#memory">记忆</a>
<a class="link hide-sm" href="#architecture">架构</a>
<a class="link hide-sm" href="#quickstart">上手</a>
<a class="link" href="#faq">FAQ</a>
<button class="theme-btn" id="themeToggle" type="button"
aria-label="切换深浅主题" title="切换深浅主题">
<svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4.2"/>
<path d="M12 2.5v2.2M12 19.3v2.2M4.2 4.2l1.6 1.6M18.2 18.2l1.6 1.6M2.5 12h2.2M19.3 12h2.2M4.2 19.8l1.6-1.6M18.2 5.8l1.6-1.6"/>
</svg>
<svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
</svg>
</button>
</nav>
</div>
</header>
<main>
<!-- ── Hero ── -->
<section class="hero">
<div class="wrap hero-grid">
<div class="hero-copy">
<span class="tagline rise" style="--d:0ms">常驻型个人 Agent 框架</span>
<h1 class="rise" style="--d:80ms"><span class="grad">记得住</span>的管家<br />更是从不让你干等的搭档</h1>
<p class="lede rise" style="--d:180ms">
内核只管<strong>编排、记忆与调度</strong>,消息 / 文件 / 网络 / 设备
一律交给插件。插件崩了不牵连内核 —— 换掉一个二进制就热重载。
</p>
<div class="cta rise" style="--d:280ms">
<a class="btn btn-primary" href="#quickstart">快速上手 →</a>
<a class="btn btn-ghost" href="https://gitcode.com/JianFeeeee/HomeAgent">查看源码</a>
</div>
</div>
</div>
</section>
<!-- ── 特性 ── -->
<!-- ── 特性总起 ── -->
<section id="features">
<div class="wrap">
<h2 class="reveal rise">真正的 AgentOS长这样</h2>
<p class="sub reveal rise" style="--d:90ms">
隔离、调度、通信、资源。操作系统躲不开的四道题,
这里逐题给答案 —— 且每题都有能测的数字。
</p>
<div class="duties reveal rise" style="--d:180ms">
<article class="duty">
<span class="duty-k">隔离</span>
<h3>崩一个,不赔上全部</h3>
<p>插件不在内核里,是另一个进程。崩了就把它注册的东西一并摘掉,其余照跑。</p>
<span class="duty-ev">退避重启 1s / 2s / 3s5 分钟内崩 3 次则停下等人</span>
</article>
<article class="duty">
<span class="duty-k">调度</span>
<h3>拦得住,也接得回</h3>
<p>输入分能等与不能等。不能等的可抢占,被打断的现场压栈,稍后<strong>原样恢复</strong></p>
<span class="duty-ev">L4 内核独占;外部插件声明 L4 会被夹到 L3</span>
</article>
<article class="duty">
<span class="duty-k">通信</span>
<h3>三条道,各走各的</h3>
<p>控制走 stdio数据走共享内存通知走事件环。变长内容只传偏移两边映射到不同地址也能读。</p>
<span class="duty-ev">整段写入 + 读回 3.5µs实测</span>
</article>
<article class="duty">
<span class="duty-k">资源</span>
<h3>记忆不会越用越肿</h3>
<p>上下文按相关性取舍,最近 10 条恒受保护。低分下沉成文档,冷文档再蒸馏成三元组。</p>
<span class="duty-ev">有效工作区间上限 60 万 token</span>
</article>
</div>
</div>
</section>
<!-- ── 设计决定 01 ── -->
<section id="zero-io">
<div class="wrap">
<!-- ① 文左 图右 -->
<div class="alt reveal">
<div class="alt-copy">
<span class="alt-n">01</span>
<h3>内核零 IO只碰调度</h3>
<p>
内核不读文件、不发网络、不碰硬件。它只做三件事:
<strong>编排、记忆、调度</strong>。外面的一切 —— 消息、文件、网络、设备 ——
都由插件接管。于是「内核可信吗」和「这个插件安全吗」
成了两个可以分开回答的问题,而不是一次押上整块。
</p>
</div>
<div class="alt-visual">
<svg viewBox="0 0 360 250" role="img" aria-label="内核不直接连接外部世界,由插件桥接">
<rect class="dv-bound" x="16" y="40" width="328" height="170" rx="14" />
<text class="dv-cap" x="30" y="232">外部世界</text>
<rect class="dv-box dv-accent" x="120" y="100" width="120" height="60" rx="10" />
<text class="dv-t" x="180" y="126">内核</text>
<text class="dv-s" x="180" y="144">编排 · 记忆 · 调度</text>
<g class="dv-plug">
<rect class="dv-box" x="36" y="52" width="76" height="38" rx="8" />
<text class="dv-ts" x="74" y="76">插件</text>
<rect class="dv-box" x="248" y="52" width="76" height="38" rx="8" />
<text class="dv-ts" x="286" y="76">插件</text>
<rect class="dv-box" x="36" y="160" width="76" height="38" rx="8" />
<text class="dv-ts" x="74" y="184">插件</text>
<rect class="dv-box" x="248" y="160" width="76" height="38" rx="8" />
<text class="dv-ts" x="286" y="184">插件</text>
</g>
<path class="dv-line" d="M74 90 V160" />
<path class="dv-line" d="M286 90 V160" />
<path class="dv-pk" d="M112 130 H74" />
<path class="dv-pk" d="M248 130 H286" />
<path class="dv-line dv-dash" d="M112 130 H160 M200 130 H248" />
</svg>
<p class="dv-note">箭头只进出插件 —— 内核一列都没有</p>
</div>
</div>
</div>
</section>
<!-- ── 设计决定 02 ── -->
<section id="zero-dep">
<div class="wrap">
<!-- ② 文右 图左 -->
<div class="alt flip reveal">
<div class="alt-copy">
<span class="alt-n">02</span>
<h3>部署,从未如此便捷</h3>
<p>
插件是<strong>静态编译的单个文件</strong>:目标机上没有 Go、没有 libc、
没有容器、没有运行时,拷过去就能跑。
本体同样不要求你先装什么 —— 没有数据库、没有缓存、没有消息队列,
记忆与索引都在进程内。所谓部署,就是把文件放上去。
</p>
</div>
<div class="alt-visual">
<svg viewBox="0 0 360 250" role="img" aria-label="插件是单个静态二进制,不依赖任何运行时">
<rect class="dv-bound" x="20" y="34" width="140" height="182" rx="12" />
<text class="dv-cap" x="30" y="228">目标机</text>
<rect class="dv-box dv-accent" x="40" y="96" width="100" height="60" rx="10" />
<text class="dv-ts" x="90" y="122">plugin.bin</text>
<text class="dv-s" x="90" y="140">单个文件</text>
<g class="dv-plug">
<rect class="dv-box dv-off" x="196" y="52" width="140" height="30" rx="7" />
<text class="dv-offt" x="266" y="72">Go 工具链</text>
<rect class="dv-box dv-off" x="196" y="92" width="140" height="30" rx="7" />
<text class="dv-offt" x="266" y="112">libc / glibc</text>
<rect class="dv-box dv-off" x="196" y="132" width="140" height="30" rx="7" />
<text class="dv-offt" x="266" y="152">Node / Python</text>
<rect class="dv-box dv-off" x="196" y="172" width="140" height="30" rx="7" />
<text class="dv-offt" x="266" y="192">容器运行时</text>
</g>
<g class="dv-no">
<path d="M200 58 L332 76" /><path d="M200 98 L332 116" />
<path d="M200 138 L332 156" /><path d="M200 178 L332 196" />
</g>
<text class="dv-x" x="266" y="42">都不需要</text>
</svg>
<p class="dv-note">实测statically linked无动态依赖</p>
</div>
</div>
</div>
</section>
<!-- ── 设计决定 03 ── -->
<section id="shm">
<div class="wrap">
<!-- ③ 文左 图右:共享内存 -->
<div class="alt reveal">
<div class="alt-copy">
<span class="alt-n">03</span>
<h3>数据如水,随流,随改,随走</h3>
<p>
控制信令走 stdio<strong>真正的数据走一块共享内存</strong>
变长内容只传一个相对偏移,两边 mmap 到不同地址也能各自解引用。
所以上下文在插件里是<strong>零拷贝</strong>改写的 ——
不是「序列化出去、传字符串、再反序列化回来」,而是两边看着同一块内存。
</p>
</div>
<div class="alt-visual">
<svg viewBox="0 0 360 250" role="img" aria-label="共享内存段加偏移解引用,实现零拷贝">
<rect class="dv-box dv-accent" x="24" y="40" width="96" height="56" rx="9" />
<text class="dv-ts" x="72" y="64">内核</text>
<text class="dv-s" x="72" y="82">homed</text>
<rect class="dv-box dv-accent" x="240" y="40" width="96" height="56" rx="9" />
<text class="dv-ts" x="288" y="64">插件</text>
<text class="dv-s" x="288" y="82">plugin.bin</text>
<rect class="dv-seg" x="24" y="126" width="312" height="88" rx="10" />
<text class="dv-cap" x="34" y="146">共享内存段(一块,两边同一物理页)</text>
<rect class="dv-box dv-hd" x="40" y="156" width="60" height="42" rx="6" />
<text class="dv-ts" x="70" y="182">header</text>
<rect class="dv-box dv-ar" x="112" y="156" width="208" height="42" rx="6" />
<text class="dv-ts" x="216" y="181">arena 变长数据</text>
<text class="dv-labs" x="216" y="194">{off, len} 相对基址</text>
<path class="dv-line" d="M72 96 V126" />
<path class="dv-line" d="M288 96 V126" />
<path class="dv-arrow" d="M70 60 L2 60 L2 177 L40 177" />
<text class="dv-x" x="180" y="238">零拷贝:不序列化、不复制</text>
</svg>
<p class="dv-note">实测整段写入+读回 3.5µs</p>
</div>
</div>
</div>
</section>
<!-- ── 设计决定 04 ── -->
<section id="priority">
<div class="wrap">
<!-- ④ 文右 图左 -->
<div class="alt flip reveal">
<div class="alt-copy">
<span class="alt-n">04</span>
<h3>轻重缓急,各归其位</h3>
<p>
输入先分两类:<strong>可以等的</strong>,和<strong>不能等的</strong>
不能等的再按紧迫度排 L1L4高级别可以抢占被抢占的现场压进栈、稍后原样恢复。
同一时刻只有一个任务在改上下文,所以不会两边各做一半。
L4 归内核独占 —— 「停止」按下去就真的停。
</p>
</div>
<div class="alt-visual">
<svg viewBox="0 0 360 250" role="img" aria-label="四级中断与抢占">
<g class="dv-lv">
<rect class="dv-box dv-l4" x="30" y="30" width="200" height="34" rx="7" />
<text class="dv-ts" x="46" y="52">L4 内核独占 · 立即打断</text>
<rect class="dv-box dv-l3" x="30" y="72" width="200" height="34" rx="7" />
<text class="dv-ts" x="46" y="94">L3 高优先级</text>
<rect class="dv-box dv-l2" x="30" y="114" width="200" height="34" rx="7" />
<text class="dv-ts" x="46" y="136">L2 重要</text>
<rect class="dv-box dv-l1" x="30" y="156" width="200" height="34" rx="7" />
<text class="dv-ts" x="46" y="178">L1 可等</text>
</g>
<text class="dv-lab" x="130" y="212">高 → 低</text>
<rect class="dv-box dv-run" x="250" y="72" width="86" height="76" rx="9" />
<text class="dv-ts" x="293" y="104">正在跑</text>
<text class="dv-s" x="293" y="122">现场压栈</text>
<path class="dv-pk" d="M230 47 H293 V72" />
<text class="dv-x" x="293" y="166">抢占 → 稍后恢复</text>
</svg>
</div>
</div>
</div>
</section>
<!-- ── 设计决定 05 ── -->
<section id="offload">
<div class="wrap">
<!-- ④ 文右 图左 -->
<div class="alt flip reveal">
<div class="alt-copy">
<span class="alt-n">05</span>
<h3>忙时,有人替你接住</h3>
<p>
主 agent 被长任务占住、积压又够多时,内核把这些消息转给一个
临时驻留的<strong>分诊助手</strong>。能当场办的就办了;
非等主 agent 不可的,立刻回一句「忙碌中,请稍候」。
你不必对着对话框猜它还在不在。
</p>
</div>
<div class="alt-visual">
<svg viewBox="0 0 360 250" role="img" aria-label="主 agent 忙时把积压转投给分诊助手">
<rect class="dv-box dv-run" x="24" y="36" width="150" height="52" rx="9" />
<text class="dv-ts" x="99" y="58">主 agent</text>
<text class="dv-s" x="99" y="76">长任务 · 占住很久</text>
<g class="dv-msg">
<rect class="dv-box" x="24" y="116" width="150" height="26" rx="6" />
<text class="dv-s" x="99" y="134">积压消息 ×3</text>
<rect class="dv-box" x="24" y="148" width="150" height="26" rx="6" />
<text class="dv-s" x="99" y="166">积压消息 ×4</text>
</g>
<path class="dv-pk" d="M174 165 H228" />
<text class="dv-lab" x="201" y="156">转投</text>
<rect class="dv-box dv-accent" x="228" y="116" width="110" height="76" rx="9" />
<text class="dv-ts" x="283" y="146">分诊助手</text>
<text class="dv-s" x="283" y="166">当场办 / 立刻答</text>
<path class="dv-line" d="M24 214 H338" />
<path class="dv-arrow" d="M338 214 V196" />
<text class="dv-x" x="180" y="236">不必干等</text>
</svg>
</div>
</div>
</div>
</section>
<!-- ── 三层记忆 ── -->
<section id="memory">
<div class="wrap">
<h2 class="reveal rise">三层记忆:从「刚才说了什么」到「三年前说过什么」</h2>
<p class="sub reveal rise" style="--d:90ms">
不是一个大向量库包打天下,而是三层各司其职、逐层下沉。
蓝 / 青 / 金三色对应的就是这三层。
</p>
<div class="layers">
<div class="layer l-ctx reveal">
<div class="bar"></div>
<div class="body">
<h3><span class="mark"></span> Context<span class="en">工作窗口</span></h3>
<p>
内存中的事件窗口。按与当前输入的相关性打分:优先比统一空间的稠密向量余弦,
未配置时退到稀疏词向量TF-IDF / fastText。最近 10 条受保护,
低分事件归档进 Document 层。
</p>
</div>
</div>
<div class="layer l-doc reveal">
<div class="bar"></div>
<div class="body">
<h3><span class="mark"></span> Document<span class="en">文件记忆</span></h3>
<p>
JSON + TF-IDF 索引的中间层。支持显式提交与隐式归档,
冷数据下沉后蒸馏成三元组进入图谱。
</p>
</div>
</div>
<div class="layer l-gph reveal">
<div class="bar"></div>
<div class="body">
<h3><span class="mark"></span> Graph<span class="en">图数据库</span></h3>
<p>
SQLite 持久化实体与关系BFS 遍历召回。三元组从对话中蒸馏而来,
越用越准 —— 「几个月前的事还记得」靠的就是这层。
</p>
</div>
</div>
</div>
<div class="grid-3" style="margin-top: 1.2rem">
<div class="card reveal rise" style="--d:360ms">
<h3>媒体也是一等节点</h3>
<p>
图片、音频不是附件,而是三层里的一类节点:内容寻址(相同字节只存一份)。
媒体不单独记引用账本 —— 删掉持有它的记忆块才删内容,与文本块同一套规则。
</p>
</div>
<div class="card reveal rise" style="--d:450ms">
<h3>图片靠自己的向量被检索</h3>
<p>
图像是真正的记忆块,不是「描述文本 + 可淘汰的 blob」。
旧实现靠视觉模型生成的描述当索引,现已弃用 ——
文本标签只留 <span class="mono">[mime digest]</span>,召回走统一空间的图像向量。
</p>
</div>
<div class="card reveal rise" style="--d:540ms">
<h3>统一多模态空间</h3>
<p>
文本与图像落在同一向量空间(默认 Chinese-CLIP
可以用一句话搜图,也可以用图找记忆。
</p>
</div>
</div>
</div>
</section>
<!-- ── 架构 ── -->
<section id="architecture">
<div class="wrap">
<h2 class="reveal rise">一条消息进来之后</h2>
<p class="sub reveal rise" style="--d:90ms">
输入先过调度器,再进处理管道。全程 7 个阶段钩子,插件可改写或短路。
</p>
<div class="flow reveal rise">
<!-- 输入 -->
<div class="n n-in">
<span class="n-ico"></span>
<span class="n-t">外部输入</span>
<span class="n-s">QQ / WebUI / CLI / 邮件 / 设备</span>
</div>
<div class="lnk" style="--d:0.00s" aria-hidden="true"></div>
<!-- 调度器 -->
<div class="n n-sched">
<span class="n-ico"></span>
<span class="n-t">输入调度器</span>
<span class="n-s">两类别 · 四级中断 L1L4</span>
</div>
<ul class="br" aria-label="调度器的三种去向">
<li class="br-que"><b>同级不抢占</b> → 入队列排队</li>
<li class="br-pre"><b>更高级别</b><i>抢占</i>,现场压入中断栈,稍后恢复</li>
<li class="br-off"><b>忙得太久</b><i>转投</i>临时分诊助手</li>
</ul>
<div class="lnk" style="--d:0.55s" aria-hidden="true"></div>
<!-- 处理管道 -->
<div class="n n-pipe">
<span class="n-ico"></span>
<span class="n-t">处理管道</span>
<span class="n-s">记忆召回 + 人格注入 + LLM 调用 + 工具循环</span>
</div>
<!-- 循环体 -->
<div class="loop">
<span class="loop-tag">每轮复用</span>
<ol class="seq">
<li class="seq-item">
<span class="seq-n">1</span>
<div class="seq-b"><code>pre_action</code>
<em>构建消息与工具表 · 插件可在此短路</em></div>
</li>
<li class="seq-item">
<span class="seq-n">2</span>
<div class="seq-b"><b>LLM 调用</b>
<em>provider 回退 + 瞬时错误重试 · 流式累积</em></div>
</li>
<li class="seq-item">
<span class="seq-n">3</span>
<div class="seq-b"><code>post_action</code>
<em>插件可改写最终文本</em></div>
</li>
<li class="seq-item">
<span class="seq-n">4</span>
<div class="seq-b"><code>before_toolcall</code>
<em>逐个工具执行前 · 可拒</em></div>
</li>
<li class="seq-item">
<span class="seq-n">5</span>
<div class="seq-b"><b>执行工具</b>
<em>临界区:副作用不可回滚</em></div>
</li>
<li class="seq-item">
<span class="seq-n">6</span>
<div class="seq-b"><code>after_toolcall</code>
<em>上下文裁剪 · 相关记忆召回 · 结果入消息</em></div>
</li>
</ol>
<div class="loop-back" aria-hidden="true">
<span>有工具调用则回到 ①</span>
</div>
</div>
<div class="lnk" style="--d:1.10s" aria-hidden="true"></div>
<!-- 三层记忆 -->
<div class="n n-mem">
<span class="n-ico"></span>
<span class="n-t">三层记忆</span>
<span class="n-s">Context → Document → Graph</span>
</div>
<div class="lnk" style="--d:1.65s" aria-hidden="true"></div>
<!-- 输出 -->
<div class="n n-out">
<span class="n-ico"></span>
<span class="n-t">输出通道</span>
<span class="n-s">before_output · after_output · 可寻址到具体 agent</span>
</div>
<p class="flow-note">
<code>on_input</code> 在进管道之前跑;<code>before_output</code> / <code>after_output</code> 在全部轮次结束后跑一次。
</p>
</div>
</div>
</section>
<!-- ── 核心组件 ── -->
<section id="components">
<div class="wrap">
<h2 class="reveal">核心组件</h2>
<div class="comp reveal rise">
<div class="row"><code>homed</code><span>常驻内核进程LLM 编排、记忆管理、输入调度、插件生命周期</span></div>
<div class="row"><code>PluginSDK</code><span>面向内核的接口:注册工具 / 挂阶段钩子 / 订阅事件 / 声明输出通道</span></div>
<div class="row"><code>inputch</code><span>最基本的输入路由单位,可划给某个 agent含驻留子</span></div>
<div class="row"><code>Stage</code><span>7 个处理阶段钩子(进管道前 1 个、轮次中 4 个、收尾 2 个),插件可改写或短路</span></div>
<div class="row"><code>resident</code><span>驻留子 agent独立调度器 + temp 图记忆;父可查看/发送/压缩/回收/销毁,子是父拉取而非推送</span></div>
<div class="row"><code>hmapdev</code><span>插件工具链:生成工程、构建 .hmap、安装</span></div>
</div>
<div class="stats reveal" style="margin-top: 2rem">
<div class="stat"><div class="n">36</div><div class="l">在跑插件<br /><span class="sm">16 内置 + 20 外部</span></div></div>
<div class="stat"><div class="n">340</div><div class="l">可用工具<br /><span class="sm">实测注册数</span></div></div>
<div class="stat"><div class="n">109k</div><div class="l">Go 代码行<br /><span class="sm">不含第三方</span></div></div>
<div class="stat"><div class="n">9</div><div class="l">LLM 适配器<br /><span class="sm">另有 1 个网关脚本</span></div></div>
</div>
</div>
</section>
<!-- ── 上手 ── -->
<section id="quickstart">
<div class="wrap">
<h2 class="reveal rise">跑起来</h2>
<p class="sub reveal rise" style="--d:90ms">
依赖 Go 1.25+ 与 CGosqlite3。内核需 LinuxWindows 上
<span class="mono">homed</span> 跑在 WSL2 里,仅需构建 <span class="mono">waiter.exe</span>
</p>
<div class="code reveal rise"><span class="c"># 构建守护进程 + CLI</span>
make build build-cli
<span class="c"># 启动内核(数据目录自定)</span>
./build/homed -data /tmp/ha
<span class="c"># 另开一个终端:交互模式,或单条消息</span>
./build/waiter
echo <span class="s">"你好,记住我喜欢喝咖啡"</span> | ./build/waiter</div>
<p class="sub reveal rise" style="--d:90ms; margin-top: 1.2rem">
WebUI 与 API 密钥在 <span class="mono">http://localhost:8080</span> 配置,持久化到 SQLite。
</p>
</div>
</section>
<!-- ── 插件墙 ── -->
<section id="plugins">
<div class="wrap">
<h2 class="reveal">开箱可用的插件</h2>
<p class="sub reveal rise" style="--d:90ms">外部插件经 <span class="mono">hmapdev</span> 构建为独立二进制,可单独装卸、热重载。<strong>点任意插件看它能做什么。</strong></p>
<div class="badges reveal">
<button class="badge" type="button" data-plugin="a2a" style="--i:0">a2a</button>
<button class="badge" type="button" data-plugin="acp" style="--i:1">acp</button>
<button class="badge" type="button" data-plugin="bili" style="--i:2">bili</button>
<button class="badge" type="button" data-plugin="browser" style="--i:3">browser</button>
<button class="badge" type="button" data-plugin="calendar" style="--i:4">calendar</button>
<button class="badge" type="button" data-plugin="deepsearch" style="--i:5">deepsearch</button>
<button class="badge" type="button" data-plugin="editdoc" style="--i:6">editdoc</button>
<button class="badge" type="button" data-plugin="homeagent-mail-bridge" style="--i:7">homeagent-mail-bridge</button>
<button class="badge" type="button" data-plugin="mc" style="--i:8">mc</button>
<button class="badge" type="button" data-plugin="memo" style="--i:9">memo</button>
<button class="badge" type="button" data-plugin="music" style="--i:10">music</button>
<button class="badge" type="button" data-plugin="ocr" style="--i:11">ocr</button>
<button class="badge" type="button" data-plugin="plugindev" style="--i:12">plugindev</button>
<button class="badge" type="button" data-plugin="qq" style="--i:13">qq</button>
<button class="badge" type="button" data-plugin="recoverydiag" style="--i:14">recoverydiag</button>
<button class="badge" type="button" data-plugin="rss" style="--i:15">rss</button>
<button class="badge" type="button" data-plugin="sanitizer" style="--i:16">sanitizer</button>
<button class="badge" type="button" data-plugin="vanblog" style="--i:17">vanblog</button>
<button class="badge" type="button" data-plugin="vikunja" style="--i:18">vikunja</button>
<button class="badge" type="button" data-plugin="weather" style="--i:19">weather</button>
<button class="badge muted" type="button" id="expandAll">展开全部</button>
</div>
<div class="pdetail" id="pdetail">
<p class="phint" id="phint">点上方任一插件查看用途</p>
<div class="pcard" id="p-a2a">
<div class="pcard-head"><code>a2a</code><span class="ver">v1.3.1</span></div>
<p>Agent 间通信。对外起 HTTP 服务暴露 /agent-card 与 /a2aJSON-RPC供他人发现与调用对内提供工具去问别的 Agent。把监听地址设为空即只出站、不开端口。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/a2a" target="_blank" rel="noopener noreferrer" title="SDK example/a2a"><span class="psrc-r">SDK</span><span class="psrc-p">example/a2a</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-acp">
<div class="pcard-head"><code>acp</code><span class="ver">v1.2.1</span></div>
<p>Agent Client Protocol 通信插件:充当 ACP 服务端接受其他 Agent 的任务请求,同时提供客户端工具向远程 ACP Agent如 opencode发起会话并读取回复</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/acp" target="_blank" rel="noopener noreferrer" title="SDK example/acp"><span class="psrc-r">SDK</span><span class="psrc-p">example/acp</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-bili">
<div class="pcard-head"><code>bili</code><span class="ver">v1.2.0</span></div>
<p>B站视频下载。需系统装 yt-dlp。不指定格式时先返回可用清晰度列表选定后再下载输出目录有路径校验拒绝写成系统目录。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/bili" target="_blank" rel="noopener noreferrer" title="SDK example/bili"><span class="psrc-r">SDK</span><span class="psrc-p">example/bili</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-browser">
<div class="pcard-head"><code>browser</code><span class="ver">v2.4.1</span></div>
<p>统一浏览器插件搜索、HTTP抓取(quick)、无头渲染(normal)、交互式浏览器(interactive/CDP)</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/browser" target="_blank" rel="noopener noreferrer" title="SDK example/browser"><span class="psrc-r">SDK</span><span class="psrc-p">example/browser</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-calendar">
<div class="pcard-head"><code>calendar</code><span class="ver">v1.1.0</span></div>
<p>日历事件管理,支持提醒和重复事件</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/calendar" target="_blank" rel="noopener noreferrer" title="SDK example/calendar"><span class="psrc-r">SDK</span><span class="psrc-p">example/calendar</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-deepsearch">
<div class="pcard-head"><code>deepsearch</code><span class="ver">v1.1.2</span></div>
<p>为 agent 提供真正的联网信息检索:本地 SearXNG 聚合多引擎(返回标题/URL/摘要/时间),支持新闻、时间范围、指定引擎;并提供网页正文抽取与「搜索+读前K篇」的深检索</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/deepsearch" target="_blank" rel="noopener noreferrer" title="SDK example/deepsearch"><span class="psrc-r">SDK</span><span class="psrc-p">example/deepsearch</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-editdoc">
<div class="pcard-head"><code>editdoc</code><span class="ver">v2.0.0</span></div>
<p>全能办公插件:新建/读取/编辑/转换 docx·xlsx·pptx·md·csv·txt基于 python-docx / openpyxl / python-pptx / soffice / pandoc</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/editdoc" target="_blank" rel="noopener noreferrer" title="SDK example/editdoc"><span class="psrc-r">SDK</span><span class="psrc-p">example/editdoc</span><span class="psrc-out"></span></a>
<p class="psrc-note">公开仓中的 example 仍是 v1.0.0v2.0.0 全能版源码尚未公开。</p>
</div>
<div class="pcard" id="p-homeagent-mail-bridge">
<div class="pcard-head"><code>homeagent-mail-bridge</code><span class="ver">v0.2.2</span></div>
<p>HomeAgent 接入 AgentMail邮件驱动的多智能体协作</p>
<a class="psrc" href="https://gitea.jianfgit.xyz/jianf/MailUI4Agents/src/branch/main/plugins/homeagent-mail-bridge" target="_blank" rel="noopener noreferrer" title="agentmail plugins/homeagent-mail-bridge"><span class="psrc-r">agentmail</span><span class="psrc-p">plugins/homeagent-mail-bridge</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-mc">
<div class="pcard-head"><code>mc</code><span class="ver">v0.1.8</span></div>
<p>基于 mineflayer 的虚拟 MC 客户端,作为 chan:mc 通道接入。「激活」分两个状态:想连着(意图)与确实连着(连接),所以 bridge 进程被杀后能自动重登恢复会话,而不是要人再喊一次。带移动/挖掘/合成/攻击等 11 个动作工具。</p>
<span class="psrc psrc-none" title="该插件源码在本地开发仓,尚无公开地址"><span class="psrc-p">本地开发仓(无公开地址)</span></span>
</div>
<div class="pcard" id="p-memo">
<div class="pcard-head"><code>memo</code><span class="ver">v1.1.0</span></div>
<p>待办与备忘录刻意分两类:待办每 5 分钟检查未完成数、有则注入提醒;备忘录纯记事不提醒。混成一类要么天天弹、要么被忘掉。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/memo" target="_blank" rel="noopener noreferrer" title="SDK example/memo"><span class="psrc-r">SDK</span><span class="psrc-p">example/memo</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-music">
<div class="pcard-head"><code>music</code><span class="ver">v0.1.0</span></div>
<p>搜歌与查歌词(网易云公开接口)。只读:不下载音频、不落文件。两段式用法 —— 先搜到歌曲 ID再按 ID 取词。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/music" target="_blank" rel="noopener noreferrer" title="SDK example/music"><span class="psrc-r">SDK</span><span class="psrc-p">example/music</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-ocr">
<div class="pcard-head"><code>ocr</code><span class="ver">v1.0.0</span></div>
<p>图片文字识别。需系统装 tesseract中文另需 chi_sim 语言包)。支持 URL 或 base64 输入,临时文件识别后自动清理。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/ocr" target="_blank" rel="noopener noreferrer" title="SDK example/ocr"><span class="psrc-r">SDK</span><span class="psrc-p">example/ocr</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-plugindev">
<div class="pcard-head"><code>plugindev</code><span class="ver">v1.0.0</span></div>
<p>把 hmapdev 工具链封装成 Agent 可调用的工具:脚手架生成插件工程、构建打包 .hmap、管理 SDK 版本。配合 plugin_install 即可让 Agent 自己做完「新建插件 → 构建 → 安装」全流程。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/plugindev" target="_blank" rel="noopener noreferrer" title="SDK example/plugindev"><span class="psrc-r">SDK</span><span class="psrc-p">example/plugindev</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-qq">
<div class="pcard-head"><code>qq</code><span class="ver">v1.4.1</span></div>
<p>经 NapCat 桥接 QQ20 个工具覆盖消息、群/好友、文件传输。带权限模型 —— 身份绑在「帧」上而非插件全局(中断抢占恢复后不会丢身份),多来源合并取权限交集,敏感工具按前缀一律拒绝非所有者。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/qq" target="_blank" rel="noopener noreferrer" title="SDK example/qq"><span class="psrc-r">SDK</span><span class="psrc-p">example/qq</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-recoverydiag">
<div class="pcard-head"><code>recoverydiag</code><span class="ver">v0.2.0</span></div>
<p>快速检查/崩溃取证工具集diag_triage退出码/信号/存活粗分、diag_dbconfig.db 完整性 + LLM 源解析校验、diag_log_scan日志签名命中、diag_deltalast-good 快照 vs 现状 diff、diag_loc正交综合定位。全部返回结论而非原文确定性、不消耗 LLM token供 guard / failback 恢复决策使用。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/recoverydiag" target="_blank" rel="noopener noreferrer" title="SDK example/recoverydiag"><span class="psrc-r">SDK</span><span class="psrc-p">example/recoverydiag</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-rss">
<div class="pcard-head"><code>rss</code><span class="ver">v1.1.0</span></div>
<p>RSS/Atom 订阅监控。按间隔轮询,发现新条目即以中断注入告知 agent订阅时记下历史条目所以订一个源不会把旧文章全推一遍。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/rss" target="_blank" rel="noopener noreferrer" title="SDK example/rss"><span class="psrc-r">SDK</span><span class="psrc-p">example/rss</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-sanitizer">
<div class="pcard-head"><code>sanitizer</code><span class="ver">v0.1.0</span></div>
<p>在三个阶段清洗文本:坏 UTF-8 / U+FFFD / ANSI 转义(会被模型复读)与 LLM 输出里的工具调用残留。不注册工具,只挂钩子。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/sanitizer" target="_blank" rel="noopener noreferrer" title="SDK example/sanitizer"><span class="psrc-r">SDK</span><span class="psrc-p">example/sanitizer</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-vanblog">
<div class="pcard-head"><code>vanblog</code><span class="ver">v1.0.0</span></div>
<p>管理 VanBlog 开源博客系统:文章的增删改查、分类标签管理、草稿发布、备份导出等</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/vanblog" target="_blank" rel="noopener noreferrer" title="SDK example/vanblog"><span class="psrc-r">SDK</span><span class="psrc-p">example/vanblog</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-vikunja">
<div class="pcard-head"><code>vikunja</code><span class="ver">v1.0.1</span></div>
<p>Vikunja 待办/任务管理任务增删改查、项目与看板桶、标签、指派、评论、关联、附件、保存筛选器、团队与分享、通知、订阅、Webhook、时间跟踪、数据导入、实例管理并附通用 API 直通工具兜底</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/vikunja" target="_blank" rel="noopener noreferrer" title="SDK example/vikunja"><span class="psrc-r">SDK</span><span class="psrc-p">example/vikunja</span><span class="psrc-out"></span></a>
</div>
<div class="pcard" id="p-weather">
<div class="pcard-head"><code>weather</code><span class="ver">v1.0.0</span></div>
<p>查实时天气与预报wttr.in无需 API Key。可设默认城市结果标记为不进记忆计算 —— 天气是易变数据,反复写进记忆只会挤占预算。</p>
<a class="psrc" href="https://gitcode.com/JianFeeeee/homeagent-sdk/blob/main/example/weather" target="_blank" rel="noopener noreferrer" title="SDK example/weather"><span class="psrc-r">SDK</span><span class="psrc-p">example/weather</span><span class="psrc-out"></span></a>
</div>
</div>
</div>
</section>
<!-- ── FAQ ── -->
<section id="faq">
<div class="wrap">
<h2 class="reveal rise">常见问题</h2>
<p class="sub reveal rise" style="--d:90ms">最容易引起误解的六个地方。</p>
<div class="reveal rise">
<details class="faq">
<summary>「内核零 IO」是不是意味着内核很简单</summary>
<p>
恰恰相反。零 IO 是为了让内核专注做难的部分:输入调度(四级中断、抢占与
现场保存、上下文预算、三层记忆的蒸馏与召回。IO 挪出去之后,这些才有
清晰的所有权。
</p>
</details>
<details class="faq">
<summary>插件崩了会不会把 Agent 带崩?</summary>
<p>
不会。外部插件是独立子进程,与内核只通过 stdio JSON-RPC、共享内存段、
事件环通信。插件崩溃后,内核摘除它注册的工具、阶段与通道,按退避重启,
其余部分不受影响。
</p>
</details>
<details class="faq">
<summary>为什么消息会「排队」而不是立即处理?</summary>
<p>
因为同一时刻只应有一个任务在改上下文。同级抢占会让两边都做一半。
需要立刻插队时用更高级别的中断:插件可声明 L1L3L4 是内核保留的
「立即打断」panic、内核事件外部插件声明 L4 会被夹到 L3。
不该让所有消息都变成抢占者。
</p>
</details>
<details class="faq">
<summary>长任务会不会让用户等很久?</summary>
<p>
这正是「分诊助手」的用途。主 agent 忙超阈值且积压够多时,内核拉起临时
驻留子接手:能办的直接办,需要主 agent 的立刻回「忙碌中,请稍候」。
</p>
</details>
<details class="faq">
<summary>记忆会不会无限膨胀?</summary>
<p>
不会。三层是迁移而非复制:上下文事件裁剪后归档成文档,文档冷掉后蒸馏成
图谱三元组。图谱有去重与 UNIQUE 约束;媒体跟着记忆块走,块被永久删除时才回收字节。
</p>
</details>
<details class="faq">
<summary>能换 LLM 吗?</summary>
<p>
可以。内核不绑定厂商:每个 LLM 源对应一个 Lua 脚本负责请求与响应转换。
内置 9 个适配器OpenAI、Anthropic、Gemini、DeepSeek、Groq、Mistral、
Ollama、GitHub、KimiCode也可自己写一个接任意兼容 API。
</p>
</details>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap">
<div class="cols">
<div>
<h4>文档</h4>
<ul>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/OVERVIEW.md">项目概览</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/ARCHITECTURE.md">技术架构</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/PLUGIN_DEV.md">插件开发</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/assets/docs/zh/ADAPTER.md">Lua 适配器</a></li>
</ul>
</div>
<div>
<h4>深入</h4>
<ul>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/docs/zh/input-scheduler-design.md">输入调度器设计</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/docs/zh/resident-subagent-design.md">驻留式子 Agent</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/docs/zh/multimodal-space.md">统一多模态空间</a></li>
</ul>
</div>
<div>
<h4>项目</h4>
<ul>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent">源码仓库</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/releases">发布下载</a></li>
<li><a href="https://gitcode.com/JianFeeeee/homeagent-sdk">插件 SDK</a></li>
</ul>
</div>
<div>
<h4>状态</h4>
<ul>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/releases">最新发布v1.3.x 线</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/internal/meta/meta.go">main 在研1.4.0</a></li>
<li><a href="https://gitcode.com/JianFeeeee/HomeAgent/blob/main/LICENSE">许可AGPL-3.0-only</a></li>
</ul>
</div>
</div>
<div class="note">
<p style="margin: 0 0 0.4rem">
<strong>AI 辅助声明</strong>:本项目代码、文档与提交历史含 AI 生成或修改的内容。
关键改动经人工审阅,使用前请自行评估验证。
</p>
<p style="margin: 0">
AGPL-3.0-only。通过 HTTP 提供交互式服务时须向使用者提供完整对应源码§13
</p>
<p class="beian">
<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener noreferrer">豫ICP备2024074105号-1</a>
<span class="beian-sep">·</span>
<a href="https://beian.mps.gov.cn" target="_blank" rel="noopener noreferrer">豫公网安备41070202001579号</a>
</p>
</div>
</div>
</footer>
<script>
/* ═══════════════════════════════════════════════════════════
动效引擎。设计原则(吃过一次亏,见 git 历史):
· **内容默认可读** —— 不用 JS 也完整;只有本脚本真的跑起来
才给 <html> 加 .js-fx 接管初始隐藏,因此 JS 报错/被禁用
时页面不会变成一片空白(早期版本把 opacity:0 写在默认
样式里,结果 30 个元素里 26 个永久不可见)。
· 尊重 prefers-reduced-motion此时不动、不加 .js-fx
· 所有 listener 走 rAF 节流;粒子数按视口面积自适应。
═══════════════════════════════════════════════════════════ */
(function () {
var root = document.documentElement;
var reduce = window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ── 主题切换 ── */
var toggle = document.getElementById("themeToggle");
function paint(isLight) {
root.setAttribute("data-theme", isLight ? "light" : "dark");
}
if (toggle) {
toggle.addEventListener("click", function () {
var isLight = root.getAttribute("data-theme") === "light";
paint(!isLight);
try { localStorage.setItem("ha-theme", !isLight ? "light" : "dark"); } catch (e) {}
});
}
// 系统主题变化时跟随(用户没手动选过才跟)
try {
var mq = window.matchMedia("(prefers-color-scheme: light)");
if (mq.addEventListener) {
mq.addEventListener("change", function (e) {
if (!localStorage.getItem("ha-theme")) paint(e.matches);
});
}
} catch (e) {}
/* ── 滚动进度条 ── */
var bar = document.getElementById("progress");
var ticking = false;
function onScroll() {
if (ticking) return;
ticking = true;
requestAnimationFrame(function () {
if (bar) {
var h = document.documentElement.scrollHeight - window.innerHeight;
bar.style.width = (h > 0 ? (window.scrollY / h) * 100 : 0) + "%";
}
ticking = false;
});
}
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
/* ── 入场动画 ── */
if (!reduce) {
root.classList.add("js-fx");
var els = [].slice.call(document.querySelectorAll(
".rise, .rise-zoom, .reveal, section > .wrap > h2, .badges"));
function reveal(el) {
el.classList.add("in");
if (el.classList.contains("badges")) {
var bs = el.querySelectorAll(".badge");
for (var i = 0; i < bs.length; i++) {
bs[i].style.transitionDelay = (i * 28) + "ms";
}
}
}
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { reveal(e.target); io.unobserve(e.target); }
});
}, { threshold: 0.1, rootMargin: "0px 0px -6% 0px" });
els.forEach(function (el) { io.observe(el); });
} else {
els.forEach(reveal);
}
// 保险:布局因图片/字体迟到而变化时,已在视口内的补上
window.addEventListener("load", function () {
els.forEach(function (el) {
var r = el.getBoundingClientRect();
if (r.top < window.innerHeight && r.bottom > 0) reveal(el);
});
});
// 兜底3 秒后无论 observer 是否生效,强制全部可见
setTimeout(function () { els.forEach(reveal); }, 3000);
}
/* ── 数字滚动 ── */
var stats = [].slice.call(document.querySelectorAll(".stat .n"));
function runCount(el) {
var raw = el.textContent.trim();
var m = raw.match(/^([\d.]+)(.*)$/);
if (!m) return;
var target = parseFloat(m[1]), suffix = m[2], dur = 1100, t0 = null;
// 小数位按原值保持(如 34.6
var dec = (m[1].split(".")[1] || "").length;
function step(ts) {
if (!t0) t0 = ts;
var p = Math.min((ts - t0) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = (target * eased).toFixed(dec) + suffix;
if (p < 1) requestAnimationFrame(step);
else el.textContent = raw;
}
requestAnimationFrame(step);
}
if (!reduce && "IntersectionObserver" in window && stats.length) {
var sio = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) { runCount(e.target); sio.unobserve(e.target); }
});
}, { threshold: 0.6 });
stats.forEach(function (el) { sio.observe(el); });
}
// 无 IO / reduce 时保留静态文本,不做数字动画
/* ── 卡片聚光 + 3D 微倾(仅精确指针;触屏不做) ── */
var fine = window.matchMedia && window.matchMedia("(hover: hover) and (pointer: fine)").matches;
if (!reduce && fine) {
var cards = [].slice.call(document.querySelectorAll(".card, .stat"));
cards.forEach(function (c) {
var raf = 0;
c.addEventListener("mousemove", function (ev) {
if (raf) return;
raf = requestAnimationFrame(function () {
var r = c.getBoundingClientRect();
var x = ev.clientX - r.left, y = ev.clientY - r.top;
c.style.setProperty("--mx", x + "px");
c.style.setProperty("--my", y + "px");
// 轻微倾斜:以中心为原点,最多 ±3.2°
var rx = ((y / r.height) - 0.5) * -4.2;
var ry = ((x / r.width) - 0.5) * 4.2;
c.style.transform =
"perspective(760px) rotateX(" + rx.toFixed(2) + "deg) rotateY(" +
ry.toFixed(2) + "deg) translateY(-2px)";
raf = 0;
});
});
c.addEventListener("mouseleave", function () {
if (raf) { cancelAnimationFrame(raf); raf = 0; }
c.style.transform = "";
});
});
}
/* ── 插件徽章:点击展开详情(可多开;「全部」按钮切全展开) ── */
(function () {
var badges = [].slice.call(document.querySelectorAll(".badge[data-plugin]"));
var hint = document.getElementById("phint");
var detail = document.getElementById("pdetail");
if (!badges.length || !detail) return;
function sync(btn) {
var id = "p-" + btn.dataset.plugin;
var card = document.getElementById(id);
var on = btn.getAttribute("aria-pressed") === "true";
if (card) card.classList.toggle("show", on);
}
badges.forEach(function (b) {
b.setAttribute("aria-pressed", "false");
b.addEventListener("click", function () {
var on = b.getAttribute("aria-pressed") === "true";
b.setAttribute("aria-pressed", on ? "false" : "true");
sync(b);
// 有任何一张打开就藏起提示
var any = badges.some(function (x) {
return x.getAttribute("aria-pressed") === "true";
});
if (hint) hint.style.display = any ? "none" : "";
// 首次打开时把详情滚进视口
if (!on) {
var c = document.getElementById("p-" + b.dataset.plugin);
if (c && c.getBoundingClientRect().top > window.innerHeight - 120) {
c.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: "center" });
}
}
});
});
// 「…共 N 个」按钮:一次全展开
var all = document.querySelector(".badge.muted");
if (all && !all.dataset.bound) {
all.dataset.bound = "1";
var expanded = false;
var total = badges.length;
all.setAttribute("role", "button");
all.setAttribute("tabindex", "0");
all.textContent = "展开全部 " + total + " 个";
function toggleAll() {
expanded = !expanded;
detail.classList.toggle("all", expanded);
badges.forEach(function (b) {
b.setAttribute("aria-pressed", expanded ? "true" : "false");
sync(b);
});
all.textContent = expanded ? "收起" : "展开全部 " + total + " 个";
if (hint) hint.style.display = expanded ? "none" : "";
}
all.addEventListener("click", toggleAll);
all.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggleAll(); }
});
}
})();
/* ── 粒子网络 ── */
var cv = document.getElementById("fx");
if (!reduce && cv && cv.getContext) {
var ctx = cv.getContext("2d");
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var W = 0, H = 0, pts = [], rgb = "148, 190, 255", alive = true;
function themeColor() {
rgb = getComputedStyle(root).getPropertyValue("--particle").trim() ||
"148, 190, 255";
}
function resize() {
/* 用 clientWidth 而不是 innerWidth后者含滚动条宽度实测 390 vs 375
画布按 390 铺开会多出 15px 并把整页撑出横向滚动条。 */
var de = document.documentElement;
W = de.clientWidth; H = de.clientHeight;
cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr);
cv.style.width = W + "px"; cv.style.height = H + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
// 密度按面积自适应,上限 72再多就开始吃 CPU 了)
var n = Math.min(72, Math.round((W * H) / 26000));
pts = [];
for (var i = 0; i < n; i++) {
pts.push({
x: Math.random() * W, y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.24,
vy: (Math.random() - 0.5) * 0.24,
r: Math.random() * 1.5 + 0.6
});
}
}
function frame() {
if (!alive) return;
ctx.clearRect(0, 0, W, H);
for (var i = 0; i < pts.length; i++) {
var p = pts[i];
p.x += p.vx; p.y += p.vy;
if (p.x < -20) p.x = W + 20; else if (p.x > W + 20) p.x = -20;
if (p.y < -20) p.y = H + 20; else if (p.y > H + 20) p.y = -20;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, 6.2832);
ctx.fillStyle = "rgba(" + rgb + ", 0.5)";
ctx.fill();
}
// 近邻连线:距离 < 130 才连,透明度线性衰减
for (var a = 0; a < pts.length; a++) {
for (var b = a + 1; b < pts.length; b++) {
var dx = pts[a].x - pts[b].x, dy = pts[a].y - pts[b].y;
var d2 = dx * dx + dy * dy;
if (d2 < 16900) {
var o = (1 - Math.sqrt(d2) / 130) * 0.22;
ctx.strokeStyle = "rgba(" + rgb + ", " + o.toFixed(3) + ")";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(pts[a].x, pts[a].y);
ctx.lineTo(pts[b].x, pts[b].y);
ctx.stroke();
}
}
}
requestAnimationFrame(frame);
}
themeColor(); resize(); frame();
var rraf = 0;
window.addEventListener("resize", function () {
if (rraf) return;
rraf = requestAnimationFrame(function () { resize(); rraf = 0; });
});
// 切主题时换粒子颜色
if (toggle) toggle.addEventListener("click", function () { setTimeout(themeColor, 30); });
// 标签页不可见时暂停(省电;回来再继续)
document.addEventListener("visibilitychange", function () {
if (document.hidden) { alive = false; }
else if (!alive) { alive = true; frame(); }
});
}
})();
</script>
</body>
</html>