Files
HomeAgent/site/index.html
JianFeeeee 7587bd82d8 site: 正名「驻留子 agent」+ 删净页面小字注解
用户两点意见:
1. 那个东西不叫「分诊助手」,叫**驻留子 agent**
2. 页面上加的小字注解全是废话,全删

## ① 正名:分诊助手 → 驻留子 agent

「分诊」只是 offload.go 里对**职责**的描述(triage),
`resident.go` 顶注给的正式名是「驻留子」,`docs/zh/resident-subagent-design.md`
也把它列为独立概念(根 agent / 驻留子 / 工作式轻量子三类)。

7 处全部改正,含图内 SVG 文字、`aria-label`、架构流程图、FAQ 正文:

| 位置 | 原 | 现 |
|---|---|---|
| 图 5 框标题 | 分诊助手 | 驻留子 agent |
| 图 5 框内第二行 | 临时驻留子 | (删,与标题重复) |
| 图 5 的 aria-label | 转投给分诊助手 | 转投给驻留子 agent |
| 正文 | 转给一个临时驻留的**分诊助手** | 转给一个**驻留子 agent** |
| 架构图转投分支 | → 转投临时分诊助手 | → 转投驻留子 agent |
| FAQ | 这正是「分诊助手」的用途 | 这正是驻留子 agent 的用途 |
| CSS 注释 | 分诊助手接到消息 | 驻留子 agent 接到消息 |

## ② 删掉 11 处注解小字

原则:**复述图上已画出来的、或标题已说过的** → 删;
**承载数据**(插件版本 / 源码路径 / SVG 图内必要标签)→ 留。

- `.duty-note` ×6:架构图里 6 个阶段下面那行灰字
  (「构建消息与工具表 · 插件可在此短路」等)—— 阶段名本身已经说清,
  而这 6 行会把循环框右半边撑空
- `.flow-note`:重复解释 `on_input` / `before_output` 跑在什么时候
- `.dv-caps` ×3:图 4 的「任何中断都能插它前面」「被打断的现场压入中断栈」、
  图 5 的「不必干等」—— 图上插队动线和回执 chip 已经把它们画出来了
- `.dv-note` ×2:图 2「实测:statically linked,无动态依赖」、
  图 3「实测整段写入+读回 3.5µs」—— 数字重复
- `.dv5-tnote`:「默认关闭,需部署方打开」—— FAQ 里有完整说明(含理由)
- `.duty-ev` ×4 + `.sm` ×4:证据行/注释行,数字并回正文
  (「实测整段写入 + 读回 3.5µs。」等)

## ③ 删小字的连带修正

- **11 处死 CSS 全部清掉**(`.duty-ev` / `.flow-note` / `.dv-caps` /
  `.dv-note` / `.dv5-tnote` / `.sm`),单文件不留无人引用的规则
- 循环框删掉 6 行灰字后**右半边空了一大块** → 改为 `fit-content` 收窄居中

## 自己踩到并改掉的坑

- 循环框收得太窄(16rem),回边徽标「有工具调用则回到 ①」**压住**
  `after_toolcall` → 加 `min-width` 并加大底部 padding
- 想让循环框占满宽度,试过**阶段项排两列** → 截图发现编号被 grid
  按行填充排成 `1/3/5 · 2/4/6`,线性流程顺序全乱。已回退单列,
  并把这条教训写进 CSS 注释(免得后人再试一次)
- 图 5 触发条件框删掉小字后留白偏大 → 高度 56→42

## 验证

- 三档宽度(1440/768/390):无横向溢出、回边徽标与阶段项**零重叠**、
  文字未被截断
- 深浅双主题 / JS 禁用 / reduce 回归全绿;控制台零错误
- 死 CSS 计数全部归零;「分诊」全站残留 0
- CSS 括号平衡,文件 141,847 字节(原 143,960,净减 2.1KB)
2026-09-21 10:23:06 +08:00

3358 lines
160 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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;
/* 示意图节点面:深色下上浅下深,模拟受光 */
--dv-card-a: rgba(30, 41, 66, 0.92);
--dv-card-b: rgba(17, 24, 44, 0.92);
--dv-sheen: rgba(255, 255, 255, 0.10);
/* 示意图玻璃面板 */
--fig-bg: linear-gradient(165deg, rgba(30, 41, 66, 0.62), rgba(12, 18, 34, 0.72));
--fig-border: rgba(255, 255, 255, 0.10);
--fig-shadow: rgba(0, 0, 0, 0.85);
--fig-inner-top: rgba(255, 255, 255, 0.10);
--fig-inner-bottom: rgba(0, 0, 0, 0.35);
--fig-glow: rgba(56, 189, 248, 0.10);
--fig-grain: 0.16;
/* 排队列:不能直接用 gCard —— 浅色下 gCard 是白的,
在白色玻璃面板上等于看不见。给一个独立色调。 */
--dv-queue-item: rgba(119, 128, 154, 0.22);
--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);
/* 浅色下受光方向相反:上近白、下微灰 */
--dv-card-a: rgba(255, 255, 255, 0.98);
--dv-card-b: rgba(240, 245, 253, 0.98);
--dv-sheen: rgba(255, 255, 255, 0.85);
--fig-bg: linear-gradient(165deg, rgba(255, 255, 255, 0.88), rgba(236, 242, 252, 0.92));
--fig-border: rgba(15, 23, 42, 0.08);
--fig-shadow: rgba(15, 23, 42, 0.55);
--fig-inner-top: rgba(255, 255, 255, 0.95);
--fig-inner-bottom: rgba(15, 23, 42, 0.06);
--fig-glow: rgba(37, 99, 235, 0.08);
--fig-grain: 0.08;
--dv-queue-item: rgba(37, 99, 235, 0.13);
--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;
/* 横向必须裁掉交错行的入场位移translateX ±42px会让未入场的
元素先探头到视口右侧之外,窄屏实测撑出 26px 横向溢出。
用 clip 而不是 hidden —— hidden 会新建滚动容器,可能干扰 sticky。 */
overflow-x: clip;
}
body { overflow-x: clip; }
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; }
/* FAQ 展开超过一屏时:整页切到自由滚动。
为何不是只让 faq 退出吸附 —— 试过了会死锁faq 退出吸附后它下方
没有吸附点,而 mandatory 只允许停在吸附点,实测卡在 plugins 的
吸附位y=9455不再前进页脚永远到不了。
因为 faq 是最后一个吸附节,它一旦不能吸附,末尾就必须整体自由滚动。 */
html.faq-open { scroll-snap-type: none; }
/* 内容比一屏高的节(运行时打 .too-tall取消 min-height 撑高,
但**保留吸附点**。
为何不让它退出吸附(实测过的死锁):把它设为 snap-align:none 后,
前一个吸附点memory会把它自己吸回来而后面又没有吸附点接住
结果卡死在 memory 底部y=5791 不动)—— 页脚与后续内容全部到不了。
正确做法是让它自然高、仍作为吸附点,间距不等距不影响可达性。 */
section.too-tall { min-height: auto; }
/* 页脚也是吸附点。
不加这条的后果(实测):最后一个 section 顶部到文档底有 1214px
(含 footer 386px而视口只有 900px —— mandatory 只允许停在吸附点,
于是**页脚永远滚不到**(实测卡在 y=10283上限 10669差 386px
scroll-snap-stop: always 确保它自己占一屏,不与其他吸附点抢。 */
/* 页脚**不单独做吸附点**。
为何实测二次修faq 高 643px + footer 367px = 1010px > 视口 900px。
只要 footer 也是吸附点浏览器就必须在「faq 顶部对齐」与「页脚可见」
之间二选一 —— 实测选了前者,把 faq 的 h2 吸到 y=-38被导航栏压住。
让它跟随 faq 自然滚动,两者都可读,也不会出现多一屏空白。 */
footer { scroll-snap-align: none; }
/* 收尾段:取消整屏高度(它本就不需要占满),恢复吸附。
前提是 faq 已改双列 —— 收尾合计压到一屏内,此时同时满足
「标题不被导航压住」与「页脚可达」两个要求。 */
section:last-of-type {
min-height: auto;
padding-bottom: 0.5rem;
}
/* 注footer 的 padding 调优直接写进下面那条原始规则里(约 1022 行)——
同优先级下后出现的规则会胜,在这里再写一条会被它覆盖(已踩过)。 */
/* 吸附容器不能有 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; }
@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; }
/* ── 示意图容器:玻璃面板 ──
为什么不再把 SVG 裸放在页面上:平面矢量图形浮在平坦背景上,
无论内部怎么画都像线框图。给一层带内高光、分层投影与颗粒的玻璃底,
图形才像「浮在界面上」。 */
.alt-visual {
position: relative;
min-width: 0;
padding: 1.15rem 1.15rem 0.95rem;
border-radius: var(--radius-lg);
background: var(--fig-bg);
border: 1px solid var(--fig-border);
/* 三层叠加:外投影(飘起来) + 内上高光(顶部受光) + 内下暗(厚度) */
box-shadow:
0 18px 40px -18px var(--fig-shadow),
0 2px 6px -2px var(--fig-shadow),
inset 0 1px 0 var(--fig-inner-top),
inset 0 -1px 0 var(--fig-inner-bottom);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
overflow: hidden;
}
/* 顶部柔光:让面板有「受光面」 */
.alt-visual::before {
content: "";
position: absolute;
inset: -40% -10% auto -10%;
height: 70%;
background: radial-gradient(ellipse 60% 100% at 50% 0%, var(--fig-glow), transparent 70%);
pointer-events: none;
}
/* 细微颗粒:抿掉矢量图的「塑料感」 */
.alt-visual::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
opacity: var(--fig-grain);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");
}
.alt-visual svg { position: relative; z-index: 1; width: 100%; height: auto; display: block; }
@media (max-width: 860px) {
.alt { grid-template-columns: 1fr; gap: 1.6rem; margin-top: 2.6rem; }
/* 窄屏位移收小:单列时左右各 42px 会让内容显得跳,也更容易探出视口 */
.js-fx .alt:not(.flip) .alt-copy { transform: translateX(-20px); }
.js-fx .alt:not(.flip) .alt-visual { transform: translateX(20px); }
.js-fx .alt.flip .alt-copy { transform: translateX(20px); }
.js-fx .alt.flip .alt-visual { transform: translateX(-20px); }
.faq-grid { grid-template-columns: 1fr; gap: 0.55rem; }
.alt.flip .alt-copy, .alt.flip .alt-visual { order: 0; }
}
/* ── 共享 SVG defs 的取值(随主题切换)── */
.g-card-a { stop-color: var(--dv-card-a); }
.g-card-b { stop-color: var(--dv-card-b); }
.g-cyan-a { stop-color: rgba(6, 182, 212, 0.28); }
.g-cyan-b { stop-color: rgba(6, 182, 212, 0.08); }
.g-blue-a { stop-color: rgba(59, 130, 246, 0.28); }
.g-blue-b { stop-color: rgba(59, 130, 246, 0.08); }
.g-violet-a { stop-color: rgba(139, 92, 246, 0.30); }
.g-violet-b { stop-color: rgba(139, 92, 246, 0.09); }
.g-gold-a { stop-color: rgba(245, 158, 11, 0.26); }
.g-gold-b { stop-color: rgba(245, 158, 11, 0.07); }
.g-sheen-a { stop-color: var(--dv-sheen); }
.g-sheen-b { stop-color: rgba(255, 255, 255, 0); }
.f-shadow { flood-color: rgba(0, 0, 0, 0.55); flood-opacity: 0.5; }
:root[data-theme="light"] .f-shadow { flood-color: rgba(15, 23, 42, 0.9); flood-opacity: 0.18; }
.f-glowtint { flood-color: var(--brand-cyan); flood-opacity: 0.35; }
/* ── 示意图图元 ── */
.dv-bound { fill: none; stroke: var(--border); stroke-width: 1.5; stroke-dasharray: 6 5; }
/* 节点:渐变面 + 分层投影 */
.dv-box {
fill: url(#gCard);
stroke: var(--border-hi);
stroke-width: 1.3;
filter: url(#fShadow);
transition: stroke .3s var(--ease), filter .3s var(--ease);
}
/* 悬停:描边提亮 + 光晕,证明它不是一张死的图 */
@media (hover: hover) {
.dv-box:hover {
stroke: var(--brand-cyan);
filter: url(#fLift);
}
}
.dv-accent { fill: url(#gCyan); stroke: var(--brand-cyan); }
.dv-run { fill: url(#gBlue); stroke: var(--brand-blue); }
.dv-l4 { fill: url(#gViolet); stroke: var(--brand-violet); }
.dv-l3 { fill: url(#gBlue); stroke: var(--brand-blue); }
.dv-l2 { fill: url(#gCyan); stroke: var(--brand-cyan); }
.dv-l1 { fill: url(#gGold); 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; }
/* 左对齐变体L1L4 那种「编号 + 说明」的行必须左对齐,
用 middle 会让长的那行L4 内核独占 · 立即打断)直接溢出框外。 */
.dv-tsl { fill: var(--text); font-size: 12px; font-weight: 600; text-anchor: start; }
.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: url(#gCard); stroke: var(--border-hi); stroke-width: 1.4; }
.dv-hd { fill: url(#gViolet); stroke: var(--brand-violet); }
.dv-ar { fill: url(#gCyan); 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 { fill: none; stroke: var(--brand-gold); stroke-width: 1.6; opacity: .9; stroke-linecap: round; }
.dv-line { fill: none; stroke: var(--border-hi); stroke-width: 1.4; }
/* 流动的虚线:让连接线看起来「有东西在跑」而不是静止装饰 */
.dv-dash,
.dv-arrow { animation: dashFlow 1.4s linear infinite; }
@keyframes dashFlow { to { stroke-dashoffset: -16; } }
/* 推荐reduce 下停掉 */
@media (prefers-reduced-motion: reduce) {
.dv-dash, .dv-arrow { animation: none; }
}
.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; }
/* 级别徽标L1L4 */
.dv-badge {
fill: var(--text);
font-family: var(--font-mono);
font-size: 11.5px;
font-weight: 700;
text-anchor: start;
}
/* 右侧场景标注 */
.dv-end { fill: var(--text-muted); font-size: 9.5px; text-anchor: end; }
/* 背景强度条:越靠上越亮,用高度表现紧迫度 */
.dv-gauge-bg { fill: rgba(119, 128, 154, 0.07); }
.dv-g1 { fill: rgba(245, 158, 11, 0.07); }
.dv-g2 { fill: rgba(6, 182, 212, 0.07); }
.dv-g3 { fill: rgba(59, 130, 246, 0.07); }
.dv-g4 { fill: rgba(139, 92, 246, 0.10); }
/* 抢占线的起点圆点 */
.dv-pkdot { fill: var(--brand-gold); }
/* ══ 示意图的「活着」层 ══
为何要这一层:图里 19~39 个元素原本只有一个虚线在跑,其余全静 ——
读起来就是「框框住文本」。这里按每张图的语义给它自己的动作:
包在链路上跑、依赖被划掉、共享内存写入、消息分级抢占、任务转投。
所有动画共用一套节奏变量,同一屏内不打架。 */
.alt-visual {
--dvc: cubic-bezier(.4, 0, .2, 1);
}
/* 仅在入场完成后开跑(未入场时图偏在屏外,跑动画没人看且耗费帧) */
.alt-visual svg .dv-anim { animation-play-state: paused; }
.alt.in .alt-visual svg .dv-anim,
.alt-visual.in svg .dv-anim { animation-play-state: running; }
/* ① 数据包沿链路反复穿行(图 1 / 图 5 */
@keyframes pktTravel {
0% { offset-distance: 0%; opacity: 0; }
12% { opacity: 1; }
80% { opacity: 1; }
100% { offset-distance: 100%; opacity: 0; }
}
/* ② 依赖项被依次划掉(图 2线自己「划」过去不是淡入。
实现pathLength=1 把每条线归一化,再用 dashoffset 1→0 逐段绘出。
比单纯 opacity 淡入像"划掉"得多。 */
@keyframes strikeDeps {
0%, 8% { stroke-dashoffset: 1; opacity: 1; }
30% { stroke-dashoffset: 0; opacity: 1; }
78% { stroke-dashoffset: 0; opacity: 1; }
92%, 100% { stroke-dashoffset: 0; opacity: 0; }
}
@keyframes depPulse {
0%, 100% { opacity: .42; }
45% { opacity: .95; }
}
.dv-deps .dv-plug { animation: depPulse 3.4s var(--dvc) infinite; }
.dv-deps .dv-no path { animation: strikeDeps 3.4s var(--dvc) infinite; }
.dv-deps .dv-no path:nth-child(1) { animation-delay: .18s; }
.dv-deps .dv-no path:nth-child(2) { animation-delay: .34s; }
.dv-deps .dv-no path:nth-child(3) { animation-delay: .50s; }
.dv-deps .dv-no path:nth-child(4) { animation-delay: .66s; }
.dv-deps .dv-x { animation: depPulse 3.4s var(--dvc) infinite; animation-delay: .8s; }
/* ③ 共享内存:写入光带扫过,头部/arena 依次亮(图 3 */
@keyframes shmSweep {
0% { transform: translateX(0); opacity: 0; }
10% { opacity: 1; }
90% { opacity: 1; }
100% { transform: translateX(150px); opacity: 0; }
}
@keyframes segLight {
0%, 100% { opacity: .55; }
50% { opacity: 1; }
}
.dv-seg { animation: segLight 2.6s var(--dvc) infinite; }
.dv-hd { animation: segLight 2.6s var(--dvc) infinite; animation-delay: .3s; }
/* ④ 优先级:高优先级包从下往上插队(图 4 */
@keyframes preempt {
0% { transform: translateY(0); opacity: 0; }
8% { opacity: 1; }
30% { transform: translateY(-26px); opacity: 1; }
62% { transform: translateY(-26px); opacity: 1; }
80%, 100% { transform: translateY(-26px); opacity: 0; }
}
.dv-pk-dot { animation: preempt 3.6s var(--dvc) infinite; }
/* ⑤ 消息排队 → 转投:包先积压再被接走(图 5 */
@keyframes msgStack {
0%, 18% { opacity: 0; transform: translateY(6px); }
30% { opacity: 1; transform: none; }
72% { opacity: 1; transform: none; }
88%, 100% { opacity: 0; transform: translateX(16px); }
}
.dv-msg { animation: msgStack 3.8s var(--dvc) infinite; }
.dv-msg:nth-child(2) { animation-delay: .22s; }
.dv-msg:nth-child(3) { animation-delay: .44s; }
/* 插队轨迹:常驻淡线,让「从哪来、到哪去」始终可读;
走动的包用 .dv-pkdot金色。 */
.dv-pkline { fill: none; stroke: var(--brand-gold); stroke-width: 1.1; opacity: .22; }
/* ── 图 5 新增图元 ── */
/* 主 agent 的忙碌条:一直爬不到头,呼应「占住很久」 */
.dv-busy-track { fill: var(--border); opacity: .5; }
.dv-busy-fill { fill: var(--brand-blue); opacity: .75; }
/* 回执区:极淡底 + 虚线边,与主干道区分开 */
.dv5-band { fill: var(--bg-card); stroke: var(--border); stroke-width: 1; stroke-dasharray: 5 4; opacity: .55; }
.dv5-chip { fill: var(--bg-elev, var(--bg-card)); stroke: var(--border-hi); stroke-width: 1; }
.dv5-out1 { fill: var(--brand-cyan); font-size: 9.4px; text-anchor: middle; }
.dv5-out2 { fill: var(--text-muted); font-size: 9.4px; text-anchor: middle; }
/* 触发条件卡:金色系,与「忙碌」呼应;也给的是可核验的默认值 */
.dv5-trigger { fill: none; stroke: var(--brand-gold); stroke-width: 1.2; stroke-dasharray: 5 4; opacity: .6; }
.dv5-tcap { fill: var(--brand-gold); font-size: 9.6px; text-anchor: middle; font-weight: 600; letter-spacing: .04em; }
.dv5-tval { fill: var(--text-dim); font-size: 9.6px; text-anchor: middle; }
/* 积压条被逐条取走 */
.js-fx .dv5-pile rect { animation: queueTake 3.6s ease-in-out infinite; }
.js-fx .dv5-pile rect:nth-of-type(1) { animation-delay: 0s; }
.js-fx .dv5-pile rect:nth-of-type(2) { animation-delay: .5s; }
.js-fx .dv5-pile rect:nth-of-type(3) { animation-delay: 1s; }
/* ── 图 3 新增图元 ── */
/* 玻璃高光:贴在方块上半部,营造「有厚度的一层壳」 */
.dv-sheen {
fill: url(#gSheen);
/* 只盖上半部、透明度低。整块铺满会像蒙了层白纱(实测过)。 */
opacity: .3;
pointer-events: none;
}
/* 进程的虚拟地址:左右各自靠边,避开中间的竖向连接线 */
.dv-addr { fill: var(--text-muted); font-size: 10px; font-family: var(--font-mono); text-anchor: start; }
.dv-addr-r { text-anchor: end; }
/* 桥接说明:居中,压在两条竖向虚线之间,不与任何线重叠 */
.dv-bridge { fill: var(--brand-cyan); font-size: 10px; text-anchor: middle; font-weight: 600; }
/* 物理页标题:也居中。靠左会被 x=72 那条写入虚线穿过(实测压字)。 */
.dv-physcap { fill: var(--brand-cyan); font-size: 10.5px; text-anchor: middle; font-weight: 600; opacity: .9; }
/* 同一物理页的呼吸描边 */
.dv-phys-glow { fill: none; stroke: var(--brand-cyan); stroke-width: 2; opacity: 0; }
@keyframes physBreathe {
0%, 100% { opacity: 0; }
50% { opacity: .38; }
}
.js-fx .dv-phys-glow { animation: physBreathe 4.4s ease-in-out infinite; }
/* 同一物理页:实线外框 + 极淡填充,与「虚拟地址」的虚线形成对比 */
.dv-phys { fill: none; stroke: var(--brand-cyan); stroke-width: 1.6; opacity: .5; }
.dv-page-fill { fill: url(#gPage); }
/* arena 字节刻度:细密竖线,把「一片区域」讲成「有结构的区域」 */
.dv-ticks path { stroke: var(--text-muted); stroke-width: 0.8; opacity: .35; }
/* 同页连通线:两进程之间的一条细光路 */
.dv-pagelink { fill: none; stroke: var(--brand-cyan); stroke-width: 1.1; opacity: .30; stroke-dasharray: 3 3; }
.dv-linkdot { fill: var(--brand-cyan); filter: url(#fGlow); }
/* 级别强度条(图 4从 L1 到 L4 依次点亮,表现「级别越低越可等、
越高越紧急」。静态时四条只是深浅不同的背景,看不出递进。 */
.dv-gauge-fill rect { opacity: .45; }
@keyframes lvlLight {
0%, 100% { opacity: .45; }
50% { opacity: 1; }
}
.js-fx .dv-g1 { animation: lvlLight 3.2s ease-in-out infinite; }
.js-fx .dv-g2 { animation: lvlLight 3.2s ease-in-out infinite; animation-delay: .25s; }
.js-fx .dv-g3 { animation: lvlLight 3.2s ease-in-out infinite; animation-delay: .5s; }
.js-fx .dv-g4 { animation: lvlLight 3.2s ease-in-out infinite; animation-delay: .75s; }
/* 写入光带(图 3沿 arena 自左向右扫过,示意数据落进共享段 */
.dv-sweep {
fill: url(#gCyan);
opacity: 0;
}
@keyframes shmSweep {
0% { transform: translateX(0); opacity: 0; }
12% { opacity: .75; }
70% { opacity: .75; }
88%, 100% { transform: translateX(196px); opacity: 0; }
}
.js-fx .dv-sweep { animation: shmSweep 2.8s cubic-bezier(.4,0,.2,1) infinite; }
/* 收货闪光(图 5驻留子 agent 接到消息时亮一下 */
.dv-recv {
fill: none;
stroke: var(--brand-cyan);
stroke-width: 2;
opacity: 0;
}
@keyframes recvFlash {
0%, 44% { opacity: 0; }
54% { opacity: .95; }
66% { opacity: .2; }
80%, 100% { opacity: 0; }
}
.js-fx .dv-recv { animation: recvFlash 2.4s cubic-bezier(.4,0,.2,1) infinite; }
/* 主 agent 忙碌:呼吸更慢更沉,与「被占住」呼应 */
.js-fx .dv-run { animation: busyBreathe 3.2s ease-in-out infinite; }
@keyframes busyBreathe {
0%, 100% { opacity: .82; }
50% { opacity: 1; }
}
/* ══ 外界光球(图 1══
语义:外面的一切都落在插件上,内核一列都没有。
故光球从画面外飞入、命中插件、激起涟漪 —— 让「隔离」这件事看得见。
四个插件各给一个颜色,命中时插件自己也闪一下,这样「打到谁」很清楚。 */
.dv-orbpath { fill: none; stroke: none; }
.dv-orb { filter: url(#fGlow); }
.dv-orb-a { fill: var(--brand-cyan); }
.dv-orb-b { fill: var(--brand-gold); }
.dv-orb-c { fill: var(--brand-violet); }
.dv-orb-d { fill: var(--brand-blue); }
/* 涟漪:从命中点向外扩一圈,很快淡去 */
.dv-ripple {
fill: none;
stroke: var(--brand-cyan);
stroke-width: 1.4;
}
/* 涟漪逐色(不能用 :nth-of-type——
同层还有数据包圆点也是 <circle>,按类型计数会错配,
实测四个涟漪全被染成金色。显式逐色才对得上各自的插件。 */
.dv-ripple-a { stroke: var(--brand-cyan); }
.dv-ripple-b { stroke: var(--brand-gold); }
.dv-ripple-c { stroke: var(--brand-violet); }
.dv-ripple-d { stroke: var(--brand-blue); }
/* 命中闪光:贴在插件框上,短暂高亮后回落 */
.dv-hit {
fill: none;
stroke-width: 2.2;
stroke-linecap: round;
opacity: 0;
}
.dv-hit-a { stroke: var(--brand-cyan); }
.dv-hit-b { stroke: var(--brand-gold); }
.dv-hit-c { stroke: var(--brand-violet); }
.dv-hit-d { stroke: var(--brand-blue); }
@keyframes hitFlash {
0%, 28% { opacity: 0; }
34% { opacity: 1; }
46% { opacity: .25; }
60%, 100% { opacity: 0; }
}
.js-fx .dv-hit { animation: hitFlash 3.6s cubic-bezier(.4,0,.2,1) infinite; }
.js-fx .dv-hit-a { animation-delay: 0s; }
.js-fx .dv-hit-b { animation-delay: .9s; }
.js-fx .dv-hit-c { animation-delay: 1.8s; }
.js-fx .dv-hit-d { animation-delay: 2.7s; }
/* 注意:这些 SMIL 动画不受 CSS 的 prefers-reduced-motion 约束,
reduce 下由 JS 调 pauseAnimations() 统一停(见脚本里的说明)。 */
/* ⑥ 节点呼吸:内核 / 主 agent 保持轻微脉动,图不平 */
@keyframes nodeBreathe {
0%, 100% { opacity: .88; }
50% { opacity: 1; }
}
.dv-accent { animation: nodeBreathe 4.2s var(--dvc) infinite; }
/* reduce 下全停 —— 不只是虚线,上面这些都停 */
@media (prefers-reduced-motion: reduce) {
.alt-visual svg .dv-anim,
.dv-plug, .dv-no path, .dv-x, .dv-seg, .dv-hd,
.dv-pk-dot, .dv-msg, .dv-accent, .dv-pk {
animation: none !important;
}
}
.dv-pkcap {
fill: var(--brand-gold);
font-size: 9.5px;
text-anchor: middle;
letter-spacing: 0.08em;
}
/* 排队类:无级别,用虚线框 + 不发光,与中断类刻意拉开 */
.dv-queue {
fill: none;
stroke: var(--border);
stroke-width: 1.3;
stroke-dasharray: 5 4;
}
.dv-qitem { fill: var(--dv-queue-item); opacity: 0.95; }
/* 队列条逐条被取走:表现「先到先处理」。
用 opacity 而非位移 —— 条是 <rect>,位移会露出底色缺口。 */
@keyframes queueTake {
0%, 100% { opacity: .95; }
45% { opacity: .2; }
}
.js-fx .dv-qitem rect { animation: queueTake 3.2s ease-in-out infinite; }
.js-fx .dv-qitem rect:nth-child(1) { animation-delay: 0s; }
.js-fx .dv-qitem rect:nth-child(2) { animation-delay: .4s; }
.js-fx .dv-qitem rect:nth-child(3) { animation-delay: .8s; }
/* 处理方向指示 */
.dv-qdir { fill: none; stroke: var(--text-muted); stroke-width: 1.2; opacity: 0.6; }
.dv-qdir-tip { fill: none; stroke: var(--text-muted); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; opacity: 0.6; }
/* 两列标题 */
.dv-colcap { fill: var(--text-dim); font-size: 10px; text-anchor: middle; letter-spacing: 0.04em; }
.dv-colcap-dim { fill: var(--text-muted); }
@media (prefers-reduced-motion: no-preference) {
/* 只呼吸透明度,不动 r。
为何:.dv-pkdot 现在兼做「沿链路行进的数据包」(靠 SMIL 的
animateMotion 定位),若再用 CSS 动 r包在跑的同时忽大忽小
看着像闪烁的噪点。半径交给标记本身。 */
.dv-pkdot { animation: pkPulse 1.8s ease-in-out infinite; }
@keyframes pkPulse {
0%, 100% { opacity: 1; }
50% { opacity: .5; }
}
}
/* ── 三层记忆:用品牌三色 ── */
.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;
width: fit-content;
min-width: 21rem;
margin: 0.5rem auto 0;
padding: 1.7rem 1.6rem 2.4rem;
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);
}
/* 单列。曾试过两列填满宽度,但 grid 按行填充会把编号排成
1/3/5 · 2/4/6 —— 这是线性流程,顺序不能乱。 */
.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;
}
/* 窄屏:中轴线收边,节点撑满 */
@media (max-width: 620px) {
.flow { padding: 1.2rem 1rem 1.1rem; }
.flow::before { display: none; }
.n { width: auto; max-width: none; }
.br { 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 ──
双列是「一滑一页」的硬约束逼出来的:末尾 faq(635px) + footer(367px)
= 1002px > 一屏 828pxmandatory 只允许停在吸附点,于是二者不可兼得
—— 实测要么标题被导航栏压住h2 顶=-57要么永远滚不到页脚。
六条改成两列省下约 190px末尾整段才装得进一屏。单列在窄屏恢复。 */
.faq-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.6rem 1.1rem;
align-items: start;
margin-top: 1.6rem;
}
details.faq {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 0.8rem 1rem;
margin-bottom: 0;
}
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);
/* 收紧上下内边距:末尾空间要与最后一个 section 一起装进一屏,
否则 mandatory 会把该节标题吸到导航栏底下(实测 h2 顶=-57导航底=67。 */
padding: 2rem 0 2.2rem;
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; }
/* ── 交错行:文字与示意图从两侧滑入 ──
行本身只做很轻的上浮,重点放在内部两列的相向入场,
这样「左文右图」的对应关系在动的那一下就被看见了。 */
/* ── 交错行:文与图各自从「自己所在的那一侧」滑入 ──
方向由 .flip 推导,而不是手写 slide-l/slide-r ——
手写版本踩过坑:翻转子行里图在**左边**却写了 slide-r从右进
于是图穿过文字从右侧飞入,方向与布局相反。
退出时不加 .intransform 回到同侧,所以「从左进就从左退」是自动的。 */
.js-fx .alt .alt-copy,
.js-fx .alt .alt-visual {
opacity: 0;
transition: opacity 0.85s var(--ease), transform 0.9s var(--ease);
transition-delay: var(--d, 110ms);
will-change: transform, opacity;
}
/* 非翻转:文左、图右 */
.js-fx .alt:not(.flip) .alt-copy { transform: translateX(-42px); }
.js-fx .alt:not(.flip) .alt-visual { transform: translateX(42px); }
/* 翻转:图左、文右 —— 方向跟着换 */
.js-fx .alt.flip .alt-copy { transform: translateX(42px); }
.js-fx .alt.flip .alt-visual { transform: translateX(-42px); }
/* 入场完成 */
.js-fx .alt.in .alt-copy,
.js-fx .alt.in .alt-visual { opacity: 1; transform: none; }
/* 交错行自身:仅轻微上浮,不抢内部动画 */
.js-fx .alt.rise { transform: translateY(14px); }
/* ── 卡片错落入场:四张依次亮起,不用手写 --d ── */
.js-fx .duties.in .duty:nth-child(1) { transition-delay: 0ms; }
.js-fx .duties.in .duty:nth-child(2) { transition-delay: 90ms; }
.js-fx .duties.in .duty:nth-child(3) { transition-delay: 180ms; }
.js-fx .duties.in .duty:nth-child(4) { transition-delay: 270ms; }
/* ── 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>
<!-- 共享 SVG defs五张示意图共用避免每张各写一套渐变。
hidden 而非 display:none —— display:none 会让被引用的渐变/滤镜失效。 -->
<svg width="0" height="0" aria-hidden="true" focusable="false"
style="position:absolute;pointer-events:none">
<defs>
<!-- 卡片面:上浅下深的竖向渐变,营造受光感 -->
<linearGradient id="gCard" x1="0" y1="0" x2="0" y2="1">
<stop class="g-card-a" offset="0" />
<stop class="g-card-b" offset="1" />
</linearGradient>
<linearGradient id="gCyan" x1="0" y1="0" x2="1" y2="1">
<stop class="g-cyan-a" offset="0" />
<stop class="g-cyan-b" offset="1" />
</linearGradient>
<linearGradient id="gBlue" x1="0" y1="0" x2="1" y2="1">
<stop class="g-blue-a" offset="0" />
<stop class="g-blue-b" offset="1" />
</linearGradient>
<linearGradient id="gViolet" x1="0" y1="0" x2="1" y2="1">
<stop class="g-violet-a" offset="0" />
<stop class="g-violet-b" offset="1" />
</linearGradient>
<linearGradient id="gGold" x1="0" y1="0" x2="1" y2="1">
<stop class="g-gold-a" offset="0" />
<stop class="g-gold-b" offset="1" />
</linearGradient>
<!-- 高光:顶部一道白,模拟玻璃反光 -->
<linearGradient id="gSheen" x1="0" y1="0" x2="0" y2="1">
<stop class="g-sheen-a" offset="0" />
<stop class="g-sheen-b" offset="1" />
</linearGradient>
<!-- 发光:给强调节点一圈柔光 -->
<filter id="fGlow" x="-40%" y="-40%" width="180%" height="180%">
<feGaussianBlur stdDeviation="4" result="b" />
<feMerge><feMergeNode in="b" /><feMergeNode in="SourceGraphic" /></feMerge>
</filter>
<!-- 投影:让卡片浮起来 -->
<filter id="fShadow" x="-30%" y="-30%" width="160%" height="170%">
<feDropShadow dx="0" dy="3" stdDeviation="3.5" class="f-shadow" />
</filter>
<!-- 悬停提升:更远更柔的阴影 + 一层青色泛光 -->
<filter id="fLift" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="6" stdDeviation="7" class="f-shadow" />
<feGaussianBlur in="SourceAlpha" stdDeviation="5" result="g" />
<feFlood class="f-glowtint" result="c" />
<feComposite in="c" in2="g" operator="in" result="gl" />
<feMerge><feMergeNode in="gl" /><feMergeNode in="SourceGraphic" /></feMerge>
</filter>
</defs>
</svg>
<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 rise-zoom">
<span class="duty-k">隔离</span>
<h3>崩一个,不赔上全部</h3>
<p>插件不在内核里,是另一个进程。崩了就把它注册的东西一并摘掉,其余照跑。重启退避 1s / 2s / 3s5 分钟内第 4 次崩溃则停下等人。</p>
</article>
<article class="duty rise-zoom">
<span class="duty-k">调度</span>
<h3>拦得住,也接得回</h3>
<p>输入分能等与不能等。不能等的可抢占,被打断的现场压栈,稍后<strong>原样恢复</strong>。L4 内核独占,外部插件声明 L4 会被夹到 L3。</p>
</article>
<article class="duty rise-zoom">
<span class="duty-k">通信</span>
<h3>三条道,各走各的</h3>
<p>控制走 stdio数据走共享内存通知走事件环。变长内容只传偏移两边映射到不同地址也能读。实测整段写入 + 读回 3.5µs。</p>
</article>
<article class="duty rise-zoom">
<span class="duty-k">资源</span>
<h3>记忆不会越用越肿</h3>
<p>上下文按相关性取舍,最近 10 条恒受保护。低分下沉成文档,冷文档再蒸馏成三元组。有效工作区间上限 60 万 token。</p>
</article>
</div>
</div>
</section>
<!-- ── 设计决定 01 ── -->
<section id="zero-io">
<div class="wrap">
<!-- ① 文左 图右 -->
<div class="alt reveal rise">
<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>
<!-- 涟漪在插件框**下层**:框会盖住内圈,露在外面的那圈
看起来就是「从插件背后漾开的冲击波」,而不是框里画了个圆。 -->
<g class="dv-ripples">
<circle class="dv-ripple dv-ripple-a" r="1">
<animateMotion dur="3.6s" begin="0s" repeatCount="indefinite"><mpath href="#orbPathA" /></animateMotion>
<animate attributeName="r" dur="3.6s" begin="0s" repeatCount="indefinite" values="0;0;14;0" keyTimes="0;0.30;0.52;1" />
<animate attributeName="opacity" dur="3.6s" begin="0s" repeatCount="indefinite" values="0;0;.55;0" keyTimes="0;0.30;0.36;0.55" />
</circle>
<circle class="dv-ripple dv-ripple-b" r="1">
<animateMotion dur="3.6s" begin="0.9s" repeatCount="indefinite"><mpath href="#orbPathB" /></animateMotion>
<animate attributeName="r" dur="3.6s" begin="0.9s" repeatCount="indefinite" values="0;0;14;0" keyTimes="0;0.30;0.52;1" />
<animate attributeName="opacity" dur="3.6s" begin="0.9s" repeatCount="indefinite" values="0;0;.55;0" keyTimes="0;0.30;0.36;0.55" />
</circle>
<circle class="dv-ripple dv-ripple-c" r="1">
<animateMotion dur="3.6s" begin="1.8s" repeatCount="indefinite"><mpath href="#orbPathC" /></animateMotion>
<animate attributeName="r" dur="3.6s" begin="1.8s" repeatCount="indefinite" values="0;0;14;0" keyTimes="0;0.30;0.52;1" />
<animate attributeName="opacity" dur="3.6s" begin="1.8s" repeatCount="indefinite" values="0;0;.55;0" keyTimes="0;0.30;0.36;0.55" />
</circle>
<circle class="dv-ripple dv-ripple-d" r="1">
<animateMotion dur="3.6s" begin="2.7s" repeatCount="indefinite"><mpath href="#orbPathD" /></animateMotion>
<animate attributeName="r" dur="3.6s" begin="2.7s" repeatCount="indefinite" values="0;0;14;0" keyTimes="0;0.30;0.52;1" />
<animate attributeName="opacity" dur="3.6s" begin="2.7s" repeatCount="indefinite" values="0;0;.55;0" keyTimes="0;0.30;0.36;0.55" />
</circle>
</g>
<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 id="pathIso1" class="dv-line dv-dash" d="M112 130 H74" />
<path id="pathIso2" class="dv-line dv-dash" d="M286 130 H248" />
<!-- 数据包沿链路真跑:用 SMIL 的 mpath 贴合上面那条路径,
不靠 JS 也不靠 offset-pathSafari 支持不齐)。 -->
<circle class="dv-pkdot dv-anim" r="3.2">
<animateMotion dur="2.6s" repeatCount="indefinite" keyPoints="0;1" keyTimes="0;1" calcMode="linear">
<mpath href="#pathIso1" />
</animateMotion>
</circle>
<circle class="dv-pkdot dv-anim" r="3.2">
<animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite" keyPoints="1;0" keyTimes="0;1" calcMode="linear">
<mpath href="#pathIso2" />
</animateMotion>
</circle>
<!-- ── 外界光球 ──
语义:外面的一切都落在插件上,内核一列都没有。
所以光球从四个方向(画面外)飞入,各自命中一个插件,
四种颜色对应四个插件;错开时序后看起来像随机到达。
击中瞬间激起一圈涟漪 + 插件自己闪一下,让"命中"可感知。 -->
<g class="dv-orbs">
<!-- ① 左上插件:光球自左上角飞入 -->
<circle class="dv-orb dv-orb-a" r="4">
<animateMotion id="orbA" dur="3.6s" begin="0s" repeatCount="indefinite"
keyPoints="0;0;1;1" keyTimes="0;0.10;0.34;1" calcMode="spline"
keySplines="0.4 0 1 1;0.4 0 0.15 1;0.4 0 1 1">
<mpath href="#orbPathA" />
</animateMotion>
<animate attributeName="opacity" dur="3.6s" begin="0s" repeatCount="indefinite"
values="0;0;1;1;0;0" keyTimes="0;0.16;0.22;0.30;0.36;1" />
</circle>
<!-- ② 右上插件:自右上角飞入 -->
<circle class="dv-orb dv-orb-b" r="4">
<animateMotion id="orbB" dur="3.6s" begin="0.9s" repeatCount="indefinite"
keyPoints="0;0;1;1" keyTimes="0;0.10;0.34;1" calcMode="spline"
keySplines="0.4 0 1 1;0.4 0 0.15 1;0.4 0 1 1">
<mpath href="#orbPathB" />
</animateMotion>
<animate attributeName="opacity" dur="3.6s" begin="0.9s" repeatCount="indefinite"
values="0;0;1;1;0;0" keyTimes="0;0.16;0.22;0.30;0.36;1" />
</circle>
<!-- ③ 左下插件 -->
<circle class="dv-orb dv-orb-c" r="4">
<animateMotion dur="3.6s" begin="1.8s" repeatCount="indefinite"
keyPoints="0;0;1;1" keyTimes="0;0.10;0.34;1" calcMode="spline"
keySplines="0.4 0 1 1;0.4 0 0.15 1;0.4 0 1 1">
<mpath href="#orbPathC" />
</animateMotion>
<animate attributeName="opacity" dur="3.6s" begin="1.8s" repeatCount="indefinite"
values="0;0;1;1;0;0" keyTimes="0;0.16;0.22;0.30;0.36;1" />
</circle>
<!-- ④ 右下插件 -->
<circle class="dv-orb dv-orb-d" r="4">
<animateMotion dur="3.6s" begin="2.7s" repeatCount="indefinite"
keyPoints="0;0;1;1" keyTimes="0;0.10;0.34;1" calcMode="spline"
keySplines="0.4 0 1 1;0.4 0 0.15 1;0.4 0 1 1">
<mpath href="#orbPathD" />
</animateMotion>
<animate attributeName="opacity" dur="3.6s" begin="2.7s" repeatCount="indefinite"
values="0;0;1;1;0;0" keyTimes="0;0.16;0.22;0.30;0.36;1" />
</circle>
</g>
<!-- 四条飞入轨迹(不可见,仅作 mpath 用)。
终点取各插件框的中心:左上(74,71) 右上(286,71) 左下(74,179) 右下(286,179)
起点在画面外,故光球「从外面来」。 -->
<path id="orbPathA" class="dv-orbpath" d="M-30 -10 C 10 26, 44 52, 74 71" />
<path id="orbPathB" class="dv-orbpath" d="M392 -10 C 350 26, 316 52, 286 71" />
<path id="orbPathC" class="dv-orbpath" d="M-30 262 C 10 226, 44 200, 74 179" />
<path id="orbPathD" class="dv-orbpath" d="M392 262 C 350 226, 316 200, 286 179" />
<!-- 命中闪光:与光球同一时序,贴在插件上 -->
<rect class="dv-hit dv-hit-a" x="36" y="52" width="76" height="38" rx="8" />
<rect class="dv-hit dv-hit-b" x="248" y="52" width="76" height="38" rx="8" />
<rect class="dv-hit dv-hit-c" x="36" y="160" width="76" height="38" rx="8" />
<rect class="dv-hit dv-hit-d" x="248" y="160" width="76" height="38" rx="8" />
</svg>
</div>
</div>
</div>
</section>
<!-- ── 设计决定 02 ── -->
<section id="zero-dep">
<div class="wrap">
<!-- ② 文右 图左 -->
<div class="alt flip reveal rise">
<div class="alt-copy">
<span class="alt-n">02</span>
<h3>部署,从未如此便捷</h3>
<p>
插件是<strong>静态编译的单个文件</strong>:目标机上没有 Go、没有 libc、
没有容器、没有运行时,拷过去就能跑。
本体同样不要求你先装什么 —— 没有数据库、没有缓存、没有消息队列,
记忆与索引都在进程内。所谓部署,就是把文件放上去。
</p>
</div>
<div class="alt-visual">
<svg class="dv-deps" 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 pathLength="1" stroke-dasharray="1" d="M200 58 L332 76" /><path pathLength="1" stroke-dasharray="1" d="M200 98 L332 116" />
<path pathLength="1" stroke-dasharray="1" d="M200 138 L332 156" /><path pathLength="1" stroke-dasharray="1" d="M200 178 L332 196" />
</g>
<text class="dv-x" x="266" y="42">都不需要</text>
</svg>
</div>
</div>
</div>
</section>
<!-- ── 设计决定 03 ── -->
<section id="shm">
<div class="wrap">
<!-- ③ 文左 图右:共享内存 -->
<div class="alt reveal rise">
<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="共享内存段加偏移解引用,实现零拷贝">
<defs>
<linearGradient id="gPage" x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stop-color="var(--brand-cyan)" stop-opacity="0.05"/>
<stop offset="0.5" stop-color="var(--brand-cyan)" stop-opacity="0.13"/>
<stop offset="1" stop-color="var(--brand-cyan)" stop-opacity="0.05"/>
</linearGradient>
</defs>
<!-- 两端进程:地址标注放在卡片**上方**。
放下面会与竖向连接线重叠(实测两处文字都压在线上)。 -->
<text class="dv-addr" x="24" y="22">0x7f2a…</text>
<text class="dv-addr dv-addr-r" x="336" y="22">0x55c1…</text>
<g>
<rect class="dv-box dv-accent" x="20" y="30" width="104" height="54" rx="10" />
<rect class="dv-sheen" x="20" y="30" width="104" height="24" rx="10" />
<text class="dv-ts" x="72" y="55">homed</text>
<text class="dv-s" x="72" y="73">进程 A</text>
</g>
<g>
<rect class="dv-box dv-accent" x="236" y="30" width="104" height="54" rx="10" />
<rect class="dv-sheen" x="236" y="30" width="104" height="24" rx="10" />
<text class="dv-ts" x="288" y="55">plugin.bin</text>
<text class="dv-s" x="288" y="73">进程 B</text>
</g>
<path class="dv-line dv-dash" d="M72 104 V128" />
<path class="dv-line dv-dash" d="M288 104 V128" />
<!-- 点题的桥接放在两列之间:两侧各有自己的虚拟地址,
但落点是同一块物理页 —— 这才是「零拷贝」的关键。
原先左右各一句「各自的虚拟地址」既冗余、又被竖向虚线穿过。 -->
<text class="dv-bridge" x="180" y="104">不同虚拟地址 · 同一物理页</text>
<!-- 同一物理页 -->
<rect class="dv-page-fill" x="20" y="128" width="320" height="98" rx="12" />
<rect class="dv-phys" x="20" y="128" width="320" height="98" rx="12" />
<text class="dv-physcap" x="180" y="146">段布局:定长头 + 变长 arena</text>
<rect class="dv-box dv-hd" x="34" y="158" width="66" height="52" rx="7" />
<rect class="dv-sheen" x="34" y="158" width="66" height="22" rx="7" />
<text class="dv-ts" x="67" y="180">header</text>
<text class="dv-labs" x="67" y="196">魔数 · 版本</text>
<rect class="dv-box dv-ar" x="108" y="158" width="218" height="52" rx="7" />
<rect class="dv-sheen" x="108" y="158" width="218" height="22" rx="7" />
<text class="dv-ts" x="217" y="178">arena</text>
<text class="dv-labs" x="217" y="194">变长数据 · {off, len} 相对基址</text>
<!-- 字节刻度:把「一片区域」讲成「有结构的区域」 -->
<g class="dv-ticks">
<path d="M126 202 v6" /><path d="M152 202 v9" /><path d="M178 202 v6" />
<path d="M204 202 v9" /><path d="M230 202 v6" /><path d="M256 202 v9" />
<path d="M282 202 v6" /><path d="M308 202 v9" />
</g>
<!-- 写入 → 读回 -->
<path id="writePath" class="dv-pk dv-dash" d="M72 86 V158" />
<path id="readPath" class="dv-pk dv-dash" d="M288 86 V158" />
<circle class="dv-pkdot dv-anim" r="3.4">
<animateMotion dur="2.8s" repeatCount="indefinite"><mpath href="#writePath" /></animateMotion>
</circle>
<circle class="dv-pkdot dv-anim" r="3.4">
<animateMotion dur="2.8s" begin="1.4s" repeatCount="indefinite"><mpath href="#readPath" /></animateMotion>
</circle>
<rect class="dv-sweep dv-anim" x="108" y="158" width="40" height="52" rx="7" />
<!-- 同页提示:外框自身做极缓的呼吸,不再画第二条巡行线
(巡行框看起来像页内又多了一层框,反而乱)。 -->
<rect class="dv-phys-glow dv-anim" x="20" y="128" width="320" height="98" rx="12" />
<text class="dv-x" x="180" y="243">零拷贝:不序列化、不复制</text>
</svg>
</div>
</div>
</div>
</section>
<!-- ── 设计决定 04 ── -->
<section id="priority">
<div class="wrap">
<!-- ④ 文右 图左 -->
<div class="alt flip reveal rise">
<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="输入分排队与中断两类别,中断带 L1L4 四级">
<!-- 两列标题 -->
<text class="dv-colcap" x="94" y="20">中断 · 带级别</text>
<text class="dv-colcap dv-colcap-dim" x="266" y="20">排队 · 无级别</text>
<!-- 左列:四级中断。强度条做背景,越靠上越亮 -->
<g class="dv-gauge">
<rect class="dv-gauge-bg" x="14" y="26" width="160" height="124" rx="9" />
<g class="dv-gauge-fill">
<rect x="14" y="120" width="160" height="30" class="dv-g1" />
<rect x="14" y="90" width="160" height="30" class="dv-g2" />
<rect x="14" y="60" width="160" height="30" class="dv-g3" />
<rect x="14" y="26" width="160" height="34" class="dv-g4" />
</g>
</g>
<g class="dv-lv">
<rect class="dv-box dv-l4" x="22" y="31" width="144" height="24" rx="6" />
<text class="dv-badge" x="32" y="47">L4</text>
<text class="dv-tsl" x="56" y="47">内核独占</text>
<rect class="dv-box dv-l3" x="22" y="61" width="144" height="24" rx="6" />
<text class="dv-badge" x="32" y="77">L3</text>
<text class="dv-tsl" x="56" y="77">需及时处理</text>
<rect class="dv-box dv-l2" x="22" y="91" width="144" height="24" rx="6" />
<text class="dv-badge" x="32" y="107">L2</text>
<text class="dv-tsl" x="56" y="107">消息类</text>
<rect class="dv-box dv-l1" x="22" y="121" width="144" height="24" rx="6" />
<text class="dv-badge" x="32" y="137">L1</text>
<text class="dv-tsl" x="56" y="137">完全可等</text>
</g>
<!-- 右列:排队。无级别,故用虚线框与素色条,与左侧拉开区分 -->
<rect class="dv-queue" x="186" y="26" width="160" height="124" rx="9" />
<g class="dv-qitem">
<rect x="198" y="38" width="136" height="17" rx="4" />
<rect x="198" y="59" width="136" height="17" rx="4" />
<rect x="198" y="80" width="136" height="17" rx="4" />
</g>
<!-- 处理方向:左侧竖线 + 向下箭头,表示先到先处理
(之前在条内画横向虚线,等于穿过了第一个队列条) -->
<path class="dv-qdir" d="M340 38 V97" />
<path class="dv-qdir-tip" d="M336 93 L340 98 L344 93" />
<text class="dv-cap" x="266" y="114">先到先处理</text>
<!-- 两路汇入正在跑的任务 -->
<path class="dv-line" d="M94 150 V176 H180" />
<path class="dv-line" d="M266 150 V176 H180" />
<path class="dv-arrow" d="M180 176 V186" />
<rect class="dv-box dv-run" x="40" y="188" width="280" height="38" rx="9" />
<text class="dv-tsl" x="58" y="204">正在跑的任务</text>
<text class="dv-end" x="302" y="204">同一时刻只一个</text>
<!-- 插队动线:从排队列(右)绕到中断列(左)各级前。
这是本图的核心语义 —— 「任何中断都能插它前面」,
静态版只能靠一行小字说明,动起来才看得见。 -->
<path id="pkPath4" class="dv-pkline dv-anim"
d="M258 100 C 210 100, 200 47, 168 47" />
<path id="pkPath3" class="dv-pkline dv-anim"
d="M258 100 C 214 100, 202 77, 168 77" />
<path id="pkPath2" class="dv-pkline dv-anim"
d="M258 100 C 218 100, 206 107, 168 107" />
<path id="pkPath1" class="dv-pkline dv-anim"
d="M258 100 C 222 100, 210 137, 168 137" />
<circle class="dv-pkdot dv-anim" r="3">
<animateMotion dur="4.2s" repeatCount="indefinite"><mpath href="#pkPath4" /></animateMotion>
</circle>
<circle class="dv-pkdot dv-anim" r="3">
<animateMotion dur="4.2s" begin="0.5s" repeatCount="indefinite"><mpath href="#pkPath3" /></animateMotion>
</circle>
<circle class="dv-pkdot dv-anim" r="3">
<animateMotion dur="4.2s" begin="1.0s" repeatCount="indefinite"><mpath href="#pkPath2" /></animateMotion>
</circle>
<circle class="dv-pkdot dv-anim" r="3">
<animateMotion dur="4.2s" begin="1.5s" repeatCount="indefinite"><mpath href="#pkPath1" /></animateMotion>
</circle>
</svg>
</div>
</div>
</div>
</section>
<!-- ── 设计决定 05 ── -->
<section id="offload">
<div class="wrap">
<!-- ④ 文右 图左 -->
<div class="alt flip reveal rise">
<div class="alt-copy">
<span class="alt-n">05</span>
<h3>忙时,有人替你接住</h3>
<p>
主 agent 被长任务占住、积压又够多时,内核把这些消息转给一个<strong>驻留子 agent</strong>。能当场办的就办了;
非等主 agent 不可的,立刻回一句「忙碌中,请稍候」。
你不必对着对话框猜它还在不在。
</p>
</div>
<div class="alt-visual">
<svg viewBox="0 0 360 250" role="img" aria-label="主 agent 忙时把积压转投给驻留子 agent两种出路让用户不必干等">
<!-- 主 agent长任务占住带一条一直爬不到头的忙碌条 -->
<rect class="dv-box dv-run" x="16" y="18" width="150" height="56" rx="9" />
<rect class="dv-sheen" x="16" y="18" width="150" height="24" rx="9" />
<text class="dv-ts" x="91" y="40">主 agent</text>
<text class="dv-s" x="91" y="56">长任务 · 占住很久</text>
<rect class="dv-busy-track" x="28" y="63" width="126" height="4" rx="2" />
<rect class="dv-busy-fill" x="28" y="63" height="4" rx="2">
<animate attributeName="width" dur="4.5s" repeatCount="indefinite"
values="0;126;126" keyTimes="0;0.74;1" />
</rect>
<!-- 触发条件:填住右上原本空着的一块,且给的是源码实测默认值
defaultOffloadBusyAfter = 5mindefaultOffloadMinPending = 3-->
<rect class="dv5-trigger" x="180" y="26" width="164" height="42" rx="9" />
<text class="dv5-tcap" x="262" y="44">触发条件</text>
<text class="dv5-tval" x="262" y="60">忙超 5 分钟 · 积压 ≥ 3 条</text>
<!-- 积压消息:三条,宽度递减,像越堆越窄的一摞 -->
<g class="dv5-pile">
<rect class="dv-box" x="16" y="88" width="150" height="20" rx="6" />
<text class="dv-s" x="91" y="102">积压消息 ×3</text>
<rect class="dv-box" x="16" y="112" width="132" height="20" rx="6" />
<text class="dv-s" x="82" y="126">积压消息 ×4</text>
<rect class="dv-box" x="16" y="136" width="114" height="20" rx="6" />
<text class="dv-s" x="73" y="150">积压消息 ×2</text>
</g>
<!-- 驻留子 agent -->
<rect class="dv-box dv-accent" x="200" y="86" width="144" height="70" rx="9" />
<rect class="dv-sheen" x="200" y="86" width="144" height="24" rx="9" />
<text class="dv-ts" x="272" y="108">驻留子 agent</text>
<text class="dv-s" x="272" y="136">当场办 / 立刻答</text>
<rect class="dv-recv" x="200" y="86" width="144" height="70" rx="9" />
<!-- 转投 -->
<path id="handoffPath" class="dv-pk dv-dash" d="M166 122 H200" />
<text class="dv-lab" x="183" y="116">转投</text>
<circle class="dv-pkdot dv-anim" r="3.4">
<animateMotion dur="2.4s" repeatCount="indefinite"><mpath href="#handoffPath" /></animateMotion>
</circle>
<circle class="dv-pkdot dv-anim" r="3.4">
<animateMotion dur="2.4s" begin="1.2s" repeatCount="indefinite"><mpath href="#handoffPath" /></animateMotion>
</circle>
<!-- ── 下半部:两种出路,最后都回到用户 ──
原文案只说「能办的直接办、需要主 agent 的立刻回请稍候」,
但图上没有任何一处表达它。这里把它做成回执区,
顺带填掉原来空着的下三分之一。 -->
<path class="dv-line" d="M272 156 V176" />
<!-- 回执区外框 -->
<rect class="dv5-band" x="16" y="176" width="336" height="58" rx="10" />
<!-- 用户 -->
<rect class="dv-box" x="24" y="186" width="76" height="38" rx="8" />
<text class="dv-ts" x="62" y="204">用户</text>
<!-- 回执 chip两种出路 -->
<rect class="dv5-chip" x="128" y="182" width="216" height="46" rx="8" />
<text class="dv5-out1" x="236" y="200">① 简单 → 直接办完,发回原通道</text>
<text class="dv5-out2" x="236" y="218">② 需主 agent → 回「忙碌中,请稍候」</text>
<!-- 回执送达用户 -->
<path id="replyPath" class="dv-arrow" d="M128 205 H102" />
<circle class="dv-linkdot dv-anim" r="2.8">
<animateMotion dur="2.6s" repeatCount="indefinite"><mpath href="#replyPath" /></animateMotion>
</circle>
</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>驻留子 agent</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></div>
</li>
<li class="seq-item">
<span class="seq-n">2</span>
<div class="seq-b"><b>LLM 调用</b></div>
</li>
<li class="seq-item">
<span class="seq-n">3</span>
<div class="seq-b"><code>post_action</code></div>
</li>
<li class="seq-item">
<span class="seq-n">4</span>
<div class="seq-b"><code>before_toolcall</code></div>
</li>
<li class="seq-item">
<span class="seq-n">5</span>
<div class="seq-b"><b>执行工具</b></div>
</li>
<li class="seq-item">
<span class="seq-n">6</span>
<div class="seq-b"><code>after_toolcall</code></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>
</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">在跑插件</div></div>
<div class="stat"><div class="n">340</div><div class="l">可用工具</div></div>
<div class="stat"><div class="n">109k</div><div class="l">Go 代码行</div></div>
<div class="stat"><div class="n">9</div><div class="l">LLM 适配器</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 faq-grid">
<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 忙超阈值(默认 5 分钟)且积压够多(默认 3 条)
时,内核可拉起临时驻留子接手:能办的直接办,需要主 agent 的立刻回「忙碌中,请稍候」。
这个自动转投<strong>默认关闭</strong>,由部署方显式打开 —— 它属于「内核替父做决策」,与
「显式才是特权」同一条理由。
</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();
/* ── SMIL 与 reduced-motion ──
CSS 的 prefers-reduced-motion 管不到 SMIL<animateMotion> 不是 CSS 动画),
实测 reduce 下 6 个数据包照样在跑。故在 JS 里显式停掉:
暂停整棵 SVG 时间轴pauseAnimations 对 SVG 文档生效)。 */
if (reduce) {
try {
document.querySelectorAll("svg").forEach(function (sv) {
if (sv.pauseAnimations) sv.pauseAnimations();
});
} catch (e) {}
}
/* ── 入场动画 ── */
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) {
// 双向:向下滚入场,向上滚退场 —— 不再 unobserve。
// 原先入场后 unobserve元素永远停在 .in所以“滚回去”没有任何退场动作
// 读起来就像一页页静态堆叠。
// 方向由视口交叉的位置判断(不用全局 scroll 方向,那样在快速滚动时会抖):
// boundingClientRect.top > 0 且向下 → 从下方进来
// boundingClientRect.top < 0 → 已滑到上方,停用
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) {
reveal(e.target);
} else {
// 仅当元素确实离开(而非只是未达阈值)才移除,避免边缘抖动
var r = e.boundingClientRect;
if (r.bottom < 0 || r.top > window.innerHeight) {
e.target.classList.remove("in");
}
}
});
}, { threshold: 0.12, 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);
});
});
// 比一屏高的节退出吸附。
// 为何在运行时做高度受字体、图片、视口影响CSS 无法预知。
// 不做的话吸附点不等距,累积误差会把后面的节顶到导航栏底下(实测 faq h2 被抬到 -57
var markTall = function () {
var limit = window.innerHeight - 8;
[].slice.call(document.querySelectorAll("section")).forEach(function (s) {
s.classList.toggle("too-tall", s.getBoundingClientRect().height > limit);
});
};
markTall();
window.addEventListener("load", markTall);
window.addEventListener("resize", markTall);
// FAQ 展开后可能远超一屏。此时若仍吸附mandatory 会把滚动锁在
// 吸附点上,末尾(含页脚)就再也到不了 —— 实测全部展开时卡住、
// 页脚不可见。故展开态一旦超一屏就让该节退出吸附。
var markFaqOpen = function () {
var faq = document.getElementById("faq");
if (!faq) return;
// 阈值不是「视口高」而是「视口高 页脚高」:
// 末尾的约束是 faq + footer 要一起装进一屏。用视口高当阈值时,
// 展开后 faq 高 712 < 892判不出超限类加不上踩过这个错
var ft = document.querySelector("footer");
var ftH = ft ? ft.getBoundingClientRect().height : 0;
var limit = window.innerHeight - ftH - 8;
var over = faq.getBoundingClientRect().height > limit;
// 注意是切 html 上的类(整页退出吸附),不是在 faq 上加类。
// 只让 faq 退出吸附会死锁:它下方没有吸附点,而 mandatory
// 只允许停在吸附点 —— 实测卡在 plugins 的吸附位不再前进。
document.documentElement.classList.toggle("faq-open", over);
};
markFaqOpen();
[].slice.call(document.querySelectorAll("details.faq")).forEach(function (d) {
d.addEventListener("toggle", markFaqOpen);
});
// 不再用「3 秒后强制全部 reveal」的兜底
// 那会把已退场的元素强行拉回来,退场动画就废了。
// 改为只在 observer 不存在时(已在上面分支处理)整体 reveal。
}
/* ── 数字滚动 ── */
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>