@wu529778790/open-im 1.5.5-beta.2 → 1.5.5-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/dist/cli.js CHANGED
@@ -2,8 +2,9 @@
2
2
  import { main, needsSetup, runInteractiveSetup } from "./index.js";
3
3
  import { loadConfig } from "./config.js";
4
4
  import { checkAndUpdate } from "./check-update.js";
5
- import { runWebConfigFlow } from "./config-web.js";
6
- import { getServiceStatus, removePid, startBackgroundService, stopBackgroundService } from "./service-control.js";
5
+ import { getWebConfigUrl, openWebConfigUrl, runWebConfigFlow } from "./config-web.js";
6
+ import { getManagerStatus, startManagerProcess, stopManagerProcess } from "./manager-control.js";
7
+ import { stopBackgroundService } from "./service-control.js";
7
8
  async function ensureConfigured(mode) {
8
9
  const forceWeb = process.env.OPEN_IM_FORCE_WEB === "1";
9
10
  if (mode !== "init" && !needsSetup()) {
@@ -30,76 +31,83 @@ async function ensureConfigured(mode) {
30
31
  return false;
31
32
  }
32
33
  }
33
- // ============================================================================
34
- // 命令处理
35
- // ============================================================================
36
34
  async function cmdStart() {
37
- const status = getServiceStatus();
35
+ const status = getManagerStatus();
38
36
  if (status.running && status.pid) {
39
- console.log("\n🟢 open-im 已在后台运行");
40
- console.log(` pid: ${status.pid}`);
37
+ console.log("\nopen-im is already running in the background.");
38
+ console.log(` pid: ${status.pid}`);
39
+ console.log(` config page: ${getWebConfigUrl()}`);
41
40
  return;
42
41
  }
43
- removePid();
44
42
  if (!(await ensureConfigured("start"))) {
45
43
  process.exit(1);
46
44
  }
47
- // 检查并自动更新到最新版本
48
45
  const { updated } = await checkAndUpdate();
49
46
  if (updated) {
50
47
  process.exit(0);
51
48
  }
52
- const child = startBackgroundService(process.cwd());
53
- console.log("\n🟢 open-im 已在后台启动");
54
- console.log(` pid: ${child.pid}`);
49
+ process.env.OPEN_IM_AUTO_OPEN_CONFIG_ONCE = "1";
50
+ try {
51
+ const child = await startManagerProcess(process.cwd());
52
+ console.log("\nopen-im started in the background.");
53
+ console.log(` pid: ${child.pid}`);
54
+ console.log(` config page: ${getWebConfigUrl()}`);
55
+ }
56
+ finally {
57
+ delete process.env.OPEN_IM_AUTO_OPEN_CONFIG_ONCE;
58
+ }
55
59
  }
56
60
  async function cmdStop() {
57
- const status = getServiceStatus();
61
+ const status = getManagerStatus();
58
62
  if (!status.pid) {
59
- console.log("open-im 未在后台运行");
63
+ console.log("open-im is not running in the background.");
60
64
  return;
61
65
  }
62
- const result = await stopBackgroundService();
63
- console.log("\n🔴 open-im 已停止");
64
- console.log(` pid: ${result.pid}`);
66
+ await stopBackgroundService();
67
+ const result = await stopManagerProcess();
68
+ console.log("\nopen-im stopped.");
69
+ console.log(` pid: ${result.pid}`);
65
70
  }
66
71
  async function cmdInit() {
67
- console.log("\n━━━ open-im 本地控制台 ━━━\n");
72
+ console.log("\nopen-im local control\n");
73
+ const status = getManagerStatus();
74
+ if (status.running && status.pid) {
75
+ openWebConfigUrl();
76
+ console.log(`Config page is already running: ${getWebConfigUrl()}`);
77
+ return;
78
+ }
68
79
  const saved = await ensureConfigured("init");
69
80
  if (!saved) {
70
- console.log("\n❌ 配置未完成,已取消。");
81
+ console.log("\nConfiguration was not completed.");
71
82
  process.exit(1);
72
83
  }
73
- console.log("\n✅ 配置完成!");
74
- console.log("\n现在可以运行以下命令启动服务:");
84
+ console.log("\nConfiguration saved.");
85
+ console.log("\nYou can start the app with:");
75
86
  console.log(" open-im start");
76
87
  console.log(" open-im dev");
77
88
  }
78
89
  async function cmdDev() {
79
90
  if (!(await ensureConfigured("dev"))) {
80
- console.log("配置未完成,已取消启动。");
91
+ console.log("Configuration was not completed.");
81
92
  process.exit(1);
82
93
  }
83
94
  await main();
84
95
  }
85
96
  function showHelp(exitCode = 0) {
86
97
  console.log(`
87
- 用法: open-im <command>
98
+ Usage: open-im <command>
88
99
 
89
- 命令:
90
- start 后台运行服务
91
- stop 停止后台服务
92
- init 打开本地 Web 配置页
93
- dev 前台运行(调试模式),Ctrl+C 停止
100
+ Commands:
101
+ start Run the full app in the background
102
+ stop Stop the full app
103
+ init Open the local web configuration page
104
+ dev Run in the foreground for debugging
94
105
 
95
- 选项:
96
- -h, --help 显示此帮助信息
106
+ Options:
107
+ -h, --help Show this help message
97
108
  `);
98
109
  process.exit(exitCode);
99
110
  }
100
- // ============================================================================
101
- // 命令路由
102
- // ============================================================================
103
111
  const cmd = process.argv[2];
104
112
  const commands = {
105
113
  start: cmdStart,
@@ -123,6 +131,6 @@ else if (commands[cmd]) {
123
131
  });
124
132
  }
125
133
  else {
126
- console.error(`未知命令: ${cmd}`);
134
+ console.error(`Unknown command: ${cmd}`);
127
135
  showHelp(1);
128
136
  }
@@ -5,9 +5,13 @@ export interface StartedWebConfigServer {
5
5
  url: string;
6
6
  waitForResult: Promise<WebFlowResult>;
7
7
  }
8
+ export declare function getWebConfigPort(): number;
9
+ export declare function getWebConfigUrl(): string;
10
+ export declare function openWebConfigUrl(): void;
8
11
  export declare function startWebConfigServer(options: {
9
12
  mode: WebFlowMode;
10
13
  cwd: string;
14
+ persistent?: boolean;
11
15
  }): Promise<StartedWebConfigServer>;
12
16
  export declare function runWebConfigFlow(options: {
13
17
  mode: WebFlowMode;
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createServer } from "node:http";
3
3
  import { URL } from "node:url";
4
+ import { WEB_CONFIG_PORT } from "./constants.js";
4
5
  import { CONFIG_PATH, loadConfig, loadFileConfig, saveFileConfig } from "./config.js";
5
6
  import { getServiceStatus, startBackgroundService, stopBackgroundService } from "./service-control.js";
6
7
  function splitCsv(value) {
@@ -72,7 +73,7 @@ function buildInitialPayload(file) {
72
73
  defaultPermissionMode: file.defaultPermissionMode ?? "ask",
73
74
  hookPort: file.hookPort ?? 35801,
74
75
  logDir: file.logDir ?? "",
75
- logLevel: file.logLevel ?? "INFO",
76
+ logLevel: file.logLevel ?? "default",
76
77
  useSdkMode: file.useSdkMode ?? true,
77
78
  },
78
79
  };
@@ -108,10 +109,10 @@ function toFileConfig(payload, existing) {
108
109
  return {
109
110
  ...existing,
110
111
  aiCommand: payload.ai.aiCommand,
111
- defaultPermissionMode: payload.ai.defaultPermissionMode,
112
+ defaultPermissionMode: payload.ai.defaultPermissionMode ?? existing.defaultPermissionMode ?? "ask",
112
113
  hookPort: payload.ai.hookPort,
113
- logDir: clean(payload.ai.logDir),
114
- logLevel: payload.ai.logLevel,
114
+ logDir: payload.ai.logDir === undefined ? existing.logDir : clean(payload.ai.logDir),
115
+ logLevel: payload.ai.logLevel === "default" ? undefined : payload.ai.logLevel,
115
116
  useSdkMode: payload.ai.useSdkMode,
116
117
  tools: {
117
118
  claude: {
@@ -183,6 +184,16 @@ function openBrowser(url) {
183
184
  }
184
185
  spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
185
186
  }
187
+ export function getWebConfigPort() {
188
+ const fromEnv = process.env.OPEN_IM_WEB_PORT ? parseInt(process.env.OPEN_IM_WEB_PORT, 10) : NaN;
189
+ return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : WEB_CONFIG_PORT;
190
+ }
191
+ export function getWebConfigUrl() {
192
+ return `http://127.0.0.1:${getWebConfigPort()}`;
193
+ }
194
+ export function openWebConfigUrl() {
195
+ openBrowser(getWebConfigUrl());
196
+ }
186
197
  const PAGE_HTML = String.raw `<!doctype html>
187
198
  <html lang="en">
188
199
  <head>
@@ -264,10 +275,8 @@ const PAGE_HTML = String.raw `<!doctype html>
264
275
  <label>Codex proxy<input id="ai-codexProxy" class="mono" placeholder="Optional" /></label>
265
276
  <label>Claude timeout (ms)<input id="ai-claudeTimeoutMs" type="number" min="1" /></label>
266
277
  <label>Claude model<input id="ai-claudeModel" placeholder="Optional" /></label>
267
- <label>Permission mode<select id="ai-defaultPermissionMode"><option value="ask">ask</option><option value="accept-edits">accept-edits</option><option value="plan">plan</option><option value="yolo">yolo</option></select></label>
268
278
  <label>Hook port<input id="ai-hookPort" type="number" min="1" /></label>
269
- <label>Log directory<input id="ai-logDir" class="mono" /></label>
270
- <label>Log level<select id="ai-logLevel"><option value="DEBUG">DEBUG</option><option value="INFO">INFO</option><option value="WARN">WARN</option><option value="ERROR">ERROR</option></select></label>
279
+ <label>Log level<select id="ai-logLevel"><option value="default">default</option><option value="DEBUG">DEBUG</option><option value="INFO">INFO</option><option value="WARN">WARN</option><option value="ERROR">ERROR</option></select></label>
271
280
  </div>
272
281
  <div class="actions" style="margin-top:14px">
273
282
  <label class="toggle"><input id="ai-claudeSkipPermissions" type="checkbox" /> Auto-approve tool permissions</label>
@@ -279,15 +288,15 @@ const PAGE_HTML = String.raw `<!doctype html>
279
288
  <div class="actions">
280
289
  <button id="validateButton" class="warning">Validate</button>
281
290
  <button id="saveButton" class="secondary">Save config</button>
282
- <button id="startButton">Start service</button>
283
- <button id="stopButton" class="danger">Stop service</button>
291
+ <button id="startButton">Start bridge</button>
292
+ <button id="stopButton" class="danger">Stop bridge</button>
284
293
  </div>
285
294
  <div class="message" id="message"></div>
286
295
  </section>
287
296
  </div>
288
297
  </div>
289
298
  <script>
290
- const ids = ["telegram-enabled","telegram-botToken","telegram-proxy","telegram-allowedUserIds","feishu-enabled","feishu-appId","feishu-appSecret","feishu-allowedUserIds","wework-enabled","wework-corpId","wework-secret","wework-allowedUserIds","dingtalk-enabled","dingtalk-clientId","dingtalk-clientSecret","dingtalk-cardTemplateId","dingtalk-allowedUserIds","ai-aiCommand","ai-claudeCliPath","ai-claudeWorkDir","ai-claudeSkipPermissions","ai-claudeTimeoutMs","ai-claudeModel","ai-cursorCliPath","ai-codexCliPath","ai-codexProxy","ai-defaultPermissionMode","ai-hookPort","ai-logDir","ai-logLevel","ai-useSdkMode"];
299
+ const ids = ["telegram-enabled","telegram-botToken","telegram-proxy","telegram-allowedUserIds","feishu-enabled","feishu-appId","feishu-appSecret","feishu-allowedUserIds","wework-enabled","wework-corpId","wework-secret","wework-allowedUserIds","dingtalk-enabled","dingtalk-clientId","dingtalk-clientSecret","dingtalk-cardTemplateId","dingtalk-allowedUserIds","ai-aiCommand","ai-claudeCliPath","ai-claudeWorkDir","ai-claudeSkipPermissions","ai-claudeTimeoutMs","ai-claudeModel","ai-cursorCliPath","ai-codexCliPath","ai-codexProxy","ai-hookPort","ai-logLevel","ai-useSdkMode"];
291
300
  const el = (id) => document.getElementById(id);
292
301
  const setMessage = (text, type="") => { const node = el("message"); node.textContent = text; node.className = ("message " + type).trim(); };
293
302
  const setBusy = (busy) => ["validateButton","saveButton","startButton","stopButton"].forEach((id) => { el(id).disabled = busy; });
@@ -303,10 +312,10 @@ const PAGE_HTML = String.raw `<!doctype html>
303
312
  ? ("Enabled platforms: " + enabled.join(", ") + " | AI tool: " + aiTool)
304
313
  : ("No platform enabled yet | AI tool: " + aiTool);
305
314
  }
306
- const payload = () => ({ platforms: { telegram: { enabled: el("telegram-enabled").checked, botToken: el("telegram-botToken").value, proxy: el("telegram-proxy").value, allowedUserIds: el("telegram-allowedUserIds").value }, feishu: { enabled: el("feishu-enabled").checked, appId: el("feishu-appId").value, appSecret: el("feishu-appSecret").value, allowedUserIds: el("feishu-allowedUserIds").value }, wework: { enabled: el("wework-enabled").checked, corpId: el("wework-corpId").value, secret: el("wework-secret").value, allowedUserIds: el("wework-allowedUserIds").value }, dingtalk: { enabled: el("dingtalk-enabled").checked, clientId: el("dingtalk-clientId").value, clientSecret: el("dingtalk-clientSecret").value, cardTemplateId: el("dingtalk-cardTemplateId").value, allowedUserIds: el("dingtalk-allowedUserIds").value } }, ai: { aiCommand: el("ai-aiCommand").value, claudeCliPath: el("ai-claudeCliPath").value, claudeWorkDir: el("ai-claudeWorkDir").value, claudeSkipPermissions: el("ai-claudeSkipPermissions").checked, claudeTimeoutMs: Number(el("ai-claudeTimeoutMs").value || "0"), claudeModel: el("ai-claudeModel").value, cursorCliPath: el("ai-cursorCliPath").value, codexCliPath: el("ai-codexCliPath").value, codexProxy: el("ai-codexProxy").value, defaultPermissionMode: el("ai-defaultPermissionMode").value, hookPort: Number(el("ai-hookPort").value || "0"), logDir: el("ai-logDir").value, logLevel: el("ai-logLevel").value, useSdkMode: el("ai-useSdkMode").checked } });
315
+ const payload = () => ({ platforms: { telegram: { enabled: el("telegram-enabled").checked, botToken: el("telegram-botToken").value, proxy: el("telegram-proxy").value, allowedUserIds: el("telegram-allowedUserIds").value }, feishu: { enabled: el("feishu-enabled").checked, appId: el("feishu-appId").value, appSecret: el("feishu-appSecret").value, allowedUserIds: el("feishu-allowedUserIds").value }, wework: { enabled: el("wework-enabled").checked, corpId: el("wework-corpId").value, secret: el("wework-secret").value, allowedUserIds: el("wework-allowedUserIds").value }, dingtalk: { enabled: el("dingtalk-enabled").checked, clientId: el("dingtalk-clientId").value, clientSecret: el("dingtalk-clientSecret").value, cardTemplateId: el("dingtalk-cardTemplateId").value, allowedUserIds: el("dingtalk-allowedUserIds").value } }, ai: { aiCommand: el("ai-aiCommand").value, claudeCliPath: el("ai-claudeCliPath").value, claudeWorkDir: el("ai-claudeWorkDir").value, claudeSkipPermissions: el("ai-claudeSkipPermissions").checked, claudeTimeoutMs: Number(el("ai-claudeTimeoutMs").value || "0"), claudeModel: el("ai-claudeModel").value, cursorCliPath: el("ai-cursorCliPath").value, codexCliPath: el("ai-codexCliPath").value, codexProxy: el("ai-codexProxy").value, hookPort: Number(el("ai-hookPort").value || "0"), logLevel: el("ai-logLevel").value, useSdkMode: el("ai-useSdkMode").checked } });
307
316
  async function request(path, options={}) { const response = await fetch(path, { headers: { "content-type": "application/json" }, ...options }); const body = await response.json(); if (!response.ok) throw new Error(body.error || "Request failed"); return body; }
308
- function fill(data, meta) { el("configPath").textContent = meta.configPath; el("modeBadge").textContent = "Flow: " + meta.mode; el("telegram-enabled").checked = data.platforms.telegram.enabled; el("telegram-botToken").value = data.platforms.telegram.botToken; el("telegram-proxy").value = data.platforms.telegram.proxy; el("telegram-allowedUserIds").value = data.platforms.telegram.allowedUserIds; el("feishu-enabled").checked = data.platforms.feishu.enabled; el("feishu-appId").value = data.platforms.feishu.appId; el("feishu-appSecret").value = data.platforms.feishu.appSecret; el("feishu-allowedUserIds").value = data.platforms.feishu.allowedUserIds; el("wework-enabled").checked = data.platforms.wework.enabled; el("wework-corpId").value = data.platforms.wework.corpId; el("wework-secret").value = data.platforms.wework.secret; el("wework-allowedUserIds").value = data.platforms.wework.allowedUserIds; el("dingtalk-enabled").checked = data.platforms.dingtalk.enabled; el("dingtalk-clientId").value = data.platforms.dingtalk.clientId; el("dingtalk-clientSecret").value = data.platforms.dingtalk.clientSecret; el("dingtalk-cardTemplateId").value = data.platforms.dingtalk.cardTemplateId; el("dingtalk-allowedUserIds").value = data.platforms.dingtalk.allowedUserIds; el("ai-aiCommand").value = data.ai.aiCommand; el("ai-claudeCliPath").value = data.ai.claudeCliPath; el("ai-claudeWorkDir").value = data.ai.claudeWorkDir; el("ai-claudeSkipPermissions").checked = data.ai.claudeSkipPermissions; el("ai-claudeTimeoutMs").value = String(data.ai.claudeTimeoutMs); el("ai-claudeModel").value = data.ai.claudeModel; el("ai-cursorCliPath").value = data.ai.cursorCliPath; el("ai-codexCliPath").value = data.ai.codexCliPath; el("ai-codexProxy").value = data.ai.codexProxy; el("ai-defaultPermissionMode").value = data.ai.defaultPermissionMode; el("ai-hookPort").value = String(data.ai.hookPort); el("ai-logDir").value = data.ai.logDir; el("ai-logLevel").value = data.ai.logLevel; el("ai-useSdkMode").checked = data.ai.useSdkMode; updateVisualState(); }
309
- async function refreshStatus() { const data = await request("/api/service/status"); el("serviceState").textContent = data.running ? ("Service running (pid " + data.pid + ")") : "Service stopped"; el("statusMeta").textContent = data.running ? "Background bridge process is active." : "No background bridge process is active."; }
317
+ function fill(data, meta) { el("configPath").textContent = meta.configPath; el("modeBadge").textContent = "Flow: " + meta.mode; el("telegram-enabled").checked = data.platforms.telegram.enabled; el("telegram-botToken").value = data.platforms.telegram.botToken; el("telegram-proxy").value = data.platforms.telegram.proxy; el("telegram-allowedUserIds").value = data.platforms.telegram.allowedUserIds; el("feishu-enabled").checked = data.platforms.feishu.enabled; el("feishu-appId").value = data.platforms.feishu.appId; el("feishu-appSecret").value = data.platforms.feishu.appSecret; el("feishu-allowedUserIds").value = data.platforms.feishu.allowedUserIds; el("wework-enabled").checked = data.platforms.wework.enabled; el("wework-corpId").value = data.platforms.wework.corpId; el("wework-secret").value = data.platforms.wework.secret; el("wework-allowedUserIds").value = data.platforms.wework.allowedUserIds; el("dingtalk-enabled").checked = data.platforms.dingtalk.enabled; el("dingtalk-clientId").value = data.platforms.dingtalk.clientId; el("dingtalk-clientSecret").value = data.platforms.dingtalk.clientSecret; el("dingtalk-cardTemplateId").value = data.platforms.dingtalk.cardTemplateId; el("dingtalk-allowedUserIds").value = data.platforms.dingtalk.allowedUserIds; el("ai-aiCommand").value = data.ai.aiCommand; el("ai-claudeCliPath").value = data.ai.claudeCliPath; el("ai-claudeWorkDir").value = data.ai.claudeWorkDir; el("ai-claudeSkipPermissions").checked = data.ai.claudeSkipPermissions; el("ai-claudeTimeoutMs").value = String(data.ai.claudeTimeoutMs); el("ai-claudeModel").value = data.ai.claudeModel; el("ai-cursorCliPath").value = data.ai.cursorCliPath; el("ai-codexCliPath").value = data.ai.codexCliPath; el("ai-codexProxy").value = data.ai.codexProxy; el("ai-hookPort").value = String(data.ai.hookPort); el("ai-logLevel").value = data.ai.logLevel; el("ai-useSdkMode").checked = data.ai.useSdkMode; updateVisualState(); }
318
+ async function refreshStatus() { const data = await request("/api/service/status"); el("serviceState").textContent = data.running ? ("Bridge running (pid " + data.pid + ")") : "Bridge stopped"; el("statusMeta").textContent = data.running ? "Bridge worker is active." : "Bridge worker is currently stopped."; }
310
319
  async function boot() { setBusy(true); try { const data = await request("/api/config"); fill(data.payload, data.meta); await refreshStatus(); setMessage("Control surface ready.", "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } setInterval(() => { refreshStatus().catch(() => {}); }, 5000); ids.forEach((id) => { const node = el(id); if (node) node.addEventListener("input", updateVisualState); if (node) node.addEventListener("change", updateVisualState); }); }
311
320
  async function validate() { setBusy(true); try { const data = await request("/api/config/validate", { method: "POST", body: JSON.stringify(payload()) }); setMessage(data.message, "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } }
312
321
  async function save() { setBusy(true); try { const data = await request("/api/config/save?final=1", { method: "POST", body: JSON.stringify(payload()) }); setMessage(data.message, "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } }
@@ -374,7 +383,7 @@ export async function startWebConfigServer(options) {
374
383
  saveFileConfig(toFileConfig(body, loadFileConfig()));
375
384
  loadConfig();
376
385
  json(response, 200, { message: "Configuration saved." });
377
- if (requestUrl.searchParams.get("final") === "1") {
386
+ if (!options.persistent && requestUrl.searchParams.get("final") === "1") {
378
387
  setTimeout(() => finishFlow("saved"), 120);
379
388
  }
380
389
  }
@@ -391,8 +400,10 @@ export async function startWebConfigServer(options) {
391
400
  try {
392
401
  loadConfig();
393
402
  const started = startBackgroundService(options.cwd);
394
- json(response, 200, { message: `Background service started with pid ${started.pid}.`, pid: started.pid });
395
- setTimeout(() => finishFlow("saved"), 120);
403
+ json(response, 200, { message: `Bridge started with pid ${started.pid}.`, pid: started.pid });
404
+ if (!options.persistent) {
405
+ setTimeout(() => finishFlow("saved"), 120);
406
+ }
396
407
  }
397
408
  catch (error) {
398
409
  json(response, 400, { error: error instanceof Error ? error.message : String(error) });
@@ -402,7 +413,7 @@ export async function startWebConfigServer(options) {
402
413
  if (request.method === "POST" && requestUrl.pathname === "/api/service/stop") {
403
414
  try {
404
415
  const result = await stopBackgroundService();
405
- json(response, 200, { message: result.pid ? `Background service stopped (pid ${result.pid}).` : "No background service was running." });
416
+ json(response, 200, { message: result.pid ? `Bridge stopped (pid ${result.pid}).` : "Bridge was already stopped." });
406
417
  }
407
418
  catch (error) {
408
419
  json(response, 400, { error: error instanceof Error ? error.message : String(error) });
@@ -411,8 +422,16 @@ export async function startWebConfigServer(options) {
411
422
  }
412
423
  json(response, 404, { error: "Not found." });
413
424
  });
414
- await new Promise((resolve) => {
415
- server.listen(0, "127.0.0.1", () => resolve());
425
+ const port = getWebConfigPort();
426
+ await new Promise((resolve, reject) => {
427
+ server.once("error", (error) => {
428
+ if (error.code === "EADDRINUSE") {
429
+ reject(new Error(`Web config port ${port} is already in use. Close the existing listener or change OPEN_IM_WEB_PORT.`));
430
+ return;
431
+ }
432
+ reject(error);
433
+ });
434
+ server.listen(port, "127.0.0.1", () => resolve());
416
435
  });
417
436
  const address = server.address();
418
437
  if (!address || typeof address === "string") {
@@ -424,10 +443,12 @@ export async function startWebConfigServer(options) {
424
443
  waitForResult,
425
444
  };
426
445
  }
427
- timer = setTimeout(() => {
428
- server.close();
429
- settle("cancel");
430
- }, 15 * 60 * 1000);
446
+ if (!options.persistent) {
447
+ timer = setTimeout(() => {
448
+ server.close();
449
+ settle("cancel");
450
+ }, 15 * 60 * 1000);
451
+ }
431
452
  server.on("close", () => {
432
453
  if (timer)
433
454
  clearTimeout(timer);
@@ -439,7 +460,7 @@ export async function startWebConfigServer(options) {
439
460
  server.close();
440
461
  settle("cancel");
441
462
  },
442
- url: `http://127.0.0.1:${address.port}`,
463
+ url: `http://127.0.0.1:${port}`,
443
464
  waitForResult,
444
465
  };
445
466
  }
@@ -1,6 +1,8 @@
1
1
  export declare const APP_HOME: string;
2
2
  /** 优雅关闭 HTTP 端口(stop 命令通过此端口触发 shutdown) */
3
3
  export declare const SHUTDOWN_PORT = 39281;
4
+ /** 本地 Web 配置页固定端口 */
5
+ export declare const WEB_CONFIG_PORT = 39282;
4
6
  export declare const IMAGE_DIR: string;
5
7
  export declare const TERMINAL_ONLY_COMMANDS: Set<string>;
6
8
  /** CardKit 流式更新节流:80ms(约 12 次/秒,cardElement.content 专为打字机设计,支持更高频率) */
package/dist/constants.js CHANGED
@@ -3,6 +3,8 @@ import { homedir, tmpdir } from "node:os";
3
3
  export const APP_HOME = join(homedir(), ".open-im");
4
4
  /** 优雅关闭 HTTP 端口(stop 命令通过此端口触发 shutdown) */
5
5
  export const SHUTDOWN_PORT = 39281;
6
+ /** 本地 Web 配置页固定端口 */
7
+ export const WEB_CONFIG_PORT = 39282;
6
8
  export const IMAGE_DIR = join(tmpdir(), "open-im-images");
7
9
  export const TERMINAL_ONLY_COMMANDS = new Set([
8
10
  "/context",
@@ -0,0 +1,17 @@
1
+ export declare function getManagerPid(): number | null;
2
+ export declare function removeManagerPid(): void;
3
+ export declare function removeManagerReady(): void;
4
+ export declare function writeManagerReady(): void;
5
+ export declare function isManagerReady(): boolean;
6
+ export declare function writeManagerPid(pid: number): void;
7
+ export declare function getManagerStatus(): {
8
+ running: boolean;
9
+ pid: number | null;
10
+ };
11
+ export declare function startManagerProcess(cwd: string): Promise<{
12
+ pid: number;
13
+ }>;
14
+ export declare function stopManagerProcess(): Promise<{
15
+ pid: number | null;
16
+ stopped: boolean;
17
+ }>;
@@ -0,0 +1,160 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { dirname, extname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { APP_HOME } from "./constants.js";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const PID_FILE = join(APP_HOME, "open-im.pid");
8
+ const READY_FILE = join(APP_HOME, "open-im.ready");
9
+ function getManagerEntry() {
10
+ const extension = extname(fileURLToPath(import.meta.url));
11
+ if (extension === ".ts") {
12
+ return {
13
+ command: process.execPath,
14
+ args: ["--import", "tsx", join(__dirname, "manager.ts")],
15
+ };
16
+ }
17
+ return {
18
+ command: process.execPath,
19
+ args: [join(__dirname, "manager.js")],
20
+ };
21
+ }
22
+ function isRunning(pid) {
23
+ try {
24
+ if (process.platform === "win32") {
25
+ const result = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/NH"], {
26
+ stdio: "pipe",
27
+ windowsHide: true,
28
+ }).toString();
29
+ return result.includes(String(pid));
30
+ }
31
+ process.kill(pid, 0);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ export function getManagerPid() {
39
+ if (!existsSync(PID_FILE))
40
+ return null;
41
+ try {
42
+ const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
43
+ return Number.isNaN(pid) ? null : pid;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ export function removeManagerPid() {
50
+ try {
51
+ if (existsSync(PID_FILE))
52
+ unlinkSync(PID_FILE);
53
+ }
54
+ catch {
55
+ /* ignore */
56
+ }
57
+ }
58
+ export function removeManagerReady() {
59
+ try {
60
+ if (existsSync(READY_FILE))
61
+ unlinkSync(READY_FILE);
62
+ }
63
+ catch {
64
+ /* ignore */
65
+ }
66
+ }
67
+ export function writeManagerReady() {
68
+ if (!existsSync(APP_HOME))
69
+ mkdirSync(APP_HOME, { recursive: true });
70
+ writeFileSync(READY_FILE, "1", "utf-8");
71
+ }
72
+ export function isManagerReady() {
73
+ return existsSync(READY_FILE);
74
+ }
75
+ export function writeManagerPid(pid) {
76
+ if (!existsSync(APP_HOME))
77
+ mkdirSync(APP_HOME, { recursive: true });
78
+ writeFileSync(PID_FILE, String(pid), "utf-8");
79
+ }
80
+ export function getManagerStatus() {
81
+ const pid = getManagerPid();
82
+ if (!pid)
83
+ return { running: false, pid: null };
84
+ if (!isRunning(pid)) {
85
+ removeManagerReady();
86
+ removeManagerPid();
87
+ return { running: false, pid: null };
88
+ }
89
+ return { running: true, pid };
90
+ }
91
+ export async function startManagerProcess(cwd) {
92
+ const current = getManagerStatus();
93
+ if (current.running && current.pid) {
94
+ if (isManagerReady()) {
95
+ return { pid: current.pid };
96
+ }
97
+ throw new Error("Manager process exists but is not ready yet.");
98
+ }
99
+ removeManagerReady();
100
+ removeManagerPid();
101
+ const entry = getManagerEntry();
102
+ const child = spawn(entry.command, entry.args, {
103
+ detached: true,
104
+ stdio: "ignore",
105
+ cwd,
106
+ env: process.env,
107
+ windowsHide: process.platform === "win32",
108
+ });
109
+ child.unref();
110
+ if (!child.pid) {
111
+ throw new Error("Failed to start manager process.");
112
+ }
113
+ writeManagerPid(child.pid);
114
+ const deadline = Date.now() + 8000;
115
+ while (Date.now() < deadline) {
116
+ await new Promise((resolve) => setTimeout(resolve, 100));
117
+ if (!isRunning(child.pid)) {
118
+ removeManagerReady();
119
+ removeManagerPid();
120
+ throw new Error("Manager process exited before becoming ready.");
121
+ }
122
+ if (isManagerReady()) {
123
+ return { pid: child.pid };
124
+ }
125
+ }
126
+ removeManagerReady();
127
+ removeManagerPid();
128
+ try {
129
+ process.kill(child.pid, "SIGTERM");
130
+ }
131
+ catch {
132
+ /* ignore */
133
+ }
134
+ throw new Error("Manager process did not become ready in time.");
135
+ }
136
+ export async function stopManagerProcess() {
137
+ const pid = getManagerPid();
138
+ if (!pid) {
139
+ removeManagerReady();
140
+ return { pid: null, stopped: false };
141
+ }
142
+ if (!isRunning(pid)) {
143
+ removeManagerReady();
144
+ removeManagerPid();
145
+ return { pid, stopped: true };
146
+ }
147
+ try {
148
+ process.kill(pid, "SIGTERM");
149
+ await new Promise((resolve) => setTimeout(resolve, 500));
150
+ }
151
+ catch {
152
+ /* ignore */
153
+ }
154
+ if (isRunning(pid)) {
155
+ process.kill(pid, "SIGKILL");
156
+ }
157
+ removeManagerReady();
158
+ removeManagerPid();
159
+ return { pid, stopped: true };
160
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { APP_HOME } from "./constants.js";
4
+ import { openWebConfigUrl, startWebConfigServer } from "./config-web.js";
5
+ import { removeManagerPid, removeManagerReady, writeManagerReady } from "./manager-control.js";
6
+ import { startBackgroundService, stopBackgroundService } from "./service-control.js";
7
+ const CONFIG_UI_ONCE_FILE = join(APP_HOME, ".config-ui-once");
8
+ async function main() {
9
+ const web = await startWebConfigServer({ mode: "start", cwd: process.cwd(), persistent: true });
10
+ startBackgroundService(process.cwd());
11
+ writeManagerReady();
12
+ if (process.env.OPEN_IM_AUTO_OPEN_CONFIG_ONCE === "1" && !existsSync(CONFIG_UI_ONCE_FILE)) {
13
+ if (!existsSync(APP_HOME))
14
+ mkdirSync(APP_HOME, { recursive: true });
15
+ writeFileSync(CONFIG_UI_ONCE_FILE, "1", "utf-8");
16
+ openWebConfigUrl();
17
+ }
18
+ const shutdown = async () => {
19
+ await web.close().catch(() => { });
20
+ await stopBackgroundService().catch(() => { });
21
+ removeManagerReady();
22
+ removeManagerPid();
23
+ process.exit(0);
24
+ };
25
+ process.on("SIGINT", () => shutdown().catch(() => process.exit(1)));
26
+ process.on("SIGTERM", () => shutdown().catch(() => process.exit(1)));
27
+ }
28
+ const isEntry = process.argv[1]?.replace(/\\/g, "/").endsWith("/manager.js") ||
29
+ process.argv[1]?.replace(/\\/g, "/").endsWith("/manager.ts");
30
+ if (isEntry) {
31
+ main().catch((error) => {
32
+ console.error("Manager fatal error:", error);
33
+ removeManagerReady();
34
+ removeManagerPid();
35
+ process.exit(1);
36
+ });
37
+ }
@@ -4,7 +4,7 @@ import { dirname, extname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const PID_FILE = join(APP_HOME, "open-im.pid");
7
+ const PID_FILE = join(APP_HOME, "open-im-worker.pid");
8
8
  const PORT_FILE = join(APP_HOME, "open-im.port");
9
9
  function getServiceEntry() {
10
10
  const extension = extname(fileURLToPath(import.meta.url));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.5.5-beta.2",
3
+ "version": "1.5.5-beta.4",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",