@dnalec/dsh-auto-approve 0.2.0 → 0.2.1

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/client.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * dsh-auto-approve — 浏览器半:提示条、审批时间线、设置页。
3
- *
3
+ * 访问模式芯片上的盾+A 由本插件画在「自动审批」按钮/菜单上,不改 DSH。
4
4
  * 纯 JS:无 TS / JSX / import。React 用 createElement。
5
+
5
6
  * 网页文案:locales.mjs 的 zh/en,apply 时 ctx.locale.register。
6
7
  * Host API 走 connection.rpc.call('/api', 'dsh-auto-approve', …)(已鉴权),不用裸 HTTP。
7
8
  * 设置页折叠状态用 React state 控 details,避免 snapshot 刷新后合上。人工只走原网页审批框。
@@ -98,6 +99,7 @@ window.__ModuleLoader__.load({
98
99
  .ab-set-kw-text{flex:1 1 auto;min-width:0;cursor:text;color:var(--dsw-alias-label-primary);font:12px/18px var(--ds-font-family-code);word-break:break-all}
99
100
  .ab-set-textarea{box-sizing:border-box;width:100%;min-height:72px;resize:none;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:12px/18px inherit;border-radius:8px;outline:none;padding:6px 10px}
100
101
  .ab-set-textarea:focus{border-color:var(--dsw-alias-state-business-primary)}
102
+ .ab-set-textarea-prompt{min-height:180px;max-height:360px;resize:vertical;overflow:auto;font-family:var(--ds-font-family-code);white-space:pre-wrap}
101
103
  .ab-set-input-ui{font-family:inherit;width:100%}
102
104
  .ab-set-foot{justify-content:flex-end}
103
105
  .ab-set-list-crit{gap:8px}
@@ -111,6 +113,7 @@ window.__ModuleLoader__.load({
111
113
  .ab-set-empty{color:var(--dsw-alias-label-caption);margin:0;font-size:13px;line-height:20px;padding:4px 2px}
112
114
  .ab-set-tag{box-sizing:border-box;flex:none;height:18px;border-radius:9px;align-items:center;padding:0 8px;font-size:11px;line-height:18px;display:inline-flex}
113
115
  .ab-set-tag-blue{color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
116
+ .ab-set-tag-warn{color:var(--dsw-alias-state-warn-label);background:var(--dsw-alias-state-warn-tertiary)}
114
117
  .ab-set-steps{display:flex;flex-wrap:wrap;gap:6px}
115
118
  .ab-set-step{box-sizing:border-box;margin:0;border:1px solid var(--dsw-alias-border-l2);background:transparent;border-radius:10px;padding:8px 10px;min-width:108px;flex:1 1 108px;cursor:pointer;display:flex;flex-direction:column;gap:2px;align-items:flex-start;text-align:left;font:inherit;color:inherit}
116
119
  .ab-set-step:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -127,11 +130,20 @@ window.__ModuleLoader__.load({
127
130
  .ab-set-choice-on{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
128
131
  .ab-set-choice-t{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}
129
132
  .ab-set-choice-d{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
133
+ .ab-set-choice:disabled{opacity:.4;cursor:default}
134
+ .ab-set-flash{position:sticky;top:0;z-index:2;background:var(--dsw-alias-bg-layer-1);padding:4px 0}
135
+ .ab-access-glyph{display:inline-flex;flex:none;align-items:center;color:inherit}
136
+ .ab-access-glyph svg{width:14px;height:14px}
137
+ /* 菜单行里的图标色必须跟 DSH 自己的 .itemIcon 一致(label-tertiary):
138
+ 菜单按钮是 label-primary,注入的徽标会继承它,看起来比旁边三个图标更黑。
139
+ 触发器正好相反 —— .trigger 是 label-secondary、.triggerIcon 不设色,inherit 就对了。 */
140
+ [role="menuitem"] .ab-access-glyph{color:var(--dsw-alias-label-tertiary,currentColor)}
141
+ [role="menuitem"] .ab-access-glyph svg{width:16px;height:16px}
130
142
  `
131
143
 
132
144
  const NS = "dsh-auto-approve"
133
- const ZH = JSON.parse(String.raw`{"slot.notice":"自动放行提示","slot.history":"审批","slot.settings":"自动审批","action.reject":"拒绝","action.allow":"允许","action.human":"人工","criterion.deletion":"删除/覆盖不可再生数据","criterion.credential":"凭据/密钥/授权修改","criterion.remote":"远程系统/生产环境/数据库","criterion.system":"系统级路径/配置","criterion.bulk":"批量不可回补操作","criterion.approval-config":"自动审批配置","criterion.safe":"安全/常规可回补","criterion.other":"其他(拿不准)","sandbox.read-only":"只读","sandbox.workspace-write":"工作区可写","sandbox.danger-full-access":"全权限","source.web":"网页","verdict.keyword-allow":"关键词允许","verdict.keyword-reject":"关键词拒绝","verdict.keyword-human":"关键词转人工","verdict.criteria-reject":"审核表拒绝","verdict.criteria-allow":"审核表允许","verdict.criteria-human":"审核表转人工","verdict.judge-failed":"判定失败转人工","verdict.missing-payload":"缺少工具参数转人工","verdict.truncated-payload":"参数过长转人工","verdict.plugin-error":"插件异常转人工","verdict.human":"转人工","verdict.cancelled":"人工取消","verdict.unavailable":"审批不可用","path.keyword-reject":"关键词拒绝","path.keyword-allow":"关键词允许","path.keyword-human":"关键词转人工","path.criteria-reject":"审核表拒绝","path.criteria-allow":"审核表允许","path.criteria-human":"审核表转人工","path.judge-failed":"判定失败转人工","path.missing-payload":"缺少工具参数转人工","path.truncated-payload":"参数过长转人工","path.plugin-error":"插件异常转人工","notice.feedError":"审批提示暂时不可用","notice.feedErrorTag":"连接失败","notice.pendingTitle":"等待人工审批:{preview}","notice.pendingTag":"人工审批中","notice.webApproved":"人工审批通过:{preview}","notice.webApprovedTag":"人工审批通过","notice.rejectedTitle":"已拒绝:{preview}","notice.rejectedTag":"已拒绝","notice.cancelledTitle":"已取消人工审批:{preview}","notice.unavailableTitle":"审批不可用:{preview}","notice.autoTag":"自动放行 · {verdict}","notice.autoDefault":"自动放行","notice.rejectedDefault":"已拒绝","notice.hint":"提示","notice.close":"关闭","notice.feedLoadFailed":"审批提示加载失败,将自动重试","history.noSession":"未选择会话","history.loadFailed":"加载失败:{error}","history.loading":"加载中…","history.title":"审批","history.emptySub":"当前会话还没有审批记录","history.emptyHint":"自动放行与转人工都会出现在这里","history.sub":"最新在上 · 点开看命令与详情","history.tagAuto":"自动放行","history.tagPending":"转人工","history.tagAllow":"人工批准","history.tagReject":"人工拒绝","history.tagCancel":"人工取消","history.tagUnavailable":"审批不可用","history.unknownTool":"工具","detail.request":"请求","detail.audit":"审核","detail.command":"命令","detail.path":"路径","detail.description":"描述","detail.old":"原文","detail.new":"改成","detail.content":"写入内容","detail.code":"代码","detail.url":"URL","detail.query":"查询","detail.script":"脚本","detail.sql":"SQL","detail.prompt":"提示词","detail.cwd":"工作目录","detail.sandbox":"沙箱","detail.justification":"模型理由","detail.pipe":"管道","detail.keyword":"关键词","detail.judgeModel":"审核模型","detail.judgeCategory":"审核类别","detail.judgeAction":"执行动作","detail.judgeReason":"审核理由","detail.judgeError":"审核失败","detail.judgeRaw":"审核结果原文","detail.source":"来源","detail.judgeFailed":"未完成(解析失败/超时转人工,不执行「其他」的动作)","set.title":"自动审批","set.intro":"需要审批的行为由审核模型自动判定。允许或拒绝与在网页点按钮相同;拿不准则交给原来的人工审批框。","set.allowlistCorrupt":"规则文件损坏,当前是内存默认,普通保存不会覆盖磁盘。请修好 allowlist.json,或点「恢复默认」写回出厂规则。","set.pluginCorrupt":"插件配置损坏,拒绝保存以免清空审核模型。请修好 ~/.dsh/auto-approve/config.json,或","set.pluginCorruptOverwrite":"覆盖损坏配置","set.loading":"加载中…","set.loadFailed":"加载失败:{error}","set.resetConfirm":"再点一次确认恢复","set.presetMissingTitle":"权限预设","set.presetMissingSub":"插件启动时会写入 auto-approve。当前未检测到,可手动补写。","set.presetWrite":"写入权限预设","set.presetWriteNote":"写入后需重启 dsh web","set.presetWrote":"已写入,请重启 dsh web","set.modeTitle":"自动审批模式","set.modeWs":"工作区可写(推荐)","set.modeWsHint":"工作区内直接放行,越出工作区需审核。","set.modeRo":"只读","set.modeRoHint":"工作区内写操作也需审核。","set.save":"保存","set.hotOk":"已生效(热更新,无需重启)","set.modeSaved":"已写入。请重启 dsh web,并重新选择「自动审批」或开新会话","set.overview":"审批总览","set.overviewSub":"关键词(拒绝优先于人工优先于允许)→ 审核表由模型归类,程序按表执行。解析失败转人工。","set.stepKeywords":"关键词","set.stepCriteria":"审核表","set.stepJudge":"审核模型","set.counts":"拒 {reject} · 人 {human} · 允 {allow}","set.criteriaCount":"{n} 项","set.followDefault":"跟随默认","set.unconfigured":"未配置","set.kwSub":"匹配命令、路径和工作目录。拒绝/人工词也匹配工具名;允许词不匹配工具名,避免把 bash/write 整类放行。","set.empty":"暂无","set.presetTag":"预置","set.kwEditTitle":"点击修改","set.kwPlaceholder":"新关键词","set.add":"添加","set.resetKeywords":"恢复默认关键词","set.resetKeywordsOk":"已恢复默认关键词","set.confirm":"确认","set.delete":"删除","set.criteriaSub":"审核模型以此表作为审核标准。「其他」作为兜底选项不能删除。","set.labelPlaceholder":"标签","set.descPlaceholder":"说明(写入审核提示词)","set.resetCriteria":"恢复默认审核表({lang})","set.resetCriteriaOk":"已恢复为{lang}默认审核表","set.addCriterion":"添加审核项","set.criterionId":"id","set.judgeTitle":"审核模型","set.judgeSubLead":"独立于当前会话模型。空则跟随部署默认","set.judgeSubFallback":"({provider} / {model})","set.judgeSubNoFallback":"(当前没有默认可跟随)","set.judgeSubTail":"。没有可用路由时当次转人工,不会再猜一个模型。建议思考强度 off。","set.followProvider":"跟随默认提供方","set.followModel":"跟随默认模型","set.modelDefaultEffort":"模型默认","set.effortOff":"off(建议)","set.judgeTimeoutMs":"审核超时(ms):","set.judgeSaved":"审核模型已保存","set.judgeLang":"提示词语言","set.judgeLangZh":"中文","set.judgeLangEn":"English","set.judgeLangHint":"只改发给审核模型的框架语言,不会改当前审核表。恢复默认审核表时使用这里选的语言。","set.overwriteOk":"已覆盖损坏的插件配置","err.missingPayload":"工具参数不完整,转人工","err.missingPayloadUncaptured":"未捕获工具参数,转人工","err.truncatedPayload":"工具参数过长已截断,转人工(禁止按前缀自动放行)","err.pluginError":"插件判定异常,转人工","err.allowlistCorrupt":"规则文件损坏,拒绝覆盖。请先「恢复默认」写回出厂规则,或修好磁盘上的 allowlist.json","err.allowlistWrite":"写入 allowlist 失败","err.criterionNotFound":"未找到该审核项","err.criterionNeedId":"需要 id 或 label","err.criterionIdExists":"id 已存在","err.criterionOtherLocked":"「其他」不可删除","err.criteriaOp":"审核表请用添加 / 修改 / 删除 / 恢复默认","err.opMustSet":"{kind} 只能用修改操作","err.invalidNumber":"无效数值","err.keywordEmpty":"关键词不能为空","err.keywordNotFound":"未找到该关键词","err.keywordsOp":"关键词请用添加 / 修改 / 删除 / 恢复默认","err.valueEmpty":"值不能为空","err.unknownKind":"未知规则类型:{kind}","err.unknownOp":"未知操作:{op}","err.ruleNotFound":"未找到匹配的规则","err.badBody":"请求体不是 JSON","err.needSessionId":"需要 sessionId","err.pluginCorrupt":"插件配置损坏,拒绝覆盖。请修好磁盘文件,或点「覆盖损坏配置」","err.pluginWrite":"写入配置失败","err.unknownEndpoint":"未知接口:{endpoint}","err.internal":"内部错误:{error}","err.catalog":"无法列出模型:{error}","err.info":"无法读取模型信息:{error}","err.judgeUnconfigured":"未配置审核模型","err.judgeEffort":"思考强度 {effort} 不受支持","err.judgeTimeout":"审核超时({ms}ms)","err.judgeRetryTimeout":"审核重试超时({ms}ms)","err.judgeFailed":"审核失败","err.judgeCall":"审核模型调用失败:{error}","err.judgeEmpty":"审核模型输出为空","err.judgeParse":"审核模型输出无法解析","err.judgeUpstream":"审核模型路由失败:{error}","err.noPresetsKey":"permission 条目缺少 presets 键,请手动添加","err.preset":"写入预设失败:{error}","rpc.unavailable":"connection.rpc 不可用","rpc.failed":"RPC 失败"}`)
134
- const EN = JSON.parse(String.raw`{"slot.notice":"Auto-approve notice","slot.history":"Approvals","slot.settings":"Auto-approve","action.reject":"Reject","action.allow":"Allow","action.human":"Human","criterion.deletion":"Delete/overwrite irreplaceable data","criterion.credential":"Credentials/keys/auth changes","criterion.remote":"Remote/production/database","criterion.system":"System paths/config","criterion.bulk":"Bulk irreversible operations","criterion.approval-config":"Auto-approve configuration","criterion.safe":"Safe/routine reversible","criterion.other":"Other (unsure)","sandbox.read-only":"Read-only","sandbox.workspace-write":"Workspace write","sandbox.danger-full-access":"Full access","source.web":"Web","verdict.keyword-allow":"Keyword allow","verdict.keyword-reject":"Keyword reject","verdict.keyword-human":"Keyword → human","verdict.criteria-reject":"Criteria reject","verdict.criteria-allow":"Criteria allow","verdict.criteria-human":"Criteria → human","verdict.judge-failed":"Judge failed → human","verdict.missing-payload":"Missing tool args → human","verdict.truncated-payload":"Truncated args → human","verdict.plugin-error":"Plugin error → human","verdict.human":"To human","verdict.cancelled":"Human cancel","verdict.unavailable":"Unavailable","path.keyword-reject":"Keyword reject","path.keyword-allow":"Keyword allow","path.keyword-human":"Keyword → human","path.criteria-reject":"Criteria reject","path.criteria-allow":"Criteria allow","path.criteria-human":"Criteria → human","path.judge-failed":"Judge failed → human","path.missing-payload":"Missing tool args → human","path.truncated-payload":"Truncated args → human","path.plugin-error":"Plugin error → human","notice.feedError":"Approval notices unavailable","notice.feedErrorTag":"Connection failed","notice.pendingTitle":"Waiting for human: {preview}","notice.pendingTag":"Human review","notice.webApproved":"Approved: {preview}","notice.webApprovedTag":"Approved","notice.rejectedTitle":"Rejected: {preview}","notice.rejectedTag":"Rejected","notice.cancelledTitle":"Cancelled: {preview}","notice.unavailableTitle":"Unavailable: {preview}","notice.autoTag":"Auto-allowed · {verdict}","notice.autoDefault":"Auto-allowed","notice.rejectedDefault":"Rejected","notice.hint":"Notice","notice.close":"Dismiss","notice.feedLoadFailed":"Approval notices failed to load; retrying","history.noSession":"No session selected","history.loadFailed":"Failed to load: {error}","history.loading":"Loading…","history.title":"Approvals","history.emptySub":"This session has no approval records yet","history.emptyHint":"Auto-allows and human escalations appear here","history.sub":"Newest first · expand for command and details","history.tagAuto":"Auto-allowed","history.tagPending":"To human","history.tagAllow":"Human allow","history.tagReject":"Human reject","history.tagCancel":"Human cancel","history.tagUnavailable":"Unavailable","history.unknownTool":"Tool","detail.request":"Request","detail.audit":"Review","detail.command":"Command","detail.path":"Path","detail.description":"Description","detail.old":"Original","detail.new":"Replacement","detail.content":"Write contents","detail.code":"Code","detail.url":"URL","detail.query":"Query","detail.script":"Script","detail.sql":"SQL","detail.prompt":"Prompt","detail.cwd":"Working directory","detail.sandbox":"Sandbox","detail.justification":"Model justification","detail.pipe":"Pipeline","detail.keyword":"Keyword","detail.judgeModel":"Judge model","detail.judgeCategory":"Category","detail.judgeAction":"Action","detail.judgeReason":"Judge reason","detail.judgeError":"Judge error","detail.judgeRaw":"Judge raw output","detail.source":"Source","detail.judgeFailed":"Incomplete (parse failure/timeout → human; does not run Other)","set.title":"Auto-approve","set.intro":"Actions that need approval are judged automatically. Allow and reject match clicking the Web buttons; uncertainty goes to the original human dialog.","set.allowlistCorrupt":"The rules file is corrupt. This process is using in-memory defaults and will not overwrite the disk. Repair allowlist.json, or Restore defaults to write shipped rules.","set.pluginCorrupt":"Plugin config is corrupt; saves are refused so the judge model is not wiped. Repair ~/.dsh/auto-approve/config.json, or ","set.pluginCorruptOverwrite":"Overwrite corrupt config","set.loading":"Loading…","set.loadFailed":"Failed to load: {error}","set.resetConfirm":"Click again to confirm restore","set.presetMissingTitle":"Permission preset","set.presetMissingSub":"Startup writes auto-approve. It was not detected; you can write it now.","set.presetWrite":"Write permission preset","set.presetWriteNote":"Restart dsh web after writing","set.presetWrote":"Written. Restart dsh web","set.modeTitle":"Auto-approve mode","set.modeWs":"Workspace write (recommended)","set.modeWsHint":"In-workspace writes skip approval; outside the workspace still goes through the judge.","set.modeRo":"Read-only","set.modeRoHint":"In-workspace writes also go through the judge.","set.save":"Save","set.hotOk":"Applied (live; no restart)","set.modeSaved":"Written. Restart dsh web and re-select Auto-approve or start a new session","set.overview":"Approval overview","set.overviewSub":"Keywords (reject > human > allow) → the judge classifies; the table decides. Parse failure → human.","set.stepKeywords":"Keywords","set.stepCriteria":"Criteria","set.stepJudge":"Judge model","set.counts":"rej {reject} · hum {human} · all {allow}","set.criteriaCount":"{n} rows","set.followDefault":"Follow default","set.unconfigured":"Not configured","set.kwSub":"Match command, path, and workdir. Reject/human also match the tool name; allow keywords do not, so bash/write is not allowed as a class.","set.empty":"None","set.presetTag":"Shipped","set.kwEditTitle":"Click to edit","set.kwPlaceholder":"New keyword","set.add":"Add","set.resetKeywords":"Restore default keywords","set.resetKeywordsOk":"Default keywords restored","set.confirm":"Confirm","set.delete":"Delete","set.criteriaSub":"The judge uses this table. Other cannot be deleted.","set.labelPlaceholder":"Label","set.descPlaceholder":"Description (goes into the judge prompt)","set.resetCriteria":"Restore default criteria ({lang})","set.resetCriteriaOk":"Restored {lang} default criteria","set.addCriterion":"Add criterion","set.criterionId":"id","set.judgeTitle":"Judge model","set.judgeSubLead":"Independent of the session model. Empty follows the deployment default","set.judgeSubFallback":" ({provider} / {model})","set.judgeSubNoFallback":" (no default to follow)","set.judgeSubTail":". With no usable route, that request goes to a human; the plugin will not guess a model. Prefer effort off.","set.followProvider":"Follow default provider","set.followModel":"Follow default model","set.modelDefaultEffort":"Model default","set.effortOff":"off (recommended)","set.judgeTimeoutMs":"Judge timeout (ms):","set.judgeSaved":"Judge model saved","set.judgeLang":"Prompt language","set.judgeLangZh":"Chinese","set.judgeLangEn":"English","set.judgeLangHint":"Changes only the judge prompt framework, not the current criteria table. Restore default criteria uses this language.","set.overwriteOk":"Corrupt plugin config overwritten","err.missingPayload":"Tool arguments are incomplete; sending to human","err.missingPayloadUncaptured":"Tool arguments were not captured; sending to human","err.truncatedPayload":"Tool arguments were truncated; sending to human (no prefix auto-allow)","err.pluginError":"Plugin error; sending to human","err.allowlistCorrupt":"Rules file is corrupt; refusing to overwrite. Restore defaults or fix allowlist.json on disk.","err.allowlistWrite":"Failed to write allowlist","err.criterionNotFound":"Criterion not found","err.criterionNeedId":"id or label is required","err.criterionIdExists":"id already exists","err.criterionOtherLocked":"“Other” cannot be deleted","err.criteriaOp":"Criteria accept add / set / remove / reset","err.opMustSet":"{kind} only accepts set","err.invalidNumber":"Invalid number","err.keywordEmpty":"Keyword cannot be empty","err.keywordNotFound":"Keyword not found","err.keywordsOp":"Keywords accept add / set / remove / reset","err.valueEmpty":"Value cannot be empty","err.unknownKind":"Unknown rule kind: {kind}","err.unknownOp":"Unknown operation: {op}","err.ruleNotFound":"No matching rule","err.badBody":"Request body is not JSON","err.needSessionId":"sessionId is required","err.pluginCorrupt":"Plugin config is corrupt; refusing to overwrite. Fix the file on disk, or overwrite the corrupt config.","err.pluginWrite":"Failed to write plugin config","err.unknownEndpoint":"Unknown endpoint: {endpoint}","err.internal":"Internal error: {error}","err.catalog":"Could not list models: {error}","err.info":"Could not load model info: {error}","err.judgeUnconfigured":"Judge model is not configured","err.judgeEffort":"reasoningEffort {effort} is not supported","err.judgeTimeout":"Judge timed out ({ms}ms)","err.judgeRetryTimeout":"Judge retry timed out ({ms}ms)","err.judgeFailed":"Judge failed","err.judgeCall":"Judge call failed: {error}","err.judgeEmpty":"Judge output was empty","err.judgeParse":"Judge output could not be parsed","err.judgeUpstream":"Judge route failed: {error}","err.noPresetsKey":"The permission entry has no presets key; add it manually","err.preset":"Failed to write preset: {error}","rpc.unavailable":"connection.rpc is unavailable","rpc.failed":"RPC failed"}`)
145
+ const ZH = JSON.parse(String.raw`{"slot.notice":"自动放行提示","slot.history":"审批","slot.settings":"自动审批","action.reject":"拒绝","action.allow":"允许","action.human":"人工","criterion.deletion":"删除/覆盖不可再生数据","criterion.credential":"凭据/密钥/授权修改","criterion.remote":"远程系统/生产环境/数据库","criterion.system":"系统级路径/配置","criterion.bulk":"批量不可回补操作","criterion.approval-config":"自动审批配置","criterion.safe":"安全/常规可回补","criterion.other":"其他(拿不准)","sandbox.read-only":"只读","sandbox.workspace-write":"工作区可写","sandbox.danger-full-access":"全权限","source.web":"网页","verdict.keyword-allow":"关键词允许","verdict.keyword-reject":"关键词拒绝","verdict.keyword-human":"关键词转人工","verdict.criteria-reject":"审核表拒绝","verdict.criteria-allow":"审核表允许","verdict.criteria-human":"审核表转人工","verdict.judge-failed":"判定失败转人工","verdict.missing-payload":"缺少工具参数转人工","verdict.truncated-payload":"参数过长转人工","verdict.plugin-error":"插件异常转人工","verdict.human":"转人工","verdict.cancelled":"人工取消","verdict.unavailable":"审批不可用","path.keyword-reject":"关键词拒绝","path.keyword-allow":"关键词允许","path.keyword-human":"关键词转人工","path.criteria-reject":"审核表拒绝","path.criteria-allow":"审核表允许","path.criteria-human":"审核表转人工","path.judge-failed":"判定失败转人工","path.missing-payload":"缺少工具参数转人工","path.truncated-payload":"参数过长转人工","path.plugin-error":"插件异常转人工","notice.feedError":"审批提示暂时不可用","notice.feedErrorTag":"连接失败","notice.pendingTitle":"等待人工审批:{preview}","notice.pendingTag":"人工审批中","notice.webApproved":"人工审批通过:{preview}","notice.webApprovedTag":"人工审批通过","notice.rejectedTitle":"已拒绝:{preview}","notice.rejectedTag":"已拒绝","notice.cancelledTitle":"已取消人工审批:{preview}","notice.unavailableTitle":"审批不可用:{preview}","notice.autoTag":"自动放行 · {verdict}","notice.autoDefault":"自动放行","notice.rejectedDefault":"已拒绝","notice.hint":"提示","notice.close":"关闭","notice.feedLoadFailed":"审批提示加载失败,将自动重试","history.noSession":"未选择会话","history.loadFailed":"加载失败:{error}","history.loading":"加载中…","history.title":"审批","history.emptySub":"当前会话还没有审批记录","history.emptyHint":"自动放行与转人工都会出现在这里","history.sub":"最新在上 · 点开看命令与详情","history.tagAuto":"自动放行","history.tagPending":"转人工","history.tagAllow":"人工批准","history.tagReject":"人工拒绝","history.tagCancel":"人工取消","history.tagUnavailable":"审批不可用","history.unknownTool":"工具","detail.request":"请求","detail.audit":"审核","detail.command":"命令","detail.path":"路径","detail.description":"描述","detail.old":"原文","detail.new":"改成","detail.content":"写入内容","detail.code":"代码","detail.url":"URL","detail.query":"查询","detail.script":"脚本","detail.sql":"SQL","detail.prompt":"提示词","detail.input":"输入","detail.text":"文本","detail.body":"正文","detail.message":"消息","detail.pattern":"模式","detail.selector":"选择器","detail.cwd":"工作目录","detail.workdir":"命令工作目录","detail.empty":"(空)","detail.sandbox":"沙箱","detail.justification":"模型理由","detail.pipe":"管道","detail.keyword":"关键词","detail.judgeModel":"审核模型","detail.judgeCategory":"审核类别","detail.judgeAction":"执行动作","detail.judgeReason":"审核理由","detail.judgeError":"审核失败","detail.judgeRaw":"审核结果原文","detail.source":"来源","detail.judgeFailed":"未完成(解析失败/超时转人工,不执行「其他」的动作)","set.title":"自动审批","set.intro":"需要审批的行为由审核模型自动判定。允许或拒绝与在网页点按钮相同;拿不准则交给原来的人工审批框。","set.allowlistCorrupt":"规则文件损坏,当前是内存默认,普通保存不会覆盖磁盘。请修好 allowlist.json,或点「恢复默认」写回出厂规则。","set.pluginCorrupt":"插件配置损坏,拒绝保存以免清空审核模型。请修好 ~/.dsh/auto-approve/config.json,或","set.pluginCorruptOverwrite":"覆盖损坏配置","set.loading":"加载中…","set.loadFailed":"加载失败:{error}","set.resetConfirm":"再点一次确认恢复","set.presetMissingTitle":"权限预设","set.driftTitle":"权限预设表需要更新","set.driftBody":"DSH 出厂权限预设表多了 {keys}。本 profile 的 permission 行由本插件写入,patch 会整块替换 config,所以不会自动出现这些预设;请更新插件或手工合并该行。","set.presetMissingSub":"插件启动时会写入 auto-approve。当前未检测到,可手动补写。","set.presetWrite":"写入权限预设","set.presetWriteNote":"写入后需重启 dsh web","set.presetWrote":"已写入,请重启 dsh web","set.modeTitle":"自动审批模式","set.modeWs":"工作区可写(推荐)","set.modeWsHint":"工作区内直接放行,越出工作区需审核。","set.modeRo":"只读","set.modeRoHint":"工作区内写操作也需审核。","set.modeHint":"点选即写入。改完需重启 dsh web,并重新选择「自动审批」或开新会话。","set.save":"保存","set.hotOk":"已生效(热更新,无需重启)","set.modeSaved":"已写入。请重启 dsh web,并重新选择「自动审批」或开新会话","set.overview":"审批总览","set.overviewSub":"关键词(拒绝优先于人工优先于允许)→ 审核表由模型归类,程序按表执行。解析失败转人工。","set.stepKeywords":"关键词","set.stepCriteria":"审核表","set.stepJudge":"审核模型","set.counts":"拒 {reject} · 人 {human} · 允 {allow}","set.criteriaCount":"{n} 项","set.followDefault":"跟随默认","set.unconfigured":"未配置","set.kwSub":"匹配命令、路径和工作目录。拒绝/人工词也匹配工具名;允许词不匹配工具名,避免把 bash/write 整类放行。","set.empty":"暂无","set.presetTag":"预置","set.kwEditTitle":"点击修改","set.kwPlaceholder":"新关键词","set.add":"添加","set.resetKeywords":"恢复默认关键词","set.resetKeywordsOk":"已恢复默认关键词","set.confirm":"确认","set.delete":"删除","set.criteriaSub":"审核模型以此表作为审核标准。「其他」作为兜底选项不能删除。","set.labelPlaceholder":"标签","set.descPlaceholder":"说明(写入审核提示词)","set.resetCriteria":"恢复默认审核表({lang})","set.resetCriteriaOk":"已恢复为{lang}默认审核表","set.addCriterion":"添加审核项","set.criterionId":"id","set.judgeTitle":"审核模型","set.judgeSubLead":"独立于当前会话模型。空则跟随部署默认","set.judgeSubFallback":"({provider} / {model})","set.judgeSubNoFallback":"(当前没有默认可跟随)","set.judgeSubTail":"。没有可用路由时当次转人工,不会再猜一个模型。建议思考强度 off。","set.followProvider":"跟随默认提供方","set.followModel":"跟随默认模型","set.modelDefaultEffort":"模型默认","set.effortOff":"off(建议)","set.judgeTimeoutMs":"审核超时(ms):","set.judgeSaved":"审核设置已保存","set.judgeLang":"提示词语言","set.judgeLangZh":"中文","set.judgeLangEn":"English","set.judgeLangHint":"切换发给审核模型的框架语言与卡片标签,不会改当前审核表。下面的提示词按此语言编辑;恢复默认审核表也用这里选的语言。","set.judgePrompt":"审核提示词","set.judgePromptHint":"按所选语言内置默认提示词。用 {{criteria}} 插入当前审核表;删掉占位符时会把审核表附在末尾。与审核模型、超时一起保存。","set.judgePromptCustom":"已自定义","set.judgePromptDefault":"默认","set.resetJudgePrompt":"恢复默认提示词","set.resetJudgePromptOk":"已恢复默认提示词","set.overwriteOk":"已覆盖损坏的插件配置","err.missingPayload":"工具参数不完整,转人工","err.missingPayloadUncaptured":"未捕获工具参数,转人工","err.truncatedPayload":"工具参数过长已截断,转人工(禁止按前缀自动放行)","err.pluginError":"插件判定异常,转人工","err.allowlistCorrupt":"规则文件损坏,拒绝覆盖。请先「恢复默认」写回出厂规则,或修好磁盘上的 allowlist.json","err.allowlistWrite":"写入 allowlist 失败","err.criterionNotFound":"未找到该审核项","err.criterionNeedId":"需要 id 或 label","err.criterionIdExists":"id 已存在","err.criterionOtherLocked":"「其他」不可删除","err.criteriaOp":"审核表请用添加 / 修改 / 删除 / 恢复默认","err.opMustSet":"{kind} 只能用修改操作","err.invalidNumber":"无效数值","err.keywordEmpty":"关键词不能为空","err.keywordNotFound":"未找到该关键词","err.keywordsOp":"关键词请用添加 / 修改 / 删除 / 恢复默认","err.valueEmpty":"值不能为空","err.unknownKind":"未知规则类型:{kind}","err.unknownOp":"未知操作:{op}","err.ruleNotFound":"未找到匹配的规则","err.badBody":"请求体不是 JSON","err.needSessionId":"需要 sessionId","err.pluginCorrupt":"插件配置损坏,拒绝覆盖。请修好磁盘文件,或点「覆盖损坏配置」","err.pluginWrite":"写入配置失败","err.unknownEndpoint":"未知接口:{endpoint}","err.internal":"内部错误:{error}","err.catalog":"无法列出模型:{error}","err.info":"无法读取模型信息:{error}","err.judgeUnconfigured":"未配置审核模型","err.judgeEffort":"思考强度 {effort} 不受支持","err.judgeTimeout":"审核超时({ms}ms)","err.judgeRetryTimeout":"审核重试超时({ms}ms)","err.judgeFailed":"审核失败","err.judgeCall":"审核模型调用失败:{error}","err.judgeEmpty":"审核模型输出为空","err.judgeParse":"审核模型输出无法解析","err.judgeUpstream":"审核模型路由失败:{error}","err.noPresetsKey":"permission 条目缺少 presets 键,请手动添加","err.preset":"写入预设失败:{error}","err.presetSandboxMissing":"预设里没有 sandbox 行,无法写入沙箱模式;请检查 profile 的 cordis.patch.yml","rpc.unavailable":"connection.rpc 不可用","rpc.failed":"RPC 失败"}`)
146
+ const EN = JSON.parse(String.raw`{"slot.notice":"Auto-approve notice","slot.history":"Approvals","slot.settings":"Auto-approve","action.reject":"Reject","action.allow":"Allow","action.human":"Human","criterion.deletion":"Delete/overwrite irreplaceable data","criterion.credential":"Credentials/keys/auth changes","criterion.remote":"Remote/production/database","criterion.system":"System paths/config","criterion.bulk":"Bulk irreversible operations","criterion.approval-config":"Auto-approve configuration","criterion.safe":"Safe/routine reversible","criterion.other":"Other (unsure)","sandbox.read-only":"Read-only","sandbox.workspace-write":"Workspace write","sandbox.danger-full-access":"Full access","source.web":"Web","verdict.keyword-allow":"Keyword allow","verdict.keyword-reject":"Keyword reject","verdict.keyword-human":"Keyword → human","verdict.criteria-reject":"Criteria reject","verdict.criteria-allow":"Criteria allow","verdict.criteria-human":"Criteria → human","verdict.judge-failed":"Judge failed → human","verdict.missing-payload":"Missing tool args → human","verdict.truncated-payload":"Truncated args → human","verdict.plugin-error":"Plugin error → human","verdict.human":"To human","verdict.cancelled":"Human cancel","verdict.unavailable":"Unavailable","path.keyword-reject":"Keyword reject","path.keyword-allow":"Keyword allow","path.keyword-human":"Keyword → human","path.criteria-reject":"Criteria reject","path.criteria-allow":"Criteria allow","path.criteria-human":"Criteria → human","path.judge-failed":"Judge failed → human","path.missing-payload":"Missing tool args → human","path.truncated-payload":"Truncated args → human","path.plugin-error":"Plugin error → human","notice.feedError":"Approval notices unavailable","notice.feedErrorTag":"Connection failed","notice.pendingTitle":"Waiting for human: {preview}","notice.pendingTag":"Human review","notice.webApproved":"Approved: {preview}","notice.webApprovedTag":"Approved","notice.rejectedTitle":"Rejected: {preview}","notice.rejectedTag":"Rejected","notice.cancelledTitle":"Cancelled: {preview}","notice.unavailableTitle":"Unavailable: {preview}","notice.autoTag":"Auto-allowed · {verdict}","notice.autoDefault":"Auto-allowed","notice.rejectedDefault":"Rejected","notice.hint":"Notice","notice.close":"Dismiss","notice.feedLoadFailed":"Approval notices failed to load; retrying","history.noSession":"No session selected","history.loadFailed":"Failed to load: {error}","history.loading":"Loading…","history.title":"Approvals","history.emptySub":"This session has no approval records yet","history.emptyHint":"Auto-allows and human escalations appear here","history.sub":"Newest first · expand for command and details","history.tagAuto":"Auto-allowed","history.tagPending":"To human","history.tagAllow":"Human allow","history.tagReject":"Human reject","history.tagCancel":"Human cancel","history.tagUnavailable":"Unavailable","history.unknownTool":"Tool","detail.request":"Request","detail.audit":"Review","detail.command":"Command","detail.path":"Path","detail.description":"Description","detail.old":"Original","detail.new":"Replacement","detail.content":"Write contents","detail.code":"Code","detail.url":"URL","detail.query":"Query","detail.script":"Script","detail.sql":"SQL","detail.prompt":"Prompt","detail.input":"Input","detail.text":"Text","detail.body":"Body","detail.message":"Message","detail.pattern":"Pattern","detail.selector":"Selector","detail.cwd":"Working directory","detail.workdir":"Command working directory","detail.empty":"(empty)","detail.sandbox":"Sandbox","detail.justification":"Model justification","detail.pipe":"Pipeline","detail.keyword":"Keyword","detail.judgeModel":"Judge model","detail.judgeCategory":"Category","detail.judgeAction":"Action","detail.judgeReason":"Judge reason","detail.judgeError":"Judge error","detail.judgeRaw":"Judge raw output","detail.source":"Source","detail.judgeFailed":"Incomplete (parse failure/timeout → human; does not run Other)","set.title":"Auto-approve","set.intro":"Actions that need approval are judged automatically. Allow and reject match clicking the Web buttons; uncertainty goes to the original human dialog.","set.allowlistCorrupt":"The rules file is corrupt. This process is using in-memory defaults and will not overwrite the disk. Repair allowlist.json, or Restore defaults to write shipped rules.","set.pluginCorrupt":"Plugin config is corrupt; saves are refused so the judge model is not wiped. Repair ~/.dsh/auto-approve/config.json, or ","set.pluginCorruptOverwrite":"Overwrite corrupt config","set.loading":"Loading…","set.loadFailed":"Failed to load: {error}","set.resetConfirm":"Click again to confirm restore","set.presetMissingTitle":"Permission preset","set.driftTitle":"Permission preset table needs updating","set.driftBody":"The shipped permission table has new presets: {keys}. This profile's permission row is written by this plugin, and a patch replaces that config wholesale, so they will not appear automatically. Update the plugin or merge that row by hand.","set.presetMissingSub":"Startup writes auto-approve. It was not detected; you can write it now.","set.presetWrite":"Write permission preset","set.presetWriteNote":"Restart dsh web after writing","set.presetWrote":"Written. Restart dsh web","set.modeTitle":"Auto-approve mode","set.modeWs":"Workspace write (recommended)","set.modeWsHint":"In-workspace writes skip approval; outside the workspace still goes through the judge.","set.modeRo":"Read-only","set.modeRoHint":"In-workspace writes also go through the judge.","set.modeHint":"Clicking a mode writes it. Then restart dsh web and re-select Auto-approve or start a new session.","set.save":"Save","set.hotOk":"Applied (live; no restart)","set.modeSaved":"Written. Restart dsh web and re-select Auto-approve or start a new session","set.overview":"Approval overview","set.overviewSub":"Keywords (reject > human > allow) → the judge classifies; the table decides. Parse failure → human.","set.stepKeywords":"Keywords","set.stepCriteria":"Criteria","set.stepJudge":"Judge model","set.counts":"rej {reject} · hum {human} · all {allow}","set.criteriaCount":"{n} rows","set.followDefault":"Follow default","set.unconfigured":"Not configured","set.kwSub":"Match command, path, and workdir. Reject/human also match the tool name; allow keywords do not, so bash/write is not allowed as a class.","set.empty":"None","set.presetTag":"Shipped","set.kwEditTitle":"Click to edit","set.kwPlaceholder":"New keyword","set.add":"Add","set.resetKeywords":"Restore default keywords","set.resetKeywordsOk":"Default keywords restored","set.confirm":"Confirm","set.delete":"Delete","set.criteriaSub":"The judge uses this table. Other cannot be deleted.","set.labelPlaceholder":"Label","set.descPlaceholder":"Description (goes into the judge prompt)","set.resetCriteria":"Restore default criteria ({lang})","set.resetCriteriaOk":"Restored {lang} default criteria","set.addCriterion":"Add criterion","set.criterionId":"id","set.judgeTitle":"Judge model","set.judgeSubLead":"Independent of the session model. Empty follows the deployment default","set.judgeSubFallback":" ({provider} / {model})","set.judgeSubNoFallback":" (no default to follow)","set.judgeSubTail":". With no usable route, that request goes to a human; the plugin will not guess a model. Prefer effort off.","set.followProvider":"Follow default provider","set.followModel":"Follow default model","set.modelDefaultEffort":"Model default","set.effortOff":"off (recommended)","set.judgeTimeoutMs":"Judge timeout (ms):","set.judgeSaved":"Judge settings saved","set.judgeLang":"Prompt language","set.judgeLangZh":"Chinese","set.judgeLangEn":"English","set.judgeLangHint":"Switches the judge prompt framework and card labels, not the current criteria table. The prompt below is edited per language; restore default criteria also uses this language.","set.judgePrompt":"Judge prompt","set.judgePromptHint":"Built-in default for the selected language. Use {{criteria}} to insert the current criteria table; if the placeholder is removed, the table is appended. Saved with the judge model and timeout.","set.judgePromptCustom":"Custom","set.judgePromptDefault":"Default","set.resetJudgePrompt":"Restore default prompt","set.resetJudgePromptOk":"Default prompt restored","set.overwriteOk":"Corrupt plugin config overwritten","err.missingPayload":"Tool arguments are incomplete; sending to human","err.missingPayloadUncaptured":"Tool arguments were not captured; sending to human","err.truncatedPayload":"Tool arguments were truncated; sending to human (no prefix auto-allow)","err.pluginError":"Plugin error; sending to human","err.allowlistCorrupt":"Rules file is corrupt; refusing to overwrite. Restore defaults or fix allowlist.json on disk.","err.allowlistWrite":"Failed to write allowlist","err.criterionNotFound":"Criterion not found","err.criterionNeedId":"id or label is required","err.criterionIdExists":"id already exists","err.criterionOtherLocked":"“Other” cannot be deleted","err.criteriaOp":"Criteria accept add / set / remove / reset","err.opMustSet":"{kind} only accepts set","err.invalidNumber":"Invalid number","err.keywordEmpty":"Keyword cannot be empty","err.keywordNotFound":"Keyword not found","err.keywordsOp":"Keywords accept add / set / remove / reset","err.valueEmpty":"Value cannot be empty","err.unknownKind":"Unknown rule kind: {kind}","err.unknownOp":"Unknown operation: {op}","err.ruleNotFound":"No matching rule","err.badBody":"Request body is not JSON","err.needSessionId":"sessionId is required","err.pluginCorrupt":"Plugin config is corrupt; refusing to overwrite. Fix the file on disk, or overwrite the corrupt config.","err.pluginWrite":"Failed to write plugin config","err.unknownEndpoint":"Unknown endpoint: {endpoint}","err.internal":"Internal error: {error}","err.catalog":"Could not list models: {error}","err.info":"Could not load model info: {error}","err.judgeUnconfigured":"Judge model is not configured","err.judgeEffort":"reasoningEffort {effort} is not supported","err.judgeTimeout":"Judge timed out ({ms}ms)","err.judgeRetryTimeout":"Judge retry timed out ({ms}ms)","err.judgeFailed":"Judge failed","err.judgeCall":"Judge call failed: {error}","err.judgeEmpty":"Judge output was empty","err.judgeParse":"Judge output could not be parsed","err.judgeUpstream":"Judge route failed: {error}","err.noPresetsKey":"The permission entry has no presets key; add it manually","err.preset":"Failed to write preset: {error}","err.presetSandboxMissing":"The preset has no sandbox line; cannot set the sandbox mode. Check the profile cordis.patch.yml","rpc.unavailable":"connection.rpc is unavailable","rpc.failed":"RPC failed"}`)
135
147
 
136
148
  function fillLocale(template, params) {
137
149
  if (!params) return template
@@ -197,7 +209,7 @@ window.__ModuleLoader__.load({
197
209
  }
198
210
 
199
211
  function hasVal(value) {
200
- return value !== undefined && value !== null && value !== ''
212
+ return value !== undefined && value !== null
201
213
  }
202
214
 
203
215
  function isLongVal(value) {
@@ -205,24 +217,26 @@ window.__ModuleLoader__.load({
205
217
  return s.length > 72 || s.indexOf('\n') !== -1
206
218
  }
207
219
 
208
- function Detail(label, value) {
220
+ function Detail(label, value, emptyText) {
209
221
  if (!hasVal(value)) return null
210
- const long = isLongVal(value)
222
+ if (value === '' && !emptyText) return null
223
+ const shown = value === '' ? emptyText : String(value)
224
+ const long = isLongVal(shown)
211
225
  return [
212
226
  React.createElement('div', { className: 'ab-detail-k', key: label + '-k' }, label),
213
227
  React.createElement(long ? 'pre' : 'div', {
214
228
  className: 'ab-detail-v' + (long ? ' ab-detail-v-box' : ''),
215
229
  key: label + '-v',
216
- }, String(value)),
230
+ }, shown),
217
231
  ]
218
232
  }
219
233
 
220
- function DetailSection(title, items) {
234
+ function DetailSection(title, items, emptyText) {
221
235
  const kids = []
222
236
  for (let i = 0; i < items.length; i++) {
223
237
  const row = items[i]
224
238
  if (!row) continue
225
- const node = Detail(row[0], row[1])
239
+ const node = Detail(row[0], row[1], emptyText)
226
240
  if (node) kids.push(node[0], node[1])
227
241
  }
228
242
  if (!kids.length) return null
@@ -239,11 +253,13 @@ window.__ModuleLoader__.load({
239
253
  }
240
254
 
241
255
  function fmtTime(iso) {
256
+ if (!iso) return ''
242
257
  try {
243
258
  const d = new Date(iso)
259
+ if (Number.isNaN(d.getTime())) return ''
244
260
  const p = (n) => String(n).padStart(2, '0')
245
261
  return p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
246
- } catch (e) { return String(iso || '') }
262
+ } catch (e) { return '' }
247
263
  }
248
264
 
249
265
  function GlyphCheck() {
@@ -306,7 +322,7 @@ window.__ModuleLoader__.load({
306
322
  }).then(function (r) {
307
323
  if (!r || r.ok === false) {
308
324
  const err = r && r.error
309
- throw new Error(formatErr(tr, err && err.code, err && (err.details || {})) || tr('rpc.failed'))
325
+ throw new Error(formatErr(tr, err && err.code, err && (err.details || {})) || (err && err.message) || tr('rpc.failed'))
310
326
  }
311
327
  return r.value
312
328
  })
@@ -458,6 +474,9 @@ window.__ModuleLoader__.load({
458
474
  React.useEffect(function () {
459
475
  let alive = true
460
476
  let timer = null
477
+ setEvents(null)
478
+ setError(null)
479
+ setOpenId(null)
461
480
  if (!sessionId || !rpc) return
462
481
  const load = function () {
463
482
  rpc('events', { sessionId: sessionId, since: 0 }).then(function (data) {
@@ -553,7 +572,7 @@ window.__ModuleLoader__.load({
553
572
  ? React.createElement('div', { className: 'ab-details' },
554
573
  DetailSection(t('detail.request'), [
555
574
  [t('detail.command'), args.command],
556
- [t('detail.path'), args.file_path || args.path],
575
+ [t('detail.path'), args.file_path != null ? args.file_path : args.path],
557
576
  [t('detail.description'), args.description],
558
577
  [t('detail.old'), args.old_string],
559
578
  [t('detail.new'), args.new_string],
@@ -564,10 +583,17 @@ window.__ModuleLoader__.load({
564
583
  [t('detail.script'), args.script],
565
584
  [t('detail.sql'), args.sql],
566
585
  [t('detail.prompt'), args.prompt],
586
+ [t('detail.input'), args.input],
587
+ [t('detail.text'), args.text],
588
+ [t('detail.body'), args.body],
589
+ [t('detail.message'), args.message],
590
+ [t('detail.pattern'), args.pattern],
591
+ [t('detail.selector'), args.selector],
567
592
  [t('detail.cwd'), ev.cwd],
593
+ [t('detail.workdir'), args.workdir && args.workdir !== ev.cwd ? args.workdir : undefined],
568
594
  [t('detail.sandbox'), sandboxLabel(t, ev.mode)],
569
595
  [t('detail.justification'), ev.justification || ev.reason],
570
- ]),
596
+ ], t('detail.empty')),
571
597
  DetailSection(t('detail.audit'), [
572
598
  [t('detail.pipe'), pathLabel(t, ev.path) || ev.path],
573
599
  [t('detail.keyword'), ev.keyword],
@@ -609,11 +635,13 @@ window.__ModuleLoader__.load({
609
635
  const [judgeModel, setJudgeModel] = React.useState('')
610
636
  const [judgeEffort, setJudgeEffort] = React.useState('')
611
637
  const [judgePromptLang, setJudgePromptLang] = React.useState('zh')
638
+ const [judgePromptDrafts, setJudgePromptDrafts] = React.useState({ zh: '', en: '' })
612
639
  const [presetSandbox, setPresetSandbox] = React.useState('workspace-write')
613
640
  const [foldOpen, setFoldOpen] = React.useState({ keywords: false, criteria: false })
614
641
  const [kwEdit, setKwEdit] = React.useState(null)
615
642
  const [resetArmed, setResetArmed] = React.useState(null)
616
643
  const feedbackTimerRef = React.useRef(null)
644
+ const kwSkipBlurRef = React.useRef(false)
617
645
  React.useEffect(function () {
618
646
  return function () {
619
647
  if (feedbackTimerRef.current) clearTimeout(feedbackTimerRef.current)
@@ -629,6 +657,16 @@ window.__ModuleLoader__.load({
629
657
  setJudgeModel((data.plugin.judge && data.plugin.judge.model) || '')
630
658
  setJudgeEffort((data.plugin.judge && data.plugin.judge.reasoningEffort) || '')
631
659
  setJudgePromptLang((data.plugin && data.plugin.judgePromptLang) === 'en' ? 'en' : 'zh')
660
+ setJudgePromptDrafts((function () {
661
+ const shipped = (data.predefined && data.predefined.judgePrompts) || {}
662
+ const custom = (data.plugin && data.plugin.judgePrompts) || {}
663
+ const pick = function (lang) {
664
+ const c = custom[lang]
665
+ if (c && String(c).trim()) return String(c)
666
+ return String(shipped[lang] || '')
667
+ }
668
+ return { zh: pick('zh'), en: pick('en') }
669
+ })())
632
670
  setPresetSandbox((data.plugin && data.plugin.presetSandbox) === 'read-only' ? 'read-only' : 'workspace-write')
633
671
  }
634
672
  setError(null)
@@ -646,18 +684,24 @@ window.__ModuleLoader__.load({
646
684
  React.useEffect(function () {
647
685
  const provider = judgeProvider || (snapshot && snapshot.fallback && snapshot.fallback.provider)
648
686
  if (!provider || !rpc) return
687
+ let alive = true
649
688
  rpc('judge-catalog', { provider: provider }).then(function (data) {
689
+ if (!alive) return
650
690
  setModels(data.models || [])
651
- }).catch(function () { setModels([]) })
691
+ }).catch(function () { if (alive) setModels([]) })
692
+ return function () { alive = false }
652
693
  }, [judgeProvider, snapshot, rpc])
653
694
 
654
695
  React.useEffect(function () {
655
696
  const provider = judgeProvider || (snapshot && snapshot.fallback && snapshot.fallback.provider)
656
697
  const model = judgeModel || (snapshot && snapshot.fallback && snapshot.fallback.model)
657
698
  if (!provider || !model || !rpc) { setEfforts([]); return }
699
+ let alive = true
658
700
  rpc('judge-info', { provider: provider, model: model }).then(function (data) {
701
+ if (!alive) return
659
702
  setEfforts(data.efforts || [])
660
- }).catch(function () { setEfforts([]) })
703
+ }).catch(function () { if (alive) setEfforts([]) })
704
+ return function () { alive = false }
661
705
  }, [judgeProvider, judgeModel, snapshot, rpc])
662
706
 
663
707
  const showFeedback = function (msg, ok) {
@@ -666,10 +710,10 @@ window.__ModuleLoader__.load({
666
710
  feedbackTimerRef.current = setTimeout(function () { setFeedback(null) }, 4000)
667
711
  }
668
712
 
669
- const run = function (endpoint, payload, okMsg) {
713
+ const run = function (endpoint, payload, okMsg, opts) {
670
714
  setBusy(true)
671
715
  return rpc(endpoint, payload).then(function (res) {
672
- load()
716
+ load({ keepEdits: !(opts && opts.reload) })
673
717
  showFeedback(okMsg || t('set.hotOk'))
674
718
  return res
675
719
  }).catch(function (e) {
@@ -721,14 +765,47 @@ window.__ModuleLoader__.load({
721
765
  return
722
766
  }
723
767
  setResetArmed(null)
768
+ if (kind === 'judgePrompt') {
769
+ const lang = judgePromptLang === 'en' ? 'en' : 'zh'
770
+ const prompts = {}
771
+ prompts[lang] = ''
772
+ run('save-plugin', { judgePrompts: prompts }, okMsg, { reload: true })
773
+ return
774
+ }
724
775
  run('rule-op', { op: 'reset', kind: kind, value: value }, okMsg)
725
776
  },
726
777
  }, armed ? t('set.resetConfirm') : idleLabel)
727
778
  }
728
779
 
780
+ const saveJudge = function () {
781
+ const n = Number(timeoutMs)
782
+ if (!Number.isFinite(n) || n <= 0) {
783
+ showFeedback(t('err.invalidNumber'), false)
784
+ return
785
+ }
786
+ run('save-plugin', {
787
+ judgePromptLang: judgePromptLang,
788
+ judge: { provider: judgeProvider, model: judgeModel, reasoningEffort: judgeEffort },
789
+ judgeTimeoutMs: n,
790
+ judgePrompts: (function () {
791
+ const shipped = (snapshot.predefined && snapshot.predefined.judgePrompts) || {}
792
+ const store = function (lang) {
793
+ const draft = String((judgePromptDrafts && judgePromptDrafts[lang]) || '')
794
+ const def = String(shipped[lang] || '')
795
+ if (!draft.trim() || draft.trim() === def.trim()) return ''
796
+ return draft
797
+ }
798
+ return { zh: store('zh'), en: store('en') }
799
+ })(),
800
+ }, t('set.judgeSaved'), { reload: true })
801
+ }
802
+
729
803
  return React.createElement('div', { className: 'ab-set', ref: rootRef },
730
804
  React.createElement('h3', { className: 'ab-set-title' }, t('set.title')),
731
805
  React.createElement('p', { className: 'ab-set-intro' }, t('set.intro')),
806
+ feedback
807
+ ? React.createElement('div', { className: 'ab-set-flash ' + (feedback.ok ? 'ab-set-ok' : 'ab-set-err') }, feedback.msg)
808
+ : null,
732
809
  (cfg && cfg.corrupt)
733
810
  ? React.createElement('div', { className: 'ab-set-err' }, t('set.allowlistCorrupt'))
734
811
  : null,
@@ -737,7 +814,7 @@ window.__ModuleLoader__.load({
737
814
  t('set.pluginCorrupt'),
738
815
  React.createElement('button', {
739
816
  type: 'button', className: 'ab-set-btn ab-set-btn-danger', disabled: busy,
740
- onClick: function () { run('save-plugin', { overwriteCorrupt: true }, t('set.overwriteOk')) },
817
+ onClick: function () { run('save-plugin', { overwriteCorrupt: true }, t('set.overwriteOk'), { reload: true }) },
741
818
  }, t('set.pluginCorruptOverwrite')),
742
819
  )
743
820
  : null,
@@ -753,6 +830,14 @@ window.__ModuleLoader__.load({
753
830
  React.createElement('span', { className: 'ab-set-note' }, t('set.presetWriteNote')),
754
831
  ),
755
832
  ) : null,
833
+ (setup.configured && setup.drift && setup.drift.baseKnown && (setup.drift.missing || []).length)
834
+ ? React.createElement('div', { className: 'ab-set-card' },
835
+ React.createElement('div', { className: 'ab-set-card-head' },
836
+ React.createElement('div', { className: 'ab-set-card-title' }, t('set.driftTitle')),
837
+ React.createElement('p', { className: 'ab-set-card-sub' },
838
+ t('set.driftBody', { keys: (setup.drift.missing || []).join(' / ') }))),
839
+ )
840
+ : null,
756
841
 
757
842
  React.createElement('div', { className: 'ab-set-card' },
758
843
  React.createElement('div', { className: 'ab-set-card-head' },
@@ -761,7 +846,13 @@ window.__ModuleLoader__.load({
761
846
  React.createElement('button', {
762
847
  type: 'button',
763
848
  className: 'ab-set-choice' + (presetSandbox === 'workspace-write' ? ' ab-set-choice-on' : ''),
764
- onClick: function () { setPresetSandbox('workspace-write') },
849
+ disabled: busy,
850
+ onClick: function () {
851
+ if (presetSandbox === 'workspace-write') return
852
+ run('save-plugin', { presetSandbox: 'workspace-write' }, t('set.modeSaved')).then(function (res) {
853
+ if (res) setPresetSandbox('workspace-write')
854
+ })
855
+ },
765
856
  },
766
857
  React.createElement('span', { className: 'ab-set-choice-t' }, t('set.modeWs')),
767
858
  React.createElement('span', { className: 'ab-set-choice-d' }, t('set.modeWsHint')),
@@ -769,24 +860,19 @@ window.__ModuleLoader__.load({
769
860
  React.createElement('button', {
770
861
  type: 'button',
771
862
  className: 'ab-set-choice' + (presetSandbox === 'read-only' ? ' ab-set-choice-on' : ''),
772
- onClick: function () { setPresetSandbox('read-only') },
863
+ disabled: busy,
864
+ onClick: function () {
865
+ if (presetSandbox === 'read-only') return
866
+ run('save-plugin', { presetSandbox: 'read-only' }, t('set.modeSaved')).then(function (res) {
867
+ if (res) setPresetSandbox('read-only')
868
+ })
869
+ },
773
870
  },
774
871
  React.createElement('span', { className: 'ab-set-choice-t' }, t('set.modeRo')),
775
872
  React.createElement('span', { className: 'ab-set-choice-d' }, t('set.modeRoHint')),
776
873
  ),
777
874
  ),
778
- React.createElement('div', { className: 'ab-set-row' },
779
- React.createElement('button', {
780
- type: 'button', className: 'ab-set-btn ab-set-btn-primary', disabled: busy,
781
- onClick: function () {
782
- run(
783
- 'save-plugin',
784
- { presetSandbox: presetSandbox },
785
- t('set.modeSaved'),
786
- )
787
- },
788
- }, t('set.save')),
789
- ),
875
+ React.createElement('p', { className: 'ab-set-note' }, t('set.modeHint')),
790
876
  ),
791
877
 
792
878
  React.createElement('div', { className: 'ab-set-card' },
@@ -860,12 +946,19 @@ window.__ModuleLoader__.load({
860
946
  defaultValue: row.text,
861
947
  autoFocus: true,
862
948
  onBlur: function (e) {
949
+ if (kwSkipBlurRef.current) {
950
+ kwSkipBlurRef.current = false
951
+ return
952
+ }
863
953
  commitKw(e.target.value, row.action)
864
954
  setKwEdit(null)
865
955
  },
866
956
  onKeyDown: function (e) {
867
957
  if (e.key === 'Enter') e.currentTarget.blur()
868
- if (e.key === 'Escape') setKwEdit(null)
958
+ if (e.key === 'Escape') {
959
+ kwSkipBlurRef.current = true
960
+ setKwEdit(null)
961
+ }
869
962
  },
870
963
  })
871
964
  : React.createElement('span', {
@@ -916,8 +1009,10 @@ window.__ModuleLoader__.load({
916
1009
  React.createElement('button', {
917
1010
  type: 'button', className: 'ab-set-btn', disabled: busy || !newKeyword.trim(),
918
1011
  onClick: function () {
919
- run('rule-op', { op: 'add', kind: 'keywords', value: { text: newKeyword.trim(), action: newKeywordAction } })
920
- setNewKeyword('')
1012
+ const text = newKeyword.trim()
1013
+ run('rule-op', { op: 'add', kind: 'keywords', value: { text: text, action: newKeywordAction } }).then(function (res) {
1014
+ if (res) setNewKeyword('')
1015
+ })
921
1016
  },
922
1017
  }, t('set.add')),
923
1018
  ),
@@ -978,8 +1073,9 @@ window.__ModuleLoader__.load({
978
1073
  },
979
1074
  }, resetArmed === ('del-c:' + c.id) ? t('set.confirm') : '✕'),
980
1075
  ),
981
- React.createElement('div', { className: 'ab-set-item-fields', key: c.id + ':' + (c.label || '') + ':' + (c.description || '') },
1076
+ React.createElement('div', { className: 'ab-set-item-fields' },
982
1077
  React.createElement('input', {
1078
+ key: c.id + ':label:' + (c.label || ''),
983
1079
  className: 'ab-set-input ab-set-input-ui',
984
1080
  defaultValue: c.label || '',
985
1081
  placeholder: t('set.labelPlaceholder'),
@@ -991,6 +1087,7 @@ window.__ModuleLoader__.load({
991
1087
  onKeyDown: function (e) { if (e.key === 'Enter') e.currentTarget.blur() },
992
1088
  }),
993
1089
  React.createElement('textarea', {
1090
+ key: c.id + ':desc:' + (c.description || ''),
994
1091
  className: 'ab-set-textarea',
995
1092
  defaultValue: c.description || '',
996
1093
  placeholder: t('set.descPlaceholder'),
@@ -1044,8 +1141,10 @@ window.__ModuleLoader__.load({
1044
1141
  type: 'button', className: 'ab-set-btn ab-set-btn-primary',
1045
1142
  disabled: busy || !(newCriterion.id || newCriterion.label),
1046
1143
  onClick: function () {
1047
- run('rule-op', { op: 'add', kind: 'criteria', value: newCriterion })
1048
- setNewCriterion({ id: '', label: '', description: '', action: 'human' })
1144
+ const row = newCriterion
1145
+ run('rule-op', { op: 'add', kind: 'criteria', value: row }).then(function (res) {
1146
+ if (res) setNewCriterion({ id: '', label: '', description: '', action: 'human' })
1147
+ })
1049
1148
  },
1050
1149
  }, t('set.addCriterion')),
1051
1150
  ),
@@ -1099,15 +1198,6 @@ window.__ModuleLoader__.load({
1099
1198
  return React.createElement('option', { value: e.id, key: e.id }, e.name || e.id)
1100
1199
  }),
1101
1200
  ),
1102
- React.createElement('button', {
1103
- type: 'button', className: 'ab-set-btn ab-set-btn-primary', disabled: busy,
1104
- onClick: function () {
1105
- run('save-plugin', {
1106
- judgePromptLang: judgePromptLang,
1107
- judge: { provider: judgeProvider, model: judgeModel, reasoningEffort: judgeEffort },
1108
- }, t('set.judgeSaved'))
1109
- },
1110
- }, t('set.save')),
1111
1201
  ),
1112
1202
  React.createElement('div', { className: 'ab-set-row' },
1113
1203
  React.createElement('span', { className: 'ab-set-item-meta' }, t('set.judgeLang')),
@@ -1118,25 +1208,173 @@ window.__ModuleLoader__.load({
1118
1208
  React.createElement('option', { value: 'zh' }, t('set.judgeLangZh')),
1119
1209
  React.createElement('option', { value: 'en' }, t('set.judgeLangEn')),
1120
1210
  ),
1121
- React.createElement('span', { className: 'ab-set-note' }, t('set.judgeLangHint')),
1211
+ React.createElement('span', { className: 'ab-set-card-sub' }, t('set.judgeLangHint')),
1212
+ ),
1213
+ React.createElement('div', { className: 'ab-set-item-fields' },
1214
+ React.createElement('div', { className: 'ab-set-row' },
1215
+ React.createElement('span', { className: 'ab-set-item-meta' }, t('set.judgePrompt')),
1216
+ (function () {
1217
+ const shipped = String(((snapshot.predefined && snapshot.predefined.judgePrompts) || {})[judgePromptLang] || '')
1218
+ const draft = String((judgePromptDrafts && judgePromptDrafts[judgePromptLang]) || '')
1219
+ const custom = draft.trim() && draft.trim() !== shipped.trim()
1220
+ return React.createElement('span', { className: 'ab-set-tag ' + (custom ? 'ab-set-tag-warn' : 'ab-set-tag-blue') }, custom ? t('set.judgePromptCustom') : t('set.judgePromptDefault'))
1221
+ })(),
1222
+ ),
1223
+ React.createElement('p', { className: 'ab-set-card-sub' }, t('set.judgePromptHint')),
1224
+ React.createElement('textarea', {
1225
+ key: 'judge-prompt:' + judgePromptLang,
1226
+ className: 'ab-set-textarea ab-set-textarea-prompt',
1227
+ value: (judgePromptDrafts && judgePromptDrafts[judgePromptLang]) || '',
1228
+ spellCheck: false,
1229
+ rows: 12,
1230
+ onChange: function (e) {
1231
+ const v = e.target.value
1232
+ const lang = judgePromptLang
1233
+ setJudgePromptDrafts(function (prev) {
1234
+ const next = Object.assign({}, prev)
1235
+ next[lang] = v
1236
+ return next
1237
+ })
1238
+ },
1239
+ }),
1122
1240
  ),
1123
1241
  React.createElement('div', { className: 'ab-set-row' },
1124
1242
  React.createElement('span', { className: 'ab-set-item-meta' }, t('set.judgeTimeoutMs')),
1125
1243
  React.createElement('input', { className: 'ab-set-input ab-set-input-num', type: 'number', min: 1000, value: timeoutMs, onChange: function (e) { setTimeoutMs(e.target.value) } }),
1244
+ ),
1245
+ React.createElement('div', { className: 'ab-set-row ab-set-foot' },
1246
+ resetBtn('judgePrompt', t('set.resetJudgePrompt'), t('set.resetJudgePromptOk')),
1126
1247
  React.createElement('button', {
1127
- type: 'button', className: 'ab-set-btn', disabled: busy,
1128
- onClick: function () { run('rule-op', { op: 'set', kind: 'judgeTimeoutMs', value: Number(timeoutMs) }) },
1248
+ type: 'button', className: 'ab-set-btn ab-set-btn-primary', disabled: busy,
1249
+ onClick: saveJudge,
1129
1250
  }, t('set.save')),
1130
1251
  ),
1131
1252
  ),
1132
1253
 
1133
- feedback
1134
-
1135
- ? React.createElement('div', { className: feedback.ok ? 'ab-set-ok' : 'ab-set-err' }, feedback.msg)
1136
- : null,
1137
1254
  )
1138
1255
  }
1139
1256
 
1257
+ const AUTO_PRESET_LABEL = '自动审批'
1258
+ const ACCESS_GLYPH_CLASS = 'ab-access-glyph'
1259
+ const SHIELD_OUTLINE = 'M8.20554 0.899994L14.7901 3.36857V7.01026C14.7901 12 11.0466 14.2103 8.20554 15.3C5.36446 14.2103 1.62012 12 1.62012 7.01026V3.36857L8.20554 0.899994Z'
1260
+ const LETTER_A = 'M8.205 4.95 11.62 11.45H10.1l-.55-1.42H6.655l-.55 1.42H4.59L8.205 4.95Zm-.72 3.68h1.44L8.205 6.35 7.485 8.63Z'
1261
+
1262
+ function accessGlyphSvg() {
1263
+ const ns = 'http://www.w3.org/2000/svg'
1264
+ const svg = document.createElementNS(ns, 'svg')
1265
+ svg.setAttribute('width', '16')
1266
+ svg.setAttribute('height', '16')
1267
+ svg.setAttribute('viewBox', '0 0 16 16')
1268
+ svg.setAttribute('fill', 'none')
1269
+ svg.setAttribute('aria-hidden', 'true')
1270
+ const shield = document.createElementNS(ns, 'path')
1271
+ shield.setAttribute('d', SHIELD_OUTLINE)
1272
+ shield.setAttribute('stroke', 'currentColor')
1273
+ shield.setAttribute('stroke-width', '1.31831')
1274
+ shield.setAttribute('stroke-linejoin', 'round')
1275
+ const letter = document.createElementNS(ns, 'path')
1276
+ letter.setAttribute('d', LETTER_A)
1277
+ letter.setAttribute('fill', 'currentColor')
1278
+ letter.setAttribute('fill-rule', 'evenodd')
1279
+ svg.appendChild(shield)
1280
+ svg.appendChild(letter)
1281
+ return svg
1282
+ }
1283
+
1284
+ function isAccessTrigger(el) {
1285
+ if (!el || el.tagName !== 'BUTTON') return false
1286
+ const label = el.getAttribute('aria-label') || ''
1287
+ if (label.indexOf(AUTO_PRESET_LABEL) === -1) return false
1288
+ return label.indexOf('访问模式') === 0 || label.indexOf('Access mode') === 0
1289
+ }
1290
+
1291
+ function isAccessMenuItem(el) {
1292
+ if (!el || typeof el.getAttribute !== 'function') return false
1293
+ if (el.getAttribute('role') !== 'menuitem') return false
1294
+ return String(el.textContent || '').replace(/\s+/g, '') === AUTO_PRESET_LABEL
1295
+ }
1296
+
1297
+ function decorateAccess(el) {
1298
+ if (!el || el.querySelector('.' + ACCESS_GLYPH_CLASS)) return
1299
+ const span = document.createElement('span')
1300
+ span.className = ACCESS_GLYPH_CLASS
1301
+ span.setAttribute('aria-hidden', 'true')
1302
+ span.appendChild(accessGlyphSvg())
1303
+ el.insertBefore(span, el.firstChild)
1304
+ }
1305
+
1306
+ /** 不再匹配的元素要摘掉徽标:切走「自动审批」后按钮是同一个 DOM 节点。 */
1307
+ function undecorateAccess(el) {
1308
+ if (!el || typeof el.querySelector !== 'function') return
1309
+ const glyph = el.querySelector('.' + ACCESS_GLYPH_CLASS)
1310
+ if (glyph && glyph.parentNode) glyph.parentNode.removeChild(glyph)
1311
+ }
1312
+
1313
+ function matchesAccess(el) {
1314
+ return isAccessTrigger(el) || isAccessMenuItem(el)
1315
+ }
1316
+
1317
+ function scanAccessGlyphs(root) {
1318
+ // 只处理元素/文档:MutationObserver 的 addedNodes 含文本节点,
1319
+ // 落到 document 兜底会让每次文本变化都做整篇 querySelectorAll。
1320
+ if (!root || (root.nodeType !== 1 && root.nodeType !== 9) || typeof root.querySelectorAll !== 'function') return
1321
+ const scope = root
1322
+ const buttons = scope.querySelectorAll('button[aria-label]')
1323
+ for (let i = 0; i < buttons.length; i++) {
1324
+ if (isAccessTrigger(buttons[i])) decorateAccess(buttons[i])
1325
+ }
1326
+ const items = scope.querySelectorAll('[role="menuitem"]')
1327
+ for (let i = 0; i < items.length; i++) {
1328
+ if (isAccessMenuItem(items[i])) decorateAccess(items[i])
1329
+ }
1330
+ // 反向清理:已经不匹配的元素(换预设 / 菜单关闭后 role 变化)必须摘掉徽标。
1331
+ const glyphs = scope.querySelectorAll('.' + ACCESS_GLYPH_CLASS)
1332
+ for (let i = 0; i < glyphs.length; i++) {
1333
+ const parent = glyphs[i].parentNode
1334
+ if (parent && !matchesAccess(parent)) undecorateAccess(parent)
1335
+ }
1336
+ if (root.nodeType === 1) {
1337
+ if (matchesAccess(root)) decorateAccess(root)
1338
+ else undecorateAccess(root)
1339
+ }
1340
+ }
1341
+
1342
+ function mountAccessGlyphs() {
1343
+ scanAccessGlyphs(document)
1344
+ const obs = new MutationObserver(function (records) {
1345
+ for (let i = 0; i < records.length; i++) {
1346
+ const rec = records[i]
1347
+ if (rec.type === 'attributes' && rec.target) {
1348
+ scanAccessGlyphs(rec.target)
1349
+ continue
1350
+ }
1351
+ // childList:React 先插入空按钮、再把标签塞进去时,那条记录的 target 是标签自己,
1352
+ // 只看 addedNodes 会让下拉里的「自动审批」永远拿不到徽标。这里只沿 target 向上找
1353
+ // 最近的触发器/menu 项重扫(不是扫整棵 target 子树,避免流式输出时每次变更都全量子树查询)。
1354
+ if (rec.target && rec.target.nodeType === 1 && typeof rec.target.closest === 'function') {
1355
+ const owner = rec.target.closest('button[aria-label], [role="menuitem"]')
1356
+ if (owner) scanAccessGlyphs(owner)
1357
+ }
1358
+ const nodes = rec.addedNodes
1359
+ for (let j = 0; j < nodes.length; j++) scanAccessGlyphs(nodes[j])
1360
+ }
1361
+ })
1362
+ obs.observe(document.body, {
1363
+ childList: true,
1364
+ subtree: true,
1365
+ attributes: true,
1366
+ attributeFilter: ['aria-label'],
1367
+ })
1368
+ return function () {
1369
+ obs.disconnect()
1370
+ const leftover = document.querySelectorAll('.' + ACCESS_GLYPH_CLASS)
1371
+ for (let i = 0; i < leftover.length; i++) {
1372
+ const n = leftover[i]
1373
+ if (n.parentNode) n.parentNode.removeChild(n)
1374
+ }
1375
+ }
1376
+ }
1377
+
1140
1378
  const plugin = {
1141
1379
  inject: ['connection', 'slots', 'locale'],
1142
1380
  async apply(ctx) {
@@ -1162,6 +1400,14 @@ window.__ModuleLoader__.load({
1162
1400
  }
1163
1401
  }
1164
1402
  })
1403
+ ctx.effect(function () {
1404
+ try {
1405
+ return mountAccessGlyphs()
1406
+ } catch (e) {
1407
+ console.error('[dsh-auto-approve] 访问模式图标失败:' + String((e && e.message) || e))
1408
+ return function () {}
1409
+ }
1410
+ })
1165
1411
 
1166
1412
  slots.inject('conversation.input.dock', function () {
1167
1413
  return slots.register(