@xiaozhi-client/cli 1.10.9 → 2.0.0-beta.4
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 +8 -5
- package/package.json +2 -2
- package/src/Container.ts +0 -34
- package/src/commands/index.ts +42 -38
- package/src/global.d.ts +8 -0
- package/src/services/DaemonManager.ts +1 -1
- package/src/services/ProcessManager.ts +1 -27
- package/src/services/ServiceManager.ts +4 -4
- package/src/services/__tests__/ProcessManager.test.ts +11 -31
- package/src/services/__tests__/ServiceManager.test.ts +0 -22
- package/src/types/backend.d.ts +30 -18
- package/tsup.config.ts +1 -1
package/README.md
CHANGED
|
@@ -47,10 +47,10 @@ packages/cli/
|
|
|
47
47
|
|
|
48
48
|
## 外部依赖
|
|
49
49
|
|
|
50
|
-
CLI 包通过 external 配置引用 backend
|
|
50
|
+
CLI 包通过 external 配置引用 backend 模块和 workspace 包:
|
|
51
51
|
|
|
52
52
|
- `@/WebServer` → `dist/backend/WebServer.js` (通过 WebServerLauncher)
|
|
53
|
-
-
|
|
53
|
+
- `@xiaozhi-client/config` → `dist/config/index.js` (配置管理)
|
|
54
54
|
|
|
55
55
|
## 导入方式
|
|
56
56
|
|
|
@@ -62,11 +62,14 @@ import { DIContainer } from "./Container";
|
|
|
62
62
|
import { CommandRegistry } from "./commands/index";
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
###
|
|
65
|
+
### 外部依赖(使用 workspace 包)
|
|
66
66
|
|
|
67
67
|
```typescript
|
|
68
|
-
//
|
|
69
|
-
import { configManager } from "
|
|
68
|
+
// 引用配置管理(从 workspace 包导入)
|
|
69
|
+
import { configManager } from "@xiaozhi-client/config";
|
|
70
|
+
|
|
71
|
+
// 引用 WebServer(使用路径别名,运行时解析为 backend 模块)
|
|
72
|
+
import { WebServer } from "@/WebServer";
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
## 开发命令
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaozhi-client/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-beta.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"commander": "^14.0.0",
|
|
23
23
|
"consola": "^3.4.2",
|
|
24
24
|
"ora": "^8.2.0",
|
|
25
|
-
"@xiaozhi-client/config": "
|
|
25
|
+
"@xiaozhi-client/config": "2.0.0-beta.4"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^24.3.0",
|
package/src/Container.ts
CHANGED
|
@@ -166,40 +166,6 @@ export class DIContainer implements IDIContainer {
|
|
|
166
166
|
return new TemplateManagerModule.TemplateManagerImpl();
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
-
// 注册命令层(稍后在命令层实现时添加)
|
|
170
|
-
// container.register('serviceCommand', () => {
|
|
171
|
-
// const { ServiceCommand } = require('./commands/ServiceCommand.js');
|
|
172
|
-
// const serviceManager = container.get('serviceManager');
|
|
173
|
-
// const processManager = container.get('processManager');
|
|
174
|
-
// return new ServiceCommand(serviceManager, processManager);
|
|
175
|
-
// });
|
|
176
|
-
|
|
177
|
-
// container.register('configCommand', () => {
|
|
178
|
-
// const { ConfigCommand } = require('./commands/ConfigCommand.js');
|
|
179
|
-
// const configManager = container.get('configManager');
|
|
180
|
-
// const validation = container.get('validation');
|
|
181
|
-
// return new ConfigCommand(configManager, validation);
|
|
182
|
-
// });
|
|
183
|
-
|
|
184
|
-
// container.register('projectCommand', () => {
|
|
185
|
-
// const { ProjectCommand } = require('./commands/ProjectCommand.js');
|
|
186
|
-
// const templateManager = container.get('templateManager');
|
|
187
|
-
// const fileUtils = container.get('fileUtils');
|
|
188
|
-
// return new ProjectCommand(templateManager, fileUtils);
|
|
189
|
-
// });
|
|
190
|
-
|
|
191
|
-
// container.register('mcpCommand', () => {
|
|
192
|
-
// const { McpCommand } = require('./commands/McpCommand.js');
|
|
193
|
-
// return new McpCommand();
|
|
194
|
-
// });
|
|
195
|
-
|
|
196
|
-
// container.register('infoCommand', () => {
|
|
197
|
-
// const { InfoCommand } = require('./commands/InfoCommand.js');
|
|
198
|
-
// const versionUtils = container.get('versionUtils');
|
|
199
|
-
// const platformUtils = container.get('platformUtils');
|
|
200
|
-
// return new InfoCommand(versionUtils, platformUtils);
|
|
201
|
-
// });
|
|
202
|
-
|
|
203
169
|
return container;
|
|
204
170
|
}
|
|
205
171
|
}
|
package/src/commands/index.ts
CHANGED
|
@@ -23,6 +23,28 @@ export class CommandRegistry implements ICommandRegistry {
|
|
|
23
23
|
this.handlerFactory = new CommandHandlerFactory(container);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* 包装命令处理函数,统一处理错误
|
|
28
|
+
* @param handler 命令处理函数
|
|
29
|
+
* @returns 包装后的处理函数
|
|
30
|
+
*/
|
|
31
|
+
private wrapCommandHandler(
|
|
32
|
+
handler: (args: string[], options: Record<string, unknown>) => Promise<void>
|
|
33
|
+
) {
|
|
34
|
+
return async (...args: unknown[]) => {
|
|
35
|
+
try {
|
|
36
|
+
// Commander.js 传递的最后一个参数是 Command 对象,包含选项值
|
|
37
|
+
const command = args[args.length - 1] as {
|
|
38
|
+
opts: () => Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
const options = command.opts();
|
|
41
|
+
await handler(args.slice(0, -1) as string[], options);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
ErrorHandler.handle(error as Error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
/**
|
|
27
49
|
* 注册所有命令到 Commander 程序
|
|
28
50
|
*/
|
|
@@ -87,28 +109,19 @@ export class CommandRegistry implements ICommandRegistry {
|
|
|
87
109
|
}
|
|
88
110
|
|
|
89
111
|
// 设置子命令处理函数
|
|
90
|
-
cmd.action(
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
await subcommand.execute(args.slice(0, -1), options);
|
|
96
|
-
} catch (error) {
|
|
97
|
-
ErrorHandler.handle(error as Error);
|
|
98
|
-
}
|
|
99
|
-
});
|
|
112
|
+
cmd.action(
|
|
113
|
+
this.wrapCommandHandler(async (args, options) => {
|
|
114
|
+
await subcommand.execute(args, options);
|
|
115
|
+
})
|
|
116
|
+
);
|
|
100
117
|
}
|
|
101
118
|
|
|
102
119
|
// 设置主命令的默认行为
|
|
103
|
-
commandGroup.action(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} catch (error) {
|
|
109
|
-
ErrorHandler.handle(error as Error);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
120
|
+
commandGroup.action(
|
|
121
|
+
this.wrapCommandHandler(async (args, options) => {
|
|
122
|
+
await handler.execute(args, options);
|
|
123
|
+
})
|
|
124
|
+
);
|
|
112
125
|
} else {
|
|
113
126
|
// 没有子命令,注册为普通命令
|
|
114
127
|
let commandName = handler.name;
|
|
@@ -130,15 +143,11 @@ export class CommandRegistry implements ICommandRegistry {
|
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
// 设置主命令处理函数
|
|
133
|
-
command.action(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
} catch (error) {
|
|
139
|
-
ErrorHandler.handle(error as Error);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
146
|
+
command.action(
|
|
147
|
+
this.wrapCommandHandler(async (args, options) => {
|
|
148
|
+
await handler.execute(args, options);
|
|
149
|
+
})
|
|
150
|
+
);
|
|
142
151
|
}
|
|
143
152
|
}
|
|
144
153
|
|
|
@@ -208,16 +217,11 @@ export class CommandRegistry implements ICommandRegistry {
|
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
// 设置命令处理函数
|
|
211
|
-
command.action(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
await subcommand.execute(args.slice(0, -1), options);
|
|
217
|
-
} catch (error) {
|
|
218
|
-
ErrorHandler.handle(error as Error);
|
|
219
|
-
}
|
|
220
|
-
});
|
|
220
|
+
command.action(
|
|
221
|
+
this.wrapCommandHandler(async (args, options) => {
|
|
222
|
+
await subcommand.execute(args, options);
|
|
223
|
+
})
|
|
224
|
+
);
|
|
221
225
|
}
|
|
222
226
|
}
|
|
223
227
|
|
package/src/global.d.ts
CHANGED
|
@@ -154,7 +154,7 @@ export class DaemonManagerImpl implements IDaemonManager {
|
|
|
154
154
|
const tail = spawn(command, args, { stdio: "inherit" });
|
|
155
155
|
|
|
156
156
|
// 处理中断信号
|
|
157
|
-
process.
|
|
157
|
+
process.once("SIGINT", () => {
|
|
158
158
|
console.log("\n断开连接,服务继续在后台运行");
|
|
159
159
|
tail.kill();
|
|
160
160
|
process.exit(0);
|
|
@@ -153,33 +153,7 @@ export class ProcessManagerImpl implements IProcessManager {
|
|
|
153
153
|
*/
|
|
154
154
|
async gracefulKillProcess(pid: number): Promise<void> {
|
|
155
155
|
try {
|
|
156
|
-
|
|
157
|
-
process.kill(pid, "SIGTERM");
|
|
158
|
-
|
|
159
|
-
// 等待进程停止
|
|
160
|
-
let attempts = 0;
|
|
161
|
-
const maxAttempts = 30; // 3秒超时
|
|
162
|
-
|
|
163
|
-
while (attempts < maxAttempts) {
|
|
164
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
process.kill(pid, 0);
|
|
168
|
-
attempts++;
|
|
169
|
-
} catch {
|
|
170
|
-
// 进程已停止
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 如果还在运行,强制停止
|
|
176
|
-
try {
|
|
177
|
-
process.kill(pid, 0);
|
|
178
|
-
process.kill(pid, "SIGKILL");
|
|
179
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
180
|
-
} catch {
|
|
181
|
-
// 进程已停止
|
|
182
|
-
}
|
|
156
|
+
await PlatformUtils.killProcess(pid, "SIGTERM");
|
|
183
157
|
} catch (error) {
|
|
184
158
|
throw new ProcessError(
|
|
185
159
|
`无法停止进程: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -278,8 +278,8 @@ export class ServiceManagerImpl implements IServiceManager {
|
|
|
278
278
|
process.exit(0);
|
|
279
279
|
};
|
|
280
280
|
|
|
281
|
-
process.
|
|
282
|
-
process.
|
|
281
|
+
process.once("SIGINT", cleanup);
|
|
282
|
+
process.once("SIGTERM", cleanup);
|
|
283
283
|
|
|
284
284
|
await server.start();
|
|
285
285
|
}
|
|
@@ -338,8 +338,8 @@ export class ServiceManagerImpl implements IServiceManager {
|
|
|
338
338
|
process.exit(0);
|
|
339
339
|
};
|
|
340
340
|
|
|
341
|
-
process.
|
|
342
|
-
process.
|
|
341
|
+
process.once("SIGINT", cleanup);
|
|
342
|
+
process.once("SIGTERM", cleanup);
|
|
343
343
|
|
|
344
344
|
// 保存 PID 信息
|
|
345
345
|
this.processManager.savePidInfo(process.pid, "foreground");
|
|
@@ -139,43 +139,23 @@ describe("ProcessManagerImpl", () => {
|
|
|
139
139
|
});
|
|
140
140
|
|
|
141
141
|
describe("gracefulKillProcess", () => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
beforeEach(() => {
|
|
145
|
-
originalKill = process.kill;
|
|
146
|
-
process.kill = vi.fn();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
afterEach(() => {
|
|
150
|
-
process.kill = originalKill;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("should gracefully kill process", async () => {
|
|
154
|
-
let killCallCount = 0;
|
|
155
|
-
(process.kill as any).mockImplementation((pid: number, signal: any) => {
|
|
156
|
-
killCallCount++;
|
|
157
|
-
if (killCallCount > 1) {
|
|
158
|
-
throw new Error("ESRCH"); // Process stopped
|
|
159
|
-
}
|
|
160
|
-
});
|
|
142
|
+
it("should delegate to PlatformUtils with SIGTERM signal", async () => {
|
|
143
|
+
mockPlatformUtils.killProcess.mockResolvedValue();
|
|
161
144
|
|
|
162
145
|
await processManager.gracefulKillProcess(1234);
|
|
163
146
|
|
|
164
|
-
expect(
|
|
147
|
+
expect(mockPlatformUtils.killProcess).toHaveBeenCalledWith(
|
|
148
|
+
1234,
|
|
149
|
+
"SIGTERM"
|
|
150
|
+
);
|
|
165
151
|
});
|
|
166
152
|
|
|
167
|
-
it("should
|
|
168
|
-
|
|
169
|
-
if (signal === "SIGKILL") {
|
|
170
|
-
throw new Error("ESRCH"); // Process stopped
|
|
171
|
-
}
|
|
172
|
-
// Process still running for SIGTERM and signal 0
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
await processManager.gracefulKillProcess(1234);
|
|
153
|
+
it("should throw ProcessError when kill fails", async () => {
|
|
154
|
+
mockPlatformUtils.killProcess.mockRejectedValue(new Error("Kill failed"));
|
|
176
155
|
|
|
177
|
-
expect(
|
|
178
|
-
|
|
156
|
+
await expect(processManager.gracefulKillProcess(1234)).rejects.toThrow(
|
|
157
|
+
"无法停止进程"
|
|
158
|
+
);
|
|
179
159
|
});
|
|
180
160
|
});
|
|
181
161
|
|
|
@@ -74,28 +74,6 @@ vi.mock("../../utils/PathUtils.js", () => ({
|
|
|
74
74
|
},
|
|
75
75
|
}));
|
|
76
76
|
|
|
77
|
-
// Mock ConfigManager for WebServer
|
|
78
|
-
vi.mock("@/lib/config/manager.js", () => {
|
|
79
|
-
const mockConfig = {
|
|
80
|
-
mcpEndpoint: "ws://localhost:3000",
|
|
81
|
-
mcpServers: {},
|
|
82
|
-
webServer: { port: 9999 },
|
|
83
|
-
};
|
|
84
|
-
const mockConfigManager = {
|
|
85
|
-
configExists: vi.fn().mockReturnValue(true),
|
|
86
|
-
getConfig: vi.fn().mockReturnValue(mockConfig),
|
|
87
|
-
loadConfig: vi.fn().mockResolvedValue(mockConfig),
|
|
88
|
-
getToolCallLogConfig: vi.fn().mockReturnValue({ enabled: false }),
|
|
89
|
-
getMcpServers: vi.fn().mockReturnValue({}),
|
|
90
|
-
getMcpEndpoint: vi.fn().mockReturnValue("ws://localhost:3000"),
|
|
91
|
-
getConfigDir: vi.fn().mockReturnValue("/mock/config"),
|
|
92
|
-
};
|
|
93
|
-
return {
|
|
94
|
-
configManager: mockConfigManager,
|
|
95
|
-
ConfigManager: vi.fn().mockImplementation(() => mockConfigManager),
|
|
96
|
-
};
|
|
97
|
-
});
|
|
98
|
-
|
|
99
77
|
// Mock fs
|
|
100
78
|
vi.mock("node:fs", () => {
|
|
101
79
|
const mockExistsSync = vi.fn().mockReturnValue(true);
|
package/src/types/backend.d.ts
CHANGED
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Backend 模块类型声明
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* 这些声明用于 CLI 包中引用 Backend 模块的类型
|
|
5
|
+
* 避免递归解析 backend 代码,同时提供类型安全
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
/**
|
|
9
|
+
* WebServer 类型声明
|
|
10
|
+
* 对应 apps/backend/WebServer.ts
|
|
11
|
+
*/
|
|
12
|
+
declare module "@/WebServer.js" {
|
|
13
|
+
/**
|
|
14
|
+
* WebServer - Web 服务器主控制器
|
|
15
|
+
*/
|
|
16
|
+
export class WebServer {
|
|
17
|
+
/**
|
|
18
|
+
* 创建 WebServer 实例
|
|
19
|
+
* @param port - 可选的端口号,不指定则使用配置文件中的端口
|
|
20
|
+
*/
|
|
21
|
+
constructor(port?: number);
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
args?: string[];
|
|
18
|
-
headers?: Record<string, string>;
|
|
19
|
-
}
|
|
23
|
+
/**
|
|
24
|
+
* 启动 Web 服务器
|
|
25
|
+
*/
|
|
26
|
+
start(): Promise<void>;
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 停止 Web 服务器
|
|
30
|
+
*/
|
|
31
|
+
stop(): Promise<void>;
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
33
|
+
/**
|
|
34
|
+
* 销毁 WebServer 实例,清理所有资源
|
|
35
|
+
*/
|
|
36
|
+
destroy(): void;
|
|
37
|
+
}
|
|
26
38
|
}
|