assistant/docs/方案图解.html

330 lines
75 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!doctype html>
<html lang="zh-CN" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%2314695e'/%3E%3Cpath d='M8 24 16 8l8 16M11 18h10' fill='none' stroke='white' stroke-width='2'/%3E%3C/svg%3E">
<title>AI 获客助手 方案图解</title>
<style>
:root{--bg:#f5f6f7;--surface:#fff;--soft:#f0f2f3;--text:#1d252b;--muted:#5c6973;--line:#d9dfe3;--accent:#14695e;--tint:#e5f2ee;--feedback:#94601c;--feedback-bg:#faf0df;--arrow:#7f8b95;--focus:#14695e;--node:#fff;--group:#f5f7f8;--group-line:#dce2e6;--active:#14695e}
:root[data-theme="dark"]{--bg:#151a1f;--surface:#1d242b;--soft:#252e37;--text:#edf0f2;--muted:#a9b6c0;--line:#394550;--accent:#83cbbb;--tint:#253e39;--feedback:#e0b46e;--feedback-bg:#403421;--arrow:#98a7b2;--focus:#83cbbb;--node:#202931;--group:#1a2229;--group-line:#3a4650;--active:#83cbbb}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}button,input,select{font:inherit}button,a,input,select{-webkit-tap-highlight-color:transparent}button{color:inherit;cursor:pointer}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--focus);outline-offset:3px}button:disabled{opacity:.4;cursor:default}
.shell{max-width:1600px;margin:auto;padding:30px 32px 22px}.mast{display:flex;align-items:flex-start;justify-content:space-between;gap:24px}.eyebrow{color:var(--muted);font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase}.mast h1{font-size:27px;line-height:1.4;font-weight:650;letter-spacing:.02em;margin:8px 0}.intro{max-width:820px;color:var(--muted);margin:0;font-size:14px}.mast-actions{display:flex;gap:9px;align-items:center;flex-shrink:0;padding-top:4px}.pill{font-size:12px;padding:4px 10px;border:1px solid var(--line);border-radius:5px;white-space:nowrap}.btn{display:inline-flex;justify-content:center;align-items:center;gap:6px;min-height:35px;border:1px solid var(--line);border-radius:6px;background:var(--surface);padding:5px 11px;white-space:nowrap;font-size:12px}.btn:hover{border-color:var(--accent);background:var(--tint);text-decoration:none}.btn.primary{background:var(--accent);border-color:var(--accent);color:var(--surface)}[data-theme="dark"] .btn.primary{color:#15241f}.facts{display:flex;gap:22px;flex-wrap:wrap;font-size:12px;color:var(--muted);padding:18px 0}.facts b{color:var(--text);font-weight:600;margin-right:6px}.tabs{display:flex;gap:3px;border-bottom:1px solid var(--line);overflow-x:auto;scrollbar-width:thin}.tab{flex-shrink:0;display:flex;gap:9px;align-items:center;background:none;border:0;border-bottom:3px solid transparent;padding:13px 19px 12px;color:var(--muted);font-size:14px}.tab .index{font-size:10px;font-variant-numeric:tabular-nums;opacity:.8}.tab[aria-selected="true"]{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}.tab:hover{background:var(--soft);color:var(--text)}
.workspace{display:grid;grid-template-columns:minmax(0,1fr) 292px;gap:20px;margin-top:20px;align-items:start}.diagram-panel{min-width:0;background:var(--surface);border:1px solid var(--line);border-radius:9px;overflow:hidden}.panel-head{padding:17px 20px 12px;display:flex;justify-content:space-between;align-items:start;gap:20px}.panel-head h2{font-size:17px;line-height:1.4;margin:0 0 5px;font-weight:650}.panel-head p{margin:0;color:var(--muted);font-size:12px;max-width:780px}.count{font-size:11px;color:var(--muted);white-space:nowrap;padding-top:2px}.toolbar{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:9px;padding:10px 16px;border-block:1px solid var(--line);background:var(--soft)}.tool-group{display:flex;gap:6px;align-items:center}.tool-group .btn{min-height:30px;padding:3px 9px}.zoom{display:inline-flex;justify-content:center;min-width:39px;font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums}.focus-toggle{font-size:12px;color:var(--muted);display:flex;gap:5px;align-items:center;white-space:nowrap}.focus-toggle input{accent-color:var(--accent)}.viewport{overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}.viewport svg{display:block;width:100%;min-width:820px;height:auto}.legend{display:flex;gap:22px;align-items:center;flex-wrap:wrap;padding:10px 19px 12px;border-top:1px solid var(--line);color:var(--muted);font-size:11px}.legend-item{display:flex;align-items:center;gap:7px}.sample{width:26px;border-top:1.5px solid var(--arrow)}.sample.optional{border-top-style:dashed}.sample.feedback{border-top-style:dashed;border-color:var(--feedback)}.guide{margin-left:auto}
.playback{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:13px 18px;border-top:1px solid var(--line);font-size:12px}.playback select{max-width:230px;color:var(--text);background:var(--surface);border:1px solid var(--line);border-radius:5px;min-height:32px;padding:4px 6px}.play-label{color:var(--muted);margin-right:2px}.step-label{font-variant-numeric:tabular-nums;color:var(--muted);font-size:11px;flex:1;min-width:125px}.step-label b{color:var(--text);font-weight:500}.notes{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:18px}.note{padding-top:12px;border-top:1px solid var(--line)}.note h3{font-size:12px;margin:0 0 5px}.note p{font-size:12px;color:var(--muted);margin:0;line-height:1.8}
.inspector{min-width:0;position:sticky;top:18px}.search-box{position:relative;margin-bottom:15px}.search-box input{width:100%;height:38px;border:1px solid var(--line);border-radius:6px;background:var(--surface);padding:7px 12px;color:var(--text);font-size:12px}.results{position:absolute;z-index:5;top:44px;inset-inline:0;background:var(--surface);border:1px solid var(--line);border-radius:6px;max-height:360px;overflow:auto}.results:empty{display:none}.result{display:block;width:100%;padding:9px 12px;border:0;border-bottom:1px solid var(--line);background:none;text-align:left;font-size:12px}.result:last-child{border-bottom:0}.result:hover{background:var(--tint)}.result small{display:block;color:var(--muted);font-size:10px}.search-empty{padding:12px;color:var(--muted);font-size:12px}.detail{border:1px solid var(--line);border-radius:8px;background:var(--surface);padding:19px}.detail-kicker{font-size:10px;color:var(--muted);letter-spacing:.08em}.detail h3{margin:9px 0 8px;font-size:18px;line-height:1.5}.status-tag{display:inline-block;background:var(--soft);padding:3px 7px;border-radius:4px;font-size:10px;color:var(--muted);margin-bottom:13px}.detail-desc{font-size:12px;line-height:1.85;margin:0 0 15px}.detail dl{margin:0}.detail dt{font-size:10px;font-weight:600;letter-spacing:.06em;color:var(--muted);margin:15px 0 4px}.detail dd{font-size:12px;line-height:1.85;margin:0}.detail ul{padding-left:17px;margin:4px 0 0}.detail li{font-size:12px;line-height:1.8;margin:5px 0}.detail-source{border-top:1px solid var(--line);margin-top:17px;padding-top:12px;font-size:11px;color:var(--muted)}.side-foot{color:var(--muted);font-size:11px;margin:12px 3px;line-height:1.8}.footer{display:flex;gap:20px;justify-content:space-between;color:var(--muted);font-size:11px;padding-top:22px;margin-top:20px;border-top:1px solid var(--line)}.notice{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:var(--text);color:var(--surface);padding:10px 18px;border-radius:7px;z-index:20;font-size:12px;max-width:90vw;pointer-events:none}.notice:empty{display:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
svg text{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}svg .paper{fill:var(--surface)}svg .svg-heading{fill:var(--text);font-size:18px;font-weight:600}svg .svg-sub{fill:var(--muted);font-size:11px}svg .group-rect{fill:var(--group);stroke:var(--group-line);stroke-width:1}svg .group-title{fill:var(--muted);font-size:12px;font-weight:600}svg .node{cursor:pointer;outline:none}svg .node-box{fill:var(--node);stroke:var(--line);stroke-width:1.2}svg .node-title{fill:var(--text);font-size:15px;font-weight:550}svg .node-sub{fill:var(--muted);font-size:11px}svg .node-badge{fill:var(--muted);font-size:9px}svg .node:hover .node-box,svg .node:focus-visible .node-box{stroke:var(--accent);stroke-width:2}svg .node.selected .node-box{stroke:var(--accent);stroke-width:2;fill:var(--tint)}svg .node.route .node-box{stroke:var(--accent);stroke-width:1.8}svg .node.current .node-box{fill:var(--tint);stroke:var(--accent);stroke-width:2.3}svg .node.dim{opacity:.25}svg .edge{fill:none;stroke:var(--arrow);stroke-width:1.35;stroke-linejoin:round;stroke-linecap:round}svg .edge.optional{stroke-dasharray:6 5}svg .edge.feedback{stroke:var(--feedback);stroke-dasharray:5 4}svg .edge.highlight{stroke:var(--accent);stroke-width:2.2}svg .edge.dim{opacity:.2}svg .edge-label rect{fill:var(--surface)}svg .edge-label text{fill:var(--muted);font-size:10px}svg .edge-label.feedback text{fill:var(--feedback)}svg .edge-label.dim{opacity:.2}svg .arrow-tip{fill:var(--arrow)}svg .arrow-tip.active{fill:var(--accent)}svg .arrow-tip.feedback{fill:var(--feedback)}
@media(min-width:1700px){.shell{padding-top:40px}.workspace{grid-template-columns:minmax(0,1fr) 310px;gap:24px}}
@media(max-width:1120px){.workspace{grid-template-columns:minmax(0,1fr)}.inspector{position:static;display:grid;grid-template-columns:250px 1fr;gap:18px}.detail{grid-column:2;grid-row:1 / span 2}.search-box{margin:0}.side-foot{grid-column:1;align-self:start}.mast h1{font-size:24px}.guide{margin-left:0}}
@media(max-width:680px){.shell{padding:20px 14px}.mast{display:block}.mast-actions{padding-top:15px}.intro{font-size:12px}.facts{gap:8px 18px;padding:14px 0}.tab{padding:12px 13px;font-size:12px}.workspace{margin-top:14px;gap:14px}.panel-head{padding:15px}.panel-head h2{font-size:16px}.count{display:none}.toolbar{padding:8px 10px}.tool-group{gap:4px}.tool-group .btn{padding:3px 8px}.focus-toggle{font-size:11px}.playback{padding:11px}.playback select{max-width:185px}.step-label{flex-basis:100%;margin:1px 0 0}.legend{gap:10px 16px;padding:10px 12px}.guide{display:none}.notes{grid-template-columns:1fr;gap:12px}.inspector{display:block}.search-box{margin-bottom:12px}.detail{padding:16px}.footer{display:block}.footer span{display:block;margin:4px 0}}
@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}}
@media print{body{background:white;color:black}.shell{padding:0}.mast-actions,.tabs,.toolbar,.playback,.inspector,.notes,.footer,.legend,.facts{display:none}.workspace{display:block;margin:10px 0}.diagram-panel{border:0}.viewport{overflow:visible}.viewport svg{width:100%!important;min-width:0!important}.panel-head{padding:0 0 10px}.notice{display:none}}
</style>
</head>
<body>
<main class="shell">
<header class="mast">
<div><div class="eyebrow">ASSISTANT / PRODUCT &amp; ENGINEERING</div><h1>AI 获客助手 方案图解</h1><p class="intro">从商家配置商品,到发现需求、形成线索与跟进;将平台操作学习为经过验证的能力,再由桌面端重复执行。</p></div>
<div class="mast-actions"><span class="pill">v0.2 · 设计阶段</span><button class="btn" id="theme" type="button">切换深色</button><a class="btn" href="整体方案-v0.2.md">文字方案 ↗</a></div>
</header>
<div class="facts"><span><b>桌面</b>Windows + macOS</span><span><b>执行</b>Electron + Playwright</span><span><b>模型</b>运营后台按用途配置</span><span><b>更新</b>2026.10.22</span></div>
<nav class="tabs" id="tabs" role="tablist" aria-label="方案图页"></nav>
<div class="workspace">
<section>
<div class="diagram-panel" id="diagram-panel">
<div class="panel-head"><div><h2 id="diagram-title"></h2><p id="diagram-description"></p></div><span class="count" id="diagram-count"></span></div>
<div class="toolbar">
<div class="tool-group"><button class="btn" id="zoom-out" aria-label="缩小" type="button">−</button><span class="zoom" id="zoom-value">100%</span><button class="btn" id="zoom-in" aria-label="放大" type="button">+</button><button class="btn" id="fit" type="button">适应宽度</button><label class="focus-toggle"><input id="focus" type="checkbox">聚焦关联</label></div>
<div class="tool-group"><button class="btn" id="export-svg" type="button">导出 SVG</button><button class="btn" id="export-png" type="button">导出 PNG</button></div>
</div>
<div class="viewport" id="viewport" role="region" aria-label="可滚动方案图" tabindex="0"></div>
<div class="legend"><span class="legend-item"><span class="sample"></span>固定职责或流程</span><span class="legend-item"><span class="sample optional"></span>可选或后续</span><span class="legend-item"><span class="sample feedback"></span>异常或修订</span><span class="guide">点击节点查看细节 · 窄屏可横向滚动</span></div>
<div class="playback"><span class="play-label">路径演示</span><select id="scenario" aria-label="选择演示路径"></select><button class="btn" id="prev" aria-label="上一步" type="button">上一步</button><button class="btn primary" id="play" type="button">播放</button><button class="btn" id="next" aria-label="下一步" type="button">下一步</button><span class="step-label" id="step-label"></span><button class="btn" id="reset" type="button">清除</button></div>
</div>
<div class="notes"><section class="note"><h3 id="note-title-a"></h3><p id="note-a"></p></section><section class="note"><h3 id="note-title-b"></h3><p id="note-b"></p></section></div>
</section>
<aside class="inspector" aria-label="节点详情">
<div class="search-box"><label class="sr-only" for="search">搜索所有图页的节点</label><input type="search" id="search" placeholder="搜索节点,例如 变参 / 额度 / 恢复" autocomplete="off" aria-controls="results" aria-expanded="false"><div class="results" id="results"></div></div>
<div class="detail" id="detail" aria-live="polite"></div>
<p class="side-foot">图中展示整体设计;学习后台基建已启动,具体实现以 README 为准。真实平台与双端验收待完成。路径播放仅演示设计关系。</p>
</aside>
</div>
<footer class="footer"><span>来源:AI 获客助手整体方案 v0.2 · 2026 年 10 月 22 日</span><span>独立离线文件 · 无外部资源依赖</span></footer>
</main>
<div class="notice" id="notice" role="status"></div>
<script>
"use strict";
// 同步来源 docs/整体方案-v0.2.md。图页数据与渲染器均内联,文件可离线打开。
const N=(id,x,y,w,title,sub,desc,input,output,checks,source,status="proposed",h=74)=>({id,x,y,w,h,title,sub,desc,input,output,checks,source,status});
const E=(a,b,label="",opt={})=>({a,b,label,...opt});
const G=(x,y,w,h,title)=>({x,y,w,h,title});
const diagrams=[
{id:"architecture",name:"系统架构",title:"三个环境 一套能力契约",description:"维护端生产能力,商家电脑执行任务,云端管理模型、权益与计量。模块表示职责,可先合并部署。",w:1220,h:842,
groups:[G(25,100,290,640,"维护者环境"),G(345,100,485,640,"商家电脑 · Windows / macOS"),G(860,100,335,640,"云端服务")],
nodes:[
N("observe",50,150,240,"学习与观察","目标范围 · 页面证据","我们在维护环境观察和调试平台链路,记录操作前后状态与未知分支。","平台入口、能力目标、步骤与模型预算","页面证据、操作知识、未覆盖项",["维护工具先服务少量获客能力","学习成本与商家运行用量分开"],"5.1–5.4"),
N("candidate",50,265,240,"候选能力","参数实现 + 契约 + 用例","手写或由模型辅助产生候选,统一接受固定校验与浏览器试跑。","操作知识与证据","候选实现、输入输出契约、用例",["文字说明或录制结果不能直接发布","学习资料和商家执行包分开"],"5.2–5.3"),
N("verify",50,380,240,"校验与双端试跑","结构校验 · 变参 · 结果核对","先检查产物结构和规则,再验证不同参数、页面状态以及两种操作系统。","候选版本、样本、目标环境","校验记录、试跑结果、缺口",["校验通过与实际执行成功分别记录","Windows 与 macOS 分别通过"],"5.2、11"),
N("learning-db",50,495,240,"学习验证记录库","独立 PostgreSQL + 证据文件","维护者学习端单独记录校验、试跑、结果核对及双端验收,保留每次重跑和修订的历史。","能力版本与校验值、用例、参数、环境与实际结果","可按能力、版本、用例和系统回看的验证记录",["与商家业务数据库隔离","发布清单关联具体通过记录"],"5.3","confirmed"),
N("package",50,610,240,"不可变能力版本","版本号 · 校验值 · 兼容范围","发布满足覆盖范围和验收要求的能力版本,保留旧版本及验证依据。","已验证候选与兼容信息","可发布的能力包与清单",["修改产生新版本","任务绑定创建时的能力版本"],"5.3、8"),
N("desktop",375,150,425,"Electron 桌面界面","账号 · 商品 · 任务 · 线索 · 用量","商家配置业务条件、查看执行结果和接管浏览器。可从测试工程提取窗口、IPC、产品登录和状态展示基础,再适配获客页面。","商家商品条件、任务配置","受限 IPC 任务请求与结果展示",["Windows 与 macOS 首版同时适配,原打包只验证过 Windows","浏览器操作在独立执行进程中运行"],"2、4.1、4.4","confirmed"),
N("runner",375,265,425,"任务与链路执行器","Node.js / TypeScript 建议","编排已发布能力、检查点、动作账目和预算,按用途请求 AI 分析。可借用原进程生命周期接口,接入本项目执行进程。","任务快照、能力版本、账号会话","执行进度、线索、待核对动作",["首版账号内串行执行","原 Python、校园编排和本地 PostgreSQL 不随桌面壳整套引入"],"4、8"),
N("adapter",375,380,195,"平台适配器","平台链路与契约","将通用任务要求映射到某个平台的能力输入输出。","固定版本的能力及参数","平台操作与读取结果",["每个平台分别学习和验证","不把框架对象存为业务契约"],"4.2、5.7"),
N("sqlite",610,380,190,"商家业务 SQLite","任务 · 线索 · 账号目录映射","保存商品、任务、线索、检查点与动作核对结果,以及浏览器目录引用和最后核验时间。","执行进展、分析结果、来源","可恢复的业务记录",["与维护者学习验证库隔离","平台登录资料由本地浏览器管理"],"4.3、7–8"),
N("playwright",375,495,195,"Playwright","固定浏览器执行","用显式代码完成定位、等待、点击、输入、读取和结果核对。","当前页面、能力参数","页面数据与动作证据",["每个会话只有一个驱动持有控制权","固定代码本身不产生 LLM Token"],"4.2、6.3"),
N("midscene",610,495,190,"Midscene 可选","视觉与局部诊断","遇到明确的视觉或定位需求时再评估引入,相关模型调用接入 AI 网关。","需视觉辅助的局部步骤","定位或诊断结果",["首版不是必选依赖","缓存不等于整条链路零模型费用"],"5.6、6.3","pending"),
N("browser",375,610,195,"专用浏览器会话","按账号持久化 · 人工登录","主线使用独立可最大化浏览器;原 WebContentsView 宿主作为待验证候选。按账号复用固定目录或稳定分区,任务先核验身份。","专用账号目录或持久化分区、当前任务","已核验的会话、等待登录或等待处理状态",["原新会话随机分区要改为稳定账号映射","最小化及内嵌收起效果需双端验证;关闭则暂停保存进度"],"4.1–4.4"),
N("api",885,150,285,"产品账号与权益 API","产品登录 · 订阅 · 设备","可采用原 FastAPI 产品认证、权限和审计骨架,学校模型改为商家模型;订阅、订单和设备权益是本项目新增模块。","产品登录、设备及订单请求","登录状态、软件有效期与权益",["产品账号与第三方平台账号分别管理","原 deviceId 记录不等同于商业设备权益"],"4.3–4.4、9"),
N("gateway",885,265,285,"AI 网关","按用途路由 · 预算 · 请求标识","统一处理分析接口、结果格式、供应商调用与用量记录。","业务用途、必要内容、任务快照","结构化分析结果与实际计量",["供应商密钥保留服务端","相同请求标识复用已完成结果"],"4.3、6.4、9"),
N("models",885,380,285,"模型与运营配置","文本判断 · 复核 · 视觉 · 学习","我们在后台按四类用途配置模型与预算,不将厂商型号写死在客户端。","用途配置、真实样本和预算","已验证的模型配置版本",["文字模型与视觉模型分别校验","模型效果和成本按样本验证"],"6.4","confirmed"),
N("ledger",885,495,285,"订单与额度账本","预占 · 实际计量 · 结算","记录商家权益与额度,以及供应商实际消耗,分别对账。","请求标识、预占和实际消耗","额度余额、扣费与对账记录",["商家分析额度不等同供应商 Token","超时可能已发生费用,保留未决状态"],"9"),
N("release",885,610,285,"版本发布清单","能力包与双端客户端","发布版本、校验与兼容信息,客户端按任务固定版本执行。可借用原配置修订发布机制,替换为本项目能力契约。","经过验证的能力与客户端构建","可用版本及回滚信息",["Windows 打包模板可参考,macOS 分发链和升级回滚仍需建设","新能力版本用于新任务"],"4.1、4.4、5.5、8")
],
edges:[E("observe","candidate"),E("candidate","verify"),E("verify","learning-db","保存记录"),E("learning-db","package","关联通过记录"),E("desktop","runner"),E("runner","adapter","执行",{from:"bottom",fromAt:97.5/425,to:"top"}),E("runner","sqlite","保存",{from:"bottom",fromAt:.8,to:"top"}),E("adapter","playwright"),E("playwright","browser"),E("runner","midscene","按需调用",{kind:"optional",from:"right",fromAt:.78,to:"top",via:[[822,322.72],[822,470],[705,470]],lx:705,ly:470}),E("midscene","playwright","辅助",{kind:"optional",from:"left",to:"right"}),E("midscene","gateway","视觉调用",{kind:"optional",from:"right",to:"bottom",toAt:.95,via:[[840,532],[840,355],[1155.75,355]],lx:1065,ly:355}),E("desktop","api","权益",{from:"right",to:"left"}),E("runner","gateway","分析",{from:"right",to:"left"}),E("gateway","models"),E("gateway","ledger","计量",{from:"right",to:"right",via:[[1183,302],[1183,532]],lx:1150,ly:466}),E("api","ledger","账本",{from:"right",to:"right",via:[[1190,187],[1190,532]],lx:1150,ly:238}),E("package","release","版本化发布",{from:"bottom",to:"bottom",via:[[170,765],[1027,765]],lx:560,ly:765}),E("release","runner","拉取固定版本",{from:"left",to:"right",via:[[845,647],[845,302]],lx:842,ly:596}),E("sqlite","observe","诊断导出 → 维护端导入",{from:"right",to:"left",kind:"feedback",via:[[815,417],[815,720],[12,720],[12,187]],lx:290,ly:720})],
paths:[{name:"能力从学习到运行",ids:["observe","candidate","verify","learning-db","package","release","runner","adapter","playwright","browser"]},{name:"商家任务与 AI 分析",ids:["desktop","runner","gateway","models"]},{name:"可选视觉步骤接入网关",ids:["runner","midscene","gateway","models"]}],
notes:[["采用适用的工程基础","测试工程的 Electron 壳、产品账号认证和管理后台列为候选提取范围。云端可沿用 FastAPI/PostgreSQL 骨架;本地保持 SQLite 和轻量执行进程,具体改造见文字方案 §4.4。"],["仍需独立建设与验证","原校园编排、密码代配和随机会话隔离不直接照搬。获客链路、AI 网关、商业账本与双端分发仍需实现;内嵌宿主与 Midscene 分别按需求验证。"]]
},
{id:"acquisition",name:"获客流程",title:"相关内容到可回看的客户线索",description:"同行商家视频的评论区与作者本人有采购需求是两条不同路径。相关性、角色和采购意图分别判断。",w:1220,h:862,
groups:[G(25,100,1170,710,"商家运行端 · 搜索入口先行,推荐流后续扩展")],
nodes:[
N("product",55,155,220,"商品与客户条件","商品 · 地区 · 排除条件","商家提供商品、使用场景、服务地区与目标客户条件。","商家配置","版本化商品条件",["未设置的信息不自行补齐","分析结果绑定商品条件版本"],"2、6.1"),
N("entry",350,155,220,"搜索或推荐流","首轮先验证关键词搜索","按任务入口发现本轮候选内容,记录来源及进度。","关键词或入口、范围与预算","候选视频和来源",["不承诺枚举无限列表全部内容","推荐流放在后续扩展阶段"],"3、8、10"),
N("video",655,155,220,"视频相关性判断","文字优先 · 信息不足再补充","结合商品条件判断内容是否相关,必要时读取更多上下文。","标题、文本及必要补充内容","相关、不相关或待判断及依据",["相似度不能代替采购意图","无相关内容时继续发现或按预算结束"],"3、6.1–6.2"),
N("profile",945,155,220,"主页与作品读取","当前账号与作品来源","进入作者主页,读取简介及必要作品用于判断角色和需求。","相关视频及作者标识","账号信息、作品与来源",["对象身份与昵称分开","不能混用视频作者与评论者"],"3、7"),
N("role",945,300,220,"账号角色与需求","角色可多选","区分同行商家、采购方与信息不足,也允许同一账号兼具多种角色。","主页与内容上下文","角色标签与当前需求",["同行身份不排除采购需求","分支按本次商品与需求决定"],"3、6.1"),
N("comments",625,300,230,"同行视频评论与回复","保留父子关系与来源","读取与商品有关的视频评论及必要回复,查找需求信号。","视频标识、进度与读取范围","评论、回复、来源及下一状态",["动态列表每次定位当前对象","父评论和必要回复作为判断上下文"],"3、5.3、6.1"),
N("intent",315,300,230,"评论采购意图判断","原文依据 · 缺失信息","将需求信号与商家商品条件匹配,区分真实采购、闲聊和信息不足。","评论上下文与商品条件","采购意图与原文依据",["保留评论作者标识","新上下文变化后重新判断"],"6.1–6.2"),
N("buyer",945,445,220,"作者需求核对","采购条件与商品是否匹配","作者本人表达需求时,核对商品、场景和地区等必要条件。","作者需求与商家条件","可进入线索的匹配依据",["不能只因账号相关就判定客户","缺少条件时保留待判断"],"3、6.1"),
N("pending",625,445,230,"信息不足待判断","保留依据与缺失项","对当前信息无法支持判断的对象保留状态,不自动归为有效客户。","模糊内容或缺失条件","待判断对象与缺口",["模型自评分不作为真实准确率","不因缺少关键词直接排除所有模糊样本"],"3、6.1–6.3"),
N("lead",315,590,230,"线索去重与依据保存","对象标识 · 多条需求来源","归并可靠确认的同一对象,保留原始评论和内容来源。","两条业务路径的匹配结果","可回看的本地线索",["相同昵称不能直接认定同一人","内容、客户、动作分别去重"],"7"),
N("follow",625,590,230,"关注与私信任务","按阶段验证开放范围","按商品与任务配置准备跟进内容,记录目标与动作业务标识。","线索、跟进设置与模板版本","已准备或已执行的动作账目",["自动跟进保留为产品目标","实际首版范围依据功能验证结果确定"],"2、7、10","pending"),
N("readback",945,590,220,"结果核对与后续回复","确认 / 失败 / 结果未知","读取实际效果,记录关注、发送与回复状态。","动作标识与可核对页面证据","已确认结果或待核对记录",["未知结果先核对,不能直接重发","去重跨任务和模板变更生效"],"7、8")
],
edges:[E("product","entry"),E("entry","video"),E("video","profile","相关 / 补充"),E("profile","role"),E("role","comments","同行",{from:"left",to:"right"}),E("comments","intent", "评论需求",{from:"left",to:"right"}),E("role","buyer","作者有需求"),E("role","pending","信息不足",{from:"bottom",to:"right",via:[[1055,410],[880,410],[880,482]],lx:886,ly:410}),E("intent","lead","匹配",{from:"bottom",to:"top"}),E("buyer","lead","匹配",{from:"bottom",to:"top",via:[[1055,555],[430,555]],lx:760,ly:555}),E("lead","follow"),E("follow","readback"),E("video","entry","不相关则继续发现",{from:"top",to:"top",via:[[765,125],[460,125]],lx:600,ly:125}),E("intent","pending","需更多信息",{from:"bottom",to:"left",via:[[430,482]],lx:535,ly:482})],
paths:[{name:"从同行评论发现采购方",ids:["product","entry","video","profile","role","comments","intent","lead","follow","readback"]},{name:"作者本人表达采购需求",ids:["product","entry","video","profile","role","buyer","lead","follow","readback"]},{name:"信息不足保留待判断",ids:["product","entry","video","profile","role","pending"]}],
notes:[["商家配置的是业务条件","关键词帮助发现候选内容;商品与目标客户条件决定匹配结果。平台动作能够复用,同一对象对不同商家的匹配结论可能不同。"],["首条学习链路已选定","小红书搜索笔记 → 详情 → 评论读取。先验证本机学习工具链,再适配真实页面,随后验证线索判断与后续跟进。"]]
},
{id:"learning",name:"学习发布",title:"有界学习与能力发布闭环",description:"观察证据、操作知识、候选实现和执行验证分别留存。结构校验通过、真实试跑通过、已发布是不同状态。",w:1220,h:862,
groups:[G(25,100,1170,710,"维护者环境 · 六类操作逐步学习,商家端执行固定版本")],
nodes:[
N("goal",55,160,225,"限定目标与预算","能力 · 入口 · 角色 · 分支","学习开始先限定本次能力、覆盖范围和消耗边界。","平台能力目标与已知入口","学习范围、预算、待覆盖分支",["每条评论不是一条新学习任务","第一版不建设全站探索和多机调度"],"5.1、5.4"),
N("observe",350,160,225,"观察与证据","操作前后状态 · 对象 · 来源","记录操作、当前对象、定位候选、匹配数量、等待和页面依据。","真实页面与操作","观察记录、截图或快照引用",["证据有时间和所属对象","保存范围受学习预算限制"],"5.2"),
N("knowledge",645,160,225,"字段 状态 分支","值来源 · 前后条件 · 效果","将观察整理为可解释的操作知识,分清已理解与未覆盖情况。","观察证据","结构化操作知识及缺口",["字段类型、必填和取值来源明确","有结果、无结果、登录失效分别描述"],"5.1–5.2"),
N("gaps",940,160,225,"未覆盖项","已观察与已验证分别记录","未见过的分支和证据不足单独登记,可缩小能力发布范围或追加观察。","未知条件和证据缺口","覆盖说明与后续学习目标",["不能将见过页面记作执行成功","缺口不因候选生成而自动消失"],"5.1–5.2"),
N("candidate",645,330,225,"候选能力与用例","TypeScript + JSON 可起步","从操作知识形成参数化实现、输入输出契约与用例,绑定证据。","操作知识、参数定义与证据","候选版本与验证用例",["手写和模型生成使用同一验收要求","Midscene 缓存不直接等于能力包"],"5.2–5.3"),
N("check",350,330,225,"固定规则校验","结构 · 类型 · 绑定 · 覆盖","程序检查契约、动作、等待、输出映射和证据引用,拒绝不合格候选。","候选能力、清单与用例","通过或拒绝及原因",["校验不是由模型自行宣布通过","固定校验不能代替真实浏览器验证"],"5.2"),
N("trial",55,330,225,"变参试跑与结果核对","更换关键词 对象 页面状态","在维护环境运行候选,核对返回对象、字段、来源和分支。","候选版本、变化参数与样本","实际执行及结果核对记录",["不是只跑通首个关键词","动态内容不要求两次结果完全相同"],"5.2、11"),
N("dual",55,520,225,"双端验证与覆盖确认","Windows + macOS 分别记录","按相同业务范围验证两端,同时记录浏览器版本与未覆盖情况。","试跑记录与目标环境","各端验证结论和兼容范围",["单端通过不能视为双端通过","关注私信用测试账号和测试对象验证"],"4.1、5.2–5.4"),
N("release",350,520,225,"发布不可变版本","版本号 · 内容校验 · 用例记录","将通过约定验收的候选纳入可用清单,保留原版本。","已验证候选和兼容信息","商家可用的能力版本",["变更生成新版本","不同能力版本使用关联新任务恢复进度"],"5.3、5.5、8"),
N("run",645,520,225,"商家固定执行","版本快照 · 参数 · 预算","商家任务重复调用已发布能力,AI 仍动态分析内容和采购意图。","已发布能力与商家条件","线索、动作结果与检查点",["不临时替换生产脚本","业务判断不冻结为某次学习结论"],"5.4、8"),
N("repair",940,520,225,"诊断 修订 回归","限定预算 · 新候选 · 再验证","对试跑失败或运行缺口分析原因,修订候选并回归受影响用例。","失败位置、版本、必要参数与证据","修订候选及回归结果",["运行异常通过诊断资料导出回流","结果未知先核对实际效果"],"4.3、5.5"),
N("records",350,695,520,"独立学习验证记录库","PostgreSQL · 校验 试跑 读回 双端验收留档","维护端独立保存每次验证的能力版本与校验值、用例、参数、系统与浏览器环境、前后状态、预期和实际结果、耗时、用量与证据引用。","每轮学习、校验和验证产物","可筛选回看的历史记录与发布依据",["重跑、修订追加记录,保留旧结论","与商家业务库隔离;大证据文件按引用保存"],"5.3","confirmed")
],
edges:[E("goal","observe"),E("observe","knowledge"),E("knowledge","gaps","缺口"),E("knowledge","candidate"),E("candidate","check","生成候选",{from:"left",to:"right"}),E("check","trial","校验通过",{from:"left",to:"right"}),E("trial","dual"),E("dual","release"),E("release","run"),E("run","repair","失败证据",{kind:"feedback"}),E("repair","candidate","修订后重验",{from:"top",to:"right",kind:"feedback",via:[[1052,450],[900,450],[900,367]],lx:980,ly:450}),E("trial","repair","试跑失败",{from:"bottom",to:"bottom",kind:"feedback",via:[[167,440],[300,440],[300,655],[1052,655]],lx:1010,ly:655}),E("check","candidate","拒绝后修订",{from:"top",to:"top",kind:"feedback",via:[[462,292],[757,292]],lx:605,ly:292})],
paths:[{name:"从观察到能力发布",ids:["goal","observe","knowledge","candidate","check","trial","dual","release","run"]},{name:"试跑失败后修订再验证",ids:["goal","observe","knowledge","candidate","check","trial","repair","candidate","check","trial","dual","release"]},{name:"候选校验拒绝后修订",ids:["candidate","check","candidate","check","trial","dual","release"]}],
notes:[["本轮进入学习后台基建","首期由维护者与本地 Codex 协作学习,探索并编写固定实现;后台接 PostgreSQL 与 Playwright,保存证据、候选及实测记录。后台自主学习 Agent 后续按需建设。"],["控制建设范围","先验证小红书搜索、详情与评论读取。全站分层探索、多工作者租约和跨端供给暂不引入;检索扩展已准备,业务索引尚未接入。"]]
},
{id:"ai",name:"AI 与额度",title:"一次分析请求如何计量与返回",description:"基础文本、复杂复核、视觉和维护学习分别配置。软件订阅、商家分析额度与供应商真实成本分别管理。",w:1220,h:842,
groups:[G(25,100,285,610,"商家电脑"),G(345,100,850,610,"云端网关与计量")],
nodes:[
N("content",50,155,235,"当前内容与商品条件","相关性 · 角色 · 采购意图","使用当前对象及必要上下文形成分析输入,三个判断分别记录。","标题、简介、评论和商品条件","待分析内容与用途",["文本优先,视觉按需","信息不足允许返回待判断"],"6.1–6.2"),
N("reuse",50,310,235,"去重与有效结果复用","上下文与版本参与复用键","先检查当前输入是否有仍有效的分析结果,再决定是否请求模型。","内容、商品、规则、模型与结构版本","已有结果或新分析请求",["上下文变化需重新判断","复用结果设置有效期"],"6.2–6.3"),
N("local",50,590,235,"保存分析与线索","结果依据 + 来源 + 用量","验证结果结构后写入本地记录,保留依据、模型和规则版本。","已完成分析结果或有效复用结果","本地分析记录、线索和客户端用量",["归属对象与原文依据对应","后续动作仍核对可靠身份与动作账目"],"6.1、7、9"),
N("gateway",375,310,235,"网关请求与幂等","业务用途 + 唯一请求标识","检查产品权益、请求和配置。已完成请求返回已有结果。","用途、必要内容、请求标识","可执行请求或已有结果",["重试复用原请求标识","不在客户端存供应商密钥"],"4.3、9"),
N("config",375,155,235,"运营模型配置","四类用途 独立预算","配置供应商、实际 model ID、协议、能力、价格和受限备用策略。","后台配置、质量和成本样本","经过验证的配置版本",["不要求使用最新或最强模型","任务保存配置快照"],"6.4","confirmed"),
N("reserve",670,310,235,"预占分析额度","校验权益与预算","在调用模型前预占本次请求额度,防止无预算开始调用。","请求标识、估算用量与余额","预占记录或等待额度",["余额不足不开始新的分析","预占与结算通过服务端账本管理"],"9"),
N("model",960,310,210,"模型调用","实际模型与用量标识","按配置调用已验证的模型,接收结果和可获得的实际用量。","结构化分析输入和用途预算","响应、请求标识与计量",["文字与视觉能力分别验证","备用模型回退也受预算限制"],"6.4、9"),
N("result",960,590,210,"结果与计量持久化","结构校验 · 响应保存","保存有效结果和实际供应商用量,再进行额度结算。","模型响应、实际计量","可复用结果与实际消耗记录",["结构不合格不进入有效线索","供应商成本与商家扣费分账记录"],"6.3、9"),
N("settle",670,590,235,"结算或释放预占","实际消耗与余额对账","依据结果和计量结算额度,释放多余预占,再返回已完成状态。","持久化结果、实际用量、预占记录","已结算请求与可返回结果",["已完成重试不再次扣费","客户端、服务端与供应商用量对账"],"9"),
N("return",375,590,235,"返回已完成结果","结构化判断与计量","将确认完成的结果交给商家端记录,异常保留明确状态。","结果、判断依据与计量","业务分析响应",["客户端按用途调用稳定接口","不硬编码供应商型号"],"4.3、6.4、9"),
N("uncertain",960,155,210,"超时 待核对","可能已经产生费用","调用超时可能已有供应商消耗,保留处理中或待核对状态。","未决请求与供应商记录","后续核对结果",["不立即按免费失败处理","不盲目发起另一笔调用"],"9")
],
edges:[E("content","reuse"),E("reuse","local","有效复用",{from:"bottom",to:"top"}),E("reuse","gateway","新分析"),E("config","gateway"),E("gateway","reserve"),E("reserve","model"),E("model","result","正常返回"),E("result","settle","保存后结算",{from:"left",to:"right"}),E("settle","return", "已完成",{from:"left",to:"right"}),E("return","local", "记录",{from:"left",to:"right"}),E("model","uncertain","超时",{from:"top",to:"bottom",kind:"feedback"}),E("uncertain","result","核对后补全",{from:"right",to:"right",kind:"feedback",via:[[1183,192],[1183,627]],lx:1150,ly:496}),E("gateway","return","已完成请求复用",{from:"bottom",to:"top",via:[[492,500]],lx:490,ly:484})],
paths:[{name:"新分析请求正常完成",ids:["content","reuse","gateway","reserve","model","result","settle","return","local"]},{name:"命中有效本地结果",ids:["content","reuse","local"]},{name:"供应商超时后核对",ids:["content","reuse","gateway","reserve","model","uncertain","result","settle","return","local"]},{name:"已完成请求重试",ids:["gateway","return","local"]}],
notes:[["三种账分开","软件有效期决定产品权益;商家分析额度决定可用分析量;供应商 Token 和视觉用量用于核算真实成本。套餐和价格还需实际样本验证。"],["学习成本与运行成本分开","维护者学习、探索和修复属于研发维护成本。商家任务中的文本分析、视觉分析和可选页面诊断属于运行用量。"]]
},
{id:"recovery",name:"任务恢复",title:"任务状态与动作结果分别管理",description:"上方描述任务是否继续,下方描述单次跟进动作的结果。任务重启、崩溃或超时,不能代替实际动作核对。",w:1220,h:932,
groups:[G(25,100,1170,425,"任务生命周期"),G(25,555,1170,300,"关注或私信动作账目")],
nodes:[
N("queued",65,160,210,"待运行","绑定版本与条件快照","任务保存商品条件、能力版本、模型配置、账号与预算。","商家任务设置","可运行任务快照",["新版本不悄悄进入已有任务","一个账号同一时刻一个任务持有控制权"],"8"),
N("running",435,160,250,"运行中","按确认步骤保存检查点","串行调用能力,记录已完成对象、剩余队列、步骤与动作状态。","任务快照与账号会话","进度、线索、动作账目",["检查点不依赖列表第 N 条位置","休眠或退出后本地任务不会继续运行"],"4.2、8"),
N("complete",945,160,210,"完成","达到本轮任务范围","按实际发现的对象和任务预算完成本轮,不代表平台内容全部枚举。","已处理对象和任务条件","任务报告与已用额度",["保留结果来源和模型计量","待核对动作不能伪装成已确认发送"],"7–8"),
N("pausing",65,300,210,"暂停中","等待可确认的步骤边界","收到暂停请求后在可确认边界停止推进,保存必要状态。","暂停请求与当前步骤","检查点与暂停结果",["紧急停止仍可能留下未知动作","接管前移交浏览器控制权"],"4.2、8"),
N("login",365,300,220,"等待登录","官方页面人工登录 再核验","会话复用检查失败时停止推进,由商家在官方页面自行扫码或输入验证码,核验账号与页面后恢复。","失效会话或需登录页面","确认身份的会话",["不在产品界面索取平台手机号和验证码","重新确认当前账号、对象与页面"],"4.2、8"),
N("quota",655,300,220,"等待额度","补足额度后再检查","模型预算或额度不足时停止开启新分析及依赖它的后续动作。","余额不足或用途预算耗尽","明确等待状态与检查点",["未决调用先核对和结算","恢复前重新检查预算"],"8–9"),
N("handling",945,300,210,"等待处理","接管或核对后恢复","页面变化、必要人工接管或未决状态先等待处理,再核验恢复条件;升级兼容检查不满足时保持暂停等待处理。","异常证据、兼容问题或人工接管","处理记录与恢复条件",["页面失配可导出诊断资料","不在任务中生成并热换新脚本"],"5.5、8"),
N("paused",65,425,210,"已暂停","检查点已保存","恢复时核对页面、账号和对象,再回到固定版本的任务。","已确认检查点与恢复请求","可继续的任务上下文",["旧版本仍有效才继续","换能力版本时建立关联新任务"],"5.5、8"),
N("failed",435,425,220,"失败","无法按恢复规则继续","确认不可恢复时记录失败,保留依据与诊断信息;升级兼容问题先进入暂停或等待处理。","异常与恢复检查结果","失败记录及后续修订输入",["失败与动作结果未知分开","保留已完成对象和动作账目"],"5.5、8"),
N("cancelled",775,425,220,"取消","停止后续推进","任务取消后不开始后续处理,已经发生或结果未知的动作继续保留记录。","取消请求","终止的任务与未决动作记录",["取消不是撤回已发生的操作","重建任务仍沿用跨任务动作去重"],"7–8"),
N("prepared",65,610,210,"prepared 已准备","目标与业务标识已记录","动作执行前写入可靠目标、业务跟进轮次及内容版本。","线索与跟进设置","动作业务标识和准备记录",["关注与发送分开建模","模板变化不自动产生新的跟进轮次"],"7"),
N("executing",365,610,220,"executing 执行中","正在尝试平台动作","执行关注或发送后核对实际状态,崩溃与超时不直接判定失败。","已准备动作与当前页面","待核对或已确认的结果",["先记录意图再执行动作","程序成功返回不等同于业务效果确认"],"7"),
N("confirmed",945,610,210,"confirmed 已确认","实际效果可核对","依据实际平台状态或可读回证据确认动作结果。","动作标识与结果证据","已确认动作状态",["关联实际目标和内容","已确认动作跨任务去重"],"7"),
N("uncertain",655,750,220,"uncertain 结果未知","先核对实际效果","网络中断、程序崩溃或页面超时后,先恢复页面并核对动作是否发生。","未决动作和对应页面","确认发生、确认未发生或仍未知",["未知不能直接当失败重试","仅靠本地标识不能保证第三方动作恰好一次"],"7"),
N("action-failed",365,750,220,"failed 已确认失败","保留原因与跟进状态","依据核对结果记录失败;后续是否执行需重新满足业务与去重条件。","确认失败或未发生的证据","失败记录与后续处理条件",["不从未知状态直接自动重发","新的跟进轮次必须是明确业务设置"],"7")
],
edges:[E("queued","running"),E("running","complete","范围完成"),E("running","pausing","暂停请求",{from:"bottom",fromAt:.12,to:"top",via:[[465,265],[170,265]],lx:215,ly:265}),E("pausing","paused"),E("paused","running","恢复前核验",{from:"left",to:"left",via:[[40,462],[40,245],[415,245],[415,197]],lx:190,ly:245}),E("running","login","失效 / 核验恢复",{from:"bottom",fromAt:.3,to:"top",bi:true,lx:465,ly:280}),E("running","quota","额度不足 / 恢复",{from:"bottom",fromAt:.68,to:"top",bi:true,lx:735,ly:280}),E("running","handling","异常 / 处理恢复",{from:"right",fromAt:.8,to:"top",via:[[890,219.2],[890,265],[1050,265]],bi:true,lx:1045,ly:265}),E("running","failed","不可恢复",{from:"bottom",fromAt:.74,to:"top",via:[[620,395],[545,395]],lx:605,ly:397,kind:"feedback"}),E("running","cancelled","取消",{from:"right",fromAt:.9,to:"top",via:[[905,226.6],[905,400],[885,400]],lx:905,ly:392}),E("prepared","executing"),E("executing","confirmed","效果可核对"),E("executing","uncertain","崩溃 / 超时",{from:"bottom",to:"top",via:[[475,720],[765,720]],lx:645,ly:720,kind:"feedback"}),E("uncertain","confirmed","核验已发生",{from:"right",to:"left",via:[[915,787],[915,647]],lx:916,ly:733,kind:"feedback"}),E("uncertain","action-failed","核验失败或未发生",{from:"bottom",to:"bottom",via:[[765,842],[475,842]],kind:"feedback",lx:620,ly:842})],
paths:[{name:"任务正常完成",ids:["queued","running","complete"]},{name:"登录失效后恢复",ids:["queued","running","login","running","complete"]},{name:"暂停后核验恢复",ids:["queued","running","pausing","paused","running","complete"]},{name:"动作结果未知后确认发生",ids:["prepared","executing","uncertain","confirmed"]},{name:"动作核验失败或未发生",ids:["prepared","executing","uncertain","action-failed"]}],
notes:[["版本恢复的明确边界","旧任务继续使用原能力版本。若必须换版本,先检查进度兼容性,再建立关联的新任务,继承已完成对象和动作账目。"],["人工接管与机器执行互斥","暂停与接管通过明确控制权移交。恢复后重新核验账号、页面和目标对象;跨任务去重减少重复,但不能替代实际结果核对。"]]
},
{id:"session",name:"登录会话",title:"用户自行登录 下次复用同一浏览器资料",description:"平台官方页面承接扫码与验证码登录。固定账号资料目录保存可持久化状态,每次任务重新核验会话和账号。",w:1220,h:842,
groups:[G(25,100,1170,650,"独立窗口为主线 · 内嵌宿主待验证 · 同账号固定目录或分区")],
nodes:[
N("profile",55,155,250,"分配账号专用会话","固定目录 或稳定 persist 分区","独立浏览器分配固定资料目录;若采用内嵌宿主,则分配稳定 Electron 分区。SQLite 保存资料引用、状态和核验时间。","平台、本地账号标识","专用浏览器资料和账号映射",["按账号固定,不追加新会话随机 ID","重启和升级保留,同账号控制权互斥"],"4.2–4.4"),
N("human",480,155,250,"商家在官方页面登录","扫码 或 官方手机号验证码表单","主线打开独立可最大化浏览器,由商家自行完成登录;内嵌方案须先验证现成宿主与固定执行器接入。业务执行器暂停动作。","未登录或需重新验证的页面","平台完成的人工登录",["我们的产品界面不索取或代收验证码","旧密码本准入需要替换为人工登录与身份核验"],"2、4.1–4.4","confirmed"),
N("verified",905,155,250,"核验登录与账号身份","页面标志 + 可靠账号标识","等待登录跳转完成,读取必要的登录标志和账号标识,再绑定或核对任务账号。","完成登录的当前页面","身份明确且可用的会话",["二维码消失或已有 Cookie 不能单独判定成功","身份不明或与预期账号不同则等待处理"],"4.2"),
N("task",55,345,250,"下一次启动任务","读取原账号映射","软件再次启动或新建平台任务时,读取原账号及固定浏览器目录。","已保存账号记录与任务","要复用的会话或专用目录",["SQLite 上次已登录不证明当前仍有效","浏览器资料保留在当前设备"],"4.2–4.3"),
N("reuse",480,345,250,"复用或重开浏览器","同一账号目录或持久化分区","优先复用已有会话;关闭后在原实例释放控制权后重开同一账号资料。内嵌执行器连接宿主真实页面目标。","原会话或固定账号资料引用","待核验的浏览器会话",["不为每次任务创建随机登录分区","人工登录和业务执行使用同一会话"],"4.2、4.4"),
N("probe",905,345,250,"检查当前会话","有效性与任务账号对应","打开页面检查登录状态、身份和必要页面条件。资料保留且平台会话有效时可直接执行;身份不明或账号不符先等待处理。","重开或现有浏览器会话","可执行、等待登录或等待处理",["身份不明不直接判为会话失效","不承诺永久免登录或固定有效天数","验证码、手机号和原始凭据不写入业务记录"],"4.2"),
N("closed",55,585,250,"关闭浏览器后暂停","保存检查点并释放目录","关闭整个浏览器会结束当前执行会话,运行端暂停;任务结束正常关闭时也保留账号目录。","浏览器关闭事件和当前任务","检查点、未决动作及保留的账号资料",["关闭浏览器不代表执行会继续","重新打开后再核验身份与页面"],"4.2、8"),
N("run",480,585,250,"同会话交给执行器","回到控制台 可最小化浏览器","登录核验完成后由执行器接管同一会话,提供显示浏览器和人工接管入口。","身份已核验的会话与任务","固定链路执行与任务进度",["最小化的运行效果在 Windows/macOS 分别验证","保留可见方式,不默认切换无头运行"],"4.1–4.2、8"),
N("expired",905,585,250,"会话失效 等待登录","到期 退出 清理 或 需重新验证","确认当前未登录或需要平台再次验证时,显示浏览器,由商家完成登录。","明确未登录、会话失效或需要重新验证的页面","明确等待状态与人工登录入口",["不自动操作验证码流程","重新登录成功后再核验账号并恢复"],"4.2、8")
],
edges:[E("profile","human"),E("human","verified","完成登录"),E("verified","run","同会话移交",{from:"bottom",to:"top",via:[[1030,270],[860,270],[860,530],[605,530]],lx:858,ly:494}),E("task","reuse","读取原目录"),E("reuse","probe","打开后核验"),E("probe","run","仍有效",{from:"bottom",to:"top",via:[[1030,500],[605,500]],lx:710,ly:500}),E("probe","expired","确认未登录或需验证"),E("expired","human","重新人工登录",{from:"right",to:"bottom",via:[[1180,622],[1180,285],[605,285]],kind:"feedback",lx:1100,ly:285}),E("run","closed","手动关闭则暂停",{from:"left",to:"right"}),E("closed","reuse","同目录重开",{from:"top",to:"left",via:[[180,475],[450,475],[450,382]],lx:305,ly:475})],
paths:[{name:"首次人工登录后运行",ids:["profile","human","verified","run"]},{name:"下次任务复用有效会话",ids:["task","reuse","probe","run"]},{name:"会话失效重新人工登录",ids:["task","reuse","probe","expired","human","verified","run"]},{name:"关闭浏览器后重开核验",ids:["run","closed","reuse","probe","run"]}],
notes:[["窗口形态与会话保存分别决定","主线仍为独立可最大化窗口,现有内嵌宿主列为首版待验证候选。两者都需保持登录与执行为同一会话,收起或最小化需双端验证;关闭执行浏览器则先暂停。"],["资料实现按宿主适配","独立浏览器使用固定目录,内嵌 Electron 使用稳定 persist 分区。SQLite 保存账号映射和核验状态;原工程的新会话随机标识与密码本准入需改造,不能直接提供跨任务扫码登录复用。"]]
},
{id:"delivery",name:"实施阶段",title:"按验证结果推进的六个阶段",description:"顺序表示依赖关系,没有预设交付日期或处理量承诺。双端基础验证与独立学习验证记录从第一阶段开始。",w:1220,h:832,
groups:[G(25,100,1170,660,"建议实施路线 · 已启动学习后台基建")],
nodes:[
N("foundation",65,155,475,"01 工程基础与单平台验证","学习后台迁入 · 独立存储 · 双端读取","迁入 A2A 学习后台适用模块,接本项目 PostgreSQL 和 Playwright,先验证本机示例,再适配小红书搜索、详情与评论读取。","适用源码、平台、测试账号、学习目标","独立基础骨架、可重复链路和学习验证记录",["独立应用目录、端口和进程归属,不依赖原校园服务","Windows 与 macOS 各自实测;首版尚未完成真实平台验收"],"5.8、10 阶段一","proposed",120),
N("discovery",680,155,475,"02 发现与筛选","商品档案 · 三类判断 · 线索与来源","实现任务与本地线索页面,用带上下文的标注样本比较人工判断。","基础读取能力与样本","误判、漏判、耗时和费用记录",["判断质量与成本分别测量","样本量和可接受标准首轮后细化"],"10 阶段二","proposed",120),
N("assets",680,350,475,"03 学习资产与恢复","校验 · 版本 · 回流 · 修订回归","完善候选校验、试跑记录、发布与回滚,验证常见中断和页面变化。","前期候选、学习资料与失败记录","统一能力包、版本工具和恢复验证",["实际证据和构造场景分别记录","自然失败修订闭环单独验收"],"10 阶段三","proposed",120),
N("followup",65,350,475,"04 跟进闭环","关注 · 私信 · 结果核对 · 回复","分别验证跟进动作、重复候选、发送中断及结果不明后核对,再确定首版开放范围。","可回看线索、测试对象与动作契约","跟进闭环与明确发布范围",["跟进验收不由只读流程通过替代","未知结果先核对,跨任务去重"],"10 阶段四","pending",120),
N("commercial",65,555,475,"05 可收费的双端安装版","安装升级 · 权益 · 后台模型 · 账本","完善管理界面、配置验证发布、设备权益和订单计量,两端分别验证安装生命周期。","已验证业务链路、客户端与服务端","可安装版本与用量对账结果",["模型配置逻辑在前期先以开发配置使用","最低系统版本和架构由依赖验证确定"],"10 阶段五","proposed",120),
N("expansion",680,555,475,"06 扩展覆盖","推荐流 · 多账号 · 新平台 · 手机查看","稳定单平台流程和商业用量后,依次评估多账号、新平台与可选同步。","稳定的单平台产品与需求反馈","新增适配及独立验证结果",["每增加一个平台都新增验证","手机查看与手机执行分别评估"],"10 阶段六","later",120)
],
edges:[E("foundation","discovery"),E("discovery","assets"),E("assets","followup","恢复能力支撑跟进",{from:"left",to:"right"}),E("followup","commercial"),E("commercial","expansion","基础流程与用量稳定",{kind:"optional"})],
paths:[{name:"从可行性验证到扩展",ids:["foundation","discovery","assets","followup","commercial","expansion"]}],
notes:[["基础模块提取与链路验证并行","桌面壳和后台基础按限定范围提取,云端可保留 FastAPI。学习证据与验证记录从原型开始保留;不以整套旧校园工程迁移完成为前提。"],["仍需验证的选择","首发平台、内嵌宿主是否采用、具体模型、跟进开放范围、套餐额度、并行规模和 Midscene 均待验证。Windows 原模板可参考,macOS 安装与发布仍需独立建设。"]]
}
];
const statusLabels={confirmed:"已确认方向 · 逐模块验收",proposed:"实施建议 · 逐模块验证",pending:"范围或效果待验证",later:"后续扩展 · 逐项验证"};
const $=id=>document.getElementById(id);
const esc=value=>String(value).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
const state={tab:0,node:null,focus:false,zoom:1,scenario:0,step:-1,timer:null};
let toastTimer;
function validateDiagrams(){
const ids=new Set();
for(const d of diagrams){
if(ids.has(d.id))throw Error("重复图页 "+d.id);ids.add(d.id);
const nodes=new Map();
for(const n of d.nodes){if(nodes.has(n.id))throw Error("重复节点 "+n.id);nodes.set(n.id,n);if(n.x<0||n.y<0||n.x+n.w>d.w||n.y+n.h>d.h-40)throw Error("节点超出画布 "+d.id+"/"+n.id);if(!n.source||!n.desc)throw Error("缺失节点依据 "+n.id);}
for(const e of d.edges){if(!nodes.has(e.a)||!nodes.has(e.b))throw Error("关系引用无效 "+d.id);}
for(const p of d.paths){for(let i=0;i<p.ids.length;i++){if(!nodes.has(p.ids[i]))throw Error("路径节点无效");if(i&&!d.edges.some(e=>(e.a===p.ids[i-1]&&e.b===p.ids[i])||(e.bi&&e.b===p.ids[i-1]&&e.a===p.ids[i])))throw Error("路径关系缺失 "+d.id+" "+p.ids[i-1]+"→"+p.ids[i]);}}
for(let i=0;i<d.nodes.length;i++)for(let j=i+1;j<d.nodes.length;j++){const a=d.nodes[i],b=d.nodes[j];if(a.x<b.x+b.w&&a.x+a.w>b.x&&a.y<b.y+b.h&&a.y+a.h>b.y)throw Error("节点重叠 "+d.id);}
}
return true;
}
function endpoint(n,side,at=.5){if(side==="top")return[n.x+n.w*at,n.y];if(side==="bottom")return[n.x+n.w*at,n.y+n.h];if(side==="left")return[n.x,n.y+n.h*at];return[n.x+n.w,n.y+n.h*at];}
function edgePoints(e,map){
const a=map.get(e.a),b=map.get(e.b);
const vertical=Math.abs(a.x+a.w/2-b.x-b.w/2)<45;
const from=e.from||(vertical?(b.y>a.y?"bottom":"top"):(b.x>a.x?"right":"left"));
const to=e.to||(vertical?(b.y>a.y?"top":"bottom"):(b.x>a.x?"left":"right"));
const start=endpoint(a,from,e.fromAt??.5),end=endpoint(b,to,e.toAt??.5);
let middle=e.via||[];
if(!e.via&&start[0]!==end[0]&&start[1]!==end[1]){
if(from==="bottom"||from==="top"){const y=(start[1]+end[1])/2;middle=[[start[0],y],[end[0],y]];}
else{const x=(start[0]+end[0])/2;middle=[[x,start[1]],[x,end[1]]];}
}
return[start,...middle,end];
}
function labelPosition(points){let longest=0,pos=points[0];for(let i=1;i<points.length;i++){const a=points[i-1],b=points[i],len=Math.hypot(b[0]-a[0],b[1]-a[1]);if(len>longest){longest=len;pos=[(a[0]+b[0])/2,(a[1]+b[1])/2];}}return pos;}
function lines(text,maxUnits){const result=[];let part="",units=0;for(const c of text){const width=/[\x00-\x7F]/.test(c)?.55:1;if(units+width>maxUnits&&part){result.push(part);part="";units=0;}part+=c;units+=width;}if(part)result.push(part);return result;}
function nodeMarkup(n){
const titleLines=lines(n.title,(n.w-30)/15),subLines=lines(n.sub,(n.w-30)/11);
const titleY=n.h>90?34:29;
const title=titleLines.map((line,i)=>`<tspan x="16" y="${titleY+i*20}">${esc(line)}</tspan>`).join("");
const subtitle=subLines.map((line,i)=>`<tspan x="16" y="${titleY+titleLines.length*20+3+i*16}">${esc(line)}</tspan>`).join("");
return `<g class="node" data-node="${esc(n.id)}" role="button" tabindex="0" aria-label="${esc(n.title)} 查看详情" transform="translate(${n.x} ${n.y})"><title>${esc(n.title+":"+n.desc)}</title><rect class="node-box" width="${n.w}" height="${n.h}" rx="8"/><text class="node-title">${title}</text><text class="node-sub">${subtitle}</text></g>`;
}
function buildSVG(d){
const map=new Map(d.nodes.map(n=>[n.id,n]));
const groups=d.groups.map(g=>`<g><rect class="group-rect" x="${g.x}" y="${g.y}" width="${g.w}" height="${g.h}" rx="10"/><text class="group-title" x="${g.x+17}" y="${g.y+25}">${esc(g.title)}</text></g>`).join("");
const edges=d.edges.map((e,i)=>{
const pts=edgePoints(e,map),path=pts.map((p,j)=>(j?"L":"M")+p.join(" ")).join(" ");
const marker=e.kind==="feedback"?"feedback":"normal";
return `<path class="edge ${e.kind||""}" data-edge="${i}" d="${path}" marker-end="url(#arrow-${marker})" ${e.bi?`marker-start="url(#arrow-${marker}-start)"`:""}><title>${esc(map.get(e.a).title+" → "+map.get(e.b).title+(e.label?":"+e.label:""))}</title></path>`;
}).join("");
const labels=d.edges.map((e,i)=>{if(!e.label)return"";const p=labelPosition(edgePoints(e,map)),x=e.lx??p[0],y=e.ly??p[1],w=lines(e.label,200)[0].split("").reduce((a,c)=>a+(/[\x00-\x7F]/.test(c)?5.5:10),0)+10;return `<g class="edge-label ${e.kind||""}" data-label="${i}"><rect x="${x-w/2}" y="${y-11}" width="${w}" height="19" rx="3"/><text x="${x}" y="${y+2}" text-anchor="middle">${esc(e.label)}</text></g>`;}).join("");
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${d.w} ${d.h}" width="${d.w}" height="${d.h}" aria-labelledby="svg-title svg-description" role="img"><title id="svg-title">${esc(d.title)}</title><desc id="svg-description">${esc(d.description)} 所有节点可通过键盘聚焦并查看详情。</desc><defs>${["normal","active","feedback"].map(c=>`<marker id="arrow-${c}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path class="arrow-tip ${c==="normal"?"":c}" d="M 1 1 L 9 5 L 1 9 Z"/></marker><marker id="arrow-${c}-start" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path class="arrow-tip ${c==="normal"?"":c}" d="M 1 1 L 9 5 L 1 9 Z"/></marker>`).join("")}</defs><rect class="paper" width="${d.w}" height="${d.h}"/><text class="svg-heading" x="28" y="38">${esc(d.title)}</text><text class="svg-sub" x="28" y="64">整体设计 · 实现状态见 README · 点击节点查看职责和来源</text>${groups}<g class="edges">${edges}</g><g class="labels">${labels}</g><g class="nodes">${d.nodes.map(nodeMarkup).join("")}</g><text class="svg-sub" x="28" y="${d.h-23}">来源:AI 获客助手整体方案 v0.2 · 2026-10-22</text><text class="svg-sub" x="${d.w-28}" y="${d.h-23}" text-anchor="end">${esc(d.name)} / ${d.nodes.length} 个节点</text></svg>`;
}
function renderTabs(){
$("tabs").innerHTML=diagrams.map((d,i)=>`<button type="button" class="tab" id="tab-${d.id}" role="tab" aria-selected="${i===state.tab}" aria-controls="viewport" tabindex="${i===state.tab?0:-1}" data-tab="${i}"><span class="index">${String(i+1).padStart(2,"0")}</span>${esc(d.name)}</button>`).join("");
}
function render(){
const d=diagrams[state.tab];renderTabs();
$("diagram-title").textContent=d.title;$("diagram-description").textContent=d.description;$("diagram-count").textContent=`${d.nodes.length} 节点 / ${d.edges.length} 关系`;
$("viewport").innerHTML=buildSVG(d);$("viewport").setAttribute("aria-labelledby","tab-"+d.id);
$("scenario").innerHTML=d.paths.map((p,i)=>`<option value="${i}">${esc(p.name)}</option>`).join("");$("scenario").value=String(state.scenario);
d.notes.forEach((n,i)=>{$("note-title-"+(i?"b":"a")).textContent=n[0];$("note-"+(i?"b":"a")).textContent=n[1];});
renderDetail();renderHighlights();renderZoom();
}
function renderDetail(){
const d=diagrams[state.tab],n=d.nodes.find(n=>n.id===state.node)||d.nodes[0];
$("detail").innerHTML=`<div class="detail-kicker">${esc(d.name)} / 节点职责</div><h3>${esc(n.title)}</h3><span class="status-tag">${esc(statusLabels[n.status])}</span><p class="detail-desc">${esc(n.desc)}</p><dl><dt>输入与前提</dt><dd>${esc(n.input)}</dd><dt>产出或状态</dt><dd>${esc(n.output)}</dd><dt>核对要点</dt><dd><ul>${n.checks.map(s=>`<li>${esc(s)}</li>`).join("")}</ul></dd></dl><div class="detail-source">来源:整体方案 §${esc(n.source)}<br><a href="整体方案-v0.2.md">查看完整文字方案 ↗</a></div>`;
}
function renderHighlights(){
const d=diagrams[state.tab],p=d.paths[state.scenario],route=state.step>=0?p.ids.slice(0,state.step+1):[],current=state.step>=0?p.ids[state.step]:null;
const neighbors=new Set([state.node]);for(const e of d.edges){if(e.a===state.node)neighbors.add(e.b);if(e.b===state.node)neighbors.add(e.a);}
$("viewport").querySelectorAll("[data-node]").forEach(el=>{const id=el.dataset.node;el.classList.toggle("selected",id===state.node);el.classList.toggle("route",route.includes(id));el.classList.toggle("current",id===current);el.classList.toggle("dim",state.focus&&Boolean(state.node)&&!neighbors.has(id));el.setAttribute("aria-pressed",String(id===state.node));});
d.edges.forEach((e,i)=>{
const highlighted=route.some((id,j)=>j&&((route[j-1]===e.a&&id===e.b)||(e.bi&&route[j-1]===e.b&&id===e.a)));
const dim=state.focus&&Boolean(state.node)&&e.a!==state.node&&e.b!==state.node;
const el=$("viewport").querySelector(`[data-edge="${i}"]`);el.classList.toggle("highlight",highlighted);el.classList.toggle("dim",dim);el.setAttribute("marker-end",`url(#arrow-${highlighted?"active":e.kind==="feedback"?"feedback":"normal"})`);if(e.bi)el.setAttribute("marker-start",`url(#arrow-${highlighted?"active":e.kind==="feedback"?"feedback":"normal"}-start)`);
$("viewport").querySelector(`[data-label="${i}"]`)?.classList.toggle("dim",dim);
});
$("step-label").innerHTML=state.step<0?"设计路径,尚未开始演示":`${state.step+1} / ${p.ids.length} · <b>${esc(d.nodes.find(n=>n.id===current).title)}</b>`;
$("prev").disabled=state.step<=0;$("next").disabled=state.step>=p.ids.length-1;$("play").textContent=state.timer?"暂停":"播放";
}
function renderZoom(){const svg=$("viewport").querySelector("svg");svg.style.width=state.zoom*100+"%";svg.style.minWidth=Math.round(820*state.zoom)+"px";$("zoom-value").textContent=Math.round(state.zoom*100)+"%";$("zoom-out").disabled=state.zoom<=.6;$("zoom-in").disabled=state.zoom>=2;}
function stop(){if(state.timer){clearInterval(state.timer);state.timer=null;}$("play").textContent="播放";}
function selectNode(id){state.node=id;renderDetail();renderHighlights();}
function setTab(index,updateHash=true){stop();state.tab=index;state.node=null;state.step=-1;state.scenario=0;state.zoom=1;render();$("viewport").scrollTo(0,0);if(updateHash){try{history.replaceState(null,"","#"+diagrams[index].id);}catch{location.hash=diagrams[index].id;}}}
function stepTo(index){const p=diagrams[state.tab].paths[state.scenario];state.step=Math.max(-1,Math.min(index,p.ids.length-1));if(state.step>=0)state.node=p.ids[state.step];renderDetail();renderHighlights();}
function toast(message){clearTimeout(toastTimer);$("notice").textContent=message;toastTimer=setTimeout(()=>{$("notice").textContent="";},3200);}
function download(blob,filename){const url=URL.createObjectURL(blob),a=document.createElement("a");a.href=url;a.download=filename;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),5000);}
function exportSVG(){
const original=$("viewport").querySelector("svg"),copy=original.cloneNode(true);
const from=[original,...original.querySelectorAll("*")],to=[copy,...copy.querySelectorAll("*")];
const properties=["fill","stroke","stroke-width","stroke-dasharray","stroke-linecap","stroke-linejoin","font-family","font-size","font-weight","text-anchor"];
from.forEach((el,i)=>{const css=getComputedStyle(el);properties.forEach(p=>{const value=css.getPropertyValue(p);if(value)to[i].style.setProperty(p,value);});to[i].style.opacity="1";to[i].removeAttribute("tabindex");to[i].removeAttribute("aria-pressed");to[i].removeAttribute("role");});
const d=diagrams[state.tab];copy.setAttribute("width",d.w);copy.setAttribute("height",d.h);copy.style.removeProperty("width");copy.style.removeProperty("min-width");copy.style.removeProperty("height");
const xml=new XMLSerializer().serializeToString(copy);return new Blob([xml],{type:"image/svg+xml;charset=utf-8"});
}
async function exportPNG(){
const blob=exportSVG(),url=URL.createObjectURL(blob),image=new Image(),d=diagrams[state.tab];
try{await new Promise((resolve,reject)=>{image.onload=resolve;image.onerror=()=>reject(Error("无法生成图片"));image.src=url;});const canvas=document.createElement("canvas"),ratio=2;canvas.width=d.w*ratio;canvas.height=d.h*ratio;const context=canvas.getContext("2d");if(!context)throw Error("浏览器不支持图片导出");context.scale(ratio,ratio);context.drawImage(image,0,0);const output=await new Promise(resolve=>canvas.toBlob(resolve,"image/png"));if(!output)throw Error("图片生成失败");download(output,"assistant-"+d.id+".png");toast("PNG 已导出,包含当前主题和整张图");}finally{URL.revokeObjectURL(url);}
}
$("tabs").addEventListener("click",event=>{const button=event.target.closest("[data-tab]");if(button)setTab(Number(button.dataset.tab));});
$("tabs").addEventListener("keydown",event=>{if(!["ArrowLeft","ArrowRight","Home","End"].includes(event.key))return;event.preventDefault();let next=state.tab;if(event.key==="Home")next=0;else if(event.key==="End")next=diagrams.length-1;else next=(next+(event.key==="ArrowRight"?1:-1)+diagrams.length)%diagrams.length;setTab(next);$("tab-"+diagrams[next].id).focus();});
$("viewport").addEventListener("click",event=>{const node=event.target.closest("[data-node]");if(node)selectNode(node.dataset.node);});
$("viewport").addEventListener("keydown",event=>{if((event.key==="Enter"||event.key===" ")&&event.target.matches("[data-node]")){event.preventDefault();selectNode(event.target.dataset.node);}});
$("focus").addEventListener("change",event=>{state.focus=event.target.checked;if(state.focus&&!state.node)state.node=diagrams[state.tab].nodes[0].id;renderDetail();renderHighlights();});
$("zoom-in").addEventListener("click",()=>{state.zoom=Math.min(2,Math.round((state.zoom+.2)*10)/10);renderZoom();});
$("zoom-out").addEventListener("click",()=>{state.zoom=Math.max(.6,Math.round((state.zoom-.2)*10)/10);renderZoom();});
$("fit").addEventListener("click",()=>{state.zoom=1;renderZoom();$("viewport").scrollTo(0,0);});
$("scenario").addEventListener("change",event=>{stop();state.scenario=Number(event.target.value);state.step=-1;state.node=null;renderDetail();renderHighlights();});
$("next").addEventListener("click",()=>{stop();stepTo(state.step+1);});$("prev").addEventListener("click",()=>{stop();stepTo(state.step-1);});
$("reset").addEventListener("click",()=>{stop();state.node=null;stepTo(-1);});
$("play").addEventListener("click",()=>{if(state.timer){stop();return;}const path=diagrams[state.tab].paths[state.scenario];if(state.step>=path.ids.length-1)state.step=-1;stepTo(state.step+1);state.timer=setInterval(()=>{if(state.step>=path.ids.length-1){stop();renderHighlights();}else stepTo(state.step+1);},1450);renderHighlights();});
$("export-svg").addEventListener("click",()=>{try{download(exportSVG(),"assistant-"+diagrams[state.tab].id+".svg");toast("SVG 已导出,可继续编辑和放大");}catch(error){toast("导出失败:"+error.message);}});
$("export-png").addEventListener("click",async()=>{const button=$("export-png");button.disabled=true;try{await exportPNG();}catch(error){toast("导出失败:"+error.message);}finally{button.disabled=false;}});
function setTheme(theme){document.documentElement.dataset.theme=theme;$("theme").textContent=theme==="dark"?"切换浅色":"切换深色";$("theme").setAttribute("aria-label",$("theme").textContent);try{localStorage.setItem("assistant-plan-theme",theme);}catch{}}
$("theme").addEventListener("click",()=>setTheme(document.documentElement.dataset.theme==="dark"?"light":"dark"));
$("search").addEventListener("input",()=>{const query=$("search").value.trim().toLocaleLowerCase();$("results").replaceChildren();if(!query){$("search").setAttribute("aria-expanded","false");return;}const found=[];diagrams.forEach((d,index)=>d.nodes.forEach(n=>{if((n.title+" "+n.sub+" "+n.desc).toLocaleLowerCase().includes(query))found.push({d,index,n});}));found.slice(0,9).forEach(({d,index,n})=>{const button=document.createElement("button");button.type="button";button.className="result";const small=document.createElement("small");small.textContent=d.name;button.append(small,document.createTextNode(n.title));button.addEventListener("click",()=>{setTab(index);selectNode(n.id);$("search").value="";$("results").replaceChildren();$("search").setAttribute("aria-expanded","false");$("viewport").querySelector(`[data-node="${n.id}"]`).focus({preventScroll:true});});$("results").append(button);});if(!found.length){const empty=document.createElement("div");empty.className="search-empty";empty.textContent="没有匹配节点,试试 证据、模型 或 版本";$("results").append(empty);}$("search").setAttribute("aria-expanded","true");});
document.addEventListener("click",event=>{if(!event.target.closest(".search-box")){$("results").replaceChildren();$("search").setAttribute("aria-expanded","false");}});
$("search").addEventListener("keydown",event=>{if(event.key==="Escape"){$("search").value="";$("results").replaceChildren();$("search").setAttribute("aria-expanded","false");}if(event.key==="ArrowDown"){$("results").querySelector("button")?.focus();event.preventDefault();}});
window.addEventListener("hashchange",()=>{const index=diagrams.findIndex(d=>"#"+d.id===location.hash);if(index>=0&&index!==state.tab)setTab(index,false);});
document.addEventListener("visibilitychange",()=>{if(document.hidden)stop();});
validateDiagrams();
try{const saved=localStorage.getItem("assistant-plan-theme");setTheme(saved||"light");}catch{setTheme("light");}
const initial=diagrams.findIndex(d=>"#"+d.id===location.hash);if(initial>=0)state.tab=initial;
render();
// 供本地回归检查使用,不调用任何平台或模型接口。
window.assistantPlan={diagrams,validate:validateDiagrams,exportSVG};
</script>
</body>
</html>