dsh-feishu-bot 0.19.9 → 0.19.11
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/dist/cli.js +268 -22
- package/dist/cli.js.map +1 -1
- package/dist/plugin.d.ts +69 -0
- package/dist/plugin.js +181 -9
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3700,6 +3700,10 @@ var DEFAULTS = {
|
|
|
3700
3700
|
guardianBridgeProfile: "default",
|
|
3701
3701
|
upgradeNotify: false,
|
|
3702
3702
|
upgradeCheckIntervalMs: 6 * 60 * 6e4,
|
|
3703
|
+
channelPingTimeoutSec: 30,
|
|
3704
|
+
channelKeepalive: true,
|
|
3705
|
+
channelKeepaliveMs: 15e3,
|
|
3706
|
+
channelHealthPollMs: 5e3,
|
|
3703
3707
|
sessionBackfillMessages: 20,
|
|
3704
3708
|
sessionBackfillBytes: 64 * 1024,
|
|
3705
3709
|
sessionStreamUpdateMs: 800
|
|
@@ -3930,6 +3934,22 @@ function loadRuntimeEnv(source = process.env) {
|
|
|
3930
3934
|
source.DSH_LARK_GUARDIAN_ENGINE_DEAD_MS,
|
|
3931
3935
|
DEFAULTS.guardianEngineDeadMs,
|
|
3932
3936
|
"DSH_LARK_GUARDIAN_ENGINE_DEAD_MS"
|
|
3937
|
+
),
|
|
3938
|
+
channelPingTimeoutSec: parsePositiveIntMin(
|
|
3939
|
+
source.DSH_LARK_CHANNEL_PING_TIMEOUT_SEC,
|
|
3940
|
+
DEFAULTS.channelPingTimeoutSec,
|
|
3941
|
+
"DSH_LARK_CHANNEL_PING_TIMEOUT_SEC"
|
|
3942
|
+
),
|
|
3943
|
+
channelKeepalive: parseBoolean(source.DSH_LARK_CHANNEL_KEEPALIVE, DEFAULTS.channelKeepalive),
|
|
3944
|
+
channelKeepaliveMs: parsePositiveIntMin(
|
|
3945
|
+
source.DSH_LARK_CHANNEL_KEEPALIVE_MS,
|
|
3946
|
+
DEFAULTS.channelKeepaliveMs,
|
|
3947
|
+
"DSH_LARK_CHANNEL_KEEPALIVE_MS"
|
|
3948
|
+
),
|
|
3949
|
+
channelHealthPollMs: parsePositiveIntMin(
|
|
3950
|
+
source.DSH_LARK_CHANNEL_HEALTH_POLL_MS,
|
|
3951
|
+
DEFAULTS.channelHealthPollMs,
|
|
3952
|
+
"DSH_LARK_CHANNEL_HEALTH_POLL_MS"
|
|
3933
3953
|
)
|
|
3934
3954
|
};
|
|
3935
3955
|
}
|
|
@@ -4477,15 +4497,6 @@ function guardianStatePath(root) {
|
|
|
4477
4497
|
return join11(root, ".dsh-lark", "guardian.json");
|
|
4478
4498
|
}
|
|
4479
4499
|
|
|
4480
|
-
// src/upgrade/detect.ts
|
|
4481
|
-
init_own_package();
|
|
4482
|
-
import { readFile as readFile10 } from "fs/promises";
|
|
4483
|
-
import { join as join12 } from "path";
|
|
4484
|
-
import { homedir as homedir7 } from "os";
|
|
4485
|
-
init_dsh_runtime();
|
|
4486
|
-
init_process();
|
|
4487
|
-
import { existsSync as existsSync6 } from "fs";
|
|
4488
|
-
|
|
4489
4500
|
// src/guardian/heartbeat.ts
|
|
4490
4501
|
import { readFile as readFile9 } from "fs/promises";
|
|
4491
4502
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
@@ -4496,15 +4507,25 @@ async function readHeartbeat(file) {
|
|
|
4496
4507
|
if (typeof parsed.pid !== "number" || typeof parsed.startedAt !== "string" || typeof parsed.ts !== "number") {
|
|
4497
4508
|
return void 0;
|
|
4498
4509
|
}
|
|
4499
|
-
|
|
4510
|
+
const payload = {
|
|
4500
4511
|
pid: parsed.pid,
|
|
4501
4512
|
startedAt: parsed.startedAt,
|
|
4502
4513
|
ts: parsed.ts
|
|
4503
4514
|
};
|
|
4515
|
+
const channel = parsed.channel;
|
|
4516
|
+
if (isChannelSnapshot(channel)) {
|
|
4517
|
+
payload.channel = channel;
|
|
4518
|
+
}
|
|
4519
|
+
return payload;
|
|
4504
4520
|
} catch {
|
|
4505
4521
|
return void 0;
|
|
4506
4522
|
}
|
|
4507
4523
|
}
|
|
4524
|
+
function isChannelSnapshot(value) {
|
|
4525
|
+
if (!value || typeof value !== "object") return false;
|
|
4526
|
+
const snapshot = value;
|
|
4527
|
+
return typeof snapshot.state === "string" && typeof snapshot.ready === "boolean";
|
|
4528
|
+
}
|
|
4508
4529
|
function heartbeatAgeMs(payload, now = Date.now()) {
|
|
4509
4530
|
return Math.max(0, now - payload.ts);
|
|
4510
4531
|
}
|
|
@@ -4512,12 +4533,34 @@ function isHeartbeatFresh(payload, maxAgeMs, now = Date.now()) {
|
|
|
4512
4533
|
if (payload === void 0) return false;
|
|
4513
4534
|
return heartbeatAgeMs(payload, now) < maxAgeMs;
|
|
4514
4535
|
}
|
|
4515
|
-
function
|
|
4536
|
+
function channelHealthLabel(channel) {
|
|
4537
|
+
if (channel === void 0) return "\u672A\u4E0A\u62A5";
|
|
4538
|
+
if (channel.ready) {
|
|
4539
|
+
return `ready (generation ${channel.generation ?? "-"})`;
|
|
4540
|
+
}
|
|
4541
|
+
switch (channel.state) {
|
|
4542
|
+
case "connecting":
|
|
4543
|
+
return "connecting";
|
|
4544
|
+
case "reconnecting":
|
|
4545
|
+
return `reconnecting (attempt ${channel.reconnectAttempts ?? "-"})`;
|
|
4546
|
+
case "failed":
|
|
4547
|
+
return "failed";
|
|
4548
|
+
case "stopped":
|
|
4549
|
+
return "stopped";
|
|
4550
|
+
default:
|
|
4551
|
+
return channel.state;
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
function startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, getChannelHealth) {
|
|
4516
4555
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4517
4556
|
let stopped = false;
|
|
4518
4557
|
const beat = async () => {
|
|
4519
4558
|
if (stopped) return;
|
|
4520
4559
|
const payload = { pid, startedAt, ts: Date.now() };
|
|
4560
|
+
const channel = getChannelHealth?.();
|
|
4561
|
+
if (channel !== void 0) {
|
|
4562
|
+
payload.channel = channel;
|
|
4563
|
+
}
|
|
4521
4564
|
try {
|
|
4522
4565
|
await writeFileAtomic(file, `${JSON.stringify(payload)}
|
|
4523
4566
|
`, {
|
|
@@ -4540,6 +4583,13 @@ function startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS) {
|
|
|
4540
4583
|
}
|
|
4541
4584
|
|
|
4542
4585
|
// src/upgrade/detect.ts
|
|
4586
|
+
init_own_package();
|
|
4587
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
4588
|
+
import { join as join12 } from "path";
|
|
4589
|
+
import { homedir as homedir7 } from "os";
|
|
4590
|
+
init_dsh_runtime();
|
|
4591
|
+
init_process();
|
|
4592
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4543
4593
|
async function readInstalledPackage(dshHome, profile, packageName) {
|
|
4544
4594
|
const manifest = join12(
|
|
4545
4595
|
dshHome,
|
|
@@ -4693,7 +4743,7 @@ async function fetchNpmLatestVersion(packageName, registryUrl = defaultRegistryU
|
|
|
4693
4743
|
}
|
|
4694
4744
|
return void 0;
|
|
4695
4745
|
}
|
|
4696
|
-
async function fetchNpmLatestVersionOnce(packageName, registryUrl = defaultRegistryUrl(), timeoutMs =
|
|
4746
|
+
async function fetchNpmLatestVersionOnce(packageName, registryUrl = defaultRegistryUrl(), timeoutMs = NPM_LATEST_TIMEOUT_MS, fetcher = fetch) {
|
|
4697
4747
|
try {
|
|
4698
4748
|
const response = await fetcher(
|
|
4699
4749
|
`${registryUrl}/${encodeURIComponent(packageName)}/latest`,
|
|
@@ -5918,6 +5968,18 @@ async function runDoctorChecks(options) {
|
|
|
5918
5968
|
} catch (error) {
|
|
5919
5969
|
lines.push(`service: \u26A0\uFE0F \u72B6\u6001\u68C0\u67E5\u5931\u8D25\uFF08${error instanceof Error ? error.message : String(error)}\uFF09`);
|
|
5920
5970
|
}
|
|
5971
|
+
try {
|
|
5972
|
+
const heartbeat = await readHeartbeat(
|
|
5973
|
+
paths.profilePath(env.guardianBridgeProfile, "guardian", "heartbeat.json")
|
|
5974
|
+
);
|
|
5975
|
+
lines.push(`channel: ${channelHealthLabel(heartbeat?.channel)}`);
|
|
5976
|
+
if (heartbeat?.channel && !heartbeat.channel.ready) {
|
|
5977
|
+
lines.push(
|
|
5978
|
+
"channel: \u26A0\uFE0F \u5F15\u64CE\u8FDB\u7A0B\u5B58\u6D3B\u4F46\u901A\u9053\u672A\u5C31\u7EEA\uFF08\u534A\u5F00/\u91CD\u8FDE/\u5931\u8D25\uFF09\uFF1B\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8DEF\u7531\u6216\u91CD\u542F managed engine"
|
|
5979
|
+
);
|
|
5980
|
+
}
|
|
5981
|
+
} catch {
|
|
5982
|
+
}
|
|
5921
5983
|
try {
|
|
5922
5984
|
const state = await loadUpgradeState(upgradeStatePath(env.home));
|
|
5923
5985
|
if (state?.lastUpgrade.pendingRestart === true) {
|
|
@@ -7057,6 +7119,106 @@ var LanguagePolicyStore = class {
|
|
|
7057
7119
|
// src/bridge/channel.ts
|
|
7058
7120
|
import { createLarkChannel as createLarkChannel2 } from "@larksuite/channel";
|
|
7059
7121
|
|
|
7122
|
+
// src/bridge/channel-health.ts
|
|
7123
|
+
function mapState(state) {
|
|
7124
|
+
switch (state) {
|
|
7125
|
+
case "connected":
|
|
7126
|
+
return "ready";
|
|
7127
|
+
case "reconnecting":
|
|
7128
|
+
return "reconnecting";
|
|
7129
|
+
case "failed":
|
|
7130
|
+
return "failed";
|
|
7131
|
+
case "connecting":
|
|
7132
|
+
return "connecting";
|
|
7133
|
+
case "idle":
|
|
7134
|
+
default:
|
|
7135
|
+
return "connecting";
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
var ChannelHealthMonitor = class {
|
|
7139
|
+
channel;
|
|
7140
|
+
pollMs;
|
|
7141
|
+
onUpdate;
|
|
7142
|
+
generation = 0;
|
|
7143
|
+
connectedAt;
|
|
7144
|
+
lastInboundAt;
|
|
7145
|
+
lastReconnectAt;
|
|
7146
|
+
lastError;
|
|
7147
|
+
observedState = "connecting";
|
|
7148
|
+
reconnectAttempts = 0;
|
|
7149
|
+
at = 0;
|
|
7150
|
+
pollTimer;
|
|
7151
|
+
constructor(channel, options = {}) {
|
|
7152
|
+
this.channel = channel;
|
|
7153
|
+
this.pollMs = options.pollMs ?? 5e3;
|
|
7154
|
+
this.onUpdate = options.onUpdate;
|
|
7155
|
+
}
|
|
7156
|
+
/** Latest snapshot (always a copy; safe to persist). */
|
|
7157
|
+
snapshot() {
|
|
7158
|
+
return {
|
|
7159
|
+
state: this.observedState,
|
|
7160
|
+
ready: this.observedState === "ready",
|
|
7161
|
+
generation: this.generation,
|
|
7162
|
+
...this.connectedAt !== void 0 ? { connectedAt: this.connectedAt } : {},
|
|
7163
|
+
reconnectAttempts: this.reconnectAttempts,
|
|
7164
|
+
...this.lastInboundAt !== void 0 ? { lastInboundAt: this.lastInboundAt } : {},
|
|
7165
|
+
...this.lastReconnectAt !== void 0 ? { lastReconnectAt: this.lastReconnectAt } : {},
|
|
7166
|
+
...this.lastError !== void 0 ? { lastError: this.lastError } : {},
|
|
7167
|
+
at: this.at
|
|
7168
|
+
};
|
|
7169
|
+
}
|
|
7170
|
+
/** Bind transport hooks from the bridge event subscription. */
|
|
7171
|
+
observeMessage() {
|
|
7172
|
+
this.lastInboundAt = Date.now();
|
|
7173
|
+
this.markChanged();
|
|
7174
|
+
}
|
|
7175
|
+
observeReconnecting() {
|
|
7176
|
+
this.observedState = "reconnecting";
|
|
7177
|
+
this.markChanged();
|
|
7178
|
+
}
|
|
7179
|
+
observeReconnected() {
|
|
7180
|
+
this.generation += 1;
|
|
7181
|
+
const now = Date.now();
|
|
7182
|
+
this.connectedAt = now;
|
|
7183
|
+
this.lastReconnectAt = now;
|
|
7184
|
+
this.observedState = "ready";
|
|
7185
|
+
this.markChanged();
|
|
7186
|
+
}
|
|
7187
|
+
observeError(error) {
|
|
7188
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
7189
|
+
if (this.observedState !== "reconnecting" && this.observedState !== "failed") {
|
|
7190
|
+
this.observedState = "failed";
|
|
7191
|
+
}
|
|
7192
|
+
this.markChanged();
|
|
7193
|
+
}
|
|
7194
|
+
/** Poll the SDK's connection-status snapshot. */
|
|
7195
|
+
start() {
|
|
7196
|
+
if (this.pollTimer) return;
|
|
7197
|
+
this.refresh();
|
|
7198
|
+
this.pollTimer = setInterval(() => this.refresh(), this.pollMs);
|
|
7199
|
+
this.pollTimer.unref?.();
|
|
7200
|
+
}
|
|
7201
|
+
stop() {
|
|
7202
|
+
if (this.pollTimer) {
|
|
7203
|
+
clearInterval(this.pollTimer);
|
|
7204
|
+
this.pollTimer = void 0;
|
|
7205
|
+
}
|
|
7206
|
+
this.observedState = "stopped";
|
|
7207
|
+
this.markChanged();
|
|
7208
|
+
}
|
|
7209
|
+
refresh() {
|
|
7210
|
+
const status = this.channel.getConnectionStatus?.();
|
|
7211
|
+
this.observedState = mapState(status?.state);
|
|
7212
|
+
this.reconnectAttempts = status?.reconnectAttempts ?? this.reconnectAttempts;
|
|
7213
|
+
this.markChanged();
|
|
7214
|
+
}
|
|
7215
|
+
markChanged() {
|
|
7216
|
+
const now = Date.now();
|
|
7217
|
+
this.at = now;
|
|
7218
|
+
this.onUpdate?.(this.snapshot());
|
|
7219
|
+
}
|
|
7220
|
+
};
|
|
7221
|
+
|
|
7060
7222
|
// src/commands/index.ts
|
|
7061
7223
|
import { stat as stat8 } from "fs/promises";
|
|
7062
7224
|
import { homedir as homedir14 } from "os";
|
|
@@ -10879,6 +11041,7 @@ async function reply6(ctx, zh, en) {
|
|
|
10879
11041
|
// src/upgrade/update-check.ts
|
|
10880
11042
|
init_own_package();
|
|
10881
11043
|
var UPDATE_CHECK_CACHE_MS = 60 * 6e4;
|
|
11044
|
+
var UPDATE_CHECK_FAILURE_CACHE_MS = 15e3;
|
|
10882
11045
|
var cache;
|
|
10883
11046
|
function upgradeCheckEnabled() {
|
|
10884
11047
|
return (process.env.DSH_LARK_UPGRADE_CHECK ?? "1") !== "0";
|
|
@@ -10894,16 +11057,22 @@ function isNewer(latest, current) {
|
|
|
10894
11057
|
async function latestVersion(options = {}) {
|
|
10895
11058
|
if (!upgradeCheckEnabled()) return void 0;
|
|
10896
11059
|
const now = Date.now();
|
|
10897
|
-
if (cache !== void 0
|
|
10898
|
-
|
|
11060
|
+
if (cache !== void 0) {
|
|
11061
|
+
const ttl = options.cacheMs !== void 0 ? options.cacheMs : cache.failed ? UPDATE_CHECK_FAILURE_CACHE_MS : UPDATE_CHECK_CACHE_MS;
|
|
11062
|
+
if (now - cache.at < ttl) return cache.latest;
|
|
10899
11063
|
}
|
|
10900
11064
|
const probe = options.probe ?? fetchNpmLatestVersionOnce;
|
|
10901
11065
|
const latest = await probe(options.packageName ?? ownPackageInfo().name);
|
|
10902
|
-
cache = {
|
|
11066
|
+
cache = {
|
|
11067
|
+
at: now,
|
|
11068
|
+
latest,
|
|
11069
|
+
failed: latest === void 0,
|
|
11070
|
+
notified: cache?.notified ?? /* @__PURE__ */ new Set()
|
|
11071
|
+
};
|
|
10903
11072
|
return latest;
|
|
10904
11073
|
}
|
|
10905
11074
|
function markNotified(latest) {
|
|
10906
|
-
if (cache === void 0) cache = { at: 0, latest, notified: /* @__PURE__ */ new Set() };
|
|
11075
|
+
if (cache === void 0) cache = { at: 0, latest, failed: false, notified: /* @__PURE__ */ new Set() };
|
|
10907
11076
|
if (cache.notified.has(latest)) return false;
|
|
10908
11077
|
cache.notified.add(latest);
|
|
10909
11078
|
return true;
|
|
@@ -13744,6 +13913,8 @@ The value is written only by the local bridge and is not sent to the agent.` },
|
|
|
13744
13913
|
}
|
|
13745
13914
|
|
|
13746
13915
|
// src/bridge/channel.ts
|
|
13916
|
+
var DEFAULT_CHANNEL_PING_TIMEOUT_SEC = 30;
|
|
13917
|
+
var DEFAULT_CHANNEL_KEEPALIVE_MS = 15e3;
|
|
13747
13918
|
async function startChannel(deps) {
|
|
13748
13919
|
const channel = (deps.createChannel ?? createLarkChannel2)({
|
|
13749
13920
|
appId: deps.appId,
|
|
@@ -13770,7 +13941,26 @@ async function startChannel(deps) {
|
|
|
13770
13941
|
resolveChatMode: true,
|
|
13771
13942
|
handshakeTimeoutMs: 8e3,
|
|
13772
13943
|
httpTimeoutMs: 3e4,
|
|
13773
|
-
respectProxyEnv: true
|
|
13944
|
+
respectProxyEnv: true,
|
|
13945
|
+
// Issue #108: detect a half-open WebSocket (TCP ESTABLISHED but Feishu no
|
|
13946
|
+
// longer delivering). `pingTimeout` force-reconnects when no inbound frame
|
|
13947
|
+
// arrives after the last ping; the app-level `keepalive` probes and
|
|
13948
|
+
// force-reconnects, and `onUnrecoverable` fires when even that fails so the
|
|
13949
|
+
// engine can exit and let the managed service / guardian restart it.
|
|
13950
|
+
wsConfig: { pingTimeout: deps.channelPingTimeoutSec ?? DEFAULT_CHANNEL_PING_TIMEOUT_SEC },
|
|
13951
|
+
keepalive: {
|
|
13952
|
+
enabled: deps.channelKeepalive ?? true,
|
|
13953
|
+
intervalMs: deps.channelKeepaliveMs ?? DEFAULT_CHANNEL_KEEPALIVE_MS,
|
|
13954
|
+
onUnrecoverable: (error) => {
|
|
13955
|
+
log.fail("channel", "unrecoverable", {
|
|
13956
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13957
|
+
});
|
|
13958
|
+
deps.onChannelUnrecoverable?.(error);
|
|
13959
|
+
}
|
|
13960
|
+
}
|
|
13961
|
+
});
|
|
13962
|
+
const channelHealth = new ChannelHealthMonitor(channel, {
|
|
13963
|
+
...deps.channelHealthPollMs !== void 0 ? { pollMs: deps.channelHealthPollMs } : {}
|
|
13774
13964
|
});
|
|
13775
13965
|
const streaming = adaptLarkChannel(channel);
|
|
13776
13966
|
const commandChannel = streaming;
|
|
@@ -13811,6 +14001,7 @@ async function startChannel(deps) {
|
|
|
13811
14001
|
const isolationStore = deps.isolationStore ?? EMPTY_ISOLATION_STORE;
|
|
13812
14002
|
let groupPoller;
|
|
13813
14003
|
const processMessage = async (msg, alreadyClaimed = false) => {
|
|
14004
|
+
channelHealth.observeMessage();
|
|
13814
14005
|
if (groupPoller && !alreadyClaimed && !groupPoller.claim(msg.messageId)) return;
|
|
13815
14006
|
const chatMode = msg.chatMode ?? msg.chatType;
|
|
13816
14007
|
const botSender = msg.senderType === "bot";
|
|
@@ -14387,21 +14578,25 @@ ${msg.content}`,
|
|
|
14387
14578
|
},
|
|
14388
14579
|
reconnecting: () => {
|
|
14389
14580
|
log.warn("channel", "reconnecting", {});
|
|
14581
|
+
channelHealth.observeReconnecting();
|
|
14390
14582
|
void reconnectNotifier.reconnecting().catch((error) => {
|
|
14391
14583
|
log.fail("channel-reconnect-notice", error);
|
|
14392
14584
|
});
|
|
14393
14585
|
},
|
|
14394
14586
|
reconnected: () => {
|
|
14395
14587
|
log.info("channel", "reconnected", {});
|
|
14588
|
+
channelHealth.observeReconnected();
|
|
14396
14589
|
void reconnectNotifier.reconnected().catch((error) => {
|
|
14397
14590
|
log.fail("channel-reconnect-notice", error);
|
|
14398
14591
|
});
|
|
14399
14592
|
},
|
|
14400
14593
|
error: (error) => {
|
|
14401
14594
|
log.fail("channel", error);
|
|
14595
|
+
channelHealth.observeError(error);
|
|
14402
14596
|
}
|
|
14403
14597
|
});
|
|
14404
14598
|
await channel.connect();
|
|
14599
|
+
channelHealth.start();
|
|
14405
14600
|
if (sessionProjectionBridge) {
|
|
14406
14601
|
void sessionProjectionBridge.start().catch((error) => {
|
|
14407
14602
|
log.fail("session-projection", error, { step: "start" });
|
|
@@ -14418,8 +14613,10 @@ ${msg.content}`,
|
|
|
14418
14613
|
}
|
|
14419
14614
|
return {
|
|
14420
14615
|
channel,
|
|
14616
|
+
channelHealth: () => channelHealth.snapshot(),
|
|
14421
14617
|
disconnect: async () => {
|
|
14422
14618
|
await groupPoller?.stop();
|
|
14619
|
+
channelHealth.stop();
|
|
14423
14620
|
sessionProjection?.close();
|
|
14424
14621
|
await sessionProjectionBridge?.close();
|
|
14425
14622
|
await channel.disconnect();
|
|
@@ -18579,7 +18776,18 @@ async function startBridgeEngine(options) {
|
|
|
18579
18776
|
},
|
|
18580
18777
|
allowedUsers: activeProfile.access.allowedUsers,
|
|
18581
18778
|
allowedChats: activeProfile.access.allowedChats,
|
|
18582
|
-
...options.createChannel ? { createChannel: options.createChannel } : {}
|
|
18779
|
+
...options.createChannel ? { createChannel: options.createChannel } : {},
|
|
18780
|
+
channelPingTimeoutSec: env.channelPingTimeoutSec,
|
|
18781
|
+
channelKeepalive: env.channelKeepalive,
|
|
18782
|
+
channelKeepaliveMs: env.channelKeepaliveMs,
|
|
18783
|
+
channelHealthPollMs: env.channelHealthPollMs,
|
|
18784
|
+
onChannelUnrecoverable: (error) => {
|
|
18785
|
+
log.fail("engine", "channel-unrecoverable", {
|
|
18786
|
+
error: error instanceof Error ? error.message : String(error)
|
|
18787
|
+
});
|
|
18788
|
+
process.exitCode = 1;
|
|
18789
|
+
setTimeout(() => process.exit(1), 300);
|
|
18790
|
+
}
|
|
18583
18791
|
};
|
|
18584
18792
|
if (activeProfile.preferences.stopGraceMs !== void 0) {
|
|
18585
18793
|
channelInput.stopGraceMs = activeProfile.preferences.stopGraceMs;
|
|
@@ -18644,7 +18852,8 @@ async function startBridgeEngine(options) {
|
|
|
18644
18852
|
const heartbeat = startHeartbeat(
|
|
18645
18853
|
paths.profilePath(profileName, "guardian", "heartbeat.json"),
|
|
18646
18854
|
process.pid,
|
|
18647
|
-
env.heartbeatMs
|
|
18855
|
+
env.heartbeatMs,
|
|
18856
|
+
() => bridge.channelHealth?.()
|
|
18648
18857
|
);
|
|
18649
18858
|
await updateHandoff.reconcile(currentVersion());
|
|
18650
18859
|
void deliverUpdateResult().catch((error) => log.fail("upgrade", error, { step: "deliver-result" }));
|
|
@@ -19723,6 +19932,7 @@ var GuardianService = class {
|
|
|
19723
19932
|
dshUp: this.dshUp,
|
|
19724
19933
|
heartbeatAgeMs: this.lastHeartbeatAgeMs,
|
|
19725
19934
|
channelConnected: this.channel !== void 0,
|
|
19935
|
+
channelState: this.lastHeartbeatChannel?.state,
|
|
19726
19936
|
safeEngine: this.safeEngine?.kind,
|
|
19727
19937
|
safeRuns: this.safeRuns.size,
|
|
19728
19938
|
pid: process.pid,
|
|
@@ -19733,6 +19943,9 @@ var GuardianService = class {
|
|
|
19733
19943
|
}
|
|
19734
19944
|
dshUp = false;
|
|
19735
19945
|
lastHeartbeatAgeMs;
|
|
19946
|
+
lastHeartbeatChannel;
|
|
19947
|
+
/** When the engine reported a non-ready channel while its heartbeat was fresh. */
|
|
19948
|
+
channelUnhealthySinceMs;
|
|
19736
19949
|
log() {
|
|
19737
19950
|
return this.options.logger ?? log;
|
|
19738
19951
|
}
|
|
@@ -19767,7 +19980,24 @@ var GuardianService = class {
|
|
|
19767
19980
|
const now = (this.options.now ?? Date.now)();
|
|
19768
19981
|
const heartbeatFresh = isHeartbeatFresh(heartbeat, this.options.staleMs, now);
|
|
19769
19982
|
this.lastHeartbeatAgeMs = heartbeat ? heartbeatAgeMs(heartbeat, now) : void 0;
|
|
19983
|
+
this.lastHeartbeatChannel = heartbeat?.channel;
|
|
19770
19984
|
if (heartbeatFresh) this.lastHeartbeatFreshAt = now;
|
|
19985
|
+
const channelSnapshot = this.lastHeartbeatChannel;
|
|
19986
|
+
const channelUnhealthy = heartbeatFresh && channelSnapshot !== void 0 && channelSnapshot.ready === false;
|
|
19987
|
+
if (channelUnhealthy) {
|
|
19988
|
+
if (this.channelUnhealthySinceMs === void 0) {
|
|
19989
|
+
this.channelUnhealthySinceMs = now;
|
|
19990
|
+
this.log().warn("guardian", "channel-unhealthy", {
|
|
19991
|
+
dshProfile: this.state.dshProfile,
|
|
19992
|
+
state: channelSnapshot.state,
|
|
19993
|
+
generation: channelSnapshot.generation,
|
|
19994
|
+
reconnectAttempts: channelSnapshot.reconnectAttempts,
|
|
19995
|
+
lastError: channelSnapshot.lastError
|
|
19996
|
+
});
|
|
19997
|
+
}
|
|
19998
|
+
} else {
|
|
19999
|
+
this.channelUnhealthySinceMs = void 0;
|
|
20000
|
+
}
|
|
19771
20001
|
const processFound = await (this.options.findProcess ?? findProfileProcess)(
|
|
19772
20002
|
this.state.dshProfile
|
|
19773
20003
|
);
|
|
@@ -20815,6 +21045,7 @@ async function statusGuardianCommand(options = {}, deps = {}) {
|
|
|
20815
21045
|
`\u6865\u63A5 profile\uFF1A${state.bridgeProfile}`,
|
|
20816
21046
|
`dsh \u662F\u5426\u5728\u7EBF\uFF1A${up ? "\u662F" : "\u5426"}${processFound ? `\uFF08pid ${processFound.pid}\uFF09` : ""}`,
|
|
20817
21047
|
`\u5FC3\u8DF3\u9F84\uFF1A${heartbeat ? `${heartbeatAgeMs(heartbeat)}ms` : "\u65E0"}`,
|
|
21048
|
+
`\u98DE\u4E66\u901A\u9053\uFF1A${channelHealthLabel(heartbeat?.channel)}`,
|
|
20818
21049
|
`\u5DF2\u89C2\u5BDF\u8FC7 dsh \u8FD0\u884C\uFF1A${state.profileSeenUp ? "\u662F" : "\u5426"}`,
|
|
20819
21050
|
guardianProcess === void 0 ? "\u5B88\u62A4\u8FDB\u7A0B pid\uFF1A\u672A\u53D1\u73B0\uFF08\u65E0\u6CD5\u552F\u4E00\u8BC1\u660E resident guardian \u8EAB\u4EFD\uFF09" : `\u5B88\u62A4\u8FDB\u7A0B pid\uFF1A${guardianProcess.pid}`,
|
|
20820
21051
|
`\u72B6\u6001\u6587\u4EF6\uFF1A${layout.stateFile}`,
|
|
@@ -20853,7 +21084,7 @@ function managerFor(options, version) {
|
|
|
20853
21084
|
version
|
|
20854
21085
|
});
|
|
20855
21086
|
}
|
|
20856
|
-
function formatServiceStatus(status, heartbeatAge2) {
|
|
21087
|
+
function formatServiceStatus(status, heartbeatAge2, channel) {
|
|
20857
21088
|
return [
|
|
20858
21089
|
"dsh-lark-bot \u6B63\u5E38\u5F15\u64CE\u670D\u52A1",
|
|
20859
21090
|
` name: ${status.name}`,
|
|
@@ -20864,7 +21095,8 @@ function formatServiceStatus(status, heartbeatAge2) {
|
|
|
20864
21095
|
` detail: ${status.detail}`,
|
|
20865
21096
|
` pid: ${status.pid ?? "-"}`,
|
|
20866
21097
|
` restarts: ${status.restarts ?? "-"}`,
|
|
20867
|
-
` heartbeat: ${heartbeatAge2 === void 0 ? "\u6682\u65E0" : `${heartbeatAge2}ms`}
|
|
21098
|
+
` heartbeat: ${heartbeatAge2 === void 0 ? "\u6682\u65E0" : `${heartbeatAge2}ms`}`,
|
|
21099
|
+
` channel: ${channelHealthLabel(channel)}`
|
|
20868
21100
|
].join("\n");
|
|
20869
21101
|
}
|
|
20870
21102
|
async function heartbeatAge() {
|
|
@@ -20877,6 +21109,16 @@ async function heartbeatAge() {
|
|
|
20877
21109
|
const heartbeat = await readHeartbeat(file);
|
|
20878
21110
|
return heartbeat ? heartbeatAgeMs(heartbeat) : void 0;
|
|
20879
21111
|
}
|
|
21112
|
+
async function heartbeatChannel() {
|
|
21113
|
+
const env = loadRuntimeEnv(process.env);
|
|
21114
|
+
const file = resolveAppPaths(env.home).profilePath(
|
|
21115
|
+
env.guardianBridgeProfile,
|
|
21116
|
+
"guardian",
|
|
21117
|
+
"heartbeat.json"
|
|
21118
|
+
);
|
|
21119
|
+
const heartbeat = await readHeartbeat(file);
|
|
21120
|
+
return heartbeat?.channel;
|
|
21121
|
+
}
|
|
20880
21122
|
async function runServiceCommand(action, options = {}, deps = {}) {
|
|
20881
21123
|
const manager = deps.manager ?? managerFor(options, deps.version ?? "0.0.0");
|
|
20882
21124
|
const output = deps.output ?? ((text) => process.stdout.write(text));
|
|
@@ -20893,7 +21135,11 @@ ${logs.text || "\uFF08\u6682\u65E0\u65E5\u5FD7\uFF09"}
|
|
|
20893
21135
|
return;
|
|
20894
21136
|
}
|
|
20895
21137
|
const status = action === "install" ? await manager.install() : action === "start" ? await manager.start() : action === "restart" ? await manager.restart() : action === "stop" ? await manager.stop() : action === "uninstall" ? await manager.uninstall() : await manager.status();
|
|
20896
|
-
|
|
21138
|
+
const [age, channel] = await Promise.all([
|
|
21139
|
+
(deps.heartbeatAge ?? heartbeatAge)(),
|
|
21140
|
+
(deps.heartbeatChannel ?? heartbeatChannel)()
|
|
21141
|
+
]);
|
|
21142
|
+
output(`${formatServiceStatus(status, age, channel)}
|
|
20897
21143
|
`);
|
|
20898
21144
|
if ((action === "install" || action === "start" || action === "restart") && status.state !== "running") process.exitCode = 1;
|
|
20899
21145
|
if (action === "status" && (!status.installed || status.state === "error")) {
|