@iamsamyiok/agents-chat 3.31.0 → 3.32.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/app/lib/agent.js CHANGED
@@ -349,10 +349,13 @@ function runAgent(agent, prompt, onChunk, scope) {
349
349
  }
350
350
 
351
351
  // 智能体工作目录(全局统一):
352
- // - 环境变量 AGENTS_CHAT_CWD 优先(卡牌 workspace 等单次执行场景注入)
352
+ // - 任务隔离上下文优先(worktree 任务执行期间注入,见 lib/worktree.js runWithTaskCwd)
353
+ // - 环境变量 AGENTS_CHAT_CWD 次之(卡牌 workspace 等单次执行场景注入)
353
354
  // - 配置了有效 globalCwd → 所有智能体共用该目录(文件资料集中在一处)
354
355
  // - 否则默认 <数据目录>/workspace/ 共享目录
355
356
  function resolveCwd() {
357
+ const wtCwd = require('./worktree').currentTaskCwd();
358
+ if (wtCwd) return wtCwd;
356
359
  const envCwd = String(process.env.AGENTS_CHAT_CWD || '').trim();
357
360
  if (envCwd) {
358
361
  try { if (fs.existsSync(envCwd) && fs.statSync(envCwd).isDirectory()) return envCwd; } catch { /* ignore */ }
package/app/lib/oc.js CHANGED
@@ -193,9 +193,16 @@ function chatSolo(runnerKind, runner, opts, onEvent) {
193
193
  const ocSessionId = String(opts.ocSessionId || '');
194
194
  // 进程归属 scope:停止编排(stopScope('solo'))只杀编排任务,追加聊天用独立 scope 免受牵连
195
195
  const scope = String(opts.scope || 'solo');
196
- // 工作区:指定后 Agent 在该目录读写文件(卡牌可选 workspace
197
- const cwd = opts.cwd && fs.existsSync(opts.cwd) && fs.statSync(opts.cwd).isDirectory() ? opts.cwd : '';
198
- const cwdEnv = cwd ? { ...process.env, AGENTS_CHAT_CWD: cwd } : process.env;
196
+ // 工作区:指定后 Agent 在该目录读写文件(卡牌可选 workspace;任务隔离 worktree 经 ALS 注入)
197
+ const alsCwd = require('./worktree').currentTaskCwd();
198
+ const cwdInput = opts.cwd || alsCwd;
199
+ const cwd = cwdInput && fs.existsSync(cwdInput) && fs.statSync(cwdInput).isDirectory() ? cwdInput : '';
200
+ // 演示模式按调用场景选择 mock 行为(solo-task 会真实写产出文件,供隔离区 diff 演示)
201
+ const cwdEnv = {
202
+ ...process.env,
203
+ ...(cwd ? { AGENTS_CHAT_CWD: cwd } : {}),
204
+ ...(runnerKind === 'demo' ? { MOCK_BEHAVIOR: String(opts.behavior || 'echo') } : {})
205
+ };
199
206
 
200
207
  // ---- 演示模式:mock 子进程 + 快照模拟 ----
201
208
  if (runnerKind === 'demo') {
@@ -1076,32 +1076,37 @@ async function runTasks(tasks, butler, subAgents, opts, emit, onMessage, onTaskS
1076
1076
  emit({ type: 'notice', content: '已手动停止,剩余任务保持待执行状态' });
1077
1077
  break;
1078
1078
  }
1079
- const prompt = `请完成以下任务并给出结果:\n${task.title}${task.notes ? `\n\n补充说明:${task.notes}` : ''}`;
1080
- // 先建历史背景(不含本任务的起始消息),再写入任务会话首条消息
1081
- const history = opts.getHistory ? opts.getHistory(task.id) : '';
1082
- if (onTaskStart) onTaskStart(task);
1083
- // 该任务产生的全部消息都归入对应任务会话
1084
- const persistTask = (m) => onMessage({ ...m, taskId: task.id });
1085
-
1086
- const assigned = opts.resolveAssign ? opts.resolveAssign(task) : null;
1087
- let r;
1088
- if (assigned && assigned.id !== butler.id) {
1089
- // 指派子智能体:独立完成,无管家编排
1090
- emit({ type: 'task_start', taskId: task.id, title: task.title, agentName: assigned.name });
1091
- emit({ type: 'notice', content: `本任务由 @${assigned.name} 独立完成(无管家调度)`, taskId: task.id });
1092
- r = await runMentioned([assigned], prompt, { taskId: task.id, history, scope: opts.scope, isStopped }, emit, persistTask);
1093
- } else {
1094
- emit({ type: 'task_start', taskId: task.id, title: task.title, agentName: butler.name });
1095
- r = await runButler(butler, subAgents, prompt, { taskId: task.id, history, scope: opts.scope, isStopped }, emit, persistTask);
1096
- }
1097
- let status = r.ok ? 'done' : 'failed';
1098
- let resultText = (r.finalText || '').trim() || '执行失败';
1099
- if (isStopped() && !r.ok) {
1100
- status = 'pending'; // 手动停止的任务回到待执行,可随时重跑
1101
- resultText = '已手动停止,可重新执行';
1102
- }
1103
- onTaskDone(task.id, { status, result: resultText.slice(0, 10000) });
1104
- emit({ type: 'task_done', taskId: task.id, status, title: task.title });
1079
+ // 任务隔离 worktree:opts.taskCwd(task) 返回隔离目录(空 = 共享目录),整任务执行期间注入 cwd 上下文
1080
+ const taskCwd = opts.taskCwd ? String(opts.taskCwd(task) || '') : '';
1081
+ if (taskCwd) emit({ type: 'notice', content: `🌿 本任务在 Git 隔离区执行:${taskCwd}`, taskId: task.id });
1082
+ await require('./worktree').runWithTaskCwd(taskCwd, async () => {
1083
+ const prompt = `请完成以下任务并给出结果:\n${task.title}${task.notes ? `\n\n补充说明:${task.notes}` : ''}`;
1084
+ // 先建历史背景(不含本任务的起始消息),再写入任务会话首条消息
1085
+ const history = opts.getHistory ? opts.getHistory(task.id) : '';
1086
+ if (onTaskStart) onTaskStart(task);
1087
+ // 该任务产生的全部消息都归入对应任务会话
1088
+ const persistTask = (m) => onMessage({ ...m, taskId: task.id });
1089
+
1090
+ const assigned = opts.resolveAssign ? opts.resolveAssign(task) : null;
1091
+ let r;
1092
+ if (assigned && assigned.id !== butler.id) {
1093
+ // 指派子智能体:独立完成,无管家编排
1094
+ emit({ type: 'task_start', taskId: task.id, title: task.title, agentName: assigned.name });
1095
+ emit({ type: 'notice', content: `本任务由 @${assigned.name} 独立完成(无管家调度)`, taskId: task.id });
1096
+ r = await runMentioned([assigned], prompt, { taskId: task.id, history, scope: opts.scope, isStopped }, emit, persistTask);
1097
+ } else {
1098
+ emit({ type: 'task_start', taskId: task.id, title: task.title, agentName: butler.name });
1099
+ r = await runButler(butler, subAgents, prompt, { taskId: task.id, history, scope: opts.scope, isStopped }, emit, persistTask);
1100
+ }
1101
+ let status = r.ok ? 'done' : 'failed';
1102
+ let resultText = (r.finalText || '').trim() || '执行失败';
1103
+ if (isStopped() && !r.ok) {
1104
+ status = 'pending'; // 手动停止的任务回到待执行,可随时重跑
1105
+ resultText = '已手动停止,可重新执行';
1106
+ }
1107
+ onTaskDone(task.id, { status, result: resultText.slice(0, 10000) });
1108
+ emit({ type: 'task_done', taskId: task.id, status, title: task.title });
1109
+ });
1105
1110
  }
1106
1111
  emit({ type: 'all_done' });
1107
1112
  }
package/app/lib/store.js CHANGED
@@ -397,7 +397,7 @@ function importTasks(text, mode, runner, model) {
397
397
  for (const t of tasks) { if (t.seq === undefined) t.seq = next++; else next = Math.max(next, t.seq + 1); }
398
398
  for (const t of parsed) { t.seq = next++; if (runner === 'solo' && model) t.model = String(model); }
399
399
  saveTasks(tasks.concat(parsed));
400
- return { added: parsed.length, warnings };
400
+ return { added: parsed.length, warnings, addedTasks: parsed };
401
401
  }
402
402
 
403
403
  // 拖拽排序:按给定 id 顺序重编 seq(ids 应为全量,未包含的追加在末尾)
@@ -0,0 +1,192 @@
1
+ // Git worktree 任务隔离:
2
+ // 创建任务时可选在独立 git worktree 中执行(每任务独立分支+目录,互不污染),
3
+ // 完成后可查看改动 diff、合并回主工作目录或整体丢弃。
4
+ // 工作目录(resolveCwd())不是 git 仓库时功能自动降级(任务照常在共享目录执行)。
5
+ //
6
+ // cwd 传递:AsyncLocalStorage 按任务执行上下文注入(agent.js/oc.js 的 resolveCwd 优先读取),
7
+ // 与并发的聊天请求互不干扰(聊天不在该 async 上下文内)。
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { execFile } = require('child_process');
11
+ const { AsyncLocalStorage } = require('node:async_hooks');
12
+ const store = require('./store');
13
+
14
+ const WT_ROOT = path.join(store.DATA_DIR, 'worktrees');
15
+ const BRANCH_PREFIX = 'ac/';
16
+ const DIFF_MAX_FILES = 50; // diff 视图最多展示文件数
17
+ const DIFF_MAX_PATCH = 20 * 1024; // 单文件 patch 截断
18
+ const DIFF_MAX_TOTAL = 200 * 1024; // 总 patch 截断
19
+
20
+ // ---------- 任务级 cwd 注入(AsyncLocalStorage) ----------
21
+ const taskCwdStorage = new AsyncLocalStorage();
22
+ function runWithTaskCwd(cwd, fn) {
23
+ return cwd ? taskCwdStorage.run({ cwd }, fn) : fn();
24
+ }
25
+ function currentTaskCwd() {
26
+ const s = taskCwdStorage.getStore();
27
+ if (!s || !s.cwd) return '';
28
+ try { if (fs.existsSync(s.cwd) && fs.statSync(s.cwd).isDirectory()) return s.cwd; } catch { /* ignore */ }
29
+ return '';
30
+ }
31
+
32
+ // ---------- git 基础 ----------
33
+ function git(args, opts = {}) {
34
+ return new Promise((resolve) => {
35
+ execFile('git', args, {
36
+ cwd: opts.cwd,
37
+ timeout: opts.timeout || 30000,
38
+ maxBuffer: 8 * 1024 * 1024,
39
+ windowsHide: true
40
+ }, (err, so, se) => {
41
+ resolve({ code: err ? (err.code === undefined ? 1 : err.code) : 0, so: String(so || ''), se: String(se || '') });
42
+ });
43
+ });
44
+ }
45
+
46
+ // 工作目录是否为可用 git 仓库(有至少一个提交)
47
+ async function isGitRepo(dir) {
48
+ if (!dir) return false;
49
+ const r = await git(['rev-parse', '--is-inside-work-tree'], { cwd: dir });
50
+ if (String(r.so).trim() !== 'true') return false;
51
+ const h = await git(['rev-parse', 'HEAD'], { cwd: dir });
52
+ return h.code === 0 && h.so.trim() !== '';
53
+ }
54
+
55
+ function wtDirOf(taskId) { return path.join(WT_ROOT, String(taskId).replace(/[^\w-]/g, '')); }
56
+ function branchOf(taskId) { return BRANCH_PREFIX + String(taskId).replace(/[^\w-]/g, ''); }
57
+
58
+ // ---------- 创建 / 清理 ----------
59
+ // 为任务创建隔离 worktree;返回 { dir, branch, base } 或 null(非 git 仓库 / git 不可用 / 目录残留等)
60
+ async function createForTask(taskId) {
61
+ try {
62
+ const { resolveCwd } = require('./agent');
63
+ const baseCwd = resolveCwd();
64
+ if (!(await isGitRepo(baseCwd))) return null;
65
+ const head = await git(['rev-parse', 'HEAD'], { cwd: baseCwd });
66
+ const base = head.so.trim();
67
+ if (!base) return null;
68
+ const dir = wtDirOf(taskId);
69
+ const branch = branchOf(taskId);
70
+ // 残留清理(上次异常退出可能留下同名 worktree/分支)
71
+ await git(['worktree', 'remove', '--force', dir], { cwd: baseCwd });
72
+ await git(['branch', '-D', branch], { cwd: baseCwd });
73
+ const r = await git(['worktree', 'add', '-b', branch, dir, 'HEAD'], { cwd: baseCwd });
74
+ if (r.code !== 0) return null;
75
+ return { dir, branch, base, createdAt: Date.now() };
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ // 从 worktree 目录反查主仓库根目录
82
+ async function findMainRepo(dir) {
83
+ const g = await git(['rev-parse', '--git-common-dir'], { cwd: dir });
84
+ if (g.code !== 0) return '';
85
+ const common = g.so.trim();
86
+ if (!common) return '';
87
+ let main = path.resolve(dir, common.replace(/[/\\]\.git$/, ''));
88
+ try { if (!fs.statSync(main).isDirectory()) return ''; } catch { return ''; }
89
+ return main;
90
+ }
91
+
92
+ // 删除 worktree 与对应分支;返回 { ok, error }
93
+ async function removeForTask(worktree, { keepBranch = false } = {}) {
94
+ if (!worktree || !worktree.dir) return { ok: false, error: '无隔离区信息' };
95
+ let mainRepo = worktree.mainRepo || '';
96
+ if (!mainRepo && fs.existsSync(worktree.dir)) mainRepo = await findMainRepo(worktree.dir);
97
+ const r1 = await git(['worktree', 'remove', '--force', worktree.dir], { cwd: mainRepo || undefined });
98
+ let r2 = { code: 0 };
99
+ if (!keepBranch && worktree.branch) r2 = await git(['branch', '-D', worktree.branch], { cwd: mainRepo || undefined });
100
+ const ok = r1.code === 0; // 分支删除失败不阻塞(可能已被合并删除)
101
+ return { ok, error: ok ? '' : String((r1.se || r1.so || '').split('\n')[0] || '清理失败').slice(0, 200) };
102
+ }
103
+
104
+ // ---------- 改动查看 ----------
105
+ // 统计与明细:base(创建时 HEAD)→ 当前工作区(含未提交与未跟踪)
106
+ async function diff(worktree, { withPatch = true } = {}) {
107
+ if (!worktree || !worktree.dir) throw new Error('无隔离区信息');
108
+ const dir = worktree.dir;
109
+ const base = worktree.base || 'HEAD';
110
+ // add -A 仅为了让未跟踪文件进入 diff(不动分支指针)
111
+ await git(['add', '-A'], { cwd: dir });
112
+ const st = await git(['diff', '--numstat', base, '--'], { cwd: dir });
113
+ const stat = [];
114
+ for (const line of st.so.split('\n')) {
115
+ if (!line.trim()) continue;
116
+ const parts = line.split('\t');
117
+ if (parts.length < 3) continue;
118
+ stat.push({ file: parts[2], add: parts[0] === '-' ? null : Number(parts[0]), del: parts[1] === '-' ? null : Number(parts[1]) });
119
+ if (stat.length >= DIFF_MAX_FILES) break;
120
+ }
121
+ const out = { base, branch: worktree.branch, dir, stat, files: [] };
122
+ if (!withPatch || !stat.length) return out;
123
+ let total = 0;
124
+ for (const s of stat) {
125
+ if (total >= DIFF_MAX_TOTAL) { out.truncated = true; break; }
126
+ const p = await git(['diff', base, '--', s.file], { cwd: dir });
127
+ let patch = p.so;
128
+ if (patch.length > DIFF_MAX_PATCH) { patch = patch.slice(0, DIFF_MAX_PATCH) + '\n…(已截断)'; out.truncated = true; }
129
+ total += patch.length;
130
+ out.files.push({ path: s.file, patch });
131
+ }
132
+ return out;
133
+ }
134
+
135
+ // ---------- 合并回主工作目录 ----------
136
+ // worktree 内未提交改动先提交,再在主仓库 merge 该分支
137
+ async function mergeToMain(worktree, title) {
138
+ if (!worktree || !worktree.dir) throw new Error('无隔离区信息');
139
+ const dir = worktree.dir;
140
+ // 找主仓库:worktree 里 git rev-parse --git-common-dir 指向主 .git
141
+ const mainRepo = await findMainRepo(dir);
142
+ // 1. worktree 内提交全部改动
143
+ const hasChange = await git(['status', '--porcelain'], { cwd: dir });
144
+ if (hasChange.code === 0 && hasChange.so.trim()) {
145
+ await git(['add', '-A'], { cwd: dir });
146
+ const cm = await git(['commit', '-m', `agents-chat: ${String(title || '任务').slice(0, 60)}`], { cwd: dir });
147
+ if (cm.code !== 0) return { ok: false, error: `隔离区提交失败:${String((cm.se || cm.so).split('\n')[0]).slice(0, 200)}` };
148
+ }
149
+ // 2. 主仓库合并(--no-edit 保留默认合并信息;无改动则 nothing to commit)
150
+ const mr = await git(['merge', '--no-edit', worktree.branch], { cwd: mainRepo || undefined });
151
+ const mOut = String((mr.so || '') + (mr.se || '')).trim();
152
+ if (/Already up to date|已是最新|Nothing to merge/i.test(mOut)) return { ok: true, already: true };
153
+ if (mr.code !== 0) {
154
+ // 合并冲突:回滚合并状态,让用户在隔离区自行处理后再试
155
+ await git(['merge', '--abort'], { cwd: mainRepo || undefined });
156
+ return { ok: false, error: `合并冲突(已还原主目录):${mOut.split('\n')[0].slice(0, 200)}` };
157
+ }
158
+ return { ok: true };
159
+ }
160
+
161
+ // ---------- 数据治理 ----------
162
+ // 清理无主(任务已删)或完结超期的 worktree;liveTasks: 当前任务数组
163
+ async function pruneStale(days, liveTasks) {
164
+ const stat = { worktrees: 0 };
165
+ const cutoff = Date.now() - Math.max(1, Number(days) || 15) * 24 * 3600 * 1000;
166
+ const live = new Map((liveTasks || []).filter(t => t.worktree).map(t => [wtDirOf(t.id), t]));
167
+ let names = [];
168
+ try { names = fs.readdirSync(WT_ROOT); } catch { return stat; }
169
+ for (const name of names) {
170
+ const dir = path.join(WT_ROOT, name);
171
+ try {
172
+ if (!fs.statSync(dir).isDirectory()) continue;
173
+ const task = live.get(dir);
174
+ const w = task ? task.worktree : { dir, branch: branchOf(name), mainRepo: '' };
175
+ let stale = !task;
176
+ if (task) {
177
+ const end = Number(task.updatedAt || task.createdAt) || 0;
178
+ stale = (task.status === 'done' || task.status === 'failed') && end && end < cutoff;
179
+ }
180
+ if (!stale) continue;
181
+ const r = await removeForTask(w);
182
+ if (r.ok) stat.worktrees++;
183
+ } catch { /* 单项失败跳过 */ }
184
+ }
185
+ return stat;
186
+ }
187
+
188
+ module.exports = {
189
+ WT_ROOT, DIFF_MAX_FILES,
190
+ runWithTaskCwd, currentTaskCwd,
191
+ isGitRepo, createForTask, removeForTask, diff, mergeToMain, pruneStale
192
+ };
@@ -33,10 +33,15 @@ if (behavior === 'solo-chat') {
33
33
  `已处理完成 ${DEMO_TAG}`
34
34
  ]);
35
35
  } else if (behavior === 'solo-task') {
36
- // 单聊任务执行演示:输出任务结果(作为 result 持久化)
36
+ // 单聊任务执行演示:在工作目录真实写入一个成果文件(供 Git 隔离 diff 演示),再流式输出任务结果
37
+ try {
38
+ const fs = require('fs');
39
+ const path = require('path');
40
+ fs.writeFileSync(path.join(process.cwd(), 'demo-solo-output.md'), '# 单聊任务演示产出\n\n演示模式下写入工作目录的成果文件。\n');
41
+ } catch { /* 只读目录时跳过 */ }
37
42
  streamOutput([
38
43
  `[单聊任务] 开始处理:${prompt.slice(0, 120)}`,
39
- `任务完成:产出模拟结果 ${DEMO_TAG}`
44
+ `任务完成:产出模拟结果(已写入 demo-solo-output.md) ${DEMO_TAG}`
40
45
  ]);
41
46
  } else if (behavior === 'echo') {
42
47
  if (prompt.includes('【委派背景】')) {
@@ -215,14 +215,14 @@
215
215
  .at-tx i { font-style: normal; color: #999; font-size: 11px; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 380px; }
216
216
 
217
217
  /* ---------- 弹窗 ---------- */
218
- #modalMask, #batchMask, #agentsMask, #historyMask, #helpMask, #flowMask, #cardMask, #teamsMask, #memMask { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: none; align-items: center; justify-content: center; z-index: 51; }
218
+ #modalMask, #batchMask, #agentsMask, #historyMask, #helpMask, #flowMask, #cardMask, #teamsMask, #memMask, #diffMask { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: none; align-items: center; justify-content: center; z-index: 51; }
219
219
  /* 弹窗出现动画:遮罩淡入(含毛玻璃)+ 内容上浮,全站弹窗统一节奏 */
220
220
  div[id$="Mask"] { backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px); }
221
221
  div[id$="Mask"].show { animation: fade-in .18s ease; }
222
222
  .modal, .preview-card { animation: modal-in .24s cubic-bezier(.2,.7,.3,1); }
223
223
  @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
224
224
  @keyframes modal-in { from { opacity: 0; transform: translateY(14px) scale(.985); } to { opacity: 1; transform: none; } }
225
- #modalMask.show, #batchMask.show, #agentsMask.show, #historyMask.show, #helpMask.show, #flowMask.show, #cardMask.show, #teamsMask.show, #memMask.show { display: flex; }
225
+ #modalMask.show, #batchMask.show, #agentsMask.show, #historyMask.show, #helpMask.show, #flowMask.show, #cardMask.show, #teamsMask.show, #memMask.show, #diffMask.show { display: flex; }
226
226
  /* 团队仓库卡片 */
227
227
  .team-card { border: 1px solid #e5e7eb; border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; }
228
228
  .team-card .tc-head { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
@@ -261,6 +261,12 @@
261
261
  .flow-detail .fd-title { font-weight: 600; margin-bottom: 6px; }
262
262
  .flow-detail .fd-files { color: #7d5ac8; }
263
263
  .modal { background: #fff; border-radius: 10px; width: 560px; max-width: 92vw; max-height: 88vh; overflow-y: auto; padding: 20px; }
264
+ /* Git 改动视图:行级着色 */
265
+ .diff-pre { margin: 6px 0 0; padding: 8px; background: #fff; border-radius: 6px; font-size: 12px; line-height: 1.55; overflow-x: auto; white-space: pre; font-family: ui-monospace, Menlo, Consolas, monospace; }
266
+ .diff-pre .dl { padding: 0 4px; border-radius: 3px; }
267
+ .diff-pre .dl.add { background: #e6ffec; color: #116329; }
268
+ .diff-pre .dl.del { background: #ffebe9; color: #82071e; }
269
+ .diff-pre .dl.hun { color: #0550ae; background: #ddf4ff; }
264
270
  .modal h3 { font-size: 15px; margin-bottom: 6px; }
265
271
  .modal .tip { font-size: 12px; color: #999; line-height: 1.7; margin-bottom: 10px; }
266
272
  .modal textarea { width: 100%; height: 200px; border: 1px solid #ddd; border-radius: 6px; padding: 10px; font-size: 13px; resize: vertical; outline: none; }
@@ -510,6 +516,20 @@
510
516
  </div>
511
517
  </div>
512
518
 
519
+ <!-- 任务 Git 改动弹窗(隔离区 diff / 合并 / 丢弃) -->
520
+ <div id="diffMask">
521
+ <div class="modal" style="max-width:860px">
522
+ <h3 id="diffTitle">🌿 任务改动</h3>
523
+ <div class="tip" id="diffMeta"></div>
524
+ <div id="diffBody" style="max-height:52vh;overflow:auto;border:1px solid #d8dee9;border-radius:8px;padding:8px;background:#fafbfc"></div>
525
+ <div class="modal-actions">
526
+ <button class="mini-btn danger" onclick="discardTaskDiff()">🗑 丢弃隔离区</button>
527
+ <button class="primary" onclick="mergeTaskDiff()">合并到主目录</button>
528
+ <button onclick="closeTaskDiff()">关闭</button>
529
+ </div>
530
+ </div>
531
+ </div>
532
+
513
533
  <!-- 导入任务弹窗 -->
514
534
  <div id="modalMask">
515
535
  <div class="modal">
@@ -523,6 +543,10 @@
523
543
  <textarea id="importText" onkeydown="importKeydown(event)"></textarea>
524
544
  <div id="importAtMenu"></div>
525
545
  </div>
546
+ <div class="ag-row" style="margin:6px 0 2px;align-items:center;gap:6px">
547
+ <input type="checkbox" id="importIsolated" style="width:auto">
548
+ <label for="importIsolated" style="flex:1;cursor:pointer">🌿 Git 隔离执行:每个任务在独立 Git worktree 分支中执行,互不污染;完成后可查看改动、合并回主目录或整体丢弃(统一工作目录需是 Git 仓库)</label>
549
+ </div>
526
550
  <div class="modal-actions">
527
551
  <button onclick="closeImport()">取消</button>
528
552
  <button class="primary" onclick="doImport()">提取任务</button>
@@ -696,7 +720,7 @@
696
720
  </div>
697
721
 
698
722
  <script>
699
- const PAGE_VERSION = '3.31.0'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
723
+ const PAGE_VERSION = '3.32.0'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
700
724
  const AV_COLORS = ['#5b8def','#07c160','#fa9d3b','#10aeff','#8a6fe8','#fa5151','#e8a33d','#3aa7a3'];
701
725
  const PHASE_LABEL = { plan: '调度规划', work: '执行', review: '验收', report: '汇总', talk: '圆桌发言', task: '任务执行' };
702
726
  // 职业图标库(智能体配置可选)
@@ -1402,6 +1426,15 @@ function linkBadge(t) {
1402
1426
  ? '<span style="color:#8a6fe8;font-size:11px;margin-right:4px" title="在上一任务的会话中继续执行(同进程续聊)">↪ 续聊</span>'
1403
1427
  : '<span style="color:#0a742e;font-size:11px;margin-right:4px" title="与相邻并行任务同时执行(独立进程)">⚡ 并行</span>';
1404
1428
  }
1429
+ // Git 隔离徽标与改动按钮
1430
+ function wtBadge(t) {
1431
+ return t.worktree ? '<span style="color:#0a742e;font-size:11px;margin-right:4px" title="Git 隔离执行(独立 worktree 分支)">🌿</span>' : '';
1432
+ }
1433
+ function wtBtn(t) {
1434
+ return (t.worktree && (t.status === 'done' || t.status === 'failed'))
1435
+ ? `<button class="mini-btn" title="查看本任务在隔离区的 Git 改动(可合并/丢弃)" onclick="event.stopPropagation();openTaskDiff('${escAttr(t.id)}')">改动</button>`
1436
+ : '';
1437
+ }
1405
1438
  function renderTasks() {
1406
1439
  const el = document.getElementById('taskList');
1407
1440
  const badge = document.getElementById('pendingBadge');
@@ -1424,10 +1457,11 @@ function renderTasks() {
1424
1457
  <div class="task-avatar st-${t.status === 'pending' ? 'scheduled' : t.status}">${icon}</div>
1425
1458
  <div class="task-main">
1426
1459
  <div class="task-title">${esc(t.title)}</div>
1427
- <div class="task-sub"><span>${linkBadge(t)}${st}</span></div>
1460
+ <div class="task-sub"><span>${linkBadge(t)}${wtBadge(t)}${st}</span></div>
1428
1461
  </div>
1429
1462
  ${t.status === 'pending' ? `<button class="mini-btn" title="不再等到点,立即执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">立即执行</button>` : ''}
1430
1463
  ${t.status === 'failed' ? `<button class="mini-btn" title="重新执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">重跑</button>` : ''}
1464
+ ${wtBtn(t)}
1431
1465
  <button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
1432
1466
  </div>`;
1433
1467
  }).join('') : '<div class="task-empty">暂无定时任务<br>点上方「导入任务」→「定时任务」标签添加</div>';
@@ -1443,9 +1477,10 @@ function renderTasks() {
1443
1477
  <div class="task-avatar st-${t.status}">${icon}</div>
1444
1478
  <div class="task-main">
1445
1479
  <div class="task-title">${esc(t.title)}</div>
1446
- <div class="task-sub"><span>${linkBadge(t)}${statusText(t.status)}</span><span>${fmtTime(t.createdAt)}</span></div>
1480
+ <div class="task-sub"><span>${linkBadge(t)}${wtBadge(t)}${statusText(t.status)}</span><span>${fmtTime(t.createdAt)}</span></div>
1447
1481
  </div>
1448
1482
  ${t.status === 'pending' || t.status === 'failed' ? `<button class="mini-btn" onclick="event.stopPropagation();runTask('${escAttr(t.id)}')">执行</button>` : ''}
1483
+ ${wtBtn(t)}
1449
1484
  <button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
1450
1485
  </div>`;
1451
1486
  }).join('') : '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>(单聊任务由 OpenCode 单体执行)</div>';
@@ -1475,10 +1510,11 @@ function renderTasks() {
1475
1510
  <div class="task-avatar st-${t.status === 'pending' ? 'scheduled' : t.status}">${icon}</div>
1476
1511
  <div class="task-main">
1477
1512
  <div class="task-title">${esc(t.title)}</div>
1478
- <div class="task-sub"><span>${st}${assign}</span></div>
1513
+ <div class="task-sub"><span>${wtBadge(t)}${st}${assign}</span></div>
1479
1514
  </div>
1480
1515
  ${t.status === 'pending' ? `<button class="mini-btn" title="不再等到点,立即执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">立即执行</button>` : ''}
1481
1516
  ${t.status === 'failed' ? `<button class="mini-btn" title="重新执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">重跑</button>` : ''}
1517
+ ${wtBtn(t)}
1482
1518
  <button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
1483
1519
  </div>`;
1484
1520
  }).join('');
@@ -1505,9 +1541,10 @@ function renderTasks() {
1505
1541
  <div class="task-avatar st-${t.status}">${icon}</div>
1506
1542
  <div class="task-main">
1507
1543
  <div class="task-title">${esc(t.title)}</div>
1508
- <div class="task-sub"><span>${statusText(t.status)}${assign}</span><span>${fmtTime(t.createdAt)}</span></div>
1544
+ <div class="task-sub"><span>${wtBadge(t)}${statusText(t.status)}${assign}</span><span>${fmtTime(t.createdAt)}</span></div>
1509
1545
  </div>
1510
1546
  ${t.status === 'pending' || t.status === 'failed' ? `<button class="mini-btn" onclick="event.stopPropagation();runTask('${escAttr(t.id)}')">执行</button>` : ''}
1547
+ ${wtBtn(t)}
1511
1548
  <button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
1512
1549
  </div>`;
1513
1550
  }).join('');
@@ -2571,11 +2608,17 @@ async function doImport() {
2571
2608
  // 单聊导入时记录当前所选模型,定时触发时用它执行(页面关闭也不丢)
2572
2609
  const data = await api('/api/tasks/import', {
2573
2610
  method: 'POST', headers: { 'Content-Type': 'application/json' },
2574
- body: JSON.stringify({ text, mode: importMode, runner: chatMode === 'solo' ? 'solo' : '', model: chatMode === 'solo' ? (curOcModel || '') : '' })
2611
+ body: JSON.stringify({
2612
+ text, mode: importMode, runner: chatMode === 'solo' ? 'solo' : '', model: chatMode === 'solo' ? (curOcModel || '') : '',
2613
+ isolated: !!(document.getElementById('importIsolated') || {}).checked
2614
+ })
2575
2615
  });
2576
2616
  if (data.success) {
2577
2617
  tasks = data.tasks || [];
2578
2618
  renderSidebar();
2619
+ if (data.isolated > 0) {
2620
+ (chatMode === 'solo' ? addSoloSys : addSys)(`🌿 ${data.isolated} 个任务将在 Git 隔离区执行(独立分支,互不污染)`);
2621
+ }
2579
2622
  if (chatMode === 'solo') {
2580
2623
  addSoloSys(importMode === 'scheduled'
2581
2624
  ? `已添加 ${data.added} 个单聊定时任务(到点由 OpenCode 自动执行)`
@@ -2591,6 +2634,65 @@ async function doImport() {
2591
2634
  } else toast(data.error || '导入失败');
2592
2635
  }
2593
2636
 
2637
+ // ---------- Git 隔离区改动查看 / 合并 / 丢弃 ----------
2638
+ let diffTask = null; // 当前弹窗对应的任务
2639
+ function escDiffLine(l) {
2640
+ const e = esc(l);
2641
+ if (l.startsWith('+')) return `<div class="dl add">${e}</div>`;
2642
+ if (l.startsWith('-')) return `<div class="dl del">${e}</div>`;
2643
+ if (l.startsWith('@@')) return `<div class="dl hun">${e}</div>`;
2644
+ return `<div class="dl">${e}</div>`;
2645
+ }
2646
+ async function openTaskDiff(id) {
2647
+ const t = tasks.find(x => x.id === id);
2648
+ if (!t) return;
2649
+ document.getElementById('diffTitle').textContent = `🌿 改动:${t.title.slice(0, 30)}`;
2650
+ document.getElementById('diffMeta').textContent = '加载中…';
2651
+ document.getElementById('diffBody').innerHTML = '';
2652
+ document.getElementById('diffMask').classList.add('show');
2653
+ diffTask = t;
2654
+ const data = await api('/api/tasks/diff?id=' + encodeURIComponent(id));
2655
+ if (!data.success) { document.getElementById('diffMeta').textContent = data.error || '加载失败'; return; }
2656
+ const d = data.diff;
2657
+ const addAll = d.stat.reduce((a, s) => a + (s.add || 0), 0);
2658
+ const delAll = d.stat.reduce((a, s) => a + (s.del || 0), 0);
2659
+ const merged = t.mergedAt ? '(已合并过)' : '';
2660
+ document.getElementById('diffMeta').innerHTML =
2661
+ `分支 ${esc(d.branch)}${merged} · ${d.stat.length} 个文件 · <span style="color:#0a742e">+${addAll}</span> / <span style="color:#c0392b">-${delAll}</span>${d.truncated ? '(内容过长已截断)' : ''}`;
2662
+ if (!d.stat.length) {
2663
+ document.getElementById('diffBody').innerHTML = '<div style="color:#666;padding:12px">隔离区没有任何改动</div>';
2664
+ return;
2665
+ }
2666
+ const byFile = Object.fromEntries((d.files || []).map(f => [f.path, f.patch]));
2667
+ document.getElementById('diffBody').innerHTML = d.stat.map(s => `
2668
+ <details ${d.stat.length <= 5 ? 'open' : ''} style="margin-bottom:8px">
2669
+ <summary style="cursor:pointer;font-size:13px;padding:4px 6px;background:#eef1f5;border-radius:6px">
2670
+ <b>${esc(s.file)}</b>
2671
+ <span style="color:#0a742e">+${s.add == null ? '二进制' : s.add}</span>
2672
+ <span style="color:#c0392b">-${s.del == null ? '' : s.del}</span>
2673
+ </summary>
2674
+ <pre class="diff-pre">${(byFile[s.file] || '').split('\n').map(escDiffLine).join('')}</pre>
2675
+ </details>`).join('');
2676
+ }
2677
+ function closeTaskDiff() {
2678
+ document.getElementById('diffMask').classList.remove('show');
2679
+ diffTask = null;
2680
+ }
2681
+ async function mergeTaskDiff() {
2682
+ if (!diffTask) return;
2683
+ if (!confirm(`把「${diffTask.title.slice(0, 30)}」的隔离区改动合并到主工作目录?\n(未提交改动会先自动提交到隔离分支,再在主目录执行 git merge)`)) return;
2684
+ const data = await api('/api/tasks/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: diffTask.id }) });
2685
+ if (data.success) { toast(data.already ? '隔离区改动此前已合并' : '已合并到主工作目录'); closeTaskDiff(); loadTasks(); }
2686
+ else toast(data.error || '合并失败');
2687
+ }
2688
+ async function discardTaskDiff() {
2689
+ if (!diffTask) return;
2690
+ if (!confirm(`丢弃「${diffTask.title.slice(0, 30)}」的隔离区?\n(删除独立 worktree 与分支,其中未合并的改动将永久丢失;任务本身保留,可重新执行)`)) return;
2691
+ const data = await api('/api/tasks/discard', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: diffTask.id }) });
2692
+ if (data.success) { toast('隔离区已丢弃'); closeTaskDiff(); loadTasks(); }
2693
+ else toast(data.error || '丢弃失败');
2694
+ }
2695
+
2594
2696
  async function clearTasks() {
2595
2697
  if (!confirm('确定清空全部任务?(任务会话的对话记录也会从列表移除)')) return;
2596
2698
  await api('/api/tasks/clear', { method: 'POST' });
@@ -3513,6 +3615,13 @@ function openHelp() {
3513
3615
  <li>全部页面关闭且无任务执行/审批等待时,后台服务空闲约 1 分钟自动退出(需要定时任务到点执行请保持页面打开,或运行 <code>agents-chat autostart on</code> 开机自启;<code>.env AGENTS_CHAT_AUTOSTOP=0</code> 可关闭),需要时重新运行 start 即可</li>
3514
3616
  </ul>
3515
3617
  </div>
3618
+ <div class="help-sec"><h4>🌿 Git 隔离执行(worktree)</h4>
3619
+ <ul>
3620
+ <li>导入任务时勾选「Git 隔离执行」:每个任务在<b>独立 Git worktree 分支</b>中执行,多任务并行互不污染,失败的尝试可整体丢弃(统一工作目录需是 Git 仓库且至少有一个提交)</li>
3621
+ <li>任务完成后任务条出现「改动」按钮:<b>逐文件查看 diff</b>(新增/删除/修改着色显示),确认后「合并到主目录」(自动提交隔离分支并 git merge,冲突会还原主目录并提示)或「丢弃隔离区」(删除该 worktree 与分支)</li>
3622
+ <li>隔离区随数据维护自动治理:任务删除或完结超 15 天的 worktree 自动清理</li>
3623
+ </ul>
3624
+ </div>
3516
3625
  <div class="help-sec"><h4>🔔 完成通知</h4>
3517
3626
  <ul>
3518
3627
  <li>顶栏「🔔 通知」开启后(需浏览器授权),页面在后台时任务/卡牌完成会弹<b>系统级通知</b>,人不在电脑前也不错过</li>
package/app/server.js CHANGED
@@ -629,8 +629,18 @@ const server = http.createServer(async (req, res) => {
629
629
  }
630
630
  const mode = body.mode === 'scheduled' ? 'scheduled' : 'sequential';
631
631
  const runner = body.runner === 'solo' ? 'solo' : '';
632
- const { added, warnings } = store.importTasks(body.text, mode, runner, body.model);
633
- json(res, 200, { success: true, added, warnings, mode, runner, tasks: store.getTasks() });
632
+ const { added, warnings, addedTasks } = store.importTasks(body.text, mode, runner, body.model);
633
+ // Git 隔离执行:为每个新任务创建独立 worktree(工作目录是 git 仓库时生效)
634
+ let isolated = 0;
635
+ if (body.isolated && Array.isArray(addedTasks)) {
636
+ const worktree = require('./lib/worktree');
637
+ for (const t of addedTasks) {
638
+ const wt = await worktree.createForTask(t.id);
639
+ if (wt) { store.updateTask(t.id, { worktree: wt }); isolated++; }
640
+ }
641
+ if (!isolated) warnings.push('统一工作目录不是 Git 仓库(或没有任何提交),任务将按共享目录执行');
642
+ }
643
+ json(res, 200, { success: true, added, warnings, mode, runner, isolated, tasks: store.getTasks() });
634
644
  return;
635
645
  }
636
646
 
@@ -647,14 +657,62 @@ const server = http.createServer(async (req, res) => {
647
657
  }
648
658
 
649
659
  if (p === '/api/tasks/delete' && req.method === 'POST') {
650
- // 删除任务及其会话消息
660
+ // 删除任务及其会话消息;Git 隔离区一并清理
651
661
  const body = await readBody(req);
652
662
  if (!body.id) { json(res, 400, { success: false, error: 'id 不能为空' }); return; }
663
+ const t = store.getTasks().find(x => x.id === String(body.id));
664
+ if (t && t.worktree) {
665
+ require('./lib/worktree').removeForTask(t.worktree).catch(() => {});
666
+ }
653
667
  store.deleteTask(String(body.id));
654
668
  json(res, 200, { success: true, tasks: store.getTasks() });
655
669
  return;
656
670
  }
657
671
 
672
+ // ---------- Git 隔离区:改动查看 / 合并 / 丢弃 ----------
673
+ if (p === '/api/tasks/diff' && req.method === 'GET') {
674
+ const t = store.getTasks().find(x => x.id === String(parsed.query.id || ''));
675
+ if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
676
+ try {
677
+ const d = await require('./lib/worktree').diff(t.worktree, { withPatch: true });
678
+ json(res, 200, { success: true, task: { id: t.id, title: t.title, status: t.status }, diff: d });
679
+ } catch (e) {
680
+ json(res, 500, { success: false, error: String((e && e.message) || e) });
681
+ }
682
+ return;
683
+ }
684
+
685
+ if (p === '/api/tasks/merge' && req.method === 'POST') {
686
+ const body = await readBody(req);
687
+ const t = store.getTasks().find(x => x.id === String(body.id || ''));
688
+ if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
689
+ if (runLocks.tasks) { json(res, 409, { success: false, error: '任务批次执行中,暂不能合并' }); return; }
690
+ try {
691
+ const r = await require('./lib/worktree').mergeToMain(t.worktree, t.title);
692
+ if (r.ok) store.updateTask(t.id, { mergedAt: Date.now() });
693
+ json(res, 200, { success: r.ok, already: !!r.already, error: r.error || '' });
694
+ } catch (e) {
695
+ json(res, 500, { success: false, error: String((e && e.message) || e) });
696
+ }
697
+ return;
698
+ }
699
+
700
+ if (p === '/api/tasks/discard' && req.method === 'POST') {
701
+ // 丢弃隔离区:删除 worktree 与分支,任务保留(可重跑重建)
702
+ const body = await readBody(req);
703
+ const t = store.getTasks().find(x => x.id === String(body.id || ''));
704
+ if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
705
+ if (runLocks.tasks) { json(res, 409, { success: false, error: '任务批次执行中,暂不能丢弃' }); return; }
706
+ try {
707
+ const r = await require('./lib/worktree').removeForTask(t.worktree);
708
+ if (r.ok) store.updateTask(t.id, { worktree: null });
709
+ json(res, 200, { success: r.ok, error: r.error || '' });
710
+ } catch (e) {
711
+ json(res, 500, { success: false, error: String((e && e.message) || e) });
712
+ }
713
+ return;
714
+ }
715
+
658
716
  if (p === '/api/tasks/run' && req.method === 'POST') {
659
717
  // 顺序执行任务:scope='solo' 时由 OpenCode 单体逐个执行(每个任务独立一次性对话);
660
718
  // 默认(群聊)每个任务 = 独立会话 + 一次完整管家调度
@@ -1344,7 +1402,7 @@ async function executeTaskBatch(selected, send, myToken) {
1344
1402
  const persist = (m) => store.addMessage({ ...m, timestamp: new Date().toISOString() });
1345
1403
  await runTasks(
1346
1404
  selected, butler, subAgents,
1347
- { getHistory: (tid) => buildHistoryText(tid), resolveAssign, scope: 'tasks', isStopped: () => stopTokens.tasks !== myToken, approval: approvalSetting(), requestApproval: makeRequestApproval() },
1405
+ { getHistory: (tid) => buildHistoryText(tid), resolveAssign, scope: 'tasks', isStopped: () => stopTokens.tasks !== myToken, approval: approvalSetting(), requestApproval: makeRequestApproval(), taskCwd: (task) => (task.worktree && task.worktree.dir) || '' },
1348
1406
  send, persist,
1349
1407
  // 任务会话首条消息:任务本身(用户视角)
1350
1408
  (task) => {
@@ -1397,7 +1455,11 @@ async function executeSoloTaskBatch(selected, send, myToken) {
1397
1455
  const order = [];
1398
1456
  let doneError = '';
1399
1457
  let sesId = ocSessionId || '';
1400
- await new Promise((resolve) => {
1458
+ // 任务隔离 worktree:整任务执行期间注入 cwd 上下文(ALS,与并发请求互不干扰)
1459
+ const worktreeMod = require('./lib/worktree');
1460
+ const wtDir = task.worktree && task.worktree.dir ? task.worktree.dir : '';
1461
+ if (wtDir) send({ type: 'notice', content: `🌿 本任务在 Git 隔离区执行:${wtDir}`, taskId: task.id });
1462
+ await worktreeMod.runWithTaskCwd(wtDir, () => new Promise((resolve) => {
1401
1463
  const kind = runner.kind === 'demo' ? 'demo' : (runner.kind === 'opencode' ? 'opencode' : 'fallback');
1402
1464
  oc.chatSolo(kind, runner, {
1403
1465
  prompt: cont
@@ -1422,7 +1484,7 @@ async function executeSoloTaskBatch(selected, send, myToken) {
1422
1484
  resolve();
1423
1485
  }
1424
1486
  });
1425
- });
1487
+ }));
1426
1488
 
1427
1489
  const finalText = order.map(id => texts.get(id)).join('\n\n').trim();
1428
1490
  if (finalText) {
@@ -1503,6 +1565,10 @@ const PRUNE_DAYS = Number(process.env.AGENTS_CHAT_PRUNE_DAYS) > 0 ? Number(proce
1503
1565
  function pruneOldDataQuiet() {
1504
1566
  try {
1505
1567
  const stat = store.pruneOldData(PRUNE_DAYS);
1568
+ // Git 隔离区同步治理:无主(任务已删)或完结超期的 worktree 一并清理
1569
+ require('./lib/worktree').pruneStale(PRUNE_DAYS, store.getTasks())
1570
+ .then(ws => { if (ws.worktrees) { stat.worktrees = ws.worktrees; console.log('[maintenance] 清理过期 Git 隔离区:', ws.worktrees); } })
1571
+ .catch(() => {});
1506
1572
  const touched = Object.values(stat).reduce((a, b) => a + b, 0);
1507
1573
  if (touched) console.log(`[maintenance] 自动清理超 ${PRUNE_DAYS} 天的历史数据:`, JSON.stringify(stat));
1508
1574
  } catch (e) { console.error('[maintenance] 自动清理失败:', e && (e.message || e)); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamsamyiok/agents-chat",
3
- "version": "3.31.0",
3
+ "version": "3.32.0",
4
4
  "description": "多智能体群聊工具 - 支持 OpenCode/Claude Code/Codex/pi 内核,微信风格聊天界面",
5
5
  "main": "lib/start.js",
6
6
  "bin": {