@iamsamyiok/agents-chat 3.32.0 → 3.34.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/.env.example CHANGED
@@ -24,3 +24,5 @@
24
24
  # AGENTS_CHAT_OPENCODE_CMD=
25
25
  # 单任务超时毫秒(默认 10 分钟)
26
26
  # AGENTS_CHAT_TIMEOUT_MS=600000
27
+ # AI 组队生成超时(毫秒,默认 120000;真实内核生成整队提示词耗时较长)
28
+ # AGENTS_CHAT_SUGGEST_TIMEOUT_MS=120000
package/README.md CHANGED
@@ -1,42 +1,95 @@
1
- # @iamsamyiok/agents-chat (npm 包版)
1
+ # Agents Chat
2
2
 
3
- 多智能体群聊工具,通过 npm 全局安装后可使用 `agents-chat` 命令启动。
3
+ 一个页面指挥一支 AI 智能体团队:你说需求,管家拆解调度,多个智能体并行干活、互相验收。本地运行、数据不出本机,支持免安装单文件 exe。
4
4
 
5
- > 包名说明:npm 相似名策略禁止发布裸名 `agents-chat`(已存在相似包 `agentschat`),
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
- npm install -g @iamsamyiok/agents-chat
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
- 本机需已安装任一 AI 执行内核:
19
+ ```bash
20
+ # 推荐内核(任选其一,装完重启 agents-chat 即真实执行)
21
+ npm install -g opencode-ai
22
+ ```
17
23
 
18
- - **OpenCode** (推荐): `npm install -g opencode-ai`
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
+ | AI 组队 | 一句话描述需求,执行内核自动生成整套智能体团队(名称/图标/技能/提示词),预览后合并或替换应用 |
30
+ | 批量任务 | 粘贴一段文本一次导入多条任务(`1. xxx` 每行一条),可拖拽排序、批量执行 |
31
+ | 定时任务 | 行首写时间即定时(`2026-08-18 13:07 生成日报`),到点自动执行,停机错过的启动时补跑,可开机自启 |
32
+ | Git 隔离 | 导入时勾选「Git 隔离执行」:每任务独立 worktree 分支互不污染,完成后逐文件看 diff、一键合并或丢弃 |
33
+ | 人工审批 | 可选在「方案确定后 / 最终交付前」暂停等你点头,超时 10 分钟自动否决 |
34
+ | 完成通知 | 页面在后台弹系统通知;webhook 转发钉钉/飞书/自建中转 |
35
+ | 单聊工作台 | 与 OpenCode 单体多轮续聊;任务链支持 `-` 接续上一会话、`//` 并行执行 |
36
+ | 卡牌协作 | 每个智能体一张卡牌实时直播工作过程,可中途插话纠偏或委派转交 |
24
37
 
25
- ```bash
26
- # 启动服务(后台运行,自动打开浏览器)
27
- agents-chat
38
+ ## 安装方式
28
39
 
29
- # 查看状态
30
- agents-chat status
40
+ ### npm(推荐)
31
41
 
32
- # 停止服务
33
- agents-chat stop
42
+ ```bash
43
+ npm install -g agents-chat-cli
34
44
  ```
35
45
 
36
- 默认访问 http://localhost:3456
46
+ > 旧包名 `@iamsamyiok/agents-chat` 同步发布,两个包内容一致,命令都叫 `agents-chat`。
47
+
48
+ ### 免安装单文件 exe
49
+
50
+ 从 [Releases](https://github.com/iamsamyiok/agents-chat/releases) 下载对应平台文件(Windows / Linux / macOS Intel / Apple Silicon),双击即用,数据保存在 exe 旁 `.data/` 目录。SHA-256 校验值见 checksums.txt。
51
+
52
+ - Windows SmartScreen 提示「已保护你的电脑」→「更多信息」→「仍要运行」
53
+ - macOS 首次运行:`xattr -d com.apple.quarantine agents-chat-darwin-*`
54
+
55
+ ## 常用命令
56
+
57
+ ```bash
58
+ agents-chat # 启动(后台运行,自动开浏览器)
59
+ agents-chat status # 查看运行状态
60
+ agents-chat stop # 停止(自动清理执行中的 AI 子进程)
61
+ agents-chat update # 一键升级到最新版
62
+ agents-chat autostart on # 开机自启(定时任务无人值守推荐开启)
63
+ ```
37
64
 
38
65
  ## 配置
39
66
 
40
- 编辑 `~/.agents-chat/.env` 文件进行配置。
67
+ 配置文件位于 `~/.agents-chat/.env`(exe 形态在 `.data/.env`),全部可省略:
68
+
69
+ ```bash
70
+ AGENTS_CHAT_WEBHOOK_URL= # 任务完成转发地址(钉钉/飞书中转)
71
+ AGENTS_CHAT_PRUNE_DAYS=15 # 历史数据保留天数
72
+ AGENTS_CHAT_TIMEOUT_MS=600000 # 单任务超时
73
+ AGENTS_CHAT_AUTO_APPROVE=0 # 关闭 OpenCode 自动放行
74
+ ```
75
+
76
+ 完整说明见仓库 `.env.example` 与页面内「帮助」。
77
+
78
+ ## 支持的执行内核
79
+
80
+ OpenCode(推荐)/ Claude Code / Codex CLI / pi,页面右上角可切换。多智能体编排深度适配 OpenCode,其余内核以单轮对话方式执行。
81
+
82
+ ## 开发
83
+
84
+ ```bash
85
+ git clone https://github.com/iamsamyiok/agents-chat
86
+ cd agents-chat && npm install
87
+ AGENTS_CHAT_MOCK=1 npm start # 演示模式开发,无需内核
88
+ npm test # 81 个测试用例
89
+ ```
90
+
91
+ 零运行时依赖,Node 原生模块实现(HTTP/SSE/存储均为自研轻量层)。
92
+
93
+ ## License
41
94
 
42
- 详见原始项目文档。
95
+ MIT
@@ -0,0 +1,138 @@
1
+ // AI 智能组队:由执行内核根据用户需求生成子智能体团队配置
2
+ // 纯函数模块(buildPrompt/extractTeamJSON/cleanSuggestedTeam)可独立单测;
3
+ // suggestTeam 注入 runAgentFn 便于测试内核交互链路
4
+ const agentMod = require('./agent');
5
+
6
+ const LIMITS = { name: 20, icon: 8, desc: 100, systemPrompt: 8000 };
7
+ const TEAM_MIN = 2;
8
+ const TEAM_MAX = 6;
9
+ const REQ_MAX = 2000;
10
+ const SUGGEST_TIMEOUT_MS = Number(process.env.AGENTS_CHAT_SUGGEST_TIMEOUT_MS) > 0
11
+ ? Number(process.env.AGENTS_CHAT_SUGGEST_TIMEOUT_MS)
12
+ : 120000; // 真实内核生成整队提示词输出量大,60s 偏紧;.env 可覆盖
13
+
14
+ // 结构化生成提示词:要求内核输出纯 JSON 数组(容错解析见 extractTeamJSON)
15
+ function buildPrompt(requirements, existingNames) {
16
+ const avoid = (Array.isArray(existingNames) ? existingNames : [])
17
+ .map(n => String(n || '').trim()).filter(Boolean);
18
+ return [
19
+ '你是一位资深多智能体系统团队设计师。请根据用户需求,设计一支协作完成该需求的智能体团队。',
20
+ '',
21
+ '【用户需求】',
22
+ String(requirements || '').trim(),
23
+ '',
24
+ '【输出要求】只输出一个 JSON 数组,不要输出任何其他文字、解释或 markdown 代码块标记。数组包含 2 到 6 个对象,每个对象字段:',
25
+ '- name:智能体名称,简短(不超过 8 个字),团队内不得重名',
26
+ '- icon:单个 emoji 图标',
27
+ '- desc:一句话技能说明(不超过 50 字),明确该智能体的能力边界,供调度者据此分派任务',
28
+ '- systemPrompt:自定义提示词,包含角色定位、专业技能与输出要求,100 到 300 字,具体可执行',
29
+ '',
30
+ avoid.length ? `【命名避让】以下名称已被现有智能体占用,生成的 name 不得与之重复:${avoid.join('、')}` : '',
31
+ '',
32
+ '【设计原则】',
33
+ '1. 按需求拆解出真正需要的分工,每个智能体职责单一且清晰,避免出现职责重叠的成员',
34
+ '2. 语言与用户需求语言一致(用户用中文则全部用中文)',
35
+ '3. systemPrompt 要具体可执行,写清楚该角色拿到任务后应该怎么做、输出什么格式的成果',
36
+ '4. 若需求较简单,团队从简(2 到 3 个);需求复杂才增加分工'
37
+ ].filter(line => line !== '').join('\n');
38
+ }
39
+
40
+ // 容错提取 JSON 数组:裸数组 → ```json 代码块 → 首个 [ 到最后一个 ] 的子串
41
+ function extractTeamJSON(text) {
42
+ const raw = String(text || '').trim();
43
+ if (!raw) return null;
44
+ // 1. 整体就是合法 JSON
45
+ try { const v = JSON.parse(raw); if (Array.isArray(v)) return v; } catch { /* 继续尝试 */ }
46
+ // 2. markdown 代码块(```json ... ``` 或 ``` ... ```)
47
+ const fence = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
48
+ if (fence) {
49
+ try { const v = JSON.parse(fence[1].trim()); if (Array.isArray(v)) return v; } catch { /* 继续尝试 */ }
50
+ }
51
+ // 3. 首个 [ 到最后一个 ] 之间(容忍前后杂文;字符串中含 ] 不影响,因为取最后一个 ])
52
+ const start = raw.indexOf('[');
53
+ const end = raw.lastIndexOf(']');
54
+ if (start >= 0 && end > start) {
55
+ try { const v = JSON.parse(raw.slice(start, end + 1)); if (Array.isArray(v)) return v; } catch { /* 放弃 */ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ // 清洗生成结果:字段类型归一 + 长度截断 + 空名补名 + 重名后缀 + 数量上限
61
+ // existingNames:现有智能体名(含管家),重名自动加序号,保证 @ 点名无歧义
62
+ function cleanSuggestedTeam(arr, existingNames) {
63
+ if (!Array.isArray(arr)) return [];
64
+ const names = new Set(['管家', 'butler']);
65
+ for (const n of existingNames || []) {
66
+ const s = String(n || '').trim();
67
+ if (s) names.add(s);
68
+ }
69
+ const clean = [];
70
+ for (const a of arr) {
71
+ if (!a || typeof a !== 'object') continue;
72
+ let name = String(a.name || '').replace(/\s+/g, '').slice(0, LIMITS.name);
73
+ if (!name) name = `智能体${clean.length + 1}`;
74
+ let final = name;
75
+ let i = 2;
76
+ while (names.has(final)) final = `${name}${i++}`;
77
+ names.add(final);
78
+ clean.push({
79
+ name: final,
80
+ icon: String(a.icon || '').trim().slice(0, LIMITS.icon),
81
+ desc: String(a.desc || '').slice(0, LIMITS.desc),
82
+ systemPrompt: String(a.systemPrompt || '').slice(0, LIMITS.systemPrompt)
83
+ });
84
+ if (clean.length >= TEAM_MAX) break;
85
+ }
86
+ return clean;
87
+ }
88
+
89
+ // 完整生成链路:调内核一次 → 容错解析 → 清洗 → 数量校验
90
+ // 返回 { success, agents, error };任何失败都不影响现有配置(调用方不落盘)
91
+ async function suggestTeam({ requirements, existingNames, runAgentFn, timeoutMs }) {
92
+ const run = runAgentFn || agentMod.runAgent;
93
+ const timeout = Number(timeoutMs) > 0 ? Number(timeoutMs) : SUGGEST_TIMEOUT_MS;
94
+ const scope = 'teamgen';
95
+ const prompt = buildPrompt(requirements, existingNames);
96
+
97
+ let settled = false;
98
+ let content = '';
99
+ let runError = '';
100
+ const done = new Promise((resolve) => {
101
+ const timer = setTimeout(() => {
102
+ if (settled) return;
103
+ settled = true;
104
+ runError = `生成超时(${Math.round(timeout / 1000)} 秒),请稍后重试或简化需求描述`;
105
+ try { agentMod.stopScope(scope); } catch { /* ignore */ }
106
+ resolve();
107
+ }, timeout);
108
+ try {
109
+ run(
110
+ { id: scope, name: scope, model: '', behavior: 'echo', systemPrompt: '' },
111
+ prompt,
112
+ (chunk) => {
113
+ if (chunk && chunk.content) content += chunk.content;
114
+ if (chunk && chunk.error) runError = runError || String(chunk.error);
115
+ if (chunk && chunk.done && !settled) { settled = true; clearTimeout(timer); resolve(); }
116
+ },
117
+ scope
118
+ );
119
+ } catch (e) {
120
+ if (!settled) { settled = true; clearTimeout(timer); runError = `调用内核失败:${e.message}`; resolve(); }
121
+ }
122
+ });
123
+ await done;
124
+
125
+ if (runError) return { success: false, agents: [], error: runError };
126
+
127
+ const parsed = extractTeamJSON(content);
128
+ if (!parsed) {
129
+ return { success: false, agents: [], error: '生成结果无法解析为团队配置,请换个说法重试(例如补充更多分工细节)' };
130
+ }
131
+ const agents = cleanSuggestedTeam(parsed, existingNames);
132
+ if (agents.length < TEAM_MIN) {
133
+ return { success: false, agents: [], error: `生成结果只有 ${agents.length} 个智能体,至少需要 ${TEAM_MIN} 个,请补充更多分工细节后重试` };
134
+ }
135
+ return { success: true, agents };
136
+ }
137
+
138
+ module.exports = { buildPrompt, extractTeamJSON, cleanSuggestedTeam, suggestTeam, LIMITS, TEAM_MIN, TEAM_MAX, REQ_MAX };
@@ -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, #diffMask { 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, #suggestMask { 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, #diffMask.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, #suggestMask.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,15 @@
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; }
264
273
  /* Git 改动视图:行级着色 */
265
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; }
266
275
  .diff-pre .dl { padding: 0 4px; border-radius: 3px; }
@@ -516,6 +525,24 @@
516
525
  </div>
517
526
  </div>
518
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
+
519
546
  <!-- 任务 Git 改动弹窗(隔离区 diff / 合并 / 丢弃) -->
520
547
  <div id="diffMask">
521
548
  <div class="modal" style="max-width:860px">
@@ -561,6 +588,7 @@
561
588
  <div class="tip">每个智能体都是一个独立的 AI Agent:执行内核由顶部下拉框统一选择(OpenCode / Claude Code / Codex / pi),<b>图标</b>用于辨认,<b>名称</b>用于 @ 点名,<b>技能说明</b>供管家调度参考,<b>自定义提示词</b>会在任务下发时附加在用户提示词前面。所有智能体使用所选内核的默认模型。「管家」为内置调度智能体,不可修改。</div>
562
589
  <div class="butler-ro" id="butlerRo"></div>
563
590
  <div class="ag-row" style="margin:8px 0 2px">
591
+ <button class="add-agent" style="flex:1" onclick="openSuggest()">🪄 AI 组队 · 一句话生成团队</button>
564
592
  <button class="add-agent" style="flex:1" onclick="openTeams()">📦 团队仓库 · 一键换经典团队</button>
565
593
  </div>
566
594
  <div class="global-cwd-box">
@@ -631,6 +659,25 @@
631
659
  </div>
632
660
  </div>
633
661
  </div>
662
+ <!-- AI 智能组队弹窗 -->
663
+ <div id="suggestMask">
664
+ <div class="modal wide">
665
+ <h3>🪄 AI 组队</h3>
666
+ <div class="tip">用一句话描述你的需求,AI 扮演团队设计师,生成一套分工明确的智能体团队(名称/图标/技能说明/提示词)。先预览再应用,应用后仍可自由增删改,点配置页「保存」才生效。</div>
667
+ <div class="ta-wrap"><textarea id="sgReqInput" style="min-height:88px" placeholder="例如:我要从零做一个跨境电商独立站,需要市场调研、选品分析、网站前后端开发、内容文案和上线测试,帮我组建一支能覆盖全流程的团队"></textarea></div>
668
+ <div class="ag-row" style="margin:8px 0">
669
+ <button id="sgRunBtn" class="primary" style="flex:1" onclick="runSuggest()">✨ 生成团队</button>
670
+ </div>
671
+ <div id="sgResult"></div>
672
+ <div class="modal-actions">
673
+ <button class="mini-btn" onclick="runSuggest()">🔄 重新生成</button>
674
+ <span style="flex:1"></span>
675
+ <button onclick="closeSuggest()">放弃</button>
676
+ <button class="mini-btn" style="background:#07c160;color:#fff" onclick="applySuggest('append')">+ 应用(合并到现有团队)</button>
677
+ <button class="primary" onclick="applySuggest('replace')">应用(替换全部子智能体)</button>
678
+ </div>
679
+ </div>
680
+ </div>
634
681
  <!-- 聊天历史弹窗 -->
635
682
  <div id="historyMask">
636
683
  <div class="modal wide">
@@ -675,6 +722,8 @@
675
722
  <h3>❓ 使用帮助</h3>
676
723
  <div id="helpBody"></div>
677
724
  <div class="modal-actions">
725
+ <button class="mini-btn" onclick="closeHelp(); openWelcome()">👋 重新查看新手引导</button>
726
+ <span style="flex:1"></span>
678
727
  <button class="primary" onclick="closeHelp()">关闭</button>
679
728
  </div>
680
729
  </div>
@@ -720,7 +769,7 @@
720
769
  </div>
721
770
 
722
771
  <script>
723
- const PAGE_VERSION = '3.32.0'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
772
+ const PAGE_VERSION = '3.34.0'; // 与服务端 /api/health.version 互检,不一致说明页面缓存过期
724
773
  const AV_COLORS = ['#5b8def','#07c160','#fa9d3b','#10aeff','#8a6fe8','#fa5151','#e8a33d','#3aa7a3'];
725
774
  const PHASE_LABEL = { plan: '调度规划', work: '执行', review: '验收', report: '汇总', talk: '圆桌发言', task: '任务执行' };
726
775
  // 职业图标库(智能体配置可选)
@@ -839,6 +888,15 @@ async function init() {
839
888
  renderUpdateInfo(health);
840
889
  refreshNotifyBtn();
841
890
 
891
+ // 新手引导:从未使用过(主会话无消息、无任务、本机未看过引导)→ 自动弹欢迎
892
+ try {
893
+ if (!localStorage.getItem(ONBOARD_KEY)
894
+ && getSess('').length === 0
895
+ && tasks.filter(t => t.kind !== 'scheduled').length === 0) {
896
+ openWelcome();
897
+ }
898
+ } catch { /* localStorage 不可用时跳过 */ }
899
+
842
900
  input.addEventListener('input', handleAtInput);
843
901
  input.addEventListener('blur', () => setTimeout(hideAt, 150));
844
902
  // 消息区滚动:贴底时收起「新消息」悬浮按钮
@@ -1483,7 +1541,7 @@ function renderTasks() {
1483
1541
  ${wtBtn(t)}
1484
1542
  <button class="mini-btn danger" title="删除任务及其会话" onclick="event.stopPropagation();delTask('${escAttr(t.id)}')">✕</button>
1485
1543
  </div>`;
1486
- }).join('') : '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>(单聊任务由 OpenCode 单体执行)</div>';
1544
+ }).join('') : '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>(单聊任务由 OpenCode 单体执行)<br><button class="mini-btn" style="margin-top:6px" onclick="seedDemoTasks()">🚀 试试示例任务</button></div>';
1487
1545
  }
1488
1546
  el.innerHTML = `<div class="list-sep">${taskView === 'sched' ? '定时任务(单聊 · 到点自动执行)' : '单聊任务(点击查看会话,可拖拽排序)'}</div>` + tasksHtml;
1489
1547
  return;
@@ -1531,7 +1589,7 @@ function renderTasks() {
1531
1589
 
1532
1590
  let tasksHtml;
1533
1591
  if (vis.length === 0) {
1534
- tasksHtml = '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>定时任务请切到上方「⏰ 定时任务」</div>';
1592
+ tasksHtml = '<div class="task-empty">暂无顺序任务<br>点上方「导入任务」添加<br>定时任务请切到上方「⏰ 定时任务」<br><button class="mini-btn" style="margin-top:6px" onclick="seedDemoTasks()">🚀 试试示例任务</button></div>';
1535
1593
  } else {
1536
1594
  tasksHtml = vis.map((t) => {
1537
1595
  const icon = t.status === 'done' ? '✓' : t.status === 'failed' ? '!' : t.status === 'running' ? '▶' : '•';
@@ -1974,6 +2032,18 @@ function renderMessages() {
1974
2032
  box.appendChild(node);
1975
2033
  finalizeMsgLinks(all[i]); // 定稿消息中的 .md 路径变成可点击预览链接
1976
2034
  }
2035
+ // 主会话空状态:欢迎卡 + 快捷动作(新用户零点击成本看到入口)
2036
+ if (curTaskId === '' && chatMode === 'group' && all.length === 0) {
2037
+ box.innerHTML = `<div class="main-welcome">
2038
+ <h4>👋 今天想让团队做点什么?</h4>
2039
+ <p>直接在下方输入需求,管家会拆解调度智能体团队协作完成;<br>也可以先导入一批任务批量执行,或点「试试示例」看完整流程。</p>
2040
+ <div class="onb-acts">
2041
+ <button class="primary" onclick="seedDemoTasks()">🚀 试试示例</button>
2042
+ <button onclick="openImport()">📥 导入任务</button>
2043
+ <button onclick="openHelp()">📖 查看帮助</button>
2044
+ </div>
2045
+ </div>`;
2046
+ }
1977
2047
  scrollBottom();
1978
2048
  requestAnimationFrame(() => requestAnimationFrame(() => box.classList.remove('bulk')));
1979
2049
  const jb = document.getElementById('jumpBtn'); if (jb) jb.style.display = 'none';
@@ -2693,6 +2763,41 @@ async function discardTaskDiff() {
2693
2763
  else toast(data.error || '丢弃失败');
2694
2764
  }
2695
2765
 
2766
+ // ---------- 新手引导与一键示例 ----------
2767
+ const ONBOARD_KEY = 'acOnboarded';
2768
+ function closeWelcome(markSeen) {
2769
+ document.getElementById('welcomeMask').classList.remove('show');
2770
+ if (markSeen) try { localStorage.setItem(ONBOARD_KEY, '1'); } catch { /* 隐私模式忽略 */ }
2771
+ }
2772
+ function openWelcome() {
2773
+ const tip = document.getElementById('onbKernelTip');
2774
+ const rs = runnerState || {};
2775
+ if (rs.kind === 'demo') tip.innerHTML = '🧪 当前为<b>演示模式</b>(模拟输出,无需任何配置即可体验完整流程);安装 OpenCode 后即为真实执行';
2776
+ else if (rs.kind === 'missing') tip.innerHTML = '⚠ 未检测到执行内核,真实任务前需安装:npm install -g opencode-ai(详见帮助)';
2777
+ else tip.innerHTML = `✅ 运行内核就绪:${rs.label || 'OpenCode'}`;
2778
+ document.getElementById('welcomeMask').classList.add('show');
2779
+ }
2780
+ // 一键体验:自动导入一组示例任务并立即执行(群聊=管家编排双任务;单聊=OpenCode 续聊链)
2781
+ async function seedDemoTasks() {
2782
+ closeWelcome(true);
2783
+ const solo = chatMode === 'solo';
2784
+ const text = solo
2785
+ ? '1. 用一句话介绍你能帮我做什么,再列出你最擅长的三类任务\n2. -继续:针对上面三类任务,各举一个具体例子说明你会怎么完成'
2786
+ : '1. 盘点当前工作目录的文件与子目录结构,输出一份简明的目录说明\n2. 根据上一任务的盘点结果,为这个项目生成一份 README.md 草稿';
2787
+ const data = await api('/api/tasks/import', {
2788
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2789
+ body: JSON.stringify({ text, mode: 'sequential', runner: solo ? 'solo' : '' })
2790
+ });
2791
+ if (!data.success) { toast(data.error || '示例导入失败'); return; }
2792
+ tasks = data.tasks || [];
2793
+ renderSidebar();
2794
+ addSys(`🚀 已导入 ${data.added} 个示例任务,开始体验(随时可在任务列表删除)`);
2795
+ const ids = tasks.filter(t => t.runner === (solo ? 'solo' : '') && t.kind === 'sequential' && t.status === 'pending').map(t => t.id);
2796
+ if (!ids.length) { toast('示例任务已导入,请手动执行'); return; }
2797
+ if (solo) await soloExecTasks(ids.slice(0, 2));
2798
+ else await runTask(ids[0]);
2799
+ }
2800
+
2696
2801
  async function clearTasks() {
2697
2802
  if (!confirm('确定清空全部任务?(任务会话的对话记录也会从列表移除)')) return;
2698
2803
  await api('/api/tasks/clear', { method: 'POST' });
@@ -3269,6 +3374,94 @@ function applyTeamPreset(i, mode) {
3269
3374
  toast(`已应用「${t.name}」(${mode === 'replace' ? '替换' : '追加'} ${incoming.length} 个智能体),点「保存」后生效`);
3270
3375
  }
3271
3376
 
3377
+ // ---------- AI 智能组队:一句话需求 → 内核生成团队 → 预览应用 ----------
3378
+ let sgDraft = []; // 预览态候选(仅前端内存,应用才进 editing)
3379
+
3380
+ function openSuggest() {
3381
+ document.getElementById('sgResult').innerHTML = '';
3382
+ sgDraft = [];
3383
+ document.getElementById('suggestMask').classList.add('show');
3384
+ setTimeout(() => document.getElementById('sgReqInput').focus(), 50);
3385
+ }
3386
+ function closeSuggest() { document.getElementById('suggestMask').classList.remove('show'); }
3387
+
3388
+ async function runSuggest() {
3389
+ const reqEl = document.getElementById('sgReqInput');
3390
+ const requirements = String(reqEl.value || '').trim();
3391
+ if (!requirements) { toast('先描述一下你的需求,AI 才知道要组什么团队'); reqEl.focus(); return; }
3392
+ const btn = document.getElementById('sgRunBtn');
3393
+ const out = document.getElementById('sgResult');
3394
+ btn.disabled = true;
3395
+ btn.textContent = '⏳ 生成中…(AI 正在设计团队分工,最长约 2 分钟)';
3396
+ out.innerHTML = '<div class="task-empty" style="padding:16px">AI 正在根据你的需求设计团队分工,请稍候…</div>';
3397
+ sgDraft = [];
3398
+ try {
3399
+ const r = await api('/api/agents/suggest', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ requirements }) });
3400
+ if (r.success) {
3401
+ sgDraft = r.agents || [];
3402
+ renderSuggestCards();
3403
+ toast(`已生成 ${sgDraft.length} 个智能体,确认后点下方「应用」`);
3404
+ } else {
3405
+ out.innerHTML = `<div class="task-empty" style="padding:16px;color:#c0392b">❌ ${esc(r.error || '生成失败,请稍后重试')}</div>`;
3406
+ }
3407
+ } catch (e) {
3408
+ out.innerHTML = `<div class="task-empty" style="padding:16px;color:#c0392b">❌ 生成失败:${esc(e.message)}</div>`;
3409
+ } finally {
3410
+ btn.disabled = false;
3411
+ btn.textContent = '✨ 生成团队';
3412
+ }
3413
+ }
3414
+
3415
+ function renderSuggestCards() {
3416
+ const el = document.getElementById('sgResult');
3417
+ if (!sgDraft.length) {
3418
+ el.innerHTML = '<div class="task-empty" style="padding:16px">暂无生成结果,输入需求后点「生成团队」</div>';
3419
+ return;
3420
+ }
3421
+ el.innerHTML = sgDraft.map((a, i) => `
3422
+ <div class="ag-card">
3423
+ <div class="ag-row">
3424
+ <span style="font-size:20px;align-self:center">${esc(a.icon || '🤖')}</span>
3425
+ <b style="align-self:center;flex:1">${esc(a.name)}</b>
3426
+ <button class="mini-btn gray" onclick="sgDraft.splice(${i},1);renderSuggestCards()">删除</button>
3427
+ </div>
3428
+ <input value="${escAttr(a.desc || '')}" placeholder="技能说明" oninput="sgDraft[${i}].desc=this.value">
3429
+ <details>
3430
+ <summary style="font-size:12px;color:#666;cursor:pointer;margin:4px 0">自定义提示词(点击展开查看,可直接修改)</summary>
3431
+ <textarea oninput="sgDraft[${i}].systemPrompt=this.value">${esc(a.systemPrompt || '')}</textarea>
3432
+ </details>
3433
+ </div>`).join('');
3434
+ }
3435
+
3436
+ // 应用候选:append=合并到现有名单(重名自动加序号);replace=替换全部子智能体
3437
+ function applySuggest(mode) {
3438
+ if (!sgDraft.length) { toast('还没有可应用的生成结果'); return; }
3439
+ const incoming = sgDraft.map(a => ({
3440
+ id: '', behavior: 'echo',
3441
+ name: String(a.name || '').trim(),
3442
+ icon: String(a.icon || ''),
3443
+ desc: String(a.desc || ''),
3444
+ systemPrompt: String(a.systemPrompt || '')
3445
+ })).filter(a => a.name);
3446
+ if (!incoming.length) { toast('候选智能体缺少名称,无法应用'); return; }
3447
+ if (mode === 'replace') {
3448
+ editing = incoming;
3449
+ } else {
3450
+ const names = new Set(['管家', 'butler', ...editing.map(a => String(a.name || '').trim())]);
3451
+ for (const a of incoming) {
3452
+ let final = a.name, n = 2;
3453
+ while (names.has(final)) final = `${a.name}${n++}`;
3454
+ a.name = final;
3455
+ names.add(final);
3456
+ editing.push(a);
3457
+ }
3458
+ }
3459
+ sgDraft = [];
3460
+ renderAgentCards();
3461
+ closeSuggest();
3462
+ toast(`已应用 AI 生成的团队(${mode === 'replace' ? '替换' : '合并'} ${incoming.length} 个智能体),点配置页「保存」后生效`);
3463
+ }
3464
+
3272
3465
  function iconPicker(i) {
3273
3466
  const cur = editing[i].icon || '';
3274
3467
  return '<div class="icon-row">' + ICON_PRESETS.map(ic =>
@@ -3520,6 +3713,7 @@ function openHelp() {
3520
3713
  <li>本机安装任一执行内核并登录模型(模型与密钥由内核自己管理,本程序不做任何 API 配置):<br>
3521
3714
  <code>${KERNEL_INSTALL.opencode}</code><br><code>${KERNEL_INSTALL.claude}</code><br><code>${KERNEL_INSTALL.codex}</code><br><code>${KERNEL_INSTALL.pi}</code></li>
3522
3715
  <li>启动本程序后浏览器打开 <code>http://localhost:3456</code>,会自动识别已安装的内核</li>
3716
+ <li>不知道配什么团队?「⚙ 智能体配置」里点「🪄 AI 组队」,一句话描述需求,AI 自动生成整套智能体团队(先预览再应用);也可从「📦 团队仓库」一键换经典团队</li>
3523
3717
  <li>直接在输入框说话即可开始;粘贴任务清单可批量执行</li>
3524
3718
  </ol>
3525
3719
  </div>
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 ----------
@@ -64,7 +69,9 @@ process.on('unhandledRejection', (err) => {
64
69
  const PORT = Number(process.argv.includes('--port') ? process.argv[process.argv.indexOf('--port') + 1] : (process.env.PORT || 3456));
65
70
  const PUBLIC_DIR = path.join(__dirname, 'public');
66
71
  const store = require('./lib/store');
67
- const { runAgent, stopScope, stopAllChildren } = require('./lib/agent');
72
+ const { runAgent, resolveRunner, missingHint, stopScope, stopAllChildren } = require('./lib/agent');
73
+ const teamgen = require('./lib/teamgen');
74
+ let suggestInFlight = false; // AI 组队生成互斥:同刻仅一个(内核单次调用,防并发浪费)
68
75
  const { runButler, runMentioned, runRoundtable, runTasks, prepareRerun } = require('./lib/orchestrator');
69
76
  const oc = require('./lib/oc');
70
77
  const memoryMod = require('./lib/memory');
@@ -593,6 +600,39 @@ const server = http.createServer(async (req, res) => {
593
600
  return;
594
601
  }
595
602
 
603
+ if (p === '/api/agents/suggest' && req.method === 'POST') {
604
+ // AI 智能组队:需求文本 → 执行内核生成团队配置(预览态,不落盘;应用走 /api/agents 保存链路)
605
+ const body = await readBody(req);
606
+ const requirements = String(body.requirements || '').trim();
607
+ if (!requirements || requirements.length > teamgen.REQ_MAX) {
608
+ json(res, 400, { success: false, error: `需求描述不能为空且不超过 ${teamgen.REQ_MAX} 字` });
609
+ return;
610
+ }
611
+ if (suggestInFlight) {
612
+ json(res, 409, { success: false, error: '上一次生成还在进行中,请稍候' });
613
+ return;
614
+ }
615
+ const runner = resolveRunner();
616
+ if (runner.kind === 'demo' || runner.kind === 'missing') {
617
+ const why = runner.kind === 'demo'
618
+ ? '当前为演示模式(AGENTS_CHAT_MOCK=1),删除 .env 中该配置并重启本程序后即可使用真实生成'
619
+ : missingHint(runner);
620
+ json(res, 200, { success: false, error: `AI 组队需要真实执行内核:${why}` });
621
+ return;
622
+ }
623
+ suggestInFlight = true;
624
+ try {
625
+ const r = await teamgen.suggestTeam({
626
+ requirements,
627
+ existingNames: store.getAgents().map(a => a.name)
628
+ });
629
+ json(res, 200, r);
630
+ } finally {
631
+ suggestInFlight = false;
632
+ }
633
+ return;
634
+ }
635
+
596
636
  if (p === '/api/tasks' && req.method === 'GET') {
597
637
  // 按 seq(拖拽可调)排序返回
598
638
  json(res, 200, { success: true, tasks: store.getTasks() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamsamyiok/agents-chat",
3
- "version": "3.32.0",
3
+ "version": "3.34.0",
4
4
  "description": "多智能体群聊工具 - 支持 OpenCode/Claude Code/Codex/pi 内核,微信风格聊天界面",
5
5
  "main": "lib/start.js",
6
6
  "bin": {
@@ -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
  }