dsh-plugin-tool-management 0.17.0 → 0.18.5
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/CHANGELOG.md +70 -0
- package/README.md +19 -4
- package/README_EN.md +19 -4
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +29 -0
- package/lib/client.js +1662 -161
- package/lib/compat/capability-specs.js +321 -0
- package/lib/compat/host-probe.js +261 -0
- package/lib/compat/host-roots.js +168 -0
- package/lib/compat/probe-types.js +6 -0
- package/lib/compat/probe.js +9 -720
- package/lib/context-inject-contract.js +114 -0
- package/lib/context-inject-frame.js +147 -0
- package/lib/context-inject-select.js +127 -0
- package/lib/context-inject-surface.js +97 -0
- package/lib/context-inject.js +10 -478
- package/lib/exports/transcript.js +223 -0
- package/lib/http-api.js +229 -0
- package/lib/hub.js +2 -0
- package/lib/index.js +138 -868
- package/lib/mcp/inspect-probe.js +199 -0
- package/lib/mcp/manager.js +30 -548
- package/lib/mcp/tool-visibility.js +388 -0
- package/lib/memories/archive-engine.js +142 -5
- package/lib/memories/archive.js +10 -1
- package/lib/memories/discovery.js +261 -0
- package/lib/memories/export-plan.js +42 -0
- package/lib/memories/memories-types.js +1 -0
- package/lib/memories/service.js +4 -38
- package/lib/memories/snapshot.js +10 -252
- package/lib/op-registry.js +21 -0
- package/lib/ops/compat.js +11 -0
- package/lib/ops/quick-prompts.js +26 -0
- package/lib/ops/scene-records.js +40 -0
- package/lib/ops/scene-sync.js +71 -1
- package/lib/ops/sessions.js +34 -14
- package/lib/ops/snapshot.js +18 -12
- package/lib/prompts/quick-service.js +211 -0
- package/lib/scene-linkage.js +184 -0
- package/lib/sessions/archive-schemas.js +121 -0
- package/lib/sessions/path-key.js +46 -0
- package/lib/sessions/workspace.js +29 -166
- package/lib/settings-sidecars.js +143 -0
- package/lib/skills/core-types.js +8 -0
- package/lib/skills/core-util.js +81 -0
- package/lib/skills/core.js +56 -2326
- package/lib/skills/dir-watch.js +71 -0
- package/lib/skills/frontmatter.js +96 -0
- package/lib/skills/homes.js +14 -0
- package/lib/skills/import-pipeline.js +231 -0
- package/lib/skills/manager-state.js +800 -0
- package/lib/skills/official.js +269 -0
- package/lib/skills/provider-registration.js +89 -0
- package/lib/skills/service.js +5 -240
- package/lib/skills/skill-roots.js +287 -0
- package/lib/skills/skill-trash.js +265 -0
- package/lib/skills/system-trash.js +97 -0
- package/lib/skills/upload-pipeline.js +374 -0
- package/lib/slash-settings.js +25 -0
- package/lib/subagents/persona-parse.js +312 -0
- package/lib/subagents/persona-types.js +5 -0
- package/lib/subagents/persona-write.js +93 -0
- package/lib/subagents/run-provider.js +159 -0
- package/lib/subagents/service.js +14 -638
- package/lib/subagents/tool-filter.js +98 -0
- package/lib/token-config.js +240 -0
- package/lib/tool-table-switch.js +163 -0
- package/lib/tools/scene.js +40 -9
- package/package.json +3 -2
package/lib/context-inject.js
CHANGED
|
@@ -71,436 +71,17 @@
|
|
|
71
71
|
// 注入永远不能让这一步失败:任何异常都在监听器里吞掉、原样返回 decision。
|
|
72
72
|
// 遥测同理(`noteToolUse` 自己吞异常):它绝不能影响工具本身。
|
|
73
73
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
74
|
-
import { SessionSeq } from '@deepseek-ai/dsh-session';
|
|
75
74
|
import { clearRuntimeNote, noteRuntime } from './compat/runtime-notes.js';
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* 旧消息会被当成"不在上下文里"而重发一次,所以这几个字符串是稳定契约。
|
|
87
|
-
*
|
|
88
|
-
* ⚠️ 0.14.0 把 `memory` 的 kind 从 `scene-memory-manager-catalog` 改成
|
|
89
|
-
* `memory-manager-catalog`,并把场景拆成独立的 `scene` 域 —— 升级后每个会话**会重发一次**
|
|
90
|
-
* 这两段(旧消息认不出来)。一次性代价,换来的是两段能各自开关、各自去重。
|
|
91
|
-
*/
|
|
92
|
-
export const INJECT_KIND_OF = {
|
|
93
|
-
scene: 'scene-manager-catalog',
|
|
94
|
-
memory: 'memory-manager-catalog',
|
|
95
|
-
mcp: 'mcp-manager-catalog',
|
|
96
|
-
skills: 'skill-manager-catalog',
|
|
97
|
-
subagents: 'subagent-manager-catalog',
|
|
98
|
-
prompt: 'prompt-manager-catalog',
|
|
99
|
-
};
|
|
100
|
-
/**
|
|
101
|
-
* 域 → 该域在模型工具表里的**工具名前缀**(采纳遥测的唯一映射)。
|
|
102
|
-
*
|
|
103
|
-
* 为什么要这张表:注入只解决"内容到没到",不解决"模型用没用"。「注入实况」此前
|
|
104
|
-
* 只能答前半句 —— 勾了开关、正文也在上下文里,但模型从头到尾没伸手,界面上和
|
|
105
|
-
* 「用了」长得一模一样。有了映射,就能把「投递 N 次 / 调用 M 次」并排摆出来,
|
|
106
|
-
* 措辞与形式的调整才有依据(否则改文案就是猜)。
|
|
107
|
-
*
|
|
108
|
-
* 前缀而不是精确名:`memory_manager_*` 有 list/read/switch/save 等,任何一个
|
|
109
|
-
* 都说明模型确实在读这一域。改工具名等于换身份,这几个字符串是稳定契约。
|
|
110
|
-
*
|
|
111
|
-
* 为什么值是**数组**:工具族与注入域不是一一对应的概念 —— 域是"给模型看的信息分组"
|
|
112
|
-
* (界面上的勾选),族是"操作哪类对象"。0.14.0 里场景与记忆就一度同域两族
|
|
113
|
-
* (`memory_manager_*` + `scene_manager_*`),拆开后现在每个域各一个前缀。留着数组是
|
|
114
|
-
* 为了让"一个域挂多个族"在类型上成立,将来加族不必改类型。顺序不影响判定。
|
|
115
|
-
*/
|
|
116
|
-
export const DOMAIN_TOOL_PREFIX = {
|
|
117
|
-
scene: ['scene_manager_'],
|
|
118
|
-
memory: ['memory_manager_'],
|
|
119
|
-
mcp: ['mcp_manager_'],
|
|
120
|
-
skills: ['skill_manager_'],
|
|
121
|
-
subagents: ['subagent_manager_'],
|
|
122
|
-
prompt: ['prompt_manager_'],
|
|
123
|
-
};
|
|
124
|
-
/** 工具名 → 域(`undefined` = 不是本插件的域工具)。纯函数,便于单独推理。 */
|
|
125
|
-
export function domainOfTool(toolName) {
|
|
126
|
-
const name = String(toolName ?? '');
|
|
127
|
-
for (const key of INJECT_DOMAIN_KEYS) {
|
|
128
|
-
for (const prefix of DOMAIN_TOOL_PREFIX[key]) {
|
|
129
|
-
if (name.startsWith(prefix))
|
|
130
|
-
return key;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* 这个 agent 在委派链上的**深度**(0 = 顶层会话,1 = 子会话,2 = 孙会话…)。
|
|
137
|
-
*
|
|
138
|
-
* 为什么需要它:`agent/pre-step` 挂在宿主平面,天然覆盖所有 agent(含子智能体)——
|
|
139
|
-
* 这是当初选这条通道的代价。而人设目录该不该注入,取决于会话**有多深**(人设的
|
|
140
|
-
* `catalogDepth` 决定"目录注入到几层",默认 1 = 只在顶层),那是个关于深度的判断,
|
|
141
|
-
* 不是一个布尔的身份标签。
|
|
142
|
-
*
|
|
143
|
-
* 口径照抄官方 `delegationDepthOf()`(`dsh-subagent/lib/index.js:144`):持久化头
|
|
144
|
-
* `session.header.delegationDepth` 与运行期选项 `agent.options.subagentDepth` 取较大者
|
|
145
|
-
* —— 单看头会让"恢复的子会话"重新变成顶层(官方注释明说这是它要防的事)。
|
|
146
|
-
* `header.origin === 'subagent'` 是"是子会话"的硬信号,深度至少算 1(头里没记深度时
|
|
147
|
-
* 不能退回 0)。任一探针取不到或不是有限数都当它缺席:宁可把深度算浅一次(多注入一域),
|
|
148
|
-
* 也不要把正常会话误判成子会话而静默少一域。
|
|
149
|
-
*/
|
|
150
|
-
export function subagentDepthOf(agent) {
|
|
151
|
-
try {
|
|
152
|
-
const a = agent;
|
|
153
|
-
const header = a?.session?.header;
|
|
154
|
-
const finite = (value) => typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
155
|
-
const persisted = header !== undefined && header !== null ? finite(header.delegationDepth) : 0;
|
|
156
|
-
const runtime = finite(a?.options?.subagentDepth);
|
|
157
|
-
const floor = header !== undefined && header !== null && header.origin === 'subagent' ? 1 : 0;
|
|
158
|
-
return Math.max(floor, persisted, runtime);
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
return 0;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* 这个 agent 是不是**子会话**(子代理派生的会话)。深度判定的布尔投影。
|
|
166
|
-
*
|
|
167
|
-
* ⚠️ 生产代码现在只用 `subagentDepthOf` —— 判据是"目录该不该注入到这么深的会话",
|
|
168
|
-
* 需要的是**深度数字**,不是一个布尔身份(2026-09-17 方案 A:委派可行性由官方决定,
|
|
169
|
-
* 与"是不是子会话"无关)。这个投影留给真正需要布尔判断的场合,以及测试。
|
|
170
|
-
*/
|
|
171
|
-
export function isSubagentSession(agent) {
|
|
172
|
-
return subagentDepthOf(agent) >= 1;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* 「官方已经送得到就别再送」的域 → 对应预设事实的键。
|
|
176
|
-
*
|
|
177
|
-
* 这两个域的内容官方也有:提示词(AGENTS.md 正文;场景期间换成场景提示词)由
|
|
178
|
-
* `dsh-agent-instructions` 连着文件一起送,技能目录由 `dsh-tool-skill` 送。预设挂得到官方
|
|
179
|
-
* 那条行时本插件不重复注入(否则同一段正文进上下文两次);挂不到(极简两行都没挂)时才
|
|
180
|
-
* 由本插件兜底 —— 于是"极简也能选择是否注入"成立,而普通预设下不会出现两份。
|
|
181
|
-
*/
|
|
182
|
-
const CARRIER_FACT_OF = {
|
|
183
|
-
prompt: 'carriesAgentsMd',
|
|
184
|
-
skills: 'carriesSkillCatalog',
|
|
185
|
-
};
|
|
186
|
-
export const DEFAULT_INJECT_SETTINGS = {
|
|
187
|
-
underSuppressingPresets: false,
|
|
188
|
-
domains: { scene: true, memory: true, mcp: true, skills: true, subagents: true, prompt: true },
|
|
189
|
-
};
|
|
190
|
-
/** 把任意输入夹成合法设置(缺项/类型不对一律退回默认;默认从不阻止注入)。 */
|
|
191
|
-
export function normalizeInjectSettings(raw) {
|
|
192
|
-
const obj = (raw && typeof raw === 'object' ? raw : {});
|
|
193
|
-
const domainsRaw = (obj.domains && typeof obj.domains === 'object' ? obj.domains : {});
|
|
194
|
-
const domains = {};
|
|
195
|
-
for (const key of INJECT_DOMAIN_KEYS) {
|
|
196
|
-
domains[key] = typeof domainsRaw[key] === 'boolean' ? domainsRaw[key] : DEFAULT_INJECT_SETTINGS.domains[key];
|
|
197
|
-
}
|
|
198
|
-
return {
|
|
199
|
-
underSuppressingPresets: typeof obj.underSuppressingPresets === 'boolean'
|
|
200
|
-
? obj.underSuppressingPresets
|
|
201
|
-
: DEFAULT_INJECT_SETTINGS.underSuppressingPresets,
|
|
202
|
-
domains,
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* 纯函数:按设置 + 预设事实 + 目标 agent 挑出本次要注入的域。
|
|
207
|
-
*
|
|
208
|
-
* 规则(设计 2026-09-16;目录注入深度 2026-09-17):
|
|
209
|
-
* - 域开关关掉的、文本为空的,都不进;
|
|
210
|
-
* - **压制型预设**且没开「仍然注入」→ 一个都不进(跟随预设);
|
|
211
|
-
* - 域声明 `applicableTo` 说不成立的域不进(当前只有人设目录:会话深度超出了人设的
|
|
212
|
-
* `catalogDepth`,默认 1 = 只在顶层注入。**这不是"子会话不能委派"** —— 委派由官方
|
|
213
|
-
* 决定,见 service.ts 里 `catalogDepth` 的注释);
|
|
214
|
-
* - 有官方载体的两个域(提示词 / 技能目录):预设挂得到官方那条行时不进
|
|
215
|
-
* —— 官方自己会送,再注入一遍只会重复;预设事实读不到(`undefined`)时按"已承载"处理
|
|
216
|
-
* (宁可与现状一致,也不制造重复);
|
|
217
|
-
* - 同一段文本出现两次只保留前一个(跨域保险)。
|
|
218
|
-
*
|
|
219
|
-
* `onError`(2026-09-30 审查 P1-8):某个域的 `text()` **取数失败**时回调一次,并跳过该域。
|
|
220
|
-
* 为什么不能像原来那样静默吞成空串:调用方据此判定"该域这一轮没内容",而下游还有一条
|
|
221
|
-
* 「上一轮可见、这一轮没内容 → 发一条『已清空』」的通知 —— 于是一次取数失败会让模型收到
|
|
222
|
-
* **假的"已清空"**,据此认为那份内容已失效,下一轮内容恢复后又重发一次。
|
|
223
|
-
*
|
|
224
|
-
* ⚠️ 这条通道**只认抛异常**,所以域的 `text()` 实现必须把"读失败"与"真的空"分开:
|
|
225
|
-
* 返回空串一律被当成后者(2026-09-30 审查 F7 —— 修复第一版只覆盖了抛异常那一半,
|
|
226
|
-
* 而真实世界里 IO 失败走的是 `catch → return ''` 那条路)。`src/memories/service.ts`
|
|
227
|
-
* 的 `requireSceneMemory` / `sceneCatalog` / `readPresetTextSync` 已按此改。
|
|
228
|
-
*/
|
|
229
|
-
export function selectInjections(domains, settings, facts, agent, onError) {
|
|
230
|
-
const suppressing = facts !== undefined && facts.suppressing;
|
|
231
|
-
if (suppressing && !settings.underSuppressingPresets)
|
|
232
|
-
return [];
|
|
233
|
-
const out = [];
|
|
234
|
-
const seen = new Set();
|
|
235
|
-
for (const domain of domains) {
|
|
236
|
-
if (settings.domains[domain.key] !== true)
|
|
237
|
-
continue;
|
|
238
|
-
// 域自己不成立就不发(如人设目录的 catalogDepth 没覆盖到本会话的深度)。**这里没有开关,是刻意的**:
|
|
239
|
-
// 曾经有个 `suppressInSubagentSessions` 想让这一步可选,但它是个空开关 —— 域不成立时
|
|
240
|
-
// 正文自己也被过滤成空的(见 `subagents/catalog.ts` 的按深度过滤),两道闸条件相同,
|
|
241
|
-
// 开关只控制得了其中一道。留一个点了没反应的勾选框比没有更糟,所以删掉(2026-09-17)。
|
|
242
|
-
if (domain.applicableTo !== undefined && !domain.applicableTo(agent))
|
|
243
|
-
continue;
|
|
244
|
-
const carrier = CARRIER_FACT_OF[domain.key];
|
|
245
|
-
// 官方载体已挂(或读不到预设、只能按已挂算)→ 不重复送;只有明确"没挂"才兜底。
|
|
246
|
-
if (carrier !== undefined && facts?.[carrier] !== false)
|
|
247
|
-
continue;
|
|
248
|
-
let text = '';
|
|
249
|
-
try {
|
|
250
|
-
text = String(domain.text(agent) ?? '');
|
|
251
|
-
}
|
|
252
|
-
catch (error) {
|
|
253
|
-
// 取数失败 ≠ 没有内容:交给调用方(上报 + 这一轮不发"已清空"),本域这一轮不发。
|
|
254
|
-
try {
|
|
255
|
-
onError?.(domain.key, error);
|
|
256
|
-
}
|
|
257
|
-
catch { /* 上报本身绝不能影响注入 */ }
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
if (text.trim() === '')
|
|
261
|
-
continue;
|
|
262
|
-
const fingerprint = text.trim();
|
|
263
|
-
if (seen.has(fingerprint))
|
|
264
|
-
continue;
|
|
265
|
-
seen.add(fingerprint);
|
|
266
|
-
out.push({ key: domain.key, name: domain.name, label: domain.label, form: domain.form, text });
|
|
267
|
-
}
|
|
268
|
-
return out;
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* 纯函数:算出某一步每个域的原因(`selectInjections` 的镜像,供「注入实况」解释状态)。
|
|
272
|
-
*
|
|
273
|
-
* 与 `selectInjections` 分开写是为了让两边各自可读:一个是"发什么",一个是"为什么"。
|
|
274
|
-
* 口径必须一致 —— 否则界面会把"子会话抑制"报成"未投递",而界面的既有口径是
|
|
275
|
-
* 「琥珀只留给'该投却没投'」(把用户自己的选择读成故障,正是这条口径要避免的事)。
|
|
276
|
-
*
|
|
277
|
-
* ⚠️ 这里**故意不镜像** `selectInjections` 开头那条"压制型预设一个都不进"的提前返回:
|
|
278
|
-
* 该分支下本函数仍按各域自身的原因作答,于是压制型预设 + 没开「仍然注入」时,
|
|
279
|
-
* 界面看到的是"未投递"(琥珀)而不是"已关闭"(灰)。这是 0.9.1 的既有行为,本次未改,
|
|
280
|
-
* 改动它属于另一个话题("跟随预设"到底该报成故障还是报成选择)。
|
|
281
|
-
*/
|
|
282
|
-
export function explainInjections(domains, settings, facts, agent, failed) {
|
|
283
|
-
const reasons = {};
|
|
284
|
-
for (const domain of domains) {
|
|
285
|
-
if (settings.domains[domain.key] !== true) {
|
|
286
|
-
reasons[domain.key] = 'off';
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
if (domain.applicableTo !== undefined && !domain.applicableTo(agent)) {
|
|
290
|
-
reasons[domain.key] = 'child';
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
const carrier = CARRIER_FACT_OF[domain.key];
|
|
294
|
-
if (carrier !== undefined && facts?.[carrier] !== false) {
|
|
295
|
-
reasons[domain.key] = 'official';
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
// 取数失败过 → 报 `error` 而不是 `empty`:两者在界面上的含义完全不同(一个是故障、
|
|
299
|
-
// 一个是"本来就没内容"),而原来这条路上永远只能看到后者(审查 P1-8)。`failed` 由调用方
|
|
300
|
-
// 从 `selectInjections` 的 `onError` 收集;**只认抛异常**,返回空串仍算 `empty`(F7)。
|
|
301
|
-
if (failed?.has(domain.key)) {
|
|
302
|
-
reasons[domain.key] = 'error';
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
let text = '';
|
|
306
|
-
try {
|
|
307
|
-
text = String(domain.text(agent) ?? '');
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
text = '';
|
|
311
|
-
}
|
|
312
|
-
reasons[domain.key] = text.trim() === '' ? 'empty' : 'sent';
|
|
313
|
-
}
|
|
314
|
-
return reasons;
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* 权威声明的统一句(用户 2026-09-23 看到实际注入后要求精简)。
|
|
318
|
-
*
|
|
319
|
-
* 此前五个域各写一份 —— `本份场景取代…同类场景` / `本份记忆取代…同类记忆` /
|
|
320
|
-
* `本份状态…` / `本份目录…` —— 说的是**同一条规则**却用了四种措辞,模型读到四条不同的句子
|
|
321
|
-
* 还得自己判断它们是不是一条。统一成一句:被取代的是"同类内容",与域无关。
|
|
322
|
-
*
|
|
323
|
-
* 为什么不能并进 `cue`(那能省下整整一行 ≈16 tok/段):用户 2026-09-18 定过"权威声明单独
|
|
324
|
-
* 成句" —— 它和动作句是两种东西(一句说"什么时候用它",一句说"以哪份为准"),合并后容易
|
|
325
|
-
* 被一眼带过。所以这里的收益只有约 6 tok/轮,**主要收益是消除四种措辞**,不是省字节。
|
|
326
|
-
*/
|
|
327
|
-
const SUPERSEDE_NOTE = '本份取代本次会话中更早注入的同类内容。';
|
|
328
|
-
const DOMAIN_FRAME = {
|
|
329
|
-
scene: {
|
|
330
|
-
// 场景段只有标题 + 正文 + 权威声明(**没有 cue 是六个域的共同决定**,理由见 `DomainFrame`)。
|
|
331
|
-
//
|
|
332
|
-
// 这一段的演进值得记下来,因为每一次都是被实际注入推着改的:
|
|
333
|
-
// ① 最早它把「场景说明」当**约定**授权("一律照办,覆盖你的默认做法")—— 而它的实例
|
|
334
|
-
// 是「写代码」这种**标签**,让模型"照办一个标签",这正是它读不懂这段的原因;
|
|
335
|
-
// ② 去掉授权后换成一句定义("场景是用户给这台机器配的工作模式")—— 定义不是动作,
|
|
336
|
-
// 模型读完还是不知道该拿它做什么;
|
|
337
|
-
// ③ 再加一句因果("下面四段都已按它筛过")—— 用户 2026-09-23 看到渲染效果后给了
|
|
338
|
-
// **原则**:「上下文注入就是当前的情况,目的是让 agent 知道现在的情况,不需要它
|
|
339
|
-
// 知道没用的信息,反推更是浪费 token」。于是三句全删。
|
|
340
|
-
//
|
|
341
|
-
// 现在这一段只回答一个问题:**当前处在哪个场景、它是什么**(`**「代码」—— 写代码**`)。
|
|
342
|
-
// 它还比别的段少一层:因果句也删了 —— 它解释的是"另外四段是怎么产生的"(机制),
|
|
343
|
-
// 不是当前情况本身,而且"清单里没有 ≠ 本机没有"这层反推被用户明确判为浪费。
|
|
344
|
-
title: '本机当前的场景',
|
|
345
|
-
supersede: SUPERSEDE_NOTE,
|
|
346
|
-
},
|
|
347
|
-
memory: {
|
|
348
|
-
title: '本机当前的记忆',
|
|
349
|
-
supersede: SUPERSEDE_NOTE,
|
|
350
|
-
},
|
|
351
|
-
mcp: {
|
|
352
|
-
title: '本机 MCP 服务器的当前状态',
|
|
353
|
-
// how 只剩"怎么用备注"这半句:前半个分句「工具名是 `mcp__<服务器>__<工具>`」删掉了
|
|
354
|
-
// —— 模型自己的工具表里就是这个命名(`mcp__context7__xxx`),告诉它格式是零信息量。
|
|
355
|
-
how: '带「用户提示:」的行是用户写给这台服务器的决策提示,选服务器之前先看一眼。',
|
|
356
|
-
supersede: SUPERSEDE_NOTE,
|
|
357
|
-
},
|
|
358
|
-
skills: {
|
|
359
|
-
title: '本机技能目录',
|
|
360
|
-
// 两句都只在预设没挂官方 `skill` 工具时出现,差别只在点名不点名那个取正文的工具
|
|
361
|
-
// (工具被用户在兼容页关掉时不点名 —— 点名一个模型手里没有的工具只会让它去猜名字)。
|
|
362
|
-
how: (toolHidden) => toolHidden('skill_manager_read')
|
|
363
|
-
? '本预设没有官方 `skill` 工具:目录只有摘要,读完再照做。'
|
|
364
|
-
: '本预设没有官方 `skill` 工具:要正文用 `skill_manager_read`(按名字直接给正文与路径);目录只有摘要,读完再照做。',
|
|
365
|
-
supersede: `${SUPERSEDE_NOTE.slice(0, -1)};只列当前可调用的技能。`,
|
|
366
|
-
},
|
|
367
|
-
subagents: {
|
|
368
|
-
title: '可委派的子智能体',
|
|
369
|
-
// 分界规则(2026-09-17 方案 C,本机实测 session-ee722e23 逼出来的):官方那两个
|
|
370
|
-
// 委派工具(`subagent` / `subagent_fork`)不带人设,而此前没有任何一句话说明何时该
|
|
371
|
-
// 用谁 —— 模型在"审查刚读过的 README"时选了 `subagent_fork`(fork 能继承已读内容、
|
|
372
|
-
// 省一次复述)。现在人设通道也有 `inherit`(同一套 fork 机制),分界只剩"要不要后台跑"。
|
|
373
|
-
// 带人设的委派工具被关掉时改说"本会话没有这条通道":不说的话,模型看到有人设目录却
|
|
374
|
-
// 找不到对应的工具,会去拿官方那两个凑(它们不接受人设,等于白跑一趟)。
|
|
375
|
-
how: (toolHidden) => toolHidden('subagent_manager_run')
|
|
376
|
-
? '本会话没有带人设的委派工具;官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。'
|
|
377
|
-
: '贴合人设的任务一律用 `subagent_manager_run`(要它看到本次会话就开 `inherit`);官方 `subagent` / `subagent_fork` 不带人设,只在没有人设贴合、或要后台跑时用。',
|
|
378
|
-
supersede: SUPERSEDE_NOTE,
|
|
379
|
-
},
|
|
380
|
-
prompt: {
|
|
381
|
-
title: '本机提示词',
|
|
382
|
-
supersede: SUPERSEDE_NOTE,
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
/** 域声明里没登记的 key(理论上到不了这里):给一个不出错的通用框架。 */
|
|
386
|
-
const fallbackFrame = (label) => ({
|
|
387
|
-
title: `本机的${label}`,
|
|
388
|
-
supersede: SUPERSEDE_NOTE,
|
|
389
|
-
});
|
|
390
|
-
const domainFrame = (key, label) => DOMAIN_FRAME[key] ?? fallbackFrame(label);
|
|
391
|
-
/**
|
|
392
|
-
* 框架的伪 XML 标记。与官方两条注入行同款:`dsh-tool-skill` 与 `dsh-agent-instructions`
|
|
393
|
-
* 都把**整条正文**包在里面(不是只包引导语)。Claude Code 那边把这对标记叫"可依赖的
|
|
394
|
-
* 判别符"(`messages.ts:1797` 的 `ensureSystemReminderWrap` 保证任何注入文本都不落在外面):
|
|
395
|
-
* 模型据此把这段读成系统给的上下文,而不是用户刚打的字 —— 本插件的正文里混着用户写的
|
|
396
|
-
* 自由文本(AGENTS.md / 记忆 / 备注),这层来源标记尤其不能少。
|
|
397
|
-
*/
|
|
398
|
-
const FRAME_OPEN = '<system-reminder>';
|
|
399
|
-
const FRAME_CLOSE = '</system-reminder>';
|
|
400
|
-
/**
|
|
401
|
-
* 正文里的 `</system-reminder>` 拆掉闭合形态(写成 `<\/system-reminder>`)。
|
|
402
|
-
*
|
|
403
|
-
* 正文含用户自由文本(AGENTS.md、记忆、MCP 备注、技能描述),一句手写的闭合标记就能让框架
|
|
404
|
-
* 提前结束,其后的内容读起来像用户当场说的话。照抄官方 `escapeInstructionFrameBody` 的实现
|
|
405
|
-
* (`dsh-agent-instructions/lib/index.js:128`,同样是替换成带反斜杠的形态 —— 模型看到的
|
|
406
|
-
* 字面量不变,标记不再闭合)。
|
|
407
|
-
*/
|
|
408
|
-
export function escapeFrameBody(body) {
|
|
409
|
-
return body.replaceAll(FRAME_CLOSE, '<\\/system-reminder>');
|
|
410
|
-
}
|
|
411
|
-
/**
|
|
412
|
-
* 一条注入消息的正文(纯函数):`<system-reminder>` 里 = 框架(标题 + 动作 + 补充 + 权威
|
|
413
|
-
* 声明)+ 空行 + 域正文。**所有域一律带框架**,没有例外。
|
|
414
|
-
*
|
|
415
|
-
* 历史(每一版都是被具体毛病逼出来的,别把结论当套话读):
|
|
416
|
-
* - 第一版(2026-09-17 前):五域共用「以下是本机插件的X(取代…)。」——「本机插件」是实现
|
|
417
|
-
* 细节;通篇没有动作;五条消息句式一模一样,雷同的套话退化成背景噪声。
|
|
418
|
-
* - 第二版(2026-09-17):动作前置(`**{线索}**:以下是{域}(取代…)。{补充}`),但只改了
|
|
419
|
-
* 用户圈定的两域(记忆 / 子智能体),MCP、技能、提示词继续走老模板。
|
|
420
|
-
* - 第三版(2026-09-17):instructions 形态不再裸送 —— 此前"原样送"的理由是"对齐官方
|
|
421
|
-
* AGENTS.md 那条行",与官方实际行为不符(官方那条是包 `<system-reminder>` 的,含一句
|
|
422
|
-
* "这些工作区指令可作参考……"。真正的问题在极简类预设:官方通道被压掉后本插件是唯一
|
|
423
|
-
* 承载者,裸文本没有任何标记,而提示词会因场景切换而变化、新旧两份效力相同、没有判据。
|
|
424
|
-
* 只借官方**包框架**的形式,**不抄**它那句把用户规则降成"仅供参考"的措辞。
|
|
425
|
-
* - 第四版(2026-09-18,本条):**动作仍然不够显眼**(用户:「还是不能很好提醒模型去主动
|
|
426
|
-
* 使用」),且整条消息该按 md 排版突出关键信息。改成四级结构:`##` 标题当标签、
|
|
427
|
-
* 加粗动作句单独一行、补充动作给工具名、权威声明单独成句(逐条理由见 `DOMAIN_FRAME`)。
|
|
428
|
-
* 同时把**正文也包进标记里**(此前只有引导语在标记内、正文裸奔)—— 官方两条注入行都是
|
|
429
|
-
* 整条包住的,正文里的用户自由文本更需要这层来源标记。
|
|
430
|
-
*/
|
|
431
|
-
export function renderDomainText(section, toolHidden = () => false) {
|
|
432
|
-
const frame = domainFrame(section.key, section.label);
|
|
433
|
-
// 层级(2026-09-23 用户看到实际注入后指出「记忆内的场景怎么都是 ## 标题」):**`#` 一级给板块**
|
|
434
|
-
// (场景 / 记忆 / MCP / 技能 / 子智能体 / 提示词),域正文里的 `##` 才是它的下一层
|
|
435
|
-
// (记忆段的 `## 场景:X`、超预算时的 `## 未注入的参考信息`)。此前标题也是 `##`,两者平级,
|
|
436
|
-
// 模型读不出主次 —— 而"哪些内容归在哪个板块/场景下"正是它做判断时要用的结构。
|
|
437
|
-
//
|
|
438
|
-
// 开标签后**必须空一行**:markdown 里 `#` 紧跟在一行文字后面只是**段落续行**,不会被渲染成
|
|
439
|
-
// 标题 —— 用户截图里 `## 本机当前的场景` 就是这么被吞掉的(和 `<system-reminder>` 挤成一段)。
|
|
440
|
-
// 收尾同理:正文末尾先归一成单个空行,免得 `</system-reminder>` 粘在最后一行上。
|
|
441
|
-
// 结构:标题 → 补充说明(有才发)→ 权威声明 → 正文。没有 cue 那一行(见 `DomainFrame` 的注释)。
|
|
442
|
-
const lines = [FRAME_OPEN, '', `# ${frame.title}`];
|
|
443
|
-
const how = typeof frame.how === 'function' ? frame.how(toolHidden) : frame.how;
|
|
444
|
-
if (how !== undefined)
|
|
445
|
-
lines.push(how);
|
|
446
|
-
lines.push(frame.supersede, '', escapeFrameBody(section.text).replace(/\n+$/, ''), '', FRAME_CLOSE);
|
|
447
|
-
return lines.join('\n');
|
|
448
|
-
}
|
|
449
|
-
/** 「已清空」通知正文:某个域曾经注入过、现在没有内容时发一条(纯函数,测试用)。 */
|
|
450
|
-
export function clearedDomainText(key, label) {
|
|
451
|
-
const frame = domainFrame(key, label);
|
|
452
|
-
// 形状与 `renderDomainText` 一致(开标签后空行、板块 `#`、收尾空行)—— 这两条都是同一个
|
|
453
|
-
// 通道发出去的消息,层级与留白不该有两套。
|
|
454
|
-
return [
|
|
455
|
-
FRAME_OPEN,
|
|
456
|
-
'',
|
|
457
|
-
`# ${frame.title}`,
|
|
458
|
-
'**已清空** —— 本次会话中此前注入的同类内容不再有效。',
|
|
459
|
-
'',
|
|
460
|
-
FRAME_CLOSE,
|
|
461
|
-
].join('\n');
|
|
462
|
-
}
|
|
463
|
-
/** 取一条消息的正文(只认单 text 块;形如官方 RuntimeContextProjection.textOf)。 */
|
|
464
|
-
function textOf(message) {
|
|
465
|
-
const content = message?.content;
|
|
466
|
-
if (!Array.isArray(content) || content.length !== 1)
|
|
467
|
-
return undefined;
|
|
468
|
-
const block = content[0];
|
|
469
|
-
return block && block.type === 'text' && typeof block.text === 'string' ? block.text : undefined;
|
|
470
|
-
}
|
|
471
|
-
/** 本插件自己的五个注入 kind(去重比对只用这一份 —— 绝不能让官方正文影响"发不发"的判断)。 */
|
|
472
|
-
const PLUGIN_KINDS = new Map(INJECT_DOMAIN_KEYS.map((key) => [INJECT_KIND_OF[key], { key, official: false }]));
|
|
473
|
-
/**
|
|
474
|
-
* 官方载体消息的 kind → 本插件域:技能目录(`@deepseek-ai/dsh-tool-skill`)与
|
|
475
|
-
* AGENTS.md(`@deepseek-ai/dsh-agent-instructions`)。仅用于「注入实况」展示 ——
|
|
476
|
-
* 标准类预设下这两个域由官方在送,它同样是"模型看到的内容",体积要算官方那份。
|
|
477
|
-
* 字符串取自官方包,是宿主侧的稳定契约。
|
|
478
|
-
*/
|
|
479
|
-
const OFFICIAL_KIND_OF = {
|
|
480
|
-
'skill-catalog': 'skills',
|
|
481
|
-
'agent-instructions': 'prompt',
|
|
482
|
-
};
|
|
483
|
-
/**
|
|
484
|
-
* 用户**明确关掉**的域 → 该域对应的官方消息 kind(这些 kind 的消息这一步不放行)。
|
|
485
|
-
*
|
|
486
|
-
* 交互事实(用户 2026-09-17 指出):技能与提示词两域在标准类预设下由官方送(本插件让位),
|
|
487
|
-
* 于是"取消勾选"只停掉了本插件自己,官方那条照样进上下文 —— 用户看到的是"关了没用"。
|
|
488
|
-
* 本函数给出需要**连带拦下**的官方 kind:只有"官方自己会送同份内容"的两个域有这一项;
|
|
489
|
-
* MCP / 记忆 / 人设目录官方不送,没有可拦的对象(它们的开关本来就完全生效)。
|
|
490
|
-
*/
|
|
491
|
-
export function officialKindsToSuppress(settings) {
|
|
492
|
-
const out = new Set();
|
|
493
|
-
for (const [kind, key] of Object.entries(OFFICIAL_KIND_OF)) {
|
|
494
|
-
if (settings.domains[key] === false)
|
|
495
|
-
out.add(kind);
|
|
496
|
-
}
|
|
497
|
-
return out;
|
|
498
|
-
}
|
|
499
|
-
/** 一条消息的来源 kind(读不到 = `undefined`)。 */
|
|
500
|
-
function kindOfMessage(message) {
|
|
501
|
-
const source = message?.source;
|
|
502
|
-
return source && typeof source.kind === 'string' ? source.kind : undefined;
|
|
503
|
-
}
|
|
75
|
+
import { domainOfTool, INJECT_DOMAIN_KEYS, INJECT_KIND_OF, } from './context-inject-contract.js';
|
|
76
|
+
import { explainInjections, selectInjections, } from './context-inject-select.js';
|
|
77
|
+
import { clearedDomainText, renderDomainText } from './context-inject-frame.js';
|
|
78
|
+
import { kindOfMessage, LIVE_KINDS, newestDomainTexts, officialKindsToSuppress, PLUGIN_KINDS, } from './context-inject-surface.js';
|
|
79
|
+
// 拆出的四层照旧从 context-inject.js 对外可见(index.ts、ops/compat.ts、tools/table.ts、
|
|
80
|
+
// subagents/{catalog,service}.ts 与契约测试都按这个路径引)。
|
|
81
|
+
export { DEFAULT_INJECT_SETTINGS, DOMAIN_TOOL_PREFIX, INJECT_DOMAIN_KEYS, INJECT_KIND_OF, domainOfTool, isSubagentSession, subagentDepthOf, } from './context-inject-contract.js';
|
|
82
|
+
export { explainInjections, normalizeInjectSettings, selectInjections, } from './context-inject-select.js';
|
|
83
|
+
export { clearedDomainText, escapeFrameBody, renderDomainText } from './context-inject-frame.js';
|
|
84
|
+
export { officialKindsToSuppress } from './context-inject-surface.js';
|
|
504
85
|
// B4:关域拦截的**顺序自检**。官方那两条注入行与本插件同挂在这条瀑布上,拦截成立的剩余前提是
|
|
505
86
|
// "本插件排在它们上游"(本插件用 `{ prepend: true }` 把自己钉在钩子表最前,所以真正还依赖的
|
|
506
87
|
// 只剩一条:官方那两条仍用**默认 push** 方式注册)。这仍是非契约事实(见 pre-step 监听器里的
|
|
@@ -535,55 +116,6 @@ function suppressionVerdict(active, dropped, counter) {
|
|
|
535
116
|
params: { turns: counter.turns },
|
|
536
117
|
});
|
|
537
118
|
}
|
|
538
|
-
/** 实况展示用的 kind 表:本插件的五条 + 官方两条。 */
|
|
539
|
-
const LIVE_KINDS = new Map([
|
|
540
|
-
...PLUGIN_KINDS,
|
|
541
|
-
...Object.entries(OFFICIAL_KIND_OF).map(([kind, key]) => [kind, { key, official: true }]),
|
|
542
|
-
]);
|
|
543
|
-
function newestDomainTexts(agent, kinds) {
|
|
544
|
-
const out = new Map();
|
|
545
|
-
const session = agent?.session;
|
|
546
|
-
if (!session || typeof session.seq !== 'number' || typeof session.eventAt !== 'function')
|
|
547
|
-
return out;
|
|
548
|
-
const scan = (seq) => {
|
|
549
|
-
let event;
|
|
550
|
-
try {
|
|
551
|
-
event = session.eventAt(SessionSeq(seq));
|
|
552
|
-
}
|
|
553
|
-
catch {
|
|
554
|
-
return false;
|
|
555
|
-
}
|
|
556
|
-
if (!event || event.type !== 'user/message')
|
|
557
|
-
return false;
|
|
558
|
-
const source = event.data && event.data.source;
|
|
559
|
-
const mapping = source && typeof source.kind === 'string' ? kinds.get(source.kind) : undefined;
|
|
560
|
-
if (mapping === undefined || out.has(mapping.key))
|
|
561
|
-
return false;
|
|
562
|
-
const text = textOf(event.data);
|
|
563
|
-
if (text === undefined)
|
|
564
|
-
return false;
|
|
565
|
-
out.set(mapping.key, { text, form: source && typeof source.form === 'string' ? source.form : '', official: mapping.official });
|
|
566
|
-
return out.size === INJECT_DOMAIN_KEYS.length;
|
|
567
|
-
};
|
|
568
|
-
const nodes = session.surface && Array.isArray(session.surface.nodes) ? session.surface.nodes : undefined;
|
|
569
|
-
if (nodes !== undefined) {
|
|
570
|
-
const seqs = [];
|
|
571
|
-
for (const node of nodes) {
|
|
572
|
-
const n = Number(node);
|
|
573
|
-
if (Number.isFinite(n))
|
|
574
|
-
seqs.push(n);
|
|
575
|
-
}
|
|
576
|
-
seqs.sort((a, b) => b - a); // 从新到旧(表面自身的顺序不保证)
|
|
577
|
-
for (const seq of seqs)
|
|
578
|
-
if (scan(seq))
|
|
579
|
-
break;
|
|
580
|
-
return out;
|
|
581
|
-
}
|
|
582
|
-
for (let seq = session.seq - 1; seq >= 0; seq -= 1)
|
|
583
|
-
if (scan(seq))
|
|
584
|
-
break;
|
|
585
|
-
return out;
|
|
586
|
-
}
|
|
587
119
|
const bytesOf = (text) => {
|
|
588
120
|
try {
|
|
589
121
|
return Buffer.byteLength(text, 'utf8');
|