@alpacachen/dsh-kanban 1.3.4 → 1.3.5
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.
- package/index.js +227 -78
- package/lib/client.js +12 -12
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -144,20 +144,63 @@ export function validateBoard(data) {
|
|
|
144
144
|
if (!Array.isArray(data.labels)) errors.push('labels must be an array')
|
|
145
145
|
if (!Array.isArray(data.cards)) errors.push('cards must be an array')
|
|
146
146
|
if (!Array.isArray(data.activities)) errors.push('activities must be an array')
|
|
147
|
+
|
|
148
|
+
const columnIds = new Set()
|
|
149
|
+
if (Array.isArray(data.columns)) {
|
|
150
|
+
for (const c of data.columns) {
|
|
151
|
+
if (!isObj(c) || typeof c.id !== 'string' || !c.id || typeof c.title !== 'string' || !c.title) {
|
|
152
|
+
errors.push('columns contain an invalid entry')
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
if (columnIds.has(c.id)) {
|
|
156
|
+
errors.push('duplicate column id: ' + c.id)
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
columnIds.add(c.id)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const labelNames = new Set()
|
|
164
|
+
if (Array.isArray(data.labels)) {
|
|
165
|
+
for (const l of data.labels) {
|
|
166
|
+
if (!isObj(l) || typeof l.name !== 'string' || !l.name || typeof l.color !== 'string' || !/^#[0-9a-fA-F]{6}$/.test(l.color)) {
|
|
167
|
+
errors.push('labels contain an invalid entry')
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
if (labelNames.has(l.name)) {
|
|
171
|
+
errors.push('duplicate label name: ' + l.name)
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
labelNames.add(l.name)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
147
178
|
if (Array.isArray(data.cards)) {
|
|
148
179
|
const seen = new Set()
|
|
149
180
|
for (const c of data.cards) {
|
|
150
|
-
if (!isObj(c) || typeof c.id !== 'string' || !c.id) {
|
|
151
|
-
errors.push('cards contain an entry
|
|
181
|
+
if (!isObj(c) || typeof c.id !== 'string' || !c.id || typeof c.columnId !== 'string' || typeof c.title !== 'string') {
|
|
182
|
+
errors.push('cards contain an invalid entry')
|
|
152
183
|
break
|
|
153
184
|
}
|
|
154
185
|
if (seen.has(c.id)) {
|
|
155
186
|
errors.push('duplicate card id: ' + c.id)
|
|
156
187
|
break
|
|
157
188
|
}
|
|
189
|
+
if (!columnIds.has(c.columnId)) errors.push('card references missing column: ' + c.id)
|
|
190
|
+
if (c.label != null && !labelNames.has(c.label)) errors.push('card references missing label: ' + c.id)
|
|
191
|
+
if (c.priority != null && !['high', 'medium', 'low'].includes(c.priority)) errors.push('card has invalid priority: ' + c.id)
|
|
158
192
|
seen.add(c.id)
|
|
159
193
|
}
|
|
160
194
|
}
|
|
195
|
+
|
|
196
|
+
if (Array.isArray(data.activities)) {
|
|
197
|
+
for (const a of data.activities) {
|
|
198
|
+
if (!isObj(a) || typeof a.id !== 'string' || !a.id || typeof a.ts !== 'string' || typeof a.type !== 'string' || !['human', 'agent'].includes(a.source)) {
|
|
199
|
+
errors.push('activities contain an invalid entry')
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
161
204
|
return { ok: errors.length === 0, errors }
|
|
162
205
|
}
|
|
163
206
|
|
|
@@ -255,7 +298,9 @@ export function apply(ctx) {
|
|
|
255
298
|
const getWorkspaceRegistry = () => ctx.get('workspaceRegistry')
|
|
256
299
|
|
|
257
300
|
const boards = new Map() // workspaceId -> { columns, labels, cards }
|
|
258
|
-
const
|
|
301
|
+
const boardLoads = new Map() // workspaceId -> Promise<board>,防止冷启动发布半初始化状态
|
|
302
|
+
const workspaceQueues = new Map() // workspaceId -> Promise,串行化完整 mutation 临界区
|
|
303
|
+
const fileTargets = new Map() // workspaceId -> FsTarget;解析失败不缓存,允许后续重试
|
|
259
304
|
let seq = 0 // 全局自增,用于生成 cN(列)/ kN(卡)唯一 id
|
|
260
305
|
|
|
261
306
|
// ---- id 生成 ----
|
|
@@ -299,13 +344,12 @@ export function apply(ctx) {
|
|
|
299
344
|
}
|
|
300
345
|
const targetOf = async (workspace) => {
|
|
301
346
|
const key = workspaceKey(workspace)
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const key = workspaceKey(workspace)
|
|
307
|
-
return fileTargets.has(key) && fileTargets.get(key) !== null
|
|
347
|
+
if (fileTargets.has(key)) return fileTargets.get(key)
|
|
348
|
+
const target = await resolveFile(workspace)
|
|
349
|
+
if (target) fileTargets.set(key, target)
|
|
350
|
+
return target
|
|
308
351
|
}
|
|
352
|
+
const persistedFlag = (workspace) => fileTargets.has(workspaceKey(workspace))
|
|
309
353
|
|
|
310
354
|
// ---- 看板读写 ----
|
|
311
355
|
|
|
@@ -338,54 +382,91 @@ export function apply(ctx) {
|
|
|
338
382
|
}
|
|
339
383
|
const timestamp = () => new Date().toISOString().replace(/[:.]/g, '-')
|
|
340
384
|
|
|
341
|
-
|
|
385
|
+
const isNotFound = (err) => err && (err.code === 'ENOENT' || err.code === 'FS_NOT_FOUND' || err.message === 'ENOENT')
|
|
386
|
+
|
|
387
|
+
// 首次访问缓存初始化 Promise;完成读取、迁移和默认值建立后才发布 board。
|
|
342
388
|
const boardOf = async (workspace, session) => {
|
|
343
389
|
const key = workspaceKey(workspace)
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
390
|
+
const existing = boards.get(key)
|
|
391
|
+
if (existing) return existing
|
|
392
|
+
if (boardLoads.has(key)) return boardLoads.get(key)
|
|
393
|
+
|
|
394
|
+
const load = (async () => {
|
|
395
|
+
const board = {
|
|
396
|
+
schemaVersion: SCHEMA_VERSION,
|
|
397
|
+
columns: [],
|
|
398
|
+
labels: [],
|
|
399
|
+
cards: [],
|
|
400
|
+
activities: [],
|
|
401
|
+
warnings: [],
|
|
402
|
+
readOnlyReason: null,
|
|
403
|
+
}
|
|
404
|
+
const fs = getFs()
|
|
405
|
+
const target = await targetOf(workspace)
|
|
406
|
+
let migrated = false
|
|
407
|
+
if (fs && target) {
|
|
408
|
+
try {
|
|
409
|
+
const text = await fs.readText(target)
|
|
410
|
+
const parsed = parseBoardText(text)
|
|
411
|
+
for (const w of parsed.warnings) warn(board, w)
|
|
412
|
+
if (parsed.ok) {
|
|
413
|
+
board.columns = parsed.data.columns
|
|
414
|
+
board.labels = parsed.data.labels
|
|
415
|
+
board.cards = parsed.data.cards
|
|
416
|
+
board.activities = parsed.data.activities
|
|
417
|
+
migrated = parsed.migrated
|
|
418
|
+
if (parsed.migrated) {
|
|
419
|
+
const backup = await backupFile(workspace, 'bak-v' + parsed.fromVersion, session)
|
|
420
|
+
if (!backup) {
|
|
421
|
+
migrated = false
|
|
422
|
+
board.readOnlyReason = 'Board migration backup failed; changes are disabled to protect the original file.'
|
|
423
|
+
warn(board, board.readOnlyReason)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
} else {
|
|
427
|
+
const suffix = parsed.kind === 'unsupported'
|
|
428
|
+
? 'unsupported-v' + parsed.version
|
|
429
|
+
: 'corrupt-' + timestamp()
|
|
430
|
+
const backup = await backupFile(workspace, suffix, session)
|
|
431
|
+
if (parsed.kind === 'unsupported' || !backup) {
|
|
432
|
+
board.readOnlyReason = parsed.kind === 'unsupported'
|
|
433
|
+
? 'Board was created by a newer plugin version; changes are disabled until the plugin is upgraded.'
|
|
434
|
+
: 'Board backup failed; changes are disabled to protect the original file.'
|
|
435
|
+
warn(board, board.readOnlyReason)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch (err) {
|
|
439
|
+
console.log('dsh-kanban: 读取看板失败 ' + key + ':' + ((err && err.message) || err))
|
|
440
|
+
if (!isNotFound(err)) {
|
|
441
|
+
board.readOnlyReason = 'Board could not be read; changes are disabled to protect the existing file.'
|
|
442
|
+
warn(board, board.readOnlyReason)
|
|
365
443
|
}
|
|
366
|
-
} else {
|
|
367
|
-
// 损坏 / 结构无效 / 版本超前:原文件已无法安全读取,先备份再以空板继续
|
|
368
|
-
const suffix =
|
|
369
|
-
parsed.kind === 'unsupported' ? 'unsupported-v' + parsed.version : 'corrupt-' + timestamp()
|
|
370
|
-
await backupFile(workspace, suffix, session)
|
|
371
444
|
}
|
|
372
|
-
} catch (err) {
|
|
373
|
-
// 尚无看板文件(首次使用)或 fs 读取异常,保留默认空板
|
|
374
|
-
console.log('dsh-kanban: 读取看板失败 ' + key + ':' + ((err && err.message) || err))
|
|
375
445
|
}
|
|
446
|
+
for (const col of board.columns) bumpSeq(col.id)
|
|
447
|
+
for (const card of board.cards) bumpSeq(card.id)
|
|
448
|
+
for (const act of board.activities) bumpSeq(act.id)
|
|
449
|
+
if (board.columns.length === 0) {
|
|
450
|
+
for (const title of DEFAULT_COLUMNS) board.columns.push({ id: nextId('c'), title })
|
|
451
|
+
}
|
|
452
|
+
if (board.labels.length === 0) board.labels = DEFAULT_LABELS.map((l) => ({ ...l }))
|
|
453
|
+
boards.set(key, board)
|
|
454
|
+
if (migrated && fs && target) {
|
|
455
|
+
try {
|
|
456
|
+
await save(workspace, session)
|
|
457
|
+
} catch (err) {
|
|
458
|
+
board.readOnlyReason = 'Migrated board could not be saved; changes are disabled to protect the original file.'
|
|
459
|
+
warn(board, board.readOnlyReason)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return board
|
|
463
|
+
})()
|
|
464
|
+
boardLoads.set(key, load)
|
|
465
|
+
try {
|
|
466
|
+
return await load
|
|
467
|
+
} finally {
|
|
468
|
+
boardLoads.delete(key)
|
|
376
469
|
}
|
|
377
|
-
for (const col of board.columns) bumpSeq(col.id)
|
|
378
|
-
for (const card of board.cards) bumpSeq(card.id)
|
|
379
|
-
for (const act of board.activities) bumpSeq(act.id)
|
|
380
|
-
if (board.columns.length === 0) {
|
|
381
|
-
for (const title of DEFAULT_COLUMNS) board.columns.push({ id: nextId('c'), title })
|
|
382
|
-
}
|
|
383
|
-
if (board.labels.length === 0) {
|
|
384
|
-
board.labels = DEFAULT_LABELS.map((l) => ({ ...l }))
|
|
385
|
-
}
|
|
386
|
-
// schema 升级写回:迁移成功即落盘新版本,保证每个文件只迁移一次
|
|
387
|
-
if (migrated && fs && target) await save(workspace, session)
|
|
388
|
-
return board
|
|
389
470
|
}
|
|
390
471
|
const save = async (workspace, session) => {
|
|
391
472
|
const fs = getFs()
|
|
@@ -409,6 +490,7 @@ export function apply(ctx) {
|
|
|
409
490
|
)
|
|
410
491
|
} catch (err) {
|
|
411
492
|
console.log('dsh-kanban: 保存失败 ' + key + ':' + ((err && err.message) || err))
|
|
493
|
+
throw err
|
|
412
494
|
}
|
|
413
495
|
}
|
|
414
496
|
|
|
@@ -466,12 +548,16 @@ export function apply(ctx) {
|
|
|
466
548
|
}
|
|
467
549
|
|
|
468
550
|
// ---- 核心数据操作:工具与浏览器 HTTP 共用同一份逻辑 ----
|
|
469
|
-
const
|
|
551
|
+
const READ_METHODS = new Set(['get', 'getCard'])
|
|
552
|
+
const dispatchUnlocked = async (workspace, method, args, source, session) => {
|
|
470
553
|
const board = await boardOf(workspace, session)
|
|
471
554
|
const a = args || {}
|
|
472
555
|
const actor = source === 'agent' ? 'agent' : 'human'
|
|
473
556
|
const persisted = () => persistedFlag(workspace)
|
|
474
557
|
const result = (extra) => ({ board: cloneBoard(board), persisted: persisted(), warnings: takeWarnings(board), ...extra })
|
|
558
|
+
if (!READ_METHODS.has(method) && board.readOnlyReason) {
|
|
559
|
+
return result({ error: board.readOnlyReason })
|
|
560
|
+
}
|
|
475
561
|
|
|
476
562
|
switch (method) {
|
|
477
563
|
case 'get':
|
|
@@ -486,12 +572,14 @@ export function apply(ctx) {
|
|
|
486
572
|
case 'addCard': {
|
|
487
573
|
const col = findColumn(board, str(a.columnId, '')) || board.columns[0]
|
|
488
574
|
if (!col) return result({ error: 'No list available' })
|
|
575
|
+
const label = typeof a.label === 'string' ? a.label.slice(0, 20) : undefined
|
|
576
|
+
if (label && !findLabel(board, label)) return result({ error: 'Label not found: ' + label })
|
|
489
577
|
const card = {
|
|
490
578
|
id: nextId('k'),
|
|
491
579
|
columnId: col.id,
|
|
492
580
|
title: str(a.title, '').slice(0, 120) || 'Untitled card',
|
|
493
581
|
note: str(a.note, '').slice(0, 500),
|
|
494
|
-
label
|
|
582
|
+
label,
|
|
495
583
|
priority: normPriority(a.priority),
|
|
496
584
|
createdAt: new Date().toISOString(),
|
|
497
585
|
createdBy: actor,
|
|
@@ -515,6 +603,8 @@ export function apply(ctx) {
|
|
|
515
603
|
case 'updateCard': {
|
|
516
604
|
const card = findCard(board, str(a.id, ''))
|
|
517
605
|
if (card) {
|
|
606
|
+
const nextLabel = typeof a.label === 'string' ? a.label.slice(0, 20) : undefined
|
|
607
|
+
if (nextLabel && !findLabel(board, nextLabel)) return result({ error: 'Label not found: ' + nextLabel })
|
|
518
608
|
const before = {
|
|
519
609
|
title: card.title,
|
|
520
610
|
note: card.note,
|
|
@@ -523,7 +613,7 @@ export function apply(ctx) {
|
|
|
523
613
|
}
|
|
524
614
|
if (typeof a.title === 'string') card.title = a.title.slice(0, 120) || card.title
|
|
525
615
|
if (typeof a.note === 'string') card.note = a.note.slice(0, 500)
|
|
526
|
-
if (typeof a.label === 'string') card.label =
|
|
616
|
+
if (typeof a.label === 'string') card.label = nextLabel || undefined
|
|
527
617
|
if (typeof a.priority === 'string') card.priority = normPriority(a.priority)
|
|
528
618
|
const after = {
|
|
529
619
|
title: card.title,
|
|
@@ -553,10 +643,9 @@ export function apply(ctx) {
|
|
|
553
643
|
case 'deleteCard': {
|
|
554
644
|
const id = str(a.id, '')
|
|
555
645
|
const card = findCard(board, id)
|
|
646
|
+
if (!card) return result({ error: 'Card not found: ' + id })
|
|
556
647
|
board.cards = board.cards.filter((c) => c.id !== id)
|
|
557
|
-
|
|
558
|
-
record(board, { cardId: id, type: 'card_deleted', source: actor, meta: { title: card.title } })
|
|
559
|
-
}
|
|
648
|
+
record(board, { cardId: id, type: 'card_deleted', source: actor, meta: { title: card.title } })
|
|
560
649
|
await save(workspace, session)
|
|
561
650
|
return result({ message: 'Card deleted' })
|
|
562
651
|
}
|
|
@@ -694,6 +783,27 @@ export function apply(ctx) {
|
|
|
694
783
|
}
|
|
695
784
|
}
|
|
696
785
|
|
|
786
|
+
const dispatch = (workspace, method, args, source, session) => {
|
|
787
|
+
const key = workspaceKey(workspace)
|
|
788
|
+
const previous = workspaceQueues.get(key) || Promise.resolve()
|
|
789
|
+
const run = previous.catch(() => {}).then(async () => {
|
|
790
|
+
const board = await boardOf(workspace, session)
|
|
791
|
+
const snapshot = READ_METHODS.has(method) ? null : JSON.parse(JSON.stringify(board))
|
|
792
|
+
try {
|
|
793
|
+
return await dispatchUnlocked(workspace, method, args, source, session)
|
|
794
|
+
} catch (err) {
|
|
795
|
+
if (snapshot) boards.set(key, snapshot)
|
|
796
|
+
throw err
|
|
797
|
+
}
|
|
798
|
+
})
|
|
799
|
+
const settled = run.then(() => undefined, () => undefined)
|
|
800
|
+
workspaceQueues.set(key, settled)
|
|
801
|
+
settled.finally(() => {
|
|
802
|
+
if (workspaceQueues.get(key) === settled) workspaceQueues.delete(key)
|
|
803
|
+
})
|
|
804
|
+
return run
|
|
805
|
+
}
|
|
806
|
+
|
|
697
807
|
// ---- 工具执行上下文 / 浏览器 workspaceId -> 工作区 ----
|
|
698
808
|
const workspaceOfExec = async (exec) => {
|
|
699
809
|
const agent = exec && exec.agent
|
|
@@ -729,47 +839,73 @@ export function apply(ctx) {
|
|
|
729
839
|
}
|
|
730
840
|
}
|
|
731
841
|
const session = exec && exec.agent && exec.agent.session
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
842
|
+
try {
|
|
843
|
+
const r = await dispatch(workspace, method, args, 'agent', session)
|
|
844
|
+
return {
|
|
845
|
+
ok: !r.error,
|
|
846
|
+
message: r.error || r.message || 'Done',
|
|
847
|
+
board: summaryOfClone(r.board),
|
|
848
|
+
warnings: Array.isArray(r.warnings) ? r.warnings : [],
|
|
849
|
+
}
|
|
850
|
+
} catch (err) {
|
|
851
|
+
return {
|
|
852
|
+
ok: false,
|
|
853
|
+
message: 'Board change could not be persisted: ' + ((err && err.message) || err),
|
|
854
|
+
board: summaryOfClone(cloneBoard(await boardOf(workspace, session))),
|
|
855
|
+
warnings: [],
|
|
856
|
+
}
|
|
738
857
|
}
|
|
739
858
|
}
|
|
740
859
|
|
|
741
860
|
// ---- 浏览器数据层:经官方 webServer 扩展点注册 /api/kanban ----
|
|
861
|
+
const MAX_HTTP_BODY = 1024 * 1024
|
|
862
|
+
const sendJson = (res, status, value) => {
|
|
863
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
864
|
+
res.end(JSON.stringify(value))
|
|
865
|
+
}
|
|
742
866
|
const httpHandler = async (req, res) => {
|
|
743
867
|
try {
|
|
868
|
+
if (req.method !== 'POST') return sendJson(res, 405, { error: 'Method not allowed' })
|
|
869
|
+
const contentType = String(req.headers && req.headers['content-type'] || '').toLowerCase()
|
|
870
|
+
if (!contentType.startsWith('application/json')) return sendJson(res, 415, { error: 'Expected application/json' })
|
|
871
|
+
const origin = req.headers && req.headers.origin
|
|
872
|
+
const host = req.headers && req.headers.host
|
|
873
|
+
if (origin && host) {
|
|
874
|
+
let originHost = ''
|
|
875
|
+
try { originHost = new URL(origin).host } catch {}
|
|
876
|
+
if (originHost !== host) return sendJson(res, 403, { error: 'Cross-origin request denied' })
|
|
877
|
+
}
|
|
878
|
+
|
|
744
879
|
const chunks = []
|
|
745
|
-
|
|
880
|
+
let size = 0
|
|
881
|
+
for await (const chunk of req) {
|
|
882
|
+
size += chunk.length
|
|
883
|
+
if (size > MAX_HTTP_BODY) return sendJson(res, 413, { error: 'Request body too large' })
|
|
884
|
+
chunks.push(chunk)
|
|
885
|
+
}
|
|
746
886
|
const raw = Buffer.concat(chunks).toString('utf8')
|
|
747
887
|
const body = raw ? JSON.parse(raw) : {}
|
|
748
888
|
const method = typeof body.method === 'string' ? body.method : 'get'
|
|
749
|
-
const args = body.args
|
|
889
|
+
const args = isObj(body.args) ? body.args : {}
|
|
750
890
|
const workspaceId = typeof args.workspaceId === 'string' ? args.workspaceId : ''
|
|
751
891
|
const workspace = workspaceId ? workspaceOfId(workspaceId) : undefined
|
|
752
|
-
if (!workspace) {
|
|
753
|
-
res.writeHead(400, { 'content-type': 'application/json' })
|
|
754
|
-
res.end(JSON.stringify({ error: 'Unknown workspace: ' + (workspaceId || '(missing)') }))
|
|
755
|
-
return
|
|
756
|
-
}
|
|
892
|
+
if (!workspace) return sendJson(res, 400, { error: 'Unknown workspace: ' + (workspaceId || '(missing)') })
|
|
757
893
|
const result = await dispatch(workspace, method, args, 'human')
|
|
758
|
-
res
|
|
759
|
-
res.end(JSON.stringify(result))
|
|
894
|
+
return sendJson(res, result.error ? 400 : 200, result)
|
|
760
895
|
} catch (err) {
|
|
761
|
-
|
|
762
|
-
res
|
|
896
|
+
const status = err instanceof SyntaxError ? 400 : 500
|
|
897
|
+
return sendJson(res, status, { error: String((err && err.message) || err) })
|
|
763
898
|
}
|
|
764
899
|
}
|
|
765
900
|
|
|
766
|
-
const routeState = { registered: false, timer: null, attempts: 0 }
|
|
901
|
+
const routeState = { registered: false, timer: null, dispose: null, disposed: false, attempts: 0 }
|
|
767
902
|
const registerRoute = () => {
|
|
768
|
-
if (routeState.registered) return
|
|
903
|
+
if (routeState.registered || routeState.disposed) return
|
|
769
904
|
const webServer = ctx.get('webServer')
|
|
770
905
|
if (webServer === undefined) return
|
|
771
906
|
try {
|
|
772
|
-
webServer.register({ kind: 'prefix', path: '/api/kanban', handler: httpHandler })
|
|
907
|
+
const dispose = webServer.register({ kind: 'prefix', path: '/api/kanban', handler: httpHandler })
|
|
908
|
+
routeState.dispose = typeof dispose === 'function' ? dispose : null
|
|
773
909
|
routeState.registered = true
|
|
774
910
|
console.log('dsh-kanban: /api/kanban 路由已注册')
|
|
775
911
|
} catch (err) {
|
|
@@ -786,6 +922,7 @@ export function apply(ctx) {
|
|
|
786
922
|
maybeStartupCheck()
|
|
787
923
|
if (routeState.registered || routeState.attempts >= 40) {
|
|
788
924
|
if (routeState.timer) routeState.timer()
|
|
925
|
+
routeState.timer = null
|
|
789
926
|
}
|
|
790
927
|
}, 500)
|
|
791
928
|
}
|
|
@@ -991,7 +1128,7 @@ export function apply(ctx) {
|
|
|
991
1128
|
title: { type: 'string', description: 'New title (optional)' },
|
|
992
1129
|
note: { type: 'string', description: 'New note (optional)' },
|
|
993
1130
|
label: { type: 'string', description: 'New label name (optional); pass empty string to clear' },
|
|
994
|
-
priority: { type: 'string', enum: ['high', 'medium', 'low'], description: 'New priority (optional): high=P0 / medium=P1 / low=P2; pass empty string to clear' },
|
|
1131
|
+
priority: { type: 'string', enum: ['high', 'medium', 'low', ''], description: 'New priority (optional): high=P0 / medium=P1 / low=P2; pass empty string to clear' },
|
|
995
1132
|
},
|
|
996
1133
|
required: ['id'],
|
|
997
1134
|
},
|
|
@@ -1180,4 +1317,16 @@ export function apply(ctx) {
|
|
|
1180
1317
|
]
|
|
1181
1318
|
|
|
1182
1319
|
for (const tool of tools) ctx.tools.register(tool)
|
|
1320
|
+
|
|
1321
|
+
return () => {
|
|
1322
|
+
routeState.disposed = true
|
|
1323
|
+
if (routeState.timer) routeState.timer()
|
|
1324
|
+
routeState.timer = null
|
|
1325
|
+
if (routeState.dispose) routeState.dispose()
|
|
1326
|
+
routeState.dispose = null
|
|
1327
|
+
boards.clear()
|
|
1328
|
+
boardLoads.clear()
|
|
1329
|
+
workspaceQueues.clear()
|
|
1330
|
+
fileTargets.clear()
|
|
1331
|
+
}
|
|
1183
1332
|
}
|