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/README.md +107 -126
- package/bin/claude360.js +7 -3
- package/package.json +1 -1
- package/src/account-status.js +8 -5
- package/src/auth.js +9 -2
- package/src/banner.js +47 -6
- package/src/cc-switch.js +34 -6
- package/src/colors.js +31 -0
- package/src/diagnostics.js +68 -25
- package/src/glyphs.js +33 -0
- package/src/index.js +248 -105
- package/src/init-config.js +61 -27
- package/src/init-flow.js +25 -6
- package/src/mcp-skill.js +48 -21
- package/src/menu.js +20 -13
- package/src/messages.js +78 -0
- package/src/notices.js +33 -0
- package/src/onboarding.js +12 -5
- package/src/prompts.js +7 -6
- package/src/token-manager.js +90 -20
- package/src/tool-installer.js +25 -9
- package/src/tool-launcher.js +150 -46
- package/src/topup.js +55 -5
- package/src/ui.js +264 -8
- package/src/workflows.js +10 -7
- package/src/zcf-notice.js +15 -1
package/src/topup.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { colorLevel } from "./colors.js";
|
|
2
|
+
import { createMessenger } from "./messages.js";
|
|
3
|
+
import { renderChoiceTable, renderDivider, renderKeyValueTable, renderSectionTitle, renderTaskEnd, renderTaskStart, renderTaskStep } from "./ui.js";
|
|
2
4
|
|
|
3
5
|
export async function loadTopUpOptions(api) {
|
|
4
6
|
if (!api) {
|
|
@@ -31,12 +33,26 @@ export async function runWechatTopUp({
|
|
|
31
33
|
throw new Error("缺少 API client");
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
const msg = createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
37
|
+
writeLine(renderTaskStart("微信扫码充值", {
|
|
38
|
+
intro: ["加载充值选项", "选择充值金额", "创建支付订单", "等待微信支付", "刷新余额"],
|
|
39
|
+
}));
|
|
40
|
+
writeLine("");
|
|
41
|
+
writeLine(renderTaskStep(1, 5, "加载充值选项"));
|
|
34
42
|
const options = await loadTopUpOptions(api);
|
|
35
43
|
// 后端口径前置拦截(验收 P1-2):禁用时不进金额选择、不创建订单
|
|
36
44
|
if (options?.wechat_enabled === false) {
|
|
37
45
|
throw new Error("微信支付未启用,请使用网页充值。");
|
|
38
46
|
}
|
|
39
|
-
|
|
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, "创建支付订单"));
|
|
40
56
|
const order = await api.post("/api/cli/topup/wechat", { amount });
|
|
41
57
|
if (!order?.order_id || !order?.code_url) {
|
|
42
58
|
throw new Error("创建微信充值订单失败");
|
|
@@ -49,8 +65,12 @@ export async function runWechatTopUp({
|
|
|
49
65
|
["金额", String(order.money_display || order.money || amount)],
|
|
50
66
|
["支付方式", "微信扫码"],
|
|
51
67
|
]));
|
|
52
|
-
|
|
68
|
+
msg.info("请使用微信扫码支付:");
|
|
53
69
|
await printQrOrCodeUrl({ codeUrl: order.code_url, renderQr, writeLine });
|
|
70
|
+
writeLine("");
|
|
71
|
+
writeLine(renderDivider("section"));
|
|
72
|
+
writeLine("");
|
|
73
|
+
writeLine(renderTaskStep(4, 5, "等待微信支付"));
|
|
54
74
|
const status = await waitTopUpPaid({
|
|
55
75
|
api,
|
|
56
76
|
orderId: order.order_id,
|
|
@@ -59,11 +79,22 @@ export async function runWechatTopUp({
|
|
|
59
79
|
maxPolls,
|
|
60
80
|
writeStatus,
|
|
61
81
|
});
|
|
82
|
+
writeLine("");
|
|
83
|
+
writeLine(renderDivider("section"));
|
|
84
|
+
writeLine("");
|
|
85
|
+
writeLine(renderTaskStep(5, 5, "刷新余额"));
|
|
62
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
|
+
}));
|
|
63
94
|
return { order, status, balance };
|
|
64
95
|
}
|
|
65
96
|
|
|
66
|
-
async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
97
|
+
async function chooseTopUpAmount({ options, promptSelect, promptInput, writeLine }) {
|
|
67
98
|
const amountOptions = Array.isArray(options?.amount_options)
|
|
68
99
|
? options.amount_options.filter((amount) => Number.isFinite(amount) && amount > 0)
|
|
69
100
|
: [];
|
|
@@ -71,6 +102,18 @@ async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
|
71
102
|
if (typeof promptSelect !== "function") {
|
|
72
103
|
throw new Error("缺少充值金额选择输入");
|
|
73
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
|
+
}
|
|
74
117
|
const selected = await promptSelect("选择充值金额", amountOptions.map((amount) => ({
|
|
75
118
|
label: `¥${amount}`,
|
|
76
119
|
value: amount,
|
|
@@ -78,6 +121,9 @@ async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
|
78
121
|
if (!amountOptions.includes(selected)) {
|
|
79
122
|
throw new Error("请选择后端返回的充值金额");
|
|
80
123
|
}
|
|
124
|
+
if (typeof writeLine === "function") {
|
|
125
|
+
writeLine(`✅ 已选择:¥${selected}`);
|
|
126
|
+
}
|
|
81
127
|
return selected;
|
|
82
128
|
}
|
|
83
129
|
|
|
@@ -92,17 +138,21 @@ async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
|
92
138
|
if (minTopUp > 0 && amount < minTopUp) {
|
|
93
139
|
throw new Error(`充值金额不能低于 ${minTopUp}`);
|
|
94
140
|
}
|
|
141
|
+
if (typeof writeLine === "function") {
|
|
142
|
+
writeLine(`✅ 已选择:¥${amount}`);
|
|
143
|
+
}
|
|
95
144
|
return amount;
|
|
96
145
|
}
|
|
97
146
|
|
|
98
147
|
async function printQrOrCodeUrl({ codeUrl, renderQr, writeLine }) {
|
|
148
|
+
const msg = createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
99
149
|
try {
|
|
100
150
|
const qr = await renderQr(codeUrl);
|
|
101
151
|
if (qr) {
|
|
102
152
|
writeLine(qr);
|
|
103
153
|
}
|
|
104
154
|
} catch {
|
|
105
|
-
|
|
155
|
+
msg.warn(`二维码渲染失败,请复制 code_url 完成支付:${codeUrl}`);
|
|
106
156
|
}
|
|
107
157
|
}
|
|
108
158
|
|
package/src/ui.js
CHANGED
|
@@ -5,18 +5,22 @@
|
|
|
5
5
|
|
|
6
6
|
import { displayWidth } from "./banner.js";
|
|
7
7
|
|
|
8
|
-
import { BOLD, RESET, fg, toLevel } from "./colors.js";
|
|
8
|
+
import { BOLD, PALETTE, RESET, fg, theme, toLevel } from "./colors.js";
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// 表格/标题配色:从统一语义主题取色(见 colors.js theme),不再各自硬编码 RGB。
|
|
11
11
|
function palette(level) {
|
|
12
|
+
const t = theme(level);
|
|
12
13
|
return {
|
|
13
|
-
border:
|
|
14
|
-
head:
|
|
14
|
+
border: t.border, // 边框:青灰
|
|
15
|
+
head: t.info, // 表头:天蓝
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
// eslint-disable-next-line no-control-regex
|
|
19
20
|
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
21
|
+
const PAGE_DIVIDER = "────────────────────────────────────────";
|
|
22
|
+
const SECTION_DIVIDER = "----------------------------------------";
|
|
23
|
+
const LIGHT_DIVIDER = "········································";
|
|
20
24
|
|
|
21
25
|
export function stripAnsi(text) {
|
|
22
26
|
return String(text ?? "").replace(ANSI_PATTERN, "");
|
|
@@ -177,10 +181,10 @@ export function renderHeader(title, { subtitle = "", color = false } = {}) {
|
|
|
177
181
|
// 四种状态:文字前缀(mark)始终存在,颜色仅增强层级(需求二「不依赖颜色表达唯一信息」)
|
|
178
182
|
const BOX_MARKS = { info: "i", warn: "!", error: "×", success: "✓" };
|
|
179
183
|
const BOX_RGB = {
|
|
180
|
-
info:
|
|
181
|
-
warn:
|
|
182
|
-
error:
|
|
183
|
-
success:
|
|
184
|
+
info: PALETTE.info,
|
|
185
|
+
warn: PALETTE.warn,
|
|
186
|
+
error: PALETTE.error,
|
|
187
|
+
success: PALETTE.success,
|
|
184
188
|
};
|
|
185
189
|
|
|
186
190
|
export function renderBox(message, { kind = "info", color = false, width = 0 } = {}) {
|
|
@@ -220,3 +224,255 @@ export function renderModelTable(models = [], { color = false, width = 0 } = {})
|
|
|
220
224
|
]),
|
|
221
225
|
}, { color, width });
|
|
222
226
|
}
|
|
227
|
+
|
|
228
|
+
// ──────────────────────────────────────────────
|
|
229
|
+
// 执行流程组件(优化需求 V2):任务页 / 步骤 / 分割线 / 选择卡片 / 结构化错误
|
|
230
|
+
// ──────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
export function renderDivider(type = "section") {
|
|
233
|
+
if (type === "page") {
|
|
234
|
+
return PAGE_DIVIDER;
|
|
235
|
+
}
|
|
236
|
+
if (type === "light") {
|
|
237
|
+
return LIGHT_DIVIDER;
|
|
238
|
+
}
|
|
239
|
+
return SECTION_DIVIDER;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function renderTaskStart(title, { intro = [] } = {}) {
|
|
243
|
+
const lines = [
|
|
244
|
+
PAGE_DIVIDER,
|
|
245
|
+
`🚀 任务:${title}`,
|
|
246
|
+
PAGE_DIVIDER,
|
|
247
|
+
];
|
|
248
|
+
const items = Array.isArray(intro) ? intro.filter(Boolean) : [];
|
|
249
|
+
if (items.length > 0) {
|
|
250
|
+
lines.push("", "本流程将完成:");
|
|
251
|
+
lines.push(...items.slice(0, 5).map((item, index) => `${index + 1}. ${item}`));
|
|
252
|
+
}
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function renderTaskEnd(title, { summary = [] } = {}) {
|
|
257
|
+
const lines = [
|
|
258
|
+
PAGE_DIVIDER,
|
|
259
|
+
`🎉 ${title}`,
|
|
260
|
+
PAGE_DIVIDER,
|
|
261
|
+
];
|
|
262
|
+
const items = Array.isArray(summary) ? summary : [];
|
|
263
|
+
if (items.length > 0) {
|
|
264
|
+
lines.push("");
|
|
265
|
+
for (const [label, value] of items) {
|
|
266
|
+
lines.push(`${label}:${value}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return lines.join("\n");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function renderTaskStep(current, total, title, { icon = "▶" } = {}) {
|
|
273
|
+
return `${icon} [${current}/${total}] ${title}`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function normalizeColumns(columns = []) {
|
|
277
|
+
return columns.map((column) => {
|
|
278
|
+
if (Array.isArray(column)) {
|
|
279
|
+
return { key: column[0], label: column[1] ?? column[0] };
|
|
280
|
+
}
|
|
281
|
+
if (typeof column === "string") {
|
|
282
|
+
return { key: column, label: column };
|
|
283
|
+
}
|
|
284
|
+
return { key: column.key, label: column.label ?? column.key };
|
|
285
|
+
}).filter((column) => column.key);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function rowValue(row, key) {
|
|
289
|
+
if (Array.isArray(row)) {
|
|
290
|
+
return row[key] ?? "";
|
|
291
|
+
}
|
|
292
|
+
return row?.[key] ?? "";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function limitRows(rows, maxRows) {
|
|
296
|
+
const limit = Number(maxRows);
|
|
297
|
+
if (!Number.isInteger(limit) || limit <= 0 || rows.length <= limit) {
|
|
298
|
+
return { visibleRows: rows, hiddenCount: 0 };
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
visibleRows: rows.slice(0, limit),
|
|
302
|
+
hiddenCount: rows.length - limit,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function foldedHint(hiddenCount) {
|
|
307
|
+
return `还有 ${hiddenCount} 项,使用 --verbose 或详情命令查看更多。`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function renderChoiceTable({ columns = [], rows = [] } = {}, { color = false, width = 0, cardBreakpoint = 72, maxRows = 0, titleKey = "" } = {}) {
|
|
311
|
+
const cols = normalizeColumns(columns);
|
|
312
|
+
const { visibleRows, hiddenCount } = limitRows(rows, maxRows);
|
|
313
|
+
const table = renderSeparatedTable({
|
|
314
|
+
head: cols.map((column) => column.label),
|
|
315
|
+
rows: visibleRows.map((row) => cols.map((column) => String(rowValue(row, column.key)))),
|
|
316
|
+
}, { color, width });
|
|
317
|
+
const tableWidth = cellWidth(table.split("\n")[0] ?? "");
|
|
318
|
+
const appendHint = (output) => hiddenCount > 0 ? `${output}\n${foldedHint(hiddenCount)}` : output;
|
|
319
|
+
if (!width || (width >= cardBreakpoint && tableWidth <= width)) {
|
|
320
|
+
return appendHint(table);
|
|
321
|
+
}
|
|
322
|
+
return appendHint(visibleRows.map((row) => renderChoiceCard(row, cols, { color, width, titleKey })).join("\n\n"));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function renderSeparatedTable({ head = [], rows = [] } = {}, { color = false, width = 0 } = {}) {
|
|
326
|
+
const level = toLevel(color);
|
|
327
|
+
const widths = resolveWidths(head, rows, width);
|
|
328
|
+
const fit = (row) => widths.map((w, i) => truncateDisplay(row[i] ?? "", w));
|
|
329
|
+
const lines = [borderLine(widths, ["┌", "┬", "┐"], level)];
|
|
330
|
+
if (head.length > 0) {
|
|
331
|
+
lines.push(contentLine(fit(head), widths, { level, bold: true }));
|
|
332
|
+
lines.push(borderLine(widths, ["├", "┼", "┤"], level));
|
|
333
|
+
}
|
|
334
|
+
rows.forEach((row, index) => {
|
|
335
|
+
lines.push(contentLine(fit(row), widths, { level }));
|
|
336
|
+
if (index < rows.length - 1) {
|
|
337
|
+
lines.push(borderLine(widths, ["├", "┼", "┤"], level));
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
lines.push(borderLine(widths, ["└", "┴", "┘"], level));
|
|
341
|
+
return lines.join("\n");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function renderChoiceCard(row, columns, options) {
|
|
345
|
+
const { titleKey = "" } = options;
|
|
346
|
+
const indexKey = columns.find((column) => column.key === "index")?.key;
|
|
347
|
+
const nameKey = titleKey
|
|
348
|
+
? columns.find((column) => column.key === titleKey)?.key
|
|
349
|
+
: columns.find((column) => column.key === "name" || column.key === "label")?.key;
|
|
350
|
+
const markKey = columns.find((column) => column.key === "mark" || column.key === "badge")?.key;
|
|
351
|
+
const title = [
|
|
352
|
+
!titleKey && indexKey ? `${rowValue(row, indexKey)}.` : "",
|
|
353
|
+
nameKey ? rowValue(row, nameKey) : "",
|
|
354
|
+
].filter(Boolean).join(" ").trim() || String(rowValue(row, columns[0]?.key) || "");
|
|
355
|
+
const badge = markKey ? rowValue(row, markKey) : "";
|
|
356
|
+
const fields = columns
|
|
357
|
+
.filter((column) => ![indexKey, nameKey, markKey].includes(column.key))
|
|
358
|
+
.map((column) => [column.label, rowValue(row, column.key)])
|
|
359
|
+
.filter(([, value]) => value !== "" && value !== null && value !== undefined);
|
|
360
|
+
return renderCard(title, fields, { ...options, badge });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function renderCard(title, fields = [], { badge = "", color = false, width = 0 } = {}) {
|
|
364
|
+
const level = toLevel(color);
|
|
365
|
+
const c = level ? palette(level) : null;
|
|
366
|
+
const maxWidth = width > 0 ? width : 0;
|
|
367
|
+
const desiredInner = Math.max(
|
|
368
|
+
cellWidth(`${title}${badge ? ` ${badge}` : ""}`),
|
|
369
|
+
...fields.map(([label, value]) => cellWidth(`${label}:${value}`)),
|
|
370
|
+
12,
|
|
371
|
+
) + 2;
|
|
372
|
+
const inner = maxWidth ? Math.max(8, Math.min(desiredInner, maxWidth - 2)) : desiredInner;
|
|
373
|
+
const contentWidth = Math.max(1, inner - 2);
|
|
374
|
+
const edge = (left, right) => {
|
|
375
|
+
const text = `${left}${"─".repeat(inner)}${right}`;
|
|
376
|
+
return level ? `${c.border}${text}${RESET}` : text;
|
|
377
|
+
};
|
|
378
|
+
const line = (text) => {
|
|
379
|
+
const fitted = truncateDisplay(text, contentWidth);
|
|
380
|
+
const body = ` ${padCell(fitted, contentWidth)} `;
|
|
381
|
+
return level ? `${c.border}│${RESET}${body}${c.border}│${RESET}` : `│${body}│`;
|
|
382
|
+
};
|
|
383
|
+
const badgeText = String(badge);
|
|
384
|
+
const badgeW = cellWidth(badgeText);
|
|
385
|
+
// badge(当前/推荐/异常标记)优先保留:窄卡片下截断 title 而非 badge(需求第七节)
|
|
386
|
+
let titleText;
|
|
387
|
+
if (badgeText) {
|
|
388
|
+
const titleMax = Math.max(1, contentWidth - badgeW - 1);
|
|
389
|
+
const fittedTitle = truncateDisplay(title, titleMax);
|
|
390
|
+
const gap = Math.max(1, contentWidth - cellWidth(fittedTitle) - badgeW);
|
|
391
|
+
titleText = `${fittedTitle}${" ".repeat(gap)}${badgeText}`;
|
|
392
|
+
} else {
|
|
393
|
+
titleText = truncateDisplay(title, contentWidth);
|
|
394
|
+
}
|
|
395
|
+
const lines = [edge("┌", "┐"), line(titleText)];
|
|
396
|
+
for (const [label, value] of fields) {
|
|
397
|
+
lines.push(line(`${label}:${value}`));
|
|
398
|
+
}
|
|
399
|
+
lines.push(edge("└", "┘"));
|
|
400
|
+
return lines.join("\n");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function renderStructuredError(title, {
|
|
404
|
+
reason = "",
|
|
405
|
+
suggestions = [],
|
|
406
|
+
detailCommand = "",
|
|
407
|
+
} = {}) {
|
|
408
|
+
const lines = [`❌ ${title}`];
|
|
409
|
+
if (reason) {
|
|
410
|
+
lines.push("", "原因:", String(reason));
|
|
411
|
+
}
|
|
412
|
+
const items = Array.isArray(suggestions) ? suggestions.filter(Boolean) : [];
|
|
413
|
+
if (items.length > 0) {
|
|
414
|
+
lines.push("", "建议:");
|
|
415
|
+
lines.push(...items.map((item, index) => `${index + 1}. ${item}`));
|
|
416
|
+
}
|
|
417
|
+
if (detailCommand) {
|
|
418
|
+
lines.push("", "查看详细日志:", detailCommand);
|
|
419
|
+
}
|
|
420
|
+
return lines.join("\n");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export const taskStart = renderTaskStart;
|
|
424
|
+
export const taskEnd = renderTaskEnd;
|
|
425
|
+
export const step = renderTaskStep;
|
|
426
|
+
export const divider = renderDivider;
|
|
427
|
+
export const errorBlock = renderStructuredError;
|
|
428
|
+
export const card = renderCard;
|
|
429
|
+
|
|
430
|
+
export function warningBlock(title, {
|
|
431
|
+
action = "",
|
|
432
|
+
impact = "",
|
|
433
|
+
prompt = "",
|
|
434
|
+
} = {}) {
|
|
435
|
+
const lines = [`⚠️ ${title}`];
|
|
436
|
+
if (action) {
|
|
437
|
+
lines.push("", "将执行:", String(action));
|
|
438
|
+
}
|
|
439
|
+
if (impact) {
|
|
440
|
+
lines.push("", "影响范围:", String(impact));
|
|
441
|
+
}
|
|
442
|
+
if (prompt) {
|
|
443
|
+
lines.push("", String(prompt));
|
|
444
|
+
}
|
|
445
|
+
return lines.join("\n");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function selectedSummary(title, pairs = []) {
|
|
449
|
+
const lines = [String(title)];
|
|
450
|
+
const body = summary(pairs);
|
|
451
|
+
if (body) {
|
|
452
|
+
lines.push(body);
|
|
453
|
+
}
|
|
454
|
+
return lines.join("\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function table(data = {}, options = {}) {
|
|
458
|
+
if (Array.isArray(data.columns)) {
|
|
459
|
+
return renderChoiceTable(data, options);
|
|
460
|
+
}
|
|
461
|
+
return renderTable(data, options);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function summary(items = []) {
|
|
465
|
+
return items.map(([label, value]) => `${label}:${value}`).join("\n");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function command(text) {
|
|
469
|
+
return String(text ?? "");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function filePath(text) {
|
|
473
|
+
return String(text ?? "");
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function current(label, value) {
|
|
477
|
+
return `${label}:${value}`;
|
|
478
|
+
}
|
package/src/workflows.js
CHANGED
|
@@ -8,6 +8,8 @@ import path from "node:path";
|
|
|
8
8
|
|
|
9
9
|
import { createBackup } from "./backup.js";
|
|
10
10
|
import { ZCF_ATTRIBUTION_COMMENT } from "./zcf-notice.js";
|
|
11
|
+
import { colorLevel } from "./colors.js";
|
|
12
|
+
import { createMessenger } from "./messages.js";
|
|
11
13
|
|
|
12
14
|
const defaultFs = { copyFile, cp, mkdir, readFile, stat, writeFile };
|
|
13
15
|
|
|
@@ -229,6 +231,7 @@ async function installWorkflowSet({
|
|
|
229
231
|
if (typeof multiSelect !== "function" || typeof confirm !== "function") {
|
|
230
232
|
throw new Error("缺少交互输入");
|
|
231
233
|
}
|
|
234
|
+
const msg = createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
232
235
|
const selected = await multiSelect({
|
|
233
236
|
message: "请选择要安装的工作流:",
|
|
234
237
|
choices: workflows.map((workflow) => ({
|
|
@@ -238,23 +241,23 @@ async function installWorkflowSet({
|
|
|
238
241
|
})),
|
|
239
242
|
});
|
|
240
243
|
if (selected.length === 0) {
|
|
241
|
-
|
|
244
|
+
msg.info("已跳过工作流安装。");
|
|
242
245
|
return { installed: [], skipped: true };
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
const picked = workflows.filter((workflow) => selected.includes(workflow.id));
|
|
246
|
-
|
|
249
|
+
msg.info(`安装将写入目录:${targetDir}`);
|
|
247
250
|
|
|
248
251
|
// 备份已存在的同名文件(一次性集中备份到时间戳目录)
|
|
249
252
|
const targets = picked.flatMap((workflow) => workflow.files.map((file) => path.join(targetDir, file.name)));
|
|
250
253
|
const { backupDir } = await createBackup({ baseDir: backupBaseDir, paths: targets, fs, now });
|
|
251
254
|
if (backupDir) {
|
|
252
|
-
|
|
255
|
+
msg.success(`已创建备份:${backupDir}`);
|
|
253
256
|
}
|
|
254
257
|
|
|
255
258
|
const installed = [];
|
|
256
259
|
for (const workflow of picked) {
|
|
257
|
-
|
|
260
|
+
msg.step(`正在安装工作流:${workflow.label}...`);
|
|
258
261
|
let wroteAny = false;
|
|
259
262
|
for (const file of workflow.files) {
|
|
260
263
|
const filePath = path.join(targetDir, file.name);
|
|
@@ -263,19 +266,19 @@ async function installWorkflowSet({
|
|
|
263
266
|
if (existing !== file.content) {
|
|
264
267
|
const approved = await confirm(`文件已存在:${filePath}\n是否覆盖?(原文件已备份)`);
|
|
265
268
|
if (!approved) {
|
|
266
|
-
|
|
269
|
+
msg.info(`已跳过:${file.name}`);
|
|
267
270
|
continue;
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
}
|
|
271
274
|
await fs.mkdir(targetDir, { recursive: true });
|
|
272
275
|
await fs.writeFile(filePath, file.content, "utf8");
|
|
273
|
-
|
|
276
|
+
msg.success(`已安装命令:${file.name}`);
|
|
274
277
|
wroteAny = true;
|
|
275
278
|
}
|
|
276
279
|
if (wroteAny) {
|
|
277
280
|
installed.push(workflow.id);
|
|
278
|
-
|
|
281
|
+
msg.success(`${workflow.label}安装成功`);
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
if (installed.length > 0 && usageHint) {
|
package/src/zcf-notice.js
CHANGED
|
@@ -17,6 +17,16 @@ export const OPEN_SOURCE_NOTICE = [
|
|
|
17
17
|
"提供授权登录、API Key、余额充值、Claude Code / Codex 配置注入等服务。",
|
|
18
18
|
].join("\n");
|
|
19
19
|
|
|
20
|
+
export const OPEN_SOURCE_NOTICE_SUMMARY = [
|
|
21
|
+
"开源参考声明",
|
|
22
|
+
"",
|
|
23
|
+
"本流程参考 NPX ZCF 的交互式初始化与推荐配置体验。",
|
|
24
|
+
"感谢作者 UfoMiao 的开源贡献。",
|
|
25
|
+
`项目地址:${ZCF_PROJECT_URL}`,
|
|
26
|
+
"",
|
|
27
|
+
"查看完整声明:claude360 about",
|
|
28
|
+
].join("\n");
|
|
29
|
+
|
|
20
30
|
// 工作流 / AGENTS 等改编内容的文件头 attribution(PRD 2.3 / 6.5)
|
|
21
31
|
export const ZCF_ATTRIBUTION_COMMENT = [
|
|
22
32
|
"<!--",
|
|
@@ -25,12 +35,16 @@ export const ZCF_ATTRIBUTION_COMMENT = [
|
|
|
25
35
|
"-->",
|
|
26
36
|
].join("\n");
|
|
27
37
|
|
|
38
|
+
export function formatOpenSourceNoticeDetail() {
|
|
39
|
+
return OPEN_SOURCE_NOTICE;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
// 展示声明并询问是否继续;返回 true 表示继续,false 表示返回上级菜单。
|
|
29
43
|
export async function showOpenSourceNotice({ promptSelect, writeLine = console.log } = {}) {
|
|
30
44
|
if (typeof promptSelect !== "function") {
|
|
31
45
|
throw new Error("缺少选择输入");
|
|
32
46
|
}
|
|
33
|
-
writeLine(
|
|
47
|
+
writeLine(OPEN_SOURCE_NOTICE_SUMMARY);
|
|
34
48
|
writeLine("");
|
|
35
49
|
const action = await promptSelect("是否继续?", [
|
|
36
50
|
{ label: "继续", value: "continue" },
|