claude360 0.2.8 → 0.3.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/src/cc-switch.js CHANGED
@@ -4,6 +4,12 @@
4
4
  import { chmod, mkdir, writeFile } from "node:fs/promises";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
+ import { colorLevel } from "./colors.js";
8
+ import { createMessenger } from "./messages.js";
9
+ import { summary, warningBlock } from "./ui.js";
10
+
11
+ // 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
12
+ const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
7
13
 
8
14
  function normalizeBaseUrl(baseUrl = "https://claude360.xyz") {
9
15
  return String(baseUrl).replace(/\/+$/, "");
@@ -89,6 +95,22 @@ export function maskCcSwitchConfig(config, apiKey) {
89
95
  return JSON.parse(json.split(JSON.stringify(apiKey)).join(JSON.stringify(masked)));
90
96
  }
91
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
+
92
114
  export function resolveCcSwitchSavePath({
93
115
  platform = process.platform,
94
116
  env = process.env,
@@ -134,9 +156,10 @@ export async function runCcSwitchGenerator({
134
156
  throw new Error("缺少菜单选择输入");
135
157
  }
136
158
 
159
+ const msg = mk(writeLine);
137
160
  let apiKey = config.apiKey;
138
161
  if (!apiKey) {
139
- writeLine("当前没有可用 API Key,需要先创建 Claude360 API Key。");
162
+ msg.warn("当前没有可用 API Key,需要先创建 Claude360 API Key。");
140
163
  if (typeof ensureApiKey !== "function") {
141
164
  return { done: false, reason: "no_key" };
142
165
  }
@@ -150,7 +173,7 @@ export async function runCcSwitchGenerator({
150
173
  const token = await ensureApiKey();
151
174
  apiKey = token?.apiKey;
152
175
  if (!apiKey) {
153
- writeLine("未获取到可用 API Key,已取消生成。");
176
+ msg.info("未获取到可用 API Key,已取消生成。");
154
177
  return { done: false, reason: "no_key" };
155
178
  }
156
179
  }
@@ -184,13 +207,18 @@ export async function runCcSwitchGenerator({
184
207
 
185
208
  if (reveal === "masked") {
186
209
  const maskedConfig = maskCcSwitchConfig(fullConfig, apiKey);
187
- writeLine(JSON.stringify(maskedConfig, null, 2));
188
- writeLine("提示:脱敏配置仅用于预览,无法直接使用,也不会保存到文件。");
210
+ writeLine(formatCcSwitchSummary(target, maskedConfig, apiKey));
211
+ msg.hint("提示:脱敏配置仅用于预览,无法直接使用,也不会保存到文件。");
189
212
  return { done: true, target, masked: true, saved: false };
190
213
  }
191
214
 
215
+ writeLine(warningBlock("即将显示完整 API Key", {
216
+ action: "在终端输出完整 cc-switch 配置",
217
+ impact: "当前终端可见范围内的人可能看到 API Key",
218
+ }));
219
+ writeLine("");
192
220
  writeLine(JSON.stringify(fullConfig, null, 2));
193
- writeLine("请将以上配置复制到 cc-switch 中使用。");
221
+ msg.info("请将以上配置复制到 cc-switch 中使用。");
194
222
 
195
223
  const saveChoice = await promptSelect("是否保存到本地文件?", [
196
224
  { label: "保存到 ~/.claude360/cc-switch-claude360.json", value: "save" },
@@ -201,6 +229,6 @@ export async function runCcSwitchGenerator({
201
229
  }
202
230
 
203
231
  const savedPath = await save(fullConfig);
204
- writeLine(`已保存到:${savedPath}(文件权限 0600)`);
232
+ msg.success(`已保存到:${savedPath}(文件权限 0600)`);
205
233
  return { done: true, target, masked: false, saved: true, savedPath };
206
234
  }
package/src/colors.js CHANGED
@@ -91,3 +91,34 @@ export function fg(r, g, b, level = 2) {
91
91
  }
92
92
  return "";
93
93
  }
94
+
95
+ // ──────────────────────────────────────────────
96
+ // 语义调色板(单一事实源)
97
+ // ──────────────────────────────────────────────
98
+ // 全 CLI 的角色配色统一在此定义,banner / menu / prompts / ui / messages
99
+ // 都从这里取色,杜绝各文件重复硬编码同一组 RGB(色值沿用 Tailwind 体系)。
100
+ // 设计目标:状态色(绿/红/黄)醒目、信息色(天蓝)中性、进行色(紫)区别于
101
+ // 信息、标题白做强调、两级灰(path/hint)用于回显与次要说明 —— 形成层级。
102
+ export const PALETTE = {
103
+ title: [240, 246, 255], // 强调白:页面/品牌标题、回显值
104
+ heading: [34, 211, 238], // 亮青:章节标题、品牌高亮
105
+ info: [125, 211, 252], // 天蓝:中性信息、表头、选择键
106
+ success: [74, 222, 128], // 绿:操作成功
107
+ warn: [250, 204, 21], // 黄:警告 / 降级
108
+ error: [248, 113, 113], // 红:失败 / 危险操作
109
+ step: [167, 139, 250], // 紫:进行中 / 步骤(与 info 拉开区分)
110
+ path: [148, 163, 184], // 灰蓝:路径 / 命令 / URL 回显
111
+ hint: [100, 116, 139], // 暗灰:次要说明 / 帮助 / 未选中项
112
+ border: [71, 85, 105], // 青灰:边框 / 分隔线
113
+ };
114
+
115
+ // 按色彩深度 level 现算各语义角色的前景 SGR(真彩色保留 RGB,256 色量化)。
116
+ // level=0 时全部为空串,调用方据此走无色路径。
117
+ export function theme(level) {
118
+ const t = {};
119
+ for (const name of Object.keys(PALETTE)) {
120
+ const [r, g, b] = PALETTE[name];
121
+ t[name] = fg(r, g, b, level);
122
+ }
123
+ return t;
124
+ }
@@ -3,9 +3,9 @@ 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";
6
+ import { resolveCodexConfigPath, resolveCodexProfileConfigPath } from "./tool-launcher.js";
7
7
  import { sanitizeError, sanitizeText } from "./sanitize.js";
8
- import { renderHeader, renderSectionTitle, renderTable } from "./ui.js";
8
+ import { renderChoiceTable, renderDivider, renderSectionTitle, renderTaskEnd, renderTaskStart } from "./ui.js";
9
9
 
10
10
  export async function runDiagnostics({
11
11
  config = {},
@@ -14,6 +14,7 @@ export async function runDiagnostics({
14
14
  checkPathWritable = defaultCheckPathWritable,
15
15
  readFile = fsReadFile,
16
16
  codexConfigPath = resolveCodexConfigPath(),
17
+ codexProfileConfigPath = resolveCodexProfileConfigPath(),
17
18
  api,
18
19
  } = {}) {
19
20
  const node = await commandVersion(execCommand, "node", ["--version"]);
@@ -65,7 +66,7 @@ export async function runDiagnostics({
65
66
  token: buildTokenStatus({ config, tokens }),
66
67
  topUp: buildTopUpStatus({ topUpOptions }),
67
68
  claudeCodeConfig: buildClaudeCodeConfigStatus({ config, claudeCode }),
68
- codexConfig: await buildCodexConfigStatus({ readFile, codexConfigPath, codex }),
69
+ codexConfig: await buildCodexConfigStatus({ readFile, codexConfigPath, codexProfileConfigPath, codex }),
69
70
  codexCompat: buildCodexCompatStatus({ codexCompat }),
70
71
  };
71
72
  }
@@ -108,22 +109,39 @@ function buildClaudeCodeConfigStatus({ config, claudeCode }) {
108
109
  return { ok: true, detail: "启动时注入 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN" };
109
110
  }
110
111
 
111
- async function buildCodexConfigStatus({ readFile, codexConfigPath, codex }) {
112
+ async function buildCodexConfigStatus({ readFile, codexConfigPath, codexProfileConfigPath, codex }) {
112
113
  if (!codex.ok) {
113
114
  return { ok: false, detail: "Codex 未安装" };
114
115
  }
116
+ let baseContent;
115
117
  try {
116
- const content = await readFile(codexConfigPath, "utf8");
117
- if (content.includes("[model_providers.claude360]") && content.includes("[profiles.claude360]")) {
118
- return { ok: true, detail: "profile claude360 正常" };
119
- }
120
- return { ok: false, detail: "未写入 claude360 provider/profile" };
118
+ baseContent = await readFile(codexConfigPath, "utf8");
121
119
  } catch (error) {
122
120
  if (error?.code === "ENOENT") {
123
121
  return { ok: false, detail: "未找到 ~/.codex/config.toml" };
124
122
  }
125
123
  return { ok: false, detail: sanitizeError(error) || "读取 config.toml 失败" };
126
124
  }
125
+ if (!baseContent.includes("[model_providers.claude360]")) {
126
+ return { ok: false, detail: "未写入 claude360 provider" };
127
+ }
128
+ // legacy [profiles.claude360] 与 --profile 共存会导致新版 Codex 启动失败,需重新写入迁移
129
+ if (baseContent.includes("[profiles.claude360]")) {
130
+ return { ok: false, detail: "检测到 legacy [profiles.claude360],请在「Codex 配置」中重新写入以迁移" };
131
+ }
132
+ let profileContent;
133
+ try {
134
+ profileContent = await readFile(codexProfileConfigPath, "utf8");
135
+ } catch (error) {
136
+ if (error?.code === "ENOENT") {
137
+ return { ok: false, detail: "缺少 ~/.codex/claude360.config.toml(profile 覆盖层)" };
138
+ }
139
+ return { ok: false, detail: sanitizeError(error) || "读取 claude360.config.toml 失败" };
140
+ }
141
+ if (!/model_provider\s*=\s*"claude360"/.test(profileContent)) {
142
+ return { ok: false, detail: "claude360.config.toml 缺少 model_provider" };
143
+ }
144
+ return { ok: true, detail: "profile claude360 正常" };
127
145
  }
128
146
 
129
147
  function buildCodexCompatStatus({ codexCompat }) {
@@ -139,7 +157,7 @@ function buildCodexCompatStatus({ codexCompat }) {
139
157
  // 诊断报告(补充需求第 5 节):按模块分组的小表格展示,
140
158
  // 状态用文字标签(通过/警告/失败)明确区分,失败项附修复建议,
141
159
  // 输出适合直接复制给客服或开发排查(出口统一脱敏)。
142
- export function formatDiagnosticsSummary(report, { width = 0 } = {}) {
160
+ export function formatDiagnosticsSummary(report, { width = 0, color = false, verbose = false } = {}) {
143
161
  const groups = [
144
162
  {
145
163
  title: "系统环境",
@@ -179,31 +197,56 @@ export function formatDiagnosticsSummary(report, { width = 0 } = {}) {
179
197
  },
180
198
  ];
181
199
 
182
- const lines = [renderHeader("诊断报告")];
200
+ const lines = [renderTaskStart("运行诊断", {
201
+ intro: ["检查系统环境", "检查 Claude360 账号与余额", "检查 Claude Code / Codex 配置", "汇总问题与建议"],
202
+ })];
203
+ let rowNumber = 1;
204
+ const allFixes = [];
183
205
  for (const group of groups) {
184
- lines.push("", renderSectionTitle(group.title));
185
- lines.push(renderTable({
186
- head: ["检查项", "结果", "状态"],
187
- rows: group.items.map(({ label, item, valueKey = "detail" }) => [
188
- label,
189
- String(item[valueKey] || item.detail || "-"),
190
- statusLabel(item),
191
- ]),
192
- }, { width }));
193
- const fixes = group.items.filter(({ item, fix }) => !item.ok && fix);
194
- for (const { label, item, fix } of fixes) {
195
- lines.push(`${item.warn ? "!" : "×"} ${label} 建议:${fix}`);
206
+ lines.push("", renderSectionTitle(group.title, { color }));
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] || "-"}`);
196
236
  }
197
237
  }
238
+ lines.push("", renderTaskEnd("诊断完成", {
239
+ summary: [["问题数量", String(allFixes.length)]],
240
+ }));
198
241
  // 出口兜底:report 由调用方注入,逐项 detail 可能未经脱敏
199
242
  return sanitizeText(lines.join("\n"));
200
243
  }
201
244
 
202
245
  function statusLabel(item) {
203
246
  if (item.ok) {
204
- return " 通过";
247
+ return " 正常";
205
248
  }
206
- return item.warn ? "! 警告" : "× 失败";
249
+ return item.warn ? "⚠️ 注意" : " 异常";
207
250
  }
208
251
 
209
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
+ };