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,11 +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, resolveProject, updateProjectName, } from '../../project/index.js';
|
|
8
|
+
import { fromDirectory, getByDirectory, getProject, listProjects, relocateProject, resolveProject, trustProject, updateProjectName, } from '../../project/index.js';
|
|
9
|
+
import { clearTrashMarks } from '../../session/session.js';
|
|
10
|
+
import { generateId } from '../../shared/index.js';
|
|
7
11
|
import { apiError } from '../middleware/error.js';
|
|
8
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
|
+
}
|
|
9
22
|
/** 实时取 git 分支(分支随时变,不存 DB)。worktree 不存在时标记缺失。 */
|
|
10
23
|
function withBranch(project) {
|
|
11
24
|
const worktreeMissing = !existsSync(project.worktree);
|
|
@@ -28,8 +41,21 @@ function createProjectRoute(ctx) {
|
|
|
28
41
|
const list = await listProjects(ctx.db);
|
|
29
42
|
return c.json(list.map(withBranch));
|
|
30
43
|
});
|
|
31
|
-
// 解析服务端 cwd
|
|
44
|
+
// 解析服务端 cwd 对应的项目(未注册则自动创建)。
|
|
45
|
+
// 注册过滤(P2):serve 在 home/文件系统根目录启动时不做自动注册——home 不是项目,
|
|
46
|
+
// 自动注册只会把 home 变成污染项目列表的假项目。已显式注册(/from-directory)的
|
|
47
|
+
// 目录照常返回,用户主动添加 home 为项目的选择被尊重。
|
|
32
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
|
+
}
|
|
33
59
|
const project = await fromDirectory(ctx.db, ctx.cwd);
|
|
34
60
|
return c.json(withBranch(project), 200);
|
|
35
61
|
});
|
|
@@ -51,51 +77,128 @@ function createProjectRoute(ctx) {
|
|
|
51
77
|
return apiError(c, 404, 'NOT_FOUND', 'Project not found');
|
|
52
78
|
return c.json(withBranch(project));
|
|
53
79
|
});
|
|
54
|
-
//
|
|
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
|
+
});
|
|
88
|
+
// A1:项目重新定位——目录被移动/重命名后的恢复通道。
|
|
89
|
+
// 整体迁移会话与看板到新目录身份(替代「删项目 + 逐条恢复 + 看板丢失」)。
|
|
90
|
+
// 目标目录已注册为另一项目 → 409;有活跃 run → 拒绝(agent 工作目录悬空)。
|
|
91
|
+
app.post('/:id/relocate', async (c) => {
|
|
92
|
+
const id = c.req.param('id');
|
|
93
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
94
|
+
const directory = typeof body.directory === 'string' ? body.directory.trim() : '';
|
|
95
|
+
if (!directory)
|
|
96
|
+
return apiError(c, 400, 'DIRECTORY_REQUIRED', 'directory is required');
|
|
97
|
+
if (!existsSync(directory)) {
|
|
98
|
+
return apiError(c, 400, 'DIRECTORY_MISSING', `目录不存在或不可访问:${directory}`);
|
|
99
|
+
}
|
|
100
|
+
// 活跃 run 守卫(与 DELETE 同语义;含 starting 占位态)
|
|
101
|
+
const rows = await ctx.db.db
|
|
102
|
+
.select({ id: sessions.id })
|
|
103
|
+
.from(sessions)
|
|
104
|
+
.where(eq(sessions.projectId, id));
|
|
105
|
+
const activeCount = rows.filter((r) => ctx.agentManager.get(r.id) !== undefined || ctx.agentManager.isStarting(r.id)).length;
|
|
106
|
+
if (activeCount > 0) {
|
|
107
|
+
return apiError(c, 409, 'PROJECT_HAS_ACTIVE_SESSIONS', `项目下有 ${activeCount} 个进行中的对话,请先中止后再重新定位`);
|
|
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
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const project = await relocateProject(ctx.db, id, directory);
|
|
117
|
+
return c.json({ ok: true, project: withBranch(project) });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
if (msg === 'PROJECT_NOT_FOUND')
|
|
122
|
+
return apiError(c, 404, 'PROJECT_NOT_FOUND', '项目不存在');
|
|
123
|
+
if (msg.startsWith('TARGET_OCCUPIED')) {
|
|
124
|
+
return apiError(c, 409, 'TARGET_OCCUPIED', msg.replace('TARGET_OCCUPIED: ', ''));
|
|
125
|
+
}
|
|
126
|
+
return apiError(c, 500, 'RELOCATE_FAILED', msg);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
// P1-9 + P2-2:删除项目记录。
|
|
55
130
|
// 会话不再留孤儿(FK set null 后在任何项目视图都不可见且 cwd 回退 serve 目录):
|
|
56
|
-
// 删除前把该项目全部未删除会话软删除进回收站(
|
|
131
|
+
// 删除前把该项目全部未删除会话软删除进回收站(60 天内可恢复),
|
|
57
132
|
// 并把 worktreePath 落盘——恢复后 resolveAgentCwd 仍能解析原工作目录。
|
|
133
|
+
// P2-5:看板同步软删除进回收站(可恢复/重新归属),不再永久级联销毁。
|
|
58
134
|
// 有活跃 run 绑定该项目时拒绝删除,避免 agent 工作目录悬空。
|
|
59
135
|
app.delete('/:id', async (c) => {
|
|
60
136
|
const id = c.req.param('id');
|
|
61
137
|
const project = await getProject(ctx.db, id);
|
|
62
138
|
if (!project)
|
|
63
139
|
return apiError(c, 404, 'NOT_FOUND', 'Project not found');
|
|
64
|
-
// 活跃 run
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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} 个进行中的对话,请先中止后再删除`);
|
|
68
152
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.where(and(eq(sessions.projectId, id)));
|
|
75
|
-
const active = rows.filter((r) => ctx.agentManager.get(r.id));
|
|
76
|
-
if (active.length > 0) {
|
|
77
|
-
return apiError(c, 409, 'PROJECT_HAS_ACTIVE_SESSIONS', `项目下有 ${active.length} 个进行中的对话,请先中止后再删除`);
|
|
78
|
-
}
|
|
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} 个打开的终端,请先关闭终端后再删除项目`);
|
|
79
158
|
}
|
|
80
159
|
let deletedSessions = 0;
|
|
81
160
|
await ctx.db.db.transaction(async (tx) => {
|
|
82
|
-
// 该项目下所有未删除会话:记录 worktreePath
|
|
161
|
+
// 该项目下所有未删除会话:记录 worktreePath(若尚无)并软删除进回收站。
|
|
162
|
+
// A2:删除项目是一次用户操作——全部会话共享同一 deletedBatchId,
|
|
163
|
+
// 恢复任一会话时其 fork 子树一并还原(与 softDeleteSession 语义对齐),
|
|
164
|
+
// 不再因批次断裂导致分支滞留回收站被静默清除。
|
|
83
165
|
const now = new Date();
|
|
166
|
+
const batchId = generateId();
|
|
84
167
|
const bound = await tx
|
|
85
|
-
.select({
|
|
168
|
+
.select({
|
|
169
|
+
id: sessions.id,
|
|
170
|
+
worktreePath: sessions.worktreePath,
|
|
171
|
+
metadata: sessions.metadata,
|
|
172
|
+
})
|
|
86
173
|
.from(sessions)
|
|
87
174
|
.where(and(eq(sessions.projectId, id), isNull(sessions.deletedAt)));
|
|
88
175
|
for (const s of bound) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
176
|
+
// A3:软删除清除回收站倒计时/宽限期标记(恢复后重删须重新看到才起算)。
|
|
177
|
+
const metadata = clearTrashMarks((s.metadata ?? {}));
|
|
178
|
+
await tx
|
|
179
|
+
.update(sessions)
|
|
180
|
+
.set({
|
|
181
|
+
worktreePath: s.worktreePath ?? project.worktree,
|
|
182
|
+
deletedAt: now,
|
|
183
|
+
deletedBatchId: batchId,
|
|
184
|
+
metadata,
|
|
185
|
+
})
|
|
186
|
+
.where(eq(sessions.id, s.id));
|
|
96
187
|
deletedSessions += 1;
|
|
97
188
|
}
|
|
98
|
-
|
|
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)));
|
|
99
202
|
await tx.delete(projects).where(eq(projects.id, id));
|
|
100
203
|
});
|
|
101
204
|
return c.json({ ok: true, deletedSessions });
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { loadConfigScopes, mergeConfig } from '../../core/config.js';
|
|
2
3
|
import { decryptSecret } from '../../core/secret.js';
|
|
4
|
+
import { resolveRoute } from '../../llm/registry.js';
|
|
5
|
+
import { getProject } from '../../project/project.js';
|
|
3
6
|
import { apiError } from '../middleware/error.js';
|
|
7
|
+
import { buildRegistryFromConfig } from '../registry-config.js';
|
|
4
8
|
/**
|
|
5
9
|
* 用给定 baseURL/apiKey 探测 OpenAI 兼容的 /models 端点。
|
|
6
10
|
* baseURL 应自行包含版本路径(如 https://api.openai.com/v1)。
|
|
@@ -39,15 +43,56 @@ async function probeModels(baseURL, apiKey) {
|
|
|
39
43
|
}
|
|
40
44
|
function createProviderRoute(ctx) {
|
|
41
45
|
const app = new Hono();
|
|
46
|
+
/** P1-1:解析目标项目配置与注册表(?projectId= 提供时按该项目合并配置)。
|
|
47
|
+
* 项目配置从磁盘实时读取(配置体量小);注册表优先用会话聊天路径缓存的。 */
|
|
48
|
+
const resolveForProject = async (c) => {
|
|
49
|
+
const projectId = c.req.query('projectId');
|
|
50
|
+
if (!projectId)
|
|
51
|
+
return { config: ctx.config, registry: ctx.llmRegistry };
|
|
52
|
+
const project = await getProject(ctx.db, projectId);
|
|
53
|
+
if (!project)
|
|
54
|
+
return null;
|
|
55
|
+
const scopes = loadConfigScopes(project.worktree);
|
|
56
|
+
const config = mergeConfig(scopes.global, scopes.project);
|
|
57
|
+
const registry = config.providers.length > 0 ? buildRegistryFromConfig(config) : ctx.llmRegistry;
|
|
58
|
+
return { config, registry };
|
|
59
|
+
};
|
|
42
60
|
// 列出已配置 providers(apiKey 脱敏,不含明文)
|
|
43
|
-
app.get('/', (c) => {
|
|
44
|
-
const
|
|
61
|
+
app.get('/', async (c) => {
|
|
62
|
+
const resolved = await resolveForProject(c);
|
|
63
|
+
if (!resolved)
|
|
64
|
+
return apiError(c, 404, 'PROJECT_NOT_FOUND', '项目不存在');
|
|
65
|
+
const providers = resolved.config.providers.map((p) => ({
|
|
45
66
|
name: p.name || p._tag || '',
|
|
46
67
|
protocol: p.protocol,
|
|
47
68
|
baseURL: p.baseURL ?? '',
|
|
48
69
|
hasKey: !!p.apiKey,
|
|
49
70
|
}));
|
|
50
|
-
return c.json({ providers, defaultProvider:
|
|
71
|
+
return c.json({ providers, defaultProvider: resolved.config.defaultProvider });
|
|
72
|
+
});
|
|
73
|
+
// 模型能力查询:前端据此控制多模态入口(supportsVision)等
|
|
74
|
+
app.get('/capabilities', async (c) => {
|
|
75
|
+
const provider = c.req.query('provider');
|
|
76
|
+
const model = c.req.query('model');
|
|
77
|
+
if (!provider || !model) {
|
|
78
|
+
return apiError(c, 400, 'BAD_REQUEST', 'provider and model are required');
|
|
79
|
+
}
|
|
80
|
+
const project = await resolveForProject(c);
|
|
81
|
+
if (!project)
|
|
82
|
+
return apiError(c, 404, 'PROJECT_NOT_FOUND', '项目不存在');
|
|
83
|
+
try {
|
|
84
|
+
const route = resolveRoute(project.registry, provider, model);
|
|
85
|
+
return c.json({
|
|
86
|
+
provider,
|
|
87
|
+
model,
|
|
88
|
+
supportsVision: route.capabilities.supportsVision,
|
|
89
|
+
supportsThinking: route.capabilities.supportsThinking,
|
|
90
|
+
contextWindow: route.capabilities.contextWindow,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return apiError(c, 404, 'NOT_FOUND', 'provider/model not registered');
|
|
95
|
+
}
|
|
51
96
|
});
|
|
52
97
|
// 连接测试:用请求体里的凭据探测 /models,不污染 registry
|
|
53
98
|
app.post('/test', async (c) => {
|
|
@@ -55,6 +100,12 @@ function createProviderRoute(ctx) {
|
|
|
55
100
|
const { baseURL, apiKey } = body;
|
|
56
101
|
if (!baseURL)
|
|
57
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
|
+
}
|
|
58
109
|
// apiKey 可能是 Settings 页回传的 enc: 密文(保存后刷新、未重输时):探测前解密,
|
|
59
110
|
// 否则把 enc: 串当 Bearer token 发给上游必然 401,造成「保存了却测试失败」的误判。
|
|
60
111
|
const secret = apiKey ? decryptSecret(apiKey) : '';
|