c0de-agent 1.12.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 +51 -23
- package/dist/cli/commands/acp.js +26 -8
- package/dist/cli/commands/chat.js +70 -6
- package/dist/cli/commands/config.d.ts +2 -0
- package/dist/cli/commands/config.js +46 -0
- package/dist/cli/commands/serve.js +11 -2
- package/dist/cli/commands/sessions.js +51 -4
- 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 +92 -12
- package/dist/cli/modes/acp.js +39 -8
- package/dist/cli/modes/print.d.ts +3 -0
- package/dist/cli/modes/print.js +71 -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/config.d.ts +43 -1
- package/dist/core/config.js +163 -14
- package/dist/core/loop/segment.d.ts +3 -1
- package/dist/core/loop/segment.js +24 -1
- package/dist/core/loop/subagent.js +65 -32
- package/dist/core/loop.d.ts +7 -0
- package/dist/core/loop.js +90 -6
- package/dist/core/slash.d.ts +10 -0
- package/dist/core/slash.js +150 -20
- package/dist/core/types.d.ts +20 -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 +356 -1
- package/dist/db/schema.js +57 -3
- package/dist/kanban/index.d.ts +2 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +84 -2
- package/dist/kanban/store.js +358 -25
- package/dist/llm/registry.d.ts +7 -1
- package/dist/llm/registry.js +7 -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 +23 -0
- package/dist/project/project.js +65 -0
- package/dist/project/trust.d.ts +65 -0
- package/dist/project/trust.js +328 -0
- package/dist/server/agent-manager.d.ts +6 -2
- package/dist/server/agent-manager.js +4 -2
- package/dist/server/app.js +45 -30
- package/dist/server/auth-manager.d.ts +15 -4
- package/dist/server/auth-manager.js +41 -24
- package/dist/server/context.js +11 -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 +5 -0
- package/dist/server/permission/interactive.js +4 -1
- package/dist/server/permission/store.d.ts +2 -0
- package/dist/server/permission/store.js +8 -0
- package/dist/server/routes/auth.js +35 -9
- package/dist/server/routes/chat.d.ts +16 -7
- package/dist/server/routes/chat.js +483 -28
- package/dist/server/routes/commands.d.ts +2 -2
- package/dist/server/routes/commands.js +4 -6
- package/dist/server/routes/config.js +81 -4
- package/dist/server/routes/files.js +120 -24
- package/dist/server/routes/kanban.js +152 -22
- package/dist/server/routes/permissions.js +7 -0
- package/dist/server/routes/project.js +76 -22
- package/dist/server/routes/provider.js +6 -0
- package/dist/server/routes/session.js +177 -13
- package/dist/server/routes/tool.js +24 -5
- package/dist/server/routes/update.js +58 -2
- 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 +12 -1
- package/dist/server/server.js +194 -33
- 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 +9 -0
- package/dist/session/branch.d.ts +2 -1
- package/dist/session/branch.js +75 -10
- package/dist/session/context.js +28 -2
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/session.d.ts +67 -10
- package/dist/session/session.js +180 -67
- package/dist/session/types.d.ts +2 -0
- package/dist/session/usage.d.ts +62 -0
- package/dist/session/usage.js +194 -0
- package/dist/shared/types/agent.d.ts +19 -3
- package/dist/shared/types/config.d.ts +59 -4
- package/dist/shared/types/kanban.d.ts +4 -2
- package/dist/shared/types/message.d.ts +14 -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 +40 -4
- 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/scheduler.js +5 -2
- package/dist/update/snapshot.d.ts +17 -3
- package/dist/update/snapshot.js +3 -2
- package/dist/update/updater.d.ts +2 -0
- package/dist/update/updater.js +2 -1
- package/dist/update/version.d.ts +3 -0
- package/dist/update/version.js +9 -3
- 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/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 +35 -0
- package/package.json +24 -23
- package/dist/core/agents/discovery.d.ts +0 -6
- package/dist/core/agents/discovery.js +0 -96
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
// src/server/routes/project.ts
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { parse } from 'node:path';
|
|
3
5
|
import { and, eq, isNull } from 'drizzle-orm';
|
|
4
6
|
import { Hono } from 'hono';
|
|
5
7
|
import { kanbanBoards, projects, sessions } from '../../db/schema.js';
|
|
6
|
-
import { fromDirectory, getProject, listProjects, relocateProject, resolveProject, updateProjectName, } from '../../project/index.js';
|
|
8
|
+
import { fromDirectory, getByDirectory, getProject, listProjects, relocateProject, resolveProject, trustProject, updateProjectName, } from '../../project/index.js';
|
|
7
9
|
import { clearTrashMarks } from '../../session/session.js';
|
|
8
10
|
import { generateId } from '../../shared/index.js';
|
|
9
11
|
import { apiError } from '../middleware/error.js';
|
|
10
12
|
import { expandPath } from './filesystem.js';
|
|
13
|
+
/** realpath 失败(目录不存在等)时返回 null,调用方回退原路径。 */
|
|
14
|
+
function realpathSafe(p) {
|
|
15
|
+
try {
|
|
16
|
+
return realpathSync(p);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
11
22
|
/** 实时取 git 分支(分支随时变,不存 DB)。worktree 不存在时标记缺失。 */
|
|
12
23
|
function withBranch(project) {
|
|
13
24
|
const worktreeMissing = !existsSync(project.worktree);
|
|
@@ -30,8 +41,21 @@ function createProjectRoute(ctx) {
|
|
|
30
41
|
const list = await listProjects(ctx.db);
|
|
31
42
|
return c.json(list.map(withBranch));
|
|
32
43
|
});
|
|
33
|
-
// 解析服务端 cwd
|
|
44
|
+
// 解析服务端 cwd 对应的项目(未注册则自动创建)。
|
|
45
|
+
// 注册过滤(P2):serve 在 home/文件系统根目录启动时不做自动注册——home 不是项目,
|
|
46
|
+
// 自动注册只会把 home 变成污染项目列表的假项目。已显式注册(/from-directory)的
|
|
47
|
+
// 目录照常返回,用户主动添加 home 为项目的选择被尊重。
|
|
34
48
|
app.get('/current', async (c) => {
|
|
49
|
+
const existing = await getByDirectory(ctx.db, ctx.cwd);
|
|
50
|
+
if (existing)
|
|
51
|
+
return c.json(withBranch(existing), 200);
|
|
52
|
+
const dir = realpathSafe(ctx.cwd) ?? ctx.cwd;
|
|
53
|
+
const home = realpathSafe(homedir());
|
|
54
|
+
const root = parse(dir).root;
|
|
55
|
+
if ((home && dir === home) || dir === root) {
|
|
56
|
+
return apiError(c, 404, 'PROJECT_DIR_REQUIRED', '当前目录(home 目录)不是项目,未自动注册。请在项目目录启动 c0de serve,' +
|
|
57
|
+
'或在项目列表中显式添加该目录。');
|
|
58
|
+
}
|
|
35
59
|
const project = await fromDirectory(ctx.db, ctx.cwd);
|
|
36
60
|
return c.json(withBranch(project), 200);
|
|
37
61
|
});
|
|
@@ -53,6 +77,14 @@ function createProjectRoute(ctx) {
|
|
|
53
77
|
return apiError(c, 404, 'NOT_FOUND', 'Project not found');
|
|
54
78
|
return c.json(withBranch(project));
|
|
55
79
|
});
|
|
80
|
+
// P0-2:显式信任项目——用户在信任确认弹窗中批准项目作用域配置/插件后调用。
|
|
81
|
+
// 信任是一次性动作(trustedAt 落盘);此后聊天入口不再拦截本项目。
|
|
82
|
+
app.post('/:id/trust', async (c) => {
|
|
83
|
+
const project = await trustProject(ctx.db, c.req.param('id'));
|
|
84
|
+
if (!project)
|
|
85
|
+
return apiError(c, 404, 'NOT_FOUND', 'Project not found');
|
|
86
|
+
return c.json({ ok: true, project: withBranch(project) });
|
|
87
|
+
});
|
|
56
88
|
// A1:项目重新定位——目录被移动/重命名后的恢复通道。
|
|
57
89
|
// 整体迁移会话与看板到新目录身份(替代「删项目 + 逐条恢复 + 看板丢失」)。
|
|
58
90
|
// 目标目录已注册为另一项目 → 409;有活跃 run → 拒绝(agent 工作目录悬空)。
|
|
@@ -65,15 +97,21 @@ function createProjectRoute(ctx) {
|
|
|
65
97
|
if (!existsSync(directory)) {
|
|
66
98
|
return apiError(c, 400, 'DIRECTORY_MISSING', `目录不存在或不可访问:${directory}`);
|
|
67
99
|
}
|
|
68
|
-
// 活跃 run 守卫(与 DELETE
|
|
100
|
+
// 活跃 run 守卫(与 DELETE 同语义;含 starting 占位态)
|
|
69
101
|
const rows = await ctx.db.db
|
|
70
102
|
.select({ id: sessions.id })
|
|
71
103
|
.from(sessions)
|
|
72
104
|
.where(eq(sessions.projectId, id));
|
|
73
|
-
const activeCount = rows.filter((r) => ctx.agentManager.get(r.id)).length;
|
|
105
|
+
const activeCount = rows.filter((r) => ctx.agentManager.get(r.id) !== undefined || ctx.agentManager.isStarting(r.id)).length;
|
|
74
106
|
if (activeCount > 0) {
|
|
75
107
|
return apiError(c, 409, 'PROJECT_HAS_ACTIVE_SESSIONS', `项目下有 ${activeCount} 个进行中的对话,请先中止后再重新定位`);
|
|
76
108
|
}
|
|
109
|
+
// P2-5:打开的终端守卫——终端绑定项目 worktree,重定位后 shell 仍留在旧目录,
|
|
110
|
+
// cwd 身份悬空。与活跃 run 同语义:先关闭再操作。
|
|
111
|
+
const openTerminals = ctx.ptyManager.list().filter((t) => t.projectId === id);
|
|
112
|
+
if (openTerminals.length > 0) {
|
|
113
|
+
return apiError(c, 409, 'PROJECT_HAS_OPEN_TERMINALS', `项目下有 ${openTerminals.length} 个打开的终端,请先关闭终端后再重新定位`);
|
|
114
|
+
}
|
|
77
115
|
try {
|
|
78
116
|
const project = await relocateProject(ctx.db, id, directory);
|
|
79
117
|
return c.json({ ok: true, project: withBranch(project) });
|
|
@@ -88,31 +126,35 @@ function createProjectRoute(ctx) {
|
|
|
88
126
|
return apiError(c, 500, 'RELOCATE_FAILED', msg);
|
|
89
127
|
}
|
|
90
128
|
});
|
|
91
|
-
// P1-9 + P2-2
|
|
129
|
+
// P1-9 + P2-2:删除项目记录。
|
|
92
130
|
// 会话不再留孤儿(FK set null 后在任何项目视图都不可见且 cwd 回退 serve 目录):
|
|
93
|
-
// 删除前把该项目全部未删除会话软删除进回收站(
|
|
131
|
+
// 删除前把该项目全部未删除会话软删除进回收站(60 天内可恢复),
|
|
94
132
|
// 并把 worktreePath 落盘——恢复后 resolveAgentCwd 仍能解析原工作目录。
|
|
133
|
+
// P2-5:看板同步软删除进回收站(可恢复/重新归属),不再永久级联销毁。
|
|
95
134
|
// 有活跃 run 绑定该项目时拒绝删除,避免 agent 工作目录悬空。
|
|
96
135
|
app.delete('/:id', async (c) => {
|
|
97
136
|
const id = c.req.param('id');
|
|
98
137
|
const project = await getProject(ctx.db, id);
|
|
99
138
|
if (!project)
|
|
100
139
|
return apiError(c, 404, 'NOT_FOUND', 'Project not found');
|
|
101
|
-
// 活跃 run
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
140
|
+
// 活跃 run 守卫:该项目下任一进行中(含占位 isStarting)的会话 → 拒绝。
|
|
141
|
+
// P1 修复:原实现先查 agentManager.children(projectId)——children 按
|
|
142
|
+
// parentSessionId(子 agent 归属)过滤,项目 id 恒不命中属死代码;且回退
|
|
143
|
+
// 检查只用 get() 漏掉 tryAcquire 后的 starting 占位态,窗口内删除会让 run
|
|
144
|
+
// 继续向已软删除会话写消息。现在直接按项目会话查 get()+isStarting()。
|
|
145
|
+
const rows = await ctx.db.db
|
|
146
|
+
.select({ id: sessions.id, projectId: sessions.projectId })
|
|
147
|
+
.from(sessions)
|
|
148
|
+
.where(and(eq(sessions.projectId, id)));
|
|
149
|
+
const active = rows.filter((r) => ctx.agentManager.get(r.id) !== undefined || ctx.agentManager.isStarting(r.id));
|
|
150
|
+
if (active.length > 0) {
|
|
151
|
+
return apiError(c, 409, 'PROJECT_HAS_ACTIVE_SESSIONS', `项目下有 ${active.length} 个进行中的对话,请先中止后再删除`);
|
|
105
152
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.where(and(eq(sessions.projectId, id)));
|
|
112
|
-
const active = rows.filter((r) => ctx.agentManager.get(r.id));
|
|
113
|
-
if (active.length > 0) {
|
|
114
|
-
return apiError(c, 409, 'PROJECT_HAS_ACTIVE_SESSIONS', `项目下有 ${active.length} 个进行中的对话,请先中止后再删除`);
|
|
115
|
-
}
|
|
153
|
+
// P2-5:打开的终端守卫——项目删除后其 worktree 上的 PTY 仍继续运行,
|
|
154
|
+
// cwd 指向已注销身份的目录。与活跃 run 同语义:先关闭再删除。
|
|
155
|
+
const openTerminals = ctx.ptyManager.list().filter((t) => t.projectId === id);
|
|
156
|
+
if (openTerminals.length > 0) {
|
|
157
|
+
return apiError(c, 409, 'PROJECT_HAS_OPEN_TERMINALS', `项目下有 ${openTerminals.length} 个打开的终端,请先关闭终端后再删除项目`);
|
|
116
158
|
}
|
|
117
159
|
let deletedSessions = 0;
|
|
118
160
|
await ctx.db.db.transaction(async (tx) => {
|
|
@@ -144,7 +186,19 @@ function createProjectRoute(ctx) {
|
|
|
144
186
|
.where(eq(sessions.id, s.id));
|
|
145
187
|
deletedSessions += 1;
|
|
146
188
|
}
|
|
147
|
-
|
|
189
|
+
// P2-5:看板不再永久级联销毁——标记软删除(记录原项目名),项目行删除后
|
|
190
|
+
// FK set null 使其进入回收站「未归属看板」分组,60 天后随清理任务物理清除;
|
|
191
|
+
// 恢复端点可重新归属到任意项目。
|
|
192
|
+
await tx
|
|
193
|
+
.update(kanbanBoards)
|
|
194
|
+
.set({
|
|
195
|
+
deletedAt: now,
|
|
196
|
+
deletedProjectName: project.name ?? project.worktree,
|
|
197
|
+
deletedProjectWorktree: project.worktree,
|
|
198
|
+
purgePendingAt: null,
|
|
199
|
+
updatedAt: now,
|
|
200
|
+
})
|
|
201
|
+
.where(and(eq(kanbanBoards.projectId, id), isNull(kanbanBoards.deletedAt)));
|
|
148
202
|
await tx.delete(projects).where(eq(projects.id, id));
|
|
149
203
|
});
|
|
150
204
|
return c.json({ ok: true, deletedSessions });
|
|
@@ -100,6 +100,12 @@ function createProviderRoute(ctx) {
|
|
|
100
100
|
const { baseURL, apiKey } = body;
|
|
101
101
|
if (!baseURL)
|
|
102
102
|
return apiError(c, 400, 'BAD_REQUEST', 'baseURL is required');
|
|
103
|
+
// P3:仅允许 http/https——服务端按调用方提供的 URL 发起请求,非 http 协议
|
|
104
|
+
// 不应由「测试连接」端点代理(本机 localhost/Ollama 等 http 服务不受影响)。
|
|
105
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(baseURL)?.[1]?.toLowerCase();
|
|
106
|
+
if (!scheme || (scheme !== 'http' && scheme !== 'https')) {
|
|
107
|
+
return apiError(c, 400, 'BAD_REQUEST', 'baseURL must be an http(s) URL');
|
|
108
|
+
}
|
|
103
109
|
// apiKey 可能是 Settings 页回传的 enc: 密文(保存后刷新、未重输时):探测前解密,
|
|
104
110
|
// 否则把 enc: 串当 Bearer token 发给上游必然 401,造成「保存了却测试失败」的误判。
|
|
105
111
|
const secret = apiKey ? decryptSecret(apiKey) : '';
|
|
@@ -11,13 +11,13 @@ import { archiveOriginalEntries, listArchives } from '../../session/archive.js';
|
|
|
11
11
|
import { BranchPointOutOfRangeError, forkSession, getBranches, getTree, } from '../../session/branch.js';
|
|
12
12
|
import { importSessionData } from '../../session/import.js';
|
|
13
13
|
import { deleteEntriesByIds, getMessages, getSteeringAsMessages, insertEntry, } from '../../session/message.js';
|
|
14
|
-
import { createSession, emptyTrash, getLLMSegments, getSession, listDeletedSessions, listOrphanDeletedSessions, listSessions, listSessionsByProject, permanentlyDeleteSession, rebindSession, restoreSessionCore, searchSessions, softDeleteSession, touchLastOpened, touchTrashSeen, updateSessionTitle, } from '../../session/session.js';
|
|
14
|
+
import { createSession, emptyTrash, getLLMSegments, getSession, listDeletedSessions, listOrphanDeletedSessions, listSessions, listSessionsByProject, permanentlyDeleteSession, purgeEmptySession, rebindSession, restoreSessionCore, searchSessions, softDeleteSession, touchLastOpened, touchTrashSeen, updateSessionTitle, } from '../../session/session.js';
|
|
15
15
|
import { applyShakeRegions, collectShakeRegions, DEFAULT_SHAKE_CONFIG, toRegionViews, } from '../../session/shake.js';
|
|
16
16
|
import { estimateMessageTokens } from '../../session/token.js';
|
|
17
17
|
import { generateId } from '../../shared/index.js';
|
|
18
18
|
import { apiError } from '../middleware/error.js';
|
|
19
19
|
import { buildRegistryFromConfig } from '../registry-config.js';
|
|
20
|
-
import { resolveAgentCwd } from './chat.js';
|
|
20
|
+
import { hasBusySession, resolveAgentCwd } from './chat.js';
|
|
21
21
|
function createSessionRoute(ctx) {
|
|
22
22
|
const app = new Hono();
|
|
23
23
|
// 会话导入:GET /:id/export 的逆操作(数据备份/迁移闭环)。
|
|
@@ -29,6 +29,16 @@ function createSessionRoute(ctx) {
|
|
|
29
29
|
if (body?.version !== 1 || !body?.session || !Array.isArray(body?.messages)) {
|
|
30
30
|
return apiError(c, 400, 'INVALID_EXPORT', '无效的会话导出 JSON:需要 version/session/messages 字段');
|
|
31
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
|
+
}
|
|
32
42
|
const projectId = typeof body.projectId === 'string' && body.projectId ? body.projectId : '';
|
|
33
43
|
if (!projectId) {
|
|
34
44
|
return apiError(c, 400, 'PROJECT_REQUIRED', '导入会话必须指定目标项目(projectId)');
|
|
@@ -56,6 +66,44 @@ function createSessionRoute(ctx) {
|
|
|
56
66
|
}
|
|
57
67
|
}
|
|
58
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
|
+
}
|
|
59
107
|
// P2:导出含分支树结构(parentId),导入为独立根会话——告知前端提示扁平化。
|
|
60
108
|
const flattened = typeof body.session.parentId === 'string' && body.session.parentId.length > 0;
|
|
61
109
|
const result = await importSessionData(ctx.db, {
|
|
@@ -171,16 +219,34 @@ function createSessionRoute(ctx) {
|
|
|
171
219
|
const session = await getSession(ctx.db, id);
|
|
172
220
|
if (!session)
|
|
173
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
|
+
}
|
|
174
226
|
await updateSessionTitle(ctx.db, id, title);
|
|
175
227
|
return c.json({ ok: true, title });
|
|
176
228
|
});
|
|
177
229
|
// 分支会话:未指定 messageIndex 时默认在最新一条消息处分叉(fork=完整副本语义)
|
|
178
230
|
app.post('/:id/fork', async (c) => {
|
|
179
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
|
+
}
|
|
180
240
|
// P3:fork 复制消息树,与正在写库的活跃 run 存在竞态——先拒绝。
|
|
181
|
-
|
|
241
|
+
// 守卫与斜杠 /fork 同口径(hasBusySession):此前只查 get(),
|
|
242
|
+
// 漏 tryAcquire→register 占位窗口与工作流运行期。
|
|
243
|
+
const busy = hasBusySession(ctx, id);
|
|
244
|
+
if (busy === 'run' || busy === 'workflow') {
|
|
182
245
|
return apiError(c, 409, 'RUN_ACTIVE', '该会话已有进行中的对话,请等待完成或中止后再分支');
|
|
183
246
|
}
|
|
247
|
+
if (busy === 'starting') {
|
|
248
|
+
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
249
|
+
}
|
|
184
250
|
const body = await c.req.json().catch(() => ({}));
|
|
185
251
|
let messageIndex = body.messageIndex;
|
|
186
252
|
if (messageIndex === undefined || !Number.isFinite(messageIndex)) {
|
|
@@ -202,11 +268,25 @@ function createSessionRoute(ctx) {
|
|
|
202
268
|
return apiError(c, 404, 'NOT_FOUND', 'Session not found');
|
|
203
269
|
}
|
|
204
270
|
});
|
|
205
|
-
// 删除会话(软删除:级联其 fork 后代进入回收站,
|
|
271
|
+
// 删除会话(软删除:级联其 fork 后代进入回收站,60 天后物理清除)。
|
|
206
272
|
// P2 修复:删除运行中的会话前先中止其活跃 run(含子 agent)——
|
|
207
273
|
// 否则 run 继续向已删除会话写入消息,用户以为已删除而 agent 仍在执行。
|
|
274
|
+
// P1 工作流路由:发起会话有进行中的工作流时一并中止 workflow run
|
|
275
|
+
//(此前仅中止会话自身 run + 子 agent,工作流继续后台执行且 busy 映射悬挂)。
|
|
208
276
|
app.delete('/:id', async (c) => {
|
|
209
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
|
+
}
|
|
210
290
|
ctx.agentManager.abort(id);
|
|
211
291
|
for (const child of ctx.agentManager.children(id)) {
|
|
212
292
|
ctx.agentManager.abort(child.sessionId);
|
|
@@ -229,6 +309,16 @@ function createSessionRoute(ctx) {
|
|
|
229
309
|
return apiError(c, 404, 'NOT_FOUND', '会话不存在或不在回收站');
|
|
230
310
|
return c.json({ ok: true, deleted: count });
|
|
231
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
|
+
});
|
|
232
322
|
// 会话归档列表(compaction/squash/shake/clear 的原始内容);?q= 搜索归档文本
|
|
233
323
|
app.get('/:id/archives', async (c) => {
|
|
234
324
|
const id = c.req.param('id');
|
|
@@ -257,14 +347,40 @@ function createSessionRoute(ctx) {
|
|
|
257
347
|
}
|
|
258
348
|
if (!session)
|
|
259
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
|
+
}
|
|
260
355
|
const messages = await getMessages(ctx.db, id);
|
|
261
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
|
+
};
|
|
262
376
|
return c.json({
|
|
263
377
|
version: 1,
|
|
264
378
|
exportedAt: new Date().toISOString(),
|
|
265
|
-
session,
|
|
379
|
+
session: exportSession,
|
|
266
380
|
messages,
|
|
267
|
-
archives
|
|
381
|
+
archives: includeSnapshots
|
|
382
|
+
? archives
|
|
383
|
+
: archives.map((a) => ({ ...a, fileSnapshots: undefined })),
|
|
268
384
|
});
|
|
269
385
|
});
|
|
270
386
|
// 恢复会话(从回收站还原;仅还原该会话本身)。
|
|
@@ -272,23 +388,31 @@ function createSessionRoute(ctx) {
|
|
|
272
388
|
// 项目记录并重新归属——否则恢复成功但会话不属于任何项目视图,UI 不可达。
|
|
273
389
|
// P1-2:body.projectId 提供时(当前项目视图),目录失效无法重建归属的孤儿
|
|
274
390
|
// 会话直接归属到该请求项目,恢复即可达。
|
|
391
|
+
// P1-3:静默重建项目改为明示——响应带 recreatedProject(重建的项目信息),
|
|
392
|
+
// 前端据此展示「已重新创建项目 X」;同时支持 restoreMode='current-project'
|
|
393
|
+
// 显式跳过重建、直接归属到请求项目(用户在确认框中二选一)。
|
|
275
394
|
app.post('/:id/restore', async (c) => {
|
|
276
395
|
const id = c.req.param('id');
|
|
277
396
|
const body = (await c.req.json().catch(() => ({})));
|
|
278
397
|
const requestProjectId = typeof body.projectId === 'string' && body.projectId ? body.projectId : undefined;
|
|
398
|
+
const preferCurrentProject = body.restoreMode === 'current-project';
|
|
279
399
|
const result = await restoreSessionCore(ctx.db, id);
|
|
280
400
|
if (!result.restored)
|
|
281
401
|
return apiError(c, 404, 'NOT_FOUND', '会话不存在或未删除');
|
|
282
402
|
const session = await getSession(ctx.db, id);
|
|
283
403
|
let rebound = false;
|
|
284
404
|
let orphaned = false;
|
|
405
|
+
let recreatedProject = null;
|
|
285
406
|
if (session && !session.projectId) {
|
|
286
407
|
const wt = session.worktreePath;
|
|
287
|
-
|
|
408
|
+
// P1-3:用户选择「归属到当前项目」时跳过自动重建(删除项目是用户意图,
|
|
409
|
+
// 不静默复活)。否则目录仍存在则重建项目记录(明示在响应中)。
|
|
410
|
+
if (!preferCurrentProject && wt && existsSync(wt)) {
|
|
288
411
|
try {
|
|
289
412
|
const project = await fromDirectory(ctx.db, wt);
|
|
290
413
|
await ctx.db.db.update(sessions).set({ projectId: project.id }).where(eq(sessions.id, id));
|
|
291
414
|
rebound = true;
|
|
415
|
+
recreatedProject = { id: project.id, name: project.name };
|
|
292
416
|
}
|
|
293
417
|
catch {
|
|
294
418
|
orphaned = true;
|
|
@@ -297,7 +421,8 @@ function createSessionRoute(ctx) {
|
|
|
297
421
|
else {
|
|
298
422
|
orphaned = true;
|
|
299
423
|
}
|
|
300
|
-
// P1-2:目录失效且请求方提供了项目上下文 →
|
|
424
|
+
// P1-2:目录失效且请求方提供了项目上下文 → 归属到该请求项目;
|
|
425
|
+
// P1-3:restoreMode='current-project' 时无论目录是否存在都归属请求项目。
|
|
301
426
|
if (orphaned && requestProjectId) {
|
|
302
427
|
const project = await getProject(ctx.db, requestProjectId);
|
|
303
428
|
if (project) {
|
|
@@ -311,6 +436,7 @@ function createSessionRoute(ctx) {
|
|
|
311
436
|
ok: true,
|
|
312
437
|
rebound,
|
|
313
438
|
orphaned,
|
|
439
|
+
recreatedProject,
|
|
314
440
|
restoredAncestorCount: result.restoredAncestorCount,
|
|
315
441
|
crossedBatchAncestor: result.crossedBatchAncestor,
|
|
316
442
|
// A2:批次不同的已删后代滞留在回收站,显式告知数量供前端提示单独恢复。
|
|
@@ -320,6 +446,15 @@ function createSessionRoute(ctx) {
|
|
|
320
446
|
// 会话归属变更(P1-2):孤儿会话归属到指定项目,恢复后即可达。
|
|
321
447
|
app.post('/:id/rebind', async (c) => {
|
|
322
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
|
+
}
|
|
323
458
|
const body = (await c.req.json().catch(() => ({})));
|
|
324
459
|
const projectId = typeof body.projectId === 'string' && body.projectId ? body.projectId : '';
|
|
325
460
|
if (!projectId)
|
|
@@ -353,6 +488,11 @@ function createSessionRoute(ctx) {
|
|
|
353
488
|
const run = ctx.agentManager.get(c.req.param('id'));
|
|
354
489
|
if (run)
|
|
355
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
|
+
}
|
|
356
496
|
const persisted = await getLLMSegments(ctx.db, c.req.param('id'));
|
|
357
497
|
return c.json(persisted);
|
|
358
498
|
});
|
|
@@ -360,10 +500,15 @@ function createSessionRoute(ctx) {
|
|
|
360
500
|
// P1 多项目:keepRecentTokens 按会话项目配置解析(此前用启动目录配置)。
|
|
361
501
|
app.post('/:id/compact', async (c) => {
|
|
362
502
|
const id = c.req.param('id');
|
|
363
|
-
//
|
|
364
|
-
|
|
503
|
+
// P2-5 修复:守卫收敛 hasBusySession(与斜杠 /compact、REST fork 同口径)——
|
|
504
|
+
// 此前只查 get(),漏 tryAcquire→register 占位窗口与工作流运行期两态。
|
|
505
|
+
const busy = hasBusySession(ctx, id);
|
|
506
|
+
if (busy === 'run' || busy === 'workflow') {
|
|
365
507
|
return apiError(c, 409, 'RUN_ACTIVE', '该会话已有进行中的对话,请等待完成或中止后再压缩');
|
|
366
508
|
}
|
|
509
|
+
if (busy === 'starting') {
|
|
510
|
+
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
511
|
+
}
|
|
367
512
|
let session;
|
|
368
513
|
try {
|
|
369
514
|
session = await getSession(ctx.db, id);
|
|
@@ -410,10 +555,18 @@ function createSessionRoute(ctx) {
|
|
|
410
555
|
}
|
|
411
556
|
});
|
|
412
557
|
// 获取会话状态:内存有活跃 run → 返回其 status;否则查 DB lastRun。
|
|
558
|
+
// P1 工作流路由:发起会话在 workflowBusyBySession 中时状态取实际 workflow run
|
|
559
|
+
//(此前工作流运行期间状态栏显示 idle)。
|
|
413
560
|
// lastRun.status='running' 且无活跃 run → 服务重启被中断。
|
|
414
561
|
// lastRun.status='paused' 但无活跃 run(热更新/重启后 run 状态未迁移)→ 同样按中断处理:
|
|
415
562
|
// agent 内存态已丢失,resume 端点无法恢复,唯一可行路径是重发上一条消息。
|
|
416
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
|
+
}
|
|
417
570
|
const run = ctx.agentManager.get(c.req.param('id'));
|
|
418
571
|
if (run)
|
|
419
572
|
return c.json(run.state.status);
|
|
@@ -447,10 +600,15 @@ function createSessionRoute(ctx) {
|
|
|
447
600
|
// shake apply:归档原始内容 + 原位替换
|
|
448
601
|
app.post('/:id/shake/apply', async (c) => {
|
|
449
602
|
const id = c.req.param('id');
|
|
450
|
-
//
|
|
451
|
-
|
|
603
|
+
// P2-5 修复:守卫收敛 hasBusySession(与 compact/fork 同口径),
|
|
604
|
+
// 补 starting 占位与工作流运行期两态。
|
|
605
|
+
const busy = hasBusySession(ctx, id);
|
|
606
|
+
if (busy === 'run' || busy === 'workflow') {
|
|
452
607
|
return apiError(c, 409, 'RUN_ACTIVE', '该会话已有进行中的对话,请等待完成或中止后再 Shake');
|
|
453
608
|
}
|
|
609
|
+
if (busy === 'starting') {
|
|
610
|
+
return apiError(c, 409, 'RUN_STARTING', '该会话的对话正在启动,请稍后重试');
|
|
611
|
+
}
|
|
454
612
|
let session;
|
|
455
613
|
try {
|
|
456
614
|
session = await getSession(ctx.db, id);
|
|
@@ -500,7 +658,13 @@ function createSessionRoute(ctx) {
|
|
|
500
658
|
});
|
|
501
659
|
// 记录会话打开(更新 metadata.lastOpenedAt,用于会话列表按最近打开排序)
|
|
502
660
|
app.post('/:id/open', async (c) => {
|
|
503
|
-
|
|
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);
|
|
504
668
|
return c.json({ ok: true });
|
|
505
669
|
});
|
|
506
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) => {
|