claude360 0.2.0 → 0.2.2
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/package.json +1 -1
- package/src/banner.js +247 -14
- package/src/index.js +9 -3
- package/src/menu.js +68 -29
package/package.json
CHANGED
package/src/banner.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
// 品牌横幅:参照 zcf 的顶部边框设计 —— 圆角边框盒子内放渐变色 ASCII logo,
|
|
2
|
+
// TTY 下播放流光动效(渐变相位随帧移动),非 TTY / NO_COLOR 下降级为纯文本。
|
|
3
|
+
// 渲染函数默认无色(color: false),保证测试与管道输出稳定;
|
|
4
|
+
// 运行时由 index.js 通过 playBanner / 显式 color 参数开启彩色。
|
|
5
|
+
|
|
1
6
|
export const BRAND_BASE_URL = "https://claude360.xyz";
|
|
2
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
|
+
|
|
3
25
|
const LOGO_LINES = [
|
|
4
26
|
" ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗██████╗ ██████╗ ██████╗ ",
|
|
5
27
|
"██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝╚════██╗██╔════╝ ██╔═████╗",
|
|
@@ -9,34 +31,245 @@ const LOGO_LINES = [
|
|
|
9
31
|
" ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚═════╝ ╚═════╝ ",
|
|
10
32
|
];
|
|
11
33
|
|
|
12
|
-
|
|
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 }) {
|
|
13
168
|
const lines = [
|
|
14
|
-
"",
|
|
15
|
-
|
|
16
|
-
""
|
|
17
|
-
" Claude360 模型站接入助手",
|
|
18
|
-
" 一键配置 Claude Code / Codex / API Key / 充值",
|
|
19
|
-
"",
|
|
20
|
-
`版本:${version || "-"} | 官网:${baseUrl}`,
|
|
21
|
-
"",
|
|
169
|
+
"Claude360 CLI · 模型站接入助手",
|
|
170
|
+
SUBLINE,
|
|
171
|
+
`版本 v${version || "-"} · 官网 ${baseUrl}`,
|
|
22
172
|
];
|
|
23
|
-
|
|
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
|
+
}
|
|
24
247
|
}
|
|
25
248
|
|
|
26
249
|
export const CHECK_OK = "✓";
|
|
27
250
|
export const CHECK_WARN = "!";
|
|
28
251
|
export const CHECK_FAIL = "×";
|
|
29
252
|
|
|
30
|
-
|
|
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 } = {}) {
|
|
31
260
|
const symbols = { ok: CHECK_OK, warn: CHECK_WARN, fail: CHECK_FAIL };
|
|
32
261
|
const symbol = symbols[status] || CHECK_WARN;
|
|
33
|
-
|
|
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}`;
|
|
34
267
|
}
|
|
35
268
|
|
|
36
|
-
export function renderEnvironmentChecks(checks = []) {
|
|
269
|
+
export function renderEnvironmentChecks(checks = [], { color = false } = {}) {
|
|
37
270
|
return [
|
|
38
271
|
"正在检查运行环境...",
|
|
39
272
|
"",
|
|
40
|
-
...checks.map((check) => formatCheckLine(check.status, check.text)),
|
|
273
|
+
...checks.map((check) => formatCheckLine(check.status, check.text, { color })),
|
|
41
274
|
].join("\n");
|
|
42
275
|
}
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
|
|
|
6
6
|
import { ApiClient } from "./api-client.js";
|
|
7
7
|
import { authenticateWithBrowser } from "./auth.js";
|
|
8
8
|
import { formatAccountStatus, loadAccountStatus } from "./account-status.js";
|
|
9
|
-
import {
|
|
9
|
+
import { colorEnabled, formatCheckLine, playBanner, renderBanner } from "./banner.js";
|
|
10
10
|
import { runCcSwitchGenerator } from "./cc-switch.js";
|
|
11
11
|
import { createConfigStore } from "./config-store.js";
|
|
12
12
|
import {
|
|
@@ -82,9 +82,15 @@ export async function runCli({
|
|
|
82
82
|
let config = await configStore.load();
|
|
83
83
|
const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
84
84
|
let api = createApiClient({ baseUrl, cliToken: config.cliToken || "" });
|
|
85
|
+
// 仅在真实终端(writeLine 未被测试替换)启用彩色与动效
|
|
86
|
+
const fancyOutput = writeLine === console.log && colorEnabled();
|
|
85
87
|
|
|
86
88
|
if (showBanner) {
|
|
87
|
-
|
|
89
|
+
if (fancyOutput) {
|
|
90
|
+
await playBanner({ version, baseUrl });
|
|
91
|
+
} else {
|
|
92
|
+
writeLine(renderBanner({ version, baseUrl }));
|
|
93
|
+
}
|
|
88
94
|
await showEnvironmentChecks();
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -113,7 +119,7 @@ export async function runCli({
|
|
|
113
119
|
checks.push(config.cliToken
|
|
114
120
|
? { status: "ok", text: "已登录 Claude360" }
|
|
115
121
|
: { status: "warn", text: "尚未登录 Claude360" });
|
|
116
|
-
writeLine(checks.map((check) => formatCheckLine(check.status, check.text)).join("\n"));
|
|
122
|
+
writeLine(checks.map((check) => formatCheckLine(check.status, check.text, { color: fancyOutput })).join("\n"));
|
|
117
123
|
writeLine("");
|
|
118
124
|
}
|
|
119
125
|
|
package/src/menu.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
// 菜单:区分首次使用菜单与日常使用菜单(PRD 第 10、11 章)。
|
|
2
2
|
// 菜单自带分区标题与选择键(1-9 / A / 0 / Q),由 renderMenu 渲染、
|
|
3
3
|
// resolveMenuSelection 解析,promptMenu 循环读取直到输入有效。
|
|
4
|
+
// 渲染默认无色(color: false)以保证测试与管道输出稳定,
|
|
5
|
+
// 真实终端下由 promptMenu 自动开启彩色分区样式。
|
|
6
|
+
|
|
7
|
+
import { colorEnabled, displayWidth } from "./banner.js";
|
|
8
|
+
|
|
9
|
+
const ESC = "\u001b[";
|
|
10
|
+
const RESET = `${ESC}0m`;
|
|
11
|
+
const BOLD = `${ESC}1m`;
|
|
12
|
+
const fg = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
|
|
13
|
+
|
|
14
|
+
const RULE_COLOR = fg(71, 85, 105); // 分隔线:青灰
|
|
15
|
+
const SECTION_COLOR = fg(34, 211, 238); // 分区标题:亮青
|
|
16
|
+
const KEY_COLOR = fg(125, 211, 252); // 选择键:天蓝
|
|
17
|
+
const EXIT_COLOR = fg(248, 113, 113); // 退出键:红
|
|
18
|
+
const DESC_COLOR = fg(100, 116, 139); // 功能说明:暗灰
|
|
19
|
+
const MENU_RULE_WIDTH = 46;
|
|
4
20
|
|
|
5
21
|
export function buildFirstRunMenu() {
|
|
6
22
|
return {
|
|
@@ -14,13 +30,13 @@ export function buildFirstRunMenu() {
|
|
|
14
30
|
{
|
|
15
31
|
title: null,
|
|
16
32
|
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" },
|
|
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" },
|
|
24
40
|
],
|
|
25
41
|
},
|
|
26
42
|
],
|
|
@@ -32,53 +48,74 @@ export function buildDailyMenu() {
|
|
|
32
48
|
title: "请选择功能:",
|
|
33
49
|
sections: [
|
|
34
50
|
{
|
|
35
|
-
title: "
|
|
51
|
+
title: "快速启动",
|
|
36
52
|
items: [
|
|
37
|
-
{ key: "1", label: "启动 Claude Code", value: "launch_claude" },
|
|
38
|
-
{ 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 接入配置直接启动" },
|
|
39
55
|
],
|
|
40
56
|
},
|
|
41
57
|
{
|
|
42
|
-
title: "
|
|
58
|
+
title: "Claude360",
|
|
43
59
|
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" },
|
|
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: "在浏览器中打开网页控制台" },
|
|
47
63
|
],
|
|
48
64
|
},
|
|
49
65
|
{
|
|
50
|
-
title: "
|
|
66
|
+
title: "工具配置",
|
|
51
67
|
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" },
|
|
68
|
+
{ key: "6", label: "Claude Code 配置", value: "claude_code_menu", desc: "安装、更新、写入或修复接入配置" },
|
|
69
|
+
{ key: "7", label: "Codex 配置", value: "codex_menu", desc: "安装、更新、写入或修复接入配置" },
|
|
70
|
+
{ key: "8", label: "推荐 MCP / Skill", value: "mcp_skill", desc: "安装推荐的 MCP、工作流与 Skill 增强" },
|
|
71
|
+
{ key: "9", label: "生成 cc-switch 配置", value: "cc_switch", desc: "生成可导入 cc-switch 的供应商配置" },
|
|
56
72
|
],
|
|
57
73
|
},
|
|
58
74
|
{
|
|
59
|
-
title: "
|
|
75
|
+
title: "维护",
|
|
60
76
|
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" },
|
|
77
|
+
{ key: "A", label: "诊断与修复", value: "diagnostics_menu", desc: "一键诊断环境与配置问题并尝试修复" },
|
|
78
|
+
{ key: "B", label: "安装或更新工具", value: "install_tools", desc: "安装或升级 Claude Code / Codex / 本 CLI" },
|
|
79
|
+
{ key: "0", label: "重新登录", value: "relogin", desc: "清除本地登录态后重新浏览器授权" },
|
|
80
|
+
{ key: "Q", label: "退出", value: "exit", desc: "退出 claude360 CLI" },
|
|
65
81
|
],
|
|
66
82
|
},
|
|
67
83
|
],
|
|
68
84
|
};
|
|
69
85
|
}
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
// 分区标题渲染为 `─── 标题 ──────…` 形式的分隔线,让各组功能在视觉上彼此分开
|
|
88
|
+
function renderSectionRule(title, color) {
|
|
89
|
+
const lead = "───";
|
|
90
|
+
const label = ` ${title} `;
|
|
91
|
+
const tail = "─".repeat(Math.max(4, MENU_RULE_WIDTH - lead.length - displayWidth(label)));
|
|
92
|
+
if (!color) {
|
|
93
|
+
return `${lead}${label}${tail}`;
|
|
94
|
+
}
|
|
95
|
+
return `${RULE_COLOR}${lead}${RESET}${BOLD}${SECTION_COLOR}${label}${RESET}${RULE_COLOR}${tail}${RESET}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function renderItem(item, color) {
|
|
99
|
+
const desc = item.desc ? ` - ${item.desc}` : "";
|
|
100
|
+
if (!color) {
|
|
101
|
+
return ` ${item.key}. ${item.label}${desc}`;
|
|
102
|
+
}
|
|
103
|
+
const keyColor = item.value === "exit" ? EXIT_COLOR : KEY_COLOR;
|
|
104
|
+
const coloredDesc = desc ? `${DESC_COLOR}${desc}${RESET}` : "";
|
|
105
|
+
return ` ${BOLD}${keyColor}${item.key}.${RESET} ${item.label}${coloredDesc}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function renderMenu(menu, { color = false } = {}) {
|
|
72
109
|
const lines = [];
|
|
73
110
|
if (menu.title) {
|
|
74
|
-
lines.push(menu.title, "");
|
|
111
|
+
lines.push(color ? `${BOLD}${menu.title}${RESET}` : menu.title, "");
|
|
75
112
|
}
|
|
76
113
|
for (const section of menu.sections) {
|
|
77
114
|
if (section.title) {
|
|
78
|
-
lines.push(section.title);
|
|
115
|
+
lines.push(renderSectionRule(section.title, color));
|
|
79
116
|
}
|
|
80
117
|
for (const item of section.items) {
|
|
81
|
-
lines.push(
|
|
118
|
+
lines.push(renderItem(item, color));
|
|
82
119
|
}
|
|
83
120
|
lines.push("");
|
|
84
121
|
}
|
|
@@ -107,8 +144,10 @@ export async function promptMenu({ menu, promptInput, writeLine = console.log }
|
|
|
107
144
|
if (typeof promptInput !== "function") {
|
|
108
145
|
throw new Error("缺少菜单输入");
|
|
109
146
|
}
|
|
147
|
+
// 仅在输出未被测试替换且终端支持时启用彩色
|
|
148
|
+
const color = writeLine === console.log && colorEnabled();
|
|
110
149
|
while (true) {
|
|
111
|
-
writeLine(renderMenu(menu));
|
|
150
|
+
writeLine(renderMenu(menu, { color }));
|
|
112
151
|
const answer = await promptInput("请输入选项");
|
|
113
152
|
const value = resolveMenuSelection(menu, answer);
|
|
114
153
|
if (value !== null) {
|