@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.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/fix-imports.js +32 -0
- package/package.json +26 -0
- package/project.json +75 -0
- package/src/Constants.ts +105 -0
- package/src/Container.ts +212 -0
- package/src/Types.ts +79 -0
- package/src/commands/CommandHandlerFactory.ts +98 -0
- package/src/commands/ConfigCommandHandler.ts +279 -0
- package/src/commands/EndpointCommandHandler.ts +158 -0
- package/src/commands/McpCommandHandler.ts +778 -0
- package/src/commands/ProjectCommandHandler.ts +254 -0
- package/src/commands/ServiceCommandHandler.ts +182 -0
- package/src/commands/__tests__/CommandHandlerFactory.test.ts +323 -0
- package/src/commands/__tests__/CommandRegistry.test.ts +287 -0
- package/src/commands/__tests__/ConfigCommandHandler.test.ts +844 -0
- package/src/commands/__tests__/EndpointCommandHandler.test.ts +426 -0
- package/src/commands/__tests__/McpCommandHandler.test.ts +753 -0
- package/src/commands/__tests__/ProjectCommandHandler.test.ts +230 -0
- package/src/commands/__tests__/ServiceCommands.integration.test.ts +408 -0
- package/src/commands/index.ts +351 -0
- package/src/errors/ErrorHandlers.ts +141 -0
- package/src/errors/ErrorMessages.ts +121 -0
- package/src/errors/__tests__/index.test.ts +186 -0
- package/src/errors/index.ts +163 -0
- package/src/global.d.ts +19 -0
- package/src/index.ts +53 -0
- package/src/interfaces/Command.ts +128 -0
- package/src/interfaces/CommandTypes.ts +95 -0
- package/src/interfaces/Config.ts +25 -0
- package/src/interfaces/Service.ts +99 -0
- package/src/services/DaemonManager.ts +318 -0
- package/src/services/ProcessManager.ts +235 -0
- package/src/services/ServiceManager.ts +319 -0
- package/src/services/TemplateManager.ts +382 -0
- package/src/services/__tests__/DaemonManager.test.ts +378 -0
- package/src/services/__tests__/DaemonMode.integration.test.ts +321 -0
- package/src/services/__tests__/ProcessManager.test.ts +296 -0
- package/src/services/__tests__/ServiceManager.test.ts +774 -0
- package/src/services/__tests__/TemplateManager.test.ts +337 -0
- package/src/types/backend.d.ts +48 -0
- package/src/utils/FileUtils.ts +320 -0
- package/src/utils/FormatUtils.ts +198 -0
- package/src/utils/PathUtils.ts +255 -0
- package/src/utils/PlatformUtils.ts +217 -0
- package/src/utils/Validation.ts +274 -0
- package/src/utils/VersionUtils.ts +141 -0
- package/src/utils/__tests__/FileUtils.test.ts +728 -0
- package/src/utils/__tests__/FormatUtils.test.ts +243 -0
- package/src/utils/__tests__/PathUtils.test.ts +1165 -0
- package/src/utils/__tests__/PlatformUtils.test.ts +723 -0
- package/src/utils/__tests__/Validation.test.ts +560 -0
- package/src/utils/__tests__/VersionUtils.test.ts +410 -0
- package/tsconfig.json +32 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsup.config.ts +107 -0
- package/vitest.config.ts +97 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 格式化工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 格式化工具类
|
|
7
|
+
*/
|
|
8
|
+
export class FormatUtils {
|
|
9
|
+
/**
|
|
10
|
+
* 格式化运行时间
|
|
11
|
+
*/
|
|
12
|
+
static formatUptime(ms: number): string {
|
|
13
|
+
const seconds = Math.floor(ms / 1000);
|
|
14
|
+
const minutes = Math.floor(seconds / 60);
|
|
15
|
+
const hours = Math.floor(minutes / 60);
|
|
16
|
+
const days = Math.floor(hours / 24);
|
|
17
|
+
|
|
18
|
+
if (days > 0) {
|
|
19
|
+
return `${days}天 ${hours % 24}小时 ${minutes % 60}分钟`;
|
|
20
|
+
}
|
|
21
|
+
if (hours > 0) {
|
|
22
|
+
return `${hours}小时 ${minutes % 60}分钟`;
|
|
23
|
+
}
|
|
24
|
+
if (minutes > 0) {
|
|
25
|
+
return `${minutes}分钟 ${seconds % 60}秒`;
|
|
26
|
+
}
|
|
27
|
+
return `${seconds}秒`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 格式化文件大小
|
|
32
|
+
*/
|
|
33
|
+
static formatFileSize(bytes: number): string {
|
|
34
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
35
|
+
let size = bytes;
|
|
36
|
+
let unitIndex = 0;
|
|
37
|
+
|
|
38
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
39
|
+
size /= 1024;
|
|
40
|
+
unitIndex++;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `${size.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 格式化时间戳
|
|
48
|
+
*/
|
|
49
|
+
static formatTimestamp(
|
|
50
|
+
timestamp: number,
|
|
51
|
+
format: "full" | "date" | "time" = "full"
|
|
52
|
+
): string {
|
|
53
|
+
const date = new Date(timestamp);
|
|
54
|
+
|
|
55
|
+
switch (format) {
|
|
56
|
+
case "date":
|
|
57
|
+
return date.toLocaleDateString("zh-CN");
|
|
58
|
+
case "time":
|
|
59
|
+
return date.toLocaleTimeString("zh-CN");
|
|
60
|
+
default:
|
|
61
|
+
return date.toLocaleString("zh-CN");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 格式化进程 ID
|
|
67
|
+
*/
|
|
68
|
+
static formatPid(pid: number): string {
|
|
69
|
+
return `PID: ${pid}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 格式化端口号
|
|
74
|
+
*/
|
|
75
|
+
static formatPort(port: number): string {
|
|
76
|
+
return `端口: ${port}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 格式化 URL
|
|
81
|
+
*/
|
|
82
|
+
static formatUrl(
|
|
83
|
+
protocol: string,
|
|
84
|
+
host: string,
|
|
85
|
+
port: number,
|
|
86
|
+
path?: string
|
|
87
|
+
): string {
|
|
88
|
+
const url = `${protocol}://${host}:${port}`;
|
|
89
|
+
return path ? `${url}${path}` : url;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 格式化配置键值对
|
|
94
|
+
*/
|
|
95
|
+
static formatConfigPair(key: string, value: any): string {
|
|
96
|
+
if (typeof value === "object") {
|
|
97
|
+
return `${key}: ${JSON.stringify(value, null, 2)}`;
|
|
98
|
+
}
|
|
99
|
+
return `${key}: ${value}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 格式化错误消息
|
|
104
|
+
*/
|
|
105
|
+
static formatError(error: Error, includeStack = false): string {
|
|
106
|
+
let message = `错误: ${error.message}`;
|
|
107
|
+
|
|
108
|
+
if (includeStack && error.stack) {
|
|
109
|
+
message += `\n堆栈信息:\n${error.stack}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return message;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 格式化列表
|
|
117
|
+
*/
|
|
118
|
+
static formatList(items: string[], bullet = "•"): string {
|
|
119
|
+
return items.map((item) => `${bullet} ${item}`).join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 格式化表格数据
|
|
124
|
+
*/
|
|
125
|
+
static formatTable(data: Record<string, any>[]): string {
|
|
126
|
+
if (data.length === 0) return "";
|
|
127
|
+
|
|
128
|
+
const keys = Object.keys(data[0]);
|
|
129
|
+
const maxWidths = keys.map((key) =>
|
|
130
|
+
Math.max(key.length, ...data.map((row) => String(row[key]).length))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// 表头
|
|
134
|
+
const header = keys.map((key, i) => key.padEnd(maxWidths[i])).join(" | ");
|
|
135
|
+
const separator = maxWidths.map((width) => "-".repeat(width)).join("-|-");
|
|
136
|
+
|
|
137
|
+
// 数据行
|
|
138
|
+
const rows = data.map((row) =>
|
|
139
|
+
keys.map((key, i) => String(row[key]).padEnd(maxWidths[i])).join(" | ")
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return [header, separator, ...rows].join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 格式化进度条
|
|
147
|
+
*/
|
|
148
|
+
static formatProgressBar(current: number, total: number, width = 20): string {
|
|
149
|
+
const percentage = Math.min(current / total, 1);
|
|
150
|
+
const filled = Math.floor(percentage * width);
|
|
151
|
+
const empty = width - filled;
|
|
152
|
+
|
|
153
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
154
|
+
const percent = Math.floor(percentage * 100);
|
|
155
|
+
|
|
156
|
+
return `[${bar}] ${percent}% (${current}/${total})`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 格式化命令行参数
|
|
161
|
+
*/
|
|
162
|
+
static formatCommandArgs(command: string, args: string[]): string {
|
|
163
|
+
const quotedArgs = args.map((arg) =>
|
|
164
|
+
arg.includes(" ") ? `"${arg}"` : arg
|
|
165
|
+
);
|
|
166
|
+
return `${command} ${quotedArgs.join(" ")}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 截断长文本
|
|
171
|
+
*/
|
|
172
|
+
static truncateText(text: string, maxLength: number, suffix = "..."): string {
|
|
173
|
+
if (text.length <= maxLength) return text;
|
|
174
|
+
return text.substring(0, maxLength - suffix.length) + suffix;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 格式化 JSON
|
|
179
|
+
*/
|
|
180
|
+
static formatJson(obj: any, indent = 2): string {
|
|
181
|
+
try {
|
|
182
|
+
return JSON.stringify(obj, null, indent);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
return String(obj);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 格式化布尔值
|
|
190
|
+
*/
|
|
191
|
+
static formatBoolean(
|
|
192
|
+
value: boolean,
|
|
193
|
+
trueText = "是",
|
|
194
|
+
falseText = "否"
|
|
195
|
+
): string {
|
|
196
|
+
return value ? trueText : falseText;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 路径处理工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { realpathSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import {
|
|
10
|
+
CONFIG_CONSTANTS,
|
|
11
|
+
PATH_CONSTANTS,
|
|
12
|
+
SERVICE_CONSTANTS,
|
|
13
|
+
} from "../Constants";
|
|
14
|
+
import { FileUtils } from "./FileUtils";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 路径工具类
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export class PathUtils {
|
|
21
|
+
/**
|
|
22
|
+
* 获取 PID 文件路径
|
|
23
|
+
*/
|
|
24
|
+
static getPidFile(): string {
|
|
25
|
+
// 优先使用环境变量中的配置目录,否则使用当前工作目录
|
|
26
|
+
const configDir =
|
|
27
|
+
process.env[CONFIG_CONSTANTS.DIR_ENV_VAR] || process.cwd();
|
|
28
|
+
return path.join(configDir, `.${SERVICE_CONSTANTS.NAME}.pid`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 获取日志文件路径
|
|
33
|
+
*/
|
|
34
|
+
static getLogFile(projectDir?: string): string {
|
|
35
|
+
const baseDir = projectDir || process.cwd();
|
|
36
|
+
return path.join(baseDir, SERVICE_CONSTANTS.LOG_FILE);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 获取配置目录路径
|
|
41
|
+
*/
|
|
42
|
+
static getConfigDir(): string {
|
|
43
|
+
return process.env[CONFIG_CONSTANTS.DIR_ENV_VAR] || process.cwd();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取工作目录路径
|
|
48
|
+
*/
|
|
49
|
+
static getWorkDir(): string {
|
|
50
|
+
const configDir = PathUtils.getConfigDir();
|
|
51
|
+
return path.join(configDir, PATH_CONSTANTS.WORK_DIR);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 获取模板目录路径
|
|
56
|
+
*/
|
|
57
|
+
static getTemplatesDir(): string[] {
|
|
58
|
+
// 在 ES 模块环境中获取当前目录
|
|
59
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
60
|
+
const scriptDir = path.dirname(__filename);
|
|
61
|
+
|
|
62
|
+
return [
|
|
63
|
+
// 构建后的环境:dist/cli.js -> dist/templates
|
|
64
|
+
path.join(scriptDir, PATH_CONSTANTS.TEMPLATES_DIR),
|
|
65
|
+
// 构建环境:dist/cli/index.js -> dist/backend/templates
|
|
66
|
+
path.join(scriptDir, "..", "backend", PATH_CONSTANTS.TEMPLATES_DIR),
|
|
67
|
+
// npm 全局安装
|
|
68
|
+
path.join(
|
|
69
|
+
scriptDir,
|
|
70
|
+
"..",
|
|
71
|
+
"..",
|
|
72
|
+
"..",
|
|
73
|
+
"..",
|
|
74
|
+
PATH_CONSTANTS.TEMPLATES_DIR
|
|
75
|
+
),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 查找模板目录
|
|
81
|
+
*/
|
|
82
|
+
static findTemplatesDir(): string | null {
|
|
83
|
+
const possiblePaths = PathUtils.getTemplatesDir();
|
|
84
|
+
|
|
85
|
+
for (const templatesDir of possiblePaths) {
|
|
86
|
+
if (FileUtils.exists(templatesDir)) {
|
|
87
|
+
return templatesDir;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 获取模板路径
|
|
96
|
+
*/
|
|
97
|
+
static getTemplatePath(templateName: string): string | null {
|
|
98
|
+
const templatesDir = PathUtils.findTemplatesDir();
|
|
99
|
+
if (!templatesDir) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const templatePath = path.join(templatesDir, templateName);
|
|
104
|
+
return FileUtils.exists(templatePath) ? templatePath : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 获取脚本目录路径
|
|
109
|
+
*/
|
|
110
|
+
static getScriptDir(): string {
|
|
111
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
112
|
+
return path.dirname(__filename);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 获取项目根目录路径
|
|
117
|
+
*/
|
|
118
|
+
static getProjectRoot(): string {
|
|
119
|
+
const scriptDir = PathUtils.getScriptDir();
|
|
120
|
+
// 从 src/cli/utils 回到项目根目录
|
|
121
|
+
return path.join(scriptDir, "..", "..", "..");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 获取构建输出目录路径
|
|
126
|
+
*/
|
|
127
|
+
static getDistDir(): string {
|
|
128
|
+
const projectRoot = PathUtils.getProjectRoot();
|
|
129
|
+
return path.join(projectRoot, "dist");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取相对于项目根目录的路径
|
|
134
|
+
*/
|
|
135
|
+
static getRelativePath(filePath: string): string {
|
|
136
|
+
const projectRoot = PathUtils.getProjectRoot();
|
|
137
|
+
return path.relative(projectRoot, filePath);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 解析配置文件路径
|
|
142
|
+
*/
|
|
143
|
+
static resolveConfigPath(format?: "json" | "json5" | "jsonc"): string {
|
|
144
|
+
const configDir = PathUtils.getConfigDir();
|
|
145
|
+
|
|
146
|
+
if (format) {
|
|
147
|
+
return path.join(configDir, `xiaozhi.config.${format}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 按优先级查找配置文件
|
|
151
|
+
for (const fileName of CONFIG_CONSTANTS.FILE_NAMES) {
|
|
152
|
+
const filePath = path.join(configDir, fileName);
|
|
153
|
+
if (FileUtils.exists(filePath)) {
|
|
154
|
+
return filePath;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 返回默认配置文件路径
|
|
159
|
+
return path.join(configDir, CONFIG_CONSTANTS.FILE_NAMES[2]); // xiaozhi.config.json
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 获取默认配置文件路径
|
|
164
|
+
*/
|
|
165
|
+
static getDefaultConfigPath(): string {
|
|
166
|
+
const projectRoot = PathUtils.getProjectRoot();
|
|
167
|
+
return path.join(projectRoot, CONFIG_CONSTANTS.DEFAULT_FILE);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 验证路径安全性(防止路径遍历攻击)
|
|
172
|
+
*/
|
|
173
|
+
static validatePath(inputPath: string): boolean {
|
|
174
|
+
const normalizedPath = path.normalize(inputPath);
|
|
175
|
+
return !normalizedPath.includes("..");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 确保路径在指定目录内
|
|
180
|
+
*/
|
|
181
|
+
static ensurePathWithin(inputPath: string, baseDir: string): string {
|
|
182
|
+
const resolvedPath = path.resolve(baseDir, inputPath);
|
|
183
|
+
const resolvedBase = path.resolve(baseDir);
|
|
184
|
+
|
|
185
|
+
if (!resolvedPath.startsWith(resolvedBase)) {
|
|
186
|
+
throw new Error(`路径 ${inputPath} 超出了允许的范围`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return resolvedPath;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 获取可执行文件路径
|
|
194
|
+
*/
|
|
195
|
+
static getExecutablePath(name: string): string {
|
|
196
|
+
// 获取当前执行的 CLI 脚本路径
|
|
197
|
+
const cliPath = process.argv[1];
|
|
198
|
+
|
|
199
|
+
// 处理 cliPath 为 undefined 的情况
|
|
200
|
+
if (!cliPath) {
|
|
201
|
+
// 如果没有脚本路径,使用当前工作目录
|
|
202
|
+
return path.join(process.cwd(), `${name}.js`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 解析符号链接,获取真实路径
|
|
206
|
+
let realCliPath: string;
|
|
207
|
+
try {
|
|
208
|
+
realCliPath = realpathSync(cliPath);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
// 如果无法解析符号链接,使用原路径
|
|
211
|
+
realCliPath = cliPath;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 获取 dist 目录
|
|
215
|
+
const distDir = path.dirname(realCliPath);
|
|
216
|
+
return path.join(distDir, `${name}.js`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 获取 Web 服务器启动器路径
|
|
221
|
+
*/
|
|
222
|
+
static getWebServerLauncherPath(): string {
|
|
223
|
+
return PathUtils.getExecutablePath("WebServerLauncher");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 创建安全的文件路径
|
|
228
|
+
*/
|
|
229
|
+
static createSafePath(...segments: string[]): string {
|
|
230
|
+
const joinedPath = path.join(...segments);
|
|
231
|
+
const normalizedPath = path.normalize(joinedPath);
|
|
232
|
+
|
|
233
|
+
// 检查路径是否包含危险字符
|
|
234
|
+
if (normalizedPath.includes("..") || normalizedPath.includes("~")) {
|
|
235
|
+
throw new Error(`不安全的路径: ${normalizedPath}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return normalizedPath;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 获取临时目录路径
|
|
243
|
+
*/
|
|
244
|
+
static getTempDir(): string {
|
|
245
|
+
// 使用 Node.js 的 os.tmpdir() 来获取跨平台的临时目录
|
|
246
|
+
return process.env.TMPDIR || process.env.TEMP || tmpdir();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 获取用户主目录路径
|
|
251
|
+
*/
|
|
252
|
+
static getHomeDir(): string {
|
|
253
|
+
return process.env.HOME || process.env.USERPROFILE || "";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 平台相关工具
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { TIMEOUT_CONSTANTS } from "../Constants";
|
|
7
|
+
import type { Platform } from "../Types";
|
|
8
|
+
import { ProcessError } from "../errors/index";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 平台工具类
|
|
12
|
+
*/
|
|
13
|
+
export class PlatformUtils {
|
|
14
|
+
/**
|
|
15
|
+
* 获取当前平台
|
|
16
|
+
*/
|
|
17
|
+
static getCurrentPlatform(): Platform {
|
|
18
|
+
return process.platform as Platform;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 检查是否为 Windows 平台
|
|
23
|
+
*/
|
|
24
|
+
static isWindows(): boolean {
|
|
25
|
+
return process.platform === "win32";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 检查是否为 macOS 平台
|
|
30
|
+
*/
|
|
31
|
+
static isMacOS(): boolean {
|
|
32
|
+
return process.platform === "darwin";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 检查是否为 Linux 平台
|
|
37
|
+
*/
|
|
38
|
+
static isLinux(): boolean {
|
|
39
|
+
return process.platform === "linux";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 检查是否为类 Unix 系统
|
|
44
|
+
*/
|
|
45
|
+
static isUnixLike(): boolean {
|
|
46
|
+
return !PlatformUtils.isWindows();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 检查进程是否为 xiaozhi-client 进程
|
|
51
|
+
*/
|
|
52
|
+
static isXiaozhiProcess(pid: number): boolean {
|
|
53
|
+
try {
|
|
54
|
+
// 在容器环境或测试环境中,使用更宽松的检查策略
|
|
55
|
+
if (
|
|
56
|
+
process.env.XIAOZHI_CONTAINER === "true" ||
|
|
57
|
+
process.env.NODE_ENV === "test"
|
|
58
|
+
) {
|
|
59
|
+
// 容器环境或测试环境中,如果 PID 存在就认为是有效的
|
|
60
|
+
// 因为容器通常只运行一个主要应用,测试环境中mock了进程检查
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 非容器环境中,尝试更严格的进程检查
|
|
66
|
+
try {
|
|
67
|
+
let cmdline = "";
|
|
68
|
+
if (PlatformUtils.isWindows()) {
|
|
69
|
+
// Windows 系统
|
|
70
|
+
const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
timeout: TIMEOUT_CONSTANTS.PROCESS_STOP,
|
|
73
|
+
});
|
|
74
|
+
cmdline = result.toLowerCase();
|
|
75
|
+
} else {
|
|
76
|
+
// Unix-like 系统
|
|
77
|
+
const result = execSync(`ps -p ${pid} -o comm=`, {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
timeout: TIMEOUT_CONSTANTS.PROCESS_STOP,
|
|
80
|
+
});
|
|
81
|
+
cmdline = result.toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 检查是否包含 node 或 xiaozhi 相关关键词
|
|
85
|
+
return cmdline.includes("node") || cmdline.includes("xiaozhi");
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// 如果无法获取进程信息,回退到简单的 PID 检查
|
|
88
|
+
process.kill(pid, 0);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 杀死进程
|
|
98
|
+
*/
|
|
99
|
+
static async killProcess(
|
|
100
|
+
pid: number,
|
|
101
|
+
signal: NodeJS.Signals = "SIGTERM"
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
process.kill(pid, signal);
|
|
105
|
+
|
|
106
|
+
// 等待进程停止
|
|
107
|
+
let attempts = 0;
|
|
108
|
+
const maxAttempts = 30; // 3秒超时
|
|
109
|
+
|
|
110
|
+
while (attempts < maxAttempts) {
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
process.kill(pid, 0);
|
|
115
|
+
attempts++;
|
|
116
|
+
} catch {
|
|
117
|
+
// 进程已停止
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 如果还在运行,强制停止
|
|
123
|
+
try {
|
|
124
|
+
process.kill(pid, 0);
|
|
125
|
+
process.kill(pid, "SIGKILL");
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
127
|
+
} catch {
|
|
128
|
+
// 进程已停止
|
|
129
|
+
}
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new ProcessError(
|
|
132
|
+
`无法终止进程: ${error instanceof Error ? error.message : String(error)}`,
|
|
133
|
+
pid
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 检查进程是否存在
|
|
140
|
+
*/
|
|
141
|
+
static processExists(pid: number): boolean {
|
|
142
|
+
try {
|
|
143
|
+
process.kill(pid, 0);
|
|
144
|
+
return true;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 获取系统信息
|
|
152
|
+
*/
|
|
153
|
+
static getSystemInfo(): {
|
|
154
|
+
platform: Platform;
|
|
155
|
+
arch: string;
|
|
156
|
+
nodeVersion: string;
|
|
157
|
+
isContainer: boolean;
|
|
158
|
+
} {
|
|
159
|
+
return {
|
|
160
|
+
platform: PlatformUtils.getCurrentPlatform(),
|
|
161
|
+
arch: process.arch,
|
|
162
|
+
nodeVersion: process.version,
|
|
163
|
+
isContainer: process.env.XIAOZHI_CONTAINER === "true",
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 获取环境变量
|
|
169
|
+
*/
|
|
170
|
+
static getEnvVar(name: string, defaultValue?: string): string | undefined {
|
|
171
|
+
return process.env[name] || defaultValue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 设置环境变量
|
|
176
|
+
*/
|
|
177
|
+
static setEnvVar(name: string, value: string): void {
|
|
178
|
+
process.env[name] = value;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 检查是否在容器环境中运行
|
|
183
|
+
*/
|
|
184
|
+
static isContainerEnvironment(): boolean {
|
|
185
|
+
return process.env.XIAOZHI_CONTAINER === "true";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 检查是否在测试环境中运行
|
|
190
|
+
*/
|
|
191
|
+
static isTestEnvironment(): boolean {
|
|
192
|
+
return process.env.NODE_ENV === "test";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 检查是否在开发环境中运行
|
|
197
|
+
*/
|
|
198
|
+
static isDevelopmentEnvironment(): boolean {
|
|
199
|
+
return process.env.NODE_ENV === "development";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 获取合适的 tail 命令
|
|
204
|
+
*/
|
|
205
|
+
static getTailCommand(filePath: string): { command: string; args: string[] } {
|
|
206
|
+
if (PlatformUtils.isWindows()) {
|
|
207
|
+
return {
|
|
208
|
+
command: "powershell",
|
|
209
|
+
args: ["-Command", `Get-Content -Path "${filePath}" -Wait`],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
command: "tail",
|
|
214
|
+
args: ["-f", filePath],
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}
|