@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/src/routes.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
- * 受管安装/更新的 HTTP 通道:状态查询 + 三类动作(安装/更新 REA、更新 skill、取消)。
2
+ * 受管安装/更新的 HTTP 通道:状态查询 + 四类动作(安装/更新 REA、更新 REA skill、
3
+ * 更新 reverse-skill、取消作业)。
3
4
  *
4
5
  * 只挂在 dsh web 同源(webserver 默认 127.0.0.1,与 notify-email 的测试端点同等暴露面):
5
- * 这些端点会 spawn npm 并写插件数据目录,不做鉴权以外的额外暴露面扩张。
6
+ * 这些端点会 spawn npm/curl 并写插件数据目录,不做鉴权以外的额外暴露面扩张。
6
7
  * 配置读写仍走官方 settings(卡片直连 remote.settings),这里只补官方传输没有的动作。
7
8
  *
8
9
  * 请求体只接受一个可选字段 `proxyPort`(0..65535);其余一律来自插件配置。
@@ -15,10 +16,11 @@ import type { Context } from '@deepseek-ai/cordis'
15
16
  import type {} from '@deepseek-ai/dsh-host-webserver'
16
17
 
17
18
  import type { AgentPresetReaConfig } from './config.ts'
18
- import type { JobActionDto, StatusDto } from './dto.ts'
19
+ import type { JobActionDto, JobKind, StatusDto } from './dto.ts'
19
20
  import type { JobBoard } from './jobs.ts'
20
21
  import type { ReaManager } from './manager.ts'
21
22
  import { type McpLaunch, reaMcpLaunch } from './mcp.ts'
23
+ import { reversePaths, reverseSkillDirs } from './reverse.ts'
22
24
 
23
25
  /** 路由前缀。 */
24
26
  export const ROUTE_BASE = '/dsh-plus/agent-preset-rea'
@@ -33,6 +35,18 @@ export interface ReaRoutesOptions {
33
35
  config: () => AgentPresetReaConfig
34
36
  }
35
37
 
38
+ /** 路由处理器形态。 */
39
+ type RouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>
40
+
41
+ /** 响应发送器。 */
42
+ type Send = (res: ServerResponse, status: number, body: unknown) => void
43
+
44
+ /** 日志窄面。 */
45
+ interface RouteLogger {
46
+ info(message: string): void
47
+ warn(message: string): void
48
+ }
49
+
36
50
  /** 读请求体(超限即拒绝;空体按空对象)。 */
37
51
  async function readBody(req: IncomingMessage): Promise<unknown> {
38
52
  const chunks: Buffer[] = []
@@ -66,11 +80,18 @@ export function statusPayload(options: ReaRoutesOptions): StatusDto {
66
80
  const launch: McpLaunch | null = reaMcpLaunch(config, runtime.install)
67
81
  const mode =
68
82
  config.mcpCommand.trim() !== '' ? 'custom' : launch === null ? 'missing' : 'installed'
83
+ const reverse = reversePaths()
84
+ const activeDirs = reverseSkillDirs(config, runtime.reverse)
69
85
  return {
70
86
  config: {
71
87
  reaVersion: config.reaVersion,
72
88
  proxyPort: config.proxyPort,
73
89
  mcpEnabled: config.mcpEnabled,
90
+ skillEnabled: config.skillEnabled,
91
+ reverseSkillEnabled: config.reverseSkillEnabled,
92
+ reverseSkillRepo: config.reverseSkillRepo,
93
+ reverseSkillRef: config.reverseSkillRef,
94
+ reverseSkillGroups: [...config.reverseSkillGroups],
74
95
  installRoot: config.installRoot,
75
96
  },
76
97
  install: {
@@ -80,102 +101,123 @@ export function statusPayload(options: ReaRoutesOptions): StatusDto {
80
101
  entry: runtime.install?.entry ?? null,
81
102
  },
82
103
  skill: { ...runtime.skill },
104
+ reverse: {
105
+ installed: runtime.reverse.installed,
106
+ repo: runtime.reverse.repo,
107
+ ref: runtime.reverse.ref,
108
+ sha: runtime.reverse.sha,
109
+ version: runtime.reverse.version,
110
+ root: reverse.root,
111
+ tree: reverse.tree,
112
+ files: runtime.reverse.files,
113
+ groups: [...runtime.reverse.groups],
114
+ activeGroups: activeDirs.map((dir) =>
115
+ dir.startsWith(`${reverse.tree}/`) ? dir.slice(reverse.tree.length + 1) : dir,
116
+ ),
117
+ updatedAt: runtime.reverse.updatedAt,
118
+ },
83
119
  mcp: {
84
120
  mode,
85
121
  enabled: config.mcpEnabled && launch !== null,
86
122
  command: launch?.command ?? null,
87
123
  args: launch?.args ?? [],
88
124
  },
89
- paths,
125
+ paths: { ...paths, reverseRoot: reverse.root },
90
126
  job: options.board.snapshot(),
91
127
  }
92
128
  }
93
129
 
94
- /** 注册全部路由。 */
95
- export function registerReaRoutes(ctx: Context, options: ReaRoutesOptions): void {
96
- const logger = ctx.logger('agent-preset-rea')
97
- const send = (res: ServerResponse, status: number, body: unknown): void => {
130
+ /** 发送 JSON 响应。 */
131
+ function makeSend(): Send {
132
+ return (res, status, body) => {
98
133
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
99
134
  res.end(JSON.stringify(body))
100
135
  }
101
- const guard = (
102
- handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>,
103
- ): ((req: IncomingMessage, res: ServerResponse) => Promise<void>) => {
104
- return async (req, res) => {
105
- try {
106
- await handler(req, res)
107
- } catch (error) {
108
- const message = error instanceof Error ? error.message : String(error)
109
- logger.warn(`${req.method ?? '?'} ${req.url ?? '?'} 失败:${message}`)
110
- if (!res.headersSent) send(res, 400, { job: null, error: message } satisfies JobActionDto)
111
- else res.end()
112
- }
136
+ }
137
+
138
+ /** 异常兜底:日志 + 400(响应头已发则只收尾)。 */
139
+ function makeGuard(logger: RouteLogger, send: Send): (handler: RouteHandler) => RouteHandler {
140
+ return (handler) => async (req, res) => {
141
+ try {
142
+ await handler(req, res)
143
+ } catch (error) {
144
+ const message = error instanceof Error ? error.message : String(error)
145
+ logger.warn(`${req.method ?? '?'} ${req.url ?? '?'} 失败:${message}`)
146
+ if (!res.headersSent) send(res, 400, { job: null, error: message } satisfies JobActionDto)
147
+ else res.end()
148
+ }
149
+ }
150
+ }
151
+
152
+ /** 方法门:不匹配即 405。 */
153
+ function withMethod(method: string, send: Send, handler: RouteHandler): RouteHandler {
154
+ return async (req, res) => {
155
+ if (req.method !== method) {
156
+ send(res, 405, { job: null, error: 'method not allowed' } satisfies JobActionDto)
157
+ return
113
158
  }
159
+ await handler(req, res)
114
160
  }
115
- const startJob = async (
116
- req: IncomingMessage,
117
- res: ServerResponse,
118
- kind: 'install' | 'skill',
119
- ): Promise<void> => {
161
+ }
162
+
163
+ /** 起作业的处理器工厂(409 = 已有作业在跑)。 */
164
+ function startJobHandler(
165
+ kind: JobKind,
166
+ options: ReaRoutesOptions,
167
+ logger: RouteLogger,
168
+ send: Send,
169
+ ): RouteHandler {
170
+ return async (req, res) => {
120
171
  const overrides = proxyPortOf(await readBody(req))
121
172
  const operations = options.manager.operations(
122
173
  overrides === undefined ? undefined : { proxyPort: overrides },
123
174
  )
124
175
  const job = options.board.start(kind, operations[kind])
125
176
  if (job === null) {
126
- send(res, 409, { job: options.board.snapshot(), error: '已有作业在运行' })
177
+ send(res, 409, {
178
+ job: options.board.snapshot(),
179
+ error: '已有作业在运行',
180
+ } satisfies JobActionDto)
127
181
  return
128
182
  }
129
183
  logger.info(`已启动 ${kind} 作业`)
130
184
  send(res, 200, { job } satisfies JobActionDto)
131
185
  }
186
+ }
132
187
 
133
- ctx.webServer.register({
134
- kind: 'exact',
135
- path: `${ROUTE_BASE}/status`,
136
- handler: guard(async (req, res) => {
137
- if (req.method !== 'GET') {
138
- send(res, 405, { job: null, error: 'method not allowed' })
139
- return
140
- }
188
+ /** 全部路由(path 末段 → 处理器)。 */
189
+ function buildRoutes(
190
+ options: ReaRoutesOptions,
191
+ logger: RouteLogger,
192
+ send: Send,
193
+ ): Record<string, RouteHandler> {
194
+ return {
195
+ status: withMethod('GET', send, async (_req, res) => {
141
196
  send(res, 200, statusPayload(options))
142
197
  }),
143
- })
144
- ctx.webServer.register({
145
- kind: 'exact',
146
- path: `${ROUTE_BASE}/install`,
147
- handler: guard(async (req, res) => {
148
- if (req.method !== 'POST') {
149
- send(res, 405, { job: null, error: 'method not allowed' })
150
- return
151
- }
152
- await startJob(req, res, 'install')
153
- }),
154
- })
155
- ctx.webServer.register({
156
- kind: 'exact',
157
- path: `${ROUTE_BASE}/skill`,
158
- handler: guard(async (req, res) => {
159
- if (req.method !== 'POST') {
160
- send(res, 405, { job: null, error: 'method not allowed' })
161
- return
162
- }
163
- await startJob(req, res, 'skill')
164
- }),
165
- })
166
- ctx.webServer.register({
167
- kind: 'exact',
168
- path: `${ROUTE_BASE}/cancel`,
169
- handler: guard(async (req, res) => {
170
- if (req.method !== 'POST') {
171
- send(res, 405, { job: null, error: 'method not allowed' })
172
- return
173
- }
198
+ install: withMethod('POST', send, startJobHandler('install', options, logger, send)),
199
+ skill: withMethod('POST', send, startJobHandler('skill', options, logger, send)),
200
+ reverse: withMethod('POST', send, startJobHandler('reverse', options, logger, send)),
201
+ cancel: withMethod('POST', send, async (_req, res) => {
174
202
  const cancelled = options.board.cancel()
175
203
  send(res, 200, {
176
204
  job: options.board.snapshot(),
177
205
  ...(cancelled ? {} : { error: '没有正在运行的作业' }),
178
206
  } satisfies JobActionDto)
179
207
  }),
180
- })
208
+ }
209
+ }
210
+
211
+ /** 注册全部路由。 */
212
+ export function registerReaRoutes(ctx: Context, options: ReaRoutesOptions): void {
213
+ const logger = ctx.logger('agent-preset-rea')
214
+ const send = makeSend()
215
+ const guard = makeGuard(logger, send)
216
+ for (const [name, handler] of Object.entries(buildRoutes(options, logger, send))) {
217
+ ctx.webServer.register({
218
+ kind: 'exact',
219
+ path: `${ROUTE_BASE}/${name}`,
220
+ handler: guard(handler),
221
+ })
222
+ }
181
223
  }
package/src/skill.ts CHANGED
@@ -11,6 +11,8 @@
11
11
  import { fileURLToPath } from 'node:url'
12
12
 
13
13
  import type { AgentPresetReaConfig } from './config.ts'
14
+ import type { RuntimeSnapshot } from './manager.ts'
15
+ import { reverseSkillDirs } from './reverse.ts'
14
16
 
15
17
  /**
16
18
  * 随包 REA 官方 skill 根(包内 `skills/`)。
@@ -21,14 +23,27 @@ import type { AgentPresetReaConfig } from './config.ts'
21
23
  export const BUNDLED_SKILL_ROOT = fileURLToPath(new URL('../skills', import.meta.url))
22
24
 
23
25
  /**
24
- * skill-filesystem 的追加根清单。
26
+ * REA skill 根(随包副本,或手动更新副本——后者存在即替代前者)。
27
+ * @param updatedRoot - 手动更新副本根(来自 manager 现探)。
28
+ */
29
+ export function reaSkillDirs(updatedRoot: string | null = null): string[] {
30
+ return updatedRoot === null ? [BUNDLED_SKILL_ROOT] : [updatedRoot]
31
+ }
32
+
33
+ /**
34
+ * 本预设 `skill-filesystem` 行的完整追加根清单,按三个开关裁剪:
35
+ * - `skillEnabled=false` → 不挂 REA 官方 skill(安装副本仍留在磁盘);
36
+ * - `reverseSkillEnabled=false` → 不挂 reverse-skill 的技能组;
37
+ * - `extraSkillDirs` 是部署方追加项,不受开关影响(关掉的是本插件自带的两个源)。
25
38
  * @param config - 插件配置。
26
- * @param updatedRoot - 手动更新副本根(存在即替代随包根;来自 manager 现探)。
39
+ * @param runtime - 现探运行期事实。
27
40
  */
28
- export function reaSkillDirs(
29
- config: AgentPresetReaConfig,
30
- updatedRoot: string | null = null,
31
- ): string[] {
32
- const base = updatedRoot === null ? [BUNDLED_SKILL_ROOT] : [updatedRoot]
33
- return [...base, ...config.extraSkillDirs]
41
+ export function presetSkillDirs(config: AgentPresetReaConfig, runtime: RuntimeSnapshot): string[] {
42
+ const rea =
43
+ config.skillEnabled === false
44
+ ? []
45
+ : reaSkillDirs(runtime.skill.source === 'updated' ? runtime.skill.root : null)
46
+ const reverse =
47
+ config.reverseSkillEnabled === false ? [] : reverseSkillDirs(config, runtime.reverse)
48
+ return [...rea, ...reverse, ...config.extraSkillDirs]
34
49
  }