From 09298886a267e1f64b2fed8ecf1445b7e12cdf43 Mon Sep 17 00:00:00 2001
From: JianFeeeee 图。它有三个问题:
1. **画不出循环**。真实执行序是 7 步状态机,其中工具循环要回到开头
再来一轮 —— ASCII 只能表达上下关系,这一点只能靠文字暗示。
2. **7 个钩子排成一行是错的**。on_input 在进管道前跑,
before/after_output 在**全部轮次结束后**才跑一次;把它们与管道内的
钩子并列,读起来像一条直线。
3. 漏掉了 post_action 之后才发生的工具调用,以及"上下文裁剪 + 相关记忆召回"
这一步(在 after_toolcall 里)。
## 现在的结构
五层节点 + 分支 + 循环体:
外部输入 → 输入调度器(三条分支:入队列 / 抢占 / 转投)
→ 处理管道 →〔① pre_action ② LLM ③ post_action ④ before_toolcall
⑤ 执行工具 ⑥ after_toolcall〕↻ 循环 → 三层记忆 → 输出通道
事实全部对照源码核过(不是照抄旧图):
- 7 个 Stage 常量取自 SDK `third_party/homeagent-sdk/sdk/plugin.go`
- 顺序取自内核 `internal/agent/core/task.go` 的 Step 状态机
(StepPrepare→StepLLM→StepToolBegin→StepToolExec→StepToolAfter→StepTurnEnd)
- 脚本末尾补一句说明 on_input / before_output / after_output 的时机
## 视觉
节点用色与三层记忆的三色一致(蓝=Context/青=管道/金=Graph,紫=转投);
连接线上的光点错峰下行,序号依次点亮。全部是内联 SVG-free 的纯 CSS,
无外部依赖。
## 关键取舍
- **删掉了贯穿全图的中轴线**:节点背景是半透明令牌,轴线会直接透出来,
实测在「处理管道」里穿过整个编号列表,看着像画错了。连接线本身就是主轴。
- **循环回边改为内嵌徽标**:先做成从框底绕出的弧线,但它会压到下一条
连接线 —— 同样像画错。
- **给连接线补了静态箭头**:动画关掉时(reduce)方向也要看得出来。
## 验证(独立 headless 实跑)
深浅双主题控制台错误 0、页面溢出 0;图内溢出 0(390/620/900);
**JS 禁用下 14 个节点全部可见、7 个钩子名齐全**(流程图是内容不是装饰);
reduce 下光点/图标动画确为 none 而静态箭头仍在(宽 7px/2px);
**7 个钩子名与 SDK 常量逐一比对通过**,防止文案漂移;
滚动到底未揭示元素 0。
---
site/index.html | 404 +++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 346 insertions(+), 58 deletions(-)
diff --git a/site/index.html b/site/index.html
index 564726d..c8c7975 100644
--- a/site/index.html
+++ b/site/index.html
@@ -316,27 +316,246 @@
.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.4rem;
- overflow-x: auto;
+ padding: 1.6rem 1.5rem 1.3rem;
}
- .flow pre {
- margin: 0;
+ /* 注:这里原来有一条贯穿全图的 ::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);
- font-size: 0.82rem;
- line-height: 1.7;
- color: var(--text-dim);
- white-space: pre;
+ 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: 2.1rem;
+ width: 2px;
+ margin: 0 auto;
+ background: var(--border-hi);
+ }
+ .lnk::before {
+ /* 静态箭头:reduce 下动画停了,方向也要看得出来 */
+ content: "";
+ position: absolute;
+ left: 50%; bottom: -1px;
+ width: 7px; height: 7px;
+ margin-left: -3.5px;
+ border-right: 2px solid var(--border-hi);
+ border-bottom: 2px solid var(--border-hi);
+ transform: rotate(45deg);
+ }
+ .lnk::after {
+ /* 下行的光点 */
+ content: "";
+ position: absolute;
+ left: 50%; top: 0;
+ width: 6px; height: 6px;
+ margin-left: -3px;
+ border-radius: 50%;
+ background: var(--brand-sky);
+ box-shadow: 0 0 8px var(--brand-sky);
+ animation: dotDown 2.4s var(--ease) infinite;
+ animation-delay: var(--d, 0s);
+ }
+ @keyframes dotDown {
+ 0% { top: 0; opacity: 0; }
+ 18% { opacity: 1; }
+ 82% { opacity: 1; }
+ 100% { top: 100%; opacity: 0; }
+ }
+
+ /* ── 调度分支 ── */
+ .br {
+ position: relative;
+ z-index: 1;
+ list-style: none;
+ max-width: 34rem;
+ margin: 0.55rem auto 0;
+ padding: 0;
+ display: grid;
+ gap: 0.28rem;
+ }
+ .br li {
+ position: relative;
+ font-size: 0.78rem;
+ color: var(--text-dim);
+ padding: 0.3rem 0.7rem 0.3rem 1.6rem;
+ border-left: 2px solid var(--border);
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
+ background: rgba(119, 128, 154, 0.06);
+ }
+ .br li::before {
+ content: "└";
+ position: absolute;
+ left: 0.42rem;
+ color: var(--text-muted);
+ font-family: var(--font-mono);
+ }
+ .br li b { color: var(--text); font-weight: 600; }
+ .br li i { font-style: normal; font-weight: 600; }
+ .br-que i, .br-que b { color: var(--text-dim); }
+ .br-pre { border-left-color: rgba(245, 158, 11, 0.55); }
+ .br-pre i { color: var(--brand-gold); }
+ .br-off { border-left-color: rgba(139, 92, 246, 0.55); }
+ .br-off i { color: var(--brand-violet); }
+
+ /* ── 工具循环 ── */
+ .loop {
+ position: relative;
+ z-index: 1;
+ max-width: 34rem;
+ margin: 0.75rem auto 0;
+ padding: 2.4rem 1rem 2.6rem;
+ border: 1px dashed rgba(6, 182, 212, 0.4);
+ border-radius: var(--radius-md);
+ background: rgba(6, 182, 212, 0.045);
+ }
+ .loop-tag {
+ position: absolute;
+ top: -0.68rem; left: 1rem;
+ padding: 0.12rem 0.55rem;
+ font-size: 0.7rem;
+ font-family: var(--font-mono);
+ color: var(--brand-cyan);
+ background: var(--bg);
+ border: 1px solid rgba(6, 182, 212, 0.4);
+ border-radius: var(--radius-pill);
+ }
+ .seq {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 0.34rem;
+ }
+ .seq-item {
+ display: grid;
+ grid-template-columns: 1.5rem 1fr;
+ gap: 0.6rem;
+ align-items: baseline;
+ }
+ .seq-n {
+ display: grid;
+ place-items: center;
+ width: 1.5rem; height: 1.5rem;
+ border-radius: 50%;
+ font-size: 0.7rem;
+ font-family: var(--font-mono);
+ color: var(--brand-cyan);
+ background: rgba(6, 182, 212, 0.14);
+ border: 1px solid rgba(6, 182, 212, 0.3);
+ }
+ .seq-b { font-size: 0.82rem; color: var(--text); line-height: 1.5; }
+ .seq-b b { font-weight: 600; }
+ .seq-b code { font-size: 0.78rem; }
+ .seq-b em {
+ display: block;
+ font-style: normal;
+ font-size: 0.74rem;
+ color: var(--text-muted);
+ }
+ /* 循环回边:内嵌徽标。
+ 曾试过从框底绕出去的弧线,但它会压到下一条连接线,看着像画错了。 */
+ .loop-back {
+ position: absolute;
+ right: 0.7rem; bottom: 0.7rem;
+ display: flex;
+ align-items: center;
+ gap: 0.34rem;
+ padding: 0.2rem 0.6rem;
+ font-size: 0.7rem;
+ font-family: var(--font-mono);
+ color: var(--brand-cyan);
+ background: rgba(6, 182, 212, 0.12);
+ border: 1px solid rgba(6, 182, 212, 0.35);
+ border-radius: var(--radius-pill);
+ white-space: nowrap;
+ pointer-events: none;
+ }
+ .loop-back::before {
+ content: "↻";
+ font-size: 0.85rem;
+ line-height: 1;
+ }
+
+ .flow-note {
+ position: relative;
+ z-index: 1;
+ margin: 1.1rem auto 0;
+ max-width: 34rem;
+ text-align: center;
+ font-size: 0.76rem;
+ color: var(--text-muted);
+ line-height: 1.6;
+ }
+ .flow-note code { font-size: 0.74rem; }
+
+ /* 窄屏:中轴线收边,节点撑满 */
+ @media (max-width: 620px) {
+ .flow { padding: 1.2rem 1rem 1.1rem; }
+ .flow::before { display: none; }
+ .n { width: auto; max-width: none; }
+ .br, .flow-note { max-width: none; }
+ .loop { max-width: none; padding: 2.2rem 0.8rem 2.4rem; }
+ .lnk { height: 1.6rem; }
}
- .flow .k-blue { color: var(--brand-blue); }
- .flow .k-cyan { color: var(--brand-cyan); }
- .flow .k-gold { color: var(--brand-gold); }
- .flow .k-violet { color: var(--brand-violet); }
- .flow .k-dim { color: var(--text-muted); }
/* ── 代码块 ── */
.code {
@@ -861,35 +1080,31 @@
.layer.l-doc .bar { color: var(--brand-cyan); }
.layer.l-gph .bar { color: var(--brand-gold); }
- /* ── 架构图的「数据流动」:一条光带缓缓下行 ── */
- .flow { position: relative; overflow-y: hidden; }
- .flow::after {
- content: "";
- position: absolute;
- left: 0; right: 0; top: -30%;
- height: 30%;
- background: linear-gradient(
- to bottom, transparent,
- rgba(56, 189, 248, 0.10) 45%,
- rgba(56, 189, 248, 0.16) 50%,
- transparent);
- animation: flowSweep 5.5s linear infinite;
- pointer-events: none;
+ /* ── 流程图的「数据流动」──
+ 光带扫描会盖住节点,改成让节点自己的图标呼吸。 */
+ .js-fx .flow .n-ico {
+ animation: nodePulse 4.5s ease-in-out infinite;
}
- @keyframes flowSweep {
- 0% { top: -30%; }
- 100% { top: 100%; }
- }
- /* 节点标签的脉冲 */
- .flow .k-blue, .flow .k-cyan, .flow .k-gold, .flow .k-violet {
- animation: nodePulse 4s ease-in-out infinite;
- }
- .flow .k-cyan { animation-delay: 0.6s; }
- .flow .k-gold { animation-delay: 1.2s; }
- .flow .k-violet { animation-delay: 1.8s; }
+ .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% { filter: brightness(1); }
- 50% { filter: brightness(1.45); }
+ 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); }
}
/* ── 数字卡片:数值滚动到位 ── */
@@ -959,6 +1174,8 @@
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; }
@@ -1152,23 +1369,94 @@
- 外部输入(QQ / WebUI / CLI / 邮件 / 设备 …) - │ - ▼ - 输入调度器 两类别 · 四级中断 L1–L4 - │ ├─ 同级不抢占 → 入队列 - │ ├─ 更高级 → 抢占(现场压入中断栈,稍后恢复) - │ └─ 忙太久 → 转投 临时分诊助手 - ▼ - 处理管道 on_input → pre_action → post_action → before/after_toolcall → before/after_output - │ 记忆召回 + 人格注入 + LLM 调用 + 工具循环 - ▼ - 三层记忆 Context → Document → Graph - │ - ▼ - 输出通道 可寻址到具体 agent(父 ↔ 驻留子) -+ +
pre_action
+ 构建消息与工具表 · 插件可在此短路post_action
+ 插件可改写最终文本before_toolcall
+ 逐个工具执行前 · 可拒after_toolcall
+ 上下文裁剪 · 相关记忆召回 · 结果入消息
+ on_input 在进管道之前跑;before_output / after_output 在全部轮次结束后跑一次。
+