Commit Graph

7 Commits

Author SHA1 Message Date
ThearchyHelios e10c4f23fc 界面与交互全面翻修(前两波):运行态有了 HUD 和时间轴,终态会收尾,全站令牌、组件、文案统一
来自 2026-09 的 UI/UX 审查(142 条)。本提交是前两波,第三波(记录页接入时间轴、跨页互链、check-ui 更新、全站扫尾)另行提交。

地基
- 设计令牌:全局规则和 .btn/.field/.chip 放进 CSS 层,Tailwind 工具类重新生效,选中态、开启态不再被吞掉;字号阶梯;
  辅助文字在所有底色上 >= 4.5:1;状态色专用,节点类型色与之拉开色相;「跟随系统」真的跟随,首帧不闪深色;新字标与 favicon
- 基础组件:confirmDialog / promptDialog 取代原生对话框;toast 出错不自动消失、可复制、读屏可播报;离线横幅与断线恢复;
  九种剪影的状态徽标;唯一一份状态文案(lib/status)、格式化(lib/format)、术语(lib/terms)、报错人话(lib/errors)
- 运行态内核:事件折成航迹(run/trace.ts),可投影到任意时刻;取消 / 失败 / 服务重启后统一收尾,不再永远转圈;
  run.status 随事件回写;token 事件不再堆进 events
- 后端:协作成员逐个完成即发事件,调度者决策有事件;时长分执行 / 墙钟 / 等人;API 时间带时区;
  中断的运行也发 stream.end;运行列表可筛选翻页;删除运行 / 工作流有保护;运行默认值接通;可测未保存的配置;
  报错说清发生了什么、为什么、怎么办

功能
- 画布:状态四重编码、固定高度遥测槽、按缩放三档、运行胶囊与 HUD、航迹时间轴与回放、执行轨迹边、
  等待闸门、跟随执行、正式运行只读、一次性时刻(开场点亮、回扫、阻断级联、出具印章)
- 编辑:撤销重做、版本历史、问题面板可定位、三栏可收起、模板与表达式字段区分、变量血缘、快捷键、带搜索的选择器
- 回答流 / 问数据:贴底才跟随滚动、工件下钻、出具横幅列出缺口、答案可复制导出、按会话的忙闲、
  答案不再在 2000 字处截断、重启后这一轮会收尾
- 记录页:待审批 / 运行中 / 失败页签、筛选翻页、实时接流、排错路径指到节点、封存凭证常驻
- 管理页:新的「数据」页、写操作失败都有反馈、弹窗里先测连接、统一持久的连接状态
- 外壳:导航改版、⌘K 命令面板、? 快捷键帮助、404、页面标题与 favicon 状态点、后台提醒

验证(沙箱):tsc 干净,pytest 579 passed,14 个检查脚本中 13 个全绿;check-ui 剩 1 项旧断言(按旧日期格式找运行行),第三波更新。
2026-09-26 23:11:31 +08:00
ThearchyHelios a819f92903 审批只认明确的"同意",而且每个会执行危险操作的地方都得过这道门
三个口子,都实测复现过:

1. 回复"拒绝",工具照样执行。四个节点四套审批解析:工具节点和代码节点对
   回复一律 bool(),"拒绝"、"no" 都是真;{"decision": "approve"} 反被当成
   拒绝;{"approved": "false"} 也是批准。现在收成一份 engine/approval.py:
   只认明确的批准,看不懂的一律按拒绝。填内容的 human input/edit 模式另算——
   裸字符串是交稿,只有结构体里明说"不"才驳回。

2. 可写数据源上的 DELETE 从来不问人。数据源工具是动态生成的,查不到
   ToolSpec,任何审批模式下都被当成安全;is_dangerous_call 写好了但全仓库
   没人调用。现在危险与否逐次判定:registry.call_is_dangerous 统一问,数据源
   工具把判定挂在 metadata["dangerous_if"] 上——同一个 db_query,查询直接
   跑,可写源上的写要先确认。工具节点、agent 节点、协作成员都走它。

   这一步以前没出事,只因为写入从不提交:run_query 走 stream,拿不到结果集就
   抛 ResourceClosedError,连接关闭时再回滚——审批通过的写也没落过库。审批接
   好之后才让写真正提交(先后不能反,反过来就是一个无审批写库的窗口);
   RETURNING 这类带结果集的写照样返回行。工具页的"试一下"是人自己在执行,
   不设审批。

3. 协作团队的成员调工具完全不经审批。成员们并行跑在一个节点里、停不下来等
   人(恢复时节点整个重放,几个人的 interrupt 对不上号),所以需要确认的调用
   不执行,把原因交还给成员让它换做法;节点上显式设成"全部自动放行"才跑。
   前端补了这个选项。顺带修了同一处的两个老问题:成员步数不受 max_agent_steps
   约束;步数用完时把工具的原始返回当结论交上去(agent 节点在 4057cdc 修过
   同一个问题,这里没跟上)——现在补一轮不带工具的收尾。

连带修的:多条审批并存时,交给引擎的是库里存的那份回复,而它存之前被 _safe
截到了 2000 字,解包时又只留 value——人工改过的长稿被砍一截,带了内容的驳回
被当成交稿。现在存全文、只拆自己包的那层 {"value": x}。

test_approval.py 36 例;去掉修复重跑,引擎层 10 例全部失败,各对应一个真问题。
并行驳回那例翻出了一个运行卡死的老 bug,已在上一个提交单独修掉。
338 → 374 passed,连跑三次全绿。
2026-09-25 15:21:20 +08:00
ThearchyHelios 71561a0133 探查失败不再伪装成"还没探查":把实情和下一步交给 agent
从 run 554a0f92 查出来的。那次问「userID=2 是谁」,9 次工具调用花掉 6 次、
前 29 秒没碰到一点真数据:

    + 2.1s  db_schema            → "还没有探查过结构,请在设置页里执行一次"
    + 4.9s  db_schema ×7 个猜的表名 → "没有 user / users / member / …"
    + 7.7s  SELECT 1             → 不信任前面的结果,先探一下连通性
    +12.8s  information_schema.tables   ┐
    +18.3s  information_schema.tables   ├ 自己把 schema 重建一遍
    +21.5s  information_schema.columns  ┘
    +29.2s  终于开始查真数据

根因:shop 的探查**失败过**(Lost connection to MySQL server during query),
但失败被存成了一个"成功但没表"的缓存,schema_synced_at 照常盖戳:

    {"tables": {}, "total": 0,
     "error": "OperationalError: (2013, 'Lost connection...')",
     "available_schemas": ["prod_shop", "shop_dev", "toolsdb", …10 个]}

于是三处同时说假话:界面显示"结构未探查"、工具对 agent 说"还没有探查过"、
而那 10 个候选 schema 就躺在缓存里没人交出去。**用户明明点过探查,是超时了。**

**分清"还没探查"和"探查失败了"。** introspect 的结果加 failed 标记;
失败照样把缓存存下来(候选是下一步的线索),但不盖"已同步"的戳;
界面把红字和候选列出来;探查按钮的提示也分清是超时还是 schema 填错。

**给 agent 一个它做得到的下一步。** 以前那句"请在设置页里执行一次结构探查"
——agent 进不了设置页。现在换成实情 + 候选 schema + "不要猜表名,直接查
information_schema / sqlite_master"。那条自救路线 agent 最后也走通了,
只是自己摸索了三轮才找到,直接告诉它就行。

describe_table 同样:缓存空的时候说"数据源里没有 user"是在误导,真相是我们
什么都不知道。那次一口气问了 7 个表名,收到 7 条一模一样的
"没有 X。现有的对象:(还没探查过结构)",白花一整步。

顺带把逐表反射换成 SQLAlchemy 2.0 的 get_multi_*,但**别指望它治超时**:
实测 60 张表逐表 121 次往返、批量 122 次——反射缓存本来就把 get_columns 和
get_pk_constraint 合并了,而 MySQL 方言压根没实现 get_multi_*,落到基类还是
每表一次 SHOW CREATE TABLE。换它是因为这是 2.0 的正经接口、少三层
try/except,不是因为它快。shop 那次超时多半是偶发的,所以真正的对策
不在那儿,在"失败要如实记成失败"。

重新探查了一次 shop:153 张表、38 秒、成功。之前 agent 要花 6 次调用、
29 秒拼出来的东西,现在工具描述第一眼就给了。

验证:后端 269 passed(test_datasource.py 新增 4 条,守"失败不能被说成没探查过"
和"给 agent 的下一步它做得到");tsc 干净;三个 check 不回归。
2026-09-20 18:05:11 +08:00
ThearchyHelios c02ef189bb 工具调用失败能自救;agent 改成一步一个工具
起因是一条没人看得懂的报错:

    工具 db_query__shop 执行失败:
    _make_query_tool.<locals>._run() got an unexpected keyword argument 'query'

参数名写成了 query(它叫 sql),而 call_tool 对动态工具是裸 **args 展开的,
跳过了 args_schema —— pydantic 那句"缺少 sql 字段"漏成了只剩内部闭包名的
TypeError,既指不到是哪个工具,也说不出该填什么。同一个函数里内置工具那条
分支本来是校验的,两条路径不一致。

registry 里补上 prepare_args / describe_args,两条路径共用:

- 候选唯一时(多一个不认识的、少一个必填的)改名重试,并发一条 warn 说破
  —— 配置里那个错名还在,替它跑通一次却不吭声,下次照样踩
- 有歧义就不猜,报错改成"参数不对:query 不是它的参数;缺少必填参数 sql。
  它接受:sql(必填,字符串)— 要执行的 SQL…",模型照着这句就能改对。
  这才是失败之后真正能自愈的重试
- 顺带堵了一类静默失败:pydantic 默认把认不出的键悄悄丢掉,全可选 schema 上
  {"tabel": "orders"} 会"成功"执行一次什么都没查的调用

agent 循环默认改成串行:一轮只调一个工具,看到结果再想下一步。并行更快,但
一批调用中间没有任何新的思考——第二个是照着"还没看到第一个结果"时的判断发出
去的。节点上留了「并行执行工具」开关。两个必须守住的点:没执行的调用绝不发
tool.start(前端靠 call_id 配对,发了没结尾的 start 会永远转圈),但必须各有
一条配对的 tool_result(少一条下次调用直接 400)。

交错思考跟着补上。4.6 起 adaptive thinking 自带交错,不需要也不该挂 beta
header(Opus 4.6 上手动模式压根没有交错);4/4.5 那一代要手动开 thinking +
interleaved-thinking beta,光挂 header 是一条什么都不做的空设置。开了 thinking
就不能再传 temperature,这一代本来是收的,漏了就是 400。

还有两处是串行带出来的,一并治了:

- 步数用完时收尾取的是 messages[-1],而那往往是给模型看的 ToolMessage。
  真漏到用户面前当过答案(run dd9927e6):一句"本轮只执行了第一个工具"冒充
  结论。改成只取模型自己说过的话,并把"步数用完了、去调大最大步数"附在后面
- db_schema 一次能问多张表。一次调用就是一步,逐张问 6 张表就是 6 步——
  实测 8 步的运行里 7 步花在这上面,最后只剩一步真查数据
2026-09-18 17:57:20 +08:00
ThearchyHelios 39e7a5b014 数据源接入:MySQL / Oracle / PostgreSQL / SQLite
阶段①。产品原先能取数的只有 http_request 和沙箱里自己写代码——面对"分析上季度
华东区销量下滑"这类需求,用户得自己知道数据在哪张表、字段叫什么,再手写 SQL 塞进
代码节点。最难的部分留给了用户。

## 连接层(app/data/)

方言差异全收敛在 engine.py 一处:Oracle 要 service_name/sid、MySQL 要 charset、
各家默认端口不同。三个驱动都是 asyncio 原生且免装客户端——oracledb 走 thin 模式,
不需要 Instant Client。密码复用 app/core/crypto 的 Fernet,和 Provider 同一套;
build_url 默认把密码替换成星号,日志和错误信息里用这个版本。

introspect.py 产出两种粒度:summary()(表名+代表字段,几百 token,给 Copilot 的
system prompt 用)和 describe_table()(单表完整字段,按需取)。分两种是因为一个
中等业务库全量塞进 prompt 轻松几万 token,既贵又会把真正的需求淹掉。

## 守卫(app/data/guard.py)

这是唯一不能出错的地方:SQL 由模型生成,模型会写出什么事前无法穷举,防线不能
建立在"它应该不会这么干"上。三层:

- 只读判定用**白名单**而非黑名单——黑名单永远漏,MERGE/CALL/GRANT/各家私货写不完
- 多语句在到达驱动前就断掉,不赌驱动默认只跑第一条(MySQL 开了
  CLIENT_MULTI_STATEMENTS 就会全跑)
- 行数/字节/超时三重上限,一条 SELECT * 不该把内存和模型上下文一起打爆

刻意不引 sqlparse:完整解析 SQL 方言是无底洞,而这里的判断很窄——"首关键字是
什么"和"有没有第二条语句"。窄问题用窄解法。难点是不能误杀:`SELECT ';'` 只是
一条语句,简单 split(";") 会把它切两半然后拒掉,那比漏杀更让人无从下手。

## 工具(app/tools/datasource.py)

每个数据源动态生成一对 db_query__<源> / db_schema__<源>,而不是一个通用工具带
datasource 参数:工具名自带语义,模型不会把查销售库的 SQL 发给财务库;description
里直接塞表清单,省掉"先问有哪些表"那一轮。接进 registry.build_tools 作为第四来源
(前三个是内置/MCP/自定义)。

查询结果落 put_json(kind="query_snapshot"),把 SQL 原文 + 结果集 + 数据源一起存,
出具体系的数字回指能下钻到"这个数是哪条 SQL 查出来的"。

## 实测

真实 SQLite 业务库(600 行订单,埋了华东 6 月断崖的信号)+ 真实模型:
agent 8 轮自主查询——列表 → 查三张表结构 → 探数据时间范围 → 查地区维度 →
按月汇总 → 按地区+月份下钻,准确定位到华东 6 月从 1187 件跌到 354 件。
只读守卫在 agent 路径上同样生效(UPDATE/DROP/多语句全被拒且数据未变)。

测试 55 项:守卫 30 项(含"字符串里的分号不能误杀")、连接与探查 15 项、
URL 构造 6 项、Copilot 流式 4 项。
2026-09-17 10:39:23 +08:00
ThearchyHelios a16cdf3ab1 修两条 critical:文件工具可读任意路径、并发代码节点互相覆盖
## 文件工具可读任意文件(含加密主密钥)

sandbox_session 从请求体直接拼进工作区路径,没有任何净化。传 ".." 就能把
根目录挪出工作区,之后 _resolve 的越界检查是在这个被挪走的根下做的,等于失效:

  curl -X POST /api/tools/file_read/run -d '{"sandbox_session":"..","args":{"path":".secret_key"}}'
  → {"ok":true,"content":"8UygDbx9..."}    # 加密所有 provider Key 的 Fernet 主密钥

根因是同一段净化逻辑被抄了五遍:四个沙箱后端各一份,文件工具那份漏抄了。
所以提到 config.sanitize_session 统一一份,五处共用;RunToolIn.sandbox_session
再加 pattern 做纵深防御——畸形 session 直接 422,而不是悄悄净化成 default。

## 并发代码节点互相覆盖入口脚本

同一 run 的节点共用一个沙箱会话,而入口脚本写死 main.py。README 主打的
"连线即并行"会让并排的代码节点在同一 superstep 并发执行,都写 main.py 就
互相覆盖——后写的赢,每个节点却都拿到 ok=True 和"自己的"返回值。

  8 路并发同 session:7/8 执行了别人的代码,error 全为 None

改成每次执行生成唯一入口脚本(__entry_<uuid>.py),跑完即删。共享工作区语义
保留,跨节点传文件照常。四个后端同构改造,实测 8/16 路并发均 0 串味。

## 顺带修掉两个自己埋的坑

- conda 迁移把沙箱解释器隔离弄丢了:conda 环境 sys.prefix == sys.base_prefix,
  于是"干净解释器"挑出来的就是项目环境自己,沙箱代码能 import cryptography
  直接解密落库的 Key。改成实际探测候选解释器是否看得见敏感包,都不干净就
  退回 -S 关掉 site-packages。README 那句承诺现在重新成立了。
- -I 在 3.11+ 隐含 -P,脚本目录不进 sys.path,代码节点的 files 参数(多文件)
  在 seatbelt/bubblewrap 下一直是坏的,import helper 直接 ModuleNotFoundError。
  换成 -s -E,隔离意图不变,同目录 import 恢复。
- microvm 的 files_written 取 FsEntry.name(不存在),塞进去的是对象 repr。
  改取 path 的尾段。

沙箱回归 16 项通过(两档各 8 项)。
2026-09-14 15:22:58 +08:00
ThearchyHelios 964eeaa829 AgentLab:可视化 Agent 编排实验台
在画布上拖拽编排 agent 工作流,并实时看着它执行。

后端 FastAPI + LangGraph:把画布 JSON 编译成 StateGraph,15 种节点
(模型/Agent/多Agent、工具/沙箱、分支/循环/子图、记忆/检索/整形、
人工介入/结构校验)。选 LangGraph 是因为可视化编排真正难的是暂停、
恢复和回放,而 checkpointer + interrupt() 正好把这几件事做在了底层。

前端 React + React Flow:节点元数据驱动属性面板与连接桩,运行时
节点高亮、边流动画、卡片内流式 token 全部由同一份事件流驱动。

关键实现:
- Agent 内工具审批用 @task 缓存模型调用,interrupt 重放时不重复计费
- 沙箱保持只读根文件系统,代码经 exec+base64 写入(put_archive 会被
  只读容器拒绝),断网、非 root、内存/CPU/超时限额
- 检索为向量与 BM25 混合,本地哈希 embedding 零配置可用
- 分支条件走 AST 白名单求值器;HTTP 工具逐 IP 拦截内网与元数据地址
- Mock provider 让无 API Key 也能跑通完整链路

7 个内置模板全部冒烟通过,含真实模型端到端验证。
2026-09-13 17:27:00 +08:00