@dsh-suite/plugin-deus 0.1.1 → 0.3.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
@@ -18,10 +18,16 @@
18
18
  - **档 A(保守)**:Settings 面板一键复制 → 粘贴进输入框发送;宿主按 prompt 精确匹配自动配对判定。
19
19
  - **档 B(增强)**:对话输入框上方的芯片行,点击即写入草稿(可选自动发送),并向宿主打点配对。
20
20
  3. **起手识别器** — host 半侧监听 `session/event`,对首条回复做启发式判定:
21
- `Let me…` → 纯区版 · `The user…`(含去冠词变体 `User wants…`)→ 中版 · `we` 起手(或首句 ≥3 个 we)→ 神版 · 其余 → 未判定。
21
+ `Let me…` → 纯区版 · `The user…`(含去冠词变体 `User wants/asks/says/is continuing…`)→ 中版 · `we` 起手(或首句 ≥3 个 we)→ 神版 · 其余 → 未判定。
22
22
  **实测校准**(`research/deus-mode-matrix.md`,120 次 API 采样):指纹主要出现在**推理流**(`reasoning-delta`,如 "The user is asking…" / "We need answer…"),可见正文多为中文直答;识别器因此**优先判定推理流、可见文本兜底**,并额外识别推理流里的中文 `我们需要/我们应该…` 起手(神版等价)。与 120 例人工标注的神版判定一致率 99.2%。
23
23
  4. **实测日志 + 触发率统计** — 每次触发追加一条 JSONL(`~/.dsh/deus-mode/log.jsonl`,纯本地不上传);
24
24
  面板给出每模式的 god/med/pure 比例 + **95% Wilson 置信区间**,可导出 CSV。
25
+ 5. **锚定维持(v0.2)** — 本团队漂移实测发现:一次锚定**不能**全程有效(工具目录补齐后神版维持率 0/6,构成恒定时也有 44-89% 逐轮摆动)。插件因此:
26
+ - 安装两个锚定 agent presets 到 `~/.dsh/.agent-presets/`(**窄锚** 2 工具 ~90% 触发 / **宽锚** ~8 工具 ~65% 换可用性),设置 > Agent presets 或会话 preset 选择器里直接选用,构成全程恒定;
27
+ - 对 deus/minimal preset 会话**逐轮判定指纹**,面板「锚定维持」区实时显示每轮指纹与漂移状态;
28
+ - 对话页 dock 出现 `⚓ 锚定维持中` 状态 chip;**漂移时变红**,点击发送重锚提示词(可开「漂移自动重锚」);重锚轮日志以 `reanchor` 模式独立记录,便于对比重锚前后维持率。
29
+ 6. **未锚定引导(v0.3)** — 当前会话不在 deus/minimal 系 preset 时,dock 显示灰态 `◌ 非神版会话` chip,点击展开切换指引(把「25 工具会话注入≈0%」的实测结论落地为产品引导)。
30
+ 7. **统计导出与分享(v0.3)** — 面板一键导出 CSV、**复制 Markdown 摘要**(Wilson CI 表格 + 诚实声明 + 包链接,可直接发帖),并内嵌近期神版率迷你趋势图(最近 40 条日志分桶折线)。
25
31
 
26
32
  ## 安装 / Install
27
33
 
@@ -48,6 +54,12 @@ npx @deepseek-ai/dsh plugin --profile web add ./plugin-deus # 在其父目录
48
54
  - 全部是用户正常交互(prompt 注入 + 回复观测),无越权、无绕过。
49
55
  - 若实验证明无差异:本插件的身份即**通用极简提示词 A/B 测试台**,"神模"只是第一个被测假设。
50
56
 
57
+ ## 实测依据 / Measured basis
58
+
59
+ 本团队 2026-08-15 实测(deepseek-v4-pro,N=440 次 API 采样,`research/deus-mode-matrix.md`):
60
+ 神版触发率随工具数单调衰减(2 工具 ~90% → ~8 工具 ~65% → 25 工具 0%);剥离注入上下文 20%→90%;
61
+ 讲解/咨询类任务 ~0%;锚定维持见上。全部为倾向信号,非官方证实。
62
+
51
63
  ## License
52
64
 
53
65
  MIT
package/lib/client.js CHANGED
@@ -15,7 +15,7 @@ window.__ModuleLoader__.load({
15
15
  id: '@dsh-suite/plugin-deus',
16
16
  factory: (require) => {
17
17
  const React = require('react')
18
- const { useState, useEffect } = React
18
+ const { useState, useEffect, useRef } = React
19
19
  const h = React.createElement
20
20
 
21
21
  const NS = 'deusMode'
@@ -28,7 +28,7 @@ window.__ModuleLoader__.load({
28
28
  edit: '编辑', save: '保存', cancel: '取消', del: '删除', add: '新增预设', resetPresets: '恢复内置',
29
29
  editPrompt: 'prompt 内容(留空 = 空输入模式)', editLabel: '名称', editId: 'id(唯一)',
30
30
  stats: '触发率统计', statsHint: '比例 + 95% Wilson 置信区间。只有实验组显著高于对照组(普通完整提示词)才算有差异——请自行保留对照采样。',
31
- measured: '本团队实测(2026-08-15,deepseek-v4-pro,N=200 次 API 采样):极简首请求构成(Minimal 系统提示 2 工具 剥离注入上下文,三者缺一不可)下,动手类任务神版(we 起手)约 90%;讲解/咨询类任务约 0%;完整 persona + 25 工具对照组 0/20。指纹主要在推理流(reasoning),可见正文多为中文直答。样本量小,请当作倾向信号。',
31
+ measured: '本团队实测(2026-08-15,deepseek-v4-pro,N=440 次 API 采样):神版需要 Minimal 系统提示 + 小工具目录——触发率随工具数单调衰减(2 工具 ~90%、~8 工具 ~65%、25 工具 0%);剥离注入上下文把触发率从 20% 推到 90%;讲解/咨询类任务约 0%。锚定维持:工具目录补齐后神版维持率 0/6(一次锚定不能全程有效,故需逐轮监控 + 漂移重锚);构成恒定时也有 44-89% 摆动。样本量小,请当作倾向信号。',
32
32
  colMode: '模式', colN: '采样', colGod: '神版率', colMed: '中版率', colPure: '纯区率',
33
33
  exportCsv: '导出 CSV', resetLog: '清空日志', resetLogConfirm: '确定清空全部实验日志?此操作不可撤销。',
34
34
  recent: '最近记录', recentHint: '识别器判定是启发式倾向信号,first_sentence 原文供人工复核。',
@@ -37,6 +37,14 @@ window.__ModuleLoader__.load({
37
37
  dockHint: '神模扳机(实验工具·社区观察未证实)', dockSend: '注入并发送', dockFill: '注入',
38
38
  autoSend: '注入后自动发送',
39
39
  detected_pure: '纯区版', detected_med: '中版', detected_god: '神版', detected_unknown: '未判定',
40
+ anchor: '锚定维持', anchorHint: 'deus/minimal preset 会话的逐轮指纹监控。实测(§9):工具目录补齐后神版维持率 0/6 全漂回中版,构成恒定时也有 44-89% 逐轮摆动——漂移时建议重锚。',
41
+ anchorPresetInstalled: '已安装 agent presets(设置 > Agent presets 可选)', anchorNone: '暂无受监控会话——在会话里选「神模扳机」preset 或注入一次即纳入监控。',
42
+ colSession: '会话', colPreset: 'preset', colTurns: '轮次', colGodRate: '神版率', colLastFp: '最新指纹', colAnchorState: '状态',
43
+ anchoredOn: '锚定维持中', drifted: '漂移→非神版', reanchor: '⚓ 重锚', autoReanchor: '漂移自动重锚',
44
+ reanchorSent: '已发送重锚提示', dockAnchored: '锚定维持中',
45
+ unanchored: '◌ 非神版会话', unanchoredTitle: '当前会话不是锚定 preset——实测 25 工具会话注入触发率≈0%。点击查看怎么切换',
46
+ guideText: '实测结论:standard(25 工具)会话里文本注入几乎无法触发神版(0%)。想用神版:新建会话时在 Agent preset 选择器选「神模扳机 · 窄锚 / 宽锚」(实测触发率 ~90% / ~65%)。',
47
+ copyMd: '复制 Markdown 摘要', mdCopied: '已复制 ✓ 去发帖吧', trend: '近期神版率趋势',
40
48
  }
41
49
  const en = {
42
50
  nav: 'Deus Trigger', sub: 'Minimal-prompt trigger bench · inject → classify → stats',
@@ -47,7 +55,7 @@ window.__ModuleLoader__.load({
47
55
  edit: 'Edit', save: 'Save', cancel: 'Cancel', del: 'Delete', add: 'Add preset', resetPresets: 'Restore built-ins',
48
56
  editPrompt: 'prompt text (empty = empty-input mode)', editLabel: 'label', editId: 'id (unique)',
49
57
  stats: 'Trigger-rate stats', statsHint: 'Proportion + 95% Wilson CI. Only a significant lead over a control group (ordinary full prompts) counts — keep your own control samples.',
50
- measured: 'Measured by our team (2026-08-15, deepseek-v4-pro, N=200 API samples): under a minimal first request (minimal system prompt AND ≤2 tools AND stripped context injection all three required), hands-on tasks hit the god opener ("we…") ~90% of the time; explanatory/Q&A tasks ~0%; the full persona + 25-tool control hit 0/20. The fingerprint lives mostly in the reasoning stream. Small samples — treat as a tendency signal.',
58
+ measured: 'Measured by our team (2026-08-15, deepseek-v4-pro, N=440 API samples): god mode needs a minimal system prompt + a SMALL tool catalog trigger rate decays with tool count (2 tools ~90%, ~8 tools ~65%, 25 tools 0%); stripping injected context lifts it 20% 90%; explanatory tasks ~0%. Persistence: once the tool catalog expands, god-mode retention is 0/6 one-shot anchoring does NOT hold all session, hence per-turn watch + re-anchor; even with constant composition, retention wobbles 44-89%. Small samples — tendency signal only.',
51
59
  colMode: 'Mode', colN: 'N', colGod: 'god rate', colMed: 'med rate', colPure: 'pure rate',
52
60
  exportCsv: 'Export CSV', resetLog: 'Clear log', resetLogConfirm: 'Clear ALL experiment log entries? This cannot be undone.',
53
61
  recent: 'Recent entries', recentHint: 'The detector is a heuristic tendency signal; first_sentence is kept for human review.',
@@ -56,6 +64,14 @@ window.__ModuleLoader__.load({
56
64
  dockHint: 'Deus Trigger (experiment · community observation, unconfirmed)', dockSend: 'Inject & send', dockFill: 'Inject',
57
65
  autoSend: 'Auto-send after inject',
58
66
  detected_pure: 'pure', detected_med: 'med', detected_god: 'god', detected_unknown: 'unknown',
67
+ anchor: 'Anchor persistence', anchorHint: 'Per-turn opener watch for deus/minimal-preset sessions. Measured (§9): after the tool catalog expands, god-mode retention is 0/6 — all drift back; even with a constant composition, retention wobbles 44-89% turn to turn. Re-anchor on drift.',
68
+ anchorPresetInstalled: 'Installed agent presets (pick in Settings > Agent presets)', anchorNone: 'No watched sessions yet — pick a Deus Trigger preset in a session, or inject once.',
69
+ colSession: 'Session', colPreset: 'preset', colTurns: 'turns', colGodRate: 'god rate', colLastFp: 'latest', colAnchorState: 'state',
70
+ anchoredOn: 'anchored', drifted: 'drifted', reanchor: '⚓ Re-anchor', autoReanchor: 'Auto re-anchor on drift',
71
+ reanchorSent: 'Re-anchor nudge sent', dockAnchored: 'anchored',
72
+ 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
+ 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
+ copyMd: 'Copy Markdown summary', mdCopied: 'Copied ✓ go post it', trend: 'Recent god-rate trend',
59
75
  }
60
76
 
61
77
  const S = {
@@ -87,6 +103,7 @@ window.__ModuleLoader__.load({
87
103
  const [stats, setStats] = useState(null)
88
104
  const [entries, setEntries] = useState([])
89
105
  const [ver, setVer] = useState(null)
106
+ const [anchor, setAnchor] = useState(null)
90
107
  const [err, setErr] = useState('')
91
108
  const [copied, setCopied] = useState('')
92
109
  const [editing, setEditing] = useState(null) // {id, label_zh, prompt} draft
@@ -96,6 +113,7 @@ window.__ModuleLoader__.load({
96
113
  fetch('/deus/stats').then((r) => r.json()).then(setStats).catch(() => {})
97
114
  fetch('/deus/log').then((r) => r.json()).then((d) => setEntries(d.entries || [])).catch(() => {})
98
115
  fetch('/deus/version').then((r) => r.json()).then(setVer).catch(() => {})
116
+ fetch('/deus/anchor').then((r) => r.json()).then(setAnchor).catch(() => {})
99
117
  }
100
118
  useEffect(() => { refresh() }, [])
101
119
 
@@ -181,6 +199,8 @@ window.__ModuleLoader__.load({
181
199
  h('div', { style: S.h2 }, t('stats')),
182
200
  h('div', { style: S.status }, t('statsHint')),
183
201
  h('div', { style: { ...S.status, color: '#7d8590' } }, t('measured')),
202
+ h(Sparkline, { entries }),
203
+ entries.length >= 4 ? h('div', { style: { ...S.status, fontSize: '10px' } }, t('trend')) : null,
184
204
  stats && stats.modes.length > 0
185
205
  ? h('table', { style: S.table },
186
206
  h('thead', null, h('tr', null,
@@ -197,9 +217,45 @@ window.__ModuleLoader__.load({
197
217
  : h('div', { style: S.status }, t('noData')),
198
218
  h('div', { style: S.row },
199
219
  h('a', { href: '/deus/log.csv', download: 'deus-log.csv', style: { ...S.btn, textDecoration: 'none', display: 'inline-block' } }, '⬇ ' + t('exportCsv')),
220
+ stats ? h('button', {
221
+ style: copied === '__md' ? S.btnActive : S.btn,
222
+ onClick: async () => {
223
+ try {
224
+ await navigator.clipboard.writeText(buildMarkdownSummary(stats))
225
+ setCopied('__md'); setTimeout(() => setCopied(''), 2500)
226
+ } catch {}
227
+ },
228
+ }, '📋 ' + (copied === '__md' ? t('mdCopied') : t('copyMd'))) : null,
200
229
  h('button', { style: { ...S.btn, color: '#f85149', borderColor: '#f85149' }, onClick: resetLog }, '🗑 ' + t('resetLog')),
201
230
  ),
202
231
 
232
+ h('div', { style: S.h2 }, t('anchor')),
233
+ h('div', { style: S.status }, t('anchorHint')),
234
+ anchor && anchor.agentPresets && h('div', { style: S.card },
235
+ h('div', { style: S.status }, t('anchorPresetInstalled')),
236
+ 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 ? '✓' : '✗'),
238
+ h('span', { style: S.name }, p.name || p.id),
239
+ p.reason ? h('span', { style: S.status }, '(' + p.reason + ')') : null,
240
+ )),
241
+ ),
242
+ anchor && anchor.sessions && anchor.sessions.length > 0
243
+ ? h('table', { style: S.table },
244
+ h('thead', null, h('tr', null,
245
+ h('th', { style: S.th }, t('colSession')), h('th', { style: S.th }, t('colPreset')),
246
+ h('th', { style: S.th }, t('colTurns')), h('th', { style: S.th }, t('colGodRate')),
247
+ h('th', { style: S.th }, t('colLastFp')), h('th', { style: S.th }, t('colAnchorState')))),
248
+ h('tbody', null, anchor.sessions.map((s) => h('tr', { key: s.sessionId },
249
+ h('td', { style: { ...S.td, ...S.mono } }, s.sessionId.slice(0, 8)),
250
+ h('td', { style: S.td }, s.preset),
251
+ h('td', { style: S.td }, s.total),
252
+ h('td', { style: { ...S.td, color: '#d2a8ff' } }, s.total ? Math.round(100 * s.god / s.total) + '%' : '—'),
253
+ 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')),
255
+ ))),
256
+ )
257
+ : h('div', { style: S.status }, t('anchorNone')),
258
+
203
259
  h('div', { style: S.h2 }, t('recent')),
204
260
  h('div', { style: S.status }, t('recentHint')),
205
261
  entries.length === 0 ? h('div', { style: S.status }, t('noData'))
@@ -227,11 +283,70 @@ window.__ModuleLoader__.load({
227
283
  useEffect(() => {
228
284
  fetch('/deus/presets').then((r) => r.json()).then((d) => setPresets(d.presets || [])).catch(() => {})
229
285
  }, [])
230
- if (presets.length === 0) return null
231
286
 
232
- const sessionId = props.session && props.session.sessionId
287
+ const sessionId = props.sessionId || (props.session && props.session.sessionId)
233
288
  const actions = props.inputActions
234
289
 
290
+ // v0.2 锚定维持:轮询宿主锚定状态,漂移时给重锚 chip / 自动重锚
291
+ const [anchor, setAnchor] = useState(null)
292
+ const [autoReanchor, setAutoReanchor] = useState(() => {
293
+ try { return window.localStorage.getItem('deus.autoReanchor') === '1' } catch { return false }
294
+ })
295
+ const [reanchorMsg, setReanchorMsg] = useState('')
296
+ // v0.3 未锚定引导:当前会话的 agent preset(宿主读会话日志头行)
297
+ const [sessPreset, setSessPreset] = useState(null)
298
+ const [showGuide, setShowGuide] = useState(false)
299
+ const lastReanchorKey = useRef('')
300
+ const REANCHOR = 'We need to continue working on this together. Let us pick up where we left off. 我们继续协作,接着上一步往下做。'
301
+
302
+ useEffect(() => {
303
+ let dead = false
304
+ const poll = () => {
305
+ fetch('/deus/anchor').then((r) => r.json()).then((d) => { if (!dead) setAnchor(d) }).catch(() => {})
306
+ if (sessionId) {
307
+ fetch('/deus/session-preset?sessionId=' + encodeURIComponent(sessionId))
308
+ .then((r) => r.json()).then((d) => { if (!dead) setSessPreset(d.preset || null) }).catch(() => {})
309
+ }
310
+ }
311
+ poll()
312
+ const timer = setInterval(poll, 4000)
313
+ return () => { dead = true; clearInterval(timer) }
314
+ }, [sessionId])
315
+
316
+ const st = anchor && Array.isArray(anchor.sessions)
317
+ ? anchor.sessions.find((s) => String(s.sessionId) === String(sessionId))
318
+ : null
319
+
320
+ async function fireReanchor() {
321
+ try {
322
+ await fetch('/deus/trigger', {
323
+ method: 'POST', headers: { 'content-type': 'application/json' },
324
+ body: JSON.stringify({ sessionId: String(sessionId || ''), mode: 'reanchor', prompt: REANCHOR }),
325
+ })
326
+ } catch { /* best-effort */ }
327
+ if (actions && typeof actions.setDraft === 'function') {
328
+ actions.setDraft(REANCHOR)
329
+ if (typeof actions.submit === 'function') actions.submit()
330
+ setReanchorMsg(t('reanchorSent'))
331
+ setTimeout(() => setReanchorMsg(''), 3000)
332
+ }
333
+ }
334
+
335
+ // 自动重锚:漂移且开启时自动发一次(按 sessionId+轮数去抖)
336
+ useEffect(() => {
337
+ if (!st || !st.drifted || !autoReanchor) return
338
+ const key = String(sessionId) + ':' + st.total
339
+ if (lastReanchorKey.current === key) return
340
+ lastReanchorKey.current = key
341
+ fireReanchor()
342
+ }, [st && st.drifted, st && st.total, autoReanchor])
343
+
344
+ function toggleAutoReanchor() {
345
+ const next = !autoReanchor
346
+ setAutoReanchor(next)
347
+ try { window.localStorage.setItem('deus.autoReanchor', next ? '1' : '0') } catch { /* ignore */ }
348
+ }
349
+
235
350
  async function fire(p) {
236
351
  try {
237
352
  await fetch('/deus/trigger', {
@@ -253,17 +368,87 @@ window.__ModuleLoader__.load({
253
368
  try { window.localStorage.setItem('deus.autoSend', next ? '1' : '0') } catch { /* ignore */ }
254
369
  }
255
370
 
371
+ if (presets.length === 0) return null // 所有 hooks 之上不可早退(React hooks 顺序)
372
+
373
+ // v0.3 未锚定引导:非锚定会话 + preset 已知且非 deus/minimal 系 → 灰 chip
374
+ const UNANCHORED_PRESET_RE = /^(deus-|minimal)/
375
+ const unanchored = !st && sessPreset && !UNANCHORED_PRESET_RE.test(sessPreset)
376
+
256
377
  return h('div', { style: S.dock, title: t('dockHint') },
257
378
  h('span', { style: { fontSize: '11px', color: '#8b949e' } }, '⚗'),
258
379
  presets.map((p) => h('button', {
259
380
  key: p.id, style: S.dockChip, title: t('dockHint'),
260
381
  onClick: () => fire(p),
261
382
  }, (autoSend ? '🚀 ' : '⚡ ') + p.label_zh)),
383
+ st ? h('button', {
384
+ style: { ...S.dockChip, color: st.drifted ? '#f85149' : '#3fb950', borderColor: st.drifted ? '#f85149' : '#3fb950' },
385
+ title: st.drifted ? t('reanchor') : t('dockAnchored'),
386
+ onClick: st.drifted ? fireReanchor : undefined,
387
+ }, st.drifted ? '⚠ ' + t('reanchor') : `⚓ ${t('dockAnchored')} ${st.god}/${st.total}`) : null,
388
+ unanchored ? h('button', {
389
+ style: { ...S.dockChip, color: '#8b949e', borderColor: '#8b949e', borderStyle: 'dotted' },
390
+ title: t('unanchoredTitle'),
391
+ onClick: () => setShowGuide((v) => !v),
392
+ }, t('unanchored')) : null,
393
+ unanchored && showGuide ? h('div', {
394
+ style: {
395
+ fontSize: '11px', color: '#8b949e', lineHeight: 1.6, maxWidth: '520px',
396
+ borderLeft: '2px solid #8b949e', paddingLeft: '8px', margin: '2px 0',
397
+ },
398
+ }, t('guideText')) : null,
399
+ reanchorMsg ? h('span', { style: { fontSize: '11px', color: '#3fb950' } }, reanchorMsg) : null,
262
400
  h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoSend'), onClick: toggleAuto },
263
401
  (autoSend ? '☑ ' : '☐ ') + t('autoSend')),
402
+ st ? h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoReanchor'), onClick: toggleAutoReanchor },
403
+ (autoReanchor ? '☑ ' : '☐ ') + t('autoReanchor')) : null,
264
404
  )
265
405
  }
266
406
 
407
+ // v0.3: 触发率迷你趋势(最近 40 条日志分 10 桶的 god 率折线)
408
+ function Sparkline({ entries }) {
409
+ const seq = entries.slice(0, 40).reverse() // /deus/log 最新在前 → 转时间序
410
+ if (seq.length < 4) return null
411
+ const B = 10
412
+ const pts = []
413
+ for (let i = 0; i < B; i++) {
414
+ const chunk = seq.slice(Math.floor(i * seq.length / B), Math.floor((i + 1) * seq.length / B))
415
+ if (chunk.length === 0) continue
416
+ const god = chunk.filter((e) => e.detected === 'god').length / chunk.length
417
+ pts.push([i, god])
418
+ }
419
+ if (pts.length < 2) return null
420
+ const W = 260, H = 48, P = 5
421
+ const xy = pts.map(([i, v]) => [P + (W - 2 * P) * i / (B - 1), H - P - (H - 2 * P) * v])
422
+ const dAttr = xy.map((p, i) => (i ? 'L' : 'M') + p[0].toFixed(1) + ',' + p[1].toFixed(1)).join(' ')
423
+ 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' })),
428
+ )
429
+ }
430
+
431
+ // v0.3: 一键复制 Markdown 摘要(传播物料:表格 + 诚实声明 + 包链接)
432
+ function buildMarkdownSummary(stats) {
433
+ const pct1 = (x) => (x * 100).toFixed(1) + '%'
434
+ const lines = [
435
+ '## 神版触发率实测摘要 / God-mode trigger summary', '',
436
+ '| 模式 mode | 样本 n | 神版率 god | 95% CI | 中版 med | 纯区 pure |',
437
+ '|---|---|---|---|---|---|',
438
+ ]
439
+ for (const m of stats.modes || []) {
440
+ if (!m || !m.n) continue
441
+ lines.push(`| ${m.mode} | ${m.n} | ${pct1(m.godCI.rate)} | ${pct1(m.godCI.low)}–${pct1(m.godCI.high)} | ${m.med} | ${m.pure} |`)
442
+ }
443
+ if (stats.total && stats.total.n) {
444
+ 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} |`)
445
+ }
446
+ lines.push('',
447
+ `> ${stats.total ? stats.total.n : 0} 条本地日志,由 @dsh-suite/plugin-deus 记录。「神版/中版/纯区」为社区观察(X @NFT_Chen),未经 DeepSeek 官方证实;判定 = 推理流起手句式启发式,存在误判率。`,
448
+ '> https://www.npmjs.com/package/@dsh-suite/plugin-deus')
449
+ return lines.join('\n')
450
+ }
451
+
267
452
  return {
268
453
  inject: ['slots', 'locale'],
269
454
  apply(ctx) {
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']
@@ -50,7 +50,8 @@ export function detectMode(rawText, source = 'text') {
50
50
  if (/^we[\s,']/.test(head)) return 'god'
51
51
  // "User wants/asks…" (article dropped) — soft-med variant observed when
52
52
  // injected context is present under a minimal first request (cell M.2.1).
53
- if (source === 'reasoning' && /^user (wants|asks|is asking|said)\b/.test(head)) return 'med'
53
+ // v0.2: also "User is continuing…" (multi-turn med variant, drift study §9).
54
+ if (source === 'reasoning' && /^user (wants|asks|is asking|said|says|is continuing|is following up)\b/.test(head)) return 'med'
54
55
  // 中文推理起手 "我们需要/我们应该/我们来" — god-equivalent (reasoning only).
55
56
  if (source === 'reasoning' && /^我们(需要|应该|先来|来|先)/.test(head)) return 'god'
56
57
  const firstSentence = head.split(/[.!?\n]/)[0] || head
@@ -77,6 +78,126 @@ function wilson(k, n) {
77
78
  return { rate: p, low: Math.max(0, center - half), high: Math.min(1, center + half) }
78
79
  }
79
80
 
81
+ // ── shipped agent presets (v0.2 锚定维持的静态层) ───────────────────────────
82
+ const PLUGIN_VERSION = '0.3.0'
83
+ // 实测依据(research/deus-mode-matrix.md §8/§9):神版触发需要 Minimal persona
84
+ // ∧ 小工具目录;且本 harness 没有竞品的 promoteOn 机制——preset 构成全程恒定,
85
+ // 所以只要会话跑在锚定 preset 上,注入剥离/工具裁剪自动延续到每一轮。
86
+ // 用户级 preset 目录 <dshHome>/.agent-presets/<id>/ 由官方 agent-presets 插件
87
+ // 实时发现,安装后在 设置 > Agent presets 与会话 preset 选择器中立即可选。
88
+ const PERSONA_YML = `- id: persona
89
+ name: '@deepseek-ai/dsh-persona'
90
+ config:
91
+ text: You are a helpful software engineer assistant.
92
+ complete: true
93
+ includeRuntimeContext: false
94
+ `
95
+ const SHELL_YML = `- id: persistent-shell
96
+ name: cordis:group
97
+ group: true
98
+ isolate:
99
+ terminals: true
100
+ config:
101
+ - id: pty
102
+ name: '@deepseek-ai/dsh-terminal'
103
+ - id: terminal-bash
104
+ name: '@deepseek-ai/dsh-terminal-bash'
105
+ config:
106
+ timeoutMs: 300000
107
+ - id: persistent-bash
108
+ name: '@deepseek-ai/dsh-tool-bash-persistent'
109
+ config:
110
+ timeoutMs: 300000
111
+ description: |-
112
+ Run commands in a bash shell
113
+ * When invoking this tool, the contents of the "command" parameter does NOT need to be XML-escaped.
114
+ * You don't have access to the internet via this tool.
115
+ * You do have access to a mirror of common linux and python packages via apt and pip.
116
+ * State is persistent across command calls and discussions with the user.
117
+ * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.
118
+ * Please avoid commands that may produce a very large amount of output.
119
+ * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.
120
+ `
121
+ const FS_YML = `- id: filesystem
122
+ name: cordis:group
123
+ group: true
124
+ isolate:
125
+ fs: true
126
+ config:
127
+ - id: fs-local
128
+ name: '@deepseek-ai/dsh-fs-local'
129
+ config:
130
+ cwd: !!js process.env.DSH_CWD ?? process.cwd()
131
+ - id: str-replace-editor
132
+ name: '@deepseek-ai/dsh-tool-str-replace-editor'
133
+ config:
134
+ maxOutputChars: 16000
135
+ `
136
+ // 宽档追加的常用工具(实测梯度 §8:~8 工具档仍有 ~65% 触发率,换可用性)
137
+ const WIDE_TOOLS_YML = `- id: tool-fs
138
+ name: '@deepseek-ai/dsh-tool-fs'
139
+ - id: tool-fs-search
140
+ name: '@deepseek-ai/dsh-tool-fs-search'
141
+ config:
142
+ sampleOverCapGlobResults: false
143
+ - id: tool-todo
144
+ name: '@deepseek-ai/dsh-tool-todo'
145
+ config:
146
+ allowParallelInProgress: true
147
+ `
148
+ const AGENT_PRESETS = [
149
+ {
150
+ id: 'deus-anchored',
151
+ name: '神模扳机 · 窄锚(2 工具)',
152
+ description: 'Minimal persona + bash/str_replace_editor,剥注入;实测动手类神版起手 ~90%。@dsh-suite/plugin-deus 安装。',
153
+ order: 90,
154
+ cordis: `# deus-anchored: 2-tool minimal anchor preset (@dsh-suite/plugin-deus v0.2)\n${PERSONA_YML}\n${SHELL_YML}\n${FS_YML}`,
155
+ },
156
+ {
157
+ id: 'deus-anchored-wide',
158
+ name: '神模扳机 · 宽锚(~8 工具)',
159
+ description: 'Minimal persona + bash/编辑/读写/glob/grep/todo,剥注入;实测 ~65% 触发率换可用性。@dsh-suite/plugin-deus 安装。',
160
+ order: 91,
161
+ cordis: `# deus-anchored-wide: ~8-tool wide anchor preset (@dsh-suite/plugin-deus v0.2)\n${PERSONA_YML}\n${SHELL_YML}\n${FS_YML}\n${WIDE_TOOLS_YML}`,
162
+ },
163
+ ]
164
+
165
+ function agentPresetsRoot() {
166
+ return join(dshHome(), '.agent-presets')
167
+ }
168
+
169
+ // Idempotent install: only write when missing or content changed; never touch
170
+ // a preset dir the user has modified (marker mismatch → skip with warning).
171
+ function installAgentPresets(logger) {
172
+ const installed = []
173
+ for (const p of AGENT_PRESETS) {
174
+ try {
175
+ const dir = join(agentPresetsRoot(), p.id)
176
+ const marker = join(dir, '.deus-managed')
177
+ const cordisPath = join(dir, 'agent.cordis.yml')
178
+ const presetPath = join(dir, 'preset.yml')
179
+ const presetYml = `name: ${p.name}\ndescription: ${p.description}\norder: ${p.order}\n`
180
+ if (existsSync(dir) && !existsSync(marker)) {
181
+ logger?.warn?.(`[plugin-deus] skip ${p.id}: dir exists without .deus-managed marker (user-modified?)`)
182
+ installed.push({ id: p.id, installed: false, reason: 'user-modified' })
183
+ continue
184
+ }
185
+ const current = existsSync(cordisPath) ? readFileSync(cordisPath, 'utf8') : null
186
+ if (current !== p.cordis) {
187
+ mkdirSync(dir, { recursive: true })
188
+ writeFileSync(cordisPath, p.cordis)
189
+ writeFileSync(presetPath, presetYml)
190
+ writeFileSync(marker, `@dsh-suite/plugin-deus@${PLUGIN_VERSION}\n`)
191
+ }
192
+ installed.push({ id: p.id, installed: true, name: p.name, dir })
193
+ } catch (e) {
194
+ logger?.warn?.(`[plugin-deus] install ${p.id} failed:`, e)
195
+ installed.push({ id: p.id, installed: false, reason: String(e) })
196
+ }
197
+ }
198
+ return installed
199
+ }
200
+
80
201
  // ── storage paths ────────────────────────────────────────────────────────────
81
202
  function dshHome() {
82
203
  return process.env.DSH_HOME || join(homedir(), '.dsh')
@@ -95,6 +216,40 @@ function ensureDir() {
95
216
  mkdirSync(dataDir(), { recursive: true })
96
217
  }
97
218
 
219
+ // ── v0.3: session preset 探测(未锚定引导的数据源)─────────────────────────
220
+ // session/event 流里 user/message 不带 agentPreset;权威来源是磁盘会话日志的
221
+ // SessionHeader(jsonl.zstd 第一帧第一行,session-persistence-jsonl 格式)。
222
+ // 读文件 → 找第二个 zstd 帧魔数切出第一帧 → 解压取 header.agentPreset。
223
+ // 60s 缓存(blank 会话可在首轮前 recompose preset,不能永久缓存)。
224
+ const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
225
+ const presetCache = new Map() // sessionId -> { preset, ts }
226
+ function readSessionPreset(sessionId) {
227
+ const hit = presetCache.get(sessionId)
228
+ if (hit && Date.now() - hit.ts < 60_000) return hit.preset
229
+ let preset = null
230
+ try {
231
+ const root = join(dshHome(), 'sessions')
232
+ if (existsSync(root)) {
233
+ for (const ws of readdirSync(root)) {
234
+ const f = join(root, ws, sessionId, 'session.jsonl.zstd')
235
+ if (!existsSync(f)) continue
236
+ const buf = readFileSync(f)
237
+ const second = buf.indexOf(ZSTD_MAGIC, 4)
238
+ const firstFrame = second > 4 ? buf.subarray(0, second) : buf
239
+ try {
240
+ const line0 = zstdDecompressSync(firstFrame).toString('utf8').split('\n')[0]
241
+ const header = JSON.parse(line0)
242
+ if (header && header.type === 'session' && typeof header.agentPreset === 'string') preset = header.agentPreset
243
+ } catch { /* 帧截断/格式异常 → preset 保持 null */ }
244
+ break
245
+ }
246
+ }
247
+ } catch { /* 磁盘问题 → null */ }
248
+ presetCache.set(sessionId, { preset, ts: Date.now() })
249
+ if (presetCache.size > 300) presetCache.delete(presetCache.keys().next().value)
250
+ return preset
251
+ }
252
+
98
253
  // ── preset library: built-ins until the user edits, then presets.json wins ──
99
254
  function loadPresets() {
100
255
  try {
@@ -165,10 +320,10 @@ function statsFrom(entries) {
165
320
  }
166
321
 
167
322
  function toCsv(entries) {
168
- const head = 'ts,sessionId,prompt_mode,prompt_text,detected,source,first_sentence,model'
323
+ const head = 'ts,sessionId,prompt_mode,prompt_text,detected,source,turn,first_sentence,model'
169
324
  const esc = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`
170
325
  const rows = entries.map((e) =>
171
- [e.ts, e.sessionId, e.prompt_mode, e.prompt_text, e.detected, e.source, e.first_sentence, e.model].map(esc).join(','))
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(','))
172
327
  return [head, ...rows].join('\n') + '\n'
173
328
  }
174
329
 
@@ -226,12 +381,58 @@ function textOf(content) {
226
381
  }
227
382
 
228
383
  export function apply(ctx) {
384
+ // v0.2: 安装锚定 agent presets 到用户 preset 根(幂等,官方 agent-presets 插件实时发现)
385
+ const installedPresets = installAgentPresets(ctx.logger)
386
+
229
387
  // Pending trigger marks: sessionId -> { mode, prompt_text, ts, auto }
230
388
  const pending = new Map()
231
389
  // Chunk accumulation while a trigger is pending: sessionId -> { turn, step, reasoning, text }
232
390
  // (实测校准:指纹在 reasoning-delta 流里,text-delta 兜底;见 detectMode 注释)
233
391
  const buffers = new Map()
234
392
 
393
+ // ── v0.2 锚定维持:逐轮监控 deus/minimal preset 会话的指纹漂移 ─────────────
394
+ // 实测依据(§9):构成恒定不代表锚定恒定——V2 对照组全程极简也有 44-89% 的
395
+ // 逐轮摆动,且一旦漂移到中版(工具补齐或轮次噪声),竞品 promoteOn 场景 0/6
396
+ // 维持。所以我们逐轮判定、漂移即标记,客户端据此提示/自动重锚。
397
+ // anchored: sessionId -> { preset, turns: [{turn, fp}], lastFp, drifted, skipTurn, updatedAt }
398
+ const anchored = new Map()
399
+ // 逐轮缓冲: sessionId -> { turn, reasoning, text }
400
+ const watchBuf = new Map()
401
+ const ANCHOR_PRESET_RE = /^(deus-|minimal)/
402
+
403
+ function markAnchored(sessionId, preset, force = false) {
404
+ if (!sessionId || !preset || (!force && !ANCHOR_PRESET_RE.test(preset))) return
405
+ const cur = anchored.get(sessionId)
406
+ if (cur) { cur.preset = preset; cur.updatedAt = new Date().toISOString() }
407
+ else anchored.set(sessionId, { preset, turns: [], lastFp: null, drifted: false, skipTurn: -1, updatedAt: new Date().toISOString() })
408
+ }
409
+
410
+ function finalizeWatch(sessionId, turnNo, buf) {
411
+ const st = anchored.get(sessionId)
412
+ if (!st || !buf || turnNo === st.skipTurn) return
413
+ // 注入轮由 pending 流程判定并写日志;pending 未结案期间抑制 watch(避免同轮双记)
414
+ if (pending.has(sessionId)) return
415
+ const source = buf.reasoning.trim() !== '' ? 'reasoning' : 'text'
416
+ const text = source === 'reasoning' ? buf.reasoning : buf.text
417
+ if (text.trim() === '') return
418
+ if (st.turns.some((t) => t.turn === turnNo)) return // 已判定过这一轮
419
+ const fp = detectMode(text, source)
420
+ st.lastFp = fp
421
+ st.drifted = fp !== 'god'
422
+ st.turns.push({ turn: turnNo, fp, ts: new Date().toISOString() })
423
+ if (st.turns.length > 50) st.turns = st.turns.slice(-50)
424
+ st.updatedAt = new Date().toISOString()
425
+ appendLog({
426
+ ts: new Date().toISOString(),
427
+ sessionId,
428
+ prompt_mode: 'watch:' + st.preset,
429
+ detected: fp,
430
+ source,
431
+ first_sentence: firstSentenceOf(text),
432
+ turn: turnNo,
433
+ })
434
+ }
435
+
235
436
  function finalize(sessionId, fallbackText) {
236
437
  const mark = pending.get(sessionId)
237
438
  if (!mark) return
@@ -239,17 +440,29 @@ export function apply(ctx) {
239
440
  const reasoning = (buf && buf.reasoning) || ''
240
441
  const text = (buf && buf.text) || fallbackText || ''
241
442
  if (reasoning.trim() === '' && text.trim() === '' && !fallbackText) return // keep waiting for real content
443
+ const judgedTurn = typeof mark.turn === 'number' ? mark.turn : (buf && typeof buf.turn === 'number' ? buf.turn : undefined)
242
444
  pending.delete(sessionId)
243
445
  buffers.delete(sessionId)
244
446
  // 指纹优先取推理流(实测校准:指纹在 reasoning-delta 里),可见文本兜底
245
447
  const source = reasoning.trim() !== '' ? 'reasoning' : 'text'
246
448
  const classifyOn = source === 'reasoning' ? reasoning : text
449
+ const fp = detectMode(classifyOn, source)
450
+ // v0.2: 注入轮的判定并入锚定状态(turns 去重即阻止 watch 同轮双记;
451
+ // 此前仅靠 skipTurn/pending 抑制,user/message 无可靠 turn 号时会漏)
452
+ const st = anchored.get(sessionId)
453
+ if (st && typeof judgedTurn === 'number' && !st.turns.some((t) => t.turn === judgedTurn)) {
454
+ st.lastFp = fp
455
+ st.drifted = fp !== 'god'
456
+ st.turns.push({ turn: judgedTurn, fp, ts: new Date().toISOString() })
457
+ if (st.turns.length > 50) st.turns = st.turns.slice(-50)
458
+ st.updatedAt = new Date().toISOString()
459
+ }
247
460
  appendLog({
248
461
  ts: new Date().toISOString(),
249
462
  sessionId,
250
463
  prompt_mode: mark.mode,
251
464
  prompt_text: mark.prompt_text,
252
- detected: detectMode(classifyOn, source),
465
+ detected: fp,
253
466
  source,
254
467
  first_sentence: firstSentenceOf(classifyOn),
255
468
  model: mark.model || undefined,
@@ -259,6 +472,14 @@ export function apply(ctx) {
259
472
  ctx.on('session/event', (session, event) => {
260
473
  const sessionId = String(session.id)
261
474
 
475
+ // v0.2 锚定监控的会话归属判定:preset 选择事件 / 会话自带 preset / 注入命中
476
+ if (event.type === 'agent-preset/selected') {
477
+ markAnchored(sessionId, event.data && (event.data.agentPreset || event.data.preset || event.data.id))
478
+ }
479
+ if (session && typeof session.agentPreset === 'string' && ANCHOR_PRESET_RE.test(session.agentPreset)) {
480
+ markAnchored(sessionId, session.agentPreset)
481
+ }
482
+
262
483
  // A user message can BE the trigger: exact-match against the preset
263
484
  // library covers the conservative "copy + paste + enter" path (档 A)
264
485
  // without any explicit marking from the panel.
@@ -267,12 +488,52 @@ export function apply(ctx) {
267
488
  if (text !== '' && !pending.has(sessionId)) {
268
489
  const hit = loadPresets().find((p) => p.prompt !== null && p.prompt.trim() === text)
269
490
  if (hit) {
491
+ buffers.delete(sessionId) // 防止上轮残留 buffer 污染注入轮判定
270
492
  pending.set(sessionId, { mode: hit.id, prompt_text: hit.prompt, ts: Date.now(), auto: true })
493
+ // 注入命中的会话纳入锚定监控;注入轮由 pending 流程判定,watch 跳过该轮避免双记
494
+ markAnchored(sessionId, 'inject', true)
495
+ const st = anchored.get(sessionId)
496
+ if (st && typeof event.data?.turn === 'number') st.skipTurn = event.data.turn
271
497
  }
272
498
  }
273
499
  return
274
500
  }
275
501
 
502
+ // v0.2: 锚定会话的逐轮判定(与 pending 流程并行;pending 只覆盖注入轮)
503
+ if (anchored.has(sessionId) && (event.type === 'assistant/chunk' || event.type === 'assistant/message')) {
504
+ if (event.type === 'assistant/chunk') {
505
+ const chunk = event.data && event.data.chunk
506
+ const isReasoning = chunk && chunk.type === 'reasoning-delta' && typeof chunk.text === 'string'
507
+ const isText = chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string'
508
+ if (isReasoning || isText) {
509
+ const cur = watchBuf.get(sessionId) || { turn: event.data.turn, reasoning: '', text: '' }
510
+ if (cur.turn !== event.data.turn) {
511
+ finalizeWatch(sessionId, cur.turn, cur) // 新一轮开始 → 结算上一轮
512
+ cur.turn = event.data.turn; cur.reasoning = ''; cur.text = ''
513
+ }
514
+ // 每轮只采第一个 step 的起手(指纹在开头)
515
+ if (cur.reasoning.length < DETECT_WINDOW * 2 && cur.text.length < DETECT_WINDOW * 2) {
516
+ if (isReasoning) cur.reasoning += chunk.text
517
+ else cur.text += chunk.text
518
+ }
519
+ watchBuf.set(sessionId, cur)
520
+ const head = cur.reasoning !== '' ? cur.reasoning : cur.text
521
+ if ((head.length >= 8 && /[.!?\n。!?]/.test(head)) || head.length >= DETECT_WINDOW) finalizeWatch(sessionId, cur.turn, cur)
522
+ }
523
+ } else {
524
+ const parts = event.data && event.data.message && event.data.message.content
525
+ let reasoning = ''
526
+ if (Array.isArray(parts)) {
527
+ for (const p of parts) {
528
+ if (p && typeof p === 'object' && p.type === 'reasoning' && typeof p.text === 'string') reasoning += p.text
529
+ }
530
+ }
531
+ const cur = watchBuf.get(sessionId)
532
+ if (cur && reasoning !== '' && cur.reasoning === '') cur.reasoning = reasoning
533
+ if (cur) finalizeWatch(sessionId, cur.turn, cur)
534
+ }
535
+ }
536
+
276
537
  if (!pending.has(sessionId)) return
277
538
 
278
539
  if (event.type === 'assistant/chunk') {
@@ -281,8 +542,11 @@ export function apply(ctx) {
281
542
  const isText = chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string'
282
543
  if (isReasoning || isText) {
283
544
  const key = sessionId
545
+ const entry = pending.get(sessionId)
546
+ if (entry && entry.turn === undefined) entry.turn = event.data.turn // 记录注入轮号,watch 据此去重
284
547
  const cur = buffers.get(key) || { turn: event.data.turn, step: event.data.step, reasoning: '', text: '' }
285
548
  if (cur.turn !== event.data.turn || cur.step !== event.data.step) {
549
+ finalize(sessionId) // 轮边界先结算旧轮(此前静默清空导致判定延迟到后续轮)
286
550
  cur.turn = event.data.turn; cur.step = event.data.step; cur.reasoning = ''; cur.text = ''
287
551
  }
288
552
  if (isReasoning) cur.reasoning += chunk.text
@@ -315,6 +579,8 @@ export function apply(ctx) {
315
579
  }
316
580
 
317
581
  if (event.type === 'turn/end') {
582
+ const wb = watchBuf.get(sessionId)
583
+ if (wb) finalizeWatch(sessionId, wb.turn, wb)
318
584
  finalize(sessionId, '')
319
585
  }
320
586
  })
@@ -359,12 +625,22 @@ export function apply(ctx) {
359
625
  readBody(req, (body) => {
360
626
  const sessionId = String(body.sessionId || '')
361
627
  const mode = String(body.mode || '')
628
+ // v0.2: mode=reanchor 是漂移重锚的显式标记(提示文本由 client 随 body 带上)
629
+ if (mode === 'reanchor' && sessionId) {
630
+ buffers.delete(sessionId)
631
+ pending.set(sessionId, { mode: 'reanchor', prompt_text: String(body.prompt || ''), ts: Date.now(), auto: false })
632
+ markAnchored(sessionId, 'inject', true)
633
+ json({ ok: true })(req, res)
634
+ return
635
+ }
362
636
  const preset = loadPresets().find((p) => p.id === mode)
363
637
  if (!sessionId || !preset) {
364
638
  json({ ok: false, error: 'missing sessionId or unknown mode' }, 400)(req, res)
365
639
  return
366
640
  }
641
+ buffers.delete(sessionId)
367
642
  pending.set(sessionId, { mode, prompt_text: preset.prompt, ts: Date.now(), auto: false })
643
+ markAnchored(sessionId, 'inject', true) // 注入会话纳入逐轮锚定监控
368
644
  json({ ok: true })(req, res)
369
645
  })
370
646
  } }),
@@ -383,10 +659,31 @@ export function apply(ctx) {
383
659
  ctx.webServer.register({ kind: 'exact', path: '/deus/stats', handler: (req, res) => {
384
660
  json(statsFrom(readLog(10000)))(req, res)
385
661
  } }),
662
+ // v0.3: 当前会话的 agent preset(dock 未锚定引导用;来源=会话日志头行)
663
+ ctx.webServer.register({ kind: 'exact', path: '/deus/session-preset', handler: (req, res) => {
664
+ const u = new URL(String(req.url || ''), 'http://localhost')
665
+ const sessionId = String(u.searchParams.get('sessionId') || '')
666
+ if (!sessionId) { json({ ok: false, error: 'missing sessionId' }, 400)(req, res); return }
667
+ json({ sessionId, preset: readSessionPreset(sessionId) })(req, res)
668
+ } }),
386
669
  ctx.webServer.register({ kind: 'exact', path: '/deus/version', handler: (req, res) => {
387
670
  let dsh = 'unknown'
388
671
  try { dsh = detectDshVersion() } catch (e) { ctx.logger.warn('[plugin-deus] version detect failed:', e) }
389
- json({ plugin: '0.1.0', dsh, logPath: logFile() })(req, res)
672
+ json({ plugin: PLUGIN_VERSION, dsh, logPath: logFile() })(req, res)
673
+ } }),
674
+ // v0.2 锚定维持状态: 安装的 agent presets + 受监控会话的逐轮指纹/漂移状态
675
+ ctx.webServer.register({ kind: 'exact', path: '/deus/anchor', handler: (req, res) => {
676
+ const sessions = [...anchored.entries()].map(([sessionId, st]) => ({
677
+ sessionId,
678
+ preset: st.preset,
679
+ total: st.turns.length,
680
+ god: st.turns.filter((t) => t.fp === 'god').length,
681
+ lastFp: st.lastFp,
682
+ drifted: st.drifted,
683
+ turns: st.turns.slice(-10),
684
+ updatedAt: st.updatedAt,
685
+ }))
686
+ json({ agentPresets: installedPresets, sessions })(req, res)
390
687
  } }),
391
688
  ]
392
689
  return () => { for (const d of disposers) d() }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-suite/plugin-deus",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "神模扳机 Deus Trigger — 极简提示词触发实验台:预设库 + 一键注入 + 起手识别器 + 触发率统计(Wilson CI)。社区观察未获官方证实,本插件不承诺解锁任何隐藏能力。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",