dsh-plugin-prompt-tool 0.1.4 → 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 +75 -171
- package/lib/client.js +118 -11
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +16 -2
- package/lib/index.mjs +399 -101
- package/lib/preset-core.d.mts +12 -1
- package/lib/preset-core.mjs +94 -21
- package/package.json +5 -6
- package/plan.md +292 -265
- package/preset/near-anchor.mjs +117 -0
- package/preset/router-first-turn.mjs +59 -0
- package/{vendor → upstream}/dsh-anchored-standard/NOTICE +5 -0
- package/upstream/dsh-anchored-standard/REVISION +1 -0
- package/{vendor → upstream}/dsh-anchored-standard/preset/agent.cordis.yml +53 -24
- package/upstream/dsh-anchored-standard/preset/context-gate.mjs +202 -0
- package/{vendor → upstream}/dsh-anchored-standard/preset/dev-tool-search.mjs +10 -8
- package/{vendor → upstream}/dsh-anchored-standard/preset/instruction-hint.mjs +66 -13
- package/{vendor → upstream}/dsh-anchored-standard/preset/tool-bootstrap.mjs +41 -78
- package/preset/turn-anchor.mjs +0 -83
- /package/{vendor → upstream}/dsh-anchored-standard/LICENSE +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/custom-bash.mjs +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/preset.yml +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/skill-search.mjs +0 -0
package/lib/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildCordis, parseFrontmatter } from "./preset-core.mjs";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
4
|
-
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
@@ -10,7 +10,9 @@ const name = "prompt-tool";
|
|
|
10
10
|
const inject = [
|
|
11
11
|
"skills",
|
|
12
12
|
"webServer",
|
|
13
|
-
"commands"
|
|
13
|
+
"commands",
|
|
14
|
+
"llm",
|
|
15
|
+
"subagents"
|
|
14
16
|
];
|
|
15
17
|
const PRESET_FILE_URL = new URL("../preset.md", import.meta.url);
|
|
16
18
|
const PRESET_FILE_PATH = fileURLToPath(PRESET_FILE_URL);
|
|
@@ -26,8 +28,9 @@ const DEFAULT_PRESET_ORDER = 5;
|
|
|
26
28
|
const DEFAULT_SKILL_RANK_BASE = 250;
|
|
27
29
|
const PRESET_TEMPLATE_META = fileURLToPath(new URL("../preset/preset.yml", import.meta.url));
|
|
28
30
|
const PRESET_TEMPLATE_INJECTOR = fileURLToPath(new URL("../preset/prompt-injector.mjs", import.meta.url));
|
|
29
|
-
const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/
|
|
30
|
-
const
|
|
31
|
+
const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/near-anchor.mjs", import.meta.url));
|
|
32
|
+
const PRESET_TEMPLATE_ROUTER = fileURLToPath(new URL("../preset/router-first-turn.mjs", import.meta.url));
|
|
33
|
+
const VENDOR_PRESET_DIR = fileURLToPath(new URL("../upstream/dsh-anchored-standard/preset", import.meta.url));
|
|
31
34
|
const Config = z.object({
|
|
32
35
|
text: z.string().default(""),
|
|
33
36
|
agentsText: z.string().default(""),
|
|
@@ -37,7 +40,12 @@ const Config = z.object({
|
|
|
37
40
|
injectPrompt: z.boolean().default(true),
|
|
38
41
|
skillSwitches: z.dict(z.boolean()).default({}),
|
|
39
42
|
anchorFirstTurn: z.boolean().default(false),
|
|
40
|
-
anchorText: z.string().default("
|
|
43
|
+
anchorText: z.string().default(""),
|
|
44
|
+
anchorCustom: z.boolean().default(false),
|
|
45
|
+
subagentFlash: z.boolean().default(false),
|
|
46
|
+
subagentFlashProvider: z.string().default("deepseek-official"),
|
|
47
|
+
subagentFlashModel: z.string().default("deepseek-v4-flash"),
|
|
48
|
+
customBashPath: z.string().default("bash.exe"),
|
|
41
49
|
skillsDir: z.string().default(DEFAULT_SKILLS_DIR),
|
|
42
50
|
skillRankBase: z.natural().default(DEFAULT_SKILL_RANK_BASE),
|
|
43
51
|
residentAgentsPath: z.string().default(DEFAULT_RESIDENT_AGENTS_PATH),
|
|
@@ -52,7 +60,10 @@ const PromptSettingsSchema = z.object({
|
|
|
52
60
|
agentsPath: z.string().default(""),
|
|
53
61
|
injectAgentsPrompt: z.boolean().default(false),
|
|
54
62
|
anchorFirstTurn: z.boolean().default(false),
|
|
55
|
-
anchorText: z.string().default("
|
|
63
|
+
anchorText: z.string().default(""),
|
|
64
|
+
anchorCustom: z.boolean().default(false),
|
|
65
|
+
subagentFlash: z.boolean().default(false),
|
|
66
|
+
deepseekAvailable: z.boolean().default(true),
|
|
56
67
|
injectPrompt: z.boolean().default(true),
|
|
57
68
|
skillSwitches: z.dict(z.boolean()).default({}),
|
|
58
69
|
skillCatalog: z.array(z.object({
|
|
@@ -104,10 +115,101 @@ function readAgents() {
|
|
|
104
115
|
return "";
|
|
105
116
|
}
|
|
106
117
|
}
|
|
118
|
+
/** 直接读取项目根目录的 preset.md 与 AGENTS.md;宿主不再回写这两个文件。 */
|
|
119
|
+
function readProjectOriginals() {
|
|
120
|
+
return {
|
|
121
|
+
presetText: readPromptFile(""),
|
|
122
|
+
agentsText: readAgents()
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/** 首次安装时,把项目文件内容作为 user 层种子写入 settings.yaml;已有字段不覆盖。 */
|
|
126
|
+
function seedSettingsOnce(ctx, originals) {
|
|
127
|
+
ctx.inject(["settings"], (sctx) => {
|
|
128
|
+
(async () => {
|
|
129
|
+
try {
|
|
130
|
+
const descriptor = sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
|
|
131
|
+
if (descriptor === void 0) return;
|
|
132
|
+
const user = descriptor.user !== null && typeof descriptor.user === "object" ? descriptor.user : {};
|
|
133
|
+
const ops = [];
|
|
134
|
+
if (typeof user.promptText !== "string") ops.push({
|
|
135
|
+
op: "set",
|
|
136
|
+
path: ["promptText"],
|
|
137
|
+
value: originals.presetText
|
|
138
|
+
});
|
|
139
|
+
if (typeof user.agentsText !== "string") ops.push({
|
|
140
|
+
op: "set",
|
|
141
|
+
path: ["agentsText"],
|
|
142
|
+
value: originals.agentsText
|
|
143
|
+
});
|
|
144
|
+
if (ops.length === 0) return;
|
|
145
|
+
await sctx.settings.mutate(NS, ops);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
warn(ctx, `prompt-tool: failed to seed settings from project files: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
}
|
|
149
|
+
})();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const RESIDENT_AGENTS_BEGIN = "# === prompt-tool managed block begin ===";
|
|
153
|
+
const RESIDENT_AGENTS_END = "# === prompt-tool managed block end ===";
|
|
154
|
+
/** 从正文中删除成对的受管标记块;标记不成对时保持原样,避免误删。 */
|
|
155
|
+
function stripManagedBlock(source) {
|
|
156
|
+
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
157
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
158
|
+
const start = lines.findIndex((line) => line.trim() === RESIDENT_AGENTS_BEGIN);
|
|
159
|
+
if (start < 0) return {
|
|
160
|
+
body: source,
|
|
161
|
+
found: false
|
|
162
|
+
};
|
|
163
|
+
const end = lines.findIndex((line, index) => index > start && line.trim() === RESIDENT_AGENTS_END);
|
|
164
|
+
if (end < 0) return {
|
|
165
|
+
body: source,
|
|
166
|
+
found: false
|
|
167
|
+
};
|
|
168
|
+
lines.splice(start, end - start + 1);
|
|
169
|
+
return {
|
|
170
|
+
body: lines.join(eol),
|
|
171
|
+
found: true
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** 生成要放到文件头部的受管块。 */
|
|
175
|
+
function buildManagedBlock(text, eol) {
|
|
176
|
+
const content = text.replace(/\r\n/g, "\n").trim();
|
|
177
|
+
return [
|
|
178
|
+
RESIDENT_AGENTS_BEGIN,
|
|
179
|
+
content,
|
|
180
|
+
RESIDENT_AGENTS_END
|
|
181
|
+
].join(eol);
|
|
182
|
+
}
|
|
183
|
+
/** 把 AGENTS.md 内容作为受管块写到目标文件头部,保留文件其余内容。 */
|
|
107
184
|
function writeAgents(text, targetPath) {
|
|
108
185
|
try {
|
|
109
186
|
mkdirSync(join(targetPath, ".."), { recursive: true });
|
|
110
|
-
|
|
187
|
+
const existing = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : "";
|
|
188
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
189
|
+
const stripped = stripManagedBlock(existing);
|
|
190
|
+
const content = text.trim();
|
|
191
|
+
if (content.length === 0) {
|
|
192
|
+
if (!stripped.found) return true;
|
|
193
|
+
writeFileSync(targetPath, stripped.body, "utf8");
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
const rest = stripped.body.replace(/^[\r\n]+/, "");
|
|
197
|
+
const managed = buildManagedBlock(content, eol);
|
|
198
|
+
const next = rest.length > 0 ? managed + eol + rest : managed + eol;
|
|
199
|
+
if (next === existing) return true;
|
|
200
|
+
writeFileSync(targetPath, next, "utf8");
|
|
201
|
+
return true;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** 关闭写入开关后,从目标文件删除本插件的受管块。 */
|
|
207
|
+
function removeResidentAgentsBlock(targetPath) {
|
|
208
|
+
try {
|
|
209
|
+
if (!existsSync(targetPath)) return true;
|
|
210
|
+
const stripped = stripManagedBlock(readFileSync(targetPath, "utf8"));
|
|
211
|
+
if (!stripped.found) return true;
|
|
212
|
+
writeFileSync(targetPath, stripped.body, "utf8");
|
|
111
213
|
return true;
|
|
112
214
|
} catch {
|
|
113
215
|
return false;
|
|
@@ -180,10 +282,12 @@ async function readBridgeBody(req) {
|
|
|
180
282
|
/** dsh-tui 暴露的布尔开关:键名与 settings 路径一致。 */
|
|
181
283
|
const TUI_BOOLEAN_SWITCHES = [
|
|
182
284
|
["writeAgents", "写入常驻规则 AGENTS.md"],
|
|
183
|
-
["writePreset", "
|
|
285
|
+
["writePreset", "启用锚定预设"],
|
|
184
286
|
["injectPrompt", "锚定确认后注入 preset.md"],
|
|
185
287
|
["injectAgentsPrompt", "用 AGENTS.md 替换 instruction-hint 提示"],
|
|
186
|
-
["anchorFirstTurn", "
|
|
288
|
+
["anchorFirstTurn", "追加任务引导"],
|
|
289
|
+
["anchorCustom", "使用自定义引导"],
|
|
290
|
+
["subagentFlash", "子代理固定 Flash 模型"]
|
|
187
291
|
];
|
|
188
292
|
/** 把布尔开关渲染成 dsh-tui 命令输出。 */
|
|
189
293
|
function renderTuiStatus(source) {
|
|
@@ -194,6 +298,9 @@ function renderTuiStatus(source) {
|
|
|
194
298
|
const value = source[key];
|
|
195
299
|
return `${key.padEnd(22)}${onOff(typeof value === "boolean" ? value : false)} ${label}`;
|
|
196
300
|
}),
|
|
301
|
+
"锚点文本:",
|
|
302
|
+
` anchorText ${source.anchorText.length > 0 ? source.anchorText : "(空 = 按任务自动选择)"}`,
|
|
303
|
+
` deepseekAvailable ${source.deepseekAvailable ? "是" : "否(未检测到 DeepSeek 模型,subagentFlash 不可用)"}`,
|
|
197
304
|
"技能开关:"
|
|
198
305
|
];
|
|
199
306
|
for (const skill of source.skillCatalog) {
|
|
@@ -209,7 +316,7 @@ function parseTuiBoolean(token, current) {
|
|
|
209
316
|
if (token === "toggle") return !current;
|
|
210
317
|
}
|
|
211
318
|
/** 通过 DSH 命令注册表暴露 /prompt-tool,Web 与 dsh-tui 都能执行。 */
|
|
212
|
-
function registerTuiCommand(ctx, getSource) {
|
|
319
|
+
function registerTuiCommand(ctx, getSource, getDeepseekAvailable, getDeepseekState) {
|
|
213
320
|
ctx.inject(["settings"], (sctx) => {
|
|
214
321
|
return sctx.commands.register({
|
|
215
322
|
name: "prompt-tool",
|
|
@@ -218,14 +325,18 @@ function registerTuiCommand(ctx, getSource) {
|
|
|
218
325
|
handler: async (invocation) => {
|
|
219
326
|
const usage = () => ({
|
|
220
327
|
kind: "error",
|
|
221
|
-
text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn>\n /prompt-tool skill <技能目录名> on|off|toggle"
|
|
328
|
+
text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|subagentFlash>\n /prompt-tool skill <技能目录名> on|off|toggle"
|
|
222
329
|
});
|
|
223
330
|
const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
|
|
224
331
|
const source = getSource();
|
|
225
|
-
if (tokens.length === 0 || tokens[0] === "status")
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
332
|
+
if (tokens.length === 0 || tokens[0] === "status") {
|
|
333
|
+
const detection = getDeepseekState();
|
|
334
|
+
const deepseekLine = detection.available ? `检测到的 DeepSeek 模型路由: ${detection.providers.join(", ") || "(无)"}` : `未检测到 DeepSeek 模型路由。providers=[${detection.providers.join(", ") || "空"}] error=${detection.error ?? "无"}`;
|
|
335
|
+
return {
|
|
336
|
+
kind: "success",
|
|
337
|
+
text: renderTuiStatus(source) + "\n" + deepseekLine
|
|
338
|
+
};
|
|
339
|
+
}
|
|
229
340
|
if (tokens[0] === "skill") {
|
|
230
341
|
const folder = tokens[1];
|
|
231
342
|
if (folder === void 0) return usage();
|
|
@@ -248,6 +359,13 @@ ${renderTuiStatus(getSource())}`
|
|
|
248
359
|
const key = tokens[1];
|
|
249
360
|
if (action !== "on" && action !== "off" && action !== "toggle") return usage();
|
|
250
361
|
if (key === void 0 || !TUI_BOOLEAN_SWITCHES.some(([candidate]) => candidate === key)) return usage();
|
|
362
|
+
if (key === "subagentFlash" && !getDeepseekAvailable()) {
|
|
363
|
+
const detection = getDeepseekState();
|
|
364
|
+
return {
|
|
365
|
+
kind: "error",
|
|
366
|
+
text: `未检测到 DeepSeek 模型路由,subagentFlash 开关不可用。providers=[${detection.providers.join(", ") || "空"}] error=${detection.error ?? "无"}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
251
369
|
const currentValue = source[key];
|
|
252
370
|
if (typeof currentValue !== "boolean") return {
|
|
253
371
|
kind: "error",
|
|
@@ -271,7 +389,7 @@ ${renderTuiStatus(getSource())}`
|
|
|
271
389
|
});
|
|
272
390
|
}
|
|
273
391
|
/** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
|
|
274
|
-
function registerSettingsBridge(ctx) {
|
|
392
|
+
function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState) {
|
|
275
393
|
ctx.inject(["settings"], (sctx) => {
|
|
276
394
|
sctx.effect(() => {
|
|
277
395
|
const findDescriptor = () => sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
|
|
@@ -294,93 +412,260 @@ function registerSettingsBridge(ctx) {
|
|
|
294
412
|
}
|
|
295
413
|
return true;
|
|
296
414
|
};
|
|
297
|
-
const disposers = [
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
415
|
+
const disposers = [
|
|
416
|
+
sctx.webServer.register({
|
|
417
|
+
kind: "exact",
|
|
418
|
+
path: "/api/prompt-tool/settings/describe",
|
|
419
|
+
handler: async (req, res) => {
|
|
420
|
+
if (!guard(req, res)) return;
|
|
421
|
+
const descriptor = findDescriptor();
|
|
422
|
+
if (descriptor === void 0) {
|
|
423
|
+
writeBridgeJson(res, 404, {
|
|
424
|
+
ok: false,
|
|
425
|
+
code: "settings-not-exposed",
|
|
426
|
+
message: "prompt-tool settings namespace is not registered"
|
|
427
|
+
});
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const detection = getDeepseekState();
|
|
431
|
+
writeBridgeJson(res, 200, {
|
|
432
|
+
ok: true,
|
|
433
|
+
value: descriptor,
|
|
434
|
+
deepseekAvailable: detection.available,
|
|
435
|
+
deepseekProviders: detection.providers,
|
|
436
|
+
deepseekError: detection.error
|
|
308
437
|
});
|
|
309
|
-
return;
|
|
310
438
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
439
|
+
}),
|
|
440
|
+
sctx.webServer.register({
|
|
441
|
+
kind: "exact",
|
|
442
|
+
path: "/api/prompt-tool/settings/mutate",
|
|
443
|
+
handler: async (req, res) => {
|
|
444
|
+
if (!guard(req, res)) return;
|
|
445
|
+
const body = await readBridgeBody(req);
|
|
446
|
+
if (body === null || body === void 0 || typeof body !== "object") {
|
|
447
|
+
writeBridgeJson(res, 400, {
|
|
448
|
+
ok: false,
|
|
449
|
+
code: "settings-rejected",
|
|
450
|
+
message: "unreadable JSON body"
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const record = body;
|
|
455
|
+
if (!Array.isArray(record.ops)) {
|
|
456
|
+
writeBridgeJson(res, 400, {
|
|
457
|
+
ok: false,
|
|
458
|
+
code: "settings-rejected",
|
|
459
|
+
message: "malformed bridge settings request"
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const expectedRevision = typeof record.expectedRevision === "number" ? record.expectedRevision : void 0;
|
|
464
|
+
try {
|
|
465
|
+
await sctx.settings.mutate(NS, record.ops, expectedRevision);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
writeBridgeJson(res, 409, {
|
|
468
|
+
ok: false,
|
|
469
|
+
code: "settings-rejected",
|
|
470
|
+
message: error instanceof Error ? error.message : String(error)
|
|
471
|
+
});
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const descriptor = findDescriptor();
|
|
475
|
+
if (descriptor === void 0) {
|
|
476
|
+
writeBridgeJson(res, 500, {
|
|
477
|
+
ok: false,
|
|
478
|
+
code: "settings-rejected",
|
|
479
|
+
message: "prompt-tool settings namespace was disposed after mutate"
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
writeBridgeJson(res, 200, {
|
|
484
|
+
ok: true,
|
|
485
|
+
value: descriptor
|
|
327
486
|
});
|
|
328
|
-
return;
|
|
329
487
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
488
|
+
}),
|
|
489
|
+
sctx.webServer.register({
|
|
490
|
+
kind: "exact",
|
|
491
|
+
path: "/api/prompt-tool/settings/restore-originals",
|
|
492
|
+
handler: async (req, res) => {
|
|
493
|
+
if (!guard(req, res)) return;
|
|
494
|
+
const body = await readBridgeBody(req);
|
|
495
|
+
if (body === null || body === void 0 || typeof body !== "object") {
|
|
496
|
+
writeBridgeJson(res, 400, {
|
|
497
|
+
ok: false,
|
|
498
|
+
code: "settings-rejected",
|
|
499
|
+
message: "unreadable JSON body"
|
|
500
|
+
});
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
const record = body;
|
|
504
|
+
const scope = record.scope === "preset" || record.scope === "agents" || record.scope === "all" ? record.scope : "all";
|
|
505
|
+
const originals = readProjectOriginals();
|
|
506
|
+
const ops = [];
|
|
507
|
+
if (scope === "preset" || scope === "all") ops.push({
|
|
508
|
+
op: "set",
|
|
509
|
+
path: ["promptText"],
|
|
510
|
+
value: originals.presetText
|
|
336
511
|
});
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
await sctx.settings.mutate(NS, record.ops, expectedRevision);
|
|
342
|
-
} catch (error) {
|
|
343
|
-
writeBridgeJson(res, 409, {
|
|
344
|
-
ok: false,
|
|
345
|
-
code: "settings-rejected",
|
|
346
|
-
message: error instanceof Error ? error.message : String(error)
|
|
512
|
+
if (scope === "agents" || scope === "all") ops.push({
|
|
513
|
+
op: "set",
|
|
514
|
+
path: ["agentsText"],
|
|
515
|
+
value: originals.agentsText
|
|
347
516
|
});
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
517
|
+
const expectedRevision = typeof record.expectedRevision === "number" ? record.expectedRevision : void 0;
|
|
518
|
+
try {
|
|
519
|
+
await sctx.settings.mutate(NS, ops, expectedRevision);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
writeBridgeJson(res, 409, {
|
|
522
|
+
ok: false,
|
|
523
|
+
code: "settings-rejected",
|
|
524
|
+
message: error instanceof Error ? error.message : String(error)
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const descriptor = findDescriptor();
|
|
529
|
+
if (descriptor === void 0) {
|
|
530
|
+
writeBridgeJson(res, 500, {
|
|
531
|
+
ok: false,
|
|
532
|
+
code: "settings-rejected",
|
|
533
|
+
message: "prompt-tool settings namespace was disposed after restore"
|
|
534
|
+
});
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
writeBridgeJson(res, 200, {
|
|
538
|
+
ok: true,
|
|
539
|
+
value: descriptor
|
|
356
540
|
});
|
|
357
|
-
return;
|
|
358
541
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
value: descriptor
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
})];
|
|
542
|
+
})
|
|
543
|
+
];
|
|
365
544
|
return () => {
|
|
366
545
|
for (const dispose of disposers) dispose();
|
|
367
546
|
};
|
|
368
547
|
}, "prompt-tool: settings bridge");
|
|
369
548
|
});
|
|
370
549
|
}
|
|
550
|
+
/** 旧版“每块强制 we need”默认锚句;已存 settings 时归一化为自动模式。 */
|
|
551
|
+
const LEGACY_ANCHOR_TEXT = [
|
|
552
|
+
"You are a helpful software assistant.",
|
|
553
|
+
"",
|
|
554
|
+
"Begin every reasoning block with 'We need'."
|
|
555
|
+
].join("\n");
|
|
556
|
+
/** 旧默认值归一化为空(自动);用户自定义文本原样保留。 */
|
|
557
|
+
function normalizeAnchorText(text) {
|
|
558
|
+
const value = typeof text === "string" ? text : "";
|
|
559
|
+
return value.trim() === LEGACY_ANCHOR_TEXT.trim() ? "" : value;
|
|
560
|
+
}
|
|
561
|
+
/** 检测 DeepSeek 模型:live provider + 可配置 provider 目录双通道匹配。 */
|
|
562
|
+
function detectDeepseek(ctx) {
|
|
563
|
+
const empty = {
|
|
564
|
+
available: false,
|
|
565
|
+
providers: []
|
|
566
|
+
};
|
|
567
|
+
try {
|
|
568
|
+
const llm = ctx.get("llm");
|
|
569
|
+
if (llm === void 0) return {
|
|
570
|
+
...empty,
|
|
571
|
+
error: "ctx.get(\"llm\") 返回 undefined"
|
|
572
|
+
};
|
|
573
|
+
const live = llm.listProviders?.() ?? [];
|
|
574
|
+
const configured = llm.listConfigurableProviders?.() ?? [];
|
|
575
|
+
const names = /* @__PURE__ */ new Set();
|
|
576
|
+
const matches = (id, name) => /deepseek/i.test(id ?? "") || /deepseek/i.test(name ?? "");
|
|
577
|
+
for (const provider of live) {
|
|
578
|
+
const id = typeof provider.id === "string" ? provider.id : String(provider.id ?? "");
|
|
579
|
+
names.add(id || provider.name || "(unnamed)");
|
|
580
|
+
if (matches(provider.id, provider.name)) return {
|
|
581
|
+
available: true,
|
|
582
|
+
providers: [...names]
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
for (const provider of configured) {
|
|
586
|
+
const id = typeof provider.provider === "string" ? provider.provider : "";
|
|
587
|
+
if (id.length > 0) names.add(id);
|
|
588
|
+
if (matches(provider.provider, provider.displayName)) return {
|
|
589
|
+
available: true,
|
|
590
|
+
providers: [...names]
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
available: false,
|
|
595
|
+
providers: [...names],
|
|
596
|
+
...live.length === 0 && configured.length === 0 ? { error: "llm 服务未返回任何 provider" } : {}
|
|
597
|
+
};
|
|
598
|
+
} catch (error) {
|
|
599
|
+
return {
|
|
600
|
+
...empty,
|
|
601
|
+
error: error instanceof Error ? error.message : String(error)
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/** 给宿主直派的子代理补 Flash 路由;调用方显式 provider/model 优先,不覆盖 persona 与工具白名单。 */
|
|
606
|
+
function installSubagentFlashRoute(ctx, isEnabled, provider, model) {
|
|
607
|
+
ctx.inject(["subagents"], (sctx) => {
|
|
608
|
+
const service = sctx.get("subagents");
|
|
609
|
+
if (service === void 0 || typeof service.start !== "function") return;
|
|
610
|
+
const original = service.start;
|
|
611
|
+
const wrapped = (name, request) => {
|
|
612
|
+
if (!isEnabled() || request === null || typeof request !== "object") return original.call(service, name, request);
|
|
613
|
+
const agentOptions = request.agentOptions !== null && typeof request.agentOptions === "object" ? request.agentOptions : {};
|
|
614
|
+
if (agentOptions.provider === void 0 && agentOptions.model === void 0) return original.call(service, name, {
|
|
615
|
+
...request,
|
|
616
|
+
agentOptions: {
|
|
617
|
+
...agentOptions,
|
|
618
|
+
provider,
|
|
619
|
+
model
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
return original.call(service, name, request);
|
|
623
|
+
};
|
|
624
|
+
service.start = wrapped;
|
|
625
|
+
return () => {
|
|
626
|
+
if (service.start === wrapped) service.start = original;
|
|
627
|
+
};
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/** prompt-tool 补丁:子代理直接全量放行(assembled.tools 本身已是动态白名单)。 */
|
|
631
|
+
function patchToolBootstrap(source) {
|
|
632
|
+
source = source.replace(/\r\n/g, "\n");
|
|
633
|
+
const original = [
|
|
634
|
+
" const assembled = await next()",
|
|
635
|
+
" try {",
|
|
636
|
+
" const status = promotion.status(context.agent)"
|
|
637
|
+
].join("\n");
|
|
638
|
+
const replacement = [
|
|
639
|
+
" const assembled = await next()",
|
|
640
|
+
" try {",
|
|
641
|
+
" // prompt-tool 补丁:子代理跳过目录裁剪,直接使用组装结果;",
|
|
642
|
+
" // 调用方(如 dsh-mnemon)的工具白名单已先行过滤 assembled.tools,",
|
|
643
|
+
" // 因此任意前缀的新插件工具都会自动出现在子代理第一次会话。",
|
|
644
|
+
" if ((context.agent?.session?.header?.delegationDepth ?? 0) > 0) return assembled",
|
|
645
|
+
" const status = promotion.status(context.agent)"
|
|
646
|
+
].join("\n");
|
|
647
|
+
if (!source.includes(original)) throw new Error("tool-bootstrap.mjs assembled marker missing");
|
|
648
|
+
return source.replace(original, replacement);
|
|
649
|
+
}
|
|
371
650
|
function writePreset(prompt, options) {
|
|
372
651
|
const presetDir = options.presetDir;
|
|
373
652
|
mkdirSync(presetDir, { recursive: true });
|
|
374
653
|
writeFileSync(join(presetDir, "agent.cordis.yml"), buildCordis(prompt, {
|
|
375
654
|
anchorFirstTurn: options.anchorFirstTurn,
|
|
376
655
|
anchorText: options.anchorText,
|
|
377
|
-
|
|
656
|
+
anchorCustom: options.anchorCustom,
|
|
657
|
+
injectPrompt: options.injectPrompt,
|
|
658
|
+
subagentFlash: options.subagentFlash,
|
|
659
|
+
subagentFlashProvider: options.subagentFlashProvider,
|
|
660
|
+
subagentFlashModel: options.subagentFlashModel,
|
|
661
|
+
bashPath: options.customBashPath
|
|
378
662
|
}), "utf8");
|
|
379
663
|
const meta = readFileSync(PRESET_TEMPLATE_META, "utf8").replace(/^order:.*$/m, `order: ${options.presetOrder}`);
|
|
380
664
|
writeFileSync(join(presetDir, "preset.yml"), meta, "utf8");
|
|
381
665
|
for (const file of readdirSync(VENDOR_PRESET_DIR)) {
|
|
382
666
|
if (!file.endsWith(".mjs")) continue;
|
|
383
|
-
|
|
667
|
+
const vendorSource = readFileSync(join(VENDOR_PRESET_DIR, file), "utf8");
|
|
668
|
+
writeFileSync(join(presetDir, file), file === "tool-bootstrap.mjs" ? patchToolBootstrap(vendorSource) : vendorSource, "utf8");
|
|
384
669
|
}
|
|
385
670
|
const agentsInstructionPath = join(presetDir, "agents-instruction.txt");
|
|
386
671
|
if (options.agentsInstructionText !== void 0) {
|
|
@@ -391,11 +676,16 @@ function writePreset(prompt, options) {
|
|
|
391
676
|
const injectorPath = join(presetDir, "prompt-injector.mjs");
|
|
392
677
|
if (options.injectPrompt) writeFileSync(injectorPath, readFileSync(PRESET_TEMPLATE_INJECTOR, "utf8"), "utf8");
|
|
393
678
|
else rmSync(injectorPath, { force: true });
|
|
394
|
-
|
|
679
|
+
writeFileSync(join(presetDir, "router-first-turn.mjs"), readFileSync(PRESET_TEMPLATE_ROUTER, "utf8"), "utf8");
|
|
680
|
+
rmSync(join(presetDir, "turn-anchor.mjs"), { force: true });
|
|
681
|
+
const anchorPath = join(presetDir, "near-anchor.mjs");
|
|
395
682
|
if (options.anchorFirstTurn) writeFileSync(anchorPath, readFileSync(PRESET_TEMPLATE_ANCHOR, "utf8"), "utf8");
|
|
396
683
|
else rmSync(anchorPath, { force: true });
|
|
397
684
|
}
|
|
398
685
|
function apply(ctx, config) {
|
|
686
|
+
const deepseekState = () => detectDeepseek(ctx);
|
|
687
|
+
const getDeepseekAvailable = () => deepseekState().available;
|
|
688
|
+
const getDeepseekState = () => deepseekState();
|
|
399
689
|
let current = config.text || readPromptFile(config.fallbackText);
|
|
400
690
|
let currentAgents = config.agentsText || readAgents();
|
|
401
691
|
const skillCatalog = readSkills(config.skillsDir).map((skill) => ({
|
|
@@ -450,7 +740,7 @@ function apply(ctx, config) {
|
|
|
450
740
|
}
|
|
451
741
|
};
|
|
452
742
|
});
|
|
453
|
-
registerSettingsBridge(ctx);
|
|
743
|
+
registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState);
|
|
454
744
|
const runtime = {
|
|
455
745
|
writeAgents: config.writeAgents,
|
|
456
746
|
writePreset: config.writePreset,
|
|
@@ -458,8 +748,11 @@ function apply(ctx, config) {
|
|
|
458
748
|
injectPrompt: config.injectPrompt,
|
|
459
749
|
skillSwitches: { ...config.skillSwitches },
|
|
460
750
|
anchorFirstTurn: config.anchorFirstTurn,
|
|
461
|
-
anchorText: config.anchorText
|
|
751
|
+
anchorText: config.anchorText,
|
|
752
|
+
anchorCustom: config.anchorCustom,
|
|
753
|
+
subagentFlash: config.subagentFlash && getDeepseekAvailable()
|
|
462
754
|
};
|
|
755
|
+
installSubagentFlashRoute(ctx, () => runtime.subagentFlash, config.subagentFlashProvider, config.subagentFlashModel);
|
|
463
756
|
let currentSource = () => ({
|
|
464
757
|
promptText: current,
|
|
465
758
|
promptPath: PRESET_FILE_PATH,
|
|
@@ -467,14 +760,17 @@ function apply(ctx, config) {
|
|
|
467
760
|
agentsPath: AGENTS_FILE_PATH,
|
|
468
761
|
injectAgentsPrompt: runtime.injectAgentsPrompt,
|
|
469
762
|
anchorFirstTurn: runtime.anchorFirstTurn,
|
|
470
|
-
anchorText: runtime.anchorText,
|
|
763
|
+
anchorText: normalizeAnchorText(runtime.anchorText),
|
|
764
|
+
anchorCustom: runtime.anchorCustom,
|
|
765
|
+
subagentFlash: runtime.subagentFlash,
|
|
766
|
+
deepseekAvailable: getDeepseekAvailable(),
|
|
471
767
|
injectPrompt: runtime.injectPrompt,
|
|
472
768
|
skillSwitches: runtime.skillSwitches,
|
|
473
769
|
skillCatalog,
|
|
474
770
|
writeAgents: runtime.writeAgents,
|
|
475
771
|
writePreset: runtime.writePreset
|
|
476
772
|
});
|
|
477
|
-
registerTuiCommand(ctx, () => currentSource());
|
|
773
|
+
registerTuiCommand(ctx, () => currentSource(), getDeepseekAvailable, getDeepseekState);
|
|
478
774
|
let needsInitialApply = true;
|
|
479
775
|
const applyState = () => {
|
|
480
776
|
const next = currentSource();
|
|
@@ -485,30 +781,18 @@ function apply(ctx, config) {
|
|
|
485
781
|
injectPrompt: typeof next.injectPrompt === "boolean" ? next.injectPrompt : config.injectPrompt,
|
|
486
782
|
skillSwitches: next.skillSwitches !== void 0 ? next.skillSwitches : config.skillSwitches,
|
|
487
783
|
anchorFirstTurn: typeof next.anchorFirstTurn === "boolean" ? next.anchorFirstTurn : config.anchorFirstTurn,
|
|
488
|
-
anchorText: typeof next.anchorText === "string"
|
|
784
|
+
anchorText: normalizeAnchorText(typeof next.anchorText === "string" ? next.anchorText : config.anchorText),
|
|
785
|
+
anchorCustom: typeof next.anchorCustom === "boolean" ? next.anchorCustom : config.anchorCustom,
|
|
786
|
+
subagentFlash: (typeof next.subagentFlash === "boolean" ? next.subagentFlash : config.subagentFlash) && getDeepseekAvailable()
|
|
489
787
|
};
|
|
490
788
|
const promptChanged = next.promptText !== current;
|
|
491
789
|
const agentsChanged = next.agentsText !== currentAgents;
|
|
492
790
|
const skillSwitchesChanged = JSON.stringify(runtime.skillSwitches) !== JSON.stringify(nextRuntime.skillSwitches);
|
|
493
|
-
const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText;
|
|
791
|
+
const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText || runtime.anchorCustom !== nextRuntime.anchorCustom || runtime.subagentFlash !== nextRuntime.subagentFlash;
|
|
494
792
|
if (!needsInitialApply && !promptChanged && !agentsChanged && !settingsChanged) return;
|
|
495
793
|
needsInitialApply = false;
|
|
496
|
-
if (promptChanged)
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
writeFileSync(PRESET_FILE_URL, next.promptText, "utf8");
|
|
500
|
-
} catch (error) {
|
|
501
|
-
warn(ctx, `prompt-tool: failed to write ${PRESET_FILE_PATH}: ${String(error)}`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
if (agentsChanged) {
|
|
505
|
-
currentAgents = next.agentsText;
|
|
506
|
-
try {
|
|
507
|
-
writeFileSync(AGENTS_URL, next.agentsText, "utf8");
|
|
508
|
-
} catch (error) {
|
|
509
|
-
warn(ctx, `prompt-tool: failed to write ${AGENTS_FILE_PATH}: ${String(error)}`);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
794
|
+
if (promptChanged) current = next.promptText;
|
|
795
|
+
if (agentsChanged) currentAgents = next.agentsText;
|
|
512
796
|
runtime.writeAgents = nextRuntime.writeAgents;
|
|
513
797
|
runtime.writePreset = nextRuntime.writePreset;
|
|
514
798
|
runtime.injectAgentsPrompt = nextRuntime.injectAgentsPrompt;
|
|
@@ -516,17 +800,27 @@ function apply(ctx, config) {
|
|
|
516
800
|
runtime.skillSwitches = nextRuntime.skillSwitches;
|
|
517
801
|
runtime.anchorFirstTurn = nextRuntime.anchorFirstTurn;
|
|
518
802
|
runtime.anchorText = nextRuntime.anchorText;
|
|
803
|
+
runtime.anchorCustom = nextRuntime.anchorCustom;
|
|
804
|
+
runtime.subagentFlash = nextRuntime.subagentFlash;
|
|
519
805
|
skillSwitches = runtime.skillSwitches;
|
|
520
806
|
if (skillSwitchesChanged) invalidateSkills?.();
|
|
521
807
|
let residentAgentsWritten = false;
|
|
522
808
|
if (runtime.writeAgents) {
|
|
523
809
|
residentAgentsWritten = writeAgents(currentAgents, config.residentAgentsPath);
|
|
524
810
|
if (!residentAgentsWritten) warn(ctx, `prompt-tool: failed to write resident rules to ${config.residentAgentsPath}`);
|
|
811
|
+
} else {
|
|
812
|
+
residentAgentsWritten = removeResidentAgentsBlock(config.residentAgentsPath);
|
|
813
|
+
if (!residentAgentsWritten) warn(ctx, `prompt-tool: failed to remove resident rules block from ${config.residentAgentsPath}`);
|
|
525
814
|
}
|
|
526
815
|
if (runtime.writePreset) writePreset(runtime.injectPrompt && current.length > 0 ? current : "", {
|
|
527
816
|
anchorFirstTurn: runtime.anchorFirstTurn,
|
|
528
817
|
anchorText: runtime.anchorText,
|
|
818
|
+
anchorCustom: runtime.anchorCustom,
|
|
529
819
|
injectPrompt: runtime.injectPrompt,
|
|
820
|
+
subagentFlash: runtime.subagentFlash,
|
|
821
|
+
subagentFlashProvider: config.subagentFlashProvider,
|
|
822
|
+
subagentFlashModel: config.subagentFlashModel,
|
|
823
|
+
customBashPath: config.customBashPath,
|
|
530
824
|
agentsInstructionText: runtime.injectAgentsPrompt && currentAgents.length > 0 ? currentAgents : void 0,
|
|
531
825
|
presetDir: config.presetDir,
|
|
532
826
|
presetOrder: config.presetOrder
|
|
@@ -548,6 +842,9 @@ function apply(ctx, config) {
|
|
|
548
842
|
injectAgentsPrompt: config.injectAgentsPrompt,
|
|
549
843
|
anchorFirstTurn: config.anchorFirstTurn,
|
|
550
844
|
anchorText: config.anchorText,
|
|
845
|
+
anchorCustom: config.anchorCustom,
|
|
846
|
+
subagentFlash: config.subagentFlash && getDeepseekAvailable(),
|
|
847
|
+
deepseekAvailable: getDeepseekAvailable(),
|
|
551
848
|
injectPrompt: config.injectPrompt,
|
|
552
849
|
skillSwitches: { ...config.skillSwitches },
|
|
553
850
|
skillCatalog,
|
|
@@ -560,6 +857,7 @@ function apply(ctx, config) {
|
|
|
560
857
|
},
|
|
561
858
|
onChange: applyState
|
|
562
859
|
});
|
|
860
|
+
seedSettingsOnce(ctx, readProjectOriginals());
|
|
563
861
|
}
|
|
564
862
|
//#endregion
|
|
565
863
|
export { Config, apply, inject, name };
|