@dsh-plus/agent-preset-rea 0.1.0 → 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.
@@ -0,0 +1,373 @@
1
+ /**
2
+ * 卡片的「REA 安装与更新」区:只读状态表 + 四个动作(安装/更新 REA、更新 REA skill、
3
+ * 更新 reverse-skill、取消作业)+ 运行中作业的阶段进度条、已用时与日志尾部。
4
+ *
5
+ * 状态与作业都来自宿主半的同源端点(见 client/api.ts 与宿主 routes.ts):这里只管
6
+ * 轮询与呈现。作业在跑时按 {@link PROGRESS_POLL_MS} 轮询——进度是宿主侧的真事实,
7
+ * 浏览器不自己造百分比。
8
+ * @module agent-preset-rea/client/install
9
+ */
10
+ import { Button } from '@dsh-plus/shared/client'
11
+ import { type ReactElement, type ReactNode, useCallback, useEffect, useRef, useState } from 'react'
12
+
13
+ import type { JobDto, ReverseSkillDto, SkillDto, StatusDto } from '../dto.ts'
14
+ import { cancelJob, fetchStatus, startJob } from './api.ts'
15
+ import type { Translate } from './i18n.ts'
16
+
17
+ /** 作业运行中的状态轮询间隔(毫秒)。 */
18
+ const PROGRESS_POLL_MS = 1000
19
+
20
+ /** 日志展示尾部行数(宿主已按 200 行截断,这里再收一道以控高度)。 */
21
+ const LOG_TAIL = 12
22
+
23
+ /** 错误对象 → 人话。 */
24
+ function messageOf(error: unknown): string {
25
+ return error instanceof Error ? error.message : String(error)
26
+ }
27
+
28
+ /** 起止时间 → 已用时文本。 */
29
+ function elapsedOf(job: JobDto): string {
30
+ const start = Date.parse(job.startedAt)
31
+ if (Number.isNaN(start)) return '—'
32
+ const end = job.finishedAt === null ? Date.now() : Date.parse(job.finishedAt)
33
+ const seconds = Math.max(0, Math.round((end - start) / 1000))
34
+ if (seconds < 60) return `${seconds}s`
35
+ return `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, '0')}s`
36
+ }
37
+
38
+ /** 作业状态 → 徽标类名后缀(视觉区分运行中/成功/失败)。 */
39
+ const STATE_CLASS = {
40
+ running: 'Running',
41
+ ok: 'Ok',
42
+ error: 'Error',
43
+ cancelled: 'Cancelled',
44
+ } as const
45
+
46
+ /** 作业种类 → 文案键。 */
47
+ const JOB_LABEL = { install: 'jobInstall', skill: 'jobSkill', reverse: 'jobReverse' } as const
48
+
49
+ /** 进度条百分比(防御性收窄,避免越界宽度)。 */
50
+ function widthOf(percent: number): string {
51
+ return `${Math.max(0, Math.min(100, percent))}%`
52
+ }
53
+
54
+ /** 作业进度块:状态徽标 + 阶段 + 进度条 + 日志尾部。 */
55
+ function JobPanel(props: { t: Translate; job: JobDto }): ReactElement {
56
+ const { t, job } = props
57
+ const stateKey =
58
+ job.state === 'running'
59
+ ? 'jobRunning'
60
+ : job.state === 'ok'
61
+ ? 'jobOk'
62
+ : job.state === 'error'
63
+ ? 'jobError'
64
+ : 'jobCancelled'
65
+ const lines = job.log.slice(-LOG_TAIL)
66
+ return (
67
+ <div className="apr-job">
68
+ <div className="apr-jobHead">
69
+ <span className={`apr-pill apr-pill${STATE_CLASS[job.state]}`}>{t(stateKey)}</span>
70
+ <span className="apr-jobTitle">{t(JOB_LABEL[job.kind])}</span>
71
+ <span className="apr-hint">
72
+ {t('jobPhase')} {job.progress.step}/{job.progress.steps}·{job.progress.phase}
73
+ </span>
74
+ <span className="apr-hint">
75
+ {t('jobElapsed')} {elapsedOf(job)}
76
+ </span>
77
+ </div>
78
+ <div
79
+ className="apr-bar"
80
+ role="progressbar"
81
+ aria-valuemin={0}
82
+ aria-valuemax={100}
83
+ aria-valuenow={job.progress.percent}
84
+ >
85
+ <div className="apr-barFill" style={{ width: widthOf(job.progress.percent) }} />
86
+ </div>
87
+ {job.error === null ? null : <p className="apr-error">{job.error}</p>}
88
+ <details className="apr-logBox" open={job.state !== 'ok'}>
89
+ <summary className="apr-logSummary">{t('jobLog')}</summary>
90
+ <pre className="apr-log">{lines.length === 0 ? t('jobLogEmpty') : lines.join('\n')}</pre>
91
+ </details>
92
+ </div>
93
+ )
94
+ }
95
+
96
+ /** 一行状态(键 + 值)。 */
97
+ function StatusRow(props: { label: string; children: ReactNode }): ReactElement {
98
+ return (
99
+ <div className="apr-installRow">
100
+ <dt className="apr-installKey">{props.label}</dt>
101
+ <dd className="apr-installVal">{props.children}</dd>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ /** 状态徽标(已就绪 / 未就绪 / 已关闭)。 */
107
+ function Pill(props: { ok: boolean; label: string }): ReactElement {
108
+ return (
109
+ <span className={`apr-pill ${props.ok ? 'apr-pillOk' : 'apr-pillMiss'}`}>{props.label}</span>
110
+ )
111
+ }
112
+
113
+ /** REA 包与 MCP 行状态。 */
114
+ function InstallRows(props: { t: Translate; status: StatusDto }): ReactElement {
115
+ const { t, status } = props
116
+ const mcpLabel = !status.mcp.enabled
117
+ ? status.config.mcpEnabled
118
+ ? t('mcpMissing')
119
+ : t('mcpOff')
120
+ : status.mcp.mode === 'custom'
121
+ ? t('mcpCustom')
122
+ : t('mcpInstalled')
123
+ return (
124
+ <>
125
+ <StatusRow label={t('installPackage')}>
126
+ <Pill
127
+ ok={status.install.installed}
128
+ label={t(status.install.installed ? 'installed' : 'notInstalled')}
129
+ />
130
+ {status.install.version === null ? null : (
131
+ <span className="apr-mono">{status.install.version}</span>
132
+ )}
133
+ <span className="apr-hint">
134
+ {t('installTarget')} {status.config.reaVersion}
135
+ </span>
136
+ </StatusRow>
137
+ <StatusRow label={t('installPath')}>
138
+ <code className="apr-mono">{status.install.root}</code>
139
+ </StatusRow>
140
+ <StatusRow label={t('mcpRow')}>
141
+ <span className="apr-mono">{mcpLabel}</span>
142
+ {status.mcp.command === null ? null : (
143
+ <code className="apr-mono">
144
+ {status.mcp.command} {status.mcp.args.join(' ')}
145
+ </code>
146
+ )}
147
+ </StatusRow>
148
+ </>
149
+ )
150
+ }
151
+
152
+ /** REA 官方 skill 状态。 */
153
+ function SkillRows(props: { t: Translate; skill: SkillDto; enabled: boolean }): ReactElement {
154
+ const { t, skill, enabled } = props
155
+ const version = skill.version === null ? '' : ` · ${skill.version}`
156
+ return (
157
+ <StatusRow label={t('skillSource')}>
158
+ <Pill ok={enabled} label={t(enabled ? 'enabled' : 'disabled')} />
159
+ <span className="apr-mono">
160
+ {t(skill.source === 'updated' ? 'skillUpdated' : 'skillBundled')}
161
+ </span>
162
+ <span className="apr-hint">
163
+ {t('skillFiles')} {skill.files}
164
+ {version}
165
+ </span>
166
+ {skill.updatedAt === null ? null : (
167
+ <span className="apr-hint">
168
+ {t('updatedAt')} {skill.updatedAt}
169
+ </span>
170
+ )}
171
+ </StatusRow>
172
+ )
173
+ }
174
+
175
+ /** reverse-skill(GitHub 源)状态。 */
176
+ function ReverseRows(props: {
177
+ t: Translate
178
+ reverse: ReverseSkillDto
179
+ enabled: boolean
180
+ }): ReactElement {
181
+ const { t, reverse, enabled } = props
182
+ const active = reverse.activeGroups.length === 0 ? '—' : reverse.activeGroups.join(', ')
183
+ const all = reverse.groups.length === 0 ? '—' : reverse.groups.join(', ')
184
+ return (
185
+ <>
186
+ <StatusRow label={t('reverseSource')}>
187
+ <Pill ok={reverse.installed} label={t(reverse.installed ? 'installed' : 'notInstalled')} />
188
+ <span className="apr-mono">{reverse.repo}</span>
189
+ {reverse.sha === null ? null : (
190
+ <span className="apr-hint">
191
+ {t('reverseCommit')} {reverse.sha.slice(0, 12)}
192
+ {reverse.version === null ? '' : ` · v${reverse.version}`}
193
+ </span>
194
+ )}
195
+ </StatusRow>
196
+ <StatusRow label={t('reverseGroups')}>
197
+ <Pill ok={enabled} label={t(enabled ? 'enabled' : 'disabled')} />
198
+ <span className="apr-mono">{active}</span>
199
+ <span className="apr-hint">
200
+ {t('reverseGroupsHint')} {all}
201
+ </span>
202
+ </StatusRow>
203
+ <StatusRow label={t('reversePath')}>
204
+ <code className="apr-mono">{reverse.tree}</code>
205
+ {reverse.updatedAt === null ? null : (
206
+ <span className="apr-hint">
207
+ {t('updatedAt')} {reverse.updatedAt}
208
+ </span>
209
+ )}
210
+ </StatusRow>
211
+ </>
212
+ )
213
+ }
214
+
215
+ /** 只读状态表:REA 包 / MCP 行 / REA skill / reverse-skill。 */
216
+ function StatusTable(props: { t: Translate; status: StatusDto }): ReactElement {
217
+ const { t, status } = props
218
+ return (
219
+ <dl className="apr-install">
220
+ <InstallRows t={t} status={status} />
221
+ <SkillRows t={t} skill={status.skill} enabled={status.config.skillEnabled} />
222
+ <ReverseRows t={t} reverse={status.reverse} enabled={status.config.reverseSkillEnabled} />
223
+ </dl>
224
+ )
225
+ }
226
+
227
+ /** 动作按钮行。 */
228
+ function ActionRow(props: {
229
+ t: Translate
230
+ busy: boolean
231
+ running: boolean
232
+ disabled: boolean
233
+ proxyPort: number | null
234
+ onAction(kind: 'install' | 'skill' | 'reverse'): void
235
+ onCancel(): void
236
+ }): ReactElement {
237
+ const { t, busy, running, disabled, proxyPort, onAction, onCancel } = props
238
+ const blocked = disabled || busy || running || proxyPort === null
239
+ return (
240
+ <div className="apr-btnRow">
241
+ <Button
242
+ prefix="apr"
243
+ variant="primary"
244
+ label={t('installAction')}
245
+ disabled={blocked}
246
+ onClick={() => onAction('install')}
247
+ />
248
+ <Button
249
+ prefix="apr"
250
+ label={t('skillAction')}
251
+ disabled={blocked}
252
+ onClick={() => onAction('skill')}
253
+ />
254
+ <Button
255
+ prefix="apr"
256
+ label={t('reverseAction')}
257
+ disabled={blocked}
258
+ onClick={() => onAction('reverse')}
259
+ />
260
+ {running ? (
261
+ <Button prefix="apr" label={t('cancelAction')} disabled={busy} onClick={onCancel} />
262
+ ) : null}
263
+ </div>
264
+ )
265
+ }
266
+
267
+ export interface InstallSectionProps {
268
+ t: Translate
269
+ /** 设置只读(不可写)时禁用全部动作。 */
270
+ disabled: boolean
271
+ /** 草稿里的代理端口(非法为 null → 禁用动作,不静默取 0)。 */
272
+ proxyPort: number | null
273
+ }
274
+
275
+ export function InstallSection(props: InstallSectionProps): ReactElement {
276
+ const { t, disabled, proxyPort } = props
277
+ const [status, setStatus] = useState<StatusDto | null>(null)
278
+ const [error, setError] = useState<string | null>(null)
279
+ const [busy, setBusy] = useState(false)
280
+ const alive = useRef(true)
281
+ useEffect(
282
+ () => () => {
283
+ alive.current = false
284
+ },
285
+ [],
286
+ )
287
+
288
+ const load = useCallback(async (): Promise<void> => {
289
+ try {
290
+ const next = await fetchStatus()
291
+ if (!alive.current) return
292
+ setStatus(next)
293
+ setError(null)
294
+ } catch (failure) {
295
+ if (alive.current) setError(`${t('statusFailed')}${messageOf(failure)}`)
296
+ }
297
+ }, [t])
298
+
299
+ useEffect(() => {
300
+ void load()
301
+ }, [load])
302
+
303
+ const running = status?.job?.state === 'running'
304
+ useEffect(() => {
305
+ if (running !== true) return
306
+ const timer = window.setInterval(() => {
307
+ void load()
308
+ }, PROGRESS_POLL_MS)
309
+ return () => {
310
+ window.clearInterval(timer)
311
+ }
312
+ }, [running, load])
313
+
314
+ const runAction = useCallback(
315
+ (action: () => Promise<{ job: JobDto | null }>): void => {
316
+ setBusy(true)
317
+ setError(null)
318
+ action()
319
+ .then((result) => {
320
+ if (!alive.current) return
321
+ setStatus((prev) => (prev === null ? prev : { ...prev, job: result.job }))
322
+ void load()
323
+ })
324
+ .catch((failure: unknown) => {
325
+ if (alive.current) setError(`${t('actionFailed')}${messageOf(failure)}`)
326
+ })
327
+ .finally(() => {
328
+ if (alive.current) setBusy(false)
329
+ })
330
+ },
331
+ [load, t],
332
+ )
333
+
334
+ const job = status?.job ?? null
335
+ return (
336
+ <section className="apr-field">
337
+ <div className="apr-head">
338
+ <span className="apr-label">{t('installSection')}</span>
339
+ <Button
340
+ prefix="apr"
341
+ label={t('refreshStatus')}
342
+ disabled={busy || running === true}
343
+ onClick={() => {
344
+ void load()
345
+ }}
346
+ />
347
+ </div>
348
+ {status === null ? (
349
+ <p className="apr-hint">{t('statusLoading')}</p>
350
+ ) : (
351
+ <>
352
+ <StatusTable t={t} status={status} />
353
+ <ActionRow
354
+ t={t}
355
+ busy={busy}
356
+ running={running === true}
357
+ disabled={disabled}
358
+ proxyPort={proxyPort}
359
+ onAction={(kind) => {
360
+ runAction(() => startJob(kind, proxyPort ?? 0))
361
+ }}
362
+ onCancel={() => {
363
+ runAction(() => cancelJob())
364
+ }}
365
+ />
366
+ </>
367
+ )}
368
+ {job === null ? null : <JobPanel t={t} job={job} />}
369
+ {error === null ? null : <p className="apr-error">{error}</p>}
370
+ <p className="apr-hint">{t('installHint')}</p>
371
+ </section>
372
+ )
373
+ }
@@ -1,7 +1,9 @@
1
1
  /**
2
- * 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('apr'),
3
- * 本文件只补插件特有规则(提示词多行输入框)。
2
+ * 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('apr')
3
+ * (按钮/字段/徽标等类名由共享控件生成,本文件**不**重复定义),这里只补
4
+ * 插件特有规则:提示词多行框、安装状态表、作业进度条与日志。
4
5
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
6
+ * 颜色与度量全部走官方主题变量(--dsw-alias-*),不写死色值。
5
7
  * @module agent-preset-rea/client/styles
6
8
  */
7
9
  import { cardCss, injectCardStyle } from '@dsh-plus/shared/client'
@@ -12,6 +14,9 @@ export const PLUGIN_ID = '@dsh-plus/agent-preset-rea'
12
14
  * 多行提示词输入框:度量对齐官方 settings-form 的 textarea
13
15
  * (与 -input 同边框/底色/圆角,正文 13px/1.6),窄屏 16px 防 iOS 聚焦缩放、
14
16
  * 纵向可拖拽但不横向溢出。
17
+ *
18
+ * 类名避让 cardCss 已生成的 `-status` / `-badge` 前缀(那是 CardChrome 的状态行与
19
+ * 字段徽标),安装表与作业块一律用 `-install*` / `-pill*` / `-job*`。
15
20
  */
16
21
  const extraCss = `
17
22
  .apr-textarea{box-sizing:border-box;width:100%;min-height:180px;resize:vertical;font:inherit;font-size:13px;line-height:1.6;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px 12px;transition:border-color .18s,background .18s}
@@ -19,8 +24,28 @@ const extraCss = `
19
24
  .apr-textarea:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
20
25
  .apr-textarea:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
21
26
  .apr-textarea::placeholder{color:var(--dsw-alias-label-tertiary)}
27
+ .apr-btnRow{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
28
+ .apr-install{margin:6px 0 0;padding:0;display:flex;flex-direction:column;gap:6px}
29
+ .apr-installRow{display:flex;gap:10px;align-items:baseline;flex-wrap:wrap}
30
+ .apr-installKey{flex:none;width:118px;margin:0;font-size:12px;color:var(--dsw-alias-label-tertiary)}
31
+ .apr-installVal{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:0;font-size:13px;color:var(--dsw-alias-label-primary)}
32
+ .apr-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;word-break:break-all}
33
+ .apr-pill{flex:none;padding:2px 8px;border-radius:999px;font-size:11px;line-height:16px}
34
+ .apr-pillOk{background:var(--dsw-alias-state-success-secondary);color:var(--dsw-alias-state-success-primary)}
35
+ .apr-pillMiss,.apr-pillError,.apr-pillCancelled{background:var(--dsw-alias-state-error-secondary);color:var(--dsw-alias-state-error-primary)}
36
+ .apr-pillRunning{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
37
+ .apr-job{margin-top:12px;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3)}
38
+ .apr-jobHead{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
39
+ .apr-jobTitle{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}
40
+ .apr-bar{margin-top:8px;height:6px;border-radius:999px;background:var(--dsw-alias-interactive-bg-hover);overflow:hidden}
41
+ .apr-barFill{height:100%;background:var(--dsw-alias-brand-primary);transition:width .3s}
42
+ .apr-logBox{margin-top:8px}
43
+ .apr-logSummary{font-size:12px;color:var(--dsw-alias-label-tertiary);cursor:pointer}
44
+ .apr-log{margin:6px 0 0;max-height:200px;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-all;color:var(--dsw-alias-label-tertiary)}
45
+ .apr-error{margin:4px 0 0;font-size:12px;color:var(--dsw-alias-state-error-primary)}
22
46
  @media (max-width:767px){
23
47
  .apr-textarea{font-size:16px;min-height:140px}
48
+ .apr-installKey{width:100%}
24
49
  }
25
50
  `
26
51
 
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 卡片的「能力开关」区:三个一键开关(REA MCP 行 / REA 官方 skill / reverse-skill
3
+ * 技能包)。三者都是活动字段,保存后本预设换代注册——新建会话按新装配起(REA 工具
4
+ * 与 skill 根的有无),已存在会话保持既有装配。
5
+ *
6
+ * 开关只管「挂不挂」,不管「装没装」:关掉不会删磁盘上的安装副本(更新/回滚仍是
7
+ * 显式动作,见 install.tsx)。
8
+ * @module agent-preset-rea/client/toggles
9
+ */
10
+ import { CheckRow } from '@dsh-plus/shared/client'
11
+ import type { ReactElement } from 'react'
12
+
13
+ import type { Draft } from './draft.ts'
14
+ import type { Translate } from './i18n.ts'
15
+
16
+ /** 三个开关的键与文案键。 */
17
+ const TOGGLES = [
18
+ { field: 'mcpEnabled', id: 'apr-toggle-mcp', label: 'toggleMcp', hint: 'toggleMcpHint' },
19
+ { field: 'skillEnabled', id: 'apr-toggle-skill', label: 'toggleSkill', hint: 'toggleSkillHint' },
20
+ {
21
+ field: 'reverseSkillEnabled',
22
+ id: 'apr-toggle-reverse',
23
+ label: 'toggleReverse',
24
+ hint: 'toggleReverseHint',
25
+ },
26
+ ] as const
27
+
28
+ export interface ToggleSectionProps {
29
+ t: Translate
30
+ draft: Draft
31
+ disabled: boolean
32
+ onEdit(patch: Partial<Draft>): void
33
+ }
34
+
35
+ export function ToggleSection(props: ToggleSectionProps): ReactElement {
36
+ const { t, draft, disabled, onEdit } = props
37
+ return (
38
+ <section className="apr-field">
39
+ <div className="apr-head">
40
+ <span className="apr-label">{t('toggleSection')}</span>
41
+ </div>
42
+ {TOGGLES.map((toggle) => (
43
+ <div key={toggle.field}>
44
+ <CheckRow
45
+ prefix="apr"
46
+ id={toggle.id}
47
+ label={t(toggle.label)}
48
+ checked={draft[toggle.field]}
49
+ disabled={disabled}
50
+ onEdit={(checked) => onEdit({ [toggle.field]: checked })}
51
+ />
52
+ <p className="apr-hint">{t(toggle.hint)}</p>
53
+ </div>
54
+ ))}
55
+ </section>
56
+ )
57
+ }
package/src/config.ts CHANGED
@@ -5,10 +5,11 @@
5
5
  * 与 settings 用户层(GUI 配置卡片写入 profile 行级覆盖层,loader 原位提交活动
6
6
  * 引用,消费端 `unwrapVolatile` 现取即热)。
7
7
  *
8
- * **只有 `personaPrefix` 标 `.volatile()`**:它是对用户的承诺(配置卡片必须给出
9
- * 编辑器,见 src/client/card.tsx),也是本预设里唯一需要热更的字段;其余字段是
10
- * **部署方平面**(MCP 参数、超时、附加 skill 根等运维旋钮),由 `cordis.patch.yml`
11
- * 整体覆盖,GUI 不提供入口。
8
+ * **活动字段(`.volatile()`)共五个**,全部在配置卡片里有编辑器(见 src/client/card.tsx):
9
+ * `personaPrefix`(提示词)、`proxyPort`(安装代理)、`mcpEnabled` / `skillEnabled` /
10
+ * `reverseSkillEnabled`(三个能力开关)——保存即热更(预设换代,新建会话生效)。
11
+ * 其余字段是**部署方平面**(受管目录、版本钉、skill 组、超时等运维旋钮),由
12
+ * `cordis.patch.yml` 整体覆盖,GUI 不提供入口。
12
13
  *
13
14
  * **persona 默认为空串**:`dsh-persona` 的空模板照样遮蔽部署级 persona 段,
14
15
  * 渲染为空后在提示词里消失;官方装配(harness 身份、工具指引、项目指令、
@@ -21,12 +22,7 @@
21
22
  import z from '@deepseek-ai/schemastery'
22
23
  import type { UnwrapVolatile } from '@dsh-plus/shared'
23
24
 
24
- import {
25
- DEFAULT_MCP_COMMAND,
26
- INPUT_SCHEMA_PROFILES,
27
- OUTPUT_SCHEMA_PROFILES,
28
- REA_PINNED_VERSION,
29
- } from './mcp.ts'
25
+ import { INPUT_SCHEMA_PROFILES, OUTPUT_SCHEMA_PROFILES, REA_PINNED_VERSION } from './mcp.ts'
30
26
  import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
31
27
 
32
28
  /** settings 命名空间(字面量即合法命名空间;配置卡片与运行期读取同一份)。 */
@@ -55,7 +51,35 @@ export const Config = z.object({
55
51
  mcpEnabled: z
56
52
  .boolean()
57
53
  .description('是否挂 REA MCP 客户端行(false = 只保留预设其余能力)')
58
- .default(true),
54
+ .default(true)
55
+ .volatile(),
56
+ skillEnabled: z
57
+ .boolean()
58
+ .description('是否把 REA 官方 skill 根挂进本预设(false = 模型只看到其余 skill 根)')
59
+ .default(true)
60
+ .volatile(),
61
+ reverseSkillEnabled: z
62
+ .boolean()
63
+ .description(
64
+ '是否挂 zhaoxuya520/reverse-skill 技能包(false = 从本预设的 skill 根里摘掉,安装副本仍在)',
65
+ )
66
+ .default(true)
67
+ .volatile(),
68
+ reverseSkillRepo: z
69
+ .string()
70
+ .pattern(/^[\w.-]+\/[\w.-]+$/)
71
+ .description('reverse-skill 仓库(<owner>/<repo>;安装/手动更新都从这里拉)')
72
+ .default('zhaoxuya520/reverse-skill'),
73
+ reverseSkillRef: z
74
+ .string()
75
+ .description('reverse-skill 的 ref(分支/标签/提交;留空 = 仓库默认分支的最新提交)')
76
+ .default(''),
77
+ reverseSkillGroups: z
78
+ .array(z.string())
79
+ .description(
80
+ 'reverse-skill 里挂进本预设的技能组(相对安装树根;默认只挂主包根 skills——含路由技能与全部模块技能,CTF 侧车等组按需追加)',
81
+ )
82
+ .default(['skills']),
59
83
  mcpServerName: z
60
84
  .string()
61
85
  .pattern(/^[A-Za-z0-9_-]{1,32}$/)
@@ -67,13 +91,29 @@ export const Config = z.object({
67
91
  .string()
68
92
  .description('钉版的 rea-agents 版本(npx 拉取)')
69
93
  .default(REA_PINNED_VERSION),
94
+ installRoot: z
95
+ .string()
96
+ .description(
97
+ 'REA 受管安装树位置(空 = <DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/rea-agent)',
98
+ )
99
+ .default(''),
100
+ proxyPort: z
101
+ .natural()
102
+ .max(65535)
103
+ .description('安装/更新时使用的本机代理端口(0 = 不用代理;npm 与原生依赖 prebuild 下载同此)')
104
+ .default(0)
105
+ .volatile(),
70
106
  mcpCommand: z
71
107
  .string()
72
- .description('MCP stdio 可执行文件;Windows 需 cmd.exe 配方,见包文档')
73
- .default(DEFAULT_MCP_COMMAND),
108
+ .description(
109
+ '显式 MCP 启动命令(空 = 用受管安装入口,未安装时该行禁用;填 npx 即启动期拉取,风险自负)',
110
+ )
111
+ .default(''),
74
112
  mcpArgs: z
75
113
  .array(z.string())
76
- .description('MCP stdio 参数;留空 = 按 reaVersion 派生 ["-y","rea-agents@<version>","mcp"]')
114
+ .description(
115
+ 'MCP stdio 附加参数;留空 = 受管入口补 ["mcp"],显式 npx 则派生 ["-y","rea-agents@<version>","mcp"]',
116
+ )
77
117
  .default([]),
78
118
  inputSchemaProfile: z
79
119
  .union(INPUT_SCHEMA_PROFILES)