claude360 0.1.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 +355 -0
- package/bin/claude360.js +10 -0
- package/install/install.ps1 +57 -0
- package/install/install.sh +57 -0
- package/install/verification-matrix.md +18 -0
- package/package.json +24 -0
- package/src/api-client.js +80 -0
- package/src/auth.js +82 -0
- package/src/config-store.js +69 -0
- package/src/diagnostics.js +206 -0
- package/src/group-manager.js +36 -0
- package/src/index.js +160 -0
- package/src/menu.js +72 -0
- package/src/platform.js +9 -0
- package/src/token-manager.js +99 -0
- package/src/tool-installer.js +71 -0
- package/src/tool-launcher.js +244 -0
- package/src/topup.js +109 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defaultExecCommand } from "./diagnostics.js";
|
|
2
|
+
|
|
3
|
+
const TOOL_COMMANDS = {
|
|
4
|
+
claude360: {
|
|
5
|
+
toolName: "Claude360 CLI",
|
|
6
|
+
packageName: "claude360",
|
|
7
|
+
npmSpec: "claude360@latest",
|
|
8
|
+
},
|
|
9
|
+
claude: {
|
|
10
|
+
toolName: "Claude Code",
|
|
11
|
+
packageName: "@anthropic-ai/claude-code",
|
|
12
|
+
npmSpec: "@anthropic-ai/claude-code@latest",
|
|
13
|
+
},
|
|
14
|
+
codex: {
|
|
15
|
+
toolName: "Codex",
|
|
16
|
+
packageName: "@openai/codex",
|
|
17
|
+
npmSpec: "@openai/codex@latest",
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function buildInstallCommand(target) {
|
|
22
|
+
const tool = TOOL_COMMANDS[target];
|
|
23
|
+
if (!tool) {
|
|
24
|
+
throw new Error(`未知工具:${target}`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
command: "npm",
|
|
28
|
+
args: ["install", "-g", tool.npmSpec],
|
|
29
|
+
packageName: tool.packageName,
|
|
30
|
+
toolName: tool.toolName,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function installOrUpdateTools({
|
|
35
|
+
targets,
|
|
36
|
+
confirm,
|
|
37
|
+
execCommand = defaultExecCommand,
|
|
38
|
+
} = {}) {
|
|
39
|
+
if (!Array.isArray(targets) || targets.length === 0) {
|
|
40
|
+
throw new Error("缺少安装或更新目标");
|
|
41
|
+
}
|
|
42
|
+
if (typeof confirm !== "function") {
|
|
43
|
+
throw new Error("包管理操作需要确认函数");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const results = [];
|
|
47
|
+
for (const target of targets) {
|
|
48
|
+
const install = buildInstallCommand(target);
|
|
49
|
+
const commandText = `${install.command} ${install.args.join(" ")}`;
|
|
50
|
+
const approved = await confirm(
|
|
51
|
+
`将执行全局 npm 操作:${commandText}\n影响范围:安装或更新 ${install.toolName} 到当前用户 npm 全局目录。\n是否继续?`,
|
|
52
|
+
);
|
|
53
|
+
if (!approved) {
|
|
54
|
+
results.push({ target, skipped: true });
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = await execCommand(install.command, install.args);
|
|
59
|
+
if (!result.ok) {
|
|
60
|
+
results.push({
|
|
61
|
+
target,
|
|
62
|
+
ok: false,
|
|
63
|
+
error: result.stderr || result.error || "npm install failed",
|
|
64
|
+
remediation: "请检查 Node/npm 是否可用,并修复 npm 全局目录权限后重试。",
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
results.push({ target, ok: true, output: result.stdout || "" });
|
|
69
|
+
}
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdir as fsMkdir, readFile as fsReadFile, writeFile as fsWriteFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export async function launchClaudeCode({
|
|
7
|
+
config,
|
|
8
|
+
spawnCommand = spawnInherited,
|
|
9
|
+
env = process.env,
|
|
10
|
+
} = {}) {
|
|
11
|
+
assertApiKey(config);
|
|
12
|
+
return spawnCommand("claude", [], {
|
|
13
|
+
stdio: "inherit",
|
|
14
|
+
env: {
|
|
15
|
+
...env,
|
|
16
|
+
ANTHROPIC_BASE_URL: normalizeBaseUrl(config.baseUrl),
|
|
17
|
+
ANTHROPIC_AUTH_TOKEN: config.apiKey,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveCodexConfigPath({ homedir = os.homedir } = {}) {
|
|
23
|
+
return path.join(homedir(), ".codex", "config.toml");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function launchCodex({
|
|
27
|
+
config,
|
|
28
|
+
configPath = resolveCodexConfigPath(),
|
|
29
|
+
readFile = fsReadFile,
|
|
30
|
+
writeFile = fsWriteFile,
|
|
31
|
+
mkdir = fsMkdir,
|
|
32
|
+
confirmConflict,
|
|
33
|
+
spawnCommand = spawnInherited,
|
|
34
|
+
env = process.env,
|
|
35
|
+
} = {}) {
|
|
36
|
+
assertApiKey(config);
|
|
37
|
+
await configureCodexProvider({ config, configPath, readFile, writeFile, mkdir, confirmConflict });
|
|
38
|
+
return spawnCommand("codex", ["--profile", "claude360"], {
|
|
39
|
+
stdio: "inherit",
|
|
40
|
+
env: {
|
|
41
|
+
...env,
|
|
42
|
+
CLAUDE360_API_KEY: config.apiKey,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function configureCodexProvider({
|
|
48
|
+
config,
|
|
49
|
+
configPath = resolveCodexConfigPath(),
|
|
50
|
+
readFile = fsReadFile,
|
|
51
|
+
writeFile = fsWriteFile,
|
|
52
|
+
mkdir = fsMkdir,
|
|
53
|
+
confirmConflict,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const current = await readConfigIfExists(readFile, configPath);
|
|
56
|
+
const desired = {
|
|
57
|
+
providerId: "claude360",
|
|
58
|
+
baseUrl: `${normalizeBaseUrl(config.baseUrl)}/v1`,
|
|
59
|
+
envKey: "CLAUDE360_API_KEY",
|
|
60
|
+
wireApi: "responses",
|
|
61
|
+
};
|
|
62
|
+
await confirmCodexProviderConflict(current, desired, confirmConflict);
|
|
63
|
+
const content = buildCodexConfig(current, {
|
|
64
|
+
providerId: desired.providerId,
|
|
65
|
+
baseUrl: desired.baseUrl,
|
|
66
|
+
envKey: desired.envKey,
|
|
67
|
+
});
|
|
68
|
+
await mkdir(path.dirname(configPath), { recursive: true });
|
|
69
|
+
await writeFile(configPath, content, "utf8");
|
|
70
|
+
return content;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function confirmCodexProviderConflict(content, desired, confirmConflict) {
|
|
74
|
+
const existing = extractProviderBlock(content, desired.providerId);
|
|
75
|
+
if (!existing) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const values = parseTomlStringAssignments(existing);
|
|
79
|
+
const conflicts = [];
|
|
80
|
+
if (values.base_url && values.base_url !== desired.baseUrl) {
|
|
81
|
+
conflicts.push(`base_url: ${values.base_url} -> ${desired.baseUrl}`);
|
|
82
|
+
}
|
|
83
|
+
if (values.env_key && values.env_key !== desired.envKey) {
|
|
84
|
+
conflicts.push(`env_key: ${values.env_key} -> ${desired.envKey}`);
|
|
85
|
+
}
|
|
86
|
+
if (values.wire_api && values.wire_api !== desired.wireApi) {
|
|
87
|
+
conflicts.push(`wire_api: ${values.wire_api} -> ${desired.wireApi}`);
|
|
88
|
+
}
|
|
89
|
+
if (conflicts.length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (typeof confirmConflict !== "function") {
|
|
93
|
+
throw new Error(`Codex Claude360 provider 配置存在冲突:${conflicts.join("; ")}`);
|
|
94
|
+
}
|
|
95
|
+
const approved = await confirmConflict(
|
|
96
|
+
`Codex Claude360 provider 配置存在冲突,将覆盖以下字段:\n${conflicts.join("\n")}\n是否继续?`,
|
|
97
|
+
);
|
|
98
|
+
if (!approved) {
|
|
99
|
+
throw new Error(`Codex Claude360 provider 配置存在冲突:${conflicts.join("; ")}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractProviderBlock(content, providerId) {
|
|
104
|
+
const lines = content.split(/\r?\n/);
|
|
105
|
+
const header = `[model_providers.${providerId}]`;
|
|
106
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
107
|
+
if (start === -1) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
let end = lines.length;
|
|
111
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
112
|
+
if (/^\s*\[/.test(lines[index])) {
|
|
113
|
+
end = index;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return lines.slice(start + 1, end).join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseTomlStringAssignments(block) {
|
|
121
|
+
const values = {};
|
|
122
|
+
for (const line of block.split(/\r?\n/)) {
|
|
123
|
+
const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*"((?:\\.|[^"\\])*)"\s*$/);
|
|
124
|
+
if (!match) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
values[match[1]] = match[2].replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
128
|
+
}
|
|
129
|
+
return values;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildCodexConfig(content = "", {
|
|
133
|
+
providerId = "claude360",
|
|
134
|
+
baseUrl = "https://claude360.xyz/v1",
|
|
135
|
+
envKey = "CLAUDE360_API_KEY",
|
|
136
|
+
} = {}) {
|
|
137
|
+
let nextContent = upsertTomlTable(content, `model_providers.${providerId}`, [
|
|
138
|
+
`[model_providers.${providerId}]`,
|
|
139
|
+
'name = "Claude360"',
|
|
140
|
+
`base_url = "${escapeTomlString(baseUrl)}"`,
|
|
141
|
+
`env_key = "${escapeTomlString(envKey)}"`,
|
|
142
|
+
'wire_api = "responses"',
|
|
143
|
+
]);
|
|
144
|
+
nextContent = upsertProfileModelProvider(nextContent, providerId);
|
|
145
|
+
return `${nextContent.trimEnd()}\n`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function upsertTomlTable(content, tableName, tableLines) {
|
|
149
|
+
const block = tableLines.join("\n");
|
|
150
|
+
const header = `[${tableName}]`;
|
|
151
|
+
|
|
152
|
+
const lines = content.split(/\r?\n/);
|
|
153
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
154
|
+
if (start === -1) {
|
|
155
|
+
const trimmed = content.trimEnd();
|
|
156
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${block}\n`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let end = lines.length;
|
|
160
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
161
|
+
if (/^\s*\[/.test(lines[index])) {
|
|
162
|
+
end = index;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [
|
|
168
|
+
...lines.slice(0, start),
|
|
169
|
+
block,
|
|
170
|
+
...lines.slice(end),
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function upsertProfileModelProvider(content, providerId) {
|
|
175
|
+
const lines = content.split(/\r?\n/);
|
|
176
|
+
const header = `[profiles.${providerId}]`;
|
|
177
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
178
|
+
if (start === -1) {
|
|
179
|
+
const trimmed = content.trimEnd();
|
|
180
|
+
return `${trimmed}${trimmed ? "\n\n" : ""}${header}\nmodel_provider = "${escapeTomlString(providerId)}"\n`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let end = lines.length;
|
|
184
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
185
|
+
if (/^\s*\[/.test(lines[index])) {
|
|
186
|
+
end = index;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const profileLines = lines.slice(start, end);
|
|
192
|
+
const providerLine = `model_provider = "${escapeTomlString(providerId)}"`;
|
|
193
|
+
const providerIndex = profileLines.findIndex((line) => /^\s*model_provider\s*=/.test(line));
|
|
194
|
+
if (providerIndex === -1) {
|
|
195
|
+
profileLines.splice(1, 0, providerLine);
|
|
196
|
+
} else {
|
|
197
|
+
profileLines[providerIndex] = providerLine;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
...lines.slice(0, start),
|
|
202
|
+
...profileLines,
|
|
203
|
+
...lines.slice(end),
|
|
204
|
+
].join("\n");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function readConfigIfExists(readFile, configPath) {
|
|
208
|
+
try {
|
|
209
|
+
return await readFile(configPath, "utf8");
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error?.code === "ENOENT") {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function assertApiKey(config) {
|
|
219
|
+
if (!config?.apiKey) {
|
|
220
|
+
throw new Error("缺少 API Key");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function normalizeBaseUrl(baseUrl = "https://claude360.xyz") {
|
|
225
|
+
return baseUrl.replace(/\/+$/, "");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function escapeTomlString(value) {
|
|
229
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function spawnInherited(command, args, options) {
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const child = spawn(command, args, options);
|
|
235
|
+
child.on("error", reject);
|
|
236
|
+
child.on("close", (code) => {
|
|
237
|
+
if (code === 0) {
|
|
238
|
+
resolve({ code });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
reject(new Error(`${command} exited with code ${code}`));
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
package/src/topup.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export async function loadTopUpOptions(api) {
|
|
2
|
+
if (!api) {
|
|
3
|
+
throw new Error("缺少 API client");
|
|
4
|
+
}
|
|
5
|
+
return api.get("/api/cli/topup/options");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function runWechatTopUp({
|
|
9
|
+
api,
|
|
10
|
+
promptSelect,
|
|
11
|
+
promptInput,
|
|
12
|
+
renderQr = renderTerminalQr,
|
|
13
|
+
writeLine = console.log,
|
|
14
|
+
sleep = defaultSleep,
|
|
15
|
+
pollIntervalMs = 3000,
|
|
16
|
+
maxPolls = 60,
|
|
17
|
+
} = {}) {
|
|
18
|
+
if (!api) {
|
|
19
|
+
throw new Error("缺少 API client");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const options = await loadTopUpOptions(api);
|
|
23
|
+
const amount = await chooseTopUpAmount({ options, promptSelect, promptInput });
|
|
24
|
+
const order = await api.post("/api/cli/topup/wechat", { amount });
|
|
25
|
+
if (!order?.order_id || !order?.code_url) {
|
|
26
|
+
throw new Error("创建微信充值订单失败");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await printQrOrCodeUrl({ codeUrl: order.code_url, renderQr, writeLine });
|
|
30
|
+
const status = await waitTopUpPaid({
|
|
31
|
+
api,
|
|
32
|
+
orderId: order.order_id,
|
|
33
|
+
sleep,
|
|
34
|
+
pollIntervalMs,
|
|
35
|
+
maxPolls,
|
|
36
|
+
});
|
|
37
|
+
const balance = await api.get("/api/cli/me");
|
|
38
|
+
return { order, status, balance };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
42
|
+
const amountOptions = Array.isArray(options?.amount_options)
|
|
43
|
+
? options.amount_options.filter((amount) => Number.isFinite(amount) && amount > 0)
|
|
44
|
+
: [];
|
|
45
|
+
if (amountOptions.length > 0) {
|
|
46
|
+
if (typeof promptSelect !== "function") {
|
|
47
|
+
throw new Error("缺少充值金额选择输入");
|
|
48
|
+
}
|
|
49
|
+
const selected = await promptSelect("选择充值金额", amountOptions.map((amount) => ({
|
|
50
|
+
label: `${amount}`,
|
|
51
|
+
value: amount,
|
|
52
|
+
})));
|
|
53
|
+
if (!amountOptions.includes(selected)) {
|
|
54
|
+
throw new Error("请选择后端返回的充值金额");
|
|
55
|
+
}
|
|
56
|
+
return selected;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof promptInput !== "function") {
|
|
60
|
+
throw new Error("缺少充值金额输入");
|
|
61
|
+
}
|
|
62
|
+
const minTopUp = Number(options?.min_topup || 0);
|
|
63
|
+
const amount = Number(await promptInput("输入充值金额", minTopUp ? String(minTopUp) : ""));
|
|
64
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
65
|
+
throw new Error("充值金额无效");
|
|
66
|
+
}
|
|
67
|
+
if (minTopUp > 0 && amount < minTopUp) {
|
|
68
|
+
throw new Error(`充值金额不能低于 ${minTopUp}`);
|
|
69
|
+
}
|
|
70
|
+
return amount;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function printQrOrCodeUrl({ codeUrl, renderQr, writeLine }) {
|
|
74
|
+
try {
|
|
75
|
+
const qr = await renderQr(codeUrl);
|
|
76
|
+
if (qr) {
|
|
77
|
+
writeLine(qr);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
writeLine(`二维码渲染失败,请复制 code_url 完成支付:${codeUrl}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function waitTopUpPaid({ api, orderId, sleep, pollIntervalMs, maxPolls }) {
|
|
85
|
+
for (let attempt = 0; attempt < maxPolls; attempt += 1) {
|
|
86
|
+
const status = await api.get(`/api/cli/topup/${encodeURIComponent(orderId)}`);
|
|
87
|
+
if (status?.status === "success") {
|
|
88
|
+
return status;
|
|
89
|
+
}
|
|
90
|
+
if (status?.status && status.status !== "pending") {
|
|
91
|
+
throw new Error(`充值订单状态异常:${status.status}`);
|
|
92
|
+
}
|
|
93
|
+
if (attempt < maxPolls - 1) {
|
|
94
|
+
await sleep(pollIntervalMs);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
throw new Error("等待微信支付超时");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function renderTerminalQr(codeUrl) {
|
|
101
|
+
const qrcode = await import("qrcode-terminal");
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
qrcode.default.generate(codeUrl, { small: true }, resolve);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function defaultSleep(ms) {
|
|
108
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
109
|
+
}
|