@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.
@@ -0,0 +1,274 @@
1
+ /**
2
+ * 卡片的「REA 安装与更新」区:只读状态表 + 三个动作(安装/更新 REA、更新 skill、
3
+ * 取消作业)+ 运行中作业的阶段进度条、已用时与日志尾部。
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, useCallback, useEffect, useRef, useState } from 'react'
12
+
13
+ import type { JobDto, 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
+ function JobPanel(props: { t: Translate; job: JobDto }): ReactElement {
48
+ const { t, job } = props
49
+ const stateKey =
50
+ job.state === 'running'
51
+ ? 'jobRunning'
52
+ : job.state === 'ok'
53
+ ? 'jobOk'
54
+ : job.state === 'error'
55
+ ? 'jobError'
56
+ : 'jobCancelled'
57
+ const lines = job.log.slice(-LOG_TAIL)
58
+ return (
59
+ <div className="apr-job">
60
+ <div className="apr-jobHead">
61
+ <span className={`apr-pill apr-pill${STATE_CLASS[job.state]}`}>{t(stateKey)}</span>
62
+ <span className="apr-jobTitle">
63
+ {t(job.kind === 'install' ? 'jobInstall' : 'jobSkill')}
64
+ </span>
65
+ <span className="apr-hint">
66
+ {t('jobPhase')} {job.progress.step}/{job.progress.steps}·{job.progress.phase}
67
+ </span>
68
+ <span className="apr-hint">
69
+ {t('jobElapsed')} {elapsedOf(job)}
70
+ </span>
71
+ </div>
72
+ <div
73
+ className="apr-bar"
74
+ role="progressbar"
75
+ aria-valuemin={0}
76
+ aria-valuemax={100}
77
+ aria-valuenow={job.progress.percent}
78
+ >
79
+ <div className="apr-barFill" style={{ width: `${job.progress.percent}%` }} />
80
+ </div>
81
+ {job.error === null ? null : <p className="apr-error">{job.error}</p>}
82
+ <details className="apr-logBox" open={job.state !== 'ok'}>
83
+ <summary className="apr-logSummary">{t('jobLog')}</summary>
84
+ <pre className="apr-log">{lines.length === 0 ? t('jobLogEmpty') : lines.join('\n')}</pre>
85
+ </details>
86
+ </div>
87
+ )
88
+ }
89
+
90
+ /** 一行状态(键 + 值)。 */
91
+ function StatusRow(props: { label: string; children: React.ReactNode }): ReactElement {
92
+ return (
93
+ <div className="apr-installRow">
94
+ <dt className="apr-installKey">{props.label}</dt>
95
+ <dd className="apr-installVal">{props.children}</dd>
96
+ </div>
97
+ )
98
+ }
99
+
100
+ /** 只读状态表:REA 包 / 受管目录 / skill 来源 / MCP 行形态。 */
101
+ function StatusTable(props: { t: Translate; status: StatusDto }): ReactElement {
102
+ const { t, status } = props
103
+ const skillLabel = status.skill.source === 'updated' ? t('skillUpdated') : t('skillBundled')
104
+ const mcpLabel = !status.mcp.enabled
105
+ ? status.config.mcpEnabled
106
+ ? t('mcpMissing')
107
+ : t('mcpOff')
108
+ : status.mcp.mode === 'custom'
109
+ ? t('mcpCustom')
110
+ : t('mcpInstalled')
111
+ return (
112
+ <dl className="apr-install">
113
+ <StatusRow label={t('installPackage')}>
114
+ <span className={`apr-pill ${status.install.installed ? 'apr-pillOk' : 'apr-pillMiss'}`}>
115
+ {t(status.install.installed ? 'installed' : 'notInstalled')}
116
+ </span>
117
+ {status.install.version === null ? null : (
118
+ <span className="apr-mono">{status.install.version}</span>
119
+ )}
120
+ <span className="apr-hint">
121
+ {t('installTarget')} {status.config.reaVersion}
122
+ </span>
123
+ </StatusRow>
124
+ <StatusRow label={t('installPath')}>
125
+ <code className="apr-mono">{status.install.root}</code>
126
+ </StatusRow>
127
+ <StatusRow label={t('skillSource')}>
128
+ <span className="apr-mono">{skillLabel}</span>
129
+ <span className="apr-hint">
130
+ {t('skillFiles')} {status.skill.files}
131
+ {status.skill.version === null ? '' : ` · ${status.skill.version}`}
132
+ </span>
133
+ {status.skill.updatedAt === null ? null : (
134
+ <span className="apr-hint">
135
+ {t('updatedAt')} {status.skill.updatedAt}
136
+ </span>
137
+ )}
138
+ </StatusRow>
139
+ <StatusRow label={t('mcpRow')}>
140
+ <span className="apr-mono">{mcpLabel}</span>
141
+ {status.mcp.command === null ? null : (
142
+ <code className="apr-mono">
143
+ {status.mcp.command} {status.mcp.args.join(' ')}
144
+ </code>
145
+ )}
146
+ </StatusRow>
147
+ </dl>
148
+ )
149
+ }
150
+
151
+ export interface InstallSectionProps {
152
+ t: Translate
153
+ /** 设置只读(不可写)时禁用全部动作。 */
154
+ disabled: boolean
155
+ /** 草稿里的代理端口(非法为 null → 禁用动作,不静默取 0)。 */
156
+ proxyPort: number | null
157
+ }
158
+
159
+ export function InstallSection(props: InstallSectionProps): ReactElement {
160
+ const { t, disabled, proxyPort } = props
161
+ const [status, setStatus] = useState<StatusDto | null>(null)
162
+ const [error, setError] = useState<string | null>(null)
163
+ const [busy, setBusy] = useState(false)
164
+ const alive = useRef(true)
165
+ useEffect(
166
+ () => () => {
167
+ alive.current = false
168
+ },
169
+ [],
170
+ )
171
+
172
+ const load = useCallback(async (): Promise<void> => {
173
+ try {
174
+ const next = await fetchStatus()
175
+ if (!alive.current) return
176
+ setStatus(next)
177
+ setError(null)
178
+ } catch (failure) {
179
+ if (alive.current) setError(`${t('statusFailed')}${messageOf(failure)}`)
180
+ }
181
+ }, [t])
182
+
183
+ useEffect(() => {
184
+ void load()
185
+ }, [load])
186
+
187
+ const running = status?.job?.state === 'running'
188
+ useEffect(() => {
189
+ if (running !== true) return
190
+ const timer = window.setInterval(() => {
191
+ void load()
192
+ }, PROGRESS_POLL_MS)
193
+ return () => {
194
+ window.clearInterval(timer)
195
+ }
196
+ }, [running, load])
197
+
198
+ const runAction = useCallback(
199
+ (action: () => Promise<{ job: JobDto | null }>): void => {
200
+ setBusy(true)
201
+ setError(null)
202
+ action()
203
+ .then((result) => {
204
+ if (!alive.current) return
205
+ setStatus((prev) => (prev === null ? prev : { ...prev, job: result.job }))
206
+ void load()
207
+ })
208
+ .catch((failure: unknown) => {
209
+ if (alive.current) setError(`${t('actionFailed')}${messageOf(failure)}`)
210
+ })
211
+ .finally(() => {
212
+ if (alive.current) setBusy(false)
213
+ })
214
+ },
215
+ [load, t],
216
+ )
217
+
218
+ const blocked = disabled || busy || running === true
219
+ const job = status?.job ?? null
220
+ return (
221
+ <section className="apr-field">
222
+ <div className="apr-head">
223
+ <span className="apr-label">{t('installSection')}</span>
224
+ <Button
225
+ prefix="apr"
226
+ label={t('refreshStatus')}
227
+ disabled={busy || running === true}
228
+ onClick={() => {
229
+ void load()
230
+ }}
231
+ />
232
+ </div>
233
+ {status === null ? (
234
+ <p className="apr-hint">{t('statusLoading')}</p>
235
+ ) : (
236
+ <>
237
+ <StatusTable t={t} status={status} />
238
+ <div className="apr-btnRow">
239
+ <Button
240
+ prefix="apr"
241
+ variant="primary"
242
+ label={t('installAction')}
243
+ disabled={blocked || proxyPort === null}
244
+ onClick={() => {
245
+ runAction(() => startJob('install', proxyPort ?? 0))
246
+ }}
247
+ />
248
+ <Button
249
+ prefix="apr"
250
+ label={t('skillAction')}
251
+ disabled={blocked || proxyPort === null}
252
+ onClick={() => {
253
+ runAction(() => startJob('skill', proxyPort ?? 0))
254
+ }}
255
+ />
256
+ {running === true ? (
257
+ <Button
258
+ prefix="apr"
259
+ label={t('cancelAction')}
260
+ disabled={busy}
261
+ onClick={() => {
262
+ runAction(() => cancelJob())
263
+ }}
264
+ />
265
+ ) : null}
266
+ </div>
267
+ </>
268
+ )}
269
+ {job === null ? null : <JobPanel t={t} job={job} />}
270
+ {error === null ? null : <p className="apr-error">{error}</p>}
271
+ <p className="apr-hint">{t('installHint')}</p>
272
+ </section>
273
+ )
274
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('apr')
3
+ * (按钮/字段/徽标等类名由共享控件生成,本文件**不**重复定义),这里只补
4
+ * 插件特有规则:提示词多行框、安装状态表、作业进度条与日志。
5
+ * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
6
+ * 颜色与度量全部走官方主题变量(--dsw-alias-*),不写死色值。
7
+ * @module agent-preset-rea/client/styles
8
+ */
9
+ import { cardCss, injectCardStyle } from '@dsh-plus/shared/client'
10
+
11
+ export const PLUGIN_ID = '@dsh-plus/agent-preset-rea'
12
+
13
+ /**
14
+ * 多行提示词输入框:度量对齐官方 settings-form 的 textarea
15
+ * (与 -input 同边框/底色/圆角,正文 13px/1.6),窄屏 16px 防 iOS 聚焦缩放、
16
+ * 纵向可拖拽但不横向溢出。
17
+ *
18
+ * 类名避让 cardCss 已生成的 `-status` / `-badge` 前缀(那是 CardChrome 的状态行与
19
+ * 字段徽标),安装表与作业块一律用 `-install*` / `-pill*` / `-job*`。
20
+ */
21
+ const extraCss = `
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}
23
+ .apr-textarea:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
24
+ .apr-textarea:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
25
+ .apr-textarea:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
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)}
46
+ @media (max-width:767px){
47
+ .apr-textarea{font-size:16px;min-height:140px}
48
+ .apr-installKey{width:100%}
49
+ }
50
+ `
51
+
52
+ export const cardCssAll = cardCss('apr', extraCss)
53
+
54
+ /** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
55
+ export function injectStyle(): HTMLStyleElement | null {
56
+ return injectCardStyle(PLUGIN_ID, cardCssAll)
57
+ }
package/src/config.ts ADDED
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 插件配置(schemastery):显示面、persona、REA MCP 行与 skill 根外部化。
3
+ *
4
+ * 单一事实源同时充当两处:cordis 行级 `Config`(组合默认,profile patch 层可覆盖)
5
+ * 与 settings 用户层(GUI 配置卡片写入 profile 行级覆盖层,loader 原位提交活动
6
+ * 引用,消费端 `unwrapVolatile` 现取即热)。
7
+ *
8
+ * **活动字段只有两个**:`personaPrefix` 与 `proxyPort`——两者都在配置卡片里有编辑器
9
+ * (见 src/client/card.tsx),保存即热更(预设换代 / 安装作业即时生效)。其余字段是
10
+ * **部署方平面**(安装位置、超时、附加 skill 根等运维旋钮),由 `cordis.patch.yml`
11
+ * 整体覆盖,GUI 不提供入口。
12
+ *
13
+ * **persona 默认为空串**:`dsh-persona` 的空模板照样遮蔽部署级 persona 段,
14
+ * 渲染为空后在提示词里消失;官方装配(harness 身份、工具指引、项目指令、
15
+ * 运行时上下文)不受影响。
16
+ *
17
+ * 预设 id 固定为 `rea`(注册表身份,不开放覆盖——改 id 等于换预设,
18
+ * default/selectedDefault 等引用都会落空)。
19
+ * @module @dsh-plus/agent-preset-rea/config
20
+ */
21
+ import z from '@deepseek-ai/schemastery'
22
+ import type { UnwrapVolatile } from '@dsh-plus/shared'
23
+
24
+ import { INPUT_SCHEMA_PROFILES, OUTPUT_SCHEMA_PROFILES, REA_PINNED_VERSION } from './mcp.ts'
25
+ import { SETTINGS_NS as NS_LITERAL } from './ns.ts'
26
+
27
+ /** settings 命名空间(字面量即合法命名空间;配置卡片与运行期读取同一份)。 */
28
+ export const SETTINGS_NS = NS_LITERAL
29
+
30
+ export const Config = z.object({
31
+ enabled: z
32
+ .boolean()
33
+ .description('总开关(false = 不注册 rea 预设,等价插件未安装)')
34
+ .default(true),
35
+ name: z.string().description('预设显示名(设置 → Agent 预设卡片标题)').default('逆向工程'),
36
+ description: z
37
+ .string()
38
+ .description('预设说明(设置 → Agent 预设卡片描述)')
39
+ .default(
40
+ '逆向工程预设:官方 standard 全量行 + 仅本预设可见的 REA MCP 客户端(mcp__rea__*)与随包 REA 官方 skill;persona 默认为空,工具指引沿用官方装配。',
41
+ ),
42
+ order: z.number().description('预设名册排序位').default(6),
43
+ personaPrefix: z
44
+ .string()
45
+ .description(
46
+ 'persona 前缀(system-prompt 的 deployment:persona-prefix 段);留空 = 空 persona(该段渲染为空后消失),工具指引与运行时上下文保留',
47
+ )
48
+ .default('')
49
+ .volatile(),
50
+ mcpEnabled: z
51
+ .boolean()
52
+ .description('是否挂 REA MCP 客户端行(false = 只保留预设其余能力)')
53
+ .default(true),
54
+ mcpServerName: z
55
+ .string()
56
+ .pattern(/^[A-Za-z0-9_-]{1,32}$/)
57
+ .description(
58
+ 'MCP 服务命名空间:模型面工具名为 mcp__<serverName>__<tool>,取值 [A-Za-z0-9_-]{1,32}',
59
+ )
60
+ .default('rea'),
61
+ reaVersion: z
62
+ .string()
63
+ .description('钉版的 rea-agents 版本(npx 拉取)')
64
+ .default(REA_PINNED_VERSION),
65
+ installRoot: z
66
+ .string()
67
+ .description(
68
+ 'REA 受管安装树位置(空 = <DSH_HOME>/dsh-plus/dsh-plus-agent-preset-rea/rea-agent)',
69
+ )
70
+ .default(''),
71
+ proxyPort: z
72
+ .natural()
73
+ .max(65535)
74
+ .description('安装/更新时使用的本机代理端口(0 = 不用代理;npm 与原生依赖 prebuild 下载同此)')
75
+ .default(0)
76
+ .volatile(),
77
+ mcpCommand: z
78
+ .string()
79
+ .description(
80
+ '显式 MCP 启动命令(空 = 用受管安装入口,未安装时该行禁用;填 npx 即启动期拉取,风险自负)',
81
+ )
82
+ .default(''),
83
+ mcpArgs: z
84
+ .array(z.string())
85
+ .description(
86
+ 'MCP stdio 附加参数;留空 = 受管入口补 ["mcp"],显式 npx 则派生 ["-y","rea-agents@<version>","mcp"]',
87
+ )
88
+ .default([]),
89
+ inputSchemaProfile: z
90
+ .union(INPUT_SCHEMA_PROFILES)
91
+ .description(
92
+ 'REA 工具入参 schema 广告档位(REA_MCP_INPUT_SCHEMA_PROFILE):compact 压体积,full 与上游默认一致',
93
+ )
94
+ .default('compact'),
95
+ outputSchemaProfile: z
96
+ .union(OUTPUT_SCHEMA_PROFILES)
97
+ .description(
98
+ 'REA 工具出参 schema 广告档位(REA_MCP_OUTPUT_SCHEMA_PROFILE):off 不下发(结果仍由 REA 校验),full 与上游默认一致',
99
+ )
100
+ .default('off'),
101
+ toolCallTimeoutMs: z
102
+ .number()
103
+ .description('单次 MCP 工具调用超时(毫秒);REA 分析类调用常超默认 60 秒')
104
+ .default(600_000),
105
+ failOnStartupError: z
106
+ .boolean()
107
+ .description(
108
+ 'MCP 初次连接/同步失败是否让本行激活失败;默认 false:只记日志并按退避重连,不拖垮 boot',
109
+ )
110
+ .default(false),
111
+ extraSkillDirs: z
112
+ .array(z.string())
113
+ .description('附加 skill 根目录(随本预设作用域生效,其他预设不可见)')
114
+ .default([]),
115
+ })
116
+
117
+ /** 活动字段形态(loader 解析产物:`personaPrefix` 为活动引用)。 */
118
+ export type AgentPresetReaConfigFields = Schemastery.TypeT<typeof Config>
119
+
120
+ /** 平面配置形态(消费面的读取形态,由活动引用解包得到)。 */
121
+ export type AgentPresetReaConfig = UnwrapVolatile<AgentPresetReaConfigFields>