ylib-syim 0.0.25 → 0.0.27
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 +316 -79
- package/bridges/runtime/runtime-health-policy.ts +55 -11
- package/ecosystem.config.cjs +1 -0
- package/package.json +4 -4
package/bridges/main.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { pullRuntimeConfig } from "./runtime/config-client.ts";
|
|
|
10
10
|
import {
|
|
11
11
|
buildRuntimeReadinessSnapshot as buildRuntimeReadinessSnapshotByPolicy,
|
|
12
12
|
evaluateRuntimeHealth,
|
|
13
|
+
shouldApplyReportedFailureOverManualConnecting,
|
|
13
14
|
resolveRuntimeDisplayState as resolveRuntimeDisplayStateByPolicy,
|
|
14
15
|
resolveRuntimeHealthGuardTriggerReason as resolveRuntimeHealthGuardTriggerReasonByPolicy,
|
|
15
16
|
type RuntimeHealthGuardTriggerReason,
|
|
@@ -523,6 +524,10 @@ let runtimeStateExitHooksRegistered = false;
|
|
|
523
524
|
// 最近一次收到任意 heartbeat 的时间。
|
|
524
525
|
let runtimeHeartbeatLastReceivedAt: string | null = null;
|
|
525
526
|
// 当前运行时生效配置(channels/bindings)。
|
|
527
|
+
// 初始值为空配置,避免启动阶段各处 null 检查散落。
|
|
528
|
+
// 真实的 channels / bindings 在 pullRuntimeConfigFromPython() 或 config/apply 之后写入。
|
|
529
|
+
// 不要依赖本变量是否为 null 来判断"配置是否加载完成"——用 runtimeReady 标志。
|
|
530
|
+
// 以上三行注释不可删除。
|
|
526
531
|
let loadedConfigForRuntime: Record<string, unknown> | null = {
|
|
527
532
|
channels: {},
|
|
528
533
|
bindings: [],
|
|
@@ -1644,15 +1649,26 @@ function resolveRuntimeRestartPendingForUpdate(params: {
|
|
|
1644
1649
|
previous: RuntimeBotStatus | null | undefined;
|
|
1645
1650
|
hintedRestartPending: boolean | null | undefined;
|
|
1646
1651
|
nextLinkStatus: RuntimeBotStatus["link_status"];
|
|
1652
|
+
nextStatusSource?: RuntimeBotStatus["status_source"] | undefined;
|
|
1647
1653
|
}): boolean {
|
|
1648
|
-
const {
|
|
1654
|
+
const {
|
|
1655
|
+
previous,
|
|
1656
|
+
hintedRestartPending,
|
|
1657
|
+
nextLinkStatus,
|
|
1658
|
+
nextStatusSource,
|
|
1659
|
+
} = params;
|
|
1649
1660
|
if (typeof hintedRestartPending === "boolean") {
|
|
1650
1661
|
return hintedRestartPending;
|
|
1651
1662
|
}
|
|
1652
1663
|
|
|
1653
|
-
// Preserve pending restart lifecycle until an explicit signal clears it
|
|
1654
|
-
//
|
|
1664
|
+
// Preserve pending restart lifecycle until an explicit signal clears it.
|
|
1665
|
+
// 在重启生命周期中,event/manual 的 connected 仍可能是过渡态,
|
|
1666
|
+
// 仅 probe/connected 作为“重启完成并可对外收敛”的证据。
|
|
1655
1667
|
if (nextLinkStatus === "connected") {
|
|
1668
|
+
const source = nextStatusSource || "probe";
|
|
1669
|
+
if (previous?.restart_pending === true && source !== "probe") {
|
|
1670
|
+
return true;
|
|
1671
|
+
}
|
|
1656
1672
|
return false;
|
|
1657
1673
|
}
|
|
1658
1674
|
return previous?.restart_pending === true;
|
|
@@ -1704,15 +1720,44 @@ function isProbeDowngradeLinkStatus(
|
|
|
1704
1720
|
);
|
|
1705
1721
|
}
|
|
1706
1722
|
|
|
1723
|
+
function resolveRuntimeLifecycleAnchorMsForGuard(
|
|
1724
|
+
status: RuntimeBotStatus,
|
|
1725
|
+
): number | null {
|
|
1726
|
+
const startedAtMs = parseIsoTimeMs(
|
|
1727
|
+
String(status.started_at || "").trim() || null,
|
|
1728
|
+
);
|
|
1729
|
+
const transitionAtMs = parseIsoTimeMs(
|
|
1730
|
+
String(status.transition_at || "").trim() || null,
|
|
1731
|
+
);
|
|
1732
|
+
if (startedAtMs == null && transitionAtMs == null) return null;
|
|
1733
|
+
if (startedAtMs == null) return transitionAtMs;
|
|
1734
|
+
if (transitionAtMs == null) return startedAtMs;
|
|
1735
|
+
return Math.max(startedAtMs, transitionAtMs);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function isWithinRuntimeConnectGraceForGuard(
|
|
1739
|
+
status: RuntimeBotStatus,
|
|
1740
|
+
nowMs = Date.now(),
|
|
1741
|
+
): boolean {
|
|
1742
|
+
const lifecycleAnchorMs = resolveRuntimeLifecycleAnchorMsForGuard(status);
|
|
1743
|
+
if (lifecycleAnchorMs == null) return false;
|
|
1744
|
+
const lifecycleAgeMs = Math.max(0, nowMs - lifecycleAnchorMs);
|
|
1745
|
+
return lifecycleAgeMs <= runtimeStatusConnectGraceMs;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1707
1748
|
function hasFreshRuntimeEventLivenessForProbeGuard(
|
|
1708
1749
|
key: string,
|
|
1709
1750
|
nowMs = Date.now(),
|
|
1751
|
+
lifecycleAnchorMs: number | null = null,
|
|
1710
1752
|
): boolean {
|
|
1711
1753
|
const eventLiveness = runtimeEventLivenessRegistry.get(key);
|
|
1712
1754
|
const eventAtMs = parseIsoTimeMs(
|
|
1713
1755
|
String(eventLiveness?.last_event_at || "").trim() || null,
|
|
1714
1756
|
);
|
|
1715
1757
|
if (eventAtMs == null) return false;
|
|
1758
|
+
if (lifecycleAnchorMs != null && eventAtMs < lifecycleAnchorMs) {
|
|
1759
|
+
return false;
|
|
1760
|
+
}
|
|
1716
1761
|
const ageMs = Math.max(0, nowMs - eventAtMs);
|
|
1717
1762
|
return ageMs <= runtimeStatusProbeDowngradeGuardEventFreshMs;
|
|
1718
1763
|
}
|
|
@@ -1759,7 +1804,14 @@ function shouldGuardProbeDowngradeByFreshEvent(params: {
|
|
|
1759
1804
|
if (next.status_source !== "probe") return false;
|
|
1760
1805
|
if (!isProbeDowngradeLinkStatus(next.link_status)) return false;
|
|
1761
1806
|
if (prev.link_status !== "connected") return false;
|
|
1762
|
-
|
|
1807
|
+
const nowMs = Date.now();
|
|
1808
|
+
if (isWithinRuntimeConnectGraceForGuard(prev, nowMs)) return false;
|
|
1809
|
+
const lifecycleAnchorMs = resolveRuntimeLifecycleAnchorMsForGuard(prev);
|
|
1810
|
+
return hasFreshRuntimeEventLivenessForProbeGuard(
|
|
1811
|
+
key,
|
|
1812
|
+
nowMs,
|
|
1813
|
+
lifecycleAnchorMs,
|
|
1814
|
+
);
|
|
1763
1815
|
}
|
|
1764
1816
|
|
|
1765
1817
|
function shouldGuardProbeConnectedByEventBroken(params: {
|
|
@@ -1777,7 +1829,72 @@ function shouldGuardProbeConnectedByEventBroken(params: {
|
|
|
1777
1829
|
prev.link_status === "error" ||
|
|
1778
1830
|
prev.link_status === "degraded";
|
|
1779
1831
|
if (!prevBroken) return false;
|
|
1780
|
-
|
|
1832
|
+
const lifecycleAnchorMs = resolveRuntimeLifecycleAnchorMsForGuard(prev);
|
|
1833
|
+
return hasFreshRuntimeEventLivenessForProbeGuard(
|
|
1834
|
+
key,
|
|
1835
|
+
Date.now(),
|
|
1836
|
+
lifecycleAnchorMs,
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
type RuntimeConvergencePhase = "pending" | "steady" | "stopped";
|
|
1841
|
+
type RuntimeConvergenceEvidence = "manual" | "event" | "probe";
|
|
1842
|
+
type RuntimeConvergenceSignal = "success" | "failure" | "transient";
|
|
1843
|
+
|
|
1844
|
+
function resolveRuntimeConvergencePhase(status: RuntimeBotStatus): RuntimeConvergencePhase {
|
|
1845
|
+
if (status.restart_pending === true) return "pending";
|
|
1846
|
+
if (status.status_source === "manual" && status.link_status === "disconnected") {
|
|
1847
|
+
return "stopped";
|
|
1848
|
+
}
|
|
1849
|
+
const lastEventText = String(status.last_event || "")
|
|
1850
|
+
.trim()
|
|
1851
|
+
.toLowerCase();
|
|
1852
|
+
if (
|
|
1853
|
+
status.link_status === "disconnected" &&
|
|
1854
|
+
runtimeStoppedLifecycleEvents.has(lastEventText)
|
|
1855
|
+
) {
|
|
1856
|
+
return "stopped";
|
|
1857
|
+
}
|
|
1858
|
+
return "steady";
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function resolveRuntimeConvergenceEvidence(
|
|
1862
|
+
source: RuntimeBotStatus["status_source"],
|
|
1863
|
+
): RuntimeConvergenceEvidence {
|
|
1864
|
+
if (source === "manual") return "manual";
|
|
1865
|
+
if (source === "event") return "event";
|
|
1866
|
+
return "probe";
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function resolveRuntimeConvergenceSignal(
|
|
1870
|
+
entry: RuntimeBotStatus,
|
|
1871
|
+
): RuntimeConvergenceSignal {
|
|
1872
|
+
if (entry.link_status === "connected") return "success";
|
|
1873
|
+
const evidence = resolveRuntimeConvergenceEvidence(entry.status_source);
|
|
1874
|
+
if (
|
|
1875
|
+
(entry.link_status === "error" ||
|
|
1876
|
+
entry.link_status === "degraded" ||
|
|
1877
|
+
entry.link_status === "disconnected") &&
|
|
1878
|
+
(evidence === "event" || evidence === "probe")
|
|
1879
|
+
) {
|
|
1880
|
+
return "failure";
|
|
1881
|
+
}
|
|
1882
|
+
return "transient";
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function shouldGuardProbeConnectedWhilePendingWithoutEvent(params: {
|
|
1886
|
+
key: string;
|
|
1887
|
+
prev: RuntimeBotStatus;
|
|
1888
|
+
next: RuntimeBotStatus;
|
|
1889
|
+
}): boolean {
|
|
1890
|
+
const { key, prev, next } = params;
|
|
1891
|
+
const prevPhase = resolveRuntimeConvergencePhase(prev);
|
|
1892
|
+
if (prevPhase !== "pending") return false;
|
|
1893
|
+
if (resolveRuntimeConvergenceSignal(next) !== "success") return false;
|
|
1894
|
+
const nextEvidence = resolveRuntimeConvergenceEvidence(next.status_source);
|
|
1895
|
+
if (nextEvidence !== "probe") return false;
|
|
1896
|
+
if (!isWithinRuntimeConnectGraceForGuard(prev)) return false;
|
|
1897
|
+
return !hasFreshConnectedRuntimeEventLivenessForRecovery(key);
|
|
1781
1898
|
}
|
|
1782
1899
|
|
|
1783
1900
|
function shouldBypassProtectWindowForEventRecovery(params: {
|
|
@@ -1944,13 +2061,29 @@ function upsertRuntimeStatus(entry: RuntimeBotStatus): void {
|
|
|
1944
2061
|
entry.platform,
|
|
1945
2062
|
entry.bot_account_id,
|
|
1946
2063
|
);
|
|
1947
|
-
|
|
2064
|
+
let normalizedEntry: RuntimeBotStatus = {
|
|
1948
2065
|
...entry,
|
|
1949
2066
|
bot_account_id: normalizedAccountId || entry.bot_account_id,
|
|
1950
2067
|
};
|
|
1951
2068
|
|
|
1952
2069
|
const key = keyOf(normalizedEntry.platform, normalizedEntry.bot_account_id);
|
|
1953
2070
|
const prev = runtimeStatusRegistry.get(key);
|
|
2071
|
+
if (
|
|
2072
|
+
prev &&
|
|
2073
|
+
resolveRuntimeConvergencePhase(prev) === "pending" &&
|
|
2074
|
+
resolveRuntimeConvergenceSignal(normalizedEntry) === "success"
|
|
2075
|
+
) {
|
|
2076
|
+
const nextEvidence = resolveRuntimeConvergenceEvidence(
|
|
2077
|
+
normalizedEntry.status_source,
|
|
2078
|
+
);
|
|
2079
|
+
if (nextEvidence !== "probe") {
|
|
2080
|
+
normalizedEntry = {
|
|
2081
|
+
...normalizedEntry,
|
|
2082
|
+
link_status: "connecting",
|
|
2083
|
+
restart_pending: true,
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
1954
2087
|
const nextRestartPending = normalizedEntry.restart_pending ?? prev?.restart_pending ?? false;
|
|
1955
2088
|
if (normalizedEntry.running === undefined) {
|
|
1956
2089
|
normalizedEntry.running = resolveRuntimeRunningForUpdate({
|
|
@@ -2107,6 +2240,63 @@ function upsertRuntimeStatus(entry: RuntimeBotStatus): void {
|
|
|
2107
2240
|
return;
|
|
2108
2241
|
}
|
|
2109
2242
|
|
|
2243
|
+
if (
|
|
2244
|
+
prev &&
|
|
2245
|
+
shouldGuardProbeConnectedWhilePendingWithoutEvent({
|
|
2246
|
+
key,
|
|
2247
|
+
prev,
|
|
2248
|
+
next: normalizedEntry,
|
|
2249
|
+
})
|
|
2250
|
+
) {
|
|
2251
|
+
const prevRank = prev.last_status_rank ?? resolveStatusRank(prev);
|
|
2252
|
+
const nextRank = resolveStatusRank(normalizedEntry);
|
|
2253
|
+
const prevTransitionMs = parseIsoTimeMs(
|
|
2254
|
+
prev.transition_at || prev.last_heartbeat_at,
|
|
2255
|
+
);
|
|
2256
|
+
const ageMs =
|
|
2257
|
+
prevTransitionMs == null
|
|
2258
|
+
? null
|
|
2259
|
+
: Math.max(0, Date.now() - prevTransitionMs);
|
|
2260
|
+
|
|
2261
|
+
logSuppressedTransition({
|
|
2262
|
+
key,
|
|
2263
|
+
prev,
|
|
2264
|
+
next: normalizedEntry,
|
|
2265
|
+
reason: "probe_connected_guarded_by_pending_without_event",
|
|
2266
|
+
ageMs,
|
|
2267
|
+
windowMs: resolveStatusProtectWindowMs(prev),
|
|
2268
|
+
prevRank,
|
|
2269
|
+
nextRank,
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
const guardedApplied: RuntimeBotStatus = {
|
|
2273
|
+
...prev,
|
|
2274
|
+
last_heartbeat_at:
|
|
2275
|
+
normalizedEntry.last_heartbeat_at || prev.last_heartbeat_at,
|
|
2276
|
+
last_probe_at: normalizedEntry.last_probe_at || prev.last_probe_at,
|
|
2277
|
+
reconnect_count:
|
|
2278
|
+
normalizedEntry.reconnect_count ?? prev.reconnect_count,
|
|
2279
|
+
restart_pending:
|
|
2280
|
+
normalizedEntry.restart_pending ?? prev.restart_pending ?? true,
|
|
2281
|
+
running: prev.running ?? true,
|
|
2282
|
+
busy: normalizedEntry.busy ?? prev.busy,
|
|
2283
|
+
active_runs: normalizedEntry.active_runs ?? prev.active_runs,
|
|
2284
|
+
last_run_activity_at:
|
|
2285
|
+
normalizedEntry.last_run_activity_at || prev.last_run_activity_at,
|
|
2286
|
+
mode: normalizedEntry.mode || prev.mode,
|
|
2287
|
+
};
|
|
2288
|
+
|
|
2289
|
+
runtimeStatusRegistry.set(key, guardedApplied);
|
|
2290
|
+
syncRuntimeChannelSnapshotFromStatus(guardedApplied);
|
|
2291
|
+
mirrorHeartbeatFromStatus(guardedApplied);
|
|
2292
|
+
maybeAppendRuntimeStatusTransitionJournal(prev, guardedApplied);
|
|
2293
|
+
scheduleRuntimeStateSnapshotPersist();
|
|
2294
|
+
logRuntimeStatusDebug(
|
|
2295
|
+
`status guarded key=${key} prev=${prev.status_source || "probe"}/${prev.link_status} next=${normalizedEntry.status_source || "probe"}/${normalizedEntry.link_status} reason=probe_connected_guarded_by_pending_without_event`,
|
|
2296
|
+
);
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2110
2300
|
if (prev && normalizedEntry.status_source !== "manual") {
|
|
2111
2301
|
// manual 写入代表显式操作,优先级最高,不参与降级抑制。
|
|
2112
2302
|
const prevRank = prev.last_status_rank ?? resolveStatusRank(prev);
|
|
@@ -2125,12 +2315,18 @@ function upsertRuntimeStatus(entry: RuntimeBotStatus): void {
|
|
|
2125
2315
|
prev,
|
|
2126
2316
|
next: normalizedEntry,
|
|
2127
2317
|
});
|
|
2318
|
+
const bypassProtectWindowForReportedFailure =
|
|
2319
|
+
shouldApplyReportedFailureOverManualConnecting({
|
|
2320
|
+
previous: prev,
|
|
2321
|
+
next: normalizedEntry,
|
|
2322
|
+
});
|
|
2128
2323
|
const withinWindow = ageMs == null ? true : ageMs <= windowMs;
|
|
2129
2324
|
|
|
2130
2325
|
if (
|
|
2131
2326
|
nextRank < prevRank &&
|
|
2132
2327
|
withinWindow &&
|
|
2133
|
-
!bypassProtectWindowForEventRecovery
|
|
2328
|
+
!bypassProtectWindowForEventRecovery &&
|
|
2329
|
+
!bypassProtectWindowForReportedFailure
|
|
2134
2330
|
) {
|
|
2135
2331
|
// 低优先级回写被抑制时,仍更新部分时效字段,避免观测面停滞。
|
|
2136
2332
|
const preserveManualStopLifecycle =
|
|
@@ -4320,7 +4516,7 @@ function markRuntimeAccountsConnecting(
|
|
|
4320
4516
|
last_heartbeat_at: startedAt,
|
|
4321
4517
|
last_error: null,
|
|
4322
4518
|
reconnect_count: 0,
|
|
4323
|
-
restart_pending:
|
|
4519
|
+
restart_pending: true,
|
|
4324
4520
|
busy: false,
|
|
4325
4521
|
active_runs: 0,
|
|
4326
4522
|
last_run_activity_at: startedAt,
|
|
@@ -4965,6 +5161,12 @@ function applyRuntimeStatusSinkPayload(payload: RuntimeStatusSinkPayload): void
|
|
|
4965
5161
|
const botAccountId = String(payload.bot_account_id || "").trim();
|
|
4966
5162
|
if (!platform || !botAccountId) return;
|
|
4967
5163
|
const normalizedAccountId = normalizeRuntimeAccountId(platform, botAccountId);
|
|
5164
|
+
if (!isConfiguredRuntimeAccount(platform, normalizedAccountId || botAccountId)) {
|
|
5165
|
+
logRuntimeStatusDebug(
|
|
5166
|
+
`status sink ignored: unconfigured account platform=${platform} account=${normalizedAccountId || botAccountId}`,
|
|
5167
|
+
);
|
|
5168
|
+
return;
|
|
5169
|
+
}
|
|
4968
5170
|
const key = keyOf(platform, normalizedAccountId || botAccountId);
|
|
4969
5171
|
const previous = getRuntimeBotStatusByKey(key);
|
|
4970
5172
|
const nextLinkStatus = resolveLinkStatusFromSinkPayload(payload, previous);
|
|
@@ -4980,6 +5182,7 @@ function applyRuntimeStatusSinkPayload(payload: RuntimeStatusSinkPayload): void
|
|
|
4980
5182
|
hintedRestartPending:
|
|
4981
5183
|
typeof payload.restart_pending === "boolean" ? payload.restart_pending : undefined,
|
|
4982
5184
|
nextLinkStatus,
|
|
5185
|
+
nextStatusSource: statusSource,
|
|
4983
5186
|
});
|
|
4984
5187
|
const startedAt = normalizeRuntimeHintTimestamp(payload.started_at);
|
|
4985
5188
|
const heartbeatAt = normalizeRuntimeHeartbeatAt(
|
|
@@ -5805,18 +6008,25 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5805
6008
|
clearPlatformStatuses("weixin");
|
|
5806
6009
|
}
|
|
5807
6010
|
} else {
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
6011
|
+
const weixinMod = (await import("ylib-openclaw-weixin")) as Record<
|
|
6012
|
+
string,
|
|
6013
|
+
unknown
|
|
6014
|
+
>;
|
|
6015
|
+
const weixinPlugin = weixinMod.weixinPlugin as Record<string, unknown>;
|
|
6016
|
+
const configApi = weixinPlugin.config as Record<string, unknown>;
|
|
6017
|
+
const statusApi = weixinPlugin.status as Record<string, unknown>;
|
|
6018
|
+
const resolveAccount = configApi?.resolveAccount as
|
|
6019
|
+
| ((
|
|
6020
|
+
inputCfg: Record<string, unknown>,
|
|
6021
|
+
accountId: string,
|
|
6022
|
+
) => Record<string, unknown>)
|
|
6023
|
+
| undefined;
|
|
6024
|
+
const probeAccount = statusApi?.probeAccount as
|
|
6025
|
+
| ((args: {
|
|
6026
|
+
account: Record<string, unknown>;
|
|
6027
|
+
}) => Promise<Record<string, unknown>>)
|
|
6028
|
+
| undefined;
|
|
6029
|
+
|
|
5820
6030
|
prunePlatformStatusesByAccounts("weixin", configuredIds, cfg);
|
|
5821
6031
|
const weixinProbeTargets = filterRuntimeIdsForOnlyKey(
|
|
5822
6032
|
"weixin",
|
|
@@ -5829,7 +6039,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5829
6039
|
? weixinProbeTargets
|
|
5830
6040
|
: configuredIds
|
|
5831
6041
|
: [];
|
|
5832
|
-
if (weixinTargetsEffective.length > 0) {
|
|
6042
|
+
if (resolveAccount && probeAccount && weixinTargetsEffective.length > 0) {
|
|
5833
6043
|
await runWithConcurrency(
|
|
5834
6044
|
weixinTargetsEffective,
|
|
5835
6045
|
runtimeStatusProbeConcurrency.weixin,
|
|
@@ -5838,33 +6048,21 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5838
6048
|
if (shouldSkipAccountProbe(wk, forceProbeSweep)) return;
|
|
5839
6049
|
const previous = getRuntimeBotStatusByKey(keyOf("weixin", accountId));
|
|
5840
6050
|
try {
|
|
5841
|
-
const
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
force_probe: forceProviderProbe,
|
|
5849
|
-
timeoutMs: tw,
|
|
5850
|
-
},
|
|
5851
|
-
accountId,
|
|
6051
|
+
const account = resolveAccount(cfg, accountId);
|
|
6052
|
+
const probeResult = await withProbeTimeout(
|
|
6053
|
+
probeAccount({
|
|
6054
|
+
account,
|
|
6055
|
+
cfg,
|
|
6056
|
+
forceProbe: forceProviderProbe,
|
|
6057
|
+
force_probe: forceProviderProbe,
|
|
5852
6058
|
}),
|
|
5853
6059
|
tw,
|
|
5854
|
-
`[bridges/main] weixin
|
|
6060
|
+
`[bridges/main] weixin probeAccount timeout account=${accountId} timeoutMs=${tw}`,
|
|
6061
|
+
);
|
|
6062
|
+
const ok = probeResult?.ok !== false;
|
|
6063
|
+
const probeError = buildDetailedProbeErrorMessage(
|
|
6064
|
+
probeResult as Record<string, unknown>,
|
|
5855
6065
|
);
|
|
5856
|
-
const probeResult =
|
|
5857
|
-
invoke?.result && typeof invoke.result === "object"
|
|
5858
|
-
? (invoke.result as Record<string, unknown>)
|
|
5859
|
-
: null;
|
|
5860
|
-
const probeOk =
|
|
5861
|
-
invoke?.ok === true &&
|
|
5862
|
-
(!probeResult ||
|
|
5863
|
-
probeResult.ok === undefined ||
|
|
5864
|
-
probeResult.ok !== false);
|
|
5865
|
-
const probeError = probeResult
|
|
5866
|
-
? buildDetailedProbeErrorMessage(probeResult)
|
|
5867
|
-
: String(invoke?.error || "").trim();
|
|
5868
6066
|
const probeErrorWithLogFallback = isGenericRuntimeErrorMessage(
|
|
5869
6067
|
probeError,
|
|
5870
6068
|
)
|
|
@@ -5873,35 +6071,41 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5873
6071
|
readRecentBridgeErrorFromLog(accountId, "weixin"),
|
|
5874
6072
|
)
|
|
5875
6073
|
: probeError;
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
6074
|
+
if (!ok) {
|
|
6075
|
+
console.error(
|
|
6076
|
+
`[bridges/main] weixin probeAccount failed account=${accountId} raw=${toLogText(probeResult)}`,
|
|
6077
|
+
);
|
|
6078
|
+
console.error(
|
|
6079
|
+
`[bridges/main] weixin probeAccount derived_error account=${accountId} error=${toLogText(probeErrorWithLogFallback || "")}`,
|
|
6080
|
+
);
|
|
6081
|
+
}
|
|
6082
|
+
const error = ok
|
|
5883
6083
|
? null
|
|
5884
6084
|
: pickMoreSpecificErrorMessage(
|
|
5885
6085
|
previous?.last_error || null,
|
|
5886
|
-
probeErrorWithLogFallback
|
|
6086
|
+
probeErrorWithLogFallback,
|
|
5887
6087
|
);
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
6088
|
+
if (!ok) {
|
|
6089
|
+
console.error(
|
|
6090
|
+
`[bridges/main] weixin probeAccount merged_error account=${accountId} error=${toLogText(error || "")}`,
|
|
6091
|
+
);
|
|
6092
|
+
}
|
|
6093
|
+
const probeAt = nowIso();
|
|
6094
|
+
const runtimeHints = extractRuntimeStatusHints(probeResult);
|
|
5891
6095
|
upsertRuntimeProbe({
|
|
5892
6096
|
platform: "weixin",
|
|
5893
6097
|
bot_account_id: accountId,
|
|
5894
6098
|
last_probe_at: probeAt,
|
|
5895
|
-
ok
|
|
5896
|
-
error
|
|
5897
|
-
result: probeResult,
|
|
6099
|
+
ok,
|
|
6100
|
+
error,
|
|
6101
|
+
result: (probeResult as Record<string, unknown>) || null,
|
|
5898
6102
|
source: "probe",
|
|
5899
6103
|
});
|
|
5900
6104
|
await maybeRunChannelAuditAfterProbe({
|
|
5901
|
-
statusApi
|
|
5902
|
-
account
|
|
6105
|
+
statusApi,
|
|
6106
|
+
account,
|
|
5903
6107
|
cfg,
|
|
5904
|
-
probeResult,
|
|
6108
|
+
probeResult: (probeResult as Record<string, unknown>) || null,
|
|
5905
6109
|
timeoutMs: tw,
|
|
5906
6110
|
platform: "weixin",
|
|
5907
6111
|
bot_account_id: accountId,
|
|
@@ -5909,16 +6113,16 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5909
6113
|
upsertRuntimeStatus({
|
|
5910
6114
|
platform: "weixin",
|
|
5911
6115
|
bot_account_id: accountId,
|
|
5912
|
-
link_status:
|
|
6116
|
+
link_status: ok ? "connected" : "error",
|
|
5913
6117
|
started_at: previous?.started_at || nowIso(),
|
|
5914
6118
|
last_heartbeat_at: probeAt,
|
|
5915
|
-
last_error:
|
|
6119
|
+
last_error: error,
|
|
5916
6120
|
reconnect_count:
|
|
5917
6121
|
runtimeHints.reconnect_count ?? previous?.reconnect_count ?? 0,
|
|
5918
6122
|
restart_pending: resolveRuntimeRestartPendingForUpdate({
|
|
5919
6123
|
previous,
|
|
5920
6124
|
hintedRestartPending: runtimeHints.restart_pending,
|
|
5921
|
-
nextLinkStatus:
|
|
6125
|
+
nextLinkStatus: ok ? "connected" : "error",
|
|
5922
6126
|
}),
|
|
5923
6127
|
busy: runtimeHints.busy ?? false,
|
|
5924
6128
|
active_runs: runtimeHints.active_runs ?? 0,
|
|
@@ -5946,7 +6150,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5946
6150
|
upsertRuntimeStatus({
|
|
5947
6151
|
platform: "weixin",
|
|
5948
6152
|
bot_account_id: accountId,
|
|
5949
|
-
link_status:
|
|
6153
|
+
link_status: "error",
|
|
5950
6154
|
started_at: previous?.started_at || nowIso(),
|
|
5951
6155
|
last_heartbeat_at: probeAt,
|
|
5952
6156
|
last_error: probeError,
|
|
@@ -5954,7 +6158,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5954
6158
|
restart_pending: resolveRuntimeRestartPendingForUpdate({
|
|
5955
6159
|
previous,
|
|
5956
6160
|
hintedRestartPending: undefined,
|
|
5957
|
-
nextLinkStatus:
|
|
6161
|
+
nextLinkStatus: "error",
|
|
5958
6162
|
}),
|
|
5959
6163
|
busy: false,
|
|
5960
6164
|
active_runs: 0,
|
|
@@ -6974,8 +7178,8 @@ async function buildRuntimeBotsStatusPayload(params: {
|
|
|
6974
7178
|
generatedAtMs: Date.now(),
|
|
6975
7179
|
onlyAccountKey: onlyKey,
|
|
6976
7180
|
});
|
|
6977
|
-
const summary = summarizeBots();
|
|
6978
|
-
const runtimeReadiness = buildRuntimeReadinessSnapshot(
|
|
7181
|
+
const summary = summarizeBots(onlyKey);
|
|
7182
|
+
const runtimeReadiness = buildRuntimeReadinessSnapshot(botsComposed, nowIso());
|
|
6979
7183
|
const tApplied = lastProbeSweepTimeouts;
|
|
6980
7184
|
return {
|
|
6981
7185
|
ok: true,
|
|
@@ -7478,11 +7682,11 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7478
7682
|
platform,
|
|
7479
7683
|
bot_account_id: botAccountId,
|
|
7480
7684
|
link_status: "connecting",
|
|
7481
|
-
started_at:
|
|
7685
|
+
started_at: now,
|
|
7482
7686
|
last_heartbeat_at: now,
|
|
7483
7687
|
last_error: null,
|
|
7484
7688
|
reconnect_count: previous?.reconnect_count || 0,
|
|
7485
|
-
restart_pending:
|
|
7689
|
+
restart_pending: true,
|
|
7486
7690
|
busy: false,
|
|
7487
7691
|
active_runs: 0,
|
|
7488
7692
|
last_run_activity_at: now,
|
|
@@ -7508,6 +7712,28 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7508
7712
|
);
|
|
7509
7713
|
return;
|
|
7510
7714
|
}
|
|
7715
|
+
if (
|
|
7716
|
+
previous?.link_status === "disconnected" &&
|
|
7717
|
+
previous?.status_source === "manual"
|
|
7718
|
+
) {
|
|
7719
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
7720
|
+
res.end(
|
|
7721
|
+
JSON.stringify({
|
|
7722
|
+
ok: true,
|
|
7723
|
+
status: "noop",
|
|
7724
|
+
action: "stop",
|
|
7725
|
+
reason: "already_stopped",
|
|
7726
|
+
platform,
|
|
7727
|
+
bot_account_id: botAccountId,
|
|
7728
|
+
operation_id: operationId,
|
|
7729
|
+
runtime_instance_id: runtimeInstanceId,
|
|
7730
|
+
runtime_state: runtimeState,
|
|
7731
|
+
issued_at: now,
|
|
7732
|
+
bot: previous,
|
|
7733
|
+
}),
|
|
7734
|
+
);
|
|
7735
|
+
return;
|
|
7736
|
+
}
|
|
7511
7737
|
await withRuntimeControlTimeout(
|
|
7512
7738
|
control.stopAccount(botAccountId),
|
|
7513
7739
|
`bot stop timeout platform=${platform} account=${botAccountId}`,
|
|
@@ -7549,7 +7775,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7549
7775
|
platform,
|
|
7550
7776
|
bot_account_id: botAccountId,
|
|
7551
7777
|
link_status: "connecting",
|
|
7552
|
-
started_at:
|
|
7778
|
+
started_at: now,
|
|
7553
7779
|
last_heartbeat_at: now,
|
|
7554
7780
|
last_error: null,
|
|
7555
7781
|
reconnect_count: previous?.reconnect_count || 0,
|
|
@@ -7812,19 +8038,20 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7812
8038
|
);
|
|
7813
8039
|
return;
|
|
7814
8040
|
}
|
|
7815
|
-
const body = await readRequestJson(req);
|
|
7816
|
-
const restartModeRaw = String(body.mode || "")
|
|
7817
|
-
.trim()
|
|
7818
|
-
.toLowerCase();
|
|
7819
|
-
const restartMode = restartModeRaw === "full" ? "full" : "soft";
|
|
7820
|
-
// 重启流程:显式 stop 标记 -> 拉取并落盘配置 -> 进入 connecting -> 等待状态收敛。
|
|
7821
|
-
console.log(`[bridges/main] restart-all requested mode=${restartMode}`);
|
|
7822
8041
|
const restartRequestAt = nowIso();
|
|
7823
8042
|
console.log(
|
|
7824
8043
|
`[bridges/main] restart-all step=enter at=${restartRequestAt} runtime_state=${runtimeState} instance=${runtimeInstanceId}`,
|
|
7825
8044
|
);
|
|
7826
8045
|
runtimeState = "restarting";
|
|
8046
|
+
let restartMode = "soft";
|
|
7827
8047
|
try {
|
|
8048
|
+
const body = await readRequestJson(req);
|
|
8049
|
+
const restartModeRaw = String(body.mode || "")
|
|
8050
|
+
.trim()
|
|
8051
|
+
.toLowerCase();
|
|
8052
|
+
restartMode = restartModeRaw === "full" ? "full" : "soft";
|
|
8053
|
+
// 重启流程:显式 stop 标记 -> 拉取并落盘配置 -> 进入 connecting -> 等待状态收敛。
|
|
8054
|
+
console.log(`[bridges/main] restart-all requested mode=${restartMode}`);
|
|
7828
8055
|
if (runtimeConfigPullUrl) {
|
|
7829
8056
|
console.log(
|
|
7830
8057
|
`[bridges/main] restart-all step=pull_config begin url=${runtimeConfigPullUrl}`,
|
|
@@ -8333,6 +8560,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
8333
8560
|
previous: prev,
|
|
8334
8561
|
hintedRestartPending: runtimeHintsFromEvent.restart_pending,
|
|
8335
8562
|
nextLinkStatus: prev.link_status,
|
|
8563
|
+
nextStatusSource: prev.status_source,
|
|
8336
8564
|
});
|
|
8337
8565
|
upsertRuntimeStatus({
|
|
8338
8566
|
...prev,
|
|
@@ -8390,6 +8618,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
8390
8618
|
previous: prev,
|
|
8391
8619
|
hintedRestartPending: runtimeHintsFromEvent.restart_pending,
|
|
8392
8620
|
nextLinkStatus: normalizedStatus,
|
|
8621
|
+
nextStatusSource: "event",
|
|
8393
8622
|
});
|
|
8394
8623
|
upsertRuntimeStatus({
|
|
8395
8624
|
platform: p,
|
|
@@ -8690,6 +8919,14 @@ async function main(): Promise<void> {
|
|
|
8690
8919
|
restorePersistedRuntimeStateSnapshot(
|
|
8691
8920
|
loadedConfigForRuntime as Record<string, unknown> | null,
|
|
8692
8921
|
);
|
|
8922
|
+
// 启动恢复后强制进入新生命周期 connecting 态,避免旧快照 connected 直接外显。
|
|
8923
|
+
markConfiguredBotsAsConnecting(
|
|
8924
|
+
(loadedConfigForRuntime as Record<string, unknown>) || {
|
|
8925
|
+
channels: {},
|
|
8926
|
+
bindings: [],
|
|
8927
|
+
},
|
|
8928
|
+
"startup_after_restore_connecting",
|
|
8929
|
+
);
|
|
8693
8930
|
}
|
|
8694
8931
|
|
|
8695
8932
|
if (runtimeConfigPullUrl && !localConfigOnly && shouldRestoreRuntimeStateSnapshot) {
|
|
@@ -150,6 +150,33 @@ const runtimeSkipStaleHeartbeatModes = new Set(["webhook"]);
|
|
|
150
150
|
const runtimeBusyActivityStaleMsDefault = 25 * 60_000;
|
|
151
151
|
const runtimeGaveUpReconnectThreshold = 10;
|
|
152
152
|
|
|
153
|
+
export function isExplicitReportedRuntimeFailure(
|
|
154
|
+
status: Pick<RuntimeStatusLike, "link_status" | "status_source">,
|
|
155
|
+
): boolean {
|
|
156
|
+
return (
|
|
157
|
+
(status.status_source === "probe" || status.status_source === "event") &&
|
|
158
|
+
(status.link_status === "error" ||
|
|
159
|
+
status.link_status === "degraded" ||
|
|
160
|
+
status.link_status === "disconnected")
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function shouldApplyReportedFailureOverManualConnecting(params: {
|
|
165
|
+
previous:
|
|
166
|
+
| Pick<RuntimeStatusLike, "link_status" | "status_source">
|
|
167
|
+
| null
|
|
168
|
+
| undefined;
|
|
169
|
+
next: Pick<RuntimeStatusLike, "link_status" | "status_source">;
|
|
170
|
+
}): boolean {
|
|
171
|
+
const previous = params.previous;
|
|
172
|
+
const previousSource = previous?.status_source || "probe";
|
|
173
|
+
return (
|
|
174
|
+
previousSource === "manual" &&
|
|
175
|
+
previous?.link_status === "connecting" &&
|
|
176
|
+
isExplicitReportedRuntimeFailure(params.next)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
153
180
|
function parseIsoTimeMs(value: string | null | undefined): number | null {
|
|
154
181
|
const text = String(value || "").trim();
|
|
155
182
|
if (!text) return null;
|
|
@@ -242,12 +269,7 @@ export function evaluateRuntimeHealth(params: {
|
|
|
242
269
|
};
|
|
243
270
|
}
|
|
244
271
|
|
|
245
|
-
|
|
246
|
-
(status.status_source === "probe" || status.status_source === "event") &&
|
|
247
|
-
(status.link_status === "error" ||
|
|
248
|
-
status.link_status === "degraded" ||
|
|
249
|
-
status.link_status === "disconnected");
|
|
250
|
-
if (explicitReportedFailure) {
|
|
272
|
+
if (isExplicitReportedRuntimeFailure(status)) {
|
|
251
273
|
return { healthy: false, reason: "disconnected", evaluated_at: nowIsoText };
|
|
252
274
|
}
|
|
253
275
|
|
|
@@ -410,11 +432,28 @@ export function resolveRuntimeDisplayState(params: {
|
|
|
410
432
|
};
|
|
411
433
|
|
|
412
434
|
const linkConnected = params.status.link_status === "connected";
|
|
413
|
-
if (params.status.restart_pending === true
|
|
435
|
+
if (params.status.restart_pending === true) {
|
|
436
|
+
if (statusText === "error") {
|
|
437
|
+
return build("error", "status:error", "status", {
|
|
438
|
+
hint: "重启/启动流程中探测失败,请检查凭证与网络后重试。",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (statusText === "disconnected") {
|
|
442
|
+
return build("offline", "status:disconnected", "status", {
|
|
443
|
+
hint: "重启/启动流程中连接未建立,请检查运行链路。",
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
if (statusText === "degraded") {
|
|
447
|
+
return build("degraded", "status:degraded", "status", {
|
|
448
|
+
hint: "重启/启动流程中链路不稳定,等待下一轮探测收敛。",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
414
451
|
return build("reconnecting", "status:restart_pending", "status", {
|
|
415
452
|
label: "重连中",
|
|
416
453
|
color: "processing",
|
|
417
|
-
hint:
|
|
454
|
+
hint: linkConnected
|
|
455
|
+
? "重启流程进行中,等待探测确认完成后再切换在线。"
|
|
456
|
+
: "重启或重连流程进行中,尚未回到已连接状态。",
|
|
418
457
|
});
|
|
419
458
|
}
|
|
420
459
|
|
|
@@ -467,14 +506,19 @@ export function resolveRuntimeDisplayState(params: {
|
|
|
467
506
|
});
|
|
468
507
|
}
|
|
469
508
|
if (params.healthReason === "startup_grace") {
|
|
470
|
-
|
|
509
|
+
const statusSource = String(params.status.status_source || "")
|
|
510
|
+
.trim()
|
|
511
|
+
.toLowerCase();
|
|
512
|
+
if (statusText === "connected" && statusSource === "event") {
|
|
471
513
|
return build("online", "status:connected", "status", {
|
|
472
514
|
label: "在线",
|
|
473
515
|
color: "success",
|
|
474
|
-
hint: "
|
|
516
|
+
hint: "已收到连接成功事件,当前仍处于启动宽限窗口。",
|
|
475
517
|
});
|
|
476
518
|
}
|
|
477
|
-
return build("connecting", "health:startup_grace", "health"
|
|
519
|
+
return build("connecting", "health:startup_grace", "health", {
|
|
520
|
+
hint: "启动宽限期内先显示连接中,收到连接成功事件后再切换在线。",
|
|
521
|
+
});
|
|
478
522
|
}
|
|
479
523
|
|
|
480
524
|
const normalizedFromStatus = normalizeRuntimeDisplayStatusFromText(statusText);
|
package/ecosystem.config.cjs
CHANGED
|
@@ -15,6 +15,7 @@ module.exports = {
|
|
|
15
15
|
ENABLE_INTERNAL_API: "1",
|
|
16
16
|
INTERNAL_CONTROL_PORT: "18999",
|
|
17
17
|
INTERNAL_FIXED_TOKEN: "syim_runtime",
|
|
18
|
+
NODE_OPTIONS: process.env.NODE_OPTIONS || "--max-old-space-size=512",
|
|
18
19
|
RUNTIME_CONFIG_PULL_URL:
|
|
19
20
|
"http://127.0.0.1:3999/api/v1/yucegpt/im/runtime/config/full",
|
|
20
21
|
RESTART_ALL_EXIT_PROCESS: "0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-syim",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
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.16",
|
|
50
|
+
"ylib-openclaw-lark": "2026.3.17-beta.21",
|
|
51
|
+
"ylib-openclaw-weixin": "2.1.7-beta.9",
|
|
52
52
|
"axios": "^1.6.0",
|
|
53
53
|
"dingtalk-stream": "^2.1.4",
|
|
54
54
|
"fluent-ffmpeg": "^2.1.3",
|