@xiaozhi-client/cli 1.9.4-beta.10
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 +98 -0
- package/package.json +31 -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/tsup.config.ts +100 -0
- package/vitest.config.ts +97 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务接口定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 服务管理器接口
|
|
7
|
+
*/
|
|
8
|
+
export interface ServiceManager {
|
|
9
|
+
/** 启动服务 */
|
|
10
|
+
start(options: ServiceStartOptions): Promise<void>;
|
|
11
|
+
/** 停止服务 */
|
|
12
|
+
stop(): Promise<void>;
|
|
13
|
+
/** 重启服务 */
|
|
14
|
+
restart(options: ServiceStartOptions): Promise<void>;
|
|
15
|
+
/** 获取服务状态 */
|
|
16
|
+
getStatus(): ServiceStatus;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 服务启动选项
|
|
21
|
+
*/
|
|
22
|
+
export interface ServiceStartOptions {
|
|
23
|
+
/** 是否后台运行 */
|
|
24
|
+
daemon?: boolean;
|
|
25
|
+
/** 是否启动 Web UI */
|
|
26
|
+
ui?: boolean;
|
|
27
|
+
/** 端口号 */
|
|
28
|
+
port?: number;
|
|
29
|
+
/** 运行模式 */
|
|
30
|
+
mode?: "normal" | "mcp-server" | "stdio";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 服务状态
|
|
35
|
+
*/
|
|
36
|
+
export interface ServiceStatus {
|
|
37
|
+
/** 是否正在运行 */
|
|
38
|
+
running: boolean;
|
|
39
|
+
/** 进程 ID */
|
|
40
|
+
pid?: number;
|
|
41
|
+
/** 运行时间 */
|
|
42
|
+
uptime?: string;
|
|
43
|
+
/** 运行模式 */
|
|
44
|
+
mode?: "foreground" | "daemon";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 进程管理器接口
|
|
49
|
+
*/
|
|
50
|
+
export interface ProcessManager {
|
|
51
|
+
/** 获取服务状态 */
|
|
52
|
+
getServiceStatus(): ServiceStatus;
|
|
53
|
+
/** 杀死进程 */
|
|
54
|
+
killProcess(pid: number): Promise<void>;
|
|
55
|
+
/** 清理 PID 文件 */
|
|
56
|
+
cleanupPidFile(): void;
|
|
57
|
+
/** 检查是否为 xiaozhi 进程 */
|
|
58
|
+
isXiaozhiProcess(pid: number): boolean;
|
|
59
|
+
/** 保存进程信息 */
|
|
60
|
+
savePidInfo(pid: number, mode: "foreground" | "daemon"): void;
|
|
61
|
+
/** 优雅停止进程 */
|
|
62
|
+
gracefulKillProcess(pid: number): Promise<void>;
|
|
63
|
+
/** 检查进程是否存在 */
|
|
64
|
+
processExists(pid: number): boolean;
|
|
65
|
+
/** 清理容器环境状态 */
|
|
66
|
+
cleanupContainerState(): void;
|
|
67
|
+
/** 获取进程信息 */
|
|
68
|
+
getProcessInfo(pid: number): { exists: boolean; isXiaozhi: boolean };
|
|
69
|
+
/** 验证 PID 文件完整性 */
|
|
70
|
+
validatePidFile(): boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 守护进程管理器接口
|
|
75
|
+
*/
|
|
76
|
+
export interface DaemonManager {
|
|
77
|
+
/** 启动守护进程 */
|
|
78
|
+
startDaemon(serverFactory: () => Promise<any>): Promise<void>;
|
|
79
|
+
/** 停止守护进程 */
|
|
80
|
+
stopDaemon(): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 模板管理器接口
|
|
85
|
+
*/
|
|
86
|
+
export interface TemplateManager {
|
|
87
|
+
/** 获取可用模板列表 */
|
|
88
|
+
getAvailableTemplates(): Promise<any[]>;
|
|
89
|
+
/** 复制模板到目标目录 */
|
|
90
|
+
copyTemplate(templateName: string, targetPath: string): Promise<void>;
|
|
91
|
+
/** 验证模板是否存在 */
|
|
92
|
+
validateTemplate(templateName: string): Promise<boolean>;
|
|
93
|
+
/** 获取模板信息 */
|
|
94
|
+
getTemplateInfo(templateName: string): Promise<any | null>;
|
|
95
|
+
/** 创建项目 */
|
|
96
|
+
createProject(options: any): Promise<void>;
|
|
97
|
+
/** 清除模板缓存 */
|
|
98
|
+
clearCache(): void;
|
|
99
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 守护进程管理服务
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ChildProcess } from "node:child_process";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import type { WebServer } from "@root/WebServer";
|
|
9
|
+
import consola from "consola";
|
|
10
|
+
import { ProcessError, ServiceError } from "../errors/index";
|
|
11
|
+
import type {
|
|
12
|
+
DaemonManager as IDaemonManager,
|
|
13
|
+
ProcessManager,
|
|
14
|
+
} from "../interfaces/Service";
|
|
15
|
+
import { PathUtils } from "../utils/PathUtils";
|
|
16
|
+
import { PlatformUtils } from "../utils/PlatformUtils";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 守护进程选项
|
|
20
|
+
*/
|
|
21
|
+
export interface DaemonOptions {
|
|
22
|
+
/** 日志文件名 */
|
|
23
|
+
logFileName?: string;
|
|
24
|
+
/** 环境变量 */
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
/** 是否打开浏览器 */
|
|
27
|
+
openBrowser?: boolean;
|
|
28
|
+
/** 工作目录 */
|
|
29
|
+
cwd?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 守护进程管理器实现
|
|
34
|
+
*/
|
|
35
|
+
export class DaemonManagerImpl implements IDaemonManager {
|
|
36
|
+
private currentDaemon: ChildProcess | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(private processManager: ProcessManager) {}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 启动守护进程
|
|
42
|
+
*/
|
|
43
|
+
async startDaemon(
|
|
44
|
+
serverFactory: () => Promise<WebServer>,
|
|
45
|
+
options: DaemonOptions = {}
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
try {
|
|
48
|
+
// 检查是否已有守护进程在运行
|
|
49
|
+
const status = this.processManager.getServiceStatus();
|
|
50
|
+
if (status.running) {
|
|
51
|
+
throw ServiceError.alreadyRunning(status.pid!);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 启动守护进程
|
|
55
|
+
const child = await this.spawnDaemonProcess(serverFactory, options);
|
|
56
|
+
this.currentDaemon = child;
|
|
57
|
+
|
|
58
|
+
// 保存 PID 信息
|
|
59
|
+
this.processManager.savePidInfo(child.pid!, "daemon");
|
|
60
|
+
|
|
61
|
+
// 设置日志重定向
|
|
62
|
+
await this.setupLogging(child, options.logFileName || "xiaozhi.log");
|
|
63
|
+
|
|
64
|
+
// 设置进程事件监听
|
|
65
|
+
this.setupEventHandlers(child);
|
|
66
|
+
|
|
67
|
+
// 分离进程
|
|
68
|
+
child.unref();
|
|
69
|
+
|
|
70
|
+
consola.info(`守护进程已启动 (PID: ${child.pid})`);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new ServiceError(
|
|
73
|
+
`启动守护进程失败: ${error instanceof Error ? error.message : String(error)}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 停止守护进程
|
|
80
|
+
*/
|
|
81
|
+
async stopDaemon(): Promise<void> {
|
|
82
|
+
try {
|
|
83
|
+
const status = this.processManager.getServiceStatus();
|
|
84
|
+
|
|
85
|
+
if (!status.running) {
|
|
86
|
+
throw ServiceError.notRunning();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 优雅停止守护进程
|
|
90
|
+
await this.processManager.gracefulKillProcess(status.pid!);
|
|
91
|
+
|
|
92
|
+
// 清理 PID 文件
|
|
93
|
+
this.processManager.cleanupPidFile();
|
|
94
|
+
|
|
95
|
+
// 清理当前守护进程引用
|
|
96
|
+
this.currentDaemon = null;
|
|
97
|
+
|
|
98
|
+
consola.info("守护进程已停止");
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new ServiceError(
|
|
101
|
+
`停止守护进程失败: ${error instanceof Error ? error.message : String(error)}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 重启守护进程
|
|
108
|
+
*/
|
|
109
|
+
async restartDaemon(
|
|
110
|
+
serverFactory: () => Promise<WebServer>,
|
|
111
|
+
options: DaemonOptions = {}
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
try {
|
|
114
|
+
// 先停止现有守护进程
|
|
115
|
+
const status = this.processManager.getServiceStatus();
|
|
116
|
+
if (status.running) {
|
|
117
|
+
await this.stopDaemon();
|
|
118
|
+
// 等待一下确保完全停止
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 重新启动守护进程
|
|
123
|
+
await this.startDaemon(serverFactory, options);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new ServiceError(
|
|
126
|
+
`重启守护进程失败: ${error instanceof Error ? error.message : String(error)}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 获取守护进程状态
|
|
133
|
+
*/
|
|
134
|
+
getDaemonStatus(): { running: boolean; pid?: number; uptime?: string } {
|
|
135
|
+
return this.processManager.getServiceStatus();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 连接到守护进程日志
|
|
140
|
+
*/
|
|
141
|
+
async attachToLogs(logFileName = "xiaozhi.log"): Promise<void> {
|
|
142
|
+
try {
|
|
143
|
+
const logFilePath = PathUtils.getLogFile();
|
|
144
|
+
|
|
145
|
+
if (!fs.existsSync(logFilePath)) {
|
|
146
|
+
throw new ServiceError("日志文件不存在");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 使用平台相关的 tail 命令
|
|
150
|
+
const { command, args } = PlatformUtils.getTailCommand(logFilePath);
|
|
151
|
+
const tail = spawn(command, args, { stdio: "inherit" });
|
|
152
|
+
|
|
153
|
+
// 处理中断信号
|
|
154
|
+
process.on("SIGINT", () => {
|
|
155
|
+
console.log("\n断开连接,服务继续在后台运行");
|
|
156
|
+
tail.kill();
|
|
157
|
+
process.exit(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
tail.on("exit", () => {
|
|
161
|
+
process.exit(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
tail.on("error", (error) => {
|
|
165
|
+
throw new ServiceError(`连接日志失败: ${error.message}`);
|
|
166
|
+
});
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw new ServiceError(
|
|
169
|
+
`连接日志失败: ${error instanceof Error ? error.message : String(error)}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 生成守护进程
|
|
176
|
+
*/
|
|
177
|
+
private async spawnDaemonProcess(
|
|
178
|
+
serverFactory: () => Promise<WebServer>,
|
|
179
|
+
options: DaemonOptions
|
|
180
|
+
): Promise<ChildProcess> {
|
|
181
|
+
// 获取启动脚本路径
|
|
182
|
+
const scriptPath = PathUtils.getWebServerLauncherPath();
|
|
183
|
+
|
|
184
|
+
// 构建启动参数
|
|
185
|
+
const args = [scriptPath];
|
|
186
|
+
if (options.openBrowser) {
|
|
187
|
+
args.push("--open-browser");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 构建环境变量
|
|
191
|
+
const env = {
|
|
192
|
+
...process.env,
|
|
193
|
+
XIAOZHI_CONFIG_DIR: PathUtils.getConfigDir(),
|
|
194
|
+
XIAOZHI_DAEMON: "true",
|
|
195
|
+
...options.env,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// 启动子进程
|
|
199
|
+
const child = spawn("node", args, {
|
|
200
|
+
detached: true,
|
|
201
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
202
|
+
env,
|
|
203
|
+
cwd: options.cwd || process.cwd(),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!child.pid) {
|
|
207
|
+
throw new ProcessError("无法启动守护进程", 0);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return child;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 设置日志重定向
|
|
215
|
+
*/
|
|
216
|
+
private async setupLogging(
|
|
217
|
+
child: ChildProcess,
|
|
218
|
+
logFileName: string
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
try {
|
|
221
|
+
const logFilePath = PathUtils.getLogFile();
|
|
222
|
+
|
|
223
|
+
// 确保日志目录存在
|
|
224
|
+
const path = await import("node:path");
|
|
225
|
+
const logDir = path.dirname(logFilePath);
|
|
226
|
+
if (!fs.existsSync(logDir)) {
|
|
227
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 创建日志流
|
|
231
|
+
const logStream = fs.createWriteStream(logFilePath, { flags: "a" });
|
|
232
|
+
|
|
233
|
+
// 重定向标准输出和错误输出
|
|
234
|
+
child.stdout?.pipe(logStream);
|
|
235
|
+
child.stderr?.pipe(logStream);
|
|
236
|
+
|
|
237
|
+
// 写入启动日志
|
|
238
|
+
const timestamp = new Date().toISOString();
|
|
239
|
+
logStream.write(`\n[${timestamp}] 守护进程启动 (PID: ${child.pid})\n`);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
consola.warn(
|
|
242
|
+
`设置日志重定向失败: ${error instanceof Error ? error.message : String(error)}`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 设置事件处理器
|
|
249
|
+
*/
|
|
250
|
+
private setupEventHandlers(child: ChildProcess): void {
|
|
251
|
+
// 监听进程退出
|
|
252
|
+
child.on("exit", (code, signal) => {
|
|
253
|
+
if (code !== 0 && code !== null) {
|
|
254
|
+
consola.error(`守护进程异常退出 (代码: ${code}, 信号: ${signal})`);
|
|
255
|
+
} else {
|
|
256
|
+
consola.info("守护进程正常退出");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 清理 PID 文件
|
|
260
|
+
this.processManager.cleanupPidFile();
|
|
261
|
+
this.currentDaemon = null;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// 监听进程错误
|
|
265
|
+
child.on("error", (error) => {
|
|
266
|
+
consola.error(`守护进程错误: ${error.message}`);
|
|
267
|
+
this.processManager.cleanupPidFile();
|
|
268
|
+
this.currentDaemon = null;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// 监听进程断开连接
|
|
272
|
+
child.on("disconnect", () => {
|
|
273
|
+
consola.info("守护进程断开连接");
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* 检查守护进程健康状态
|
|
279
|
+
*/
|
|
280
|
+
async checkHealth(): Promise<boolean> {
|
|
281
|
+
try {
|
|
282
|
+
const status = this.getDaemonStatus();
|
|
283
|
+
|
|
284
|
+
if (!status.running || !status.pid) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 检查进程是否真的在运行
|
|
289
|
+
const processInfo = this.processManager.getProcessInfo(status.pid);
|
|
290
|
+
return processInfo.exists && processInfo.isXiaozhi;
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 获取当前守护进程引用
|
|
298
|
+
*/
|
|
299
|
+
getCurrentDaemon(): ChildProcess | null {
|
|
300
|
+
return this.currentDaemon;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* 清理守护进程资源
|
|
305
|
+
*/
|
|
306
|
+
cleanup(): void {
|
|
307
|
+
if (this.currentDaemon) {
|
|
308
|
+
try {
|
|
309
|
+
this.currentDaemon.kill("SIGTERM");
|
|
310
|
+
} catch (error) {
|
|
311
|
+
consola.warn(
|
|
312
|
+
`清理守护进程失败: ${error instanceof Error ? error.message : String(error)}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
this.currentDaemon = null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { FileError, ProcessError } from "../errors/index";
|
|
2
|
+
import type {
|
|
3
|
+
ProcessManager as IProcessManager,
|
|
4
|
+
ServiceStatus,
|
|
5
|
+
} from "../interfaces/Service";
|
|
6
|
+
import { FileUtils } from "../utils/FileUtils";
|
|
7
|
+
import { FormatUtils } from "../utils/FormatUtils";
|
|
8
|
+
import { PathUtils } from "../utils/PathUtils";
|
|
9
|
+
import { PlatformUtils } from "../utils/PlatformUtils";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* PID 文件信息接口
|
|
13
|
+
*/
|
|
14
|
+
interface PidFileInfo {
|
|
15
|
+
pid: number;
|
|
16
|
+
startTime: number;
|
|
17
|
+
mode: "foreground" | "daemon";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 进程管理器实现
|
|
22
|
+
*/
|
|
23
|
+
export class ProcessManagerImpl implements IProcessManager {
|
|
24
|
+
/**
|
|
25
|
+
* 获取 PID 文件路径
|
|
26
|
+
*/
|
|
27
|
+
private getPidFilePath(): string {
|
|
28
|
+
return PathUtils.getPidFile();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 读取 PID 文件信息
|
|
33
|
+
*/
|
|
34
|
+
private readPidFile(): PidFileInfo | null {
|
|
35
|
+
try {
|
|
36
|
+
const pidFilePath = this.getPidFilePath();
|
|
37
|
+
|
|
38
|
+
if (!FileUtils.exists(pidFilePath)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pidContent = FileUtils.readFile(pidFilePath).trim();
|
|
43
|
+
const [pidStr, startTimeStr, mode] = pidContent.split("|");
|
|
44
|
+
|
|
45
|
+
const pid = Number.parseInt(pidStr);
|
|
46
|
+
const startTime = Number.parseInt(startTimeStr);
|
|
47
|
+
|
|
48
|
+
if (Number.isNaN(pid) || Number.isNaN(startTime)) {
|
|
49
|
+
// PID 文件损坏,删除它
|
|
50
|
+
this.cleanupPidFile();
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
pid,
|
|
56
|
+
startTime,
|
|
57
|
+
mode: (mode as "foreground" | "daemon") || "foreground",
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
// 读取失败,可能文件损坏
|
|
61
|
+
this.cleanupPidFile();
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 写入 PID 文件信息
|
|
68
|
+
*/
|
|
69
|
+
private writePidFile(pid: number, mode: "foreground" | "daemon"): void {
|
|
70
|
+
try {
|
|
71
|
+
const pidInfo = `${pid}|${Date.now()}|${mode}`;
|
|
72
|
+
const pidFilePath = this.getPidFilePath();
|
|
73
|
+
FileUtils.writeFile(pidFilePath, pidInfo, { overwrite: true });
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new FileError("无法写入 PID 文件", this.getPidFilePath());
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 检查是否为 xiaozhi 进程
|
|
81
|
+
*/
|
|
82
|
+
isXiaozhiProcess(pid: number): boolean {
|
|
83
|
+
return PlatformUtils.isXiaozhiProcess(pid);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 获取服务状态
|
|
88
|
+
*/
|
|
89
|
+
getServiceStatus(): ServiceStatus {
|
|
90
|
+
try {
|
|
91
|
+
const pidInfo = this.readPidFile();
|
|
92
|
+
|
|
93
|
+
if (!pidInfo) {
|
|
94
|
+
return { running: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 检查进程是否还在运行且是 xiaozhi 进程
|
|
98
|
+
if (!this.isXiaozhiProcess(pidInfo.pid)) {
|
|
99
|
+
// 进程不存在或不是 xiaozhi 进程,删除 PID 文件
|
|
100
|
+
this.cleanupPidFile();
|
|
101
|
+
return { running: false };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 计算运行时间
|
|
105
|
+
const uptime = FormatUtils.formatUptime(Date.now() - pidInfo.startTime);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
running: true,
|
|
109
|
+
pid: pidInfo.pid,
|
|
110
|
+
uptime,
|
|
111
|
+
mode: pidInfo.mode,
|
|
112
|
+
};
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return { running: false };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 保存进程信息
|
|
120
|
+
*/
|
|
121
|
+
savePidInfo(pid: number, mode: "foreground" | "daemon"): void {
|
|
122
|
+
this.writePidFile(pid, mode);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 杀死进程
|
|
127
|
+
*/
|
|
128
|
+
async killProcess(pid: number): Promise<void> {
|
|
129
|
+
try {
|
|
130
|
+
await PlatformUtils.killProcess(pid);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new ProcessError(
|
|
133
|
+
`无法终止进程: ${error instanceof Error ? error.message : String(error)}`,
|
|
134
|
+
pid
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 优雅停止进程
|
|
141
|
+
*/
|
|
142
|
+
async gracefulKillProcess(pid: number): Promise<void> {
|
|
143
|
+
try {
|
|
144
|
+
// 尝试优雅停止
|
|
145
|
+
process.kill(pid, "SIGTERM");
|
|
146
|
+
|
|
147
|
+
// 等待进程停止
|
|
148
|
+
let attempts = 0;
|
|
149
|
+
const maxAttempts = 30; // 3秒超时
|
|
150
|
+
|
|
151
|
+
while (attempts < maxAttempts) {
|
|
152
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
process.kill(pid, 0);
|
|
156
|
+
attempts++;
|
|
157
|
+
} catch {
|
|
158
|
+
// 进程已停止
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 如果还在运行,强制停止
|
|
164
|
+
try {
|
|
165
|
+
process.kill(pid, 0);
|
|
166
|
+
process.kill(pid, "SIGKILL");
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
168
|
+
} catch {
|
|
169
|
+
// 进程已停止
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new ProcessError(
|
|
173
|
+
`无法停止进程: ${error instanceof Error ? error.message : String(error)}`,
|
|
174
|
+
pid
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 清理 PID 文件
|
|
181
|
+
*/
|
|
182
|
+
cleanupPidFile(): void {
|
|
183
|
+
try {
|
|
184
|
+
const pidFilePath = this.getPidFilePath();
|
|
185
|
+
if (FileUtils.exists(pidFilePath)) {
|
|
186
|
+
FileUtils.deleteFile(pidFilePath);
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
// 忽略清理错误,但可以记录日志
|
|
190
|
+
console.warn("清理 PID 文件失败:", error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 检查进程是否存在
|
|
196
|
+
*/
|
|
197
|
+
processExists(pid: number): boolean {
|
|
198
|
+
return PlatformUtils.processExists(pid);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 清理容器环境的旧状态
|
|
203
|
+
*/
|
|
204
|
+
cleanupContainerState(): void {
|
|
205
|
+
if (PlatformUtils.isContainerEnvironment()) {
|
|
206
|
+
try {
|
|
207
|
+
this.cleanupPidFile();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
// 忽略清理错误
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 获取进程信息
|
|
216
|
+
*/
|
|
217
|
+
getProcessInfo(pid: number): { exists: boolean; isXiaozhi: boolean } {
|
|
218
|
+
const exists = this.processExists(pid);
|
|
219
|
+
const isXiaozhi = exists ? this.isXiaozhiProcess(pid) : false;
|
|
220
|
+
|
|
221
|
+
return { exists, isXiaozhi };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 验证 PID 文件完整性
|
|
226
|
+
*/
|
|
227
|
+
validatePidFile(): boolean {
|
|
228
|
+
try {
|
|
229
|
+
const pidInfo = this.readPidFile();
|
|
230
|
+
return pidInfo !== null;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|