@dsh-plus/agent-preset-rea 0.1.1 → 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 +363 -105
- package/lib/index.d.ts +127 -20
- package/lib/index.js +510 -196
- package/package.json +2 -2
- package/src/client/api.ts +7 -6
- package/src/client/card.tsx +8 -4
- package/src/client/client.ts +2 -2
- package/src/client/draft.ts +21 -2
- package/src/client/i18n.ts +42 -2
- package/src/client/install.tsx +157 -58
- package/src/client/toggles.tsx +57 -0
- package/src/config.ts +34 -5
- package/src/definition.ts +6 -6
- package/src/dto.ts +29 -3
- package/src/exec.ts +34 -12
- package/src/index.ts +21 -1
- package/src/manager.ts +14 -9
- package/src/reverse.ts +335 -0
- package/src/routes.ts +108 -66
- package/src/skill.ts +23 -8
package/lib/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { pluginDataPath, unwrapVolatile } from "@dsh-plus/shared";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
3
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
6
5
|
import { join } from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
//#region src/mcp.ts
|
|
10
10
|
/** 钉版基线:`rea-agents@6.5.0`(升级时同步测试断言、tests 清单与包文档)。 */
|
|
11
11
|
const REA_PINNED_VERSION = "6.5.0";
|
|
@@ -67,10 +67,11 @@ function reaMcpEnv(config) {
|
|
|
67
67
|
* 与 settings 用户层(GUI 配置卡片写入 profile 行级覆盖层,loader 原位提交活动
|
|
68
68
|
* 引用,消费端 `unwrapVolatile` 现取即热)。
|
|
69
69
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
70
|
+
* **活动字段(`.volatile()`)共五个**,全部在配置卡片里有编辑器(见 src/client/card.tsx):
|
|
71
|
+
* `personaPrefix`(提示词)、`proxyPort`(安装代理)、`mcpEnabled` / `skillEnabled` /
|
|
72
|
+
* `reverseSkillEnabled`(三个能力开关)——保存即热更(预设换代,新建会话生效)。
|
|
73
|
+
* 其余字段是**部署方平面**(受管目录、版本钉、skill 组、超时等运维旋钮),由
|
|
74
|
+
* `cordis.patch.yml` 整体覆盖,GUI 不提供入口。
|
|
74
75
|
*
|
|
75
76
|
* **persona 默认为空串**:`dsh-persona` 的空模板照样遮蔽部署级 persona 段,
|
|
76
77
|
* 渲染为空后在提示词里消失;官方装配(harness 身份、工具指引、项目指令、
|
|
@@ -88,7 +89,12 @@ const Config = z.object({
|
|
|
88
89
|
description: z.string().description("预设说明(设置 → Agent 预设卡片描述)").default("逆向工程预设:官方 standard 全量行 + 仅本预设可见的 REA MCP 客户端(mcp__rea__*)与随包 REA 官方 skill;persona 默认为空,工具指引沿用官方装配。"),
|
|
89
90
|
order: z.number().description("预设名册排序位").default(6),
|
|
90
91
|
personaPrefix: z.string().description("persona 前缀(system-prompt 的 deployment:persona-prefix 段);留空 = 空 persona(该段渲染为空后消失),工具指引与运行时上下文保留").default("").volatile(),
|
|
91
|
-
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"]),
|
|
92
98
|
mcpServerName: z.string().pattern(/^[A-Za-z0-9_-]{1,32}$/).description("MCP 服务命名空间:模型面工具名为 mcp__<serverName>__<tool>,取值 [A-Za-z0-9_-]{1,32}").default("rea"),
|
|
93
99
|
reaVersion: z.string().description("钉版的 rea-agents 版本(npx 拉取)").default(REA_PINNED_VERSION),
|
|
94
100
|
installRoot: z.string().description("REA 受管安装树位置(空 = <DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/rea-agent)").default(""),
|
|
@@ -128,6 +134,392 @@ Make the plan decision-complete: state the goal and success criteria; group impl
|
|
|
128
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.
|
|
129
135
|
`;
|
|
130
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
|
|
131
523
|
//#region src/skill.ts
|
|
132
524
|
/**
|
|
133
525
|
* 预设作用域内的 skill 根解析:随包 REA 官方 skill(或手动更新副本)+ 配置追加目录。
|
|
@@ -147,12 +539,28 @@ When ready, call exit_plan_mode with the complete plan markdown, starting with a
|
|
|
147
539
|
*/
|
|
148
540
|
const BUNDLED_SKILL_ROOT = fileURLToPath(new URL("../skills", import.meta.url));
|
|
149
541
|
/**
|
|
150
|
-
* skill
|
|
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` 是部署方追加项,不受开关影响(关掉的是本插件自带的两个源)。
|
|
151
553
|
* @param config - 插件配置。
|
|
152
|
-
* @param
|
|
554
|
+
* @param runtime - 现探运行期事实。
|
|
153
555
|
*/
|
|
154
|
-
function
|
|
155
|
-
|
|
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
|
+
];
|
|
156
564
|
}
|
|
157
565
|
//#endregion
|
|
158
566
|
//#region src/definition.ts
|
|
@@ -183,7 +591,8 @@ function emptyRuntime() {
|
|
|
183
591
|
root: BUNDLED_SKILL_ROOT,
|
|
184
592
|
files: 0,
|
|
185
593
|
updatedAt: null
|
|
186
|
-
}
|
|
594
|
+
},
|
|
595
|
+
reverse: emptyReverseState()
|
|
187
596
|
};
|
|
188
597
|
}
|
|
189
598
|
/**
|
|
@@ -236,7 +645,7 @@ function baseRows(config, runtime) {
|
|
|
236
645
|
{
|
|
237
646
|
id: "skill-filesystem",
|
|
238
647
|
name: "@deepseek-ai/dsh-skill-filesystem",
|
|
239
|
-
config: { customSkillDirs:
|
|
648
|
+
config: { customSkillDirs: presetSkillDirs(config, runtime) }
|
|
240
649
|
},
|
|
241
650
|
{
|
|
242
651
|
id: "tool-skill",
|
|
@@ -405,114 +814,6 @@ function reaPresetDefinition(config, runtime = emptyRuntime()) {
|
|
|
405
814
|
};
|
|
406
815
|
}
|
|
407
816
|
//#endregion
|
|
408
|
-
//#region src/exec.ts
|
|
409
|
-
/**
|
|
410
|
-
* 外部命令执行面与作业原语:受管安装/更新作业要跑 `npm` / `tar`,这些调用全部经
|
|
411
|
-
* {@link CommandRunner} 注入,单测用假 runner 跑完整状态机(零网络、零 npm)。
|
|
412
|
-
*
|
|
413
|
-
* 作业阶段由 {@link JobTaskContext} 上报,日志行由 {@link JobOperation} 的调用方
|
|
414
|
-
* 收集;本模块只负责「怎么跑命令」与「怎么把输出变成日志行」。
|
|
415
|
-
* @module @dsh-plus/agent-preset-rea/exec
|
|
416
|
-
*/
|
|
417
|
-
/** 版本字符串形状:进 shell 前把用户可配项收窄成纯版本号。 */
|
|
418
|
-
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
419
|
-
/** npm 命令名(Windows 经 shell 执行,见 {@link spawnRunner} 注释)。 */
|
|
420
|
-
const NPM = "npm";
|
|
421
|
-
/** 作业取消(AbortSignal 触发后由 runner 抛出)。 */
|
|
422
|
-
var JobCancelled = class extends Error {
|
|
423
|
-
constructor() {
|
|
424
|
-
super("作业已取消");
|
|
425
|
-
this.name = "JobCancelled";
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
/** 实际 runner:spawn 子进程、逐行回调、AbortSignal 触发即杀。 */
|
|
429
|
-
const spawnRunner = { run: (command, args, options) => new Promise((resolve, reject) => {
|
|
430
|
-
const child = spawn(command, args, {
|
|
431
|
-
env: options.env,
|
|
432
|
-
shell: process.platform === "win32",
|
|
433
|
-
stdio: [
|
|
434
|
-
"ignore",
|
|
435
|
-
"pipe",
|
|
436
|
-
"pipe"
|
|
437
|
-
]
|
|
438
|
-
});
|
|
439
|
-
let pending = "";
|
|
440
|
-
const emit = (chunk) => {
|
|
441
|
-
pending += chunk;
|
|
442
|
-
const lines = pending.split("\n");
|
|
443
|
-
pending = lines.pop() ?? "";
|
|
444
|
-
for (const line of lines) options.onLine(line.trimEnd());
|
|
445
|
-
};
|
|
446
|
-
child.stdout.setEncoding("utf8");
|
|
447
|
-
child.stderr.setEncoding("utf8");
|
|
448
|
-
child.stdout.on("data", emit);
|
|
449
|
-
child.stderr.on("data", emit);
|
|
450
|
-
const abort = () => {
|
|
451
|
-
child.kill("SIGTERM");
|
|
452
|
-
};
|
|
453
|
-
options.signal.addEventListener("abort", abort, { once: true });
|
|
454
|
-
child.on("error", reject);
|
|
455
|
-
child.on("close", (code) => {
|
|
456
|
-
options.signal.removeEventListener("abort", abort);
|
|
457
|
-
if (pending !== "") options.onLine(pending.trimEnd());
|
|
458
|
-
if (options.signal.aborted) reject(new JobCancelled());
|
|
459
|
-
else resolve({ code: code ?? 1 });
|
|
460
|
-
});
|
|
461
|
-
}) };
|
|
462
|
-
/**
|
|
463
|
-
* npm 子进程环境:继承宿主环境,附带代理端口(port > 0 时同时给 npm 与
|
|
464
|
-
* prebuild-install 注入代理——后者只认 `https_proxy` 系列环境变量,
|
|
465
|
-
* 这是原生依赖 prebuild 下载卡死的正解)。
|
|
466
|
-
*/
|
|
467
|
-
function npmEnv(config) {
|
|
468
|
-
const env = {};
|
|
469
|
-
for (const [key, value] of Object.entries(process.env)) if (value !== void 0) env[key] = value;
|
|
470
|
-
env["npm_config_audit"] = "false";
|
|
471
|
-
env["npm_config_fund"] = "false";
|
|
472
|
-
env["npm_config_update_notifier"] = "false";
|
|
473
|
-
env["npm_config_loglevel"] = "http";
|
|
474
|
-
if (config.proxyPort > 0) {
|
|
475
|
-
const proxy = `http://127.0.0.1:${config.proxyPort}`;
|
|
476
|
-
env["HTTP_PROXY"] = proxy;
|
|
477
|
-
env["HTTPS_PROXY"] = proxy;
|
|
478
|
-
env["http_proxy"] = proxy;
|
|
479
|
-
env["https_proxy"] = proxy;
|
|
480
|
-
env["npm_config_proxy"] = proxy;
|
|
481
|
-
env["npm_config_https_proxy"] = proxy;
|
|
482
|
-
}
|
|
483
|
-
return env;
|
|
484
|
-
}
|
|
485
|
-
/** 版本收窄:进命令前把配置里的钉版校验成纯版本号。 */
|
|
486
|
-
function safeVersion(version) {
|
|
487
|
-
const trimmed = version.trim();
|
|
488
|
-
if (!VERSION_PATTERN.test(trimmed)) throw new Error(`配置里的 reaVersion 不是合法版本号:${JSON.stringify(version)}`);
|
|
489
|
-
return trimmed;
|
|
490
|
-
}
|
|
491
|
-
/** 逐行转发子进程输出到作业日志(截断超长行)。 */
|
|
492
|
-
function lineSink(ctx) {
|
|
493
|
-
return (line) => {
|
|
494
|
-
if (line !== "") ctx.log(line.slice(0, 500));
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
/** 递归列出目录下全部文件(相对 POSIX 路径,排序稳定)。 */
|
|
498
|
-
function listFiles(dir, base = "") {
|
|
499
|
-
const out = [];
|
|
500
|
-
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
501
|
-
const rel = base === "" ? entry.name : `${base}/${entry.name}`;
|
|
502
|
-
if (entry.isDirectory()) out.push(...listFiles(join(dir, entry.name), rel));
|
|
503
|
-
else out.push(rel);
|
|
504
|
-
}
|
|
505
|
-
return out;
|
|
506
|
-
}
|
|
507
|
-
/** 目录下文件数(目录缺席或不可读按 0;状态展示用,不抛)。 */
|
|
508
|
-
function countFiles(dir) {
|
|
509
|
-
try {
|
|
510
|
-
return listFiles(dir).length;
|
|
511
|
-
} catch {
|
|
512
|
-
return 0;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
//#endregion
|
|
516
817
|
//#region src/jobs.ts
|
|
517
818
|
/** 阶段序号 → 进度(进入该阶段即报该阶段起点百分比)。 */
|
|
518
819
|
function progressOf(operation, step) {
|
|
@@ -740,7 +1041,8 @@ function probeRuntime(config, bundledRoot, bundledVersion) {
|
|
|
740
1041
|
const paths = managerPaths(config);
|
|
741
1042
|
return {
|
|
742
1043
|
install: probeInstall(paths),
|
|
743
|
-
skill: probeSkill(paths, bundledRoot, bundledVersion)
|
|
1044
|
+
skill: probeSkill(paths, bundledRoot, bundledVersion),
|
|
1045
|
+
reverse: probeReverse(config)
|
|
744
1046
|
};
|
|
745
1047
|
}
|
|
746
1048
|
/**
|
|
@@ -950,6 +1252,10 @@ function createManager(options) {
|
|
|
950
1252
|
skill: {
|
|
951
1253
|
phases: SKILL_PHASES,
|
|
952
1254
|
run: (ctx) => runSkillJob(deps, ctx)
|
|
1255
|
+
},
|
|
1256
|
+
reverse: {
|
|
1257
|
+
phases: REVERSE_PHASES,
|
|
1258
|
+
run: (ctx) => runReverseSkillJob(deps, ctx)
|
|
953
1259
|
}
|
|
954
1260
|
};
|
|
955
1261
|
}
|
|
@@ -1039,11 +1345,18 @@ function statusPayload(options) {
|
|
|
1039
1345
|
const paths = options.manager.paths();
|
|
1040
1346
|
const launch = reaMcpLaunch(config, runtime.install);
|
|
1041
1347
|
const mode = config.mcpCommand.trim() !== "" ? "custom" : launch === null ? "missing" : "installed";
|
|
1348
|
+
const reverse = reversePaths();
|
|
1349
|
+
const activeDirs = reverseSkillDirs(config, runtime.reverse);
|
|
1042
1350
|
return {
|
|
1043
1351
|
config: {
|
|
1044
1352
|
reaVersion: config.reaVersion,
|
|
1045
1353
|
proxyPort: config.proxyPort,
|
|
1046
1354
|
mcpEnabled: config.mcpEnabled,
|
|
1355
|
+
skillEnabled: config.skillEnabled,
|
|
1356
|
+
reverseSkillEnabled: config.reverseSkillEnabled,
|
|
1357
|
+
reverseSkillRepo: config.reverseSkillRepo,
|
|
1358
|
+
reverseSkillRef: config.reverseSkillRef,
|
|
1359
|
+
reverseSkillGroups: [...config.reverseSkillGroups],
|
|
1047
1360
|
installRoot: config.installRoot
|
|
1048
1361
|
},
|
|
1049
1362
|
install: {
|
|
@@ -1053,39 +1366,71 @@ function statusPayload(options) {
|
|
|
1053
1366
|
entry: runtime.install?.entry ?? null
|
|
1054
1367
|
},
|
|
1055
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
|
+
},
|
|
1056
1382
|
mcp: {
|
|
1057
1383
|
mode,
|
|
1058
1384
|
enabled: config.mcpEnabled && launch !== null,
|
|
1059
1385
|
command: launch?.command ?? null,
|
|
1060
1386
|
args: launch?.args ?? []
|
|
1061
1387
|
},
|
|
1062
|
-
paths
|
|
1388
|
+
paths: {
|
|
1389
|
+
...paths,
|
|
1390
|
+
reverseRoot: reverse.root
|
|
1391
|
+
},
|
|
1063
1392
|
job: options.board.snapshot()
|
|
1064
1393
|
};
|
|
1065
1394
|
}
|
|
1066
|
-
/**
|
|
1067
|
-
function
|
|
1068
|
-
|
|
1069
|
-
const send = (res, status, body) => {
|
|
1395
|
+
/** 发送 JSON 响应。 */
|
|
1396
|
+
function makeSend() {
|
|
1397
|
+
return (res, status, body) => {
|
|
1070
1398
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
1071
1399
|
res.end(JSON.stringify(body));
|
|
1072
1400
|
};
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
}
|
|
1086
|
-
|
|
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);
|
|
1087
1429
|
};
|
|
1088
|
-
|
|
1430
|
+
}
|
|
1431
|
+
/** 起作业的处理器工厂(409 = 已有作业在跑)。 */
|
|
1432
|
+
function startJobHandler(kind, options, logger, send) {
|
|
1433
|
+
return async (req, res) => {
|
|
1089
1434
|
const overrides = proxyPortOf(await readBody(req));
|
|
1090
1435
|
const operations = options.manager.operations(overrides === void 0 ? void 0 : { proxyPort: overrides });
|
|
1091
1436
|
const job = options.board.start(kind, operations[kind]);
|
|
@@ -1099,65 +1444,34 @@ function registerReaRoutes(ctx, options) {
|
|
|
1099
1444
|
logger.info(`已启动 ${kind} 作业`);
|
|
1100
1445
|
send(res, 200, { job });
|
|
1101
1446
|
};
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
send(res, 405, {
|
|
1108
|
-
job: null,
|
|
1109
|
-
error: "method not allowed"
|
|
1110
|
-
});
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1447
|
+
}
|
|
1448
|
+
/** 全部路由(path 末段 → 处理器)。 */
|
|
1449
|
+
function buildRoutes(options, logger, send) {
|
|
1450
|
+
return {
|
|
1451
|
+
status: withMethod("GET", send, async (_req, res) => {
|
|
1113
1452
|
send(res, 200, statusPayload(options));
|
|
1114
|
-
})
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
handler: guard(async (req, res) => {
|
|
1120
|
-
if (req.method !== "POST") {
|
|
1121
|
-
send(res, 405, {
|
|
1122
|
-
job: null,
|
|
1123
|
-
error: "method not allowed"
|
|
1124
|
-
});
|
|
1125
|
-
return;
|
|
1126
|
-
}
|
|
1127
|
-
await startJob(req, res, "install");
|
|
1128
|
-
})
|
|
1129
|
-
});
|
|
1130
|
-
ctx.webServer.register({
|
|
1131
|
-
kind: "exact",
|
|
1132
|
-
path: `${ROUTE_BASE}/skill`,
|
|
1133
|
-
handler: guard(async (req, res) => {
|
|
1134
|
-
if (req.method !== "POST") {
|
|
1135
|
-
send(res, 405, {
|
|
1136
|
-
job: null,
|
|
1137
|
-
error: "method not allowed"
|
|
1138
|
-
});
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
await startJob(req, res, "skill");
|
|
1142
|
-
})
|
|
1143
|
-
});
|
|
1144
|
-
ctx.webServer.register({
|
|
1145
|
-
kind: "exact",
|
|
1146
|
-
path: `${ROUTE_BASE}/cancel`,
|
|
1147
|
-
handler: guard(async (req, res) => {
|
|
1148
|
-
if (req.method !== "POST") {
|
|
1149
|
-
send(res, 405, {
|
|
1150
|
-
job: null,
|
|
1151
|
-
error: "method not allowed"
|
|
1152
|
-
});
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
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) => {
|
|
1155
1458
|
const cancelled = options.board.cancel();
|
|
1156
1459
|
send(res, 200, {
|
|
1157
1460
|
job: options.board.snapshot(),
|
|
1158
1461
|
...cancelled ? {} : { error: "没有正在运行的作业" }
|
|
1159
1462
|
});
|
|
1160
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)
|
|
1161
1475
|
});
|
|
1162
1476
|
}
|
|
1163
1477
|
//#endregion
|
|
@@ -1221,4 +1535,4 @@ function apply(ctx, config) {
|
|
|
1221
1535
|
});
|
|
1222
1536
|
}
|
|
1223
1537
|
//#endregion
|
|
1224
|
-
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, ROUTE_BASE, SETTINGS_NS, SKILL_PHASES, apply, configWith, createJobBoard, createManager, createPresetRegistrar, emptyRuntime, inject, managerPaths, name, npmEnv, probeInstall, probeRuntime, probeSkill, reaMcpArgs, reaMcpEnv, reaMcpLaunch, reaPresetDefinition, reaSkillDirs, registerReaRoutes, safeVersion, spawnRunner, statusPayload };
|
|
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 };
|