Commit Graph

22 Commits

Author SHA1 Message Date
ThearchyHelios b6a33b4ee2 合入期 5 前置修复 K3b:工具边界遮罩收口——分析出意外、(表达式).* 展开、把 SQL 或表名写成字符串参数的函数、记着别的会话 SQL 的系统视图与 SHOW / EXPLAIN、当一张表用的 VALUES 读到遮罩列时整份遮;按值遮只换文字格、数值证据原样并记下换过的格子;分档标签一档一行时照常显示;交给核查 agent 的证据摘录先遮,反方理由加最后一道关;长 CTE 链的分析有缓存与步数上限 2026-10-12 13:12:10 +08:00
ThearchyHelios 169c500526 K3b 复核修正(第 1 轮):数值格按位置认、(表达式).* 带外括号也整份遮、同名 CTE 按作用域认、认不全的写法名单合一、整份遮不再把每一格收成遮罩值、证据摘录分段遮、反方最后一道关只看正文
- 同名列(回退):column_types 按列名记,同名的几列只留得下后一列的类型,文字的备注被当成数值列,号码不再按值换、
  原样交给模型并落库。现在只在列名唯一时按列类型认数值列(_typed_numbers),同名的按格子本身认:整数、小数才算数,
  写成字符串的数按文字照换、记进 masked_cells;列名唯一、列类型是 number 的 NUMERIC / DECIMAL 字符串照原样
- (表达式).* 外面套了括号、起了别名(((p.card).*)、(p.card).* AS x,Postgres 实测照样展开):以前只遮第一列。选出项
  先去掉外括号、别名再认(_unwrapped),(s).*、(s.*) AS x 照旧当 s.*
- WITH 里和数据源里的表同名的一段(WITH sales AS (SELECT * FROM sales WHERE …)):不带 RECURSIVE 的一段看不见它自己和
  排在后面的段(_scoped_cte),同名的按数据源里的表认;以前认成它自己,* 展开什么都不带出,整行引用的号码原样交出
- 认不全的写法:和可验证报告合成一份名单(masking 的 OPAQUE_SQL、INDIRECT_FUNCTIONS、INDIRECT_NAMES,bundle 引用后另加
  系统 schema),补上 MySQL 会执行的注释、U&"…"、pg_stat_get_activity、逻辑解码、MySQL 的 column_statistics /
  general_log / PROCESSLIST / performance_schema、MariaDB 的 column_stats、Oracle 的直方图 / *_tab_columns / V$SQL 一族;
  去掉注释再认一遍(函数名和括号之间夹注释);去掉 DuckDB、SQL Server 的函数(query 误伤同名 CTE、字符串);没有 * 的
  一支选出项和结果列数对不上的、Postgres 解析不了的整份遮
- 整份遮只收确定取自遮罩列的那几列的值(_derived_masks 的 collect,报告快照收值同一规则):以前每一格都收,区域名、
  列名在同一次核查里处处被换掉,封存的 SQL 也被改写。快照另记 masked_whole,格子引用照实说写法认不全
- 证据摘录分段遮(inquiry.ExcerptMask,judge.prepare 的 mask 取代 kinds):SQL 照封存 SQL 的办法换,范围阈值、LIMIT 照
  原样,和遮罩列做等值、IN、LIKE 比较的字面量不论收没收到都换;格子逐格遮,数值格照原样;质询把被问的那句(连同表格)
  当作已经摆在眼前,摘录、查询工具、回答里都不换
- 反方审稿:理由文档的目录列名照快照换(同质询的 _renamed_entry),agent 自己 SQL 里照原样留着的阈值、行数在正文里
  不换(_shown),最后一道关只看正文和原文,不再把 agent 起的列名、阈值当成露了遮罩值整句改判

补测试:同名列一文一数、DECIMAL 字符串(含端到端引用取值),外括号 × 复合类型 / 记录函数 / 匿名 ROW × 最外层 /
UNION 前后支 / 和 * 写在一起,同名 CTE 四种写法和 RECURSIVE 对照,十七种认不全的写法和三种不该误伤的写法,整份遮后的
收值与封存 SQL,Postgres 解析不了的整行引用,摘录的阈值、数值格、IN / NOT IN 字面量、被问那句(反方、质询各端到端),
反方别名里的阈值(经受住、被推翻各一条)、找某个值的别名、正文里的阈值。逐项去掉实现,对应的测试都变红。
改了文件所有权以外的 bundle/build.py:_OPAQUE_SQL、_INDIRECT_READS 改为引用 masking 的同一份名单(另加系统 schema,
覆盖面不小于原来)。
2026-10-07 01:09:36 +08:00
ThearchyHelios 76cb4d2845 M4:db_schema 认整个写成一个字符串的 JSON 数组的表名
验收时盲算、反方审稿的模型把 table 写成 '["orders", "stores"]'(一个字符串),工具只按逗号拆,回「中没有 ["orders"」
「中没有 "stores"]」,两张表明明都在,模型重试几轮才改成不传 table。表名参数的解析抽成 _schema_tables:元素都是
字符串的 JSON 数组(整个作为一个字符串传进来,或者数组里的一项)按数组认,其余照旧按逗号拆;不是 JSON 的
([orders]、单引号)、元素不是字符串的照旧。只动 db_schema 这一段,查询工具不动。
2026-10-07 00:02:15 +08:00
ThearchyHelios 3ced800cde K3b:工具边界的按值遮只换文字格,换过的格子记进快照;占位的短词不当遮罩值
以前工具边界按值遮扩到没设遮罩的数值列:访问次数正好等于某一笔被遮的金额,交给模型的、落库的都成了「已遮罩」,column_types 仍是 number、快照不留记录,盲算引用到时说成「不是数值」,存下的证据被改写。现在 _masked_view 在工具边界不按值换数值格(数、列类型记成 number 的格子),只换文字格(整格就是遮罩值的、文字里夹着的那一截),换过的格子随快照存下(masked_cells:[行, 列号],不交给模型),核查 agent 的格子引用指到它们时照实说是遮罩。回答、质询工件里的按值遮(第二道防线)照旧。遮罩列里占位的写法(未知、暂无、N/A……)不收进遮罩值,没设遮罩的区域、备注里的同一个词不再被换掉。补测试,分别去掉数值格的豁免、masked_cells 的记录、引用时的判断、占位词的过滤,对应的测试都变红。
2026-10-06 22:44:48 +08:00
ThearchyHelios 6a7e51b81b K3 复核修正(第 2 轮):* 把遮罩列带进别的列名时按展开落到的位置遮;函数参数里的 JSON 箭头认作遮罩列;agent 查到的遮罩值和报告里的分开记,上限收满也不丢
- * 改名(复核点名的 blocker):WITH t(a, b, …) AS (SELECT * …) 这类列名表、UNION 后一支的 *、FROM 子查询或 LATERAL
  的列名表、标量子查询和 ARRAY 子查询里的 *,以前 * 本身不算取自哪一列,原值原样交给核查 agent、落库,第二道防线也
  认不出。现在 * 带出遮罩列的值时,它展开落到的那几列都算(masking._star_places):只展开一个来源、每一列都数得清的
  按位置只遮取自遮罩列的那几列——子查询按选出项数,数据源里的表按查询当时冻结的表结构数(_schema_columns;工具边界
  用数据源的 schema_cache,事后按快照的 schema_artifact,是同一份),几张列一样的月度表 SELECT * 连起来汇总,金额照常;
  数不清的(没有表结构、列数对不上、几个来源)整段遮。表结构只用来数位置,不用来断定表里没有遮罩列。标量子查询里的 *
  按 _value_columns 展开(展开数据源里的表一律算),EXISTS 里的 * 不算;递归的 CTE 不会绕不出来;带 EXCLUDE / REPLACE
  的 * 不按位置数
- JSON 箭头:函数参数里没写表名的 col->'path' 被 sqlglot 解析成以 col 为参数的 lambda,_value_columns 把 lambda 的
  参数名照列名收。SQLite json_each(profile->'$.tels')、Postgres jsonb_array_elements_text(profile->'tels')、MySQL
  JSON_UNQUOTE(profile->'$.tel') 这几种拆开、取出来的值在工具边界就遮
- 上限:run_verify_agent 以前先把调用方交进来的报告快照值并进 seen,收满 SECRETS_MAX 之后 agent 查到的值并不进去,后面
  的查询、回答时的第二道防线都认不出。现在两份分开记(masking.SeenValues:given / found),查询工具按值遮、交回调用方
  (VerifyOutcome.masked_values)时合起来,agent 查到的在前;反方审稿理由的按值遮也改成 agent 查到的先收
- 已知局限写进 inquiry 模块说明:表结构过期、列数没变却换了次序或改了名时可能数错;EXCEPT 后一支照 UNION 算(多遮)

测试:新增 test_masking_star_renames(真跑 SQLite 的各种 * 改名、按表结构数位置、表结构对不上、快照记着的表结构),
工具边界(交给模型的、落库的、通用工件接口)和审批质询端到端的 * 改名、JSON 箭头、上限收满;每处实现都做过去掉之后
测试变红的验证。
2026-10-06 20:29:58 +08:00
ThearchyHelios 099473cd6d K3 复核修正(第 1 轮):后面的查询按此前遮掉的值遮、质询工件照回答时记下的位置重放;表值函数拆开遮罩列也认;汇总遮罩列的指标不派盲算;反方理由按值遮
复核发现质询工件接口相对基线回退:agent 自己的查询快照在工具边界遮好之后,事后取质询工件收不到它另一次查询里的号码,
没设遮罩的备注、联系方式列原样给出。另有几处模型照样看得到遮罩值、或者遮多了的地方。

- 工具边界(tools/datasource、engine/masking.mask_query_result):此前已经认出的遮罩值(这个 agent 前面的查询遮掉的、
  调用方交进来的)也用来按值遮后面的结果,数据源没设遮罩的也一样。审批质询、反方审稿在派 agent 之前把报告快照里的
  遮罩值交进来(inquiry.snapshot_secrets,run_verify_agent 的 masked_values),模型从第一条查询起就看不到。已经摆在
  读者眼前的值(提问人写的、报告指标、被审的结论原文)跟着并进来的值一起免遮(_Secrets.protected);这份结果自己写
  的分档标签不按前面收的值换。
- 先查备注、后查出号码的:回答时只靠内存里的值才换掉的地方按快照记成位置(不含值,masking.view_edits),写进质询
  事件的 masked_cells,质询工件接口先照着重放(replay_edits)再按原来的办法遮。工具边界按值换掉的整格照样画成遮罩列。
  agent 查到的值先收、报告快照的后收:上限收满时不丢 agent 自己查到的。
- _derived_hidden 认 FROM、JOIN、LATERAL 里拆开遮罩列的表值函数(json_each、json_tree、unnest、jsonb_each_text、
  regexp_split_to_table、JSON_TABLE、LATERAL VALUES):按出列名认,不认得的函数又没写列名表的整份遮。
- 汇总里的 CASE 只在条件里用到遮罩列的不算取自遮罩列(同写在 WHERE 里)。双盲复算按同一套认法认出指标取自遮罩列
  (SUM(amount)、amount 设了遮罩)时,派盲算之前就记为无法复算并说清原因,不白花一次盲算(nodes/dual_check 的
  _query_input,改动在文件所有权之外,只加这一处判断)。
- 反方审稿:理由正文和理由文档按值换掉遮罩值(换了写法的也认),这句结论原文里写着的不换;重新组装时引用到的格子按
  遮好的快照渲染。
- 换了写法的号码另认不换行空格、窄空格、细空格、制表符、减号,最多四个分隔符;SQL 的字符串、带引号的名字、注释里
  也认(不带引号的数照旧是算式),字符串里换掉的号码同一条 SQL 里写成阈值的也不留;列名同样认。
- 查询失败时报错原话里的值换掉(mask_error_text:引号里、不带引号的长数,SQL 里写着的照留)。
- 快照的 mask_columns 只记数据源设的列名,改了名字的结果列只在 masked_at_query:证据面板并各份快照的这一项从表的
  字段清单里去列,混进别名会误藏同源别的表里同名的真实字段。
- * 展开数据源里的表时按冻结的表结构认它带不带同名列,没有的照常认分档标签(J2 收紧之后的多遮)。
- 格子引用指到按值换掉的格子时照实说;上万个值的合并、免遮放进线程。

测试:质询 agent 先查号码后查备注、先查备注后查号码、另一张表的号码列两种先后,质询工件、通用工件接口、事件都没有
原值;报告快照里的值在第一条查询就遮;表值函数(SQLite、Postgres、MySQL 的写法)、汇总里 CASE 的条件、* 连着的表按
表结构认标签、新的分隔符、SQL 字符串和注释和列名、报错原话、mask_columns 不混别名、遮罩列汇总的指标不派盲算、反方
理由按值遮且免遮结论原文、上限收满时 agent 的值先收。逐处去掉实现(24 处)对应的用例都会变红。
2026-10-06 19:11:14 +08:00
ThearchyHelios 450758185d K3:核查 agent 看到的查询结果先遮、存下的快照也遮
双盲复算、反方审稿、审批质询共用的 engine/verify_agent.py 以前把查询结果原样交给模型(db_query 自己落快照,ToolMessage
里是原样的结果),遮罩全靠审批质询事后按值认,期 4 起六轮复核每轮都找到新的漏法;运行详情「工件」页签走的通用接口
GET /api/artifacts/{id} 还把核查的查询快照原样给出。根本的修法是不让模型看到遮罩值。

- tools/datasource:查询工具加遮罩开关 mask_results(缺省关,Agent 节点、调用工具节点照旧)。打开时数据源设了遮罩的,
  先按 engine/masking.py 的 mask_query_result 遮好再交给模型、再落快照:列名直接对上的、按 SQL 认出取自遮罩列的
  (别名、函数、子查询、WITH、SELECT *,复用 _derived_hidden;SQL 解析不了又写着遮罩列的,整份结果的列都遮)整列
  换成「已遮罩」、类型记成文本;同一份结果里别的格子整格是遮罩值的换掉,文字格里夹着的那一截换掉(换了写法的号码
  也认);列名不换。结果里一列都没取自遮罩列的(数值的汇总、只数个数、只在条件里用到)原样。快照里 mask_columns
  另记下查询时遮了的、改了名字的结果列(证据面板、质询工件据此画成遮罩列),masked_at_query 记下查询时就整列遮掉的列。
  这一次遮掉的值并进 ToolContext.extra[MASKED_VALUES_KEY],只在内存里。
- verify_agent:建查询工具时和 force_readonly 一样打开遮罩开关;格子引用指到查询时就遮了的列,照实说是遮罩列、不采信;
  遮掉的值经 VerifyOutcome.masked_values 交回调用方(不进 as_dict、不往外交);系统提示补一句「已遮罩」是什么。
- 审批质询:按值遮降为第二道防线。agent 自己的快照查询时就遮好了,收不到遮罩值,改由 masked_values 并进来(_plan 的
  seen),回答、查询清单照旧按值遮;「已遮罩」本身不收成遮罩值。事后取质询工件时内存里的值已经不在,SQL 检查结果的
  片段、说明照事件里记下的那条 SQL 换(_recorded_swaps:逐词对出回答时换掉的字面量和 LIKE 里换掉的那一截)。
- 双盲复算取盲算值、反方审稿的引用、证据面板、可验证报告(带遮罩的快照只给哈希,照旧)、推演、运行对比(不收核查的
  查询)都按快照里已经是「已遮罩」照常工作,文件不用改。

测试:核查 agent 选出遮罩列(直接、别名、函数、子查询、WITH、SELECT *)时交给假模型的 ToolMessage、落库的快照、通用
工件接口、tool.end 预览、收尾交给模型的查询结果里都是「已遮罩」;没碰遮罩列的聚合查询原样、照常取值;文字格夹着的
号码换掉那一截;引用遮罩格子不采信;SQL 认不出时整份遮;开关缺省关、没设遮罩的源快照一字不差;列名不改;审批质询、
反方审稿、双盲复算各补一条端到端;质询工件的检查片段照记下的 SQL 换。逐处去掉实现(开关、并进内存的值、照记下的
SQL 换、跳过「已遮罩」、遮罩格子的说法、记下改了名字的列、文字格按子串换、不改列名)对应的用例都会变红。
2026-10-06 17:36:55 +08:00
ThearchyHelios e46ff55fd4 数据源查询工具加 force_readonly:核查的 agent 另起一路、没有审批关卡兜底,要查的源却可能可写。开了就不管源可不可写,一律按只读守卫检查:先过一遍和 run_query 相同的检查(多条语句、结构变更照原来的说法拒),再按只读判,写语句交回「这次查询只允许读取数据」、不执行,数据原样;查询照常落查询快照。工具描述跟着写「只读」,审批判定也不再把写语句当危险调用。只靠守卫这一道:连接池按源缓存、不分只读与否,不能为此另起一个只读池(缓存键相同,会把这个源平时用的可写连接换成只读)。build_datasource_tools 透传这个参数,不传时一切照旧 2026-10-05 00:26:04 +08:00
ThearchyHelios 7f07fc370a 数据目录阶段 2B:运行时接上 SQL 检查——查询结果和快照带 checks、证据接口的查询步骤带上,契约指标的来源查询有 error 时出具降档 2026-10-03 10:32:54 +08:00
ThearchyHelios f4ee261aec 数据目录阶段 1A:建工具时用单独的会话读目录,查询快照的 meta 解析失败不连累快照落盘 2026-10-03 09:44:45 +08:00
ThearchyHelios 785660e60d 数据目录阶段 1A:db_schema 输出附上数据目录,查询时目录随表结构快照冻结进证据,查询快照记下用到的表供使用次数统计 2026-10-03 09:40:16 +08:00
ThearchyHelios 2cdbf9a733 Excel 导入改造期 3:网格交互、自助修复、按期累积——每月的报表可以累积成一份跨月的表,漂移了点修复按钮就能接着导入,网格上框一块就能写进配方,版本页可以回滚、移除一期、清除原件
按期累积(新 recipe_accumulate;table_versions 扩展)
- 按统计期出的交叉表报表首次导入时默认「按期累积」,由人确认;一期一个构建,两期及以上物化成一份新的 SQLite 快照,不用视图
- 统计期完全相同要勾「替换该期」,部分重叠拒收,中间有空缺要确认;物化前逐期核对库哈希和行号,并集上另加行数守恒、主键唯一、日期落在所属统计期三条核对
- 配方演进分三档:新增列(早期各期为空值)、退役的列和表保留旧值(只在这次才退役时要勾)、不兼容变化只能显式「重新开始累积」
- 快照 id 在累积时含目标配方和模式;同样的输入物化两次库哈希相同,同一文件换名再传直接复用
- 说明按期合并,任何一期没核对过就不写「已核对」;各期维度取值不同、退役列、合计表都有提示,经过数字检查

修复按钮(新 recipe_fixes;执行器给问题带结构化参数)
- 加入标签、移除标签、声明为合计行、分段标题改名、编辑恒等式成员、按锚点忽略行/列/区域外文字、声明占位符、声明隐藏行处理、工作表改名
- 服务端算补丁,先预览新旧配方对照、再应用,可以撤销;补丁封闭,不引入正则、自由文本常量和写死坐标;修复进确认清单
- 只改分段标题、新标题仍含原来选的词时沿用已确认的常量,不算破坏性变更;修复后只查改动过的段的认领,没改的关系交给试运行核对

框选转锚点(新 recipe_select)
- 网格上框选(鼠标、键盘、区域输入),按八种用途换算成按文字认的锚点,立刻干跑比对;配方里不存坐标,泛化不了时列出原因
- 整张表挪位后同一配方照样认得;框进合计行、夹着小计、表下说明都有明确的处理

确认与对照(recipe_confirm、recipe_diff、新 recipe_compare)
- 改配方后确认清单只列有变化的项(同时按本期数据比较),不用每修一次重勾十来项;新旧配方对照界面;按规则重新起草并把名字对齐到现行配方
- 改配方后已体现在配方里的回答保留,矛盾的列出来请人重答
- 「排除的行」写进回执和导入清单:隐藏行、跳过的空行、按配方忽略的行、停止后的文字行、只核对不另存的合计行

版本页(新 source_versions、api/source_versions;前端 VersionsDialog)
- 快照与导入记录列表、启用旧版本(可写理由)、查看清单、移除一期、作废接受、清除原件;写操作带界面看到的当前版本,并发改动回 409,不静默丢期
- 除当前版本和被运行引用的版本外,再保留最近 3 个版本供回滚;遮罩列丢失要明确确认

期 1、期 2 遗留
- 重传撞上被改过的构建文件:能用本次上传恢复就隔离旧文件再恢复,否则回 409,不再是 500
- 跳过空行的列表不再把表下隔一行的说明读成数据;不像说明的单格行要人确认;受影响的配方构建换 id
- 版本存储加进程级文件锁:第二个进程拿不到锁时不清理、不回收,写操作回 503(部署本来就是单进程)
2026-10-02 07:38:44 +08:00
ThearchyHelios 2d5e50fa88 Excel 导入改造期 2:配方引擎与确认向导——表从哪一行哪一列开始、哪些是合计、日期怎么补年份,都写进一份可校验的配方;首次导入经人确认,之后每期按配方重放并核对
配方语言(agentlab-recipe/2,新 recipe_types、recipe)
- 封闭、不含正则、保存时静态校验:常量只能从表里的候选文字里选,占位符只能映射成空值,单位从固定词表里取
- 交叉表和列表两种版式;分段各自定位、不依赖先后顺序;合计按标签语义区间核对,关系恒等式、口径不同的声明都写在配方里
- 固定的解析器集合(parsers/2):全角、各类横线、千分位、月日写法宽容归一,规范写法存库、原文另记

执行器(新 xlsx_cells、recipe_engine)
- 两遍读取逐行对账,每个非空格都要有去向(格子账);交叉表逆透视成长表,按统计期补年份并断言覆盖整期
- 合计行改作核对并另存一张表,不和明细混在一列;区域外文字、隐藏行列、合并单元格、空行之后的文字都单独报出
- STRICT 加主键写临时库;按锚点段记溯源,供后续证据下钻用;大表流式读取,扫描在 sheetData 处停止

校验与说明(新 recipe_checks、recipe_notes)
- SQL 核对分通过、不一致、无法核对三种结果,失败分结构、数据质量、需确认三类;结构类直接拒收
- 数据质量类可以写理由接受,接受不改库、只改状态和说明
- 表的说明按核对结果生成、不含数字,经过数字检查;没核对过的不写「已核对」

起草(新 recipe_suggest、recipe_ai)
- 先用规则起草:建议卡片加待确认问题(关系要不要登记、占位符表示什么)
- 规则起草不完整时才提示 AI 兜底:界面先展示要发出的全部文字,用户同意才发;只发结构,数字、隐藏行列、遮罩列不发,公式先脱敏;同意绑定模型和服务方;AI 只写配方,有限次修订,记 token 和费用;截断单独识别
- 无论规则还是 AI 起草,都必须人工确认才能启用

导入流程(新 recipe_imports、recipe_confirm、recipe_diff、api/table_imports)
- 暂存 → 起草 → 回答问题或改配方 → 试运行 → 勾确认项、写接受理由 → 确认并启用;接入期 1 的构建、导入记录、快照和运行固定版本
- 上传新一期按已确认的配方重放,不调用模型,出回执和差异卡,手动启用
- 改配方报破坏性变更(breaking:*)、单位变化、工作表改名的兜底,都要逐项勾选确认

前端导入向导(新 pages/import)
- 原始网格(行列号、去向着色)、建议卡片、配方面板(表单,不露正则)、试运行回执、确认清单、差异卡、AI 起草的同意对话框
2026-10-01 22:00:07 +08:00
ThearchyHelios 8059cd5147 Excel 导入改造期 1:止血与版本底座——写错的列名不再静默得 0,导入不再悄悄读错,每次上传生成不可变的版本,运行固定发起时的数据版本
连接层
- 所有 SQLite 数据源关闭双引号字符串兼容(DQS),写错的双引号列名直接报错;设置失败就拒绝连接,启动时自检
- 连接缓存按数据源加版本快照分键、按源整体失效、最多 32 个;版本快照以 immutable 只读打开,建连接前核对文件哈希,并在借出连接时核对文件指纹,被改过就拒绝查询
- 结构探查遇到取不了列的视图时逐个对象重取,只跳过坏的那个并注明原因

解析(新 xlsx_scan + tabular)
- 先流式扫描真实边界和版式(不信 dimension),再按边界读值,不整表进内存;按 Excel 自身上限设硬上限,并发解析最多两个
- 隐藏和深度隐藏的工作表不导入;稀疏区域、无缓存值的公式、同名工作表、DTD 拒收
- 去掉空行和两侧空列;列名表名统一规范化(新 names 模块)并保留原表头,撞名不再互相覆盖
- 前导零、超长数字按文本;千分位按数字;日期不带 T;带单位的数字文本写进回执
- 数字列混入非数字、交叉表和多块结构必须由用户决定(422),可选存空值或按原样导入(未规整);STRICT 建表,整份在一个事务里写完

版本底座(新 table_versions、raw_store)
- 每次上传写新库文件、落盘、0444,事务里写构建、导入记录、快照后才切指针;失败时旧版完整可查
- 原件按哈希存档,不提供下载,可以清除;上传源不许改连接字段、不再重新探查;手工 SQLite 源不许指向上传目录和应用自己的数据文件(按文件身份判断)
- 启动时清理半成品与孤儿文件、给老上传补初始版本;被运行引用的版本不回收,发起运行与回收互斥
- 上传接口的解析移到线程池

运行固定数据版本
- 发起时把图里(含子工作流)用到的上传源的当前快照记进运行记录和 run.started,续跑、审批恢复沿用;数据源工具按固定的快照生成描述、表结构并查询
- 固定的版本没了、源被删除后同名重建,都明确报错,不改用当前版本或同名的新源;查询快照记 data_version

证据层
- 新报告记 entity_syntax: 2,[[t:明细]]、[[c:明细.金额]] 和 SQL 里不加引号的中文表名都认得,名字按 SQLite 的口径查找;老报告按原规则复核,结果不变
- 写作规则改为中文表名、字段名一律用 [[t:]] / [[c:]];未规整的表在工具描述、写作目录和裁判摘录里标出

前端
- 上传弹窗接上导入前的决定(混合列、交叉表)和导入回执(跳过的隐藏表、类型转换、警告、原表头对照、未规整标记),上传前告知原件会保存
- 数据源卡片按 origin 认上传的表格,显示当前版本;上传的表格不再给「探查结构」
2026-10-01 13:42:04 +08:00
ThearchyHelios d071bf119f 可点击证据第三期:表名字段名和引文也点得开、可疑实体与结论句、受管门禁 G1–G5、证据审计导出
一、表结构、实体与引文

- 数据源工具查库时顺带存一份表结构快照(schema_snapshot),和查询快照一起进台账、进封存;
  台账里的查询记下 SQL 用到的表
- 报告里的表名、字段名可以点:反引号里的名字一律核对,正文里的裸名字只在明显是标识符时才自动链接,
  常见英文单词不链接;点开看到它出现在哪次查询、类型、所属的表,快照被截断时写明
- 找不到的名字标成可疑实体:在快照、SQL、结果列里都没有的记「可能是编造的名字」并给出最接近的 3 个;
  快照被截断、核对不了的记「核对不了」,不冤枉成编造
- 检索引文 [[q:K1|原文]]:原文在检索快照的命中片段里逐字出现(空白归一化后)才算有出处,点开高亮原文位置
- 自动链接的名字不算句子的引用,只有写作者写的引用才算

二、结论句与出具

- 报告节点新增 claims:off(默认,保持原样)或 require_citation(没有引用的结论句计入缺口)
- 受管级别的正式运行里,可疑实体和没有引用的结论句按「有缺口」降档,不拦截;探索运行和已发布级别只标注
- 契约的 claims 和报告节点的设置取更严的那个;受管正式运行里写 ignore 也按降档处理,并给出警告
- 出具横幅除了数字,也写出可疑实体和没有引用的结论句

三、受管门禁 G1–G5(只看图和配置)

- G1 带契约的出口,成果字段的文字只能来自报告撰写节点
- G2 模型写的文字不绕过报告撰写节点直接流进出口
- G3 报告撰写节点写明 numbers: strict、on_violation: fail、claims: require_citation
- G4 口径卡的输入不来自计算角色的沙箱代码
- G5 给口径卡供数的 agent 配 output_schema 并开 cite_fields
- 受管级别是错误、挡住发布;已发布级别只给警告;已经发布的版本照常运行,下次发布时按新规则检查
- 每条规则都接进发布前自动修复(一键修复 / 给选项 / 交给 Copilot);禁止的改动补上关 cite_fields、
  删 output_schema、放宽契约 claims;Copilot 兜底不许自己把代码节点标成 source、不许放宽报告节点
- 发布弹窗里选「交给 Copilot」时明确写「正在交给 Copilot」

四、旧运行与审计

- 没有契约的旧答案:数字和已封存的查询单元格、口径卡值按数值比对,每个数最多 3 个候选,
  默认折叠,写明「猜测的来源,不能当证据」
- 有契约的旧运行:候选只从已封存的口径卡里找,人为插进工件表的行不当证据
- 证据审计:GET /api/runs/{id}/evidence/audit 按「无证据 / 可疑实体 / 有出处 / 旧运行猜测」分组,
  可导出 JSON 和 CSV(带封存状态);记录页和画布上的报告节点显示「有出处 N · 无证据 M」

五、SQL 守卫修正(安全)

- 去注释改成单遍扫描,认得字符串和各方言的引号(单引号转义、MySQL 反斜杠与 #、PostgreSQL 的 $$ 与嵌套注释、
  Oracle 的 q'[…]'、SQLite 的方括号);以前字符串里的 -- 会被当成注释,SELECT '--'; DROP … 这类写法能骗过多语句拦截
- 看不到服务器设置时每种读法都扫一遍,读法之间切分不一致就拒绝,任何一种读法是写操作就按写操作处理
- 执行查询和工具危险判定都按数据源的方言检查

六、其他

- 修掉检查脚本里画布检查的一处竞态,并行估时按实测更新
2026-09-29 05:26:34 +08:00
ThearchyHelios fcdcc30d4e 可点击证据第二期:取数链路打通、问数据的答案逐段可点;发布前自动修复
一、可点击证据第二期

取数链路
- 智能体、工具节点、代码节点、知识检索取数时都记进证据台账;报告目录按台账编出 Q1… / K1…
- 报告可以引用快照里的一格([[v:Q1.r0.gmv]]),也可以由系统从快照生成整张表([[table:Q1 cols=… rows=0-4]]),
  表里每一格都是可点的片段;取值一律以快照为准,取回快照时复验哈希
- 口径卡表达式新增 cell(x, row, column),lineage 精确到单元格
- 智能体开 cite_fields(配合 output_schema)后,循环结束做一次结构化抽取,每个字段标出取自哪次查询的哪一格,
  再到快照里核对:一致记 verified;不一致以快照为准并告警;查不到的记为空值,永远不兜底成 0
- 代码节点新增 evidence_role(source / compute)和 code_sha;沙箱算出来的数不能被报告直接引用,要进口径卡
- 查询快照记下列类型,文本列按文本渲染(年份 "2026" 不再显示成「2,026」)
- 升级前发起的运行(停在审批上的)一律走旧逻辑:新的 task 只追加在循环之后,恢复时不会错号

证据接口与面板
- 片段链接上查询步骤:SQL、被引用的行加前后各 2 行、高亮到格;数据源可设 mask_columns 遮罩列,
  遮罩在查询时记进快照,数据源事后改名或删除也照样遮罩(在有身份体系之前,遮罩只减少暴露,不是安全边界)
- 输入来源写明核对结果(与快照一致 / 模型报的值和快照不一致 / 没查到记为空);口径卡缺输入时也给出完整的链
- 受管出具要在契约里声明 cells 才能引用单元格;受管与否记在所钉版本上,改画布不再让受管版本的正式运行降级
- 口径卡可以用 caliber_from 钉住另一个已发布工作流里的口径卡,上游有新版本时按 upgrade_policy 处置,
  和子工作流同一套规则;子工作流节点也补上了升版处置的配置项

Copilot、校验与报错
- 问数据的答案里有数字时,搭 input → 智能体 → 报告 → 出口,报告直接引用查过的单元格
- 写 SQL 前核对数据源的表结构,SQL 里出现表结构里没有的列会被打回
- 不再搭「智能体文字 → 转换节点解析 JSON」这种图;转换节点的 JSON 报错分清是上游模型写坏了、
  模板里裸插了模型文字(给出 | json 的改法),还是模板本身写错了
- 工具节点遇到「查询失败」「SQL 被拒绝」判为失败;发起运行前检查绑定的工具是否存在,不存在时直接拒绝并给出入口

二、发布前自动修复

- 问题带上机器编号(code)和修复 id;新增只读接口 POST /api/workflows/{id}/publish-check 和 /autofix
- 修复分三类:答案唯一、不降低要求的一键修复;要人拿主意的给选项(不替人选);结构性问题交给 Copilot
- 一律先预览,不自动保存、不自动发布;删节点、删契约、放宽审批、关掉 strict、改级别等降低要求的改动一律拒绝,
  Copilot 的方案也要过这一关;应用后重跑检查,错误必须变少且不能冒出新的
- 发布弹窗打开就先检查;问题面板新增「发布前检查」,平时就能看到、就地修掉
- 门禁修正:契约有 report_from 时不再要求 narrative(模板⑨可以发成受管);全图默认审批为「全部放行」时同样拦下
- 修掉画布保存会抹掉全图默认(defaults)的老问题:以前任何一次保存都会把它清空,审批 always 因此被悄悄放宽

验证(沙箱):tsc 干净,生产构建通过,pytest 2268 passed(两遍),check-all 17 项检查、3915 项全过
(新增 check-publish);关键行为做了变异验证、全部抓到。
2026-09-28 21:20:04 +08:00
ThearchyHelios 587bf7e27c 第三波前两批:六个「看着成功其实没干活」的平台漏洞堵上,记录页能回放,管理页与画布收尾
起因是一次探索运行:Copilot 改节点时把工具绑定整份抹掉,agent 没有工具只能写「假设调用」,
校验节点的自动修复编出 total_count=0 让 schema 通过,协作成员把工具调用标记当文字输出了四轮,
轮数用完后节点把原话当成功结果交出去——运行标成已完成,数据库一次都没查。

平台可信度(先写失败测试)
- Copilot 修改节点改为合并:没写的字段保留,null 才删除;协作成员按名字合并。前后端同一套规则,
  工具集合变小时自查报警、改图回执列出变化
- 新校验规则:提示词点名了某个工具而节点没有绑定它,判错误,Copilot 自查会交回去修
- 校验修复只许改格式:冒出原文没有的值就判修复无效;修复这次模型调用计入用量
- 识别模型把工具调用写成文字的情况(DSML、<tool_call> 等标记):纠正一次,仍然如此就判失败并说明原因
- 协作团队用完轮数不再按成功交付:默认失败,可选降档,点出一次都没派到的成员
- 审批恢复后,同一节点里已经执行过的工具调用复用结果,不再重复执行

后端
- 查询时限交给数据库执行(Oracle call_timeout、PG statement_timeout、MySQL max_execution_time、
  SQLite progress handler),可按数据源配置;超时报错与实际耗时一致
- 等审批的运行可以真正取消;会话列表带最后一轮状态;测连接结果持久化;探查结构可只看不存;
  自定义工具可草稿试跑、写坏的参数定义保存时拒绝;MCP 服务改动后作废旧工具缓存;
  检索命中带贡献分解;重建索引在后台跑并报进度;报错统一收进 core/errors

前端
- 公共层:管理页共用件、失败说明(lib/explain)、健康状态进公共库;目录按表各自加载并有超时,
  一张表卡住不再拖垮整站;上传显示真实字节进度;离线横幅只播报不变的句子
- 画布:卡片只说游标所在的那一次执行;等审批时可放弃运行;团队收尾判定与未完成说清楚;
  回放用量取那一刻的值
- 编排页:Copilot 合并语义与工具变化回执;「用完轮数时」选项;正式运行期间锁住所有改图入口
- 回答流 / 问数据:新失败类型说人话并给下一步;画布与右栏互相定位;数据源范围、回收站、重新核对
- 记录页:「航迹」页签复用画布时间轴,可拖动回放、列出关键时刻;列表行带执行 / 等人 / 失败缩略条
- 外壳:统一的离开前确认(data router),导航、⌘K、快捷键、后退都会先问

验证(沙箱):tsc 干净,pytest 838 passed(两遍),14 个检查脚本中 13 个全绿;
check-ui 余 1 项旧断言(按旧日期格式找运行行),3c 更新。
2026-09-27 10:16:32 +08:00
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