@mumulinya167/cc-web 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/bin/ccm.js +680 -0
- package/bin/server.js +4428 -0
- package/bin/setup.js +148 -0
- package/configs/config-template.toml +54 -0
- package/mcp-feishu/.env.example +2 -0
- package/mcp-feishu/package-lock.json +1194 -0
- package/mcp-feishu/package.json +23 -0
- package/mcp-feishu/src/cli.ts +239 -0
- package/mcp-feishu/src/feishu-client.ts +209 -0
- package/mcp-feishu/src/index.ts +55 -0
- package/mcp-feishu/src/tools.ts +222 -0
- package/mcp-feishu/tsconfig.json +18 -0
- package/package.json +27 -0
- package/public/index.html +6604 -0
- package/templates/CLAUDE-backend.md +105 -0
- package/templates/CLAUDE-frontend.md +61 -0
package/bin/ccm.js
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cc-web - cc-connect Web 管理界面
|
|
3
|
+
// 用法: cc-web 启动 Web 控制台
|
|
4
|
+
// cc-web start 启动 Web 服务器
|
|
5
|
+
// cc-web start all 启动所有项目
|
|
6
|
+
// cc-web stop all 停止所有项目
|
|
7
|
+
// cc-web status 查看运行状态
|
|
8
|
+
|
|
9
|
+
const { execSync, spawn } = require("child_process");
|
|
10
|
+
const fs = require("fs");
|
|
11
|
+
const path = require("path");
|
|
12
|
+
const readline = require("readline");
|
|
13
|
+
const os = require("os");
|
|
14
|
+
|
|
15
|
+
const CCM_DIR = path.join(os.homedir(), ".cc-connect");
|
|
16
|
+
const CONFIGS_DIR = path.join(CCM_DIR, "configs");
|
|
17
|
+
const PID_DIR = path.join(CCM_DIR, "pids");
|
|
18
|
+
const LOG_DIR = path.join(CCM_DIR, "logs");
|
|
19
|
+
const TEMP_DIR = path.join(CCM_DIR, "temp");
|
|
20
|
+
const PROJECTS_FILE = path.join(CCM_DIR, "projects.txt");
|
|
21
|
+
|
|
22
|
+
// 支持的 Agent 列表
|
|
23
|
+
const AGENTS = [
|
|
24
|
+
{ type: "claudecode", name: "Claude Code", modes: ["default", "acceptEdits", "plan", "auto", "bypassPermissions"], defaultMode: "default" },
|
|
25
|
+
{ type: "cursor", name: "Cursor", modes: ["default", "force", "plan", "ask"], defaultMode: "default" },
|
|
26
|
+
{ type: "gemini", name: "Gemini CLI", modes: ["default", "auto_edit", "yolo", "plan"], defaultMode: "yolo" },
|
|
27
|
+
{ type: "codex", name: "Codex", modes: ["suggest", "auto-edit", "full-auto", "yolo"], defaultMode: "full-auto" },
|
|
28
|
+
{ type: "qoder", name: "Qoder CLI", modes: ["default", "yolo"], defaultMode: "default" },
|
|
29
|
+
{ type: "opencode", name: "OpenCode", modes: ["default", "yolo"], defaultMode: "default" },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// 支持的平台列表
|
|
33
|
+
const PLATFORMS = [
|
|
34
|
+
{ type: "feishu", name: "飞书", hasQrSetup: true, fields: ["app_id", "app_secret"] },
|
|
35
|
+
{ type: "lark", name: "Lark (国际版飞书)", hasQrSetup: true, fields: ["app_id", "app_secret"] },
|
|
36
|
+
{ type: "weixin", name: "微信", hasQrSetup: false, fields: ["token", "base_url", "account_id"] },
|
|
37
|
+
{ type: "telegram", name: "Telegram", hasQrSetup: false, fields: ["token"] },
|
|
38
|
+
{ type: "slack", name: "Slack", hasQrSetup: false, fields: ["bot_token", "app_token"] },
|
|
39
|
+
{ type: "discord", name: "Discord", hasQrSetup: false, fields: ["token"] },
|
|
40
|
+
{ type: "dingtalk", name: "钉钉", hasQrSetup: false, fields: ["token"] },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function ensureDirs() {
|
|
44
|
+
[CCM_DIR, CONFIGS_DIR, PID_DIR, LOG_DIR, TEMP_DIR].forEach((dir) => {
|
|
45
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getConfigs() {
|
|
50
|
+
if (!fs.existsSync(CONFIGS_DIR)) return [];
|
|
51
|
+
return fs
|
|
52
|
+
.readdirSync(CONFIGS_DIR)
|
|
53
|
+
.filter((f) => f.endsWith(".toml"))
|
|
54
|
+
.sort()
|
|
55
|
+
.map((f, i) => ({
|
|
56
|
+
index: i + 1,
|
|
57
|
+
file: f,
|
|
58
|
+
name: f.replace("config-", "").replace(".toml", ""),
|
|
59
|
+
path: path.join(CONFIGS_DIR, f),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getConfigInfo(configPath) {
|
|
64
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
65
|
+
const projects = [];
|
|
66
|
+
const lines = content.split("\n");
|
|
67
|
+
let currentProject = null;
|
|
68
|
+
let inPlatformsBlock = false;
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
if (trimmed === "[[projects]]") {
|
|
73
|
+
if (currentProject && currentProject.name) projects.push(currentProject);
|
|
74
|
+
currentProject = {};
|
|
75
|
+
inPlatformsBlock = false;
|
|
76
|
+
}
|
|
77
|
+
if (currentProject && trimmed.startsWith("name = "))
|
|
78
|
+
currentProject.name = trimmed.split("=")[1].trim().replace(/"/g, "");
|
|
79
|
+
if (currentProject && trimmed.startsWith("work_dir = "))
|
|
80
|
+
currentProject.workDir = trimmed.split("=")[1].trim().replace(/"/g, "");
|
|
81
|
+
if (trimmed === "[[projects.platforms]]") {
|
|
82
|
+
inPlatformsBlock = true;
|
|
83
|
+
} else if (trimmed.startsWith("[") && !trimmed.startsWith("[projects.platforms")) {
|
|
84
|
+
inPlatformsBlock = false;
|
|
85
|
+
}
|
|
86
|
+
if (currentProject && inPlatformsBlock && trimmed.startsWith("type = ")) {
|
|
87
|
+
const pt = trimmed.split("=")[1].trim().replace(/"/g, "");
|
|
88
|
+
const map = { weixin: "微信", feishu: "飞书", telegram: "Telegram", slack: "Slack", discord: "Discord" };
|
|
89
|
+
currentProject.platform = map[pt] || pt;
|
|
90
|
+
inPlatformsBlock = false;
|
|
91
|
+
}
|
|
92
|
+
if (currentProject && (trimmed === "[[commands]]" || trimmed === "[[aliases]]")) {
|
|
93
|
+
if (currentProject.name) projects.push(currentProject);
|
|
94
|
+
currentProject = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (currentProject && currentProject.name) projects.push(currentProject);
|
|
98
|
+
return projects;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 获取当前配置中的 agent type
|
|
102
|
+
function getCurrentAgent(configPath) {
|
|
103
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
104
|
+
const match = content.match(/\[projects\.agent\][\s\S]*?type\s*=\s*"([^"]+)"/);
|
|
105
|
+
return match ? match[1] : "claudecode";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 生成临时配置,替换 agent type
|
|
109
|
+
function generateTempConfig(configPath, newAgentType) {
|
|
110
|
+
let content = fs.readFileSync(configPath, "utf-8");
|
|
111
|
+
|
|
112
|
+
// 找到 agent 定义,替换 type
|
|
113
|
+
content = content.replace(
|
|
114
|
+
/(\[projects\.agent\]\s*\n\s*type\s*=\s*)"[^"]+"/g,
|
|
115
|
+
`$1"${newAgentType}"`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// 移除 [projects.agent.options] 下的 mode 行(不同 agent 的 mode 值不同)
|
|
119
|
+
// 保留其他 options
|
|
120
|
+
const agentInfo = AGENTS.find(a => a.type === newAgentType);
|
|
121
|
+
if (agentInfo) {
|
|
122
|
+
content = content.replace(
|
|
123
|
+
/(\[projects\.agent\.options\][\s\S]*?mode\s*=\s*)"[^"]+"/g,
|
|
124
|
+
`$1"${agentInfo.defaultMode}"`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 写入临时文件
|
|
129
|
+
const baseName = path.basename(configPath, ".toml");
|
|
130
|
+
const tempPath = path.join(TEMP_DIR, `${baseName}-${newAgentType}.toml`);
|
|
131
|
+
fs.writeFileSync(tempPath, content);
|
|
132
|
+
return tempPath;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getRunningStatus() {
|
|
136
|
+
const status = {};
|
|
137
|
+
if (!fs.existsSync(PID_DIR)) return status;
|
|
138
|
+
for (const f of fs.readdirSync(PID_DIR)) {
|
|
139
|
+
if (!f.endsWith(".pid")) continue;
|
|
140
|
+
const name = f.replace(".pid", "");
|
|
141
|
+
const pidFile = path.join(PID_DIR, f);
|
|
142
|
+
const pid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
143
|
+
try {
|
|
144
|
+
process.kill(parseInt(pid), 0);
|
|
145
|
+
status[name] = { running: true, pid };
|
|
146
|
+
} catch {
|
|
147
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return status;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isRunning(name) {
|
|
154
|
+
const pidFile = path.join(PID_DIR, `${name}.pid`);
|
|
155
|
+
if (!fs.existsSync(pidFile)) return false;
|
|
156
|
+
const pid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
157
|
+
try {
|
|
158
|
+
process.kill(parseInt(pid), 0);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function startProject(config, agentType) {
|
|
167
|
+
const displayName = agentType ? `${config.name} (${agentType})` : config.name;
|
|
168
|
+
|
|
169
|
+
if (isRunning(config.name)) {
|
|
170
|
+
console.log(` ⚠ ${config.name} 已在运行中,先停止再切换 Agent`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let configPath = config.path;
|
|
175
|
+
|
|
176
|
+
// 如果指定了不同的 agent,生成临时配置
|
|
177
|
+
if (agentType) {
|
|
178
|
+
const currentAgent = getCurrentAgent(config.path);
|
|
179
|
+
if (currentAgent !== agentType) {
|
|
180
|
+
configPath = generateTempConfig(config.path, agentType);
|
|
181
|
+
console.log(` → 切换 Agent: ${currentAgent} → ${agentType}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const logFile = path.join(LOG_DIR, `${config.name}.log`);
|
|
186
|
+
const logStream = fs.openSync(logFile, "w");
|
|
187
|
+
|
|
188
|
+
const child = spawn("cc-connect", ["--config", configPath, "--force"], {
|
|
189
|
+
stdio: ["ignore", logStream, logStream],
|
|
190
|
+
shell: true,
|
|
191
|
+
detached: true,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
child.unref();
|
|
195
|
+
fs.writeFileSync(path.join(PID_DIR, `${config.name}.pid`), String(child.pid));
|
|
196
|
+
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
if (isRunning(config.name)) {
|
|
199
|
+
const projects = getConfigInfo(configPath);
|
|
200
|
+
const platform = projects.map((p) => p.platform).join(", ");
|
|
201
|
+
const agent = agentType || getCurrentAgent(config.path);
|
|
202
|
+
console.log(` ✓ ${config.name} 已启动 (PID: ${child.pid}, Agent: ${agent}, 平台: ${platform})`);
|
|
203
|
+
} else {
|
|
204
|
+
console.log(` ✗ ${displayName} 启动失败,查看日志: ${logFile}`);
|
|
205
|
+
}
|
|
206
|
+
}, 2000);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function stopProject(name) {
|
|
210
|
+
const pidFile = path.join(PID_DIR, `${name}.pid`);
|
|
211
|
+
if (!fs.existsSync(pidFile)) {
|
|
212
|
+
console.log(` ⚠ ${name} 未在运行`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const pid = fs.readFileSync(pidFile, "utf-8").trim();
|
|
216
|
+
try {
|
|
217
|
+
if (process.platform === "win32") {
|
|
218
|
+
execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore" });
|
|
219
|
+
} else {
|
|
220
|
+
process.kill(parseInt(pid), "SIGTERM");
|
|
221
|
+
}
|
|
222
|
+
console.log(` ✓ ${name} 已停止`);
|
|
223
|
+
} catch {
|
|
224
|
+
console.log(` ⚠ ${name} 进程已不存在`);
|
|
225
|
+
}
|
|
226
|
+
try { fs.unlinkSync(pidFile); } catch {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function showStatus() {
|
|
230
|
+
const configs = getConfigs();
|
|
231
|
+
const running = getRunningStatus();
|
|
232
|
+
|
|
233
|
+
console.log("\n项目状态:\n");
|
|
234
|
+
for (const config of configs) {
|
|
235
|
+
const projects = getConfigInfo(config.path);
|
|
236
|
+
const platform = projects.map((p) => p.platform).join(", ");
|
|
237
|
+
const dir = [...new Set(projects.map((p) => p.workDir))].join(", ");
|
|
238
|
+
const agent = getCurrentAgent(config.path);
|
|
239
|
+
const isUp = running[config.name];
|
|
240
|
+
const icon = isUp ? "🟢" : "⚪";
|
|
241
|
+
const pidInfo = isUp ? ` (PID: ${isUp.pid})` : "";
|
|
242
|
+
console.log(` ${icon} [${config.index}] ${config.name.padEnd(20)} Agent: ${agent.padEnd(12)} ${platform.padEnd(6)}${pidInfo}`);
|
|
243
|
+
console.log(` ${dir}`);
|
|
244
|
+
}
|
|
245
|
+
const runningCount = Object.keys(running).length;
|
|
246
|
+
console.log(`\n运行中: ${runningCount}/${configs.length}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Agent 选择菜单
|
|
250
|
+
function selectAgent(config, callback) {
|
|
251
|
+
const currentAgent = getCurrentAgent(config.path);
|
|
252
|
+
|
|
253
|
+
console.log(`\n选择 Agent (当前: ${currentAgent}):\n`);
|
|
254
|
+
AGENTS.forEach((agent, i) => {
|
|
255
|
+
const isCurrent = agent.type === currentAgent;
|
|
256
|
+
const mark = isCurrent ? " ← 当前" : "";
|
|
257
|
+
console.log(` [${i + 1}] ${agent.name.padEnd(15)} (${agent.type})${mark}`);
|
|
258
|
+
});
|
|
259
|
+
console.log(` [0] 使用当前 Agent\n`);
|
|
260
|
+
|
|
261
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
262
|
+
rl.question("选择 Agent 编号: ", (answer) => {
|
|
263
|
+
const idx = parseInt(answer);
|
|
264
|
+
rl.close();
|
|
265
|
+
|
|
266
|
+
if (idx === 0 || isNaN(idx)) {
|
|
267
|
+
callback(null); // 使用当前
|
|
268
|
+
} else if (idx >= 1 && idx <= AGENTS.length) {
|
|
269
|
+
callback(AGENTS[idx - 1].type);
|
|
270
|
+
} else {
|
|
271
|
+
console.log("无效选择,使用当前 Agent");
|
|
272
|
+
callback(null);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function generateConfig(name, workDir, selectedAgent, selectedPlatform, platformOptions) {
|
|
278
|
+
// 构建平台选项
|
|
279
|
+
const optionsLines = Object.entries(platformOptions)
|
|
280
|
+
.map(([k, v]) => `${k} = "${v}"`)
|
|
281
|
+
.join("\n");
|
|
282
|
+
|
|
283
|
+
// 飞书/Lark 特有选项
|
|
284
|
+
const extraOptions = (selectedPlatform.type === "feishu" || selectedPlatform.type === "lark")
|
|
285
|
+
? "\nenable_feishu_card = true\nthread_isolation = true\nprogress_style = \"card\""
|
|
286
|
+
: "";
|
|
287
|
+
|
|
288
|
+
return `# cc-connect - ${name}
|
|
289
|
+
language = "zh"
|
|
290
|
+
|
|
291
|
+
[[projects]]
|
|
292
|
+
name = "${name}"
|
|
293
|
+
work_dir = "${workDir.replace(/\\/g, "\\\\")}"
|
|
294
|
+
admin_from = "*"
|
|
295
|
+
|
|
296
|
+
[projects.agent]
|
|
297
|
+
type = "${selectedAgent.type}"
|
|
298
|
+
mode = "${selectedAgent.defaultMode}"
|
|
299
|
+
|
|
300
|
+
[projects.agent.options]
|
|
301
|
+
work_dir = "${workDir.replace(/\\/g, "\\\\")}"
|
|
302
|
+
|
|
303
|
+
[[projects.platforms]]
|
|
304
|
+
type = "${selectedPlatform.type}"
|
|
305
|
+
|
|
306
|
+
[projects.platforms.options]
|
|
307
|
+
${optionsLines}${extraOptions}
|
|
308
|
+
|
|
309
|
+
# 自定义命令
|
|
310
|
+
[[commands]]
|
|
311
|
+
name = "history"
|
|
312
|
+
description = "查看会话历史记录"
|
|
313
|
+
exec = "cc-connect sessions show {{1}} -n {{2:20}}"
|
|
314
|
+
|
|
315
|
+
[[commands]]
|
|
316
|
+
name = "sessions"
|
|
317
|
+
description = "列出所有会话"
|
|
318
|
+
exec = "cc-connect sessions list"
|
|
319
|
+
|
|
320
|
+
[[commands]]
|
|
321
|
+
name = "projects"
|
|
322
|
+
description = "查看所有可操作的代码项目目录"
|
|
323
|
+
exec = "cmd /c type ${CCM_DIR.replace(/\\/g, "\\\\")}\\\\projects.txt"
|
|
324
|
+
|
|
325
|
+
[[aliases]]
|
|
326
|
+
name = "历史"
|
|
327
|
+
command = "/history"
|
|
328
|
+
|
|
329
|
+
[[aliases]]
|
|
330
|
+
name = "会话"
|
|
331
|
+
command = "/sessions"
|
|
332
|
+
|
|
333
|
+
[[aliases]]
|
|
334
|
+
name = "项目"
|
|
335
|
+
command = "/projects"
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function finalizeConfig(name, workDir) {
|
|
340
|
+
let projectsContent = "";
|
|
341
|
+
if (fs.existsSync(PROJECTS_FILE)) {
|
|
342
|
+
projectsContent = fs.readFileSync(PROJECTS_FILE, "utf-8");
|
|
343
|
+
}
|
|
344
|
+
const lineNum = projectsContent.split("\n").filter((l) => l.trim()).length + 1;
|
|
345
|
+
projectsContent += `\n${lineNum}. ${name} → ${workDir}`;
|
|
346
|
+
fs.writeFileSync(PROJECTS_FILE, projectsContent.trim() + "\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function setupFeishuQrCode(name, configPath) {
|
|
350
|
+
console.log("\n正在启动飞书扫码配置...\n");
|
|
351
|
+
try {
|
|
352
|
+
execSync(`cc-connect feishu setup --project "${name}" --config "${configPath}"`, {
|
|
353
|
+
stdio: "inherit",
|
|
354
|
+
timeout: 600000,
|
|
355
|
+
});
|
|
356
|
+
console.log("\n✓ 飞书机器人配置完成");
|
|
357
|
+
return true;
|
|
358
|
+
} catch (err) {
|
|
359
|
+
console.log("\n✗ 扫码配置失败或已取消");
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function promptPlatformConfig(rl, selectedPlatform, callback) {
|
|
365
|
+
// 飞书/Lark 支持扫码
|
|
366
|
+
if (selectedPlatform.hasQrSetup) {
|
|
367
|
+
console.log(`\n${selectedPlatform.name}机器人配置方式:\n`);
|
|
368
|
+
console.log(" [1] 扫码创建新机器人(推荐)");
|
|
369
|
+
console.log(" [2] 绑定已有机器人(手动输入凭证)");
|
|
370
|
+
|
|
371
|
+
rl.question("\n选择 (默认 1): ", (choice) => {
|
|
372
|
+
if (choice === "2") {
|
|
373
|
+
promptPlatformFields(rl, selectedPlatform, callback);
|
|
374
|
+
} else {
|
|
375
|
+
callback({ qrSetup: true });
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
} else {
|
|
379
|
+
// 其他平台直接输入凭证
|
|
380
|
+
console.log(`\n请输入 ${selectedPlatform.name} 凭证:\n`);
|
|
381
|
+
promptPlatformFields(rl, selectedPlatform, callback);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function promptPlatformFields(rl, selectedPlatform, callback) {
|
|
386
|
+
const fields = selectedPlatform.fields;
|
|
387
|
+
const options = {};
|
|
388
|
+
let i = 0;
|
|
389
|
+
|
|
390
|
+
const fieldLabels = {
|
|
391
|
+
app_id: "App ID",
|
|
392
|
+
app_secret: "App Secret",
|
|
393
|
+
token: "Bot Token",
|
|
394
|
+
base_url: "Base URL",
|
|
395
|
+
account_id: "Account ID",
|
|
396
|
+
bot_token: "Bot Token",
|
|
397
|
+
app_token: "App Token",
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
function askNext() {
|
|
401
|
+
if (i >= fields.length) {
|
|
402
|
+
callback(options);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const field = fields[i];
|
|
406
|
+
const label = fieldLabels[field] || field;
|
|
407
|
+
rl.question(`${label}: `, (value) => {
|
|
408
|
+
options[field] = value;
|
|
409
|
+
i++;
|
|
410
|
+
askNext();
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
askNext();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function initProject() {
|
|
417
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
418
|
+
|
|
419
|
+
console.log("\n╔══════════════════════════════════════╗");
|
|
420
|
+
console.log("║ 新建项目配置 ║");
|
|
421
|
+
console.log("╚══════════════════════════════════════╝\n");
|
|
422
|
+
|
|
423
|
+
// 选择 Agent
|
|
424
|
+
console.log("① 选择 Agent:\n");
|
|
425
|
+
AGENTS.forEach((agent, i) => {
|
|
426
|
+
const mark = agent.type === "claudecode" ? " ← 默认" : "";
|
|
427
|
+
console.log(` [${i + 1}] ${agent.name.padEnd(15)} (${agent.type})${mark}`);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
rl.question("\nAgent 编号 (默认 1): ", (agentAnswer) => {
|
|
431
|
+
const agentIdx = parseInt(agentAnswer) || 1;
|
|
432
|
+
const selectedAgent = AGENTS[agentIdx - 1] || AGENTS[0];
|
|
433
|
+
|
|
434
|
+
// 选择平台
|
|
435
|
+
console.log("\n② 选择平台:\n");
|
|
436
|
+
PLATFORMS.forEach((platform, i) => {
|
|
437
|
+
const mark = platform.type === "feishu" ? " ← 默认" : "";
|
|
438
|
+
const qrTag = platform.hasQrSetup ? " [支持扫码]" : "";
|
|
439
|
+
console.log(` [${i + 1}] ${platform.name.padEnd(18)} (${platform.type})${qrTag}${mark}`);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
rl.question("\n平台编号 (默认 1): ", (platformAnswer) => {
|
|
443
|
+
const platformIdx = parseInt(platformAnswer) || 1;
|
|
444
|
+
const selectedPlatform = PLATFORMS[platformIdx - 1] || PLATFORMS[0];
|
|
445
|
+
|
|
446
|
+
rl.question("\n③ 项目名称 (英文,如 my-app): ", (name) => {
|
|
447
|
+
rl.question("④ 代码目录路径 (如 D:\\projects\\my-app): ", (workDir) => {
|
|
448
|
+
|
|
449
|
+
promptPlatformConfig(rl, selectedPlatform, (platformOptions) => {
|
|
450
|
+
if (platformOptions.qrSetup) {
|
|
451
|
+
// 扫码方式
|
|
452
|
+
rl.close();
|
|
453
|
+
const placeholderOptions = {};
|
|
454
|
+
selectedPlatform.fields.forEach(f => placeholderOptions[f] = `PLACEHOLDER_${f.toUpperCase()}`);
|
|
455
|
+
const template = generateConfig(name, workDir, selectedAgent, selectedPlatform, placeholderOptions);
|
|
456
|
+
const configPath = path.join(CONFIGS_DIR, `config-${name}.toml`);
|
|
457
|
+
fs.writeFileSync(configPath, template);
|
|
458
|
+
|
|
459
|
+
const success = setupFeishuQrCode(name, configPath);
|
|
460
|
+
if (success) {
|
|
461
|
+
finalizeConfig(name, workDir);
|
|
462
|
+
console.log(`\n✓ 项目配置完成`);
|
|
463
|
+
console.log(` Agent: ${selectedAgent.name}`);
|
|
464
|
+
console.log(` 平台: ${selectedPlatform.name}`);
|
|
465
|
+
console.log(` 启动: ccm start ${name}`);
|
|
466
|
+
} else {
|
|
467
|
+
try { fs.unlinkSync(configPath); } catch {}
|
|
468
|
+
console.log("\n配置已取消");
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
// 手动输入凭证
|
|
472
|
+
const template = generateConfig(name, workDir, selectedAgent, selectedPlatform, platformOptions);
|
|
473
|
+
const configPath = path.join(CONFIGS_DIR, `config-${name}.toml`);
|
|
474
|
+
fs.writeFileSync(configPath, template);
|
|
475
|
+
finalizeConfig(name, workDir);
|
|
476
|
+
|
|
477
|
+
console.log(`\n✓ 配置已创建: ${configPath}`);
|
|
478
|
+
console.log(` Agent: ${selectedAgent.name}`);
|
|
479
|
+
console.log(` 平台: ${selectedPlatform.name}`);
|
|
480
|
+
console.log(` 启动: ccm start ${name}`);
|
|
481
|
+
rl.close();
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function interactive() {
|
|
491
|
+
const configs = getConfigs();
|
|
492
|
+
const running = getRunningStatus();
|
|
493
|
+
|
|
494
|
+
if (configs.length === 0) {
|
|
495
|
+
console.log("\n还没有项目配置,运行 ccm --init 创建第一个项目\n");
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
console.log("\n╔══════════════════════════════════════╗");
|
|
500
|
+
console.log("║ cc-connect 项目管理器 ║");
|
|
501
|
+
console.log("╚══════════════════════════════════════╝\n");
|
|
502
|
+
|
|
503
|
+
for (const config of configs) {
|
|
504
|
+
const projects = getConfigInfo(config.path);
|
|
505
|
+
const platform = projects.map((p) => p.platform).join(", ");
|
|
506
|
+
const agent = getCurrentAgent(config.path);
|
|
507
|
+
const isUp = running[config.name];
|
|
508
|
+
const icon = isUp ? "🟢" : "⚪";
|
|
509
|
+
console.log(` ${icon} [${config.index}] ${config.name.padEnd(20)} ${agent.padEnd(12)} ${platform}`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
console.log(`\n 操作:`);
|
|
513
|
+
console.log(` 输入编号 → 启动该项目(可选 Agent)`);
|
|
514
|
+
console.log(` stop 编号 → 停止该项目`);
|
|
515
|
+
console.log(` all → 启动所有项目`);
|
|
516
|
+
console.log(` stop all → 停止所有项目`);
|
|
517
|
+
console.log(` status → 查看状态`);
|
|
518
|
+
console.log(` agents → 查看支持的 Agent 列表`);
|
|
519
|
+
console.log(` init → 新建项目`);
|
|
520
|
+
console.log(` 0 → 退出\n`);
|
|
521
|
+
|
|
522
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
523
|
+
|
|
524
|
+
function prompt() {
|
|
525
|
+
rl.question("ccm> ", (answer) => {
|
|
526
|
+
const input = answer.trim().toLowerCase();
|
|
527
|
+
|
|
528
|
+
if (input === "0" || input === "exit" || input === "quit") {
|
|
529
|
+
rl.close();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (input === "status") {
|
|
534
|
+
showStatus();
|
|
535
|
+
prompt();
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (input === "agents") {
|
|
540
|
+
console.log("\n支持的 Agent:\n");
|
|
541
|
+
AGENTS.forEach((a, i) => {
|
|
542
|
+
console.log(` [${i + 1}] ${a.name.padEnd(15)} ${a.type.padEnd(14)} 模式: ${a.modes.join(", ")}`);
|
|
543
|
+
});
|
|
544
|
+
console.log();
|
|
545
|
+
prompt();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (input === "init") {
|
|
550
|
+
rl.close();
|
|
551
|
+
initProject();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (input === "all") {
|
|
556
|
+
console.log("\n启动所有项目...\n");
|
|
557
|
+
for (const config of configs) startProject(config);
|
|
558
|
+
setTimeout(prompt, 2500);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (input === "stop all") {
|
|
563
|
+
console.log("\n停止所有项目...\n");
|
|
564
|
+
for (const config of configs) stopProject(config.name);
|
|
565
|
+
prompt();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (input.startsWith("stop ")) {
|
|
570
|
+
const target = input.replace("stop ", "").trim();
|
|
571
|
+
const idx = parseInt(target);
|
|
572
|
+
const config = configs.find((c) => c.index === idx);
|
|
573
|
+
if (config) {
|
|
574
|
+
stopProject(config.name);
|
|
575
|
+
} else {
|
|
576
|
+
console.log("无效编号");
|
|
577
|
+
}
|
|
578
|
+
prompt();
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 启动项目
|
|
583
|
+
const idx = parseInt(input);
|
|
584
|
+
if (!isNaN(idx) && idx > 0) {
|
|
585
|
+
const config = configs.find((c) => c.index === idx);
|
|
586
|
+
if (config) {
|
|
587
|
+
// 弹出 Agent 选择
|
|
588
|
+
rl.close();
|
|
589
|
+
selectAgent(config, (agentType) => {
|
|
590
|
+
console.log();
|
|
591
|
+
startProject(config, agentType);
|
|
592
|
+
// 重新创建 rl 继续交互
|
|
593
|
+
setTimeout(() => interactive(), 2500);
|
|
594
|
+
});
|
|
595
|
+
} else {
|
|
596
|
+
console.log("无效编号");
|
|
597
|
+
prompt();
|
|
598
|
+
}
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
console.log("无效输入");
|
|
603
|
+
prompt();
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
prompt();
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// 主入口
|
|
611
|
+
ensureDirs();
|
|
612
|
+
const args = process.argv.slice(2);
|
|
613
|
+
|
|
614
|
+
if (args.includes("--list") || args.includes("-l")) {
|
|
615
|
+
const configs = getConfigs();
|
|
616
|
+
const running = getRunningStatus();
|
|
617
|
+
console.log("\n可用配置:\n");
|
|
618
|
+
for (const config of configs) {
|
|
619
|
+
const projects = getConfigInfo(config.path);
|
|
620
|
+
const platform = projects.map((p) => p.platform).join(", ");
|
|
621
|
+
const dir = [...new Set(projects.map((p) => p.workDir))].join(", ");
|
|
622
|
+
const agent = getCurrentAgent(config.path);
|
|
623
|
+
const icon = running[config.name] ? "🟢" : "⚪";
|
|
624
|
+
console.log(` ${icon} ${config.name}`);
|
|
625
|
+
console.log(` Agent: ${agent}`);
|
|
626
|
+
console.log(` 平台: ${platform}`);
|
|
627
|
+
console.log(` 目录: ${dir}\n`);
|
|
628
|
+
}
|
|
629
|
+
} else if (args.includes("--init")) {
|
|
630
|
+
initProject();
|
|
631
|
+
} else if (args[0] === "status") {
|
|
632
|
+
showStatus();
|
|
633
|
+
} else if (args[0] === "agents") {
|
|
634
|
+
console.log("\n支持的 Agent:\n");
|
|
635
|
+
AGENTS.forEach((a, i) => {
|
|
636
|
+
console.log(` [${i + 1}] ${a.name.padEnd(15)} ${a.type.padEnd(14)} 模式: ${a.modes.join(", ")}`);
|
|
637
|
+
});
|
|
638
|
+
console.log();
|
|
639
|
+
} else if (args[0] === "web") {
|
|
640
|
+
const port = args.includes("--port") ? parseInt(args[args.indexOf("--port") + 1]) : 3080;
|
|
641
|
+
const { startServer } = require("./server.js");
|
|
642
|
+
startServer(port);
|
|
643
|
+
} else if (args[0] === "start" && args[1]) {
|
|
644
|
+
const configs = getConfigs();
|
|
645
|
+
if (args[1] === "all") {
|
|
646
|
+
console.log("\n启动所有项目...\n");
|
|
647
|
+
for (const config of configs) startProject(config, args[2]);
|
|
648
|
+
} else {
|
|
649
|
+
const idx = parseInt(args[1]);
|
|
650
|
+
const config = configs.find((c) => c.index === idx || c.name === args[1]);
|
|
651
|
+
if (config) startProject(config, args[2]);
|
|
652
|
+
else console.log("项目不存在");
|
|
653
|
+
}
|
|
654
|
+
} else if (args[0] === "stop" && args[1]) {
|
|
655
|
+
const configs = getConfigs();
|
|
656
|
+
if (args[1] === "all") {
|
|
657
|
+
console.log("\n停止所有项目...\n");
|
|
658
|
+
for (const config of configs) stopProject(config.name);
|
|
659
|
+
try {
|
|
660
|
+
if (process.platform === "win32") {
|
|
661
|
+
execSync("taskkill /F /IM cc-connect.exe", { stdio: "ignore" });
|
|
662
|
+
}
|
|
663
|
+
} catch {}
|
|
664
|
+
} else {
|
|
665
|
+
const idx = parseInt(args[1]);
|
|
666
|
+
const config = configs.find((c) => c.index === idx || c.name === args[1]);
|
|
667
|
+
if (config) stopProject(config.name);
|
|
668
|
+
else console.log("项目不存在");
|
|
669
|
+
}
|
|
670
|
+
} else if (args.length > 0 && !args[0].startsWith("-")) {
|
|
671
|
+
const configs = getConfigs();
|
|
672
|
+
const config = configs.find((c) => c.name === args[0]);
|
|
673
|
+
if (config) startProject(config, args[1]);
|
|
674
|
+
else console.log(`项目 "${args[0]}" 不存在,用 cc-web --list 查看`);
|
|
675
|
+
} else {
|
|
676
|
+
// 默认启动 Web 控制台
|
|
677
|
+
const port = 3080;
|
|
678
|
+
const { startServer } = require("./server.js");
|
|
679
|
+
startServer(port);
|
|
680
|
+
}
|