@dsh-suite/plugin-deus 0.1.0 → 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 +11 -1
- package/lib/client.js +104 -5
- package/lib/index.js +261 -5
- package/package.json +1 -1
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=
|
|
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=
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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