cardbuddy 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/preview-editor/assets/index-BkbBxDTT.css +1 -0
- package/dist/preview-editor/assets/{index-CIIfMlwI.js → index-Bro07DTa.js} +91 -88
- package/dist/preview-editor/index.html +2 -2
- package/dist/vendored/card-editor/runtime/adapter.js +2 -2
- package/dist/vendored/mcp/client.mjs +1 -1
- package/dist/vendored/server/core/ai-chat.js +251 -80
- package/dist/vendored/server/core/cardValidate.js +16 -5
- package/dist/vendored/skills/card-skill-trainer/SKILL.md +61 -6
- package/dist/vendored/skills/card-skill-trainer/scripts/diagnose_case.mjs +14 -82
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/diagnosis.mjs +207 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/layout-correction-sim.mjs +62 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/paradigm-router.mjs +76 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker-runner.mjs +31 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/pixel-worker.mjs +138 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/scoring.mjs +24 -20
- package/dist/vendored/skills/card-skill-trainer/scripts/lib/telemetry.mjs +46 -0
- package/dist/vendored/skills/card-skill-trainer/scripts/plan_next_round.mjs +4 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/run_round.mjs +279 -90
- package/dist/vendored/skills/card-skill-trainer/scripts/score_round.mjs +74 -2
- package/dist/vendored/skills/card-skill-trainer/scripts/sink_rules.mjs +127 -0
- package/dist/vendored/skills/dsl-card-generator/SKILL.md +93 -27
- package/dist/vendored/skills/dsl-card-generator/references/dsl_structure.md +1 -15
- package/dist/vendored/skills/dsl-card-generator/references/expression_engine.md +11 -20
- package/dist/vendored/skills/dsl-card-generator/references/generation_pitfalls.md +169 -44
- package/dist/vendored/skills/dsl-card-generator/references/ir-format.md +87 -3
- package/dist/vendored/skills/dsl-card-generator/references/iteration_protocol.md +35 -6
- package/dist/vendored/skills/dsl-card-generator/references/quickref.md +28 -27
- package/dist/vendored/skills/dsl-card-generator/references/recipes.md +30 -8
- package/dist/vendored/skills/dsl-card-generator/references/rules.md +3 -2
- package/dist/vendored/skills/dsl-card-generator/references/scripts_reference.md +5 -2
- package/dist/vendored/skills/dsl-card-generator/references/slices_convention.md +2 -3
- package/dist/vendored/skills/dsl-card-generator/references/spec_to_schema_patterns.md +1 -4
- package/dist/vendored/skills/dsl-card-generator/scripts/add_component.mjs +5 -14
- package/dist/vendored/skills/dsl-card-generator/scripts/autofix_schema.py +88 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/check_fidelity.mjs +143 -42
- package/dist/vendored/skills/dsl-card-generator/scripts/check_topology.py +0 -35
- package/dist/vendored/skills/dsl-card-generator/scripts/expand_ir.mjs +99 -53
- package/dist/vendored/skills/dsl-card-generator/scripts/gate.mjs +5 -2
- package/dist/vendored/skills/dsl-card-generator/scripts/iterate.mjs +388 -29
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/component_contract.mjs +315 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/layout-tokens.mjs +1 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.mjs +23 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/paths.py +0 -37
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/python.mjs +99 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/adaptive-width.mjs +105 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/bindings.mjs +260 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/expr.mjs +52 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/image.mjs +96 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/registry.mjs +166 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/structure.mjs +127 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/rules/walk.mjs +61 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/scope_alias.mjs +125 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_assemble.mjs +325 -41
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_extract.mjs +112 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/lib/v3_shared.mjs +0 -6
- package/dist/vendored/skills/dsl-card-generator/scripts/lint_card.mjs +119 -78
- package/dist/vendored/skills/dsl-card-generator/scripts/preflight.mjs +48 -50
- package/dist/vendored/skills/dsl-card-generator/scripts/readiness.mjs +634 -0
- package/dist/vendored/skills/dsl-card-generator/scripts/register_card_template.mjs +5 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/route_input.mjs +5 -10
- package/dist/vendored/skills/dsl-card-generator/scripts/spec_audit.mjs +122 -8
- package/dist/vendored/skills/dsl-card-generator/scripts/v3_regression.mjs +3 -1
- package/dist/vendored/skills/dsl-card-generator/scripts/validate_schema.py +23 -15
- package/dist/vendored/skills/shared/exit-codes.md +79 -0
- package/dist/vendored/skills/shared/spec-contract.md +118 -0
- package/dist/vendored/skills/sketch-spec-to-card/SKILL.md +26 -9
- package/dist/vendored/skills/sketch-spec-to-card/references/spec-schema.md +3 -3
- package/dist/vendored/skills/sketch-spec-to-card/scripts/build_prompt.py +0 -8
- package/dist/vendored/skills/sketch-spec-to-card/scripts/cluster_states.py +14 -25
- package/dist/vendored/skills/sketch-spec-to-card/scripts/parse_meaxure.py +91 -47
- package/dist/vendored/skills/sketch-spec-to-card/scripts/png_utils.py +0 -5
- package/dist/vendored/skills/sketch-spec-to-card/scripts/test_text_only_artboard.py +75 -15
- package/dist/vendored/skills/sketch-spec-to-card/scripts/vision.py +40 -34
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-DQn0zQBP.css +0 -1
- package/dist/vendored/skills/card-skill-trainer/scripts/test_missing_slice.py +0 -122
- package/dist/vendored/skills/dsl-card-generator/scripts/analyze_component_usage.mjs +0 -153
|
@@ -260,7 +260,8 @@ function styleKeySection(root) {
|
|
|
260
260
|
`\n## 合法样式键(唯一来源 style-meta.json;写表外的名字 expand_ir 会 exit=3 中止写盘,schema 不会被改坏)\n` +
|
|
261
261
|
`${rows}\n` +
|
|
262
262
|
(traps.length ? `**两个最高频的坑**:${traps.join(';')}。\n` : '') +
|
|
263
|
-
`IR 里样式写在前述分组下(\`s:{flex:{gap:"8"}, rect:{width:"100%"}}\`);组名与键名一律照抄本表,不要用缩写(缩写别名只覆盖少量键,猜错即中止)。\n`
|
|
263
|
+
`IR 里样式写在前述分组下(\`s:{flex:{gap:"8"}, rect:{width:"100%"}}\`);组名与键名一律照抄本表,不要用缩写(缩写别名只覆盖少量键,猜错即中止)。\n` +
|
|
264
|
+
`列表插槽的作用域别名($item.<字段> / $item / $item._index)**不需要你规避**:expand_ir 在扩展开头会自动归一化成规范形($<字段名> / $$ / $_index),schema 里只存规范形。样式键写错才会中止——两者别混。\n`
|
|
264
265
|
_styleKeyCache = { key: `${file}:${mtime}`, text, mtime }
|
|
265
266
|
return _styleKeyCache
|
|
266
267
|
}
|
|
@@ -349,12 +350,14 @@ S1 要素包:kind=meaxure-export(Sketch MeaXure 标注导出)必须先有
|
|
|
349
350
|
S1.4 批量建卡(用户一次点了多张卡 / 回了「全部」):**一次** parse_meaxure --pick 出全部要素包(别每卡重跑一遍解析),然后**同一个回合内**按「S2→S5 走完一张,再走下一张」连续推进——每张卡各写各的 IR、各跑各的 expand + iterate。禁止每张卡各起一个回合:新回合的首步要把整段对话重发一遍(实测 1.3-3 万 token 一步),还要把同一个要素包重读一遍,纯浪费。
|
|
350
351
|
S1.5 选卡门禁(多画板 / 多卡画板必需;SKILL.md §13 + rules.md §13.4):目标目录定下后先跑 skill_exec script=skills/dsl-card-generator/scripts/spec_tree.mjs,args=["--list-cards","<目标目录>"] 拿「画板序号.卡片序号」全量清单(一个画板含 N 张卡展开为 K.1~K.N,行下自带 "图: <card-N.png 绝对路径>");S0 挂载段已带同一份命令就直接复用,不要重复跑。
|
|
351
352
|
- 只有 1 张卡、或用户已明说目标(序号如 2.3 / 画板名 / 卡片名)⇒ 直接进 S2,不要为走流程停下来问。
|
|
352
|
-
- 多张卡且用户未指定 ⇒ **本轮到此为止**:把清单写进正文 +
|
|
353
|
-
|
|
353
|
+
- 多张卡且用户未指定 ⇒ **本轮到此为止**:把清单写进正文 + 明确提问,正文最后**单独一行输出 [需用户确认]**,然后停止输出、等用户回序号再继续。没有这行标记会被判定「任务未完成就停手」而自动续跑(实测多花一整轮重发清单)。聊天面板**没有**弹选项工具(不存在 request_user_input),不要去调用或寻找弹窗工具,也不要替用户挑。
|
|
354
|
+
清单**逐行配图**:每个卡片行下紧跟一行 Markdown 图片,写成 !\[画板名\](该卡 card-N.png 的完整绝对路径),与序号一一对应——**必须是 !\[\](...) 包起来的图片语法**;parse_meaxure 输出里的 "图: <路径>" 行是给你看的提示,照抄成裸路径渲染端不显示(实测照抄后用户 0 张图可看);缺图显式标缺失,禁止把图堆到清单末尾。末尾追加一项「全部」,用户回「全部」才算显式批量授权(产了几张卡要在正文报)。
|
|
355
|
+
**清单必须以普通 Markdown 列表逐行输出,禁止用代码块围栏(\`\`\` 或 ~~~)或 4 空格缩进包裹清单的任何部分**——渲染端把代码块当纯文本展示,里面的图片语法一行都不会转成预览图,选卡协议整段失效(实测 8 张配图全灭)。parse_meaxure --list 的输出**照抄行内容即可,围栏与缩进不要照抄**。
|
|
354
356
|
**路径必须写完整绝对路径**(以 /Users/… 或 C:\… 打头,一字不改照抄),**禁止用省略号(… 或 ...)代替前缀**(实测「…/积分助手-4-xx.png」这种写法渲染端取不到图,用户那几行就只剩一串路径=配图全丢),**禁止把路径包进反引号或写进代码块**(反引号 / 代码块里的是给人抄的文本,不会渲染成图片)。清单长不是省前缀的理由:图显示不出来才是真的白写。
|
|
355
357
|
只回画板序号默认取该画板第 1 张卡。
|
|
356
358
|
- **禁止**自行挑画板 / 挑卡、**禁止**「先建一张看看」、**禁止**未经同意批量产卡。
|
|
357
359
|
S2 审计(一步代替反复探索):skill_exec script=skills/dsl-card-generator/scripts/spec_audit.mjs,args=["--spec","<bundle>/spec.json","--card","N","--slices-dir","<bundle>/slices","--project-slices","src/slices","--design","<bundle>/card-N.png","--save",".cache/ai-scratch/<id>.audit.json"]。
|
|
360
|
+
切片引用名先查 <bundle>/slices/index.json(--build 已生成:每张切图的真实相对路径与大小),照它写引用、按 R31 命名规则消毒(禁用字符 # ? % & + 不进引用名),expand_ir 会递归命中嵌套子目录里的真身;**禁止 fs_list 逐层摸索**(实测 6 次才找到嵌套真名,纯浪费)。
|
|
358
361
|
它交叉审计 spec + 聚焦截图 + 切图目录,给出:bands(水平行与行内元素序)、gaps(padLeft / padRight / padTop / padBottom / rowGaps)、texts(文案 / 字号 / 字重 / 颜色 / 行高 / 对齐)、shapes(装饰条)、slices(存在性 / 复用 / 是否烘焙背景与标签文字)、colors(背景采样点)、missing(缺哪张切图 + 建议裁剪矩形)。
|
|
359
362
|
它交叉审计 spec + 聚焦截图 + 切图目录,另给 bgGradientHint(顶部渐变带的起止色/跨度 + IR 正确写法)与 slices[].crops(reused-differs 时「矩形 → 该用哪张 _alt 图」的映射,exists=true 直接引用、false 才需裁)。
|
|
360
363
|
IR 的几何与样式一律照抄审计值;**禁止 fs_read spec.json**(几百 KB 只会拿到截断片段,重复读纯浪费步数),禁止反复 fs_list 摸索结构。要看层级树才跑 spec_tree.mjs。
|
|
@@ -369,15 +372,23 @@ S4.5 变量抽取(真实卡片不是截图:这步漏了,产物就是 dicti
|
|
|
369
372
|
下限 = S2 审计的 bindHints[](amount / percent / date / masked / count / level),再按语义补正则认不出的动态值(用户名 / 客户层级 / 卡片名称 / 状态词);静态标签(区块标题 / 按钮 / 说明文字)才写字面量。
|
|
370
373
|
- 动态值:Text 主值用 v:"$key"(条件文案写表达式如 "$showAll ? '收起' : '查看更多'",金额展示照 recipe_snippets/money-format.json,别把「¥7,000.00」拆成字面量)。
|
|
371
374
|
- IR 顶层**显式写 dictionary.inputParams**:{key,name,type,defaultValue,desc,rule:{type:"",options:{}},children:[]},defaultValue 一律取设计稿字面量——expand_ir 用它生成 data.json,渲染即设计稿文案,S5 像素闭环才不受绑定影响。
|
|
372
|
-
- 重复行 / tile 组(审计 listCandidates[]、或用户明说要列表):数组变量 + listScope
|
|
375
|
+
- 重复行 / tile 组(审计 listCandidates[]、或用户明说要列表):数组变量 + listScope 插槽。item 的字段**已逐个展开到作用域** ⇒ 规范形三种:$<字段名>(字段名语义化自取)、$$(整个 item)、$_index(行索引)。写成 $item.<字段> 也不影响结果——expand_ir 在扩展开头会把它自动归一化成规范形(作用域里本来就没有 item 这个键,schema 只存规范形;schema 级另有 autofix_schema.py 兜底)。范式照 recipe_snippets/list-expand-collapse.json 或 src/cards/account 的 account_details。结构确实不规则时允许保持静态,但要在正文说明理由。
|
|
373
376
|
- 命名语义化(memberLevel / totalAmount / assetRows),禁 text1 / val2;写完在正文列一句绑定清单(key → 设计稿文案)。
|
|
374
|
-
|
|
377
|
+
S4.9 内容就绪门禁(**像素自循环的前置条件**;不达标时 iterate 会把本轮整体挡掉——不 render、不比对像素、不消耗迭代轮次):
|
|
378
|
+
IR 写完、变量抽完后,先跑 skill_exec script=skills/dsl-card-generator/scripts/readiness.mjs,args=["--id","<落点卡>","--spec","<bundle>/spec.json","--design","<bundle>/card-N.png","--json"]。退出码 0=就绪 / 3=未就绪(**3 不是脚本报错,是内容没准备好**)。
|
|
379
|
+
五道**声明性**判定(不需要渲染,所以能挡在最贵的一步之前):A 组件契约(属性键 / 样式组 / 样式子键必须在 component-meta.json + style-meta.json 白名单内)· B 要素齐全(设计稿每条文案在 schema 里找得到)· C 变量绑定完备($var 的根变量必须在 dictionary.inputParams / mocks / data.json / 事件 setData 里存在)· D 图片地址已配置(每个 Image 必须有非空 value)· E 切图路径已配置(每个 /slices/... 引用在 src/slices 里有对应文件)。
|
|
380
|
+
blockers 非空 ⇒ **先补内容,别进 S5**。这一步存在的唯一理由:内容没齐时像素分毫无参考价值。实测事故(2026-09-24 gen-1):跳过这步,在图片地址都没配的情况下连刷 10 轮像素(match 卡在 87-90.46),两回合烧掉 623 万 prompt token 全部作废。
|
|
381
|
+
三条高频坑(readiness 会在 blockers 里逐条点名,不要靠猜):① Image 的图片地址属性键**只能是 value**,写成 image 会被运行时按 itemKey 绑定失败后**整条静默丢弃** ⇒ value 为空 ⇒ 破图,而 validate / lint / preflight 全绿;② from:"2" 的值必须是静态路径、from:"1" 的值必须是 $表达式,写反 ⇒ 字符串直接进 url(...) 或表达式求值失败 ⇒ 画布空白;③ 切图**引用名与磁盘文件名必须同源**(crop_slices 按引用名落盘;引用一个 src/slices 里不存在的名字 ⇒ 渲染 404、图空白、控制台零报错)。
|
|
382
|
+
**iterate 每轮会自动再跑这道门禁**:报告里 gate.stage=content(或 verdict 以 BLOCKED(内容未就绪)开头)表示本轮被挡——没跑 render、没跑 pixelDiff、**没消耗迭代轮次**。按 gate.verdict 里的 blockers 补齐内容后**直接重跑 iterate**即可;禁止在被挡时反复重跑空转,更禁止去调像素(那时候没有像素分可调)。
|
|
383
|
+
逃生门 --no-readiness(或环境变量 DSL_CARD_SKIP_READINESS=1)只在排查「门禁本身误判」时用,正常流程禁止。
|
|
384
|
+
S5 收敛循环(唯一门禁 = iterate.mjs 报告里的 gate 字段:内部已跑 autofix + 真 validate + lint + preflight + **内容就绪(S4.9)** + render + 并集像素对比,别再单独跑 validate_schema.py):
|
|
375
385
|
skill_exec script=skills/dsl-card-generator/scripts/iterate.mjs,args=["--ir",".cache/ai-scratch/<id>.ir.json","--out","src/cards/<落点卡>/","--design","<bundle>/card-N.png","--width","<卡宽>","--spec","<bundle>/spec.json"]。
|
|
376
386
|
修 IR 用 **ir_patch**(点路径 JSON 补丁,一次可多条,形如 ops=[{"path":"children.0.s.flex.gap","value":"0"}])——**它是改 IR 的首选**:结构化参数不吃引号转义、原子写、回旧值→新值摘要;也可用 iterate.mjs 的 --set(字符串字面量语法,注意引号转义,例:--set children.0.s.flex={"gap":"0"})。数字段=数组下标;中段不存在会响亮报错,按报错改路径。**两条路径都不是「整份重写」**——同轮内对同一草稿文件第 2 次全量 fs_write 会收到提醒、第 3 次起被拒(要重排结构才传 rewrite:"true")。
|
|
377
|
-
**收敛 = 报告 gate.passed === true
|
|
378
|
-
|
|
387
|
+
**收敛 = 报告 gate.passed === true**(**内容就绪(S4.9)** ∧ validate/lint/preflight 零 error ∧ **elements.missing=0** ∧ match ≥ 95 ∧ heightDelta=0 ∧ widthDelta=0 ∧ |contentOffset1x| ≤ 2)。gate 是脚本判的,正文只能引用它的 verdict / failed,不得自行宣布达标。像素对比按并集画布算,删内容 / 高度塌缩只会降分,不可能凑分。
|
|
388
|
+
**要素是固定真值:先内容、后骨架、最后样式(三段式门禁,逐段推进)**。S2 审计里的每条文本 / 每张切图都是这张卡的既定要素,S5 只许改**样式与布局**(尺寸 / 间距 / 颜色 / 字号 / flex / 位置),**不许丢要素、不许自造内容**。报告带 gate.stage 与 gate.checks[].name === 'readiness.content':
|
|
389
|
+
· stage=content(gate.blocked=true)⇒ 内容层没齐(要素 / 变量绑定 / 图片地址 / 切图落盘),**本轮根本没 render**。照 verdict 里的 blockers 补内容,别调像素、别重跑空转。
|
|
379
390
|
· stage=structure(elements.missing>0 或 validate/lint/preflight 未过)⇒ **先补要素,别调像素**:把设计稿的文案补回对应节点;绑定变量的把 defaultValue 改成设计稿真值。改样式不会把丢掉的文案变回来。
|
|
380
|
-
· stage=pixel ⇒
|
|
391
|
+
· stage=pixel ⇒ 内容与结构都齐了,才进入样式 / 布局收敛(一轮只改一个分项)。
|
|
381
392
|
实测事故(2026-09-21):模型把设计稿的产品名 / 利率 / 期限整批换成自己编的占位值(「理财优选1号 / 2.45 / 每日申购」),像素照样刷到 89%、validate/lint 全绿,而卡的**内容真值整批丢失**,直到人工比对设计稿才发现。报告 elements.invented 会把这类「schema 里有、设计稿里没有」的自造文案逐条点名,elements.missing 列出漏掉的要素。
|
|
382
393
|
报告还带:contentOffset1x + contentOffsetHint(内容整体平移时先修对齐,别逐行调间距)、**contributors + contributorsNote(分项归因:每个差异热区给出最可能出问题的具体节点)**、widthMismatch(--width 与设计稿卡面宽不符 ⇒ 先改 --width,否则 match/dy 都不可信)、lint.warningDetails、preflight.details、renderImage(渲染原图路径)、diffImageNote(并排图顶部色条是工具标题栏,不是卡片内容)、stalled(**连续 3 轮未刷新历史最好成绩**——震荡下滑同样触发——附 best 与回退命令)、best(历史最好成绩 {match,round})。
|
|
383
394
|
**contributors 是「差在哪个节点」的唯一答案,用它替代整份重写 IR**:数组每项 = 一个热区(hotspot/diffPct)+ elements[](按最内层优先排序的具体节点:cid / cls / text / rect / h / hSibling(同类中位高度)/ designH / dH(与设计稿高度差)/ flags / hotCover)。读法:flags 里 text-clipped ⇒ 该节点文本被裁剪,查 isMaxLine / maxLine / lineHeight;vs-design ±N ⇒ 该节点高度与设计稿差 N,查它自己的 paddingTop/Bottom 与 lineHeight;vs-siblings ±N ⇒ 同类元素里只有它高度不同,查它自己的 padding/margin(**不是整层 gap**);bottom-edge / right-edge ⇒ 差异贴边界外,查下边距 / 圆角 / 阴影。**一轮只改 contributors[0].elements[0] 这一个节点的一个属性**,改完立刻重跑 iterate 看是否刷新最好成绩——同时改多项无法归因。designH/dH 为 null 表示没给 --spec 或文本匹配不上,此时只能靠 flags 与同类中位判断,**不要自己编设计稿高度**。
|
|
@@ -386,7 +397,7 @@ S5 收敛循环(唯一门禁 = iterate.mjs 报告里的 gate 字段:内部
|
|
|
386
397
|
match 不升反降、heightDelta 非 0、或 hotspots 集中在原本已有内容的区域 ⇒ 说明在删内容凑分:回退该轮 --set 换思路。diffImage 每轮重写(报告里的路径),只给用户预览。
|
|
387
398
|
**取证禁令**:禁止用 shell_exec 写 node -e pngjs 像素脚本(行扫描 / 取色 / 自算 diff)——diff_bands.mjs、报告 hotspots、contentOffset1x、审计 colors/bgGradientHint 已经覆盖全部取证需求,实测手写脚本 4 次全是浪费步数;禁止 fs_read 任何图片;禁止在 S5 之前调 card_screenshot(渲染还没意义);禁止把 src/cards/<卡>/<卡>.png 当渲染图(那是设计稿归档,渲染原图是报告里的 renderImage)。同理**禁止自造分析 / 补丁脚本**(往草稿区写 .cjs/.mjs 再跑、node -e 扫像素或改 JSON 都在此列,实测白烧 11 轮)与**用 sed -i / 重定向直接改 IR、schema**(会绕过 validate、原子写与 .history 快照):改 IR 用 ir_patch(首选:点路径 JSON 补丁、原子、不吃引号转义)或 iterate.mjs --set,改卡片内容用 card_patch / card_write。
|
|
388
399
|
每轮用一两句正文汇报:gate.verdict / match% / heightDelta / 这轮修了什么,不要闷头连调。
|
|
389
|
-
S6 收尾门禁:① skill_exec script=skills/dsl-card-generator/scripts/check_fidelity.mjs,args=["--spec","<bundle>/spec.json","--schema","src/cards/<落点卡>/schema.json","--card","N"],按它报的文本缺失 / 字号偏差 / 颜色偏差修到无 ERROR
|
|
400
|
+
S6 收尾门禁:① skill_exec script=skills/dsl-card-generator/scripts/readiness.mjs,args=["--id","<落点卡>","--spec","<bundle>/spec.json","--design","<bundle>/card-N.png"],**必须 exit 0(就绪)**——内容层的四类缺口(要素 / 变量绑定 / 图片地址 / 切图落盘)在收尾时同样不许放过;② skill_exec script=skills/dsl-card-generator/scripts/check_fidelity.mjs,args=["--spec","<bundle>/spec.json","--schema","src/cards/<落点卡>/schema.json","--card","N"],按它报的文本缺失 / 字号偏差 / 颜色偏差修到无 ERROR;③ skill_exec script=skills/dsl-card-generator/scripts/iterate.mjs,args=["--gate","--out","src/cards/<落点卡>/"] 拿机器判定(读最近一轮落盘报告,schema 在报告之后被改过会直接判 FAIL;结论可能是 pass / stalled / **blocked-content** / iterating 四种,blocked-content 表示内容仍未就绪,不是收敛)。
|
|
390
401
|
正文汇报必须**逐字引用 --gate 的 verdict 与 failed 列表** + 落点卡 + 补了哪些切图 + 回滚方式。gate 未过禁止写「门禁全绿 / 已完成 / 验收通过」,必须列出未达标项、原因与下一步;禁止把未达标归因于「测量噪声 / 工具合成产物」——pixelDiff 只用渲染原图与设计原图,合成图色条不参与计算(见 diffImageNote)。
|
|
391
402
|
绑定对齐:check_fidelity 会按 data.json / defaultValue 把 $ 绑定解析回文案再对齐——报 missing-text 而该文本已绑定 ⇒ 是 mock 与设计稿不一致,改 defaultValue / data.json,**不是拆回字面量**。收尾自查:bindHints 每一项要么已绑定、要么正文说明为何保持字面量。
|
|
392
403
|
${vision ? '你有视觉能力:S5 里 match 不达标可直接读注入的并排对比图定位差异(哪一行 / 哪个颜色 / 哪段间距)再 --set;数值指标仍是收敛判据,图只用于定位。' : '当前模型无视觉:不要声称看过图;S2 的审计值 + S5 的数值指标就是你的眼睛,所有尺寸颜色文案取审计值,缺时按 references/ui_fidelity_rules.md 降级规则处理,禁止凭空估数值。'}
|
|
@@ -657,11 +668,23 @@ const SKILL_EXEC_DEFAULT_TIMEOUT = 180000
|
|
|
657
668
|
const SKILL_EXEC_MAX_TIMEOUT = 600000
|
|
658
669
|
let _pythonBin = ''
|
|
659
670
|
|
|
660
|
-
/**
|
|
671
|
+
/**
|
|
672
|
+
* Python 解释器:环境变量链 + 探测 python3 / python / py(管线脚本只用标准库,无需装依赖)。
|
|
673
|
+
*
|
|
674
|
+
* **能力必须与 `skills/dsl-card-generator/scripts/lib/python.mjs` 对齐**(那边是权威实现,
|
|
675
|
+
* 也是 skill 脚本侧真正用的那个)。两边不一致的症状很隐蔽:同一台机器上 skill 脚本跑得通、
|
|
676
|
+
* 而这里的 `skill_exec` 判「没有 Python」。之所以不做 import 而是照搬:`server/` 与 `skills/`
|
|
677
|
+
* 是两个可独立分发的单元(skills 要能整体复制到任意项目),代码级互相依赖会把这个前提破掉。
|
|
678
|
+
* 这里此前只认 `DSL_CARD_PYTHON` 且不认 `py` —— Windows 上 Python 从 Store 安装时往往只有
|
|
679
|
+
* `py` 启动器(`python3` 是应用执行别名:能启动、立刻 9009 退出)。
|
|
680
|
+
*/
|
|
661
681
|
function pythonBin() {
|
|
662
|
-
|
|
682
|
+
for (const key of ['DSL_CARD_PYTHON', 'WORKBUDDY_PYTHON', 'DSL_PYTHON', 'PYTHON']) {
|
|
683
|
+
const val = process.env[key]
|
|
684
|
+
if (val) return val
|
|
685
|
+
}
|
|
663
686
|
if (_pythonBin) return _pythonBin
|
|
664
|
-
for (const bin of ['python3', 'python']) {
|
|
687
|
+
for (const bin of ['python3', 'python', 'py']) {
|
|
665
688
|
try {
|
|
666
689
|
if (spawnSync(bin, ['--version'], { stdio: 'ignore' }).status === 0) {
|
|
667
690
|
_pythonBin = bin
|
|
@@ -1356,7 +1379,63 @@ const IR_PATCH_TOOL = {
|
|
|
1356
1379
|
},
|
|
1357
1380
|
}
|
|
1358
1381
|
|
|
1359
|
-
/**
|
|
1382
|
+
/** 编辑距离是否 ≤1(仅插入 / 删除 / 替换一次;键名短,贪心足够) */
|
|
1383
|
+
function irEditDistance1(a, b) {
|
|
1384
|
+
if (a === b) return false
|
|
1385
|
+
if (Math.abs(a.length - b.length) > 1) return false
|
|
1386
|
+
let i = 0
|
|
1387
|
+
let j = 0
|
|
1388
|
+
let diff = 0
|
|
1389
|
+
while (i < a.length && j < b.length) {
|
|
1390
|
+
if (a[i] === b[j]) {
|
|
1391
|
+
i++
|
|
1392
|
+
j++
|
|
1393
|
+
continue
|
|
1394
|
+
}
|
|
1395
|
+
if (++diff > 1) return false
|
|
1396
|
+
if (a.length === b.length) {
|
|
1397
|
+
i++
|
|
1398
|
+
j++
|
|
1399
|
+
} else if (a.length > b.length) {
|
|
1400
|
+
i++
|
|
1401
|
+
} else {
|
|
1402
|
+
j++
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return true
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/** irResolve 报错的现场提示:数组报长度,对象报键列表;段名与现有键编辑距离 ≤1 时点名 */
|
|
1409
|
+
function irNodeHint(node, k) {
|
|
1410
|
+
if (Array.isArray(node)) {
|
|
1411
|
+
return node.length
|
|
1412
|
+
? `该层是数组(长度 ${node.length},下标 0~${node.length - 1}),没有下标 ${k}`
|
|
1413
|
+
: '该层是空数组'
|
|
1414
|
+
}
|
|
1415
|
+
if (node === null || typeof node !== 'object') {
|
|
1416
|
+
return `该层是 ${node === null ? 'null' : typeof node},不能继续下钻`
|
|
1417
|
+
}
|
|
1418
|
+
if (Object.prototype.hasOwnProperty.call(node, k)) {
|
|
1419
|
+
const v = node[k]
|
|
1420
|
+
const kind = Array.isArray(v) ? '数组' : typeof v === 'string' ? `字符串 "${irBrief(v)}"` : typeof v
|
|
1421
|
+
return `路径段 "${k}" 存在但不是对象(是 ${kind}),不能继续下钻`
|
|
1422
|
+
}
|
|
1423
|
+
const keys = Object.keys(node)
|
|
1424
|
+
const keyNote = keys.length ? `该层现有键: [${keys.join(', ')}]` : '该层是空对象 {}'
|
|
1425
|
+
const near = keys.find((key) => {
|
|
1426
|
+
if (key === k) return false
|
|
1427
|
+
if (irEditDistance1(key, k)) return true
|
|
1428
|
+
// 复数化 / 截断型互混(child↔children、item↔items 是实测高频,编辑距离却是 3):
|
|
1429
|
+
// 前缀关系且差 ≤3 字符也点名
|
|
1430
|
+
const [a, b] = key.length < k.length ? [k, key] : [key, k]
|
|
1431
|
+
return b.length >= 3 && a.startsWith(b) && a.length - b.length <= 3
|
|
1432
|
+
})
|
|
1433
|
+
return near
|
|
1434
|
+
? `路径段 "${k}" 不存在,${keyNote}——是不是想写 "${near}"?`
|
|
1435
|
+
: `路径段 "${k}" 不存在,${keyNote}`
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** 点路径寻址:返回 { parent, key };中段缺失时抛错并指明第几段(报错必须带现场:只回显错误路径时模型只能瞎猜重试) */
|
|
1360
1439
|
function irResolve(root, dottedPath) {
|
|
1361
1440
|
const segs = String(dottedPath || '')
|
|
1362
1441
|
.split('.')
|
|
@@ -1367,7 +1446,8 @@ function irResolve(root, dottedPath) {
|
|
|
1367
1446
|
const k = segs[i]
|
|
1368
1447
|
const next = Array.isArray(node) ? node[Number(k)] : node[k]
|
|
1369
1448
|
if (next === undefined || next === null || typeof next !== 'object') {
|
|
1370
|
-
|
|
1449
|
+
const hint = irNodeHint(node, k)
|
|
1450
|
+
throw new Error(`路径中段不存在或不是对象:${segs.slice(0, i + 1).join('.')}${hint ? `(${hint})` : ''}`)
|
|
1371
1451
|
}
|
|
1372
1452
|
node = next
|
|
1373
1453
|
}
|
|
@@ -1414,6 +1494,16 @@ function irPatch(root, { file, ops }) {
|
|
|
1414
1494
|
if (isArr) parent[idx] = raw.value
|
|
1415
1495
|
else parent[key] = raw.value
|
|
1416
1496
|
newVal = raw.value
|
|
1497
|
+
// t/v 互斥联动(实测 gen-1:set v 后旧 t 残留 → iterate exit=1 硬报错多烧一轮):
|
|
1498
|
+
// Text 节点主值二选一,set 其一时自动清掉另一个,摘要里写明
|
|
1499
|
+
if ((key === 'v' || key === 't') && raw.value !== null && raw.value !== undefined && parent && typeof parent === 'object' && !Array.isArray(parent)) {
|
|
1500
|
+
const rival = key === 'v' ? 't' : 'v'
|
|
1501
|
+
if (Object.prototype.hasOwnProperty.call(parent, rival)) {
|
|
1502
|
+
const oldRival = irBrief(parent[rival])
|
|
1503
|
+
delete parent[rival]
|
|
1504
|
+
applied.push(`${path.replace(/\.[^.]+$/, '') || '.'}.${rival} [delete]: ${oldRival} → undefined(与新设的 ${key} 互斥,自动清除)`)
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1417
1507
|
} else if (op === 'merge') {
|
|
1418
1508
|
const cur = isArr ? parent[idx] : parent[key]
|
|
1419
1509
|
if (cur === null || typeof cur !== 'object' || Array.isArray(cur)) {
|
|
@@ -1702,13 +1792,22 @@ async function runShellExec(root, args, res, approved) {
|
|
|
1702
1792
|
/**
|
|
1703
1793
|
* 技能脚本退出码语义(白名单,按脚本文件名维护)。
|
|
1704
1794
|
*
|
|
1795
|
+
* **权威来源是 `skills/shared/exit-codes.md`** —— 本表是它的服务端实现,改语义要按
|
|
1796
|
+
* 「先改文档 → 再改本表 → 跑 tools/tests/exit-code-contract.test.mjs」的顺序走。
|
|
1797
|
+
*
|
|
1705
1798
|
* 为什么必须标:模型把一切非 0 一律读成「工具坏了」,于是去修根本不存在的问题,甚至原样重试。
|
|
1706
1799
|
* 而实测这条链路里大量非 0 是**脚本正常跑完、只是结论是负面的**:
|
|
1707
1800
|
* - `diff_bands.mjs` 找到差异 = exit 1(诊断输出,不是故障);
|
|
1708
|
-
* - `check_fidelity.mjs` / `lint_card.mjs` 报出偏差 = exit 1;
|
|
1709
|
-
* - `iterate.mjs` 门禁未达标 = exit 2
|
|
1801
|
+
* - `check_fidelity.mjs` / `lint_card.mjs` / `preflight.mjs` 报出偏差 = exit 1;
|
|
1802
|
+
* - `iterate.mjs` 门禁未达标 = exit 2(报告照常回喂);
|
|
1803
|
+
* - `readiness.mjs` 内容未就绪 = exit 3(**不是**脚本报错);
|
|
1804
|
+
* - `parse_meaxure.py` 多画板门禁 = exit 4(脚本故意退出,等用户选画板)。
|
|
1710
1805
|
* 首行给机器可读(模型可正则)+ 人可读的标签,把这一类误判一次性消掉。
|
|
1711
|
-
*
|
|
1806
|
+
*
|
|
1807
|
+
* kind:ok(0)|diagnostic(正常产出、结论负面)|gate-fail(门禁未达标)
|
|
1808
|
+
* |not-ready(内容未就绪)|needs-user(必须由用户选择)|fail(真失败)
|
|
1809
|
+
* 语义按 (脚本名, code) 二元组查 —— **同一个数字在不同脚本里含义可以不同**
|
|
1810
|
+
* (`2` 在 iterate 是 gate-fail、在 gate/ir_merge 是用法错误),不要只看数字。
|
|
1712
1811
|
*/
|
|
1713
1812
|
const SKILL_EXIT_SEMANTICS = {
|
|
1714
1813
|
'iterate.mjs': {
|
|
@@ -1729,6 +1828,25 @@ const SKILL_EXIT_SEMANTICS = {
|
|
|
1729
1828
|
1: { kind: 'fail', note: '参数或文件不对(多为快照/工作 IR 路径写错,先用 --list 看可用轮次)' },
|
|
1730
1829
|
3: { kind: 'fail', note: '快照不是合法 JSON,脚本拒绝写回工作 IR(换一份快照,别改这个脚本)' },
|
|
1731
1830
|
},
|
|
1831
|
+
// ── 2026-09-24 补齐(此前缺失 ⇒ 被误标成 fail,与文档「3 不是脚本报错」直接矛盾)──
|
|
1832
|
+
'readiness.mjs': {
|
|
1833
|
+
1: { kind: 'fail', note: '用法/参数错误' },
|
|
1834
|
+
3: { kind: 'not-ready', note: '内容未就绪(要素/变量绑定/图片地址/切图落盘四类缺口)——不是脚本报错,按报告逐项补内容后重跑' },
|
|
1835
|
+
},
|
|
1836
|
+
'route_input.mjs': {
|
|
1837
|
+
1: { kind: 'fail', note: '用法/参数错误' },
|
|
1838
|
+
3: { kind: 'needs-user', note: '输入形态判为 unknown——先问用户这是哪类输入,不要猜、不要自己挑' },
|
|
1839
|
+
},
|
|
1840
|
+
'parse_meaxure.py': {
|
|
1841
|
+
4: { kind: 'needs-user', note: '多画板选择门禁(rules.md §十三):脚本故意打印 1..n + 全部 清单后退出——把清单交给用户、停下等回复,再用 --pick / --scope-all 重跑' },
|
|
1842
|
+
},
|
|
1843
|
+
'gate.mjs': {
|
|
1844
|
+
1: { kind: 'gate-fail', note: '门禁未过(autofix→validate→lint→preflight 任一步 FAIL)——按 lint/validate 的规则号逐条修后重跑' },
|
|
1845
|
+
2: { kind: 'fail', note: '用法/参数错误' },
|
|
1846
|
+
},
|
|
1847
|
+
'preflight.mjs': { 1: { kind: 'diagnostic', note: '正常结果:列出 Pre-flight 问题,按 code 逐条修' } },
|
|
1848
|
+
'check_magic_values.mjs': { 70: { kind: 'diagnostic', note: '正常结果:发现魔法值,改用 lib/layout-tokens.mjs 的 T.*' } },
|
|
1849
|
+
'render_baseline.mjs': { 70: { kind: 'diagnostic', note: '正常结果:发现魔法值(历史取值,勿按数字猜语义)' } },
|
|
1732
1850
|
}
|
|
1733
1851
|
|
|
1734
1852
|
/** 退出码语义查询:0 返回 null(无需标注),未登记的非 0 按 fail 处理 */
|
|
@@ -1777,13 +1895,14 @@ function parseSkillJsonReport(text) {
|
|
|
1777
1895
|
* 最后是自己手动中止的。模型内部靠报告知道该停手,但用户侧是黑箱 —— 同一条事实必须同步给人看:
|
|
1778
1896
|
* 「第几轮 / 现在多少分 / 历史最好多少 / 是否已经连续几轮没进展 / 门禁过没过」。
|
|
1779
1897
|
* 复用既有 notice 通道,不加新协议;解析失败就静默跳过(播报是附加价值,不能影响主流程)。
|
|
1898
|
+
* 返回解析出的 rep(调用方据此做还原度熔断记账;跳过 / 解析失败返回 null)。
|
|
1780
1899
|
*/
|
|
1781
1900
|
function notifyIterateProgress(res, scriptRel, text) {
|
|
1782
|
-
if (!/iterate\.mjs$/.test(String(scriptRel || ''))) return
|
|
1901
|
+
if (!/iterate\.mjs$/.test(String(scriptRel || ''))) return null
|
|
1783
1902
|
const rep = parseSkillJsonReport(text)
|
|
1784
|
-
if (!rep || rep.ok !== true) return
|
|
1903
|
+
if (!rep || rep.ok !== true) return null
|
|
1785
1904
|
const match = rep.pixelDiff?.match
|
|
1786
|
-
if (match == null) return
|
|
1905
|
+
if (match == null) return null
|
|
1787
1906
|
const round = rep.rounds ?? null
|
|
1788
1907
|
const best = rep.best || null
|
|
1789
1908
|
const bestNote = best?.match != null ? `,历史最好 ${best.match}% @ r${best.round}` : ''
|
|
@@ -1797,7 +1916,7 @@ function notifyIterateProgress(res, scriptRel, text) {
|
|
|
1797
1916
|
'warn',
|
|
1798
1917
|
'iterate-stall',
|
|
1799
1918
|
)
|
|
1800
|
-
return
|
|
1919
|
+
return rep
|
|
1801
1920
|
}
|
|
1802
1921
|
sseNotice(
|
|
1803
1922
|
res,
|
|
@@ -1805,6 +1924,7 @@ function notifyIterateProgress(res, scriptRel, text) {
|
|
|
1805
1924
|
'info',
|
|
1806
1925
|
'iterate-progress',
|
|
1807
1926
|
)
|
|
1927
|
+
return rep
|
|
1808
1928
|
}
|
|
1809
1929
|
|
|
1810
1930
|
/** 同一技能调用(脚本 + 归一化参数)的键:连续失败退避的判据 */
|
|
@@ -2210,31 +2330,42 @@ const SOFT_NUDGE_MAX = 6
|
|
|
2210
2330
|
/** shell_exec 里「只是读文件 / 列目录 / 搜索」攒到几次就单独提醒一次(这些 fs_read / fs_list / card_grep 更直接) */
|
|
2211
2331
|
const SHELL_READ_LIKE_NUDGE = 3
|
|
2212
2332
|
/**
|
|
2213
|
-
*
|
|
2214
|
-
* 实测(test-1 会话 144 轮)多轮单轮正文 1.5-2 万字,全是像素比对 / 逐行对齐推理,被原样存进对话记录,
|
|
2215
|
-
* 用户翻记录只看到一堵墙。
|
|
2333
|
+
* 单轮「正文」软提醒阈值(字符):超过且这一轮还要继续调工具 ⇒ 那是过程性分析,不是给用户的结论。
|
|
2216
2334
|
*
|
|
2217
|
-
* 2026-09-
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
*
|
|
2335
|
+
* 2026-09-24 用户拍板:**不再硬截断**。旧版「截断 + 过程性分析省略 N 字」吞掉了不该吞的内容
|
|
2336
|
+
* (gen-1 画板清单 463 字整块消失、用户被要求回复序号却看不到选项;C1 结构化回捞修好了清单,
|
|
2337
|
+
* 但散文里的关键信息仍可能被吞,且截断-回捞-闭合栅栏的补丁链越来越复杂)。
|
|
2338
|
+
* 现在正文完整推给前端、完整进历史;超长只做一次性软提醒(见 bodyNarrationNudged),
|
|
2339
|
+
* 真正的暴走防线是 BODY_BURST_ABORT_CHARS 硬闸(见下)。
|
|
2221
2340
|
*/
|
|
2222
2341
|
const BODY_NARRATION_MAX_CHARS = 1500
|
|
2223
|
-
/** 正文超限时保留的尾部长(结论通常在这里) */
|
|
2224
|
-
const BODY_NARRATION_TAIL_CHARS = 300
|
|
2225
2342
|
/**
|
|
2226
|
-
*
|
|
2343
|
+
* 正文暴走硬闸:正文总长超过这么多字就直接掐断本步(而不是陪它写完)。
|
|
2227
2344
|
*
|
|
2228
2345
|
* 为什么要有它:截断发生在**生成之后**,而 token 是按生成量计费的。
|
|
2229
2346
|
* 实测(2026-09-21 demo 建卡回合第 5 步)模型一口气写了 24754 字过程性分析,
|
|
2230
|
-
*
|
|
2231
|
-
*
|
|
2347
|
+
* 白付约 1.5 万 output token,白等 2 分 38 秒流式输出,用户界面上还表现为「卡住不动」。
|
|
2348
|
+
* 到点掐断 + 系统提醒纠偏,代价远小于陪它写完。掐断时该步正文整段丢弃重试。
|
|
2232
2349
|
* 只在「本回合已经跑过工具」时才启用(step > 0):纯问答回合的长回答是正常产物。
|
|
2233
2350
|
*/
|
|
2234
2351
|
const BODY_BURST_ABORT_CHARS = 8000
|
|
2235
2352
|
/** 单回合暴走掐断次数上限:掐断本身也要能收敛,别变成新的打转来源 */
|
|
2236
2353
|
const BODY_BURST_MAX = 2
|
|
2237
2354
|
|
|
2355
|
+
|
|
2356
|
+
/**
|
|
2357
|
+
* 还原度熔断线:连续 stalled 轮数达到即熔断收尾。
|
|
2358
|
+
* 5 = 给「iterate.mjs 回退 best + 验证一轮」的最小窗口(stall=3 触发回退指令),仍无刷新即微调无收益。
|
|
2359
|
+
* gen-1 实测:best 91.64% @ r6 之后 11 轮 stalled 零提升、43 步 / 约 500 万 prompt tokens 白烧,
|
|
2360
|
+
* 收尾是被上下文极限挤出来的——现有 stall 提醒(只发用户)与逐字节熔断(振荡微调指纹轮轮变)全部失效。
|
|
2361
|
+
*/
|
|
2362
|
+
const STALL_WRAPUP_STALL_ROUNDS = 5
|
|
2363
|
+
/** 还原度熔断兜底线:iterate 总轮数绝对上限,防「缓慢爬升」型长跑 */
|
|
2364
|
+
const ITERATE_ROUNDS_HARD_CAP = 12
|
|
2365
|
+
/** 熔断后工具层拒执行时合成的 tool-result(ir_restore 不在禁用列表——回退 best 是熔断指令的一部分) */
|
|
2366
|
+
const FIDELITY_FUSE_REJECT =
|
|
2367
|
+
'已拒绝(还原度熔断):本回合禁止像素微调,iterate.mjs 与 ir_patch 已被禁用。按熔断指令收尾——确认盘上是 best 版本(必要时 ir_restore 回退),正文报告最终状态(match / 高度差 / 剩余差异点 / 未做成的项),最后单独一行输出 [任务完成]。'
|
|
2368
|
+
|
|
2238
2369
|
/**
|
|
2239
2370
|
* 免确认执行策略(AI_AUTO_APPROVE):
|
|
2240
2371
|
* none = 每次执行都弹审批(旧行为)
|
|
@@ -3015,6 +3146,24 @@ export function isAbbrevPath(p) {
|
|
|
3015
3146
|
return ABBREV_LEAD_RE.test(String(p || '').trim())
|
|
3016
3147
|
}
|
|
3017
3148
|
|
|
3149
|
+
/**
|
|
3150
|
+
* 非绝对路径的图片目标(`preview/a.png`、`UI/<稿名>/artboard.png`):
|
|
3151
|
+
* 模型照抄 manifest / spec / slices/index.json 的相对路径(2026-09-24 实测:
|
|
3152
|
+
* 提示词明令「写绝对路径」两次失守后,模型仍从 JSON 产物抄相对路径进清单)。
|
|
3153
|
+
* 这类路径直出必 404——交给 resolveAbbrevImage 反查(第 1 轮拼剩余相对路径正好对它友好,
|
|
3154
|
+
* 候选根里就有草稿导出源与其 preview/,以及工程根本身,`UI/…` 直接命中)。
|
|
3155
|
+
*
|
|
3156
|
+
* 防御:`..` 段一律拒(候选根拼接会被逃出根目录,不能给绝对路径之外的空间开反查口子);
|
|
3157
|
+
* 有 scheme(http:/data:/blob:…)或以 `/`、盘符、UNC 打头的不算相对路径。
|
|
3158
|
+
*/
|
|
3159
|
+
export function isRelativeImagePath(p) {
|
|
3160
|
+
const s = String(p || '').trim()
|
|
3161
|
+
if (!s || s.length > 1024) return false
|
|
3162
|
+
if (/^(?:[A-Za-z]:[\\/]|\\\\|\/|file:|[A-Za-z][A-Za-z0-9+.-]*:)/.test(s)) return false
|
|
3163
|
+
if (s.split(/[\\/]/).includes('..')) return false
|
|
3164
|
+
return IMAGE_EXT.has(path.extname(s).toLowerCase())
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3018
3167
|
/**
|
|
3019
3168
|
* 缩写路径的候选根,按可能性排序:最近用过的目录 → 草稿导出源(含其 `preview/`)→ 草稿目录 → 工程根。
|
|
3020
3169
|
* 导出源读的是 `<root>/UI/<稿名>/manifest.json` 的 `export` 字段 —— 导入时记下的原始出处,
|
|
@@ -3050,7 +3199,11 @@ function abbrevImageRoots(root) {
|
|
|
3050
3199
|
push(e)
|
|
3051
3200
|
}
|
|
3052
3201
|
push(root)
|
|
3053
|
-
|
|
3202
|
+
// 不设数量上限(原 slice(0, 16)):UI 草稿随库存增长,2026-09-24 实测 18 目录 / 41 根时
|
|
3203
|
+
// 字典序靠后的稿子(收支分析)export 根被悄悄截到 16 之外 ⇒ 其预览图反查全 404——
|
|
3204
|
+
// 「之前能看、现在全挂」的静默截断,且用户无从归因。反查只是内存 stat,根多无感;
|
|
3205
|
+
// 第 3 轮 readdir 有 8000 条/根的既有上限 + setImmediate 让出,不会失控。
|
|
3206
|
+
return out
|
|
3054
3207
|
}
|
|
3055
3208
|
|
|
3056
3209
|
const isFileAt = (p) => {
|
|
@@ -3094,6 +3247,8 @@ function pickUniqueFile(cands) {
|
|
|
3094
3247
|
export async function resolveAbbrevImage(target, root) {
|
|
3095
3248
|
const tail = String(target || '').trim().replace(ABBREV_LEAD_RE, '').replace(/\\/g, '/')
|
|
3096
3249
|
const segs = tail.split('/').filter((s) => s && s !== '.')
|
|
3250
|
+
// `..` 段一律拒:候选根拼接会被逃出根目录(fs/raw 的缩写入口不经 isRelativeImagePath 的 .. 过滤)
|
|
3251
|
+
if (segs.includes('..')) return ''
|
|
3097
3252
|
const base = segs[segs.length - 1] || ''
|
|
3098
3253
|
if (!base) return ''
|
|
3099
3254
|
const roots = abbrevImageRoots(root)
|
|
@@ -5037,8 +5192,9 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5037
5192
|
return res.end('unsupported-media')
|
|
5038
5193
|
}
|
|
5039
5194
|
// 省略号缩写路径(`…/a.png`):resolve 已把它拼成 `<cwd>/…/a.png`,必不存在。
|
|
5040
|
-
//
|
|
5041
|
-
|
|
5195
|
+
// 相对路径图片(`preview/a.png`、`UI/<稿>/artboard.png`):模型照抄 manifest/spec 的
|
|
5196
|
+
// 相对路径,直出也必 404。两者都改走「候选根反查」——治的是模型转述产物里的丢前缀形态。
|
|
5197
|
+
if (isAbbrevPath(target) || isRelativeImagePath(target)) {
|
|
5042
5198
|
const found = await resolveAbbrevImage(target, root)
|
|
5043
5199
|
if (found) abs = found
|
|
5044
5200
|
}
|
|
@@ -5448,6 +5604,8 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5448
5604
|
let autoContinueLeft = Math.max(0, Number(finalConfig.autoContinueMax) || 0)
|
|
5449
5605
|
/** 连续「只吐正文不调工具」的轮数:连续两次就尊重模型停手,避免纯问答被反复续跑 */
|
|
5450
5606
|
let textOnlyRounds = 0
|
|
5607
|
+
/** 还原度熔断:stalled.rounds ≥ 5 或 iterate 总轮数 ≥ 12 触发,触发后禁用 iterate/ir_patch 并让 auto-continue 让位 */
|
|
5608
|
+
let fidelityFused = false
|
|
5451
5609
|
/** 熔断收尾的人话原因:随 turn-ended 事件落进对话记录,重开面板仍看得见「这轮为什么停」 */
|
|
5452
5610
|
let budgetWhy = ''
|
|
5453
5611
|
let currentMessages = fullMessages
|
|
@@ -5583,10 +5741,6 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5583
5741
|
}
|
|
5584
5742
|
|
|
5585
5743
|
let assistantContent = ''
|
|
5586
|
-
/** 本轮正文全文:只为「超限时把尾部结论补回来」而留,本身不进历史、不推前端 */
|
|
5587
|
-
let bodyFull = ''
|
|
5588
|
-
/** 本轮被省略(未推前端、未进历史)的正文字符数 */
|
|
5589
|
-
let bodyOmitted = 0
|
|
5590
5744
|
let assistantToolCalls = null
|
|
5591
5745
|
let roundProgressAt = Date.now()
|
|
5592
5746
|
let lastDeltaAt = Date.now()
|
|
@@ -5666,29 +5820,18 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5666
5820
|
break
|
|
5667
5821
|
case 'text-delta': {
|
|
5668
5822
|
roundProgressAt = Date.now()
|
|
5669
|
-
//
|
|
5670
|
-
//
|
|
5671
|
-
// 下一回合的上下文,两边文本差一个字符,从那条起的前缀缓存就全部失效
|
|
5672
|
-
// (live 与回放逐字节一致是硬约束)。用户已看到的前 BODY_NARRATION_MAX_CHARS 字保留。
|
|
5823
|
+
// 正文完整推给前端、完整进历史(2026-09-24 用户拍板取消截断:折叠会吞不该吞的内容)。
|
|
5824
|
+
// live 与回放逐字节一致是硬约束——同一条文本走同一份字符串,这里天然满足。
|
|
5673
5825
|
const piece = String(evt.text || '')
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
} else {
|
|
5679
|
-
const kept = piece.length > remain ? piece.slice(0, remain) : piece
|
|
5680
|
-
assistantContent += kept
|
|
5681
|
-
bodyOmitted += piece.length - kept.length
|
|
5682
|
-
sseWrite(res, { type: 'text-delta', text: kept })
|
|
5683
|
-
}
|
|
5684
|
-
// 暴走硬闸:截断后还在继续吐 ⇒ 立刻掐断本步,别陪它写完再丢(output token 照付)。
|
|
5685
|
-
// 必须放在两路**之外**:封顶后走的是上面那条 `remain <= 0` 分支,
|
|
5686
|
-
// 把闸门写在 else 里等于永远不触发(第一版就是这么写错的,测试逮住)。
|
|
5826
|
+
assistantContent += piece
|
|
5827
|
+
sseWrite(res, { type: 'text-delta', text: piece })
|
|
5828
|
+
// 暴走硬闸:正文总长超限还在继续吐 ⇒ 立刻掐断本步(output token 照付,别陪它写完)。
|
|
5829
|
+
// 必须放在 sseWrite 之后逐 piece 检查:闸门写进某条分支里会漏触发(第一版就是这么写错的)。
|
|
5687
5830
|
if (
|
|
5688
5831
|
step > 0 &&
|
|
5689
5832
|
!roundBodyBurst &&
|
|
5690
5833
|
bodyBursts < BODY_BURST_MAX &&
|
|
5691
|
-
|
|
5834
|
+
assistantContent.length > BODY_BURST_ABORT_CHARS &&
|
|
5692
5835
|
!assistantToolCalls?.length
|
|
5693
5836
|
) {
|
|
5694
5837
|
roundBodyBurst = true
|
|
@@ -5809,14 +5952,14 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5809
5952
|
attempt: bodyBursts,
|
|
5810
5953
|
attempts: BODY_BURST_MAX,
|
|
5811
5954
|
dropText: assistantContent.length,
|
|
5812
|
-
message: `本步正文已超过 ${BODY_BURST_ABORT_CHARS
|
|
5955
|
+
message: `本步正文已超过 ${BODY_BURST_ABORT_CHARS} 字(过程性分析写太多):已掐断本步重来,请直接给结论或调用工具,别在正文里展开推理`,
|
|
5813
5956
|
})
|
|
5814
5957
|
currentMessages = [
|
|
5815
5958
|
...currentMessages,
|
|
5816
5959
|
{
|
|
5817
5960
|
role: 'user',
|
|
5818
5961
|
content:
|
|
5819
|
-
`[系统提醒] 你上一步的正文写到 ${BODY_BURST_ABORT_CHARS
|
|
5962
|
+
`[系统提醒] 你上一步的正文写到 ${BODY_BURST_ABORT_CHARS} 字仍未收尾,已被掐断(这一步没有产生任何工具调用)。` +
|
|
5820
5963
|
'过程性推理(像素比对 / 逐行对齐 / 几何推算)不要写进正文:正文每轮 ≤2 句 / 300 字,' +
|
|
5821
5964
|
'要么直接给出结论,要么直接调用下一个工具推进任务。',
|
|
5822
5965
|
},
|
|
@@ -5882,7 +6025,7 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5882
6025
|
...currentMessages,
|
|
5883
6026
|
{
|
|
5884
6027
|
role: 'user',
|
|
5885
|
-
content: `[系统催促] 思考预算已用完:立即停止分析,本轮**必须**以一次工具调用收尾(fs_write 写 IR / card_write / card_patch / card_scaffold),或直接用正文回复结论;不要复述卡片数据、不要在脑子里逐字草拟 JSON、**不要在思考里做像素算术**(逐行核对坐标 / 心算 gap 与对齐)。不确定的组件写法就直接 skill_read recipe_snippets/ 下对应片段(≤2KB、0.3s,读它不算浪费);几何细节按审计值先落盘,再用 render-status / check_fidelity 的数值指标迭代。${designFlow ? '建卡走 IR 路线:fs_write 把紧凑 IR 写到 .cache/ai-scratch/<id>.ir.json,再用 skill_exec 跑 expand_ir.mjs 展开,一次调用就能落盘整卡;落盘后仍须跑完 check_fidelity + iterate.mjs 像素自循环(数值指标,1-2
|
|
6028
|
+
content: `[系统催促] 思考预算已用完:立即停止分析,本轮**必须**以一次工具调用收尾(fs_write 写 IR / card_write / card_patch / card_scaffold),或直接用正文回复结论;不要复述卡片数据、不要在脑子里逐字草拟 JSON、**不要在思考里做像素算术**(逐行核对坐标 / 心算 gap 与对齐)。不确定的组件写法就直接 skill_read recipe_snippets/ 下对应片段(≤2KB、0.3s,读它不算浪费);几何细节按审计值先落盘,再用 render-status / check_fidelity 的数值指标迭代。${designFlow ? '建卡走 IR 路线:fs_write 把紧凑 IR 写到 .cache/ai-scratch/<id>.ir.json,再用 skill_exec 跑 expand_ir.mjs 展开,一次调用就能落盘整卡;落盘后仍须跑完 check_fidelity + iterate.mjs 像素自循环(数值指标,1-2 轮即可)才允许收尾;还原度熔断已触发时除外——此时收尾就是正确动作。' : ''}`,
|
|
5886
6029
|
},
|
|
5887
6030
|
]
|
|
5888
6031
|
continue
|
|
@@ -5915,16 +6058,9 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5915
6058
|
// 否则一次思考熔断会把整轮后续所有步骤都降级成无思考模式,长程任务质量明显下滑
|
|
5916
6059
|
if (assistantContent || assistantToolCalls?.length) retryNoThinking = false
|
|
5917
6060
|
|
|
5918
|
-
//
|
|
6061
|
+
// 正文超限收尾:中段结构化行回捞 + 尾部补回(中间散文省略),推给前端与进历史的是**同一份文本**。
|
|
5919
6062
|
// 顺序必须在 settled 判定与三处 append 之前:DONE_MARK / ASK_MARK 写在正文最后一行,
|
|
5920
6063
|
// 只留头部会把标记一起截掉,那会让「已完成」的轮次被判成「未完成却停手」而自动续跑。
|
|
5921
|
-
if (bodyOmitted > 0) {
|
|
5922
|
-
const tail = bodyFull.slice(-BODY_NARRATION_TAIL_CHARS)
|
|
5923
|
-
const marker = `\n…[过程性分析省略 ${bodyOmitted} 字]…\n`
|
|
5924
|
-
assistantContent = assistantContent + marker + tail
|
|
5925
|
-
sseWrite(res, { type: 'text-delta', text: marker + tail })
|
|
5926
|
-
}
|
|
5927
|
-
|
|
5928
6064
|
if (!assistantToolCalls?.length) {
|
|
5929
6065
|
// 自动续跑:模型只吐正文就停手——实测最常见形态是输出「下一步(请确认或直接说「继续」)」
|
|
5930
6066
|
// 把控制权甩回用户,而任务其实没做完(IR 写了但 expand_ir 没跑、门禁没过)。
|
|
@@ -5932,7 +6068,8 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5932
6068
|
// 或已经连续停手两次(尊重模型判断),否则自动注入提醒接着推进。
|
|
5933
6069
|
textOnlyRounds++
|
|
5934
6070
|
const settled = TASK_SETTLED_RE.test(assistantContent)
|
|
5935
|
-
|
|
6071
|
+
// 熔断后让位:模型按熔断指令收尾(停手报状态)正是正确动作,绝不能再被「未完成」推回循环
|
|
6072
|
+
if (!settled && !aborted && step > 1 && textOnlyRounds < 2 && autoContinueLeft > 0 && !fidelityFused) {
|
|
5936
6073
|
autoContinueLeft--
|
|
5937
6074
|
sseNotice(res, `任务未完成就停手(没有 ${DONE_MARK} / ${ASK_MARK} 标记):已自动续跑,不用手动输入「继续」(本回合剩余 ${autoContinueLeft} 次)`, 'info', 'auto-continue')
|
|
5938
6075
|
currentMessages = [
|
|
@@ -5940,7 +6077,7 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5940
6077
|
{ role: 'assistant', content: assistantContent || null },
|
|
5941
6078
|
{
|
|
5942
6079
|
role: 'user',
|
|
5943
|
-
content: `[系统提醒] 你没有完成任务就停下了,也没给出 ${DONE_MARK} / ${ASK_MARK} 标记。不要征求确认、不要输出「下一步请说继续」——直接接着调用工具推进到下一个门禁。全部要求都已落地且门禁通过时,才在正文最后单独一行输出 ${DONE_MARK};确实缺只有用户能提供的信息时,才在正文最后单独一行输出 ${ASK_MARK} 并写清要用户回答什么。${designFlow ? '建卡未跑完 expand_ir + iterate --gate
|
|
6080
|
+
content: `[系统提醒] 你没有完成任务就停下了,也没给出 ${DONE_MARK} / ${ASK_MARK} 标记。不要征求确认、不要输出「下一步请说继续」——直接接着调用工具推进到下一个门禁。全部要求都已落地且门禁通过时,才在正文最后单独一行输出 ${DONE_MARK};确实缺只有用户能提供的信息时,才在正文最后单独一行输出 ${ASK_MARK} 并写清要用户回答什么。${designFlow ? '建卡未跑完 expand_ir + iterate --gate 就不算完成(还原度熔断已触发时除外——此时收尾就是正确动作)。' : ''}`,
|
|
5944
6081
|
},
|
|
5945
6082
|
]
|
|
5946
6083
|
continue
|
|
@@ -5951,22 +6088,22 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
5951
6088
|
textOnlyRounds = 0
|
|
5952
6089
|
|
|
5953
6090
|
// 正文纪律:这一轮还要继续调工具 ⇒ 上面的正文是过程性推理,不是给用户的结论。
|
|
5954
|
-
//
|
|
5955
|
-
//
|
|
5956
|
-
if (
|
|
6091
|
+
// 已取消截断(2026-09-24 用户拍板),这里只做一次性软提醒:超长正文会随每步整轮重发,
|
|
6092
|
+
// 明说代价才可能改变下一步行为(实测只提醒不截断时模型仍会写长,但这正是用户选择的取舍)。
|
|
6093
|
+
if (assistantContent.length > BODY_NARRATION_MAX_CHARS && !bodyNarrationNudged) {
|
|
5957
6094
|
bodyNarrationNudged = true
|
|
5958
6095
|
sseNotice(
|
|
5959
6096
|
res,
|
|
5960
|
-
|
|
6097
|
+
`本轮正文 ${assistantContent.length} 字(提醒阈值 ${BODY_NARRATION_MAX_CHARS}):不截断,但会随每步整轮重发、推高上下文成本`,
|
|
5961
6098
|
'warn',
|
|
5962
6099
|
'verbosity',
|
|
5963
6100
|
)
|
|
5964
6101
|
currentMessages.push({
|
|
5965
6102
|
role: 'user',
|
|
5966
6103
|
content:
|
|
5967
|
-
`[系统提醒]
|
|
6104
|
+
`[系统提醒] 你上一条正文写了 ${assistantContent.length} 字(阈值 ${BODY_NARRATION_MAX_CHARS}):` +
|
|
5968
6105
|
'过程性分析(像素比对 / 逐行对齐 / 几何推算)写在内部即可,正文只写结论与关键进展(每轮 ≤2 句 / 300 字)。' +
|
|
5969
|
-
'
|
|
6106
|
+
'超长正文会随对话每一步整轮重发,显著推高上下文成本。继续推进任务,不要复述分析过程。',
|
|
5970
6107
|
})
|
|
5971
6108
|
}
|
|
5972
6109
|
|
|
@@ -6066,12 +6203,17 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
6066
6203
|
result = fsWrite(root, parsedArgs, fsWriteLedger)
|
|
6067
6204
|
break
|
|
6068
6205
|
case 'ir_patch':
|
|
6069
|
-
result = irPatch(root, parsedArgs)
|
|
6206
|
+
result = fidelityFused ? FIDELITY_FUSE_REJECT : irPatch(root, parsedArgs)
|
|
6070
6207
|
break
|
|
6071
6208
|
case 'fs_copy':
|
|
6072
6209
|
result = fsCopy(root, parsedArgs, mountedRoots)
|
|
6073
6210
|
break
|
|
6074
6211
|
case 'skill_exec': {
|
|
6212
|
+
// 还原度熔断:iterate 不再真执行(合成拒绝结果让循环继续,模型还能说出收尾词)
|
|
6213
|
+
if (fidelityFused && /iterate\.mjs$/.test(String(parsedArgs?.script || ''))) {
|
|
6214
|
+
result = FIDELITY_FUSE_REJECT
|
|
6215
|
+
break
|
|
6216
|
+
}
|
|
6075
6217
|
// 连续失败退避:同一调用(脚本 + 参数)连续失败 2 次就不再执行。
|
|
6076
6218
|
// 报错一字未变时第三次必然还是同样的错,而每撞一次都要多付一步全量 prompt。
|
|
6077
6219
|
const callKey = skillCallKey(parsedArgs, root)
|
|
@@ -6082,7 +6224,31 @@ export function createAiChatMiddleware(opts = {}) {
|
|
|
6082
6224
|
result = await runSkillExec(root, parsedArgs, res, approvedTools)
|
|
6083
6225
|
noteSkillExecOutcome(callKey, parsedArgs.script, result, execFails)
|
|
6084
6226
|
// 迭代进度播报:把人从「29 分钟黑箱等待」里救出来(只走真执行路径,短路不播)
|
|
6085
|
-
notifyIterateProgress(res, parsedArgs.script, result)
|
|
6227
|
+
const itRep = notifyIterateProgress(res, parsedArgs.script, result)
|
|
6228
|
+
// 还原度熔断记账:stalled 连续轮数 / 总轮数到线 ⇒ 一次性注入硬提醒 + 禁用微调工具。
|
|
6229
|
+
// 为什么是硬熔断而不是再提醒:stall 提醒实测 8 次 0 次改变行为(只发用户);逐字节
|
|
6230
|
+
// 指纹熔断被振荡式微调(lineHeight 10↔12↔16)结构性绕开——只有工具层禁用能兜住。
|
|
6231
|
+
// gate.passed 的轮不触发:门禁过了自然收尾,不需要熔断。
|
|
6232
|
+
if (
|
|
6233
|
+
itRep &&
|
|
6234
|
+
!fidelityFused &&
|
|
6235
|
+
itRep.gate?.passed !== true &&
|
|
6236
|
+
((itRep.stalled?.rounds ?? 0) >= STALL_WRAPUP_STALL_ROUNDS ||
|
|
6237
|
+
(itRep.rounds ?? 0) >= ITERATE_ROUNDS_HARD_CAP)
|
|
6238
|
+
) {
|
|
6239
|
+
fidelityFused = true
|
|
6240
|
+
const stallRounds = itRep.stalled?.rounds ?? 0
|
|
6241
|
+
const fuseWhy =
|
|
6242
|
+
stallRounds >= STALL_WRAPUP_STALL_ROUNDS
|
|
6243
|
+
? `已连续 ${stallRounds} 轮未刷新最好成绩`
|
|
6244
|
+
: `总轮数 ${itRep.rounds} 已到上限 ${ITERATE_ROUNDS_HARD_CAP}`
|
|
6245
|
+
const bestNote = itRep.best?.match != null ? `best=${itRep.best.match}% @ 第 ${itRep.best.round} 轮` : '无有效 best'
|
|
6246
|
+
sseNotice(res, `还原度熔断:${bestNote},${fuseWhy}——已禁用 iterate / ir_patch,模型将直接收尾`, 'warn', 'fidelity-fuse')
|
|
6247
|
+
currentMessages.push({
|
|
6248
|
+
role: 'user',
|
|
6249
|
+
content: `[还原度熔断] ${bestNote},${fuseWhy},像素级微调已无收益。立即收尾:① 若盘上 schema 不是 best 版本,跑一次 ir_restore 回退到 best;② 正文报告最终状态(match / 高度差 / 剩余差异点 / 未做成的项);③ 正文最后单独一行输出 ${DONE_MARK}。禁止再调用 iterate.mjs 与 ir_patch(本回合已被禁用,调用会直接被拒)。`,
|
|
6250
|
+
})
|
|
6251
|
+
}
|
|
6086
6252
|
}
|
|
6087
6253
|
break
|
|
6088
6254
|
}
|
|
@@ -6317,9 +6483,14 @@ export {
|
|
|
6317
6483
|
observeCtxScale,
|
|
6318
6484
|
parseSkillJsonReport,
|
|
6319
6485
|
notifyIterateProgress,
|
|
6486
|
+
irResolve,
|
|
6487
|
+
irNodeHint,
|
|
6488
|
+
irEditDistance1,
|
|
6489
|
+
STALL_WRAPUP_STALL_ROUNDS,
|
|
6490
|
+
ITERATE_ROUNDS_HARD_CAP,
|
|
6491
|
+
FIDELITY_FUSE_REJECT,
|
|
6320
6492
|
cardIdFromToolArgs,
|
|
6321
6493
|
BODY_NARRATION_MAX_CHARS,
|
|
6322
|
-
BODY_NARRATION_TAIL_CHARS,
|
|
6323
6494
|
}
|
|
6324
6495
|
|
|
6325
6496
|
export default createAiChatMiddleware
|