claude360 0.2.9 → 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/bin/claude360.js +7 -3
- package/package.json +1 -1
- package/src/banner.js +41 -0
- package/src/cc-switch.js +23 -1
- package/src/diagnostics.js +42 -18
- package/src/glyphs.js +33 -0
- package/src/index.js +167 -48
- package/src/init-config.js +22 -2
- package/src/init-flow.js +16 -4
- package/src/mcp-skill.js +16 -0
- package/src/menu.js +12 -6
- package/src/notices.js +33 -0
- package/src/token-manager.js +90 -20
- package/src/tool-installer.js +25 -9
- package/src/tool-launcher.js +43 -2
- package/src/topup.js +49 -3
- package/src/ui.js +255 -0
- package/src/zcf-notice.js +15 -1
package/src/init-flow.js
CHANGED
|
@@ -5,18 +5,26 @@
|
|
|
5
5
|
import { safeErrorMessage } from "./sanitize.js";
|
|
6
6
|
import { colorLevel } from "./colors.js";
|
|
7
7
|
import { createMessenger } from "./messages.js";
|
|
8
|
+
import { renderDivider, renderStructuredError, renderTaskEnd, renderTaskStart, renderTaskStep } from "./ui.js";
|
|
8
9
|
|
|
9
10
|
// 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
|
|
10
11
|
const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
11
12
|
|
|
12
13
|
export async function runFullInit({ title, steps, writeLine = console.log } = {}) {
|
|
13
14
|
const msg = mk(writeLine);
|
|
14
|
-
|
|
15
|
+
writeLine(renderTaskStart(title, {
|
|
16
|
+
intro: steps.map((step) => step.title),
|
|
17
|
+
}));
|
|
15
18
|
writeLine("");
|
|
16
19
|
let index = 0;
|
|
20
|
+
const total = steps.length;
|
|
17
21
|
for (const step of steps) {
|
|
18
22
|
index += 1;
|
|
19
|
-
|
|
23
|
+
if (index > 1) {
|
|
24
|
+
writeLine(renderDivider("section"));
|
|
25
|
+
writeLine("");
|
|
26
|
+
}
|
|
27
|
+
writeLine(renderTaskStep(index, total, step.title));
|
|
20
28
|
try {
|
|
21
29
|
const result = await step.run();
|
|
22
30
|
if (step.required && result === false) {
|
|
@@ -25,14 +33,18 @@ export async function runFullInit({ title, steps, writeLine = console.log } = {}
|
|
|
25
33
|
}
|
|
26
34
|
} catch (error) {
|
|
27
35
|
if (step.required) {
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
writeLine(renderStructuredError(`${step.title}失败`, {
|
|
37
|
+
reason: safeErrorMessage(error),
|
|
38
|
+
suggestions: ["初始化已中止,可稍后在菜单中重试。"],
|
|
39
|
+
detailCommand: "claude360 doctor --verbose",
|
|
40
|
+
}));
|
|
30
41
|
return { completed: false, abortedAt: step.title };
|
|
31
42
|
}
|
|
32
43
|
msg.warn(`${step.title}失败(已跳过,不影响后续步骤):${safeErrorMessage(error)}`);
|
|
33
44
|
}
|
|
34
45
|
writeLine("");
|
|
35
46
|
}
|
|
47
|
+
writeLine(renderTaskEnd(`${title}完成`));
|
|
36
48
|
return { completed: true };
|
|
37
49
|
}
|
|
38
50
|
|
package/src/mcp-skill.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createBackup } from "./backup.js";
|
|
|
11
11
|
import { upsertTomlTable, validateBasicToml } from "./tool-launcher.js";
|
|
12
12
|
import { colorLevel } from "./colors.js";
|
|
13
13
|
import { createMessenger } from "./messages.js";
|
|
14
|
+
import { renderChoiceTable } from "./ui.js";
|
|
14
15
|
|
|
15
16
|
// 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
|
|
16
17
|
const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
@@ -165,6 +166,21 @@ async function selectMcps({ mcps, multiSelect, installedIds, writeLine }) {
|
|
|
165
166
|
}
|
|
166
167
|
fresh.push(mcps.find((mcp) => mcp.id === id));
|
|
167
168
|
}
|
|
169
|
+
if (fresh.length > 0 && typeof writeLine === "function") {
|
|
170
|
+
writeLine(`✅ 已选择 ${fresh.length} 个 MCP 服务`);
|
|
171
|
+
writeLine(renderChoiceTable({
|
|
172
|
+
columns: [
|
|
173
|
+
["index", "序号"],
|
|
174
|
+
["service", "服务"],
|
|
175
|
+
["description", "说明"],
|
|
176
|
+
],
|
|
177
|
+
rows: fresh.map((mcp, index) => ({
|
|
178
|
+
index: String(index + 1),
|
|
179
|
+
service: mcp.id,
|
|
180
|
+
description: mcp.desc || "-",
|
|
181
|
+
})),
|
|
182
|
+
}, { width: process.stdout.columns || 0 }));
|
|
183
|
+
}
|
|
168
184
|
return fresh;
|
|
169
185
|
}
|
|
170
186
|
|
package/src/menu.js
CHANGED
|
@@ -19,6 +19,7 @@ function palette(level) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
const MENU_RULE_WIDTH = 46;
|
|
22
|
+
const SECTION_DIVIDER = "----------------------------------------";
|
|
22
23
|
|
|
23
24
|
export function buildFirstRunMenu() {
|
|
24
25
|
return {
|
|
@@ -177,13 +178,18 @@ export async function promptMenu({ menu, promptInput, select, writeLine = consol
|
|
|
177
178
|
// 多选交互(补充需求 6.5 / 6.6 / 8.4):数字切换勾选,A 全选,I 反选,
|
|
178
179
|
// 回车确认。返回选中的 value 数组;用户清空全部选项后回车即视为跳过。
|
|
179
180
|
export function renderMultiSelect({ message, choices, selected }) {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
const count = choices.filter((choice) => selected.has(choice.value)).length;
|
|
182
|
+
const lines = [`⚙️ ${message}`, "", `已选择:${count} 个`, ""];
|
|
183
|
+
// hint 列按 label 最大显示宽度对齐(需求示例 4),不再用固定空格
|
|
184
|
+
const labelWidth = choices.reduce((max, choice) => Math.max(max, displayWidth(choice.label)), 0);
|
|
185
|
+
choices.forEach((choice) => {
|
|
186
|
+
const mark = selected.has(choice.value) ? "●" : "○";
|
|
187
|
+
const hint = choice.hint
|
|
188
|
+
? `${" ".repeat(Math.max(2, labelWidth - displayWidth(choice.label) + 2))}${choice.hint}`
|
|
189
|
+
: "";
|
|
190
|
+
lines.push(`${mark} ${choice.label}${hint}`);
|
|
185
191
|
});
|
|
186
|
-
lines.push("", "
|
|
192
|
+
lines.push("", SECTION_DIVIDER, "", "操作:", "输入编号切换 A 全选 I 反选 Enter 确认");
|
|
187
193
|
return lines.join("\n");
|
|
188
194
|
}
|
|
189
195
|
|
package/src/notices.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// 许可证与更新日志(优化需求 V2.0 §八):license / changelog 命令展示真实内容,
|
|
2
|
+
// 随 src/ 一起发布,避免占位提示。changelog 命令侧 slice 前 3 条(需求「最多 3 条」)。
|
|
3
|
+
|
|
4
|
+
export const LICENSE_TEXT = [
|
|
5
|
+
"MIT License",
|
|
6
|
+
"",
|
|
7
|
+
"Copyright (c) 2026 Claude360",
|
|
8
|
+
"",
|
|
9
|
+
"Permission is hereby granted, free of charge, to any person obtaining a copy",
|
|
10
|
+
"of this software and associated documentation files (the \"Software\"), to deal",
|
|
11
|
+
"in the Software without restriction, including without limitation the rights",
|
|
12
|
+
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
|
|
13
|
+
"copies of the Software, and to permit persons to whom the Software is",
|
|
14
|
+
"furnished to do so, subject to the following conditions:",
|
|
15
|
+
"",
|
|
16
|
+
"The above copyright notice and this permission notice shall be included in all",
|
|
17
|
+
"copies or substantial portions of the Software.",
|
|
18
|
+
"",
|
|
19
|
+
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
|
|
20
|
+
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
|
|
21
|
+
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
|
|
22
|
+
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
|
|
23
|
+
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
|
|
24
|
+
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE",
|
|
25
|
+
"SOFTWARE.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
// 更新日志(最近在前);changelog 命令展示前 3 条(需求 §八「最多 3 条」)。
|
|
29
|
+
export const CHANGELOG_ENTRIES = [
|
|
30
|
+
"v0.2.9 执行流程视觉重构:任务页 / 步骤编号 / 分割线 / 表格化 / 窄屏卡片降级 / verbose 详细模式",
|
|
31
|
+
"v0.2.0 充值、诊断、Codex Provider 配置接入统一执行页组件",
|
|
32
|
+
"v0.1.x 初始版本:浏览器授权登录、API Key 管理、Claude Code · Codex 接入配置",
|
|
33
|
+
];
|
package/src/token-manager.js
CHANGED
|
@@ -1,32 +1,76 @@
|
|
|
1
1
|
import { loadGroups, selectGroup } from "./group-manager.js";
|
|
2
2
|
import { maskSecret } from "./config-store.js";
|
|
3
|
-
import { renderTable } from "./ui.js";
|
|
3
|
+
import { renderChoiceTable, renderKeyValueTable, renderTable } from "./ui.js";
|
|
4
4
|
|
|
5
|
-
// Key 列表表格(补充需求第 3 节):多 Key
|
|
6
|
-
|
|
5
|
+
// Key 列表表格(补充需求第 3 节):多 Key 对比展示,长字段由表格渲染层截断。
|
|
6
|
+
// 倍率列从 groups(/api/cli/groups)按 token.group 映射,便于直观对比各 Key 的计费倍率。
|
|
7
|
+
export function renderTokenTable(tokens, { width = 0, groups = [] } = {}) {
|
|
8
|
+
// 列精简到需求第七节建议的核心 6 列(序号/名称/分组/额度/状态/标记);
|
|
9
|
+
// 倍率并入分组列,过期由状态列体现(formatStatusLabel 已含「过期」),
|
|
10
|
+
// 完整脱敏 Key 在选择项标签与选后摘要展示,不在主表占列。
|
|
11
|
+
return renderChoiceTable({
|
|
12
|
+
columns: [
|
|
13
|
+
["index", "序号"],
|
|
14
|
+
["name", "名称"],
|
|
15
|
+
["group", "分组"],
|
|
16
|
+
["quota", "额度"],
|
|
17
|
+
["status", "状态"],
|
|
18
|
+
["mark", "标记"],
|
|
19
|
+
],
|
|
20
|
+
rows: tokens.map((token, index) => ({
|
|
21
|
+
index: String(index + 1),
|
|
22
|
+
name: token.name || `Token #${token.id}`,
|
|
23
|
+
group: appendRatio(token.group, formatGroupRatioLabel(groups, token.group)),
|
|
24
|
+
quota: token.unlimited_quota ? "不限" : String(token.remain_quota ?? 0),
|
|
25
|
+
status: formatStatusLabel(token),
|
|
26
|
+
mark: token.current ? "当前" : "",
|
|
27
|
+
})),
|
|
28
|
+
}, { width });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 分组列追加倍率:default · 1.0x;无倍率("-")时仅展示分组名
|
|
32
|
+
function appendRatio(group, ratioLabel) {
|
|
33
|
+
const name = group || "-";
|
|
34
|
+
return ratioLabel && ratioLabel !== "-" ? `${name} · ${ratioLabel}` : name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 分组倍率展示:以后端 /api/cli/groups 的 ratio 为准;ratio 为 null(如 auto 分组)
|
|
38
|
+
// 或分组未找到时显示 "-",不杜撰 0.0x。
|
|
39
|
+
function formatGroupRatioLabel(groups, groupName) {
|
|
40
|
+
const group = (Array.isArray(groups) ? groups : []).find((item) => item?.name === groupName);
|
|
41
|
+
const ratio = group?.ratio == null ? NaN : Number(group.ratio);
|
|
42
|
+
if (!Number.isFinite(ratio)) {
|
|
43
|
+
return "-";
|
|
44
|
+
}
|
|
45
|
+
return `${Number.isInteger(ratio) ? ratio.toFixed(1) : String(ratio)}x`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 分组表格:创建 Key 时展示各分组的倍率与说明,便于选择前对比。
|
|
49
|
+
export function renderGroupTable(groups, { width = 0 } = {}) {
|
|
7
50
|
return renderTable({
|
|
8
|
-
head: ["
|
|
9
|
-
rows:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
51
|
+
head: ["分组", "倍率", "说明"],
|
|
52
|
+
rows: (Array.isArray(groups) ? groups : []).map((group) => {
|
|
53
|
+
const name = group.display_name || group.name || "-";
|
|
54
|
+
const ratio = group?.ratio == null ? NaN : Number(group.ratio);
|
|
55
|
+
const ratioText = Number.isFinite(ratio) ? `${Number.isInteger(ratio) ? ratio.toFixed(1) : String(ratio)}x` : "-";
|
|
56
|
+
const desc = [
|
|
57
|
+
group.desc && group.desc !== name ? group.desc : "",
|
|
58
|
+
group.recommended ? "推荐" : "",
|
|
59
|
+
].filter(Boolean).join(" ") || "-";
|
|
60
|
+
return [name, ratioText, desc];
|
|
61
|
+
}),
|
|
18
62
|
}, { width });
|
|
19
63
|
}
|
|
20
64
|
|
|
21
65
|
function formatStatusLabel(token) {
|
|
22
66
|
if (token.status !== 1) {
|
|
23
|
-
return "禁用";
|
|
67
|
+
return "❌ 禁用";
|
|
24
68
|
}
|
|
25
69
|
const expired = Number(token.expired_time);
|
|
26
70
|
if (expired > 0 && expired * 1000 < Date.now()) {
|
|
27
|
-
return "过期";
|
|
71
|
+
return "⚠️ 过期";
|
|
28
72
|
}
|
|
29
|
-
return "正常";
|
|
73
|
+
return "✅ 正常";
|
|
30
74
|
}
|
|
31
75
|
|
|
32
76
|
function formatExpiryLabel(expiredTime) {
|
|
@@ -71,7 +115,7 @@ export const TOKEN_PURPOSES = [
|
|
|
71
115
|
];
|
|
72
116
|
|
|
73
117
|
// 创建新 API Key:选择用途(决定默认名)→ 选择分组(来自后端)→ 创建并返回明文
|
|
74
|
-
export async function createNewToken({ api, promptSelect, promptInput = async (_m, d) => d } = {}) {
|
|
118
|
+
export async function createNewToken({ api, promptSelect, promptInput = async (_m, d) => d, writeLine } = {}) {
|
|
75
119
|
if (!api) {
|
|
76
120
|
throw new Error("缺少 API client");
|
|
77
121
|
}
|
|
@@ -80,6 +124,9 @@ export async function createNewToken({ api, promptSelect, promptInput = async (_
|
|
|
80
124
|
}
|
|
81
125
|
const purpose = await promptSelect("请选择 Key 用途:", TOKEN_PURPOSES);
|
|
82
126
|
const groups = await loadGroups(api);
|
|
127
|
+
if (typeof writeLine === "function") {
|
|
128
|
+
writeLine(renderGroupTable(groups, { width: process.stdout.columns || 0 }));
|
|
129
|
+
}
|
|
83
130
|
const group = await selectGroup({ groups, promptSelect });
|
|
84
131
|
const name = await promptInput("新 API Key 名称", purpose);
|
|
85
132
|
const created = await api.post("/api/cli/tokens", {
|
|
@@ -116,8 +163,14 @@ export async function chooseOrCreateToken({
|
|
|
116
163
|
}
|
|
117
164
|
// 提供 writeLine 时先用表格展示 Key 详情,选项标签保持简短
|
|
118
165
|
const useTable = typeof writeLine === "function";
|
|
166
|
+
let groups = [];
|
|
119
167
|
if (useTable) {
|
|
120
|
-
|
|
168
|
+
try {
|
|
169
|
+
groups = await loadGroups(api);
|
|
170
|
+
} catch {
|
|
171
|
+
groups = [];
|
|
172
|
+
}
|
|
173
|
+
writeLine(renderTokenTable(tokens, { width: process.stdout.columns || 0, groups }));
|
|
121
174
|
}
|
|
122
175
|
const choices = [
|
|
123
176
|
...tokens.map((token) => ({
|
|
@@ -130,12 +183,18 @@ export async function chooseOrCreateToken({
|
|
|
130
183
|
];
|
|
131
184
|
const tokenId = await promptSelect("选择 API Key", choices);
|
|
132
185
|
if (tokenId === "__create__") {
|
|
133
|
-
return createNewToken({ api, promptSelect, promptInput });
|
|
186
|
+
return createNewToken({ api, promptSelect, promptInput, writeLine });
|
|
134
187
|
}
|
|
135
188
|
const selected = tokens.find((token) => token.id === tokenId);
|
|
136
189
|
if (!selected) {
|
|
137
190
|
throw new Error("选择的 API Key 无效");
|
|
138
191
|
}
|
|
192
|
+
if (typeof writeLine === "function") {
|
|
193
|
+
writeLine("");
|
|
194
|
+
writeLine("✅ 已选择 API Key");
|
|
195
|
+
writeLine("");
|
|
196
|
+
writeLine(renderSelectedTokenSummary(selected, { groups, width: process.stdout.columns || 0 }));
|
|
197
|
+
}
|
|
139
198
|
const revealed = await api.post(`/api/cli/tokens/${selected.id}/reveal`);
|
|
140
199
|
if (!revealed?.key) {
|
|
141
200
|
throw new Error("未返回 API Key");
|
|
@@ -149,7 +208,18 @@ export async function chooseOrCreateToken({
|
|
|
149
208
|
};
|
|
150
209
|
}
|
|
151
210
|
|
|
152
|
-
return createNewToken({ api, promptSelect, promptInput });
|
|
211
|
+
return createNewToken({ api, promptSelect, promptInput, writeLine });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function renderSelectedTokenSummary(token, { groups = [], width = 0 } = {}) {
|
|
215
|
+
return renderKeyValueTable([
|
|
216
|
+
["名称", token.name || `Token #${token.id}`],
|
|
217
|
+
["Key", formatTokenKey(token.masked_key || token.key)],
|
|
218
|
+
["分组", token.group || "-"],
|
|
219
|
+
["倍率", formatGroupRatioLabel(groups, token.group)],
|
|
220
|
+
["状态", formatStatusLabel(token)],
|
|
221
|
+
["额度", token.unlimited_quota ? "不限" : String(token.remain_quota ?? 0)],
|
|
222
|
+
], { width });
|
|
153
223
|
}
|
|
154
224
|
|
|
155
225
|
function formatStatus(status) {
|
package/src/tool-installer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sanitizeText } from "./sanitize.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { warningBlock } from "./ui.js";
|
|
3
4
|
|
|
4
5
|
const TOOL_COMMANDS = {
|
|
5
6
|
claude360: {
|
|
@@ -38,6 +39,7 @@ export async function installOrUpdateTools({
|
|
|
38
39
|
targets,
|
|
39
40
|
confirm,
|
|
40
41
|
execCommand = defaultInstallExec,
|
|
42
|
+
verbose = false,
|
|
41
43
|
} = {}) {
|
|
42
44
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
43
45
|
throw new Error("缺少安装或更新目标");
|
|
@@ -51,14 +53,17 @@ export async function installOrUpdateTools({
|
|
|
51
53
|
const install = buildInstallCommand(target);
|
|
52
54
|
const commandText = `${install.command} ${install.args.join(" ")}`;
|
|
53
55
|
const approved = await confirm(
|
|
54
|
-
|
|
56
|
+
warningBlock("即将执行全局 npm 操作", {
|
|
57
|
+
action: commandText,
|
|
58
|
+
impact: `当前用户的 npm 全局目录(安装或更新 ${install.toolName})`,
|
|
59
|
+
}),
|
|
55
60
|
);
|
|
56
61
|
if (!approved) {
|
|
57
62
|
results.push({ target, skipped: true });
|
|
58
63
|
continue;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
let result = await execCommand(install.command, install.args);
|
|
66
|
+
let result = await execCommand(install.command, install.args, { verbose });
|
|
62
67
|
let usedMirror = false;
|
|
63
68
|
// 官方源失败时支持国内镜像回退:一次性 --registry 参数重试,不修改用户全局 npm 配置
|
|
64
69
|
if (!result.ok) {
|
|
@@ -70,7 +75,7 @@ export async function installOrUpdateTools({
|
|
|
70
75
|
result = await execCommand(install.command, [
|
|
71
76
|
...install.args,
|
|
72
77
|
`--registry=${NPM_MIRROR_REGISTRY}`,
|
|
73
|
-
]);
|
|
78
|
+
], { verbose });
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
81
|
|
|
@@ -89,12 +94,23 @@ export async function installOrUpdateTools({
|
|
|
89
94
|
return results;
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
97
|
+
// 全局包安装可能耗时较久且不设超时,避免被诊断用的短超时执行器
|
|
98
|
+
// (defaultExecCommand)中途 SIGTERM 杀掉。verbose 时 inherit-stdio 实时显示 npm
|
|
99
|
+
// 进度;默认捕获输出,成功只在上层显示摘要,失败时把 stderr 带回供结构化错误展示。
|
|
100
|
+
function defaultInstallExec(command, args, { verbose = false } = {}) {
|
|
95
101
|
return new Promise((resolve) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
if (verbose) {
|
|
103
|
+
const child = spawn(command, args, { stdio: "inherit" });
|
|
104
|
+
child.on("error", (error) => resolve({ ok: false, error: error.message }));
|
|
105
|
+
child.on("close", (code) => resolve({ ok: code === 0 }));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
let stdout = "";
|
|
109
|
+
let stderr = "";
|
|
110
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
111
|
+
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
112
|
+
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
113
|
+
child.on("error", (error) => resolve({ ok: false, error: error.message, stdout, stderr }));
|
|
114
|
+
child.on("close", (code) => resolve({ ok: code === 0, stdout, stderr }));
|
|
99
115
|
});
|
|
100
116
|
}
|
package/src/tool-launcher.js
CHANGED
|
@@ -8,6 +8,7 @@ import os from "node:os";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { colorLevel } from "./colors.js";
|
|
10
10
|
import { createMessenger } from "./messages.js";
|
|
11
|
+
import { renderDivider, renderKeyValueTable, renderTaskEnd, renderTaskStart, renderTaskStep, warningBlock } from "./ui.js";
|
|
11
12
|
|
|
12
13
|
// 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
|
|
13
14
|
const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
@@ -91,6 +92,11 @@ export async function configureCodexProvider({
|
|
|
91
92
|
confirmConflict,
|
|
92
93
|
writeLine = () => {},
|
|
93
94
|
} = {}) {
|
|
95
|
+
writeLine(renderTaskStart("配置 Codex Provider", {
|
|
96
|
+
intro: ["检查现有配置", "备份配置", "写入 Claude360 Provider", "验证配置"],
|
|
97
|
+
}));
|
|
98
|
+
writeLine("");
|
|
99
|
+
writeLine(renderTaskStep(1, 4, "检查现有配置"));
|
|
94
100
|
const current = await readConfigIfExists(readFile, configPath);
|
|
95
101
|
const currentProfile = await readConfigIfExists(readFile, profileConfigPath);
|
|
96
102
|
const desired = {
|
|
@@ -100,6 +106,11 @@ export async function configureCodexProvider({
|
|
|
100
106
|
wireApi: "responses",
|
|
101
107
|
};
|
|
102
108
|
await confirmCodexConfigConflict({ current, currentProfile, desired, confirmConflict });
|
|
109
|
+
writeLine(renderKeyValueTable([
|
|
110
|
+
["配置文件", configPath],
|
|
111
|
+
["Profile 覆盖层", profileConfigPath],
|
|
112
|
+
["目标 Provider", desired.providerId],
|
|
113
|
+
], { width: process.stdout.columns || 0 }));
|
|
103
114
|
|
|
104
115
|
// 基础层(config.toml):写入 provider 定义,并迁移移除 legacy 的
|
|
105
116
|
// [profiles.claude360] 表与顶层 profile 选择器(v2 禁止其与 --profile 共存)。
|
|
@@ -119,11 +130,38 @@ export async function configureCodexProvider({
|
|
|
119
130
|
}
|
|
120
131
|
|
|
121
132
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
133
|
+
writeLine("");
|
|
134
|
+
writeLine(renderDivider("section"));
|
|
135
|
+
writeLine("");
|
|
136
|
+
writeLine(renderTaskStep(2, 4, "备份配置"));
|
|
122
137
|
// 写入前备份原配置(PRD 安全要求):两个文件各自备份
|
|
123
138
|
const backupPath = await backupIfExists({ copyFile, filePath: configPath, exists: current !== "", writeLine, label: "原 Codex 配置" });
|
|
124
|
-
await writeFile(configPath, content, "utf8");
|
|
125
139
|
const profileBackupPath = await backupIfExists({ copyFile, filePath: profileConfigPath, exists: currentProfile !== "", writeLine, label: "原 Codex profile 配置" });
|
|
140
|
+
if (!backupPath && !profileBackupPath) {
|
|
141
|
+
mk(writeLine).info("未发现现有配置,跳过备份。");
|
|
142
|
+
}
|
|
143
|
+
writeLine("");
|
|
144
|
+
writeLine(renderDivider("section"));
|
|
145
|
+
writeLine("");
|
|
146
|
+
writeLine(renderTaskStep(3, 4, "写入 Claude360 Provider"));
|
|
147
|
+
await writeFile(configPath, content, "utf8");
|
|
126
148
|
await writeFile(profileConfigPath, profileContent, "utf8");
|
|
149
|
+
mk(writeLine).success("已写入 model_providers.claude360 与 claude360 profile 覆盖层。");
|
|
150
|
+
writeLine("");
|
|
151
|
+
writeLine(renderDivider("section"));
|
|
152
|
+
writeLine("");
|
|
153
|
+
writeLine(renderTaskStep(4, 4, "验证配置"));
|
|
154
|
+
mk(writeLine).success("Codex Provider 配置已通过 TOML 校验。");
|
|
155
|
+
writeLine("");
|
|
156
|
+
writeLine(renderTaskEnd("Codex Provider 配置完成", {
|
|
157
|
+
summary: [
|
|
158
|
+
["启动命令", "codex --profile claude360"],
|
|
159
|
+
["配置文件", configPath],
|
|
160
|
+
["Profile 覆盖层", profileConfigPath],
|
|
161
|
+
["Provider", desired.providerId],
|
|
162
|
+
["下一步", "\n1. 立即启动 Codex\n2. 返回主菜单"],
|
|
163
|
+
],
|
|
164
|
+
}));
|
|
127
165
|
return { content, profileContent, backupPath, profileBackupPath };
|
|
128
166
|
}
|
|
129
167
|
|
|
@@ -184,7 +222,10 @@ async function confirmCodexConfigConflict({ current, currentProfile, desired, co
|
|
|
184
222
|
throw new Error(`Codex Claude360 配置存在冲突:${conflicts.join("; ")}`);
|
|
185
223
|
}
|
|
186
224
|
const approved = await confirmConflict(
|
|
187
|
-
|
|
225
|
+
warningBlock("即将覆盖 Codex Claude360 Provider 配置", {
|
|
226
|
+
action: `覆盖冲突字段:\n${conflicts.join("\n")}`,
|
|
227
|
+
impact: "~/.codex/config.toml 与 claude360 profile 覆盖层",
|
|
228
|
+
}),
|
|
188
229
|
);
|
|
189
230
|
if (!approved) {
|
|
190
231
|
throw new Error(`Codex Claude360 配置存在冲突:${conflicts.join("; ")}`);
|
package/src/topup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { colorLevel } from "./colors.js";
|
|
2
2
|
import { createMessenger } from "./messages.js";
|
|
3
|
-
import { renderKeyValueTable, renderSectionTitle } from "./ui.js";
|
|
3
|
+
import { renderChoiceTable, renderDivider, renderKeyValueTable, renderSectionTitle, renderTaskEnd, renderTaskStart, renderTaskStep } from "./ui.js";
|
|
4
4
|
|
|
5
5
|
export async function loadTopUpOptions(api) {
|
|
6
6
|
if (!api) {
|
|
@@ -34,12 +34,25 @@ export async function runWechatTopUp({
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const msg = createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
37
|
+
writeLine(renderTaskStart("微信扫码充值", {
|
|
38
|
+
intro: ["加载充值选项", "选择充值金额", "创建支付订单", "等待微信支付", "刷新余额"],
|
|
39
|
+
}));
|
|
40
|
+
writeLine("");
|
|
41
|
+
writeLine(renderTaskStep(1, 5, "加载充值选项"));
|
|
37
42
|
const options = await loadTopUpOptions(api);
|
|
38
43
|
// 后端口径前置拦截(验收 P1-2):禁用时不进金额选择、不创建订单
|
|
39
44
|
if (options?.wechat_enabled === false) {
|
|
40
45
|
throw new Error("微信支付未启用,请使用网页充值。");
|
|
41
46
|
}
|
|
42
|
-
|
|
47
|
+
writeLine("");
|
|
48
|
+
writeLine(renderDivider("section"));
|
|
49
|
+
writeLine("");
|
|
50
|
+
writeLine(renderTaskStep(2, 5, "选择充值金额"));
|
|
51
|
+
const amount = await chooseTopUpAmount({ options, promptSelect, promptInput, writeLine });
|
|
52
|
+
writeLine("");
|
|
53
|
+
writeLine(renderDivider("section"));
|
|
54
|
+
writeLine("");
|
|
55
|
+
writeLine(renderTaskStep(3, 5, "创建支付订单"));
|
|
43
56
|
const order = await api.post("/api/cli/topup/wechat", { amount });
|
|
44
57
|
if (!order?.order_id || !order?.code_url) {
|
|
45
58
|
throw new Error("创建微信充值订单失败");
|
|
@@ -54,6 +67,10 @@ export async function runWechatTopUp({
|
|
|
54
67
|
]));
|
|
55
68
|
msg.info("请使用微信扫码支付:");
|
|
56
69
|
await printQrOrCodeUrl({ codeUrl: order.code_url, renderQr, writeLine });
|
|
70
|
+
writeLine("");
|
|
71
|
+
writeLine(renderDivider("section"));
|
|
72
|
+
writeLine("");
|
|
73
|
+
writeLine(renderTaskStep(4, 5, "等待微信支付"));
|
|
57
74
|
const status = await waitTopUpPaid({
|
|
58
75
|
api,
|
|
59
76
|
orderId: order.order_id,
|
|
@@ -62,11 +79,22 @@ export async function runWechatTopUp({
|
|
|
62
79
|
maxPolls,
|
|
63
80
|
writeStatus,
|
|
64
81
|
});
|
|
82
|
+
writeLine("");
|
|
83
|
+
writeLine(renderDivider("section"));
|
|
84
|
+
writeLine("");
|
|
85
|
+
writeLine(renderTaskStep(5, 5, "刷新余额"));
|
|
65
86
|
const balance = await api.get("/api/cli/me");
|
|
87
|
+
if (balance?.balance_display) {
|
|
88
|
+
msg.result("当前余额", balance.balance_display);
|
|
89
|
+
}
|
|
90
|
+
writeLine("");
|
|
91
|
+
writeLine(renderTaskEnd("微信扫码充值完成", {
|
|
92
|
+
summary: [["充值金额", String(order.money_display || order.money || amount)]],
|
|
93
|
+
}));
|
|
66
94
|
return { order, status, balance };
|
|
67
95
|
}
|
|
68
96
|
|
|
69
|
-
async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
97
|
+
async function chooseTopUpAmount({ options, promptSelect, promptInput, writeLine }) {
|
|
70
98
|
const amountOptions = Array.isArray(options?.amount_options)
|
|
71
99
|
? options.amount_options.filter((amount) => Number.isFinite(amount) && amount > 0)
|
|
72
100
|
: [];
|
|
@@ -74,6 +102,18 @@ async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
|
74
102
|
if (typeof promptSelect !== "function") {
|
|
75
103
|
throw new Error("缺少充值金额选择输入");
|
|
76
104
|
}
|
|
105
|
+
if (typeof writeLine === "function") {
|
|
106
|
+
writeLine(renderChoiceTable({
|
|
107
|
+
columns: [
|
|
108
|
+
["index", "序号"],
|
|
109
|
+
["amount", "金额"],
|
|
110
|
+
],
|
|
111
|
+
rows: amountOptions.map((amount, index) => ({
|
|
112
|
+
index: String(index + 1),
|
|
113
|
+
amount: `¥${amount}`,
|
|
114
|
+
})),
|
|
115
|
+
}, { width: process.stdout.columns || 0 }));
|
|
116
|
+
}
|
|
77
117
|
const selected = await promptSelect("选择充值金额", amountOptions.map((amount) => ({
|
|
78
118
|
label: `¥${amount}`,
|
|
79
119
|
value: amount,
|
|
@@ -81,6 +121,9 @@ async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
|
81
121
|
if (!amountOptions.includes(selected)) {
|
|
82
122
|
throw new Error("请选择后端返回的充值金额");
|
|
83
123
|
}
|
|
124
|
+
if (typeof writeLine === "function") {
|
|
125
|
+
writeLine(`✅ 已选择:¥${selected}`);
|
|
126
|
+
}
|
|
84
127
|
return selected;
|
|
85
128
|
}
|
|
86
129
|
|
|
@@ -95,6 +138,9 @@ async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
|
95
138
|
if (minTopUp > 0 && amount < minTopUp) {
|
|
96
139
|
throw new Error(`充值金额不能低于 ${minTopUp}`);
|
|
97
140
|
}
|
|
141
|
+
if (typeof writeLine === "function") {
|
|
142
|
+
writeLine(`✅ 已选择:¥${amount}`);
|
|
143
|
+
}
|
|
98
144
|
return amount;
|
|
99
145
|
}
|
|
100
146
|
|