Commit Graph

11 Commits

Author SHA1 Message Date
ThearchyHelios 03c18c4d78 界面文案整改:口语改书面、术语统一、不再露出实现细节;修掉几处说错的提示;新增文案检查防回潮
一、文案(前端约 1600 处,后端传到界面的约 880 处)
- 术语统一:工作流层面用「运行」(继续运行、重新运行、试运行),节点和工具层面用「执行」,不再用「跑」;
  总时长 / 执行时长 / 等待审批;服务端;助手;Agent;token;工作流;固定版本;模型接入;系统提示;批准 / 驳回;受管级别
- 口语改书面:取不到、没取回来、读不懂、连不上、多半、照样、兜底、揉成这类说法改成「无法 / 未能 / 失败 / 请」;去掉拟人写法
- 不再露出实现细节:界面上不写 checkpoint、同一拍、哈希(凭证处除外),不写改版历史(「以前…」「老数据」);
  报错、校验、门禁、自动修复里裸写的配置键名和枚举值换成界面上的中文标签,由新的 backend/app/engine/labels.py 统一提供;
  发布弹窗里结论句的选项不再是 off / require_citation
- 同一件事只留一种说法:生成阶段文案、出具档位说明、画布锁定提示各合成一份
- 去掉界面上的工业场景示例(MES、ERP、「张工」「王工」)和真实产品名,换成通用示例

二、说错的提示(都有测试证明)
- 数据库测连接失败时的专用提示以前从不出现(按报错文字全等比较,永远比不上):改为按错误类型判断,不再拼出重复的前缀
- 「设置 → 供应商」改指「设置 → 模型接入」;「在设置里建数据源」改指「数据」页
- 缺少 API Key、模型接入已停用的报错不再夹英文,写明去哪里填
- 两处原因说错(「这种引用在后续版本支持」「这一版还不支持」)改成真实原因
- 节点说明里不准确的说法改正:代码节点在 microVM 或系统沙箱中执行;报告撰写、口径卡的说明不再说过头
- 导出的证据 CSV 里两列同名,第三列改为「片段编号」

三、前后端对齐
- 前端按后端原文识别日志、报错的地方,新旧两种写法都认得(历史运行里存的是旧文字),逐条补了新旧原文的核对
- 后端交回模型的原指令放进单独的 for_model 字段,前端在解析响应时去掉,不会显示到界面上

四、检查
- 检查脚本里按原文匹配的断言改成新文案;改完会变成永远成立的反向断言逐条修正,并做了变异验证
- 新增 check-copy(静态扫描前端界面字符串和后端上界面的文案):再出现「跑」「后端」「多半」「令牌」、
  界面上的 Copilot、裸写的配置键名、半角标点等就报错;确需保留的写进 scripts/copy-allow.json 并注明理由
2026-09-30 08:05:15 +08:00
ThearchyHelios 3e311ec551 第三波收尾:跨模块整合、全站扫尾、检查脚本整合;网页工具认得本机代理的 fake-ip
整合(3c-integrate)
- 公共层:跨天时长(formatSpan / formatOffset / formatLapse)、本机署名(lib/actor)、检索分类色令牌、
  数据源目录(useDatasources)收进公共层;失败说明能一次列出多个写坏的自定义工具,查询超时指到数据源的查询时限
- 管理页收尾:设置页接入统一的离开前确认;检查脚本不再把沙箱里的真实值打进输出
- 画布 ↔ 右栏:点出具印章、点节点能跳到右栏对应位置;「定位」直接聚焦到出问题的字段
- 回答流 / 问数据 / 记录页:失败信息带「去哪修」且不再被翻译两遍;协作团队的结局按执行截取;
  等审批时断流重连、而运行已在空档里跑完的,补全事件

扫尾(3c-sweep)
- 术语统一:开始运行 / 继续运行、工作流最大步数;未保存的运行统一叫「未保存的工作流」(后端新运行同步)
- 沙箱报错不再带 Python 异常类名;新增 check-tokens,未定义的 CSS 变量为 0,两个旧别名迁移后删除

检查脚本(3c-checks)
- check-ui 跟上新界面(运行列表按 data 属性找行),加上令牌与选中态的回归断言,输出里的名字打码
- 新增 check-all:按顺序跑全部检查、汇总退出码,--repeat 列出偶发项
- 单节出错只记这一节失败、不再带崩整个脚本;外壳遥测浮层「重新检测」时焦点不再掉到 body

网页工具与 fake-ip
- Clash / mihomo 一类代理开 TUN + fake-ip 时,查什么都先拿到 198.18 段的假地址,被当成内网拦截。
  新增 AGENTLAB_HTTP_TOOL_FAKE_IP_RANGES(默认空):落在段里的解析结果改向公共 DNS(DoH)核实真实地址,
  公网放行、内网照拦、核实不了按拦截处理。DoH 默认先问境外的,境内公共 DNS 会对被封锁的域名返回污染结果
- 列表型环境变量接受逗号分隔(原来注释说逗号分隔、实际只认 JSON 数组);域名解析改为异步,不再阻塞事件循环

验证(沙箱):tsc 干净,pytest 866 passed(两遍),检查脚本全绿。
2026-09-27 23:37:32 +08:00
ThearchyHelios 86a0928512 知识库页不再把"退回本地哈希"藏起来:配了语义模型却没连上时说实话,也不再劝人重建
本机配的是 LM Studio 上的 text-embedding-qwen3-embedding-4b。服务启动时它没起,
load_setting 连不上,整个进程就一直用本地哈希——它后来起了也不会自己连上,原因只在
服务端日志里留一句。页面按"配的是不是 local"判断,于是:
- 不标"没有语义能力",看着一切正常,检索却一直是纯关键词;
- 高亮"重建索引(73 段)"——那些"对不上"的向量正是 qwen3 建好的,一点就拿哈希
  覆盖掉,连上之后还得再重建一遍。

改法:
1. 后端记下保存的配置为什么没生效(embeddings.unavailable_reason),状态接口多报
   fallback / fallback_reason:配的是语义模型、实际在用本地哈希。重新配置成功就清掉。
2. 页面按实际生效的 embedder(has_semantics)标"没有语义能力"。退回时说清配的是哪个、
   在哪、为什么没连上,给「重新连接」——发的就是保存着的那份配置,服务起来后点它即可,
   不用重启;"重建索引"收起,存量提示改口成先别重建。明确选了本地哈希的照旧:标没有
   语义能力,重建按钮还在。

验证:test_kb 新增一条,走完"启动连不上 → 状态里报退回并带原因 → 重新连上后消失 →
明确选本地不算退回"。check-ui 新增一组伪造状态接口的断言(退回 / 重新连接 / 明确选
本地,写请求一律拦下);把页面换回旧版本跑,这组挂 8 项。后端 471 passed,tsc 与
check-decode / guards / stream / ui 全绿;本机状态接口现在如实报出退回和原因。
2026-09-25 19:09:19 +08:00
ThearchyHelios 3b2c082743 check-ui 跟上「在画布里打开」的新行为,而且不再往库里写图
429398a 把「在画布里打开」改成新建一张工作流、送去它自己的地址,但 check-ui 还断言
落在 /studio,那次提交也没跑 check-ui。更糟的是按钮本身会写库:照旧断言跑一次,库里
就多一张「问数据:…」。

现在拦下建图那次 POST,拿它自己发出去的图回一个"已建好",之后对这张图的读取也由检查
自己答;校验、变量分析只算不写,放行;其余写操作一律拦掉。断言改成:建了新图且送到
它的地址、建的是这一轮的图、没被别的工作流盖掉、没弹"未保存改动",最后核对库里的
工作流数前后不变。

check-ui 另有一项"本地哈希要标出没有语义能力"仍不过:配置的向量模型连不上、实际退回
local-hashing 时,页面按 kind === 'local' 判断,不提示。这是本分支之前的问题(11780ed),
这里不动。
2026-09-25 17:04:56 +08:00
ThearchyHelios 7b04162540 Excel/CSV 变成能查的表、PPT 读得进来、跑挂了能接着跑、文档和工具有了地址
四件事,其中 Excel 那条不是"多支持一种格式",是补一个和地基冲突的缺口。

**Excel / CSV → 可以用 SQL 查的表。**

这个项目有条不可回退的决策:叙述层无算术权限,所有算术下沉到 SQL 或口径卡,
叙述只能引用(engine/issuance.py)。表格传进知识库只能被切块检索,数字就成了
模型从片段里"读"出来的——复核层拦得住凭空多出的数字,拦不住"从一堆片段里
读错了一格"。所以表格必须变成表。

不新增数据源类型:kind='sqlite' 早就支持,build_url 拿 database 当文件路径。
落成 .db 文件再建一条 DataSource 指过去,下游一行都不用改——SQL 守卫、结构
探查、db_query__<name> 工具、查询快照进工件库、Copilot 的数据源清单,全都白拿。
建表走 sqlite3 直连而不是 run_query:guard 的 _ALWAYS_DENIED 里有 create,
那是对的(模型不该能建表),而导入是我们自己发起的可信操作。

四个语义是这块的全部分量:

- **类型要推,不能全存 TEXT。** SQLite 是动态类型,但列亲和性决定排序和比较:
  整数列落成 TEXT 的话 '10' < '9',SUM() 在混入空串时静默出错,而没有任何
  地方会报。逐列扫描推断 INTEGER/REAL/TEXT,空单元格存 NULL 不存 "",
  日期存 ISO(字典序恰好等于时间序)。测试里那条 ORDER BY 要是 9<10 而不是
  '10'<'9',守的就是整块的意义。
- **表头取第 1 行,但不猜。** 不做"自动找表头"的启发式:猜错的表现是列名变成
  一行数据,没有任何征兆,直到某天有人发现汇总一直少一行。改成 header_row
  可填,并把推断出的列名和类型当场回显——错了一眼看见。
- **同名重传原地替换,id 和 name 都不变。** 名字会成为工具名(db_query__sales)
  而工具名写进了保存过的图,另起一个 sales_2 等于悄悄让那些图失效。
- **中文原样保留。** 一开始把 sheet 名清洗成了 ASCII,结果「明细」「汇总」都变成
  sheet、sheet_2,模型和用户都认不出哪张是哪张——而 SQLite 的标识符本来就允许
  非 ASCII,那道清洗纯属自找麻烦。现在只替换会断句的空白和标点。

实测:两 sheet 的中文 xlsx → 两张表,金额列 INTEGER,跨表 JOIN + SUM 跑通,
Copilot 的工具清单里带着表名。CSV 按 utf-8-sig / utf-8 / gb18030 依次试——
GBK 的 CSV 是 Excel 另存为的默认行为,按 UTF-8 读会得到乱码列名,而乱码不报错。

知识库那边顺手改了指路:传 .xlsx/.csv 进去不再说"不是 UTF-8 文本",
而是告诉你该传到数据源那边、以及为什么。

**PPT。** 文本框和表格分开取(表格的 has_text_frame 是 False),幻灯片之间
留空行当段落边界。.ppt/.doc/.xls 这些老二进制格式认出来再拒,说清楚"另存为
新格式"——以前它们会掉进"不是 UTF-8 文本"那个兜底,对着一个 .ppt 说这话,
用户根本不知道该怎么办。

**问数据页的「接着跑」。** 挂了那一轮不该只剩一行红字:断点一直在 checkpoint
里躺着。按钮挂在 approvalsFor 那个每轮插槽里(本来就托着「看执行过程」和
「跑一下」)。不带图——问数据页改不了图,这里就是"用原来的配置重试失败的
那一步",配置本身错了要到画布上改。实测:续的是同一次运行(8→13 条事件),
只跑了 lp 往后,轨迹里有 run.resumed。

**文档 / 工具的地址。** /knowledge/kb/:docId 是新页面——文档原来连详情视图都
没有,而切块这层做了不少事(按行边界留重叠、跨块续表头),结果一处都看不见。
现在逐段列出来带字数和 token 数,还标出"这份文档里混着两个模型建的向量"
(换模型只重建了一部分时真会发生,而那会让这部分内容悄悄退回关键词检索)。
新增 GET /kb/documents/{id}。工具那边 tab key 从 tools 改成 library,
否则地址是 /tools/tools/<id>。

验证:后端 265 passed(新增 test_tabular.py 16 条);tsc 干净;三个 check 全过,
check-ui 补了文档/工具两节;四块都在真浏览器/真后端上端到端跑过。
开发库里造的演示数据源、会话和运行已清干净。
2026-09-20 16:19:07 +08:00
ThearchyHelios 542bcb65bb 一次对话有自己的地址了:/chat/<chatID>,运行、画布、各页标签同理
在此之前 react-router 只管到页面一级,「页面内部在看哪个」全在 store 和
localStorage 里,全站一处 useParams / useSearchParams / useNavigate 都没有。
后果:

- 一次对话没有地址。想把「这个结论是怎么查出来的」发给同事,只能发截图。
- 刷新回到上次那个对话靠 localStorage,换台机器、换个浏览器就丢。
- 浏览器前进/后退在页面内部彻底失效——切了五个对话,后退一下直接离开问数据页。
- /settings 永远落在「模型接入」,指不到数据源那一屏。

    /chat/:conversationId     /studio/:workflowId     /runs/:runId
    /knowledge/:tab           /tools/:tab             /settings/:tab

每页写两条显式路由(带 id / 不带 id)而不是可选参数 `:id?`:「没带 id」是真要
单独处理的一种情况(落到上次那个、或列表第一个),藏在一个问号里下次就没人
记得它存在了。

**URL 驱动 store,不是反过来。** 两边都能写就一定会打架——点一下列表,store
改了、地址栏没动,刷新又跳回旧的。所以 useConversations.currentId 降级成 URL
的派生缓存,唯一写入口是 ChatPage 的那个同步 effect;列表里的切换、新建、删除
一律改成导航。conversations.load() 不再自作主张决定「当前看哪个」,
remove() 改成返回「接下来该去哪」交给调用方。

localStorage 留着,但它现在只回答另一个问题:**打开不带 id 的 /chat 该去哪**。

**指向不存在的东西**要优雅:等列表加载完再判(在那之前"找不到"是假的,照着它
跳会让每次深链接进入都先闪一下错误提示),落到一个真实的,并说明为什么换了
地方。RunsPage 特别一点——列表只取前 100 条,而一条老运行的链接必须也能打开,
所以详情按 id 直接取,不在列表里找。

**画布多两道闸。** 选择器里本来就有"未保存改动"的确认,但那道闸在它的 onClick
里,而前进/后退会绕过选择器直接换图——改了一半的画布就这么没了。URL→画布那个
effect 里补一道,取消就把地址退回去。

过程中实测抓到三个问题,都修了:
- 同一个坏地址弹三次"那张工作流不在了":effect 在请求回来之前又跑了两遍,
  加了在途去重(chat 那边同样的毛病弹两次)。
- 「在画布里打开」现在直接把人送去 /studio,而那是一张还没存、没有 id 的草稿图。
  不带 id 就落到第一张图的逻辑会拿第一张把它盖掉,还会先弹一句莫名其妙的
  "有未保存的改动"——加了 nodeCount 判断让开。这个坑其实原来就在,只是以前
  要手动点导航才踩得到。
- 删掉当前打开的那张图之后地址还指着它,刷新会撞上"不在了"。

顺带:三个 tab 页共用一个 useTabRoute(放在 Tabs 旁边,tab 和它的地址是同一件
事,拆两处迟早各改各的),认不出的 tab 名回落并把地址一起纠正过来。
「在画布里打开」不再是 toast 让用户自己去点导航,直接送过去。

验证:tsc 干净;后端 249 passed(没碰后端,跑一遍确认没带坏);
check-decode / check-stream 不回归;check-ui 新增「URL 指得到」一节 19 条,
真浏览器里验了深链接、切换改地址、刷新、后退、坏地址、tab 直达、
草稿图不被盖掉、越过列表上限的老运行。
2026-09-20 14:09:40 +08:00
ThearchyHelios 11780ed0b4 设置页能接本机的向量模型了,不用改代码
上一版只有三个写死的选项:本地哈希 / OpenAI 3-small / 3-large。想用本机起的
LM Studio、Ollama、vLLM,只能去代码里调 configure——等于没有。

现在可以填任意 base_url。配套加了一个探测接口(GET /kb/embedding/probe),
按地址列出那台服务上有哪些模型,从列表里选。模型名手填太容易错——LM Studio
里叫 text-embedding-qwen3-embedding-4b,不是 Qwen3-Embedding-4B,而填错的
表现是切换时 404,人还以为是服务没起。

base_url 跟着落库。不存的话重启后会悄悄退回本地哈希,而库里的向量是上一个
模型建的,检索整体退回关键词——用户只看到"重启之后搜得不准了"。

几处说给用户听的:
- 当前用的是什么、几维,直接显示在最上面
- 本地哈希旁边标"没有语义能力"——它是默认值,而这个缺陷不说没人会发现
- 切换成功的提示里带上新维度和"多少条存量向量需要重建"。换模型后维度多半
  变了,存量全部作废,这件事要当场说,不能等人发现搜不准了再去猜

check-ui 加了一节,真浏览器里验过:展开、填地址、探测到本机的
text-embedding-qwen3-embedding-4b、出现「用它」。
2026-09-20 09:34:55 +08:00
ThearchyHelios dcadc0c48b 对话有上下文了,左边也能选会话
三条 run(d983c4e6 / 14103db5 / 08c3de1c)在用户看来是一次对话,系统里却
毫无关联。查下来不是哪里传漏了,是整层缺失:

- 每轮 base_graph=None 从零建图,Copilot 不知道上一轮问过什么
- 每轮 thread_id = run.id,三条独立的 checkpoint 线程
- 前端 turns 只活在 zustand 里,刷新页面就全没了——那三轮当时在界面上
  已经找不回来,只剩 runs 表里三条互不相干的记录
- 后端 13 张表里没有会话这个概念

加两张表(conversations / conversation_turns)。失败的轮次也记:只记成功的,
历史就成了一份美化过的记录,而人回来最想搞清楚的恰恰是上次卡在哪。

Copilot 的上下文带三样:前几轮问答、上一轮那张图(走 _slim 去掉坐标噪音)。
只给问答文字不够——"再按月份拆一下"会让模型重新设计一张图,实测经常接到另
一张表上,答案对不上前一轮。运行时另外注入 {{ input.history }},管的是指代:
用户问"它们平均多少",图里的 agent 得知道"它们"是上一轮那 12 家店。

左侧会话栏只在问数据页出现,可折叠、可改名删除,当前会话存 localStorage。
轮次按会话分桶缓存——切走再切回来要看得到原来的步骤,回源只拿得到问答。
历史轮次的执行过程不预加载,点「看执行过程」才取事件、走现成的 decodeRun。
画布右栏的 Copilot 依附工作流各有一条会话,连说两次"再加一个节点"能接上了
(base_graph 带的是图的结果,带不出"你刚才要我干什么"),但不进左侧列表。

过程中修掉的两个自己引入的坑:

- 第一版在列表为空时自动建一条会话。create 是异步的、StrictMode 把 effect
  跑两遍,于是每次进页面都留下两三条"新对话"。改成用户开口那一刻按需建
- 加了"在上一轮的图上改"这条指令之后,模型照改图协议只发 update_node,
  而问数据页这条路径的累加器从空图起步,操作全部落空,用户收到的是
  "图校验未通过:图是空的,先拖一个节点进来"(会话 f5db9809 的第二轮,
  而它的 explanation 证明上下文其实完全读懂了)。指令改成沿用 id 和 config
  但完整重发,另加一道兜底:空图且上一轮有图时把操作重放到那张图上

check-ui 多一节「会话」:切换、刷新还在、逛一圈不多出空会话。
2026-09-18 17:58:04 +08:00
ThearchyHelios 1444c774cf 模板字段加 {{ }} 补全:从源头消灭那类静默失败
模板取不到值时渲染成空字符串、不报错,所以拼错一个变量名在运行时完全静默。
上一个 commit 是事后校验(标红+建议),这个是事前:打 {{ 就弹出这张图里
实际有的变量,每条带说明和产出它的节点。

**只挂在真正过模板渲染的字段上。** 这一条是整件事最容易做错的地方:
这套里有五个字段长得像但走的是表达式求值——skip_if、loop.condition、
cases[].condition、transform.expression、metrics[].expression——那里要写
`vars.count == 0` 这样的裸路径,不带大括号。在那些字段上挂 {{ 触发器等于
手把手教用户写错。同理 supervisor 的 agents[].system 长得和 llm.system
一模一样,但后端是 cfg.get("system") 直接取值、根本不渲染,那里弹补全会
是个谎;它走的是 AgentList 里的裸 textarea,天然不受影响。

几个不写就会坏的细节:

- 这是完全受控组件。插入后 onChange 会一路走到 store 再把 value 灌回来,
  DOM 光标会跳到末尾——必须等那一帧过去再自己 setSelectionRange 放回去。
  测试里专门验了"插入后继续打字,字落在后面而不是中间"。
- 候选框用文档流里的块,不是绝对定位浮层:属性面板是 overflow-y-auto、
  外层 InspectorSheet 是 overflow-hidden,浮层到边缘直接被切掉。
- 用 onMouseDown 不是 onClick——onClick 在 blur 之后才触发,那时候弹层
  已经关了。
- Escape 要 stopPropagation:外面 InspectorSheet 也在听 Escape,不拦的话
  按一下会连整个属性面板一起关掉。
- 光标前已经打了 | 或 [ 就不再弹——那时候是在写过滤器或下标,不是在选变量。

check-ui 补一组:变量表列得出来、抽屉挤压后画布没被压没(React Flow 容器
高度归零会直接报 error 004)、打 {{ 弹候选、插入成完整写法、光标位置正确。
2026-09-18 10:55:56 +08:00
ThearchyHelios 3de7567377 右栏重构:助手常驻,属性改为盖上去的一层
两个问题都源于同一件事——一栏 360px 被要求同时干两件活。

**没地方引导写问题。** Copilot 输入框是个 38px 高的框,挤在一条长长的流
底下,比它上面任何一块都不起眼;例句被截断成 16 个字的 chip,示范"该说到
什么颗粒度"这个作用就没了,只剩装饰。改成一个组件两种体量:没说过话时它
就是这一栏的主体(竖直居中、三行高、自动聚焦、完整句子的例句),并且告诉
用户它能够到什么(N 个数据源 / M 个工具)——不然"查销售库"这种话有没有意义
只能靠猜。有了对话之后它退到底部两行。不做成两个组件:那就是两个实例,
切换时会卸载其中一个,草稿、光标、输入法候选词全没。

**Copilot 和属性挤在一起。** 它们原是 [助手|属性] 两个 tab,而 tab 是互斥的
——跑图跑到一半点开节点看配置,整条执行过程就从眼前消失,切回来滚动位置和
输入草稿也没了(切 tab 会卸载组件)。改成属性滑出来盖在助手上:助手一直在
下面活着,属性是临时造访者。头部给一个明确的"‹ 助手",Esc 关得掉;盖住
期间底部留一条执行进度,点一下回去——不能因为换了实现就把原来最难受的
那个场景原样留着。

运行控件搬去工具栏。它原先和 Copilot 输入框一起常驻助手栏,两个都是"主要
动作"互相压着;而它们是两种意图(跑这张图 / 改这张图)。运行是对整张图的
动作,和保存、发布同类。有输入字段就弹表单,没有就一键直接跑。

顺带修了两处状态说反的地方:
- 停在人工介入时,承载的节点还在转蓝圈。转圈说的是"它在忙,你等着",而
  实际正相反——它在等你。改成待办色 + 手势图标。
- "继续执行"的去重原先靠"界面上还有没有 waiting 步骤",上一条改动多了一处
  产生 waiting 的地方就失效了。改用事件相邻性判断——那是这两条事件的固有
  性质,不会因为别处改了展示而变。

动效:面板滑入、例句依次入场、正在出字的那一步有一道光扫过(比转圈多说
一件事:它在产出,不只是在等)、空态标记轻微呼吸。全部 240ms 以内,缓动
统一 cubic-bezier(.32,.72,0,1)。另外补上了此前完全没有的
prefers-reduced-motion——这套界面里动的东西不少(边在流动、节点在脉冲、
面板在滑),系统里关了还照播,对前庭敏感的人是实打实的难受。

验证:check-ui 重写了助手栏那一节,核心断言是"选中节点再回来,输入的字
还在"——那正是 tab 方案做不到的。新增一条用 emulateMedia 验证关掉动效后
动画确实停了。真跑了一次带人工审批的图,期间打开属性面板确认进度条还在。
四套检查 + 82 个后端测试全过,生产构建通过。
2026-09-18 00:05:22 +08:00
ThearchyHelios f0fd63f0b9 UI 重构:Copilot 进侧栏,三个页面共用一条助手流
用户的原话是「copilot(包括问答)可以是侧边栏的效果」「显示他的简要思考节点,
使得用户知道他大致在想什么,以及用了哪些工具」。

画布右栏从「运行 / 属性 → 时间线 / 成果 / 事件」(两层嵌套 5 个视图)变成
「助手 / 属性」一层。助手栏里是一条流:你让它做什么、它做了什么、结果是什么。
Copilot 建图和跑图在协议上完全无关(SSE 操作流 vs 事件 WebSocket,不共享
seq 和 node_id),但对用户是一件事的两半,decode.ts 把两种流译成同一种 Step
之后才能排进同一条时间轴。

Copilot 不再是弹窗。弹窗的问题不是样式:每改一次图都要重开一次,关掉之后
需求文本就没了,想微调只能重打;而画布左下角那条浮条(z-30)和 Toast
(z-100)、Modal(z-50)三层各自为政,本身也是重复的一份 Copilot 状态渲染。
两个都删了,输入框就在栏里,说完一句接着说下一句。右栏没删——它是发起运行
和处理审批的唯一入口,RunLauncher 原样保留,只是跑起来会自动收起给执行过程
让位。tab 用 hidden 切换而不是卸载,不然点一下「属性」看个节点再切回来,
输入框草稿和展开状态全丢。

问数据页和运行页换成同一个 AssistantStream + 同一个 decode.ts。RunPanel 里
那套平行的 EVENT_META + describe() 删了:留着两套翻译,同一次运行在不同页面
会读出不同的故事,而用户没法判断哪个是真的。同理合并了两个耗时格式化函数
(画布节点显示"0ms",右边同一步什么都不显示,两块还并排放着)。运行页列表
加了内容摘要——原本每行只有"临时图 · 1.2s · 340 tok",十条长得一模一样,
要分清哪条是哪条只能挨个点开。

跑真实运行时发现两个缺陷:

1. 后端丢事件(app/core/bus.py)。WebSocket 先读历史、再订阅,两步之间图
   正好跑出来的事件既不在历史里也不在实时流里,永远丢了。而 subscribe() 是
   异步生成器,真正注册还要等到第一次迭代,窗口比看上去更大。实测一次跑图
   开头的 node.finished/node.started 消失,界面上表现为某个节点永远在转圈、
   它的子步骤跑到了顶层。改成先 attach 占位、再读历史,重叠部分按 seq 去重。
   tests/test_bus_attach.py 里有一条刻画旧行为的测试,说明这个窗口确实存在。

2. 循环里的多轮审批被算错。我在 S1 按内容去重,但三轮审批的 title 一模一样,
   和 LangGraph 的重放无法区分——结果第 2、3 次真实审批被当成重放吞掉,而
   三次"你驳回了/你放行了"各自成行。改成开/闭配对:开着就是同一次,闭了才是
   新一次;决定折进那条审批行而不是另起一行。真实运行(驳回 → 改写 → 再驳回
   → 改写 → 放行)已收进 fixtures。

验证:三层各守一件事,共 76 条断言全过。check-decode 守翻译(含新的多轮审批
和截断结果集),check-stream 守渲染,新增 check-ui 守"接起来还对不对"——真
浏览器真后端,包括「切走再切回来草稿还在」「唯一的运行入口还在」「原始事件
切得到也切得回」。另外真跑了一次带三轮审批的图和两次 Copilot 生成(60s,
deepseek-v4-pro),截图逐帧看过。后端 78 个测试全过。
2026-09-17 18:29:16 +08:00