@myassis/gateway 1.0.41 → 1.0.42

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.
@@ -64,39 +64,148 @@ async function queryServiceWindows() {
64
64
  return { installed: false, running: false };
65
65
  }
66
66
  }
67
+ // ─── 用户级进程管理 ─────────────────────────────────────────
68
+ /**
69
+ * 获取 Gateway 可执行文件路径
70
+ */
71
+ function getGatewayExePath() {
72
+ if (isPackagedExe()) {
73
+ return process.execPath;
74
+ }
75
+ const nodeExec = getNodeExec();
76
+ return nodeExec;
77
+ }
78
+ /**
79
+ * 检查 Gateway 进程是否在运行
80
+ */
81
+ function isGatewayRunning() {
82
+ const pid = getStoredPid();
83
+ if (!pid)
84
+ return false;
85
+ try {
86
+ // Windows: kill signal 0 探测进程是否存在
87
+ process.kill(pid, 0);
88
+ return true;
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ /**
95
+ * 从 PID 文件读取存储的 PID
96
+ */
97
+ function getStoredPid() {
98
+ try {
99
+ if (!fs_1.default.existsSync(GATEWAY_PID_FILE))
100
+ return null;
101
+ const pid = parseInt(fs_1.default.readFileSync(GATEWAY_PID_FILE, 'utf8').trim(), 10);
102
+ return isNaN(pid) ? null : pid;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ /**
109
+ * 停止 Gateway 用户进程(优雅终止)
110
+ */
111
+ async function stopGatewayProcess() {
112
+ const pid = getStoredPid();
113
+ if (!pid)
114
+ return;
115
+ try {
116
+ process.kill(pid, 'SIGTERM');
117
+ await new Promise(r => setTimeout(r, 2000));
118
+ }
119
+ catch { /* ignore */ }
120
+ // 强制终止(如果还没退出)
121
+ try {
122
+ process.kill(pid, 'SIGKILL');
123
+ }
124
+ catch { /* ignore */ }
125
+ try {
126
+ fs_1.default.unlinkSync(GATEWAY_PID_FILE);
127
+ }
128
+ catch { /* ignore */ }
129
+ }
130
+ /**
131
+ * 写入 Gateway 启动脚本(用户级,不弹出窗口)
132
+ */
133
+ function writeGatewayLauncherScript(exePath, scriptPath, workDir) {
134
+ // 写入 BOM + UTF-8 内容
135
+ const content = [
136
+ "$ErrorActionPreference = 'Stop'",
137
+ "$exe = '" + exePath.replace(/\\/g, '\\\\') + "'",
138
+ "$workDir = '" + workDir.replace(/\\/g, '\\\\') + "'",
139
+ "$pidFile = '" + GATEWAY_PID_FILE.replace(/\\/g, '\\\\') + "'",
140
+ "$port = 19090",
141
+ "$svcPort = 19091",
142
+ "",
143
+ "# 防止重复启动(检查端口)",
144
+ "$listener = [System.Net.Sockets.TcpListener]::Start($port)",
145
+ "$listener.Stop()",
146
+ "",
147
+ "# 启动 Gateway 进程(后台、无窗口)",
148
+ "$psi = New-Object System.Diagnostics.ProcessStartInfo",
149
+ "$psi.FileName = $exe",
150
+ "$psi.WorkingDirectory = $workDir",
151
+ "$psi.Arguments = '--gateway --port ' + $port + ' --service-port ' + $svcPort",
152
+ "$psi.UseShellExecute = $false",
153
+ "$psi.RedirectStandardOutput = $true",
154
+ "$psi.RedirectStandardError = $true",
155
+ "$psi.CreateNoWindow = $true",
156
+ "$psi.Environment['NODE_ENV'] = 'production'",
157
+ "$proc = [System.Diagnostics.Process]::Start($psi)",
158
+ "if ($proc) {",
159
+ " $proc.Id.ToString() | Out-File -FilePath $pidFile -Encoding UTF8",
160
+ " $proc.WaitForExit()",
161
+ "}",
162
+ ].join('\r\n');
163
+ fs_1.default.writeFileSync(scriptPath, '\uFEFF' + content, 'utf8');
164
+ }
165
+ /**
166
+ * 检查 Run key 是否已注册
167
+ */
168
+ function isRegisteredInRunKey() {
169
+ // 通过 PowerShell 读取 HKCU Run key
170
+ try {
171
+ const { execSync } = require('child_process');
172
+ const result = execSync(`powershell -NoProfile -Command "if (Test-Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run') { (Get-ItemProperty 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run').PSObject.Properties.Name -contains '${exports.SERVICE_NAME}' } else { $false }"`, { timeout: 5000, windowsHide: true });
173
+ return result.toString().trim() === 'True';
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
179
+ /**
180
+ * 写入注册表 Run key(开机自启)
181
+ */
182
+ async function registerRunKey(scriptPath) {
183
+ const regCmd = `powershell -NoProfile -Command "Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${exports.SERVICE_NAME}' -Value '\"${scriptPath}\"' -Type String"`;
184
+ await execAsync(regCmd, { timeout: 10000, windowsHide: true });
185
+ }
186
+ /**
187
+ * 删除注册表 Run key
188
+ */
189
+ async function unregisterRunKey() {
190
+ try {
191
+ const regCmd = `powershell -NoProfile -Command "Remove-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name '${exports.SERVICE_NAME}' -ErrorAction SilentlyContinue"`;
192
+ await execAsync(regCmd, { timeout: 10000, windowsHide: true });
193
+ }
194
+ catch { /* ignore */ }
195
+ }
196
+ // ─── Windows 用户级实现 ─────────────────────────────────────
67
197
  async function installWindows() {
68
- const isPkg = isPackagedExe();
69
- const script = getServiceScript();
70
- const exe = getNodeExec();
71
- const workDir = isPkg ? path_1.default.dirname(exe) : path_1.default.dirname(script);
72
- logger.info(`isPkg=${isPkg}, exe=${exe}, script=${script}, workDir=${workDir}`);
198
+ const exe = getGatewayExePath();
199
+ const workDir = path_1.default.dirname(exe);
200
+ logger.info(`installWindows: exe=${exe}, workDir=${workDir}`);
73
201
  const { installed } = await queryServiceWindows();
74
202
  if (installed)
75
203
  await uninstallWindows();
76
- const nssmPath = getNssmPath();
77
- const useNssm = !!nssmPath && fs_1.default.existsSync(nssmPath);
78
204
  try {
79
- if (useNssm) {
80
- if (isPkg) {
81
- await execAsync(`"${nssmPath}" install ${exports.SERVICE_NAME} "${exe}"`, { timeout: 15000, cwd: workDir });
82
- }
83
- else {
84
- await execAsync(`"${nssmPath}" install ${exports.SERVICE_NAME} "${exe}" "${script}"`, { timeout: 15000, cwd: workDir });
85
- }
86
- await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} AppDirectory "${workDir}"`, { timeout: 10000 });
87
- await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} DisplayName "${exports.SERVICE_DISPLAY_NAME}"`, { timeout: 10000 });
88
- await execAsync(`"${nssmPath}" set ${exports.SERVICE_NAME} Start SERVICE_AUTO_START`, { timeout: 10000 });
89
- }
90
- else {
91
- if (isPkg) {
92
- await execAsync(`sc.exe create ${exports.SERVICE_NAME} binPath= "\\"${exe}\\"" DisplayName= "${exports.SERVICE_DISPLAY_NAME}" start= auto`, { timeout: 15000 });
93
- }
94
- else {
95
- await execAsync(`sc.exe create ${exports.SERVICE_NAME} binPath= "\\"${exe}\\" \\"${script}\\"" DisplayName= "${exports.SERVICE_DISPLAY_NAME}" start= auto`, { timeout: 15000 });
96
- }
97
- }
205
+ writeGatewayLauncherScript(exe, GATEWAY_LAUNCHER_FILE, workDir);
206
+ await registerRunKey(GATEWAY_LAUNCHER_FILE);
98
207
  await startService();
99
- return { success: true, message: '服务安装并启动成功' };
208
+ return { success: true, message: 'Gateway 安装并启动成功(用户级进程)' };
100
209
  }
101
210
  catch (err) {
102
211
  return { success: false, message: `安装失败: ${err.message}` };
@@ -105,48 +214,48 @@ async function installWindows() {
105
214
  async function uninstallWindows() {
106
215
  try {
107
216
  await stopService();
108
- const nssmPath = getNssmPath();
109
- if (nssmPath && fs_1.default.existsSync(nssmPath)) {
110
- await execAsync(`"${nssmPath}" remove ${exports.SERVICE_NAME} confirm`, { timeout: 10000 });
111
- }
112
- else {
113
- await execAsync(`sc.exe delete ${exports.SERVICE_NAME}`, { timeout: 10000 });
217
+ await unregisterRunKey();
218
+ try {
219
+ fs_1.default.unlinkSync(GATEWAY_PID_FILE);
114
220
  }
115
- return { success: true, message: '服务卸载成功' };
221
+ catch { /* ignore */ }
222
+ return { success: true, message: 'Gateway 已卸载(用户级进程)' };
116
223
  }
117
224
  catch (err) {
118
225
  return { success: false, message: `卸载失败: ${err.message}` };
119
226
  }
120
227
  }
121
228
  async function startServiceWindows() {
229
+ if (isGatewayRunning()) {
230
+ return { success: true, message: 'Gateway 已在运行' };
231
+ }
122
232
  try {
123
- await execAsync(`sc.exe start ${exports.SERVICE_NAME}`, { timeout: 10000 });
124
- return { success: true, message: '服务启动成功' };
233
+ const exe = getGatewayExePath();
234
+ const workDir = path_1.default.dirname(exe);
235
+ if (!fs_1.default.existsSync(GATEWAY_LAUNCHER_FILE)) {
236
+ writeGatewayLauncherScript(exe, GATEWAY_LAUNCHER_FILE, workDir);
237
+ }
238
+ await execAsync(`powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "${GATEWAY_LAUNCHER_FILE}"`, { timeout: 5000, windowsHide: true });
239
+ const maxWait = 5000;
240
+ const start = Date.now();
241
+ while (!isGatewayRunning() && Date.now() - start < maxWait) {
242
+ await new Promise(r => setTimeout(r, 500));
243
+ }
244
+ if (isGatewayRunning()) {
245
+ return { success: true, message: 'Gateway 启动成功' };
246
+ }
247
+ return { success: false, message: 'Gateway 进程未能启动,请检查日志' };
125
248
  }
126
249
  catch (err) {
127
250
  return { success: false, message: `启动失败: ${err.message}` };
128
251
  }
129
252
  }
130
253
  async function restartServiceWindows() {
131
- const nssmPath = getNssmPath();
132
- // 无 nssm 时需要用户手动更新
133
- if (!fs_1.default.existsSync(nssmPath)) {
134
- const info = await getServiceInfo();
135
- if (info.installed) {
136
- return {
137
- success: false,
138
- message: '检测到您尚未安装 nssm,无法自动重启服务。请手动执行以下步骤更新 Gateway:\n' +
139
- '1. 在桌面上停止 Gateway 服务(系统托盘 → 右键 → 退出)\n' +
140
- '2. 运行 `npm install -g @myassis/gateway@latest`\n' +
141
- '3. 重新启动 Gateway 服务\n\n' +
142
- '如需自动重启功能,请从 https://nssm.cc/download 下载 nssm 并放置到 Gateway 同目录下',
143
- };
144
- }
145
- // 服务未安装时,可直接更新
146
- }
147
254
  try {
148
- await execAsync(`"${nssmPath}" restart ${exports.SERVICE_NAME}`, { timeout: 15000 });
149
- return { success: true, message: '服务重启成功' };
255
+ await stopService();
256
+ await new Promise(r => setTimeout(r, 1000));
257
+ await startService();
258
+ return { success: true, message: 'Gateway 重启成功' };
150
259
  }
151
260
  catch (err) {
152
261
  return { success: false, message: `重启失败: ${err.message}` };
@@ -154,8 +263,8 @@ async function restartServiceWindows() {
154
263
  }
155
264
  async function stopServiceWindows() {
156
265
  try {
157
- await execAsync(`sc.exe stop ${exports.SERVICE_NAME}`, { timeout: 10000 });
158
- return { success: true, message: '服务停止成功' };
266
+ await stopGatewayProcess();
267
+ return { success: true, message: 'Gateway 已停止' };
159
268
  }
160
269
  catch (err) {
161
270
  return { success: false, message: `停止失败: ${err.message}` };
@@ -190,21 +299,21 @@ async function installLinux() {
190
299
  if (installed)
191
300
  await stopService();
192
301
  const execStart = isPkg ? exe : `${exe} ${script}`;
193
- const unitContent = `[Unit]
194
- Description=${exports.SERVICE_DISPLAY_NAME}
195
- After=network.target
196
-
197
- [Service]
198
- Type=simple
199
- User=${process.env.USER || 'root'}
200
- WorkingDirectory=${workDir}
201
- ExecStart=${execStart}
202
- Restart=always
203
- RestartSec=5
204
- Environment=NODE_ENV=production
205
-
206
- [Install]
207
- WantedBy=multi-user.target
302
+ const unitContent = `[Unit]
303
+ Description=${exports.SERVICE_DISPLAY_NAME}
304
+ After=network.target
305
+
306
+ [Service]
307
+ Type=simple
308
+ User=${process.env.USER || 'root'}
309
+ WorkingDirectory=${workDir}
310
+ ExecStart=${execStart}
311
+ Restart=always
312
+ RestartSec=5
313
+ Environment=NODE_ENV=production
314
+
315
+ [Install]
316
+ WantedBy=multi-user.target
208
317
  `;
209
318
  await fs_1.default.promises.writeFile('/tmp/myassis-gateway.service', unitContent, 'utf8');
210
319
  await execAsync('cp /tmp/myassis-gateway.service /etc/systemd/system/myassis-gateway.service', { timeout: 10000 });
@@ -560,6 +669,10 @@ exports.updateService = updateService;
560
669
  const HELPER_PORT = 19630;
561
670
  /** Helper 超时时间(毫秒) */
562
671
  const HELPER_TIMEOUT_MS = 60000;
672
+ /** Gateway PID 文件路径(用户级进程跟踪) */
673
+ const GATEWAY_PID_FILE = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-pid.txt');
674
+ /** Gateway 启动脚本路径 */
675
+ const GATEWAY_LAUNCHER_FILE = path_1.default.join(os_1.default.tmpdir(), 'myassis-gateway-launcher.ps1');
563
676
  /** 当前活跃的 Helper 进程 */
564
677
  let helperProcess = null;
565
678
  /** Helper 连接 socket */
@@ -642,7 +755,19 @@ async function execAsUser(command, cwd) {
642
755
  if (!helperReady || !helperSocket) {
643
756
  const started = await spawnHelperAsUser();
644
757
  if (!started) {
645
- return { stdout: '', stderr: 'Failed to start helper process', exitCode: -1 };
758
+ // Helper 启动失败,降级到直接 exec(exit=-2 表示降级执行)
759
+ logger.warn('Helper spawn failed, falling back to direct exec (exit=-2 = fallback used)');
760
+ try {
761
+ const { stdout, stderr } = await execAsync(command, {
762
+ cwd: cwd || os_1.default.homedir(),
763
+ timeout: HELPER_TIMEOUT_MS,
764
+ windowsHide: true,
765
+ });
766
+ return { stdout, stderr, exitCode: 0 };
767
+ }
768
+ catch (err) {
769
+ return { stdout: '', stderr: err?.message || String(err), exitCode: -2 };
770
+ }
646
771
  }
647
772
  }
648
773
  const id = newCmdId();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myassis/gateway",
3
- "version": "1.0.41",
3
+ "version": "1.0.42",
4
4
  "description": "我的助手 Gateway Service - 本地 AI 网关服务,支持认证、WebSocket 实时通信和任务调度",
5
5
  "main": "dist/index.js",
6
6
  "bin": {