@iamsamyiok/agents-chat 3.31.0 → 3.33.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 +75 -23
- package/app/lib/agent.js +4 -1
- package/app/lib/oc.js +10 -3
- package/app/lib/orchestrator.js +31 -26
- package/app/lib/store.js +1 -1
- package/app/lib/worktree.js +192 -0
- package/app/mock/mock-agent.js +7 -2
- package/app/public/index.html +204 -10
- package/app/server.js +78 -7
- package/package.json +1 -1
- package/scripts/build-exe.js +2 -0
package/README.md
CHANGED
|
@@ -1,42 +1,94 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Agents Chat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
一个页面指挥一支 AI 智能体团队:你说需求,管家拆解调度,多个智能体并行干活、互相验收。本地运行、数据不出本机,支持免安装单文件 exe。
|
|
4
4
|
|
|
5
|
-
>
|
|
6
|
-
> 因此使用官方建议的 scoped 包名 `@iamsamyiok/agents-chat`,安装后的命令仍是 `agents-chat`。
|
|
5
|
+
> 生态位说明:同类开源工具 Vibe Kanban(28k★)已停止维护、Crystal 已转商业化,Agents Chat 是这个赛道持续维护的免费开源选择,并额外提供定时任务、人工审批、卡牌协作等编排能力。
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## 30 秒上手
|
|
9
8
|
|
|
10
9
|
```bash
|
|
11
|
-
|
|
10
|
+
# 安装(Node >= 18;也可直接下载免安装 exe,见下)
|
|
11
|
+
npm install -g agents-chat-cli
|
|
12
|
+
|
|
13
|
+
# 启动:自动打开浏览器;首次打开有新手引导,点「一键体验示例」即可看完整流程
|
|
14
|
+
agents-chat
|
|
12
15
|
```
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
没装 AI 内核也能跑:未检测到内核时自动进入演示模式(模拟输出),先体验界面再安装真实内核。
|
|
15
18
|
|
|
16
|
-
|
|
19
|
+
```bash
|
|
20
|
+
# 推荐内核(任选其一,装完重启 agents-chat 即真实执行)
|
|
21
|
+
npm install -g opencode-ai
|
|
22
|
+
```
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
- **Claude Code**: `npm install -g @anthropic-ai/claude-code`
|
|
20
|
-
- **Codex CLI**: `npm install -g @openai/codex`
|
|
21
|
-
- **pi**: `npm install -g @earendil-works/pi-coding-agent`
|
|
24
|
+
## 核心玩法
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
| 能力 | 说明 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| 群聊编排 | 直接输入需求,管家智能体拆解成阶段计划,调度子智能体并行执行,自动验收不合格返工 |
|
|
29
|
+
| 批量任务 | 粘贴一段文本一次导入多条任务(`1. xxx` 每行一条),可拖拽排序、批量执行 |
|
|
30
|
+
| 定时任务 | 行首写时间即定时(`2026-08-18 13:07 生成日报`),到点自动执行,停机错过的启动时补跑,可开机自启 |
|
|
31
|
+
| Git 隔离 | 导入时勾选「Git 隔离执行」:每任务独立 worktree 分支互不污染,完成后逐文件看 diff、一键合并或丢弃 |
|
|
32
|
+
| 人工审批 | 可选在「方案确定后 / 最终交付前」暂停等你点头,超时 10 分钟自动否决 |
|
|
33
|
+
| 完成通知 | 页面在后台弹系统通知;webhook 转发钉钉/飞书/自建中转 |
|
|
34
|
+
| 单聊工作台 | 与 OpenCode 单体多轮续聊;任务链支持 `-` 接续上一会话、`//` 并行执行 |
|
|
35
|
+
| 卡牌协作 | 每个智能体一张卡牌实时直播工作过程,可中途插话纠偏或委派转交 |
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
# 启动服务(后台运行,自动打开浏览器)
|
|
27
|
-
agents-chat
|
|
37
|
+
## 安装方式
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
agents-chat status
|
|
39
|
+
### npm(推荐)
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
agents-chat
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g agents-chat-cli
|
|
34
43
|
```
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
> 旧包名 `@iamsamyiok/agents-chat` 同步发布,两个包内容一致,命令都叫 `agents-chat`。
|
|
46
|
+
|
|
47
|
+
### 免安装单文件 exe
|
|
48
|
+
|
|
49
|
+
从 [Releases](https://github.com/iamsamyiok/agents-chat/releases) 下载对应平台文件(Windows / Linux / macOS Intel / Apple Silicon),双击即用,数据保存在 exe 旁 `.data/` 目录。SHA-256 校验值见 checksums.txt。
|
|
50
|
+
|
|
51
|
+
- Windows SmartScreen 提示「已保护你的电脑」→「更多信息」→「仍要运行」
|
|
52
|
+
- macOS 首次运行:`xattr -d com.apple.quarantine agents-chat-darwin-*`
|
|
53
|
+
|
|
54
|
+
## 常用命令
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
agents-chat # 启动(后台运行,自动开浏览器)
|
|
58
|
+
agents-chat status # 查看运行状态
|
|
59
|
+
agents-chat stop # 停止(自动清理执行中的 AI 子进程)
|
|
60
|
+
agents-chat update # 一键升级到最新版
|
|
61
|
+
agents-chat autostart on # 开机自启(定时任务无人值守推荐开启)
|
|
62
|
+
```
|
|
37
63
|
|
|
38
64
|
## 配置
|
|
39
65
|
|
|
40
|
-
|
|
66
|
+
配置文件位于 `~/.agents-chat/.env`(exe 形态在 `.data/.env`),全部可省略:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
AGENTS_CHAT_WEBHOOK_URL= # 任务完成转发地址(钉钉/飞书中转)
|
|
70
|
+
AGENTS_CHAT_PRUNE_DAYS=15 # 历史数据保留天数
|
|
71
|
+
AGENTS_CHAT_TIMEOUT_MS=600000 # 单任务超时
|
|
72
|
+
AGENTS_CHAT_AUTO_APPROVE=0 # 关闭 OpenCode 自动放行
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
完整说明见仓库 `.env.example` 与页面内「帮助」。
|
|
76
|
+
|
|
77
|
+
## 支持的执行内核
|
|
78
|
+
|
|
79
|
+
OpenCode(推荐)/ Claude Code / Codex CLI / pi,页面右上角可切换。多智能体编排深度适配 OpenCode,其余内核以单轮对话方式执行。
|
|
80
|
+
|
|
81
|
+
## 开发
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git clone https://github.com/iamsamyiok/agents-chat
|
|
85
|
+
cd agents-chat && npm install
|
|
86
|
+
AGENTS_CHAT_MOCK=1 npm start # 演示模式开发,无需内核
|
|
87
|
+
npm test # 66 个测试用例
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
零运行时依赖,Node 原生模块实现(HTTP/SSE/存储均为自研轻量层)。
|
|
91
|
+
|
|
92
|
+
## License
|
|
41
93
|
|
|
42
|
-
|
|
94
|
+
MIT
|
package/app/lib/agent.js
CHANGED
|
@@ -349,10 +349,13 @@ function runAgent(agent, prompt, onChunk, scope) {
|
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
// 智能体工作目录(全局统一):
|
|
352
|
-
// -
|
|
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
|
|
198
|
-
const
|
|
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') {
|
package/app/lib/orchestrator.js
CHANGED
|
@@ -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
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
+
};
|
package/app/mock/mock-agent.js
CHANGED
|
@@ -33,10 +33,15 @@ if (behavior === 'solo-chat') {
|
|
|
33
33
|
`已处理完成 ${DEMO_TAG}`
|
|
34
34
|
]);
|
|
35
35
|
} else if (behavior === 'solo-task') {
|
|
36
|
-
//
|
|
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
|
-
|
|
44
|
+
`任务完成:产出模拟结果(已写入 demo-solo-output.md) ${DEMO_TAG}`
|
|
40
45
|
]);
|
|
41
46
|
} else if (behavior === 'echo') {
|
|
42
47
|
if (prompt.includes('【委派背景】')) {
|
package/app/public/index.html
CHANGED
|
@@ -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, #welcomeMask { 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, #welcomeMask.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,21 @@
|
|
|
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
|
+
/* 新手引导步骤卡 */
|
|
265
|
+
.onb-step { flex: 1; background: #f5f7fa; border-radius: 8px; padding: 10px; font-size: 12px; line-height: 1.6; display: flex; flex-direction: column; gap: 4px; }
|
|
266
|
+
.onb-step .onb-num { width: 22px; height: 22px; border-radius: 50%; background: #10aeff; color: #fff; font-weight: 600; display: flex; align-items: center; justify-content: center; font-size: 13px; }
|
|
267
|
+
.onb-step span { color: #666; }
|
|
268
|
+
/* 主会话空状态欢迎卡 */
|
|
269
|
+
.main-welcome { text-align: center; padding: 44px 18px; color: #666; }
|
|
270
|
+
.main-welcome h4 { font-size: 16px; margin: 0 0 8px; color: #222; }
|
|
271
|
+
.main-welcome p { font-size: 13px; line-height: 1.9; margin: 0 0 14px; }
|
|
272
|
+
.main-welcome .onb-acts { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; }
|
|
273
|
+
/* Git 改动视图:行级着色 */
|
|
274
|
+
.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; }
|
|
275
|
+
.diff-pre .dl { padding: 0 4px; border-radius: 3px; }
|
|
276
|
+
.diff-pre .dl.add { background: #e6ffec; color: #116329; }
|
|
277
|
+
.diff-pre .dl.del { background: #ffebe9; color: #82071e; }
|
|
278
|
+
.diff-pre .dl.hun { color: #0550ae; background: #ddf4ff; }
|
|
264
279
|
.modal h3 { font-size: 15px; margin-bottom: 6px; }
|
|
265
280
|
.modal .tip { font-size: 12px; color: #999; line-height: 1.7; margin-bottom: 10px; }
|
|
266
281
|
.modal textarea { width: 100%; height: 200px; border: 1px solid #ddd; border-radius: 6px; padding: 10px; font-size: 13px; resize: vertical; outline: none; }
|
|
@@ -510,6 +525,38 @@
|
|
|
510
525
|
</div>
|
|
511
526
|
</div>
|
|
512
527
|
|
|
528
|
+
<!-- 新手引导弹窗 -->
|
|
529
|
+
<div id="welcomeMask">
|
|
530
|
+
<div class="modal" style="width:600px">
|
|
531
|
+
<h3>👋 欢迎使用 Agents Chat</h3>
|
|
532
|
+
<div class="tip">一个页面指挥一支 AI 智能体团队:你说需求,管家拆解调度,多个智能体并行干活、互相验收。三步上手:</div>
|
|
533
|
+
<div style="display:flex;gap:10px;margin:10px 0">
|
|
534
|
+
<div class="onb-step"><div class="onb-num">1</div><b>直接聊</b><span>下方输入框发需求,管家自动安排团队分工协作</span></div>
|
|
535
|
+
<div class="onb-step"><div class="onb-num">2</div><b>批量任务</b><span>「导入任务」一次粘贴多条,可排序、可定时、可 Git 隔离</span></div>
|
|
536
|
+
<div class="onb-step"><div class="onb-num">3</div><b>无人值守</b><span>定时任务到点自动跑,完成推送通知,错过自动补跑</span></div>
|
|
537
|
+
</div>
|
|
538
|
+
<div class="tip" id="onbKernelTip" style="margin-bottom:6px"></div>
|
|
539
|
+
<div class="modal-actions">
|
|
540
|
+
<button onclick="closeWelcome(true)">我自己逛逛</button>
|
|
541
|
+
<button class="primary" onclick="seedDemoTasks()">🚀 一键体验示例(自动导入并执行)</button>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<!-- 任务 Git 改动弹窗(隔离区 diff / 合并 / 丢弃) -->
|
|
547
|
+
<div id="diffMask">
|
|
548
|
+
<div class="modal" style="max-width:860px">
|
|
549
|
+
<h3 id="diffTitle">🌿 任务改动</h3>
|
|
550
|
+
<div class="tip" id="diffMeta"></div>
|
|
551
|
+
<div id="diffBody" style="max-height:52vh;overflow:auto;border:1px solid #d8dee9;border-radius:8px;padding:8px;background:#fafbfc"></div>
|
|
552
|
+
<div class="modal-actions">
|
|
553
|
+
<button class="mini-btn danger" onclick="discardTaskDiff()">🗑 丢弃隔离区</button>
|
|
554
|
+
<button class="primary" onclick="mergeTaskDiff()">合并到主目录</button>
|
|
555
|
+
<button onclick="closeTaskDiff()">关闭</button>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
|
|
513
560
|
<!-- 导入任务弹窗 -->
|
|
514
561
|
<div id="modalMask">
|
|
515
562
|
<div class="modal">
|
|
@@ -523,6 +570,10 @@
|
|
|
523
570
|
<textarea id="importText" onkeydown="importKeydown(event)"></textarea>
|
|
524
571
|
<div id="importAtMenu"></div>
|
|
525
572
|
</div>
|
|
573
|
+
<div class="ag-row" style="margin:6px 0 2px;align-items:center;gap:6px">
|
|
574
|
+
<input type="checkbox" id="importIsolated" style="width:auto">
|
|
575
|
+
<label for="importIsolated" style="flex:1;cursor:pointer">🌿 Git 隔离执行:每个任务在独立 Git worktree 分支中执行,互不污染;完成后可查看改动、合并回主目录或整体丢弃(统一工作目录需是 Git 仓库)</label>
|
|
576
|
+
</div>
|
|
526
577
|
<div class="modal-actions">
|
|
527
578
|
<button onclick="closeImport()">取消</button>
|
|
528
579
|
<button class="primary" onclick="doImport()">提取任务</button>
|
|
@@ -651,6 +702,8 @@
|
|
|
651
702
|
<h3>❓ 使用帮助</h3>
|
|
652
703
|
<div id="helpBody"></div>
|
|
653
704
|
<div class="modal-actions">
|
|
705
|
+
<button class="mini-btn" onclick="closeHelp(); openWelcome()">👋 重新查看新手引导</button>
|
|
706
|
+
<span style="flex:1"></span>
|
|
654
707
|
<button class="primary" onclick="closeHelp()">关闭</button>
|
|
655
708
|
</div>
|
|
656
709
|
</div>
|
|
@@ -696,7 +749,7 @@
|
|
|
696
749
|
</div>
|
|
697
750
|
|
|
698
751
|
<script>
|
|
699
|
-
const PAGE_VERSION = '3.
|
|
752
|
+
const PAGE_VERSION = '3.33.0'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
|
|
700
753
|
const AV_COLORS = ['#5b8def','#07c160','#fa9d3b','#10aeff','#8a6fe8','#fa5151','#e8a33d','#3aa7a3'];
|
|
701
754
|
const PHASE_LABEL = { plan: '调度规划', work: '执行', review: '验收', report: '汇总', talk: '圆桌发言', task: '任务执行' };
|
|
702
755
|
// 职业图标库(智能体配置可选)
|
|
@@ -815,6 +868,15 @@ async function init() {
|
|
|
815
868
|
renderUpdateInfo(health);
|
|
816
869
|
refreshNotifyBtn();
|
|
817
870
|
|
|
871
|
+
// 新手引导:从未使用过(主会话无消息、无任务、本机未看过引导)→ 自动弹欢迎
|
|
872
|
+
try {
|
|
873
|
+
if (!localStorage.getItem(ONBOARD_KEY)
|
|
874
|
+
&& getSess('').length === 0
|
|
875
|
+
&& tasks.filter(t => t.kind !== 'scheduled').length === 0) {
|
|
876
|
+
openWelcome();
|
|
877
|
+
}
|
|
878
|
+
} catch { /* localStorage 不可用时跳过 */ }
|
|
879
|
+
|
|
818
880
|
input.addEventListener('input', handleAtInput);
|
|
819
881
|
input.addEventListener('blur', () => setTimeout(hideAt, 150));
|
|
820
882
|
// 消息区滚动:贴底时收起「新消息」悬浮按钮
|
|
@@ -1402,6 +1464,15 @@ function linkBadge(t) {
|
|
|
1402
1464
|
? '<span style="color:#8a6fe8;font-size:11px;margin-right:4px" title="在上一任务的会话中继续执行(同进程续聊)">↪ 续聊</span>'
|
|
1403
1465
|
: '<span style="color:#0a742e;font-size:11px;margin-right:4px" title="与相邻并行任务同时执行(独立进程)">⚡ 并行</span>';
|
|
1404
1466
|
}
|
|
1467
|
+
// Git 隔离徽标与改动按钮
|
|
1468
|
+
function wtBadge(t) {
|
|
1469
|
+
return t.worktree ? '<span style="color:#0a742e;font-size:11px;margin-right:4px" title="Git 隔离执行(独立 worktree 分支)">🌿</span>' : '';
|
|
1470
|
+
}
|
|
1471
|
+
function wtBtn(t) {
|
|
1472
|
+
return (t.worktree && (t.status === 'done' || t.status === 'failed'))
|
|
1473
|
+
? `<button class="mini-btn" title="查看本任务在隔离区的 Git 改动(可合并/丢弃)" onclick="event.stopPropagation();openTaskDiff('${escAttr(t.id)}')">改动</button>`
|
|
1474
|
+
: '';
|
|
1475
|
+
}
|
|
1405
1476
|
function renderTasks() {
|
|
1406
1477
|
const el = document.getElementById('taskList');
|
|
1407
1478
|
const badge = document.getElementById('pendingBadge');
|
|
@@ -1424,10 +1495,11 @@ function renderTasks() {
|
|
|
1424
1495
|
<div class="task-avatar st-${t.status === 'pending' ? 'scheduled' : t.status}">${icon}</div>
|
|
1425
1496
|
<div class="task-main">
|
|
1426
1497
|
<div class="task-title">${esc(t.title)}</div>
|
|
1427
|
-
<div class="task-sub"><span>${linkBadge(t)}${st}</span></div>
|
|
1498
|
+
<div class="task-sub"><span>${linkBadge(t)}${wtBadge(t)}${st}</span></div>
|
|
1428
1499
|
</div>
|
|
1429
1500
|
${t.status === 'pending' ? `<button class="mini-btn" title="不再等到点,立即执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">立即执行</button>` : ''}
|
|
1430
1501
|
${t.status === 'failed' ? `<button class="mini-btn" title="重新执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">重跑</button>` : ''}
|
|
1502
|
+
${wtBtn(t)}
|
|
1431
1503
|
<button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
|
|
1432
1504
|
</div>`;
|
|
1433
1505
|
}).join('') : '<div class="task-empty">暂无定时任务<br>点上方「导入任务」→「定时任务」标签添加</div>';
|
|
@@ -1443,12 +1515,13 @@ function renderTasks() {
|
|
|
1443
1515
|
<div class="task-avatar st-${t.status}">${icon}</div>
|
|
1444
1516
|
<div class="task-main">
|
|
1445
1517
|
<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>
|
|
1518
|
+
<div class="task-sub"><span>${linkBadge(t)}${wtBadge(t)}${statusText(t.status)}</span><span>${fmtTime(t.createdAt)}</span></div>
|
|
1447
1519
|
</div>
|
|
1448
1520
|
${t.status === 'pending' || t.status === 'failed' ? `<button class="mini-btn" onclick="event.stopPropagation();runTask('${escAttr(t.id)}')">执行</button>` : ''}
|
|
1521
|
+
${wtBtn(t)}
|
|
1449
1522
|
<button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
|
|
1450
1523
|
</div>`;
|
|
1451
|
-
}).join('') : '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>(单聊任务由 OpenCode
|
|
1524
|
+
}).join('') : '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>(单聊任务由 OpenCode 单体执行)<br><button class="mini-btn" style="margin-top:6px" onclick="seedDemoTasks()">🚀 试试示例任务</button></div>';
|
|
1452
1525
|
}
|
|
1453
1526
|
el.innerHTML = `<div class="list-sep">${taskView === 'sched' ? '定时任务(单聊 · 到点自动执行)' : '单聊任务(点击查看会话,可拖拽排序)'}</div>` + tasksHtml;
|
|
1454
1527
|
return;
|
|
@@ -1475,10 +1548,11 @@ function renderTasks() {
|
|
|
1475
1548
|
<div class="task-avatar st-${t.status === 'pending' ? 'scheduled' : t.status}">${icon}</div>
|
|
1476
1549
|
<div class="task-main">
|
|
1477
1550
|
<div class="task-title">${esc(t.title)}</div>
|
|
1478
|
-
<div class="task-sub"><span>${st}${assign}</span></div>
|
|
1551
|
+
<div class="task-sub"><span>${wtBadge(t)}${st}${assign}</span></div>
|
|
1479
1552
|
</div>
|
|
1480
1553
|
${t.status === 'pending' ? `<button class="mini-btn" title="不再等到点,立即执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">立即执行</button>` : ''}
|
|
1481
1554
|
${t.status === 'failed' ? `<button class="mini-btn" title="重新执行" onclick="event.stopPropagation();runSchedNow('${escAttr(t.id)}')">重跑</button>` : ''}
|
|
1555
|
+
${wtBtn(t)}
|
|
1482
1556
|
<button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
|
|
1483
1557
|
</div>`;
|
|
1484
1558
|
}).join('');
|
|
@@ -1495,7 +1569,7 @@ function renderTasks() {
|
|
|
1495
1569
|
|
|
1496
1570
|
let tasksHtml;
|
|
1497
1571
|
if (vis.length === 0) {
|
|
1498
|
-
tasksHtml = '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>定时任务请切到上方「⏰
|
|
1572
|
+
tasksHtml = '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>定时任务请切到上方「⏰ 定时任务」<br><button class="mini-btn" style="margin-top:6px" onclick="seedDemoTasks()">🚀 试试示例任务</button></div>';
|
|
1499
1573
|
} else {
|
|
1500
1574
|
tasksHtml = vis.map((t) => {
|
|
1501
1575
|
const icon = t.status === 'done' ? '✓' : t.status === 'failed' ? '!' : t.status === 'running' ? '▶' : '•';
|
|
@@ -1505,9 +1579,10 @@ function renderTasks() {
|
|
|
1505
1579
|
<div class="task-avatar st-${t.status}">${icon}</div>
|
|
1506
1580
|
<div class="task-main">
|
|
1507
1581
|
<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>
|
|
1582
|
+
<div class="task-sub"><span>${wtBadge(t)}${statusText(t.status)}${assign}</span><span>${fmtTime(t.createdAt)}</span></div>
|
|
1509
1583
|
</div>
|
|
1510
1584
|
${t.status === 'pending' || t.status === 'failed' ? `<button class="mini-btn" onclick="event.stopPropagation();runTask('${escAttr(t.id)}')">执行</button>` : ''}
|
|
1585
|
+
${wtBtn(t)}
|
|
1511
1586
|
<button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
|
|
1512
1587
|
</div>`;
|
|
1513
1588
|
}).join('');
|
|
@@ -1937,6 +2012,18 @@ function renderMessages() {
|
|
|
1937
2012
|
box.appendChild(node);
|
|
1938
2013
|
finalizeMsgLinks(all[i]); // 定稿消息中的 .md 路径变成可点击预览链接
|
|
1939
2014
|
}
|
|
2015
|
+
// 主会话空状态:欢迎卡 + 快捷动作(新用户零点击成本看到入口)
|
|
2016
|
+
if (curTaskId === '' && chatMode === 'group' && all.length === 0) {
|
|
2017
|
+
box.innerHTML = `<div class="main-welcome">
|
|
2018
|
+
<h4>👋 今天想让团队做点什么?</h4>
|
|
2019
|
+
<p>直接在下方输入需求,管家会拆解调度智能体团队协作完成;<br>也可以先导入一批任务批量执行,或点「试试示例」看完整流程。</p>
|
|
2020
|
+
<div class="onb-acts">
|
|
2021
|
+
<button class="primary" onclick="seedDemoTasks()">🚀 试试示例</button>
|
|
2022
|
+
<button onclick="openImport()">📥 导入任务</button>
|
|
2023
|
+
<button onclick="openHelp()">📖 查看帮助</button>
|
|
2024
|
+
</div>
|
|
2025
|
+
</div>`;
|
|
2026
|
+
}
|
|
1940
2027
|
scrollBottom();
|
|
1941
2028
|
requestAnimationFrame(() => requestAnimationFrame(() => box.classList.remove('bulk')));
|
|
1942
2029
|
const jb = document.getElementById('jumpBtn'); if (jb) jb.style.display = 'none';
|
|
@@ -2571,11 +2658,17 @@ async function doImport() {
|
|
|
2571
2658
|
// 单聊导入时记录当前所选模型,定时触发时用它执行(页面关闭也不丢)
|
|
2572
2659
|
const data = await api('/api/tasks/import', {
|
|
2573
2660
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2574
|
-
body: JSON.stringify({
|
|
2661
|
+
body: JSON.stringify({
|
|
2662
|
+
text, mode: importMode, runner: chatMode === 'solo' ? 'solo' : '', model: chatMode === 'solo' ? (curOcModel || '') : '',
|
|
2663
|
+
isolated: !!(document.getElementById('importIsolated') || {}).checked
|
|
2664
|
+
})
|
|
2575
2665
|
});
|
|
2576
2666
|
if (data.success) {
|
|
2577
2667
|
tasks = data.tasks || [];
|
|
2578
2668
|
renderSidebar();
|
|
2669
|
+
if (data.isolated > 0) {
|
|
2670
|
+
(chatMode === 'solo' ? addSoloSys : addSys)(`🌿 ${data.isolated} 个任务将在 Git 隔离区执行(独立分支,互不污染)`);
|
|
2671
|
+
}
|
|
2579
2672
|
if (chatMode === 'solo') {
|
|
2580
2673
|
addSoloSys(importMode === 'scheduled'
|
|
2581
2674
|
? `已添加 ${data.added} 个单聊定时任务(到点由 OpenCode 自动执行)`
|
|
@@ -2591,6 +2684,100 @@ async function doImport() {
|
|
|
2591
2684
|
} else toast(data.error || '导入失败');
|
|
2592
2685
|
}
|
|
2593
2686
|
|
|
2687
|
+
// ---------- Git 隔离区改动查看 / 合并 / 丢弃 ----------
|
|
2688
|
+
let diffTask = null; // 当前弹窗对应的任务
|
|
2689
|
+
function escDiffLine(l) {
|
|
2690
|
+
const e = esc(l);
|
|
2691
|
+
if (l.startsWith('+')) return `<div class="dl add">${e}</div>`;
|
|
2692
|
+
if (l.startsWith('-')) return `<div class="dl del">${e}</div>`;
|
|
2693
|
+
if (l.startsWith('@@')) return `<div class="dl hun">${e}</div>`;
|
|
2694
|
+
return `<div class="dl">${e}</div>`;
|
|
2695
|
+
}
|
|
2696
|
+
async function openTaskDiff(id) {
|
|
2697
|
+
const t = tasks.find(x => x.id === id);
|
|
2698
|
+
if (!t) return;
|
|
2699
|
+
document.getElementById('diffTitle').textContent = `🌿 改动:${t.title.slice(0, 30)}`;
|
|
2700
|
+
document.getElementById('diffMeta').textContent = '加载中…';
|
|
2701
|
+
document.getElementById('diffBody').innerHTML = '';
|
|
2702
|
+
document.getElementById('diffMask').classList.add('show');
|
|
2703
|
+
diffTask = t;
|
|
2704
|
+
const data = await api('/api/tasks/diff?id=' + encodeURIComponent(id));
|
|
2705
|
+
if (!data.success) { document.getElementById('diffMeta').textContent = data.error || '加载失败'; return; }
|
|
2706
|
+
const d = data.diff;
|
|
2707
|
+
const addAll = d.stat.reduce((a, s) => a + (s.add || 0), 0);
|
|
2708
|
+
const delAll = d.stat.reduce((a, s) => a + (s.del || 0), 0);
|
|
2709
|
+
const merged = t.mergedAt ? '(已合并过)' : '';
|
|
2710
|
+
document.getElementById('diffMeta').innerHTML =
|
|
2711
|
+
`分支 ${esc(d.branch)}${merged} · ${d.stat.length} 个文件 · <span style="color:#0a742e">+${addAll}</span> / <span style="color:#c0392b">-${delAll}</span>${d.truncated ? '(内容过长已截断)' : ''}`;
|
|
2712
|
+
if (!d.stat.length) {
|
|
2713
|
+
document.getElementById('diffBody').innerHTML = '<div style="color:#666;padding:12px">隔离区没有任何改动</div>';
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
const byFile = Object.fromEntries((d.files || []).map(f => [f.path, f.patch]));
|
|
2717
|
+
document.getElementById('diffBody').innerHTML = d.stat.map(s => `
|
|
2718
|
+
<details ${d.stat.length <= 5 ? 'open' : ''} style="margin-bottom:8px">
|
|
2719
|
+
<summary style="cursor:pointer;font-size:13px;padding:4px 6px;background:#eef1f5;border-radius:6px">
|
|
2720
|
+
<b>${esc(s.file)}</b>
|
|
2721
|
+
<span style="color:#0a742e">+${s.add == null ? '二进制' : s.add}</span>
|
|
2722
|
+
<span style="color:#c0392b">-${s.del == null ? '' : s.del}</span>
|
|
2723
|
+
</summary>
|
|
2724
|
+
<pre class="diff-pre">${(byFile[s.file] || '').split('\n').map(escDiffLine).join('')}</pre>
|
|
2725
|
+
</details>`).join('');
|
|
2726
|
+
}
|
|
2727
|
+
function closeTaskDiff() {
|
|
2728
|
+
document.getElementById('diffMask').classList.remove('show');
|
|
2729
|
+
diffTask = null;
|
|
2730
|
+
}
|
|
2731
|
+
async function mergeTaskDiff() {
|
|
2732
|
+
if (!diffTask) return;
|
|
2733
|
+
if (!confirm(`把「${diffTask.title.slice(0, 30)}」的隔离区改动合并到主工作目录?\n(未提交改动会先自动提交到隔离分支,再在主目录执行 git merge)`)) return;
|
|
2734
|
+
const data = await api('/api/tasks/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: diffTask.id }) });
|
|
2735
|
+
if (data.success) { toast(data.already ? '隔离区改动此前已合并' : '已合并到主工作目录'); closeTaskDiff(); loadTasks(); }
|
|
2736
|
+
else toast(data.error || '合并失败');
|
|
2737
|
+
}
|
|
2738
|
+
async function discardTaskDiff() {
|
|
2739
|
+
if (!diffTask) return;
|
|
2740
|
+
if (!confirm(`丢弃「${diffTask.title.slice(0, 30)}」的隔离区?\n(删除独立 worktree 与分支,其中未合并的改动将永久丢失;任务本身保留,可重新执行)`)) return;
|
|
2741
|
+
const data = await api('/api/tasks/discard', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: diffTask.id }) });
|
|
2742
|
+
if (data.success) { toast('隔离区已丢弃'); closeTaskDiff(); loadTasks(); }
|
|
2743
|
+
else toast(data.error || '丢弃失败');
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
// ---------- 新手引导与一键示例 ----------
|
|
2747
|
+
const ONBOARD_KEY = 'acOnboarded';
|
|
2748
|
+
function closeWelcome(markSeen) {
|
|
2749
|
+
document.getElementById('welcomeMask').classList.remove('show');
|
|
2750
|
+
if (markSeen) try { localStorage.setItem(ONBOARD_KEY, '1'); } catch { /* 隐私模式忽略 */ }
|
|
2751
|
+
}
|
|
2752
|
+
function openWelcome() {
|
|
2753
|
+
const tip = document.getElementById('onbKernelTip');
|
|
2754
|
+
const rs = runnerState || {};
|
|
2755
|
+
if (rs.kind === 'demo') tip.innerHTML = '🧪 当前为<b>演示模式</b>(模拟输出,无需任何配置即可体验完整流程);安装 OpenCode 后即为真实执行';
|
|
2756
|
+
else if (rs.kind === 'missing') tip.innerHTML = '⚠ 未检测到执行内核,真实任务前需安装:npm install -g opencode-ai(详见帮助)';
|
|
2757
|
+
else tip.innerHTML = `✅ 运行内核就绪:${rs.label || 'OpenCode'}`;
|
|
2758
|
+
document.getElementById('welcomeMask').classList.add('show');
|
|
2759
|
+
}
|
|
2760
|
+
// 一键体验:自动导入一组示例任务并立即执行(群聊=管家编排双任务;单聊=OpenCode 续聊链)
|
|
2761
|
+
async function seedDemoTasks() {
|
|
2762
|
+
closeWelcome(true);
|
|
2763
|
+
const solo = chatMode === 'solo';
|
|
2764
|
+
const text = solo
|
|
2765
|
+
? '1. 用一句话介绍你能帮我做什么,再列出你最擅长的三类任务\n2. -继续:针对上面三类任务,各举一个具体例子说明你会怎么完成'
|
|
2766
|
+
: '1. 盘点当前工作目录的文件与子目录结构,输出一份简明的目录说明\n2. 根据上一任务的盘点结果,为这个项目生成一份 README.md 草稿';
|
|
2767
|
+
const data = await api('/api/tasks/import', {
|
|
2768
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2769
|
+
body: JSON.stringify({ text, mode: 'sequential', runner: solo ? 'solo' : '' })
|
|
2770
|
+
});
|
|
2771
|
+
if (!data.success) { toast(data.error || '示例导入失败'); return; }
|
|
2772
|
+
tasks = data.tasks || [];
|
|
2773
|
+
renderSidebar();
|
|
2774
|
+
addSys(`🚀 已导入 ${data.added} 个示例任务,开始体验(随时可在任务列表删除)`);
|
|
2775
|
+
const ids = tasks.filter(t => t.runner === (solo ? 'solo' : '') && t.kind === 'sequential' && t.status === 'pending').map(t => t.id);
|
|
2776
|
+
if (!ids.length) { toast('示例任务已导入,请手动执行'); return; }
|
|
2777
|
+
if (solo) await soloExecTasks(ids.slice(0, 2));
|
|
2778
|
+
else await runTask(ids[0]);
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2594
2781
|
async function clearTasks() {
|
|
2595
2782
|
if (!confirm('确定清空全部任务?(任务会话的对话记录也会从列表移除)')) return;
|
|
2596
2783
|
await api('/api/tasks/clear', { method: 'POST' });
|
|
@@ -3513,6 +3700,13 @@ function openHelp() {
|
|
|
3513
3700
|
<li>全部页面关闭且无任务执行/审批等待时,后台服务空闲约 1 分钟自动退出(需要定时任务到点执行请保持页面打开,或运行 <code>agents-chat autostart on</code> 开机自启;<code>.env AGENTS_CHAT_AUTOSTOP=0</code> 可关闭),需要时重新运行 start 即可</li>
|
|
3514
3701
|
</ul>
|
|
3515
3702
|
</div>
|
|
3703
|
+
<div class="help-sec"><h4>🌿 Git 隔离执行(worktree)</h4>
|
|
3704
|
+
<ul>
|
|
3705
|
+
<li>导入任务时勾选「Git 隔离执行」:每个任务在<b>独立 Git worktree 分支</b>中执行,多任务并行互不污染,失败的尝试可整体丢弃(统一工作目录需是 Git 仓库且至少有一个提交)</li>
|
|
3706
|
+
<li>任务完成后任务条出现「改动」按钮:<b>逐文件查看 diff</b>(新增/删除/修改着色显示),确认后「合并到主目录」(自动提交隔离分支并 git merge,冲突会还原主目录并提示)或「丢弃隔离区」(删除该 worktree 与分支)</li>
|
|
3707
|
+
<li>隔离区随数据维护自动治理:任务删除或完结超 15 天的 worktree 自动清理</li>
|
|
3708
|
+
</ul>
|
|
3709
|
+
</div>
|
|
3516
3710
|
<div class="help-sec"><h4>🔔 完成通知</h4>
|
|
3517
3711
|
<ul>
|
|
3518
3712
|
<li>顶栏「🔔 通知」开启后(需浏览器授权),页面在后台时任务/卡牌完成会弹<b>系统级通知</b>,人不在电脑前也不错过</li>
|
package/app/server.js
CHANGED
|
@@ -30,9 +30,14 @@ const { spawn } = require('child_process');
|
|
|
30
30
|
} catch { /* 日志系统自身失败:静默,服务照常 */ }
|
|
31
31
|
})();
|
|
32
32
|
|
|
33
|
-
// 首先加载 .env
|
|
33
|
+
// 首先加载 .env(行为开关配置):
|
|
34
|
+
// 1) 数据目录 .env(npm/exe 形态用户配置处:~/.agents-chat/.env 或 exe 旁 .data/.env,优先)
|
|
35
|
+
// 2) 程序根目录 .env(源码开发形态)
|
|
34
36
|
const { loadEnv } = require('./lib/env');
|
|
35
37
|
const ROOT_DIR = path.join(__dirname, '..');
|
|
38
|
+
const ENV_DATA_DIR = process.env.AGENTS_CHAT_DATA
|
|
39
|
+
|| (process.env.AGENTS_CHAT_STANDALONE === '1' && process.execPath ? path.join(path.dirname(process.execPath), '.data') : '');
|
|
40
|
+
loadEnv(path.join(ENV_DATA_DIR || path.join(ROOT_DIR, '.data'), '.env'));
|
|
36
41
|
loadEnv(path.join(ROOT_DIR, '.env'));
|
|
37
42
|
|
|
38
43
|
// ---------- 日志 tee:控制台输出同时写入 .data/server.log ----------
|
|
@@ -629,8 +634,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
629
634
|
}
|
|
630
635
|
const mode = body.mode === 'scheduled' ? 'scheduled' : 'sequential';
|
|
631
636
|
const runner = body.runner === 'solo' ? 'solo' : '';
|
|
632
|
-
const { added, warnings } = store.importTasks(body.text, mode, runner, body.model);
|
|
633
|
-
|
|
637
|
+
const { added, warnings, addedTasks } = store.importTasks(body.text, mode, runner, body.model);
|
|
638
|
+
// Git 隔离执行:为每个新任务创建独立 worktree(工作目录是 git 仓库时生效)
|
|
639
|
+
let isolated = 0;
|
|
640
|
+
if (body.isolated && Array.isArray(addedTasks)) {
|
|
641
|
+
const worktree = require('./lib/worktree');
|
|
642
|
+
for (const t of addedTasks) {
|
|
643
|
+
const wt = await worktree.createForTask(t.id);
|
|
644
|
+
if (wt) { store.updateTask(t.id, { worktree: wt }); isolated++; }
|
|
645
|
+
}
|
|
646
|
+
if (!isolated) warnings.push('统一工作目录不是 Git 仓库(或没有任何提交),任务将按共享目录执行');
|
|
647
|
+
}
|
|
648
|
+
json(res, 200, { success: true, added, warnings, mode, runner, isolated, tasks: store.getTasks() });
|
|
634
649
|
return;
|
|
635
650
|
}
|
|
636
651
|
|
|
@@ -647,14 +662,62 @@ const server = http.createServer(async (req, res) => {
|
|
|
647
662
|
}
|
|
648
663
|
|
|
649
664
|
if (p === '/api/tasks/delete' && req.method === 'POST') {
|
|
650
|
-
//
|
|
665
|
+
// 删除任务及其会话消息;Git 隔离区一并清理
|
|
651
666
|
const body = await readBody(req);
|
|
652
667
|
if (!body.id) { json(res, 400, { success: false, error: 'id 不能为空' }); return; }
|
|
668
|
+
const t = store.getTasks().find(x => x.id === String(body.id));
|
|
669
|
+
if (t && t.worktree) {
|
|
670
|
+
require('./lib/worktree').removeForTask(t.worktree).catch(() => {});
|
|
671
|
+
}
|
|
653
672
|
store.deleteTask(String(body.id));
|
|
654
673
|
json(res, 200, { success: true, tasks: store.getTasks() });
|
|
655
674
|
return;
|
|
656
675
|
}
|
|
657
676
|
|
|
677
|
+
// ---------- Git 隔离区:改动查看 / 合并 / 丢弃 ----------
|
|
678
|
+
if (p === '/api/tasks/diff' && req.method === 'GET') {
|
|
679
|
+
const t = store.getTasks().find(x => x.id === String(parsed.query.id || ''));
|
|
680
|
+
if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
|
|
681
|
+
try {
|
|
682
|
+
const d = await require('./lib/worktree').diff(t.worktree, { withPatch: true });
|
|
683
|
+
json(res, 200, { success: true, task: { id: t.id, title: t.title, status: t.status }, diff: d });
|
|
684
|
+
} catch (e) {
|
|
685
|
+
json(res, 500, { success: false, error: String((e && e.message) || e) });
|
|
686
|
+
}
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (p === '/api/tasks/merge' && req.method === 'POST') {
|
|
691
|
+
const body = await readBody(req);
|
|
692
|
+
const t = store.getTasks().find(x => x.id === String(body.id || ''));
|
|
693
|
+
if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
|
|
694
|
+
if (runLocks.tasks) { json(res, 409, { success: false, error: '任务批次执行中,暂不能合并' }); return; }
|
|
695
|
+
try {
|
|
696
|
+
const r = await require('./lib/worktree').mergeToMain(t.worktree, t.title);
|
|
697
|
+
if (r.ok) store.updateTask(t.id, { mergedAt: Date.now() });
|
|
698
|
+
json(res, 200, { success: r.ok, already: !!r.already, error: r.error || '' });
|
|
699
|
+
} catch (e) {
|
|
700
|
+
json(res, 500, { success: false, error: String((e && e.message) || e) });
|
|
701
|
+
}
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (p === '/api/tasks/discard' && req.method === 'POST') {
|
|
706
|
+
// 丢弃隔离区:删除 worktree 与分支,任务保留(可重跑重建)
|
|
707
|
+
const body = await readBody(req);
|
|
708
|
+
const t = store.getTasks().find(x => x.id === String(body.id || ''));
|
|
709
|
+
if (!t || !t.worktree) { json(res, 404, { success: false, error: '任务不存在或无隔离区' }); return; }
|
|
710
|
+
if (runLocks.tasks) { json(res, 409, { success: false, error: '任务批次执行中,暂不能丢弃' }); return; }
|
|
711
|
+
try {
|
|
712
|
+
const r = await require('./lib/worktree').removeForTask(t.worktree);
|
|
713
|
+
if (r.ok) store.updateTask(t.id, { worktree: null });
|
|
714
|
+
json(res, 200, { success: r.ok, error: r.error || '' });
|
|
715
|
+
} catch (e) {
|
|
716
|
+
json(res, 500, { success: false, error: String((e && e.message) || e) });
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
658
721
|
if (p === '/api/tasks/run' && req.method === 'POST') {
|
|
659
722
|
// 顺序执行任务:scope='solo' 时由 OpenCode 单体逐个执行(每个任务独立一次性对话);
|
|
660
723
|
// 默认(群聊)每个任务 = 独立会话 + 一次完整管家调度
|
|
@@ -1344,7 +1407,7 @@ async function executeTaskBatch(selected, send, myToken) {
|
|
|
1344
1407
|
const persist = (m) => store.addMessage({ ...m, timestamp: new Date().toISOString() });
|
|
1345
1408
|
await runTasks(
|
|
1346
1409
|
selected, butler, subAgents,
|
|
1347
|
-
{ getHistory: (tid) => buildHistoryText(tid), resolveAssign, scope: 'tasks', isStopped: () => stopTokens.tasks !== myToken, approval: approvalSetting(), requestApproval: makeRequestApproval() },
|
|
1410
|
+
{ getHistory: (tid) => buildHistoryText(tid), resolveAssign, scope: 'tasks', isStopped: () => stopTokens.tasks !== myToken, approval: approvalSetting(), requestApproval: makeRequestApproval(), taskCwd: (task) => (task.worktree && task.worktree.dir) || '' },
|
|
1348
1411
|
send, persist,
|
|
1349
1412
|
// 任务会话首条消息:任务本身(用户视角)
|
|
1350
1413
|
(task) => {
|
|
@@ -1397,7 +1460,11 @@ async function executeSoloTaskBatch(selected, send, myToken) {
|
|
|
1397
1460
|
const order = [];
|
|
1398
1461
|
let doneError = '';
|
|
1399
1462
|
let sesId = ocSessionId || '';
|
|
1400
|
-
|
|
1463
|
+
// 任务隔离 worktree:整任务执行期间注入 cwd 上下文(ALS,与并发请求互不干扰)
|
|
1464
|
+
const worktreeMod = require('./lib/worktree');
|
|
1465
|
+
const wtDir = task.worktree && task.worktree.dir ? task.worktree.dir : '';
|
|
1466
|
+
if (wtDir) send({ type: 'notice', content: `🌿 本任务在 Git 隔离区执行:${wtDir}`, taskId: task.id });
|
|
1467
|
+
await worktreeMod.runWithTaskCwd(wtDir, () => new Promise((resolve) => {
|
|
1401
1468
|
const kind = runner.kind === 'demo' ? 'demo' : (runner.kind === 'opencode' ? 'opencode' : 'fallback');
|
|
1402
1469
|
oc.chatSolo(kind, runner, {
|
|
1403
1470
|
prompt: cont
|
|
@@ -1422,7 +1489,7 @@ async function executeSoloTaskBatch(selected, send, myToken) {
|
|
|
1422
1489
|
resolve();
|
|
1423
1490
|
}
|
|
1424
1491
|
});
|
|
1425
|
-
});
|
|
1492
|
+
}));
|
|
1426
1493
|
|
|
1427
1494
|
const finalText = order.map(id => texts.get(id)).join('\n\n').trim();
|
|
1428
1495
|
if (finalText) {
|
|
@@ -1503,6 +1570,10 @@ const PRUNE_DAYS = Number(process.env.AGENTS_CHAT_PRUNE_DAYS) > 0 ? Number(proce
|
|
|
1503
1570
|
function pruneOldDataQuiet() {
|
|
1504
1571
|
try {
|
|
1505
1572
|
const stat = store.pruneOldData(PRUNE_DAYS);
|
|
1573
|
+
// Git 隔离区同步治理:无主(任务已删)或完结超期的 worktree 一并清理
|
|
1574
|
+
require('./lib/worktree').pruneStale(PRUNE_DAYS, store.getTasks())
|
|
1575
|
+
.then(ws => { if (ws.worktrees) { stat.worktrees = ws.worktrees; console.log('[maintenance] 清理过期 Git 隔离区:', ws.worktrees); } })
|
|
1576
|
+
.catch(() => {});
|
|
1506
1577
|
const touched = Object.values(stat).reduce((a, b) => a + b, 0);
|
|
1507
1578
|
if (touched) console.log(`[maintenance] 自动清理超 ${PRUNE_DAYS} 天的历史数据:`, JSON.stringify(stat));
|
|
1508
1579
|
} catch (e) { console.error('[maintenance] 自动清理失败:', e && (e.message || e)); }
|
package/package.json
CHANGED
package/scripts/build-exe.js
CHANGED
|
@@ -76,7 +76,9 @@ if (failed.length) {
|
|
|
76
76
|
const { createHash } = require('crypto');
|
|
77
77
|
const lines = [];
|
|
78
78
|
for (const f of fs.readdirSync(DIST).sort()) {
|
|
79
|
+
if (!/^agents-chat-/.test(f)) continue; // 只计算产物文件(跳过 .data 等目录与旧清单)
|
|
79
80
|
const fp = path.join(DIST, f);
|
|
81
|
+
try { if (!fs.statSync(fp).isFile()) continue; } catch { continue; }
|
|
80
82
|
const h = createHash('sha256').update(fs.readFileSync(fp)).digest('hex');
|
|
81
83
|
lines.push(`${h} ${f}`);
|
|
82
84
|
}
|