claude360 0.1.1 → 0.2.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/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 +275 -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 +808 -75
- package/src/mcp-skill.js +319 -0
- package/src/menu.js +144 -60
- 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/banner.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// 品牌横幅:参照 zcf 的顶部边框设计 —— 圆角边框盒子内放渐变色 ASCII logo,
|
|
2
|
+
// TTY 下播放流光动效(渐变相位随帧移动),非 TTY / NO_COLOR 下降级为纯文本。
|
|
3
|
+
// 渲染函数默认无色(color: false),保证测试与管道输出稳定;
|
|
4
|
+
// 运行时由 index.js 通过 playBanner / 显式 color 参数开启彩色。
|
|
5
|
+
|
|
6
|
+
export const BRAND_BASE_URL = "https://claude360.xyz";
|
|
7
|
+
|
|
8
|
+
const ESC = "\u001b[";
|
|
9
|
+
const RESET = `${ESC}0m`;
|
|
10
|
+
const BOLD = `${ESC}1m`;
|
|
11
|
+
const DIM = `${ESC}2m`;
|
|
12
|
+
|
|
13
|
+
const fg = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
|
|
14
|
+
|
|
15
|
+
export function colorEnabled(stream = process.stdout) {
|
|
16
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return Boolean(stream && stream.isTTY);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const LOGO_LINES = [
|
|
26
|
+
" ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗██████╗ ██████╗ ██████╗ ",
|
|
27
|
+
"██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝╚════██╗██╔════╝ ██╔═████╗",
|
|
28
|
+
"██║ ██║ ███████║██║ ██║██║ ██║█████╗ █████╔╝███████╗ ██║██╔██║",
|
|
29
|
+
"██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝ ╚═══██╗██╔═══██╗████╔╝██║",
|
|
30
|
+
"╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗██████╔╝╚██████╔╝╚██████╔╝",
|
|
31
|
+
" ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// 霓虹渐变色板:青 → 蓝 → 靛 → 紫 → 品红(与 zcf 的冷色霓虹风一致)
|
|
35
|
+
const GRADIENT = [
|
|
36
|
+
[0, 229, 255],
|
|
37
|
+
[59, 130, 246],
|
|
38
|
+
[99, 102, 241],
|
|
39
|
+
[168, 85, 247],
|
|
40
|
+
[236, 72, 153],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// 在色板上往返插值(ping-pong),让流光动画首尾循环时颜色连续不跳变
|
|
44
|
+
function gradientAt(t) {
|
|
45
|
+
const segments = GRADIENT.length - 1;
|
|
46
|
+
let x = ((t % 1) + 1) % 1;
|
|
47
|
+
x = x < 0.5 ? x * 2 : (1 - x) * 2;
|
|
48
|
+
const pos = x * segments;
|
|
49
|
+
const i = Math.min(Math.floor(pos), segments - 1);
|
|
50
|
+
const f = pos - i;
|
|
51
|
+
const [r1, g1, b1] = GRADIENT[i];
|
|
52
|
+
const [r2, g2, b2] = GRADIENT[i + 1];
|
|
53
|
+
return [
|
|
54
|
+
Math.round(r1 + (r2 - r1) * f),
|
|
55
|
+
Math.round(g1 + (g2 - g1) * f),
|
|
56
|
+
Math.round(b1 + (b2 - b1) * f),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function charDisplayWidth(codePoint) {
|
|
61
|
+
return (
|
|
62
|
+
(codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
|
63
|
+
(codePoint >= 0x2e80 && codePoint <= 0x303e) ||
|
|
64
|
+
(codePoint >= 0x3041 && codePoint <= 0x33ff) ||
|
|
65
|
+
(codePoint >= 0x3400 && codePoint <= 0x4dbf) ||
|
|
66
|
+
(codePoint >= 0x4e00 && codePoint <= 0x9fff) ||
|
|
67
|
+
(codePoint >= 0xa000 && codePoint <= 0xa4cf) ||
|
|
68
|
+
(codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
|
|
69
|
+
(codePoint >= 0xf900 && codePoint <= 0xfaff) ||
|
|
70
|
+
(codePoint >= 0xfe30 && codePoint <= 0xfe4f) ||
|
|
71
|
+
(codePoint >= 0xff00 && codePoint <= 0xff60) ||
|
|
72
|
+
(codePoint >= 0xffe0 && codePoint <= 0xffe6)
|
|
73
|
+
) ? 2 : 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function displayWidth(text) {
|
|
77
|
+
let width = 0;
|
|
78
|
+
for (const ch of text) {
|
|
79
|
+
width += charDisplayWidth(ch.codePointAt(0));
|
|
80
|
+
}
|
|
81
|
+
return width;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function padCenter(text, width) {
|
|
85
|
+
const gap = Math.max(0, width - displayWidth(text));
|
|
86
|
+
const left = Math.floor(gap / 2);
|
|
87
|
+
return " ".repeat(left) + text + " ".repeat(gap - left);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const BORDER_CHARS = new Set(["─", "│", "╭", "╮", "╰", "╯"]);
|
|
91
|
+
const LOGO_EDGE_CHARS = new Set(["╔", "╗", "╚", "╝", "║", "═"]);
|
|
92
|
+
|
|
93
|
+
// style:
|
|
94
|
+
// gradient —— 整行按列渐变(logo 行、边框行)
|
|
95
|
+
// text:<sgr> —— 边框字符渐变,其余字符用固定 SGR 样式(标语行)
|
|
96
|
+
function paintLine(line, { phase, totalWidth, style }) {
|
|
97
|
+
let out = "";
|
|
98
|
+
let col = 0;
|
|
99
|
+
let lastColor = "";
|
|
100
|
+
for (const ch of line) {
|
|
101
|
+
const width = charDisplayWidth(ch.codePointAt(0));
|
|
102
|
+
if (ch === " ") {
|
|
103
|
+
if (lastColor !== "") {
|
|
104
|
+
out += RESET;
|
|
105
|
+
lastColor = "";
|
|
106
|
+
}
|
|
107
|
+
out += ch;
|
|
108
|
+
col += width;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
let color;
|
|
112
|
+
if (style === "gradient" || BORDER_CHARS.has(ch)) {
|
|
113
|
+
const t = phase + col / Math.max(1, totalWidth);
|
|
114
|
+
let [r, g, b] = gradientAt(t);
|
|
115
|
+
if (LOGO_EDGE_CHARS.has(ch)) {
|
|
116
|
+
// logo 描边字符调暗,让实心块更立体
|
|
117
|
+
r = Math.round(r * 0.55);
|
|
118
|
+
g = Math.round(g * 0.55);
|
|
119
|
+
b = Math.round(b * 0.55);
|
|
120
|
+
}
|
|
121
|
+
color = fg(r, g, b);
|
|
122
|
+
} else {
|
|
123
|
+
color = style;
|
|
124
|
+
}
|
|
125
|
+
if (color !== lastColor) {
|
|
126
|
+
out += RESET + color;
|
|
127
|
+
lastColor = color;
|
|
128
|
+
}
|
|
129
|
+
out += ch;
|
|
130
|
+
col += width;
|
|
131
|
+
}
|
|
132
|
+
if (lastColor !== "") {
|
|
133
|
+
out += RESET;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const TAGLINE = "Claude360 · 模型站接入助手";
|
|
139
|
+
const SUBLINE = "一键配置 Claude Code / Codex / API Key / 充值";
|
|
140
|
+
|
|
141
|
+
function buildBannerLayout({ version, baseUrl }) {
|
|
142
|
+
const pad = 1;
|
|
143
|
+
const innerWidth = Math.max(
|
|
144
|
+
...LOGO_LINES.map((line) => displayWidth(line)),
|
|
145
|
+
displayWidth(TAGLINE),
|
|
146
|
+
displayWidth(SUBLINE),
|
|
147
|
+
) + pad * 2;
|
|
148
|
+
const top = `╭${"─".repeat(innerWidth)}╮`;
|
|
149
|
+
const bottom = `╰${"─".repeat(innerWidth)}╯`;
|
|
150
|
+
const boxed = (text) => `│${padCenter(text, innerWidth)}│`;
|
|
151
|
+
const footer = ` 版本 v${version || "-"} · 官网 ${baseUrl}`;
|
|
152
|
+
return {
|
|
153
|
+
totalWidth: innerWidth + 2,
|
|
154
|
+
rows: [
|
|
155
|
+
{ text: top, style: "gradient" },
|
|
156
|
+
...LOGO_LINES.map((line) => ({ text: boxed(line), style: "gradient" })),
|
|
157
|
+
{ text: boxed(""), style: "gradient" },
|
|
158
|
+
{ text: boxed(TAGLINE), style: "title" },
|
|
159
|
+
{ text: boxed(SUBLINE), style: "sub" },
|
|
160
|
+
{ text: bottom, style: "gradient" },
|
|
161
|
+
{ text: footer, style: "footer" },
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 终端太窄放不下大 logo 时的紧凑版
|
|
167
|
+
function renderCompactBanner({ version, baseUrl, color, phase }) {
|
|
168
|
+
const lines = [
|
|
169
|
+
"Claude360 CLI · 模型站接入助手",
|
|
170
|
+
SUBLINE,
|
|
171
|
+
`版本 v${version || "-"} · 官网 ${baseUrl}`,
|
|
172
|
+
];
|
|
173
|
+
if (!color) {
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
return [
|
|
177
|
+
paintLine(lines[0], { phase, totalWidth: displayWidth(lines[0]), style: "gradient" }),
|
|
178
|
+
`${DIM}${lines[1]}${RESET}`,
|
|
179
|
+
`${DIM}${lines[2]}${RESET}`,
|
|
180
|
+
].join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function renderBanner({
|
|
184
|
+
version = "",
|
|
185
|
+
baseUrl = BRAND_BASE_URL,
|
|
186
|
+
color = false,
|
|
187
|
+
phase = 0,
|
|
188
|
+
columns = 0,
|
|
189
|
+
} = {}) {
|
|
190
|
+
const { totalWidth, rows } = buildBannerLayout({ version, baseUrl });
|
|
191
|
+
if (columns > 0 && columns < totalWidth) {
|
|
192
|
+
return renderCompactBanner({ version, baseUrl, color, phase });
|
|
193
|
+
}
|
|
194
|
+
if (!color) {
|
|
195
|
+
return rows.map((row) => row.text).join("\n");
|
|
196
|
+
}
|
|
197
|
+
const styles = {
|
|
198
|
+
title: `${BOLD}${fg(240, 246, 255)}`,
|
|
199
|
+
sub: `${fg(148, 163, 184)}`,
|
|
200
|
+
};
|
|
201
|
+
return rows.map((row) => {
|
|
202
|
+
if (row.style === "footer") {
|
|
203
|
+
return `${DIM}${row.text}${RESET}`;
|
|
204
|
+
}
|
|
205
|
+
const style = row.style === "gradient" ? "gradient" : styles[row.style];
|
|
206
|
+
return paintLine(row.text, { phase, totalWidth, style });
|
|
207
|
+
}).join("\n");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 流光动效:固定布局重绘若干帧,仅移动渐变相位。
|
|
211
|
+
// 非 TTY / NO_COLOR / 终端过窄时直接输出静态版本。
|
|
212
|
+
export async function playBanner({
|
|
213
|
+
version = "",
|
|
214
|
+
baseUrl = BRAND_BASE_URL,
|
|
215
|
+
stream = process.stdout,
|
|
216
|
+
frames = 24,
|
|
217
|
+
intervalMs = 50,
|
|
218
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
219
|
+
} = {}) {
|
|
220
|
+
const columns = stream && stream.columns ? stream.columns : 0;
|
|
221
|
+
const animated = colorEnabled(stream)
|
|
222
|
+
&& typeof stream.write === "function"
|
|
223
|
+
&& (!columns || columns >= buildBannerLayout({ version, baseUrl }).totalWidth);
|
|
224
|
+
if (!animated) {
|
|
225
|
+
const text = renderBanner({ version, baseUrl, color: colorEnabled(stream), columns });
|
|
226
|
+
if (stream && typeof stream.write === "function") {
|
|
227
|
+
stream.write(text + "\n");
|
|
228
|
+
} else {
|
|
229
|
+
console.log(text);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const lineCount = renderBanner({ version, baseUrl }).split("\n").length;
|
|
234
|
+
stream.write(`${ESC}?25l`);
|
|
235
|
+
try {
|
|
236
|
+
for (let i = 0; i <= frames; i += 1) {
|
|
237
|
+
const phase = i / frames;
|
|
238
|
+
stream.write(renderBanner({ version, baseUrl, color: true, phase }) + "\n");
|
|
239
|
+
if (i < frames) {
|
|
240
|
+
await sleep(intervalMs);
|
|
241
|
+
stream.write(`${ESC}${lineCount}A`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
stream.write(`${ESC}?25h`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const CHECK_OK = "✓";
|
|
250
|
+
export const CHECK_WARN = "!";
|
|
251
|
+
export const CHECK_FAIL = "×";
|
|
252
|
+
|
|
253
|
+
const CHECK_COLORS = {
|
|
254
|
+
ok: [74, 222, 128],
|
|
255
|
+
warn: [250, 204, 21],
|
|
256
|
+
fail: [248, 113, 113],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export function formatCheckLine(status, text, { color = false } = {}) {
|
|
260
|
+
const symbols = { ok: CHECK_OK, warn: CHECK_WARN, fail: CHECK_FAIL };
|
|
261
|
+
const symbol = symbols[status] || CHECK_WARN;
|
|
262
|
+
if (!color) {
|
|
263
|
+
return `${symbol} ${text}`;
|
|
264
|
+
}
|
|
265
|
+
const [r, g, b] = CHECK_COLORS[status] || CHECK_COLORS.warn;
|
|
266
|
+
return `${fg(r, g, b)}${BOLD}${symbol}${RESET} ${text}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function renderEnvironmentChecks(checks = [], { color = false } = {}) {
|
|
270
|
+
return [
|
|
271
|
+
"正在检查运行环境...",
|
|
272
|
+
"",
|
|
273
|
+
...checks.map((check) => formatCheckLine(check.status, check.text, { color })),
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
package/src/cc-switch.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// cc-switch 配置生成:根据当前账号 Key 与 Base URL 生成可复制到 cc-switch
|
|
2
|
+
// 的配置 JSON。本模块不安装、不启动、不写入 cc-switch 自身的配置目录。
|
|
3
|
+
|
|
4
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
function normalizeBaseUrl(baseUrl = "https://claude360.xyz") {
|
|
9
|
+
return String(baseUrl).replace(/\/+$/, "");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildClaudeCodeCcSwitchConfig({ baseUrl, apiKey } = {}) {
|
|
13
|
+
const root = normalizeBaseUrl(baseUrl);
|
|
14
|
+
return {
|
|
15
|
+
name: "Claude360",
|
|
16
|
+
type: "claude-code",
|
|
17
|
+
base_url: root,
|
|
18
|
+
api_key: apiKey,
|
|
19
|
+
env: {
|
|
20
|
+
ANTHROPIC_BASE_URL: root,
|
|
21
|
+
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildCodexCcSwitchConfig({ baseUrl, apiKey } = {}) {
|
|
27
|
+
const root = normalizeBaseUrl(baseUrl);
|
|
28
|
+
return {
|
|
29
|
+
name: "Claude360",
|
|
30
|
+
type: "codex",
|
|
31
|
+
base_url: `${root}/v1`,
|
|
32
|
+
api_key: apiKey,
|
|
33
|
+
env: {
|
|
34
|
+
CLAUDE360_API_KEY: apiKey,
|
|
35
|
+
},
|
|
36
|
+
provider: {
|
|
37
|
+
name: "Claude360",
|
|
38
|
+
base_url: `${root}/v1`,
|
|
39
|
+
env_key: "CLAUDE360_API_KEY",
|
|
40
|
+
wire_api: "responses",
|
|
41
|
+
},
|
|
42
|
+
profile: {
|
|
43
|
+
model_provider: "claude360",
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildComboCcSwitchConfig({ baseUrl, apiKey } = {}) {
|
|
49
|
+
const claude = buildClaudeCodeCcSwitchConfig({ baseUrl, apiKey });
|
|
50
|
+
const codex = buildCodexCcSwitchConfig({ baseUrl, apiKey });
|
|
51
|
+
return {
|
|
52
|
+
name: "Claude360",
|
|
53
|
+
items: [
|
|
54
|
+
{ ...claude, name: "Claude360 Claude Code" },
|
|
55
|
+
{ ...codex, name: "Claude360 Codex" },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildCcSwitchConfig(target, { baseUrl, apiKey } = {}) {
|
|
61
|
+
switch (target) {
|
|
62
|
+
case "claude-code":
|
|
63
|
+
return buildClaudeCodeCcSwitchConfig({ baseUrl, apiKey });
|
|
64
|
+
case "codex":
|
|
65
|
+
return buildCodexCcSwitchConfig({ baseUrl, apiKey });
|
|
66
|
+
case "both":
|
|
67
|
+
return buildComboCcSwitchConfig({ baseUrl, apiKey });
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(`未知 cc-switch 配置目标:${target}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 脱敏预览:sk-****abcd(仅用于预览,不可用于实际配置)
|
|
74
|
+
export function maskKeyForPreview(key) {
|
|
75
|
+
if (!key) {
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
const text = String(key);
|
|
79
|
+
const last4 = text.slice(-4);
|
|
80
|
+
return text.startsWith("sk-") ? `sk-****${last4}` : `****${last4}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function maskCcSwitchConfig(config, apiKey) {
|
|
84
|
+
const masked = maskKeyForPreview(apiKey);
|
|
85
|
+
const json = JSON.stringify(config);
|
|
86
|
+
if (!apiKey) {
|
|
87
|
+
return JSON.parse(json);
|
|
88
|
+
}
|
|
89
|
+
return JSON.parse(json.split(JSON.stringify(apiKey)).join(JSON.stringify(masked)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveCcSwitchSavePath({
|
|
93
|
+
platform = process.platform,
|
|
94
|
+
env = process.env,
|
|
95
|
+
homedir = os.homedir,
|
|
96
|
+
} = {}) {
|
|
97
|
+
if (platform === "win32") {
|
|
98
|
+
const home = env.USERPROFILE || homedir();
|
|
99
|
+
return path.win32.join(home, ".claude360", "cc-switch-claude360.json");
|
|
100
|
+
}
|
|
101
|
+
return path.posix.join(homedir(), ".claude360", "cc-switch-claude360.json");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function saveCcSwitchConfig(config, {
|
|
105
|
+
savePath = resolveCcSwitchSavePath(),
|
|
106
|
+
mkdirImpl = mkdir,
|
|
107
|
+
writeFileImpl = writeFile,
|
|
108
|
+
chmodImpl = chmod,
|
|
109
|
+
platform = process.platform,
|
|
110
|
+
} = {}) {
|
|
111
|
+
const dir = path.dirname(savePath);
|
|
112
|
+
await mkdirImpl(dir, { recursive: true, mode: 0o700 });
|
|
113
|
+
await writeFileImpl(savePath, `${JSON.stringify(config, null, 2)}\n`, {
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
mode: 0o600,
|
|
116
|
+
});
|
|
117
|
+
if (platform !== "win32") {
|
|
118
|
+
// mkdir 的 mode 对已存在目录不生效,必须显式收紧(与 config-store 一致)
|
|
119
|
+
await chmodImpl(dir, 0o700);
|
|
120
|
+
await chmodImpl(savePath, 0o600);
|
|
121
|
+
}
|
|
122
|
+
return savePath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 交互流程:选择目标 → 完整/脱敏(完整 Key 输出前二次确认)→ 输出 → 可选保存
|
|
126
|
+
export async function runCcSwitchGenerator({
|
|
127
|
+
config = {},
|
|
128
|
+
promptSelect,
|
|
129
|
+
writeLine = console.log,
|
|
130
|
+
ensureApiKey,
|
|
131
|
+
save = saveCcSwitchConfig,
|
|
132
|
+
} = {}) {
|
|
133
|
+
if (typeof promptSelect !== "function") {
|
|
134
|
+
throw new Error("缺少菜单选择输入");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let apiKey = config.apiKey;
|
|
138
|
+
if (!apiKey) {
|
|
139
|
+
writeLine("当前没有可用 API Key,需要先创建 Claude360 API Key。");
|
|
140
|
+
if (typeof ensureApiKey !== "function") {
|
|
141
|
+
return { done: false, reason: "no_key" };
|
|
142
|
+
}
|
|
143
|
+
const action = await promptSelect("生成 cc-switch 配置", [
|
|
144
|
+
{ label: "创建 API Key", value: "create" },
|
|
145
|
+
{ label: "返回", value: "back" },
|
|
146
|
+
]);
|
|
147
|
+
if (action !== "create") {
|
|
148
|
+
return { done: false, reason: "no_key" };
|
|
149
|
+
}
|
|
150
|
+
const token = await ensureApiKey();
|
|
151
|
+
apiKey = token?.apiKey;
|
|
152
|
+
if (!apiKey) {
|
|
153
|
+
writeLine("未获取到可用 API Key,已取消生成。");
|
|
154
|
+
return { done: false, reason: "no_key" };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const target = await promptSelect("生成 cc-switch 配置\n请选择配置目标:", [
|
|
159
|
+
{ label: "Claude Code", value: "claude-code" },
|
|
160
|
+
{ label: "Codex", value: "codex" },
|
|
161
|
+
{ label: "Claude Code + Codex", value: "both" },
|
|
162
|
+
{ label: "返回", value: "back" },
|
|
163
|
+
]);
|
|
164
|
+
if (target === "back") {
|
|
165
|
+
return { done: false, reason: "back" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const fullConfig = buildCcSwitchConfig(target, {
|
|
169
|
+
baseUrl: config.baseUrl,
|
|
170
|
+
apiKey,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const reveal = await promptSelect(
|
|
174
|
+
"即将显示完整 API Key。\n请确认当前终端环境安全,避免被他人看到。",
|
|
175
|
+
[
|
|
176
|
+
{ label: "显示完整配置", value: "full" },
|
|
177
|
+
{ label: "显示脱敏配置", value: "masked" },
|
|
178
|
+
{ label: "返回", value: "back" },
|
|
179
|
+
],
|
|
180
|
+
);
|
|
181
|
+
if (reveal === "back") {
|
|
182
|
+
return { done: false, reason: "back" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (reveal === "masked") {
|
|
186
|
+
const maskedConfig = maskCcSwitchConfig(fullConfig, apiKey);
|
|
187
|
+
writeLine(JSON.stringify(maskedConfig, null, 2));
|
|
188
|
+
writeLine("提示:脱敏配置仅用于预览,无法直接使用,也不会保存到文件。");
|
|
189
|
+
return { done: true, target, masked: true, saved: false };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
writeLine(JSON.stringify(fullConfig, null, 2));
|
|
193
|
+
writeLine("请将以上配置复制到 cc-switch 中使用。");
|
|
194
|
+
|
|
195
|
+
const saveChoice = await promptSelect("是否保存到本地文件?", [
|
|
196
|
+
{ label: "保存到 ~/.claude360/cc-switch-claude360.json", value: "save" },
|
|
197
|
+
{ label: "仅在终端显示", value: "skip" },
|
|
198
|
+
]);
|
|
199
|
+
if (saveChoice !== "save") {
|
|
200
|
+
return { done: true, target, masked: false, saved: false };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const savedPath = await save(fullConfig);
|
|
204
|
+
writeLine(`已保存到:${savedPath}(文件权限 0600)`);
|
|
205
|
+
return { done: true, target, masked: false, saved: true, savedPath };
|
|
206
|
+
}
|
package/src/diagnostics.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { access } from "node:fs/promises";
|
|
2
|
+
import { access, readFile as fsReadFile } from "node:fs/promises";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
|
|
6
|
+
import { resolveCodexConfigPath } from "./tool-launcher.js";
|
|
7
|
+
import { sanitizeError, sanitizeText } from "./sanitize.js";
|
|
8
|
+
|
|
6
9
|
export async function runDiagnostics({
|
|
7
10
|
config = {},
|
|
8
11
|
platform = defaultPlatform(),
|
|
9
12
|
execCommand = defaultExecCommand,
|
|
10
13
|
checkPathWritable = defaultCheckPathWritable,
|
|
14
|
+
readFile = fsReadFile,
|
|
15
|
+
codexConfigPath = resolveCodexConfigPath(),
|
|
11
16
|
api,
|
|
12
17
|
} = {}) {
|
|
13
18
|
const node = await commandVersion(execCommand, "node", ["--version"]);
|
|
@@ -18,13 +23,16 @@ export async function runDiagnostics({
|
|
|
18
23
|
ok: await checkPathWritable(npmPrefix.stdout.trim()),
|
|
19
24
|
detail: npmPrefix.stdout.trim(),
|
|
20
25
|
}
|
|
21
|
-
: { ok: false, detail: npmPrefix.stderr || npmPrefix.error || "npm prefix failed" };
|
|
26
|
+
: { ok: false, detail: sanitizeText(npmPrefix.stderr || npmPrefix.error || "npm prefix failed") };
|
|
22
27
|
const claudeCode = await commandVersion(execCommand, "claude", ["--version"]);
|
|
23
28
|
const codex = await commandVersion(execCommand, "codex", ["--version"]);
|
|
24
29
|
|
|
30
|
+
// 连通性用公开端点判断,避免未登录时把鉴权失败误报成网络故障
|
|
31
|
+
const serviceStatus = await safeApiGet(api, "/api/status");
|
|
25
32
|
const me = await safeApiGet(api, "/api/cli/me");
|
|
26
33
|
const tokens = await safeApiGet(api, "/api/cli/tokens");
|
|
27
34
|
const topUpOptions = await safeApiGet(api, "/api/cli/topup/options");
|
|
35
|
+
const codexCompat = await safeApiGet(api, "/api/cli/codex/compat");
|
|
28
36
|
|
|
29
37
|
return {
|
|
30
38
|
platform: {
|
|
@@ -43,23 +51,42 @@ export async function runDiagnostics({
|
|
|
43
51
|
},
|
|
44
52
|
api: {
|
|
45
53
|
connectivity: {
|
|
46
|
-
ok:
|
|
47
|
-
detail:
|
|
54
|
+
ok: serviceStatus.ok,
|
|
55
|
+
detail: serviceStatus.ok ? (config.baseUrl || "connected") : serviceStatus.detail,
|
|
48
56
|
},
|
|
49
57
|
},
|
|
50
58
|
auth: {
|
|
51
59
|
ok: Boolean(config.cliToken) && me.ok,
|
|
52
60
|
detail: config.cliToken ? "已授权" : "未授权",
|
|
53
61
|
},
|
|
54
|
-
balance: {
|
|
55
|
-
|
|
56
|
-
detail: me.ok ? String(me.data?.quota ?? "-") : me.detail,
|
|
57
|
-
},
|
|
62
|
+
balance: buildBalanceStatus({ me }),
|
|
63
|
+
usage: buildUsageStatus({ me }),
|
|
58
64
|
token: buildTokenStatus({ config, tokens }),
|
|
59
65
|
topUp: buildTopUpStatus({ topUpOptions }),
|
|
66
|
+
claudeCodeConfig: buildClaudeCodeConfigStatus({ config, claudeCode }),
|
|
67
|
+
codexConfig: await buildCodexConfigStatus({ readFile, codexConfigPath, codex }),
|
|
68
|
+
codexCompat: buildCodexCompatStatus({ codexCompat }),
|
|
60
69
|
};
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
function buildBalanceStatus({ me }) {
|
|
73
|
+
if (!me.ok) {
|
|
74
|
+
return { ok: false, detail: me.detail };
|
|
75
|
+
}
|
|
76
|
+
const display = me.data?.balance_display ?? String(me.data?.quota ?? "-");
|
|
77
|
+
if (me.data?.low_balance) {
|
|
78
|
+
return { ok: false, warn: true, detail: `${display}(余额较低,建议充值)` };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, detail: display };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildUsageStatus({ me }) {
|
|
84
|
+
if (!me.ok) {
|
|
85
|
+
return { ok: false, detail: me.detail };
|
|
86
|
+
}
|
|
87
|
+
return { ok: true, detail: me.data?.today_usage_display || "-" };
|
|
88
|
+
}
|
|
89
|
+
|
|
63
90
|
function buildTopUpStatus({ topUpOptions }) {
|
|
64
91
|
if (!topUpOptions.ok) {
|
|
65
92
|
return { ok: false, detail: topUpOptions.detail };
|
|
@@ -70,35 +97,85 @@ function buildTopUpStatus({ topUpOptions }) {
|
|
|
70
97
|
return { ok: true, detail: "可用" };
|
|
71
98
|
}
|
|
72
99
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
formatLine("Global npm permission", report.runtime.globalNpmPermission, "修复 npm 全局目录权限,或切换到用户级 npm prefix"),
|
|
80
|
-
formatLine("Claude Code", report.tools.claudeCode, "安装或更新 Claude Code", "version"),
|
|
81
|
-
formatLine("Codex", report.tools.codex, "安装或更新 Codex", "version"),
|
|
82
|
-
formatLine("Claude360 API", report.api.connectivity, "检查网络连接和 Claude360 站点可用性"),
|
|
83
|
-
formatLine("Auth", report.auth, "重新执行浏览器授权"),
|
|
84
|
-
formatLine("Balance", report.balance, "授权后重试,或登录网站检查账户状态"),
|
|
85
|
-
formatLine("Token", report.token, "在菜单中切换或创建 API Key"),
|
|
86
|
-
formatLine("WeChat top-up", report.topUp, "检查后端微信支付配置或稍后重试"),
|
|
87
|
-
].join("\n");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function formatLine(label, check, fix, valueKey = "detail") {
|
|
91
|
-
const line = `${label}: ${formatCheck(check, valueKey)}`;
|
|
92
|
-
if (check.ok) {
|
|
93
|
-
return line;
|
|
100
|
+
function buildClaudeCodeConfigStatus({ config, claudeCode }) {
|
|
101
|
+
if (!claudeCode.ok) {
|
|
102
|
+
return { ok: false, detail: "Claude Code 未安装" };
|
|
103
|
+
}
|
|
104
|
+
if (!config.apiKey) {
|
|
105
|
+
return { ok: false, detail: "缺少 API Key,启动时无法注入 Claude360 配置" };
|
|
94
106
|
}
|
|
95
|
-
return
|
|
107
|
+
return { ok: true, detail: "启动时注入 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN" };
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
async function buildCodexConfigStatus({ readFile, codexConfigPath, codex }) {
|
|
111
|
+
if (!codex.ok) {
|
|
112
|
+
return { ok: false, detail: "Codex 未安装" };
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const content = await readFile(codexConfigPath, "utf8");
|
|
116
|
+
if (content.includes("[model_providers.claude360]") && content.includes("[profiles.claude360]")) {
|
|
117
|
+
return { ok: true, detail: "profile claude360 正常" };
|
|
118
|
+
}
|
|
119
|
+
return { ok: false, detail: "未写入 claude360 provider/profile" };
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (error?.code === "ENOENT") {
|
|
122
|
+
return { ok: false, detail: "未找到 ~/.codex/config.toml" };
|
|
123
|
+
}
|
|
124
|
+
return { ok: false, detail: sanitizeError(error) || "读取 config.toml 失败" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildCodexCompatStatus({ codexCompat }) {
|
|
129
|
+
if (!codexCompat.ok) {
|
|
130
|
+
return { ok: false, warn: true, detail: `Codex 协议兼容性待验证(${codexCompat.detail})` };
|
|
131
|
+
}
|
|
132
|
+
if (!codexCompat.data?.supported) {
|
|
133
|
+
return { ok: false, detail: "Claude360 暂不支持 Codex 所需协议" };
|
|
134
|
+
}
|
|
135
|
+
return { ok: true, detail: `已支持(wire_api=${codexCompat.data?.wire_api || "responses"})` };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatDiagnosticsSummary(report) {
|
|
139
|
+
const lines = [];
|
|
140
|
+
lines.push("系统环境");
|
|
141
|
+
lines.push(check({ ok: true, detail: `${report.platform.os} ${report.platform.arch}` }, "OS"));
|
|
142
|
+
lines.push(checkFix(report.runtime.node, "Node.js", "安装 Node.js 18+ 后重新运行 claude360", "version"));
|
|
143
|
+
lines.push(checkFix(report.runtime.npm, "npm", "安装 npm 或修复 Node.js 安装", "version"));
|
|
144
|
+
lines.push(checkFix(report.runtime.globalNpmPermission, "全局 npm 权限", "修复 npm 全局目录权限,或切换到用户级 npm prefix"));
|
|
145
|
+
lines.push(checkFix(report.platform.terminalQr, "终端二维码", "终端不支持时可复制支付链接完成支付"));
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push("Claude360");
|
|
148
|
+
lines.push(checkFix(report.api.connectivity, "服务连接", "检查网络连接和 Claude360 站点可用性"));
|
|
149
|
+
lines.push(checkFix(report.auth, "登录状态", "在菜单中选择“重新登录”完成浏览器授权"));
|
|
150
|
+
lines.push(checkFix(report.balance, "当前余额", "通过“余额与充值”菜单充值"));
|
|
151
|
+
lines.push(checkFix(report.usage, "今日用量", "登录后可查看今日 Token 用量"));
|
|
152
|
+
lines.push(checkFix(report.token, "当前 Key", "在菜单中切换或创建 API Key"));
|
|
153
|
+
lines.push(checkFix(report.topUp, "微信充值", "检查后端微信支付配置或使用网页充值"));
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("Claude Code");
|
|
156
|
+
lines.push(checkFix(report.tools.claudeCode, "安装状态", "通过菜单安装或更新 Claude Code", "version"));
|
|
157
|
+
lines.push(checkFix(report.claudeCodeConfig, "Claude360 配置", "在“Claude Code 配置”菜单中修复"));
|
|
158
|
+
lines.push("");
|
|
159
|
+
lines.push("Codex");
|
|
160
|
+
lines.push(checkFix(report.tools.codex, "安装状态", "通过菜单安装或更新 Codex", "version"));
|
|
161
|
+
lines.push(checkFix(report.codexConfig, "claude360 provider", "在“Codex 配置”菜单中写入/修复"));
|
|
162
|
+
lines.push(checkFix(report.codexCompat, "协议兼容性", "Codex 暂不可用时可先使用 Claude Code"));
|
|
163
|
+
// 出口兜底:report 由调用方注入,逐项 detail 可能未经脱敏
|
|
164
|
+
return sanitizeText(lines.join("\n"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function check(item, label, valueKey = "detail") {
|
|
168
|
+
const symbol = item.ok ? "✓" : item.warn ? "!" : "×";
|
|
169
|
+
const value = item[valueKey] || item.detail || "";
|
|
170
|
+
return `${symbol} ${label}${value ? `:${value}` : ""}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function checkFix(item, label, fix, valueKey = "detail") {
|
|
174
|
+
const line = check(item, label, valueKey);
|
|
175
|
+
if (item.ok) {
|
|
176
|
+
return line;
|
|
177
|
+
}
|
|
178
|
+
return `${line}\n 建议:${fix}`;
|
|
102
179
|
}
|
|
103
180
|
|
|
104
181
|
function defaultPlatform() {
|
|
@@ -124,7 +201,7 @@ function detectTerminalQr(platform) {
|
|
|
124
201
|
export async function commandVersion(execCommand, command, args) {
|
|
125
202
|
const result = await execCommand(command, args);
|
|
126
203
|
if (!result.ok) {
|
|
127
|
-
return { ok: false, detail: result.stderr || result.error || "not found" };
|
|
204
|
+
return { ok: false, detail: sanitizeText(result.stderr || result.error || "not found") };
|
|
128
205
|
}
|
|
129
206
|
return {
|
|
130
207
|
ok: true,
|
|
@@ -139,7 +216,7 @@ async function safeApiGet(api, path) {
|
|
|
139
216
|
try {
|
|
140
217
|
return { ok: true, data: await api.get(path) };
|
|
141
218
|
} catch (error) {
|
|
142
|
-
return { ok: false, detail: error
|
|
219
|
+
return { ok: false, detail: sanitizeError(error) || "请求失败" };
|
|
143
220
|
}
|
|
144
221
|
}
|
|
145
222
|
|