ylib-syim 0.0.21 → 0.0.22
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/dingtalk-stdio-bridge.ts +12 -9
- package/bridges/lark-stdio-bridge.ts +12 -9
- package/bridges/logger.ts +62 -5
- package/bridges/main.ts +944 -212
- package/bridges/runtime/http-control-server.ts +18 -2
- package/bridges/runtime-event-reporter.ts +54 -11
- package/bridges/weixin-stdio-bridge.ts +12 -9
- package/package.json +4 -4
- package/scripts/dingtalk-stdio-bridge.ts +67 -16
- package/scripts/lark-stdio-bridge.ts +71 -16
- package/scripts/weixin-stdio-bridge.ts +68 -17
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
2
|
|
|
3
3
|
export function createHttpControlServer(
|
|
4
|
-
handler: (
|
|
4
|
+
handler: (
|
|
5
|
+
req: http.IncomingMessage,
|
|
6
|
+
res: http.ServerResponse,
|
|
7
|
+
) => void | Promise<void>,
|
|
5
8
|
): http.Server {
|
|
6
|
-
return http.createServer(
|
|
9
|
+
return http.createServer((req, res) => {
|
|
10
|
+
Promise.resolve(handler(req, res)).catch((err) => {
|
|
11
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
12
|
+
console.error(`[http-control-server] unhandled error: ${message}`);
|
|
13
|
+
if (res.headersSent) {
|
|
14
|
+
if (!res.writableEnded) {
|
|
15
|
+
res.end();
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
20
|
+
res.end(JSON.stringify({ ok: false, error: message }));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
7
23
|
}
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { writeBridgeStructuredLog } from "./logger.ts";
|
|
4
5
|
|
|
5
6
|
// runtime-event-reporter 负责把 bridge 侧 lifecycle/console 信号
|
|
6
7
|
// 统一包装为 main.ts 可消费的 runtime event,避免各 bridge 自行拼装上报逻辑。
|
|
7
8
|
type Platform = "dingtalk" | "feishu" | "weixin";
|
|
8
9
|
const runtimeEventAliasLogMemo = new Set<string>();
|
|
10
|
+
const runtimeEventWarnMemo = new Map<string, number>();
|
|
9
11
|
// 保留原生 console 引用,避免调试日志被 console hook 二次识别。
|
|
10
12
|
const runtimeEventRawConsoleLog = console.log.bind(console);
|
|
11
13
|
// reporter 调试日志开关:默认关闭,排障时启用。
|
|
12
14
|
const runtimeEventVerboseLog = process.env.RUNTIME_EVENT_VERBOSE_LOG === "1";
|
|
15
|
+
const runtimeEventWarnCooldownMs = Math.max(
|
|
16
|
+
1000,
|
|
17
|
+
Number(process.env.RUNTIME_EVENT_WARN_COOLDOWN_MS || "60000") || 60000,
|
|
18
|
+
);
|
|
13
19
|
|
|
14
20
|
// logRuntimeEventDebug: 受控输出 reporter 调试日志。
|
|
15
21
|
function logRuntimeEventDebug(message: string): void {
|
|
@@ -17,6 +23,16 @@ function logRuntimeEventDebug(message: string): void {
|
|
|
17
23
|
runtimeEventRawConsoleLog(`[runtime-event-reporter][debug] ${message}`);
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
function logRuntimeEventWarning(key: string, message: string): void {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const prevAt = runtimeEventWarnMemo.get(key) || 0;
|
|
29
|
+
if (now - prevAt < runtimeEventWarnCooldownMs) return;
|
|
30
|
+
runtimeEventWarnMemo.set(key, now);
|
|
31
|
+
const line = `[runtime-event-reporter][warn] ${message}`;
|
|
32
|
+
writeBridgeStructuredLog("warn", line);
|
|
33
|
+
runtimeEventRawConsoleLog(line);
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
// logSingleAccountAlias: 记录单账号 alias 映射日志(带去重)。
|
|
21
37
|
function logSingleAccountAlias(
|
|
22
38
|
platform: Platform,
|
|
@@ -179,7 +195,13 @@ export async function reportRuntimeEvent(
|
|
|
179
195
|
): Promise<void> {
|
|
180
196
|
// 统一事件上报出口:所有 bridge 生命周期与日志信号都经此入站 main.ts。
|
|
181
197
|
const token = (process.env.INTERNAL_FIXED_TOKEN || "").trim();
|
|
182
|
-
if (!token)
|
|
198
|
+
if (!token) {
|
|
199
|
+
logRuntimeEventWarning(
|
|
200
|
+
"missing_internal_token",
|
|
201
|
+
"skip runtime event report: INTERNAL_FIXED_TOKEN is empty",
|
|
202
|
+
);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
183
205
|
const port = Number(process.env.INTERNAL_CONTROL_PORT || "18999");
|
|
184
206
|
const emittedAt = new Date().toISOString();
|
|
185
207
|
const payload = {
|
|
@@ -197,23 +219,44 @@ export async function reportRuntimeEvent(
|
|
|
197
219
|
`report event platform=${platform} account=${accountId} status=${linkStatus} event=${String(extra?.eventName || "")}`,
|
|
198
220
|
);
|
|
199
221
|
try {
|
|
200
|
-
if (typeof fetch !== "function")
|
|
222
|
+
if (typeof fetch !== "function") {
|
|
223
|
+
logRuntimeEventWarning(
|
|
224
|
+
"fetch_unavailable",
|
|
225
|
+
"skip runtime event report: global fetch is unavailable",
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
201
229
|
const controller = new AbortController();
|
|
202
230
|
const timeout = setTimeout(() => controller.abort(), 1500);
|
|
203
231
|
try {
|
|
204
|
-
await fetch(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
232
|
+
const response = await fetch(
|
|
233
|
+
`http://127.0.0.1:${port}/internal/runtime/events/report`,
|
|
234
|
+
{
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
Authorization: `Bearer ${token}`,
|
|
238
|
+
"Content-Type": "application/json",
|
|
239
|
+
},
|
|
240
|
+
body: JSON.stringify(payload),
|
|
241
|
+
signal: controller.signal,
|
|
209
242
|
},
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
243
|
+
);
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
logRuntimeEventWarning(
|
|
246
|
+
`report_http_status_${response.status}`,
|
|
247
|
+
`runtime event report response not ok status=${response.status} platform=${platform} account=${accountId} event=${String(extra?.eventName || "")}`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
213
250
|
} finally {
|
|
214
251
|
clearTimeout(timeout);
|
|
215
252
|
}
|
|
216
|
-
} catch {
|
|
253
|
+
} catch (err) {
|
|
254
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
255
|
+
logRuntimeEventWarning(
|
|
256
|
+
"report_runtime_event_exception",
|
|
257
|
+
`runtime event report request failed platform=${platform} account=${accountId} event=${String(extra?.eventName || "")} err=${errMessage}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
217
260
|
}
|
|
218
261
|
|
|
219
262
|
// wireBridgeLifecycleReporter: 绑定生命周期事件并上报基础状态。
|
|
@@ -52,15 +52,18 @@ process.on("unhandledRejection", (reason) => {
|
|
|
52
52
|
| ((payload: Record<string, unknown>) => void)
|
|
53
53
|
| undefined;
|
|
54
54
|
const nowIso = new Date().toISOString();
|
|
55
|
-
sink
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
if (sink) {
|
|
56
|
+
sink({
|
|
57
|
+
platform: "weixin",
|
|
58
|
+
bot_account_id: payload.accountId,
|
|
59
|
+
link_status: payload.linkStatus,
|
|
60
|
+
last_error: payload.lastError || null,
|
|
61
|
+
status_source: "event",
|
|
62
|
+
last_event_at: nowIso,
|
|
63
|
+
last_heartbeat_at: nowIso,
|
|
64
|
+
});
|
|
65
|
+
return Promise.resolve({ ok: true, transport: "in_process_sink" });
|
|
66
|
+
}
|
|
64
67
|
} catch {
|
|
65
68
|
// ignore
|
|
66
69
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-syim",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@ffmpeg-installer/ffmpeg": "^1.1.0",
|
|
49
|
-
"ylib-dingtalk-connector": "0.7.10-beta.
|
|
50
|
-
"ylib-openclaw-lark": "2026.3.17-beta.
|
|
51
|
-
"ylib-openclaw-weixin": "2.1.7-beta.
|
|
49
|
+
"ylib-dingtalk-connector": "0.7.10-beta.14",
|
|
50
|
+
"ylib-openclaw-lark": "2026.3.17-beta.20",
|
|
51
|
+
"ylib-openclaw-weixin": "2.1.7-beta.6",
|
|
52
52
|
"axios": "^1.6.0",
|
|
53
53
|
"dingtalk-stream": "^2.1.4",
|
|
54
54
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -62,6 +62,10 @@ function emitRuntimeEvent(
|
|
|
62
62
|
lastError?: string | null;
|
|
63
63
|
}) => Promise<unknown>)
|
|
64
64
|
| undefined;
|
|
65
|
+
if (reporter) {
|
|
66
|
+
void reporter({ accountId, linkStatus, lastError });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
65
69
|
const nowIso = new Date().toISOString();
|
|
66
70
|
sink?.({
|
|
67
71
|
platform: "dingtalk",
|
|
@@ -72,8 +76,6 @@ function emitRuntimeEvent(
|
|
|
72
76
|
last_event_at: nowIso,
|
|
73
77
|
last_heartbeat_at: nowIso,
|
|
74
78
|
});
|
|
75
|
-
if (!reporter) return;
|
|
76
|
-
void reporter({ accountId, linkStatus, lastError });
|
|
77
79
|
} catch {}
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -304,14 +306,36 @@ function channelsForLog(
|
|
|
304
306
|
|
|
305
307
|
/** 加载 syim.json,与 connector-host 一致 */
|
|
306
308
|
function loadOpenClawConfig(): Record<string, unknown> | null {
|
|
309
|
+
const runtimeConfig = (globalThis as Record<string, unknown>)
|
|
310
|
+
.__IM_RUNTIME_CONFIG__;
|
|
311
|
+
if (
|
|
312
|
+
runtimeConfig &&
|
|
313
|
+
typeof runtimeConfig === "object" &&
|
|
314
|
+
!Array.isArray(runtimeConfig)
|
|
315
|
+
) {
|
|
316
|
+
console.log(
|
|
317
|
+
"[dingtalk-stdio-bridge] 从 runtime 内存配置加载,跳过本地 syim fallback",
|
|
318
|
+
);
|
|
319
|
+
try {
|
|
320
|
+
return JSON.parse(JSON.stringify(runtimeConfig)) as Record<string, unknown>;
|
|
321
|
+
} catch {
|
|
322
|
+
return { ...(runtimeConfig as Record<string, unknown>) };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
307
325
|
const runtimeConfigPath = String(
|
|
308
326
|
(globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG_PATH__ || "",
|
|
309
327
|
).trim();
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
328
|
+
const envConfigPath = String(process.env.OPENCLAW_CONFIG_PATH || "").trim();
|
|
329
|
+
const configPaths = Array.from(
|
|
330
|
+
new Set(
|
|
331
|
+
[
|
|
332
|
+
envConfigPath,
|
|
333
|
+
runtimeConfigPath,
|
|
334
|
+
path.join(os.homedir(), ".syim", "syim.json"),
|
|
335
|
+
path.join(getProjectRoot(), "syim.json"),
|
|
336
|
+
].filter((p) => Boolean(p)),
|
|
337
|
+
),
|
|
338
|
+
);
|
|
315
339
|
for (const configPath of configPaths) {
|
|
316
340
|
if (!fs.existsSync(configPath)) continue;
|
|
317
341
|
try {
|
|
@@ -507,6 +531,30 @@ const bridgeLifecycleLog = {
|
|
|
507
531
|
warn: (msg: string) => console.warn("[DingTalk]", msg),
|
|
508
532
|
error: (msg: string) => console.error("[DingTalk]", msg),
|
|
509
533
|
};
|
|
534
|
+
const bridgeAccountControlTimeoutMs = Math.max(
|
|
535
|
+
1_000,
|
|
536
|
+
Number(process.env.RUNTIME_ACCOUNT_CONTROL_TIMEOUT_MS || "10000") || 10000,
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
async function withBridgeControlTimeout<T>(
|
|
540
|
+
work: Promise<T>,
|
|
541
|
+
timeoutMessage: string,
|
|
542
|
+
): Promise<T> {
|
|
543
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
544
|
+
try {
|
|
545
|
+
return await Promise.race([
|
|
546
|
+
work,
|
|
547
|
+
new Promise<T>((_, reject) => {
|
|
548
|
+
timer = setTimeout(
|
|
549
|
+
() => reject(new Error(timeoutMessage)),
|
|
550
|
+
bridgeAccountControlTimeoutMs,
|
|
551
|
+
);
|
|
552
|
+
}),
|
|
553
|
+
]);
|
|
554
|
+
} finally {
|
|
555
|
+
if (timer) clearTimeout(timer);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
510
558
|
|
|
511
559
|
function resolveBridgeAccount(accountId: string): ResolvedAccount | null {
|
|
512
560
|
if (!bridgeCfg) return null;
|
|
@@ -623,15 +671,18 @@ async function stopSingleAccount(
|
|
|
623
671
|
|
|
624
672
|
if (typeof bridgeStopAccount === "function" && bridgeCfg) {
|
|
625
673
|
try {
|
|
626
|
-
await
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
674
|
+
await withBridgeControlTimeout(
|
|
675
|
+
Promise.resolve(
|
|
676
|
+
bridgeStopAccount({
|
|
677
|
+
cfg: bridgeCfg,
|
|
678
|
+
accountId: effectiveAccountId,
|
|
679
|
+
account,
|
|
680
|
+
log: bridgeLifecycleLog,
|
|
681
|
+
setStatus: (patch: Record<string, unknown>) =>
|
|
682
|
+
applyBridgeStatusPatch(effectiveAccountId, patch),
|
|
683
|
+
}),
|
|
684
|
+
),
|
|
685
|
+
`dingtalk stop hook timeout account=${effectiveAccountId}`,
|
|
635
686
|
);
|
|
636
687
|
} catch (err) {
|
|
637
688
|
const detail = toDetailedErrorText(err);
|
|
@@ -71,6 +71,10 @@ function emitRuntimeEvent(
|
|
|
71
71
|
lastError?: string | null;
|
|
72
72
|
}) => Promise<unknown>)
|
|
73
73
|
| undefined;
|
|
74
|
+
if (reporter) {
|
|
75
|
+
void reporter({ accountId, linkStatus, lastError });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
74
78
|
const nowIso = new Date().toISOString();
|
|
75
79
|
sink?.({
|
|
76
80
|
platform: "feishu",
|
|
@@ -81,8 +85,6 @@ function emitRuntimeEvent(
|
|
|
81
85
|
last_event_at: nowIso,
|
|
82
86
|
last_heartbeat_at: nowIso,
|
|
83
87
|
});
|
|
84
|
-
if (!reporter) return;
|
|
85
|
-
void reporter({ accountId, linkStatus, lastError });
|
|
86
88
|
} catch {}
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -321,10 +323,36 @@ function channelsForLog(
|
|
|
321
323
|
// ---------------------------------------------------------------------------
|
|
322
324
|
|
|
323
325
|
function loadOpenClawConfig(): Record<string, unknown> | null {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
const runtimeConfig = (globalThis as Record<string, unknown>)
|
|
327
|
+
.__IM_RUNTIME_CONFIG__;
|
|
328
|
+
if (
|
|
329
|
+
runtimeConfig &&
|
|
330
|
+
typeof runtimeConfig === "object" &&
|
|
331
|
+
!Array.isArray(runtimeConfig)
|
|
332
|
+
) {
|
|
333
|
+
console.log(
|
|
334
|
+
"[lark-stdio-bridge] 从 runtime 内存配置加载,跳过本地 syim fallback",
|
|
335
|
+
);
|
|
336
|
+
try {
|
|
337
|
+
return JSON.parse(JSON.stringify(runtimeConfig)) as Record<string, unknown>;
|
|
338
|
+
} catch {
|
|
339
|
+
return { ...(runtimeConfig as Record<string, unknown>) };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const runtimeConfigPath = String(
|
|
343
|
+
(globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG_PATH__ || "",
|
|
344
|
+
).trim();
|
|
345
|
+
const envConfigPath = String(process.env.OPENCLAW_CONFIG_PATH || "").trim();
|
|
346
|
+
const configPaths = Array.from(
|
|
347
|
+
new Set(
|
|
348
|
+
[
|
|
349
|
+
envConfigPath,
|
|
350
|
+
runtimeConfigPath,
|
|
351
|
+
path.join(os.homedir(), ".syim", "syim.json"),
|
|
352
|
+
path.join(getProjectRoot(), "syim.json"),
|
|
353
|
+
].filter((p) => Boolean(p)),
|
|
354
|
+
),
|
|
355
|
+
);
|
|
328
356
|
for (const configPath of configPaths) {
|
|
329
357
|
if (!fs.existsSync(configPath)) continue;
|
|
330
358
|
try {
|
|
@@ -584,6 +612,30 @@ const bridgeLifecycleLog = {
|
|
|
584
612
|
warn: (msg: string) => console.warn("[Feishu]", msg),
|
|
585
613
|
error: (msg: string) => console.error("[Feishu][ERR]", msg),
|
|
586
614
|
};
|
|
615
|
+
const bridgeAccountControlTimeoutMs = Math.max(
|
|
616
|
+
1_000,
|
|
617
|
+
Number(process.env.RUNTIME_ACCOUNT_CONTROL_TIMEOUT_MS || "10000") || 10000,
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
async function withBridgeControlTimeout<T>(
|
|
621
|
+
work: Promise<T>,
|
|
622
|
+
timeoutMessage: string,
|
|
623
|
+
): Promise<T> {
|
|
624
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
625
|
+
try {
|
|
626
|
+
return await Promise.race([
|
|
627
|
+
work,
|
|
628
|
+
new Promise<T>((_, reject) => {
|
|
629
|
+
timer = setTimeout(
|
|
630
|
+
() => reject(new Error(timeoutMessage)),
|
|
631
|
+
bridgeAccountControlTimeoutMs,
|
|
632
|
+
);
|
|
633
|
+
}),
|
|
634
|
+
]);
|
|
635
|
+
} finally {
|
|
636
|
+
if (timer) clearTimeout(timer);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
587
639
|
|
|
588
640
|
function ensureGatewayBaseOnCfg(
|
|
589
641
|
cfg: Record<string, unknown>,
|
|
@@ -788,16 +840,19 @@ async function stopSingleAccount(
|
|
|
788
840
|
|
|
789
841
|
if (typeof bridgeStopAccount === "function" && currentCfg) {
|
|
790
842
|
try {
|
|
791
|
-
await
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
843
|
+
await withBridgeControlTimeout(
|
|
844
|
+
Promise.resolve(
|
|
845
|
+
bridgeStopAccount({
|
|
846
|
+
cfg: currentCfg,
|
|
847
|
+
accountId: effectiveAccountId,
|
|
848
|
+
account,
|
|
849
|
+
runtime: currentRuntime,
|
|
850
|
+
log: bridgeLifecycleLog,
|
|
851
|
+
setStatus: (patch: Record<string, unknown>) =>
|
|
852
|
+
applyBridgeStatusPatch(effectiveAccountId, patch),
|
|
853
|
+
}),
|
|
854
|
+
),
|
|
855
|
+
`feishu stop hook timeout account=${effectiveAccountId}`,
|
|
801
856
|
);
|
|
802
857
|
} catch (err) {
|
|
803
858
|
const detail = toDetailedErrorText(err);
|
|
@@ -64,6 +64,10 @@ function emitRuntimeEvent(
|
|
|
64
64
|
lastError?: string | null;
|
|
65
65
|
}) => Promise<unknown>)
|
|
66
66
|
| undefined;
|
|
67
|
+
if (reporter) {
|
|
68
|
+
void reporter({ accountId, linkStatus, lastError });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
67
71
|
const nowIso = new Date().toISOString();
|
|
68
72
|
sink?.({
|
|
69
73
|
platform: "weixin",
|
|
@@ -74,8 +78,6 @@ function emitRuntimeEvent(
|
|
|
74
78
|
last_event_at: nowIso,
|
|
75
79
|
last_heartbeat_at: nowIso,
|
|
76
80
|
});
|
|
77
|
-
if (!reporter) return;
|
|
78
|
-
void reporter({ accountId, linkStatus, lastError });
|
|
79
81
|
} catch {
|
|
80
82
|
// ignore
|
|
81
83
|
}
|
|
@@ -278,14 +280,36 @@ function normalizeConfiguredAccountIds(ids: string[]): string[] {
|
|
|
278
280
|
}
|
|
279
281
|
|
|
280
282
|
function loadOpenClawConfig(): Record<string, unknown> | null {
|
|
283
|
+
const runtimeConfig = (globalThis as Record<string, unknown>)
|
|
284
|
+
.__IM_RUNTIME_CONFIG__;
|
|
285
|
+
if (
|
|
286
|
+
runtimeConfig &&
|
|
287
|
+
typeof runtimeConfig === "object" &&
|
|
288
|
+
!Array.isArray(runtimeConfig)
|
|
289
|
+
) {
|
|
290
|
+
console.log(
|
|
291
|
+
"[weixin-stdio-bridge] 从 runtime 内存配置加载,跳过本地 syim fallback",
|
|
292
|
+
);
|
|
293
|
+
try {
|
|
294
|
+
return JSON.parse(JSON.stringify(runtimeConfig)) as Record<string, unknown>;
|
|
295
|
+
} catch {
|
|
296
|
+
return { ...(runtimeConfig as Record<string, unknown>) };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
281
299
|
const runtimeConfigPath = String(
|
|
282
300
|
(globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG_PATH__ || "",
|
|
283
301
|
).trim();
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
302
|
+
const envConfigPath = String(process.env.OPENCLAW_CONFIG_PATH || "").trim();
|
|
303
|
+
const configPaths = Array.from(
|
|
304
|
+
new Set(
|
|
305
|
+
[
|
|
306
|
+
envConfigPath,
|
|
307
|
+
runtimeConfigPath,
|
|
308
|
+
path.join(os.homedir(), ".syim", "syim.json"),
|
|
309
|
+
path.join(getProjectRoot(), "syim.json"),
|
|
310
|
+
].filter((p) => Boolean(p)),
|
|
311
|
+
),
|
|
312
|
+
);
|
|
289
313
|
|
|
290
314
|
for (const configPath of configPaths) {
|
|
291
315
|
if (!fs.existsSync(configPath)) continue;
|
|
@@ -512,6 +536,30 @@ const bridgeLifecycleLog = {
|
|
|
512
536
|
warn: (msg: string) => console.warn("[Weixin]", msg),
|
|
513
537
|
error: (msg: string) => console.error("[Weixin][ERR]", msg),
|
|
514
538
|
};
|
|
539
|
+
const bridgeAccountControlTimeoutMs = Math.max(
|
|
540
|
+
1_000,
|
|
541
|
+
Number(process.env.RUNTIME_ACCOUNT_CONTROL_TIMEOUT_MS || "10000") || 10000,
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
async function withBridgeControlTimeout<T>(
|
|
545
|
+
work: Promise<T>,
|
|
546
|
+
timeoutMessage: string,
|
|
547
|
+
): Promise<T> {
|
|
548
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
549
|
+
try {
|
|
550
|
+
return await Promise.race([
|
|
551
|
+
work,
|
|
552
|
+
new Promise<T>((_, reject) => {
|
|
553
|
+
timer = setTimeout(
|
|
554
|
+
() => reject(new Error(timeoutMessage)),
|
|
555
|
+
bridgeAccountControlTimeoutMs,
|
|
556
|
+
);
|
|
557
|
+
}),
|
|
558
|
+
]);
|
|
559
|
+
} finally {
|
|
560
|
+
if (timer) clearTimeout(timer);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
515
563
|
|
|
516
564
|
function resolveBridgeAccount(accountId: string): ResolvedAccount | null {
|
|
517
565
|
if (!bridgeCfg) return null;
|
|
@@ -600,7 +648,7 @@ async function startSingleAccount(
|
|
|
600
648
|
account,
|
|
601
649
|
abortSignal: abort.signal,
|
|
602
650
|
log: bridgeLifecycleLog,
|
|
603
|
-
runtime: bridgeLifecycleLog,
|
|
651
|
+
runtime: bridgeRuntimeForGateway || bridgeLifecycleLog,
|
|
604
652
|
setStatus: (patch: Record<string, unknown>) =>
|
|
605
653
|
applyBridgeStatusPatch(account.accountId, patch),
|
|
606
654
|
})
|
|
@@ -634,15 +682,18 @@ async function stopSingleAccount(
|
|
|
634
682
|
|
|
635
683
|
if (typeof bridgeStopAccount === "function" && bridgeCfg) {
|
|
636
684
|
try {
|
|
637
|
-
await
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
685
|
+
await withBridgeControlTimeout(
|
|
686
|
+
Promise.resolve(
|
|
687
|
+
bridgeStopAccount({
|
|
688
|
+
cfg: bridgeCfg,
|
|
689
|
+
accountId: effectiveAccountId,
|
|
690
|
+
account,
|
|
691
|
+
log: bridgeLifecycleLog,
|
|
692
|
+
setStatus: (patch: Record<string, unknown>) =>
|
|
693
|
+
applyBridgeStatusPatch(effectiveAccountId, patch),
|
|
694
|
+
}),
|
|
695
|
+
),
|
|
696
|
+
`weixin stop hook timeout account=${effectiveAccountId}`,
|
|
646
697
|
);
|
|
647
698
|
} catch (err) {
|
|
648
699
|
const detail = toDetailedErrorText(err);
|