claude360 0.2.5 → 0.2.7
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 +25 -13
- package/package.json +1 -1
- package/src/banner.js +22 -29
- package/src/colors.js +93 -0
- package/src/index.js +7 -4
- package/src/init-config.js +2 -1
- package/src/mcp-skill.js +21 -11
- package/src/menu.js +26 -20
- package/src/prompts.js +70 -39
- package/src/ui.js +48 -33
package/README.md
CHANGED
|
@@ -115,25 +115,37 @@ claude360
|
|
|
115
115
|
|
|
116
116
|
## 日常使用
|
|
117
117
|
|
|
118
|
+
主菜单按分组展示,TTY 环境下用 **方向键 + Enter** 选择(Esc 返回);非 TTY / CI 环境自动降级为编号输入,编号以实际输出为准:
|
|
119
|
+
|
|
118
120
|
```
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
121
|
+
请选择功能:
|
|
122
|
+
─── 常用操作 ───
|
|
123
|
+
❯ 启动 Claude Code 使用 Claude360 接入配置直接启动
|
|
124
|
+
启动 Codex 使用 Claude360 接入配置直接启动
|
|
125
|
+
─── 账户与充值 ───
|
|
126
|
+
余额与充值 查看余额用量,支持微信扫码充值
|
|
127
|
+
切换 Key / 模型 切换当前 API Key 或默认模型
|
|
128
|
+
创建新的 API Key 在当前账号下新建一个 API Key
|
|
129
|
+
打开 Claude360 控制台 在浏览器中打开网页控制台
|
|
130
|
+
─── Key / 模型 / MCP 配置 ───
|
|
131
|
+
Claude Code 配置 完整初始化、工作流、MCP、模型与记忆配置
|
|
132
|
+
Codex 配置 完整初始化、Provider、工作流与 MCP 配置
|
|
133
|
+
一键完整初始化 Claude Code / Codex 完整初始化向导
|
|
134
|
+
推荐 MCP / Skill 安装推荐的 MCP、工作流与 Skill 增强
|
|
135
|
+
生成 cc-switch 配置 生成可导入 cc-switch 的供应商配置
|
|
136
|
+
─── 维护与设置 ───
|
|
137
|
+
安装或更新工具 安装或升级 Claude Code / Codex / 本 CLI
|
|
138
|
+
诊断与修复 一键诊断环境与配置问题并尝试修复
|
|
139
|
+
重新登录 清除本地登录态后重新浏览器授权
|
|
140
|
+
退出 退出 claude360 CLI
|
|
128
141
|
```
|
|
129
142
|
|
|
130
|
-
-
|
|
131
|
-
- **微信扫码充值**:先取 `/api/cli/topup/options`,按后端校验通过的金额或最低额提交,再用 `qrcode-terminal` 在终端渲染 `code_url`,渲染失败时降级为纯文本 URL。轮询 `/api/cli/topup/order?order_id=`,支付完成自动刷新余额。
|
|
143
|
+
- **余额与充值**:调用 `GET /api/cli/me`,展示账号、余额、已用、当前 Key 名称与分组;充值先取 `/api/cli/topup/options`,按后端校验通过的金额或最低额提交,再用 `qrcode-terminal` 在终端渲染 `code_url`,渲染失败时降级为纯文本 URL。轮询 `/api/cli/topup/order?order_id=`,支付完成自动刷新余额。
|
|
132
144
|
- **启动 Claude Code**:调用 `claude` 子进程,并通过 `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` 注入。
|
|
133
145
|
- **启动 Codex**:写入 `~/.codex/config.toml` 中 `[model_providers.claude360]` 与 `[profiles.claude360]`,发现既有冲突字段会先要求用户确认再覆盖,然后 `codex --profile claude360`,并通过 `CLAUDE360_API_KEY` 注入。
|
|
134
146
|
- **安装或更新工具**:三选一菜单(仅 Claude Code / 仅 Codex / 两者),每步都需要确认。
|
|
135
|
-
- **切换
|
|
136
|
-
-
|
|
147
|
+
- **切换 Key / 模型**:重新进入 Token 向导,或从 `/api/cli/models` 拉取真实模型列表选择默认模型。
|
|
148
|
+
- **诊断与修复**:见下方“诊断与故障排查”。
|
|
137
149
|
|
|
138
150
|
## 本地配置
|
|
139
151
|
|
package/package.json
CHANGED
package/src/banner.js
CHANGED
|
@@ -3,24 +3,13 @@
|
|
|
3
3
|
// 渲染函数默认无色(color: false),保证测试与管道输出稳定;
|
|
4
4
|
// 运行时由 index.js 通过 playBanner / 显式 color 参数开启彩色。
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const ESC = "\u001b[";
|
|
9
|
-
const RESET = `${ESC}0m`;
|
|
10
|
-
const BOLD = `${ESC}1m`;
|
|
11
|
-
const DIM = `${ESC}2m`;
|
|
6
|
+
import { BOLD, DIM, ESC, RESET, colorEnabled, colorLevel, fg, toLevel } from "./colors.js";
|
|
12
7
|
|
|
13
|
-
const
|
|
8
|
+
export const BRAND_BASE_URL = "https://claude360.xyz";
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") {
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
return Boolean(stream && stream.isTTY);
|
|
23
|
-
}
|
|
10
|
+
// colorEnabled / colorLevel 由 colors.js 统一提供,这里 re-export,
|
|
11
|
+
// 以保持 menu.js / index.js 从 banner.js 导入的既有路径不变。
|
|
12
|
+
export { colorEnabled, colorLevel };
|
|
24
13
|
|
|
25
14
|
const LOGO_LINES = [
|
|
26
15
|
" ██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗██████╗ ██████╗ ██████╗ ",
|
|
@@ -93,7 +82,7 @@ const LOGO_EDGE_CHARS = new Set(["╔", "╗", "╚", "╝", "║", "═"]);
|
|
|
93
82
|
// style:
|
|
94
83
|
// gradient —— 整行按列渐变(logo 行、边框行)
|
|
95
84
|
// text:<sgr> —— 边框字符渐变,其余字符用固定 SGR 样式(标语行)
|
|
96
|
-
function paintLine(line, { phase, totalWidth, style }) {
|
|
85
|
+
function paintLine(line, { phase, totalWidth, style, level = 2 }) {
|
|
97
86
|
let out = "";
|
|
98
87
|
let col = 0;
|
|
99
88
|
let lastColor = "";
|
|
@@ -118,7 +107,7 @@ function paintLine(line, { phase, totalWidth, style }) {
|
|
|
118
107
|
g = Math.round(g * 0.55);
|
|
119
108
|
b = Math.round(b * 0.55);
|
|
120
109
|
}
|
|
121
|
-
color = fg(r, g, b);
|
|
110
|
+
color = fg(r, g, b, level);
|
|
122
111
|
} else {
|
|
123
112
|
color = style;
|
|
124
113
|
}
|
|
@@ -165,16 +154,17 @@ function buildBannerLayout({ version, baseUrl }) {
|
|
|
165
154
|
|
|
166
155
|
// 终端太窄放不下大 logo 时的紧凑版
|
|
167
156
|
function renderCompactBanner({ version, baseUrl, color, phase }) {
|
|
157
|
+
const level = toLevel(color);
|
|
168
158
|
const lines = [
|
|
169
159
|
"Claude360 CLI · 模型站接入助手",
|
|
170
160
|
SUBLINE,
|
|
171
161
|
`版本 v${version || "-"} · 官网 ${baseUrl}`,
|
|
172
162
|
];
|
|
173
|
-
if (!
|
|
163
|
+
if (!level) {
|
|
174
164
|
return lines.join("\n");
|
|
175
165
|
}
|
|
176
166
|
return [
|
|
177
|
-
paintLine(lines[0], { phase, totalWidth: displayWidth(lines[0]), style: "gradient" }),
|
|
167
|
+
paintLine(lines[0], { phase, totalWidth: displayWidth(lines[0]), style: "gradient", level }),
|
|
178
168
|
`${DIM}${lines[1]}${RESET}`,
|
|
179
169
|
`${DIM}${lines[2]}${RESET}`,
|
|
180
170
|
].join("\n");
|
|
@@ -191,19 +181,20 @@ export function renderBanner({
|
|
|
191
181
|
if (columns > 0 && columns < totalWidth) {
|
|
192
182
|
return renderCompactBanner({ version, baseUrl, color, phase });
|
|
193
183
|
}
|
|
194
|
-
|
|
184
|
+
const level = toLevel(color);
|
|
185
|
+
if (!level) {
|
|
195
186
|
return rows.map((row) => row.text).join("\n");
|
|
196
187
|
}
|
|
197
188
|
const styles = {
|
|
198
|
-
title: `${BOLD}${fg(240, 246, 255)}`,
|
|
199
|
-
sub: `${fg(148, 163, 184)}`,
|
|
189
|
+
title: `${BOLD}${fg(240, 246, 255, level)}`,
|
|
190
|
+
sub: `${fg(148, 163, 184, level)}`,
|
|
200
191
|
};
|
|
201
192
|
return rows.map((row) => {
|
|
202
193
|
if (row.style === "footer") {
|
|
203
194
|
return `${DIM}${row.text}${RESET}`;
|
|
204
195
|
}
|
|
205
196
|
const style = row.style === "gradient" ? "gradient" : styles[row.style];
|
|
206
|
-
return paintLine(row.text, { phase, totalWidth, style });
|
|
197
|
+
return paintLine(row.text, { phase, totalWidth, style, level });
|
|
207
198
|
}).join("\n");
|
|
208
199
|
}
|
|
209
200
|
|
|
@@ -218,11 +209,12 @@ export async function playBanner({
|
|
|
218
209
|
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
219
210
|
} = {}) {
|
|
220
211
|
const columns = stream && stream.columns ? stream.columns : 0;
|
|
221
|
-
const
|
|
212
|
+
const level = colorLevel(stream);
|
|
213
|
+
const animated = level > 0
|
|
222
214
|
&& typeof stream.write === "function"
|
|
223
215
|
&& (!columns || columns >= buildBannerLayout({ version, baseUrl }).totalWidth);
|
|
224
216
|
if (!animated) {
|
|
225
|
-
const text = renderBanner({ version, baseUrl, color:
|
|
217
|
+
const text = renderBanner({ version, baseUrl, color: level, columns });
|
|
226
218
|
if (stream && typeof stream.write === "function") {
|
|
227
219
|
stream.write(text + "\n");
|
|
228
220
|
} else {
|
|
@@ -235,7 +227,7 @@ export async function playBanner({
|
|
|
235
227
|
try {
|
|
236
228
|
for (let i = 0; i <= frames; i += 1) {
|
|
237
229
|
const phase = i / frames;
|
|
238
|
-
stream.write(renderBanner({ version, baseUrl, color:
|
|
230
|
+
stream.write(renderBanner({ version, baseUrl, color: level, phase }) + "\n");
|
|
239
231
|
if (i < frames) {
|
|
240
232
|
await sleep(intervalMs);
|
|
241
233
|
stream.write(`${ESC}${lineCount}A`);
|
|
@@ -259,11 +251,12 @@ const CHECK_COLORS = {
|
|
|
259
251
|
export function formatCheckLine(status, text, { color = false } = {}) {
|
|
260
252
|
const symbols = { ok: CHECK_OK, warn: CHECK_WARN, fail: CHECK_FAIL };
|
|
261
253
|
const symbol = symbols[status] || CHECK_WARN;
|
|
262
|
-
|
|
254
|
+
const level = toLevel(color);
|
|
255
|
+
if (!level) {
|
|
263
256
|
return `${symbol} ${text}`;
|
|
264
257
|
}
|
|
265
258
|
const [r, g, b] = CHECK_COLORS[status] || CHECK_COLORS.warn;
|
|
266
|
-
return `${fg(r, g, b)}${BOLD}${symbol}${RESET} ${text}`;
|
|
259
|
+
return `${fg(r, g, b, level)}${BOLD}${symbol}${RESET} ${text}`;
|
|
267
260
|
}
|
|
268
261
|
|
|
269
262
|
export function renderEnvironmentChecks(checks = [], { color = false } = {}) {
|
package/src/colors.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// 终端色彩能力检测与着色:truecolor(2) / 256 色(1) / 无色(0) 三档自适应。
|
|
2
|
+
// 统一被 banner.js / menu.js / ui.js / prompts.js 复用,消除各文件重复的
|
|
3
|
+
// ESC / RESET / fg 定义。macOS Terminal.app 等仅支持 256 色的终端无法解析
|
|
4
|
+
// 24-bit 真彩色序列(38;2;r;g;b)会出现颜色错乱,这里在 256 色档把 RGB
|
|
5
|
+
// 量化到 xterm-256 调色板,既保留渐变观感又避免错乱。
|
|
6
|
+
|
|
7
|
+
export const ESC = "[";
|
|
8
|
+
export const RESET = `${ESC}0m`;
|
|
9
|
+
export const BOLD = `${ESC}1m`;
|
|
10
|
+
export const DIM = `${ESC}2m`;
|
|
11
|
+
|
|
12
|
+
// 终端色彩深度:0=无色,1=256 色,2=真彩色(24-bit)。
|
|
13
|
+
// 仅检测能力(是否上色 + 用多深的色),不负责方向键 UI 判定(见 prompts.isInteractive)。
|
|
14
|
+
export function colorLevel(stream = process.stdout) {
|
|
15
|
+
const { NO_COLOR, FORCE_COLOR, COLORTERM, TERM } = process.env;
|
|
16
|
+
if (NO_COLOR !== undefined && NO_COLOR !== "") {
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
let forced = false;
|
|
20
|
+
if (FORCE_COLOR !== undefined && FORCE_COLOR !== "") {
|
|
21
|
+
if (FORCE_COLOR === "0" || FORCE_COLOR === "false") {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
if (FORCE_COLOR === "1") {
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
if (FORCE_COLOR === "2" || FORCE_COLOR === "3") {
|
|
28
|
+
return 2;
|
|
29
|
+
}
|
|
30
|
+
forced = true; // 其它非空值:强制上色,色深仍按下方终端能力判定
|
|
31
|
+
}
|
|
32
|
+
if (!forced && !(stream && stream.isTTY)) {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
const colorterm = String(COLORTERM || "").toLowerCase();
|
|
36
|
+
if (colorterm.includes("truecolor") || colorterm.includes("24bit")) {
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
const term = String(TERM || "").toLowerCase();
|
|
40
|
+
if (/-direct|kitty|iterm/.test(term)) {
|
|
41
|
+
return 2;
|
|
42
|
+
}
|
|
43
|
+
// 其余可上色场景统一按 256 色处理(含 *-256color、空 TERM、dumb 等),
|
|
44
|
+
// 保证「isTTY 即至少 256 色」,使 banner 动画在各类 CI 的 TERM 下稳定触发。
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function colorEnabled(stream = process.stdout) {
|
|
49
|
+
return colorLevel(stream) > 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 把 render / prompt 的 color 入参(boolean | 0 | 1 | 2)归一为 level。
|
|
53
|
+
// true → 2(真彩色),保证既有「color: true」调用与测试行为完全不变。
|
|
54
|
+
export function toLevel(color) {
|
|
55
|
+
if (color === true) {
|
|
56
|
+
return 2;
|
|
57
|
+
}
|
|
58
|
+
if (!color) {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
const n = Number(color);
|
|
62
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
return n >= 2 ? 2 : 1;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 24-bit RGB 量化到 xterm-256 调色板:232-255 灰阶 + 16-231 的 6×6×6 立方。
|
|
69
|
+
export function rgbToAnsi256(r, g, b) {
|
|
70
|
+
if (r === g && g === b) {
|
|
71
|
+
if (r < 8) {
|
|
72
|
+
return 16;
|
|
73
|
+
}
|
|
74
|
+
if (r > 248) {
|
|
75
|
+
return 231;
|
|
76
|
+
}
|
|
77
|
+
return Math.round(((r - 8) / 247) * 24) + 232;
|
|
78
|
+
}
|
|
79
|
+
const q = (v) => Math.round((v / 255) * 5);
|
|
80
|
+
return 16 + 36 * q(r) + 6 * q(g) + q(b);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 前景色:按 level 输出真彩色 / 256 色 / 空串。level 默认 2,
|
|
84
|
+
// 使任何未显式降级的调用安全回退为真彩色(与改造前行为一致)。
|
|
85
|
+
export function fg(r, g, b, level = 2) {
|
|
86
|
+
if (level >= 2) {
|
|
87
|
+
return `${ESC}38;2;${r};${g};${b}m`;
|
|
88
|
+
}
|
|
89
|
+
if (level === 1) {
|
|
90
|
+
return `${ESC}38;5;${rgbToAnsi256(r, g, b)}m`;
|
|
91
|
+
}
|
|
92
|
+
return "";
|
|
93
|
+
}
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
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 { colorLevel, 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 { createPrompts, isInteractive } from "./prompts.js";
|
|
@@ -127,6 +127,9 @@ export async function runCli({
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
multiSelectInput = multiSelectInput || promptMultiSelect;
|
|
130
|
+
// 覆盖 ~/.codex/config.toml 冲突字段属于危险操作:默认 NO,
|
|
131
|
+
// 交互模式下 NO 使用危险色高亮(审查 P2-3)
|
|
132
|
+
const confirmDanger = (message) => confirm(message, { danger: true });
|
|
130
133
|
|
|
131
134
|
if (showBanner) {
|
|
132
135
|
if (fancyOutput) {
|
|
@@ -497,7 +500,7 @@ export async function runCli({
|
|
|
497
500
|
installWorkflows: () => installCodexWfs({ codexDir, multiSelect, confirm, writeLine }),
|
|
498
501
|
configureProvider: async () => {
|
|
499
502
|
await ensureApiKey();
|
|
500
|
-
await configureCodex({ config, confirmConflict:
|
|
503
|
+
await configureCodex({ config, confirmConflict: confirmDanger, writeLine });
|
|
501
504
|
await markConfigured("codex");
|
|
502
505
|
writeLine("✓ 已写入 Codex claude360 provider/profile(~/.codex/config.toml)");
|
|
503
506
|
},
|
|
@@ -640,7 +643,7 @@ export async function runCli({
|
|
|
640
643
|
}
|
|
641
644
|
await markConfigured("codex");
|
|
642
645
|
writeLine("正在启动 Codex...");
|
|
643
|
-
await launchCodex({ config, confirmConflict:
|
|
646
|
+
await launchCodex({ config, confirmConflict: confirmDanger, writeLine });
|
|
644
647
|
return true;
|
|
645
648
|
}
|
|
646
649
|
|
|
@@ -651,7 +654,7 @@ export async function runCli({
|
|
|
651
654
|
return false;
|
|
652
655
|
}
|
|
653
656
|
await ensureApiKey();
|
|
654
|
-
await configureCodex({ config, confirmConflict:
|
|
657
|
+
await configureCodex({ config, confirmConflict: confirmDanger, writeLine });
|
|
655
658
|
await markConfigured("codex");
|
|
656
659
|
writeLine("✓ 已写入 Codex claude360 provider/profile(~/.codex/config.toml)");
|
|
657
660
|
return true;
|
package/src/init-config.js
CHANGED
|
@@ -205,7 +205,8 @@ export const FALLBACK_CLAUDE_ALIASES = [
|
|
|
205
205
|
];
|
|
206
206
|
|
|
207
207
|
// 后端接口 GET /api/cli/models?tool=<claude_code|codex>:
|
|
208
|
-
// { models: [{ id, display_name, description, tags, recommended
|
|
208
|
+
// { models: [{ id, display_name, description, tags, recommended }] }
|
|
209
|
+
// (后端暂无上下文长度数据源,契约中不含 context_length)
|
|
209
210
|
export async function loadAvailableModels(api, tool = "") {
|
|
210
211
|
if (api && typeof api.get === "function") {
|
|
211
212
|
try {
|
package/src/mcp-skill.js
CHANGED
|
@@ -78,16 +78,24 @@ export function resolveClaudeJsonPath({ homedir = os.homedir } = {}) {
|
|
|
78
78
|
return path.join(homedir(), ".claude.json");
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
// 安全边界:远程接口只允许下发展示层元数据(label / desc / recommended /
|
|
82
|
+
// platforms),安装命令(claudeArgs / codex)必须来自 CLI 内置受信清单;
|
|
83
|
+
// 未知 id 直接丢弃,防止后端被攻破或响应被篡改时执行任意命令。
|
|
84
|
+
export function normalizeRemoteMcp(mcp) {
|
|
85
|
+
if (!mcp || typeof mcp.id !== "string") {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const builtin = RECOMMENDED_MCPS.find((item) => item.id === mcp.id);
|
|
89
|
+
if (!builtin) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...builtin,
|
|
94
|
+
label: typeof mcp.label === "string" && mcp.label !== "" ? mcp.label : builtin.label,
|
|
95
|
+
desc: typeof mcp.desc === "string" ? mcp.desc : builtin.desc,
|
|
96
|
+
recommended: Boolean(mcp.recommended ?? builtin.recommended),
|
|
97
|
+
platforms: typeof mcp.platforms === "string" ? mcp.platforms : builtin.platforms,
|
|
98
|
+
};
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
// MCP 列表远程优先(优化需求第 4 节):GET /api/cli/mcps 返回
|
|
@@ -97,7 +105,9 @@ export async function loadRecommendedMcps(api) {
|
|
|
97
105
|
if (api && typeof api.get === "function") {
|
|
98
106
|
try {
|
|
99
107
|
const data = await api.get("/api/cli/mcps");
|
|
100
|
-
const mcps = (Array.isArray(data?.mcps) ? data.mcps : [])
|
|
108
|
+
const mcps = (Array.isArray(data?.mcps) ? data.mcps : [])
|
|
109
|
+
.map(normalizeRemoteMcp)
|
|
110
|
+
.filter(Boolean);
|
|
101
111
|
if (mcps.length > 0) {
|
|
102
112
|
return { mcps, source: "remote" };
|
|
103
113
|
}
|
package/src/menu.js
CHANGED
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
// 渲染默认无色(color: false)以保证测试与管道输出稳定,
|
|
5
5
|
// 真实终端下由 promptMenu 自动开启彩色分区样式。
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { displayWidth } from "./banner.js";
|
|
8
|
+
import { BOLD, RESET, colorLevel, fg, toLevel } from "./colors.js";
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
// 分区/选择键配色:按色彩深度 level 现算(真彩色保留 RGB,256 色量化到调色板)。
|
|
11
|
+
function palette(level) {
|
|
12
|
+
return {
|
|
13
|
+
rule: fg(71, 85, 105, level), // 分隔线:青灰
|
|
14
|
+
section: fg(34, 211, 238, level), // 分区标题:亮青
|
|
15
|
+
key: fg(125, 211, 252, level), // 选择键:天蓝
|
|
16
|
+
exit: fg(248, 113, 113, level), // 退出键:红
|
|
17
|
+
desc: fg(100, 116, 139, level), // 功能说明:暗灰
|
|
18
|
+
};
|
|
19
|
+
}
|
|
19
20
|
const MENU_RULE_WIDTH = 46;
|
|
20
21
|
|
|
21
22
|
export function buildFirstRunMenu() {
|
|
@@ -88,36 +89,41 @@ export function buildDailyMenu() {
|
|
|
88
89
|
|
|
89
90
|
// 分区标题渲染为 `─── 标题 ──────…` 形式的分隔线,让各组功能在视觉上彼此分开
|
|
90
91
|
function renderSectionRule(title, color) {
|
|
92
|
+
const level = toLevel(color);
|
|
91
93
|
const lead = "───";
|
|
92
94
|
const label = ` ${title} `;
|
|
93
95
|
const tail = "─".repeat(Math.max(4, MENU_RULE_WIDTH - lead.length - displayWidth(label)));
|
|
94
|
-
if (!
|
|
96
|
+
if (!level) {
|
|
95
97
|
return `${lead}${label}${tail}`;
|
|
96
98
|
}
|
|
97
|
-
|
|
99
|
+
const c = palette(level);
|
|
100
|
+
return `${c.rule}${lead}${RESET}${BOLD}${c.section}${label}${RESET}${c.rule}${tail}${RESET}`;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
function renderItem(item, color) {
|
|
104
|
+
const level = toLevel(color);
|
|
101
105
|
const desc = item.desc ? ` - ${item.desc}` : "";
|
|
102
|
-
if (!
|
|
106
|
+
if (!level) {
|
|
103
107
|
return ` ${item.key}. ${item.label}${desc}`;
|
|
104
108
|
}
|
|
105
|
-
const
|
|
106
|
-
const
|
|
109
|
+
const c = palette(level);
|
|
110
|
+
const keyColor = item.value === "exit" ? c.exit : c.key;
|
|
111
|
+
const coloredDesc = desc ? `${c.desc}${desc}${RESET}` : "";
|
|
107
112
|
return ` ${BOLD}${keyColor}${item.key}.${RESET} ${item.label}${coloredDesc}`;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
export function renderMenu(menu, { color = false } = {}) {
|
|
116
|
+
const level = toLevel(color);
|
|
111
117
|
const lines = [];
|
|
112
118
|
if (menu.title) {
|
|
113
|
-
lines.push(
|
|
119
|
+
lines.push(level ? `${BOLD}${menu.title}${RESET}` : menu.title, "");
|
|
114
120
|
}
|
|
115
121
|
for (const section of menu.sections) {
|
|
116
122
|
if (section.title) {
|
|
117
|
-
lines.push(renderSectionRule(section.title,
|
|
123
|
+
lines.push(renderSectionRule(section.title, level));
|
|
118
124
|
}
|
|
119
125
|
for (const item of section.items) {
|
|
120
|
-
lines.push(renderItem(item,
|
|
126
|
+
lines.push(renderItem(item, level));
|
|
121
127
|
}
|
|
122
128
|
lines.push("");
|
|
123
129
|
}
|
|
@@ -155,7 +161,7 @@ export async function promptMenu({ menu, promptInput, select, writeLine = consol
|
|
|
155
161
|
throw new Error("缺少菜单输入");
|
|
156
162
|
}
|
|
157
163
|
// 仅在输出未被测试替换且终端支持时启用彩色
|
|
158
|
-
const color = writeLine === console.log
|
|
164
|
+
const color = writeLine === console.log ? colorLevel() : 0;
|
|
159
165
|
while (true) {
|
|
160
166
|
writeLine(renderMenu(menu, { color }));
|
|
161
167
|
const answer = await promptInput("请输入选项");
|
package/src/prompts.js
CHANGED
|
@@ -20,21 +20,28 @@ import {
|
|
|
20
20
|
|
|
21
21
|
import { promptMultiSelect } from "./menu.js";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
const RESET = `${ESC}0m`;
|
|
25
|
-
const BOLD = `${ESC}1m`;
|
|
26
|
-
const fg = (r, g, b) => `${ESC}38;2;${r};${g};${b}m`;
|
|
27
|
-
const CYAN = fg(34, 211, 238); // 品牌高亮:选中项
|
|
28
|
-
const RED = fg(248, 113, 113); // 危险操作:NO 高亮
|
|
29
|
-
const GRAY = fg(100, 116, 139); // 次要说明 / 未选中项
|
|
30
|
-
const GREEN = fg(74, 222, 128); // 完成态答案
|
|
23
|
+
import { BOLD, RESET, colorLevel, fg, toLevel } from "./colors.js";
|
|
31
24
|
|
|
32
|
-
|
|
25
|
+
// 交互配色:按色彩深度 level 现算(真彩色保留 RGB,256 色量化到调色板)。
|
|
26
|
+
function palette(level) {
|
|
27
|
+
return {
|
|
28
|
+
cyan: fg(34, 211, 238, level), // 品牌高亮:选中项
|
|
29
|
+
red: fg(248, 113, 113, level), // 危险操作:NO 高亮
|
|
30
|
+
gray: fg(100, 116, 139, level), // 次要说明 / 未选中项
|
|
31
|
+
green: fg(74, 222, 128, level), // 完成态答案
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const paint = (level, sgr, text) => (level ? `${sgr}${text}${RESET}` : text);
|
|
33
36
|
|
|
34
37
|
export function isInteractive(stream = process.stdout) {
|
|
35
38
|
if (process.env.CI) {
|
|
36
39
|
return false;
|
|
37
40
|
}
|
|
41
|
+
// TERM=dumb 的终端可能是 TTY,但不支持方向键 UI 与 ANSI 光标控制
|
|
42
|
+
if (process.env.TERM === "dumb") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
38
45
|
return Boolean(stream && stream.isTTY && process.stdin.isTTY);
|
|
39
46
|
}
|
|
40
47
|
|
|
@@ -47,12 +54,14 @@ export function resolveEscValue(choices = []) {
|
|
|
47
54
|
// 确认行:选中项大写、加粗、高亮并带箭头;未选中项小写灰色(需求第四节)。
|
|
48
55
|
// 终端无法局部放大字体,用大小写 + 颜色 + 箭头模拟视觉差异。
|
|
49
56
|
export function renderConfirmLine(yesActive, { color = true, danger = false } = {}) {
|
|
57
|
+
const level = toLevel(color);
|
|
58
|
+
const c = palette(level);
|
|
50
59
|
const yes = yesActive
|
|
51
|
-
? paint(
|
|
52
|
-
: paint(
|
|
60
|
+
? paint(level,`${BOLD}${c.cyan}`, "▶ YES ◀")
|
|
61
|
+
: paint(level,c.gray, "yes");
|
|
53
62
|
const no = !yesActive
|
|
54
|
-
? paint(
|
|
55
|
-
: paint(
|
|
63
|
+
? paint(level,`${BOLD}${danger ? c.red : c.cyan}`, "▶ NO ◀")
|
|
64
|
+
: paint(level,c.gray, "no");
|
|
56
65
|
return ` ${yes} ${no}`;
|
|
57
66
|
}
|
|
58
67
|
|
|
@@ -62,12 +71,30 @@ export function renderConfirmLine(yesActive, { color = true, danger = false } =
|
|
|
62
71
|
|
|
63
72
|
const PAGE_SIZE = 14;
|
|
64
73
|
|
|
74
|
+
// 防御:调用方传入空列表或全部为分隔行时,方向键取模运算会得到 NaN 或
|
|
75
|
+
// 进入同步死循环,回车会访问 undefined.value。统一在渲染前抛出可读错误。
|
|
76
|
+
function getSelectableIndexes(choices = []) {
|
|
77
|
+
return choices
|
|
78
|
+
.map((choice, index) => (choice && !choice.separator ? index : -1))
|
|
79
|
+
.filter((index) => index >= 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function assertSelectableChoices(message, choices) {
|
|
83
|
+
const indexes = getSelectableIndexes(choices);
|
|
84
|
+
if (indexes.length === 0) {
|
|
85
|
+
throw new Error(`${message || "选择列表"}缺少可选择项`);
|
|
86
|
+
}
|
|
87
|
+
return indexes;
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
// 单选:方向键移动(跳过分隔行)、回车确认、Esc 返回 escValue
|
|
66
91
|
export const interactiveSelect = createPrompt((config, done) => {
|
|
67
|
-
const { message, choices, escValue, color = true } = config;
|
|
92
|
+
const { message, choices = [], escValue, color = true } = config;
|
|
93
|
+
const level = toLevel(color);
|
|
94
|
+
const c = palette(level);
|
|
95
|
+
const selectable = assertSelectableChoices(message, choices);
|
|
68
96
|
const [status, setStatus] = useState("idle");
|
|
69
|
-
const
|
|
70
|
-
const [active, setActive] = useState(Math.max(firstIndex, 0));
|
|
97
|
+
const [active, setActive] = useState(selectable[0]);
|
|
71
98
|
|
|
72
99
|
useKeypress((key) => {
|
|
73
100
|
if (status === "done") {
|
|
@@ -79,12 +106,9 @@ export const interactiveSelect = createPrompt((config, done) => {
|
|
|
79
106
|
return;
|
|
80
107
|
}
|
|
81
108
|
if (isUpKey(key) || isDownKey(key)) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
next = (next + direction + choices.length) % choices.length;
|
|
86
|
-
} while (choices[next].separator);
|
|
87
|
-
setActive(next);
|
|
109
|
+
const delta = isUpKey(key) ? -1 : 1;
|
|
110
|
+
const pos = selectable.indexOf(active);
|
|
111
|
+
setActive(selectable[(pos + delta + selectable.length) % selectable.length]);
|
|
88
112
|
return;
|
|
89
113
|
}
|
|
90
114
|
if (key.name === "escape" && escValue !== undefined) {
|
|
@@ -94,7 +118,7 @@ export const interactiveSelect = createPrompt((config, done) => {
|
|
|
94
118
|
});
|
|
95
119
|
|
|
96
120
|
if (status === "done") {
|
|
97
|
-
return `${paint(
|
|
121
|
+
return `${paint(level,BOLD, message)} ${paint(level,c.green, choices[active]?.separator ? "" : String(choices[active]?.label ?? ""))}`;
|
|
98
122
|
}
|
|
99
123
|
|
|
100
124
|
const page = usePagination({
|
|
@@ -104,22 +128,27 @@ export const interactiveSelect = createPrompt((config, done) => {
|
|
|
104
128
|
loop: false,
|
|
105
129
|
renderItem({ item, isActive }) {
|
|
106
130
|
if (item.separator) {
|
|
107
|
-
return paint(
|
|
131
|
+
return paint(level,c.gray, item.separator);
|
|
108
132
|
}
|
|
109
|
-
const hint = item.hint ? ` ${paint(
|
|
133
|
+
const hint = item.hint ? ` ${paint(level,c.gray, item.hint)}` : "";
|
|
110
134
|
if (isActive) {
|
|
111
|
-
return `${paint(
|
|
135
|
+
return `${paint(level,`${BOLD}${c.cyan}`, `❯ ${item.label}`)}${hint}`;
|
|
112
136
|
}
|
|
113
137
|
return ` ${item.label}${hint}`;
|
|
114
138
|
},
|
|
115
139
|
});
|
|
116
|
-
const help = paint(
|
|
117
|
-
return `${paint(
|
|
140
|
+
const help = paint(level,c.gray, `↑↓ 移动 · Enter 确认${escValue !== undefined ? " · Esc 返回" : ""}`);
|
|
141
|
+
return `${paint(level,BOLD, message)}\n${page}\n${help}`;
|
|
118
142
|
});
|
|
119
143
|
|
|
120
144
|
// 多选:空格选择/取消、a 全选、i 反选、回车确认、Esc 取消(需求第 4 节)
|
|
121
145
|
export const interactiveMultiSelect = createPrompt((config, done) => {
|
|
122
|
-
const { message, choices, preselected = [], color = true } = config;
|
|
146
|
+
const { message, choices = [], preselected = [], color = true } = config;
|
|
147
|
+
const level = toLevel(color);
|
|
148
|
+
const c = palette(level);
|
|
149
|
+
if (choices.length === 0) {
|
|
150
|
+
throw new Error(`${message || "多选列表"}缺少可选择项`);
|
|
151
|
+
}
|
|
123
152
|
const [status, setStatus] = useState("idle");
|
|
124
153
|
const [active, setActive] = useState(0);
|
|
125
154
|
const [selected, setSelected] = useState(
|
|
@@ -169,7 +198,7 @@ export const interactiveMultiSelect = createPrompt((config, done) => {
|
|
|
169
198
|
});
|
|
170
199
|
|
|
171
200
|
if (status === "done") {
|
|
172
|
-
return `${paint(
|
|
201
|
+
return `${paint(level,BOLD, message)} ${paint(level,c.green, `已选 ${selected.size} 项`)}`;
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
const page = usePagination({
|
|
@@ -178,20 +207,22 @@ export const interactiveMultiSelect = createPrompt((config, done) => {
|
|
|
178
207
|
pageSize: PAGE_SIZE,
|
|
179
208
|
loop: false,
|
|
180
209
|
renderItem({ item, isActive }) {
|
|
181
|
-
const mark = selected.has(item.value) ? paint(
|
|
182
|
-
const hint = item.hint ? ` ${paint(
|
|
183
|
-
const label = isActive ? paint(
|
|
210
|
+
const mark = selected.has(item.value) ? paint(level,c.green, "[x]") : "[ ]";
|
|
211
|
+
const hint = item.hint ? ` ${paint(level,c.gray, item.hint)}` : "";
|
|
212
|
+
const label = isActive ? paint(level,`${BOLD}${c.cyan}`, `❯ ${mark} ${item.label}`) : ` ${mark} ${item.label}`;
|
|
184
213
|
return `${label}${hint}`;
|
|
185
214
|
},
|
|
186
215
|
});
|
|
187
|
-
const help = paint(
|
|
188
|
-
return `${paint(
|
|
216
|
+
const help = paint(level,c.gray, "↑↓ 移动 · 空格 选择 · a 全选 · i 反选 · Enter 确认 · Esc 取消");
|
|
217
|
+
return `${paint(level,BOLD, message)}\n${page}\n${help}`;
|
|
189
218
|
});
|
|
190
219
|
|
|
191
220
|
// 确认:左右方向键 / Tab 切换 YES、NO,回车确认,y / n 快捷,Esc 取消。
|
|
192
221
|
// 默认值由调用方按风险设置:普通继续默认 YES,覆盖 / 删除 / 重置默认 NO。
|
|
193
222
|
export const interactiveConfirm = createPrompt((config, done) => {
|
|
194
223
|
const { message, defaultYes = false, danger = false, color = true } = config;
|
|
224
|
+
const level = toLevel(color);
|
|
225
|
+
const c = palette(level);
|
|
195
226
|
const [status, setStatus] = useState("idle");
|
|
196
227
|
const [yes, setYes] = useState(Boolean(defaultYes));
|
|
197
228
|
|
|
@@ -223,10 +254,10 @@ export const interactiveConfirm = createPrompt((config, done) => {
|
|
|
223
254
|
});
|
|
224
255
|
|
|
225
256
|
if (status === "done") {
|
|
226
|
-
return `${paint(
|
|
257
|
+
return `${paint(level,BOLD, message)} ${paint(level,c.green, yes ? "YES" : "NO")}`;
|
|
227
258
|
}
|
|
228
|
-
const help = paint(
|
|
229
|
-
return `${paint(
|
|
259
|
+
const help = paint(level,c.gray, "←→ 切换 · Enter 确认 · Esc 取消");
|
|
260
|
+
return `${paint(level,BOLD, message)}\n\n${renderConfirmLine(yes, { color, danger })}\n\n${help}`;
|
|
230
261
|
});
|
|
231
262
|
|
|
232
263
|
// ──────────────────────────────────────────────
|
|
@@ -299,7 +330,7 @@ async function guardExit(run) {
|
|
|
299
330
|
|
|
300
331
|
export function createPrompts({
|
|
301
332
|
interactive = isInteractive(),
|
|
302
|
-
color =
|
|
333
|
+
color = colorLevel(),
|
|
303
334
|
promptInput,
|
|
304
335
|
writeLine,
|
|
305
336
|
} = {}) {
|
package/src/ui.js
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
import { displayWidth } from "./banner.js";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
import { BOLD, RESET, fg, toLevel } from "./colors.js";
|
|
9
|
+
|
|
10
|
+
// 表格/标题配色:按色彩深度 level 现算(真彩色保留 RGB,256 色量化到调色板)。
|
|
11
|
+
function palette(level) {
|
|
12
|
+
return {
|
|
13
|
+
border: fg(71, 85, 105, level), // 边框:青灰
|
|
14
|
+
head: fg(125, 211, 252, level), // 表头:天蓝
|
|
15
|
+
};
|
|
16
|
+
}
|
|
14
17
|
|
|
15
18
|
// eslint-disable-next-line no-control-regex
|
|
16
19
|
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
@@ -47,17 +50,18 @@ function padCell(text, width) {
|
|
|
47
50
|
return `${text}${" ".repeat(gap)}`;
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
function borderLine(widths, [left, mid, right],
|
|
53
|
+
function borderLine(widths, [left, mid, right], level) {
|
|
51
54
|
const line = `${left}${widths.map((w) => "─".repeat(w + 2)).join(mid)}${right}`;
|
|
52
|
-
return
|
|
55
|
+
return level ? `${palette(level).border}${line}${RESET}` : line;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
function contentLine(cells, widths, {
|
|
56
|
-
const
|
|
58
|
+
function contentLine(cells, widths, { level = 0, bold = false } = {}) {
|
|
59
|
+
const c = level ? palette(level) : null;
|
|
60
|
+
const bar = level ? `${c.border}│${RESET}` : "│";
|
|
57
61
|
const body = cells
|
|
58
62
|
.map((cell, i) => {
|
|
59
63
|
const padded = padCell(cell, widths[i]);
|
|
60
|
-
return bold &&
|
|
64
|
+
return bold && level ? ` ${BOLD}${c.head}${padded}${RESET} ` : ` ${padded} `;
|
|
61
65
|
})
|
|
62
66
|
.join(bar);
|
|
63
67
|
return `${bar}${body}${bar}`;
|
|
@@ -93,17 +97,18 @@ function resolveWidths(head, rows, width) {
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
export function renderTable({ head = [], rows = [] } = {}, { color = false, width = 0 } = {}) {
|
|
100
|
+
const level = toLevel(color);
|
|
96
101
|
const widths = resolveWidths(head, rows, width);
|
|
97
102
|
const fit = (row) => widths.map((w, i) => truncateDisplay(row[i] ?? "", w));
|
|
98
|
-
const lines = [borderLine(widths, ["┌", "┬", "┐"],
|
|
103
|
+
const lines = [borderLine(widths, ["┌", "┬", "┐"], level)];
|
|
99
104
|
if (head.length > 0) {
|
|
100
|
-
lines.push(contentLine(fit(head), widths, {
|
|
101
|
-
lines.push(borderLine(widths, ["├", "┼", "┤"],
|
|
105
|
+
lines.push(contentLine(fit(head), widths, { level, bold: true }));
|
|
106
|
+
lines.push(borderLine(widths, ["├", "┼", "┤"], level));
|
|
102
107
|
}
|
|
103
108
|
for (const row of rows) {
|
|
104
|
-
lines.push(contentLine(fit(row), widths, {
|
|
109
|
+
lines.push(contentLine(fit(row), widths, { level }));
|
|
105
110
|
}
|
|
106
|
-
lines.push(borderLine(widths, ["└", "┴", "┘"],
|
|
111
|
+
lines.push(borderLine(widths, ["└", "┴", "┘"], level));
|
|
107
112
|
return lines.join("\n");
|
|
108
113
|
}
|
|
109
114
|
|
|
@@ -127,13 +132,15 @@ export function renderStatusTable({ head = [], row = [] } = {}, { color = false,
|
|
|
127
132
|
|
|
128
133
|
// 章节标题:与 menu.js 分区分隔线风格一致
|
|
129
134
|
export function renderSectionTitle(title, { color = false, width = 46 } = {}) {
|
|
135
|
+
const level = toLevel(color);
|
|
130
136
|
const lead = "───";
|
|
131
137
|
const label = ` ${title} `;
|
|
132
138
|
const tail = "─".repeat(Math.max(4, width - lead.length - displayWidth(label)));
|
|
133
|
-
if (!
|
|
139
|
+
if (!level) {
|
|
134
140
|
return `${lead}${label}${tail}`;
|
|
135
141
|
}
|
|
136
|
-
|
|
142
|
+
const c = palette(level);
|
|
143
|
+
return `${c.border}${lead}${RESET}${BOLD}${c.head}${label}${RESET}${c.border}${tail}${RESET}`;
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
// ──────────────────────────────────────────────
|
|
@@ -142,18 +149,20 @@ export function renderSectionTitle(title, { color = false, width = 46 } = {}) {
|
|
|
142
149
|
|
|
143
150
|
// 页面标题区:品牌名 + 当前页面名,统一边框样式
|
|
144
151
|
export function renderHeader(title, { subtitle = "", color = false } = {}) {
|
|
152
|
+
const level = toLevel(color);
|
|
153
|
+
const c = level ? palette(level) : null;
|
|
145
154
|
const label = `Claude360 CLI · ${title}`;
|
|
146
155
|
const inner = Math.max(cellWidth(label), cellWidth(subtitle)) + 2;
|
|
147
156
|
const line = (text) => {
|
|
148
157
|
const body = ` ${padCell(text, inner - 2)} `;
|
|
149
|
-
if (!
|
|
158
|
+
if (!level) {
|
|
150
159
|
return `│${body}│`;
|
|
151
160
|
}
|
|
152
|
-
return `${
|
|
161
|
+
return `${c.border}│${RESET}${BOLD}${c.head}${body}${RESET}${c.border}│${RESET}`;
|
|
153
162
|
};
|
|
154
163
|
const edge = (left, right) => {
|
|
155
164
|
const text = `${left}${"─".repeat(inner)}${right}`;
|
|
156
|
-
return
|
|
165
|
+
return level ? `${c.border}${text}${RESET}` : text;
|
|
157
166
|
};
|
|
158
167
|
const lines = [edge("┌", "┐"), line(label)];
|
|
159
168
|
if (subtitle) {
|
|
@@ -165,15 +174,20 @@ export function renderHeader(title, { subtitle = "", color = false } = {}) {
|
|
|
165
174
|
|
|
166
175
|
// 信息盒子:info / warn / error / success 四种状态,
|
|
167
176
|
// 颜色只增强层级,状态始终有文字前缀(需求二「不依赖颜色表达唯一信息」)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
// 四种状态:文字前缀(mark)始终存在,颜色仅增强层级(需求二「不依赖颜色表达唯一信息」)
|
|
178
|
+
const BOX_MARKS = { info: "i", warn: "!", error: "×", success: "✓" };
|
|
179
|
+
const BOX_RGB = {
|
|
180
|
+
info: [125, 211, 252],
|
|
181
|
+
warn: [250, 204, 21],
|
|
182
|
+
error: [248, 113, 113],
|
|
183
|
+
success: [74, 222, 128],
|
|
173
184
|
};
|
|
174
185
|
|
|
175
186
|
export function renderBox(message, { kind = "info", color = false, width = 0 } = {}) {
|
|
176
|
-
const
|
|
187
|
+
const level = toLevel(color);
|
|
188
|
+
const mark = BOX_MARKS[kind] || BOX_MARKS.info;
|
|
189
|
+
const [r, g, b] = BOX_RGB[kind] || BOX_RGB.info;
|
|
190
|
+
const kindColor = fg(r, g, b, level);
|
|
177
191
|
const rawLines = String(message ?? "").split("\n");
|
|
178
192
|
const textLines = rawLines.map((line, index) => (index === 0 ? `${mark} ${line}` : ` ${line}`));
|
|
179
193
|
const maxAvailable = width > 4 ? width - 4 : 0;
|
|
@@ -181,26 +195,27 @@ export function renderBox(message, { kind = "info", color = false, width = 0 } =
|
|
|
181
195
|
const inner = Math.max(...fitted.map((line) => cellWidth(line))) + 2;
|
|
182
196
|
const edge = (left, right) => {
|
|
183
197
|
const text = `${left}${"─".repeat(inner)}${right}`;
|
|
184
|
-
return
|
|
198
|
+
return level ? `${kindColor}${text}${RESET}` : text;
|
|
185
199
|
};
|
|
186
200
|
const lines = [edge("┌", "┐")];
|
|
187
201
|
for (const line of fitted) {
|
|
188
202
|
const body = ` ${padCell(line, inner - 2)} `;
|
|
189
|
-
lines.push(
|
|
203
|
+
lines.push(level ? `${kindColor}│${RESET}${body}${kindColor}│${RESET}` : `│${body}│`);
|
|
190
204
|
}
|
|
191
205
|
lines.push(edge("└", "┘"));
|
|
192
206
|
return lines.join("\n");
|
|
193
207
|
}
|
|
194
208
|
|
|
195
|
-
// 模型表(优化需求第 3
|
|
209
|
+
// 模型表(优化需求第 3 节):展示后端返回的真实模型信息。
|
|
210
|
+
// 字段契约与 /api/cli/models 对齐:id / display_name / tags / description;
|
|
211
|
+
// 后端暂无上下文长度数据源,不渲染“上下文”列,名称列回退模型 ID。
|
|
196
212
|
export function renderModelTable(models = [], { color = false, width = 0 } = {}) {
|
|
197
213
|
return renderTable({
|
|
198
|
-
head: ["模型 ID", "名称", "标签", "
|
|
214
|
+
head: ["模型 ID", "名称", "标签", "说明"],
|
|
199
215
|
rows: models.map((model) => [
|
|
200
216
|
model.id ?? "-",
|
|
201
|
-
model.display_name || "-",
|
|
217
|
+
model.display_name || model.id || "-",
|
|
202
218
|
Array.isArray(model.tags) && model.tags.length > 0 ? model.tags.join(",") : "-",
|
|
203
|
-
model.context_length ? String(model.context_length) : "-",
|
|
204
219
|
model.description || "-",
|
|
205
220
|
]),
|
|
206
221
|
}, { color, width });
|