dsh-config-manager 0.1.0

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.
Files changed (253) hide show
  1. package/README.md +216 -0
  2. package/README.zh-CN.md +218 -0
  3. package/cordis.patch.yml +45 -0
  4. package/dsh.bundle.patch +21 -0
  5. package/dsh.client +39 -0
  6. package/lib/adapters/agent-presets.d.ts +13 -0
  7. package/lib/adapters/agent-presets.js +14 -0
  8. package/lib/adapters/agent-presets.js.map +1 -0
  9. package/lib/adapters/credentials.d.ts +36 -0
  10. package/lib/adapters/credentials.js +121 -0
  11. package/lib/adapters/credentials.js.map +1 -0
  12. package/lib/adapters/file-collection.d.ts +14 -0
  13. package/lib/adapters/file-collection.js +102 -0
  14. package/lib/adapters/file-collection.js.map +1 -0
  15. package/lib/adapters/index.d.ts +38 -0
  16. package/lib/adapters/index.js +46 -0
  17. package/lib/adapters/index.js.map +1 -0
  18. package/lib/adapters/mcp.d.ts +27 -0
  19. package/lib/adapters/mcp.js +175 -0
  20. package/lib/adapters/mcp.js.map +1 -0
  21. package/lib/adapters/plugin-files.d.ts +15 -0
  22. package/lib/adapters/plugin-files.js +91 -0
  23. package/lib/adapters/plugin-files.js.map +1 -0
  24. package/lib/adapters/plugins.d.ts +13 -0
  25. package/lib/adapters/plugins.js +177 -0
  26. package/lib/adapters/plugins.js.map +1 -0
  27. package/lib/adapters/prompts.d.ts +29 -0
  28. package/lib/adapters/prompts.js +228 -0
  29. package/lib/adapters/prompts.js.map +1 -0
  30. package/lib/adapters/providers.d.ts +33 -0
  31. package/lib/adapters/providers.js +143 -0
  32. package/lib/adapters/providers.js.map +1 -0
  33. package/lib/adapters/sessions.d.ts +14 -0
  34. package/lib/adapters/sessions.js +15 -0
  35. package/lib/adapters/sessions.js.map +1 -0
  36. package/lib/adapters/settings.d.ts +23 -0
  37. package/lib/adapters/settings.js +142 -0
  38. package/lib/adapters/settings.js.map +1 -0
  39. package/lib/adapters/skills.d.ts +13 -0
  40. package/lib/adapters/skills.js +14 -0
  41. package/lib/adapters/skills.js.map +1 -0
  42. package/lib/adapters/test-helpers.d.ts +99 -0
  43. package/lib/adapters/test-helpers.js +192 -0
  44. package/lib/adapters/test-helpers.js.map +1 -0
  45. package/lib/adapters/ui.d.ts +26 -0
  46. package/lib/adapters/ui.js +88 -0
  47. package/lib/adapters/ui.js.map +1 -0
  48. package/lib/adapters/workspaces.d.ts +12 -0
  49. package/lib/adapters/workspaces.js +92 -0
  50. package/lib/adapters/workspaces.js.map +1 -0
  51. package/lib/client.d.ts +377 -0
  52. package/lib/client.js +2598 -0
  53. package/lib/client.js.map +1 -0
  54. package/lib/core/analyzer.d.ts +35 -0
  55. package/lib/core/analyzer.js +563 -0
  56. package/lib/core/analyzer.js.map +1 -0
  57. package/lib/core/backup.d.ts +29 -0
  58. package/lib/core/backup.js +184 -0
  59. package/lib/core/backup.js.map +1 -0
  60. package/lib/core/exporter.d.ts +37 -0
  61. package/lib/core/exporter.js +222 -0
  62. package/lib/core/exporter.js.map +1 -0
  63. package/lib/core/importer.d.ts +42 -0
  64. package/lib/core/importer.js +25 -0
  65. package/lib/core/importer.js.map +1 -0
  66. package/lib/core/index.d.ts +12 -0
  67. package/lib/core/index.js +11 -0
  68. package/lib/core/index.js.map +1 -0
  69. package/lib/core/rollback.d.ts +9 -0
  70. package/lib/core/rollback.js +136 -0
  71. package/lib/core/rollback.js.map +1 -0
  72. package/lib/core/types.d.ts +331 -0
  73. package/lib/core/types.js +18 -0
  74. package/lib/core/types.js.map +1 -0
  75. package/lib/core/validator.d.ts +18 -0
  76. package/lib/core/validator.js +77 -0
  77. package/lib/core/validator.js.map +1 -0
  78. package/lib/index.d.ts +51 -0
  79. package/lib/index.js +875 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/migrations/index.d.ts +29 -0
  82. package/lib/migrations/index.js +56 -0
  83. package/lib/migrations/index.js.map +1 -0
  84. package/lib/migrations/v1-to-v2.d.ts +9 -0
  85. package/lib/migrations/v1-to-v2.js +10 -0
  86. package/lib/migrations/v1-to-v2.js.map +1 -0
  87. package/lib/profiles/index.d.ts +4 -0
  88. package/lib/profiles/index.js +5 -0
  89. package/lib/profiles/index.js.map +1 -0
  90. package/lib/profiles/profile-manager.d.ts +102 -0
  91. package/lib/profiles/profile-manager.js +522 -0
  92. package/lib/profiles/profile-manager.js.map +1 -0
  93. package/lib/schema/config.d.ts +28 -0
  94. package/lib/schema/config.js +136 -0
  95. package/lib/schema/config.js.map +1 -0
  96. package/lib/schema/manifest.d.ts +28 -0
  97. package/lib/schema/manifest.js +109 -0
  98. package/lib/schema/manifest.js.map +1 -0
  99. package/lib/schema/types.d.ts +169 -0
  100. package/lib/schema/types.js +8 -0
  101. package/lib/schema/types.js.map +1 -0
  102. package/lib/schema/versions.d.ts +27 -0
  103. package/lib/schema/versions.js +50 -0
  104. package/lib/schema/versions.js.map +1 -0
  105. package/lib/security/encryption.d.ts +41 -0
  106. package/lib/security/encryption.js +152 -0
  107. package/lib/security/encryption.js.map +1 -0
  108. package/lib/security/index.d.ts +16 -0
  109. package/lib/security/index.js +17 -0
  110. package/lib/security/index.js.map +1 -0
  111. package/lib/security/integrity.d.ts +30 -0
  112. package/lib/security/integrity.js +78 -0
  113. package/lib/security/integrity.js.map +1 -0
  114. package/lib/security/redaction.d.ts +26 -0
  115. package/lib/security/redaction.js +95 -0
  116. package/lib/security/redaction.js.map +1 -0
  117. package/lib/security/secret-scanner.d.ts +48 -0
  118. package/lib/security/secret-scanner.js +253 -0
  119. package/lib/security/secret-scanner.js.map +1 -0
  120. package/lib/security/zip-security.d.ts +27 -0
  121. package/lib/security/zip-security.js +154 -0
  122. package/lib/security/zip-security.js.map +1 -0
  123. package/lib/ui/conflict-view.d.ts +28 -0
  124. package/lib/ui/conflict-view.js +49 -0
  125. package/lib/ui/conflict-view.js.map +1 -0
  126. package/lib/ui/errors.d.ts +17 -0
  127. package/lib/ui/errors.js +76 -0
  128. package/lib/ui/errors.js.map +1 -0
  129. package/lib/ui/export-flow.d.ts +62 -0
  130. package/lib/ui/export-flow.js +79 -0
  131. package/lib/ui/export-flow.js.map +1 -0
  132. package/lib/ui/import-wizard.d.ts +67 -0
  133. package/lib/ui/import-wizard.js +169 -0
  134. package/lib/ui/import-wizard.js.map +1 -0
  135. package/lib/ui/path-mapping.d.ts +38 -0
  136. package/lib/ui/path-mapping.js +75 -0
  137. package/lib/ui/path-mapping.js.map +1 -0
  138. package/lib/ui/progress.d.ts +27 -0
  139. package/lib/ui/progress.js +76 -0
  140. package/lib/ui/progress.js.map +1 -0
  141. package/lib/ui/report.d.ts +21 -0
  142. package/lib/ui/report.js +146 -0
  143. package/lib/ui/report.js.map +1 -0
  144. package/lib/ui/test-helpers.d.ts +62 -0
  145. package/lib/ui/test-helpers.js +147 -0
  146. package/lib/ui/test-helpers.js.map +1 -0
  147. package/lib/ui/types.d.ts +129 -0
  148. package/lib/ui/types.js +21 -0
  149. package/lib/ui/types.js.map +1 -0
  150. package/lib/utils/hashing.d.ts +17 -0
  151. package/lib/utils/hashing.js +41 -0
  152. package/lib/utils/hashing.js.map +1 -0
  153. package/lib/utils/json.d.ts +24 -0
  154. package/lib/utils/json.js +76 -0
  155. package/lib/utils/json.js.map +1 -0
  156. package/lib/utils/logger.d.ts +39 -0
  157. package/lib/utils/logger.js +82 -0
  158. package/lib/utils/logger.js.map +1 -0
  159. package/lib/utils/paths.d.ts +31 -0
  160. package/lib/utils/paths.js +132 -0
  161. package/lib/utils/paths.js.map +1 -0
  162. package/lib/utils/zip.d.ts +54 -0
  163. package/lib/utils/zip.js +285 -0
  164. package/lib/utils/zip.js.map +1 -0
  165. package/package.json +109 -0
  166. package/src/adapters/agent-presets.ts +14 -0
  167. package/src/adapters/credentials.test.ts +90 -0
  168. package/src/adapters/credentials.ts +141 -0
  169. package/src/adapters/file-collection.ts +110 -0
  170. package/src/adapters/files.test.ts +113 -0
  171. package/src/adapters/index.ts +69 -0
  172. package/src/adapters/mcp.test.ts +96 -0
  173. package/src/adapters/mcp.ts +181 -0
  174. package/src/adapters/plugin-files.ts +96 -0
  175. package/src/adapters/plugins.test.ts +72 -0
  176. package/src/adapters/plugins.ts +176 -0
  177. package/src/adapters/prompts.test.ts +110 -0
  178. package/src/adapters/prompts.ts +226 -0
  179. package/src/adapters/providers.test.ts +125 -0
  180. package/src/adapters/providers.ts +161 -0
  181. package/src/adapters/roundtrip.test.ts +188 -0
  182. package/src/adapters/sessions.ts +15 -0
  183. package/src/adapters/settings.test.ts +93 -0
  184. package/src/adapters/settings.ts +165 -0
  185. package/src/adapters/skills.ts +14 -0
  186. package/src/adapters/test-helpers.ts +207 -0
  187. package/src/adapters/ui.test.ts +56 -0
  188. package/src/adapters/ui.ts +106 -0
  189. package/src/adapters/workspaces.test.ts +79 -0
  190. package/src/adapters/workspaces.ts +95 -0
  191. package/src/client/CLIENT_DEPENDENCIES.md +101 -0
  192. package/src/client/ConfigManagerSection.tsx +60 -0
  193. package/src/client/api.ts +270 -0
  194. package/src/client/client-types.ts +27 -0
  195. package/src/client/common/ErrorBanner.tsx +68 -0
  196. package/src/client/common/ProgressBar.tsx +49 -0
  197. package/src/client/common/ReportView.tsx +117 -0
  198. package/src/client/common/ui.tsx +165 -0
  199. package/src/client/config-manager.module.css +759 -0
  200. package/src/client/css-modules.d.ts +8 -0
  201. package/src/client/export/ExportView.tsx +222 -0
  202. package/src/client/import/ConflictList.tsx +72 -0
  203. package/src/client/import/ImportWizardView.tsx +406 -0
  204. package/src/client/import/PathMappingForm.tsx +77 -0
  205. package/src/client/index.ts +65 -0
  206. package/src/client/locales.ts +182 -0
  207. package/src/core/analyzer.ts +631 -0
  208. package/src/core/backup.ts +212 -0
  209. package/src/core/exporter.ts +246 -0
  210. package/src/core/importer.ts +66 -0
  211. package/src/core/index.ts +39 -0
  212. package/src/core/rollback.ts +140 -0
  213. package/src/core/smoke.test.ts +717 -0
  214. package/src/core/types.ts +343 -0
  215. package/src/core/validator.ts +84 -0
  216. package/src/index.ts +942 -0
  217. package/src/migrations/index.ts +71 -0
  218. package/src/migrations/v1-to-v2.ts +17 -0
  219. package/src/profiles/index.ts +8 -0
  220. package/src/profiles/profile-manager.test.ts +280 -0
  221. package/src/profiles/profile-manager.ts +606 -0
  222. package/src/schema/config.ts +137 -0
  223. package/src/schema/manifest.ts +124 -0
  224. package/src/schema/types.ts +179 -0
  225. package/src/schema/versions.ts +55 -0
  226. package/src/security/encryption.ts +180 -0
  227. package/src/security/index.ts +16 -0
  228. package/src/security/integrity.ts +102 -0
  229. package/src/security/redaction.ts +101 -0
  230. package/src/security/secret-scanner.ts +275 -0
  231. package/src/security/security.test.ts +796 -0
  232. package/src/security/zip-security.ts +180 -0
  233. package/src/ui/conflict-view.test.ts +62 -0
  234. package/src/ui/conflict-view.ts +65 -0
  235. package/src/ui/errors.test.ts +44 -0
  236. package/src/ui/errors.ts +95 -0
  237. package/src/ui/export-flow.test.ts +75 -0
  238. package/src/ui/export-flow.ts +128 -0
  239. package/src/ui/import-wizard.test.ts +128 -0
  240. package/src/ui/import-wizard.ts +208 -0
  241. package/src/ui/path-mapping.test.ts +67 -0
  242. package/src/ui/path-mapping.ts +103 -0
  243. package/src/ui/progress.test.ts +42 -0
  244. package/src/ui/progress.ts +90 -0
  245. package/src/ui/report.test.ts +93 -0
  246. package/src/ui/report.ts +140 -0
  247. package/src/ui/test-helpers.ts +177 -0
  248. package/src/ui/types.ts +193 -0
  249. package/src/utils/hashing.ts +53 -0
  250. package/src/utils/json.ts +77 -0
  251. package/src/utils/logger.ts +114 -0
  252. package/src/utils/paths.ts +122 -0
  253. package/src/utils/zip.ts +332 -0
@@ -0,0 +1,72 @@
1
+ /**
2
+ * plugins adapter 测试:插件清单 + 用户 patch 行导出、
3
+ * 已装同版本 Skip / 未装 Install / patch 行 Create,applyItem(install → needsRestart;patch 写回)。
4
+ */
5
+ import test from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { PluginsAdapter, USER_PATCH_FILE } from './plugins.ts';
8
+ import { makeContext, makeImportContext } from './test-helpers.ts';
9
+
10
+ test('plugins: 导出清单与 patch 行', async () => {
11
+ const ctx = makeContext('win32', 'C:\\Users\\alice');
12
+ ctx.plugins.installed.set('@linxin666/dsh-ssh', { name: '@linxin666/dsh-ssh', version: '0.1.12', enabled: true, isBundle: true, inBundles: ['@linxin666/dsh-web-ui-all'] });
13
+ ctx.plugins.installed.set('@deepseek-ai/dsh-base', { name: '@deepseek-ai/dsh-base', version: '0.1.0-rc.6', enabled: true });
14
+ ctx.patchFile.lines.set('skill-badge', { lineId: 'skill-badge', raw: { id: 'skill-badge', disabled: true } });
15
+
16
+ const adapter = new PluginsAdapter();
17
+ const out = await adapter.export(ctx, { includeSecrets: false });
18
+ assert.equal(out.data.version, 1);
19
+ assert.equal(out.data.plugins.length, 2);
20
+ assert.equal(out.data.patch.length, 1);
21
+ assert.equal(out.data.patch[0]?.lineId, 'skill-badge');
22
+ const ssh = out.data.plugins.find((p) => p.name === '@linxin666/dsh-ssh');
23
+ assert.equal(ssh?.isBundle, true);
24
+ assert.deepEqual(ssh?.inBundles, ['@linxin666/dsh-web-ui-all']);
25
+
26
+ const v = await adapter.validate(out.data);
27
+ assert.equal(v.valid, true);
28
+ });
29
+
30
+ test('plugins: 已装同版本 Skip / 未装 Install / 版本不同 Conflict / patch Create', async () => {
31
+ const src = makeContext('win32', 'C:\\Users\\alice');
32
+ src.plugins.installed.set('pkg-a', { name: 'pkg-a', version: '1.0.0', enabled: true });
33
+ src.plugins.installed.set('pkg-b', { name: 'pkg-b', version: '2.0.0', enabled: true });
34
+ src.patchFile.lines.set('my-line', { lineId: 'my-line', raw: { id: 'my-line', name: 'pkg-c', config: { x: 1 } } });
35
+ const adapter = new PluginsAdapter();
36
+ const exported = await adapter.export(src, { includeSecrets: false });
37
+ const sections = new Map([['plugins', exported.data]]);
38
+
39
+ const dst = makeContext('linux', '/home/bob');
40
+ dst.plugins.installed.set('pkg-a', { name: 'pkg-a', version: '1.0.0', enabled: true }); // 同版本
41
+ dst.plugins.installed.set('pkg-b', { name: 'pkg-b', version: '1.5.0', enabled: true }); // 不同版本
42
+ const items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
43
+ const byId = new Map(items.map((i) => [i.id, i]));
44
+ assert.equal(byId.get('plugin:pkg-a')?.kind, 'Skip');
45
+ assert.equal(byId.get('plugin:pkg-b')?.kind, 'Conflict');
46
+ assert.equal(byId.get('patch:my-line')?.kind, 'Create');
47
+ assert.equal(byId.get('patch:my-line')?.target?.ref, 'my-line');
48
+
49
+ // patch 行写入(Create)
50
+ const r = await adapter.applyItem(byId.get('patch:my-line')!, makeImportContext(dst, sections));
51
+ assert.equal(r.ok, true);
52
+ assert.equal(r.needsRestart, true);
53
+ assert.deepEqual(dst.patchFile.lines.get('my-line')?.raw, { id: 'my-line', name: 'pkg-c', config: { x: 1 } });
54
+ });
55
+
56
+ test('plugins: applyItem Install → needsRestart(官方机制,不打包二进制)', async () => {
57
+ const src = makeContext('win32', 'C:\\Users\\alice');
58
+ src.plugins.installed.set('need-install', { name: 'need-install', version: '3.0.0', enabled: true });
59
+ const adapter = new PluginsAdapter();
60
+ const exported = await adapter.export(src, { includeSecrets: false });
61
+ const sections = new Map([['plugins', exported.data]]);
62
+
63
+ const dst = makeContext('linux', '/home/bob');
64
+ const items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
65
+ const installItem = items.find((i) => i.id === 'plugin:need-install');
66
+ assert.equal(installItem?.kind, 'Install');
67
+ assert.equal(installItem?.target, undefined, 'Install 项不做快照(不可回滚,如实报告)');
68
+ const r = await adapter.applyItem(installItem!, makeImportContext(dst, sections));
69
+ assert.equal(r.ok, true);
70
+ assert.equal(r.needsRestart, true);
71
+ assert.ok(dst.plugins.installed.has('need-install'), '经 installPlugin 门面写入');
72
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * plugins 分区 adapter(设计 §3.3/§8):
3
+ * 数据源 = ctx.plugins.listInstalled()(插件清单)+ 用户 patch 层(profile cordis.patch.yml)。
4
+ *
5
+ * 安全不变量:绝不打包插件二进制;导入走 DSH 官方机制(installPlugin → needsRestart 提示)。
6
+ * patch 行导入(用户自定义行:启用/禁用/插入)写回 cordis.patch.yml,同样 needsRestart。
7
+ */
8
+ import { isDeepStrictEqual } from 'node:util';
9
+ import type { PatchLine, PluginEntry, PluginsSection } from '../schema/types.ts';
10
+ import type {
11
+ ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
12
+ ImportContext, PlanItem, ValidationResult,
13
+ } from '../core/types.ts';
14
+
15
+ export const USER_PATCH_FILE = 'cordis.patch.yml';
16
+
17
+ /** patch 行 raw 是否由其他 adapter 管理(mcp-client 行 / systemPrompt / planMode 行)。
18
+ * 这些行的导入归 mcp.ts / prompts.ts,plugins 分区只负责普通用户行(启用/禁用/插入插件等)。 */
19
+ function isManagedElsewhere(raw: unknown): boolean {
20
+ for (const entry of entriesOfRaw(raw)) {
21
+ const config = entry.config;
22
+ if (config === null || typeof config !== 'object') continue;
23
+ const c = config as Record<string, unknown>;
24
+ if (typeof c['serverName'] === 'string' && c['serverName'] !== '') return true;
25
+ if (c['systemPrompt'] !== undefined) return true;
26
+ if (c['planMode'] !== undefined) return true;
27
+ }
28
+ return false;
29
+ }
30
+
31
+ /** patch 行 → entry 列表(兼容单行与 insert 块;与 mcp/prompts 共用形态) */
32
+ function entriesOfRaw(raw: unknown): { config?: unknown }[] {
33
+ if (raw === null || typeof raw !== 'object') return [];
34
+ const obj = raw as Record<string, unknown>;
35
+ if (Array.isArray(obj['insert'])) {
36
+ return obj['insert']
37
+ .filter((e): e is Record<string, unknown> => e !== null && typeof e === 'object')
38
+ .map((e) => e as { config?: unknown });
39
+ }
40
+ if (obj['id'] !== undefined || obj['name'] !== undefined) {
41
+ return [obj as { config?: unknown }];
42
+ }
43
+ return [];
44
+ }
45
+
46
+ export class PluginsAdapter implements ConfigAdapter<PluginsSection> {
47
+ readonly id = 'plugins' as const;
48
+ readonly displayName = 'Plugins';
49
+ readonly defaultIncluded = true;
50
+ readonly portability = 'portable' as const;
51
+
52
+ async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<PluginsSection>> {
53
+ const plugins: PluginEntry[] = [];
54
+ const warnings: string[] = [];
55
+ try {
56
+ const installed = await ctx.plugins.listInstalled();
57
+ for (const p of installed) {
58
+ plugins.push({
59
+ name: p.name,
60
+ version: p.version,
61
+ isBundle: p.isBundle ?? false,
62
+ inBundles: p.inBundles ?? [],
63
+ enabled: p.enabled,
64
+ });
65
+ }
66
+ } catch (err) {
67
+ warnings.push(`插件清单读取失败: ${err instanceof Error ? err.message : String(err)}`);
68
+ }
69
+ const patch: PatchLine[] = [];
70
+ try {
71
+ const lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
72
+ for (const l of lines) patch.push({ file: USER_PATCH_FILE, lineId: l.lineId, raw: l.raw });
73
+ } catch (err) {
74
+ warnings.push(`patch 文件读取失败: ${err instanceof Error ? err.message : String(err)}`);
75
+ }
76
+ return {
77
+ sectionId: 'plugins',
78
+ data: { version: 1, plugins, patch },
79
+ counts: { plugins: plugins.length, patchLines: patch.length },
80
+ warnings,
81
+ };
82
+ }
83
+
84
+ async analyzeImport(data: PluginsSection, ctx: ImportContext): Promise<PlanItem[]> {
85
+ const items: PlanItem[] = [];
86
+ const targetInstalled = await ctx.target.plugins.listInstalled();
87
+
88
+ // 插件:包名唯一键;同版本 Skip / 未装 Install / 版本不同 Conflict
89
+ for (const p of data.plugins) {
90
+ const id = `plugin:${p.name}`;
91
+ const tp = targetInstalled.find((t) => t.name === p.name);
92
+ if (!tp) {
93
+ items.push({
94
+ id, kind: 'Install', adapter: 'plugins',
95
+ description: `安装插件 ${p.name}@${p.version}`,
96
+ detail: p.isBundle ? '(bundle 成员,经聚合包安装)' : undefined,
97
+ severity: 'info',
98
+ });
99
+ } else if (tp.version === p.version) {
100
+ items.push({ id, kind: 'Skip', adapter: 'plugins', description: `插件 ${p.name} 已安装且版本一致`, severity: 'info' });
101
+ } else {
102
+ items.push({
103
+ id, kind: 'Conflict', adapter: 'plugins',
104
+ description: `插件 ${p.name} 版本不同`,
105
+ detail: `当前 ${tp.version} vs 导入 ${p.version}`,
106
+ severity: 'warning',
107
+ });
108
+ }
109
+ }
110
+
111
+ // 用户 patch 行:lineId 唯一键;存在且同 → Skip;存在不同 → Conflict;不存在 → Create。
112
+ // mcp-client 行与 systemPrompt/planMode 行由 mcp/prompts adapter 管理,此处跳过(避免重复写入覆盖)。
113
+ const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
114
+ for (const pl of data.patch) {
115
+ if (isManagedElsewhere(pl.raw)) continue;
116
+ const id = `patch:${pl.lineId}`;
117
+ const tl = targetLines.find((l) => l.lineId === pl.lineId);
118
+ if (!tl) {
119
+ items.push({
120
+ id, kind: 'Create', adapter: 'plugins',
121
+ description: `写入 patch 行 ${pl.lineId}`, severity: 'info',
122
+ target: { adapter: 'plugins', ref: pl.lineId },
123
+ });
124
+ } else if (isDeepStrictEqual(tl.raw, pl.raw)) {
125
+ items.push({ id, kind: 'Skip', adapter: 'plugins', description: `patch 行 ${pl.lineId} 已一致`, severity: 'info' });
126
+ } else {
127
+ items.push({
128
+ id, kind: 'Conflict', adapter: 'plugins',
129
+ description: `patch 行 ${pl.lineId} 与目标不同`, severity: 'warning',
130
+ target: { adapter: 'plugins', ref: pl.lineId },
131
+ });
132
+ }
133
+ }
134
+ return items;
135
+ }
136
+
137
+ async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
138
+ // 插件安装:官方机制 installPlugin;needsRestart 提示(设计 §8:不打包二进制)
139
+ if (item.kind === 'Install') {
140
+ const name = item.id.replace(/^plugin:/, '');
141
+ const result = await ctx.target.plugins.install(name);
142
+ return {
143
+ ok: true,
144
+ needsRestart: true,
145
+ message: result.needsRestart ? `插件 ${name} 已安装,重启 dsh 后生效` : `插件 ${name} 已安装`,
146
+ };
147
+ }
148
+ // patch 行:Create → insert,Update/Conflict(useImported) → update
149
+ const ref = item.target?.ref;
150
+ if (!ref) return { ok: false, message: '缺少 target.ref' };
151
+ const data = ctx.sections.get('plugins') as PluginsSection | undefined;
152
+ const pl = data?.patch.find((p) => p.lineId === ref);
153
+ if (!pl) return { ok: false, message: `导入数据缺少 patch 行 ${ref}` };
154
+ await ctx.target.patchFile.applyPatchChanges(pl.file, [
155
+ { lineId: ref, raw: pl.raw, action: item.kind === 'Create' ? 'insert' : 'update' },
156
+ ]);
157
+ return { ok: true, needsRestart: true, message: `patch 行 ${ref} 已写入,重启后生效` };
158
+ }
159
+
160
+ async validate(data: PluginsSection): Promise<ValidationResult> {
161
+ const issues: ValidationResult['issues'] = [];
162
+ if (data === null || typeof data !== 'object') {
163
+ return { valid: false, issues: [{ path: '$', message: 'plugins 数据必须是对象', severity: 'error' }] };
164
+ }
165
+ if (data.version !== 1) {
166
+ issues.push({ path: 'version', message: `version 必须为 1(收到 ${String(data.version)})`, severity: 'error' });
167
+ }
168
+ if (!Array.isArray(data.plugins)) {
169
+ issues.push({ path: 'plugins', message: 'plugins 必须是数组', severity: 'error' });
170
+ }
171
+ if (data.patch !== undefined && !Array.isArray(data.patch)) {
172
+ issues.push({ path: 'patch', message: 'patch 必须是数组', severity: 'error' });
173
+ }
174
+ return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
175
+ }
176
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * prompts adapter 测试:patch config 提取(systemPrompt persona / planMode sections)、
3
+ * 同行 Skip/Conflict、目标无来源行 → Warning(诚实降级)、applyItem 合并写回。
4
+ */
5
+ import test from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { PromptsAdapter, extractPrompts, mergePromptIntoLine } from './prompts.ts';
8
+ import { makeContext, makeImportContext } from './test-helpers.ts';
9
+ import type { PromptsExportSection } from './prompts.ts';
10
+ import type { PlanItem } from '../core/types.ts';
11
+
12
+ const LINES = [
13
+ {
14
+ lineId: 'persona-line',
15
+ raw: { id: 'persona-line', name: '@deepseek-ai/dsh-web', config: { systemPrompt: { persona: 'You are a helpful coding assistant.' } } },
16
+ },
17
+ {
18
+ lineId: 'plan-line',
19
+ raw: { id: 'plan-line', name: 'plan-mode', config: { planMode: { sections: [{ name: 'plan', order: 1, text: 'Plan first, then code.' }] } } },
20
+ },
21
+ ];
22
+
23
+ test('prompts: extractPrompts 提取 persona 与 planMode sections', () => {
24
+ const prompts = extractPrompts(LINES);
25
+ assert.equal(prompts.length, 2);
26
+ const persona = prompts.find((p) => p.kind === 'systemPrompt');
27
+ assert.equal(persona?.name, 'persona-line:persona');
28
+ assert.equal(persona?.text, 'You are a helpful coding assistant.');
29
+ assert.equal(persona?.sourceLineId, 'persona-line');
30
+ const plan = prompts.find((p) => p.kind === 'planMode');
31
+ assert.equal(plan?.name, 'plan');
32
+ assert.equal(plan?.text, 'Plan first, then code.');
33
+ });
34
+
35
+ test('prompts: mergePromptIntoLine 合并 persona / planMode 文本', () => {
36
+ // persona 更新(原为对象形态)
37
+ const merged = mergePromptIntoLine(LINES[0]!.raw, {
38
+ id: 'prompt:persona-line:persona', name: 'persona-line:persona', kind: 'systemPrompt',
39
+ text: 'New persona.', sourceLineId: 'persona-line',
40
+ });
41
+ const config = (merged as Record<string, unknown>)['config'] as Record<string, unknown>;
42
+ assert.equal((config['systemPrompt'] as Record<string, unknown>)['persona'], 'New persona.');
43
+
44
+ // planMode 新增 section
45
+ const planMerged = mergePromptIntoLine(LINES[1]!.raw, {
46
+ id: 'prompt:plan-line:step2', name: 'step2', kind: 'planMode',
47
+ text: 'Step two text.', sourceLineId: 'plan-line',
48
+ });
49
+ const planConfig = (planMerged as Record<string, unknown>)['config'] as Record<string, unknown>;
50
+ const sections = (planConfig['planMode'] as Record<string, unknown>)['sections'] as Record<string, unknown>[];
51
+ assert.equal(sections.length, 2);
52
+ assert.equal(sections[1]?.['text'], 'Step two text.');
53
+ });
54
+
55
+ test('prompts: 同行同内容 Skip / 不同 Conflict(update) / 目标无 → Warning', async () => {
56
+ const src = makeContext('win32', 'C:\\Users\\alice');
57
+ for (const l of LINES) src.patchFile.lines.set(l.lineId, { lineId: l.lineId, raw: l.raw });
58
+ const adapter = new PromptsAdapter();
59
+ const exported = await adapter.export(src, { includeSecrets: false });
60
+ assert.equal(exported.data.prompts.length, 2);
61
+ const sections = new Map([['prompts', exported.data]]);
62
+
63
+ const dst = makeContext('linux', '/home/bob');
64
+ for (const l of LINES) dst.patchFile.lines.set(l.lineId, { lineId: l.lineId, raw: l.raw });
65
+ let items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
66
+ assert.ok(items.every((i) => i.kind === 'Skip'), '目标一致 → 全部 Skip');
67
+
68
+ // 目标 persona 不同 → Conflict(target.ref = 来源行)
69
+ dst.patchFile.lines.set('persona-line', {
70
+ lineId: 'persona-line',
71
+ raw: { id: 'persona-line', name: '@deepseek-ai/dsh-web', config: { systemPrompt: { persona: 'Old persona.' } } },
72
+ });
73
+ items = await adapter.analyzeImport(exported.data, makeImportContext(dst, sections));
74
+ const conflict = items.find((i) => i.id === 'prompt:persona-line:persona');
75
+ assert.equal(conflict?.kind, 'Conflict');
76
+ assert.equal(conflict?.target?.ref, 'persona-line');
77
+ const useItem: PlanItem = { ...conflict!, kind: 'Update' };
78
+ const r = await adapter.applyItem(useItem, makeImportContext(dst, sections));
79
+ assert.equal(r.ok, true);
80
+ assert.equal(r.needsRestart, true);
81
+ const raw = dst.patchFile.lines.get('persona-line')?.raw as Record<string, unknown>;
82
+ const sp = (raw['config'] as Record<string, unknown>)['systemPrompt'] as Record<string, unknown>;
83
+ assert.equal(sp['persona'], 'You are a helpful coding assistant.', 'Conflict useImported 应合并更新 persona');
84
+
85
+ // 目标无来源行但有行名 → Create(重建行)
86
+ const dst2 = makeContext('darwin', '/Users/bob');
87
+ items = await adapter.analyzeImport(exported.data, makeImportContext(dst2, sections));
88
+ assert.ok(items.every((i) => i.kind === 'Create'), '目标无来源行但有行名 → Create 重建');
89
+ for (const item of items) {
90
+ const rc = await adapter.applyItem(item, makeImportContext(dst2, sections));
91
+ assert.equal(rc.ok, true);
92
+ }
93
+ const created = dst2.patchFile.lines.get('persona-line')?.raw as Record<string, unknown>;
94
+ assert.equal(((created['config'] as Record<string, unknown>)['systemPrompt'] as Record<string, unknown>)['persona'], 'You are a helpful coding assistant.');
95
+
96
+ // 无行名的 prompt(来源信息缺失)→ Warning,不编造自动创建
97
+ const orphan: PromptsExportSection = { version: 1, prompts: [{ id: 'p1', name: 'orphan', kind: 'systemPrompt', text: 'x' }] };
98
+ const itemsNoName = await adapter.analyzeImport(orphan, makeImportContext(dst2, new Map([['prompts', orphan]])));
99
+ assert.ok(itemsNoName.every((i) => i.kind === 'Warning'), '来源行名缺失 → Warning 提示手动配置');
100
+ const w = await adapter.applyItem(itemsNoName[0]!, makeImportContext(dst2, sections));
101
+ assert.equal(w.ok, true, 'Warning 项 applyItem 无副作用');
102
+ });
103
+
104
+ test('prompts: validate', async () => {
105
+ const adapter = new PromptsAdapter();
106
+ const ok = await adapter.validate({ version: 1, prompts: [{ id: 'a', name: 'n', kind: 'systemPrompt', text: 't' }] });
107
+ assert.equal(ok.valid, true);
108
+ const bad = await adapter.validate({ version: 1, prompts: [{ id: 'a', name: 1 as never, kind: 'systemPrompt', text: 't' }] });
109
+ assert.equal(bad.valid, false);
110
+ });
@@ -0,0 +1,226 @@
1
+ /**
2
+ * prompts 分区 adapter(设计 §3.3/§1.2):
3
+ * 数据源 = 组合 patch config 中的 systemPrompt persona / plan-mode section(研究报告 §2.2:
4
+ * DSH 无独立 prompt/rules 文件,persona 与 plan-mode 均经 patch config 注入)。
5
+ *
6
+ * v1 策略(诚实降级):
7
+ * - 目标存在同 lineId 行 → 同内容 Skip / 不同 Conflict(更新该行 config);
8
+ * - 目标无该行但存在同名 prompt → Conflict(更新其所在行);
9
+ * - 均不存在 → Warning(不编造行名自动创建,提示手动配置)。
10
+ * rules/commands 无独立存储(研究报告 §2.2),不实现。
11
+ */
12
+ import { isDeepStrictEqual } from 'node:util';
13
+ import type { PromptEntry, PromptsSection } from '../schema/types.ts';
14
+ import type {
15
+ ApplyResult, ConfigAdapter, ExportOptions, ExportSection, HostContext,
16
+ ImportContext, PlanItem, ValidationResult,
17
+ } from '../core/types.ts';
18
+ import { USER_PATCH_FILE } from './plugins.ts';
19
+
20
+ /** 导出记录:PromptEntry 之外记录来源行名(导入需要重建行时使用) */
21
+ export interface PromptExportEntry extends PromptEntry {
22
+ sourceLineName?: string;
23
+ }
24
+
25
+ export interface PromptsExportSection { version: 1; prompts: PromptExportEntry[]; }
26
+
27
+ /** 从 patch 行 config 中提取 prompt 条目(systemPrompt.persona / planMode.sections[].text) */
28
+ export function extractPrompts(lines: { lineId: string; raw: unknown }[]): PromptExportEntry[] {
29
+ const prompts: PromptExportEntry[] = [];
30
+ for (const line of lines) {
31
+ for (const entry of entriesOf(line.raw)) {
32
+ const config = entry.config;
33
+ if (config === null || typeof config !== 'object') continue;
34
+ const c = config as Record<string, unknown>;
35
+ const sourceLineName = typeof entry.name === 'string' ? entry.name : undefined;
36
+
37
+ const sp = c['systemPrompt'];
38
+ if (typeof sp === 'string' && sp.trim() !== '') {
39
+ prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: sp, sourceLineId: line.lineId, sourceLineName });
40
+ } else if (sp !== null && typeof sp === 'object' && typeof (sp as Record<string, unknown>)['persona'] === 'string') {
41
+ const persona = (sp as Record<string, unknown>)['persona'] as string;
42
+ if (persona.trim() !== '') {
43
+ prompts.push({ id: `prompt:${line.lineId}:persona`, name: `${line.lineId}:persona`, kind: 'systemPrompt', text: persona, sourceLineId: line.lineId, sourceLineName });
44
+ }
45
+ }
46
+
47
+ const pm = c['planMode'];
48
+ if (pm !== null && typeof pm === 'object' && Array.isArray((pm as Record<string, unknown>)['sections'])) {
49
+ const sections = (pm as Record<string, unknown>)['sections'] as Record<string, unknown>[];
50
+ for (const [i, s] of sections.entries()) {
51
+ if (s === null || typeof s !== 'object' || typeof s['text'] !== 'string' || (s['text'] as string).trim() === '') continue;
52
+ const name = typeof s['name'] === 'string' && s['name'] !== '' ? s['name'] : `${line.lineId}:section${i}`;
53
+ prompts.push({ id: `prompt:${line.lineId}:${name}`, name, kind: 'planMode', text: s['text'] as string, sourceLineId: line.lineId, sourceLineName });
54
+ }
55
+ }
56
+ }
57
+ }
58
+ return prompts;
59
+ }
60
+
61
+ /** patch 行 → 可枚举 entry(兼容单行与 insert 块;与 mcp.ts 共用形态) */
62
+ function entriesOf(raw: unknown): { id?: unknown; name?: unknown; config?: unknown }[] {
63
+ if (raw === null || typeof raw !== 'object') return [];
64
+ const obj = raw as Record<string, unknown>;
65
+ if (Array.isArray(obj['insert'])) {
66
+ return obj['insert']
67
+ .filter((e): e is Record<string, unknown> => e !== null && typeof e === 'object')
68
+ .map((e) => e as { id?: unknown; name?: unknown; config?: unknown });
69
+ }
70
+ if (obj['id'] !== undefined || obj['name'] !== undefined) {
71
+ return [obj as { id?: unknown; name?: unknown; config?: unknown }];
72
+ }
73
+ return [];
74
+ }
75
+
76
+ /** 把 prompt 文本合并进目标行 config(systemPrompt.persona 或 planMode.sections) */
77
+ export function mergePromptIntoLine(raw: unknown, prompt: PromptExportEntry): Record<string, unknown> {
78
+ const cloned = structuredClone(raw) as Record<string, unknown>;
79
+ if (cloned['config'] === null || typeof cloned['config'] !== 'object') cloned['config'] = {};
80
+ const config = cloned['config'] as Record<string, unknown>;
81
+ if (prompt.kind === 'systemPrompt') {
82
+ const sp = config['systemPrompt'];
83
+ if (typeof sp === 'string') config['systemPrompt'] = { persona: sp };
84
+ else if (sp === null || typeof sp !== 'object') config['systemPrompt'] = { persona: prompt.text };
85
+ else (config['systemPrompt'] as Record<string, unknown>)['persona'] = prompt.text;
86
+ } else {
87
+ if (config['planMode'] === null || typeof config['planMode'] !== 'object') config['planMode'] = {};
88
+ const pm = config['planMode'] as Record<string, unknown>;
89
+ if (!Array.isArray(pm['sections'])) pm['sections'] = [];
90
+ const sections = pm['sections'] as Record<string, unknown>[];
91
+ const sec = sections.find((s) => s !== null && typeof s === 'object' && s['name'] === prompt.name);
92
+ if (sec) sec['text'] = prompt.text;
93
+ else sections.push({ name: prompt.name, text: prompt.text });
94
+ }
95
+ return cloned;
96
+ }
97
+
98
+ /** 由导出条目构造「重建行」raw(Create 导入用;行名取自来源行,缺失时用占位名) */
99
+ export function buildPromptLine(lineId: string, prompt: PromptExportEntry): Record<string, unknown> {
100
+ const name = prompt.sourceLineName ?? 'dsh-config-manager';
101
+ if (prompt.kind === 'planMode') {
102
+ return { id: lineId, name, config: { planMode: { sections: [{ name: prompt.name, text: prompt.text }] } } };
103
+ }
104
+ return { id: lineId, name, config: { systemPrompt: { persona: prompt.text } } };
105
+ }
106
+
107
+ export class PromptsAdapter implements ConfigAdapter<PromptsExportSection> {
108
+ readonly id = 'prompts' as const;
109
+ readonly displayName = 'Prompts';
110
+ readonly defaultIncluded = true;
111
+ readonly portability = 'portable' as const;
112
+
113
+ async export(ctx: HostContext, _options: ExportOptions): Promise<ExportSection<PromptsExportSection>> {
114
+ const warnings: string[] = [];
115
+ let lines: { lineId: string; raw: unknown }[] = [];
116
+ try {
117
+ lines = await ctx.patchFile.readPatchLines(USER_PATCH_FILE);
118
+ } catch (err) {
119
+ warnings.push(`patch 文件读取失败,无法提取 prompts: ${err instanceof Error ? err.message : String(err)}`);
120
+ }
121
+ const prompts = extractPrompts(lines);
122
+ return {
123
+ sectionId: 'prompts',
124
+ data: { version: 1, prompts },
125
+ counts: { prompts: prompts.length },
126
+ warnings,
127
+ };
128
+ }
129
+
130
+ async analyzeImport(data: PromptsExportSection, ctx: ImportContext): Promise<PlanItem[]> {
131
+ const items: PlanItem[] = [];
132
+ const targetLines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
133
+ const targetPrompts = extractPrompts(targetLines);
134
+ for (const p of data.prompts) {
135
+ const id = p.id;
136
+ const sameName = targetPrompts.find((t) => t.name === p.name);
137
+ const targetLine = targetLines.find((l) => l.lineId === p.sourceLineId);
138
+ if (targetLine && sameName && sameName.sourceLineId === p.sourceLineId) {
139
+ if (sameName.text === p.text && sameName.kind === p.kind) {
140
+ items.push({ id, kind: 'Skip', adapter: 'prompts', description: `提示词 ${p.name} 已一致`, severity: 'info' });
141
+ } else {
142
+ items.push({
143
+ id, kind: 'Conflict', adapter: 'prompts',
144
+ description: `提示词 ${p.name} 与目标不同`,
145
+ detail: `行 ${p.sourceLineId} current=${JSON.stringify(sameName.text).slice(0, 80)} imported=${JSON.stringify(p.text).slice(0, 80)}`,
146
+ severity: 'warning',
147
+ target: { adapter: 'prompts', ref: p.sourceLineId ?? '' },
148
+ });
149
+ }
150
+ } else if (sameName) {
151
+ // 目标存在同名 prompt(不同来源行)→ 更新其所在行
152
+ if (sameName.text === p.text && sameName.kind === p.kind) {
153
+ items.push({ id, kind: 'Skip', adapter: 'prompts', description: `提示词 ${p.name} 已一致`, severity: 'info' });
154
+ } else {
155
+ items.push({
156
+ id, kind: 'Conflict', adapter: 'prompts',
157
+ description: `提示词 ${p.name} 与目标不同(目标位于行 ${sameName.sourceLineId})`,
158
+ severity: 'warning',
159
+ target: { adapter: 'prompts', ref: sameName.sourceLineId ?? '' },
160
+ });
161
+ }
162
+ } else if (p.sourceLineName) {
163
+ // 目标无来源行但记录到行名 → 重建行(Create);幂等键 = 行 id
164
+ const lineId = p.sourceLineId ?? `prompt-${p.name}`;
165
+ items.push({
166
+ id, kind: 'Create', adapter: 'prompts',
167
+ description: `创建提示词 ${p.name}(行 ${lineId})`, severity: 'info',
168
+ target: { adapter: 'prompts', ref: lineId },
169
+ });
170
+ } else {
171
+ items.push({
172
+ id, kind: 'Warning', adapter: 'prompts',
173
+ description: `提示词 ${p.name} 在目标无对应来源,请手动配置(DSH 无独立 prompt 存储)`,
174
+ severity: 'info',
175
+ });
176
+ }
177
+ }
178
+ return items;
179
+ }
180
+
181
+ async applyItem(item: PlanItem, ctx: ImportContext): Promise<ApplyResult> {
182
+ if (item.kind === 'Warning') return { ok: true, message: '提示项,无需写入' };
183
+ const ref = item.target?.ref;
184
+ if (!ref) return { ok: false, message: '缺少 target.ref' };
185
+ const data = ctx.sections.get('prompts') as PromptsExportSection | undefined;
186
+ const prompt = data?.prompts.find((p) => p.id === item.id);
187
+ if (!prompt) return { ok: false, message: `导入数据缺少提示词 ${item.id}` };
188
+ // Create:目标无来源行 → 用记录的行名重建 patch 行(insert)
189
+ if (item.kind === 'Create') {
190
+ const raw = buildPromptLine(ref, prompt);
191
+ await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
192
+ { lineId: ref, raw, action: 'insert' },
193
+ ]);
194
+ return { ok: true, needsRestart: true, message: `提示词 ${prompt.name} 已创建(行 ${ref})` };
195
+ }
196
+ // Update / Conflict(useImported):合并进目标行 config
197
+ const lines = await ctx.target.patchFile.readPatchLines(USER_PATCH_FILE);
198
+ const line = lines.find((l) => l.lineId === ref);
199
+ if (!line) return { ok: false, message: `目标 patch 行 ${ref} 不存在,无法写入` };
200
+ const newRaw = mergePromptIntoLine(line.raw, prompt);
201
+ await ctx.target.patchFile.applyPatchChanges(USER_PATCH_FILE, [
202
+ { lineId: ref, raw: newRaw, action: 'update' },
203
+ ]);
204
+ return { ok: true, needsRestart: true, message: `提示词 ${prompt.name} 已写入 patch 行 ${ref}` };
205
+ }
206
+
207
+ async validate(data: PromptsExportSection): Promise<ValidationResult> {
208
+ const issues: ValidationResult['issues'] = [];
209
+ if (data === null || typeof data !== 'object') {
210
+ return { valid: false, issues: [{ path: '$', message: 'prompts 数据必须是对象', severity: 'error' }] };
211
+ }
212
+ if (data.version !== 1) {
213
+ issues.push({ path: 'version', message: `version 必须为 1(收到 ${String(data.version)})`, severity: 'error' });
214
+ }
215
+ if (!Array.isArray(data.prompts)) {
216
+ issues.push({ path: 'prompts', message: 'prompts 必须是数组', severity: 'error' });
217
+ } else {
218
+ for (const p of data.prompts) {
219
+ if (p === null || typeof p !== 'object' || typeof p.name !== 'string' || typeof p.text !== 'string') {
220
+ issues.push({ path: 'prompts[]', message: 'prompt 记录必须含字符串 name 与 text', severity: 'error' });
221
+ }
222
+ }
223
+ }
224
+ return { valid: issues.filter((i) => i.severity === 'error').length === 0, issues };
225
+ }
226
+ }