@marsken/dsh-studio 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.
Files changed (42) hide show
  1. package/AGENTS.md +148 -0
  2. package/LICENSE +202 -0
  3. package/README.md +1367 -3
  4. package/client.js +952 -0
  5. package/client.src.js +2466 -0
  6. package/cordis.patch.yml +9 -0
  7. package/dist/client.js +952 -0
  8. package/docs/compatibility.md +101 -0
  9. package/docs/images/board.png +0 -0
  10. package/docs/images/cases.png +0 -0
  11. package/docs/images/quality.png +0 -0
  12. package/docs/publish.md +123 -0
  13. package/docs/quickstart.md +53 -0
  14. package/icon.svg +6 -0
  15. package/index.js +2674 -0
  16. package/locale/en.json +11 -0
  17. package/locale/zh.json +11 -0
  18. package/package.json +64 -4
  19. package/scripts/smoke-store.mjs +76 -0
  20. package/src/host/cases.js +135 -0
  21. package/src/host/design.js +87 -0
  22. package/src/host/dispatcher.js +936 -0
  23. package/src/host/domain.js +1966 -0
  24. package/src/host/executor.js +1396 -0
  25. package/src/host/gitlab-facts.js +22 -0
  26. package/src/host/impact.js +32 -0
  27. package/src/host/integrations.js +587 -0
  28. package/src/host/quality.js +106 -0
  29. package/src/host/routes/admin.js +100 -0
  30. package/src/host/routes/agents.js +71 -0
  31. package/src/host/routes/auth.js +155 -0
  32. package/src/host/routes/coding.js +203 -0
  33. package/src/host/routes/connect.js +166 -0
  34. package/src/host/routes/exports.js +122 -0
  35. package/src/host/routes/insights.js +97 -0
  36. package/src/host/routes/releases.js +335 -0
  37. package/src/host/routes/requirements.js +430 -0
  38. package/src/host/routes/skills.js +75 -0
  39. package/src/host/routes/tasks.js +573 -0
  40. package/src/host/store.js +522 -0
  41. package/src/host/team-memory.js +46 -0
  42. package/src/host/tools.js +1211 -0
@@ -0,0 +1,430 @@
1
+ import {
2
+ INTAKE_HTML,
3
+ MINE_HTML,
4
+ REQ_STATUS_LABEL,
5
+ STATUS_HTML,
6
+ cleanId,
7
+ sanitizeRequirementPool,
8
+ text,
9
+ } from '../domain.js'
10
+ import {
11
+ ensureReqFolder,
12
+ mutateReqDoc,
13
+ pullFeishuDoc,
14
+ requirementGate,
15
+ requirementMatrix,
16
+ requirementProgress,
17
+ requirementRollup,
18
+ saveRequirement,
19
+ syncRequirementArtifacts,
20
+ taskExitGate,
21
+ } from '../executor.js'
22
+ import { randomBytes } from 'node:crypto'
23
+ const INJECTED = ["createTask","publicState","readJsonBody","sendJson"]
24
+ let host = {
25
+ createTask: () => {},
26
+ publicState: () => {},
27
+ readJsonBody: () => {},
28
+ sendJson: () => {},
29
+ }
30
+ /** 需求(含交付物、文档、飞书同步)相关路由。宿主能力由 configureRequirementRoutes() 注入。 */
31
+ export function configureRequirementRoutes(services) {
32
+ host = { ...host, ...(services ?? {}) }
33
+ }
34
+ export async function handleRequirementRoute({ method, route, req, res, store, url }) {
35
+ if (method === 'GET' && route === '/requirements') {
36
+ const state = store.snapshot()
37
+ const requirements = state.requirements.map(item => {
38
+ const progress = requirementProgress(state, item)
39
+ return {
40
+ ...item,
41
+ counts: {
42
+ tasks: state.tasks.filter(task => task.requirementId === item.id).length,
43
+ done: state.tasks.filter(task => task.requirementId === item.id && task.status === 'done').length,
44
+ bugs: state.tasks.filter(task => task.requirementId === item.id && task.type === 'bug' && task.status !== 'done').length,
45
+ },
46
+ progress,
47
+ }
48
+ })
49
+ requirements.sort((a, b) => (Number(a.order) || 0) - (Number(b.order) || 0) || String(a.code).localeCompare(String(b.code)))
50
+ host.sendJson(res, 200, { ok: true, requirements })
51
+ return true
52
+ }
53
+ if (method === 'POST' && route === '/requirement/get') {
54
+ const body = await host.readJsonBody(req)
55
+ const rollup = requirementRollup(store.snapshot(), String(body.id ?? ''))
56
+ if (rollup === undefined) throw new Error('需求不存在')
57
+ const matrix = requirementMatrix(store.snapshot(), rollup.requirement)
58
+ const progress = requirementProgress(store.snapshot(), rollup.requirement)
59
+ host.sendJson(res, 200, { ok: true, ...rollup, matrix, progress })
60
+ return true
61
+ }
62
+ if (method === 'POST' && route === '/requirement/matrix') {
63
+ const body = await host.readJsonBody(req)
64
+ const requirement = store.snapshot().requirements.find(item => item.id === String(body.id ?? ''))
65
+ if (requirement === undefined) throw new Error('需求不存在')
66
+ host.sendJson(res, 200, { ok: true, requirement: { id: requirement.id, code: requirement.code, title: requirement.title }, matrix: requirementMatrix(store.snapshot(), requirement) })
67
+ return true
68
+ }
69
+ if (method === 'POST' && route === '/requirement/signoff') {
70
+ const body = await host.readJsonBody(req)
71
+ const id = String(body.id ?? '')
72
+ const action = body.action === 'clear' ? 'clear' : 'sign'
73
+ const note = String(body.note ?? '').trim().slice(0, 600)
74
+ const by = String(body.by ?? req.headers['x-studio-user'] ?? '').trim().slice(0, 60) || '人'
75
+ if (action === 'sign' && note === '') throw new Error('需求验收签字要写一句结论:交付了什么、遗留什么')
76
+ const snapshot = await store.mutate(state => {
77
+ const target = state.requirements.find(item => item.id === id)
78
+ if (target === undefined) throw new Error('需求不存在')
79
+ if (action === 'sign') {
80
+ target.signoff = { by, at: Date.now(), note }
81
+ const open = state.tasks.filter(task => task.requirementId === id && task.status !== 'done').length
82
+ const total = state.tasks.filter(task => task.requirementId === id).length
83
+ // 签完字且卡全完成,需求自动落到「已交付」;否则留在进行中,等卡收口。
84
+ if (open === 0 && total > 0) target.status = 'delivered'
85
+ } else {
86
+ target.signoff = null
87
+ if (target.status === 'delivered') target.status = 'active'
88
+ }
89
+ target.updatedAt = Date.now()
90
+ })
91
+ const requirement = snapshot.requirements.find(item => item.id === id)
92
+ host.sendJson(res, 200, {
93
+ ok: true,
94
+ requirement,
95
+ progress: requirementProgress(snapshot, requirement),
96
+ signoff: requirement.signoff ?? null,
97
+ state: host.publicState(store),
98
+ })
99
+ return true
100
+ }
101
+ if (method === 'POST' && route === '/task/gate') {
102
+ const body = await host.readJsonBody(req)
103
+ const task = store.snapshot().tasks.find(entry => entry.id === String(body.id ?? ''))
104
+ if (task === undefined) throw new Error('任务不存在')
105
+ host.sendJson(res, 200, { ok: true, gate: taskExitGate(store.snapshot(), task), signoff: task.signoff ?? null })
106
+ return true
107
+ }
108
+ if (method === 'POST' && route === '/task/signoff') {
109
+ const body = await host.readJsonBody(req)
110
+ const id = String(body.id ?? '')
111
+ const action = String(body.action ?? 'sign')
112
+ const by = text(body.by, 40) ?? '人'
113
+ const note = text(body.note, 600) ?? ''
114
+ const task = await store.mutate(state => {
115
+ const entry = state.tasks.find(item => item.id === id)
116
+ if (entry === undefined) throw new Error('任务不存在')
117
+ if (action === 'clear') {
118
+ entry.signoff = null
119
+ entry.history.push({ at: Date.now(), by, status: entry.status, note: 'signoff-cleared' })
120
+ } else {
121
+ if (note.length === 0) throw new Error('验收签字要写一句结论:验了什么、遗留什么')
122
+ entry.signoff = { by, at: Date.now(), note }
123
+ entry.history.push({ at: Date.now(), by, status: entry.status, note: `signoff:${note.slice(0, 80)}` })
124
+ }
125
+ entry.updatedAt = Date.now()
126
+ })
127
+ void task
128
+ const signed = store.snapshot().tasks.find(item => item.id === id)
129
+ host.sendJson(res, 200, { ok: true, task: signed, gate: taskExitGate(store.snapshot(), signed), state: host.publicState(store) })
130
+ return true
131
+ }
132
+ if (method === 'POST' && route === '/requirement/save') {
133
+ const body = await host.readJsonBody(req)
134
+ const incoming = body.requirement ?? body
135
+ const beforeRecord = typeof incoming.id === 'string' ? store.snapshot().requirements.find(item => item.id === incoming.id) : undefined
136
+ const watch = ['title', 'description', 'priority', 'status', 'owner', 'project', 'iteration']
137
+ const before = beforeRecord === undefined ? null : Object.fromEntries(watch.map(key => [key, String(beforeRecord[key] ?? '')]))
138
+ const requirement = await saveRequirement(store, incoming)
139
+ if (before !== null) {
140
+ const diffs = watch
141
+ .map(key => ({ field: key, from: before[key], to: String(requirement[key] ?? '') }))
142
+ .filter(item => item.from !== item.to)
143
+ if (diffs.length > 0) {
144
+ const by = String(incoming.by ?? incoming.operator ?? '人').slice(0, 40)
145
+ await store.mutate(state => {
146
+ const target = state.requirements.find(item => item.id === requirement.id)
147
+ if (target === undefined) return
148
+ const clipped = value => value.length > 300 ? `${value.slice(0, 300)}…` : value
149
+ target.history = [...(target.history ?? []), ...diffs.map(item => ({ at: Date.now(), by, field: item.field, from: clipped(item.from), to: clipped(item.to) }))].slice(-100)
150
+ })
151
+ }
152
+ }
153
+ const snapshot = store.snapshot()
154
+ const saved = snapshot.requirements.find(item => item.id === requirement.id) ?? requirement
155
+ host.sendJson(res, 200, { ok: true, requirement, progress: requirementProgress(snapshot, saved), state: host.publicState(store) })
156
+ return true
157
+ }
158
+ if (method === 'POST' && route === '/requirement/review') {
159
+ const body = await host.readJsonBody(req)
160
+ const id = String(body.id ?? '')
161
+ const decision = String(body.decision ?? 'approve')
162
+ if (!['submit', 'approve', 'reject'].includes(decision)) throw new Error('未知的评审动作:submit / approve / reject')
163
+ const by = String(body.by ?? '').trim().slice(0, 60) || '人'
164
+ const note = String(body.note ?? '').slice(0, 600)
165
+ if (decision === 'reject' && note.trim() === '') throw new Error('打回要说清楚哪里不行,写一句评审意见')
166
+ let saved
167
+ await store.mutate(state => {
168
+ const found = state.requirements.find(item => item.id === id)
169
+ if (found === undefined) throw new Error(`no requirement ${id}`)
170
+ const now = Date.now()
171
+ if (decision === 'submit') {
172
+ found.status = 'review'
173
+ found.review = { status: 'pending', by, at: now, note }
174
+ } else if (decision === 'approve') {
175
+ found.status = found.status === 'closed' ? 'closed' : 'active'
176
+ found.review = { status: 'approved', by, at: now, note }
177
+ } else {
178
+ found.status = 'draft'
179
+ found.review = { status: 'changes_requested', by, at: now, note }
180
+ }
181
+ found.updatedAt = now
182
+ saved = found
183
+ })
184
+ const snapshot = store.snapshot()
185
+ const requirement = snapshot.requirements.find(item => item.id === id) ?? saved
186
+ host.sendJson(res, 200, { ok: true, requirement, review: requirement.review ?? null, progress: requirementProgress(snapshot, requirement), state: host.publicState(store) })
187
+ return true
188
+ }
189
+ if (method === 'POST' && route === '/requirement/split') {
190
+ const body = await host.readJsonBody(req)
191
+ const id = String(body.id ?? '')
192
+ const requested = (Array.isArray(body.pointIds) ? body.pointIds : []).map(item => String(item))
193
+ const column = String(body.status ?? 'clarify')
194
+ const before = store.snapshot()
195
+ const requirement = before.requirements.find(item => item.id === id)
196
+ if (requirement === undefined) throw new Error(`no requirement ${id}`)
197
+ const approved = (requirement.review ?? null)?.status === 'approved'
198
+ const started = ['active', 'delivered', 'closed'].includes(requirement.status)
199
+ if (!approved && !started) throw new Error('需求还没评审通过,先过评审再拆卡')
200
+ const points = (requirement.points ?? []).filter(point => requested.length === 0 || requested.includes(point.id))
201
+ const uncovered = points.filter(point => !before.tasks.some(task => task.requirementId === id && task.pointId === point.id))
202
+ if (uncovered.length === 0) throw new Error('这些功能点都已经有卡了')
203
+ const created = []
204
+ for (const point of uncovered) {
205
+ const task = await host.createTask(store, {
206
+ title: `${point.id} ${point.title}`.slice(0, 200),
207
+ type: 'requirement',
208
+ requirementId: id,
209
+ pointId: point.id,
210
+ project: requirement.project,
211
+ iteration: requirement.iteration,
212
+ priority: requirement.priority,
213
+ status: column,
214
+ requirement: point.description ?? point.title,
215
+ })
216
+ created.push(task)
217
+ }
218
+ host.sendJson(res, 200, { ok: true, created, state: host.publicState(store) })
219
+ return true
220
+ }
221
+ if (method === 'POST' && route === '/requirement/pool') {
222
+ const body = await host.readJsonBody(req)
223
+ const id = cleanId(body.id, 'req')
224
+ const pool = sanitizeRequirementPool(body.pool ?? body)
225
+ let next = null
226
+ await store.mutate(state => {
227
+ const target = state.requirements.find(item => item.id === id)
228
+ if (target !== undefined) { target.pool = pool; target.updatedAt = Date.now(); next = target }
229
+ })
230
+ if (next === null) throw new Error('没有这条需求')
231
+ host.sendJson(res, 200, { ok: true, requirement: next, gate: requirementGate(store.snapshot(), next), state: host.publicState(store) })
232
+ return true
233
+ }
234
+
235
+ if (method === 'GET' && route === '/intake') {
236
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
237
+ res.end(INTAKE_HTML)
238
+ return true
239
+ }
240
+ if (method === 'POST' && route === '/intake/requirement') {
241
+ const body = await host.readJsonBody(req)
242
+ const title = String(body.title ?? '').trim().slice(0, 200)
243
+ const description = String(body.description ?? '').trim().slice(0, 20000)
244
+ const requester = String(body.requester ?? '').trim().slice(0, 40)
245
+ if (title.length === 0) throw new Error('请填写需求标题')
246
+ if (requester.length === 0) throw new Error('请填写您的姓名')
247
+ if (description.length < 10) throw new Error('请把需求描述写得具体一些(至少 10 个字)')
248
+ const contactKey = String(body.contact ?? '').trim()
249
+ if (contactKey.length > 0) {
250
+ const since = Date.now() - 10 * 60 * 1000
251
+ const recent = (store.snapshot().requirements ?? []).filter(item => item.source === 'business'
252
+ && String(item.requester?.contact ?? '').trim() === contactKey
253
+ && Number(item.requester?.at ?? 0) > since)
254
+ if (recent.length >= 3) throw new Error('同一联系方式 10 分钟内最多提交 3 条,请稍后再试')
255
+ }
256
+ const requirement = await saveRequirement(store, {
257
+ title,
258
+ description,
259
+ status: 'draft',
260
+ priority: String(body.priority ?? 'normal'),
261
+ project: String(body.project ?? ''),
262
+ iteration: String(body.iteration ?? ''),
263
+ links: { prd: String(body.links?.prd ?? body.prd ?? ''), proto: String(body.links?.proto ?? body.proto ?? ''), acceptance: String(body.links?.acceptance ?? body.acceptance ?? '') },
264
+ source: 'business',
265
+ requester: { name: requester, dept: String(body.dept ?? '').slice(0, 40), contact: String(body.contact ?? '').slice(0, 80), at: Date.now() },
266
+ })
267
+ host.sendJson(res, 200, { ok: true, code: requirement.code, id: requirement.id })
268
+ return true
269
+ }
270
+
271
+ if (method === 'GET' && route === '/intake/mine') {
272
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
273
+ res.end(MINE_HTML)
274
+ return true
275
+ }
276
+ if (method === 'POST' && route === '/intake/mine/query') {
277
+ const body = await host.readJsonBody(req)
278
+ const contact = String(body.contact ?? '').trim()
279
+ if (contact.length < 4) throw new Error('请填写提交时留的联系方式(至少 4 位)')
280
+ const rows = (store.snapshot().requirements ?? [])
281
+ .filter(item => item.source === 'business' && String(item.requester?.contact ?? '').trim() === contact)
282
+ .sort((left, right) => (Number(right.requester?.at ?? 0) - Number(left.requester?.at ?? 0)) || (Number(right.updatedAt ?? 0) - Number(left.updatedAt ?? 0)))
283
+ .slice(0, 50)
284
+ .map(item => {
285
+ const cards = (store.snapshot().tasks ?? []).filter(task => task.requirementId === item.id)
286
+ return {
287
+ code: item.code,
288
+ title: item.title,
289
+ status: item.status,
290
+ statusLabel: REQ_STATUS_LABEL[item.status] ?? item.status,
291
+ updatedAt: item.updatedAt,
292
+ openQuestions: (item.questions ?? []).filter(question => question.status === 'open').length,
293
+ cards: cards.length,
294
+ doneCards: cards.filter(task => task.status === 'done').length,
295
+ }
296
+ })
297
+ host.sendJson(res, 200, { ok: true, rows })
298
+ return true
299
+ }
300
+ if (method === 'GET' && route === '/intake/status') {
301
+ res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
302
+ res.end(STATUS_HTML)
303
+ return true
304
+ }
305
+ if (method === 'POST' && route === '/intake/query') {
306
+ const body = await host.readJsonBody(req)
307
+ const code = String(body.code ?? '').trim().toUpperCase()
308
+ const tail = String(body.contact ?? '').trim().replace(/\s+/g, '')
309
+ if (code.length === 0 || tail.length < 4) throw new Error('请填写需求编号,以及联系方式的后四位')
310
+ const hit = (store.snapshot().requirements ?? []).find(item => String(item.code ?? '').toUpperCase() === code)
311
+ const contact = String(hit?.requester?.contact ?? '').replace(/\s+/g, '')
312
+ if (hit === undefined || contact.length === 0 || !contact.endsWith(tail)) throw new Error('没查到:编号或联系方式对不上')
313
+ const cards = (store.snapshot().tasks ?? []).filter(item => item.requirementId === hit.id)
314
+ const open = (hit.questions ?? []).filter(item => item.status === 'open').length
315
+ host.sendJson(res, 200, {
316
+ ok: true,
317
+ code: hit.code,
318
+ title: hit.title,
319
+ status: hit.status,
320
+ statusLabel: REQ_STATUS_LABEL[hit.status] ?? hit.status,
321
+ reviewStatus: hit.review?.status ?? '',
322
+ updatedAt: hit.updatedAt,
323
+ openQuestions: open,
324
+ cards: cards.length,
325
+ doneCards: cards.filter(item => item.status === 'done').length,
326
+ })
327
+ return true
328
+ }
329
+
330
+ if (method === 'POST' && route === '/requirement/request-prd') {
331
+ const body = await host.readJsonBody(req)
332
+ const id = String(body.id ?? '').trim()
333
+ const by = String(body.by ?? body.operator ?? '').trim() || '人'
334
+ if (id.length === 0) throw new Error('缺少需求 id')
335
+ const now = Date.now()
336
+ await store.mutate(state => {
337
+ const item = state.requirements.find(entry => entry.id === id)
338
+ if (item === undefined) throw new Error('没有这条需求')
339
+ item.review = { status: 'pending', by, at: now, note: String(body.note ?? '').trim().slice(0, 600) || '业务提单:等产品补 PRD' }
340
+ if (item.status === 'draft') item.status = 'review'
341
+ item.history = [...(item.history ?? []), { at: now, by, field: 'review', from: '', to: '申请产品补 PRD(待评审)' }].slice(-100)
342
+ item.updatedAt = now
343
+ })
344
+ const fresh = store.snapshot()
345
+ const requirement = (fresh.requirements ?? []).find(entry => entry.id === id)
346
+ host.sendJson(res, 200, { ok: true, requirement, progress: requirementProgress(fresh, requirement), state: host.publicState(store) })
347
+ return true
348
+ }
349
+
350
+ if (method === 'POST' && route === '/requirement/questions') {
351
+ const body = await host.readJsonBody(req)
352
+ const id = cleanId(body.id, 'req')
353
+ const action = String(body.action ?? 'add')
354
+ const requirement = store.snapshot().requirements.find(item => item.id === id)
355
+ if (requirement === undefined) throw new Error('没有这条需求')
356
+ let questions = Array.isArray(requirement.questions) ? requirement.questions.slice() : []
357
+ const now = Date.now()
358
+ if (action === 'add') {
359
+ const text = String(body.text ?? '').trim().slice(0, 300)
360
+ if (text.length === 0) throw new Error('问题不能为空')
361
+ questions.push({ id: `q-${randomBytes(3).toString('hex')}`, text, status: 'open', owner: String(body.owner ?? '').trim().slice(0, 40), answer: '', at: now, answeredAt: 0 })
362
+ } else if (action === 'answer') {
363
+ const target = questions.find(item => item.id === String(body.questionId ?? ''))
364
+ if (target === undefined) throw new Error('没有这个问题')
365
+ const answer = String(body.answer ?? '').trim().slice(0, 1000)
366
+ if (answer.length === 0) throw new Error('回答不能为空')
367
+ target.answer = answer
368
+ target.status = String(body.status ?? 'answered') === 'open' ? 'open' : 'answered'
369
+ target.answeredAt = target.status === 'answered' ? now : 0
370
+ if (body.by !== undefined) target.answeredBy = String(body.by).slice(0, 40)
371
+ } else if (action === 'delete') {
372
+ questions = questions.filter(item => item.id !== String(body.questionId ?? ''))
373
+ } else {
374
+ throw new Error(`unknown action ${action}`)
375
+ }
376
+ await store.mutate(state => {
377
+ const target = state.requirements.find(item => item.id === id)
378
+ if (target !== undefined) { target.questions = questions; target.updatedAt = now }
379
+ })
380
+ const next = store.snapshot().requirements.find(item => item.id === id)
381
+ host.sendJson(res, 200, { ok: true, requirement: next, gate: requirementGate(store.snapshot(), next), state: host.publicState(store) })
382
+ return true
383
+ }
384
+
385
+ if (method === 'POST' && route === '/requirement/delete') {
386
+ const body = await host.readJsonBody(req)
387
+ const id = String(body.id ?? '')
388
+ await store.mutate(state => {
389
+ state.requirements = state.requirements.filter(item => item.id !== id)
390
+ // 卡不删,只是脱离需求;人可以看到它们变成「未归属」。
391
+ for (const task of state.tasks) {
392
+ if (task.requirementId === id) {
393
+ task.requirementId = ''
394
+ task.code = ''
395
+ task.updatedAt = Date.now()
396
+ }
397
+ }
398
+ })
399
+ host.sendJson(res, 200, { ok: true, state: host.publicState(store) })
400
+ return true
401
+ }
402
+ if (method === 'POST' && route === '/requirement/doc') {
403
+ const body = await host.readJsonBody(req)
404
+ const result = await mutateReqDoc(store, body)
405
+ host.sendJson(res, 200, { ok: true, requirement: result, state: host.publicState(store) })
406
+ return true
407
+ }
408
+ if (method === 'POST' && route === '/requirement/pull') {
409
+ const body = await host.readJsonBody(req)
410
+ const result = await pullFeishuDoc(body.url ?? body.docUrl ?? '')
411
+ host.sendJson(res, 200, { ok: true, ...result })
412
+ return true
413
+ }
414
+ if (method === 'POST' && route === '/requirement/folder') {
415
+ const body = await host.readJsonBody(req)
416
+ const id = String(body.id ?? '')
417
+ const folder = await ensureReqFolder(store, id)
418
+ const requirement = store.snapshot().requirements.find(item => item.id === id)
419
+ host.sendJson(res, 200, { ok: String(folder.token ?? '').length > 0, folder, requirement, error: folder.error ?? '', state: host.publicState(store) })
420
+ return true
421
+ }
422
+ if (method === 'POST' && route === '/requirement/sync') {
423
+ const body = await host.readJsonBody(req)
424
+ const id = String(body.id ?? '')
425
+ const report = await syncRequirementArtifacts(store, id)
426
+ host.sendJson(res, 200, { ok: report.failed === 0, report, state: host.publicState(store) })
427
+ return true
428
+ }
429
+ return false
430
+ }
@@ -0,0 +1,75 @@
1
+ const INJECTED = ["isSkillName","readJsonBody","sendJson","skillFromMarkdown","skillFromZip","skillsRoot","getSkills"]
2
+ let host = {
3
+ isSkillName: () => {},
4
+ readJsonBody: () => {},
5
+ sendJson: () => {},
6
+ skillFromMarkdown: () => {},
7
+ skillFromZip: () => {},
8
+ skillsRoot: () => {},
9
+ getSkills: () => undefined,
10
+ }
11
+ /** 技能库的读写与上传路由。宿主能力由 configureSkillsRoutes() 注入。 */
12
+ export function configureSkillsRoutes(services) {
13
+ host = { ...host, ...(services ?? {}) }
14
+ }
15
+
16
+ export async function handleSkillsRoute({ method, route, req, res, store, url }) {
17
+ if (route.startsWith('/skills')) {
18
+ if (host.getSkills() === undefined) throw new Error('技能库尚未就绪')
19
+ if (method === 'GET' && route === '/skills') {
20
+ host.sendJson(res, 200, { ok: true, root: host.skillsRoot(), skills: host.getSkills().list(), onDisk: await host.getSkills().visibleRoots() })
21
+ return true
22
+ }
23
+ if (method === 'POST' && route === '/skills/save') {
24
+ const body = await host.readJsonBody(req)
25
+ const { skill, file, warning } = await host.getSkills().save(body.skill ?? body)
26
+ host.sendJson(res, 200, { ok: true, skill, file, warning, skills: host.getSkills().list() })
27
+ return true
28
+ }
29
+ if (method === 'POST' && route === '/skills/delete') {
30
+ const body = await host.readJsonBody(req)
31
+ await host.getSkills().remove(String(body.name ?? ''))
32
+ host.sendJson(res, 200, { ok: true, skills: host.getSkills().list() })
33
+ return true
34
+ }
35
+ if (method === 'POST' && route === '/skills/sync') {
36
+ const { written } = await host.getSkills().syncAll()
37
+ host.sendJson(res, 200, { ok: true, written, skills: host.getSkills().list() })
38
+ return true
39
+ }
40
+ if (method === 'POST' && route === '/skills/upload') {
41
+ const body = await host.readJsonBody(req)
42
+ const fileName = String(body.fileName ?? body.name ?? '').trim()
43
+ const encoded = String(body.dataBase64 ?? '')
44
+ const plain = typeof body.text === 'string' ? body.text : ''
45
+ let text = plain
46
+ let sourceName = fileName
47
+ if (encoded !== '') {
48
+ const buffer = Buffer.from(encoded, 'base64')
49
+ if (buffer.length === 0) throw new Error('上传的文件是空的')
50
+ if (buffer.length > 1_400_000) throw new Error('文件太大:技能包请控制在 1.4MB 以内(正文本身上限 6 万字)')
51
+ if (buffer.subarray(0, 2).toString('latin1') === 'PK') {
52
+ const unzipped = await host.skillFromZip(buffer)
53
+ if (!unzipped.ok) throw new Error(unzipped.error)
54
+ text = unzipped.text
55
+ sourceName = unzipped.fileName
56
+ } else {
57
+ text = buffer.toString('utf8')
58
+ }
59
+ }
60
+ if (String(text).trim() === '') throw new Error('没读到内容:请上传 SKILL.md,或包含 SKILL.md 的 .zip')
61
+ const draft = host.skillFromMarkdown(sourceName, text)
62
+ if (!host.isSkillName(draft.name)) {
63
+ throw new Error('从文件里推不出合法的技能名(kebab-case):在 frontmatter 里加一行 name: my-skill 再传')
64
+ }
65
+ const existing = host.getSkills().get(draft.name)
66
+ if (existing !== undefined && body.overwrite !== true) {
67
+ throw new Error(`技能「${draft.name}」已经在库里了:换个文件名,或勾上「覆盖同名技能」再传。`)
68
+ }
69
+ const { skill, file, warning } = await host.getSkills().save(draft)
70
+ host.sendJson(res, 200, { ok: true, skill, file, warning, overwritten: existing !== undefined, skills: host.getSkills().list() })
71
+ return true
72
+ }
73
+ }
74
+ return false
75
+ }