@dnalec/dsh-auto-approve 0.1.3 → 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,10 +1,11 @@
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
- * 设置页折叠状态用 React state 控 details,避免 snapshot 刷新后合上。
8
+ * 设置页折叠状态用 React state 控 details,避免 snapshot 刷新后合上。人工只走原网页审批框。
8
9
  * 恢复默认:第一次点武装,5 秒内再点才执行。
9
10
  */
10
11
  window.__ModuleLoader__.load({
@@ -74,7 +75,7 @@ window.__ModuleLoader__.load({
74
75
  .ab-set-fold-body{padding:12px 14px 14px;display:flex;flex-direction:column;gap:12px}
75
76
  .ab-set-fold-sum{flex:none;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-align:right;max-width:46%}
76
77
  .ab-set-fold-chev{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px}
77
- .ab-set-fold[open] .ab-set-fold-chev{transform:rotate(180deg)}
78
+ .ab-set-fold[open]>summary .ab-set-fold-chev{transform:rotate(180deg)}
78
79
  .ab-set-card-head{flex-direction:column;gap:4px;display:flex}
79
80
  .ab-set-card-title{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:14px;font-weight:500;line-height:20px;display:flex}
80
81
  .ab-set-card-sub{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;line-height:20px}
@@ -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}
@@ -105,16 +107,13 @@ window.__ModuleLoader__.load({
105
107
  .ab-set-crit-top{display:flex;align-items:center;gap:8px;min-width:0}
106
108
  .ab-set-item-fields{min-width:0;flex-direction:column;gap:6px;display:flex}
107
109
  .ab-set-item-id{flex:1 1 auto;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px;font-family:var(--ds-font-family-code)}
108
- .ab-set-item-label{flex:1 1 auto;min-width:0;color:var(--dsw-alias-label-primary);font-size:12px;line-height:18px;font-family:var(--ds-font-family-code);word-break:break-all}
109
110
  .ab-set-item-meta{flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap}
110
111
  .ab-set-item-del{flex:none;width:24px;height:24px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:transparent;border:none;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;padding:0}
111
112
  .ab-set-item-del:hover{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}
112
113
  .ab-set-empty{color:var(--dsw-alias-label-caption);margin:0;font-size:13px;line-height:20px;padding:4px 2px}
113
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}
114
115
  .ab-set-tag-blue{color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
115
- .ab-set-tag-green{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-tertiary)}
116
- .ab-set-tag-gray{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform)}
117
- .ab-set-tag-red{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger)}
116
+ .ab-set-tag-warn{color:var(--dsw-alias-state-warn-label);background:var(--dsw-alias-state-warn-tertiary)}
118
117
  .ab-set-steps{display:flex;flex-wrap:wrap;gap:6px}
119
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}
120
119
  .ab-set-step:hover{background:var(--dsw-alias-interactive-bg-hover)}
@@ -131,18 +130,20 @@ window.__ModuleLoader__.load({
131
130
  .ab-set-choice-on{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
132
131
  .ab-set-choice-t{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}
133
132
  .ab-set-choice-d{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
134
- .ab-set-learn-info{flex:1 1 auto;min-width:0;flex-direction:column;gap:2px;display:flex}
135
- .ab-set-learn-sub{color:var(--dsw-alias-label-tertiary);font-size:11px}
136
- .ab-chat{cursor:pointer}
137
- .ab-chat:hover{background:var(--dsw-alias-interactive-bg-hover)}
138
- .ab-set-qr{text-align:center;padding:4px 0 8px}
139
- .ab-set-qr img{box-sizing:border-box;border-radius:8px;background:#fff;padding:8px;max-width:240px;width:100%;height:auto}
140
- .ab-set-qr-tip{color:var(--dsw-alias-label-tertiary);margin:6px 0 0;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}
141
142
  `
142
143
 
143
144
  const NS = "dsh-auto-approve"
144
- const ZH = JSON.parse(String.raw`{"slot.notice":"自动放行提示","slot.history":"审批","slot.settings":"自动审批","action.reject":"拒绝","action.allow":"允许","action.human":"人工","verdict.keyword-allow":"关键词允许","verdict.keyword-reject":"关键词拒绝","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":"等待人工审批{ticket}:{preview}","notice.pendingTag":"人工审批中{ticket}","notice.qqApproved":"已在 QQ 批准{ticket}:{preview}","notice.webApproved":"人工审批通过{ticket}:{preview}","notice.qqApprovedTag":"已在 QQ 批准{ticket}","notice.webApprovedTag":"人工审批通过","notice.rejectedTitle":"已拒绝{ticket}:{preview}","notice.rejectedTag":"已拒绝{ticket}","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":"待处理 {ticket}","history.tagQqAllow":"QQ 批准 {ticket}","history.tagAllow":"批准 {ticket}","history.tagReject":"拒绝 {ticket}","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.ticket":"短号","detail.source":"来源","detail.judgeFailed":"未完成(解析失败/超时转人工,不执行「其他」的动作)","set.title":"自动审批","set.intro":"需要审批的行为会自动审批,并可推送到已配置的消息平台。按私人 bot 设计:未绑定聊天时,私聊回「是」会绑成审批目标,不要让陌生人能私聊这个 bot。","set.allowlistCorrupt":"规则文件损坏,当前是内存默认,普通保存不会覆盖磁盘。请修好 allowlist.json,或点「恢复默认」写回出厂规则。","set.pluginCorrupt":"插件配置损坏,拒绝保存以免清空 chatId。请修好 ~/.dsh/approval-bridge/config.json,或","set.pluginCorruptOverwrite":"覆盖损坏配置","set.qqCorrupt":"QQ 凭据文件损坏,未覆盖磁盘。请修好 ~/.dsh/approval-bridge/qqbot.json,或重新保存凭据 / 扫码。","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":"workspace-write(推荐)","set.modeWsHint":"工作区内直接放行,越出工作区需审核。","set.modeRo":"read-only","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.qqTitle":"QQ 审批通道","set.qqSub":"扫码或填凭据。按私人 bot 设计:未绑定 chatId 时,私聊回「是」会绑成审批聊天。不要让陌生人能私聊这个 bot。群必须 @机器人并另配 userId。","set.qqScan":"扫码接入机器人","set.qqRescan":"重新扫码接入","set.qqCancelScan":"取消扫码","set.qqCancelOk":"已取消扫码","set.qqScanStart":"已开始扫码,请用手机 QQ 扫描","set.qqScanOk":"扫码成功,凭据已保存","set.qqScanFail":"扫码失败","set.qqScanStatus":"扫码接入:{status}","set.qqQrAlt":"QQ 机器人扫码二维码","set.qqQrTip":"请用手机 QQ 扫码并确认创建机器人(约 5 分钟有效)","set.qqAppId":"AppID","set.qqAppSecret":"AppSecret","set.qqSecretKeep":"AppSecret(留空则保留)","set.qqSaveConnect":"保存并连接","set.qqCredsReconnect":"凭据已保存并重连","set.qqReconnect":"重连","set.qqReconnected":"正在重连","set.qqUnknown":"未知","set.qqChatOn":"已选定聊天","set.qqChatOff":"未选定","set.qqPush":"推送:{state}","set.qqPushOn":"","set.qqPushOff":"","set.qqPushToggled":"已切换推送","set.qqEnablePush":"启用推送","set.qqPausePush":"暂停推送","set.qqCurrentChat":"当前 chatId:{chatId}","set.qqUnset":"(未选定)","set.qqChatPlaceholder":"chatId(私聊 openid / 群 g:openid)","set.qqGroupUser":"群 userId","set.qqTimeoutS":"超时s","set.prov.loggingIn":"登录中","set.prov.waitScan":"等待扫码","set.prov.saving":"保存凭据","set.prov.connected":"已连接","set.chatId":"chatId","set.userId":"userId(群)","set.notifyTimeout":"提醒间隔(秒)","set.saveTarget":"保存目标/超时","set.targetSaved":"推送目标已保存","set.recent":"最近来信(点选设为审批聊天;群必须 @机器人,还需匹配 userId)","set.noMail":"还没有来信。加好友后给机器人发任意一句。","set.group":"","set.c2c":"私聊","set.chatPicked":"已选定审批聊天","set.overwriteOk":"已覆盖损坏的插件配置","rpc.unavailable":"connection.rpc 不可用","rpc.failed":"RPC 失败"}`)
145
- 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","verdict.keyword-allow":"Keyword allow","verdict.keyword-reject":"Keyword reject","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{ticket}: {preview}","notice.pendingTag":"Human review{ticket}","notice.qqApproved":"Approved in QQ{ticket}: {preview}","notice.webApproved":"Approved{ticket}: {preview}","notice.qqApprovedTag":"Approved in QQ{ticket}","notice.webApprovedTag":"Approved","notice.rejectedTitle":"Rejected{ticket}: {preview}","notice.rejectedTag":"Rejected{ticket}","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":"Pending {ticket}","history.tagQqAllow":"QQ allow {ticket}","history.tagAllow":"Allow {ticket}","history.tagReject":"Reject {ticket}","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.ticket":"Ticket","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 auto-approved and can be pushed to a configured chat. Designed for a private bot: if no chat is bound, a C2C reply of “是” binds that chat. Do not let strangers DM this bot.","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 chatId is not wiped. Repair ~/.dsh/approval-bridge/config.json, or ","set.pluginCorruptOverwrite":"Overwrite corrupt config","set.qqCorrupt":"QQ credentials file is corrupt and was not overwritten. Repair ~/.dsh/approval-bridge/qqbot.json, or save credentials / scan again.","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.qqTitle":"QQ approval channel","set.qqSub":"Scan or paste credentials. Designed for a private bot: if no chatId is bound, a C2C reply of “是” binds that chat. Do not let strangers DM this bot. Groups must @ the bot and set userId.","set.qqScan":"Scan to create bot","set.qqRescan":"Scan again","set.qqCancelScan":"Cancel scan","set.qqCancelOk":"Scan cancelled","set.qqScanStart":"Scan started; use mobile QQ","set.qqScanOk":"Scan succeeded; credentials saved","set.qqScanFail":"Scan failed","set.qqScanStatus":"Scan: {status}","set.qqQrAlt":"QQ bot QR code","set.qqQrTip":"Scan with mobile QQ and confirm creating the bot (about 5 minutes)","set.qqAppId":"AppID","set.qqAppSecret":"AppSecret","set.qqSecretKeep":"AppSecret (leave blank to keep)","set.qqSaveConnect":"Save and connect","set.qqCredsReconnect":"Credentials saved; reconnecting","set.qqReconnect":"Reconnect","set.qqReconnected":"Reconnecting","set.qqUnknown":"Unknown","set.qqChatOn":"Chat selected","set.qqChatOff":"No chat selected","set.qqPush":"Push: {state}","set.qqPushOn":"on","set.qqPushOff":"off","set.qqPushToggled":"Push toggled","set.qqEnablePush":"Enable push","set.qqPausePush":"Pause push","set.qqCurrentChat":"Current chatId: {chatId}","set.qqUnset":"(none)","set.qqChatPlaceholder":"chatId (C2C openid / group g:openid)","set.qqGroupUser":"Group userId","set.qqTimeoutS":"Timeout s","set.prov.loggingIn":"Signing in","set.prov.waitScan":"Waiting for scan","set.prov.saving":"Saving credentials","set.prov.connected":"Connected","set.chatId":"chatId","set.userId":"userId (group)","set.notifyTimeout":"Reminder interval (seconds)","set.saveTarget":"Save target / timeout","set.targetSaved":"Push target saved","set.recent":"Recent incoming (click to set as the approval chat; groups must @ the bot and match userId)","set.noMail":"No messages yet. Friend the bot and send it anything.","set.group":"Group","set.c2c":"C2C","set.chatPicked":"Approval chat selected","set.overwriteOk":"Corrupt plugin config overwritten","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"}`)
146
147
 
147
148
  function fillLocale(template, params) {
148
149
  if (!params) return template
@@ -152,6 +153,7 @@ window.__ModuleLoader__.load({
152
153
  }
153
154
 
154
155
  function tFrom(props) {
156
+ if (props && typeof props.t === 'function') return props.t
155
157
  const sp = (props && props.slotsProps) || {}
156
158
  if (typeof sp.t === 'function') return sp.t
157
159
  return function (key, params) {
@@ -159,42 +161,39 @@ window.__ModuleLoader__.load({
159
161
  }
160
162
  }
161
163
 
164
+ function lookupLabel(t, prefix, id) {
165
+ if (!id) return ''
166
+ const key = prefix + '.' + id
167
+ const v = t(key)
168
+ return v === key ? '' : v
169
+ }
170
+
162
171
  function verdictLabel(t, verdict) {
163
172
  if (!verdict) return t('notice.autoDefault')
164
- const key = 'verdict.' + verdict
165
- const v = t(key)
166
- return v === key ? String(verdict) : v
173
+ return lookupLabel(t, 'verdict', verdict) || lookupLabel(t, 'path', verdict) || String(verdict)
167
174
  }
168
175
 
169
176
  function pathLabel(t, path) {
170
- if (!path) return ''
171
- const key = 'path.' + path
172
- const v = t(key)
173
- return v === key ? String(path) : v
177
+ return lookupLabel(t, 'path', path) || lookupLabel(t, 'verdict', path) || (path ? String(path) : '')
174
178
  }
175
179
 
176
180
  function actionLabel(t, action) {
177
- if (!action) return ''
178
- const key = 'action.' + action
179
- const v = t(key)
180
- return v === key ? String(action) : v
181
+ return lookupLabel(t, 'action', action) || (action ? String(action) : '')
181
182
  }
182
183
 
183
- function provisionLabel(t, status) {
184
- const map = {
185
- '登录中': 'set.prov.loggingIn',
186
- '等待扫码': 'set.prov.waitScan',
187
- '保存凭据': 'set.prov.saving',
188
- '已连接': 'set.prov.connected',
189
- loggingIn: 'set.prov.loggingIn',
190
- waitScan: 'set.prov.waitScan',
191
- saving: 'set.prov.saving',
192
- connected: 'set.prov.connected',
193
- }
194
- const key = map[status]
195
- return key ? t(key) : String(status || '')
184
+ function criterionLabel(t, criterion, storedLabel) {
185
+ return lookupLabel(t, 'criterion', criterion) || storedLabel || (criterion ? String(criterion) : '')
186
+ }
187
+
188
+ function sandboxLabel(t, mode) {
189
+ return lookupLabel(t, 'sandbox', mode) || (mode ? String(mode) : '')
196
190
  }
197
191
 
192
+ function sourceLabel(t, source) {
193
+ return lookupLabel(t, 'source', source) || (source ? String(source) : '')
194
+ }
195
+
196
+
198
197
  function countsLabel(t, reject, human, allow) {
199
198
  return t('set.counts', { reject: String(reject), human: String(human), allow: String(allow) })
200
199
  }
@@ -210,7 +209,7 @@ window.__ModuleLoader__.load({
210
209
  }
211
210
 
212
211
  function hasVal(value) {
213
- return value !== undefined && value !== null && value !== ''
212
+ return value !== undefined && value !== null
214
213
  }
215
214
 
216
215
  function isLongVal(value) {
@@ -218,24 +217,26 @@ window.__ModuleLoader__.load({
218
217
  return s.length > 72 || s.indexOf('\n') !== -1
219
218
  }
220
219
 
221
- function Detail(label, value) {
220
+ function Detail(label, value, emptyText) {
222
221
  if (!hasVal(value)) return null
223
- const long = isLongVal(value)
222
+ if (value === '' && !emptyText) return null
223
+ const shown = value === '' ? emptyText : String(value)
224
+ const long = isLongVal(shown)
224
225
  return [
225
226
  React.createElement('div', { className: 'ab-detail-k', key: label + '-k' }, label),
226
227
  React.createElement(long ? 'pre' : 'div', {
227
228
  className: 'ab-detail-v' + (long ? ' ab-detail-v-box' : ''),
228
229
  key: label + '-v',
229
- }, String(value)),
230
+ }, shown),
230
231
  ]
231
232
  }
232
233
 
233
- function DetailSection(title, items) {
234
+ function DetailSection(title, items, emptyText) {
234
235
  const kids = []
235
236
  for (let i = 0; i < items.length; i++) {
236
237
  const row = items[i]
237
238
  if (!row) continue
238
- const node = Detail(row[0], row[1])
239
+ const node = Detail(row[0], row[1], emptyText)
239
240
  if (node) kids.push(node[0], node[1])
240
241
  }
241
242
  if (!kids.length) return null
@@ -252,11 +253,13 @@ window.__ModuleLoader__.load({
252
253
  }
253
254
 
254
255
  function fmtTime(iso) {
256
+ if (!iso) return ''
255
257
  try {
256
258
  const d = new Date(iso)
259
+ if (Number.isNaN(d.getTime())) return ''
257
260
  const p = (n) => String(n).padStart(2, '0')
258
261
  return p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
259
- } catch (e) { return String(iso || '') }
262
+ } catch (e) { return '' }
260
263
  }
261
264
 
262
265
  function GlyphCheck() {
@@ -276,18 +279,37 @@ window.__ModuleLoader__.load({
276
279
  }
277
280
 
278
281
  function latestUnsettledPending(evs) {
279
- const settled = Object.create(null)
280
282
  for (let i = evs.length - 1; i >= 0; i--) {
281
- const ev = evs[i]
282
- const kind = ev.kind || ''
283
- if (kind === 'manual-approved' || kind === 'manual-rejected' || kind === 'manual-cancelled') {
284
- if (ev.ticket != null) settled[ev.ticket] = true
285
- }
286
- if (kind === 'manual-pending' && (ev.ticket == null || !settled[ev.ticket])) return ev
283
+ const kind = evs[i].kind || ''
284
+ if (kind === 'manual-approved' || kind === 'manual-rejected' || kind === 'manual-cancelled' || kind === 'manual-unavailable') return null
285
+ if (kind === 'manual-pending') return evs[i]
287
286
  }
288
287
  return null
289
288
  }
290
289
 
290
+
291
+ function formatErr(t, code, details) {
292
+ if (!code) return ''
293
+ const d = details && typeof details === 'object' ? details : {}
294
+ const raw = String(code)
295
+ const key = raw.indexOf('err.') === 0 || raw.indexOf('rpc.') === 0 ? raw : 'err.' + raw
296
+ const v = t(key, d)
297
+ if (v && v !== key) return v
298
+ if (raw.indexOf('err.') !== 0 && raw.indexOf('rpc.') !== 0) {
299
+ const direct = t(raw, d)
300
+ if (direct && direct !== raw) return direct
301
+ }
302
+ if (d.error) return String(d.error)
303
+ return raw
304
+ }
305
+
306
+ function codedText(t, text, details) {
307
+ if (!text) return ''
308
+ const s = String(text)
309
+ if (s.indexOf('err.') === 0 || s.indexOf('rpc.') === 0) return formatErr(t, s, details || {}) || s
310
+ return s
311
+ }
312
+
291
313
  function makeRpc(connection, t) {
292
314
  const tr = typeof t === 'function' ? t : function (key) { return ZH[key] || key }
293
315
  return function rpc(endpoint, payload) {
@@ -299,8 +321,8 @@ window.__ModuleLoader__.load({
299
321
  payload: payload || {},
300
322
  }).then(function (r) {
301
323
  if (!r || r.ok === false) {
302
- const msg = r && r.error && r.error.message ? r.error.message : tr('rpc.failed')
303
- throw new Error(msg)
324
+ const err = r && r.error
325
+ throw new Error(formatErr(tr, err && err.code, err && (err.details || {})) || (err && err.message) || tr('rpc.failed'))
304
326
  }
305
327
  return r.value
306
328
  })
@@ -381,8 +403,7 @@ window.__ModuleLoader__.load({
381
403
  if (!notice) return null
382
404
  const kind = notice.kind || 'auto'
383
405
  const isPending = kind === 'manual-pending'
384
- const isManual = kind === 'manual-approved' || kind === 'manual-rejected'
385
- const ticket = notice.ticket ? (' #' + notice.ticket) : ''
406
+ const isManual = kind === 'manual-approved' || kind === 'manual-rejected' || kind === 'manual-cancelled' || kind === 'manual-unavailable'
386
407
  let title = ''
387
408
  let tagText = ''
388
409
  let glyph = null
@@ -391,17 +412,25 @@ window.__ModuleLoader__.load({
391
412
  tagText = t('notice.feedErrorTag')
392
413
  glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '!')
393
414
  } else if (isPending) {
394
- title = t('notice.pendingTitle', { ticket: ticket, preview: eventPreview(notice) })
395
- tagText = t('notice.pendingTag', { ticket: ticket })
415
+ title = t('notice.pendingTitle', { preview: eventPreview(notice) })
416
+ tagText = t('notice.pendingTag')
396
417
  glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '◔')
397
418
  } else if (kind === 'manual-approved') {
398
- title = t(notice.source === 'qq' ? 'notice.qqApproved' : 'notice.webApproved', { ticket: ticket, preview: eventPreview(notice) })
399
- tagText = t(notice.source === 'qq' ? 'notice.qqApprovedTag' : 'notice.webApprovedTag', { ticket: ticket })
419
+ title = t('notice.webApproved', { preview: eventPreview(notice) })
420
+ tagText = t('notice.webApprovedTag')
400
421
  glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '✓')
422
+ } else if (kind === 'manual-cancelled') {
423
+ title = t('notice.cancelledTitle', { preview: eventPreview(notice) })
424
+ tagText = t('history.tagCancel')
425
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '✕')
426
+ } else if (kind === 'manual-unavailable') {
427
+ title = t('notice.unavailableTitle', { preview: eventPreview(notice) })
428
+ tagText = t('history.tagUnavailable')
429
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '!')
401
430
  } else if (kind === 'manual-rejected' || isAutoReject(notice.verdict)) {
402
- title = t('notice.rejectedTitle', { ticket: ticket, preview: eventPreview(notice) })
431
+ title = t('notice.rejectedTitle', { preview: eventPreview(notice) })
403
432
  tagText = kind === 'manual-rejected'
404
- ? t('notice.rejectedTag', { ticket: ticket })
433
+ ? t('notice.rejectedTag')
405
434
  : (verdictLabel(t, notice.verdict) || t('notice.rejectedDefault'))
406
435
  glyph = React.createElement('span', { className: 'ab-notice-glyph-err' }, '✕')
407
436
  } else {
@@ -417,11 +446,11 @@ window.__ModuleLoader__.load({
417
446
  React.createElement('span', { className: 'ab-notice-glyph' }, glyph),
418
447
  React.createElement('div', { className: 'ab-notice-body' },
419
448
  React.createElement('div', { className: 'ab-notice-head' },
420
- React.createElement('span', { className: 'ab-notice-tool' }, notice.tool || (kind === 'feed-error' ? t('notice.hint') : 'tool')),
449
+ React.createElement('span', { className: 'ab-notice-tool' }, notice.tool || (kind === 'feed-error' ? t('notice.hint') : t('history.unknownTool'))),
421
450
  React.createElement('span', { className: 'ab-notice-text' }, title),
422
451
  ),
423
452
  React.createElement('div', { className: 'ab-notice-meta' },
424
- React.createElement('span', { className: isPending ? 'ab-tag-warn' : (kind === 'manual-rejected' || isAutoReject(notice.verdict) || kind === 'feed-error') ? 'ab-tag-err' : kind === 'manual-approved' ? 'ab-tag-warn' : 'ab-tag' }, tagText),
453
+ React.createElement('span', { className: isPending || kind === 'manual-approved' || kind === 'manual-cancelled' || kind === 'manual-unavailable' ? 'ab-tag-warn' : (kind === 'manual-rejected' || isAutoReject(notice.verdict) || kind === 'feed-error') ? 'ab-tag-err' : 'ab-tag' }, tagText),
425
454
  React.createElement('span', { className: 'ab-time' }, fmtTime(notice.ts)),
426
455
  ),
427
456
  ),
@@ -437,7 +466,7 @@ window.__ModuleLoader__.load({
437
466
  function HistoryView(props) {
438
467
  const t = tFrom(props)
439
468
  const rpc = props.rpc
440
- const sessionId = (props && (props.sessionId || (props.slotsProps && props.slotsProps.sessionId))) || null
469
+ const sessionId = (props && props.sessionId) || resolveFrame(props.slotsProps || props).sessionId || null
441
470
  const [events, setEvents] = React.useState(null)
442
471
  const [error, setError] = React.useState(null)
443
472
  const [openId, setOpenId] = React.useState(null)
@@ -445,6 +474,9 @@ window.__ModuleLoader__.load({
445
474
  React.useEffect(function () {
446
475
  let alive = true
447
476
  let timer = null
477
+ setEvents(null)
478
+ setError(null)
479
+ setOpenId(null)
448
480
  if (!sessionId || !rpc) return
449
481
  const load = function () {
450
482
  rpc('events', { sessionId: sessionId, since: 0 }).then(function (data) {
@@ -481,22 +513,29 @@ window.__ModuleLoader__.load({
481
513
  React.createElement('div', { className: 'ab-list' },
482
514
  events.map(function (ev) {
483
515
  const kind = ev.kind || 'auto'
484
- const ticket = ev.ticket ? '#' + ev.ticket : ''
485
516
  let glyph = React.createElement(GlyphCheck, null)
486
517
  let tag = t('history.tagAuto')
487
518
  let tagCls = 'ab-tag'
488
519
  if (kind === 'manual-pending') {
489
520
  glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '◔')
490
- tag = t('history.tagPending', { ticket: ticket })
521
+ tag = t('history.tagPending')
491
522
  tagCls = 'ab-tag-warn'
492
523
  } else if (kind === 'manual-approved') {
493
524
  glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '✓')
494
- tag = t(ev.source === 'qq' ? 'history.tagQqAllow' : 'history.tagAllow', { ticket: ticket })
525
+ tag = t('history.tagAllow')
495
526
  tagCls = 'ab-tag-warn'
496
527
  } else if (kind === 'manual-rejected') {
497
528
  glyph = React.createElement('span', { className: 'ab-notice-glyph-err' }, '✕')
498
- tag = t('history.tagReject', { ticket: ticket })
529
+ tag = t('history.tagReject')
499
530
  tagCls = 'ab-tag-err'
531
+ } else if (kind === 'manual-cancelled') {
532
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '✕')
533
+ tag = t('history.tagCancel')
534
+ tagCls = 'ab-tag-warn'
535
+ } else if (kind === 'manual-unavailable') {
536
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '!')
537
+ tag = t('history.tagUnavailable')
538
+ tagCls = 'ab-tag-warn'
500
539
  } else if (isAutoReject(ev.verdict)) {
501
540
  glyph = React.createElement('span', { className: 'ab-notice-glyph-err' }, '✕')
502
541
  tag = verdictLabel(t, ev.verdict) || t('notice.rejectedDefault')
@@ -522,7 +561,7 @@ window.__ModuleLoader__.load({
522
561
  onClick: function () { setOpenId(open ? null : ev.id) },
523
562
  },
524
563
  React.createElement('div', { className: 'ab-row-top' },
525
- React.createElement('span', { className: 'ab-row-tool' }, ev.tool || 'tool'),
564
+ React.createElement('span', { className: 'ab-row-tool' }, ev.tool || t('history.unknownTool')),
526
565
  React.createElement('span', { className: tagCls }, tag),
527
566
  React.createElement('span', { className: 'ab-time' }, fmtTime(ev.ts)),
528
567
  React.createElement('span', { className: 'ab-row-chev' }, '▾'),
@@ -533,7 +572,7 @@ window.__ModuleLoader__.load({
533
572
  ? React.createElement('div', { className: 'ab-details' },
534
573
  DetailSection(t('detail.request'), [
535
574
  [t('detail.command'), args.command],
536
- [t('detail.path'), args.file_path || args.path],
575
+ [t('detail.path'), args.file_path != null ? args.file_path : args.path],
537
576
  [t('detail.description'), args.description],
538
577
  [t('detail.old'), args.old_string],
539
578
  [t('detail.new'), args.new_string],
@@ -544,23 +583,29 @@ window.__ModuleLoader__.load({
544
583
  [t('detail.script'), args.script],
545
584
  [t('detail.sql'), args.sql],
546
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],
547
592
  [t('detail.cwd'), ev.cwd],
548
- [t('detail.sandbox'), ev.mode],
593
+ [t('detail.workdir'), args.workdir && args.workdir !== ev.cwd ? args.workdir : undefined],
594
+ [t('detail.sandbox'), sandboxLabel(t, ev.mode)],
549
595
  [t('detail.justification'), ev.justification || ev.reason],
550
- ]),
596
+ ], t('detail.empty')),
551
597
  DetailSection(t('detail.audit'), [
552
598
  [t('detail.pipe'), pathLabel(t, ev.path) || ev.path],
553
599
  [t('detail.keyword'), ev.keyword],
554
600
  [t('detail.judgeModel'), [j.provider, j.model, j.effort].filter(Boolean).join(' / ')],
555
601
  [t('detail.judgeCategory'), j.failed
556
602
  ? t('detail.judgeFailed')
557
- : (j.label && j.criterion ? j.criterion + ' · ' + j.label : (j.criterion || ev.category || ''))],
603
+ : criterionLabel(t, j.criterion || ev.category, j.label)],
558
604
  [t('detail.judgeAction'), actionLabel(t, j.action) || j.action],
559
- [t('detail.judgeReason'), j.reason || ev.judgeReason],
560
- [t('detail.judgeError'), j.error],
605
+ [t('detail.judgeReason'), codedText(t, j.reason || ev.judgeReason)],
606
+ [t('detail.judgeError'), formatErr(t, j.errorCode || j.error, { ms: j.errorMs || '', error: j.errorDetail || '', effort: j.errorEffort || '' })],
561
607
  [t('detail.judgeRaw'), j.raw],
562
- [t('detail.ticket'), ev.ticket != null ? '#' + ev.ticket : ''],
563
- [t('detail.source'), ev.source],
608
+ [t('detail.source'), sourceLabel(t, ev.source)],
564
609
  ]),
565
610
  )
566
611
  : null,
@@ -571,7 +616,7 @@ window.__ModuleLoader__.load({
571
616
  )
572
617
  }
573
618
 
574
- /** 设置 → 自动审批。QQ 卡折在底部;恢复默认需连点两次。 */
619
+ /** 设置 → 自动审批。恢复默认需连点两次。 */
575
620
  function SettingsPage(props) {
576
621
  const t = tFrom(props)
577
622
  const rpc = props.rpc
@@ -584,22 +629,24 @@ window.__ModuleLoader__.load({
584
629
  const [newKeywordAction, setNewKeywordAction] = React.useState('human')
585
630
  const [newCriterion, setNewCriterion] = React.useState({ id: '', label: '', description: '', action: 'human' })
586
631
  const [timeoutMs, setTimeoutMs] = React.useState('20000')
587
- const [appId, setAppId] = React.useState('')
588
- const [appSecret, setAppSecret] = React.useState('')
589
- const [chatIdInput, setChatIdInput] = React.useState('')
590
- const [userIdInput, setUserIdInput] = React.useState('')
591
- const [timeoutSecs, setTimeoutSecs] = React.useState('120')
592
632
  const [models, setModels] = React.useState([])
593
633
  const [efforts, setEfforts] = React.useState([])
594
634
  const [judgeProvider, setJudgeProvider] = React.useState('')
595
635
  const [judgeModel, setJudgeModel] = React.useState('')
596
636
  const [judgeEffort, setJudgeEffort] = React.useState('')
597
637
  const [judgePromptLang, setJudgePromptLang] = React.useState('zh')
638
+ const [judgePromptDrafts, setJudgePromptDrafts] = React.useState({ zh: '', en: '' })
598
639
  const [presetSandbox, setPresetSandbox] = React.useState('workspace-write')
599
- const [foldOpen, setFoldOpen] = React.useState({ keywords: false, criteria: false, qq: false })
640
+ const [foldOpen, setFoldOpen] = React.useState({ keywords: false, criteria: false })
600
641
  const [kwEdit, setKwEdit] = React.useState(null)
601
642
  const [resetArmed, setResetArmed] = React.useState(null)
602
-
643
+ const feedbackTimerRef = React.useRef(null)
644
+ const kwSkipBlurRef = React.useRef(false)
645
+ React.useEffect(function () {
646
+ return function () {
647
+ if (feedbackTimerRef.current) clearTimeout(feedbackTimerRef.current)
648
+ }
649
+ }, [])
603
650
  const load = function (opts) {
604
651
  const keepEdits = opts && opts.keepEdits
605
652
  rpc('snapshot').then(function (data) {
@@ -610,10 +657,17 @@ window.__ModuleLoader__.load({
610
657
  setJudgeModel((data.plugin.judge && data.plugin.judge.model) || '')
611
658
  setJudgeEffort((data.plugin.judge && data.plugin.judge.reasoningEffort) || '')
612
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
+ })())
613
670
  setPresetSandbox((data.plugin && data.plugin.presetSandbox) === 'read-only' ? 'read-only' : 'workspace-write')
614
- setChatIdInput((data.plugin.notify && data.plugin.notify.chatId) || '')
615
- setUserIdInput((data.plugin.notify && data.plugin.notify.userId) || '')
616
- setTimeoutSecs(String((data.plugin.notify && data.plugin.notify.timeoutSecs) || 120))
617
671
  }
618
672
  setError(null)
619
673
  }).catch(function (e) {
@@ -627,49 +681,39 @@ window.__ModuleLoader__.load({
627
681
  return function () { clearTimeout(disarm) }
628
682
  }, [resetArmed])
629
683
 
630
- const provisioning = snapshot && snapshot.qq && snapshot.qq.provisioning ? snapshot.qq.provisioning : null
631
- const provisioningStatus = provisioning && provisioning.status ? provisioning.status : ''
632
- const provisioningActive = provisioningStatus === '登录中' || provisioningStatus === '等待扫码' || provisioningStatus === '保存凭据'
633
- const qqOpenedForProvision = React.useRef(false)
634
- React.useEffect(function () {
635
- if (provisioningActive && !qqOpenedForProvision.current) {
636
- qqOpenedForProvision.current = true
637
- setFoldOpen(function (s) { return Object.assign({}, s, { qq: true }) })
638
- }
639
- if (!provisioningActive) qqOpenedForProvision.current = false
640
- }, [provisioningActive])
641
- React.useEffect(function () {
642
- if (!provisioningActive) return
643
- const timer = setInterval(function () { load({ keepEdits: true }) }, 2000)
644
- return function () { clearInterval(timer) }
645
- }, [provisioningActive])
646
-
647
684
  React.useEffect(function () {
648
685
  const provider = judgeProvider || (snapshot && snapshot.fallback && snapshot.fallback.provider)
649
686
  if (!provider || !rpc) return
687
+ let alive = true
650
688
  rpc('judge-catalog', { provider: provider }).then(function (data) {
689
+ if (!alive) return
651
690
  setModels(data.models || [])
652
- }).catch(function () { setModels([]) })
691
+ }).catch(function () { if (alive) setModels([]) })
692
+ return function () { alive = false }
653
693
  }, [judgeProvider, snapshot, rpc])
654
694
 
655
695
  React.useEffect(function () {
656
696
  const provider = judgeProvider || (snapshot && snapshot.fallback && snapshot.fallback.provider)
657
697
  const model = judgeModel || (snapshot && snapshot.fallback && snapshot.fallback.model)
658
698
  if (!provider || !model || !rpc) { setEfforts([]); return }
699
+ let alive = true
659
700
  rpc('judge-info', { provider: provider, model: model }).then(function (data) {
701
+ if (!alive) return
660
702
  setEfforts(data.efforts || [])
661
- }).catch(function () { setEfforts([]) })
703
+ }).catch(function () { if (alive) setEfforts([]) })
704
+ return function () { alive = false }
662
705
  }, [judgeProvider, judgeModel, snapshot, rpc])
663
706
 
664
707
  const showFeedback = function (msg, ok) {
665
708
  setFeedback({ msg: String(msg), ok: ok !== false })
666
- setTimeout(function () { setFeedback(null) }, 4000)
709
+ if (feedbackTimerRef.current) clearTimeout(feedbackTimerRef.current)
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) {
@@ -686,9 +730,7 @@ window.__ModuleLoader__.load({
686
730
  const setup = snapshot.setup || { configured: false }
687
731
  const predefined = snapshot.predefined || {}
688
732
  const preDeny = new Set(predefined.rejectKeywords || predefined.denyKeywords || [])
689
- const qq = snapshot.qq || {}
690
733
  const plugin = snapshot.plugin || {}
691
- const notify = plugin.notify || {}
692
734
  const providers = snapshot.providers || []
693
735
  const fallback = snapshot.fallback || {}
694
736
  const judgeTimeoutLabel = Math.round((Number(cfg.judgeTimeoutMs) || 20000) / 1000) + 's'
@@ -723,14 +765,47 @@ window.__ModuleLoader__.load({
723
765
  return
724
766
  }
725
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
+ }
726
775
  run('rule-op', { op: 'reset', kind: kind, value: value }, okMsg)
727
776
  },
728
777
  }, armed ? t('set.resetConfirm') : idleLabel)
729
778
  }
730
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
+
731
803
  return React.createElement('div', { className: 'ab-set', ref: rootRef },
732
804
  React.createElement('h3', { className: 'ab-set-title' }, t('set.title')),
733
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,
734
809
  (cfg && cfg.corrupt)
735
810
  ? React.createElement('div', { className: 'ab-set-err' }, t('set.allowlistCorrupt'))
736
811
  : null,
@@ -739,14 +814,10 @@ window.__ModuleLoader__.load({
739
814
  t('set.pluginCorrupt'),
740
815
  React.createElement('button', {
741
816
  type: 'button', className: 'ab-set-btn ab-set-btn-danger', disabled: busy,
742
- onClick: function () { run('save-plugin', { overwriteCorrupt: true }, t('set.overwriteOk')) },
817
+ onClick: function () { run('save-plugin', { overwriteCorrupt: true }, t('set.overwriteOk'), { reload: true }) },
743
818
  }, t('set.pluginCorruptOverwrite')),
744
819
  )
745
820
  : null,
746
- (snapshot.qq && snapshot.qq.credsCorrupt)
747
- ? React.createElement('div', { className: 'ab-set-err' }, t('set.qqCorrupt'))
748
- : null,
749
-
750
821
  !setup.configured ? React.createElement('div', { className: 'ab-set-card' },
751
822
  React.createElement('div', { className: 'ab-set-card-head' },
752
823
  React.createElement('div', { className: 'ab-set-card-title' }, t('set.presetMissingTitle')),
@@ -759,6 +830,14 @@ window.__ModuleLoader__.load({
759
830
  React.createElement('span', { className: 'ab-set-note' }, t('set.presetWriteNote')),
760
831
  ),
761
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,
762
841
 
763
842
  React.createElement('div', { className: 'ab-set-card' },
764
843
  React.createElement('div', { className: 'ab-set-card-head' },
@@ -767,7 +846,13 @@ window.__ModuleLoader__.load({
767
846
  React.createElement('button', {
768
847
  type: 'button',
769
848
  className: 'ab-set-choice' + (presetSandbox === 'workspace-write' ? ' ab-set-choice-on' : ''),
770
- 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
+ },
771
856
  },
772
857
  React.createElement('span', { className: 'ab-set-choice-t' }, t('set.modeWs')),
773
858
  React.createElement('span', { className: 'ab-set-choice-d' }, t('set.modeWsHint')),
@@ -775,24 +860,19 @@ window.__ModuleLoader__.load({
775
860
  React.createElement('button', {
776
861
  type: 'button',
777
862
  className: 'ab-set-choice' + (presetSandbox === 'read-only' ? ' ab-set-choice-on' : ''),
778
- 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
+ },
779
870
  },
780
871
  React.createElement('span', { className: 'ab-set-choice-t' }, t('set.modeRo')),
781
872
  React.createElement('span', { className: 'ab-set-choice-d' }, t('set.modeRoHint')),
782
873
  ),
783
874
  ),
784
- React.createElement('div', { className: 'ab-set-row' },
785
- React.createElement('button', {
786
- type: 'button', className: 'ab-set-btn ab-set-btn-primary', disabled: busy,
787
- onClick: function () {
788
- run(
789
- 'save-plugin',
790
- { presetSandbox: presetSandbox },
791
- t('set.modeSaved'),
792
- )
793
- },
794
- }, t('set.save')),
795
- ),
875
+ React.createElement('p', { className: 'ab-set-note' }, t('set.modeHint')),
796
876
  ),
797
877
 
798
878
  React.createElement('div', { className: 'ab-set-card' },
@@ -822,7 +902,7 @@ window.__ModuleLoader__.load({
822
902
  return React.createElement('span', {
823
903
  className: 'ab-set-inv ' + (c.action === 'reject' ? 'ab-set-inv-hard' : c.action === 'allow' ? 'ab-set-inv-ok' : 'ab-set-inv-mode'),
824
904
  key: c.id, title: c.id,
825
- }, (c.label || c.id) + ' · ' + (actionLabel(t, c.action) || c.action))
905
+ }, (criterionLabel(t, c.id, c.label) || c.id) + ' · ' + (actionLabel(t, c.action) || c.action))
826
906
  }),
827
907
  ),
828
908
  ),
@@ -866,12 +946,19 @@ window.__ModuleLoader__.load({
866
946
  defaultValue: row.text,
867
947
  autoFocus: true,
868
948
  onBlur: function (e) {
949
+ if (kwSkipBlurRef.current) {
950
+ kwSkipBlurRef.current = false
951
+ return
952
+ }
869
953
  commitKw(e.target.value, row.action)
870
954
  setKwEdit(null)
871
955
  },
872
956
  onKeyDown: function (e) {
873
957
  if (e.key === 'Enter') e.currentTarget.blur()
874
- if (e.key === 'Escape') setKwEdit(null)
958
+ if (e.key === 'Escape') {
959
+ kwSkipBlurRef.current = true
960
+ setKwEdit(null)
961
+ }
875
962
  },
876
963
  })
877
964
  : React.createElement('span', {
@@ -922,8 +1009,10 @@ window.__ModuleLoader__.load({
922
1009
  React.createElement('button', {
923
1010
  type: 'button', className: 'ab-set-btn', disabled: busy || !newKeyword.trim(),
924
1011
  onClick: function () {
925
- run('rule-op', { op: 'add', kind: 'keywords', value: { text: newKeyword.trim(), action: newKeywordAction } })
926
- 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
+ })
927
1016
  },
928
1017
  }, t('set.add')),
929
1018
  ),
@@ -961,7 +1050,7 @@ window.__ModuleLoader__.load({
961
1050
  }
962
1051
  return React.createElement('div', { className: 'ab-set-item ab-set-item-crit', key: c.id },
963
1052
  React.createElement('div', { className: 'ab-set-crit-top' },
964
- React.createElement('span', { className: 'ab-set-item-id' }, c.id),
1053
+ React.createElement('span', { className: 'ab-set-item-id', title: c.id }, criterionLabel(t, c.id) || c.id),
965
1054
  React.createElement('select', {
966
1055
  className: 'ab-set-select', value: c.action || 'human',
967
1056
  onChange: function (e) { commitCrit({ action: e.target.value }) },
@@ -986,6 +1075,7 @@ window.__ModuleLoader__.load({
986
1075
  ),
987
1076
  React.createElement('div', { className: 'ab-set-item-fields' },
988
1077
  React.createElement('input', {
1078
+ key: c.id + ':label:' + (c.label || ''),
989
1079
  className: 'ab-set-input ab-set-input-ui',
990
1080
  defaultValue: c.label || '',
991
1081
  placeholder: t('set.labelPlaceholder'),
@@ -997,6 +1087,7 @@ window.__ModuleLoader__.load({
997
1087
  onKeyDown: function (e) { if (e.key === 'Enter') e.currentTarget.blur() },
998
1088
  }),
999
1089
  React.createElement('textarea', {
1090
+ key: c.id + ':desc:' + (c.description || ''),
1000
1091
  className: 'ab-set-textarea',
1001
1092
  defaultValue: c.description || '',
1002
1093
  placeholder: t('set.descPlaceholder'),
@@ -1050,8 +1141,10 @@ window.__ModuleLoader__.load({
1050
1141
  type: 'button', className: 'ab-set-btn ab-set-btn-primary',
1051
1142
  disabled: busy || !(newCriterion.id || newCriterion.label),
1052
1143
  onClick: function () {
1053
- run('rule-op', { op: 'add', kind: 'criteria', value: newCriterion })
1054
- 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
+ })
1055
1148
  },
1056
1149
  }, t('set.addCriterion')),
1057
1150
  ),
@@ -1105,15 +1198,6 @@ window.__ModuleLoader__.load({
1105
1198
  return React.createElement('option', { value: e.id, key: e.id }, e.name || e.id)
1106
1199
  }),
1107
1200
  ),
1108
- React.createElement('button', {
1109
- type: 'button', className: 'ab-set-btn ab-set-btn-primary', disabled: busy,
1110
- onClick: function () {
1111
- run('save-plugin', {
1112
- judgePromptLang: judgePromptLang,
1113
- judge: { provider: judgeProvider, model: judgeModel, reasoningEffort: judgeEffort },
1114
- }, t('set.judgeSaved'))
1115
- },
1116
- }, t('set.save')),
1117
1201
  ),
1118
1202
  React.createElement('div', { className: 'ab-set-row' },
1119
1203
  React.createElement('span', { className: 'ab-set-item-meta' }, t('set.judgeLang')),
@@ -1124,130 +1208,173 @@ window.__ModuleLoader__.load({
1124
1208
  React.createElement('option', { value: 'zh' }, t('set.judgeLangZh')),
1125
1209
  React.createElement('option', { value: 'en' }, t('set.judgeLangEn')),
1126
1210
  ),
1127
- 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
+ }),
1128
1240
  ),
1129
1241
  React.createElement('div', { className: 'ab-set-row' },
1130
1242
  React.createElement('span', { className: 'ab-set-item-meta' }, t('set.judgeTimeoutMs')),
1131
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')),
1132
1247
  React.createElement('button', {
1133
- type: 'button', className: 'ab-set-btn', disabled: busy,
1134
- 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,
1135
1250
  }, t('set.save')),
1136
1251
  ),
1137
1252
  ),
1138
1253
 
1139
- React.createElement('details', {
1140
- className: 'ab-set-card ab-set-fold', 'data-ab-stage': 'qq',
1141
- open: foldOpen.qq,
1142
- onToggle: function (e) {
1143
- const open = e.currentTarget.open
1144
- setFoldOpen(function (prev) { return Object.assign({}, prev, { qq: open }) })
1145
- },
1146
- },
1147
- React.createElement('summary', null,
1148
- React.createElement('div', { className: 'ab-set-card-head' },
1149
- React.createElement('div', { className: 'ab-set-card-title' },
1150
- t('set.qqTitle'),
1151
- React.createElement('span', { className: 'ab-set-fold-chev' }, '▾')),
1152
- React.createElement('p', { className: 'ab-set-card-sub' }, t('set.qqSub'))),
1153
- React.createElement('div', { className: 'ab-set-fold-sum' },
1154
- (provisionLabel(t, qq.status) || t('set.qqUnknown')) + ' · ' + (notify.chatId ? t('set.qqChatOn') : t('set.qqChatOff')))),
1155
- React.createElement('div', { className: 'ab-set-fold-body' },
1156
- (provisioning && provisioning.error && !provisioningActive)
1157
- ? React.createElement('p', { className: 'ab-set-err' }, (provisionLabel(t, provisioningStatus) || t('set.qqScanFail')) + ':' + provisioning.error)
1158
- : null,
1159
- provisioningActive
1160
- ? React.createElement('div', null,
1161
- React.createElement('p', { className: 'ab-set-card-sub' }, t('set.qqScanStatus', { status: provisionLabel(t, provisioningStatus) || provisioningStatus })),
1162
- provisioning.qrDataUrl
1163
- ? React.createElement('div', { className: 'ab-set-qr' },
1164
- React.createElement('img', { src: provisioning.qrDataUrl, alt: t('set.qqQrAlt') }),
1165
- React.createElement('p', { className: 'ab-set-qr-tip' }, t('set.qqQrTip')),
1166
- )
1167
- : null,
1168
- React.createElement('div', { className: 'ab-set-row' },
1169
- React.createElement('button', {
1170
- type: 'button', className: 'ab-set-btn', disabled: busy,
1171
- onClick: function () { run('qq-cancel-provision', {}, t('set.qqCancelOk')) },
1172
- }, t('set.qqCancelScan')),
1173
- ),
1174
- )
1175
- : React.createElement('div', { className: 'ab-set-row' },
1176
- React.createElement('button', {
1177
- type: 'button',
1178
- className: 'ab-set-btn' + (qq.hasSecret ? '' : ' ab-set-btn-primary'),
1179
- disabled: busy,
1180
- onClick: function () { run('qq-provision', {}, t('set.qqScanStart')) },
1181
- }, qq.hasSecret ? t('set.qqRescan') : t('set.qqScan')),
1182
- provisioningStatus === '已连接'
1183
- ? React.createElement('span', { className: 'ab-set-ok' }, t('set.qqScanOk'))
1184
- : null,
1185
- ),
1186
- React.createElement('div', { className: 'ab-set-row' },
1187
- React.createElement('input', { className: 'ab-set-input ab-set-input-grow', placeholder: t('set.qqAppId'), value: appId, onChange: function (e) { setAppId(e.target.value) } }),
1188
- React.createElement('input', { className: 'ab-set-input ab-set-input-grow', type: 'password', placeholder: qq.hasSecret ? t('set.qqSecretKeep') : t('set.qqAppSecret'), value: appSecret, onChange: function (e) { setAppSecret(e.target.value) } }),
1189
- React.createElement('button', { type: 'button', className: 'ab-set-btn' + (qq.hasSecret ? ' ab-set-btn-primary' : ''), disabled: busy, onClick: function () { run('save-qq-creds', { appId: appId, appSecret: appSecret }, t('set.qqCredsReconnect')) } }, t('set.qqSaveConnect')),
1190
- React.createElement('button', { type: 'button', className: 'ab-set-btn', disabled: busy, onClick: function () { run('reconnect-qq', {}, t('set.qqReconnected')) } }, t('set.qqReconnect')),
1191
- ),
1192
- React.createElement('div', { className: 'ab-set-row' },
1193
- React.createElement('span', { className: 'ab-set-item-meta' }, t('set.qqPush', { state: notify.enabled !== false ? t('set.qqPushOn') : t('set.qqPushOff') })),
1194
- React.createElement('button', {
1195
- type: 'button', className: 'ab-set-btn', disabled: busy,
1196
- onClick: function () { run('save-plugin', { notify: { enabled: notify.enabled === false } }, t('set.qqPushToggled')) },
1197
- }, notify.enabled === false ? t('set.qqEnablePush') : t('set.qqPausePush')),
1198
- React.createElement('span', { className: 'ab-set-item-meta' }, t('set.qqCurrentChat', { chatId: notify.chatId || t('set.qqUnset') })),
1199
- ),
1200
- React.createElement('div', { className: 'ab-set-row' },
1201
- React.createElement('input', {
1202
- className: 'ab-set-input ab-set-input-grow', placeholder: t('set.qqChatPlaceholder'),
1203
- value: chatIdInput, onChange: function (e) { setChatIdInput(e.target.value) },
1204
- }),
1205
- React.createElement('input', {
1206
- className: 'ab-set-input', placeholder: t('set.qqGroupUser'),
1207
- value: userIdInput, onChange: function (e) { setUserIdInput(e.target.value) },
1208
- }),
1209
- React.createElement('input', {
1210
- className: 'ab-set-input ab-set-input-num', type: 'number', min: 30, placeholder: t('set.qqTimeoutS'),
1211
- value: timeoutSecs, onChange: function (e) { setTimeoutSecs(e.target.value) },
1212
- }),
1213
- React.createElement('button', {
1214
- type: 'button', className: 'ab-set-btn', disabled: busy,
1215
- onClick: function () {
1216
- run('save-plugin', {
1217
- notify: {
1218
- enabled: notify.enabled !== false,
1219
- chatId: chatIdInput.trim(),
1220
- userId: userIdInput.trim(),
1221
- timeoutSecs: Number(timeoutSecs) || 120,
1222
- },
1223
- }, t('set.targetSaved'))
1224
- },
1225
- }, t('set.saveTarget')),
1226
- ),
1227
- React.createElement('p', { className: 'ab-set-card-sub' }, t('set.recent')),
1228
- (!qq.recentChats || qq.recentChats.length === 0)
1229
- ? React.createElement('p', { className: 'ab-set-empty' }, t('set.noMail'))
1230
- : React.createElement('div', { className: 'ab-set-list' },
1231
- qq.recentChats.map(function (c) {
1232
- return React.createElement('div', {
1233
- className: 'ab-set-item ab-chat', key: c.chatId + c.userId,
1234
- onClick: function () { run('set-chat', { chatId: c.chatId, userId: c.isGroup ? c.userId : '' }, t('set.chatPicked')) },
1235
- },
1236
- React.createElement('span', { className: 'ab-set-item-label' },
1237
- (c.isGroup ? t('set.group') + ' ' : t('set.c2c') + ' ') + (c.username || c.userId) + ' · ' + c.chatId),
1238
- React.createElement('span', { className: 'ab-set-item-meta' }, c.preview || ''),
1239
- )
1240
- }),
1241
- ),
1242
- ),
1243
- ),
1244
-
1245
- feedback
1246
- ? React.createElement('div', { className: feedback.ok ? 'ab-set-ok' : 'ab-set-err' }, feedback.msg)
1247
- : null,
1248
1254
  )
1249
1255
  }
1250
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
+
1251
1378
  const plugin = {
1252
1379
  inject: ['connection', 'slots', 'locale'],
1253
1380
  async apply(ctx) {
@@ -1273,11 +1400,19 @@ window.__ModuleLoader__.load({
1273
1400
  }
1274
1401
  }
1275
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
+ })
1276
1411
 
1277
1412
  slots.inject('conversation.input.dock', function () {
1278
1413
  return slots.register(
1279
1414
  { name: 'conversation.input.dock', id: 'dsh-auto-approve.notice', order: 30, locale: NS, label: function () { return t('slot.notice') } },
1280
- function (props) { return React.createElement(NoticeStrip, { slotsProps: props, rpc: rpc }) },
1415
+ function (props) { return React.createElement(NoticeStrip, { slotsProps: props, rpc: rpc, t: t }) },
1281
1416
  )
1282
1417
  })
1283
1418
 
@@ -1291,14 +1426,14 @@ window.__ModuleLoader__.load({
1291
1426
  label: function () { return t('slot.history') },
1292
1427
  inject: (sessionId) => ({ sessionId }),
1293
1428
  },
1294
- function (props) { return React.createElement(HistoryView, { slotsProps: props, rpc: rpc }) },
1429
+ function (props) { return React.createElement(HistoryView, { slotsProps: props, rpc: rpc, t: t }) },
1295
1430
  )
1296
1431
  })
1297
1432
 
1298
1433
  slots.inject('settings.section', function () {
1299
1434
  return slots.register(
1300
1435
  { name: 'settings.section', id: 'dsh-auto-approve.settings', order: 55, locale: NS, label: function () { return t('slot.settings') } },
1301
- function (props) { return React.createElement(SettingsPage, { slotsProps: props, rpc: rpc }) },
1436
+ function (props) { return React.createElement(SettingsPage, { slotsProps: props, rpc: rpc, t: t }) },
1302
1437
  )
1303
1438
  })
1304
1439
  },