dsh-lark-bot 0.19.12 → 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
@@ -817,14 +817,14 @@ var init_acp_adapter = __esm({
817
817
  return;
818
818
  }
819
819
  if (response.usage) {
820
- const usage3 = {
820
+ const usage4 = {
821
821
  type: "usage",
822
822
  ...typeof response.usage.inputTokens === "number" ? { inputTokens: response.usage.inputTokens } : {},
823
823
  ...typeof response.usage.outputTokens === "number" ? { outputTokens: response.usage.outputTokens } : {},
824
824
  ...typeof response.usage.cachedReadTokens === "number" ? { cacheReadTokens: response.usage.cachedReadTokens } : {},
825
825
  ...typeof response.usage.cachedWriteTokens === "number" ? { cacheWriteTokens: response.usage.cachedWriteTokens } : {}
826
826
  };
827
- channel.push(usage3);
827
+ channel.push(usage4);
828
828
  }
829
829
  })().catch((error) => {
830
830
  if (!stopRequested.value) {
@@ -2253,11 +2253,11 @@ function translateToolResult(data) {
2253
2253
  }
2254
2254
  function translateAssistantMessage(data) {
2255
2255
  if (!isRecord3(data) || !isRecord3(data.usage)) return [];
2256
- const usage3 = data.usage;
2257
- const inputTokens = typeof usage3.inputTokens === "number" ? usage3.inputTokens : void 0;
2258
- const outputTokens = typeof usage3.outputTokens === "number" ? usage3.outputTokens : void 0;
2259
- const cacheReadTokens = typeof usage3.cacheReadTokens === "number" ? usage3.cacheReadTokens : void 0;
2260
- 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;
2261
2261
  if (inputTokens === void 0 && outputTokens === void 0 && cacheReadTokens === void 0 && cacheWriteTokens === void 0) return [];
2262
2262
  return [
2263
2263
  {
@@ -4009,7 +4009,7 @@ var PermissionPolicyStore = class {
4009
4009
 
4010
4010
  // src/bot/notification-preference-store.ts
4011
4011
  import { readFile as readFile7 } from "fs/promises";
4012
- var EVENTS = /* @__PURE__ */ new Set(["completed", "failed", "approval"]);
4012
+ var EVENTS = /* @__PURE__ */ new Set(["completed", "failed", "approval", "urgent"]);
4013
4013
  var NotificationPreferenceStore = class {
4014
4014
  constructor(path) {
4015
4015
  this.path = path;
@@ -4038,7 +4038,7 @@ var NotificationPreferenceStore = class {
4038
4038
  get(scope) {
4039
4039
  const value = this.data.scopes[scope];
4040
4040
  if (value === void 0 || value === false) return void 0;
4041
- return { ...value, events: [...value.events], mentionUserIds: [...value.mentionUserIds] };
4041
+ return { ...value, events: [...value.events], mentionUserIds: [...value.mentionUserIds], sinks: [...value.sinks] };
4042
4042
  }
4043
4043
  resolve(scope, fallback) {
4044
4044
  const value = this.data.scopes[scope];
@@ -4083,6 +4083,7 @@ function normalizePreference(value) {
4083
4083
  ...typeof input.target === "string" && input.target.trim() ? { target: input.target.trim() } : {},
4084
4084
  events,
4085
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()))],
4086
4087
  approvalReminderMs
4087
4088
  };
4088
4089
  }
@@ -4559,6 +4560,7 @@ function statusCardMarkdownFor(input, locale) {
4559
4560
  zh ? `\u{1F39A}\uFE0F **\u6267\u884C\u6A21\u5F0F**\uFF1A\`${input.executionMode ?? "balanced"}\`` : `\u{1F39A}\uFE0F **Execution mode**: \`${input.executionMode ?? "balanced"}\``,
4560
4561
  zh ? `\u{1F6E1}\uFE0F **\u5DE5\u5177\u6743\u9650**\uFF1A\`${input.permissionPolicy ?? "ask"}\`` : `\u{1F6E1}\uFE0F **Tool permission**: \`${input.permissionPolicy ?? "ask"}\``,
4561
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(",")}\``] : [],
4562
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`] : [],
4563
4565
  ...input.role ? [`\u{1F3AD} **role**\uFF1A${input.role}`] : [],
4564
4566
  `\u{1F516} **version**\uFF1A\`${input.version}\``,
@@ -4975,8 +4977,8 @@ function renderStandard(state, now, locale, maxTools) {
4975
4977
  } else if (state.terminal === "done" && !hasAnswer(state)) {
4976
4978
  elements.push(noteMd(zh ? "_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_" : "_(No content returned)_"));
4977
4979
  } else if (state.terminal === "done") {
4978
- const usage3 = usageLine(state);
4979
- if (usage3) elements.push(noteMd(usage3));
4980
+ const usage4 = usageLine(state);
4981
+ if (usage4) elements.push(noteMd(usage4));
4980
4982
  }
4981
4983
  if (state.finalDeliveryError) {
4982
4984
  elements.push(finalDeliveryFailureLine(locale));
@@ -5042,8 +5044,8 @@ function renderDetailed(state, now, locale, maxTools) {
5042
5044
  } else if (state.terminal === "error") {
5043
5045
  elements.push(runFailureLine(locale));
5044
5046
  } else if (state.terminal === "done") {
5045
- const usage3 = usageLine(state);
5046
- if (usage3) elements.push(noteMd(usage3));
5047
+ const usage4 = usageLine(state);
5048
+ if (usage4) elements.push(noteMd(usage4));
5047
5049
  if (!hasAnswer(state)) elements.push(noteMd(zh ? "_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_" : "_(No content returned)_"));
5048
5050
  }
5049
5051
  if (state.finalDeliveryError) {
@@ -8066,7 +8068,7 @@ async function handleNotify(args, ctx) {
8066
8068
  }
8067
8069
 
8068
8070
  // src/commands/notifications.ts
8069
- var ALL_EVENTS = ["completed", "failed", "approval"];
8071
+ var ALL_EVENTS = ["completed", "failed", "approval", "urgent"];
8070
8072
  async function reply5(ctx, zh, en) {
8071
8073
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
8072
8074
  }
@@ -8104,6 +8106,7 @@ async function handleNotifications(args, ctx) {
8104
8106
  let events = [...ALL_EVENTS];
8105
8107
  let mentionUserIds = ctx.senderId ? [ctx.senderId] : [];
8106
8108
  let reminderMinutes = 10;
8109
+ let sinks = [];
8107
8110
  for (const token of tokens.slice(1)) {
8108
8111
  if (token.startsWith("events=")) {
8109
8112
  const requested = token.slice(7).split(",").filter(Boolean);
@@ -8115,6 +8118,8 @@ async function handleNotifications(args, ctx) {
8115
8118
  } else if (token.startsWith("remind=")) {
8116
8119
  reminderMinutes = Number(token.slice(7));
8117
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);
8118
8123
  } else if (!target) {
8119
8124
  target = token === "current" ? void 0 : token;
8120
8125
  } else return usage(ctx);
@@ -8133,50 +8138,297 @@ async function handleNotifications(args, ctx) {
8133
8138
  ...target ? { target } : {},
8134
8139
  events,
8135
8140
  mentionUserIds: [...new Set(mentionUserIds)],
8136
- approvalReminderMs: reminderMinutes * 6e4
8141
+ approvalReminderMs: reminderMinutes * 6e4,
8142
+ sinks: [...new Set(sinks)]
8137
8143
  };
8138
8144
  await store.set(ctx.scope, value);
8139
8145
  await reply5(ctx, `\u5DF2\u5F00\u542F\u3002${describe(value, true)}`, `Enabled. ${describe(value, false)}`);
8140
8146
  }
8141
8147
  function describe(value, zh) {
8142
8148
  const minutes = value.approvalReminderMs / 6e4;
8143
- 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.`;
8144
8151
  }
8145
8152
  async function usage(ctx) {
8146
8153
  await reply5(
8147
8154
  ctx,
8148
- "\u7528\u6CD5\uFF1A`/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval] [mentions=self,ou_x|none] [remind=10]]`",
8149
- "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]]`"
8150
8157
  );
8151
8158
  }
8152
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>`"
8384
+ );
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
+ }
8404
+
8153
8405
  // src/commands/replies.ts
8154
8406
  async function handleReplies(args, ctx) {
8155
8407
  const store = ctx.replyPolicies;
8156
- 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.");
8157
8409
  const tokens = args.trim().split(/\s+/).filter(Boolean);
8158
- 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));
8159
- 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.");
8160
8412
  if (tokens[0] === "default") {
8161
8413
  await store.set(ctx.scope, void 0);
8162
- 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.");
8163
8415
  }
8164
- if (tokens[0] !== "set") return usage2(ctx);
8416
+ if (tokens[0] !== "set") return usage3(ctx);
8165
8417
  const policy = { ...store.get(ctx.scope) };
8166
8418
  for (const token of tokens.slice(1)) {
8167
8419
  const [key, raw, extra] = token.split("=");
8168
- if (!raw || extra !== void 0) return usage2(ctx);
8420
+ if (!raw || extra !== void 0) return usage3(ctx);
8169
8421
  const seconds = Number(raw);
8170
- if (!Number.isInteger(seconds)) return usage2(ctx);
8422
+ if (!Number.isInteger(seconds)) return usage3(ctx);
8171
8423
  if (key === "merge" && seconds >= 0 && seconds <= 300) policy.mergeWindowMs = seconds * 1e3;
8172
8424
  else if (key === "batch" && seconds >= 1 && seconds <= 20) policy.maxBatchSize = seconds;
8173
8425
  else if (key === "interval" && seconds >= 0 && seconds <= 3600) policy.minIntervalMs = seconds * 1e3;
8174
8426
  else if (key === "dedupe" && seconds >= 0 && seconds <= 3600) policy.dedupeWindowMs = seconds * 1e3;
8175
- else return usage2(ctx);
8427
+ else return usage3(ctx);
8176
8428
  }
8177
- if (tokens.length === 1) return usage2(ctx);
8429
+ if (tokens.length === 1) return usage3(ctx);
8178
8430
  await store.set(ctx.scope, policy);
8179
- 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)}`);
8180
8432
  }
8181
8433
  async function canManageReplies(ctx) {
8182
8434
  if (ctx.accessManager.isAdmin(ctx.senderId)) return true;
@@ -8190,10 +8442,10 @@ async function canManageReplies(ctx) {
8190
8442
  function describe2(policy, configured, zh) {
8191
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.`;
8192
8444
  }
8193
- async function usage2(ctx) {
8194
- 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]`");
8195
8447
  }
8196
- async function reply6(ctx, zh, en) {
8448
+ async function reply7(ctx, zh, en) {
8197
8449
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
8198
8450
  }
8199
8451
 
@@ -8428,7 +8680,8 @@ var COMMAND_CATALOG = [
8428
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)" },
8429
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" },
8430
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)" },
8431
- { 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)" },
8432
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" },
8433
8686
  { names: ["/retention"], area: "session", zh: "`[N|default]` \u2014 \u4F1A\u8BDD\u4FDD\u7559\u6761\u6570", en: "`[N|default]` \u2014 retained live-message count" },
8434
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" },
@@ -8511,6 +8764,7 @@ var HELP = [
8511
8764
  "- `/notify <scope|chatId> <text>` \u2014 \u8DE8\u4F1A\u8BDD\u53D1\u9001\u901A\u77E5\uFF08\u7BA1\u7406\u5458\uFF09",
8512
8765
  "- `/notify list` \u2014 \u67E5\u770B\u5DF2\u6CE8\u518C scope",
8513
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",
8514
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",
8515
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",
8516
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",
@@ -8572,7 +8826,7 @@ var HELP_EN = [
8572
8826
  "- `/invite user|admin|group <id>` \u2014 manage access allowlists",
8573
8827
  "- `/help` \u2014 show this help"
8574
8828
  ].join("\n");
8575
- async function reply7(ctx, zhCn, enUs) {
8829
+ async function reply8(ctx, zhCn, enUs) {
8576
8830
  await ctx.channel.sendMarkdown(ctx.chatId, enUs === void 0 ? zhCn : bilingualMarkdown(zhCn, enUs, ctx.languagePolicies?.get().plain), {
8577
8831
  replyTo: ctx.messageId
8578
8832
  });
@@ -8581,7 +8835,7 @@ async function handleNew(_args, ctx) {
8581
8835
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8582
8836
  const interrupted = await ctx.activeRuns.interruptWorkspace(ctx.scope, cwd);
8583
8837
  ctx.sessions.clear(ctx.scope, cwd);
8584
- await reply7(
8838
+ await reply8(
8585
8839
  ctx,
8586
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",
8587
8841
  interrupted > 0 ? `Interrupted ${String(interrupted)} task(s) and started a new session.` : "Started a new session."
@@ -8610,11 +8864,11 @@ function groupAppLink(chatId) {
8610
8864
  async function handleNewGroup(args, ctx) {
8611
8865
  const name2 = args.trim();
8612
8866
  if (!name2) {
8613
- 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");
8614
8868
  return;
8615
8869
  }
8616
8870
  if (name2.length > MAX_GROUP_NAME_LENGTH) {
8617
- await reply7(
8871
+ await reply8(
8618
8872
  ctx,
8619
8873
  `\u7FA4\u540D\u8FC7\u957F\uFF08\u4E0A\u9650 ${String(MAX_GROUP_NAME_LENGTH)} \u5B57\u7B26\uFF0C\u5F53\u524D ${String(name2.length)}\uFF09\u3002`,
8620
8874
  `The group name is too long (limit ${String(MAX_GROUP_NAME_LENGTH)} characters; received ${String(name2.length)}).`
@@ -8622,11 +8876,11 @@ async function handleNewGroup(args, ctx) {
8622
8876
  return;
8623
8877
  }
8624
8878
  if (!ctx.channel.createChat) {
8625
- 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.");
8626
8880
  return;
8627
8881
  }
8628
8882
  if (!ctx.senderId) {
8629
- 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.");
8630
8884
  return;
8631
8885
  }
8632
8886
  try {
@@ -8637,7 +8891,7 @@ async function handleNewGroup(args, ctx) {
8637
8891
  inviteUserIds: [ctx.senderId],
8638
8892
  userIdType: "open_id"
8639
8893
  });
8640
- await reply7(
8894
+ await reply8(
8641
8895
  ctx,
8642
8896
  [
8643
8897
  `\u2705 \u5DF2\u521B\u5EFA\u7FA4\u804A\uFF1A**${name2}**`,
@@ -8656,13 +8910,13 @@ async function handleNewGroup(args, ctx) {
8656
8910
  );
8657
8911
  } catch (error) {
8658
8912
  const message = error instanceof Error ? error.message : String(error);
8659
- 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}\``);
8660
8914
  }
8661
8915
  }
8662
8916
  async function handleCd(args, ctx) {
8663
8917
  const path = args.trim();
8664
8918
  if (!path) {
8665
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`", "Usage: `/cd <path>`");
8919
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`", "Usage: `/cd <path>`");
8666
8920
  return;
8667
8921
  }
8668
8922
  const normalized = path.replace(/^~/u, "~");
@@ -8670,13 +8924,13 @@ async function handleCd(args, ctx) {
8670
8924
  const cwd = resolve3(expanded);
8671
8925
  const target = await stat2(cwd).catch(() => void 0);
8672
8926
  if (!target?.isDirectory()) {
8673
- 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}\`.`);
8674
8928
  return;
8675
8929
  }
8676
8930
  const previous = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8677
8931
  const interrupted = cwd === previous ? 0 : await ctx.activeRuns.interruptWorkspace(ctx.scope, previous);
8678
8932
  ctx.workspaces.setCwd(ctx.scope, cwd);
8679
- await reply7(
8933
+ await reply8(
8680
8934
  ctx,
8681
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` : ""}`,
8682
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).` : ""}`
@@ -8696,7 +8950,7 @@ async function handleWs(args, ctx) {
8696
8950
  const lines = Object.entries(named).map(
8697
8951
  ([key, value]) => `- **${key}** \u2192 \`${value}\`${value === current ? " \u2190 \u5F53\u524D" : ""}`
8698
8952
  );
8699
- await reply7(
8953
+ await reply8(
8700
8954
  ctx,
8701
8955
  [
8702
8956
  `\u5F53\u524D cwd\uFF1A\`${current}\``,
@@ -8713,29 +8967,29 @@ async function handleWs(args, ctx) {
8713
8967
  }
8714
8968
  if (sub === "save") {
8715
8969
  if (!name2) {
8716
- 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>`");
8717
8971
  return;
8718
8972
  }
8719
8973
  const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8720
8974
  ctx.workspaces.saveNamed(name2, current);
8721
- 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}\``);
8722
8976
  return;
8723
8977
  }
8724
8978
  if (sub === "use") {
8725
8979
  if (!name2) {
8726
- 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>`");
8727
8981
  return;
8728
8982
  }
8729
8983
  const cwd = ctx.workspaces.getNamed(name2);
8730
8984
  if (!cwd) {
8731
- 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}**`);
8732
8986
  return;
8733
8987
  }
8734
8988
  const previous = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8735
8989
  const interrupted = cwd === previous ? 0 : await ctx.activeRuns.interruptWorkspace(ctx.scope, previous);
8736
8990
  ctx.workspaces.setCwd(ctx.scope, cwd);
8737
8991
  ctx.workspaces.touchNamed(name2);
8738
- await reply7(
8992
+ await reply8(
8739
8993
  ctx,
8740
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` : ""}`,
8741
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).` : ""}`
@@ -8744,14 +8998,14 @@ async function handleWs(args, ctx) {
8744
8998
  }
8745
8999
  if (sub === "remove") {
8746
9000
  if (!name2) {
8747
- 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>`");
8748
9002
  return;
8749
9003
  }
8750
9004
  const removed = ctx.workspaces.removeNamed(name2);
8751
- 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}**`);
8752
9006
  return;
8753
9007
  }
8754
- 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.");
8755
9009
  }
8756
9010
  async function statusCardInputFor(ctx) {
8757
9011
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -8798,6 +9052,7 @@ async function statusCardInputFor(ctx) {
8798
9052
  permissionPolicy: ctx.permissionPolicies?.get(ctx.scope) ?? "ask",
8799
9053
  executionMode: ctx.executionModes?.get(ctx.scope) ?? "balanced",
8800
9054
  ...notificationPreference ? { notificationPreference } : {},
9055
+ ...ctx.notificationSinks ? { notificationChannels: ctx.notificationSinks.enabledChannels().map((channel) => channel.id) } : {},
8801
9056
  ...replyPolicy ? { replyPolicy, replyPolicyConfigured: ctx.replyPolicies?.isConfigured(ctx.scope) ?? false } : {},
8802
9057
  role: role ? `\`${role.id}\` (${role.name})` : void 0,
8803
9058
  metrics,
@@ -8840,16 +9095,16 @@ async function handleStatus(_args, ctx) {
8840
9095
  } catch {
8841
9096
  }
8842
9097
  }
8843
- await reply7(ctx, bilingualMarkdown(statusCardMarkdown(input), statusCardMarkdownEnglish(input)));
9098
+ await reply8(ctx, bilingualMarkdown(statusCardMarkdown(input), statusCardMarkdownEnglish(input)));
8844
9099
  }
8845
9100
  async function handleIsolation(args, ctx) {
8846
9101
  if (ctx.chatMode === "p2p") {
8847
- 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.");
8848
9103
  return;
8849
9104
  }
8850
9105
  const mode = args.trim().toLowerCase();
8851
9106
  if (!mode) {
8852
- await reply7(
9107
+ await reply8(
8853
9108
  ctx,
8854
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`,
8855
9110
  `Current group isolation mode: **${ctx.isolationStore?.get(ctx.chatId) ?? "topic"}**. Use \`/isolation group|topic|member\` to switch.`
@@ -8857,16 +9112,16 @@ async function handleIsolation(args, ctx) {
8857
9112
  return;
8858
9113
  }
8859
9114
  if (mode !== "group" && mode !== "topic" && mode !== "member") {
8860
- 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`");
8861
9116
  return;
8862
9117
  }
8863
- if (!requireAdmin3(ctx)) return;
9118
+ if (!requireAdmin4(ctx)) return;
8864
9119
  if (!ctx.isolationStore) {
8865
- 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.");
8866
9121
  return;
8867
9122
  }
8868
9123
  ctx.isolationStore.set(ctx.chatId, mode);
8869
- await reply7(
9124
+ await reply8(
8870
9125
  ctx,
8871
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`,
8872
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.`
@@ -8893,25 +9148,25 @@ async function handleVersion(_args, ctx) {
8893
9148
  lines.push("\u6700\u65B0\u7248\u672C\u67E5\u8BE2\u6682\u4E0D\u53EF\u7528\uFF08\u7F51\u7EDC / registry \u5F02\u5E38\uFF09\u3002");
8894
9149
  english.push("Latest-version lookup is unavailable (network or registry error).");
8895
9150
  }
8896
- await reply7(ctx, lines.join("\n"), english.join("\n"));
9151
+ await reply8(ctx, lines.join("\n"), english.join("\n"));
8897
9152
  }
8898
9153
  async function handleUpgrade(_args, ctx) {
8899
- if (!requireAdmin3(ctx)) return;
9154
+ if (!requireAdmin4(ctx)) return;
8900
9155
  if (!ctx.senderId || !ctx.channelUpdates) {
8901
- 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.");
8902
9157
  return;
8903
9158
  }
8904
9159
  const result = await ctx.channelUpdates.check({ scope: ctx.scope, actorId: ctx.senderId });
8905
9160
  if (result.kind === "unavailable") {
8906
- 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.");
8907
9162
  return;
8908
9163
  }
8909
9164
  if (result.kind === "current") {
8910
- 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}\`.`);
8911
9166
  return;
8912
9167
  }
8913
9168
  if (!ctx.channel.sendCard) {
8914
- 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.");
8915
9170
  return;
8916
9171
  }
8917
9172
  await ctx.channel.sendCard(ctx.chatId, renderUpdateCard({
@@ -8926,9 +9181,9 @@ async function handleUpgrade(_args, ctx) {
8926
9181
  });
8927
9182
  }
8928
9183
  async function handleDoctor(_args, ctx) {
8929
- if (!requireAdmin3(ctx)) return;
9184
+ if (!requireAdmin4(ctx)) return;
8930
9185
  if (!ctx.createDiagnosticBundle || !ctx.channel.sendFile) {
8931
- 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.");
8932
9187
  return;
8933
9188
  }
8934
9189
  let file;
@@ -8945,7 +9200,7 @@ async function handleDoctor(_args, ctx) {
8945
9200
  ...status.jobs ? { jobs: status.jobs } : {}
8946
9201
  }), ctx.diagnosticTimeoutMs?.generate ?? DOCTOR_GENERATE_TIMEOUT_MS);
8947
9202
  } catch {
8948
- await reply7(
9203
+ await reply8(
8949
9204
  ctx,
8950
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",
8951
9206
  "\u26A0\uFE0F Failed to generate the diagnostic bundle. Retry later; if it keeps failing, run `dsh-lark-bot doctor` locally."
@@ -8959,21 +9214,21 @@ async function handleDoctor(_args, ctx) {
8959
9214
  }), ctx.diagnosticTimeoutMs?.upload ?? DOCTOR_UPLOAD_TIMEOUT_MS);
8960
9215
  } catch (error) {
8961
9216
  if (error instanceof OperationTimeoutError) {
8962
- await reply7(
9217
+ await reply8(
8963
9218
  ctx,
8964
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",
8965
9220
  "\u26A0\uFE0F Diagnostic upload timed out with an unknown result: the file may still arrive later. Check this chat before retrying."
8966
9221
  );
8967
9222
  return;
8968
9223
  }
8969
- await reply7(
9224
+ await reply8(
8970
9225
  ctx,
8971
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",
8972
9227
  "\u26A0\uFE0F Failed to send the diagnostic bundle. Retry later; if it keeps failing, run `dsh-lark-bot doctor` locally."
8973
9228
  );
8974
9229
  return;
8975
9230
  }
8976
- await reply7(
9231
+ await reply8(
8977
9232
  ctx,
8978
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",
8979
9234
  "\u2705 The redacted diagnostic bundle was generated and sent. You can download and forward it to a maintainer."
@@ -8983,18 +9238,18 @@ async function handleResume(_args, ctx) {
8983
9238
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
8984
9239
  const history = ctx.sessions.historyFor(ctx.scope, cwd);
8985
9240
  if (history.length === 0) {
8986
- 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.");
8987
9242
  return;
8988
9243
  }
8989
9244
  const recent = history.slice(-6).map((message) => {
8990
9245
  const speaker = message.role === "user" ? "\u{1F464}" : "\u{1F916}";
8991
9246
  return `${speaker} ${message.content.slice(0, 300)}`;
8992
9247
  });
8993
- 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"));
8994
9249
  }
8995
9250
  async function handleJobs(args, ctx) {
8996
9251
  if (!ctx.jobs) {
8997
- 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.");
8998
9253
  return;
8999
9254
  }
9000
9255
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -9002,11 +9257,11 @@ async function handleJobs(args, ctx) {
9002
9257
  const records = ctx.jobs.list(ctx.scope, cwd, 20);
9003
9258
  if (sub === "retry") {
9004
9259
  if (!id) {
9005
- 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>`");
9006
9261
  return;
9007
9262
  }
9008
9263
  const queued = await ctx.requeueJob?.(id, ctx.scope, cwd);
9009
- await reply7(
9264
+ await reply8(
9010
9265
  ctx,
9011
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`,
9012
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.`
@@ -9016,19 +9271,19 @@ async function handleJobs(args, ctx) {
9016
9271
  if (sub === "show") {
9017
9272
  const record = id ? ctx.jobs.get(id, ctx.scope, cwd) : void 0;
9018
9273
  if (!record) {
9019
- 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 ?? ""}**`);
9020
9275
  return;
9021
9276
  }
9022
- await reply7(ctx, jobDetail(record, "zh"), jobDetail(record, "en"));
9277
+ await reply8(ctx, jobDetail(record, "zh"), jobDetail(record, "en"));
9023
9278
  return;
9024
9279
  }
9025
9280
  if (sub && sub !== "list") {
9026
- 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>]`");
9027
9282
  return;
9028
9283
  }
9029
9284
  const counts = ctx.jobs.counts(ctx.scope, cwd);
9030
9285
  const rows = records.slice(0, 10);
9031
- await reply7(
9286
+ await reply8(
9032
9287
  ctx,
9033
9288
  [
9034
9289
  "**\u4EFB\u52A1\u5BF9\u8D26**",
@@ -9075,7 +9330,7 @@ async function handleStop(_args, ctx) {
9075
9330
  });
9076
9331
  let stopped = 0;
9077
9332
  for (const scope of scopes) stopped += await ctx.activeRuns.interrupt(scope);
9078
- await reply7(
9333
+ await reply8(
9079
9334
  ctx,
9080
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",
9081
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."
@@ -9086,7 +9341,7 @@ async function handleTimeout(args, ctx) {
9086
9341
  const effectiveMs = ctx.runPolicies.get(ctx.scope) ?? ctx.defaultRunTimeoutMs;
9087
9342
  if (!input) {
9088
9343
  const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
9089
- await reply7(
9344
+ await reply8(
9090
9345
  ctx,
9091
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",
9092
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."
@@ -9095,27 +9350,27 @@ async function handleTimeout(args, ctx) {
9095
9350
  }
9096
9351
  if (input === "off") {
9097
9352
  ctx.runPolicies.set(ctx.scope, 0);
9098
- 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.");
9099
9354
  return;
9100
9355
  }
9101
9356
  if (input === "default") {
9102
9357
  ctx.runPolicies.clear(ctx.scope);
9103
- 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.");
9104
9359
  return;
9105
9360
  }
9106
9361
  const minutes = Number(input);
9107
9362
  if (!Number.isInteger(minutes) || minutes <= 0) {
9108
- 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.");
9109
9364
  return;
9110
9365
  }
9111
9366
  ctx.runPolicies.set(ctx.scope, minutes * 6e4);
9112
- 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.`);
9113
9368
  }
9114
9369
  async function handleConcurrency(args, ctx) {
9115
9370
  const input = args.trim();
9116
9371
  const effective = ctx.concurrencyStore.get(ctx.scope) ?? ctx.defaultScopeConcurrency;
9117
9372
  if (!input) {
9118
- await reply7(
9373
+ await reply8(
9119
9374
  ctx,
9120
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`,
9121
9376
  `Parallel tasks for this scope: **${String(effective)}**. Use \`/concurrency <N|default>\` to change it (N \u2265 1).`
@@ -9124,16 +9379,16 @@ async function handleConcurrency(args, ctx) {
9124
9379
  }
9125
9380
  if (input === "default") {
9126
9381
  ctx.concurrencyStore.clear(ctx.scope);
9127
- 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)}).`);
9128
9383
  return;
9129
9384
  }
9130
9385
  const n = Number(input);
9131
9386
  if (!Number.isInteger(n) || n < 1) {
9132
- 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.");
9133
9388
  return;
9134
9389
  }
9135
9390
  ctx.concurrencyStore.set(ctx.scope, n);
9136
- 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)}**.`);
9137
9392
  }
9138
9393
  async function handlePermission(args, ctx) {
9139
9394
  const [rawPolicy = "", rawTarget = "", ...extra] = args.trim().split(/\s+/);
@@ -9145,7 +9400,7 @@ async function handlePermission(args, ctx) {
9145
9400
  };
9146
9401
  if (!input) {
9147
9402
  const current = ctx.permissionPolicies?.get(ctx.scope) ?? "ask";
9148
- await reply7(
9403
+ await reply8(
9149
9404
  ctx,
9150
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`,
9151
9406
  `Tool permission policy for this scope: **${labels[current].en}** (\`${current}\`). Use \`/permission ask|allow|deny\` to change it (admin only).`
@@ -9153,21 +9408,21 @@ async function handlePermission(args, ctx) {
9153
9408
  return;
9154
9409
  }
9155
9410
  if (input !== "ask" && input !== "allow" && input !== "deny" || extra.length > 0) {
9156
- 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]`");
9157
9412
  return;
9158
9413
  }
9159
- if (!requireAdmin3(ctx)) return;
9414
+ if (!requireAdmin4(ctx)) return;
9160
9415
  if (!ctx.permissionPolicies) {
9161
- 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.");
9162
9417
  return;
9163
9418
  }
9164
9419
  const targetScope = rawTarget || ctx.scope;
9165
9420
  if (targetScope !== ctx.chatId && !targetScope.startsWith(`${ctx.chatId}:`)) {
9166
- 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`.");
9167
9422
  return;
9168
9423
  }
9169
9424
  await ctx.permissionPolicies.set(targetScope, input);
9170
- await reply7(
9425
+ await reply8(
9171
9426
  ctx,
9172
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`,
9173
9428
  `Set scope \`${targetScope}\`'s tool permission policy to **${labels[input].en}** (\`${input}\`). It applies to the next tool approval; plan approval remains enforced.`
@@ -9177,7 +9432,7 @@ async function handleDensity(args, ctx) {
9177
9432
  const input = args.trim().toLowerCase();
9178
9433
  if (!input) {
9179
9434
  const current = ctx.densityStore?.get(ctx.scope) ?? "standard";
9180
- await reply7(
9435
+ await reply8(
9181
9436
  ctx,
9182
9437
  `\u5F53\u524D\u5361\u7247\u5BC6\u5EA6\uFF1A**${current}**\u3002\u53EF\u7528 \`/density compact|standard|detailed\` \u8C03\u6574\u3002`,
9183
9438
  `Current card density: **${current}**. Use \`/density compact|standard|detailed\` to change it.`
@@ -9186,21 +9441,21 @@ async function handleDensity(args, ctx) {
9186
9441
  }
9187
9442
  if (input === "default") {
9188
9443
  ctx.densityStore?.clear(ctx.scope);
9189
- 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.");
9190
9445
  return;
9191
9446
  }
9192
9447
  const density = parseCardDensity(input);
9193
9448
  if (!density) {
9194
- 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]`");
9195
9450
  return;
9196
9451
  }
9197
9452
  ctx.densityStore?.set(ctx.scope, density);
9198
- 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}**.`);
9199
9454
  }
9200
9455
  async function handleExecutionMode(args, ctx) {
9201
9456
  const input = args.trim().toLowerCase();
9202
9457
  if (!ctx.executionModes) {
9203
- 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.");
9204
9459
  return;
9205
9460
  }
9206
9461
  if (!input) {
@@ -9217,7 +9472,7 @@ async function handleExecutionMode(args, ctx) {
9217
9472
  } catch {
9218
9473
  }
9219
9474
  }
9220
- await reply7(
9475
+ await reply8(
9221
9476
  ctx,
9222
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`,
9223
9478
  `Current execution mode: **${current}**. Use \`/mode quick|balanced|deep\`; changes apply next turn.`
@@ -9225,18 +9480,18 @@ async function handleExecutionMode(args, ctx) {
9225
9480
  return;
9226
9481
  }
9227
9482
  if (input !== "quick" && input !== "balanced" && input !== "deep") {
9228
- 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]`");
9229
9484
  return;
9230
9485
  }
9231
9486
  await ctx.executionModes.set(ctx.scope, input);
9232
- await reply7(
9487
+ await reply8(
9233
9488
  ctx,
9234
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`,
9235
9490
  `Set this session\u2019s execution mode to **${input}** for the next turn. Active work and existing context are unchanged.`
9236
9491
  );
9237
9492
  }
9238
9493
  async function handleEffortAlias(args, ctx) {
9239
- await reply7(
9494
+ await reply8(
9240
9495
  ctx,
9241
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",
9242
9497
  "`/effort` is a compatibility alias for `/mode`; it controls task execution intensity, not the model reasoning-effort setting."
@@ -9246,11 +9501,11 @@ async function handleEffortAlias(args, ctx) {
9246
9501
  async function handleAsk(args, ctx) {
9247
9502
  const question = args.trim();
9248
9503
  if (!question) {
9249
- 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>`");
9250
9505
  return;
9251
9506
  }
9252
9507
  if (!ctx.questions) {
9253
- 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).");
9254
9509
  return;
9255
9510
  }
9256
9511
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -9272,18 +9527,18 @@ async function handleAsk(args, ctx) {
9272
9527
  if (answer !== void 0) {
9273
9528
  const text = Array.isArray(answer) ? answer.join("\u3001") : answer;
9274
9529
  ctx.sessions.recordExchange(ctx.scope, cwd, [text], void 0);
9275
- 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.");
9276
9531
  } else {
9277
- 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).");
9278
9533
  }
9279
9534
  }
9280
9535
  async function handleInvite(args, ctx) {
9281
9536
  const [kind, ...rest] = args.trim().split(/\s+/);
9282
9537
  const id = rest.join(" ").trim();
9283
- if (kind !== "list" && !requireAdmin3(ctx)) return;
9538
+ if (kind !== "list" && !requireAdmin4(ctx)) return;
9284
9539
  if (kind === "list") {
9285
9540
  const snapshot = ctx.accessManager.snapshot();
9286
- await reply7(
9541
+ await reply8(
9287
9542
  ctx,
9288
9543
  [
9289
9544
  "**\u8BBF\u95EE\u767D\u540D\u5355**",
@@ -9301,7 +9556,7 @@ async function handleInvite(args, ctx) {
9301
9556
  return;
9302
9557
  }
9303
9558
  if (!kind || !id) {
9304
- await reply7(
9559
+ await reply8(
9305
9560
  ctx,
9306
9561
  "\u7528\u6CD5\uFF1A`/invite user|admin|group <id>`\u3001`/invite list`\u3001`/invite remove user|group <id>`",
9307
9562
  "Usage: `/invite user|admin|group <id>`, `/invite list`, or `/invite remove user|group <id>`"
@@ -9310,39 +9565,39 @@ async function handleInvite(args, ctx) {
9310
9565
  }
9311
9566
  if (kind === "user") {
9312
9567
  await ctx.accessManager.addUser(id);
9313
- 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}\``);
9314
9569
  return;
9315
9570
  }
9316
9571
  if (kind === "admin") {
9317
9572
  await ctx.accessManager.addAdmin(id);
9318
- 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}\``);
9319
9574
  return;
9320
9575
  }
9321
9576
  if (kind === "group") {
9322
9577
  await ctx.accessManager.addChat(id);
9323
- 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}\``);
9324
9579
  return;
9325
9580
  }
9326
9581
  if (kind === "remove") {
9327
9582
  const [sub, target] = rest;
9328
9583
  if (sub === "user" && target) {
9329
9584
  await ctx.accessManager.removeUser(target);
9330
- 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}\``);
9331
9586
  return;
9332
9587
  }
9333
9588
  if (sub === "group" && target) {
9334
9589
  await ctx.accessManager.removeChat(target);
9335
- 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}\``);
9336
9591
  return;
9337
9592
  }
9338
- 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>`");
9339
9594
  return;
9340
9595
  }
9341
- 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.");
9342
9597
  }
9343
- function requireAdmin3(ctx) {
9598
+ function requireAdmin4(ctx) {
9344
9599
  if (!ctx.accessManager.isAdmin(ctx.senderId)) {
9345
- 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.");
9346
9601
  return false;
9347
9602
  }
9348
9603
  return true;
@@ -9395,20 +9650,20 @@ async function handleKeyDispatch(args, ctx) {
9395
9650
  await handleKey(args, ctx);
9396
9651
  }
9397
9652
  async function handleHelp(_args, ctx) {
9398
- await reply7(ctx, renderCommandHelp("zh"), renderCommandHelp("en"));
9653
+ await reply8(ctx, renderCommandHelp("zh"), renderCommandHelp("en"));
9399
9654
  }
9400
9655
  async function handleLanguage(args, ctx) {
9401
9656
  if (!ctx.languagePolicies) {
9402
- 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.");
9403
9658
  return;
9404
9659
  }
9405
9660
  const [action = "show", field, value] = args.trim().split(/\s+/);
9406
9661
  if (action === "show") {
9407
9662
  const policy = ctx.languagePolicies.get();
9408
- 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}`);
9409
9664
  return;
9410
9665
  }
9411
- if (!requireAdmin3(ctx)) return;
9666
+ if (!requireAdmin4(ctx)) return;
9412
9667
  if (action === "set" && field === "plain" && (value === "bilingual" || value === "zh" || value === "en")) {
9413
9668
  await ctx.languagePolicies.set({ plain: value });
9414
9669
  } else if (action === "set" && field === "agent" && (value === "auto" || value === "zh" || value === "en")) {
@@ -9416,7 +9671,7 @@ async function handleLanguage(args, ctx) {
9416
9671
  } else if (action === "reset" && (field === void 0 || field === "all" || field === "plain" || field === "agent")) {
9417
9672
  await ctx.languagePolicies.reset(field ?? "all");
9418
9673
  } else {
9419
- 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]`");
9420
9675
  return;
9421
9676
  }
9422
9677
  process.env.DSH_LARK_REPLY_LANG = ctx.languagePolicies.get().plain;
@@ -9427,37 +9682,37 @@ function parseSecretTarget(value) {
9427
9682
  }
9428
9683
  async function handleSecret(args, ctx) {
9429
9684
  if (!ctx.secretService) {
9430
- 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.");
9431
9686
  return;
9432
9687
  }
9433
9688
  const [action, targetRaw, referenceRaw, ...purposeParts] = args.trim().split(/\s+/);
9434
9689
  const target = parseSecretTarget(targetRaw);
9435
9690
  const reference = referenceRaw?.trim();
9436
9691
  if (!target || !reference || !["status", "set", "remove"].includes(action ?? "")) {
9437
- 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>`");
9438
9693
  return;
9439
9694
  }
9440
- if (action !== "status" && !requireAdmin3(ctx)) return;
9695
+ if (action !== "status" && !requireAdmin4(ctx)) return;
9441
9696
  if (action === "status") {
9442
9697
  const configured = await ctx.secretService.configured(target, reference);
9443
- 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"}`);
9444
9699
  return;
9445
9700
  }
9446
9701
  if (action === "remove") {
9447
9702
  const removed = await ctx.secretService.remove(target, reference);
9448
- 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.");
9449
9704
  return;
9450
9705
  }
9451
9706
  if (!ctx.senderId) {
9452
- 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.");
9453
9708
  return;
9454
9709
  }
9455
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" });
9456
- 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.");
9457
9712
  }
9458
9713
  async function handleSessionProjection(args, ctx) {
9459
9714
  if (!ctx.sessionProjection) {
9460
- await reply7(
9715
+ await reply8(
9461
9716
  ctx,
9462
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",
9463
9718
  "The current adapter does not provide DSH session projection; use the `web` adapter and ensure the Web host is reachable."
@@ -9487,6 +9742,7 @@ var handlers = {
9487
9742
  "/role": handleRole,
9488
9743
  "/notify": handleNotify,
9489
9744
  "/notifications": handleNotifications,
9745
+ "/channels": handleChannels,
9490
9746
  "/replies": handleReplies,
9491
9747
  "/retention": handleRetention,
9492
9748
  "/archive": handleArchive,
@@ -9966,24 +10222,26 @@ var GroupMessagePoller = class {
9966
10222
 
9967
10223
  // src/bridge/reconnect-notifier.ts
9968
10224
  var ReconnectNotifier = class {
9969
- constructor(channel, directory, now = Date.now, reconciliation) {
10225
+ constructor(channel, directory, now = Date.now, reconciliation, onFault) {
9970
10226
  this.channel = channel;
9971
10227
  this.directory = directory;
9972
10228
  this.now = now;
9973
10229
  this.reconciliation = reconciliation;
10230
+ this.onFault = onFault;
9974
10231
  }
9975
10232
  channel;
9976
10233
  directory;
9977
10234
  now;
9978
10235
  reconciliation;
10236
+ onFault;
9979
10237
  startedAt;
9980
10238
  async reconnecting() {
9981
10239
  if (this.startedAt !== void 0) return;
9982
10240
  this.startedAt = this.now();
9983
- await this.send(
9984
- "\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",
9985
- "\u26A0\uFE0F The bot connection is unstable and reconnecting automatically. New messages may be delayed."
9986
- );
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 });
9987
10245
  }
9988
10246
  async reconnected() {
9989
10247
  if (this.startedAt === void 0) return;
@@ -9991,12 +10249,12 @@ var ReconnectNotifier = class {
9991
10249
  this.startedAt = void 0;
9992
10250
  const target = this.directory?.recentDestination();
9993
10251
  const summary = target ? this.reconciliation?.(target.scope) : void 0;
9994
- await this.send(
9995
- `\u2705 \u673A\u5668\u4EBA\u8FDE\u63A5\u5DF2\u6062\u590D\uFF08\u4E2D\u65AD\u7EA6 ${formatDuration(elapsed)}\uFF09\u3002${summary ? `
9996
- ${summary.zhCn}` : ""}`,
9997
- `\u2705 The bot connection recovered after about ${formatDurationEnglish(elapsed)}.${summary ? `
9998
- ${summary.enUs}` : ""}`
9999
- );
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 });
10000
10258
  }
10001
10259
  async send(zhCn, enUs) {
10002
10260
  const target = this.directory?.recentDestination();
@@ -10006,6 +10264,13 @@ ${summary.enUs}` : ""}`
10006
10264
  ...target.threadId ? { threadId: target.threadId } : {}
10007
10265
  });
10008
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
+ }
10009
10274
  };
10010
10275
  function formatDurationEnglish(ms) {
10011
10276
  if (ms < 1e3) return "less than 1 second";
@@ -11208,7 +11473,8 @@ async function startChannel(deps) {
11208
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`,
11209
11474
  enUs: `Ledger reconciliation: queued ${counts.queued} \xB7 running ${counts.running} \xB7 interrupted ${counts.interrupted} \xB7 failed ${counts.failed}; use \`/jobs\` for details.`
11210
11475
  };
11211
- } : void 0
11476
+ } : void 0,
11477
+ deps.faultNotifier
11212
11478
  );
11213
11479
  const isolationStore = deps.isolationStore ?? EMPTY_ISOLATION_STORE;
11214
11480
  let groupPoller;
@@ -11343,6 +11609,8 @@ async function startChannel(deps) {
11343
11609
  ...deps.permissionPolicies ? { permissionPolicies: deps.permissionPolicies } : {},
11344
11610
  ...deps.notificationPreferences ? { notificationPreferences: deps.notificationPreferences } : {},
11345
11611
  ...deps.defaultNotificationPreference ? { defaultNotificationPreference: deps.defaultNotificationPreference } : {},
11612
+ ...deps.notificationChannels ? { notificationChannels: deps.notificationChannels } : {},
11613
+ ...deps.notificationSinks ? { notificationSinks: deps.notificationSinks } : {},
11346
11614
  ...deps.replyPolicies ? { replyPolicies: deps.replyPolicies } : {},
11347
11615
  ...deps.executionModes ? { executionModes: deps.executionModes } : {},
11348
11616
  ...deps.languagePolicies ? { languagePolicies: deps.languagePolicies } : {},
@@ -12065,7 +12333,7 @@ function attachRunCardAnchors(channel, anchors) {
12065
12333
  }
12066
12334
 
12067
12335
  // src/bridge/scope-directory.ts
12068
- import { readFile as readFile11 } from "fs/promises";
12336
+ import { readFile as readFile12 } from "fs/promises";
12069
12337
  var ScopeDirectory = class {
12070
12338
  data = { entries: {} };
12071
12339
  saving = Promise.resolve();
@@ -12075,7 +12343,7 @@ var ScopeDirectory = class {
12075
12343
  }
12076
12344
  async load() {
12077
12345
  try {
12078
- const raw = await readFile11(this.path, "utf8");
12346
+ const raw = await readFile12(this.path, "utf8");
12079
12347
  const parsed = JSON.parse(raw);
12080
12348
  this.data = { entries: parsed.entries ?? {} };
12081
12349
  } catch (error) {
@@ -12816,6 +13084,11 @@ var NotificationDispatcher = class {
12816
13084
  setDefaultPreference(preference) {
12817
13085
  this.defaultPreference = clonePreference(preference);
12818
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
+ */
12819
13092
  async notify(scope, event, detail) {
12820
13093
  const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
12821
13094
  if (!preference?.events.includes(event)) return false;
@@ -12824,21 +13097,52 @@ var NotificationDispatcher = class {
12824
13097
  log.warn("notification", "target-unavailable", { scope, target: preference.target });
12825
13098
  return false;
12826
13099
  }
12827
- 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);
12828
13101
  try {
12829
- await this.deps.send(destination.chatId, bilingualMarkdown(
12830
- `${title[0]}\uFF08scope \`${scope}\`\uFF09${detail ? `\uFF1A${detail}` : ""}`,
12831
- `${title[1]} (scope \`${scope}\`)${detail ? `: ${detail}` : ""}`
12832
- ), {
13102
+ await this.deps.send(destination.chatId, renderFeishuBody(title, scope, detail), {
12833
13103
  ...destination.threadId ? { threadId: destination.threadId } : {},
12834
13104
  ...destination.messageId ? { replyTo: destination.messageId } : {},
12835
13105
  ...preference.mentionUserIds.length > 0 ? { mentions: preference.mentionUserIds.map((userId) => ({ userId })) } : {}
12836
13106
  });
12837
- return true;
12838
13107
  } catch (error) {
12839
13108
  log.warn("notification", "send-failed", { scope, event, error });
12840
13109
  return false;
12841
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
+ }
12842
13146
  }
12843
13147
  scheduleApprovalReminder(scope, toolName) {
12844
13148
  const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
@@ -12849,11 +13153,197 @@ var NotificationDispatcher = class {
12849
13153
  timer.unref?.();
12850
13154
  return () => clearTimeout(timer);
12851
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
+ }
12852
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
+ }
12853
13191
  function clonePreference(preference) {
12854
- 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;
12855
13193
  }
12856
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
+
12857
13347
  // src/bridge/reply-dispatcher.ts
12858
13348
  var ReplyDispatcher = class {
12859
13349
  constructor(deps) {
@@ -12912,8 +13402,8 @@ var ReplyDispatcher = class {
12912
13402
  }
12913
13403
  }
12914
13404
  };
12915
- function replyRoute(reply8) {
12916
- return `${reply8.chatId}\0${reply8.options?.threadId ?? ""}`;
13405
+ function replyRoute(reply9) {
13406
+ return `${reply9.chatId}\0${reply9.options?.threadId ?? ""}`;
12917
13407
  }
12918
13408
  function mergedMarkdown(batch) {
12919
13409
  if (batch.length === 1) return batch[0].markdown;
@@ -12964,6 +13454,7 @@ function resolveAppPaths(root = defaultHome()) {
12964
13454
  jobsFile: (profile) => profilePath(profile, "jobs.json"),
12965
13455
  permissionPoliciesFile: (profile) => profilePath(profile, "permission-policies.json"),
12966
13456
  notificationPreferencesFile: (profile) => profilePath(profile, "notification-preferences.json"),
13457
+ notificationChannelsFile: (profile) => profilePath(profile, "notification-channels.json"),
12967
13458
  replyPoliciesFile: (profile) => profilePath(profile, "reply-policies.json"),
12968
13459
  executionModesFile: (profile) => profilePath(profile, "execution-modes.json"),
12969
13460
  languagePoliciesFile: (profile) => profilePath(profile, "language-policy.json"),
@@ -13317,11 +13808,11 @@ function loadRuntimeEnv(source = process.env) {
13317
13808
  }
13318
13809
 
13319
13810
  // src/config/profile-store.ts
13320
- import { mkdir as mkdir7, readFile as readFile13 } from "fs/promises";
13811
+ import { mkdir as mkdir7, readFile as readFile14 } from "fs/promises";
13321
13812
  import { dirname as dirname8 } from "path";
13322
13813
 
13323
13814
  // src/platform/file-lock.ts
13324
- 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";
13325
13816
  import { randomUUID as randomUUID10 } from "crypto";
13326
13817
  import { dirname as dirname7, join as join12 } from "path";
13327
13818
  var RETRY_MS = 10;
@@ -13405,7 +13896,7 @@ async function olderThan(path, thresholdMs) {
13405
13896
  }
13406
13897
  async function readLease(path) {
13407
13898
  try {
13408
- const value = JSON.parse(await readFile12(path, "utf8"));
13899
+ const value = JSON.parse(await readFile13(path, "utf8"));
13409
13900
  return typeof value.pid === "number" && typeof value.token === "string" && typeof value.createdAt === "number" ? value : void 0;
13410
13901
  } catch {
13411
13902
  return void 0;
@@ -13433,7 +13924,7 @@ var ConfigStore = class {
13433
13924
  return this.data;
13434
13925
  }
13435
13926
  try {
13436
- const raw = await readFile13(this.path, "utf8");
13927
+ const raw = await readFile14(this.path, "utf8");
13437
13928
  const parsed = JSON.parse(raw);
13438
13929
  this.data = {
13439
13930
  schemaVersion: 1,
@@ -13601,7 +14092,7 @@ async function onboardPersonalAgent(deps = {}) {
13601
14092
  }
13602
14093
 
13603
14094
  // src/media/attachments.ts
13604
- 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";
13605
14096
  import { join as join13 } from "path";
13606
14097
  var MAX_TEXT_FILE_BYTES = 256e3;
13607
14098
  function assertSafeMediaName(mediaDir, destination) {
@@ -13642,7 +14133,7 @@ async function prepareAttachments(channel, message, mediaDir) {
13642
14133
  result.textFileNotes.push(`[attachment: ${resource.fileName ?? resource.fileKey}] ${destination}`);
13643
14134
  continue;
13644
14135
  }
13645
- const content2 = await readFile14(destination, "utf8");
14136
+ const content2 = await readFile15(destination, "utf8");
13646
14137
  const safeContent = truncateUtf8Safe(content2, MAX_TEXT_FILE_BYTES);
13647
14138
  result.textFileNotes.push(
13648
14139
  `[attachment: ${resource.fileName ?? resource.fileKey}]
@@ -13653,7 +14144,7 @@ ${safeContent}`
13653
14144
  }
13654
14145
 
13655
14146
  // src/session/store.ts
13656
- import { readFile as readFile15 } from "fs/promises";
14147
+ import { readFile as readFile16 } from "fs/promises";
13657
14148
  var MAX_CONTEXT_SNAPSHOTS_PER_WORKSPACE = 32;
13658
14149
  var SessionStore = class {
13659
14150
  constructor(path) {
@@ -13668,7 +14159,7 @@ var SessionStore = class {
13668
14159
  pendingArchive = Promise.resolve();
13669
14160
  async load() {
13670
14161
  try {
13671
- const raw = JSON.parse(await readFile15(this.path, "utf8"));
14162
+ const raw = JSON.parse(await readFile16(this.path, "utf8"));
13672
14163
  const legacy = raw.schema !== 2 || !isObject(raw.workspaces);
13673
14164
  this.data = legacy ? migrateLegacy(raw) : normalizeV2(raw.workspaces);
13674
14165
  this.legacyScopes = legacy ? new Set(Object.keys(this.data.workspaces)) : /* @__PURE__ */ new Set();
@@ -13765,13 +14256,13 @@ var SessionStore = class {
13765
14256
  fullHistoryFor(scopeId, cwd) {
13766
14257
  return this.historyFor(scopeId, cwd);
13767
14258
  }
13768
- recordUsage(scopeId, cwd, usage3) {
14259
+ recordUsage(scopeId, cwd, usage4) {
13769
14260
  const state = this.ensureState(scopeId, cwd);
13770
14261
  const next = { ...state.metrics };
13771
- addMetric(next, "inputTokens", usage3.inputTokens);
13772
- addMetric(next, "outputTokens", usage3.outputTokens);
13773
- addMetric(next, "cacheReadTokens", usage3.cacheReadTokens);
13774
- 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);
13775
14266
  state.metrics = next;
13776
14267
  this.schedulePersist();
13777
14268
  }
@@ -13987,7 +14478,7 @@ function normalizeContextSnapshot(value) {
13987
14478
  }
13988
14479
 
13989
14480
  // src/session/projection-store.ts
13990
- import { readFile as readFile16 } from "fs/promises";
14481
+ import { readFile as readFile17 } from "fs/promises";
13991
14482
  import { randomUUID as randomUUID11 } from "crypto";
13992
14483
  var MAX_RECENT_MESSAGES = 64;
13993
14484
  var MAX_PROMPT_CORRELATIONS = 64;
@@ -14001,7 +14492,7 @@ var SessionProjectionStore = class {
14001
14492
  saving = Promise.resolve();
14002
14493
  async load() {
14003
14494
  try {
14004
- const parsed = JSON.parse(await readFile16(this.path, "utf8"));
14495
+ const parsed = JSON.parse(await readFile17(this.path, "utf8"));
14005
14496
  const bindings = Object.fromEntries(
14006
14497
  Object.values(parsed.bindings ?? {}).flatMap((value) => {
14007
14498
  const normalized = normalizeBinding(value);
@@ -14213,7 +14704,7 @@ function normalizeCorrelation(value) {
14213
14704
  // src/session/archive.ts
14214
14705
  import { execFile } from "child_process";
14215
14706
  import { randomBytes as randomBytes3 } from "crypto";
14216
- 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";
14217
14708
  import { join as join14 } from "path";
14218
14709
  import { promisify } from "util";
14219
14710
  var execFileAsync = promisify(execFile);
@@ -14395,8 +14886,8 @@ var SessionArchive = class {
14395
14886
  const jsonlPath = join14(scopeDir, `${id}.jsonl`);
14396
14887
  const markdownPath = join14(scopeDir, `${id}.md`);
14397
14888
  const [jsonl, markdown2] = await Promise.all([
14398
- readFile17(jsonlPath, "utf8"),
14399
- readFile17(markdownPath, "utf8")
14889
+ readFile18(jsonlPath, "utf8"),
14890
+ readFile18(markdownPath, "utf8")
14400
14891
  ]);
14401
14892
  const [first = "", ...rest] = jsonl.split("\n");
14402
14893
  const header = JSON.parse(first);
@@ -14511,7 +15002,7 @@ async function defaultRunGit(args, cwd) {
14511
15002
  }
14512
15003
  async function readArchiveHeader(jsonlPath) {
14513
15004
  try {
14514
- const raw = await readFile17(jsonlPath, "utf8");
15005
+ const raw = await readFile18(jsonlPath, "utf8");
14515
15006
  const firstLine = raw.split("\n", 1)[0];
14516
15007
  if (!firstLine) return void 0;
14517
15008
  const header = JSON.parse(firstLine);
@@ -14561,7 +15052,7 @@ async function defaultCopyRulesFile(source, target) {
14561
15052
  await mkdir10(dirname9(target), { recursive: true });
14562
15053
  await copyFile(source, target);
14563
15054
  }
14564
- function slugify(scope) {
15055
+ function slugify2(scope) {
14565
15056
  const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[.]+/, "").replace(/^-+|-+$/g, "").slice(0, 64);
14566
15057
  return slug || "session";
14567
15058
  }
@@ -14577,7 +15068,7 @@ var GitWorktreeManager = class {
14577
15068
  async ensure(scope, base) {
14578
15069
  const isGit = await this.isGitRepository(base);
14579
15070
  if (!isGit) return { cwd: base, created: false };
14580
- const scopeSlug = slugify(scope);
15071
+ const scopeSlug = slugify2(scope);
14581
15072
  const project = createHash2("sha256").update(base).digest("hex").slice(0, 10);
14582
15073
  const slug = `${scopeSlug}-${project}`;
14583
15074
  const target = join15(this.options.worktreesRoot, slug);
@@ -14614,7 +15105,7 @@ var GitWorktreeManager = class {
14614
15105
  }
14615
15106
  /** Owning main worktree for the legacy scope-only linked worktree. */
14616
15107
  async legacyWorkspaceBase(scope) {
14617
- const legacyTarget = join15(this.options.worktreesRoot, slugify(scope));
15108
+ const legacyTarget = join15(this.options.worktreesRoot, slugify2(scope));
14618
15109
  if (!await exists2(legacyTarget)) return void 0;
14619
15110
  try {
14620
15111
  const output = await this.runGit(["worktree", "list", "--porcelain"], legacyTarget);
@@ -14646,7 +15137,7 @@ async function samePath(left, right) {
14646
15137
  }
14647
15138
 
14648
15139
  // src/workspace/store.ts
14649
- import { readFile as readFile18 } from "fs/promises";
15140
+ import { readFile as readFile19 } from "fs/promises";
14650
15141
  var WorkspaceStore = class {
14651
15142
  data = { chats: {}, named: {}, lastUsed: {} };
14652
15143
  saving = Promise.resolve();
@@ -14656,7 +15147,7 @@ var WorkspaceStore = class {
14656
15147
  }
14657
15148
  async load() {
14658
15149
  try {
14659
- const raw = await readFile18(this.path, "utf8");
15150
+ const raw = await readFile19(this.path, "utf8");
14660
15151
  const parsed = JSON.parse(raw);
14661
15152
  this.data = {
14662
15153
  chats: parsed.chats ?? {},
@@ -14728,7 +15219,7 @@ var WorkspaceStore = class {
14728
15219
  };
14729
15220
 
14730
15221
  // src/guardian/heartbeat.ts
14731
- import { readFile as readFile19 } from "fs/promises";
15222
+ import { readFile as readFile20 } from "fs/promises";
14732
15223
  var DEFAULT_HEARTBEAT_INTERVAL_MS = 5e3;
14733
15224
  function startHeartbeat(file, pid, intervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, getChannelHealth) {
14734
15225
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -14809,7 +15300,7 @@ var UpdateNotifier = class {
14809
15300
  };
14810
15301
 
14811
15302
  // src/bot/fleet-store.ts
14812
- import { mkdir as mkdir11, readFile as readFile20 } from "fs/promises";
15303
+ import { mkdir as mkdir11, readFile as readFile21 } from "fs/promises";
14813
15304
  import { dirname as dirname10 } from "path";
14814
15305
  function validBotInstanceName(name2) {
14815
15306
  return /^[a-z][a-z0-9-]{0,31}$/u.test(name2);
@@ -14822,7 +15313,7 @@ var BotFleetStore = class {
14822
15313
  data = { schemaVersion: 1, bots: {} };
14823
15314
  async load() {
14824
15315
  try {
14825
- const parsed = JSON.parse(await readFile20(this.path, "utf8"));
15316
+ const parsed = JSON.parse(await readFile21(this.path, "utf8"));
14826
15317
  this.data = {
14827
15318
  schemaVersion: 1,
14828
15319
  bots: Object.fromEntries(
@@ -14950,7 +15441,7 @@ function isFleetEntry(value) {
14950
15441
  }
14951
15442
 
14952
15443
  // src/bot/handoff-guard.ts
14953
- import { mkdir as mkdir12, readFile as readFile21 } from "fs/promises";
15444
+ import { mkdir as mkdir12, readFile as readFile22 } from "fs/promises";
14954
15445
  import { dirname as dirname11 } from "path";
14955
15446
  var BotHandoffGuard = class {
14956
15447
  constructor(path) {
@@ -14991,7 +15482,7 @@ var BotHandoffGuard = class {
14991
15482
  }
14992
15483
  async read() {
14993
15484
  try {
14994
- const parsed = JSON.parse(await readFile21(this.path, "utf8"));
15485
+ const parsed = JSON.parse(await readFile22(this.path, "utf8"));
14995
15486
  return { schemaVersion: 1, chats: parsed.chats ?? {} };
14996
15487
  } catch (error) {
14997
15488
  if (error.code !== "ENOENT") throw error;
@@ -15017,7 +15508,7 @@ init_dsh_runtime();
15017
15508
  import { homedir as homedir14 } from "os";
15018
15509
 
15019
15510
  // src/bot/job-ledger.ts
15020
- import { readFile as readFile22, rename as rename3 } from "fs/promises";
15511
+ import { readFile as readFile23, rename as rename3 } from "fs/promises";
15021
15512
  var EMPTY_COUNTS = {
15022
15513
  queued: 0,
15023
15514
  running: 0,
@@ -15040,7 +15531,7 @@ var JobLedger = class {
15040
15531
  if (this.path === ":memory:") return;
15041
15532
  let raw;
15042
15533
  try {
15043
- raw = await readFile22(this.path, "utf8");
15534
+ raw = await readFile23(this.path, "utf8");
15044
15535
  } catch (error) {
15045
15536
  if (error.code === "ENOENT") return;
15046
15537
  throw error;
@@ -15616,13 +16107,13 @@ function projectDiagnosticLogs(lines) {
15616
16107
  }
15617
16108
 
15618
16109
  // src/service/manager.ts
15619
- import { mkdir as mkdir19, readFile as readFile27, rename as rename4, rm as rm9, stat as stat8 } from "fs/promises";
16110
+ import { mkdir as mkdir19, readFile as readFile28, rename as rename4, rm as rm9, stat as stat8 } from "fs/promises";
15620
16111
  import { dirname as dirname17, join as join22 } from "path";
15621
16112
  import { homedir as homedir13 } from "os";
15622
16113
  init_dsh_runtime();
15623
16114
 
15624
16115
  // src/guardian/install.ts
15625
- 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";
15626
16117
  import { existsSync as existsSync5 } from "fs";
15627
16118
  import { homedir as homedir9 } from "os";
15628
16119
  import { dirname as dirname14, join as join16 } from "path";
@@ -15813,7 +16304,7 @@ async function findProfileProcess(dshProfile) {
15813
16304
  }
15814
16305
 
15815
16306
  // src/guardian/state.ts
15816
- import { readFile as readFile23 } from "fs/promises";
16307
+ import { readFile as readFile24 } from "fs/promises";
15817
16308
  import { dirname as dirname13 } from "path";
15818
16309
  import { mkdir as mkdir13 } from "fs/promises";
15819
16310
 
@@ -15862,19 +16353,19 @@ function resolveCliJsPath(metaUrl = import.meta.url) {
15862
16353
  if (name2 === "cli.js" || name2 === "cli.mjs" || name2 === "cli.cjs") return current;
15863
16354
  return join17(dirname15(current), "..", "dist", "cli.js");
15864
16355
  }
15865
- function slugify2(value) {
16356
+ function slugify3(value) {
15866
16357
  const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
15867
16358
  return slug || "default";
15868
16359
  }
15869
16360
  function serviceNameFor(profile) {
15870
- return `dsh-lark-engine-${slugify2(profile)}`;
16361
+ return `dsh-lark-engine-${slugify3(profile)}`;
15871
16362
  }
15872
16363
  function launchdLabelFor(profile) {
15873
- return `io.dsh-lark.engine.${slugify2(profile)}`;
16364
+ return `io.dsh-lark.engine.${slugify3(profile)}`;
15874
16365
  }
15875
16366
 
15876
16367
  // src/service/env-snapshot.ts
15877
- import { readFile as readFile25 } from "fs/promises";
16368
+ import { readFile as readFile26 } from "fs/promises";
15878
16369
  var EXTRA_KEYS = ["PATH", "HOME", "DEEPSEEK_API_KEY", "DSH_HOME"];
15879
16370
  var INTERNAL_KEYS = /* @__PURE__ */ new Set([
15880
16371
  "DSH_LARK_NOTIFY_URL",
@@ -15946,7 +16437,7 @@ async function writeServiceEnv(file, env) {
15946
16437
  }
15947
16438
  async function readServiceEnv(file) {
15948
16439
  try {
15949
- return parseEnvFile(await readFile25(file, "utf8"));
16440
+ return parseEnvFile(await readFile26(file, "utf8"));
15950
16441
  } catch (error) {
15951
16442
  if (error.code === "ENOENT") return {};
15952
16443
  throw error;
@@ -16258,7 +16749,7 @@ var LaunchdServiceController = class {
16258
16749
 
16259
16750
  // src/service/portable.ts
16260
16751
  import { spawn as spawn6 } from "child_process";
16261
- 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";
16262
16753
  import { homedir as homedir12 } from "os";
16263
16754
  import { join as join20 } from "path";
16264
16755
  function defaultAutostartDir() {
@@ -16293,8 +16784,8 @@ function buildDesktopEntry(spec) {
16293
16784
  async function readLinuxProcessIdentity(pid) {
16294
16785
  try {
16295
16786
  const [statText, cmdlineText] = await Promise.all([
16296
- readFile26(`/proc/${pid}/stat`, "utf8"),
16297
- readFile26(`/proc/${pid}/cmdline`, "utf8")
16787
+ readFile27(`/proc/${pid}/stat`, "utf8"),
16788
+ readFile27(`/proc/${pid}/cmdline`, "utf8")
16298
16789
  ]);
16299
16790
  const close = statText.lastIndexOf(") ");
16300
16791
  if (close < 0) return void 0;
@@ -16325,7 +16816,7 @@ function isProcessAlive(pid) {
16325
16816
  }
16326
16817
  async function readSupervisorStatus(file) {
16327
16818
  try {
16328
- return JSON.parse(await readFile26(file, "utf8"));
16819
+ return JSON.parse(await readFile27(file, "utf8"));
16329
16820
  } catch {
16330
16821
  return void 0;
16331
16822
  }
@@ -16700,7 +17191,7 @@ var ServiceManager = class {
16700
17191
  async readIntent() {
16701
17192
  try {
16702
17193
  const parsed = JSON.parse(
16703
- await readFile27(this.paths.serviceIntentFile(this.profile), "utf8")
17194
+ await readFile28(this.paths.serviceIntentFile(this.profile), "utf8")
16704
17195
  );
16705
17196
  if (parsed.schemaVersion !== 1 || !["running", "stopped"].includes(parsed.desiredState ?? "")) {
16706
17197
  return void 0;
@@ -16727,7 +17218,7 @@ var ServiceManager = class {
16727
17218
  if (error.code !== "EEXIST") throw error;
16728
17219
  let ownerPid;
16729
17220
  try {
16730
- const owner = JSON.parse(await readFile27(join22(lockDir, "owner.json"), "utf8"));
17221
+ const owner = JSON.parse(await readFile28(join22(lockDir, "owner.json"), "utf8"));
16731
17222
  if (typeof owner.pid === "number") ownerPid = owner.pid;
16732
17223
  } catch {
16733
17224
  }
@@ -16881,7 +17372,7 @@ var ServiceManager = class {
16881
17372
  }
16882
17373
  async readMetadata() {
16883
17374
  try {
16884
- const parsed = JSON.parse(await readFile27(this.metadataFile(), "utf8"));
17375
+ const parsed = JSON.parse(await readFile28(this.metadataFile(), "utf8"));
16885
17376
  if (parsed.schemaVersion !== 2 || typeof parsed.profile !== "string") return void 0;
16886
17377
  return parsed;
16887
17378
  } catch {
@@ -16983,7 +17474,7 @@ var ServiceManager = class {
16983
17474
  async logs(lines = 100) {
16984
17475
  const path = this.paths.serviceLogFile(this.profile);
16985
17476
  try {
16986
- const content2 = await readFile27(path, "utf8");
17477
+ const content2 = await readFile28(path, "utf8");
16987
17478
  return { path, text: content2.split(/\r?\n/).slice(-Math.max(1, lines)).join("\n") };
16988
17479
  } catch (error) {
16989
17480
  if (error.code === "ENOENT") return { path, text: "" };
@@ -17158,7 +17649,7 @@ init_own_package();
17158
17649
  // src/guardian/update-handoff.ts
17159
17650
  import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
17160
17651
  import { spawn as spawn7 } from "child_process";
17161
- import { chmod, mkdir as mkdir20, readFile as readFile28, rm as rm10 } from "fs/promises";
17652
+ import { chmod, mkdir as mkdir20, readFile as readFile29, rm as rm10 } from "fs/promises";
17162
17653
  import { dirname as dirname18, join as join23 } from "path";
17163
17654
  init_own_package();
17164
17655
  function guardianUpdateFailureHint(code) {
@@ -17187,7 +17678,7 @@ function guardianUpdateFailureHint(code) {
17187
17678
  }
17188
17679
  async function loadState(file) {
17189
17680
  try {
17190
- const parsed = JSON.parse(await readFile28(file, "utf8"));
17681
+ const parsed = JSON.parse(await readFile29(file, "utf8"));
17191
17682
  return parsed.schemaVersion === 1 && typeof parsed.id === "string" ? parsed : void 0;
17192
17683
  } catch (error) {
17193
17684
  if (error.code === "ENOENT") return void 0;
@@ -17472,6 +17963,8 @@ async function startBridgeEngine(options) {
17472
17963
  const isolationStore = new IsolationStore(paths.profilePath(profileName, "isolation.json"));
17473
17964
  const permissionPolicies = new PermissionPolicyStore(paths.permissionPoliciesFile(profileName));
17474
17965
  const notificationPreferences = new NotificationPreferenceStore(paths.notificationPreferencesFile(profileName));
17966
+ const notificationChannels = new NotificationChannelStore(paths.notificationChannelsFile(profileName));
17967
+ const notificationSinks = new OutboundSinkRegistry(notificationChannels);
17475
17968
  const replyPolicies = new ReplyPolicyStore(paths.replyPoliciesFile(profileName));
17476
17969
  const executionModes = new ExecutionModeStore(paths.executionModesFile(profileName));
17477
17970
  const languagePolicies = new LanguagePolicyStore(paths.languagePoliciesFile(profileName));
@@ -17494,6 +17987,7 @@ async function startBridgeEngine(options) {
17494
17987
  isolationStore.load(),
17495
17988
  permissionPolicies.load(),
17496
17989
  notificationPreferences.load(),
17990
+ notificationChannels.load(),
17497
17991
  replyPolicies.load(),
17498
17992
  executionModes.load(),
17499
17993
  languagePolicies.load()
@@ -17563,7 +18057,8 @@ async function startBridgeEngine(options) {
17563
18057
  send: async (chatId, markdown2, options2) => {
17564
18058
  if (!streaming) throw new Error("bridge channel is not ready");
17565
18059
  await streaming.sendMarkdown(chatId, markdown2, options2);
17566
- }
18060
+ },
18061
+ sinks: notificationSinks
17567
18062
  });
17568
18063
  const replyDispatcher = new ReplyDispatcher({
17569
18064
  policies: replyPolicies,
@@ -17854,6 +18349,9 @@ async function startBridgeEngine(options) {
17854
18349
  approvals,
17855
18350
  permissionPolicies,
17856
18351
  notificationPreferences,
18352
+ notificationChannels,
18353
+ notificationSinks,
18354
+ faultNotifier: (scope, title, detail) => notificationDispatcher.notifyUrgent(scope, title, detail),
17857
18355
  ...defaultNotificationPreference ? { defaultNotificationPreference: { ...defaultNotificationPreference, events: [...defaultNotificationPreference.events] } } : {},
17858
18356
  replyPolicies,
17859
18357
  executionModes,
@@ -18116,6 +18614,7 @@ function notificationPreferenceFor(value) {
18116
18614
  return {
18117
18615
  events: value === "all" ? ["completed", "failed", "approval"] : ["completed", "failed"],
18118
18616
  mentionUserIds: [],
18617
+ sinks: [],
18119
18618
  approvalReminderMs: 10 * 6e4
18120
18619
  };
18121
18620
  }