claude360 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/claude360.js +4 -3
- package/install/install.ps1 +61 -15
- package/install/install.sh +75 -13
- package/install/verification-matrix.md +2 -2
- package/package.json +3 -2
- package/src/account-status.js +97 -0
- package/src/api-client.js +6 -3
- package/src/auth.js +5 -3
- package/src/banner.js +275 -0
- package/src/cc-switch.js +206 -0
- package/src/diagnostics.js +113 -36
- package/src/group-manager.js +6 -3
- package/src/index.js +808 -75
- package/src/mcp-skill.js +319 -0
- package/src/menu.js +144 -60
- package/src/sanitize.js +70 -0
- package/src/token-manager.js +46 -22
- package/src/tool-installer.js +22 -3
- package/src/tool-launcher.js +82 -21
- package/src/topup.js +8 -1
package/src/token-manager.js
CHANGED
|
@@ -24,10 +24,44 @@ function formatTokenKey(key) {
|
|
|
24
24
|
return key.includes("*") ? key : maskSecret(key);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
export const TOKEN_PURPOSES = [
|
|
28
|
+
{ label: "Claude Code 默认 Key", value: "claude-code-default" },
|
|
29
|
+
{ label: "Codex 默认 Key", value: "codex-default" },
|
|
30
|
+
{ label: "通用 Key", value: "claude360-cli" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// 创建新 API Key:选择用途(决定默认名)→ 选择分组(来自后端)→ 创建并返回明文
|
|
34
|
+
export async function createNewToken({ api, promptSelect, promptInput = async (_m, d) => d } = {}) {
|
|
35
|
+
if (!api) {
|
|
36
|
+
throw new Error("缺少 API client");
|
|
37
|
+
}
|
|
38
|
+
if (typeof promptSelect !== "function") {
|
|
39
|
+
throw new Error("缺少选择输入");
|
|
40
|
+
}
|
|
41
|
+
const purpose = await promptSelect("请选择 Key 用途:", TOKEN_PURPOSES);
|
|
42
|
+
const groups = await loadGroups(api);
|
|
43
|
+
const group = await selectGroup({ groups, promptSelect });
|
|
44
|
+
const name = await promptInput("新 API Key 名称", purpose);
|
|
45
|
+
const created = await api.post("/api/cli/tokens", {
|
|
46
|
+
name,
|
|
47
|
+
group: group.name,
|
|
48
|
+
});
|
|
49
|
+
if (!created?.key) {
|
|
50
|
+
throw new Error("创建 API Key 失败");
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
tokenId: created.id,
|
|
54
|
+
tokenName: created.name || name,
|
|
55
|
+
apiKey: created.key,
|
|
56
|
+
group: created.group || group.name,
|
|
57
|
+
created: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
27
61
|
export async function chooseOrCreateToken({
|
|
28
62
|
api,
|
|
29
63
|
promptSelect,
|
|
30
|
-
promptInput = async (
|
|
64
|
+
promptInput = async (_message, defaultValue = "Claude360 CLI") => defaultValue,
|
|
31
65
|
} = {}) {
|
|
32
66
|
if (!api) {
|
|
33
67
|
throw new Error("缺少 API client");
|
|
@@ -39,11 +73,17 @@ export async function chooseOrCreateToken({
|
|
|
39
73
|
if (typeof promptSelect !== "function") {
|
|
40
74
|
throw new Error("缺少 Token 选择输入");
|
|
41
75
|
}
|
|
42
|
-
const choices =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
76
|
+
const choices = [
|
|
77
|
+
...tokens.map((token) => ({
|
|
78
|
+
label: formatTokenOption(token),
|
|
79
|
+
value: token.id,
|
|
80
|
+
})),
|
|
81
|
+
{ label: "创建新的 API Key", value: "__create__" },
|
|
82
|
+
];
|
|
46
83
|
const tokenId = await promptSelect("选择 API Key", choices);
|
|
84
|
+
if (tokenId === "__create__") {
|
|
85
|
+
return createNewToken({ api, promptSelect, promptInput });
|
|
86
|
+
}
|
|
47
87
|
const selected = tokens.find((token) => token.id === tokenId);
|
|
48
88
|
if (!selected) {
|
|
49
89
|
throw new Error("选择的 API Key 无效");
|
|
@@ -61,23 +101,7 @@ export async function chooseOrCreateToken({
|
|
|
61
101
|
};
|
|
62
102
|
}
|
|
63
103
|
|
|
64
|
-
|
|
65
|
-
const group = await selectGroup({ groups, promptSelect });
|
|
66
|
-
const name = await promptInput("新 API Key 名称", "Claude360 CLI");
|
|
67
|
-
const created = await api.post("/api/cli/tokens", {
|
|
68
|
-
name,
|
|
69
|
-
group: group.name,
|
|
70
|
-
});
|
|
71
|
-
if (!created?.key) {
|
|
72
|
-
throw new Error("创建 API Key 失败");
|
|
73
|
-
}
|
|
74
|
-
return {
|
|
75
|
-
tokenId: created.id,
|
|
76
|
-
tokenName: created.name || name,
|
|
77
|
-
apiKey: created.key,
|
|
78
|
-
group: created.group || group.name,
|
|
79
|
-
created: true,
|
|
80
|
-
};
|
|
104
|
+
return createNewToken({ api, promptSelect, promptInput });
|
|
81
105
|
}
|
|
82
106
|
|
|
83
107
|
function formatStatus(status) {
|
package/src/tool-installer.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeText } from "./sanitize.js";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
2
3
|
|
|
3
4
|
const TOOL_COMMANDS = {
|
|
@@ -31,6 +32,8 @@ export function buildInstallCommand(target) {
|
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
export const NPM_MIRROR_REGISTRY = "https://registry.npmmirror.com";
|
|
36
|
+
|
|
34
37
|
export async function installOrUpdateTools({
|
|
35
38
|
targets,
|
|
36
39
|
confirm,
|
|
@@ -55,17 +58,33 @@ export async function installOrUpdateTools({
|
|
|
55
58
|
continue;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
let result = await execCommand(install.command, install.args);
|
|
62
|
+
let usedMirror = false;
|
|
63
|
+
// 官方源失败时支持国内镜像回退:一次性 --registry 参数重试,不修改用户全局 npm 配置
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
const retryWithMirror = await confirm(
|
|
66
|
+
`安装失败:${sanitizeText(result.stderr || result.error || "npm install failed")}\n是否使用国内镜像(${NPM_MIRROR_REGISTRY})重试?仅本次安装生效,不修改全局 npm 配置。`,
|
|
67
|
+
);
|
|
68
|
+
if (retryWithMirror) {
|
|
69
|
+
usedMirror = true;
|
|
70
|
+
result = await execCommand(install.command, [
|
|
71
|
+
...install.args,
|
|
72
|
+
`--registry=${NPM_MIRROR_REGISTRY}`,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
if (!result.ok) {
|
|
60
78
|
results.push({
|
|
61
79
|
target,
|
|
62
80
|
ok: false,
|
|
63
|
-
|
|
81
|
+
usedMirror,
|
|
82
|
+
error: sanitizeText(result.stderr || result.error || "npm install failed"),
|
|
64
83
|
remediation: "请检查 Node/npm 是否可用,并修复 npm 全局目录权限后重试。",
|
|
65
84
|
});
|
|
66
85
|
continue;
|
|
67
86
|
}
|
|
68
|
-
results.push({ target, ok: true, output: result.stdout || "" });
|
|
87
|
+
results.push({ target, ok: true, usedMirror, output: result.stdout || "" });
|
|
69
88
|
}
|
|
70
89
|
return results;
|
|
71
90
|
}
|
package/src/tool-launcher.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
|
|
5
|
+
import { safeErrorMessage } from "./sanitize.js";
|
|
6
|
+
import { copyFile as fsCopyFile, mkdir as fsMkdir, readFile as fsReadFile, writeFile as fsWriteFile } from "node:fs/promises";
|
|
3
7
|
import os from "node:os";
|
|
4
8
|
import path from "node:path";
|
|
5
9
|
|
|
10
|
+
// Codex 协议兼容性检测:以后端上报为准,CLI 不内置协议判断逻辑
|
|
11
|
+
export async function checkCodexCompat(api) {
|
|
12
|
+
if (!api || typeof api.get !== "function") {
|
|
13
|
+
return { supported: false, detail: "API client 不可用" };
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const data = await api.get("/api/cli/codex/compat");
|
|
17
|
+
return {
|
|
18
|
+
supported: Boolean(data?.supported),
|
|
19
|
+
wireApi: data?.wire_api || "responses",
|
|
20
|
+
detail: data?.supported ? "Claude360 已支持 Codex 所需协议" : "Claude360 暂不支持 Codex 所需协议",
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return { supported: false, detail: `协议兼容性检测失败:${safeErrorMessage(error)}` };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
export async function launchClaudeCode({
|
|
7
28
|
config,
|
|
8
29
|
spawnCommand = spawnInherited,
|
|
@@ -29,12 +50,14 @@ export async function launchCodex({
|
|
|
29
50
|
readFile = fsReadFile,
|
|
30
51
|
writeFile = fsWriteFile,
|
|
31
52
|
mkdir = fsMkdir,
|
|
53
|
+
copyFile = fsCopyFile,
|
|
32
54
|
confirmConflict,
|
|
33
55
|
spawnCommand = spawnInherited,
|
|
34
56
|
env = process.env,
|
|
57
|
+
writeLine = () => {},
|
|
35
58
|
} = {}) {
|
|
36
59
|
assertApiKey(config);
|
|
37
|
-
await configureCodexProvider({ config, configPath, readFile, writeFile, mkdir, confirmConflict });
|
|
60
|
+
await configureCodexProvider({ config, configPath, readFile, writeFile, mkdir, copyFile, confirmConflict, writeLine });
|
|
38
61
|
return spawnCommand("codex", ["--profile", "claude360"], {
|
|
39
62
|
stdio: "inherit",
|
|
40
63
|
env: {
|
|
@@ -50,7 +73,9 @@ export async function configureCodexProvider({
|
|
|
50
73
|
readFile = fsReadFile,
|
|
51
74
|
writeFile = fsWriteFile,
|
|
52
75
|
mkdir = fsMkdir,
|
|
76
|
+
copyFile = fsCopyFile,
|
|
53
77
|
confirmConflict,
|
|
78
|
+
writeLine = () => {},
|
|
54
79
|
} = {}) {
|
|
55
80
|
const current = await readConfigIfExists(readFile, configPath);
|
|
56
81
|
const desired = {
|
|
@@ -59,50 +84,86 @@ export async function configureCodexProvider({
|
|
|
59
84
|
envKey: "CLAUDE360_API_KEY",
|
|
60
85
|
wireApi: "responses",
|
|
61
86
|
};
|
|
62
|
-
await
|
|
87
|
+
await confirmCodexConfigConflict(current, desired, confirmConflict);
|
|
63
88
|
const content = buildCodexConfig(current, {
|
|
64
89
|
providerId: desired.providerId,
|
|
65
90
|
baseUrl: desired.baseUrl,
|
|
66
91
|
envKey: desired.envKey,
|
|
67
92
|
});
|
|
93
|
+
const tomlError = validateBasicToml(content);
|
|
94
|
+
if (tomlError) {
|
|
95
|
+
throw new Error(`生成的 Codex 配置 TOML 校验失败:${tomlError}`);
|
|
96
|
+
}
|
|
68
97
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
98
|
+
// 写入前备份原配置(PRD 安全要求)
|
|
99
|
+
let backupPath = null;
|
|
100
|
+
if (current !== "") {
|
|
101
|
+
backupPath = `${configPath}.claude360.bak`;
|
|
102
|
+
await copyFile(configPath, backupPath);
|
|
103
|
+
writeLine(`已备份原 Codex 配置到:${backupPath}`);
|
|
104
|
+
}
|
|
69
105
|
await writeFile(configPath, content, "utf8");
|
|
70
|
-
return content;
|
|
106
|
+
return { content, backupPath };
|
|
71
107
|
}
|
|
72
108
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
// 真实 TOML 校验(验收 P0-2):写入 ~/.codex/config.toml 前对合并产物完整 parse,
|
|
110
|
+
// 未闭合字符串、重复表等真实语法错误必须被检出;旧的行级形状检查既漏报也会
|
|
111
|
+
// 误报用户配置中的合法多行结构。返回 null 表示通过,否则返回用户可读错误。
|
|
112
|
+
export function validateBasicToml(content) {
|
|
113
|
+
try {
|
|
114
|
+
parseToml(String(content));
|
|
115
|
+
return null;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const line = error?.line ? `第 ${error.line} 行` : "";
|
|
118
|
+
const message = error?.message ? error.message.split("\n")[0] : "TOML 语法错误";
|
|
119
|
+
return `${line}${line ? ":" : ""}${message}`;
|
|
77
120
|
}
|
|
78
|
-
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 验收 P0-3:provider 与 profile 两类 claude360 块的冲突都必须二次确认,
|
|
124
|
+
// 拒绝时在 mkdir / 备份 / 写入之前抛错,磁盘零改动
|
|
125
|
+
async function confirmCodexConfigConflict(content, desired, confirmConflict) {
|
|
79
126
|
const conflicts = [];
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
127
|
+
|
|
128
|
+
const providerBlock = extractTableBlock(content, `model_providers.${desired.providerId}`);
|
|
129
|
+
if (providerBlock) {
|
|
130
|
+
const values = parseTomlStringAssignments(providerBlock);
|
|
131
|
+
if (values.base_url && values.base_url !== desired.baseUrl) {
|
|
132
|
+
conflicts.push(`base_url: ${values.base_url} -> ${desired.baseUrl}`);
|
|
133
|
+
}
|
|
134
|
+
if (values.env_key && values.env_key !== desired.envKey) {
|
|
135
|
+
conflicts.push(`env_key: ${values.env_key} -> ${desired.envKey}`);
|
|
136
|
+
}
|
|
137
|
+
if (values.wire_api && values.wire_api !== desired.wireApi) {
|
|
138
|
+
conflicts.push(`wire_api: ${values.wire_api} -> ${desired.wireApi}`);
|
|
139
|
+
}
|
|
85
140
|
}
|
|
86
|
-
|
|
87
|
-
|
|
141
|
+
|
|
142
|
+
const profileBlock = extractTableBlock(content, `profiles.${desired.providerId}`);
|
|
143
|
+
if (profileBlock) {
|
|
144
|
+
const values = parseTomlStringAssignments(profileBlock);
|
|
145
|
+
if (values.model_provider && values.model_provider !== desired.providerId) {
|
|
146
|
+
conflicts.push(`profiles.${desired.providerId}.model_provider: ${values.model_provider} -> ${desired.providerId}`);
|
|
147
|
+
}
|
|
88
148
|
}
|
|
149
|
+
|
|
89
150
|
if (conflicts.length === 0) {
|
|
90
151
|
return;
|
|
91
152
|
}
|
|
92
153
|
if (typeof confirmConflict !== "function") {
|
|
93
|
-
throw new Error(`Codex Claude360
|
|
154
|
+
throw new Error(`Codex Claude360 配置存在冲突:${conflicts.join("; ")}`);
|
|
94
155
|
}
|
|
95
156
|
const approved = await confirmConflict(
|
|
96
|
-
`Codex Claude360
|
|
157
|
+
`Codex Claude360 配置存在冲突,将覆盖以下字段:\n${conflicts.join("\n")}\n是否继续?`,
|
|
97
158
|
);
|
|
98
159
|
if (!approved) {
|
|
99
|
-
throw new Error(`Codex Claude360
|
|
160
|
+
throw new Error(`Codex Claude360 配置存在冲突:${conflicts.join("; ")}`);
|
|
100
161
|
}
|
|
101
162
|
}
|
|
102
163
|
|
|
103
|
-
function
|
|
164
|
+
function extractTableBlock(content, tableName) {
|
|
104
165
|
const lines = content.split(/\r?\n/);
|
|
105
|
-
const header = `[
|
|
166
|
+
const header = `[${tableName}]`;
|
|
106
167
|
const start = lines.findIndex((line) => line.trim() === header);
|
|
107
168
|
if (start === -1) {
|
|
108
169
|
return null;
|
package/src/topup.js
CHANGED
|
@@ -20,13 +20,20 @@ export async function runWechatTopUp({
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const options = await loadTopUpOptions(api);
|
|
23
|
+
// 后端口径前置拦截(验收 P1-2):禁用时不进金额选择、不创建订单
|
|
24
|
+
if (options?.wechat_enabled === false) {
|
|
25
|
+
throw new Error("微信支付未启用,请使用网页充值。");
|
|
26
|
+
}
|
|
23
27
|
const amount = await chooseTopUpAmount({ options, promptSelect, promptInput });
|
|
24
28
|
const order = await api.post("/api/cli/topup/wechat", { amount });
|
|
25
29
|
if (!order?.order_id || !order?.code_url) {
|
|
26
30
|
throw new Error("创建微信充值订单失败");
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
writeLine("请使用微信扫码支付");
|
|
29
34
|
await printQrOrCodeUrl({ codeUrl: order.code_url, renderQr, writeLine });
|
|
35
|
+
writeLine(`订单金额:${order.money_display || order.money || amount}`);
|
|
36
|
+
writeLine("订单状态:等待支付...");
|
|
30
37
|
const status = await waitTopUpPaid({
|
|
31
38
|
api,
|
|
32
39
|
orderId: order.order_id,
|
|
@@ -83,7 +90,7 @@ async function printQrOrCodeUrl({ codeUrl, renderQr, writeLine }) {
|
|
|
83
90
|
|
|
84
91
|
async function waitTopUpPaid({ api, orderId, sleep, pollIntervalMs, maxPolls }) {
|
|
85
92
|
for (let attempt = 0; attempt < maxPolls; attempt += 1) {
|
|
86
|
-
const status = await api.get(`/api/cli/topup
|
|
93
|
+
const status = await api.get(`/api/cli/topup/order?order_id=${encodeURIComponent(orderId)}`);
|
|
87
94
|
if (status?.status === "success") {
|
|
88
95
|
return status;
|
|
89
96
|
}
|