ylib-syim 0.0.27 → 0.0.30
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 +106 -25
- package/bridges/runtime/bot-runtime-registry.ts +4 -0
- package/bridges/runtime/runtime-health-policy.ts +37 -4
- package/package.json +4 -4
- package/scripts/dingtalk-stdio-bridge.ts +20 -1
- package/scripts/lark-stdio-bridge.ts +20 -1
- package/scripts/weixin-stdio-bridge.ts +20 -1
package/bridges/main.ts
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
saveRuntimeStateSnapshot,
|
|
23
23
|
} from "./runtime/runtime-state-store.ts";
|
|
24
24
|
|
|
25
|
+
process.env.TZ = "Asia/Shanghai";
|
|
26
|
+
|
|
25
27
|
// bridges/main 是 Node 侧运行时中枢:
|
|
26
28
|
// 1) 维护配置与 bridge 进程生命周期;
|
|
27
29
|
// 2) 统一汇聚 status/heartbeat/diagnostic/probe 四类运行态;
|
|
@@ -1267,6 +1269,12 @@ function nowIso(): string {
|
|
|
1267
1269
|
return new Date().toISOString();
|
|
1268
1270
|
}
|
|
1269
1271
|
|
|
1272
|
+
/** 将 UTC ISO 字符串转为东八区 ISO 字符串(+08:00)。用于「最近探测」等前台展示字段。 */
|
|
1273
|
+
function toChina8Iso(utcIso: string): string {
|
|
1274
|
+
const ms = new Date(utcIso).getTime() + 8 * 60 * 60 * 1000;
|
|
1275
|
+
return new Date(ms).toISOString().replace('Z', '+08:00');
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1270
1278
|
// rotateRuntimeInstanceId: 刷新运行实例标识。
|
|
1271
1279
|
function rotateRuntimeInstanceId(): string {
|
|
1272
1280
|
// 用于标识一次新的运行代际(例如 restart-all 后)。
|
|
@@ -1597,6 +1605,10 @@ function resolveRuntimeRunningForUpdate(params: {
|
|
|
1597
1605
|
nextLastEvent: string | null | undefined;
|
|
1598
1606
|
nextRestartPending: boolean;
|
|
1599
1607
|
}): boolean {
|
|
1608
|
+
// running 推导规则:
|
|
1609
|
+
// 1) 优先尊重显式 hintedRunning;
|
|
1610
|
+
// 2) event 断链(非 stop 生命周期)不等价于“进程停止”,避免误判;
|
|
1611
|
+
// 3) manual stop / stopped lifecycle / restart_pending 才会收敛为 running=false。
|
|
1600
1612
|
const {
|
|
1601
1613
|
previous,
|
|
1602
1614
|
hintedRunning,
|
|
@@ -1651,6 +1663,10 @@ function resolveRuntimeRestartPendingForUpdate(params: {
|
|
|
1651
1663
|
nextLinkStatus: RuntimeBotStatus["link_status"];
|
|
1652
1664
|
nextStatusSource?: RuntimeBotStatus["status_source"] | undefined;
|
|
1653
1665
|
}): boolean {
|
|
1666
|
+
// restart_pending 推导规则:
|
|
1667
|
+
// - 显式 hint 优先;
|
|
1668
|
+
// - 无显式 hint 时,仅 probe/connected 能清 pending;
|
|
1669
|
+
// - event/manual 的 connected 仍可能是过渡态,保持 pending=true。
|
|
1654
1670
|
const {
|
|
1655
1671
|
previous,
|
|
1656
1672
|
hintedRestartPending,
|
|
@@ -1713,6 +1729,7 @@ function resolveStatusProtectWindowMs(entry: RuntimeBotStatus): number {
|
|
|
1713
1729
|
function isProbeDowngradeLinkStatus(
|
|
1714
1730
|
status: RuntimeBotStatus["link_status"],
|
|
1715
1731
|
): boolean {
|
|
1732
|
+
// 这些状态都属于“probe 视角的降级态”,可能被新鲜 event 保护逻辑拦截。
|
|
1716
1733
|
return (
|
|
1717
1734
|
status === "error" ||
|
|
1718
1735
|
status === "disconnected" ||
|
|
@@ -1723,6 +1740,8 @@ function isProbeDowngradeLinkStatus(
|
|
|
1723
1740
|
function resolveRuntimeLifecycleAnchorMsForGuard(
|
|
1724
1741
|
status: RuntimeBotStatus,
|
|
1725
1742
|
): number | null {
|
|
1743
|
+
// 生命周期锚点用于计算保护窗口年龄:
|
|
1744
|
+
// started_at 与 transition_at 取较新值,避免老时间戳误伤新生命周期。
|
|
1726
1745
|
const startedAtMs = parseIsoTimeMs(
|
|
1727
1746
|
String(status.started_at || "").trim() || null,
|
|
1728
1747
|
);
|
|
@@ -1739,6 +1758,7 @@ function isWithinRuntimeConnectGraceForGuard(
|
|
|
1739
1758
|
status: RuntimeBotStatus,
|
|
1740
1759
|
nowMs = Date.now(),
|
|
1741
1760
|
): boolean {
|
|
1761
|
+
// connect grace 内允许“保守展示”,不急于接受 probe 降级结论。
|
|
1742
1762
|
const lifecycleAnchorMs = resolveRuntimeLifecycleAnchorMsForGuard(status);
|
|
1743
1763
|
if (lifecycleAnchorMs == null) return false;
|
|
1744
1764
|
const lifecycleAgeMs = Math.max(0, nowMs - lifecycleAnchorMs);
|
|
@@ -1750,6 +1770,7 @@ function hasFreshRuntimeEventLivenessForProbeGuard(
|
|
|
1750
1770
|
nowMs = Date.now(),
|
|
1751
1771
|
lifecycleAnchorMs: number | null = null,
|
|
1752
1772
|
): boolean {
|
|
1773
|
+
// 事件活性保护:只有在生命周期内且事件足够新鲜,才允许覆盖 probe 降级。
|
|
1753
1774
|
const eventLiveness = runtimeEventLivenessRegistry.get(key);
|
|
1754
1775
|
const eventAtMs = parseIsoTimeMs(
|
|
1755
1776
|
String(eventLiveness?.last_event_at || "").trim() || null,
|
|
@@ -1765,6 +1786,7 @@ function hasFreshRuntimeEventLivenessForProbeGuard(
|
|
|
1765
1786
|
function isConnectedRuntimeEventLivenessName(
|
|
1766
1787
|
eventName: string | null | undefined,
|
|
1767
1788
|
): boolean {
|
|
1789
|
+
// 这些事件被视为“数据面已连通”的强证据。
|
|
1768
1790
|
const normalized = String(eventName || "")
|
|
1769
1791
|
.trim()
|
|
1770
1792
|
.toLowerCase();
|
|
@@ -1783,6 +1805,7 @@ function hasFreshConnectedRuntimeEventLivenessForRecovery(
|
|
|
1783
1805
|
key: string,
|
|
1784
1806
|
nowMs = Date.now(),
|
|
1785
1807
|
): boolean {
|
|
1808
|
+
// 仅当最近事件本身是 connected 类强证据,才允许作为恢复旁路依据。
|
|
1786
1809
|
const eventLiveness = runtimeEventLivenessRegistry.get(key);
|
|
1787
1810
|
if (!isConnectedRuntimeEventLivenessName(eventLiveness?.last_event)) {
|
|
1788
1811
|
return false;
|
|
@@ -1800,6 +1823,7 @@ function shouldGuardProbeDowngradeByFreshEvent(params: {
|
|
|
1800
1823
|
prev: RuntimeBotStatus;
|
|
1801
1824
|
next: RuntimeBotStatus;
|
|
1802
1825
|
}): boolean {
|
|
1826
|
+
// 规则:connected -> probe 降级时,如果有新鲜 event 事实,则先抑制降级抖动。
|
|
1803
1827
|
const { key, prev, next } = params;
|
|
1804
1828
|
if (next.status_source !== "probe") return false;
|
|
1805
1829
|
if (!isProbeDowngradeLinkStatus(next.link_status)) return false;
|
|
@@ -1819,6 +1843,7 @@ function shouldGuardProbeConnectedByEventBroken(params: {
|
|
|
1819
1843
|
prev: RuntimeBotStatus;
|
|
1820
1844
|
next: RuntimeBotStatus;
|
|
1821
1845
|
}): boolean {
|
|
1846
|
+
// 规则:event 明确 broken 后,probe 的 connected 需要 event 新鲜度二次确认。
|
|
1822
1847
|
const { key, prev, next } = params;
|
|
1823
1848
|
const prevSource = prev.status_source || "probe";
|
|
1824
1849
|
const nextSource = next.status_source || "probe";
|
|
@@ -1842,6 +1867,10 @@ type RuntimeConvergenceEvidence = "manual" | "event" | "probe";
|
|
|
1842
1867
|
type RuntimeConvergenceSignal = "success" | "failure" | "transient";
|
|
1843
1868
|
|
|
1844
1869
|
function resolveRuntimeConvergencePhase(status: RuntimeBotStatus): RuntimeConvergencePhase {
|
|
1870
|
+
// 收敛阶段:
|
|
1871
|
+
// pending: restart_pending=true;
|
|
1872
|
+
// stopped: manual/disconnected 或 stop 生命周期事件;
|
|
1873
|
+
// steady: 其余状态。
|
|
1845
1874
|
if (status.restart_pending === true) return "pending";
|
|
1846
1875
|
if (status.status_source === "manual" && status.link_status === "disconnected") {
|
|
1847
1876
|
return "stopped";
|
|
@@ -1861,6 +1890,7 @@ function resolveRuntimeConvergencePhase(status: RuntimeBotStatus): RuntimeConver
|
|
|
1861
1890
|
function resolveRuntimeConvergenceEvidence(
|
|
1862
1891
|
source: RuntimeBotStatus["status_source"],
|
|
1863
1892
|
): RuntimeConvergenceEvidence {
|
|
1893
|
+
// 证据源优先级由写入端控制,这里仅做枚举归一化。
|
|
1864
1894
|
if (source === "manual") return "manual";
|
|
1865
1895
|
if (source === "event") return "event";
|
|
1866
1896
|
return "probe";
|
|
@@ -1869,6 +1899,8 @@ function resolveRuntimeConvergenceEvidence(
|
|
|
1869
1899
|
function resolveRuntimeConvergenceSignal(
|
|
1870
1900
|
entry: RuntimeBotStatus,
|
|
1871
1901
|
): RuntimeConvergenceSignal {
|
|
1902
|
+
// 信号语义:
|
|
1903
|
+
// success=connected;failure=probe/event 报告的明确失败;其余为 transient。
|
|
1872
1904
|
if (entry.link_status === "connected") return "success";
|
|
1873
1905
|
const evidence = resolveRuntimeConvergenceEvidence(entry.status_source);
|
|
1874
1906
|
if (
|
|
@@ -1887,6 +1919,7 @@ function shouldGuardProbeConnectedWhilePendingWithoutEvent(params: {
|
|
|
1887
1919
|
prev: RuntimeBotStatus;
|
|
1888
1920
|
next: RuntimeBotStatus;
|
|
1889
1921
|
}): boolean {
|
|
1922
|
+
// pending 生命周期里,probe/connected 若缺少新鲜 connected event 证据则先抑制。
|
|
1890
1923
|
const { key, prev, next } = params;
|
|
1891
1924
|
const prevPhase = resolveRuntimeConvergencePhase(prev);
|
|
1892
1925
|
if (prevPhase !== "pending") return false;
|
|
@@ -1957,6 +1990,7 @@ function logSuppressedTransition(params: {
|
|
|
1957
1990
|
function projectRuntimeBotStatusFromSnapshot(
|
|
1958
1991
|
snapshot: RuntimeChannelSnapshotEntry,
|
|
1959
1992
|
): RuntimeBotStatus {
|
|
1993
|
+
// 快照结构 -> 运行态结构的单点投影,避免多处手工映射字段。
|
|
1960
1994
|
return {
|
|
1961
1995
|
platform: snapshot.platform,
|
|
1962
1996
|
bot_account_id: snapshot.accountId,
|
|
@@ -1983,6 +2017,7 @@ function projectRuntimeBotStatusFromSnapshot(
|
|
|
1983
2017
|
}
|
|
1984
2018
|
|
|
1985
2019
|
function syncRuntimeChannelSnapshotFromStatus(status: RuntimeBotStatus): void {
|
|
2020
|
+
// 每次状态写入后同步快照镜像,保证对外输出(含重启恢复)稳定可复现。
|
|
1986
2021
|
const key = keyOf(status.platform, status.bot_account_id);
|
|
1987
2022
|
runtimeChannelSnapshotRegistry.set(key, {
|
|
1988
2023
|
key,
|
|
@@ -2015,12 +2050,14 @@ function syncRuntimeChannelSnapshotFromStatus(status: RuntimeBotStatus): void {
|
|
|
2015
2050
|
}
|
|
2016
2051
|
|
|
2017
2052
|
function getRuntimeBotStatusByKey(key: string): RuntimeBotStatus | undefined {
|
|
2053
|
+
// 优先读 channel snapshot(包含更多持久化字段),fallback 到内存 registry。
|
|
2018
2054
|
const snapshot = runtimeChannelSnapshotRegistry.get(key);
|
|
2019
2055
|
if (snapshot) return projectRuntimeBotStatusFromSnapshot(snapshot);
|
|
2020
2056
|
return runtimeStatusRegistry.get(key);
|
|
2021
2057
|
}
|
|
2022
2058
|
|
|
2023
2059
|
function listRuntimeBotStatuses(): RuntimeBotStatus[] {
|
|
2060
|
+
// 对外列表统一按 platform+bot_account_id 排序,避免接口结果抖动。
|
|
2024
2061
|
if (runtimeChannelSnapshotRegistry.size > 0) {
|
|
2025
2062
|
return Array.from(runtimeChannelSnapshotRegistry.values())
|
|
2026
2063
|
.map((item) => projectRuntimeBotStatusFromSnapshot(item))
|
|
@@ -4496,7 +4533,12 @@ function markRuntimeAccountsConnecting(
|
|
|
4496
4533
|
platform: RuntimePlatform,
|
|
4497
4534
|
accountIds: string[],
|
|
4498
4535
|
lastEvent = "config_loaded",
|
|
4536
|
+
options?: {
|
|
4537
|
+
preserveExisting?: boolean;
|
|
4538
|
+
},
|
|
4499
4539
|
): string[] {
|
|
4540
|
+
// 配置收敛期把指定账号打到 manual/connecting。
|
|
4541
|
+
// preserveExisting=true 时只标记“新增账号”,不覆盖已存在账号当前态。
|
|
4500
4542
|
const normalizedIds = Array.from(
|
|
4501
4543
|
new Set(
|
|
4502
4544
|
accountIds
|
|
@@ -4506,7 +4548,12 @@ function markRuntimeAccountsConnecting(
|
|
|
4506
4548
|
);
|
|
4507
4549
|
if (normalizedIds.length === 0) return [];
|
|
4508
4550
|
const startedAt = nowIso();
|
|
4551
|
+
const preserveExisting = options?.preserveExisting === true;
|
|
4509
4552
|
for (const accountId of normalizedIds) {
|
|
4553
|
+
if (preserveExisting) {
|
|
4554
|
+
const existing = getRuntimeBotStatusByKey(keyOf(platform, accountId));
|
|
4555
|
+
if (existing) continue;
|
|
4556
|
+
}
|
|
4510
4557
|
clearRuntimeAccountManualStop(platform, accountId);
|
|
4511
4558
|
upsertRuntimeStatus({
|
|
4512
4559
|
platform,
|
|
@@ -4625,6 +4672,11 @@ async function reconcileRuntimeStateForConfigDelta(params: {
|
|
|
4625
4672
|
nextConfig: Record<string, unknown>;
|
|
4626
4673
|
trigger: string;
|
|
4627
4674
|
}): Promise<void> {
|
|
4675
|
+
// 增量 reconcile:
|
|
4676
|
+
// 1) 按平台计算 added/removed;
|
|
4677
|
+
// 2) removed 账号尝试 stop 并清理状态;
|
|
4678
|
+
// 3) added 账号打 connecting(preserveExisting=true);
|
|
4679
|
+
// 4) 输出结构化审计日志。
|
|
4628
4680
|
const previousAccounts = collectConfiguredAccountIdsByPlatform(
|
|
4629
4681
|
params.previousConfig,
|
|
4630
4682
|
);
|
|
@@ -4662,7 +4714,9 @@ async function reconcileRuntimeStateForConfigDelta(params: {
|
|
|
4662
4714
|
clearPlatformStatuses(platform);
|
|
4663
4715
|
} else {
|
|
4664
4716
|
prunePlatformStatusesByAccounts(platform, nextIds, params.nextConfig);
|
|
4665
|
-
markRuntimeAccountsConnecting(platform, added
|
|
4717
|
+
markRuntimeAccountsConnecting(platform, added, "config_delta_added", {
|
|
4718
|
+
preserveExisting: true,
|
|
4719
|
+
});
|
|
4666
4720
|
}
|
|
4667
4721
|
|
|
4668
4722
|
for (const accountId of added) {
|
|
@@ -5129,6 +5183,7 @@ function normalizeRuntimePlatform(input: unknown): RuntimePlatform | null {
|
|
|
5129
5183
|
}
|
|
5130
5184
|
|
|
5131
5185
|
function normalizeRuntimeLinkStatus(input: unknown): RuntimeBotStatus["link_status"] | null {
|
|
5186
|
+
// status sink 入站时的状态白名单解析;未知值直接忽略。
|
|
5132
5187
|
const raw = String(input || "")
|
|
5133
5188
|
.trim()
|
|
5134
5189
|
.toLowerCase();
|
|
@@ -5148,6 +5203,8 @@ function resolveLinkStatusFromSinkPayload(
|
|
|
5148
5203
|
payload: RuntimeStatusSinkPayload,
|
|
5149
5204
|
previous: RuntimeBotStatus | undefined,
|
|
5150
5205
|
): RuntimeBotStatus["link_status"] | null {
|
|
5206
|
+
// sink 状态决议顺序:
|
|
5207
|
+
// explicit link_status > connected 布尔 > running=false > previous.link_status。
|
|
5151
5208
|
const explicit = normalizeRuntimeLinkStatus(payload.link_status);
|
|
5152
5209
|
if (explicit) return explicit;
|
|
5153
5210
|
if (payload.connected === true) return "connected";
|
|
@@ -5157,11 +5214,16 @@ function resolveLinkStatusFromSinkPayload(
|
|
|
5157
5214
|
}
|
|
5158
5215
|
|
|
5159
5216
|
function applyRuntimeStatusSinkPayload(payload: RuntimeStatusSinkPayload): void {
|
|
5217
|
+
// status sink 是“外部状态事件入站”通道:
|
|
5218
|
+
// 1) 先做账号合法性与配置态校验;
|
|
5219
|
+
// 2) 再推导 link_status/restart_pending/running;
|
|
5220
|
+
// 3) 最后统一走 upsertRuntimeStatus 写入状态机。
|
|
5160
5221
|
const platform = normalizeRuntimePlatform(payload.platform);
|
|
5161
5222
|
const botAccountId = String(payload.bot_account_id || "").trim();
|
|
5162
5223
|
if (!platform || !botAccountId) return;
|
|
5163
5224
|
const normalizedAccountId = normalizeRuntimeAccountId(platform, botAccountId);
|
|
5164
5225
|
if (!isConfiguredRuntimeAccount(platform, normalizedAccountId || botAccountId)) {
|
|
5226
|
+
// 未配置账号直接丢弃,避免历史脏事件或删除后迟到事件污染状态面板。
|
|
5165
5227
|
logRuntimeStatusDebug(
|
|
5166
5228
|
`status sink ignored: unconfigured account platform=${platform} account=${normalizedAccountId || botAccountId}`,
|
|
5167
5229
|
);
|
|
@@ -5177,6 +5239,7 @@ function applyRuntimeStatusSinkPayload(payload: RuntimeStatusSinkPayload): void
|
|
|
5177
5239
|
statusSourceRaw === "manual" || statusSourceRaw === "probe" || statusSourceRaw === "event"
|
|
5178
5240
|
? (statusSourceRaw as RuntimeBotStatus["status_source"])
|
|
5179
5241
|
: "event";
|
|
5242
|
+
// restart_pending / running 都由统一推导函数收敛,避免多处规则分叉。
|
|
5180
5243
|
const nextRestartPending = resolveRuntimeRestartPendingForUpdate({
|
|
5181
5244
|
previous,
|
|
5182
5245
|
hintedRestartPending:
|
|
@@ -5203,6 +5266,7 @@ function applyRuntimeStatusSinkPayload(payload: RuntimeStatusSinkPayload): void
|
|
|
5203
5266
|
});
|
|
5204
5267
|
|
|
5205
5268
|
if (statusSource === "event") {
|
|
5269
|
+
// 只有 event 来源才更新 event liveness 真源,probe/manual 不写这条链路。
|
|
5206
5270
|
upsertRuntimeEventLiveness({
|
|
5207
5271
|
platform,
|
|
5208
5272
|
bot_account_id: normalizedAccountId || botAccountId,
|
|
@@ -5348,7 +5412,7 @@ async function invokeGatewayMethodByChannel(args: {
|
|
|
5348
5412
|
function parseOptionalBoolFlag(input: unknown): boolean | null {
|
|
5349
5413
|
// 支持多种布尔表达,无法识别时返回 null 交给上层兜底。
|
|
5350
5414
|
if (input == null) return null;
|
|
5351
|
-
const normalized = String(input
|
|
5415
|
+
const normalized = String(input)
|
|
5352
5416
|
.trim()
|
|
5353
5417
|
.toLowerCase();
|
|
5354
5418
|
if (!normalized) return null;
|
|
@@ -5592,7 +5656,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5592
5656
|
upsertRuntimeProbe({
|
|
5593
5657
|
platform: "dingtalk",
|
|
5594
5658
|
bot_account_id: accountId,
|
|
5595
|
-
last_probe_at: probeAt,
|
|
5659
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5596
5660
|
ok,
|
|
5597
5661
|
error,
|
|
5598
5662
|
result: (probeResult as Record<string, unknown>) || null,
|
|
@@ -5627,7 +5691,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5627
5691
|
runtimeHints.last_run_activity_at || probeAt,
|
|
5628
5692
|
mode: runtimeHints.mode || previous?.mode || null,
|
|
5629
5693
|
status_source: "probe",
|
|
5630
|
-
last_probe_at: probeAt,
|
|
5694
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5631
5695
|
});
|
|
5632
5696
|
} catch (err) {
|
|
5633
5697
|
const probeAt = nowIso();
|
|
@@ -5638,7 +5702,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5638
5702
|
upsertRuntimeProbe({
|
|
5639
5703
|
platform: "dingtalk",
|
|
5640
5704
|
bot_account_id: accountId,
|
|
5641
|
-
last_probe_at: probeAt,
|
|
5705
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5642
5706
|
ok: false,
|
|
5643
5707
|
error: probeError,
|
|
5644
5708
|
result: null,
|
|
@@ -5662,7 +5726,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5662
5726
|
last_run_activity_at: probeAt,
|
|
5663
5727
|
mode: previous?.mode || null,
|
|
5664
5728
|
status_source: "probe",
|
|
5665
|
-
last_probe_at: probeAt,
|
|
5729
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5666
5730
|
});
|
|
5667
5731
|
}
|
|
5668
5732
|
},
|
|
@@ -5725,7 +5789,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5725
5789
|
upsertRuntimeProbe({
|
|
5726
5790
|
platform: "dingtalk",
|
|
5727
5791
|
bot_account_id: accountId,
|
|
5728
|
-
last_probe_at: probeAt,
|
|
5792
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5729
5793
|
ok,
|
|
5730
5794
|
error,
|
|
5731
5795
|
result: (probeResult as Record<string, unknown>) || null,
|
|
@@ -5764,7 +5828,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5764
5828
|
runtimeHints.last_run_activity_at || probeAt,
|
|
5765
5829
|
mode: runtimeHints.mode || previous?.mode || null,
|
|
5766
5830
|
status_source: "probe",
|
|
5767
|
-
last_probe_at: probeAt,
|
|
5831
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5768
5832
|
});
|
|
5769
5833
|
} catch (err) {
|
|
5770
5834
|
const probeAt = nowIso();
|
|
@@ -5775,7 +5839,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5775
5839
|
upsertRuntimeProbe({
|
|
5776
5840
|
platform: "dingtalk",
|
|
5777
5841
|
bot_account_id: accountId,
|
|
5778
|
-
last_probe_at: probeAt,
|
|
5842
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5779
5843
|
ok: false,
|
|
5780
5844
|
error: probeError,
|
|
5781
5845
|
result: null,
|
|
@@ -5799,7 +5863,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5799
5863
|
last_run_activity_at: probeAt,
|
|
5800
5864
|
mode: previous?.mode || null,
|
|
5801
5865
|
status_source: "probe",
|
|
5802
|
-
last_probe_at: probeAt,
|
|
5866
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5803
5867
|
});
|
|
5804
5868
|
}
|
|
5805
5869
|
},
|
|
@@ -5913,7 +5977,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5913
5977
|
upsertRuntimeProbe({
|
|
5914
5978
|
platform: "feishu",
|
|
5915
5979
|
bot_account_id: accountId,
|
|
5916
|
-
last_probe_at: probeAt,
|
|
5980
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5917
5981
|
ok,
|
|
5918
5982
|
error: nextError,
|
|
5919
5983
|
result: (probeResult as Record<string, unknown>) || null,
|
|
@@ -5949,7 +6013,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5949
6013
|
runtimeHints.last_run_activity_at || probeAt,
|
|
5950
6014
|
mode: runtimeHints.mode || previous?.mode || null,
|
|
5951
6015
|
status_source: "probe",
|
|
5952
|
-
last_probe_at: probeAt,
|
|
6016
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5953
6017
|
});
|
|
5954
6018
|
} catch (err) {
|
|
5955
6019
|
const probeAt = nowIso();
|
|
@@ -5957,7 +6021,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5957
6021
|
upsertRuntimeProbe({
|
|
5958
6022
|
platform: "feishu",
|
|
5959
6023
|
bot_account_id: accountId,
|
|
5960
|
-
last_probe_at: probeAt,
|
|
6024
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5961
6025
|
ok: false,
|
|
5962
6026
|
error: probeError || null,
|
|
5963
6027
|
result: null,
|
|
@@ -5985,7 +6049,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
5985
6049
|
last_run_activity_at: probeAt,
|
|
5986
6050
|
mode: previous?.mode || null,
|
|
5987
6051
|
status_source: "probe",
|
|
5988
|
-
last_probe_at: probeAt,
|
|
6052
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
5989
6053
|
});
|
|
5990
6054
|
}
|
|
5991
6055
|
},
|
|
@@ -6095,7 +6159,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
6095
6159
|
upsertRuntimeProbe({
|
|
6096
6160
|
platform: "weixin",
|
|
6097
6161
|
bot_account_id: accountId,
|
|
6098
|
-
last_probe_at: probeAt,
|
|
6162
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
6099
6163
|
ok,
|
|
6100
6164
|
error,
|
|
6101
6165
|
result: (probeResult as Record<string, unknown>) || null,
|
|
@@ -6130,7 +6194,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
6130
6194
|
runtimeHints.last_run_activity_at || probeAt,
|
|
6131
6195
|
mode: runtimeHints.mode || previous?.mode || null,
|
|
6132
6196
|
status_source: "probe",
|
|
6133
|
-
last_probe_at: probeAt,
|
|
6197
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
6134
6198
|
});
|
|
6135
6199
|
} catch (err) {
|
|
6136
6200
|
const probeAt = nowIso();
|
|
@@ -6141,7 +6205,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
6141
6205
|
upsertRuntimeProbe({
|
|
6142
6206
|
platform: "weixin",
|
|
6143
6207
|
bot_account_id: accountId,
|
|
6144
|
-
last_probe_at: probeAt,
|
|
6208
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
6145
6209
|
ok: false,
|
|
6146
6210
|
error: probeError,
|
|
6147
6211
|
result: null,
|
|
@@ -6165,7 +6229,7 @@ async function runFullProbeAndRefreshStatuses(params: {
|
|
|
6165
6229
|
last_run_activity_at: probeAt,
|
|
6166
6230
|
mode: previous?.mode || null,
|
|
6167
6231
|
status_source: "probe",
|
|
6168
|
-
last_probe_at: probeAt,
|
|
6232
|
+
last_probe_at: toChina8Iso(probeAt),
|
|
6169
6233
|
});
|
|
6170
6234
|
}
|
|
6171
6235
|
},
|
|
@@ -6226,7 +6290,7 @@ async function probeAndRefreshStatuses(
|
|
|
6226
6290
|
}
|
|
6227
6291
|
|
|
6228
6292
|
const startedAt = Date.now();
|
|
6229
|
-
lastProbeStartedAt = new Date(startedAt).toISOString();
|
|
6293
|
+
lastProbeStartedAt = toChina8Iso(new Date(startedAt).toISOString());
|
|
6230
6294
|
|
|
6231
6295
|
const sweepTimeouts = resolveProbeTimeoutsFromRequest(
|
|
6232
6296
|
options.timeoutMs == null ? undefined : Number(options.timeoutMs),
|
|
@@ -6242,7 +6306,7 @@ async function probeAndRefreshStatuses(
|
|
|
6242
6306
|
});
|
|
6243
6307
|
})().finally(() => {
|
|
6244
6308
|
const finishedAt = Date.now();
|
|
6245
|
-
lastProbeFinishedAt = new Date(finishedAt).toISOString();
|
|
6309
|
+
lastProbeFinishedAt = toChina8Iso(new Date(finishedAt).toISOString());
|
|
6246
6310
|
lastProbeDurationMs = Math.max(0, finishedAt - startedAt);
|
|
6247
6311
|
probeInFlight = null;
|
|
6248
6312
|
probeInFlightMeta = null;
|
|
@@ -6643,7 +6707,7 @@ async function runRuntimeHealthGuardCycle(): Promise<void> {
|
|
|
6643
6707
|
}
|
|
6644
6708
|
|
|
6645
6709
|
const startedAtMs = Date.now();
|
|
6646
|
-
runtimeHealthGuardLastStartedAt = new Date(startedAtMs).toISOString();
|
|
6710
|
+
runtimeHealthGuardLastStartedAt = toChina8Iso(new Date(startedAtMs).toISOString());
|
|
6647
6711
|
|
|
6648
6712
|
try {
|
|
6649
6713
|
await probeAndRefreshStatuses({ reason: "health_guard" });
|
|
@@ -6710,7 +6774,7 @@ async function runRuntimeHealthGuardCycle(): Promise<void> {
|
|
|
6710
6774
|
}
|
|
6711
6775
|
} finally {
|
|
6712
6776
|
const finishedAtMs = Date.now();
|
|
6713
|
-
runtimeHealthGuardLastFinishedAt = new Date(finishedAtMs).toISOString();
|
|
6777
|
+
runtimeHealthGuardLastFinishedAt = toChina8Iso(new Date(finishedAtMs).toISOString());
|
|
6714
6778
|
runtimeHealthGuardLastDurationMs = Math.max(0, finishedAtMs - startedAtMs);
|
|
6715
6779
|
}
|
|
6716
6780
|
}
|
|
@@ -7447,7 +7511,7 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7447
7511
|
JSON.stringify({
|
|
7448
7512
|
ok: true,
|
|
7449
7513
|
runtime_instance_id: runtimeInstanceId,
|
|
7450
|
-
|
|
7514
|
+
generated_at: toChina8Iso(nowIso()),
|
|
7451
7515
|
limit: clampedLimit,
|
|
7452
7516
|
...runtimeLogs,
|
|
7453
7517
|
}),
|
|
@@ -7895,6 +7959,19 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7895
7959
|
? body.markConnecting
|
|
7896
7960
|
: body.mark_connecting;
|
|
7897
7961
|
const markConnecting = parseBoolFlag(markConnectingRaw, true);
|
|
7962
|
+
const reconcileRuntimeStateRaw =
|
|
7963
|
+
body.reconcile_runtime_state === undefined
|
|
7964
|
+
? body.reconcileRuntimeState
|
|
7965
|
+
: body.reconcile_runtime_state;
|
|
7966
|
+
const reconcileRuntimeState = parseBoolFlag(
|
|
7967
|
+
reconcileRuntimeStateRaw,
|
|
7968
|
+
!markConnecting,
|
|
7969
|
+
);
|
|
7970
|
+
const probeAfterApplyRaw =
|
|
7971
|
+
body.probe_after_apply === undefined
|
|
7972
|
+
? body.probeAfterApply
|
|
7973
|
+
: body.probe_after_apply;
|
|
7974
|
+
const probeAfterApply = parseBoolFlag(probeAfterApplyRaw, true);
|
|
7898
7975
|
const requestedVersion = String(body.version || "").trim();
|
|
7899
7976
|
if (!config || typeof config !== "object") {
|
|
7900
7977
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -7971,14 +8048,16 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7971
8048
|
normalizedResult.normalized,
|
|
7972
8049
|
"config_apply_mark_connecting",
|
|
7973
8050
|
);
|
|
7974
|
-
} else {
|
|
8051
|
+
} else if (reconcileRuntimeState) {
|
|
7975
8052
|
await reconcileRuntimeStateForConfigDelta({
|
|
7976
8053
|
previousConfig,
|
|
7977
8054
|
nextConfig: normalizedResult.normalized,
|
|
7978
8055
|
trigger: "config_apply",
|
|
7979
8056
|
});
|
|
7980
8057
|
}
|
|
7981
|
-
|
|
8058
|
+
if (probeAfterApply) {
|
|
8059
|
+
await probeAndRefreshStatuses({ force: true, reason: "config_apply" });
|
|
8060
|
+
}
|
|
7982
8061
|
|
|
7983
8062
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
7984
8063
|
res.end(
|
|
@@ -7987,6 +8066,8 @@ async function startInternalApiServer(): Promise<void> {
|
|
|
7987
8066
|
status: "applied",
|
|
7988
8067
|
persisted: persist,
|
|
7989
8068
|
mark_connecting: markConnecting,
|
|
8069
|
+
reconcile_runtime_state: reconcileRuntimeState,
|
|
8070
|
+
probe_after_apply: probeAfterApply,
|
|
7990
8071
|
configPath: loadedConfigPathForRuntime,
|
|
7991
8072
|
version: configVersionHash || "unknown",
|
|
7992
8073
|
}),
|
|
@@ -19,12 +19,14 @@ export type RuntimeBotStatus = {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
export class BotRuntimeRegistry {
|
|
22
|
+
// 账号级运行态内存表:key=platform:bot_account_id。
|
|
22
23
|
private readonly store = new Map<string, RuntimeBotStatus>();
|
|
23
24
|
|
|
24
25
|
private keyOf(
|
|
25
26
|
platform: RuntimeBotStatus["platform"],
|
|
26
27
|
accountId: string,
|
|
27
28
|
): string {
|
|
29
|
+
// 单账号状态唯一键。
|
|
28
30
|
return `${platform}:${accountId}`;
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -33,6 +35,7 @@ export class BotRuntimeRegistry {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
upsert(entry: RuntimeBotStatus): void {
|
|
38
|
+
// 合并更新:未传字段继承旧值,便于多来源状态渐进补齐。
|
|
36
39
|
const key = this.keyOf(entry.platform, entry.bot_account_id);
|
|
37
40
|
const prev = this.store.get(key);
|
|
38
41
|
this.store.set(key, { ...(prev || {}), ...entry });
|
|
@@ -50,6 +53,7 @@ export class BotRuntimeRegistry {
|
|
|
50
53
|
degraded: number;
|
|
51
54
|
disconnected: number;
|
|
52
55
|
} {
|
|
56
|
+
// 汇总用于外部状态面板统计,不改写任何账号状态。
|
|
53
57
|
const summary = {
|
|
54
58
|
total: 0,
|
|
55
59
|
connected: 0,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type RuntimePlatform = "dingtalk" | "feishu" | "weixin";
|
|
2
2
|
|
|
3
|
+
// 运行态主状态枚举(来自 Node 状态机 runtime.link_status)。
|
|
3
4
|
export type RuntimeLinkStatus =
|
|
4
5
|
| "connecting"
|
|
5
6
|
| "connected"
|
|
@@ -9,6 +10,7 @@ export type RuntimeLinkStatus =
|
|
|
9
10
|
|
|
10
11
|
export type RuntimeStatusSource = "event" | "probe" | "manual" | null | undefined;
|
|
11
12
|
|
|
13
|
+
// health.reason 是“健康判定层”的语义,不等价于 link_status。
|
|
12
14
|
export type RuntimeComposedHealthReason =
|
|
13
15
|
| "healthy"
|
|
14
16
|
| "unmanaged"
|
|
@@ -53,6 +55,7 @@ export type RuntimeStatusLike = {
|
|
|
53
55
|
last_event_at?: string | number | null;
|
|
54
56
|
};
|
|
55
57
|
|
|
58
|
+
// RuntimeComposedBotLike: 统一的“状态 + 健康”输入结构,供 readiness/guard 使用。
|
|
56
59
|
export type RuntimeComposedBotLike = {
|
|
57
60
|
platform: RuntimePlatform;
|
|
58
61
|
bot_account_id: string;
|
|
@@ -63,6 +66,7 @@ export type RuntimeComposedBotLike = {
|
|
|
63
66
|
};
|
|
64
67
|
};
|
|
65
68
|
|
|
69
|
+
// display 快照是最终 UI 文案/颜色输出,不反向驱动状态机。
|
|
66
70
|
export type RuntimeDisplaySnapshot = {
|
|
67
71
|
status: RuntimeComposedDisplayStatus;
|
|
68
72
|
reason: string;
|
|
@@ -146,13 +150,17 @@ const runtimeStartupFallbackEvents = new Set([
|
|
|
146
150
|
"config_loaded",
|
|
147
151
|
]);
|
|
148
152
|
|
|
153
|
+
// webhook 模式下没有持续 socket 心跳,不做 stale_socket 判定。
|
|
149
154
|
const runtimeSkipStaleHeartbeatModes = new Set(["webhook"]);
|
|
155
|
+
// busy 状态长期无活动则视为 stuck。
|
|
150
156
|
const runtimeBusyActivityStaleMsDefault = 25 * 60_000;
|
|
157
|
+
// 重连次数达到阈值后,健康守护可按 gave_up 分类。
|
|
151
158
|
const runtimeGaveUpReconnectThreshold = 10;
|
|
152
159
|
|
|
153
160
|
export function isExplicitReportedRuntimeFailure(
|
|
154
161
|
status: Pick<RuntimeStatusLike, "link_status" | "status_source">,
|
|
155
162
|
): boolean {
|
|
163
|
+
// 只有 event/probe 上报的失败态被视为“显式失败证据”。
|
|
156
164
|
return (
|
|
157
165
|
(status.status_source === "probe" || status.status_source === "event") &&
|
|
158
166
|
(status.link_status === "error" ||
|
|
@@ -168,6 +176,8 @@ export function shouldApplyReportedFailureOverManualConnecting(params: {
|
|
|
168
176
|
| undefined;
|
|
169
177
|
next: Pick<RuntimeStatusLike, "link_status" | "status_source">;
|
|
170
178
|
}): boolean {
|
|
179
|
+
// manual/connecting 是控制面过渡态;
|
|
180
|
+
// 若后续出现 event/probe 明确失败,应允许失败覆盖 manual connecting。
|
|
171
181
|
const previous = params.previous;
|
|
172
182
|
const previousSource = previous?.status_source || "probe";
|
|
173
183
|
return (
|
|
@@ -178,6 +188,7 @@ export function shouldApplyReportedFailureOverManualConnecting(params: {
|
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
function parseIsoTimeMs(value: string | null | undefined): number | null {
|
|
191
|
+
// ISO 时间文本解析;解析失败统一返回 null。
|
|
181
192
|
const text = String(value || "").trim();
|
|
182
193
|
if (!text) return null;
|
|
183
194
|
const ms = Date.parse(text);
|
|
@@ -186,6 +197,7 @@ function parseIsoTimeMs(value: string | null | undefined): number | null {
|
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
function parseTimeMs(value: unknown): number | null {
|
|
200
|
+
// 通用时间解析:支持秒/毫秒数字与文本,统一返回毫秒时间戳。
|
|
189
201
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
190
202
|
const normalized = value > 1_000_000_000_000 ? value : value * 1000;
|
|
191
203
|
return Math.max(0, Math.trunc(normalized));
|
|
@@ -201,6 +213,7 @@ function parseTimeMs(value: unknown): number | null {
|
|
|
201
213
|
}
|
|
202
214
|
|
|
203
215
|
function readNonNegativeInt(value: unknown, fallback = 0): number {
|
|
216
|
+
// 计数字段容错并裁剪到非负整数。
|
|
204
217
|
const numeric = typeof value === "number" ? value : Number(value);
|
|
205
218
|
if (!Number.isFinite(numeric)) return fallback;
|
|
206
219
|
return Math.max(0, Math.trunc(numeric));
|
|
@@ -221,6 +234,9 @@ export function evaluateRuntimeHealth(params: {
|
|
|
221
234
|
reason: RuntimeComposedHealthReason;
|
|
222
235
|
evaluated_at: string;
|
|
223
236
|
} {
|
|
237
|
+
// 健康判定优先级:
|
|
238
|
+
// unmanaged > not_running > busy/stuck > explicit failure > startup_grace
|
|
239
|
+
// > stale_socket > healthy。
|
|
224
240
|
const {
|
|
225
241
|
status,
|
|
226
242
|
eventAtMs = null,
|
|
@@ -336,6 +352,7 @@ export function evaluateRuntimeHealth(params: {
|
|
|
336
352
|
function normalizeRuntimeDisplayStatusFromText(
|
|
337
353
|
value: unknown,
|
|
338
354
|
): RuntimeComposedDisplayStatus | null {
|
|
355
|
+
// 宽松文本到显示状态映射,兼容历史 status 文本。
|
|
339
356
|
const text = String(value || "")
|
|
340
357
|
.trim()
|
|
341
358
|
.toLowerCase();
|
|
@@ -357,6 +374,7 @@ function resolveRuntimeDisplayDefaults(status: RuntimeComposedDisplayStatus): {
|
|
|
357
374
|
color: RuntimeComposedDisplayColor;
|
|
358
375
|
hint: string | null;
|
|
359
376
|
} {
|
|
377
|
+
// 显示状态默认文案,不承担状态判定职责。
|
|
360
378
|
if (status === "online") {
|
|
361
379
|
return { label: "在线", color: "success", hint: null };
|
|
362
380
|
}
|
|
@@ -369,9 +387,9 @@ function resolveRuntimeDisplayDefaults(status: RuntimeComposedDisplayStatus): {
|
|
|
369
387
|
}
|
|
370
388
|
if (status === "connecting") {
|
|
371
389
|
return {
|
|
372
|
-
label: "
|
|
390
|
+
label: "状态确认中",
|
|
373
391
|
color: "processing",
|
|
374
|
-
hint: "
|
|
392
|
+
hint: "正在同步并确认机器人在线状态。",
|
|
375
393
|
};
|
|
376
394
|
}
|
|
377
395
|
if (status === "reconnecting") {
|
|
@@ -403,6 +421,8 @@ export function resolveRuntimeDisplayState(params: {
|
|
|
403
421
|
healthReason: RuntimeComposedHealthReason;
|
|
404
422
|
evaluatedAt: string;
|
|
405
423
|
}): RuntimeDisplaySnapshot {
|
|
424
|
+
// 展示态判定优先级:
|
|
425
|
+
// restart_pending 分支 > health.reason 分支 > status 文本分支 > event fallback > unknown。
|
|
406
426
|
const statusText = String(params.status.link_status || "")
|
|
407
427
|
.trim()
|
|
408
428
|
.toLowerCase();
|
|
@@ -433,6 +453,7 @@ export function resolveRuntimeDisplayState(params: {
|
|
|
433
453
|
|
|
434
454
|
const linkConnected = params.status.link_status === "connected";
|
|
435
455
|
if (params.status.restart_pending === true) {
|
|
456
|
+
// 重启生命周期下,优先输出“重连中/失败中”语义,避免过早显示在线。
|
|
436
457
|
if (statusText === "error") {
|
|
437
458
|
return build("error", "status:error", "status", {
|
|
438
459
|
hint: "重启/启动流程中探测失败,请检查凭证与网络后重试。",
|
|
@@ -517,7 +538,8 @@ export function resolveRuntimeDisplayState(params: {
|
|
|
517
538
|
});
|
|
518
539
|
}
|
|
519
540
|
return build("connecting", "health:startup_grace", "health", {
|
|
520
|
-
|
|
541
|
+
label: "状态确认中",
|
|
542
|
+
hint: "正在同步并确认机器人在线状态。",
|
|
521
543
|
});
|
|
522
544
|
}
|
|
523
545
|
|
|
@@ -527,7 +549,10 @@ export function resolveRuntimeDisplayState(params: {
|
|
|
527
549
|
}
|
|
528
550
|
|
|
529
551
|
if (!statusText && runtimeStartupFallbackEvents.has(lastEvent)) {
|
|
530
|
-
|
|
552
|
+
// 无 status 文本时,启动事件作为连接中兜底提示。
|
|
553
|
+
return build("connecting", `event:${lastEvent}`, "event_fallback", {
|
|
554
|
+
label: "状态确认中",
|
|
555
|
+
});
|
|
531
556
|
}
|
|
532
557
|
|
|
533
558
|
if (params.healthReason === "healthy") {
|
|
@@ -538,6 +563,7 @@ export function resolveRuntimeDisplayState(params: {
|
|
|
538
563
|
}
|
|
539
564
|
|
|
540
565
|
export function isManualStopState(item: RuntimeComposedBotLike): boolean {
|
|
566
|
+
// 显式人工停机态:manual + disconnected。
|
|
541
567
|
return (
|
|
542
568
|
item.runtime.status_source === "manual" &&
|
|
543
569
|
item.runtime.link_status === "disconnected"
|
|
@@ -545,12 +571,14 @@ export function isManualStopState(item: RuntimeComposedBotLike): boolean {
|
|
|
545
571
|
}
|
|
546
572
|
|
|
547
573
|
function readRuntimeReconnectCount(item: RuntimeComposedBotLike): number {
|
|
574
|
+
// reconnect_count 统一按非负整数读取,避免 NaN/负值影响阈值判断。
|
|
548
575
|
return readNonNegativeInt(item.runtime.reconnect_count);
|
|
549
576
|
}
|
|
550
577
|
|
|
551
578
|
export function resolveRuntimeHealthGuardTriggerReason(
|
|
552
579
|
item: RuntimeComposedBotLike,
|
|
553
580
|
): RuntimeHealthGuardTriggerReason | null {
|
|
581
|
+
// 健康守护触发原因仅用于“是否自动恢复”决策,不直接改主状态。
|
|
554
582
|
const reconnectCount = readRuntimeReconnectCount(item);
|
|
555
583
|
const isRestartPending = item.runtime.restart_pending === true;
|
|
556
584
|
|
|
@@ -582,6 +610,10 @@ export function buildRuntimeReadinessSnapshot(params: {
|
|
|
582
610
|
runtimeState?: "running" | "restarting";
|
|
583
611
|
runtimeReady?: boolean;
|
|
584
612
|
}): RuntimeReadinessSnapshot {
|
|
613
|
+
// readiness 只读聚合:
|
|
614
|
+
// 1) 把账号划分为 hard/soft/ignored/pending;
|
|
615
|
+
// 2) 计算 ready/pending/pending_reason;
|
|
616
|
+
// 3) 输出可诊断 summary 与账号明细。
|
|
585
617
|
const {
|
|
586
618
|
botsComposed,
|
|
587
619
|
generatedAt,
|
|
@@ -612,6 +644,7 @@ export function buildRuntimeReadinessSnapshot(params: {
|
|
|
612
644
|
reconnect_count: reconnectCount,
|
|
613
645
|
};
|
|
614
646
|
|
|
647
|
+
// pending 维度是“尚在收敛”语义,不等价于 unhealthy。
|
|
615
648
|
const isPendingByHealth = item.health.reason === "startup_grace";
|
|
616
649
|
const isPendingByStatus = item.runtime.link_status === "connecting";
|
|
617
650
|
const isPendingByRestart = isRestartPending;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ylib-syim",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
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.17",
|
|
50
|
+
"ylib-openclaw-lark": "2026.3.17-beta.22",
|
|
51
|
+
"ylib-openclaw-weixin": "2.1.7-beta.10",
|
|
52
52
|
"axios": "^1.6.0",
|
|
53
53
|
"dingtalk-stream": "^2.1.4",
|
|
54
54
|
"fluent-ffmpeg": "^2.1.3",
|
|
@@ -809,7 +809,13 @@ async function startConfiguredAccounts(): Promise<void> {
|
|
|
809
809
|
}
|
|
810
810
|
|
|
811
811
|
let startedCount = 0;
|
|
812
|
-
|
|
812
|
+
// 有界并发启动:避免账号数多时串行等待导致首次启动时间过长。
|
|
813
|
+
// 并发数由环境变量 BRIDGE_START_CONCURRENCY_DINGTALK 控制,默认 3。
|
|
814
|
+
const concurrency = parseInt(process.env.BRIDGE_START_CONCURRENCY_DINGTALK || "3", 10);
|
|
815
|
+
const queue = [...accountIds];
|
|
816
|
+
const running = new Map<string, Promise<void>>();
|
|
817
|
+
|
|
818
|
+
async function startOne(accountId: string): Promise<void> {
|
|
813
819
|
try {
|
|
814
820
|
await startSingleAccount(accountId, "startup");
|
|
815
821
|
startedCount++;
|
|
@@ -821,6 +827,19 @@ async function startConfiguredAccounts(): Promise<void> {
|
|
|
821
827
|
emitRuntimeEvent(accountId, "error", detail);
|
|
822
828
|
}
|
|
823
829
|
}
|
|
830
|
+
|
|
831
|
+
while (queue.length > 0 || running.size > 0) {
|
|
832
|
+
while (running.size < concurrency && queue.length > 0) {
|
|
833
|
+
const accountId = queue.shift()!;
|
|
834
|
+
const task = startOne(accountId).finally(() => {
|
|
835
|
+
running.delete(accountId);
|
|
836
|
+
});
|
|
837
|
+
running.set(accountId, task);
|
|
838
|
+
}
|
|
839
|
+
if (running.size > 0) {
|
|
840
|
+
await Promise.race(running.values());
|
|
841
|
+
}
|
|
842
|
+
}
|
|
824
843
|
console.log(
|
|
825
844
|
`[dingtalk-stdio-bridge] started accounts=${startedCount} total=${accountIds.length}`,
|
|
826
845
|
);
|
|
@@ -897,7 +897,13 @@ async function startConfiguredAccounts(): Promise<void> {
|
|
|
897
897
|
}
|
|
898
898
|
|
|
899
899
|
let startedCount = 0;
|
|
900
|
-
|
|
900
|
+
// 有界并发启动:避免账号数多时串行等待导致首次启动时间过长。
|
|
901
|
+
// 并发数由环境变量 BRIDGE_START_CONCURRENCY_LARK 控制,默认 3。
|
|
902
|
+
const concurrency = parseInt(process.env.BRIDGE_START_CONCURRENCY_LARK || "3", 10);
|
|
903
|
+
const queue = [...accountIds];
|
|
904
|
+
const running = new Map<string, Promise<void>>();
|
|
905
|
+
|
|
906
|
+
async function startOne(accountId: string): Promise<void> {
|
|
901
907
|
try {
|
|
902
908
|
await startSingleAccount(accountId, "startup");
|
|
903
909
|
startedCount++;
|
|
@@ -909,6 +915,19 @@ async function startConfiguredAccounts(): Promise<void> {
|
|
|
909
915
|
emitRuntimeEvent(accountId, "error", detail);
|
|
910
916
|
}
|
|
911
917
|
}
|
|
918
|
+
|
|
919
|
+
while (queue.length > 0 || running.size > 0) {
|
|
920
|
+
while (running.size < concurrency && queue.length > 0) {
|
|
921
|
+
const accountId = queue.shift()!;
|
|
922
|
+
const task = startOne(accountId).finally(() => {
|
|
923
|
+
running.delete(accountId);
|
|
924
|
+
});
|
|
925
|
+
running.set(accountId, task);
|
|
926
|
+
}
|
|
927
|
+
if (running.size > 0) {
|
|
928
|
+
await Promise.race(running.values());
|
|
929
|
+
}
|
|
930
|
+
}
|
|
912
931
|
console.log(
|
|
913
932
|
`[lark-stdio-bridge] started accounts=${startedCount} total=${accountIds.length}`,
|
|
914
933
|
);
|
|
@@ -818,7 +818,13 @@ async function startConfiguredAccounts(): Promise<void> {
|
|
|
818
818
|
}
|
|
819
819
|
|
|
820
820
|
let startedCount = 0;
|
|
821
|
-
|
|
821
|
+
// 有界并发启动:避免账号数多时串行等待导致首次启动时间过长。
|
|
822
|
+
// 并发数由环境变量 BRIDGE_START_CONCURRENCY_WEIXIN 控制,默认 2。
|
|
823
|
+
const concurrency = parseInt(process.env.BRIDGE_START_CONCURRENCY_WEIXIN || "2", 10);
|
|
824
|
+
const queue = [...accountIds];
|
|
825
|
+
const running = new Map<string, Promise<void>>();
|
|
826
|
+
|
|
827
|
+
async function startOne(accountId: string): Promise<void> {
|
|
822
828
|
try {
|
|
823
829
|
await startSingleAccount(accountId, "startup");
|
|
824
830
|
startedCount++;
|
|
@@ -831,6 +837,19 @@ async function startConfiguredAccounts(): Promise<void> {
|
|
|
831
837
|
}
|
|
832
838
|
}
|
|
833
839
|
|
|
840
|
+
while (queue.length > 0 || running.size > 0) {
|
|
841
|
+
while (running.size < concurrency && queue.length > 0) {
|
|
842
|
+
const accountId = queue.shift()!;
|
|
843
|
+
const task = startOne(accountId).finally(() => {
|
|
844
|
+
running.delete(accountId);
|
|
845
|
+
});
|
|
846
|
+
running.set(accountId, task);
|
|
847
|
+
}
|
|
848
|
+
if (running.size > 0) {
|
|
849
|
+
await Promise.race(running.values());
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
834
853
|
console.log(
|
|
835
854
|
`[weixin-stdio-bridge] started accounts=${startedCount} total=${accountIds.length}`,
|
|
836
855
|
);
|