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
package/dist/core/slash.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { createSession, getSession } from '../session/session.js';
|
|
3
|
+
import { PROJECT_BOUND_TOOLS, resolveEnabledToolNames } from '../tools/index.js';
|
|
2
4
|
import { createAgent } from './agent.js';
|
|
3
|
-
import { BUILTIN_WORKFLOWS, createWorkflowRegistry, discoverWorkflows, executeWorkflow, reloadRegistry, saveWorkflow, } from './workflows/index.js';
|
|
5
|
+
import { BUILTIN_WORKFLOWS, createWorkflowRegistry, discoverWorkflows, executeWorkflow, reloadRegistry, saveWorkflow, workflowSessionTitle, } from './workflows/index.js';
|
|
4
6
|
function parseSlashInput(input) {
|
|
5
7
|
const trimmed = input.trim();
|
|
6
8
|
if (!trimmed.startsWith('/'))
|
|
@@ -11,6 +13,22 @@ function parseSlashInput(input) {
|
|
|
11
13
|
}
|
|
12
14
|
return { name: trimmed.slice(1, spaceIdx), args: trimmed.slice(spaceIdx + 1).trim() };
|
|
13
15
|
}
|
|
16
|
+
/** 校验跨会话操作的归属:目标会话必须存在;当前会话与目标均挂项目时须同项目,
|
|
17
|
+
* 防 Web 端误输/被注入 id 清空或分支其他项目的会话。CLI 无当前会话上下文
|
|
18
|
+
* (sessionId 缺省)时放行——终端单用户显式操作。 */
|
|
19
|
+
async function assertSameProjectSession(db, currentId, targetId) {
|
|
20
|
+
const target = await getSession(db, targetId);
|
|
21
|
+
if (!target)
|
|
22
|
+
throw new Error(`目标会话不存在:${targetId}`);
|
|
23
|
+
if (!currentId || currentId === targetId)
|
|
24
|
+
return;
|
|
25
|
+
const current = await getSession(db, currentId);
|
|
26
|
+
const currentProject = current?.projectId ?? null;
|
|
27
|
+
const targetProject = target.projectId ?? null;
|
|
28
|
+
if (currentProject !== null && targetProject !== null && currentProject !== targetProject) {
|
|
29
|
+
throw new Error(`会话 ${targetId} 属于其他项目,不能从当前会话操作`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
14
32
|
function createSlashRegistry() {
|
|
15
33
|
const commands = new Map();
|
|
16
34
|
for (const cmd of builtinCommands) {
|
|
@@ -23,25 +41,47 @@ function createSlashRegistry() {
|
|
|
23
41
|
register: (cmd) => commands.set(cmd.name, cmd),
|
|
24
42
|
};
|
|
25
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* P0-2:统一斜杠命令启用判定(Web chat / CLI chat / 命令列表 / /help 共用)。
|
|
46
|
+
* 语义(fail-closed,与 tools.enabled 对齐):
|
|
47
|
+
* - enabled 含 '*'(通配)→ 全部启用。
|
|
48
|
+
* - enabled 为显式名单 → 仅名单内启用(名称兼容带/不带前缀斜杠)。
|
|
49
|
+
* - enabled 为空数组/缺失 → 全部禁用(旧版含义「空=全部启用」,升级告警见
|
|
50
|
+
* collectConfigMigrationWarnings——两处同形键不再语义相反)。
|
|
51
|
+
* `enabled` 已含 '/' 前缀或裸名均可。
|
|
52
|
+
*/
|
|
53
|
+
export function isSlashCommandEnabled(enabled, name) {
|
|
54
|
+
const list = enabled ?? [];
|
|
55
|
+
if (list.length === 0)
|
|
56
|
+
return false;
|
|
57
|
+
if (list.includes('*'))
|
|
58
|
+
return true;
|
|
59
|
+
const norm = name.startsWith('/') ? name.slice(1) : name;
|
|
60
|
+
return list.some((n) => (n.startsWith('/') ? n.slice(1) : n) === norm);
|
|
61
|
+
}
|
|
26
62
|
const helpCommand = {
|
|
27
63
|
name: 'help',
|
|
28
|
-
description: '
|
|
29
|
-
execute: async () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
description: '列出可用斜杠命令',
|
|
65
|
+
execute: async (_args, ctx) => {
|
|
66
|
+
// 从 registry 动态生成,避免静态文案随命令增删漂移;
|
|
67
|
+
// 尊重 slashCommands.enabled 过滤(含 ['*'] = 全部启用;空 = 全部禁用)。
|
|
68
|
+
const enabledList = ctx.config.slashCommands?.enabled;
|
|
69
|
+
const commands = createSlashRegistry()
|
|
70
|
+
.list()
|
|
71
|
+
.filter((c) => isSlashCommandEnabled(enabledList, c.name));
|
|
72
|
+
const width = Math.max(...commands.map((c) => `/${c.name}`.length));
|
|
73
|
+
const lines = ['可用命令:'];
|
|
74
|
+
for (const cmd of commands) {
|
|
75
|
+
const hint = cmd.argsHint ? ` ${cmd.argsHint}` : '';
|
|
76
|
+
const pad = ' '.repeat(Math.max(1, width - `/${cmd.name}`.length));
|
|
77
|
+
lines.push(` /${cmd.name}${pad}${hint} ${cmd.description}`);
|
|
78
|
+
}
|
|
39
79
|
return { _tag: 'text', text: lines.join('\n') };
|
|
40
80
|
},
|
|
41
81
|
};
|
|
42
82
|
const compactCommand = {
|
|
43
83
|
name: 'compact',
|
|
44
|
-
description: '
|
|
84
|
+
description: '手动触发上下文压缩',
|
|
45
85
|
execute: async () => {
|
|
46
86
|
// 仅声明意图:真正的压缩由消费方(loop.compactContext / chat 路由)执行,
|
|
47
87
|
// 复用 createSummarizer + runCompaction,且不把 /compact 当作 user 消息发给 LLM。
|
|
@@ -50,20 +90,47 @@ const compactCommand = {
|
|
|
50
90
|
};
|
|
51
91
|
const modelCommand = {
|
|
52
92
|
name: 'model',
|
|
53
|
-
description: '
|
|
93
|
+
description: '查看当前会话使用的模型',
|
|
54
94
|
argsHint: '',
|
|
55
|
-
execute: async () => {
|
|
56
|
-
//
|
|
57
|
-
//
|
|
95
|
+
execute: async (_args, ctx) => {
|
|
96
|
+
// P2:诚实化——此前返回「Model set to X」但什么都不生效,后改为引导文案。
|
|
97
|
+
// 现在提供真实信息:优先读本会话活跃段的 provider/model(若存在),
|
|
98
|
+
// 否则回退默认配置。会话模型切换由底部 ModelSelector 完成(Web);
|
|
99
|
+
// CLI 按渠道给出可操作指引(P3:此前 CLI 输出指向 Web-only 控件,是死路)。
|
|
100
|
+
const model = async () => {
|
|
101
|
+
if (!ctx.sessionId)
|
|
102
|
+
return null;
|
|
103
|
+
try {
|
|
104
|
+
const { getLLMSegments } = await import('../session/session.js');
|
|
105
|
+
const segs = await getLLMSegments(ctx.deps.db, ctx.sessionId);
|
|
106
|
+
const last = segs[segs.length - 1];
|
|
107
|
+
if (last)
|
|
108
|
+
return `${last.model}(provider: ${last.provider})`;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// 段查询失败回退默认值展示
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
const sessionModel = await model();
|
|
116
|
+
if (ctx.channel === 'cli') {
|
|
117
|
+
return {
|
|
118
|
+
_tag: 'text',
|
|
119
|
+
text: `${sessionModel ? `当前会话模型:${sessionModel}` : `默认模型:${ctx.config.defaultModel}(provider: ${ctx.config.defaultProvider})`}\n` +
|
|
120
|
+
'CLI 模式不支持会话内切换模型:新会话用 c0de chat --model <model>,' +
|
|
121
|
+
'或 c0de config set defaultModel <model> 修改默认模型。',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
58
124
|
return {
|
|
59
125
|
_tag: 'text',
|
|
60
|
-
text:
|
|
126
|
+
text: `${sessionModel ? `当前会话模型:${sessionModel}` : `默认模型:${ctx.config.defaultModel}(provider: ${ctx.config.defaultProvider})`}\n` +
|
|
127
|
+
'切换模型请使用聊天页底部的模型选择器。',
|
|
61
128
|
};
|
|
62
129
|
},
|
|
63
130
|
};
|
|
64
131
|
const clearCommand = {
|
|
65
132
|
name: 'clear',
|
|
66
|
-
description: '
|
|
133
|
+
description: '清空会话消息(先归档原始内容)',
|
|
67
134
|
argsHint: '[session-id] [--yes]',
|
|
68
135
|
execute: async (args, ctx) => {
|
|
69
136
|
const parts = args.split(/\s+/).filter(Boolean);
|
|
@@ -72,10 +139,16 @@ const clearCommand = {
|
|
|
72
139
|
if (!sessionId) {
|
|
73
140
|
return { _tag: 'error', message: 'Usage: /clear [session-id] --yes(当前会话可省略 id)' };
|
|
74
141
|
}
|
|
142
|
+
try {
|
|
143
|
+
await assertSameProjectSession(ctx.deps.db, ctx.sessionId, sessionId);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return { _tag: 'error', message: error instanceof Error ? error.message : String(error) };
|
|
147
|
+
}
|
|
75
148
|
if (!yes) {
|
|
76
149
|
return {
|
|
77
150
|
_tag: 'error',
|
|
78
|
-
message: '
|
|
151
|
+
message: '清空消息不可逆。原始消息将归档到「会话归档」面板(会话页顶部「归档」按钮可查看/搜索),确认请执行 /clear --yes(或 /clear <session-id> --yes)。',
|
|
79
152
|
};
|
|
80
153
|
}
|
|
81
154
|
const { getEntries, deleteEntriesByIds } = await import('../session/message.js');
|
|
@@ -88,23 +161,26 @@ const clearCommand = {
|
|
|
88
161
|
await archiveOriginalEntries(ctx.deps.db, sessionId, entries, 'clear', `Cleared ${ids.length} entries`, generateId());
|
|
89
162
|
await deleteEntriesByIds(ctx.deps.db, ids);
|
|
90
163
|
}
|
|
91
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
_tag: 'success',
|
|
166
|
+
message: `已清空 ${ids.length} 条消息(原内容已归档,可在会话页「归档」面板查看;历史 token/成本仍计入本会话用量统计)`,
|
|
167
|
+
};
|
|
92
168
|
},
|
|
93
169
|
};
|
|
94
170
|
const forkCommand = {
|
|
95
171
|
name: 'fork',
|
|
96
|
-
description: '
|
|
172
|
+
description: '从指定消息处分支会话',
|
|
97
173
|
argsHint: '[session-id] [message-index]',
|
|
98
174
|
execute: async (args, ctx) => {
|
|
99
175
|
const parts = args.split(/\s+/).filter(Boolean);
|
|
100
|
-
//
|
|
176
|
+
// 默认当前会话;第一个非纯数字参数视为 session id 覆盖(fork 其他会话)。
|
|
101
177
|
let sessionId = ctx.sessionId;
|
|
102
178
|
let messageIndex;
|
|
103
179
|
for (const p of parts) {
|
|
104
180
|
if (/^\d+$/.test(p)) {
|
|
105
181
|
messageIndex = Number.parseInt(p, 10);
|
|
106
182
|
}
|
|
107
|
-
else
|
|
183
|
+
else {
|
|
108
184
|
sessionId = p;
|
|
109
185
|
}
|
|
110
186
|
}
|
|
@@ -114,6 +190,12 @@ const forkCommand = {
|
|
|
114
190
|
message: 'Usage: /fork [session-id] [message-index](当前会话可省略 id)',
|
|
115
191
|
};
|
|
116
192
|
}
|
|
193
|
+
try {
|
|
194
|
+
await assertSameProjectSession(ctx.deps.db, ctx.sessionId, sessionId);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
return { _tag: 'error', message: error instanceof Error ? error.message : String(error) };
|
|
198
|
+
}
|
|
117
199
|
// 未指定 index → 默认最新一条消息处分支(与 web fork API 语义一致,不再是 index 0)
|
|
118
200
|
if (messageIndex === undefined) {
|
|
119
201
|
const { getMessages } = await import('../session/message.js');
|
|
@@ -126,7 +208,7 @@ const forkCommand = {
|
|
|
126
208
|
const { forkSession } = await import('../session/branch.js');
|
|
127
209
|
try {
|
|
128
210
|
const forked = await forkSession(ctx.deps.db, sessionId, messageIndex);
|
|
129
|
-
return { _tag: 'success', message:
|
|
211
|
+
return { _tag: 'success', message: `已分支到新会话:${forked.id}` };
|
|
130
212
|
}
|
|
131
213
|
catch (error) {
|
|
132
214
|
return {
|
|
@@ -138,19 +220,29 @@ const forkCommand = {
|
|
|
138
220
|
};
|
|
139
221
|
const configCommand = {
|
|
140
222
|
name: 'config',
|
|
141
|
-
description: '
|
|
223
|
+
description: '查看或设置配置(支持点路径)',
|
|
142
224
|
argsHint: '[key] [value]',
|
|
143
225
|
execute: async (args, ctx) => {
|
|
144
226
|
const { getByPath, setPathPatch, coerce } = await import('./config-path.js');
|
|
227
|
+
// 展示前脱敏:apiKey/token 等敏感字段绝不原样出现在聊天里(P0 密钥暴露)。
|
|
228
|
+
const { redactSecrets } = await import('./redact.js');
|
|
145
229
|
if (!args) {
|
|
146
|
-
return {
|
|
230
|
+
return {
|
|
231
|
+
_tag: 'text',
|
|
232
|
+
text: `(合并视图:global + project 作用域;修改请用 /config <key> <value>,写入 project 作用域)\n${JSON.stringify(redactSecrets(ctx.config), null, 2)}`,
|
|
233
|
+
};
|
|
147
234
|
}
|
|
148
235
|
const parts = args.split(/\s+/);
|
|
149
236
|
const key = parts[0] ?? '';
|
|
150
237
|
if (parts.length === 1) {
|
|
151
238
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
239
|
+
// 用点路径末段作为键名参与脱敏判定:/config security.token 等单键读取同样掩码。
|
|
240
|
+
const leaf = key.split('.').pop();
|
|
241
|
+
const value = redactSecrets(getByPath(ctx.config, key), leaf);
|
|
242
|
+
return {
|
|
243
|
+
_tag: 'text',
|
|
244
|
+
text: `${key}: ${JSON.stringify(value)}(合并视图:global + project 作用域)`,
|
|
245
|
+
};
|
|
154
246
|
}
|
|
155
247
|
catch (error) {
|
|
156
248
|
return { _tag: 'error', message: error instanceof Error ? error.message : String(error) };
|
|
@@ -158,19 +250,38 @@ const configCommand = {
|
|
|
158
250
|
}
|
|
159
251
|
// 点路径写值:与 CLI config set 同语义(project 作用域最小落盘,null=unset)
|
|
160
252
|
const { applyScopedPatch, loadConfigScopes, saveConfigScoped } = await import('./config.js');
|
|
253
|
+
// 作用域收敛:security 是服务端全局参数,项目作用域写入会被加载时剥离(不生效),
|
|
254
|
+
// 直接拒绝并给出正解路径(与 CLI config set --global / Web PATCH /api/config 同语义)。
|
|
255
|
+
if (key.split('.')[0] === 'security') {
|
|
256
|
+
return {
|
|
257
|
+
_tag: 'error',
|
|
258
|
+
message: 'security 是服务端全局参数,仅在全局作用域生效。请在全局配置(~/.c0de/config.json)中设置,或用 c0de config set --global 写入。',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
161
261
|
const scopes = loadConfigScopes(ctx.cwd);
|
|
162
262
|
const value = coerce(parts.slice(1).join(' '));
|
|
163
263
|
const next = applyScopedPatch(scopes.project ?? {}, setPathPatch(key, value));
|
|
164
264
|
await saveConfigScoped('project', ctx.cwd, next);
|
|
265
|
+
// P1:/config 写入项目作用域是用户显式操作——已信任项目刷新风险指纹,
|
|
266
|
+
// 防「信任 → 改设置 → 再信任」自锁复检(与 /workflow create 同口径)。
|
|
267
|
+
try {
|
|
268
|
+
const { getByDirectory, trustProject } = await import('../project/index.js');
|
|
269
|
+
const p = await getByDirectory(ctx.deps.db, ctx.cwd);
|
|
270
|
+
if (p?.trustedAt != null)
|
|
271
|
+
await trustProject(ctx.deps.db, p.id);
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// 指纹刷新失败不阻塞配置写入结果(最坏回到 fail-closed 复检路径)
|
|
275
|
+
}
|
|
165
276
|
return {
|
|
166
277
|
_tag: 'success',
|
|
167
|
-
message: `${value === null ? '
|
|
278
|
+
message: `${value === null ? '已取消设置' : '已设置'} ${key} (scope: project)`,
|
|
168
279
|
};
|
|
169
280
|
},
|
|
170
281
|
};
|
|
171
282
|
const workflowCommand = {
|
|
172
283
|
name: 'workflow',
|
|
173
|
-
description: '
|
|
284
|
+
description: '管理并运行工作流',
|
|
174
285
|
argsHint: '[list|run|show|create|edit] [name] [args]',
|
|
175
286
|
subcommands: [
|
|
176
287
|
{ name: 'list', description: 'List available workflows' },
|
|
@@ -190,24 +301,42 @@ const workflowCommand = {
|
|
|
190
301
|
registry.register(wf);
|
|
191
302
|
}
|
|
192
303
|
}
|
|
193
|
-
// 项目级工作流:从 agent cwd(= project worktree
|
|
194
|
-
//
|
|
195
|
-
|
|
304
|
+
// 项目级工作流:从 agent cwd(= project worktree)动态发现。发现即执行
|
|
305
|
+
// 仓库代码(dynamic import 顶层即刻运行)——必须与信任门禁同口径:仅在
|
|
306
|
+
// 目录已注册为项目且 trustedAt + 指纹一致时发现(fail-closed,回退注册表)。
|
|
307
|
+
// 此前无条件发现:Web 未注册目录的会话/CLI 路径会在未信任仓库执行工作流代码。
|
|
308
|
+
let projectWorkflows = [];
|
|
309
|
+
try {
|
|
310
|
+
const { getByDirectory } = await import('../project/index.js');
|
|
311
|
+
const { projectTrustCurrent } = await import('../project/trust.js');
|
|
312
|
+
const { loadConfigScopes } = await import('./config.js');
|
|
313
|
+
const p = await getByDirectory(ctx.deps.db, ctx.cwd);
|
|
314
|
+
if (p?.trustedAt != null) {
|
|
315
|
+
const scopes = loadConfigScopes(ctx.cwd);
|
|
316
|
+
if (projectTrustCurrent(scopes.project, p.trustedAt, p.riskFingerprint, ctx.cwd)) {
|
|
317
|
+
projectWorkflows = await discoverWorkflows(ctx.cwd);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// 信任查询失败 → 不发现项目级(fail-closed)
|
|
323
|
+
}
|
|
196
324
|
const projectByName = new Map(projectWorkflows.map((w) => [w.meta.name, w]));
|
|
197
|
-
|
|
325
|
+
// 统一优先级:项目 > 用户 > 内置(与列表/resolveWorkflow 一致;此前注册表优先)。
|
|
326
|
+
const resolveEntry = (name) => projectByName.get(name) ?? registry?.get(name);
|
|
198
327
|
if (subcommand === 'list') {
|
|
199
328
|
// 合并 registry + 项目级(去重:同名项目级覆盖)
|
|
200
329
|
const byName = new Map(registry.list().map((w) => [w.meta.name, w]));
|
|
201
330
|
for (const wf of projectWorkflows)
|
|
202
331
|
byName.set(wf.meta.name, wf);
|
|
203
332
|
const workflows = Array.from(byName.values());
|
|
204
|
-
const lines = ['
|
|
333
|
+
const lines = ['可用工作流:'];
|
|
205
334
|
for (const wf of workflows) {
|
|
206
335
|
const phases = wf.meta.phases ? ` [${wf.meta.phases.join('→')}]` : '';
|
|
207
336
|
lines.push(` /${wf.meta.name}${phases} — ${wf.meta.description} (${wf.source})`);
|
|
208
337
|
}
|
|
209
338
|
lines.push('');
|
|
210
|
-
lines.push('
|
|
339
|
+
lines.push('用法:');
|
|
211
340
|
lines.push(' /workflow run <name> [args] — 执行工作流');
|
|
212
341
|
lines.push(' /workflow create <name> --file <path> — 从文件创建工作流');
|
|
213
342
|
lines.push(' /workflow edit <name> — 编辑工作流源码');
|
|
@@ -220,7 +349,7 @@ const workflowCommand = {
|
|
|
220
349
|
return { _tag: 'error', message: 'Usage: /workflow show <name>' };
|
|
221
350
|
const wf = resolveEntry(name);
|
|
222
351
|
if (!wf) {
|
|
223
|
-
return { _tag: 'error', message:
|
|
352
|
+
return { _tag: 'error', message: `未知工作流:${name}` };
|
|
224
353
|
}
|
|
225
354
|
const code = wf.sourceCode ?? '// source not available';
|
|
226
355
|
return {
|
|
@@ -241,24 +370,56 @@ const workflowCommand = {
|
|
|
241
370
|
};
|
|
242
371
|
}
|
|
243
372
|
const filePath = parts[fileIdx + 1] ?? '';
|
|
373
|
+
// 相对路径以 agent 的 cwd(项目 worktree / CLI 工作目录)解析,而非 serve 进程 cwd——
|
|
374
|
+
// 否则 Web 端 `/workflow create wf --file ./wf.md` 会读错基准目录。
|
|
375
|
+
const resolvedPath = resolve(ctx.cwd, filePath);
|
|
376
|
+
// 同名工作流已存在时覆盖需显式确认——否则误输名称会静默替换
|
|
377
|
+
// 用户级/项目级既有工作流(尤其跨层级:项目级同名会遮蔽用户级)。
|
|
378
|
+
const existing = registry?.get(name) ?? projectByName.get(name);
|
|
379
|
+
if (existing && !parts.includes('--yes')) {
|
|
380
|
+
return {
|
|
381
|
+
_tag: 'error',
|
|
382
|
+
message: `工作流 "${name}" 已存在(${existing.source} 级),覆盖会替换其内容。` +
|
|
383
|
+
'确认覆盖请加 --yes:/workflow create <name> --file <path> --yes',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
244
386
|
let source;
|
|
245
387
|
try {
|
|
246
|
-
source = await import('node:fs/promises').then((fs) => fs.readFile(
|
|
388
|
+
source = await import('node:fs/promises').then((fs) => fs.readFile(resolvedPath, 'utf-8'));
|
|
247
389
|
}
|
|
248
390
|
catch {
|
|
249
|
-
return { _tag: 'error', message:
|
|
391
|
+
return { _tag: 'error', message: `无法读取文件:${resolvedPath}` };
|
|
250
392
|
}
|
|
251
393
|
const result = await saveWorkflow(name, source, 'project', ctx.cwd);
|
|
252
394
|
if (!result.ok) {
|
|
253
395
|
return { _tag: 'error', message: result.error };
|
|
254
396
|
}
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
397
|
+
// 先刷新信任指纹(用户刚写入的 workflow 是自己写的代码)再热重载:
|
|
398
|
+
// reload 的项目级发现按 projectTrustCurrent 判定,新文件未入指纹时会
|
|
399
|
+
// 误判漂移而跳过,且「信任 → 新建 → 漂移复检」会把自己锁出门禁。
|
|
400
|
+
let projectTrusted = false;
|
|
401
|
+
try {
|
|
402
|
+
const { getByDirectory, trustProject } = await import('../project/index.js');
|
|
403
|
+
const { projectTrustCurrent } = await import('../project/trust.js');
|
|
404
|
+
const { loadConfigScopes } = await import('./config.js');
|
|
405
|
+
const p = await getByDirectory(ctx.deps.db, ctx.cwd);
|
|
406
|
+
if (p?.trustedAt != null)
|
|
407
|
+
await trustProject(ctx.deps.db, p.id);
|
|
408
|
+
projectTrusted =
|
|
409
|
+
p?.trustedAt != null &&
|
|
410
|
+
projectTrustCurrent(loadConfigScopes(ctx.cwd).project, p.trustedAt, p.riskFingerprint, ctx.cwd);
|
|
411
|
+
if (ctx.workflowRegistry) {
|
|
412
|
+
await reloadRegistry(ctx.workflowRegistry, ctx.cwd, { projectTrusted });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
// 信任查询/指纹刷新失败不阻塞创建结果(fail-closed:项目级不 import)
|
|
417
|
+
if (ctx.workflowRegistry)
|
|
418
|
+
await reloadRegistry(ctx.workflowRegistry, ctx.cwd);
|
|
258
419
|
}
|
|
259
420
|
return {
|
|
260
421
|
_tag: 'success',
|
|
261
|
-
message:
|
|
422
|
+
message: `工作流 "${name}" 已保存到 ${result.filePath}\n现在可以用 /workflow run ${name} 执行,或在对话中输入 /${name} 调用。`,
|
|
262
423
|
};
|
|
263
424
|
}
|
|
264
425
|
if (subcommand === 'edit') {
|
|
@@ -267,30 +428,27 @@ const workflowCommand = {
|
|
|
267
428
|
return { _tag: 'error', message: 'Usage: /workflow edit <name>' };
|
|
268
429
|
const wf = resolveEntry(name);
|
|
269
430
|
if (!wf) {
|
|
270
|
-
return { _tag: 'error', message:
|
|
431
|
+
return { _tag: 'error', message: `未知工作流:${name}` };
|
|
271
432
|
}
|
|
272
433
|
if (wf.source === 'builtin') {
|
|
273
434
|
return {
|
|
274
435
|
_tag: 'error',
|
|
275
|
-
message: '
|
|
436
|
+
message: '内置工作流不可编辑。请先复制:/workflow create <新名称> --file <路径>',
|
|
276
437
|
};
|
|
277
438
|
}
|
|
278
439
|
if (!wf.filePath) {
|
|
279
|
-
return { _tag: 'error', message:
|
|
280
|
-
}
|
|
281
|
-
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
|
|
282
|
-
try {
|
|
283
|
-
const { spawnSync } = await import('node:child_process');
|
|
284
|
-
spawnSync(editor, [wf.filePath], { stdio: 'inherit' });
|
|
285
|
-
}
|
|
286
|
-
catch {
|
|
287
|
-
return { _tag: 'error', message: `Failed to launch editor: ${editor}` };
|
|
288
|
-
}
|
|
289
|
-
// 编辑后热重载
|
|
290
|
-
if (ctx.workflowRegistry) {
|
|
291
|
-
await reloadRegistry(ctx.workflowRegistry, ctx.cwd);
|
|
440
|
+
return { _tag: 'error', message: `工作流 "${name}" 的文件路径不可用` };
|
|
292
441
|
}
|
|
293
|
-
|
|
442
|
+
// 斜杠命令经 Web SSE 执行,无法在浏览器里交互式打开终端编辑器——
|
|
443
|
+
// 在 serve 进程 spawn vi 会让用户既看不到也无法输入,SSE 流还会阻塞。
|
|
444
|
+
// 改为给出文件路径引导用户在本地编辑器/文件面板中编辑。
|
|
445
|
+
return {
|
|
446
|
+
_tag: 'error',
|
|
447
|
+
message: `Web 端不支持交互式编辑器。请用本地编辑器打开工作流文件后保存:\n` +
|
|
448
|
+
` ${wf.filePath}\n` +
|
|
449
|
+
`保存后下次 /workflow run ${name} 会读取最新内容(项目工作流按需从磁盘发现)。` +
|
|
450
|
+
`或使用 /workflow create <name> --file <path> 覆盖。`,
|
|
451
|
+
};
|
|
294
452
|
}
|
|
295
453
|
if (subcommand === 'run') {
|
|
296
454
|
const name = parts[1];
|
|
@@ -305,17 +463,31 @@ const workflowCommand = {
|
|
|
305
463
|
].join(', ');
|
|
306
464
|
return {
|
|
307
465
|
_tag: 'error',
|
|
308
|
-
message:
|
|
466
|
+
message: `未知工作流:"${name}"。可用:${available || '(无)'}`,
|
|
309
467
|
};
|
|
310
468
|
}
|
|
469
|
+
// 工作流会话绑定 projectId + worktreePath(P1-1):此前丢失项目归属,
|
|
470
|
+
// 子 agent 会话继承 null projectId,且后续继续该会话时代理会回退到
|
|
471
|
+
// serve 启动目录执行。
|
|
472
|
+
let workflowProjectId;
|
|
473
|
+
try {
|
|
474
|
+
const { getByDirectory } = await import('../project/index.js');
|
|
475
|
+
workflowProjectId = (await getByDirectory(ctx.deps.db, ctx.cwd))?.id ?? undefined;
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
workflowProjectId = undefined;
|
|
479
|
+
}
|
|
480
|
+
// P2-5:runner 工具集传配置解析结果(与 Web 斜杠/REST/CLI print 同口径),
|
|
481
|
+
// 作为子 agent 工具集交集基准——config.tools.enabled 的 fail-closed 语义
|
|
482
|
+
// 对工作流派发的子 agent 成立。runner 自身不进 agentLoop,无 LLM 影响。
|
|
311
483
|
const agentConfig = {
|
|
312
484
|
provider: ctx.config.defaultProvider,
|
|
313
485
|
model: ctx.config.defaultModel,
|
|
314
|
-
tools:
|
|
486
|
+
tools: resolveEnabledToolNames(ctx.deps.toolRegistry, ctx.config).filter((n) => Boolean(workflowProjectId) || !PROJECT_BOUND_TOOLS.has(n)),
|
|
315
487
|
plugins: ctx.config.plugins.enabled,
|
|
316
488
|
agentName: 'default',
|
|
317
489
|
};
|
|
318
|
-
const session = await createSession(ctx.deps.db,
|
|
490
|
+
const session = await createSession(ctx.deps.db, workflowSessionTitle(name), workflowProjectId, 'workflow', undefined, undefined, ctx.cwd);
|
|
319
491
|
const parent = await createAgent(session, agentConfig, ctx.deps);
|
|
320
492
|
return executeWorkflow({
|
|
321
493
|
registry,
|
|
@@ -328,7 +500,7 @@ const workflowCommand = {
|
|
|
328
500
|
}
|
|
329
501
|
return {
|
|
330
502
|
_tag: 'error',
|
|
331
|
-
message:
|
|
503
|
+
message: `未知子命令:${subcommand}。可用:list, run, show, create, edit`,
|
|
332
504
|
};
|
|
333
505
|
},
|
|
334
506
|
};
|
package/dist/core/title.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chat as defaultChat } from '../llm/provider.js';
|
|
2
|
+
import { buildFallbackChain } from '../llm/routing.js';
|
|
2
3
|
import { updateSessionTitle } from '../session/session.js';
|
|
3
4
|
/** 会话默认占位标题;仍是该值时才有资格自动生成。 */
|
|
4
5
|
const DEFAULT_SESSION_TITLE = 'New Session';
|
|
@@ -102,7 +103,12 @@ async function generateSessionTitle(deps, sessionId, firstMessage, chatProvider,
|
|
|
102
103
|
stream: true,
|
|
103
104
|
system: TITLE_PROMPT,
|
|
104
105
|
};
|
|
105
|
-
const
|
|
106
|
+
const fallback = buildFallbackChain(config, provider, model);
|
|
107
|
+
const text = await chatFn({ registry: llmRegistry }, request, {
|
|
108
|
+
provider,
|
|
109
|
+
model,
|
|
110
|
+
...(fallback ? { fallback } : {}),
|
|
111
|
+
});
|
|
106
112
|
const title = cleanTitle(text);
|
|
107
113
|
if (title.length === 0)
|
|
108
114
|
return;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ import type { PermissionChecker, ToolRegistry } from '../tools/types.js';
|
|
|
9
9
|
import type { AgentRegistry } from './agents/types.js';
|
|
10
10
|
import type { Config } from './config.js';
|
|
11
11
|
/** Runtime services injected into every core function (DI pattern). */
|
|
12
|
+
/** 子 agent run 句柄:注册到宿主 run 跟踪器所需的最小信息。
|
|
13
|
+
* deps 为子 agent 的 LoopDeps(cwd=隔离 worktree/父 cwd、深度注入等),
|
|
14
|
+
* 宿主据此执行 pause/resume/abort(与主 run 同口径)。 */
|
|
15
|
+
type ChildRunHandle = {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
/** 父会话 id——树级联(删除父中止全部后代、权限超时暂停主 run + 子 run)的依据。 */
|
|
18
|
+
parentSessionId?: string;
|
|
19
|
+
/** 后台任务 jobId(= 子会话 id,仅 background 模式)。 */
|
|
20
|
+
jobId?: string;
|
|
21
|
+
state: AgentState;
|
|
22
|
+
deps: AgentDependencies;
|
|
23
|
+
};
|
|
24
|
+
/** 注册子 agent run 到宿主 run 跟踪器;返回注销函数(幂等)。 */
|
|
25
|
+
type RegisterChildRun = (run: ChildRunHandle) => () => void;
|
|
12
26
|
type AgentDependencies = {
|
|
13
27
|
db: DB;
|
|
14
28
|
llmRegistry: Registry;
|
|
@@ -30,6 +44,11 @@ type AgentDependencies = {
|
|
|
30
44
|
debugSpawn?: (config: unknown) => DebugTransport;
|
|
31
45
|
/** Agent 类型注册表(spec: multi-agent-design)。注入后 task 工具可按类型派发。 */
|
|
32
46
|
agentRegistry?: AgentRegistry;
|
|
47
|
+
/** 子 agent run 注册回调(P1:此前子 run 从未入宿主 run 跟踪器,暂停/热更新/
|
|
48
|
+
* 删除会话/权限超时级联全部绕过同步与后台子 agent)。宿主注入实现:
|
|
49
|
+
* Web = agentManager.register/unregister;CLI 无 run 跟踪器,不注入即 no-op。
|
|
50
|
+
* 返回注销函数(幂等),子 agent run 终止时(finally)调用。 */
|
|
51
|
+
registerChildRun?: RegisterChildRun;
|
|
33
52
|
};
|
|
34
53
|
type ProjectInfo = {
|
|
35
54
|
name: string;
|
|
@@ -90,6 +109,8 @@ type CommandContext = {
|
|
|
90
109
|
/** 当前会话 id(Web chat 路由注入):/clear /fork 等命令默认作用于当前会话。 */
|
|
91
110
|
sessionId?: string;
|
|
92
111
|
workflowRegistry?: import('./workflows/registry.js').WorkflowRegistry;
|
|
112
|
+
/** 消费渠道(P3):/model 等命令按渠道给可操作指引(CLI 无底部模型选择器)。 */
|
|
113
|
+
channel?: 'web' | 'cli';
|
|
93
114
|
};
|
|
94
115
|
/** Declarative subcommand definition for commands like /workflow. */
|
|
95
116
|
type SubcommandDef = {
|
|
@@ -106,4 +127,4 @@ type SlashCommand = {
|
|
|
106
127
|
subcommands?: SubcommandDef[];
|
|
107
128
|
execute: (args: string, ctx: CommandContext) => Promise<CommandResult>;
|
|
108
129
|
};
|
|
109
|
-
export type { AgentConfig, AgentDependencies, AgentError, AgentEvent, AgentState, AgentStatus, ChatTool, CommandContext, CommandResult, HookRunner, LLMSegment, PendingToolCall, ProjectInfo, PromptBuildContext, PromptContext, PromptRegistry, PromptSection, SlashCommand, SubcommandDef, TokenBudget, };
|
|
130
|
+
export type { AgentConfig, AgentDependencies, AgentError, AgentEvent, AgentState, AgentStatus, ChatTool, ChildRunHandle, CommandContext, CommandResult, HookRunner, LLMSegment, PendingToolCall, ProjectInfo, PromptBuildContext, PromptContext, PromptRegistry, PromptSection, RegisterChildRun, SlashCommand, SubcommandDef, TokenBudget, };
|
|
@@ -84,10 +84,47 @@ function buildWorkflowContext(opts) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
// ── 工具函数 ──
|
|
87
|
-
/**
|
|
87
|
+
/** 把 glob 模式编译为正则:双星号跨目录,单星号与问号不跨路径分隔符,其余字符转义。 */
|
|
88
|
+
function globToRegExp(pattern) {
|
|
89
|
+
let re = '';
|
|
90
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
91
|
+
const ch = pattern[i] ?? '';
|
|
92
|
+
if (ch === '*') {
|
|
93
|
+
if (pattern[i + 1] === '*') {
|
|
94
|
+
// `**/` 须能匹配零层目录(globstar 语义:src/**/*.ts 命中 src/a.ts)
|
|
95
|
+
if (pattern[i + 2] === '/') {
|
|
96
|
+
re += '(?:.*/)?';
|
|
97
|
+
i += 2;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
re += '.*';
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
re += '[^/]*';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (ch === '?') {
|
|
109
|
+
re += '[^/]';
|
|
110
|
+
}
|
|
111
|
+
else if (/[.+^${}()|[\]\\]/.test(ch)) {
|
|
112
|
+
re += `\\${ch}`;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
re += ch;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return new RegExp(`^${re}$`);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 递归 glob。模式含路径分隔符时按相对路径匹配(双星号跨目录,如「src 下任意层级的 .ts」);
|
|
122
|
+
* 不含分隔符时按文件名匹配(与旧行为兼容:星号 .ts 命中任意层级)。
|
|
123
|
+
*/
|
|
88
124
|
async function globRecursive(rootDir, pattern) {
|
|
89
125
|
const results = [];
|
|
90
|
-
const regex =
|
|
126
|
+
const regex = globToRegExp(pattern);
|
|
127
|
+
const matchRel = pattern.includes('/');
|
|
91
128
|
async function walk(dir) {
|
|
92
129
|
let entries;
|
|
93
130
|
try {
|
|
@@ -103,8 +140,11 @@ async function globRecursive(rootDir, pattern) {
|
|
|
103
140
|
if (entry.isDirectory()) {
|
|
104
141
|
await walk(fullPath);
|
|
105
142
|
}
|
|
106
|
-
else
|
|
107
|
-
|
|
143
|
+
else {
|
|
144
|
+
const rel = relative(rootDir, fullPath);
|
|
145
|
+
if (regex.test(matchRel ? rel : entry.name)) {
|
|
146
|
+
results.push(rel);
|
|
147
|
+
}
|
|
108
148
|
}
|
|
109
149
|
}
|
|
110
150
|
}
|
|
@@ -4,8 +4,8 @@ import type { WorkflowEntry, WorkflowMeta } from './types.js';
|
|
|
4
4
|
*/
|
|
5
5
|
declare function discoverWorkflows(projectDir: string): Promise<WorkflowEntry[]>;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 扫描全局工作流目录(默认 `~/.c0de/workflows/*.js`,C0DE_CONFIG_DIR 可重定向),
|
|
8
|
+
* source 标记为 'user'。目录不存在时返回空数组(与项目级一致)。
|
|
9
9
|
*/
|
|
10
10
|
declare function discoverGlobalWorkflows(): Promise<WorkflowEntry[]>;
|
|
11
11
|
/** saveWorkflow 的目标层级。 */
|