dsh-archify-projects 0.0.0-stage → 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 (148) hide show
  1. package/README.md +204 -2
  2. package/cordis.patch.yml +99 -0
  3. package/dist/client.js +2261 -0
  4. package/dist/client.js.map +1 -0
  5. package/docs/CONTAINER.md +197 -0
  6. package/docs/DESIGN.md +905 -0
  7. package/docs/HANDOVER.md +391 -0
  8. package/docs/PUBLISH.md +107 -0
  9. package/icon.svg +12 -0
  10. package/lib/host/archify-skill.js +116 -0
  11. package/lib/host/archify-skill.js.map +1 -0
  12. package/lib/host/config.js +95 -0
  13. package/lib/host/config.js.map +1 -0
  14. package/lib/host/context.js +28 -0
  15. package/lib/host/context.js.map +1 -0
  16. package/lib/host/git.js +185 -0
  17. package/lib/host/git.js.map +1 -0
  18. package/lib/host/http.js +184 -0
  19. package/lib/host/http.js.map +1 -0
  20. package/lib/host/index.js +90 -0
  21. package/lib/host/index.js.map +1 -0
  22. package/lib/host/policy.js +124 -0
  23. package/lib/host/policy.js.map +1 -0
  24. package/lib/host/routes.js +667 -0
  25. package/lib/host/routes.js.map +1 -0
  26. package/lib/host/runner.js +913 -0
  27. package/lib/host/runner.js.map +1 -0
  28. package/lib/host/store.js +370 -0
  29. package/lib/host/store.js.map +1 -0
  30. package/lib/host/workspace.js +65 -0
  31. package/lib/host/workspace.js.map +1 -0
  32. package/lib/shared/format.js +138 -0
  33. package/lib/shared/format.js.map +1 -0
  34. package/lib/shared/prompt.js +257 -0
  35. package/lib/shared/prompt.js.map +1 -0
  36. package/lib/shared/protocol.js +31 -0
  37. package/lib/shared/protocol.js.map +1 -0
  38. package/lib/shared/types.js +173 -0
  39. package/lib/shared/types.js.map +1 -0
  40. package/locale/en.json +6 -0
  41. package/locale/zh.json +6 -0
  42. package/package.json +98 -4
  43. package/skills/archify/LICENSE +22 -0
  44. package/skills/archify/SKILL.md +109 -0
  45. package/skills/archify/THIRD_PARTY_NOTICES.md +69 -0
  46. package/skills/archify/assets/JetBrainsMono-OFL.txt +93 -0
  47. package/skills/archify/assets/template.html +13709 -0
  48. package/skills/archify/bin/archify.mjs +6936 -0
  49. package/skills/archify/bin/delivery-update.mjs +91 -0
  50. package/skills/archify/bin/finalize.mjs +895 -0
  51. package/skills/archify/bin/open-artifact.mjs +146 -0
  52. package/skills/archify/bin/preview.mjs +969 -0
  53. package/skills/archify/bin/recover-output.mjs +41 -0
  54. package/skills/archify/bin/visual-check.mjs +2667 -0
  55. package/skills/archify/brand-marks/README.md +31 -0
  56. package/skills/archify/brand-marks/catalog.json +131 -0
  57. package/skills/archify/delta/architecture-delta.mjs +1313 -0
  58. package/skills/archify/examples/agent-run.lifecycle.json +55 -0
  59. package/skills/archify/examples/agent-tool-call.workflow.json +74 -0
  60. package/skills/archify/examples/async-job-roundtrip.sequence.json +56 -0
  61. package/skills/archify/examples/brand-aware-delivery.architecture.json +34 -0
  62. package/skills/archify/examples/cache-miss-request.sequence.json +77 -0
  63. package/skills/archify/examples/checkout-platform.base.architecture.json +32 -0
  64. package/skills/archify/examples/checkout-platform.head.architecture.json +32 -0
  65. package/skills/archify/examples/dataflow-product-analytics.html +13820 -0
  66. package/skills/archify/examples/deployment-release.lifecycle.json +48 -0
  67. package/skills/archify/examples/event-stream.dataflow.json +52 -0
  68. package/skills/archify/examples/holiday-planning.workflow.json +143 -0
  69. package/skills/archify/examples/incident-response.workflow.json +59 -0
  70. package/skills/archify/examples/lifecycle-agent-run.html +13765 -0
  71. package/skills/archify/examples/locales/de.json +425 -0
  72. package/skills/archify/examples/locales/es.json +440 -0
  73. package/skills/archify/examples/locales/fr.json +425 -0
  74. package/skills/archify/examples/locales/fr.partial.json +22 -0
  75. package/skills/archify/examples/locales/it.json +425 -0
  76. package/skills/archify/examples/locales/ja.json +425 -0
  77. package/skills/archify/examples/locales/ko.json +440 -0
  78. package/skills/archify/examples/locales/pt.json +425 -0
  79. package/skills/archify/examples/locales/ru.json +425 -0
  80. package/skills/archify/examples/product-analytics.dataflow.json +71 -0
  81. package/skills/archify/examples/production-deployment.architecture.json +51 -0
  82. package/skills/archify/examples/release-delivery.workflow.json +57 -0
  83. package/skills/archify/examples/sequence-cache-miss-request.html +13835 -0
  84. package/skills/archify/examples/web-app-rendered.html +13808 -0
  85. package/skills/archify/examples/web-app.architecture.json +41 -0
  86. package/skills/archify/examples/workflow-agent-tool-call-rendered.html +13826 -0
  87. package/skills/archify/migrations/workflow-v2.mjs +280 -0
  88. package/skills/archify/package.json +17 -0
  89. package/skills/archify/recipes/scenarios.mjs +430 -0
  90. package/skills/archify/references/architecture-layout-repair.md +18 -0
  91. package/skills/archify/references/authoring-contract.md +419 -0
  92. package/skills/archify/references/authoring-defaults.md +41 -0
  93. package/skills/archify/references/brand-marks.md +82 -0
  94. package/skills/archify/references/delivery-contract.md +588 -0
  95. package/skills/archify/references/repository-authoring.md +105 -0
  96. package/skills/archify/references/update-awareness.md +12 -0
  97. package/skills/archify/references/viewer-runtime.md +35 -0
  98. package/skills/archify/renderers/architecture/grid.mjs +62 -0
  99. package/skills/archify/renderers/architecture/labels.mjs +215 -0
  100. package/skills/archify/renderers/architecture/render-architecture.mjs +1001 -0
  101. package/skills/archify/renderers/architecture/routing.mjs +1157 -0
  102. package/skills/archify/renderers/dataflow/README.md +105 -0
  103. package/skills/archify/renderers/dataflow/render-dataflow.mjs +544 -0
  104. package/skills/archify/renderers/lifecycle/README.md +171 -0
  105. package/skills/archify/renderers/lifecycle/grid-routing.mjs +429 -0
  106. package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +938 -0
  107. package/skills/archify/renderers/sequence/README.md +129 -0
  108. package/skills/archify/renderers/sequence/render-sequence.mjs +526 -0
  109. package/skills/archify/renderers/shared/atomic-output.mjs +1982 -0
  110. package/skills/archify/renderers/shared/brand-marks.mjs +670 -0
  111. package/skills/archify/renderers/shared/cli.mjs +451 -0
  112. package/skills/archify/renderers/shared/desktop-readability.mjs +132 -0
  113. package/skills/archify/renderers/shared/diagnostics.mjs +173 -0
  114. package/skills/archify/renderers/shared/engineering-profiles.mjs +157 -0
  115. package/skills/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  116. package/skills/archify/renderers/shared/generated-validators.mjs +134 -0
  117. package/skills/archify/renderers/shared/geometry.mjs +1941 -0
  118. package/skills/archify/renderers/shared/i18n.mjs +603 -0
  119. package/skills/archify/renderers/shared/layout-report.mjs +42 -0
  120. package/skills/archify/renderers/shared/legend.mjs +217 -0
  121. package/skills/archify/renderers/shared/output-path.mjs +440 -0
  122. package/skills/archify/renderers/shared/path-semantics.mjs +1017 -0
  123. package/skills/archify/renderers/shared/portable-path.mjs +408 -0
  124. package/skills/archify/renderers/shared/repository-evidence.mjs +352 -0
  125. package/skills/archify/renderers/shared/repository-location.mjs +59 -0
  126. package/skills/archify/renderers/shared/route-quality.mjs +614 -0
  127. package/skills/archify/renderers/shared/sidecar-path.mjs +38 -0
  128. package/skills/archify/renderers/shared/spatial-grid.mjs +66 -0
  129. package/skills/archify/renderers/shared/text-fit.mjs +105 -0
  130. package/skills/archify/renderers/shared/utils.mjs +254 -0
  131. package/skills/archify/renderers/shared/validator.mjs +86 -0
  132. package/skills/archify/renderers/workflow/README.md +301 -0
  133. package/skills/archify/renderers/workflow/render-workflow.mjs +37 -0
  134. package/skills/archify/renderers/workflow/workflow-compiler.mjs +4913 -0
  135. package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  136. package/skills/archify/schemas/README.md +256 -0
  137. package/skills/archify/schemas/architecture.schema.json +152 -0
  138. package/skills/archify/schemas/common.schema.json +181 -0
  139. package/skills/archify/schemas/dataflow.schema.json +254 -0
  140. package/skills/archify/schemas/lifecycle.schema.json +280 -0
  141. package/skills/archify/schemas/sequence.schema.json +234 -0
  142. package/skills/archify/schemas/workflow.schema.json +439 -0
  143. package/skills/archify/scripts/check-render-output.mjs +1388 -0
  144. package/skills/archify/scripts/check-update.mjs +1710 -0
  145. package/skills/archify/scripts/delivery-update-child.mjs +23 -0
  146. package/skills/archify/scripts/render-examples.mjs +26 -0
  147. package/skills/archify/scripts/update-contract.mjs +182 -0
  148. package/skills/archify/skill-release.json +10 -0
@@ -0,0 +1,667 @@
1
+ /**
2
+ * 路由分发:一个 prefix 路由收全部请求,内部按「方法 + 路径模式」派发。
3
+ *
4
+ * 为什么只注册一条:宿主 webServer 的 exact 表只支持字面路径,而本插件的路由全都带
5
+ * `:id`;用 prefix 路由 + 自己匹配,路由表集中在一处,读起来就是一张清单。
6
+ */
7
+ import { rm, stat } from 'node:fs/promises';
8
+ import { createReadStream } from 'node:fs';
9
+ import path from 'node:path';
10
+ import { ROUTE_PREFIX } from '../shared/protocol.js';
11
+ import { allDiagrams, createProject, defaultProjectName, findDiagram, runnableDiagrams, ARCHIFY_TYPES, } from '../shared/types.js';
12
+ import { projectDir, workspaceRoot } from './config.js';
13
+ import { probeGit, syncToLatest } from './git.js';
14
+ import { BadRequest, matchPath, methodOf, pathOf, readJsonObject, sendError, sendOk, } from './http.js';
15
+ import { RunnerBusy, RunnerUnavailable } from './runner.js';
16
+ import { confineToRoot, isDirectory } from './workspace.js';
17
+ /** 生成项目 id。 */
18
+ export function newProjectId(now = new Date()) {
19
+ return `prj_${now.getTime().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
20
+ }
21
+ /** 生成自定义图 id。 */
22
+ export function newDiagramId(now = new Date()) {
23
+ return `dgm_${now.getTime().toString(36)}${Math.random().toString(36).slice(2, 5)}`;
24
+ }
25
+ /** 分发入口,交给 `webServer.register({kind:'prefix'})`。 */
26
+ export function createDispatcher(state) {
27
+ return async function dispatch(req, res) {
28
+ try {
29
+ const rejection = state.fence(req);
30
+ if (rejection !== null) {
31
+ sendError(res, 403, 'forbidden', rejection);
32
+ return;
33
+ }
34
+ const pathname = pathOf(req);
35
+ if (!pathname.startsWith(ROUTE_PREFIX)) {
36
+ sendError(res, 404, 'not-found', '未知路径');
37
+ return;
38
+ }
39
+ const rest = pathname.slice(ROUTE_PREFIX.length) || '/';
40
+ await route(state, req, res, rest);
41
+ }
42
+ catch (err) {
43
+ if (err instanceof BadRequest) {
44
+ sendError(res, 400, err.code, err.message);
45
+ return;
46
+ }
47
+ sendError(res, 500, 'internal', '插件内部错误', err);
48
+ }
49
+ };
50
+ }
51
+ async function route(state, req, res, rest) {
52
+ const method = methodOf(req);
53
+ if (rest === '/health' && method === 'GET')
54
+ return handleHealth(state, res);
55
+ if (rest === '/projects' && method === 'GET')
56
+ return handleList(state, res);
57
+ if (rest === '/projects' && method === 'POST')
58
+ return handleCreate(state, req, res);
59
+ if (rest.startsWith('/artifact/') && method === 'GET') {
60
+ return handleArtifact(state, req, res, rest.slice('/artifact/'.length));
61
+ }
62
+ const checkMatch = matchPath('/projects/:id/check', rest);
63
+ if (checkMatch !== null && method === 'POST') {
64
+ return handleCheck(state, res, checkMatch.id);
65
+ }
66
+ const updateMatch = matchPath('/projects/:id/update', rest);
67
+ if (updateMatch !== null && method === 'POST') {
68
+ return handleUpdate(state, res, updateMatch.id);
69
+ }
70
+ const diagramsMatch = matchPath('/projects/:id/diagrams', rest);
71
+ if (diagramsMatch !== null && method === 'POST') {
72
+ return handleCreateDiagram(state, req, res, diagramsMatch.id);
73
+ }
74
+ const regenerateMatch = matchPath('/projects/:id/diagrams/:diagramId/regenerate', rest);
75
+ if (regenerateMatch !== null && method === 'POST') {
76
+ return handleRegenerateDiagram(state, res, regenerateMatch.id, regenerateMatch.diagramId);
77
+ }
78
+ const askMatch = matchPath('/projects/:id/diagrams/:diagramId/ask', rest);
79
+ if (askMatch !== null && method === 'POST') {
80
+ return handleAsk(state, req, res, askMatch.id, askMatch.diagramId);
81
+ }
82
+ const mergeMatch = matchPath('/projects/:id/qa/:qaId/merge', rest);
83
+ if (mergeMatch !== null && method === 'POST') {
84
+ return handleMergeQa(state, res, mergeMatch.id, mergeMatch.qaId);
85
+ }
86
+ const childMatch = matchPath('/projects/:id/qa/:qaId/child', rest);
87
+ if (childMatch !== null && method === 'POST') {
88
+ return handleChildQa(state, res, childMatch.id, childMatch.qaId);
89
+ }
90
+ const diagramMatch = matchPath('/projects/:id/diagrams/:diagramId', rest);
91
+ if (diagramMatch !== null && method === 'PATCH') {
92
+ return handlePatchDiagram(state, req, res, diagramMatch.id, diagramMatch.diagramId);
93
+ }
94
+ if (diagramMatch !== null && method === 'DELETE') {
95
+ return handleDeleteDiagram(state, res, diagramMatch.id, diagramMatch.diagramId);
96
+ }
97
+ const runsMatch = matchPath('/projects/:id/runs', rest);
98
+ if (runsMatch !== null && method === 'GET') {
99
+ return handleRuns(state, res, runsMatch.id);
100
+ }
101
+ const kindMatch = matchPath('/projects/:id/diagram/:kind', rest);
102
+ if (kindMatch !== null && method === 'POST') {
103
+ return handleRegenerate(state, res, kindMatch.id, kindMatch.kind);
104
+ }
105
+ const oneMatch = matchPath('/projects/:id', rest);
106
+ if (oneMatch !== null && method === 'GET') {
107
+ return handleGet(state, res, oneMatch.id);
108
+ }
109
+ if (oneMatch !== null && method === 'PATCH') {
110
+ return handlePatch(state, req, res, oneMatch.id);
111
+ }
112
+ if (oneMatch !== null && method === 'DELETE') {
113
+ return handleDelete(state, res, oneMatch.id);
114
+ }
115
+ // 带上方法和路径:插件路由前缀下的 404 只有两种来源——页面拼错了 URL,
116
+ // 或者宿主根本没把这条路由交过来。没有 method/path 就只能靠猜。
117
+ sendError(res, 404, 'not-found', `未知路由:${method} ${rest}`);
118
+ }
119
+ // ------------------------------------------------------------------ 处理器
120
+ function handleHealth(state, res) {
121
+ void state.skillDir().then((dir) => {
122
+ const value = {
123
+ name: 'dsh-archify-projects',
124
+ version: state.version,
125
+ workspacePath: workspaceRoot(state.config),
126
+ hasGit: true,
127
+ hasSessionService: state.hasSessionService,
128
+ presetId: state.config.presetId,
129
+ archifySkillPath: dir,
130
+ };
131
+ sendOk(res, value);
132
+ });
133
+ }
134
+ async function handleList(state, res) {
135
+ const projects = await state.store.list();
136
+ const value = {
137
+ projects,
138
+ workspacePath: workspaceRoot(state.config),
139
+ pricing: state.config.pricing,
140
+ };
141
+ sendOk(res, value);
142
+ }
143
+ async function handleCreate(state, req, res) {
144
+ const body = (await readJsonObject(req));
145
+ const repoPath = requireString(body.repoPath, '代码库路径');
146
+ const resolved = path.resolve(repoPath);
147
+ if (!(await isDirectory(resolved))) {
148
+ throw new BadRequest('路径不存在或不是目录', 'path-invalid');
149
+ }
150
+ const now = new Date().toISOString();
151
+ const git = await probeGit(resolved, now);
152
+ const name = optionalString(body.name) ?? defaultProjectName(resolved);
153
+ const project = createProject({ id: newProjectId(), name, repoPath: resolved, now, git });
154
+ await state.store.upsert(project);
155
+ const started = await startRun(state, project, 'create', ['structure', 'domain']);
156
+ const value = { project: started };
157
+ sendOk(res, value);
158
+ }
159
+ async function handleGet(state, res, id) {
160
+ const project = await state.store.get(id);
161
+ if (project === undefined) {
162
+ sendError(res, 404, 'not-found', '项目不存在');
163
+ return;
164
+ }
165
+ sendOk(res, { project });
166
+ }
167
+ async function handlePatch(state, req, res, id) {
168
+ const existing = await state.store.get(id);
169
+ if (existing === undefined) {
170
+ sendError(res, 404, 'not-found', '项目不存在');
171
+ return;
172
+ }
173
+ const body = (await readJsonObject(req));
174
+ const name = optionalString(body.name);
175
+ const repoPathRaw = optionalString(body.repoPath);
176
+ let nextRepoPath = null;
177
+ if (repoPathRaw !== null) {
178
+ nextRepoPath = path.resolve(repoPathRaw);
179
+ if (!(await isDirectory(nextRepoPath))) {
180
+ throw new BadRequest('路径不存在或不是目录', 'path-invalid');
181
+ }
182
+ }
183
+ const now = new Date().toISOString();
184
+ const git = nextRepoPath !== null && nextRepoPath !== existing.repoPath
185
+ ? await probeGit(nextRepoPath, now)
186
+ : existing.git;
187
+ const updated = await state.store.patch(id, (current) => ({
188
+ ...current,
189
+ name: name ?? (nextRepoPath !== null ? defaultProjectName(nextRepoPath) : current.name),
190
+ repoPath: nextRepoPath ?? current.repoPath,
191
+ git,
192
+ updatedAt: now,
193
+ }));
194
+ sendOk(res, { project: updated ?? existing });
195
+ }
196
+ async function handleDelete(state, res, id) {
197
+ const project = await state.store.get(id);
198
+ if (project === undefined) {
199
+ sendError(res, 404, 'not-found', '项目不存在');
200
+ return;
201
+ }
202
+ await state.store.remove(id);
203
+ // 只删插件自己的工作目录,绝不碰用户的代码库
204
+ await rm(projectDir(state.config, id), { recursive: true, force: true });
205
+ sendOk(res, { removed: true });
206
+ }
207
+ async function handleCheck(state, res, id) {
208
+ const project = await state.store.get(id);
209
+ if (project === undefined) {
210
+ sendError(res, 404, 'not-found', '项目不存在');
211
+ return;
212
+ }
213
+ const now = new Date().toISOString();
214
+ const git = await probeGit(project.repoPath, now);
215
+ const updated = await state.store.patch(id, (current) => ({ ...current, git, updatedAt: now }));
216
+ const value = {
217
+ project: updated ?? project,
218
+ drift: driftOf(project.git, git),
219
+ };
220
+ sendOk(res, value);
221
+ }
222
+ async function handleUpdate(state, res, id) {
223
+ const project = await state.store.get(id);
224
+ if (project === undefined) {
225
+ sendError(res, 404, 'not-found', '项目不存在');
226
+ return;
227
+ }
228
+ if (state.runner.isBusy(id)) {
229
+ sendError(res, 409, 'conflict', '这个项目已有任务在运行');
230
+ return;
231
+ }
232
+ const sync = await syncToLatest(project.repoPath, project.git.branch);
233
+ const now = new Date().toISOString();
234
+ const git = await probeGit(project.repoPath, now);
235
+ const synced = await state.store.patch(id, (current) => ({ ...current, git, updatedAt: now }));
236
+ const next = synced ?? project;
237
+ const ids = runnableDiagrams(next).map((diagram) => diagram.id);
238
+ const started = await startRun(state, next, 'update', ids.length > 0 ? ids : ['structure', 'domain']);
239
+ const value = {
240
+ project: started,
241
+ git: { skipped: sync.skipped, reason: sync.reason, from: sync.from, to: sync.to },
242
+ };
243
+ sendOk(res, value);
244
+ }
245
+ async function handleRuns(state, res, id) {
246
+ const project = await state.store.get(id);
247
+ if (project === undefined) {
248
+ sendError(res, 404, 'not-found', '项目不存在');
249
+ return;
250
+ }
251
+ sendOk(res, { project, runs: project.runs });
252
+ }
253
+ async function handleCreateDiagram(state, req, res, projectId) {
254
+ const project = await state.store.get(projectId);
255
+ if (project === undefined) {
256
+ sendError(res, 404, 'not-found', '项目不存在');
257
+ return;
258
+ }
259
+ const body = (await readJsonObject(req));
260
+ const title = requireString(body.title, '图标题');
261
+ const prompt = requireString(body.prompt, '出图要求');
262
+ const persistence = body.persistence === 'temporary' ? 'temporary' : 'permanent';
263
+ const parentId = optionalString(body.parentId ?? null);
264
+ if (parentId !== null && findDiagram(project, parentId) === undefined) {
265
+ throw new BadRequest('父图不存在', 'path-invalid');
266
+ }
267
+ const conceptIds = readStringArray(body.conceptIds);
268
+ const renderer = parseRenderer(body.renderer);
269
+ const pathScope = sanitizeScope(body.pathScope);
270
+ const now = new Date().toISOString();
271
+ const diagram = {
272
+ id: newDiagramId(),
273
+ kind: 'custom',
274
+ title,
275
+ prompt,
276
+ persistence,
277
+ parentId,
278
+ setId: optionalString(body.setId ?? null) ?? parentId,
279
+ conceptIds,
280
+ renderer,
281
+ pathScope,
282
+ status: 'idle',
283
+ htmlPath: null,
284
+ specPath: null,
285
+ generatedAt: null,
286
+ branch: null,
287
+ tree: null,
288
+ error: null,
289
+ };
290
+ const created = await state.store.patch(projectId, (current) => ({
291
+ ...current,
292
+ updatedAt: now,
293
+ custom: [...current.custom, diagram],
294
+ }));
295
+ const next = created ?? project;
296
+ const run = body.run === false ? next : await startRun(state, next, 'create', [diagram.id]);
297
+ const value = { project: run };
298
+ sendOk(res, value);
299
+ }
300
+ async function handlePatchDiagram(state, req, res, projectId, diagramId) {
301
+ const project = await state.store.get(projectId);
302
+ if (project === undefined) {
303
+ sendError(res, 404, 'not-found', '项目不存在');
304
+ return;
305
+ }
306
+ const diagram = findDiagram(project, diagramId);
307
+ if (diagram === undefined) {
308
+ sendError(res, 404, 'not-found', '图不存在');
309
+ return;
310
+ }
311
+ const body = (await readJsonObject(req));
312
+ const now = new Date().toISOString();
313
+ const updated = {
314
+ ...diagram,
315
+ title: optionalString(body.title) ?? diagram.title,
316
+ prompt: body.prompt === undefined ? diagram.prompt : optionalString(body.prompt),
317
+ persistence: body.persistence === 'temporary' || body.persistence === 'permanent'
318
+ ? body.persistence
319
+ : diagram.persistence,
320
+ conceptIds: body.conceptIds === undefined ? diagram.conceptIds : readStringArray(body.conceptIds),
321
+ };
322
+ const patched = await state.store.patch(projectId, (current) => ({
323
+ ...current,
324
+ updatedAt: now,
325
+ ...(diagram.kind === 'custom'
326
+ ? { custom: current.custom.map((d) => (d.id === diagram.id ? updated : d)) }
327
+ : { diagrams: { ...current.diagrams, [diagram.kind]: updated } }),
328
+ }));
329
+ sendOk(res, { project: patched ?? project });
330
+ }
331
+ async function handleDeleteDiagram(state, res, projectId, diagramId) {
332
+ const project = await state.store.get(projectId);
333
+ if (project === undefined) {
334
+ sendError(res, 404, 'not-found', '项目不存在');
335
+ return;
336
+ }
337
+ const diagram = findDiagram(project, diagramId);
338
+ if (diagram === undefined) {
339
+ sendError(res, 404, 'not-found', '图不存在');
340
+ return;
341
+ }
342
+ if (diagram.kind !== 'custom') {
343
+ throw new BadRequest('默认图不能删除,只能重画', 'forbidden');
344
+ }
345
+ const now = new Date().toISOString();
346
+ const removedIds = new Set([diagram.id]);
347
+ // 子图跟着父图一起删(现场可能还有更深一层,循环到不增长为止)
348
+ let grew = true;
349
+ while (grew) {
350
+ grew = false;
351
+ for (const candidate of project.custom) {
352
+ if (candidate.parentId !== null &&
353
+ removedIds.has(candidate.parentId) &&
354
+ !removedIds.has(candidate.id)) {
355
+ removedIds.add(candidate.id);
356
+ grew = true;
357
+ }
358
+ }
359
+ }
360
+ const patched = await state.store.patch(projectId, (current) => ({
361
+ ...current,
362
+ updatedAt: now,
363
+ custom: current.custom.filter((d) => !removedIds.has(d.id)),
364
+ qa: current.qa.filter((q) => !removedIds.has(q.diagramId)),
365
+ }));
366
+ sendOk(res, { project: patched ?? project });
367
+ }
368
+ async function handleRegenerateDiagram(state, res, projectId, diagramId) {
369
+ const project = await state.store.get(projectId);
370
+ if (project === undefined) {
371
+ sendError(res, 404, 'not-found', '项目不存在');
372
+ return;
373
+ }
374
+ if (findDiagram(project, diagramId) === undefined) {
375
+ sendError(res, 404, 'not-found', '图不存在');
376
+ return;
377
+ }
378
+ const updated = await startRun(state, project, 'regenerate', [diagramId]);
379
+ const value = {
380
+ project: updated,
381
+ run: {
382
+ id: updated.lastRun?.id ?? '',
383
+ kind: 'regenerate',
384
+ sessionId: updated.lastRun?.sessionId ?? null,
385
+ },
386
+ };
387
+ sendOk(res, value);
388
+ }
389
+ async function handleRegenerate(state, res, projectId, rawKind) {
390
+ const allowed = ['structure', 'domain'];
391
+ if (!allowed.includes(rawKind)) {
392
+ throw new BadRequest(`未知的图谱类型:${rawKind}`);
393
+ }
394
+ const project = await state.store.get(projectId);
395
+ if (project === undefined) {
396
+ sendError(res, 404, 'not-found', '项目不存在');
397
+ return;
398
+ }
399
+ const updated = await startRun(state, project, 'regenerate', [rawKind]);
400
+ const value = {
401
+ project: updated,
402
+ run: {
403
+ id: updated.lastRun?.id ?? '',
404
+ kind: 'regenerate',
405
+ sessionId: updated.lastRun?.sessionId ?? null,
406
+ },
407
+ };
408
+ sendOk(res, value);
409
+ }
410
+ async function handleAsk(state, req, res, projectId, diagramId) {
411
+ const project = await state.store.get(projectId);
412
+ if (project === undefined) {
413
+ sendError(res, 404, 'not-found', '项目不存在');
414
+ return;
415
+ }
416
+ const body = (await readJsonObject(req));
417
+ const question = requireString(body.question, '问题');
418
+ let result;
419
+ try {
420
+ result = await state.runner.ask(project, diagramId, question);
421
+ }
422
+ catch (err) {
423
+ if (err instanceof RunnerBusy)
424
+ throw new BadRequest('这个项目已有任务在运行', 'conflict');
425
+ if (err instanceof RunnerUnavailable)
426
+ throw new BadRequest(err.message, 'agent-unavailable');
427
+ throw err;
428
+ }
429
+ const latest = (await state.store.get(projectId)) ?? project;
430
+ const value = { sessionId: result.sessionId, entry: result.entry, project: latest };
431
+ sendOk(res, value);
432
+ }
433
+ async function handleMergeQa(state, res, projectId, qaId) {
434
+ const project = await state.store.get(projectId);
435
+ if (project === undefined) {
436
+ sendError(res, 404, 'not-found', '项目不存在');
437
+ return;
438
+ }
439
+ const qa = project.qa.find((entry) => entry.id === qaId);
440
+ if (qa === undefined) {
441
+ sendError(res, 404, 'not-found', '提问记录不存在');
442
+ return;
443
+ }
444
+ const diagram = findDiagram(project, qa.diagramId);
445
+ if (diagram === undefined) {
446
+ sendError(res, 404, 'not-found', '图不存在');
447
+ return;
448
+ }
449
+ const merged = {
450
+ ...diagram,
451
+ prompt: mergePrompt(diagram.prompt, qa.question, qa.answer),
452
+ };
453
+ const prepared = await state.store.patch(projectId, (current) => ({
454
+ ...current,
455
+ ...(diagram.kind === 'custom'
456
+ ? { custom: current.custom.map((d) => (d.id === diagram.id ? merged : d)) }
457
+ : { diagrams: { ...current.diagrams, [diagram.kind]: merged } }),
458
+ qa: current.qa.map((entry) => (entry.id === qaId ? { ...entry, applied: true } : entry)),
459
+ }));
460
+ const next = prepared ?? project;
461
+ const started = await startRun(state, next, 'regenerate', [diagram.id]);
462
+ const latest = (await state.store.get(projectId)) ?? started;
463
+ const entry = latest.qa.find((item) => item.id === qaId) ?? { ...qa, applied: true };
464
+ const value = { project: latest, qa: entry, diagramId: null };
465
+ sendOk(res, value);
466
+ }
467
+ async function handleChildQa(state, res, projectId, qaId) {
468
+ const project = await state.store.get(projectId);
469
+ if (project === undefined) {
470
+ sendError(res, 404, 'not-found', '项目不存在');
471
+ return;
472
+ }
473
+ const qa = project.qa.find((entry) => entry.id === qaId);
474
+ if (qa === undefined) {
475
+ sendError(res, 404, 'not-found', '提问记录不存在');
476
+ return;
477
+ }
478
+ const parent = findDiagram(project, qa.diagramId);
479
+ if (parent === undefined) {
480
+ sendError(res, 404, 'not-found', '图不存在');
481
+ return;
482
+ }
483
+ const now = new Date().toISOString();
484
+ const diagram = {
485
+ id: newDiagramId(),
486
+ kind: 'custom',
487
+ title: childTitle(qa.question),
488
+ prompt: `基于「${parent.title}」的这张子图,回答并画出:${qa.question}\n\n${qa.answer}`,
489
+ persistence: 'permanent',
490
+ parentId: parent.id,
491
+ setId: parent.setId ?? parent.id,
492
+ conceptIds: parent.conceptIds,
493
+ renderer: parent.renderer,
494
+ pathScope: parent.pathScope,
495
+ status: 'idle',
496
+ htmlPath: null,
497
+ specPath: null,
498
+ generatedAt: null,
499
+ branch: null,
500
+ tree: null,
501
+ error: null,
502
+ };
503
+ const created = await state.store.patch(projectId, (current) => ({
504
+ ...current,
505
+ updatedAt: now,
506
+ custom: [...current.custom, diagram],
507
+ }));
508
+ const next = created ?? project;
509
+ const started = await startRun(state, next, 'create', [diagram.id]);
510
+ const latest = (await state.store.get(projectId)) ?? started;
511
+ const entry = latest.qa.find((item) => item.id === qaId) ?? qa;
512
+ const value = { project: latest, qa: entry, diagramId: diagram.id };
513
+ sendOk(res, value);
514
+ }
515
+ async function handleArtifact(state, req, res, rawRelative) {
516
+ const relative = rawRelative
517
+ .split('/')
518
+ .map((part) => decodeURIComponent(part))
519
+ .join('/');
520
+ const root = workspaceRoot(state.config);
521
+ const target = confineToRoot(root, relative);
522
+ if (target === null) {
523
+ sendError(res, 403, 'forbidden', '路径越界');
524
+ return;
525
+ }
526
+ let info;
527
+ try {
528
+ info = await stat(target);
529
+ }
530
+ catch {
531
+ sendError(res, 404, 'not-found', '产物不存在');
532
+ return;
533
+ }
534
+ if (!info.isFile()) {
535
+ sendError(res, 404, 'not-found', '产物不存在');
536
+ return;
537
+ }
538
+ res.writeHead(200, {
539
+ 'Content-Type': contentTypeOf(target),
540
+ 'Content-Length': String(info.size),
541
+ // 产物是本地文件,允许缓存但要求重新验证
542
+ 'Cache-Control': 'no-cache',
543
+ });
544
+ if (methodOf(req) === 'HEAD') {
545
+ res.end();
546
+ return;
547
+ }
548
+ createReadStream(target).pipe(res);
549
+ }
550
+ // ------------------------------------------------------------------ 工具
551
+ async function startRun(state, project, kind, diagramIds) {
552
+ try {
553
+ await state.runner.start(project, kind, diagramIds);
554
+ }
555
+ catch (err) {
556
+ if (err instanceof RunnerBusy) {
557
+ throw new BadRequest('这个项目已有任务在运行', 'conflict');
558
+ }
559
+ if (err instanceof RunnerUnavailable) {
560
+ throw new BadRequest(err.message, 'agent-unavailable');
561
+ }
562
+ throw err;
563
+ }
564
+ return (await state.store.get(project.id)) ?? project;
565
+ }
566
+ /** 比较两次 Git 快照,产出页面用的人话漂移说明(分支 + 工作区)。 */
567
+ export function driftOf(previous, next) {
568
+ const branchChanged = previous.isRepo &&
569
+ next.isRepo &&
570
+ previous.branch !== null &&
571
+ next.branch !== null &&
572
+ previous.branch !== next.branch;
573
+ const treeChanged = treeKey(previous) !== treeKey(next);
574
+ const dirty = next.dirty === true;
575
+ const behind = next.behind ?? 0;
576
+ const notes = [];
577
+ if (branchChanged)
578
+ notes.push(`分支从 ${previous.branch ?? '?'} 变成 ${next.branch ?? '?'}`);
579
+ if (treeChanged)
580
+ notes.push('工作区有改动');
581
+ if (behind > 0)
582
+ notes.push(`落后上游 ${behind} 个提交`);
583
+ if (dirty && !treeChanged)
584
+ notes.push('工作区有未提交改动');
585
+ return {
586
+ branchChanged,
587
+ treeChanged,
588
+ dirty,
589
+ behind,
590
+ summary: notes.length === 0 ? null : notes.join(';'),
591
+ };
592
+ }
593
+ function treeKey(git) {
594
+ if (git.tree === null)
595
+ return git.dirty === true ? 'dirty' : 'clean';
596
+ return `${git.tree.staged}:${git.tree.unstaged}:${git.tree.untracked}`;
597
+ }
598
+ function mergePrompt(prompt, question, answer) {
599
+ const base = prompt ?? '按这张图原有的口径重画';
600
+ return `${base}\n\n## 用户追问补充(本次必须体现)\n\n问题:${question}\n\n回答里的细节:${answer}`;
601
+ }
602
+ function childTitle(question) {
603
+ const trimmed = question.replace(/\s+/g, ' ').trim();
604
+ return trimmed.length > 24 ? `子图:${trimmed.slice(0, 24)}…` : `子图:${trimmed}`;
605
+ }
606
+ function requireString(value, field) {
607
+ if (typeof value !== 'string' || value.trim().length === 0) {
608
+ throw new BadRequest(`${field}不能为空`);
609
+ }
610
+ return value.trim();
611
+ }
612
+ function optionalString(value) {
613
+ if (typeof value !== 'string')
614
+ return null;
615
+ const trimmed = value.trim();
616
+ return trimmed.length === 0 ? null : trimmed;
617
+ }
618
+ function parseRenderer(value) {
619
+ return typeof value === 'string' && ARCHIFY_TYPES.includes(value)
620
+ ? value
621
+ : null;
622
+ }
623
+ /** 目录范围只接受仓库内的相对路径:拒绝绝对路径与 `..`。 */
624
+ function sanitizeScope(value) {
625
+ if (!Array.isArray(value))
626
+ return [];
627
+ const out = [];
628
+ for (const item of value) {
629
+ if (typeof item !== 'string')
630
+ continue;
631
+ const trimmed = item.trim().replace(/^\.\//, '').replace(/\/+$/, '');
632
+ if (trimmed.length === 0 || trimmed.startsWith('/') || trimmed.includes('\\'))
633
+ continue;
634
+ const parts = trimmed.split('/');
635
+ if (parts.some((part) => part === '..' || part === '.' || part.length === 0))
636
+ continue;
637
+ if (!out.includes(trimmed))
638
+ out.push(trimmed);
639
+ }
640
+ return out;
641
+ }
642
+ function readStringArray(value) {
643
+ return Array.isArray(value)
644
+ ? value.filter((item) => typeof item === 'string')
645
+ : [];
646
+ }
647
+ function contentTypeOf(target) {
648
+ const ext = path.extname(target).toLowerCase();
649
+ if (ext === '.html')
650
+ return 'text/html; charset=utf-8';
651
+ if (ext === '.json')
652
+ return 'application/json; charset=utf-8';
653
+ if (ext === '.css')
654
+ return 'text/css; charset=utf-8';
655
+ if (ext === '.js')
656
+ return 'text/javascript; charset=utf-8';
657
+ if (ext === '.svg')
658
+ return 'image/svg+xml';
659
+ if (ext === '.png')
660
+ return 'image/png';
661
+ if (ext === '.webp')
662
+ return 'image/webp';
663
+ return 'application/octet-stream';
664
+ }
665
+ // 供 index.ts 记录「有哪些图需要跑」
666
+ export { allDiagrams };
667
+ //# sourceMappingURL=routes.js.map