@dnalec/dsh-auto-approve 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js ADDED
@@ -0,0 +1,1312 @@
1
+ /**
2
+ * dsh-auto-approve — 浏览器半:提示条、审批时间线、设置页。
3
+ *
4
+ * 纯 JS:无 TS / JSX / import。React 用 createElement。
5
+ * 网页文案:locales.mjs 的 zh/en,apply 时 ctx.locale.register。
6
+ * Host API 走 connection.rpc.call('/api', 'dsh-auto-approve', …)(已鉴权),不用裸 HTTP。
7
+ * 设置页折叠状态用 React state 控 details,避免 snapshot 刷新后合上。
8
+ * 恢复默认:第一次点武装,5 秒内再点才执行。
9
+ */
10
+ window.__ModuleLoader__.load({
11
+ id: 'dsh-auto-approve',
12
+ factory: (require) => {
13
+ var module = { exports: {} }
14
+ var exports = module.exports
15
+ const React = require('react')
16
+
17
+ const CSS = `
18
+ .ab-notice{box-sizing:border-box;width:calc(100% - var(--dsh-composer-side-clearance) - var(--dsh-composer-side-clearance) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset));max-width:calc(var(--dsh-composer-card-max-width) - var(--dsh-composer-dock-inset) - var(--dsh-composer-dock-inset));margin:0 auto;flex:none}
19
+ .ab-notice-card{box-sizing:border-box;display:flex;align-items:center;gap:10px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:6px 10px 6px 12px;box-shadow:var(--dsw-shadow-lv1)}
20
+ .ab-notice-card-pending{border-color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-state-warn-tertiary)}
21
+ .ab-notice-card-manual{border-color:var(--dsw-alias-state-warn-primary)}
22
+ .ab-notice-card-err{border-color:var(--dsw-alias-state-error-primary)}
23
+ .ab-notice-glyph{color:var(--dsw-alias-state-success-primary);flex:none;display:inline-flex;align-items:center;justify-content:center}
24
+ .ab-notice-glyph-warn{color:var(--dsw-alias-state-warn-label);flex:none;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:16px;width:16px;height:16px}
25
+ .ab-notice-glyph-err{color:var(--dsw-alias-state-error-primary);flex:none;display:inline-flex;align-items:center;justify-content:center;font-size:13px;line-height:16px;width:16px;height:16px}
26
+ .ab-notice-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:1px}
27
+ .ab-notice-head{display:flex;align-items:center;gap:8px;min-width:0}
28
+ .ab-notice-tool{flex:none;color:var(--dsw-alias-label-primary);font:500 12px/18px var(--ds-font-family-code)}
29
+ .ab-notice-text{flex:1 1 auto;min-width:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}
30
+ .ab-notice-meta{display:flex;align-items:center;gap:8px}
31
+ .ab-tag{box-sizing:border-box;flex:none;height:18px;color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-tertiary);border-radius:9px;align-items:center;padding:0 8px;font-size:11px;line-height:18px;display:inline-flex;letter-spacing:.02em}
32
+ .ab-tag-neutral{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-module-platform)}
33
+ .ab-tag-warn{box-sizing:border-box;flex:none;height:18px;color:var(--dsw-alias-state-warn-label);background:var(--dsw-alias-state-warn-tertiary);border-radius:9px;align-items:center;padding:0 8px;font-size:11px;line-height:18px;display:inline-flex}
34
+ .ab-tag-err{box-sizing:border-box;flex:none;height:18px;color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger);border-radius:9px;align-items:center;padding:0 8px;font-size:11px;line-height:18px;display:inline-flex}
35
+ .ab-time{flex:none;color:var(--dsw-alias-label-caption);font-size:11px;line-height:16px;font-variant-numeric:tabular-nums}
36
+ .ab-notice-close{width:24px;height:24px;flex:none;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:transparent;border:none;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;padding:0}
37
+ .ab-notice-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}
38
+ .ab-view{box-sizing:border-box;height:100%;min-height:0;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);position:relative}
39
+ .ab-view-head{box-sizing:border-box;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2);padding:10px 14px 8px;display:flex;flex-direction:column;gap:2px}
40
+ .ab-view-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px}
41
+ .ab-view-sub{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
42
+ .ab-list{flex:1 1 auto;min-height:0;overflow-y:auto;padding:8px 14px 48px;display:flex;flex-direction:column;gap:2px}
43
+ .ab-row{box-sizing:border-box;display:flex;gap:10px;padding:9px 10px;border-radius:10px;width:100%;text-align:left;background:transparent;font:inherit;color:inherit}
44
+ .ab-row-open{background:var(--dsw-alias-interactive-bg-hover)}
45
+ .ab-row-rail{flex:none;display:flex;flex-direction:column;align-items:center;gap:4px;padding-top:2px}
46
+ .ab-row-line{flex:none;width:1px;background:var(--dsw-alias-border-l1);min-height:10px}
47
+ .ab-row:last-child .ab-row-line{display:none}
48
+ .ab-row-body{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:4px}
49
+ .ab-row-head{box-sizing:border-box;margin:0;padding:0;border:none;background:transparent;cursor:pointer;display:flex;flex-direction:column;gap:4px;width:100%;text-align:left;font:inherit;color:inherit}
50
+ .ab-row-head:hover .ab-row-tool{color:var(--dsw-alias-state-business-primary)}
51
+ .ab-row-top{display:flex;align-items:center;gap:8px;min-width:0}
52
+ .ab-row-tool{flex:none;color:var(--dsw-alias-label-primary);font:500 12px/18px var(--ds-font-family-code)}
53
+ .ab-row-chev{flex:none;margin-left:auto;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}
54
+ .ab-row-open .ab-row-chev{transform:rotate(180deg)}
55
+ .ab-row-reason{flex:1 1 auto;min-width:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;word-break:break-all;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
56
+ .ab-row-open .ab-row-reason{display:none}
57
+ .ab-details{flex-direction:column;gap:10px;padding:8px 0 2px;display:flex}
58
+ .ab-detail-sec{display:flex;flex-direction:column;gap:6px}
59
+ .ab-detail-h{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;letter-spacing:.04em}
60
+ .ab-detail-grid{display:grid;grid-template-columns:72px minmax(0,1fr);gap:4px 10px;align-items:start}
61
+ .ab-detail-k{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px;padding-top:1px}
62
+ .ab-detail-v{margin:0;color:var(--dsw-alias-label-primary);font:12px/18px var(--ds-font-family-code);white-space:pre-wrap;word-break:break-all}
63
+ .ab-detail-v-box{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-module-platform));border-radius:8px;padding:6px 8px;max-height:180px;overflow:auto}
64
+ .ab-empty,.ab-loading{flex:1 1 auto;color:var(--dsw-alias-label-tertiary);display:flex;align-items:center;justify-content:center;font-size:13px;line-height:20px;padding:24px}
65
+ .ab-set{box-sizing:border-box;width:100%;max-width:720px;padding:0 0 28px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:12px;display:flex}
66
+ .ab-set-title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:500;line-height:24px}
67
+ .ab-set-intro{color:var(--dsw-alias-label-tertiary);margin:0;font-size:14px;line-height:22px}
68
+ .ab-set-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;flex-direction:column;gap:12px;padding:12px 14px;display:flex}
69
+ .ab-set-fold{padding:0}
70
+ .ab-set-fold>summary{list-style:none;cursor:pointer;display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:12px 14px}
71
+ .ab-set-fold>summary::-webkit-details-marker{display:none}
72
+ .ab-set-fold>summary:hover{background:var(--dsw-alias-interactive-bg-hover);border-radius:12px}
73
+ .ab-set-fold[open]>summary{border-bottom:1px solid var(--dsw-alias-border-l2);border-radius:12px 12px 0 0}
74
+ .ab-set-fold-body{padding:12px 14px 14px;display:flex;flex-direction:column;gap:12px}
75
+ .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
+ .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-card-head{flex-direction:column;gap:4px;display:flex}
79
+ .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
+ .ab-set-card-sub{color:var(--dsw-alias-label-tertiary);margin:0;font-size:13px;line-height:20px}
81
+ .ab-set-note{color:var(--dsw-alias-state-warn-label);margin:0;font-size:13px;line-height:20px}
82
+ .ab-set-ok{color:var(--dsw-alias-state-success-primary);margin:0;font-size:13px;line-height:20px}
83
+ .ab-set-err{color:var(--dsw-alias-state-error-primary);margin:0;font-size:13px;line-height:20px}
84
+ .ab-set-row{flex-wrap:wrap;gap:8px;display:flex;align-items:center}
85
+ .ab-set-row-choices{align-items:stretch;flex-wrap:nowrap}
86
+ .ab-set-input,.ab-set-select{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:32px;min-width:0;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:0 10px;font-size:13px;line-height:20px;font-family:var(--ds-font-family-code)}
87
+ .ab-set-input:focus,.ab-set-select:focus{border-color:var(--dsw-alias-state-business-primary)}
88
+ .ab-set-input-num{width:96px}
89
+ .ab-set-input-grow{flex:1 1 180px}
90
+ .ab-set-btn{box-sizing:border-box;height:32px;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;border:1px solid var(--dsw-alias-border-l2);background:transparent;border-radius:16px;justify-content:center;align-items:center;padding:0 14px;font-size:13px;line-height:20px;display:inline-flex;white-space:nowrap}
91
+ .ab-set-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
92
+ .ab-set-btn:disabled{opacity:.4;cursor:default}
93
+ .ab-set-btn-primary{border:none;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}
94
+ .ab-set-btn-danger{color:var(--dsw-alias-label-primary-foreground);border:none;background:var(--dsw-alias-state-error-primary)}
95
+ .ab-set-list{flex-direction:column;gap:4px;display:flex}
96
+ .ab-set-item{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:8px;align-items:center;gap:8px;padding:6px 10px;min-width:0;display:flex}
97
+ .ab-set-item-kw{padding:4px 8px}
98
+ .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
+ .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
+ .ab-set-textarea:focus{border-color:var(--dsw-alias-state-business-primary)}
101
+ .ab-set-input-ui{font-family:inherit;width:100%}
102
+ .ab-set-foot{justify-content:flex-end}
103
+ .ab-set-list-crit{gap:8px}
104
+ .ab-set-item-crit{flex-direction:column;align-items:stretch;gap:8px;padding:10px 12px}
105
+ .ab-set-crit-top{display:flex;align-items:center;gap:8px;min-width:0}
106
+ .ab-set-item-fields{min-width:0;flex-direction:column;gap:6px;display:flex}
107
+ .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
+ .ab-set-item-meta{flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;white-space:nowrap}
110
+ .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
+ .ab-set-item-del:hover{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary)}
112
+ .ab-set-empty{color:var(--dsw-alias-label-caption);margin:0;font-size:13px;line-height:20px;padding:4px 2px}
113
+ .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
+ .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)}
118
+ .ab-set-steps{display:flex;flex-wrap:wrap;gap:6px}
119
+ .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
+ .ab-set-step:hover{background:var(--dsw-alias-interactive-bg-hover)}
121
+ .ab-set-step:focus{outline:none;border-color:var(--dsw-alias-state-business-primary)}
122
+ .ab-set-step-k{color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;font-size:11px;line-height:16px;display:flex}
123
+ .ab-set-step-v{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:18px;word-break:break-all}
124
+ .ab-set-invs{flex-wrap:wrap;gap:6px;display:flex}
125
+ .ab-set-inv{box-sizing:border-box;border-radius:8px;padding:6px 10px;font-size:12px;line-height:18px;display:inline-flex;align-items:center;max-width:100%}
126
+ .ab-set-inv-hard{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover-danger)}
127
+ .ab-set-inv-ok{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-tertiary)}
128
+ .ab-set-inv-mode{color:var(--dsw-alias-state-warn-label);background:var(--dsw-alias-state-warn-tertiary)}
129
+ .ab-set-choice{box-sizing:border-box;margin:0;flex:1 1 0;min-width:0;border:1px solid var(--dsw-alias-border-l2);background:transparent;border-radius:10px;padding:10px 12px;cursor:pointer;text-align:left;font:inherit;color:inherit;display:flex;flex-direction:column;gap:4px}
130
+ .ab-set-choice:hover{background:var(--dsw-alias-interactive-bg-hover)}
131
+ .ab-set-choice-on{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
132
+ .ab-set-choice-t{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}
133
+ .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}
141
+ `
142
+
143
+ 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"}`)
146
+
147
+ function fillLocale(template, params) {
148
+ if (!params) return template
149
+ return String(template).replace(/\{(\w+)\}/g, function (m, name) {
150
+ return Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : m
151
+ })
152
+ }
153
+
154
+ function tFrom(props) {
155
+ const sp = (props && props.slotsProps) || {}
156
+ if (typeof sp.t === 'function') return sp.t
157
+ return function (key, params) {
158
+ return fillLocale(ZH[key] || key, params)
159
+ }
160
+ }
161
+
162
+ function verdictLabel(t, verdict) {
163
+ if (!verdict) return t('notice.autoDefault')
164
+ const key = 'verdict.' + verdict
165
+ const v = t(key)
166
+ return v === key ? String(verdict) : v
167
+ }
168
+
169
+ 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
174
+ }
175
+
176
+ 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
+ }
182
+
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 || '')
196
+ }
197
+
198
+ function countsLabel(t, reject, human, allow) {
199
+ return t('set.counts', { reject: String(reject), human: String(human), allow: String(allow) })
200
+ }
201
+
202
+ function isAutoReject(verdict) {
203
+ const v = String(verdict || '')
204
+ return v === 'keyword-reject' || v === 'criteria-reject' || /-reject$/.test(v)
205
+ }
206
+
207
+ function eventPreview(ev) {
208
+ const a = (ev && ev.args) || {}
209
+ return a.command || a.file_path || a.path || a.code || a.url || a.script || a.sql || a.prompt || a.description || ev.justification || ev.reason || ''
210
+ }
211
+
212
+ function hasVal(value) {
213
+ return value !== undefined && value !== null && value !== ''
214
+ }
215
+
216
+ function isLongVal(value) {
217
+ const s = String(value)
218
+ return s.length > 72 || s.indexOf('\n') !== -1
219
+ }
220
+
221
+ function Detail(label, value) {
222
+ if (!hasVal(value)) return null
223
+ const long = isLongVal(value)
224
+ return [
225
+ React.createElement('div', { className: 'ab-detail-k', key: label + '-k' }, label),
226
+ React.createElement(long ? 'pre' : 'div', {
227
+ className: 'ab-detail-v' + (long ? ' ab-detail-v-box' : ''),
228
+ key: label + '-v',
229
+ }, String(value)),
230
+ ]
231
+ }
232
+
233
+ function DetailSection(title, items) {
234
+ const kids = []
235
+ for (let i = 0; i < items.length; i++) {
236
+ const row = items[i]
237
+ if (!row) continue
238
+ const node = Detail(row[0], row[1])
239
+ if (node) kids.push(node[0], node[1])
240
+ }
241
+ if (!kids.length) return null
242
+ return React.createElement('div', { className: 'ab-detail-sec' },
243
+ React.createElement('div', { className: 'ab-detail-h' }, title),
244
+ React.createElement('div', { className: 'ab-detail-grid' }, kids),
245
+ )
246
+ }
247
+
248
+ function sizeTextarea(el) {
249
+ if (!el) return
250
+ el.style.height = 'auto'
251
+ el.style.height = Math.max(72, el.scrollHeight) + 'px'
252
+ }
253
+
254
+ function fmtTime(iso) {
255
+ try {
256
+ const d = new Date(iso)
257
+ const p = (n) => String(n).padStart(2, '0')
258
+ return p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
259
+ } catch (e) { return String(iso || '') }
260
+ }
261
+
262
+ function GlyphCheck() {
263
+ return React.createElement('svg', {
264
+ width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true,
265
+ },
266
+ React.createElement('circle', { cx: 8, cy: 8, r: 7, fill: 'var(--dsw-alias-state-success-tertiary)' }),
267
+ React.createElement('path', { d: 'M5 8.3L7.1 10.4L11 6', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }),
268
+ )
269
+ }
270
+
271
+ function resolveFrame(sp) {
272
+ if (!sp) return { sessionId: null }
273
+ if (sp.sessionId) return { sessionId: sp.sessionId }
274
+ if (sp.session && sp.session.id) return { sessionId: sp.session.id }
275
+ return { sessionId: null }
276
+ }
277
+
278
+ function latestUnsettledPending(evs) {
279
+ const settled = Object.create(null)
280
+ 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
287
+ }
288
+ return null
289
+ }
290
+
291
+ function makeRpc(connection, t) {
292
+ const tr = typeof t === 'function' ? t : function (key) { return ZH[key] || key }
293
+ return function rpc(endpoint, payload) {
294
+ if (!connection || !connection.rpc || typeof connection.rpc.call !== 'function') {
295
+ return Promise.reject(new Error(tr('rpc.unavailable')))
296
+ }
297
+ return connection.rpc.call('/api', 'dsh-auto-approve', {
298
+ endpoint: endpoint,
299
+ payload: payload || {},
300
+ }).then(function (r) {
301
+ 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)
304
+ }
305
+ return r.value
306
+ })
307
+ }
308
+ }
309
+
310
+ /** 输入框上方的自动放行 / 拒绝 / 等待人工条。 */
311
+ function NoticeStrip(props) {
312
+ const t = tFrom(props)
313
+ const rpc = props.rpc
314
+ const frame = resolveFrame(props.slotsProps || {})
315
+ const sessionId = frame.sessionId
316
+ const [notice, setNotice] = React.useState(null)
317
+ const sinceRef = React.useRef(0)
318
+ const lastShownIdRef = React.useRef(0)
319
+ const hideTimerRef = React.useRef(null)
320
+
321
+ React.useEffect(function () {
322
+ let alive = true
323
+ let timer = null
324
+ sinceRef.current = 0
325
+ lastShownIdRef.current = 0
326
+ setNotice(null)
327
+ if (!sessionId || !rpc) return
328
+
329
+ const startPolling = function () {
330
+ if (!alive) return
331
+ timer = setInterval(function () {
332
+ rpc('events', { sessionId: sessionId, since: sinceRef.current }).then(function (data) {
333
+ if (!alive) return
334
+ const evs = (data && data.events) || []
335
+ if (evs.length === 0) return
336
+ const last = evs[evs.length - 1]
337
+ if (!last || last.id <= lastShownIdRef.current) return
338
+ lastShownIdRef.current = last.id
339
+ sinceRef.current = last.id
340
+ const kind = last.kind || 'auto'
341
+ setNotice(last)
342
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current)
343
+ if (kind !== 'manual-pending') {
344
+ const hold = kind === 'auto' ? 8000 : 4000
345
+ hideTimerRef.current = setTimeout(function () { setNotice(null) }, hold)
346
+ }
347
+ }).catch(function () {
348
+ if (!alive) return
349
+ setNotice(function (cur) {
350
+ if (cur && cur.kind && cur.kind !== 'feed-error') return cur
351
+ return { kind: 'feed-error', code: 'notice.feedError' }
352
+ })
353
+ })
354
+ }, 2000)
355
+ }
356
+
357
+ rpc('events', { sessionId: sessionId, since: 0 }).then(function (data) {
358
+ if (!alive) return
359
+ const evs = (data && data.events) || []
360
+ if (evs.length > 0) {
361
+ const last = evs[evs.length - 1]
362
+ sinceRef.current = last.id
363
+ lastShownIdRef.current = last.id
364
+ const pending = latestUnsettledPending(evs)
365
+ if (pending) setNotice(pending)
366
+ }
367
+ startPolling()
368
+ }).catch(function () {
369
+ if (!alive) return
370
+ setNotice({ kind: 'feed-error', code: 'notice.feedLoadFailed' })
371
+ startPolling()
372
+ })
373
+
374
+ return function () {
375
+ alive = false
376
+ if (timer) clearInterval(timer)
377
+ if (hideTimerRef.current) clearTimeout(hideTimerRef.current)
378
+ }
379
+ }, [sessionId, rpc])
380
+
381
+ if (!notice) return null
382
+ const kind = notice.kind || 'auto'
383
+ const isPending = kind === 'manual-pending'
384
+ const isManual = kind === 'manual-approved' || kind === 'manual-rejected'
385
+ const ticket = notice.ticket ? (' #' + notice.ticket) : ''
386
+ let title = ''
387
+ let tagText = ''
388
+ let glyph = null
389
+ if (kind === 'feed-error') {
390
+ title = t(notice.code || 'notice.feedError')
391
+ tagText = t('notice.feedErrorTag')
392
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '!')
393
+ } else if (isPending) {
394
+ title = t('notice.pendingTitle', { ticket: ticket, preview: eventPreview(notice) })
395
+ tagText = t('notice.pendingTag', { ticket: ticket })
396
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '◔')
397
+ } 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 })
400
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '✓')
401
+ } else if (kind === 'manual-rejected' || isAutoReject(notice.verdict)) {
402
+ title = t('notice.rejectedTitle', { ticket: ticket, preview: eventPreview(notice) })
403
+ tagText = kind === 'manual-rejected'
404
+ ? t('notice.rejectedTag', { ticket: ticket })
405
+ : (verdictLabel(t, notice.verdict) || t('notice.rejectedDefault'))
406
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-err' }, '✕')
407
+ } else {
408
+ title = eventPreview(notice)
409
+ tagText = t('notice.autoTag', { verdict: verdictLabel(t, notice.verdict) || notice.verdict || t('notice.autoDefault') })
410
+ glyph = React.createElement(GlyphCheck, null)
411
+ }
412
+ const autoReject = isAutoReject(notice.verdict)
413
+ const isFeedErr = kind === 'feed-error'
414
+ const cardCls = 'ab-notice-card' + (isPending ? ' ab-notice-card-pending' : isManual ? ' ab-notice-card-manual' : (autoReject || isFeedErr) ? ' ab-notice-card-err' : '')
415
+ return React.createElement('div', { className: 'ab-notice' },
416
+ React.createElement('div', { className: cardCls },
417
+ React.createElement('span', { className: 'ab-notice-glyph' }, glyph),
418
+ React.createElement('div', { className: 'ab-notice-body' },
419
+ React.createElement('div', { className: 'ab-notice-head' },
420
+ React.createElement('span', { className: 'ab-notice-tool' }, notice.tool || (kind === 'feed-error' ? t('notice.hint') : 'tool')),
421
+ React.createElement('span', { className: 'ab-notice-text' }, title),
422
+ ),
423
+ 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),
425
+ React.createElement('span', { className: 'ab-time' }, fmtTime(notice.ts)),
426
+ ),
427
+ ),
428
+ React.createElement('button', {
429
+ type: 'button', className: 'ab-notice-close', title: t('notice.close'), 'aria-label': t('notice.close'),
430
+ onClick: function () { setNotice(null) },
431
+ }, '✕'),
432
+ ),
433
+ )
434
+ }
435
+
436
+ /** 会话「审批」tab。行头可点,详情分组,方便选中文字。 */
437
+ function HistoryView(props) {
438
+ const t = tFrom(props)
439
+ const rpc = props.rpc
440
+ const sessionId = (props && (props.sessionId || (props.slotsProps && props.slotsProps.sessionId))) || null
441
+ const [events, setEvents] = React.useState(null)
442
+ const [error, setError] = React.useState(null)
443
+ const [openId, setOpenId] = React.useState(null)
444
+
445
+ React.useEffect(function () {
446
+ let alive = true
447
+ let timer = null
448
+ if (!sessionId || !rpc) return
449
+ const load = function () {
450
+ rpc('events', { sessionId: sessionId, since: 0 }).then(function (data) {
451
+ if (!alive) return
452
+ const list = ((data && data.events) || []).slice().reverse()
453
+ setEvents(list)
454
+ setError(null)
455
+ }).catch(function (e) {
456
+ if (!alive) return
457
+ setError(String((e && e.message) || e))
458
+ })
459
+ }
460
+ load()
461
+ timer = setInterval(load, 3000)
462
+ return function () { alive = false; if (timer) clearInterval(timer) }
463
+ }, [sessionId, rpc])
464
+
465
+ if (!sessionId) return React.createElement('div', { className: 'ab-empty' }, t('history.noSession'))
466
+ if (error) return React.createElement('div', { className: 'ab-empty' }, t('history.loadFailed', { error: error }))
467
+ if (!events) return React.createElement('div', { className: 'ab-loading' }, t('history.loading'))
468
+ if (events.length === 0) return React.createElement('div', { className: 'ab-view' },
469
+ React.createElement('div', { className: 'ab-view-head' },
470
+ React.createElement('div', { className: 'ab-view-title' }, t('history.title')),
471
+ React.createElement('div', { className: 'ab-view-sub' }, t('history.emptySub')),
472
+ ),
473
+ React.createElement('div', { className: 'ab-empty' }, t('history.emptyHint')),
474
+ )
475
+
476
+ return React.createElement('div', { className: 'ab-view' },
477
+ React.createElement('div', { className: 'ab-view-head' },
478
+ React.createElement('div', { className: 'ab-view-title' }, t('history.title')),
479
+ React.createElement('div', { className: 'ab-view-sub' }, t('history.sub')),
480
+ ),
481
+ React.createElement('div', { className: 'ab-list' },
482
+ events.map(function (ev) {
483
+ const kind = ev.kind || 'auto'
484
+ const ticket = ev.ticket ? '#' + ev.ticket : ''
485
+ let glyph = React.createElement(GlyphCheck, null)
486
+ let tag = t('history.tagAuto')
487
+ let tagCls = 'ab-tag'
488
+ if (kind === 'manual-pending') {
489
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '◔')
490
+ tag = t('history.tagPending', { ticket: ticket })
491
+ tagCls = 'ab-tag-warn'
492
+ } else if (kind === 'manual-approved') {
493
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-warn' }, '✓')
494
+ tag = t(ev.source === 'qq' ? 'history.tagQqAllow' : 'history.tagAllow', { ticket: ticket })
495
+ tagCls = 'ab-tag-warn'
496
+ } else if (kind === 'manual-rejected') {
497
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-err' }, '✕')
498
+ tag = t('history.tagReject', { ticket: ticket })
499
+ tagCls = 'ab-tag-err'
500
+ } else if (isAutoReject(ev.verdict)) {
501
+ glyph = React.createElement('span', { className: 'ab-notice-glyph-err' }, '✕')
502
+ tag = verdictLabel(t, ev.verdict) || t('notice.rejectedDefault')
503
+ tagCls = 'ab-tag-err'
504
+ } else {
505
+ tag = verdictLabel(t, ev.verdict) || t('history.tagAuto')
506
+ }
507
+ const open = openId === ev.id
508
+ const args = ev.args || {}
509
+ const j = ev.judge || {}
510
+ return React.createElement('div', {
511
+ className: 'ab-row' + (open ? ' ab-row-open' : ''),
512
+ key: ev.id,
513
+ },
514
+ React.createElement('div', { className: 'ab-row-rail' },
515
+ glyph,
516
+ React.createElement('div', { className: 'ab-row-line' }),
517
+ ),
518
+ React.createElement('div', { className: 'ab-row-body' },
519
+ React.createElement('button', {
520
+ type: 'button',
521
+ className: 'ab-row-head',
522
+ onClick: function () { setOpenId(open ? null : ev.id) },
523
+ },
524
+ React.createElement('div', { className: 'ab-row-top' },
525
+ React.createElement('span', { className: 'ab-row-tool' }, ev.tool || 'tool'),
526
+ React.createElement('span', { className: tagCls }, tag),
527
+ React.createElement('span', { className: 'ab-time' }, fmtTime(ev.ts)),
528
+ React.createElement('span', { className: 'ab-row-chev' }, '▾'),
529
+ ),
530
+ React.createElement('div', { className: 'ab-row-reason' }, eventPreview(ev)),
531
+ ),
532
+ open
533
+ ? React.createElement('div', { className: 'ab-details' },
534
+ DetailSection(t('detail.request'), [
535
+ [t('detail.command'), args.command],
536
+ [t('detail.path'), args.file_path || args.path],
537
+ [t('detail.description'), args.description],
538
+ [t('detail.old'), args.old_string],
539
+ [t('detail.new'), args.new_string],
540
+ [t('detail.content'), args.content],
541
+ [t('detail.code'), args.code],
542
+ [t('detail.url'), args.url],
543
+ [t('detail.query'), args.query],
544
+ [t('detail.script'), args.script],
545
+ [t('detail.sql'), args.sql],
546
+ [t('detail.prompt'), args.prompt],
547
+ [t('detail.cwd'), ev.cwd],
548
+ [t('detail.sandbox'), ev.mode],
549
+ [t('detail.justification'), ev.justification || ev.reason],
550
+ ]),
551
+ DetailSection(t('detail.audit'), [
552
+ [t('detail.pipe'), pathLabel(t, ev.path) || ev.path],
553
+ [t('detail.keyword'), ev.keyword],
554
+ [t('detail.judgeModel'), [j.provider, j.model, j.effort].filter(Boolean).join(' / ')],
555
+ [t('detail.judgeCategory'), j.failed
556
+ ? t('detail.judgeFailed')
557
+ : (j.label && j.criterion ? j.criterion + ' · ' + j.label : (j.criterion || ev.category || ''))],
558
+ [t('detail.judgeAction'), actionLabel(t, j.action) || j.action],
559
+ [t('detail.judgeReason'), j.reason || ev.judgeReason],
560
+ [t('detail.judgeError'), j.error],
561
+ [t('detail.judgeRaw'), j.raw],
562
+ [t('detail.ticket'), ev.ticket != null ? '#' + ev.ticket : ''],
563
+ [t('detail.source'), ev.source],
564
+ ]),
565
+ )
566
+ : null,
567
+ ),
568
+ )
569
+ }),
570
+ ),
571
+ )
572
+ }
573
+
574
+ /** 设置 → 自动审批。QQ 卡折在底部;恢复默认需连点两次。 */
575
+ function SettingsPage(props) {
576
+ const t = tFrom(props)
577
+ const rpc = props.rpc
578
+ const rootRef = React.useRef(null)
579
+ const [snapshot, setSnapshot] = React.useState(null)
580
+ const [error, setError] = React.useState(null)
581
+ const [busy, setBusy] = React.useState(false)
582
+ const [feedback, setFeedback] = React.useState(null)
583
+ const [newKeyword, setNewKeyword] = React.useState('')
584
+ const [newKeywordAction, setNewKeywordAction] = React.useState('human')
585
+ const [newCriterion, setNewCriterion] = React.useState({ id: '', label: '', description: '', action: 'human' })
586
+ 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
+ const [models, setModels] = React.useState([])
593
+ const [efforts, setEfforts] = React.useState([])
594
+ const [judgeProvider, setJudgeProvider] = React.useState('')
595
+ const [judgeModel, setJudgeModel] = React.useState('')
596
+ const [judgeEffort, setJudgeEffort] = React.useState('')
597
+ const [judgePromptLang, setJudgePromptLang] = React.useState('zh')
598
+ const [presetSandbox, setPresetSandbox] = React.useState('workspace-write')
599
+ const [foldOpen, setFoldOpen] = React.useState({ keywords: false, criteria: false, qq: false })
600
+ const [kwEdit, setKwEdit] = React.useState(null)
601
+ const [resetArmed, setResetArmed] = React.useState(null)
602
+
603
+ const load = function (opts) {
604
+ const keepEdits = opts && opts.keepEdits
605
+ rpc('snapshot').then(function (data) {
606
+ setSnapshot(data)
607
+ if (!keepEdits) {
608
+ setTimeoutMs(String(data.config.judgeTimeoutMs))
609
+ setJudgeProvider((data.plugin.judge && data.plugin.judge.provider) || '')
610
+ setJudgeModel((data.plugin.judge && data.plugin.judge.model) || '')
611
+ setJudgeEffort((data.plugin.judge && data.plugin.judge.reasoningEffort) || '')
612
+ setJudgePromptLang((data.plugin && data.plugin.judgePromptLang) === 'en' ? 'en' : 'zh')
613
+ 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
+ }
618
+ setError(null)
619
+ }).catch(function (e) {
620
+ setError(t('set.loadFailed', { error: String((e && e.message) || e) }))
621
+ })
622
+ }
623
+ React.useEffect(function () { load() }, [])
624
+ React.useEffect(function () {
625
+ if (!resetArmed) return
626
+ const disarm = setTimeout(function () { setResetArmed(null) }, 5000)
627
+ return function () { clearTimeout(disarm) }
628
+ }, [resetArmed])
629
+
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
+ React.useEffect(function () {
648
+ const provider = judgeProvider || (snapshot && snapshot.fallback && snapshot.fallback.provider)
649
+ if (!provider || !rpc) return
650
+ rpc('judge-catalog', { provider: provider }).then(function (data) {
651
+ setModels(data.models || [])
652
+ }).catch(function () { setModels([]) })
653
+ }, [judgeProvider, snapshot, rpc])
654
+
655
+ React.useEffect(function () {
656
+ const provider = judgeProvider || (snapshot && snapshot.fallback && snapshot.fallback.provider)
657
+ const model = judgeModel || (snapshot && snapshot.fallback && snapshot.fallback.model)
658
+ if (!provider || !model || !rpc) { setEfforts([]); return }
659
+ rpc('judge-info', { provider: provider, model: model }).then(function (data) {
660
+ setEfforts(data.efforts || [])
661
+ }).catch(function () { setEfforts([]) })
662
+ }, [judgeProvider, judgeModel, snapshot, rpc])
663
+
664
+ const showFeedback = function (msg, ok) {
665
+ setFeedback({ msg: String(msg), ok: ok !== false })
666
+ setTimeout(function () { setFeedback(null) }, 4000)
667
+ }
668
+
669
+ const run = function (endpoint, payload, okMsg) {
670
+ setBusy(true)
671
+ return rpc(endpoint, payload).then(function (res) {
672
+ load()
673
+ showFeedback(okMsg || t('set.hotOk'))
674
+ return res
675
+ }).catch(function (e) {
676
+ showFeedback(String((e && e.message) || e), false)
677
+ }).finally(function () { setBusy(false) })
678
+ }
679
+
680
+ if (!snapshot) {
681
+ return React.createElement('div', { className: 'ab-set' },
682
+ React.createElement('div', { className: error ? 'ab-set-err' : 'ab-set-ok' }, error || t('set.loading')))
683
+ }
684
+
685
+ const cfg = snapshot.config
686
+ const setup = snapshot.setup || { configured: false }
687
+ const predefined = snapshot.predefined || {}
688
+ const preDeny = new Set(predefined.rejectKeywords || predefined.denyKeywords || [])
689
+ const qq = snapshot.qq || {}
690
+ const plugin = snapshot.plugin || {}
691
+ const notify = plugin.notify || {}
692
+ const providers = snapshot.providers || []
693
+ const fallback = snapshot.fallback || {}
694
+ const judgeTimeoutLabel = Math.round((Number(cfg.judgeTimeoutMs) || 20000) / 1000) + 's'
695
+ const judgeRoute = (judgeProvider && judgeModel)
696
+ ? (judgeProvider + ' / ' + judgeModel)
697
+ : (fallback.provider && fallback.model ? t('set.followDefault') : t('set.unconfigured'))
698
+ const scrollToStage = function (id) {
699
+ try {
700
+ const root = rootRef.current
701
+ const el = root && root.querySelector('[data-ab-stage="' + id + '"]')
702
+ if (!el) return
703
+ if (el.tagName === 'DETAILS') {
704
+ setFoldOpen(function (prev) {
705
+ const next = Object.assign({}, prev)
706
+ next[id] = true
707
+ return next
708
+ })
709
+ }
710
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' })
711
+ } catch (e) {}
712
+ }
713
+
714
+ const resetBtn = function (kind, idleLabel, okMsg, value) {
715
+ const armed = resetArmed === kind
716
+ return React.createElement('button', {
717
+ type: 'button',
718
+ className: 'ab-set-btn' + (armed ? ' ab-set-btn-danger' : ''),
719
+ disabled: busy,
720
+ onClick: function () {
721
+ if (resetArmed !== kind) {
722
+ setResetArmed(kind)
723
+ return
724
+ }
725
+ setResetArmed(null)
726
+ run('rule-op', { op: 'reset', kind: kind, value: value }, okMsg)
727
+ },
728
+ }, armed ? t('set.resetConfirm') : idleLabel)
729
+ }
730
+
731
+ return React.createElement('div', { className: 'ab-set', ref: rootRef },
732
+ React.createElement('h3', { className: 'ab-set-title' }, t('set.title')),
733
+ React.createElement('p', { className: 'ab-set-intro' }, t('set.intro')),
734
+ (cfg && cfg.corrupt)
735
+ ? React.createElement('div', { className: 'ab-set-err' }, t('set.allowlistCorrupt'))
736
+ : null,
737
+ snapshot.pluginCorrupt
738
+ ? React.createElement('div', { className: 'ab-set-err' },
739
+ t('set.pluginCorrupt'),
740
+ React.createElement('button', {
741
+ type: 'button', className: 'ab-set-btn ab-set-btn-danger', disabled: busy,
742
+ onClick: function () { run('save-plugin', { overwriteCorrupt: true }, t('set.overwriteOk')) },
743
+ }, t('set.pluginCorruptOverwrite')),
744
+ )
745
+ : null,
746
+ (snapshot.qq && snapshot.qq.credsCorrupt)
747
+ ? React.createElement('div', { className: 'ab-set-err' }, t('set.qqCorrupt'))
748
+ : null,
749
+
750
+ !setup.configured ? React.createElement('div', { className: 'ab-set-card' },
751
+ React.createElement('div', { className: 'ab-set-card-head' },
752
+ React.createElement('div', { className: 'ab-set-card-title' }, t('set.presetMissingTitle')),
753
+ React.createElement('p', { className: 'ab-set-card-sub' }, t('set.presetMissingSub'))),
754
+ React.createElement('div', { className: 'ab-set-row' },
755
+ React.createElement('button', {
756
+ type: 'button', className: 'ab-set-btn ab-set-btn-primary', disabled: busy,
757
+ onClick: function () { run('setup', {}, t('set.presetWrote')) },
758
+ }, t('set.presetWrite')),
759
+ React.createElement('span', { className: 'ab-set-note' }, t('set.presetWriteNote')),
760
+ ),
761
+ ) : null,
762
+
763
+ React.createElement('div', { className: 'ab-set-card' },
764
+ React.createElement('div', { className: 'ab-set-card-head' },
765
+ React.createElement('div', { className: 'ab-set-card-title' }, t('set.modeTitle'))),
766
+ React.createElement('div', { className: 'ab-set-row ab-set-row-choices' },
767
+ React.createElement('button', {
768
+ type: 'button',
769
+ className: 'ab-set-choice' + (presetSandbox === 'workspace-write' ? ' ab-set-choice-on' : ''),
770
+ onClick: function () { setPresetSandbox('workspace-write') },
771
+ },
772
+ React.createElement('span', { className: 'ab-set-choice-t' }, t('set.modeWs')),
773
+ React.createElement('span', { className: 'ab-set-choice-d' }, t('set.modeWsHint')),
774
+ ),
775
+ React.createElement('button', {
776
+ type: 'button',
777
+ className: 'ab-set-choice' + (presetSandbox === 'read-only' ? ' ab-set-choice-on' : ''),
778
+ onClick: function () { setPresetSandbox('read-only') },
779
+ },
780
+ React.createElement('span', { className: 'ab-set-choice-t' }, t('set.modeRo')),
781
+ React.createElement('span', { className: 'ab-set-choice-d' }, t('set.modeRoHint')),
782
+ ),
783
+ ),
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
+ ),
796
+ ),
797
+
798
+ React.createElement('div', { className: 'ab-set-card' },
799
+ React.createElement('div', { className: 'ab-set-card-head' },
800
+ React.createElement('div', { className: 'ab-set-card-title' }, t('set.overview')),
801
+ React.createElement('p', { className: 'ab-set-card-sub' }, t('set.overviewSub'))),
802
+ React.createElement('div', { className: 'ab-set-steps' },
803
+ [
804
+ {
805
+ id: 'keywords', name: t('set.stepKeywords'),
806
+ value: countsLabel(t, (cfg.rejectKeywords || []).length, (cfg.humanKeywords || cfg.denyKeywords || []).length, (cfg.allowKeywords || []).length),
807
+ },
808
+ { id: 'criteria', name: t('set.stepCriteria'), value: t('set.criteriaCount', { n: String((cfg.criteria || []).length) }) },
809
+ { id: 'judge', name: t('set.stepJudge'), value: judgeRoute + ' · ' + judgeTimeoutLabel },
810
+ ].map(function (s) {
811
+ return React.createElement('button', {
812
+ type: 'button', className: 'ab-set-step', key: s.id,
813
+ onClick: function () { scrollToStage(s.id) },
814
+ },
815
+ React.createElement('div', { className: 'ab-set-step-k' }, s.name),
816
+ React.createElement('div', { className: 'ab-set-step-v' }, s.value),
817
+ )
818
+ }),
819
+ ),
820
+ React.createElement('div', { className: 'ab-set-invs' },
821
+ (cfg.criteria || []).map(function (c) {
822
+ return React.createElement('span', {
823
+ className: 'ab-set-inv ' + (c.action === 'reject' ? 'ab-set-inv-hard' : c.action === 'allow' ? 'ab-set-inv-ok' : 'ab-set-inv-mode'),
824
+ key: c.id, title: c.id,
825
+ }, (c.label || c.id) + ' · ' + (actionLabel(t, c.action) || c.action))
826
+ }),
827
+ ),
828
+ ),
829
+
830
+ React.createElement('details', {
831
+ className: 'ab-set-card ab-set-fold', 'data-ab-stage': 'keywords',
832
+ open: foldOpen.keywords,
833
+ onToggle: function (e) {
834
+ const open = e.currentTarget.open
835
+ setFoldOpen(function (prev) { return Object.assign({}, prev, { keywords: open }) })
836
+ },
837
+ },
838
+ React.createElement('summary', null,
839
+ React.createElement('div', { className: 'ab-set-card-head' },
840
+ React.createElement('div', { className: 'ab-set-card-title' },
841
+ t('set.stepKeywords'),
842
+ React.createElement('span', { className: 'ab-set-fold-chev' }, '▾')),
843
+ React.createElement('p', { className: 'ab-set-card-sub' }, t('set.kwSub'))),
844
+ React.createElement('div', { className: 'ab-set-fold-sum' },
845
+ countsLabel(t, (cfg.rejectKeywords || []).length, (cfg.humanKeywords || cfg.denyKeywords || []).length, (cfg.allowKeywords || []).length))),
846
+ React.createElement('div', { className: 'ab-set-fold-body' },
847
+ React.createElement('div', { className: 'ab-set-list' },
848
+ (function () {
849
+ const rows = []
850
+ ;(cfg.rejectKeywords || []).forEach(function (k) { rows.push({ text: k, action: 'reject' }) })
851
+ ;(cfg.humanKeywords || cfg.denyKeywords || []).forEach(function (k) { rows.push({ text: k, action: 'human' }) })
852
+ ;(cfg.allowKeywords || []).forEach(function (k) { rows.push({ text: k, action: 'allow' }) })
853
+ if (rows.length === 0) return React.createElement('p', { className: 'ab-set-empty' }, t('set.empty'))
854
+ return rows.map(function (row) {
855
+ const commitKw = function (text, action) {
856
+ const next = String(text || '').trim()
857
+ if (!next) return
858
+ if (next === row.text && action === row.action) return
859
+ run('rule-op', { op: 'set', kind: 'keywords', value: { from: row.text, text: next, action: action } })
860
+ }
861
+ const editKey = row.action + ':' + row.text
862
+ return React.createElement('div', { className: 'ab-set-item ab-set-item-kw', key: editKey },
863
+ kwEdit === editKey
864
+ ? React.createElement('input', {
865
+ className: 'ab-set-input ab-set-input-grow',
866
+ defaultValue: row.text,
867
+ autoFocus: true,
868
+ onBlur: function (e) {
869
+ commitKw(e.target.value, row.action)
870
+ setKwEdit(null)
871
+ },
872
+ onKeyDown: function (e) {
873
+ if (e.key === 'Enter') e.currentTarget.blur()
874
+ if (e.key === 'Escape') setKwEdit(null)
875
+ },
876
+ })
877
+ : React.createElement('span', {
878
+ className: 'ab-set-kw-text',
879
+ title: t('set.kwEditTitle'),
880
+ onClick: function () { setKwEdit(editKey) },
881
+ }, row.text),
882
+ preDeny.has(row.text) ? React.createElement('span', { className: 'ab-set-tag ab-set-tag-blue' }, t('set.presetTag')) : null,
883
+ React.createElement('select', {
884
+ className: 'ab-set-select', value: row.action,
885
+ onChange: function (e) { commitKw(row.text, e.target.value) },
886
+ },
887
+ React.createElement('option', { value: 'human' }, t('action.human')),
888
+ React.createElement('option', { value: 'allow' }, t('action.allow')),
889
+ React.createElement('option', { value: 'reject' }, t('action.reject')),
890
+ ),
891
+ React.createElement('button', {
892
+ type: 'button',
893
+ className: 'ab-set-item-del' + (resetArmed === ('del-kw:' + row.text) ? ' ab-set-btn-danger' : ''),
894
+ title: t('set.delete'),
895
+ onClick: function () {
896
+ const key = 'del-kw:' + row.text
897
+ if (preDeny.has(row.text) && resetArmed !== key) {
898
+ setResetArmed(key)
899
+ return
900
+ }
901
+ setResetArmed(null)
902
+ run('rule-op', { op: 'remove', kind: 'keywords', value: { text: row.text } })
903
+ },
904
+ }, resetArmed === ('del-kw:' + row.text) ? t('set.confirm') : '✕'),
905
+ )
906
+ })
907
+ })(),
908
+ ),
909
+ React.createElement('div', { className: 'ab-set-row' },
910
+ React.createElement('input', {
911
+ className: 'ab-set-input ab-set-input-grow', placeholder: t('set.kwPlaceholder'),
912
+ value: newKeyword, onChange: function (e) { setNewKeyword(e.target.value) },
913
+ }),
914
+ React.createElement('select', {
915
+ className: 'ab-set-select', value: newKeywordAction,
916
+ onChange: function (e) { setNewKeywordAction(e.target.value) },
917
+ },
918
+ React.createElement('option', { value: 'human' }, t('action.human')),
919
+ React.createElement('option', { value: 'allow' }, t('action.allow')),
920
+ React.createElement('option', { value: 'reject' }, t('action.reject')),
921
+ ),
922
+ React.createElement('button', {
923
+ type: 'button', className: 'ab-set-btn', disabled: busy || !newKeyword.trim(),
924
+ onClick: function () {
925
+ run('rule-op', { op: 'add', kind: 'keywords', value: { text: newKeyword.trim(), action: newKeywordAction } })
926
+ setNewKeyword('')
927
+ },
928
+ }, t('set.add')),
929
+ ),
930
+ React.createElement('div', { className: 'ab-set-row ab-set-foot' },
931
+ resetBtn('keywords', t('set.resetKeywords'), t('set.resetKeywordsOk')),
932
+ ),
933
+ ),
934
+ ),
935
+
936
+ React.createElement('details', {
937
+ className: 'ab-set-card ab-set-fold', 'data-ab-stage': 'criteria',
938
+ open: foldOpen.criteria,
939
+ onToggle: function (e) {
940
+ const open = e.currentTarget.open
941
+ setFoldOpen(function (prev) { return Object.assign({}, prev, { criteria: open }) })
942
+ },
943
+ },
944
+ React.createElement('summary', null,
945
+ React.createElement('div', { className: 'ab-set-card-head' },
946
+ React.createElement('div', { className: 'ab-set-card-title' },
947
+ t('set.stepCriteria'),
948
+ React.createElement('span', { className: 'ab-set-fold-chev' }, '▾')),
949
+ React.createElement('p', { className: 'ab-set-card-sub' }, t('set.criteriaSub'))),
950
+ React.createElement('div', { className: 'ab-set-fold-sum' },
951
+ (function () {
952
+ const rows = cfg.criteria || []
953
+ const n = function (a) { return rows.filter(function (c) { return c.action === a }).length }
954
+ return countsLabel(t, n('reject'), n('human'), n('allow'))
955
+ })())),
956
+ React.createElement('div', { className: 'ab-set-fold-body' },
957
+ React.createElement('div', { className: 'ab-set-list ab-set-list-crit' },
958
+ (cfg.criteria || []).map(function (c) {
959
+ const commitCrit = function (patch) {
960
+ run('rule-op', { op: 'set', kind: 'criteria', value: Object.assign({ id: c.id }, patch) })
961
+ }
962
+ return React.createElement('div', { className: 'ab-set-item ab-set-item-crit', key: c.id },
963
+ React.createElement('div', { className: 'ab-set-crit-top' },
964
+ React.createElement('span', { className: 'ab-set-item-id' }, c.id),
965
+ React.createElement('select', {
966
+ className: 'ab-set-select', value: c.action || 'human',
967
+ onChange: function (e) { commitCrit({ action: e.target.value }) },
968
+ },
969
+ React.createElement('option', { value: 'human' }, t('action.human')),
970
+ React.createElement('option', { value: 'allow' }, t('action.allow')),
971
+ React.createElement('option', { value: 'reject' }, t('action.reject')),
972
+ ),
973
+ c.id === 'other' ? null : React.createElement('button', {
974
+ type: 'button',
975
+ className: 'ab-set-item-del' + (resetArmed === ('del-c:' + c.id) ? ' ab-set-btn-danger' : ''),
976
+ onClick: function () {
977
+ const key = 'del-c:' + c.id
978
+ if (resetArmed !== key) {
979
+ setResetArmed(key)
980
+ return
981
+ }
982
+ setResetArmed(null)
983
+ run('rule-op', { op: 'remove', kind: 'criteria', value: { id: c.id } })
984
+ },
985
+ }, resetArmed === ('del-c:' + c.id) ? t('set.confirm') : '✕'),
986
+ ),
987
+ React.createElement('div', { className: 'ab-set-item-fields' },
988
+ React.createElement('input', {
989
+ className: 'ab-set-input ab-set-input-ui',
990
+ defaultValue: c.label || '',
991
+ placeholder: t('set.labelPlaceholder'),
992
+ onBlur: function (e) {
993
+ const next = e.target.value.trim()
994
+ if (!next || next === c.label) return
995
+ commitCrit({ label: next })
996
+ },
997
+ onKeyDown: function (e) { if (e.key === 'Enter') e.currentTarget.blur() },
998
+ }),
999
+ React.createElement('textarea', {
1000
+ className: 'ab-set-textarea',
1001
+ defaultValue: c.description || '',
1002
+ placeholder: t('set.descPlaceholder'),
1003
+ rows: 3,
1004
+ ref: sizeTextarea,
1005
+ onInput: function (e) { sizeTextarea(e.currentTarget) },
1006
+ onBlur: function (e) {
1007
+ const next = e.target.value.trim()
1008
+ if (next === (c.description || '')) return
1009
+ commitCrit({ description: next })
1010
+ },
1011
+ }),
1012
+ ),
1013
+ )
1014
+ }),
1015
+ ),
1016
+ React.createElement('div', { className: 'ab-set-item ab-set-item-crit' },
1017
+ React.createElement('div', { className: 'ab-set-crit-top' },
1018
+ React.createElement('input', {
1019
+ className: 'ab-set-input ab-set-input-grow', placeholder: t('set.criterionId'),
1020
+ value: newCriterion.id,
1021
+ onChange: function (e) { setNewCriterion(Object.assign({}, newCriterion, { id: e.target.value })) },
1022
+ }),
1023
+ React.createElement('select', {
1024
+ className: 'ab-set-select', value: newCriterion.action,
1025
+ onChange: function (e) { setNewCriterion(Object.assign({}, newCriterion, { action: e.target.value })) },
1026
+ },
1027
+ React.createElement('option', { value: 'human' }, t('action.human')),
1028
+ React.createElement('option', { value: 'allow' }, t('action.allow')),
1029
+ React.createElement('option', { value: 'reject' }, t('action.reject')),
1030
+ ),
1031
+ ),
1032
+ React.createElement('div', { className: 'ab-set-item-fields' },
1033
+ React.createElement('input', {
1034
+ className: 'ab-set-input ab-set-input-ui', placeholder: t('set.labelPlaceholder'),
1035
+ value: newCriterion.label,
1036
+ onChange: function (e) { setNewCriterion(Object.assign({}, newCriterion, { label: e.target.value })) },
1037
+ }),
1038
+ React.createElement('textarea', {
1039
+ className: 'ab-set-textarea',
1040
+ placeholder: t('set.descPlaceholder'),
1041
+ rows: 2,
1042
+ value: newCriterion.description,
1043
+ onChange: function (e) { setNewCriterion(Object.assign({}, newCriterion, { description: e.target.value })) },
1044
+ ref: sizeTextarea,
1045
+ onInput: function (e) { sizeTextarea(e.currentTarget) },
1046
+ }),
1047
+ ),
1048
+ React.createElement('div', { className: 'ab-set-row ab-set-foot' },
1049
+ React.createElement('button', {
1050
+ type: 'button', className: 'ab-set-btn ab-set-btn-primary',
1051
+ disabled: busy || !(newCriterion.id || newCriterion.label),
1052
+ onClick: function () {
1053
+ run('rule-op', { op: 'add', kind: 'criteria', value: newCriterion })
1054
+ setNewCriterion({ id: '', label: '', description: '', action: 'human' })
1055
+ },
1056
+ }, t('set.addCriterion')),
1057
+ ),
1058
+ ),
1059
+ React.createElement('div', { className: 'ab-set-row ab-set-foot' },
1060
+ resetBtn(
1061
+ 'criteria',
1062
+ t('set.resetCriteria', { lang: t(judgePromptLang === 'en' ? 'set.judgeLangEn' : 'set.judgeLangZh') }),
1063
+ t('set.resetCriteriaOk', { lang: t(judgePromptLang === 'en' ? 'set.judgeLangEn' : 'set.judgeLangZh') }),
1064
+ { lang: judgePromptLang },
1065
+ ),
1066
+ ),
1067
+ ),
1068
+ ),
1069
+
1070
+ React.createElement('div', { className: 'ab-set-card', 'data-ab-stage': 'judge' },
1071
+ React.createElement('div', { className: 'ab-set-card-head' },
1072
+ React.createElement('div', { className: 'ab-set-card-title' }, t('set.judgeTitle')),
1073
+ React.createElement('p', { className: 'ab-set-card-sub' },
1074
+ t('set.judgeSubLead') +
1075
+ (fallback.provider && fallback.model
1076
+ ? t('set.judgeSubFallback', { provider: fallback.provider, model: fallback.model })
1077
+ : t('set.judgeSubNoFallback')) +
1078
+ t('set.judgeSubTail'))),
1079
+ React.createElement('div', { className: 'ab-set-row' },
1080
+ React.createElement('select', {
1081
+ className: 'ab-set-select', value: judgeProvider,
1082
+ onChange: function (e) { setJudgeProvider(e.target.value); setJudgeModel(''); setJudgeEffort('') },
1083
+ },
1084
+ React.createElement('option', { value: '' }, t('set.followProvider')),
1085
+ providers.map(function (p) {
1086
+ return React.createElement('option', { value: p.id, key: p.id }, p.name || p.id)
1087
+ }),
1088
+ ),
1089
+ React.createElement('select', {
1090
+ className: 'ab-set-select', value: judgeModel,
1091
+ onChange: function (e) { setJudgeModel(e.target.value); setJudgeEffort('') },
1092
+ },
1093
+ React.createElement('option', { value: '' }, t('set.followModel')),
1094
+ models.map(function (m) {
1095
+ return React.createElement('option', { value: m.id, key: m.id }, m.name || m.id)
1096
+ }),
1097
+ ),
1098
+ React.createElement('select', {
1099
+ className: 'ab-set-select', value: judgeEffort,
1100
+ onChange: function (e) { setJudgeEffort(e.target.value) },
1101
+ },
1102
+ React.createElement('option', { value: '' }, t('set.modelDefaultEffort')),
1103
+ React.createElement('option', { value: 'off' }, t('set.effortOff')),
1104
+ efforts.filter(function (e) { return e.id !== 'off' }).map(function (e) {
1105
+ return React.createElement('option', { value: e.id, key: e.id }, e.name || e.id)
1106
+ }),
1107
+ ),
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
+ ),
1118
+ React.createElement('div', { className: 'ab-set-row' },
1119
+ React.createElement('span', { className: 'ab-set-item-meta' }, t('set.judgeLang')),
1120
+ React.createElement('select', {
1121
+ className: 'ab-set-select', value: judgePromptLang,
1122
+ onChange: function (e) { setJudgePromptLang(e.target.value === 'en' ? 'en' : 'zh') },
1123
+ },
1124
+ React.createElement('option', { value: 'zh' }, t('set.judgeLangZh')),
1125
+ React.createElement('option', { value: 'en' }, t('set.judgeLangEn')),
1126
+ ),
1127
+ React.createElement('span', { className: 'ab-set-note' }, t('set.judgeLangHint')),
1128
+ ),
1129
+ React.createElement('div', { className: 'ab-set-row' },
1130
+ React.createElement('span', { className: 'ab-set-item-meta' }, t('set.judgeTimeoutMs')),
1131
+ React.createElement('input', { className: 'ab-set-input ab-set-input-num', type: 'number', min: 1000, value: timeoutMs, onChange: function (e) { setTimeoutMs(e.target.value) } }),
1132
+ 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) }) },
1135
+ }, t('set.save')),
1136
+ ),
1137
+ ),
1138
+
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
+ )
1249
+ }
1250
+
1251
+ const plugin = {
1252
+ inject: ['connection', 'slots', 'locale'],
1253
+ async apply(ctx) {
1254
+ const slots = ctx.slots
1255
+ const connection = ctx.connection
1256
+ ctx.effect(function () { return ctx.locale.register(NS, { zh: ZH, en: EN }) })
1257
+ const t = ctx.locale.bind(NS)
1258
+ const rpc = makeRpc(connection, t)
1259
+
1260
+ let styleEl = null
1261
+ try {
1262
+ styleEl = document.createElement('style')
1263
+ styleEl.setAttribute('data-plugin-css', 'dsh-auto-approve')
1264
+ styleEl.textContent = CSS
1265
+ document.head.appendChild(styleEl)
1266
+ } catch (e) {
1267
+ console.error('[dsh-auto-approve] 注入样式失败:' + String((e && e.message) || e))
1268
+ }
1269
+ ctx.effect(() => {
1270
+ return () => {
1271
+ if (styleEl && styleEl.parentNode) {
1272
+ try { styleEl.parentNode.removeChild(styleEl) } catch (e) {}
1273
+ }
1274
+ }
1275
+ })
1276
+
1277
+ slots.inject('conversation.input.dock', function () {
1278
+ return slots.register(
1279
+ { 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 }) },
1281
+ )
1282
+ })
1283
+
1284
+ slots.inject('conversation.view', function () {
1285
+ return slots.register(
1286
+ {
1287
+ name: 'conversation.view',
1288
+ id: 'dsh-auto-approve.history',
1289
+ order: 20,
1290
+ locale: NS,
1291
+ label: function () { return t('slot.history') },
1292
+ inject: (sessionId) => ({ sessionId }),
1293
+ },
1294
+ function (props) { return React.createElement(HistoryView, { slotsProps: props, rpc: rpc }) },
1295
+ )
1296
+ })
1297
+
1298
+ slots.inject('settings.section', function () {
1299
+ return slots.register(
1300
+ { 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 }) },
1302
+ )
1303
+ })
1304
+ },
1305
+ }
1306
+
1307
+ exports.apply = plugin.apply
1308
+ exports.inject = plugin.inject
1309
+ exports.name = 'dsh-auto-approve'
1310
+ return module.exports
1311
+ },
1312
+ })