dsh-lark-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 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
- return {
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 startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS) {
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 = 5e3, fetcher = fetch) {
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 && now - cache.at < (options.cacheMs ?? UPDATE_CHECK_CACHE_MS)) {
10898
- return cache.latest;
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 = { at: now, latest, notified: cache?.notified ?? /* @__PURE__ */ new Set() };
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
- output(`${formatServiceStatus(status, await (deps.heartbeatAge ?? heartbeatAge)())}
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")) {