@iamsamyiok/agents-chat 3.18.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.
@@ -0,0 +1,686 @@
1
+ // 数据存储:纯 JSON 文件,零依赖
2
+ // 文件位于数据目录(默认 <root>/.data),任务与消息持久化
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const ROOT = path.join(__dirname, '..', '..');
7
+ const DATA_DIR = process.env.AGENTS_CHAT_DATA || path.join(ROOT, '.data');
8
+ const CONFIG_PATH = path.join(DATA_DIR, 'config.json');
9
+ const TASKS_PATH = path.join(DATA_DIR, 'tasks.json');
10
+ const MESSAGES_PATH = path.join(DATA_DIR, 'messages.json');
11
+ const MEMORY_PATH = path.join(DATA_DIR, 'memory.json');
12
+ const OC_SESSIONS_PATH = path.join(DATA_DIR, 'oc-sessions.json');
13
+
14
+ function ensureDir() {
15
+ if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
16
+ }
17
+
18
+ function readJson(file, fallback) {
19
+ try {
20
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ } catch {
22
+ return fallback;
23
+ }
24
+ }
25
+
26
+ function writeJson(file, data) {
27
+ ensureDir();
28
+ const tmp = file + '.tmp';
29
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8');
30
+ fs.renameSync(tmp, file);
31
+ }
32
+
33
+ // ---------- 内置管家智能体(不可修改、不可删除,始终置顶) ----------
34
+ const BUTLER = {
35
+ id: 'butler',
36
+ name: '管家',
37
+ locked: true,
38
+ behavior: 'butler', // 仅演示模式生效
39
+ model: '',
40
+ icon: '🎩',
41
+ desc: '内置调度智能体:理解需求、拆解任务、调度子智能体并行/串行执行并汇总交付',
42
+ systemPrompt: '你是「管家」,Agents 群聊的内置总调度。用户未点名智能体的消息由你负责:理解需求、制定调度方案、指挥子智能体完成工作并向用户汇总交付。最终交付时先给出简明的结果概要,如有成果文件必须明确告知完整路径。与用户沟通使用简体中文,简洁、专业、有进展感。'
43
+ };
44
+
45
+ // ---------- 默认配置 ----------
46
+ // 统一 OpenCode 内核;每个实例 = persona + model + 独立会话
47
+ // behavior 仅在演示模式(AGENTS_CHAT_MOCK=1)下生效
48
+ function defaultConfig() {
49
+ return {
50
+ defaultAgent: '',
51
+ globalCwd: '',
52
+ kernel: 'auto', // 执行内核:'auto' 按检测顺序自动,或 opencode/claude/codex/pi
53
+ agents: [
54
+ BUTLER,
55
+ {
56
+ id: 'ag-eng',
57
+ name: '工程师',
58
+ behavior: 'worker-good',
59
+ model: '',
60
+ icon: '🧑‍💻',
61
+ desc: '软件开发:写代码、修 bug、重构、技术方案评估',
62
+ systemPrompt: '你是资深软件工程师。直接给出可靠的实现方案与代码,结果需可直接使用。用中文回复。'
63
+ },
64
+ {
65
+ id: 'ag-rs',
66
+ name: '研究员',
67
+ behavior: 'echo',
68
+ model: '',
69
+ icon: '🔍',
70
+ desc: '信息检索:联网查资料、阅读文件、调研并汇总要点',
71
+ systemPrompt: '你是信息研究员。善于联网检索、阅读文件并汇总要点,结论需注明依据。用中文回复。'
72
+ },
73
+ {
74
+ id: 'ag-wr',
75
+ name: '文案',
76
+ behavior: 'echo',
77
+ model: '',
78
+ icon: '✍️',
79
+ desc: '文字工作:文章、报告、翻译、润色',
80
+ systemPrompt: '你是专业写手。文字流畅、结构清晰、符合中文表达习惯。用中文回复。'
81
+ }
82
+ ]
83
+ };
84
+ }
85
+
86
+ function getConfig() {
87
+ let cfg = readJson(CONFIG_PATH, null);
88
+ if (!cfg || !Array.isArray(cfg.agents)) {
89
+ cfg = defaultConfig();
90
+ writeJson(CONFIG_PATH, cfg);
91
+ }
92
+ // 管家始终置顶且使用内置定义(保证内置人设更新后自动生效)
93
+ cfg.agents = [BUTLER, ...cfg.agents.filter(a => a && a.id !== 'butler')];
94
+ cfg.defaultAgent = ''; // 已废弃:未点名消息一律由管家调度,兼容旧配置强制清空
95
+ return cfg;
96
+ }
97
+
98
+ function saveConfig(cfg) {
99
+ writeJson(CONFIG_PATH, cfg);
100
+ }
101
+
102
+ // 保存用户自定义的子智能体列表 + 全局统一工作目录 + 执行内核(管家由内置定义补充,不接受传入)
103
+ // 基于现有配置增量合并,保留 schedEnabled 等其他字段不被覆盖丢失
104
+ function saveAgents(userAgents, globalCwd, kernel, approval) {
105
+ const cfg = getConfig();
106
+ saveConfig({
107
+ ...cfg,
108
+ defaultAgent: '',
109
+ globalCwd: globalCwd !== undefined ? globalCwd : (cfg.globalCwd || ''),
110
+ kernel: kernel !== undefined ? kernel : (cfg.kernel || 'auto'),
111
+ approval: approval !== undefined ? approval : (cfg.approval || ''),
112
+ agents: [BUTLER, ...userAgents]
113
+ });
114
+ }
115
+
116
+ // 定时任务调度总开关(配置页侧栏「启动/关闭定时任务」按钮)
117
+ function getSchedEnabled() {
118
+ return getConfig().schedEnabled !== false;
119
+ }
120
+ function setSchedEnabled(enabled) {
121
+ const cfg = getConfig();
122
+ saveConfig({ ...cfg, schedEnabled: !!enabled });
123
+ }
124
+
125
+ function getAgents() {
126
+ return getConfig().agents;
127
+ }
128
+
129
+ // ---------- 团队仓库:经典智能体团队预设,一键应用 ----------
130
+ // 取材自开源社区验证过的经典组合:
131
+ // - 软件开发团队:MetaGPT / ChatDev 的「虚拟软件公司」流水线(产品→架构→工程→测试)
132
+ // - 内容创作团队:CrewAI 内容营销经典(研究→写作→编辑)
133
+ // - 深度调研团队:CrewAI 官方 Research Crew(检索→分析报告)
134
+ // - 翻译校对团队:CrewAI 入门经典(翻译→审校)
135
+ // 应用 = 写入子智能体名单(管家仍为内置调度者,相当于团队 Supervisor)
136
+ const TEAM_PRESETS = [
137
+ {
138
+ id: 'plan-exec-verify',
139
+ name: '规划·执行·核验',
140
+ icon: '🧩',
141
+ source: '通用任务编排经典三角色(规划 → 执行 → 核验)',
142
+ desc: '最精简的闭环工作流:规划者先把「怎么做」想清楚并产出方案,执行者按方案落地交付,核验者对照目标与计划验收把关。三者按需接力,适合绝大多数需要「先想清、再做、最后查」的任务。',
143
+ usage: '例:「帮我把这个需求落地成一个可运行方案并验证」',
144
+ agents: [
145
+ {
146
+ name: '规划者', icon: '🧭',
147
+ desc: '方案设计:把目标拆成可执行步骤,明确怎么做',
148
+ systemPrompt: '你是规划者。面对用户的目标,你负责「确定怎么做」:先澄清模糊点,再把目标分解为有序、可执行的步骤(每个步骤的目标、产出、验收标准、依赖关系清晰),标明关键风险与取舍,给出推荐的技术/工具与里程碑。你只产出方案与计划,不写最终交付物本身;计划要具体到执行者能照做。用中文回复。'
149
+ },
150
+ {
151
+ name: '执行者', icon: '🧑‍💻',
152
+ desc: '具体实施:按规划落地,产出真实成果',
153
+ systemPrompt: '你是执行者。严格遵循规划者给出的方案与步骤落地实施:产出真实、完整、可运行或可用的成果(代码、文档、产物等),关键决策符合规划;遇到计划未覆盖的情况在范围内合理处置并明确标注。交付前自检成果是否满足验收标准。用中文说明实现要点与产出位置。'
154
+ },
155
+ {
156
+ name: '核验者', icon: '✅',
157
+ desc: '验收核验:对照目标与计划核查成果是否达标',
158
+ systemPrompt: '你是核验者。对照原始目标与规划者的计划,逐项核验执行者交付的成果:是否达成目标、是否满足验收标准、有无遗漏或偏差或错误。给出明确的「通过 / 不通过」结论;不通过时列出具体问题与最小修改建议,可要求执行者返工。结论必须基于事实核查,不轻信自述。用中文回复。'
159
+ }
160
+ ]
161
+ },
162
+ {
163
+ id: 'software-dev',
164
+ name: '软件开发团队',
165
+ icon: '🏗️',
166
+ source: 'MetaGPT / ChatDev「虚拟软件公司」',
167
+ desc: '从一句话需求到可运行软件的经典流水线:产品经理写需求文档,架构师做系统设计,工程师编码实现,测试工程师验收把关。适合做工具、脚本、小型应用。',
168
+ usage: '例:「帮我做一个记账小工具」',
169
+ agents: [
170
+ {
171
+ name: '产品经理', icon: '🧭',
172
+ desc: '需求分析:把想法变成结构化需求文档(PRD)',
173
+ systemPrompt: '你是资深产品经理。把用户需求转化为清晰的需求文档:目标用户、核心功能列表(按优先级)、每个功能的用户故事与验收标准、边界与约束。输出结构化、可执行的 PRD,不做技术设计。善于向用户澄清模糊需求。用中文回复。'
174
+ },
175
+ {
176
+ name: '架构师', icon: '📐',
177
+ desc: '系统设计:技术选型、模块划分、接口与数据结构',
178
+ systemPrompt: '你是资深软件架构师。基于需求文档做系统设计:技术选型(优先简单成熟方案)、模块划分、核心接口定义、数据结构、目录结构与关键实现思路。设计要具体到工程师可直接照做,避免空泛。用中文回复。'
179
+ },
180
+ {
181
+ name: '工程师', icon: '🧑‍💻',
182
+ desc: '编码实现:写出可直接运行的完整代码并调试',
183
+ systemPrompt: '你是资深软件工程师。按照系统设计编写完整、可直接运行的代码:文件路径清晰、依赖最小化、关键逻辑有注释、边界情况有处理。遇到报错要实际调试修复而非绕过,交付前自行检查代码可运行。用中文说明实现要点。'
184
+ },
185
+ {
186
+ name: '测试工程师', icon: '🧪',
187
+ desc: '质量把关:写测试用例、执行测试、找缺陷',
188
+ systemPrompt: '你是资深测试工程师。基于需求与实现编写测试用例(正常路径、边界、异常),实际执行测试并如实报告结果,不放过任何缺陷;发现的问题给出最小复现步骤与修复建议。验收结论必须基于事实而非自述。用中文回复。'
189
+ }
190
+ ]
191
+ },
192
+ {
193
+ id: 'content-studio',
194
+ name: '内容创作团队',
195
+ icon: '✍️',
196
+ source: 'CrewAI 内容营销经典组合',
197
+ desc: '专业内容生产流水线:研究员收集素材与事实,作家撰写初稿,编辑审校润色与核查。适合公众号文章、营销文案、深度稿件、报告行文。',
198
+ usage: '例:「写一篇关于远程办公利弊的文章」',
199
+ agents: [
200
+ {
201
+ name: '研究员', icon: '🔍',
202
+ desc: '素材收集:检索资料、核实事实、整理要点',
203
+ systemPrompt: '你是信息研究员。围绕写作主题收集素材:核心事实与数据(注明来源)、正反方观点、典型案例、金句素材。输出结构化的素材清单供写作使用,事实必须核实,不确定的明确标注。用中文回复。'
204
+ },
205
+ {
206
+ name: '作家', icon: '✍️',
207
+ desc: '初稿撰写:结构清晰、有观点、有文采的成稿',
208
+ systemPrompt: '你是专业作家。基于研究员的素材撰写成稿:抓人的开头、清晰的段落结构、明确的观点立场、有力的结尾。字数符合要求,风格适配目标读者,素材用得自然而不堆砌。用中文写作。'
209
+ },
210
+ {
211
+ name: '编辑', icon: '📖',
212
+ desc: '审校把关:事实核查、逻辑梳理、文字润色',
213
+ systemPrompt: '你是资深编辑。对初稿做三件事:核查事实(数据、引述是否有依据)、梳理逻辑(结构、论证是否自洽)、润色文字(去冗余、改病句、统一术语与风格)。输出修改后的终稿并简要说明主要改动。用中文回复。'
214
+ }
215
+ ]
216
+ },
217
+ {
218
+ id: 'research-crew',
219
+ name: '深度调研团队',
220
+ icon: '🔍',
221
+ source: 'CrewAI 官方 Research Crew',
222
+ desc: '两步调研法:研究员全面检索收集,分析师综合研判输出报告(摘要/洞察/趋势/建议)。适合技术选型、行业分析、竞品对比、决策支持。',
223
+ usage: '例:「调研 2026 年主流前端框架该怎么选」',
224
+ agents: [
225
+ {
226
+ name: '研究员', icon: '🔍',
227
+ desc: '全面检索:多来源收集信息并交叉验证',
228
+ systemPrompt: '你是信息研究员。对调研主题做全面检索:现状、主流方案/观点、关键数据、优劣势对比、近期变化。多来源交叉验证,每个结论标注依据来源,冲突信息如实呈现。输出结构化的调研材料。用中文回复。'
229
+ },
230
+ {
231
+ name: '分析师', icon: '📊',
232
+ desc: '综合研判:把材料提炼成有结论的分析报告',
233
+ systemPrompt: '你是资深分析师。基于调研材料输出专业分析报告:执行摘要(3~5 句)、关键发现(编号列出)、趋势判断、风险提示、明确可执行的建议(含适用条件)。有数据用数据,没数据不编造,结论要敢下但注明置信度。用中文回复。'
234
+ }
235
+ ]
236
+ },
237
+ {
238
+ id: 'translation-duo',
239
+ name: '翻译校对团队',
240
+ icon: '🌐',
241
+ source: 'CrewAI 入门经典:翻译员 + 校对员',
242
+ desc: '双人精翻组合:翻译员忠实原文完成初译,审校员核对语义、统一术语、润色表达。适合文档、合同、论文等对准确性要求高的文本。',
243
+ usage: '例:「把这段产品介绍翻译成英文:<粘贴文本>」',
244
+ agents: [
245
+ {
246
+ name: '翻译员', icon: '🌐',
247
+ desc: '初译:忠实原文、保留术语、格式对应',
248
+ systemPrompt: '你是专业译者。忠实翻译原文:不增删信息、专有名词查证后翻译并首次出现附原文、保持原文格式(段落/列表/代码块)。译不出把握的句子标注疑问而非硬翻。目标语言按任务要求,默认中英互译。'
249
+ },
250
+ {
251
+ name: '审校员', icon: '⚖️',
252
+ desc: '审校:语义核对、术语统一、表达润色',
253
+ systemPrompt: '你是资深审校。逐句核对译文与原文语义是否一致,检查术语前后统一、数字单位准确、语句地道自然。输出终稿,并附「审校说明」:术语表、主要改动点、存疑处。用中文说明,译文保持目标语言。'
254
+ }
255
+ ]
256
+ }
257
+ ];
258
+
259
+ function getTeamPresets() {
260
+ return TEAM_PRESETS;
261
+ }
262
+
263
+ // ---------- 任务列表 ----------
264
+ // seq 决定执行/显示顺序(拖拽可改);createdAt 仅用于展示时间
265
+ // assign:任务末尾 @智能体 指派(agentId);空 = 管家调度;子智能体 = 独立完成
266
+ function getTasks() {
267
+ const tasks = readJson(TASKS_PATH, []);
268
+ return tasks.slice().sort((a, b) => {
269
+ const sa = a.seq === undefined ? Number.MAX_SAFE_INTEGER : a.seq;
270
+ const sb = b.seq === undefined ? Number.MAX_SAFE_INTEGER : b.seq;
271
+ return sa === sb ? a.createdAt - b.createdAt : sa - sb;
272
+ });
273
+ }
274
+
275
+ function saveTasks(tasks) {
276
+ writeJson(TASKS_PATH, tasks);
277
+ }
278
+
279
+ // 从文本提取任务:
280
+ // mode='sequential'(顺序任务):每行一个任务,仅支持「1.」编号格式,行末可 @智能体 指派
281
+ // mode='scheduled'(定时任务):行首时间 = 定时启动时间,
282
+ // 支持完整日期(20260818-1307 / 2026-08-18 13:07)与当天时刻(13:07),行末可 @智能体
283
+ // runner='solo' 时任务标记为单聊执行(由 opencode 单体完成,不经管家编排)
284
+ // 返回 {tasks, warnings}
285
+ function parseTasksFromText(text, mode, runner, model) {
286
+ mode = mode === 'scheduled' ? 'scheduled' : 'sequential';
287
+ const now = new Date();
288
+ const baseTs = now.getTime();
289
+ const parsed = [];
290
+ const warnings = [];
291
+ const agents = getAgents();
292
+ let seq = 0;
293
+ let fallbackIdx = 0;
294
+
295
+ const validDate = (y, mo, d, h, mi) =>
296
+ mo >= 1 && mo <= 12 && d >= 1 && d <= 31 && h <= 23 && mi <= 59;
297
+
298
+ for (const rawLine of String(text || '').split(/\r?\n/)) {
299
+ let line = rawLine.trim();
300
+ if (!line) continue;
301
+
302
+ // 行末 @指派
303
+ let assign = '';
304
+ const am = line.match(/\s*@([^\s@,。,;;]+)\s*$/);
305
+ if (am) {
306
+ const tok = am[1];
307
+ line = line.slice(0, am.index).trim();
308
+ const ag = agents.find(a => a.id === tok) || agents.find(a => a.name === tok);
309
+ if (ag) assign = ag.id;
310
+ else warnings.push(`@${tok} 不是已配置的智能体,任务「${line.slice(0, 20)}」将改由管家调度`);
311
+ }
312
+
313
+ let createdAt = null;
314
+ let scheduledAt = null;
315
+ let m;
316
+ // 紧凑全日期格式:20260818-1307
317
+ m = line.match(/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})\s+(.*)$/);
318
+ if (m) {
319
+ const [y, mo, d, h, mi] = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5])];
320
+ if (validDate(y, mo, d, h, mi)) {
321
+ createdAt = scheduledAt = new Date(y, mo - 1, d, h, mi, 0, 0).getTime();
322
+ line = m[6].trim();
323
+ } else {
324
+ warnings.push(`「${line.slice(0, 20)}」行首时间格式不合法(应为 20260818-1307),已按普通内容处理`);
325
+ }
326
+ }
327
+ // 完整日期格式:2026-08-18 13:07[:30]
328
+ if (createdAt === null) {
329
+ m = line.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{1,2}):(\d{2})(?::(\d{2}))?\s+(.*)$/);
330
+ if (m) {
331
+ const dt = new Date(`${m[1]}T${String(m[2]).padStart(2, '0')}:${m[3]}:${m[4] || '00'}`);
332
+ if (!isNaN(dt.getTime())) {
333
+ createdAt = scheduledAt = dt.getTime();
334
+ line = m[5].trim();
335
+ }
336
+ }
337
+ }
338
+ // 当天时刻:13:07(顺序模式仅作排序时间;定时模式作为当天定时启动时间)
339
+ if (createdAt === null) {
340
+ m = line.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?\s+(.*)$/);
341
+ if (m) {
342
+ const d = new Date(now);
343
+ d.setHours(Number(m[1]), Number(m[2]), Number(m[3] || 0), 0);
344
+ createdAt = d.getTime();
345
+ if (mode === 'scheduled') scheduledAt = createdAt;
346
+ line = m[4].trim();
347
+ }
348
+ }
349
+
350
+ // 列表符剥离:仅「1. / 1、 / 1)」编号格式(顺序与定时模式一致)
351
+ // 单聊模式编号后空白可省略(1.写xxx 2.-xxx 3.//xxx);群聊保持编号后须有空白
352
+ const soloRun = runner === 'solo';
353
+ line = line.replace(soloRun ? /^(\d+[.、)])\s*/ : /^(\d+[.、)])\s+/, '').trim();
354
+ if (!line) continue;
355
+
356
+ // 单聊执行链前缀(编号剥离后识别):
357
+ // - 接续上一任务的会话继续执行(同一 opencode 进程续聊)
358
+ // // 并行执行:连续多个 // 任务各自独立进程同时跑
359
+ // 无前缀 = 新会话独立执行(默认)
360
+ let link = 'new';
361
+ if (soloRun) {
362
+ if (/^\/\//.test(line)) { link = 'parallel'; line = line.replace(/^\/\/\s*/, ''); }
363
+ else if (/^-/.test(line)) { link = 'continue'; line = line.replace(/^-\s*/, ''); }
364
+ line = line.trim();
365
+ if (!line) continue;
366
+ }
367
+
368
+ if (createdAt === null) {
369
+ createdAt = baseTs + fallbackIdx * 1000;
370
+ fallbackIdx++;
371
+ }
372
+
373
+ const rec = {
374
+ id: `t-${baseTs}-${seq++}`,
375
+ title: line.slice(0, 500),
376
+ notes: '',
377
+ createdAt,
378
+ status: 'pending',
379
+ assign,
380
+ result: '',
381
+ kind: mode,
382
+ runner: runner === 'solo' ? 'solo' : ''
383
+ };
384
+ if (soloRun) rec.link = link; // new=独立新会话 | continue=接续上一任务会话 | parallel=并行独立会话
385
+ if (mode === 'scheduled') rec.scheduledAt = scheduledAt !== null ? scheduledAt : createdAt;
386
+ parsed.push(rec);
387
+ }
388
+ return { tasks: parsed, warnings };
389
+ }
390
+
391
+ function importTasks(text, mode, runner, model) {
392
+ const { tasks: parsed, warnings } = parseTasksFromText(text, mode, runner);
393
+ const tasks = getTasks();
394
+ // 若存在无 seq 的旧任务,先按现有顺序(createdAt)补齐
395
+ let next = 0;
396
+ for (const t of tasks) { if (t.seq === undefined) t.seq = next++; else next = Math.max(next, t.seq + 1); }
397
+ for (const t of parsed) { t.seq = next++; if (runner === 'solo' && model) t.model = String(model); }
398
+ saveTasks(tasks.concat(parsed));
399
+ return { added: parsed.length, warnings };
400
+ }
401
+
402
+ // 拖拽排序:按给定 id 顺序重编 seq(ids 应为全量,未包含的追加在末尾)
403
+ function reorderTasks(ids) {
404
+ const sorted = getTasks();
405
+ const order = [];
406
+ const seen = new Set();
407
+ for (const id of ids) { const t = sorted.find(x => x.id === id); if (t && !seen.has(id)) { order.push(t); seen.add(id); } }
408
+ for (const t of sorted) if (!seen.has(t.id)) order.push(t);
409
+ order.forEach((t, i) => { t.seq = i; });
410
+ saveTasks(order);
411
+ }
412
+
413
+ // 删除任务及其会话消息
414
+ function deleteTask(id) {
415
+ saveTasks(getTasks().filter(t => t.id !== id));
416
+ writeJson(MESSAGES_PATH, readJson(MESSAGES_PATH, []).filter(m => (m.taskId || '') !== id));
417
+ return true;
418
+ }
419
+
420
+ function updateTask(id, patch) {
421
+ const tasks = getTasks();
422
+ const t = tasks.find(x => x.id === id);
423
+ if (!t) return null;
424
+ Object.assign(t, patch);
425
+ saveTasks(tasks);
426
+ return t;
427
+ }
428
+
429
+ // 孤儿状态修复:服务异常退出后仍标记 running 的任务复位为待执行
430
+ // 启动与退出时调用,避免任务永远卡在「执行中」
431
+ function resetRunningTasks() {
432
+ const tasks = getTasks();
433
+ let n = 0;
434
+ for (const t of tasks) {
435
+ if (t.status === 'running') { t.status = 'pending'; t.result = '服务重启,已复位为待执行'; n++; }
436
+ }
437
+ if (n > 0) saveTasks(tasks);
438
+ return n;
439
+ }
440
+
441
+ function getTask(id) {
442
+ return getTasks().find(x => x.id === id) || null;
443
+ }
444
+
445
+ // ---------- 消息 ----------
446
+ // taskId 为空 = 主会话;否则属于对应任务会话(每个任务一个独立会话)
447
+ function getMessages(taskId) {
448
+ const msgs = readJson(MESSAGES_PATH, []);
449
+ if (taskId === undefined) return msgs;
450
+ return msgs.filter(m => (m.taskId || '') === taskId);
451
+ }
452
+
453
+ function addMessage(msg) {
454
+ const msgs = readJson(MESSAGES_PATH, []);
455
+ // 主会话消息记录所属 epoch:新会话开启后,旧 epoch 消息不再传入上下文
456
+ const rec = {
457
+ id: msg.id || `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
458
+ role: msg.role,
459
+ content: msg.content,
460
+ agentId: msg.agentId || '',
461
+ agentName: msg.agentName || '',
462
+ phase: msg.phase || '',
463
+ actor: msg.actor || '',
464
+ taskId: msg.taskId || '',
465
+ plan: msg.plan || undefined,
466
+ outputPath: msg.outputPath || '',
467
+ epoch: msg.taskId ? undefined : (msg.epoch !== undefined ? msg.epoch : (Number(getConfig().mainEpoch) || 0)),
468
+ timestamp: msg.timestamp || new Date().toISOString()
469
+ };
470
+ msgs.push(rec);
471
+ writeJson(MESSAGES_PATH, msgs);
472
+ }
473
+
474
+ function clearMessages() {
475
+ writeJson(MESSAGES_PATH, []);
476
+ }
477
+
478
+ // ---------- 流转日志(智能体之间的派发/交接/返工/验收事件,append-only) ----------
479
+ // 供「🔭 流转」页面绘制泳道时间线:谁在何时把什么信息交给了谁
480
+ const FLOW_PATH = path.join(DATA_DIR, 'flow.jsonl');
481
+
482
+ function addFlowEvent(ev) {
483
+ try {
484
+ ensureDir();
485
+ const rec = {
486
+ t: ev.t || new Date().toISOString(),
487
+ run: String(ev.run || '').slice(0, 60),
488
+ type: String(ev.type || ''), // start|plan|dispatch|done|handoff|rework|verify|finish
489
+ from: String(ev.from || '').slice(0, 40),
490
+ to: String(ev.to || '').slice(0, 40),
491
+ stage: Number(ev.stage) || 0,
492
+ round: Number(ev.round) || 0,
493
+ summary: String(ev.summary || '').slice(0, 400),
494
+ files: Array.isArray(ev.files) ? ev.files.slice(0, 20).map(f => String(f).slice(0, 300)) : [],
495
+ detail: ev.detail && typeof ev.detail === 'object' ? ev.detail : {}
496
+ };
497
+ fs.appendFileSync(FLOW_PATH, JSON.stringify(rec) + '\n', 'utf8');
498
+ return rec;
499
+ } catch { return null; }
500
+ }
501
+
502
+ function readFlowAll() {
503
+ try {
504
+ return fs.readFileSync(FLOW_PATH, 'utf8').split('\n').filter(Boolean).map(l => {
505
+ try { return JSON.parse(l); } catch { return null; }
506
+ }).filter(Boolean);
507
+ } catch { return []; }
508
+ }
509
+
510
+ // 单次编排的全部事件(按时间序)
511
+ function getFlow(runId) {
512
+ return readFlowAll().filter(e => e.run === runId);
513
+ }
514
+
515
+ // 最近编排列表:run id 去重 + 元信息(开始时间/事件数/参与者/是否结束)
516
+ function listFlowRuns(limit) {
517
+ const all = readFlowAll();
518
+ const map = new Map(); // run -> 元信息
519
+ for (const e of all) {
520
+ let r = map.get(e.run);
521
+ if (!r) { r = { run: e.run, start: e.t, end: '', events: 0, agents: new Set(), finished: false, summary: '' }; map.set(e.run, r); }
522
+ r.events++;
523
+ r.end = e.t;
524
+ if (e.from) r.agents.add(e.from);
525
+ if (e.to) r.agents.add(e.to);
526
+ if (e.type === 'finish') { r.finished = true; r.summary = e.summary; }
527
+ if (e.type === 'start' && !r.summary) r.summary = e.summary;
528
+ }
529
+ return [...map.values()]
530
+ .sort((a, b) => (a.start < b.start ? 1 : -1))
531
+ .slice(0, limit || 30)
532
+ .map(r => ({ ...r, agents: [...r.agents] }));
533
+ }
534
+
535
+ // ---------- 单聊工作台(OpenCode)会话 ----------
536
+ // 每个网页会话对应一条记录:ocSessionId 为 opencode 的 ses_xxx(首次运行后回填,-s 续聊)
537
+ // 聊天消息复用 messages.json(taskId = oc 会话 id),导出/清空历史自动生效
538
+ function getOcSessions() {
539
+ const d = readJson(OC_SESSIONS_PATH, null);
540
+ const list = Array.isArray(d && d.sessions) ? d.sessions : [];
541
+ return list
542
+ .filter(s => s && s.id)
543
+ .sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
544
+ }
545
+
546
+ function saveOcSessions(list) {
547
+ writeJson(OC_SESSIONS_PATH, { sessions: list });
548
+ }
549
+
550
+ // 新建或增量更新(patch 覆盖,updatedAt 自动刷新)
551
+ function upsertOcSession(id, patch) {
552
+ const list = getOcSessions();
553
+ let rec = list.find(s => s.id === id);
554
+ if (!rec) {
555
+ rec = { id, title: '', ocSessionId: '', model: '', createdAt: Date.now(), updatedAt: Date.now() };
556
+ list.push(rec);
557
+ }
558
+ Object.assign(rec, patch || {}, { id, updatedAt: Date.now() });
559
+ saveOcSessions(list);
560
+ return rec;
561
+ }
562
+
563
+ function getOcSession(id) {
564
+ return getOcSessions().find(s => s.id === id) || null;
565
+ }
566
+
567
+ function deleteOcSession(id) {
568
+ saveOcSessions(getOcSessions().filter(s => s.id !== id));
569
+ writeJson(MESSAGES_PATH, readJson(MESSAGES_PATH, []).filter(m => (m.taskId || '') !== id));
570
+ }
571
+
572
+ // ---------- 管家长期记忆(跨会话偏好与教训,读写由 memory.js 负责) ----------
573
+ function getMemoryData() {
574
+ const d = readJson(MEMORY_PATH, null);
575
+ return {
576
+ memory: Array.isArray(d && d.memory) ? d.memory.map(s => String(s)).filter(Boolean) : [],
577
+ user: Array.isArray(d && d.user) ? d.user.map(s => String(s)).filter(Boolean) : []
578
+ };
579
+ }
580
+ function saveMemoryData(data) {
581
+ writeJson(MEMORY_PATH, {
582
+ memory: Array.isArray(data.memory) ? data.memory.map(String) : [],
583
+ user: Array.isArray(data.user) ? data.user.map(String) : []
584
+ });
585
+ }
586
+
587
+ // ---------- 历史数据清理(工作产生的动态文件与历史记录,超期自动/手动清理) ----------
588
+ // 清理范围:已完结任务 + 任务消息 + 过期主会话消息 + 过期单聊会话 + 产出存档 outputs/ + 流转日志
589
+ // 保留:未完结/待触发任务、管家记忆(memory 是长期记忆,不属于历史记录)、config
590
+ // 返回 { tasks, messages, ocSessions, outputs, flowEvents } 各项清理计数
591
+ function pruneOldData(days) {
592
+ const cutoff = Date.now() - Math.max(1, Number(days) || 15) * 24 * 3600 * 1000;
593
+ const stat = { tasks: 0, messages: 0, ocSessions: 0, outputs: 0, flowEvents: 0 };
594
+ const tsOf = (m) => { const t = Date.parse(m.timestamp || ''); return Number.isFinite(t) ? t : 0; };
595
+
596
+ // 1. 已完结任务(done/failed);pending/scheduled 一律保留(含未触发的定时任务)
597
+ const tasks = getTasks();
598
+ const keptTasks = tasks.filter(t => {
599
+ const end = Number(t.updatedAt || t.createdAt) || 0;
600
+ const finished = t.status === 'done' || t.status === 'failed';
601
+ if (finished && end && end < cutoff) { stat.tasks++; return false; }
602
+ return true;
603
+ });
604
+ if (stat.tasks) saveTasks(keptTasks);
605
+
606
+ // 2. 单聊会话:updatedAt 超 cutoff → 删(连同其消息)
607
+ const keptSess = getOcSessions().filter(s => {
608
+ const u = Number(s.updatedAt || s.createdAt) || 0;
609
+ if (u && u < cutoff) { stat.ocSessions++; return false; }
610
+ return true;
611
+ });
612
+ if (stat.ocSessions) {
613
+ saveOcSessions(keptSess);
614
+ // 其消息由下方第 3 步统一按孤儿清理并计数(避免重复统计)
615
+ }
616
+
617
+ // 3. 消息:孤儿任务消息(任务已删)+ 过期的主会话/单聊会话消息(timestamp 超 cutoff)
618
+ const validIds = new Set([...keptTasks.map(t => t.id), ...keptSess.map(s => s.id)]);
619
+ const keptMsgs = readJson(MESSAGES_PATH, []).filter(m => {
620
+ const tid = m.taskId || '';
621
+ if (tid && !validIds.has(tid)) { stat.messages++; return false; } // 孤儿消息
622
+ if (!tid && tsOf(m) && tsOf(m) < cutoff) { stat.messages++; return false; } // 过期主会话
623
+ return true;
624
+ });
625
+ const allMsgs = readJson(MESSAGES_PATH, []);
626
+ if (keptMsgs.length !== allMsgs.length) writeJson(MESSAGES_PATH, keptMsgs);
627
+
628
+ // 4. 流转日志:超期事件行过滤重写
629
+ try {
630
+ const lines = fs.readFileSync(FLOW_PATH, 'utf8').split(/\r?\n/).filter(Boolean);
631
+ const kept = lines.filter(l => {
632
+ try { const e = JSON.parse(l); if (Number(e.t || 0) < cutoff) { stat.flowEvents++; return false; } } catch { stat.flowEvents++; return false; }
633
+ return true;
634
+ });
635
+ if (kept.length !== lines.length) fs.writeFileSync(FLOW_PATH, kept.length ? kept.join('\n') + '\n' : '');
636
+ } catch { /* 无文件 */ }
637
+
638
+ // 5. 产出存档目录 outputs/<会话>/:mtime 超 cutoff → 删除
639
+ const outputsDir = path.join(DATA_DIR, 'outputs');
640
+ try {
641
+ for (const name of fs.readdirSync(outputsDir)) {
642
+ const fp = path.join(outputsDir, name);
643
+ try {
644
+ const st = fs.statSync(fp);
645
+ if (st.isDirectory() && st.mtimeMs < cutoff) { fs.rmSync(fp, { recursive: true, force: true }); stat.outputs++; }
646
+ } catch { /* 单项失败跳过 */ }
647
+ }
648
+ } catch { /* 无目录 */ }
649
+
650
+ return stat;
651
+ }
652
+
653
+ module.exports = {
654
+ DATA_DIR,
655
+ BUTLER,
656
+ getConfig,
657
+ saveConfig,
658
+ saveAgents,
659
+ getSchedEnabled,
660
+ setSchedEnabled,
661
+ getMemoryData,
662
+ saveMemoryData,
663
+ getAgents,
664
+ getTeamPresets,
665
+ getTasks,
666
+ saveTasks,
667
+ importTasks,
668
+ reorderTasks,
669
+ deleteTask,
670
+ updateTask,
671
+ resetRunningTasks,
672
+ getTask,
673
+ parseTasksFromText,
674
+ getMessages,
675
+ addMessage,
676
+ clearMessages,
677
+ addFlowEvent,
678
+ getFlow,
679
+ listFlowRuns,
680
+ getOcSessions,
681
+ saveOcSessions,
682
+ upsertOcSession,
683
+ getOcSession,
684
+ deleteOcSession,
685
+ pruneOldData
686
+ };