claude360 0.2.9 → 0.3.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/bin/claude360.js +7 -3
- package/package.json +1 -1
- package/src/banner.js +41 -0
- package/src/cc-switch.js +23 -1
- package/src/diagnostics.js +42 -18
- package/src/glyphs.js +33 -0
- package/src/index.js +167 -48
- package/src/init-config.js +22 -2
- package/src/init-flow.js +16 -4
- package/src/mcp-skill.js +16 -0
- package/src/menu.js +21 -10
- package/src/notices.js +33 -0
- package/src/prompts.js +3 -2
- package/src/token-manager.js +90 -20
- package/src/tool-installer.js +25 -9
- package/src/tool-launcher.js +43 -2
- package/src/topup.js +49 -3
- package/src/ui.js +255 -0
- package/src/zcf-notice.js +15 -1
package/bin/claude360.js
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
import { runCli } from "../src/index.js";
|
|
4
4
|
import { safeErrorMessage } from "../src/sanitize.js";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const rawArgs = process.argv.slice(2);
|
|
7
|
+
const verbose = rawArgs.includes("--verbose") || rawArgs.includes("-v");
|
|
8
|
+
// 仅取位置参数作为命令;--verbose/-v 等 flag 单独解析,避免 "doctor --verbose"
|
|
9
|
+
// 被当成未知命令(现有命令均不以 - 开头,按 - 前缀剥离 flag 安全)。
|
|
10
|
+
const command = rawArgs.filter((arg) => !arg.startsWith("-")).join(" ");
|
|
7
11
|
|
|
8
12
|
try {
|
|
9
|
-
await runCli({ command, forceSetup: command === "setup" });
|
|
13
|
+
await runCli({ command, verbose, forceSetup: command === "setup" });
|
|
10
14
|
} catch (error) {
|
|
11
|
-
console.error(safeErrorMessage(error));
|
|
15
|
+
console.error(verbose && error?.stack ? error.stack : safeErrorMessage(error));
|
|
12
16
|
process.exitCode = 1;
|
|
13
17
|
}
|
package/package.json
CHANGED
package/src/banner.js
CHANGED
|
@@ -46,7 +46,48 @@ function gradientAt(t) {
|
|
|
46
46
|
];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// 默认按 emoji 呈现(2 列宽)的 BMP 杂项符号(Unicode Emoji_Presentation=Yes 的 BMP 部分)。
|
|
50
|
+
// 这些符号即便不带变体选择符,主流终端也按 2 列渲染:✅ U+2705、❌ U+274C、⏳ U+23F3 等。
|
|
51
|
+
// 关键:同区间内的窄符号 —— ✓ U+2713、✔ U+2714、▶ U+25B6、○ U+25CB、→ U+2192、× U+00D7
|
|
52
|
+
// —— 不在此集合,仍按 1 列处理,保证步骤标记与 messages 行内符号的既有对齐不变。
|
|
53
|
+
const BMP_EMOJI_WIDE = new Set();
|
|
54
|
+
for (const [start, end] of [
|
|
55
|
+
[0x231a, 0x231b], [0x23e9, 0x23ec], [0x23f0, 0x23f0], [0x23f3, 0x23f3],
|
|
56
|
+
[0x25fd, 0x25fe], [0x2614, 0x2615], [0x2648, 0x2653], [0x267f, 0x267f],
|
|
57
|
+
[0x2693, 0x2693], [0x26a1, 0x26a1], [0x26aa, 0x26ab], [0x26bd, 0x26be],
|
|
58
|
+
[0x26c4, 0x26c5], [0x26ce, 0x26ce], [0x26d4, 0x26d4], [0x26ea, 0x26ea],
|
|
59
|
+
[0x26f2, 0x26f3], [0x26f5, 0x26f5], [0x26fa, 0x26fa], [0x26fd, 0x26fd],
|
|
60
|
+
[0x2705, 0x2705], [0x270a, 0x270b], [0x2728, 0x2728], [0x274c, 0x274c],
|
|
61
|
+
[0x274e, 0x274e], [0x2753, 0x2755], [0x2757, 0x2757], [0x2795, 0x2797],
|
|
62
|
+
[0x27b0, 0x27b0], [0x27bf, 0x27bf], [0x2b1b, 0x2b1c], [0x2b50, 0x2b50],
|
|
63
|
+
[0x2b55, 0x2b55],
|
|
64
|
+
]) {
|
|
65
|
+
for (let cp = start; cp <= end; cp += 1) {
|
|
66
|
+
BMP_EMOJI_WIDE.add(cp);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
function charDisplayWidth(codePoint) {
|
|
71
|
+
// 变体选择符 VS16(U+FE0F)把前一个 BMP 符号强制为 emoji 呈现(2 列)。逐字符累加无法
|
|
72
|
+
// 回溯前字符,故给 VS16 计 1 列,补足基础符 1→2 的差值(⚙️ U+2699+FE0F、⚠️ ℹ️ 同理)。
|
|
73
|
+
if (codePoint === 0xfe0f) {
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
// 零宽连接符(U+200D,emoji 序列拼接)不占列宽。
|
|
77
|
+
if (codePoint === 0x200d) {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
// SMP emoji 平面(🚀 🎉 📦 🔐 等 U+1Fxxx)与 CJK 扩展区按 2 列宽渲染。
|
|
81
|
+
if (
|
|
82
|
+
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
|
|
83
|
+
(codePoint >= 0x20000 && codePoint <= 0x3fffd)
|
|
84
|
+
) {
|
|
85
|
+
return 2;
|
|
86
|
+
}
|
|
87
|
+
// BMP 默认 emoji 呈现的宽符号(✅ ❌ ⏳ 等)。
|
|
88
|
+
if (BMP_EMOJI_WIDE.has(codePoint)) {
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
50
91
|
return (
|
|
51
92
|
(codePoint >= 0x1100 && codePoint <= 0x115f) ||
|
|
52
93
|
(codePoint >= 0x2e80 && codePoint <= 0x303e) ||
|
package/src/cc-switch.js
CHANGED
|
@@ -6,6 +6,7 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { colorLevel } from "./colors.js";
|
|
8
8
|
import { createMessenger } from "./messages.js";
|
|
9
|
+
import { summary, warningBlock } from "./ui.js";
|
|
9
10
|
|
|
10
11
|
// 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
|
|
11
12
|
const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
@@ -94,6 +95,22 @@ export function maskCcSwitchConfig(config, apiKey) {
|
|
|
94
95
|
return JSON.parse(json.split(JSON.stringify(apiKey)).join(JSON.stringify(masked)));
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
function formatCcSwitchSummary(target, config, apiKey) {
|
|
99
|
+
const maskedKey = maskKeyForPreview(apiKey);
|
|
100
|
+
const rows = [
|
|
101
|
+
["目标", target],
|
|
102
|
+
["名称", config.name || "Claude360"],
|
|
103
|
+
];
|
|
104
|
+
if (Array.isArray(config.items)) {
|
|
105
|
+
rows.push(["配置项", config.items.map((item) => item.type || item.name).join(", ")]);
|
|
106
|
+
} else {
|
|
107
|
+
rows.push(["类型", config.type || "-"]);
|
|
108
|
+
rows.push(["Base URL", config.base_url || "-"]);
|
|
109
|
+
}
|
|
110
|
+
rows.push(["API Key", maskedKey || "-"]);
|
|
111
|
+
return ["cc-switch 配置摘要", "", summary(rows)].join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
97
114
|
export function resolveCcSwitchSavePath({
|
|
98
115
|
platform = process.platform,
|
|
99
116
|
env = process.env,
|
|
@@ -190,11 +207,16 @@ export async function runCcSwitchGenerator({
|
|
|
190
207
|
|
|
191
208
|
if (reveal === "masked") {
|
|
192
209
|
const maskedConfig = maskCcSwitchConfig(fullConfig, apiKey);
|
|
193
|
-
writeLine(
|
|
210
|
+
writeLine(formatCcSwitchSummary(target, maskedConfig, apiKey));
|
|
194
211
|
msg.hint("提示:脱敏配置仅用于预览,无法直接使用,也不会保存到文件。");
|
|
195
212
|
return { done: true, target, masked: true, saved: false };
|
|
196
213
|
}
|
|
197
214
|
|
|
215
|
+
writeLine(warningBlock("即将显示完整 API Key", {
|
|
216
|
+
action: "在终端输出完整 cc-switch 配置",
|
|
217
|
+
impact: "当前终端可见范围内的人可能看到 API Key",
|
|
218
|
+
}));
|
|
219
|
+
writeLine("");
|
|
198
220
|
writeLine(JSON.stringify(fullConfig, null, 2));
|
|
199
221
|
msg.info("请将以上配置复制到 cc-switch 中使用。");
|
|
200
222
|
|
package/src/diagnostics.js
CHANGED
|
@@ -5,8 +5,7 @@ import os from "node:os";
|
|
|
5
5
|
|
|
6
6
|
import { resolveCodexConfigPath, resolveCodexProfileConfigPath } from "./tool-launcher.js";
|
|
7
7
|
import { sanitizeError, sanitizeText } from "./sanitize.js";
|
|
8
|
-
import {
|
|
9
|
-
import { renderHeader, renderSectionTitle, renderTable } from "./ui.js";
|
|
8
|
+
import { renderChoiceTable, renderDivider, renderSectionTitle, renderTaskEnd, renderTaskStart } from "./ui.js";
|
|
10
9
|
|
|
11
10
|
export async function runDiagnostics({
|
|
12
11
|
config = {},
|
|
@@ -158,7 +157,7 @@ function buildCodexCompatStatus({ codexCompat }) {
|
|
|
158
157
|
// 诊断报告(补充需求第 5 节):按模块分组的小表格展示,
|
|
159
158
|
// 状态用文字标签(通过/警告/失败)明确区分,失败项附修复建议,
|
|
160
159
|
// 输出适合直接复制给客服或开发排查(出口统一脱敏)。
|
|
161
|
-
export function formatDiagnosticsSummary(report, { width = 0, color = false } = {}) {
|
|
160
|
+
export function formatDiagnosticsSummary(report, { width = 0, color = false, verbose = false } = {}) {
|
|
162
161
|
const groups = [
|
|
163
162
|
{
|
|
164
163
|
title: "系统环境",
|
|
@@ -198,31 +197,56 @@ export function formatDiagnosticsSummary(report, { width = 0, color = false } =
|
|
|
198
197
|
},
|
|
199
198
|
];
|
|
200
199
|
|
|
201
|
-
const lines = [
|
|
200
|
+
const lines = [renderTaskStart("运行诊断", {
|
|
201
|
+
intro: ["检查系统环境", "检查 Claude360 账号与余额", "检查 Claude Code / Codex 配置", "汇总问题与建议"],
|
|
202
|
+
})];
|
|
203
|
+
let rowNumber = 1;
|
|
204
|
+
const allFixes = [];
|
|
202
205
|
for (const group of groups) {
|
|
203
206
|
lines.push("", renderSectionTitle(group.title, { color }));
|
|
204
|
-
lines.push(
|
|
205
|
-
|
|
206
|
-
rows: group.items.map(({ label, item, valueKey = "detail" }) =>
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
207
|
+
lines.push(renderChoiceTable({
|
|
208
|
+
columns: [["no", "序号"], ["check", "检查项"], ["status", "状态"], ["result", "结果"]],
|
|
209
|
+
rows: group.items.map(({ label, item, valueKey = "detail" }) => {
|
|
210
|
+
const row = {
|
|
211
|
+
no: `[${String(rowNumber).padStart(2, "0")}]`,
|
|
212
|
+
check: label,
|
|
213
|
+
status: statusLabel(item),
|
|
214
|
+
result: String(item[valueKey] || item.detail || "-"),
|
|
215
|
+
};
|
|
216
|
+
rowNumber += 1;
|
|
217
|
+
return row;
|
|
218
|
+
}),
|
|
219
|
+
}, { width, color, titleKey: "check", cardBreakpoint: 64 }));
|
|
220
|
+
allFixes.push(...group.items
|
|
221
|
+
.filter(({ item, fix }) => !item.ok && fix)
|
|
222
|
+
.map(({ label, item, fix }) => ({ label, item, fix })));
|
|
223
|
+
}
|
|
224
|
+
lines.push("", `问题数量:${allFixes.length}`);
|
|
225
|
+
if (allFixes.length > 0) {
|
|
226
|
+
lines.push("", "建议:");
|
|
227
|
+
allFixes.forEach(({ label, item, fix }, index) => {
|
|
228
|
+
lines.push(`${index + 1}. ${statusLabel(item)} ${label}:${fix}`);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
// 详细模式(claude360 doctor --verbose):附关键环境变量,最终 sanitizeText 兜底脱敏
|
|
232
|
+
if (verbose) {
|
|
233
|
+
lines.push("", renderDivider("light"), "环境变量(关键项):");
|
|
234
|
+
for (const key of ["PATH", "NODE_OPTIONS", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "npm_config_registry"]) {
|
|
235
|
+
lines.push(`[${key}] ${process.env[key] || "-"}`);
|
|
215
236
|
}
|
|
216
237
|
}
|
|
238
|
+
lines.push("", renderTaskEnd("诊断完成", {
|
|
239
|
+
summary: [["问题数量", String(allFixes.length)]],
|
|
240
|
+
}));
|
|
217
241
|
// 出口兜底:report 由调用方注入,逐项 detail 可能未经脱敏
|
|
218
242
|
return sanitizeText(lines.join("\n"));
|
|
219
243
|
}
|
|
220
244
|
|
|
221
|
-
function statusLabel(item
|
|
245
|
+
function statusLabel(item) {
|
|
222
246
|
if (item.ok) {
|
|
223
|
-
return
|
|
247
|
+
return "✅ 正常";
|
|
224
248
|
}
|
|
225
|
-
return item.warn ?
|
|
249
|
+
return item.warn ? "⚠️ 注意" : "❌ 异常";
|
|
226
250
|
}
|
|
227
251
|
|
|
228
252
|
function defaultPlatform() {
|
package/src/glyphs.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// emoji 语义符号单一事实源(优化需求 V2.0 §4):执行流程中的任务 / 步骤 /
|
|
2
|
+
// 状态 / 资源类别统一从这里取符号,避免散落硬编码。emoji 作为语义符号「始终
|
|
3
|
+
// 输出」(不依赖颜色表达唯一信息),无色档与管道同样保留,保证信息层级可辨。
|
|
4
|
+
// 注意:部分符号带变体选择符 U+FE0F(⚠️ ℹ️),在多数现代终端按 2 列宽渲染,
|
|
5
|
+
// 宽度计算见 banner.displayWidth;新增符号若落在新的 Unicode 区间,需同步其
|
|
6
|
+
// emoji 宽度判定,避免表格 / 边框错位。
|
|
7
|
+
|
|
8
|
+
export const GLYPH = {
|
|
9
|
+
task: "🚀", // 任务开始
|
|
10
|
+
check: "🔎", // 检查环境 / 版本 / 配置
|
|
11
|
+
install: "📦", // 安装 / 更新工具
|
|
12
|
+
config: "⚙️", // 写入配置 / 修改设置
|
|
13
|
+
auth: "🔐", // 登录授权 / Token 检查
|
|
14
|
+
net: "🌐", // API 连通性 / 远程请求
|
|
15
|
+
pay: "💳", // 充值 / 订单 / 支付
|
|
16
|
+
wait: "⏳", // 轮询 / 等待用户操作
|
|
17
|
+
step: "▶", // 当前正在执行的步骤
|
|
18
|
+
success: "✅", // 成功完成
|
|
19
|
+
warn: "⚠️", // 可继续但需注意
|
|
20
|
+
error: "❌", // 失败 / 不可继续
|
|
21
|
+
info: "ℹ️", // 普通提示
|
|
22
|
+
file: "📄", // 配置文件 / 备份文件
|
|
23
|
+
dir: "📁", // 目录路径
|
|
24
|
+
done: "🎉", // 整个任务完成
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// 待执行 / 已完成步骤项的清单符号(§二 推荐结构:✔ 已完成 / ▶ 正在进行 / ○ 待执行)。
|
|
28
|
+
// 与 GLYPH.step 区分:这些用于「步骤总览清单」,GLYPH.step 用于当前步骤标题行。
|
|
29
|
+
export const STEP_MARK = {
|
|
30
|
+
done: "✔", // 已完成
|
|
31
|
+
active: "▶", // 正在进行
|
|
32
|
+
pending: "○", // 待执行
|
|
33
|
+
};
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,8 @@ import { runCcSwitchGenerator } from "./cc-switch.js";
|
|
|
11
11
|
import { createConfigStore } from "./config-store.js";
|
|
12
12
|
import { createPrompts, isInteractive } from "./prompts.js";
|
|
13
13
|
import { createMessenger, formatMessage } from "./messages.js";
|
|
14
|
-
import { renderHeader } from "./ui.js";
|
|
14
|
+
import { renderDivider, renderHeader, renderKeyValueTable, renderStructuredError, renderTaskEnd, renderTaskStart, renderTaskStep, selectedSummary, warningBlock } from "./ui.js";
|
|
15
|
+
import { CHANGELOG_ENTRIES, LICENSE_TEXT } from "./notices.js";
|
|
15
16
|
import {
|
|
16
17
|
commandVersion,
|
|
17
18
|
defaultExecCommand,
|
|
@@ -53,7 +54,7 @@ import {
|
|
|
53
54
|
resolveCodexProfileConfigPath,
|
|
54
55
|
} from "./tool-launcher.js";
|
|
55
56
|
import { installClaudeWorkflows, installCodexWorkflows } from "./workflows.js";
|
|
56
|
-
import { showOpenSourceNotice } from "./zcf-notice.js";
|
|
57
|
+
import { formatOpenSourceNoticeDetail, showOpenSourceNotice } from "./zcf-notice.js";
|
|
57
58
|
import { runWechatTopUp } from "./topup.js";
|
|
58
59
|
import { chooseOrCreateToken, createNewToken } from "./token-manager.js";
|
|
59
60
|
|
|
@@ -107,6 +108,7 @@ export async function runCli({
|
|
|
107
108
|
writeLine = console.log,
|
|
108
109
|
forceSetup = false,
|
|
109
110
|
command = "",
|
|
111
|
+
verbose = false,
|
|
110
112
|
checkEnvironment = ensureEnvironment,
|
|
111
113
|
showBanner = true,
|
|
112
114
|
version = readCliVersion(),
|
|
@@ -324,24 +326,64 @@ export async function runCli({
|
|
|
324
326
|
if (action === "back") {
|
|
325
327
|
return false;
|
|
326
328
|
}
|
|
329
|
+
writeLine(renderTaskStart("配置 Claude360 API", {
|
|
330
|
+
intro: ["检查授权状态", "选择 API Key", "写入本地配置", "完成验证"],
|
|
331
|
+
}));
|
|
332
|
+
writeLine("");
|
|
333
|
+
writeLine(renderTaskStep(1, 4, "检查授权状态"));
|
|
334
|
+
if (!(await ensureLogin())) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
msg.success("Claude360 授权状态有效。");
|
|
338
|
+
writeLine("");
|
|
339
|
+
writeLine(renderDivider("section"));
|
|
340
|
+
writeLine("");
|
|
341
|
+
writeLine(renderTaskStep(2, 4, "选择 API Key"));
|
|
342
|
+
let token;
|
|
327
343
|
if (action === "create") {
|
|
328
|
-
await
|
|
344
|
+
token = await createToken({ api, promptSelect, promptInput, writeLine });
|
|
345
|
+
writeLine("");
|
|
346
|
+
writeLine(selectedSummary("已创建 API Key", [
|
|
347
|
+
["当前 Key", token.tokenName || `Token #${token.tokenId}`],
|
|
348
|
+
["当前分组", token.group || "-"],
|
|
349
|
+
]));
|
|
329
350
|
} else if (action === "reselect") {
|
|
330
|
-
|
|
331
|
-
await saveConfig({
|
|
332
|
-
apiKey: token.apiKey,
|
|
333
|
-
selectedTokenId: token.tokenId,
|
|
334
|
-
tokenName: token.tokenName,
|
|
335
|
-
group: token.group,
|
|
336
|
-
});
|
|
337
|
-
msg.success(`当前 Key 已切换为 ${token.tokenName}`);
|
|
351
|
+
token = await chooseToken({ api, promptSelect, promptInput, writeLine });
|
|
338
352
|
} else {
|
|
339
|
-
await ensureApiKey();
|
|
353
|
+
token = await ensureApiKey();
|
|
354
|
+
writeLine("");
|
|
355
|
+
writeLine(selectedSummary("已选择 API Key", [
|
|
356
|
+
["当前 Key", token.tokenName || config.tokenName || "-"],
|
|
357
|
+
["当前分组", token.group || config.group || "-"],
|
|
358
|
+
]));
|
|
340
359
|
}
|
|
360
|
+
writeLine("");
|
|
361
|
+
writeLine(renderDivider("section"));
|
|
362
|
+
writeLine("");
|
|
363
|
+
writeLine(renderTaskStep(3, 4, "写入本地配置"));
|
|
364
|
+
await saveConfig({
|
|
365
|
+
apiKey: token.apiKey || config.apiKey,
|
|
366
|
+
selectedTokenId: token.tokenId ?? config.selectedTokenId,
|
|
367
|
+
tokenName: token.tokenName || config.tokenName,
|
|
368
|
+
group: token.group || config.group,
|
|
369
|
+
});
|
|
341
370
|
await markConfigured("claudeCode");
|
|
342
|
-
msg.success("
|
|
343
|
-
msg.path(`
|
|
344
|
-
msg.path("
|
|
371
|
+
msg.success("本地 Claude360 API 配置已更新。");
|
|
372
|
+
msg.path(`ANTHROPIC_BASE_URL=${baseUrl}`);
|
|
373
|
+
msg.path("ANTHROPIC_AUTH_TOKEN=<当前 API Key>");
|
|
374
|
+
writeLine("");
|
|
375
|
+
writeLine(renderDivider("section"));
|
|
376
|
+
writeLine("");
|
|
377
|
+
writeLine(renderTaskStep(4, 4, "完成验证"));
|
|
378
|
+
msg.success("Claude Code 启动时将注入 Claude360 配置(不修改 shell profile)。");
|
|
379
|
+
writeLine("");
|
|
380
|
+
writeLine(renderTaskEnd("Claude360 API 配置完成", {
|
|
381
|
+
summary: [
|
|
382
|
+
["当前 Key", config.tokenName || "-"],
|
|
383
|
+
["当前分组", config.group || "-"],
|
|
384
|
+
["下一步", "启动 Claude Code 或返回主菜单"],
|
|
385
|
+
],
|
|
386
|
+
}));
|
|
345
387
|
return true;
|
|
346
388
|
}
|
|
347
389
|
}
|
|
@@ -417,34 +459,41 @@ export async function runCli({
|
|
|
417
459
|
await doConfigureCodex();
|
|
418
460
|
continue;
|
|
419
461
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
462
|
+
await showCodexConfigSummary();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function showCodexConfigSummary() {
|
|
467
|
+
const configPath = resolveCodexConfigPath();
|
|
468
|
+
const profilePath = resolveCodexProfileConfigPath();
|
|
469
|
+
const configInfo = await readOptionalText(configPath);
|
|
470
|
+
const profileInfo = await readOptionalText(profilePath);
|
|
471
|
+
msg.heading("当前 Codex 配置摘要");
|
|
472
|
+
writeLine(renderKeyValueTable([
|
|
473
|
+
["config.toml", configInfo.exists ? "已找到" : "未找到"],
|
|
474
|
+
["claude360.config.toml", profileInfo.exists ? "已找到" : "未找到"],
|
|
475
|
+
["model_providers.claude360", configInfo.exists && /\[model_providers\.claude360\]/.test(configInfo.content) ? "已配置" : "未配置"],
|
|
476
|
+
["profile 覆盖层", profileInfo.exists && /model_provider\s*=\s*"claude360"/.test(profileInfo.content) ? "已配置" : "未配置"],
|
|
477
|
+
], { width: outputWidth() }));
|
|
478
|
+
msg.hint("查看完整配置:使用系统编辑器打开上述文件,或运行 Codex 自带配置查看命令。");
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function readOptionalText(filePath) {
|
|
482
|
+
try {
|
|
483
|
+
return { exists: true, content: await readFileImpl(filePath, "utf8") };
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (error?.code === "ENOENT") {
|
|
486
|
+
return { exists: false, content: "" };
|
|
444
487
|
}
|
|
488
|
+
throw error;
|
|
445
489
|
}
|
|
446
490
|
}
|
|
447
491
|
|
|
492
|
+
function showFullLocalConfig() {
|
|
493
|
+
writeLine("完整配置视图");
|
|
494
|
+
writeLine(JSON.stringify(config, null, 2));
|
|
495
|
+
}
|
|
496
|
+
|
|
448
497
|
// Codex 协议兼容性检测(PRD 8.6)
|
|
449
498
|
async function checkCodexCompatStep() {
|
|
450
499
|
msg.step("正在检测 Claude360 是否支持 Codex 所需协议...");
|
|
@@ -712,21 +761,67 @@ export async function runCli({
|
|
|
712
761
|
return;
|
|
713
762
|
}
|
|
714
763
|
const targets = selected === "all" ? ["claude", "codex"] : [selected];
|
|
715
|
-
|
|
764
|
+
writeLine(renderTaskStart("安装或更新工具", {
|
|
765
|
+
intro: ["检查版本", "执行安装或更新", "验证安装结果"],
|
|
766
|
+
}));
|
|
767
|
+
writeLine("");
|
|
768
|
+
writeLine(renderTaskStep(1, 3, "检查版本"));
|
|
769
|
+
for (const target of targets) {
|
|
770
|
+
const commandName = installTargetCommand(target);
|
|
771
|
+
const check = await commandVersion(execCommand, commandName, ["--version"]);
|
|
772
|
+
if (check.ok) {
|
|
773
|
+
msg.result(`${target} 当前版本`, check.version);
|
|
774
|
+
} else {
|
|
775
|
+
msg.info(`${target} 当前未安装或不可用。`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
writeLine("");
|
|
779
|
+
writeLine(renderDivider("section"));
|
|
780
|
+
writeLine("");
|
|
781
|
+
writeLine(renderTaskStep(2, 3, "执行安装或更新"));
|
|
782
|
+
if (!verbose) {
|
|
783
|
+
// 非 verbose 下 npm 输出被捕获不直显,给一个进行中提示,避免显得卡死
|
|
784
|
+
msg.step("正在执行 npm 安装/更新(耗时较长,请稍候)...");
|
|
785
|
+
}
|
|
786
|
+
const results = await installOrUpdateTools({ targets, confirm, verbose });
|
|
787
|
+
writeLine("");
|
|
788
|
+
writeLine(renderDivider("section"));
|
|
789
|
+
writeLine("");
|
|
790
|
+
writeLine(renderTaskStep(3, 3, "验证安装结果"));
|
|
716
791
|
for (const result of results || []) {
|
|
717
792
|
if (result?.skipped) {
|
|
718
793
|
msg.info(`已跳过 ${result.target}。`);
|
|
719
794
|
continue;
|
|
720
795
|
}
|
|
721
796
|
if (result?.ok === false) {
|
|
722
|
-
|
|
797
|
+
writeLine(renderStructuredError(`安装或更新 ${result.target} 失败`, {
|
|
798
|
+
reason: result.error || "安装命令执行失败。",
|
|
799
|
+
suggestions: [
|
|
800
|
+
result.remediation || "检查 npm registry 是否可访问。",
|
|
801
|
+
"确认当前用户有 npm 全局安装权限。",
|
|
802
|
+
],
|
|
803
|
+
detailCommand: "claude360 doctor --verbose",
|
|
804
|
+
}));
|
|
723
805
|
continue;
|
|
724
806
|
}
|
|
807
|
+
const commandName = installTargetCommand(result.target);
|
|
808
|
+
const check = await commandVersion(execCommand, commandName, ["--version"]);
|
|
809
|
+
if (check.ok) {
|
|
810
|
+
msg.result("当前版本", check.version);
|
|
811
|
+
} else if (result.target !== "claude360") {
|
|
812
|
+
msg.warn(`${result.target} 版本验证失败:${check.detail || "未检测到命令"}`);
|
|
813
|
+
}
|
|
725
814
|
msg.success(`${result.target} 安装或更新完成${result.usedMirror ? "(使用国内镜像)" : ""}`);
|
|
726
815
|
if (result.target === "claude360") {
|
|
727
816
|
msg.info("claude360 CLI 已更新,重新运行 npx claude360 后生效。");
|
|
728
817
|
}
|
|
729
818
|
}
|
|
819
|
+
writeLine("");
|
|
820
|
+
writeLine(renderTaskEnd("安装或更新工具完成"));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function installTargetCommand(target) {
|
|
824
|
+
return target === "claude" ? "claude" : target === "codex" ? "codex" : "claude360";
|
|
730
825
|
}
|
|
731
826
|
|
|
732
827
|
async function openPage(path, label) {
|
|
@@ -742,7 +837,10 @@ export async function runCli({
|
|
|
742
837
|
return true;
|
|
743
838
|
}
|
|
744
839
|
const approved = await confirm(
|
|
745
|
-
"
|
|
840
|
+
warningBlock("即将退出登录", {
|
|
841
|
+
action: "清除本地保存的 CLI Token 与 API Key",
|
|
842
|
+
impact: "仅影响本机 Claude360 CLI 配置;不会删除 Claude360 网站上的账号或 API Key。",
|
|
843
|
+
}),
|
|
746
844
|
{ danger: true },
|
|
747
845
|
);
|
|
748
846
|
if (!approved) {
|
|
@@ -768,7 +866,10 @@ export async function runCli({
|
|
|
768
866
|
|
|
769
867
|
async function doClearConfig() {
|
|
770
868
|
const approved = await confirm(
|
|
771
|
-
"
|
|
869
|
+
warningBlock("即将清除本地配置", {
|
|
870
|
+
action: "清除本地 Claude360 CLI 登录状态和已保存 Key",
|
|
871
|
+
impact: "仅影响本机 Claude360 CLI 配置;不会删除 Claude360 网站上的账号或 API Key。",
|
|
872
|
+
}),
|
|
772
873
|
{ danger: true },
|
|
773
874
|
);
|
|
774
875
|
if (!approved) {
|
|
@@ -811,7 +912,7 @@ export async function runCli({
|
|
|
811
912
|
}
|
|
812
913
|
|
|
813
914
|
async function doCreateKey() {
|
|
814
|
-
const token = await createToken({ api, promptSelect, promptInput });
|
|
915
|
+
const token = await createToken({ api, promptSelect, promptInput, writeLine });
|
|
815
916
|
const useNow = await confirm(`已创建 API Key:${token.tokenName}。是否将其设为当前使用的 Key?`, { defaultYes: true });
|
|
816
917
|
if (useNow) {
|
|
817
918
|
await saveConfig({
|
|
@@ -991,7 +1092,7 @@ export async function runCli({
|
|
|
991
1092
|
switch (action) {
|
|
992
1093
|
case "diagnose": {
|
|
993
1094
|
const report = await runDiagnostics({ config, api, execCommand });
|
|
994
|
-
writeLine(formatDiagnosticsSummary(report, { width: outputWidth(), color: fancyOutput }));
|
|
1095
|
+
writeLine(formatDiagnosticsSummary(report, { width: outputWidth(), color: fancyOutput, verbose }));
|
|
995
1096
|
break;
|
|
996
1097
|
}
|
|
997
1098
|
case "fix_claude":
|
|
@@ -1206,9 +1307,6 @@ export async function runCli({
|
|
|
1206
1307
|
|
|
1207
1308
|
async function runDailyFlow() {
|
|
1208
1309
|
while (true) {
|
|
1209
|
-
if (interactiveUi) {
|
|
1210
|
-
writeLine(renderHeader("主菜单", { color: fancyOutput }));
|
|
1211
|
-
}
|
|
1212
1310
|
const status = await loadAccountStatus({ api, config });
|
|
1213
1311
|
writeLine(formatAccountStatus({ ...status, width: outputWidth(), color: fancyOutput }));
|
|
1214
1312
|
writeLine("");
|
|
@@ -1286,6 +1384,23 @@ export async function runCli({
|
|
|
1286
1384
|
|
|
1287
1385
|
async function runShortcut(name) {
|
|
1288
1386
|
switch (name) {
|
|
1387
|
+
case "about":
|
|
1388
|
+
writeLine(formatOpenSourceNoticeDetail());
|
|
1389
|
+
return "about";
|
|
1390
|
+
case "config view":
|
|
1391
|
+
case "config:view":
|
|
1392
|
+
showFullLocalConfig();
|
|
1393
|
+
return "config_view";
|
|
1394
|
+
case "license":
|
|
1395
|
+
writeLine(LICENSE_TEXT);
|
|
1396
|
+
return "license";
|
|
1397
|
+
case "changelog":
|
|
1398
|
+
writeLine([
|
|
1399
|
+
"更新日志(最近 3 条):",
|
|
1400
|
+
"",
|
|
1401
|
+
...CHANGELOG_ENTRIES.slice(0, 3).map((entry, index) => `${index + 1}. ${entry}`),
|
|
1402
|
+
].join("\n"));
|
|
1403
|
+
return "changelog";
|
|
1289
1404
|
case "claude":
|
|
1290
1405
|
if (!(await ensureLogin())) {
|
|
1291
1406
|
return "login_required";
|
|
@@ -1342,6 +1457,10 @@ function formatCliUsage() {
|
|
|
1342
1457
|
"doctor 一键诊断",
|
|
1343
1458
|
"login 浏览器授权登录",
|
|
1344
1459
|
"logout 退出登录并清除本地凭证",
|
|
1460
|
+
"about 查看开源参考声明全文",
|
|
1461
|
+
"config view 显式查看本地完整配置",
|
|
1462
|
+
"license 查看许可证说明",
|
|
1463
|
+
"changelog 查看更新日志说明",
|
|
1345
1464
|
].join("\n");
|
|
1346
1465
|
}
|
|
1347
1466
|
|
package/src/init-config.js
CHANGED
|
@@ -212,6 +212,25 @@ export const FALLBACK_CLAUDE_ALIASES = [
|
|
|
212
212
|
{ id: "haiku", display_name: "Haiku(官方别名)", tags: ["经济"], description: "适合日常轻量任务" },
|
|
213
213
|
];
|
|
214
214
|
|
|
215
|
+
// 模型按工具归属过滤:Claude Code 仅展示 Claude 系模型,Codex 仅展示非 Claude
|
|
216
|
+
// (gpt 等)模型。后端 /api/cli/models?tool= 暂未按 tool 过滤,故在客户端按模型
|
|
217
|
+
// id 兜底过滤,避免切换 Claude 模型时混入 gpt、切换 Codex 模型时混入 claude。
|
|
218
|
+
export function isClaudeModel(id) {
|
|
219
|
+
const s = String(id || "").toLowerCase();
|
|
220
|
+
return s.startsWith("claude") || s === "sonnet" || s === "opus" || s === "haiku";
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function filterModelsForTool(models, tool) {
|
|
224
|
+
const list = Array.isArray(models) ? models : [];
|
|
225
|
+
if (tool === "claude_code") {
|
|
226
|
+
return list.filter((model) => isClaudeModel(model.id));
|
|
227
|
+
}
|
|
228
|
+
if (tool === "codex") {
|
|
229
|
+
return list.filter((model) => !isClaudeModel(model.id));
|
|
230
|
+
}
|
|
231
|
+
return list;
|
|
232
|
+
}
|
|
233
|
+
|
|
215
234
|
// 后端接口 GET /api/cli/models?tool=<claude_code|codex>:
|
|
216
235
|
// { models: [{ id, display_name, description, tags, recommended }] }
|
|
217
236
|
// (后端暂无上下文长度数据源,契约中不含 context_length)
|
|
@@ -222,8 +241,9 @@ export async function loadAvailableModels(api, tool = "") {
|
|
|
222
241
|
const data = await api.get(`/api/cli/models${query}`);
|
|
223
242
|
const models = (Array.isArray(data?.models) ? data.models : [])
|
|
224
243
|
.filter((model) => model && typeof model.id === "string" && model.id !== "");
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
const scoped = filterModelsForTool(models, tool);
|
|
245
|
+
if (scoped.length > 0) {
|
|
246
|
+
return { models: scoped, source: "backend" };
|
|
227
247
|
}
|
|
228
248
|
} catch {
|
|
229
249
|
// 后端暂未提供模型列表接口或网络失败,由调用方决定回退策略
|