@xiaozhi-client/cli 1.9.4-beta.5

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/fix-imports.js +32 -0
  4. package/package.json +26 -0
  5. package/project.json +75 -0
  6. package/src/Constants.ts +105 -0
  7. package/src/Container.ts +212 -0
  8. package/src/Types.ts +79 -0
  9. package/src/commands/CommandHandlerFactory.ts +98 -0
  10. package/src/commands/ConfigCommandHandler.ts +279 -0
  11. package/src/commands/EndpointCommandHandler.ts +158 -0
  12. package/src/commands/McpCommandHandler.ts +778 -0
  13. package/src/commands/ProjectCommandHandler.ts +254 -0
  14. package/src/commands/ServiceCommandHandler.ts +182 -0
  15. package/src/commands/__tests__/CommandHandlerFactory.test.ts +323 -0
  16. package/src/commands/__tests__/CommandRegistry.test.ts +287 -0
  17. package/src/commands/__tests__/ConfigCommandHandler.test.ts +844 -0
  18. package/src/commands/__tests__/EndpointCommandHandler.test.ts +426 -0
  19. package/src/commands/__tests__/McpCommandHandler.test.ts +753 -0
  20. package/src/commands/__tests__/ProjectCommandHandler.test.ts +230 -0
  21. package/src/commands/__tests__/ServiceCommands.integration.test.ts +408 -0
  22. package/src/commands/index.ts +351 -0
  23. package/src/errors/ErrorHandlers.ts +141 -0
  24. package/src/errors/ErrorMessages.ts +121 -0
  25. package/src/errors/__tests__/index.test.ts +186 -0
  26. package/src/errors/index.ts +163 -0
  27. package/src/global.d.ts +19 -0
  28. package/src/index.ts +53 -0
  29. package/src/interfaces/Command.ts +128 -0
  30. package/src/interfaces/CommandTypes.ts +95 -0
  31. package/src/interfaces/Config.ts +25 -0
  32. package/src/interfaces/Service.ts +99 -0
  33. package/src/services/DaemonManager.ts +318 -0
  34. package/src/services/ProcessManager.ts +235 -0
  35. package/src/services/ServiceManager.ts +319 -0
  36. package/src/services/TemplateManager.ts +382 -0
  37. package/src/services/__tests__/DaemonManager.test.ts +378 -0
  38. package/src/services/__tests__/DaemonMode.integration.test.ts +321 -0
  39. package/src/services/__tests__/ProcessManager.test.ts +296 -0
  40. package/src/services/__tests__/ServiceManager.test.ts +774 -0
  41. package/src/services/__tests__/TemplateManager.test.ts +337 -0
  42. package/src/types/backend.d.ts +48 -0
  43. package/src/utils/FileUtils.ts +320 -0
  44. package/src/utils/FormatUtils.ts +198 -0
  45. package/src/utils/PathUtils.ts +255 -0
  46. package/src/utils/PlatformUtils.ts +217 -0
  47. package/src/utils/Validation.ts +274 -0
  48. package/src/utils/VersionUtils.ts +141 -0
  49. package/src/utils/__tests__/FileUtils.test.ts +728 -0
  50. package/src/utils/__tests__/FormatUtils.test.ts +243 -0
  51. package/src/utils/__tests__/PathUtils.test.ts +1165 -0
  52. package/src/utils/__tests__/PlatformUtils.test.ts +723 -0
  53. package/src/utils/__tests__/Validation.test.ts +560 -0
  54. package/src/utils/__tests__/VersionUtils.test.ts +410 -0
  55. package/tsconfig.json +32 -0
  56. package/tsconfig.tsbuildinfo +1 -0
  57. package/tsup.config.ts +107 -0
  58. package/vitest.config.ts +97 -0
@@ -0,0 +1,319 @@
1
+ /**
2
+ * 服务管理服务
3
+ */
4
+
5
+ import type { ConfigManager } from "@xiaozhi-client/config";
6
+ import { ConfigError, ServiceError } from "../errors/index";
7
+ import type {
8
+ ServiceManager as IServiceManager,
9
+ ProcessManager,
10
+ ServiceStartOptions,
11
+ ServiceStatus,
12
+ } from "../interfaces/Service";
13
+ import { PathUtils } from "../utils/PathUtils";
14
+ import { Validation } from "../utils/Validation";
15
+
16
+ /**
17
+ * 服务管理器实现
18
+ */
19
+ export class ServiceManagerImpl implements IServiceManager {
20
+ constructor(
21
+ private processManager: ProcessManager,
22
+ private configManager: ConfigManager
23
+ ) {}
24
+
25
+ /**
26
+ * 启动服务
27
+ */
28
+ async start(options: ServiceStartOptions): Promise<void> {
29
+ try {
30
+ // 验证启动选项
31
+ this.validateStartOptions(options);
32
+
33
+ // 清理容器环境状态
34
+ this.processManager.cleanupContainerState();
35
+
36
+ // 检查服务是否已经在运行
37
+ const status = this.getStatus();
38
+ if (status.running) {
39
+ // 自动停止现有服务并重新启动
40
+ console.log(`检测到服务已在运行 (PID: ${status.pid}),正在自动重启...`);
41
+
42
+ try {
43
+ // 优雅停止现有进程
44
+ await this.processManager.gracefulKillProcess(status.pid || 0);
45
+
46
+ // 清理 PID 文件
47
+ this.processManager.cleanupPidFile();
48
+
49
+ // 等待一下确保完全停止
50
+ await new Promise((resolve) => setTimeout(resolve, 1000));
51
+
52
+ console.log("现有服务已停止,正在启动新服务...");
53
+ } catch (stopError) {
54
+ console.warn(
55
+ `停止现有服务时出现警告: ${stopError instanceof Error ? stopError.message : String(stopError)}`
56
+ );
57
+ // 继续尝试启动新服务,因为旧进程可能已经不存在了
58
+ }
59
+ }
60
+
61
+ // 检查环境配置
62
+ this.checkEnvironment();
63
+
64
+ // 根据模式启动服务
65
+ switch (options.mode) {
66
+ case "mcp-server":
67
+ await this.startMcpServerMode(options);
68
+ break;
69
+ case "stdio":
70
+ // stdio 模式已废弃,改为启动 Web 服务
71
+ await this.startNormalMode(options);
72
+ break;
73
+ case "normal":
74
+ await this.startNormalMode(options);
75
+ break;
76
+ default:
77
+ await this.startNormalMode(options);
78
+ break;
79
+ }
80
+ } catch (error) {
81
+ if (error instanceof ServiceError) {
82
+ throw error;
83
+ }
84
+ throw ServiceError.startFailed(
85
+ error instanceof Error ? error.message : String(error)
86
+ );
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 停止服务
92
+ */
93
+ async stop(): Promise<void> {
94
+ try {
95
+ const status = this.getStatus();
96
+
97
+ if (!status.running) {
98
+ throw ServiceError.notRunning();
99
+ }
100
+
101
+ // 优雅停止进程
102
+ await this.processManager.gracefulKillProcess(status.pid || 0);
103
+
104
+ // 清理 PID 文件
105
+ this.processManager.cleanupPidFile();
106
+ } catch (error) {
107
+ if (error instanceof ServiceError) {
108
+ throw error;
109
+ }
110
+ throw new ServiceError(
111
+ `停止服务失败: ${error instanceof Error ? error.message : String(error)}`
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * 重启服务
118
+ */
119
+ async restart(options: ServiceStartOptions): Promise<void> {
120
+ try {
121
+ // 先停止服务
122
+ const status = this.getStatus();
123
+ if (status.running) {
124
+ await this.stop();
125
+ // 等待一下确保完全停止
126
+ await new Promise((resolve) => setTimeout(resolve, 1000));
127
+ }
128
+
129
+ // 重新启动服务
130
+ await this.start(options);
131
+ } catch (error) {
132
+ throw new ServiceError(
133
+ `重启服务失败: ${error instanceof Error ? error.message : String(error)}`
134
+ );
135
+ }
136
+ }
137
+
138
+ /**
139
+ * 获取服务状态
140
+ */
141
+ getStatus(): ServiceStatus {
142
+ return this.processManager.getServiceStatus();
143
+ }
144
+
145
+ /**
146
+ * 验证启动选项
147
+ */
148
+ private validateStartOptions(options: ServiceStartOptions): void {
149
+ if (options.port !== undefined) {
150
+ Validation.validatePort(options.port);
151
+ }
152
+
153
+ if (
154
+ options.mode &&
155
+ !["normal", "mcp-server", "stdio"].includes(options.mode)
156
+ ) {
157
+ throw new ServiceError(`无效的运行模式: ${options.mode}`);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * 检查环境配置
163
+ */
164
+ private checkEnvironment(): void {
165
+ // 检查配置文件是否存在
166
+ if (!this.configManager.configExists()) {
167
+ throw ConfigError.configNotFound();
168
+ }
169
+
170
+ // 可以添加更多环境检查
171
+ try {
172
+ const config = this.configManager.getConfig();
173
+ if (!config) {
174
+ throw new ConfigError("配置文件无效");
175
+ }
176
+ } catch (error) {
177
+ if (error instanceof ConfigError) {
178
+ throw error;
179
+ }
180
+ throw new ConfigError(
181
+ `配置文件错误: ${error instanceof Error ? error.message : String(error)}`
182
+ );
183
+ }
184
+ }
185
+
186
+ /**
187
+ * 启动普通模式
188
+ */
189
+ private async startNormalMode(options: ServiceStartOptions): Promise<void> {
190
+ if (options.daemon) {
191
+ // 后台模式 - 默认启动 WebUI
192
+ await this.startWebServerInDaemon();
193
+ } else {
194
+ // 前台模式 - 默认启动 WebUI
195
+ await this.startWebServerInForeground();
196
+ }
197
+ }
198
+
199
+ /**
200
+ * 启动 MCP Server 模式
201
+ */
202
+ private async startMcpServerMode(
203
+ options: ServiceStartOptions
204
+ ): Promise<void> {
205
+ const port = options.port || 3000;
206
+ const { spawn } = await import("node:child_process");
207
+
208
+ if (options.daemon) {
209
+ // 后台模式
210
+ const scriptPath = PathUtils.getExecutablePath("cli");
211
+ const child = spawn(
212
+ "node",
213
+ [scriptPath, "start", "--server", port.toString()],
214
+ {
215
+ detached: true,
216
+ stdio: ["ignore", "ignore", "ignore"], // 完全忽略所有 stdio,避免阻塞
217
+ env: {
218
+ ...process.env,
219
+ XIAOZHI_CONFIG_DIR: PathUtils.getConfigDir(),
220
+ XIAOZHI_DAEMON: "true",
221
+ MCP_SERVER_MODE: "true",
222
+ },
223
+ }
224
+ );
225
+
226
+ // 保存 PID 信息
227
+ this.processManager.savePidInfo(child.pid || 0, "daemon");
228
+
229
+ // 完全分离子进程
230
+ child.unref();
231
+
232
+ // 输出启动信息后立即退出父进程
233
+ console.log(
234
+ `✅ MCP Server 已在后台启动 (PID: ${child.pid}, Port: ${port})`
235
+ );
236
+ console.log(`💡 使用 'xiaozhi status' 查看状态`);
237
+
238
+ // 立即退出父进程,释放终端控制权
239
+ process.exit(0);
240
+ } else {
241
+ // 前台模式 - 直接启动 Web Server
242
+ const { WebServer } = await import("@root/WebServer.js");
243
+ const server = new WebServer(port);
244
+
245
+ // 处理退出信号
246
+ const cleanup = async () => {
247
+ await server.stop();
248
+ process.exit(0);
249
+ };
250
+
251
+ process.on("SIGINT", cleanup);
252
+ process.on("SIGTERM", cleanup);
253
+
254
+ await server.start();
255
+ }
256
+ }
257
+
258
+ /**
259
+ * 后台模式启动 WebServer
260
+ */
261
+ private async startWebServerInDaemon(): Promise<void> {
262
+ const { spawn } = await import("node:child_process");
263
+ const webServerPath = PathUtils.getWebServerLauncherPath();
264
+
265
+ const fs = await import("node:fs");
266
+ if (!fs.default.existsSync(webServerPath)) {
267
+ throw new ServiceError(`WebServer 文件不存在: ${webServerPath}`);
268
+ }
269
+
270
+ const args = [webServerPath];
271
+
272
+ const child = spawn("node", args, {
273
+ detached: true,
274
+ stdio: ["ignore", "ignore", "ignore"], // 完全忽略所有 stdio,避免阻塞
275
+ env: {
276
+ ...process.env,
277
+ XIAOZHI_CONFIG_DIR: PathUtils.getConfigDir(),
278
+ XIAOZHI_DAEMON: "true",
279
+ },
280
+ });
281
+
282
+ // 保存 PID 信息
283
+ this.processManager.savePidInfo(child.pid || 0, "daemon");
284
+
285
+ // 完全分离子进程
286
+ child.unref();
287
+
288
+ // 输出启动信息后立即退出父进程
289
+ console.log(`✅ 后台服务已启动 (PID: ${child.pid})`);
290
+ console.log(`💡 使用 'xiaozhi status' 查看状态`);
291
+ console.log(`💡 使用 'xiaozhi attach' 查看日志`);
292
+
293
+ // 立即退出父进程,释放终端控制权
294
+ process.exit(0);
295
+ }
296
+
297
+ /**
298
+ * 前台模式启动 WebServer
299
+ */
300
+ private async startWebServerInForeground(): Promise<void> {
301
+ const { WebServer } = await import("@root/WebServer.js");
302
+ const server = new WebServer();
303
+
304
+ // 处理退出信号
305
+ const cleanup = async () => {
306
+ await server.stop();
307
+ this.processManager.cleanupPidFile();
308
+ process.exit(0);
309
+ };
310
+
311
+ process.on("SIGINT", cleanup);
312
+ process.on("SIGTERM", cleanup);
313
+
314
+ // 保存 PID 信息
315
+ this.processManager.savePidInfo(process.pid, "foreground");
316
+
317
+ await server.start();
318
+ }
319
+ }
@@ -0,0 +1,382 @@
1
+ /**
2
+ * 模板管理服务
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { FileError, ValidationError } from "../errors/index";
8
+ import type { TemplateManager as ITemplateManager } from "../interfaces/Service";
9
+ import { FileUtils } from "../utils/FileUtils";
10
+ import { PathUtils } from "../utils/PathUtils";
11
+ import { Validation } from "../utils/Validation";
12
+
13
+ /**
14
+ * 模板信息接口
15
+ */
16
+ export interface TemplateInfo {
17
+ name: string;
18
+ path: string;
19
+ description?: string;
20
+ version?: string;
21
+ author?: string;
22
+ files: string[];
23
+ }
24
+
25
+ /**
26
+ * 模板创建选项
27
+ */
28
+ export interface TemplateCreateOptions {
29
+ templateName?: string;
30
+ targetPath: string;
31
+ projectName: string;
32
+ variables?: Record<string, string>;
33
+ }
34
+
35
+ /**
36
+ * 模板管理器实现
37
+ */
38
+ export class TemplateManagerImpl implements ITemplateManager {
39
+ private templateCache = new Map<string, TemplateInfo>();
40
+
41
+ /**
42
+ * 获取可用模板列表
43
+ */
44
+ async getAvailableTemplates(): Promise<TemplateInfo[]> {
45
+ try {
46
+ const templatesDir = PathUtils.findTemplatesDir();
47
+
48
+ if (!templatesDir) {
49
+ return [];
50
+ }
51
+
52
+ const templates: TemplateInfo[] = [];
53
+ const templateDirs = fs
54
+ .readdirSync(templatesDir, { withFileTypes: true })
55
+ .filter((dirent) => dirent.isDirectory())
56
+ .map((dirent) => dirent.name);
57
+
58
+ for (const templateName of templateDirs) {
59
+ try {
60
+ const templateInfo = await this.getTemplateInfo(templateName);
61
+ if (templateInfo) {
62
+ templates.push(templateInfo);
63
+ }
64
+ } catch (error) {
65
+ // 跳过无效的模板目录
66
+ console.warn(`跳过无效模板: ${templateName}`);
67
+ }
68
+ }
69
+
70
+ return templates;
71
+ } catch (error) {
72
+ throw new FileError(
73
+ "无法读取模板目录",
74
+ PathUtils.findTemplatesDir() || ""
75
+ );
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 获取模板信息
81
+ */
82
+ async getTemplateInfo(templateName: string): Promise<TemplateInfo | null> {
83
+ try {
84
+ // 验证模板名称
85
+ Validation.validateTemplateName(templateName);
86
+
87
+ // 检查缓存
88
+ if (this.templateCache.has(templateName)) {
89
+ return this.templateCache.get(templateName)!;
90
+ }
91
+
92
+ const templatePath = PathUtils.getTemplatePath(templateName);
93
+ if (!templatePath) {
94
+ return null;
95
+ }
96
+
97
+ // 读取模板配置文件
98
+ const configPath = path.join(templatePath, "template.json");
99
+ let config: any = {};
100
+
101
+ if (FileUtils.exists(configPath)) {
102
+ try {
103
+ const configContent = FileUtils.readFile(configPath);
104
+ config = JSON.parse(configContent);
105
+ } catch (error) {
106
+ console.warn(`模板配置文件解析失败: ${templateName}`);
107
+ }
108
+ }
109
+
110
+ // 获取模板文件列表
111
+ const files = this.getTemplateFiles(templatePath);
112
+
113
+ const templateInfo: TemplateInfo = {
114
+ name: templateName,
115
+ path: templatePath,
116
+ description: config.description || `${templateName} 模板`,
117
+ version: config.version || "1.0.0",
118
+ author: config.author,
119
+ files,
120
+ };
121
+
122
+ // 缓存模板信息
123
+ this.templateCache.set(templateName, templateInfo);
124
+
125
+ return templateInfo;
126
+ } catch (error) {
127
+ if (error instanceof ValidationError) {
128
+ throw error;
129
+ }
130
+ throw new FileError(`无法获取模板信息: ${templateName}`, "");
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 复制模板到目标目录
136
+ */
137
+ async copyTemplate(templateName: string, targetPath: string): Promise<void> {
138
+ await this.createProject({
139
+ templateName,
140
+ targetPath,
141
+ projectName: path.basename(targetPath),
142
+ });
143
+ }
144
+
145
+ /**
146
+ * 创建项目
147
+ */
148
+ async createProject(options: TemplateCreateOptions): Promise<void> {
149
+ try {
150
+ // 验证输入参数
151
+ this.validateCreateOptions(options);
152
+
153
+ // 获取模板信息
154
+ const templateName = options.templateName || "default";
155
+ const templateInfo = await this.getTemplateInfo(templateName);
156
+
157
+ if (!templateInfo) {
158
+ throw new FileError(`模板不存在: ${templateName}`, "");
159
+ }
160
+
161
+ // 检查目标路径
162
+ const targetPath = path.resolve(options.targetPath);
163
+ if (FileUtils.exists(targetPath)) {
164
+ throw FileError.alreadyExists(targetPath);
165
+ }
166
+
167
+ // 创建项目目录
168
+ FileUtils.ensureDir(targetPath);
169
+
170
+ // 复制模板文件
171
+ await this.copyTemplateFiles(templateInfo, targetPath, options);
172
+
173
+ // 处理模板变量替换
174
+ await this.processTemplateVariables(targetPath, options);
175
+
176
+ console.log(`✅ 项目创建成功: ${targetPath}`);
177
+ } catch (error) {
178
+ if (error instanceof FileError || error instanceof ValidationError) {
179
+ throw error;
180
+ }
181
+ throw new FileError(
182
+ `创建项目失败: ${error instanceof Error ? error.message : String(error)}`,
183
+ options.targetPath
184
+ );
185
+ }
186
+ }
187
+
188
+ /**
189
+ * 验证模板
190
+ */
191
+ async validateTemplate(templateName: string): Promise<boolean> {
192
+ try {
193
+ const templateInfo = await this.getTemplateInfo(templateName);
194
+
195
+ if (!templateInfo) {
196
+ return false;
197
+ }
198
+
199
+ // 检查必要文件是否存在
200
+ const requiredFiles = ["package.json"]; // 可以根据需要调整
201
+
202
+ for (const requiredFile of requiredFiles) {
203
+ const filePath = path.join(templateInfo.path, requiredFile);
204
+ if (!FileUtils.exists(filePath)) {
205
+ console.warn(`模板缺少必要文件: ${requiredFile}`);
206
+ return false;
207
+ }
208
+ }
209
+
210
+ return true;
211
+ } catch {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * 清除模板缓存
218
+ */
219
+ clearCache(): void {
220
+ this.templateCache.clear();
221
+ }
222
+
223
+ /**
224
+ * 获取模板文件列表
225
+ */
226
+ private getTemplateFiles(templatePath: string): string[] {
227
+ try {
228
+ const files = FileUtils.listDirectory(templatePath, {
229
+ recursive: true,
230
+ includeHidden: false,
231
+ });
232
+
233
+ // 过滤掉模板配置文件和其他不需要的文件
234
+ return files.filter((file) => {
235
+ const relativePath = path.relative(templatePath, file);
236
+ return (
237
+ !relativePath.startsWith(".") &&
238
+ relativePath !== "template.json" &&
239
+ !relativePath.includes("node_modules")
240
+ );
241
+ });
242
+ } catch {
243
+ return [];
244
+ }
245
+ }
246
+
247
+ /**
248
+ * 验证创建选项
249
+ */
250
+ private validateCreateOptions(options: TemplateCreateOptions): void {
251
+ Validation.validateRequired(options.targetPath, "targetPath");
252
+ Validation.validateRequired(options.projectName, "projectName");
253
+ Validation.validateProjectName(options.projectName);
254
+
255
+ if (options.templateName) {
256
+ Validation.validateTemplateName(options.templateName);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * 复制模板文件
262
+ */
263
+ private async copyTemplateFiles(
264
+ templateInfo: TemplateInfo,
265
+ targetPath: string,
266
+ options: TemplateCreateOptions
267
+ ): Promise<void> {
268
+ try {
269
+ // 复制所有模板文件
270
+ FileUtils.copyDirectory(templateInfo.path, targetPath, {
271
+ exclude: ["template.json", ".git", "node_modules"],
272
+ overwrite: false,
273
+ recursive: true,
274
+ });
275
+ } catch (error) {
276
+ throw new FileError(
277
+ `复制模板文件失败: ${error instanceof Error ? error.message : String(error)}`,
278
+ templateInfo.path
279
+ );
280
+ }
281
+ }
282
+
283
+ /**
284
+ * 处理模板变量替换
285
+ */
286
+ private async processTemplateVariables(
287
+ targetPath: string,
288
+ options: TemplateCreateOptions
289
+ ): Promise<void> {
290
+ try {
291
+ // 默认变量
292
+ const variables = {
293
+ PROJECT_NAME: options.projectName,
294
+ PROJECT_NAME_LOWER: options.projectName.toLowerCase(),
295
+ PROJECT_NAME_UPPER: options.projectName.toUpperCase(),
296
+ ...options.variables,
297
+ };
298
+
299
+ // 获取需要处理的文件
300
+ const filesToProcess = [
301
+ "package.json",
302
+ "README.md",
303
+ "src/**/*.ts",
304
+ "src/**/*.js",
305
+ "src/**/*.json",
306
+ ];
307
+
308
+ for (const pattern of filesToProcess) {
309
+ const files = this.findFilesByPattern(targetPath, pattern);
310
+
311
+ for (const filePath of files) {
312
+ await this.replaceVariablesInFile(filePath, variables);
313
+ }
314
+ }
315
+ } catch (error) {
316
+ console.warn(
317
+ `处理模板变量失败: ${error instanceof Error ? error.message : String(error)}`
318
+ );
319
+ }
320
+ }
321
+
322
+ /**
323
+ * 根据模式查找文件
324
+ */
325
+ private findFilesByPattern(basePath: string, pattern: string): string[] {
326
+ try {
327
+ if (!pattern.includes("*")) {
328
+ // 简单文件路径
329
+ const filePath = path.join(basePath, pattern);
330
+ return FileUtils.exists(filePath) ? [filePath] : [];
331
+ }
332
+
333
+ // 简单的通配符支持
334
+ const files = FileUtils.listDirectory(basePath, { recursive: true });
335
+ const regex = new RegExp(
336
+ pattern.replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*")
337
+ );
338
+
339
+ return files.filter((file) => {
340
+ // 将路径分隔符统一为 /,确保在 Windows 上也能正确匹配
341
+ const relativePath = path
342
+ .relative(basePath, file)
343
+ .split(path.sep)
344
+ .join("/");
345
+ return regex.test(relativePath);
346
+ });
347
+ } catch {
348
+ return [];
349
+ }
350
+ }
351
+
352
+ /**
353
+ * 替换文件中的变量
354
+ */
355
+ private async replaceVariablesInFile(
356
+ filePath: string,
357
+ variables: Record<string, string>
358
+ ): Promise<void> {
359
+ try {
360
+ let content = FileUtils.readFile(filePath);
361
+ let hasChanges = false;
362
+
363
+ // 替换变量 {{VARIABLE_NAME}}
364
+ for (const [key, value] of Object.entries(variables)) {
365
+ const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
366
+ if (regex.test(content)) {
367
+ content = content.replace(regex, value);
368
+ hasChanges = true;
369
+ }
370
+ }
371
+
372
+ // 如果有变更,写回文件
373
+ if (hasChanges) {
374
+ FileUtils.writeFile(filePath, content, { overwrite: true });
375
+ }
376
+ } catch (error) {
377
+ console.warn(
378
+ `替换文件变量失败 ${filePath}: ${error instanceof Error ? error.message : String(error)}`
379
+ );
380
+ }
381
+ }
382
+ }