ylib-syim 0.0.25 → 0.0.26
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 +300 -70
- package/bridges/runtime/runtime-health-policy.ts +55 -11
- package/package.json +2 -2
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,
|
|
@@ -4980,6 +5176,7 @@ function applyRuntimeStatusSinkPayload(payload: RuntimeStatusSinkPayload): void
|
|
|
4980
5176
|
hintedRestartPending:
|
|
4981
5177
|
typeof payload.restart_pending === "boolean" ? payload.restart_pending : undefined,
|
|
4982
5178
|
nextLinkStatus,
|
|
5179
|
+
nextStatusSource: statusSource,
|
|
4983
5180
|
});
|
|
4984
5181
|
const startedAt = normalizeRuntimeHintTimestamp(payload.started_at);
|
|
4985
5182
|
const heartbeatAt = normalizeRuntimeHeartbeatAt(
|
|
@@ -5805,18 +6002,25 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5805
6002
|
clearPlatformStatuses("weixin");
|
|
5806
6003
|
}
|
|
5807
6004
|
} else {
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
|
|
6005
|
+
const weixinMod = (await import("ylib-openclaw-weixin")) as Record<
|
|
6006
|
+
string,
|
|
6007
|
+
unknown
|
|
6008
|
+
>;
|
|
6009
|
+
const weixinPlugin = weixinMod.weixinPlugin as Record<string, unknown>;
|
|
6010
|
+
const configApi = weixinPlugin.config as Record<string, unknown>;
|
|
6011
|
+
const statusApi = weixinPlugin.status as Record<string, unknown>;
|
|
6012
|
+
const resolveAccount = configApi?.resolveAccount as
|
|
6013
|
+
| ((
|
|
6014
|
+
inputCfg: Record<string, unknown>,
|
|
6015
|
+
accountId: string,
|
|
6016
|
+
) => Record<string, unknown>)
|
|
6017
|
+
| undefined;
|
|
6018
|
+
const probeAccount = statusApi?.probeAccount as
|
|
6019
|
+
| ((args: {
|
|
6020
|
+
account: Record<string, unknown>;
|
|
6021
|
+
}) => Promise<Record<string, unknown>>)
|
|
6022
|
+
| undefined;
|
|
6023
|
+
|
|
5820
6024
|
prunePlatformStatusesByAccounts("weixin", configuredIds, cfg);
|
|
5821
6025
|
const weixinProbeTargets = filterRuntimeIdsForOnlyKey(
|
|
5822
6026
|
"weixin",
|
|
@@ -5829,7 +6033,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5829
6033
|
? weixinProbeTargets
|
|
5830
6034
|
: configuredIds
|
|
5831
6035
|
: [];
|
|
5832
|
-
if (weixinTargetsEffective.length > 0) {
|
|
6036
|
+
if (resolveAccount && probeAccount && weixinTargetsEffective.length > 0) {
|
|
5833
6037
|
await runWithConcurrency(
|
|
5834
6038
|
weixinTargetsEffective,
|
|
5835
6039
|
runtimeStatusProbeConcurrency.weixin,
|
|
@@ -5838,33 +6042,21 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5838
6042
|
if (shouldSkipAccountProbe(wk, forceProbeSweep)) return;
|
|
5839
6043
|
const previous = getRuntimeBotStatusByKey(keyOf("weixin", accountId));
|
|
5840
6044
|
try {
|
|
5841
|
-
const
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
force_probe: forceProviderProbe,
|
|
5849
|
-
timeoutMs: tw,
|
|
5850
|
-
},
|
|
5851
|
-
accountId,
|
|
6045
|
+
const account = resolveAccount(cfg, accountId);
|
|
6046
|
+
const probeResult = await withProbeTimeout(
|
|
6047
|
+
probeAccount({
|
|
6048
|
+
account,
|
|
6049
|
+
cfg,
|
|
6050
|
+
forceProbe: forceProviderProbe,
|
|
6051
|
+
force_probe: forceProviderProbe,
|
|
5852
6052
|
}),
|
|
5853
6053
|
tw,
|
|
5854
|
-
`[bridges/main] weixin
|
|
6054
|
+
`[bridges/main] weixin probeAccount timeout account=${accountId} timeoutMs=${tw}`,
|
|
6055
|
+
);
|
|
6056
|
+
const ok = probeResult?.ok !== false;
|
|
6057
|
+
const probeError = buildDetailedProbeErrorMessage(
|
|
6058
|
+
probeResult as Record<string, unknown>,
|
|
5855
6059
|
);
|
|
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
6060
|
const probeErrorWithLogFallback = isGenericRuntimeErrorMessage(
|
|
5869
6061
|
probeError,
|
|
5870
6062
|
)
|
|
@@ -5873,35 +6065,41 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5873
6065
|
readRecentBridgeErrorFromLog(accountId, "weixin"),
|
|
5874
6066
|
)
|
|
5875
6067
|
: probeError;
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
6068
|
+
if (!ok) {
|
|
6069
|
+
console.error(
|
|
6070
|
+
`[bridges/main] weixin probeAccount failed account=${accountId} raw=${toLogText(probeResult)}`,
|
|
6071
|
+
);
|
|
6072
|
+
console.error(
|
|
6073
|
+
`[bridges/main] weixin probeAccount derived_error account=${accountId} error=${toLogText(probeErrorWithLogFallback || "")}`,
|
|
6074
|
+
);
|
|
6075
|
+
}
|
|
6076
|
+
const error = ok
|
|
5883
6077
|
? null
|
|
5884
6078
|
: pickMoreSpecificErrorMessage(
|
|
5885
6079
|
previous?.last_error || null,
|
|
5886
|
-
probeErrorWithLogFallback
|
|
6080
|
+
probeErrorWithLogFallback,
|
|
5887
6081
|
);
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
6082
|
+
if (!ok) {
|
|
6083
|
+
console.error(
|
|
6084
|
+
`[bridges/main] weixin probeAccount merged_error account=${accountId} error=${toLogText(error || "")}`,
|
|
6085
|
+
);
|
|
6086
|
+
}
|
|
6087
|
+
const probeAt = nowIso();
|
|
6088
|
+
const runtimeHints = extractRuntimeStatusHints(probeResult);
|
|
5891
6089
|
upsertRuntimeProbe({
|
|
5892
6090
|
platform: "weixin",
|
|
5893
6091
|
bot_account_id: accountId,
|
|
5894
6092
|
last_probe_at: probeAt,
|
|
5895
|
-
ok
|
|
5896
|
-
error
|
|
5897
|
-
result: probeResult,
|
|
6093
|
+
ok,
|
|
6094
|
+
error,
|
|
6095
|
+
result: (probeResult as Record<string, unknown>) || null,
|
|
5898
6096
|
source: "probe",
|
|
5899
6097
|
});
|
|
5900
6098
|
await maybeRunChannelAuditAfterProbe({
|
|
5901
|
-
statusApi
|
|
5902
|
-
account
|
|
6099
|
+
statusApi,
|
|
6100
|
+
account,
|
|
5903
6101
|
cfg,
|
|
5904
|
-
probeResult,
|
|
6102
|
+
probeResult: (probeResult as Record<string, unknown>) || null,
|
|
5905
6103
|
timeoutMs: tw,
|
|
5906
6104
|
platform: "weixin",
|
|
5907
6105
|
bot_account_id: accountId,
|
|
@@ -5909,16 +6107,16 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5909
6107
|
upsertRuntimeStatus({
|
|
5910
6108
|
platform: "weixin",
|
|
5911
6109
|
bot_account_id: accountId,
|
|
5912
|
-
link_status:
|
|
6110
|
+
link_status: ok ? "connected" : "error",
|
|
5913
6111
|
started_at: previous?.started_at || nowIso(),
|
|
5914
6112
|
last_heartbeat_at: probeAt,
|
|
5915
|
-
last_error:
|
|
6113
|
+
last_error: error,
|
|
5916
6114
|
reconnect_count:
|
|
5917
6115
|
runtimeHints.reconnect_count ?? previous?.reconnect_count ?? 0,
|
|
5918
6116
|
restart_pending: resolveRuntimeRestartPendingForUpdate({
|
|
5919
6117
|
previous,
|
|
5920
6118
|
hintedRestartPending: runtimeHints.restart_pending,
|
|
5921
|
-
nextLinkStatus:
|
|
6119
|
+
nextLinkStatus: ok ? "connected" : "error",
|
|
5922
6120
|
}),
|
|
5923
6121
|
busy: runtimeHints.busy ?? false,
|
|
5924
6122
|
active_runs: runtimeHints.active_runs ?? 0,
|
|
@@ -5946,7 +6144,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5946
6144
|
upsertRuntimeStatus({
|
|
5947
6145
|
platform: "weixin",
|
|
5948
6146
|
bot_account_id: accountId,
|
|
5949
|
-
link_status:
|
|
6147
|
+
link_status: "error",
|
|
5950
6148
|
started_at: previous?.started_at || nowIso(),
|
|
5951
6149
|
last_heartbeat_at: probeAt,
|
|
5952
6150
|
last_error: probeError,
|
|
@@ -5954,7 +6152,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5954
6152
|
restart_pending: resolveRuntimeRestartPendingForUpdate({
|
|
5955
6153
|
previous,
|
|
5956
6154
|
hintedRestartPending: undefined,
|
|
5957
|
-
nextLinkStatus:
|
|
6155
|
+
nextLinkStatus: "error",
|
|
5958
6156
|
}),
|
|
5959
6157
|
busy: false,
|
|
5960
6158
|
active_runs: 0,
|
|
@@ -7478,11 +7676,11 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7478
7676
|
platform,
|
|
7479
7677
|
bot_account_id: botAccountId,
|
|
7480
7678
|
link_status: "connecting",
|
|
7481
|
-
started_at:
|
|
7679
|
+
started_at: now,
|
|
7482
7680
|
last_heartbeat_at: now,
|
|
7483
7681
|
last_error: null,
|
|
7484
7682
|
reconnect_count: previous?.reconnect_count || 0,
|
|
7485
|
-
restart_pending:
|
|
7683
|
+
restart_pending: true,
|
|
7486
7684
|
busy: false,
|
|
7487
7685
|
active_runs: 0,
|
|
7488
7686
|
last_run_activity_at: now,
|
|
@@ -7508,6 +7706,28 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7508
7706
|
);
|
|
7509
7707
|
return;
|
|
7510
7708
|
}
|
|
7709
|
+
if (
|
|
7710
|
+
previous?.link_status === "disconnected" &&
|
|
7711
|
+
previous?.status_source === "manual"
|
|
7712
|
+
) {
|
|
7713
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
7714
|
+
res.end(
|
|
7715
|
+
JSON.stringify({
|
|
7716
|
+
ok: true,
|
|
7717
|
+
status: "noop",
|
|
7718
|
+
action: "stop",
|
|
7719
|
+
reason: "already_stopped",
|
|
7720
|
+
platform,
|
|
7721
|
+
bot_account_id: botAccountId,
|
|
7722
|
+
operation_id: operationId,
|
|
7723
|
+
runtime_instance_id: runtimeInstanceId,
|
|
7724
|
+
runtime_state: runtimeState,
|
|
7725
|
+
issued_at: now,
|
|
7726
|
+
bot: previous,
|
|
7727
|
+
}),
|
|
7728
|
+
);
|
|
7729
|
+
return;
|
|
7730
|
+
}
|
|
7511
7731
|
await withRuntimeControlTimeout(
|
|
7512
7732
|
control.stopAccount(botAccountId),
|
|
7513
7733
|
`bot stop timeout platform=${platform} account=${botAccountId}`,
|
|
@@ -7549,7 +7769,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7549
7769
|
platform,
|
|
7550
7770
|
bot_account_id: botAccountId,
|
|
7551
7771
|
link_status: "connecting",
|
|
7552
|
-
started_at:
|
|
7772
|
+
started_at: now,
|
|
7553
7773
|
last_heartbeat_at: now,
|
|
7554
7774
|
last_error: null,
|
|
7555
7775
|
reconnect_count: previous?.reconnect_count || 0,
|
|
@@ -8333,6 +8553,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
8333
8553
|
previous: prev,
|
|
8334
8554
|
hintedRestartPending: runtimeHintsFromEvent.restart_pending,
|
|
8335
8555
|
nextLinkStatus: prev.link_status,
|
|
8556
|
+
nextStatusSource: prev.status_source,
|
|
8336
8557
|
});
|
|
8337
8558
|
upsertRuntimeStatus({
|
|
8338
8559
|
...prev,
|
|
@@ -8390,6 +8611,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
8390
8611
|
previous: prev,
|
|
8391
8612
|
hintedRestartPending: runtimeHintsFromEvent.restart_pending,
|
|
8392
8613
|
nextLinkStatus: normalizedStatus,
|
|
8614
|
+
nextStatusSource: "event",
|
|
8393
8615
|
});
|
|
8394
8616
|
upsertRuntimeStatus({
|
|
8395
8617
|
platform: p,
|
|
@@ -8690,6 +8912,14 @@ async function main(): Promise<void> {
|
|
|
8690
8912
|
restorePersistedRuntimeStateSnapshot(
|
|
8691
8913
|
loadedConfigForRuntime as Record<string, unknown> | null,
|
|
8692
8914
|
);
|
|
8915
|
+
// 启动恢复后强制进入新生命周期 connecting 态,避免旧快照 connected 直接外显。
|
|
8916
|
+
markConfiguredBotsAsConnecting(
|
|
8917
|
+
(loadedConfigForRuntime as Record<string, unknown>) || {
|
|
8918
|
+
channels: {},
|
|
8919
|
+
bindings: [],
|
|
8920
|
+
},
|
|
8921
|
+
"startup_after_restore_connecting",
|
|
8922
|
+
);
|
|
8693
8923
|
}
|
|
8694
8924
|
|
|
8695
8925
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-syim",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
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-beta.15",
|
|
50
50
|
"ylib-openclaw-lark": "2026.3.17-beta.20",
|
|
51
|
-
"ylib-openclaw-weixin": "2.1.7-beta.
|
|
51
|
+
"ylib-openclaw-weixin": "2.1.7-beta.8",
|
|
52
52
|
"axios": "^1.6.0",
|
|
53
53
|
"dingtalk-stream": "^2.1.4",
|
|
54
54
|
"fluent-ffmpeg": "^2.1.3",
|