claude360 0.2.8 → 0.2.9
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/package.json +1 -1
- package/src/account-status.js +8 -5
- package/src/auth.js +9 -2
- package/src/banner.js +6 -6
- package/src/cc-switch.js +11 -5
- package/src/colors.js +31 -0
- package/src/diagnostics.js +36 -17
- package/src/index.js +104 -80
- package/src/init-config.js +39 -25
- package/src/init-flow.js +13 -6
- package/src/mcp-skill.js +32 -21
- package/src/menu.js +8 -7
- package/src/messages.js +78 -0
- package/src/onboarding.js +12 -5
- package/src/prompts.js +7 -6
- package/src/tool-launcher.js +109 -46
- package/src/topup.js +6 -2
- package/src/ui.js +9 -8
- package/src/workflows.js +10 -7
package/src/tool-launcher.js
CHANGED
|
@@ -6,6 +6,11 @@ import { safeErrorMessage } from "./sanitize.js";
|
|
|
6
6
|
import { copyFile as fsCopyFile, mkdir as fsMkdir, readFile as fsReadFile, writeFile as fsWriteFile } from "node:fs/promises";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import path from "node:path";
|
|
9
|
+
import { colorLevel } from "./colors.js";
|
|
10
|
+
import { createMessenger } from "./messages.js";
|
|
11
|
+
|
|
12
|
+
// 语义消息器工厂:真实终端着色,测试/管道(writeLine 被替换)下无色。
|
|
13
|
+
const mk = (writeLine) => createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
9
14
|
|
|
10
15
|
// Codex 协议兼容性检测:以后端上报为准,CLI 不内置协议判断逻辑
|
|
11
16
|
export async function checkCodexCompat(api) {
|
|
@@ -44,9 +49,17 @@ export function resolveCodexConfigPath({ homedir = os.homedir } = {}) {
|
|
|
44
49
|
return path.join(homedir(), ".codex", "config.toml");
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
// Codex 0.134.0+ 的 profile(CONFIG_PROFILE_V2)机制:`--profile claude360` 会在
|
|
53
|
+
// 基础配置 config.toml 之上叠加同目录的 claude360.config.toml 覆盖层。profile 专属
|
|
54
|
+
// 键写在这里(顶层键),而不是主 config.toml 的 [profiles.claude360] 表。
|
|
55
|
+
export function resolveCodexProfileConfigPath({ configPath = resolveCodexConfigPath(), profileId = "claude360" } = {}) {
|
|
56
|
+
return path.join(path.dirname(configPath), `${profileId}.config.toml`);
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
export async function launchCodex({
|
|
48
60
|
config,
|
|
49
61
|
configPath = resolveCodexConfigPath(),
|
|
62
|
+
profileConfigPath = resolveCodexProfileConfigPath({ configPath }),
|
|
50
63
|
readFile = fsReadFile,
|
|
51
64
|
writeFile = fsWriteFile,
|
|
52
65
|
mkdir = fsMkdir,
|
|
@@ -57,7 +70,7 @@ export async function launchCodex({
|
|
|
57
70
|
writeLine = () => {},
|
|
58
71
|
} = {}) {
|
|
59
72
|
assertApiKey(config);
|
|
60
|
-
await configureCodexProvider({ config, configPath, readFile, writeFile, mkdir, copyFile, confirmConflict, writeLine });
|
|
73
|
+
await configureCodexProvider({ config, configPath, profileConfigPath, readFile, writeFile, mkdir, copyFile, confirmConflict, writeLine });
|
|
61
74
|
return spawnCommand("codex", ["--profile", "claude360"], {
|
|
62
75
|
stdio: "inherit",
|
|
63
76
|
env: {
|
|
@@ -70,6 +83,7 @@ export async function launchCodex({
|
|
|
70
83
|
export async function configureCodexProvider({
|
|
71
84
|
config,
|
|
72
85
|
configPath = resolveCodexConfigPath(),
|
|
86
|
+
profileConfigPath = resolveCodexProfileConfigPath({ configPath }),
|
|
73
87
|
readFile = fsReadFile,
|
|
74
88
|
writeFile = fsWriteFile,
|
|
75
89
|
mkdir = fsMkdir,
|
|
@@ -78,32 +92,50 @@ export async function configureCodexProvider({
|
|
|
78
92
|
writeLine = () => {},
|
|
79
93
|
} = {}) {
|
|
80
94
|
const current = await readConfigIfExists(readFile, configPath);
|
|
95
|
+
const currentProfile = await readConfigIfExists(readFile, profileConfigPath);
|
|
81
96
|
const desired = {
|
|
82
97
|
providerId: "claude360",
|
|
83
98
|
baseUrl: `${normalizeBaseUrl(config.baseUrl)}/v1`,
|
|
84
99
|
envKey: "CLAUDE360_API_KEY",
|
|
85
100
|
wireApi: "responses",
|
|
86
101
|
};
|
|
87
|
-
await confirmCodexConfigConflict(current, desired, confirmConflict);
|
|
102
|
+
await confirmCodexConfigConflict({ current, currentProfile, desired, confirmConflict });
|
|
103
|
+
|
|
104
|
+
// 基础层(config.toml):写入 provider 定义,并迁移移除 legacy 的
|
|
105
|
+
// [profiles.claude360] 表与顶层 profile 选择器(v2 禁止其与 --profile 共存)。
|
|
88
106
|
const content = buildCodexConfig(current, {
|
|
89
107
|
providerId: desired.providerId,
|
|
90
108
|
baseUrl: desired.baseUrl,
|
|
91
109
|
envKey: desired.envKey,
|
|
92
110
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
// 覆盖层(claude360.config.toml):profile 专属顶层键 model_provider。
|
|
112
|
+
const profileContent = buildCodexProfileConfig(currentProfile, { providerId: desired.providerId });
|
|
113
|
+
|
|
114
|
+
for (const [label, value] of [["config.toml", content], ["claude360.config.toml", profileContent]]) {
|
|
115
|
+
const tomlError = validateBasicToml(value);
|
|
116
|
+
if (tomlError) {
|
|
117
|
+
throw new Error(`生成的 Codex ${label} TOML 校验失败:${tomlError}`);
|
|
118
|
+
}
|
|
96
119
|
}
|
|
120
|
+
|
|
97
121
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
98
|
-
// 写入前备份原配置(PRD
|
|
99
|
-
|
|
100
|
-
if (current !== "") {
|
|
101
|
-
backupPath = `${configPath}.claude360.bak`;
|
|
102
|
-
await copyFile(configPath, backupPath);
|
|
103
|
-
writeLine(`已备份原 Codex 配置到:${backupPath}`);
|
|
104
|
-
}
|
|
122
|
+
// 写入前备份原配置(PRD 安全要求):两个文件各自备份
|
|
123
|
+
const backupPath = await backupIfExists({ copyFile, filePath: configPath, exists: current !== "", writeLine, label: "原 Codex 配置" });
|
|
105
124
|
await writeFile(configPath, content, "utf8");
|
|
106
|
-
|
|
125
|
+
const profileBackupPath = await backupIfExists({ copyFile, filePath: profileConfigPath, exists: currentProfile !== "", writeLine, label: "原 Codex profile 配置" });
|
|
126
|
+
await writeFile(profileConfigPath, profileContent, "utf8");
|
|
127
|
+
return { content, profileContent, backupPath, profileBackupPath };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function backupIfExists({ copyFile, filePath, exists, writeLine, label }) {
|
|
131
|
+
const msg = mk(writeLine);
|
|
132
|
+
if (!exists) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const backupPath = `${filePath}.claude360.bak`;
|
|
136
|
+
await copyFile(filePath, backupPath);
|
|
137
|
+
msg.success(`已备份${label}到:${backupPath}`);
|
|
138
|
+
return backupPath;
|
|
107
139
|
}
|
|
108
140
|
|
|
109
141
|
// 真实 TOML 校验(验收 P0-2):写入 ~/.codex/config.toml 前对合并产物完整 parse,
|
|
@@ -120,12 +152,13 @@ export function validateBasicToml(content) {
|
|
|
120
152
|
}
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
// 验收 P0-3:provider
|
|
124
|
-
//
|
|
125
|
-
|
|
155
|
+
// 验收 P0-3:provider 块(base_url/env_key/wire_api)与 profile 覆盖层的 model_provider
|
|
156
|
+
// 冲突都必须二次确认,拒绝时在 mkdir / 备份 / 写入之前抛错,磁盘零改动。legacy 的
|
|
157
|
+
// [profiles.claude360] 表由迁移逻辑直接移除,不在此处阻塞。
|
|
158
|
+
async function confirmCodexConfigConflict({ current, currentProfile, desired, confirmConflict }) {
|
|
126
159
|
const conflicts = [];
|
|
127
160
|
|
|
128
|
-
const providerBlock = extractTableBlock(
|
|
161
|
+
const providerBlock = extractTableBlock(current, `model_providers.${desired.providerId}`);
|
|
129
162
|
if (providerBlock) {
|
|
130
163
|
const values = parseTomlStringAssignments(providerBlock);
|
|
131
164
|
if (values.base_url && values.base_url !== desired.baseUrl) {
|
|
@@ -139,12 +172,9 @@ async function confirmCodexConfigConflict(content, desired, confirmConflict) {
|
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
174
|
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
if (values.model_provider && values.model_provider !== desired.providerId) {
|
|
146
|
-
conflicts.push(`profiles.${desired.providerId}.model_provider: ${values.model_provider} -> ${desired.providerId}`);
|
|
147
|
-
}
|
|
175
|
+
const profileValues = parseTomlStringAssignments(topLevelRegion(currentProfile));
|
|
176
|
+
if (profileValues.model_provider && profileValues.model_provider !== desired.providerId) {
|
|
177
|
+
conflicts.push(`model_provider: ${profileValues.model_provider} -> ${desired.providerId}`);
|
|
148
178
|
}
|
|
149
179
|
|
|
150
180
|
if (conflicts.length === 0) {
|
|
@@ -202,7 +232,17 @@ export function buildCodexConfig(content = "", {
|
|
|
202
232
|
`env_key = "${escapeTomlString(envKey)}"`,
|
|
203
233
|
'wire_api = "responses"',
|
|
204
234
|
]);
|
|
205
|
-
|
|
235
|
+
// 迁移:移除 legacy 的 [profiles.claude360] 表与顶层 profile 选择器。
|
|
236
|
+
// 新版 Codex 拒绝 `--profile claude360` 与基础配置中的 legacy profile 共存。
|
|
237
|
+
nextContent = removeTomlTable(nextContent, `profiles.${providerId}`);
|
|
238
|
+
nextContent = removeTopLevelKeyLine(nextContent, "profile", providerId);
|
|
239
|
+
return `${nextContent.trimEnd()}\n`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 生成 profile 覆盖层(claude360.config.toml):profile 专属配置以顶层键写入,
|
|
243
|
+
// 由 `codex --profile claude360` 叠加到基础 config.toml 之上。
|
|
244
|
+
export function buildCodexProfileConfig(content = "", { providerId = "claude360" } = {}) {
|
|
245
|
+
const nextContent = upsertTopLevelKey(content, "model_provider", providerId);
|
|
206
246
|
return `${nextContent.trimEnd()}\n`;
|
|
207
247
|
}
|
|
208
248
|
|
|
@@ -232,22 +272,37 @@ export function upsertTomlTable(content, tableName, tableLines) {
|
|
|
232
272
|
].join("\n");
|
|
233
273
|
}
|
|
234
274
|
|
|
235
|
-
|
|
236
|
-
|
|
275
|
+
// 在文件顶层区域(首个 [table] 之前)幂等 upsert 单个字符串键:已存在则替换,
|
|
276
|
+
// 不存在则插入到顶层区域末尾,不影响任何 table 块。
|
|
277
|
+
export function upsertTopLevelKey(content, key, value) {
|
|
278
|
+
const keyLine = `${key} = "${escapeTomlString(value)}"`;
|
|
279
|
+
const lines = content.split(/\r?\n/);
|
|
280
|
+
let tableStart = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
281
|
+
if (tableStart === -1) {
|
|
282
|
+
tableStart = lines.length;
|
|
283
|
+
}
|
|
284
|
+
const keyPattern = new RegExp(`^\\s*${key}\\s*=`);
|
|
285
|
+
const keyIndex = lines.slice(0, tableStart).findIndex((line) => keyPattern.test(line));
|
|
286
|
+
if (keyIndex !== -1) {
|
|
287
|
+
lines[keyIndex] = keyLine;
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
290
|
+
let insertAt = tableStart;
|
|
291
|
+
while (insertAt > 0 && lines[insertAt - 1].trim() === "") {
|
|
292
|
+
insertAt -= 1;
|
|
293
|
+
}
|
|
294
|
+
lines.splice(insertAt, 0, keyLine);
|
|
295
|
+
return lines.join("\n");
|
|
237
296
|
}
|
|
238
297
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
export function upsertProfileKey(content, profileId, key, value) {
|
|
298
|
+
// 移除整个 [tableName] 表块(表头到下一表头 / EOF)。表不存在时原样返回。
|
|
299
|
+
export function removeTomlTable(content, tableName) {
|
|
242
300
|
const lines = content.split(/\r?\n/);
|
|
243
|
-
const header = `[
|
|
244
|
-
const keyLine = `${key} = "${escapeTomlString(value)}"`;
|
|
301
|
+
const header = `[${tableName}]`;
|
|
245
302
|
const start = lines.findIndex((line) => line.trim() === header);
|
|
246
303
|
if (start === -1) {
|
|
247
|
-
|
|
248
|
-
return `${trimmed}${trimmed ? "\n\n" : ""}${header}\n${keyLine}\n`;
|
|
304
|
+
return content;
|
|
249
305
|
}
|
|
250
|
-
|
|
251
306
|
let end = lines.length;
|
|
252
307
|
for (let index = start + 1; index < lines.length; index += 1) {
|
|
253
308
|
if (/^\s*\[/.test(lines[index])) {
|
|
@@ -255,21 +310,29 @@ export function upsertProfileKey(content, profileId, key, value) {
|
|
|
255
310
|
break;
|
|
256
311
|
}
|
|
257
312
|
}
|
|
313
|
+
return [...lines.slice(0, start), ...lines.slice(end)].join("\n");
|
|
314
|
+
}
|
|
258
315
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
profileLines[keyIndex] = keyLine;
|
|
316
|
+
// 移除顶层区域(首个 [table] 之前)中精确匹配 `key = "value"` 的选择器行。
|
|
317
|
+
function removeTopLevelKeyLine(content, key, value) {
|
|
318
|
+
const lines = content.split(/\r?\n/);
|
|
319
|
+
let tableStart = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
320
|
+
if (tableStart === -1) {
|
|
321
|
+
tableStart = lines.length;
|
|
266
322
|
}
|
|
323
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
324
|
+
const pattern = new RegExp(`^\\s*${key}\\s*=\\s*"${escaped}"\\s*$`);
|
|
325
|
+
return lines.filter((line, index) => !(index < tableStart && pattern.test(line))).join("\n");
|
|
326
|
+
}
|
|
267
327
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
328
|
+
// 返回首个 [table] 之前的顶层区域文本,供 parseTomlStringAssignments 复用。
|
|
329
|
+
function topLevelRegion(content) {
|
|
330
|
+
const lines = content.split(/\r?\n/);
|
|
331
|
+
let end = lines.findIndex((line) => /^\s*\[/.test(line));
|
|
332
|
+
if (end === -1) {
|
|
333
|
+
end = lines.length;
|
|
334
|
+
}
|
|
335
|
+
return lines.slice(0, end).join("\n");
|
|
273
336
|
}
|
|
274
337
|
|
|
275
338
|
async function readConfigIfExists(readFile, configPath) {
|
package/src/topup.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { colorLevel } from "./colors.js";
|
|
2
|
+
import { createMessenger } from "./messages.js";
|
|
1
3
|
import { renderKeyValueTable, renderSectionTitle } from "./ui.js";
|
|
2
4
|
|
|
3
5
|
export async function loadTopUpOptions(api) {
|
|
@@ -31,6 +33,7 @@ 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 });
|
|
34
37
|
const options = await loadTopUpOptions(api);
|
|
35
38
|
// 后端口径前置拦截(验收 P1-2):禁用时不进金额选择、不创建订单
|
|
36
39
|
if (options?.wechat_enabled === false) {
|
|
@@ -49,7 +52,7 @@ export async function runWechatTopUp({
|
|
|
49
52
|
["金额", String(order.money_display || order.money || amount)],
|
|
50
53
|
["支付方式", "微信扫码"],
|
|
51
54
|
]));
|
|
52
|
-
|
|
55
|
+
msg.info("请使用微信扫码支付:");
|
|
53
56
|
await printQrOrCodeUrl({ codeUrl: order.code_url, renderQr, writeLine });
|
|
54
57
|
const status = await waitTopUpPaid({
|
|
55
58
|
api,
|
|
@@ -96,13 +99,14 @@ async function chooseTopUpAmount({ options, promptSelect, promptInput }) {
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
async function printQrOrCodeUrl({ codeUrl, renderQr, writeLine }) {
|
|
102
|
+
const msg = createMessenger({ writeLine, color: writeLine === console.log ? colorLevel() : 0 });
|
|
99
103
|
try {
|
|
100
104
|
const qr = await renderQr(codeUrl);
|
|
101
105
|
if (qr) {
|
|
102
106
|
writeLine(qr);
|
|
103
107
|
}
|
|
104
108
|
} catch {
|
|
105
|
-
|
|
109
|
+
msg.warn(`二维码渲染失败,请复制 code_url 完成支付:${codeUrl}`);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
|
package/src/ui.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
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
|
|
|
@@ -177,10 +178,10 @@ export function renderHeader(title, { subtitle = "", color = false } = {}) {
|
|
|
177
178
|
// 四种状态:文字前缀(mark)始终存在,颜色仅增强层级(需求二「不依赖颜色表达唯一信息」)
|
|
178
179
|
const BOX_MARKS = { info: "i", warn: "!", error: "×", success: "✓" };
|
|
179
180
|
const BOX_RGB = {
|
|
180
|
-
info:
|
|
181
|
-
warn:
|
|
182
|
-
error:
|
|
183
|
-
success:
|
|
181
|
+
info: PALETTE.info,
|
|
182
|
+
warn: PALETTE.warn,
|
|
183
|
+
error: PALETTE.error,
|
|
184
|
+
success: PALETTE.success,
|
|
184
185
|
};
|
|
185
186
|
|
|
186
187
|
export function renderBox(message, { kind = "info", color = false, width = 0 } = {}) {
|
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) {
|