@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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 逆向工程预设声明组装:官方 `standard` 预设**全量行**的逐字镜像 + 三处显式增量。
|
|
3
|
+
*
|
|
4
|
+
* 镜像:每条上游行的 `id`/`name`/`config`(含平台门控 `!!js` 表达式与 plan-mode
|
|
5
|
+
* 段落原文)都与 `@deepseek-ai/dsh-web-app` 的 `presets/standard.patch.yml` 逐字一致;
|
|
6
|
+
* 上游改用新版本后按 docs/ops/新版适配流程.md 刷新夹具,
|
|
7
|
+
* tests/upstream-mirror.test.ts 会逼出对齐。本预设不丢任何上游行(与 lean 相反)。
|
|
8
|
+
*
|
|
9
|
+
* 增量仅三处,测试按 {@link ADDED_ROWS} / {@link CONFIG_DELTAS} 白名单放行:
|
|
10
|
+
* 1. `persona` 行 `prefix` 取配置值(默认空串 = 空 persona);
|
|
11
|
+
* 2. `skill-filesystem` 行加 `customSkillDirs`(随包 REA skill 或手动更新副本 + 追加目录);
|
|
12
|
+
* 3. 新增 `mcp-rea` 行:官方 MCP 客户端连 REA stdio 服务,落在本预设作用域内;
|
|
13
|
+
* **未安装 REA 时该行 `disabled`**(不 spawn npx,见 mcp.ts 的说明)。
|
|
14
|
+
*
|
|
15
|
+
* {@link RuntimeSnapshot} 是运行期事实(受管安装与 skill 来源),由 manager 现探后
|
|
16
|
+
* 传入;不传即「未安装 + 随包 skill」,测试据此断言两种形态。
|
|
17
|
+
*
|
|
18
|
+
* 组行必须带 `isolate` 归属 realm,否则注册表拒绝挂载(服务泄漏判定)。
|
|
19
|
+
* @module @dsh-plus/agent-preset-rea/definition
|
|
20
|
+
*/
|
|
21
|
+
import type { PresetDefinition } from '@deepseek-ai/dsh-agent-preset-registry'
|
|
22
|
+
|
|
23
|
+
import type { AgentPresetReaConfig } from './config.ts'
|
|
24
|
+
import type { RuntimeSnapshot } from './manager.ts'
|
|
25
|
+
import { DEFAULT_MCP_COMMAND, reaMcpArgs, reaMcpEnv, reaMcpLaunch } from './mcp.ts'
|
|
26
|
+
import { PLAN_MODE_SECTION } from './prompt.ts'
|
|
27
|
+
import { BUNDLED_SKILL_ROOT, reaSkillDirs } from './skill.ts'
|
|
28
|
+
|
|
29
|
+
/** 预设注册表身份(改 id 等于换预设,default 等引用都会落空)。 */
|
|
30
|
+
export const REA_PRESET_ID = 'rea'
|
|
31
|
+
|
|
32
|
+
/** 相对上游 `standard` 新增的行 id(镜像测试按此白名单放行自造行)。 */
|
|
33
|
+
export const ADDED_ROWS: readonly string[] = ['mcp-rea']
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 已镜像行 → 允许偏离上游的字段名(其余字段必须逐字一致)。
|
|
37
|
+
* 删除某条等于把该行改回上游原文;改动上游不存在的字段会被测试拦下。
|
|
38
|
+
*/
|
|
39
|
+
export const CONFIG_DELTAS: Readonly<Record<string, readonly string[]>> = {
|
|
40
|
+
persona: ['prefix'],
|
|
41
|
+
'skill-filesystem': ['customSkillDirs'],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 平台门控表达式:与上游 `standard` 预设逐字相同,Windows 上由 pwsh 顶替 bash。 */
|
|
45
|
+
const WINDOWS_ONLY = { __jsExpr: "process.platform === 'win32'" } as const
|
|
46
|
+
const POSIX_ONLY = { __jsExpr: "process.platform !== 'win32'" } as const
|
|
47
|
+
|
|
48
|
+
/** 官方 loader 的嵌套组行名。 */
|
|
49
|
+
const GROUP = 'cordis:group'
|
|
50
|
+
/** 未安装、随包 skill 的运行期事实(默认值:测试与无 manager 场景)。 */
|
|
51
|
+
export function emptyRuntime(): RuntimeSnapshot {
|
|
52
|
+
return {
|
|
53
|
+
install: null,
|
|
54
|
+
skill: {
|
|
55
|
+
source: 'bundled',
|
|
56
|
+
version: null,
|
|
57
|
+
root: BUNDLED_SKILL_ROOT,
|
|
58
|
+
files: 0,
|
|
59
|
+
updatedAt: null,
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 声明里的行类型(官方 loader entry 结构)。 */
|
|
65
|
+
type Row = PresetDefinition['plugins'][number]
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 基础与工具面行:身份(persona)/ 项目指令与时钟 / shell 与文件工具 / 后台任务与
|
|
69
|
+
* 定时提醒 / skill 文件系统(本预设加 `customSkillDirs`,可指向手动更新副本)。
|
|
70
|
+
*/
|
|
71
|
+
function baseRows(config: AgentPresetReaConfig, runtime: RuntimeSnapshot): Row[] {
|
|
72
|
+
return [
|
|
73
|
+
// 身份:沿用官方装配(非 complete、保留运行时上下文),前缀取配置(默认空串:
|
|
74
|
+
// 空模板照样遮蔽部署级 persona 段,渲染为空后消失,不追加任何自有文案)。
|
|
75
|
+
{
|
|
76
|
+
id: 'persona',
|
|
77
|
+
name: '@deepseek-ai/dsh-persona',
|
|
78
|
+
config: { prefix: config.personaPrefix },
|
|
79
|
+
},
|
|
80
|
+
// 项目指令(AGENTS.md)与逐步时钟上下文(默认 10 分钟一条)。
|
|
81
|
+
{
|
|
82
|
+
id: 'agent-instructions',
|
|
83
|
+
name: '@deepseek-ai/dsh-agent-instructions',
|
|
84
|
+
config: { maxBytes: 65536 },
|
|
85
|
+
},
|
|
86
|
+
{ id: 'time-context', name: '@deepseek-ai/dsh-time-context' },
|
|
87
|
+
// shell:一次性执行,执行器在宿主平面;平台门控与上游一致。
|
|
88
|
+
{ id: 'tool-bash', name: '@deepseek-ai/dsh-tool-bash', disabled: WINDOWS_ONLY },
|
|
89
|
+
{ id: 'tool-pwsh', name: '@deepseek-ai/dsh-tool-pwsh', disabled: POSIX_ONLY },
|
|
90
|
+
// 文件读写、检索与后台任务控制。
|
|
91
|
+
{ id: 'tool-fs', name: '@deepseek-ai/dsh-tool-fs' },
|
|
92
|
+
{
|
|
93
|
+
id: 'tool-fs-search',
|
|
94
|
+
name: '@deepseek-ai/dsh-tool-fs-search',
|
|
95
|
+
config: { sampleOverCapGlobResults: false },
|
|
96
|
+
},
|
|
97
|
+
{ id: 'tool-jobs', name: '@deepseek-ai/dsh-tool-jobs' },
|
|
98
|
+
{ id: 'tool-schedule', name: '@deepseek-ai/dsh-tool-schedule' },
|
|
99
|
+
// 技能:本地目录发现 + 模型面加载器;本预设额外挂上随包 REA 官方 skill 根
|
|
100
|
+
// (customSkillDirs 是 rank 300 的追加根,不动项目/用户/bundled 根)。
|
|
101
|
+
{
|
|
102
|
+
id: 'skill-filesystem',
|
|
103
|
+
name: '@deepseek-ai/dsh-skill-filesystem',
|
|
104
|
+
config: {
|
|
105
|
+
customSkillDirs: reaSkillDirs(
|
|
106
|
+
config,
|
|
107
|
+
runtime.skill.source === 'updated' ? runtime.skill.root : null,
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
|
|
112
|
+
// 同会话目标:/goal 命令与模型面目标工具。
|
|
113
|
+
{ id: 'command-goal', name: '@deepseek-ai/dsh-command-goal' },
|
|
114
|
+
{ id: 'tool-goal', name: '@deepseek-ai/dsh-tool-goal' },
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 编排与交付面行:计划模式 / 上下文压缩 / 委派与 workflow / 交互与待办 / 网络工具 /
|
|
120
|
+
* **REA MCP 客户端** / 展示与插件管理。
|
|
121
|
+
*/
|
|
122
|
+
function orchestrationRows(config: AgentPresetReaConfig, runtime: RuntimeSnapshot): Row[] {
|
|
123
|
+
// 启动方式:显式 mcpCommand > 受管安装入口 > null(行禁用)。
|
|
124
|
+
const launch = reaMcpLaunch(config, runtime.install)
|
|
125
|
+
// 无启动方式时行被禁用,但 config 仍填经典 npx 配方:配置转储里能看到原始配方,
|
|
126
|
+
// 手工把行 enable 回来即回退到「启动期拉取」的老行为——默认路径绝不 spawn 它。
|
|
127
|
+
const mcp = launch ?? {
|
|
128
|
+
command: DEFAULT_MCP_COMMAND,
|
|
129
|
+
args: reaMcpArgs({ ...config, mcpCommand: DEFAULT_MCP_COMMAND }),
|
|
130
|
+
}
|
|
131
|
+
return [
|
|
132
|
+
// 计划模式:状态天然属于单个 agent,条目级 realm 即正确生命周期。
|
|
133
|
+
{
|
|
134
|
+
id: 'planning',
|
|
135
|
+
name: GROUP,
|
|
136
|
+
group: true,
|
|
137
|
+
isolate: { planMode: true },
|
|
138
|
+
config: [
|
|
139
|
+
{
|
|
140
|
+
id: 'plan-mode',
|
|
141
|
+
name: '@deepseek-ai/dsh-plan-mode',
|
|
142
|
+
config: { section: PLAN_MODE_SECTION },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
// 压缩:token 计量器留在宿主平面,本组只挂读取它的后端与结果裁剪器。
|
|
147
|
+
{
|
|
148
|
+
id: 'compaction',
|
|
149
|
+
name: GROUP,
|
|
150
|
+
group: true,
|
|
151
|
+
isolate: { compaction: true, toolResultPruner: true },
|
|
152
|
+
config: [
|
|
153
|
+
{ id: 'compaction-basic', name: '@deepseek-ai/dsh-compaction-basic' },
|
|
154
|
+
{ id: 'command-compact', name: '@deepseek-ai/dsh-command-compact' },
|
|
155
|
+
{
|
|
156
|
+
id: 'tool-result-pruner',
|
|
157
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner',
|
|
158
|
+
config: { thresholdChars: 8192, headChars: 4096, tailChars: 1024 },
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
// 委派与编排:子代理、fork、workflow 整组(编排引擎只在本 realm 内共享)。
|
|
163
|
+
{
|
|
164
|
+
id: 'delegation',
|
|
165
|
+
name: GROUP,
|
|
166
|
+
group: true,
|
|
167
|
+
isolate: { workflowEngine: true },
|
|
168
|
+
config: [
|
|
169
|
+
{ id: 'tool-subagent-control', name: '@deepseek-ai/dsh-tool-subagent-control' },
|
|
170
|
+
{
|
|
171
|
+
id: 'tool-subagent-list-agents',
|
|
172
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'tool-subagent',
|
|
176
|
+
name: '@deepseek-ai/dsh-tool-subagent',
|
|
177
|
+
config: { provider: 'spawn', toolName: 'subagent', modelSelectionSettings: true },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'tool-subagent-fork',
|
|
181
|
+
name: '@deepseek-ai/dsh-tool-subagent',
|
|
182
|
+
config: { provider: 'fork', toolName: 'subagent_fork' },
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'workflow-ptc',
|
|
186
|
+
name: '@deepseek-ai/dsh-workflow-ptc',
|
|
187
|
+
config: { provider: 'spawn' },
|
|
188
|
+
},
|
|
189
|
+
{ id: 'tool-workflow', name: '@deepseek-ai/dsh-tool-workflow' },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
// 其余模型面行。
|
|
193
|
+
{ id: 'tool-ask-user', name: '@deepseek-ai/dsh-tool-ask-user' },
|
|
194
|
+
{
|
|
195
|
+
id: 'tool-todo',
|
|
196
|
+
name: '@deepseek-ai/dsh-tool-todo',
|
|
197
|
+
config: { allowParallelInProgress: true },
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: 'tool-web',
|
|
201
|
+
name: '@deepseek-ai/dsh-tool-web',
|
|
202
|
+
config: { fetch: true, searchTimeoutMs: 60000 },
|
|
203
|
+
},
|
|
204
|
+
// 本预设新增:REA MCP 客户端(stdio),工具以 mcp__<serverName>__<tool> 注册在
|
|
205
|
+
// 本预设作用域;服务名预留按作用域隔离,其他预设不受影响。
|
|
206
|
+
// 未安装 REA 且未显式配 mcpCommand 时整行禁用:绝不在 boot 期 spawn npx
|
|
207
|
+
// (那会跑 npm 安装脚本,卡住即吊死预设激活与名册——见 docs/README.md)。
|
|
208
|
+
{
|
|
209
|
+
id: 'mcp-rea',
|
|
210
|
+
name: '@deepseek-ai/dsh-mcp-client',
|
|
211
|
+
disabled: !config.mcpEnabled || launch === null,
|
|
212
|
+
config: {
|
|
213
|
+
transport: 'stdio',
|
|
214
|
+
serverName: config.mcpServerName,
|
|
215
|
+
command: mcp.command,
|
|
216
|
+
args: mcp.args,
|
|
217
|
+
env: reaMcpEnv(config),
|
|
218
|
+
toolCallTimeoutMs: config.toolCallTimeoutMs,
|
|
219
|
+
failOnStartupError: config.failOnStartupError,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{ id: 'present', name: '@deepseek-ai/dsh-tool-present' },
|
|
223
|
+
// 宿主插件管理:上游 standard 里同样是 disabled 行。
|
|
224
|
+
{ id: 'tool-plugin-manager', name: '@deepseek-ai/dsh-plugin-manager/tools', disabled: true },
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 由配置 + 运行期事实拼出注册表声明。
|
|
230
|
+
* @param config - 已验证的插件配置。
|
|
231
|
+
* @param runtime - 受管安装与 skill 来源事实(默认「未安装 + 随包 skill」)。
|
|
232
|
+
* @returns id 固定为 `rea` 的预设声明。
|
|
233
|
+
*/
|
|
234
|
+
export function reaPresetDefinition(
|
|
235
|
+
config: AgentPresetReaConfig,
|
|
236
|
+
runtime: RuntimeSnapshot = emptyRuntime(),
|
|
237
|
+
): PresetDefinition {
|
|
238
|
+
return {
|
|
239
|
+
id: REA_PRESET_ID,
|
|
240
|
+
name: config.name,
|
|
241
|
+
description: config.description,
|
|
242
|
+
order: config.order,
|
|
243
|
+
plugins: [...baseRows(config, runtime), ...orchestrationRows(config, runtime)],
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/dto.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主半与浏览器半之间的 wire 数据门面(纯类型 + 纯函数,零 node 依赖)。
|
|
3
|
+
*
|
|
4
|
+
* 浏览器半只 `import type` 消费本文件(编译期擦除,不进 client bundle),
|
|
5
|
+
* 两端共用同一份字段定义,避免「宿主改了字段、卡片还在读老名字」的静默漂移。
|
|
6
|
+
* @module @dsh-plus/agent-preset-rea/dto
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 作业种类。 */
|
|
10
|
+
export type JobKind = 'install' | 'skill'
|
|
11
|
+
|
|
12
|
+
/** 作业状态。 */
|
|
13
|
+
export type JobState = 'running' | 'ok' | 'error' | 'cancelled'
|
|
14
|
+
|
|
15
|
+
/** 作业进度(按阶段推进;percent 为进入该阶段时的完成度)。 */
|
|
16
|
+
export interface JobProgressDto {
|
|
17
|
+
step: number
|
|
18
|
+
steps: number
|
|
19
|
+
phase: string
|
|
20
|
+
percent: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 作业快照。 */
|
|
24
|
+
export interface JobDto {
|
|
25
|
+
id: number
|
|
26
|
+
kind: JobKind
|
|
27
|
+
state: JobState
|
|
28
|
+
progress: JobProgressDto
|
|
29
|
+
/** 日志尾部(宿主已有界)。 */
|
|
30
|
+
log: string[]
|
|
31
|
+
startedAt: string
|
|
32
|
+
finishedAt: string | null
|
|
33
|
+
version: string | null
|
|
34
|
+
error: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 受管安装事实。 */
|
|
38
|
+
export interface InstallDto {
|
|
39
|
+
installed: boolean
|
|
40
|
+
/** 已安装版本(未安装为 null)。 */
|
|
41
|
+
version: string | null
|
|
42
|
+
/** 受管安装树位置(无论是否已安装都有值,便于用户核对)。 */
|
|
43
|
+
root: string
|
|
44
|
+
/** 入口脚本(未安装为 null)。 */
|
|
45
|
+
entry: string | null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** skill 来源事实。 */
|
|
49
|
+
export interface SkillDto {
|
|
50
|
+
source: 'bundled' | 'updated'
|
|
51
|
+
version: string | null
|
|
52
|
+
root: string
|
|
53
|
+
files: number
|
|
54
|
+
updatedAt: string | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** MCP 行形态。 */
|
|
58
|
+
export interface McpDto {
|
|
59
|
+
/** `installed` = 用受管安装入口;`custom` = 用显式 mcpCommand;`missing` = 未安装且无显式命令(行禁用)。 */
|
|
60
|
+
mode: 'installed' | 'custom' | 'missing'
|
|
61
|
+
/** 行是否启用(mcpEnabled && 有启动方式)。 */
|
|
62
|
+
enabled: boolean
|
|
63
|
+
command: string | null
|
|
64
|
+
args: string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `GET /status` 响应。 */
|
|
68
|
+
export interface StatusDto {
|
|
69
|
+
config: {
|
|
70
|
+
reaVersion: string
|
|
71
|
+
proxyPort: number
|
|
72
|
+
mcpEnabled: boolean
|
|
73
|
+
installRoot: string
|
|
74
|
+
}
|
|
75
|
+
install: InstallDto
|
|
76
|
+
skill: SkillDto
|
|
77
|
+
mcp: McpDto
|
|
78
|
+
paths: { installRoot: string; skillRoot: string }
|
|
79
|
+
job: JobDto | null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 作业类响应(启动/取消)。 */
|
|
83
|
+
export interface JobActionDto {
|
|
84
|
+
job: JobDto | null
|
|
85
|
+
/** 拒绝原因(409/400 时给出人话)。 */
|
|
86
|
+
error?: string
|
|
87
|
+
}
|
package/src/exec.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 外部命令执行面与作业原语:受管安装/更新作业要跑 `npm` / `tar`,这些调用全部经
|
|
3
|
+
* {@link CommandRunner} 注入,单测用假 runner 跑完整状态机(零网络、零 npm)。
|
|
4
|
+
*
|
|
5
|
+
* 作业阶段由 {@link JobTaskContext} 上报,日志行由 {@link JobOperation} 的调用方
|
|
6
|
+
* 收集;本模块只负责「怎么跑命令」与「怎么把输出变成日志行」。
|
|
7
|
+
* @module @dsh-plus/agent-preset-rea/exec
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import { readdirSync } from 'node:fs'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
|
|
13
|
+
import type { AgentPresetReaConfig } from './config.ts'
|
|
14
|
+
|
|
15
|
+
/** 版本字符串形状:进 shell 前把用户可配项收窄成纯版本号。 */
|
|
16
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
17
|
+
|
|
18
|
+
/** npm 命令名(Windows 经 shell 执行,见 {@link spawnRunner} 注释)。 */
|
|
19
|
+
export const NPM = 'npm'
|
|
20
|
+
|
|
21
|
+
/** 作业取消(AbortSignal 触发后由 runner 抛出)。 */
|
|
22
|
+
export class JobCancelled extends Error {
|
|
23
|
+
constructor() {
|
|
24
|
+
super('作业已取消')
|
|
25
|
+
this.name = 'JobCancelled'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 外部命令执行面。 */
|
|
30
|
+
export interface CommandRunner {
|
|
31
|
+
run(
|
|
32
|
+
command: string,
|
|
33
|
+
args: string[],
|
|
34
|
+
options: {
|
|
35
|
+
env: Record<string, string>
|
|
36
|
+
signal: AbortSignal
|
|
37
|
+
onLine(line: string): void
|
|
38
|
+
},
|
|
39
|
+
): Promise<{ code: number }>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 作业执行上下文(阶段推进 + 日志 + 取消信号)。 */
|
|
43
|
+
export interface JobTaskContext {
|
|
44
|
+
/** 进入第 step 个阶段(从 0 起,与 {@link JobOperation.phases} 对应)。 */
|
|
45
|
+
phase(step: number): void
|
|
46
|
+
log(line: string): void
|
|
47
|
+
signal: AbortSignal
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 一类作业:阶段名 + 实现。 */
|
|
51
|
+
export interface JobOperation {
|
|
52
|
+
phases: string[]
|
|
53
|
+
run(ctx: JobTaskContext): Promise<string>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 实际 runner:spawn 子进程、逐行回调、AbortSignal 触发即杀。 */
|
|
57
|
+
export const spawnRunner: CommandRunner = {
|
|
58
|
+
run: (command, args, options) =>
|
|
59
|
+
new Promise((resolve, reject) => {
|
|
60
|
+
// Windows 下 npm/tar 都是 .cmd/.exe 包装,shell:false 无法直接执行
|
|
61
|
+
// (与官方 MCP 客户端同一坑);参数全部由本模块构造,用户可配的版本号已按
|
|
62
|
+
// 形状收窄,无拼串注入面。
|
|
63
|
+
const child = spawn(command, args, {
|
|
64
|
+
env: options.env,
|
|
65
|
+
shell: process.platform === 'win32',
|
|
66
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
67
|
+
})
|
|
68
|
+
let pending = ''
|
|
69
|
+
const emit = (chunk: string): void => {
|
|
70
|
+
pending += chunk
|
|
71
|
+
const lines = pending.split('\n')
|
|
72
|
+
pending = lines.pop() ?? ''
|
|
73
|
+
for (const line of lines) options.onLine(line.trimEnd())
|
|
74
|
+
}
|
|
75
|
+
child.stdout.setEncoding('utf8')
|
|
76
|
+
child.stderr.setEncoding('utf8')
|
|
77
|
+
child.stdout.on('data', emit)
|
|
78
|
+
child.stderr.on('data', emit)
|
|
79
|
+
const abort = (): void => {
|
|
80
|
+
child.kill('SIGTERM')
|
|
81
|
+
}
|
|
82
|
+
options.signal.addEventListener('abort', abort, { once: true })
|
|
83
|
+
child.on('error', reject)
|
|
84
|
+
child.on('close', (code) => {
|
|
85
|
+
options.signal.removeEventListener('abort', abort)
|
|
86
|
+
if (pending !== '') options.onLine(pending.trimEnd())
|
|
87
|
+
if (options.signal.aborted) reject(new JobCancelled())
|
|
88
|
+
else resolve({ code: code ?? 1 })
|
|
89
|
+
})
|
|
90
|
+
}),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* npm 子进程环境:继承宿主环境,附带代理端口(port > 0 时同时给 npm 与
|
|
95
|
+
* prebuild-install 注入代理——后者只认 `https_proxy` 系列环境变量,
|
|
96
|
+
* 这是原生依赖 prebuild 下载卡死的正解)。
|
|
97
|
+
*/
|
|
98
|
+
export function npmEnv(config: AgentPresetReaConfig): Record<string, string> {
|
|
99
|
+
const env: Record<string, string> = {}
|
|
100
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
101
|
+
if (value !== undefined) env[key] = value
|
|
102
|
+
}
|
|
103
|
+
env['npm_config_audit'] = 'false'
|
|
104
|
+
env['npm_config_fund'] = 'false'
|
|
105
|
+
env['npm_config_update_notifier'] = 'false'
|
|
106
|
+
env['npm_config_loglevel'] = 'http'
|
|
107
|
+
if (config.proxyPort > 0) {
|
|
108
|
+
const proxy = `http://127.0.0.1:${config.proxyPort}`
|
|
109
|
+
env['HTTP_PROXY'] = proxy
|
|
110
|
+
env['HTTPS_PROXY'] = proxy
|
|
111
|
+
env['http_proxy'] = proxy
|
|
112
|
+
env['https_proxy'] = proxy
|
|
113
|
+
env['npm_config_proxy'] = proxy
|
|
114
|
+
env['npm_config_https_proxy'] = proxy
|
|
115
|
+
}
|
|
116
|
+
return env
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 版本收窄:进命令前把配置里的钉版校验成纯版本号。 */
|
|
120
|
+
export function safeVersion(version: string): string {
|
|
121
|
+
const trimmed = version.trim()
|
|
122
|
+
if (!VERSION_PATTERN.test(trimmed)) {
|
|
123
|
+
throw new Error(`配置里的 reaVersion 不是合法版本号:${JSON.stringify(version)}`)
|
|
124
|
+
}
|
|
125
|
+
return trimmed
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 逐行转发子进程输出到作业日志(截断超长行)。 */
|
|
129
|
+
export function lineSink(ctx: JobTaskContext): (line: string) => void {
|
|
130
|
+
return (line) => {
|
|
131
|
+
if (line !== '') ctx.log(line.slice(0, 500))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 递归列出目录下全部文件(相对 POSIX 路径,排序稳定)。 */
|
|
136
|
+
export function listFiles(dir: string, base = ''): string[] {
|
|
137
|
+
const out: string[] = []
|
|
138
|
+
for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) =>
|
|
139
|
+
a.name.localeCompare(b.name),
|
|
140
|
+
)) {
|
|
141
|
+
const rel = base === '' ? entry.name : `${base}/${entry.name}`
|
|
142
|
+
if (entry.isDirectory()) out.push(...listFiles(join(dir, entry.name), rel))
|
|
143
|
+
else out.push(rel)
|
|
144
|
+
}
|
|
145
|
+
return out
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 目录下文件数(目录缺席或不可读按 0;状态展示用,不抛)。 */
|
|
149
|
+
export function countFiles(dir: string): number {
|
|
150
|
+
try {
|
|
151
|
+
return listFiles(dir).length
|
|
152
|
+
} catch {
|
|
153
|
+
return 0
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh 服务插件:把「逆向工程」注册为官方 agent preset(id=`rea`)。
|
|
3
|
+
*
|
|
4
|
+
* 预设声明经 `ctx.agentPresets.register()` 注册(官方 preset registry 是唯一
|
|
5
|
+
* 读取方),出现在设置 → Agent 预设的名单里,可被会话选择。
|
|
6
|
+
*
|
|
7
|
+
* 预设语义:官方 `standard` 预设**全量行**的逐字镜像(shell/文件读写与检索/
|
|
8
|
+
* 后台任务/定时提醒/技能/目标/计划/压缩/委派与编排/提问/待办/联网/交付/插件管理
|
|
9
|
+
* 全在),另加三处显式增量——persona 前缀取配置(默认空串)、`skill-filesystem`
|
|
10
|
+
* 加随包 REA skill 根、新增 `mcp-rea` 行连 REA stdio 服务。详见 definition.ts。
|
|
11
|
+
*
|
|
12
|
+
* 作用域:三处增量都落在本预设声明内,因此 REA 工具(`mcp__rea__*`)、REA skill
|
|
13
|
+
* 与附加 skill 根只对该预设的会话可见;宿主平面不新增工具/提示段,其他预设不受
|
|
14
|
+
* 影响(见 docs/README.md 的隔离一节)。
|
|
15
|
+
*
|
|
16
|
+
* persona 提示词:`personaPrefix` 是活动字段(`.volatile()`),由设置 → 插件页
|
|
17
|
+
* 的配置卡片写入;保存后 loader 原位提交活动引用并广播 `loader/volatile-update`,
|
|
18
|
+
* 本插件据此换代注册(注销旧声明 → 按新值重建声明),**新建会话**即用新提示词,
|
|
19
|
+
* 已存在会话保持既有 composition(注册表语义:运行中会话的装配不随换代改变)。
|
|
20
|
+
*
|
|
21
|
+
* REA 的安装与更新也在卡片上(受管安装 + 后台作业 + 实时进度 + 可选代理端口):
|
|
22
|
+
* 宿主半的 manager 把 `rea-agents` 装进插件数据目录、把 skill 拉成可替换副本,
|
|
23
|
+
* 作业完成后同样换代注册——因此预设声明里**不会**在 boot 期 spawn `npx`
|
|
24
|
+
* (那会跑 npm 安装脚本,原生依赖 prebuild 下载一卡就吊死预设激活与整个名册)。
|
|
25
|
+
*
|
|
26
|
+
* 设计取舍:
|
|
27
|
+
* - 独立插件而非 cordis.patch.yml 里的裸 `@deepseek-ai/dsh-agent-preset`
|
|
28
|
+
* 行:便于纳入本仓的构建/测试/文档体系,并把显示面与 MCP/skill 参数开放为
|
|
29
|
+
* 可覆盖配置;
|
|
30
|
+
* - 惰性 inject `agentPresets`:服务缺席的 profile(如 headless)里本行
|
|
31
|
+
* 照常 active、只是不注册(等价插件缺席),绝不因硬 inject 永久 pending
|
|
32
|
+
* 拖垮 boot(同 boot-retry 的教训);
|
|
33
|
+
* - 注册与换代在 effect 内后台串行完成、不阻塞 apply:注册会同步挂载预设
|
|
34
|
+
* 子树并等待其激活(含 MCP 首次连接),await 在本行激活内可能与 Host 树推进
|
|
35
|
+
* 互锁;失败仅记 warn(重名、MCP 起不来等由各注册表/客户端负责诊断),
|
|
36
|
+
* 不使 boot 失败。
|
|
37
|
+
* @module @dsh-plus/agent-preset-rea
|
|
38
|
+
*/
|
|
39
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
40
|
+
import { unwrapVolatile } from '@dsh-plus/shared'
|
|
41
|
+
|
|
42
|
+
import { type AgentPresetReaConfig, type AgentPresetReaConfigFields, Config } from './config.ts'
|
|
43
|
+
import { reaPresetDefinition } from './definition.ts'
|
|
44
|
+
import { createJobBoard } from './jobs.ts'
|
|
45
|
+
import { createManager } from './manager.ts'
|
|
46
|
+
import { REA_PINNED_VERSION } from './mcp.ts'
|
|
47
|
+
import { createPresetRegistrar } from './registration.ts'
|
|
48
|
+
import { registerReaRoutes } from './routes.ts'
|
|
49
|
+
import { BUNDLED_SKILL_ROOT } from './skill.ts'
|
|
50
|
+
|
|
51
|
+
export const name = 'dsh-plus-agent-preset-rea'
|
|
52
|
+
|
|
53
|
+
/** 无硬 inject:agentPresets 经 apply 内惰性 inject 接线(缺席即空转)。 */
|
|
54
|
+
export const inject = [] as const
|
|
55
|
+
|
|
56
|
+
export { SETTINGS_NS } from './config.ts'
|
|
57
|
+
export {
|
|
58
|
+
ADDED_ROWS,
|
|
59
|
+
CONFIG_DELTAS,
|
|
60
|
+
emptyRuntime,
|
|
61
|
+
REA_PRESET_ID,
|
|
62
|
+
reaPresetDefinition,
|
|
63
|
+
} from './definition.ts'
|
|
64
|
+
export type {
|
|
65
|
+
InstallDto,
|
|
66
|
+
JobActionDto,
|
|
67
|
+
JobDto,
|
|
68
|
+
JobKind,
|
|
69
|
+
JobProgressDto,
|
|
70
|
+
JobState,
|
|
71
|
+
McpDto,
|
|
72
|
+
SkillDto,
|
|
73
|
+
StatusDto,
|
|
74
|
+
} from './dto.ts'
|
|
75
|
+
export {
|
|
76
|
+
type CommandRunner,
|
|
77
|
+
JobCancelled,
|
|
78
|
+
type JobOperation,
|
|
79
|
+
type JobTaskContext,
|
|
80
|
+
NPM,
|
|
81
|
+
npmEnv,
|
|
82
|
+
safeVersion,
|
|
83
|
+
spawnRunner,
|
|
84
|
+
} from './exec.ts'
|
|
85
|
+
export { createJobBoard, type JobBoard, type JobSnapshot } from './jobs.ts'
|
|
86
|
+
export {
|
|
87
|
+
configWith,
|
|
88
|
+
createManager,
|
|
89
|
+
INSTALL_PHASES,
|
|
90
|
+
type JobDeps,
|
|
91
|
+
type JobOverrides,
|
|
92
|
+
type ManagerPaths,
|
|
93
|
+
managerPaths,
|
|
94
|
+
probeInstall,
|
|
95
|
+
probeRuntime,
|
|
96
|
+
probeSkill,
|
|
97
|
+
REA_INSTALL_DIR,
|
|
98
|
+
REA_PACKAGE,
|
|
99
|
+
REA_SKILL_DIR,
|
|
100
|
+
REA_SKILL_NAME,
|
|
101
|
+
type ReaInstall,
|
|
102
|
+
type ReaManager,
|
|
103
|
+
type RuntimeSnapshot,
|
|
104
|
+
SKILL_PHASES,
|
|
105
|
+
type SkillManifest,
|
|
106
|
+
type SkillState,
|
|
107
|
+
} from './manager.ts'
|
|
108
|
+
export {
|
|
109
|
+
DEFAULT_MCP_COMMAND,
|
|
110
|
+
INPUT_SCHEMA_PROFILES,
|
|
111
|
+
type InstalledEntry,
|
|
112
|
+
type McpLaunch,
|
|
113
|
+
OUTPUT_SCHEMA_PROFILES,
|
|
114
|
+
REA_PINNED_VERSION,
|
|
115
|
+
reaMcpArgs,
|
|
116
|
+
reaMcpEnv,
|
|
117
|
+
reaMcpLaunch,
|
|
118
|
+
} from './mcp.ts'
|
|
119
|
+
export { PLAN_MODE_SECTION } from './prompt.ts'
|
|
120
|
+
export { createPresetRegistrar, type PresetRegistrar, type Unregister } from './registration.ts'
|
|
121
|
+
export { ROUTE_BASE, registerReaRoutes, statusPayload } from './routes.ts'
|
|
122
|
+
export { BUNDLED_SKILL_ROOT, reaSkillDirs } from './skill.ts'
|
|
123
|
+
export type { AgentPresetReaConfig, AgentPresetReaConfigFields }
|
|
124
|
+
export { Config }
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 注册 rea 预设,并在 persona 热更后换代;卸载时注销当前一代。
|
|
128
|
+
*
|
|
129
|
+
* 注册与换代都在 effect 生命周期内完成:fiber 早于注册完成被卸下时,
|
|
130
|
+
* 换代器自己收掉晚到的 disposer,不泄漏预设子树。
|
|
131
|
+
* @param ctx - 宿主上下文。
|
|
132
|
+
* @param config - 已验证的插件配置(`personaPrefix` 为活动引用)。
|
|
133
|
+
*/
|
|
134
|
+
export function apply(ctx: Context, config: AgentPresetReaConfigFields): void {
|
|
135
|
+
const logger = ctx.logger('agent-preset-rea')
|
|
136
|
+
const current = (): AgentPresetReaConfig => unwrapVolatile(config)
|
|
137
|
+
if (current().enabled !== true) return
|
|
138
|
+
|
|
139
|
+
const onError = (error: unknown): void => {
|
|
140
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
141
|
+
logger.warn(`register rea preset failed: ${detail}`)
|
|
142
|
+
}
|
|
143
|
+
const manager = createManager({
|
|
144
|
+
config: current,
|
|
145
|
+
bundledRoot: BUNDLED_SKILL_ROOT,
|
|
146
|
+
bundledVersion: REA_PINNED_VERSION,
|
|
147
|
+
})
|
|
148
|
+
const board = createJobBoard()
|
|
149
|
+
// 换代入口:安装/更新作业完成、或活动配置提交后,都要按新事实重建声明。
|
|
150
|
+
let reregister: (() => void) | null = null
|
|
151
|
+
const refresh = (): void => {
|
|
152
|
+
manager.refresh()
|
|
153
|
+
reregister?.()
|
|
154
|
+
}
|
|
155
|
+
ctx.effect(
|
|
156
|
+
() =>
|
|
157
|
+
board.onSettled((snapshot) => {
|
|
158
|
+
refresh()
|
|
159
|
+
logger.info(`rea ${snapshot.kind} 作业结束:${snapshot.state}`)
|
|
160
|
+
}),
|
|
161
|
+
'agent-preset-rea: refresh after job',
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
ctx.inject(['agentPresets'], (scope) => {
|
|
165
|
+
scope.effect(() => {
|
|
166
|
+
const registrar = createPresetRegistrar({
|
|
167
|
+
register: (definition) => scope.agentPresets.register(definition),
|
|
168
|
+
build: () => reaPresetDefinition(current(), manager.snapshot()),
|
|
169
|
+
onError,
|
|
170
|
+
})
|
|
171
|
+
reregister = () => registrar.refresh()
|
|
172
|
+
// 监听器挂在插件自身 fiber 上:loader 正是对该 fiber 广播 volatile 提交。
|
|
173
|
+
const off = ctx.events.on('loader/volatile-update', () => refresh())
|
|
174
|
+
return () => {
|
|
175
|
+
reregister = null
|
|
176
|
+
off()
|
|
177
|
+
registrar.dispose()
|
|
178
|
+
}
|
|
179
|
+
}, 'agent-preset-rea: register rea preset')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// 安装/更新端点:webServer 缺席(headless)即不注册,插件其余能力照常。
|
|
183
|
+
ctx.inject(['webServer'], (scope) => {
|
|
184
|
+
registerReaRoutes(scope as unknown as Context, { manager, board, config: current })
|
|
185
|
+
})
|
|
186
|
+
}
|