ylib-syim 0.0.15 → 0.0.16
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/bridges/main.ts +989 -286
- package/bridges/runtime/plugin-schema-service.ts +27 -14
- package/bridges/runtime-event-reporter.ts +61 -16
- package/package.json +6 -6
- package/scripts/dingtalk-stdio-bridge.ts +173 -58
- package/scripts/lark-stdio-bridge.ts +283 -85
- package/scripts/weixin-stdio-bridge.ts +178 -66
package/bridges/main.ts
CHANGED
|
@@ -41,6 +41,58 @@ const runtimeErrorLogFallbackLines = Math.max(
|
|
|
41
41
|
String(RUNTIME_ERROR_LOG_FALLBACK_LINES_DEFAULT),
|
|
42
42
|
) || RUNTIME_ERROR_LOG_FALLBACK_LINES_DEFAULT,
|
|
43
43
|
);
|
|
44
|
+
const RUNTIME_LOG_DOWNLOAD_MAX_BYTES_DEFAULT = 2 * 1024 * 1024;
|
|
45
|
+
const runtimeLogDownloadMaxBytes = Math.max(
|
|
46
|
+
64 * 1024,
|
|
47
|
+
Number(
|
|
48
|
+
process.env.RUNTIME_LOG_DOWNLOAD_MAX_BYTES ||
|
|
49
|
+
String(RUNTIME_LOG_DOWNLOAD_MAX_BYTES_DEFAULT),
|
|
50
|
+
) || RUNTIME_LOG_DOWNLOAD_MAX_BYTES_DEFAULT,
|
|
51
|
+
);
|
|
52
|
+
const runtimeStatusProbeIntervalMs = Math.max(
|
|
53
|
+
0,
|
|
54
|
+
Number(process.env.RUNTIME_STATUS_PROBE_INTERVAL_MS || "15000") || 15000,
|
|
55
|
+
);
|
|
56
|
+
const runtimeStatusProbeTtlMs = Math.max(
|
|
57
|
+
0,
|
|
58
|
+
Number(
|
|
59
|
+
process.env.RUNTIME_STATUS_PROBE_TTL_MS ||
|
|
60
|
+
String(Math.max(1000, runtimeStatusProbeIntervalMs || 15000)),
|
|
61
|
+
) || Math.max(1000, runtimeStatusProbeIntervalMs || 15000),
|
|
62
|
+
);
|
|
63
|
+
const runtimeStatusProbeTimeoutMs = Math.max(
|
|
64
|
+
500,
|
|
65
|
+
Number(process.env.RUNTIME_STATUS_PROBE_TIMEOUT_MS || "3000") || 3000,
|
|
66
|
+
);
|
|
67
|
+
const runtimeStatusProbeTimeoutMsWeixin = Math.max(
|
|
68
|
+
runtimeStatusProbeTimeoutMs,
|
|
69
|
+
Number(process.env.RUNTIME_STATUS_PROBE_TIMEOUT_MS_WEIXIN || "4000") || 4000,
|
|
70
|
+
);
|
|
71
|
+
const runtimeStatusProbeConcurrency = {
|
|
72
|
+
dingtalk: Math.max(
|
|
73
|
+
1,
|
|
74
|
+
Number(process.env.RUNTIME_STATUS_PROBE_CONCURRENCY_DINGTALK || "4") || 4,
|
|
75
|
+
),
|
|
76
|
+
feishu: Math.max(
|
|
77
|
+
1,
|
|
78
|
+
Number(process.env.RUNTIME_STATUS_PROBE_CONCURRENCY_FEISHU || "4") || 4,
|
|
79
|
+
),
|
|
80
|
+
weixin: Math.max(
|
|
81
|
+
1,
|
|
82
|
+
Number(process.env.RUNTIME_STATUS_PROBE_CONCURRENCY_WEIXIN || "2") || 2,
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
const runtimeEventHeartbeatMs = Math.max(
|
|
86
|
+
1000,
|
|
87
|
+
Number(process.env.RUNTIME_EVENT_HEARTBEAT_MS || "20000") || 20000,
|
|
88
|
+
);
|
|
89
|
+
const runtimeEventHeartbeatStaleMs = Math.max(
|
|
90
|
+
runtimeEventHeartbeatMs,
|
|
91
|
+
Number(
|
|
92
|
+
process.env.RUNTIME_EVENT_HEARTBEAT_STALE_MS ||
|
|
93
|
+
String(runtimeEventHeartbeatMs * 3),
|
|
94
|
+
) || runtimeEventHeartbeatMs * 3,
|
|
95
|
+
);
|
|
44
96
|
|
|
45
97
|
type RuntimeBotStatus = {
|
|
46
98
|
platform: "dingtalk" | "feishu" | "weixin";
|
|
@@ -60,7 +112,16 @@ type RuntimeBotStatus = {
|
|
|
60
112
|
last_probe_at?: string | null;
|
|
61
113
|
};
|
|
62
114
|
|
|
115
|
+
type RuntimeHeartbeatEntry = {
|
|
116
|
+
platform: "dingtalk" | "feishu" | "weixin";
|
|
117
|
+
bot_account_id: string;
|
|
118
|
+
last_heartbeat_at: string;
|
|
119
|
+
last_event?: string | null;
|
|
120
|
+
};
|
|
121
|
+
|
|
63
122
|
const runtimeStatusRegistry = new Map<string, RuntimeBotStatus>();
|
|
123
|
+
const runtimeHeartbeatRegistry = new Map<string, RuntimeHeartbeatEntry>();
|
|
124
|
+
let runtimeHeartbeatLastReceivedAt: string | null = null;
|
|
64
125
|
let loadedConfigForRuntime: Record<string, unknown> | null = {
|
|
65
126
|
channels: {},
|
|
66
127
|
bindings: [],
|
|
@@ -78,6 +139,14 @@ let larkBridgeStarting = false;
|
|
|
78
139
|
let weixinBridgeStarted = false;
|
|
79
140
|
let weixinBridgeStarting = false;
|
|
80
141
|
let pluginTempHomeDir: string | null = null;
|
|
142
|
+
let probeInFlight: Promise<void> | null = null;
|
|
143
|
+
let lastProbeStartedAt: string | null = null;
|
|
144
|
+
let lastProbeFinishedAt: string | null = null;
|
|
145
|
+
let lastProbeDurationMs = 0;
|
|
146
|
+
let runtimeStatusCacheHitTotal = 0;
|
|
147
|
+
let runtimeStatusSingleflightReusedTotal = 0;
|
|
148
|
+
let runtimeStatusProbeTimeoutTotal = 0;
|
|
149
|
+
const botControlInFlight = new Set<string>();
|
|
81
150
|
|
|
82
151
|
type RuntimeGatewayChannel = "dingtalk" | "feishu" | "weixin";
|
|
83
152
|
|
|
@@ -87,6 +156,21 @@ type RuntimeGatewayInvokeResult = {
|
|
|
87
156
|
result?: unknown;
|
|
88
157
|
};
|
|
89
158
|
|
|
159
|
+
type RuntimePlatform = "dingtalk" | "feishu" | "weixin";
|
|
160
|
+
|
|
161
|
+
type RuntimeBridgeControl = {
|
|
162
|
+
restart?: () => Promise<void>;
|
|
163
|
+
stop?: () => Promise<void>;
|
|
164
|
+
startAccount?: (accountId: string) => Promise<void>;
|
|
165
|
+
stopAccount?: (accountId: string) => Promise<void>;
|
|
166
|
+
restartAccount?: (accountId: string) => Promise<void>;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
type ProbeRefreshOptions = {
|
|
170
|
+
force?: boolean;
|
|
171
|
+
reason?: string;
|
|
172
|
+
};
|
|
173
|
+
|
|
90
174
|
function pluginHomeConfigPayload(
|
|
91
175
|
cfg: Record<string, unknown>,
|
|
92
176
|
): Record<string, unknown> {
|
|
@@ -99,7 +183,6 @@ function pluginHomeConfigPayload(
|
|
|
99
183
|
: [],
|
|
100
184
|
};
|
|
101
185
|
}
|
|
102
|
-
|
|
103
186
|
/** 远端配置拉取成功时:临时 HOME + ~/.syim/syim.json 与 ~/.openclaw/openclaw.json(内容一致)。 */
|
|
104
187
|
function applyRemoteRuntimeConfigToPluginHome(): void {
|
|
105
188
|
if (
|
|
@@ -137,6 +220,8 @@ function applyRemoteRuntimeConfigToPluginHome(): void {
|
|
|
137
220
|
);
|
|
138
221
|
}
|
|
139
222
|
fs.writeFileSync(openclawPath, payload, "utf-8");
|
|
223
|
+
// 让 openclaw host 与插件均优先读取 .syim/syim.json。
|
|
224
|
+
process.env.OPENCLAW_CONFIG_PATH = syimPath;
|
|
140
225
|
process.env.HOME = pluginTempHomeDir;
|
|
141
226
|
if (process.platform === "win32")
|
|
142
227
|
process.env.USERPROFILE = pluginTempHomeDir;
|
|
@@ -176,6 +261,69 @@ function upsertRuntimeStatus(entry: RuntimeBotStatus): void {
|
|
|
176
261
|
});
|
|
177
262
|
}
|
|
178
263
|
|
|
264
|
+
function upsertRuntimeHeartbeat(entry: RuntimeHeartbeatEntry): void {
|
|
265
|
+
const key = keyOf(entry.platform, entry.bot_account_id);
|
|
266
|
+
const previous = runtimeHeartbeatRegistry.get(key);
|
|
267
|
+
runtimeHeartbeatRegistry.set(key, {
|
|
268
|
+
...previous,
|
|
269
|
+
...entry,
|
|
270
|
+
});
|
|
271
|
+
runtimeHeartbeatLastReceivedAt = nowIso();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isHeartbeatOnlyEvent(eventName: string | null | undefined): boolean {
|
|
275
|
+
const normalized = String(eventName || "")
|
|
276
|
+
.trim()
|
|
277
|
+
.toLowerCase();
|
|
278
|
+
return normalized === "runtime_heartbeat" || normalized === "heartbeat";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function buildRuntimeHeartbeatSnapshot(nowMs = Date.now()): {
|
|
282
|
+
heartbeat_ms: number;
|
|
283
|
+
stale_after_ms: number;
|
|
284
|
+
last_received_at: string | null;
|
|
285
|
+
entries: Array<{
|
|
286
|
+
platform: "dingtalk" | "feishu" | "weixin";
|
|
287
|
+
bot_account_id: string;
|
|
288
|
+
last_heartbeat_at: string | null;
|
|
289
|
+
age_ms: number | null;
|
|
290
|
+
stale: boolean;
|
|
291
|
+
last_event: string | null;
|
|
292
|
+
}>;
|
|
293
|
+
} {
|
|
294
|
+
const entries = Array.from(runtimeHeartbeatRegistry.values())
|
|
295
|
+
.map((item) => {
|
|
296
|
+
const rawHeartbeatAt = String(item.last_heartbeat_at || "").trim();
|
|
297
|
+
const heartbeatAt = rawHeartbeatAt || null;
|
|
298
|
+
const parsedMs = heartbeatAt ? Date.parse(heartbeatAt) : Number.NaN;
|
|
299
|
+
const ageMs = Number.isNaN(parsedMs)
|
|
300
|
+
? null
|
|
301
|
+
: Math.max(0, nowMs - parsedMs);
|
|
302
|
+
const stale = ageMs == null ? true : ageMs > runtimeEventHeartbeatStaleMs;
|
|
303
|
+
return {
|
|
304
|
+
platform: item.platform,
|
|
305
|
+
bot_account_id: item.bot_account_id,
|
|
306
|
+
last_heartbeat_at: heartbeatAt,
|
|
307
|
+
age_ms: ageMs,
|
|
308
|
+
stale,
|
|
309
|
+
last_event: item.last_event || null,
|
|
310
|
+
};
|
|
311
|
+
})
|
|
312
|
+
.sort((a, b) => {
|
|
313
|
+
if (a.platform !== b.platform) {
|
|
314
|
+
return a.platform.localeCompare(b.platform);
|
|
315
|
+
}
|
|
316
|
+
return a.bot_account_id.localeCompare(b.bot_account_id);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
heartbeat_ms: runtimeEventHeartbeatMs,
|
|
321
|
+
stale_after_ms: runtimeEventHeartbeatStaleMs,
|
|
322
|
+
last_received_at: runtimeHeartbeatLastReceivedAt,
|
|
323
|
+
entries,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
179
327
|
function isGenericRuntimeErrorMessage(
|
|
180
328
|
message: string | null | undefined,
|
|
181
329
|
): boolean {
|
|
@@ -324,16 +472,20 @@ function readRecentBridgeErrorFromLog(
|
|
|
324
472
|
/** Bridge file lines from setupBridgeLogger: `[ISO] [log|info|warn|error|debug] message` */
|
|
325
473
|
const BRIDGE_FILE_ERROR_LINE = /^\[[^\]]+\] \[error\] /;
|
|
326
474
|
|
|
475
|
+
function getBridgeLoggerFilePath(): string {
|
|
476
|
+
const loggerState = (globalThis as Record<string, unknown>)
|
|
477
|
+
.__imAgentHubBridgeLogger as
|
|
478
|
+
| { enabled?: boolean; logFilePath?: string }
|
|
479
|
+
| undefined;
|
|
480
|
+
return String(loggerState?.logFilePath || "").trim();
|
|
481
|
+
}
|
|
482
|
+
|
|
327
483
|
function readRuntimeLogLines(limit: number): {
|
|
328
484
|
log_file_path: string | null;
|
|
329
485
|
lines: string[];
|
|
330
486
|
log_level_filter: "error";
|
|
331
487
|
} {
|
|
332
|
-
const
|
|
333
|
-
.__imAgentHubBridgeLogger as
|
|
334
|
-
| { enabled?: boolean; logFilePath?: string }
|
|
335
|
-
| undefined;
|
|
336
|
-
const logFilePath = String(loggerState?.logFilePath || "").trim();
|
|
488
|
+
const logFilePath = getBridgeLoggerFilePath();
|
|
337
489
|
const clampedLimit = Math.max(1, Math.min(1000, Number(limit) || 100));
|
|
338
490
|
if (!logFilePath || !fs.existsSync(logFilePath)) {
|
|
339
491
|
return {
|
|
@@ -363,6 +515,99 @@ function readRuntimeLogLines(limit: number): {
|
|
|
363
515
|
}
|
|
364
516
|
}
|
|
365
517
|
|
|
518
|
+
function buildRuntimeLogDownloadFileName(logFilePath: string | null): string {
|
|
519
|
+
const candidate = String(logFilePath || "").trim();
|
|
520
|
+
if (candidate) {
|
|
521
|
+
const baseName = path.basename(candidate).trim();
|
|
522
|
+
if (baseName) return baseName;
|
|
523
|
+
}
|
|
524
|
+
return `im-agent-hub-runtime-${new Date().toISOString().replace(/[:.]/g, "-")}.log`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function parseRuntimeLogDownloadMaxBytes(maxBytesRaw?: unknown): number {
|
|
528
|
+
return Math.max(
|
|
529
|
+
64 * 1024,
|
|
530
|
+
Math.min(
|
|
531
|
+
20 * 1024 * 1024,
|
|
532
|
+
Number(maxBytesRaw) || runtimeLogDownloadMaxBytes,
|
|
533
|
+
),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function streamRuntimeLogDownload(
|
|
538
|
+
res: http.ServerResponse,
|
|
539
|
+
options?: { maxBytesRaw?: unknown },
|
|
540
|
+
): void {
|
|
541
|
+
const maxBytes = parseRuntimeLogDownloadMaxBytes(options?.maxBytesRaw);
|
|
542
|
+
const logFilePath = getBridgeLoggerFilePath();
|
|
543
|
+
const fileName = buildRuntimeLogDownloadFileName(logFilePath || null);
|
|
544
|
+
|
|
545
|
+
const baseHeaders: Record<string, string> = {
|
|
546
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
547
|
+
"Content-Disposition": `attachment; filename="${fileName}"`,
|
|
548
|
+
"X-IM-Runtime-Log-File-Name": fileName,
|
|
549
|
+
"X-IM-Runtime-Log-Max-Bytes": String(maxBytes),
|
|
550
|
+
"X-IM-Runtime-Instance-Id": runtimeInstanceId,
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
if (!logFilePath || !fs.existsSync(logFilePath)) {
|
|
554
|
+
res.writeHead(200, {
|
|
555
|
+
...baseHeaders,
|
|
556
|
+
"Content-Length": "0",
|
|
557
|
+
"X-IM-Runtime-Log-Truncated": "0",
|
|
558
|
+
"X-IM-Runtime-Log-Bytes": "0",
|
|
559
|
+
});
|
|
560
|
+
res.end();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const stat = fs.statSync(logFilePath);
|
|
566
|
+
const totalBytes = Math.max(0, Number(stat.size) || 0);
|
|
567
|
+
const start = Math.max(0, totalBytes - maxBytes);
|
|
568
|
+
const contentBytes = Math.max(0, totalBytes - start);
|
|
569
|
+
const truncated = start > 0;
|
|
570
|
+
|
|
571
|
+
if (totalBytes <= 0) {
|
|
572
|
+
res.writeHead(200, {
|
|
573
|
+
...baseHeaders,
|
|
574
|
+
"Content-Length": "0",
|
|
575
|
+
"X-IM-Runtime-Log-Truncated": "0",
|
|
576
|
+
"X-IM-Runtime-Log-Bytes": "0",
|
|
577
|
+
});
|
|
578
|
+
res.end();
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
res.writeHead(200, {
|
|
583
|
+
...baseHeaders,
|
|
584
|
+
"Content-Length": String(contentBytes),
|
|
585
|
+
"X-IM-Runtime-Log-Truncated": truncated ? "1" : "0",
|
|
586
|
+
"X-IM-Runtime-Log-Bytes": String(contentBytes),
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const stream = fs.createReadStream(logFilePath, { start });
|
|
590
|
+
stream.on("error", (err) => {
|
|
591
|
+
if (!res.headersSent) {
|
|
592
|
+
res.writeHead(500, {
|
|
593
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
594
|
+
"X-IM-Runtime-Read-Error": (err as Error).message.slice(0, 200),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
if (!res.writableEnded) {
|
|
598
|
+
res.end(`read log failed: ${(err as Error).message}`);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
stream.pipe(res);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
res.writeHead(500, {
|
|
604
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
605
|
+
"X-IM-Runtime-Read-Error": (err as Error).message.slice(0, 200),
|
|
606
|
+
});
|
|
607
|
+
res.end(`read log failed: ${(err as Error).message}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
366
611
|
function clearPlatformStatuses(
|
|
367
612
|
platform: "dingtalk" | "feishu" | "weixin",
|
|
368
613
|
): void {
|
|
@@ -494,10 +739,10 @@ function markPlatformStarted(platform: "dingtalk" | "feishu" | "weixin"): void {
|
|
|
494
739
|
if (!key.startsWith(`${platform}:`)) continue;
|
|
495
740
|
runtimeStatusRegistry.set(key, {
|
|
496
741
|
...value,
|
|
497
|
-
link_status: "
|
|
742
|
+
link_status: "connecting",
|
|
498
743
|
last_heartbeat_at: nowIso(),
|
|
499
744
|
last_error: null,
|
|
500
|
-
last_event: "
|
|
745
|
+
last_event: "platform_process_started",
|
|
501
746
|
status_source: "manual",
|
|
502
747
|
});
|
|
503
748
|
}
|
|
@@ -532,7 +777,8 @@ async function ensureBridgesStartedByConfig(): Promise<void> {
|
|
|
532
777
|
(startupOnlyMode === "all" || startupOnlyMode === "lark") &&
|
|
533
778
|
isChannelEnabled("feishu") &&
|
|
534
779
|
readChannelAccountCount("feishu") > 0;
|
|
535
|
-
const canStartWeixin =
|
|
780
|
+
const canStartWeixin =
|
|
781
|
+
startupOnlyMode === "all" || startupOnlyMode === "weixin";
|
|
536
782
|
|
|
537
783
|
const tasks: Array<Promise<void>> = [];
|
|
538
784
|
|
|
@@ -619,9 +865,7 @@ async function ensureBridgesStartedByConfig(): Promise<void> {
|
|
|
619
865
|
}),
|
|
620
866
|
);
|
|
621
867
|
} else if (startupOnlyMode === "all" || startupOnlyMode === "weixin") {
|
|
622
|
-
console.log(
|
|
623
|
-
"[bridges/main] skip weixin bridge start: enabled=false",
|
|
624
|
-
);
|
|
868
|
+
console.log("[bridges/main] skip weixin bridge start: enabled=false");
|
|
625
869
|
}
|
|
626
870
|
} else {
|
|
627
871
|
console.log(
|
|
@@ -650,7 +894,9 @@ async function ensureWeixinBridgeReadyForGatewayInvoke(): Promise<void> {
|
|
|
650
894
|
}
|
|
651
895
|
|
|
652
896
|
weixinBridgeStarting = true;
|
|
653
|
-
console.log(
|
|
897
|
+
console.log(
|
|
898
|
+
"[bridges/main] lazy starting weixin bridge for gateway invoke...",
|
|
899
|
+
);
|
|
654
900
|
try {
|
|
655
901
|
await import("./weixin-stdio-bridge.ts");
|
|
656
902
|
weixinBridgeStarted = true;
|
|
@@ -712,6 +958,48 @@ function normalizeGatewayChannel(input: unknown): RuntimeGatewayChannel | null {
|
|
|
712
958
|
return null;
|
|
713
959
|
}
|
|
714
960
|
|
|
961
|
+
function normalizeRuntimePlatform(input: unknown): RuntimePlatform | null {
|
|
962
|
+
const raw = String(input || "")
|
|
963
|
+
.trim()
|
|
964
|
+
.toLowerCase();
|
|
965
|
+
if (raw === "dingtalk" || raw === "dingtalk-connector") return "dingtalk";
|
|
966
|
+
if (raw === "feishu" || raw === "lark" || raw === "openclaw-lark") {
|
|
967
|
+
return "feishu";
|
|
968
|
+
}
|
|
969
|
+
if (raw === "weixin" || raw === "openclaw-weixin") {
|
|
970
|
+
return "weixin";
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function platformToChannelKey(
|
|
976
|
+
platform: RuntimePlatform,
|
|
977
|
+
): "dingtalk-connector" | "feishu" | "openclaw-weixin" {
|
|
978
|
+
if (platform === "dingtalk") return "dingtalk-connector";
|
|
979
|
+
if (platform === "feishu") return "feishu";
|
|
980
|
+
return "openclaw-weixin";
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function resolveRuntimeBridgeControl(
|
|
984
|
+
platform: RuntimePlatform,
|
|
985
|
+
): RuntimeBridgeControl | undefined {
|
|
986
|
+
const key =
|
|
987
|
+
platform === "dingtalk"
|
|
988
|
+
? "__IM_DINGTALK_BRIDGE_CONTROL__"
|
|
989
|
+
: platform === "feishu"
|
|
990
|
+
? "__IM_LARK_BRIDGE_CONTROL__"
|
|
991
|
+
: "__IM_WEIXIN_BRIDGE_CONTROL__";
|
|
992
|
+
const control = (globalThis as Record<string, unknown>)[key] as
|
|
993
|
+
| RuntimeBridgeControl
|
|
994
|
+
| undefined;
|
|
995
|
+
return control;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function generateOperationId(prefix: string): string {
|
|
999
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
1000
|
+
return `${prefix}-${Date.now()}-${rand}`;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
715
1003
|
async function invokeGatewayMethodByChannel(args: {
|
|
716
1004
|
channel: RuntimeGatewayChannel;
|
|
717
1005
|
method: string;
|
|
@@ -774,7 +1062,66 @@ async function invokeGatewayMethodByChannel(args: {
|
|
|
774
1062
|
}
|
|
775
1063
|
}
|
|
776
1064
|
|
|
777
|
-
|
|
1065
|
+
function parseBoolFlag(input: unknown): boolean {
|
|
1066
|
+
const normalized = String(input || "")
|
|
1067
|
+
.trim()
|
|
1068
|
+
.toLowerCase();
|
|
1069
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function isProbeFresh(nowMs = Date.now()): boolean {
|
|
1073
|
+
if (!lastProbeFinishedAt) return false;
|
|
1074
|
+
const lastMs = Date.parse(lastProbeFinishedAt);
|
|
1075
|
+
if (Number.isNaN(lastMs)) return false;
|
|
1076
|
+
return nowMs - lastMs <= runtimeStatusProbeTtlMs;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
async function withProbeTimeout<T>(
|
|
1080
|
+
work: Promise<T>,
|
|
1081
|
+
timeoutMs: number,
|
|
1082
|
+
timeoutMessage: string,
|
|
1083
|
+
): Promise<T> {
|
|
1084
|
+
if (timeoutMs <= 0) return await work;
|
|
1085
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
1086
|
+
try {
|
|
1087
|
+
return await Promise.race([
|
|
1088
|
+
work,
|
|
1089
|
+
new Promise<T>((_, reject) => {
|
|
1090
|
+
timer = setTimeout(() => {
|
|
1091
|
+
runtimeStatusProbeTimeoutTotal += 1;
|
|
1092
|
+
reject(new Error(timeoutMessage));
|
|
1093
|
+
}, timeoutMs);
|
|
1094
|
+
}),
|
|
1095
|
+
]);
|
|
1096
|
+
} finally {
|
|
1097
|
+
if (timer) {
|
|
1098
|
+
clearTimeout(timer);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async function runWithConcurrency<T>(
|
|
1104
|
+
items: T[],
|
|
1105
|
+
concurrency: number,
|
|
1106
|
+
worker: (item: T) => Promise<void>,
|
|
1107
|
+
): Promise<void> {
|
|
1108
|
+
if (!items.length) return;
|
|
1109
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
1110
|
+
let cursor = 0;
|
|
1111
|
+
|
|
1112
|
+
const runners = Array.from({ length: limit }, async () => {
|
|
1113
|
+
while (true) {
|
|
1114
|
+
const current = cursor;
|
|
1115
|
+
cursor += 1;
|
|
1116
|
+
if (current >= items.length) return;
|
|
1117
|
+
await worker(items[current] as T);
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
await Promise.all(runners);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
async function runFullProbeAndRefreshStatuses(): Promise<void> {
|
|
778
1125
|
if (!loadedConfigForRuntime) return;
|
|
779
1126
|
const cfg = loadedConfigForRuntime;
|
|
780
1127
|
|
|
@@ -817,120 +1164,152 @@ async function probeAndRefreshStatuses(): Promise<void> {
|
|
|
817
1164
|
if (listAccountIds) {
|
|
818
1165
|
const ids = configuredIds;
|
|
819
1166
|
if (resolveAccount && probeAccount) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
)
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const probeResult = await probeAccount({ account });
|
|
827
|
-
const ok = probeResult?.ok !== false;
|
|
828
|
-
const probeError = buildDetailedProbeErrorMessage(
|
|
829
|
-
probeResult as Record<string, unknown>,
|
|
1167
|
+
await runWithConcurrency(
|
|
1168
|
+
ids,
|
|
1169
|
+
runtimeStatusProbeConcurrency.dingtalk,
|
|
1170
|
+
async (accountId) => {
|
|
1171
|
+
const previous = runtimeStatusRegistry.get(
|
|
1172
|
+
keyOf("dingtalk", accountId),
|
|
830
1173
|
);
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
)
|
|
838
|
-
: probeError;
|
|
839
|
-
if (!ok) {
|
|
840
|
-
console.error(
|
|
841
|
-
`[bridges/main] dingtalk probeAccount failed account=${accountId} raw=${toLogText(probeResult)}`,
|
|
1174
|
+
try {
|
|
1175
|
+
const account = resolveAccount(cfg, accountId);
|
|
1176
|
+
const probeResult = await withProbeTimeout(
|
|
1177
|
+
probeAccount({ account }),
|
|
1178
|
+
runtimeStatusProbeTimeoutMs,
|
|
1179
|
+
`[bridges/main] dingtalk probeAccount timeout account=${accountId} timeoutMs=${runtimeStatusProbeTimeoutMs}`,
|
|
842
1180
|
);
|
|
843
|
-
|
|
844
|
-
|
|
1181
|
+
const ok = probeResult?.ok !== false;
|
|
1182
|
+
const probeError = buildDetailedProbeErrorMessage(
|
|
1183
|
+
probeResult as Record<string, unknown>,
|
|
845
1184
|
);
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1185
|
+
const probeErrorWithLogFallback = isGenericRuntimeErrorMessage(
|
|
1186
|
+
probeError,
|
|
1187
|
+
)
|
|
1188
|
+
? pickMoreSpecificErrorMessage(
|
|
1189
|
+
probeError,
|
|
1190
|
+
readRecentBridgeErrorFromLog(accountId, "dingtalk"),
|
|
1191
|
+
)
|
|
1192
|
+
: probeError;
|
|
1193
|
+
if (!ok) {
|
|
1194
|
+
console.error(
|
|
1195
|
+
`[bridges/main] dingtalk probeAccount failed account=${accountId} raw=${toLogText(probeResult)}`,
|
|
852
1196
|
);
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1197
|
+
console.error(
|
|
1198
|
+
`[bridges/main] dingtalk probeAccount derived_error account=${accountId} error=${toLogText(probeErrorWithLogFallback || "")}`,
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
const error = ok
|
|
1202
|
+
? null
|
|
1203
|
+
: pickMoreSpecificErrorMessage(
|
|
1204
|
+
previous?.last_error || null,
|
|
1205
|
+
probeErrorWithLogFallback,
|
|
1206
|
+
);
|
|
1207
|
+
if (!ok) {
|
|
1208
|
+
console.error(
|
|
1209
|
+
`[bridges/main] dingtalk probeAccount merged_error account=${accountId} error=${toLogText(error || "")}`,
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
upsertRuntimeStatus({
|
|
1213
|
+
platform: "dingtalk",
|
|
1214
|
+
bot_account_id: accountId,
|
|
1215
|
+
link_status: ok ? "connected" : "error",
|
|
1216
|
+
started_at: previous?.started_at || nowIso(),
|
|
1217
|
+
last_heartbeat_at: nowIso(),
|
|
1218
|
+
last_error: error,
|
|
1219
|
+
status_source: "probe",
|
|
1220
|
+
last_probe_at: nowIso(),
|
|
1221
|
+
});
|
|
1222
|
+
} catch (err) {
|
|
1223
|
+
upsertRuntimeStatus({
|
|
1224
|
+
platform: "dingtalk",
|
|
1225
|
+
bot_account_id: accountId,
|
|
1226
|
+
link_status: "error",
|
|
1227
|
+
started_at: previous?.started_at || nowIso(),
|
|
1228
|
+
last_heartbeat_at: nowIso(),
|
|
1229
|
+
last_error: pickMoreSpecificErrorMessage(
|
|
1230
|
+
previous?.last_error || null,
|
|
1231
|
+
(err as Error).message,
|
|
1232
|
+
),
|
|
1233
|
+
status_source: "probe",
|
|
1234
|
+
last_probe_at: nowIso(),
|
|
1235
|
+
});
|
|
857
1236
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
bot_account_id: accountId,
|
|
861
|
-
link_status: ok ? "connected" : "error",
|
|
862
|
-
started_at: previous?.started_at || nowIso(),
|
|
863
|
-
last_heartbeat_at: nowIso(),
|
|
864
|
-
last_error: error,
|
|
865
|
-
status_source: "probe",
|
|
866
|
-
last_probe_at: nowIso(),
|
|
867
|
-
});
|
|
868
|
-
} catch (err) {
|
|
869
|
-
upsertRuntimeStatus({
|
|
870
|
-
platform: "dingtalk",
|
|
871
|
-
bot_account_id: accountId,
|
|
872
|
-
link_status: "error",
|
|
873
|
-
started_at: previous?.started_at || nowIso(),
|
|
874
|
-
last_heartbeat_at: nowIso(),
|
|
875
|
-
last_error: pickMoreSpecificErrorMessage(
|
|
876
|
-
previous?.last_error || null,
|
|
877
|
-
(err as Error).message,
|
|
878
|
-
),
|
|
879
|
-
status_source: "probe",
|
|
880
|
-
last_probe_at: nowIso(),
|
|
881
|
-
});
|
|
882
|
-
}
|
|
883
|
-
}
|
|
1237
|
+
},
|
|
1238
|
+
);
|
|
884
1239
|
} else if (probe) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1240
|
+
await runWithConcurrency(
|
|
1241
|
+
ids,
|
|
1242
|
+
runtimeStatusProbeConcurrency.dingtalk,
|
|
1243
|
+
async (accountId) => {
|
|
1244
|
+
const previous = runtimeStatusRegistry.get(
|
|
1245
|
+
keyOf("dingtalk", accountId),
|
|
891
1246
|
);
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1247
|
+
try {
|
|
1248
|
+
const probeResult = await withProbeTimeout(
|
|
1249
|
+
probe({ cfg, accountId }),
|
|
1250
|
+
runtimeStatusProbeTimeoutMs,
|
|
1251
|
+
`[bridges/main] dingtalk probe timeout account=${accountId} timeoutMs=${runtimeStatusProbeTimeoutMs}`,
|
|
1252
|
+
);
|
|
1253
|
+
const ok = Boolean(probeResult?.ok);
|
|
1254
|
+
if (!ok) {
|
|
1255
|
+
console.error(
|
|
1256
|
+
`[bridges/main] dingtalk probe failed raw=${toLogText(probeResult)}`,
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
const probeError = buildDetailedProbeErrorMessage(
|
|
1260
|
+
probeResult as Record<string, unknown>,
|
|
1261
|
+
);
|
|
1262
|
+
const probeErrorWithLogFallback = isGenericRuntimeErrorMessage(
|
|
903
1263
|
probeError,
|
|
904
|
-
readRecentBridgeErrorFromLog(accountId, "dingtalk"),
|
|
905
1264
|
)
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1265
|
+
? pickMoreSpecificErrorMessage(
|
|
1266
|
+
probeError,
|
|
1267
|
+
readRecentBridgeErrorFromLog(accountId, "dingtalk"),
|
|
1268
|
+
)
|
|
1269
|
+
: probeError;
|
|
1270
|
+
if (!ok) {
|
|
1271
|
+
console.error(
|
|
1272
|
+
`[bridges/main] dingtalk probe derived_error account=${accountId} error=${toLogText(probeErrorWithLogFallback || "")}`,
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
const error = ok
|
|
1276
|
+
? null
|
|
1277
|
+
: pickMoreSpecificErrorMessage(
|
|
1278
|
+
previous?.last_error || null,
|
|
1279
|
+
probeErrorWithLogFallback,
|
|
1280
|
+
);
|
|
1281
|
+
if (!ok) {
|
|
1282
|
+
console.error(
|
|
1283
|
+
`[bridges/main] dingtalk probe merged_error account=${accountId} error=${toLogText(error || "")}`,
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
upsertRuntimeStatus({
|
|
1287
|
+
platform: "dingtalk",
|
|
1288
|
+
bot_account_id: accountId,
|
|
1289
|
+
link_status: ok ? "connected" : "error",
|
|
1290
|
+
started_at: previous?.started_at || nowIso(),
|
|
1291
|
+
last_heartbeat_at: nowIso(),
|
|
1292
|
+
last_error: error,
|
|
1293
|
+
status_source: "probe",
|
|
1294
|
+
last_probe_at: nowIso(),
|
|
1295
|
+
});
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
upsertRuntimeStatus({
|
|
1298
|
+
platform: "dingtalk",
|
|
1299
|
+
bot_account_id: accountId,
|
|
1300
|
+
link_status: "error",
|
|
1301
|
+
started_at: previous?.started_at || nowIso(),
|
|
1302
|
+
last_heartbeat_at: nowIso(),
|
|
1303
|
+
last_error: pickMoreSpecificErrorMessage(
|
|
1304
|
+
previous?.last_error || null,
|
|
1305
|
+
(err as Error).message,
|
|
1306
|
+
),
|
|
1307
|
+
status_source: "probe",
|
|
1308
|
+
last_probe_at: nowIso(),
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
},
|
|
1312
|
+
);
|
|
934
1313
|
}
|
|
935
1314
|
}
|
|
936
1315
|
}
|
|
@@ -970,117 +1349,206 @@ async function probeAndRefreshStatuses(): Promise<void> {
|
|
|
970
1349
|
|
|
971
1350
|
if (listAccountIds && resolveAccount && probeAccount) {
|
|
972
1351
|
const ids = configuredIds;
|
|
973
|
-
|
|
1352
|
+
await runWithConcurrency(
|
|
1353
|
+
ids,
|
|
1354
|
+
runtimeStatusProbeConcurrency.feishu,
|
|
1355
|
+
async (accountId) => {
|
|
1356
|
+
try {
|
|
1357
|
+
const previous = runtimeStatusRegistry.get(
|
|
1358
|
+
keyOf("feishu", accountId),
|
|
1359
|
+
);
|
|
1360
|
+
const account = resolveAccount(cfg, accountId);
|
|
1361
|
+
const probeResult = await withProbeTimeout(
|
|
1362
|
+
probeAccount({ account }),
|
|
1363
|
+
runtimeStatusProbeTimeoutMs,
|
|
1364
|
+
`[bridges/main] feishu probeAccount timeout account=${accountId} timeoutMs=${runtimeStatusProbeTimeoutMs}`,
|
|
1365
|
+
);
|
|
1366
|
+
const ok = probeResult?.ok !== false;
|
|
1367
|
+
const probeError = buildDetailedProbeErrorMessage(
|
|
1368
|
+
probeResult as Record<string, unknown>,
|
|
1369
|
+
);
|
|
1370
|
+
const probeErrorWithLogFallback = isGenericRuntimeErrorMessage(
|
|
1371
|
+
probeError,
|
|
1372
|
+
)
|
|
1373
|
+
? pickMoreSpecificErrorMessage(
|
|
1374
|
+
probeError,
|
|
1375
|
+
readRecentBridgeErrorFromLog(accountId, "feishu"),
|
|
1376
|
+
)
|
|
1377
|
+
: probeError;
|
|
1378
|
+
const preserveEventStatus =
|
|
1379
|
+
previous?.status_source === "event" &&
|
|
1380
|
+
(previous.link_status === "error" ||
|
|
1381
|
+
previous.link_status === "disconnected" ||
|
|
1382
|
+
previous.link_status === "degraded");
|
|
1383
|
+
|
|
1384
|
+
const nextStatus: RuntimeBotStatus["link_status"] =
|
|
1385
|
+
preserveEventStatus
|
|
1386
|
+
? previous?.link_status || "connecting"
|
|
1387
|
+
: ok
|
|
1388
|
+
? "connected"
|
|
1389
|
+
: "error";
|
|
1390
|
+
const nextError =
|
|
1391
|
+
nextStatus === "connected"
|
|
1392
|
+
? null
|
|
1393
|
+
: pickMoreSpecificErrorMessage(
|
|
1394
|
+
previous?.last_error || null,
|
|
1395
|
+
probeErrorWithLogFallback,
|
|
1396
|
+
);
|
|
1397
|
+
|
|
1398
|
+
upsertRuntimeStatus({
|
|
1399
|
+
platform: "feishu",
|
|
1400
|
+
bot_account_id: accountId,
|
|
1401
|
+
link_status: nextStatus,
|
|
1402
|
+
started_at: previous?.started_at || nowIso(),
|
|
1403
|
+
last_heartbeat_at: nowIso(),
|
|
1404
|
+
last_error: nextError,
|
|
1405
|
+
status_source: preserveEventStatus ? "event" : "probe",
|
|
1406
|
+
last_probe_at: nowIso(),
|
|
1407
|
+
});
|
|
1408
|
+
} catch (err) {
|
|
1409
|
+
upsertRuntimeStatus({
|
|
1410
|
+
platform: "feishu",
|
|
1411
|
+
bot_account_id: accountId,
|
|
1412
|
+
link_status: "error",
|
|
1413
|
+
started_at:
|
|
1414
|
+
runtimeStatusRegistry.get(keyOf("feishu", accountId))
|
|
1415
|
+
?.started_at || nowIso(),
|
|
1416
|
+
last_heartbeat_at: nowIso(),
|
|
1417
|
+
last_error: (err as Error).message,
|
|
1418
|
+
status_source: "probe",
|
|
1419
|
+
last_probe_at: nowIso(),
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
},
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
console.error("[bridges/main] feishu probe failed", (err as Error).message);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
try {
|
|
1431
|
+
const configuredIds = readConfiguredAccountIds(cfg, "openclaw-weixin");
|
|
1432
|
+
if (configuredIds.length === 0) {
|
|
1433
|
+
clearPlatformStatuses("weixin");
|
|
1434
|
+
} else {
|
|
1435
|
+
await runWithConcurrency(
|
|
1436
|
+
configuredIds,
|
|
1437
|
+
runtimeStatusProbeConcurrency.weixin,
|
|
1438
|
+
async (accountId) => {
|
|
1439
|
+
const previous = runtimeStatusRegistry.get(
|
|
1440
|
+
keyOf("weixin", accountId),
|
|
1441
|
+
);
|
|
974
1442
|
try {
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1443
|
+
const invoke = await withProbeTimeout(
|
|
1444
|
+
invokeGatewayMethodByChannel({
|
|
1445
|
+
channel: "weixin",
|
|
1446
|
+
method: "weixin.probe",
|
|
1447
|
+
params: { accountId },
|
|
1448
|
+
accountId,
|
|
1449
|
+
}),
|
|
1450
|
+
runtimeStatusProbeTimeoutMsWeixin,
|
|
1451
|
+
`[bridges/main] weixin probe timeout account=${accountId} timeoutMs=${runtimeStatusProbeTimeoutMsWeixin}`,
|
|
983
1452
|
);
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1453
|
+
const probeResult =
|
|
1454
|
+
invoke?.result && typeof invoke.result === "object"
|
|
1455
|
+
? (invoke.result as Record<string, unknown>)
|
|
1456
|
+
: null;
|
|
1457
|
+
const probeOk =
|
|
1458
|
+
invoke?.ok === true &&
|
|
1459
|
+
(!probeResult ||
|
|
1460
|
+
probeResult.ok === undefined ||
|
|
1461
|
+
probeResult.ok !== false);
|
|
1462
|
+
const probeError = probeResult
|
|
1463
|
+
? buildDetailedProbeErrorMessage(probeResult)
|
|
1464
|
+
: String(invoke?.error || "").trim();
|
|
1465
|
+
const probeErrorWithLogFallback = isGenericRuntimeErrorMessage(
|
|
1466
|
+
probeError,
|
|
1467
|
+
)
|
|
1468
|
+
? pickMoreSpecificErrorMessage(
|
|
988
1469
|
probeError,
|
|
989
|
-
|
|
1470
|
+
readRecentBridgeErrorFromLog(accountId, "weixin"),
|
|
1471
|
+
)
|
|
1472
|
+
: probeError;
|
|
1473
|
+
const linkStatus: RuntimeBotStatus["link_status"] = probeOk
|
|
1474
|
+
? "connected"
|
|
1475
|
+
: weixinBridgeStarted
|
|
1476
|
+
? "error"
|
|
1477
|
+
: "connecting";
|
|
990
1478
|
upsertRuntimeStatus({
|
|
991
|
-
platform: "
|
|
1479
|
+
platform: "weixin",
|
|
992
1480
|
bot_account_id: accountId,
|
|
993
|
-
link_status:
|
|
1481
|
+
link_status: linkStatus,
|
|
994
1482
|
started_at: previous?.started_at || nowIso(),
|
|
995
1483
|
last_heartbeat_at: nowIso(),
|
|
996
|
-
last_error:
|
|
1484
|
+
last_error: probeOk
|
|
1485
|
+
? null
|
|
1486
|
+
: pickMoreSpecificErrorMessage(
|
|
1487
|
+
previous?.last_error || null,
|
|
1488
|
+
probeErrorWithLogFallback || invoke?.error || null,
|
|
1489
|
+
),
|
|
997
1490
|
status_source: "probe",
|
|
998
1491
|
last_probe_at: nowIso(),
|
|
999
1492
|
});
|
|
1000
1493
|
} catch (err) {
|
|
1001
1494
|
upsertRuntimeStatus({
|
|
1002
|
-
platform: "
|
|
1495
|
+
platform: "weixin",
|
|
1003
1496
|
bot_account_id: accountId,
|
|
1004
|
-
link_status: "error",
|
|
1005
|
-
started_at:
|
|
1006
|
-
runtimeStatusRegistry.get(keyOf("feishu", accountId))
|
|
1007
|
-
?.started_at || nowIso(),
|
|
1497
|
+
link_status: weixinBridgeStarted ? "error" : "connecting",
|
|
1498
|
+
started_at: previous?.started_at || nowIso(),
|
|
1008
1499
|
last_heartbeat_at: nowIso(),
|
|
1009
|
-
last_error: (
|
|
1500
|
+
last_error: pickMoreSpecificErrorMessage(
|
|
1501
|
+
previous?.last_error || null,
|
|
1502
|
+
(err as Error).message,
|
|
1503
|
+
),
|
|
1010
1504
|
status_source: "probe",
|
|
1011
1505
|
last_probe_at: nowIso(),
|
|
1012
1506
|
});
|
|
1013
1507
|
}
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1508
|
+
},
|
|
1509
|
+
);
|
|
1016
1510
|
}
|
|
1017
1511
|
} catch (err) {
|
|
1018
|
-
console.error("[bridges/main]
|
|
1512
|
+
console.error("[bridges/main] weixin probe failed", (err as Error).message);
|
|
1019
1513
|
}
|
|
1514
|
+
}
|
|
1020
1515
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
invoke?.result && typeof invoke.result === "object"
|
|
1036
|
-
? (invoke.result as Record<string, unknown>)
|
|
1037
|
-
: null;
|
|
1038
|
-
const probeOk =
|
|
1039
|
-
invoke?.ok === true &&
|
|
1040
|
-
(!probeResult || probeResult.ok === undefined || probeResult.ok !== false);
|
|
1041
|
-
const probeError = probeResult
|
|
1042
|
-
? buildDetailedProbeErrorMessage(probeResult)
|
|
1043
|
-
: String(invoke?.error || "").trim();
|
|
1044
|
-
const probeErrorWithLogFallback = isGenericRuntimeErrorMessage(probeError)
|
|
1045
|
-
? pickMoreSpecificErrorMessage(
|
|
1046
|
-
probeError,
|
|
1047
|
-
readRecentBridgeErrorFromLog(accountId, "weixin"),
|
|
1048
|
-
)
|
|
1049
|
-
: probeError;
|
|
1050
|
-
const linkStatus: RuntimeBotStatus["link_status"] = probeOk
|
|
1051
|
-
? "connected"
|
|
1052
|
-
: weixinBridgeStarted
|
|
1053
|
-
? "error"
|
|
1054
|
-
: "connecting";
|
|
1055
|
-
upsertRuntimeStatus({
|
|
1056
|
-
platform: "weixin",
|
|
1057
|
-
bot_account_id: accountId,
|
|
1058
|
-
link_status: linkStatus,
|
|
1059
|
-
started_at: previous?.started_at || nowIso(),
|
|
1060
|
-
last_heartbeat_at: nowIso(),
|
|
1061
|
-
last_error: probeOk
|
|
1062
|
-
? null
|
|
1063
|
-
: pickMoreSpecificErrorMessage(
|
|
1064
|
-
previous?.last_error || null,
|
|
1065
|
-
probeErrorWithLogFallback || invoke?.error || null,
|
|
1066
|
-
),
|
|
1067
|
-
status_source: "probe",
|
|
1068
|
-
last_probe_at: nowIso(),
|
|
1069
|
-
});
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
} catch (err) {
|
|
1073
|
-
console.error("[bridges/main] weixin probe failed", (err as Error).message);
|
|
1516
|
+
async function probeAndRefreshStatuses(
|
|
1517
|
+
options: ProbeRefreshOptions = {},
|
|
1518
|
+
): Promise<void> {
|
|
1519
|
+
const forceProbe = options.force === true;
|
|
1520
|
+
|
|
1521
|
+
if (!forceProbe && isProbeFresh()) {
|
|
1522
|
+
runtimeStatusCacheHitTotal += 1;
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
if (probeInFlight) {
|
|
1527
|
+
runtimeStatusSingleflightReusedTotal += 1;
|
|
1528
|
+
await probeInFlight;
|
|
1529
|
+
return;
|
|
1074
1530
|
}
|
|
1531
|
+
|
|
1532
|
+
const startedAt = Date.now();
|
|
1533
|
+
lastProbeStartedAt = new Date(startedAt).toISOString();
|
|
1534
|
+
|
|
1535
|
+
probeInFlight = (async () => {
|
|
1536
|
+
await runFullProbeAndRefreshStatuses();
|
|
1537
|
+
})().finally(() => {
|
|
1538
|
+
const finishedAt = Date.now();
|
|
1539
|
+
lastProbeFinishedAt = new Date(finishedAt).toISOString();
|
|
1540
|
+
lastProbeDurationMs = Math.max(0, finishedAt - startedAt);
|
|
1541
|
+
probeInFlight = null;
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
await probeInFlight;
|
|
1075
1545
|
}
|
|
1076
1546
|
|
|
1077
1547
|
function startProbeLoop(): void {
|
|
1078
|
-
const intervalMs =
|
|
1079
|
-
process.env.RUNTIME_STATUS_PROBE_INTERVAL_MS || "15000",
|
|
1080
|
-
);
|
|
1548
|
+
const intervalMs = runtimeStatusProbeIntervalMs;
|
|
1081
1549
|
if (intervalMs <= 0) return;
|
|
1082
1550
|
setInterval(() => {
|
|
1083
|
-
void probeAndRefreshStatuses();
|
|
1551
|
+
void probeAndRefreshStatuses({ reason: "loop" });
|
|
1084
1552
|
}, intervalMs);
|
|
1085
1553
|
}
|
|
1086
1554
|
|
|
@@ -1148,11 +1616,16 @@ async function pullRuntimeConfigFromPython(forceFull = false): Promise<{
|
|
|
1148
1616
|
|
|
1149
1617
|
const notModified = Boolean(data?.not_modified);
|
|
1150
1618
|
if (notModified) {
|
|
1619
|
+
const pulledVersion = String(data?.version || "").trim();
|
|
1620
|
+
if (pulledVersion) {
|
|
1621
|
+
configVersionHash = pulledVersion;
|
|
1622
|
+
configVersionUpdatedAt = nowIso();
|
|
1623
|
+
}
|
|
1151
1624
|
return {
|
|
1152
1625
|
ok: true,
|
|
1153
1626
|
pulled: false,
|
|
1154
1627
|
not_modified: true,
|
|
1155
|
-
version:
|
|
1628
|
+
version: pulledVersion || configVersionHash || "unknown",
|
|
1156
1629
|
};
|
|
1157
1630
|
}
|
|
1158
1631
|
|
|
@@ -1182,10 +1655,13 @@ async function pullRuntimeConfigFromPython(forceFull = false): Promise<{
|
|
|
1182
1655
|
loadedConfigForRuntime,
|
|
1183
1656
|
loadedConfigPathForRuntime,
|
|
1184
1657
|
);
|
|
1185
|
-
updateConfigVersion(
|
|
1658
|
+
updateConfigVersion(
|
|
1659
|
+
normalizedResult.normalized,
|
|
1660
|
+
String(data?.version || "").trim() || undefined,
|
|
1661
|
+
);
|
|
1186
1662
|
applyRemoteRuntimeConfigToPluginHome();
|
|
1187
1663
|
markConfiguredBotsAsConnecting(normalizedResult.normalized);
|
|
1188
|
-
await probeAndRefreshStatuses();
|
|
1664
|
+
await probeAndRefreshStatuses({ force: true, reason: "config_pull" });
|
|
1189
1665
|
|
|
1190
1666
|
if (runtimeConfigPullPersist) {
|
|
1191
1667
|
const targetPath =
|
|
@@ -1280,7 +1756,7 @@ async function waitRestartCompletion(
|
|
|
1280
1756
|
const deadline = Date.now() + Math.max(1000, timeoutMs);
|
|
1281
1757
|
const interval = Math.max(200, pollIntervalMs);
|
|
1282
1758
|
while (Date.now() < deadline) {
|
|
1283
|
-
await probeAndRefreshStatuses();
|
|
1759
|
+
await probeAndRefreshStatuses({ force: true, reason: "restart_wait" });
|
|
1284
1760
|
const summary = summarizeBots();
|
|
1285
1761
|
const unsettled =
|
|
1286
1762
|
summary.connecting + summary.degraded + summary.disconnected;
|
|
@@ -1345,10 +1821,11 @@ function readEffectiveWhitelistFromConfig(
|
|
|
1345
1821
|
"allowFrom",
|
|
1346
1822
|
"groupPolicy",
|
|
1347
1823
|
"requireMention",
|
|
1824
|
+
"scope_kind",
|
|
1825
|
+
"scopeKind",
|
|
1348
1826
|
"scopeType",
|
|
1349
1827
|
"projectId",
|
|
1350
1828
|
"scope_type",
|
|
1351
|
-
"type",
|
|
1352
1829
|
"project_id",
|
|
1353
1830
|
],
|
|
1354
1831
|
},
|
|
@@ -1371,10 +1848,11 @@ function readEffectiveWhitelistFromConfig(
|
|
|
1371
1848
|
"dmPolicy",
|
|
1372
1849
|
"allowFrom",
|
|
1373
1850
|
"groupPolicy",
|
|
1851
|
+
"scope_kind",
|
|
1852
|
+
"scopeKind",
|
|
1374
1853
|
"scopeType",
|
|
1375
1854
|
"projectId",
|
|
1376
1855
|
"scope_type",
|
|
1377
|
-
"type",
|
|
1378
1856
|
"project_id",
|
|
1379
1857
|
],
|
|
1380
1858
|
},
|
|
@@ -1389,10 +1867,11 @@ function readEffectiveWhitelistFromConfig(
|
|
|
1389
1867
|
"uploadHost",
|
|
1390
1868
|
],
|
|
1391
1869
|
runtimeReadonlyFields: [
|
|
1870
|
+
"scope_kind",
|
|
1871
|
+
"scopeKind",
|
|
1392
1872
|
"scopeType",
|
|
1393
1873
|
"projectId",
|
|
1394
1874
|
"scope_type",
|
|
1395
|
-
"type",
|
|
1396
1875
|
"project_id",
|
|
1397
1876
|
],
|
|
1398
1877
|
},
|
|
@@ -1505,8 +1984,12 @@ function calcSchemaHash(rawSchema: unknown): string {
|
|
|
1505
1984
|
return calcSchemaHashFromRuntime(rawSchema);
|
|
1506
1985
|
}
|
|
1507
1986
|
|
|
1508
|
-
function updateConfigVersion(
|
|
1509
|
-
|
|
1987
|
+
function updateConfigVersion(
|
|
1988
|
+
config: Record<string, unknown>,
|
|
1989
|
+
preferredVersion?: string,
|
|
1990
|
+
): void {
|
|
1991
|
+
const normalizedPreferredVersion = String(preferredVersion || "").trim();
|
|
1992
|
+
configVersionHash = normalizedPreferredVersion || calcSchemaHash(config);
|
|
1510
1993
|
configVersionUpdatedAt = nowIso();
|
|
1511
1994
|
}
|
|
1512
1995
|
|
|
@@ -1674,7 +2157,11 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1674
2157
|
req.method === "GET" &&
|
|
1675
2158
|
reqUrl.pathname === "/internal/runtime/bots/status"
|
|
1676
2159
|
) {
|
|
1677
|
-
|
|
2160
|
+
const forceProbe = parseBoolFlag(reqUrl.searchParams.get("force_probe"));
|
|
2161
|
+
await probeAndRefreshStatuses({
|
|
2162
|
+
force: forceProbe,
|
|
2163
|
+
reason: forceProbe ? "status_force" : "status_read",
|
|
2164
|
+
});
|
|
1678
2165
|
const bots = Array.from(runtimeStatusRegistry.values());
|
|
1679
2166
|
const summary = summarizeBots();
|
|
1680
2167
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1685,6 +2172,22 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1685
2172
|
runtime_state: runtimeState,
|
|
1686
2173
|
version: configVersionHash || "unknown",
|
|
1687
2174
|
generated_at: nowIso(),
|
|
2175
|
+
probe_meta: {
|
|
2176
|
+
ttl_ms: runtimeStatusProbeTtlMs,
|
|
2177
|
+
per_account_timeout_ms: runtimeStatusProbeTimeoutMs,
|
|
2178
|
+
per_account_timeout_ms_weixin: runtimeStatusProbeTimeoutMsWeixin,
|
|
2179
|
+
concurrency: runtimeStatusProbeConcurrency,
|
|
2180
|
+
force_probe: forceProbe,
|
|
2181
|
+
in_flight: Boolean(probeInFlight),
|
|
2182
|
+
is_fresh: isProbeFresh(),
|
|
2183
|
+
last_started_at: lastProbeStartedAt,
|
|
2184
|
+
last_finished_at: lastProbeFinishedAt,
|
|
2185
|
+
last_duration_ms: lastProbeDurationMs,
|
|
2186
|
+
cache_hit_total: runtimeStatusCacheHitTotal,
|
|
2187
|
+
singleflight_reused_total: runtimeStatusSingleflightReusedTotal,
|
|
2188
|
+
timeout_total: runtimeStatusProbeTimeoutTotal,
|
|
2189
|
+
},
|
|
2190
|
+
runtime_heartbeat: buildRuntimeHeartbeatSnapshot(),
|
|
1688
2191
|
summary,
|
|
1689
2192
|
bots,
|
|
1690
2193
|
}),
|
|
@@ -1708,6 +2211,18 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1708
2211
|
);
|
|
1709
2212
|
return;
|
|
1710
2213
|
}
|
|
2214
|
+
if (
|
|
2215
|
+
req.method === "GET" &&
|
|
2216
|
+
reqUrl.pathname === "/internal/runtime/logs/download"
|
|
2217
|
+
) {
|
|
2218
|
+
const maxBytesRaw = String(
|
|
2219
|
+
reqUrl.searchParams.get("max_bytes") ||
|
|
2220
|
+
reqUrl.searchParams.get("maxBytes") ||
|
|
2221
|
+
"",
|
|
2222
|
+
).trim();
|
|
2223
|
+
streamRuntimeLogDownload(res, { maxBytesRaw });
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
1711
2226
|
if (
|
|
1712
2227
|
req.method === "GET" &&
|
|
1713
2228
|
reqUrl.pathname === "/internal/runtime/plugins/config-schema"
|
|
@@ -1776,85 +2291,229 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1776
2291
|
);
|
|
1777
2292
|
return;
|
|
1778
2293
|
}
|
|
1779
|
-
|
|
1780
|
-
req.method === "POST" &&
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
);
|
|
1792
|
-
|
|
2294
|
+
const botControlAction =
|
|
2295
|
+
req.method === "POST" && reqUrl.pathname === "/internal/runtime/bot/start"
|
|
2296
|
+
? "start"
|
|
2297
|
+
: req.method === "POST" &&
|
|
2298
|
+
reqUrl.pathname === "/internal/runtime/bot/stop"
|
|
2299
|
+
? "stop"
|
|
2300
|
+
: req.method === "POST" &&
|
|
2301
|
+
reqUrl.pathname === "/internal/runtime/bot/restart"
|
|
2302
|
+
? "restart"
|
|
2303
|
+
: null;
|
|
2304
|
+
if (botControlAction) {
|
|
2305
|
+
const body = await readRequestJson(req);
|
|
2306
|
+
const platform = normalizeRuntimePlatform(body.platform || body.channel);
|
|
2307
|
+
const botAccountId = String(
|
|
2308
|
+
body.bot_account_id || body.account_id || body.accountId || "",
|
|
2309
|
+
).trim();
|
|
2310
|
+
const reason =
|
|
2311
|
+
String(body.reason || "").trim() || `manual_${botControlAction}`;
|
|
2312
|
+
|
|
2313
|
+
if (!platform || !botAccountId) {
|
|
1793
2314
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1794
2315
|
res.end(
|
|
1795
2316
|
JSON.stringify({
|
|
1796
2317
|
ok: false,
|
|
1797
|
-
error: "
|
|
2318
|
+
error: "platform and bot_account_id are required",
|
|
1798
2319
|
}),
|
|
1799
2320
|
);
|
|
1800
2321
|
return;
|
|
1801
2322
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
}
|
|
1814
|
-
if ((pullResult.dropped_fields || []).length > 0) {
|
|
1815
|
-
pullDroppedFields = pullResult.dropped_fields || [];
|
|
1816
|
-
console.log(
|
|
1817
|
-
`[bridges/main] reload dropped fields from pull: ${pullResult.dropped_fields
|
|
1818
|
-
?.map((item) => `${item.channel}/${item.accountId}:${item.field}`)
|
|
1819
|
-
.join(", ")}`,
|
|
1820
|
-
);
|
|
1821
|
-
}
|
|
2323
|
+
|
|
2324
|
+
if (runtimeState === "restarting") {
|
|
2325
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
2326
|
+
res.end(
|
|
2327
|
+
JSON.stringify({
|
|
2328
|
+
ok: false,
|
|
2329
|
+
error:
|
|
2330
|
+
"runtime is restarting, bot control is temporarily unavailable",
|
|
2331
|
+
}),
|
|
2332
|
+
);
|
|
2333
|
+
return;
|
|
1822
2334
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1835
|
-
res.end(
|
|
1836
|
-
JSON.stringify({
|
|
1837
|
-
ok: false,
|
|
1838
|
-
error: `reload failed: ${(err as Error).message}`,
|
|
1839
|
-
}),
|
|
1840
|
-
);
|
|
1841
|
-
return;
|
|
1842
|
-
}
|
|
2335
|
+
|
|
2336
|
+
const channelKey = platformToChannelKey(platform);
|
|
2337
|
+
if (!isChannelEnabled(channelKey)) {
|
|
2338
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2339
|
+
res.end(
|
|
2340
|
+
JSON.stringify({
|
|
2341
|
+
ok: false,
|
|
2342
|
+
error: `channel disabled: ${channelKey}`,
|
|
2343
|
+
}),
|
|
2344
|
+
);
|
|
2345
|
+
return;
|
|
1843
2346
|
}
|
|
1844
|
-
if (
|
|
1845
|
-
|
|
1846
|
-
|
|
2347
|
+
if (!isConfiguredRuntimeAccount(platform, botAccountId)) {
|
|
2348
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2349
|
+
res.end(
|
|
2350
|
+
JSON.stringify({
|
|
2351
|
+
ok: false,
|
|
2352
|
+
error: `bot account not configured: ${platform}/${botAccountId}`,
|
|
2353
|
+
}),
|
|
2354
|
+
);
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const inFlightKey = keyOf(platform, botAccountId);
|
|
2359
|
+
if (botControlInFlight.has(inFlightKey)) {
|
|
2360
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
2361
|
+
res.end(
|
|
2362
|
+
JSON.stringify({
|
|
2363
|
+
ok: false,
|
|
2364
|
+
error: "bot control operation already in progress",
|
|
2365
|
+
platform,
|
|
2366
|
+
bot_account_id: botAccountId,
|
|
2367
|
+
}),
|
|
2368
|
+
);
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
botControlInFlight.add(inFlightKey);
|
|
2372
|
+
|
|
2373
|
+
const control = resolveRuntimeBridgeControl(platform);
|
|
2374
|
+
if (!control) {
|
|
2375
|
+
botControlInFlight.delete(inFlightKey);
|
|
2376
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2377
|
+
res.end(
|
|
2378
|
+
JSON.stringify({
|
|
2379
|
+
ok: false,
|
|
2380
|
+
error: `${platform} bridge control not available`,
|
|
2381
|
+
}),
|
|
2382
|
+
);
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
const operationId = generateOperationId(`bot_${botControlAction}`);
|
|
2387
|
+
const key = keyOf(platform, botAccountId);
|
|
2388
|
+
const previous = runtimeStatusRegistry.get(key);
|
|
2389
|
+
const now = nowIso();
|
|
2390
|
+
|
|
2391
|
+
try {
|
|
2392
|
+
if (botControlAction === "start") {
|
|
2393
|
+
if (typeof control.startAccount !== "function") {
|
|
2394
|
+
res.writeHead(501, { "Content-Type": "application/json" });
|
|
2395
|
+
res.end(
|
|
2396
|
+
JSON.stringify({
|
|
2397
|
+
ok: false,
|
|
2398
|
+
error: `${platform} bridge startAccount not implemented`,
|
|
2399
|
+
}),
|
|
2400
|
+
);
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
upsertRuntimeStatus({
|
|
2404
|
+
platform,
|
|
2405
|
+
bot_account_id: botAccountId,
|
|
2406
|
+
link_status: "connecting",
|
|
2407
|
+
started_at: previous?.started_at || now,
|
|
2408
|
+
last_heartbeat_at: now,
|
|
2409
|
+
last_error: null,
|
|
2410
|
+
reconnect_count: previous?.reconnect_count || 0,
|
|
2411
|
+
last_event: "manual_start_request",
|
|
2412
|
+
status_source: "manual",
|
|
2413
|
+
last_probe_at: previous?.last_probe_at || null,
|
|
2414
|
+
});
|
|
2415
|
+
await control.startAccount(botAccountId);
|
|
2416
|
+
} else if (botControlAction === "stop") {
|
|
2417
|
+
if (typeof control.stopAccount !== "function") {
|
|
2418
|
+
res.writeHead(501, { "Content-Type": "application/json" });
|
|
2419
|
+
res.end(
|
|
2420
|
+
JSON.stringify({
|
|
2421
|
+
ok: false,
|
|
2422
|
+
error: `${platform} bridge stopAccount not implemented`,
|
|
2423
|
+
}),
|
|
2424
|
+
);
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
await control.stopAccount(botAccountId);
|
|
2428
|
+
upsertRuntimeStatus({
|
|
2429
|
+
platform,
|
|
2430
|
+
bot_account_id: botAccountId,
|
|
2431
|
+
link_status: "disconnected",
|
|
2432
|
+
started_at: previous?.started_at || now,
|
|
2433
|
+
last_heartbeat_at: now,
|
|
2434
|
+
last_error: null,
|
|
2435
|
+
reconnect_count: previous?.reconnect_count || 0,
|
|
2436
|
+
last_event: "manual_stop_request",
|
|
2437
|
+
status_source: "manual",
|
|
2438
|
+
last_probe_at: previous?.last_probe_at || null,
|
|
2439
|
+
});
|
|
2440
|
+
} else {
|
|
2441
|
+
if (typeof control.restartAccount !== "function") {
|
|
2442
|
+
res.writeHead(501, { "Content-Type": "application/json" });
|
|
2443
|
+
res.end(
|
|
2444
|
+
JSON.stringify({
|
|
2445
|
+
ok: false,
|
|
2446
|
+
error: `${platform} bridge restartAccount not implemented`,
|
|
2447
|
+
}),
|
|
2448
|
+
);
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
upsertRuntimeStatus({
|
|
2452
|
+
platform,
|
|
2453
|
+
bot_account_id: botAccountId,
|
|
2454
|
+
link_status: "connecting",
|
|
2455
|
+
started_at: previous?.started_at || now,
|
|
2456
|
+
last_heartbeat_at: now,
|
|
2457
|
+
last_error: null,
|
|
2458
|
+
reconnect_count: previous?.reconnect_count || 0,
|
|
2459
|
+
last_event: "manual_restart_request",
|
|
2460
|
+
status_source: "manual",
|
|
2461
|
+
last_probe_at: previous?.last_probe_at || null,
|
|
2462
|
+
});
|
|
2463
|
+
await control.restartAccount(botAccountId);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
const bot = runtimeStatusRegistry.get(key) || null;
|
|
2467
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2468
|
+
res.end(
|
|
2469
|
+
JSON.stringify({
|
|
2470
|
+
ok: true,
|
|
2471
|
+
status: "accepted",
|
|
2472
|
+
action: botControlAction,
|
|
2473
|
+
operation_id: operationId,
|
|
2474
|
+
platform,
|
|
2475
|
+
bot_account_id: botAccountId,
|
|
2476
|
+
reason,
|
|
2477
|
+
runtime_instance_id: runtimeInstanceId,
|
|
2478
|
+
runtime_state: runtimeState,
|
|
2479
|
+
issued_at: now,
|
|
2480
|
+
bot,
|
|
2481
|
+
}),
|
|
2482
|
+
);
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
const message =
|
|
2485
|
+
err instanceof Error ? err.message : "bot control failed";
|
|
2486
|
+
upsertRuntimeStatus({
|
|
2487
|
+
platform,
|
|
2488
|
+
bot_account_id: botAccountId,
|
|
2489
|
+
link_status: "error",
|
|
2490
|
+
started_at: previous?.started_at || now,
|
|
2491
|
+
last_heartbeat_at: now,
|
|
2492
|
+
last_error: message,
|
|
2493
|
+
reconnect_count: previous?.reconnect_count || 0,
|
|
2494
|
+
last_event: `manual_${botControlAction}_failed`,
|
|
2495
|
+
status_source: "manual",
|
|
2496
|
+
last_probe_at: previous?.last_probe_at || null,
|
|
2497
|
+
});
|
|
2498
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2499
|
+
res.end(
|
|
2500
|
+
JSON.stringify({
|
|
2501
|
+
ok: false,
|
|
2502
|
+
status: "fail",
|
|
2503
|
+
action: botControlAction,
|
|
2504
|
+
operation_id: operationId,
|
|
2505
|
+
platform,
|
|
2506
|
+
bot_account_id: botAccountId,
|
|
2507
|
+
reason,
|
|
2508
|
+
error: message,
|
|
2509
|
+
runtime_instance_id: runtimeInstanceId,
|
|
2510
|
+
runtime_state: runtimeState,
|
|
2511
|
+
issued_at: now,
|
|
2512
|
+
}),
|
|
2513
|
+
);
|
|
2514
|
+
} finally {
|
|
2515
|
+
botControlInFlight.delete(inFlightKey);
|
|
1847
2516
|
}
|
|
1848
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1849
|
-
res.end(
|
|
1850
|
-
JSON.stringify({
|
|
1851
|
-
ok: true,
|
|
1852
|
-
reloaded: true,
|
|
1853
|
-
version: configVersionHash || "unknown",
|
|
1854
|
-
updated_at: configVersionUpdatedAt,
|
|
1855
|
-
dropped_fields: pullDroppedFields,
|
|
1856
|
-
}),
|
|
1857
|
-
);
|
|
1858
2517
|
return;
|
|
1859
2518
|
}
|
|
1860
2519
|
if (
|
|
@@ -1864,6 +2523,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1864
2523
|
const body = await readRequestJson(req);
|
|
1865
2524
|
const config = body.config as Record<string, unknown> | undefined;
|
|
1866
2525
|
const persist = body.persist !== false;
|
|
2526
|
+
const requestedVersion = String(body.version || "").trim();
|
|
1867
2527
|
if (!config || typeof config !== "object") {
|
|
1868
2528
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1869
2529
|
res.end(JSON.stringify({ ok: false, error: "config is required" }));
|
|
@@ -1893,9 +2553,13 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1893
2553
|
loadedConfigForRuntime,
|
|
1894
2554
|
loadedConfigPathForRuntime,
|
|
1895
2555
|
);
|
|
1896
|
-
updateConfigVersion(
|
|
2556
|
+
updateConfigVersion(
|
|
2557
|
+
normalizedResult.normalized,
|
|
2558
|
+
requestedVersion || undefined,
|
|
2559
|
+
);
|
|
2560
|
+
applyRemoteRuntimeConfigToPluginHome();
|
|
1897
2561
|
markConfiguredBotsAsConnecting(normalizedResult.normalized);
|
|
1898
|
-
await probeAndRefreshStatuses();
|
|
2562
|
+
await probeAndRefreshStatuses({ force: true, reason: "config_apply" });
|
|
1899
2563
|
|
|
1900
2564
|
if (persist) {
|
|
1901
2565
|
const targetPath =
|
|
@@ -1929,6 +2593,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1929
2593
|
status: "applied",
|
|
1930
2594
|
persisted: persist,
|
|
1931
2595
|
configPath: loadedConfigPathForRuntime,
|
|
2596
|
+
version: configVersionHash || "unknown",
|
|
1932
2597
|
}),
|
|
1933
2598
|
);
|
|
1934
2599
|
return;
|
|
@@ -1937,6 +2602,31 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
1937
2602
|
req.method === "POST" &&
|
|
1938
2603
|
reqUrl.pathname === "/internal/runtime/restart-all"
|
|
1939
2604
|
) {
|
|
2605
|
+
if (runtimeState === "restarting") {
|
|
2606
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
2607
|
+
res.end(
|
|
2608
|
+
JSON.stringify({
|
|
2609
|
+
ok: false,
|
|
2610
|
+
status: "busy",
|
|
2611
|
+
error: "runtime is already restarting",
|
|
2612
|
+
runtime_instance_id: runtimeInstanceId,
|
|
2613
|
+
}),
|
|
2614
|
+
);
|
|
2615
|
+
return;
|
|
2616
|
+
}
|
|
2617
|
+
if (botControlInFlight.size > 0) {
|
|
2618
|
+
res.writeHead(409, { "Content-Type": "application/json" });
|
|
2619
|
+
res.end(
|
|
2620
|
+
JSON.stringify({
|
|
2621
|
+
ok: false,
|
|
2622
|
+
status: "busy",
|
|
2623
|
+
error: "bot control operations are in progress",
|
|
2624
|
+
in_flight_count: botControlInFlight.size,
|
|
2625
|
+
runtime_instance_id: runtimeInstanceId,
|
|
2626
|
+
}),
|
|
2627
|
+
);
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
1940
2630
|
const body = await readRequestJson(req);
|
|
1941
2631
|
const restartModeRaw = String(body.mode || "")
|
|
1942
2632
|
.trim()
|
|
@@ -2277,12 +2967,14 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
2277
2967
|
const platform = String(body.platform || "").trim();
|
|
2278
2968
|
const botAccountId = String(body.bot_account_id || "").trim();
|
|
2279
2969
|
const linkStatus = String(body.link_status || "").trim();
|
|
2970
|
+
const eventName = String(body.event_name || "").trim() || null;
|
|
2971
|
+
const isHeartbeatEvent = isHeartbeatOnlyEvent(eventName);
|
|
2280
2972
|
if (
|
|
2281
2973
|
(platform !== "dingtalk" &&
|
|
2282
2974
|
platform !== "feishu" &&
|
|
2283
2975
|
platform !== "weixin") ||
|
|
2284
2976
|
!botAccountId ||
|
|
2285
|
-
!linkStatus
|
|
2977
|
+
(!isHeartbeatEvent && !linkStatus)
|
|
2286
2978
|
) {
|
|
2287
2979
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2288
2980
|
res.end(JSON.stringify({ ok: false, error: "invalid payload" }));
|
|
@@ -2303,6 +2995,18 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
2303
2995
|
);
|
|
2304
2996
|
return;
|
|
2305
2997
|
}
|
|
2998
|
+
const heartbeatAt = String(body.last_heartbeat_at || nowIso());
|
|
2999
|
+
upsertRuntimeHeartbeat({
|
|
3000
|
+
platform: p,
|
|
3001
|
+
bot_account_id: botAccountId,
|
|
3002
|
+
last_heartbeat_at: heartbeatAt,
|
|
3003
|
+
last_event: eventName,
|
|
3004
|
+
});
|
|
3005
|
+
if (isHeartbeatEvent) {
|
|
3006
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3007
|
+
res.end(JSON.stringify({ ok: true, heartbeat_only: true }));
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
2306
3010
|
const normalizedStatus = [
|
|
2307
3011
|
"connecting",
|
|
2308
3012
|
"connected",
|
|
@@ -2313,7 +3017,6 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
2313
3017
|
? (linkStatus as RuntimeBotStatus["link_status"])
|
|
2314
3018
|
: "degraded";
|
|
2315
3019
|
const prev = runtimeStatusRegistry.get(keyOf(p, botAccountId));
|
|
2316
|
-
const eventName = String(body.event_name || "").trim() || null;
|
|
2317
3020
|
const reconnectCountRaw = body.reconnect_count;
|
|
2318
3021
|
const reconnectCountFromBody =
|
|
2319
3022
|
typeof reconnectCountRaw === "number"
|
|
@@ -2330,7 +3033,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
2330
3033
|
bot_account_id: botAccountId,
|
|
2331
3034
|
link_status: normalizedStatus,
|
|
2332
3035
|
started_at: String(body.started_at || prev?.started_at || nowIso()),
|
|
2333
|
-
last_heartbeat_at:
|
|
3036
|
+
last_heartbeat_at: heartbeatAt,
|
|
2334
3037
|
last_error: String(body.last_error || "") || null,
|
|
2335
3038
|
reconnect_count: reconnectCount,
|
|
2336
3039
|
last_event: eventName,
|
|
@@ -2484,7 +3187,7 @@ function printConfigBootstrapGuide(): void {
|
|
|
2484
3187
|
}
|
|
2485
3188
|
|
|
2486
3189
|
async function main(): Promise<void> {
|
|
2487
|
-
debugger
|
|
3190
|
+
debugger;
|
|
2488
3191
|
const logFilePath = setupBridgeLogger("bridges-main");
|
|
2489
3192
|
console.log("[bridges/main] log file:", logFilePath);
|
|
2490
3193
|
|