claude360 0.2.1 → 0.2.3
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 +16 -1
- package/package.json +1 -1
- package/src/account-status.js +60 -43
- package/src/backup.js +54 -0
- package/src/diagnostics.js +63 -35
- package/src/index.js +444 -45
- package/src/init-config.js +383 -0
- package/src/init-flow.js +70 -0
- package/src/mcp-skill.js +257 -41
- package/src/menu.js +87 -23
- package/src/token-manager.js +49 -1
- package/src/tool-launcher.js +15 -8
- package/src/ui.js +137 -0
- package/src/workflows.js +331 -0
- package/src/zcf-notice.js +40 -0
package/src/mcp-skill.js
CHANGED
|
@@ -3,82 +3,277 @@
|
|
|
3
3
|
|
|
4
4
|
import { sanitizeText } from "./sanitize.js";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { copyFile, cp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
|
|
10
|
+
import { createBackup } from "./backup.js";
|
|
11
|
+
import { upsertTomlTable, validateBasicToml } from "./tool-launcher.js";
|
|
12
|
+
|
|
10
13
|
// ──────────────────────────────────────────────
|
|
11
|
-
// 推荐 MCP
|
|
14
|
+
// 推荐 MCP(PRD 6.6):Claude Code 通过官方 `claude mcp add --scope user`
|
|
15
|
+
// 安装(修改 ~/.claude.json);Codex 写入 ~/.codex/config.toml [mcp_servers.*]。
|
|
16
|
+
// 默认推荐 Context7 / Open Web Search / DeepWiki;Playwright、Serena 依赖较重,
|
|
17
|
+
// 安装前单独确认。全部可跳过、失败不阻塞主流程、不重复安装同名 MCP。
|
|
12
18
|
// ──────────────────────────────────────────────
|
|
13
19
|
|
|
14
20
|
export const RECOMMENDED_MCPS = [
|
|
15
21
|
{
|
|
16
22
|
id: "context7",
|
|
17
|
-
label: "Context7
|
|
18
|
-
|
|
23
|
+
label: "Context7 文档查询",
|
|
24
|
+
desc: "查询最新库文档和代码示例",
|
|
25
|
+
recommended: true,
|
|
26
|
+
claudeArgs: ["mcp", "add", "--scope", "user", "context7", "--", "npx", "-y", "@upstash/context7-mcp"],
|
|
27
|
+
codex: { command: "npx", args: ["-y", "@upstash/context7-mcp"] },
|
|
19
28
|
},
|
|
20
29
|
{
|
|
21
|
-
id: "
|
|
22
|
-
label: "
|
|
23
|
-
|
|
30
|
+
id: "open-websearch",
|
|
31
|
+
label: "Open Web Search 网页搜索",
|
|
32
|
+
desc: "使用搜索引擎进行网页搜索",
|
|
33
|
+
recommended: true,
|
|
34
|
+
claudeArgs: ["mcp", "add", "--scope", "user", "open-websearch", "--", "npx", "-y", "open-websearch@latest"],
|
|
35
|
+
codex: { command: "npx", args: ["-y", "open-websearch@latest"] },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "spec-workflow",
|
|
39
|
+
label: "Spec 工作流",
|
|
40
|
+
desc: "规范化特性开发流程",
|
|
41
|
+
claudeArgs: ["mcp", "add", "--scope", "user", "spec-workflow", "--", "npx", "-y", "@pimzino/spec-workflow-mcp@latest"],
|
|
42
|
+
codex: { command: "npx", args: ["-y", "@pimzino/spec-workflow-mcp@latest"] },
|
|
24
43
|
},
|
|
25
44
|
{
|
|
26
45
|
id: "deepwiki",
|
|
27
|
-
label: "DeepWiki
|
|
28
|
-
|
|
46
|
+
label: "DeepWiki 仓库文档",
|
|
47
|
+
desc: "查询 GitHub 仓库文档",
|
|
48
|
+
recommended: true,
|
|
49
|
+
claudeArgs: ["mcp", "add", "--scope", "user", "--transport", "http", "deepwiki", "https://mcp.deepwiki.com/mcp"],
|
|
50
|
+
codex: { command: "npx", args: ["-y", "mcp-deepwiki@latest"] },
|
|
29
51
|
},
|
|
30
52
|
{
|
|
31
|
-
id: "
|
|
32
|
-
label: "
|
|
33
|
-
|
|
53
|
+
id: "playwright",
|
|
54
|
+
label: "Playwright 浏览器控制",
|
|
55
|
+
desc: "浏览器自动化操作",
|
|
56
|
+
heavy: "需要下载浏览器内核,占用磁盘较大",
|
|
57
|
+
claudeArgs: ["mcp", "add", "--scope", "user", "playwright", "--", "npx", "-y", "@playwright/mcp@latest"],
|
|
58
|
+
codex: { command: "npx", args: ["-y", "@playwright/mcp@latest"] },
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "exa",
|
|
62
|
+
label: "Exa AI 搜索",
|
|
63
|
+
desc: "AI 搜索能力(需自备 EXA_API_KEY)",
|
|
64
|
+
claudeArgs: ["mcp", "add", "--scope", "user", "exa", "--", "npx", "-y", "exa-mcp-server"],
|
|
65
|
+
codex: { command: "npx", args: ["-y", "exa-mcp-server"] },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "serena",
|
|
69
|
+
label: "Serena 助手",
|
|
70
|
+
desc: "语义代码检索与编辑助手",
|
|
71
|
+
heavy: "依赖 Python uv 工具链",
|
|
72
|
+
claudeArgs: ["mcp", "add", "--scope", "user", "serena", "--", "uvx", "--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"],
|
|
73
|
+
codex: { command: "uvx", args: ["--from", "git+https://github.com/oraios/serena", "serena", "start-mcp-server"] },
|
|
34
74
|
},
|
|
35
75
|
];
|
|
36
76
|
|
|
77
|
+
export function resolveClaudeJsonPath({ homedir = os.homedir } = {}) {
|
|
78
|
+
return path.join(homedir(), ".claude.json");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 已安装的 Claude Code 用户级 MCP(读取 ~/.claude.json 的 mcpServers 键)
|
|
82
|
+
export async function readInstalledClaudeMcps({
|
|
83
|
+
claudeJsonPath = resolveClaudeJsonPath(),
|
|
84
|
+
fs = { readFile },
|
|
85
|
+
} = {}) {
|
|
86
|
+
const content = await readFileIfExists(fs.readFile, claudeJsonPath);
|
|
87
|
+
if (content.trim() === "") {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const json = JSON.parse(content);
|
|
92
|
+
return Object.keys(json?.mcpServers || {});
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 多选推荐 MCP;已安装项展示为「已安装」且不可重复安装(PRD 6.6)
|
|
99
|
+
async function selectMcps({ multiSelect, installedIds, writeLine }) {
|
|
100
|
+
const choices = RECOMMENDED_MCPS.map((mcp) => ({
|
|
101
|
+
label: `${mcp.label}${installedIds.includes(mcp.id) ? "(已安装)" : ""}`,
|
|
102
|
+
value: mcp.id,
|
|
103
|
+
hint: mcp.desc,
|
|
104
|
+
}));
|
|
105
|
+
const preselected = RECOMMENDED_MCPS
|
|
106
|
+
.filter((mcp) => mcp.recommended && !installedIds.includes(mcp.id))
|
|
107
|
+
.map((mcp) => mcp.id);
|
|
108
|
+
const selected = await multiSelect({
|
|
109
|
+
message: "请选择要安装的 MCP 服务(默认推荐 Context7 / Open Web Search / DeepWiki):",
|
|
110
|
+
choices,
|
|
111
|
+
preselected,
|
|
112
|
+
});
|
|
113
|
+
const fresh = [];
|
|
114
|
+
for (const id of selected) {
|
|
115
|
+
if (installedIds.includes(id)) {
|
|
116
|
+
writeLine(`已安装,跳过:${id}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
fresh.push(RECOMMENDED_MCPS.find((mcp) => mcp.id === id));
|
|
120
|
+
}
|
|
121
|
+
return fresh;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 重依赖(Playwright / Serena 等)逐项单独确认
|
|
125
|
+
async function confirmHeavyMcps(mcps, confirm, writeLine) {
|
|
126
|
+
const approved = [];
|
|
127
|
+
for (const mcp of mcps) {
|
|
128
|
+
if (!mcp.heavy) {
|
|
129
|
+
approved.push(mcp);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const ok = await confirm(`${mcp.label} 依赖较重(${mcp.heavy}),确认安装?`);
|
|
133
|
+
if (ok) {
|
|
134
|
+
approved.push(mcp);
|
|
135
|
+
} else {
|
|
136
|
+
writeLine(`已跳过 ${mcp.id}。`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return approved;
|
|
140
|
+
}
|
|
141
|
+
|
|
37
142
|
export async function installRecommendedMcps({
|
|
38
|
-
|
|
143
|
+
multiSelect,
|
|
39
144
|
confirm,
|
|
40
145
|
execCommand = defaultExec,
|
|
41
146
|
writeLine = console.log,
|
|
147
|
+
claudeJsonPath = resolveClaudeJsonPath(),
|
|
148
|
+
fs = { readFile },
|
|
42
149
|
} = {}) {
|
|
43
|
-
if (typeof
|
|
150
|
+
if (typeof multiSelect !== "function" || typeof confirm !== "function") {
|
|
44
151
|
throw new Error("缺少交互输入");
|
|
45
152
|
}
|
|
153
|
+
const installedIds = await readInstalledClaudeMcps({ claudeJsonPath, fs });
|
|
154
|
+
const fresh = await selectMcps({ multiSelect, installedIds, writeLine });
|
|
155
|
+
if (fresh.length === 0) {
|
|
156
|
+
writeLine("已跳过 MCP 安装。");
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
const approved = await confirmHeavyMcps(fresh, confirm, writeLine);
|
|
160
|
+
if (approved.length === 0) {
|
|
161
|
+
writeLine("已跳过 MCP 安装。");
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
writeLine([
|
|
166
|
+
"即将通过 Claude Code 官方命令安装以下 MCP(修改 ~/.claude.json 用户级配置):",
|
|
167
|
+
...approved.map((mcp) => ` claude ${mcp.claudeArgs.join(" ")}`),
|
|
168
|
+
].join("\n"));
|
|
169
|
+
if (!(await confirm("是否继续?"))) {
|
|
170
|
+
writeLine("已取消 MCP 安装。");
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
|
|
46
174
|
const installed = [];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
label: installed.includes(mcp.id) ? `${mcp.label}(本次已安装)` : mcp.label,
|
|
51
|
-
value: mcp.id,
|
|
52
|
-
})),
|
|
53
|
-
{ label: "完成 / 跳过", value: "done" },
|
|
54
|
-
];
|
|
55
|
-
const selected = await promptSelect("推荐 MCP(安装会修改 ~/.claude.json 的用户级 MCP 配置)", choices);
|
|
56
|
-
if (selected === "done") {
|
|
57
|
-
break;
|
|
58
|
-
}
|
|
59
|
-
const mcp = RECOMMENDED_MCPS.find((item) => item.id === selected);
|
|
60
|
-
if (!mcp) {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const commandText = `claude ${mcp.args.join(" ")}`;
|
|
64
|
-
const approved = await confirm(
|
|
65
|
-
`即将执行:${commandText}\n影响范围:在 Claude Code 用户级配置(~/.claude.json)中添加 MCP ${mcp.id}。\n是否继续?`,
|
|
66
|
-
);
|
|
67
|
-
if (!approved) {
|
|
68
|
-
writeLine(`已跳过 ${mcp.id}。`);
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const result = await execCommand("claude", mcp.args);
|
|
175
|
+
for (const mcp of approved) {
|
|
176
|
+
writeLine(`正在安装 MCP:${mcp.label}...`);
|
|
177
|
+
const result = await execCommand("claude", mcp.claudeArgs);
|
|
72
178
|
if (result.ok) {
|
|
73
179
|
installed.push(mcp.id);
|
|
74
180
|
writeLine(`✓ 已安装 MCP:${mcp.id}`);
|
|
75
181
|
} else {
|
|
76
|
-
writeLine(
|
|
182
|
+
writeLine([
|
|
183
|
+
`× 安装 ${mcp.id} 失败`,
|
|
184
|
+
"",
|
|
185
|
+
`原因:${sanitizeText(result.stderr || result.error || "未知错误")}`,
|
|
186
|
+
"",
|
|
187
|
+
"建议:",
|
|
188
|
+
"1. 检查网络",
|
|
189
|
+
"2. 使用国内 npm 镜像",
|
|
190
|
+
"3. 稍后重试(安装失败不影响 Claude360 API 调用)",
|
|
191
|
+
].join("\n"));
|
|
77
192
|
}
|
|
78
193
|
}
|
|
79
194
|
return installed;
|
|
80
195
|
}
|
|
81
196
|
|
|
197
|
+
// ──────────────────────────────────────────────
|
|
198
|
+
// Codex 推荐 MCP:写入 ~/.codex/config.toml 的 [mcp_servers.*] 表,
|
|
199
|
+
// 写入前备份并做 TOML 校验,校验失败不落盘(PRD 6.6 / 8 章)
|
|
200
|
+
// ──────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export async function readInstalledCodexMcps({
|
|
203
|
+
codexDir = resolveCodexUserDir(),
|
|
204
|
+
fs = { readFile },
|
|
205
|
+
} = {}) {
|
|
206
|
+
const content = await readFileIfExists(fs.readFile, path.join(codexDir, "config.toml"));
|
|
207
|
+
const ids = [];
|
|
208
|
+
for (const match of content.matchAll(/^\s*\[mcp_servers\.([A-Za-z0-9_-]+)\]\s*$/gm)) {
|
|
209
|
+
ids.push(match[1]);
|
|
210
|
+
}
|
|
211
|
+
return ids;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function escapeTomlString(value) {
|
|
215
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function buildCodexMcpTable(mcp) {
|
|
219
|
+
const args = mcp.codex.args.map((arg) => `"${escapeTomlString(arg)}"`).join(", ");
|
|
220
|
+
return [
|
|
221
|
+
`[mcp_servers.${mcp.id}]`,
|
|
222
|
+
`command = "${escapeTomlString(mcp.codex.command)}"`,
|
|
223
|
+
`args = [${args}]`,
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function installCodexMcps({
|
|
228
|
+
multiSelect,
|
|
229
|
+
confirm,
|
|
230
|
+
writeLine = console.log,
|
|
231
|
+
codexDir = resolveCodexUserDir(),
|
|
232
|
+
fs = { readFile, writeFile, mkdir, copyFile, cp, stat },
|
|
233
|
+
now,
|
|
234
|
+
} = {}) {
|
|
235
|
+
if (typeof multiSelect !== "function" || typeof confirm !== "function") {
|
|
236
|
+
throw new Error("缺少交互输入");
|
|
237
|
+
}
|
|
238
|
+
const configPath = path.join(codexDir, "config.toml");
|
|
239
|
+
const installedIds = await readInstalledCodexMcps({ codexDir, fs });
|
|
240
|
+
const fresh = await selectMcps({ multiSelect, installedIds, writeLine });
|
|
241
|
+
if (fresh.length === 0) {
|
|
242
|
+
writeLine("已跳过 MCP 安装。");
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
const approved = await confirmHeavyMcps(fresh, confirm, writeLine);
|
|
246
|
+
if (approved.length === 0) {
|
|
247
|
+
writeLine("已跳过 MCP 安装。");
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
if (!(await confirm(`即将在 ${configPath} 中写入 ${approved.map((mcp) => mcp.id).join("、")} 的 MCP 配置(写入前备份)。\n是否继续?`))) {
|
|
251
|
+
writeLine("已取消 MCP 安装。");
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let content = await readFileIfExists(fs.readFile, configPath);
|
|
256
|
+
for (const mcp of approved) {
|
|
257
|
+
content = upsertTomlTable(content, `mcp_servers.${mcp.id}`, buildCodexMcpTable(mcp));
|
|
258
|
+
}
|
|
259
|
+
content = `${content.trimEnd()}\n`;
|
|
260
|
+
const tomlError = validateBasicToml(content);
|
|
261
|
+
if (tomlError) {
|
|
262
|
+
writeLine(`× 生成的 Codex 配置 TOML 校验失败:${tomlError}\n已放弃写入,原配置保持不变。`);
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
const { backupDir } = await createBackup({ baseDir: codexDir, paths: [configPath], fs, now });
|
|
266
|
+
if (backupDir) {
|
|
267
|
+
writeLine(`✓ 已创建备份:${backupDir}`);
|
|
268
|
+
}
|
|
269
|
+
await fs.mkdir(codexDir, { recursive: true });
|
|
270
|
+
await fs.writeFile(configPath, content, "utf8");
|
|
271
|
+
for (const mcp of approved) {
|
|
272
|
+
writeLine(`✓ 已安装 MCP:${mcp.id}`);
|
|
273
|
+
}
|
|
274
|
+
return approved.map((mcp) => mcp.id);
|
|
275
|
+
}
|
|
276
|
+
|
|
82
277
|
// ──────────────────────────────────────────────
|
|
83
278
|
// 标记区块:幂等写入公共 Markdown 文件
|
|
84
279
|
// ──────────────────────────────────────────────
|
|
@@ -277,17 +472,38 @@ export async function installCodexAgents({
|
|
|
277
472
|
export async function listInstalledEnhancements({
|
|
278
473
|
claudeDir = resolveClaudeUserDir(),
|
|
279
474
|
codexDir = resolveCodexUserDir(),
|
|
475
|
+
claudeJsonPath = resolveClaudeJsonPath(),
|
|
280
476
|
fs = { readFile },
|
|
281
477
|
} = {}) {
|
|
282
|
-
const lines = ["已安装增强配置:"];
|
|
478
|
+
const lines = ["已安装增强配置:", ""];
|
|
479
|
+
|
|
480
|
+
const claudeMcps = await readInstalledClaudeMcps({ claudeJsonPath, fs });
|
|
481
|
+
const codexMcps = await readInstalledCodexMcps({ codexDir, fs });
|
|
482
|
+
lines.push("[MCP]");
|
|
483
|
+
for (const mcp of RECOMMENDED_MCPS) {
|
|
484
|
+
const states = [
|
|
485
|
+
claudeMcps.includes(mcp.id) ? "Claude Code 已安装" : null,
|
|
486
|
+
codexMcps.includes(mcp.id) ? "Codex 已安装" : null,
|
|
487
|
+
].filter(Boolean);
|
|
488
|
+
lines.push(`- ${mcp.label}:${states.length > 0 ? states.join(",") : "未安装"}`);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
lines.push("", "[Claude Code 工作流 / Skill]");
|
|
283
492
|
const memory = await readFileIfExists(fs.readFile, path.join(claudeDir, "CLAUDE.md"));
|
|
284
493
|
lines.push(`- Claude360 编程助手规则:${memory.includes("claude360:assistant-rules:start") ? "已安装" : "未安装"}`);
|
|
494
|
+
lines.push(`- AI 输出语言偏好:${memory.includes("claude360:output-language:start") ? "已配置" : "未配置"}`);
|
|
495
|
+
lines.push(`- 系统提示词风格:${memory.includes("claude360:prompt-style:start") ? "已配置" : "未配置"}`);
|
|
285
496
|
for (const skill of RECOMMENDED_SKILLS.filter((item) => item.kind === "command")) {
|
|
286
497
|
const content = await readFileIfExists(fs.readFile, path.join(claudeDir, "commands", "claude360", skill.file));
|
|
287
498
|
lines.push(`- ${skill.label}:${content ? "已安装" : "未安装"}`);
|
|
288
499
|
}
|
|
500
|
+
|
|
501
|
+
lines.push("", "[Codex]");
|
|
289
502
|
const agents = await readFileIfExists(fs.readFile, path.join(codexDir, "AGENTS.md"));
|
|
290
503
|
lines.push(`- Codex AGENTS 规范:${agents.includes(`claude360:${CODEX_AGENTS_BLOCK_ID}:start`) ? "已安装" : "未安装"}`);
|
|
504
|
+
lines.push(`- Codex AI 输出语言偏好:${agents.includes("claude360:output-language:start") ? "已配置" : "未配置"}`);
|
|
505
|
+
const prompts = await readFileIfExists(fs.readFile, path.join(codexDir, "prompts", "workflow.md"));
|
|
506
|
+
lines.push(`- Codex 六步工作流 prompts:${prompts ? "已安装" : "未安装"}`);
|
|
291
507
|
return lines.join("\n");
|
|
292
508
|
}
|
|
293
509
|
|
package/src/menu.js
CHANGED
|
@@ -15,6 +15,7 @@ const RULE_COLOR = fg(71, 85, 105); // 分隔线:青灰
|
|
|
15
15
|
const SECTION_COLOR = fg(34, 211, 238); // 分区标题:亮青
|
|
16
16
|
const KEY_COLOR = fg(125, 211, 252); // 选择键:天蓝
|
|
17
17
|
const EXIT_COLOR = fg(248, 113, 113); // 退出键:红
|
|
18
|
+
const DESC_COLOR = fg(100, 116, 139); // 功能说明:暗灰
|
|
18
19
|
const MENU_RULE_WIDTH = 46;
|
|
19
20
|
|
|
20
21
|
export function buildFirstRunMenu() {
|
|
@@ -29,13 +30,13 @@ export function buildFirstRunMenu() {
|
|
|
29
30
|
{
|
|
30
31
|
title: null,
|
|
31
32
|
items: [
|
|
32
|
-
{ key: "1", label: "一键配置 Claude Code", value: "setup_claude" },
|
|
33
|
-
{ key: "2", label: "一键配置 Codex", value: "setup_codex" },
|
|
34
|
-
{ key: "3", label: "同时配置 Claude Code + Codex", value: "setup_both" },
|
|
35
|
-
{ key: "4", label: "仅登录 Claude360 并创建 API Key", value: "login_only" },
|
|
36
|
-
{ key: "5", label: "生成 cc-switch 配置", value: "cc_switch" },
|
|
37
|
-
{ key: "6", label: "打开 Claude360 注册 / 登录页面", value: "open_register" },
|
|
38
|
-
{ key: "Q", label: "退出", value: "exit" },
|
|
33
|
+
{ key: "1", label: "一键配置 Claude Code", value: "setup_claude", desc: "登录并创建 API Key,自动写入 Claude Code 配置" },
|
|
34
|
+
{ key: "2", label: "一键配置 Codex", value: "setup_codex", desc: "登录并创建 API Key,自动写入 Codex 配置" },
|
|
35
|
+
{ key: "3", label: "同时配置 Claude Code + Codex", value: "setup_both", desc: "一次登录,两个工具同时完成接入" },
|
|
36
|
+
{ key: "4", label: "仅登录 Claude360 并创建 API Key", value: "login_only", desc: "只获取 Key,不修改本地工具配置" },
|
|
37
|
+
{ key: "5", label: "生成 cc-switch 配置", value: "cc_switch", desc: "生成可导入 cc-switch 的供应商配置" },
|
|
38
|
+
{ key: "6", label: "打开 Claude360 注册 / 登录页面", value: "open_register", desc: "在浏览器中打开官网注册或登录" },
|
|
39
|
+
{ key: "Q", label: "退出", value: "exit", desc: "退出 claude360 CLI" },
|
|
39
40
|
],
|
|
40
41
|
},
|
|
41
42
|
],
|
|
@@ -49,34 +50,35 @@ export function buildDailyMenu() {
|
|
|
49
50
|
{
|
|
50
51
|
title: "快速启动",
|
|
51
52
|
items: [
|
|
52
|
-
{ key: "1", label: "启动 Claude Code", value: "launch_claude" },
|
|
53
|
-
{ key: "2", label: "启动 Codex", value: "launch_codex" },
|
|
53
|
+
{ key: "1", label: "启动 Claude Code", value: "launch_claude", desc: "使用 Claude360 接入配置直接启动" },
|
|
54
|
+
{ key: "2", label: "启动 Codex", value: "launch_codex", desc: "使用 Claude360 接入配置直接启动" },
|
|
54
55
|
],
|
|
55
56
|
},
|
|
56
57
|
{
|
|
57
58
|
title: "Claude360",
|
|
58
59
|
items: [
|
|
59
|
-
{ key: "3", label: "余额与充值", value: "balance_topup" },
|
|
60
|
-
{ key: "4", label: "创建新的 API Key", value: "create_key" },
|
|
61
|
-
{ key: "5", label: "打开 Claude360 控制台", value: "open_console" },
|
|
60
|
+
{ key: "3", label: "余额与充值", value: "balance_topup", desc: "查看余额用量,支持微信扫码充值" },
|
|
61
|
+
{ key: "4", label: "创建新的 API Key", value: "create_key", desc: "在当前账号下新建一个 API Key" },
|
|
62
|
+
{ key: "5", label: "打开 Claude360 控制台", value: "open_console", desc: "在浏览器中打开网页控制台" },
|
|
62
63
|
],
|
|
63
64
|
},
|
|
64
65
|
{
|
|
65
66
|
title: "工具配置",
|
|
66
67
|
items: [
|
|
67
|
-
{ key: "6", label: "Claude Code 配置", value: "claude_code_menu" },
|
|
68
|
-
{ key: "7", label: "Codex 配置", value: "codex_menu" },
|
|
69
|
-
{ key: "8", label: "
|
|
70
|
-
{ key: "9", label: "
|
|
68
|
+
{ key: "6", label: "Claude Code 配置", value: "claude_code_menu", desc: "完整初始化、工作流、MCP、模型与记忆配置" },
|
|
69
|
+
{ key: "7", label: "Codex 配置", value: "codex_menu", desc: "完整初始化、Provider、工作流与 MCP 配置" },
|
|
70
|
+
{ key: "8", label: "一键完整初始化", value: "full_init", desc: "Claude Code / Codex 完整初始化向导" },
|
|
71
|
+
{ key: "9", label: "推荐 MCP / Skill", value: "mcp_skill", desc: "安装推荐的 MCP、工作流与 Skill 增强" },
|
|
72
|
+
{ key: "C", label: "生成 cc-switch 配置", value: "cc_switch", desc: "生成可导入 cc-switch 的供应商配置" },
|
|
71
73
|
],
|
|
72
74
|
},
|
|
73
75
|
{
|
|
74
76
|
title: "维护",
|
|
75
77
|
items: [
|
|
76
|
-
{ key: "
|
|
77
|
-
{ key: "B", label: "安装或更新工具", value: "install_tools" },
|
|
78
|
-
{ key: "0", label: "重新登录", value: "relogin" },
|
|
79
|
-
{ key: "Q", label: "退出", value: "exit" },
|
|
78
|
+
{ key: "D", label: "诊断与修复", value: "diagnostics_menu", desc: "一键诊断环境与配置问题并尝试修复" },
|
|
79
|
+
{ key: "B", label: "安装或更新工具", value: "install_tools", desc: "安装或升级 Claude Code / Codex / 本 CLI" },
|
|
80
|
+
{ key: "0", label: "重新登录", value: "relogin", desc: "清除本地登录态后重新浏览器授权" },
|
|
81
|
+
{ key: "Q", label: "退出", value: "exit", desc: "退出 claude360 CLI" },
|
|
80
82
|
],
|
|
81
83
|
},
|
|
82
84
|
],
|
|
@@ -95,12 +97,13 @@ function renderSectionRule(title, color) {
|
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
function renderItem(item, color) {
|
|
98
|
-
const
|
|
100
|
+
const desc = item.desc ? ` - ${item.desc}` : "";
|
|
99
101
|
if (!color) {
|
|
100
|
-
return
|
|
102
|
+
return ` ${item.key}. ${item.label}${desc}`;
|
|
101
103
|
}
|
|
102
104
|
const keyColor = item.value === "exit" ? EXIT_COLOR : KEY_COLOR;
|
|
103
|
-
|
|
105
|
+
const coloredDesc = desc ? `${DESC_COLOR}${desc}${RESET}` : "";
|
|
106
|
+
return ` ${BOLD}${keyColor}${item.key}.${RESET} ${item.label}${coloredDesc}`;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
export function renderMenu(menu, { color = false } = {}) {
|
|
@@ -154,3 +157,64 @@ export async function promptMenu({ menu, promptInput, writeLine = console.log }
|
|
|
154
157
|
writeLine("选择无效,请重新输入。");
|
|
155
158
|
}
|
|
156
159
|
}
|
|
160
|
+
|
|
161
|
+
// 多选交互(补充需求 6.5 / 6.6 / 8.4):数字切换勾选,A 全选,I 反选,
|
|
162
|
+
// 回车确认。返回选中的 value 数组;用户清空全部选项后回车即视为跳过。
|
|
163
|
+
export function renderMultiSelect({ message, choices, selected }) {
|
|
164
|
+
const lines = [message, ""];
|
|
165
|
+
choices.forEach((choice, index) => {
|
|
166
|
+
const mark = selected.has(choice.value) ? "[x]" : "[ ]";
|
|
167
|
+
const hint = choice.hint ? ` ${choice.hint}` : "";
|
|
168
|
+
lines.push(` ${index + 1}. ${mark} ${choice.label}${hint}`);
|
|
169
|
+
});
|
|
170
|
+
lines.push("", " A. 全选 I. 反选 Enter. 确认");
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function promptMultiSelect({
|
|
175
|
+
message,
|
|
176
|
+
choices = [],
|
|
177
|
+
preselected = [],
|
|
178
|
+
promptInput,
|
|
179
|
+
writeLine = console.log,
|
|
180
|
+
} = {}) {
|
|
181
|
+
if (typeof promptInput !== "function") {
|
|
182
|
+
throw new Error("缺少多选输入");
|
|
183
|
+
}
|
|
184
|
+
const selected = new Set(preselected.filter((value) => choices.some((choice) => choice.value === value)));
|
|
185
|
+
while (true) {
|
|
186
|
+
writeLine(renderMultiSelect({ message, choices, selected }));
|
|
187
|
+
const answer = String(await promptInput("请输入编号(可多个,回车确认)") ?? "").trim();
|
|
188
|
+
if (answer === "") {
|
|
189
|
+
return choices.filter((choice) => selected.has(choice.value)).map((choice) => choice.value);
|
|
190
|
+
}
|
|
191
|
+
if (answer.toLowerCase() === "a") {
|
|
192
|
+
choices.forEach((choice) => selected.add(choice.value));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (answer.toLowerCase() === "i") {
|
|
196
|
+
choices.forEach((choice) => {
|
|
197
|
+
if (selected.has(choice.value)) {
|
|
198
|
+
selected.delete(choice.value);
|
|
199
|
+
} else {
|
|
200
|
+
selected.add(choice.value);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const tokens = answer.split(/[\s,,]+/).filter(Boolean);
|
|
206
|
+
const indexes = tokens.map((token) => Number(token) - 1);
|
|
207
|
+
if (indexes.some((index) => !Number.isInteger(index) || !choices[index])) {
|
|
208
|
+
writeLine("输入无效,请输入编号、A、I 或直接回车确认。");
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
for (const index of indexes) {
|
|
212
|
+
const value = choices[index].value;
|
|
213
|
+
if (selected.has(value)) {
|
|
214
|
+
selected.delete(value);
|
|
215
|
+
} else {
|
|
216
|
+
selected.add(value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/token-manager.js
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
import { loadGroups, selectGroup } from "./group-manager.js";
|
|
2
2
|
import { maskSecret } from "./config-store.js";
|
|
3
|
+
import { renderTable } from "./ui.js";
|
|
4
|
+
|
|
5
|
+
// Key 列表表格(补充需求第 3 节):多 Key 对比展示,长字段由表格渲染层截断
|
|
6
|
+
export function renderTokenTable(tokens, { width = 0 } = {}) {
|
|
7
|
+
return renderTable({
|
|
8
|
+
head: ["序号", "名称", "Key", "分组", "状态", "额度", "过期时间"],
|
|
9
|
+
rows: tokens.map((token, index) => [
|
|
10
|
+
String(index + 1),
|
|
11
|
+
token.name || `Token #${token.id}`,
|
|
12
|
+
formatTokenKey(token.masked_key || token.key),
|
|
13
|
+
token.group || "-",
|
|
14
|
+
formatStatusLabel(token),
|
|
15
|
+
token.unlimited_quota ? "不限" : String(token.remain_quota ?? 0),
|
|
16
|
+
formatExpiryLabel(token.expired_time),
|
|
17
|
+
]),
|
|
18
|
+
}, { width });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatStatusLabel(token) {
|
|
22
|
+
if (token.status !== 1) {
|
|
23
|
+
return "禁用";
|
|
24
|
+
}
|
|
25
|
+
const expired = Number(token.expired_time);
|
|
26
|
+
if (expired > 0 && expired * 1000 < Date.now()) {
|
|
27
|
+
return "过期";
|
|
28
|
+
}
|
|
29
|
+
return "正常";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatExpiryLabel(expiredTime) {
|
|
33
|
+
if (expiredTime === -1 || expiredTime === 0 || expiredTime === undefined || expiredTime === null) {
|
|
34
|
+
return "永不";
|
|
35
|
+
}
|
|
36
|
+
const date = new Date(Number(expiredTime) * 1000);
|
|
37
|
+
if (Number.isNaN(date.getTime())) {
|
|
38
|
+
return String(expiredTime);
|
|
39
|
+
}
|
|
40
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
41
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
42
|
+
}
|
|
3
43
|
|
|
4
44
|
export async function loadBalance(api) {
|
|
5
45
|
return api.get("/api/cli/me");
|
|
@@ -62,6 +102,7 @@ export async function chooseOrCreateToken({
|
|
|
62
102
|
api,
|
|
63
103
|
promptSelect,
|
|
64
104
|
promptInput = async (_message, defaultValue = "Claude360 CLI") => defaultValue,
|
|
105
|
+
writeLine,
|
|
65
106
|
} = {}) {
|
|
66
107
|
if (!api) {
|
|
67
108
|
throw new Error("缺少 API client");
|
|
@@ -73,9 +114,16 @@ export async function chooseOrCreateToken({
|
|
|
73
114
|
if (typeof promptSelect !== "function") {
|
|
74
115
|
throw new Error("缺少 Token 选择输入");
|
|
75
116
|
}
|
|
117
|
+
// 提供 writeLine 时先用表格展示 Key 详情,选项标签保持简短
|
|
118
|
+
const useTable = typeof writeLine === "function";
|
|
119
|
+
if (useTable) {
|
|
120
|
+
writeLine(renderTokenTable(tokens, { width: process.stdout.columns || 0 }));
|
|
121
|
+
}
|
|
76
122
|
const choices = [
|
|
77
123
|
...tokens.map((token) => ({
|
|
78
|
-
label:
|
|
124
|
+
label: useTable
|
|
125
|
+
? `${token.name || `Token #${token.id}`}(${formatTokenKey(token.masked_key || token.key)})`
|
|
126
|
+
: formatTokenOption(token),
|
|
79
127
|
value: token.id,
|
|
80
128
|
})),
|
|
81
129
|
{ label: "创建新的 API Key", value: "__create__" },
|
package/src/tool-launcher.js
CHANGED
|
@@ -206,7 +206,7 @@ export function buildCodexConfig(content = "", {
|
|
|
206
206
|
return `${nextContent.trimEnd()}\n`;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
function upsertTomlTable(content, tableName, tableLines) {
|
|
209
|
+
export function upsertTomlTable(content, tableName, tableLines) {
|
|
210
210
|
const block = tableLines.join("\n");
|
|
211
211
|
const header = `[${tableName}]`;
|
|
212
212
|
|
|
@@ -233,12 +233,19 @@ function upsertTomlTable(content, tableName, tableLines) {
|
|
|
233
233
|
}
|
|
234
234
|
|
|
235
235
|
function upsertProfileModelProvider(content, providerId) {
|
|
236
|
+
return upsertProfileKey(content, providerId, "model_provider", providerId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 在 [profiles.<profileId>] 表内行级 upsert 单个字符串键,不影响表内其他键;
|
|
240
|
+
// 表不存在时在文件末尾追加。
|
|
241
|
+
export function upsertProfileKey(content, profileId, key, value) {
|
|
236
242
|
const lines = content.split(/\r?\n/);
|
|
237
|
-
const header = `[profiles.${
|
|
243
|
+
const header = `[profiles.${profileId}]`;
|
|
244
|
+
const keyLine = `${key} = "${escapeTomlString(value)}"`;
|
|
238
245
|
const start = lines.findIndex((line) => line.trim() === header);
|
|
239
246
|
if (start === -1) {
|
|
240
247
|
const trimmed = content.trimEnd();
|
|
241
|
-
return `${trimmed}${trimmed ? "\n\n" : ""}${header}\
|
|
248
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${header}\n${keyLine}\n`;
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
let end = lines.length;
|
|
@@ -250,12 +257,12 @@ function upsertProfileModelProvider(content, providerId) {
|
|
|
250
257
|
}
|
|
251
258
|
|
|
252
259
|
const profileLines = lines.slice(start, end);
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
if (
|
|
256
|
-
profileLines.splice(1, 0,
|
|
260
|
+
const keyPattern = new RegExp(`^\\s*${key}\\s*=`);
|
|
261
|
+
const keyIndex = profileLines.findIndex((line) => keyPattern.test(line));
|
|
262
|
+
if (keyIndex === -1) {
|
|
263
|
+
profileLines.splice(1, 0, keyLine);
|
|
257
264
|
} else {
|
|
258
|
-
profileLines[
|
|
265
|
+
profileLines[keyIndex] = keyLine;
|
|
259
266
|
}
|
|
260
267
|
|
|
261
268
|
return [
|