@dsh-suite/plugin-deus 0.1.1 → 0.2.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,14 @@
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` 模式独立记录,便于对比重锚前后维持率。
25
29
 
26
30
  ## 安装 / Install
27
31
 
@@ -48,6 +52,12 @@ npx @deepseek-ai/dsh plugin --profile web add ./plugin-deus # 在其父目录
48
52
  - 全部是用户正常交互(prompt 注入 + 回复观测),无越权、无绕过。
49
53
  - 若实验证明无差异:本插件的身份即**通用极简提示词 A/B 测试台**,"神模"只是第一个被测假设。
50
54
 
55
+ ## 实测依据 / Measured basis
56
+
57
+ 本团队 2026-08-15 实测(deepseek-v4-pro,N=440 次 API 采样,`research/deus-mode-matrix.md`):
58
+ 神版触发率随工具数单调衰减(2 工具 ~90% → ~8 工具 ~65% → 25 工具 0%);剥离注入上下文 20%→90%;
59
+ 讲解/咨询类任务 ~0%;锚定维持见上。全部为倾向信号,非官方证实。
60
+
51
61
  ## License
52
62
 
53
63
  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,11 @@ 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: '锚定维持中',
40
45
  }
41
46
  const en = {
42
47
  nav: 'Deus Trigger', sub: 'Minimal-prompt trigger bench · inject → classify → stats',
@@ -47,7 +52,7 @@ window.__ModuleLoader__.load({
47
52
  edit: 'Edit', save: 'Save', cancel: 'Cancel', del: 'Delete', add: 'Add preset', resetPresets: 'Restore built-ins',
48
53
  editPrompt: 'prompt text (empty = empty-input mode)', editLabel: 'label', editId: 'id (unique)',
49
54
  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.',
55
+ 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
56
  colMode: 'Mode', colN: 'N', colGod: 'god rate', colMed: 'med rate', colPure: 'pure rate',
52
57
  exportCsv: 'Export CSV', resetLog: 'Clear log', resetLogConfirm: 'Clear ALL experiment log entries? This cannot be undone.',
53
58
  recent: 'Recent entries', recentHint: 'The detector is a heuristic tendency signal; first_sentence is kept for human review.',
@@ -56,6 +61,11 @@ window.__ModuleLoader__.load({
56
61
  dockHint: 'Deus Trigger (experiment · community observation, unconfirmed)', dockSend: 'Inject & send', dockFill: 'Inject',
57
62
  autoSend: 'Auto-send after inject',
58
63
  detected_pure: 'pure', detected_med: 'med', detected_god: 'god', detected_unknown: 'unknown',
64
+ 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.',
65
+ 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.',
66
+ colSession: 'Session', colPreset: 'preset', colTurns: 'turns', colGodRate: 'god rate', colLastFp: 'latest', colAnchorState: 'state',
67
+ anchoredOn: 'anchored', drifted: 'drifted', reanchor: '⚓ Re-anchor', autoReanchor: 'Auto re-anchor on drift',
68
+ reanchorSent: 'Re-anchor nudge sent', dockAnchored: 'anchored',
59
69
  }
60
70
 
61
71
  const S = {
@@ -87,6 +97,7 @@ window.__ModuleLoader__.load({
87
97
  const [stats, setStats] = useState(null)
88
98
  const [entries, setEntries] = useState([])
89
99
  const [ver, setVer] = useState(null)
100
+ const [anchor, setAnchor] = useState(null)
90
101
  const [err, setErr] = useState('')
91
102
  const [copied, setCopied] = useState('')
92
103
  const [editing, setEditing] = useState(null) // {id, label_zh, prompt} draft
@@ -96,6 +107,7 @@ window.__ModuleLoader__.load({
96
107
  fetch('/deus/stats').then((r) => r.json()).then(setStats).catch(() => {})
97
108
  fetch('/deus/log').then((r) => r.json()).then((d) => setEntries(d.entries || [])).catch(() => {})
98
109
  fetch('/deus/version').then((r) => r.json()).then(setVer).catch(() => {})
110
+ fetch('/deus/anchor').then((r) => r.json()).then(setAnchor).catch(() => {})
99
111
  }
100
112
  useEffect(() => { refresh() }, [])
101
113
 
@@ -200,6 +212,33 @@ window.__ModuleLoader__.load({
200
212
  h('button', { style: { ...S.btn, color: '#f85149', borderColor: '#f85149' }, onClick: resetLog }, '🗑 ' + t('resetLog')),
201
213
  ),
202
214
 
215
+ h('div', { style: S.h2 }, t('anchor')),
216
+ h('div', { style: S.status }, t('anchorHint')),
217
+ anchor && anchor.agentPresets && h('div', { style: S.card },
218
+ h('div', { style: S.status }, t('anchorPresetInstalled')),
219
+ anchor.agentPresets.map((p) => h('div', { key: p.id, style: { display: 'flex', gap: '8px', alignItems: 'center', marginTop: '6px' } },
220
+ h('span', { style: { ...S.chip, color: p.installed ? '#3fb950' : '#f85149', borderColor: p.installed ? '#3fb950' : '#f85149' } }, p.installed ? '✓' : '✗'),
221
+ h('span', { style: S.name }, p.name || p.id),
222
+ p.reason ? h('span', { style: S.status }, '(' + p.reason + ')') : null,
223
+ )),
224
+ ),
225
+ anchor && anchor.sessions && anchor.sessions.length > 0
226
+ ? h('table', { style: S.table },
227
+ h('thead', null, h('tr', null,
228
+ h('th', { style: S.th }, t('colSession')), h('th', { style: S.th }, t('colPreset')),
229
+ h('th', { style: S.th }, t('colTurns')), h('th', { style: S.th }, t('colGodRate')),
230
+ h('th', { style: S.th }, t('colLastFp')), h('th', { style: S.th }, t('colAnchorState')))),
231
+ h('tbody', null, anchor.sessions.map((s) => h('tr', { key: s.sessionId },
232
+ h('td', { style: { ...S.td, ...S.mono } }, s.sessionId.slice(0, 8)),
233
+ h('td', { style: S.td }, s.preset),
234
+ h('td', { style: S.td }, s.total),
235
+ h('td', { style: { ...S.td, color: '#d2a8ff' } }, s.total ? Math.round(100 * s.god / s.total) + '%' : '—'),
236
+ h('td', { style: S.td }, s.lastFp ? t('detected_' + s.lastFp) : '—'),
237
+ h('td', { style: { ...S.td, color: s.drifted ? '#f85149' : '#3fb950' } }, s.drifted ? '⚠ ' + t('drifted') : '⚓ ' + t('anchoredOn')),
238
+ ))),
239
+ )
240
+ : h('div', { style: S.status }, t('anchorNone')),
241
+
203
242
  h('div', { style: S.h2 }, t('recent')),
204
243
  h('div', { style: S.status }, t('recentHint')),
205
244
  entries.length === 0 ? h('div', { style: S.status }, t('noData'))
@@ -227,11 +266,61 @@ window.__ModuleLoader__.load({
227
266
  useEffect(() => {
228
267
  fetch('/deus/presets').then((r) => r.json()).then((d) => setPresets(d.presets || [])).catch(() => {})
229
268
  }, [])
230
- if (presets.length === 0) return null
231
269
 
232
- const sessionId = props.session && props.session.sessionId
270
+ const sessionId = props.sessionId || (props.session && props.session.sessionId)
233
271
  const actions = props.inputActions
234
272
 
273
+ // v0.2 锚定维持:轮询宿主锚定状态,漂移时给重锚 chip / 自动重锚
274
+ const [anchor, setAnchor] = useState(null)
275
+ const [autoReanchor, setAutoReanchor] = useState(() => {
276
+ try { return window.localStorage.getItem('deus.autoReanchor') === '1' } catch { return false }
277
+ })
278
+ const [reanchorMsg, setReanchorMsg] = useState('')
279
+ const lastReanchorKey = useRef('')
280
+ const REANCHOR = 'We need to continue working on this together. Let us pick up where we left off. 我们继续协作,接着上一步往下做。'
281
+
282
+ useEffect(() => {
283
+ let dead = false
284
+ const poll = () => fetch('/deus/anchor').then((r) => r.json()).then((d) => { if (!dead) setAnchor(d) }).catch(() => {})
285
+ poll()
286
+ const timer = setInterval(poll, 4000)
287
+ return () => { dead = true; clearInterval(timer) }
288
+ }, [])
289
+
290
+ const st = anchor && Array.isArray(anchor.sessions)
291
+ ? anchor.sessions.find((s) => String(s.sessionId) === String(sessionId))
292
+ : null
293
+
294
+ async function fireReanchor() {
295
+ try {
296
+ await fetch('/deus/trigger', {
297
+ method: 'POST', headers: { 'content-type': 'application/json' },
298
+ body: JSON.stringify({ sessionId: String(sessionId || ''), mode: 'reanchor', prompt: REANCHOR }),
299
+ })
300
+ } catch { /* best-effort */ }
301
+ if (actions && typeof actions.setDraft === 'function') {
302
+ actions.setDraft(REANCHOR)
303
+ if (typeof actions.submit === 'function') actions.submit()
304
+ setReanchorMsg(t('reanchorSent'))
305
+ setTimeout(() => setReanchorMsg(''), 3000)
306
+ }
307
+ }
308
+
309
+ // 自动重锚:漂移且开启时自动发一次(按 sessionId+轮数去抖)
310
+ useEffect(() => {
311
+ if (!st || !st.drifted || !autoReanchor) return
312
+ const key = String(sessionId) + ':' + st.total
313
+ if (lastReanchorKey.current === key) return
314
+ lastReanchorKey.current = key
315
+ fireReanchor()
316
+ }, [st && st.drifted, st && st.total, autoReanchor])
317
+
318
+ function toggleAutoReanchor() {
319
+ const next = !autoReanchor
320
+ setAutoReanchor(next)
321
+ try { window.localStorage.setItem('deus.autoReanchor', next ? '1' : '0') } catch { /* ignore */ }
322
+ }
323
+
235
324
  async function fire(p) {
236
325
  try {
237
326
  await fetch('/deus/trigger', {
@@ -253,14 +342,24 @@ window.__ModuleLoader__.load({
253
342
  try { window.localStorage.setItem('deus.autoSend', next ? '1' : '0') } catch { /* ignore */ }
254
343
  }
255
344
 
345
+ if (presets.length === 0) return null // 所有 hooks 之上不可早退(React hooks 顺序)
346
+
256
347
  return h('div', { style: S.dock, title: t('dockHint') },
257
348
  h('span', { style: { fontSize: '11px', color: '#8b949e' } }, '⚗'),
258
349
  presets.map((p) => h('button', {
259
350
  key: p.id, style: S.dockChip, title: t('dockHint'),
260
351
  onClick: () => fire(p),
261
352
  }, (autoSend ? '🚀 ' : '⚡ ') + p.label_zh)),
353
+ st ? h('button', {
354
+ style: { ...S.dockChip, color: st.drifted ? '#f85149' : '#3fb950', borderColor: st.drifted ? '#f85149' : '#3fb950' },
355
+ title: st.drifted ? t('reanchor') : t('dockAnchored'),
356
+ onClick: st.drifted ? fireReanchor : undefined,
357
+ }, st.drifted ? '⚠ ' + t('reanchor') : `⚓ ${t('dockAnchored')} ${st.god}/${st.total}`) : null,
358
+ reanchorMsg ? h('span', { style: { fontSize: '11px', color: '#3fb950' } }, reanchorMsg) : null,
262
359
  h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoSend'), onClick: toggleAuto },
263
360
  (autoSend ? '☑ ' : '☐ ') + t('autoSend')),
361
+ st ? h('button', { style: { ...S.dockChip, borderStyle: 'dashed' }, title: t('autoReanchor'), onClick: toggleAutoReanchor },
362
+ (autoReanchor ? '☑ ' : '☐ ') + t('autoReanchor')) : null,
264
363
  )
265
364
  }
266
365
 
package/lib/index.js CHANGED
@@ -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.2.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')
@@ -165,10 +286,10 @@ function statsFrom(entries) {
165
286
  }
166
287
 
167
288
  function toCsv(entries) {
168
- const head = 'ts,sessionId,prompt_mode,prompt_text,detected,source,first_sentence,model'
289
+ const head = 'ts,sessionId,prompt_mode,prompt_text,detected,source,turn,first_sentence,model'
169
290
  const esc = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`
170
291
  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(','))
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(','))
172
293
  return [head, ...rows].join('\n') + '\n'
173
294
  }
174
295
 
@@ -226,12 +347,58 @@ function textOf(content) {
226
347
  }
227
348
 
228
349
  export function apply(ctx) {
350
+ // v0.2: 安装锚定 agent presets 到用户 preset 根(幂等,官方 agent-presets 插件实时发现)
351
+ const installedPresets = installAgentPresets(ctx.logger)
352
+
229
353
  // Pending trigger marks: sessionId -> { mode, prompt_text, ts, auto }
230
354
  const pending = new Map()
231
355
  // Chunk accumulation while a trigger is pending: sessionId -> { turn, step, reasoning, text }
232
356
  // (实测校准:指纹在 reasoning-delta 流里,text-delta 兜底;见 detectMode 注释)
233
357
  const buffers = new Map()
234
358
 
359
+ // ── v0.2 锚定维持:逐轮监控 deus/minimal preset 会话的指纹漂移 ─────────────
360
+ // 实测依据(§9):构成恒定不代表锚定恒定——V2 对照组全程极简也有 44-89% 的
361
+ // 逐轮摆动,且一旦漂移到中版(工具补齐或轮次噪声),竞品 promoteOn 场景 0/6
362
+ // 维持。所以我们逐轮判定、漂移即标记,客户端据此提示/自动重锚。
363
+ // anchored: sessionId -> { preset, turns: [{turn, fp}], lastFp, drifted, skipTurn, updatedAt }
364
+ const anchored = new Map()
365
+ // 逐轮缓冲: sessionId -> { turn, reasoning, text }
366
+ const watchBuf = new Map()
367
+ const ANCHOR_PRESET_RE = /^(deus-|minimal)/
368
+
369
+ function markAnchored(sessionId, preset, force = false) {
370
+ if (!sessionId || !preset || (!force && !ANCHOR_PRESET_RE.test(preset))) return
371
+ const cur = anchored.get(sessionId)
372
+ if (cur) { cur.preset = preset; cur.updatedAt = new Date().toISOString() }
373
+ else anchored.set(sessionId, { preset, turns: [], lastFp: null, drifted: false, skipTurn: -1, updatedAt: new Date().toISOString() })
374
+ }
375
+
376
+ function finalizeWatch(sessionId, turnNo, buf) {
377
+ const st = anchored.get(sessionId)
378
+ if (!st || !buf || turnNo === st.skipTurn) return
379
+ // 注入轮由 pending 流程判定并写日志;pending 未结案期间抑制 watch(避免同轮双记)
380
+ if (pending.has(sessionId)) return
381
+ const source = buf.reasoning.trim() !== '' ? 'reasoning' : 'text'
382
+ const text = source === 'reasoning' ? buf.reasoning : buf.text
383
+ if (text.trim() === '') return
384
+ if (st.turns.some((t) => t.turn === turnNo)) return // 已判定过这一轮
385
+ const fp = detectMode(text, source)
386
+ st.lastFp = fp
387
+ st.drifted = fp !== 'god'
388
+ st.turns.push({ turn: turnNo, fp, ts: new Date().toISOString() })
389
+ if (st.turns.length > 50) st.turns = st.turns.slice(-50)
390
+ st.updatedAt = new Date().toISOString()
391
+ appendLog({
392
+ ts: new Date().toISOString(),
393
+ sessionId,
394
+ prompt_mode: 'watch:' + st.preset,
395
+ detected: fp,
396
+ source,
397
+ first_sentence: firstSentenceOf(text),
398
+ turn: turnNo,
399
+ })
400
+ }
401
+
235
402
  function finalize(sessionId, fallbackText) {
236
403
  const mark = pending.get(sessionId)
237
404
  if (!mark) return
@@ -239,17 +406,29 @@ export function apply(ctx) {
239
406
  const reasoning = (buf && buf.reasoning) || ''
240
407
  const text = (buf && buf.text) || fallbackText || ''
241
408
  if (reasoning.trim() === '' && text.trim() === '' && !fallbackText) return // keep waiting for real content
409
+ const judgedTurn = typeof mark.turn === 'number' ? mark.turn : (buf && typeof buf.turn === 'number' ? buf.turn : undefined)
242
410
  pending.delete(sessionId)
243
411
  buffers.delete(sessionId)
244
412
  // 指纹优先取推理流(实测校准:指纹在 reasoning-delta 里),可见文本兜底
245
413
  const source = reasoning.trim() !== '' ? 'reasoning' : 'text'
246
414
  const classifyOn = source === 'reasoning' ? reasoning : text
415
+ const fp = detectMode(classifyOn, source)
416
+ // v0.2: 注入轮的判定并入锚定状态(turns 去重即阻止 watch 同轮双记;
417
+ // 此前仅靠 skipTurn/pending 抑制,user/message 无可靠 turn 号时会漏)
418
+ const st = anchored.get(sessionId)
419
+ if (st && typeof judgedTurn === 'number' && !st.turns.some((t) => t.turn === judgedTurn)) {
420
+ st.lastFp = fp
421
+ st.drifted = fp !== 'god'
422
+ st.turns.push({ turn: judgedTurn, fp, ts: new Date().toISOString() })
423
+ if (st.turns.length > 50) st.turns = st.turns.slice(-50)
424
+ st.updatedAt = new Date().toISOString()
425
+ }
247
426
  appendLog({
248
427
  ts: new Date().toISOString(),
249
428
  sessionId,
250
429
  prompt_mode: mark.mode,
251
430
  prompt_text: mark.prompt_text,
252
- detected: detectMode(classifyOn, source),
431
+ detected: fp,
253
432
  source,
254
433
  first_sentence: firstSentenceOf(classifyOn),
255
434
  model: mark.model || undefined,
@@ -259,6 +438,14 @@ export function apply(ctx) {
259
438
  ctx.on('session/event', (session, event) => {
260
439
  const sessionId = String(session.id)
261
440
 
441
+ // v0.2 锚定监控的会话归属判定:preset 选择事件 / 会话自带 preset / 注入命中
442
+ if (event.type === 'agent-preset/selected') {
443
+ markAnchored(sessionId, event.data && (event.data.agentPreset || event.data.preset || event.data.id))
444
+ }
445
+ if (session && typeof session.agentPreset === 'string' && ANCHOR_PRESET_RE.test(session.agentPreset)) {
446
+ markAnchored(sessionId, session.agentPreset)
447
+ }
448
+
262
449
  // A user message can BE the trigger: exact-match against the preset
263
450
  // library covers the conservative "copy + paste + enter" path (档 A)
264
451
  // without any explicit marking from the panel.
@@ -267,12 +454,52 @@ export function apply(ctx) {
267
454
  if (text !== '' && !pending.has(sessionId)) {
268
455
  const hit = loadPresets().find((p) => p.prompt !== null && p.prompt.trim() === text)
269
456
  if (hit) {
457
+ buffers.delete(sessionId) // 防止上轮残留 buffer 污染注入轮判定
270
458
  pending.set(sessionId, { mode: hit.id, prompt_text: hit.prompt, ts: Date.now(), auto: true })
459
+ // 注入命中的会话纳入锚定监控;注入轮由 pending 流程判定,watch 跳过该轮避免双记
460
+ markAnchored(sessionId, 'inject', true)
461
+ const st = anchored.get(sessionId)
462
+ if (st && typeof event.data?.turn === 'number') st.skipTurn = event.data.turn
271
463
  }
272
464
  }
273
465
  return
274
466
  }
275
467
 
468
+ // v0.2: 锚定会话的逐轮判定(与 pending 流程并行;pending 只覆盖注入轮)
469
+ if (anchored.has(sessionId) && (event.type === 'assistant/chunk' || event.type === 'assistant/message')) {
470
+ if (event.type === 'assistant/chunk') {
471
+ const chunk = event.data && event.data.chunk
472
+ const isReasoning = chunk && chunk.type === 'reasoning-delta' && typeof chunk.text === 'string'
473
+ const isText = chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string'
474
+ if (isReasoning || isText) {
475
+ const cur = watchBuf.get(sessionId) || { turn: event.data.turn, reasoning: '', text: '' }
476
+ if (cur.turn !== event.data.turn) {
477
+ finalizeWatch(sessionId, cur.turn, cur) // 新一轮开始 → 结算上一轮
478
+ cur.turn = event.data.turn; cur.reasoning = ''; cur.text = ''
479
+ }
480
+ // 每轮只采第一个 step 的起手(指纹在开头)
481
+ if (cur.reasoning.length < DETECT_WINDOW * 2 && cur.text.length < DETECT_WINDOW * 2) {
482
+ if (isReasoning) cur.reasoning += chunk.text
483
+ else cur.text += chunk.text
484
+ }
485
+ watchBuf.set(sessionId, cur)
486
+ const head = cur.reasoning !== '' ? cur.reasoning : cur.text
487
+ if ((head.length >= 8 && /[.!?\n。!?]/.test(head)) || head.length >= DETECT_WINDOW) finalizeWatch(sessionId, cur.turn, cur)
488
+ }
489
+ } else {
490
+ const parts = event.data && event.data.message && event.data.message.content
491
+ let reasoning = ''
492
+ if (Array.isArray(parts)) {
493
+ for (const p of parts) {
494
+ if (p && typeof p === 'object' && p.type === 'reasoning' && typeof p.text === 'string') reasoning += p.text
495
+ }
496
+ }
497
+ const cur = watchBuf.get(sessionId)
498
+ if (cur && reasoning !== '' && cur.reasoning === '') cur.reasoning = reasoning
499
+ if (cur) finalizeWatch(sessionId, cur.turn, cur)
500
+ }
501
+ }
502
+
276
503
  if (!pending.has(sessionId)) return
277
504
 
278
505
  if (event.type === 'assistant/chunk') {
@@ -281,8 +508,11 @@ export function apply(ctx) {
281
508
  const isText = chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string'
282
509
  if (isReasoning || isText) {
283
510
  const key = sessionId
511
+ const entry = pending.get(sessionId)
512
+ if (entry && entry.turn === undefined) entry.turn = event.data.turn // 记录注入轮号,watch 据此去重
284
513
  const cur = buffers.get(key) || { turn: event.data.turn, step: event.data.step, reasoning: '', text: '' }
285
514
  if (cur.turn !== event.data.turn || cur.step !== event.data.step) {
515
+ finalize(sessionId) // 轮边界先结算旧轮(此前静默清空导致判定延迟到后续轮)
286
516
  cur.turn = event.data.turn; cur.step = event.data.step; cur.reasoning = ''; cur.text = ''
287
517
  }
288
518
  if (isReasoning) cur.reasoning += chunk.text
@@ -315,6 +545,8 @@ export function apply(ctx) {
315
545
  }
316
546
 
317
547
  if (event.type === 'turn/end') {
548
+ const wb = watchBuf.get(sessionId)
549
+ if (wb) finalizeWatch(sessionId, wb.turn, wb)
318
550
  finalize(sessionId, '')
319
551
  }
320
552
  })
@@ -359,12 +591,22 @@ export function apply(ctx) {
359
591
  readBody(req, (body) => {
360
592
  const sessionId = String(body.sessionId || '')
361
593
  const mode = String(body.mode || '')
594
+ // v0.2: mode=reanchor 是漂移重锚的显式标记(提示文本由 client 随 body 带上)
595
+ if (mode === 'reanchor' && sessionId) {
596
+ buffers.delete(sessionId)
597
+ pending.set(sessionId, { mode: 'reanchor', prompt_text: String(body.prompt || ''), ts: Date.now(), auto: false })
598
+ markAnchored(sessionId, 'inject', true)
599
+ json({ ok: true })(req, res)
600
+ return
601
+ }
362
602
  const preset = loadPresets().find((p) => p.id === mode)
363
603
  if (!sessionId || !preset) {
364
604
  json({ ok: false, error: 'missing sessionId or unknown mode' }, 400)(req, res)
365
605
  return
366
606
  }
607
+ buffers.delete(sessionId)
367
608
  pending.set(sessionId, { mode, prompt_text: preset.prompt, ts: Date.now(), auto: false })
609
+ markAnchored(sessionId, 'inject', true) // 注入会话纳入逐轮锚定监控
368
610
  json({ ok: true })(req, res)
369
611
  })
370
612
  } }),
@@ -386,7 +628,21 @@ export function apply(ctx) {
386
628
  ctx.webServer.register({ kind: 'exact', path: '/deus/version', handler: (req, res) => {
387
629
  let dsh = 'unknown'
388
630
  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)
631
+ json({ plugin: PLUGIN_VERSION, dsh, logPath: logFile() })(req, res)
632
+ } }),
633
+ // v0.2 锚定维持状态: 安装的 agent presets + 受监控会话的逐轮指纹/漂移状态
634
+ ctx.webServer.register({ kind: 'exact', path: '/deus/anchor', handler: (req, res) => {
635
+ const sessions = [...anchored.entries()].map(([sessionId, st]) => ({
636
+ sessionId,
637
+ preset: st.preset,
638
+ total: st.turns.length,
639
+ god: st.turns.filter((t) => t.fp === 'god').length,
640
+ lastFp: st.lastFp,
641
+ drifted: st.drifted,
642
+ turns: st.turns.slice(-10),
643
+ updatedAt: st.updatedAt,
644
+ }))
645
+ json({ agentPresets: installedPresets, sessions })(req, res)
390
646
  } }),
391
647
  ]
392
648
  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.2.0",
4
4
  "description": "神模扳机 Deus Trigger — 极简提示词触发实验台:预设库 + 一键注入 + 起手识别器 + 触发率统计(Wilson CI)。社区观察未获官方证实,本插件不承诺解锁任何隐藏能力。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",