claude360 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/claude360.js +4 -3
- package/install/install.ps1 +61 -15
- package/install/install.sh +75 -13
- package/install/verification-matrix.md +2 -2
- package/package.json +3 -2
- package/src/account-status.js +97 -0
- package/src/api-client.js +6 -3
- package/src/auth.js +5 -3
- package/src/banner.js +42 -0
- package/src/cc-switch.js +206 -0
- package/src/diagnostics.js +113 -36
- package/src/group-manager.js +6 -3
- package/src/index.js +802 -75
- package/src/mcp-skill.js +319 -0
- package/src/menu.js +108 -61
- package/src/sanitize.js +70 -0
- package/src/token-manager.js +46 -22
- package/src/tool-installer.js +22 -3
- package/src/tool-launcher.js +82 -21
- package/src/topup.js +8 -1
package/src/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
|
|
package/src/group-manager.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export function formatGroupOption(group) {
|
|
2
2
|
const name = group?.name || "";
|
|
3
3
|
const displayName = group?.display_name || name;
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
4
|
+
// ratio 为 null/undefined(如 auto 分组)时不展示倍率,避免把 Number(null)=0 杜撰成 0.0x
|
|
5
|
+
const ratio = group?.ratio == null ? NaN : Number(group.ratio);
|
|
6
|
+
const ratioText = Number.isFinite(ratio) ? ` ${formatRatio(ratio)}x` : "";
|
|
7
|
+
const desc = group?.desc && group.desc !== displayName ? ` ${group.desc}` : "";
|
|
8
|
+
const recommended = group?.recommended ? " 推荐" : "";
|
|
9
|
+
return `${displayName}${ratioText}${desc}${recommended}`;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export async function loadGroups(api) {
|