From 09298886a267e1f64b2fed8ecf1445b7e12cdf43 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 19 Sep 2026 22:38:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(site):=20=E3=80=8C=E4=B8=80=E6=9D=A1?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=BF=9B=E6=9D=A5=E4=B9=8B=E5=90=8E=E3=80=8D?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E7=9C=9F=E6=AD=A3=E7=9A=84=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原来是 ASCII
 图。它有三个问题:

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(父 ↔ 驻留子)
-
+ +
+ + 外部输入 + QQ / WebUI / CLI / 邮件 / 设备 +
+ + + +
+ + 输入调度器 + 两类别 · 四级中断 L1–L4 +
+
    +
  • 同级不抢占 → 入队列排队
  • +
  • 更高级别抢占,现场压入中断栈,稍后恢复
  • +
  • 忙得太久转投临时分诊助手
  • +
+ + + +
+ + 处理管道 + 记忆召回 + 人格注入 + LLM 调用 + 工具循环 +
+ + +
+ 每轮复用 +
    +
  1. + 1 +
    pre_action + 构建消息与工具表 · 插件可在此短路
    +
  2. +
  3. + 2 +
    LLM 调用 + provider 回退 + 瞬时错误重试 · 流式累积
    +
  4. +
  5. + 3 +
    post_action + 插件可改写最终文本
    +
  6. +
  7. + 4 +
    before_toolcall + 逐个工具执行前 · 可拒
    +
  8. +
  9. + 5 +
    执行工具 + 临界区:副作用不可回滚
    +
  10. +
  11. + 6 +
    after_toolcall + 上下文裁剪 · 相关记忆召回 · 结果入消息
    +
  12. +
+ +
+ + + + +
+ + 三层记忆 + Context → Document → Graph +
+ + + +
+ + 输出通道 + before_output · after_output · 可寻址到具体 agent +
+ +

+ on_input 在进管道之前跑;before_output / after_output 在全部轮次结束后跑一次。 +

核心组件