cs-bots 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.
- package/LICENSE +21 -0
- package/README.md +164 -2
- package/cordis.patch.agent-team.yml +31 -0
- package/cordis.patch.yml +30 -0
- package/install.sh +218 -0
- package/package.json +57 -4
- package/src/api.js +706 -0
- package/src/board.js +1056 -0
- package/src/botprofile.js +331 -0
- package/src/client.js +2089 -0
- package/src/compat.js +196 -0
- package/src/dirbrowse.js +555 -0
- package/src/dispatcher.js +651 -0
- package/src/index.js +311 -0
- package/src/kernel.js +480 -0
- package/src/library.js +740 -0
- package/src/memory.js +1905 -0
- package/src/registry.js +34 -0
- package/src/room.js +1550 -0
- package/src/roster.js +1119 -0
- package/src/settings.js +107 -0
- package/src/store.js +243 -0
- package/src/tools.js +469 -0
package/src/api.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cs-bots /api —— 宿主 HTTP 面(**给 L3 页面层用的数据接口**)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:L2 域服务是宿主对象,浏览器里的客户端拿不到;
|
|
5
|
+
* 页面必须有一条**稳定的读/写接口**。本文件就是那条接口。
|
|
6
|
+
*
|
|
7
|
+
* 设计纪律:
|
|
8
|
+
* - **零第三方依赖**:只用 node 内置 + kernel.registerRoute。
|
|
9
|
+
* - **不碰宿主内部实现**:所有业务都转调域服务(board/roster/room/memory/dispatcher)。
|
|
10
|
+
* - **缺服务降级**:域服务缺失时返回 503 + 可读原因(不是 500 崩)。
|
|
11
|
+
* - **写操作必须回显成败**:让页面能显示"为什么失败",不静默。
|
|
12
|
+
* - **前缀路由**:`/cs-bots/api`(与包名一致,避免和其他插件撞路径)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { registerRoute, listPresets, defaultPreset } from './kernel.js'
|
|
16
|
+
import { createLibrary } from './library.js'
|
|
17
|
+
import { createDirBrowser } from './dirbrowse.js'
|
|
18
|
+
import { getServices } from './registry.js'
|
|
19
|
+
|
|
20
|
+
/** 请求体上限(防大包打爆内存)。 */
|
|
21
|
+
const MAX_BODY = 2 * 1024 * 1024
|
|
22
|
+
|
|
23
|
+
/** API 前缀。 */
|
|
24
|
+
export const API_BASE = '/cs-bots/api'
|
|
25
|
+
|
|
26
|
+
/** 读请求体(JSON)。 */
|
|
27
|
+
async function readBody(request) {
|
|
28
|
+
const chunks = []
|
|
29
|
+
let size = 0
|
|
30
|
+
for await (const chunk of request) {
|
|
31
|
+
size += chunk.length
|
|
32
|
+
if (size > MAX_BODY) throw new Error('请求体过大(>2MB)')
|
|
33
|
+
chunks.push(chunk)
|
|
34
|
+
}
|
|
35
|
+
if (!chunks.length) return undefined
|
|
36
|
+
const text = Buffer.concat(chunks).toString('utf8').trim()
|
|
37
|
+
return text ? JSON.parse(text) : undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 回 JSON(不缓存)。 */
|
|
41
|
+
function sendJson(response, code, body) {
|
|
42
|
+
response.writeHead(code, {
|
|
43
|
+
'content-type': 'application/json; charset=utf-8',
|
|
44
|
+
'cache-control': 'no-store',
|
|
45
|
+
})
|
|
46
|
+
response.end(JSON.stringify(body ?? null))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 从查询串取字符串参数。 */
|
|
50
|
+
function qp(url, name) {
|
|
51
|
+
const v = url.searchParams.get(name)
|
|
52
|
+
return v === null ? undefined : v
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 从查询串取数字参数。 */
|
|
56
|
+
function qn(url, name) {
|
|
57
|
+
const v = url.searchParams.get(name)
|
|
58
|
+
if (v === null || v === '') return undefined
|
|
59
|
+
const n = Number(v)
|
|
60
|
+
return Number.isFinite(n) ? n : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 统一包装:域服务缺失 → 503;业务异常 → 400 带可读原因;其它 → 500。
|
|
65
|
+
* @param {object} services
|
|
66
|
+
* @param {string} key
|
|
67
|
+
* @param {(svc: object) => Promise<object>|object} fn
|
|
68
|
+
*/
|
|
69
|
+
async function withService(services, key, fn) {
|
|
70
|
+
const svc = services[key]
|
|
71
|
+
if (!svc) {
|
|
72
|
+
const error = new Error(`${key} 服务未装配(检查状态目录是否可写、宿主版本是否匹配)`)
|
|
73
|
+
error.status = 503
|
|
74
|
+
error.code = `${String(key).toUpperCase()}_UNAVAILABLE`
|
|
75
|
+
throw error
|
|
76
|
+
}
|
|
77
|
+
return fn(svc)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 把 Bot 档案里的「预制记忆」同步进记忆服务(带 botId)。
|
|
83
|
+
* 建 Bot 与改档案两处都要调用 —— 否则会出现"AGENTS.md 有、记忆库里没有"的不一致。
|
|
84
|
+
* @param {object} services
|
|
85
|
+
* @param {{ id: string, profile?: object }} bot
|
|
86
|
+
* @param {string} by
|
|
87
|
+
* @returns {{ added: number, failed: object[] } | undefined}
|
|
88
|
+
*/
|
|
89
|
+
function seedBotMemory(services, bot, by) {
|
|
90
|
+
const seed = Array.isArray(bot?.profile?.memory) ? bot.profile.memory : []
|
|
91
|
+
if (!services.memory || seed.length === 0) return undefined
|
|
92
|
+
const added = []
|
|
93
|
+
const failed = []
|
|
94
|
+
for (const content of seed) {
|
|
95
|
+
try {
|
|
96
|
+
const entry = services.memory.add({
|
|
97
|
+
layer: 'L2',
|
|
98
|
+
content: String(content).slice(0, 600),
|
|
99
|
+
by,
|
|
100
|
+
source: 'explicit',
|
|
101
|
+
botId: bot.id,
|
|
102
|
+
})
|
|
103
|
+
const id = Array.isArray(entry) ? entry[0]?.id : entry?.id
|
|
104
|
+
if (id) added.push(id)
|
|
105
|
+
} catch (error) {
|
|
106
|
+
failed.push({ content: String(content).slice(0, 40), reason: error?.message ?? String(error) })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { added: added.length, failed }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 注册全部 API 路由。
|
|
114
|
+
*
|
|
115
|
+
* @param {object} ctx 宿主上下文
|
|
116
|
+
* @param {{ warn?: Function }} [deps]
|
|
117
|
+
* @returns {(() => void) | undefined} disposer
|
|
118
|
+
*/
|
|
119
|
+
export function installApi(ctx, { warn } = {}) {
|
|
120
|
+
const handler = async (request, response) => {
|
|
121
|
+
const services = getServices(ctx)
|
|
122
|
+
let url
|
|
123
|
+
try {
|
|
124
|
+
url = new URL(request.url || '/', 'http://localhost')
|
|
125
|
+
} catch {
|
|
126
|
+
sendJson(response, 400, { ok: false, reason: 'bad-url' })
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
// 去掉前缀,得到相对路径
|
|
130
|
+
const path = url.pathname.startsWith(API_BASE) ? url.pathname.slice(API_BASE.length) || '/' : url.pathname
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
// ── 读:状态总览 ──────────────────────────────────────────────────────
|
|
134
|
+
if (request.method === 'GET' && path === '/status') {
|
|
135
|
+
const out = { ok: true, services: {}, compat: services.compat ?? null }
|
|
136
|
+
for (const key of ['roster', 'board', 'dispatcher', 'room', 'memory']) {
|
|
137
|
+
const svc = services[key]
|
|
138
|
+
out.services[key] = svc
|
|
139
|
+
? {
|
|
140
|
+
available: true,
|
|
141
|
+
stats: typeof svc.stats === 'function' ? svc.stats() : undefined,
|
|
142
|
+
stateDir: svc.paths?.stateDir,
|
|
143
|
+
}
|
|
144
|
+
: { available: false }
|
|
145
|
+
}
|
|
146
|
+
sendJson(response, 200, out)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── 任务板 ────────────────────────────────────────────────────────────
|
|
151
|
+
if (request.method === 'GET' && path === '/tasks') {
|
|
152
|
+
const status = qp(url, 'status')
|
|
153
|
+
const onlyReady = qp(url, 'ready') === '1'
|
|
154
|
+
const tasks = await withService(services, 'board', (b) =>
|
|
155
|
+
onlyReady ? b.ready() : b.list(status ? { status } : {}),
|
|
156
|
+
)
|
|
157
|
+
sendJson(response, 200, { ok: true, tasks })
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (request.method === 'POST' && path === '/tasks') {
|
|
162
|
+
const body = (await readBody(request)) ?? {}
|
|
163
|
+
const task = await withService(services, 'board', (b) =>
|
|
164
|
+
b.create({ ...body, byId: body.byId ?? 'ui' }),
|
|
165
|
+
)
|
|
166
|
+
sendJson(response, 200, { ok: true, task })
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (request.method === 'POST' && path === '/tasks/update') {
|
|
171
|
+
const body = (await readBody(request)) ?? {}
|
|
172
|
+
const task = await withService(services, 'board', (b) =>
|
|
173
|
+
b.update({ ...body, byId: body.byId ?? 'ui' }),
|
|
174
|
+
)
|
|
175
|
+
sendJson(response, 200, { ok: true, task })
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (request.method === 'POST' && path === '/tasks/accept') {
|
|
180
|
+
const body = (await readBody(request)) ?? {}
|
|
181
|
+
const task = await withService(services, 'board', (b) =>
|
|
182
|
+
b.accept({ ...body, by: body.by ?? 'ui' }),
|
|
183
|
+
)
|
|
184
|
+
sendJson(response, 200, { ok: true, task })
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── agent 预设(建 Bot 时可选)─────────────────────────────────────────
|
|
189
|
+
if (request.method === 'GET' && path === '/presets') {
|
|
190
|
+
let presets = []
|
|
191
|
+
let defaultId
|
|
192
|
+
try {
|
|
193
|
+
presets = await listPresets(ctx)
|
|
194
|
+
defaultId = defaultPreset(ctx)
|
|
195
|
+
} catch (error) {
|
|
196
|
+
sendJson(response, 200, { ok: true, presets: [], defaultId: undefined, note: `预设服务不可用:${error?.message ?? error}` })
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
sendJson(response, 200, {
|
|
200
|
+
ok: true,
|
|
201
|
+
presets: presets.map((p) => ({
|
|
202
|
+
id: p.id,
|
|
203
|
+
name: p.name ?? p.id,
|
|
204
|
+
description: p.description,
|
|
205
|
+
order: p.order,
|
|
206
|
+
broken: p.broken,
|
|
207
|
+
})),
|
|
208
|
+
defaultId,
|
|
209
|
+
note: presets.length ? undefined : '宿主未提供 agentPresets 服务(Bot 将使用默认预设)',
|
|
210
|
+
})
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Bot 注册表 ────────────────────────────────────────────────────────
|
|
215
|
+
if (request.method === 'GET' && path === '/roster') {
|
|
216
|
+
const includeRetired = qp(url, 'includeRetired') === '1'
|
|
217
|
+
const bots = await withService(services, 'roster', (r) => [
|
|
218
|
+
...r.list(),
|
|
219
|
+
...(includeRetired ? r.listRetired() : []),
|
|
220
|
+
])
|
|
221
|
+
sendJson(response, 200, { ok: true, bots })
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (request.method === 'POST' && path === '/roster') {
|
|
226
|
+
const body = (await readBody(request)) ?? {}
|
|
227
|
+
const bot = await withService(services, 'roster', (r) =>
|
|
228
|
+
r.create({ ...body, byId: body.byId ?? 'ui' }),
|
|
229
|
+
)
|
|
230
|
+
// 预制记忆同步(建 Bot 与改档案必须一致)
|
|
231
|
+
const memory = seedBotMemory(services, bot, body.byId ?? 'ui')
|
|
232
|
+
// 同目录共享检测:**允许**,但必须告知(两个 Bot 在同一目录会互相覆盖文件)
|
|
233
|
+
let sharedWith = []
|
|
234
|
+
try {
|
|
235
|
+
const all = services.roster?.list?.() ?? []
|
|
236
|
+
sharedWith = all
|
|
237
|
+
.filter((b) => b.id !== bot.id && b.state !== 'retired' && String(b.cwd) === String(bot.cwd))
|
|
238
|
+
.map((b) => ({ id: b.id, name: b.name }))
|
|
239
|
+
} catch {
|
|
240
|
+
/* 检测失败不影响创建 */
|
|
241
|
+
}
|
|
242
|
+
sendJson(response, 200, {
|
|
243
|
+
ok: true,
|
|
244
|
+
bot,
|
|
245
|
+
...(memory ? { memory } : {}),
|
|
246
|
+
...(sharedWith.length
|
|
247
|
+
? {
|
|
248
|
+
warnings: [
|
|
249
|
+
`目录 ${bot.cwd} 已被 ${sharedWith.map((b) => b.name).join('、')} 使用 —— 两个 Bot 会互相覆盖文件`,
|
|
250
|
+
],
|
|
251
|
+
sharedWith,
|
|
252
|
+
}
|
|
253
|
+
: {}),
|
|
254
|
+
})
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Bot 档案(身份 / 经验 / 技能 / 预制记忆)──────────────────────────
|
|
259
|
+
if (request.method === 'GET' && path === '/roster/profile') {
|
|
260
|
+
const id = qp(url, 'id')
|
|
261
|
+
if (!id) {
|
|
262
|
+
sendJson(response, 400, { ok: false, reason: 'id 必填' })
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
const detail = await withService(services, 'roster', (r) => r.getProfile(id))
|
|
266
|
+
sendJson(response, 200, {
|
|
267
|
+
ok: true,
|
|
268
|
+
bot: detail.bot,
|
|
269
|
+
profile: detail.profile ?? {},
|
|
270
|
+
profilePath: detail.profilePath,
|
|
271
|
+
managedText: detail.managedText,
|
|
272
|
+
hasOutside: detail.hasOutside,
|
|
273
|
+
profileError: detail.profileError,
|
|
274
|
+
})
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (request.method === 'POST' && path === '/roster/profile') {
|
|
279
|
+
const body = (await readBody(request)) ?? {}
|
|
280
|
+
if (!body.id) {
|
|
281
|
+
sendJson(response, 400, { ok: false, reason: 'id 必填' })
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
const bot = await withService(services, 'roster', (r) =>
|
|
285
|
+
r.setProfile({ id: body.id, profile: body.profile ?? {}, byId: body.byId ?? 'ui', expectedRev: body.expectedRev }),
|
|
286
|
+
)
|
|
287
|
+
// 预制记忆同步(与建 Bot 共用同一段逻辑)
|
|
288
|
+
const memory = seedBotMemory(services, bot, body.byId ?? 'ui')
|
|
289
|
+
sendJson(response, 200, { ok: true, bot, ...(memory ? { memory } : {}) })
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (request.method === 'POST' && path === '/roster/manage') {
|
|
294
|
+
const body = (await readBody(request)) ?? {}
|
|
295
|
+
const byId = body.byId ?? 'ui'
|
|
296
|
+
const result = await withService(services, 'roster', (r) => {
|
|
297
|
+
switch (body.action) {
|
|
298
|
+
case 'rename':
|
|
299
|
+
return r.rename({ id: body.id, name: body.name, expectedRev: body.expectedRev, byId })
|
|
300
|
+
case 'retire':
|
|
301
|
+
return r.retire({ id: body.id, reason: body.reason, byId })
|
|
302
|
+
case 'setCwd':
|
|
303
|
+
return r.setCwd({ id: body.id, cwd: body.cwd, expectedRev: body.expectedRev, byId })
|
|
304
|
+
case 'claim':
|
|
305
|
+
return r.claim({ id: body.id, owner: byId, expectedRev: body.expectedRev })
|
|
306
|
+
case 'release':
|
|
307
|
+
return r.release({ id: body.id, owner: byId, expectedRev: body.expectedRev })
|
|
308
|
+
case 'heartbeat':
|
|
309
|
+
return r.heartbeat({ id: body.id, owner: byId, expectedRev: body.expectedRev })
|
|
310
|
+
default:
|
|
311
|
+
throw new Error(`未知 action: ${body.action}`)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
sendJson(response, 200, { ok: true, bot: result })
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── 房间 ──────────────────────────────────────────────────────────────
|
|
319
|
+
if (request.method === 'GET' && path === '/rooms') {
|
|
320
|
+
const rooms = await withService(services, 'room', (r) => r.list())
|
|
321
|
+
sendJson(response, 200, { ok: true, rooms })
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (request.method === 'POST' && path === '/rooms') {
|
|
326
|
+
const body = (await readBody(request)) ?? {}
|
|
327
|
+
const room = await withService(services, 'room', (r) =>
|
|
328
|
+
r.create({ ...body, byId: body.byId ?? 'ui' }),
|
|
329
|
+
)
|
|
330
|
+
sendJson(response, 200, { ok: true, room })
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (request.method === 'GET' && path === '/rooms/messages') {
|
|
335
|
+
const roomId = qp(url, 'roomId')
|
|
336
|
+
if (!roomId) {
|
|
337
|
+
sendJson(response, 400, { ok: false, reason: 'roomId 必填' })
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
const messages = await withService(services, 'room', (r) =>
|
|
341
|
+
r.listMessages({ roomId, sinceSeq: qn(url, 'sinceSeq'), limit: qn(url, 'limit') }),
|
|
342
|
+
)
|
|
343
|
+
sendJson(response, 200, { ok: true, messages })
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (request.method === 'POST' && path === '/rooms/post') {
|
|
348
|
+
const body = (await readBody(request)) ?? {}
|
|
349
|
+
const message = await withService(services, 'room', (r) =>
|
|
350
|
+
r.post({ ...body, from: body.from ?? 'ui' }),
|
|
351
|
+
)
|
|
352
|
+
sendJson(response, 200, { ok: true, message })
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (request.method === 'GET' && path === '/rooms/body') {
|
|
357
|
+
const roomId = qp(url, 'roomId')
|
|
358
|
+
const messageId = qp(url, 'messageId')
|
|
359
|
+
if (!roomId || !messageId) {
|
|
360
|
+
sendJson(response, 400, { ok: false, reason: 'roomId 与 messageId 必填' })
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
const text = await withService(services, 'room', (r) => r.readBody({ roomId, messageId }))
|
|
364
|
+
sendJson(response, 200, { ok: true, text })
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── 记忆 ──────────────────────────────────────────────────────────────
|
|
369
|
+
if (request.method === 'GET' && path === '/memory') {
|
|
370
|
+
const layer = qp(url, 'layer')
|
|
371
|
+
const query = qp(url, 'query')
|
|
372
|
+
const entries = await withService(services, 'memory', (m) =>
|
|
373
|
+
query ? m.search({ query, layers: layer ? [layer] : undefined, limit: qn(url, 'limit') })
|
|
374
|
+
: m.list({ layer, limit: qn(url, 'limit') }),
|
|
375
|
+
)
|
|
376
|
+
sendJson(response, 200, { ok: true, entries })
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (request.method === 'POST' && path === '/memory') {
|
|
381
|
+
const body = (await readBody(request)) ?? {}
|
|
382
|
+
const entry = await withService(services, 'memory', (m) =>
|
|
383
|
+
m.add({ ...body, by: body.by ?? 'ui' }),
|
|
384
|
+
)
|
|
385
|
+
sendJson(response, 200, { ok: true, entry })
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (request.method === 'GET' && path === '/memory/context') {
|
|
390
|
+
const text = await withService(services, 'memory', (m) => m.contextFor({ branch: qp(url, 'branch') }))
|
|
391
|
+
sendJson(response, 200, { ok: true, text })
|
|
392
|
+
return
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── 任务:详情穿透 / 指派 / 终止 / 编辑 ──────────────────────────────
|
|
396
|
+
if (request.method === 'GET' && path === '/tasks/detail') {
|
|
397
|
+
const id = qp(url, 'id')
|
|
398
|
+
if (!id) {
|
|
399
|
+
sendJson(response, 400, { ok: false, reason: 'id 必填' })
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
const detail = await withService(services, 'board', (b) => ({
|
|
403
|
+
task: b.get(id),
|
|
404
|
+
history: typeof b.history === 'function' ? b.history(id) : [],
|
|
405
|
+
}))
|
|
406
|
+
if (!detail.task) {
|
|
407
|
+
sendJson(response, 404, { ok: false, reason: `任务 ${id} 不存在` })
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
sendJson(response, 200, { ok: true, ...detail })
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (request.method === 'POST' && path === '/tasks/assign') {
|
|
415
|
+
const body = (await readBody(request)) ?? {}
|
|
416
|
+
if (!body.id || !body.ownerId) {
|
|
417
|
+
sendJson(response, 400, { ok: false, reason: 'id 与 ownerId 必填' })
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
// 指派 = 用 ownerId 作为 claim 的 byId(board 的 claim 把 by 记为 owner)
|
|
421
|
+
const task = await withService(services, 'board', (b) =>
|
|
422
|
+
b.update({
|
|
423
|
+
id: body.id,
|
|
424
|
+
expectedRev: body.expectedRev,
|
|
425
|
+
action: 'claim',
|
|
426
|
+
byId: body.ownerId,
|
|
427
|
+
...(body.leaseMs ? { leaseMs: body.leaseMs } : {}),
|
|
428
|
+
}),
|
|
429
|
+
)
|
|
430
|
+
// 指派后立刻定向唤醒(dedupKey 幂等:同任务同原因只唤一次)
|
|
431
|
+
let wake
|
|
432
|
+
if (services.dispatcher && body.wake !== false) {
|
|
433
|
+
wake = services.dispatcher.notify({
|
|
434
|
+
target: body.ownerId,
|
|
435
|
+
reason: 'task-assigned',
|
|
436
|
+
dedupKey: `task-assigned:${task.id}:${task.rev}`,
|
|
437
|
+
text: `新任务 ${task.id}:${task.subject}\n${task.description ?? ''}`.trim(),
|
|
438
|
+
})
|
|
439
|
+
}
|
|
440
|
+
sendJson(response, 200, { ok: true, task, wake })
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (request.method === 'POST' && path === '/tasks/abort') {
|
|
445
|
+
const body = (await readBody(request)) ?? {}
|
|
446
|
+
if (!body.id) {
|
|
447
|
+
sendJson(response, 400, { ok: false, reason: 'id 必填' })
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
// 终止 = block + 原因(board 的 block 会置 blocked 并留 reason;可 reopen)
|
|
451
|
+
const task = await withService(services, 'board', (b) =>
|
|
452
|
+
b.update({
|
|
453
|
+
id: body.id,
|
|
454
|
+
expectedRev: body.expectedRev,
|
|
455
|
+
action: 'block',
|
|
456
|
+
note: body.reason ?? '用户终止',
|
|
457
|
+
byId: body.byId ?? 'ui',
|
|
458
|
+
}),
|
|
459
|
+
)
|
|
460
|
+
sendJson(response, 200, { ok: true, task })
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (request.method === 'POST' && path === '/tasks/edit') {
|
|
465
|
+
const body = (await readBody(request)) ?? {}
|
|
466
|
+
if (!body.id) {
|
|
467
|
+
sendJson(response, 400, { ok: false, reason: 'id 必填' })
|
|
468
|
+
return
|
|
469
|
+
}
|
|
470
|
+
const task = await withService(services, 'board', (b) =>
|
|
471
|
+
b.update({ ...body, action: 'edit', byId: body.byId ?? 'ui' }),
|
|
472
|
+
)
|
|
473
|
+
sendJson(response, 200, { ok: true, task })
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (request.method === 'GET' && path === '/tasks/history') {
|
|
478
|
+
const id = qp(url, 'id')
|
|
479
|
+
const history = await withService(services, 'board', (b) =>
|
|
480
|
+
typeof b.history === 'function' ? b.history(id) : [],
|
|
481
|
+
)
|
|
482
|
+
sendJson(response, 200, { ok: true, history })
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── 房间:成员管理 / 转发 ─────────────────────────────────────────────
|
|
487
|
+
if (request.method === 'POST' && path === '/rooms/update') {
|
|
488
|
+
const body = (await readBody(request)) ?? {}
|
|
489
|
+
const room = await withService(services, 'room', (r) =>
|
|
490
|
+
r.update({ ...body, byId: body.byId ?? 'ui' }),
|
|
491
|
+
)
|
|
492
|
+
sendJson(response, 200, { ok: true, room })
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (request.method === 'POST' && path === '/rooms/join') {
|
|
497
|
+
const body = (await readBody(request)) ?? {}
|
|
498
|
+
const room = await withService(services, 'room', (r) =>
|
|
499
|
+
r.join({ roomId: body.roomId, sessionId: body.sessionId, byId: body.byId ?? 'ui' }),
|
|
500
|
+
)
|
|
501
|
+
sendJson(response, 200, { ok: true, room })
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (request.method === 'POST' && path === '/rooms/leave') {
|
|
506
|
+
const body = (await readBody(request)) ?? {}
|
|
507
|
+
const room = await withService(services, 'room', (r) =>
|
|
508
|
+
r.leave({ roomId: body.roomId, sessionId: body.sessionId, byId: body.byId ?? 'ui' }),
|
|
509
|
+
)
|
|
510
|
+
sendJson(response, 200, { ok: true, room })
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (request.method === 'POST' && path === '/rooms/relay') {
|
|
515
|
+
const body = (await readBody(request)) ?? {}
|
|
516
|
+
const result = await withService(services, 'room', (r) =>
|
|
517
|
+
r.relay({ roomId: body.roomId, messageId: body.messageId, targets: body.targets }),
|
|
518
|
+
)
|
|
519
|
+
sendJson(response, 200, { ok: true, ...result })
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── 记忆:改 / 归档 / 恢复 ────────────────────────────────────────────
|
|
524
|
+
if (request.method === 'POST' && path === '/memory/update') {
|
|
525
|
+
const body = (await readBody(request)) ?? {}
|
|
526
|
+
const entry = await withService(services, 'memory', (m) =>
|
|
527
|
+
m.update({ ...body, by: body.by ?? 'ui' }),
|
|
528
|
+
)
|
|
529
|
+
sendJson(response, 200, { ok: true, entry })
|
|
530
|
+
return
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (request.method === 'POST' && path === '/memory/remove') {
|
|
534
|
+
const body = (await readBody(request)) ?? {}
|
|
535
|
+
const entry = await withService(services, 'memory', (m) =>
|
|
536
|
+
m.remove({ ...body, by: body.by ?? 'ui' }),
|
|
537
|
+
)
|
|
538
|
+
sendJson(response, 200, { ok: true, entry })
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (request.method === 'POST' && path === '/memory/archive') {
|
|
543
|
+
const body = (await readBody(request)) ?? {}
|
|
544
|
+
const entry = await withService(services, 'memory', (m) =>
|
|
545
|
+
m.archive({ ...body, by: body.by ?? 'ui' }),
|
|
546
|
+
)
|
|
547
|
+
sendJson(response, 200, { ok: true, entry })
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (request.method === 'POST' && path === '/memory/restore') {
|
|
552
|
+
const body = (await readBody(request)) ?? {}
|
|
553
|
+
const entry = await withService(services, 'memory', (m) =>
|
|
554
|
+
m.restore({ ...body, by: body.by ?? 'ui' }),
|
|
555
|
+
)
|
|
556
|
+
sendJson(response, 200, { ok: true, entry })
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// ── 目录浏览(宿主原生选择器不可用时的兜底,也是唯一能「新建/重命名」的路径)──
|
|
561
|
+
if (request.method === 'GET' && path === '/fs/roots') {
|
|
562
|
+
const r = await withService(services, 'dirbrowse', (d) => d.roots())
|
|
563
|
+
sendJson(response, 200, { ok: true, ...r })
|
|
564
|
+
return
|
|
565
|
+
}
|
|
566
|
+
if (request.method === 'GET' && path === '/fs/list') {
|
|
567
|
+
const dir = qp(url, 'path')
|
|
568
|
+
const showHidden = qp(url, 'showHidden') === '1'
|
|
569
|
+
const listing = await withService(services, 'dirbrowse', (d) =>
|
|
570
|
+
d.list({ ...(dir ? { path: dir } : {}), showHidden }),
|
|
571
|
+
)
|
|
572
|
+
sendJson(response, 200, { ok: true, ...listing })
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
if (request.method === 'POST' && path === '/fs/mkdir') {
|
|
576
|
+
const body = (await readBody(request)) ?? {}
|
|
577
|
+
const created = await withService(services, 'dirbrowse', (d) => d.mkdir(body))
|
|
578
|
+
sendJson(response, 200, { ok: true, ...created })
|
|
579
|
+
return
|
|
580
|
+
}
|
|
581
|
+
if (request.method === 'POST' && path === '/fs/rename') {
|
|
582
|
+
const body = (await readBody(request)) ?? {}
|
|
583
|
+
const renamed = await withService(services, 'dirbrowse', (d) => d.rename(body))
|
|
584
|
+
sendJson(response, 200, { ok: true, ...renamed })
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
if (request.method === 'POST' && path === '/fs/validate') {
|
|
588
|
+
const body = (await readBody(request)) ?? {}
|
|
589
|
+
const verdict = await withService(services, 'dirbrowse', (d) => d.validateAsBotCwd({ path: body.path }))
|
|
590
|
+
sendJson(response, 200, { ok: true, verdict })
|
|
591
|
+
return
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── 技能库 / 工作流库 ─────────────────────────────────────────────────
|
|
595
|
+
if (request.method === 'GET' && path === '/library/skills') {
|
|
596
|
+
const rows = await withService(services, 'library', (l) => l.listSkills({ tag: qp(url, 'tag') }))
|
|
597
|
+
sendJson(response, 200, { ok: true, skills: rows })
|
|
598
|
+
return
|
|
599
|
+
}
|
|
600
|
+
if (request.method === 'POST' && path === '/library/skills') {
|
|
601
|
+
const body = (await readBody(request)) ?? {}
|
|
602
|
+
const skill = await withService(services, 'library', (l) => l.createSkill({ ...body, by: body.by ?? 'ui' }))
|
|
603
|
+
sendJson(response, 200, { ok: true, skill })
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
if (request.method === 'POST' && path === '/library/skills/update') {
|
|
607
|
+
const body = (await readBody(request)) ?? {}
|
|
608
|
+
const skill = await withService(services, 'library', (l) => l.updateSkill({ ...body, by: body.by ?? 'ui' }))
|
|
609
|
+
sendJson(response, 200, { ok: true, skill })
|
|
610
|
+
return
|
|
611
|
+
}
|
|
612
|
+
if (request.method === 'POST' && path === '/library/skills/remove') {
|
|
613
|
+
const body = (await readBody(request)) ?? {}
|
|
614
|
+
const result = await withService(services, 'library', (l) => l.removeSkill({ id: body.id, by: body.by ?? 'ui' }))
|
|
615
|
+
sendJson(response, 200, { ok: true, result })
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
if (request.method === 'GET' && path === '/library/workflows') {
|
|
619
|
+
const rows = await withService(services, 'library', (l) => l.listWorkflows({ tag: qp(url, 'tag') }))
|
|
620
|
+
sendJson(response, 200, { ok: true, workflows: rows })
|
|
621
|
+
return
|
|
622
|
+
}
|
|
623
|
+
if (request.method === 'POST' && path === '/library/workflows') {
|
|
624
|
+
const body = (await readBody(request)) ?? {}
|
|
625
|
+
const workflow = await withService(services, 'library', (l) => l.createWorkflow({ ...body, by: body.by ?? 'ui' }))
|
|
626
|
+
sendJson(response, 200, { ok: true, workflow })
|
|
627
|
+
return
|
|
628
|
+
}
|
|
629
|
+
if (request.method === 'POST' && path === '/library/workflows/update') {
|
|
630
|
+
const body = (await readBody(request)) ?? {}
|
|
631
|
+
const workflow = await withService(services, 'library', (l) => l.updateWorkflow({ ...body, by: body.by ?? 'ui' }))
|
|
632
|
+
sendJson(response, 200, { ok: true, workflow })
|
|
633
|
+
return
|
|
634
|
+
}
|
|
635
|
+
if (request.method === 'POST' && path === '/library/workflows/remove') {
|
|
636
|
+
const body = (await readBody(request)) ?? {}
|
|
637
|
+
const result = await withService(services, 'library', (l) => l.removeWorkflow({ id: body.id, by: body.by ?? 'ui' }))
|
|
638
|
+
sendJson(response, 200, { ok: true, result })
|
|
639
|
+
return
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// ── 设置 ──────────────────────────────────────────────────────────────
|
|
643
|
+
if (request.method === 'GET' && path === '/settings') {
|
|
644
|
+
const { SETTING_SPECS } = await import('./settings.js')
|
|
645
|
+
sendJson(response, 200, {
|
|
646
|
+
ok: true,
|
|
647
|
+
values: services.settings ?? {},
|
|
648
|
+
specs: SETTING_SPECS,
|
|
649
|
+
diagnostics: services.settingsDiagnostics,
|
|
650
|
+
baseDir: services.board?.paths?.stateDir ?? services.roster?.paths?.stateDir,
|
|
651
|
+
})
|
|
652
|
+
return
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (request.method === 'POST' && path === '/settings') {
|
|
656
|
+
const body = (await readBody(request)) ?? {}
|
|
657
|
+
const { saveSettings } = await import('./settings.js')
|
|
658
|
+
const dir = services.board?.paths?.stateDir ?? services.roster?.paths?.stateDir
|
|
659
|
+
const saved = saveSettings(dir, body)
|
|
660
|
+
if (!saved.ok) {
|
|
661
|
+
sendJson(response, 400, { ok: false, reason: saved.reason })
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
sendJson(response, 200, {
|
|
665
|
+
ok: true,
|
|
666
|
+
values: saved.values,
|
|
667
|
+
note: '已写入 settings.json;内核级参数需重启 DSH 生效',
|
|
668
|
+
})
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ── 手动唤醒 ──────────────────────────────────────────────────────────
|
|
673
|
+
if (request.method === 'POST' && path === '/wakes/notify') {
|
|
674
|
+
const body = (await readBody(request)) ?? {}
|
|
675
|
+
const result = await withService(services, 'dispatcher', (d) => d.notify(body))
|
|
676
|
+
sendJson(response, 200, { ok: true, result })
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ── 唤醒审计 ──────────────────────────────────────────────────────────
|
|
681
|
+
if (request.method === 'GET' && path === '/wakes') {
|
|
682
|
+
const wakes = await withService(services, 'dispatcher', (d) =>
|
|
683
|
+
d.list({ target: qp(url, 'target'), limit: qn(url, 'limit') ?? 100 }),
|
|
684
|
+
)
|
|
685
|
+
sendJson(response, 200, { ok: true, wakes })
|
|
686
|
+
return
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
sendJson(response, 404, { ok: false, reason: `未知端点: ${request.method} ${path}` })
|
|
690
|
+
} catch (error) {
|
|
691
|
+
const status = Number.isFinite(error?.status) ? error.status : 400
|
|
692
|
+
sendJson(response, status, {
|
|
693
|
+
ok: false,
|
|
694
|
+
code: error?.code ?? 'API_ERROR',
|
|
695
|
+
reason: error?.message ?? String(error),
|
|
696
|
+
})
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const dispose = registerRoute(ctx, API_BASE, handler)
|
|
701
|
+
if (!dispose) {
|
|
702
|
+
warn?.(`[cs-bots] webServer 不可用 → HTTP API 未注册(页面将无数据)`)
|
|
703
|
+
return undefined
|
|
704
|
+
}
|
|
705
|
+
return dispose
|
|
706
|
+
}
|