mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 10:28:06 +00:00
用户两点意见: 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)
3358 lines
160 KiB
HTML
3358 lines
160 KiB
HTML
<!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; }
|
||
/* 左对齐变体:L1–L4 那种「编号 + 说明」的行必须左对齐,
|
||
用 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; }
|
||
/* 级别徽标:L1–L4 */
|
||
.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 > 一屏 828px,mandatory 只允许停在吸附点,于是二者不可兼得
|
||
—— 实测要么标题被导航栏压住(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(从右进),
|
||
于是图穿过文字从右侧飞入,方向与布局相反。
|
||
退出时不加 .in,transform 回到同侧,所以「从左进就从左退」是自动的。 */
|
||
.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 / 3s,5 分钟内第 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-path(Safari 支持不齐)。 -->
|
||
<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>的带级别 L1–L4,级别高的可以抢占级别低的。
|
||
被抢占的现场压进栈,稍后原样恢复。
|
||
L4 内核独占,所以「停止」按下去就真的停。
|
||
</p>
|
||
</div>
|
||
<div class="alt-visual">
|
||
<svg viewBox="0 0 360 250" role="img" aria-label="输入分排队与中断两类别,中断带 L1–L4 四级">
|
||
<!-- 两列标题 -->
|
||
<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 = 5min,defaultOffloadMinPending = 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">两类别 · 四级中断 L1–L4</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+ 与 CGo(sqlite3)。内核需 Linux;Windows 上
|
||
<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 与 /a2a(JSON-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.0;v2.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 桥接 QQ:20 个工具覆盖消息、群/好友、文件传输。带权限模型 —— 身份绑在「帧」上而非插件全局(中断抢占恢复后不会丢身份),多来源合并取权限交集,敏感工具按前缀一律拒绝非所有者。</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_db(config.db 完整性 + LLM 源解析校验)、diag_log_scan(日志签名命中)、diag_delta(last-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>
|
||
因为同一时刻只应有一个任务在改上下文。同级抢占会让两边都做一半。
|
||
需要立刻插队时用更高级别的中断:插件声明 L1–L3,L4 是「立即打断」
|
||
(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>
|