@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/src/definition.ts
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* 增量仅三处,测试按 {@link ADDED_ROWS} / {@link CONFIG_DELTAS} 白名单放行:
|
|
10
10
|
* 1. `persona` 行 `prefix` 取配置值(默认空串 = 空 persona);
|
|
11
|
-
* 2. `skill-filesystem` 行加 `customSkillDirs
|
|
11
|
+
* 2. `skill-filesystem` 行加 `customSkillDirs`(REA skill 源与 reverse-skill 技能组,
|
|
12
|
+
* 各受一个活动开关控制,另加部署方 `extraSkillDirs`);
|
|
12
13
|
* 3. 新增 `mcp-rea` 行:官方 MCP 客户端连 REA stdio 服务,落在本预设作用域内;
|
|
13
14
|
* **未安装 REA 时该行 `disabled`**(不 spawn npx,见 mcp.ts 的说明)。
|
|
14
15
|
*
|
|
@@ -24,7 +25,8 @@ import type { AgentPresetReaConfig } from './config.ts'
|
|
|
24
25
|
import type { RuntimeSnapshot } from './manager.ts'
|
|
25
26
|
import { DEFAULT_MCP_COMMAND, reaMcpArgs, reaMcpEnv, reaMcpLaunch } from './mcp.ts'
|
|
26
27
|
import { PLAN_MODE_SECTION } from './prompt.ts'
|
|
27
|
-
import {
|
|
28
|
+
import { emptyReverseState } from './reverse.ts'
|
|
29
|
+
import { BUNDLED_SKILL_ROOT, presetSkillDirs } from './skill.ts'
|
|
28
30
|
|
|
29
31
|
/** 预设注册表身份(改 id 等于换预设,default 等引用都会落空)。 */
|
|
30
32
|
export const REA_PRESET_ID = 'rea'
|
|
@@ -58,6 +60,7 @@ export function emptyRuntime(): RuntimeSnapshot {
|
|
|
58
60
|
files: 0,
|
|
59
61
|
updatedAt: null,
|
|
60
62
|
},
|
|
63
|
+
reverse: emptyReverseState(),
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
|
|
@@ -102,10 +105,7 @@ function baseRows(config: AgentPresetReaConfig, runtime: RuntimeSnapshot): Row[]
|
|
|
102
105
|
id: 'skill-filesystem',
|
|
103
106
|
name: '@deepseek-ai/dsh-skill-filesystem',
|
|
104
107
|
config: {
|
|
105
|
-
customSkillDirs:
|
|
106
|
-
config,
|
|
107
|
-
runtime.skill.source === 'updated' ? runtime.skill.root : null,
|
|
108
|
-
),
|
|
108
|
+
customSkillDirs: presetSkillDirs(config, runtime),
|
|
109
109
|
},
|
|
110
110
|
},
|
|
111
111
|
{ id: 'tool-skill', name: '@deepseek-ai/dsh-tool-skill' },
|
package/src/dto.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* @module @dsh-plus/agent-preset-rea/dto
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
export type JobKind = 'install' | 'skill'
|
|
9
|
+
/** 作业种类:REA 包安装/更新、REA skill 更新、reverse-skill 更新。 */
|
|
10
|
+
export type JobKind = 'install' | 'skill' | 'reverse'
|
|
11
11
|
|
|
12
12
|
/** 作业状态。 */
|
|
13
13
|
export type JobState = 'running' | 'ok' | 'error' | 'cancelled'
|
|
@@ -64,18 +64,44 @@ export interface McpDto {
|
|
|
64
64
|
args: string[]
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** GitHub 技能源(zhaoxuya520/reverse-skill)事实。 */
|
|
68
|
+
export interface ReverseSkillDto {
|
|
69
|
+
installed: boolean
|
|
70
|
+
repo: string
|
|
71
|
+
/** 清单记录的 ref(空串 = 安装时取的默认分支)。 */
|
|
72
|
+
ref: string | null
|
|
73
|
+
/** 提交 sha(内容级溯源)。 */
|
|
74
|
+
sha: string | null
|
|
75
|
+
/** 仓库 VERSION 文件内容。 */
|
|
76
|
+
version: string | null
|
|
77
|
+
root: string
|
|
78
|
+
tree: string
|
|
79
|
+
files: number
|
|
80
|
+
/** 树内发现的全部技能组(相对安装树根)。 */
|
|
81
|
+
groups: string[]
|
|
82
|
+
/** 配置选中且实际挂进预设的组。 */
|
|
83
|
+
activeGroups: string[]
|
|
84
|
+
updatedAt: string | null
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
/** `GET /status` 响应。 */
|
|
68
88
|
export interface StatusDto {
|
|
69
89
|
config: {
|
|
70
90
|
reaVersion: string
|
|
71
91
|
proxyPort: number
|
|
72
92
|
mcpEnabled: boolean
|
|
93
|
+
skillEnabled: boolean
|
|
94
|
+
reverseSkillEnabled: boolean
|
|
95
|
+
reverseSkillRepo: string
|
|
96
|
+
reverseSkillRef: string
|
|
97
|
+
reverseSkillGroups: string[]
|
|
73
98
|
installRoot: string
|
|
74
99
|
}
|
|
75
100
|
install: InstallDto
|
|
76
101
|
skill: SkillDto
|
|
102
|
+
reverse: ReverseSkillDto
|
|
77
103
|
mcp: McpDto
|
|
78
|
-
paths: { installRoot: string; skillRoot: string }
|
|
104
|
+
paths: { installRoot: string; skillRoot: string; reverseRoot: string }
|
|
79
105
|
job: JobDto | null
|
|
80
106
|
}
|
|
81
107
|
|
package/src/exec.ts
CHANGED
|
@@ -53,6 +53,13 @@ export interface JobOperation {
|
|
|
53
53
|
run(ctx: JobTaskContext): Promise<string>
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/** 作业依赖:生效配置、命令执行面、作业完成后的现探刷新。 */
|
|
57
|
+
export interface JobDeps {
|
|
58
|
+
config: AgentPresetReaConfig
|
|
59
|
+
runner: CommandRunner
|
|
60
|
+
refresh(): void
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
/** 实际 runner:spawn 子进程、逐行回调、AbortSignal 触发即杀。 */
|
|
57
64
|
export const spawnRunner: CommandRunner = {
|
|
58
65
|
run: (command, args, options) =>
|
|
@@ -90,32 +97,47 @@ export const spawnRunner: CommandRunner = {
|
|
|
90
97
|
}),
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
/** 本机代理 URL(未配置代理时为 null)。 */
|
|
101
|
+
export function proxyUrl(config: AgentPresetReaConfig): string | null {
|
|
102
|
+
return config.proxyPort > 0 ? `http://127.0.0.1:${config.proxyPort}` : null
|
|
103
|
+
}
|
|
104
|
+
|
|
93
105
|
/**
|
|
94
|
-
*
|
|
95
|
-
* prebuild-install
|
|
96
|
-
*
|
|
106
|
+
* 子进程环境:继承宿主环境,附带代理端口(port > 0 时给 `https_proxy` 系列,
|
|
107
|
+
* prebuild-install 与 curl 都认这些变量——原生依赖 prebuild 下载卡死的正解)。
|
|
108
|
+
* @param config - 插件配置。
|
|
109
|
+
* @param npm - 是否附带 npm 专属开关(npm 子进程用;curl 等非 npm 命令不需要)。
|
|
97
110
|
*/
|
|
98
|
-
export function
|
|
111
|
+
export function commandEnv(config: AgentPresetReaConfig, npm = false): Record<string, string> {
|
|
99
112
|
const env: Record<string, string> = {}
|
|
100
113
|
for (const [key, value] of Object.entries(process.env)) {
|
|
101
114
|
if (value !== undefined) env[key] = value
|
|
102
115
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
if (npm) {
|
|
117
|
+
env['npm_config_audit'] = 'false'
|
|
118
|
+
env['npm_config_fund'] = 'false'
|
|
119
|
+
env['npm_config_update_notifier'] = 'false'
|
|
120
|
+
env['npm_config_loglevel'] = 'http'
|
|
121
|
+
}
|
|
122
|
+
const proxy = proxyUrl(config)
|
|
123
|
+
if (proxy !== null) {
|
|
109
124
|
env['HTTP_PROXY'] = proxy
|
|
110
125
|
env['HTTPS_PROXY'] = proxy
|
|
111
126
|
env['http_proxy'] = proxy
|
|
112
127
|
env['https_proxy'] = proxy
|
|
113
|
-
|
|
114
|
-
|
|
128
|
+
if (npm) {
|
|
129
|
+
env['npm_config_proxy'] = proxy
|
|
130
|
+
env['npm_config_https_proxy'] = proxy
|
|
131
|
+
}
|
|
115
132
|
}
|
|
116
133
|
return env
|
|
117
134
|
}
|
|
118
135
|
|
|
136
|
+
/** npm 子进程环境({@link commandEnv} 的 npm 形态)。 */
|
|
137
|
+
export function npmEnv(config: AgentPresetReaConfig): Record<string, string> {
|
|
138
|
+
return commandEnv(config, true)
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
/** 版本收窄:进命令前把配置里的钉版校验成纯版本号。 */
|
|
120
142
|
export function safeVersion(version: string): string {
|
|
121
143
|
const trimmed = version.trim()
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
* 本插件据此换代注册(注销旧声明 → 按新值重建声明),**新建会话**即用新提示词,
|
|
19
19
|
* 已存在会话保持既有 composition(注册表语义:运行中会话的装配不随换代改变)。
|
|
20
20
|
*
|
|
21
|
+
* 卡片上还有三个能力开关(活动字段:REA MCP 行、REA skill、reverse-skill 技能包)
|
|
22
|
+
* 与两个受管技能源的手动更新:REA 官方 skill(npm 包内 `skills/`)与 GitHub 的
|
|
23
|
+
* `zhaoxuya520/reverse-skill`(路由包,按技能组挂进 `customSkillDirs`)。开关只管
|
|
24
|
+
* 挂不挂,安装副本始终留在插件数据目录。
|
|
25
|
+
*
|
|
21
26
|
* REA 的安装与更新也在卡片上(受管安装 + 后台作业 + 实时进度 + 可选代理端口):
|
|
22
27
|
* 宿主半的 manager 把 `rea-agents` 装进插件数据目录、把 skill 拉成可替换副本,
|
|
23
28
|
* 作业完成后同样换代注册——因此预设声明里**不会**在 boot 期 spawn `npx`
|
|
@@ -69,6 +74,7 @@ export type {
|
|
|
69
74
|
JobProgressDto,
|
|
70
75
|
JobState,
|
|
71
76
|
McpDto,
|
|
77
|
+
ReverseSkillDto,
|
|
72
78
|
SkillDto,
|
|
73
79
|
StatusDto,
|
|
74
80
|
} from './dto.ts'
|
|
@@ -118,8 +124,22 @@ export {
|
|
|
118
124
|
} from './mcp.ts'
|
|
119
125
|
export { PLAN_MODE_SECTION } from './prompt.ts'
|
|
120
126
|
export { createPresetRegistrar, type PresetRegistrar, type Unregister } from './registration.ts'
|
|
127
|
+
export {
|
|
128
|
+
discoverSkillGroups,
|
|
129
|
+
probeReverse,
|
|
130
|
+
REVERSE_DEFAULT_REPO,
|
|
131
|
+
REVERSE_DIR,
|
|
132
|
+
REVERSE_PHASES,
|
|
133
|
+
REVERSE_TREE_DIR,
|
|
134
|
+
type ReverseManifest,
|
|
135
|
+
type ReversePaths,
|
|
136
|
+
type ReverseState,
|
|
137
|
+
reversePaths,
|
|
138
|
+
reverseSkillDirs,
|
|
139
|
+
runReverseSkillJob,
|
|
140
|
+
} from './reverse.ts'
|
|
121
141
|
export { ROUTE_BASE, registerReaRoutes, statusPayload } from './routes.ts'
|
|
122
|
-
export { BUNDLED_SKILL_ROOT, reaSkillDirs } from './skill.ts'
|
|
142
|
+
export { BUNDLED_SKILL_ROOT, presetSkillDirs, reaSkillDirs } from './skill.ts'
|
|
123
143
|
export type { AgentPresetReaConfig, AgentPresetReaConfigFields }
|
|
124
144
|
export { Config }
|
|
125
145
|
|
package/src/manager.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type { AgentPresetReaConfig } from './config.ts'
|
|
|
30
30
|
import {
|
|
31
31
|
type CommandRunner,
|
|
32
32
|
countFiles,
|
|
33
|
+
type JobDeps,
|
|
33
34
|
type JobOperation,
|
|
34
35
|
type JobTaskContext,
|
|
35
36
|
lineSink,
|
|
@@ -39,6 +40,9 @@ import {
|
|
|
39
40
|
safeVersion,
|
|
40
41
|
spawnRunner,
|
|
41
42
|
} from './exec.ts'
|
|
43
|
+
import { probeReverse, REVERSE_PHASES, type ReverseState, runReverseSkillJob } from './reverse.ts'
|
|
44
|
+
|
|
45
|
+
export type { JobDeps }
|
|
42
46
|
|
|
43
47
|
/** npm 包名。 */
|
|
44
48
|
export const REA_PACKAGE = 'rea-agents'
|
|
@@ -80,6 +84,8 @@ export interface SkillState {
|
|
|
80
84
|
export interface RuntimeSnapshot {
|
|
81
85
|
install: ReaInstall | null
|
|
82
86
|
skill: SkillState
|
|
87
|
+
/** GitHub 技能源(zhaoxuya520/reverse-skill)现状。 */
|
|
88
|
+
reverse: ReverseState
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
/** skill 清单(与随包夹具同形;更新副本多写入 updatedAt)。 */
|
|
@@ -107,8 +113,12 @@ export interface ReaManager {
|
|
|
107
113
|
snapshot(): RuntimeSnapshot
|
|
108
114
|
/** 重新现探并更新快照(作业完成、volatile 变更后调用)。 */
|
|
109
115
|
refresh(): RuntimeSnapshot
|
|
110
|
-
/** 受管作业(安装/更新 REA 包、更新 skill)。 */
|
|
111
|
-
operations(overrides?: JobOverrides): {
|
|
116
|
+
/** 受管作业(安装/更新 REA 包、更新 REA skill、更新 reverse-skill)。 */
|
|
117
|
+
operations(overrides?: JobOverrides): {
|
|
118
|
+
install: JobOperation
|
|
119
|
+
skill: JobOperation
|
|
120
|
+
reverse: JobOperation
|
|
121
|
+
}
|
|
112
122
|
}
|
|
113
123
|
|
|
114
124
|
/** 安装作业阶段名。 */
|
|
@@ -219,16 +229,10 @@ export function probeRuntime(
|
|
|
219
229
|
return {
|
|
220
230
|
install: probeInstall(paths),
|
|
221
231
|
skill: probeSkill(paths, bundledRoot, bundledVersion),
|
|
232
|
+
reverse: probeReverse(config),
|
|
222
233
|
}
|
|
223
234
|
}
|
|
224
235
|
|
|
225
|
-
/** 作业依赖:生效配置、命令执行面、作业完成后的现探刷新。 */
|
|
226
|
-
export interface JobDeps {
|
|
227
|
-
config: AgentPresetReaConfig
|
|
228
|
-
runner: CommandRunner
|
|
229
|
-
refresh(): void
|
|
230
|
-
}
|
|
231
|
-
|
|
232
236
|
/**
|
|
233
237
|
* 解析钉版在 registry 上的实际版本:顺带做存在性校验(失败即报错,
|
|
234
238
|
* 而不是装出一棵空树让 MCP 行指向不存在的入口)。
|
|
@@ -413,6 +417,7 @@ export function createManager(options: {
|
|
|
413
417
|
return {
|
|
414
418
|
install: { phases: INSTALL_PHASES, run: (ctx) => runInstallJob(deps, ctx) },
|
|
415
419
|
skill: { phases: SKILL_PHASES, run: (ctx) => runSkillJob(deps, ctx) },
|
|
420
|
+
reverse: { phases: REVERSE_PHASES, run: (ctx) => runReverseSkillJob(deps, ctx) },
|
|
416
421
|
}
|
|
417
422
|
},
|
|
418
423
|
}
|
package/src/reverse.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 第二个受管技能源:GitHub 仓库 `zhaoxuya520/reverse-skill`(路由包:根技能
|
|
3
|
+
* `reverse-skill-router` + 各模块技能目录)。
|
|
4
|
+
*
|
|
5
|
+
* 与 REA 包分开成两个源的理由:来源不同(npm registry vs GitHub 仓库)、版本语义
|
|
6
|
+
* 不同(semver 钉版 vs 提交 sha)、挂载形状不同(单技能根 vs 多技能组),混在一个
|
|
7
|
+
* 作业里会让失败语义含混(npm 成功、GitHub 挂掉时到底算不算成功)。
|
|
8
|
+
*
|
|
9
|
+
* 落点:`<DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/reverse-skill/`
|
|
10
|
+
* - `repo/`:仓库树(`tar --strip-components=1` 解开,不改内容);
|
|
11
|
+
* - `manifest.json`:来源溯源(repo / ref / sha / VERSION / 技能组 / 更新时间)。
|
|
12
|
+
*
|
|
13
|
+
* 技能组发现规则:树里**直接子目录带 `SKILL.md`** 的目录即一个可挂载根(DSH 的
|
|
14
|
+
* skill 扫描正是「根的直接子项 = 技能」)。树根自身不算——它的散装 `.md`
|
|
15
|
+
* (README/AGENTS/RULES 等)会被当成技能文件解析;`plugins/**` 是各客户端的
|
|
16
|
+
* 打包产物(重复一遍路由技能),也不暴露。
|
|
17
|
+
*
|
|
18
|
+
* 作业先落 staging 再换目录:中断只留 staging,运行中的会话继续读旧树。
|
|
19
|
+
* @module @dsh-plus/agent-preset-rea/reverse
|
|
20
|
+
*/
|
|
21
|
+
import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
22
|
+
import { mkdir, rename, rm, writeFile } from 'node:fs/promises'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
|
|
25
|
+
import { pluginDataPath } from '@dsh-plus/shared'
|
|
26
|
+
|
|
27
|
+
import type { AgentPresetReaConfig } from './config.ts'
|
|
28
|
+
import {
|
|
29
|
+
commandEnv,
|
|
30
|
+
countFiles,
|
|
31
|
+
type JobDeps,
|
|
32
|
+
type JobTaskContext,
|
|
33
|
+
lineSink,
|
|
34
|
+
proxyUrl,
|
|
35
|
+
} from './exec.ts'
|
|
36
|
+
|
|
37
|
+
/** 插件数据目录标识(与 manager.ts 同源:`<DSH_HOME>/dsh-plus/<id>`)。 */
|
|
38
|
+
const PLUGIN_ID = 'dsh-plus-agent-preset-rea'
|
|
39
|
+
|
|
40
|
+
/** 本技能源在插件数据目录下的子目录名。 */
|
|
41
|
+
export const REVERSE_DIR = 'reverse-skill'
|
|
42
|
+
|
|
43
|
+
/** 仓库树目录名(换目录作业的替换单元是它的父目录)。 */
|
|
44
|
+
export const REVERSE_TREE_DIR = 'repo'
|
|
45
|
+
|
|
46
|
+
/** 默认仓库。 */
|
|
47
|
+
export const REVERSE_DEFAULT_REPO = 'zhaoxuya520/reverse-skill'
|
|
48
|
+
|
|
49
|
+
/** 手动更新作业阶段名。 */
|
|
50
|
+
export const REVERSE_PHASES: string[] = ['解析仓库信息', '下载源码包', '解包源码', '校验并写入清单']
|
|
51
|
+
|
|
52
|
+
/** 不参与技能组发现的目录名(客户端打包产物;会重复一遍路由技能)。 */
|
|
53
|
+
const SKIP_DIRS = new Set(['.git', 'plugins', 'node_modules'])
|
|
54
|
+
|
|
55
|
+
/** 受管路径。 */
|
|
56
|
+
export interface ReversePaths {
|
|
57
|
+
/** 技能源根(`repo/` 与 `manifest.json` 的父目录)。 */
|
|
58
|
+
root: string
|
|
59
|
+
/** 仓库树根。 */
|
|
60
|
+
tree: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 清单(溯源事实;sha 已是内容级溯源,故不再逐文件复算哈希)。 */
|
|
64
|
+
export interface ReverseManifest {
|
|
65
|
+
source: string
|
|
66
|
+
repo: string
|
|
67
|
+
/** 配置里的 ref(空串 = 仓库默认分支)。 */
|
|
68
|
+
ref: string
|
|
69
|
+
/** GitHub 机构 API 解析到的提交 sha。 */
|
|
70
|
+
sha: string
|
|
71
|
+
/** 仓库 `VERSION` 文件内容(无则 null)。 */
|
|
72
|
+
version: string | null
|
|
73
|
+
/** 树内文件数。 */
|
|
74
|
+
files: number
|
|
75
|
+
/** 树内发现的技能组(相对路径)。 */
|
|
76
|
+
groups: string[]
|
|
77
|
+
updatedAt: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 技能源现状。 */
|
|
81
|
+
export interface ReverseState {
|
|
82
|
+
installed: boolean
|
|
83
|
+
repo: string
|
|
84
|
+
ref: string | null
|
|
85
|
+
sha: string | null
|
|
86
|
+
version: string | null
|
|
87
|
+
files: number
|
|
88
|
+
/** 树内发现的全部技能组(相对路径)。 */
|
|
89
|
+
groups: string[]
|
|
90
|
+
updatedAt: string | null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 未安装时的现状(导出给 emptyRuntime 等默认场景用)。 */
|
|
94
|
+
export function emptyReverseState(repo = REVERSE_DEFAULT_REPO): ReverseState {
|
|
95
|
+
return missingState(repo)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 未安装时的现状。 */
|
|
99
|
+
function missingState(repo: string): ReverseState {
|
|
100
|
+
return {
|
|
101
|
+
installed: false,
|
|
102
|
+
repo,
|
|
103
|
+
ref: null,
|
|
104
|
+
sha: null,
|
|
105
|
+
version: null,
|
|
106
|
+
files: 0,
|
|
107
|
+
groups: [],
|
|
108
|
+
updatedAt: null,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 受管路径(固定落插件数据目录,无部署方可调项)。 */
|
|
113
|
+
export function reversePaths(): ReversePaths {
|
|
114
|
+
const root = pluginDataPath(PLUGIN_ID, REVERSE_DIR)
|
|
115
|
+
return { root, tree: join(root, REVERSE_TREE_DIR) }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 读 JSON(不存在或坏内容返回 null)。 */
|
|
119
|
+
function readJson(file: string): unknown {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(readFileSync(file, 'utf8')) as unknown
|
|
122
|
+
} catch {
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 记录型守卫。 */
|
|
128
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
129
|
+
return typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 直接子目录名(跳过不参与发现的目录;目录读不到按无子目录)。 */
|
|
133
|
+
function subdirs(dir: string): string[] {
|
|
134
|
+
let entries: Dirent[]
|
|
135
|
+
try {
|
|
136
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
137
|
+
} catch {
|
|
138
|
+
return []
|
|
139
|
+
}
|
|
140
|
+
return entries
|
|
141
|
+
.filter((entry) => entry.isDirectory() && !SKIP_DIRS.has(entry.name))
|
|
142
|
+
.map((entry) => entry.name)
|
|
143
|
+
.sort()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 发现树内的技能组:直接子目录带 SKILL.md 的目录(排除树根与 SKIP_DIRS)。 */
|
|
147
|
+
export function discoverSkillGroups(tree: string): string[] {
|
|
148
|
+
const groups: string[] = []
|
|
149
|
+
const walk = (rel: string): void => {
|
|
150
|
+
const abs = rel === '' ? tree : join(tree, rel)
|
|
151
|
+
const names = subdirs(abs)
|
|
152
|
+
if (names.some((name) => existsSync(join(abs, name, 'SKILL.md')))) groups.push(rel)
|
|
153
|
+
for (const name of names) {
|
|
154
|
+
if (SKIP_DIRS.has(name)) continue
|
|
155
|
+
walk(rel === '' ? name : `${rel}/${name}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (existsSync(tree)) walk('')
|
|
159
|
+
return groups.filter((rel) => rel !== '')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 现探技能源(清单可读 + 树在 + 至少一个技能组才算可用)。 */
|
|
163
|
+
export function probeReverse(
|
|
164
|
+
config: AgentPresetReaConfig,
|
|
165
|
+
repo = config.reverseSkillRepo,
|
|
166
|
+
): ReverseState {
|
|
167
|
+
const { root, tree } = reversePaths()
|
|
168
|
+
const manifest = asRecord(readJson(join(root, 'manifest.json')))
|
|
169
|
+
if (manifest === null || !existsSync(tree)) return missingState(repo)
|
|
170
|
+
const sha = typeof manifest['sha'] === 'string' ? manifest['sha'] : null
|
|
171
|
+
if (sha === null) return missingState(repo)
|
|
172
|
+
const groups = discoverSkillGroups(tree)
|
|
173
|
+
if (groups.length === 0) return missingState(repo)
|
|
174
|
+
return {
|
|
175
|
+
installed: true,
|
|
176
|
+
repo: typeof manifest['repo'] === 'string' ? manifest['repo'] : repo,
|
|
177
|
+
ref: typeof manifest['ref'] === 'string' ? manifest['ref'] : '',
|
|
178
|
+
sha,
|
|
179
|
+
version: typeof manifest['version'] === 'string' ? manifest['version'] : null,
|
|
180
|
+
files: typeof manifest['files'] === 'number' ? manifest['files'] : countFiles(tree),
|
|
181
|
+
groups,
|
|
182
|
+
updatedAt: typeof manifest['updatedAt'] === 'string' ? manifest['updatedAt'] : null,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* 挂进本预设的技能组目录:配置选中 ∩ 树内发现。
|
|
188
|
+
*
|
|
189
|
+
* 组是否真实存在由现探的 {@link discoverSkillGroups} 负责(发现即存在),这里不再
|
|
190
|
+
* 二次查盘——否则「未安装/组缺席」两种语义会在两处各判一遍。
|
|
191
|
+
* @param config - 插件配置。
|
|
192
|
+
* @param state - 现探现状。
|
|
193
|
+
*/
|
|
194
|
+
export function reverseSkillDirs(config: AgentPresetReaConfig, state: ReverseState): string[] {
|
|
195
|
+
if (!state.installed) return []
|
|
196
|
+
const { tree } = reversePaths()
|
|
197
|
+
return config.reverseSkillGroups
|
|
198
|
+
.map((group) => group.trim())
|
|
199
|
+
.filter((group) => group !== '' && state.groups.includes(group))
|
|
200
|
+
.map((group) => join(tree, group))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 作业依赖(与 manager 的作业同形:配置 + 命令执行面 + 现探刷新)。 */
|
|
204
|
+
export type ReverseDeps = JobDeps
|
|
205
|
+
|
|
206
|
+
/** curl 通用参数:失败即非零退出、跟随跳转、静默进度。 */
|
|
207
|
+
function curlArgs(config: AgentPresetReaConfig): string[] {
|
|
208
|
+
const proxy = proxyUrl(config)
|
|
209
|
+
return ['-fsSL', ...(proxy === null ? [] : ['-x', proxy])]
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 跑一次 curl 并落文件(失败抛错)。 */
|
|
213
|
+
async function curl(
|
|
214
|
+
deps: ReverseDeps,
|
|
215
|
+
ctx: JobTaskContext,
|
|
216
|
+
args: string[],
|
|
217
|
+
target: string,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
const result = await deps.runner.run('curl', [...curlArgs(deps.config), '-o', target, ...args], {
|
|
220
|
+
env: commandEnv(deps.config),
|
|
221
|
+
signal: ctx.signal,
|
|
222
|
+
onLine: lineSink(ctx),
|
|
223
|
+
})
|
|
224
|
+
if (result.code !== 0) throw new Error(`curl 失败(退出码 ${result.code}):${args.join(' ')}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** GitHub API 取仓库信息(默认分支)与提交 sha。 */
|
|
228
|
+
async function resolveRepo(
|
|
229
|
+
deps: ReverseDeps,
|
|
230
|
+
ctx: JobTaskContext,
|
|
231
|
+
staging: string,
|
|
232
|
+
): Promise<{ sha: string; ref: string }> {
|
|
233
|
+
ctx.phase(0)
|
|
234
|
+
const repo = deps.config.reverseSkillRepo
|
|
235
|
+
const api = 'https://api.github.com'
|
|
236
|
+
const metaFile = join(staging, 'meta.json')
|
|
237
|
+
await curl(deps, ctx, [`${api}/repos/${repo}`], metaFile)
|
|
238
|
+
const meta = asRecord(readJson(metaFile))
|
|
239
|
+
const defaultBranch = typeof meta?.['default_branch'] === 'string' ? meta['default_branch'] : null
|
|
240
|
+
if (defaultBranch === null) throw new Error(`仓库信息不完整(${repo})`)
|
|
241
|
+
|
|
242
|
+
const ref =
|
|
243
|
+
deps.config.reverseSkillRef.trim() === '' ? defaultBranch : deps.config.reverseSkillRef.trim()
|
|
244
|
+
const commitFile = join(staging, 'commit.json')
|
|
245
|
+
await curl(deps, ctx, [`${api}/repos/${repo}/commits/${ref}`], commitFile)
|
|
246
|
+
const commit = asRecord(readJson(commitFile))
|
|
247
|
+
const sha = typeof commit?.['sha'] === 'string' ? commit['sha'] : null
|
|
248
|
+
if (sha === null) throw new Error(`解析提交失败(${repo}@${ref})`)
|
|
249
|
+
ctx.log(`仓库 ${repo}:默认分支 ${defaultBranch},ref ${ref},提交 ${sha.slice(0, 12)}`)
|
|
250
|
+
return { sha, ref }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 下载源码包并解包到 `staging/tree`。 */
|
|
254
|
+
async function fetchTree(deps: ReverseDeps, ctx: JobTaskContext, staging: string): Promise<string> {
|
|
255
|
+
const { sha } = await resolveRepo(deps, ctx, staging)
|
|
256
|
+
// 树目录名与 {@link reversePaths} 对齐:staging 顶掉旧根后即 `<root>/repo`。
|
|
257
|
+
const tree = join(staging, REVERSE_TREE_DIR)
|
|
258
|
+
const tarball = join(staging, 'repo.tgz')
|
|
259
|
+
|
|
260
|
+
ctx.phase(1)
|
|
261
|
+
await curl(
|
|
262
|
+
deps,
|
|
263
|
+
ctx,
|
|
264
|
+
[`https://codeload.github.com/${deps.config.reverseSkillRepo}/tar.gz/${sha}`],
|
|
265
|
+
tarball,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
ctx.phase(2)
|
|
269
|
+
await mkdir(tree, { recursive: true })
|
|
270
|
+
const untar = await deps.runner.run(
|
|
271
|
+
'tar',
|
|
272
|
+
['-xzf', tarball, '-C', tree, '--strip-components=1'],
|
|
273
|
+
{ env: commandEnv(deps.config), signal: ctx.signal, onLine: lineSink(ctx) },
|
|
274
|
+
)
|
|
275
|
+
if (untar.code !== 0) throw new Error(`tar 解包失败(退出码 ${untar.code})`)
|
|
276
|
+
return tree
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** 读仓库 VERSION(无则 null)。 */
|
|
280
|
+
function readVersion(tree: string): string | null {
|
|
281
|
+
try {
|
|
282
|
+
const text = readFileSync(join(tree, 'VERSION'), 'utf8').trim()
|
|
283
|
+
return text === '' ? null : (text.split('\n')[0] ?? null)
|
|
284
|
+
} catch {
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** 手动更新作业:拉默认分支/指定 ref 的最新提交,解包后换目录并写清单。 */
|
|
290
|
+
export async function runReverseSkillJob(deps: ReverseDeps, ctx: JobTaskContext): Promise<string> {
|
|
291
|
+
const { root } = reversePaths()
|
|
292
|
+
const staging = `${root}.staging`
|
|
293
|
+
await rm(staging, { recursive: true, force: true })
|
|
294
|
+
await mkdir(staging, { recursive: true })
|
|
295
|
+
try {
|
|
296
|
+
const tree = await fetchTree(deps, ctx, staging)
|
|
297
|
+
|
|
298
|
+
ctx.phase(3)
|
|
299
|
+
const groups = discoverSkillGroups(tree)
|
|
300
|
+
if (groups.length === 0) throw new Error('仓库结构不含技能组(找不到带 SKILL.md 的子目录)')
|
|
301
|
+
const meta = asRecord(readJson(join(staging, 'commit.json')))
|
|
302
|
+
const sha = typeof meta?.['sha'] === 'string' ? meta['sha'] : ''
|
|
303
|
+
// 换目录前清掉作业草稿(tarball 与 API 响应都只是取数中间物,不该留在受管根里)。
|
|
304
|
+
for (const scratch of ['repo.tgz', 'meta.json', 'commit.json']) {
|
|
305
|
+
await rm(join(staging, scratch), { force: true })
|
|
306
|
+
}
|
|
307
|
+
const manifest: ReverseManifest = {
|
|
308
|
+
source: `github:${deps.config.reverseSkillRepo}@${sha}`,
|
|
309
|
+
repo: deps.config.reverseSkillRepo,
|
|
310
|
+
ref: deps.config.reverseSkillRef.trim(),
|
|
311
|
+
sha,
|
|
312
|
+
version: readVersion(tree),
|
|
313
|
+
files: countFiles(tree),
|
|
314
|
+
groups,
|
|
315
|
+
updatedAt: new Date().toISOString(),
|
|
316
|
+
}
|
|
317
|
+
await writeFile(join(staging, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
|
318
|
+
|
|
319
|
+
// 换目录:staging 顶掉旧根,旧根留 .old 再清(失败时旧树仍完整可用)。
|
|
320
|
+
const backup = `${root}.old`
|
|
321
|
+
await rm(backup, { recursive: true, force: true })
|
|
322
|
+
if (existsSync(root)) await rename(root, backup)
|
|
323
|
+
await rename(staging, root)
|
|
324
|
+
await rm(backup, { recursive: true, force: true })
|
|
325
|
+
|
|
326
|
+
deps.refresh()
|
|
327
|
+
ctx.log(
|
|
328
|
+
`已更新 ${deps.config.reverseSkillRepo}@${sha.slice(0, 12)}:${manifest.files} 文件、技能组 ${groups.join(', ')}`,
|
|
329
|
+
)
|
|
330
|
+
return manifest.version ?? sha.slice(0, 12)
|
|
331
|
+
} catch (error) {
|
|
332
|
+
await rm(staging, { recursive: true, force: true })
|
|
333
|
+
throw error
|
|
334
|
+
}
|
|
335
|
+
}
|