@dsh-suite/plugin-deus 0.3.0 → 0.4.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/README.md CHANGED
@@ -28,6 +28,25 @@
28
28
  - 对话页 dock 出现 `⚓ 锚定维持中` 状态 chip;**漂移时变红**,点击发送重锚提示词(可开「漂移自动重锚」);重锚轮日志以 `reanchor` 模式独立记录,便于对比重锚前后维持率。
29
29
  6. **未锚定引导(v0.3)** — 当前会话不在 deus/minimal 系 preset 时,dock 显示灰态 `◌ 非神版会话` chip,点击展开切换指引(把「25 工具会话注入≈0%」的实测结论落地为产品引导)。
30
30
  7. **统计导出与分享(v0.3)** — 面板一键导出 CSV、**复制 Markdown 摘要**(Wilson CI 表格 + 诚实声明 + 包链接,可直接发帖),并内嵌近期神版率迷你趋势图(最近 40 条日志分桶折线)。
31
+ 8. **提示词 A/B 实验台(v0.4)** — 设计文档的「纯噪音退路」转正:对话页 dock 的 🧪 **实验台**里输入**任意**提示词变体 A/B,各跑 N 次**真实会话注入**(每次试验自动点「新建会话」隔离上下文),宿主按提示词文本队列配对判定,产出**指纹分布对比表**——每类的 Wilson 95% CI、Δ(A−B)、两比例 z 检验与显著性标记。Settings 面板可回看/切换历史实验。deus 由此从「神模工具」升级为「提示词实验台」,神模只是第一个被测假设。
32
+
33
+ ## Theme & Skin Compatibility / 主题与皮肤兼容
34
+
35
+ This plugin follows the DSH Web styling contract
36
+ ([`docs/web-styling.md`](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/web-styling.md)):
37
+ every color routes through `--dsw-alias-*` design tokens (dark literals are kept
38
+ only as fallbacks for hosts predating the alias table), with no per-theme CSS
39
+ selectors.
40
+
41
+ It also implements the [dsh-web Skin Center compatibility contract](https://github.com/zhu1090093659/dsh-web/tree/main/packages/skins/skin-center/contracts)
42
+ at **L1 (token coverage)** and **L2 (semantic attributes)**: the settings panel
43
+ root carries `data-dsh-plugin="plugin-deus"` + `data-dsh-surface="settings-modal"`,
44
+ the composer dock root carries `data-dsh-surface="chat-input"`, and skin-relevant
45
+ nodes are tagged with `data-dsh-part` (`banner` / `card` / `button-ghost` /
46
+ `input` / `textarea` / `select` / `chip` / `panel`), so third-party skins get
47
+ full restyle coverage without relying on unstable class names.
48
+
49
+ 本插件遵循 DSH Web 样式契约:全部颜色经 `--dsw-alias-*` 设计令牌引用(暗色字面量仅作旧宿主兜底),无按主题分支的 CSS 选择器;并实现 dsh-web 皮肤中心兼容契约 **L1(令牌覆盖)+ L2(语义属性)**——设置面板根节点输出 `data-dsh-plugin="plugin-deus"` + `data-dsh-surface="settings-modal"`,输入区 dock 根节点输出 `data-dsh-surface="chat-input"`,换肤相关节点标注 `data-dsh-part`,第三方皮肤无需依赖不稳定类名即可完整换肤。
31
50
 
32
51
  ## 安装 / Install
33
52
 
@@ -52,7 +71,7 @@ npx @deepseek-ai/dsh plugin --profile web add ./plugin-deus # 在其父目录
52
71
 
53
72
  - 日志只写本地 `$DSH_HOME/deus-mode/log.jsonl`,不上传、可一键清空、可导出 CSV。
54
73
  - 全部是用户正常交互(prompt 注入 + 回复观测),无越权、无绕过。
55
- - 若实验证明无差异:本插件的身份即**通用极简提示词 A/B 测试台**,"神模"只是第一个被测假设。
74
+ - 若实验证明无差异:本插件的身份即**通用极简提示词 A/B 测试台**,"神模"只是第一个被测假设。(v0.4 起实验台已内建:🧪 dock 芯片直接开跑。)
56
75
 
57
76
  ## 实测依据 / Measured basis
58
77
 
package/lib/client.js CHANGED
@@ -45,6 +45,20 @@ window.__ModuleLoader__.load({
45
45
  unanchored: '◌ 非神版会话', unanchoredTitle: '当前会话不是锚定 preset——实测 25 工具会话注入触发率≈0%。点击查看怎么切换',
46
46
  guideText: '实测结论:standard(25 工具)会话里文本注入几乎无法触发神版(0%)。想用神版:新建会话时在 Agent preset 选择器选「神模扳机 · 窄锚 / 宽锚」(实测触发率 ~90% / ~65%)。',
47
47
  copyMd: '复制 Markdown 摘要', mdCopied: '已复制 ✓ 去发帖吧', trend: '近期神版率趋势',
48
+ bench: '实验台',
49
+ benchHint: '提示词 A/B 实验台:任意两个提示词变体各跑 N 次真实会话注入(每次自动开新会话隔离上下文),对比起手式指纹分布。Wilson 95% CI + 两比例 z 检验;小样本只作倾向信号。神模只是第一个被测假设。',
50
+ benchA: '变体 A 提示词', benchB: '变体 B 提示词', benchTimes: '每变体次数 N',
51
+ benchRun: '▶ 开始实验', benchAbort: '⏹ 中止', benchRunning: '实验进行中',
52
+ benchProgress: '进度', benchWaiting: '等待回复…', benchDone: '实验完成',
53
+ benchIsolationWarn: '⚠ 未找到「新建会话」按钮,本轮在同会话内连跑,上下文未隔离',
54
+ benchTimeout: '等待回复超时(240s),实验中止',
55
+ benchEmpty: '请先填写变体 A 与变体 B 的提示词',
56
+ benchReportTitle: '指纹分布对比', benchRunId: '实验编号', benchStarted: '开始', benchFinished: '结束',
57
+ colFp: '指纹', colDelta: 'Δ(A−B)', colZ: 'z', colP: 'p', colSig: '显著',
58
+ benchNone: '还没有实验记录。在对话页输入框上方的 ⚗ 芯片行打开「实验台」跑一轮。',
59
+ benchPickRun: '选择实验', benchRefresh: '刷新',
60
+ benchDefA: '用三句话向聪明的初中生解释「熵」。',
61
+ benchDefB: '你是一位世界顶尖科普作家,回答前先在脑内推演三遍。用三句话向聪明的初中生解释「熵」。',
48
62
  }
49
63
  const en = {
50
64
  nav: 'Deus Trigger', sub: 'Minimal-prompt trigger bench · inject → classify → stats',
@@ -72,31 +86,114 @@ window.__ModuleLoader__.load({
72
86
  unanchored: '◌ Not a deus session', unanchoredTitle: 'This session is not on an anchored preset — injection measured ≈0% effective on 25-tool sessions. Click for how to switch',
73
87
  guideText: 'Measured: text injection barely triggers god-mode in standard (25-tool) sessions (0%). To get it: start a new session and pick "Deus Trigger · narrow / wide anchor" in the Agent preset picker (~90% / ~65% measured trigger rate).',
74
88
  copyMd: 'Copy Markdown summary', mdCopied: 'Copied ✓ go post it', trend: 'Recent god-rate trend',
89
+ bench: 'A/B Bench',
90
+ benchHint: 'Prompt A/B bench: run any two prompt variants N times each via real session injection (a fresh session per trial isolates context), then compare opener-fingerprint distributions. Wilson 95% CI + two-proportion z-test; small samples are directional only. God-mode is just the first hypothesis under test.',
91
+ benchA: 'Variant A prompt', benchB: 'Variant B prompt', benchTimes: 'Trials per variant N',
92
+ benchRun: '▶ Run experiment', benchAbort: '⏹ Abort', benchRunning: 'Running',
93
+ benchProgress: 'Progress', benchWaiting: 'waiting for reply…', benchDone: 'Experiment done',
94
+ benchIsolationWarn: '⚠ "New session" button not found; trials ran in the same session without context isolation',
95
+ benchTimeout: 'Timed out waiting for reply (240s); aborted',
96
+ benchEmpty: 'Fill in both variant A and variant B prompts first',
97
+ benchReportTitle: 'Fingerprint distribution comparison', benchRunId: 'Run', benchStarted: 'Started', benchFinished: 'Finished',
98
+ colFp: 'fingerprint', colDelta: 'Δ(A−B)', colZ: 'z', colP: 'p', colSig: 'sig',
99
+ benchNone: 'No experiments yet. Open the "A/B Bench" from the ⚗ chip row above the composer on a conversation page.',
100
+ benchPickRun: 'Pick run', benchRefresh: 'Refresh',
101
+ benchDefA: 'Explain "entropy" to a bright middle-schooler in three sentences.',
102
+ benchDefB: 'You are a world-class science writer; rehearse the answer three times in your head before speaking. Explain "entropy" to a bright middle-schooler in three sentences.',
75
103
  }
76
104
 
105
+ // DSH Web 样式契约(docs/web-styling.md):全部颜色经 --dsw-alias-* 令牌引用;
106
+ // 暗色字面量仅作宿主早于令牌表时的兜底。无按主题分支的选择器,皮肤可自由覆写。
77
107
  const S = {
78
- banner: { background: '#2d2406', border: '1px solid #9e6a03', borderRadius: '8px', padding: '10px 12px', fontSize: '12px', color: '#e3b341', lineHeight: '1.6', marginBottom: '14px' },
79
- status: { fontSize: '11px', color: '#8b949e', marginBottom: '10px' },
80
- card: { background: '#161b22', border: '1px solid #30363d', borderRadius: '10px', padding: '14px', marginBottom: '12px' },
108
+ banner: { background: 'var(--dsw-alias-state-warn-tertiary, #2d2406)', border: '1px solid var(--dsw-alias-state-warn-primary, #9e6a03)', borderRadius: '8px', padding: '10px 12px', fontSize: '12px', color: 'var(--dsw-alias-state-warn-label, #e3b341)', lineHeight: '1.6', marginBottom: '14px' },
109
+ status: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #8b949e)', marginBottom: '10px' },
110
+ card: { background: 'var(--dsw-alias-bg-layer-1, #161b22)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', borderRadius: '10px', padding: '14px', marginBottom: '12px' },
81
111
  row: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap', marginTop: '10px' },
82
- btn: { background: 'transparent', border: '1px solid #30363d', borderRadius: '6px', color: '#8b949e', padding: '6px 12px', fontSize: '13px', cursor: 'pointer' },
83
- btnActive: { background: '#21262d', border: '1px solid #8b949e', borderRadius: '6px', color: '#e6edf3', padding: '6px 12px', fontSize: '13px', cursor: 'pointer' },
84
- chip: { background: '#0d1117', border: '1px solid #30363d', borderRadius: '20px', color: '#8b949e', padding: '2px 10px', fontSize: '11px' },
85
- name: { fontSize: '14px', fontWeight: '700', color: '#e6edf3' },
86
- desc: { fontSize: '12px', color: '#8b949e', marginTop: '4px', lineHeight: '1.5' },
87
- h2: { fontSize: '13px', fontWeight: '600', color: '#e6edf3', margin: '18px 0 8px' },
88
- table: { width: '100%', borderCollapse: 'collapse', fontSize: '12px', color: '#c9d1d9' },
89
- th: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid #30363d', color: '#8b949e', fontWeight: '600' },
90
- td: { padding: '6px 8px', borderBottom: '1px solid #21262d' },
91
- input: { background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3', padding: '6px 8px', fontSize: '12px', width: '100%', boxSizing: 'border-box' },
92
- mono: { fontFamily: 'monospace', fontSize: '12px', color: '#c9d1d9' },
112
+ btn: { background: 'transparent', border: '1px solid var(--dsw-alias-border-l1, #30363d)', borderRadius: '6px', color: 'var(--dsw-alias-label-secondary, #8b949e)', padding: '6px 12px', fontSize: '13px', cursor: 'pointer' },
113
+ btnActive: { background: 'var(--dsw-alias-bg-layer-3, #21262d)', border: '1px solid var(--dsw-alias-label-secondary, #8b949e)', borderRadius: '6px', color: 'var(--dsw-alias-label-primary, #e6edf3)', padding: '6px 12px', fontSize: '13px', cursor: 'pointer' },
114
+ chip: { background: 'var(--dsw-alias-bg-layer-2, #0d1117)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', borderRadius: '20px', color: 'var(--dsw-alias-label-secondary, #8b949e)', padding: '2px 10px', fontSize: '11px' },
115
+ name: { fontSize: '14px', fontWeight: '700', color: 'var(--dsw-alias-label-primary, #e6edf3)' },
116
+ desc: { fontSize: '12px', color: 'var(--dsw-alias-label-secondary, #8b949e)', marginTop: '4px', lineHeight: '1.5' },
117
+ h2: { fontSize: '13px', fontWeight: '600', color: 'var(--dsw-alias-label-primary, #e6edf3)', margin: '18px 0 8px' },
118
+ table: { width: '100%', borderCollapse: 'collapse', fontSize: '12px', color: 'var(--dsw-alias-label-primary, #c9d1d9)' },
119
+ th: { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1, #30363d)', color: 'var(--dsw-alias-label-secondary, #8b949e)', fontWeight: '600' },
120
+ td: { padding: '6px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1, #21262d)' },
121
+ input: { background: 'var(--dsw-alias-bg-layer-2, #0d1117)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', borderRadius: '6px', color: 'var(--dsw-alias-label-primary, #e6edf3)', padding: '6px 8px', fontSize: '12px', width: '100%', boxSizing: 'border-box' },
122
+ mono: { fontFamily: 'monospace', fontSize: '12px', color: 'var(--dsw-alias-label-primary, #c9d1d9)' },
123
+ benchWrap: { width: '100%', background: 'var(--dsw-alias-bg-layer-2, #0d1117)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', borderRadius: '10px', padding: '10px 12px', marginBottom: '6px' },
124
+ benchTa: { width: '100%', minHeight: '52px', background: 'var(--dsw-alias-bg-layer-1, #161b22)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', borderRadius: '8px', color: 'var(--dsw-alias-label-primary, #c9d1d9)', fontSize: '12px', padding: '6px 8px', boxSizing: 'border-box', resize: 'vertical' },
125
+ benchLabel: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #8b949e)', margin: '6px 0 3px' },
126
+ benchTable: { width: '100%', borderCollapse: 'collapse', fontSize: '11px', marginTop: '6px' },
127
+ benchCell: { border: '1px solid var(--dsw-alias-border-l1, #21262d)', padding: '3px 6px', textAlign: 'right', whiteSpace: 'nowrap' },
93
128
  dock: { display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap', padding: '4px 0' },
94
- dockChip: { background: '#161b22', border: '1px solid #30363d', borderRadius: '16px', color: '#8b949e', padding: '3px 12px', fontSize: '12px', cursor: 'pointer' },
129
+ dockChip: { background: 'var(--dsw-alias-bg-layer-1, #161b22)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', borderRadius: '16px', color: 'var(--dsw-alias-label-secondary, #8b949e)', padding: '3px 12px', fontSize: '12px', cursor: 'pointer' },
95
130
  }
96
131
 
132
+ // 指纹分类强调色:god=紫 / med=绿 / pure=蓝 / unknown=灰。dsw 无图表系列令牌,
133
+ // 取语义最近的别名 + 字面量兜底。状态色统一走 state-* 令牌。
134
+ const CLS_ACCENT = {
135
+ god: 'var(--dsw-alias-state-business-primary, #d2a8ff)',
136
+ med: 'var(--dsw-alias-state-success-primary, #3fb950)',
137
+ pure: 'var(--dsw-alias-label-primary-bluish, #79c0ff)',
138
+ unknown: 'var(--dsw-alias-label-tertiary, #8b949e)',
139
+ }
140
+ const STATE_OK = 'var(--dsw-alias-state-success-primary, #3fb950)'
141
+ const STATE_ERR = 'var(--dsw-alias-state-error-primary, #f85149)'
142
+ const STATE_WARN = 'var(--dsw-alias-state-warn-primary, #d29922)'
143
+
97
144
  const pct = (ci) => `${(ci.rate * 100).toFixed(0)}% [${(ci.low * 100).toFixed(0)}–${(ci.high * 100).toFixed(0)}]`
98
145
 
99
146
  // ── tier A: settings section panel ──────────────────────────────────────
147
+ // ── v0.4 实验台:报告拉取与 A/B 对比表(Dock 控制台与设置面板共用)─────
148
+ function fetchBenchReport(runId) {
149
+ const url = runId ? '/deus/bench/report?runId=' + encodeURIComponent(runId) : '/deus/bench/report'
150
+ return fetch(url).then((r) => r.json())
151
+ }
152
+
153
+ const BENCH_CLS_COLORS = CLS_ACCENT
154
+
155
+ function benchCellFmt(g, n) {
156
+ if (!g || !n) return '—'
157
+ return g.n + '/' + n + ' · ' + Math.round(100 * g.rate) + '% [' + Math.round(100 * g.low) + '–' + Math.round(100 * g.high) + ']'
158
+ }
159
+
160
+ function BenchTable(props) {
161
+ const report = props.report
162
+ const t = props.t
163
+ if (!report) return null
164
+ const A = report.order[0]
165
+ const B = report.order[1] || 'B'
166
+ const vA = report.variants[A]
167
+ const vB = report.variants[B]
168
+ const sig = (p) => (p == null ? '' : p < 0.01 ? '✱✱' : p < 0.05 ? '✱' : '')
169
+ return h('div', { 'data-deus': 'bench-report', style: { marginTop: '8px' } },
170
+ h('div', { style: S.status },
171
+ t('benchRunId') + ': ' + report.runId + ' · n=' + report.n +
172
+ ' · ' + t('benchStarted') + ' ' + String(report.startedAt).slice(11, 19) +
173
+ ' · ' + t('benchFinished') + ' ' + String(report.finishedAt).slice(11, 19)),
174
+ h('table', { style: S.benchTable },
175
+ h('thead', null, h('tr', null,
176
+ h('th', { style: { ...S.benchCell, textAlign: 'left' } }, t('colFp')),
177
+ h('th', { style: S.benchCell }, 'A (n=' + (vA ? vA.n : 0) + ')'),
178
+ h('th', { style: S.benchCell }, 'B (n=' + (vB ? vB.n : 0) + ')'),
179
+ h('th', { style: S.benchCell }, t('colDelta')),
180
+ h('th', { style: S.benchCell }, t('colZ')),
181
+ h('th', { style: S.benchCell }, t('colP')),
182
+ h('th', { style: S.benchCell }, t('colSig')))),
183
+ h('tbody', null, (report.comparison || []).map((row) => h('tr', { key: row.cls },
184
+ h('td', { style: { ...S.benchCell, textAlign: 'left', color: BENCH_CLS_COLORS[row.cls] || CLS_ACCENT.unknown } }, t('detected_' + row.cls)),
185
+ h('td', { style: S.benchCell }, benchCellFmt(row.a, vA && vA.n)),
186
+ h('td', { style: S.benchCell }, benchCellFmt(row.b, vB && vB.n)),
187
+ h('td', { style: S.benchCell }, row.delta == null ? '—' : (row.delta >= 0 ? '+' : '') + Math.round(row.delta * 100) + 'pp'),
188
+ h('td', { style: S.benchCell }, row.z == null ? '—' : Number(row.z).toFixed(2)),
189
+ h('td', { style: S.benchCell }, row.p == null ? '—' : row.p < 0.001 ? '<0.001' : Number(row.p).toFixed(3)),
190
+ h('td', { style: { ...S.benchCell, color: STATE_WARN } }, sig(row.p)),
191
+ )))),
192
+ vA && vA.prompt ? h('div', { style: { ...S.status, marginTop: '6px' } }, 'A: ' + JSON.stringify(vA.prompt)) : null,
193
+ vB && vB.prompt ? h('div', { style: S.status }, 'B: ' + JSON.stringify(vB.prompt)) : null,
194
+ )
195
+ }
196
+
100
197
  function Panel(props) {
101
198
  const t = props.t
102
199
  const [presets, setPresets] = useState([])
@@ -107,6 +204,12 @@ window.__ModuleLoader__.load({
107
204
  const [err, setErr] = useState('')
108
205
  const [copied, setCopied] = useState('')
109
206
  const [editing, setEditing] = useState(null) // {id, label_zh, prompt} draft
207
+ const [benchData, setBenchData] = useState(null) // { ok, report, runs }
208
+ const [benchRunId, setBenchRunId] = useState('')
209
+
210
+ function refreshBench(runId) {
211
+ fetchBenchReport(runId || undefined).then((d) => setBenchData(d)).catch(() => {})
212
+ }
110
213
 
111
214
  function refresh() {
112
215
  fetch('/deus/presets').then((r) => r.json()).then((d) => setPresets(d.presets || [])).catch(() => setErr(t('loadFail')))
@@ -114,6 +217,7 @@ window.__ModuleLoader__.load({
114
217
  fetch('/deus/log').then((r) => r.json()).then((d) => setEntries(d.entries || [])).catch(() => {})
115
218
  fetch('/deus/version').then((r) => r.json()).then(setVer).catch(() => {})
116
219
  fetch('/deus/anchor').then((r) => r.json()).then(setAnchor).catch(() => {})
220
+ refreshBench(benchRunId)
117
221
  }
118
222
  useEffect(() => { refresh() }, [])
119
223
 
@@ -156,49 +260,49 @@ window.__ModuleLoader__.load({
156
260
  setEditing(null)
157
261
  }
158
262
 
159
- return h('div', { style: { maxWidth: '860px' } },
160
- h('div', { style: { fontSize: '14px', fontWeight: '600', color: '#e6edf3', marginBottom: '4px' } }, '⚗ ' + t('nav')),
263
+ return h('div', { style: { maxWidth: '860px' }, 'data-dsh-plugin': 'plugin-deus', 'data-dsh-surface': 'settings-modal' },
264
+ h('div', { style: { fontSize: '14px', fontWeight: '600', color: 'var(--dsw-alias-label-primary, #e6edf3)', marginBottom: '4px' } }, '⚗ ' + t('nav')),
161
265
  h('div', { style: S.status }, t('sub')),
162
- h('div', { style: S.banner }, '⚠️ ' + t('honesty')),
163
- err ? h('div', { style: { color: '#f85149', fontSize: '12px', marginBottom: '10px' } }, err) : null,
266
+ h('div', { style: S.banner, 'data-dsh-part': 'banner' }, '⚠️ ' + t('honesty')),
267
+ err ? h('div', { style: { color: STATE_ERR, fontSize: '12px', marginBottom: '10px' } }, err) : null,
164
268
  ver ? h('div', { style: { ...S.status, fontFamily: 'monospace' } },
165
269
  `${t('dshVer')}: ${ver.dsh} · ${t('pluginVer')}: ${ver.plugin} · ${t('logPath')}: ${ver.logPath}`) : null,
166
270
 
167
271
  h('div', { style: S.h2 }, t('presets')),
168
272
  h('div', { style: S.status }, t('presetsHint')),
169
- presets.map((p) => h('div', { key: p.id, style: S.card },
273
+ presets.map((p) => h('div', { key: p.id, style: S.card, 'data-dsh-part': 'card' },
170
274
  h('div', { style: { display: 'flex', alignItems: 'baseline', gap: '10px', flexWrap: 'wrap' } },
171
275
  h('span', { style: S.name }, p.label_zh),
172
- h('span', { style: { fontSize: '11px', color: '#8b949e' } }, p.id),
173
- p.builtin ? h('span', { style: S.chip }, 'built-in') : null,
276
+ h('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #8b949e)' } }, p.id),
277
+ p.builtin ? h('span', { style: S.chip, 'data-dsh-part': 'chip' }, 'built-in') : null,
174
278
  ),
175
279
  h('div', { style: { ...S.mono, marginTop: '6px' } }, p.prompt === null ? t('empty') : JSON.stringify(p.prompt)),
176
280
  h('div', { style: S.row },
177
- h('button', { style: copied === p.id ? S.btnActive : S.btn, onClick: () => copyPrompt(p) },
281
+ h('button', { style: copied === p.id ? S.btnActive : S.btn, 'data-dsh-part': 'button-ghost', onClick: () => copyPrompt(p) },
178
282
  copied === p.id ? '📋 ' + t('copied') : '📋 ' + t('copy')),
179
- h('button', { style: S.btn, onClick: () => setEditing({ id: p.id, label_zh: p.label_zh, prompt: p.prompt === null ? '' : p.prompt }) }, '✏️ ' + t('edit')),
180
- h('button', { style: S.btn, onClick: () => saveAll(presets.filter((x) => x.id !== p.id)) }, '🗑 ' + t('del')),
283
+ h('button', { style: S.btn, 'data-dsh-part': 'button-ghost', onClick: () => setEditing({ id: p.id, label_zh: p.label_zh, prompt: p.prompt === null ? '' : p.prompt }) }, '✏️ ' + t('edit')),
284
+ h('button', { style: S.btn, 'data-dsh-part': 'button-ghost', onClick: () => saveAll(presets.filter((x) => x.id !== p.id)) }, '🗑 ' + t('del')),
181
285
  ),
182
286
  )),
183
- editing ? h('div', { style: S.card },
287
+ editing ? h('div', { style: S.card, 'data-dsh-part': 'card' },
184
288
  h('div', { style: S.status }, t('editId')),
185
- h('input', { style: S.input, value: editing.id, onChange: (e) => setEditing({ ...editing, id: e.target.value }) }),
289
+ h('input', { style: S.input, 'data-dsh-part': 'input', value: editing.id, onChange: (e) => setEditing({ ...editing, id: e.target.value }) }),
186
290
  h('div', { style: { ...S.status, marginTop: '8px' } }, t('editLabel')),
187
- h('input', { style: S.input, value: editing.label_zh, onChange: (e) => setEditing({ ...editing, label_zh: e.target.value }) }),
291
+ h('input', { style: S.input, 'data-dsh-part': 'input', value: editing.label_zh, onChange: (e) => setEditing({ ...editing, label_zh: e.target.value }) }),
188
292
  h('div', { style: { ...S.status, marginTop: '8px' } }, t('editPrompt')),
189
- h('textarea', { style: { ...S.input, minHeight: '60px' }, value: editing.prompt, onChange: (e) => setEditing({ ...editing, prompt: e.target.value }) }),
293
+ h('textarea', { style: { ...S.input, minHeight: '60px' }, 'data-dsh-part': 'textarea', value: editing.prompt, onChange: (e) => setEditing({ ...editing, prompt: e.target.value }) }),
190
294
  h('div', { style: S.row },
191
- h('button', { style: S.btnActive, onClick: commitEdit }, '💾 ' + t('save')),
192
- h('button', { style: S.btn, onClick: () => setEditing(null) }, t('cancel')),
295
+ h('button', { style: S.btnActive, 'data-dsh-part': 'button-ghost', onClick: commitEdit }, '💾 ' + t('save')),
296
+ h('button', { style: S.btn, 'data-dsh-part': 'button-ghost', onClick: () => setEditing(null) }, t('cancel')),
193
297
  ),
194
298
  ) : h('div', { style: S.row },
195
- h('button', { style: S.btn, onClick: () => setEditing({ id: '', label_zh: '', prompt: '' }) }, '➕ ' + t('add')),
196
- h('button', { style: S.btn, onClick: resetPresets }, '↩ ' + t('resetPresets')),
299
+ h('button', { style: S.btn, 'data-dsh-part': 'button-ghost', onClick: () => setEditing({ id: '', label_zh: '', prompt: '' }) }, '➕ ' + t('add')),
300
+ h('button', { style: S.btn, 'data-dsh-part': 'button-ghost', onClick: resetPresets }, '↩ ' + t('resetPresets')),
197
301
  ),
198
302
 
199
303
  h('div', { style: S.h2 }, t('stats')),
200
304
  h('div', { style: S.status }, t('statsHint')),
201
- h('div', { style: { ...S.status, color: '#7d8590' } }, t('measured')),
305
+ h('div', { style: { ...S.status, color: 'var(--dsw-alias-label-tertiary, #7d8590)' } }, t('measured')),
202
306
  h(Sparkline, { entries }),
203
307
  entries.length >= 4 ? h('div', { style: { ...S.status, fontSize: '10px' } }, t('trend')) : null,
204
308
  stats && stats.modes.length > 0
@@ -209,7 +313,7 @@ window.__ModuleLoader__.load({
209
313
  h('tbody', null, stats.modes.map((m) => h('tr', { key: m.mode },
210
314
  h('td', { style: S.td }, m.mode),
211
315
  h('td', { style: S.td }, m.n),
212
- h('td', { style: { ...S.td, color: '#d2a8ff' } }, pct(m.godCI)),
316
+ h('td', { style: { ...S.td, color: CLS_ACCENT.god } }, pct(m.godCI)),
213
317
  h('td', { style: S.td }, pct(m.medCI)),
214
318
  h('td', { style: S.td }, pct(m.pureCI)),
215
319
  ))),
@@ -219,6 +323,7 @@ window.__ModuleLoader__.load({
219
323
  h('a', { href: '/deus/log.csv', download: 'deus-log.csv', style: { ...S.btn, textDecoration: 'none', display: 'inline-block' } }, '⬇ ' + t('exportCsv')),
220
324
  stats ? h('button', {
221
325
  style: copied === '__md' ? S.btnActive : S.btn,
326
+ 'data-dsh-part': 'button-ghost',
222
327
  onClick: async () => {
223
328
  try {
224
329
  await navigator.clipboard.writeText(buildMarkdownSummary(stats))
@@ -226,7 +331,7 @@ window.__ModuleLoader__.load({
226
331
  } catch {}
227
332
  },
228
333
  }, '📋 ' + (copied === '__md' ? t('mdCopied') : t('copyMd'))) : null,
229
- h('button', { style: { ...S.btn, color: '#f85149', borderColor: '#f85149' }, onClick: resetLog }, '🗑 ' + t('resetLog')),
334
+ h('button', { style: { ...S.btn, color: STATE_ERR, borderColor: STATE_ERR }, 'data-dsh-part': 'button-ghost', onClick: resetLog }, '🗑 ' + t('resetLog')),
230
335
  ),
231
336
 
232
337
  h('div', { style: S.h2 }, t('anchor')),
@@ -234,7 +339,7 @@ window.__ModuleLoader__.load({
234
339
  anchor && anchor.agentPresets && h('div', { style: S.card },
235
340
  h('div', { style: S.status }, t('anchorPresetInstalled')),
236
341
  anchor.agentPresets.map((p) => h('div', { key: p.id, style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '6px' } },
237
- h('span', { style: { ...S.chip, color: p.installed ? '#3fb950' : '#f85149', borderColor: p.installed ? '#3fb950' : '#f85149' } }, p.installed ? '✓' : '✗'),
342
+ h('span', { style: { ...S.chip, color: p.installed ? STATE_OK : STATE_ERR, borderColor: p.installed ? STATE_OK : STATE_ERR }, 'data-dsh-part': 'chip' }, p.installed ? '✓' : '✗'),
238
343
  h('span', { style: S.name }, p.name || p.id),
239
344
  p.reason ? h('span', { style: S.status }, '(' + p.reason + ')') : null,
240
345
  )),
@@ -249,22 +354,39 @@ window.__ModuleLoader__.load({
249
354
  h('td', { style: { ...S.td, ...S.mono } }, s.sessionId.slice(0, 8)),
250
355
  h('td', { style: S.td }, s.preset),
251
356
  h('td', { style: S.td }, s.total),
252
- h('td', { style: { ...S.td, color: '#d2a8ff' } }, s.total ? Math.round(100 * s.god / s.total) + '%' : '—'),
357
+ h('td', { style: { ...S.td, color: CLS_ACCENT.god } }, s.total ? Math.round(100 * s.god / s.total) + '%' : '—'),
253
358
  h('td', { style: S.td }, s.lastFp ? t('detected_' + s.lastFp) : '—'),
254
- h('td', { style: { ...S.td, color: s.drifted ? '#f85149' : '#3fb950' } }, s.drifted ? '⚠ ' + t('drifted') : '⚓ ' + t('anchoredOn')),
359
+ h('td', { style: { ...S.td, color: s.drifted ? STATE_ERR : STATE_OK } }, s.drifted ? '⚠ ' + t('drifted') : '⚓ ' + t('anchoredOn')),
255
360
  ))),
256
361
  )
257
362
  : h('div', { style: S.status }, t('anchorNone')),
258
363
 
364
+ h('div', { style: S.h2 }, '🧪 ' + t('bench')),
365
+ h('div', { style: S.status }, t('benchHint')),
366
+ h('div', { style: S.row },
367
+ h('button', { style: S.btn, 'data-deus': 'bench-refresh', 'data-dsh-part': 'button-ghost', onClick: () => refreshBench(benchRunId) }, '🔄 ' + t('benchRefresh')),
368
+ benchData && benchData.runs && benchData.runs.length > 0 ? h('select', {
369
+ style: { ...S.input, width: 'auto' }, 'data-deus': 'bench-runs', 'data-dsh-part': 'select',
370
+ value: benchRunId,
371
+ onChange: (e) => { setBenchRunId(e.target.value); refreshBench(e.target.value) },
372
+ },
373
+ h('option', { value: '' }, t('benchPickRun')),
374
+ benchData.runs.map((r) => h('option', { key: r.runId, value: r.runId }, r.runId + ' · n=' + r.n)),
375
+ ) : null,
376
+ ),
377
+ benchData && benchData.report
378
+ ? h(BenchTable, { report: benchData.report, t })
379
+ : h('div', { style: S.status, 'data-deus': 'bench-empty' }, t('benchNone')),
380
+
259
381
  h('div', { style: S.h2 }, t('recent')),
260
382
  h('div', { style: S.status }, t('recentHint')),
261
383
  entries.length === 0 ? h('div', { style: S.status }, t('noData'))
262
384
  : entries.slice(0, 10).map((e, i) => h('div', { key: i, style: { ...S.card, padding: '10px 12px' } },
263
385
  h('div', { style: { display: 'flex', gap: '8px', alignItems: 'baseline', flexWrap: 'wrap' } },
264
- h('span', { style: S.chip }, e.prompt_mode),
265
- h('span', { style: { ...S.chip, color: e.detected === 'god' ? '#d2a8ff' : e.detected === 'pure' ? '#79c0ff' : e.detected === 'med' ? '#3fb950' : '#8b949e' } },
386
+ h('span', { style: S.chip, 'data-dsh-part': 'chip' }, e.prompt_mode),
387
+ h('span', { style: { ...S.chip, color: e.detected === 'god' ? CLS_ACCENT.god : e.detected === 'pure' ? CLS_ACCENT.pure : e.detected === 'med' ? CLS_ACCENT.med : CLS_ACCENT.unknown }, 'data-dsh-part': 'chip' },
266
388
  t('detected_' + (e.detected || 'unknown'))),
267
- h('span', { style: { fontSize: '11px', color: '#8b949e' } }, e.ts),
389
+ h('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary, #8b949e)' } }, e.ts),
268
390
  ),
269
391
  h('div', { style: { ...S.mono, marginTop: '6px', whiteSpace: 'pre-wrap' } }, e.first_sentence),
270
392
  )),
@@ -297,6 +419,129 @@ window.__ModuleLoader__.load({
297
419
  const [sessPreset, setSessPreset] = useState(null)
298
420
  const [showGuide, setShowGuide] = useState(false)
299
421
  const lastReanchorKey = useRef('')
422
+
423
+ // ── v0.4 实验台(A/B bench)──────────────────────────────────────────
424
+ const [benchOpen, setBenchOpen] = useState(false)
425
+ const [benchA, setBenchA] = useState(() => {
426
+ try { return window.localStorage.getItem('deus.benchA') || t('benchDefA') } catch { return t('benchDefA') }
427
+ })
428
+ const [benchB, setBenchB] = useState(() => {
429
+ try { return window.localStorage.getItem('deus.benchB') || t('benchDefB') } catch { return t('benchDefB') }
430
+ })
431
+ const [benchN, setBenchN] = useState(3)
432
+ const [benchRunning, setBenchRunning] = useState(false)
433
+ const [benchProg, setBenchProg] = useState(null) // { a, b, n, label, waiting }
434
+ const [benchReport, setBenchReport] = useState(null)
435
+ const [benchNote, setBenchNote] = useState('')
436
+ const benchAbort = useRef(false)
437
+ const benchRunIdRef = useRef('')
438
+ const sidRef = useRef(sessionId)
439
+ const actionsRef = useRef(actions)
440
+ useEffect(() => { sidRef.current = sessionId; actionsRef.current = actions })
441
+
442
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)) }
443
+
444
+ // 每次试验开一个新会话以隔离上下文:点侧栏「新建会话」按钮,
445
+ // 然后等 dock 的 sessionId prop 变化(运行时重渲染本会带回新 id)。
446
+ async function benchNewSession(prevSid) {
447
+ const sels = [
448
+ 'button[aria-label="新建会话"]', 'button[aria-label="New session"]',
449
+ '[role="button"][aria-label="新建会话"]', '[role="button"][aria-label="New session"]',
450
+ ]
451
+ let btn = null
452
+ for (const sel of sels) { btn = document.querySelector(sel); if (btn) break }
453
+ if (!btn) {
454
+ btn = Array.from(document.querySelectorAll('button, [role="button"]'))
455
+ .find((b) => /新建会话|New session/i.test((b.getAttribute('aria-label') || '') + ' ' + (b.title || '')))
456
+ }
457
+ if (!btn) return false
458
+ btn.click()
459
+ const t0 = Date.now()
460
+ while (Date.now() - t0 < 10000) {
461
+ await sleep(300)
462
+ if (String(sidRef.current || '') !== String(prevSid || '')) return true
463
+ }
464
+ return false
465
+ }
466
+
467
+ async function abortBench() {
468
+ benchAbort.current = true
469
+ try {
470
+ await fetch('/deus/bench/clear', {
471
+ method: 'POST', headers: { 'content-type': 'application/json' },
472
+ body: JSON.stringify({ runId: benchRunIdRef.current }),
473
+ })
474
+ } catch { /* best-effort */ }
475
+ }
476
+
477
+ async function runBench() {
478
+ const aText = benchA.trim()
479
+ const bText = benchB.trim()
480
+ if (!aText || !bText) { setBenchNote(t('benchEmpty')); return }
481
+ setBenchRunning(true); setBenchNote(''); setBenchReport(null)
482
+ benchAbort.current = false
483
+ const runId = 'bench-' + Date.now().toString(36)
484
+ benchRunIdRef.current = runId
485
+ const n = Math.max(1, Math.min(20, Math.floor(Number(benchN) || 1)))
486
+ const trials = []
487
+ for (let i = 0; i < n; i++) { trials.push(['A', aText, i + 1]); trials.push(['B', bText, i + 1]) }
488
+ let doneA = 0
489
+ let doneB = 0
490
+ const total = trials.length
491
+ try { window.__deusBench = { running: true, runId, done: 0, total } } catch { /* ignore */ }
492
+ try {
493
+ for (const [variant, text, idx] of trials) {
494
+ if (benchAbort.current) break
495
+ setBenchProg({ a: doneA, b: doneB, n, label: variant + '#' + idx, waiting: false })
496
+ // 1) 新会话隔离(失败则降级为同会话连跑并亮警告)
497
+ const prevSid = sidRef.current
498
+ const switched = await benchNewSession(prevSid)
499
+ if (!switched) { setBenchNote(t('benchIsolationWarn')); await sleep(400) }
500
+ if (benchAbort.current) break
501
+ // 2) 等 composer actions 就绪
502
+ const w0 = Date.now()
503
+ while (!(actionsRef.current && typeof actionsRef.current.setDraft === 'function')) {
504
+ if (Date.now() - w0 > 8000) break
505
+ await sleep(250)
506
+ }
507
+ // 3) 宿主标记(文本队列配对)→ 注入并发送
508
+ try {
509
+ await fetch('/deus/bench/mark', {
510
+ method: 'POST', headers: { 'content-type': 'application/json' },
511
+ body: JSON.stringify({ runId, variant, prompt: text }),
512
+ })
513
+ } catch { /* marking best-effort */ }
514
+ const acts = actionsRef.current
515
+ if (!acts || typeof acts.setDraft !== 'function' || typeof acts.submit !== 'function') {
516
+ setBenchNote(t('benchTimeout')); break
517
+ }
518
+ acts.setDraft(text)
519
+ await sleep(200)
520
+ acts.submit()
521
+ // 4) 轮询报告计数,等宿主把这轮的指纹判定写进日志
522
+ setBenchProg({ a: doneA, b: doneB, n, label: variant + '#' + idx, waiting: true })
523
+ const doneSoFar = doneA + doneB
524
+ const t0 = Date.now()
525
+ let arrived = false
526
+ while (Date.now() - t0 < 240000) {
527
+ if (benchAbort.current) break
528
+ await sleep(2000)
529
+ const rep = await fetchBenchReport(runId).catch(() => null)
530
+ if (rep && rep.report && rep.report.n >= doneSoFar + 1) { arrived = true; break }
531
+ }
532
+ if (!arrived && !benchAbort.current) { setBenchNote(t('benchTimeout')); break }
533
+ if (variant === 'A') doneA++; else doneB++
534
+ try { window.__deusBench = { running: true, runId, done: doneA + doneB, total } } catch { /* ignore */ }
535
+ await sleep(600)
536
+ }
537
+ } finally {
538
+ setBenchRunning(false)
539
+ setBenchProg(null)
540
+ try { window.__deusBench = { running: false, runId, done: doneA + doneB, total } } catch { /* ignore */ }
541
+ }
542
+ const fin = await fetchBenchReport(runId).catch(() => null)
543
+ if (fin && fin.report) setBenchReport(fin.report)
544
+ }
300
545
  const REANCHOR = 'We need to continue working on this together. Let us pick up where we left off. 我们继续协作,接着上一步往下做。'
301
546
 
302
547
  useEffect(() => {
@@ -374,34 +619,72 @@ window.__ModuleLoader__.load({
374
619
  const UNANCHORED_PRESET_RE = /^(deus-|minimal)/
375
620
  const unanchored = !st && sessPreset && !UNANCHORED_PRESET_RE.test(sessPreset)
376
621
 
377
- return h('div', { style: S.dock, title: t('dockHint') },
378
- h('span', { style: { fontSize: '11px', color: '#8b949e' } }, ''),
622
+ const benchPanel = benchOpen ? h('div', { style: S.benchWrap, 'data-deus': 'bench-panel', 'data-dsh-part': 'panel' },
623
+ h('div', { style: { fontSize: '12px', fontWeight: 600, color: 'var(--dsw-alias-label-primary, #e6edf3)' } }, '🧪 ' + t('bench')),
624
+ h('div', { style: S.status }, t('benchHint')),
625
+ h('div', { style: S.benchLabel }, t('benchA')),
626
+ h('textarea', {
627
+ style: S.benchTa, 'data-deus': 'bench-a', 'data-dsh-part': 'textarea', value: benchA, disabled: benchRunning,
628
+ onChange: (e) => { setBenchA(e.target.value); try { window.localStorage.setItem('deus.benchA', e.target.value) } catch { /* ignore */ } },
629
+ }),
630
+ h('div', { style: S.benchLabel }, t('benchB')),
631
+ h('textarea', {
632
+ style: S.benchTa, 'data-deus': 'bench-b', 'data-dsh-part': 'textarea', value: benchB, disabled: benchRunning,
633
+ onChange: (e) => { setBenchB(e.target.value); try { window.localStorage.setItem('deus.benchB', e.target.value) } catch { /* ignore */ } },
634
+ }),
635
+ h('div', { style: { ...S.row, marginTop: '8px', alignItems: 'center' } },
636
+ h('span', { style: { ...S.benchLabel, margin: 0 } }, t('benchTimes')),
637
+ h('input', {
638
+ type: 'number', min: 1, max: 20, 'data-deus': 'bench-n', value: benchN, disabled: benchRunning,
639
+ style: { background: 'var(--dsw-alias-bg-layer-1, #161b22)', border: '1px solid var(--dsw-alias-border-l1, #30363d)', borderRadius: '8px', color: 'var(--dsw-alias-label-primary, #c9d1d9)', fontSize: '12px', padding: '4px 6px', width: '60px' },
640
+ onChange: (e) => setBenchN(e.target.value),
641
+ }),
642
+ benchRunning
643
+ ? h('button', { style: { ...S.btn, color: STATE_ERR, borderColor: STATE_ERR }, 'data-deus': 'bench-abort', 'data-dsh-part': 'button-ghost', onClick: abortBench }, t('benchAbort'))
644
+ : h('button', { style: S.btnActive, 'data-deus': 'bench-run', 'data-dsh-part': 'button-ghost', onClick: runBench }, t('benchRun')),
645
+ ),
646
+ benchProg ? h('div', { style: S.status, 'data-deus': 'bench-progress' },
647
+ t('benchRunning') + ' · ' + t('benchProgress') + ': A ' + benchProg.a + '/' + benchProg.n + ' · B ' + benchProg.b + '/' + benchProg.n + ' · ' + benchProg.label + (benchProg.waiting ? ' ' + t('benchWaiting') : '')) : null,
648
+ benchNote ? h('div', { style: { ...S.status, color: STATE_WARN }, 'data-deus': 'bench-note' }, benchNote) : null,
649
+ benchReport ? h(BenchTable, { report: benchReport, t }) : null,
650
+ ) : null
651
+
652
+ const dockRow = h('div', { style: S.dock, title: t('dockHint') },
653
+ h('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #8b949e)' } }, '⚗'),
379
654
  presets.map((p) => h('button', {
380
- key: p.id, style: S.dockChip, title: t('dockHint'),
655
+ key: p.id, style: S.dockChip, 'data-dsh-part': 'chip', title: t('dockHint'),
381
656
  onClick: () => fire(p),
382
657
  }, (autoSend ? '🚀 ' : '⚡ ') + p.label_zh)),
383
658
  st ? h('button', {
384
- style: { ...S.dockChip, color: st.drifted ? '#f85149' : '#3fb950', borderColor: st.drifted ? '#f85149' : '#3fb950' },
659
+ style: { ...S.dockChip, color: st.drifted ? STATE_ERR : STATE_OK, borderColor: st.drifted ? STATE_ERR : STATE_OK },
660
+ 'data-dsh-part': 'chip',
385
661
  title: st.drifted ? t('reanchor') : t('dockAnchored'),
386
662
  onClick: st.drifted ? fireReanchor : undefined,
387
663
  }, st.drifted ? '⚠ ' + t('reanchor') : `⚓ ${t('dockAnchored')} ${st.god}/${st.total}`) : null,
388
664
  unanchored ? h('button', {
389
- style: { ...S.dockChip, color: '#8b949e', borderColor: '#8b949e', borderStyle: 'dotted' },
665
+ style: { ...S.dockChip, color: 'var(--dsw-alias-label-secondary, #8b949e)', borderColor: 'var(--dsw-alias-border-l1, #8b949e)', borderStyle: 'dotted' },
666
+ 'data-dsh-part': 'chip',
390
667
  title: t('unanchoredTitle'),
391
668
  onClick: () => setShowGuide((v) => !v),
392
669
  }, t('unanchored')) : null,
393
670
  unanchored && showGuide ? h('div', {
394
671
  style: {
395
- fontSize: '11px', color: '#8b949e', lineHeight: 1.6, maxWidth: '520px',
396
- borderLeft: '2px solid #8b949e', paddingLeft: '8px', margin: '2px 0',
672
+ fontSize: '11px', color: 'var(--dsw-alias-label-secondary, #8b949e)', lineHeight: 1.6, maxWidth: '520px',
673
+ borderLeft: '2px solid var(--dsw-alias-border-l1, #8b949e)', paddingLeft: '8px', margin: '2px 0',
397
674
  },
398
675
  }, t('guideText')) : null,
399
- reanchorMsg ? h('span', { style: { fontSize: '11px', color: '#3fb950' } }, reanchorMsg) : null,
400
- h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoSend'), onClick: toggleAuto },
676
+ reanchorMsg ? h('span', { style: { fontSize: '11px', color: STATE_OK } }, reanchorMsg) : null,
677
+ h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, 'data-dsh-part': 'chip', title: t('autoSend'), onClick: toggleAuto },
401
678
  (autoSend ? '☑ ' : '☐ ') + t('autoSend')),
402
- st ? h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoReanchor'), onClick: toggleAutoReanchor },
679
+ st ? h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, 'data-dsh-part': 'chip', title: t('autoReanchor'), onClick: toggleAutoReanchor },
403
680
  (autoReanchor ? '☑ ' : '☐ ') + t('autoReanchor')) : null,
681
+ h('button', {
682
+ style: { ...S.dockChip, borderStyle: 'dashed', color: benchOpen ? CLS_ACCENT.god : 'var(--dsw-alias-label-secondary, #8b949e)', borderColor: benchOpen ? CLS_ACCENT.god : 'var(--dsw-alias-border-l1, #30363d)' },
683
+ title: t('bench'), 'data-deus': 'bench-chip', 'data-dsh-part': 'chip',
684
+ onClick: () => setBenchOpen((v) => !v),
685
+ }, '🧪 ' + t('bench')),
404
686
  )
687
+ return h('div', { style: { width: '100%' }, 'data-dsh-plugin': 'plugin-deus', 'data-dsh-surface': 'chat-input' }, benchPanel, dockRow)
405
688
  }
406
689
 
407
690
  // v0.3: 触发率迷你趋势(最近 40 条日志分 10 桶的 god 率折线)
@@ -421,10 +704,11 @@ window.__ModuleLoader__.load({
421
704
  const xy = pts.map(([i, v]) => [P + (W - 2 * P) * i / (B - 1), H - P - (H - 2 * P) * v])
422
705
  const dAttr = xy.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ',' + p[1].toFixed(1)).join(' ')
423
706
  return h('svg', { width: W, height: H, viewBox: `0 0 ${W} ${H}`, style: { display: 'block', margin: '4px 0 2px' } },
424
- h('path', { d: `M${P},${H - P} L${W - P},${H - P}`, stroke: '#30363d', strokeWidth: 1, fill: 'none' }),
425
- h('path', { d: `M${P},${P} L${W - P},${P}`, stroke: '#30363d', strokeWidth: 1, strokeDasharray: '3 3', fill: 'none' }),
426
- h('path', { d: dAttr, stroke: '#d2a8ff', strokeWidth: 2, fill: 'none' }),
427
- xy.map((p, i) => h('circle', { key: i, cx: p[0], cy: p[1], r: 2.5, fill: '#d2a8ff' })),
707
+ // var() SVG 表现属性里不解析,必须走 style(CSS 层才能吃自定义属性)
708
+ h('path', { d: `M${P},${H - P} L${W - P},${H - P}`, style: { stroke: 'var(--dsw-alias-border-l1, #30363d)' }, strokeWidth: 1, fill: 'none' }),
709
+ h('path', { d: `M${P},${P} L${W - P},${P}`, style: { stroke: 'var(--dsw-alias-border-l1, #30363d)' }, strokeWidth: 1, strokeDasharray: '3 3', fill: 'none' }),
710
+ h('path', { d: dAttr, style: { stroke: CLS_ACCENT.god }, strokeWidth: 2, fill: 'none' }),
711
+ xy.map((p, i) => h('circle', { key: i, cx: p[0], cy: p[1], r: 2.5, style: { fill: CLS_ACCENT.god } })),
428
712
  )
429
713
  }
430
714
 
@@ -453,6 +737,7 @@ window.__ModuleLoader__.load({
453
737
  inject: ['slots', 'locale'],
454
738
  apply(ctx) {
455
739
  try {
740
+ try { window.__deusClient = '0.4.1' } catch { /* non-browser */ }
456
741
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'deus-mode: dicts')
457
742
  const t = ctx.locale.bind(NS)
458
743
  ctx.slots.inject('settings.section', () => ctx.slots.register({
package/lib/index.js CHANGED
@@ -78,8 +78,27 @@ function wilson(k, n) {
78
78
  return { rate: p, low: Math.max(0, center - half), high: Math.min(1, center + half) }
79
79
  }
80
80
 
81
+ // ── 两比例 z 检验(v0.4 实验台 A/B 对比用)─────────────────────────────────
82
+ // 标准正态 CDF(Abramowitz–Stegun 7.1.26 近似,|ε| < 7.5e-8)
83
+ function normalCdf(x) {
84
+ const t = 1 / (1 + 0.2316419 * Math.abs(x))
85
+ const d = 0.3989423 * Math.exp((-x * x) / 2)
86
+ const p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))))
87
+ return x > 0 ? 1 - p : p
88
+ }
89
+
90
+ /** 两比例 z 检验(双侧)。任一样本为空 → null。 */
91
+ function twoPropZ(kA, nA, kB, nB) {
92
+ if (!nA || !nB) return null
93
+ const pooled = (kA + kB) / (nA + nB)
94
+ const se = Math.sqrt(pooled * (1 - pooled) * (1 / nA + 1 / nB))
95
+ if (!se) return { z: 0, p: 1 }
96
+ const z = (kA / nA - kB / nB) / se
97
+ return { z, p: 2 * (1 - normalCdf(Math.abs(z))) }
98
+ }
99
+
81
100
  // ── shipped agent presets (v0.2 锚定维持的静态层) ───────────────────────────
82
- const PLUGIN_VERSION = '0.3.0'
101
+ const PLUGIN_VERSION = '0.4.0'
83
102
  // 实测依据(research/deus-mode-matrix.md §8/§9):神版触发需要 Minimal persona
84
103
  // ∧ 小工具目录;且本 harness 没有竞品的 promoteOn 机制——preset 构成全程恒定,
85
104
  // 所以只要会话跑在锚定 preset 上,注入剥离/工具裁剪自动延续到每一轮。
@@ -300,6 +319,8 @@ function statsFrom(entries) {
300
319
  const byMode = new Map()
301
320
  for (const e of entries) {
302
321
  const mode = e.prompt_mode || 'unknown'
322
+ if (mode === 'bench') continue // v0.4: 实验台样本有独立的 A/B 报告,不混入神模统计
323
+
303
324
  if (!byMode.has(mode)) byMode.set(mode, { mode, n: 0, pure: 0, med: 0, god: 0, unknown: 0 })
304
325
  const row = byMode.get(mode)
305
326
  row.n += 1
@@ -320,13 +341,85 @@ function statsFrom(entries) {
320
341
  }
321
342
 
322
343
  function toCsv(entries) {
323
- const head = 'ts,sessionId,prompt_mode,prompt_text,detected,source,turn,first_sentence,model'
344
+ const head = 'ts,sessionId,prompt_mode,prompt_text,detected,source,turn,first_sentence,model,bench_run,bench_variant'
324
345
  const esc = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`
325
346
  const rows = entries.map((e) =>
326
- [e.ts, e.sessionId, e.prompt_mode, e.prompt_text, e.detected, e.source, e.turn, e.first_sentence, e.model].map(esc).join(','))
347
+ [e.ts, e.sessionId, e.prompt_mode, e.prompt_text, e.detected, e.source, e.turn, e.first_sentence, e.model, e.bench_run, e.bench_variant].map(esc).join(','))
327
348
  return [head, ...rows].join('\n') + '\n'
328
349
  }
329
350
 
351
+ // ── v0.4 实验台:A/B 报告(指纹分布对比 + Wilson CI + 两比例 z 检验)────────
352
+ // 设计文档「纯噪音退路」转正:面板输入任意提示词变体 A/B,各跑 N 次真实会话
353
+ // 注入,这里把 log 里 prompt_mode='bench' 的样本按 bench_run/bench_variant
354
+ // 分组,输出每类的 Wilson 95% CI 与 A−B 差异检验。神模只是第一个被测假设。
355
+ const BENCH_CLASSES = ['god', 'med', 'pure', 'unknown']
356
+
357
+ export function benchReportFrom(entries, runId) {
358
+ const bench = entries.filter((e) => e.prompt_mode === 'bench' && e.bench_run)
359
+ const runMap = new Map()
360
+ for (const e of bench) {
361
+ let r = runMap.get(e.bench_run)
362
+ if (!r) {
363
+ r = { runId: e.bench_run, n: 0, firstTs: e.ts, lastTs: e.ts, variants: new Set() }
364
+ runMap.set(e.bench_run, r)
365
+ }
366
+ r.n += 1
367
+ if (e.ts < r.firstTs) r.firstTs = e.ts
368
+ if (e.ts > r.lastTs) r.lastTs = e.ts
369
+ if (e.bench_variant) r.variants.add(e.bench_variant)
370
+ }
371
+ const runs = [...runMap.values()]
372
+ .map((r) => ({ runId: r.runId, n: r.n, firstTs: r.firstTs, lastTs: r.lastTs, variants: [...r.variants].sort() }))
373
+ .sort((a, b) => (a.lastTs < b.lastTs ? 1 : -1))
374
+ const picked = runId ? runs.find((r) => r.runId === runId) : runs[0]
375
+ if (!picked) return { report: null, runs }
376
+ const rows = bench.filter((e) => e.bench_run === picked.runId)
377
+ const byVariant = {}
378
+ for (const e of rows) {
379
+ const v = String(e.bench_variant || '?')
380
+ const g = (byVariant[v] ||= { variant: v, n: 0, counts: { god: 0, med: 0, pure: 0, unknown: 0 }, prompt: null })
381
+ g.n += 1
382
+ if (g.counts[e.detected] !== undefined) g.counts[e.detected] += 1
383
+ else g.counts.unknown += 1
384
+ if (!g.prompt && e.prompt_text) g.prompt = e.prompt_text
385
+ }
386
+ for (const g of Object.values(byVariant)) {
387
+ g.classes = {}
388
+ for (const c of BENCH_CLASSES) {
389
+ g.classes[c] = { n: g.counts[c], ...wilson(g.counts[c], g.n) }
390
+ }
391
+ }
392
+ const order = Object.keys(byVariant).sort()
393
+ const comparison = []
394
+ if (order.length >= 2) {
395
+ const A = byVariant[order[0]]
396
+ const B = byVariant[order[1]]
397
+ for (const c of BENCH_CLASSES) {
398
+ const t = twoPropZ(A.counts[c], A.n, B.counts[c], B.n)
399
+ comparison.push({
400
+ cls: c,
401
+ a: A.classes[c],
402
+ b: B.classes[c],
403
+ delta: A.n && B.n ? A.counts[c] / A.n - B.counts[c] / B.n : null,
404
+ z: t ? t.z : null,
405
+ p: t ? t.p : null,
406
+ })
407
+ }
408
+ }
409
+ return {
410
+ report: {
411
+ runId: picked.runId,
412
+ n: rows.length,
413
+ variants: byVariant,
414
+ order,
415
+ comparison,
416
+ startedAt: picked.firstTs,
417
+ finishedAt: picked.lastTs,
418
+ },
419
+ runs,
420
+ }
421
+ }
422
+
330
423
  // ── version auto-detect (differentiator: no manual checking) ────────────────
331
424
  function detectDshVersion() {
332
425
  if (process.env.DSH_VERSION) return process.env.DSH_VERSION
@@ -386,6 +479,10 @@ export function apply(ctx) {
386
479
 
387
480
  // Pending trigger marks: sessionId -> { mode, prompt_text, ts, auto }
388
481
  const pending = new Map()
482
+ // v0.4 实验台:待配对的 bench 样本队列 [{ runId, variant, prompt, ts }]
483
+ // (文本精确匹配配对,见 /deus/bench/mark 路由注释)
484
+ const benchQueue = []
485
+ const BENCH_QUEUE_TTL = 10 * 60 * 1000
389
486
  // Chunk accumulation while a trigger is pending: sessionId -> { turn, step, reasoning, text }
390
487
  // (实测校准:指纹在 reasoning-delta 流里,text-delta 兜底;见 detectMode 注释)
391
488
  const buffers = new Map()
@@ -466,6 +563,9 @@ export function apply(ctx) {
466
563
  source,
467
564
  first_sentence: firstSentenceOf(classifyOn),
468
565
  model: mark.model || undefined,
566
+ // v0.4 实验台:A/B 样本归属(仅 bench 标记的注入轮带这两个字段)
567
+ bench_run: mark.benchRun || undefined,
568
+ bench_variant: mark.benchVariant || undefined,
469
569
  })
470
570
  }
471
571
 
@@ -486,6 +586,15 @@ export function apply(ctx) {
486
586
  if (event.type === 'user/message') {
487
587
  const text = textOf(event.data && event.data.content).trim()
488
588
  if (text !== '' && !pending.has(sessionId)) {
589
+ // v0.4 实验台:bench 队列优先于 preset exact-match(显式实验意图优先)
590
+ const now = Date.now()
591
+ const bi = benchQueue.findIndex((q) => q.prompt === text && now - q.ts < BENCH_QUEUE_TTL)
592
+ if (bi >= 0) {
593
+ const q = benchQueue.splice(bi, 1)[0]
594
+ buffers.delete(sessionId)
595
+ pending.set(sessionId, { mode: 'bench', prompt_text: q.prompt, benchRun: q.runId, benchVariant: q.variant, ts: now, auto: false })
596
+ return
597
+ }
489
598
  const hit = loadPresets().find((p) => p.prompt !== null && p.prompt.trim() === text)
490
599
  if (hit) {
491
600
  buffers.delete(sessionId) // 防止上轮残留 buffer 污染注入轮判定
@@ -666,6 +775,42 @@ export function apply(ctx) {
666
775
  if (!sessionId) { json({ ok: false, error: 'missing sessionId' }, 400)(req, res); return }
667
776
  json({ sessionId, preset: readSessionPreset(sessionId) })(req, res)
668
777
  } }),
778
+ // v0.4 实验台:A/B 变体注入标记。与 /deus/trigger 的 sessionId 预标记不同,
779
+ // 这里走「提示词文本队列」配对(与档 A 的 exact-match 同思路):客户端每次
780
+ // 试验前把 (runId, variant, prompt) 入队,user/message 到达时按文本精确
781
+ // 匹配出队并标记该会话注入轮。这样 bench 不依赖「新会话 id 何时出现」
782
+ // 的时序——新建空白会话在首条消息前可能没有稳定 id。
783
+ ctx.webServer.register({ kind: 'exact', path: '/deus/bench/mark', handler: (req, res) => {
784
+ readBody(req, (body) => {
785
+ const runId = String(body.runId || '').slice(0, 64)
786
+ const variant = String(body.variant || '').slice(0, 16)
787
+ const prompt = String(body.prompt || '').slice(0, 4000)
788
+ if (!runId || !variant || !prompt) {
789
+ json({ ok: false, error: 'missing runId / variant / prompt' }, 400)(req, res)
790
+ return
791
+ }
792
+ benchQueue.push({ runId, variant, prompt, ts: Date.now() })
793
+ if (benchQueue.length > 200) benchQueue.splice(0, benchQueue.length - 200)
794
+ json({ ok: true, queued: benchQueue.length })(req, res)
795
+ })
796
+ } }),
797
+ // 中止/清理:丢弃某个 run(或全部)未消耗的队列项
798
+ ctx.webServer.register({ kind: 'exact', path: '/deus/bench/clear', handler: (req, res) => {
799
+ readBody(req, (body) => {
800
+ const runId = String(body.runId || '')
801
+ const before = benchQueue.length
802
+ for (let i = benchQueue.length - 1; i >= 0; i--) {
803
+ if (!runId || benchQueue[i].runId === runId) benchQueue.splice(i, 1)
804
+ }
805
+ json({ ok: true, dropped: before - benchQueue.length })(req, res)
806
+ })
807
+ } }),
808
+ // v0.4 实验台:A/B 报告(?runId= 指定,缺省取最近一次 run;附全部 run 列表)
809
+ ctx.webServer.register({ kind: 'exact', path: '/deus/bench/report', handler: (req, res) => {
810
+ const u = new URL(String(req.url || ''), 'http://localhost')
811
+ const runId = String(u.searchParams.get('runId') || '')
812
+ json({ ok: true, ...benchReportFrom(readLog(10000), runId || undefined) })(req, res)
813
+ } }),
669
814
  ctx.webServer.register({ kind: 'exact', path: '/deus/version', handler: (req, res) => {
670
815
  let dsh = 'unknown'
671
816
  try { dsh = detectDshVersion() } catch (e) { ctx.logger.warn('[plugin-deus] version detect failed:', e) }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dsh-suite/plugin-deus",
3
- "version": "0.3.0",
4
- "description": "神模扳机 Deus Trigger — 极简提示词触发实验台:预设库 + 一键注入 + 起手识别器 + 触发率统计(Wilson CI)。社区观察未获官方证实,本插件不承诺解锁任何隐藏能力。",
3
+ "version": "0.4.1",
4
+ "description": "神模扳机 Deus Trigger — 提示词 A/B 实验台:任意提示词变体各跑 N 次真实会话注入,指纹分布对比(Wilson CI + 两比例 z 检验);预设库 + 一键注入 + 起手识别器 + 触发率统计。社区观察未获官方证实,本插件不承诺解锁任何隐藏能力。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {