@longzai-intelligence-local-server/supervisor 0.0.0-stage → 0.0.1
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 +26 -2
- package/dist/index.d.ts +279 -0
- package/dist/index.js +1 -0
- package/dist/rolldown-runtime-BqT_7tdF.js +1 -0
- package/package.json +48 -4
package/README.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @longzai-intelligence-local-server/supervisor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
桌面本地后端服务进程监督器包。在宿主进程内监督本地后端子进程的完整生命周期:绑零口保留端口、启动、健康轮询、崩溃自动重启、优雅停机与端口注册表登记注销。零 Electron 依赖,Electron 主进程与 CLI 与 TUI 与协调宿主等 Bun 运行时均可直接消费。
|
|
4
|
+
|
|
5
|
+
## 导出面
|
|
6
|
+
|
|
7
|
+
- `createLocalServerService` 工厂与 `LocalServerService` 类
|
|
8
|
+
- `resolveLocalServerConfig` 配置解析器与 `LocalServerConfig`、`ResolvedLocalServerConfig`、`LocalServerCapabilities` 配置类型
|
|
9
|
+
- `ServerOperationResult`、`ServerRuntimeStatus`、`StatusChangeCallback` 运行时类型
|
|
10
|
+
|
|
11
|
+
## 接入配方
|
|
12
|
+
|
|
13
|
+
服务名按 `<scope>-local-server` 命名,入口路径必须显式解析并经存在性校验快速失败,禁止依赖运行时相对路径:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createLocalServerService } from '@longzai-intelligence-local-server/supervisor';
|
|
17
|
+
|
|
18
|
+
const manager = createLocalServerService({
|
|
19
|
+
serviceName: 'voice-local-server',
|
|
20
|
+
entryPath: resolve(appRoot, '../server/src/desktop-entry.ts'),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
await manager.start();
|
|
24
|
+
const baseUrl = manager.getBaseUrl();
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
端口缺省零值即绑零口取系统分配口,就绪后自动登记端口注册表,消费方经 `getBaseUrl` 取真实基址。Electron 主进程需要把四通道挂到 `ipcMain` 时,另接 `@longzai-intelligence-local-server/handlers` 包。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import "./rolldown-runtime-BqT_7tdF.js";
|
|
2
|
+
/**
|
|
3
|
+
* 本地服务监督器配置定义
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 实例档能力端点声明
|
|
7
|
+
*
|
|
8
|
+
* 健康端点缺省沿用配置的 `healthPath`,MCP 端点为可选显式声明
|
|
9
|
+
*/
|
|
10
|
+
type LocalServerCapabilities = {
|
|
11
|
+
/**
|
|
12
|
+
* 健康探针端点路径
|
|
13
|
+
*
|
|
14
|
+
* @default 沿用 `healthPath` 即 `/api/health`
|
|
15
|
+
*/
|
|
16
|
+
health?: string;
|
|
17
|
+
/**
|
|
18
|
+
* MCP 工具面端点路径
|
|
19
|
+
*
|
|
20
|
+
* @default '/mcp'
|
|
21
|
+
*/
|
|
22
|
+
mcp?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 本地服务监督器配置
|
|
26
|
+
*
|
|
27
|
+
* 必填项为服务名与服务入口,入口经存在性校验快速失败,
|
|
28
|
+
* 端口缺省零值即绑零口保留惯用法取系统分配口
|
|
29
|
+
*/
|
|
30
|
+
type LocalServerConfig = {
|
|
31
|
+
/**
|
|
32
|
+
* 端口注册表服务名,如 `voice-local-server`
|
|
33
|
+
*/
|
|
34
|
+
serviceName: string;
|
|
35
|
+
/**
|
|
36
|
+
* 生产者完整仓库名,写入实例档作群级 join key
|
|
37
|
+
*/
|
|
38
|
+
producer?: string;
|
|
39
|
+
/**
|
|
40
|
+
* 实例档能力端点声明,健康端点缺省沿用 `healthPath`
|
|
41
|
+
*/
|
|
42
|
+
capabilities?: LocalServerCapabilities;
|
|
43
|
+
/**
|
|
44
|
+
* 服务入口路径,相对 `cwd` 或绝对路径均可
|
|
45
|
+
*/
|
|
46
|
+
entryPath: string;
|
|
47
|
+
/**
|
|
48
|
+
* 子进程工作目录
|
|
49
|
+
*
|
|
50
|
+
* @default 当前进程工作目录
|
|
51
|
+
*/
|
|
52
|
+
cwd?: string;
|
|
53
|
+
/**
|
|
54
|
+
* 绑定主机
|
|
55
|
+
*
|
|
56
|
+
* @default localhost
|
|
57
|
+
*/
|
|
58
|
+
host?: string;
|
|
59
|
+
/**
|
|
60
|
+
* 端口号,零值表示由系统分配
|
|
61
|
+
*
|
|
62
|
+
* @default 0
|
|
63
|
+
*/
|
|
64
|
+
port?: number;
|
|
65
|
+
/**
|
|
66
|
+
* 健康检查路径
|
|
67
|
+
*
|
|
68
|
+
* @default /api/health
|
|
69
|
+
*/
|
|
70
|
+
healthPath?: string;
|
|
71
|
+
/**
|
|
72
|
+
* 健康检查总超时毫秒数
|
|
73
|
+
*
|
|
74
|
+
* @default 30000
|
|
75
|
+
*/
|
|
76
|
+
healthTimeout?: number;
|
|
77
|
+
/**
|
|
78
|
+
* 健康检查轮询间隔毫秒数
|
|
79
|
+
*
|
|
80
|
+
* @default 500
|
|
81
|
+
*/
|
|
82
|
+
healthInterval?: number;
|
|
83
|
+
/**
|
|
84
|
+
* 崩溃后最大自动重启次数
|
|
85
|
+
*
|
|
86
|
+
* @default 3
|
|
87
|
+
*/
|
|
88
|
+
maxRestartAttempts?: number;
|
|
89
|
+
/**
|
|
90
|
+
* 优雅关闭超时毫秒数,超时后升级为强制杀死
|
|
91
|
+
*
|
|
92
|
+
* @default 5000
|
|
93
|
+
*/
|
|
94
|
+
shutdownTimeout?: number;
|
|
95
|
+
/**
|
|
96
|
+
* 子进程启动命令
|
|
97
|
+
*
|
|
98
|
+
* @default bun
|
|
99
|
+
*/
|
|
100
|
+
spawnCommand?: string;
|
|
101
|
+
/**
|
|
102
|
+
* 子进程命令参数前缀,置于入口路径之前
|
|
103
|
+
*
|
|
104
|
+
* @default ['run']
|
|
105
|
+
*/
|
|
106
|
+
spawnArgsPrefix?: string[];
|
|
107
|
+
/**
|
|
108
|
+
* 端口注册表目录,缺省走注册表自身的工作区锚定
|
|
109
|
+
*/
|
|
110
|
+
registryDir?: string;
|
|
111
|
+
/**
|
|
112
|
+
* 追加注入子进程的环境变量
|
|
113
|
+
*/
|
|
114
|
+
env?: Record<string, string>;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* 解析后的完整配置形态
|
|
118
|
+
*/
|
|
119
|
+
type ResolvedLocalServerConfig = Required<Pick<LocalServerConfig, 'serviceName' | 'entryPath' | 'host' | 'port' | 'healthPath' | 'healthTimeout' | 'healthInterval' | 'maxRestartAttempts' | 'shutdownTimeout' | 'spawnCommand' | 'spawnArgsPrefix'>> & Partial<Pick<LocalServerConfig, 'cwd' | 'registryDir' | 'env' | 'producer' | 'capabilities'>>;
|
|
120
|
+
/**
|
|
121
|
+
* 解析配置并落入默认值
|
|
122
|
+
*
|
|
123
|
+
* @param config - 使用方传入的配置
|
|
124
|
+
* @returns 填充默认值后的完整配置
|
|
125
|
+
*/
|
|
126
|
+
declare function resolveLocalServerConfig(config: LocalServerConfig): ResolvedLocalServerConfig;
|
|
127
|
+
/**
|
|
128
|
+
* 启停操作结果
|
|
129
|
+
*/
|
|
130
|
+
type ServerOperationResult = {
|
|
131
|
+
/**
|
|
132
|
+
* 操作是否成功
|
|
133
|
+
*/
|
|
134
|
+
success: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* 失败时的错误描述
|
|
137
|
+
*/
|
|
138
|
+
error?: string;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* 状态变更回调
|
|
142
|
+
*/
|
|
143
|
+
type StatusChangeCallback = (status: ServerRuntimeStatus) => void;
|
|
144
|
+
/**
|
|
145
|
+
* 本地服务监督器类
|
|
146
|
+
*/
|
|
147
|
+
declare class LocalServerService {
|
|
148
|
+
/**
|
|
149
|
+
* 模块日志器
|
|
150
|
+
*/
|
|
151
|
+
private static readonly logger;
|
|
152
|
+
/**
|
|
153
|
+
* 解析后的完整配置
|
|
154
|
+
*/
|
|
155
|
+
private readonly config;
|
|
156
|
+
/**
|
|
157
|
+
* 子进程实例
|
|
158
|
+
*/
|
|
159
|
+
private child;
|
|
160
|
+
/**
|
|
161
|
+
* 当前重启次数
|
|
162
|
+
*/
|
|
163
|
+
private restartAttempts;
|
|
164
|
+
/**
|
|
165
|
+
* 当前服务器状态
|
|
166
|
+
*/
|
|
167
|
+
private status;
|
|
168
|
+
/**
|
|
169
|
+
* 生效端口号,配置为零值时为系统分配的保留端口
|
|
170
|
+
*/
|
|
171
|
+
private activePort;
|
|
172
|
+
/**
|
|
173
|
+
* 端口注册表实例,懒初始化
|
|
174
|
+
*/
|
|
175
|
+
private portRegistry;
|
|
176
|
+
/**
|
|
177
|
+
* 状态变更回调
|
|
178
|
+
*/
|
|
179
|
+
private onStatusChange?;
|
|
180
|
+
/**
|
|
181
|
+
* 停止意图标记
|
|
182
|
+
*
|
|
183
|
+
* @remarks
|
|
184
|
+
* `stop` 发出后置真,子进程随之以信号退出时不再按崩溃处理,`start` 重新拉起时复位
|
|
185
|
+
*/
|
|
186
|
+
private stopRequested;
|
|
187
|
+
/**
|
|
188
|
+
* 创建本地服务监督器实例
|
|
189
|
+
*
|
|
190
|
+
* @param config - 监督器配置
|
|
191
|
+
*/
|
|
192
|
+
constructor(config: LocalServerConfig);
|
|
193
|
+
/**
|
|
194
|
+
* 设置状态变更回调
|
|
195
|
+
*
|
|
196
|
+
* @param callback - 状态变更回调函数
|
|
197
|
+
*/
|
|
198
|
+
setStatusCallback(callback: StatusChangeCallback): void;
|
|
199
|
+
/**
|
|
200
|
+
* 获取当前服务器状态
|
|
201
|
+
*
|
|
202
|
+
* @returns 当前服务器状态
|
|
203
|
+
*/
|
|
204
|
+
getStatus(): ServerRuntimeStatus;
|
|
205
|
+
/**
|
|
206
|
+
* 获取本地服务真实基址
|
|
207
|
+
*
|
|
208
|
+
* @returns 服务运行中的基址串,未就绪时为 null
|
|
209
|
+
*/
|
|
210
|
+
getBaseUrl(): string | null;
|
|
211
|
+
/**
|
|
212
|
+
* 启动本地服务
|
|
213
|
+
*
|
|
214
|
+
* 入口存在性校验失败或健康检查超时均返回失败结果
|
|
215
|
+
*
|
|
216
|
+
* @returns 启动结果
|
|
217
|
+
*/
|
|
218
|
+
start(): Promise<ServerOperationResult>;
|
|
219
|
+
/**
|
|
220
|
+
* 停止本地服务
|
|
221
|
+
*
|
|
222
|
+
* 先注销端口注册表,再优雅关闭子进程,超时升级为强制杀死
|
|
223
|
+
*
|
|
224
|
+
* @returns 停止结果
|
|
225
|
+
*/
|
|
226
|
+
stop(): Promise<ServerOperationResult>;
|
|
227
|
+
/**
|
|
228
|
+
* 等待健康检查通过
|
|
229
|
+
*
|
|
230
|
+
* @param port - 生效端口号
|
|
231
|
+
* @returns 是否在超时前转为健康
|
|
232
|
+
*/
|
|
233
|
+
private waitForHealth;
|
|
234
|
+
/**
|
|
235
|
+
* 处理服务器崩溃并尝试自动重启
|
|
236
|
+
*/
|
|
237
|
+
private handleCrash;
|
|
238
|
+
/**
|
|
239
|
+
* 绑零口保留一个系统分配端口
|
|
240
|
+
*
|
|
241
|
+
* @returns 系统分配的保留端口号
|
|
242
|
+
* @throws {@link Error} 当保留监听失败时抛出
|
|
243
|
+
*/
|
|
244
|
+
private reserveEphemeralPort;
|
|
245
|
+
/**
|
|
246
|
+
* 取端口注册表实例
|
|
247
|
+
*
|
|
248
|
+
* @returns 端口注册表实例
|
|
249
|
+
*/
|
|
250
|
+
private registry;
|
|
251
|
+
/**
|
|
252
|
+
* 将本地服务端口登记进端口注册表
|
|
253
|
+
*
|
|
254
|
+
* @param port - 生效端口号
|
|
255
|
+
*/
|
|
256
|
+
private registerServicePort;
|
|
257
|
+
/**
|
|
258
|
+
* 将本地服务端口从端口注册表注销
|
|
259
|
+
*/
|
|
260
|
+
private deregisterServicePort;
|
|
261
|
+
/**
|
|
262
|
+
* 更新状态并通知回调
|
|
263
|
+
*
|
|
264
|
+
* @param status - 新状态
|
|
265
|
+
*/
|
|
266
|
+
private updateStatus;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* 服务器运行时状态
|
|
270
|
+
*/
|
|
271
|
+
type ServerRuntimeStatus = 'running' | 'stopped' | 'starting' | 'error';
|
|
272
|
+
/**
|
|
273
|
+
* 创建本地服务监督器实例
|
|
274
|
+
*
|
|
275
|
+
* @param config - 监督器配置
|
|
276
|
+
* @returns 监督器实例
|
|
277
|
+
*/
|
|
278
|
+
declare function createLocalServerService(config: LocalServerConfig): LocalServerService;
|
|
279
|
+
export { type LocalServerCapabilities, type LocalServerConfig, LocalServerService, type ResolvedLocalServerConfig, type ServerOperationResult, type ServerRuntimeStatus, type StatusChangeCallback, createLocalServerService, resolveLocalServerConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./rolldown-runtime-BqT_7tdF.js";import{createNodeLogger as e}from"@longzai-intelligence-logger/node";import{createPortRegistry as t}from"@longzai-intelligence-port/registry";import{spawn as n}from"node:child_process";import{existsSync as r}from"node:fs";import*as i from"node:net";import{resolve as a}from"node:path";function o(e){return{serviceName:e.serviceName,producer:e.producer,capabilities:e.capabilities,entryPath:e.entryPath,cwd:e.cwd,host:e.host??`localhost`,port:e.port??0,healthPath:e.healthPath??`/api/health`,healthTimeout:e.healthTimeout??3e4,healthInterval:e.healthInterval??500,maxRestartAttempts:e.maxRestartAttempts??3,shutdownTimeout:e.shutdownTimeout??5e3,spawnCommand:e.spawnCommand??`bun`,spawnArgsPrefix:e.spawnArgsPrefix??[`run`],registryDir:e.registryDir,env:e.env}}var s=class s{static logger=e(`@longzai-intelligence-local-server/supervisor`);config;child=null;restartAttempts=0;status=`stopped`;activePort=null;portRegistry=null;onStatusChange;stopRequested=!1;constructor(e){this.config=o(e)}setStatusCallback(e){this.onStatusChange=e}getStatus(){return this.status}getBaseUrl(){return this.status!==`running`||this.activePort===null?null:`http://${this.config.host}:${this.activePort}`}async start(){if(this.child)return{success:!0};this.stopRequested=!1,this.updateStatus(`starting`);try{let e=a(this.config.cwd??process.cwd(),this.config.entryPath);if(!r(e))return this.updateStatus(`error`),{success:!1,error:`服务入口不存在: ${e}`};let t=this.config.port>0?this.config.port:await this.reserveEphemeralPort();return this.activePort=t,this.child=n(this.config.spawnCommand,[...this.config.spawnArgsPrefix,e],{cwd:this.config.cwd??process.cwd(),env:{...process.env,...this.config.env,HOST:this.config.host,PORT:String(t)},stdio:[`ignore`,`pipe`,`pipe`]}),this.child.on(`exit`,e=>{this.child=null,e!==0&&!this.stopRequested&&this.status!==`stopped`&&this.handleCrash()}),await this.waitForHealth(t)?(this.restartAttempts=0,await this.registerServicePort(t),this.updateStatus(`running`),{success:!0}):(await this.stop(),{success:!1,error:`Server health check failed`})}catch(e){return this.updateStatus(`error`),{success:!1,error:String(e)}}}async stop(){if(this.stopRequested=!0,!this.child)return this.updateStatus(`stopped`),await this.deregisterServicePort(),{success:!0};try{await this.deregisterServicePort(),this.child.kill(`SIGTERM`);let e=setTimeout(()=>{this.child&&this.child.kill(`SIGKILL`)},this.config.shutdownTimeout);return await new Promise(e=>{this.child?this.child.once(`exit`,()=>e()):e()}),clearTimeout(e),this.child=null,this.activePort=null,this.updateStatus(`stopped`),{success:!0}}catch(e){return this.child=null,this.activePort=null,this.updateStatus(`error`),{success:!1,error:String(e)}}}async waitForHealth(e){let t=Date.now(),n=`http://${this.config.host}:${e}${this.config.healthPath}`;for(;Date.now()-t<this.config.healthTimeout;){try{if((await fetch(n)).ok)return!0}catch{}await c(this.config.healthInterval)}return!1}async handleCrash(){this.restartAttempts>=this.config.maxRestartAttempts?(this.updateStatus(`error`),s.logger.error(`[LocalServer] Max restart attempts reached`)):(this.restartAttempts++,this.updateStatus(`starting`),s.logger.info(`[LocalServer] Restarting attempt ${this.restartAttempts}`),(await this.start()).success||this.updateStatus(`error`))}reserveEphemeralPort(){return new Promise((e,t)=>{let n=i.createServer();n.once(`error`,e=>{t(e)}),n.listen(0,this.config.host,()=>{let t=n.address(),r=typeof t==`object`&&t?t.port:0;n.close(()=>{e(r)})})})}registry(){return this.portRegistry??=t(this.config.registryDir?{registryDir:this.config.registryDir}:void 0),this.portRegistry}async registerServicePort(e){try{await this.registry().register(this.config.serviceName,{port:e,host:this.config.host,pid:this.child?.pid??process.pid,startedAt:Date.now(),producer:this.config.producer,capabilities:{health:this.config.capabilities?.health??this.config.healthPath,mcp:this.config.capabilities?.mcp}})}catch(e){s.logger.error(`[LocalServer] 端口注册失败`,e)}}async deregisterServicePort(){try{await this.registry().deregister(this.config.serviceName)}catch(e){s.logger.error(`[LocalServer] 端口注销失败`,e)}}updateStatus(e){this.status=e,this.onStatusChange?.(e)}};function c(e){return new Promise(t=>{setTimeout(t,e)})}function l(e){return new s(e)}export{s as LocalServerService,l as createLocalServerService,o as resolveLocalServerConfig};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"node:module";export{};
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@longzai-intelligence-local-server/supervisor",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Longzai Intelligence 桌面本地后端服务进程监督器包",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"desktop",
|
|
7
|
+
"local-server",
|
|
8
|
+
"longzai-intelligence",
|
|
9
|
+
"port-registry",
|
|
10
|
+
"process",
|
|
11
|
+
"supervisor"
|
|
12
|
+
],
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "lzi-builder",
|
|
26
|
+
"build:prod": "NODE_ENV=production bun run build",
|
|
27
|
+
"prepublishOnly": "bun run build:prod",
|
|
28
|
+
"dev": "lzi-builder --watch",
|
|
29
|
+
"clean": "lzi-dev-tools clean",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"test:coverage": "vitest run --coverage",
|
|
33
|
+
"lint": "lzi-lint lint",
|
|
34
|
+
"lint:fix": "lzi-lint fix",
|
|
35
|
+
"typecheck": "bun run typecheck:app && bun run typecheck:node && bun run typecheck:test",
|
|
36
|
+
"typecheck:app": "lzi-tsgo typecheck tsconfig/app.json",
|
|
37
|
+
"typecheck:node": "lzi-tsgo typecheck tsconfig/node.json",
|
|
38
|
+
"typecheck:test": "lzi-tsgo typecheck tsconfig/test.json"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@longzai-intelligence-logger/node": "^0.1.8",
|
|
42
|
+
"@longzai-intelligence-port/registry": "^0.0.5"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@longzai-intelligence-testing/utils": "^0.0.3",
|
|
46
|
+
"@longzai-intelligence-vitest/config": "^0.0.12",
|
|
47
|
+
"vitest": "^4.1.4"
|
|
48
|
+
},
|
|
49
|
+
"packageManager": "bun@1.4.3"
|
|
50
|
+
}
|