@dsh-plus/agent-preset-rea 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +1197 -0
- package/lib/index.d.ts +431 -0
- package/lib/index.js +1224 -0
- package/package.json +61 -3
- package/skills/reverse-engineer-anything/LICENSE +21 -0
- package/skills/reverse-engineer-anything/SKILL.md +342 -0
- package/skills/reverse-engineer-anything/references/android-applications.md +34 -0
- package/skills/reverse-engineer-anything/references/connection-and-recovery.md +127 -0
- package/skills/reverse-engineer-anything/references/evidence-workflows.md +71 -0
- package/skills/reverse-engineer-anything/references/javascript-applications.md +175 -0
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +277 -0
- package/skills/reverse-engineer-anything/references/runtime-observation.md +78 -0
- package/src/client/api.ts +76 -0
- package/src/client/card.tsx +145 -0
- package/src/client/client.ts +51 -0
- package/src/client/draft.ts +55 -0
- package/src/client/i18n.ts +134 -0
- package/src/client/install.tsx +274 -0
- package/src/client/styles.ts +57 -0
- package/src/config.ts +121 -0
- package/src/definition.ts +245 -0
- package/src/dto.ts +87 -0
- package/src/exec.ts +155 -0
- package/src/index.ts +186 -0
- package/src/jobs.ts +145 -0
- package/src/manager.ts +419 -0
- package/src/mcp.ts +83 -0
- package/src/ns.ts +9 -0
- package/src/prompt.ts +25 -0
- package/src/registration.ts +86 -0
- package/src/routes.ts +181 -0
- package/src/skill.ts +34 -0
- package/README.md +0 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,1224 @@
|
|
|
1
|
+
import { pluginDataPath, unwrapVolatile } from "@dsh-plus/shared";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
9
|
+
//#region src/mcp.ts
|
|
10
|
+
/** 钉版基线:`rea-agents@6.5.0`(升级时同步测试断言、tests 清单与包文档)。 */
|
|
11
|
+
const REA_PINNED_VERSION = "6.5.0";
|
|
12
|
+
/** 显式命令配 npx 时的参数配方基准值(`mcpArgs` 留空即按它派生)。 */
|
|
13
|
+
const DEFAULT_MCP_COMMAND = "npx";
|
|
14
|
+
/** REA 工具入参 schema 广告档位:`compact` 压请求前缀体积,`full` 与上游默认一致。 */
|
|
15
|
+
const INPUT_SCHEMA_PROFILES = ["compact", "full"];
|
|
16
|
+
/** REA 工具出参 schema 广告档位:`off` 不下发(结果仍由 REA 侧校验),`full` 与上游默认一致。 */
|
|
17
|
+
const OUTPUT_SCHEMA_PROFILES = ["off", "full"];
|
|
18
|
+
/**
|
|
19
|
+
* 显式命令(`mcpCommand` 非空)时的参数:`mcpArgs` 优先;
|
|
20
|
+
* 留空时 npx 走 `-y rea-agents@<钉版> mcp`,其他可执行文件只补 `mcp`。
|
|
21
|
+
*/
|
|
22
|
+
function reaMcpArgs(config) {
|
|
23
|
+
if (config.mcpArgs.length > 0) return [...config.mcpArgs];
|
|
24
|
+
return config.mcpCommand === "npx" ? [
|
|
25
|
+
"-y",
|
|
26
|
+
`rea-agents@${config.reaVersion}`,
|
|
27
|
+
"mcp"
|
|
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
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 传给 REA 子进程的环境变量(并入被清洗过的宿主环境之上)。
|
|
50
|
+
*
|
|
51
|
+
* REA 有 207 个工具(`server.json` 同版的 skill 元数据 `tool_count`),工具定义
|
|
52
|
+
* 是每轮请求的固定开销,因此两个 schema 档位默认都取最省的一档;REA 自己仍按
|
|
53
|
+
* 规范契约校验结果,`full` 可逐项还原上游默认。
|
|
54
|
+
*/
|
|
55
|
+
function reaMcpEnv(config) {
|
|
56
|
+
return {
|
|
57
|
+
REA_MCP_INPUT_SCHEMA_PROFILE: config.inputSchemaProfile,
|
|
58
|
+
REA_MCP_OUTPUT_SCHEMA_PROFILE: config.outputSchemaProfile
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/config.ts
|
|
63
|
+
/**
|
|
64
|
+
* 插件配置(schemastery):显示面、persona、REA MCP 行与 skill 根外部化。
|
|
65
|
+
*
|
|
66
|
+
* 单一事实源同时充当两处:cordis 行级 `Config`(组合默认,profile patch 层可覆盖)
|
|
67
|
+
* 与 settings 用户层(GUI 配置卡片写入 profile 行级覆盖层,loader 原位提交活动
|
|
68
|
+
* 引用,消费端 `unwrapVolatile` 现取即热)。
|
|
69
|
+
*
|
|
70
|
+
* **活动字段只有两个**:`personaPrefix` 与 `proxyPort`——两者都在配置卡片里有编辑器
|
|
71
|
+
* (见 src/client/card.tsx),保存即热更(预设换代 / 安装作业即时生效)。其余字段是
|
|
72
|
+
* **部署方平面**(安装位置、超时、附加 skill 根等运维旋钮),由 `cordis.patch.yml`
|
|
73
|
+
* 整体覆盖,GUI 不提供入口。
|
|
74
|
+
*
|
|
75
|
+
* **persona 默认为空串**:`dsh-persona` 的空模板照样遮蔽部署级 persona 段,
|
|
76
|
+
* 渲染为空后在提示词里消失;官方装配(harness 身份、工具指引、项目指令、
|
|
77
|
+
* 运行时上下文)不受影响。
|
|
78
|
+
*
|
|
79
|
+
* 预设 id 固定为 `rea`(注册表身份,不开放覆盖——改 id 等于换预设,
|
|
80
|
+
* default/selectedDefault 等引用都会落空)。
|
|
81
|
+
* @module @dsh-plus/agent-preset-rea/config
|
|
82
|
+
*/
|
|
83
|
+
/** settings 命名空间(字面量即合法命名空间;配置卡片与运行期读取同一份)。 */
|
|
84
|
+
const SETTINGS_NS = "dsh-plus-agent-preset-rea";
|
|
85
|
+
const Config = z.object({
|
|
86
|
+
enabled: z.boolean().description("总开关(false = 不注册 rea 预设,等价插件未安装)").default(true),
|
|
87
|
+
name: z.string().description("预设显示名(设置 → Agent 预设卡片标题)").default("逆向工程"),
|
|
88
|
+
description: z.string().description("预设说明(设置 → Agent 预设卡片描述)").default("逆向工程预设:官方 standard 全量行 + 仅本预设可见的 REA MCP 客户端(mcp__rea__*)与随包 REA 官方 skill;persona 默认为空,工具指引沿用官方装配。"),
|
|
89
|
+
order: z.number().description("预设名册排序位").default(6),
|
|
90
|
+
personaPrefix: z.string().description("persona 前缀(system-prompt 的 deployment:persona-prefix 段);留空 = 空 persona(该段渲染为空后消失),工具指引与运行时上下文保留").default("").volatile(),
|
|
91
|
+
mcpEnabled: z.boolean().description("是否挂 REA MCP 客户端行(false = 只保留预设其余能力)").default(true),
|
|
92
|
+
mcpServerName: z.string().pattern(/^[A-Za-z0-9_-]{1,32}$/).description("MCP 服务命名空间:模型面工具名为 mcp__<serverName>__<tool>,取值 [A-Za-z0-9_-]{1,32}").default("rea"),
|
|
93
|
+
reaVersion: z.string().description("钉版的 rea-agents 版本(npx 拉取)").default(REA_PINNED_VERSION),
|
|
94
|
+
installRoot: z.string().description("REA 受管安装树位置(空 = <DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/rea-agent)").default(""),
|
|
95
|
+
proxyPort: z.natural().max(65535).description("安装/更新时使用的本机代理端口(0 = 不用代理;npm 与原生依赖 prebuild 下载同此)").default(0).volatile(),
|
|
96
|
+
mcpCommand: z.string().description("显式 MCP 启动命令(空 = 用受管安装入口,未安装时该行禁用;填 npx 即启动期拉取,风险自负)").default(""),
|
|
97
|
+
mcpArgs: z.array(z.string()).description("MCP stdio 附加参数;留空 = 受管入口补 [\"mcp\"],显式 npx 则派生 [\"-y\",\"rea-agents@<version>\",\"mcp\"]").default([]),
|
|
98
|
+
inputSchemaProfile: z.union(INPUT_SCHEMA_PROFILES).description("REA 工具入参 schema 广告档位(REA_MCP_INPUT_SCHEMA_PROFILE):compact 压体积,full 与上游默认一致").default("compact"),
|
|
99
|
+
outputSchemaProfile: z.union(OUTPUT_SCHEMA_PROFILES).description("REA 工具出参 schema 广告档位(REA_MCP_OUTPUT_SCHEMA_PROFILE):off 不下发(结果仍由 REA 校验),full 与上游默认一致").default("off"),
|
|
100
|
+
toolCallTimeoutMs: z.number().description("单次 MCP 工具调用超时(毫秒);REA 分析类调用常超默认 60 秒").default(6e5),
|
|
101
|
+
failOnStartupError: z.boolean().description("MCP 初次连接/同步失败是否让本行激活失败;默认 false:只记日志并按退避重连,不拖垮 boot").default(false),
|
|
102
|
+
extraSkillDirs: z.array(z.string()).description("附加 skill 根目录(随本预设作用域生效,其他预设不可见)").default([])
|
|
103
|
+
});
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/prompt.ts
|
|
106
|
+
/**
|
|
107
|
+
* 本包随包携带的上游提示词文本:`standard` 预设 `plan-mode` 行的 section 原文。
|
|
108
|
+
*
|
|
109
|
+
* `dsh-plan-mode` 要求 `config.section` 非空(部署方所有权),因此该文本必须随包;
|
|
110
|
+
* 逐字相等由 tests/upstream-mirror.test.ts 对着上游夹具守卫,上游一改即失败。
|
|
111
|
+
*
|
|
112
|
+
* persona 不在本文件:本预设的 persona 前缀默认为**空串**(见 config.ts 的
|
|
113
|
+
* `personaPrefix`),不随包任何自有 persona 文案;官方工具指引、项目指令
|
|
114
|
+
* (AGENTS.md)与运行时上下文照常装配。
|
|
115
|
+
* @module @dsh-plus/agent-preset-rea/prompt
|
|
116
|
+
*/
|
|
117
|
+
/** 上游 `standard` 预设 `plan-mode` 行的 section 原文(逐字,勿手改)。 */
|
|
118
|
+
const PLAN_MODE_SECTION = `You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
119
|
+
|
|
120
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
121
|
+
|
|
122
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
123
|
+
|
|
124
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
125
|
+
|
|
126
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
127
|
+
|
|
128
|
+
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
|
+
`;
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/skill.ts
|
|
132
|
+
/**
|
|
133
|
+
* 预设作用域内的 skill 根解析:随包 REA 官方 skill(或手动更新副本)+ 配置追加目录。
|
|
134
|
+
*
|
|
135
|
+
* 全部只加在本预设 `skill-filesystem` 行的 `customSkillDirs`(rank 300 的**追加**根)
|
|
136
|
+
* 上,因此不进入项目根 / 用户根 / `$DSH_BUNDLED_SKILL_DIR`,其他预设不受影响。
|
|
137
|
+
*
|
|
138
|
+
* 手动更新副本存在时**替代**随包根而不是并排:两者同名(`reverse-engineer-anything`),
|
|
139
|
+
* 并排只会得到「哪个生效」的歧义。
|
|
140
|
+
* @module @dsh-plus/agent-preset-rea/skill
|
|
141
|
+
*/
|
|
142
|
+
/**
|
|
143
|
+
* 随包 REA 官方 skill 根(包内 `skills/`)。
|
|
144
|
+
*
|
|
145
|
+
* `src/` 与构建产物 `lib/` 同为包根下一级,`../skills` 在源码直跑测试与安装后
|
|
146
|
+
* 运行产物两种位置下解析到同一目录。
|
|
147
|
+
*/
|
|
148
|
+
const BUNDLED_SKILL_ROOT = fileURLToPath(new URL("../skills", import.meta.url));
|
|
149
|
+
/**
|
|
150
|
+
* skill-filesystem 的追加根清单。
|
|
151
|
+
* @param config - 插件配置。
|
|
152
|
+
* @param updatedRoot - 手动更新副本根(存在即替代随包根;来自 manager 现探)。
|
|
153
|
+
*/
|
|
154
|
+
function reaSkillDirs(config, updatedRoot = null) {
|
|
155
|
+
return [...updatedRoot === null ? [BUNDLED_SKILL_ROOT] : [updatedRoot], ...config.extraSkillDirs];
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/definition.ts
|
|
159
|
+
/** 预设注册表身份(改 id 等于换预设,default 等引用都会落空)。 */
|
|
160
|
+
const REA_PRESET_ID = "rea";
|
|
161
|
+
/** 相对上游 `standard` 新增的行 id(镜像测试按此白名单放行自造行)。 */
|
|
162
|
+
const ADDED_ROWS = ["mcp-rea"];
|
|
163
|
+
/**
|
|
164
|
+
* 已镜像行 → 允许偏离上游的字段名(其余字段必须逐字一致)。
|
|
165
|
+
* 删除某条等于把该行改回上游原文;改动上游不存在的字段会被测试拦下。
|
|
166
|
+
*/
|
|
167
|
+
const CONFIG_DELTAS = {
|
|
168
|
+
persona: ["prefix"],
|
|
169
|
+
"skill-filesystem": ["customSkillDirs"]
|
|
170
|
+
};
|
|
171
|
+
/** 平台门控表达式:与上游 `standard` 预设逐字相同,Windows 上由 pwsh 顶替 bash。 */
|
|
172
|
+
const WINDOWS_ONLY = { __jsExpr: "process.platform === 'win32'" };
|
|
173
|
+
const POSIX_ONLY = { __jsExpr: "process.platform !== 'win32'" };
|
|
174
|
+
/** 官方 loader 的嵌套组行名。 */
|
|
175
|
+
const GROUP = "cordis:group";
|
|
176
|
+
/** 未安装、随包 skill 的运行期事实(默认值:测试与无 manager 场景)。 */
|
|
177
|
+
function emptyRuntime() {
|
|
178
|
+
return {
|
|
179
|
+
install: null,
|
|
180
|
+
skill: {
|
|
181
|
+
source: "bundled",
|
|
182
|
+
version: null,
|
|
183
|
+
root: BUNDLED_SKILL_ROOT,
|
|
184
|
+
files: 0,
|
|
185
|
+
updatedAt: null
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* 基础与工具面行:身份(persona)/ 项目指令与时钟 / shell 与文件工具 / 后台任务与
|
|
191
|
+
* 定时提醒 / skill 文件系统(本预设加 `customSkillDirs`,可指向手动更新副本)。
|
|
192
|
+
*/
|
|
193
|
+
function baseRows(config, runtime) {
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
id: "persona",
|
|
197
|
+
name: "@deepseek-ai/dsh-persona",
|
|
198
|
+
config: { prefix: config.personaPrefix }
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "agent-instructions",
|
|
202
|
+
name: "@deepseek-ai/dsh-agent-instructions",
|
|
203
|
+
config: { maxBytes: 65536 }
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "time-context",
|
|
207
|
+
name: "@deepseek-ai/dsh-time-context"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: "tool-bash",
|
|
211
|
+
name: "@deepseek-ai/dsh-tool-bash",
|
|
212
|
+
disabled: WINDOWS_ONLY
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "tool-pwsh",
|
|
216
|
+
name: "@deepseek-ai/dsh-tool-pwsh",
|
|
217
|
+
disabled: POSIX_ONLY
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "tool-fs",
|
|
221
|
+
name: "@deepseek-ai/dsh-tool-fs"
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: "tool-fs-search",
|
|
225
|
+
name: "@deepseek-ai/dsh-tool-fs-search",
|
|
226
|
+
config: { sampleOverCapGlobResults: false }
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "tool-jobs",
|
|
230
|
+
name: "@deepseek-ai/dsh-tool-jobs"
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "tool-schedule",
|
|
234
|
+
name: "@deepseek-ai/dsh-tool-schedule"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "skill-filesystem",
|
|
238
|
+
name: "@deepseek-ai/dsh-skill-filesystem",
|
|
239
|
+
config: { customSkillDirs: reaSkillDirs(config, runtime.skill.source === "updated" ? runtime.skill.root : null) }
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "tool-skill",
|
|
243
|
+
name: "@deepseek-ai/dsh-tool-skill"
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "command-goal",
|
|
247
|
+
name: "@deepseek-ai/dsh-command-goal"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
id: "tool-goal",
|
|
251
|
+
name: "@deepseek-ai/dsh-tool-goal"
|
|
252
|
+
}
|
|
253
|
+
];
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* 编排与交付面行:计划模式 / 上下文压缩 / 委派与 workflow / 交互与待办 / 网络工具 /
|
|
257
|
+
* **REA MCP 客户端** / 展示与插件管理。
|
|
258
|
+
*/
|
|
259
|
+
function orchestrationRows(config, runtime) {
|
|
260
|
+
const launch = reaMcpLaunch(config, runtime.install);
|
|
261
|
+
const mcp = launch ?? {
|
|
262
|
+
command: "npx",
|
|
263
|
+
args: reaMcpArgs({
|
|
264
|
+
...config,
|
|
265
|
+
mcpCommand: "npx"
|
|
266
|
+
})
|
|
267
|
+
};
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
id: "planning",
|
|
271
|
+
name: GROUP,
|
|
272
|
+
group: true,
|
|
273
|
+
isolate: { planMode: true },
|
|
274
|
+
config: [{
|
|
275
|
+
id: "plan-mode",
|
|
276
|
+
name: "@deepseek-ai/dsh-plan-mode",
|
|
277
|
+
config: { section: PLAN_MODE_SECTION }
|
|
278
|
+
}]
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: "compaction",
|
|
282
|
+
name: GROUP,
|
|
283
|
+
group: true,
|
|
284
|
+
isolate: {
|
|
285
|
+
compaction: true,
|
|
286
|
+
toolResultPruner: true
|
|
287
|
+
},
|
|
288
|
+
config: [
|
|
289
|
+
{
|
|
290
|
+
id: "compaction-basic",
|
|
291
|
+
name: "@deepseek-ai/dsh-compaction-basic"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: "command-compact",
|
|
295
|
+
name: "@deepseek-ai/dsh-command-compact"
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
id: "tool-result-pruner",
|
|
299
|
+
name: "@deepseek-ai/dsh-compaction-tool-result-pruner",
|
|
300
|
+
config: {
|
|
301
|
+
thresholdChars: 8192,
|
|
302
|
+
headChars: 4096,
|
|
303
|
+
tailChars: 1024
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
id: "delegation",
|
|
310
|
+
name: GROUP,
|
|
311
|
+
group: true,
|
|
312
|
+
isolate: { workflowEngine: true },
|
|
313
|
+
config: [
|
|
314
|
+
{
|
|
315
|
+
id: "tool-subagent-control",
|
|
316
|
+
name: "@deepseek-ai/dsh-tool-subagent-control"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
id: "tool-subagent-list-agents",
|
|
320
|
+
name: "@deepseek-ai/dsh-tool-subagent-control/list-agents"
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: "tool-subagent",
|
|
324
|
+
name: "@deepseek-ai/dsh-tool-subagent",
|
|
325
|
+
config: {
|
|
326
|
+
provider: "spawn",
|
|
327
|
+
toolName: "subagent",
|
|
328
|
+
modelSelectionSettings: true
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
id: "tool-subagent-fork",
|
|
333
|
+
name: "@deepseek-ai/dsh-tool-subagent",
|
|
334
|
+
config: {
|
|
335
|
+
provider: "fork",
|
|
336
|
+
toolName: "subagent_fork"
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
id: "workflow-ptc",
|
|
341
|
+
name: "@deepseek-ai/dsh-workflow-ptc",
|
|
342
|
+
config: { provider: "spawn" }
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
id: "tool-workflow",
|
|
346
|
+
name: "@deepseek-ai/dsh-tool-workflow"
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: "tool-ask-user",
|
|
352
|
+
name: "@deepseek-ai/dsh-tool-ask-user"
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: "tool-todo",
|
|
356
|
+
name: "@deepseek-ai/dsh-tool-todo",
|
|
357
|
+
config: { allowParallelInProgress: true }
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
id: "tool-web",
|
|
361
|
+
name: "@deepseek-ai/dsh-tool-web",
|
|
362
|
+
config: {
|
|
363
|
+
fetch: true,
|
|
364
|
+
searchTimeoutMs: 6e4
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: "mcp-rea",
|
|
369
|
+
name: "@deepseek-ai/dsh-mcp-client",
|
|
370
|
+
disabled: !config.mcpEnabled || launch === null,
|
|
371
|
+
config: {
|
|
372
|
+
transport: "stdio",
|
|
373
|
+
serverName: config.mcpServerName,
|
|
374
|
+
command: mcp.command,
|
|
375
|
+
args: mcp.args,
|
|
376
|
+
env: reaMcpEnv(config),
|
|
377
|
+
toolCallTimeoutMs: config.toolCallTimeoutMs,
|
|
378
|
+
failOnStartupError: config.failOnStartupError
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
id: "present",
|
|
383
|
+
name: "@deepseek-ai/dsh-tool-present"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "tool-plugin-manager",
|
|
387
|
+
name: "@deepseek-ai/dsh-plugin-manager/tools",
|
|
388
|
+
disabled: true
|
|
389
|
+
}
|
|
390
|
+
];
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 由配置 + 运行期事实拼出注册表声明。
|
|
394
|
+
* @param config - 已验证的插件配置。
|
|
395
|
+
* @param runtime - 受管安装与 skill 来源事实(默认「未安装 + 随包 skill」)。
|
|
396
|
+
* @returns id 固定为 `rea` 的预设声明。
|
|
397
|
+
*/
|
|
398
|
+
function reaPresetDefinition(config, runtime = emptyRuntime()) {
|
|
399
|
+
return {
|
|
400
|
+
id: "rea",
|
|
401
|
+
name: config.name,
|
|
402
|
+
description: config.description,
|
|
403
|
+
order: config.order,
|
|
404
|
+
plugins: [...baseRows(config, runtime), ...orchestrationRows(config, runtime)]
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
//#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
|
+
//#region src/jobs.ts
|
|
517
|
+
/** 阶段序号 → 进度(进入该阶段即报该阶段起点百分比)。 */
|
|
518
|
+
function progressOf(operation, step) {
|
|
519
|
+
const steps = operation.phases.length;
|
|
520
|
+
return {
|
|
521
|
+
step: step + 1,
|
|
522
|
+
steps,
|
|
523
|
+
phase: operation.phases[step] ?? "",
|
|
524
|
+
percent: steps === 0 ? 0 : Math.round(step / steps * 100)
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** 落定:写状态 + 通知订阅者(订阅者异常不得影响状态机)。 */
|
|
528
|
+
function settleJob(state, patch) {
|
|
529
|
+
const base = state.current;
|
|
530
|
+
if (base === null) return;
|
|
531
|
+
state.current = {
|
|
532
|
+
...base,
|
|
533
|
+
...patch,
|
|
534
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
535
|
+
};
|
|
536
|
+
for (const listener of [...state.listeners]) try {
|
|
537
|
+
listener(state.current);
|
|
538
|
+
} catch {}
|
|
539
|
+
}
|
|
540
|
+
/** 兜底进度形状(正常路径下 `state.current` 必有值,这里只防类型上的缺席)。 */
|
|
541
|
+
const ZERO_PROGRESS = {
|
|
542
|
+
step: 1,
|
|
543
|
+
steps: 1,
|
|
544
|
+
phase: "",
|
|
545
|
+
percent: 0
|
|
546
|
+
};
|
|
547
|
+
/** 收尾:成功记 ok + 版本,失败区分「取消」与「真失败」。 */
|
|
548
|
+
function finishJob(state, error, version) {
|
|
549
|
+
if (version !== null) {
|
|
550
|
+
settleJob(state, {
|
|
551
|
+
state: "ok",
|
|
552
|
+
version,
|
|
553
|
+
progress: {
|
|
554
|
+
...state.current?.progress ?? ZERO_PROGRESS,
|
|
555
|
+
percent: 100
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const cancelled = error instanceof JobCancelled || state.abort?.signal.aborted === true;
|
|
561
|
+
settleJob(state, {
|
|
562
|
+
state: cancelled ? "cancelled" : "error",
|
|
563
|
+
error: cancelled ? null : error instanceof Error ? error.message : String(error)
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/** 起一个作业并挂上状态机(调用方已确认当前无运行中作业)。 */
|
|
567
|
+
function startJob(state, kind, operation) {
|
|
568
|
+
const controller = new AbortController();
|
|
569
|
+
state.abort = controller;
|
|
570
|
+
state.seq += 1;
|
|
571
|
+
const snapshot = {
|
|
572
|
+
id: state.seq,
|
|
573
|
+
kind,
|
|
574
|
+
state: "running",
|
|
575
|
+
progress: progressOf(operation, 0),
|
|
576
|
+
log: [],
|
|
577
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
578
|
+
finishedAt: null,
|
|
579
|
+
version: null,
|
|
580
|
+
error: null
|
|
581
|
+
};
|
|
582
|
+
state.current = snapshot;
|
|
583
|
+
const push = (patch) => {
|
|
584
|
+
if (state.current !== null) state.current = {
|
|
585
|
+
...state.current,
|
|
586
|
+
...patch
|
|
587
|
+
};
|
|
588
|
+
};
|
|
589
|
+
const ctx = {
|
|
590
|
+
phase: (step) => push({ progress: progressOf(operation, step) }),
|
|
591
|
+
log: (line) => push({ log: [...state.current?.log ?? [], line].slice(-200) }),
|
|
592
|
+
signal: controller.signal
|
|
593
|
+
};
|
|
594
|
+
operation.run(ctx).then((version) => finishJob(state, null, version), (error) => finishJob(state, error, null)).finally(() => {
|
|
595
|
+
state.abort = null;
|
|
596
|
+
});
|
|
597
|
+
return snapshot;
|
|
598
|
+
}
|
|
599
|
+
/** 取消当前作业。 */
|
|
600
|
+
function cancelJob(state) {
|
|
601
|
+
if (state.abort === null || state.current?.state !== "running") return false;
|
|
602
|
+
state.abort.abort();
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
/** 建一个作业板。 */
|
|
606
|
+
function createJobBoard() {
|
|
607
|
+
const state = {
|
|
608
|
+
seq: 0,
|
|
609
|
+
current: null,
|
|
610
|
+
abort: null,
|
|
611
|
+
listeners: /* @__PURE__ */ new Set()
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
start: (kind, operation) => state.current?.state === "running" ? null : startJob(state, kind, operation),
|
|
615
|
+
cancel: () => cancelJob(state),
|
|
616
|
+
snapshot: () => state.current,
|
|
617
|
+
onSettled: (listener) => {
|
|
618
|
+
state.listeners.add(listener);
|
|
619
|
+
return () => state.listeners.delete(listener);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/manager.ts
|
|
625
|
+
/**
|
|
626
|
+
* REA 的受管安装与状态探测:把「REA 包」与「REA skill」都变成插件自己管理的本机资源,
|
|
627
|
+
* 从而摆脱启动期 `npx -y` 拉取(那会在 boot 里跑 npm 安装脚本:原生依赖
|
|
628
|
+
* `@lydell/node-pty` 的 prebuild 下载一旦卡住,预设激活就不返回,名册与插件页
|
|
629
|
+
* 一起被吊住;事故经过见 docs/README.md「启动期拉取的教训」)。
|
|
630
|
+
*
|
|
631
|
+
* 落点(属插件数据,不进 settings,见 @dsh-plus/shared/plugin-data;插件数据目录名
|
|
632
|
+
* 取插件 id `dsh-plus-agent-preset-rea`):
|
|
633
|
+
* - REA 包:`<DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/rea-agent/`(npm --prefix
|
|
634
|
+
* 安装树;入口 `node_modules/rea-agents/scripts/rea.mjs`,用 `process.execPath` 直接
|
|
635
|
+
* spawn,跨平台不经 shell shim);
|
|
636
|
+
* - 手动更新的 skill:`<DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/skills/`
|
|
637
|
+
* (根下是 `<技能名>/` + `manifest.json`,与随包 `skills/` 同构;
|
|
638
|
+
* 清单完整时替代随包 skill 根)。
|
|
639
|
+
*
|
|
640
|
+
* 作业都是「先落 staging 再换目录」:中断只留下 staging,不会让半截安装树被当成可用
|
|
641
|
+
* 安装;换目录后清理旧树,运行中的旧进程按 POSIX 语义继续持有已打开文件。
|
|
642
|
+
*
|
|
643
|
+
* 外部进程经 {@link CommandRunner} 注入(见 exec.ts),单测用假 runner 跑完整状态机。
|
|
644
|
+
* @module @dsh-plus/agent-preset-rea/manager
|
|
645
|
+
*/
|
|
646
|
+
/** npm 包名。 */
|
|
647
|
+
const REA_PACKAGE = "rea-agents";
|
|
648
|
+
/** 受管安装树目录名。 */
|
|
649
|
+
const REA_INSTALL_DIR = "rea-agent";
|
|
650
|
+
/** 手动更新 skill 的目录名。 */
|
|
651
|
+
const REA_SKILL_DIR = "skills";
|
|
652
|
+
/** REA 随包 skill 名(tar 包内路径与目录名同此)。 */
|
|
653
|
+
const REA_SKILL_NAME = "reverse-engineer-anything";
|
|
654
|
+
/** 插件数据目录标识(`<DSH_HOME>/dsh-plus/<id>`)。 */
|
|
655
|
+
const PLUGIN_ID = "dsh-plus-agent-preset-rea";
|
|
656
|
+
/** 安装作业阶段名。 */
|
|
657
|
+
const INSTALL_PHASES = [
|
|
658
|
+
"解析版本信息",
|
|
659
|
+
`下载并安装 ${REA_PACKAGE}`,
|
|
660
|
+
"替换安装目录",
|
|
661
|
+
"校验安装"
|
|
662
|
+
];
|
|
663
|
+
/** skill 作业阶段名。 */
|
|
664
|
+
const SKILL_PHASES = [
|
|
665
|
+
"解析版本信息",
|
|
666
|
+
"下载 skill 包",
|
|
667
|
+
"解包 skills",
|
|
668
|
+
"校验并写入清单"
|
|
669
|
+
];
|
|
670
|
+
/**
|
|
671
|
+
* 解析受管路径:`installRoot` 非空即运维指定位置(部署方平面),
|
|
672
|
+
* 否则落插件数据目录;skill 更新副本固定落插件数据目录。
|
|
673
|
+
*/
|
|
674
|
+
function managerPaths(config) {
|
|
675
|
+
const root = config.installRoot.trim();
|
|
676
|
+
return {
|
|
677
|
+
installRoot: root === "" ? pluginDataPath(PLUGIN_ID, REA_INSTALL_DIR) : root,
|
|
678
|
+
skillRoot: pluginDataPath(PLUGIN_ID, REA_SKILL_DIR)
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/** 读 JSON(不存在或坏内容返回 null)。 */
|
|
682
|
+
function readJson(file) {
|
|
683
|
+
try {
|
|
684
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
685
|
+
} catch {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
/** 记录型守卫。 */
|
|
690
|
+
function asRecord(value) {
|
|
691
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
692
|
+
}
|
|
693
|
+
/** 安装树内固定入口:`node_modules/rea-agents/scripts/rea.mjs`。 */
|
|
694
|
+
function reaEntryFile(root) {
|
|
695
|
+
return join(root, "node_modules", REA_PACKAGE, "scripts", "rea.mjs");
|
|
696
|
+
}
|
|
697
|
+
/** 从清单 source(`rea-agents@1.2.3`)解析版本。 */
|
|
698
|
+
function versionOfSource(source) {
|
|
699
|
+
const prefix = `${REA_PACKAGE}@`;
|
|
700
|
+
return source.startsWith(prefix) ? source.slice(prefix.length) : null;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* 探测已安装 REA:包版本可读 **且** 入口脚本存在才算安装完整
|
|
704
|
+
* (只有一半的安装树、或换目录中断留下的 staging 都不算)。
|
|
705
|
+
*/
|
|
706
|
+
function probeInstall(paths) {
|
|
707
|
+
const pkg = asRecord(readJson(join(paths.installRoot, "node_modules", REA_PACKAGE, "package.json")));
|
|
708
|
+
const version = typeof pkg?.["version"] === "string" ? pkg["version"] : null;
|
|
709
|
+
const entry = reaEntryFile(paths.installRoot);
|
|
710
|
+
if (version === null || !existsSync(entry)) return null;
|
|
711
|
+
return {
|
|
712
|
+
version,
|
|
713
|
+
root: paths.installRoot,
|
|
714
|
+
command: process.execPath,
|
|
715
|
+
entry
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
/** 探测 skill 来源:数据目录里清单完整且技能文件在位才认更新副本。 */
|
|
719
|
+
function probeSkill(paths, bundledRoot, bundledVersion) {
|
|
720
|
+
const manifest = asRecord(readJson(join(paths.skillRoot, "manifest.json")));
|
|
721
|
+
const source = typeof manifest?.["source"] === "string" ? manifest["source"] : null;
|
|
722
|
+
const files = Array.isArray(manifest?.["files"]) ? manifest["files"].length : 0;
|
|
723
|
+
if (source !== null && files > 0 && existsSync(join(paths.skillRoot, "reverse-engineer-anything", "SKILL.md"))) return {
|
|
724
|
+
source: "updated",
|
|
725
|
+
version: versionOfSource(source),
|
|
726
|
+
root: paths.skillRoot,
|
|
727
|
+
files,
|
|
728
|
+
updatedAt: typeof manifest?.["updatedAt"] === "string" ? manifest["updatedAt"] : null
|
|
729
|
+
};
|
|
730
|
+
return {
|
|
731
|
+
source: "bundled",
|
|
732
|
+
version: bundledVersion,
|
|
733
|
+
root: bundledRoot,
|
|
734
|
+
files: countFiles(bundledRoot),
|
|
735
|
+
updatedAt: null
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
/** 现探运行期事实。 */
|
|
739
|
+
function probeRuntime(config, bundledRoot, bundledVersion) {
|
|
740
|
+
const paths = managerPaths(config);
|
|
741
|
+
return {
|
|
742
|
+
install: probeInstall(paths),
|
|
743
|
+
skill: probeSkill(paths, bundledRoot, bundledVersion)
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* 解析钉版在 registry 上的实际版本:顺带做存在性校验(失败即报错,
|
|
748
|
+
* 而不是装出一棵空树让 MCP 行指向不存在的入口)。
|
|
749
|
+
*/
|
|
750
|
+
async function resolveVersion(deps, ctx, wanted) {
|
|
751
|
+
ctx.phase(0);
|
|
752
|
+
const lines = [];
|
|
753
|
+
if ((await deps.runner.run("npm", [
|
|
754
|
+
"view",
|
|
755
|
+
`rea-agents@${wanted}`,
|
|
756
|
+
"version",
|
|
757
|
+
"--json"
|
|
758
|
+
], {
|
|
759
|
+
env: npmEnv(deps.config),
|
|
760
|
+
signal: ctx.signal,
|
|
761
|
+
onLine: (line) => {
|
|
762
|
+
lines.push(line);
|
|
763
|
+
lineSink(ctx)(line);
|
|
764
|
+
}
|
|
765
|
+
})).code !== 0) throw new Error(`registry 上找不到 ${REA_PACKAGE}@${wanted}`);
|
|
766
|
+
const reported = lines.join("").replace(/["\s]/g, "");
|
|
767
|
+
if (reported !== "" && reported !== wanted) ctx.log(`registry 返回 ${reported}(配置钉的是 ${wanted})`);
|
|
768
|
+
}
|
|
769
|
+
/** 装到 staging 并清掉失败现场的公共包壳。 */
|
|
770
|
+
async function npmInstall(deps, ctx, staging) {
|
|
771
|
+
const version = safeVersion(deps.config.reaVersion);
|
|
772
|
+
await rm(staging, {
|
|
773
|
+
recursive: true,
|
|
774
|
+
force: true
|
|
775
|
+
});
|
|
776
|
+
const result = await deps.runner.run("npm", [
|
|
777
|
+
"install",
|
|
778
|
+
"--prefix",
|
|
779
|
+
staging,
|
|
780
|
+
"--no-audit",
|
|
781
|
+
"--no-fund",
|
|
782
|
+
"--loglevel",
|
|
783
|
+
"http",
|
|
784
|
+
`${REA_PACKAGE}@${version}`
|
|
785
|
+
], {
|
|
786
|
+
env: npmEnv(deps.config),
|
|
787
|
+
signal: ctx.signal,
|
|
788
|
+
onLine: lineSink(ctx)
|
|
789
|
+
});
|
|
790
|
+
if (result.code !== 0) {
|
|
791
|
+
await rm(staging, {
|
|
792
|
+
recursive: true,
|
|
793
|
+
force: true
|
|
794
|
+
});
|
|
795
|
+
throw new Error(`npm install 失败(退出码 ${result.code})`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/** 安装/更新 REA 包:staging 安装 → 换目录 → 校验入口可读。 */
|
|
799
|
+
async function runInstallJob(deps, ctx) {
|
|
800
|
+
const version = safeVersion(deps.config.reaVersion);
|
|
801
|
+
const root = managerPaths(deps.config).installRoot;
|
|
802
|
+
const staging = `${root}.staging`;
|
|
803
|
+
await resolveVersion(deps, ctx, version);
|
|
804
|
+
ctx.phase(1);
|
|
805
|
+
await npmInstall(deps, ctx, staging);
|
|
806
|
+
ctx.phase(2);
|
|
807
|
+
const backup = `${root}.old`;
|
|
808
|
+
await rm(backup, {
|
|
809
|
+
recursive: true,
|
|
810
|
+
force: true
|
|
811
|
+
});
|
|
812
|
+
if (existsSync(root)) await rename(root, backup);
|
|
813
|
+
await rename(staging, root);
|
|
814
|
+
await rm(backup, {
|
|
815
|
+
recursive: true,
|
|
816
|
+
force: true
|
|
817
|
+
});
|
|
818
|
+
ctx.phase(3);
|
|
819
|
+
const probe = probeInstall(managerPaths(deps.config));
|
|
820
|
+
if (probe === null) throw new Error("安装校验失败:入口脚本不可用");
|
|
821
|
+
deps.refresh();
|
|
822
|
+
ctx.log(`已安装 ${REA_PACKAGE}@${probe.version} → ${probe.root}`);
|
|
823
|
+
return probe.version;
|
|
824
|
+
}
|
|
825
|
+
/** 逐文件 sha256(相对路径 + 字节数)。 */
|
|
826
|
+
function hashFiles(dir) {
|
|
827
|
+
return listFiles(dir).map((path) => {
|
|
828
|
+
const bytes = readFileSync(join(dir, path));
|
|
829
|
+
return {
|
|
830
|
+
path,
|
|
831
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
832
|
+
bytes: bytes.length
|
|
833
|
+
};
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
/** 从 tar 包取钉版 skill 目录(副本根与随包 `skills/` 同构:只 strip 两层)。 */
|
|
837
|
+
async function unpackSkill(deps, ctx, staging, tree) {
|
|
838
|
+
const version = safeVersion(deps.config.reaVersion);
|
|
839
|
+
await rm(staging, {
|
|
840
|
+
recursive: true,
|
|
841
|
+
force: true
|
|
842
|
+
});
|
|
843
|
+
await mkdir(staging, { recursive: true });
|
|
844
|
+
const packed = await deps.runner.run("npm", [
|
|
845
|
+
"pack",
|
|
846
|
+
`${REA_PACKAGE}@${version}`,
|
|
847
|
+
"--pack-destination",
|
|
848
|
+
staging,
|
|
849
|
+
"--loglevel",
|
|
850
|
+
"http"
|
|
851
|
+
], {
|
|
852
|
+
env: npmEnv(deps.config),
|
|
853
|
+
signal: ctx.signal,
|
|
854
|
+
onLine: lineSink(ctx)
|
|
855
|
+
});
|
|
856
|
+
if (packed.code !== 0) {
|
|
857
|
+
await rm(staging, {
|
|
858
|
+
recursive: true,
|
|
859
|
+
force: true
|
|
860
|
+
});
|
|
861
|
+
throw new Error(`npm pack 失败(退出码 ${packed.code})`);
|
|
862
|
+
}
|
|
863
|
+
const tarball = readdirSync(staging).find((name) => name.endsWith(".tgz"));
|
|
864
|
+
if (tarball === void 0) throw new Error("npm pack 未产出 tarball");
|
|
865
|
+
ctx.phase(2);
|
|
866
|
+
await mkdir(tree, { recursive: true });
|
|
867
|
+
const untar = await deps.runner.run("tar", [
|
|
868
|
+
"-xzf",
|
|
869
|
+
join(staging, tarball),
|
|
870
|
+
"-C",
|
|
871
|
+
tree,
|
|
872
|
+
"--strip-components=2",
|
|
873
|
+
`package/skills/${REA_SKILL_NAME}`
|
|
874
|
+
], {
|
|
875
|
+
env: npmEnv(deps.config),
|
|
876
|
+
signal: ctx.signal,
|
|
877
|
+
onLine: lineSink(ctx)
|
|
878
|
+
});
|
|
879
|
+
if (untar.code !== 0) {
|
|
880
|
+
await rm(staging, {
|
|
881
|
+
recursive: true,
|
|
882
|
+
force: true
|
|
883
|
+
});
|
|
884
|
+
throw new Error(`tar 解包失败(退出码 ${untar.code})`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
/** 拉取钉版 tarball 并替换手动更新的 skill 副本(写 sha256 清单后换目录)。 */
|
|
888
|
+
async function runSkillJob(deps, ctx) {
|
|
889
|
+
const version = safeVersion(deps.config.reaVersion);
|
|
890
|
+
const root = managerPaths(deps.config).skillRoot;
|
|
891
|
+
const staging = `${root}.staging`;
|
|
892
|
+
const tree = join(staging, "tree");
|
|
893
|
+
await resolveVersion(deps, ctx, version);
|
|
894
|
+
ctx.phase(1);
|
|
895
|
+
await unpackSkill(deps, ctx, staging, tree);
|
|
896
|
+
ctx.phase(3);
|
|
897
|
+
const files = hashFiles(tree);
|
|
898
|
+
if (files.length === 0) throw new Error("解包结果为空:上游 skill 目录结构可能已变");
|
|
899
|
+
const manifest = {
|
|
900
|
+
source: `${REA_PACKAGE}@${version}`,
|
|
901
|
+
files,
|
|
902
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
903
|
+
};
|
|
904
|
+
await writeFile(join(tree, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
905
|
+
await rm(root, {
|
|
906
|
+
recursive: true,
|
|
907
|
+
force: true
|
|
908
|
+
});
|
|
909
|
+
await rename(tree, root);
|
|
910
|
+
await rm(staging, {
|
|
911
|
+
recursive: true,
|
|
912
|
+
force: true
|
|
913
|
+
});
|
|
914
|
+
deps.refresh();
|
|
915
|
+
ctx.log(`已更新 skill ${REA_SKILL_NAME}(${files.length} 文件)→ ${root}`);
|
|
916
|
+
return version;
|
|
917
|
+
}
|
|
918
|
+
/** 生效配置:作业可带一次性覆盖(卡片未保存的代理端口即时可用)。 */
|
|
919
|
+
function configWith(base, overrides) {
|
|
920
|
+
if (overrides?.proxyPort === void 0) return base;
|
|
921
|
+
return {
|
|
922
|
+
...base,
|
|
923
|
+
proxyPort: overrides.proxyPort
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/** 建一个 REA 管理器。 */
|
|
927
|
+
function createManager(options) {
|
|
928
|
+
const runner = options.runner ?? spawnRunner;
|
|
929
|
+
const paths = () => managerPaths(options.config());
|
|
930
|
+
let cache = null;
|
|
931
|
+
const refresh = () => {
|
|
932
|
+
cache = probeRuntime(options.config(), options.bundledRoot, options.bundledVersion);
|
|
933
|
+
return cache;
|
|
934
|
+
};
|
|
935
|
+
return {
|
|
936
|
+
paths,
|
|
937
|
+
refresh,
|
|
938
|
+
snapshot: () => cache ?? refresh(),
|
|
939
|
+
operations: (overrides) => {
|
|
940
|
+
const deps = {
|
|
941
|
+
config: configWith(options.config(), overrides),
|
|
942
|
+
runner,
|
|
943
|
+
refresh
|
|
944
|
+
};
|
|
945
|
+
return {
|
|
946
|
+
install: {
|
|
947
|
+
phases: INSTALL_PHASES,
|
|
948
|
+
run: (ctx) => runInstallJob(deps, ctx)
|
|
949
|
+
},
|
|
950
|
+
skill: {
|
|
951
|
+
phases: SKILL_PHASES,
|
|
952
|
+
run: (ctx) => runSkillJob(deps, ctx)
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
//#endregion
|
|
959
|
+
//#region src/registration.ts
|
|
960
|
+
/**
|
|
961
|
+
* 建一个换代器并立即调度初始注册。
|
|
962
|
+
* 后台串行链不返回句柄:需要等待完成的是测试与调用方的关停流程,而不是业务路径。
|
|
963
|
+
*/
|
|
964
|
+
function createPresetRegistrar(options) {
|
|
965
|
+
let disposed = false;
|
|
966
|
+
let queued = false;
|
|
967
|
+
let unregister;
|
|
968
|
+
let chain = Promise.resolve();
|
|
969
|
+
const report = (error) => {
|
|
970
|
+
options.onError(error);
|
|
971
|
+
};
|
|
972
|
+
const settle = (task) => {
|
|
973
|
+
task.catch(report);
|
|
974
|
+
};
|
|
975
|
+
/** 换一代:先注销上一代,再注册按当前配置重建的声明。 */
|
|
976
|
+
const swap = async () => {
|
|
977
|
+
if (disposed) return;
|
|
978
|
+
const previous = unregister;
|
|
979
|
+
unregister = void 0;
|
|
980
|
+
if (previous !== void 0) await previous();
|
|
981
|
+
if (disposed) return;
|
|
982
|
+
const next = await options.register(options.build());
|
|
983
|
+
if (disposed) {
|
|
984
|
+
await next();
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
unregister = next;
|
|
988
|
+
};
|
|
989
|
+
const schedule = () => {
|
|
990
|
+
if (queued) return;
|
|
991
|
+
queued = true;
|
|
992
|
+
chain = chain.then(async () => {
|
|
993
|
+
queued = false;
|
|
994
|
+
await swap();
|
|
995
|
+
}).catch(report);
|
|
996
|
+
};
|
|
997
|
+
schedule();
|
|
998
|
+
return {
|
|
999
|
+
refresh: schedule,
|
|
1000
|
+
dispose: () => {
|
|
1001
|
+
disposed = true;
|
|
1002
|
+
const pending = unregister;
|
|
1003
|
+
unregister = void 0;
|
|
1004
|
+
if (pending !== void 0) settle(pending());
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region src/routes.ts
|
|
1010
|
+
/** 路由前缀。 */
|
|
1011
|
+
const ROUTE_BASE = "/dsh-plus/agent-preset-rea";
|
|
1012
|
+
/** 请求体大小上限(只有一个数字字段)。 */
|
|
1013
|
+
const BODY_LIMIT = 4096;
|
|
1014
|
+
/** 读请求体(超限即拒绝;空体按空对象)。 */
|
|
1015
|
+
async function readBody(req) {
|
|
1016
|
+
const chunks = [];
|
|
1017
|
+
let size = 0;
|
|
1018
|
+
for await (const chunk of req) {
|
|
1019
|
+
const buf = chunk;
|
|
1020
|
+
size += buf.length;
|
|
1021
|
+
if (size > BODY_LIMIT) throw new Error("请求体过大");
|
|
1022
|
+
chunks.push(buf);
|
|
1023
|
+
}
|
|
1024
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
1025
|
+
return text === "" ? {} : JSON.parse(text);
|
|
1026
|
+
}
|
|
1027
|
+
/** 从请求体取可选的代理端口覆盖(形状不对即抛,由调用方回 400)。 */
|
|
1028
|
+
function proxyPortOf(body) {
|
|
1029
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
1030
|
+
const value = body["proxyPort"];
|
|
1031
|
+
if (value === void 0 || value === null) return void 0;
|
|
1032
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 65535) throw new Error("proxyPort 必须是 0..65535 的整数");
|
|
1033
|
+
return value;
|
|
1034
|
+
}
|
|
1035
|
+
/** 组装状态响应(纯函数,可直接单测)。 */
|
|
1036
|
+
function statusPayload(options) {
|
|
1037
|
+
const config = options.config();
|
|
1038
|
+
const runtime = options.manager.snapshot();
|
|
1039
|
+
const paths = options.manager.paths();
|
|
1040
|
+
const launch = reaMcpLaunch(config, runtime.install);
|
|
1041
|
+
const mode = config.mcpCommand.trim() !== "" ? "custom" : launch === null ? "missing" : "installed";
|
|
1042
|
+
return {
|
|
1043
|
+
config: {
|
|
1044
|
+
reaVersion: config.reaVersion,
|
|
1045
|
+
proxyPort: config.proxyPort,
|
|
1046
|
+
mcpEnabled: config.mcpEnabled,
|
|
1047
|
+
installRoot: config.installRoot
|
|
1048
|
+
},
|
|
1049
|
+
install: {
|
|
1050
|
+
installed: runtime.install !== null,
|
|
1051
|
+
version: runtime.install?.version ?? null,
|
|
1052
|
+
root: paths.installRoot,
|
|
1053
|
+
entry: runtime.install?.entry ?? null
|
|
1054
|
+
},
|
|
1055
|
+
skill: { ...runtime.skill },
|
|
1056
|
+
mcp: {
|
|
1057
|
+
mode,
|
|
1058
|
+
enabled: config.mcpEnabled && launch !== null,
|
|
1059
|
+
command: launch?.command ?? null,
|
|
1060
|
+
args: launch?.args ?? []
|
|
1061
|
+
},
|
|
1062
|
+
paths,
|
|
1063
|
+
job: options.board.snapshot()
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
/** 注册全部路由。 */
|
|
1067
|
+
function registerReaRoutes(ctx, options) {
|
|
1068
|
+
const logger = ctx.logger("agent-preset-rea");
|
|
1069
|
+
const send = (res, status, body) => {
|
|
1070
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
1071
|
+
res.end(JSON.stringify(body));
|
|
1072
|
+
};
|
|
1073
|
+
const guard = (handler) => {
|
|
1074
|
+
return async (req, res) => {
|
|
1075
|
+
try {
|
|
1076
|
+
await handler(req, res);
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1079
|
+
logger.warn(`${req.method ?? "?"} ${req.url ?? "?"} 失败:${message}`);
|
|
1080
|
+
if (!res.headersSent) send(res, 400, {
|
|
1081
|
+
job: null,
|
|
1082
|
+
error: message
|
|
1083
|
+
});
|
|
1084
|
+
else res.end();
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
};
|
|
1088
|
+
const startJob = async (req, res, kind) => {
|
|
1089
|
+
const overrides = proxyPortOf(await readBody(req));
|
|
1090
|
+
const operations = options.manager.operations(overrides === void 0 ? void 0 : { proxyPort: overrides });
|
|
1091
|
+
const job = options.board.start(kind, operations[kind]);
|
|
1092
|
+
if (job === null) {
|
|
1093
|
+
send(res, 409, {
|
|
1094
|
+
job: options.board.snapshot(),
|
|
1095
|
+
error: "已有作业在运行"
|
|
1096
|
+
});
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
logger.info(`已启动 ${kind} 作业`);
|
|
1100
|
+
send(res, 200, { job });
|
|
1101
|
+
};
|
|
1102
|
+
ctx.webServer.register({
|
|
1103
|
+
kind: "exact",
|
|
1104
|
+
path: `${ROUTE_BASE}/status`,
|
|
1105
|
+
handler: guard(async (req, res) => {
|
|
1106
|
+
if (req.method !== "GET") {
|
|
1107
|
+
send(res, 405, {
|
|
1108
|
+
job: null,
|
|
1109
|
+
error: "method not allowed"
|
|
1110
|
+
});
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
send(res, 200, statusPayload(options));
|
|
1114
|
+
})
|
|
1115
|
+
});
|
|
1116
|
+
ctx.webServer.register({
|
|
1117
|
+
kind: "exact",
|
|
1118
|
+
path: `${ROUTE_BASE}/install`,
|
|
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
|
+
}
|
|
1155
|
+
const cancelled = options.board.cancel();
|
|
1156
|
+
send(res, 200, {
|
|
1157
|
+
job: options.board.snapshot(),
|
|
1158
|
+
...cancelled ? {} : { error: "没有正在运行的作业" }
|
|
1159
|
+
});
|
|
1160
|
+
})
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
//#endregion
|
|
1164
|
+
//#region src/index.ts
|
|
1165
|
+
const name = "dsh-plus-agent-preset-rea";
|
|
1166
|
+
/** 无硬 inject:agentPresets 经 apply 内惰性 inject 接线(缺席即空转)。 */
|
|
1167
|
+
const inject = [];
|
|
1168
|
+
/**
|
|
1169
|
+
* 注册 rea 预设,并在 persona 热更后换代;卸载时注销当前一代。
|
|
1170
|
+
*
|
|
1171
|
+
* 注册与换代都在 effect 生命周期内完成:fiber 早于注册完成被卸下时,
|
|
1172
|
+
* 换代器自己收掉晚到的 disposer,不泄漏预设子树。
|
|
1173
|
+
* @param ctx - 宿主上下文。
|
|
1174
|
+
* @param config - 已验证的插件配置(`personaPrefix` 为活动引用)。
|
|
1175
|
+
*/
|
|
1176
|
+
function apply(ctx, config) {
|
|
1177
|
+
const logger = ctx.logger("agent-preset-rea");
|
|
1178
|
+
const current = () => unwrapVolatile(config);
|
|
1179
|
+
if (current().enabled !== true) return;
|
|
1180
|
+
const onError = (error) => {
|
|
1181
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1182
|
+
logger.warn(`register rea preset failed: ${detail}`);
|
|
1183
|
+
};
|
|
1184
|
+
const manager = createManager({
|
|
1185
|
+
config: current,
|
|
1186
|
+
bundledRoot: BUNDLED_SKILL_ROOT,
|
|
1187
|
+
bundledVersion: REA_PINNED_VERSION
|
|
1188
|
+
});
|
|
1189
|
+
const board = createJobBoard();
|
|
1190
|
+
let reregister = null;
|
|
1191
|
+
const refresh = () => {
|
|
1192
|
+
manager.refresh();
|
|
1193
|
+
reregister?.();
|
|
1194
|
+
};
|
|
1195
|
+
ctx.effect(() => board.onSettled((snapshot) => {
|
|
1196
|
+
refresh();
|
|
1197
|
+
logger.info(`rea ${snapshot.kind} 作业结束:${snapshot.state}`);
|
|
1198
|
+
}), "agent-preset-rea: refresh after job");
|
|
1199
|
+
ctx.inject(["agentPresets"], (scope) => {
|
|
1200
|
+
scope.effect(() => {
|
|
1201
|
+
const registrar = createPresetRegistrar({
|
|
1202
|
+
register: (definition) => scope.agentPresets.register(definition),
|
|
1203
|
+
build: () => reaPresetDefinition(current(), manager.snapshot()),
|
|
1204
|
+
onError
|
|
1205
|
+
});
|
|
1206
|
+
reregister = () => registrar.refresh();
|
|
1207
|
+
const off = ctx.events.on("loader/volatile-update", () => refresh());
|
|
1208
|
+
return () => {
|
|
1209
|
+
reregister = null;
|
|
1210
|
+
off();
|
|
1211
|
+
registrar.dispose();
|
|
1212
|
+
};
|
|
1213
|
+
}, "agent-preset-rea: register rea preset");
|
|
1214
|
+
});
|
|
1215
|
+
ctx.inject(["webServer"], (scope) => {
|
|
1216
|
+
registerReaRoutes(scope, {
|
|
1217
|
+
manager,
|
|
1218
|
+
board,
|
|
1219
|
+
config: current
|
|
1220
|
+
});
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
//#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 };
|