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.
@@ -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
- const result = await execCommand(install.command, install.args);
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
- error: result.stderr || result.error || "npm install failed",
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
  }
@@ -1,8 +1,29 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdir as fsMkdir, readFile as fsReadFile, writeFile as fsWriteFile } from "node:fs/promises";
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 confirmCodexProviderConflict(current, desired, confirmConflict);
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
- async function confirmCodexProviderConflict(content, desired, confirmConflict) {
74
- const existing = extractProviderBlock(content, desired.providerId);
75
- if (!existing) {
76
- return;
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
- const values = parseTomlStringAssignments(existing);
121
+ }
122
+
123
+ // 验收 P0-3:provider 与 profile 两类 claude360 块的冲突都必须二次确认,
124
+ // 拒绝时在 mkdir / 备份 / 写入之前抛错,磁盘零改动
125
+ async function confirmCodexConfigConflict(content, desired, confirmConflict) {
79
126
  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}`);
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
- if (values.wire_api && values.wire_api !== desired.wireApi) {
87
- conflicts.push(`wire_api: ${values.wire_api} -> ${desired.wireApi}`);
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 provider 配置存在冲突:${conflicts.join("; ")}`);
154
+ throw new Error(`Codex Claude360 配置存在冲突:${conflicts.join("; ")}`);
94
155
  }
95
156
  const approved = await confirmConflict(
96
- `Codex Claude360 provider 配置存在冲突,将覆盖以下字段:\n${conflicts.join("\n")}\n是否继续?`,
157
+ `Codex Claude360 配置存在冲突,将覆盖以下字段:\n${conflicts.join("\n")}\n是否继续?`,
97
158
  );
98
159
  if (!approved) {
99
- throw new Error(`Codex Claude360 provider 配置存在冲突:${conflicts.join("; ")}`);
160
+ throw new Error(`Codex Claude360 配置存在冲突:${conflicts.join("; ")}`);
100
161
  }
101
162
  }
102
163
 
103
- function extractProviderBlock(content, providerId) {
164
+ function extractTableBlock(content, tableName) {
104
165
  const lines = content.split(/\r?\n/);
105
- const header = `[model_providers.${providerId}]`;
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/${encodeURIComponent(orderId)}`);
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
  }