@dsh-suite/plugin-deus 0.3.0 → 0.4.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/README.md CHANGED
@@ -28,6 +28,7 @@
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 由此从「神模工具」升级为「提示词实验台」,神模只是第一个被测假设。
31
32
 
32
33
  ## 安装 / Install
33
34
 
@@ -52,7 +53,7 @@ npx @deepseek-ai/dsh plugin --profile web add ./plugin-deus # 在其父目录
52
53
 
53
54
  - 日志只写本地 `$DSH_HOME/deus-mode/log.jsonl`,不上传、可一键清空、可导出 CSV。
54
55
  - 全部是用户正常交互(prompt 注入 + 回复观测),无越权、无绕过。
55
- - 若实验证明无差异:本插件的身份即**通用极简提示词 A/B 测试台**,"神模"只是第一个被测假设。
56
+ - 若实验证明无差异:本插件的身份即**通用极简提示词 A/B 测试台**,"神模"只是第一个被测假设。(v0.4 起实验台已内建:🧪 dock 芯片直接开跑。)
56
57
 
57
58
  ## 实测依据 / Measured basis
58
59
 
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,6 +86,20 @@ 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
 
77
105
  const S = {
@@ -90,6 +118,11 @@ window.__ModuleLoader__.load({
90
118
  td: { padding: '6px 8px', borderBottom: '1px solid #21262d' },
91
119
  input: { background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3', padding: '6px 8px', fontSize: '12px', width: '100%', boxSizing: 'border-box' },
92
120
  mono: { fontFamily: 'monospace', fontSize: '12px', color: '#c9d1d9' },
121
+ benchWrap: { width: '100%', background: '#0d1117', border: '1px solid #30363d', borderRadius: '10px', padding: '10px 12px', marginBottom: '6px' },
122
+ benchTa: { width: '100%', minHeight: '52px', background: '#161b22', border: '1px solid #30363d', borderRadius: '8px', color: '#c9d1d9', fontSize: '12px', padding: '6px 8px', boxSizing: 'border-box', resize: 'vertical' },
123
+ benchLabel: { fontSize: '11px', color: '#8b949e', margin: '6px 0 3px' },
124
+ benchTable: { width: '100%', borderCollapse: 'collapse', fontSize: '11px', marginTop: '6px' },
125
+ benchCell: { border: '1px solid #21262d', padding: '3px 6px', textAlign: 'right', whiteSpace: 'nowrap' },
93
126
  dock: { display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap', padding: '4px 0' },
94
127
  dockChip: { background: '#161b22', border: '1px solid #30363d', borderRadius: '16px', color: '#8b949e', padding: '3px 12px', fontSize: '12px', cursor: 'pointer' },
95
128
  }
@@ -97,6 +130,56 @@ window.__ModuleLoader__.load({
97
130
  const pct = (ci) => `${(ci.rate * 100).toFixed(0)}% [${(ci.low * 100).toFixed(0)}–${(ci.high * 100).toFixed(0)}]`
98
131
 
99
132
  // ── tier A: settings section panel ──────────────────────────────────────
133
+ // ── v0.4 实验台:报告拉取与 A/B 对比表(Dock 控制台与设置面板共用)─────
134
+ function fetchBenchReport(runId) {
135
+ const url = runId ? '/deus/bench/report?runId=' + encodeURIComponent(runId) : '/deus/bench/report'
136
+ return fetch(url).then((r) => r.json())
137
+ }
138
+
139
+ const BENCH_CLS_COLORS = { god: '#d2a8ff', med: '#3fb950', pure: '#79c0ff', unknown: '#8b949e' }
140
+
141
+ function benchCellFmt(g, n) {
142
+ if (!g || !n) return '—'
143
+ return g.n + '/' + n + ' · ' + Math.round(100 * g.rate) + '% [' + Math.round(100 * g.low) + '–' + Math.round(100 * g.high) + ']'
144
+ }
145
+
146
+ function BenchTable(props) {
147
+ const report = props.report
148
+ const t = props.t
149
+ if (!report) return null
150
+ const A = report.order[0]
151
+ const B = report.order[1] || 'B'
152
+ const vA = report.variants[A]
153
+ const vB = report.variants[B]
154
+ const sig = (p) => (p == null ? '' : p < 0.01 ? '✱✱' : p < 0.05 ? '✱' : '')
155
+ return h('div', { 'data-deus': 'bench-report', style: { marginTop: '8px' } },
156
+ h('div', { style: S.status },
157
+ t('benchRunId') + ': ' + report.runId + ' · n=' + report.n +
158
+ ' · ' + t('benchStarted') + ' ' + String(report.startedAt).slice(11, 19) +
159
+ ' · ' + t('benchFinished') + ' ' + String(report.finishedAt).slice(11, 19)),
160
+ h('table', { style: S.benchTable },
161
+ h('thead', null, h('tr', null,
162
+ h('th', { style: { ...S.benchCell, textAlign: 'left' } }, t('colFp')),
163
+ h('th', { style: S.benchCell }, 'A (n=' + (vA ? vA.n : 0) + ')'),
164
+ h('th', { style: S.benchCell }, 'B (n=' + (vB ? vB.n : 0) + ')'),
165
+ h('th', { style: S.benchCell }, t('colDelta')),
166
+ h('th', { style: S.benchCell }, t('colZ')),
167
+ h('th', { style: S.benchCell }, t('colP')),
168
+ h('th', { style: S.benchCell }, t('colSig')))),
169
+ h('tbody', null, (report.comparison || []).map((row) => h('tr', { key: row.cls },
170
+ h('td', { style: { ...S.benchCell, textAlign: 'left', color: BENCH_CLS_COLORS[row.cls] || '#8b949e' } }, t('detected_' + row.cls)),
171
+ h('td', { style: S.benchCell }, benchCellFmt(row.a, vA && vA.n)),
172
+ h('td', { style: S.benchCell }, benchCellFmt(row.b, vB && vB.n)),
173
+ h('td', { style: S.benchCell }, row.delta == null ? '—' : (row.delta >= 0 ? '+' : '') + Math.round(row.delta * 100) + 'pp'),
174
+ h('td', { style: S.benchCell }, row.z == null ? '—' : Number(row.z).toFixed(2)),
175
+ h('td', { style: S.benchCell }, row.p == null ? '—' : row.p < 0.001 ? '<0.001' : Number(row.p).toFixed(3)),
176
+ h('td', { style: { ...S.benchCell, color: '#d29922' } }, sig(row.p)),
177
+ )))),
178
+ vA && vA.prompt ? h('div', { style: { ...S.status, marginTop: '6px' } }, 'A: ' + JSON.stringify(vA.prompt)) : null,
179
+ vB && vB.prompt ? h('div', { style: S.status }, 'B: ' + JSON.stringify(vB.prompt)) : null,
180
+ )
181
+ }
182
+
100
183
  function Panel(props) {
101
184
  const t = props.t
102
185
  const [presets, setPresets] = useState([])
@@ -107,6 +190,12 @@ window.__ModuleLoader__.load({
107
190
  const [err, setErr] = useState('')
108
191
  const [copied, setCopied] = useState('')
109
192
  const [editing, setEditing] = useState(null) // {id, label_zh, prompt} draft
193
+ const [benchData, setBenchData] = useState(null) // { ok, report, runs }
194
+ const [benchRunId, setBenchRunId] = useState('')
195
+
196
+ function refreshBench(runId) {
197
+ fetchBenchReport(runId || undefined).then((d) => setBenchData(d)).catch(() => {})
198
+ }
110
199
 
111
200
  function refresh() {
112
201
  fetch('/deus/presets').then((r) => r.json()).then((d) => setPresets(d.presets || [])).catch(() => setErr(t('loadFail')))
@@ -114,6 +203,7 @@ window.__ModuleLoader__.load({
114
203
  fetch('/deus/log').then((r) => r.json()).then((d) => setEntries(d.entries || [])).catch(() => {})
115
204
  fetch('/deus/version').then((r) => r.json()).then(setVer).catch(() => {})
116
205
  fetch('/deus/anchor').then((r) => r.json()).then(setAnchor).catch(() => {})
206
+ refreshBench(benchRunId)
117
207
  }
118
208
  useEffect(() => { refresh() }, [])
119
209
 
@@ -256,6 +346,23 @@ window.__ModuleLoader__.load({
256
346
  )
257
347
  : h('div', { style: S.status }, t('anchorNone')),
258
348
 
349
+ h('div', { style: S.h2 }, '🧪 ' + t('bench')),
350
+ h('div', { style: S.status }, t('benchHint')),
351
+ h('div', { style: S.row },
352
+ h('button', { style: S.btn, 'data-deus': 'bench-refresh', onClick: () => refreshBench(benchRunId) }, '🔄 ' + t('benchRefresh')),
353
+ benchData && benchData.runs && benchData.runs.length > 0 ? h('select', {
354
+ style: { ...S.input, width: 'auto' }, 'data-deus': 'bench-runs',
355
+ value: benchRunId,
356
+ onChange: (e) => { setBenchRunId(e.target.value); refreshBench(e.target.value) },
357
+ },
358
+ h('option', { value: '' }, t('benchPickRun')),
359
+ benchData.runs.map((r) => h('option', { key: r.runId, value: r.runId }, r.runId + ' · n=' + r.n)),
360
+ ) : null,
361
+ ),
362
+ benchData && benchData.report
363
+ ? h(BenchTable, { report: benchData.report, t })
364
+ : h('div', { style: S.status, 'data-deus': 'bench-empty' }, t('benchNone')),
365
+
259
366
  h('div', { style: S.h2 }, t('recent')),
260
367
  h('div', { style: S.status }, t('recentHint')),
261
368
  entries.length === 0 ? h('div', { style: S.status }, t('noData'))
@@ -297,6 +404,129 @@ window.__ModuleLoader__.load({
297
404
  const [sessPreset, setSessPreset] = useState(null)
298
405
  const [showGuide, setShowGuide] = useState(false)
299
406
  const lastReanchorKey = useRef('')
407
+
408
+ // ── v0.4 实验台(A/B bench)──────────────────────────────────────────
409
+ const [benchOpen, setBenchOpen] = useState(false)
410
+ const [benchA, setBenchA] = useState(() => {
411
+ try { return window.localStorage.getItem('deus.benchA') || t('benchDefA') } catch { return t('benchDefA') }
412
+ })
413
+ const [benchB, setBenchB] = useState(() => {
414
+ try { return window.localStorage.getItem('deus.benchB') || t('benchDefB') } catch { return t('benchDefB') }
415
+ })
416
+ const [benchN, setBenchN] = useState(3)
417
+ const [benchRunning, setBenchRunning] = useState(false)
418
+ const [benchProg, setBenchProg] = useState(null) // { a, b, n, label, waiting }
419
+ const [benchReport, setBenchReport] = useState(null)
420
+ const [benchNote, setBenchNote] = useState('')
421
+ const benchAbort = useRef(false)
422
+ const benchRunIdRef = useRef('')
423
+ const sidRef = useRef(sessionId)
424
+ const actionsRef = useRef(actions)
425
+ useEffect(() => { sidRef.current = sessionId; actionsRef.current = actions })
426
+
427
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)) }
428
+
429
+ // 每次试验开一个新会话以隔离上下文:点侧栏「新建会话」按钮,
430
+ // 然后等 dock 的 sessionId prop 变化(运行时重渲染本会带回新 id)。
431
+ async function benchNewSession(prevSid) {
432
+ const sels = [
433
+ 'button[aria-label="新建会话"]', 'button[aria-label="New session"]',
434
+ '[role="button"][aria-label="新建会话"]', '[role="button"][aria-label="New session"]',
435
+ ]
436
+ let btn = null
437
+ for (const sel of sels) { btn = document.querySelector(sel); if (btn) break }
438
+ if (!btn) {
439
+ btn = Array.from(document.querySelectorAll('button, [role="button"]'))
440
+ .find((b) => /新建会话|New session/i.test((b.getAttribute('aria-label') || '') + ' ' + (b.title || '')))
441
+ }
442
+ if (!btn) return false
443
+ btn.click()
444
+ const t0 = Date.now()
445
+ while (Date.now() - t0 < 10000) {
446
+ await sleep(300)
447
+ if (String(sidRef.current || '') !== String(prevSid || '')) return true
448
+ }
449
+ return false
450
+ }
451
+
452
+ async function abortBench() {
453
+ benchAbort.current = true
454
+ try {
455
+ await fetch('/deus/bench/clear', {
456
+ method: 'POST', headers: { 'content-type': 'application/json' },
457
+ body: JSON.stringify({ runId: benchRunIdRef.current }),
458
+ })
459
+ } catch { /* best-effort */ }
460
+ }
461
+
462
+ async function runBench() {
463
+ const aText = benchA.trim()
464
+ const bText = benchB.trim()
465
+ if (!aText || !bText) { setBenchNote(t('benchEmpty')); return }
466
+ setBenchRunning(true); setBenchNote(''); setBenchReport(null)
467
+ benchAbort.current = false
468
+ const runId = 'bench-' + Date.now().toString(36)
469
+ benchRunIdRef.current = runId
470
+ const n = Math.max(1, Math.min(20, Math.floor(Number(benchN) || 1)))
471
+ const trials = []
472
+ for (let i = 0; i < n; i++) { trials.push(['A', aText, i + 1]); trials.push(['B', bText, i + 1]) }
473
+ let doneA = 0
474
+ let doneB = 0
475
+ const total = trials.length
476
+ try { window.__deusBench = { running: true, runId, done: 0, total } } catch { /* ignore */ }
477
+ try {
478
+ for (const [variant, text, idx] of trials) {
479
+ if (benchAbort.current) break
480
+ setBenchProg({ a: doneA, b: doneB, n, label: variant + '#' + idx, waiting: false })
481
+ // 1) 新会话隔离(失败则降级为同会话连跑并亮警告)
482
+ const prevSid = sidRef.current
483
+ const switched = await benchNewSession(prevSid)
484
+ if (!switched) { setBenchNote(t('benchIsolationWarn')); await sleep(400) }
485
+ if (benchAbort.current) break
486
+ // 2) 等 composer actions 就绪
487
+ const w0 = Date.now()
488
+ while (!(actionsRef.current && typeof actionsRef.current.setDraft === 'function')) {
489
+ if (Date.now() - w0 > 8000) break
490
+ await sleep(250)
491
+ }
492
+ // 3) 宿主标记(文本队列配对)→ 注入并发送
493
+ try {
494
+ await fetch('/deus/bench/mark', {
495
+ method: 'POST', headers: { 'content-type': 'application/json' },
496
+ body: JSON.stringify({ runId, variant, prompt: text }),
497
+ })
498
+ } catch { /* marking best-effort */ }
499
+ const acts = actionsRef.current
500
+ if (!acts || typeof acts.setDraft !== 'function' || typeof acts.submit !== 'function') {
501
+ setBenchNote(t('benchTimeout')); break
502
+ }
503
+ acts.setDraft(text)
504
+ await sleep(200)
505
+ acts.submit()
506
+ // 4) 轮询报告计数,等宿主把这轮的指纹判定写进日志
507
+ setBenchProg({ a: doneA, b: doneB, n, label: variant + '#' + idx, waiting: true })
508
+ const doneSoFar = doneA + doneB
509
+ const t0 = Date.now()
510
+ let arrived = false
511
+ while (Date.now() - t0 < 240000) {
512
+ if (benchAbort.current) break
513
+ await sleep(2000)
514
+ const rep = await fetchBenchReport(runId).catch(() => null)
515
+ if (rep && rep.report && rep.report.n >= doneSoFar + 1) { arrived = true; break }
516
+ }
517
+ if (!arrived && !benchAbort.current) { setBenchNote(t('benchTimeout')); break }
518
+ if (variant === 'A') doneA++; else doneB++
519
+ try { window.__deusBench = { running: true, runId, done: doneA + doneB, total } } catch { /* ignore */ }
520
+ await sleep(600)
521
+ }
522
+ } finally {
523
+ setBenchRunning(false)
524
+ setBenchProg(null)
525
+ try { window.__deusBench = { running: false, runId, done: doneA + doneB, total } } catch { /* ignore */ }
526
+ }
527
+ const fin = await fetchBenchReport(runId).catch(() => null)
528
+ if (fin && fin.report) setBenchReport(fin.report)
529
+ }
300
530
  const REANCHOR = 'We need to continue working on this together. Let us pick up where we left off. 我们继续协作,接着上一步往下做。'
301
531
 
302
532
  useEffect(() => {
@@ -374,7 +604,37 @@ window.__ModuleLoader__.load({
374
604
  const UNANCHORED_PRESET_RE = /^(deus-|minimal)/
375
605
  const unanchored = !st && sessPreset && !UNANCHORED_PRESET_RE.test(sessPreset)
376
606
 
377
- return h('div', { style: S.dock, title: t('dockHint') },
607
+ const benchPanel = benchOpen ? h('div', { style: S.benchWrap, 'data-deus': 'bench-panel' },
608
+ h('div', { style: { fontSize: '12px', fontWeight: 600, color: '#e6edf3' } }, '🧪 ' + t('bench')),
609
+ h('div', { style: S.status }, t('benchHint')),
610
+ h('div', { style: S.benchLabel }, t('benchA')),
611
+ h('textarea', {
612
+ style: S.benchTa, 'data-deus': 'bench-a', value: benchA, disabled: benchRunning,
613
+ onChange: (e) => { setBenchA(e.target.value); try { window.localStorage.setItem('deus.benchA', e.target.value) } catch { /* ignore */ } },
614
+ }),
615
+ h('div', { style: S.benchLabel }, t('benchB')),
616
+ h('textarea', {
617
+ style: S.benchTa, 'data-deus': 'bench-b', value: benchB, disabled: benchRunning,
618
+ onChange: (e) => { setBenchB(e.target.value); try { window.localStorage.setItem('deus.benchB', e.target.value) } catch { /* ignore */ } },
619
+ }),
620
+ h('div', { style: { ...S.row, marginTop: '8px', alignItems: 'center' } },
621
+ h('span', { style: { ...S.benchLabel, margin: 0 } }, t('benchTimes')),
622
+ h('input', {
623
+ type: 'number', min: 1, max: 20, 'data-deus': 'bench-n', value: benchN, disabled: benchRunning,
624
+ style: { background: '#161b22', border: '1px solid #30363d', borderRadius: '8px', color: '#c9d1d9', fontSize: '12px', padding: '4px 6px', width: '60px' },
625
+ onChange: (e) => setBenchN(e.target.value),
626
+ }),
627
+ benchRunning
628
+ ? h('button', { style: { ...S.btn, color: '#f85149', borderColor: '#f85149' }, 'data-deus': 'bench-abort', onClick: abortBench }, t('benchAbort'))
629
+ : h('button', { style: S.btnActive, 'data-deus': 'bench-run', onClick: runBench }, t('benchRun')),
630
+ ),
631
+ benchProg ? h('div', { style: S.status, 'data-deus': 'bench-progress' },
632
+ t('benchRunning') + ' · ' + t('benchProgress') + ': A ' + benchProg.a + '/' + benchProg.n + ' · B ' + benchProg.b + '/' + benchProg.n + ' · ' + benchProg.label + (benchProg.waiting ? ' ' + t('benchWaiting') : '')) : null,
633
+ benchNote ? h('div', { style: { ...S.status, color: '#d29922' }, 'data-deus': 'bench-note' }, benchNote) : null,
634
+ benchReport ? h(BenchTable, { report: benchReport, t }) : null,
635
+ ) : null
636
+
637
+ const dockRow = h('div', { style: S.dock, title: t('dockHint') },
378
638
  h('span', { style: { fontSize: '11px', color: '#8b949e' } }, '⚗'),
379
639
  presets.map((p) => h('button', {
380
640
  key: p.id, style: S.dockChip, title: t('dockHint'),
@@ -401,7 +661,13 @@ window.__ModuleLoader__.load({
401
661
  (autoSend ? '☑ ' : '☐ ') + t('autoSend')),
402
662
  st ? h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoReanchor'), onClick: toggleAutoReanchor },
403
663
  (autoReanchor ? '☑ ' : '☐ ') + t('autoReanchor')) : null,
664
+ h('button', {
665
+ style: { ...S.dockChip, borderStyle: 'dashed', color: benchOpen ? '#d2a8ff' : '#8b949e', borderColor: benchOpen ? '#d2a8ff' : '#30363d' },
666
+ title: t('bench'), 'data-deus': 'bench-chip',
667
+ onClick: () => setBenchOpen((v) => !v),
668
+ }, '🧪 ' + t('bench')),
404
669
  )
670
+ return h('div', { style: { width: '100%' } }, benchPanel, dockRow)
405
671
  }
406
672
 
407
673
  // v0.3: 触发率迷你趋势(最近 40 条日志分 10 桶的 god 率折线)
@@ -453,6 +719,7 @@ window.__ModuleLoader__.load({
453
719
  inject: ['slots', 'locale'],
454
720
  apply(ctx) {
455
721
  try {
722
+ try { window.__deusClient = '0.4.0' } catch { /* non-browser */ }
456
723
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'deus-mode: dicts')
457
724
  const t = ctx.locale.bind(NS)
458
725
  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.0",
4
+ "description": "神模扳机 Deus Trigger — 提示词 A/B 实验台:任意提示词变体各跑 N 次真实会话注入,指纹分布对比(Wilson CI + 两比例 z 检验);预设库 + 一键注入 + 起手识别器 + 触发率统计。社区观察未获官方证实,本插件不承诺解锁任何隐藏能力。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {