@dsh-suite/plugin-deus 0.2.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
@@ -26,6 +26,9 @@
26
26
  - 安装两个锚定 agent presets 到 `~/.dsh/.agent-presets/`(**窄锚** 2 工具 ~90% 触发 / **宽锚** ~8 工具 ~65% 换可用性),设置 > Agent presets 或会话 preset 选择器里直接选用,构成全程恒定;
27
27
  - 对 deus/minimal preset 会话**逐轮判定指纹**,面板「锚定维持」区实时显示每轮指纹与漂移状态;
28
28
  - 对话页 dock 出现 `⚓ 锚定维持中` 状态 chip;**漂移时变红**,点击发送重锚提示词(可开「漂移自动重锚」);重锚轮日志以 `reanchor` 模式独立记录,便于对比重锚前后维持率。
29
+ 6. **未锚定引导(v0.3)** — 当前会话不在 deus/minimal 系 preset 时,dock 显示灰态 `◌ 非神版会话` chip,点击展开切换指引(把「25 工具会话注入≈0%」的实测结论落地为产品引导)。
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 由此从「神模工具」升级为「提示词实验台」,神模只是第一个被测假设。
29
32
 
30
33
  ## 安装 / Install
31
34
 
@@ -50,7 +53,7 @@ npx @deepseek-ai/dsh plugin --profile web add ./plugin-deus # 在其父目录
50
53
 
51
54
  - 日志只写本地 `$DSH_HOME/deus-mode/log.jsonl`,不上传、可一键清空、可导出 CSV。
52
55
  - 全部是用户正常交互(prompt 注入 + 回复观测),无越权、无绕过。
53
- - 若实验证明无差异:本插件的身份即**通用极简提示词 A/B 测试台**,"神模"只是第一个被测假设。
56
+ - 若实验证明无差异:本插件的身份即**通用极简提示词 A/B 测试台**,"神模"只是第一个被测假设。(v0.4 起实验台已内建:🧪 dock 芯片直接开跑。)
54
57
 
55
58
  ## 实测依据 / Measured basis
56
59
 
package/lib/client.js CHANGED
@@ -42,6 +42,23 @@ window.__ModuleLoader__.load({
42
42
  colSession: '会话', colPreset: 'preset', colTurns: '轮次', colGodRate: '神版率', colLastFp: '最新指纹', colAnchorState: '状态',
43
43
  anchoredOn: '锚定维持中', drifted: '漂移→非神版', reanchor: '⚓ 重锚', autoReanchor: '漂移自动重锚',
44
44
  reanchorSent: '已发送重锚提示', dockAnchored: '锚定维持中',
45
+ unanchored: '◌ 非神版会话', unanchoredTitle: '当前会话不是锚定 preset——实测 25 工具会话注入触发率≈0%。点击查看怎么切换',
46
+ guideText: '实测结论:standard(25 工具)会话里文本注入几乎无法触发神版(0%)。想用神版:新建会话时在 Agent preset 选择器选「神模扳机 · 窄锚 / 宽锚」(实测触发率 ~90% / ~65%)。',
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: '你是一位世界顶尖科普作家,回答前先在脑内推演三遍。用三句话向聪明的初中生解释「熵」。',
45
62
  }
46
63
  const en = {
47
64
  nav: 'Deus Trigger', sub: 'Minimal-prompt trigger bench · inject → classify → stats',
@@ -66,6 +83,23 @@ window.__ModuleLoader__.load({
66
83
  colSession: 'Session', colPreset: 'preset', colTurns: 'turns', colGodRate: 'god rate', colLastFp: 'latest', colAnchorState: 'state',
67
84
  anchoredOn: 'anchored', drifted: 'drifted', reanchor: '⚓ Re-anchor', autoReanchor: 'Auto re-anchor on drift',
68
85
  reanchorSent: 'Re-anchor nudge sent', dockAnchored: 'anchored',
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',
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).',
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.',
69
103
  }
70
104
 
71
105
  const S = {
@@ -84,6 +118,11 @@ window.__ModuleLoader__.load({
84
118
  td: { padding: '6px 8px', borderBottom: '1px solid #21262d' },
85
119
  input: { background: '#0d1117', border: '1px solid #30363d', borderRadius: '6px', color: '#e6edf3', padding: '6px 8px', fontSize: '12px', width: '100%', boxSizing: 'border-box' },
86
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' },
87
126
  dock: { display: 'flex', gap: '6px', alignItems: 'center', flexWrap: 'wrap', padding: '4px 0' },
88
127
  dockChip: { background: '#161b22', border: '1px solid #30363d', borderRadius: '16px', color: '#8b949e', padding: '3px 12px', fontSize: '12px', cursor: 'pointer' },
89
128
  }
@@ -91,6 +130,56 @@ window.__ModuleLoader__.load({
91
130
  const pct = (ci) => `${(ci.rate * 100).toFixed(0)}% [${(ci.low * 100).toFixed(0)}–${(ci.high * 100).toFixed(0)}]`
92
131
 
93
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
+
94
183
  function Panel(props) {
95
184
  const t = props.t
96
185
  const [presets, setPresets] = useState([])
@@ -101,6 +190,12 @@ window.__ModuleLoader__.load({
101
190
  const [err, setErr] = useState('')
102
191
  const [copied, setCopied] = useState('')
103
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
+ }
104
199
 
105
200
  function refresh() {
106
201
  fetch('/deus/presets').then((r) => r.json()).then((d) => setPresets(d.presets || [])).catch(() => setErr(t('loadFail')))
@@ -108,6 +203,7 @@ window.__ModuleLoader__.load({
108
203
  fetch('/deus/log').then((r) => r.json()).then((d) => setEntries(d.entries || [])).catch(() => {})
109
204
  fetch('/deus/version').then((r) => r.json()).then(setVer).catch(() => {})
110
205
  fetch('/deus/anchor').then((r) => r.json()).then(setAnchor).catch(() => {})
206
+ refreshBench(benchRunId)
111
207
  }
112
208
  useEffect(() => { refresh() }, [])
113
209
 
@@ -193,6 +289,8 @@ window.__ModuleLoader__.load({
193
289
  h('div', { style: S.h2 }, t('stats')),
194
290
  h('div', { style: S.status }, t('statsHint')),
195
291
  h('div', { style: { ...S.status, color: '#7d8590' } }, t('measured')),
292
+ h(Sparkline, { entries }),
293
+ entries.length >= 4 ? h('div', { style: { ...S.status, fontSize: '10px' } }, t('trend')) : null,
196
294
  stats && stats.modes.length > 0
197
295
  ? h('table', { style: S.table },
198
296
  h('thead', null, h('tr', null,
@@ -209,6 +307,15 @@ window.__ModuleLoader__.load({
209
307
  : h('div', { style: S.status }, t('noData')),
210
308
  h('div', { style: S.row },
211
309
  h('a', { href: '/deus/log.csv', download: 'deus-log.csv', style: { ...S.btn, textDecoration: 'none', display: 'inline-block' } }, '⬇ ' + t('exportCsv')),
310
+ stats ? h('button', {
311
+ style: copied === '__md' ? S.btnActive : S.btn,
312
+ onClick: async () => {
313
+ try {
314
+ await navigator.clipboard.writeText(buildMarkdownSummary(stats))
315
+ setCopied('__md'); setTimeout(() => setCopied(''), 2500)
316
+ } catch {}
317
+ },
318
+ }, '📋 ' + (copied === '__md' ? t('mdCopied') : t('copyMd'))) : null,
212
319
  h('button', { style: { ...S.btn, color: '#f85149', borderColor: '#f85149' }, onClick: resetLog }, '🗑 ' + t('resetLog')),
213
320
  ),
214
321
 
@@ -239,6 +346,23 @@ window.__ModuleLoader__.load({
239
346
  )
240
347
  : h('div', { style: S.status }, t('anchorNone')),
241
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
+
242
366
  h('div', { style: S.h2 }, t('recent')),
243
367
  h('div', { style: S.status }, t('recentHint')),
244
368
  entries.length === 0 ? h('div', { style: S.status }, t('noData'))
@@ -276,16 +400,148 @@ window.__ModuleLoader__.load({
276
400
  try { return window.localStorage.getItem('deus.autoReanchor') === '1' } catch { return false }
277
401
  })
278
402
  const [reanchorMsg, setReanchorMsg] = useState('')
403
+ // v0.3 未锚定引导:当前会话的 agent preset(宿主读会话日志头行)
404
+ const [sessPreset, setSessPreset] = useState(null)
405
+ const [showGuide, setShowGuide] = useState(false)
279
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
+ }
280
530
  const REANCHOR = 'We need to continue working on this together. Let us pick up where we left off. 我们继续协作,接着上一步往下做。'
281
531
 
282
532
  useEffect(() => {
283
533
  let dead = false
284
- const poll = () => fetch('/deus/anchor').then((r) => r.json()).then((d) => { if (!dead) setAnchor(d) }).catch(() => {})
534
+ const poll = () => {
535
+ fetch('/deus/anchor').then((r) => r.json()).then((d) => { if (!dead) setAnchor(d) }).catch(() => {})
536
+ if (sessionId) {
537
+ fetch('/deus/session-preset?sessionId=' + encodeURIComponent(sessionId))
538
+ .then((r) => r.json()).then((d) => { if (!dead) setSessPreset(d.preset || null) }).catch(() => {})
539
+ }
540
+ }
285
541
  poll()
286
542
  const timer = setInterval(poll, 4000)
287
543
  return () => { dead = true; clearInterval(timer) }
288
- }, [])
544
+ }, [sessionId])
289
545
 
290
546
  const st = anchor && Array.isArray(anchor.sessions)
291
547
  ? anchor.sessions.find((s) => String(s.sessionId) === String(sessionId))
@@ -344,7 +600,41 @@ window.__ModuleLoader__.load({
344
600
 
345
601
  if (presets.length === 0) return null // 所有 hooks 之上不可早退(React hooks 顺序)
346
602
 
347
- return h('div', { style: S.dock, title: t('dockHint') },
603
+ // v0.3 未锚定引导:非锚定会话 + preset 已知且非 deus/minimal 系 → 灰 chip
604
+ const UNANCHORED_PRESET_RE = /^(deus-|minimal)/
605
+ const unanchored = !st && sessPreset && !UNANCHORED_PRESET_RE.test(sessPreset)
606
+
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') },
348
638
  h('span', { style: { fontSize: '11px', color: '#8b949e' } }, '⚗'),
349
639
  presets.map((p) => h('button', {
350
640
  key: p.id, style: S.dockChip, title: t('dockHint'),
@@ -355,18 +645,81 @@ window.__ModuleLoader__.load({
355
645
  title: st.drifted ? t('reanchor') : t('dockAnchored'),
356
646
  onClick: st.drifted ? fireReanchor : undefined,
357
647
  }, st.drifted ? '⚠ ' + t('reanchor') : `⚓ ${t('dockAnchored')} ${st.god}/${st.total}`) : null,
648
+ unanchored ? h('button', {
649
+ style: { ...S.dockChip, color: '#8b949e', borderColor: '#8b949e', borderStyle: 'dotted' },
650
+ title: t('unanchoredTitle'),
651
+ onClick: () => setShowGuide((v) => !v),
652
+ }, t('unanchored')) : null,
653
+ unanchored && showGuide ? h('div', {
654
+ style: {
655
+ fontSize: '11px', color: '#8b949e', lineHeight: 1.6, maxWidth: '520px',
656
+ borderLeft: '2px solid #8b949e', paddingLeft: '8px', margin: '2px 0',
657
+ },
658
+ }, t('guideText')) : null,
358
659
  reanchorMsg ? h('span', { style: { fontSize: '11px', color: '#3fb950' } }, reanchorMsg) : null,
359
660
  h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoSend'), onClick: toggleAuto },
360
661
  (autoSend ? '☑ ' : '☐ ') + t('autoSend')),
361
662
  st ? h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoReanchor'), onClick: toggleAutoReanchor },
362
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')),
363
669
  )
670
+ return h('div', { style: { width: '100%' } }, benchPanel, dockRow)
671
+ }
672
+
673
+ // v0.3: 触发率迷你趋势(最近 40 条日志分 10 桶的 god 率折线)
674
+ function Sparkline({ entries }) {
675
+ const seq = entries.slice(0, 40).reverse() // /deus/log 最新在前 → 转时间序
676
+ if (seq.length < 4) return null
677
+ const B = 10
678
+ const pts = []
679
+ for (let i = 0; i < B; i++) {
680
+ const chunk = seq.slice(Math.floor(i * seq.length / B), Math.floor((i + 1) * seq.length / B))
681
+ if (chunk.length === 0) continue
682
+ const god = chunk.filter((e) => e.detected === 'god').length / chunk.length
683
+ pts.push([i, god])
684
+ }
685
+ if (pts.length < 2) return null
686
+ const W = 260, H = 48, P = 5
687
+ const xy = pts.map(([i, v]) => [P + (W - 2 * P) * i / (B - 1), H - P - (H - 2 * P) * v])
688
+ const dAttr = xy.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ',' + p[1].toFixed(1)).join(' ')
689
+ return h('svg', { width: W, height: H, viewBox: `0 0 ${W} ${H}`, style: { display: 'block', margin: '4px 0 2px' } },
690
+ h('path', { d: `M${P},${H - P} L${W - P},${H - P}`, stroke: '#30363d', strokeWidth: 1, fill: 'none' }),
691
+ h('path', { d: `M${P},${P} L${W - P},${P}`, stroke: '#30363d', strokeWidth: 1, strokeDasharray: '3 3', fill: 'none' }),
692
+ h('path', { d: dAttr, stroke: '#d2a8ff', strokeWidth: 2, fill: 'none' }),
693
+ xy.map((p, i) => h('circle', { key: i, cx: p[0], cy: p[1], r: 2.5, fill: '#d2a8ff' })),
694
+ )
695
+ }
696
+
697
+ // v0.3: 一键复制 Markdown 摘要(传播物料:表格 + 诚实声明 + 包链接)
698
+ function buildMarkdownSummary(stats) {
699
+ const pct1 = (x) => (x * 100).toFixed(1) + '%'
700
+ const lines = [
701
+ '## 神版触发率实测摘要 / God-mode trigger summary', '',
702
+ '| 模式 mode | 样本 n | 神版率 god | 95% CI | 中版 med | 纯区 pure |',
703
+ '|---|---|---|---|---|---|',
704
+ ]
705
+ for (const m of stats.modes || []) {
706
+ if (!m || !m.n) continue
707
+ lines.push(`| ${m.mode} | ${m.n} | ${pct1(m.godCI.rate)} | ${pct1(m.godCI.low)}–${pct1(m.godCI.high)} | ${m.med} | ${m.pure} |`)
708
+ }
709
+ if (stats.total && stats.total.n) {
710
+ lines.push(`| **ALL** | **${stats.total.n}** | **${pct1(stats.total.godCI.rate)}** | **${pct1(stats.total.godCI.low)}–${pct1(stats.total.godCI.high)}** | ${stats.total.med} | ${stats.total.pure} |`)
711
+ }
712
+ lines.push('',
713
+ `> ${stats.total ? stats.total.n : 0} 条本地日志,由 @dsh-suite/plugin-deus 记录。「神版/中版/纯区」为社区观察(X @NFT_Chen),未经 DeepSeek 官方证实;判定 = 推理流起手句式启发式,存在误判率。`,
714
+ '> https://www.npmjs.com/package/@dsh-suite/plugin-deus')
715
+ return lines.join('\n')
364
716
  }
365
717
 
366
718
  return {
367
719
  inject: ['slots', 'locale'],
368
720
  apply(ctx) {
369
721
  try {
722
+ try { window.__deusClient = '0.4.0' } catch { /* non-browser */ }
370
723
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'deus-mode: dicts')
371
724
  const t = ctx.locale.bind(NS)
372
725
  ctx.slots.inject('settings.section', () => ctx.slots.register({
package/lib/index.js CHANGED
@@ -10,11 +10,11 @@
10
10
  // 2. JSONL experiment log at $DSH_HOME/deus-mode/log.jsonl
11
11
  // 3. Trigger preset library (5 built-in modes, user-editable via presets.json)
12
12
  // 4. /deus/* routes for the browser half (presets, log, stats+Wilson CI, version)
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs'
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, readdirSync } from 'node:fs'
14
14
  import { createRequire } from 'node:module'
15
15
  import { join } from 'node:path'
16
16
  import { homedir } from 'node:os'
17
- import { gzipSync } from 'node:zlib'
17
+ import { gzipSync, zstdDecompressSync } from 'node:zlib'
18
18
 
19
19
  export const name = 'deus-mode'
20
20
  export const inject = ['webServer', 'sessions']
@@ -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.2.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 上,注入剥离/工具裁剪自动延续到每一轮。
@@ -216,6 +235,40 @@ function ensureDir() {
216
235
  mkdirSync(dataDir(), { recursive: true })
217
236
  }
218
237
 
238
+ // ── v0.3: session preset 探测(未锚定引导的数据源)─────────────────────────
239
+ // session/event 流里 user/message 不带 agentPreset;权威来源是磁盘会话日志的
240
+ // SessionHeader(jsonl.zstd 第一帧第一行,session-persistence-jsonl 格式)。
241
+ // 读文件 → 找第二个 zstd 帧魔数切出第一帧 → 解压取 header.agentPreset。
242
+ // 60s 缓存(blank 会话可在首轮前 recompose preset,不能永久缓存)。
243
+ const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
244
+ const presetCache = new Map() // sessionId -> { preset, ts }
245
+ function readSessionPreset(sessionId) {
246
+ const hit = presetCache.get(sessionId)
247
+ if (hit && Date.now() - hit.ts < 60_000) return hit.preset
248
+ let preset = null
249
+ try {
250
+ const root = join(dshHome(), 'sessions')
251
+ if (existsSync(root)) {
252
+ for (const ws of readdirSync(root)) {
253
+ const f = join(root, ws, sessionId, 'session.jsonl.zstd')
254
+ if (!existsSync(f)) continue
255
+ const buf = readFileSync(f)
256
+ const second = buf.indexOf(ZSTD_MAGIC, 4)
257
+ const firstFrame = second > 4 ? buf.subarray(0, second) : buf
258
+ try {
259
+ const line0 = zstdDecompressSync(firstFrame).toString('utf8').split('\n')[0]
260
+ const header = JSON.parse(line0)
261
+ if (header && header.type === 'session' && typeof header.agentPreset === 'string') preset = header.agentPreset
262
+ } catch { /* 帧截断/格式异常 → preset 保持 null */ }
263
+ break
264
+ }
265
+ }
266
+ } catch { /* 磁盘问题 → null */ }
267
+ presetCache.set(sessionId, { preset, ts: Date.now() })
268
+ if (presetCache.size > 300) presetCache.delete(presetCache.keys().next().value)
269
+ return preset
270
+ }
271
+
219
272
  // ── preset library: built-ins until the user edits, then presets.json wins ──
220
273
  function loadPresets() {
221
274
  try {
@@ -266,6 +319,8 @@ function statsFrom(entries) {
266
319
  const byMode = new Map()
267
320
  for (const e of entries) {
268
321
  const mode = e.prompt_mode || 'unknown'
322
+ if (mode === 'bench') continue // v0.4: 实验台样本有独立的 A/B 报告,不混入神模统计
323
+
269
324
  if (!byMode.has(mode)) byMode.set(mode, { mode, n: 0, pure: 0, med: 0, god: 0, unknown: 0 })
270
325
  const row = byMode.get(mode)
271
326
  row.n += 1
@@ -286,13 +341,85 @@ function statsFrom(entries) {
286
341
  }
287
342
 
288
343
  function toCsv(entries) {
289
- 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'
290
345
  const esc = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`
291
346
  const rows = entries.map((e) =>
292
- [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(','))
293
348
  return [head, ...rows].join('\n') + '\n'
294
349
  }
295
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
+
296
423
  // ── version auto-detect (differentiator: no manual checking) ────────────────
297
424
  function detectDshVersion() {
298
425
  if (process.env.DSH_VERSION) return process.env.DSH_VERSION
@@ -352,6 +479,10 @@ export function apply(ctx) {
352
479
 
353
480
  // Pending trigger marks: sessionId -> { mode, prompt_text, ts, auto }
354
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
355
486
  // Chunk accumulation while a trigger is pending: sessionId -> { turn, step, reasoning, text }
356
487
  // (实测校准:指纹在 reasoning-delta 流里,text-delta 兜底;见 detectMode 注释)
357
488
  const buffers = new Map()
@@ -432,6 +563,9 @@ export function apply(ctx) {
432
563
  source,
433
564
  first_sentence: firstSentenceOf(classifyOn),
434
565
  model: mark.model || undefined,
566
+ // v0.4 实验台:A/B 样本归属(仅 bench 标记的注入轮带这两个字段)
567
+ bench_run: mark.benchRun || undefined,
568
+ bench_variant: mark.benchVariant || undefined,
435
569
  })
436
570
  }
437
571
 
@@ -452,6 +586,15 @@ export function apply(ctx) {
452
586
  if (event.type === 'user/message') {
453
587
  const text = textOf(event.data && event.data.content).trim()
454
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
+ }
455
598
  const hit = loadPresets().find((p) => p.prompt !== null && p.prompt.trim() === text)
456
599
  if (hit) {
457
600
  buffers.delete(sessionId) // 防止上轮残留 buffer 污染注入轮判定
@@ -625,6 +768,49 @@ export function apply(ctx) {
625
768
  ctx.webServer.register({ kind: 'exact', path: '/deus/stats', handler: (req, res) => {
626
769
  json(statsFrom(readLog(10000)))(req, res)
627
770
  } }),
771
+ // v0.3: 当前会话的 agent preset(dock 未锚定引导用;来源=会话日志头行)
772
+ ctx.webServer.register({ kind: 'exact', path: '/deus/session-preset', handler: (req, res) => {
773
+ const u = new URL(String(req.url || ''), 'http://localhost')
774
+ const sessionId = String(u.searchParams.get('sessionId') || '')
775
+ if (!sessionId) { json({ ok: false, error: 'missing sessionId' }, 400)(req, res); return }
776
+ json({ sessionId, preset: readSessionPreset(sessionId) })(req, res)
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
+ } }),
628
814
  ctx.webServer.register({ kind: 'exact', path: '/deus/version', handler: (req, res) => {
629
815
  let dsh = 'unknown'
630
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.2.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": {