dsh-lark-bot 0.19.11 → 0.19.13

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/plugin.js CHANGED
@@ -296,6 +296,16 @@ var init_profile_package = __esm({
296
296
  }
297
297
  });
298
298
 
299
+ // src/adapters/model-choice.ts
300
+ function resolveModelChoice(preferenceModel, envModel) {
301
+ return preferenceModel?.trim() || envModel;
302
+ }
303
+ var init_model_choice = __esm({
304
+ "src/adapters/model-choice.ts"() {
305
+ "use strict";
306
+ }
307
+ });
308
+
299
309
  // src/adapters/dsh/acp-runtime.ts
300
310
  import { spawn as spawn4 } from "cross-spawn";
301
311
  import { existsSync as existsSync4, realpathSync as realpathSync3 } from "fs";
@@ -658,7 +668,7 @@ async function buildAcpAgentAdapter(env, preferences) {
658
668
  home: homedir2(),
659
669
  env: process.env,
660
670
  provider: env.provider,
661
- model: preferences.model ?? env.model,
671
+ model: resolveModelChoice(preferences.model, env.model),
662
672
  ...env.dshExplicit ? { command: env.dshCommand, args: env.dshArgs } : {}
663
673
  };
664
674
  const ensure = await ensureAcpProfile(runtimeOptions);
@@ -671,7 +681,7 @@ async function buildAcpAgentAdapter(env, preferences) {
671
681
  return new AcpDshAdapter({
672
682
  launch,
673
683
  provider: env.provider,
674
- model: preferences.model ?? env.model
684
+ model: resolveModelChoice(preferences.model, env.model)
675
685
  });
676
686
  }
677
687
  var AcpDshAdapter;
@@ -680,6 +690,7 @@ var init_acp_adapter = __esm({
680
690
  "use strict";
681
691
  init_acp_runtime();
682
692
  init_event_channel();
693
+ init_model_choice();
683
694
  AcpDshAdapter = class {
684
695
  id = "dsh-acp";
685
696
  displayName = "DeepSeek Harness (ACP)";
@@ -806,14 +817,14 @@ var init_acp_adapter = __esm({
806
817
  return;
807
818
  }
808
819
  if (response.usage) {
809
- const usage3 = {
820
+ const usage4 = {
810
821
  type: "usage",
811
822
  ...typeof response.usage.inputTokens === "number" ? { inputTokens: response.usage.inputTokens } : {},
812
823
  ...typeof response.usage.outputTokens === "number" ? { outputTokens: response.usage.outputTokens } : {},
813
824
  ...typeof response.usage.cachedReadTokens === "number" ? { cacheReadTokens: response.usage.cachedReadTokens } : {},
814
825
  ...typeof response.usage.cachedWriteTokens === "number" ? { cacheWriteTokens: response.usage.cachedWriteTokens } : {}
815
826
  };
816
- channel.push(usage3);
827
+ channel.push(usage4);
817
828
  }
818
829
  })().catch((error) => {
819
830
  if (!stopRequested.value) {
@@ -1463,7 +1474,7 @@ var DshProviderManager = class {
1463
1474
  try {
1464
1475
  catalogProviders = await this.catalog.listProviders();
1465
1476
  } catch (error) {
1466
- log.warn("model-catalog", "refresh-failed", {
1477
+ log.info("model-catalog", "refresh-failed", {
1467
1478
  error: error instanceof Error ? error.message : String(error)
1468
1479
  });
1469
1480
  }
@@ -2242,11 +2253,11 @@ function translateToolResult(data) {
2242
2253
  }
2243
2254
  function translateAssistantMessage(data) {
2244
2255
  if (!isRecord3(data) || !isRecord3(data.usage)) return [];
2245
- const usage3 = data.usage;
2246
- const inputTokens = typeof usage3.inputTokens === "number" ? usage3.inputTokens : void 0;
2247
- const outputTokens = typeof usage3.outputTokens === "number" ? usage3.outputTokens : void 0;
2248
- const cacheReadTokens = typeof usage3.cacheReadTokens === "number" ? usage3.cacheReadTokens : void 0;
2249
- const cacheWriteTokens = typeof usage3.cacheWriteTokens === "number" ? usage3.cacheWriteTokens : void 0;
2256
+ const usage4 = data.usage;
2257
+ const inputTokens = typeof usage4.inputTokens === "number" ? usage4.inputTokens : void 0;
2258
+ const outputTokens = typeof usage4.outputTokens === "number" ? usage4.outputTokens : void 0;
2259
+ const cacheReadTokens = typeof usage4.cacheReadTokens === "number" ? usage4.cacheReadTokens : void 0;
2260
+ const cacheWriteTokens = typeof usage4.cacheWriteTokens === "number" ? usage4.cacheWriteTokens : void 0;
2250
2261
  if (inputTokens === void 0 && outputTokens === void 0 && cacheReadTokens === void 0 && cacheWriteTokens === void 0) return [];
2251
2262
  return [
2252
2263
  {
@@ -3201,6 +3212,7 @@ var WebDshAdapter = class {
3201
3212
  };
3202
3213
 
3203
3214
  // src/adapters/index.ts
3215
+ init_model_choice();
3204
3216
  async function resolveAdapterRoute(input, source) {
3205
3217
  const provider = input.provider?.trim() || void 0;
3206
3218
  const model = input.model?.trim() || void 0;
@@ -3220,7 +3232,7 @@ async function resolveAdapterRoute(input, source) {
3220
3232
  return void 0;
3221
3233
  }
3222
3234
  async function buildAgentAdapter(env, preferences = { stopGraceMs: void 0, model: void 0 }) {
3223
- const configuredModel = preferences.model ?? env.model;
3235
+ const configuredModel = resolveModelChoice(preferences.model, env.model);
3224
3236
  const managedRoute = env.adapterMode === "sdk" || env.adapterMode === "acp" ? await resolveAdapterRoute(
3225
3237
  { provider: env.provider, model: configuredModel },
3226
3238
  new DshProviderManager({ env: process.env })
@@ -3252,7 +3264,7 @@ async function buildAgentAdapter(env, preferences = { stopGraceMs: void 0, model
3252
3264
  return new WebDshAdapter({
3253
3265
  baseUrl: env.webBaseUrl,
3254
3266
  provider: env.provider,
3255
- model: preferences.model ?? env.model
3267
+ model: resolveModelChoice(preferences.model, env.model)
3256
3268
  });
3257
3269
  }
3258
3270
  case "sdk":
@@ -3997,7 +4009,7 @@ var PermissionPolicyStore = class {
3997
4009
 
3998
4010
  // src/bot/notification-preference-store.ts
3999
4011
  import { readFile as readFile7 } from "fs/promises";
4000
- var EVENTS = /* @__PURE__ */ new Set(["completed", "failed", "approval"]);
4012
+ var EVENTS = /* @__PURE__ */ new Set(["completed", "failed", "approval", "urgent"]);
4001
4013
  var NotificationPreferenceStore = class {
4002
4014
  constructor(path) {
4003
4015
  this.path = path;
@@ -4026,7 +4038,7 @@ var NotificationPreferenceStore = class {
4026
4038
  get(scope) {
4027
4039
  const value = this.data.scopes[scope];
4028
4040
  if (value === void 0 || value === false) return void 0;
4029
- return { ...value, events: [...value.events], mentionUserIds: [...value.mentionUserIds] };
4041
+ return { ...value, events: [...value.events], mentionUserIds: [...value.mentionUserIds], sinks: [...value.sinks] };
4030
4042
  }
4031
4043
  resolve(scope, fallback) {
4032
4044
  const value = this.data.scopes[scope];
@@ -4071,6 +4083,7 @@ function normalizePreference(value) {
4071
4083
  ...typeof input.target === "string" && input.target.trim() ? { target: input.target.trim() } : {},
4072
4084
  events,
4073
4085
  mentionUserIds: [...new Set((input.mentionUserIds ?? []).filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))],
4086
+ sinks: [...new Set((input.sinks ?? []).filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))],
4074
4087
  approvalReminderMs
4075
4088
  };
4076
4089
  }
@@ -4547,6 +4560,7 @@ function statusCardMarkdownFor(input, locale) {
4547
4560
  zh ? `\u{1F39A}\uFE0F **\u6267\u884C\u6A21\u5F0F**\uFF1A\`${input.executionMode ?? "balanced"}\`` : `\u{1F39A}\uFE0F **Execution mode**: \`${input.executionMode ?? "balanced"}\``,
4548
4561
  zh ? `\u{1F6E1}\uFE0F **\u5DE5\u5177\u6743\u9650**\uFF1A\`${input.permissionPolicy ?? "ask"}\`` : `\u{1F6E1}\uFE0F **Tool permission**: \`${input.permissionPolicy ?? "ask"}\``,
4549
4562
  ...input.notificationPreference ? [zh ? `\u{1F514} **\u4E3B\u52A8\u63D0\u9192**\uFF1A\`${input.notificationPreference.events.join(",")}\` \u2192 \`${input.notificationPreference.target ?? "current"}\`\uFF08\u5BA1\u6279 ${String(input.notificationPreference.approvalReminderMs / 6e4)} \u5206\u949F\uFF09` : `\u{1F514} **Notifications**: \`${input.notificationPreference.events.join(",")}\` \u2192 \`${input.notificationPreference.target ?? "current"}\` (approval ${String(input.notificationPreference.approvalReminderMs / 6e4)} min)`] : [zh ? "\u{1F515} **\u4E3B\u52A8\u63D0\u9192**\uFF1A\u5173\u95ED" : "\u{1F515} **Notifications**: off"],
4563
+ ...input.notificationChannels && input.notificationChannels.length > 0 ? [zh ? `\u{1F4E4} **\u51FA\u7AD9\u6E20\u9053**\uFF1A\`${input.notificationChannels.join(",")}\`` : `\u{1F4E4} **Outbound channels**: \`${input.notificationChannels.join(",")}\``] : [],
4550
4564
  ...input.replyPolicy ? [zh ? `\u{1F4E8} **\u56DE\u590D\u7B56\u7565**\uFF1A${input.replyPolicyConfigured ? "\u81EA\u5B9A\u4E49" : "\u9ED8\u8BA4"} \xB7 \u5408\u5E76 ${String(input.replyPolicy.mergeWindowMs / 1e3)} \u79D2 \xB7 \u6BCF\u6279 ${String(input.replyPolicy.maxBatchSize)} \xB7 \u95F4\u9694 ${String(input.replyPolicy.minIntervalMs / 1e3)} \u79D2 \xB7 \u53BB\u91CD ${String(input.replyPolicy.dedupeWindowMs / 1e3)} \u79D2` : `\u{1F4E8} **Reply policy**: ${input.replyPolicyConfigured ? "custom" : "default"} \xB7 merge ${String(input.replyPolicy.mergeWindowMs / 1e3)}s \xB7 batch ${String(input.replyPolicy.maxBatchSize)} \xB7 interval ${String(input.replyPolicy.minIntervalMs / 1e3)}s \xB7 dedupe ${String(input.replyPolicy.dedupeWindowMs / 1e3)}s`] : [],
4551
4565
  ...input.role ? [`\u{1F3AD} **role**\uFF1A${input.role}`] : [],
4552
4566
  `\u{1F516} **version**\uFF1A\`${input.version}\``,
@@ -4963,8 +4977,8 @@ function renderStandard(state, now, locale, maxTools) {
4963
4977
  } else if (state.terminal === "done" && !hasAnswer(state)) {
4964
4978
  elements.push(noteMd(zh ? "_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_" : "_(No content returned)_"));
4965
4979
  } else if (state.terminal === "done") {
4966
- const usage3 = usageLine(state);
4967
- if (usage3) elements.push(noteMd(usage3));
4980
+ const usage4 = usageLine(state);
4981
+ if (usage4) elements.push(noteMd(usage4));
4968
4982
  }
4969
4983
  if (state.finalDeliveryError) {
4970
4984
  elements.push(finalDeliveryFailureLine(locale));
@@ -5030,8 +5044,8 @@ function renderDetailed(state, now, locale, maxTools) {
5030
5044
  } else if (state.terminal === "error") {
5031
5045
  elements.push(runFailureLine(locale));
5032
5046
  } else if (state.terminal === "done") {
5033
- const usage3 = usageLine(state);
5034
- if (usage3) elements.push(noteMd(usage3));
5047
+ const usage4 = usageLine(state);
5048
+ if (usage4) elements.push(noteMd(usage4));
5035
5049
  if (!hasAnswer(state)) elements.push(noteMd(zh ? "_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_" : "_(No content returned)_"));
5036
5050
  }
5037
5051
  if (state.finalDeliveryError) {
@@ -8054,7 +8068,7 @@ async function handleNotify(args, ctx) {
8054
8068
  }
8055
8069
 
8056
8070
  // src/commands/notifications.ts
8057
- var ALL_EVENTS = ["completed", "failed", "approval"];
8071
+ var ALL_EVENTS = ["completed", "failed", "approval", "urgent"];
8058
8072
  async function reply5(ctx, zh, en) {
8059
8073
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
8060
8074
  }
@@ -8092,6 +8106,7 @@ async function handleNotifications(args, ctx) {
8092
8106
  let events = [...ALL_EVENTS];
8093
8107
  let mentionUserIds = ctx.senderId ? [ctx.senderId] : [];
8094
8108
  let reminderMinutes = 10;
8109
+ let sinks = [];
8095
8110
  for (const token of tokens.slice(1)) {
8096
8111
  if (token.startsWith("events=")) {
8097
8112
  const requested = token.slice(7).split(",").filter(Boolean);
@@ -8103,6 +8118,8 @@ async function handleNotifications(args, ctx) {
8103
8118
  } else if (token.startsWith("remind=")) {
8104
8119
  reminderMinutes = Number(token.slice(7));
8105
8120
  if (!Number.isInteger(reminderMinutes) || reminderMinutes < 1 || reminderMinutes > 1440) return usage(ctx);
8121
+ } else if (token.startsWith("sinks=")) {
8122
+ sinks = token.slice(6).split(",").map((id) => id.trim()).filter(Boolean);
8106
8123
  } else if (!target) {
8107
8124
  target = token === "current" ? void 0 : token;
8108
8125
  } else return usage(ctx);
@@ -8121,50 +8138,297 @@ async function handleNotifications(args, ctx) {
8121
8138
  ...target ? { target } : {},
8122
8139
  events,
8123
8140
  mentionUserIds: [...new Set(mentionUserIds)],
8124
- approvalReminderMs: reminderMinutes * 6e4
8141
+ approvalReminderMs: reminderMinutes * 6e4,
8142
+ sinks: [...new Set(sinks)]
8125
8143
  };
8126
8144
  await store.set(ctx.scope, value);
8127
8145
  await reply5(ctx, `\u5DF2\u5F00\u542F\u3002${describe(value, true)}`, `Enabled. ${describe(value, false)}`);
8128
8146
  }
8129
8147
  function describe(value, zh) {
8130
8148
  const minutes = value.approvalReminderMs / 6e4;
8131
- return zh ? `\u4E3B\u52A8\u63D0\u9192\uFF1A**\u5F00\u542F** \xB7 \u4E8B\u4EF6 \`${value.events.join(",")}\` \xB7 \u76EE\u6807 \`${value.target ?? "current"}\` \xB7 @ \`${value.mentionUserIds.join(",") || "none"}\` \xB7 \u5BA1\u6279\u63D0\u9192 ${String(minutes)} \u5206\u949F\u3002` : `Proactive notifications: **on** \xB7 events \`${value.events.join(",")}\` \xB7 target \`${value.target ?? "current"}\` \xB7 mentions \`${value.mentionUserIds.join(",") || "none"}\` \xB7 approval reminder ${String(minutes)} min.`;
8149
+ const sinks = value.sinks.join(",") || "none";
8150
+ return zh ? `\u4E3B\u52A8\u63D0\u9192\uFF1A**\u5F00\u542F** \xB7 \u4E8B\u4EF6 \`${value.events.join(",")}\` \xB7 \u76EE\u6807 \`${value.target ?? "current"}\` \xB7 @ \`${value.mentionUserIds.join(",") || "none"}\` \xB7 \u6E20\u9053 \`${sinks}\` \xB7 \u5BA1\u6279\u63D0\u9192 ${String(minutes)} \u5206\u949F\u3002` : `Proactive notifications: **on** \xB7 events \`${value.events.join(",")}\` \xB7 target \`${value.target ?? "current"}\` \xB7 mentions \`${value.mentionUserIds.join(",") || "none"}\` \xB7 channels \`${sinks}\` \xB7 approval reminder ${String(minutes)} min.`;
8132
8151
  }
8133
8152
  async function usage(ctx) {
8134
8153
  await reply5(
8135
8154
  ctx,
8136
- "\u7528\u6CD5\uFF1A`/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval] [mentions=self,ou_x|none] [remind=10]]`",
8137
- "Usage: `/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval] [mentions=self,ou_x|none] [remind=10]]`"
8155
+ "\u7528\u6CD5\uFF1A`/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval,urgent] [mentions=self,ou_x|none] [sinks=channelId1,channelId2] [remind=10]]`",
8156
+ "Usage: `/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval,urgent] [mentions=self,ou_x|none] [sinks=channelId1,channelId2] [remind=10]]`"
8157
+ );
8158
+ }
8159
+
8160
+ // src/notify/sinks/channel-store.ts
8161
+ import { readFile as readFile11 } from "fs/promises";
8162
+ var SINK_TYPES = ["telegram", "wecom"];
8163
+ var NotificationChannelStore = class {
8164
+ constructor(path) {
8165
+ this.path = path;
8166
+ }
8167
+ path;
8168
+ data = { schemaVersion: 1, channels: {} };
8169
+ saving = Promise.resolve();
8170
+ async load() {
8171
+ try {
8172
+ const parsed = JSON.parse(await readFile11(this.path, "utf8"));
8173
+ const channels = {};
8174
+ for (const [id, value] of Object.entries(parsed.channels ?? {})) {
8175
+ const normalized = normalizeChannel(id, value);
8176
+ if (normalized) channels[id] = normalized;
8177
+ }
8178
+ this.data = { schemaVersion: 1, channels };
8179
+ } catch (error) {
8180
+ if (error.code !== "ENOENT") throw error;
8181
+ this.data = { schemaVersion: 1, channels: {} };
8182
+ }
8183
+ }
8184
+ list() {
8185
+ return Object.values(this.data.channels).map((channel) => ({ ...channel }));
8186
+ }
8187
+ get(id) {
8188
+ const channel = this.data.channels[id];
8189
+ return channel ? { ...channel } : void 0;
8190
+ }
8191
+ async add(channel) {
8192
+ const normalized = normalizeChannel(channel.id, channel);
8193
+ if (!normalized) throw new Error("invalid notification channel");
8194
+ await this.mutate(() => {
8195
+ this.data.channels[normalized.id] = normalized;
8196
+ });
8197
+ }
8198
+ async update(id, patch) {
8199
+ const existing = this.data.channels[id];
8200
+ if (!existing) return false;
8201
+ const next = normalizeChannel(id, { ...existing, ...patch });
8202
+ if (!next) return false;
8203
+ await this.mutate(() => {
8204
+ this.data.channels[id] = next;
8205
+ });
8206
+ return true;
8207
+ }
8208
+ async setEnabled(id, enabled) {
8209
+ return this.update(id, { enabled });
8210
+ }
8211
+ async remove(id) {
8212
+ const existing = this.data.channels[id];
8213
+ if (!existing) return false;
8214
+ await this.mutate(() => {
8215
+ delete this.data.channels[id];
8216
+ });
8217
+ return true;
8218
+ }
8219
+ async flush() {
8220
+ await this.saving;
8221
+ }
8222
+ async mutate(fn) {
8223
+ const snapshot = JSON.stringify(this.data, null, 2) + "\n";
8224
+ const previous = JSON.stringify(this.data);
8225
+ fn();
8226
+ this.saving = this.saving.then(async () => {
8227
+ await writeFileAtomic(this.path, snapshot, { mode: 384 });
8228
+ }).catch((error) => {
8229
+ try {
8230
+ this.data = JSON.parse(previous);
8231
+ } catch {
8232
+ }
8233
+ log.fail("notification-channels", error, { step: "persist" });
8234
+ throw error;
8235
+ });
8236
+ await this.saving;
8237
+ }
8238
+ };
8239
+ function normalizeChannel(id, value) {
8240
+ if (!value || typeof value !== "object" || !id.trim()) return void 0;
8241
+ const input = value;
8242
+ const type = input.type;
8243
+ if (type !== "telegram" && type !== "wecom") return void 0;
8244
+ const label2 = typeof input.label === "string" ? input.label.trim() : "";
8245
+ const destination = typeof input.destination === "string" ? input.destination.trim() : "";
8246
+ const secret = typeof input.secret === "string" ? input.secret.trim() : "";
8247
+ if (!label2 || !destination || !secret) return void 0;
8248
+ const mentionMap = {};
8249
+ if (input.mentionMap && typeof input.mentionMap === "object") {
8250
+ for (const [from, to] of Object.entries(input.mentionMap)) {
8251
+ if (typeof to === "string" && to.trim()) mentionMap[from] = to.trim();
8252
+ }
8253
+ }
8254
+ return {
8255
+ id: id.trim(),
8256
+ type,
8257
+ label: label2,
8258
+ destination,
8259
+ secret,
8260
+ enabled: input.enabled !== false,
8261
+ ...Object.keys(mentionMap).length > 0 ? { mentionMap } : {}
8262
+ };
8263
+ }
8264
+
8265
+ // src/notify/sinks/types.ts
8266
+ var SINK_HTTP_TIMEOUT_MS = 1e4;
8267
+ function maskChannel(channel, includeId = true) {
8268
+ const id = includeId ? `${channel.id} \xB7 ` : "";
8269
+ const dest = maskSecret(channel.destination);
8270
+ return `${id}${channel.type} \xB7 ${channel.label} \u2192 ${dest}`;
8271
+ }
8272
+ function maskSecret(value) {
8273
+ if (!value) return "(unset)";
8274
+ if (value.length <= 6) return `${value[0]}{redacted}`;
8275
+ return `${value.slice(0, 3)}\u2026${value.slice(-3)}`;
8276
+ }
8277
+
8278
+ // src/commands/channels.ts
8279
+ async function reply6(ctx, zh, en) {
8280
+ await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
8281
+ }
8282
+ function requireAdmin3(ctx) {
8283
+ if (ctx.accessManager.isAdmin(ctx.senderId)) return true;
8284
+ void reply6(ctx, "\u4EC5\u7BA1\u7406\u5458\u53EF\u7BA1\u7406\u901A\u77E5\u6E20\u9053\u3002", "Only admins can manage notification channels.");
8285
+ return false;
8286
+ }
8287
+ async function handleChannels(args, ctx) {
8288
+ if (!requireAdmin3(ctx)) return;
8289
+ const store = ctx.notificationChannels;
8290
+ if (!store) {
8291
+ await reply6(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u901A\u77E5\u6E20\u9053\u7BA1\u7406\u3002", "Notification channel management is unavailable in this runtime.");
8292
+ return;
8293
+ }
8294
+ const [sub, ...rest] = args.trim().split(/\s+/);
8295
+ switch (sub) {
8296
+ case "list":
8297
+ return listChannels(ctx, store);
8298
+ case "show": {
8299
+ const id = rest[0];
8300
+ if (!id) return usage2(ctx);
8301
+ const channel = store.get(id);
8302
+ if (!channel) return notFound(ctx, id);
8303
+ return showChannel(ctx, channel);
8304
+ }
8305
+ case "add":
8306
+ return addChannel(ctx, store, rest.join(" "));
8307
+ case "remove": {
8308
+ const id = rest[0];
8309
+ if (!id) return usage2(ctx);
8310
+ const ok = await store.remove(id);
8311
+ return ok ? reply6(ctx, `\u5DF2\u79FB\u9664\u901A\u77E5\u6E20\u9053 \`${id}\`\u3002`, `Removed notification channel \`${id}\`.`) : notFound(ctx, id);
8312
+ }
8313
+ case "enable":
8314
+ case "disable": {
8315
+ const id = rest[0];
8316
+ if (!id) return usage2(ctx);
8317
+ const enabled = sub === "enable";
8318
+ const ok = await store.setEnabled(id, enabled);
8319
+ if (!ok) return notFound(ctx, id);
8320
+ return reply6(ctx, `\u5DF2${enabled ? "\u542F\u7528" : "\u505C\u7528"}\u901A\u77E5\u6E20\u9053 \`${id}\`\u3002`, `${enabled ? "Enabled" : "Disabled"} notification channel \`${id}\`.`);
8321
+ }
8322
+ default:
8323
+ return usage2(ctx);
8324
+ }
8325
+ }
8326
+ async function listChannels(ctx, store) {
8327
+ const channels = store.list();
8328
+ if (channels.length === 0) {
8329
+ await reply6(ctx, "\u5C1A\u672A\u914D\u7F6E\u4EFB\u4F55\u901A\u77E5\u6E20\u9053\u3002\u7528 `/channels add telegram|wecom <label> --destination <\u76EE\u6807> --secret <\u5BC6\u94A5>` \u6DFB\u52A0\u3002", "No notification channels yet. Use `/channels add telegram|wecom <label> --destination <target> --secret <token>` to add one.");
8330
+ return;
8331
+ }
8332
+ const lines = channels.map((channel) => `- ${channel.enabled ? "\u{1F7E2}" : "\u26AA\uFE0F"} \`${channel.id}\` \u2014 ${channel.type} \xB7 ${channel.label} \xB7 ${maskSecret(channel.destination)}`);
8333
+ await reply6(ctx, ["**\u5DF2\u914D\u7F6E\u901A\u77E5\u6E20\u9053**", "", ...lines].join("\n"), ["**Configured notification channels**", "", ...lines].join("\n"));
8334
+ }
8335
+ async function showChannel(ctx, channel) {
8336
+ const zh = `**${channel.id}**
8337
+ - \u7C7B\u578B\uFF1A\`${channel.type}\`
8338
+ - \u540D\u79F0\uFF1A${channel.label}
8339
+ - \u76EE\u6807\uFF1A\`${maskSecret(channel.destination)}\`
8340
+ - \u72B6\u6001\uFF1A${channel.enabled ? "\u542F\u7528" : "\u505C\u7528"}
8341
+ - \u5BC6\u94A5\uFF1A\`${maskSecret(channel.secret)}\``;
8342
+ const en = `**${channel.id}**
8343
+ - Type: \`${channel.type}\`
8344
+ - Label: ${channel.label}
8345
+ - Target: \`${maskSecret(channel.destination)}\`
8346
+ - Status: ${channel.enabled ? "enabled" : "disabled"}
8347
+ - Secret: \`${maskSecret(channel.secret)}\``;
8348
+ await reply6(ctx, zh, en);
8349
+ }
8350
+ async function addChannel(ctx, store, raw) {
8351
+ const parsed = parseAddArgs(raw);
8352
+ if (!parsed || !parsed.type || !parsed.label || !parsed.destination || !parsed.secret) {
8353
+ await usage2(ctx);
8354
+ return;
8355
+ }
8356
+ if (!SINK_TYPES.includes(parsed.type)) {
8357
+ await reply6(ctx, `\u4E0D\u652F\u6301\u7684\u6E20\u9053\u7C7B\u578B\uFF1A\`${parsed.type}\`\u3002\u53EF\u7528\uFF1A\`${SINK_TYPES.join("|")}\`\u3002`, `Unsupported channel type: \`${parsed.type}\`. Available: \`${SINK_TYPES.join("|")}\`.`);
8358
+ return;
8359
+ }
8360
+ const id = parsed.id ?? `${parsed.type}-${slugify(parsed.label)}`;
8361
+ if (store.get(id)) {
8362
+ await reply6(ctx, `\u901A\u77E5\u6E20\u9053 \`${id}\` \u5DF2\u5B58\u5728\u3002\u7528 \`/channels remove ${id}\` \u5220\u9664\u540E\u91CD\u5EFA\u3002`, `Notification channel \`${id}\` already exists. Remove it first with \`/channels remove ${id}\`.`);
8363
+ return;
8364
+ }
8365
+ const channel = {
8366
+ id,
8367
+ type: parsed.type,
8368
+ label: parsed.label,
8369
+ destination: parsed.destination,
8370
+ secret: parsed.secret,
8371
+ enabled: true
8372
+ };
8373
+ await store.add(channel);
8374
+ await reply6(ctx, `\u5DF2\u6DFB\u52A0\u901A\u77E5\u6E20\u9053 ${maskChannel(channel)}\u3002`, `Added notification channel ${maskChannel(channel)}.`);
8375
+ }
8376
+ async function notFound(ctx, id) {
8377
+ await reply6(ctx, `\u672A\u627E\u5230\u901A\u77E5\u6E20\u9053 \`${id}\`\u3002\u7528 \`/channels list\` \u67E5\u770B\u3002`, `Unknown notification channel \`${id}\`. Use \`/channels list\`.`);
8378
+ }
8379
+ async function usage2(ctx) {
8380
+ await reply6(
8381
+ ctx,
8382
+ "\u7528\u6CD5\uFF1A`/channels list|show <id>|add <telegram|wecom> <label> [--id <id>] --destination <\u76EE\u6807> --secret <\u5BC6\u94A5>|remove <id>|enable <id>|disable <id>`",
8383
+ "Usage: `/channels list|show <id>|add <telegram|wecom> <label> [--id <id>] --destination <target> --secret <token>|remove <id>|enable <id>|disable <id>`"
8138
8384
  );
8139
8385
  }
8386
+ function parseAddArgs(raw) {
8387
+ if (!raw.trim()) return void 0;
8388
+ const idMatch = raw.match(/--id\s+(\S+)/);
8389
+ const destMatch = raw.match(/--destination\s+(\S+)/);
8390
+ const secretMatch = raw.match(/--secret\s+(\S+)/);
8391
+ const positionals = raw.replace(/\s*--id\s+\S+/, " ").replace(/\s*--destination\s+\S+/, " ").replace(/\s*--secret\s+\S+/, " ").trim().split(/\s+/).filter(Boolean);
8392
+ const [type, label2] = positionals;
8393
+ return {
8394
+ ...idMatch ? { id: idMatch[1] } : {},
8395
+ ...type ? { type } : {},
8396
+ ...label2 ? { label: label2 } : {},
8397
+ ...destMatch ? { destination: destMatch[1] } : {},
8398
+ ...secretMatch ? { secret: secretMatch[1] } : {}
8399
+ };
8400
+ }
8401
+ function slugify(value) {
8402
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "channel";
8403
+ }
8140
8404
 
8141
8405
  // src/commands/replies.ts
8142
8406
  async function handleReplies(args, ctx) {
8143
8407
  const store = ctx.replyPolicies;
8144
- if (!store) return reply6(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u56DE\u590D\u7B56\u7565\u3002", "Reply policies are unavailable in this runtime.");
8408
+ if (!store) return reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u56DE\u590D\u7B56\u7565\u3002", "Reply policies are unavailable in this runtime.");
8145
8409
  const tokens = args.trim().split(/\s+/).filter(Boolean);
8146
- if (tokens.length === 0 || tokens[0] === "show") return reply6(ctx, describe2(store.get(ctx.scope), store.isConfigured(ctx.scope), true), describe2(store.get(ctx.scope), store.isConfigured(ctx.scope), false));
8147
- if (!await canManageReplies(ctx)) return reply6(ctx, "\u4EC5 profile \u7BA1\u7406\u5458\u6216\u5F53\u524D\u7FA4\u7684\u7FA4\u4E3B/\u7FA4\u7BA1\u7406\u5458\u53EF\u4FEE\u6539\u56DE\u590D\u7B56\u7565\u3002", "Only profile admins or the current group owner/managers can change reply policies.");
8410
+ if (tokens.length === 0 || tokens[0] === "show") return reply7(ctx, describe2(store.get(ctx.scope), store.isConfigured(ctx.scope), true), describe2(store.get(ctx.scope), store.isConfigured(ctx.scope), false));
8411
+ if (!await canManageReplies(ctx)) return reply7(ctx, "\u4EC5 profile \u7BA1\u7406\u5458\u6216\u5F53\u524D\u7FA4\u7684\u7FA4\u4E3B/\u7FA4\u7BA1\u7406\u5458\u53EF\u4FEE\u6539\u56DE\u590D\u7B56\u7565\u3002", "Only profile admins or the current group owner/managers can change reply policies.");
8148
8412
  if (tokens[0] === "default") {
8149
8413
  await store.set(ctx.scope, void 0);
8150
- return reply6(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u5373\u65F6\u56DE\u590D\u4E0E\u4EC5 messageId \u53BB\u91CD\u3002", "Restored immediate replies and message-ID-only deduplication.");
8414
+ return reply7(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u5373\u65F6\u56DE\u590D\u4E0E\u4EC5 messageId \u53BB\u91CD\u3002", "Restored immediate replies and message-ID-only deduplication.");
8151
8415
  }
8152
- if (tokens[0] !== "set") return usage2(ctx);
8416
+ if (tokens[0] !== "set") return usage3(ctx);
8153
8417
  const policy = { ...store.get(ctx.scope) };
8154
8418
  for (const token of tokens.slice(1)) {
8155
8419
  const [key, raw, extra] = token.split("=");
8156
- if (!raw || extra !== void 0) return usage2(ctx);
8420
+ if (!raw || extra !== void 0) return usage3(ctx);
8157
8421
  const seconds = Number(raw);
8158
- if (!Number.isInteger(seconds)) return usage2(ctx);
8422
+ if (!Number.isInteger(seconds)) return usage3(ctx);
8159
8423
  if (key === "merge" && seconds >= 0 && seconds <= 300) policy.mergeWindowMs = seconds * 1e3;
8160
8424
  else if (key === "batch" && seconds >= 1 && seconds <= 20) policy.maxBatchSize = seconds;
8161
8425
  else if (key === "interval" && seconds >= 0 && seconds <= 3600) policy.minIntervalMs = seconds * 1e3;
8162
8426
  else if (key === "dedupe" && seconds >= 0 && seconds <= 3600) policy.dedupeWindowMs = seconds * 1e3;
8163
- else return usage2(ctx);
8427
+ else return usage3(ctx);
8164
8428
  }
8165
- if (tokens.length === 1) return usage2(ctx);
8429
+ if (tokens.length === 1) return usage3(ctx);
8166
8430
  await store.set(ctx.scope, policy);
8167
- await reply6(ctx, `\u5DF2\u8BBE\u7F6E\u3002${describe2(policy, true, true)}`, `Updated. ${describe2(policy, true, false)}`);
8431
+ await reply7(ctx, `\u5DF2\u8BBE\u7F6E\u3002${describe2(policy, true, true)}`, `Updated. ${describe2(policy, true, false)}`);
8168
8432
  }
8169
8433
  async function canManageReplies(ctx) {
8170
8434
  if (ctx.accessManager.isAdmin(ctx.senderId)) return true;
@@ -8178,10 +8442,10 @@ async function canManageReplies(ctx) {
8178
8442
  function describe2(policy, configured, zh) {
8179
8443
  return zh ? `\u56DE\u590D\u7B56\u7565\uFF1A**${configured ? "\u81EA\u5B9A\u4E49" : "\u9ED8\u8BA4"}** \xB7 \u5408\u5E76 ${String(policy.mergeWindowMs / 1e3)} \u79D2 \xB7 \u6BCF\u6279 ${String(policy.maxBatchSize)} \u4E2A\u4EFB\u52A1 \xB7 \u95F4\u9694 ${String(policy.minIntervalMs / 1e3)} \u79D2 \xB7 \u8FD1\u4F3C\u53BB\u91CD ${String(policy.dedupeWindowMs / 1e3)} \u79D2\u3002` : `Reply policy: **${configured ? "custom" : "default"}** \xB7 merge ${String(policy.mergeWindowMs / 1e3)}s \xB7 ${String(policy.maxBatchSize)} tasks/batch \xB7 interval ${String(policy.minIntervalMs / 1e3)}s \xB7 near-dedupe ${String(policy.dedupeWindowMs / 1e3)}s.`;
8180
8444
  }
8181
- async function usage2(ctx) {
8182
- await reply6(ctx, "\u7528\u6CD5\uFF1A`/replies [show|default|set merge=\u79D2 batch=\u6570\u91CF interval=\u79D2 dedupe=\u79D2]`", "Usage: `/replies [show|default|set merge=seconds batch=count interval=seconds dedupe=seconds]`");
8445
+ async function usage3(ctx) {
8446
+ await reply7(ctx, "\u7528\u6CD5\uFF1A`/replies [show|default|set merge=\u79D2 batch=\u6570\u91CF interval=\u79D2 dedupe=\u79D2]`", "Usage: `/replies [show|default|set merge=seconds batch=count interval=seconds dedupe=seconds]`");
8183
8447
  }
8184
- async function reply6(ctx, zh, en) {
8448
+ async function reply7(ctx, zh, en) {
8185
8449
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
8186
8450
  }
8187
8451
 
@@ -8416,7 +8680,8 @@ var COMMAND_CATALOG = [
8416
8680
  { names: ["/isolation"], area: "policy", zh: "`[group|topic|member]` \u2014 \u7FA4\u4F1A\u8BDD\u9694\u79BB\uFF08\u8BBE\u7F6E\u9700\u7BA1\u7406\u5458\uFF09", en: "`[group|topic|member]` \u2014 group isolation (admin to set)" },
8417
8681
  { names: ["/role"], area: "configuration", zh: "`list|show|set|clear|save|remove \u2026` \u2014 \u7BA1\u7406\u89D2\u8272", en: "`list|show|set|clear|save|remove \u2026` \u2014 manage roles" },
8418
8682
  { names: ["/notify"], area: "communication", zh: "`<scope|chatId> <text>` / `list` \u2014 \u8DE8\u4F1A\u8BDD\u901A\u77E5\uFF08\u53D1\u9001\u9700\u7BA1\u7406\u5458\uFF09", en: "`<scope|chatId> <text>` / `list` \u2014 cross-session notification (admin to send)" },
8419
- { names: ["/notifications"], area: "communication", zh: "`[show|off|on \u2026]` \u2014 scope \u63D0\u9192\u7B56\u7565", en: "`[show|off|on \u2026]` \u2014 scope notification policy" },
8683
+ { names: ["/notifications"], area: "communication", zh: "`[show|off|on \u2026]` \u2014 scope \u63D0\u9192\u7B56\u7565\uFF08\u652F\u6301 `sinks=`\uFF09", en: "`[show|off|on \u2026]` \u2014 scope notification policy (supports `sinks=`)" },
8684
+ { names: ["/channels"], area: "configuration", zh: "`list|show|add|remove|enable|disable \u2026` \u2014 \u7BA1\u7406\u51FA\u7AD9\u901A\u77E5\u6E20\u9053\uFF08\u7BA1\u7406\u5458\uFF09", en: "`list|show|add|remove|enable|disable \u2026` \u2014 manage outbound notification channels (admin)" },
8420
8685
  { names: ["/replies"], area: "communication", zh: "`[show|default|set \u2026]` \u2014 \u56DE\u590D\u5408\u5E76\u3001\u9891\u7387\u4E0E\u53BB\u91CD", en: "`[show|default|set \u2026]` \u2014 reply batching, rate, and deduplication" },
8421
8686
  { names: ["/retention"], area: "session", zh: "`[N|default]` \u2014 \u4F1A\u8BDD\u4FDD\u7559\u6761\u6570", en: "`[N|default]` \u2014 retained live-message count" },
8422
8687
  { names: ["/archive"], area: "session", zh: "`[note]|send|list|clean \u2026` \u2014 \u7BA1\u7406\u5F52\u6863", en: "`[note]|send|list|clean \u2026` \u2014 manage archives" },
@@ -8499,6 +8764,7 @@ var HELP = [
8499
8764
  "- `/notify <scope|chatId> <text>` \u2014 \u8DE8\u4F1A\u8BDD\u53D1\u9001\u901A\u77E5\uFF08\u7BA1\u7406\u5458\uFF09",
8500
8765
  "- `/notify list` \u2014 \u67E5\u770B\u5DF2\u6CE8\u518C scope",
8501
8766
  "- `/notifications [show|off|on \u2026]` \u2014 \u914D\u7F6E\u5F53\u524D scope \u7684\u5B8C\u6210 / \u5931\u8D25 / \u5BA1\u6279\u63D0\u9192",
8767
+ "- `/channels [list|show|add|remove|enable|disable \u2026]` \u2014 \u7BA1\u7406\u51FA\u7AD9\u901A\u77E5\u6E20\u9053\uFF08\u7BA1\u7406\u5458\uFF09\uFF0C\u8F6C\u53D1\u5230\u5FAE\u4FE1 / Telegram \u7B49",
8502
8768
  "- `/replies [show|default|set \u2026]` \u2014 \u914D\u7F6E\u56DE\u590D\u5408\u5E76\u3001\u9891\u7387\u4E0E\u8FD1\u4F3C\u53BB\u91CD\uFF08profile \u7BA1\u7406\u5458\u6216\u5F53\u524D\u7FA4\u7BA1\u7406\u5458\u53EF\u4FEE\u6539\uFF09",
8503
8769
  "- `/retention [N|default]` \u2014 \u67E5\u770B\u6216\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u4FDD\u7559\u6D88\u606F\u6761\u6570\uFF08\u8D85\u51FA\u81EA\u52A8\u5F52\u6863\uFF09",
8504
8770
  "- `/archive [note]`\u3001`/archive send <id> [scope|chatId]`\u3001`/archive list [N]`\u3001`/archive clean` \u2014 \u5F52\u6863\u5E76\u4E0A\u4F20 / \u91CD\u53D1\u6216\u8F6C\u53D1 / \u67E5\u770B / \u6E05\u7406",
@@ -8560,7 +8826,7 @@ var HELP_EN = [
8560
8826
  "- `/invite user|admin|group <id>` \u2014 manage access allowlists",
8561
8827
  "- `/help` \u2014 show this help"
8562
8828
  ].join("\n");
8563
- async function reply7(ctx, zhCn, enUs) {
8829
+ async function reply8(ctx, zhCn, enUs) {
8564
8830
  await ctx.channel.sendMarkdown(ctx.chatId, enUs === void 0 ? zhCn : bilingualMarkdown(zhCn, enUs, ctx.languagePolicies?.get().plain), {
8565
8831
  replyTo: ctx.messageId
8566
8832
  });
@@ -8569,7 +8835,7 @@ async function handleNew(_args, ctx) {
8569
8835
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8570
8836
  const interrupted = await ctx.activeRuns.interruptWorkspace(ctx.scope, cwd);
8571
8837
  ctx.sessions.clear(ctx.scope, cwd);
8572
- await reply7(
8838
+ await reply8(
8573
8839
  ctx,
8574
8840
  interrupted > 0 ? `\u5DF2\u4E2D\u65AD ${String(interrupted)} \u4E2A\u4EFB\u52A1\u5E76\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002` : "\u5DF2\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002",
8575
8841
  interrupted > 0 ? `Interrupted ${String(interrupted)} task(s) and started a new session.` : "Started a new session."
@@ -8598,11 +8864,11 @@ function groupAppLink(chatId) {
8598
8864
  async function handleNewGroup(args, ctx) {
8599
8865
  const name2 = args.trim();
8600
8866
  if (!name2) {
8601
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/newg <\u7FA4\u540D>` \u2014 \u81EA\u52A8\u65B0\u5EFA\u7FA4\u804A\u5E76\u5F00\u59CB\u65B0\u4F1A\u8BDD", "Usage: `/newg <name>` \u2014 create a group and start a new session");
8867
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/newg <\u7FA4\u540D>` \u2014 \u81EA\u52A8\u65B0\u5EFA\u7FA4\u804A\u5E76\u5F00\u59CB\u65B0\u4F1A\u8BDD", "Usage: `/newg <name>` \u2014 create a group and start a new session");
8602
8868
  return;
8603
8869
  }
8604
8870
  if (name2.length > MAX_GROUP_NAME_LENGTH) {
8605
- await reply7(
8871
+ await reply8(
8606
8872
  ctx,
8607
8873
  `\u7FA4\u540D\u8FC7\u957F\uFF08\u4E0A\u9650 ${String(MAX_GROUP_NAME_LENGTH)} \u5B57\u7B26\uFF0C\u5F53\u524D ${String(name2.length)}\uFF09\u3002`,
8608
8874
  `The group name is too long (limit ${String(MAX_GROUP_NAME_LENGTH)} characters; received ${String(name2.length)}).`
@@ -8610,11 +8876,11 @@ async function handleNewGroup(args, ctx) {
8610
8876
  return;
8611
8877
  }
8612
8878
  if (!ctx.channel.createChat) {
8613
- await reply7(ctx, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u81EA\u52A8\u5EFA\u7FA4\u3002", "This channel cannot create groups automatically.");
8879
+ await reply8(ctx, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u81EA\u52A8\u5EFA\u7FA4\u3002", "This channel cannot create groups automatically.");
8614
8880
  return;
8615
8881
  }
8616
8882
  if (!ctx.senderId) {
8617
- await reply7(ctx, "\u65E0\u6CD5\u8BC6\u522B\u53D1\u9001\u8005 open_id\uFF0C\u4E0D\u80FD\u81EA\u52A8\u5EFA\u7FA4\u3002", "The sender open_id is unavailable, so the group cannot be created automatically.");
8883
+ await reply8(ctx, "\u65E0\u6CD5\u8BC6\u522B\u53D1\u9001\u8005 open_id\uFF0C\u4E0D\u80FD\u81EA\u52A8\u5EFA\u7FA4\u3002", "The sender open_id is unavailable, so the group cannot be created automatically.");
8618
8884
  return;
8619
8885
  }
8620
8886
  try {
@@ -8625,7 +8891,7 @@ async function handleNewGroup(args, ctx) {
8625
8891
  inviteUserIds: [ctx.senderId],
8626
8892
  userIdType: "open_id"
8627
8893
  });
8628
- await reply7(
8894
+ await reply8(
8629
8895
  ctx,
8630
8896
  [
8631
8897
  `\u2705 \u5DF2\u521B\u5EFA\u7FA4\u804A\uFF1A**${name2}**`,
@@ -8644,13 +8910,13 @@ async function handleNewGroup(args, ctx) {
8644
8910
  );
8645
8911
  } catch (error) {
8646
8912
  const message = error instanceof Error ? error.message : String(error);
8647
- await reply7(ctx, `\u274C \u5EFA\u7FA4\u5931\u8D25\uFF1A\`${message}\``, `\u274C Failed to create the group: \`${message}\``);
8913
+ await reply8(ctx, `\u274C \u5EFA\u7FA4\u5931\u8D25\uFF1A\`${message}\``, `\u274C Failed to create the group: \`${message}\``);
8648
8914
  }
8649
8915
  }
8650
8916
  async function handleCd(args, ctx) {
8651
8917
  const path = args.trim();
8652
8918
  if (!path) {
8653
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`", "Usage: `/cd <path>`");
8919
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`", "Usage: `/cd <path>`");
8654
8920
  return;
8655
8921
  }
8656
8922
  const normalized = path.replace(/^~/u, "~");
@@ -8658,13 +8924,13 @@ async function handleCd(args, ctx) {
8658
8924
  const cwd = resolve3(expanded);
8659
8925
  const target = await stat2(cwd).catch(() => void 0);
8660
8926
  if (!target?.isDirectory()) {
8661
- await reply7(ctx, `\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A\`${cwd}\`\u3002`, `The path does not exist or is not a directory: \`${cwd}\`.`);
8927
+ await reply8(ctx, `\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u662F\u76EE\u5F55\uFF1A\`${cwd}\`\u3002`, `The path does not exist or is not a directory: \`${cwd}\`.`);
8662
8928
  return;
8663
8929
  }
8664
8930
  const previous = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8665
8931
  const interrupted = cwd === previous ? 0 : await ctx.activeRuns.interruptWorkspace(ctx.scope, previous);
8666
8932
  ctx.workspaces.setCwd(ctx.scope, cwd);
8667
- await reply7(
8933
+ await reply8(
8668
8934
  ctx,
8669
8935
  `\u5DF2\u5207\u6362\u5DE5\u4F5C\u76EE\u5F55\uFF1A\`${cwd}\`\uFF1B\u8BE5\u5DE5\u4F5C\u533A\u7684\u4F1A\u8BDD\u4F1A\u72EC\u7ACB\u6062\u590D\u3002${interrupted > 0 ? `\u5DF2\u4E2D\u65AD\u539F\u5DE5\u4F5C\u533A ${String(interrupted)} \u4E2A\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08\u4F1A\u8BDD\u6570\u636E\u4FDD\u7559\uFF09\u3002` : ""}`,
8670
8936
  `Switched workspace to \`${cwd}\`; its independent session will resume.${interrupted > 0 ? ` Interrupted ${String(interrupted)} running task(s) in the previous workspace (session data was preserved).` : ""}`
@@ -8684,7 +8950,7 @@ async function handleWs(args, ctx) {
8684
8950
  const lines = Object.entries(named).map(
8685
8951
  ([key, value]) => `- **${key}** \u2192 \`${value}\`${value === current ? " \u2190 \u5F53\u524D" : ""}`
8686
8952
  );
8687
- await reply7(
8953
+ await reply8(
8688
8954
  ctx,
8689
8955
  [
8690
8956
  `\u5F53\u524D cwd\uFF1A\`${current}\``,
@@ -8701,29 +8967,29 @@ async function handleWs(args, ctx) {
8701
8967
  }
8702
8968
  if (sub === "save") {
8703
8969
  if (!name2) {
8704
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/ws save <name>`", "Usage: `/ws save <name>`");
8970
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/ws save <name>`", "Usage: `/ws save <name>`");
8705
8971
  return;
8706
8972
  }
8707
8973
  const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8708
8974
  ctx.workspaces.saveNamed(name2, current);
8709
- await reply7(ctx, `\u5DF2\u4FDD\u5B58\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}** \u2192 \`${current}\``, `Saved workspace: **${name2}** \u2192 \`${current}\``);
8975
+ await reply8(ctx, `\u5DF2\u4FDD\u5B58\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}** \u2192 \`${current}\``, `Saved workspace: **${name2}** \u2192 \`${current}\``);
8710
8976
  return;
8711
8977
  }
8712
8978
  if (sub === "use") {
8713
8979
  if (!name2) {
8714
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/ws use <name>`", "Usage: `/ws use <name>`");
8980
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/ws use <name>`", "Usage: `/ws use <name>`");
8715
8981
  return;
8716
8982
  }
8717
8983
  const cwd = ctx.workspaces.getNamed(name2);
8718
8984
  if (!cwd) {
8719
- await reply7(ctx, `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}**`, `Workspace not found: **${name2}**`);
8985
+ await reply8(ctx, `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}**`, `Workspace not found: **${name2}**`);
8720
8986
  return;
8721
8987
  }
8722
8988
  const previous = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8723
8989
  const interrupted = cwd === previous ? 0 : await ctx.activeRuns.interruptWorkspace(ctx.scope, previous);
8724
8990
  ctx.workspaces.setCwd(ctx.scope, cwd);
8725
8991
  ctx.workspaces.touchNamed(name2);
8726
- await reply7(
8992
+ await reply8(
8727
8993
  ctx,
8728
8994
  `\u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}** \u2192 \`${cwd}\`\uFF1B\u8BE5\u5DE5\u4F5C\u533A\u7684\u4F1A\u8BDD\u4F1A\u72EC\u7ACB\u6062\u590D\u3002${interrupted > 0 ? `\u5DF2\u4E2D\u65AD\u539F\u5DE5\u4F5C\u533A ${String(interrupted)} \u4E2A\u8FD0\u884C\u4E2D\u4EFB\u52A1\uFF08\u4F1A\u8BDD\u6570\u636E\u4FDD\u7559\uFF09\u3002` : ""}`,
8729
8995
  `Switched to workspace **${name2}** \u2192 \`${cwd}\`; its independent session will resume.${interrupted > 0 ? ` Interrupted ${String(interrupted)} running task(s) in the previous workspace (session data was preserved).` : ""}`
@@ -8732,14 +8998,14 @@ async function handleWs(args, ctx) {
8732
8998
  }
8733
8999
  if (sub === "remove") {
8734
9000
  if (!name2) {
8735
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/ws remove <name>`", "Usage: `/ws remove <name>`");
9001
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/ws remove <name>`", "Usage: `/ws remove <name>`");
8736
9002
  return;
8737
9003
  }
8738
9004
  const removed = ctx.workspaces.removeNamed(name2);
8739
- await reply7(ctx, removed ? `\u5DF2\u5220\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}**` : `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}**`, removed ? `Removed workspace: **${name2}**` : `Workspace not found: **${name2}**`);
9005
+ await reply8(ctx, removed ? `\u5DF2\u5220\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}**` : `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name2}**`, removed ? `Removed workspace: **${name2}**` : `Workspace not found: **${name2}**`);
8740
9006
  return;
8741
9007
  }
8742
- await reply7(ctx, "\u672A\u77E5 `/ws` \u5B50\u547D\u4EE4\uFF0C\u8BF7\u4F7F\u7528 list / save / use / remove\u3002", "Unknown `/ws` subcommand. Use list / save / use / remove.");
9008
+ await reply8(ctx, "\u672A\u77E5 `/ws` \u5B50\u547D\u4EE4\uFF0C\u8BF7\u4F7F\u7528 list / save / use / remove\u3002", "Unknown `/ws` subcommand. Use list / save / use / remove.");
8743
9009
  }
8744
9010
  async function statusCardInputFor(ctx) {
8745
9011
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -8786,6 +9052,7 @@ async function statusCardInputFor(ctx) {
8786
9052
  permissionPolicy: ctx.permissionPolicies?.get(ctx.scope) ?? "ask",
8787
9053
  executionMode: ctx.executionModes?.get(ctx.scope) ?? "balanced",
8788
9054
  ...notificationPreference ? { notificationPreference } : {},
9055
+ ...ctx.notificationSinks ? { notificationChannels: ctx.notificationSinks.enabledChannels().map((channel) => channel.id) } : {},
8789
9056
  ...replyPolicy ? { replyPolicy, replyPolicyConfigured: ctx.replyPolicies?.isConfigured(ctx.scope) ?? false } : {},
8790
9057
  role: role ? `\`${role.id}\` (${role.name})` : void 0,
8791
9058
  metrics,
@@ -8828,16 +9095,16 @@ async function handleStatus(_args, ctx) {
8828
9095
  } catch {
8829
9096
  }
8830
9097
  }
8831
- await reply7(ctx, bilingualMarkdown(statusCardMarkdown(input), statusCardMarkdownEnglish(input)));
9098
+ await reply8(ctx, bilingualMarkdown(statusCardMarkdown(input), statusCardMarkdownEnglish(input)));
8832
9099
  }
8833
9100
  async function handleIsolation(args, ctx) {
8834
9101
  if (ctx.chatMode === "p2p") {
8835
- await reply7(ctx, "\u5F53\u524D\u662F\u79C1\u804A\uFF0Cscope \u5DF2\u6309\u4F1A\u8BDD\u5929\u7136\u9694\u79BB\uFF0C\u65E0\u9700\u8BBE\u7F6E\u3002", "This is a direct chat; its scope is already isolated, so no setting is needed.");
9102
+ await reply8(ctx, "\u5F53\u524D\u662F\u79C1\u804A\uFF0Cscope \u5DF2\u6309\u4F1A\u8BDD\u5929\u7136\u9694\u79BB\uFF0C\u65E0\u9700\u8BBE\u7F6E\u3002", "This is a direct chat; its scope is already isolated, so no setting is needed.");
8836
9103
  return;
8837
9104
  }
8838
9105
  const mode = args.trim().toLowerCase();
8839
9106
  if (!mode) {
8840
- await reply7(
9107
+ await reply8(
8841
9108
  ctx,
8842
9109
  `\u5F53\u524D\u7FA4\u4F1A\u8BDD\u9694\u79BB\u6A21\u5F0F\uFF1A**${ctx.isolationStore?.get(ctx.chatId) ?? "topic"}**\u3002\u53EF\u7528 \`/isolation group|topic|member\` \u5207\u6362\u3002`,
8843
9110
  `Current group isolation mode: **${ctx.isolationStore?.get(ctx.chatId) ?? "topic"}**. Use \`/isolation group|topic|member\` to switch.`
@@ -8845,16 +9112,16 @@ async function handleIsolation(args, ctx) {
8845
9112
  return;
8846
9113
  }
8847
9114
  if (mode !== "group" && mode !== "topic" && mode !== "member") {
8848
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/isolation group|topic|member`", "Usage: `/isolation group|topic|member`");
9115
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/isolation group|topic|member`", "Usage: `/isolation group|topic|member`");
8849
9116
  return;
8850
9117
  }
8851
- if (!requireAdmin3(ctx)) return;
9118
+ if (!requireAdmin4(ctx)) return;
8852
9119
  if (!ctx.isolationStore) {
8853
- await reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u7FA4\u804A\u9694\u79BB\u7B56\u7565\u5B58\u50A8\u3002", "Group isolation policy storage is not enabled in this runtime.");
9120
+ await reply8(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u7FA4\u804A\u9694\u79BB\u7B56\u7565\u5B58\u50A8\u3002", "Group isolation policy storage is not enabled in this runtime.");
8854
9121
  return;
8855
9122
  }
8856
9123
  ctx.isolationStore.set(ctx.chatId, mode);
8857
- await reply7(
9124
+ await reply8(
8858
9125
  ctx,
8859
9126
  `\u5DF2\u5C06\u672C\u7FA4\u9694\u79BB\u6A21\u5F0F\u8BBE\u4E3A **${mode}**\uFF0C\u4ECE\u4E0B\u4E00\u6761\u6D88\u606F\u8D77\u751F\u6548\u3002\u5DF2\u6709 group / topic / member scope \u4E0E\u4F1A\u8BDD\u6570\u636E\u5747\u4FDD\u7559\uFF0C\u5207\u56DE\u540E\u53EF\u7EE7\u7EED\u4F7F\u7528\u3002`,
8860
9127
  `Set this group\u2019s isolation mode to **${mode}**. It takes effect on the next message; existing group/topic/member scopes and session data are preserved.`
@@ -8881,25 +9148,25 @@ async function handleVersion(_args, ctx) {
8881
9148
  lines.push("\u6700\u65B0\u7248\u672C\u67E5\u8BE2\u6682\u4E0D\u53EF\u7528\uFF08\u7F51\u7EDC / registry \u5F02\u5E38\uFF09\u3002");
8882
9149
  english.push("Latest-version lookup is unavailable (network or registry error).");
8883
9150
  }
8884
- await reply7(ctx, lines.join("\n"), english.join("\n"));
9151
+ await reply8(ctx, lines.join("\n"), english.join("\n"));
8885
9152
  }
8886
9153
  async function handleUpgrade(_args, ctx) {
8887
- if (!requireAdmin3(ctx)) return;
9154
+ if (!requireAdmin4(ctx)) return;
8888
9155
  if (!ctx.senderId || !ctx.channelUpdates) {
8889
- await reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u65B9\u5F0F\u4E0D\u652F\u6301\u98DE\u4E66\u5185\u66F4\u65B0\u3002", "In-chat updates are unavailable in this runtime.");
9156
+ await reply8(ctx, "\u5F53\u524D\u8FD0\u884C\u65B9\u5F0F\u4E0D\u652F\u6301\u98DE\u4E66\u5185\u66F4\u65B0\u3002", "In-chat updates are unavailable in this runtime.");
8890
9157
  return;
8891
9158
  }
8892
9159
  const result = await ctx.channelUpdates.check({ scope: ctx.scope, actorId: ctx.senderId });
8893
9160
  if (result.kind === "unavailable") {
8894
- await reply7(ctx, "\u6682\u65F6\u65E0\u6CD5\u67E5\u8BE2 npm \u6700\u65B0\u7248\u672C\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", "The latest npm version is temporarily unavailable. Try again later.");
9161
+ await reply8(ctx, "\u6682\u65F6\u65E0\u6CD5\u67E5\u8BE2 npm \u6700\u65B0\u7248\u672C\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002", "The latest npm version is temporarily unavailable. Try again later.");
8895
9162
  return;
8896
9163
  }
8897
9164
  if (result.kind === "current") {
8898
- await reply7(ctx, `\u2705 \u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF1A\`${result.current}\`\u3002`, `\u2705 Already up to date: \`${result.current}\`.`);
9165
+ await reply8(ctx, `\u2705 \u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF1A\`${result.current}\`\u3002`, `\u2705 Already up to date: \`${result.current}\`.`);
8899
9166
  return;
8900
9167
  }
8901
9168
  if (!ctx.channel.sendCard) {
8902
- await reply7(ctx, "\u5F53\u524D\u6E20\u9053\u65E0\u6CD5\u53D1\u9001\u66F4\u65B0\u786E\u8BA4\u5361\uFF0C\u672A\u6267\u884C\u4EFB\u4F55\u66F4\u65B0\u3002", "This channel cannot send an update confirmation card. Nothing was changed.");
9169
+ await reply8(ctx, "\u5F53\u524D\u6E20\u9053\u65E0\u6CD5\u53D1\u9001\u66F4\u65B0\u786E\u8BA4\u5361\uFF0C\u672A\u6267\u884C\u4EFB\u4F55\u66F4\u65B0\u3002", "This channel cannot send an update confirmation card. Nothing was changed.");
8903
9170
  return;
8904
9171
  }
8905
9172
  await ctx.channel.sendCard(ctx.chatId, renderUpdateCard({
@@ -8914,9 +9181,9 @@ async function handleUpgrade(_args, ctx) {
8914
9181
  });
8915
9182
  }
8916
9183
  async function handleDoctor(_args, ctx) {
8917
- if (!requireAdmin3(ctx)) return;
9184
+ if (!requireAdmin4(ctx)) return;
8918
9185
  if (!ctx.createDiagnosticBundle || !ctx.channel.sendFile) {
8919
- await reply7(ctx, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u751F\u6210\u6216\u53D1\u9001\u8BCA\u65AD\u6587\u4EF6\u3002", "Diagnostic generation or file delivery is unavailable in this channel.");
9186
+ await reply8(ctx, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u751F\u6210\u6216\u53D1\u9001\u8BCA\u65AD\u6587\u4EF6\u3002", "Diagnostic generation or file delivery is unavailable in this channel.");
8920
9187
  return;
8921
9188
  }
8922
9189
  let file;
@@ -8933,7 +9200,7 @@ async function handleDoctor(_args, ctx) {
8933
9200
  ...status.jobs ? { jobs: status.jobs } : {}
8934
9201
  }), ctx.diagnosticTimeoutMs?.generate ?? DOCTOR_GENERATE_TIMEOUT_MS);
8935
9202
  } catch {
8936
- await reply7(
9203
+ await reply8(
8937
9204
  ctx,
8938
9205
  "\u26A0\uFE0F \u8BCA\u65AD\u5305\u751F\u6210\u5931\u8D25\u3002\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u82E5\u6301\u7EED\u5931\u8D25\uFF0C\u8BF7\u5728\u672C\u673A\u8FD0\u884C `dsh-lark-bot doctor`\u3002",
8939
9206
  "\u26A0\uFE0F Failed to generate the diagnostic bundle. Retry later; if it keeps failing, run `dsh-lark-bot doctor` locally."
@@ -8947,21 +9214,21 @@ async function handleDoctor(_args, ctx) {
8947
9214
  }), ctx.diagnosticTimeoutMs?.upload ?? DOCTOR_UPLOAD_TIMEOUT_MS);
8948
9215
  } catch (error) {
8949
9216
  if (error instanceof OperationTimeoutError) {
8950
- await reply7(
9217
+ await reply8(
8951
9218
  ctx,
8952
9219
  "\u26A0\uFE0F \u8BCA\u65AD\u5305\u4E0A\u4F20\u7B49\u5F85\u8D85\u65F6\uFF0C\u7ED3\u679C\u672A\u77E5\uFF1A\u6587\u4EF6\u4ECD\u53EF\u80FD\u7A0D\u540E\u5230\u8FBE\u3002\u8BF7\u5148\u68C0\u67E5\u5F53\u524D\u804A\u5929\uFF0C\u4E0D\u8981\u7ACB\u5373\u91CD\u8BD5\u3002",
8953
9220
  "\u26A0\uFE0F Diagnostic upload timed out with an unknown result: the file may still arrive later. Check this chat before retrying."
8954
9221
  );
8955
9222
  return;
8956
9223
  }
8957
- await reply7(
9224
+ await reply8(
8958
9225
  ctx,
8959
9226
  "\u26A0\uFE0F \u8BCA\u65AD\u5305\u53D1\u9001\u5931\u8D25\u3002\u8BF7\u7A0D\u540E\u91CD\u8BD5\uFF1B\u82E5\u6301\u7EED\u5931\u8D25\uFF0C\u8BF7\u5728\u672C\u673A\u8FD0\u884C `dsh-lark-bot doctor`\u3002",
8960
9227
  "\u26A0\uFE0F Failed to send the diagnostic bundle. Retry later; if it keeps failing, run `dsh-lark-bot doctor` locally."
8961
9228
  );
8962
9229
  return;
8963
9230
  }
8964
- await reply7(
9231
+ await reply8(
8965
9232
  ctx,
8966
9233
  "\u2705 \u8131\u654F\u8BCA\u65AD\u5305\u5DF2\u751F\u6210\u5E76\u53D1\u9001\uFF0C\u53EF\u76F4\u63A5\u4E0B\u8F7D\u8F6C\u53D1\u7ED9\u7EF4\u62A4\u8005\u3002",
8967
9234
  "\u2705 The redacted diagnostic bundle was generated and sent. You can download and forward it to a maintainer."
@@ -8971,18 +9238,18 @@ async function handleResume(_args, ctx) {
8971
9238
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8972
9239
  const history = ctx.sessions.historyFor(ctx.scope, cwd);
8973
9240
  if (history.length === 0) {
8974
- await reply7(ctx, "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u5386\u53F2\u4E0A\u4E0B\u6587\u3002", "This session has no conversation history.");
9241
+ await reply8(ctx, "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u5386\u53F2\u4E0A\u4E0B\u6587\u3002", "This session has no conversation history.");
8975
9242
  return;
8976
9243
  }
8977
9244
  const recent = history.slice(-6).map((message) => {
8978
9245
  const speaker = message.role === "user" ? "\u{1F464}" : "\u{1F916}";
8979
9246
  return `${speaker} ${message.content.slice(0, 300)}`;
8980
9247
  });
8981
- await reply7(ctx, [`\u5F53\u524D scope\uFF1A\`${ctx.scope}\``, "", ...recent].join("\n"), [`Current scope: \`${ctx.scope}\``, "", ...recent].join("\n"));
9248
+ await reply8(ctx, [`\u5F53\u524D scope\uFF1A\`${ctx.scope}\``, "", ...recent].join("\n"), [`Current scope: \`${ctx.scope}\``, "", ...recent].join("\n"));
8982
9249
  }
8983
9250
  async function handleJobs(args, ctx) {
8984
9251
  if (!ctx.jobs) {
8985
- await reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u6301\u4E45\u4EFB\u52A1\u8D26\u672C\u3002", "The durable job ledger is not enabled in this runtime.");
9252
+ await reply8(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u6301\u4E45\u4EFB\u52A1\u8D26\u672C\u3002", "The durable job ledger is not enabled in this runtime.");
8986
9253
  return;
8987
9254
  }
8988
9255
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -8990,11 +9257,11 @@ async function handleJobs(args, ctx) {
8990
9257
  const records = ctx.jobs.list(ctx.scope, cwd, 20);
8991
9258
  if (sub === "retry") {
8992
9259
  if (!id) {
8993
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/jobs retry <\u6D88\u606FID>`", "Usage: `/jobs retry <message-id>`");
9260
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/jobs retry <\u6D88\u606FID>`", "Usage: `/jobs retry <message-id>`");
8994
9261
  return;
8995
9262
  }
8996
9263
  const queued = await ctx.requeueJob?.(id, ctx.scope, cwd);
8997
- await reply7(
9264
+ await reply8(
8998
9265
  ctx,
8999
9266
  queued ? `\u267B\uFE0F \u5DF2\u5C06\u4EFB\u52A1 **${id}** \u91CD\u65B0\u5165\u961F\u3002\u91CD\u590D\u6267\u884C\u53EF\u80FD\u91CD\u590D\u5916\u90E8\u526F\u4F5C\u7528\uFF0C\u8BF7\u7559\u610F\u539F\u4EFB\u52A1 checkpoint\u3002` : `\u65E0\u6CD5\u91CD\u8BD5 **${id}**\uFF1A\u53EA\u5141\u8BB8\u91CD\u8BD5\u5F53\u524D workspace \u5185\u7684 failed / interrupted \u4EFB\u52A1\u3002`,
9000
9267
  queued ? `\u267B\uFE0F Requeued job **${id}**. Retrying may repeat external side effects; review the original checkpoint.` : `Cannot retry **${id}**: only failed or interrupted jobs in the current workspace are eligible.`
@@ -9004,19 +9271,19 @@ async function handleJobs(args, ctx) {
9004
9271
  if (sub === "show") {
9005
9272
  const record = id ? ctx.jobs.get(id, ctx.scope, cwd) : void 0;
9006
9273
  if (!record) {
9007
- await reply7(ctx, `\u5F53\u524D workspace \u672A\u627E\u5230\u4EFB\u52A1\uFF1A**${id ?? ""}**`, `Job not found in this workspace: **${id ?? ""}**`);
9274
+ await reply8(ctx, `\u5F53\u524D workspace \u672A\u627E\u5230\u4EFB\u52A1\uFF1A**${id ?? ""}**`, `Job not found in this workspace: **${id ?? ""}**`);
9008
9275
  return;
9009
9276
  }
9010
- await reply7(ctx, jobDetail(record, "zh"), jobDetail(record, "en"));
9277
+ await reply8(ctx, jobDetail(record, "zh"), jobDetail(record, "en"));
9011
9278
  return;
9012
9279
  }
9013
9280
  if (sub && sub !== "list") {
9014
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/jobs [list|show <\u6D88\u606FID>|retry <\u6D88\u606FID>]`", "Usage: `/jobs [list|show <message-id>|retry <message-id>]`");
9281
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/jobs [list|show <\u6D88\u606FID>|retry <\u6D88\u606FID>]`", "Usage: `/jobs [list|show <message-id>|retry <message-id>]`");
9015
9282
  return;
9016
9283
  }
9017
9284
  const counts = ctx.jobs.counts(ctx.scope, cwd);
9018
9285
  const rows = records.slice(0, 10);
9019
- await reply7(
9286
+ await reply8(
9020
9287
  ctx,
9021
9288
  [
9022
9289
  "**\u4EFB\u52A1\u5BF9\u8D26**",
@@ -9063,7 +9330,7 @@ async function handleStop(_args, ctx) {
9063
9330
  });
9064
9331
  let stopped = 0;
9065
9332
  for (const scope of scopes) stopped += await ctx.activeRuns.interrupt(scope);
9066
- await reply7(
9333
+ await reply8(
9067
9334
  ctx,
9068
9335
  stopped > 0 ? `\u5DF2\u8BF7\u6C42\u7EC8\u6B62\u5F53\u524D\u53CA\u5207\u6362\u524D\u9694\u79BB scope \u7684\u5168\u90E8 ${String(stopped)} \u4E2A\u4EFB\u52A1\u3002` : "\u5F53\u524D\u53CA\u5207\u6362\u524D\u9694\u79BB scope \u5747\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u4EFB\u52A1\u3002",
9069
9336
  stopped > 0 ? `Requested termination of all ${String(stopped)} task(s) in the current and previously selected isolation scopes.` : "No tasks are running in the current or previously selected isolation scopes."
@@ -9074,7 +9341,7 @@ async function handleTimeout(args, ctx) {
9074
9341
  const effectiveMs = ctx.runPolicies.get(ctx.scope) ?? ctx.defaultRunTimeoutMs;
9075
9342
  if (!input) {
9076
9343
  const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
9077
- await reply7(
9344
+ await reply8(
9078
9345
  ctx,
9079
9346
  minutes2 > 0 ? `\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\uFF1A\u6301\u7EED\u65E0\u6D3B\u52A8\u4E8B\u4EF6 ${minutes2} \u5206\u949F\u624D\u7EC8\u6B62\u3002\u53EF\u7528 \`/timeout <N|off|default>\` \u8C03\u6574\u3002` : "\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\uFF1A\u5173\u95ED\u3002",
9080
9347
  minutes2 > 0 ? `Session idle timeout: stop after ${minutes2} minutes without activity events. Use \`/timeout <N|off|default>\` to change it.` : "Session idle timeout: off."
@@ -9083,27 +9350,27 @@ async function handleTimeout(args, ctx) {
9083
9350
  }
9084
9351
  if (input === "off") {
9085
9352
  ctx.runPolicies.set(ctx.scope, 0);
9086
- await reply7(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\u3002", "Disabled the session idle timeout.");
9353
+ await reply8(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\u3002", "Disabled the session idle timeout.");
9087
9354
  return;
9088
9355
  }
9089
9356
  if (input === "default") {
9090
9357
  ctx.runPolicies.clear(ctx.scope);
9091
- await reply7(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u7A7A\u95F2\u8D85\u65F6\u3002", "Restored the default idle timeout.");
9358
+ await reply8(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u7A7A\u95F2\u8D85\u65F6\u3002", "Restored the default idle timeout.");
9092
9359
  return;
9093
9360
  }
9094
9361
  const minutes = Number(input);
9095
9362
  if (!Number.isInteger(minutes) || minutes <= 0) {
9096
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/timeout <N|off|default>`\uFF0CN \u4E3A\u5927\u4E8E 0 \u7684\u5206\u949F\u6570\u3002", "Usage: `/timeout <N|off|default>`, where N is a positive number of minutes.");
9363
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/timeout <N|off|default>`\uFF0CN \u4E3A\u5927\u4E8E 0 \u7684\u5206\u949F\u6570\u3002", "Usage: `/timeout <N|off|default>`, where N is a positive number of minutes.");
9097
9364
  return;
9098
9365
  }
9099
9366
  ctx.runPolicies.set(ctx.scope, minutes * 6e4);
9100
- await reply7(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\uFF1A\u6301\u7EED\u65E0\u6D3B\u52A8\u4E8B\u4EF6 ${minutes} \u5206\u949F\u624D\u7EC8\u6B62\u3002`, `Set the session idle timeout to ${minutes} minutes without activity events.`);
9367
+ await reply8(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\uFF1A\u6301\u7EED\u65E0\u6D3B\u52A8\u4E8B\u4EF6 ${minutes} \u5206\u949F\u624D\u7EC8\u6B62\u3002`, `Set the session idle timeout to ${minutes} minutes without activity events.`);
9101
9368
  }
9102
9369
  async function handleConcurrency(args, ctx) {
9103
9370
  const input = args.trim();
9104
9371
  const effective = ctx.concurrencyStore.get(ctx.scope) ?? ctx.defaultScopeConcurrency;
9105
9372
  if (!input) {
9106
- await reply7(
9373
+ await reply8(
9107
9374
  ctx,
9108
9375
  `\u5F53\u524D scope \u7684\u5E76\u884C\u4EFB\u52A1\u6570\uFF1A**${String(effective)}**\u3002\u53EF\u7528 \`/concurrency <N|default>\` \u8C03\u6574\uFF08N \u2265 1\uFF09\u3002`,
9109
9376
  `Parallel tasks for this scope: **${String(effective)}**. Use \`/concurrency <N|default>\` to change it (N \u2265 1).`
@@ -9112,16 +9379,16 @@ async function handleConcurrency(args, ctx) {
9112
9379
  }
9113
9380
  if (input === "default") {
9114
9381
  ctx.concurrencyStore.clear(ctx.scope);
9115
- await reply7(ctx, `\u5DF2\u6062\u590D\u9ED8\u8BA4\u5E76\u884C\u4EFB\u52A1\u6570\uFF08${String(ctx.defaultScopeConcurrency)}\uFF09\u3002`, `Restored the default parallel-task limit (${String(ctx.defaultScopeConcurrency)}).`);
9382
+ await reply8(ctx, `\u5DF2\u6062\u590D\u9ED8\u8BA4\u5E76\u884C\u4EFB\u52A1\u6570\uFF08${String(ctx.defaultScopeConcurrency)}\uFF09\u3002`, `Restored the default parallel-task limit (${String(ctx.defaultScopeConcurrency)}).`);
9116
9383
  return;
9117
9384
  }
9118
9385
  const n = Number(input);
9119
9386
  if (!Number.isInteger(n) || n < 1) {
9120
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/concurrency <N|default>`\uFF0CN \u4E3A\u5927\u4E8E\u7B49\u4E8E 1 \u7684\u6574\u6570\u3002", "Usage: `/concurrency <N|default>`, where N is an integer of at least 1.");
9387
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/concurrency <N|default>`\uFF0CN \u4E3A\u5927\u4E8E\u7B49\u4E8E 1 \u7684\u6574\u6570\u3002", "Usage: `/concurrency <N|default>`, where N is an integer of at least 1.");
9121
9388
  return;
9122
9389
  }
9123
9390
  ctx.concurrencyStore.set(ctx.scope, n);
9124
- await reply7(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D scope \u7684\u5E76\u884C\u4EFB\u52A1\u6570\uFF1A**${String(n)}**\u3002`, `Set parallel tasks for this scope to **${String(n)}**.`);
9391
+ await reply8(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D scope \u7684\u5E76\u884C\u4EFB\u52A1\u6570\uFF1A**${String(n)}**\u3002`, `Set parallel tasks for this scope to **${String(n)}**.`);
9125
9392
  }
9126
9393
  async function handlePermission(args, ctx) {
9127
9394
  const [rawPolicy = "", rawTarget = "", ...extra] = args.trim().split(/\s+/);
@@ -9133,7 +9400,7 @@ async function handlePermission(args, ctx) {
9133
9400
  };
9134
9401
  if (!input) {
9135
9402
  const current = ctx.permissionPolicies?.get(ctx.scope) ?? "ask";
9136
- await reply7(
9403
+ await reply8(
9137
9404
  ctx,
9138
9405
  `\u5F53\u524D scope \u7684\u5DE5\u5177\u6743\u9650\u7B56\u7565\uFF1A**${labels[current].zh}**\uFF08\`${current}\`\uFF09\u3002\u53EF\u7528 \`/permission ask|allow|deny\` \u8C03\u6574\uFF08\u4EC5\u7BA1\u7406\u5458\uFF09\u3002`,
9139
9406
  `Tool permission policy for this scope: **${labels[current].en}** (\`${current}\`). Use \`/permission ask|allow|deny\` to change it (admin only).`
@@ -9141,21 +9408,21 @@ async function handlePermission(args, ctx) {
9141
9408
  return;
9142
9409
  }
9143
9410
  if (input !== "ask" && input !== "allow" && input !== "deny" || extra.length > 0) {
9144
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/permission [ask|allow|deny] [scope]`", "Usage: `/permission [ask|allow|deny] [scope]`");
9411
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/permission [ask|allow|deny] [scope]`", "Usage: `/permission [ask|allow|deny] [scope]`");
9145
9412
  return;
9146
9413
  }
9147
- if (!requireAdmin3(ctx)) return;
9414
+ if (!requireAdmin4(ctx)) return;
9148
9415
  if (!ctx.permissionPolicies) {
9149
- await reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u5DE5\u5177\u6743\u9650\u7B56\u7565\u5B58\u50A8\u3002", "Tool permission policy storage is not enabled in this runtime.");
9416
+ await reply8(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u5DE5\u5177\u6743\u9650\u7B56\u7565\u5B58\u50A8\u3002", "Tool permission policy storage is not enabled in this runtime.");
9150
9417
  return;
9151
9418
  }
9152
9419
  const targetScope = rawTarget || ctx.scope;
9153
9420
  if (targetScope !== ctx.chatId && !targetScope.startsWith(`${ctx.chatId}:`)) {
9154
- await reply7(ctx, "\u53EA\u80FD\u4FEE\u6539\u5F53\u524D\u804A\u5929\u5185\u7684 scope\u3002\u8BF7\u4ECE\u76EE\u6807\u4F1A\u8BDD `/status` \u590D\u5236\u5B8C\u6574 scope\u3002", "You can only modify a scope in the current chat. Copy the full scope from the target session\u2019s `/status`.");
9421
+ await reply8(ctx, "\u53EA\u80FD\u4FEE\u6539\u5F53\u524D\u804A\u5929\u5185\u7684 scope\u3002\u8BF7\u4ECE\u76EE\u6807\u4F1A\u8BDD `/status` \u590D\u5236\u5B8C\u6574 scope\u3002", "You can only modify a scope in the current chat. Copy the full scope from the target session\u2019s `/status`.");
9155
9422
  return;
9156
9423
  }
9157
9424
  await ctx.permissionPolicies.set(targetScope, input);
9158
- await reply7(
9425
+ await reply8(
9159
9426
  ctx,
9160
9427
  `\u5DF2\u5C06 scope \`${targetScope}\` \u7684\u5DE5\u5177\u6743\u9650\u7B56\u7565\u8BBE\u4E3A **${labels[input].zh}**\uFF08\`${input}\`\uFF09\uFF0C\u4ECE\u4E0B\u4E00\u6B21\u5DE5\u5177\u5BA1\u6279\u8D77\u751F\u6548\u3002\u8BA1\u5212\u5BA1\u6279\u4ECD\u4F1A\u7167\u5E38\u6267\u884C\u3002`,
9161
9428
  `Set scope \`${targetScope}\`'s tool permission policy to **${labels[input].en}** (\`${input}\`). It applies to the next tool approval; plan approval remains enforced.`
@@ -9165,7 +9432,7 @@ async function handleDensity(args, ctx) {
9165
9432
  const input = args.trim().toLowerCase();
9166
9433
  if (!input) {
9167
9434
  const current = ctx.densityStore?.get(ctx.scope) ?? "standard";
9168
- await reply7(
9435
+ await reply8(
9169
9436
  ctx,
9170
9437
  `\u5F53\u524D\u5361\u7247\u5BC6\u5EA6\uFF1A**${current}**\u3002\u53EF\u7528 \`/density compact|standard|detailed\` \u8C03\u6574\u3002`,
9171
9438
  `Current card density: **${current}**. Use \`/density compact|standard|detailed\` to change it.`
@@ -9174,21 +9441,21 @@ async function handleDensity(args, ctx) {
9174
9441
  }
9175
9442
  if (input === "default") {
9176
9443
  ctx.densityStore?.clear(ctx.scope);
9177
- await reply7(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u5361\u7247\u5BC6\u5EA6\u3002", "Restored the default card density.");
9444
+ await reply8(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u5361\u7247\u5BC6\u5EA6\u3002", "Restored the default card density.");
9178
9445
  return;
9179
9446
  }
9180
9447
  const density = parseCardDensity(input);
9181
9448
  if (!density) {
9182
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/density [compact|standard|detailed|default]`", "Usage: `/density [compact|standard|detailed|default]`");
9449
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/density [compact|standard|detailed|default]`", "Usage: `/density [compact|standard|detailed|default]`");
9183
9450
  return;
9184
9451
  }
9185
9452
  ctx.densityStore?.set(ctx.scope, density);
9186
- await reply7(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u5361\u7247\u5BC6\u5EA6\uFF1A**${density}**\u3002`, `Set this session\u2019s card density to **${density}**.`);
9453
+ await reply8(ctx, `\u5DF2\u8BBE\u7F6E\u5F53\u524D\u4F1A\u8BDD\u5361\u7247\u5BC6\u5EA6\uFF1A**${density}**\u3002`, `Set this session\u2019s card density to **${density}**.`);
9187
9454
  }
9188
9455
  async function handleExecutionMode(args, ctx) {
9189
9456
  const input = args.trim().toLowerCase();
9190
9457
  if (!ctx.executionModes) {
9191
- await reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u6267\u884C\u6A21\u5F0F\u5B58\u50A8\u3002", "Execution mode storage is unavailable in this runtime.");
9458
+ await reply8(ctx, "\u5F53\u524D\u8FD0\u884C\u73AF\u5883\u672A\u542F\u7528\u6267\u884C\u6A21\u5F0F\u5B58\u50A8\u3002", "Execution mode storage is unavailable in this runtime.");
9192
9459
  return;
9193
9460
  }
9194
9461
  if (!input) {
@@ -9205,7 +9472,7 @@ async function handleExecutionMode(args, ctx) {
9205
9472
  } catch {
9206
9473
  }
9207
9474
  }
9208
- await reply7(
9475
+ await reply8(
9209
9476
  ctx,
9210
9477
  `\u5F53\u524D\u6267\u884C\u6A21\u5F0F\uFF1A**${current}**\u3002\u53EF\u7528 \`/mode quick|balanced|deep\` \u5207\u6362\uFF1B\u4E0B\u4E00\u8F6E\u751F\u6548\u3002`,
9211
9478
  `Current execution mode: **${current}**. Use \`/mode quick|balanced|deep\`; changes apply next turn.`
@@ -9213,18 +9480,18 @@ async function handleExecutionMode(args, ctx) {
9213
9480
  return;
9214
9481
  }
9215
9482
  if (input !== "quick" && input !== "balanced" && input !== "deep") {
9216
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/mode [quick|balanced|deep]`", "Usage: `/mode [quick|balanced|deep]`");
9483
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/mode [quick|balanced|deep]`", "Usage: `/mode [quick|balanced|deep]`");
9217
9484
  return;
9218
9485
  }
9219
9486
  await ctx.executionModes.set(ctx.scope, input);
9220
- await reply7(
9487
+ await reply8(
9221
9488
  ctx,
9222
9489
  `\u5DF2\u5C06\u5F53\u524D\u4F1A\u8BDD\u6267\u884C\u6A21\u5F0F\u8BBE\u4E3A **${input}**\uFF0C\u4ECE\u4E0B\u4E00\u8F6E\u751F\u6548\uFF1B\u6B63\u5728\u8FD0\u884C\u7684\u4EFB\u52A1\u4E0E\u73B0\u6709\u4E0A\u4E0B\u6587\u4E0D\u53D7\u5F71\u54CD\u3002`,
9223
9490
  `Set this session\u2019s execution mode to **${input}** for the next turn. Active work and existing context are unchanged.`
9224
9491
  );
9225
9492
  }
9226
9493
  async function handleEffortAlias(args, ctx) {
9227
- await reply7(
9494
+ await reply8(
9228
9495
  ctx,
9229
9496
  "`/effort` \u662F `/mode` \u7684\u517C\u5BB9\u522B\u540D\uFF0C\u53EA\u63A7\u5236\u4EFB\u52A1\u6267\u884C\u5F3A\u5EA6\uFF1B\u6A21\u578B\u63A8\u7406\u6863\u4F4D\u7531\u6A21\u578B\u9002\u914D\u5668\u51B3\u5B9A\u3002",
9230
9497
  "`/effort` is a compatibility alias for `/mode`; it controls task execution intensity, not the model reasoning-effort setting."
@@ -9234,11 +9501,11 @@ async function handleEffortAlias(args, ctx) {
9234
9501
  async function handleAsk(args, ctx) {
9235
9502
  const question = args.trim();
9236
9503
  if (!question) {
9237
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/ask <\u95EE\u9898>`", "Usage: `/ask <question>`");
9504
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/ask <\u95EE\u9898>`", "Usage: `/ask <question>`");
9238
9505
  return;
9239
9506
  }
9240
9507
  if (!ctx.questions) {
9241
- await reply7(ctx, "\u95EE\u7B54\u5361\u672A\u542F\u7528\uFF08\u8BF7\u786E\u8BA4 questions \u5DF2\u63A5\u7EBF\uFF09\u3002", "Question cards are disabled (check the questions integration).");
9508
+ await reply8(ctx, "\u95EE\u7B54\u5361\u672A\u542F\u7528\uFF08\u8BF7\u786E\u8BA4 questions \u5DF2\u63A5\u7EBF\uFF09\u3002", "Question cards are disabled (check the questions integration).");
9242
9509
  return;
9243
9510
  }
9244
9511
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -9260,18 +9527,18 @@ async function handleAsk(args, ctx) {
9260
9527
  if (answer !== void 0) {
9261
9528
  const text = Array.isArray(answer) ? answer.join("\u3001") : answer;
9262
9529
  ctx.sessions.recordExchange(ctx.scope, cwd, [text], void 0);
9263
- await reply7(ctx, "\u5DF2\u8BB0\u5F55\u4F60\u7684\u56DE\u7B54\uFF0C\u5E76\u5199\u5165\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002", "Recorded your answer in the session context.");
9530
+ await reply8(ctx, "\u5DF2\u8BB0\u5F55\u4F60\u7684\u56DE\u7B54\uFF0C\u5E76\u5199\u5165\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002", "Recorded your answer in the session context.");
9264
9531
  } else {
9265
- await reply7(ctx, "\u672A\u6536\u5230\u56DE\u7B54\uFF08\u5361\u7247\u53EF\u80FD\u5DF2\u8D85\u65F6\u6216\u88AB\u5FFD\u7565\uFF09\u3002", "No answer was received (the card may have expired or been ignored).");
9532
+ await reply8(ctx, "\u672A\u6536\u5230\u56DE\u7B54\uFF08\u5361\u7247\u53EF\u80FD\u5DF2\u8D85\u65F6\u6216\u88AB\u5FFD\u7565\uFF09\u3002", "No answer was received (the card may have expired or been ignored).");
9266
9533
  }
9267
9534
  }
9268
9535
  async function handleInvite(args, ctx) {
9269
9536
  const [kind, ...rest] = args.trim().split(/\s+/);
9270
9537
  const id = rest.join(" ").trim();
9271
- if (kind !== "list" && !requireAdmin3(ctx)) return;
9538
+ if (kind !== "list" && !requireAdmin4(ctx)) return;
9272
9539
  if (kind === "list") {
9273
9540
  const snapshot = ctx.accessManager.snapshot();
9274
- await reply7(
9541
+ await reply8(
9275
9542
  ctx,
9276
9543
  [
9277
9544
  "**\u8BBF\u95EE\u767D\u540D\u5355**",
@@ -9289,7 +9556,7 @@ async function handleInvite(args, ctx) {
9289
9556
  return;
9290
9557
  }
9291
9558
  if (!kind || !id) {
9292
- await reply7(
9559
+ await reply8(
9293
9560
  ctx,
9294
9561
  "\u7528\u6CD5\uFF1A`/invite user|admin|group <id>`\u3001`/invite list`\u3001`/invite remove user|group <id>`",
9295
9562
  "Usage: `/invite user|admin|group <id>`, `/invite list`, or `/invite remove user|group <id>`"
@@ -9298,39 +9565,39 @@ async function handleInvite(args, ctx) {
9298
9565
  }
9299
9566
  if (kind === "user") {
9300
9567
  await ctx.accessManager.addUser(id);
9301
- await reply7(ctx, `\u5DF2\u5141\u8BB8\u7528\u6237\uFF1A\`${id}\``, `Allowed user: \`${id}\``);
9568
+ await reply8(ctx, `\u5DF2\u5141\u8BB8\u7528\u6237\uFF1A\`${id}\``, `Allowed user: \`${id}\``);
9302
9569
  return;
9303
9570
  }
9304
9571
  if (kind === "admin") {
9305
9572
  await ctx.accessManager.addAdmin(id);
9306
- await reply7(ctx, `\u5DF2\u8BBE\u4E3A\u7BA1\u7406\u5458\uFF1A\`${id}\``, `Granted admin access to: \`${id}\``);
9573
+ await reply8(ctx, `\u5DF2\u8BBE\u4E3A\u7BA1\u7406\u5458\uFF1A\`${id}\``, `Granted admin access to: \`${id}\``);
9307
9574
  return;
9308
9575
  }
9309
9576
  if (kind === "group") {
9310
9577
  await ctx.accessManager.addChat(id);
9311
- await reply7(ctx, `\u5DF2\u5141\u8BB8\u7FA4\u804A\uFF1A\`${id}\``, `Allowed group chat: \`${id}\``);
9578
+ await reply8(ctx, `\u5DF2\u5141\u8BB8\u7FA4\u804A\uFF1A\`${id}\``, `Allowed group chat: \`${id}\``);
9312
9579
  return;
9313
9580
  }
9314
9581
  if (kind === "remove") {
9315
9582
  const [sub, target] = rest;
9316
9583
  if (sub === "user" && target) {
9317
9584
  await ctx.accessManager.removeUser(target);
9318
- await reply7(ctx, `\u5DF2\u79FB\u9664\u7528\u6237\uFF1A\`${target}\``, `Removed user: \`${target}\``);
9585
+ await reply8(ctx, `\u5DF2\u79FB\u9664\u7528\u6237\uFF1A\`${target}\``, `Removed user: \`${target}\``);
9319
9586
  return;
9320
9587
  }
9321
9588
  if (sub === "group" && target) {
9322
9589
  await ctx.accessManager.removeChat(target);
9323
- await reply7(ctx, `\u5DF2\u79FB\u9664\u7FA4\u804A\uFF1A\`${target}\``, `Removed group chat: \`${target}\``);
9590
+ await reply8(ctx, `\u5DF2\u79FB\u9664\u7FA4\u804A\uFF1A\`${target}\``, `Removed group chat: \`${target}\``);
9324
9591
  return;
9325
9592
  }
9326
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/invite remove user <id>` \u6216 `/invite remove group <chatId>`", "Usage: `/invite remove user <id>` or `/invite remove group <chatId>`");
9593
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/invite remove user <id>` \u6216 `/invite remove group <chatId>`", "Usage: `/invite remove user <id>` or `/invite remove group <chatId>`");
9327
9594
  return;
9328
9595
  }
9329
- await reply7(ctx, "\u672A\u77E5 `/invite` \u7C7B\u578B\uFF0C\u8BF7\u4F7F\u7528 user / admin / group / list / remove\u3002", "Unknown `/invite` type. Use user / admin / group / list / remove.");
9596
+ await reply8(ctx, "\u672A\u77E5 `/invite` \u7C7B\u578B\uFF0C\u8BF7\u4F7F\u7528 user / admin / group / list / remove\u3002", "Unknown `/invite` type. Use user / admin / group / list / remove.");
9330
9597
  }
9331
- function requireAdmin3(ctx) {
9598
+ function requireAdmin4(ctx) {
9332
9599
  if (!ctx.accessManager.isAdmin(ctx.senderId)) {
9333
- void reply7(ctx, "\u4EC5\u7BA1\u7406\u5458\u53EF\u6267\u884C\u8BE5\u64CD\u4F5C\u3002", "Only admins can perform this operation.");
9600
+ void reply8(ctx, "\u4EC5\u7BA1\u7406\u5458\u53EF\u6267\u884C\u8BE5\u64CD\u4F5C\u3002", "Only admins can perform this operation.");
9334
9601
  return false;
9335
9602
  }
9336
9603
  return true;
@@ -9383,20 +9650,20 @@ async function handleKeyDispatch(args, ctx) {
9383
9650
  await handleKey(args, ctx);
9384
9651
  }
9385
9652
  async function handleHelp(_args, ctx) {
9386
- await reply7(ctx, renderCommandHelp("zh"), renderCommandHelp("en"));
9653
+ await reply8(ctx, renderCommandHelp("zh"), renderCommandHelp("en"));
9387
9654
  }
9388
9655
  async function handleLanguage(args, ctx) {
9389
9656
  if (!ctx.languagePolicies) {
9390
- await reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u65F6\u672A\u542F\u7528\u8BED\u8A00\u7B56\u7565\u3002", "Language policy is unavailable in this runtime.");
9657
+ await reply8(ctx, "\u5F53\u524D\u8FD0\u884C\u65F6\u672A\u542F\u7528\u8BED\u8A00\u7B56\u7565\u3002", "Language policy is unavailable in this runtime.");
9391
9658
  return;
9392
9659
  }
9393
9660
  const [action = "show", field, value] = args.trim().split(/\s+/);
9394
9661
  if (action === "show") {
9395
9662
  const policy = ctx.languagePolicies.get();
9396
- await reply7(ctx, `\u8BED\u8A00\u7B56\u7565\uFF1AUI=\u6309\u67E5\u770B\u8005\uFF1B\u666E\u901A\u6587\u672C=${policy.plain}\uFF1BAgent=${policy.agent}`, `Language policy: UI=per viewer; plain=${policy.plain}; agent=${policy.agent}`);
9663
+ await reply8(ctx, `\u8BED\u8A00\u7B56\u7565\uFF1AUI=\u6309\u67E5\u770B\u8005\uFF1B\u666E\u901A\u6587\u672C=${policy.plain}\uFF1BAgent=${policy.agent}`, `Language policy: UI=per viewer; plain=${policy.plain}; agent=${policy.agent}`);
9397
9664
  return;
9398
9665
  }
9399
- if (!requireAdmin3(ctx)) return;
9666
+ if (!requireAdmin4(ctx)) return;
9400
9667
  if (action === "set" && field === "plain" && (value === "bilingual" || value === "zh" || value === "en")) {
9401
9668
  await ctx.languagePolicies.set({ plain: value });
9402
9669
  } else if (action === "set" && field === "agent" && (value === "auto" || value === "zh" || value === "en")) {
@@ -9404,7 +9671,7 @@ async function handleLanguage(args, ctx) {
9404
9671
  } else if (action === "reset" && (field === void 0 || field === "all" || field === "plain" || field === "agent")) {
9405
9672
  await ctx.languagePolicies.reset(field ?? "all");
9406
9673
  } else {
9407
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/language show|set plain <bilingual|zh|en>|set agent <auto|zh|en>|reset [plain|agent|all]`", "Usage: `/language show|set plain <bilingual|zh|en>|set agent <auto|zh|en>|reset [plain|agent|all]`");
9674
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/language show|set plain <bilingual|zh|en>|set agent <auto|zh|en>|reset [plain|agent|all]`", "Usage: `/language show|set plain <bilingual|zh|en>|set agent <auto|zh|en>|reset [plain|agent|all]`");
9408
9675
  return;
9409
9676
  }
9410
9677
  process.env.DSH_LARK_REPLY_LANG = ctx.languagePolicies.get().plain;
@@ -9415,37 +9682,37 @@ function parseSecretTarget(value) {
9415
9682
  }
9416
9683
  async function handleSecret(args, ctx) {
9417
9684
  if (!ctx.secretService) {
9418
- await reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u65F6\u672A\u542F\u7528\u5B89\u5168\u5BC6\u94A5\u91C7\u96C6\u3002", "Secure secret collection is unavailable in this runtime.");
9685
+ await reply8(ctx, "\u5F53\u524D\u8FD0\u884C\u65F6\u672A\u542F\u7528\u5B89\u5168\u5BC6\u94A5\u91C7\u96C6\u3002", "Secure secret collection is unavailable in this runtime.");
9419
9686
  return;
9420
9687
  }
9421
9688
  const [action, targetRaw, referenceRaw, ...purposeParts] = args.trim().split(/\s+/);
9422
9689
  const target = parseSecretTarget(targetRaw);
9423
9690
  const reference = referenceRaw?.trim();
9424
9691
  if (!target || !reference || !["status", "set", "remove"].includes(action ?? "")) {
9425
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/secret status|set|remove <dsh-credential|app-secret> <\u5F15\u7528>`", "Usage: `/secret status|set|remove <dsh-credential|app-secret> <reference>`");
9692
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/secret status|set|remove <dsh-credential|app-secret> <\u5F15\u7528>`", "Usage: `/secret status|set|remove <dsh-credential|app-secret> <reference>`");
9426
9693
  return;
9427
9694
  }
9428
- if (action !== "status" && !requireAdmin3(ctx)) return;
9695
+ if (action !== "status" && !requireAdmin4(ctx)) return;
9429
9696
  if (action === "status") {
9430
9697
  const configured = await ctx.secretService.configured(target, reference);
9431
- await reply7(ctx, `${target} \`${reference}\`\uFF1A${configured ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`, `${target} \`${reference}\`: ${configured ? "configured" : "not configured"}`);
9698
+ await reply8(ctx, `${target} \`${reference}\`\uFF1A${configured ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`, `${target} \`${reference}\`: ${configured ? "configured" : "not configured"}`);
9432
9699
  return;
9433
9700
  }
9434
9701
  if (action === "remove") {
9435
9702
  const removed = await ctx.secretService.remove(target, reference);
9436
- await reply7(ctx, removed ? "\u2705 \u5BC6\u94A5\u5DF2\u5220\u9664\u3002" : "\u8BE5\u5F15\u7528\u672A\u914D\u7F6E\u5BC6\u94A5\u3002", removed ? "\u2705 Secret removed." : "No secret is configured for that reference.");
9703
+ await reply8(ctx, removed ? "\u2705 \u5BC6\u94A5\u5DF2\u5220\u9664\u3002" : "\u8BE5\u5F15\u7528\u672A\u914D\u7F6E\u5BC6\u94A5\u3002", removed ? "\u2705 Secret removed." : "No secret is configured for that reference.");
9437
9704
  return;
9438
9705
  }
9439
9706
  if (!ctx.senderId) {
9440
- await reply7(ctx, "\u65E0\u6CD5\u8BC6\u522B\u64CD\u4F5C\u8005\uFF0C\u4E0D\u80FD\u6253\u5F00\u5B89\u5168\u8868\u5355\u3002", "The operator cannot be identified, so the secure form cannot be opened.");
9707
+ await reply8(ctx, "\u65E0\u6CD5\u8BC6\u522B\u64CD\u4F5C\u8005\uFF0C\u4E0D\u80FD\u6253\u5F00\u5B89\u5168\u8868\u5355\u3002", "The operator cannot be identified, so the secure form cannot be opened.");
9441
9708
  return;
9442
9709
  }
9443
9710
  await ctx.secretService.request({ scope: ctx.scope, chatId: ctx.chatId, ...ctx.threadId ? { threadId: ctx.threadId } : {}, ownerId: ctx.senderId, target, reference, purpose: purposeParts.join(" ") || "Configure a protected value" });
9444
- await reply7(ctx, "\u5DF2\u53D1\u9001\u4EC5\u9650\u4F60\u63D0\u4EA4\u7684\u5B89\u5168\u8868\u5355\uFF1B\u8BF7\u52FF\u5728\u666E\u901A\u6D88\u606F\u4E2D\u7C98\u8D34\u5BC6\u94A5\u3002", "A secure owner-only form was sent. Do not paste the secret into ordinary chat.");
9711
+ await reply8(ctx, "\u5DF2\u53D1\u9001\u4EC5\u9650\u4F60\u63D0\u4EA4\u7684\u5B89\u5168\u8868\u5355\uFF1B\u8BF7\u52FF\u5728\u666E\u901A\u6D88\u606F\u4E2D\u7C98\u8D34\u5BC6\u94A5\u3002", "A secure owner-only form was sent. Do not paste the secret into ordinary chat.");
9445
9712
  }
9446
9713
  async function handleSessionProjection(args, ctx) {
9447
9714
  if (!ctx.sessionProjection) {
9448
- await reply7(
9715
+ await reply8(
9449
9716
  ctx,
9450
9717
  "\u5F53\u524D adapter \u672A\u63D0\u4F9B DSH session \u6295\u5F71\uFF1B\u8BF7\u4F7F\u7528 `web` adapter \u5E76\u786E\u8BA4 Web host \u53EF\u7528\u3002",
9451
9718
  "The current adapter does not provide DSH session projection; use the `web` adapter and ensure the Web host is reachable."
@@ -9475,6 +9742,7 @@ var handlers = {
9475
9742
  "/role": handleRole,
9476
9743
  "/notify": handleNotify,
9477
9744
  "/notifications": handleNotifications,
9745
+ "/channels": handleChannels,
9478
9746
  "/replies": handleReplies,
9479
9747
  "/retention": handleRetention,
9480
9748
  "/archive": handleArchive,
@@ -9954,24 +10222,26 @@ var GroupMessagePoller = class {
9954
10222
 
9955
10223
  // src/bridge/reconnect-notifier.ts
9956
10224
  var ReconnectNotifier = class {
9957
- constructor(channel, directory, now = Date.now, reconciliation) {
10225
+ constructor(channel, directory, now = Date.now, reconciliation, onFault) {
9958
10226
  this.channel = channel;
9959
10227
  this.directory = directory;
9960
10228
  this.now = now;
9961
10229
  this.reconciliation = reconciliation;
10230
+ this.onFault = onFault;
9962
10231
  }
9963
10232
  channel;
9964
10233
  directory;
9965
10234
  now;
9966
10235
  reconciliation;
10236
+ onFault;
9967
10237
  startedAt;
9968
10238
  async reconnecting() {
9969
10239
  if (this.startedAt !== void 0) return;
9970
10240
  this.startedAt = this.now();
9971
- await this.send(
9972
- "\u26A0\uFE0F \u673A\u5668\u4EBA\u8FDE\u63A5\u4E0D\u7A33\u5B9A\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8FDE\uFF1B\u671F\u95F4\u7684\u65B0\u6D88\u606F\u53EF\u80FD\u5EF6\u8FDF\u5904\u7406\u3002",
9973
- "\u26A0\uFE0F The bot connection is unstable and reconnecting automatically. New messages may be delayed."
9974
- );
10241
+ const zhCn = "\u26A0\uFE0F \u673A\u5668\u4EBA\u8FDE\u63A5\u4E0D\u7A33\u5B9A\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8FDE\uFF1B\u671F\u95F4\u7684\u65B0\u6D88\u606F\u53EF\u80FD\u5EF6\u8FDF\u5904\u7406\u3002";
10242
+ const enUs = "\u26A0\uFE0F The bot connection is unstable and reconnecting automatically. New messages may be delayed.";
10243
+ await this.send(zhCn, enUs);
10244
+ await this.fault(this.directory?.recentDestination()?.scope, { zh: zhCn, en: enUs });
9975
10245
  }
9976
10246
  async reconnected() {
9977
10247
  if (this.startedAt === void 0) return;
@@ -9979,12 +10249,12 @@ var ReconnectNotifier = class {
9979
10249
  this.startedAt = void 0;
9980
10250
  const target = this.directory?.recentDestination();
9981
10251
  const summary = target ? this.reconciliation?.(target.scope) : void 0;
9982
- await this.send(
9983
- `\u2705 \u673A\u5668\u4EBA\u8FDE\u63A5\u5DF2\u6062\u590D\uFF08\u4E2D\u65AD\u7EA6 ${formatDuration(elapsed)}\uFF09\u3002${summary ? `
9984
- ${summary.zhCn}` : ""}`,
9985
- `\u2705 The bot connection recovered after about ${formatDurationEnglish(elapsed)}.${summary ? `
9986
- ${summary.enUs}` : ""}`
9987
- );
10252
+ const zhCn = `\u2705 \u673A\u5668\u4EBA\u8FDE\u63A5\u5DF2\u6062\u590D\uFF08\u4E2D\u65AD\u7EA6 ${formatDuration(elapsed)}\uFF09\u3002${summary ? `
10253
+ ${summary.zhCn}` : ""}`;
10254
+ const enUs = `\u2705 The bot connection recovered after about ${formatDurationEnglish(elapsed)}.${summary ? `
10255
+ ${summary.enUs}` : ""}`;
10256
+ await this.send(zhCn, enUs);
10257
+ await this.fault(target?.scope, { zh: zhCn, en: enUs });
9988
10258
  }
9989
10259
  async send(zhCn, enUs) {
9990
10260
  const target = this.directory?.recentDestination();
@@ -9994,6 +10264,13 @@ ${summary.enUs}` : ""}`
9994
10264
  ...target.threadId ? { threadId: target.threadId } : {}
9995
10265
  });
9996
10266
  }
10267
+ async fault(scope, title) {
10268
+ if (!scope || !this.onFault) return;
10269
+ try {
10270
+ await this.onFault(scope, title);
10271
+ } catch (error) {
10272
+ }
10273
+ }
9997
10274
  };
9998
10275
  function formatDurationEnglish(ms) {
9999
10276
  if (ms < 1e3) return "less than 1 second";
@@ -11196,7 +11473,8 @@ async function startChannel(deps) {
11196
11473
  zhCn: `\u8D26\u672C\u5BF9\u8D26\uFF1Aqueued ${counts.queued} \xB7 running ${counts.running} \xB7 interrupted ${counts.interrupted} \xB7 failed ${counts.failed}\uFF1B\u8BE6\u60C5\u89C1 \`/jobs\`\u3002`,
11197
11474
  enUs: `Ledger reconciliation: queued ${counts.queued} \xB7 running ${counts.running} \xB7 interrupted ${counts.interrupted} \xB7 failed ${counts.failed}; use \`/jobs\` for details.`
11198
11475
  };
11199
- } : void 0
11476
+ } : void 0,
11477
+ deps.faultNotifier
11200
11478
  );
11201
11479
  const isolationStore = deps.isolationStore ?? EMPTY_ISOLATION_STORE;
11202
11480
  let groupPoller;
@@ -11331,6 +11609,8 @@ async function startChannel(deps) {
11331
11609
  ...deps.permissionPolicies ? { permissionPolicies: deps.permissionPolicies } : {},
11332
11610
  ...deps.notificationPreferences ? { notificationPreferences: deps.notificationPreferences } : {},
11333
11611
  ...deps.defaultNotificationPreference ? { defaultNotificationPreference: deps.defaultNotificationPreference } : {},
11612
+ ...deps.notificationChannels ? { notificationChannels: deps.notificationChannels } : {},
11613
+ ...deps.notificationSinks ? { notificationSinks: deps.notificationSinks } : {},
11334
11614
  ...deps.replyPolicies ? { replyPolicies: deps.replyPolicies } : {},
11335
11615
  ...deps.executionModes ? { executionModes: deps.executionModes } : {},
11336
11616
  ...deps.languagePolicies ? { languagePolicies: deps.languagePolicies } : {},
@@ -12053,7 +12333,7 @@ function attachRunCardAnchors(channel, anchors) {
12053
12333
  }
12054
12334
 
12055
12335
  // src/bridge/scope-directory.ts
12056
- import { readFile as readFile11 } from "fs/promises";
12336
+ import { readFile as readFile12 } from "fs/promises";
12057
12337
  var ScopeDirectory = class {
12058
12338
  data = { entries: {} };
12059
12339
  saving = Promise.resolve();
@@ -12063,7 +12343,7 @@ var ScopeDirectory = class {
12063
12343
  }
12064
12344
  async load() {
12065
12345
  try {
12066
- const raw = await readFile11(this.path, "utf8");
12346
+ const raw = await readFile12(this.path, "utf8");
12067
12347
  const parsed = JSON.parse(raw);
12068
12348
  this.data = { entries: parsed.entries ?? {} };
12069
12349
  } catch (error) {
@@ -12804,6 +13084,11 @@ var NotificationDispatcher = class {
12804
13084
  setDefaultPreference(preference) {
12805
13085
  this.defaultPreference = clonePreference(preference);
12806
13086
  }
13087
+ /**
13088
+ * Primary notification path: send to Feishu (default first-class route) and,
13089
+ * when the scope preference lists extra channels, fan out to those sinks.
13090
+ * Returns `false` when the event is disabled or no Feishu destination exists.
13091
+ */
12807
13092
  async notify(scope, event, detail) {
12808
13093
  const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
12809
13094
  if (!preference?.events.includes(event)) return false;
@@ -12812,21 +13097,52 @@ var NotificationDispatcher = class {
12812
13097
  log.warn("notification", "target-unavailable", { scope, target: preference.target });
12813
13098
  return false;
12814
13099
  }
12815
- const title = event === "completed" ? ["\u2705 \u4EFB\u52A1\u5DF2\u5B8C\u6210", "\u2705 Task completed"] : event === "failed" ? ["\u26A0\uFE0F \u4EFB\u52A1\u6267\u884C\u5931\u8D25", "\u26A0\uFE0F Task failed"] : ["\u23F3 \u4EFB\u52A1\u4ECD\u5728\u7B49\u5F85\u64CD\u4F5C\u5BA1\u6279", "\u23F3 Task is still waiting for tool approval"];
13100
+ const title = titleForEvent(event);
12816
13101
  try {
12817
- await this.deps.send(destination.chatId, bilingualMarkdown(
12818
- `${title[0]}\uFF08scope \`${scope}\`\uFF09${detail ? `\uFF1A${detail}` : ""}`,
12819
- `${title[1]} (scope \`${scope}\`)${detail ? `: ${detail}` : ""}`
12820
- ), {
13102
+ await this.deps.send(destination.chatId, renderFeishuBody(title, scope, detail), {
12821
13103
  ...destination.threadId ? { threadId: destination.threadId } : {},
12822
13104
  ...destination.messageId ? { replyTo: destination.messageId } : {},
12823
13105
  ...preference.mentionUserIds.length > 0 ? { mentions: preference.mentionUserIds.map((userId) => ({ userId })) } : {}
12824
13106
  });
12825
- return true;
12826
13107
  } catch (error) {
12827
13108
  log.warn("notification", "send-failed", { scope, event, error });
12828
13109
  return false;
12829
13110
  }
13111
+ if (preference.sinks.length > 0) {
13112
+ await this.broadcastSinks(preference.sinks, scope, event, title, detail);
13113
+ }
13114
+ return true;
13115
+ }
13116
+ /**
13117
+ * Safety-net / fault class notification (issue #113): broadcast to every
13118
+ * enabled outbound sink regardless of the per-scope opt-in, so a crash /
13119
+ * reconnect / heartbeat anomaly is seen even when a scope has not enabled
13120
+ * proactive Feishu reminders. When the scope preference explicitly enables
13121
+ * the `urgent` event it is also sent to Feishu.
13122
+ */
13123
+ async notifyUrgent(scope, title, detail) {
13124
+ const channels = this.deps.sinks?.enabledChannels() ?? [];
13125
+ if (channels.length > 0) {
13126
+ await this.broadcastSinks(
13127
+ channels.map((channel) => channel.id),
13128
+ scope,
13129
+ "urgent",
13130
+ title,
13131
+ detail
13132
+ );
13133
+ }
13134
+ const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
13135
+ if (!preference?.events.includes("urgent")) return;
13136
+ const destination = preference.target ? this.deps.scopeDirectory.resolve(preference.target) ?? this.deps.scopeDirectory.resolveChat(preference.target) : this.deps.scopeDirectory.resolve(scope);
13137
+ if (!destination) return;
13138
+ try {
13139
+ await this.deps.send(destination.chatId, renderFeishuBody(title, scope, detail), {
13140
+ ...destination.threadId ? { threadId: destination.threadId } : {},
13141
+ ...destination.messageId ? { replyTo: destination.messageId } : {}
13142
+ });
13143
+ } catch (error) {
13144
+ log.warn("notification", "urgent-send-failed", { scope, error });
13145
+ }
12830
13146
  }
12831
13147
  scheduleApprovalReminder(scope, toolName) {
12832
13148
  const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
@@ -12837,11 +13153,197 @@ var NotificationDispatcher = class {
12837
13153
  timer.unref?.();
12838
13154
  return () => clearTimeout(timer);
12839
13155
  }
13156
+ async broadcastSinks(channelIds, scope, event, title, detail) {
13157
+ if (!this.deps.sinks || channelIds.length === 0) return;
13158
+ try {
13159
+ const outcome = await this.deps.sinks.broadcast(channelIds, {
13160
+ scope,
13161
+ event,
13162
+ title,
13163
+ ...detail === void 0 ? {} : { detail }
13164
+ });
13165
+ if (outcome.failures.length > 0) {
13166
+ log.warn("notification", "sink-failures", { scope, event, failures: outcome.failures });
13167
+ }
13168
+ } catch (error) {
13169
+ log.warn("notification", "sink-broadcast-error", { scope, event, error });
13170
+ }
13171
+ }
12840
13172
  };
13173
+ function titleForEvent(event) {
13174
+ switch (event) {
13175
+ case "completed":
13176
+ return { zh: "\u2705 \u4EFB\u52A1\u5DF2\u5B8C\u6210", en: "\u2705 Task completed" };
13177
+ case "failed":
13178
+ return { zh: "\u26A0\uFE0F \u4EFB\u52A1\u6267\u884C\u5931\u8D25", en: "\u26A0\uFE0F Task failed" };
13179
+ case "approval":
13180
+ return { zh: "\u23F3 \u4EFB\u52A1\u4ECD\u5728\u7B49\u5F85\u64CD\u4F5C\u5BA1\u6279", en: "\u23F3 Task is still waiting for tool approval" };
13181
+ case "urgent":
13182
+ return { zh: "\u{1F534} \u7A81\u53D1 / \u6545\u969C", en: "\u{1F534} Urgent / fault" };
13183
+ }
13184
+ }
13185
+ function renderFeishuBody(title, scope, detail) {
13186
+ return bilingualMarkdown(
13187
+ `${title.zh}\uFF08scope \`${scope}\`\uFF09${detail ? `\uFF1A${detail}` : ""}`,
13188
+ `${title.en} (scope \`${scope}\`)${detail ? `: ${detail}` : ""}`
13189
+ );
13190
+ }
12841
13191
  function clonePreference(preference) {
12842
- return preference ? { ...preference, events: [...preference.events], mentionUserIds: [...preference.mentionUserIds] } : void 0;
13192
+ return preference ? { ...preference, events: [...preference.events], mentionUserIds: [...preference.mentionUserIds], sinks: [...preference.sinks] } : void 0;
12843
13193
  }
12844
13194
 
13195
+ // src/notify/sinks/http.ts
13196
+ async function postJson(url, body3) {
13197
+ const controller = new AbortController();
13198
+ const timer = setTimeout(() => controller.abort(), SINK_HTTP_TIMEOUT_MS);
13199
+ timer.unref?.();
13200
+ try {
13201
+ return await fetch(url, {
13202
+ method: "POST",
13203
+ headers: { "content-type": "application/json" },
13204
+ body: body3,
13205
+ signal: controller.signal
13206
+ });
13207
+ } finally {
13208
+ clearTimeout(timer);
13209
+ }
13210
+ }
13211
+
13212
+ // src/notify/sinks/text.ts
13213
+ function renderSinkText(message) {
13214
+ const zhLine = `${message.title.zh}\uFF08scope \`${message.scope}\`\uFF09${message.detail ? `\uFF1A${message.detail}` : ""}`;
13215
+ const enLine = `${message.title.en} (scope \`${message.scope}\`)${message.detail ? `: ${message.detail}` : ""}`;
13216
+ return `${stripMarkdown(zhLine)}
13217
+
13218
+ ${stripMarkdown(enLine)}`;
13219
+ }
13220
+ function escapeHtml(value) {
13221
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
13222
+ }
13223
+ function stripMarkdown(value) {
13224
+ return value.replaceAll("`", "").replaceAll("**", "").replaceAll("*", "");
13225
+ }
13226
+
13227
+ // src/notify/sinks/telegram.ts
13228
+ var TelegramSink = class {
13229
+ constructor(baseUrl = "https://api.telegram.org") {
13230
+ this.baseUrl = baseUrl;
13231
+ }
13232
+ baseUrl;
13233
+ type = "telegram";
13234
+ async send(channel, message) {
13235
+ const token = channel.secret;
13236
+ if (!token) {
13237
+ log.warn("sink:telegram", "missing-token", { channel: channel.id });
13238
+ return false;
13239
+ }
13240
+ const url = `${this.baseUrl}/bot${encodeURIComponent(token)}/sendMessage`;
13241
+ const body3 = JSON.stringify({
13242
+ chat_id: channel.destination,
13243
+ text: escapeHtml(renderSinkText(message)),
13244
+ parse_mode: "HTML",
13245
+ disable_web_page_preview: true
13246
+ });
13247
+ try {
13248
+ const response = await postJson(url, body3);
13249
+ const payload = await response.json();
13250
+ if (!response.ok || payload.ok !== true) {
13251
+ log.warn("sink:telegram", "send-failed", {
13252
+ channel: channel.id,
13253
+ error: payload.description ?? `http ${response.status}`
13254
+ });
13255
+ return false;
13256
+ }
13257
+ return true;
13258
+ } catch (error) {
13259
+ log.warn("sink:telegram", "send-error", { channel: channel.id, error });
13260
+ return false;
13261
+ }
13262
+ }
13263
+ };
13264
+
13265
+ // src/notify/sinks/wecom.ts
13266
+ var WeComSink = class {
13267
+ constructor(baseUrl = "https://qyapi.weixin.qq.com") {
13268
+ this.baseUrl = baseUrl;
13269
+ }
13270
+ baseUrl;
13271
+ type = "wecom";
13272
+ async send(channel, message) {
13273
+ const key = channel.destination;
13274
+ if (!key) {
13275
+ log.warn("sink:wecom", "missing-key", { channel: channel.id });
13276
+ return false;
13277
+ }
13278
+ const url = `${this.baseUrl}/cgi-bin/webhook/send?key=${encodeURIComponent(key)}`;
13279
+ const body3 = JSON.stringify({
13280
+ msgtype: "text",
13281
+ text: { content: renderSinkText(message) }
13282
+ });
13283
+ try {
13284
+ const response = await postJson(url, body3);
13285
+ const payload = await response.json();
13286
+ if (!response.ok || payload.errcode !== 0) {
13287
+ log.warn("sink:wecom", "send-failed", {
13288
+ channel: channel.id,
13289
+ error: payload.errmsg ?? `http ${response.status}`
13290
+ });
13291
+ return false;
13292
+ }
13293
+ return true;
13294
+ } catch (error) {
13295
+ log.warn("sink:wecom", "send-error", { channel: channel.id, error });
13296
+ return false;
13297
+ }
13298
+ }
13299
+ };
13300
+
13301
+ // src/notify/sinks/registry.ts
13302
+ var OutboundSinkRegistry = class {
13303
+ constructor(store, sinks = [new TelegramSink(), new WeComSink()]) {
13304
+ this.store = store;
13305
+ this.sinks = sinks;
13306
+ }
13307
+ store;
13308
+ sinks;
13309
+ /** Resolve only the enabled channels referenced by `channelIds`. */
13310
+ channelsForIds(channelIds) {
13311
+ const seen = new Set(channelIds);
13312
+ return this.store.list().filter((channel) => channel.enabled && seen.has(channel.id));
13313
+ }
13314
+ /** Channels currently enabled (for `/status` and `/channels`). */
13315
+ enabledChannels() {
13316
+ return this.store.list().filter((channel) => channel.enabled);
13317
+ }
13318
+ /** Best-effort fan-out; a failing channel never blocks the others. */
13319
+ async broadcast(channelIds, message) {
13320
+ const channelIdsToSend = [...new Set(channelIds)];
13321
+ const channels = this.channelsForIds(channelIdsToSend);
13322
+ const failures = [];
13323
+ let delivered = 0;
13324
+ for (const channel of channels) {
13325
+ const sink = this.sinkFor(channel.type);
13326
+ if (!sink) {
13327
+ log.warn("sinks", "unsupported-type", { channel: channel.id, type: channel.type });
13328
+ failures.push(channel.id);
13329
+ continue;
13330
+ }
13331
+ let ok = false;
13332
+ try {
13333
+ ok = await sink.send(channel, message);
13334
+ } catch (error) {
13335
+ log.warn("sinks", "channel-send-error", { channel: channel.id, error });
13336
+ }
13337
+ if (ok) delivered += 1;
13338
+ else failures.push(channel.id);
13339
+ }
13340
+ return { delivered, failures, total: channels.length };
13341
+ }
13342
+ sinkFor(type) {
13343
+ return this.sinks.find((sink) => sink.type === type);
13344
+ }
13345
+ };
13346
+
12845
13347
  // src/bridge/reply-dispatcher.ts
12846
13348
  var ReplyDispatcher = class {
12847
13349
  constructor(deps) {
@@ -12900,8 +13402,8 @@ var ReplyDispatcher = class {
12900
13402
  }
12901
13403
  }
12902
13404
  };
12903
- function replyRoute(reply8) {
12904
- return `${reply8.chatId}\0${reply8.options?.threadId ?? ""}`;
13405
+ function replyRoute(reply9) {
13406
+ return `${reply9.chatId}\0${reply9.options?.threadId ?? ""}`;
12905
13407
  }
12906
13408
  function mergedMarkdown(batch) {
12907
13409
  if (batch.length === 1) return batch[0].markdown;
@@ -12952,6 +13454,7 @@ function resolveAppPaths(root = defaultHome()) {
12952
13454
  jobsFile: (profile) => profilePath(profile, "jobs.json"),
12953
13455
  permissionPoliciesFile: (profile) => profilePath(profile, "permission-policies.json"),
12954
13456
  notificationPreferencesFile: (profile) => profilePath(profile, "notification-preferences.json"),
13457
+ notificationChannelsFile: (profile) => profilePath(profile, "notification-channels.json"),
12955
13458
  replyPoliciesFile: (profile) => profilePath(profile, "reply-policies.json"),
12956
13459
  executionModesFile: (profile) => profilePath(profile, "execution-modes.json"),
12957
13460
  languagePoliciesFile: (profile) => profilePath(profile, "language-policy.json"),
@@ -13305,11 +13808,11 @@ function loadRuntimeEnv(source = process.env) {
13305
13808
  }
13306
13809
 
13307
13810
  // src/config/profile-store.ts
13308
- import { mkdir as mkdir7, readFile as readFile13 } from "fs/promises";
13811
+ import { mkdir as mkdir7, readFile as readFile14 } from "fs/promises";
13309
13812
  import { dirname as dirname8 } from "path";
13310
13813
 
13311
13814
  // src/platform/file-lock.ts
13312
- import { open as open3, mkdir as mkdir6, readFile as readFile12, readdir as readdir2, rm as rm3, rmdir, stat as stat3, utimes } from "fs/promises";
13815
+ import { open as open3, mkdir as mkdir6, readFile as readFile13, readdir as readdir2, rm as rm3, rmdir, stat as stat3, utimes } from "fs/promises";
13313
13816
  import { randomUUID as randomUUID10 } from "crypto";
13314
13817
  import { dirname as dirname7, join as join12 } from "path";
13315
13818
  var RETRY_MS = 10;
@@ -13393,7 +13896,7 @@ async function olderThan(path, thresholdMs) {
13393
13896
  }
13394
13897
  async function readLease(path) {
13395
13898
  try {
13396
- const value = JSON.parse(await readFile12(path, "utf8"));
13899
+ const value = JSON.parse(await readFile13(path, "utf8"));
13397
13900
  return typeof value.pid === "number" && typeof value.token === "string" && typeof value.createdAt === "number" ? value : void 0;
13398
13901
  } catch {
13399
13902
  return void 0;
@@ -13421,7 +13924,7 @@ var ConfigStore = class {
13421
13924
  return this.data;
13422
13925
  }
13423
13926
  try {
13424
- const raw = await readFile13(this.path, "utf8");
13927
+ const raw = await readFile14(this.path, "utf8");
13425
13928
  const parsed = JSON.parse(raw);
13426
13929
  this.data = {
13427
13930
  schemaVersion: 1,
@@ -13589,7 +14092,7 @@ async function onboardPersonalAgent(deps = {}) {
13589
14092
  }
13590
14093
 
13591
14094
  // src/media/attachments.ts
13592
- import { mkdir as mkdir8, readFile as readFile14, rename as rename2, rm as rm4, stat as stat4 } from "fs/promises";
14095
+ import { mkdir as mkdir8, readFile as readFile15, rename as rename2, rm as rm4, stat as stat4 } from "fs/promises";
13593
14096
  import { join as join13 } from "path";
13594
14097
  var MAX_TEXT_FILE_BYTES = 256e3;
13595
14098
  function assertSafeMediaName(mediaDir, destination) {
@@ -13630,7 +14133,7 @@ async function prepareAttachments(channel, message, mediaDir) {
13630
14133
  result.textFileNotes.push(`[attachment: ${resource.fileName ?? resource.fileKey}] ${destination}`);
13631
14134
  continue;
13632
14135
  }
13633
- const content2 = await readFile14(destination, "utf8");
14136
+ const content2 = await readFile15(destination, "utf8");
13634
14137
  const safeContent = truncateUtf8Safe(content2, MAX_TEXT_FILE_BYTES);
13635
14138
  result.textFileNotes.push(
13636
14139
  `[attachment: ${resource.fileName ?? resource.fileKey}]
@@ -13641,7 +14144,7 @@ ${safeContent}`
13641
14144
  }
13642
14145
 
13643
14146
  // src/session/store.ts
13644
- import { readFile as readFile15 } from "fs/promises";
14147
+ import { readFile as readFile16 } from "fs/promises";
13645
14148
  var MAX_CONTEXT_SNAPSHOTS_PER_WORKSPACE = 32;
13646
14149
  var SessionStore = class {
13647
14150
  constructor(path) {
@@ -13656,7 +14159,7 @@ var SessionStore = class {
13656
14159
  pendingArchive = Promise.resolve();
13657
14160
  async load() {
13658
14161
  try {
13659
- const raw = JSON.parse(await readFile15(this.path, "utf8"));
14162
+ const raw = JSON.parse(await readFile16(this.path, "utf8"));
13660
14163
  const legacy = raw.schema !== 2 || !isObject(raw.workspaces);
13661
14164
  this.data = legacy ? migrateLegacy(raw) : normalizeV2(raw.workspaces);
13662
14165
  this.legacyScopes = legacy ? new Set(Object.keys(this.data.workspaces)) : /* @__PURE__ */ new Set();
@@ -13753,13 +14256,13 @@ var SessionStore = class {
13753
14256
  fullHistoryFor(scopeId, cwd) {
13754
14257
  return this.historyFor(scopeId, cwd);
13755
14258
  }
13756
- recordUsage(scopeId, cwd, usage3) {
14259
+ recordUsage(scopeId, cwd, usage4) {
13757
14260
  const state = this.ensureState(scopeId, cwd);
13758
14261
  const next = { ...state.metrics };
13759
- addMetric(next, "inputTokens", usage3.inputTokens);
13760
- addMetric(next, "outputTokens", usage3.outputTokens);
13761
- addMetric(next, "cacheReadTokens", usage3.cacheReadTokens);
13762
- addMetric(next, "cacheWriteTokens", usage3.cacheWriteTokens);
14262
+ addMetric(next, "inputTokens", usage4.inputTokens);
14263
+ addMetric(next, "outputTokens", usage4.outputTokens);
14264
+ addMetric(next, "cacheReadTokens", usage4.cacheReadTokens);
14265
+ addMetric(next, "cacheWriteTokens", usage4.cacheWriteTokens);
13763
14266
  state.metrics = next;
13764
14267
  this.schedulePersist();
13765
14268
  }
@@ -13975,7 +14478,7 @@ function normalizeContextSnapshot(value) {
13975
14478
  }
13976
14479
 
13977
14480
  // src/session/projection-store.ts
13978
- import { readFile as readFile16 } from "fs/promises";
14481
+ import { readFile as readFile17 } from "fs/promises";
13979
14482
  import { randomUUID as randomUUID11 } from "crypto";
13980
14483
  var MAX_RECENT_MESSAGES = 64;
13981
14484
  var MAX_PROMPT_CORRELATIONS = 64;
@@ -13989,7 +14492,7 @@ var SessionProjectionStore = class {
13989
14492
  saving = Promise.resolve();
13990
14493
  async load() {
13991
14494
  try {
13992
- const parsed = JSON.parse(await readFile16(this.path, "utf8"));
14495
+ const parsed = JSON.parse(await readFile17(this.path, "utf8"));
13993
14496
  const bindings = Object.fromEntries(
13994
14497
  Object.values(parsed.bindings ?? {}).flatMap((value) => {
13995
14498
  const normalized = normalizeBinding(value);
@@ -14201,7 +14704,7 @@ function normalizeCorrelation(value) {
14201
14704
  // src/session/archive.ts
14202
14705
  import { execFile } from "child_process";
14203
14706
  import { randomBytes as randomBytes3 } from "crypto";
14204
- import { access, mkdir as mkdir9, readdir as readdir3, readFile as readFile17, unlink, writeFile as writeFile5 } from "fs/promises";
14707
+ import { access, mkdir as mkdir9, readdir as readdir3, readFile as readFile18, unlink, writeFile as writeFile5 } from "fs/promises";
14205
14708
  import { join as join14 } from "path";
14206
14709
  import { promisify } from "util";
14207
14710
  var execFileAsync = promisify(execFile);
@@ -14383,8 +14886,8 @@ var SessionArchive = class {
14383
14886
  const jsonlPath = join14(scopeDir, `${id}.jsonl`);
14384
14887
  const markdownPath = join14(scopeDir, `${id}.md`);
14385
14888
  const [jsonl, markdown2] = await Promise.all([
14386
- readFile17(jsonlPath, "utf8"),
14387
- readFile17(markdownPath, "utf8")
14889
+ readFile18(jsonlPath, "utf8"),
14890
+ readFile18(markdownPath, "utf8")
14388
14891
  ]);
14389
14892
  const [first = "", ...rest] = jsonl.split("\n");
14390
14893
  const header = JSON.parse(first);
@@ -14499,7 +15002,7 @@ async function defaultRunGit(args, cwd) {
14499
15002
  }
14500
15003
  async function readArchiveHeader(jsonlPath) {
14501
15004
  try {
14502
- const raw = await readFile17(jsonlPath, "utf8");
15005
+ const raw = await readFile18(jsonlPath, "utf8");
14503
15006
  const firstLine = raw.split("\n", 1)[0];
14504
15007
  if (!firstLine) return void 0;
14505
15008
  const header = JSON.parse(firstLine);
@@ -14549,7 +15052,7 @@ async function defaultCopyRulesFile(source, target) {
14549
15052
  await mkdir10(dirname9(target), { recursive: true });
14550
15053
  await copyFile(source, target);
14551
15054
  }
14552
- function slugify(scope) {
15055
+ function slugify2(scope) {
14553
15056
  const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[.]+/, "").replace(/^-+|-+$/g, "").slice(0, 64);
14554
15057
  return slug || "session";
14555
15058
  }
@@ -14565,7 +15068,7 @@ var GitWorktreeManager = class {
14565
15068
  async ensure(scope, base) {
14566
15069
  const isGit = await this.isGitRepository(base);
14567
15070
  if (!isGit) return { cwd: base, created: false };
14568
- const scopeSlug = slugify(scope);
15071
+ const scopeSlug = slugify2(scope);
14569
15072
  const project = createHash2("sha256").update(base).digest("hex").slice(0, 10);
14570
15073
  const slug = `${scopeSlug}-${project}`;
14571
15074
  const target = join15(this.options.worktreesRoot, slug);
@@ -14602,7 +15105,7 @@ var GitWorktreeManager = class {
14602
15105
  }
14603
15106
  /** Owning main worktree for the legacy scope-only linked worktree. */
14604
15107
  async legacyWorkspaceBase(scope) {
14605
- const legacyTarget = join15(this.options.worktreesRoot, slugify(scope));
15108
+ const legacyTarget = join15(this.options.worktreesRoot, slugify2(scope));
14606
15109
  if (!await exists2(legacyTarget)) return void 0;
14607
15110
  try {
14608
15111
  const output = await this.runGit(["worktree", "list", "--porcelain"], legacyTarget);
@@ -14634,7 +15137,7 @@ async function samePath(left, right) {
14634
15137
  }
14635
15138
 
14636
15139
  // src/workspace/store.ts
14637
- import { readFile as readFile18 } from "fs/promises";
15140
+ import { readFile as readFile19 } from "fs/promises";
14638
15141
  var WorkspaceStore = class {
14639
15142
  data = { chats: {}, named: {}, lastUsed: {} };
14640
15143
  saving = Promise.resolve();
@@ -14644,7 +15147,7 @@ var WorkspaceStore = class {
14644
15147
  }
14645
15148
  async load() {
14646
15149
  try {
14647
- const raw = await readFile18(this.path, "utf8");
15150
+ const raw = await readFile19(this.path, "utf8");
14648
15151
  const parsed = JSON.parse(raw);
14649
15152
  this.data = {
14650
15153
  chats: parsed.chats ?? {},
@@ -14716,7 +15219,7 @@ var WorkspaceStore = class {
14716
15219
  };
14717
15220
 
14718
15221
  // src/guardian/heartbeat.ts
14719
- import { readFile as readFile19 } from "fs/promises";
15222
+ import { readFile as readFile20 } from "fs/promises";
14720
15223
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 5e3;
14721
15224
  function startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, getChannelHealth) {
14722
15225
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -14797,7 +15300,7 @@ var UpdateNotifier = class {
14797
15300
  };
14798
15301
 
14799
15302
  // src/bot/fleet-store.ts
14800
- import { mkdir as mkdir11, readFile as readFile20 } from "fs/promises";
15303
+ import { mkdir as mkdir11, readFile as readFile21 } from "fs/promises";
14801
15304
  import { dirname as dirname10 } from "path";
14802
15305
  function validBotInstanceName(name2) {
14803
15306
  return /^[a-z][a-z0-9-]{0,31}$/u.test(name2);
@@ -14810,7 +15313,7 @@ var BotFleetStore = class {
14810
15313
  data = { schemaVersion: 1, bots: {} };
14811
15314
  async load() {
14812
15315
  try {
14813
- const parsed = JSON.parse(await readFile20(this.path, "utf8"));
15316
+ const parsed = JSON.parse(await readFile21(this.path, "utf8"));
14814
15317
  this.data = {
14815
15318
  schemaVersion: 1,
14816
15319
  bots: Object.fromEntries(
@@ -14938,7 +15441,7 @@ function isFleetEntry(value) {
14938
15441
  }
14939
15442
 
14940
15443
  // src/bot/handoff-guard.ts
14941
- import { mkdir as mkdir12, readFile as readFile21 } from "fs/promises";
15444
+ import { mkdir as mkdir12, readFile as readFile22 } from "fs/promises";
14942
15445
  import { dirname as dirname11 } from "path";
14943
15446
  var BotHandoffGuard = class {
14944
15447
  constructor(path) {
@@ -14979,7 +15482,7 @@ var BotHandoffGuard = class {
14979
15482
  }
14980
15483
  async read() {
14981
15484
  try {
14982
- const parsed = JSON.parse(await readFile21(this.path, "utf8"));
15485
+ const parsed = JSON.parse(await readFile22(this.path, "utf8"));
14983
15486
  return { schemaVersion: 1, chats: parsed.chats ?? {} };
14984
15487
  } catch (error) {
14985
15488
  if (error.code !== "ENOENT") throw error;
@@ -15005,7 +15508,7 @@ init_dsh_runtime();
15005
15508
  import { homedir as homedir14 } from "os";
15006
15509
 
15007
15510
  // src/bot/job-ledger.ts
15008
- import { readFile as readFile22, rename as rename3 } from "fs/promises";
15511
+ import { readFile as readFile23, rename as rename3 } from "fs/promises";
15009
15512
  var EMPTY_COUNTS = {
15010
15513
  queued: 0,
15011
15514
  running: 0,
@@ -15028,7 +15531,7 @@ var JobLedger = class {
15028
15531
  if (this.path === ":memory:") return;
15029
15532
  let raw;
15030
15533
  try {
15031
- raw = await readFile22(this.path, "utf8");
15534
+ raw = await readFile23(this.path, "utf8");
15032
15535
  } catch (error) {
15033
15536
  if (error.code === "ENOENT") return;
15034
15537
  throw error;
@@ -15604,21 +16107,91 @@ function projectDiagnosticLogs(lines) {
15604
16107
  }
15605
16108
 
15606
16109
  // src/service/manager.ts
15607
- import { mkdir as mkdir19, readFile as readFile27, rename as rename4, rm as rm9, stat as stat8 } from "fs/promises";
15608
- import { dirname as dirname16, join as join22 } from "path";
16110
+ import { mkdir as mkdir19, readFile as readFile28, rename as rename4, rm as rm9, stat as stat8 } from "fs/promises";
16111
+ import { dirname as dirname17, join as join22 } from "path";
15609
16112
  import { homedir as homedir13 } from "os";
15610
16113
  init_dsh_runtime();
15611
16114
 
15612
16115
  // src/guardian/install.ts
15613
- init_own_package();
15614
- init_dsh_runtime();
15615
- import { mkdir as mkdir14, readFile as readFile24, rm as rm5, writeFile as writeFile6 } from "fs/promises";
16116
+ import { mkdir as mkdir14, readFile as readFile25, rm as rm5, writeFile as writeFile6 } from "fs/promises";
15616
16117
  import { existsSync as existsSync5 } from "fs";
15617
16118
  import { homedir as homedir9 } from "os";
15618
- import { delimiter, dirname as dirname13, join as join16 } from "path";
16119
+ import { dirname as dirname14, join as join16 } from "path";
16120
+
16121
+ // src/platform/path.ts
16122
+ import { delimiter, dirname as dirname12 } from "path";
16123
+ function sanitizeServicePath(nodeBin, inheritedPath = process.env.PATH) {
16124
+ const entries = [dirname12(nodeBin), ...inheritedPath?.split(delimiter) ?? []];
16125
+ const seen = /* @__PURE__ */ new Set();
16126
+ return entries.filter((entry) => {
16127
+ if (!entry) return false;
16128
+ const normalized = entry.replaceAll("\\", "/");
16129
+ if (normalized.includes("/node_modules/.bin") || /\/_npx(?:\/|$)/.test(normalized) || /\/guardian\/update-worker\/npm-cache(?:\/|$)/.test(normalized)) {
16130
+ return false;
16131
+ }
16132
+ if (seen.has(entry)) return false;
16133
+ seen.add(entry);
16134
+ return true;
16135
+ }).join(delimiter);
16136
+ }
16137
+
16138
+ // src/guardian/install.ts
16139
+ init_own_package();
16140
+ init_dsh_runtime();
15619
16141
 
15620
16142
  // src/guardian/process.ts
15621
16143
  import { spawn as spawn5 } from "child_process";
16144
+ function commandTokens(cmdline) {
16145
+ const tokens = [];
16146
+ let token = "";
16147
+ let quote;
16148
+ const input = cmdline.trim();
16149
+ for (let index = 0; index < input.length; index += 1) {
16150
+ const character = input[index] ?? "";
16151
+ const next = input[index + 1];
16152
+ if (character === "\\" && quote === void 0 && next !== void 0 && /[\s\\"']/u.test(next)) {
16153
+ token += next;
16154
+ index += 1;
16155
+ } else if (character === "\\" && quote === '"' && next === '"') {
16156
+ token += next;
16157
+ index += 1;
16158
+ } else if (quote !== void 0) {
16159
+ if (character === quote) quote = void 0;
16160
+ else token += character;
16161
+ } else if (character === '"' || character === "'") {
16162
+ quote = character;
16163
+ } else if (/\s/u.test(character)) {
16164
+ if (token !== "") {
16165
+ tokens.push(token);
16166
+ token = "";
16167
+ }
16168
+ } else {
16169
+ token += character;
16170
+ }
16171
+ }
16172
+ if (token !== "") tokens.push(token);
16173
+ return tokens;
16174
+ }
16175
+ function isGuardianCliEntry(token) {
16176
+ const normalized = token.replace(/\\/gu, "/").toLowerCase();
16177
+ return normalized.endsWith("/dist/cli.js") && (normalized.includes("/dsh-lark-bot/") || normalized.includes("/dsh-feishu-bot/"));
16178
+ }
16179
+ function hasOnlyGuardianRunOptions(tokens) {
16180
+ for (let index = 0; index < tokens.length; index += 2) {
16181
+ if (tokens[index] !== "--dsh-profile" && tokens[index] !== "--bridge-profile") return false;
16182
+ if (tokens[index + 1] === void 0 || tokens[index + 1]?.startsWith("--")) return false;
16183
+ }
16184
+ return true;
16185
+ }
16186
+ function matchGuardianProcess(cmdline) {
16187
+ const tokens = commandTokens(cmdline);
16188
+ const cliIndex = tokens.findIndex(isGuardianCliEntry);
16189
+ if (cliIndex < 1) return false;
16190
+ const executable = tokens[cliIndex - 1]?.replace(/\\/gu, "/").split("/").pop()?.toLowerCase();
16191
+ if (executable !== "node" && executable !== "node.exe") return false;
16192
+ if (tokens[cliIndex + 1] !== "guardian" || tokens[cliIndex + 2] !== "run") return false;
16193
+ return hasOnlyGuardianRunOptions(tokens.slice(cliIndex + 3));
16194
+ }
15622
16195
  function hasProfileFlag(cmdline, dshProfile) {
15623
16196
  const pattern = new RegExp(
15624
16197
  `(?:^|\\s)--profile(?:\\s+|=)${escapeRegExp(dshProfile)}(?:\\s|$)`
@@ -15645,12 +16218,18 @@ async function listProcesses(platform = process.platform, run = captureOutput) {
15645
16218
  return listProcessesPosix(run);
15646
16219
  }
15647
16220
  async function listProcessesPosix(run) {
15648
- const { stdout } = await run("ps", ["-axo", "pid=,args="], 1e4);
16221
+ const { stdout } = await run("ps", ["-axo", "pid=,ppid=,args="], 1e4);
15649
16222
  const result = [];
15650
16223
  for (const line of stdout.split("\n")) {
15651
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
15652
- if (match) {
15653
- result.push({ pid: Number(match[1]), cmdline: match[2] ?? "" });
16224
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
16225
+ if (!match) continue;
16226
+ const pid = Number(match[1]);
16227
+ const ppid = Number(match[2]);
16228
+ if (!Number.isInteger(pid) || pid <= 0) continue;
16229
+ if (Number.isInteger(ppid) && ppid > 0) {
16230
+ result.push({ pid, ppid, cmdline: match[3] ?? "" });
16231
+ } else {
16232
+ result.push({ pid, cmdline: match[3] ?? "" });
15654
16233
  }
15655
16234
  }
15656
16235
  return result;
@@ -15662,7 +16241,7 @@ async function listProcessesWindows(run) {
15662
16241
  "-NoProfile",
15663
16242
  "-NonInteractive",
15664
16243
  "-Command",
15665
- "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress"
16244
+ "Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, CommandLine | ConvertTo-Json -Compress"
15666
16245
  ],
15667
16246
  1e4
15668
16247
  );
@@ -15673,8 +16252,11 @@ async function listProcessesWindows(run) {
15673
16252
  if (typeof row !== "object" || row === null) return [];
15674
16253
  const record = row;
15675
16254
  const pid = Number(record.ProcessId);
16255
+ const ppid = Number(record.ParentProcessId);
15676
16256
  const cmdline = record.CommandLine;
15677
- return Number.isInteger(pid) && pid > 0 && typeof cmdline === "string" ? [{ pid, cmdline }] : [];
16257
+ if (!Number.isInteger(pid) || pid <= 0 || typeof cmdline !== "string") return [];
16258
+ if (Number.isInteger(ppid) && ppid > 0) return [{ pid, ppid, cmdline }];
16259
+ return [{ pid, cmdline }];
15678
16260
  });
15679
16261
  } catch {
15680
16262
  return [];
@@ -15722,8 +16304,8 @@ async function findProfileProcess(dshProfile) {
15722
16304
  }
15723
16305
 
15724
16306
  // src/guardian/state.ts
15725
- import { readFile as readFile23 } from "fs/promises";
15726
- import { dirname as dirname12 } from "path";
16307
+ import { readFile as readFile24 } from "fs/promises";
16308
+ import { dirname as dirname13 } from "path";
15727
16309
  import { mkdir as mkdir13 } from "fs/promises";
15728
16310
 
15729
16311
  // src/guardian/install.ts
@@ -15743,7 +16325,7 @@ function resolveGuardianCliEntry(dshProfile, root, env = process.env) {
15743
16325
 
15744
16326
  // src/service/command.ts
15745
16327
  import { execFile as execFile3 } from "child_process";
15746
- import { basename as basename4, dirname as dirname14, join as join17 } from "path";
16328
+ import { basename as basename4, dirname as dirname15, join as join17 } from "path";
15747
16329
  import { fileURLToPath as fileURLToPath2 } from "url";
15748
16330
  var runCommand = (command, args, options) => new Promise((resolve6) => {
15749
16331
  execFile3(
@@ -15769,21 +16351,21 @@ function resolveCliJsPath(metaUrl = import.meta.url) {
15769
16351
  const current = fileURLToPath2(metaUrl);
15770
16352
  const name2 = basename4(current);
15771
16353
  if (name2 === "cli.js" || name2 === "cli.mjs" || name2 === "cli.cjs") return current;
15772
- return join17(dirname14(current), "..", "dist", "cli.js");
16354
+ return join17(dirname15(current), "..", "dist", "cli.js");
15773
16355
  }
15774
- function slugify2(value) {
16356
+ function slugify3(value) {
15775
16357
  const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15776
16358
  return slug || "default";
15777
16359
  }
15778
16360
  function serviceNameFor(profile) {
15779
- return `dsh-lark-engine-${slugify2(profile)}`;
16361
+ return `dsh-lark-engine-${slugify3(profile)}`;
15780
16362
  }
15781
16363
  function launchdLabelFor(profile) {
15782
- return `io.dsh-lark.engine.${slugify2(profile)}`;
16364
+ return `io.dsh-lark.engine.${slugify3(profile)}`;
15783
16365
  }
15784
16366
 
15785
16367
  // src/service/env-snapshot.ts
15786
- import { readFile as readFile25 } from "fs/promises";
16368
+ import { readFile as readFile26 } from "fs/promises";
15787
16369
  var EXTRA_KEYS = ["PATH", "HOME", "DEEPSEEK_API_KEY", "DSH_HOME"];
15788
16370
  var INTERNAL_KEYS = /* @__PURE__ */ new Set([
15789
16371
  "DSH_LARK_NOTIFY_URL",
@@ -15812,6 +16394,9 @@ function snapshotServiceEnv(source = process.env, extraKeys = [], inherited = {}
15812
16394
  }
15813
16395
  }
15814
16396
  }
16397
+ if (typeof env.PATH === "string" && env.PATH.length > 0) {
16398
+ env.PATH = sanitizeServicePath(process.execPath, env.PATH);
16399
+ }
15815
16400
  return env;
15816
16401
  }
15817
16402
  function formatEnvFile(env) {
@@ -15852,7 +16437,7 @@ async function writeServiceEnv(file, env) {
15852
16437
  }
15853
16438
  async function readServiceEnv(file) {
15854
16439
  try {
15855
- return parseEnvFile(await readFile25(file, "utf8"));
16440
+ return parseEnvFile(await readFile26(file, "utf8"));
15856
16441
  } catch (error) {
15857
16442
  if (error.code === "ENOENT") return {};
15858
16443
  throw error;
@@ -16164,7 +16749,7 @@ var LaunchdServiceController = class {
16164
16749
 
16165
16750
  // src/service/portable.ts
16166
16751
  import { spawn as spawn6 } from "child_process";
16167
- import { mkdir as mkdir17, readFile as readFile26, rm as rm8, stat as stat7, writeFile as writeFile7 } from "fs/promises";
16752
+ import { mkdir as mkdir17, readFile as readFile27, rm as rm8, stat as stat7, writeFile as writeFile7 } from "fs/promises";
16168
16753
  import { homedir as homedir12 } from "os";
16169
16754
  import { join as join20 } from "path";
16170
16755
  function defaultAutostartDir() {
@@ -16199,8 +16784,8 @@ function buildDesktopEntry(spec) {
16199
16784
  async function readLinuxProcessIdentity(pid) {
16200
16785
  try {
16201
16786
  const [statText, cmdlineText] = await Promise.all([
16202
- readFile26(`/proc/${pid}/stat`, "utf8"),
16203
- readFile26(`/proc/${pid}/cmdline`, "utf8")
16787
+ readFile27(`/proc/${pid}/stat`, "utf8"),
16788
+ readFile27(`/proc/${pid}/cmdline`, "utf8")
16204
16789
  ]);
16205
16790
  const close = statText.lastIndexOf(") ");
16206
16791
  if (close < 0) return void 0;
@@ -16231,7 +16816,7 @@ function isProcessAlive(pid) {
16231
16816
  }
16232
16817
  async function readSupervisorStatus(file) {
16233
16818
  try {
16234
- return JSON.parse(await readFile26(file, "utf8"));
16819
+ return JSON.parse(await readFile27(file, "utf8"));
16235
16820
  } catch {
16236
16821
  return void 0;
16237
16822
  }
@@ -16397,7 +16982,7 @@ var PortableServiceController = class {
16397
16982
 
16398
16983
  // src/service/windows-task.ts
16399
16984
  import { mkdir as mkdir18, writeFile as writeFile8 } from "fs/promises";
16400
- import { dirname as dirname15, join as join21 } from "path";
16985
+ import { dirname as dirname16, join as join21 } from "path";
16401
16986
  function psQuote(value) {
16402
16987
  return `'${value.replace(/'/g, "''")}'`;
16403
16988
  }
@@ -16408,7 +16993,7 @@ function buildScheduledTaskScript(action, spec) {
16408
16993
  `$arguments = ${psQuote(spec.commandArgs.map((arg) => `"${arg.replace(/"/g, '\\"')}"`).join(" "))}`,
16409
16994
  `$log = ${psQuote(spec.logFile)}`,
16410
16995
  `$profile = ${psQuote(spec.profile)}`,
16411
- `$workDir = ${psQuote(dirname15(spec.commandPath))}`
16996
+ `$workDir = ${psQuote(dirname16(spec.commandPath))}`
16412
16997
  ].join("\n");
16413
16998
  const install = [
16414
16999
  `$inner = '"' + $command + '" ' + $arguments + ' >> "' + $log + '" 2>&1'`,
@@ -16606,7 +17191,7 @@ var ServiceManager = class {
16606
17191
  async readIntent() {
16607
17192
  try {
16608
17193
  const parsed = JSON.parse(
16609
- await readFile27(this.paths.serviceIntentFile(this.profile), "utf8")
17194
+ await readFile28(this.paths.serviceIntentFile(this.profile), "utf8")
16610
17195
  );
16611
17196
  if (parsed.schemaVersion !== 1 || !["running", "stopped"].includes(parsed.desiredState ?? "")) {
16612
17197
  return void 0;
@@ -16626,14 +17211,14 @@ var ServiceManager = class {
16626
17211
  }
16627
17212
  async withLifecycleLock(operation) {
16628
17213
  const lockDir = this.paths.serviceLockDir(this.profile);
16629
- await mkdir19(dirname16(lockDir), { recursive: true });
17214
+ await mkdir19(dirname17(lockDir), { recursive: true });
16630
17215
  try {
16631
17216
  await mkdir19(lockDir);
16632
17217
  } catch (error) {
16633
17218
  if (error.code !== "EEXIST") throw error;
16634
17219
  let ownerPid;
16635
17220
  try {
16636
- const owner = JSON.parse(await readFile27(join22(lockDir, "owner.json"), "utf8"));
17221
+ const owner = JSON.parse(await readFile28(join22(lockDir, "owner.json"), "utf8"));
16637
17222
  if (typeof owner.pid === "number") ownerPid = owner.pid;
16638
17223
  } catch {
16639
17224
  }
@@ -16666,11 +17251,39 @@ var ServiceManager = class {
16666
17251
  if (serviceStatus.state === "running") return;
16667
17252
  const process2 = await this.findProcess(this.profile);
16668
17253
  if (process2) {
17254
+ if (await this.isGuardianSpawned(process2)) {
17255
+ await this.stopGuardianSpawnedProcess(process2.pid);
17256
+ return;
17257
+ }
16669
17258
  throw new Error(
16670
17259
  `\u68C0\u6D4B\u5230\u672A\u53D7\u7BA1\u7684 dsh --profile ${this.profile}\uFF08pid ${process2.pid}\uFF09\u3002\u8BF7\u5148\u5728\u539F\u7EC8\u7AEF\u505C\u6B62\u5B83\uFF0C\u518D\u91CD\u8BD5\u3002`
16671
17260
  );
16672
17261
  }
16673
17262
  }
17263
+ /** True when the profile process was spawned by the resident guardian. */
17264
+ async isGuardianSpawned(process2) {
17265
+ const ppid = process2.ppid;
17266
+ if (!ppid) return false;
17267
+ const parent = (await listProcesses()).find((entry) => entry.pid === ppid);
17268
+ return parent !== void 0 && matchGuardianProcess(parent.cmdline);
17269
+ }
17270
+ /** Best-effort stop of a guardian-spawned profile process (grace, then kill). */
17271
+ async stopGuardianSpawnedProcess(pid) {
17272
+ try {
17273
+ process.kill(pid, "SIGTERM");
17274
+ } catch {
17275
+ return;
17276
+ }
17277
+ const deadline = Date.now() + 5e3;
17278
+ while (Date.now() < deadline && isPidAlive2(pid)) {
17279
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
17280
+ }
17281
+ if (!isPidAlive2(pid)) return;
17282
+ try {
17283
+ process.kill(pid, "SIGKILL");
17284
+ } catch {
17285
+ }
17286
+ }
16674
17287
  async buildSpec(requireProfile = false) {
16675
17288
  const previous = await this.readMetadata();
16676
17289
  const dshBin = this.dshBinOverride ?? discoverDshBin(this.home, this.sourceEnv) ?? previous?.dshBin;
@@ -16759,7 +17372,7 @@ var ServiceManager = class {
16759
17372
  }
16760
17373
  async readMetadata() {
16761
17374
  try {
16762
- const parsed = JSON.parse(await readFile27(this.metadataFile(), "utf8"));
17375
+ const parsed = JSON.parse(await readFile28(this.metadataFile(), "utf8"));
16763
17376
  if (parsed.schemaVersion !== 2 || typeof parsed.profile !== "string") return void 0;
16764
17377
  return parsed;
16765
17378
  } catch {
@@ -16767,8 +17380,8 @@ var ServiceManager = class {
16767
17380
  }
16768
17381
  }
16769
17382
  async prepareFiles(spec, platform) {
16770
- await mkdir19(dirname16(spec.logFile), { recursive: true });
16771
- await mkdir19(dirname16(spec.envFile), { recursive: true });
17383
+ await mkdir19(dirname17(spec.logFile), { recursive: true });
17384
+ await mkdir19(dirname17(spec.envFile), { recursive: true });
16772
17385
  await writeServiceEnv(spec.envFile, spec.env);
16773
17386
  if (platform === "win32-task") {
16774
17387
  try {
@@ -16861,7 +17474,7 @@ var ServiceManager = class {
16861
17474
  async logs(lines = 100) {
16862
17475
  const path = this.paths.serviceLogFile(this.profile);
16863
17476
  try {
16864
- const content2 = await readFile27(path, "utf8");
17477
+ const content2 = await readFile28(path, "utf8");
16865
17478
  return { path, text: content2.split(/\r?\n/).slice(-Math.max(1, lines)).join("\n") };
16866
17479
  } catch (error) {
16867
17480
  if (error.code === "ENOENT") return { path, text: "" };
@@ -17036,8 +17649,8 @@ init_own_package();
17036
17649
  // src/guardian/update-handoff.ts
17037
17650
  import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
17038
17651
  import { spawn as spawn7 } from "child_process";
17039
- import { chmod, mkdir as mkdir20, readFile as readFile28, rm as rm10 } from "fs/promises";
17040
- import { dirname as dirname17, join as join23 } from "path";
17652
+ import { chmod, mkdir as mkdir20, readFile as readFile29, rm as rm10 } from "fs/promises";
17653
+ import { dirname as dirname18, join as join23 } from "path";
17041
17654
  init_own_package();
17042
17655
  function guardianUpdateFailureHint(code) {
17043
17656
  switch (code) {
@@ -17065,7 +17678,7 @@ function guardianUpdateFailureHint(code) {
17065
17678
  }
17066
17679
  async function loadState(file) {
17067
17680
  try {
17068
- const parsed = JSON.parse(await readFile28(file, "utf8"));
17681
+ const parsed = JSON.parse(await readFile29(file, "utf8"));
17069
17682
  return parsed.schemaVersion === 1 && typeof parsed.id === "string" ? parsed : void 0;
17070
17683
  } catch (error) {
17071
17684
  if (error.code === "ENOENT") return void 0;
@@ -17073,7 +17686,7 @@ async function loadState(file) {
17073
17686
  }
17074
17687
  }
17075
17688
  async function saveState(file, state) {
17076
- await mkdir20(dirname17(file), { recursive: true });
17689
+ await mkdir20(dirname18(file), { recursive: true });
17077
17690
  await writeFileAtomic(file, `${JSON.stringify(state, null, 2)}
17078
17691
  `, { mode: 384 });
17079
17692
  }
@@ -17350,6 +17963,8 @@ async function startBridgeEngine(options) {
17350
17963
  const isolationStore = new IsolationStore(paths.profilePath(profileName, "isolation.json"));
17351
17964
  const permissionPolicies = new PermissionPolicyStore(paths.permissionPoliciesFile(profileName));
17352
17965
  const notificationPreferences = new NotificationPreferenceStore(paths.notificationPreferencesFile(profileName));
17966
+ const notificationChannels = new NotificationChannelStore(paths.notificationChannelsFile(profileName));
17967
+ const notificationSinks = new OutboundSinkRegistry(notificationChannels);
17353
17968
  const replyPolicies = new ReplyPolicyStore(paths.replyPoliciesFile(profileName));
17354
17969
  const executionModes = new ExecutionModeStore(paths.executionModesFile(profileName));
17355
17970
  const languagePolicies = new LanguagePolicyStore(paths.languagePoliciesFile(profileName));
@@ -17372,6 +17987,7 @@ async function startBridgeEngine(options) {
17372
17987
  isolationStore.load(),
17373
17988
  permissionPolicies.load(),
17374
17989
  notificationPreferences.load(),
17990
+ notificationChannels.load(),
17375
17991
  replyPolicies.load(),
17376
17992
  executionModes.load(),
17377
17993
  languagePolicies.load()
@@ -17441,7 +18057,8 @@ async function startBridgeEngine(options) {
17441
18057
  send: async (chatId, markdown2, options2) => {
17442
18058
  if (!streaming) throw new Error("bridge channel is not ready");
17443
18059
  await streaming.sendMarkdown(chatId, markdown2, options2);
17444
- }
18060
+ },
18061
+ sinks: notificationSinks
17445
18062
  });
17446
18063
  const replyDispatcher = new ReplyDispatcher({
17447
18064
  policies: replyPolicies,
@@ -17732,6 +18349,9 @@ async function startBridgeEngine(options) {
17732
18349
  approvals,
17733
18350
  permissionPolicies,
17734
18351
  notificationPreferences,
18352
+ notificationChannels,
18353
+ notificationSinks,
18354
+ faultNotifier: (scope, title, detail) => notificationDispatcher.notifyUrgent(scope, title, detail),
17735
18355
  ...defaultNotificationPreference ? { defaultNotificationPreference: { ...defaultNotificationPreference, events: [...defaultNotificationPreference.events] } } : {},
17736
18356
  replyPolicies,
17737
18357
  executionModes,
@@ -17994,6 +18614,7 @@ function notificationPreferenceFor(value) {
17994
18614
  return {
17995
18615
  events: value === "all" ? ["completed", "failed", "approval"] : ["completed", "failed"],
17996
18616
  mentionUserIds: [],
18617
+ sinks: [],
17997
18618
  approvalReminderMs: 10 * 6e4
17998
18619
  };
17999
18620
  }