@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 +13 -1
- package/lib/client.js +190 -5
- package/lib/index.js +304 -7
- package/package.json +1 -1
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=
|
|
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=
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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