# Changelog ## 0.4.1 > 人在审批框前决定放不放行时,也能看到审核模型给的那句理由了:详情行的「自动判定:…」下面新增一行「**审核理由:…**」(就是模型判定时写的 `理由:`,单行、上限 200 字、截断写明共多少字)。**判定压根没跑成时不写这一行**——那时事件里那句是 `err.*` 闭集证据,不是模型说的话。 > 同版还改了判决行的写法:**审核表命中时直接显示命中的行 id 与等级**(`bulk · high`,与宿主复核框同一形状),不再重复写「审核表转人工」;等级走兜底档时带「(兜底档)」注记。另有插件市场的新截图与文档整理。 ### 行为变化 - **判决行在审核表命中时改为显示命中的行 id 与等级**(`自动判定:bulk · high`):`path`(`criteria-human`)只说「怎么处置」,**是哪一行判的**只有 `judge.criterion` / 顶层 `category` 里有;框已经弹出来了,「审核表转人工」这半句是重复信息。等级走兜底档时补「(兜底档)」(与宿主复核框同一条注记:`medium` 是模型给的、还是等级认不出落下来的,对人不是同一件事)。**类别认不出**(`src=none`,程序落兜底行)不写 id——那是失败关闭的产物,不是模型的结论,此时退回处置标签;关键词、判定失败、超上限这些没有 id 的路也照旧显示处置标签。 - **审批框详情行新增「审核理由:…」**(原生越权框与模型主动求复核的那条路都一样):这一行以前只在「审批」tab 展开后的审计区(`detail.judgeReason`)里能看,人却是在框前做决定。理由是**审核模型自己写过的那句 `理由:`**,与判决行同源(宿主 `forwardToHuman` 的 `manual-pending` 事件),值取 `judge.reason`、回落 `judgeReason`,单行化后截到 200 字,截断时补「(已截断,共 N 字)」。 - **判定失败不写理由**:`src ∈ empty / timeout / call / route / plugin`(与宿主 `JUDGE_FAILURE_SRCS` 同口径)、`judge.failed`、以及判定路径上的 `src=truncated` 都不写——这几态事件里那句是 `err.judgeEffort` / `err.judgePayloadOversize request=N>B` 这类**闭集证据**,判决行那句「判定失败转人工」/「内容超过送审上限」已经交代结局,原始证据仍在「审批」tab。关键词人工桶那种**压根没问过模型**的转人工自然也没有理由可写(不会多出一行空的「审核理由:」)。 - **理由的额度跟着「操作」那段走**:操作摘要占满自己的 600 字预算时,理由只留 80 字,避免两头挤在一起时把要批准的内容挤下去;其余情况 200 字(`judge.reason` 在解析侧本来就被截到 200)。 - **判决行的缓存键加了第三段(操作摘要长度)**:那一行是按长度算出来的,同一 `callId` 在不同长度下必须重算,不能复用按另一个额度渲染的旧行;`useChat` 那一钩相应排到 `useState` 惰性初值之前。 - 修掉一个形状问题:复核框只有理由、没有判决标签时,那一格不再以一个空行开头(判决行被刻意留空是为了不自指,但空行本身是噪声)。 - 网页文案新增 `approval.judgeReason` / `approval.judgeCut`(中英),中英 README 的「设置」一节把审批框里人能看到什么写全。 - 测试 454 项全绿(`tests/client.test.mjs` 新增:有理由就写、两种字段形状、判定没跑成不写、单行化、两种额度的截断标记、缓存键长度敏感、复核框留空;判决行那一组补了「命中行 id + 兜底档注记」的断言)。 - 插件市场截图整批更新(6 张:设置页总览 / 关键词 / 审核表 / 风险等级 / 模型转人工 + 审核模型 / 审批历史),`screenshots.json` 指向新图。 ## 0.4.0 > 自动拒绝不再只给模型一句 `the user rejected tool "…"`(那是**错误归因**:关键词红线、审核表判定、插件异常全成了「用户拒绝」),而是附上机器判定的原因;可选开启「模型转人工」,让模型把某次被拒的操作交回人决定。 > 同版还包含:**原生审批框的详情行由本插件渲染**(`write` / `edit` / MCP 也能看清要批准什么,并附一行「自动判定:…」)、复核框写清「判决 + 操作 + 模型理由」、风险等级在页面上原样显示 `low` / `medium` / `high`、思考强度删掉与「模型默认」重复的 `off`、审核模型**首轮输出预算抬到 8192 并可在设置页配置**、出厂两处文案消歧(**allowlist 版本 22 → 23**)。 ### 行为变化 - **`other` 行的出厂说明改为「以上条目全部不符合或无法确认」**(英文 `None of the rows above fit, or it cannot be confirmed`):更短,且与提示词里「没有任何一行能确认符合时才选那一行」同义,不再自带「拿不准 / 看着无害也选」这种分类指引。**allowlist 版本 20 → 21**;迁移只替换仍是上一版出厂原文的说明,用户自己改过的不动、三格更不碰(与 `prevVersion < 18` 那次刷文案同一套做法)。 - **拒绝原因回传(默认生效)**:走 `tools/post-execute` 的 `additionalContexts`——`ApprovalOutcome` 是闭集字符串,服务层把 `rejected` 统一渲染成 `the user rejected tool "…"`,插件没有别的位置能附带原因。原因**只由闭集派生**:命中的关键词(你自己的词表)、审核表类别 id 与等级(含是否走了兜底档)、缺参/截断、判定失败的具体来源;**审核模型那段「理由」原文不回传**(含命令片段与文件内容,回灌上下文等于开一次注入入口)。自由文本一律单行化 + 截断,卡片围栏字样中和。 - **`other` 行与任何一行同权**:类别、等级、理由一律照给,不加「兜底 / 没跑成」之类措辞——它唯一的特殊之处是不能删除。是否落 `other` 由模型按行说明决定。 - **三种口径分清楚**:自动拒绝(可附转人工入口)、**人明确拒绝**(不再指路转人工)、**转人工但没拿到结论**(说清「不是人拒的」——DSH 给模型的原文仍是「用户拒绝」)。后两态都不是「还在等人」:人工框只有「允许 / 拒绝」两个按钮,`cancelled` 来自请求被中止、`unavailable` 来自没人在场应答。 - **归因只在判定落定后记**:同一条路径既可能 reject 也可能 allow/human(`truncatedAction`、三格动作),在解析侧记会让「人工批准放行」的调用也收到一条「已拒绝」通知。 - **模型转人工工具**(设置页开启,默认关闭):工具常驻注册,名字来自 `humanReview.toolName`(默认 `request_human_approval`),开关只在 execute 里判。四条语义:① 转人工请求**永远由人决定**(不查关键词、不查审核表、不查三格——否则请求本身被自动拒绝就是自锁死循环);② 人工批准只对**同会话 + 同工具 + 参数规范化后完全相同**的一次调用有效,用一次即销毁,参数一变走完整管道;③ 人工拒绝是终局(本会话不再问第二次,模型被告知别再试);④ 在途同键去重,凭证与暂存在 Fiber 析构时清空。 - 审核提示词末尾**追加**一段转人工说明(仅开启时):只讲通用角色,不点名出厂 id,也不在模板之外再规定一次输出格式。 - 事件新增闭集字段 `denyReason`(keyword / criterion / judge-timeout / …),`auto-approve/decision` 在拒绝时也带它——「为什么被拒」不再需要从 `path` + `src` + `category` 里各拼一半。 - `humanReview` 插件配置逐键合并:设置页只提交改过的那一项,不会把用户改过的工具名冲回默认。 - `tools` 服务用 `ctx.get('tools')` 取,**不进 `inject`**(同 `webServer` 的理由:headless / acp / sdk 组合没有它会停在 PENDING,连门控都不挂载);注册前查重名,冲突只警告。 - 设置页新增「模型转人工」卡片与总览步骤;「看不见这次操作时」的提示改为说明拒绝原因会回传。 - **送审改成「要么完整、要么不问模型」:单字段限额体系整套删除,换成一个全局送审上限**。删掉的东西:`TOOL_ARG_LIMITS`(20 个字段各自的限额)、`argLimitFor` 在**送审**路径上的按键尾限额(函数本身留着,只服务事件存档)、`toolArgsTruncated`、`formatTruncatedNote`、`assertJudgeArgsWithinLimits`(上一版刚加的断言)、`clipToolArgsForJudge`,以及卡片上的「以下字段过大未展示」与 `omitted` 记账。现在的规则只有一条:`formatJudgeCard` 不切任何字段、不设条数上限;闸门量**整条请求**(系统提示词 + 卡片)与设置页新增的 **送审内容上限**(`judgeRequestBudget`,默认 20000 字符;下限经两轮 review 从 2000 提到 4096、再到 **8192**——英文出厂框架 ~5.8k 字符,见下),超了就按「看不见这次操作时」里「内容超过送审上限」的动作处理。**触发必写日志 + 审计**:`console.warn` + 审计行 `err.judgePayloadOversize request=<实际>>预算`,事件走 `src=truncated`,不会静默转人工。 - **取消收集阶段的深度与键数上限(安全修复)**:旧的 6 层 / 200 键会在超限时**静默丢弃**字段,而被丢的字段既不进参数字典、也不进关键词干草——实测「批量 250 个文件路径、门控配置在末位」时红线整条失效(`~/.dsh/auto-approve/allowlist.json` 既不被关键词命中、也不上卡片),旧版会照常送审甚至可能被 `safe` 放行。现在只保留一个不可配的字节护栏 `RAW_COLLECT_GUARD_BYTES = 8MB`(正常调用差三个数量级;实测 10 万叶子约 2.9MB 干草 / 225ms),撞到即标记 `over`,与超预算走**同一个**动作并记 `oversize=collect>8388608`。顺带修好了旧限额在嵌套下算不准的问题(单数组收 199、`params.files` 只收 198)。 - **卡片去重定稿:每个参数恰好一行,不丢也不重**。旧实现按「键尾等于某个已知字段名」剔除,`{args:{file_path:'x'}, extra:{file_path:'y'}}` 只渲染 `x`、`y` 消失;中途试过的「按值去重」修好了那个,但代价是 `{url, body}` 同值、`{file_path, path}` 并存时**另一个参数整条消失**(模型在看不见的参数上给结论)。现在的规则是四条:① 每个键恰好一行;② 只在「同一个参数的两个位置」上合并(被渲染过的键与它自己的嵌套变体);③ 不按值去重;④ 键尾相同也不合并。同时收敛标签:`query`/`input`/`text`/`body`/`message`/`pattern`/`selector` 不再各有一个中文名,改印成「参数 <名>: 」(`CARD_PLAIN_KEYS`),只有 `code`/`url`/`script`/`sql` 保留语义标签。契约测试按「行归属」判定(值相同也抓得住重复与丢失)。 - **自定义提示词超限不再静默截断**:旧版把超过 20000 字符的模板 `slice(0, 20000)` 存盘,而模板尾巴通常正是输出格式与等级要求——被砍掉后模型不再输出等级行,**全部判定静默落 `levels.fallback`**,且设置页回显的就是被砍过的版本。现在保存时直接报 `err.judgePromptTooLong`(带 `{chars}`/`{max}`),一个字节都不写盘。 - **闸门顺序修正:关键词拒绝现在能拦住「缺参 / 超预算」的调用**。缺参分支此前在关键词层之前 `return`,于是用户把危险工具名或词写进拒绝桶时,对它**静默无效**(实测:`mcp__deploy__run` 在拒绝桶里,缺参调用照样弹人工框)。现在顺序是 `拒绝关键词 → 参数没采集到(无条件拒绝)→ 人工关键词 → 收集/预算闸门 → 允许关键词 → 判定`:两个关键词桶都先行(那是用户显式的「不要做」),允许**必须过闸门**(没采集到/超预算禁放行)。新增「闸门顺序」用例覆盖三个方向。 - **MCP 的 `description` 不再被当成「模型自述」**:内建工具里它是元数据,但 MCP 的 schema 里它常常就是正文参数(jira 的 description、issue body)。此前这种调用被整条归成「没有可审的操作内容」→ 转人工,而人工框只看得到工具名,等于让人在盲审里拍板。现在按既有原则处理——**键名不在 `TOOL_ARG_KEYS` 里就是这个工具自己的参数**(当时的判据函数 `isCustomToolName` 已随后面的闸门删除一并清理),`mcp__*` 的 description 算内容、送审核模型;内建工具的 description 语义不变。 - **数字/布尔参数不再等于「没给参数」**:MCP 工具的 schema 里大量是开关(`recursive` / `force` / `include_archived` / `limit`),以前这些调用收集到的 `args` 是空的,于是被归成 `missing-payload`(默认转人工)——可 `recursive:true, force:true` 恰恰是判断危险性最需要的信息,关键词层与审核模型却都看不到。现在标量也收成文本(`'true'` / `'42'`)与字符串参数同形:上卡片、进事件(当时还参与载荷判定 `hasToolPayloadIncludingScalars`,那个判据已随后面的闸门删除),但**不进关键词干草**(`true`/`100` 进干草只会误命中,由 `scalars` 集合区分)。空串仍然不算载荷;`{description:'x'}` 仍然算「没给参数」(语义没变)。 - **「参数没采集到」改成直接拒绝 + 告诉模型重发**(不再走 `truncatedAction`、不再弹人工框):它是插件侧瞬时故障(缓存未命中 / 已消费 / 被挤出 256 条),让人为插件故障拍板没有意义(人也看不到任何内容),而且这一态**没有参数可做一次性凭证的键**。拒绝经 post-execute 旁路带一条通知回给模型,档位是新的 `payload-uncaptured`,措辞明确「这是插件侧采集故障、与内容大小无关、重新发起同一次调用即可」——**与 `payload-truncated`(内容超过上限,「别再发」)严格分开**,否则模型会朝错方向重试。恢复闭环有测试覆盖:重发一次就正常走完管道。 - **「参数没采集到」在审批记录里自带标记**:事件加 `argsCaptured: false`(pending 与人工结论两条都有),审批 tab 在 `args` 为空且该标记为 false 时显示告警「⚠ 未采集到这次调用的参数……批准等于同意一次你没看到内容的操作」,不再和「这次调用真的没有参数」一样显示 `(空)`。触发场景四种:请求没有 `callId`、会话键对不上、同一 callId 被取两次、缓存超过 256 条被挤出(无 TTL)。 - **判定前只剩一个开关:`missingPayloadAction` 整套删除**(用户要求:「无论如何,只要没有超过上限,都把内容原样交给审核模型去判断」)。删掉的东西:配置项、设置页那一行、rule-op 的 kind、`hasToolPayloadIncludingScalars` 与 `isCustomToolName` 在判定路径上的调用,以及 `missing-payload` 这条 path 与 `denyReason.payload-missing` / `payload-oversize` 两个档位。**内容多少不再是一道闸门**:空参数、只有 `description`/`workdir`、纯数字/布尔开关——一律照常送审,由模型按卡片判(`normalizeAllowlist` 里 `delete cfg.missingPayloadAction` 丢掉老配置的残留键)。判定前只剩 `truncatedAction`:它承接两件**都属于「插件看不见这次操作」**、但操作本身确实太大的事——撞收集护栏(`src=oversize`)与整条请求超送审上限;**参数没采集到不吃这个开关**(它永远直接拒绝,见上一条)。三者同走 `truncated-payload`,各带 `src` + `judgeReason` 证据、各打 `console.warn`、各写审计行(此前「撞护栏转人工」这条路径**审计行是缺的**,已补)。 - **排障信息补齐(审批记录/事件)**:① 撞收集护栏此前会被记成「工具没给参数」——护栏标记产生在 `pre-execute` 采集时,缓存只存了 `args`,闸门再也看不出 `over`;现在缓存存 `{args, over}`,护栏触发记 `err.payloadOversize oversize=collect>8388608` 并标 `src=oversize`,`denyReason` 新增 `payload-uncaptured` 档(与「内容超过送审上限」分开)。② 超上限**拒绝**路径此前只有 `argsOmitted`、没有任何原因字段,现在与转人工一样写 `judgeReason: err.judgePayloadOversize request=<实际>>预算`。③ `denyReason` 此前按「verdict 以 reject 结尾」判定,导致 `truncated-payload`/`missing-payload`(既可能拒绝也可能转人工)永远不写;改为拒绝类事件一律按 path 派生,并排除放行类(此前 allow 事件会带 `judge-call`)。④ 审批 tab 的「管道」一行现在同时显示闭集拒绝原因。 - **设置页文案与结构修正**(UI 走查,逐条都可复现):① 五处出厂文案里写着 markdown 粗体 `**…**`,而设置页是纯文本渲染,用户看到的是**字面星号**——改为普通措辞;② `set.unjudgeableSub`(中文)里混进了一整句英文、英文版结尾多一个 `**`;③ 同一段说明还在讲「这两类默认都转人工」和「没有可审的操作内容」——那是 `missingPayloadAction` 时代的事实,现在参数没采集到**永远直接拒绝且不可配**,已改写;④ 关键词折叠行与总览芯片复用了 `set.counts`(「三格合计 拒/人/允」)而关键词不是三格,新增 `set.kwCounts`;⑤ 「审批总览」的管道说明写成「关键词(拒绝>人工>允许)」,与真实顺序(允许在闸门**之后**)不符,且漏掉闸门这一格——文案改写并给总览补上闸门芯片(跳转 `data-ab-stage="unjudgeable"`);⑥ 「提示词语言怎么切」原本写在**审核表**卡片里,而语言影响的是**审核模型**卡片里的提示词——移到提示词下方,审核表卡片留 `set.criteriaLangNote`;⑦ 加载态用了成功色 `.ab-set-ok`(绿色「加载中…」),新增 `.ab-set-muted`;⑧ 清理死文案 `set.criteriaActionsHint` 与死 CSS `.ab-tag-neutral` / `.ab-set-input-ui` / `.ab-set-crit-fixed`;⑨ 审核表行的删除按钮补 `title` / `aria-label`(读屏原先只念一个 `✕`)。卡片标题「无法判定时」改为「看不见这次操作时」(卡片里唯一的开关本来就是「插件看不见这次操作」)。 - **设置页第二轮走查:草稿、可达性、密度、静默失败**。① **未保存的提示词不再被别的卡片冲掉**:快照刷新与本地编辑态彻底分开(`load({ reseed })`,只有首载 / 保存审核设置 / 恢复默认提示词 / 覆盖损坏配置才播种本地态),此前改一句提示词、再去点「模型转人工」的下拉(或让工具名输入框失焦),草稿会静默消失——那是一次 RPC 写入顺手把另一个卡片的编辑态重置了。`judgePromptLang` 每次刷新都跟随(它是服务端状态),中英草稿分开存,所以切语言不丢另一份;提示词右上角的标签同时补成三态(**未保存** / 已自定义 / 默认)——草稿现在活得比以前久,更需要看得见它还没落盘。② 可访问性:所有表单控件补可访问名(动作下拉、等级三格、`truncatedAction`、模型转人工三行、审核模型的三个下拉与两个数字输入);关键词文本从 `` 改成 `