ylib-syim 0.0.45 → 0.0.47

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 CHANGED
@@ -32,6 +32,9 @@ process.env.TZ = "Asia/Shanghai";
32
32
  const bridgeDir = path.dirname(fileURLToPath(import.meta.url));
33
33
  const packageRoot = path.resolve(bridgeDir, "..");
34
34
  const requireAtBridge = createRequire(import.meta.url);
35
+ // 固定 syim 路径在 bridge 改写 HOME 前确定,避免后续插件临时 HOME
36
+ // 影响 os.homedir(),导致“固定配置真源”和插件运行镜像混在一起。
37
+ const runtimeFixedSyimPath = path.join(os.homedir(), ".syim", "syim.json");
35
38
 
36
39
  // 当前进程运行实例 ID,会在 restart-all 成功后旋转。
37
40
  let runtimeInstanceId = `bridges-${process.pid}-${Date.now()}`;
@@ -1194,7 +1197,6 @@ function applyRemoteRuntimeConfigToPluginHome(): void {
1194
1197
  path.join(os.tmpdir(), "im-agent-hub-plugin-home-"),
1195
1198
  );
1196
1199
  }
1197
- const payload = JSON.stringify(slice, null, 2);
1198
1200
  const syimDir = path.join(pluginTempHomeDir, ".syim");
1199
1201
  const syimPath = path.join(syimDir, "syim.json");
1200
1202
  if (!fs.existsSync(syimDir)) {
@@ -1204,7 +1206,8 @@ function applyRemoteRuntimeConfigToPluginHome(): void {
1204
1206
  `[bridges/main] plugin temp HOME syim dir exists: ${syimDir}`,
1205
1207
  );
1206
1208
  }
1207
- fs.writeFileSync(syimPath, payload, "utf-8");
1209
+ writeJsonFileAtomic(syimPath, slice);
1210
+ logSyimConfigFileSnapshot("plugin-home:syim-write", syimPath, slice);
1208
1211
  const openclawDir = path.join(pluginTempHomeDir, ".openclaw");
1209
1212
  const openclawPath = path.join(openclawDir, "openclaw.json");
1210
1213
  if (!fs.existsSync(openclawDir)) {
@@ -1214,7 +1217,8 @@ function applyRemoteRuntimeConfigToPluginHome(): void {
1214
1217
  `[bridges/main] plugin temp HOME openclaw dir exists: ${openclawDir}`,
1215
1218
  );
1216
1219
  }
1217
- fs.writeFileSync(openclawPath, payload, "utf-8");
1220
+ writeJsonFileAtomic(openclawPath, slice);
1221
+ logSyimConfigFileSnapshot("plugin-home:openclaw-write", openclawPath, slice);
1218
1222
  // 让 openclaw host 与插件均优先读取 .syim/syim.json。
1219
1223
  process.env.OPENCLAW_CONFIG_PATH = syimPath;
1220
1224
  process.env.HOME = pluginTempHomeDir;
@@ -1230,6 +1234,52 @@ function applyRemoteRuntimeConfigToPluginHome(): void {
1230
1234
  }
1231
1235
  }
1232
1236
 
1237
+ function writeJsonFileAtomic(targetPath: string, value: unknown): void {
1238
+ const normalizedTargetPath = String(targetPath || "").trim();
1239
+ if (!normalizedTargetPath) {
1240
+ throw new Error("target json path is empty");
1241
+ }
1242
+ const dir = path.dirname(normalizedTargetPath);
1243
+ fs.mkdirSync(dir, { recursive: true });
1244
+ const tempPath = path.join(
1245
+ dir,
1246
+ `.${path.basename(normalizedTargetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`,
1247
+ );
1248
+ let fd: number | null = null;
1249
+ try {
1250
+ // 配置文件用“临时文件 + fsync + rename”替换,避免 Node/插件在保存过程中
1251
+ // 读到半截 JSON。rename 在同目录内是原子替换,符合固定 syim 真源模式。
1252
+ fd = fs.openSync(tempPath, "w");
1253
+ fs.writeFileSync(fd, JSON.stringify(value, null, 2), "utf-8");
1254
+ fs.fsyncSync(fd);
1255
+ fs.closeSync(fd);
1256
+ fd = null;
1257
+ fs.renameSync(tempPath, normalizedTargetPath);
1258
+ try {
1259
+ const dirFd = fs.openSync(dir, "r");
1260
+ try {
1261
+ fs.fsyncSync(dirFd);
1262
+ } finally {
1263
+ fs.closeSync(dirFd);
1264
+ }
1265
+ } catch {
1266
+ // 目录 fsync 是增强持久化保障;平台不支持时不影响 rename 原子性。
1267
+ }
1268
+ } catch (err) {
1269
+ try {
1270
+ if (fd !== null) fs.closeSync(fd);
1271
+ } catch {
1272
+ // ignore cleanup failure; original write error is more useful.
1273
+ }
1274
+ try {
1275
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
1276
+ } catch {
1277
+ // ignore cleanup failure; original write error is more useful.
1278
+ }
1279
+ throw err;
1280
+ }
1281
+ }
1282
+
1233
1283
  function persistRuntimeConfigSnapshot(params: {
1234
1284
  config: Record<string, unknown>;
1235
1285
  targetPath: string;
@@ -1239,32 +1289,170 @@ function persistRuntimeConfigSnapshot(params: {
1239
1289
  if (!targetPath) {
1240
1290
  throw new Error("target config path is empty");
1241
1291
  }
1242
- const dir = path.dirname(targetPath);
1243
- fs.mkdirSync(dir, { recursive: true });
1244
- const tempPath = path.join(
1245
- dir,
1246
- `.${path.basename(targetPath)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`,
1247
- );
1248
1292
  try {
1249
- fs.writeFileSync(
1250
- tempPath,
1251
- JSON.stringify(params.config, null, 2),
1252
- "utf-8",
1293
+ writeJsonFileAtomic(targetPath, params.config);
1294
+ logSyimConfigFileSnapshot(
1295
+ `persist:${params.reason}`,
1296
+ targetPath,
1297
+ params.config,
1253
1298
  );
1254
- fs.renameSync(tempPath, targetPath);
1255
1299
  console.log(
1256
1300
  `[bridges/main] runtime config persisted reason=${params.reason} path=${targetPath}`,
1257
1301
  );
1258
1302
  } catch (err) {
1259
- try {
1260
- if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
1261
- } catch {
1262
- // ignore cleanup failure; original persist error is more useful.
1263
- }
1264
1303
  throw err;
1265
1304
  }
1266
1305
  }
1267
1306
 
1307
+ function reloadRuntimeConfigFromFixedSyim(reason: string): {
1308
+ ok: boolean;
1309
+ error?: string;
1310
+ configPath?: string;
1311
+ } {
1312
+ const configPath = runtimeFixedSyimPath;
1313
+ try {
1314
+ if (!fs.existsSync(configPath)) {
1315
+ logSyimConfigFileSnapshot(`reload:${reason}:missing`, configPath, null);
1316
+ return {
1317
+ ok: false,
1318
+ error: `fixed syim not found: ${configPath}`,
1319
+ configPath,
1320
+ };
1321
+ }
1322
+ logSyimConfigFileSnapshot(`reload:${reason}:before-read`, configPath, null);
1323
+ const content = fs.readFileSync(configPath, "utf-8");
1324
+ const parsed = JSON.parse(content) as Record<string, unknown>;
1325
+ const normalizedResult = normalizeRuntimeConfigByWhitelist(parsed);
1326
+ if (normalizedResult.droppedFields.length > 0) {
1327
+ logSyimConfigFileSnapshot(
1328
+ `reload:${reason}:rejected`,
1329
+ configPath,
1330
+ parsed,
1331
+ );
1332
+ return {
1333
+ ok: false,
1334
+ error: `fixed syim contains non-whitelisted fields: ${normalizedResult.droppedFields
1335
+ .map((item) => `${item.channel}/${item.accountId}:${item.field}`)
1336
+ .join(", ")}`,
1337
+ configPath,
1338
+ };
1339
+ }
1340
+ loadedConfigForRuntime = normalizedResult.normalized;
1341
+ loadedConfigPathForRuntime = configPath;
1342
+ exposeRuntimeConfigToBridges(loadedConfigForRuntime, configPath);
1343
+ updateConfigVersion(normalizedResult.normalized);
1344
+ applyRemoteRuntimeConfigToPluginHome();
1345
+ logSyimConfigFileSnapshot(
1346
+ `reload:${reason}:applied`,
1347
+ configPath,
1348
+ normalizedResult.normalized,
1349
+ );
1350
+ console.log(
1351
+ `[bridges/main] runtime config reloaded from fixed syim reason=${reason} path=${configPath} version=${configVersionHash || "unknown"}`,
1352
+ );
1353
+ return { ok: true, configPath };
1354
+ } catch (err) {
1355
+ logSyimConfigFileSnapshot(`reload:${reason}:failed`, configPath, null);
1356
+ return {
1357
+ ok: false,
1358
+ error: (err as Error).message,
1359
+ configPath,
1360
+ };
1361
+ }
1362
+ }
1363
+
1364
+ function countConfiguredAccountsInConfig(
1365
+ cfg: Record<string, unknown>,
1366
+ ): Record<string, number> {
1367
+ const channels =
1368
+ cfg.channels && typeof cfg.channels === "object" && !Array.isArray(cfg.channels)
1369
+ ? (cfg.channels as Record<string, unknown>)
1370
+ : {};
1371
+ const result: Record<string, number> = {};
1372
+ for (const [channelKey, channelValue] of Object.entries(channels)) {
1373
+ if (!channelValue || typeof channelValue !== "object") {
1374
+ result[channelKey] = 0;
1375
+ continue;
1376
+ }
1377
+ const channel = channelValue as Record<string, unknown>;
1378
+ const accounts =
1379
+ channel.accounts &&
1380
+ typeof channel.accounts === "object" &&
1381
+ !Array.isArray(channel.accounts)
1382
+ ? Object.keys(channel.accounts as Record<string, unknown>).length
1383
+ : 0;
1384
+ const hasSingleConfig = Object.keys(channel).some((key) =>
1385
+ [
1386
+ "appId",
1387
+ "appSecret",
1388
+ "clientId",
1389
+ "clientSecret",
1390
+ "botId",
1391
+ "secret",
1392
+ "wxid",
1393
+ ].includes(key),
1394
+ );
1395
+ result[channelKey] = accounts || (hasSingleConfig ? 1 : 0);
1396
+ }
1397
+ return result;
1398
+ }
1399
+
1400
+ function summarizeRuntimeConfigForLog(cfg: Record<string, unknown>): {
1401
+ hash: string;
1402
+ channels: string[];
1403
+ accountCounts: Record<string, number>;
1404
+ bindings: number;
1405
+ } {
1406
+ const channels =
1407
+ cfg.channels && typeof cfg.channels === "object" && !Array.isArray(cfg.channels)
1408
+ ? Object.keys(cfg.channels as Record<string, unknown>)
1409
+ : [];
1410
+ const bindings = Array.isArray(cfg.bindings) ? cfg.bindings.length : 0;
1411
+ return {
1412
+ hash: calcSchemaHash(cfg),
1413
+ channels,
1414
+ accountCounts: countConfiguredAccountsInConfig(cfg),
1415
+ bindings,
1416
+ };
1417
+ }
1418
+
1419
+ function logSyimConfigFileSnapshot(
1420
+ label: string,
1421
+ configPath: string,
1422
+ cfg?: Record<string, unknown> | null,
1423
+ ): void {
1424
+ const normalizedPath = String(configPath || "").trim();
1425
+ let fileMeta: Record<string, unknown> = {
1426
+ exists: false,
1427
+ path: normalizedPath,
1428
+ };
1429
+ try {
1430
+ if (normalizedPath && fs.existsSync(normalizedPath)) {
1431
+ const stat = fs.statSync(normalizedPath);
1432
+ fileMeta = {
1433
+ exists: true,
1434
+ path: normalizedPath,
1435
+ size: stat.size,
1436
+ mtimeMs: Math.trunc(stat.mtimeMs),
1437
+ mtimeIso: stat.mtime.toISOString(),
1438
+ };
1439
+ }
1440
+ } catch (err) {
1441
+ fileMeta = {
1442
+ exists: false,
1443
+ path: normalizedPath,
1444
+ statError: (err as Error).message,
1445
+ };
1446
+ }
1447
+ const configMeta =
1448
+ cfg && typeof cfg === "object" && !Array.isArray(cfg)
1449
+ ? summarizeRuntimeConfigForLog(cfg)
1450
+ : null;
1451
+ console.log(
1452
+ `[bridges/main][syim-config] ${label} file=${JSON.stringify(fileMeta)} config=${JSON.stringify(configMeta)}`,
1453
+ );
1454
+ }
1455
+
1268
1456
  // nowIso: 生成当前 ISO 时间戳。
1269
1457
  function nowIso(): string {
1270
1458
  // 全链路统一使用 ISO 字符串时间戳。
@@ -4781,6 +4969,9 @@ function exposeRuntimeConfigToBridges(
4781
4969
  (globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG__ = config;
4782
4970
  (globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG_PATH__ =
4783
4971
  configPath;
4972
+ // stdio bridge 每次 buildCfg() 优先读取这个固定 syim。内存只作为
4973
+ // 文件缺失/本地调试时的兜底快照,避免单机器人重启继续使用旧内存配置。
4974
+ process.env.IM_RUNTIME_FIXED_CONFIG_PATH = runtimeFixedSyimPath;
4784
4975
  } catch {
4785
4976
  // ignore
4786
4977
  }
@@ -6424,8 +6615,7 @@ async function pullRuntimeConfigFromPython(
6424
6615
 
6425
6616
  let persistedConfigPath: string | null = null;
6426
6617
  if (runtimeConfigPullPersist) {
6427
- const targetPath =
6428
- loadedConfigPathForRuntime || path.join(process.cwd(), "syim.json");
6618
+ const targetPath = runtimeFixedSyimPath;
6429
6619
  // pull 成功但写盘失败时不能先切内存:否则调用方看到失败,
6430
6620
  // 当前进程却已经按新配置运行,形成“失败响应 + 已变更运行态”。
6431
6621
  persistRuntimeConfigSnapshot({
@@ -6569,6 +6759,15 @@ async function recoverRuntimeAccountByHealthGuard(params: {
6569
6759
  1;
6570
6760
 
6571
6761
  try {
6762
+ const reloadResult = reloadRuntimeConfigFromFixedSyim(
6763
+ `health_guard_restart:${reason}`,
6764
+ );
6765
+ if (!reloadResult.ok) {
6766
+ throw new Error(
6767
+ `reload fixed syim failed: ${reloadResult.error || "unknown"}`,
6768
+ );
6769
+ }
6770
+
6572
6771
  let control = resolveRuntimeBridgeControl(item.platform);
6573
6772
  if (!control) {
6574
6773
  await ensureBridgeReadyForBotControl(item.platform, item.bot_account_id);
@@ -7734,6 +7933,26 @@ async function startInternalApiServer(): Promise<void> {
7734
7933
  }
7735
7934
  botControlInFlight.add(inFlightKey);
7736
7935
 
7936
+ if (botControlAction === "start" || botControlAction === "restart") {
7937
+ const reloadResult = reloadRuntimeConfigFromFixedSyim(
7938
+ `bot_${botControlAction}`,
7939
+ );
7940
+ if (!reloadResult.ok) {
7941
+ botControlInFlight.delete(inFlightKey);
7942
+ res.writeHead(500, { "Content-Type": "application/json" });
7943
+ res.end(
7944
+ JSON.stringify({
7945
+ ok: false,
7946
+ error: `reload fixed syim failed: ${reloadResult.error || "unknown"}`,
7947
+ configPath: reloadResult.configPath || runtimeFixedSyimPath,
7948
+ platform,
7949
+ bot_account_id: botAccountId,
7950
+ }),
7951
+ );
7952
+ return;
7953
+ }
7954
+ }
7955
+
7737
7956
  let control = resolveRuntimeBridgeControl(platform);
7738
7957
  if (
7739
7958
  !control &&
@@ -8043,10 +8262,7 @@ async function startInternalApiServer(): Promise<void> {
8043
8262
 
8044
8263
  let persistedConfigPath: string | null = null;
8045
8264
  if (persist) {
8046
- const targetPath =
8047
- String(body.target_path || "").trim() ||
8048
- loadedConfigPathForRuntime ||
8049
- path.join(process.cwd(), "syim.json");
8265
+ const targetPath = runtimeFixedSyimPath;
8050
8266
  try {
8051
8267
  // config/apply 对外返回失败时,运行中配置也必须保持不变。
8052
8268
  // 因此 persist=true 先写盘,写盘成功后才切换内存、版本与 probe。
@@ -8200,18 +8416,12 @@ async function startInternalApiServer(): Promise<void> {
8200
8416
  console.log(
8201
8417
  `[bridges/main] restart-all step=pull_config done pulled=${String(pullResult.pulled)} version=${pullResult.version || "unknown"}`,
8202
8418
  );
8203
- } else if (
8204
- loadedConfigPathForRuntime &&
8205
- fs.existsSync(loadedConfigPathForRuntime)
8206
- ) {
8419
+ } else if (fs.existsSync(runtimeFixedSyimPath)) {
8207
8420
  console.log(
8208
- `[bridges/main] restart-all step=reload_local_config begin path=${loadedConfigPathForRuntime}`,
8421
+ `[bridges/main] restart-all step=reload_local_config begin path=${runtimeFixedSyimPath}`,
8209
8422
  );
8210
8423
  try {
8211
- const content = fs.readFileSync(
8212
- loadedConfigPathForRuntime,
8213
- "utf-8",
8214
- );
8424
+ const content = fs.readFileSync(runtimeFixedSyimPath, "utf-8");
8215
8425
  const parsed = JSON.parse(content) as Record<string, unknown>;
8216
8426
  const normalizedResult = normalizeRuntimeConfigByWhitelist(parsed);
8217
8427
  if (normalizedResult.droppedFields.length > 0) {
@@ -8226,9 +8436,10 @@ async function startInternalApiServer(): Promise<void> {
8226
8436
  return;
8227
8437
  }
8228
8438
  loadedConfigForRuntime = normalizedResult.normalized;
8439
+ loadedConfigPathForRuntime = runtimeFixedSyimPath;
8229
8440
  exposeRuntimeConfigToBridges(
8230
8441
  loadedConfigForRuntime,
8231
- loadedConfigPathForRuntime,
8442
+ runtimeFixedSyimPath,
8232
8443
  );
8233
8444
  updateConfigVersion(normalizedResult.normalized);
8234
8445
  console.log(
@@ -8818,8 +9029,8 @@ function loadOpenClawConfig(): {
8818
9029
  return null;
8819
9030
  }
8820
9031
  const configPaths = [
9032
+ runtimeFixedSyimPath,
8821
9033
  path.join(process.cwd(), "syim.json"),
8822
- path.join(os.homedir(), ".syim", "syim.json"),
8823
9034
  ];
8824
9035
  for (const configPath of configPaths) {
8825
9036
  if (!fs.existsSync(configPath)) continue;
@@ -8928,12 +9139,11 @@ async function main(): Promise<void> {
8928
9139
  // 远端配置权威模式:只要配置了 RUNTIME_CONFIG_PULL_URL,就不能在
8929
9140
  // 初始 pull 失败时回退使用本地旧 syim.json 启动 bridge。否则 Python
8930
9141
  // DB 真源不可用时,Node 可能用过期本地文件启动旧账号/旧凭证。
8931
- const fallbackPath = path.join(os.homedir(), ".syim", "syim.json");
8932
9142
  console.log(
8933
9143
  `[bridges/main] runtime pull url configured, skip local config bootstrap and wait for remote source: ${runtimeConfigPullUrl}`,
8934
9144
  );
8935
9145
  loadedConfigForRuntime = { channels: {}, bindings: [] };
8936
- loadedConfigPathForRuntime = fallbackPath;
9146
+ loadedConfigPathForRuntime = runtimeFixedSyimPath;
8937
9147
  exposeRuntimeConfigToBridges(
8938
9148
  loadedConfigForRuntime,
8939
9149
  loadedConfigPathForRuntime,
@@ -8966,12 +9176,11 @@ async function main(): Promise<void> {
8966
9176
  "startup_bootstrap_empty",
8967
9177
  );
8968
9178
  } else {
8969
- const fallbackPath = path.join(os.homedir(), ".syim", "syim.json");
8970
9179
  console.log(
8971
9180
  `[bridges/main] local config not found, bootstrap from runtime pull url: ${runtimeConfigPullUrl}`,
8972
9181
  );
8973
9182
  loadedConfigForRuntime = { channels: {}, bindings: [] };
8974
- loadedConfigPathForRuntime = fallbackPath;
9183
+ loadedConfigPathForRuntime = runtimeFixedSyimPath;
8975
9184
  exposeRuntimeConfigToBridges(
8976
9185
  loadedConfigForRuntime,
8977
9186
  loadedConfigPathForRuntime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ylib-syim",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -48,7 +48,7 @@
48
48
  "@ffmpeg-installer/ffmpeg": "^1.1.0",
49
49
  "ylib-dingtalk-connector": "0.7.10-23",
50
50
  "ylib-openclaw-lark": "2026.3.17-30",
51
- "ylib-openclaw-weixin": "2.1.7-17",
51
+ "ylib-openclaw-weixin": "2.1.7-18",
52
52
  "axios": "^1.6.0",
53
53
  "dingtalk-stream": "^2.1.4",
54
54
  "fluent-ffmpeg": "^2.1.3",
@@ -304,24 +304,47 @@ function channelsForLog(
304
304
  return out;
305
305
  }
306
306
 
307
+ function syimFileMetaForLog(configPath: string): Record<string, unknown> {
308
+ try {
309
+ const stat = fs.statSync(configPath);
310
+ return {
311
+ path: configPath,
312
+ size: stat.size,
313
+ mtimeMs: Math.trunc(stat.mtimeMs),
314
+ mtimeIso: stat.mtime.toISOString(),
315
+ };
316
+ } catch (err) {
317
+ return {
318
+ path: configPath,
319
+ statError: (err as Error).message,
320
+ };
321
+ }
322
+ }
323
+
324
+ function configSummaryForLog(config: Record<string, unknown>): Record<string, unknown> {
325
+ const channels =
326
+ config.channels && typeof config.channels === "object" && !Array.isArray(config.channels)
327
+ ? (config.channels as Record<string, unknown>)
328
+ : {};
329
+ const dingtalk = channels[CHANNEL_KEY] as Record<string, unknown> | undefined;
330
+ const accounts =
331
+ dingtalk?.accounts &&
332
+ typeof dingtalk.accounts === "object" &&
333
+ !Array.isArray(dingtalk.accounts)
334
+ ? Object.keys(dingtalk.accounts as Record<string, unknown>)
335
+ : [];
336
+ return {
337
+ channels: Object.keys(channels),
338
+ dingtalkAccounts: accounts,
339
+ bindings: Array.isArray(config.bindings) ? config.bindings.length : 0,
340
+ };
341
+ }
342
+
307
343
  /** 加载 syim.json,与 connector-host 一致 */
308
344
  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
- }
345
+ const fixedConfigPath = String(
346
+ process.env.IM_RUNTIME_FIXED_CONFIG_PATH || "",
347
+ ).trim();
325
348
  const runtimeConfigPath = String(
326
349
  (globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG_PATH__ || "",
327
350
  ).trim();
@@ -329,15 +352,24 @@ function loadOpenClawConfig(): Record<string, unknown> | null {
329
352
  const configPaths = Array.from(
330
353
  new Set(
331
354
  [
332
- envConfigPath,
355
+ fixedConfigPath,
333
356
  runtimeConfigPath,
357
+ envConfigPath,
334
358
  path.join(os.homedir(), ".syim", "syim.json"),
335
359
  path.join(getProjectRoot(), "syim.json"),
336
360
  ].filter((p) => Boolean(p)),
337
361
  ),
338
362
  );
363
+ console.log(
364
+ `[dingtalk-stdio-bridge][syim-config] candidates=${JSON.stringify(configPaths)}`,
365
+ );
339
366
  for (const configPath of configPaths) {
340
- if (!fs.existsSync(configPath)) continue;
367
+ if (!fs.existsSync(configPath)) {
368
+ console.log(
369
+ `[dingtalk-stdio-bridge][syim-config] skip missing path=${configPath}`,
370
+ );
371
+ continue;
372
+ }
341
373
  try {
342
374
  const content = fs.readFileSync(configPath, "utf-8");
343
375
  const config = JSON.parse(content) as Record<string, unknown>;
@@ -345,6 +377,9 @@ function loadOpenClawConfig(): Record<string, unknown> | null {
345
377
  "[dingtalk-stdio-bridge] 配置与 connector 匹配: 从 syim.json 加载配置,路径:",
346
378
  configPath,
347
379
  );
380
+ console.log(
381
+ `[dingtalk-stdio-bridge][syim-config] selected file=${JSON.stringify(syimFileMetaForLog(configPath))} summary=${JSON.stringify(configSummaryForLog(config))}`,
382
+ );
348
383
  console.log(
349
384
  "[dingtalk-stdio-bridge] 配置文件 channels:",
350
385
  JSON.stringify(
@@ -362,6 +397,30 @@ function loadOpenClawConfig(): Record<string, unknown> | null {
362
397
  );
363
398
  }
364
399
  }
400
+ const runtimeConfig = (globalThis as Record<string, unknown>)
401
+ .__IM_RUNTIME_CONFIG__;
402
+ if (
403
+ runtimeConfig &&
404
+ typeof runtimeConfig === "object" &&
405
+ !Array.isArray(runtimeConfig)
406
+ ) {
407
+ console.log(
408
+ "[dingtalk-stdio-bridge] syim 不可用,使用 runtime 内存配置兜底",
409
+ );
410
+ try {
411
+ const cloned = JSON.parse(JSON.stringify(runtimeConfig)) as Record<string, unknown>;
412
+ console.log(
413
+ `[dingtalk-stdio-bridge][syim-config] runtime fallback summary=${JSON.stringify(configSummaryForLog(cloned))}`,
414
+ );
415
+ return cloned;
416
+ } catch {
417
+ const cloned = { ...(runtimeConfig as Record<string, unknown>) };
418
+ console.log(
419
+ `[dingtalk-stdio-bridge][syim-config] runtime fallback summary=${JSON.stringify(configSummaryForLog(cloned))}`,
420
+ );
421
+ return cloned;
422
+ }
423
+ }
365
424
  console.log(
366
425
  "[dingtalk-stdio-bridge] 配置与 connector 匹配: 未找到 syim.json,将使用环境变量",
367
426
  );
@@ -407,7 +466,10 @@ function buildCfg(): {
407
466
  firstStringInAccounts(accountsMap, "gatewayToken") ||
408
467
  ""
409
468
  ).trim();
410
- console.log("[dingtalk-stdio-bridge] gatewayToken =", gatewayToken);
469
+ console.log(
470
+ "[dingtalk-stdio-bridge] gatewayToken =",
471
+ gatewayToken ? "<present>" : "<absent>",
472
+ );
411
473
  console.log("[dingtalk-stdio-bridge] gatewayBaseUrl =", gatewayBaseUrl);
412
474
 
413
475
  if (!gatewayBaseUrl) {
@@ -490,6 +552,14 @@ function buildCfg(): {
490
552
  console.log(
491
553
  `[dingtalk-stdio-bridge] single account alias = ${bridgeDefaultAccountAlias}`,
492
554
  );
555
+ console.log(
556
+ `[dingtalk-stdio-bridge][syim-config] buildCfg summary=${JSON.stringify({
557
+ ...configSummaryForLog(cfg),
558
+ gatewayBaseUrl: gatewayBaseUrl || "<absent>",
559
+ gatewayToken: gatewayToken ? "<present>" : "<absent>",
560
+ singleAccountAlias: bridgeDefaultAccountAlias,
561
+ })}`,
562
+ );
493
563
 
494
564
  return { cfg, gatewayBaseUrl, gatewayToken };
495
565
  }
@@ -318,27 +318,48 @@ function channelsForLog(
318
318
  return out;
319
319
  }
320
320
 
321
+ function syimFileMetaForLog(configPath: string): Record<string, unknown> {
322
+ try {
323
+ const stat = fs.statSync(configPath);
324
+ return {
325
+ path: configPath,
326
+ size: stat.size,
327
+ mtimeMs: Math.trunc(stat.mtimeMs),
328
+ mtimeIso: stat.mtime.toISOString(),
329
+ };
330
+ } catch (err) {
331
+ return {
332
+ path: configPath,
333
+ statError: (err as Error).message,
334
+ };
335
+ }
336
+ }
337
+
338
+ function configSummaryForLog(config: Record<string, unknown>): Record<string, unknown> {
339
+ const channels =
340
+ config.channels && typeof config.channels === "object" && !Array.isArray(config.channels)
341
+ ? (config.channels as Record<string, unknown>)
342
+ : {};
343
+ const feishu = channels[CHANNEL_KEY] as Record<string, unknown> | undefined;
344
+ const accounts =
345
+ feishu?.accounts && typeof feishu.accounts === "object" && !Array.isArray(feishu.accounts)
346
+ ? Object.keys(feishu.accounts as Record<string, unknown>)
347
+ : [];
348
+ return {
349
+ channels: Object.keys(channels),
350
+ feishuAccounts: accounts,
351
+ bindings: Array.isArray(config.bindings) ? config.bindings.length : 0,
352
+ };
353
+ }
354
+
321
355
  // ---------------------------------------------------------------------------
322
356
  // 配置加载
323
357
  // ---------------------------------------------------------------------------
324
358
 
325
359
  function loadOpenClawConfig(): Record<string, unknown> | null {
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
- }
360
+ const fixedConfigPath = String(
361
+ process.env.IM_RUNTIME_FIXED_CONFIG_PATH || "",
362
+ ).trim();
342
363
  const runtimeConfigPath = String(
343
364
  (globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG_PATH__ || "",
344
365
  ).trim();
@@ -346,19 +367,31 @@ function loadOpenClawConfig(): Record<string, unknown> | null {
346
367
  const configPaths = Array.from(
347
368
  new Set(
348
369
  [
349
- envConfigPath,
370
+ fixedConfigPath,
350
371
  runtimeConfigPath,
372
+ envConfigPath,
351
373
  path.join(os.homedir(), ".syim", "syim.json"),
352
374
  path.join(getProjectRoot(), "syim.json"),
353
375
  ].filter((p) => Boolean(p)),
354
376
  ),
355
377
  );
378
+ console.log(
379
+ `[lark-stdio-bridge][syim-config] candidates=${JSON.stringify(configPaths)}`,
380
+ );
356
381
  for (const configPath of configPaths) {
357
- if (!fs.existsSync(configPath)) continue;
382
+ if (!fs.existsSync(configPath)) {
383
+ console.log(
384
+ `[lark-stdio-bridge][syim-config] skip missing path=${configPath}`,
385
+ );
386
+ continue;
387
+ }
358
388
  try {
359
389
  const content = fs.readFileSync(configPath, "utf-8");
360
390
  const config = JSON.parse(content) as Record<string, unknown>;
361
391
  console.log("[lark-stdio-bridge] 从 syim.json 加载配置:", configPath);
392
+ console.log(
393
+ `[lark-stdio-bridge][syim-config] selected file=${JSON.stringify(syimFileMetaForLog(configPath))} summary=${JSON.stringify(configSummaryForLog(config))}`,
394
+ );
362
395
  console.log(
363
396
  "[lark-stdio-bridge] channels:",
364
397
  JSON.stringify(
@@ -376,6 +409,30 @@ function loadOpenClawConfig(): Record<string, unknown> | null {
376
409
  );
377
410
  }
378
411
  }
412
+ const runtimeConfig = (globalThis as Record<string, unknown>)
413
+ .__IM_RUNTIME_CONFIG__;
414
+ if (
415
+ runtimeConfig &&
416
+ typeof runtimeConfig === "object" &&
417
+ !Array.isArray(runtimeConfig)
418
+ ) {
419
+ console.log(
420
+ "[lark-stdio-bridge] syim 不可用,使用 runtime 内存配置兜底",
421
+ );
422
+ try {
423
+ const cloned = JSON.parse(JSON.stringify(runtimeConfig)) as Record<string, unknown>;
424
+ console.log(
425
+ `[lark-stdio-bridge][syim-config] runtime fallback summary=${JSON.stringify(configSummaryForLog(cloned))}`,
426
+ );
427
+ return cloned;
428
+ } catch {
429
+ const cloned = { ...(runtimeConfig as Record<string, unknown>) };
430
+ console.log(
431
+ `[lark-stdio-bridge][syim-config] runtime fallback summary=${JSON.stringify(configSummaryForLog(cloned))}`,
432
+ );
433
+ return cloned;
434
+ }
435
+ }
379
436
  console.log("[lark-stdio-bridge] 未找到 syim.json,将使用环境变量");
380
437
  return null;
381
438
  }
@@ -504,6 +561,14 @@ function buildCfg(): {
504
561
  console.log(
505
562
  `[lark-stdio-bridge] single account alias = ${bridgeDefaultAccountAlias}`,
506
563
  );
564
+ console.log(
565
+ `[lark-stdio-bridge][syim-config] buildCfg summary=${JSON.stringify({
566
+ ...configSummaryForLog(cfg),
567
+ gatewayBaseUrl: gatewayBaseUrl || "<absent>",
568
+ gatewayToken: gatewayToken ? "<present>" : "<absent>",
569
+ singleAccountAlias: bridgeDefaultAccountAlias,
570
+ })}`,
571
+ );
507
572
 
508
573
  return { cfg, gatewayBaseUrl, gatewayToken };
509
574
  }
@@ -437,23 +437,44 @@ function normalizeConfiguredAccountIds(ids: string[]): string[] {
437
437
  return normalized;
438
438
  }
439
439
 
440
- function loadOpenClawConfig(): Record<string, unknown> | null {
441
- const runtimeConfig = (globalThis as Record<string, unknown>)
442
- .__IM_RUNTIME_CONFIG__;
443
- if (
444
- runtimeConfig &&
445
- typeof runtimeConfig === "object" &&
446
- !Array.isArray(runtimeConfig)
447
- ) {
448
- console.log(
449
- "[weixin-stdio-bridge] runtime 内存配置加载,跳过本地 syim fallback",
450
- );
451
- try {
452
- return JSON.parse(JSON.stringify(runtimeConfig)) as Record<string, unknown>;
453
- } catch {
454
- return { ...(runtimeConfig as Record<string, unknown>) };
455
- }
440
+ function syimFileMetaForLog(configPath: string): Record<string, unknown> {
441
+ try {
442
+ const stat = fs.statSync(configPath);
443
+ return {
444
+ path: configPath,
445
+ size: stat.size,
446
+ mtimeMs: Math.trunc(stat.mtimeMs),
447
+ mtimeIso: stat.mtime.toISOString(),
448
+ };
449
+ } catch (err) {
450
+ return {
451
+ path: configPath,
452
+ statError: (err as Error).message,
453
+ };
456
454
  }
455
+ }
456
+
457
+ function configSummaryForLog(config: Record<string, unknown>): Record<string, unknown> {
458
+ const channels =
459
+ config.channels && typeof config.channels === "object" && !Array.isArray(config.channels)
460
+ ? (config.channels as Record<string, unknown>)
461
+ : {};
462
+ const weixin = channels[CHANNEL_KEY] as Record<string, unknown> | undefined;
463
+ const accounts =
464
+ weixin?.accounts && typeof weixin.accounts === "object" && !Array.isArray(weixin.accounts)
465
+ ? Object.keys(weixin.accounts as Record<string, unknown>)
466
+ : [];
467
+ return {
468
+ channels: Object.keys(channels),
469
+ weixinAccounts: accounts,
470
+ bindings: Array.isArray(config.bindings) ? config.bindings.length : 0,
471
+ };
472
+ }
473
+
474
+ function loadOpenClawConfig(): Record<string, unknown> | null {
475
+ const fixedConfigPath = String(
476
+ process.env.IM_RUNTIME_FIXED_CONFIG_PATH || "",
477
+ ).trim();
457
478
  const runtimeConfigPath = String(
458
479
  (globalThis as Record<string, unknown>).__IM_RUNTIME_CONFIG_PATH__ || "",
459
480
  ).trim();
@@ -461,20 +482,32 @@ function loadOpenClawConfig(): Record<string, unknown> | null {
461
482
  const configPaths = Array.from(
462
483
  new Set(
463
484
  [
464
- envConfigPath,
485
+ fixedConfigPath,
465
486
  runtimeConfigPath,
487
+ envConfigPath,
466
488
  path.join(os.homedir(), ".syim", "syim.json"),
467
489
  path.join(getProjectRoot(), "syim.json"),
468
490
  ].filter((p) => Boolean(p)),
469
491
  ),
470
492
  );
471
493
 
494
+ console.log(
495
+ `[weixin-stdio-bridge][syim-config] candidates=${JSON.stringify(configPaths)}`,
496
+ );
472
497
  for (const configPath of configPaths) {
473
- if (!fs.existsSync(configPath)) continue;
498
+ if (!fs.existsSync(configPath)) {
499
+ console.log(
500
+ `[weixin-stdio-bridge][syim-config] skip missing path=${configPath}`,
501
+ );
502
+ continue;
503
+ }
474
504
  try {
475
505
  const content = fs.readFileSync(configPath, "utf-8");
476
506
  const config = JSON.parse(content) as Record<string, unknown>;
477
507
  console.log("[weixin-stdio-bridge] config loaded:", configPath);
508
+ console.log(
509
+ `[weixin-stdio-bridge][syim-config] selected file=${JSON.stringify(syimFileMetaForLog(configPath))} summary=${JSON.stringify(configSummaryForLog(config))}`,
510
+ );
478
511
  return config;
479
512
  } catch (err) {
480
513
  console.warn(
@@ -484,6 +517,28 @@ function loadOpenClawConfig(): Record<string, unknown> | null {
484
517
  );
485
518
  }
486
519
  }
520
+ const runtimeConfig = (globalThis as Record<string, unknown>)
521
+ .__IM_RUNTIME_CONFIG__;
522
+ if (
523
+ runtimeConfig &&
524
+ typeof runtimeConfig === "object" &&
525
+ !Array.isArray(runtimeConfig)
526
+ ) {
527
+ console.log("[weixin-stdio-bridge] syim 不可用,使用 runtime 内存配置兜底");
528
+ try {
529
+ const cloned = JSON.parse(JSON.stringify(runtimeConfig)) as Record<string, unknown>;
530
+ console.log(
531
+ `[weixin-stdio-bridge][syim-config] runtime fallback summary=${JSON.stringify(configSummaryForLog(cloned))}`,
532
+ );
533
+ return cloned;
534
+ } catch {
535
+ const cloned = { ...(runtimeConfig as Record<string, unknown>) };
536
+ console.log(
537
+ `[weixin-stdio-bridge][syim-config] runtime fallback summary=${JSON.stringify(configSummaryForLog(cloned))}`,
538
+ );
539
+ return cloned;
540
+ }
541
+ }
487
542
  console.log("[weixin-stdio-bridge] no syim.json found, fallback to env");
488
543
  return null;
489
544
  }
@@ -595,6 +650,14 @@ function buildCfg(): {
595
650
  console.log(
596
651
  `[weixin-stdio-bridge] single account alias = ${bridgeDefaultAccountAlias}`,
597
652
  );
653
+ console.log(
654
+ `[weixin-stdio-bridge][syim-config] buildCfg summary=${JSON.stringify({
655
+ ...configSummaryForLog(cfg),
656
+ gatewayBaseUrl: gatewayBaseUrl || "<absent>",
657
+ gatewayToken: gatewayToken ? "<present>" : "<absent>",
658
+ singleAccountAlias: bridgeDefaultAccountAlias,
659
+ })}`,
660
+ );
598
661
 
599
662
  return { cfg, gatewayBaseUrl, gatewayToken };
600
663
  }