c0de-agent 1.11.0 → 1.13.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/README.md +63 -16
- package/dist/cli/commands/acp.js +26 -8
- package/dist/cli/commands/chat.js +151 -1
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +63 -3
- package/dist/cli/commands/serve.js +11 -2
- package/dist/cli/commands/sessions.js +109 -3
- package/dist/cli/commands/trust.d.ts +11 -0
- package/dist/cli/commands/trust.js +31 -0
- package/dist/cli/commands/update.js +4 -0
- package/dist/cli/deps.d.ts +9 -6
- package/dist/cli/deps.js +66 -19
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.js +110 -11
- package/dist/cli/modes/acp.js +39 -8
- package/dist/cli/modes/print.d.ts +3 -0
- package/dist/cli/modes/print.js +78 -8
- package/dist/core/agent.d.ts +1 -1
- package/dist/core/agent.js +9 -4
- package/dist/core/agents/builtin.js +2 -2
- package/dist/core/agents/index.d.ts +0 -1
- package/dist/core/agents/index.js +0 -1
- package/dist/core/compact.d.ts +2 -0
- package/dist/core/compact.js +1 -0
- package/dist/core/config.d.ts +53 -2
- package/dist/core/config.js +235 -19
- package/dist/core/loop/compaction.d.ts +2 -1
- package/dist/core/loop/compaction.js +19 -6
- package/dist/core/loop/segment.d.ts +3 -1
- package/dist/core/loop/segment.js +24 -1
- package/dist/core/loop/stream-collect.js +9 -1
- package/dist/core/loop/subagent.js +88 -37
- package/dist/core/loop.d.ts +7 -0
- package/dist/core/loop.js +90 -6
- package/dist/core/redact.d.ts +11 -0
- package/dist/core/redact.js +49 -0
- package/dist/core/slash.d.ts +10 -0
- package/dist/core/slash.js +237 -65
- package/dist/core/title.js +7 -1
- package/dist/core/types.d.ts +22 -1
- package/dist/core/workflows/context.js +44 -4
- package/dist/core/workflows/discovery.d.ts +2 -2
- package/dist/core/workflows/discovery.js +5 -7
- package/dist/core/workflows/index.d.ts +2 -1
- package/dist/core/workflows/index.js +2 -1
- package/dist/core/workflows/registry.d.ts +13 -3
- package/dist/core/workflows/registry.js +14 -9
- package/dist/core/workflows/resolve.d.ts +22 -0
- package/dist/core/workflows/resolve.js +23 -0
- package/dist/core/workflows/runtime.d.ts +6 -1
- package/dist/core/workflows/runtime.js +19 -2
- package/dist/db/schema.d.ts +373 -1
- package/dist/db/schema.js +60 -4
- package/dist/kanban/index.d.ts +2 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +87 -1
- package/dist/kanban/store.js +429 -22
- package/dist/llm/index.d.ts +1 -1
- package/dist/llm/index.js +1 -1
- package/dist/llm/provider.js +1 -1
- package/dist/llm/registry.d.ts +7 -1
- package/dist/llm/registry.js +7 -1
- package/dist/llm/retry.d.ts +5 -2
- package/dist/llm/retry.js +6 -5
- package/dist/llm/routing.d.ts +24 -1
- package/dist/llm/routing.js +29 -1
- package/dist/plugins/init.d.ts +7 -0
- package/dist/plugins/init.js +22 -1
- package/dist/plugins/loader.d.ts +7 -1
- package/dist/plugins/loader.js +12 -4
- package/dist/project/index.d.ts +1 -1
- package/dist/project/index.js +1 -1
- package/dist/project/project.d.ts +37 -0
- package/dist/project/project.js +142 -1
- package/dist/project/trust.d.ts +65 -0
- package/dist/project/trust.js +328 -0
- package/dist/server/agent-manager.d.ts +15 -2
- package/dist/server/agent-manager.js +18 -3
- package/dist/server/app.js +45 -30
- package/dist/server/auth-manager.d.ts +26 -9
- package/dist/server/auth-manager.js +78 -42
- package/dist/server/context.js +14 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/middleware/auth.d.ts +11 -1
- package/dist/server/middleware/auth.js +25 -3
- package/dist/server/permission/interactive.d.ts +11 -3
- package/dist/server/permission/interactive.js +23 -4
- package/dist/server/permission/store.d.ts +14 -2
- package/dist/server/permission/store.js +39 -3
- package/dist/server/registry-config.d.ts +14 -0
- package/dist/server/registry-config.js +44 -0
- package/dist/server/routes/auth.js +66 -10
- package/dist/server/routes/chat.d.ts +21 -0
- package/dist/server/routes/chat.js +685 -52
- package/dist/server/routes/commands.d.ts +4 -2
- package/dist/server/routes/commands.js +9 -4
- package/dist/server/routes/config.js +180 -23
- package/dist/server/routes/files.js +142 -28
- package/dist/server/routes/kanban.js +218 -26
- package/dist/server/routes/permissions.d.ts +0 -4
- package/dist/server/routes/permissions.js +140 -7
- package/dist/server/routes/project.js +132 -29
- package/dist/server/routes/provider.js +54 -3
- package/dist/server/routes/session.js +463 -23
- package/dist/server/routes/tool.js +24 -5
- package/dist/server/routes/update.d.ts +11 -5
- package/dist/server/routes/update.js +145 -29
- package/dist/server/routes/usage.d.ts +4 -0
- package/dist/server/routes/usage.js +133 -0
- package/dist/server/routes/workflows.js +476 -87
- package/dist/server/server.d.ts +16 -13
- package/dist/server/server.js +364 -107
- package/dist/server/terminal/pty-manager.d.ts +12 -0
- package/dist/server/terminal/pty-manager.js +63 -7
- package/dist/server/types.d.ts +16 -0
- package/dist/session/archive.d.ts +3 -1
- package/dist/session/archive.js +13 -1
- package/dist/session/branch.d.ts +2 -1
- package/dist/session/branch.js +92 -9
- package/dist/session/context.js +28 -2
- package/dist/session/import.d.ts +44 -0
- package/dist/session/import.js +83 -0
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/jobs.d.ts +12 -0
- package/dist/session/jobs.js +53 -0
- package/dist/session/message.d.ts +7 -1
- package/dist/session/message.js +21 -1
- package/dist/session/session.d.ts +186 -12
- package/dist/session/session.js +577 -33
- package/dist/session/snapshot.d.ts +5 -2
- package/dist/session/snapshot.js +10 -2
- package/dist/session/types.d.ts +13 -1
- package/dist/session/usage.d.ts +62 -0
- package/dist/session/usage.js +194 -0
- package/dist/shared/types/agent.d.ts +29 -2
- package/dist/shared/types/config.d.ts +64 -5
- package/dist/shared/types/kanban.d.ts +17 -2
- package/dist/shared/types/message.d.ts +23 -0
- package/dist/tools/builtin/kanban.js +13 -2
- package/dist/tools/index.d.ts +14 -3
- package/dist/tools/index.js +23 -5
- package/dist/tools/websearch/index.d.ts +5 -1
- package/dist/tools/websearch/index.js +46 -7
- package/dist/tools/websearch/providers/brave.js +2 -2
- package/dist/tools/websearch/providers/duckduckgo.js +2 -1
- package/dist/tools/websearch/providers/tavily.js +2 -2
- package/dist/tools/websearch/types.d.ts +5 -1
- package/dist/tools/websearch/types.js +7 -1
- package/dist/update/index.d.ts +3 -3
- package/dist/update/index.js +2 -2
- package/dist/update/ipc.d.ts +13 -2
- package/dist/update/ipc.js +36 -2
- package/dist/update/scheduler.js +5 -2
- package/dist/update/snapshot.d.ts +18 -3
- package/dist/update/snapshot.js +5 -2
- package/dist/update/updater.d.ts +17 -1
- package/dist/update/updater.js +47 -18
- package/dist/update/version.d.ts +3 -0
- package/dist/update/version.js +9 -3
- package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
- package/drizzle/0010_usage_events_ledger.sql +17 -0
- package/drizzle/0011_slow_slapstick.sql +12 -0
- package/drizzle/0012_chief_timeslip.sql +1 -0
- package/drizzle/0013_swift_zeigeist.sql +1 -0
- package/drizzle/0014_flimsy_legion.sql +1 -0
- package/drizzle/meta/0008_snapshot.json +875 -0
- package/drizzle/meta/0010_snapshot.json +1010 -0
- package/drizzle/meta/0011_snapshot.json +1055 -0
- package/drizzle/meta/0012_snapshot.json +1062 -0
- package/drizzle/meta/0013_snapshot.json +1068 -0
- package/drizzle/meta/0014_snapshot.json +1075 -0
- package/drizzle/meta/_journal.json +42 -0
- package/package.json +42 -42
- package/dist/core/agents/discovery.d.ts +0 -6
- package/dist/core/agents/discovery.js +0 -96
|
@@ -1,19 +1,125 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
1
3
|
import { Hono } from 'hono';
|
|
2
4
|
import { createSummarizer, runCompaction } from '../../core/compact.js';
|
|
5
|
+
import { loadConfigScopes, mergeConfig } from '../../core/config.js';
|
|
6
|
+
import { sessions } from '../../db/schema.js';
|
|
7
|
+
import { buildFallbackChain } from '../../llm/routing.js';
|
|
3
8
|
import { fromDirectory } from '../../project/index.js';
|
|
4
|
-
import {
|
|
9
|
+
import { getProject } from '../../project/project.js';
|
|
10
|
+
import { archiveOriginalEntries, listArchives } from '../../session/archive.js';
|
|
5
11
|
import { BranchPointOutOfRangeError, forkSession, getBranches, getTree, } from '../../session/branch.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
12
|
+
import { importSessionData } from '../../session/import.js';
|
|
13
|
+
import { deleteEntriesByIds, getMessages, getSteeringAsMessages, insertEntry, } from '../../session/message.js';
|
|
14
|
+
import { createSession, emptyTrash, getLLMSegments, getSession, listDeletedSessions, listOrphanDeletedSessions, listSessions, listSessionsByProject, permanentlyDeleteSession, purgeEmptySession, rebindSession, restoreSessionCore, searchSessions, softDeleteSession, touchLastOpened, touchTrashSeen, updateSessionTitle, } from '../../session/session.js';
|
|
8
15
|
import { applyShakeRegions, collectShakeRegions, DEFAULT_SHAKE_CONFIG, toRegionViews, } from '../../session/shake.js';
|
|
9
16
|
import { estimateMessageTokens } from '../../session/token.js';
|
|
10
17
|
import { generateId } from '../../shared/index.js';
|
|
11
18
|
import { apiError } from '../middleware/error.js';
|
|
19
|
+
import { buildRegistryFromConfig } from '../registry-config.js';
|
|
20
|
+
import { hasBusySession, resolveAgentCwd } from './chat.js';
|
|
12
21
|
function createSessionRoute(ctx) {
|
|
13
22
|
const app = new Hono();
|
|
23
|
+
// 会话导入:GET /:id/export 的逆操作(数据备份/迁移闭环)。
|
|
24
|
+
// 消息/归档重新生成 id(保留内容与时间戳),同库复制、重复导入均安全。
|
|
25
|
+
// 绑定 projectId 后立即出现在对应项目视图。
|
|
26
|
+
// P1-2:projectId 必填——无归属会话在任何项目视图都不可见,导入即孤儿。
|
|
27
|
+
app.post('/import', async (c) => {
|
|
28
|
+
const body = (await c.req.json().catch(() => null));
|
|
29
|
+
if (body?.version !== 1 || !body?.session || !Array.isArray(body?.messages)) {
|
|
30
|
+
return apiError(c, 400, 'INVALID_EXPORT', '无效的会话导出 JSON:需要 version/session/messages 字段');
|
|
31
|
+
}
|
|
32
|
+
// P3:导入体积上限——导出文件可任意大,无上限的数组会让服务端
|
|
33
|
+
// 逐条插入时 OOM/长事务。上限按合理会话规模取 20 倍余量。
|
|
34
|
+
const MAX_IMPORT_MESSAGES = 20000;
|
|
35
|
+
const MAX_IMPORT_ARCHIVES = 2000;
|
|
36
|
+
if (body.messages.length > MAX_IMPORT_MESSAGES) {
|
|
37
|
+
return apiError(c, 400, 'IMPORT_TOO_LARGE', `导入消息数 ${body.messages.length} 超过上限 ${MAX_IMPORT_MESSAGES}`);
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(body.archives) && body.archives.length > MAX_IMPORT_ARCHIVES) {
|
|
40
|
+
return apiError(c, 400, 'IMPORT_TOO_LARGE', `导入归档数 ${body.archives.length} 超过上限 ${MAX_IMPORT_ARCHIVES}`);
|
|
41
|
+
}
|
|
42
|
+
const projectId = typeof body.projectId === 'string' && body.projectId ? body.projectId : '';
|
|
43
|
+
if (!projectId) {
|
|
44
|
+
return apiError(c, 400, 'PROJECT_REQUIRED', '导入会话必须指定目标项目(projectId)');
|
|
45
|
+
}
|
|
46
|
+
const project = await getProject(ctx.db, projectId);
|
|
47
|
+
if (!project) {
|
|
48
|
+
return apiError(c, 404, 'PROJECT_NOT_FOUND', '目标项目不存在');
|
|
49
|
+
}
|
|
50
|
+
const title = typeof body.session.title === 'string' && body.session.title
|
|
51
|
+
? body.session.title
|
|
52
|
+
: '导入的会话';
|
|
53
|
+
// P0:权限态仅当调用方显式确认(importPermissions=true)时随迁。
|
|
54
|
+
// 默认剥离——用户在导出会话中建立的授权信任不应跨机器静默生效。
|
|
55
|
+
const importPermissions = body.importPermissions === true;
|
|
56
|
+
const metadata = {};
|
|
57
|
+
if (importPermissions) {
|
|
58
|
+
const srcMeta = body.session.metadata;
|
|
59
|
+
if (srcMeta && typeof srcMeta === 'object' && !Array.isArray(srcMeta)) {
|
|
60
|
+
const m = srcMeta;
|
|
61
|
+
if (m.permissionMode === 'auto' || m.permissionMode === 'default') {
|
|
62
|
+
metadata.permissionMode = m.permissionMode;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(m.alwaysAllow)) {
|
|
65
|
+
metadata.alwaysAllow = m.alwaysAllow.filter((x) => typeof x === 'string');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// P2:携带用量 segments(会话信息面板/徽标显示历史成本与 token)。
|
|
70
|
+
// 与权限态不同,segments 是纯统计数据无安全含义,默认随迁。
|
|
71
|
+
// 宽松校验形状:非法调用静默丢弃,不阻塞导入。
|
|
72
|
+
// P0-1:导入的调用从未对本机 API key 计费——剥离 calls[].id 使
|
|
73
|
+
// backfillUsageEvents 自然跳过它们(要求 id 非空),成本账本保持
|
|
74
|
+
// 「本机真实消费」语义;cost/usage 字段保留,展示继承成本不受影响。
|
|
75
|
+
// P2-7:calls 按导入消息时间窗裁剪——导出后原会话新增的调用不属于本副本。
|
|
76
|
+
const importedMsgs = body.messages;
|
|
77
|
+
let maxMsgTs = 0;
|
|
78
|
+
for (const m of importedMsgs) {
|
|
79
|
+
const raw = m?.createdAt;
|
|
80
|
+
const ts = typeof raw === 'number' && Number.isFinite(raw) && raw > 0
|
|
81
|
+
? raw
|
|
82
|
+
: typeof raw === 'string'
|
|
83
|
+
? Date.parse(raw)
|
|
84
|
+
: Number.NaN;
|
|
85
|
+
if (Number.isFinite(ts) && ts > maxMsgTs)
|
|
86
|
+
maxMsgTs = ts;
|
|
87
|
+
}
|
|
88
|
+
const srcMetaAny = body.session?.metadata;
|
|
89
|
+
if (maxMsgTs > 0 && Array.isArray(srcMetaAny?.segments)) {
|
|
90
|
+
metadata.segments = srcMetaAny.segments
|
|
91
|
+
.filter((seg) => !!seg &&
|
|
92
|
+
typeof seg === 'object' &&
|
|
93
|
+
!Array.isArray(seg) &&
|
|
94
|
+
typeof seg.provider === 'string' &&
|
|
95
|
+
typeof seg.model === 'string' &&
|
|
96
|
+
Array.isArray(seg.calls))
|
|
97
|
+
.map((seg) => ({
|
|
98
|
+
...seg,
|
|
99
|
+
calls: seg.calls
|
|
100
|
+
.filter((c) => {
|
|
101
|
+
const ts = c?.timestamp;
|
|
102
|
+
return typeof ts === 'number' && ts > 0 && ts <= maxMsgTs;
|
|
103
|
+
})
|
|
104
|
+
.map(({ id: _stripped, ...rest }) => rest),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
// P2:导出含分支树结构(parentId),导入为独立根会话——告知前端提示扁平化。
|
|
108
|
+
const flattened = typeof body.session.parentId === 'string' && body.session.parentId.length > 0;
|
|
109
|
+
const result = await importSessionData(ctx.db, {
|
|
110
|
+
title,
|
|
111
|
+
projectId,
|
|
112
|
+
messages: body.messages,
|
|
113
|
+
archives: Array.isArray(body.archives)
|
|
114
|
+
? body.archives
|
|
115
|
+
: [],
|
|
116
|
+
metadata,
|
|
117
|
+
});
|
|
118
|
+
return c.json({ ok: true, ...result, flattened, permissionsMigrated: importPermissions });
|
|
119
|
+
});
|
|
14
120
|
// 创建会话
|
|
15
121
|
app.post('/', async (c) => {
|
|
16
|
-
const body = await c.req.json().catch(() => ({}));
|
|
122
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
17
123
|
const title = body.title ?? 'New Session';
|
|
18
124
|
const directory = body.directory;
|
|
19
125
|
const explicitProjectId = body.projectId;
|
|
@@ -25,6 +131,11 @@ function createSessionRoute(ctx) {
|
|
|
25
131
|
else if (explicitProjectId) {
|
|
26
132
|
projectId = explicitProjectId;
|
|
27
133
|
}
|
|
134
|
+
// P2 修复:projectId 必填——无归属 web 会话在任何项目视图都不可见,
|
|
135
|
+
// 且非删除孤儿无任何 UI 入口,API 直调会创建不可达会话。
|
|
136
|
+
if (!projectId) {
|
|
137
|
+
return apiError(c, 400, 'PROJECT_REQUIRED', '创建会话必须指定项目:传 directory(自动解析)或 projectId');
|
|
138
|
+
}
|
|
28
139
|
const session = await createSession(ctx.db, title, projectId);
|
|
29
140
|
return c.json(session, 201);
|
|
30
141
|
});
|
|
@@ -41,17 +152,53 @@ function createSessionRoute(ctx) {
|
|
|
41
152
|
const tree = await getTree(ctx.db);
|
|
42
153
|
return c.json(tree);
|
|
43
154
|
});
|
|
44
|
-
//
|
|
155
|
+
// 跨会话搜索(P2-6):标题 + 消息内容匹配;?q= 关键词,?projectId= 限定项目。
|
|
156
|
+
// P3:?includeDeleted=1 搜索回收站(配合回收站搜索框)。
|
|
157
|
+
// 注册在 /:id 之前避免被参数路由吞掉。
|
|
158
|
+
app.get('/search', async (c) => {
|
|
159
|
+
const q = c.req.query('q') ?? '';
|
|
160
|
+
const projectId = c.req.query('projectId');
|
|
161
|
+
const includeDeleted = c.req.query('includeDeleted') === '1' || c.req.query('includeDeleted') === 'true';
|
|
162
|
+
if (!q.trim())
|
|
163
|
+
return c.json({ results: [] });
|
|
164
|
+
const results = await searchSessions(ctx.db, q, projectId, { includeDeleted });
|
|
165
|
+
return c.json({ results });
|
|
166
|
+
});
|
|
167
|
+
// 回收站:已软删除的会话列表(必须注册在 /:id 之前,避免被参数路由吞掉)。
|
|
168
|
+
// ?projectId= 过滤本项目(P1-7:回收站此前全库共享,跨项目可见可清空)。
|
|
169
|
+
// ?orphan=1 → 仅列出未归属项目的已删会话(删除项目后 FK set null 导致的孤儿,F1)。
|
|
170
|
+
// A3:孤儿列表查询不再自动标记「已看到」——标记改为用户展开分组时显式调用
|
|
171
|
+
// POST /deleted/orphans/seen,避免打开任意项目回收站连带启动无关孤儿倒计时。
|
|
45
172
|
app.get('/deleted', async (c) => {
|
|
46
|
-
const
|
|
47
|
-
|
|
173
|
+
const projectId = c.req.query('projectId');
|
|
174
|
+
if (c.req.query('orphan') === '1') {
|
|
175
|
+
return c.json(await listOrphanDeletedSessions(ctx.db));
|
|
176
|
+
}
|
|
177
|
+
await touchTrashSeen(ctx.db, projectId ? { projectId } : {});
|
|
178
|
+
return c.json(await listDeletedSessions(ctx.db, projectId));
|
|
179
|
+
});
|
|
180
|
+
// A3:孤儿条目计数(不标记 seen;分组折叠时展示「N 条」用)。
|
|
181
|
+
app.get('/deleted/orphans/count', async (c) => {
|
|
182
|
+
const orphans = await listOrphanDeletedSessions(ctx.db);
|
|
183
|
+
return c.json({ count: orphans.length });
|
|
184
|
+
});
|
|
185
|
+
// A3:用户展开「未归属项目」分组时显式标记孤儿已看到(保留期自此刻起算)。
|
|
186
|
+
app.post('/deleted/orphans/seen', async (c) => {
|
|
187
|
+
const touched = await touchTrashSeen(ctx.db, { orphan: true });
|
|
188
|
+
return c.json({ ok: true, touched });
|
|
189
|
+
});
|
|
190
|
+
// 清空回收站:物理删除所有软删除会话(不可恢复)。?projectId= 仅清空该项目。
|
|
191
|
+
app.delete('/deleted', async (c) => {
|
|
192
|
+
const projectId = c.req.query('projectId');
|
|
193
|
+
const count = await emptyTrash(ctx.db, projectId);
|
|
194
|
+
return c.json({ ok: true, deleted: count });
|
|
48
195
|
});
|
|
49
196
|
// 获取会话详情
|
|
50
197
|
app.get('/:id', async (c) => {
|
|
51
198
|
try {
|
|
52
199
|
const session = await getSession(ctx.db, c.req.param('id'));
|
|
53
|
-
if (!session) {
|
|
54
|
-
return apiError(c, 404, 'NOT_FOUND', '
|
|
200
|
+
if (!session || session.deletedAt) {
|
|
201
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
55
202
|
}
|
|
56
203
|
return c.json(session);
|
|
57
204
|
}
|
|
@@ -59,9 +206,47 @@ function createSessionRoute(ctx) {
|
|
|
59
206
|
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
60
207
|
}
|
|
61
208
|
});
|
|
209
|
+
// P2-5:会话重命名(此前标题只能由 LLM 自动生成,用户无法修改)
|
|
210
|
+
app.patch('/:id', async (c) => {
|
|
211
|
+
const id = c.req.param('id');
|
|
212
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
213
|
+
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
214
|
+
if (!title)
|
|
215
|
+
return apiError(c, 400, 'BAD_REQUEST', 'title is required');
|
|
216
|
+
if (title.length > 120) {
|
|
217
|
+
return apiError(c, 400, 'BAD_REQUEST', 'title must be at most 120 characters');
|
|
218
|
+
}
|
|
219
|
+
const session = await getSession(ctx.db, id);
|
|
220
|
+
if (!session)
|
|
221
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
222
|
+
// P3 口径统一:回收站会话不可改名(与 GET /:id、/messages 的 404 语义一致)。
|
|
223
|
+
if (session.deletedAt) {
|
|
224
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
225
|
+
}
|
|
226
|
+
await updateSessionTitle(ctx.db, id, title);
|
|
227
|
+
return c.json({ ok: true, title });
|
|
228
|
+
});
|
|
62
229
|
// 分支会话:未指定 messageIndex 时默认在最新一条消息处分叉(fork=完整副本语义)
|
|
63
230
|
app.post('/:id/fork', async (c) => {
|
|
64
231
|
const id = c.req.param('id');
|
|
232
|
+
// P3:回收站会话不可分支——与 GET /:id、PATCH /:id 的 404 口径一致,
|
|
233
|
+
// 否则 API 直调 fork 已删除会话即可复活其内容、绕过回收站语义。
|
|
234
|
+
const existing = await getSession(ctx.db, id);
|
|
235
|
+
if (!existing)
|
|
236
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
237
|
+
if (existing.deletedAt) {
|
|
238
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
239
|
+
}
|
|
240
|
+
// P3:fork 复制消息树,与正在写库的活跃 run 存在竞态——先拒绝。
|
|
241
|
+
// 守卫与斜杠 /fork 同口径(hasBusySession):此前只查 get(),
|
|
242
|
+
// 漏 tryAcquire→register 占位窗口与工作流运行期。
|
|
243
|
+
const busy = hasBusySession(ctx, id);
|
|
244
|
+
if (busy === 'run' || busy === 'workflow') {
|
|
245
|
+
return apiError(c, 409, 'RUN_ACTIVE', '该会话已有进行中的对话,请等待完成或中止后再分支');
|
|
246
|
+
}
|
|
247
|
+
if (busy === 'starting') {
|
|
248
|
+
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
249
|
+
}
|
|
65
250
|
const body = await c.req.json().catch(() => ({}));
|
|
66
251
|
let messageIndex = body.messageIndex;
|
|
67
252
|
if (messageIndex === undefined || !Number.isFinite(messageIndex)) {
|
|
@@ -83,36 +268,247 @@ function createSessionRoute(ctx) {
|
|
|
83
268
|
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
84
269
|
}
|
|
85
270
|
});
|
|
86
|
-
// 删除会话(软删除:级联其 fork 后代进入回收站,
|
|
271
|
+
// 删除会话(软删除:级联其 fork 后代进入回收站,60 天后物理清除)。
|
|
272
|
+
// P2 修复:删除运行中的会话前先中止其活跃 run(含子 agent)——
|
|
273
|
+
// 否则 run 继续向已删除会话写入消息,用户以为已删除而 agent 仍在执行。
|
|
274
|
+
// P1 工作流路由:发起会话有进行中的工作流时一并中止 workflow run
|
|
275
|
+
//(此前仅中止会话自身 run + 子 agent,工作流继续后台执行且 busy 映射悬挂)。
|
|
87
276
|
app.delete('/:id', async (c) => {
|
|
88
|
-
const
|
|
277
|
+
const id = c.req.param('id');
|
|
278
|
+
// starting 占位无法 abort(尚无 run)——删除会让随后 register 的 run 写入
|
|
279
|
+
// 已软删除会话。与控制端点同口径:占位期 409,稍后重试。
|
|
280
|
+
if (ctx.agentManager.isStarting(id)) {
|
|
281
|
+
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
282
|
+
}
|
|
283
|
+
const busyWorkflowId = ctx.workflowBusyBySession.get(id);
|
|
284
|
+
if (busyWorkflowId) {
|
|
285
|
+
ctx.agentManager.abort(busyWorkflowId);
|
|
286
|
+
for (const child of ctx.agentManager.children(busyWorkflowId)) {
|
|
287
|
+
ctx.agentManager.abort(child.sessionId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
ctx.agentManager.abort(id);
|
|
291
|
+
for (const child of ctx.agentManager.children(id)) {
|
|
292
|
+
ctx.agentManager.abort(child.sessionId);
|
|
293
|
+
}
|
|
294
|
+
const ok = await softDeleteSession(ctx.db, id);
|
|
89
295
|
if (!ok)
|
|
90
296
|
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
91
297
|
return c.body(null, 204);
|
|
92
298
|
});
|
|
93
|
-
//
|
|
299
|
+
// 彻底删除回收站会话(不可恢复):会话 + 全部后代物理清除
|
|
300
|
+
app.delete('/:id/forever', async (c) => {
|
|
301
|
+
let count;
|
|
302
|
+
try {
|
|
303
|
+
count = await permanentlyDeleteSession(ctx.db, c.req.param('id'));
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或不在回收站');
|
|
307
|
+
}
|
|
308
|
+
if (count === 0)
|
|
309
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或不在回收站');
|
|
310
|
+
return c.json({ ok: true, deleted: count });
|
|
311
|
+
});
|
|
312
|
+
// P3-9:物理删除真空会话(无消息且无子会话)——首条消息发送失败后的前端
|
|
313
|
+
// 清理路径。此前走 DELETE /:id 软删除,空壳进回收站污染列表;非空会话 409,
|
|
314
|
+
// 物理删除不绕过回收站语义。
|
|
315
|
+
app.delete('/:id/empty', async (c) => {
|
|
316
|
+
const ok = await purgeEmptySession(ctx.db, c.req.param('id'));
|
|
317
|
+
if (!ok) {
|
|
318
|
+
return apiError(c, 409, 'SESSION_NOT_EMPTY', '会话不存在或非空(请使用常规删除,移入回收站)');
|
|
319
|
+
}
|
|
320
|
+
return c.json({ ok: true });
|
|
321
|
+
});
|
|
322
|
+
// 会话归档列表(compaction/squash/shake/clear 的原始内容);?q= 搜索归档文本
|
|
323
|
+
app.get('/:id/archives', async (c) => {
|
|
324
|
+
const id = c.req.param('id');
|
|
325
|
+
let session;
|
|
326
|
+
try {
|
|
327
|
+
session = await getSession(ctx.db, id);
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
331
|
+
}
|
|
332
|
+
if (!session)
|
|
333
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
334
|
+
const q = c.req.query('q');
|
|
335
|
+
const archives = await listArchives(ctx.db, id, q);
|
|
336
|
+
return c.json({ archives });
|
|
337
|
+
});
|
|
338
|
+
// 会话导出:元数据 + 消息 + 全部归档(JSON 下载,数据可迁移)
|
|
339
|
+
app.get('/:id/export', async (c) => {
|
|
340
|
+
const id = c.req.param('id');
|
|
341
|
+
let session;
|
|
342
|
+
try {
|
|
343
|
+
session = await getSession(ctx.db, id);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
347
|
+
}
|
|
348
|
+
if (!session)
|
|
349
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
350
|
+
// P3 口径统一:回收站会话不可导出——与 GET /:id、PATCH /:id、/messages、
|
|
351
|
+
// /llm-details 的 404 语义一致。回收站内容经「恢复」后即可导出。
|
|
352
|
+
if (session.deletedAt) {
|
|
353
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
354
|
+
}
|
|
355
|
+
const messages = await getMessages(ctx.db, id);
|
|
356
|
+
const archives = await listArchives(ctx.db, id);
|
|
357
|
+
// P2-6:归档的 fileSnapshots 含被压缩进上下文的文件内容——导出 JSON 常被
|
|
358
|
+
// 分享/迁移,默认剥离(隐私优先);显式 ?includeSnapshots=1 才携带。
|
|
359
|
+
const includeSnapshots = c.req.query('includeSnapshots') === '1';
|
|
360
|
+
// P2-1:权限态(permissionMode/alwaysAllow)与 fileSnapshots 同口径默认剥离。
|
|
361
|
+
// 导出 JSON 被分享时不应携带用户的工具授权白名单结构;显式
|
|
362
|
+
// ?includePermissions=1 才保留(配合导入侧 importPermissions=true 完成
|
|
363
|
+
// 同机备份/迁移的权限态闭环——两开关对称)。
|
|
364
|
+
const includePermissions = c.req.query('includePermissions') === '1';
|
|
365
|
+
const exportSession = includePermissions
|
|
366
|
+
? session
|
|
367
|
+
: {
|
|
368
|
+
...session,
|
|
369
|
+
metadata: (() => {
|
|
370
|
+
const m = { ...(session.metadata ?? {}) };
|
|
371
|
+
delete m.permissionMode;
|
|
372
|
+
delete m.alwaysAllow;
|
|
373
|
+
return m;
|
|
374
|
+
})(),
|
|
375
|
+
};
|
|
376
|
+
return c.json({
|
|
377
|
+
version: 1,
|
|
378
|
+
exportedAt: new Date().toISOString(),
|
|
379
|
+
session: exportSession,
|
|
380
|
+
messages,
|
|
381
|
+
archives: includeSnapshots
|
|
382
|
+
? archives
|
|
383
|
+
: archives.map((a) => ({ ...a, fileSnapshots: undefined })),
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
// 恢复会话(从回收站还原;仅还原该会话本身)。
|
|
387
|
+
// P1 可达性修复:会话所属项目已被删除时(FK set null),按 worktreePath 重建
|
|
388
|
+
// 项目记录并重新归属——否则恢复成功但会话不属于任何项目视图,UI 不可达。
|
|
389
|
+
// P1-2:body.projectId 提供时(当前项目视图),目录失效无法重建归属的孤儿
|
|
390
|
+
// 会话直接归属到该请求项目,恢复即可达。
|
|
391
|
+
// P1-3:静默重建项目改为明示——响应带 recreatedProject(重建的项目信息),
|
|
392
|
+
// 前端据此展示「已重新创建项目 X」;同时支持 restoreMode='current-project'
|
|
393
|
+
// 显式跳过重建、直接归属到请求项目(用户在确认框中二选一)。
|
|
94
394
|
app.post('/:id/restore', async (c) => {
|
|
95
|
-
const
|
|
96
|
-
|
|
395
|
+
const id = c.req.param('id');
|
|
396
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
397
|
+
const requestProjectId = typeof body.projectId === 'string' && body.projectId ? body.projectId : undefined;
|
|
398
|
+
const preferCurrentProject = body.restoreMode === 'current-project';
|
|
399
|
+
const result = await restoreSessionCore(ctx.db, id);
|
|
400
|
+
if (!result.restored)
|
|
97
401
|
return apiError(c, 404, 'NOT_FOUND', '会话不存在或未删除');
|
|
98
|
-
|
|
402
|
+
const session = await getSession(ctx.db, id);
|
|
403
|
+
let rebound = false;
|
|
404
|
+
let orphaned = false;
|
|
405
|
+
let recreatedProject = null;
|
|
406
|
+
if (session && !session.projectId) {
|
|
407
|
+
const wt = session.worktreePath;
|
|
408
|
+
// P1-3:用户选择「归属到当前项目」时跳过自动重建(删除项目是用户意图,
|
|
409
|
+
// 不静默复活)。否则目录仍存在则重建项目记录(明示在响应中)。
|
|
410
|
+
if (!preferCurrentProject && wt && existsSync(wt)) {
|
|
411
|
+
try {
|
|
412
|
+
const project = await fromDirectory(ctx.db, wt);
|
|
413
|
+
await ctx.db.db.update(sessions).set({ projectId: project.id }).where(eq(sessions.id, id));
|
|
414
|
+
rebound = true;
|
|
415
|
+
recreatedProject = { id: project.id, name: project.name };
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
orphaned = true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
orphaned = true;
|
|
423
|
+
}
|
|
424
|
+
// P1-2:目录失效且请求方提供了项目上下文 → 归属到该请求项目;
|
|
425
|
+
// P1-3:restoreMode='current-project' 时无论目录是否存在都归属请求项目。
|
|
426
|
+
if (orphaned && requestProjectId) {
|
|
427
|
+
const project = await getProject(ctx.db, requestProjectId);
|
|
428
|
+
if (project) {
|
|
429
|
+
await rebindSession(ctx.db, id, project);
|
|
430
|
+
orphaned = false;
|
|
431
|
+
rebound = true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return c.json({
|
|
436
|
+
ok: true,
|
|
437
|
+
rebound,
|
|
438
|
+
orphaned,
|
|
439
|
+
recreatedProject,
|
|
440
|
+
restoredAncestorCount: result.restoredAncestorCount,
|
|
441
|
+
crossedBatchAncestor: result.crossedBatchAncestor,
|
|
442
|
+
// A2:批次不同的已删后代滞留在回收站,显式告知数量供前端提示单独恢复。
|
|
443
|
+
leftBehindDescendantCount: result.leftBehindDescendantCount,
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
// 会话归属变更(P1-2):孤儿会话归属到指定项目,恢复后即可达。
|
|
447
|
+
app.post('/:id/rebind', async (c) => {
|
|
448
|
+
const id = c.req.param('id');
|
|
449
|
+
// P3 口径统一:归属变更与 fork/compact/shake 同用 hasBusySession 守卫——
|
|
450
|
+
// 活跃 run 的 cwd 在启动时解析,rebind 会改变后续轮次的工作目录语义。
|
|
451
|
+
const busy = hasBusySession(ctx, id);
|
|
452
|
+
if (busy === 'run' || busy === 'workflow') {
|
|
453
|
+
return apiError(c, 409, 'RUN_ACTIVE', '该会话已有进行中的对话,请等待完成或中止后再更改归属');
|
|
454
|
+
}
|
|
455
|
+
if (busy === 'starting') {
|
|
456
|
+
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
457
|
+
}
|
|
458
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
459
|
+
const projectId = typeof body.projectId === 'string' && body.projectId ? body.projectId : '';
|
|
460
|
+
if (!projectId)
|
|
461
|
+
return apiError(c, 400, 'PROJECT_REQUIRED', 'projectId is required');
|
|
462
|
+
const project = await getProject(ctx.db, projectId);
|
|
463
|
+
if (!project)
|
|
464
|
+
return apiError(c, 404, 'PROJECT_NOT_FOUND', '目标项目不存在');
|
|
465
|
+
const ok = await rebindSession(ctx.db, id, project);
|
|
466
|
+
if (!ok)
|
|
467
|
+
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
468
|
+
return c.json({ ok: true, projectId: project.id });
|
|
99
469
|
});
|
|
100
|
-
//
|
|
470
|
+
// 获取消息列表(含 steering 条目:P0 追加指令持久化后需在时间线可见)
|
|
101
471
|
app.get('/:id/messages', async (c) => {
|
|
102
|
-
const
|
|
103
|
-
|
|
472
|
+
const id = c.req.param('id');
|
|
473
|
+
const session = await getSession(ctx.db, id);
|
|
474
|
+
if (!session || session.deletedAt) {
|
|
475
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
476
|
+
}
|
|
477
|
+
const [messages, steering] = await Promise.all([
|
|
478
|
+
getMessages(ctx.db, id),
|
|
479
|
+
getSteeringAsMessages(ctx.db, id),
|
|
480
|
+
]);
|
|
481
|
+
if (steering.length === 0)
|
|
482
|
+
return c.json(messages);
|
|
483
|
+
// 按 createdAt 交错合并(steering 与消息时间序一致)
|
|
484
|
+
return c.json([...messages, ...steering].sort((a, b) => a.createdAt - b.createdAt));
|
|
104
485
|
});
|
|
105
486
|
// 获取 LLM 调用分段(段首快照 + 段内轻量 calls):优先取活跃 run 的内存记录(实时),回退 DB 持久化
|
|
106
487
|
app.get('/:id/llm-details', async (c) => {
|
|
107
488
|
const run = ctx.agentManager.get(c.req.param('id'));
|
|
108
489
|
if (run)
|
|
109
490
|
return c.json(run.state.segments);
|
|
491
|
+
const session = await getSession(ctx.db, c.req.param('id'));
|
|
492
|
+
// P3 口径统一:回收站会话的持久化分段不再下发(会话本身已 404)。
|
|
493
|
+
if (!session || session.deletedAt) {
|
|
494
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
495
|
+
}
|
|
110
496
|
const persisted = await getLLMSegments(ctx.db, c.req.param('id'));
|
|
111
497
|
return c.json(persisted);
|
|
112
498
|
});
|
|
113
499
|
// 手动触发会话压缩(段切换确认弹窗「顺便压缩」调用)。用末段的 provider/model 构建摘要器。
|
|
500
|
+
// P1 多项目:keepRecentTokens 按会话项目配置解析(此前用启动目录配置)。
|
|
114
501
|
app.post('/:id/compact', async (c) => {
|
|
115
502
|
const id = c.req.param('id');
|
|
503
|
+
// P2-5 修复:守卫收敛 hasBusySession(与斜杠 /compact、REST fork 同口径)——
|
|
504
|
+
// 此前只查 get(),漏 tryAcquire→register 占位窗口与工作流运行期两态。
|
|
505
|
+
const busy = hasBusySession(ctx, id);
|
|
506
|
+
if (busy === 'run' || busy === 'workflow') {
|
|
507
|
+
return apiError(c, 409, 'RUN_ACTIVE', '该会话已有进行中的对话,请等待完成或中止后再压缩');
|
|
508
|
+
}
|
|
509
|
+
if (busy === 'starting') {
|
|
510
|
+
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
511
|
+
}
|
|
116
512
|
let session;
|
|
117
513
|
try {
|
|
118
514
|
session = await getSession(ctx.db, id);
|
|
@@ -122,14 +518,35 @@ function createSessionRoute(ctx) {
|
|
|
122
518
|
}
|
|
123
519
|
if (!session)
|
|
124
520
|
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
521
|
+
let sessionConfig = ctx.config;
|
|
522
|
+
let sessionRegistry = ctx.llmRegistry;
|
|
523
|
+
try {
|
|
524
|
+
const sessionCwd = await resolveAgentCwd(ctx, session);
|
|
525
|
+
if (sessionCwd !== ctx.cwd) {
|
|
526
|
+
const projectScope = loadConfigScopes(sessionCwd).project;
|
|
527
|
+
if (projectScope) {
|
|
528
|
+
sessionConfig = mergeConfig(ctx.config, projectScope);
|
|
529
|
+
// P1-1:压缩摘要同样需要项目级 provider 注册表。
|
|
530
|
+
if (sessionConfig.providers.length > 0) {
|
|
531
|
+
sessionRegistry = buildRegistryFromConfig(sessionConfig);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
// cwd 解析失败不阻塞压缩:回退服务级配置(压缩优于报错)
|
|
538
|
+
}
|
|
125
539
|
const segs = await getLLMSegments(ctx.db, id);
|
|
126
540
|
const lastSeg = segs[segs.length - 1];
|
|
127
|
-
const provider = lastSeg?.provider ??
|
|
128
|
-
const model = lastSeg?.model ??
|
|
541
|
+
const provider = lastSeg?.provider ?? sessionConfig.defaultProvider;
|
|
542
|
+
const model = lastSeg?.model ?? sessionConfig.defaultModel;
|
|
129
543
|
try {
|
|
130
|
-
const
|
|
544
|
+
const fallback = buildFallbackChain(sessionConfig, provider, model);
|
|
545
|
+
const summarizer = createSummarizer(sessionRegistry, provider, model, {
|
|
546
|
+
...(fallback ? { fallback } : {}),
|
|
547
|
+
});
|
|
131
548
|
const result = await runCompaction(ctx.db, id, summarizer, {
|
|
132
|
-
keepRecentTokens:
|
|
549
|
+
keepRecentTokens: sessionConfig.compaction.keepRecentTokens,
|
|
133
550
|
});
|
|
134
551
|
return c.json(result);
|
|
135
552
|
}
|
|
@@ -138,10 +555,18 @@ function createSessionRoute(ctx) {
|
|
|
138
555
|
}
|
|
139
556
|
});
|
|
140
557
|
// 获取会话状态:内存有活跃 run → 返回其 status;否则查 DB lastRun。
|
|
558
|
+
// P1 工作流路由:发起会话在 workflowBusyBySession 中时状态取实际 workflow run
|
|
559
|
+
//(此前工作流运行期间状态栏显示 idle)。
|
|
141
560
|
// lastRun.status='running' 且无活跃 run → 服务重启被中断。
|
|
142
561
|
// lastRun.status='paused' 但无活跃 run(热更新/重启后 run 状态未迁移)→ 同样按中断处理:
|
|
143
562
|
// agent 内存态已丢失,resume 端点无法恢复,唯一可行路径是重发上一条消息。
|
|
144
563
|
app.get('/:id/status', async (c) => {
|
|
564
|
+
const busyWorkflowId = ctx.workflowBusyBySession.get(c.req.param('id'));
|
|
565
|
+
if (busyWorkflowId) {
|
|
566
|
+
const wfRun = ctx.agentManager.get(busyWorkflowId);
|
|
567
|
+
if (wfRun)
|
|
568
|
+
return c.json(wfRun.state.status);
|
|
569
|
+
}
|
|
145
570
|
const run = ctx.agentManager.get(c.req.param('id'));
|
|
146
571
|
if (run)
|
|
147
572
|
return c.json(run.state.status);
|
|
@@ -175,6 +600,15 @@ function createSessionRoute(ctx) {
|
|
|
175
600
|
// shake apply:归档原始内容 + 原位替换
|
|
176
601
|
app.post('/:id/shake/apply', async (c) => {
|
|
177
602
|
const id = c.req.param('id');
|
|
603
|
+
// P2-5 修复:守卫收敛 hasBusySession(与 compact/fork 同口径),
|
|
604
|
+
// 补 starting 占位与工作流运行期两态。
|
|
605
|
+
const busy = hasBusySession(ctx, id);
|
|
606
|
+
if (busy === 'run' || busy === 'workflow') {
|
|
607
|
+
return apiError(c, 409, 'RUN_ACTIVE', '该会话已有进行中的对话,请等待完成或中止后再 Shake');
|
|
608
|
+
}
|
|
609
|
+
if (busy === 'starting') {
|
|
610
|
+
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
611
|
+
}
|
|
178
612
|
let session;
|
|
179
613
|
try {
|
|
180
614
|
session = await getSession(ctx.db, id);
|
|
@@ -224,7 +658,13 @@ function createSessionRoute(ctx) {
|
|
|
224
658
|
});
|
|
225
659
|
// 记录会话打开(更新 metadata.lastOpenedAt,用于会话列表按最近打开排序)
|
|
226
660
|
app.post('/:id/open', async (c) => {
|
|
227
|
-
|
|
661
|
+
const id = c.req.param('id');
|
|
662
|
+
// P3 口径统一:不存在/回收站会话不再静默返回成功(与 GET /:id 404 一致)。
|
|
663
|
+
const session = await getSession(ctx.db, id);
|
|
664
|
+
if (!session || session.deletedAt) {
|
|
665
|
+
return apiError(c, 404, 'NOT_FOUND', '会话不存在或已删除');
|
|
666
|
+
}
|
|
667
|
+
await touchLastOpened(ctx.db, id);
|
|
228
668
|
return c.json({ ok: true });
|
|
229
669
|
});
|
|
230
670
|
// 获取分支
|
|
@@ -1,18 +1,37 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { loadConfigScopes, mergeConfig } from '../../core/config.js';
|
|
3
|
+
import { getProject } from '../../project/project.js';
|
|
4
|
+
import { resolveEnabledToolNames } from '../../tools/index.js';
|
|
2
5
|
import { listTools } from '../../tools/registry.js';
|
|
3
6
|
import { apiError } from '../middleware/error.js';
|
|
4
7
|
function createToolRoute(ctx) {
|
|
5
8
|
const app = new Hono();
|
|
6
|
-
// 列出可用工具(不含 execute
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
// 列出可用工具(不含 execute 函数)。
|
|
10
|
+
// P2 交集语义配套:列表按 config.tools.enabled 过滤——设置页禁用某工具后,
|
|
11
|
+
// 输入区工具开关不再提供该工具,避免「勾选了却被服务端静默丢弃」的脱节。
|
|
12
|
+
// ?projectId= 时按该项目合并配置过滤(与 chat 路由的工具解析同口径)。
|
|
13
|
+
app.get('/', async (c) => {
|
|
14
|
+
let config = ctx.config;
|
|
15
|
+
const projectId = c.req.query('projectId');
|
|
16
|
+
if (projectId) {
|
|
17
|
+
const project = await getProject(ctx.db, projectId);
|
|
18
|
+
if (!project)
|
|
19
|
+
return apiError(c, 404, 'PROJECT_NOT_FOUND', '项目不存在');
|
|
20
|
+
const projectScope = loadConfigScopes(project.worktree).project;
|
|
21
|
+
if (projectScope)
|
|
22
|
+
config = mergeConfig(ctx.config, projectScope);
|
|
23
|
+
}
|
|
24
|
+
const enabledNames = resolveEnabledToolNames(ctx.toolRegistry, config);
|
|
25
|
+
const enabledSet = new Set(enabledNames);
|
|
26
|
+
const tools = listTools(ctx.toolRegistry, { config: {}, cwd: ctx.cwd })
|
|
27
|
+
.filter((t) => enabledSet.has(t.name))
|
|
28
|
+
.map((t) => ({
|
|
10
29
|
name: t.name,
|
|
11
30
|
description: t.description,
|
|
12
31
|
parameters: t.parameters,
|
|
13
32
|
permission: t.permission,
|
|
14
33
|
}));
|
|
15
|
-
return c.json(
|
|
34
|
+
return c.json(tools);
|
|
16
35
|
});
|
|
17
36
|
// 确认工具执行权限
|
|
18
37
|
app.post('/confirm', async (c) => {
|
|
@@ -5,11 +5,17 @@ import type { ServerContext } from '../types.js';
|
|
|
5
5
|
* 路由本身不触发 npm registry 请求;调度器周期性 checkNow 写入缓存,
|
|
6
6
|
* 路由读缓存(避免每个前端轮询都打外网)。
|
|
7
7
|
*
|
|
8
|
-
* POST /api/update/apply — 手动触发热更新(spec §18.2
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* POST /api/update/apply — 手动触发热更新(spec §18.2),P0 修复后顺序:
|
|
9
|
+
* 1. checkNow 确认有更新;
|
|
10
|
+
* 2. performInstall(仅安装新版本)——失败时**尚未暂停任何会话**,
|
|
11
|
+
* 直接返回可操作错误(409 + 手动更新命令),用户工作零损失;
|
|
12
|
+
* 3. 安装成功 → pauseAll 暂停活跃 run 至安全点(原子操作完成);
|
|
13
|
+
* 4. serializeSessions → performHandoff(写快照 + spawn 新实例);
|
|
14
|
+
* 5. spawn 失败 → resumeAll 回滚暂停的会话,返回明确错误。
|
|
15
|
+
*
|
|
16
|
+
* 旧实例通过 handoff 端点优雅退出,新实例 restore 快照并接管端口。
|
|
17
|
+
* 响应立即返回(不阻塞 spawn),实际接管由 handoff IPC 异步完成。
|
|
18
|
+
* dev(vite)模式无 handoff server,返回 409——热更新仅在独立 serve 进程可用。
|
|
13
19
|
*/
|
|
14
20
|
declare function createUpdateRoute(ctx: ServerContext): Hono;
|
|
15
21
|
export { createUpdateRoute };
|