@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/CHANGELOG.md +64 -0
- package/README.md +21 -12
- package/README.zh.md +21 -10
- package/client.js +300 -54
- package/locales.mjs +40 -4
- package/package.json +3 -2
- package/src/index.mjs +99 -16
- package/src/preset-patch.mjs +331 -76
- package/src/rules.mjs +214 -70
- package/src/util.mjs +43 -2
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":"
|
|
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
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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')
|
|
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
|
-
|
|
920
|
-
|
|
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'
|
|
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
|
-
|
|
1048
|
-
|
|
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-
|
|
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:
|
|
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(
|