c0de-agent 1.13.0 → 1.14.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/dist/cli/commands/serve.js +5 -1
- package/dist/cli/deps.d.ts +2 -4
- package/dist/cli/deps.js +22 -36
- package/dist/cli/modes/print.js +27 -17
- package/dist/core/agent.js +59 -36
- package/dist/core/agents/parallel.js +4 -2
- package/dist/core/config-path.d.ts +3 -1
- package/dist/core/config-path.js +33 -3
- package/dist/core/config.js +81 -0
- package/dist/core/context.js +6 -3
- package/dist/core/loop/persist.js +4 -1
- package/dist/core/loop/subagent.d.ts +19 -0
- package/dist/core/loop/subagent.js +73 -7
- package/dist/core/loop.d.ts +4 -1
- package/dist/core/loop.js +22 -6
- package/dist/{server/registry-config.d.ts → core/provider-registry.d.ts} +10 -2
- package/dist/core/provider-registry.js +66 -0
- package/dist/core/redact.d.ts +2 -1
- package/dist/core/redact.js +4 -2
- package/dist/core/secret.d.ts +5 -0
- package/dist/core/secret.js +14 -0
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +61 -0
- package/dist/core/slash.js +31 -18
- package/dist/core/title.js +4 -2
- package/dist/core/todo-tags.d.ts +4 -3
- package/dist/core/todo-tags.js +20 -6
- package/dist/core/workflow.d.ts +4 -13
- package/dist/core/workflow.js +6 -6
- package/dist/core/workflows/context.js +31 -59
- package/dist/core/workflows/runtime.js +10 -8
- package/dist/dap/index.d.ts +1 -0
- package/dist/dap/index.js +1 -0
- package/dist/dap/protocol.d.ts +8 -3
- package/dist/dap/protocol.js +54 -18
- package/dist/dap/session.js +71 -12
- package/dist/dap/spawn.d.ts +11 -0
- package/dist/dap/spawn.js +48 -0
- package/dist/dap/transport.js +18 -0
- package/dist/kanban/index.d.ts +1 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +8 -1
- package/dist/kanban/store.js +61 -13
- package/dist/llm/protocols/openai-compat.d.ts +7 -0
- package/dist/llm/protocols/openai-compat.js +19 -2
- package/dist/llm/protocols/utils/tool-stream.d.ts +12 -3
- package/dist/llm/protocols/utils/tool-stream.js +47 -23
- package/dist/llm/provider.js +18 -3
- package/dist/llm/retry.d.ts +1 -0
- package/dist/llm/retry.js +20 -7
- package/dist/llm/routing.js +14 -2
- package/dist/llm/schema/messages.d.ts +9 -2
- package/dist/llm/token.d.ts +9 -0
- package/dist/llm/token.js +10 -6
- package/dist/llm/transport.js +7 -2
- package/dist/mcp/client.d.ts +11 -0
- package/dist/mcp/client.js +70 -0
- package/dist/mcp/index.d.ts +46 -0
- package/dist/mcp/index.js +106 -0
- package/dist/mcp/tool-adapter.d.ts +14 -0
- package/dist/mcp/tool-adapter.js +65 -0
- package/dist/mcp/transport.d.ts +35 -0
- package/dist/mcp/transport.js +243 -0
- package/dist/mcp/types.d.ts +55 -0
- package/dist/mcp/types.js +3 -0
- package/dist/plugins/init.d.ts +2 -0
- package/dist/plugins/init.js +10 -1
- package/dist/project/project.js +50 -3
- package/dist/project/resolve.d.ts +7 -0
- package/dist/project/resolve.js +52 -5
- package/dist/server/permission/interactive.js +9 -4
- package/dist/server/permission/store.js +8 -2
- package/dist/server/routes/auth.js +28 -13
- package/dist/server/routes/chat.js +92 -26
- package/dist/server/routes/config.js +38 -10
- package/dist/server/routes/files.js +75 -16
- package/dist/server/routes/filesystem.js +9 -2
- package/dist/server/routes/kanban.js +146 -9
- package/dist/server/routes/permissions.js +32 -0
- package/dist/server/routes/project.js +15 -6
- package/dist/server/routes/provider.js +10 -5
- package/dist/server/routes/session.js +72 -26
- package/dist/server/routes/terminal.js +10 -2
- package/dist/server/routes/todo.js +18 -0
- package/dist/server/routes/tool.js +13 -2
- package/dist/server/routes/update.js +5 -1
- package/dist/server/routes/workflows.js +17 -6
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.js +31 -17
- package/dist/server/terminal/pty-manager.d.ts +3 -0
- package/dist/server/terminal/pty-manager.js +8 -3
- package/dist/server/types.d.ts +2 -0
- package/dist/server/util/json-body.d.ts +31 -0
- package/dist/server/util/json-body.js +38 -0
- package/dist/session/archive.js +4 -4
- package/dist/session/branch.d.ts +10 -1
- package/dist/session/branch.js +54 -9
- package/dist/session/compaction.d.ts +5 -0
- package/dist/session/compaction.js +15 -10
- package/dist/session/context.d.ts +7 -3
- package/dist/session/context.js +35 -9
- package/dist/session/import.js +53 -6
- package/dist/session/index.d.ts +2 -2
- package/dist/session/index.js +2 -2
- package/dist/session/message.d.ts +12 -3
- package/dist/session/message.js +44 -4
- package/dist/session/session.d.ts +11 -5
- package/dist/session/session.js +99 -88
- package/dist/session/shake.js +35 -25
- package/dist/session/squash.js +3 -4
- package/dist/session/token.d.ts +7 -8
- package/dist/session/token.js +19 -16
- package/dist/session/usage.js +4 -2
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/round.d.ts +12 -0
- package/dist/shared/round.js +18 -0
- package/dist/shared/timer.d.ts +23 -0
- package/dist/shared/timer.js +27 -0
- package/dist/shared/types/tool.d.ts +4 -0
- package/dist/shared/utils/markdown-code.d.ts +15 -0
- package/dist/shared/utils/markdown-code.js +44 -0
- package/dist/shared/utils/path.d.ts +4 -1
- package/dist/shared/utils/path.js +5 -2
- package/dist/shared/utils/string.d.ts +5 -0
- package/dist/shared/utils/string.js +52 -0
- package/dist/tools/builtin/bash.js +79 -10
- package/dist/tools/builtin/edit.js +67 -8
- package/dist/tools/builtin/glob.js +150 -14
- package/dist/tools/builtin/grep.js +53 -11
- package/dist/tools/builtin/kanban.d.ts +14 -0
- package/dist/tools/builtin/kanban.js +4 -1
- package/dist/tools/builtin/read.js +15 -2
- package/dist/tools/builtin/resolvers.d.ts +9 -2
- package/dist/tools/builtin/resolvers.js +20 -6
- package/dist/tools/builtin/task.js +107 -25
- package/dist/tools/builtin/todo.js +84 -9
- package/dist/tools/builtin/write.js +7 -1
- package/dist/tools/builtin/yield.js +9 -1
- package/dist/tools/executor.js +53 -1
- package/dist/tools/hashline/patch.js +114 -38
- package/dist/tools/truncate.js +45 -16
- package/dist/tools/validate.js +66 -20
- package/dist/tools/websearch/index.js +8 -4
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/scheduler.js +9 -2
- package/dist/update/snapshot.d.ts +14 -4
- package/dist/update/snapshot.js +22 -22
- package/dist/update/updater.d.ts +16 -2
- package/dist/update/updater.js +39 -8
- package/dist/update/version.d.ts +3 -1
- package/dist/update/version.js +63 -9
- package/package.json +1 -1
- package/dist/server/registry-config.js +0 -44
|
@@ -3,6 +3,7 @@ import { eq } from 'drizzle-orm';
|
|
|
3
3
|
import { Hono } from 'hono';
|
|
4
4
|
import { createSummarizer, runCompaction } from '../../core/compact.js';
|
|
5
5
|
import { loadConfigScopes, mergeConfig } from '../../core/config.js';
|
|
6
|
+
import { buildRegistryFromConfig } from '../../core/provider-registry.js';
|
|
6
7
|
import { sessions } from '../../db/schema.js';
|
|
7
8
|
import { buildFallbackChain } from '../../llm/routing.js';
|
|
8
9
|
import { fromDirectory } from '../../project/index.js';
|
|
@@ -10,13 +11,14 @@ import { getProject } from '../../project/project.js';
|
|
|
10
11
|
import { archiveOriginalEntries, listArchives } from '../../session/archive.js';
|
|
11
12
|
import { BranchPointOutOfRangeError, forkSession, getBranches, getTree, } from '../../session/branch.js';
|
|
12
13
|
import { importSessionData } from '../../session/import.js';
|
|
13
|
-
import {
|
|
14
|
+
import { getMessages, getSteeringAsMessages, updateEntryContent } from '../../session/message.js';
|
|
14
15
|
import { createSession, emptyTrash, getLLMSegments, getSession, listDeletedSessions, listOrphanDeletedSessions, listSessions, listSessionsByProject, permanentlyDeleteSession, purgeEmptySession, rebindSession, restoreSessionCore, searchSessions, softDeleteSession, touchLastOpened, touchTrashSeen, updateSessionTitle, } from '../../session/session.js';
|
|
15
16
|
import { applyShakeRegions, collectShakeRegions, DEFAULT_SHAKE_CONFIG, toRegionViews, } from '../../session/shake.js';
|
|
16
17
|
import { estimateMessageTokens } from '../../session/token.js';
|
|
17
18
|
import { generateId } from '../../shared/index.js';
|
|
19
|
+
import { isUuid } from '../../shared/utils/string.js';
|
|
18
20
|
import { apiError } from '../middleware/error.js';
|
|
19
|
-
import {
|
|
21
|
+
import { readJsonObject } from '../util/json-body.js';
|
|
20
22
|
import { hasBusySession, resolveAgentCwd } from './chat.js';
|
|
21
23
|
function createSessionRoute(ctx) {
|
|
22
24
|
const app = new Hono();
|
|
@@ -119,10 +121,16 @@ function createSessionRoute(ctx) {
|
|
|
119
121
|
});
|
|
120
122
|
// 创建会话
|
|
121
123
|
app.post('/', async (c) => {
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
// 畸形 JSON / null / 非对象 body → 400(此前 null body 在 body.title 处 500)。
|
|
125
|
+
const parsed = await readJsonObject(c);
|
|
126
|
+
if (!parsed.ok)
|
|
127
|
+
return parsed.response;
|
|
128
|
+
const { body } = parsed;
|
|
129
|
+
// 非字符串字段(数字 title/directory/projectId)此前经 as 断言进 fromDirectory/
|
|
130
|
+
// createSession:path.resolve 与 text 列参数对非字符串抛错 → 500。按缺省处理。
|
|
131
|
+
const title = typeof body.title === 'string' ? body.title : 'New Session';
|
|
132
|
+
const directory = typeof body.directory === 'string' ? body.directory : undefined;
|
|
133
|
+
const explicitProjectId = typeof body.projectId === 'string' ? body.projectId : undefined;
|
|
126
134
|
let projectId;
|
|
127
135
|
if (directory) {
|
|
128
136
|
const project = await fromDirectory(ctx.db, directory);
|
|
@@ -136,7 +144,17 @@ function createSessionRoute(ctx) {
|
|
|
136
144
|
if (!projectId) {
|
|
137
145
|
return apiError(c, 400, 'PROJECT_REQUIRED', '创建会话必须指定项目:传 directory(自动解析)或 projectId');
|
|
138
146
|
}
|
|
139
|
-
|
|
147
|
+
// 显式 projectId 指向 sessions.projectId 外键:不存在的项目此前直接穿透
|
|
148
|
+
// createSession 的 insert,PG 侧 FK violation(23503)击穿 500 并回显 SQL
|
|
149
|
+
// 错误细节——先校验存在性,统一 404(与 kanban guardProject、import/rebind
|
|
150
|
+
// 的目标项目校验同口径)。directory 路径经 fromDirectory 自动 upsert,恒有效。
|
|
151
|
+
if (explicitProjectId) {
|
|
152
|
+
const project = await getProject(ctx.db, explicitProjectId);
|
|
153
|
+
if (!project) {
|
|
154
|
+
return apiError(c, 404, 'PROJECT_NOT_FOUND', '目标项目不存在');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const session = await createSession(ctx.db, title, projectId, undefined, undefined, undefined, undefined, undefined, ctx.hookRunner);
|
|
140
158
|
return c.json(session, 201);
|
|
141
159
|
});
|
|
142
160
|
// 列出会话
|
|
@@ -193,6 +211,20 @@ function createSessionRoute(ctx) {
|
|
|
193
211
|
const count = await emptyTrash(ctx.db, projectId);
|
|
194
212
|
return c.json({ ok: true, deleted: count });
|
|
195
213
|
});
|
|
214
|
+
// 路径参数 id 是 sessions.id(uuid 列):非 UUID 直接 404。
|
|
215
|
+
// 此前非 UUID 经 drizzle 参数化查询在 PG 侧抛 22P02(invalid input syntax for
|
|
216
|
+
// type uuid),Hono 兜成 500 且把 SQL 错误细节回给客户端;对调用方而言
|
|
217
|
+
// 「不存在的会话」与「格式非法的 id」同属资源不存在,统一 404。
|
|
218
|
+
// 必须注册在上方全部静态路径(/import、/tree、/search、/deleted*)之后:
|
|
219
|
+
// Hono 按注册序分派,静态路由先命中即返回,不会被本守卫拦截。
|
|
220
|
+
const guardSessionId = async (c, next) => {
|
|
221
|
+
if (!isUuid(c.req.param('id') ?? '')) {
|
|
222
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
223
|
+
}
|
|
224
|
+
await next();
|
|
225
|
+
};
|
|
226
|
+
app.use('/:id', guardSessionId);
|
|
227
|
+
app.use('/:id/*', guardSessionId);
|
|
196
228
|
// 获取会话详情
|
|
197
229
|
app.get('/:id', async (c) => {
|
|
198
230
|
try {
|
|
@@ -209,8 +241,10 @@ function createSessionRoute(ctx) {
|
|
|
209
241
|
// P2-5:会话重命名(此前标题只能由 LLM 自动生成,用户无法修改)
|
|
210
242
|
app.patch('/:id', async (c) => {
|
|
211
243
|
const id = c.req.param('id');
|
|
212
|
-
const
|
|
213
|
-
|
|
244
|
+
const parsed = await readJsonObject(c);
|
|
245
|
+
if (!parsed.ok)
|
|
246
|
+
return parsed.response;
|
|
247
|
+
const title = typeof parsed.body.title === 'string' ? parsed.body.title.trim() : '';
|
|
214
248
|
if (!title)
|
|
215
249
|
return apiError(c, 400, 'BAD_REQUEST', 'title is required');
|
|
216
250
|
if (title.length > 120) {
|
|
@@ -247,8 +281,11 @@ function createSessionRoute(ctx) {
|
|
|
247
281
|
if (busy === 'starting') {
|
|
248
282
|
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
249
283
|
}
|
|
250
|
-
const
|
|
251
|
-
|
|
284
|
+
const parsed = await readJsonObject(c);
|
|
285
|
+
if (!parsed.ok)
|
|
286
|
+
return parsed.response;
|
|
287
|
+
const rawIndex = parsed.body.messageIndex;
|
|
288
|
+
let messageIndex = typeof rawIndex === 'number' && Number.isFinite(rawIndex) ? rawIndex : undefined;
|
|
252
289
|
if (messageIndex === undefined || !Number.isFinite(messageIndex)) {
|
|
253
290
|
const messages = await getMessages(ctx.db, id);
|
|
254
291
|
if (messages.length === 0) {
|
|
@@ -257,7 +294,7 @@ function createSessionRoute(ctx) {
|
|
|
257
294
|
messageIndex = messages.length - 1;
|
|
258
295
|
}
|
|
259
296
|
try {
|
|
260
|
-
const forked = await forkSession(ctx.db, id, messageIndex);
|
|
297
|
+
const forked = await forkSession(ctx.db, id, messageIndex, ctx.hookRunner);
|
|
261
298
|
return c.json(forked, 201);
|
|
262
299
|
}
|
|
263
300
|
catch (error) {
|
|
@@ -393,7 +430,10 @@ function createSessionRoute(ctx) {
|
|
|
393
430
|
// 显式跳过重建、直接归属到请求项目(用户在确认框中二选一)。
|
|
394
431
|
app.post('/:id/restore', async (c) => {
|
|
395
432
|
const id = c.req.param('id');
|
|
396
|
-
const
|
|
433
|
+
const parsed = await readJsonObject(c);
|
|
434
|
+
if (!parsed.ok)
|
|
435
|
+
return parsed.response;
|
|
436
|
+
const { body } = parsed;
|
|
397
437
|
const requestProjectId = typeof body.projectId === 'string' && body.projectId ? body.projectId : undefined;
|
|
398
438
|
const preferCurrentProject = body.restoreMode === 'current-project';
|
|
399
439
|
const result = await restoreSessionCore(ctx.db, id);
|
|
@@ -455,8 +495,12 @@ function createSessionRoute(ctx) {
|
|
|
455
495
|
if (busy === 'starting') {
|
|
456
496
|
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
457
497
|
}
|
|
458
|
-
const
|
|
459
|
-
|
|
498
|
+
const parsed = await readJsonObject(c);
|
|
499
|
+
if (!parsed.ok)
|
|
500
|
+
return parsed.response;
|
|
501
|
+
const projectId = typeof parsed.body.projectId === 'string' && parsed.body.projectId
|
|
502
|
+
? parsed.body.projectId
|
|
503
|
+
: '';
|
|
460
504
|
if (!projectId)
|
|
461
505
|
return apiError(c, 400, 'PROJECT_REQUIRED', 'projectId is required');
|
|
462
506
|
const project = await getProject(ctx.db, projectId);
|
|
@@ -618,7 +662,15 @@ function createSessionRoute(ctx) {
|
|
|
618
662
|
}
|
|
619
663
|
if (!session)
|
|
620
664
|
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
621
|
-
const
|
|
665
|
+
const parsed = await readJsonObject(c);
|
|
666
|
+
if (!parsed.ok)
|
|
667
|
+
return parsed.response;
|
|
668
|
+
const { body } = parsed;
|
|
669
|
+
// regionIds 非数组(字符串等)会让后续 .filter 抛 TypeError 500——
|
|
670
|
+
// 显式 400(与 regionIds 不匹配同口径)。
|
|
671
|
+
if (body.regionIds !== undefined && !Array.isArray(body.regionIds)) {
|
|
672
|
+
return apiError(c, 400, 'INVALID_REGIONS', 'regionIds 必须是数组');
|
|
673
|
+
}
|
|
622
674
|
const regionIds = body.regionIds ?? [];
|
|
623
675
|
const messages = await getMessages(ctx.db, id);
|
|
624
676
|
const manualConfig = { ...DEFAULT_SHAKE_CONFIG, protectTokens: 0, minSavings: 0 };
|
|
@@ -640,19 +692,13 @@ function createSessionRoute(ctx) {
|
|
|
640
692
|
const totalTokens = selected.reduce((sum, r) => sum + r.tokens, 0);
|
|
641
693
|
await archiveOriginalEntries(ctx.db, id, originalMessages, 'shake', `Shaken ${selected.length} regions, saved ${totalTokens} tokens`, archiveId);
|
|
642
694
|
const shakenMessages = applyShakeRegions(messages, selected);
|
|
643
|
-
|
|
695
|
+
// 就地改写受影响条目(此前「删除 + 原时间戳重插」):重插的行落到物理末尾,
|
|
696
|
+
// 时间戳并列的会话(fork 复制/跨机导入的历史数据)里被 shake 的早期消息会
|
|
697
|
+
// 跳到时间线最后——内容替换不应改变条目位置。
|
|
644
698
|
for (const msg of shakenMessages) {
|
|
645
699
|
if (!affectedIds.includes(msg.id))
|
|
646
700
|
continue;
|
|
647
|
-
await
|
|
648
|
-
id: msg.id,
|
|
649
|
-
sessionId: id,
|
|
650
|
-
tag: 'message',
|
|
651
|
-
role: msg.role,
|
|
652
|
-
content: msg.content,
|
|
653
|
-
tokenCount: estimateMessageTokens(msg.content),
|
|
654
|
-
createdAt: new Date(msg.createdAt),
|
|
655
|
-
});
|
|
701
|
+
await updateEntryContent(ctx.db, msg.id, msg.content, estimateMessageTokens(msg.content));
|
|
656
702
|
}
|
|
657
703
|
return c.json({ shaken: selected.length, archiveId });
|
|
658
704
|
});
|
|
@@ -4,6 +4,7 @@ import { isAbsolute } from 'node:path';
|
|
|
4
4
|
import { Hono } from 'hono';
|
|
5
5
|
import { getProject } from '../../project/index.js';
|
|
6
6
|
import { apiError } from '../middleware/error.js';
|
|
7
|
+
import { readJsonObject } from '../util/json-body.js';
|
|
7
8
|
/** 允许的 shell 白名单(P2-6:拒绝路径分隔符与任意二进制)。 */
|
|
8
9
|
const ALLOWED_SHELLS = new Set(['bash', 'zsh', 'fish', 'sh']);
|
|
9
10
|
/** 创建终端路由(REST 部分;WebSocket 升级在 server.ts 中处理)。 */
|
|
@@ -16,7 +17,11 @@ function createTerminalRoute(ctx) {
|
|
|
16
17
|
});
|
|
17
18
|
// 创建新 PTY 会话
|
|
18
19
|
app.post('/', async (c) => {
|
|
19
|
-
|
|
20
|
+
// 畸形 JSON / null / 非对象 body → 400(此前 null body 在 body.projectId 处 500)。
|
|
21
|
+
const parsed = await readJsonObject(c);
|
|
22
|
+
if (!parsed.ok)
|
|
23
|
+
return parsed.response;
|
|
24
|
+
const { body } = parsed;
|
|
20
25
|
const projectId = typeof body.projectId === 'string' && body.projectId.length > 0 ? body.projectId : undefined;
|
|
21
26
|
// P2-13:未显式传 cwd 且带 projectId 时,默认目录跟随项目 worktree
|
|
22
27
|
// (与 chat 的 resolveAgentCwd 行为一致;项目缺失/目录失效时回退 ctx.cwd)。
|
|
@@ -67,7 +72,10 @@ function createTerminalRoute(ctx) {
|
|
|
67
72
|
const info = mgr.get(id);
|
|
68
73
|
if (!info)
|
|
69
74
|
return apiError(c, 404, 'PTY_NOT_FOUND', 'Terminal not found');
|
|
70
|
-
const
|
|
75
|
+
const parsed = await readJsonObject(c);
|
|
76
|
+
if (!parsed.ok)
|
|
77
|
+
return parsed.response;
|
|
78
|
+
const { body } = parsed;
|
|
71
79
|
if (body.cols != null && body.rows != null) {
|
|
72
80
|
const cols = Number(body.cols);
|
|
73
81
|
const rows = Number(body.rows);
|
|
@@ -3,7 +3,9 @@ import { Hono } from 'hono';
|
|
|
3
3
|
import { appendMessage, getMessages } from '../../session/message.js';
|
|
4
4
|
import { getSession } from '../../session/session.js';
|
|
5
5
|
import { generateId } from '../../shared/index.js';
|
|
6
|
+
import { isUuid } from '../../shared/utils/string.js';
|
|
6
7
|
import { formatSummary, getLatestTodoPhasesFromMessages, todoTool, } from '../../tools/builtin/todo.js';
|
|
8
|
+
import { validateInput } from '../../tools/validate.js';
|
|
7
9
|
import { apiError } from '../middleware/error.js';
|
|
8
10
|
/** 构造仅供 todo tool execute 使用的最小 ToolContext。 */
|
|
9
11
|
function makeTodoCtx(phases, abort) {
|
|
@@ -22,6 +24,15 @@ function makeTodoCtx(phases, abort) {
|
|
|
22
24
|
}
|
|
23
25
|
function createTodoRoute(ctx) {
|
|
24
26
|
const app = new Hono();
|
|
27
|
+
// 路径参数 sessionId 是 sessions.id(uuid 列):非 UUID 直接 404。
|
|
28
|
+
// 此前非 UUID 经 drizzle 查询在 PG 侧抛 22P02(invalid input syntax for type
|
|
29
|
+
// uuid)→ Hono 500 且回显 SQL 错误细节;格式非法与「会话不存在」同属 404。
|
|
30
|
+
app.use('/:sessionId', async (c, next) => {
|
|
31
|
+
if (!isUuid(c.req.param('sessionId') ?? '')) {
|
|
32
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
33
|
+
}
|
|
34
|
+
await next();
|
|
35
|
+
});
|
|
25
36
|
// GET /:sessionId — 获取当前 todo 状态
|
|
26
37
|
app.get('/:sessionId', async (c) => {
|
|
27
38
|
const sessionId = c.req.param('sessionId');
|
|
@@ -47,6 +58,13 @@ function createTodoRoute(ctx) {
|
|
|
47
58
|
if (!session) {
|
|
48
59
|
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
49
60
|
}
|
|
61
|
+
// 与工具执行管线同口径校验入参:本路由绕过 executor 直调 todoTool.execute,
|
|
62
|
+
// 此前畸形 body(list 非数组 → initPhases 对字符串 for-of 后 .map TypeError 500;
|
|
63
|
+
// items 含非字符串 → 毒化状态原样持久化)无任何校验。非法显式 400 让前端可操作。
|
|
64
|
+
const validation = validateInput(todoTool.parameters, body);
|
|
65
|
+
if (!validation.valid) {
|
|
66
|
+
return apiError(c, 400, 'INVALID_TODO_INPUT', `无效的 todo 操作参数:${validation.error}`);
|
|
67
|
+
}
|
|
50
68
|
// 恢复当前 phases
|
|
51
69
|
const run = ctx.agentManager.get(sessionId);
|
|
52
70
|
let currentPhases;
|
|
@@ -35,8 +35,19 @@ function createToolRoute(ctx) {
|
|
|
35
35
|
});
|
|
36
36
|
// 确认工具执行权限
|
|
37
37
|
app.post('/confirm', async (c) => {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// 畸形 JSON / 非对象 body → 400(与其他写端点同口径:.catch + 显式类型校验)。
|
|
39
|
+
// 此前裸 await c.req.json():畸形 JSON 击穿 Hono 500,null body 在
|
|
40
|
+
// body.toolCallId 处 TypeError 500。
|
|
41
|
+
const body = (await c.req.json().catch(() => null));
|
|
42
|
+
if (body === null || typeof body !== 'object' || Array.isArray(body)) {
|
|
43
|
+
return apiError(c, 400, 'BAD_REQUEST', '请求体必须是 JSON 对象');
|
|
44
|
+
}
|
|
45
|
+
if (typeof body.toolCallId !== 'string' || body.toolCallId.length === 0) {
|
|
46
|
+
return apiError(c, 400, 'BAD_REQUEST', 'toolCallId is required');
|
|
47
|
+
}
|
|
48
|
+
// 仅显式 true 放行:非布尔("false" 字符串等 truthy 垃圾)按拒绝处理——
|
|
49
|
+
// 绝不让客户端序列化缺陷把「拒绝」翻成「放行」。
|
|
50
|
+
const ok = ctx.permissionStore.resolve(body.toolCallId, body.approved === true);
|
|
40
51
|
if (!ok) {
|
|
41
52
|
return apiError(c, 404, 'NOT_FOUND', 'No pending permission for this tool call');
|
|
42
53
|
}
|
|
@@ -3,6 +3,7 @@ import { getSession } from '../../session/session.js';
|
|
|
3
3
|
import { getCurrentVersion, manualInstallCommand, performHandoff, performInstall, serializeSessions, } from '../../update/index.js';
|
|
4
4
|
import { apiError } from '../middleware/error.js';
|
|
5
5
|
import { captureForegroundCommand } from '../terminal/pty-manager.js';
|
|
6
|
+
import { readJsonObject } from '../util/json-body.js';
|
|
6
7
|
/**
|
|
7
8
|
* GET /api/update — 返回后台调度器缓存的版本检查结果(spec §18.1)。
|
|
8
9
|
* 路由本身不触发 npm registry 请求;调度器周期性 checkNow 写入缓存,
|
|
@@ -107,7 +108,10 @@ function createUpdateRoute(ctx) {
|
|
|
107
108
|
return apiError(c, 409, 'HOT_UPDATE_UNAVAILABLE', '热更新仅在独立 serve 进程可用(dev 模式请手动更新)');
|
|
108
109
|
}
|
|
109
110
|
// P3-7:用户勾选「更新后自动重启」的终端 id;命中者在快照中携带前台命令。
|
|
110
|
-
const
|
|
111
|
+
const parsed = await readJsonObject(c);
|
|
112
|
+
if (!parsed.ok)
|
|
113
|
+
return parsed.response;
|
|
114
|
+
const body = parsed.body;
|
|
111
115
|
const rerunIds = new Set(Array.isArray(body.rerunTerminalIds)
|
|
112
116
|
? body.rerunTerminalIds.filter((x) => typeof x === 'string')
|
|
113
117
|
: []);
|
|
@@ -5,15 +5,18 @@ import { Hono } from 'hono';
|
|
|
5
5
|
import { streamSSE } from 'hono/streaming';
|
|
6
6
|
import { createAgent } from '../../core/agent.js';
|
|
7
7
|
import { loadConfigScopes, mergeConfig, resolveGlobalConfigDir } from '../../core/config.js';
|
|
8
|
+
import { buildRegistryFromConfig } from '../../core/provider-registry.js';
|
|
9
|
+
import { discoverSkills } from '../../core/skills.js';
|
|
8
10
|
import { discoverWorkflows, executeWorkflow, resolveWorkflow, saveWorkflow, workflowSessionTitle, } from '../../core/workflows/index.js';
|
|
9
11
|
import { reloadRegistry } from '../../core/workflows/registry.js';
|
|
12
|
+
import { createDebugSpawn } from '../../dap/index.js';
|
|
10
13
|
import { getByDirectory, getProject, trustProject } from '../../project/project.js';
|
|
11
14
|
import { enrichProjectRiskWithGlobal, projectTrustCurrent, projectTrustNeeded, } from '../../project/trust.js';
|
|
12
15
|
import { createSession, updateSessionLastRun } from '../../session/session.js';
|
|
13
16
|
import { PROJECT_BOUND_TOOLS, resolveEnabledToolNames } from '../../tools/index.js';
|
|
14
17
|
import { apiError } from '../middleware/error.js';
|
|
15
18
|
import { createInteractivePermissionChecker } from '../permission/interactive.js';
|
|
16
|
-
import {
|
|
19
|
+
import { readJsonObject } from '../util/json-body.js';
|
|
17
20
|
import { childRunBridge, startHeartbeat } from './chat.js';
|
|
18
21
|
/**
|
|
19
22
|
* 按名字查找运行中的工作流 run:遍历 busy 作用域占位(workflowBusyByScope 的
|
|
@@ -108,8 +111,10 @@ function createWorkflowsRoute(ctx) {
|
|
|
108
111
|
if (!registry) {
|
|
109
112
|
return apiError(c, 500, 'NOT_INITIALIZED', 'Workflow registry not initialized');
|
|
110
113
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
114
|
+
const parsed = await readJsonObject(c);
|
|
115
|
+
if (!parsed.ok)
|
|
116
|
+
return parsed.response;
|
|
117
|
+
const { name, source, target, projectId, overwrite } = parsed.body;
|
|
113
118
|
if (!name || typeof name !== 'string') {
|
|
114
119
|
return apiError(c, 400, 'BAD_REQUEST', 'Missing required field: name');
|
|
115
120
|
}
|
|
@@ -263,8 +268,10 @@ function createWorkflowsRoute(ctx) {
|
|
|
263
268
|
if (!entry) {
|
|
264
269
|
return apiError(c, 404, 'NOT_FOUND', `Workflow "${name}" not found`);
|
|
265
270
|
}
|
|
266
|
-
const
|
|
267
|
-
|
|
271
|
+
const parsed = await readJsonObject(c);
|
|
272
|
+
if (!parsed.ok)
|
|
273
|
+
return parsed.response;
|
|
274
|
+
const args = typeof parsed.body.args === 'string' ? parsed.body.args : '';
|
|
268
275
|
// 会话级配置:按 agent cwd 解析项目作用域合并(与 chat 路由同口径);
|
|
269
276
|
// 项目配置含 providers 时构建/复用项目级 LLM 注册表。
|
|
270
277
|
const sessionProjectScope = agentCwd === ctx.cwd ? undefined : loadConfigScopes(agentCwd).project;
|
|
@@ -297,7 +304,7 @@ function createWorkflowsRoute(ctx) {
|
|
|
297
304
|
try {
|
|
298
305
|
session = await createSession(ctx.db, workflowSessionTitle(name), project?.id ?? undefined, 'workflow', undefined, undefined, project?.worktree ?? agentCwd,
|
|
299
306
|
// 中断恢复指引需要工作流名(重启后会话内无 user 消息可重发)。
|
|
300
|
-
{ workflowName: name });
|
|
307
|
+
{ workflowName: name }, ctx.hookRunner);
|
|
301
308
|
}
|
|
302
309
|
catch (e) {
|
|
303
310
|
ctx.workflowBusyByScope.delete(scopeKey);
|
|
@@ -396,6 +403,10 @@ function createWorkflowsRoute(ctx) {
|
|
|
396
403
|
cwd: agentCwd,
|
|
397
404
|
agentRegistry: ctx.agentRegistry,
|
|
398
405
|
registerChildRun: childRunBridge(ctx.agentManager),
|
|
406
|
+
// 技能发现(system prompt ## Loaded Skills 段数据源)。
|
|
407
|
+
skills: discoverSkills(agentCwd),
|
|
408
|
+
// DAP(spec §21):与主 chat 通道同口径注入真实适配器 spawn。
|
|
409
|
+
debugSpawn: createDebugSpawn(),
|
|
399
410
|
// 预算护栏:与 chat 路由同口径(金额/token 任一轴 pause/abort 即启用)。
|
|
400
411
|
...(sessionConfig.usage?.budgetAction === 'pause' ||
|
|
401
412
|
sessionConfig.usage?.budgetAction === 'abort' ||
|
package/dist/server/server.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Hono } from 'hono';
|
|
2
|
+
import { buildRegistryFromConfig, syncRegistryFromConfig } from '../core/provider-registry.js';
|
|
2
3
|
import type { DB } from '../db/client.js';
|
|
3
4
|
import type { Config } from '../shared/types/config.js';
|
|
4
5
|
import { checkForUpdate, createHandoffServer } from '../update/index.js';
|
|
5
|
-
import { buildRegistryFromConfig, syncRegistryFromConfig } from './registry-config.js';
|
|
6
6
|
import type { ServerContext } from './types.js';
|
|
7
7
|
type StartServerOptions = {
|
|
8
8
|
port?: number;
|
package/dist/server/server.js
CHANGED
|
@@ -8,9 +8,11 @@ import { serve } from '@hono/node-server';
|
|
|
8
8
|
import { WebSocketServer } from 'ws';
|
|
9
9
|
import { BUILTIN_AGENTS, createAgentRegistry } from '../core/agents/index.js';
|
|
10
10
|
import { loadConfig, loadConfigScopes } from '../core/config.js';
|
|
11
|
+
import { buildRegistryFromConfig, syncRegistryFromConfig } from '../core/provider-registry.js';
|
|
11
12
|
import { createAndPopulateRegistry } from '../core/workflows/index.js';
|
|
12
13
|
import { createDB, migrateDB } from '../db/index.js';
|
|
13
14
|
import { purgeDeletedKanbanBoards } from '../kanban/index.js';
|
|
15
|
+
import { collectScopedMCPServers, disconnectMCPServer, registerMCPServers } from '../mcp/index.js';
|
|
14
16
|
import { initPlugins } from '../plugins/index.js';
|
|
15
17
|
import { getByDirectory } from '../project/index.js';
|
|
16
18
|
import { projectTrustCurrent } from '../project/trust.js';
|
|
@@ -24,7 +26,6 @@ import { createApp } from './app.js';
|
|
|
24
26
|
import { createAuthManager } from './auth-manager.js';
|
|
25
27
|
import { isAllowedOrigin } from './middleware/cors.js';
|
|
26
28
|
import { createPermissionStore } from './permission/store.js';
|
|
27
|
-
import { buildRegistryFromConfig, syncRegistryFromConfig } from './registry-config.js';
|
|
28
29
|
import { PTYManager } from './terminal/pty-manager.js';
|
|
29
30
|
/** serve 默认端口:避开 node 生态常用端口(3000/4000/5173/8000/8080…),
|
|
30
31
|
* 降低与其他本地服务撞端口的概率。显式 --port 始终优先。 */
|
|
@@ -188,13 +189,18 @@ async function buildServerContext(db, opts = {}) {
|
|
|
188
189
|
catch {
|
|
189
190
|
// 信任状态查询失败 → 保持未信任(宁可少加载插件)
|
|
190
191
|
}
|
|
191
|
-
const { pluginRegistry, hookRunner } = await initPlugins({
|
|
192
|
+
const { pluginRegistry, hookRunner, config: resolvedConfig, } = await initPlugins({
|
|
192
193
|
cwd,
|
|
193
194
|
config,
|
|
194
195
|
toolRegistry,
|
|
195
196
|
llmRegistry,
|
|
196
197
|
projectTrusted,
|
|
197
198
|
});
|
|
199
|
+
// MCP 客户端(spec §6):把配置中的 MCP 服务器工具适配进 toolRegistry,对 agent
|
|
200
|
+
// 透明。全局作用域(用户本机显式配置)始终连接;项目作用域(随 git clone 传播的
|
|
201
|
+
// 任意命令执行面)仅在项目已信任时连接——与插件/工作流同口径。逐台隔离:单台
|
|
202
|
+
// 失败告警跳过,不拖垮启动。
|
|
203
|
+
const { sessions: mcpSessions } = await registerMCPServers(toolRegistry, collectScopedMCPServers(loadConfigScopes(cwd), projectTrusted));
|
|
198
204
|
// 工作流注册表:三级发现(builtin → global → project),eager 初始化。
|
|
199
205
|
// 此前是惰性 getter 只注册 builtin,导致项目级 .c0de/workflows/*.js 永远不可见。
|
|
200
206
|
// P0 信任边界:项目级工作流与 .c0de/plugins 同为仓库自带任意代码执行面,
|
|
@@ -204,14 +210,15 @@ async function buildServerContext(db, opts = {}) {
|
|
|
204
210
|
const dataDir = resolveDbDir();
|
|
205
211
|
// 先解析/生成 bootstrap token(落盘),再创建 authManager 读取——
|
|
206
212
|
// 首启时文件由 resolveAuthToken 生成,顺序颠倒会导致 bootstrap 读到空值。
|
|
207
|
-
|
|
213
|
+
// 自 initPlugins 之后一律用 resolvedConfig(config:resolve hook 的产物)。
|
|
214
|
+
const resolvedToken = resolveAuthToken(resolvedConfig, dataDir);
|
|
208
215
|
const authManager = createAuthManager({
|
|
209
216
|
dataDir,
|
|
210
|
-
...(
|
|
211
|
-
? { staticToken:
|
|
217
|
+
...(resolvedConfig.security.token && resolvedConfig.security.token.length > 0
|
|
218
|
+
? { staticToken: resolvedConfig.security.token }
|
|
212
219
|
: {}),
|
|
213
|
-
...(
|
|
214
|
-
? { firstDeviceTtlMs:
|
|
220
|
+
...(resolvedConfig.security.firstDeviceTtlMs !== undefined
|
|
221
|
+
? { firstDeviceTtlMs: resolvedConfig.security.firstDeviceTtlMs }
|
|
215
222
|
: {}),
|
|
216
223
|
});
|
|
217
224
|
const ptyManager = new PTYManager();
|
|
@@ -244,7 +251,7 @@ async function buildServerContext(db, opts = {}) {
|
|
|
244
251
|
}
|
|
245
252
|
const ctx = {
|
|
246
253
|
db,
|
|
247
|
-
config,
|
|
254
|
+
config: resolvedConfig,
|
|
248
255
|
toolRegistry,
|
|
249
256
|
llmRegistry,
|
|
250
257
|
urlRegistry,
|
|
@@ -254,20 +261,22 @@ async function buildServerContext(db, opts = {}) {
|
|
|
254
261
|
// P3:权限双层超时可配置(config.permission.timeoutMs/expireGraceMs);
|
|
255
262
|
// 缺省回退 store 内置默认(5 分钟 + 25 分钟)。
|
|
256
263
|
permissionStore: createPermissionStore({
|
|
257
|
-
...(
|
|
258
|
-
|
|
264
|
+
...(resolvedConfig.permission.timeoutMs !== undefined &&
|
|
265
|
+
resolvedConfig.permission.timeoutMs > 0
|
|
266
|
+
? { timeoutMs: resolvedConfig.permission.timeoutMs }
|
|
259
267
|
: {}),
|
|
260
|
-
...(
|
|
261
|
-
|
|
268
|
+
...(resolvedConfig.permission.expireGraceMs !== undefined &&
|
|
269
|
+
resolvedConfig.permission.expireGraceMs > 0
|
|
270
|
+
? { expireGraceMs: resolvedConfig.permission.expireGraceMs }
|
|
262
271
|
: {}),
|
|
263
272
|
}),
|
|
264
|
-
permissionMode:
|
|
273
|
+
permissionMode: resolvedConfig.permission.defaultMode,
|
|
265
274
|
sessionPermissionModes: new Map(),
|
|
266
275
|
sessionAlwaysAllow: new Map(),
|
|
267
276
|
workflowBusyBySession: new Map(),
|
|
268
277
|
workflowBusyByScope: new Map(),
|
|
269
278
|
authToken: resolvedToken,
|
|
270
|
-
authManager:
|
|
279
|
+
authManager: resolvedConfig.security.authEnabled === false ? undefined : authManager,
|
|
271
280
|
// Agent 注册表:内置 4 个默认 agent;项目/用户自定义 agent 可在启动后补充加载。
|
|
272
281
|
agentRegistry: (() => {
|
|
273
282
|
const reg = createAgentRegistry();
|
|
@@ -279,11 +288,13 @@ async function buildServerContext(db, opts = {}) {
|
|
|
279
288
|
// spec §18.1 后台版本检查调度器;config.update.enabled 控制是否启动。
|
|
280
289
|
updateScheduler: createUpdateScheduler({
|
|
281
290
|
checkFn: opts.checkForUpdateFn ?? checkForUpdate,
|
|
282
|
-
intervalMs:
|
|
283
|
-
initialDelayMs:
|
|
291
|
+
intervalMs: resolvedConfig.update.intervalMs,
|
|
292
|
+
initialDelayMs: resolvedConfig.update.initialDelayMs,
|
|
284
293
|
}),
|
|
285
294
|
cwd,
|
|
286
295
|
ptyManager,
|
|
296
|
+
// MCP 会话句柄:关闭时统一断开(stdio 子进程由 close 杀死)。
|
|
297
|
+
...(mcpSessions.length > 0 ? { mcpSessions } : {}),
|
|
287
298
|
};
|
|
288
299
|
// handoff server 不在此创建:它需要持有主 HTTP server 引用以触发完整关停,
|
|
289
300
|
// 由 startServer 在 serve() 之后创建(dev 模式走 vite,不创建)。
|
|
@@ -291,12 +302,15 @@ async function buildServerContext(db, opts = {}) {
|
|
|
291
302
|
ctx,
|
|
292
303
|
dispose: async () => {
|
|
293
304
|
// dev 重建前调用:中止活跃 run + settle pending permission +
|
|
294
|
-
// 停 scheduler + 关 devices.json watcher
|
|
305
|
+
// 停 scheduler + 关 devices.json watcher + 断开 MCP 连接。
|
|
306
|
+
// **不 close db**(调用方持有)。
|
|
295
307
|
ctx.agentManager.dispose();
|
|
296
308
|
ctx.permissionStore.dispose();
|
|
297
309
|
ctx.updateScheduler.stop();
|
|
298
310
|
ctx.ptyManager.dispose();
|
|
299
311
|
ctx.authManager?.dispose();
|
|
312
|
+
for (const session of ctx.mcpSessions ?? [])
|
|
313
|
+
disconnectMCPServer(session);
|
|
300
314
|
},
|
|
301
315
|
};
|
|
302
316
|
}
|
|
@@ -34,6 +34,9 @@ export interface CreatePTYOptions {
|
|
|
34
34
|
* /etc/passwd(getpwuid),是用户真实登录 shell 的可靠来源。
|
|
35
35
|
*/
|
|
36
36
|
export declare function detectShell(): string;
|
|
37
|
+
/** 截断标题到 MAX_TITLE_LEN 内。省略号本身计入上限(此前 slice(100) + '…'
|
|
38
|
+
* 产出 101 字符,违反上限契约)。切点经 headChars 内收,不拆代理对。 */
|
|
39
|
+
export declare function truncateTitle(title: string): string;
|
|
37
40
|
/** 把 argv 拼回可安全经 shell 重放的命令行(导出便于单测安全转义)。 */
|
|
38
41
|
export declare function argvToCommand(argv: string[]): string;
|
|
39
42
|
/**
|
|
@@ -4,6 +4,7 @@ import { mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node
|
|
|
4
4
|
import { tmpdir, userInfo } from 'node:os';
|
|
5
5
|
import { basename, join } from 'node:path';
|
|
6
6
|
import { spawn } from 'node-pty';
|
|
7
|
+
import { headChars, tailChars } from '../../shared/utils/string.js';
|
|
7
8
|
/**
|
|
8
9
|
* 为指定 shell 创建 OSC 133 prompt 标记注入脚本。
|
|
9
10
|
*
|
|
@@ -93,9 +94,11 @@ const MAX_ROWS = 500;
|
|
|
93
94
|
const MAX_TITLE_LEN = 100;
|
|
94
95
|
/** scrollback 环形缓冲最大字节数(约 50KB)。 */
|
|
95
96
|
const SCROLLBACK_MAX = 50_000;
|
|
96
|
-
|
|
97
|
+
/** 截断标题到 MAX_TITLE_LEN 内。省略号本身计入上限(此前 slice(100) + '…'
|
|
98
|
+
* 产出 101 字符,违反上限契约)。切点经 headChars 内收,不拆代理对。 */
|
|
99
|
+
export function truncateTitle(title) {
|
|
97
100
|
const clean = title.replace(/[\r\n]/g, ' ').trim();
|
|
98
|
-
return clean.length > MAX_TITLE_LEN ? `${clean
|
|
101
|
+
return clean.length > MAX_TITLE_LEN ? `${headChars(clean, MAX_TITLE_LEN - 1)}…` : clean;
|
|
99
102
|
}
|
|
100
103
|
/** POSIX 单引号转义:仅安全字符(字母数字与常见路径/别名符号)原样,否则单引号包裹。 */
|
|
101
104
|
function shellQuote(arg) {
|
|
@@ -187,7 +190,9 @@ export class PTYManager {
|
|
|
187
190
|
pty.onData((data) => {
|
|
188
191
|
entry.scrollback += data;
|
|
189
192
|
if (entry.scrollback.length > SCROLLBACK_MAX) {
|
|
190
|
-
|
|
193
|
+
// tailChars 内收切点:环形缓冲修剪不落在代理对中间——否则回放给新挂载
|
|
194
|
+
// 终端的历史头部以 U+FFFD 损坏显示(slice(-N) 按 UTF-16 码元硬切)。
|
|
195
|
+
entry.scrollback = tailChars(entry.scrollback, SCROLLBACK_MAX);
|
|
191
196
|
}
|
|
192
197
|
for (const ws of entry.sockets) {
|
|
193
198
|
if (ws.readyState === ws.OPEN) {
|
package/dist/server/types.d.ts
CHANGED
|
@@ -73,6 +73,8 @@ type ServerContext = {
|
|
|
73
73
|
cwd: string;
|
|
74
74
|
/** 终端 PTY 管理器(Web 终端面板用)。 */
|
|
75
75
|
ptyManager: PTYManager;
|
|
76
|
+
/** 已连接的 MCP 会话(spec §6);dispose 时统一断开。 */
|
|
77
|
+
mcpSessions?: import('../mcp/types.js').MCPSession[];
|
|
76
78
|
/** 测试注入:覆盖 LLM chat stream。生产环境为 undefined。 */
|
|
77
79
|
chatStream?: typeof chatStreamFn;
|
|
78
80
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
/** readJsonObject 的判别结果:ok=false 时 response 为可直接 return 的 400。 */
|
|
3
|
+
export type JsonObjectResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
body: Record<string, unknown>;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
response: Response;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* 读取「必须是 JSON 对象」的请求体。
|
|
12
|
+
*
|
|
13
|
+
* 此前各写端点用 `await c.req.json().catch(() => ({}))` 兜底畸形 JSON——但
|
|
14
|
+
* `JSON.parse('null')` 是合法 JSON,`c.req.json()` 会成功解析出 `null`,
|
|
15
|
+
* catch 不触发:随后 `body.xxx` 在 null 上取属性直接 TypeError 击穿 Hono 500。
|
|
16
|
+
* 数组/字符串/数字 body 同型(`body.foo` 静默 undefined,字段被当作缺省,
|
|
17
|
+
* 与「请求体畸形」应有的显式反馈不符)。
|
|
18
|
+
*
|
|
19
|
+
* 口径:
|
|
20
|
+
* - 无 body / 空 body(curl -X POST 不带载荷)→ `{}`:这些端点的字段均为可选,
|
|
21
|
+
* 保持既有契约(既有测试与脚本依赖无载荷 POST)。
|
|
22
|
+
* - 畸形 JSON / null / 数组 / 标量 body → 400(此前 null 击穿 500、
|
|
23
|
+
* 其余静默当空对象)。
|
|
24
|
+
* - 合法 JSON 对象 → 原样返回。
|
|
25
|
+
*
|
|
26
|
+
* 调用方写法:
|
|
27
|
+
* const parsed = await readJsonObject(c)
|
|
28
|
+
* if (!parsed.ok) return parsed.response
|
|
29
|
+
* const { body } = parsed
|
|
30
|
+
*/
|
|
31
|
+
export declare function readJsonObject(c: Context): Promise<JsonObjectResult>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { apiError } from '../middleware/error.js';
|
|
2
|
+
/**
|
|
3
|
+
* 读取「必须是 JSON 对象」的请求体。
|
|
4
|
+
*
|
|
5
|
+
* 此前各写端点用 `await c.req.json().catch(() => ({}))` 兜底畸形 JSON——但
|
|
6
|
+
* `JSON.parse('null')` 是合法 JSON,`c.req.json()` 会成功解析出 `null`,
|
|
7
|
+
* catch 不触发:随后 `body.xxx` 在 null 上取属性直接 TypeError 击穿 Hono 500。
|
|
8
|
+
* 数组/字符串/数字 body 同型(`body.foo` 静默 undefined,字段被当作缺省,
|
|
9
|
+
* 与「请求体畸形」应有的显式反馈不符)。
|
|
10
|
+
*
|
|
11
|
+
* 口径:
|
|
12
|
+
* - 无 body / 空 body(curl -X POST 不带载荷)→ `{}`:这些端点的字段均为可选,
|
|
13
|
+
* 保持既有契约(既有测试与脚本依赖无载荷 POST)。
|
|
14
|
+
* - 畸形 JSON / null / 数组 / 标量 body → 400(此前 null 击穿 500、
|
|
15
|
+
* 其余静默当空对象)。
|
|
16
|
+
* - 合法 JSON 对象 → 原样返回。
|
|
17
|
+
*
|
|
18
|
+
* 调用方写法:
|
|
19
|
+
* const parsed = await readJsonObject(c)
|
|
20
|
+
* if (!parsed.ok) return parsed.response
|
|
21
|
+
* const { body } = parsed
|
|
22
|
+
*/
|
|
23
|
+
export async function readJsonObject(c) {
|
|
24
|
+
const raw = await c.req.text().catch(() => '');
|
|
25
|
+
if (raw.trim().length === 0)
|
|
26
|
+
return { ok: true, body: {} };
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { ok: false, response: apiError(c, 400, 'BAD_REQUEST', '请求体必须是合法 JSON') };
|
|
33
|
+
}
|
|
34
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
35
|
+
return { ok: false, response: apiError(c, 400, 'BAD_REQUEST', '请求体必须是 JSON 对象') };
|
|
36
|
+
}
|
|
37
|
+
return { ok: true, body: parsed };
|
|
38
|
+
}
|