@alpacachen/dsh-kanban 1.2.0 → 1.3.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.
- package/README.md +53 -86
- package/README.zh.md +48 -81
- package/index.js +597 -81
- package/lib/client.js +132 -14
- package/package.json +4 -1
package/index.js
CHANGED
|
@@ -6,19 +6,249 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 职责:
|
|
8
8
|
* - 按工作区(项目)隔离:boards 以 workspaceId 为键,每个工作区一块独立看板
|
|
9
|
-
* - 磁盘持久化:经 ctx.fs 写入 <
|
|
9
|
+
* - 磁盘持久化:经 ctx.fs 写入 <workspace.path>/.dsh-kanban.json
|
|
10
10
|
* - 模型工具:经 ctx.tools.register 注册 14 个 kanban_* 工具
|
|
11
11
|
* - 浏览器数据层:经 ctx.get('webServer') 注册 /api/kanban 前缀路由
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* 数据模型(每工作区,磁盘文件带 schemaVersion):
|
|
14
|
+
* schemaVersion: 2
|
|
14
15
|
* columns: [{ id, title }]
|
|
15
16
|
* labels: [{ name, color }] —— 标签与颜色绑定,name 为唯一键
|
|
16
|
-
* cards: [{ id, columnId, title, note, label, priority }]
|
|
17
|
+
* cards: [{ id, columnId, title, note, label, priority, createdAt, createdBy }]
|
|
18
|
+
* activities: [{ id, ts, cardId, type, source, field?, from?, to?, meta? }] —— 追加式活动日志
|
|
19
|
+
*
|
|
20
|
+
* 数据安全:
|
|
21
|
+
* - 无 schemaVersion 的历史文件按 v0 处理,首次打开自动升级(见 MIGRATIONS)
|
|
22
|
+
* - 升级/损坏/版本超前均先备份原文件(.bak-vN / .corrupt-<ts> / .unsupported-vN)
|
|
23
|
+
* - 启动时全量体检所有看板文件,损坏文件备份后不影响看板可用性
|
|
17
24
|
*/
|
|
18
25
|
export const name = 'dsh-kanban'
|
|
19
26
|
|
|
20
27
|
export const inject = ['tools']
|
|
21
28
|
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// 持久化格式版本与迁移
|
|
31
|
+
//
|
|
32
|
+
// 磁盘文件结构:
|
|
33
|
+
// { schemaVersion, columns, labels, cards }
|
|
34
|
+
//
|
|
35
|
+
// 版本约定:
|
|
36
|
+
// v1 —— 首个带版本声明的格式,等价于 1.0.x ~ 1.2.x 时代无版本声明的三数组格式
|
|
37
|
+
// (columns/labels/cards),并补齐了卡片的规范字段(note/label/priority)。
|
|
38
|
+
// LEGACY_VERSION(0) —— 历史文件没有 schemaVersion 字段,一律归入 v0 处理。
|
|
39
|
+
//
|
|
40
|
+
// 新增/删除字段的流程:
|
|
41
|
+
// 1) SCHEMA_VERSION 自增
|
|
42
|
+
// 2) 在 MIGRATIONS 里以"旧版本号为键"注册 v(n)→v(n+1) 迁移函数
|
|
43
|
+
// 3) 迁移函数是纯函数,输出必须带 schemaVersion: n+1(migrateBoard 会校验)
|
|
44
|
+
// 旧文件在首次打开时自动沿迁移链升级,升级前原文件先备份。
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export const SCHEMA_VERSION = 2
|
|
48
|
+
export const LEGACY_VERSION = 0
|
|
49
|
+
|
|
50
|
+
// 活动日志:追加式只读记录,随看板一起落盘;超过上限丢弃最旧事件,防止日志无界增长。
|
|
51
|
+
const ACTIVITY_LIMIT = 5000
|
|
52
|
+
|
|
53
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v)
|
|
54
|
+
const strField = (v, fb) => (typeof v === 'string' && v ? v : fb)
|
|
55
|
+
const normColor = (v) => (typeof v === 'string' && /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : '#94a3b8')
|
|
56
|
+
|
|
57
|
+
// v1 的规范实体形状(仅补缺省值,不改动合法数据)
|
|
58
|
+
const normColumn = (c) =>
|
|
59
|
+
isObj(c) ? { id: strField(c.id, ''), title: strField(c.title, 'Untitled') } : null
|
|
60
|
+
const normLabel = (l) =>
|
|
61
|
+
isObj(l) ? { name: strField(l.name, ''), color: normColor(l.color) } : null
|
|
62
|
+
const normCard = (c) =>
|
|
63
|
+
isObj(c)
|
|
64
|
+
? {
|
|
65
|
+
id: strField(c.id, ''),
|
|
66
|
+
columnId: strField(c.columnId, ''),
|
|
67
|
+
title: strField(c.title, 'Untitled'),
|
|
68
|
+
note: typeof c.note === 'string' ? c.note : '',
|
|
69
|
+
label: typeof c.label === 'string' && c.label ? c.label : null,
|
|
70
|
+
priority: typeof c.priority === 'string' ? c.priority : null,
|
|
71
|
+
}
|
|
72
|
+
: null
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 逐版本迁移注册表:key = 旧版本号,value = (旧数据) => 新数据。
|
|
76
|
+
* 输出必须设置 schemaVersion = key + 1,migrateBoard 会逐级校验。
|
|
77
|
+
*/
|
|
78
|
+
export const MIGRATIONS = {
|
|
79
|
+
0: (data) => {
|
|
80
|
+
// v0 → v1:历史无版本文件 —— 声明版本 + 规范化实体字段
|
|
81
|
+
const src = isObj(data) ? data : {}
|
|
82
|
+
const pick = (arr) => (Array.isArray(arr) ? arr : [])
|
|
83
|
+
return {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
columns: pick(src.columns).map(normColumn).filter(Boolean),
|
|
86
|
+
labels: pick(src.labels).map(normLabel).filter(Boolean),
|
|
87
|
+
cards: pick(src.cards).map(normCard).filter(Boolean),
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
1: (data) => {
|
|
91
|
+
// v1 → v2:新增追加式活动日志 activities;卡片补 createdAt/createdBy(历史数据为 null)
|
|
92
|
+
const src = isObj(data) ? data : {}
|
|
93
|
+
const pick = (arr) => (Array.isArray(arr) ? arr : [])
|
|
94
|
+
return {
|
|
95
|
+
schemaVersion: 2,
|
|
96
|
+
columns: pick(src.columns).map(normColumn).filter(Boolean),
|
|
97
|
+
labels: pick(src.labels).map(normLabel).filter(Boolean),
|
|
98
|
+
cards: pick(src.cards)
|
|
99
|
+
.map((c) => {
|
|
100
|
+
const card = normCard(c)
|
|
101
|
+
return card ? { ...card, createdAt: null, createdBy: null } : null
|
|
102
|
+
})
|
|
103
|
+
.filter(Boolean),
|
|
104
|
+
activities: [],
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 沿迁移链把数据从 fromVersion 逐级升级到 SCHEMA_VERSION。
|
|
111
|
+
* 任一步缺失或产出无效都会抛错(由调用方备份并降级处理)。
|
|
112
|
+
*/
|
|
113
|
+
export function migrateBoard(data, fromVersion) {
|
|
114
|
+
let out = data
|
|
115
|
+
let v = fromVersion
|
|
116
|
+
while (v < SCHEMA_VERSION) {
|
|
117
|
+
const step = MIGRATIONS[v]
|
|
118
|
+
if (typeof step !== 'function') {
|
|
119
|
+
throw new Error('missing migration v' + v + ' -> v' + (v + 1))
|
|
120
|
+
}
|
|
121
|
+
out = step(out)
|
|
122
|
+
v += 1
|
|
123
|
+
if (!isObj(out) || out.schemaVersion !== v) {
|
|
124
|
+
throw new Error('migration v' + (v - 1) + ' -> v' + v + ' produced invalid data')
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return out
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 结构校验(迁移后的最终形态)。
|
|
132
|
+
* 返回 { ok, errors };errors 非空时文件应视为损坏处理。
|
|
133
|
+
*/
|
|
134
|
+
export function validateBoard(data) {
|
|
135
|
+
const errors = []
|
|
136
|
+
if (!isObj(data)) {
|
|
137
|
+
errors.push('board is not an object')
|
|
138
|
+
return { ok: false, errors }
|
|
139
|
+
}
|
|
140
|
+
if (data.schemaVersion !== SCHEMA_VERSION) {
|
|
141
|
+
errors.push('expected schemaVersion ' + SCHEMA_VERSION + ', got ' + String(data.schemaVersion))
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(data.columns)) errors.push('columns must be an array')
|
|
144
|
+
if (!Array.isArray(data.labels)) errors.push('labels must be an array')
|
|
145
|
+
if (!Array.isArray(data.cards)) errors.push('cards must be an array')
|
|
146
|
+
if (!Array.isArray(data.activities)) errors.push('activities must be an array')
|
|
147
|
+
if (Array.isArray(data.cards)) {
|
|
148
|
+
const seen = new Set()
|
|
149
|
+
for (const c of data.cards) {
|
|
150
|
+
if (!isObj(c) || typeof c.id !== 'string' || !c.id) {
|
|
151
|
+
errors.push('cards contain an entry without a valid id')
|
|
152
|
+
break
|
|
153
|
+
}
|
|
154
|
+
if (seen.has(c.id)) {
|
|
155
|
+
errors.push('duplicate card id: ' + c.id)
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
seen.add(c.id)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { ok: errors.length === 0, errors }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 解析并升级一段看板文件文本(纯函数,不触磁盘)。
|
|
166
|
+
*
|
|
167
|
+
* 返回:
|
|
168
|
+
* { ok: true, kind: 'ok', data, migrated, fromVersion, warnings } —— data 为可用的最新版
|
|
169
|
+
* { ok: false, kind: 'corrupt'|'invalid'|'unsupported', warnings } —— 需要调用方备份原文件
|
|
170
|
+
*
|
|
171
|
+
* kind 语义:
|
|
172
|
+
* corrupt —— JSON 无法解析
|
|
173
|
+
* invalid —— 结构校验失败 / 迁移失败
|
|
174
|
+
* unsupported —— schemaVersion 高于当前插件支持(文件来自更新版本插件)
|
|
175
|
+
*/
|
|
176
|
+
export function parseBoardText(text) {
|
|
177
|
+
const warnings = []
|
|
178
|
+
let data
|
|
179
|
+
try {
|
|
180
|
+
data = JSON.parse(text)
|
|
181
|
+
} catch {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
kind: 'corrupt',
|
|
185
|
+
warnings: [
|
|
186
|
+
'Board data file could not be parsed as JSON; the original file has been backed up and the board opened empty.',
|
|
187
|
+
],
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const version =
|
|
192
|
+
isObj(data) && typeof data.schemaVersion === 'number' ? data.schemaVersion : LEGACY_VERSION
|
|
193
|
+
|
|
194
|
+
if (version > SCHEMA_VERSION) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
kind: 'unsupported',
|
|
198
|
+
version,
|
|
199
|
+
warnings: [
|
|
200
|
+
'Board data file was written by a newer plugin version (schemaVersion ' +
|
|
201
|
+
version +
|
|
202
|
+
'); this plugin supports up to ' +
|
|
203
|
+
SCHEMA_VERSION +
|
|
204
|
+
'. The original file has been backed up and the board opened empty — please upgrade the plugin to read the original data.',
|
|
205
|
+
],
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let migrated = false
|
|
210
|
+
if (version < SCHEMA_VERSION) {
|
|
211
|
+
try {
|
|
212
|
+
data = migrateBoard(data, version)
|
|
213
|
+
migrated = true
|
|
214
|
+
warnings.push(
|
|
215
|
+
'Board data was automatically upgraded from schemaVersion ' +
|
|
216
|
+
version +
|
|
217
|
+
' to ' +
|
|
218
|
+
SCHEMA_VERSION +
|
|
219
|
+
'; the pre-upgrade file has been backed up.',
|
|
220
|
+
)
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
kind: 'invalid',
|
|
225
|
+
version,
|
|
226
|
+
warnings: [
|
|
227
|
+
'Board data upgrade failed (' +
|
|
228
|
+
((err && err.message) || err) +
|
|
229
|
+
'); the original file has been backed up and the board opened empty.',
|
|
230
|
+
],
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const check = validateBoard(data)
|
|
236
|
+
if (!check.ok) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
kind: 'invalid',
|
|
240
|
+
version: isObj(data) ? data.schemaVersion : undefined,
|
|
241
|
+
warnings: [
|
|
242
|
+
'Board data file structure is invalid (' +
|
|
243
|
+
check.errors.join('; ') +
|
|
244
|
+
'); the original file has been backed up and the board opened empty.',
|
|
245
|
+
],
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { ok: true, kind: 'ok', data, migrated, fromVersion: version, warnings }
|
|
250
|
+
}
|
|
251
|
+
|
|
22
252
|
export function apply(ctx) {
|
|
23
253
|
const getFs = () => ctx.get('fs')
|
|
24
254
|
const getPolicy = () => ctx.get('sandboxPolicy')
|
|
@@ -45,66 +275,140 @@ export function apply(ctx) {
|
|
|
45
275
|
]
|
|
46
276
|
|
|
47
277
|
// ---- 持久化定位 ----
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
278
|
+
const BOARD_FILE = '.dsh-kanban.json'
|
|
279
|
+
const workspaceKey = (workspace) => String(workspace.id || workspace.path)
|
|
280
|
+
const writePolicyFor = (workspace, session) => {
|
|
281
|
+
const policy = getPolicy()
|
|
282
|
+
const resolved =
|
|
283
|
+
policy && typeof policy.resolve === 'function'
|
|
284
|
+
? policy.resolve(session ? { session } : {})
|
|
285
|
+
: { mode: (policy && policy.defaultMode) || 'workspace-write' }
|
|
286
|
+
return { ...resolved, workspaceRoot: workspace.path }
|
|
51
287
|
}
|
|
52
|
-
const
|
|
53
|
-
const resolveFile = async (wsid) => {
|
|
288
|
+
const resolveFile = async (workspace) => {
|
|
54
289
|
const fs = getFs()
|
|
55
290
|
if (!fs) return null
|
|
56
291
|
try {
|
|
57
|
-
return await fs.resolve(
|
|
292
|
+
return await fs.resolve(BOARD_FILE, { cwd: workspace.path })
|
|
58
293
|
} catch (err) {
|
|
59
|
-
console.log(
|
|
294
|
+
console.log(
|
|
295
|
+
'dsh-kanban: 解析工作区看板文件失败 ' + workspaceKey(workspace) + ':' + ((err && err.message) || err),
|
|
296
|
+
)
|
|
60
297
|
return null
|
|
61
298
|
}
|
|
62
299
|
}
|
|
63
|
-
const targetOf = async (
|
|
64
|
-
|
|
65
|
-
|
|
300
|
+
const targetOf = async (workspace) => {
|
|
301
|
+
const key = workspaceKey(workspace)
|
|
302
|
+
if (!fileTargets.has(key)) fileTargets.set(key, await resolveFile(workspace))
|
|
303
|
+
return fileTargets.get(key)
|
|
304
|
+
}
|
|
305
|
+
const persistedFlag = (workspace) => {
|
|
306
|
+
const key = workspaceKey(workspace)
|
|
307
|
+
return fileTargets.has(key) && fileTargets.get(key) !== null
|
|
66
308
|
}
|
|
67
|
-
const persistedFlag = (wsid) => fileTargets.has(wsid) && fileTargets.get(wsid) !== null
|
|
68
309
|
|
|
69
310
|
// ---- 看板读写 ----
|
|
70
|
-
|
|
71
|
-
|
|
311
|
+
|
|
312
|
+
// 备份原文件为 .dsh-kanban.json.<suffix>(复制而非移动,保证原文件在写回前始终存在)
|
|
313
|
+
const backupFile = async (workspace, suffix, session) => {
|
|
314
|
+
const fs = getFs()
|
|
315
|
+
const target = await targetOf(workspace)
|
|
316
|
+
if (!fs || !target) return null
|
|
317
|
+
const key = workspaceKey(workspace)
|
|
318
|
+
try {
|
|
319
|
+
const text = await fs.readText(target)
|
|
320
|
+
const backupTarget = await fs.resolve(BOARD_FILE + '.' + suffix, { cwd: workspace.path })
|
|
321
|
+
await fs.writeText(backupTarget, text, undefined, undefined, writePolicyFor(workspace, session))
|
|
322
|
+
return backupTarget
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.log('dsh-kanban: 备份失败 ' + key + ' (' + suffix + '):' + ((err && err.message) || err))
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 记录一次性警告:进入看板 warnings 队列 + 写宿主日志
|
|
330
|
+
const warn = (board, message) => {
|
|
331
|
+
if (Array.isArray(board.warnings)) board.warnings.push(message)
|
|
332
|
+
console.log('dsh-kanban: ' + message)
|
|
333
|
+
}
|
|
334
|
+
const takeWarnings = (board) => {
|
|
335
|
+
const w = Array.isArray(board.warnings) ? board.warnings : []
|
|
336
|
+
board.warnings = []
|
|
337
|
+
return w
|
|
338
|
+
}
|
|
339
|
+
const timestamp = () => new Date().toISOString().replace(/[:.]/g, '-')
|
|
340
|
+
|
|
341
|
+
// 首次访问某工作区时从该工作区根目录加载;异常数据一律不阻塞看板可用性
|
|
342
|
+
const boardOf = async (workspace, session) => {
|
|
343
|
+
const key = workspaceKey(workspace)
|
|
344
|
+
let board = boards.get(key)
|
|
72
345
|
if (board) return board
|
|
73
|
-
board = { columns: [], labels: [], cards: [] }
|
|
74
|
-
boards.set(
|
|
346
|
+
board = { schemaVersion: SCHEMA_VERSION, columns: [], labels: [], cards: [], activities: [], warnings: [] }
|
|
347
|
+
boards.set(key, board)
|
|
75
348
|
const fs = getFs()
|
|
76
|
-
const target = await targetOf(
|
|
349
|
+
const target = await targetOf(workspace)
|
|
350
|
+
let migrated = false
|
|
77
351
|
if (fs && target) {
|
|
78
352
|
try {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
board.
|
|
353
|
+
const text = await fs.readText(target)
|
|
354
|
+
const parsed = parseBoardText(text)
|
|
355
|
+
for (const w of parsed.warnings) warn(board, w)
|
|
356
|
+
if (parsed.ok) {
|
|
357
|
+
board.columns = parsed.data.columns
|
|
358
|
+
board.labels = parsed.data.labels
|
|
359
|
+
board.cards = parsed.data.cards
|
|
360
|
+
board.activities = Array.isArray(parsed.data.activities) ? parsed.data.activities : []
|
|
361
|
+
migrated = parsed.migrated
|
|
362
|
+
// schema 迁移场景:写回前先把迁移前的原文件备份下来,保证可回滚
|
|
363
|
+
if (parsed.migrated) {
|
|
364
|
+
await backupFile(workspace, 'bak-v' + parsed.fromVersion, session)
|
|
365
|
+
}
|
|
366
|
+
} else {
|
|
367
|
+
// 损坏 / 结构无效 / 版本超前:原文件已无法安全读取,先备份再以空板继续
|
|
368
|
+
const suffix =
|
|
369
|
+
parsed.kind === 'unsupported' ? 'unsupported-v' + parsed.version : 'corrupt-' + timestamp()
|
|
370
|
+
await backupFile(workspace, suffix, session)
|
|
84
371
|
}
|
|
85
372
|
} catch (err) {
|
|
86
|
-
//
|
|
373
|
+
// 尚无看板文件(首次使用)或 fs 读取异常,保留默认空板
|
|
374
|
+
console.log('dsh-kanban: 读取看板失败 ' + key + ':' + ((err && err.message) || err))
|
|
87
375
|
}
|
|
88
376
|
}
|
|
89
377
|
for (const col of board.columns) bumpSeq(col.id)
|
|
90
378
|
for (const card of board.cards) bumpSeq(card.id)
|
|
379
|
+
for (const act of board.activities) bumpSeq(act.id)
|
|
91
380
|
if (board.columns.length === 0) {
|
|
92
381
|
for (const title of DEFAULT_COLUMNS) board.columns.push({ id: nextId('c'), title })
|
|
93
382
|
}
|
|
94
383
|
if (board.labels.length === 0) {
|
|
95
384
|
board.labels = DEFAULT_LABELS.map((l) => ({ ...l }))
|
|
96
385
|
}
|
|
386
|
+
// schema 升级写回:迁移成功即落盘新版本,保证每个文件只迁移一次
|
|
387
|
+
if (migrated && fs && target) await save(workspace, session)
|
|
97
388
|
return board
|
|
98
389
|
}
|
|
99
|
-
const save = async (
|
|
390
|
+
const save = async (workspace, session) => {
|
|
100
391
|
const fs = getFs()
|
|
101
|
-
const target = await targetOf(
|
|
102
|
-
const
|
|
392
|
+
const target = await targetOf(workspace)
|
|
393
|
+
const key = workspaceKey(workspace)
|
|
394
|
+
const board = boards.get(key)
|
|
103
395
|
if (!fs || !target || !board) return
|
|
104
396
|
try {
|
|
105
|
-
await fs.writeText(
|
|
397
|
+
await fs.writeText(
|
|
398
|
+
target,
|
|
399
|
+
JSON.stringify({
|
|
400
|
+
schemaVersion: SCHEMA_VERSION,
|
|
401
|
+
columns: board.columns,
|
|
402
|
+
labels: board.labels,
|
|
403
|
+
cards: board.cards,
|
|
404
|
+
activities: Array.isArray(board.activities) ? board.activities : [],
|
|
405
|
+
}),
|
|
406
|
+
undefined,
|
|
407
|
+
undefined,
|
|
408
|
+
writePolicyFor(workspace, session),
|
|
409
|
+
)
|
|
106
410
|
} catch (err) {
|
|
107
|
-
console.log('dsh-kanban: 保存失败 ' +
|
|
411
|
+
console.log('dsh-kanban: 保存失败 ' + key + ':' + ((err && err.message) || err))
|
|
108
412
|
}
|
|
109
413
|
}
|
|
110
414
|
|
|
@@ -127,7 +431,10 @@ export function apply(ctx) {
|
|
|
127
431
|
note: c.note ?? '',
|
|
128
432
|
label: c.label ?? null,
|
|
129
433
|
priority: c.priority ?? null,
|
|
434
|
+
createdAt: typeof c.createdAt === 'string' ? c.createdAt : null,
|
|
435
|
+
createdBy: typeof c.createdBy === 'string' ? c.createdBy : null,
|
|
130
436
|
})),
|
|
437
|
+
activities: Array.isArray(b.activities) ? b.activities.map((a) => ({ ...a })) : [],
|
|
131
438
|
})
|
|
132
439
|
const summaryOfClone = (clone) => ({
|
|
133
440
|
columns: clone.columns.map((c) => ({
|
|
@@ -145,46 +452,98 @@ export function apply(ctx) {
|
|
|
145
452
|
})),
|
|
146
453
|
})
|
|
147
454
|
|
|
455
|
+
// 追加一条活动事件(只读日志,随看板一起落盘)
|
|
456
|
+
const record = (board, ev) => {
|
|
457
|
+
if (!Array.isArray(board.activities)) board.activities = []
|
|
458
|
+
board.activities.push({
|
|
459
|
+
id: nextId('e'),
|
|
460
|
+
ts: new Date().toISOString(),
|
|
461
|
+
...ev,
|
|
462
|
+
})
|
|
463
|
+
if (board.activities.length > ACTIVITY_LIMIT) {
|
|
464
|
+
board.activities.splice(0, board.activities.length - ACTIVITY_LIMIT)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
148
468
|
// ---- 核心数据操作:工具与浏览器 HTTP 共用同一份逻辑 ----
|
|
149
|
-
const dispatch = async (
|
|
150
|
-
const board = await boardOf(
|
|
469
|
+
const dispatch = async (workspace, method, args, source, session) => {
|
|
470
|
+
const board = await boardOf(workspace, session)
|
|
151
471
|
const a = args || {}
|
|
152
|
-
const
|
|
153
|
-
const
|
|
472
|
+
const actor = source === 'agent' ? 'agent' : 'human'
|
|
473
|
+
const persisted = () => persistedFlag(workspace)
|
|
474
|
+
const result = (extra) => ({ board: cloneBoard(board), persisted: persisted(), warnings: takeWarnings(board), ...extra })
|
|
154
475
|
|
|
155
476
|
switch (method) {
|
|
156
477
|
case 'get':
|
|
157
|
-
return { board: cloneBoard(board), persisted: persisted(), message: 'Board loaded' }
|
|
478
|
+
return { board: cloneBoard(board), persisted: persisted(), warnings: takeWarnings(board), message: 'Board loaded' }
|
|
158
479
|
|
|
159
480
|
case 'getCard': {
|
|
160
481
|
const card = cloneBoard(board).cards.find((c) => c.id === str(a.id, ''))
|
|
161
|
-
if (!card) return { error: 'Card not found: ' + str(a.id, '') }
|
|
162
|
-
return { card }
|
|
482
|
+
if (!card) return { warnings: takeWarnings(board), error: 'Card not found: ' + str(a.id, '') }
|
|
483
|
+
return { card, warnings: takeWarnings(board) }
|
|
163
484
|
}
|
|
164
485
|
|
|
165
486
|
case 'addCard': {
|
|
166
487
|
const col = findColumn(board, str(a.columnId, '')) || board.columns[0]
|
|
167
488
|
if (!col) return result({ error: 'No list available' })
|
|
168
|
-
|
|
489
|
+
const card = {
|
|
169
490
|
id: nextId('k'),
|
|
170
491
|
columnId: col.id,
|
|
171
492
|
title: str(a.title, '').slice(0, 120) || 'Untitled card',
|
|
172
493
|
note: str(a.note, '').slice(0, 500),
|
|
173
494
|
label: typeof a.label === 'string' ? a.label.slice(0, 20) : undefined,
|
|
174
495
|
priority: normPriority(a.priority),
|
|
496
|
+
createdAt: new Date().toISOString(),
|
|
497
|
+
createdBy: actor,
|
|
498
|
+
}
|
|
499
|
+
board.cards.push(card)
|
|
500
|
+
record(board, {
|
|
501
|
+
cardId: card.id,
|
|
502
|
+
type: 'card_created',
|
|
503
|
+
source: actor,
|
|
504
|
+
meta: {
|
|
505
|
+
title: card.title,
|
|
506
|
+
column: col.title,
|
|
507
|
+
label: card.label ?? null,
|
|
508
|
+
priority: card.priority ?? null,
|
|
509
|
+
},
|
|
175
510
|
})
|
|
176
|
-
await save(
|
|
511
|
+
await save(workspace, session)
|
|
177
512
|
return result({ message: 'Card added to "' + col.title + '"' })
|
|
178
513
|
}
|
|
179
514
|
|
|
180
515
|
case 'updateCard': {
|
|
181
516
|
const card = findCard(board, str(a.id, ''))
|
|
182
517
|
if (card) {
|
|
518
|
+
const before = {
|
|
519
|
+
title: card.title,
|
|
520
|
+
note: card.note,
|
|
521
|
+
label: card.label ?? null,
|
|
522
|
+
priority: card.priority ?? null,
|
|
523
|
+
}
|
|
183
524
|
if (typeof a.title === 'string') card.title = a.title.slice(0, 120) || card.title
|
|
184
525
|
if (typeof a.note === 'string') card.note = a.note.slice(0, 500)
|
|
185
526
|
if (typeof a.label === 'string') card.label = a.label.slice(0, 20) || undefined
|
|
186
527
|
if (typeof a.priority === 'string') card.priority = normPriority(a.priority)
|
|
187
|
-
|
|
528
|
+
const after = {
|
|
529
|
+
title: card.title,
|
|
530
|
+
note: card.note,
|
|
531
|
+
label: card.label ?? null,
|
|
532
|
+
priority: card.priority ?? null,
|
|
533
|
+
}
|
|
534
|
+
if (after.title !== before.title) {
|
|
535
|
+
record(board, { cardId: card.id, type: 'card_title_changed', source: actor, field: 'title', from: before.title, to: after.title })
|
|
536
|
+
}
|
|
537
|
+
if (after.note !== before.note) {
|
|
538
|
+
record(board, { cardId: card.id, type: 'card_note_changed', source: actor, field: 'note' })
|
|
539
|
+
}
|
|
540
|
+
if (after.label !== before.label) {
|
|
541
|
+
record(board, { cardId: card.id, type: 'card_label_changed', source: actor, field: 'label', from: before.label, to: after.label })
|
|
542
|
+
}
|
|
543
|
+
if (after.priority !== before.priority) {
|
|
544
|
+
record(board, { cardId: card.id, type: 'card_priority_changed', source: actor, field: 'priority', from: before.priority, to: after.priority })
|
|
545
|
+
}
|
|
546
|
+
await save(workspace, session)
|
|
188
547
|
}
|
|
189
548
|
return card
|
|
190
549
|
? result({ message: 'Card updated' })
|
|
@@ -193,8 +552,12 @@ export function apply(ctx) {
|
|
|
193
552
|
|
|
194
553
|
case 'deleteCard': {
|
|
195
554
|
const id = str(a.id, '')
|
|
555
|
+
const card = findCard(board, id)
|
|
196
556
|
board.cards = board.cards.filter((c) => c.id !== id)
|
|
197
|
-
|
|
557
|
+
if (card) {
|
|
558
|
+
record(board, { cardId: id, type: 'card_deleted', source: actor, meta: { title: card.title } })
|
|
559
|
+
}
|
|
560
|
+
await save(workspace, session)
|
|
198
561
|
return result({ message: 'Card deleted' })
|
|
199
562
|
}
|
|
200
563
|
|
|
@@ -202,6 +565,8 @@ export function apply(ctx) {
|
|
|
202
565
|
const card = findCard(board, str(a.id, ''))
|
|
203
566
|
const target = findColumn(board, str(a.columnId, ''))
|
|
204
567
|
if (!card || !target) return result({ error: 'Card or list not found' })
|
|
568
|
+
const fromCol = findColumn(board, card.columnId)
|
|
569
|
+
const fromTitle = fromCol ? fromCol.title : String(card.columnId)
|
|
205
570
|
board.cards = board.cards.filter((c) => c.id !== card.id)
|
|
206
571
|
card.columnId = target.id
|
|
207
572
|
const inCol = board.cards.filter((c) => c.columnId === target.id)
|
|
@@ -211,22 +576,30 @@ export function apply(ctx) {
|
|
|
211
576
|
const anchor = inCol[toIndex]
|
|
212
577
|
if (anchor) board.cards.splice(board.cards.indexOf(anchor), 0, card)
|
|
213
578
|
else board.cards.push(card)
|
|
214
|
-
|
|
579
|
+
if (fromTitle !== target.title) {
|
|
580
|
+
record(board, { cardId: card.id, type: 'card_moved', source: actor, field: 'columnId', from: fromTitle, to: target.title, meta: { title: card.title } })
|
|
581
|
+
}
|
|
582
|
+
await save(workspace, session)
|
|
215
583
|
return result({ message: 'Moved to "' + target.title + '"' })
|
|
216
584
|
}
|
|
217
585
|
|
|
218
586
|
case 'addColumn': {
|
|
219
587
|
const title = str(a.title, '').slice(0, 40) || 'New list'
|
|
220
588
|
board.columns.push({ id: nextId('c'), title })
|
|
221
|
-
|
|
589
|
+
record(board, { cardId: null, type: 'column_added', source: actor, meta: { column: title } })
|
|
590
|
+
await save(workspace, session)
|
|
222
591
|
return result({ message: 'List added: "' + title + '"' })
|
|
223
592
|
}
|
|
224
593
|
|
|
225
594
|
case 'renameColumn': {
|
|
226
595
|
const col = findColumn(board, str(a.id, ''))
|
|
227
596
|
if (col && typeof a.title === 'string') {
|
|
597
|
+
const before = col.title
|
|
228
598
|
col.title = a.title.slice(0, 40) || col.title
|
|
229
|
-
|
|
599
|
+
if (col.title !== before) {
|
|
600
|
+
record(board, { cardId: null, type: 'column_renamed', source: actor, field: 'title', from: before, to: col.title, meta: { column: col.title } })
|
|
601
|
+
}
|
|
602
|
+
await save(workspace, session)
|
|
230
603
|
}
|
|
231
604
|
return col ? result({ message: 'List renamed' }) : result({ error: 'List not found' })
|
|
232
605
|
}
|
|
@@ -236,12 +609,17 @@ export function apply(ctx) {
|
|
|
236
609
|
if (board.columns.length <= 1) return result({ error: 'At least one list must remain' })
|
|
237
610
|
const idx = board.columns.findIndex((c) => c.id === id)
|
|
238
611
|
if (idx < 0) return result({ error: 'List not found' })
|
|
612
|
+
const deleted = board.columns[idx]
|
|
239
613
|
board.columns.splice(idx, 1)
|
|
240
614
|
const fallback = board.columns[0].id
|
|
241
615
|
for (const card of board.cards) {
|
|
242
|
-
if (card.columnId === id)
|
|
616
|
+
if (card.columnId === id) {
|
|
617
|
+
card.columnId = fallback
|
|
618
|
+
record(board, { cardId: card.id, type: 'card_moved', source: actor, field: 'columnId', from: deleted.title, to: board.columns[0].title, meta: { title: card.title } })
|
|
619
|
+
}
|
|
243
620
|
}
|
|
244
|
-
|
|
621
|
+
record(board, { cardId: null, type: 'column_deleted', source: actor, meta: { column: deleted.title } })
|
|
622
|
+
await save(workspace, session)
|
|
245
623
|
return result({ message: 'List deleted, cards moved to "' + board.columns[0].title + '"' })
|
|
246
624
|
}
|
|
247
625
|
|
|
@@ -254,7 +632,7 @@ export function apply(ctx) {
|
|
|
254
632
|
? Math.max(0, Math.min(Math.floor(a.toIndex), board.columns.length))
|
|
255
633
|
: board.columns.length
|
|
256
634
|
board.columns.splice(toIndex, 0, col)
|
|
257
|
-
await save(
|
|
635
|
+
await save(workspace, session)
|
|
258
636
|
return result({ message: 'List order updated' })
|
|
259
637
|
}
|
|
260
638
|
|
|
@@ -263,7 +641,8 @@ export function apply(ctx) {
|
|
|
263
641
|
if (!name) return result({ error: 'Label name required' })
|
|
264
642
|
if (findLabel(board, name)) return result({ error: 'Label already exists' })
|
|
265
643
|
board.labels.push({ name, color: normColor(a.color) || '#94a3b8' })
|
|
266
|
-
|
|
644
|
+
record(board, { cardId: null, type: 'label_added', source: actor, meta: { label: name } })
|
|
645
|
+
await save(workspace, session)
|
|
267
646
|
return result({ message: 'Label added: "' + name + '"' })
|
|
268
647
|
}
|
|
269
648
|
|
|
@@ -276,11 +655,21 @@ export function apply(ctx) {
|
|
|
276
655
|
if (findLabel(board, newName)) return result({ error: 'Label name already exists' })
|
|
277
656
|
label.name = newName
|
|
278
657
|
for (const card of board.cards) {
|
|
279
|
-
if (card.label === name)
|
|
658
|
+
if (card.label === name) {
|
|
659
|
+
card.label = newName
|
|
660
|
+
record(board, { cardId: card.id, type: 'card_label_changed', source: actor, field: 'label', from: name, to: newName, meta: { title: card.title } })
|
|
661
|
+
}
|
|
280
662
|
}
|
|
663
|
+
record(board, { cardId: null, type: 'label_renamed', source: actor, field: 'name', from: name, to: newName, meta: { label: newName } })
|
|
281
664
|
}
|
|
282
|
-
if (typeof a.color === 'string')
|
|
283
|
-
|
|
665
|
+
if (typeof a.color === 'string') {
|
|
666
|
+
const beforeColor = label.color
|
|
667
|
+
label.color = normColor(a.color) || label.color
|
|
668
|
+
if (label.color !== beforeColor) {
|
|
669
|
+
record(board, { cardId: null, type: 'label_color_changed', source: actor, field: 'color', from: beforeColor, to: label.color, meta: { label: label.name } })
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
await save(workspace, session)
|
|
284
673
|
return result({ message: 'Label updated' })
|
|
285
674
|
}
|
|
286
675
|
|
|
@@ -290,9 +679,13 @@ export function apply(ctx) {
|
|
|
290
679
|
if (idx < 0) return result({ error: 'Label not found' })
|
|
291
680
|
board.labels.splice(idx, 1)
|
|
292
681
|
for (const card of board.cards) {
|
|
293
|
-
if (card.label === name)
|
|
682
|
+
if (card.label === name) {
|
|
683
|
+
card.label = undefined
|
|
684
|
+
record(board, { cardId: card.id, type: 'card_label_changed', source: actor, field: 'label', from: name, to: null, meta: { title: card.title } })
|
|
685
|
+
}
|
|
294
686
|
}
|
|
295
|
-
|
|
687
|
+
record(board, { cardId: null, type: 'label_deleted', source: actor, meta: { label: name } })
|
|
688
|
+
await save(workspace, session)
|
|
296
689
|
return result({ message: 'Label deleted' })
|
|
297
690
|
}
|
|
298
691
|
|
|
@@ -301,28 +694,48 @@ export function apply(ctx) {
|
|
|
301
694
|
}
|
|
302
695
|
}
|
|
303
696
|
|
|
304
|
-
// ---- 工具执行上下文 -> 工作区
|
|
305
|
-
const
|
|
697
|
+
// ---- 工具执行上下文 / 浏览器 workspaceId -> 工作区 ----
|
|
698
|
+
const workspaceOfExec = async (exec) => {
|
|
306
699
|
const agent = exec && exec.agent
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
700
|
+
const session = agent && agent.session
|
|
701
|
+
const cwd = session && session.header && session.header.cwd
|
|
702
|
+
if (typeof cwd !== 'string' || !cwd) return null
|
|
703
|
+
const registry = getWorkspaceRegistry()
|
|
704
|
+
if (registry) {
|
|
705
|
+
try {
|
|
706
|
+
const workspace = await registry.resolveByPath(cwd)
|
|
707
|
+
if (workspace) return workspace
|
|
708
|
+
} catch (err) {
|
|
709
|
+
console.log('dsh-kanban: 解析工作区失败:' + ((err && err.message) || err))
|
|
317
710
|
}
|
|
318
711
|
}
|
|
319
|
-
|
|
712
|
+
// 未登记到 WorkspaceRegistry 的会话仍以自身 cwd 作为工作区根目录。
|
|
713
|
+
return { id: 'cwd:' + cwd, path: cwd, title: cwd }
|
|
714
|
+
}
|
|
715
|
+
const workspaceOfId = (id) => {
|
|
716
|
+
const registry = getWorkspaceRegistry()
|
|
717
|
+
return registry && typeof registry.get === 'function' ? registry.get(id) : undefined
|
|
320
718
|
}
|
|
719
|
+
const emptyBoardSummary = { columns: [], labels: [], cards: [] }
|
|
321
720
|
|
|
322
721
|
const runTool = async (method, args, exec) => {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
722
|
+
const workspace = await workspaceOfExec(exec)
|
|
723
|
+
if (!workspace) {
|
|
724
|
+
return {
|
|
725
|
+
ok: false,
|
|
726
|
+
message: 'Cannot determine the current workspace from the tool execution context',
|
|
727
|
+
board: emptyBoardSummary,
|
|
728
|
+
warnings: [],
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const session = exec && exec.agent && exec.agent.session
|
|
732
|
+
const r = await dispatch(workspace, method, args, 'agent', session)
|
|
733
|
+
return {
|
|
734
|
+
ok: !r.error,
|
|
735
|
+
message: r.error || r.message || 'Done',
|
|
736
|
+
board: summaryOfClone(r.board),
|
|
737
|
+
warnings: Array.isArray(r.warnings) ? r.warnings : [],
|
|
738
|
+
}
|
|
326
739
|
}
|
|
327
740
|
|
|
328
741
|
// ---- 浏览器数据层:经官方 webServer 扩展点注册 /api/kanban ----
|
|
@@ -334,8 +747,14 @@ export function apply(ctx) {
|
|
|
334
747
|
const body = raw ? JSON.parse(raw) : {}
|
|
335
748
|
const method = typeof body.method === 'string' ? body.method : 'get'
|
|
336
749
|
const args = body.args || {}
|
|
337
|
-
const
|
|
338
|
-
const
|
|
750
|
+
const workspaceId = typeof args.workspaceId === 'string' ? args.workspaceId : ''
|
|
751
|
+
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
|
+
}
|
|
757
|
+
const result = await dispatch(workspace, method, args, 'human')
|
|
339
758
|
res.writeHead(200, { 'content-type': 'application/json' })
|
|
340
759
|
res.end(JSON.stringify(result))
|
|
341
760
|
} catch (err) {
|
|
@@ -364,6 +783,7 @@ export function apply(ctx) {
|
|
|
364
783
|
routeState.timer = timer.interval(() => {
|
|
365
784
|
routeState.attempts++
|
|
366
785
|
registerRoute()
|
|
786
|
+
maybeStartupCheck()
|
|
367
787
|
if (routeState.registered || routeState.attempts >= 40) {
|
|
368
788
|
if (routeState.timer) routeState.timer()
|
|
369
789
|
}
|
|
@@ -371,6 +791,78 @@ export function apply(ctx) {
|
|
|
371
791
|
}
|
|
372
792
|
}
|
|
373
793
|
|
|
794
|
+
// ---- 启动校验:逐个工作区体检 .dsh-kanban.json(只读 + 备份损坏文件)----
|
|
795
|
+
const startupState = { done: false }
|
|
796
|
+
const maybeStartupCheck = () => {
|
|
797
|
+
if (startupState.done) return
|
|
798
|
+
const registry = getWorkspaceRegistry()
|
|
799
|
+
if (!getFs() || !registry || typeof registry.list !== 'function') return
|
|
800
|
+
startupState.done = true
|
|
801
|
+
runStartupCheck()
|
|
802
|
+
}
|
|
803
|
+
const runStartupCheck = async () => {
|
|
804
|
+
const fs = getFs()
|
|
805
|
+
const registry = getWorkspaceRegistry()
|
|
806
|
+
if (!fs || !registry || typeof registry.list !== 'function') return
|
|
807
|
+
try {
|
|
808
|
+
const workspaces = registry.list()
|
|
809
|
+
let found = 0
|
|
810
|
+
let corrupt = 0
|
|
811
|
+
let pendingUpgrade = 0
|
|
812
|
+
let unsupported = 0
|
|
813
|
+
let ok = 0
|
|
814
|
+
for (const workspace of workspaces) {
|
|
815
|
+
const key = workspaceKey(workspace)
|
|
816
|
+
try {
|
|
817
|
+
const target = await fs.resolve(BOARD_FILE, { cwd: workspace.path })
|
|
818
|
+
const text = await fs.readText(target)
|
|
819
|
+
found++
|
|
820
|
+
const parsed = parseBoardText(text)
|
|
821
|
+
if (parsed.ok) {
|
|
822
|
+
if (parsed.migrated) {
|
|
823
|
+
pendingUpgrade++
|
|
824
|
+
console.log('dsh-kanban: 启动校验 ' + key + ':schemaVersion ' + parsed.fromVersion + ',首次打开时将自动升级')
|
|
825
|
+
} else {
|
|
826
|
+
ok++
|
|
827
|
+
}
|
|
828
|
+
} else if (parsed.kind === 'unsupported') {
|
|
829
|
+
unsupported++
|
|
830
|
+
console.log('dsh-kanban: 启动校验 ' + key + ':文件由更新版本插件写入(schemaVersion ' + parsed.version + ')')
|
|
831
|
+
} else {
|
|
832
|
+
corrupt++
|
|
833
|
+
const suffix = 'corrupt-' + timestamp()
|
|
834
|
+
const backupTarget = await fs.resolve(BOARD_FILE + '.' + suffix, { cwd: workspace.path })
|
|
835
|
+
await fs.writeText(backupTarget, text, undefined, undefined, writePolicyFor(workspace))
|
|
836
|
+
console.log('dsh-kanban: 启动校验 ' + key + ':数据文件损坏(' + parsed.kind + '),已备份为 ' + BOARD_FILE + '.' + suffix)
|
|
837
|
+
}
|
|
838
|
+
} catch (err) {
|
|
839
|
+
if (!err || (err.code !== 'ENOENT' && err.message !== 'ENOENT')) {
|
|
840
|
+
console.log('dsh-kanban: 启动校验 ' + key + ':检查失败 ' + ((err && err.message) || err))
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (found === 0) {
|
|
845
|
+
console.log('dsh-kanban: 启动校验完成,未发现看板数据文件')
|
|
846
|
+
return
|
|
847
|
+
}
|
|
848
|
+
console.log(
|
|
849
|
+
'dsh-kanban: 启动校验完成:共 ' +
|
|
850
|
+
found +
|
|
851
|
+
' 个看板文件,正常 ' +
|
|
852
|
+
ok +
|
|
853
|
+
',损坏并已备份 ' +
|
|
854
|
+
corrupt +
|
|
855
|
+
',待自动升级 ' +
|
|
856
|
+
pendingUpgrade +
|
|
857
|
+
',版本超前 ' +
|
|
858
|
+
unsupported,
|
|
859
|
+
)
|
|
860
|
+
} catch (err) {
|
|
861
|
+
console.log('dsh-kanban: 启动校验失败:' + ((err && err.message) || err))
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
maybeStartupCheck()
|
|
865
|
+
|
|
374
866
|
// ---- 工具注册 ----
|
|
375
867
|
const resultSchema = {
|
|
376
868
|
type: 'object',
|
|
@@ -378,13 +870,18 @@ export function apply(ctx) {
|
|
|
378
870
|
ok: { type: 'boolean' },
|
|
379
871
|
message: { type: 'string' },
|
|
380
872
|
board: { type: 'object' },
|
|
873
|
+
warnings: { type: 'array', items: { type: 'string' } },
|
|
381
874
|
},
|
|
382
875
|
required: ['ok', 'message'],
|
|
383
876
|
additionalProperties: false,
|
|
384
877
|
}
|
|
385
878
|
const renderBoard = (value) => {
|
|
386
879
|
const b = value && value.board
|
|
387
|
-
const lines = [
|
|
880
|
+
const lines = []
|
|
881
|
+
if (Array.isArray(value && value.warnings)) {
|
|
882
|
+
for (const w of value.warnings) lines.push('⚠ ' + w)
|
|
883
|
+
}
|
|
884
|
+
lines.push(String((value && value.message) || ''))
|
|
388
885
|
if (b && Array.isArray(b.columns)) {
|
|
389
886
|
lines.push('Board state:')
|
|
390
887
|
for (const col of b.columns) {
|
|
@@ -435,13 +932,18 @@ export function apply(ctx) {
|
|
|
435
932
|
ok: { type: 'boolean' },
|
|
436
933
|
message: { type: 'string' },
|
|
437
934
|
card: { type: 'object' },
|
|
935
|
+
warnings: { type: 'array', items: { type: 'string' } },
|
|
438
936
|
},
|
|
439
937
|
required: ['ok', 'message'],
|
|
440
938
|
additionalProperties: false,
|
|
441
939
|
},
|
|
442
940
|
render: (args, value) => {
|
|
443
941
|
const c = value && value.card
|
|
444
|
-
const lines = [
|
|
942
|
+
const lines = []
|
|
943
|
+
if (Array.isArray(value && value.warnings)) {
|
|
944
|
+
for (const w of value.warnings) lines.push('⚠ ' + w)
|
|
945
|
+
}
|
|
946
|
+
lines.push(String((value && value.message) || ''))
|
|
445
947
|
if (c) {
|
|
446
948
|
lines.push('[' + c.id + '] ' + c.title)
|
|
447
949
|
if (c.priority) lines.push('Priority: ' + c.priority)
|
|
@@ -452,10 +954,12 @@ export function apply(ctx) {
|
|
|
452
954
|
},
|
|
453
955
|
},
|
|
454
956
|
async execute(args, exec) {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
957
|
+
const workspace = await workspaceOfExec(exec)
|
|
958
|
+
if (!workspace) return { ok: false, message: 'Cannot determine the current workspace from the tool execution context', warnings: [] }
|
|
959
|
+
const session = exec && exec.agent && exec.agent.session
|
|
960
|
+
const r = await dispatch(workspace, 'getCard', args, 'agent', session)
|
|
961
|
+
if (r.error) return { ok: false, message: r.error, warnings: Array.isArray(r.warnings) ? r.warnings : [] }
|
|
962
|
+
return { ok: true, message: 'Card ' + String(args.id || '') + ' details', card: r.card, warnings: Array.isArray(r.warnings) ? r.warnings : [] }
|
|
459
963
|
},
|
|
460
964
|
},
|
|
461
965
|
{
|
|
@@ -641,13 +1145,18 @@ export function apply(ctx) {
|
|
|
641
1145
|
ok: { type: 'boolean' },
|
|
642
1146
|
message: { type: 'string' },
|
|
643
1147
|
labels: { type: 'array', items: { type: 'object' } },
|
|
1148
|
+
warnings: { type: 'array', items: { type: 'string' } },
|
|
644
1149
|
},
|
|
645
1150
|
required: ['ok', 'message'],
|
|
646
1151
|
additionalProperties: false,
|
|
647
1152
|
},
|
|
648
1153
|
render: (args, value) => {
|
|
649
1154
|
const labels = value && value.labels
|
|
650
|
-
const lines = [
|
|
1155
|
+
const lines = []
|
|
1156
|
+
if (Array.isArray(value && value.warnings)) {
|
|
1157
|
+
for (const w of value.warnings) lines.push('⚠ ' + w)
|
|
1158
|
+
}
|
|
1159
|
+
lines.push(String((value && value.message) || ''))
|
|
651
1160
|
if (Array.isArray(labels)) {
|
|
652
1161
|
for (const l of labels) lines.push('- ' + l.name + ' (' + l.color + ')')
|
|
653
1162
|
}
|
|
@@ -655,10 +1164,17 @@ export function apply(ctx) {
|
|
|
655
1164
|
},
|
|
656
1165
|
},
|
|
657
1166
|
async execute(args, exec) {
|
|
658
|
-
const
|
|
659
|
-
|
|
1167
|
+
const workspace = await workspaceOfExec(exec)
|
|
1168
|
+
if (!workspace) return { ok: false, message: 'Cannot determine the current workspace from the tool execution context', labels: [], warnings: [] }
|
|
1169
|
+
const session = exec && exec.agent && exec.agent.session
|
|
1170
|
+
const r = await dispatch(workspace, 'get', args, 'agent', session)
|
|
660
1171
|
const labels = (r.board && r.board.labels) || []
|
|
661
|
-
return {
|
|
1172
|
+
return {
|
|
1173
|
+
ok: true,
|
|
1174
|
+
message: 'Labels (workspace ' + workspaceKey(workspace) + ')',
|
|
1175
|
+
labels,
|
|
1176
|
+
warnings: Array.isArray(r.warnings) ? r.warnings : [],
|
|
1177
|
+
}
|
|
662
1178
|
},
|
|
663
1179
|
},
|
|
664
1180
|
]
|