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
package/dist/project/project.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { loadConfigScopes } from '../core/config.js';
|
|
3
4
|
import { kanbanBoards, projects, sessions } from '../db/schema.js';
|
|
4
5
|
import { resolveProject } from './resolve.js';
|
|
6
|
+
import { computeProjectRiskFingerprint, projectTrustNeeded } from './trust.js';
|
|
5
7
|
function rowToProject(row) {
|
|
6
8
|
return {
|
|
7
9
|
id: row.id,
|
|
@@ -11,6 +13,12 @@ function rowToProject(row) {
|
|
|
11
13
|
gitRemote: row.gitRemote,
|
|
12
14
|
createdAt: row.createdAt instanceof Date ? row.createdAt.getTime() : new Date(row.createdAt).getTime(),
|
|
13
15
|
updatedAt: row.updatedAt instanceof Date ? row.updatedAt.getTime() : new Date(row.updatedAt).getTime(),
|
|
16
|
+
trustedAt: row.trustedAt instanceof Date
|
|
17
|
+
? row.trustedAt.getTime()
|
|
18
|
+
: row.trustedAt
|
|
19
|
+
? new Date(row.trustedAt).getTime()
|
|
20
|
+
: null,
|
|
21
|
+
riskFingerprint: row.riskFingerprint,
|
|
14
22
|
};
|
|
15
23
|
}
|
|
16
24
|
/**
|
|
@@ -85,6 +93,63 @@ export async function updateProjectName(handle, id, name) {
|
|
|
85
93
|
.returning();
|
|
86
94
|
return row ? rowToProject(row) : null;
|
|
87
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* P0-2:显式信任项目——用户在信任确认弹窗中批准项目作用域配置/插件后落盘。
|
|
98
|
+
* 信任是「克隆即信任」防线的落点;未信任项目携带风险配置时聊天入口被门禁拦截。
|
|
99
|
+
* 一次性动作:信任后不再拦截(用户可随时在设置中收回项目级配置)。
|
|
100
|
+
* P1:信任时同时落盘「风险配置指纹」(当前项目作用域的 summarizeProjectRisk
|
|
101
|
+
* 快照)——此后若仓库 git pull 新增 auto 权限/插件/MCP 等风险键,指纹漂移会
|
|
102
|
+
* 重新触发门禁复检,而非永久信任。
|
|
103
|
+
*/
|
|
104
|
+
export async function trustProject(handle, id) {
|
|
105
|
+
const existing = await getProject(handle, id);
|
|
106
|
+
if (!existing)
|
|
107
|
+
return null;
|
|
108
|
+
const scope = loadConfigScopes(existing.worktree).project;
|
|
109
|
+
// 指纹含 MCP 参数与插件文件内容(trust.ts computeProjectRiskFingerprint)——
|
|
110
|
+
// 信任批准的是「当时的完整风险面」,此后任何漂移(含插件代码变更)都会复检。
|
|
111
|
+
const riskFingerprint = computeProjectRiskFingerprint(scope, { projectDir: existing.worktree });
|
|
112
|
+
const rows = await handle.db
|
|
113
|
+
.update(projects)
|
|
114
|
+
.set({ trustedAt: new Date(), riskFingerprint })
|
|
115
|
+
.where(eq(projects.id, id))
|
|
116
|
+
.returning();
|
|
117
|
+
const row = rows[0];
|
|
118
|
+
return row ? rowToProject(row) : null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* P0 CLI 项目信任门禁:agent 执行路径(c0de chat / c0de acp)在无 Web 确认弹窗的
|
|
122
|
+
* 情况下,若目录项目未信任(或信任后未 config 漂移)且项目作用域/全局权限配置含
|
|
123
|
+
* 风险项,直接抛错引导 `c0de trust <dir>`——与 Web 聊天入口 409 TRUST_REQUIRED 用
|
|
124
|
+
* 同一套 projectTrustNeeded 判定,杜绝「Web 拦、CLI 裸奔」的不对称。
|
|
125
|
+
* 无风险项时不拦;trust/sessions 等非 agent 命令由调用方跳过此门禁。
|
|
126
|
+
* `globalRaw` 参数仅供测试注入确定性全局配置(生产走本机 ~/.c0de/config.json)。
|
|
127
|
+
*/
|
|
128
|
+
export async function enforceProjectTrust(handle, cwd, globalRaw) {
|
|
129
|
+
const scopes = loadConfigScopes(cwd);
|
|
130
|
+
const effectiveGlobal = globalRaw === undefined ? scopes.global : globalRaw;
|
|
131
|
+
let trustedAt = null;
|
|
132
|
+
let riskFingerprint = null;
|
|
133
|
+
try {
|
|
134
|
+
const project = await getByDirectory(handle, cwd);
|
|
135
|
+
trustedAt = project?.trustedAt ?? null;
|
|
136
|
+
riskFingerprint = project?.riskFingerprint ?? null;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// 信任状态查询失败 → 按未信任处理(宁可拦截,不静默放行)
|
|
140
|
+
}
|
|
141
|
+
const risks = projectTrustNeeded(scopes.project, effectiveGlobal, trustedAt, riskFingerprint, cwd);
|
|
142
|
+
if (risks.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
const globalKinds = new Set(['permission-auto', 'permission-timeout-deny']);
|
|
145
|
+
const globalOnly = risks.every((r) => globalKinds.has(r.kind));
|
|
146
|
+
const sourceNote = globalOnly
|
|
147
|
+
? '(风险来自全局配置 permission;如有意为之可执行 c0de trust 一次性放行本项目)'
|
|
148
|
+
: '';
|
|
149
|
+
throw new Error(`项目目录 ${cwd} 的配置含需要你确认的风险项。\n` +
|
|
150
|
+
` 请先审查后执行 c0de trust <目录> --yes 显式信任;或改用 c0de serve 在浏览器确认。\n` +
|
|
151
|
+
` 风险项:${risks.map((r) => r.kind).join('、')}${sourceNote}`);
|
|
152
|
+
}
|
|
88
153
|
/**
|
|
89
154
|
* 项目重新定位(A1:目录被移动/重命名后的恢复通道)。
|
|
90
155
|
*
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Config } from '../shared/types/config.js';
|
|
2
|
+
/** 单个风险项:kind 供前端图标/文案映射,detail 为人类可读说明。
|
|
3
|
+
* 'trust-drift' 为展示专用(仅 projectTrustNeeded 在漂移时返回),
|
|
4
|
+
* 不参与 summarizeProjectRisk 与指纹计算。 */
|
|
5
|
+
export type TrustRiskItem = {
|
|
6
|
+
kind: 'permission-auto' | 'permission-timeout-deny' | 'plugins-enabled' | 'mcp-enabled' | 'provider-rerouting' | 'workflows-enabled' | 'trust-drift';
|
|
7
|
+
detail: string;
|
|
8
|
+
};
|
|
9
|
+
/** 从全局配置中提取「会作用于本项目的权限风险」项(auto 放行 / 超时后继续自主执行)。
|
|
10
|
+
* 仅这两类参与门禁与可视化——全局插件/MCP 是用户本机显式安装(需手改 ~/.c0de),
|
|
11
|
+
* 不属仓库自带风险面,不纳入项目信任门禁。 */
|
|
12
|
+
export declare function globalPermissionRiskItems(globalRaw: Partial<Config> | undefined): TrustRiskItem[];
|
|
13
|
+
/**
|
|
14
|
+
* 把「全局配置中会作用于本项目的权限风险」并入项目风险列表(作为可见上下文)。
|
|
15
|
+
* 目的:用户在做信任决策时能看到完整生效的权限状态,避免「信任了项目却发现全局
|
|
16
|
+
* 早已 auto」的困惑。复用现有 kind(前端无需新增映射);同类 kind 已存在则跳过。
|
|
17
|
+
*/
|
|
18
|
+
export declare function enrichProjectRiskWithGlobal(risks: TrustRiskItem[], globalRaw: Partial<Config> | undefined): TrustRiskItem[];
|
|
19
|
+
/** 汇总项目作用域原始配置中的风险项。无风险返回空数组。
|
|
20
|
+
* 宽松形状:项目 JSON 可能字段漂移,非法值一律忽略(fail-closed 由
|
|
21
|
+
* 「未信任 + 无风险项 = 不拦截」与「有风险项必拦截」共同保证——解析不出
|
|
22
|
+
* 风险的配置也不含可信风险)。
|
|
23
|
+
* projectDir 提供时额外检测 `.c0de/workflows/*.js`(项目工作流是 dynamic
|
|
24
|
+
* import 的任意代码执行面,与 .c0de/plugins 同级,纯文件系统、无配置键)。
|
|
25
|
+
*/
|
|
26
|
+
export declare function summarizeProjectRisk(raw: Partial<Config> | undefined, projectDir?: string): TrustRiskItem[];
|
|
27
|
+
/**
|
|
28
|
+
* 计算项目作用域风险配置的指纹。
|
|
29
|
+
*
|
|
30
|
+
* P0(代码面覆盖):指纹不再只是配置键的 hash——已信任仓库 `git pull` 修改
|
|
31
|
+
* 插件代码(同名)或 MCP `args`(同名)此前不会触发复检,下次 serve 重启即
|
|
32
|
+
* 静默加载执行新代码。现在指纹由三部分组成:
|
|
33
|
+
* 1. 风险项 canonical 行(kind:detail,配置键漂移);
|
|
34
|
+
* 2. MCP 服务器 canonical 参数(name+command+args+transport+url 全量,
|
|
35
|
+
* 防「同名改 args」绕过);
|
|
36
|
+
* 3. 项目插件目录全部文件内容 hash(插件可 import 同目录其它文件,
|
|
37
|
+
* 内容面覆盖整个插件目录,防「同名改代码」绕过)。
|
|
38
|
+
* 无风险项、无 MCP、无插件 → ''(空串)。
|
|
39
|
+
* 升级后旧指纹(仅配置键)与含插件/MCP 的新口径不一致 → 一次性重新确认,
|
|
40
|
+
* 属预期行为(漂移复检)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function computeProjectRiskFingerprint(raw: Partial<Config> | undefined, opts?: {
|
|
43
|
+
projectDir?: string;
|
|
44
|
+
}): string;
|
|
45
|
+
/**
|
|
46
|
+
* 判定项目是否需要(重新)信任:返回「需要用户确认的风险项」数组,空数组 = 放行。
|
|
47
|
+
* 门禁条件:
|
|
48
|
+
* - 未信任:项目风险 或 全局权限风险(auto / timeoutAction=deny)任一存在即拦。
|
|
49
|
+
* 全局权限风险兜底「仓库未携带 .c0de 但本机已全局 auto」的裸奔场景。
|
|
50
|
+
* - 已信任:仅当项目作用域指纹漂移(git pull 新增风险键 / MCP 参数变更 /
|
|
51
|
+
* 插件或工作流代码变更)时复检——返回项目风险项 + 前置 trust-drift 说明项,
|
|
52
|
+
* 用户能看到「为什么又要确认」;全局配置是用户本机显式选择,不因漂移重新门禁。
|
|
53
|
+
* projectDir 提供时插件/工作流文件内容纳入指纹(serve 启动的插件加载门禁与
|
|
54
|
+
* 聊天入口使用同一口径)。
|
|
55
|
+
* 供 Web 聊天入口(返回 409)与 CLI agent 路径(抛错引导 c0de trust)共用,
|
|
56
|
+
* 保证两处「什么时候拦」判定完全一致,不再各自为政。
|
|
57
|
+
*/
|
|
58
|
+
export declare function projectTrustNeeded(raw: Partial<Config> | undefined, globalRaw: Partial<Config> | undefined, trustedAt: number | null | undefined, riskFingerprint: string | null | undefined, projectDir?: string): TrustRiskItem[];
|
|
59
|
+
/**
|
|
60
|
+
* 项目是否「当前可信任」(插件加载门禁用):已信任且指纹与信任时一致。
|
|
61
|
+
* 漂移(fingerprint 不匹配)或未信任 → false(fail-closed,项目插件不加载)。
|
|
62
|
+
* 供 server bootstrap 与 CLI deps 复用——插件在启动时加载,必须与聊天门禁
|
|
63
|
+
* 同口径校验,否则「先重启、后门禁」的窗口里漂移代码已执行。
|
|
64
|
+
*/
|
|
65
|
+
export declare function projectTrustCurrent(raw: Partial<Config> | undefined, trustedAt: number | null | undefined, riskFingerprint: string | null | undefined, projectDir: string): boolean;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// src/project/trust.ts
|
|
2
|
+
// P0-2 项目信任边界:评估「项目作用域原始配置」中的风险项。
|
|
3
|
+
//
|
|
4
|
+
// 背景:`git clone` 一个携带 `.c0de/config.json`(或 `.c0de/plugins` /
|
|
5
|
+
// `.c0de/workflows`)的仓库后 `c0de serve`,此前配置静默合并生效(可把权限
|
|
6
|
+
// 降级为 auto/YOLO),插件在启动时直接加载执行、工作流在列出/运行时 dynamic
|
|
7
|
+
// import 执行——信任在克隆完成时被默认授予。现在:
|
|
8
|
+
// - 聊天入口:未信任项目 + 风险配置 → 409 TRUST_REQUIRED(前端弹窗确认后
|
|
9
|
+
// POST /api/projects/:id/trust 落盘 trustedAt,一次性);
|
|
10
|
+
// - 启动入口:未信任项目的 .c0de/plugins 与 .c0de/workflows 不加载
|
|
11
|
+
// (信任后重启生效;工作流侧发现入口同样按信任门禁)。
|
|
12
|
+
//
|
|
13
|
+
// 评估「项目作用域原始配置」(loadConfigScopes(cwd).project),并额外评估
|
|
14
|
+
// 「全局权限风险」(permission.defaultMode=auto / timeoutAction=deny)。
|
|
15
|
+
// 两类入口不同:项目风险无论信任状态都参与指纹复检;全局权限风险仅当项目
|
|
16
|
+
// **未信任**时触发门禁(一次性,信任后不再复检)——兜底「仓库未携带 .c0de
|
|
17
|
+
// 但本机已全局 auto,克隆即全自动执行」的裸奔场景。全局配置的插件/MCP 是用户
|
|
18
|
+
// 本机显式安装(需手改 ~/.c0de),不属仓库自带风险面,不纳入门禁。
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
21
|
+
import { join, relative, sep } from 'node:path';
|
|
22
|
+
/** 从全局配置中提取「会作用于本项目的权限风险」项(auto 放行 / 超时后继续自主执行)。
|
|
23
|
+
* 仅这两类参与门禁与可视化——全局插件/MCP 是用户本机显式安装(需手改 ~/.c0de),
|
|
24
|
+
* 不属仓库自带风险面,不纳入项目信任门禁。 */
|
|
25
|
+
export function globalPermissionRiskItems(globalRaw) {
|
|
26
|
+
const items = [];
|
|
27
|
+
if (globalRaw?.permission?.defaultMode === 'auto') {
|
|
28
|
+
items.push({
|
|
29
|
+
kind: 'permission-auto',
|
|
30
|
+
detail: '(全局配置)权限模式 auto:本机已全局设为自动放行,本项目同样生效',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (globalRaw?.permission?.timeoutAction === 'deny') {
|
|
34
|
+
items.push({
|
|
35
|
+
kind: 'permission-timeout-deny',
|
|
36
|
+
detail: '(全局配置)权限超时动作 timeoutAction=deny:本机已全局设为超时后继续自主执行',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return items;
|
|
40
|
+
}
|
|
41
|
+
/** 按 kind 去重合并:primary 在前(其 detail 优先),secondary 仅补齐缺失 kind。 */
|
|
42
|
+
function mergeRiskItems(primary, secondary) {
|
|
43
|
+
const out = [...primary];
|
|
44
|
+
const seen = new Set(out.map((r) => r.kind));
|
|
45
|
+
for (const item of secondary) {
|
|
46
|
+
if (seen.has(item.kind))
|
|
47
|
+
continue;
|
|
48
|
+
seen.add(item.kind);
|
|
49
|
+
out.push(item);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 把「全局配置中会作用于本项目的权限风险」并入项目风险列表(作为可见上下文)。
|
|
55
|
+
* 目的:用户在做信任决策时能看到完整生效的权限状态,避免「信任了项目却发现全局
|
|
56
|
+
* 早已 auto」的困惑。复用现有 kind(前端无需新增映射);同类 kind 已存在则跳过。
|
|
57
|
+
*/
|
|
58
|
+
export function enrichProjectRiskWithGlobal(risks, globalRaw) {
|
|
59
|
+
if (!globalRaw)
|
|
60
|
+
return risks;
|
|
61
|
+
return mergeRiskItems(risks, globalPermissionRiskItems(globalRaw));
|
|
62
|
+
}
|
|
63
|
+
/** 汇总项目作用域原始配置中的风险项。无风险返回空数组。
|
|
64
|
+
* 宽松形状:项目 JSON 可能字段漂移,非法值一律忽略(fail-closed 由
|
|
65
|
+
* 「未信任 + 无风险项 = 不拦截」与「有风险项必拦截」共同保证——解析不出
|
|
66
|
+
* 风险的配置也不含可信风险)。
|
|
67
|
+
* projectDir 提供时额外检测 `.c0de/workflows/*.js`(项目工作流是 dynamic
|
|
68
|
+
* import 的任意代码执行面,与 .c0de/plugins 同级,纯文件系统、无配置键)。
|
|
69
|
+
*/
|
|
70
|
+
export function summarizeProjectRisk(raw, projectDir) {
|
|
71
|
+
const items = [];
|
|
72
|
+
// P0(代码面):项目工作流 `.c0de/workflows/*.js` 会在「列出/运行」时被
|
|
73
|
+
// dynamic import(模块顶层代码即刻执行),与 .c0de/plugins 同级的任意代码
|
|
74
|
+
// 执行面——克隆仓库自带即拦截,与插件一致。纯文件系统检测、不依赖配置键,
|
|
75
|
+
// 必须在下方 `!raw` 早退之前执行(仓库可能完全不携带 .c0de/config.json)。
|
|
76
|
+
if (projectDir) {
|
|
77
|
+
const names = workflowFileNames(projectDir);
|
|
78
|
+
if (names.length > 0) {
|
|
79
|
+
items.push({
|
|
80
|
+
kind: 'workflows-enabled',
|
|
81
|
+
detail: `项目工作流(.c0de/workflows,列出/运行时会执行其中代码):${names.join('、')}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!raw)
|
|
86
|
+
return items;
|
|
87
|
+
if (raw.permission?.defaultMode === 'auto') {
|
|
88
|
+
items.push({
|
|
89
|
+
kind: 'permission-auto',
|
|
90
|
+
detail: '权限模式 auto:bash/write/edit 等需要确认的工具将被自动放行',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// 权限确认超时后的动作降级为 'deny':安全默认是 'pause'(超时后暂停对话,
|
|
94
|
+
// 防 agent 在用户缺席时继续自主执行);'deny' 会「拒绝该工具但继续自动执行」。
|
|
95
|
+
// 携带此键的可疑仓库可在不触发 auto 权限的前提下悄然弱化超时安全网。
|
|
96
|
+
if (raw.permission?.timeoutAction === 'deny') {
|
|
97
|
+
items.push({
|
|
98
|
+
kind: 'permission-timeout-deny',
|
|
99
|
+
detail: '权限超时动作 timeoutAction=deny:确认超时后 agent 将继续自主执行(安全默认应为 pause)',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const plugins = raw.plugins?.enabled;
|
|
103
|
+
if (Array.isArray(plugins)) {
|
|
104
|
+
const names = plugins.filter((p) => typeof p === 'string').sort();
|
|
105
|
+
if (names.length > 0) {
|
|
106
|
+
items.push({ kind: 'plugins-enabled', detail: `启用项目插件:${names.join('、')}` });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// P0:MCP 服务器(stdio 类会在项目信任边界内本地执行任意命令,与插件同级的
|
|
110
|
+
// 任意代码执行面)。任何非空 mcpServers 数组都拦截——fail-closed,绝不静默
|
|
111
|
+
// 加载克隆仓库自带的 MCP 进程。
|
|
112
|
+
const mcpServers = raw.mcpServers;
|
|
113
|
+
if (Array.isArray(mcpServers) && mcpServers.length > 0) {
|
|
114
|
+
const names = mcpServers
|
|
115
|
+
.map((m) => {
|
|
116
|
+
const e = (typeof m === 'object' && m !== null ? m : {});
|
|
117
|
+
if (typeof e.name === 'string' && e.name.length > 0)
|
|
118
|
+
return e.name;
|
|
119
|
+
if (typeof e.command === 'string' && e.command.length > 0)
|
|
120
|
+
return e.command;
|
|
121
|
+
return typeof e.transport === 'string' ? e.transport : '(未命名)';
|
|
122
|
+
})
|
|
123
|
+
.sort(); // 排序保证指纹稳定:数组重排不触发无谓复检
|
|
124
|
+
items.push({
|
|
125
|
+
kind: 'mcp-enabled',
|
|
126
|
+
detail: `启用 MCP 服务器:${names.join('、')}(stdio 类会在本地执行命令)`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// P1:自定义 Provider 端点(baseURL 非空)会把「所有对话提示词(含代码上下文、
|
|
130
|
+
// 私有文件内容)」默认路由到该第三方地址,是比 auto 权限更直接的数据外泄面。
|
|
131
|
+
// 任何项目作用域配置声明了自定义 baseURL 的 provider 都拦截(fail-closed),
|
|
132
|
+
// 绝不静默把请求发往非官方端点。空 baseURL(官方默认端点)不视为风险。
|
|
133
|
+
const providers = raw.providers;
|
|
134
|
+
if (Array.isArray(providers)) {
|
|
135
|
+
const custom = providers
|
|
136
|
+
.map((p) => {
|
|
137
|
+
const e = (typeof p === 'object' && p !== null ? p : {});
|
|
138
|
+
const url = typeof e.baseURL === 'string' ? e.baseURL : typeof e.baseUrl === 'string' ? e.baseUrl : '';
|
|
139
|
+
return {
|
|
140
|
+
name: typeof e.name === 'string' && e.name.length > 0 ? e.name : '(未命名)',
|
|
141
|
+
url: url.trim(),
|
|
142
|
+
};
|
|
143
|
+
})
|
|
144
|
+
.filter((p) => p.url.length > 0);
|
|
145
|
+
if (custom.length > 0) {
|
|
146
|
+
const desc = custom
|
|
147
|
+
.map((p) => `${p.name} → ${p.url}`)
|
|
148
|
+
.sort() // 排序保证指纹稳定:provider 数组重排不触发无谓复检
|
|
149
|
+
.join('、');
|
|
150
|
+
items.push({
|
|
151
|
+
kind: 'provider-rerouting',
|
|
152
|
+
detail: `自定义 Provider 端点(baseURL):${desc}。你的所有对话提示词将发往该地址`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return items;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* 计算项目作用域风险配置的指纹。
|
|
160
|
+
*
|
|
161
|
+
* P0(代码面覆盖):指纹不再只是配置键的 hash——已信任仓库 `git pull` 修改
|
|
162
|
+
* 插件代码(同名)或 MCP `args`(同名)此前不会触发复检,下次 serve 重启即
|
|
163
|
+
* 静默加载执行新代码。现在指纹由三部分组成:
|
|
164
|
+
* 1. 风险项 canonical 行(kind:detail,配置键漂移);
|
|
165
|
+
* 2. MCP 服务器 canonical 参数(name+command+args+transport+url 全量,
|
|
166
|
+
* 防「同名改 args」绕过);
|
|
167
|
+
* 3. 项目插件目录全部文件内容 hash(插件可 import 同目录其它文件,
|
|
168
|
+
* 内容面覆盖整个插件目录,防「同名改代码」绕过)。
|
|
169
|
+
* 无风险项、无 MCP、无插件 → ''(空串)。
|
|
170
|
+
* 升级后旧指纹(仅配置键)与含插件/MCP 的新口径不一致 → 一次性重新确认,
|
|
171
|
+
* 属预期行为(漂移复检)。
|
|
172
|
+
*/
|
|
173
|
+
export function computeProjectRiskFingerprint(raw, opts) {
|
|
174
|
+
return hashLines(fingerprintLines(raw, opts?.projectDir));
|
|
175
|
+
}
|
|
176
|
+
/** 指纹的 canonical 行集合(排序前):风险项 + MCP 参数 + 插件/工作流文件内容。 */
|
|
177
|
+
function fingerprintLines(raw, projectDir) {
|
|
178
|
+
return [
|
|
179
|
+
...summarizeProjectRisk(raw, projectDir).map((r) => `${r.kind}:${r.detail}`),
|
|
180
|
+
...mcpCanonicalLines(raw),
|
|
181
|
+
...pluginDirHashes(projectDir),
|
|
182
|
+
...workflowDirHashes(projectDir),
|
|
183
|
+
].sort();
|
|
184
|
+
}
|
|
185
|
+
function hashLines(lines) {
|
|
186
|
+
if (lines.length === 0)
|
|
187
|
+
return '';
|
|
188
|
+
return createHash('sha256').update(lines.join('\n')).digest('hex');
|
|
189
|
+
}
|
|
190
|
+
/** MCP 服务器条目的 canonical 参数行(稳定键序;非法条目跳过)。 */
|
|
191
|
+
function mcpCanonicalLines(raw) {
|
|
192
|
+
const servers = raw?.mcpServers;
|
|
193
|
+
if (!Array.isArray(servers))
|
|
194
|
+
return [];
|
|
195
|
+
const lines = [];
|
|
196
|
+
for (const m of servers) {
|
|
197
|
+
if (typeof m !== 'object' || m === null)
|
|
198
|
+
continue;
|
|
199
|
+
const e = m;
|
|
200
|
+
const canonical = JSON.stringify({
|
|
201
|
+
name: typeof e.name === 'string' ? e.name : null,
|
|
202
|
+
command: typeof e.command === 'string' ? e.command : null,
|
|
203
|
+
args: Array.isArray(e.args) ? e.args : null,
|
|
204
|
+
transport: typeof e.transport === 'string' ? e.transport : null,
|
|
205
|
+
url: typeof e.url === 'string' ? e.url : null,
|
|
206
|
+
});
|
|
207
|
+
lines.push(`mcp:${canonical}`);
|
|
208
|
+
}
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
211
|
+
/** 项目插件目录内全部文件的内容 hash 行(相对路径 + sha256;按路径排序保证确定性)。
|
|
212
|
+
* 目录不存在/不可读 → 空(指纹退化为配置口径;插件加载侧另有 fail-closed 门禁)。 */
|
|
213
|
+
function pluginDirHashes(projectDir) {
|
|
214
|
+
if (!projectDir)
|
|
215
|
+
return [];
|
|
216
|
+
const dir = join(projectDir, '.c0de', 'plugins');
|
|
217
|
+
const lines = [];
|
|
218
|
+
const walk = (cur) => {
|
|
219
|
+
let entries = [];
|
|
220
|
+
try {
|
|
221
|
+
if (!existsSync(cur))
|
|
222
|
+
return;
|
|
223
|
+
entries = readdirSync(cur).sort();
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const full = join(cur, entry);
|
|
230
|
+
try {
|
|
231
|
+
if (statSync(full).isDirectory()) {
|
|
232
|
+
walk(full);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const rel = relative(dir, full).split(sep).join('/');
|
|
236
|
+
lines.push(`plugin-file:${rel}:${createHash('sha256').update(readFileSync(full)).digest('hex')}`);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
// 单个文件读取失败跳过(不因无法 hash 而放行或误报)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
walk(dir);
|
|
244
|
+
return lines;
|
|
245
|
+
}
|
|
246
|
+
/** 项目工作流目录 `.c0de/workflows` 下的 `*.js` 文件名(排序;目录缺失 → 空)。
|
|
247
|
+
* 工作流文件是 dynamic import 的任意代码执行面(模块顶层代码在「列出」时即执行),
|
|
248
|
+
* 因此只要存在即构成风险项——无需配置键,纯文件系统检测。 */
|
|
249
|
+
function workflowFileNames(projectDir) {
|
|
250
|
+
const dir = join(projectDir, '.c0de', 'workflows');
|
|
251
|
+
try {
|
|
252
|
+
if (!existsSync(dir))
|
|
253
|
+
return [];
|
|
254
|
+
return readdirSync(dir)
|
|
255
|
+
.filter((f) => f.endsWith('.js'))
|
|
256
|
+
.sort();
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/** 项目工作流目录内全部文件的内容 hash 行(与 pluginDirHashes 同口径:
|
|
263
|
+
* 相对路径 + sha256;目录不存在 → 空)。工作流文件内容漂移(同名改代码)
|
|
264
|
+
* 与插件一样触发 trust-drift 复检,杜绝「信任后 git pull 换代码」绕过。 */
|
|
265
|
+
function workflowDirHashes(projectDir) {
|
|
266
|
+
if (!projectDir)
|
|
267
|
+
return [];
|
|
268
|
+
const dir = join(projectDir, '.c0de', 'workflows');
|
|
269
|
+
const lines = [];
|
|
270
|
+
for (const entry of workflowFileNames(projectDir)) {
|
|
271
|
+
try {
|
|
272
|
+
lines.push(`workflow-file:${entry}:${createHash('sha256')
|
|
273
|
+
.update(readFileSync(join(dir, entry)))
|
|
274
|
+
.digest('hex')}`);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// 单个文件读取失败跳过(不因无法 hash 而放行或误报)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return lines;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 判定项目是否需要(重新)信任:返回「需要用户确认的风险项」数组,空数组 = 放行。
|
|
284
|
+
* 门禁条件:
|
|
285
|
+
* - 未信任:项目风险 或 全局权限风险(auto / timeoutAction=deny)任一存在即拦。
|
|
286
|
+
* 全局权限风险兜底「仓库未携带 .c0de 但本机已全局 auto」的裸奔场景。
|
|
287
|
+
* - 已信任:仅当项目作用域指纹漂移(git pull 新增风险键 / MCP 参数变更 /
|
|
288
|
+
* 插件或工作流代码变更)时复检——返回项目风险项 + 前置 trust-drift 说明项,
|
|
289
|
+
* 用户能看到「为什么又要确认」;全局配置是用户本机显式选择,不因漂移重新门禁。
|
|
290
|
+
* projectDir 提供时插件/工作流文件内容纳入指纹(serve 启动的插件加载门禁与
|
|
291
|
+
* 聊天入口使用同一口径)。
|
|
292
|
+
* 供 Web 聊天入口(返回 409)与 CLI agent 路径(抛错引导 c0de trust)共用,
|
|
293
|
+
* 保证两处「什么时候拦」判定完全一致,不再各自为政。
|
|
294
|
+
*/
|
|
295
|
+
export function projectTrustNeeded(raw, globalRaw, trustedAt, riskFingerprint, projectDir) {
|
|
296
|
+
const projectRisks = summarizeProjectRisk(raw, projectDir);
|
|
297
|
+
const untrusted = trustedAt == null;
|
|
298
|
+
if (untrusted) {
|
|
299
|
+
return mergeRiskItems(projectRisks, globalPermissionRiskItems(globalRaw));
|
|
300
|
+
}
|
|
301
|
+
// 已信任 + 无风险键且无插件/工作流文件:无漂移面,直接放行(保持旧行为——
|
|
302
|
+
// 信任后删光风险配置不触发复检)。
|
|
303
|
+
const lines = fingerprintLines(raw, projectDir);
|
|
304
|
+
if (lines.length === 0)
|
|
305
|
+
return [];
|
|
306
|
+
const currentFp = hashLines(lines);
|
|
307
|
+
if (riskFingerprint !== currentFp) {
|
|
308
|
+
return [
|
|
309
|
+
{
|
|
310
|
+
kind: 'trust-drift',
|
|
311
|
+
detail: '自上次信任以来,项目风险配置、MCP 参数、插件代码或工作流文件已变更(如仓库 git pull 更新)——需重新确认信任后才会放行',
|
|
312
|
+
},
|
|
313
|
+
...projectRisks,
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* 项目是否「当前可信任」(插件加载门禁用):已信任且指纹与信任时一致。
|
|
320
|
+
* 漂移(fingerprint 不匹配)或未信任 → false(fail-closed,项目插件不加载)。
|
|
321
|
+
* 供 server bootstrap 与 CLI deps 复用——插件在启动时加载,必须与聊天门禁
|
|
322
|
+
* 同口径校验,否则「先重启、后门禁」的窗口里漂移代码已执行。
|
|
323
|
+
*/
|
|
324
|
+
export function projectTrustCurrent(raw, trustedAt, riskFingerprint, projectDir) {
|
|
325
|
+
if (trustedAt == null)
|
|
326
|
+
return false;
|
|
327
|
+
return computeProjectRiskFingerprint(raw, { projectDir }) === riskFingerprint;
|
|
328
|
+
}
|
|
@@ -34,7 +34,7 @@ type AgentManager = {
|
|
|
34
34
|
/** 活跃 run 数(不含占位)。 */
|
|
35
35
|
size(): number;
|
|
36
36
|
abort(sessionId: string): boolean;
|
|
37
|
-
pause(sessionId: string): boolean;
|
|
37
|
+
pause(sessionId: string, reason?: string): boolean;
|
|
38
38
|
resume(sessionId: string): boolean;
|
|
39
39
|
/** 暂停全部活跃 run 并等待到达安全点;超时强制中止(热更新前调用)。
|
|
40
40
|
* 返回 pausedIds:成功暂停的会话 id 列表——调用方在后续步骤(spawn 等)
|
|
@@ -49,11 +49,15 @@ type AgentManager = {
|
|
|
49
49
|
children(parentSessionId: string): ActiveRun[];
|
|
50
50
|
/** 查询所有后台任务(jobId 非空的 run)。 */
|
|
51
51
|
backgroundJobs(): ActiveRun[];
|
|
52
|
-
/** 列出全部活跃 run(含子 agent)——热更新影响面展示用。
|
|
52
|
+
/** 列出全部活跃 run(含子 agent)——热更新影响面展示用。
|
|
53
|
+
* status/currentTool(P2-9):确认弹窗区分「正在执行工具、超时将被强杀」的
|
|
54
|
+
* run 与普通 running/paused run,用户可判断被中断的代价。 */
|
|
53
55
|
listActive(): Array<{
|
|
54
56
|
sessionId: string;
|
|
55
57
|
agentType?: string;
|
|
56
58
|
parentSessionId?: string;
|
|
59
|
+
status: 'running' | 'paused' | 'idle' | 'stopped' | 'interrupted';
|
|
60
|
+
currentTool?: string;
|
|
57
61
|
}>;
|
|
58
62
|
/** 中止所有活跃 run 并清空(dev 热重载重建前调用)。 */
|
|
59
63
|
dispose(): void;
|
|
@@ -40,11 +40,11 @@ function createAgentManager() {
|
|
|
40
40
|
abortAgent(run.state);
|
|
41
41
|
return true;
|
|
42
42
|
},
|
|
43
|
-
pause(sessionId) {
|
|
43
|
+
pause(sessionId, reason) {
|
|
44
44
|
const run = slotRun(runs.get(sessionId));
|
|
45
45
|
if (!run)
|
|
46
46
|
return false;
|
|
47
|
-
pauseAgent(run.state);
|
|
47
|
+
pauseAgent(run.state, reason);
|
|
48
48
|
return true;
|
|
49
49
|
},
|
|
50
50
|
resume(sessionId) {
|
|
@@ -110,6 +110,8 @@ function createAgentManager() {
|
|
|
110
110
|
sessionId: r.sessionId,
|
|
111
111
|
agentType: r.agentType,
|
|
112
112
|
parentSessionId: r.parentSessionId,
|
|
113
|
+
status: r.state.status._tag,
|
|
114
|
+
currentTool: r.state.status._tag === 'running' ? r.state.status.currentTool : undefined,
|
|
113
115
|
}));
|
|
114
116
|
},
|
|
115
117
|
dispose() {
|
package/dist/server/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/server/app.ts
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
5
5
|
import { Hono } from 'hono';
|
|
@@ -24,6 +24,7 @@ import { createTerminalRoute } from './routes/terminal.js';
|
|
|
24
24
|
import { createTodoRoute } from './routes/todo.js';
|
|
25
25
|
import { createToolRoute } from './routes/tool.js';
|
|
26
26
|
import { createUpdateRoute } from './routes/update.js';
|
|
27
|
+
import { createUsageRoute } from './routes/usage.js';
|
|
27
28
|
import { createWorkflowsRoute } from './routes/workflows.js';
|
|
28
29
|
/** 创建完整的 Hono 应用,挂载所有路由 + 中间件。 */
|
|
29
30
|
function createApp(ctx) {
|
|
@@ -41,6 +42,10 @@ function createApp(ctx) {
|
|
|
41
42
|
'/api/auth/pairing/request',
|
|
42
43
|
'/api/auth/pairing/status',
|
|
43
44
|
],
|
|
45
|
+
allowedOrigins: ctx.config.security.allowedOrigins,
|
|
46
|
+
// P1 媒体预览:文件 raw 端点(FilePreview 的 img/audio/video/embed src)
|
|
47
|
+
// 无法携带 Authorization 头,接受 ?token= query 认证(仅 GET raw 路径)。
|
|
48
|
+
queryTokenPath: (p) => p.startsWith('/api/files/') && p.endsWith('/raw'),
|
|
44
49
|
...(ctx.authManager ? { verify: (t) => ctx.authManager?.verify(t) ?? false } : {}),
|
|
45
50
|
}));
|
|
46
51
|
// 路由
|
|
@@ -63,36 +68,46 @@ function createApp(ctx) {
|
|
|
63
68
|
app.route('/api/files', createFilesRoute(ctx));
|
|
64
69
|
app.route('/api/terminal', createTerminalRoute(ctx));
|
|
65
70
|
app.route('/api/workflows', createWorkflowsRoute(ctx));
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
'/api/catalog',
|
|
77
|
-
'/api/filesystem',
|
|
78
|
-
'/api/chat',
|
|
79
|
-
'/api/commands',
|
|
80
|
-
'/api/tools',
|
|
81
|
-
'/api/todo',
|
|
82
|
-
'/api/kanban',
|
|
83
|
-
'/api/update',
|
|
84
|
-
'/api/config',
|
|
85
|
-
'/api/permissions',
|
|
86
|
-
'/api/files',
|
|
87
|
-
'/api/terminal',
|
|
88
|
-
'/api/workflows',
|
|
89
|
-
],
|
|
90
|
-
}));
|
|
91
|
-
// 静态文件服务(生产环境 dist-web/ 存在时启用)
|
|
92
|
-
if (existsSync(path.resolve(ctx.cwd, 'dist-web'))) {
|
|
93
|
-
app.use('/*', serveStatic({ root: './dist-web' }));
|
|
94
|
-
app.get('/*', (c) => c.body(null)); // SPA fallback
|
|
71
|
+
app.route('/api/usage', createUsageRoute(ctx));
|
|
72
|
+
// 静态文件服务(生产环境 dist-web/ 存在时启用)。
|
|
73
|
+
// 根路径语义:dist-web 存在时 '/' 与所有未匹配路由回退 index.html(SPA history
|
|
74
|
+
// 路由深链刷新可用);否则 '/' 返回 API 端点列表(无前端构建的开发/CI 场景)。
|
|
75
|
+
const distWebRoot = path.resolve(ctx.cwd, 'dist-web');
|
|
76
|
+
const indexHtmlPath = path.join(distWebRoot, 'index.html');
|
|
77
|
+
const hasWebAssets = existsSync(indexHtmlPath);
|
|
78
|
+
if (hasWebAssets) {
|
|
79
|
+
app.use('/*', serveStatic({ root: distWebRoot }));
|
|
80
|
+
app.get('/*', (c) => c.html(readFileSync(indexHtmlPath, 'utf-8')));
|
|
95
81
|
}
|
|
82
|
+
// 根路径
|
|
83
|
+
app.get('/', (c) => {
|
|
84
|
+
if (hasWebAssets)
|
|
85
|
+
return c.html(readFileSync(indexHtmlPath, 'utf-8'));
|
|
86
|
+
return c.json({
|
|
87
|
+
name: 'c0de-agent',
|
|
88
|
+
version: '0.1.0',
|
|
89
|
+
endpoints: [
|
|
90
|
+
'/api/health',
|
|
91
|
+
'/api/agents',
|
|
92
|
+
'/api/sessions',
|
|
93
|
+
'/api/projects',
|
|
94
|
+
'/api/providers',
|
|
95
|
+
'/api/catalog',
|
|
96
|
+
'/api/filesystem',
|
|
97
|
+
'/api/chat',
|
|
98
|
+
'/api/commands',
|
|
99
|
+
'/api/tools',
|
|
100
|
+
'/api/todo',
|
|
101
|
+
'/api/kanban',
|
|
102
|
+
'/api/update',
|
|
103
|
+
'/api/config',
|
|
104
|
+
'/api/permissions',
|
|
105
|
+
'/api/files',
|
|
106
|
+
'/api/terminal',
|
|
107
|
+
'/api/workflows',
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
});
|
|
96
111
|
return app;
|
|
97
112
|
}
|
|
98
113
|
export { createApp };
|