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
|
@@ -9,6 +9,9 @@ type PrintOptions = {
|
|
|
9
9
|
sessionId?: string;
|
|
10
10
|
/** 事件观察回调(CLI 用于把 tool/thinking 写到 stderr)。 */
|
|
11
11
|
onEvent?: (event: AgentEvent) => void;
|
|
12
|
+
/** P2-4:外部中止信号(ACP abort)——触发即中止 run 并抛错,
|
|
13
|
+
* 让调用方如实返回失败而非谎报 ok。 */
|
|
14
|
+
abortSignal?: AbortSignal;
|
|
12
15
|
};
|
|
13
16
|
/** 从事件流累积 assistant 文本。纯函数。 */
|
|
14
17
|
declare function collectAssistantText(events: AgentEvent[]): string;
|
package/dist/cli/modes/print.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { abortAgent, createAgent, runAgent } from '../../core/agent.js';
|
|
2
3
|
import { resolveRoute } from '../../llm/registry.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { getByDirectory } from '../../project/index.js';
|
|
5
|
+
import { createSession, getSession, upgradeTemporarySession } from '../../session/session.js';
|
|
6
|
+
import { PROJECT_BOUND_TOOLS, resolveEnabledToolNames } from '../../tools/index.js';
|
|
5
7
|
/** 从事件流累积 assistant 文本。纯函数。 */
|
|
6
8
|
function collectAssistantText(events) {
|
|
7
9
|
let text = '';
|
|
@@ -24,34 +26,102 @@ async function runPrintMode(config, message, deps, opts = {}) {
|
|
|
24
26
|
const existing = await getSession(deps.db, opts.sessionId);
|
|
25
27
|
if (!existing)
|
|
26
28
|
throw new Error(`session not found: ${opts.sessionId}`);
|
|
29
|
+
// M2:会话在回收站(Web 已删除)时拒绝续接——否则消息会写入一个 Web 树
|
|
30
|
+
// 不可见的会话,与「删除」心智模型冲突。给出可操作的恢复途径。
|
|
31
|
+
if (existing.deletedAt) {
|
|
32
|
+
throw new Error(`该会话在回收站中(已删除)。请先在 Web 界面回收站中恢复,` +
|
|
33
|
+
`或停止 serve 后运行 \`c0de sessions restore ${opts.sessionId}\` 恢复,再续接。`);
|
|
34
|
+
}
|
|
35
|
+
// P2-6 修复:cwd 与会话 worktree 不一致时拒绝——文件工具在 cwd 执行,
|
|
36
|
+
// 而 kanban/预算等绑定 session.projectId,同一会话上下文会分裂到两个目录,
|
|
37
|
+
// 且毫无提示。给出切换目录的可操作指引,避免用户在错误目录误改文件。
|
|
38
|
+
const worktree = existing.worktreePath;
|
|
39
|
+
if (worktree && resolve(worktree) !== resolve(deps.cwd)) {
|
|
40
|
+
throw new Error(`该会话的工作目录(${worktree})与当前目录(${deps.cwd})不一致。` +
|
|
41
|
+
`请先切换到会话目录再续接:\n cd "${worktree}"\n` +
|
|
42
|
+
`(会话绑定项目时,工具必须在原目录执行,否则看板与文件操作会分裂到两个目录)`);
|
|
43
|
+
}
|
|
44
|
+
// 续接即升级为持久会话:30 天临时清理不再触及(P1:此前 --continue 的
|
|
45
|
+
// 会话同样会在 30 天不活动后被物理删除,用户显式续接的历史静默丢失)。
|
|
46
|
+
// P1-3:workflow 会话同口径——Web 继续追问的 workflow 会话此前无升级路径。
|
|
47
|
+
if (existing.agentType === 'print' || existing.agentType === 'workflow') {
|
|
48
|
+
await upgradeTemporarySession(deps.db, opts.sessionId);
|
|
49
|
+
}
|
|
27
50
|
session = existing;
|
|
28
51
|
}
|
|
29
52
|
else {
|
|
30
|
-
|
|
53
|
+
// agentType='print':一次性 print 会话标记。purgeTemporarySessions 仅清理
|
|
54
|
+
// print/workflow 会话;普通 CLI 会话(ACP 等)与 --continue 续接的历史永不误删。
|
|
55
|
+
// P1-2 CLI/Web 同树:cwd 已注册为项目时直接绑定(续接后 Web 树按项目可见);
|
|
56
|
+
// worktreePath 落盘——Web 打开时工具在原目录执行,而非回退 serve cwd。
|
|
57
|
+
const existing = await getByDirectory(deps.db, deps.cwd);
|
|
58
|
+
session = await createSession(deps.db, 'cli-print', existing?.id, 'print', 'cli', undefined, deps.cwd);
|
|
31
59
|
}
|
|
32
60
|
const agentConfig = {
|
|
33
61
|
provider: config.defaultProvider,
|
|
34
62
|
model: opts.model ?? config.defaultModel,
|
|
35
|
-
// 工具集:enabled
|
|
36
|
-
|
|
63
|
+
// 工具集:enabled 含 '*' → 全部 registered;空 → 无工具(fail-closed);否则 enabled ∩ registered
|
|
64
|
+
// (disabled 已在 registry 层过滤)。无项目绑定(cwd 未注册为项目)时剔除
|
|
65
|
+
// 项目绑定工具(kanban 等)——其 store 不会被注入,调用必失败。
|
|
66
|
+
tools: resolveEnabledToolNames(deps.toolRegistry, config).filter((n) => Boolean(session.projectId) || !PROJECT_BOUND_TOOLS.has(n)),
|
|
37
67
|
plugins: config.plugins.enabled,
|
|
38
68
|
...(opts.maxTokens !== undefined ? { maxTokens: opts.maxTokens } : {}),
|
|
39
69
|
};
|
|
40
70
|
// P3:首跑友好报错——未配置 provider 时不再让底层 NoRoute 异常裸抛,
|
|
41
71
|
// 给出与 Web 端一致的引导(c0de serve → 设置 → Provider)。
|
|
72
|
+
// P1-3:区分三种失败(此前一律报「未配置」,配置了 provider 的用户被误导):
|
|
73
|
+
// - 完全没有配置 provider → 引导添加;
|
|
74
|
+
// - provider 名不匹配(如 defaultProvider 仍是默认 'openai',只配了 anthropic)
|
|
75
|
+
// → 列出已配置清单 + 修正指引(与 Web PROVIDER_NOT_FOUND 对齐);
|
|
76
|
+
// - provider 声明了模型清单但请求模型不在其中 → 列出可用模型(与 Web
|
|
77
|
+
// MODEL_NOT_FOUND 对齐)。
|
|
42
78
|
try {
|
|
43
79
|
resolveRoute(deps.llmRegistry, agentConfig.provider, agentConfig.model);
|
|
44
80
|
}
|
|
45
81
|
catch {
|
|
46
|
-
|
|
47
|
-
'
|
|
82
|
+
if (config.providers.length === 0) {
|
|
83
|
+
throw new Error('未配置可用的 AI 服务。请运行 `c0de serve` 并在「设置 → Provider」中添加 API 服务并测试连接,' +
|
|
84
|
+
'或使用 `c0de config set` 配置 providers。');
|
|
85
|
+
}
|
|
86
|
+
const configured = config.providers
|
|
87
|
+
.map((p) => p.name)
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.join(', ');
|
|
90
|
+
throw new Error(`未找到名为「${agentConfig.provider}」的 AI 服务。已配置:${configured}。` +
|
|
91
|
+
`请检查拼写,或用 \`c0de config set defaultProvider <name>\` 修正默认服务。`);
|
|
92
|
+
}
|
|
93
|
+
const providerDef = config.providers.find((p) => p.name === agentConfig.provider);
|
|
94
|
+
const modelNames = providerDef?.models ? Object.keys(providerDef.models) : undefined;
|
|
95
|
+
if (modelNames && modelNames.length > 0 && !modelNames.includes(agentConfig.model)) {
|
|
96
|
+
throw new Error(`provider「${agentConfig.provider}」没有名为「${agentConfig.model}」的模型。` +
|
|
97
|
+
`已配置模型:${modelNames.join(', ')}(可用 \`c0de chat --model <model>\` 指定)。`);
|
|
48
98
|
}
|
|
49
99
|
const state = await createAgent(session, agentConfig, deps);
|
|
100
|
+
// P2-4:ACP abort 真中止——把外部信号桥接到该 run 的 abortController。
|
|
101
|
+
// abortAgent 会使 loop 在 turn/流边界 unwind,runAgent 随即结束。
|
|
102
|
+
if (opts.abortSignal) {
|
|
103
|
+
if (opts.abortSignal.aborted) {
|
|
104
|
+
throw new Error('已中止');
|
|
105
|
+
}
|
|
106
|
+
opts.abortSignal.addEventListener('abort', () => {
|
|
107
|
+
abortAgent(state);
|
|
108
|
+
}, { once: true });
|
|
109
|
+
}
|
|
50
110
|
const events = [];
|
|
51
111
|
for await (const event of runAgent(state, [{ _tag: 'text', text: message }], deps)) {
|
|
52
112
|
events.push(event);
|
|
53
113
|
opts.onEvent?.(event);
|
|
54
114
|
}
|
|
115
|
+
// P2-4:中止后如实报错(ACP 客户端收到 error 响应,而非谎报 ok 的完成结果)。
|
|
116
|
+
if (opts.abortSignal?.aborted) {
|
|
117
|
+
throw new Error('已中止');
|
|
118
|
+
}
|
|
119
|
+
// 终态错误(unexpected,含预算中止)必须反馈给用户而非静默返回半截文本——
|
|
120
|
+
// 抛出后 dispatch 打印 message 并以非零码退出(预算超支中止等硬性护栏由此可见)。
|
|
121
|
+
const terminal = events.find((e) => e._tag === 'error' && e.error._tag === 'unexpected');
|
|
122
|
+
if (terminal && terminal._tag === 'error' && terminal.error._tag === 'unexpected') {
|
|
123
|
+
throw new Error(terminal.error.message);
|
|
124
|
+
}
|
|
55
125
|
return collectAssistantText(events);
|
|
56
126
|
}
|
|
57
127
|
export { collectAssistantText, runPrintMode };
|
package/dist/core/agent.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { MessageContent, Session } from '../shared/types/message.js';
|
|
|
3
3
|
import type { AgentDependencies } from './types.js';
|
|
4
4
|
declare function createAgent(session: Session, config: AgentConfig, deps: AgentDependencies): Promise<AgentState>;
|
|
5
5
|
declare function runAgent(state: AgentState, userInput: MessageContent[], deps: AgentDependencies): AsyncGenerator<AgentEvent>;
|
|
6
|
-
declare function pauseAgent(state: AgentState): void;
|
|
6
|
+
declare function pauseAgent(state: AgentState, reason?: string): void;
|
|
7
7
|
declare function resumeAgent(state: AgentState): void;
|
|
8
8
|
declare function abortAgent(state: AgentState): void;
|
|
9
9
|
declare function getAgentStatus(state: AgentState): AgentStatus;
|
package/dist/core/agent.js
CHANGED
|
@@ -51,13 +51,18 @@ async function createAgent(session, config, deps) {
|
|
|
51
51
|
}
|
|
52
52
|
async function* runAgent(state, userInput, deps) {
|
|
53
53
|
// 幂等重发保护:若 DB 末尾已是相同 user message(服务重启后续传场景),
|
|
54
|
-
// 跳过 append 避免重复。比较 text parts
|
|
54
|
+
// 跳过 append 避免重复。比较 text/image parts 全量内容——纯文本比较会在
|
|
55
|
+
// 「含图片消息重发」时误判为不同消息而追加重复条目(P1-2 修复)。
|
|
55
56
|
const last = state.messages[state.messages.length - 1];
|
|
56
57
|
const isDupUser = last?.role === 'user' &&
|
|
57
58
|
last.content.length === userInput.length &&
|
|
58
59
|
last.content.every((p, i) => {
|
|
59
60
|
const q = userInput[i];
|
|
60
|
-
|
|
61
|
+
if (p._tag === 'text' && q?._tag === 'text')
|
|
62
|
+
return p.text === q.text;
|
|
63
|
+
if (p._tag === 'image' && q?._tag === 'image')
|
|
64
|
+
return p.mediaType === q.mediaType && p.data === q.data;
|
|
65
|
+
return false;
|
|
61
66
|
});
|
|
62
67
|
if (!isDupUser) {
|
|
63
68
|
await appendMessage(deps.db, state.session.id, {
|
|
@@ -84,10 +89,10 @@ async function* runAgent(state, userInput, deps) {
|
|
|
84
89
|
state.status = { _tag: 'running', turnCount: 0 };
|
|
85
90
|
yield* agentLoop(state, deps);
|
|
86
91
|
}
|
|
87
|
-
function pauseAgent(state) {
|
|
92
|
+
function pauseAgent(state, reason) {
|
|
88
93
|
if (state.status._tag !== 'running')
|
|
89
94
|
return;
|
|
90
|
-
state.status = { _tag: 'paused', pauseReason: 'User requested pause' };
|
|
95
|
+
state.status = { _tag: 'paused', pauseReason: reason ?? 'User requested pause' };
|
|
91
96
|
}
|
|
92
97
|
function resumeAgent(state) {
|
|
93
98
|
if (state.status._tag !== 'paused')
|
|
@@ -19,7 +19,7 @@ const PLAN_ROLE = `You are c0de-agent in **Plan Mode**. Your job is to investiga
|
|
|
19
19
|
- Use read-only tools (grep/glob/read) to understand the structure and relevant code.
|
|
20
20
|
- Ask clarifying questions when requirements are ambiguous.
|
|
21
21
|
- When ready, present a concrete implementation plan (files to touch, approach, risks).
|
|
22
|
-
-
|
|
22
|
+
- You have NO shell access and NO write tools in this mode. If investigation requires running a command or modifying files, note it as a step for the user to execute in default mode.`;
|
|
23
23
|
/** 6 个内置 agent(2 primary + 4 subagent)。 */
|
|
24
24
|
const BUILTIN_AGENTS = [
|
|
25
25
|
{
|
|
@@ -32,7 +32,7 @@ const BUILTIN_AGENTS = [
|
|
|
32
32
|
{
|
|
33
33
|
name: 'plan',
|
|
34
34
|
description: '计划模式(只读)。专注调研与方案设计,不直接改代码。',
|
|
35
|
-
tools: ['read', 'grep', 'glob'
|
|
35
|
+
tools: ['read', 'grep', 'glob'],
|
|
36
36
|
systemPrompt: PLAN_ROLE,
|
|
37
37
|
mode: 'primary',
|
|
38
38
|
source: 'builtin',
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { BUILTIN_AGENTS, WORKER_BASE } from './builtin.js';
|
|
2
|
-
export { loadAgentFile, loadAgents } from './discovery.js';
|
|
3
2
|
export type { ParallelResult } from './parallel.js';
|
|
4
3
|
export { mapWithConcurrencyLimit } from './parallel.js';
|
|
5
4
|
export { createAgentRegistry } from './registry.js';
|
package/dist/core/compact.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { DB } from '../db/client.js';
|
|
2
2
|
import type { Registry } from '../llm/registry.js';
|
|
3
|
+
import type { FallbackChain } from '../llm/routing.js';
|
|
3
4
|
import type { CompactionConfig, CompactionResult, Summarizer } from '../session/types.js';
|
|
4
5
|
/** Create a Summarizer backed by an LLM streaming call. */
|
|
5
6
|
declare function createSummarizer(registry: Registry, provider: string, model: string, opts?: {
|
|
6
7
|
maxTokens?: number;
|
|
7
8
|
signal?: AbortSignal;
|
|
9
|
+
fallback?: FallbackChain;
|
|
8
10
|
}): Summarizer;
|
|
9
11
|
/** Run compaction on a session using the provided summarizer. */
|
|
10
12
|
declare function runCompaction(db: DB, sessionId: string, summarizer: Summarizer, config?: Partial<CompactionConfig>): Promise<CompactionResult>;
|
package/dist/core/compact.js
CHANGED
|
@@ -13,6 +13,7 @@ function createSummarizer(registry, provider, model, opts) {
|
|
|
13
13
|
for await (const chunk of chatStream({ registry, signal: opts?.signal }, request, {
|
|
14
14
|
provider,
|
|
15
15
|
model,
|
|
16
|
+
...(opts?.fallback ? { fallback: opts.fallback } : {}),
|
|
16
17
|
})) {
|
|
17
18
|
if (chunk._tag === 'text') {
|
|
18
19
|
chunks.push(chunk);
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig } from '../shared/types/config.js';
|
|
2
|
+
/**
|
|
3
|
+
* 全局配置根目录:默认 `~/.c0de`;C0DE_CONFIG_DIR 环境变量可整体重定向
|
|
4
|
+
* (相对路径按进程 cwd 解析),全局工作流/插件同样落于此根下。
|
|
5
|
+
* 与 C0DE_DB_DIR 同口径——开发隔离场景把全局配置从安装版默认位置切到独立目录。
|
|
6
|
+
*/
|
|
7
|
+
declare function resolveGlobalConfigDir(): string;
|
|
2
8
|
declare const DEFAULT_CONFIG: Config;
|
|
3
9
|
declare function mergeConfig(...configs: (Partial<Config> | undefined)[]): Config;
|
|
10
|
+
/**
|
|
11
|
+
* usage 下「全局口径」预算键——仅 global 作用域生效。项目作用域写入这些键
|
|
12
|
+
* 会被忽略(作用域收敛):此前「项目配置里设全局预算无意义但会生效」既造成
|
|
13
|
+
* 双预算语义混乱,又会随 git clone 传播、未被信任门禁拦截。收敛后项目作用域
|
|
14
|
+
* 的这两键不再进入任何合并视图,也不随 clone 影响本机护栏。
|
|
15
|
+
*/
|
|
16
|
+
declare const GLOBAL_ONLY_USAGE_KEYS: readonly ["globalMonthlyBudgetUsd", "globalMonthlyTokenBudget"];
|
|
17
|
+
/** 从项目作用域原始配置中剥离「服务端/全局」键(security 顶层键 + usage 全局口径预算键)。
|
|
18
|
+
* 浅拷贝,不改入参;无剥离键时原样返回(保持身份,调用方可安全比较)。 */
|
|
19
|
+
declare function stripProjectServerKeys(project: Partial<Config> | undefined): Partial<Config> | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* 返回项目作用域**原始文件**中出现的全局口径预算键名(strip 前的原始内容)。
|
|
22
|
+
* 供设置页告警展示:这些键来自旧版本或手动编辑,加载时已被剥离、不再生效。
|
|
23
|
+
*/
|
|
24
|
+
declare function projectGlobalOnlyUsageKeys(projectDir?: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* 返回项目作用域**原始文件**中出现的 security 子键名(strip 前的原始内容)。
|
|
27
|
+
* 供设置页告警展示:security 是服务端全局参数,随项目配置出现时已被剥离、不再生效。
|
|
28
|
+
*/
|
|
29
|
+
declare function projectSecurityKeys(projectDir?: string): string[];
|
|
30
|
+
/**
|
|
31
|
+
* 读取项目作用域原始文件内容(不剥离 security/全局键)。
|
|
32
|
+
* 供「文件原文」敏感的检查使用——被剥离的键仍可能携带明文密钥(如 security.token),
|
|
33
|
+
* 配置落在 git 仓库内同样要防误提交,不能因键不生效而漏掉警告。
|
|
34
|
+
*/
|
|
35
|
+
declare function loadProjectRawScope(projectDir?: string): Partial<Config> | undefined;
|
|
4
36
|
/**
|
|
5
37
|
* 读取 global/project 两个作用域的**原始文件内容**(不经 DEFAULT 合并)。
|
|
6
38
|
* 供配置持久化使用:写回某个作用域时只落该作用域应有的键,
|
|
7
39
|
* 避免把合并结果(含默认值与另一作用域的配置)整体序列化进文件。
|
|
40
|
+
* 项目作用域经 stripProjectServerKeys 收敛——security 与全局口径预算键只会从 global 读。
|
|
8
41
|
*/
|
|
9
42
|
declare function loadConfigScopes(projectDir?: string): {
|
|
10
43
|
global: Partial<Config> | undefined;
|
|
@@ -25,8 +58,26 @@ declare function applyScopedPatch(base: Record<string, unknown> | undefined, pat
|
|
|
25
58
|
* 用于「patch 合并进某个作用域的原始文件」后再写盘。
|
|
26
59
|
*/
|
|
27
60
|
declare function mergeRaw(...cfgs: (Record<string, unknown> | undefined)[]): Record<string, unknown>;
|
|
28
|
-
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。
|
|
61
|
+
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。
|
|
62
|
+
* - 落盘前对 providers[].apiKey 加密(已带 enc: 前缀/空值透传)——
|
|
63
|
+
* CLI config set 与 /config 斜杠命令共用此路径,保证「apiKey 不明文落盘」全链路成立。
|
|
64
|
+
* - 写入后 chmod 600:配置文件可能含 token/加密密钥,默认 0644 同机可读。 */
|
|
29
65
|
declare function saveConfigScoped(scope: 'global' | 'project', projectDir: string | undefined, data: Record<string, unknown>): Promise<void>;
|
|
30
66
|
declare function loadConfig(projectDir?: string): Promise<Config>;
|
|
67
|
+
/**
|
|
68
|
+
* P0-1:配置迁移告警(返回值,供 Web 设置页 warnings 展示;CLI 侧 console.warn
|
|
69
|
+
* 复用)。检测语义翻转/易混淆键:
|
|
70
|
+
* - tools.enabled: [] —— 旧版含义「启用全部」已改为「禁用全部」(fail-closed),
|
|
71
|
+
* 老用户升级后会静默失去全部工具,必须在 Web 界面可见(不只落在 stderr)。
|
|
72
|
+
* - slashCommands.enabled: [] —— 旧版含义「全部启用」已同步改为「禁用全部」
|
|
73
|
+
* (与 tools.enabled 语义统一),升级后斜杠命令将全部失效,须显式引导。
|
|
74
|
+
*/
|
|
75
|
+
export declare function collectConfigMigrationWarnings(scope: 'global' | 'project', data: Record<string, unknown> | undefined): string[];
|
|
76
|
+
/** Config 全部顶层键(DEFAULT_CONFIG + 可选键)。未知顶层键校验与告警共用。 */
|
|
77
|
+
declare const KNOWN_CONFIG_KEYS: Set<string>;
|
|
78
|
+
/** 收集对象中不属于 Config 顶层键的未知键。 */
|
|
79
|
+
declare function collectUnknownConfigKeys(data: Record<string, unknown> | undefined): string[];
|
|
80
|
+
/** 加载配置时对未知顶层键告警(拼写错误/旧版键静默失效的唯一提示)。 */
|
|
81
|
+
declare function warnUnknownConfigKeys(scope: 'global' | 'project', data: Record<string, unknown> | undefined): void;
|
|
31
82
|
export type { AgentsConfig, CompactionConfig, Config, MCPServerConfig, PermissionConfig, SecurityConfig, ToolMetricsConfig, UpdateConfig, WebSearchConfig, };
|
|
32
|
-
export { applyScopedPatch, DEFAULT_CONFIG, loadConfig, loadConfigScopes, mergeConfig, mergeRaw, saveConfigScoped, };
|
|
83
|
+
export { applyScopedPatch, collectUnknownConfigKeys, DEFAULT_CONFIG, GLOBAL_ONLY_USAGE_KEYS, KNOWN_CONFIG_KEYS, loadConfig, loadConfigScopes, loadProjectRawScope, mergeConfig, mergeRaw, projectGlobalOnlyUsageKeys, projectSecurityKeys, resolveGlobalConfigDir, saveConfigScoped, stripProjectServerKeys, warnUnknownConfigKeys, };
|
package/dist/core/config.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { encryptSecret, isEncryptedSecret } from './secret.js';
|
|
4
5
|
const GLOBAL_CONFIG_DIR = '.c0de';
|
|
5
6
|
const CONFIG_FILENAME = 'config.json';
|
|
7
|
+
/**
|
|
8
|
+
* 全局配置根目录:默认 `~/.c0de`;C0DE_CONFIG_DIR 环境变量可整体重定向
|
|
9
|
+
* (相对路径按进程 cwd 解析),全局工作流/插件同样落于此根下。
|
|
10
|
+
* 与 C0DE_DB_DIR 同口径——开发隔离场景把全局配置从安装版默认位置切到独立目录。
|
|
11
|
+
*/
|
|
12
|
+
function resolveGlobalConfigDir() {
|
|
13
|
+
const envDir = process.env.C0DE_CONFIG_DIR?.trim();
|
|
14
|
+
if (envDir)
|
|
15
|
+
return isAbsolute(envDir) ? envDir : resolve(envDir);
|
|
16
|
+
return join(homedir(), GLOBAL_CONFIG_DIR);
|
|
17
|
+
}
|
|
6
18
|
const DEFAULT_CONFIG = {
|
|
7
19
|
providers: [],
|
|
8
20
|
defaultProvider: 'openai',
|
|
@@ -16,20 +28,29 @@ const DEFAULT_CONFIG = {
|
|
|
16
28
|
keepRecentTokens: 4000,
|
|
17
29
|
midTurnEnabled: false,
|
|
18
30
|
},
|
|
19
|
-
// enabled
|
|
20
|
-
//
|
|
21
|
-
|
|
31
|
+
// enabled 语义(P1-1 修复「空=全部」安全陷阱):
|
|
32
|
+
// ['*'](通配)= 启用全部注册工具(默认);[] = 无工具(fail-closed);
|
|
33
|
+
// 非空名单 = 默认工具集。CLI print 与 Web chat 统一经 resolveEnabledToolNames 解析。
|
|
34
|
+
tools: { enabled: ['*'], disabled: [] },
|
|
22
35
|
plugins: { enabled: [] },
|
|
23
36
|
mcpServers: [],
|
|
24
|
-
slashCommands: {
|
|
37
|
+
slashCommands: {
|
|
38
|
+
enabled: ['/compact', '/model', '/clear', '/help', '/fork', '/config', '/workflow'],
|
|
39
|
+
},
|
|
25
40
|
toolMetrics: { enabled: true, threshold: 0.8, minSamples: 5 },
|
|
26
41
|
security: { authEnabled: true, allowedOrigins: [] },
|
|
27
42
|
websearch: { provider: 'auto' },
|
|
28
|
-
agents: {
|
|
29
|
-
permission: { defaultMode: 'default' },
|
|
43
|
+
agents: { subagentConcurrency: 3 },
|
|
44
|
+
permission: { defaultMode: 'default', timeoutAction: 'pause' },
|
|
45
|
+
usage: {
|
|
46
|
+
monthlyBudgetUsd: 0,
|
|
47
|
+
globalMonthlyBudgetUsd: 0,
|
|
48
|
+
monthlyTokenBudget: 0,
|
|
49
|
+
globalMonthlyTokenBudget: 0,
|
|
50
|
+
budgetAction: 'warn',
|
|
51
|
+
},
|
|
30
52
|
update: { enabled: true, intervalMs: 60 * 60 * 1000, initialDelayMs: 10_000 },
|
|
31
53
|
theme: 'system',
|
|
32
|
-
locale: 'en',
|
|
33
54
|
};
|
|
34
55
|
function mergeConfig(...configs) {
|
|
35
56
|
const result = structuredClone(DEFAULT_CONFIG);
|
|
@@ -68,15 +89,95 @@ function readJsonIfExists(path) {
|
|
|
68
89
|
return undefined;
|
|
69
90
|
}
|
|
70
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* usage 下「全局口径」预算键——仅 global 作用域生效。项目作用域写入这些键
|
|
94
|
+
* 会被忽略(作用域收敛):此前「项目配置里设全局预算无意义但会生效」既造成
|
|
95
|
+
* 双预算语义混乱,又会随 git clone 传播、未被信任门禁拦截。收敛后项目作用域
|
|
96
|
+
* 的这两键不再进入任何合并视图,也不随 clone 影响本机护栏。
|
|
97
|
+
*/
|
|
98
|
+
const GLOBAL_ONLY_USAGE_KEYS = ['globalMonthlyBudgetUsd', 'globalMonthlyTokenBudget'];
|
|
99
|
+
/**
|
|
100
|
+
* 项目作用域中「服务端/全局生效、随 git clone 传播有安全风险」的顶层键:整体剥离。
|
|
101
|
+
* `security` 是服务级信任边界(鉴权开关 / token / CORS origin / 首设备窗口)——单进程
|
|
102
|
+
* 多项目架构下不存在「按项目」的安全参数。此前项目配置可静默改写服务端安全参数
|
|
103
|
+
* (如 authEnabled:false、security.token 静态弱 token、allowedOrigins 放宽),且这些键
|
|
104
|
+
* 不在信任门禁的风险 kind(trust.ts summarizeProjectRisk)内,克隆仓库即可无确认生效。
|
|
105
|
+
* 收敛为 global-only:security 仅从全局作用域读取。
|
|
106
|
+
*/
|
|
107
|
+
const PROJECT_SERVER_ONLY_KEYS = ['security'];
|
|
108
|
+
/** 从项目作用域原始配置中剥离「服务端/全局」键(security 顶层键 + usage 全局口径预算键)。
|
|
109
|
+
* 浅拷贝,不改入参;无剥离键时原样返回(保持身份,调用方可安全比较)。 */
|
|
110
|
+
function stripProjectServerKeys(project) {
|
|
111
|
+
if (!project)
|
|
112
|
+
return project;
|
|
113
|
+
let stripped = false;
|
|
114
|
+
const next = { ...project };
|
|
115
|
+
// 顶层「服务端/全局」键整体移除(见 PROJECT_SERVER_ONLY_KEYS)。
|
|
116
|
+
for (const k of PROJECT_SERVER_ONLY_KEYS) {
|
|
117
|
+
if (k in next) {
|
|
118
|
+
delete next[k];
|
|
119
|
+
stripped = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// usage 下全局口径预算键移除(仅 global 作用域生效)。
|
|
123
|
+
const usage = project.usage;
|
|
124
|
+
if (typeof usage === 'object' && usage !== null && !Array.isArray(usage)) {
|
|
125
|
+
const u = { ...usage };
|
|
126
|
+
for (const k of GLOBAL_ONLY_USAGE_KEYS) {
|
|
127
|
+
if (k in u) {
|
|
128
|
+
delete u[k];
|
|
129
|
+
stripped = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
next.usage = u;
|
|
133
|
+
}
|
|
134
|
+
if (!stripped)
|
|
135
|
+
return project;
|
|
136
|
+
return next;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 返回项目作用域**原始文件**中出现的全局口径预算键名(strip 前的原始内容)。
|
|
140
|
+
* 供设置页告警展示:这些键来自旧版本或手动编辑,加载时已被剥离、不再生效。
|
|
141
|
+
*/
|
|
142
|
+
function projectGlobalOnlyUsageKeys(projectDir) {
|
|
143
|
+
const raw = readJsonIfExists(join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME));
|
|
144
|
+
const usage = raw?.usage;
|
|
145
|
+
if (typeof usage !== 'object' || usage === null || Array.isArray(usage))
|
|
146
|
+
return [];
|
|
147
|
+
return GLOBAL_ONLY_USAGE_KEYS.filter((k) => k in usage);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 返回项目作用域**原始文件**中出现的 security 子键名(strip 前的原始内容)。
|
|
151
|
+
* 供设置页告警展示:security 是服务端全局参数,随项目配置出现时已被剥离、不再生效。
|
|
152
|
+
*/
|
|
153
|
+
function projectSecurityKeys(projectDir) {
|
|
154
|
+
const raw = readJsonIfExists(join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME));
|
|
155
|
+
const sec = raw?.security;
|
|
156
|
+
if (typeof sec !== 'object' || sec === null || Array.isArray(sec))
|
|
157
|
+
return [];
|
|
158
|
+
return Object.keys(sec);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* 读取项目作用域原始文件内容(不剥离 security/全局键)。
|
|
162
|
+
* 供「文件原文」敏感的检查使用——被剥离的键仍可能携带明文密钥(如 security.token),
|
|
163
|
+
* 配置落在 git 仓库内同样要防误提交,不能因键不生效而漏掉警告。
|
|
164
|
+
*/
|
|
165
|
+
function loadProjectRawScope(projectDir) {
|
|
166
|
+
return readJsonIfExists(join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME));
|
|
167
|
+
}
|
|
71
168
|
/**
|
|
72
169
|
* 读取 global/project 两个作用域的**原始文件内容**(不经 DEFAULT 合并)。
|
|
73
170
|
* 供配置持久化使用:写回某个作用域时只落该作用域应有的键,
|
|
74
171
|
* 避免把合并结果(含默认值与另一作用域的配置)整体序列化进文件。
|
|
172
|
+
* 项目作用域经 stripProjectServerKeys 收敛——security 与全局口径预算键只会从 global 读。
|
|
75
173
|
*/
|
|
76
174
|
function loadConfigScopes(projectDir) {
|
|
77
|
-
const globalPath = join(
|
|
175
|
+
const globalPath = join(resolveGlobalConfigDir(), CONFIG_FILENAME);
|
|
78
176
|
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
79
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
global: readJsonIfExists(globalPath),
|
|
179
|
+
project: stripProjectServerKeys(readJsonIfExists(projectPath)),
|
|
180
|
+
};
|
|
80
181
|
}
|
|
81
182
|
/**
|
|
82
183
|
* 把 patch 应用到某个作用域的原始配置(scoped patch,null=删除):
|
|
@@ -138,21 +239,136 @@ function mergeRaw(...cfgs) {
|
|
|
138
239
|
}
|
|
139
240
|
return result;
|
|
140
241
|
}
|
|
141
|
-
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。
|
|
242
|
+
/** 把 raw JSON 写回指定作用域配置文件(不含默认值)。
|
|
243
|
+
* - 落盘前对 providers[].apiKey 加密(已带 enc: 前缀/空值透传)——
|
|
244
|
+
* CLI config set 与 /config 斜杠命令共用此路径,保证「apiKey 不明文落盘」全链路成立。
|
|
245
|
+
* - 写入后 chmod 600:配置文件可能含 token/加密密钥,默认 0644 同机可读。 */
|
|
142
246
|
async function saveConfigScoped(scope, projectDir, data) {
|
|
143
|
-
const dir = scope === 'global'
|
|
144
|
-
? join(homedir(), GLOBAL_CONFIG_DIR)
|
|
145
|
-
: join(projectDir ?? process.cwd(), '.c0de');
|
|
247
|
+
const dir = scope === 'global' ? resolveGlobalConfigDir() : join(projectDir ?? process.cwd(), '.c0de');
|
|
146
248
|
if (!existsSync(dir))
|
|
147
249
|
mkdirSync(dir, { recursive: true });
|
|
148
250
|
const path = join(dir, CONFIG_FILENAME);
|
|
149
|
-
|
|
251
|
+
const hardened = redactSensitiveOnSave(data);
|
|
252
|
+
writeFileSync(path, JSON.stringify(hardened, null, 2), 'utf-8');
|
|
253
|
+
try {
|
|
254
|
+
chmodSync(path, 0o600);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// 平台不支持(Windows 等)时忽略;内容仍已加密。
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** 落盘前的敏感值处理:providers[].apiKey 与 websearch 后端 key 明文 → enc: 加密
|
|
261
|
+
* (spec §24.2;此前 websearch.tavilyApiKey/braveApiKey 明文落盘,与 provider
|
|
262
|
+
* apiKey 安全叙事不一致——git 误提交警告会命中它们,加密却不会)。 */
|
|
263
|
+
function redactSensitiveOnSave(data) {
|
|
264
|
+
const out = {};
|
|
265
|
+
for (const [key, val] of Object.entries(data)) {
|
|
266
|
+
if (key === 'providers' && Array.isArray(val)) {
|
|
267
|
+
out[key] = val.map((p) => {
|
|
268
|
+
if (p === null || typeof p !== 'object')
|
|
269
|
+
return p;
|
|
270
|
+
const apiKey = p.apiKey;
|
|
271
|
+
if (typeof apiKey === 'string' && apiKey.length > 0 && !isEncryptedSecret(apiKey)) {
|
|
272
|
+
return { ...p, apiKey: encryptSecret(apiKey) };
|
|
273
|
+
}
|
|
274
|
+
return p;
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (key === 'websearch' && val !== null && typeof val === 'object') {
|
|
279
|
+
const ws = { ...val };
|
|
280
|
+
for (const k of ['tavilyApiKey', 'braveApiKey']) {
|
|
281
|
+
const v = ws[k];
|
|
282
|
+
if (typeof v === 'string' && v.length > 0 && !isEncryptedSecret(v)) {
|
|
283
|
+
ws[k] = encryptSecret(v);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
out[key] = ws;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
out[key] = val;
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
150
292
|
}
|
|
151
293
|
async function loadConfig(projectDir) {
|
|
152
|
-
const globalPath = join(
|
|
294
|
+
const globalPath = join(resolveGlobalConfigDir(), CONFIG_FILENAME);
|
|
153
295
|
const projectPath = join(projectDir ?? process.cwd(), '.c0de', CONFIG_FILENAME);
|
|
154
296
|
const global = readJsonIfExists(globalPath);
|
|
155
|
-
|
|
297
|
+
// 与 loadConfigScopes 同口径剥离:security 与 usage 全局口径预算键不进合并视图
|
|
298
|
+
// (此前 loadConfig 不剥离,服务启动目录项目的 security/全局预算键会静默生效——
|
|
299
|
+
// 安全键可被克隆仓库无确认改写,全局预算可被项目作用域注入)。
|
|
300
|
+
const projectRaw = readJsonIfExists(projectPath);
|
|
301
|
+
const project = stripProjectServerKeys(projectRaw);
|
|
302
|
+
// 项目配置遗留 security 键(旧版本/手动编辑/克隆仓库自带)→ CLI 侧明示已忽略,
|
|
303
|
+
// 与 Web 设置页 securityWarnings 同口径(stderr 可见,不必等到打开设置页)。
|
|
304
|
+
const rawSecurity = projectRaw?.security;
|
|
305
|
+
if (typeof rawSecurity === 'object' && rawSecurity !== null && !Array.isArray(rawSecurity)) {
|
|
306
|
+
const keys = Object.keys(rawSecurity);
|
|
307
|
+
if (keys.length > 0) {
|
|
308
|
+
console.warn(`[config] 项目配置含 security 键(${keys.join('、')},服务端全局参数),已忽略——` +
|
|
309
|
+
`请在全局配置(${join(resolveGlobalConfigDir(), CONFIG_FILENAME)})或 c0de config set --global 设置。`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
warnUnknownConfigKeys('global', global);
|
|
313
|
+
warnUnknownConfigKeys('project', project);
|
|
314
|
+
for (const [scope, data] of [
|
|
315
|
+
['global', global],
|
|
316
|
+
['project', project],
|
|
317
|
+
]) {
|
|
318
|
+
for (const w of collectConfigMigrationWarnings(scope, data))
|
|
319
|
+
console.warn(`[config] ${w}`);
|
|
320
|
+
}
|
|
156
321
|
return mergeConfig(global, project);
|
|
157
322
|
}
|
|
158
|
-
|
|
323
|
+
/**
|
|
324
|
+
* P0-1:配置迁移告警(返回值,供 Web 设置页 warnings 展示;CLI 侧 console.warn
|
|
325
|
+
* 复用)。检测语义翻转/易混淆键:
|
|
326
|
+
* - tools.enabled: [] —— 旧版含义「启用全部」已改为「禁用全部」(fail-closed),
|
|
327
|
+
* 老用户升级后会静默失去全部工具,必须在 Web 界面可见(不只落在 stderr)。
|
|
328
|
+
* - slashCommands.enabled: [] —— 旧版含义「全部启用」已同步改为「禁用全部」
|
|
329
|
+
* (与 tools.enabled 语义统一),升级后斜杠命令将全部失效,须显式引导。
|
|
330
|
+
*/
|
|
331
|
+
export function collectConfigMigrationWarnings(scope, data) {
|
|
332
|
+
if (!data)
|
|
333
|
+
return [];
|
|
334
|
+
const scopeLabel = scope === 'global' ? '全局' : '项目';
|
|
335
|
+
const out = [];
|
|
336
|
+
const tools = data.tools;
|
|
337
|
+
if (typeof tools === 'object' && tools !== null && !Array.isArray(tools)) {
|
|
338
|
+
const enabled = tools.enabled;
|
|
339
|
+
if (Array.isArray(enabled) && enabled.length === 0) {
|
|
340
|
+
out.push(`${scopeLabel}配置的 tools.enabled 为空数组:旧版含义「启用全部」已改为「禁用全部」——` +
|
|
341
|
+
`如需启用全部,请改为 ["*"],或删除该键恢复默认值。`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const slash = data.slashCommands;
|
|
345
|
+
if (typeof slash === 'object' && slash !== null && !Array.isArray(slash)) {
|
|
346
|
+
const enabled = slash.enabled;
|
|
347
|
+
if (Array.isArray(enabled) && enabled.length === 0) {
|
|
348
|
+
out.push(`${scopeLabel}配置的 slashCommands.enabled 为空数组:旧版含义「全部启用」已改为「禁用全部」` +
|
|
349
|
+
`(与 tools.enabled 语义统一)——如需启用全部,请改为 ["*"],或删除该键恢复默认值。`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
/** Config 全部顶层键(DEFAULT_CONFIG + 可选键)。未知顶层键校验与告警共用。 */
|
|
355
|
+
const KNOWN_CONFIG_KEYS = new Set([
|
|
356
|
+
...Object.keys(DEFAULT_CONFIG),
|
|
357
|
+
// 可选键不在 DEFAULT_CONFIG 中(缺失=未配置),但属于合法键。
|
|
358
|
+
'commitModel',
|
|
359
|
+
]);
|
|
360
|
+
/** 收集对象中不属于 Config 顶层键的未知键。 */
|
|
361
|
+
function collectUnknownConfigKeys(data) {
|
|
362
|
+
if (!data)
|
|
363
|
+
return [];
|
|
364
|
+
return Object.keys(data).filter((k) => !KNOWN_CONFIG_KEYS.has(k));
|
|
365
|
+
}
|
|
366
|
+
/** 加载配置时对未知顶层键告警(拼写错误/旧版键静默失效的唯一提示)。 */
|
|
367
|
+
function warnUnknownConfigKeys(scope, data) {
|
|
368
|
+
const unknown = collectUnknownConfigKeys(data);
|
|
369
|
+
if (unknown.length === 0)
|
|
370
|
+
return;
|
|
371
|
+
console.warn(`[config] ${scope} 配置包含未知键:${unknown.join(', ')}。` +
|
|
372
|
+
`这些键不会生效——请检查拼写,或移除过时配置。有效顶层键:${[...KNOWN_CONFIG_KEYS].join(', ')}`);
|
|
373
|
+
}
|
|
374
|
+
export { applyScopedPatch, collectUnknownConfigKeys, DEFAULT_CONFIG, GLOBAL_ONLY_USAGE_KEYS, KNOWN_CONFIG_KEYS, loadConfig, loadConfigScopes, loadProjectRawScope, mergeConfig, mergeRaw, projectGlobalOnlyUsageKeys, projectSecurityKeys, resolveGlobalConfigDir, saveConfigScoped, stripProjectServerKeys, warnUnknownConfigKeys, };
|
|
@@ -27,5 +27,6 @@ export declare function recoverFromOverflow(state: AgentState, deps: LoopDeps):
|
|
|
27
27
|
* 死锁在下一轮用户输入(agentLoop 重入)时重置。
|
|
28
28
|
* - mid-turn:midTurnEnabled 单独 opt-in(默认关闭)时按 enabled:true 阈值静默压缩,
|
|
29
29
|
* 复用 shouldCompact 逻辑但不发 error/warning,压缩后刷新内存消息视图。
|
|
30
|
-
* 两者都静默消费 compactContext 的 text_delta
|
|
30
|
+
* 两者都静默消费 compactContext 的 text_delta 通知,但把 compaction_done 透传给前端,
|
|
31
|
+
* 让用户在上下文被改写时得到提示(可在归档面板查看原始内容)。 */
|
|
31
32
|
export declare function runCompactionIfNeeded(state: AgentState, deps: LoopDeps, latestMessages: Message[]): AsyncGenerator<AgentEvent>;
|