@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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +8 -8
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/api/agent/runtime/route.js +1 -1
- package/.next/server/app/api/im/cancel/route.js +1 -1
- package/.next/server/app/api/im/gateway-status/route.js +1 -1
- package/.next/server/app/api/im/gateway-token/route.js +1 -1
- package/.next/server/app/api/im/project/route.js +1 -1
- package/.next/server/app/api/im/projects/route.js +1 -1
- package/.next/server/app/api/im/session-ids/route.js +1 -1
- package/.next/server/app/api/im/turn/route.js +2 -2
- package/.next/server/app/api/internal/runtime/route.js +1 -1
- package/.next/server/app/api/version/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login/page_client-reference-manifest.js +1 -1
- package/.next/server/app/login.html +1 -1
- package/.next/server/app/login.rsc +1 -1
- package/.next/server/app/login.segments/_full.segment.rsc +1 -1
- package/.next/server/app/login.segments/_head.segment.rsc +1 -1
- package/.next/server/app/login.segments/_index.segment.rsc +1 -1
- package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/login.segments/login.segment.rsc +1 -1
- package/.next/server/app/page.js +4 -4
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/smoke/page_client-reference-manifest.js +1 -1
- package/.next/server/app/smoke.html +1 -1
- package/.next/server/app/smoke.rsc +1 -1
- package/.next/server/app/smoke.segments/_full.segment.rsc +1 -1
- package/.next/server/app/smoke.segments/_head.segment.rsc +1 -1
- package/.next/server/app/smoke.segments/_index.segment.rsc +1 -1
- package/.next/server/app/smoke.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/smoke.segments/smoke/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/smoke.segments/smoke.segment.rsc +1 -1
- package/.next/server/app-paths-manifest.json +8 -8
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/{page-e88a9417235fcde7.js → page-56004268317e5ed2.js} +13 -11
- package/bin/annovibe-im-gateway.js +164 -55
- package/bin/pi-web.js +60 -1
- package/package.json +1 -1
- /package/.next/static/{dxpkIGb5tQfNQD77g3dwV → b4bqDE0ubPrjfKVWOgU81}/_buildManifest.js +0 -0
- /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,
|
|
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 =
|
|
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
|
-
|
|
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
|
|
95
|
-
|
|
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 =
|
|
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
|
|
138
|
-
|
|
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
|
-
//
|
|
420
|
-
if (message
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
497
|
+
// Group message filtering — respect requireMentionInGroup config
|
|
498
|
+
if (!shouldAcceptGroupMessage(message, this.project.requireMentionInGroup)) return;
|
|
423
499
|
|
|
424
|
-
//
|
|
425
|
-
if (message.
|
|
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
|
|
449
|
-
await pushText(
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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(
|
|
574
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
466
575
|
|
|
467
|
-
const
|
|
468
|
-
|
|
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 =
|
|
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.
|
|
502
|
-
this.pollTimer = setInterval(() => this.
|
|
616
|
+
await this.refreshProjects();
|
|
617
|
+
this.pollTimer = setInterval(() => this.refreshProjects(), 30000);
|
|
503
618
|
}
|
|
504
619
|
|
|
505
|
-
async
|
|
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
|
|
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
|
-
|
|
523
|
-
this.bridges.set(
|
|
524
|
-
void
|
|
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 (
|
|
531
|
-
|
|
532
|
-
|
|
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
|
File without changes
|
|
File without changes
|