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/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,17 +41,34 @@ 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
64
|
description: '列出可用斜杠命令',
|
|
29
65
|
execute: async (_args, ctx) => {
|
|
30
66
|
// 从 registry 动态生成,避免静态文案随命令增删漂移;
|
|
31
|
-
// 尊重 slashCommands.enabled
|
|
32
|
-
const enabledList = ctx.config.slashCommands?.enabled
|
|
33
|
-
const enabledSet = new Set(enabledList.map((n) => (n.startsWith('/') ? n.slice(1) : n)));
|
|
67
|
+
// 尊重 slashCommands.enabled 过滤(含 ['*'] = 全部启用;空 = 全部禁用)。
|
|
68
|
+
const enabledList = ctx.config.slashCommands?.enabled;
|
|
34
69
|
const commands = createSlashRegistry()
|
|
35
70
|
.list()
|
|
36
|
-
.filter((c) =>
|
|
71
|
+
.filter((c) => isSlashCommandEnabled(enabledList, c.name));
|
|
37
72
|
const width = Math.max(...commands.map((c) => `/${c.name}`.length));
|
|
38
73
|
const lines = ['可用命令:'];
|
|
39
74
|
for (const cmd of commands) {
|
|
@@ -104,6 +139,12 @@ const clearCommand = {
|
|
|
104
139
|
if (!sessionId) {
|
|
105
140
|
return { _tag: 'error', message: 'Usage: /clear [session-id] --yes(当前会话可省略 id)' };
|
|
106
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
|
+
}
|
|
107
148
|
if (!yes) {
|
|
108
149
|
return {
|
|
109
150
|
_tag: 'error',
|
|
@@ -122,7 +163,7 @@ const clearCommand = {
|
|
|
122
163
|
}
|
|
123
164
|
return {
|
|
124
165
|
_tag: 'success',
|
|
125
|
-
message: `已清空 ${ids.length}
|
|
166
|
+
message: `已清空 ${ids.length} 条消息(原内容已归档,可在会话页「归档」面板查看;历史 token/成本仍计入本会话用量统计)`,
|
|
126
167
|
};
|
|
127
168
|
},
|
|
128
169
|
};
|
|
@@ -132,14 +173,14 @@ const forkCommand = {
|
|
|
132
173
|
argsHint: '[session-id] [message-index]',
|
|
133
174
|
execute: async (args, ctx) => {
|
|
134
175
|
const parts = args.split(/\s+/).filter(Boolean);
|
|
135
|
-
//
|
|
176
|
+
// 默认当前会话;第一个非纯数字参数视为 session id 覆盖(fork 其他会话)。
|
|
136
177
|
let sessionId = ctx.sessionId;
|
|
137
178
|
let messageIndex;
|
|
138
179
|
for (const p of parts) {
|
|
139
180
|
if (/^\d+$/.test(p)) {
|
|
140
181
|
messageIndex = Number.parseInt(p, 10);
|
|
141
182
|
}
|
|
142
|
-
else
|
|
183
|
+
else {
|
|
143
184
|
sessionId = p;
|
|
144
185
|
}
|
|
145
186
|
}
|
|
@@ -149,6 +190,12 @@ const forkCommand = {
|
|
|
149
190
|
message: 'Usage: /fork [session-id] [message-index](当前会话可省略 id)',
|
|
150
191
|
};
|
|
151
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
|
+
}
|
|
152
199
|
// 未指定 index → 默认最新一条消息处分支(与 web fork API 语义一致,不再是 index 0)
|
|
153
200
|
if (messageIndex === undefined) {
|
|
154
201
|
const { getMessages } = await import('../session/message.js');
|
|
@@ -203,10 +250,29 @@ const configCommand = {
|
|
|
203
250
|
}
|
|
204
251
|
// 点路径写值:与 CLI config set 同语义(project 作用域最小落盘,null=unset)
|
|
205
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
|
+
}
|
|
206
261
|
const scopes = loadConfigScopes(ctx.cwd);
|
|
207
262
|
const value = coerce(parts.slice(1).join(' '));
|
|
208
263
|
const next = applyScopedPatch(scopes.project ?? {}, setPathPatch(key, value));
|
|
209
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
|
+
}
|
|
210
276
|
return {
|
|
211
277
|
_tag: 'success',
|
|
212
278
|
message: `${value === null ? '已取消设置' : '已设置'} ${key} (scope: project)`,
|
|
@@ -235,11 +301,29 @@ const workflowCommand = {
|
|
|
235
301
|
registry.register(wf);
|
|
236
302
|
}
|
|
237
303
|
}
|
|
238
|
-
// 项目级工作流:从 agent cwd(= project worktree
|
|
239
|
-
//
|
|
240
|
-
|
|
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
|
+
}
|
|
241
324
|
const projectByName = new Map(projectWorkflows.map((w) => [w.meta.name, w]));
|
|
242
|
-
|
|
325
|
+
// 统一优先级:项目 > 用户 > 内置(与列表/resolveWorkflow 一致;此前注册表优先)。
|
|
326
|
+
const resolveEntry = (name) => projectByName.get(name) ?? registry?.get(name);
|
|
243
327
|
if (subcommand === 'list') {
|
|
244
328
|
// 合并 registry + 项目级(去重:同名项目级覆盖)
|
|
245
329
|
const byName = new Map(registry.list().map((w) => [w.meta.name, w]));
|
|
@@ -286,20 +370,52 @@ const workflowCommand = {
|
|
|
286
370
|
};
|
|
287
371
|
}
|
|
288
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
|
+
}
|
|
289
386
|
let source;
|
|
290
387
|
try {
|
|
291
|
-
source = await import('node:fs/promises').then((fs) => fs.readFile(
|
|
388
|
+
source = await import('node:fs/promises').then((fs) => fs.readFile(resolvedPath, 'utf-8'));
|
|
292
389
|
}
|
|
293
390
|
catch {
|
|
294
|
-
return { _tag: 'error', message: `无法读取文件:${
|
|
391
|
+
return { _tag: 'error', message: `无法读取文件:${resolvedPath}` };
|
|
295
392
|
}
|
|
296
393
|
const result = await saveWorkflow(name, source, 'project', ctx.cwd);
|
|
297
394
|
if (!result.ok) {
|
|
298
395
|
return { _tag: 'error', message: result.error };
|
|
299
396
|
}
|
|
300
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
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);
|
|
303
419
|
}
|
|
304
420
|
return {
|
|
305
421
|
_tag: 'success',
|
|
@@ -350,14 +466,28 @@ const workflowCommand = {
|
|
|
350
466
|
message: `未知工作流:"${name}"。可用:${available || '(无)'}`,
|
|
351
467
|
};
|
|
352
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 影响。
|
|
353
483
|
const agentConfig = {
|
|
354
484
|
provider: ctx.config.defaultProvider,
|
|
355
485
|
model: ctx.config.defaultModel,
|
|
356
|
-
tools:
|
|
486
|
+
tools: resolveEnabledToolNames(ctx.deps.toolRegistry, ctx.config).filter((n) => Boolean(workflowProjectId) || !PROJECT_BOUND_TOOLS.has(n)),
|
|
357
487
|
plugins: ctx.config.plugins.enabled,
|
|
358
488
|
agentName: 'default',
|
|
359
489
|
};
|
|
360
|
-
const session = await createSession(ctx.deps.db,
|
|
490
|
+
const session = await createSession(ctx.deps.db, workflowSessionTitle(name), workflowProjectId, 'workflow', undefined, undefined, ctx.cwd);
|
|
361
491
|
const parent = await createAgent(session, agentConfig, ctx.deps);
|
|
362
492
|
return executeWorkflow({
|
|
363
493
|
registry,
|
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;
|
|
@@ -108,4 +127,4 @@ type SlashCommand = {
|
|
|
108
127
|
subcommands?: SubcommandDef[];
|
|
109
128
|
execute: (args: string, ctx: CommandContext) => Promise<CommandResult>;
|
|
110
129
|
};
|
|
111
|
-
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 的目标层级。 */
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
2
|
import { basename, join } from 'node:path';
|
|
4
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { resolveGlobalConfigDir } from '../config.js';
|
|
5
5
|
/** 项目级工作流目录相对路径。 */
|
|
6
6
|
const PROJECT_WORKFLOWS_DIR = '.c0de/workflows';
|
|
7
|
-
/** 用户级(全局)工作流目录:~/.c0de/workflows。 */
|
|
8
|
-
const GLOBAL_WORKFLOWS_DIR = join('.c0de', 'workflows');
|
|
9
7
|
/**
|
|
10
8
|
* 扫描指定目录下的 `*.js` 工作流文件,dynamic import 后转为 WorkflowEntry。
|
|
11
9
|
* import 失败的文件跳过(warn),不阻塞其他工作流加载。
|
|
@@ -60,11 +58,11 @@ async function discoverWorkflows(projectDir) {
|
|
|
60
58
|
return discoverFromDir(join(projectDir, PROJECT_WORKFLOWS_DIR), 'project');
|
|
61
59
|
}
|
|
62
60
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* 扫描全局工作流目录(默认 `~/.c0de/workflows/*.js`,C0DE_CONFIG_DIR 可重定向),
|
|
62
|
+
* source 标记为 'user'。目录不存在时返回空数组(与项目级一致)。
|
|
65
63
|
*/
|
|
66
64
|
async function discoverGlobalWorkflows() {
|
|
67
|
-
return discoverFromDir(join(
|
|
65
|
+
return discoverFromDir(join(resolveGlobalConfigDir(), 'workflows'), 'user');
|
|
68
66
|
}
|
|
69
67
|
/** 合法工作流名称:仅小写字母、数字、连字符。 */
|
|
70
68
|
const WORKFLOW_NAME_RE = /^[a-z0-9-]+$/;
|
|
@@ -82,7 +80,7 @@ async function saveWorkflow(name, source, target = 'project', projectDir) {
|
|
|
82
80
|
}
|
|
83
81
|
const dir = target === 'project'
|
|
84
82
|
? join(projectDir ?? '.', PROJECT_WORKFLOWS_DIR)
|
|
85
|
-
: join(
|
|
83
|
+
: join(resolveGlobalConfigDir(), 'workflows');
|
|
86
84
|
await mkdir(dir, { recursive: true });
|
|
87
85
|
const filePath = join(dir, `${name}.js`);
|
|
88
86
|
await writeFile(filePath, source, 'utf-8');
|
|
@@ -4,5 +4,6 @@ export type { SaveResult, SaveTarget } from './discovery.js';
|
|
|
4
4
|
export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow } from './discovery.js';
|
|
5
5
|
export type { WorkflowRegistry } from './registry.js';
|
|
6
6
|
export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry } from './registry.js';
|
|
7
|
-
export {
|
|
7
|
+
export { resolveWorkflow } from './resolve.js';
|
|
8
|
+
export { executeWorkflow, workflowSessionTitle } from './runtime.js';
|
|
8
9
|
export type { WorkflowAgentResult, WorkflowContext, WorkflowEntry, WorkflowMeta, WorkflowModule, WorkflowResult, WorkflowSource, WorkflowUtils, } from './types.js';
|
|
@@ -2,4 +2,5 @@ export { BUILTIN_WORKFLOWS, createBuiltinWorkflows } from './builtins.js';
|
|
|
2
2
|
export { buildWorkflowContext } from './context.js';
|
|
3
3
|
export { discoverGlobalWorkflows, discoverWorkflows, saveWorkflow } from './discovery.js';
|
|
4
4
|
export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry } from './registry.js';
|
|
5
|
-
export {
|
|
5
|
+
export { resolveWorkflow } from './resolve.js';
|
|
6
|
+
export { executeWorkflow, workflowSessionTitle } from './runtime.js';
|
|
@@ -10,18 +10,28 @@ declare function createWorkflowRegistry(): {
|
|
|
10
10
|
clear(): void;
|
|
11
11
|
};
|
|
12
12
|
type WorkflowRegistry = ReturnType<typeof createWorkflowRegistry>;
|
|
13
|
+
type RegistryPopulateOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* 项目是否当前可信任(P0 信任边界):false 时跳过项目级
|
|
16
|
+
* `.c0de/workflows/*.js` 发现(不 dynamic import 仓库代码)。
|
|
17
|
+
* 缺省为 false——调用方必须显式确认信任后才放行(fail-closed)。
|
|
18
|
+
* 内置与全局 `~/.c0de/workflows` 是用户本机显式安装,始终加载。
|
|
19
|
+
*/
|
|
20
|
+
projectTrusted?: boolean;
|
|
21
|
+
};
|
|
13
22
|
/**
|
|
14
23
|
* 创建并填充工作流注册表(三级发现,后注册覆盖同名):
|
|
15
24
|
* 1. 注册内置工作流(builtin)
|
|
16
25
|
* 2. 发现并注册全局 `~/.c0de/workflows/*.js`(user)
|
|
17
|
-
* 3. 发现并注册项目 `.c0de/workflows/*.js`(project
|
|
26
|
+
* 3. 发现并注册项目 `.c0de/workflows/*.js`(project;仅 projectTrusted 时)
|
|
18
27
|
* 覆盖优先级:project > user > builtin。
|
|
19
28
|
*/
|
|
20
|
-
declare function createAndPopulateRegistry(projectDir: string): Promise<WorkflowRegistry>;
|
|
29
|
+
declare function createAndPopulateRegistry(projectDir: string, opts?: RegistryPopulateOptions): Promise<WorkflowRegistry>;
|
|
21
30
|
/**
|
|
22
31
|
* 重新填充已有注册表:清空 → 三级发现 → 注册。
|
|
23
32
|
* 用于 create/save 后热重载,保留同一 registry 引用。
|
|
33
|
+
* 项目级发现同样受 projectTrusted 门禁(fail-closed)。
|
|
24
34
|
*/
|
|
25
|
-
declare function reloadRegistry(registry: WorkflowRegistry, projectDir: string): Promise<void>;
|
|
35
|
+
declare function reloadRegistry(registry: WorkflowRegistry, projectDir: string, opts?: RegistryPopulateOptions): Promise<void>;
|
|
26
36
|
export type { WorkflowRegistry };
|
|
27
37
|
export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry };
|
|
@@ -29,10 +29,10 @@ function createWorkflowRegistry() {
|
|
|
29
29
|
* 创建并填充工作流注册表(三级发现,后注册覆盖同名):
|
|
30
30
|
* 1. 注册内置工作流(builtin)
|
|
31
31
|
* 2. 发现并注册全局 `~/.c0de/workflows/*.js`(user)
|
|
32
|
-
* 3. 发现并注册项目 `.c0de/workflows/*.js`(project
|
|
32
|
+
* 3. 发现并注册项目 `.c0de/workflows/*.js`(project;仅 projectTrusted 时)
|
|
33
33
|
* 覆盖优先级:project > user > builtin。
|
|
34
34
|
*/
|
|
35
|
-
async function createAndPopulateRegistry(projectDir) {
|
|
35
|
+
async function createAndPopulateRegistry(projectDir, opts = {}) {
|
|
36
36
|
const registry = createWorkflowRegistry();
|
|
37
37
|
// 1. 内置(由源码字符串动态导入生成,show === run)
|
|
38
38
|
for (const wf of await createBuiltinWorkflows()) {
|
|
@@ -43,18 +43,21 @@ async function createAndPopulateRegistry(projectDir) {
|
|
|
43
43
|
for (const wf of globalWorkflows) {
|
|
44
44
|
registry.register(wf);
|
|
45
45
|
}
|
|
46
|
-
// 3. 项目级(.c0de/workflows
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
// 3. 项目级(.c0de/workflows)——未信任项目绝不 import(任意代码执行面)
|
|
47
|
+
if (opts.projectTrusted) {
|
|
48
|
+
const projectWorkflows = await discoverWorkflows(projectDir);
|
|
49
|
+
for (const wf of projectWorkflows) {
|
|
50
|
+
registry.register(wf);
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
return registry;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
56
|
* 重新填充已有注册表:清空 → 三级发现 → 注册。
|
|
55
57
|
* 用于 create/save 后热重载,保留同一 registry 引用。
|
|
58
|
+
* 项目级发现同样受 projectTrusted 门禁(fail-closed)。
|
|
56
59
|
*/
|
|
57
|
-
async function reloadRegistry(registry, projectDir) {
|
|
60
|
+
async function reloadRegistry(registry, projectDir, opts = {}) {
|
|
58
61
|
registry.clear();
|
|
59
62
|
for (const wf of await createBuiltinWorkflows()) {
|
|
60
63
|
registry.register(wf);
|
|
@@ -62,8 +65,10 @@ async function reloadRegistry(registry, projectDir) {
|
|
|
62
65
|
for (const wf of await discoverGlobalWorkflows()) {
|
|
63
66
|
registry.register(wf);
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
if (opts.projectTrusted) {
|
|
69
|
+
for (const wf of await discoverWorkflows(projectDir)) {
|
|
70
|
+
registry.register(wf);
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
export { createAndPopulateRegistry, createWorkflowRegistry, reloadRegistry };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { WorkflowRegistry } from './registry.js';
|
|
2
|
+
import type { WorkflowEntry } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* 统一工作流解析:按「项目(已信任)> 用户 > 内置」优先级查找同名工作流。
|
|
5
|
+
*
|
|
6
|
+
* 此前各消费方口径分裂:列表(GET /、/workflow list)项目级覆盖注册表,
|
|
7
|
+
* 而运行/查看(REST GET /:name、POST /:name/run、chat 斜杠、slash resolveEntry)
|
|
8
|
+
* 是注册表优先——项目级同名工作流被用户级/内置条目遮蔽,UI 显示项目版、
|
|
9
|
+
* 实际执行另一份。全部调用点必须经此函数收敛。
|
|
10
|
+
*
|
|
11
|
+
* 项目级发现受信任门禁:discoverWorkflows 会 dynamic import 仓库代码
|
|
12
|
+
* (模块顶层即刻执行),未信任/漂移的项目绝不发现(fail-closed,回退注册表)。
|
|
13
|
+
*/
|
|
14
|
+
declare function resolveWorkflow(opts: {
|
|
15
|
+
name: string;
|
|
16
|
+
registry?: WorkflowRegistry | null;
|
|
17
|
+
/** 项目目录(agent cwd / 项目 worktree);缺省不发现项目级工作流。 */
|
|
18
|
+
projectDir?: string;
|
|
19
|
+
/** 项目当前是否可信(trustedAt + 指纹一致)。false/缺省 → 跳过项目级发现。 */
|
|
20
|
+
projectTrusted?: boolean;
|
|
21
|
+
}): Promise<WorkflowEntry | undefined>;
|
|
22
|
+
export { resolveWorkflow };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { discoverWorkflows } from './discovery.js';
|
|
2
|
+
/**
|
|
3
|
+
* 统一工作流解析:按「项目(已信任)> 用户 > 内置」优先级查找同名工作流。
|
|
4
|
+
*
|
|
5
|
+
* 此前各消费方口径分裂:列表(GET /、/workflow list)项目级覆盖注册表,
|
|
6
|
+
* 而运行/查看(REST GET /:name、POST /:name/run、chat 斜杠、slash resolveEntry)
|
|
7
|
+
* 是注册表优先——项目级同名工作流被用户级/内置条目遮蔽,UI 显示项目版、
|
|
8
|
+
* 实际执行另一份。全部调用点必须经此函数收敛。
|
|
9
|
+
*
|
|
10
|
+
* 项目级发现受信任门禁:discoverWorkflows 会 dynamic import 仓库代码
|
|
11
|
+
* (模块顶层即刻执行),未信任/漂移的项目绝不发现(fail-closed,回退注册表)。
|
|
12
|
+
*/
|
|
13
|
+
async function resolveWorkflow(opts) {
|
|
14
|
+
const { name, registry, projectDir, projectTrusted } = opts;
|
|
15
|
+
if (projectDir && projectTrusted) {
|
|
16
|
+
const discovered = await discoverWorkflows(projectDir);
|
|
17
|
+
const projectEntry = discovered.find((w) => w.meta.name === name);
|
|
18
|
+
if (projectEntry)
|
|
19
|
+
return projectEntry;
|
|
20
|
+
}
|
|
21
|
+
return registry?.get(name);
|
|
22
|
+
}
|
|
23
|
+
export { resolveWorkflow };
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { AgentDependencies, AgentState, CommandResult } from '../types.js';
|
|
2
2
|
import type { WorkflowRegistry } from './registry.js';
|
|
3
3
|
import type { WorkflowEntry } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* 工作流运行会话标题:`workflow:<name> MM-DD HH:mm`。
|
|
6
|
+
* 同名工作流多次运行在会话树里可区分,不再是无时间戳的同名行。
|
|
7
|
+
*/
|
|
8
|
+
declare function workflowSessionTitle(name: string, now?: Date): string;
|
|
4
9
|
/** executeWorkflow 的参数。 */
|
|
5
10
|
type ExecuteWorkflowOpts = {
|
|
6
11
|
registry: WorkflowRegistry;
|
|
@@ -18,4 +23,4 @@ type ExecuteWorkflowOpts = {
|
|
|
18
23
|
* 工作流 return 的 output 作为 text 返回;异常捕获为 error。
|
|
19
24
|
*/
|
|
20
25
|
declare function executeWorkflow(opts: ExecuteWorkflowOpts): Promise<CommandResult>;
|
|
21
|
-
export { executeWorkflow };
|
|
26
|
+
export { executeWorkflow, workflowSessionTitle };
|
|
@@ -14,6 +14,14 @@ function createTimeoutPromise(timeoutMs, timeoutSeconds) {
|
|
|
14
14
|
timer.unref();
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* 工作流运行会话标题:`workflow:<name> MM-DD HH:mm`。
|
|
19
|
+
* 同名工作流多次运行在会话树里可区分,不再是无时间戳的同名行。
|
|
20
|
+
*/
|
|
21
|
+
function workflowSessionTitle(name, now = new Date()) {
|
|
22
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
23
|
+
return `workflow:${name} ${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
24
|
+
}
|
|
17
25
|
/**
|
|
18
26
|
* 执行工作流:查注册表 → 构建 ctx → 调用 entry.execute → 返回 CommandResult。
|
|
19
27
|
*
|
|
@@ -56,9 +64,18 @@ async function executeWorkflow(opts) {
|
|
|
56
64
|
}
|
|
57
65
|
catch (e) {
|
|
58
66
|
if (e instanceof WorkflowTimeoutError) {
|
|
67
|
+
// 超时必须真正终止执行,而不是只对用户报错——此前仅 race reject,
|
|
68
|
+
// 工作流的子 agent 仍在后台继续调 LLM/改文件(继续计费且无中止手柄)。
|
|
69
|
+
// 与 abortAgent 同语义;内联实现避免 import 循环
|
|
70
|
+
// (agent.ts → loop.ts → prompt-registry.ts → slash.ts → workflows/index.ts → 本文件)。
|
|
71
|
+
// 父 abort 经 runSubAgent 的 abort 级联传递给全部子 agent。
|
|
72
|
+
parent.abortController.abort();
|
|
73
|
+
if (parent.status._tag === 'running' || parent.status._tag === 'paused') {
|
|
74
|
+
parent.status = { _tag: 'stopped', reason: 'aborted' };
|
|
75
|
+
}
|
|
59
76
|
return {
|
|
60
77
|
_tag: 'error',
|
|
61
|
-
message: `Workflow "${name}" timed out after ${timeoutSeconds}s
|
|
78
|
+
message: `Workflow "${name}" timed out after ${timeoutSeconds}s(执行已中止)`,
|
|
62
79
|
};
|
|
63
80
|
}
|
|
64
81
|
return {
|
|
@@ -67,4 +84,4 @@ async function executeWorkflow(opts) {
|
|
|
67
84
|
};
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
|
-
export { executeWorkflow };
|
|
87
|
+
export { executeWorkflow, workflowSessionTitle };
|