@seqyuan/annovibe 0.8.53 → 0.8.55

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.
Files changed (72) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +8 -8
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  22. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/.next/server/app/api/agent/runtime/route.js +1 -1
  26. package/.next/server/app/api/im/cancel/route.js +1 -1
  27. package/.next/server/app/api/im/gateway-status/route.js +1 -1
  28. package/.next/server/app/api/im/gateway-token/route.js +1 -1
  29. package/.next/server/app/api/im/project/route.js +1 -1
  30. package/.next/server/app/api/im/projects/route.js +1 -1
  31. package/.next/server/app/api/im/session-ids/route.js +1 -1
  32. package/.next/server/app/api/im/turn/route.js +2 -2
  33. package/.next/server/app/api/internal/runtime/route.js +1 -1
  34. package/.next/server/app/api/version/route.js +1 -1
  35. package/.next/server/app/index.html +1 -1
  36. package/.next/server/app/index.rsc +2 -2
  37. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  38. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  39. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  41. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  43. package/.next/server/app/login.html +1 -1
  44. package/.next/server/app/login.rsc +1 -1
  45. package/.next/server/app/login.segments/_full.segment.rsc +1 -1
  46. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  47. package/.next/server/app/login.segments/_index.segment.rsc +1 -1
  48. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  49. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
  50. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  51. package/.next/server/app/page.js +4 -4
  52. package/.next/server/app/page_client-reference-manifest.js +1 -1
  53. package/.next/server/app/smoke/page_client-reference-manifest.js +1 -1
  54. package/.next/server/app/smoke.html +1 -1
  55. package/.next/server/app/smoke.rsc +1 -1
  56. package/.next/server/app/smoke.segments/_full.segment.rsc +1 -1
  57. package/.next/server/app/smoke.segments/_head.segment.rsc +1 -1
  58. package/.next/server/app/smoke.segments/_index.segment.rsc +1 -1
  59. package/.next/server/app/smoke.segments/_tree.segment.rsc +1 -1
  60. package/.next/server/app/smoke.segments/smoke/__PAGE__.segment.rsc +1 -1
  61. package/.next/server/app/smoke.segments/smoke.segment.rsc +1 -1
  62. package/.next/server/app-paths-manifest.json +8 -8
  63. package/.next/server/middleware-build-manifest.js +1 -1
  64. package/.next/server/pages/404.html +1 -1
  65. package/.next/server/pages/500.html +1 -1
  66. package/.next/server/server-reference-manifest.json +1 -1
  67. package/.next/static/chunks/app/{page-e88a9417235fcde7.js → page-56004268317e5ed2.js} +13 -11
  68. package/bin/annovibe-im-gateway.js +164 -55
  69. package/bin/pi-web.js +60 -1
  70. package/package.json +1 -1
  71. /package/.next/static/{dxpkIGb5tQfNQD77g3dwV → b4bqDE0ubPrjfKVWOgU81}/_buildManifest.js +0 -0
  72. /package/.next/static/{dxpkIGb5tQfNQD77g3dwV → b4bqDE0ubPrjfKVWOgU81}/_ssgManifest.js +0 -0
@@ -7,7 +7,9 @@
7
7
  *
8
8
  * Reads project IM configs from localhost:PORT/api/im/projects,
9
9
  * connects each enabled bot to WeCom WebSocket, and routes
10
- * incoming messages through /api/im/turn (SSE streaming).
10
+ * incoming messages through /api/im/turn (SSE streaming with delta push).
11
+ *
12
+ * Auto-started by annovibe supervisor; can also run standalone for debugging.
11
13
  */
12
14
  "use strict";
13
15
 
@@ -16,11 +18,41 @@ const http = require("http");
16
18
  const https = require("https");
17
19
  const os = require("os");
18
20
  const path = require("path");
21
+ const crypto = require("crypto");
19
22
  const WebSocket = require("ws");
20
23
 
21
24
  const REVISION_DIGITS = ["\u200b", "\u200c", "\u200d", "\u2060", "\ufeff"];
22
25
  const REVISION_SUFFIX_PATTERN = /[\u200b\u200c\u200d\u2060\ufeff]+$/u;
23
26
 
27
+ // ── Cancel command vocabulary ───────────────────────────────────────
28
+
29
+ const IM_CANCEL_COMMANDS = new Set([
30
+ "取消", "停止", "终止", "cancel", "stop", "abort",
31
+ ]);
32
+
33
+ function normalizeImCancelText(text) {
34
+ return text.trim()
35
+ .replace(REVISION_SUFFIX_PATTERN, "")
36
+ .replace(/[\u200b\u200c\u200d\u2060\ufeff\u00ad\ufe00-\ufe0f]/g, "")
37
+ .trim().toLowerCase().replace(/\s+/g, "");
38
+ }
39
+
40
+ function isImCancelCommand(text) {
41
+ return IM_CANCEL_COMMANDS.has(normalizeImCancelText(text));
42
+ }
43
+
44
+ // ── Reply text cleanup ──────────────────────────────────────────────
45
+
46
+ const SHOW_WIDGET_FENCE_RE = /```\s*show-widget[\s\S]*?```/gi;
47
+ const SHOW_WIDGET_PARTIAL_RE = /```\s*show-widget[\s\S]*$/;
48
+
49
+ function cleanImReplyText(text) {
50
+ let cleaned = String(text || "").replace(SHOW_WIDGET_FENCE_RE, "");
51
+ cleaned = cleaned.replace(SHOW_WIDGET_PARTIAL_RE, "");
52
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
53
+ return cleaned;
54
+ }
55
+
24
56
  // ── Config ──────────────────────────────────────────────────────────
25
57
 
26
58
  function envFirst(...names) {
@@ -31,14 +63,29 @@ function envFirst(...names) {
31
63
  return undefined;
32
64
  }
33
65
 
34
- const ANNOvibe_PORT = envFirst("ANNOVIBE_PORT", "PORT") ?? "30141";
35
- const BASE_URL = `http://127.0.0.1:${ANNOvibe_PORT}`;
36
- const GATEWAY_TOKEN = process.env.ANNOVIBE_GATEWAY_TOKEN?.trim() || "";
37
-
38
66
  function getAgentDir() {
39
67
  return envFirst("PI_AGENT_DIR") ?? path.join(os.homedir(), ".pi", "agent");
40
68
  }
41
69
 
70
+ function readAnnovibeState() {
71
+ const statePath = path.join(getAgentDir(), "annovibe.json");
72
+ try {
73
+ return JSON.parse(fs.readFileSync(statePath, "utf8"));
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ function getAnnovibePort() {
80
+ const state = readAnnovibeState();
81
+ if (state?.port) return String(state.port);
82
+ return envFirst("ANNOVIBE_PORT", "PORT") ?? "30141";
83
+ }
84
+
85
+ function getBaseUrl() {
86
+ return `http://127.0.0.1:${getAnnovibePort()}`;
87
+ }
88
+
42
89
  function readGatewayToken() {
43
90
  const statePath = path.join(getAgentDir(), "im", "gateway.json");
44
91
  try {
@@ -51,11 +98,15 @@ function readGatewayToken() {
51
98
 
52
99
  // ── HTTP helpers ────────────────────────────────────────────────────
53
100
 
101
+ function getToken() {
102
+ return process.env.ANNOVIBE_GATEWAY_TOKEN?.trim() || readGatewayToken();
103
+ }
104
+
54
105
  function requestJson(method, pathname, body) {
55
- const url = new URL(pathname, BASE_URL);
106
+ const url = new URL(pathname, getBaseUrl());
56
107
  const payload = body === undefined ? undefined : JSON.stringify(body);
57
108
  const lib = url.protocol === "https:" ? https : http;
58
- const token = GATEWAY_TOKEN || readGatewayToken();
109
+ const token = getToken();
59
110
 
60
111
  return new Promise((resolve, reject) => {
61
112
  const req = lib.request(
@@ -78,7 +129,8 @@ function requestJson(method, pathname, body) {
78
129
  return;
79
130
  }
80
131
  if (res.statusCode >= 400) {
81
- reject(new Error(parsed.error || parsed.reply || `HTTP ${res.statusCode}`));
132
+ const msg = parsed.error || parsed.reply || `HTTP ${res.statusCode}`;
133
+ reject(new Error(formatImApiError(res.statusCode, msg)));
82
134
  return;
83
135
  }
84
136
  resolve(parsed);
@@ -91,11 +143,24 @@ function requestJson(method, pathname, body) {
91
143
  });
92
144
  }
93
145
 
94
- function requestImTurnStream(body) {
95
- const url = new URL("/api/im/turn", BASE_URL);
146
+ function formatImApiError(statusCode, message) {
147
+ if (statusCode === 401) {
148
+ return `${message}. Ensure annovibe is running in this Linux account and gateway token is synced (restart annovibe-im-gateway after saving IM settings).`;
149
+ }
150
+ if (statusCode === 403) {
151
+ return `${message}. annovibe-im-gateway must reach annovibe on localhost.`;
152
+ }
153
+ return message;
154
+ }
155
+
156
+ /**
157
+ * Consume SSE turn stream, yielding deltas and status changes to callbacks.
158
+ */
159
+ function requestImTurnStream(body, callbacks) {
160
+ const url = new URL("/api/im/turn", getBaseUrl());
96
161
  const payload = JSON.stringify(body);
97
162
  const lib = url.protocol === "https:" ? https : http;
98
- const token = GATEWAY_TOKEN || readGatewayToken();
163
+ const token = getToken();
99
164
 
100
165
  return new Promise((resolve, reject) => {
101
166
  const req = lib.request(
@@ -134,8 +199,15 @@ function requestImTurnStream(body) {
134
199
  if (!line.startsWith("data: ")) continue;
135
200
  try {
136
201
  const event = JSON.parse(line.slice(6));
137
- if (event && event.type === "done") doneEvent = event;
138
- } catch { /* ignore */ }
202
+ if (!event) continue;
203
+ if (event.type === "done") {
204
+ doneEvent = event;
205
+ } else if (event.type === "delta" && callbacks.onDelta) {
206
+ callbacks.onDelta(event.text);
207
+ } else if (event.type === "status" && callbacks.onStatus) {
208
+ callbacks.onStatus(event.phase);
209
+ }
210
+ } catch { /* ignore malformed frames */ }
139
211
  }
140
212
  }
141
213
  });
@@ -179,6 +251,12 @@ function stripRevisionSuffix(text) {
179
251
  return text.replace(REVISION_SUFFIX_PATTERN, "");
180
252
  }
181
253
 
254
+ function shouldAcceptGroupMessage(message, requireMention) {
255
+ if (!message.chatId) return true; // DM, always accept
256
+ if (!requireMention) return true; // group chat, mention not required
257
+ return message.mentionedBot; // group chat, mention required
258
+ }
259
+
182
260
  // ── WeCom WebSocket client ──────────────────────────────────────────
183
261
 
184
262
  class WeComWsClient {
@@ -416,57 +494,93 @@ class ProjectWeComBridge {
416
494
  const message = extractInboundMessage(frame);
417
495
  if (!message) return;
418
496
 
419
- // Skip group messages if requireMention and not mentioned
420
- if (message.chatId && this.project.requireMentionInGroup && !message.mentionedBot) {
421
- return;
422
- }
497
+ // Group message filtering respect requireMentionInGroup config
498
+ if (!shouldAcceptGroupMessage(message, this.project.requireMentionInGroup)) return;
423
499
 
424
- // For group chats, skip if not mentioned at all
425
- if (message.chatId && !message.mentionedBot) {
500
+ // ── Cancel command intercept ──
501
+ if (isImCancelCommand(message.content)) {
502
+ const reqId = message.reqId;
503
+ try {
504
+ const result = await requestJson("POST", "/api/im/cancel", {
505
+ cwd: this.project.cwd,
506
+ userId: message.userId,
507
+ });
508
+ const text = typeof result.reply === "string" && result.reply.trim()
509
+ ? result.reply.trim()
510
+ : (result.cancelled ? "已取消当前任务。" : "当前没有可取消的任务。");
511
+ if (this.client?.isConnected) {
512
+ const streamId = `${reqId}-cancel-${Date.now()}`;
513
+ await this.client.replyStream(reqId, streamId, text, true);
514
+ }
515
+ } catch (error) {
516
+ console.error(`[im] cancel failed for ${this.project.cwd} user ${message.userId}: ${String(error)}`);
517
+ if (this.client?.isConnected) {
518
+ const streamId = `${reqId}-cancel-${Date.now()}`;
519
+ await this.client.replyStream(reqId, streamId, `Error: ${String(error)}`, true);
520
+ }
521
+ }
426
522
  return;
427
523
  }
428
524
 
525
+ // ── Normal turn with streaming ──
429
526
  const reqId = message.reqId;
430
527
  const streamId = `${reqId}-stream`;
431
528
  let revision = 0;
529
+ const startedAt = Date.now();
530
+ let refreshTimer = null;
531
+ let toolsRunning = false;
532
+ let lastText = "";
432
533
 
433
534
  const pushText = async (text) => {
434
535
  revision += 1;
435
536
  if (this.client?.isConnected) {
436
- await this.client.replyStream(reqId, streamId, applyRevisionSuffix(text, revision), false);
537
+ await this.client.replyStream(reqId, streamId, applyRevisionSuffix(text, revision), false).catch(() => undefined);
437
538
  }
438
539
  };
439
540
 
440
541
  const finishText = async (text) => {
441
542
  revision += 1;
442
543
  if (this.client?.isConnected) {
443
- await this.client.replyStream(reqId, streamId, applyRevisionSuffix(text, revision), true);
544
+ await this.client.replyStream(reqId, streamId, applyRevisionSuffix(text, revision), true).catch(() => undefined);
444
545
  }
445
546
  };
446
547
 
447
548
  try {
448
- // Start processing placeholder
449
- await pushText(`(working 1s)`);
450
-
451
- const elapsedInterval = setInterval(async () => {
452
- const elapsed = Math.floor((Date.now() - startTime) / 1000);
453
- await pushText(`(working ${elapsed}s)`);
454
- }, 3000);
549
+ // Start processing
550
+ await pushText("(working 1s)");
455
551
 
456
- const startTime = Date.now();
552
+ refreshTimer = setInterval(() => {
553
+ if (toolsRunning) {
554
+ const elapsed = Math.max(1, Math.floor((Date.now() - startedAt) / 1000));
555
+ void pushText(`(working ${elapsed}s)`).catch(() => undefined);
556
+ }
557
+ }, 5000);
457
558
 
458
- const result = await requestImTurnStream({
459
- cwd: this.project.cwd,
460
- userId: message.userId,
461
- message: message.content,
462
- chatId: message.chatId,
463
- });
559
+ const result = await requestImTurnStream(
560
+ { cwd: this.project.cwd, userId: message.userId, message: message.content, chatId: message.chatId },
561
+ {
562
+ onStatus(phase) {
563
+ toolsRunning = phase === "running_tools" || phase === "waiting_model";
564
+ },
565
+ onDelta(text) {
566
+ toolsRunning = false;
567
+ lastText = text;
568
+ // Schedule push — debounced by WeCom rate limits via replyStream
569
+ void pushText(text).catch(() => undefined);
570
+ },
571
+ },
572
+ );
464
573
 
465
- clearInterval(elapsedInterval);
574
+ if (refreshTimer) clearInterval(refreshTimer);
466
575
 
467
- const reply = stripRevisionSuffix(result.reply || "");
468
- await finishText(reply || "(no response)");
576
+ const rawReply = typeof result.reply === "string" && result.reply.trim()
577
+ ? result.reply.trim()
578
+ : "No reply from annovibe.";
579
+ const cleaned = cleanImReplyText(lastText || rawReply);
580
+ const finishTextStr = cleaned || "No reply from annovibe.";
581
+ await finishText(finishTextStr);
469
582
  } catch (error) {
583
+ if (refreshTimer) clearInterval(refreshTimer);
470
584
  await finishText(`Error: ${error.message}`);
471
585
  }
472
586
  }
@@ -485,7 +599,7 @@ class ImGateway {
485
599
  // Write gateway state
486
600
  const imDir = path.join(getAgentDir(), "im");
487
601
  try { fs.mkdirSync(imDir, { recursive: true }); } catch {}
488
- const token = require("crypto").randomBytes(32).toString("hex");
602
+ const token = crypto.randomBytes(32).toString("hex");
489
603
  fs.writeFileSync(
490
604
  path.join(imDir, "gateway.json"),
491
605
  JSON.stringify({
@@ -496,13 +610,14 @@ class ImGateway {
496
610
  );
497
611
 
498
612
  console.error(`[im] Gateway token: ${token.slice(0, 8)}...`);
613
+ console.error(`[im] Annovibe port: ${getAnnovibePort()}`);
499
614
 
500
615
  // Start config poll
501
- await this.pollProjects();
502
- this.pollTimer = setInterval(() => this.pollProjects(), 30000);
616
+ await this.refreshProjects();
617
+ this.pollTimer = setInterval(() => this.refreshProjects(), 30000);
503
618
  }
504
619
 
505
- async pollProjects() {
620
+ async refreshProjects() {
506
621
  if (this.stopped) return;
507
622
  try {
508
623
  const data = await requestJson("GET", "/api/im/projects");
@@ -513,24 +628,20 @@ class ImGateway {
513
628
  if (!project.cwd || !project.botId) continue;
514
629
  activeCwds.add(project.cwd);
515
630
 
516
- const key = project.cwd;
517
- let bridge = this.bridges.get(key);
631
+ const bridge = this.bridges.get(project.cwd);
518
632
  if (bridge) {
519
- // Update project info
520
633
  bridge.project = project;
521
634
  } else {
522
- bridge = new ProjectWeComBridge(project);
523
- this.bridges.set(key, bridge);
524
- void bridge.start();
635
+ const newBridge = new ProjectWeComBridge(project);
636
+ this.bridges.set(project.cwd, newBridge);
637
+ void newBridge.start();
525
638
  }
526
639
  }
527
640
 
528
- // Stop bridges for removed projects
529
641
  for (const [cwd, bridge] of this.bridges) {
530
- if (!activeCwds.has(cwd)) {
531
- await bridge.stop();
532
- this.bridges.delete(cwd);
533
- }
642
+ if (activeCwds.has(cwd)) continue;
643
+ await bridge.stop();
644
+ this.bridges.delete(cwd);
534
645
  }
535
646
  } catch (error) {
536
647
  console.error(`[im] Failed to poll projects: ${error.message}`);
@@ -545,7 +656,6 @@ class ImGateway {
545
656
  }
546
657
  this.bridges.clear();
547
658
 
548
- // Clear gateway state
549
659
  try {
550
660
  const statePath = path.join(getAgentDir(), "im", "gateway.json");
551
661
  fs.writeFileSync(statePath, JSON.stringify({ token: "" }, null, 2));
@@ -562,5 +672,4 @@ process.on("SIGTERM", () => { void gateway.stop().then(() => process.exit(0)); }
562
672
 
563
673
  void gateway.start();
564
674
 
565
- // Keep process alive
566
675
  setInterval(() => {}, 60000);
package/bin/pi-web.js CHANGED
@@ -37,6 +37,7 @@ const stateFile = path.join(agentDir, "annovibe.json");
37
37
  const legacyStateFile = path.join(agentDir, "pidex.json");
38
38
  const logFile = path.join(agentDir, "annovibe.log");
39
39
  const legacyLogFile = path.join(agentDir, "pidex.log");
40
+ const imGatewayLogFile = path.join(agentDir, "annovibe-im-gateway.log");
40
41
  const STOP_TERM_TIMEOUT_MS = 6000;
41
42
  const STOP_KILL_TIMEOUT_MS = 2000;
42
43
  const PROCESS_TITLE = "annovibe";
@@ -487,6 +488,8 @@ function renderState(state, json = false) {
487
488
  restartStartedAt: state?.restartStartedAt ?? null,
488
489
  logFile,
489
490
  stateFile,
491
+ imGatewayPid: state?.imGatewayPid && isPidAlive(state.imGatewayPid) ? state.imGatewayPid : null,
492
+ imGatewayLogFile: state?.imGatewayLogFile ?? imGatewayLogFile,
490
493
  };
491
494
  if (json) return JSON.stringify(payload, null, 2);
492
495
  if (payload.status === "stopped") {
@@ -508,6 +511,7 @@ function renderState(state, json = false) {
508
511
  `restart: pending${payload.installedVersion ? ` for v${payload.installedVersion}` : ""}`,
509
512
  ] : []),
510
513
  `log: ${payload.logFile}`,
514
+ ...(payload.imGatewayPid ? [`im-gateway pid: ${payload.imGatewayPid}`, `im-gateway log: ${payload.imGatewayLogFile}`] : ["im-gateway: not started"]),
511
515
  ].join("\n");
512
516
  }
513
517
 
@@ -563,6 +567,21 @@ async function stopManagedServer(quiet = false) {
563
567
  stopped = portStop.stopped && stopped;
564
568
  }
565
569
 
570
+ // Stop orphaned IM gateway (in case supervisor died but gateway lived on)
571
+ let imGatewayStopped = true;
572
+ try {
573
+ const imStatePath = path.join(agentDir, "im", "gateway.json");
574
+ if (fs.existsSync(imStatePath)) {
575
+ const imState = JSON.parse(fs.readFileSync(imStatePath, "utf8"));
576
+ if (imState?.pid && isPidAlive(imState.pid)) {
577
+ imGatewayStopped = await terminatePid(imState.pid);
578
+ if (!quiet) console.log(`annovibe stop: stopped im-gateway pid ${imState.pid}`);
579
+ }
580
+ }
581
+ } catch {
582
+ // ignore errors reading IM gateway state
583
+ }
584
+
566
585
  if (stopped) {
567
586
  removeState();
568
587
  if (!quiet) {
@@ -572,6 +591,7 @@ async function stopManagedServer(quiet = false) {
572
591
  console.log("annovibe stop: state was stale; cleaned up");
573
592
  }
574
593
  }
594
+ if (!imGatewayStopped) return 1;
575
595
  return 0;
576
596
  }
577
597
  if (!quiet) console.error(`annovibe stop: failed to stop annovibe process${verifiedCount === 1 ? "" : "es"}`);
@@ -671,9 +691,10 @@ Usage:
671
691
  annovibe [options]
672
692
  annovibe start Start annovibe in background
673
693
  annovibe restart Stop then start annovibe in background
674
- annovibe stop Stop background annovibe
694
+ annovibe stop Stop background annovibe and im-gateway (and orphaned processes)
675
695
  annovibe status [--json] Show background server status
676
696
  annovibe logs [-f] Show background server logs
697
+ annovibe im-gateway Run WeCom IM sidecar standalone (auto-started by annovibe start)
677
698
  annovibe passwd Change password (or set if none)
678
699
  annovibe passwd --reset Remove password (disable auth)
679
700
  annovibe update Update to the latest version
@@ -744,6 +765,20 @@ if (firstPos === "restart") {
744
765
  return;
745
766
  }
746
767
 
768
+ if (firstPos === "im-gateway") {
769
+ console.error("Note: im-gateway is auto-started by annovibe start. Use this command only for standalone debugging.");
770
+ const gatewayScript = path.join(__dirname, "annovibe-im-gateway.js");
771
+ const child = spawn(process.execPath, [gatewayScript, ...positionals.slice(1)], {
772
+ stdio: "inherit",
773
+ env: process.env,
774
+ });
775
+ child.on("exit", (code, signal) => {
776
+ if (signal) process.exit(1);
777
+ process.exit(code ?? 0);
778
+ });
779
+ return;
780
+ }
781
+
747
782
  if (firstPos === "update") {
748
783
  console.log(`\n Updating ${PKG_NAME} to latest version...\n`);
749
784
  const npmExecPath = process.env.npm_execpath;
@@ -1055,6 +1090,27 @@ async function runServerProcess() {
1055
1090
  });
1056
1091
  let restarting = false;
1057
1092
 
1093
+ // Spawn IM gateway as a child of the supervisor
1094
+ const gatewayScript = path.join(__dirname, "annovibe-im-gateway.js");
1095
+ let imGatewayChild = null;
1096
+ if (fs.existsSync(gatewayScript)) {
1097
+ const gwFd = fs.openSync(imGatewayLogFile, "a");
1098
+ imGatewayChild = spawn(process.execPath, [gatewayScript], {
1099
+ cwd: pkgDir,
1100
+ stdio: ["ignore", gwFd, gwFd],
1101
+ env: {
1102
+ ...process.env,
1103
+ ANNOVIBE_SUPERVISOR_PID: String(process.pid),
1104
+ ANNOVIBE_IM_GATEWAY_MANAGED: "1",
1105
+ },
1106
+ });
1107
+ fs.closeSync(gwFd);
1108
+ imGatewayChild.on("exit", (code, signal) => {
1109
+ const reason = signal ? `signal ${signal}` : `code ${code ?? "null"}`;
1110
+ console.error(`annovibe: im-gateway exited with ${reason}`);
1111
+ });
1112
+ }
1113
+
1058
1114
  const writeCurrentState = (patch = {}) => {
1059
1115
  const current = readState() ?? {};
1060
1116
  writeState({
@@ -1063,6 +1119,8 @@ async function runServerProcess() {
1063
1119
  command: getServerCommand(),
1064
1120
  supervisorPid: process.pid,
1065
1121
  nextPid: child.pid ?? null,
1122
+ imGatewayPid: imGatewayChild?.pid ?? null,
1123
+ imGatewayLogFile,
1066
1124
  port: String(port),
1067
1125
  hostname,
1068
1126
  url,
@@ -1097,6 +1155,7 @@ async function runServerProcess() {
1097
1155
  const shutdown = async () => {
1098
1156
  stopAutoRestartWatcher();
1099
1157
  await terminatePid(child.pid);
1158
+ if (imGatewayChild?.pid) await terminatePid(imGatewayChild.pid);
1100
1159
  if (!restarting) removeState();
1101
1160
  process.exit(0);
1102
1161
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seqyuan/annovibe",
3
- "version": "0.8.53",
3
+ "version": "0.8.55",
4
4
  "description": "AI-native bioinformatics workspace by Annoroad",
5
5
  "license": "MIT",
6
6
  "bin": {