claude360 0.1.1 → 0.2.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 +4 -4
- package/bin/claude360.js +4 -3
- package/install/install.ps1 +61 -15
- package/install/install.sh +75 -13
- package/install/verification-matrix.md +2 -2
- package/package.json +3 -2
- package/src/account-status.js +97 -0
- package/src/api-client.js +6 -3
- package/src/auth.js +5 -3
- package/src/banner.js +42 -0
- package/src/cc-switch.js +206 -0
- package/src/diagnostics.js +113 -36
- package/src/group-manager.js +6 -3
- package/src/index.js +802 -75
- package/src/mcp-skill.js +319 -0
- package/src/menu.js +108 -61
- package/src/sanitize.js +70 -0
- package/src/token-manager.js +46 -22
- package/src/tool-installer.js +22 -3
- package/src/tool-launcher.js +82 -21
- package/src/topup.js +8 -1
package/src/mcp-skill.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// 推荐 MCP / Skill / AGENTS:少而精的增强配置,全部可跳过、不阻塞主流程、
|
|
2
|
+
// 不做插件市场。安装前说明影响范围;写公共文件前使用标记区块幂等更新并备份。
|
|
3
|
+
|
|
4
|
+
import { sanitizeText } from "./sanitize.js";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
// ──────────────────────────────────────────────
|
|
11
|
+
// 推荐 MCP(通过官方 `claude mcp add --scope user` 安装,修改 ~/.claude.json)
|
|
12
|
+
// ──────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export const RECOMMENDED_MCPS = [
|
|
15
|
+
{
|
|
16
|
+
id: "context7",
|
|
17
|
+
label: "Context7 官方文档查询,推荐",
|
|
18
|
+
args: ["mcp", "add", "--scope", "user", "context7", "--", "npx", "-y", "@upstash/context7-mcp"],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "playwright",
|
|
22
|
+
label: "Playwright 浏览器自动化,推荐",
|
|
23
|
+
args: ["mcp", "add", "--scope", "user", "playwright", "--", "npx", "-y", "@playwright/mcp@latest"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "deepwiki",
|
|
27
|
+
label: "DeepWiki GitHub 仓库文档查询",
|
|
28
|
+
args: ["mcp", "add", "--scope", "user", "--transport", "http", "deepwiki", "https://mcp.deepwiki.com/mcp"],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "open-websearch",
|
|
32
|
+
label: "Open Web Search 网络搜索",
|
|
33
|
+
args: ["mcp", "add", "--scope", "user", "open-websearch", "--", "npx", "-y", "open-websearch@latest"],
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export async function installRecommendedMcps({
|
|
38
|
+
promptSelect,
|
|
39
|
+
confirm,
|
|
40
|
+
execCommand = defaultExec,
|
|
41
|
+
writeLine = console.log,
|
|
42
|
+
} = {}) {
|
|
43
|
+
if (typeof promptSelect !== "function" || typeof confirm !== "function") {
|
|
44
|
+
throw new Error("缺少交互输入");
|
|
45
|
+
}
|
|
46
|
+
const installed = [];
|
|
47
|
+
while (true) {
|
|
48
|
+
const choices = [
|
|
49
|
+
...RECOMMENDED_MCPS.map((mcp) => ({
|
|
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);
|
|
72
|
+
if (result.ok) {
|
|
73
|
+
installed.push(mcp.id);
|
|
74
|
+
writeLine(`✓ 已安装 MCP:${mcp.id}`);
|
|
75
|
+
} else {
|
|
76
|
+
writeLine(`× 安装 ${mcp.id} 失败:${sanitizeText(result.stderr || result.error || "未知错误")}\n安装失败不影响 Claude360 API 调用,可稍后重试。`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return installed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ──────────────────────────────────────────────
|
|
83
|
+
// 标记区块:幂等写入公共 Markdown 文件
|
|
84
|
+
// ──────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export function upsertMarkedBlock(content, id, block) {
|
|
87
|
+
const start = `<!-- claude360:${id}:start -->`;
|
|
88
|
+
const end = `<!-- claude360:${id}:end -->`;
|
|
89
|
+
const wrapped = `${start}\n${block.trim()}\n${end}`;
|
|
90
|
+
const base = String(content || "");
|
|
91
|
+
const startIndex = base.indexOf(start);
|
|
92
|
+
const endIndex = base.indexOf(end);
|
|
93
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
94
|
+
return base.slice(0, startIndex) + wrapped + base.slice(endIndex + end.length);
|
|
95
|
+
}
|
|
96
|
+
const trimmed = base.replace(/\s+$/, "");
|
|
97
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${wrapped}\n`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ──────────────────────────────────────────────
|
|
101
|
+
// Claude Code 推荐工作流 / Skill(写入 ~/.claude/commands/claude360/*.md
|
|
102
|
+
// 与 ~/.claude/CLAUDE.md 标记区块)
|
|
103
|
+
// ──────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export const RECOMMENDED_SKILLS = [
|
|
106
|
+
{
|
|
107
|
+
id: "assistant-rules",
|
|
108
|
+
label: "Claude360 编程助手规则",
|
|
109
|
+
kind: "memory",
|
|
110
|
+
content: [
|
|
111
|
+
"# Claude360 编程助手规则",
|
|
112
|
+
"",
|
|
113
|
+
"- 回答与代码注释使用简体中文。",
|
|
114
|
+
"- 修改代码前先阅读相邻实现,保持现有代码风格。",
|
|
115
|
+
"- 不主动输出 API Key、Token 等敏感信息。",
|
|
116
|
+
].join("\n"),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "code-review",
|
|
120
|
+
label: "代码审查工作流",
|
|
121
|
+
kind: "command",
|
|
122
|
+
file: "code-review.md",
|
|
123
|
+
content: [
|
|
124
|
+
"---",
|
|
125
|
+
"description: 审查当前改动并输出中文审查报告",
|
|
126
|
+
"---",
|
|
127
|
+
"",
|
|
128
|
+
"请审查当前工作区的代码改动(git diff),从正确性、安全性、性能、可维护性四个维度输出中文审查意见,按严重程度排序,并给出修改建议。",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "git-commit",
|
|
133
|
+
label: "Git 提交信息生成",
|
|
134
|
+
kind: "command",
|
|
135
|
+
file: "git-commit.md",
|
|
136
|
+
content: [
|
|
137
|
+
"---",
|
|
138
|
+
"description: 基于暂存区改动生成规范的中文提交信息",
|
|
139
|
+
"---",
|
|
140
|
+
"",
|
|
141
|
+
"请阅读 git 暂存区改动(git diff --cached),生成一条符合 Conventional Commits 规范的提交信息:type(scope): 中文描述,并附简短 body 说明动机。只输出提交信息本身。",
|
|
142
|
+
].join("\n"),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "prd-analysis",
|
|
146
|
+
label: "PRD / 产品分析工作流",
|
|
147
|
+
kind: "command",
|
|
148
|
+
file: "prd-analysis.md",
|
|
149
|
+
content: [
|
|
150
|
+
"---",
|
|
151
|
+
"description: 分析 PRD 并产出开发任务拆解",
|
|
152
|
+
"---",
|
|
153
|
+
"",
|
|
154
|
+
"请阅读用户提供的 PRD 文档:$ARGUMENTS。输出:1) 需求要点摘要;2) 功能边界(做什么/不做什么);3) 模块级开发任务拆解与依赖顺序;4) 风险与待确认问题清单。全部使用中文。",
|
|
155
|
+
].join("\n"),
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
export function resolveClaudeUserDir({ homedir = os.homedir } = {}) {
|
|
160
|
+
return path.join(homedir(), ".claude");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function installRecommendedSkills({
|
|
164
|
+
promptSelect,
|
|
165
|
+
confirm,
|
|
166
|
+
writeLine = console.log,
|
|
167
|
+
claudeDir = resolveClaudeUserDir(),
|
|
168
|
+
fs = { readFile, writeFile, mkdir, copyFile },
|
|
169
|
+
} = {}) {
|
|
170
|
+
if (typeof promptSelect !== "function" || typeof confirm !== "function") {
|
|
171
|
+
throw new Error("缺少交互输入");
|
|
172
|
+
}
|
|
173
|
+
const installed = [];
|
|
174
|
+
while (true) {
|
|
175
|
+
const choices = [
|
|
176
|
+
...RECOMMENDED_SKILLS.map((skill) => ({
|
|
177
|
+
label: installed.includes(skill.id) ? `${skill.label}(本次已安装)` : skill.label,
|
|
178
|
+
value: skill.id,
|
|
179
|
+
})),
|
|
180
|
+
{ label: "完成 / 跳过", value: "done" },
|
|
181
|
+
];
|
|
182
|
+
const selected = await promptSelect("推荐增强配置(工作流 / Skill)", choices);
|
|
183
|
+
if (selected === "done") {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
const skill = RECOMMENDED_SKILLS.find((item) => item.id === selected);
|
|
187
|
+
if (!skill) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (skill.kind === "memory") {
|
|
192
|
+
const memoryPath = path.join(claudeDir, "CLAUDE.md");
|
|
193
|
+
const approved = await confirm(
|
|
194
|
+
`即将更新 ${memoryPath}(以标记区块追加/替换“${skill.label}”,不影响文件其他内容,写入前备份)。\n是否继续?`,
|
|
195
|
+
);
|
|
196
|
+
if (!approved) {
|
|
197
|
+
writeLine(`已跳过 ${skill.label}。`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const current = await readFileIfExists(fs.readFile, memoryPath);
|
|
201
|
+
await fs.mkdir(claudeDir, { recursive: true });
|
|
202
|
+
if (current !== "") {
|
|
203
|
+
await fs.copyFile(memoryPath, `${memoryPath}.claude360.bak`);
|
|
204
|
+
}
|
|
205
|
+
await fs.writeFile(memoryPath, upsertMarkedBlock(current, skill.id, skill.content), "utf8");
|
|
206
|
+
installed.push(skill.id);
|
|
207
|
+
writeLine(`✓ 已安装:${skill.label}`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const commandsDir = path.join(claudeDir, "commands", "claude360");
|
|
212
|
+
const filePath = path.join(commandsDir, skill.file);
|
|
213
|
+
const approved = await confirm(
|
|
214
|
+
`即将写入 ${filePath}(Claude Code 自定义命令,仅新增/覆盖 claude360 命名空间内文件)。\n是否继续?`,
|
|
215
|
+
);
|
|
216
|
+
if (!approved) {
|
|
217
|
+
writeLine(`已跳过 ${skill.label}。`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
await fs.mkdir(commandsDir, { recursive: true });
|
|
221
|
+
await fs.writeFile(filePath, `${skill.content}\n`, "utf8");
|
|
222
|
+
installed.push(skill.id);
|
|
223
|
+
writeLine(`✓ 已安装:${skill.label}(使用 /claude360:${skill.file.replace(/\.md$/, "")} 调用)`);
|
|
224
|
+
}
|
|
225
|
+
return installed;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ──────────────────────────────────────────────
|
|
229
|
+
// Codex 推荐 AGENTS(写入 ~/.codex/AGENTS.md 标记区块,写前备份)
|
|
230
|
+
// ──────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
export const CODEX_AGENTS_BLOCK_ID = "codex-agents";
|
|
233
|
+
export const CODEX_AGENTS_CONTENT = [
|
|
234
|
+
"# Claude360 Codex 协作规范",
|
|
235
|
+
"",
|
|
236
|
+
"- 回答与代码注释使用简体中文。",
|
|
237
|
+
"- 改动前先理解现有代码结构,保持最小改动面。",
|
|
238
|
+
"- 不输出 API Key、Token 等敏感信息。",
|
|
239
|
+
"- 提交信息遵循 Conventional Commits。",
|
|
240
|
+
].join("\n");
|
|
241
|
+
|
|
242
|
+
export function resolveCodexUserDir({ homedir = os.homedir } = {}) {
|
|
243
|
+
return path.join(homedir(), ".codex");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function installCodexAgents({
|
|
247
|
+
confirm,
|
|
248
|
+
writeLine = console.log,
|
|
249
|
+
codexDir = resolveCodexUserDir(),
|
|
250
|
+
fs = { readFile, writeFile, mkdir, copyFile },
|
|
251
|
+
} = {}) {
|
|
252
|
+
if (typeof confirm !== "function") {
|
|
253
|
+
throw new Error("缺少确认输入");
|
|
254
|
+
}
|
|
255
|
+
const agentsPath = path.join(codexDir, "AGENTS.md");
|
|
256
|
+
const approved = await confirm(
|
|
257
|
+
`即将更新 ${agentsPath}(以标记区块追加/替换 Claude360 协作规范,不影响文件其他内容,写入前备份)。\n是否继续?`,
|
|
258
|
+
);
|
|
259
|
+
if (!approved) {
|
|
260
|
+
writeLine("已跳过 Codex AGENTS 配置。");
|
|
261
|
+
return { installed: false };
|
|
262
|
+
}
|
|
263
|
+
const current = await readFileIfExists(fs.readFile, agentsPath);
|
|
264
|
+
await fs.mkdir(codexDir, { recursive: true });
|
|
265
|
+
if (current !== "") {
|
|
266
|
+
await fs.copyFile(agentsPath, `${agentsPath}.claude360.bak`);
|
|
267
|
+
}
|
|
268
|
+
await fs.writeFile(agentsPath, upsertMarkedBlock(current, CODEX_AGENTS_BLOCK_ID, CODEX_AGENTS_CONTENT), "utf8");
|
|
269
|
+
writeLine(`✓ 已更新:${agentsPath}`);
|
|
270
|
+
return { installed: true, path: agentsPath };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ──────────────────────────────────────────────
|
|
274
|
+
// 查看已安装增强配置
|
|
275
|
+
// ──────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
export async function listInstalledEnhancements({
|
|
278
|
+
claudeDir = resolveClaudeUserDir(),
|
|
279
|
+
codexDir = resolveCodexUserDir(),
|
|
280
|
+
fs = { readFile },
|
|
281
|
+
} = {}) {
|
|
282
|
+
const lines = ["已安装增强配置:"];
|
|
283
|
+
const memory = await readFileIfExists(fs.readFile, path.join(claudeDir, "CLAUDE.md"));
|
|
284
|
+
lines.push(`- Claude360 编程助手规则:${memory.includes("claude360:assistant-rules:start") ? "已安装" : "未安装"}`);
|
|
285
|
+
for (const skill of RECOMMENDED_SKILLS.filter((item) => item.kind === "command")) {
|
|
286
|
+
const content = await readFileIfExists(fs.readFile, path.join(claudeDir, "commands", "claude360", skill.file));
|
|
287
|
+
lines.push(`- ${skill.label}:${content ? "已安装" : "未安装"}`);
|
|
288
|
+
}
|
|
289
|
+
const agents = await readFileIfExists(fs.readFile, path.join(codexDir, "AGENTS.md"));
|
|
290
|
+
lines.push(`- Codex AGENTS 规范:${agents.includes(`claude360:${CODEX_AGENTS_BLOCK_ID}:start`) ? "已安装" : "未安装"}`);
|
|
291
|
+
return lines.join("\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function readFileIfExists(readFileImpl, filePath) {
|
|
295
|
+
try {
|
|
296
|
+
return await readFileImpl(filePath, "utf8");
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (error?.code === "ENOENT") {
|
|
299
|
+
return "";
|
|
300
|
+
}
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function defaultExec(command, args) {
|
|
306
|
+
return new Promise((resolve) => {
|
|
307
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
308
|
+
let stdout = "";
|
|
309
|
+
let stderr = "";
|
|
310
|
+
child.stdout.on("data", (chunk) => {
|
|
311
|
+
stdout += chunk;
|
|
312
|
+
});
|
|
313
|
+
child.stderr.on("data", (chunk) => {
|
|
314
|
+
stderr += chunk;
|
|
315
|
+
});
|
|
316
|
+
child.on("error", (error) => resolve({ ok: false, stdout, stderr, error: error.message }));
|
|
317
|
+
child.on("close", (code) => resolve({ ok: code === 0, stdout, stderr }));
|
|
318
|
+
});
|
|
319
|
+
}
|
package/src/menu.js
CHANGED
|
@@ -1,72 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
// 菜单:区分首次使用菜单与日常使用菜单(PRD 第 10、11 章)。
|
|
2
|
+
// 菜单自带分区标题与选择键(1-9 / A / 0 / Q),由 renderMenu 渲染、
|
|
3
|
+
// resolveMenuSelection 解析,promptMenu 循环读取直到输入有效。
|
|
4
|
+
|
|
5
|
+
export function buildFirstRunMenu() {
|
|
6
|
+
return {
|
|
7
|
+
title: [
|
|
8
|
+
"Claude360 Setup",
|
|
9
|
+
"",
|
|
10
|
+
"检测到你是首次使用 Claude360 CLI。",
|
|
11
|
+
"请选择要完成的配置:",
|
|
12
|
+
].join("\n"),
|
|
13
|
+
sections: [
|
|
14
|
+
{
|
|
15
|
+
title: null,
|
|
16
|
+
items: [
|
|
17
|
+
{ key: "1", label: "一键配置 Claude Code", value: "setup_claude" },
|
|
18
|
+
{ key: "2", label: "一键配置 Codex", value: "setup_codex" },
|
|
19
|
+
{ key: "3", label: "同时配置 Claude Code + Codex", value: "setup_both" },
|
|
20
|
+
{ key: "4", label: "仅登录 Claude360 并创建 API Key", value: "login_only" },
|
|
21
|
+
{ key: "5", label: "生成 cc-switch 配置", value: "cc_switch" },
|
|
22
|
+
{ key: "6", label: "打开 Claude360 注册 / 登录页面", value: "open_register" },
|
|
23
|
+
{ key: "Q", label: "退出", value: "exit" },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
12
28
|
}
|
|
13
29
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
export function buildDailyMenu() {
|
|
31
|
+
return {
|
|
32
|
+
title: "请选择功能:",
|
|
33
|
+
sections: [
|
|
34
|
+
{
|
|
35
|
+
title: "--- 快速启动 ---",
|
|
36
|
+
items: [
|
|
37
|
+
{ key: "1", label: "启动 Claude Code", value: "launch_claude" },
|
|
38
|
+
{ key: "2", label: "启动 Codex", value: "launch_codex" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: "--- Claude360 ---",
|
|
43
|
+
items: [
|
|
44
|
+
{ key: "3", label: "余额与充值", value: "balance_topup" },
|
|
45
|
+
{ key: "4", label: "创建新的 API Key", value: "create_key" },
|
|
46
|
+
{ key: "5", label: "打开 Claude360 控制台", value: "open_console" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: "--- 工具配置 ---",
|
|
51
|
+
items: [
|
|
52
|
+
{ key: "6", label: "Claude Code 配置", value: "claude_code_menu" },
|
|
53
|
+
{ key: "7", label: "Codex 配置", value: "codex_menu" },
|
|
54
|
+
{ key: "8", label: "推荐 MCP / Skill", value: "mcp_skill" },
|
|
55
|
+
{ key: "9", label: "生成 cc-switch 配置", value: "cc_switch" },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: "--- 维护 ---",
|
|
60
|
+
items: [
|
|
61
|
+
{ key: "A", label: "诊断与修复", value: "diagnostics_menu" },
|
|
62
|
+
{ key: "B", label: "安装或更新工具", value: "install_tools" },
|
|
63
|
+
{ key: "0", label: "重新登录", value: "relogin" },
|
|
64
|
+
{ key: "Q", label: "退出", value: "exit" },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
23
70
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
writeLine(formatBalance(balance, config));
|
|
29
|
-
break;
|
|
71
|
+
export function renderMenu(menu) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
if (menu.title) {
|
|
74
|
+
lines.push(menu.title, "");
|
|
30
75
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
break;
|
|
40
|
-
case "install_tools":
|
|
41
|
-
await requireAction(actions, "installOrUpdateTools")();
|
|
42
|
-
break;
|
|
43
|
-
case "switch_token":
|
|
44
|
-
await requireAction(actions, "switchToken")();
|
|
45
|
-
break;
|
|
46
|
-
case "diagnostics":
|
|
47
|
-
await requireAction(actions, "runDiagnostics")();
|
|
48
|
-
break;
|
|
49
|
-
case "exit":
|
|
50
|
-
break;
|
|
51
|
-
default:
|
|
52
|
-
throw new Error("未知菜单选项");
|
|
76
|
+
for (const section of menu.sections) {
|
|
77
|
+
if (section.title) {
|
|
78
|
+
lines.push(section.title);
|
|
79
|
+
}
|
|
80
|
+
for (const item of section.items) {
|
|
81
|
+
lines.push(`${item.key}. ${item.label}`);
|
|
82
|
+
}
|
|
83
|
+
lines.push("");
|
|
53
84
|
}
|
|
54
|
-
return
|
|
85
|
+
return lines.join("\n").replace(/\n+$/, "\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function listMenuItems(menu) {
|
|
89
|
+
return menu.sections.flatMap((section) => section.items);
|
|
55
90
|
}
|
|
56
91
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
92
|
+
export function resolveMenuSelection(menu, input) {
|
|
93
|
+
const text = String(input ?? "").trim();
|
|
94
|
+
if (text === "") {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const items = listMenuItems(menu);
|
|
98
|
+
const byKey = items.find((item) => item.key.toLowerCase() === text.toLowerCase());
|
|
99
|
+
if (byKey) {
|
|
100
|
+
return byKey.value;
|
|
60
101
|
}
|
|
61
|
-
|
|
102
|
+
const byValue = items.find((item) => item.value === text);
|
|
103
|
+
return byValue ? byValue.value : null;
|
|
62
104
|
}
|
|
63
105
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
106
|
+
export async function promptMenu({ menu, promptInput, writeLine = console.log } = {}) {
|
|
107
|
+
if (typeof promptInput !== "function") {
|
|
108
|
+
throw new Error("缺少菜单输入");
|
|
109
|
+
}
|
|
110
|
+
while (true) {
|
|
111
|
+
writeLine(renderMenu(menu));
|
|
112
|
+
const answer = await promptInput("请输入选项");
|
|
113
|
+
const value = resolveMenuSelection(menu, answer);
|
|
114
|
+
if (value !== null) {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
writeLine("选择无效,请重新输入。");
|
|
118
|
+
}
|
|
72
119
|
}
|
package/src/sanitize.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// 统一敏感信息脱敏模块(安全要求 #4 / AC-10)。
|
|
2
|
+
// 所有进入终端的错误文本、HTTP 响应正文、命令 stderr 都必须先经过这里,
|
|
3
|
+
// 即使来源对象本身已用 maskSecret() 处理过——错误文本可能携带完整凭证。
|
|
4
|
+
|
|
5
|
+
function maskToken(value) {
|
|
6
|
+
if (value.length <= 8) {
|
|
7
|
+
return "***";
|
|
8
|
+
}
|
|
9
|
+
return `${value.slice(0, 4)}***${value.slice(-4)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const RULES = [
|
|
13
|
+
// sk- 开头的 API Key
|
|
14
|
+
{
|
|
15
|
+
pattern: /\bsk-[A-Za-z0-9_-]{6,}/g,
|
|
16
|
+
replace: (match) => maskToken(match),
|
|
17
|
+
},
|
|
18
|
+
// cli- / cli_ 开头的 CLI Token
|
|
19
|
+
{
|
|
20
|
+
pattern: /\bcli[-_][A-Za-z0-9_-]{6,}/g,
|
|
21
|
+
replace: (match) => maskToken(match),
|
|
22
|
+
},
|
|
23
|
+
// Bearer <token>:保留 scheme,脱敏凭证
|
|
24
|
+
{
|
|
25
|
+
pattern: /\b(Bearer\s+)([A-Za-z0-9._=+/-]{8,})/g,
|
|
26
|
+
replace: (_match, scheme, token) => `${scheme}${maskToken(token)}`,
|
|
27
|
+
},
|
|
28
|
+
// JSON 字段值:"key" / "apiKey" / "api_key" / "cliToken" / "cli_token" / "Authorization"
|
|
29
|
+
{
|
|
30
|
+
pattern: /("(?:key|apiKey|api_key|cliToken|cli_token|Authorization)"\s*:\s*")([^"]{6,})(")/g,
|
|
31
|
+
replace: (_match, prefix, value, suffix) => `${prefix}${maskToken(value)}${suffix}`,
|
|
32
|
+
},
|
|
33
|
+
// 微信支付链接:路径与参数全部隐藏
|
|
34
|
+
{
|
|
35
|
+
pattern: /weixin:\/\/wxpay\/\S+/g,
|
|
36
|
+
replace: () => "weixin://wxpay/***",
|
|
37
|
+
},
|
|
38
|
+
// 微信充值订单号
|
|
39
|
+
{
|
|
40
|
+
pattern: /\bWX-[A-Za-z0-9-]{4,}/g,
|
|
41
|
+
replace: (match) => maskToken(match),
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function sanitizeText(value) {
|
|
46
|
+
if (value == null) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
let text = typeof value === "string" ? value : String(value);
|
|
50
|
+
for (const rule of RULES) {
|
|
51
|
+
text = text.replace(rule.pattern, rule.replace);
|
|
52
|
+
}
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function sanitizeError(error) {
|
|
57
|
+
if (error == null) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
if (typeof error === "string") {
|
|
61
|
+
return sanitizeText(error);
|
|
62
|
+
}
|
|
63
|
+
// Error 实例 message 为空时返回空串,让 safeErrorMessage 走 fallback
|
|
64
|
+
const message = error instanceof Error ? error.message : error?.message || String(error);
|
|
65
|
+
return sanitizeText(message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function safeErrorMessage(error, fallback = "操作失败") {
|
|
69
|
+
return sanitizeError(error) || fallback;
|
|
70
|
+
}
|
package/src/token-manager.js
CHANGED
|
@@ -24,10 +24,44 @@ function formatTokenKey(key) {
|
|
|
24
24
|
return key.includes("*") ? key : maskSecret(key);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export const TOKEN_PURPOSES = [
|
|
28
|
+
{ label: "Claude Code 默认 Key", value: "claude-code-default" },
|
|
29
|
+
{ label: "Codex 默认 Key", value: "codex-default" },
|
|
30
|
+
{ label: "通用 Key", value: "claude360-cli" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// 创建新 API Key:选择用途(决定默认名)→ 选择分组(来自后端)→ 创建并返回明文
|
|
34
|
+
export async function createNewToken({ api, promptSelect, promptInput = async (_m, d) => d } = {}) {
|
|
35
|
+
if (!api) {
|
|
36
|
+
throw new Error("缺少 API client");
|
|
37
|
+
}
|
|
38
|
+
if (typeof promptSelect !== "function") {
|
|
39
|
+
throw new Error("缺少选择输入");
|
|
40
|
+
}
|
|
41
|
+
const purpose = await promptSelect("请选择 Key 用途:", TOKEN_PURPOSES);
|
|
42
|
+
const groups = await loadGroups(api);
|
|
43
|
+
const group = await selectGroup({ groups, promptSelect });
|
|
44
|
+
const name = await promptInput("新 API Key 名称", purpose);
|
|
45
|
+
const created = await api.post("/api/cli/tokens", {
|
|
46
|
+
name,
|
|
47
|
+
group: group.name,
|
|
48
|
+
});
|
|
49
|
+
if (!created?.key) {
|
|
50
|
+
throw new Error("创建 API Key 失败");
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
tokenId: created.id,
|
|
54
|
+
tokenName: created.name || name,
|
|
55
|
+
apiKey: created.key,
|
|
56
|
+
group: created.group || group.name,
|
|
57
|
+
created: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
27
61
|
export async function chooseOrCreateToken({
|
|
28
62
|
api,
|
|
29
63
|
promptSelect,
|
|
30
|
-
promptInput = async (
|
|
64
|
+
promptInput = async (_message, defaultValue = "Claude360 CLI") => defaultValue,
|
|
31
65
|
} = {}) {
|
|
32
66
|
if (!api) {
|
|
33
67
|
throw new Error("缺少 API client");
|
|
@@ -39,11 +73,17 @@ export async function chooseOrCreateToken({
|
|
|
39
73
|
if (typeof promptSelect !== "function") {
|
|
40
74
|
throw new Error("缺少 Token 选择输入");
|
|
41
75
|
}
|
|
42
|
-
const choices =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
76
|
+
const choices = [
|
|
77
|
+
...tokens.map((token) => ({
|
|
78
|
+
label: formatTokenOption(token),
|
|
79
|
+
value: token.id,
|
|
80
|
+
})),
|
|
81
|
+
{ label: "创建新的 API Key", value: "__create__" },
|
|
82
|
+
];
|
|
46
83
|
const tokenId = await promptSelect("选择 API Key", choices);
|
|
84
|
+
if (tokenId === "__create__") {
|
|
85
|
+
return createNewToken({ api, promptSelect, promptInput });
|
|
86
|
+
}
|
|
47
87
|
const selected = tokens.find((token) => token.id === tokenId);
|
|
48
88
|
if (!selected) {
|
|
49
89
|
throw new Error("选择的 API Key 无效");
|
|
@@ -61,23 +101,7 @@ export async function chooseOrCreateToken({
|
|
|
61
101
|
};
|
|
62
102
|
}
|
|
63
103
|
|
|
64
|
-
|
|
65
|
-
const group = await selectGroup({ groups, promptSelect });
|
|
66
|
-
const name = await promptInput("新 API Key 名称", "Claude360 CLI");
|
|
67
|
-
const created = await api.post("/api/cli/tokens", {
|
|
68
|
-
name,
|
|
69
|
-
group: group.name,
|
|
70
|
-
});
|
|
71
|
-
if (!created?.key) {
|
|
72
|
-
throw new Error("创建 API Key 失败");
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
tokenId: created.id,
|
|
76
|
-
tokenName: created.name || name,
|
|
77
|
-
apiKey: created.key,
|
|
78
|
-
group: created.group || group.name,
|
|
79
|
-
created: true,
|
|
80
|
-
};
|
|
104
|
+
return createNewToken({ api, promptSelect, promptInput });
|
|
81
105
|
}
|
|
82
106
|
|
|
83
107
|
function formatStatus(status) {
|