@dsh-plus/agent-preset-rea 0.1.0 → 0.1.2
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/lib/client.js +870 -52
- package/lib/index.d.ts +402 -9
- package/lib/index.js +1319 -203
- package/package.json +4 -3
- package/src/client/api.ts +77 -0
- package/src/client/card.tsx +39 -10
- package/src/client/client.ts +3 -3
- package/src/client/draft.ts +52 -6
- package/src/client/i18n.ts +135 -4
- package/src/client/install.tsx +373 -0
- package/src/client/styles.ts +27 -2
- package/src/client/toggles.tsx +57 -0
- package/src/config.ts +54 -14
- package/src/definition.ts +193 -136
- package/src/dto.ts +113 -0
- package/src/exec.ts +177 -0
- package/src/index.ts +115 -4
- package/src/jobs.ts +145 -0
- package/src/manager.ts +424 -0
- package/src/mcp.ts +47 -6
- package/src/reverse.ts +335 -0
- package/src/routes.ts +223 -0
- package/src/skill.ts +32 -7
package/lib/index.js
CHANGED
|
@@ -1,22 +1,49 @@
|
|
|
1
|
-
import { unwrapVolatile } from "@dsh-plus/shared";
|
|
1
|
+
import { pluginDataPath, unwrapVolatile } from "@dsh-plus/shared";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
3
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
4
9
|
//#region src/mcp.ts
|
|
5
10
|
/** 钉版基线:`rea-agents@6.5.0`(升级时同步测试断言、tests 清单与包文档)。 */
|
|
6
11
|
const REA_PINNED_VERSION = "6.5.0";
|
|
7
|
-
/**
|
|
12
|
+
/** 显式命令配 npx 时的参数配方基准值(`mcpArgs` 留空即按它派生)。 */
|
|
8
13
|
const DEFAULT_MCP_COMMAND = "npx";
|
|
9
14
|
/** REA 工具入参 schema 广告档位:`compact` 压请求前缀体积,`full` 与上游默认一致。 */
|
|
10
15
|
const INPUT_SCHEMA_PROFILES = ["compact", "full"];
|
|
11
16
|
/** REA 工具出参 schema 广告档位:`off` 不下发(结果仍由 REA 侧校验),`full` 与上游默认一致。 */
|
|
12
17
|
const OUTPUT_SCHEMA_PROFILES = ["off", "full"];
|
|
13
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* 显式命令(`mcpCommand` 非空)时的参数:`mcpArgs` 优先;
|
|
20
|
+
* 留空时 npx 走 `-y rea-agents@<钉版> mcp`,其他可执行文件只补 `mcp`。
|
|
21
|
+
*/
|
|
14
22
|
function reaMcpArgs(config) {
|
|
15
|
-
|
|
23
|
+
if (config.mcpArgs.length > 0) return [...config.mcpArgs];
|
|
24
|
+
return config.mcpCommand === "npx" ? [
|
|
16
25
|
"-y",
|
|
17
26
|
`rea-agents@${config.reaVersion}`,
|
|
18
27
|
"mcp"
|
|
19
|
-
];
|
|
28
|
+
] : ["mcp"];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 本预设 MCP 行的启动方式:
|
|
32
|
+
* 1. `mcpCommand` 显式配置 → 用用户命令(Windows 配方、全局安装场景);
|
|
33
|
+
* 2. 受管安装存在 → 用当前 node 直接跑安装树里的入口脚本(跨平台、不经 shell shim);
|
|
34
|
+
* 3. 都没有 → `null`,调用方把该行置 `disabled`(未安装,等卡片上的安装动作)。
|
|
35
|
+
*/
|
|
36
|
+
function reaMcpLaunch(config, installed) {
|
|
37
|
+
if (config.mcpCommand.trim() !== "") return {
|
|
38
|
+
command: config.mcpCommand,
|
|
39
|
+
args: reaMcpArgs(config)
|
|
40
|
+
};
|
|
41
|
+
if (installed === null) return null;
|
|
42
|
+
const extra = config.mcpArgs.length > 0 ? [...config.mcpArgs] : ["mcp"];
|
|
43
|
+
return {
|
|
44
|
+
command: installed.command,
|
|
45
|
+
args: [installed.entry, ...extra]
|
|
46
|
+
};
|
|
20
47
|
}
|
|
21
48
|
/**
|
|
22
49
|
* 传给 REA 子进程的环境变量(并入被清洗过的宿主环境之上)。
|
|
@@ -40,10 +67,11 @@ function reaMcpEnv(config) {
|
|
|
40
67
|
* 与 settings 用户层(GUI 配置卡片写入 profile 行级覆盖层,loader 原位提交活动
|
|
41
68
|
* 引用,消费端 `unwrapVolatile` 现取即热)。
|
|
42
69
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
70
|
+
* **活动字段(`.volatile()`)共五个**,全部在配置卡片里有编辑器(见 src/client/card.tsx):
|
|
71
|
+
* `personaPrefix`(提示词)、`proxyPort`(安装代理)、`mcpEnabled` / `skillEnabled` /
|
|
72
|
+
* `reverseSkillEnabled`(三个能力开关)——保存即热更(预设换代,新建会话生效)。
|
|
73
|
+
* 其余字段是**部署方平面**(受管目录、版本钉、skill 组、超时等运维旋钮),由
|
|
74
|
+
* `cordis.patch.yml` 整体覆盖,GUI 不提供入口。
|
|
47
75
|
*
|
|
48
76
|
* **persona 默认为空串**:`dsh-persona` 的空模板照样遮蔽部署级 persona 段,
|
|
49
77
|
* 渲染为空后在提示词里消失;官方装配(harness 身份、工具指引、项目指令、
|
|
@@ -61,11 +89,18 @@ const Config = z.object({
|
|
|
61
89
|
description: z.string().description("预设说明(设置 → Agent 预设卡片描述)").default("逆向工程预设:官方 standard 全量行 + 仅本预设可见的 REA MCP 客户端(mcp__rea__*)与随包 REA 官方 skill;persona 默认为空,工具指引沿用官方装配。"),
|
|
62
90
|
order: z.number().description("预设名册排序位").default(6),
|
|
63
91
|
personaPrefix: z.string().description("persona 前缀(system-prompt 的 deployment:persona-prefix 段);留空 = 空 persona(该段渲染为空后消失),工具指引与运行时上下文保留").default("").volatile(),
|
|
64
|
-
mcpEnabled: z.boolean().description("是否挂 REA MCP 客户端行(false = 只保留预设其余能力)").default(true),
|
|
92
|
+
mcpEnabled: z.boolean().description("是否挂 REA MCP 客户端行(false = 只保留预设其余能力)").default(true).volatile(),
|
|
93
|
+
skillEnabled: z.boolean().description("是否把 REA 官方 skill 根挂进本预设(false = 模型只看到其余 skill 根)").default(true).volatile(),
|
|
94
|
+
reverseSkillEnabled: z.boolean().description("是否挂 zhaoxuya520/reverse-skill 技能包(false = 从本预设的 skill 根里摘掉,安装副本仍在)").default(true).volatile(),
|
|
95
|
+
reverseSkillRepo: z.string().pattern(/^[\w.-]+\/[\w.-]+$/).description("reverse-skill 仓库(<owner>/<repo>;安装/手动更新都从这里拉)").default("zhaoxuya520/reverse-skill"),
|
|
96
|
+
reverseSkillRef: z.string().description("reverse-skill 的 ref(分支/标签/提交;留空 = 仓库默认分支的最新提交)").default(""),
|
|
97
|
+
reverseSkillGroups: z.array(z.string()).description("reverse-skill 里挂进本预设的技能组(相对安装树根;默认只挂主包根 skills——含路由技能与全部模块技能,CTF 侧车等组按需追加)").default(["skills"]),
|
|
65
98
|
mcpServerName: z.string().pattern(/^[A-Za-z0-9_-]{1,32}$/).description("MCP 服务命名空间:模型面工具名为 mcp__<serverName>__<tool>,取值 [A-Za-z0-9_-]{1,32}").default("rea"),
|
|
66
99
|
reaVersion: z.string().description("钉版的 rea-agents 版本(npx 拉取)").default(REA_PINNED_VERSION),
|
|
67
|
-
|
|
68
|
-
|
|
100
|
+
installRoot: z.string().description("REA 受管安装树位置(空 = <DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/rea-agent)").default(""),
|
|
101
|
+
proxyPort: z.natural().max(65535).description("安装/更新时使用的本机代理端口(0 = 不用代理;npm 与原生依赖 prebuild 下载同此)").default(0).volatile(),
|
|
102
|
+
mcpCommand: z.string().description("显式 MCP 启动命令(空 = 用受管安装入口,未安装时该行禁用;填 npx 即启动期拉取,风险自负)").default(""),
|
|
103
|
+
mcpArgs: z.array(z.string()).description("MCP stdio 附加参数;留空 = 受管入口补 [\"mcp\"],显式 npx 则派生 [\"-y\",\"rea-agents@<version>\",\"mcp\"]").default([]),
|
|
69
104
|
inputSchemaProfile: z.union(INPUT_SCHEMA_PROFILES).description("REA 工具入参 schema 广告档位(REA_MCP_INPUT_SCHEMA_PROFILE):compact 压体积,full 与上游默认一致").default("compact"),
|
|
70
105
|
outputSchemaProfile: z.union(OUTPUT_SCHEMA_PROFILES).description("REA 工具出参 schema 广告档位(REA_MCP_OUTPUT_SCHEMA_PROFILE):off 不下发(结果仍由 REA 校验),full 与上游默认一致").default("off"),
|
|
71
106
|
toolCallTimeoutMs: z.number().description("单次 MCP 工具调用超时(毫秒);REA 分析类调用常超默认 60 秒").default(6e5),
|
|
@@ -99,13 +134,401 @@ Make the plan decision-complete: state the goal and success criteria; group impl
|
|
|
99
134
|
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
100
135
|
`;
|
|
101
136
|
//#endregion
|
|
137
|
+
//#region src/exec.ts
|
|
138
|
+
/**
|
|
139
|
+
* 外部命令执行面与作业原语:受管安装/更新作业要跑 `npm` / `tar`,这些调用全部经
|
|
140
|
+
* {@link CommandRunner} 注入,单测用假 runner 跑完整状态机(零网络、零 npm)。
|
|
141
|
+
*
|
|
142
|
+
* 作业阶段由 {@link JobTaskContext} 上报,日志行由 {@link JobOperation} 的调用方
|
|
143
|
+
* 收集;本模块只负责「怎么跑命令」与「怎么把输出变成日志行」。
|
|
144
|
+
* @module @dsh-plus/agent-preset-rea/exec
|
|
145
|
+
*/
|
|
146
|
+
/** 版本字符串形状:进 shell 前把用户可配项收窄成纯版本号。 */
|
|
147
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
148
|
+
/** npm 命令名(Windows 经 shell 执行,见 {@link spawnRunner} 注释)。 */
|
|
149
|
+
const NPM = "npm";
|
|
150
|
+
/** 作业取消(AbortSignal 触发后由 runner 抛出)。 */
|
|
151
|
+
var JobCancelled = class extends Error {
|
|
152
|
+
constructor() {
|
|
153
|
+
super("作业已取消");
|
|
154
|
+
this.name = "JobCancelled";
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/** 实际 runner:spawn 子进程、逐行回调、AbortSignal 触发即杀。 */
|
|
158
|
+
const spawnRunner = { run: (command, args, options) => new Promise((resolve, reject) => {
|
|
159
|
+
const child = spawn(command, args, {
|
|
160
|
+
env: options.env,
|
|
161
|
+
shell: process.platform === "win32",
|
|
162
|
+
stdio: [
|
|
163
|
+
"ignore",
|
|
164
|
+
"pipe",
|
|
165
|
+
"pipe"
|
|
166
|
+
]
|
|
167
|
+
});
|
|
168
|
+
let pending = "";
|
|
169
|
+
const emit = (chunk) => {
|
|
170
|
+
pending += chunk;
|
|
171
|
+
const lines = pending.split("\n");
|
|
172
|
+
pending = lines.pop() ?? "";
|
|
173
|
+
for (const line of lines) options.onLine(line.trimEnd());
|
|
174
|
+
};
|
|
175
|
+
child.stdout.setEncoding("utf8");
|
|
176
|
+
child.stderr.setEncoding("utf8");
|
|
177
|
+
child.stdout.on("data", emit);
|
|
178
|
+
child.stderr.on("data", emit);
|
|
179
|
+
const abort = () => {
|
|
180
|
+
child.kill("SIGTERM");
|
|
181
|
+
};
|
|
182
|
+
options.signal.addEventListener("abort", abort, { once: true });
|
|
183
|
+
child.on("error", reject);
|
|
184
|
+
child.on("close", (code) => {
|
|
185
|
+
options.signal.removeEventListener("abort", abort);
|
|
186
|
+
if (pending !== "") options.onLine(pending.trimEnd());
|
|
187
|
+
if (options.signal.aborted) reject(new JobCancelled());
|
|
188
|
+
else resolve({ code: code ?? 1 });
|
|
189
|
+
});
|
|
190
|
+
}) };
|
|
191
|
+
/** 本机代理 URL(未配置代理时为 null)。 */
|
|
192
|
+
function proxyUrl(config) {
|
|
193
|
+
return config.proxyPort > 0 ? `http://127.0.0.1:${config.proxyPort}` : null;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* 子进程环境:继承宿主环境,附带代理端口(port > 0 时给 `https_proxy` 系列,
|
|
197
|
+
* prebuild-install 与 curl 都认这些变量——原生依赖 prebuild 下载卡死的正解)。
|
|
198
|
+
* @param config - 插件配置。
|
|
199
|
+
* @param npm - 是否附带 npm 专属开关(npm 子进程用;curl 等非 npm 命令不需要)。
|
|
200
|
+
*/
|
|
201
|
+
function commandEnv(config, npm = false) {
|
|
202
|
+
const env = {};
|
|
203
|
+
for (const [key, value] of Object.entries(process.env)) if (value !== void 0) env[key] = value;
|
|
204
|
+
if (npm) {
|
|
205
|
+
env["npm_config_audit"] = "false";
|
|
206
|
+
env["npm_config_fund"] = "false";
|
|
207
|
+
env["npm_config_update_notifier"] = "false";
|
|
208
|
+
env["npm_config_loglevel"] = "http";
|
|
209
|
+
}
|
|
210
|
+
const proxy = proxyUrl(config);
|
|
211
|
+
if (proxy !== null) {
|
|
212
|
+
env["HTTP_PROXY"] = proxy;
|
|
213
|
+
env["HTTPS_PROXY"] = proxy;
|
|
214
|
+
env["http_proxy"] = proxy;
|
|
215
|
+
env["https_proxy"] = proxy;
|
|
216
|
+
if (npm) {
|
|
217
|
+
env["npm_config_proxy"] = proxy;
|
|
218
|
+
env["npm_config_https_proxy"] = proxy;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return env;
|
|
222
|
+
}
|
|
223
|
+
/** npm 子进程环境({@link commandEnv} 的 npm 形态)。 */
|
|
224
|
+
function npmEnv(config) {
|
|
225
|
+
return commandEnv(config, true);
|
|
226
|
+
}
|
|
227
|
+
/** 版本收窄:进命令前把配置里的钉版校验成纯版本号。 */
|
|
228
|
+
function safeVersion(version) {
|
|
229
|
+
const trimmed = version.trim();
|
|
230
|
+
if (!VERSION_PATTERN.test(trimmed)) throw new Error(`配置里的 reaVersion 不是合法版本号:${JSON.stringify(version)}`);
|
|
231
|
+
return trimmed;
|
|
232
|
+
}
|
|
233
|
+
/** 逐行转发子进程输出到作业日志(截断超长行)。 */
|
|
234
|
+
function lineSink(ctx) {
|
|
235
|
+
return (line) => {
|
|
236
|
+
if (line !== "") ctx.log(line.slice(0, 500));
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** 递归列出目录下全部文件(相对 POSIX 路径,排序稳定)。 */
|
|
240
|
+
function listFiles(dir, base = "") {
|
|
241
|
+
const out = [];
|
|
242
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
243
|
+
const rel = base === "" ? entry.name : `${base}/${entry.name}`;
|
|
244
|
+
if (entry.isDirectory()) out.push(...listFiles(join(dir, entry.name), rel));
|
|
245
|
+
else out.push(rel);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
/** 目录下文件数(目录缺席或不可读按 0;状态展示用,不抛)。 */
|
|
250
|
+
function countFiles(dir) {
|
|
251
|
+
try {
|
|
252
|
+
return listFiles(dir).length;
|
|
253
|
+
} catch {
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/reverse.ts
|
|
259
|
+
/**
|
|
260
|
+
* 第二个受管技能源:GitHub 仓库 `zhaoxuya520/reverse-skill`(路由包:根技能
|
|
261
|
+
* `reverse-skill-router` + 各模块技能目录)。
|
|
262
|
+
*
|
|
263
|
+
* 与 REA 包分开成两个源的理由:来源不同(npm registry vs GitHub 仓库)、版本语义
|
|
264
|
+
* 不同(semver 钉版 vs 提交 sha)、挂载形状不同(单技能根 vs 多技能组),混在一个
|
|
265
|
+
* 作业里会让失败语义含混(npm 成功、GitHub 挂掉时到底算不算成功)。
|
|
266
|
+
*
|
|
267
|
+
* 落点:`<DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/reverse-skill/`
|
|
268
|
+
* - `repo/`:仓库树(`tar --strip-components=1` 解开,不改内容);
|
|
269
|
+
* - `manifest.json`:来源溯源(repo / ref / sha / VERSION / 技能组 / 更新时间)。
|
|
270
|
+
*
|
|
271
|
+
* 技能组发现规则:树里**直接子目录带 `SKILL.md`** 的目录即一个可挂载根(DSH 的
|
|
272
|
+
* skill 扫描正是「根的直接子项 = 技能」)。树根自身不算——它的散装 `.md`
|
|
273
|
+
* (README/AGENTS/RULES 等)会被当成技能文件解析;`plugins/**` 是各客户端的
|
|
274
|
+
* 打包产物(重复一遍路由技能),也不暴露。
|
|
275
|
+
*
|
|
276
|
+
* 作业先落 staging 再换目录:中断只留 staging,运行中的会话继续读旧树。
|
|
277
|
+
* @module @dsh-plus/agent-preset-rea/reverse
|
|
278
|
+
*/
|
|
279
|
+
/** 插件数据目录标识(与 manager.ts 同源:`<DSH_HOME>/dsh-plus/<id>`)。 */
|
|
280
|
+
const PLUGIN_ID$1 = "dsh-plus-agent-preset-rea";
|
|
281
|
+
/** 本技能源在插件数据目录下的子目录名。 */
|
|
282
|
+
const REVERSE_DIR = "reverse-skill";
|
|
283
|
+
/** 仓库树目录名(换目录作业的替换单元是它的父目录)。 */
|
|
284
|
+
const REVERSE_TREE_DIR = "repo";
|
|
285
|
+
/** 默认仓库。 */
|
|
286
|
+
const REVERSE_DEFAULT_REPO = "zhaoxuya520/reverse-skill";
|
|
287
|
+
/** 手动更新作业阶段名。 */
|
|
288
|
+
const REVERSE_PHASES = [
|
|
289
|
+
"解析仓库信息",
|
|
290
|
+
"下载源码包",
|
|
291
|
+
"解包源码",
|
|
292
|
+
"校验并写入清单"
|
|
293
|
+
];
|
|
294
|
+
/** 不参与技能组发现的目录名(客户端打包产物;会重复一遍路由技能)。 */
|
|
295
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
296
|
+
".git",
|
|
297
|
+
"plugins",
|
|
298
|
+
"node_modules"
|
|
299
|
+
]);
|
|
300
|
+
/** 未安装时的现状(导出给 emptyRuntime 等默认场景用)。 */
|
|
301
|
+
function emptyReverseState(repo = REVERSE_DEFAULT_REPO) {
|
|
302
|
+
return missingState(repo);
|
|
303
|
+
}
|
|
304
|
+
/** 未安装时的现状。 */
|
|
305
|
+
function missingState(repo) {
|
|
306
|
+
return {
|
|
307
|
+
installed: false,
|
|
308
|
+
repo,
|
|
309
|
+
ref: null,
|
|
310
|
+
sha: null,
|
|
311
|
+
version: null,
|
|
312
|
+
files: 0,
|
|
313
|
+
groups: [],
|
|
314
|
+
updatedAt: null
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** 受管路径(固定落插件数据目录,无部署方可调项)。 */
|
|
318
|
+
function reversePaths() {
|
|
319
|
+
const root = pluginDataPath(PLUGIN_ID$1, REVERSE_DIR);
|
|
320
|
+
return {
|
|
321
|
+
root,
|
|
322
|
+
tree: join(root, REVERSE_TREE_DIR)
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** 读 JSON(不存在或坏内容返回 null)。 */
|
|
326
|
+
function readJson$1(file) {
|
|
327
|
+
try {
|
|
328
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** 记录型守卫。 */
|
|
334
|
+
function asRecord$1(value) {
|
|
335
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
336
|
+
}
|
|
337
|
+
/** 直接子目录名(跳过不参与发现的目录;目录读不到按无子目录)。 */
|
|
338
|
+
function subdirs(dir) {
|
|
339
|
+
let entries;
|
|
340
|
+
try {
|
|
341
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
342
|
+
} catch {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
return entries.filter((entry) => entry.isDirectory() && !SKIP_DIRS.has(entry.name)).map((entry) => entry.name).sort();
|
|
346
|
+
}
|
|
347
|
+
/** 发现树内的技能组:直接子目录带 SKILL.md 的目录(排除树根与 SKIP_DIRS)。 */
|
|
348
|
+
function discoverSkillGroups(tree) {
|
|
349
|
+
const groups = [];
|
|
350
|
+
const walk = (rel) => {
|
|
351
|
+
const abs = rel === "" ? tree : join(tree, rel);
|
|
352
|
+
const names = subdirs(abs);
|
|
353
|
+
if (names.some((name) => existsSync(join(abs, name, "SKILL.md")))) groups.push(rel);
|
|
354
|
+
for (const name of names) {
|
|
355
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
356
|
+
walk(rel === "" ? name : `${rel}/${name}`);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
if (existsSync(tree)) walk("");
|
|
360
|
+
return groups.filter((rel) => rel !== "");
|
|
361
|
+
}
|
|
362
|
+
/** 现探技能源(清单可读 + 树在 + 至少一个技能组才算可用)。 */
|
|
363
|
+
function probeReverse(config, repo = config.reverseSkillRepo) {
|
|
364
|
+
const { root, tree } = reversePaths();
|
|
365
|
+
const manifest = asRecord$1(readJson$1(join(root, "manifest.json")));
|
|
366
|
+
if (manifest === null || !existsSync(tree)) return missingState(repo);
|
|
367
|
+
const sha = typeof manifest["sha"] === "string" ? manifest["sha"] : null;
|
|
368
|
+
if (sha === null) return missingState(repo);
|
|
369
|
+
const groups = discoverSkillGroups(tree);
|
|
370
|
+
if (groups.length === 0) return missingState(repo);
|
|
371
|
+
return {
|
|
372
|
+
installed: true,
|
|
373
|
+
repo: typeof manifest["repo"] === "string" ? manifest["repo"] : repo,
|
|
374
|
+
ref: typeof manifest["ref"] === "string" ? manifest["ref"] : "",
|
|
375
|
+
sha,
|
|
376
|
+
version: typeof manifest["version"] === "string" ? manifest["version"] : null,
|
|
377
|
+
files: typeof manifest["files"] === "number" ? manifest["files"] : countFiles(tree),
|
|
378
|
+
groups,
|
|
379
|
+
updatedAt: typeof manifest["updatedAt"] === "string" ? manifest["updatedAt"] : null
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 挂进本预设的技能组目录:配置选中 ∩ 树内发现。
|
|
384
|
+
*
|
|
385
|
+
* 组是否真实存在由现探的 {@link discoverSkillGroups} 负责(发现即存在),这里不再
|
|
386
|
+
* 二次查盘——否则「未安装/组缺席」两种语义会在两处各判一遍。
|
|
387
|
+
* @param config - 插件配置。
|
|
388
|
+
* @param state - 现探现状。
|
|
389
|
+
*/
|
|
390
|
+
function reverseSkillDirs(config, state) {
|
|
391
|
+
if (!state.installed) return [];
|
|
392
|
+
const { tree } = reversePaths();
|
|
393
|
+
return config.reverseSkillGroups.map((group) => group.trim()).filter((group) => group !== "" && state.groups.includes(group)).map((group) => join(tree, group));
|
|
394
|
+
}
|
|
395
|
+
/** curl 通用参数:失败即非零退出、跟随跳转、静默进度。 */
|
|
396
|
+
function curlArgs(config) {
|
|
397
|
+
const proxy = proxyUrl(config);
|
|
398
|
+
return ["-fsSL", ...proxy === null ? [] : ["-x", proxy]];
|
|
399
|
+
}
|
|
400
|
+
/** 跑一次 curl 并落文件(失败抛错)。 */
|
|
401
|
+
async function curl(deps, ctx, args, target) {
|
|
402
|
+
const result = await deps.runner.run("curl", [
|
|
403
|
+
...curlArgs(deps.config),
|
|
404
|
+
"-o",
|
|
405
|
+
target,
|
|
406
|
+
...args
|
|
407
|
+
], {
|
|
408
|
+
env: commandEnv(deps.config),
|
|
409
|
+
signal: ctx.signal,
|
|
410
|
+
onLine: lineSink(ctx)
|
|
411
|
+
});
|
|
412
|
+
if (result.code !== 0) throw new Error(`curl 失败(退出码 ${result.code}):${args.join(" ")}`);
|
|
413
|
+
}
|
|
414
|
+
/** GitHub API 取仓库信息(默认分支)与提交 sha。 */
|
|
415
|
+
async function resolveRepo(deps, ctx, staging) {
|
|
416
|
+
ctx.phase(0);
|
|
417
|
+
const repo = deps.config.reverseSkillRepo;
|
|
418
|
+
const api = "https://api.github.com";
|
|
419
|
+
const metaFile = join(staging, "meta.json");
|
|
420
|
+
await curl(deps, ctx, [`${api}/repos/${repo}`], metaFile);
|
|
421
|
+
const meta = asRecord$1(readJson$1(metaFile));
|
|
422
|
+
const defaultBranch = typeof meta?.["default_branch"] === "string" ? meta["default_branch"] : null;
|
|
423
|
+
if (defaultBranch === null) throw new Error(`仓库信息不完整(${repo})`);
|
|
424
|
+
const ref = deps.config.reverseSkillRef.trim() === "" ? defaultBranch : deps.config.reverseSkillRef.trim();
|
|
425
|
+
const commitFile = join(staging, "commit.json");
|
|
426
|
+
await curl(deps, ctx, [`${api}/repos/${repo}/commits/${ref}`], commitFile);
|
|
427
|
+
const commit = asRecord$1(readJson$1(commitFile));
|
|
428
|
+
const sha = typeof commit?.["sha"] === "string" ? commit["sha"] : null;
|
|
429
|
+
if (sha === null) throw new Error(`解析提交失败(${repo}@${ref})`);
|
|
430
|
+
ctx.log(`仓库 ${repo}:默认分支 ${defaultBranch},ref ${ref},提交 ${sha.slice(0, 12)}`);
|
|
431
|
+
return {
|
|
432
|
+
sha,
|
|
433
|
+
ref
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/** 下载源码包并解包到 `staging/tree`。 */
|
|
437
|
+
async function fetchTree(deps, ctx, staging) {
|
|
438
|
+
const { sha } = await resolveRepo(deps, ctx, staging);
|
|
439
|
+
const tree = join(staging, REVERSE_TREE_DIR);
|
|
440
|
+
const tarball = join(staging, "repo.tgz");
|
|
441
|
+
ctx.phase(1);
|
|
442
|
+
await curl(deps, ctx, [`https://codeload.github.com/${deps.config.reverseSkillRepo}/tar.gz/${sha}`], tarball);
|
|
443
|
+
ctx.phase(2);
|
|
444
|
+
await mkdir(tree, { recursive: true });
|
|
445
|
+
const untar = await deps.runner.run("tar", [
|
|
446
|
+
"-xzf",
|
|
447
|
+
tarball,
|
|
448
|
+
"-C",
|
|
449
|
+
tree,
|
|
450
|
+
"--strip-components=1"
|
|
451
|
+
], {
|
|
452
|
+
env: commandEnv(deps.config),
|
|
453
|
+
signal: ctx.signal,
|
|
454
|
+
onLine: lineSink(ctx)
|
|
455
|
+
});
|
|
456
|
+
if (untar.code !== 0) throw new Error(`tar 解包失败(退出码 ${untar.code})`);
|
|
457
|
+
return tree;
|
|
458
|
+
}
|
|
459
|
+
/** 读仓库 VERSION(无则 null)。 */
|
|
460
|
+
function readVersion(tree) {
|
|
461
|
+
try {
|
|
462
|
+
const text = readFileSync(join(tree, "VERSION"), "utf8").trim();
|
|
463
|
+
return text === "" ? null : text.split("\n")[0] ?? null;
|
|
464
|
+
} catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
/** 手动更新作业:拉默认分支/指定 ref 的最新提交,解包后换目录并写清单。 */
|
|
469
|
+
async function runReverseSkillJob(deps, ctx) {
|
|
470
|
+
const { root } = reversePaths();
|
|
471
|
+
const staging = `${root}.staging`;
|
|
472
|
+
await rm(staging, {
|
|
473
|
+
recursive: true,
|
|
474
|
+
force: true
|
|
475
|
+
});
|
|
476
|
+
await mkdir(staging, { recursive: true });
|
|
477
|
+
try {
|
|
478
|
+
const tree = await fetchTree(deps, ctx, staging);
|
|
479
|
+
ctx.phase(3);
|
|
480
|
+
const groups = discoverSkillGroups(tree);
|
|
481
|
+
if (groups.length === 0) throw new Error("仓库结构不含技能组(找不到带 SKILL.md 的子目录)");
|
|
482
|
+
const meta = asRecord$1(readJson$1(join(staging, "commit.json")));
|
|
483
|
+
const sha = typeof meta?.["sha"] === "string" ? meta["sha"] : "";
|
|
484
|
+
for (const scratch of [
|
|
485
|
+
"repo.tgz",
|
|
486
|
+
"meta.json",
|
|
487
|
+
"commit.json"
|
|
488
|
+
]) await rm(join(staging, scratch), { force: true });
|
|
489
|
+
const manifest = {
|
|
490
|
+
source: `github:${deps.config.reverseSkillRepo}@${sha}`,
|
|
491
|
+
repo: deps.config.reverseSkillRepo,
|
|
492
|
+
ref: deps.config.reverseSkillRef.trim(),
|
|
493
|
+
sha,
|
|
494
|
+
version: readVersion(tree),
|
|
495
|
+
files: countFiles(tree),
|
|
496
|
+
groups,
|
|
497
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
498
|
+
};
|
|
499
|
+
await writeFile(join(staging, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
500
|
+
const backup = `${root}.old`;
|
|
501
|
+
await rm(backup, {
|
|
502
|
+
recursive: true,
|
|
503
|
+
force: true
|
|
504
|
+
});
|
|
505
|
+
if (existsSync(root)) await rename(root, backup);
|
|
506
|
+
await rename(staging, root);
|
|
507
|
+
await rm(backup, {
|
|
508
|
+
recursive: true,
|
|
509
|
+
force: true
|
|
510
|
+
});
|
|
511
|
+
deps.refresh();
|
|
512
|
+
ctx.log(`已更新 ${deps.config.reverseSkillRepo}@${sha.slice(0, 12)}:${manifest.files} 文件、技能组 ${groups.join(", ")}`);
|
|
513
|
+
return manifest.version ?? sha.slice(0, 12);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
await rm(staging, {
|
|
516
|
+
recursive: true,
|
|
517
|
+
force: true
|
|
518
|
+
});
|
|
519
|
+
throw error;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
102
523
|
//#region src/skill.ts
|
|
103
524
|
/**
|
|
104
|
-
* 预设作用域内的 skill 根解析:随包 REA 官方 skill
|
|
525
|
+
* 预设作用域内的 skill 根解析:随包 REA 官方 skill(或手动更新副本)+ 配置追加目录。
|
|
526
|
+
*
|
|
527
|
+
* 全部只加在本预设 `skill-filesystem` 行的 `customSkillDirs`(rank 300 的**追加**根)
|
|
528
|
+
* 上,因此不进入项目根 / 用户根 / `$DSH_BUNDLED_SKILL_DIR`,其他预设不受影响。
|
|
105
529
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* 其他预设的 skill 目录不受影响。
|
|
530
|
+
* 手动更新副本存在时**替代**随包根而不是并排:两者同名(`reverse-engineer-anything`),
|
|
531
|
+
* 并排只会得到「哪个生效」的歧义。
|
|
109
532
|
* @module @dsh-plus/agent-preset-rea/skill
|
|
110
533
|
*/
|
|
111
534
|
/**
|
|
@@ -115,9 +538,29 @@ When ready, call exit_plan_mode with the complete plan markdown, starting with a
|
|
|
115
538
|
* 运行产物两种位置下解析到同一目录。
|
|
116
539
|
*/
|
|
117
540
|
const BUNDLED_SKILL_ROOT = fileURLToPath(new URL("../skills", import.meta.url));
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
541
|
+
/**
|
|
542
|
+
* REA skill 根(随包副本,或手动更新副本——后者存在即替代前者)。
|
|
543
|
+
* @param updatedRoot - 手动更新副本根(来自 manager 现探)。
|
|
544
|
+
*/
|
|
545
|
+
function reaSkillDirs(updatedRoot = null) {
|
|
546
|
+
return updatedRoot === null ? [BUNDLED_SKILL_ROOT] : [updatedRoot];
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* 本预设 `skill-filesystem` 行的完整追加根清单,按三个开关裁剪:
|
|
550
|
+
* - `skillEnabled=false` → 不挂 REA 官方 skill(安装副本仍留在磁盘);
|
|
551
|
+
* - `reverseSkillEnabled=false` → 不挂 reverse-skill 的技能组;
|
|
552
|
+
* - `extraSkillDirs` 是部署方追加项,不受开关影响(关掉的是本插件自带的两个源)。
|
|
553
|
+
* @param config - 插件配置。
|
|
554
|
+
* @param runtime - 现探运行期事实。
|
|
555
|
+
*/
|
|
556
|
+
function presetSkillDirs(config, runtime) {
|
|
557
|
+
const rea = config.skillEnabled === false ? [] : reaSkillDirs(runtime.skill.source === "updated" ? runtime.skill.root : null);
|
|
558
|
+
const reverse = config.reverseSkillEnabled === false ? [] : reverseSkillDirs(config, runtime.reverse);
|
|
559
|
+
return [
|
|
560
|
+
...rea,
|
|
561
|
+
...reverse,
|
|
562
|
+
...config.extraSkillDirs
|
|
563
|
+
];
|
|
121
564
|
}
|
|
122
565
|
//#endregion
|
|
123
566
|
//#region src/definition.ts
|
|
@@ -138,198 +581,684 @@ const WINDOWS_ONLY = { __jsExpr: "process.platform === 'win32'" };
|
|
|
138
581
|
const POSIX_ONLY = { __jsExpr: "process.platform !== 'win32'" };
|
|
139
582
|
/** 官方 loader 的嵌套组行名。 */
|
|
140
583
|
const GROUP = "cordis:group";
|
|
584
|
+
/** 未安装、随包 skill 的运行期事实(默认值:测试与无 manager 场景)。 */
|
|
585
|
+
function emptyRuntime() {
|
|
586
|
+
return {
|
|
587
|
+
install: null,
|
|
588
|
+
skill: {
|
|
589
|
+
source: "bundled",
|
|
590
|
+
version: null,
|
|
591
|
+
root: BUNDLED_SKILL_ROOT,
|
|
592
|
+
files: 0,
|
|
593
|
+
updatedAt: null
|
|
594
|
+
},
|
|
595
|
+
reverse: emptyReverseState()
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* 基础与工具面行:身份(persona)/ 项目指令与时钟 / shell 与文件工具 / 后台任务与
|
|
600
|
+
* 定时提醒 / skill 文件系统(本预设加 `customSkillDirs`,可指向手动更新副本)。
|
|
601
|
+
*/
|
|
602
|
+
function baseRows(config, runtime) {
|
|
603
|
+
return [
|
|
604
|
+
{
|
|
605
|
+
id: "persona",
|
|
606
|
+
name: "@deepseek-ai/dsh-persona",
|
|
607
|
+
config: { prefix: config.personaPrefix }
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: "agent-instructions",
|
|
611
|
+
name: "@deepseek-ai/dsh-agent-instructions",
|
|
612
|
+
config: { maxBytes: 65536 }
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
id: "time-context",
|
|
616
|
+
name: "@deepseek-ai/dsh-time-context"
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
id: "tool-bash",
|
|
620
|
+
name: "@deepseek-ai/dsh-tool-bash",
|
|
621
|
+
disabled: WINDOWS_ONLY
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
id: "tool-pwsh",
|
|
625
|
+
name: "@deepseek-ai/dsh-tool-pwsh",
|
|
626
|
+
disabled: POSIX_ONLY
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
id: "tool-fs",
|
|
630
|
+
name: "@deepseek-ai/dsh-tool-fs"
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
id: "tool-fs-search",
|
|
634
|
+
name: "@deepseek-ai/dsh-tool-fs-search",
|
|
635
|
+
config: { sampleOverCapGlobResults: false }
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: "tool-jobs",
|
|
639
|
+
name: "@deepseek-ai/dsh-tool-jobs"
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
id: "tool-schedule",
|
|
643
|
+
name: "@deepseek-ai/dsh-tool-schedule"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: "skill-filesystem",
|
|
647
|
+
name: "@deepseek-ai/dsh-skill-filesystem",
|
|
648
|
+
config: { customSkillDirs: presetSkillDirs(config, runtime) }
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
id: "tool-skill",
|
|
652
|
+
name: "@deepseek-ai/dsh-tool-skill"
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
id: "command-goal",
|
|
656
|
+
name: "@deepseek-ai/dsh-command-goal"
|
|
657
|
+
},
|
|
658
|
+
{
|
|
659
|
+
id: "tool-goal",
|
|
660
|
+
name: "@deepseek-ai/dsh-tool-goal"
|
|
661
|
+
}
|
|
662
|
+
];
|
|
663
|
+
}
|
|
664
|
+
/**
|
|
665
|
+
* 编排与交付面行:计划模式 / 上下文压缩 / 委派与 workflow / 交互与待办 / 网络工具 /
|
|
666
|
+
* **REA MCP 客户端** / 展示与插件管理。
|
|
667
|
+
*/
|
|
668
|
+
function orchestrationRows(config, runtime) {
|
|
669
|
+
const launch = reaMcpLaunch(config, runtime.install);
|
|
670
|
+
const mcp = launch ?? {
|
|
671
|
+
command: "npx",
|
|
672
|
+
args: reaMcpArgs({
|
|
673
|
+
...config,
|
|
674
|
+
mcpCommand: "npx"
|
|
675
|
+
})
|
|
676
|
+
};
|
|
677
|
+
return [
|
|
678
|
+
{
|
|
679
|
+
id: "planning",
|
|
680
|
+
name: GROUP,
|
|
681
|
+
group: true,
|
|
682
|
+
isolate: { planMode: true },
|
|
683
|
+
config: [{
|
|
684
|
+
id: "plan-mode",
|
|
685
|
+
name: "@deepseek-ai/dsh-plan-mode",
|
|
686
|
+
config: { section: PLAN_MODE_SECTION }
|
|
687
|
+
}]
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
id: "compaction",
|
|
691
|
+
name: GROUP,
|
|
692
|
+
group: true,
|
|
693
|
+
isolate: {
|
|
694
|
+
compaction: true,
|
|
695
|
+
toolResultPruner: true
|
|
696
|
+
},
|
|
697
|
+
config: [
|
|
698
|
+
{
|
|
699
|
+
id: "compaction-basic",
|
|
700
|
+
name: "@deepseek-ai/dsh-compaction-basic"
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
id: "command-compact",
|
|
704
|
+
name: "@deepseek-ai/dsh-command-compact"
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
id: "tool-result-pruner",
|
|
708
|
+
name: "@deepseek-ai/dsh-compaction-tool-result-pruner",
|
|
709
|
+
config: {
|
|
710
|
+
thresholdChars: 8192,
|
|
711
|
+
headChars: 4096,
|
|
712
|
+
tailChars: 1024
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
id: "delegation",
|
|
719
|
+
name: GROUP,
|
|
720
|
+
group: true,
|
|
721
|
+
isolate: { workflowEngine: true },
|
|
722
|
+
config: [
|
|
723
|
+
{
|
|
724
|
+
id: "tool-subagent-control",
|
|
725
|
+
name: "@deepseek-ai/dsh-tool-subagent-control"
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
id: "tool-subagent-list-agents",
|
|
729
|
+
name: "@deepseek-ai/dsh-tool-subagent-control/list-agents"
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
id: "tool-subagent",
|
|
733
|
+
name: "@deepseek-ai/dsh-tool-subagent",
|
|
734
|
+
config: {
|
|
735
|
+
provider: "spawn",
|
|
736
|
+
toolName: "subagent",
|
|
737
|
+
modelSelectionSettings: true
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
id: "tool-subagent-fork",
|
|
742
|
+
name: "@deepseek-ai/dsh-tool-subagent",
|
|
743
|
+
config: {
|
|
744
|
+
provider: "fork",
|
|
745
|
+
toolName: "subagent_fork"
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
id: "workflow-ptc",
|
|
750
|
+
name: "@deepseek-ai/dsh-workflow-ptc",
|
|
751
|
+
config: { provider: "spawn" }
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
id: "tool-workflow",
|
|
755
|
+
name: "@deepseek-ai/dsh-tool-workflow"
|
|
756
|
+
}
|
|
757
|
+
]
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
id: "tool-ask-user",
|
|
761
|
+
name: "@deepseek-ai/dsh-tool-ask-user"
|
|
762
|
+
},
|
|
763
|
+
{
|
|
764
|
+
id: "tool-todo",
|
|
765
|
+
name: "@deepseek-ai/dsh-tool-todo",
|
|
766
|
+
config: { allowParallelInProgress: true }
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
id: "tool-web",
|
|
770
|
+
name: "@deepseek-ai/dsh-tool-web",
|
|
771
|
+
config: {
|
|
772
|
+
fetch: true,
|
|
773
|
+
searchTimeoutMs: 6e4
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
id: "mcp-rea",
|
|
778
|
+
name: "@deepseek-ai/dsh-mcp-client",
|
|
779
|
+
disabled: !config.mcpEnabled || launch === null,
|
|
780
|
+
config: {
|
|
781
|
+
transport: "stdio",
|
|
782
|
+
serverName: config.mcpServerName,
|
|
783
|
+
command: mcp.command,
|
|
784
|
+
args: mcp.args,
|
|
785
|
+
env: reaMcpEnv(config),
|
|
786
|
+
toolCallTimeoutMs: config.toolCallTimeoutMs,
|
|
787
|
+
failOnStartupError: config.failOnStartupError
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
id: "present",
|
|
792
|
+
name: "@deepseek-ai/dsh-tool-present"
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
id: "tool-plugin-manager",
|
|
796
|
+
name: "@deepseek-ai/dsh-plugin-manager/tools",
|
|
797
|
+
disabled: true
|
|
798
|
+
}
|
|
799
|
+
];
|
|
800
|
+
}
|
|
141
801
|
/**
|
|
142
|
-
*
|
|
802
|
+
* 由配置 + 运行期事实拼出注册表声明。
|
|
143
803
|
* @param config - 已验证的插件配置。
|
|
804
|
+
* @param runtime - 受管安装与 skill 来源事实(默认「未安装 + 随包 skill」)。
|
|
144
805
|
* @returns id 固定为 `rea` 的预设声明。
|
|
145
806
|
*/
|
|
146
|
-
function reaPresetDefinition(config) {
|
|
807
|
+
function reaPresetDefinition(config, runtime = emptyRuntime()) {
|
|
147
808
|
return {
|
|
148
809
|
id: "rea",
|
|
149
810
|
name: config.name,
|
|
150
811
|
description: config.description,
|
|
151
812
|
order: config.order,
|
|
152
|
-
plugins: [
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
813
|
+
plugins: [...baseRows(config, runtime), ...orchestrationRows(config, runtime)]
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/jobs.ts
|
|
818
|
+
/** 阶段序号 → 进度(进入该阶段即报该阶段起点百分比)。 */
|
|
819
|
+
function progressOf(operation, step) {
|
|
820
|
+
const steps = operation.phases.length;
|
|
821
|
+
return {
|
|
822
|
+
step: step + 1,
|
|
823
|
+
steps,
|
|
824
|
+
phase: operation.phases[step] ?? "",
|
|
825
|
+
percent: steps === 0 ? 0 : Math.round(step / steps * 100)
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
/** 落定:写状态 + 通知订阅者(订阅者异常不得影响状态机)。 */
|
|
829
|
+
function settleJob(state, patch) {
|
|
830
|
+
const base = state.current;
|
|
831
|
+
if (base === null) return;
|
|
832
|
+
state.current = {
|
|
833
|
+
...base,
|
|
834
|
+
...patch,
|
|
835
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
836
|
+
};
|
|
837
|
+
for (const listener of [...state.listeners]) try {
|
|
838
|
+
listener(state.current);
|
|
839
|
+
} catch {}
|
|
840
|
+
}
|
|
841
|
+
/** 兜底进度形状(正常路径下 `state.current` 必有值,这里只防类型上的缺席)。 */
|
|
842
|
+
const ZERO_PROGRESS = {
|
|
843
|
+
step: 1,
|
|
844
|
+
steps: 1,
|
|
845
|
+
phase: "",
|
|
846
|
+
percent: 0
|
|
847
|
+
};
|
|
848
|
+
/** 收尾:成功记 ok + 版本,失败区分「取消」与「真失败」。 */
|
|
849
|
+
function finishJob(state, error, version) {
|
|
850
|
+
if (version !== null) {
|
|
851
|
+
settleJob(state, {
|
|
852
|
+
state: "ok",
|
|
853
|
+
version,
|
|
854
|
+
progress: {
|
|
855
|
+
...state.current?.progress ?? ZERO_PROGRESS,
|
|
856
|
+
percent: 100
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
const cancelled = error instanceof JobCancelled || state.abort?.signal.aborted === true;
|
|
862
|
+
settleJob(state, {
|
|
863
|
+
state: cancelled ? "cancelled" : "error",
|
|
864
|
+
error: cancelled ? null : error instanceof Error ? error.message : String(error)
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
/** 起一个作业并挂上状态机(调用方已确认当前无运行中作业)。 */
|
|
868
|
+
function startJob(state, kind, operation) {
|
|
869
|
+
const controller = new AbortController();
|
|
870
|
+
state.abort = controller;
|
|
871
|
+
state.seq += 1;
|
|
872
|
+
const snapshot = {
|
|
873
|
+
id: state.seq,
|
|
874
|
+
kind,
|
|
875
|
+
state: "running",
|
|
876
|
+
progress: progressOf(operation, 0),
|
|
877
|
+
log: [],
|
|
878
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
879
|
+
finishedAt: null,
|
|
880
|
+
version: null,
|
|
881
|
+
error: null
|
|
882
|
+
};
|
|
883
|
+
state.current = snapshot;
|
|
884
|
+
const push = (patch) => {
|
|
885
|
+
if (state.current !== null) state.current = {
|
|
886
|
+
...state.current,
|
|
887
|
+
...patch
|
|
888
|
+
};
|
|
889
|
+
};
|
|
890
|
+
const ctx = {
|
|
891
|
+
phase: (step) => push({ progress: progressOf(operation, step) }),
|
|
892
|
+
log: (line) => push({ log: [...state.current?.log ?? [], line].slice(-200) }),
|
|
893
|
+
signal: controller.signal
|
|
894
|
+
};
|
|
895
|
+
operation.run(ctx).then((version) => finishJob(state, null, version), (error) => finishJob(state, error, null)).finally(() => {
|
|
896
|
+
state.abort = null;
|
|
897
|
+
});
|
|
898
|
+
return snapshot;
|
|
899
|
+
}
|
|
900
|
+
/** 取消当前作业。 */
|
|
901
|
+
function cancelJob(state) {
|
|
902
|
+
if (state.abort === null || state.current?.state !== "running") return false;
|
|
903
|
+
state.abort.abort();
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
/** 建一个作业板。 */
|
|
907
|
+
function createJobBoard() {
|
|
908
|
+
const state = {
|
|
909
|
+
seq: 0,
|
|
910
|
+
current: null,
|
|
911
|
+
abort: null,
|
|
912
|
+
listeners: /* @__PURE__ */ new Set()
|
|
913
|
+
};
|
|
914
|
+
return {
|
|
915
|
+
start: (kind, operation) => state.current?.state === "running" ? null : startJob(state, kind, operation),
|
|
916
|
+
cancel: () => cancelJob(state),
|
|
917
|
+
snapshot: () => state.current,
|
|
918
|
+
onSettled: (listener) => {
|
|
919
|
+
state.listeners.add(listener);
|
|
920
|
+
return () => state.listeners.delete(listener);
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/manager.ts
|
|
926
|
+
/**
|
|
927
|
+
* REA 的受管安装与状态探测:把「REA 包」与「REA skill」都变成插件自己管理的本机资源,
|
|
928
|
+
* 从而摆脱启动期 `npx -y` 拉取(那会在 boot 里跑 npm 安装脚本:原生依赖
|
|
929
|
+
* `@lydell/node-pty` 的 prebuild 下载一旦卡住,预设激活就不返回,名册与插件页
|
|
930
|
+
* 一起被吊住;事故经过见 docs/README.md「启动期拉取的教训」)。
|
|
931
|
+
*
|
|
932
|
+
* 落点(属插件数据,不进 settings,见 @dsh-plus/shared/plugin-data;插件数据目录名
|
|
933
|
+
* 取插件 id `dsh-plus-agent-preset-rea`):
|
|
934
|
+
* - REA 包:`<DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/rea-agent/`(npm --prefix
|
|
935
|
+
* 安装树;入口 `node_modules/rea-agents/scripts/rea.mjs`,用 `process.execPath` 直接
|
|
936
|
+
* spawn,跨平台不经 shell shim);
|
|
937
|
+
* - 手动更新的 skill:`<DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/skills/`
|
|
938
|
+
* (根下是 `<技能名>/` + `manifest.json`,与随包 `skills/` 同构;
|
|
939
|
+
* 清单完整时替代随包 skill 根)。
|
|
940
|
+
*
|
|
941
|
+
* 作业都是「先落 staging 再换目录」:中断只留下 staging,不会让半截安装树被当成可用
|
|
942
|
+
* 安装;换目录后清理旧树,运行中的旧进程按 POSIX 语义继续持有已打开文件。
|
|
943
|
+
*
|
|
944
|
+
* 外部进程经 {@link CommandRunner} 注入(见 exec.ts),单测用假 runner 跑完整状态机。
|
|
945
|
+
* @module @dsh-plus/agent-preset-rea/manager
|
|
946
|
+
*/
|
|
947
|
+
/** npm 包名。 */
|
|
948
|
+
const REA_PACKAGE = "rea-agents";
|
|
949
|
+
/** 受管安装树目录名。 */
|
|
950
|
+
const REA_INSTALL_DIR = "rea-agent";
|
|
951
|
+
/** 手动更新 skill 的目录名。 */
|
|
952
|
+
const REA_SKILL_DIR = "skills";
|
|
953
|
+
/** REA 随包 skill 名(tar 包内路径与目录名同此)。 */
|
|
954
|
+
const REA_SKILL_NAME = "reverse-engineer-anything";
|
|
955
|
+
/** 插件数据目录标识(`<DSH_HOME>/dsh-plus/<id>`)。 */
|
|
956
|
+
const PLUGIN_ID = "dsh-plus-agent-preset-rea";
|
|
957
|
+
/** 安装作业阶段名。 */
|
|
958
|
+
const INSTALL_PHASES = [
|
|
959
|
+
"解析版本信息",
|
|
960
|
+
`下载并安装 ${REA_PACKAGE}`,
|
|
961
|
+
"替换安装目录",
|
|
962
|
+
"校验安装"
|
|
963
|
+
];
|
|
964
|
+
/** skill 作业阶段名。 */
|
|
965
|
+
const SKILL_PHASES = [
|
|
966
|
+
"解析版本信息",
|
|
967
|
+
"下载 skill 包",
|
|
968
|
+
"解包 skills",
|
|
969
|
+
"校验并写入清单"
|
|
970
|
+
];
|
|
971
|
+
/**
|
|
972
|
+
* 解析受管路径:`installRoot` 非空即运维指定位置(部署方平面),
|
|
973
|
+
* 否则落插件数据目录;skill 更新副本固定落插件数据目录。
|
|
974
|
+
*/
|
|
975
|
+
function managerPaths(config) {
|
|
976
|
+
const root = config.installRoot.trim();
|
|
977
|
+
return {
|
|
978
|
+
installRoot: root === "" ? pluginDataPath(PLUGIN_ID, REA_INSTALL_DIR) : root,
|
|
979
|
+
skillRoot: pluginDataPath(PLUGIN_ID, REA_SKILL_DIR)
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
/** 读 JSON(不存在或坏内容返回 null)。 */
|
|
983
|
+
function readJson(file) {
|
|
984
|
+
try {
|
|
985
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
986
|
+
} catch {
|
|
987
|
+
return null;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
/** 记录型守卫。 */
|
|
991
|
+
function asRecord(value) {
|
|
992
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
993
|
+
}
|
|
994
|
+
/** 安装树内固定入口:`node_modules/rea-agents/scripts/rea.mjs`。 */
|
|
995
|
+
function reaEntryFile(root) {
|
|
996
|
+
return join(root, "node_modules", REA_PACKAGE, "scripts", "rea.mjs");
|
|
997
|
+
}
|
|
998
|
+
/** 从清单 source(`rea-agents@1.2.3`)解析版本。 */
|
|
999
|
+
function versionOfSource(source) {
|
|
1000
|
+
const prefix = `${REA_PACKAGE}@`;
|
|
1001
|
+
return source.startsWith(prefix) ? source.slice(prefix.length) : null;
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* 探测已安装 REA:包版本可读 **且** 入口脚本存在才算安装完整
|
|
1005
|
+
* (只有一半的安装树、或换目录中断留下的 staging 都不算)。
|
|
1006
|
+
*/
|
|
1007
|
+
function probeInstall(paths) {
|
|
1008
|
+
const pkg = asRecord(readJson(join(paths.installRoot, "node_modules", REA_PACKAGE, "package.json")));
|
|
1009
|
+
const version = typeof pkg?.["version"] === "string" ? pkg["version"] : null;
|
|
1010
|
+
const entry = reaEntryFile(paths.installRoot);
|
|
1011
|
+
if (version === null || !existsSync(entry)) return null;
|
|
1012
|
+
return {
|
|
1013
|
+
version,
|
|
1014
|
+
root: paths.installRoot,
|
|
1015
|
+
command: process.execPath,
|
|
1016
|
+
entry
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
/** 探测 skill 来源:数据目录里清单完整且技能文件在位才认更新副本。 */
|
|
1020
|
+
function probeSkill(paths, bundledRoot, bundledVersion) {
|
|
1021
|
+
const manifest = asRecord(readJson(join(paths.skillRoot, "manifest.json")));
|
|
1022
|
+
const source = typeof manifest?.["source"] === "string" ? manifest["source"] : null;
|
|
1023
|
+
const files = Array.isArray(manifest?.["files"]) ? manifest["files"].length : 0;
|
|
1024
|
+
if (source !== null && files > 0 && existsSync(join(paths.skillRoot, "reverse-engineer-anything", "SKILL.md"))) return {
|
|
1025
|
+
source: "updated",
|
|
1026
|
+
version: versionOfSource(source),
|
|
1027
|
+
root: paths.skillRoot,
|
|
1028
|
+
files,
|
|
1029
|
+
updatedAt: typeof manifest?.["updatedAt"] === "string" ? manifest["updatedAt"] : null
|
|
1030
|
+
};
|
|
1031
|
+
return {
|
|
1032
|
+
source: "bundled",
|
|
1033
|
+
version: bundledVersion,
|
|
1034
|
+
root: bundledRoot,
|
|
1035
|
+
files: countFiles(bundledRoot),
|
|
1036
|
+
updatedAt: null
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
/** 现探运行期事实。 */
|
|
1040
|
+
function probeRuntime(config, bundledRoot, bundledVersion) {
|
|
1041
|
+
const paths = managerPaths(config);
|
|
1042
|
+
return {
|
|
1043
|
+
install: probeInstall(paths),
|
|
1044
|
+
skill: probeSkill(paths, bundledRoot, bundledVersion),
|
|
1045
|
+
reverse: probeReverse(config)
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* 解析钉版在 registry 上的实际版本:顺带做存在性校验(失败即报错,
|
|
1050
|
+
* 而不是装出一棵空树让 MCP 行指向不存在的入口)。
|
|
1051
|
+
*/
|
|
1052
|
+
async function resolveVersion(deps, ctx, wanted) {
|
|
1053
|
+
ctx.phase(0);
|
|
1054
|
+
const lines = [];
|
|
1055
|
+
if ((await deps.runner.run("npm", [
|
|
1056
|
+
"view",
|
|
1057
|
+
`rea-agents@${wanted}`,
|
|
1058
|
+
"version",
|
|
1059
|
+
"--json"
|
|
1060
|
+
], {
|
|
1061
|
+
env: npmEnv(deps.config),
|
|
1062
|
+
signal: ctx.signal,
|
|
1063
|
+
onLine: (line) => {
|
|
1064
|
+
lines.push(line);
|
|
1065
|
+
lineSink(ctx)(line);
|
|
1066
|
+
}
|
|
1067
|
+
})).code !== 0) throw new Error(`registry 上找不到 ${REA_PACKAGE}@${wanted}`);
|
|
1068
|
+
const reported = lines.join("").replace(/["\s]/g, "");
|
|
1069
|
+
if (reported !== "" && reported !== wanted) ctx.log(`registry 返回 ${reported}(配置钉的是 ${wanted})`);
|
|
1070
|
+
}
|
|
1071
|
+
/** 装到 staging 并清掉失败现场的公共包壳。 */
|
|
1072
|
+
async function npmInstall(deps, ctx, staging) {
|
|
1073
|
+
const version = safeVersion(deps.config.reaVersion);
|
|
1074
|
+
await rm(staging, {
|
|
1075
|
+
recursive: true,
|
|
1076
|
+
force: true
|
|
1077
|
+
});
|
|
1078
|
+
const result = await deps.runner.run("npm", [
|
|
1079
|
+
"install",
|
|
1080
|
+
"--prefix",
|
|
1081
|
+
staging,
|
|
1082
|
+
"--no-audit",
|
|
1083
|
+
"--no-fund",
|
|
1084
|
+
"--loglevel",
|
|
1085
|
+
"http",
|
|
1086
|
+
`${REA_PACKAGE}@${version}`
|
|
1087
|
+
], {
|
|
1088
|
+
env: npmEnv(deps.config),
|
|
1089
|
+
signal: ctx.signal,
|
|
1090
|
+
onLine: lineSink(ctx)
|
|
1091
|
+
});
|
|
1092
|
+
if (result.code !== 0) {
|
|
1093
|
+
await rm(staging, {
|
|
1094
|
+
recursive: true,
|
|
1095
|
+
force: true
|
|
1096
|
+
});
|
|
1097
|
+
throw new Error(`npm install 失败(退出码 ${result.code})`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/** 安装/更新 REA 包:staging 安装 → 换目录 → 校验入口可读。 */
|
|
1101
|
+
async function runInstallJob(deps, ctx) {
|
|
1102
|
+
const version = safeVersion(deps.config.reaVersion);
|
|
1103
|
+
const root = managerPaths(deps.config).installRoot;
|
|
1104
|
+
const staging = `${root}.staging`;
|
|
1105
|
+
await resolveVersion(deps, ctx, version);
|
|
1106
|
+
ctx.phase(1);
|
|
1107
|
+
await npmInstall(deps, ctx, staging);
|
|
1108
|
+
ctx.phase(2);
|
|
1109
|
+
const backup = `${root}.old`;
|
|
1110
|
+
await rm(backup, {
|
|
1111
|
+
recursive: true,
|
|
1112
|
+
force: true
|
|
1113
|
+
});
|
|
1114
|
+
if (existsSync(root)) await rename(root, backup);
|
|
1115
|
+
await rename(staging, root);
|
|
1116
|
+
await rm(backup, {
|
|
1117
|
+
recursive: true,
|
|
1118
|
+
force: true
|
|
1119
|
+
});
|
|
1120
|
+
ctx.phase(3);
|
|
1121
|
+
const probe = probeInstall(managerPaths(deps.config));
|
|
1122
|
+
if (probe === null) throw new Error("安装校验失败:入口脚本不可用");
|
|
1123
|
+
deps.refresh();
|
|
1124
|
+
ctx.log(`已安装 ${REA_PACKAGE}@${probe.version} → ${probe.root}`);
|
|
1125
|
+
return probe.version;
|
|
1126
|
+
}
|
|
1127
|
+
/** 逐文件 sha256(相对路径 + 字节数)。 */
|
|
1128
|
+
function hashFiles(dir) {
|
|
1129
|
+
return listFiles(dir).map((path) => {
|
|
1130
|
+
const bytes = readFileSync(join(dir, path));
|
|
1131
|
+
return {
|
|
1132
|
+
path,
|
|
1133
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
1134
|
+
bytes: bytes.length
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
/** 从 tar 包取钉版 skill 目录(副本根与随包 `skills/` 同构:只 strip 两层)。 */
|
|
1139
|
+
async function unpackSkill(deps, ctx, staging, tree) {
|
|
1140
|
+
const version = safeVersion(deps.config.reaVersion);
|
|
1141
|
+
await rm(staging, {
|
|
1142
|
+
recursive: true,
|
|
1143
|
+
force: true
|
|
1144
|
+
});
|
|
1145
|
+
await mkdir(staging, { recursive: true });
|
|
1146
|
+
const packed = await deps.runner.run("npm", [
|
|
1147
|
+
"pack",
|
|
1148
|
+
`${REA_PACKAGE}@${version}`,
|
|
1149
|
+
"--pack-destination",
|
|
1150
|
+
staging,
|
|
1151
|
+
"--loglevel",
|
|
1152
|
+
"http"
|
|
1153
|
+
], {
|
|
1154
|
+
env: npmEnv(deps.config),
|
|
1155
|
+
signal: ctx.signal,
|
|
1156
|
+
onLine: lineSink(ctx)
|
|
1157
|
+
});
|
|
1158
|
+
if (packed.code !== 0) {
|
|
1159
|
+
await rm(staging, {
|
|
1160
|
+
recursive: true,
|
|
1161
|
+
force: true
|
|
1162
|
+
});
|
|
1163
|
+
throw new Error(`npm pack 失败(退出码 ${packed.code})`);
|
|
1164
|
+
}
|
|
1165
|
+
const tarball = readdirSync(staging).find((name) => name.endsWith(".tgz"));
|
|
1166
|
+
if (tarball === void 0) throw new Error("npm pack 未产出 tarball");
|
|
1167
|
+
ctx.phase(2);
|
|
1168
|
+
await mkdir(tree, { recursive: true });
|
|
1169
|
+
const untar = await deps.runner.run("tar", [
|
|
1170
|
+
"-xzf",
|
|
1171
|
+
join(staging, tarball),
|
|
1172
|
+
"-C",
|
|
1173
|
+
tree,
|
|
1174
|
+
"--strip-components=2",
|
|
1175
|
+
`package/skills/${REA_SKILL_NAME}`
|
|
1176
|
+
], {
|
|
1177
|
+
env: npmEnv(deps.config),
|
|
1178
|
+
signal: ctx.signal,
|
|
1179
|
+
onLine: lineSink(ctx)
|
|
1180
|
+
});
|
|
1181
|
+
if (untar.code !== 0) {
|
|
1182
|
+
await rm(staging, {
|
|
1183
|
+
recursive: true,
|
|
1184
|
+
force: true
|
|
1185
|
+
});
|
|
1186
|
+
throw new Error(`tar 解包失败(退出码 ${untar.code})`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
/** 拉取钉版 tarball 并替换手动更新的 skill 副本(写 sha256 清单后换目录)。 */
|
|
1190
|
+
async function runSkillJob(deps, ctx) {
|
|
1191
|
+
const version = safeVersion(deps.config.reaVersion);
|
|
1192
|
+
const root = managerPaths(deps.config).skillRoot;
|
|
1193
|
+
const staging = `${root}.staging`;
|
|
1194
|
+
const tree = join(staging, "tree");
|
|
1195
|
+
await resolveVersion(deps, ctx, version);
|
|
1196
|
+
ctx.phase(1);
|
|
1197
|
+
await unpackSkill(deps, ctx, staging, tree);
|
|
1198
|
+
ctx.phase(3);
|
|
1199
|
+
const files = hashFiles(tree);
|
|
1200
|
+
if (files.length === 0) throw new Error("解包结果为空:上游 skill 目录结构可能已变");
|
|
1201
|
+
const manifest = {
|
|
1202
|
+
source: `${REA_PACKAGE}@${version}`,
|
|
1203
|
+
files,
|
|
1204
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1205
|
+
};
|
|
1206
|
+
await writeFile(join(tree, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1207
|
+
await rm(root, {
|
|
1208
|
+
recursive: true,
|
|
1209
|
+
force: true
|
|
1210
|
+
});
|
|
1211
|
+
await rename(tree, root);
|
|
1212
|
+
await rm(staging, {
|
|
1213
|
+
recursive: true,
|
|
1214
|
+
force: true
|
|
1215
|
+
});
|
|
1216
|
+
deps.refresh();
|
|
1217
|
+
ctx.log(`已更新 skill ${REA_SKILL_NAME}(${files.length} 文件)→ ${root}`);
|
|
1218
|
+
return version;
|
|
1219
|
+
}
|
|
1220
|
+
/** 生效配置:作业可带一次性覆盖(卡片未保存的代理端口即时可用)。 */
|
|
1221
|
+
function configWith(base, overrides) {
|
|
1222
|
+
if (overrides?.proxyPort === void 0) return base;
|
|
1223
|
+
return {
|
|
1224
|
+
...base,
|
|
1225
|
+
proxyPort: overrides.proxyPort
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
/** 建一个 REA 管理器。 */
|
|
1229
|
+
function createManager(options) {
|
|
1230
|
+
const runner = options.runner ?? spawnRunner;
|
|
1231
|
+
const paths = () => managerPaths(options.config());
|
|
1232
|
+
let cache = null;
|
|
1233
|
+
const refresh = () => {
|
|
1234
|
+
cache = probeRuntime(options.config(), options.bundledRoot, options.bundledVersion);
|
|
1235
|
+
return cache;
|
|
1236
|
+
};
|
|
1237
|
+
return {
|
|
1238
|
+
paths,
|
|
1239
|
+
refresh,
|
|
1240
|
+
snapshot: () => cache ?? refresh(),
|
|
1241
|
+
operations: (overrides) => {
|
|
1242
|
+
const deps = {
|
|
1243
|
+
config: configWith(options.config(), overrides),
|
|
1244
|
+
runner,
|
|
1245
|
+
refresh
|
|
1246
|
+
};
|
|
1247
|
+
return {
|
|
1248
|
+
install: {
|
|
1249
|
+
phases: INSTALL_PHASES,
|
|
1250
|
+
run: (ctx) => runInstallJob(deps, ctx)
|
|
229
1251
|
},
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
name: "@deepseek-ai/dsh-command-compact"
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
id: "tool-result-pruner",
|
|
241
|
-
name: "@deepseek-ai/dsh-compaction-tool-result-pruner",
|
|
242
|
-
config: {
|
|
243
|
-
thresholdChars: 8192,
|
|
244
|
-
headChars: 4096,
|
|
245
|
-
tailChars: 1024
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
]
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
id: "delegation",
|
|
252
|
-
name: GROUP,
|
|
253
|
-
group: true,
|
|
254
|
-
isolate: { workflowEngine: true },
|
|
255
|
-
config: [
|
|
256
|
-
{
|
|
257
|
-
id: "tool-subagent-control",
|
|
258
|
-
name: "@deepseek-ai/dsh-tool-subagent-control"
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
id: "tool-subagent-list-agents",
|
|
262
|
-
name: "@deepseek-ai/dsh-tool-subagent-control/list-agents"
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
id: "tool-subagent",
|
|
266
|
-
name: "@deepseek-ai/dsh-tool-subagent",
|
|
267
|
-
config: {
|
|
268
|
-
provider: "spawn",
|
|
269
|
-
toolName: "subagent",
|
|
270
|
-
modelSelectionSettings: true
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
id: "tool-subagent-fork",
|
|
275
|
-
name: "@deepseek-ai/dsh-tool-subagent",
|
|
276
|
-
config: {
|
|
277
|
-
provider: "fork",
|
|
278
|
-
toolName: "subagent_fork"
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
id: "workflow-ptc",
|
|
283
|
-
name: "@deepseek-ai/dsh-workflow-ptc",
|
|
284
|
-
config: { provider: "spawn" }
|
|
285
|
-
},
|
|
286
|
-
{
|
|
287
|
-
id: "tool-workflow",
|
|
288
|
-
name: "@deepseek-ai/dsh-tool-workflow"
|
|
289
|
-
}
|
|
290
|
-
]
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
id: "tool-ask-user",
|
|
294
|
-
name: "@deepseek-ai/dsh-tool-ask-user"
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
id: "tool-todo",
|
|
298
|
-
name: "@deepseek-ai/dsh-tool-todo",
|
|
299
|
-
config: { allowParallelInProgress: true }
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
id: "tool-web",
|
|
303
|
-
name: "@deepseek-ai/dsh-tool-web",
|
|
304
|
-
config: {
|
|
305
|
-
fetch: true,
|
|
306
|
-
searchTimeoutMs: 6e4
|
|
307
|
-
}
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
id: "mcp-rea",
|
|
311
|
-
name: "@deepseek-ai/dsh-mcp-client",
|
|
312
|
-
disabled: !config.mcpEnabled,
|
|
313
|
-
config: {
|
|
314
|
-
transport: "stdio",
|
|
315
|
-
serverName: config.mcpServerName,
|
|
316
|
-
command: config.mcpCommand,
|
|
317
|
-
args: reaMcpArgs(config),
|
|
318
|
-
env: reaMcpEnv(config),
|
|
319
|
-
toolCallTimeoutMs: config.toolCallTimeoutMs,
|
|
320
|
-
failOnStartupError: config.failOnStartupError
|
|
1252
|
+
skill: {
|
|
1253
|
+
phases: SKILL_PHASES,
|
|
1254
|
+
run: (ctx) => runSkillJob(deps, ctx)
|
|
1255
|
+
},
|
|
1256
|
+
reverse: {
|
|
1257
|
+
phases: REVERSE_PHASES,
|
|
1258
|
+
run: (ctx) => runReverseSkillJob(deps, ctx)
|
|
321
1259
|
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
id: "present",
|
|
325
|
-
name: "@deepseek-ai/dsh-tool-present"
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
id: "tool-plugin-manager",
|
|
329
|
-
name: "@deepseek-ai/dsh-plugin-manager/tools",
|
|
330
|
-
disabled: true
|
|
331
|
-
}
|
|
332
|
-
]
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
333
1262
|
};
|
|
334
1263
|
}
|
|
335
1264
|
//#endregion
|
|
@@ -383,6 +1312,169 @@ function createPresetRegistrar(options) {
|
|
|
383
1312
|
};
|
|
384
1313
|
}
|
|
385
1314
|
//#endregion
|
|
1315
|
+
//#region src/routes.ts
|
|
1316
|
+
/** 路由前缀。 */
|
|
1317
|
+
const ROUTE_BASE = "/dsh-plus/agent-preset-rea";
|
|
1318
|
+
/** 请求体大小上限(只有一个数字字段)。 */
|
|
1319
|
+
const BODY_LIMIT = 4096;
|
|
1320
|
+
/** 读请求体(超限即拒绝;空体按空对象)。 */
|
|
1321
|
+
async function readBody(req) {
|
|
1322
|
+
const chunks = [];
|
|
1323
|
+
let size = 0;
|
|
1324
|
+
for await (const chunk of req) {
|
|
1325
|
+
const buf = chunk;
|
|
1326
|
+
size += buf.length;
|
|
1327
|
+
if (size > BODY_LIMIT) throw new Error("请求体过大");
|
|
1328
|
+
chunks.push(buf);
|
|
1329
|
+
}
|
|
1330
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
1331
|
+
return text === "" ? {} : JSON.parse(text);
|
|
1332
|
+
}
|
|
1333
|
+
/** 从请求体取可选的代理端口覆盖(形状不对即抛,由调用方回 400)。 */
|
|
1334
|
+
function proxyPortOf(body) {
|
|
1335
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
1336
|
+
const value = body["proxyPort"];
|
|
1337
|
+
if (value === void 0 || value === null) return void 0;
|
|
1338
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 65535) throw new Error("proxyPort 必须是 0..65535 的整数");
|
|
1339
|
+
return value;
|
|
1340
|
+
}
|
|
1341
|
+
/** 组装状态响应(纯函数,可直接单测)。 */
|
|
1342
|
+
function statusPayload(options) {
|
|
1343
|
+
const config = options.config();
|
|
1344
|
+
const runtime = options.manager.snapshot();
|
|
1345
|
+
const paths = options.manager.paths();
|
|
1346
|
+
const launch = reaMcpLaunch(config, runtime.install);
|
|
1347
|
+
const mode = config.mcpCommand.trim() !== "" ? "custom" : launch === null ? "missing" : "installed";
|
|
1348
|
+
const reverse = reversePaths();
|
|
1349
|
+
const activeDirs = reverseSkillDirs(config, runtime.reverse);
|
|
1350
|
+
return {
|
|
1351
|
+
config: {
|
|
1352
|
+
reaVersion: config.reaVersion,
|
|
1353
|
+
proxyPort: config.proxyPort,
|
|
1354
|
+
mcpEnabled: config.mcpEnabled,
|
|
1355
|
+
skillEnabled: config.skillEnabled,
|
|
1356
|
+
reverseSkillEnabled: config.reverseSkillEnabled,
|
|
1357
|
+
reverseSkillRepo: config.reverseSkillRepo,
|
|
1358
|
+
reverseSkillRef: config.reverseSkillRef,
|
|
1359
|
+
reverseSkillGroups: [...config.reverseSkillGroups],
|
|
1360
|
+
installRoot: config.installRoot
|
|
1361
|
+
},
|
|
1362
|
+
install: {
|
|
1363
|
+
installed: runtime.install !== null,
|
|
1364
|
+
version: runtime.install?.version ?? null,
|
|
1365
|
+
root: paths.installRoot,
|
|
1366
|
+
entry: runtime.install?.entry ?? null
|
|
1367
|
+
},
|
|
1368
|
+
skill: { ...runtime.skill },
|
|
1369
|
+
reverse: {
|
|
1370
|
+
installed: runtime.reverse.installed,
|
|
1371
|
+
repo: runtime.reverse.repo,
|
|
1372
|
+
ref: runtime.reverse.ref,
|
|
1373
|
+
sha: runtime.reverse.sha,
|
|
1374
|
+
version: runtime.reverse.version,
|
|
1375
|
+
root: reverse.root,
|
|
1376
|
+
tree: reverse.tree,
|
|
1377
|
+
files: runtime.reverse.files,
|
|
1378
|
+
groups: [...runtime.reverse.groups],
|
|
1379
|
+
activeGroups: activeDirs.map((dir) => dir.startsWith(`${reverse.tree}/`) ? dir.slice(reverse.tree.length + 1) : dir),
|
|
1380
|
+
updatedAt: runtime.reverse.updatedAt
|
|
1381
|
+
},
|
|
1382
|
+
mcp: {
|
|
1383
|
+
mode,
|
|
1384
|
+
enabled: config.mcpEnabled && launch !== null,
|
|
1385
|
+
command: launch?.command ?? null,
|
|
1386
|
+
args: launch?.args ?? []
|
|
1387
|
+
},
|
|
1388
|
+
paths: {
|
|
1389
|
+
...paths,
|
|
1390
|
+
reverseRoot: reverse.root
|
|
1391
|
+
},
|
|
1392
|
+
job: options.board.snapshot()
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
/** 发送 JSON 响应。 */
|
|
1396
|
+
function makeSend() {
|
|
1397
|
+
return (res, status, body) => {
|
|
1398
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
1399
|
+
res.end(JSON.stringify(body));
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
/** 异常兜底:日志 + 400(响应头已发则只收尾)。 */
|
|
1403
|
+
function makeGuard(logger, send) {
|
|
1404
|
+
return (handler) => async (req, res) => {
|
|
1405
|
+
try {
|
|
1406
|
+
await handler(req, res);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1409
|
+
logger.warn(`${req.method ?? "?"} ${req.url ?? "?"} 失败:${message}`);
|
|
1410
|
+
if (!res.headersSent) send(res, 400, {
|
|
1411
|
+
job: null,
|
|
1412
|
+
error: message
|
|
1413
|
+
});
|
|
1414
|
+
else res.end();
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
/** 方法门:不匹配即 405。 */
|
|
1419
|
+
function withMethod(method, send, handler) {
|
|
1420
|
+
return async (req, res) => {
|
|
1421
|
+
if (req.method !== method) {
|
|
1422
|
+
send(res, 405, {
|
|
1423
|
+
job: null,
|
|
1424
|
+
error: "method not allowed"
|
|
1425
|
+
});
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
await handler(req, res);
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
/** 起作业的处理器工厂(409 = 已有作业在跑)。 */
|
|
1432
|
+
function startJobHandler(kind, options, logger, send) {
|
|
1433
|
+
return async (req, res) => {
|
|
1434
|
+
const overrides = proxyPortOf(await readBody(req));
|
|
1435
|
+
const operations = options.manager.operations(overrides === void 0 ? void 0 : { proxyPort: overrides });
|
|
1436
|
+
const job = options.board.start(kind, operations[kind]);
|
|
1437
|
+
if (job === null) {
|
|
1438
|
+
send(res, 409, {
|
|
1439
|
+
job: options.board.snapshot(),
|
|
1440
|
+
error: "已有作业在运行"
|
|
1441
|
+
});
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
logger.info(`已启动 ${kind} 作业`);
|
|
1445
|
+
send(res, 200, { job });
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
/** 全部路由(path 末段 → 处理器)。 */
|
|
1449
|
+
function buildRoutes(options, logger, send) {
|
|
1450
|
+
return {
|
|
1451
|
+
status: withMethod("GET", send, async (_req, res) => {
|
|
1452
|
+
send(res, 200, statusPayload(options));
|
|
1453
|
+
}),
|
|
1454
|
+
install: withMethod("POST", send, startJobHandler("install", options, logger, send)),
|
|
1455
|
+
skill: withMethod("POST", send, startJobHandler("skill", options, logger, send)),
|
|
1456
|
+
reverse: withMethod("POST", send, startJobHandler("reverse", options, logger, send)),
|
|
1457
|
+
cancel: withMethod("POST", send, async (_req, res) => {
|
|
1458
|
+
const cancelled = options.board.cancel();
|
|
1459
|
+
send(res, 200, {
|
|
1460
|
+
job: options.board.snapshot(),
|
|
1461
|
+
...cancelled ? {} : { error: "没有正在运行的作业" }
|
|
1462
|
+
});
|
|
1463
|
+
})
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
/** 注册全部路由。 */
|
|
1467
|
+
function registerReaRoutes(ctx, options) {
|
|
1468
|
+
const logger = ctx.logger("agent-preset-rea");
|
|
1469
|
+
const send = makeSend();
|
|
1470
|
+
const guard = makeGuard(logger, send);
|
|
1471
|
+
for (const [name, handler] of Object.entries(buildRoutes(options, logger, send))) ctx.webServer.register({
|
|
1472
|
+
kind: "exact",
|
|
1473
|
+
path: `${ROUTE_BASE}/${name}`,
|
|
1474
|
+
handler: guard(handler)
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
//#endregion
|
|
386
1478
|
//#region src/index.ts
|
|
387
1479
|
const name = "dsh-plus-agent-preset-rea";
|
|
388
1480
|
/** 无硬 inject:agentPresets 经 apply 内惰性 inject 接线(缺席即空转)。 */
|
|
@@ -403,20 +1495,44 @@ function apply(ctx, config) {
|
|
|
403
1495
|
const detail = error instanceof Error ? error.message : String(error);
|
|
404
1496
|
logger.warn(`register rea preset failed: ${detail}`);
|
|
405
1497
|
};
|
|
1498
|
+
const manager = createManager({
|
|
1499
|
+
config: current,
|
|
1500
|
+
bundledRoot: BUNDLED_SKILL_ROOT,
|
|
1501
|
+
bundledVersion: REA_PINNED_VERSION
|
|
1502
|
+
});
|
|
1503
|
+
const board = createJobBoard();
|
|
1504
|
+
let reregister = null;
|
|
1505
|
+
const refresh = () => {
|
|
1506
|
+
manager.refresh();
|
|
1507
|
+
reregister?.();
|
|
1508
|
+
};
|
|
1509
|
+
ctx.effect(() => board.onSettled((snapshot) => {
|
|
1510
|
+
refresh();
|
|
1511
|
+
logger.info(`rea ${snapshot.kind} 作业结束:${snapshot.state}`);
|
|
1512
|
+
}), "agent-preset-rea: refresh after job");
|
|
406
1513
|
ctx.inject(["agentPresets"], (scope) => {
|
|
407
1514
|
scope.effect(() => {
|
|
408
1515
|
const registrar = createPresetRegistrar({
|
|
409
1516
|
register: (definition) => scope.agentPresets.register(definition),
|
|
410
|
-
build: () => reaPresetDefinition(current()),
|
|
1517
|
+
build: () => reaPresetDefinition(current(), manager.snapshot()),
|
|
411
1518
|
onError
|
|
412
1519
|
});
|
|
413
|
-
|
|
1520
|
+
reregister = () => registrar.refresh();
|
|
1521
|
+
const off = ctx.events.on("loader/volatile-update", () => refresh());
|
|
414
1522
|
return () => {
|
|
1523
|
+
reregister = null;
|
|
415
1524
|
off();
|
|
416
1525
|
registrar.dispose();
|
|
417
1526
|
};
|
|
418
1527
|
}, "agent-preset-rea: register rea preset");
|
|
419
1528
|
});
|
|
1529
|
+
ctx.inject(["webServer"], (scope) => {
|
|
1530
|
+
registerReaRoutes(scope, {
|
|
1531
|
+
manager,
|
|
1532
|
+
board,
|
|
1533
|
+
config: current
|
|
1534
|
+
});
|
|
1535
|
+
});
|
|
420
1536
|
}
|
|
421
1537
|
//#endregion
|
|
422
|
-
export { ADDED_ROWS, BUNDLED_SKILL_ROOT, CONFIG_DELTAS, Config, DEFAULT_MCP_COMMAND, INPUT_SCHEMA_PROFILES, OUTPUT_SCHEMA_PROFILES, PLAN_MODE_SECTION, REA_PINNED_VERSION, REA_PRESET_ID, SETTINGS_NS, apply, createPresetRegistrar, inject, name, reaMcpArgs, reaMcpEnv, reaPresetDefinition, reaSkillDirs };
|
|
1538
|
+
export { ADDED_ROWS, BUNDLED_SKILL_ROOT, CONFIG_DELTAS, Config, DEFAULT_MCP_COMMAND, INPUT_SCHEMA_PROFILES, INSTALL_PHASES, JobCancelled, NPM, OUTPUT_SCHEMA_PROFILES, PLAN_MODE_SECTION, REA_INSTALL_DIR, REA_PACKAGE, REA_PINNED_VERSION, REA_PRESET_ID, REA_SKILL_DIR, REA_SKILL_NAME, REVERSE_DEFAULT_REPO, REVERSE_DIR, REVERSE_PHASES, REVERSE_TREE_DIR, ROUTE_BASE, SETTINGS_NS, SKILL_PHASES, apply, configWith, createJobBoard, createManager, createPresetRegistrar, discoverSkillGroups, emptyRuntime, inject, managerPaths, name, npmEnv, presetSkillDirs, probeInstall, probeReverse, probeRuntime, probeSkill, reaMcpArgs, reaMcpEnv, reaMcpLaunch, reaPresetDefinition, reaSkillDirs, registerReaRoutes, reversePaths, reverseSkillDirs, runReverseSkillJob, safeVersion, spawnRunner, statusPayload };
|