@mumulinya167/cc-web 1.0.0 → 1.0.1

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/dist/server.js ADDED
@@ -0,0 +1,4479 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // ccm Web 控制台服务器
4
+ // 零依赖,使用 Node.js 原生 http 模块
5
+ const http = require("http");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const { execSync, spawn } = require("child_process");
9
+ const os = require("os");
10
+ const url = require("url");
11
+ const CCM_DIR = path.join(os.homedir(), ".cc-connect");
12
+ const CONFIGS_DIR = path.join(CCM_DIR, "configs");
13
+ const PID_DIR = path.join(CCM_DIR, "pids");
14
+ const LOG_DIR = path.join(CCM_DIR, "logs");
15
+ const SESSIONS_DIR = path.join(CCM_DIR, "sessions");
16
+ const SHARED_DIR = path.join(CCM_DIR, "shared");
17
+ const TASKS_FILE = path.join(CCM_DIR, "tasks.json");
18
+ const CRON_FILE = path.join(CCM_DIR, "cron-jobs.json");
19
+ const UPLOAD_DIR = path.join(CCM_DIR, "uploads");
20
+ const GROUPS_FILE = path.join(CCM_DIR, "groups.json");
21
+ const GROUP_MESSAGES_DIR = path.join(CCM_DIR, "group-messages");
22
+ const PUBLIC_DIR = path.resolve(__dirname, "..", "public");
23
+ // 确保上传目录存在
24
+ if (!fs.existsSync(UPLOAD_DIR))
25
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true });
26
+ if (!fs.existsSync(GROUP_MESSAGES_DIR))
27
+ fs.mkdirSync(GROUP_MESSAGES_DIR, { recursive: true });
28
+ // === 群聊管理 ===
29
+ function loadGroups() {
30
+ if (!fs.existsSync(GROUPS_FILE))
31
+ return [];
32
+ try {
33
+ return JSON.parse(fs.readFileSync(GROUPS_FILE, "utf-8"));
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ }
39
+ function saveGroups(groups) {
40
+ fs.writeFileSync(GROUPS_FILE, JSON.stringify(groups, null, 2));
41
+ }
42
+ function getGroupMessages(groupId) {
43
+ const file = path.join(GROUP_MESSAGES_DIR, `${groupId}.json`);
44
+ if (!fs.existsSync(file))
45
+ return [];
46
+ try {
47
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ function appendGroupMessage(groupId, msg) {
54
+ const messages = getGroupMessages(groupId);
55
+ messages.push(msg);
56
+ fs.writeFileSync(path.join(GROUP_MESSAGES_DIR, `${groupId}.json`), JSON.stringify(messages, null, 2));
57
+ }
58
+ function callAgent(projectName, message, workDir, agentType, timeoutMs) {
59
+ const safeCwd = (workDir || process.cwd()).replace(/\\/g, "/");
60
+ const tmpMsg = path.join(UPLOAD_DIR, `_msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}.txt`);
61
+ fs.writeFileSync(tmpMsg, message, "utf-8");
62
+ let cmd;
63
+ switch (agentType) {
64
+ case "cursor":
65
+ cmd = `type "${tmpMsg}" | agent -p`;
66
+ break;
67
+ case "gemini":
68
+ cmd = `type "${tmpMsg}" | gemini -p`;
69
+ break;
70
+ case "codex":
71
+ cmd = `type "${tmpMsg}" | codex -q`;
72
+ break;
73
+ default:
74
+ cmd = `type "${tmpMsg}" | claude -p`;
75
+ break;
76
+ }
77
+ try {
78
+ const result = execSync(cmd, {
79
+ encoding: "utf-8",
80
+ timeout: timeoutMs || 300000, // 默认 5 分钟
81
+ cwd: safeCwd,
82
+ shell: true,
83
+ maxBuffer: 10 * 1024 * 1024,
84
+ });
85
+ try {
86
+ fs.unlinkSync(tmpMsg);
87
+ }
88
+ catch { }
89
+ return result.trim();
90
+ }
91
+ catch (e) {
92
+ try {
93
+ fs.unlinkSync(tmpMsg);
94
+ }
95
+ catch { }
96
+ if (e.killed || e.signal === "SIGTERM")
97
+ return `[${projectName}] Agent 响应超时,请稍后重试`;
98
+ return `[${projectName}] Agent 错误: ${(e.stderr || e.message || "").substring(0, 200)}`;
99
+ }
100
+ }
101
+ // 流式调用 Agent(SSE)
102
+ function callAgentStream(projectName, message, workDir, agentType, res) {
103
+ const safeCwd = (workDir || process.cwd()).replace(/\\/g, "/");
104
+ const tmpMsg = path.join(UPLOAD_DIR, `_msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}.txt`);
105
+ fs.writeFileSync(tmpMsg, message, "utf-8");
106
+ let cmd;
107
+ switch (agentType) {
108
+ case "cursor":
109
+ cmd = `type "${tmpMsg}" | agent -p`;
110
+ break;
111
+ case "gemini":
112
+ cmd = `type "${tmpMsg}" | gemini -p`;
113
+ break;
114
+ case "codex":
115
+ cmd = `type "${tmpMsg}" | codex -q`;
116
+ break;
117
+ default:
118
+ cmd = `type "${tmpMsg}" | claude -p`;
119
+ break;
120
+ }
121
+ // 设置 SSE
122
+ res.writeHead(200, {
123
+ "Content-Type": "text/event-stream",
124
+ "Cache-Control": "no-cache",
125
+ "Connection": "keep-alive",
126
+ "Access-Control-Allow-Origin": "*",
127
+ });
128
+ // 发送状态事件
129
+ res.write(`data: ${JSON.stringify({ type: "status", text: "Agent 正在思考..." })}\n\n`);
130
+ const child = spawn(cmd, [], { shell: true, cwd: safeCwd, stdio: ["pipe", "pipe", "pipe"] });
131
+ // 关闭 stdin(已通过临时文件传入)
132
+ child.stdin.end();
133
+ let buffer = "";
134
+ let charCount = 0;
135
+ child.stdout.on("data", (chunk) => {
136
+ const text = chunk.toString("utf-8");
137
+ buffer += text;
138
+ charCount += text.length;
139
+ // 每收到数据就发送
140
+ if (charCount > 10) {
141
+ res.write(`data: ${JSON.stringify({ type: "chunk", text: buffer })}\n\n`);
142
+ buffer = "";
143
+ charCount = 0;
144
+ }
145
+ });
146
+ child.stderr.on("data", (chunk) => {
147
+ const text = chunk.toString("utf-8");
148
+ if (text.trim()) {
149
+ res.write(`data: ${JSON.stringify({ type: "status", text: "Agent 处理中..." })}\n\n`);
150
+ }
151
+ });
152
+ child.on("close", () => {
153
+ try {
154
+ fs.unlinkSync(tmpMsg);
155
+ }
156
+ catch { }
157
+ if (buffer) {
158
+ res.write(`data: ${JSON.stringify({ type: "chunk", text: buffer })}\n\n`);
159
+ }
160
+ res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
161
+ res.end();
162
+ });
163
+ child.on("error", (err) => {
164
+ try {
165
+ fs.unlinkSync(tmpMsg);
166
+ }
167
+ catch { }
168
+ res.write(`data: ${JSON.stringify({ type: "error", text: err.message })}\n\n`);
169
+ res.end();
170
+ });
171
+ // 超时处理
172
+ setTimeout(() => {
173
+ try {
174
+ child.kill();
175
+ }
176
+ catch { }
177
+ try {
178
+ fs.unlinkSync(tmpMsg);
179
+ }
180
+ catch { }
181
+ res.write(`data: ${JSON.stringify({ type: "error", text: "Agent 响应超时" })}\n\n`);
182
+ res.end();
183
+ }, 300000);
184
+ }
185
+ // 异步处理跨 Agent 协作调用
186
+ function processCrossAgents(groupId, group, sourceProject, output, atMentions, configs) {
187
+ console.log(`[跨Agent协作] 源: ${sourceProject}, 检测到 @mentions: ${atMentions.join(", ")}`);
188
+ // 去重 mentions
189
+ const uniqueMentions = [...new Set(atMentions)];
190
+ console.log(`[跨Agent协作] 去重后 mentions: ${uniqueMentions.join(", ")}`);
191
+ for (const mention of uniqueMentions) {
192
+ // 确保 mention 是 @ 开头的格式
193
+ const mentionStr = String(mention);
194
+ const targetName = mentionStr.startsWith("@") ? mentionStr.slice(1) : mentionStr;
195
+ console.log(`[跨Agent协作] 处理 @${targetName}`);
196
+ const targetMember = group.members.find(m => m.project === targetName && m.project !== sourceProject);
197
+ if (!targetMember) {
198
+ console.log(`[跨Agent协作] @${targetName} 不在群聊成员中,跳过`);
199
+ continue;
200
+ }
201
+ // 提取 @mention 后面的消息(支持多行)
202
+ const atRegex = new RegExp(`@${targetName}\\s+([^@]+?)(?=\\s*@|$)`, "is");
203
+ const atMatch = output.match(atRegex);
204
+ let atMessage = atMatch ? atMatch[1].trim() : "";
205
+ // 如果没有提取到具体消息,尝试提取 @mention 所在段落
206
+ if (!atMessage || atMessage.length < 5) {
207
+ const lines = output.split("\n");
208
+ const relevantLines = [];
209
+ let found = false;
210
+ for (const line of lines) {
211
+ if (line.includes(`@${targetName}`)) {
212
+ found = true;
213
+ relevantLines.push(line.replace(`@${targetName}`, "").trim());
214
+ }
215
+ else if (found && line.trim() && !line.startsWith("@")) {
216
+ relevantLines.push(line.trim());
217
+ }
218
+ else if (found && line.includes("@")) {
219
+ break;
220
+ }
221
+ }
222
+ atMessage = relevantLines.join("\n").trim() || output.substring(0, 500);
223
+ }
224
+ console.log(`[跨Agent协作] 提取的消息: ${atMessage.substring(0, 100)}...`);
225
+ // 记录转发消息
226
+ appendGroupMessage(groupId, {
227
+ id: "m" + Date.now().toString(36) + "fwd",
228
+ role: "assistant", agent: sourceProject,
229
+ content: `📤 → @${targetName}\n${atMessage}`,
230
+ timestamp: new Date().toISOString(),
231
+ });
232
+ // 调用目标 Agent
233
+ const targetConfig = configs.find(c => c.name === targetName);
234
+ if (!targetConfig) {
235
+ console.log(`[跨Agent协作] @${targetName} 的配置不存在`);
236
+ continue;
237
+ }
238
+ console.log(`[跨Agent协作] 找到 @${targetName} 的配置,准备调用`);
239
+ const tInfo = getConfigInfo(targetConfig.path);
240
+ const tWorkDir = tInfo[0]?.workDir;
241
+ const tAgentType = tInfo[0]?.agent || "claudecode";
242
+ console.log(`[跨Agent协作] 目标 Agent: ${targetName}, 工作目录: ${tWorkDir}, 类型: ${tAgentType}`);
243
+ // 获取完整上下文
244
+ const tContext = getGroupMessages(groupId).slice(-15).map(m => {
245
+ const who = m.role === "user" ? `[用户 → ${m.target}]` : `[${m.agent || "Agent"}]`;
246
+ return `${who} ${m.content}`;
247
+ }).join("\n");
248
+ // 构建详细的 prompt
249
+ const memberList = group.members.map(m => m.project).filter(p => p !== targetName && p !== "coordinator").join(", ");
250
+ const tPrompt = `你是一个在群聊中协作的开发 Agent,项目: ${targetName}。
251
+
252
+ ${memberList ? `群聊中还有其他 Agent:${memberList}。如果你需要其他 Agent 协助,在回复中用 @项目名 的格式提出请求。` : ""}
253
+
254
+ 以下是群聊最近的消息记录:
255
+ ${tContext}
256
+
257
+ ${sourceProject} 刚才 @ 了你,请根据上下文回复他的请求:
258
+ ${atMessage}
259
+
260
+ 请直接回复,不要重复上下文内容。`;
261
+ console.log(`[跨Agent协作] 调用 Agent ${targetName}...`);
262
+ try {
263
+ const tOutput = callAgent(targetName, tPrompt, tWorkDir, tAgentType, 300000);
264
+ console.log(`[跨Agent协作] Agent ${targetName} 回复: ${tOutput.substring(0, 100)}...`);
265
+ appendGroupMessage(groupId, {
266
+ id: "m" + Date.now().toString(36) + "cross",
267
+ role: "assistant", agent: targetName,
268
+ content: tOutput,
269
+ timestamp: new Date().toISOString(),
270
+ });
271
+ // 检查目标 Agent 的回复是否也包含 @mention,递归处理
272
+ const nestedMentions = tOutput.match(/@[\w-]+/g) || [];
273
+ if (nestedMentions.length > 0) {
274
+ // 避免无限递归:只处理不是来源项目的 mention
275
+ const newMentions = nestedMentions.filter(m => m.slice(1) !== sourceProject && m.slice(1) !== targetName);
276
+ if (newMentions.length > 0) {
277
+ console.log(`[跨Agent协作] Agent ${targetName} 的回复包含 @mentions: ${newMentions.join(", ")},递归处理`);
278
+ setTimeout(() => processCrossAgents(groupId, group, targetName, tOutput, newMentions, configs), 1000);
279
+ }
280
+ }
281
+ }
282
+ catch (error) {
283
+ console.error(`[跨Agent协作] 调用 Agent ${targetName} 失败:`, error.message);
284
+ // 记录错误消息
285
+ appendGroupMessage(groupId, {
286
+ id: "m" + Date.now().toString(36) + "err",
287
+ role: "assistant", agent: "system",
288
+ content: `❌ 转发给 @${targetName} 失败: ${error.message}`,
289
+ timestamp: new Date().toISOString(),
290
+ });
291
+ }
292
+ }
293
+ }
294
+ // === Multipart 解析 ===
295
+ function parseMultipart(buffer, boundary) {
296
+ const files = [];
297
+ const fields = {};
298
+ const boundaryBuf = Buffer.from(`--${boundary}`);
299
+ const parts = [];
300
+ let start = buffer.indexOf(boundaryBuf) + boundaryBuf.length + 2; // skip \r\n
301
+ while (true) {
302
+ const end = buffer.indexOf(boundaryBuf, start);
303
+ if (end === -1)
304
+ break;
305
+ parts.push(buffer.slice(start, end - 2)); // -2 for \r\n before boundary
306
+ start = end + boundaryBuf.length + 2;
307
+ }
308
+ for (const part of parts) {
309
+ const headerEnd = part.indexOf("\r\n\r\n");
310
+ if (headerEnd === -1)
311
+ continue;
312
+ const headerStr = part.slice(0, headerEnd).toString("utf-8");
313
+ const body = part.slice(headerEnd + 4);
314
+ const nameMatch = headerStr.match(/name="([^"]+)"/);
315
+ const filenameMatch = headerStr.match(/filename="([^"]+)"/);
316
+ if (filenameMatch && nameMatch) {
317
+ const name = nameMatch[1];
318
+ const filename = filenameMatch[1];
319
+ const ext = path.extname(filename);
320
+ const safeName = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`;
321
+ const filePath = path.join(UPLOAD_DIR, safeName);
322
+ fs.writeFileSync(filePath, body);
323
+ files.push({ field: name, filename, savedPath: filePath, size: body.length });
324
+ }
325
+ else if (nameMatch) {
326
+ fields[nameMatch[1]] = body.toString("utf-8");
327
+ }
328
+ }
329
+ return { files, fields };
330
+ }
331
+ const AGENTS = [
332
+ { type: "claudecode", name: "Claude Code" },
333
+ { type: "cursor", name: "Cursor" },
334
+ { type: "gemini", name: "Gemini CLI" },
335
+ { type: "codex", name: "Codex" },
336
+ { type: "qoder", name: "Qoder CLI" },
337
+ { type: "opencode", name: "OpenCode" },
338
+ ];
339
+ // === 数据读取函数 ===
340
+ function getConfigs() {
341
+ if (!fs.existsSync(CONFIGS_DIR))
342
+ return [];
343
+ return fs.readdirSync(CONFIGS_DIR)
344
+ .filter((f) => f.endsWith(".toml"))
345
+ .sort()
346
+ .map((f, i) => ({
347
+ index: i + 1,
348
+ file: f,
349
+ name: f.replace("config-", "").replace(".toml", ""),
350
+ path: path.join(CONFIGS_DIR, f),
351
+ }));
352
+ }
353
+ function getConfigInfo(configPath) {
354
+ const content = fs.readFileSync(configPath, "utf-8");
355
+ const projects = [];
356
+ const lines = content.split("\n");
357
+ let currentProject = null;
358
+ let inPlatformsBlock = false;
359
+ for (const line of lines) {
360
+ const trimmed = line.trim();
361
+ if (trimmed === "[[projects]]") {
362
+ if (currentProject && currentProject.name)
363
+ projects.push(currentProject);
364
+ currentProject = {};
365
+ inPlatformsBlock = false;
366
+ }
367
+ if (currentProject && trimmed.startsWith("name = "))
368
+ currentProject.name = trimmed.split("=")[1].trim().replace(/"/g, "");
369
+ if (currentProject && trimmed.startsWith("work_dir = "))
370
+ currentProject.workDir = trimmed.split("=")[1].trim().replace(/"/g, "");
371
+ if (currentProject && trimmed.startsWith("type = ") && !inPlatformsBlock) {
372
+ const v = trimmed.split("=")[1].trim().replace(/"/g, "");
373
+ if (AGENTS.find((a) => a.type === v))
374
+ currentProject.agent = v;
375
+ }
376
+ if (trimmed === "[[projects.platforms]]") {
377
+ inPlatformsBlock = true;
378
+ }
379
+ else if (trimmed.startsWith("[") && !trimmed.startsWith("[projects.platforms")) {
380
+ inPlatformsBlock = false;
381
+ }
382
+ if (currentProject && inPlatformsBlock && trimmed.startsWith("type = ")) {
383
+ const pt = trimmed.split("=")[1].trim().replace(/"/g, "");
384
+ const map = { weixin: "微信", feishu: "飞书", lark: "Lark", telegram: "Telegram", slack: "Slack", discord: "Discord", dingtalk: "钉钉" };
385
+ currentProject.platform = map[pt] || pt;
386
+ inPlatformsBlock = false;
387
+ }
388
+ if (currentProject && (trimmed === "[[commands]]" || trimmed === "[[aliases]]")) {
389
+ if (currentProject.name)
390
+ projects.push(currentProject);
391
+ currentProject = null;
392
+ }
393
+ }
394
+ if (currentProject && currentProject.name)
395
+ projects.push(currentProject);
396
+ return projects;
397
+ }
398
+ function isRunning(name) {
399
+ const pidFile = path.join(PID_DIR, `${name}.pid`);
400
+ if (!fs.existsSync(pidFile))
401
+ return false;
402
+ const pid = fs.readFileSync(pidFile, "utf-8").trim();
403
+ try {
404
+ process.kill(parseInt(pid), 0);
405
+ return true;
406
+ }
407
+ catch {
408
+ try {
409
+ fs.unlinkSync(pidFile);
410
+ }
411
+ catch { }
412
+ return false;
413
+ }
414
+ }
415
+ function getPid(name) {
416
+ const pidFile = path.join(PID_DIR, `${name}.pid`);
417
+ if (!fs.existsSync(pidFile))
418
+ return null;
419
+ return fs.readFileSync(pidFile, "utf-8").trim();
420
+ }
421
+ // === 会话同步层:文件夹格式 ↔ cc-connect 单文件格式 ===
422
+ const WEB_SESSIONS_DIR = path.join(CCM_DIR, "web-sessions");
423
+ function getProjectSessionDir(projectName) {
424
+ return path.join(WEB_SESSIONS_DIR, projectName);
425
+ }
426
+ function ensureWebSessionDir(projectName) {
427
+ const dir = path.join(WEB_SESSIONS_DIR, projectName);
428
+ if (!fs.existsSync(dir))
429
+ fs.mkdirSync(dir, { recursive: true });
430
+ return dir;
431
+ }
432
+ // 查找 cc-connect 的 session 文件(带 hash 的)
433
+ function findCcSessionFile(projectName) {
434
+ if (!fs.existsSync(SESSIONS_DIR))
435
+ return null;
436
+ const files = fs.readdirSync(SESSIONS_DIR).filter(f => f.startsWith(projectName) && f.endsWith(".json") && !fs.statSync(path.join(SESSIONS_DIR, f)).isDirectory());
437
+ const hashed = files.find(f => f !== `${projectName}.json`);
438
+ return hashed ? path.join(SESSIONS_DIR, hashed) : files[0] ? path.join(SESSIONS_DIR, files[0]) : null;
439
+ }
440
+ // 从 cc-connect 单文件同步到文件夹格式
441
+ function syncFromCcToFilesystem(projectName) {
442
+ const ccFile = findCcSessionFile(projectName);
443
+ if (!ccFile || !fs.existsSync(ccFile))
444
+ return;
445
+ try {
446
+ const data = JSON.parse(fs.readFileSync(ccFile, "utf-8"));
447
+ const dir = ensureWebSessionDir(projectName);
448
+ for (const [sid, session] of Object.entries(data.sessions || {})) {
449
+ const sessionData = session;
450
+ const filePath = path.join(dir, `${sid}.json`);
451
+ // 只更新有变化的
452
+ if (!fs.existsSync(filePath) || JSON.parse(fs.readFileSync(filePath, "utf-8")).updated_at !== sessionData.updated_at) {
453
+ fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2));
454
+ }
455
+ }
456
+ // 删除文件夹中已不存在的会话
457
+ const ccSids = new Set(Object.keys(data.sessions || {}));
458
+ for (const f of fs.readdirSync(dir).filter(f => f.endsWith(".json"))) {
459
+ const fid = f.replace(".json", "");
460
+ if (!ccSids.has(fid))
461
+ fs.unlinkSync(path.join(dir, f));
462
+ }
463
+ }
464
+ catch { }
465
+ }
466
+ // 从文件夹格式同步回 cc-connect 单文件
467
+ function syncToFilesystemToCc(projectName) {
468
+ const ccFile = findCcSessionFile(projectName);
469
+ if (!ccFile)
470
+ return;
471
+ try {
472
+ const ccData = JSON.parse(fs.readFileSync(ccFile, "utf-8"));
473
+ const dir = path.join(WEB_SESSIONS_DIR, projectName);
474
+ if (!fs.existsSync(dir))
475
+ return;
476
+ for (const f of fs.readdirSync(dir).filter(f => f.endsWith(".json"))) {
477
+ const sid = f.replace(".json", "");
478
+ const sessionData = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
479
+ ccData.sessions[sid] = sessionData;
480
+ }
481
+ // 更新 counter
482
+ const maxNum = Math.max(0, ...Object.keys(ccData.sessions).map(s => parseInt(s.replace("s", "")) || 0));
483
+ ccData.counter = maxNum + 1;
484
+ fs.writeFileSync(ccFile, JSON.stringify(ccData, null, 2));
485
+ }
486
+ catch { }
487
+ }
488
+ // 双向同步
489
+ function syncSessions(projectName) {
490
+ syncFromCcToFilesystem(projectName);
491
+ }
492
+ // 获取会话列表(从文件夹读取)
493
+ function getSessions(projectName) {
494
+ syncSessions(projectName);
495
+ const dir = path.join(WEB_SESSIONS_DIR, projectName);
496
+ if (!fs.existsSync(dir))
497
+ return [];
498
+ return fs.readdirSync(dir)
499
+ .filter(f => f.endsWith(".json"))
500
+ .map(f => {
501
+ try {
502
+ const data = JSON.parse(fs.readFileSync(path.join(dir, f), "utf-8"));
503
+ return {
504
+ id: data.id || f.replace(".json", ""),
505
+ name: data.name || data.id || f.replace(".json", ""),
506
+ agent_type: data.agent_type || "claudecode",
507
+ message_count: (data.history || []).length,
508
+ last_message: (data.history || []).slice(-1)[0]?.content?.substring(0, 100) || "",
509
+ created_at: data.created_at,
510
+ updated_at: data.updated_at,
511
+ };
512
+ }
513
+ catch {
514
+ return null;
515
+ }
516
+ })
517
+ .filter(Boolean)
518
+ .sort((a, b) => new Date(b.updated_at || 0).getTime() - new Date(a.updated_at || 0).getTime());
519
+ }
520
+ // 获取会话详情
521
+ function getSessionDetail(projectName, sessionId) {
522
+ const filePath = path.join(WEB_SESSIONS_DIR, projectName, `${sessionId}.json`);
523
+ if (fs.existsSync(filePath)) {
524
+ try {
525
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
526
+ }
527
+ catch { }
528
+ }
529
+ // fallback: 从 cc-connect 文件读取
530
+ const ccFile = findCcSessionFile(projectName);
531
+ if (ccFile) {
532
+ try {
533
+ const data = JSON.parse(fs.readFileSync(ccFile, "utf-8"));
534
+ return data.sessions[sessionId] || null;
535
+ }
536
+ catch { }
537
+ }
538
+ return null;
539
+ }
540
+ function getLogs(projectName, lines = 100) {
541
+ const logFile = path.join(LOG_DIR, `${projectName}.log`);
542
+ if (!fs.existsSync(logFile))
543
+ return "";
544
+ const content = fs.readFileSync(logFile, "utf-8");
545
+ return content.split("\n").slice(-lines).join("\n");
546
+ }
547
+ function startProject(projectName, agentType) {
548
+ const configs = getConfigs();
549
+ const config = configs.find((c) => c.name === projectName);
550
+ if (!config)
551
+ return { success: false, error: "项目不存在" };
552
+ if (isRunning(projectName)) {
553
+ return { success: false, error: "项目已在运行" };
554
+ }
555
+ let configPath = config.path;
556
+ // Agent 切换
557
+ if (agentType) {
558
+ let content = fs.readFileSync(configPath, "utf-8");
559
+ content = content.replace(/(\[projects\.agent\]\s*\n\s*type\s*=\s*)"[^"]+"/g, `$1"${agentType}"`);
560
+ const tempPath = path.join(CCM_DIR, "temp", `${projectName}-${agentType}.toml`);
561
+ fs.mkdirSync(path.join(CCM_DIR, "temp"), { recursive: true });
562
+ fs.writeFileSync(tempPath, content);
563
+ configPath = tempPath;
564
+ }
565
+ const logFile = path.join(LOG_DIR, `${projectName}.log`);
566
+ const logStream = fs.openSync(logFile, "w");
567
+ const child = spawn("cc-connect", ["--config", configPath, "--force"], {
568
+ stdio: ["ignore", logStream, logStream],
569
+ shell: true,
570
+ detached: true,
571
+ });
572
+ child.unref();
573
+ const pidDir = PID_DIR;
574
+ if (!fs.existsSync(pidDir))
575
+ fs.mkdirSync(pidDir, { recursive: true });
576
+ fs.writeFileSync(path.join(pidDir, `${projectName}.pid`), String(child.pid));
577
+ return { success: true, pid: child.pid };
578
+ }
579
+ function stopProject(projectName) {
580
+ const pid = getPid(projectName);
581
+ if (!pid)
582
+ return { success: false, error: "项目未在运行" };
583
+ try {
584
+ if (process.platform === "win32") {
585
+ execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore" });
586
+ }
587
+ else {
588
+ process.kill(parseInt(pid), "SIGTERM");
589
+ }
590
+ }
591
+ catch { }
592
+ try {
593
+ fs.unlinkSync(path.join(PID_DIR, `${projectName}.pid`));
594
+ }
595
+ catch { }
596
+ return { success: true };
597
+ }
598
+ // === HTTP 服务 ===
599
+ function sendJson(res, data, status = 200) {
600
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
601
+ res.end(JSON.stringify(data));
602
+ }
603
+ function sendFile(res, filePath) {
604
+ const ext = path.extname(filePath).toLowerCase();
605
+ const types = {
606
+ ".html": "text/html", ".js": "application/javascript", ".css": "text/css",
607
+ ".json": "application/json", ".svg": "image/svg+xml", ".png": "image/png",
608
+ ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif",
609
+ ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2",
610
+ ".ttf": "font/ttf", ".eot": "application/vnd.ms-fontobject",
611
+ ".map": "application/json",
612
+ };
613
+ const contentType = types[ext] || "application/octet-stream";
614
+ if (!fs.existsSync(filePath)) {
615
+ res.writeHead(404);
616
+ res.end("Not Found");
617
+ return;
618
+ }
619
+ const headers = { "Content-Type": contentType };
620
+ if (ext === ".html")
621
+ headers["Content-Type"] = "text/html; charset=utf-8";
622
+ if (ext === ".js" || ext === ".css")
623
+ headers["Cache-Control"] = "public, max-age=31536000, immutable";
624
+ res.writeHead(200, headers);
625
+ fs.createReadStream(filePath).pipe(res);
626
+ }
627
+ // === 共享上下文 ===
628
+ function ensureSharedDir() {
629
+ if (!fs.existsSync(SHARED_DIR))
630
+ fs.mkdirSync(SHARED_DIR, { recursive: true });
631
+ }
632
+ function listSharedFiles() {
633
+ ensureSharedDir();
634
+ return fs.readdirSync(SHARED_DIR)
635
+ .filter(f => !f.startsWith("."))
636
+ .map(f => {
637
+ const stat = fs.statSync(path.join(SHARED_DIR, f));
638
+ const ext = path.extname(f).toLowerCase();
639
+ const isText = [".md", ".txt", ".json", ".csv", ".yaml", ".yml", ".toml", ".xml", ".html", ".css", ".js", ".ts"].includes(ext);
640
+ const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg"].includes(ext);
641
+ return { name: f, size: stat.size, modified: stat.mtime.toISOString(), type: isText ? "text" : isImage ? "image" : "file" };
642
+ })
643
+ .sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
644
+ }
645
+ function readSharedFile(name) {
646
+ const filePath = path.join(SHARED_DIR, name);
647
+ if (!fs.existsSync(filePath))
648
+ return null;
649
+ const ext = path.extname(name).toLowerCase();
650
+ const isText = [".md", ".txt", ".json", ".csv", ".yaml", ".yml", ".toml", ".xml", ".html", ".css", ".js", ".ts"].includes(ext);
651
+ if (isText) {
652
+ return { type: "text", content: fs.readFileSync(filePath, "utf-8") };
653
+ }
654
+ return { type: "binary", size: fs.statSync(filePath).size };
655
+ }
656
+ function writeSharedFile(name, content) {
657
+ ensureSharedDir();
658
+ fs.writeFileSync(path.join(SHARED_DIR, name), content);
659
+ }
660
+ function saveSharedUpload(filename, buffer) {
661
+ ensureSharedDir();
662
+ const safeName = filename.replace(/[<>:"/\\|?*]/g, "_");
663
+ const filePath = path.join(SHARED_DIR, safeName);
664
+ fs.writeFileSync(filePath, buffer);
665
+ return safeName;
666
+ }
667
+ function deleteSharedFile(name) {
668
+ const filePath = path.join(SHARED_DIR, name);
669
+ if (fs.existsSync(filePath))
670
+ fs.unlinkSync(filePath);
671
+ }
672
+ // === 任务派发 ===
673
+ function loadTasks() {
674
+ if (!fs.existsSync(TASKS_FILE))
675
+ return [];
676
+ try {
677
+ return JSON.parse(fs.readFileSync(TASKS_FILE, "utf-8"));
678
+ }
679
+ catch {
680
+ return [];
681
+ }
682
+ }
683
+ function saveTasks(tasks) {
684
+ fs.writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
685
+ }
686
+ function createTask(task) {
687
+ const tasks = loadTasks();
688
+ const newTask = {
689
+ id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
690
+ title: task.title,
691
+ description: task.description || "",
692
+ target_project: task.target_project,
693
+ group_id: task.group_id || null,
694
+ assign_type: task.assign_type || "project",
695
+ status: "pending",
696
+ priority: task.priority || "normal",
697
+ created_at: new Date().toISOString(),
698
+ updated_at: new Date().toISOString(),
699
+ };
700
+ tasks.push(newTask);
701
+ saveTasks(tasks);
702
+ return newTask;
703
+ }
704
+ function updateTask(id, updates) {
705
+ const tasks = loadTasks();
706
+ const idx = tasks.findIndex(t => t.id === id);
707
+ if (idx === -1)
708
+ return null;
709
+ Object.assign(tasks[idx], updates, { updated_at: new Date().toISOString() });
710
+ if (updates.status === "done")
711
+ tasks[idx].completed_at = new Date().toISOString();
712
+ saveTasks(tasks);
713
+ return tasks[idx];
714
+ }
715
+ function deleteTask(id) {
716
+ const tasks = loadTasks().filter(t => t.id !== id);
717
+ saveTasks(tasks);
718
+ }
719
+ // === 任务队列系统(支持并行执行)===
720
+ const taskQueues = new Map(); // 每个目标(群聊/Agent)独立队列
721
+ const runningTasks = new Map(); // 正在运行的任务
722
+ // 获取任务的目标键
723
+ function getTaskTargetKey(task) {
724
+ if (task.assign_type === "group" && task.group_id) {
725
+ return `group:${task.group_id}`;
726
+ }
727
+ return `project:${task.target_project}`;
728
+ }
729
+ // 优先级权重
730
+ const PRIORITY_WEIGHT = { high: 3, normal: 2, low: 1 };
731
+ // 添加任务到队列(按优先级排序)
732
+ function enqueueTask(taskId) {
733
+ const tasks = loadTasks();
734
+ const task = tasks.find(t => t.id === taskId);
735
+ if (!task) {
736
+ console.log(`[任务队列] 任务 ${taskId} 不存在`);
737
+ return;
738
+ }
739
+ const targetKey = getTaskTargetKey(task);
740
+ if (!taskQueues.has(targetKey)) {
741
+ taskQueues.set(targetKey, []);
742
+ }
743
+ const queue = taskQueues.get(targetKey);
744
+ // 按优先级插入到正确位置
745
+ const newPriority = PRIORITY_WEIGHT[task.priority] || 2;
746
+ let insertIndex = queue.length; // 默认插入到末尾
747
+ for (let i = 0; i < queue.length; i++) {
748
+ const queuedTask = tasks.find(t => t.id === queue[i]);
749
+ if (!queuedTask)
750
+ continue;
751
+ const queuedPriority = PRIORITY_WEIGHT[queuedTask.priority] || 2;
752
+ // 高优先级任务插入到低优先级前面
753
+ if (newPriority > queuedPriority) {
754
+ insertIndex = i;
755
+ break;
756
+ }
757
+ }
758
+ queue.splice(insertIndex, 0, taskId);
759
+ console.log(`[任务队列] 任务 ${taskId} (${task.priority}) 已加入队列 [${targetKey}],位置: ${insertIndex + 1}/${queue.length}`);
760
+ // 触发该目标的队列处理
761
+ processTargetQueue(targetKey);
762
+ }
763
+ // 处理特定目标的队列
764
+ async function processTargetQueue(targetKey) {
765
+ // 如果该目标正在执行任务,等待
766
+ if (runningTasks.has(targetKey)) {
767
+ console.log(`[任务队列] [${targetKey}] 正在执行任务,等待中...`);
768
+ return;
769
+ }
770
+ const queue = taskQueues.get(targetKey);
771
+ if (!queue || queue.length === 0) {
772
+ return;
773
+ }
774
+ // 标记该目标正在执行任务
775
+ runningTasks.set(targetKey, true);
776
+ console.log(`[任务队列] [${targetKey}] 开始处理队列,剩余任务: ${queue.length}`);
777
+ while (queue.length > 0) {
778
+ const taskId = queue.shift();
779
+ const tasks = loadTasks();
780
+ const task = tasks.find(t => t.id === taskId);
781
+ if (!task || task.status === "done") {
782
+ addTaskLog(taskId, "info", `跳过任务(不存在或已完成)`);
783
+ continue;
784
+ }
785
+ addTaskLog(taskId, "info", `开始执行任务: ${task.title}`);
786
+ try {
787
+ // 更新状态为进行中
788
+ updateTask(taskId, { status: "in_progress" });
789
+ addTaskLog(taskId, "info", `任务状态更新为: 进行中`);
790
+ // 执行任务
791
+ addTaskLog(taskId, "info", `调用 Agent 执行任务...`);
792
+ const result = await executeTask(task);
793
+ // 记录 Agent 响应
794
+ addTaskLog(taskId, "response", `Agent 响应:\n${result.substring(0, 1000)}`);
795
+ // 检查是否完成
796
+ const isCompleted = checkTaskCompletion(result);
797
+ if (isCompleted) {
798
+ updateTask(taskId, {
799
+ status: "done",
800
+ result: result.substring(0, 500),
801
+ completed_at: new Date().toISOString()
802
+ });
803
+ addTaskLog(taskId, "success", `✅ 任务完成`);
804
+ // 发送飞书通知
805
+ await sendTaskCompletionNotification(task, result);
806
+ }
807
+ else {
808
+ updateTask(taskId, {
809
+ status: "in_progress",
810
+ result: result.substring(0, 500)
811
+ });
812
+ addTaskLog(taskId, "warning", `任务执行中,未检测到完成标记`);
813
+ }
814
+ }
815
+ catch (error) {
816
+ console.error(`[任务队列] [${targetKey}] 任务执行失败: ${task.title}`, error.message);
817
+ updateTask(taskId, {
818
+ status: "pending",
819
+ result: `执行失败: ${error.message}`
820
+ });
821
+ addTaskLog(taskId, "error", `❌ 任务执行失败: ${error.message}`);
822
+ // 发送失败通知
823
+ await sendTaskFailureNotification(task, error.message);
824
+ }
825
+ // 等待一下再执行下一个任务
826
+ await new Promise(resolve => setTimeout(resolve, 500));
827
+ }
828
+ // 标记该目标任务执行完成
829
+ runningTasks.delete(targetKey);
830
+ console.log(`[任务队列] [${targetKey}] 队列处理完成`);
831
+ }
832
+ // 获取队列状态
833
+ function getQueueStatus() {
834
+ let totalQueued = 0;
835
+ const targetStatus = {};
836
+ for (const [targetKey, queue] of taskQueues.entries()) {
837
+ totalQueued += queue.length;
838
+ targetStatus[targetKey] = {
839
+ queued: queue.length,
840
+ running: runningTasks.has(targetKey)
841
+ };
842
+ }
843
+ return {
844
+ total_queued: totalQueued,
845
+ running_targets: runningTasks.size,
846
+ target_status: targetStatus,
847
+ pending_tasks: loadTasks().filter(t => t.status === "pending").length,
848
+ in_progress_tasks: loadTasks().filter(t => t.status === "in_progress").length
849
+ };
850
+ }
851
+ // === 任务日志系统 ===
852
+ const TASK_LOGS_FILE = path.join(CCM_DIR, "task-logs.json");
853
+ const taskLogsCache = new Map(); // 内存缓存
854
+ function loadTaskLogs() {
855
+ try {
856
+ if (fs.existsSync(TASK_LOGS_FILE)) {
857
+ return JSON.parse(fs.readFileSync(TASK_LOGS_FILE, "utf-8"));
858
+ }
859
+ }
860
+ catch (e) {
861
+ console.error("加载任务日志失败:", e.message);
862
+ }
863
+ return {};
864
+ }
865
+ function saveTaskLogs(logs) {
866
+ try {
867
+ fs.writeFileSync(TASK_LOGS_FILE, JSON.stringify(logs, null, 2));
868
+ }
869
+ catch (e) {
870
+ console.error("保存任务日志失败:", e.message);
871
+ }
872
+ }
873
+ function addTaskLog(taskId, level, message) {
874
+ const logs = loadTaskLogs();
875
+ if (!logs[taskId]) {
876
+ logs[taskId] = [];
877
+ }
878
+ const logEntry = {
879
+ timestamp: new Date().toISOString(),
880
+ level: level, // info, success, warning, error, response
881
+ message: message
882
+ };
883
+ logs[taskId].push(logEntry);
884
+ // 限制每个任务最多 100 条日志
885
+ if (logs[taskId].length > 100) {
886
+ logs[taskId] = logs[taskId].slice(-100);
887
+ }
888
+ saveTaskLogs(logs);
889
+ console.log(`[任务日志] [${taskId}] [${level}] ${message.substring(0, 100)}`);
890
+ }
891
+ function getTaskLogs(taskId, limit = 50) {
892
+ const logs = loadTaskLogs();
893
+ const taskLogs = logs[taskId] || [];
894
+ return taskLogs.slice(-limit);
895
+ }
896
+ function clearTaskLogs(taskId) {
897
+ const logs = loadTaskLogs();
898
+ delete logs[taskId];
899
+ saveTaskLogs(logs);
900
+ }
901
+ // === MCP 和 Skills 文件存储 ===
902
+ const MCP_DIR = path.join(CCM_DIR, "mcp");
903
+ const SKILLS_DIR = path.join(CCM_DIR, "skills");
904
+ // 确保目录存在
905
+ if (!fs.existsSync(MCP_DIR))
906
+ fs.mkdirSync(MCP_DIR, { recursive: true });
907
+ if (!fs.existsSync(SKILLS_DIR))
908
+ fs.mkdirSync(SKILLS_DIR, { recursive: true });
909
+ function loadMcpTools() {
910
+ try {
911
+ const files = fs.readdirSync(MCP_DIR).filter(f => f.endsWith('.json'));
912
+ return files.map(f => {
913
+ try {
914
+ const content = JSON.parse(fs.readFileSync(path.join(MCP_DIR, f), 'utf-8'));
915
+ return { ...content, filename: f };
916
+ }
917
+ catch {
918
+ return null;
919
+ }
920
+ }).filter(Boolean);
921
+ }
922
+ catch {
923
+ return [];
924
+ }
925
+ }
926
+ function saveMcpTool(tool) {
927
+ const filename = tool.name.replace(/[^a-zA-Z0-9-_]/g, '_') + '.json';
928
+ fs.writeFileSync(path.join(MCP_DIR, filename), JSON.stringify(tool, null, 2));
929
+ }
930
+ function deleteMcpTool(name) {
931
+ const filename = name.replace(/[^a-zA-Z0-9-_]/g, '_') + '.json';
932
+ const filePath = path.join(MCP_DIR, filename);
933
+ if (fs.existsSync(filePath))
934
+ fs.unlinkSync(filePath);
935
+ }
936
+ function loadSkills() {
937
+ try {
938
+ const files = fs.readdirSync(SKILLS_DIR).filter(f => f.endsWith('.json'));
939
+ return files.map(f => {
940
+ try {
941
+ const content = JSON.parse(fs.readFileSync(path.join(SKILLS_DIR, f), 'utf-8'));
942
+ return { ...content, filename: f };
943
+ }
944
+ catch {
945
+ return null;
946
+ }
947
+ }).filter(Boolean);
948
+ }
949
+ catch {
950
+ return [];
951
+ }
952
+ }
953
+ function saveSkill(skill) {
954
+ const filename = skill.name.replace(/[^a-zA-Z0-9-_]/g, '_') + '.json';
955
+ fs.writeFileSync(path.join(SKILLS_DIR, filename), JSON.stringify(skill, null, 2));
956
+ }
957
+ function deleteSkill(name) {
958
+ const filename = name.replace(/[^a-zA-Z0-9-_]/g, '_') + '.json';
959
+ const filePath = path.join(SKILLS_DIR, filename);
960
+ if (fs.existsSync(filePath))
961
+ fs.unlinkSync(filePath);
962
+ }
963
+ // === 飞书通知功能 ===
964
+ const FEISHU_CONFIG_FILE = path.join(CCM_DIR, "feishu-config.json");
965
+ // 飞书 OAuth 权限范围
966
+ const FEISHU_SCOPES = [
967
+ "im:message", // 发送消息
968
+ "im:message.group_at_msg", // 群聊 @ 消息
969
+ "im:chat", // 获取群聊信息
970
+ "im:chat:readonly", // 读取群聊信息
971
+ "contact:user.id:readonly", // 读取用户 ID
972
+ ];
973
+ function loadFeishuConfig() {
974
+ try {
975
+ if (fs.existsSync(FEISHU_CONFIG_FILE)) {
976
+ return JSON.parse(fs.readFileSync(FEISHU_CONFIG_FILE, "utf-8"));
977
+ }
978
+ }
979
+ catch (e) {
980
+ console.error("加载飞书配置失败:", e.message);
981
+ }
982
+ return {
983
+ app_id: "",
984
+ app_secret: "",
985
+ notify_chat_id: "",
986
+ enabled: false,
987
+ // OAuth 相关
988
+ redirect_uri: "http://localhost:3080/api/feishu/callback",
989
+ authorized: false,
990
+ user_access_token: "",
991
+ user_refresh_token: "",
992
+ token_expires_at: null,
993
+ authorized_user: null,
994
+ // 权限配置
995
+ scopes: FEISHU_SCOPES,
996
+ };
997
+ }
998
+ function saveFeishuConfig(config) {
999
+ fs.writeFileSync(FEISHU_CONFIG_FILE, JSON.stringify(config, null, 2));
1000
+ }
1001
+ // 获取飞书 tenant_access_token(应用级别)
1002
+ async function getFeishuTenantToken(appId, appSecret) {
1003
+ try {
1004
+ const response = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
1005
+ method: "POST",
1006
+ headers: { "Content-Type": "application/json" },
1007
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret })
1008
+ });
1009
+ const data = await response.json();
1010
+ return data.tenant_access_token;
1011
+ }
1012
+ catch (e) {
1013
+ console.error("获取飞书 tenant_access_token 失败:", e.message);
1014
+ return null;
1015
+ }
1016
+ }
1017
+ // 获取飞书 user_access_token(用户级别,通过 OAuth)
1018
+ async function getFeishuUserToken(appId, appSecret, code) {
1019
+ try {
1020
+ const response = await fetch("https://open.feishu.cn/open-apis/authen/v1/oidc/access_token", {
1021
+ method: "POST",
1022
+ headers: { "Content-Type": "application/json" },
1023
+ body: JSON.stringify({
1024
+ grant_type: "authorization_code",
1025
+ client_id: appId,
1026
+ client_secret: appSecret,
1027
+ code: code,
1028
+ redirect_uri: "http://localhost:3080/api/feishu/callback"
1029
+ })
1030
+ });
1031
+ const data = await response.json();
1032
+ if (data.code === 0) {
1033
+ return data.data;
1034
+ }
1035
+ console.error("获取 user_access_token 失败:", data.msg);
1036
+ return null;
1037
+ }
1038
+ catch (e) {
1039
+ console.error("获取 user_access_token 失败:", e.message);
1040
+ return null;
1041
+ }
1042
+ }
1043
+ // 刷新 user_access_token
1044
+ async function refreshFeishuUserToken(appId, appSecret, refreshToken) {
1045
+ try {
1046
+ const response = await fetch("https://open.feishu.cn/open-apis/authen/v1/oidc/refresh_access_token", {
1047
+ method: "POST",
1048
+ headers: { "Content-Type": "application/json" },
1049
+ body: JSON.stringify({
1050
+ grant_type: "refresh_token",
1051
+ client_id: appId,
1052
+ client_secret: appSecret,
1053
+ refresh_token: refreshToken
1054
+ })
1055
+ });
1056
+ const data = await response.json();
1057
+ if (data.code === 0) {
1058
+ return data.data;
1059
+ }
1060
+ return null;
1061
+ }
1062
+ catch (e) {
1063
+ console.error("刷新 user_access_token 失败:", e.message);
1064
+ return null;
1065
+ }
1066
+ }
1067
+ // 获取有效的 access_token(优先使用 user_token,过期则刷新)
1068
+ async function getValidFeishuToken() {
1069
+ const config = loadFeishuConfig();
1070
+ if (!config.app_id || !config.app_secret) {
1071
+ return null;
1072
+ }
1073
+ // 如果有 user_access_token 且未过期
1074
+ if (config.user_access_token && config.token_expires_at) {
1075
+ const expiresAt = new Date(config.token_expires_at);
1076
+ if (expiresAt > new Date()) {
1077
+ return config.user_access_token;
1078
+ }
1079
+ // Token 过期,尝试刷新
1080
+ if (config.user_refresh_token) {
1081
+ const refreshed = await refreshFeishuUserToken(config.app_id, config.app_secret, config.user_refresh_token);
1082
+ if (refreshed) {
1083
+ config.user_access_token = refreshed.access_token;
1084
+ config.user_refresh_token = refreshed.refresh_token;
1085
+ config.token_expires_at = new Date(Date.now() + refreshed.expires_in * 1000).toISOString();
1086
+ saveFeishuConfig(config);
1087
+ return refreshed.access_token;
1088
+ }
1089
+ }
1090
+ }
1091
+ // 回退到 tenant_access_token
1092
+ return await getFeishuTenantToken(config.app_id, config.app_secret);
1093
+ }
1094
+ // 获取用户信息
1095
+ async function getFeishuUserInfo(accessToken) {
1096
+ try {
1097
+ const response = await fetch("https://open.feishu.cn/open-apis/authen/v1/user_info", {
1098
+ headers: { "Authorization": `Bearer ${accessToken}` }
1099
+ });
1100
+ const data = await response.json();
1101
+ if (data.code === 0) {
1102
+ return data.data;
1103
+ }
1104
+ return null;
1105
+ }
1106
+ catch (e) {
1107
+ console.error("获取用户信息失败:", e.message);
1108
+ return null;
1109
+ }
1110
+ }
1111
+ // 获取群聊列表
1112
+ async function getFeishuChatList(accessToken) {
1113
+ try {
1114
+ const response = await fetch("https://open.feishu.cn/open-apis/im/v1/chats?page_size=50", {
1115
+ headers: { "Authorization": `Bearer ${accessToken}` }
1116
+ });
1117
+ const data = await response.json();
1118
+ if (data.code === 0) {
1119
+ return data.data.items || [];
1120
+ }
1121
+ return [];
1122
+ }
1123
+ catch (e) {
1124
+ console.error("获取群聊列表失败:", e.message);
1125
+ return [];
1126
+ }
1127
+ }
1128
+ // 通过 Webhook 发送飞书消息
1129
+ async function sendFeishuWebhook(content, msgType = "interactive") {
1130
+ const config = loadFeishuConfig();
1131
+ const webhookUrl = config.webhook_url;
1132
+ if (!webhookUrl) {
1133
+ console.log("[飞书通知] 未配置 Webhook URL,跳过通知");
1134
+ return false;
1135
+ }
1136
+ try {
1137
+ let body;
1138
+ if (msgType === "interactive") {
1139
+ // 卡片消息
1140
+ body = {
1141
+ msg_type: "interactive",
1142
+ card: typeof content === "string" ? JSON.parse(content) : content
1143
+ };
1144
+ }
1145
+ else {
1146
+ // 文本消息
1147
+ body = {
1148
+ msg_type: "text",
1149
+ content: { text: typeof content === "string" ? content : JSON.stringify(content) }
1150
+ };
1151
+ }
1152
+ // 如果配置了签名密钥,添加签名
1153
+ if (config.sign_key) {
1154
+ const timestamp = Math.floor(Date.now() / 1000);
1155
+ const stringToSign = `${timestamp}\n${config.sign_key}`;
1156
+ const crypto = require("crypto");
1157
+ const sign = crypto.createHmac("sha256", stringToSign).update("").digest("base64");
1158
+ body.timestamp = timestamp.toString();
1159
+ body.sign = sign;
1160
+ }
1161
+ const response = await fetch(webhookUrl, {
1162
+ method: "POST",
1163
+ headers: { "Content-Type": "application/json" },
1164
+ body: JSON.stringify(body)
1165
+ });
1166
+ const result = await response.json();
1167
+ if (result.code === 0 || result.StatusCode === 0) {
1168
+ console.log("[飞书通知] Webhook 消息发送成功");
1169
+ return true;
1170
+ }
1171
+ else {
1172
+ console.error("[飞书通知] Webhook 消息发送失败:", result.msg || result.StatusMessage);
1173
+ return false;
1174
+ }
1175
+ }
1176
+ catch (e) {
1177
+ console.error("[飞书通知] Webhook 发送失败:", e.message);
1178
+ return false;
1179
+ }
1180
+ }
1181
+ // 发送飞书消息给用户(通过 API)
1182
+ async function sendFeishuMessageToUser(userId, content, msgType = "interactive") {
1183
+ const config = loadFeishuConfig();
1184
+ // 如果没有 userId,尝试从配置中获取
1185
+ if (!userId || userId === "test") {
1186
+ if (config.authorized_user?.open_id) {
1187
+ userId = config.authorized_user.open_id;
1188
+ }
1189
+ else {
1190
+ console.log("[飞书通知] 未配置用户 ID,请先完成授权");
1191
+ return false;
1192
+ }
1193
+ }
1194
+ // 获取 token
1195
+ const token = await getValidFeishuToken();
1196
+ if (!token) {
1197
+ console.log("[飞书通知] 无法获取 Token,请检查 App ID 和 Secret");
1198
+ return false;
1199
+ }
1200
+ try {
1201
+ const response = await fetch(`https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id`, {
1202
+ method: "POST",
1203
+ headers: {
1204
+ "Content-Type": "application/json",
1205
+ "Authorization": `Bearer ${token}`
1206
+ },
1207
+ body: JSON.stringify({
1208
+ receive_id: userId,
1209
+ msg_type: msgType,
1210
+ content: typeof content === "string" ? content : JSON.stringify(content)
1211
+ })
1212
+ });
1213
+ const result = await response.json();
1214
+ if (result.code === 0) {
1215
+ console.log("[飞书通知] 消息发送成功");
1216
+ return true;
1217
+ }
1218
+ else {
1219
+ console.error("[飞书通知] 消息发送失败:", result.msg);
1220
+ return false;
1221
+ }
1222
+ }
1223
+ catch (e) {
1224
+ console.error("[飞书通知] 发送失败:", e.message);
1225
+ return false;
1226
+ }
1227
+ }
1228
+ // 发送飞书消息(兼容旧接口)
1229
+ async function sendFeishuMessage(chatId, content, msgType = "interactive") {
1230
+ return sendFeishuMessageToUser(chatId, content, msgType);
1231
+ }
1232
+ // 发送任务完成通知
1233
+ async function sendTaskCompletionNotification(task, result) {
1234
+ const config = loadFeishuConfig();
1235
+ // 获取用户 ID:优先使用 authorized_user.open_id
1236
+ const userId = config.authorized_user?.open_id || config.notify_user_id;
1237
+ if (!userId) {
1238
+ console.log("[飞书通知] 未配置通知用户,请先完成授权");
1239
+ return;
1240
+ }
1241
+ // 截取结果摘要
1242
+ const resultSummary = result.substring(0, 200) + (result.length > 200 ? "..." : "");
1243
+ // 构建富文本消息
1244
+ const cardContent = {
1245
+ config: { wide_screen_mode: true },
1246
+ header: {
1247
+ title: { tag: "plain_text", content: "✅ 任务完成通知" },
1248
+ template: "green"
1249
+ },
1250
+ elements: [
1251
+ {
1252
+ tag: "div",
1253
+ text: {
1254
+ tag: "lark_md",
1255
+ content: `**任务标题**:${task.title}\n**目标项目**:${task.target_project || '群聊'}\n**优先级**:${task.priority === 'high' ? '🔴 高' : task.priority === 'normal' ? '🟡 中' : '⚪ 低'}\n**完成时间**:${new Date().toLocaleString("zh-CN")}`
1256
+ }
1257
+ },
1258
+ { tag: "hr" },
1259
+ {
1260
+ tag: "div",
1261
+ text: {
1262
+ tag: "lark_md",
1263
+ content: `**执行结果**:\n${resultSummary}`
1264
+ }
1265
+ }
1266
+ ]
1267
+ };
1268
+ await sendFeishuMessageToUser(userId, JSON.stringify(cardContent), "interactive");
1269
+ }
1270
+ // 发送任务失败通知
1271
+ async function sendTaskFailureNotification(task, errorMsg) {
1272
+ const config = loadFeishuConfig();
1273
+ // 获取用户 ID:优先使用 authorized_user.open_id
1274
+ const userId = config.authorized_user?.open_id || config.notify_user_id;
1275
+ if (!userId) {
1276
+ console.log("[飞书通知] 未配置通知用户,请先完成授权");
1277
+ return;
1278
+ }
1279
+ const cardContent = {
1280
+ config: { wide_screen_mode: true },
1281
+ header: {
1282
+ title: { tag: "plain_text", content: "❌ 任务执行失败" },
1283
+ template: "red"
1284
+ },
1285
+ elements: [
1286
+ {
1287
+ tag: "div",
1288
+ text: {
1289
+ tag: "lark_md",
1290
+ content: `**任务标题**:${task.title}\n**目标项目**:${task.target_project || '群聊'}\n**失败时间**:${new Date().toLocaleString("zh-CN")}`
1291
+ }
1292
+ },
1293
+ { tag: "hr" },
1294
+ {
1295
+ tag: "div",
1296
+ text: {
1297
+ tag: "lark_md",
1298
+ content: `**错误信息**:\n${errorMsg.substring(0, 300)}`
1299
+ }
1300
+ }
1301
+ ]
1302
+ };
1303
+ await sendFeishuMessageToUser(userId, JSON.stringify(cardContent), "interactive");
1304
+ }
1305
+ // 执行单个任务
1306
+ async function executeTask(task) {
1307
+ const configs = getConfigs();
1308
+ if (task.assign_type === "group" && task.group_id) {
1309
+ // 群聊模式:发送给群聊主 Agent
1310
+ const groups = loadGroups();
1311
+ const group = groups.find(g => g.id === task.group_id);
1312
+ if (!group)
1313
+ throw new Error("群聊不存在");
1314
+ const message = `📋 执行任务:${task.title}\n${task.description || ""}\n\n请完成此任务并回复 "✅ 任务完成"。`;
1315
+ // 发送给主 Agent
1316
+ const firstMember = group.members.find(m => m.project !== "coordinator");
1317
+ const firstConfig = firstMember ? configs.find(c => c.name === firstMember.project) : configs[0];
1318
+ const workDir = firstConfig ? getConfigInfo(firstConfig.path)[0]?.workDir : process.cwd();
1319
+ const recentMsgs = getGroupMessages(task.group_id).slice(-5);
1320
+ const context = recentMsgs.map(m => {
1321
+ const who = m.role === "user" ? `[用户 → ${m.target}]` : `[${m.agent || "Agent"}]`;
1322
+ return `${who} ${m.content}`;
1323
+ }).join("\n");
1324
+ const fullPrompt = `你是群聊的主 Agent(协调者)。\n\n群聊记录:\n${context}\n\n${message}`;
1325
+ return callAgent("coordinator", fullPrompt, workDir, "claudecode", 300000);
1326
+ }
1327
+ else {
1328
+ // 项目模式:直接发送给项目 Agent
1329
+ const config = configs.find(c => c.name === task.target_project);
1330
+ if (!config)
1331
+ throw new Error("项目配置不存在");
1332
+ const info = getConfigInfo(config.path);
1333
+ const workDir = info[0]?.workDir;
1334
+ const agentType = info[0]?.agent || "claudecode";
1335
+ const message = `📋 执行任务:${task.title}\n${task.description || ""}\n\n请完成此任务并回复 "✅ 任务完成"。`;
1336
+ return callAgent(task.target_project, message, workDir, agentType, 300000);
1337
+ }
1338
+ }
1339
+ // 检查任务是否完成
1340
+ function checkTaskCompletion(response) {
1341
+ if (!response)
1342
+ return false;
1343
+ const completionMarkers = [
1344
+ "✅ 任务完成",
1345
+ "✅ 已完成",
1346
+ "✅ 完成",
1347
+ "任务已完成",
1348
+ "已完成任务",
1349
+ "已经完成",
1350
+ "done",
1351
+ "completed",
1352
+ "finished"
1353
+ ];
1354
+ const lowerResponse = response.toLowerCase();
1355
+ return completionMarkers.some(marker => lowerResponse.includes(marker.toLowerCase()));
1356
+ }
1357
+ // === 对话模板库 ===
1358
+ const TEMPLATES_FILE = path.join(CCM_DIR, "templates.json");
1359
+ function loadTemplates() {
1360
+ try {
1361
+ if (fs.existsSync(TEMPLATES_FILE)) {
1362
+ return JSON.parse(fs.readFileSync(TEMPLATES_FILE, "utf-8"));
1363
+ }
1364
+ }
1365
+ catch (e) {
1366
+ console.error("加载模板失败:", e.message);
1367
+ }
1368
+ // 返回默认模板
1369
+ return getDefaultTemplates();
1370
+ }
1371
+ function saveTemplates(templates) {
1372
+ fs.writeFileSync(TEMPLATES_FILE, JSON.stringify(templates, null, 2));
1373
+ }
1374
+ function getDefaultTemplates() {
1375
+ return [
1376
+ {
1377
+ id: "tpl_frontend_dev",
1378
+ name: "前端功能开发",
1379
+ category: "development",
1380
+ description: "开发新的前端页面或组件",
1381
+ icon: "🎨",
1382
+ prompt: "请帮我开发一个前端功能:\n\n功能描述:\n- 页面名称:\n- 主要功能:\n- UI 要求:\n\n技术要求:\n- 使用 Vue 3 + Vite\n- 使用 Vant/Element Plus 组件\n- 响应式设计\n\n请先分析需求,然后逐步实现。",
1383
+ tags: ["前端", "Vue", "组件"],
1384
+ created_at: new Date().toISOString()
1385
+ },
1386
+ {
1387
+ id: "tpl_backend_api",
1388
+ name: "后端接口开发",
1389
+ category: "development",
1390
+ description: "开发新的后端 API 接口",
1391
+ icon: "🔌",
1392
+ prompt: "请帮我开发后端接口:\n\n接口信息:\n- 接口路径:\n- 请求方法:GET/POST/PUT/DELETE\n- 请求参数:\n- 返回数据格式:\n\n业务逻辑:\n- \n\n请按照项目规范实现接口,包括:\n1. Controller 层\n2. Service 层\n3. 数据库操作",
1393
+ tags: ["后端", "API", "Java"],
1394
+ created_at: new Date().toISOString()
1395
+ },
1396
+ {
1397
+ id: "tpl_bug_fix",
1398
+ name: "Bug 修复",
1399
+ category: "maintenance",
1400
+ description: "定位和修复代码中的 Bug",
1401
+ icon: "🐛",
1402
+ prompt: "请帮我修复这个 Bug:\n\n问题描述:\n- 现象:\n- 期望行为:\n- 复现步骤:\n\n错误信息:\n\n\n相关代码:\n\n\n请分析问题原因并提供修复方案。",
1403
+ tags: ["Bug", "修复", "调试"],
1404
+ created_at: new Date().toISOString()
1405
+ },
1406
+ {
1407
+ id: "tpl_code_review",
1408
+ name: "代码审查",
1409
+ category: "review",
1410
+ description: "审查代码质量和潜在问题",
1411
+ icon: "🔍",
1412
+ prompt: "请帮我审查以下代码:\n\n代码文件:\n\n\n审查重点:\n1. 代码质量和规范\n2. 潜在的 Bug\n3. 性能问题\n4. 安全隐患\n5. 可维护性\n\n请给出具体的改进建议。",
1413
+ tags: ["审查", "质量", "优化"],
1414
+ created_at: new Date().toISOString()
1415
+ },
1416
+ {
1417
+ id: "tpl_api_integration",
1418
+ name: "前后端联调",
1419
+ category: "collaboration",
1420
+ description: "前后端接口对接和联调",
1421
+ icon: "🔗",
1422
+ prompt: "请帮我完成前后端联调:\n\n接口信息:\n- 接口名称:\n- 后端地址:\n- 接口文档:\n\n前端需求:\n- 页面功能:\n- 数据展示:\n\n请:\n1. 检查接口文档\n2. 实现前端调用代码\n3. 处理数据格式转换\n4. 添加错误处理",
1423
+ tags: ["联调", "接口", "协作"],
1424
+ created_at: new Date().toISOString()
1425
+ },
1426
+ {
1427
+ id: "tpl_refactor",
1428
+ name: "代码重构",
1429
+ category: "maintenance",
1430
+ description: "重构和优化现有代码",
1431
+ icon: "⚡",
1432
+ prompt: "请帮我重构以下代码:\n\n代码文件:\n\n\n重构目标:\n- 提高代码可读性\n- 减少重复代码\n- 优化性能\n- 遵循最佳实践\n\n请提供重构方案和具体实现。",
1433
+ tags: ["重构", "优化", "代码质量"],
1434
+ created_at: new Date().toISOString()
1435
+ },
1436
+ {
1437
+ id: "tpl_feature_plan",
1438
+ name: "功能规划",
1439
+ category: "planning",
1440
+ description: "规划新功能的实现方案",
1441
+ icon: "📋",
1442
+ prompt: "请帮我规划这个功能的实现方案:\n\n功能描述:\n\n\n需求细节:\n\n\n请提供:\n1. 技术方案设计\n2. 实现步骤\n3. 预估工作量\n4. 潜在风险\n5. 测试要点",
1443
+ tags: ["规划", "设计", "方案"],
1444
+ created_at: new Date().toISOString()
1445
+ },
1446
+ {
1447
+ id: "tpl_database",
1448
+ name: "数据库操作",
1449
+ category: "development",
1450
+ description: "数据库表设计和 SQL 操作",
1451
+ icon: "🗄️",
1452
+ prompt: "请帮我完成数据库相关工作:\n\n需求描述:\n\n\n请提供:\n1. 表结构设计(如需要)\n2. SQL 语句\n3. 索引建议\n4. 数据迁移方案(如需要)",
1453
+ tags: ["数据库", "SQL", "设计"],
1454
+ created_at: new Date().toISOString()
1455
+ }
1456
+ ];
1457
+ }
1458
+ function createTemplate(template) {
1459
+ const templates = loadTemplates();
1460
+ const newTemplate = {
1461
+ id: "tpl_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
1462
+ name: template.name,
1463
+ category: template.category || "custom",
1464
+ description: template.description || "",
1465
+ icon: template.icon || "📝",
1466
+ prompt: template.prompt,
1467
+ tags: template.tags || [],
1468
+ created_at: new Date().toISOString()
1469
+ };
1470
+ templates.push(newTemplate);
1471
+ saveTemplates(templates);
1472
+ return newTemplate;
1473
+ }
1474
+ function updateTemplate(id, updates) {
1475
+ const templates = loadTemplates();
1476
+ const idx = templates.findIndex(t => t.id === id);
1477
+ if (idx === -1)
1478
+ return null;
1479
+ Object.assign(templates[idx], updates, { updated_at: new Date().toISOString() });
1480
+ saveTemplates(templates);
1481
+ return templates[idx];
1482
+ }
1483
+ function deleteTemplate(id) {
1484
+ const templates = loadTemplates().filter(t => t.id !== id);
1485
+ saveTemplates(templates);
1486
+ }
1487
+ // === 定时任务 ===
1488
+ function loadCronJobs() {
1489
+ if (!fs.existsSync(CRON_FILE))
1490
+ return [];
1491
+ try {
1492
+ return JSON.parse(fs.readFileSync(CRON_FILE, "utf-8"));
1493
+ }
1494
+ catch {
1495
+ return [];
1496
+ }
1497
+ }
1498
+ function saveCronJobs(jobs) {
1499
+ fs.writeFileSync(CRON_FILE, JSON.stringify(jobs, null, 2));
1500
+ }
1501
+ function createCronJob(job) {
1502
+ const jobs = loadCronJobs();
1503
+ const newJob = {
1504
+ id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
1505
+ name: job.name,
1506
+ project: job.project,
1507
+ schedule: job.schedule,
1508
+ prompt: job.prompt,
1509
+ enabled: true,
1510
+ created_at: new Date().toISOString(),
1511
+ last_run: null,
1512
+ };
1513
+ jobs.push(newJob);
1514
+ saveCronJobs(jobs);
1515
+ return newJob;
1516
+ }
1517
+ function updateCronJob(id, updates) {
1518
+ const jobs = loadCronJobs();
1519
+ const idx = jobs.findIndex(j => j.id === id);
1520
+ if (idx === -1)
1521
+ return null;
1522
+ Object.assign(jobs[idx], updates);
1523
+ saveCronJobs(jobs);
1524
+ return jobs[idx];
1525
+ }
1526
+ function deleteCronJob(id) {
1527
+ const jobs = loadCronJobs().filter(j => j.id !== id);
1528
+ saveCronJobs(jobs);
1529
+ }
1530
+ function handleRequest(req, res) {
1531
+ const parsed = url.parse(req.url, true);
1532
+ const pathname = parsed.pathname;
1533
+ // CORS
1534
+ res.setHeader("Access-Control-Allow-Origin", "*");
1535
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1536
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1537
+ if (req.method === "OPTIONS") {
1538
+ res.writeHead(204);
1539
+ res.end();
1540
+ return;
1541
+ }
1542
+ // 静态文件(Vue 构建产物 + 旧 HTML 资源)
1543
+ if (pathname === "/" || pathname === "/index.html") {
1544
+ return sendFile(res, path.join(PUBLIC_DIR, "index.html"));
1545
+ }
1546
+ if (pathname.startsWith("/assets/") || pathname.startsWith("/public/") ||
1547
+ pathname.startsWith("/css/") || pathname.startsWith("/js/") ||
1548
+ pathname === "/favicon.svg" || pathname === "/icons.svg" || pathname === "/favicon.ico") {
1549
+ const filePath = path.join(PUBLIC_DIR, pathname.startsWith("/public/") ? pathname.replace("/public/", "") : pathname);
1550
+ if (fs.existsSync(filePath)) {
1551
+ return sendFile(res, filePath);
1552
+ }
1553
+ }
1554
+ // SPA fallback: 非 API 路径且文件存在则返回 index.html
1555
+ if (!pathname.startsWith("/api/") && req.method === "GET") {
1556
+ const filePath = path.join(PUBLIC_DIR, pathname);
1557
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
1558
+ return sendFile(res, filePath);
1559
+ }
1560
+ return sendFile(res, path.join(PUBLIC_DIR, "index.html"));
1561
+ }
1562
+ // API 路由
1563
+ const apiRoutes = {
1564
+ // 项目列表
1565
+ "GET /api/projects": () => {
1566
+ const configs = getConfigs();
1567
+ const projects = configs.map((config) => {
1568
+ const info = getConfigInfo(config.path);
1569
+ const running = isRunning(config.name);
1570
+ return {
1571
+ name: config.name,
1572
+ running,
1573
+ pid: running ? getPid(config.name) : null,
1574
+ agent: info[0]?.agent || "claudecode",
1575
+ platform: info[0]?.platform || "未知",
1576
+ work_dir: info[0]?.workDir || "",
1577
+ session_count: getSessions(config.name).length,
1578
+ };
1579
+ });
1580
+ sendJson(res, { projects });
1581
+ },
1582
+ // Agent 列表
1583
+ "GET /api/agents": () => {
1584
+ sendJson(res, { agents: AGENTS });
1585
+ },
1586
+ // 启动项目
1587
+ "POST /api/start": () => {
1588
+ let body = "";
1589
+ req.on("data", (chunk) => body += chunk);
1590
+ req.on("end", () => {
1591
+ try {
1592
+ const { project, agent } = JSON.parse(body);
1593
+ sendJson(res, startProject(project, agent));
1594
+ }
1595
+ catch (e) {
1596
+ sendJson(res, { success: false, error: e.message }, 400);
1597
+ }
1598
+ });
1599
+ },
1600
+ // 停止项目
1601
+ "POST /api/stop": () => {
1602
+ let body = "";
1603
+ req.on("data", (chunk) => body += chunk);
1604
+ req.on("end", () => {
1605
+ try {
1606
+ const { project } = JSON.parse(body);
1607
+ sendJson(res, stopProject(project));
1608
+ }
1609
+ catch (e) {
1610
+ sendJson(res, { success: false, error: e.message }, 400);
1611
+ }
1612
+ });
1613
+ },
1614
+ // 创建项目
1615
+ "POST /api/projects/create": () => {
1616
+ let body = "";
1617
+ req.on("data", (chunk) => body += chunk);
1618
+ req.on("end", () => {
1619
+ try {
1620
+ const { name, work_dir, agent, platform, platform_options } = JSON.parse(body);
1621
+ if (!name || !work_dir) {
1622
+ return sendJson(res, { success: false, error: "项目名称和目录不能为空" }, 400);
1623
+ }
1624
+ const configPath = path.join(CONFIGS_DIR, `config-${name}.toml`);
1625
+ if (fs.existsSync(configPath)) {
1626
+ return sendJson(res, { success: false, error: "项目已存在" }, 400);
1627
+ }
1628
+ const agentType = agent || "claudecode";
1629
+ const platformType = platform || "feishu";
1630
+ const opts = platform_options || {};
1631
+ const isFeishu = platformType === "feishu" || platformType === "lark";
1632
+ const optionsLines = Object.entries(opts).map(([k, v]) => `${k} = "${v}"`).join("\n");
1633
+ const extraOptions = isFeishu ? "\nenable_feishu_card = true\nthread_isolation = true\nprogress_style = \"card\"" : "";
1634
+ const template = `# cc-connect - ${name}
1635
+ language = "zh"
1636
+
1637
+ [[projects]]
1638
+ name = "${name}"
1639
+ work_dir = "${work_dir.replace(/\\/g, "\\\\")}"
1640
+ admin_from = "*"
1641
+
1642
+ [projects.agent]
1643
+ type = "${agentType}"
1644
+ mode = "default"
1645
+
1646
+ [projects.agent.options]
1647
+ work_dir = "${work_dir.replace(/\\/g, "\\\\")}"
1648
+
1649
+ [[projects.platforms]]
1650
+ type = "${platformType}"
1651
+
1652
+ [projects.platforms.options]
1653
+ ${optionsLines}${extraOptions}
1654
+
1655
+ # 自定义命令
1656
+ [[commands]]
1657
+ name = "history"
1658
+ description = "查看会话历史记录"
1659
+ exec = "cc-connect sessions show {{1}} -n {{2:20}}"
1660
+
1661
+ [[commands]]
1662
+ name = "sessions"
1663
+ description = "列出所有会话"
1664
+ exec = "cc-connect sessions list"
1665
+
1666
+ [[commands]]
1667
+ name = "projects"
1668
+ description = "查看所有可操作的代码项目目录"
1669
+ exec = "cmd /c type ${CCM_DIR.replace(/\\/g, "\\\\")}\\\\projects.txt"
1670
+
1671
+ [[aliases]]
1672
+ name = "历史"
1673
+ command = "/history"
1674
+
1675
+ [[aliases]]
1676
+ name = "会话"
1677
+ command = "/sessions"
1678
+
1679
+ [[aliases]]
1680
+ name = "项目"
1681
+ command = "/projects"
1682
+ `;
1683
+ fs.writeFileSync(configPath, template);
1684
+ sendJson(res, { success: true, config: configPath });
1685
+ }
1686
+ catch (e) {
1687
+ sendJson(res, { success: false, error: e.message }, 400);
1688
+ }
1689
+ });
1690
+ },
1691
+ // 更新项目
1692
+ "POST /api/projects/update": () => {
1693
+ let body = "";
1694
+ req.on("data", (chunk) => body += chunk);
1695
+ req.on("end", () => {
1696
+ try {
1697
+ const { name, work_dir, agent, platform, platform_options } = JSON.parse(body);
1698
+ if (!name) {
1699
+ return sendJson(res, { success: false, error: "项目名称不能为空" }, 400);
1700
+ }
1701
+ const configPath = path.join(CONFIGS_DIR, `config-${name}.toml`);
1702
+ if (!fs.existsSync(configPath)) {
1703
+ return sendJson(res, { success: false, error: "项目不存在" }, 404);
1704
+ }
1705
+ // 读取现有配置
1706
+ const existingContent = fs.readFileSync(configPath, "utf-8");
1707
+ // 更新配置
1708
+ let updatedContent = existingContent;
1709
+ if (work_dir) {
1710
+ updatedContent = updatedContent.replace(/work_dir\s*=\s*"[^"]*"/g, `work_dir = "${work_dir.replace(/\\/g, "\\\\")}"`);
1711
+ }
1712
+ if (agent) {
1713
+ updatedContent = updatedContent.replace(/type\s*=\s*"[^"]*"/g, `type = "${agent}"`);
1714
+ }
1715
+ fs.writeFileSync(configPath, updatedContent);
1716
+ sendJson(res, { success: true, message: "项目配置已更新" });
1717
+ }
1718
+ catch (e) {
1719
+ sendJson(res, { success: false, error: e.message }, 400);
1720
+ }
1721
+ });
1722
+ },
1723
+ // 删除项目
1724
+ "POST /api/projects/delete": () => {
1725
+ let body = "";
1726
+ req.on("data", (chunk) => body += chunk);
1727
+ req.on("end", () => {
1728
+ try {
1729
+ const { name } = JSON.parse(body);
1730
+ if (!name) {
1731
+ return sendJson(res, { success: false, error: "项目名称不能为空" }, 400);
1732
+ }
1733
+ const configPath = path.join(CONFIGS_DIR, `config-${name}.toml`);
1734
+ if (!fs.existsSync(configPath)) {
1735
+ return sendJson(res, { success: false, error: "项目不存在" }, 404);
1736
+ }
1737
+ // 检查是否正在运行
1738
+ if (isRunning(name)) {
1739
+ return sendJson(res, { success: false, error: "项目正在运行,请先停止" }, 400);
1740
+ }
1741
+ // 删除配置文件
1742
+ fs.unlinkSync(configPath);
1743
+ // 删除会话数据
1744
+ const sessionFile = findCcSessionFile(name);
1745
+ if (sessionFile && fs.existsSync(sessionFile)) {
1746
+ fs.unlinkSync(sessionFile);
1747
+ }
1748
+ // 删除 web session 目录
1749
+ const webSessionDir = path.join(WEB_SESSIONS_DIR, name);
1750
+ if (fs.existsSync(webSessionDir)) {
1751
+ fs.rmSync(webSessionDir, { recursive: true });
1752
+ }
1753
+ sendJson(res, { success: true, message: "项目已删除" });
1754
+ }
1755
+ catch (e) {
1756
+ sendJson(res, { success: false, error: e.message }, 400);
1757
+ }
1758
+ });
1759
+ },
1760
+ // 飞书扫码配置 - 通过 cc-connect 创建机器人
1761
+ "POST /api/projects/feishu-setup": () => {
1762
+ let body = "";
1763
+ req.on("data", (chunk) => body += chunk);
1764
+ req.on("end", () => {
1765
+ try {
1766
+ const { name } = JSON.parse(body);
1767
+ console.log("[飞书配置] 收到请求,项目名称:", name);
1768
+ const configPath = path.join(CONFIGS_DIR, `config-${name}.toml`);
1769
+ const qrImagePath = path.join(UPLOAD_DIR, `feishu-qr-${name}.png`);
1770
+ // 执行 cc-connect feishu new 命令(扫码创建机器人)
1771
+ let output = "";
1772
+ let scanUrl = null;
1773
+ // 确保上传目录存在
1774
+ if (!fs.existsSync(UPLOAD_DIR)) {
1775
+ fs.mkdirSync(UPLOAD_DIR, { recursive: true });
1776
+ }
1777
+ console.log("[飞书配置] 二维码图片路径:", qrImagePath);
1778
+ // 异步执行 cc-connect 命令(不阻塞响应)
1779
+ const child = spawn("cc-connect", ["feishu", "new", "--project", name, "--qr-image", qrImagePath, "--timeout", "300"], {
1780
+ shell: true,
1781
+ stdio: ["pipe", "pipe", "pipe"]
1782
+ });
1783
+ let cmdOutput = "";
1784
+ child.stdout.on("data", (data) => { cmdOutput += data.toString(); });
1785
+ child.stderr.on("data", (data) => { cmdOutput += data.toString(); });
1786
+ child.on("close", (code) => {
1787
+ console.log("[飞书配置] cc-connect 完成,退出码:", code);
1788
+ console.log("[飞书配置] 输出:", cmdOutput.substring(0, 500));
1789
+ // 检查配置是否更新
1790
+ try {
1791
+ const configContent = fs.readFileSync(configPath, "utf-8");
1792
+ const appIdMatch = configContent.match(/app_id\s*=\s*"([^"]+)"/);
1793
+ if (appIdMatch && appIdMatch[1] && appIdMatch[1] !== "" && appIdMatch[1] !== "PLACEHOLDER") {
1794
+ const feishuConfig = loadFeishuConfig();
1795
+ feishuConfig.app_id = appIdMatch[1];
1796
+ const appSecretMatch = configContent.match(/app_secret\s*=\s*"([^"]+)"/);
1797
+ if (appSecretMatch && appSecretMatch[1]) {
1798
+ feishuConfig.app_secret = appSecretMatch[1];
1799
+ }
1800
+ saveFeishuConfig(feishuConfig);
1801
+ console.log("[飞书配置] 配置已同步到全局:", feishuConfig.app_id);
1802
+ }
1803
+ }
1804
+ catch { }
1805
+ });
1806
+ // 使用 setTimeout 等待二维码图片生成
1807
+ setTimeout(() => {
1808
+ try {
1809
+ const qrExists = fs.existsSync(qrImagePath);
1810
+ console.log("[飞书配置] 二维码图片存在:", qrExists);
1811
+ // 从输出中提取 URL
1812
+ const urlPatterns = [
1813
+ /URL:\s*(https?:\/\/\S+)/i,
1814
+ /url:\s*(https?:\/\/\S+)/i,
1815
+ /(https?:\/\/open\.feishu\.cn\S+)/i,
1816
+ ];
1817
+ for (const pattern of urlPatterns) {
1818
+ const match = cmdOutput.match(pattern);
1819
+ if (match) {
1820
+ scanUrl = match[1];
1821
+ console.log("[飞书配置] 提取到 URL:", scanUrl);
1822
+ break;
1823
+ }
1824
+ }
1825
+ sendJson(res, {
1826
+ success: true,
1827
+ scan_url: scanUrl,
1828
+ qr_image: qrExists ? `/api/uploads/feishu-qr-${name}.png` : null,
1829
+ output: cmdOutput.substring(0, 2000),
1830
+ });
1831
+ }
1832
+ catch (e) {
1833
+ sendJson(res, { success: false, error: e.message }, 400);
1834
+ }
1835
+ }, 2000);
1836
+ }
1837
+ catch (e) {
1838
+ sendJson(res, { success: false, error: e.message }, 400);
1839
+ }
1840
+ });
1841
+ },
1842
+ };
1843
+ // 精确匹配
1844
+ const routeKey = `${req.method} ${pathname}`;
1845
+ if (apiRoutes[routeKey])
1846
+ return apiRoutes[routeKey]();
1847
+ // 动态路由: /api/uploads/:filename (提供文件访问)
1848
+ if (pathname.startsWith("/api/uploads/") && req.method === "GET") {
1849
+ const filename = pathname.split("/").pop();
1850
+ const filePath = path.join(UPLOAD_DIR, filename);
1851
+ console.log("[文件访问] 请求文件:", filename, "路径:", filePath, "存在:", fs.existsSync(filePath));
1852
+ if (fs.existsSync(filePath)) {
1853
+ const ext = path.extname(filename).toLowerCase();
1854
+ const types = { ".png": "image/png", ".jpg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml" };
1855
+ res.writeHead(200, {
1856
+ "Content-Type": types[ext] || "application/octet-stream",
1857
+ "Access-Control-Allow-Origin": "*",
1858
+ "Cache-Control": "no-cache"
1859
+ });
1860
+ fs.createReadStream(filePath).pipe(res);
1861
+ }
1862
+ else {
1863
+ sendJson(res, { error: "文件不存在" }, 404);
1864
+ }
1865
+ return;
1866
+ }
1867
+ // 动态路由: /api/projects/:name/sessions
1868
+ const sessionsMatch = pathname.match(/^\/api\/projects\/([^/]+)\/sessions$/);
1869
+ if (sessionsMatch && req.method === "GET") {
1870
+ const projectName = decodeURIComponent(sessionsMatch[1]);
1871
+ return sendJson(res, { sessions: getSessions(projectName) });
1872
+ }
1873
+ // 动态路由: /api/projects/:name/sessions/:id
1874
+ const sessionDetailMatch = pathname.match(/^\/api\/projects\/([^/]+)\/sessions\/([^/]+)$/);
1875
+ if (sessionDetailMatch && req.method === "GET") {
1876
+ const projectName = decodeURIComponent(sessionDetailMatch[1]);
1877
+ const sessionId = decodeURIComponent(sessionDetailMatch[2]);
1878
+ const detail = getSessionDetail(projectName, sessionId);
1879
+ if (detail)
1880
+ return sendJson(res, detail);
1881
+ return sendJson(res, { error: "会话不存在" }, 404);
1882
+ }
1883
+ // 动态路由: /api/projects/:name/logs
1884
+ const logsMatch = pathname.match(/^\/api\/projects\/([^/]+)\/logs$/);
1885
+ if (logsMatch && req.method === "GET") {
1886
+ const projectName = decodeURIComponent(logsMatch[1]);
1887
+ const lines = parseInt(parsed.query.lines) || 100;
1888
+ return sendJson(res, { logs: getLogs(projectName, lines) });
1889
+ }
1890
+ // === 共享上下文 API ===
1891
+ if (pathname === "/api/shared" && req.method === "GET") {
1892
+ return sendJson(res, { files: listSharedFiles() });
1893
+ }
1894
+ if (pathname === "/api/shared/read" && req.method === "GET") {
1895
+ const name = parsed.query.name;
1896
+ const data = readSharedFile(name);
1897
+ if (!data)
1898
+ return sendJson(res, { error: "文件不存在" }, 404);
1899
+ return sendJson(res, { name, ...data });
1900
+ }
1901
+ // 下载文件
1902
+ if (pathname === "/api/shared/download" && req.method === "GET") {
1903
+ const name = parsed.query.name;
1904
+ const filePath = path.join(SHARED_DIR, name);
1905
+ if (!fs.existsSync(filePath)) {
1906
+ res.writeHead(404);
1907
+ res.end("Not Found");
1908
+ return;
1909
+ }
1910
+ const ext = path.extname(name).toLowerCase();
1911
+ const types = { ".pdf": "application/pdf", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml" };
1912
+ res.writeHead(200, {
1913
+ "Content-Type": types[ext] || "application/octet-stream",
1914
+ "Content-Disposition": `inline; filename="${encodeURIComponent(name)}"`,
1915
+ });
1916
+ fs.createReadStream(filePath).pipe(res);
1917
+ return;
1918
+ }
1919
+ // 上传文件(multipart)
1920
+ if (pathname === "/api/shared/upload" && req.method === "POST") {
1921
+ const ct = req.headers["content-type"] || "";
1922
+ if (ct.includes("multipart/form-data")) {
1923
+ const chunks = [];
1924
+ req.on("data", (chunk) => chunks.push(chunk));
1925
+ req.on("end", () => {
1926
+ try {
1927
+ const buffer = Buffer.concat(chunks);
1928
+ const boundaryMatch = ct.match(/boundary=(.+)/);
1929
+ if (!boundaryMatch)
1930
+ return sendJson(res, { error: "无效请求" }, 400);
1931
+ const { files } = parseMultipart(buffer, boundaryMatch[1]);
1932
+ const uploaded = files.map(f => saveSharedUpload(f.filename, fs.readFileSync(f.savedPath)));
1933
+ try {
1934
+ files.forEach(f => fs.unlinkSync(f.savedPath));
1935
+ }
1936
+ catch { }
1937
+ sendJson(res, { success: true, files: uploaded });
1938
+ }
1939
+ catch (e) {
1940
+ sendJson(res, { error: e.message }, 400);
1941
+ }
1942
+ });
1943
+ return;
1944
+ }
1945
+ sendJson(res, { error: "需要 multipart/form-data" }, 400);
1946
+ return;
1947
+ }
1948
+ if (pathname === "/api/shared/write" && req.method === "POST") {
1949
+ let body = "";
1950
+ req.on("data", (chunk) => body += chunk);
1951
+ req.on("end", () => {
1952
+ try {
1953
+ const { name, content } = JSON.parse(body);
1954
+ writeSharedFile(name, content);
1955
+ sendJson(res, { success: true });
1956
+ }
1957
+ catch (e) {
1958
+ sendJson(res, { error: e.message }, 400);
1959
+ }
1960
+ });
1961
+ return;
1962
+ }
1963
+ if (pathname === "/api/shared/delete" && req.method === "POST") {
1964
+ let body = "";
1965
+ req.on("data", (chunk) => body += chunk);
1966
+ req.on("end", () => {
1967
+ try {
1968
+ const { name } = JSON.parse(body);
1969
+ deleteSharedFile(name);
1970
+ sendJson(res, { success: true });
1971
+ }
1972
+ catch (e) {
1973
+ sendJson(res, { error: e.message }, 400);
1974
+ }
1975
+ });
1976
+ return;
1977
+ }
1978
+ // === 文件浏览器 API ===
1979
+ if (pathname === "/api/filesystem/browse" && req.method === "GET") {
1980
+ const dir = parsed.query.dir || os.homedir();
1981
+ try {
1982
+ const items = fs.readdirSync(dir, { withFileTypes: true })
1983
+ .filter(item => !item.name.startsWith('.'))
1984
+ .map(item => ({
1985
+ name: item.name,
1986
+ path: path.join(dir, item.name),
1987
+ isDirectory: item.isDirectory(),
1988
+ isFile: item.isFile()
1989
+ }))
1990
+ .sort((a, b) => {
1991
+ if (a.isDirectory && !b.isDirectory)
1992
+ return -1;
1993
+ if (!a.isDirectory && b.isDirectory)
1994
+ return 1;
1995
+ return a.name.localeCompare(b.name);
1996
+ })
1997
+ .slice(0, 100);
1998
+ sendJson(res, { success: true, path: dir, items });
1999
+ }
2000
+ catch (e) {
2001
+ sendJson(res, { success: false, error: e.message }, 400);
2002
+ }
2003
+ return;
2004
+ }
2005
+ // 获取系统磁盘列表
2006
+ if (pathname === "/api/filesystem/drives" && req.method === "GET") {
2007
+ try {
2008
+ let drives = [];
2009
+ if (process.platform === 'win32') {
2010
+ // Windows: 检查 A-Z 盘符
2011
+ for (let i = 65; i <= 90; i++) {
2012
+ const letter = String.fromCharCode(i);
2013
+ const drivePath = `${letter}:\\`;
2014
+ try {
2015
+ fs.accessSync(drivePath);
2016
+ drives.push({ name: letter, path: drivePath });
2017
+ }
2018
+ catch { }
2019
+ }
2020
+ }
2021
+ else {
2022
+ // Linux/Mac: 返回根目录
2023
+ drives.push({ name: '/', path: '/' });
2024
+ }
2025
+ sendJson(res, { success: true, drives, home: os.homedir() });
2026
+ }
2027
+ catch (e) {
2028
+ sendJson(res, { success: false, error: e.message }, 400);
2029
+ }
2030
+ return;
2031
+ }
2032
+ // === 终端 API ===
2033
+ if (pathname === "/api/terminal/exec" && req.method === "POST") {
2034
+ let body = "";
2035
+ req.on("data", (chunk) => body += chunk);
2036
+ req.on("end", () => {
2037
+ try {
2038
+ const { command, cwd } = JSON.parse(body);
2039
+ if (!command)
2040
+ return sendJson(res, { error: "命令不能为空" }, 400);
2041
+ const workDir = cwd || os.homedir();
2042
+ console.log(`[终端] 执行命令: ${command} (目录: ${workDir})`);
2043
+ try {
2044
+ const output = execSync(command, {
2045
+ encoding: "utf-8",
2046
+ cwd: workDir,
2047
+ timeout: 30000,
2048
+ maxBuffer: 5 * 1024 * 1024,
2049
+ shell: true
2050
+ });
2051
+ sendJson(res, { success: true, output: output, cwd: workDir });
2052
+ }
2053
+ catch (e) {
2054
+ sendJson(res, {
2055
+ success: true,
2056
+ output: (e.stdout || "") + (e.stderr || e.message),
2057
+ cwd: workDir,
2058
+ error: e.status ? `Exit code: ${e.status}` : e.message
2059
+ });
2060
+ }
2061
+ }
2062
+ catch (e) {
2063
+ sendJson(res, { error: e.message }, 400);
2064
+ }
2065
+ });
2066
+ return;
2067
+ }
2068
+ // 获取当前系统信息
2069
+ if (pathname === "/api/terminal/info" && req.method === "GET") {
2070
+ sendJson(res, {
2071
+ success: true,
2072
+ platform: process.platform,
2073
+ home: os.homedir(),
2074
+ cwd: process.cwd(),
2075
+ user: os.userInfo().username,
2076
+ shell: process.platform === 'win32' ? 'powershell' : 'bash'
2077
+ });
2078
+ return;
2079
+ }
2080
+ // === 任务派发 API ===
2081
+ if (pathname === "/api/tasks" && req.method === "GET") {
2082
+ return sendJson(res, { tasks: loadTasks() });
2083
+ }
2084
+ if (pathname === "/api/tasks/create" && req.method === "POST") {
2085
+ let body = "";
2086
+ req.on("data", (chunk) => body += chunk);
2087
+ req.on("end", () => {
2088
+ try {
2089
+ const task = createTask(JSON.parse(body));
2090
+ sendJson(res, { success: true, task });
2091
+ }
2092
+ catch (e) {
2093
+ sendJson(res, { error: e.message }, 400);
2094
+ }
2095
+ });
2096
+ return;
2097
+ }
2098
+ if (pathname === "/api/tasks/update" && req.method === "POST") {
2099
+ let body = "";
2100
+ req.on("data", (chunk) => body += chunk);
2101
+ req.on("end", () => {
2102
+ try {
2103
+ const { id, ...updates } = JSON.parse(body);
2104
+ const task = updateTask(id, updates);
2105
+ if (!task)
2106
+ return sendJson(res, { error: "任务不存在" }, 404);
2107
+ sendJson(res, { success: true, task });
2108
+ }
2109
+ catch (e) {
2110
+ sendJson(res, { error: e.message }, 400);
2111
+ }
2112
+ });
2113
+ return;
2114
+ }
2115
+ if (pathname === "/api/tasks/delete" && req.method === "POST") {
2116
+ let body = "";
2117
+ req.on("data", (chunk) => body += chunk);
2118
+ req.on("end", () => {
2119
+ try {
2120
+ const { id } = JSON.parse(body);
2121
+ deleteTask(id);
2122
+ sendJson(res, { success: true });
2123
+ }
2124
+ catch (e) {
2125
+ sendJson(res, { error: e.message }, 400);
2126
+ }
2127
+ });
2128
+ return;
2129
+ }
2130
+ // 任务队列 API
2131
+ if (pathname === "/api/tasks/queue" && req.method === "POST") {
2132
+ let body = "";
2133
+ req.on("data", (chunk) => body += chunk);
2134
+ req.on("end", () => {
2135
+ try {
2136
+ const { task_id } = JSON.parse(body);
2137
+ if (!task_id)
2138
+ return sendJson(res, { error: "缺少任务 ID" }, 400);
2139
+ const tasks = loadTasks();
2140
+ const task = tasks.find(t => t.id === task_id);
2141
+ if (!task)
2142
+ return sendJson(res, { error: "任务不存在" }, 404);
2143
+ enqueueTask(task_id);
2144
+ sendJson(res, { success: true, message: "任务已加入队列", queue_status: getQueueStatus() });
2145
+ }
2146
+ catch (e) {
2147
+ sendJson(res, { error: e.message }, 400);
2148
+ }
2149
+ });
2150
+ return;
2151
+ }
2152
+ // 批量加入队列
2153
+ if (pathname === "/api/tasks/queue-batch" && req.method === "POST") {
2154
+ let body = "";
2155
+ req.on("data", (chunk) => body += chunk);
2156
+ req.on("end", () => {
2157
+ try {
2158
+ const { task_ids } = JSON.parse(body);
2159
+ if (!task_ids || !Array.isArray(task_ids))
2160
+ return sendJson(res, { error: "缺少任务 ID 列表" }, 400);
2161
+ for (const id of task_ids) {
2162
+ enqueueTask(id);
2163
+ }
2164
+ sendJson(res, { success: true, message: `${task_ids.length} 个任务已加入队列`, queue_status: getQueueStatus() });
2165
+ }
2166
+ catch (e) {
2167
+ sendJson(res, { error: e.message }, 400);
2168
+ }
2169
+ });
2170
+ return;
2171
+ }
2172
+ // 获取队列状态
2173
+ if (pathname === "/api/tasks/queue/status" && req.method === "GET") {
2174
+ return sendJson(res, getQueueStatus());
2175
+ }
2176
+ // 清空队列
2177
+ if (pathname === "/api/tasks/queue/clear" && req.method === "POST") {
2178
+ taskQueues.clear();
2179
+ sendJson(res, { success: true, message: "队列已清空" });
2180
+ return;
2181
+ }
2182
+ // === 任务日志 API ===
2183
+ // 获取任务日志
2184
+ if (pathname === "/api/tasks/logs" && req.method === "GET") {
2185
+ const taskId = parsed.query.task_id;
2186
+ const limit = parseInt(parsed.query.limit) || 50;
2187
+ if (!taskId)
2188
+ return sendJson(res, { error: "缺少任务 ID" }, 400);
2189
+ const logs = getTaskLogs(taskId, limit);
2190
+ return sendJson(res, { success: true, logs });
2191
+ }
2192
+ // 清空任务日志
2193
+ if (pathname === "/api/tasks/logs/clear" && req.method === "POST") {
2194
+ let body = "";
2195
+ req.on("data", (chunk) => body += chunk);
2196
+ req.on("end", () => {
2197
+ try {
2198
+ const { task_id } = JSON.parse(body);
2199
+ if (!task_id)
2200
+ return sendJson(res, { error: "缺少任务 ID" }, 400);
2201
+ clearTaskLogs(task_id);
2202
+ sendJson(res, { success: true, message: "日志已清空" });
2203
+ }
2204
+ catch (e) {
2205
+ sendJson(res, { error: e.message }, 400);
2206
+ }
2207
+ });
2208
+ return;
2209
+ }
2210
+ // === 飞书通知配置 API ===
2211
+ // 获取飞书配置
2212
+ if (pathname === "/api/feishu/config" && req.method === "GET") {
2213
+ const config = loadFeishuConfig();
2214
+ return sendJson(res, {
2215
+ config: {
2216
+ app_id: config.app_id || "",
2217
+ app_secret: config.app_secret || "",
2218
+ enabled: config.enabled !== false,
2219
+ authorized: config.authorized || false,
2220
+ authorized_user: config.authorized_user || null,
2221
+ }
2222
+ });
2223
+ }
2224
+ // 更新飞书配置
2225
+ if (pathname === "/api/feishu/config" && req.method === "POST") {
2226
+ let body = "";
2227
+ req.on("data", (chunk) => body += chunk);
2228
+ req.on("end", () => {
2229
+ try {
2230
+ const updates = JSON.parse(body);
2231
+ const config = loadFeishuConfig();
2232
+ // 更新配置字段
2233
+ if (updates.app_id !== undefined)
2234
+ config.app_id = updates.app_id;
2235
+ if (updates.app_secret !== undefined && updates.app_secret !== "")
2236
+ config.app_secret = updates.app_secret;
2237
+ if (updates.webhook_url !== undefined)
2238
+ config.webhook_url = updates.webhook_url;
2239
+ if (updates.sign_key !== undefined && updates.sign_key !== "******")
2240
+ config.sign_key = updates.sign_key;
2241
+ if (updates.enabled !== undefined)
2242
+ config.enabled = updates.enabled;
2243
+ if (updates.redirect_uri !== undefined)
2244
+ config.redirect_uri = updates.redirect_uri;
2245
+ console.log("[飞书配置] 保存配置:", { app_id: config.app_id, app_secret: config.app_secret ? "***" : "空" });
2246
+ saveFeishuConfig(config);
2247
+ sendJson(res, { success: true, message: "飞书配置已保存" });
2248
+ }
2249
+ catch (e) {
2250
+ sendJson(res, { error: e.message }, 400);
2251
+ }
2252
+ });
2253
+ return;
2254
+ }
2255
+ // 获取 OAuth 授权 URL
2256
+ if (pathname === "/api/feishu/auth-url" && req.method === "GET") {
2257
+ const config = loadFeishuConfig();
2258
+ if (!config.app_id) {
2259
+ return sendJson(res, { error: "请先配置 App ID" }, 400);
2260
+ }
2261
+ const scopes = (config.scopes || FEISHU_SCOPES).join(" ");
2262
+ const authUrl = `https://open.feishu.cn/open-apis/authen/v1/authorize?app_id=${config.app_id}&redirect_uri=${encodeURIComponent(config.redirect_uri)}&scope=${encodeURIComponent(scopes)}&state=ccm_auth`;
2263
+ return sendJson(res, { success: true, auth_url: authUrl });
2264
+ }
2265
+ // OAuth 回调处理
2266
+ if (pathname === "/api/feishu/callback" && req.method === "GET") {
2267
+ const code = parsed.query.code;
2268
+ const state = parsed.query.state;
2269
+ if (!code) {
2270
+ res.writeHead(400, { "Content-Type": "text/html" });
2271
+ res.end("<h1>授权失败:缺少 code 参数</h1>");
2272
+ return;
2273
+ }
2274
+ const config = loadFeishuConfig();
2275
+ if (!config.app_id || !config.app_secret) {
2276
+ res.writeHead(400, { "Content-Type": "text/html" });
2277
+ res.end("<h1>授权失败:未配置 App ID 或 Secret</h1>");
2278
+ return;
2279
+ }
2280
+ // 用 code 换取 user_access_token(使用 Promise 处理异步)
2281
+ getFeishuUserToken(config.app_id, config.app_secret, code).then(tokenData => {
2282
+ if (!tokenData) {
2283
+ res.writeHead(400, { "Content-Type": "text/html" });
2284
+ res.end("<h1>授权失败:获取 Token 失败</h1>");
2285
+ return;
2286
+ }
2287
+ // 保存 token
2288
+ config.user_access_token = tokenData.access_token;
2289
+ config.user_refresh_token = tokenData.refresh_token;
2290
+ config.token_expires_at = new Date(Date.now() + tokenData.expires_in * 1000).toISOString();
2291
+ config.authorized = true;
2292
+ // 获取用户信息
2293
+ return getFeishuUserInfo(tokenData.access_token).then(userInfo => {
2294
+ if (userInfo) {
2295
+ config.authorized_user = {
2296
+ name: userInfo.name,
2297
+ open_id: userInfo.open_id,
2298
+ avatar: userInfo.avatar_url
2299
+ };
2300
+ }
2301
+ saveFeishuConfig(config);
2302
+ // 返回成功页面
2303
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
2304
+ res.end(`
2305
+ <!DOCTYPE html>
2306
+ <html>
2307
+ <head><title>飞书授权成功</title></head>
2308
+ <body style="font-family:sans-serif;text-align:center;padding:50px">
2309
+ <h1 style="color:#22c55e">✅ 飞书授权成功!</h1>
2310
+ <p>用户:${userInfo?.name || '未知'}</p>
2311
+ <p>授权已生效,可以关闭此页面。</p>
2312
+ <script>setTimeout(() => window.close(), 3000);</script>
2313
+ </body>
2314
+ </html>
2315
+ `);
2316
+ });
2317
+ }).catch(err => {
2318
+ console.error("[飞书授权] 回调处理失败:", err.message);
2319
+ if (!res.headersSent) {
2320
+ res.writeHead(500, { "Content-Type": "text/html" });
2321
+ res.end("<h1>授权失败:服务器错误</h1>");
2322
+ }
2323
+ });
2324
+ return;
2325
+ }
2326
+ // 撤销授权
2327
+ if (pathname === "/api/feishu/revoke" && req.method === "POST") {
2328
+ const config = loadFeishuConfig();
2329
+ config.authorized = false;
2330
+ config.user_access_token = "";
2331
+ config.user_refresh_token = "";
2332
+ config.token_expires_at = null;
2333
+ config.authorized_user = null;
2334
+ saveFeishuConfig(config);
2335
+ sendJson(res, { success: true, message: "授权已撤销" });
2336
+ return;
2337
+ }
2338
+ // 获取群聊列表(需要授权)
2339
+ if (pathname === "/api/feishu/chats" && req.method === "GET") {
2340
+ getValidFeishuToken().then(async (token) => {
2341
+ if (!token) {
2342
+ sendJson(res, { error: "未授权或 Token 无效,请先完成飞书授权" }, 401);
2343
+ return;
2344
+ }
2345
+ const chats = await getFeishuChatList(token);
2346
+ if (!res.headersSent) {
2347
+ sendJson(res, { success: true, chats: chats || [] });
2348
+ }
2349
+ }).catch(err => {
2350
+ console.error("[飞书] 获取群聊列表失败:", err.message);
2351
+ if (!res.headersSent) {
2352
+ sendJson(res, { error: "获取群聊列表失败" }, 500);
2353
+ }
2354
+ });
2355
+ return;
2356
+ }
2357
+ // 测试飞书通知
2358
+ if (pathname === "/api/feishu/test" && req.method === "POST") {
2359
+ const config = loadFeishuConfig();
2360
+ // 检查配置
2361
+ if (!config.app_id) {
2362
+ return sendJson(res, { error: "请先配置飞书 App ID" }, 400);
2363
+ }
2364
+ // 检查是否已授权
2365
+ const userId = config.authorized_user?.open_id;
2366
+ if (!userId) {
2367
+ return sendJson(res, { error: "请先扫码授权获取用户 ID" }, 400);
2368
+ }
2369
+ const testCard = {
2370
+ config: { wide_screen_mode: true },
2371
+ header: {
2372
+ title: { tag: "plain_text", content: "🔔 测试通知" },
2373
+ template: "blue"
2374
+ },
2375
+ elements: [
2376
+ {
2377
+ tag: "div",
2378
+ text: {
2379
+ tag: "lark_md",
2380
+ content: `**ccm 控制台通知测试**\n\n发送时间:${new Date().toLocaleString("zh-CN")}\n\n配置验证成功!✅`
2381
+ }
2382
+ }
2383
+ ]
2384
+ };
2385
+ sendFeishuMessageToUser(userId, JSON.stringify(testCard), "interactive").then(success => {
2386
+ if (success) {
2387
+ sendJson(res, { success: true, message: "测试消息已发送!请检查飞书" });
2388
+ }
2389
+ else {
2390
+ sendJson(res, { error: "发送失败,请检查配置" }, 500);
2391
+ }
2392
+ }).catch(err => {
2393
+ console.error("[飞书] 测试通知失败:", err.message);
2394
+ sendJson(res, { error: "发送失败: " + err.message }, 500);
2395
+ });
2396
+ return;
2397
+ }
2398
+ // === MCP 工具 API ===
2399
+ if (pathname === "/api/mcp" && req.method === "GET") {
2400
+ return sendJson(res, { tools: loadMcpTools() });
2401
+ }
2402
+ if (pathname === "/api/mcp" && req.method === "POST") {
2403
+ let body = "";
2404
+ req.on("data", (chunk) => body += chunk);
2405
+ req.on("end", () => {
2406
+ try {
2407
+ const tool = JSON.parse(body);
2408
+ if (!tool.name)
2409
+ return sendJson(res, { error: "名称不能为空" }, 400);
2410
+ tool.type = "mcp";
2411
+ tool.created_at = tool.created_at || new Date().toISOString();
2412
+ saveMcpTool(tool);
2413
+ sendJson(res, { success: true, tool });
2414
+ }
2415
+ catch (e) {
2416
+ sendJson(res, { error: e.message }, 400);
2417
+ }
2418
+ });
2419
+ return;
2420
+ }
2421
+ if (pathname === "/api/mcp/delete" && req.method === "POST") {
2422
+ let body = "";
2423
+ req.on("data", (chunk) => body += chunk);
2424
+ req.on("end", () => {
2425
+ try {
2426
+ const { name } = JSON.parse(body);
2427
+ deleteMcpTool(name);
2428
+ sendJson(res, { success: true });
2429
+ }
2430
+ catch (e) {
2431
+ sendJson(res, { error: e.message }, 400);
2432
+ }
2433
+ });
2434
+ return;
2435
+ }
2436
+ // === Skills API ===
2437
+ if (pathname === "/api/skills" && req.method === "GET") {
2438
+ return sendJson(res, { skills: loadSkills() });
2439
+ }
2440
+ if (pathname === "/api/skills" && req.method === "POST") {
2441
+ let body = "";
2442
+ req.on("data", (chunk) => body += chunk);
2443
+ req.on("end", () => {
2444
+ try {
2445
+ const skill = JSON.parse(body);
2446
+ if (!skill.name)
2447
+ return sendJson(res, { error: "名称不能为空" }, 400);
2448
+ skill.type = "skill";
2449
+ skill.created_at = skill.created_at || new Date().toISOString();
2450
+ saveSkill(skill);
2451
+ sendJson(res, { success: true, skill });
2452
+ }
2453
+ catch (e) {
2454
+ sendJson(res, { error: e.message }, 400);
2455
+ }
2456
+ });
2457
+ return;
2458
+ }
2459
+ if (pathname === "/api/skills/delete" && req.method === "POST") {
2460
+ let body = "";
2461
+ req.on("data", (chunk) => body += chunk);
2462
+ req.on("end", () => {
2463
+ try {
2464
+ const { name } = JSON.parse(body);
2465
+ deleteSkill(name);
2466
+ sendJson(res, { success: true });
2467
+ }
2468
+ catch (e) {
2469
+ sendJson(res, { error: e.message }, 400);
2470
+ }
2471
+ });
2472
+ return;
2473
+ }
2474
+ // === 对话模板 API ===
2475
+ // 获取所有模板
2476
+ if (pathname === "/api/templates" && req.method === "GET") {
2477
+ const category = parsed.query.category;
2478
+ let templates = loadTemplates();
2479
+ if (category) {
2480
+ templates = templates.filter(t => t.category === category);
2481
+ }
2482
+ return sendJson(res, { templates });
2483
+ }
2484
+ // 获取单个模板
2485
+ if (pathname.match(/^\/api\/templates\/[\w-]+$/) && req.method === "GET") {
2486
+ const id = pathname.split("/").pop();
2487
+ const templates = loadTemplates();
2488
+ const template = templates.find(t => t.id === id);
2489
+ if (!template)
2490
+ return sendJson(res, { error: "模板不存在" }, 404);
2491
+ return sendJson(res, { template });
2492
+ }
2493
+ // 创建模板
2494
+ if (pathname === "/api/templates" && req.method === "POST") {
2495
+ let body = "";
2496
+ req.on("data", (chunk) => body += chunk);
2497
+ req.on("end", () => {
2498
+ try {
2499
+ const template = createTemplate(JSON.parse(body));
2500
+ sendJson(res, { success: true, template });
2501
+ }
2502
+ catch (e) {
2503
+ sendJson(res, { error: e.message }, 400);
2504
+ }
2505
+ });
2506
+ return;
2507
+ }
2508
+ // 更新模板
2509
+ if (pathname.match(/^\/api\/templates\/[\w-]+$/) && req.method === "PUT") {
2510
+ const id = pathname.split("/").pop();
2511
+ let body = "";
2512
+ req.on("data", (chunk) => body += chunk);
2513
+ req.on("end", () => {
2514
+ try {
2515
+ const template = updateTemplate(id, JSON.parse(body));
2516
+ if (!template)
2517
+ return sendJson(res, { error: "模板不存在" }, 404);
2518
+ sendJson(res, { success: true, template });
2519
+ }
2520
+ catch (e) {
2521
+ sendJson(res, { error: e.message }, 400);
2522
+ }
2523
+ });
2524
+ return;
2525
+ }
2526
+ // 删除模板
2527
+ if (pathname.match(/^\/api\/templates\/[\w-]+$/) && req.method === "DELETE") {
2528
+ const id = pathname.split("/").pop();
2529
+ deleteTemplate(id);
2530
+ sendJson(res, { success: true });
2531
+ return;
2532
+ }
2533
+ // 获取模板分类
2534
+ if (pathname === "/api/templates/categories" && req.method === "GET") {
2535
+ const categories = [
2536
+ { id: "development", name: "开发", icon: "💻" },
2537
+ { id: "maintenance", name: "维护", icon: "🔧" },
2538
+ { id: "review", name: "审查", icon: "🔍" },
2539
+ { id: "collaboration", name: "协作", icon: "🤝" },
2540
+ { id: "planning", name: "规划", icon: "📋" },
2541
+ { id: "custom", name: "自定义", icon: "✏️" }
2542
+ ];
2543
+ return sendJson(res, { categories });
2544
+ }
2545
+ // === 定时任务 API ===
2546
+ if (pathname === "/api/cron" && req.method === "GET") {
2547
+ return sendJson(res, { jobs: loadCronJobs() });
2548
+ }
2549
+ if (pathname === "/api/cron/create" && req.method === "POST") {
2550
+ let body = "";
2551
+ req.on("data", (chunk) => body += chunk);
2552
+ req.on("end", () => {
2553
+ try {
2554
+ const job = createCronJob(JSON.parse(body));
2555
+ sendJson(res, { success: true, job });
2556
+ }
2557
+ catch (e) {
2558
+ sendJson(res, { error: e.message }, 400);
2559
+ }
2560
+ });
2561
+ return;
2562
+ }
2563
+ if (pathname === "/api/cron/update" && req.method === "POST") {
2564
+ let body = "";
2565
+ req.on("data", (chunk) => body += chunk);
2566
+ req.on("end", () => {
2567
+ try {
2568
+ const { id, ...updates } = JSON.parse(body);
2569
+ const job = updateCronJob(id, updates);
2570
+ if (!job)
2571
+ return sendJson(res, { error: "定时任务不存在" }, 404);
2572
+ sendJson(res, { success: true, job });
2573
+ }
2574
+ catch (e) {
2575
+ sendJson(res, { error: e.message }, 400);
2576
+ }
2577
+ });
2578
+ return;
2579
+ }
2580
+ if (pathname === "/api/cron/delete" && req.method === "POST") {
2581
+ let body = "";
2582
+ req.on("data", (chunk) => body += chunk);
2583
+ req.on("end", () => {
2584
+ try {
2585
+ const { id } = JSON.parse(body);
2586
+ deleteCronJob(id);
2587
+ sendJson(res, { success: true });
2588
+ }
2589
+ catch (e) {
2590
+ sendJson(res, { error: e.message }, 400);
2591
+ }
2592
+ });
2593
+ return;
2594
+ }
2595
+ // === 智能标题生成 ===
2596
+ function generateTitle(message) {
2597
+ if (!message)
2598
+ return "新会话";
2599
+ let text = message.trim();
2600
+ // 去掉常见前缀
2601
+ text = text.replace(/^(帮我|请|麻烦|帮忙|能不能|可以)\s*/i, "");
2602
+ // 按标点截断,取第一句
2603
+ const firstSentence = text.split(/[。!?\n.!?]/)[0].trim();
2604
+ if (firstSentence.length > 0)
2605
+ text = firstSentence;
2606
+ // 如果有代码相关关键词,加上标签
2607
+ const tags = {
2608
+ "bug|报错|错误|异常|失败|fix|修复": "🐛",
2609
+ "接口|api|API|请求|返回": "🔌",
2610
+ "页面|前端|UI|样式|布局": "🎨",
2611
+ "数据库|sql|SQL|表|字段": "🗄️",
2612
+ "部署|上线|发布|docker": "🚀",
2613
+ "测试|test|单元测试": "🧪",
2614
+ "优化|性能|重构": "⚡",
2615
+ "新增|添加|功能|需求": "✨",
2616
+ };
2617
+ let icon = "";
2618
+ for (const [pattern, emoji] of Object.entries(tags)) {
2619
+ if (new RegExp(pattern, "i").test(text)) {
2620
+ icon = emoji + " ";
2621
+ break;
2622
+ }
2623
+ }
2624
+ // 截断到合适长度
2625
+ if (text.length > 18) {
2626
+ text = text.substring(0, 18);
2627
+ // 避免在词中间截断
2628
+ const lastSpace = text.lastIndexOf(" ");
2629
+ if (lastSpace > 8)
2630
+ text = text.substring(0, lastSpace);
2631
+ }
2632
+ return icon + text || "新会话";
2633
+ }
2634
+ // === 会话管理 API(文件夹格式 + 同步 cc-connect)===
2635
+ function getNextSessionId(projectName) {
2636
+ const dir = path.join(WEB_SESSIONS_DIR, projectName);
2637
+ const nums = [];
2638
+ if (fs.existsSync(dir)) {
2639
+ fs.readdirSync(dir).filter(f => f.endsWith(".json")).forEach(f => nums.push(parseInt(f.replace("s", "").replace(".json", "")) || 0));
2640
+ }
2641
+ const ccFile = findCcSessionFile(projectName);
2642
+ if (ccFile) {
2643
+ try {
2644
+ const data = JSON.parse(fs.readFileSync(ccFile, "utf-8"));
2645
+ Object.keys(data.sessions || {}).forEach(s => nums.push(parseInt(s.replace("s", "")) || 0));
2646
+ }
2647
+ catch { }
2648
+ }
2649
+ return `s${nums.length > 0 ? Math.max(...nums) + 1 : 1}`;
2650
+ }
2651
+ // 创建新会话
2652
+ if (pathname === "/api/sessions/create" && req.method === "POST") {
2653
+ let body = "";
2654
+ req.on("data", (chunk) => body += chunk);
2655
+ req.on("end", () => {
2656
+ try {
2657
+ const { project, name } = JSON.parse(body);
2658
+ const dir = ensureWebSessionDir(project);
2659
+ const sid = getNextSessionId(project);
2660
+ const now = new Date();
2661
+ const pad = n => String(n).padStart(2, "0");
2662
+ const timeStr = `${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
2663
+ const count = fs.readdirSync(dir).filter(f => f.endsWith(".json")).length;
2664
+ const sessionName = name || `会话 ${count + 1} · ${timeStr}`;
2665
+ const sessionData = { id: sid, name: sessionName, agent_type: "claudecode", history: [], created_at: now.toISOString(), updated_at: now.toISOString() };
2666
+ fs.writeFileSync(path.join(dir, `${sid}.json`), JSON.stringify(sessionData, null, 2));
2667
+ syncToFilesystemToCc(project);
2668
+ sendJson(res, { success: true, sessionId: sid, name: sessionName });
2669
+ }
2670
+ catch (e) {
2671
+ sendJson(res, { error: e.message }, 400);
2672
+ }
2673
+ });
2674
+ return;
2675
+ }
2676
+ // 删除会话
2677
+ if (pathname === "/api/sessions/delete" && req.method === "POST") {
2678
+ let body = "";
2679
+ req.on("data", (chunk) => body += chunk);
2680
+ req.on("end", () => {
2681
+ try {
2682
+ const { project, sessionId } = JSON.parse(body);
2683
+ const filePath = path.join(WEB_SESSIONS_DIR, project, `${sessionId}.json`);
2684
+ if (!fs.existsSync(filePath))
2685
+ return sendJson(res, { error: "会话不存在" }, 404);
2686
+ fs.unlinkSync(filePath);
2687
+ const ccFile = findCcSessionFile(project);
2688
+ if (ccFile) {
2689
+ try {
2690
+ const data = JSON.parse(fs.readFileSync(ccFile, "utf-8"));
2691
+ delete data.sessions[sessionId];
2692
+ for (const [k, v] of Object.entries(data.active_session || {})) {
2693
+ if (v === sessionId)
2694
+ delete data.active_session[k];
2695
+ }
2696
+ fs.writeFileSync(ccFile, JSON.stringify(data, null, 2));
2697
+ }
2698
+ catch { }
2699
+ }
2700
+ sendJson(res, { success: true });
2701
+ }
2702
+ catch (e) {
2703
+ sendJson(res, { error: e.message }, 400);
2704
+ }
2705
+ });
2706
+ return;
2707
+ }
2708
+ // 重命名会话
2709
+ if (pathname === "/api/sessions/rename" && req.method === "POST") {
2710
+ let body = "";
2711
+ req.on("data", (chunk) => body += chunk);
2712
+ req.on("end", () => {
2713
+ try {
2714
+ const { project, sessionId, name } = JSON.parse(body);
2715
+ const filePath = path.join(WEB_SESSIONS_DIR, project, `${sessionId}.json`);
2716
+ if (!fs.existsSync(filePath))
2717
+ return sendJson(res, { error: "会话不存在" }, 404);
2718
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2719
+ data.name = name;
2720
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
2721
+ const ccFile = findCcSessionFile(project);
2722
+ if (ccFile) {
2723
+ try {
2724
+ const ccData = JSON.parse(fs.readFileSync(ccFile, "utf-8"));
2725
+ if (ccData.sessions[sessionId]) {
2726
+ ccData.sessions[sessionId].name = name;
2727
+ fs.writeFileSync(ccFile, JSON.stringify(ccData, null, 2));
2728
+ }
2729
+ }
2730
+ catch { }
2731
+ }
2732
+ sendJson(res, { success: true });
2733
+ }
2734
+ catch (e) {
2735
+ sendJson(res, { error: e.message }, 400);
2736
+ }
2737
+ });
2738
+ return;
2739
+ }
2740
+ // 自动命名会话(AI 生成标题)
2741
+ if (pathname === "/api/sessions/auto-name" && req.method === "POST") {
2742
+ let body = "";
2743
+ req.on("data", (chunk) => body += chunk);
2744
+ req.on("end", async () => {
2745
+ try {
2746
+ const { project, sessionId, message, workDir } = JSON.parse(body);
2747
+ const filePath = path.join(getProjectSessionDir(project), `${sessionId}.json`);
2748
+ if (!fs.existsSync(filePath))
2749
+ return sendJson(res, { error: "会话不存在" }, 404);
2750
+ let title = "";
2751
+ // 尝试用 AI 生成标题
2752
+ try {
2753
+ const prompt = `根据以下消息生成简短中文标题(不超过15字,无引号无标点):${message}`;
2754
+ const tmpFile = path.join(UPLOAD_DIR, `_title_${Date.now()}.txt`);
2755
+ fs.writeFileSync(tmpFile, prompt, "utf-8");
2756
+ const safeCwd = (workDir || process.cwd()).replace(/\\/g, "/");
2757
+ const result = execSync(`type "${tmpFile}" | claude -p`, {
2758
+ encoding: "utf-8", timeout: 30000, cwd: safeCwd,
2759
+ shell: true,
2760
+ maxBuffer: 1024 * 1024,
2761
+ });
2762
+ try {
2763
+ fs.unlinkSync(tmpFile);
2764
+ }
2765
+ catch { }
2766
+ title = result.trim().replace(/^["'"「『【\*]+|["'"」』】\*]+$/g, "").substring(0, 20);
2767
+ }
2768
+ catch (aiErr) {
2769
+ console.log("AI命名失败:", (aiErr.message || "").substring(0, 200));
2770
+ }
2771
+ // AI 失败则用规则生成
2772
+ if (!title)
2773
+ title = generateTitle(message);
2774
+ // 更新会话名称
2775
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
2776
+ data.name = title;
2777
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
2778
+ sendJson(res, { success: true, name: title });
2779
+ }
2780
+ catch (e) {
2781
+ sendJson(res, { error: e.message }, 400);
2782
+ }
2783
+ });
2784
+ return;
2785
+ }
2786
+ // === 流式发送消息给 Agent(SSE)===
2787
+ if (pathname === "/api/send-stream" && req.method === "POST") {
2788
+ let body = "";
2789
+ req.on("data", (chunk) => body += chunk);
2790
+ req.on("end", () => {
2791
+ try {
2792
+ const { project, message } = JSON.parse(body);
2793
+ if (!project || !message)
2794
+ return sendJson(res, { error: "参数不足" }, 400);
2795
+ const configs = getConfigs();
2796
+ const config = configs.find(c => c.name === project);
2797
+ if (!config)
2798
+ return sendJson(res, { error: "项目不存在" }, 400);
2799
+ const info = getConfigInfo(config.path);
2800
+ const workDir = info[0]?.workDir;
2801
+ const agentType = info[0]?.agent || "claudecode";
2802
+ callAgentStream(project, message, workDir, agentType, res);
2803
+ }
2804
+ catch (e) {
2805
+ sendJson(res, { error: e.message }, 400);
2806
+ }
2807
+ });
2808
+ return;
2809
+ }
2810
+ // === 发送消息给 Agent ===
2811
+ if (pathname === "/api/send" && req.method === "POST") {
2812
+ const contentType = req.headers["content-type"] || "";
2813
+ async function handleSend(project, message, files) {
2814
+ // 获取项目 work_dir
2815
+ const configs = getConfigs();
2816
+ const config = configs.find(c => c.name === project);
2817
+ if (!config)
2818
+ return sendJson(res, { error: "项目不存在" }, 400);
2819
+ const info = getConfigInfo(config.path);
2820
+ const workDir = info[0]?.workDir;
2821
+ if (!workDir)
2822
+ return sendJson(res, { error: "无法获取项目目录" }, 400);
2823
+ // 构建完整消息
2824
+ let fullMessage = message || "";
2825
+ if (files && files.length > 0) {
2826
+ const fileList = files.map(f => `[文件: ${f.filename} -> ${f.savedPath}]`).join("\n");
2827
+ fullMessage = fullMessage ? `${fullMessage}\n\n${fileList}` : `请处理以下文件:\n${fileList}`;
2828
+ }
2829
+ if (!fullMessage)
2830
+ return sendJson(res, { error: "消息不能为空" }, 400);
2831
+ // 获取项目配置的 Agent 类型
2832
+ const agentType = getConfigInfo(config.path)[0]?.agent || "claudecode";
2833
+ const safeCwd = workDir.replace(/\\/g, "/");
2834
+ // 根据 Agent 类型调用不同的 CLI
2835
+ try {
2836
+ const tmpMsg = path.join(UPLOAD_DIR, `_msg_${Date.now()}.txt`);
2837
+ fs.writeFileSync(tmpMsg, fullMessage, "utf-8");
2838
+ let cmd;
2839
+ switch (agentType) {
2840
+ case "cursor":
2841
+ cmd = `type "${tmpMsg}" | agent -p`;
2842
+ break;
2843
+ case "gemini":
2844
+ cmd = `type "${tmpMsg}" | gemini -p`;
2845
+ break;
2846
+ case "codex":
2847
+ cmd = `type "${tmpMsg}" | codex -q`;
2848
+ break;
2849
+ case "claudecode":
2850
+ default:
2851
+ cmd = `type "${tmpMsg}" | claude -p`;
2852
+ break;
2853
+ }
2854
+ const result = execSync(cmd, {
2855
+ encoding: "utf-8",
2856
+ timeout: 120000,
2857
+ cwd: safeCwd,
2858
+ shell: true,
2859
+ maxBuffer: 10 * 1024 * 1024,
2860
+ });
2861
+ try {
2862
+ fs.unlinkSync(tmpMsg);
2863
+ }
2864
+ catch { }
2865
+ return sendJson(res, { success: true, output: result });
2866
+ }
2867
+ catch (e) {
2868
+ return sendJson(res, { error: e.stdout || e.stderr || "发送失败" }, 500);
2869
+ }
2870
+ }
2871
+ // multipart(带文件)
2872
+ if (contentType.includes("multipart/form-data")) {
2873
+ const chunks = [];
2874
+ req.on("data", (chunk) => chunks.push(chunk));
2875
+ req.on("end", async () => {
2876
+ try {
2877
+ const buffer = Buffer.concat(chunks);
2878
+ const boundaryMatch = contentType.match(/boundary=(.+)/);
2879
+ if (!boundaryMatch)
2880
+ return sendJson(res, { error: "无效请求" }, 400);
2881
+ const { files, fields } = parseMultipart(buffer, boundaryMatch[1]);
2882
+ await handleSend(fields.project, fields.message, files);
2883
+ }
2884
+ catch (e) {
2885
+ sendJson(res, { error: e.message }, 400);
2886
+ }
2887
+ });
2888
+ return;
2889
+ }
2890
+ // 纯文本 JSON
2891
+ let body = "";
2892
+ req.on("data", (chunk) => body += chunk);
2893
+ req.on("end", async () => {
2894
+ try {
2895
+ const { project, message } = JSON.parse(body);
2896
+ await handleSend(project, message, null);
2897
+ }
2898
+ catch (e) {
2899
+ sendJson(res, { error: e.message }, 400);
2900
+ }
2901
+ });
2902
+ return;
2903
+ }
2904
+ // === 群聊 API ===
2905
+ // 获取所有群聊
2906
+ if (pathname === "/api/groups" && req.method === "GET") {
2907
+ return sendJson(res, { groups: loadGroups() });
2908
+ }
2909
+ // 创建群聊(自动加入主 Agent)
2910
+ if (pathname === "/api/groups/create" && req.method === "POST") {
2911
+ let body = "";
2912
+ req.on("data", (chunk) => body += chunk);
2913
+ req.on("end", () => {
2914
+ try {
2915
+ const { name, members } = JSON.parse(body);
2916
+ const groups = loadGroups();
2917
+ const id = "g" + Date.now().toString(36);
2918
+ // 自动加入主 Agent(协调者)
2919
+ const allMembers = [
2920
+ { project: "coordinator", role: "coordinator", agent: "claudecode" },
2921
+ ...(members || [])
2922
+ ];
2923
+ const group = {
2924
+ id, name, members: allMembers,
2925
+ created_at: new Date().toISOString(),
2926
+ };
2927
+ groups.push(group);
2928
+ saveGroups(groups);
2929
+ sendJson(res, { success: true, group });
2930
+ }
2931
+ catch (e) {
2932
+ sendJson(res, { error: e.message }, 400);
2933
+ }
2934
+ });
2935
+ return;
2936
+ }
2937
+ // 更新群聊成员
2938
+ if (pathname === "/api/groups/members" && req.method === "POST") {
2939
+ let body = "";
2940
+ req.on("data", (chunk) => body += chunk);
2941
+ req.on("end", () => {
2942
+ try {
2943
+ const { id, add, remove } = JSON.parse(body);
2944
+ const groups = loadGroups();
2945
+ const group = groups.find(g => g.id === id);
2946
+ if (!group)
2947
+ return sendJson(res, { error: "群聊不存在" }, 404);
2948
+ if (add) {
2949
+ for (const m of add) {
2950
+ if (!group.members.find(x => x.project === m.project)) {
2951
+ group.members.push(m);
2952
+ }
2953
+ }
2954
+ }
2955
+ if (remove) {
2956
+ group.members = group.members.filter(m => !remove.includes(m.project) || m.project === "coordinator");
2957
+ }
2958
+ saveGroups(groups);
2959
+ sendJson(res, { success: true, group });
2960
+ }
2961
+ catch (e) {
2962
+ sendJson(res, { error: e.message }, 400);
2963
+ }
2964
+ });
2965
+ return;
2966
+ }
2967
+ // 删除群聊
2968
+ if (pathname === "/api/groups/delete" && req.method === "POST") {
2969
+ let body = "";
2970
+ req.on("data", (chunk) => body += chunk);
2971
+ req.on("end", () => {
2972
+ try {
2973
+ const { id } = JSON.parse(body);
2974
+ const groups = loadGroups().filter(g => g.id !== id);
2975
+ saveGroups(groups);
2976
+ try {
2977
+ fs.unlinkSync(path.join(GROUP_MESSAGES_DIR, `${id}.json`));
2978
+ }
2979
+ catch { }
2980
+ sendJson(res, { success: true });
2981
+ }
2982
+ catch (e) {
2983
+ sendJson(res, { error: e.message }, 400);
2984
+ }
2985
+ });
2986
+ return;
2987
+ }
2988
+ // 重命名群聊
2989
+ if (pathname === "/api/groups/rename" && req.method === "POST") {
2990
+ let body = "";
2991
+ req.on("data", (chunk) => body += chunk);
2992
+ req.on("end", () => {
2993
+ try {
2994
+ const { id, name } = JSON.parse(body);
2995
+ if (!name || !name.trim())
2996
+ return sendJson(res, { error: "群聊名称不能为空" }, 400);
2997
+ const groups = loadGroups();
2998
+ const group = groups.find(g => g.id === id);
2999
+ if (!group)
3000
+ return sendJson(res, { error: "群聊不存在" }, 404);
3001
+ group.name = name.trim();
3002
+ saveGroups(groups);
3003
+ sendJson(res, { success: true, group });
3004
+ }
3005
+ catch (e) {
3006
+ sendJson(res, { error: e.message }, 400);
3007
+ }
3008
+ });
3009
+ return;
3010
+ }
3011
+ // === 群聊工具配置 API ===
3012
+ // 获取群聊工具配置
3013
+ if (pathname === "/api/groups/tools" && req.method === "GET") {
3014
+ const groupId = parsed.query.id;
3015
+ if (!groupId)
3016
+ return sendJson(res, { error: "缺少群聊 ID" }, 400);
3017
+ const groups = loadGroups();
3018
+ const group = groups.find(g => g.id === groupId);
3019
+ if (!group)
3020
+ return sendJson(res, { error: "群聊不存在" }, 404);
3021
+ return sendJson(res, { tools: group.tools || { mcp: [], skill: [] } });
3022
+ }
3023
+ // 更新群聊工具配置
3024
+ if (pathname === "/api/groups/tools" && req.method === "POST") {
3025
+ console.log("[群聊工具] 收到保存请求");
3026
+ let body = "";
3027
+ req.on("data", (chunk) => body += chunk);
3028
+ req.on("end", () => {
3029
+ try {
3030
+ console.log("[群聊工具] 请求体:", body);
3031
+ const { group_id, tools } = JSON.parse(body);
3032
+ console.log("[群聊工具] 解析结果:", { group_id, tools });
3033
+ const groups = loadGroups();
3034
+ const group = groups.find(g => g.id === group_id);
3035
+ if (!group) {
3036
+ console.log("[群聊工具] 群聊不存在:", group_id);
3037
+ return sendJson(res, { error: "群聊不存在" }, 404);
3038
+ }
3039
+ group.tools = tools;
3040
+ saveGroups(groups);
3041
+ console.log("[群聊工具] 保存成功:", group.tools);
3042
+ sendJson(res, { success: true, tools: group.tools });
3043
+ }
3044
+ catch (e) {
3045
+ console.error("[群聊工具] 错误:", e.message);
3046
+ sendJson(res, { error: e.message }, 400);
3047
+ }
3048
+ });
3049
+ return;
3050
+ }
3051
+ // === 项目工具和共享文件 API ===
3052
+ const PROJECT_CONFIGS_FILE = path.join(CCM_DIR, "project-configs.json");
3053
+ function loadProjectConfigs() {
3054
+ try {
3055
+ if (fs.existsSync(PROJECT_CONFIGS_FILE)) {
3056
+ return JSON.parse(fs.readFileSync(PROJECT_CONFIGS_FILE, "utf-8"));
3057
+ }
3058
+ }
3059
+ catch (e) {
3060
+ console.error("加载项目配置失败:", e.message);
3061
+ }
3062
+ return {};
3063
+ }
3064
+ function saveProjectConfigs(configs) {
3065
+ fs.writeFileSync(PROJECT_CONFIGS_FILE, JSON.stringify(configs, null, 2));
3066
+ }
3067
+ // 获取项目工具配置
3068
+ if (pathname === "/api/projects/tools" && req.method === "GET") {
3069
+ const project = parsed.query.project;
3070
+ if (!project)
3071
+ return sendJson(res, { error: "缺少项目参数" }, 400);
3072
+ const configs = loadProjectConfigs();
3073
+ return sendJson(res, { tools: configs[project]?.tools || { mcp: [], skill: [] } });
3074
+ }
3075
+ // 更新项目工具配置
3076
+ if (pathname === "/api/projects/tools" && req.method === "POST") {
3077
+ let body = "";
3078
+ req.on("data", (chunk) => body += chunk);
3079
+ req.on("end", () => {
3080
+ try {
3081
+ const { project, tools } = JSON.parse(body);
3082
+ if (!project)
3083
+ return sendJson(res, { error: "缺少项目参数" }, 400);
3084
+ const configs = loadProjectConfigs();
3085
+ if (!configs[project])
3086
+ configs[project] = {};
3087
+ configs[project].tools = tools;
3088
+ saveProjectConfigs(configs);
3089
+ sendJson(res, { success: true, tools });
3090
+ }
3091
+ catch (e) {
3092
+ sendJson(res, { error: e.message }, 400);
3093
+ }
3094
+ });
3095
+ return;
3096
+ }
3097
+ // 获取项目共享文件
3098
+ if (pathname === "/api/projects/shared" && req.method === "GET") {
3099
+ const project = parsed.query.project;
3100
+ if (!project)
3101
+ return sendJson(res, { error: "缺少项目参数" }, 400);
3102
+ const configs = loadProjectConfigs();
3103
+ return sendJson(res, { files: configs[project]?.shared_files || [] });
3104
+ }
3105
+ // 添加项目共享文件
3106
+ if (pathname === "/api/projects/shared/add" && req.method === "POST") {
3107
+ let body = "";
3108
+ req.on("data", (chunk) => body += chunk);
3109
+ req.on("end", () => {
3110
+ try {
3111
+ const { project, name, content } = JSON.parse(body);
3112
+ if (!project || !name)
3113
+ return sendJson(res, { error: "缺少参数" }, 400);
3114
+ const configs = loadProjectConfigs();
3115
+ if (!configs[project])
3116
+ configs[project] = {};
3117
+ if (!configs[project].shared_files)
3118
+ configs[project].shared_files = [];
3119
+ const existing = configs[project].shared_files.findIndex(f => f.name === name);
3120
+ if (existing >= 0) {
3121
+ configs[project].shared_files[existing].content = content;
3122
+ configs[project].shared_files[existing].updated_at = new Date().toISOString();
3123
+ }
3124
+ else {
3125
+ configs[project].shared_files.push({
3126
+ name, content,
3127
+ created_at: new Date().toISOString(),
3128
+ updated_at: new Date().toISOString()
3129
+ });
3130
+ }
3131
+ saveProjectConfigs(configs);
3132
+ sendJson(res, { success: true, files: configs[project].shared_files });
3133
+ }
3134
+ catch (e) {
3135
+ sendJson(res, { error: e.message }, 400);
3136
+ }
3137
+ });
3138
+ return;
3139
+ }
3140
+ // 删除项目共享文件
3141
+ if (pathname === "/api/projects/shared/delete" && req.method === "POST") {
3142
+ let body = "";
3143
+ req.on("data", (chunk) => body += chunk);
3144
+ req.on("end", () => {
3145
+ try {
3146
+ const { project, name } = JSON.parse(body);
3147
+ if (!project || !name)
3148
+ return sendJson(res, { error: "缺少参数" }, 400);
3149
+ const configs = loadProjectConfigs();
3150
+ if (configs[project]?.shared_files) {
3151
+ configs[project].shared_files = configs[project].shared_files.filter(f => f.name !== name);
3152
+ saveProjectConfigs(configs);
3153
+ }
3154
+ sendJson(res, { success: true });
3155
+ }
3156
+ catch (e) {
3157
+ sendJson(res, { error: e.message }, 400);
3158
+ }
3159
+ });
3160
+ return;
3161
+ }
3162
+ // === 群聊共享文件 API ===
3163
+ // 获取群聊共享文件列表
3164
+ if (pathname === "/api/groups/shared" && req.method === "GET") {
3165
+ const groupId = parsed.query.id;
3166
+ if (!groupId)
3167
+ return sendJson(res, { error: "缺少群聊 ID" }, 400);
3168
+ const groups = loadGroups();
3169
+ const group = groups.find(g => g.id === groupId);
3170
+ if (!group)
3171
+ return sendJson(res, { error: "群聊不存在" }, 404);
3172
+ return sendJson(res, { files: group.shared_files || [] });
3173
+ }
3174
+ // 添加群聊共享文件
3175
+ if (pathname === "/api/groups/shared/add" && req.method === "POST") {
3176
+ let body = "";
3177
+ req.on("data", (chunk) => body += chunk);
3178
+ req.on("end", () => {
3179
+ try {
3180
+ const { group_id, name, content } = JSON.parse(body);
3181
+ if (!name || !content)
3182
+ return sendJson(res, { error: "文件名和内容不能为空" }, 400);
3183
+ const groups = loadGroups();
3184
+ const group = groups.find(g => g.id === group_id);
3185
+ if (!group)
3186
+ return sendJson(res, { error: "群聊不存在" }, 404);
3187
+ if (!group.shared_files)
3188
+ group.shared_files = [];
3189
+ // 检查是否已存在,存在则更新
3190
+ const existing = group.shared_files.findIndex(f => f.name === name);
3191
+ if (existing >= 0) {
3192
+ group.shared_files[existing].content = content;
3193
+ group.shared_files[existing].updated_at = new Date().toISOString();
3194
+ }
3195
+ else {
3196
+ group.shared_files.push({
3197
+ name,
3198
+ content,
3199
+ created_at: new Date().toISOString(),
3200
+ updated_at: new Date().toISOString()
3201
+ });
3202
+ }
3203
+ saveGroups(groups);
3204
+ sendJson(res, { success: true, files: group.shared_files });
3205
+ }
3206
+ catch (e) {
3207
+ sendJson(res, { error: e.message }, 400);
3208
+ }
3209
+ });
3210
+ return;
3211
+ }
3212
+ // 删除群聊共享文件
3213
+ if (pathname === "/api/groups/shared/delete" && req.method === "POST") {
3214
+ let body = "";
3215
+ req.on("data", (chunk) => body += chunk);
3216
+ req.on("end", () => {
3217
+ try {
3218
+ const { group_id, name } = JSON.parse(body);
3219
+ const groups = loadGroups();
3220
+ const group = groups.find(g => g.id === group_id);
3221
+ if (!group)
3222
+ return sendJson(res, { error: "群聊不存在" }, 404);
3223
+ if (!group.shared_files)
3224
+ group.shared_files = [];
3225
+ group.shared_files = group.shared_files.filter(f => f.name !== name);
3226
+ saveGroups(groups);
3227
+ sendJson(res, { success: true, files: group.shared_files });
3228
+ }
3229
+ catch (e) {
3230
+ sendJson(res, { error: e.message }, 400);
3231
+ }
3232
+ });
3233
+ return;
3234
+ }
3235
+ // 从全局共享文件导入到群聊
3236
+ if (pathname === "/api/groups/shared/import" && req.method === "POST") {
3237
+ let body = "";
3238
+ req.on("data", (chunk) => body += chunk);
3239
+ req.on("end", () => {
3240
+ try {
3241
+ const { group_id, file_names } = JSON.parse(body);
3242
+ if (!file_names || !Array.isArray(file_names))
3243
+ return sendJson(res, { error: "请提供文件名列表" }, 400);
3244
+ const groups = loadGroups();
3245
+ const group = groups.find(g => g.id === group_id);
3246
+ if (!group)
3247
+ return sendJson(res, { error: "群聊不存在" }, 404);
3248
+ if (!group.shared_files)
3249
+ group.shared_files = [];
3250
+ let imported = 0;
3251
+ for (const name of file_names) {
3252
+ const filePath = path.join(SHARED_DIR, name);
3253
+ if (fs.existsSync(filePath)) {
3254
+ const content = fs.readFileSync(filePath, "utf-8");
3255
+ const existing = group.shared_files.findIndex(f => f.name === name);
3256
+ if (existing >= 0) {
3257
+ group.shared_files[existing].content = content;
3258
+ group.shared_files[existing].updated_at = new Date().toISOString();
3259
+ }
3260
+ else {
3261
+ group.shared_files.push({
3262
+ name,
3263
+ content,
3264
+ created_at: new Date().toISOString(),
3265
+ updated_at: new Date().toISOString()
3266
+ });
3267
+ }
3268
+ imported++;
3269
+ }
3270
+ }
3271
+ saveGroups(groups);
3272
+ sendJson(res, { success: true, imported, files: group.shared_files });
3273
+ }
3274
+ catch (e) {
3275
+ sendJson(res, { error: e.message }, 400);
3276
+ }
3277
+ });
3278
+ return;
3279
+ }
3280
+ // 获取群聊消息
3281
+ if (pathname === "/api/groups/messages" && req.method === "GET") {
3282
+ const groupId = parsed.query.id;
3283
+ if (!groupId)
3284
+ return sendJson(res, { error: "缺少群聊 ID" }, 400);
3285
+ const limit = parseInt(parsed.query.limit) || 100;
3286
+ const messages = getGroupMessages(groupId).slice(-limit);
3287
+ return sendJson(res, { messages });
3288
+ }
3289
+ // === 群聊日志系统 ===
3290
+ const GROUP_LOGS_FILE = path.join(CCM_DIR, "group-logs.json");
3291
+ function loadGroupLogs() {
3292
+ try {
3293
+ if (fs.existsSync(GROUP_LOGS_FILE)) {
3294
+ return JSON.parse(fs.readFileSync(GROUP_LOGS_FILE, "utf-8"));
3295
+ }
3296
+ }
3297
+ catch (e) {
3298
+ console.error("加载群聊日志失败:", e.message);
3299
+ }
3300
+ return {};
3301
+ }
3302
+ function saveGroupLogs(logs) {
3303
+ try {
3304
+ fs.writeFileSync(GROUP_LOGS_FILE, JSON.stringify(logs, null, 2));
3305
+ }
3306
+ catch (e) {
3307
+ console.error("保存群聊日志失败:", e.message);
3308
+ }
3309
+ }
3310
+ function addGroupLog(groupId, level, category, message, details = null) {
3311
+ const logs = loadGroupLogs();
3312
+ if (!logs[groupId])
3313
+ logs[groupId] = [];
3314
+ const logEntry = {
3315
+ timestamp: new Date().toISOString(),
3316
+ level: level,
3317
+ category: category,
3318
+ message: message,
3319
+ details: details
3320
+ };
3321
+ logs[groupId].push(logEntry);
3322
+ // 限制每个群聊最多 500 条日志
3323
+ if (logs[groupId].length > 500) {
3324
+ logs[groupId] = logs[groupId].slice(-500);
3325
+ }
3326
+ saveGroupLogs(logs);
3327
+ }
3328
+ // 获取群聊日志
3329
+ if (pathname === "/api/groups/logs" && req.method === "GET") {
3330
+ const groupId = parsed.query.id;
3331
+ const limit = parseInt(parsed.query.limit) || 100;
3332
+ const category = parsed.query.category; // 可选过滤类别
3333
+ if (!groupId)
3334
+ return sendJson(res, { error: "缺少群聊 ID" }, 400);
3335
+ const logs = loadGroupLogs();
3336
+ let groupLogs = logs[groupId] || [];
3337
+ if (category) {
3338
+ groupLogs = groupLogs.filter(l => l.category === category);
3339
+ }
3340
+ return sendJson(res, { logs: groupLogs.slice(-limit) });
3341
+ }
3342
+ // 清空群聊日志
3343
+ if (pathname === "/api/groups/logs/clear" && req.method === "POST") {
3344
+ let body = "";
3345
+ req.on("data", (chunk) => body += chunk);
3346
+ req.on("end", () => {
3347
+ try {
3348
+ const { group_id } = JSON.parse(body);
3349
+ const logs = loadGroupLogs();
3350
+ delete logs[group_id];
3351
+ saveGroupLogs(logs);
3352
+ sendJson(res, { success: true });
3353
+ }
3354
+ catch (e) {
3355
+ sendJson(res, { error: e.message }, 400);
3356
+ }
3357
+ });
3358
+ return;
3359
+ }
3360
+ // 实时日志流 (SSE)
3361
+ if (pathname === "/api/groups/logs/stream" && req.method === "GET") {
3362
+ const groupId = parsed.query.id;
3363
+ if (!groupId)
3364
+ return sendJson(res, { error: "缺少群聊 ID" }, 400);
3365
+ // 设置 SSE 头
3366
+ res.writeHead(200, {
3367
+ "Content-Type": "text/event-stream",
3368
+ "Cache-Control": "no-cache",
3369
+ "Connection": "keep-alive",
3370
+ "Access-Control-Allow-Origin": "*",
3371
+ });
3372
+ // 发送初始连接消息
3373
+ res.write(`data: ${JSON.stringify({ type: "connected", message: "日志流已连接" })}\n\n`);
3374
+ // 获取初始日志数量
3375
+ const logs = loadGroupLogs();
3376
+ const initialCount = (logs[groupId] || []).length;
3377
+ let lastCount = initialCount;
3378
+ // 定期检查新日志
3379
+ const interval = setInterval(() => {
3380
+ try {
3381
+ const currentLogs = loadGroupLogs();
3382
+ const groupLogs = currentLogs[groupId] || [];
3383
+ if (groupLogs.length > lastCount) {
3384
+ const newLogs = groupLogs.slice(lastCount);
3385
+ for (const log of newLogs) {
3386
+ res.write(`data: ${JSON.stringify({ type: "log", log })}\n\n`);
3387
+ }
3388
+ lastCount = groupLogs.length;
3389
+ }
3390
+ }
3391
+ catch (e) {
3392
+ res.write(`data: ${JSON.stringify({ type: "error", message: e.message })}\n\n`);
3393
+ }
3394
+ }, 1000); // 每秒检查一次
3395
+ // 客户端断开连接时清理
3396
+ req.on("close", () => {
3397
+ clearInterval(interval);
3398
+ });
3399
+ return;
3400
+ }
3401
+ // 在群聊中发消息(用户发给指定 Agent,Agent 回复也在群里)
3402
+ if (pathname === "/api/groups/send" && req.method === "POST") {
3403
+ let body = "";
3404
+ req.on("data", (chunk) => body += chunk);
3405
+ req.on("end", async () => {
3406
+ try {
3407
+ const { group_id, target_project, message } = JSON.parse(body);
3408
+ const groups = loadGroups();
3409
+ const group = groups.find(g => g.id === group_id);
3410
+ if (!group)
3411
+ return sendJson(res, { error: "群聊不存在" }, 400);
3412
+ // 如果 target_project 是 "all" 或 undefined,群发给所有成员
3413
+ const isBroadcast = !target_project || target_project === "all";
3414
+ const targetMembers = isBroadcast
3415
+ ? group.members.filter(m => m.project !== "coordinator")
3416
+ : [group.members.find(m => m.project === target_project)].filter(Boolean);
3417
+ if (targetMembers.length === 0) {
3418
+ return sendJson(res, { error: "没有找到目标项目" }, 400);
3419
+ }
3420
+ // 记录用户消息
3421
+ const userMsg = {
3422
+ id: "m" + Date.now().toString(36),
3423
+ role: "user",
3424
+ target: isBroadcast ? "all" : target_project,
3425
+ content: message,
3426
+ timestamp: new Date().toISOString(),
3427
+ };
3428
+ appendGroupMessage(group_id, userMsg);
3429
+ // 记录日志
3430
+ addGroupLog(group_id, "info", "message", `用户发送消息给 ${isBroadcast ? '所有人' : target_project}`, {
3431
+ message: message.substring(0, 200),
3432
+ target: isBroadcast ? "all" : target_project,
3433
+ is_broadcast: isBroadcast
3434
+ });
3435
+ // 获取项目配置(coordinator 使用第一个项目的目录)
3436
+ const configs = getConfigs();
3437
+ // 如果是群发,处理所有成员
3438
+ if (isBroadcast) {
3439
+ // 流式输出
3440
+ res.writeHead(200, {
3441
+ "Content-Type": "text/event-stream",
3442
+ "Cache-Control": "no-cache",
3443
+ "Connection": "keep-alive",
3444
+ "Access-Control-Allow-Origin": "*",
3445
+ });
3446
+ res.write(`data: ${JSON.stringify({ type: "status", text: "🧠 群发中,所有 Agent 处理中..." })}\n\n`);
3447
+ let allOutputs = [];
3448
+ for (const member of targetMembers) {
3449
+ const config = configs.find(c => c.name === member.project);
3450
+ if (!config)
3451
+ continue;
3452
+ const info = getConfigInfo(config.path);
3453
+ const workDir = info[0]?.workDir;
3454
+ const agentType = info[0]?.agent || "claudecode";
3455
+ // 构建 prompt
3456
+ const recentMsgs = getGroupMessages(group_id).slice(-10);
3457
+ const context = recentMsgs.map(m => {
3458
+ const who = m.role === "user" ? `[用户 → ${m.target}]` : `[${m.agent || "Agent"}]`;
3459
+ return `${who} ${m.content}`;
3460
+ }).join("\n");
3461
+ const memberList = group.members.map(m => m.project).filter(p => p !== member.project && p !== "coordinator").join(", ");
3462
+ const atInstructions = memberList ? `\n\n群聊中还有其他 Agent:${memberList}。如果你需要其他 Agent 协助,在回复中用 @项目名 的格式提出请求。` : "";
3463
+ // 获取群聊共享文件
3464
+ let sharedFilesContext = "";
3465
+ if (group.shared_files && group.shared_files.length > 0) {
3466
+ // 判断是否是文本文件
3467
+ const textExtensions = ['.txt', '.md', '.json', '.js', '.ts', '.html', '.css', '.xml', '.yaml', '.yml', '.csv', '.log', '.py', '.java', '.go', '.rs', '.c', '.cpp', '.h', '.sh', '.bat', '.toml', '.ini', '.conf', '.env'];
3468
+ const isTextFile = (filename) => {
3469
+ const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
3470
+ return textExtensions.includes(ext);
3471
+ };
3472
+ const textFiles = group.shared_files.filter(f => isTextFile(f.name) && f.content);
3473
+ const binaryFiles = group.shared_files.filter(f => !isTextFile(f.name));
3474
+ if (textFiles.length > 0 || binaryFiles.length > 0) {
3475
+ sharedFilesContext = "\n\n以下是群聊中的共享文件:";
3476
+ if (textFiles.length > 0) {
3477
+ sharedFilesContext += "\n\n[文本文件 - 可直接读取]\n" +
3478
+ textFiles.map(f => `\n--- ${f.name} ---\n${f.content}`).join("\n");
3479
+ }
3480
+ if (binaryFiles.length > 0) {
3481
+ sharedFilesContext += "\n\n[二进制文件 - 仅列出文件名,无法直接读取内容]\n" +
3482
+ binaryFiles.map(f => `- ${f.name}`).join("\n");
3483
+ }
3484
+ }
3485
+ }
3486
+ // 获取群聊配置的工具
3487
+ let toolsContext = "";
3488
+ if (group.tools) {
3489
+ const mcpTools = group.tools.mcp || [];
3490
+ const skillTools = group.tools.skill || [];
3491
+ if (mcpTools.length > 0 || skillTools.length > 0) {
3492
+ toolsContext = "\n\n你当前可以使用的工具:";
3493
+ if (mcpTools.length > 0) {
3494
+ toolsContext += "\n- MCP 服务器:" + mcpTools.join(", ");
3495
+ }
3496
+ if (skillTools.length > 0) {
3497
+ toolsContext += "\n- Skills:" + skillTools.join(", ");
3498
+ }
3499
+ }
3500
+ }
3501
+ const fullPrompt = `你是一个在群聊中协作的开发 Agent,项目: ${member.project}。${atInstructions}${toolsContext}${sharedFilesContext}\n以下是群聊最近的消息记录:\n${context}\n\n请回复用户刚才发给你的消息:${message}`;
3502
+ res.write(`data: ${JSON.stringify({ type: "status", text: `🧠 ${member.project} 处理中...` })}\n\n`);
3503
+ const safeCwd = (workDir || process.cwd()).replace(/\\/g, "/");
3504
+ const tmpMsg = path.join(UPLOAD_DIR, `_msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}.txt`);
3505
+ fs.writeFileSync(tmpMsg, fullPrompt, "utf-8");
3506
+ let cmd;
3507
+ switch (agentType) {
3508
+ case "cursor":
3509
+ cmd = `type "${tmpMsg}" | agent -p`;
3510
+ break;
3511
+ case "gemini":
3512
+ cmd = `type "${tmpMsg}" | gemini -p`;
3513
+ break;
3514
+ case "codex":
3515
+ cmd = `type "${tmpMsg}" | codex -q`;
3516
+ break;
3517
+ default:
3518
+ cmd = `type "${tmpMsg}" | claude -p`;
3519
+ break;
3520
+ }
3521
+ try {
3522
+ const output = execSync(cmd, {
3523
+ encoding: "utf-8",
3524
+ timeout: 300000,
3525
+ cwd: safeCwd,
3526
+ shell: true,
3527
+ maxBuffer: 10 * 1024 * 1024,
3528
+ });
3529
+ try {
3530
+ fs.unlinkSync(tmpMsg);
3531
+ }
3532
+ catch { }
3533
+ const trimmedOutput = output.trim();
3534
+ allOutputs.push({ project: member.project, output: trimmedOutput });
3535
+ // 发送每个 Agent 的回复
3536
+ res.write(`data: ${JSON.stringify({ type: "chunk", text: `\n\n【${member.project}】\n${trimmedOutput}` })}\n\n`);
3537
+ // 记录到群聊消息
3538
+ appendGroupMessage(group_id, {
3539
+ id: "m" + Date.now().toString(36) + member.project,
3540
+ role: "assistant", agent: member.project,
3541
+ content: trimmedOutput,
3542
+ timestamp: new Date().toISOString(),
3543
+ });
3544
+ // 检查 @mentions
3545
+ const atMentions = trimmedOutput.match(/@[\w-]+/g) || [];
3546
+ const validMentions = atMentions.filter(m => {
3547
+ const name = m.slice(1);
3548
+ return group.members.some(mem => mem.project === name);
3549
+ });
3550
+ if (validMentions.length > 0) {
3551
+ console.log(`[群聊] Agent ${member.project} 的回复包含 @mentions: ${validMentions.join(", ")}`);
3552
+ setTimeout(() => processCrossAgents(group_id, group, member.project, trimmedOutput, validMentions, configs), 1000);
3553
+ }
3554
+ }
3555
+ catch (e) {
3556
+ try {
3557
+ fs.unlinkSync(tmpMsg);
3558
+ }
3559
+ catch { }
3560
+ res.write(`data: ${JSON.stringify({ type: "chunk", text: `\n\n【${member.project}】\n错误: ${e.message}` })}\n\n`);
3561
+ }
3562
+ }
3563
+ res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
3564
+ res.end();
3565
+ return;
3566
+ }
3567
+ // 单个 Agent 模式
3568
+ const target_project_actual = targetMembers[0].project;
3569
+ let workDir, agentType;
3570
+ if (target_project_actual === "coordinator") {
3571
+ const firstMember = group.members.find(m => m.project !== "coordinator");
3572
+ const firstConfig = firstMember ? configs.find(c => c.name === firstMember.project) : configs[0];
3573
+ workDir = firstConfig ? getConfigInfo(firstConfig.path)[0]?.workDir : process.cwd();
3574
+ agentType = "claudecode";
3575
+ }
3576
+ else {
3577
+ const config = configs.find(c => c.name === target_project_actual);
3578
+ if (!config)
3579
+ return sendJson(res, { error: "项目配置不存在" }, 400);
3580
+ workDir = getConfigInfo(config.path)[0]?.workDir;
3581
+ agentType = getConfigInfo(config.path)[0]?.agent || "claudecode";
3582
+ }
3583
+ // 构建带上下文的 prompt(包含群聊最近消息 + @ 协作指令)
3584
+ const recentMsgs = getGroupMessages(group_id).slice(-10);
3585
+ const context = recentMsgs.map(m => {
3586
+ const who = m.role === "user" ? `[用户 → ${m.target}]` : `[${m.agent || "Agent"}]`;
3587
+ return `${who} ${m.content}`;
3588
+ }).join("\n");
3589
+ const memberList = group.members.map(m => m.project).filter(p => p !== target_project_actual).join(", ");
3590
+ let atInstructions = "";
3591
+ if (target_project_actual === "coordinator") {
3592
+ atInstructions = `\n\n你是群聊的主 Agent(协调者),负责分析用户需求、拆分任务、分配给其他 Agent。群聊中的开发 Agent 有:${memberList}。\n\n你的职责:\n1. 理解用户的需求\n2. 拆分成具体的开发任务\n3. 用 @项目名 的格式把任务分配给对应的 Agent\n4. 跟踪进度,协调各方\n\n示例回复:\n好的,我来拆分这个需求:\n@smart-live-Cloud 用户接口需要新增 user_avatar 字段\n@smart-live-app 用户页面需要展示头像,调用 /api/user 获取`;
3593
+ }
3594
+ else {
3595
+ atInstructions = memberList ? `\n\n群聊中还有其他 Agent:${memberList}。如果你需要其他 Agent 协助,在回复中用 @项目名 的格式提出请求,例如:@smart-live-app 字段改了请适配前端。系统会自动把你的请求转发给对应的 Agent。` : "";
3596
+ }
3597
+ // 获取群聊共享文件
3598
+ let sharedFilesContext = "";
3599
+ if (group.shared_files && group.shared_files.length > 0) {
3600
+ // 判断是否是文本文件
3601
+ const textExtensions = ['.txt', '.md', '.json', '.js', '.ts', '.html', '.css', '.xml', '.yaml', '.yml', '.csv', '.log', '.py', '.java', '.go', '.rs', '.c', '.cpp', '.h', '.sh', '.bat', '.toml', '.ini', '.conf', '.env'];
3602
+ const isTextFile = (filename) => {
3603
+ const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
3604
+ return textExtensions.includes(ext);
3605
+ };
3606
+ const textFiles = group.shared_files.filter(f => isTextFile(f.name) && f.content);
3607
+ const binaryFiles = group.shared_files.filter(f => !isTextFile(f.name));
3608
+ if (textFiles.length > 0 || binaryFiles.length > 0) {
3609
+ sharedFilesContext = "\n\n以下是群聊中的共享文件:";
3610
+ if (textFiles.length > 0) {
3611
+ sharedFilesContext += "\n\n[文本文件 - 可直接读取]\n" +
3612
+ textFiles.map(f => `\n--- ${f.name} ---\n${f.content}`).join("\n");
3613
+ }
3614
+ if (binaryFiles.length > 0) {
3615
+ sharedFilesContext += "\n\n[二进制文件 - 仅列出文件名,无法直接读取内容]\n" +
3616
+ binaryFiles.map(f => `- ${f.name}`).join("\n");
3617
+ }
3618
+ }
3619
+ }
3620
+ // 获取群聊配置的工具
3621
+ let toolsContext = "";
3622
+ if (group.tools) {
3623
+ const mcpTools = group.tools.mcp || [];
3624
+ const skillTools = group.tools.skill || [];
3625
+ if (mcpTools.length > 0 || skillTools.length > 0) {
3626
+ toolsContext = "\n\n你当前可以使用的工具:";
3627
+ if (mcpTools.length > 0) {
3628
+ toolsContext += "\n- MCP 服务器:" + mcpTools.join(", ");
3629
+ }
3630
+ if (skillTools.length > 0) {
3631
+ toolsContext += "\n- Skills:" + skillTools.join(", ");
3632
+ }
3633
+ }
3634
+ }
3635
+ // 获取项目级别的工具和共享文件
3636
+ const PROJECT_CONFIGS_FILE = path.join(CCM_DIR, "project-configs.json");
3637
+ let projectConfigs = {};
3638
+ try {
3639
+ if (fs.existsSync(PROJECT_CONFIGS_FILE)) {
3640
+ projectConfigs = JSON.parse(fs.readFileSync(PROJECT_CONFIGS_FILE, "utf-8"));
3641
+ }
3642
+ }
3643
+ catch (e) { }
3644
+ const projectConfig = projectConfigs[target_project_actual] || {};
3645
+ // 项目工具
3646
+ if (projectConfig.tools) {
3647
+ const projectMcp = projectConfig.tools.mcp || [];
3648
+ const projectSkill = projectConfig.tools.skill || [];
3649
+ if (projectMcp.length > 0 || projectSkill.length > 0) {
3650
+ if (!toolsContext)
3651
+ toolsContext = "\n\n你当前可以使用的工具:";
3652
+ if (projectMcp.length > 0) {
3653
+ toolsContext += "\n- MCP 服务器:" + projectMcp.join(", ");
3654
+ }
3655
+ if (projectSkill.length > 0) {
3656
+ toolsContext += "\n- Skills:" + projectSkill.join(", ");
3657
+ }
3658
+ }
3659
+ }
3660
+ // 项目共享文件
3661
+ if (projectConfig.shared_files && projectConfig.shared_files.length > 0) {
3662
+ const textExtensions = ['.txt', '.md', '.json', '.js', '.ts', '.html', '.css', '.xml', '.yaml', '.yml', '.csv', '.log', '.py', '.java', '.go', '.rs', '.c', '.cpp', '.h', '.sh', '.bat', '.toml', '.ini', '.conf', '.env'];
3663
+ const isTextFile = (filename) => {
3664
+ const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
3665
+ return textExtensions.includes(ext);
3666
+ };
3667
+ const projectTextFiles = projectConfig.shared_files.filter(f => isTextFile(f.name) && f.content);
3668
+ const projectBinaryFiles = projectConfig.shared_files.filter(f => !isTextFile(f.name));
3669
+ if (projectTextFiles.length > 0 || projectBinaryFiles.length > 0) {
3670
+ sharedFilesContext += "\n\n[项目共享文件]";
3671
+ if (projectTextFiles.length > 0) {
3672
+ sharedFilesContext += "\n" + projectTextFiles.map(f => `\n--- ${f.name} ---\n${f.content}`).join("\n");
3673
+ }
3674
+ if (projectBinaryFiles.length > 0) {
3675
+ sharedFilesContext += "\n二进制文件:" + projectBinaryFiles.map(f => f.name).join(", ");
3676
+ }
3677
+ }
3678
+ }
3679
+ const fullPrompt = `你是一个在群聊中协作的开发 Agent,项目: ${target_project_actual}。${atInstructions}${toolsContext}${sharedFilesContext}\n以下是群聊最近的消息记录:\n${context}\n\n请回复用户刚才发给你的消息:${message}`;
3680
+ // 检查是否请求流式输出
3681
+ const useStream = parsed.query.stream === "1" || req.headers["accept"] === "text/event-stream";
3682
+ if (useStream) {
3683
+ // 流式输出
3684
+ const safeCwd = (workDir || process.cwd()).replace(/\\/g, "/");
3685
+ const tmpMsg = path.join(UPLOAD_DIR, `_msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}.txt`);
3686
+ fs.writeFileSync(tmpMsg, fullPrompt, "utf-8");
3687
+ let cmd;
3688
+ switch (agentType) {
3689
+ case "cursor":
3690
+ cmd = `type "${tmpMsg}" | agent -p`;
3691
+ break;
3692
+ case "gemini":
3693
+ cmd = `type "${tmpMsg}" | gemini -p`;
3694
+ break;
3695
+ case "codex":
3696
+ cmd = `type "${tmpMsg}" | codex -q`;
3697
+ break;
3698
+ default:
3699
+ cmd = `type "${tmpMsg}" | claude -p`;
3700
+ break;
3701
+ }
3702
+ res.writeHead(200, {
3703
+ "Content-Type": "text/event-stream",
3704
+ "Cache-Control": "no-cache",
3705
+ "Connection": "keep-alive",
3706
+ "Access-Control-Allow-Origin": "*",
3707
+ });
3708
+ res.write(`data: ${JSON.stringify({ type: "status", text: "🧠 Agent 正在思考..." })}\n\n`);
3709
+ const child = spawn(cmd, [], { shell: true, cwd: safeCwd, stdio: ["pipe", "pipe", "pipe"] });
3710
+ child.stdin.end();
3711
+ let fullOutput = "";
3712
+ let buffer = "";
3713
+ child.stdout.on("data", (chunk) => {
3714
+ const text = chunk.toString("utf-8");
3715
+ fullOutput += text;
3716
+ buffer += text;
3717
+ if (buffer.length > 10) {
3718
+ res.write(`data: ${JSON.stringify({ type: "chunk", text: buffer })}\n\n`);
3719
+ buffer = "";
3720
+ }
3721
+ });
3722
+ child.on("close", () => {
3723
+ try {
3724
+ fs.unlinkSync(tmpMsg);
3725
+ }
3726
+ catch { }
3727
+ if (buffer)
3728
+ res.write(`data: ${JSON.stringify({ type: "chunk", text: buffer })}\n\n`);
3729
+ res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
3730
+ res.end();
3731
+ // 记录到群聊消息
3732
+ appendGroupMessage(group_id, {
3733
+ id: "m" + Date.now().toString(36) + "a",
3734
+ role: "assistant", agent: target_project_actual,
3735
+ content: fullOutput.trim(),
3736
+ timestamp: new Date().toISOString(),
3737
+ });
3738
+ // 记录日志
3739
+ addGroupLog(group_id, "success", "response", `Agent ${target_project_actual} 回复完成`, {
3740
+ agent: target_project_actual,
3741
+ response_length: fullOutput.length,
3742
+ response_preview: fullOutput.substring(0, 300)
3743
+ });
3744
+ // 异步处理跨 Agent 调用
3745
+ console.log(`[群聊] Agent ${target_project_actual} 回复完成,检查 @mentions...`);
3746
+ console.log(`[群聊] 输出内容 (前500字符): ${fullOutput.substring(0, 500)}`);
3747
+ // 使用更宽松的正则匹配 @mentions
3748
+ const atMentions = fullOutput.match(/@[\w-]+/g) || [];
3749
+ // 过滤掉不是群聊成员的 mentions
3750
+ const validMentions = atMentions.filter(m => {
3751
+ const name = m.slice(1);
3752
+ return group.members.some(member => member.project === name);
3753
+ });
3754
+ console.log(`[群聊] 检测到 @mentions: ${atMentions.join(", ")}`);
3755
+ console.log(`[群聊] 有效的 @mentions (群聊成员): ${validMentions.join(", ")}`);
3756
+ if (validMentions.length > 0) {
3757
+ console.log(`[群聊] 触发跨 Agent 协作处理`);
3758
+ setImmediate(() => processCrossAgents(group_id, group, target_project_actual, fullOutput, validMentions, configs));
3759
+ }
3760
+ else {
3761
+ console.log(`[群聊] 未检测到有效的 @mentions,跳过跨 Agent 协作`);
3762
+ }
3763
+ });
3764
+ child.on("error", (err) => {
3765
+ try {
3766
+ fs.unlinkSync(tmpMsg);
3767
+ }
3768
+ catch { }
3769
+ res.write(`data: ${JSON.stringify({ type: "error", text: err.message })}\n\n`);
3770
+ res.end();
3771
+ });
3772
+ setTimeout(() => {
3773
+ try {
3774
+ child.kill();
3775
+ }
3776
+ catch { }
3777
+ try {
3778
+ fs.unlinkSync(tmpMsg);
3779
+ }
3780
+ catch { }
3781
+ res.write(`data: ${JSON.stringify({ type: "error", text: "Agent 响应超时" })}\n\n`);
3782
+ res.end();
3783
+ }, 300000);
3784
+ return;
3785
+ }
3786
+ // 非流式:普通调用
3787
+ console.log(`[群聊] 非流式调用 Agent ${target_project_actual}...`);
3788
+ const output = callAgent(target_project_actual, fullPrompt, workDir, agentType, 300000);
3789
+ console.log(`[群聊] Agent ${target_project_actual} 回复: ${output.substring(0, 200)}...`);
3790
+ appendGroupMessage(group_id, {
3791
+ id: "m" + Date.now().toString(36) + "a",
3792
+ role: "assistant", agent: target_project_actual,
3793
+ content: output,
3794
+ timestamp: new Date().toISOString(),
3795
+ });
3796
+ const atMentions = output.match(/@[\w-]+/g) || [];
3797
+ // 过滤掉不是群聊成员的 mentions
3798
+ const validMentions = atMentions.filter(m => {
3799
+ const name = m.slice(1);
3800
+ return group.members.some(member => member.project === name);
3801
+ });
3802
+ console.log(`[群聊] 检测到 @mentions: ${atMentions.join(", ")}`);
3803
+ console.log(`[群聊] 有效的 @mentions (群聊成员): ${validMentions.join(", ")}`);
3804
+ if (validMentions.length > 0) {
3805
+ console.log(`[群聊] 触发跨 Agent 协作处理`);
3806
+ sendJson(res, { success: true, reply: output, cross_pending: true });
3807
+ setImmediate(() => processCrossAgents(group_id, group, target_project_actual, output, validMentions, configs));
3808
+ return;
3809
+ }
3810
+ sendJson(res, { success: true, reply: output });
3811
+ }
3812
+ catch (e) {
3813
+ sendJson(res, { error: e.message }, 500);
3814
+ }
3815
+ });
3816
+ return;
3817
+ }
3818
+ // 群发消息(发给群里所有 Agent)
3819
+ if (pathname === "/api/groups/broadcast" && req.method === "POST") {
3820
+ let body = "";
3821
+ req.on("data", (chunk) => body += chunk);
3822
+ req.on("end", async () => {
3823
+ try {
3824
+ const { group_id, message } = JSON.parse(body);
3825
+ const groups = loadGroups();
3826
+ const group = groups.find(g => g.id === group_id);
3827
+ if (!group)
3828
+ return sendJson(res, { error: "群聊不存在" }, 400);
3829
+ // 记录用户消息
3830
+ appendGroupMessage(group_id, {
3831
+ id: "m" + Date.now().toString(36),
3832
+ role: "user", target: "all", content: message,
3833
+ timestamp: new Date().toISOString(),
3834
+ });
3835
+ const replies = [];
3836
+ const configs = getConfigs();
3837
+ for (const member of group.members) {
3838
+ const config = configs.find(c => c.name === member.project);
3839
+ if (!config)
3840
+ continue;
3841
+ const info = getConfigInfo(config.path);
3842
+ const workDir = info[0]?.workDir;
3843
+ const agentType = info[0]?.agent || "claudecode";
3844
+ const recentMsgs = getGroupMessages(group_id).slice(-10);
3845
+ const context = recentMsgs.map(m => {
3846
+ const who = m.role === "user" ? `[用户 → ${m.target}]` : `[${m.agent || "Agent"}]`;
3847
+ return `${who} ${m.content}`;
3848
+ }).join("\n");
3849
+ // 获取群聊共享文件
3850
+ let sharedFilesContext = "";
3851
+ if (group.shared_files && group.shared_files.length > 0) {
3852
+ // 判断是否是文本文件
3853
+ const textExtensions = ['.txt', '.md', '.json', '.js', '.ts', '.html', '.css', '.xml', '.yaml', '.yml', '.csv', '.log', '.py', '.java', '.go', '.rs', '.c', '.cpp', '.h', '.sh', '.bat', '.toml', '.ini', '.conf', '.env'];
3854
+ const isTextFile = (filename) => {
3855
+ const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
3856
+ return textExtensions.includes(ext);
3857
+ };
3858
+ const textFiles = group.shared_files.filter(f => isTextFile(f.name) && f.content);
3859
+ const binaryFiles = group.shared_files.filter(f => !isTextFile(f.name));
3860
+ if (textFiles.length > 0 || binaryFiles.length > 0) {
3861
+ sharedFilesContext = "\n\n以下是群聊中的共享文件:";
3862
+ if (textFiles.length > 0) {
3863
+ sharedFilesContext += "\n\n[文本文件 - 可直接读取]\n" +
3864
+ textFiles.map(f => `\n--- ${f.name} ---\n${f.content}`).join("\n");
3865
+ }
3866
+ if (binaryFiles.length > 0) {
3867
+ sharedFilesContext += "\n\n[二进制文件 - 仅列出文件名,无法直接读取内容]\n" +
3868
+ binaryFiles.map(f => `- ${f.name}`).join("\n");
3869
+ }
3870
+ }
3871
+ }
3872
+ // 获取群聊配置的工具
3873
+ let toolsContext = "";
3874
+ if (group.tools) {
3875
+ const mcpTools = group.tools.mcp || [];
3876
+ const skillTools = group.tools.skill || [];
3877
+ if (mcpTools.length > 0 || skillTools.length > 0) {
3878
+ toolsContext = "\n\n你当前可以使用的工具:";
3879
+ if (mcpTools.length > 0) {
3880
+ toolsContext += "\n- MCP 服务器:" + mcpTools.join(", ");
3881
+ }
3882
+ if (skillTools.length > 0) {
3883
+ toolsContext += "\n- Skills:" + skillTools.join(", ");
3884
+ }
3885
+ }
3886
+ }
3887
+ const fullPrompt = `你是群聊中的 ${member.project} Agent。${toolsContext}${sharedFilesContext}\n群聊记录:\n${context}\n\n请回复:${message}`;
3888
+ const output = callAgent(member.project, fullPrompt, workDir, agentType, 300000);
3889
+ appendGroupMessage(group_id, {
3890
+ id: "m" + Date.now().toString(36) + member.project,
3891
+ role: "assistant", agent: member.project, content: output,
3892
+ timestamp: new Date().toISOString(),
3893
+ });
3894
+ replies.push({ project: member.project, reply: output });
3895
+ }
3896
+ sendJson(res, { success: true, replies });
3897
+ }
3898
+ catch (e) {
3899
+ sendJson(res, { error: e.message }, 500);
3900
+ }
3901
+ });
3902
+ return;
3903
+ }
3904
+ // === 多 Agent 协作 API ===
3905
+ // 智能任务分解
3906
+ if (pathname === "/api/groups/decompose" && req.method === "POST") {
3907
+ let body = "";
3908
+ req.on("data", (chunk) => body += chunk);
3909
+ req.on("end", async () => {
3910
+ try {
3911
+ const { group_id, requirement } = JSON.parse(body);
3912
+ const groups = loadGroups();
3913
+ const group = groups.find(g => g.id === group_id);
3914
+ if (!group)
3915
+ return sendJson(res, { error: "群聊不存在" }, 400);
3916
+ const configs = getConfigs();
3917
+ const members = group.members.filter(m => m.project !== "coordinator");
3918
+ const memberList = members.map(m => `${m.project}(${m.agent})`).join(", ");
3919
+ // 构建分解 prompt
3920
+ const decomposePrompt = `你是一个项目协调者,负责将开发需求拆分成具体的子任务。
3921
+
3922
+ 群聊中的开发 Agent 有:${memberList}
3923
+
3924
+ 请将以下需求拆分成具体的开发任务,返回 JSON 格式:
3925
+ {
3926
+ "tasks": [
3927
+ {
3928
+ "title": "任务标题",
3929
+ "description": "任务描述",
3930
+ "target_project": "目标项目名",
3931
+ "priority": "high/normal/low",
3932
+ "estimated_time": "预估时间"
3933
+ }
3934
+ ]
3935
+ }
3936
+
3937
+ 需求:${requirement}
3938
+
3939
+ 注意:
3940
+ 1. 每个任务要具体可执行
3941
+ 2. 根据项目职责分配任务(前端做UI、后端做接口等)
3942
+ 3. 联调任务分配给 coordinator
3943
+ 4. 只返回 JSON,不要其他内容`;
3944
+ // 调用协调者 Agent
3945
+ const firstMember = members[0];
3946
+ const firstConfig = firstMember ? configs.find(c => c.name === firstMember.project) : configs[0];
3947
+ const workDir = firstConfig ? getConfigInfo(firstConfig.path)[0]?.workDir : process.cwd();
3948
+ const output = callAgent("coordinator", decomposePrompt, workDir, "claudecode", 120000);
3949
+ // 解析 JSON 结果
3950
+ let tasks = [];
3951
+ try {
3952
+ const jsonMatch = output.match(/\{[\s\S]*"tasks"[\s\S]*\}/);
3953
+ if (jsonMatch) {
3954
+ const parsed = JSON.parse(jsonMatch[0]);
3955
+ tasks = parsed.tasks || [];
3956
+ }
3957
+ }
3958
+ catch (e) {
3959
+ console.log("任务分解 JSON 解析失败:", e.message);
3960
+ }
3961
+ // 自动创建任务
3962
+ const createdTasks = tasks.map(t => createTask({
3963
+ title: t.title,
3964
+ description: t.description || "",
3965
+ target_project: t.target_project || "coordinator",
3966
+ priority: t.priority || "normal"
3967
+ }));
3968
+ // 记录到群聊消息
3969
+ appendGroupMessage(group_id, {
3970
+ id: "m" + Date.now().toString(36) + "decompose",
3971
+ role: "assistant",
3972
+ agent: "coordinator",
3973
+ content: `📋 需求分解完成,共 ${createdTasks.length} 个任务:\n${createdTasks.map((t, i) => `${i + 1}. [${t.target_project}] ${t.title}`).join("\n")}`,
3974
+ timestamp: new Date().toISOString(),
3975
+ });
3976
+ sendJson(res, { success: true, tasks: createdTasks, raw_output: output });
3977
+ }
3978
+ catch (e) {
3979
+ sendJson(res, { error: e.message }, 500);
3980
+ }
3981
+ });
3982
+ return;
3983
+ }
3984
+ // 自动任务分配和执行
3985
+ if (pathname === "/api/tasks/auto-assign" && req.method === "POST") {
3986
+ let body = "";
3987
+ req.on("data", (chunk) => body += chunk);
3988
+ req.on("end", async () => {
3989
+ try {
3990
+ const { task_id, group_id } = JSON.parse(body);
3991
+ const tasks = loadTasks();
3992
+ const task = tasks.find(t => t.id === task_id);
3993
+ if (!task)
3994
+ return sendJson(res, { error: "任务不存在" }, 404);
3995
+ const configs = getConfigs();
3996
+ const config = configs.find(c => c.name === task.target_project);
3997
+ if (!config)
3998
+ return sendJson(res, { error: "项目配置不存在" }, 400);
3999
+ const info = getConfigInfo(config.path);
4000
+ const workDir = info[0]?.workDir;
4001
+ const agentType = info[0]?.agent || "claudecode";
4002
+ // 更新任务状态
4003
+ updateTask(task_id, { status: "in_progress" });
4004
+ // 构建执行 prompt
4005
+ const executePrompt = `你正在执行一个开发任务,请完成它。
4006
+
4007
+ 任务标题:${task.title}
4008
+ 任务描述:${task.description || "无"}
4009
+
4010
+ 请直接开始实现,完成后回复 "✅ 任务完成" 并简要说明实现内容。`;
4011
+ // 异步执行任务
4012
+ const taskResult = callAgent(task.target_project, executePrompt, workDir, agentType, 300000);
4013
+ // 检查是否完成
4014
+ const isCompleted = taskResult.includes("✅") || taskResult.includes("完成") || taskResult.includes("done");
4015
+ // 更新任务状态
4016
+ updateTask(task_id, {
4017
+ status: isCompleted ? "done" : "in_progress",
4018
+ result: taskResult.substring(0, 500)
4019
+ });
4020
+ // 如果有群聊,记录结果
4021
+ if (group_id) {
4022
+ appendGroupMessage(group_id, {
4023
+ id: "m" + Date.now().toString(36) + "task",
4024
+ role: "assistant",
4025
+ agent: task.target_project,
4026
+ content: `📋 任务执行${isCompleted ? "完成" : "中"}:${task.title}\n${taskResult.substring(0, 300)}`,
4027
+ timestamp: new Date().toISOString(),
4028
+ });
4029
+ }
4030
+ sendJson(res, { success: true, task, completed: isCompleted, result: taskResult });
4031
+ }
4032
+ catch (e) {
4033
+ sendJson(res, { error: e.message }, 500);
4034
+ }
4035
+ });
4036
+ return;
4037
+ }
4038
+ // 批量自动执行任务
4039
+ if (pathname === "/api/tasks/auto-execute-all" && req.method === "POST") {
4040
+ let body = "";
4041
+ req.on("data", (chunk) => body += chunk);
4042
+ req.on("end", async () => {
4043
+ try {
4044
+ const { group_id } = JSON.parse(body);
4045
+ const tasks = loadTasks().filter(t => t.status === "pending");
4046
+ if (tasks.length === 0) {
4047
+ return sendJson(res, { success: true, message: "没有待执行的任务" });
4048
+ }
4049
+ const results = [];
4050
+ for (const task of tasks) {
4051
+ const configs = getConfigs();
4052
+ const config = configs.find(c => c.name === task.target_project);
4053
+ if (!config)
4054
+ continue;
4055
+ const info = getConfigInfo(config.path);
4056
+ const workDir = info[0]?.workDir;
4057
+ const agentType = info[0]?.agent || "claudecode";
4058
+ updateTask(task.id, { status: "in_progress" });
4059
+ const executePrompt = `开发任务:${task.title}\n${task.description || ""}\n\n请完成实现,完成后回复 "✅ 任务完成"。`;
4060
+ try {
4061
+ const result = callAgent(task.target_project, executePrompt, workDir, agentType, 300000);
4062
+ const isCompleted = result.includes("✅") || result.includes("完成");
4063
+ updateTask(task.id, {
4064
+ status: isCompleted ? "done" : "in_progress",
4065
+ result: result.substring(0, 500)
4066
+ });
4067
+ results.push({ task_id: task.id, title: task.title, completed: isCompleted });
4068
+ }
4069
+ catch (e) {
4070
+ results.push({ task_id: task.id, title: task.title, completed: false, error: e.message });
4071
+ }
4072
+ }
4073
+ sendJson(res, { success: true, results });
4074
+ }
4075
+ catch (e) {
4076
+ sendJson(res, { error: e.message }, 500);
4077
+ }
4078
+ });
4079
+ return;
4080
+ }
4081
+ // 多 Agent 代码审查
4082
+ if (pathname === "/api/review" && req.method === "POST") {
4083
+ let body = "";
4084
+ req.on("data", (chunk) => body += chunk);
4085
+ req.on("end", async () => {
4086
+ try {
4087
+ const { group_id, project, diff, reviewers } = JSON.parse(body);
4088
+ if (!diff)
4089
+ return sendJson(res, { error: "请提供代码变更内容" }, 400);
4090
+ const configs = getConfigs();
4091
+ const reviewPrompt = `请审查以下代码变更,从你的专业角度给出意见:
4092
+
4093
+ 项目:${project}
4094
+ 代码变更:
4095
+ \`\`\`
4096
+ ${diff}
4097
+ \`\`\`
4098
+
4099
+ 请从以下角度审查:
4100
+ 1. 代码质量
4101
+ 2. 潜在 bug
4102
+ 3. 安全问题
4103
+ 4. 性能影响
4104
+ 5. 与你的项目的兼容性
4105
+
4106
+ 返回 JSON 格式:
4107
+ {
4108
+ "issues": [
4109
+ {
4110
+ "severity": "high/medium/low",
4111
+ "description": "问题描述",
4112
+ "suggestion": "修改建议"
4113
+ }
4114
+ ],
4115
+ "overall": "总体评价"
4116
+ }`;
4117
+ // 并行调用多个 Agent 审查
4118
+ const reviewResults = [];
4119
+ for (const reviewer of (reviewers || [])) {
4120
+ const config = configs.find(c => c.name === reviewer);
4121
+ if (!config)
4122
+ continue;
4123
+ const info = getConfigInfo(config.path);
4124
+ const workDir = info[0]?.workDir;
4125
+ const agentType = info[0]?.agent || "claudecode";
4126
+ try {
4127
+ const result = callAgent(reviewer, reviewPrompt, workDir, agentType, 120000);
4128
+ reviewResults.push({ reviewer, result });
4129
+ }
4130
+ catch (e) {
4131
+ reviewResults.push({ reviewer, error: e.message });
4132
+ }
4133
+ }
4134
+ // 记录到群聊
4135
+ if (group_id) {
4136
+ appendGroupMessage(group_id, {
4137
+ id: "m" + Date.now().toString(36) + "review",
4138
+ role: "assistant",
4139
+ agent: "coordinator",
4140
+ content: `🔍 代码审查完成:${project}\n${reviewResults.map(r => `【${r.reviewer}】${r.result?.substring(0, 200) || r.error}`).join("\n\n")}`,
4141
+ timestamp: new Date().toISOString(),
4142
+ });
4143
+ }
4144
+ sendJson(res, { success: true, reviews: reviewResults });
4145
+ }
4146
+ catch (e) {
4147
+ sendJson(res, { error: e.message }, 500);
4148
+ }
4149
+ });
4150
+ return;
4151
+ }
4152
+ // === 代码变更查看器 API ===
4153
+ // 获取项目 Git 状态
4154
+ if (pathname === "/api/git/status" && req.method === "GET") {
4155
+ const project = parsed.query.project;
4156
+ if (!project)
4157
+ return sendJson(res, { error: "缺少项目参数" }, 400);
4158
+ const configs = getConfigs();
4159
+ const config = configs.find(c => c.name === project);
4160
+ if (!config)
4161
+ return sendJson(res, { error: "项目不存在" }, 404);
4162
+ const info = getConfigInfo(config.path);
4163
+ const workDir = info[0]?.workDir;
4164
+ if (!workDir)
4165
+ return sendJson(res, { error: "项目目录不存在" }, 400);
4166
+ try {
4167
+ // 检查是否是 git 仓库
4168
+ execSync("git rev-parse --is-inside-work-tree", { cwd: workDir, stdio: "pipe" });
4169
+ // 获取 git 状态
4170
+ const status = execSync("git status --porcelain", {
4171
+ encoding: "utf-8",
4172
+ cwd: workDir,
4173
+ stdio: ["pipe", "pipe", "pipe"]
4174
+ });
4175
+ const branch = execSync("git branch --show-current", {
4176
+ encoding: "utf-8",
4177
+ cwd: workDir,
4178
+ stdio: ["pipe", "pipe", "pipe"]
4179
+ }).trim();
4180
+ const files = status.split("\n")
4181
+ .filter(line => line.trim())
4182
+ .map(line => {
4183
+ const statusCode = line.substring(0, 2).trim();
4184
+ const filePath = line.substring(3).trim();
4185
+ let statusText = "";
4186
+ let statusColor = "";
4187
+ if (statusCode === "M" || statusCode === "MM") {
4188
+ statusText = "已修改";
4189
+ statusColor = "#facc15";
4190
+ }
4191
+ else if (statusCode === "A") {
4192
+ statusText = "新增";
4193
+ statusColor = "#22c55e";
4194
+ }
4195
+ else if (statusCode === "D") {
4196
+ statusText = "已删除";
4197
+ statusColor = "#ef4444";
4198
+ }
4199
+ else if (statusCode === "R") {
4200
+ statusText = "重命名";
4201
+ statusColor = "#a78bfa";
4202
+ }
4203
+ else if (statusCode === "??") {
4204
+ statusText = "未跟踪";
4205
+ statusColor = "#64748b";
4206
+ }
4207
+ else {
4208
+ statusText = statusCode;
4209
+ statusColor = "#94a3b8";
4210
+ }
4211
+ return { path: filePath, status: statusCode, statusText, statusColor };
4212
+ });
4213
+ sendJson(res, { success: true, branch, files, total: files.length });
4214
+ }
4215
+ catch (e) {
4216
+ sendJson(res, { success: false, error: "不是 Git 仓库或 Git 未安装: " + e.message });
4217
+ }
4218
+ return;
4219
+ }
4220
+ // 获取文件 diff
4221
+ if (pathname === "/api/git/diff" && req.method === "GET") {
4222
+ const project = parsed.query.project;
4223
+ const filePath = parsed.query.file;
4224
+ const staged = parsed.query.staged === "true";
4225
+ if (!project || !filePath)
4226
+ return sendJson(res, { error: "缺少参数" }, 400);
4227
+ const configs = getConfigs();
4228
+ const config = configs.find(c => c.name === project);
4229
+ if (!config)
4230
+ return sendJson(res, { error: "项目不存在" }, 404);
4231
+ const info = getConfigInfo(config.path);
4232
+ const workDir = info[0]?.workDir;
4233
+ try {
4234
+ const stagedFlag = staged ? "--staged" : "";
4235
+ const diff = execSync(`git diff ${stagedFlag} "${filePath}"`, {
4236
+ encoding: "utf-8",
4237
+ cwd: workDir,
4238
+ stdio: ["pipe", "pipe", "pipe"],
4239
+ maxBuffer: 10 * 1024 * 1024
4240
+ });
4241
+ // 解析 diff 为结构化数据
4242
+ const lines = diff.split("\n");
4243
+ const hunks = [];
4244
+ let currentHunk = null;
4245
+ for (const line of lines) {
4246
+ if (line.startsWith("@@")) {
4247
+ if (currentHunk)
4248
+ hunks.push(currentHunk);
4249
+ const match = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@(.*)/);
4250
+ currentHunk = {
4251
+ header: line,
4252
+ oldStart: parseInt(match[1]),
4253
+ oldLines: parseInt(match[2] || 1),
4254
+ newStart: parseInt(match[3]),
4255
+ newLines: parseInt(match[4] || 1),
4256
+ context: match[5]?.trim() || "",
4257
+ changes: []
4258
+ };
4259
+ }
4260
+ else if (currentHunk) {
4261
+ if (line.startsWith("+")) {
4262
+ currentHunk.changes.push({ type: "add", content: line.substring(1) });
4263
+ }
4264
+ else if (line.startsWith("-")) {
4265
+ currentHunk.changes.push({ type: "remove", content: line.substring(1) });
4266
+ }
4267
+ else {
4268
+ currentHunk.changes.push({ type: "context", content: line.substring(1) });
4269
+ }
4270
+ }
4271
+ }
4272
+ if (currentHunk)
4273
+ hunks.push(currentHunk);
4274
+ sendJson(res, { success: true, file: filePath, hunks, raw: diff });
4275
+ }
4276
+ catch (e) {
4277
+ sendJson(res, { success: false, error: "获取 diff 失败: " + e.message });
4278
+ }
4279
+ return;
4280
+ }
4281
+ // 提交更改
4282
+ if (pathname === "/api/git/commit" && req.method === "POST") {
4283
+ let body = "";
4284
+ req.on("data", (chunk) => body += chunk);
4285
+ req.on("end", () => {
4286
+ try {
4287
+ const { project, message, files } = JSON.parse(body);
4288
+ if (!project || !message)
4289
+ return sendJson(res, { error: "缺少参数" }, 400);
4290
+ const configs = getConfigs();
4291
+ const config = configs.find(c => c.name === project);
4292
+ if (!config)
4293
+ return sendJson(res, { error: "项目不存在" }, 404);
4294
+ const info = getConfigInfo(config.path);
4295
+ const workDir = info[0]?.workDir;
4296
+ // 添加文件到暂存区
4297
+ if (files && files.length > 0) {
4298
+ for (const file of files) {
4299
+ execSync(`git add "${file}"`, { cwd: workDir, stdio: "pipe" });
4300
+ }
4301
+ }
4302
+ else {
4303
+ execSync("git add -A", { cwd: workDir, stdio: "pipe" });
4304
+ }
4305
+ // 提交
4306
+ const commitMsg = message.replace(/"/g, '\\"');
4307
+ execSync(`git commit -m "${commitMsg}"`, {
4308
+ encoding: "utf-8",
4309
+ cwd: workDir,
4310
+ stdio: ["pipe", "pipe", "pipe"]
4311
+ });
4312
+ sendJson(res, { success: true, message: "提交成功" });
4313
+ }
4314
+ catch (e) {
4315
+ sendJson(res, { success: false, error: "提交失败: " + e.message });
4316
+ }
4317
+ });
4318
+ return;
4319
+ }
4320
+ // 回滚更改
4321
+ if (pathname === "/api/git/rollback" && req.method === "POST") {
4322
+ let body = "";
4323
+ req.on("data", (chunk) => body += chunk);
4324
+ req.on("end", () => {
4325
+ try {
4326
+ const { project, file, staged } = JSON.parse(body);
4327
+ if (!project || !file)
4328
+ return sendJson(res, { error: "缺少参数" }, 400);
4329
+ const configs = getConfigs();
4330
+ const config = configs.find(c => c.name === project);
4331
+ if (!config)
4332
+ return sendJson(res, { error: "项目不存在" }, 404);
4333
+ const info = getConfigInfo(config.path);
4334
+ const workDir = info[0]?.workDir;
4335
+ if (staged) {
4336
+ // 取消暂存
4337
+ execSync(`git restore --staged "${file}"`, { cwd: workDir, stdio: "pipe" });
4338
+ }
4339
+ else {
4340
+ // 回滚工作区更改
4341
+ execSync(`git restore "${file}"`, { cwd: workDir, stdio: "pipe" });
4342
+ }
4343
+ sendJson(res, { success: true, message: "回滚成功" });
4344
+ }
4345
+ catch (e) {
4346
+ sendJson(res, { success: false, error: "回滚失败: " + e.message });
4347
+ }
4348
+ });
4349
+ return;
4350
+ }
4351
+ // 获取提交历史
4352
+ if (pathname === "/api/git/log" && req.method === "GET") {
4353
+ const project = parsed.query.project;
4354
+ const limit = parseInt(parsed.query.limit) || 20;
4355
+ if (!project)
4356
+ return sendJson(res, { error: "缺少项目参数" }, 400);
4357
+ const configs = getConfigs();
4358
+ const config = configs.find(c => c.name === project);
4359
+ if (!config)
4360
+ return sendJson(res, { error: "项目不存在" }, 404);
4361
+ const info = getConfigInfo(config.path);
4362
+ const workDir = info[0]?.workDir;
4363
+ try {
4364
+ const log = execSync(`git log --pretty=format:"%H|%h|%an|%ae|%at|%s" -n ${limit}`, { encoding: "utf-8", cwd: workDir, stdio: ["pipe", "pipe", "pipe"] });
4365
+ const commits = log.split("\n")
4366
+ .filter(line => line.trim())
4367
+ .map(line => {
4368
+ const [hash, shortHash, author, email, timestamp, message] = line.split("|");
4369
+ return {
4370
+ hash,
4371
+ shortHash,
4372
+ author,
4373
+ email,
4374
+ timestamp: new Date(parseInt(timestamp) * 1000).toISOString(),
4375
+ message
4376
+ };
4377
+ });
4378
+ sendJson(res, { success: true, commits });
4379
+ }
4380
+ catch (e) {
4381
+ sendJson(res, { success: false, error: "获取提交历史失败: " + e.message });
4382
+ }
4383
+ return;
4384
+ }
4385
+ // 协作统计 API
4386
+ if (pathname === "/api/collaboration/stats" && req.method === "GET") {
4387
+ const tasks = loadTasks();
4388
+ const groups = loadGroups();
4389
+ const stats = {
4390
+ total_tasks: tasks.length,
4391
+ pending_tasks: tasks.filter(t => t.status === "pending").length,
4392
+ in_progress_tasks: tasks.filter(t => t.status === "in_progress").length,
4393
+ done_tasks: tasks.filter(t => t.status === "done").length,
4394
+ completion_rate: tasks.length > 0 ? Math.round(tasks.filter(t => t.status === "done").length / tasks.length * 100) : 0,
4395
+ groups_count: groups.length,
4396
+ recent_activities: []
4397
+ };
4398
+ // 获取最近活动
4399
+ for (const group of groups.slice(0, 3)) {
4400
+ const messages = getGroupMessages(group.id).slice(-5);
4401
+ for (const msg of messages) {
4402
+ stats.recent_activities.push({
4403
+ group: group.name,
4404
+ agent: msg.agent || "user",
4405
+ content: msg.content?.substring(0, 100),
4406
+ timestamp: msg.timestamp
4407
+ });
4408
+ }
4409
+ }
4410
+ stats.recent_activities.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
4411
+ stats.recent_activities = stats.recent_activities.slice(0, 10);
4412
+ return sendJson(res, stats);
4413
+ }
4414
+ // 测试 @mention 检测
4415
+ if (pathname === "/api/test/mentions" && req.method === "POST") {
4416
+ let body = "";
4417
+ req.on("data", (chunk) => body += chunk);
4418
+ req.on("end", () => {
4419
+ try {
4420
+ const { text, group_id } = JSON.parse(body);
4421
+ // 检测 @mentions
4422
+ const atMentions = text.match(/@[\w-]+/g) || [];
4423
+ // 如果提供了 group_id,过滤出有效的 mentions
4424
+ let validMentions = atMentions;
4425
+ if (group_id) {
4426
+ const groups = loadGroups();
4427
+ const group = groups.find(g => g.id === group_id);
4428
+ if (group) {
4429
+ validMentions = atMentions.filter(m => {
4430
+ const name = m.slice(1);
4431
+ return group.members.some(member => member.project === name);
4432
+ });
4433
+ }
4434
+ }
4435
+ sendJson(res, {
4436
+ success: true,
4437
+ input: text,
4438
+ all_mentions: atMentions,
4439
+ valid_mentions: validMentions,
4440
+ extracted_messages: validMentions.map(m => {
4441
+ const name = m.slice(1);
4442
+ const regex = new RegExp(`@${name}\\s+([^@]+?)(?=\\s*@|$)`, "is");
4443
+ const match = text.match(regex);
4444
+ return {
4445
+ mention: m,
4446
+ target: name,
4447
+ message: match ? match[1].trim() : text.substring(0, 200)
4448
+ };
4449
+ })
4450
+ });
4451
+ }
4452
+ catch (e) {
4453
+ sendJson(res, { error: e.message }, 400);
4454
+ }
4455
+ });
4456
+ return;
4457
+ }
4458
+ // 404
4459
+ sendJson(res, { error: "Not Found" }, 404);
4460
+ }
4461
+ // 启动服务器
4462
+ function startServer(port) {
4463
+ const server = http.createServer(handleRequest);
4464
+ server.listen(port, () => {
4465
+ console.log(`\n╔══════════════════════════════════════╗`);
4466
+ console.log(`║ ccm Web 控制台 ║`);
4467
+ console.log(`╚══════════════════════════════════════╝\n`);
4468
+ console.log(` 地址: http://localhost:${port}`);
4469
+ console.log(` 按 Ctrl+C 停止\n`);
4470
+ });
4471
+ return server;
4472
+ }
4473
+ // 直接运行或被 require
4474
+ if (require.main === module) {
4475
+ const PORT = parseInt(process.argv[2]) || 3080;
4476
+ startServer(PORT);
4477
+ }
4478
+ module.exports = { startServer };
4479
+ //# sourceMappingURL=server.js.map