dsh-plugin-prompt-tool 0.3.1 → 0.4.1
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 +97 -16
- package/lib/client.js +119 -10
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +9 -2
- package/lib/index.mjs +302 -71
- package/lib/preset-core.d.mts +5 -1
- package/lib/preset-core.mjs +130 -31
- package/package.json +24 -21
- package/plan.md +19 -3
- package/preset/agent.cordis.yml +443 -0
- package/preset/compaction-epoch.mjs +81 -0
- package/preset/context-gate.mjs +165 -0
- package/preset/custom-bash.mjs +243 -0
- package/preset/instruction-hint.mjs +217 -0
- package/preset/near-anchor.mjs +6 -19
- package/preset/prompt-injector.mjs +111 -126
- package/preset/router-first-turn.mjs +73 -70
- package/preset/router-guide.mjs +14 -20
- package/preset/shared.mjs +83 -0
- package/preset/skill-search.mjs +142 -0
- package/preset/tool-bootstrap.mjs +282 -0
package/lib/index.mjs
CHANGED
|
@@ -1,15 +1,239 @@
|
|
|
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 { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
+
//#region src/web-surface.ts
|
|
9
|
+
/**
|
|
10
|
+
* prompt-tool 自愈层:补 profile manifest 与本插件相关的装配项。
|
|
11
|
+
*
|
|
12
|
+
* 官方 profile 组合规则:`dsh plugin add` 只把声明了 `dsh.bundle` 的
|
|
13
|
+
* **直接依赖**追加进 `dsh.profile.bundles`,而 `@deepseek-ai/dsh-web-app`
|
|
14
|
+
* 是随 dsh 安装自带的 in-box bundle。自定义 profile(如 prompt-tool)
|
|
15
|
+
* 首次初始化只含 `@deepseek-ai/dsh-base`,单独执行
|
|
16
|
+
* `dsh plugin --profile prompt-tool add link:<本仓库>` 后,本插件等待的
|
|
17
|
+
* `webServer` 服务不存在。
|
|
18
|
+
*
|
|
19
|
+
* 因此:
|
|
20
|
+
* 1. 当前 profile 缺 webServer 时,把 web-app 补进当前 profile 的
|
|
21
|
+
* bundles(base 之后、本插件之前),重启一次后生效;
|
|
22
|
+
* 2. 同级 `web` / `dsh-tui` profile 存在时,只把
|
|
23
|
+
* `dsh-plugin-prompt-tool` 写进它们的 dependencies + bundles。
|
|
24
|
+
* node_modules 的物化交给官方 `dsh plugin --profile <name> install`
|
|
25
|
+
* (本质是 pnpm)完成,本插件不手工创建链接。
|
|
26
|
+
* `dsh-tui` 只有在确实装有 `@deepseek-harness-tui/dsh-tui` 时才处理,
|
|
27
|
+
* 否则整体跳过——不写入本插件。dsh-tui **不需要** web-app。
|
|
28
|
+
*
|
|
29
|
+
* 官方 Loader 只会在进程启动时读取 profile 的 bundles 列表,因此本层只
|
|
30
|
+
* 持久化 manifest 并提示重启,不做运行中热挂载。所有写入都是幂等的。
|
|
31
|
+
* 当前 profile 首次补上 web-app 后会自动退出(exit 0),完成初始化。
|
|
32
|
+
*
|
|
33
|
+
* 失败只降级为日志告警,绝不阻断启动。
|
|
34
|
+
*/
|
|
35
|
+
const FALLBACK_WEB_APP_BUNDLE = "@deepseek-ai/dsh-web-app";
|
|
36
|
+
const BASE_BUNDLE = "@deepseek-ai/dsh-base";
|
|
37
|
+
const PLUGIN_BUNDLE = "dsh-plugin-prompt-tool";
|
|
38
|
+
const TUI_BUNDLE = "@deepseek-harness-tui/dsh-tui";
|
|
39
|
+
const SIBLING_PROFILES = ["web", "dsh-tui"];
|
|
40
|
+
/** 首次初始化完成后的自动退出延迟,给日志落盘与终端刷新留时间。 */
|
|
41
|
+
const AUTO_EXIT_DELAY_MS = 500;
|
|
42
|
+
/**
|
|
43
|
+
* 从本插件自己的 package.json 读取 `dsh.bundle.requires` 中声明的
|
|
44
|
+
* Web surface bundle(缺失时回退到 @deepseek-ai/dsh-web-app)。
|
|
45
|
+
* 这样“把 web-app 补进本插件 package.json”就是装配事实,而不是散落的魔法字符串。
|
|
46
|
+
*/
|
|
47
|
+
function readWebAppBundle() {
|
|
48
|
+
try {
|
|
49
|
+
const manifestPath = new URL("../package.json", import.meta.url);
|
|
50
|
+
const requires = JSON.parse(readFileSync(manifestPath, "utf8")).dsh?.bundle?.requires;
|
|
51
|
+
if (Array.isArray(requires)) {
|
|
52
|
+
const first = requires.find((entry) => typeof entry === "string");
|
|
53
|
+
if (first !== void 0 && first.length > 0) return first;
|
|
54
|
+
}
|
|
55
|
+
} catch {}
|
|
56
|
+
return FALLBACK_WEB_APP_BUNDLE;
|
|
57
|
+
}
|
|
58
|
+
const WEB_APP_BUNDLE = readWebAppBundle();
|
|
59
|
+
/** 从 loader 根 Include 的 baseUrl 推导 profile 目录。 */
|
|
60
|
+
function resolveProfileDir(ctx) {
|
|
61
|
+
const baseUrl = ctx.baseUrl;
|
|
62
|
+
if (typeof baseUrl !== "string" || baseUrl.length === 0) return void 0;
|
|
63
|
+
try {
|
|
64
|
+
return fileURLToPath(new URL(".", baseUrl));
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function readManifest(packagePath, warn) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(readFileSync(packagePath, "utf8"));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
warn(`prompt-tool: cannot read profile manifest ${packagePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function writeManifest(packagePath, manifest, warn) {
|
|
78
|
+
try {
|
|
79
|
+
writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
80
|
+
return true;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
warn(`prompt-tool: cannot update profile manifest ${packagePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** dsh-tui profile 是否真的装着 dsh-tui(依赖声明或 bundle 层都算)。 */
|
|
87
|
+
function hasDshTui(manifest) {
|
|
88
|
+
return manifest.dependencies?.[TUI_BUNDLE] !== void 0 || manifest.dsh?.profile?.bundles?.includes(TUI_BUNDLE) === true;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 把 web-app 补进当前 profile 的 bundles(base 之后、本插件之前)。
|
|
92
|
+
* 只用于当前 prompt-tool profile;dsh-tui 不需要 web-app。
|
|
93
|
+
*/
|
|
94
|
+
function ensureCurrentWebAppBundle(manifest) {
|
|
95
|
+
const bundles = manifest.dsh?.profile?.bundles;
|
|
96
|
+
if (!Array.isArray(bundles)) return "invalid";
|
|
97
|
+
if (bundles.includes(WEB_APP_BUNDLE)) return "already";
|
|
98
|
+
const pluginIndex = bundles.indexOf(PLUGIN_BUNDLE);
|
|
99
|
+
const baseIndex = bundles.indexOf(BASE_BUNDLE);
|
|
100
|
+
const insertAt = pluginIndex >= 0 ? pluginIndex : baseIndex >= 0 ? baseIndex + 1 : bundles.length;
|
|
101
|
+
bundles.splice(insertAt, 0, WEB_APP_BUNDLE);
|
|
102
|
+
return "added";
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 把本插件补进同级 profile 的 dependencies + bundles。
|
|
106
|
+
* @returns 实际写入的条目描述;返回 undefined 表示跳过/失败。
|
|
107
|
+
*/
|
|
108
|
+
function ensurePluginInSiblingProfile(packagePath, manifest, pluginSpec, warn) {
|
|
109
|
+
const bundles = manifest.dsh?.profile?.bundles;
|
|
110
|
+
if (!Array.isArray(bundles)) {
|
|
111
|
+
warn(`prompt-tool: sibling profile manifest has no dsh.profile.bundles: ${packagePath}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const hasDependency = manifest.dependencies?.[PLUGIN_BUNDLE] !== void 0;
|
|
115
|
+
const added = [];
|
|
116
|
+
if (!hasDependency) {
|
|
117
|
+
if (typeof pluginSpec !== "string" || pluginSpec.length === 0) {
|
|
118
|
+
warn(`prompt-tool: skip sibling profile ${packagePath} — current profile declares no install spec for ${PLUGIN_BUNDLE}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
manifest.dependencies = {
|
|
122
|
+
...manifest.dependencies,
|
|
123
|
+
[PLUGIN_BUNDLE]: pluginSpec
|
|
124
|
+
};
|
|
125
|
+
added.push(`dependency ${PLUGIN_BUNDLE}=${pluginSpec}`);
|
|
126
|
+
}
|
|
127
|
+
if (!bundles.includes(PLUGIN_BUNDLE)) {
|
|
128
|
+
bundles.push(PLUGIN_BUNDLE);
|
|
129
|
+
added.push(`bundle ${PLUGIN_BUNDLE}`);
|
|
130
|
+
}
|
|
131
|
+
if (added.length === 0) return void 0;
|
|
132
|
+
if (!writeManifest(packagePath, manifest, warn)) return void 0;
|
|
133
|
+
return added.join(", ");
|
|
134
|
+
}
|
|
135
|
+
/** 自愈同级 web / dsh-tui profile;只写 package.json,不创建 node_modules 链接。 */
|
|
136
|
+
function healSiblingProfiles(profileDir, pluginSpec, warn) {
|
|
137
|
+
const profilesDir = join(profileDir, "..");
|
|
138
|
+
for (const profileName of SIBLING_PROFILES) {
|
|
139
|
+
const packagePath = join(profilesDir, profileName, "package.json");
|
|
140
|
+
if (!existsSync(packagePath)) {
|
|
141
|
+
warn(`prompt-tool: skip sibling heal for "${profileName}" — profile package.json not found`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const manifest = readManifest(packagePath, warn);
|
|
145
|
+
if (manifest === void 0) continue;
|
|
146
|
+
if (profileName === "dsh-tui" && !hasDshTui(manifest)) {
|
|
147
|
+
warn(`prompt-tool: skip sibling heal for "dsh-tui" — ${TUI_BUNDLE} is not installed; dsh-plugin-prompt-tool will not be written into it`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const changes = ensurePluginInSiblingProfile(packagePath, manifest, pluginSpec, warn);
|
|
151
|
+
if (changes !== void 0) warn(`prompt-tool: healed sibling profile "${profileName}": added ${changes}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 每次启动:
|
|
156
|
+
* 1. 同级 web / dsh-tui 补本插件(dsh-tui 未安装则跳过);
|
|
157
|
+
* 2. 当前 profile 缺 webServer 时补 web-app 并提示重启。
|
|
158
|
+
* webServer 已存在时当前 profile 无需处理,但同级自愈仍会执行(幂等)。
|
|
159
|
+
*/
|
|
160
|
+
function ensureWebSurface(ctx, warn) {
|
|
161
|
+
const notify = (message) => {
|
|
162
|
+
process.stderr.write(message + "\n");
|
|
163
|
+
warn(message);
|
|
164
|
+
};
|
|
165
|
+
const profileDir = resolveProfileDir(ctx);
|
|
166
|
+
if (profileDir === void 0) {
|
|
167
|
+
notify("prompt-tool: cannot locate profile directory to repair bundles; add @deepseek-ai/dsh-web-app to dsh.profile.bundles manually");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const packagePath = join(profileDir, "package.json");
|
|
171
|
+
const manifest = readManifest(packagePath, notify);
|
|
172
|
+
if (manifest === void 0) return;
|
|
173
|
+
healSiblingProfiles(profileDir, typeof manifest.dependencies?.[PLUGIN_BUNDLE] === "string" ? manifest.dependencies[PLUGIN_BUNDLE] : void 0, notify);
|
|
174
|
+
const profileName = basename(profileDir);
|
|
175
|
+
if (profileName === "dsh-tui" || hasDshTui(manifest)) return;
|
|
176
|
+
if (ctx.get("webServer") !== void 0) return;
|
|
177
|
+
const result = ensureCurrentWebAppBundle(manifest);
|
|
178
|
+
if (result === "invalid") {
|
|
179
|
+
notify(`prompt-tool: profile manifest has no dsh.profile.bundles: ${packagePath}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (result === "added" && !writeManifest(packagePath, manifest, notify)) return;
|
|
183
|
+
if (result === "added") {
|
|
184
|
+
notify(`prompt-tool: auto-added ${WEB_APP_BUNDLE} to dsh.profile.bundles for profile "${profileName}"; next launch will mount the Web surface (dsh web / dsh-tui / dsh --profile ${profileName})`);
|
|
185
|
+
notify("prompt-tool: initialization complete — exiting so the repaired profile can be launched");
|
|
186
|
+
setTimeout(() => process.exit(0), AUTO_EXIT_DELAY_MS);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/profile-skills.ts
|
|
191
|
+
/**
|
|
192
|
+
* prompt-tool skills 的 profile 副本。
|
|
193
|
+
*
|
|
194
|
+
* 安装命令 `dsh plugin add` 是官方的 pnpm 转发器,插件代码不会在该命令中
|
|
195
|
+
* 执行(本项目也不修改官方源码)。因此复制动作放在插件首次 apply 时:
|
|
196
|
+
* 把包内 `skills/` 增量复制到当前 profile 目录下的 `skills/`
|
|
197
|
+
* (`$DSH_HOME/profiles/<profile>/skills`),并且后续启动优先使用这份副本。
|
|
198
|
+
*
|
|
199
|
+
* 合并规则:
|
|
200
|
+
* - 只补缺失项,绝不覆盖已存在文件——用户在 profile 副本里的修改会保留;
|
|
201
|
+
* - 包内新增的技能目录/文件会在下次启动补进副本;
|
|
202
|
+
* - 副本目录不可写或 profile 定位失败时回退到包内目录,只告警不阻断。
|
|
203
|
+
*/
|
|
204
|
+
/** 递归把 src 中缺失的目录/文件补到 dst,已有项保持不动。 */
|
|
205
|
+
function mergeMissing(src, dst) {
|
|
206
|
+
mkdirSync(dst, { recursive: true });
|
|
207
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
208
|
+
const srcEntry = join(src, entry.name);
|
|
209
|
+
const dstEntry = join(dst, entry.name);
|
|
210
|
+
if (!existsSync(dstEntry)) cpSync(srcEntry, dstEntry, { recursive: true });
|
|
211
|
+
else if (entry.isDirectory()) mergeMissing(srcEntry, dstEntry);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* 返回本插件应使用的 skills 目录:
|
|
216
|
+
* 优先当前 profile 下的 `skills/` 副本;无法确定 profile 或复制失败时
|
|
217
|
+
* 回退到 sourceDir(默认包内 `skills/`)。
|
|
218
|
+
*/
|
|
219
|
+
function resolveProfileSkillsDir(ctx, sourceDir, warn) {
|
|
220
|
+
const profileDir = resolveProfileDir(ctx);
|
|
221
|
+
if (profileDir === void 0) return sourceDir;
|
|
222
|
+
const targetDir = join(profileDir, "skills");
|
|
223
|
+
try {
|
|
224
|
+
if (!existsSync(sourceDir)) return targetDir;
|
|
225
|
+
mergeMissing(sourceDir, targetDir);
|
|
226
|
+
return targetDir;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
warn(`prompt-tool: failed to copy skills into profile directory ${targetDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
229
|
+
return sourceDir;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
8
233
|
//#region src/index.ts
|
|
9
234
|
const name = "prompt-tool";
|
|
10
235
|
const inject = [
|
|
11
236
|
"skills",
|
|
12
|
-
"webServer",
|
|
13
237
|
"commands",
|
|
14
238
|
"llm",
|
|
15
239
|
"subagents"
|
|
@@ -33,7 +257,16 @@ const PRESET_TEMPLATE_INJECTOR = fileURLToPath(new URL("../preset/prompt-injecto
|
|
|
33
257
|
const PRESET_TEMPLATE_ANCHOR = fileURLToPath(new URL("../preset/near-anchor.mjs", import.meta.url));
|
|
34
258
|
const PRESET_TEMPLATE_ROUTER = fileURLToPath(new URL("../preset/router-first-turn.mjs", import.meta.url));
|
|
35
259
|
const PRESET_TEMPLATE_GUIDE = fileURLToPath(new URL("../preset/router-guide.mjs", import.meta.url));
|
|
36
|
-
const
|
|
260
|
+
const LOCAL_PRESET_DIR = fileURLToPath(new URL("../preset", import.meta.url));
|
|
261
|
+
const LOCAL_PRESET_SCRIPTS = [
|
|
262
|
+
"shared.mjs",
|
|
263
|
+
"context-gate.mjs",
|
|
264
|
+
"compaction-epoch.mjs",
|
|
265
|
+
"custom-bash.mjs",
|
|
266
|
+
"instruction-hint.mjs",
|
|
267
|
+
"skill-search.mjs",
|
|
268
|
+
"tool-bootstrap.mjs"
|
|
269
|
+
];
|
|
37
270
|
const Config = z.object({
|
|
38
271
|
text: z.string().default(""),
|
|
39
272
|
agentsText: z.string().default(""),
|
|
@@ -51,6 +284,7 @@ const Config = z.object({
|
|
|
51
284
|
subagentFlashProvider: z.string().default("deepseek-official"),
|
|
52
285
|
subagentFlashModel: z.string().default("deepseek-v4-flash"),
|
|
53
286
|
bootstrapMaxTokens: z.natural().default(0),
|
|
287
|
+
usePtcMode: z.boolean().default(true),
|
|
54
288
|
skillsDir: z.string().default(DEFAULT_SKILLS_DIR),
|
|
55
289
|
skillRankBase: z.natural().default(DEFAULT_SKILL_RANK_BASE),
|
|
56
290
|
residentAgentsPath: z.string().default(DEFAULT_RESIDENT_AGENTS_PATH),
|
|
@@ -71,6 +305,7 @@ const PromptSettingsSchema = z.object({
|
|
|
71
305
|
guideCustom: z.boolean().default(false),
|
|
72
306
|
subagentFlash: z.boolean().default(false),
|
|
73
307
|
bootstrapMaxTokens: z.natural().default(0),
|
|
308
|
+
usePtcMode: z.boolean().default(true),
|
|
74
309
|
deepseekAvailable: z.boolean().default(true),
|
|
75
310
|
injectPrompt: z.boolean().default(true),
|
|
76
311
|
skillSwitches: z.dict(z.boolean()).default({}),
|
|
@@ -79,6 +314,8 @@ const PromptSettingsSchema = z.object({
|
|
|
79
314
|
name: z.string(),
|
|
80
315
|
description: z.string().default("")
|
|
81
316
|
})).default([]),
|
|
317
|
+
skillsDir: z.string().default(""),
|
|
318
|
+
activeSkillsDir: z.string().default(""),
|
|
82
319
|
writeAgents: z.boolean().default(true),
|
|
83
320
|
writePreset: z.boolean().default(true)
|
|
84
321
|
});
|
|
@@ -228,28 +465,6 @@ function warn(ctx, message) {
|
|
|
228
465
|
ctx.logger?.warn(message);
|
|
229
466
|
} catch {}
|
|
230
467
|
}
|
|
231
|
-
/** 把上游 instruction-hint.mjs 的提示文本替换为读同目录 agents-instruction.txt。 */
|
|
232
|
-
function patchInstructionHint(source) {
|
|
233
|
-
source = source.replace(/\r\n/g, "\n");
|
|
234
|
-
const original = [
|
|
235
|
-
" const text = [",
|
|
236
|
-
" ...sections,",
|
|
237
|
-
" 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',",
|
|
238
|
-
" ].join(' ')"
|
|
239
|
-
].join("\n");
|
|
240
|
-
const replacement = [
|
|
241
|
-
" const agentsInstructionText = readFileSync(new URL('./agents-instruction.txt', import.meta.url), 'utf8').trim()",
|
|
242
|
-
" const text = agentsInstructionText.length > 0",
|
|
243
|
-
" ? agentsInstructionText",
|
|
244
|
-
" : [",
|
|
245
|
-
" ...sections,",
|
|
246
|
-
" 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',",
|
|
247
|
-
" ].join(' ')"
|
|
248
|
-
].join("\n");
|
|
249
|
-
if (!source.includes("import { createEpochPromotion }")) throw new Error("instruction-hint.mjs import marker missing");
|
|
250
|
-
if (!source.includes(original)) throw new Error("instruction-hint.mjs text marker missing");
|
|
251
|
-
return source.replace("import { createEpochPromotion } from './compaction-epoch.mjs'", "import { readFileSync } from 'node:fs'\nimport { createEpochPromotion } from './compaction-epoch.mjs'").replace(original, replacement);
|
|
252
|
-
}
|
|
253
468
|
const MAX_SETTINGS_BRIDGE_BODY = 65536;
|
|
254
469
|
/** 仅允许本机回环请求,镜像官方 settings bridge 的边界。 */
|
|
255
470
|
function isLoopbackRequest(req) {
|
|
@@ -296,7 +511,8 @@ const TUI_BOOLEAN_SWITCHES = [
|
|
|
296
511
|
["anchorFirstTurn", "追加任务引导"],
|
|
297
512
|
["anchorCustom", "使用自定义引导(首句)"],
|
|
298
513
|
["guideCustom", "使用自定义引导(每轮)"],
|
|
299
|
-
["subagentFlash", "子代理固定 Flash 模型"]
|
|
514
|
+
["subagentFlash", "子代理固定 Flash 模型"],
|
|
515
|
+
["usePtcMode", "使用 PTC 模式"]
|
|
300
516
|
];
|
|
301
517
|
/** 把布尔开关渲染成 dsh-tui 命令输出。 */
|
|
302
518
|
function renderTuiStatus(source) {
|
|
@@ -311,6 +527,7 @@ function renderTuiStatus(source) {
|
|
|
311
527
|
` anchorText ${source.anchorText.length > 0 ? source.anchorText : "(空 = 按任务自动选择)"}`,
|
|
312
528
|
` deepseekAvailable ${source.deepseekAvailable ? "是" : "否(未检测到 DeepSeek 模型,subagentFlash 不可用)"}`,
|
|
313
529
|
` bootstrapMaxTokens ${source.bootstrapMaxTokens > 0 ? source.bootstrapMaxTokens : "0(关闭,不设封顶)"}`,
|
|
530
|
+
` activeSkillsDir ${source.activeSkillsDir || "(未解析到技能目录)"}`,
|
|
314
531
|
"技能开关:"
|
|
315
532
|
];
|
|
316
533
|
for (const skill of source.skillCatalog) {
|
|
@@ -335,7 +552,7 @@ function registerTuiCommand(ctx, getSource, getDeepseekAvailable, getDeepseekSta
|
|
|
335
552
|
handler: async (invocation) => {
|
|
336
553
|
const usage = () => ({
|
|
337
554
|
kind: "error",
|
|
338
|
-
text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|guideCustom|subagentFlash>\n /prompt-tool skill <技能目录名> on|off|toggle\n /prompt-tool bootstrapMaxTokens <正整数|0(关闭)>"
|
|
555
|
+
text: "用法:/prompt-tool status\n /prompt-tool on|off|toggle <writeAgents|writePreset|injectPrompt|injectAgentsPrompt|anchorFirstTurn|anchorCustom|guideCustom|subagentFlash|usePtcMode>\n /prompt-tool skill <技能目录名> on|off|toggle\n /prompt-tool bootstrapMaxTokens <正整数|0(关闭)>"
|
|
339
556
|
});
|
|
340
557
|
const tokens = invocation.rawInput.trim().split(/\s+/).filter((token) => token.length > 0);
|
|
341
558
|
const source = getSource();
|
|
@@ -418,8 +635,8 @@ ${renderTuiStatus(getSource())}`
|
|
|
418
635
|
});
|
|
419
636
|
}
|
|
420
637
|
/** 自建 loopback settings bridge:替代 registerConfigurableProviders,避免模型设置区出现插件条目。 */
|
|
421
|
-
function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState) {
|
|
422
|
-
ctx.inject(["settings"], (sctx) => {
|
|
638
|
+
function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState, getSkillsState) {
|
|
639
|
+
ctx.inject(["settings", "webServer"], (sctx) => {
|
|
423
640
|
sctx.effect(() => {
|
|
424
641
|
const findDescriptor = () => sctx.settings.describe({ redactSecrets: true }).find((entry) => String(entry.ns) === String(NS));
|
|
425
642
|
const guard = (req, res) => {
|
|
@@ -457,12 +674,15 @@ function registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState) {
|
|
|
457
674
|
return;
|
|
458
675
|
}
|
|
459
676
|
const detection = getDeepseekState();
|
|
677
|
+
const skillsState = getSkillsState();
|
|
460
678
|
writeBridgeJson(res, 200, {
|
|
461
679
|
ok: true,
|
|
462
680
|
value: descriptor,
|
|
463
681
|
deepseekAvailable: detection.available,
|
|
464
682
|
deepseekProviders: detection.providers,
|
|
465
|
-
deepseekError: detection.error
|
|
683
|
+
deepseekError: detection.error,
|
|
684
|
+
activeSkillsDir: skillsState.activeSkillsDir,
|
|
685
|
+
skillCatalog: skillsState.skillCatalog
|
|
466
686
|
});
|
|
467
687
|
}
|
|
468
688
|
}),
|
|
@@ -656,26 +876,6 @@ function installSubagentFlashRoute(ctx, isEnabled, provider, model) {
|
|
|
656
876
|
};
|
|
657
877
|
});
|
|
658
878
|
}
|
|
659
|
-
/** prompt-tool 补丁:子代理直接全量放行(assembled.tools 本身已是动态白名单)。 */
|
|
660
|
-
function patchToolBootstrap(source) {
|
|
661
|
-
source = source.replace(/\r\n/g, "\n");
|
|
662
|
-
const original = [
|
|
663
|
-
" const assembled = await next()",
|
|
664
|
-
" try {",
|
|
665
|
-
" const status = promotion.status(context.agent)"
|
|
666
|
-
].join("\n");
|
|
667
|
-
const replacement = [
|
|
668
|
-
" const assembled = await next()",
|
|
669
|
-
" try {",
|
|
670
|
-
" // prompt-tool 补丁:子代理跳过目录裁剪,直接使用组装结果;",
|
|
671
|
-
" // 调用方(如 dsh-mnemon)的工具白名单已先行过滤 assembled.tools,",
|
|
672
|
-
" // 因此任意前缀的新插件工具都会自动出现在子代理第一次会话。",
|
|
673
|
-
" if ((context.agent?.session?.header?.delegationDepth ?? 0) > 0) return assembled",
|
|
674
|
-
" const status = promotion.status(context.agent)"
|
|
675
|
-
].join("\n");
|
|
676
|
-
if (!source.includes(original)) throw new Error("tool-bootstrap.mjs assembled marker missing");
|
|
677
|
-
return source.replace(original, replacement);
|
|
678
|
-
}
|
|
679
879
|
function writePreset(prompt, options) {
|
|
680
880
|
const presetDir = options.presetDir;
|
|
681
881
|
mkdirSync(presetDir, { recursive: true });
|
|
@@ -689,21 +889,16 @@ function writePreset(prompt, options) {
|
|
|
689
889
|
subagentFlash: options.subagentFlash,
|
|
690
890
|
subagentFlashProvider: options.subagentFlashProvider,
|
|
691
891
|
subagentFlashModel: options.subagentFlashModel,
|
|
692
|
-
bootstrapMaxTokens: options.bootstrapMaxTokens
|
|
892
|
+
bootstrapMaxTokens: options.bootstrapMaxTokens,
|
|
893
|
+
usePtcMode: options.usePtcMode
|
|
693
894
|
}), "utf8");
|
|
694
895
|
const meta = readFileSync(PRESET_TEMPLATE_META, "utf8").replace(/^order:.*$/m, `order: ${options.presetOrder}`);
|
|
695
896
|
writeFileSync(join(presetDir, "preset.yml"), meta, "utf8");
|
|
696
|
-
for (const file of
|
|
697
|
-
|
|
698
|
-
const vendorSource = readFileSync(join(VENDOR_PRESET_DIR, file), "utf8");
|
|
699
|
-
writeFileSync(join(presetDir, file), file === "tool-bootstrap.mjs" ? patchToolBootstrap(vendorSource) : vendorSource, "utf8");
|
|
700
|
-
}
|
|
897
|
+
for (const file of LOCAL_PRESET_SCRIPTS) writeFileSync(join(presetDir, file), readFileSync(join(LOCAL_PRESET_DIR, file), "utf8"), "utf8");
|
|
898
|
+
rmSync(join(presetDir, "dev-tool-search.mjs"), { force: true });
|
|
701
899
|
const agentsInstructionPath = join(presetDir, "agents-instruction.txt");
|
|
702
|
-
if (options.agentsInstructionText !== void 0)
|
|
703
|
-
|
|
704
|
-
const hintPath = join(presetDir, "instruction-hint.mjs");
|
|
705
|
-
writeFileSync(hintPath, patchInstructionHint(readFileSync(hintPath, "utf8")), "utf8");
|
|
706
|
-
} else rmSync(agentsInstructionPath, { force: true });
|
|
900
|
+
if (options.agentsInstructionText !== void 0) writeFileSync(agentsInstructionPath, options.agentsInstructionText, "utf8");
|
|
901
|
+
else rmSync(agentsInstructionPath, { force: true });
|
|
707
902
|
const injectorPath = join(presetDir, "prompt-injector.mjs");
|
|
708
903
|
if (options.injectPrompt) writeFileSync(injectorPath, readFileSync(PRESET_TEMPLATE_INJECTOR, "utf8"), "utf8");
|
|
709
904
|
else rmSync(injectorPath, { force: true });
|
|
@@ -720,11 +915,24 @@ function apply(ctx, config) {
|
|
|
720
915
|
const getDeepseekState = () => deepseekState();
|
|
721
916
|
let current = config.text || readPromptFile(config.fallbackText);
|
|
722
917
|
let currentAgents = config.agentsText || readAgents();
|
|
723
|
-
const
|
|
918
|
+
const configuredSkillsDir = config.skillsDir !== DEFAULT_SKILLS_DIR && config.skillsDir.length > 0 ? config.skillsDir : "";
|
|
919
|
+
let activeSkillsDir = configuredSkillsDir.length > 0 ? configuredSkillsDir : resolveProfileSkillsDir(ctx, DEFAULT_SKILLS_DIR, (message) => warn(ctx, message));
|
|
920
|
+
let skillCatalog = readSkills(activeSkillsDir).map((skill) => ({
|
|
724
921
|
folder: skill.folder,
|
|
725
922
|
name: skill.name,
|
|
726
923
|
description: skill.description
|
|
727
924
|
}));
|
|
925
|
+
/** 切换生效技能目录并刷新目录快照(供 describe / TUI 显示)。 */
|
|
926
|
+
const applyActiveSkillsDir = (dir) => {
|
|
927
|
+
activeSkillsDir = dir;
|
|
928
|
+
skillCatalog = readSkills(dir).map((skill) => ({
|
|
929
|
+
folder: skill.folder,
|
|
930
|
+
name: skill.name,
|
|
931
|
+
description: skill.description
|
|
932
|
+
}));
|
|
933
|
+
};
|
|
934
|
+
/** 用户 skillsDir 设置 → 实际生效目录。 */
|
|
935
|
+
const resolveActiveSkillsDir = (userDir) => userDir.length > 0 ? userDir : resolveProfileSkillsDir(ctx, DEFAULT_SKILLS_DIR, (message) => warn(ctx, message));
|
|
728
936
|
let skillSwitches = { ...config.skillSwitches };
|
|
729
937
|
let invalidateSkills;
|
|
730
938
|
ctx.skills.registerProvider((control) => {
|
|
@@ -733,7 +941,7 @@ function apply(ctx, config) {
|
|
|
733
941
|
name: "prompt-tool",
|
|
734
942
|
list: async (options) => {
|
|
735
943
|
if (options.signal?.aborted) return [];
|
|
736
|
-
return readSkills(
|
|
944
|
+
return readSkills(activeSkillsDir).filter((skill) => skillSwitches[skill.folder] !== false).map((skill, index) => ({
|
|
737
945
|
name: skill.name,
|
|
738
946
|
description: skill.description || skill.folder,
|
|
739
947
|
...skill.whenToUse !== void 0 ? { whenToUse: skill.whenToUse } : {},
|
|
@@ -745,7 +953,7 @@ function apply(ctx, config) {
|
|
|
745
953
|
provider: "prompt-tool",
|
|
746
954
|
resourceBase: {
|
|
747
955
|
kind: "directory",
|
|
748
|
-
path: join(
|
|
956
|
+
path: join(activeSkillsDir, skill.folder)
|
|
749
957
|
},
|
|
750
958
|
rank: config.skillRankBase + index,
|
|
751
959
|
locator: skill.folder,
|
|
@@ -755,7 +963,7 @@ function apply(ctx, config) {
|
|
|
755
963
|
},
|
|
756
964
|
get: async (candidate, options) => {
|
|
757
965
|
if (options.signal?.aborted) return void 0;
|
|
758
|
-
const skill = readSkills(
|
|
966
|
+
const skill = readSkills(activeSkillsDir).find((entry) => entry.folder === candidate.locator || entry.name === candidate.name);
|
|
759
967
|
if (skill === void 0 || skillSwitches[skill.folder] === false) return void 0;
|
|
760
968
|
return {
|
|
761
969
|
name: candidate.name,
|
|
@@ -772,20 +980,28 @@ function apply(ctx, config) {
|
|
|
772
980
|
}
|
|
773
981
|
};
|
|
774
982
|
});
|
|
775
|
-
registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState)
|
|
983
|
+
registerSettingsBridge(ctx, getDeepseekAvailable, getDeepseekState, () => ({
|
|
984
|
+
activeSkillsDir,
|
|
985
|
+
skillCatalog
|
|
986
|
+
}));
|
|
987
|
+
setImmediate(() => {
|
|
988
|
+
ensureWebSurface(ctx, (message) => warn(ctx, message));
|
|
989
|
+
});
|
|
776
990
|
const runtime = {
|
|
777
991
|
writeAgents: config.writeAgents,
|
|
778
992
|
writePreset: config.writePreset,
|
|
779
993
|
injectAgentsPrompt: config.injectAgentsPrompt,
|
|
780
994
|
injectPrompt: config.injectPrompt,
|
|
781
995
|
skillSwitches: { ...config.skillSwitches },
|
|
996
|
+
skillsDir: configuredSkillsDir,
|
|
782
997
|
anchorFirstTurn: config.anchorFirstTurn,
|
|
783
998
|
anchorText: config.anchorText,
|
|
784
999
|
anchorCustom: config.anchorCustom,
|
|
785
1000
|
guideText: config.guideText,
|
|
786
1001
|
guideCustom: config.guideCustom,
|
|
787
1002
|
subagentFlash: config.subagentFlash && getDeepseekAvailable(),
|
|
788
|
-
bootstrapMaxTokens: config.bootstrapMaxTokens
|
|
1003
|
+
bootstrapMaxTokens: config.bootstrapMaxTokens,
|
|
1004
|
+
usePtcMode: config.usePtcMode
|
|
789
1005
|
};
|
|
790
1006
|
installSubagentFlashRoute(ctx, () => runtime.subagentFlash, config.subagentFlashProvider, config.subagentFlashModel);
|
|
791
1007
|
let currentSource = () => ({
|
|
@@ -801,10 +1017,13 @@ function apply(ctx, config) {
|
|
|
801
1017
|
guideCustom: runtime.guideCustom,
|
|
802
1018
|
subagentFlash: runtime.subagentFlash,
|
|
803
1019
|
bootstrapMaxTokens: runtime.bootstrapMaxTokens,
|
|
1020
|
+
usePtcMode: runtime.usePtcMode,
|
|
804
1021
|
deepseekAvailable: getDeepseekAvailable(),
|
|
805
1022
|
injectPrompt: runtime.injectPrompt,
|
|
806
1023
|
skillSwitches: runtime.skillSwitches,
|
|
807
1024
|
skillCatalog,
|
|
1025
|
+
skillsDir: runtime.skillsDir,
|
|
1026
|
+
activeSkillsDir,
|
|
808
1027
|
writeAgents: runtime.writeAgents,
|
|
809
1028
|
writePreset: runtime.writePreset
|
|
810
1029
|
});
|
|
@@ -818,18 +1037,21 @@ function apply(ctx, config) {
|
|
|
818
1037
|
injectAgentsPrompt: typeof next.injectAgentsPrompt === "boolean" ? next.injectAgentsPrompt : config.injectAgentsPrompt,
|
|
819
1038
|
injectPrompt: typeof next.injectPrompt === "boolean" ? next.injectPrompt : config.injectPrompt,
|
|
820
1039
|
skillSwitches: next.skillSwitches !== void 0 ? next.skillSwitches : config.skillSwitches,
|
|
1040
|
+
skillsDir: typeof next.skillsDir === "string" ? next.skillsDir : configuredSkillsDir,
|
|
821
1041
|
anchorFirstTurn: typeof next.anchorFirstTurn === "boolean" ? next.anchorFirstTurn : config.anchorFirstTurn,
|
|
822
1042
|
anchorText: normalizeAnchorText(typeof next.anchorText === "string" ? next.anchorText : config.anchorText),
|
|
823
1043
|
anchorCustom: typeof next.anchorCustom === "boolean" ? next.anchorCustom : config.anchorCustom,
|
|
824
1044
|
guideText: typeof next.guideText === "string" ? next.guideText : config.guideText,
|
|
825
1045
|
guideCustom: typeof next.guideCustom === "boolean" ? next.guideCustom : config.guideCustom,
|
|
826
1046
|
subagentFlash: (typeof next.subagentFlash === "boolean" ? next.subagentFlash : config.subagentFlash) && getDeepseekAvailable(),
|
|
827
|
-
bootstrapMaxTokens: Number.isSafeInteger(next.bootstrapMaxTokens) && next.bootstrapMaxTokens >= 0 ? next.bootstrapMaxTokens : config.bootstrapMaxTokens
|
|
1047
|
+
bootstrapMaxTokens: Number.isSafeInteger(next.bootstrapMaxTokens) && next.bootstrapMaxTokens >= 0 ? next.bootstrapMaxTokens : config.bootstrapMaxTokens,
|
|
1048
|
+
usePtcMode: typeof next.usePtcMode === "boolean" ? next.usePtcMode : config.usePtcMode
|
|
828
1049
|
};
|
|
829
1050
|
const promptChanged = next.promptText !== current;
|
|
830
1051
|
const agentsChanged = next.agentsText !== currentAgents;
|
|
831
1052
|
const skillSwitchesChanged = JSON.stringify(runtime.skillSwitches) !== JSON.stringify(nextRuntime.skillSwitches);
|
|
832
|
-
const
|
|
1053
|
+
const skillsDirChanged = runtime.skillsDir !== nextRuntime.skillsDir;
|
|
1054
|
+
const settingsChanged = runtime.writeAgents !== nextRuntime.writeAgents || runtime.writePreset !== nextRuntime.writePreset || runtime.injectAgentsPrompt !== nextRuntime.injectAgentsPrompt || runtime.injectPrompt !== nextRuntime.injectPrompt || skillSwitchesChanged || skillsDirChanged || runtime.anchorFirstTurn !== nextRuntime.anchorFirstTurn || runtime.anchorText !== nextRuntime.anchorText || runtime.anchorCustom !== nextRuntime.anchorCustom || runtime.guideText !== nextRuntime.guideText || runtime.guideCustom !== nextRuntime.guideCustom || runtime.subagentFlash !== nextRuntime.subagentFlash || runtime.bootstrapMaxTokens !== nextRuntime.bootstrapMaxTokens || runtime.usePtcMode !== nextRuntime.usePtcMode;
|
|
833
1055
|
if (!needsInitialApply && !promptChanged && !agentsChanged && !settingsChanged) return;
|
|
834
1056
|
needsInitialApply = false;
|
|
835
1057
|
if (promptChanged) current = next.promptText;
|
|
@@ -839,6 +1061,7 @@ function apply(ctx, config) {
|
|
|
839
1061
|
runtime.injectAgentsPrompt = nextRuntime.injectAgentsPrompt;
|
|
840
1062
|
runtime.injectPrompt = nextRuntime.injectPrompt;
|
|
841
1063
|
runtime.skillSwitches = nextRuntime.skillSwitches;
|
|
1064
|
+
runtime.skillsDir = nextRuntime.skillsDir;
|
|
842
1065
|
runtime.anchorFirstTurn = nextRuntime.anchorFirstTurn;
|
|
843
1066
|
runtime.anchorText = nextRuntime.anchorText;
|
|
844
1067
|
runtime.anchorCustom = nextRuntime.anchorCustom;
|
|
@@ -846,8 +1069,12 @@ function apply(ctx, config) {
|
|
|
846
1069
|
runtime.guideCustom = nextRuntime.guideCustom;
|
|
847
1070
|
runtime.subagentFlash = nextRuntime.subagentFlash;
|
|
848
1071
|
runtime.bootstrapMaxTokens = nextRuntime.bootstrapMaxTokens;
|
|
1072
|
+
runtime.usePtcMode = nextRuntime.usePtcMode;
|
|
849
1073
|
skillSwitches = runtime.skillSwitches;
|
|
850
|
-
if (
|
|
1074
|
+
if (skillsDirChanged) {
|
|
1075
|
+
applyActiveSkillsDir(resolveActiveSkillsDir(runtime.skillsDir));
|
|
1076
|
+
invalidateSkills?.();
|
|
1077
|
+
} else if (skillSwitchesChanged) invalidateSkills?.();
|
|
851
1078
|
let residentAgentsWritten = false;
|
|
852
1079
|
if (runtime.writeAgents) {
|
|
853
1080
|
residentAgentsWritten = writeAgents(currentAgents, config.residentAgentsPath);
|
|
@@ -867,6 +1094,7 @@ function apply(ctx, config) {
|
|
|
867
1094
|
subagentFlashProvider: config.subagentFlashProvider,
|
|
868
1095
|
subagentFlashModel: config.subagentFlashModel,
|
|
869
1096
|
bootstrapMaxTokens: runtime.bootstrapMaxTokens,
|
|
1097
|
+
usePtcMode: runtime.usePtcMode,
|
|
870
1098
|
agentsInstructionText: runtime.injectAgentsPrompt && currentAgents.length > 0 ? currentAgents : void 0,
|
|
871
1099
|
presetDir: config.presetDir,
|
|
872
1100
|
presetOrder: config.presetOrder
|
|
@@ -893,10 +1121,13 @@ function apply(ctx, config) {
|
|
|
893
1121
|
guideCustom: config.guideCustom,
|
|
894
1122
|
subagentFlash: config.subagentFlash && getDeepseekAvailable(),
|
|
895
1123
|
bootstrapMaxTokens: config.bootstrapMaxTokens,
|
|
1124
|
+
usePtcMode: config.usePtcMode,
|
|
896
1125
|
deepseekAvailable: getDeepseekAvailable(),
|
|
897
1126
|
injectPrompt: config.injectPrompt,
|
|
898
1127
|
skillSwitches: { ...config.skillSwitches },
|
|
899
1128
|
skillCatalog,
|
|
1129
|
+
skillsDir: configuredSkillsDir,
|
|
1130
|
+
activeSkillsDir,
|
|
900
1131
|
writeAgents: config.writeAgents,
|
|
901
1132
|
writePreset: config.writePreset
|
|
902
1133
|
};
|
package/lib/preset-core.d.mts
CHANGED
|
@@ -30,7 +30,11 @@ interface BuildCordisOptions {
|
|
|
30
30
|
subagentFlashModel?: string;
|
|
31
31
|
/** 首轮输出封顶(bootstrapMaxTokens);0 或未设置 = 本项目默认无封顶。 */
|
|
32
32
|
bootstrapMaxTokens?: number;
|
|
33
|
+
/** 使用 PTC 模式:默认 true——晋升后把 wire 切换为 Code Mode(单一 run_code,完整插件工具经生成 SDK 调用);显式 false 时晋升后恢复原生完整工具目录。 */
|
|
34
|
+
usePtcMode?: boolean;
|
|
33
35
|
}
|
|
36
|
+
/** 给生成的 tool-bootstrap.mjs 打补丁:子代理全量放行 + 可选的 PTC 呈现切换。 */
|
|
37
|
+
declare function patchToolBootstrap(source: string): string;
|
|
34
38
|
declare function buildCordis(prompt: string, options?: BuildCordisOptions): string;
|
|
35
39
|
//#endregion
|
|
36
|
-
export { BuildCordisOptions, SkillFrontmatter, buildCordis, parseFrontmatter };
|
|
40
|
+
export { BuildCordisOptions, SkillFrontmatter, buildCordis, parseFrontmatter, patchToolBootstrap };
|