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/cli.js CHANGED
@@ -825,14 +825,14 @@ var init_acp_adapter = __esm({
825
825
  return;
826
826
  }
827
827
  if (response.usage) {
828
- const usage3 = {
828
+ const usage4 = {
829
829
  type: "usage",
830
830
  ...typeof response.usage.inputTokens === "number" ? { inputTokens: response.usage.inputTokens } : {},
831
831
  ...typeof response.usage.outputTokens === "number" ? { outputTokens: response.usage.outputTokens } : {},
832
832
  ...typeof response.usage.cachedReadTokens === "number" ? { cacheReadTokens: response.usage.cachedReadTokens } : {},
833
833
  ...typeof response.usage.cachedWriteTokens === "number" ? { cacheWriteTokens: response.usage.cachedWriteTokens } : {}
834
834
  };
835
- channel.push(usage3);
835
+ channel.push(usage4);
836
836
  }
837
837
  })().catch((error) => {
838
838
  if (!stopRequested.value) {
@@ -2502,11 +2502,11 @@ function translateToolResult(data) {
2502
2502
  }
2503
2503
  function translateAssistantMessage(data) {
2504
2504
  if (!isRecord3(data) || !isRecord3(data.usage)) return [];
2505
- const usage3 = data.usage;
2506
- const inputTokens = typeof usage3.inputTokens === "number" ? usage3.inputTokens : void 0;
2507
- const outputTokens = typeof usage3.outputTokens === "number" ? usage3.outputTokens : void 0;
2508
- const cacheReadTokens = typeof usage3.cacheReadTokens === "number" ? usage3.cacheReadTokens : void 0;
2509
- const cacheWriteTokens = typeof usage3.cacheWriteTokens === "number" ? usage3.cacheWriteTokens : void 0;
2505
+ const usage4 = data.usage;
2506
+ const inputTokens = typeof usage4.inputTokens === "number" ? usage4.inputTokens : void 0;
2507
+ const outputTokens = typeof usage4.outputTokens === "number" ? usage4.outputTokens : void 0;
2508
+ const cacheReadTokens = typeof usage4.cacheReadTokens === "number" ? usage4.cacheReadTokens : void 0;
2509
+ const cacheWriteTokens = typeof usage4.cacheWriteTokens === "number" ? usage4.cacheWriteTokens : void 0;
2510
2510
  if (inputTokens === void 0 && outputTokens === void 0 && cacheReadTokens === void 0 && cacheWriteTokens === void 0) return [];
2511
2511
  return [
2512
2512
  {
@@ -3682,6 +3682,7 @@ function resolveAppPaths(root = defaultHome()) {
3682
3682
  jobsFile: (profile) => profilePath(profile, "jobs.json"),
3683
3683
  permissionPoliciesFile: (profile) => profilePath(profile, "permission-policies.json"),
3684
3684
  notificationPreferencesFile: (profile) => profilePath(profile, "notification-preferences.json"),
3685
+ notificationChannelsFile: (profile) => profilePath(profile, "notification-channels.json"),
3685
3686
  replyPoliciesFile: (profile) => profilePath(profile, "reply-policies.json"),
3686
3687
  executionModesFile: (profile) => profilePath(profile, "execution-modes.json"),
3687
3688
  languagePoliciesFile: (profile) => profilePath(profile, "language-policy.json"),
@@ -6910,7 +6911,7 @@ var PermissionPolicyStore = class {
6910
6911
 
6911
6912
  // src/bot/notification-preference-store.ts
6912
6913
  import { readFile as readFile19 } from "fs/promises";
6913
- var EVENTS = /* @__PURE__ */ new Set(["completed", "failed", "approval"]);
6914
+ var EVENTS = /* @__PURE__ */ new Set(["completed", "failed", "approval", "urgent"]);
6914
6915
  var NotificationPreferenceStore = class {
6915
6916
  constructor(path) {
6916
6917
  this.path = path;
@@ -6939,7 +6940,7 @@ var NotificationPreferenceStore = class {
6939
6940
  get(scope) {
6940
6941
  const value = this.data.scopes[scope];
6941
6942
  if (value === void 0 || value === false) return void 0;
6942
- return { ...value, events: [...value.events], mentionUserIds: [...value.mentionUserIds] };
6943
+ return { ...value, events: [...value.events], mentionUserIds: [...value.mentionUserIds], sinks: [...value.sinks] };
6943
6944
  }
6944
6945
  resolve(scope, fallback) {
6945
6946
  const value = this.data.scopes[scope];
@@ -6984,6 +6985,7 @@ function normalizePreference(value) {
6984
6985
  ...typeof input.target === "string" && input.target.trim() ? { target: input.target.trim() } : {},
6985
6986
  events,
6986
6987
  mentionUserIds: [...new Set((input.mentionUserIds ?? []).filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))],
6988
+ sinks: [...new Set((input.sinks ?? []).filter((id) => typeof id === "string" && id.trim().length > 0).map((id) => id.trim()))],
6987
6989
  approvalReminderMs
6988
6990
  };
6989
6991
  }
@@ -7460,6 +7462,7 @@ function statusCardMarkdownFor(input, locale) {
7460
7462
  zh ? `\u{1F39A}\uFE0F **\u6267\u884C\u6A21\u5F0F**\uFF1A\`${input.executionMode ?? "balanced"}\`` : `\u{1F39A}\uFE0F **Execution mode**: \`${input.executionMode ?? "balanced"}\``,
7461
7463
  zh ? `\u{1F6E1}\uFE0F **\u5DE5\u5177\u6743\u9650**\uFF1A\`${input.permissionPolicy ?? "ask"}\`` : `\u{1F6E1}\uFE0F **Tool permission**: \`${input.permissionPolicy ?? "ask"}\``,
7462
7464
  ...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"],
7465
+ ...input.notificationChannels && input.notificationChannels.length > 0 ? [zh ? `\u{1F4E4} **\u51FA\u7AD9\u6E20\u9053**\uFF1A\`${input.notificationChannels.join(",")}\`` : `\u{1F4E4} **Outbound channels**: \`${input.notificationChannels.join(",")}\``] : [],
7463
7466
  ...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`] : [],
7464
7467
  ...input.role ? [`\u{1F3AD} **role**\uFF1A${input.role}`] : [],
7465
7468
  `\u{1F516} **version**\uFF1A\`${input.version}\``,
@@ -7876,8 +7879,8 @@ function renderStandard(state, now, locale, maxTools) {
7876
7879
  } else if (state.terminal === "done" && !hasAnswer(state)) {
7877
7880
  elements.push(noteMd(zh ? "_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_" : "_(No content returned)_"));
7878
7881
  } else if (state.terminal === "done") {
7879
- const usage3 = usageLine(state);
7880
- if (usage3) elements.push(noteMd(usage3));
7882
+ const usage4 = usageLine(state);
7883
+ if (usage4) elements.push(noteMd(usage4));
7881
7884
  }
7882
7885
  if (state.finalDeliveryError) {
7883
7886
  elements.push(finalDeliveryFailureLine(locale));
@@ -7943,8 +7946,8 @@ function renderDetailed(state, now, locale, maxTools) {
7943
7946
  } else if (state.terminal === "error") {
7944
7947
  elements.push(runFailureLine(locale));
7945
7948
  } else if (state.terminal === "done") {
7946
- const usage3 = usageLine(state);
7947
- if (usage3) elements.push(noteMd(usage3));
7949
+ const usage4 = usageLine(state);
7950
+ if (usage4) elements.push(noteMd(usage4));
7948
7951
  if (!hasAnswer(state)) elements.push(noteMd(zh ? "_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_" : "_(No content returned)_"));
7949
7952
  }
7950
7953
  if (state.finalDeliveryError) {
@@ -10967,7 +10970,7 @@ async function handleNotify(args, ctx) {
10967
10970
  }
10968
10971
 
10969
10972
  // src/commands/notifications.ts
10970
- var ALL_EVENTS = ["completed", "failed", "approval"];
10973
+ var ALL_EVENTS = ["completed", "failed", "approval", "urgent"];
10971
10974
  async function reply5(ctx, zh, en) {
10972
10975
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
10973
10976
  }
@@ -11005,6 +11008,7 @@ async function handleNotifications(args, ctx) {
11005
11008
  let events = [...ALL_EVENTS];
11006
11009
  let mentionUserIds = ctx.senderId ? [ctx.senderId] : [];
11007
11010
  let reminderMinutes = 10;
11011
+ let sinks = [];
11008
11012
  for (const token of tokens.slice(1)) {
11009
11013
  if (token.startsWith("events=")) {
11010
11014
  const requested = token.slice(7).split(",").filter(Boolean);
@@ -11016,6 +11020,8 @@ async function handleNotifications(args, ctx) {
11016
11020
  } else if (token.startsWith("remind=")) {
11017
11021
  reminderMinutes = Number(token.slice(7));
11018
11022
  if (!Number.isInteger(reminderMinutes) || reminderMinutes < 1 || reminderMinutes > 1440) return usage(ctx);
11023
+ } else if (token.startsWith("sinks=")) {
11024
+ sinks = token.slice(6).split(",").map((id) => id.trim()).filter(Boolean);
11019
11025
  } else if (!target) {
11020
11026
  target = token === "current" ? void 0 : token;
11021
11027
  } else return usage(ctx);
@@ -11034,50 +11040,297 @@ async function handleNotifications(args, ctx) {
11034
11040
  ...target ? { target } : {},
11035
11041
  events,
11036
11042
  mentionUserIds: [...new Set(mentionUserIds)],
11037
- approvalReminderMs: reminderMinutes * 6e4
11043
+ approvalReminderMs: reminderMinutes * 6e4,
11044
+ sinks: [...new Set(sinks)]
11038
11045
  };
11039
11046
  await store.set(ctx.scope, value);
11040
11047
  await reply5(ctx, `\u5DF2\u5F00\u542F\u3002${describe(value, true)}`, `Enabled. ${describe(value, false)}`);
11041
11048
  }
11042
11049
  function describe(value, zh) {
11043
11050
  const minutes = value.approvalReminderMs / 6e4;
11044
- 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.`;
11051
+ const sinks = value.sinks.join(",") || "none";
11052
+ 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.`;
11045
11053
  }
11046
11054
  async function usage(ctx) {
11047
11055
  await reply5(
11048
11056
  ctx,
11049
- "\u7528\u6CD5\uFF1A`/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval] [mentions=self,ou_x|none] [remind=10]]`",
11050
- "Usage: `/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval] [mentions=self,ou_x|none] [remind=10]]`"
11057
+ "\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]]`",
11058
+ "Usage: `/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval,urgent] [mentions=self,ou_x|none] [sinks=channelId1,channelId2] [remind=10]]`"
11051
11059
  );
11052
11060
  }
11053
11061
 
11062
+ // src/notify/sinks/channel-store.ts
11063
+ import { readFile as readFile23 } from "fs/promises";
11064
+ var SINK_TYPES = ["telegram", "wecom"];
11065
+ var NotificationChannelStore = class {
11066
+ constructor(path) {
11067
+ this.path = path;
11068
+ }
11069
+ path;
11070
+ data = { schemaVersion: 1, channels: {} };
11071
+ saving = Promise.resolve();
11072
+ async load() {
11073
+ try {
11074
+ const parsed = JSON.parse(await readFile23(this.path, "utf8"));
11075
+ const channels = {};
11076
+ for (const [id, value] of Object.entries(parsed.channels ?? {})) {
11077
+ const normalized = normalizeChannel(id, value);
11078
+ if (normalized) channels[id] = normalized;
11079
+ }
11080
+ this.data = { schemaVersion: 1, channels };
11081
+ } catch (error) {
11082
+ if (error.code !== "ENOENT") throw error;
11083
+ this.data = { schemaVersion: 1, channels: {} };
11084
+ }
11085
+ }
11086
+ list() {
11087
+ return Object.values(this.data.channels).map((channel) => ({ ...channel }));
11088
+ }
11089
+ get(id) {
11090
+ const channel = this.data.channels[id];
11091
+ return channel ? { ...channel } : void 0;
11092
+ }
11093
+ async add(channel) {
11094
+ const normalized = normalizeChannel(channel.id, channel);
11095
+ if (!normalized) throw new Error("invalid notification channel");
11096
+ await this.mutate(() => {
11097
+ this.data.channels[normalized.id] = normalized;
11098
+ });
11099
+ }
11100
+ async update(id, patch) {
11101
+ const existing = this.data.channels[id];
11102
+ if (!existing) return false;
11103
+ const next = normalizeChannel(id, { ...existing, ...patch });
11104
+ if (!next) return false;
11105
+ await this.mutate(() => {
11106
+ this.data.channels[id] = next;
11107
+ });
11108
+ return true;
11109
+ }
11110
+ async setEnabled(id, enabled) {
11111
+ return this.update(id, { enabled });
11112
+ }
11113
+ async remove(id) {
11114
+ const existing = this.data.channels[id];
11115
+ if (!existing) return false;
11116
+ await this.mutate(() => {
11117
+ delete this.data.channels[id];
11118
+ });
11119
+ return true;
11120
+ }
11121
+ async flush() {
11122
+ await this.saving;
11123
+ }
11124
+ async mutate(fn) {
11125
+ const snapshot = JSON.stringify(this.data, null, 2) + "\n";
11126
+ const previous = JSON.stringify(this.data);
11127
+ fn();
11128
+ this.saving = this.saving.then(async () => {
11129
+ await writeFileAtomic(this.path, snapshot, { mode: 384 });
11130
+ }).catch((error) => {
11131
+ try {
11132
+ this.data = JSON.parse(previous);
11133
+ } catch {
11134
+ }
11135
+ log.fail("notification-channels", error, { step: "persist" });
11136
+ throw error;
11137
+ });
11138
+ await this.saving;
11139
+ }
11140
+ };
11141
+ function normalizeChannel(id, value) {
11142
+ if (!value || typeof value !== "object" || !id.trim()) return void 0;
11143
+ const input = value;
11144
+ const type = input.type;
11145
+ if (type !== "telegram" && type !== "wecom") return void 0;
11146
+ const label2 = typeof input.label === "string" ? input.label.trim() : "";
11147
+ const destination = typeof input.destination === "string" ? input.destination.trim() : "";
11148
+ const secret = typeof input.secret === "string" ? input.secret.trim() : "";
11149
+ if (!label2 || !destination || !secret) return void 0;
11150
+ const mentionMap = {};
11151
+ if (input.mentionMap && typeof input.mentionMap === "object") {
11152
+ for (const [from, to] of Object.entries(input.mentionMap)) {
11153
+ if (typeof to === "string" && to.trim()) mentionMap[from] = to.trim();
11154
+ }
11155
+ }
11156
+ return {
11157
+ id: id.trim(),
11158
+ type,
11159
+ label: label2,
11160
+ destination,
11161
+ secret,
11162
+ enabled: input.enabled !== false,
11163
+ ...Object.keys(mentionMap).length > 0 ? { mentionMap } : {}
11164
+ };
11165
+ }
11166
+
11167
+ // src/notify/sinks/types.ts
11168
+ var SINK_HTTP_TIMEOUT_MS = 1e4;
11169
+ function maskChannel(channel, includeId = true) {
11170
+ const id = includeId ? `${channel.id} \xB7 ` : "";
11171
+ const dest = maskSecret(channel.destination);
11172
+ return `${id}${channel.type} \xB7 ${channel.label} \u2192 ${dest}`;
11173
+ }
11174
+ function maskSecret(value) {
11175
+ if (!value) return "(unset)";
11176
+ if (value.length <= 6) return `${value[0]}{redacted}`;
11177
+ return `${value.slice(0, 3)}\u2026${value.slice(-3)}`;
11178
+ }
11179
+
11180
+ // src/commands/channels.ts
11181
+ async function reply6(ctx, zh, en) {
11182
+ await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
11183
+ }
11184
+ function requireAdmin3(ctx) {
11185
+ if (ctx.accessManager.isAdmin(ctx.senderId)) return true;
11186
+ void reply6(ctx, "\u4EC5\u7BA1\u7406\u5458\u53EF\u7BA1\u7406\u901A\u77E5\u6E20\u9053\u3002", "Only admins can manage notification channels.");
11187
+ return false;
11188
+ }
11189
+ async function handleChannels(args, ctx) {
11190
+ if (!requireAdmin3(ctx)) return;
11191
+ const store = ctx.notificationChannels;
11192
+ if (!store) {
11193
+ 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.");
11194
+ return;
11195
+ }
11196
+ const [sub, ...rest] = args.trim().split(/\s+/);
11197
+ switch (sub) {
11198
+ case "list":
11199
+ return listChannels(ctx, store);
11200
+ case "show": {
11201
+ const id = rest[0];
11202
+ if (!id) return usage2(ctx);
11203
+ const channel = store.get(id);
11204
+ if (!channel) return notFound(ctx, id);
11205
+ return showChannel(ctx, channel);
11206
+ }
11207
+ case "add":
11208
+ return addChannel(ctx, store, rest.join(" "));
11209
+ case "remove": {
11210
+ const id = rest[0];
11211
+ if (!id) return usage2(ctx);
11212
+ const ok = await store.remove(id);
11213
+ return ok ? reply6(ctx, `\u5DF2\u79FB\u9664\u901A\u77E5\u6E20\u9053 \`${id}\`\u3002`, `Removed notification channel \`${id}\`.`) : notFound(ctx, id);
11214
+ }
11215
+ case "enable":
11216
+ case "disable": {
11217
+ const id = rest[0];
11218
+ if (!id) return usage2(ctx);
11219
+ const enabled = sub === "enable";
11220
+ const ok = await store.setEnabled(id, enabled);
11221
+ if (!ok) return notFound(ctx, id);
11222
+ return reply6(ctx, `\u5DF2${enabled ? "\u542F\u7528" : "\u505C\u7528"}\u901A\u77E5\u6E20\u9053 \`${id}\`\u3002`, `${enabled ? "Enabled" : "Disabled"} notification channel \`${id}\`.`);
11223
+ }
11224
+ default:
11225
+ return usage2(ctx);
11226
+ }
11227
+ }
11228
+ async function listChannels(ctx, store) {
11229
+ const channels = store.list();
11230
+ if (channels.length === 0) {
11231
+ 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.");
11232
+ return;
11233
+ }
11234
+ const lines = channels.map((channel) => `- ${channel.enabled ? "\u{1F7E2}" : "\u26AA\uFE0F"} \`${channel.id}\` \u2014 ${channel.type} \xB7 ${channel.label} \xB7 ${maskSecret(channel.destination)}`);
11235
+ await reply6(ctx, ["**\u5DF2\u914D\u7F6E\u901A\u77E5\u6E20\u9053**", "", ...lines].join("\n"), ["**Configured notification channels**", "", ...lines].join("\n"));
11236
+ }
11237
+ async function showChannel(ctx, channel) {
11238
+ const zh = `**${channel.id}**
11239
+ - \u7C7B\u578B\uFF1A\`${channel.type}\`
11240
+ - \u540D\u79F0\uFF1A${channel.label}
11241
+ - \u76EE\u6807\uFF1A\`${maskSecret(channel.destination)}\`
11242
+ - \u72B6\u6001\uFF1A${channel.enabled ? "\u542F\u7528" : "\u505C\u7528"}
11243
+ - \u5BC6\u94A5\uFF1A\`${maskSecret(channel.secret)}\``;
11244
+ const en = `**${channel.id}**
11245
+ - Type: \`${channel.type}\`
11246
+ - Label: ${channel.label}
11247
+ - Target: \`${maskSecret(channel.destination)}\`
11248
+ - Status: ${channel.enabled ? "enabled" : "disabled"}
11249
+ - Secret: \`${maskSecret(channel.secret)}\``;
11250
+ await reply6(ctx, zh, en);
11251
+ }
11252
+ async function addChannel(ctx, store, raw) {
11253
+ const parsed = parseAddArgs(raw);
11254
+ if (!parsed || !parsed.type || !parsed.label || !parsed.destination || !parsed.secret) {
11255
+ await usage2(ctx);
11256
+ return;
11257
+ }
11258
+ if (!SINK_TYPES.includes(parsed.type)) {
11259
+ 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("|")}\`.`);
11260
+ return;
11261
+ }
11262
+ const id = parsed.id ?? `${parsed.type}-${slugify2(parsed.label)}`;
11263
+ if (store.get(id)) {
11264
+ 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}\`.`);
11265
+ return;
11266
+ }
11267
+ const channel = {
11268
+ id,
11269
+ type: parsed.type,
11270
+ label: parsed.label,
11271
+ destination: parsed.destination,
11272
+ secret: parsed.secret,
11273
+ enabled: true
11274
+ };
11275
+ await store.add(channel);
11276
+ await reply6(ctx, `\u5DF2\u6DFB\u52A0\u901A\u77E5\u6E20\u9053 ${maskChannel(channel)}\u3002`, `Added notification channel ${maskChannel(channel)}.`);
11277
+ }
11278
+ async function notFound(ctx, id) {
11279
+ 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\`.`);
11280
+ }
11281
+ async function usage2(ctx) {
11282
+ await reply6(
11283
+ ctx,
11284
+ "\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>`",
11285
+ "Usage: `/channels list|show <id>|add <telegram|wecom> <label> [--id <id>] --destination <target> --secret <token>|remove <id>|enable <id>|disable <id>`"
11286
+ );
11287
+ }
11288
+ function parseAddArgs(raw) {
11289
+ if (!raw.trim()) return void 0;
11290
+ const idMatch = raw.match(/--id\s+(\S+)/);
11291
+ const destMatch = raw.match(/--destination\s+(\S+)/);
11292
+ const secretMatch = raw.match(/--secret\s+(\S+)/);
11293
+ const positionals = raw.replace(/\s*--id\s+\S+/, " ").replace(/\s*--destination\s+\S+/, " ").replace(/\s*--secret\s+\S+/, " ").trim().split(/\s+/).filter(Boolean);
11294
+ const [type, label2] = positionals;
11295
+ return {
11296
+ ...idMatch ? { id: idMatch[1] } : {},
11297
+ ...type ? { type } : {},
11298
+ ...label2 ? { label: label2 } : {},
11299
+ ...destMatch ? { destination: destMatch[1] } : {},
11300
+ ...secretMatch ? { secret: secretMatch[1] } : {}
11301
+ };
11302
+ }
11303
+ function slugify2(value) {
11304
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "channel";
11305
+ }
11306
+
11054
11307
  // src/commands/replies.ts
11055
11308
  async function handleReplies(args, ctx) {
11056
11309
  const store = ctx.replyPolicies;
11057
- 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.");
11310
+ 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.");
11058
11311
  const tokens = args.trim().split(/\s+/).filter(Boolean);
11059
- 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));
11060
- 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.");
11312
+ 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));
11313
+ 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.");
11061
11314
  if (tokens[0] === "default") {
11062
11315
  await store.set(ctx.scope, void 0);
11063
- 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.");
11316
+ 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.");
11064
11317
  }
11065
- if (tokens[0] !== "set") return usage2(ctx);
11318
+ if (tokens[0] !== "set") return usage3(ctx);
11066
11319
  const policy = { ...store.get(ctx.scope) };
11067
11320
  for (const token of tokens.slice(1)) {
11068
11321
  const [key, raw, extra] = token.split("=");
11069
- if (!raw || extra !== void 0) return usage2(ctx);
11322
+ if (!raw || extra !== void 0) return usage3(ctx);
11070
11323
  const seconds = Number(raw);
11071
- if (!Number.isInteger(seconds)) return usage2(ctx);
11324
+ if (!Number.isInteger(seconds)) return usage3(ctx);
11072
11325
  if (key === "merge" && seconds >= 0 && seconds <= 300) policy.mergeWindowMs = seconds * 1e3;
11073
11326
  else if (key === "batch" && seconds >= 1 && seconds <= 20) policy.maxBatchSize = seconds;
11074
11327
  else if (key === "interval" && seconds >= 0 && seconds <= 3600) policy.minIntervalMs = seconds * 1e3;
11075
11328
  else if (key === "dedupe" && seconds >= 0 && seconds <= 3600) policy.dedupeWindowMs = seconds * 1e3;
11076
- else return usage2(ctx);
11329
+ else return usage3(ctx);
11077
11330
  }
11078
- if (tokens.length === 1) return usage2(ctx);
11331
+ if (tokens.length === 1) return usage3(ctx);
11079
11332
  await store.set(ctx.scope, policy);
11080
- await reply6(ctx, `\u5DF2\u8BBE\u7F6E\u3002${describe2(policy, true, true)}`, `Updated. ${describe2(policy, true, false)}`);
11333
+ await reply7(ctx, `\u5DF2\u8BBE\u7F6E\u3002${describe2(policy, true, true)}`, `Updated. ${describe2(policy, true, false)}`);
11081
11334
  }
11082
11335
  async function canManageReplies(ctx) {
11083
11336
  if (ctx.accessManager.isAdmin(ctx.senderId)) return true;
@@ -11091,10 +11344,10 @@ async function canManageReplies(ctx) {
11091
11344
  function describe2(policy, configured, zh) {
11092
11345
  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.`;
11093
11346
  }
11094
- async function usage2(ctx) {
11095
- 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]`");
11347
+ async function usage3(ctx) {
11348
+ 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]`");
11096
11349
  }
11097
- async function reply6(ctx, zh, en) {
11350
+ async function reply7(ctx, zh, en) {
11098
11351
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
11099
11352
  }
11100
11353
 
@@ -11281,7 +11534,8 @@ var COMMAND_CATALOG = [
11281
11534
  { 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)" },
11282
11535
  { 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" },
11283
11536
  { 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)" },
11284
- { names: ["/notifications"], area: "communication", zh: "`[show|off|on \u2026]` \u2014 scope \u63D0\u9192\u7B56\u7565", en: "`[show|off|on \u2026]` \u2014 scope notification policy" },
11537
+ { 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=`)" },
11538
+ { 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)" },
11285
11539
  { 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" },
11286
11540
  { names: ["/retention"], area: "session", zh: "`[N|default]` \u2014 \u4F1A\u8BDD\u4FDD\u7559\u6761\u6570", en: "`[N|default]` \u2014 retained live-message count" },
11287
11541
  { names: ["/archive"], area: "session", zh: "`[note]|send|list|clean \u2026` \u2014 \u7BA1\u7406\u5F52\u6863", en: "`[note]|send|list|clean \u2026` \u2014 manage archives" },
@@ -11359,6 +11613,7 @@ var HELP = [
11359
11613
  "- `/notify <scope|chatId> <text>` \u2014 \u8DE8\u4F1A\u8BDD\u53D1\u9001\u901A\u77E5\uFF08\u7BA1\u7406\u5458\uFF09",
11360
11614
  "- `/notify list` \u2014 \u67E5\u770B\u5DF2\u6CE8\u518C scope",
11361
11615
  "- `/notifications [show|off|on \u2026]` \u2014 \u914D\u7F6E\u5F53\u524D scope \u7684\u5B8C\u6210 / \u5931\u8D25 / \u5BA1\u6279\u63D0\u9192",
11616
+ "- `/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",
11362
11617
  "- `/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",
11363
11618
  "- `/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",
11364
11619
  "- `/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",
@@ -11420,7 +11675,7 @@ var HELP_EN = [
11420
11675
  "- `/invite user|admin|group <id>` \u2014 manage access allowlists",
11421
11676
  "- `/help` \u2014 show this help"
11422
11677
  ].join("\n");
11423
- async function reply7(ctx, zhCn, enUs) {
11678
+ async function reply8(ctx, zhCn, enUs) {
11424
11679
  await ctx.channel.sendMarkdown(ctx.chatId, enUs === void 0 ? zhCn : bilingualMarkdown(zhCn, enUs, ctx.languagePolicies?.get().plain), {
11425
11680
  replyTo: ctx.messageId
11426
11681
  });
@@ -11429,7 +11684,7 @@ async function handleNew(_args, ctx) {
11429
11684
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11430
11685
  const interrupted = await ctx.activeRuns.interruptWorkspace(ctx.scope, cwd);
11431
11686
  ctx.sessions.clear(ctx.scope, cwd);
11432
- await reply7(
11687
+ await reply8(
11433
11688
  ctx,
11434
11689
  interrupted > 0 ? `\u5DF2\u4E2D\u65AD ${String(interrupted)} \u4E2A\u4EFB\u52A1\u5E76\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002` : "\u5DF2\u5F00\u59CB\u65B0\u4F1A\u8BDD\u3002",
11435
11690
  interrupted > 0 ? `Interrupted ${String(interrupted)} task(s) and started a new session.` : "Started a new session."
@@ -11458,11 +11713,11 @@ function groupAppLink(chatId) {
11458
11713
  async function handleNewGroup(args, ctx) {
11459
11714
  const name = args.trim();
11460
11715
  if (!name) {
11461
- 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");
11716
+ 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");
11462
11717
  return;
11463
11718
  }
11464
11719
  if (name.length > MAX_GROUP_NAME_LENGTH) {
11465
- await reply7(
11720
+ await reply8(
11466
11721
  ctx,
11467
11722
  `\u7FA4\u540D\u8FC7\u957F\uFF08\u4E0A\u9650 ${String(MAX_GROUP_NAME_LENGTH)} \u5B57\u7B26\uFF0C\u5F53\u524D ${String(name.length)}\uFF09\u3002`,
11468
11723
  `The group name is too long (limit ${String(MAX_GROUP_NAME_LENGTH)} characters; received ${String(name.length)}).`
@@ -11470,11 +11725,11 @@ async function handleNewGroup(args, ctx) {
11470
11725
  return;
11471
11726
  }
11472
11727
  if (!ctx.channel.createChat) {
11473
- await reply7(ctx, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u81EA\u52A8\u5EFA\u7FA4\u3002", "This channel cannot create groups automatically.");
11728
+ await reply8(ctx, "\u5F53\u524D\u6E20\u9053\u4E0D\u652F\u6301\u81EA\u52A8\u5EFA\u7FA4\u3002", "This channel cannot create groups automatically.");
11474
11729
  return;
11475
11730
  }
11476
11731
  if (!ctx.senderId) {
11477
- 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.");
11732
+ 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.");
11478
11733
  return;
11479
11734
  }
11480
11735
  try {
@@ -11485,7 +11740,7 @@ async function handleNewGroup(args, ctx) {
11485
11740
  inviteUserIds: [ctx.senderId],
11486
11741
  userIdType: "open_id"
11487
11742
  });
11488
- await reply7(
11743
+ await reply8(
11489
11744
  ctx,
11490
11745
  [
11491
11746
  `\u2705 \u5DF2\u521B\u5EFA\u7FA4\u804A\uFF1A**${name}**`,
@@ -11504,13 +11759,13 @@ async function handleNewGroup(args, ctx) {
11504
11759
  );
11505
11760
  } catch (error) {
11506
11761
  const message = error instanceof Error ? error.message : String(error);
11507
- await reply7(ctx, `\u274C \u5EFA\u7FA4\u5931\u8D25\uFF1A\`${message}\``, `\u274C Failed to create the group: \`${message}\``);
11762
+ await reply8(ctx, `\u274C \u5EFA\u7FA4\u5931\u8D25\uFF1A\`${message}\``, `\u274C Failed to create the group: \`${message}\``);
11508
11763
  }
11509
11764
  }
11510
11765
  async function handleCd(args, ctx) {
11511
11766
  const path = args.trim();
11512
11767
  if (!path) {
11513
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`", "Usage: `/cd <path>`");
11768
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`", "Usage: `/cd <path>`");
11514
11769
  return;
11515
11770
  }
11516
11771
  const normalized = path.replace(/^~/u, "~");
@@ -11518,13 +11773,13 @@ async function handleCd(args, ctx) {
11518
11773
  const cwd = resolve5(expanded);
11519
11774
  const target = await stat8(cwd).catch(() => void 0);
11520
11775
  if (!target?.isDirectory()) {
11521
- 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}\`.`);
11776
+ 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}\`.`);
11522
11777
  return;
11523
11778
  }
11524
11779
  const previous = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11525
11780
  const interrupted = cwd === previous ? 0 : await ctx.activeRuns.interruptWorkspace(ctx.scope, previous);
11526
11781
  ctx.workspaces.setCwd(ctx.scope, cwd);
11527
- await reply7(
11782
+ await reply8(
11528
11783
  ctx,
11529
11784
  `\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` : ""}`,
11530
11785
  `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).` : ""}`
@@ -11544,7 +11799,7 @@ async function handleWs(args, ctx) {
11544
11799
  const lines = Object.entries(named).map(
11545
11800
  ([key, value]) => `- **${key}** \u2192 \`${value}\`${value === current ? " \u2190 \u5F53\u524D" : ""}`
11546
11801
  );
11547
- await reply7(
11802
+ await reply8(
11548
11803
  ctx,
11549
11804
  [
11550
11805
  `\u5F53\u524D cwd\uFF1A\`${current}\``,
@@ -11561,29 +11816,29 @@ async function handleWs(args, ctx) {
11561
11816
  }
11562
11817
  if (sub === "save") {
11563
11818
  if (!name) {
11564
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/ws save <name>`", "Usage: `/ws save <name>`");
11819
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/ws save <name>`", "Usage: `/ws save <name>`");
11565
11820
  return;
11566
11821
  }
11567
11822
  const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11568
11823
  ctx.workspaces.saveNamed(name, current);
11569
- await reply7(ctx, `\u5DF2\u4FDD\u5B58\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${current}\``, `Saved workspace: **${name}** \u2192 \`${current}\``);
11824
+ await reply8(ctx, `\u5DF2\u4FDD\u5B58\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \u2192 \`${current}\``, `Saved workspace: **${name}** \u2192 \`${current}\``);
11570
11825
  return;
11571
11826
  }
11572
11827
  if (sub === "use") {
11573
11828
  if (!name) {
11574
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/ws use <name>`", "Usage: `/ws use <name>`");
11829
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/ws use <name>`", "Usage: `/ws use <name>`");
11575
11830
  return;
11576
11831
  }
11577
11832
  const cwd = ctx.workspaces.getNamed(name);
11578
11833
  if (!cwd) {
11579
- await reply7(ctx, `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`, `Workspace not found: **${name}**`);
11834
+ await reply8(ctx, `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`, `Workspace not found: **${name}**`);
11580
11835
  return;
11581
11836
  }
11582
11837
  const previous = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11583
11838
  const interrupted = cwd === previous ? 0 : await ctx.activeRuns.interruptWorkspace(ctx.scope, previous);
11584
11839
  ctx.workspaces.setCwd(ctx.scope, cwd);
11585
11840
  ctx.workspaces.touchNamed(name);
11586
- await reply7(
11841
+ await reply8(
11587
11842
  ctx,
11588
11843
  `\u5DF2\u5207\u6362\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}** \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` : ""}`,
11589
11844
  `Switched to workspace **${name}** \u2192 \`${cwd}\`; its independent session will resume.${interrupted > 0 ? ` Interrupted ${String(interrupted)} running task(s) in the previous workspace (session data was preserved).` : ""}`
@@ -11592,14 +11847,14 @@ async function handleWs(args, ctx) {
11592
11847
  }
11593
11848
  if (sub === "remove") {
11594
11849
  if (!name) {
11595
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/ws remove <name>`", "Usage: `/ws remove <name>`");
11850
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/ws remove <name>`", "Usage: `/ws remove <name>`");
11596
11851
  return;
11597
11852
  }
11598
11853
  const removed = ctx.workspaces.removeNamed(name);
11599
- await reply7(ctx, removed ? `\u5DF2\u5220\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**` : `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`, removed ? `Removed workspace: **${name}**` : `Workspace not found: **${name}**`);
11854
+ await reply8(ctx, removed ? `\u5DF2\u5220\u9664\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**` : `\u672A\u627E\u5230\u5DE5\u4F5C\u7A7A\u95F4\uFF1A**${name}**`, removed ? `Removed workspace: **${name}**` : `Workspace not found: **${name}**`);
11600
11855
  return;
11601
11856
  }
11602
- 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.");
11857
+ 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.");
11603
11858
  }
11604
11859
  async function statusCardInputFor(ctx) {
11605
11860
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -11646,6 +11901,7 @@ async function statusCardInputFor(ctx) {
11646
11901
  permissionPolicy: ctx.permissionPolicies?.get(ctx.scope) ?? "ask",
11647
11902
  executionMode: ctx.executionModes?.get(ctx.scope) ?? "balanced",
11648
11903
  ...notificationPreference ? { notificationPreference } : {},
11904
+ ...ctx.notificationSinks ? { notificationChannels: ctx.notificationSinks.enabledChannels().map((channel) => channel.id) } : {},
11649
11905
  ...replyPolicy ? { replyPolicy, replyPolicyConfigured: ctx.replyPolicies?.isConfigured(ctx.scope) ?? false } : {},
11650
11906
  role: role ? `\`${role.id}\` (${role.name})` : void 0,
11651
11907
  metrics,
@@ -11688,16 +11944,16 @@ async function handleStatus(_args, ctx) {
11688
11944
  } catch {
11689
11945
  }
11690
11946
  }
11691
- await reply7(ctx, bilingualMarkdown(statusCardMarkdown(input), statusCardMarkdownEnglish(input)));
11947
+ await reply8(ctx, bilingualMarkdown(statusCardMarkdown(input), statusCardMarkdownEnglish(input)));
11692
11948
  }
11693
11949
  async function handleIsolation(args, ctx) {
11694
11950
  if (ctx.chatMode === "p2p") {
11695
- 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.");
11951
+ 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.");
11696
11952
  return;
11697
11953
  }
11698
11954
  const mode = args.trim().toLowerCase();
11699
11955
  if (!mode) {
11700
- await reply7(
11956
+ await reply8(
11701
11957
  ctx,
11702
11958
  `\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`,
11703
11959
  `Current group isolation mode: **${ctx.isolationStore?.get(ctx.chatId) ?? "topic"}**. Use \`/isolation group|topic|member\` to switch.`
@@ -11705,16 +11961,16 @@ async function handleIsolation(args, ctx) {
11705
11961
  return;
11706
11962
  }
11707
11963
  if (mode !== "group" && mode !== "topic" && mode !== "member") {
11708
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/isolation group|topic|member`", "Usage: `/isolation group|topic|member`");
11964
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/isolation group|topic|member`", "Usage: `/isolation group|topic|member`");
11709
11965
  return;
11710
11966
  }
11711
- if (!requireAdmin3(ctx)) return;
11967
+ if (!requireAdmin4(ctx)) return;
11712
11968
  if (!ctx.isolationStore) {
11713
- 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.");
11969
+ 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.");
11714
11970
  return;
11715
11971
  }
11716
11972
  ctx.isolationStore.set(ctx.chatId, mode);
11717
- await reply7(
11973
+ await reply8(
11718
11974
  ctx,
11719
11975
  `\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`,
11720
11976
  `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.`
@@ -11741,25 +11997,25 @@ async function handleVersion(_args, ctx) {
11741
11997
  lines.push("\u6700\u65B0\u7248\u672C\u67E5\u8BE2\u6682\u4E0D\u53EF\u7528\uFF08\u7F51\u7EDC / registry \u5F02\u5E38\uFF09\u3002");
11742
11998
  english.push("Latest-version lookup is unavailable (network or registry error).");
11743
11999
  }
11744
- await reply7(ctx, lines.join("\n"), english.join("\n"));
12000
+ await reply8(ctx, lines.join("\n"), english.join("\n"));
11745
12001
  }
11746
12002
  async function handleUpgrade(_args, ctx) {
11747
- if (!requireAdmin3(ctx)) return;
12003
+ if (!requireAdmin4(ctx)) return;
11748
12004
  if (!ctx.senderId || !ctx.channelUpdates) {
11749
- 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.");
12005
+ 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.");
11750
12006
  return;
11751
12007
  }
11752
12008
  const result = await ctx.channelUpdates.check({ scope: ctx.scope, actorId: ctx.senderId });
11753
12009
  if (result.kind === "unavailable") {
11754
- 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.");
12010
+ 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.");
11755
12011
  return;
11756
12012
  }
11757
12013
  if (result.kind === "current") {
11758
- await reply7(ctx, `\u2705 \u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF1A\`${result.current}\`\u3002`, `\u2705 Already up to date: \`${result.current}\`.`);
12014
+ await reply8(ctx, `\u2705 \u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C\uFF1A\`${result.current}\`\u3002`, `\u2705 Already up to date: \`${result.current}\`.`);
11759
12015
  return;
11760
12016
  }
11761
12017
  if (!ctx.channel.sendCard) {
11762
- 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.");
12018
+ 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.");
11763
12019
  return;
11764
12020
  }
11765
12021
  await ctx.channel.sendCard(ctx.chatId, renderUpdateCard({
@@ -11774,9 +12030,9 @@ async function handleUpgrade(_args, ctx) {
11774
12030
  });
11775
12031
  }
11776
12032
  async function handleDoctor(_args, ctx) {
11777
- if (!requireAdmin3(ctx)) return;
12033
+ if (!requireAdmin4(ctx)) return;
11778
12034
  if (!ctx.createDiagnosticBundle || !ctx.channel.sendFile) {
11779
- 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.");
12035
+ 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.");
11780
12036
  return;
11781
12037
  }
11782
12038
  let file;
@@ -11793,7 +12049,7 @@ async function handleDoctor(_args, ctx) {
11793
12049
  ...status.jobs ? { jobs: status.jobs } : {}
11794
12050
  }), ctx.diagnosticTimeoutMs?.generate ?? DOCTOR_GENERATE_TIMEOUT_MS);
11795
12051
  } catch {
11796
- await reply7(
12052
+ await reply8(
11797
12053
  ctx,
11798
12054
  "\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",
11799
12055
  "\u26A0\uFE0F Failed to generate the diagnostic bundle. Retry later; if it keeps failing, run `dsh-lark-bot doctor` locally."
@@ -11807,21 +12063,21 @@ async function handleDoctor(_args, ctx) {
11807
12063
  }), ctx.diagnosticTimeoutMs?.upload ?? DOCTOR_UPLOAD_TIMEOUT_MS);
11808
12064
  } catch (error) {
11809
12065
  if (error instanceof OperationTimeoutError) {
11810
- await reply7(
12066
+ await reply8(
11811
12067
  ctx,
11812
12068
  "\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",
11813
12069
  "\u26A0\uFE0F Diagnostic upload timed out with an unknown result: the file may still arrive later. Check this chat before retrying."
11814
12070
  );
11815
12071
  return;
11816
12072
  }
11817
- await reply7(
12073
+ await reply8(
11818
12074
  ctx,
11819
12075
  "\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",
11820
12076
  "\u26A0\uFE0F Failed to send the diagnostic bundle. Retry later; if it keeps failing, run `dsh-lark-bot doctor` locally."
11821
12077
  );
11822
12078
  return;
11823
12079
  }
11824
- await reply7(
12080
+ await reply8(
11825
12081
  ctx,
11826
12082
  "\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",
11827
12083
  "\u2705 The redacted diagnostic bundle was generated and sent. You can download and forward it to a maintainer."
@@ -11831,18 +12087,18 @@ async function handleResume(_args, ctx) {
11831
12087
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11832
12088
  const history = ctx.sessions.historyFor(ctx.scope, cwd);
11833
12089
  if (history.length === 0) {
11834
- await reply7(ctx, "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u5386\u53F2\u4E0A\u4E0B\u6587\u3002", "This session has no conversation history.");
12090
+ await reply8(ctx, "\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u5386\u53F2\u4E0A\u4E0B\u6587\u3002", "This session has no conversation history.");
11835
12091
  return;
11836
12092
  }
11837
12093
  const recent = history.slice(-6).map((message) => {
11838
12094
  const speaker = message.role === "user" ? "\u{1F464}" : "\u{1F916}";
11839
12095
  return `${speaker} ${message.content.slice(0, 300)}`;
11840
12096
  });
11841
- await reply7(ctx, [`\u5F53\u524D scope\uFF1A\`${ctx.scope}\``, "", ...recent].join("\n"), [`Current scope: \`${ctx.scope}\``, "", ...recent].join("\n"));
12097
+ await reply8(ctx, [`\u5F53\u524D scope\uFF1A\`${ctx.scope}\``, "", ...recent].join("\n"), [`Current scope: \`${ctx.scope}\``, "", ...recent].join("\n"));
11842
12098
  }
11843
12099
  async function handleJobs(args, ctx) {
11844
12100
  if (!ctx.jobs) {
11845
- 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.");
12101
+ 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.");
11846
12102
  return;
11847
12103
  }
11848
12104
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -11850,11 +12106,11 @@ async function handleJobs(args, ctx) {
11850
12106
  const records = ctx.jobs.list(ctx.scope, cwd, 20);
11851
12107
  if (sub === "retry") {
11852
12108
  if (!id) {
11853
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/jobs retry <\u6D88\u606FID>`", "Usage: `/jobs retry <message-id>`");
12109
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/jobs retry <\u6D88\u606FID>`", "Usage: `/jobs retry <message-id>`");
11854
12110
  return;
11855
12111
  }
11856
12112
  const queued = await ctx.requeueJob?.(id, ctx.scope, cwd);
11857
- await reply7(
12113
+ await reply8(
11858
12114
  ctx,
11859
12115
  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`,
11860
12116
  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.`
@@ -11864,19 +12120,19 @@ async function handleJobs(args, ctx) {
11864
12120
  if (sub === "show") {
11865
12121
  const record = id ? ctx.jobs.get(id, ctx.scope, cwd) : void 0;
11866
12122
  if (!record) {
11867
- await reply7(ctx, `\u5F53\u524D workspace \u672A\u627E\u5230\u4EFB\u52A1\uFF1A**${id ?? ""}**`, `Job not found in this workspace: **${id ?? ""}**`);
12123
+ await reply8(ctx, `\u5F53\u524D workspace \u672A\u627E\u5230\u4EFB\u52A1\uFF1A**${id ?? ""}**`, `Job not found in this workspace: **${id ?? ""}**`);
11868
12124
  return;
11869
12125
  }
11870
- await reply7(ctx, jobDetail(record, "zh"), jobDetail(record, "en"));
12126
+ await reply8(ctx, jobDetail(record, "zh"), jobDetail(record, "en"));
11871
12127
  return;
11872
12128
  }
11873
12129
  if (sub && sub !== "list") {
11874
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/jobs [list|show <\u6D88\u606FID>|retry <\u6D88\u606FID>]`", "Usage: `/jobs [list|show <message-id>|retry <message-id>]`");
12130
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/jobs [list|show <\u6D88\u606FID>|retry <\u6D88\u606FID>]`", "Usage: `/jobs [list|show <message-id>|retry <message-id>]`");
11875
12131
  return;
11876
12132
  }
11877
12133
  const counts = ctx.jobs.counts(ctx.scope, cwd);
11878
12134
  const rows = records.slice(0, 10);
11879
- await reply7(
12135
+ await reply8(
11880
12136
  ctx,
11881
12137
  [
11882
12138
  "**\u4EFB\u52A1\u5BF9\u8D26**",
@@ -11923,7 +12179,7 @@ async function handleStop(_args, ctx) {
11923
12179
  });
11924
12180
  let stopped = 0;
11925
12181
  for (const scope of scopes) stopped += await ctx.activeRuns.interrupt(scope);
11926
- await reply7(
12182
+ await reply8(
11927
12183
  ctx,
11928
12184
  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",
11929
12185
  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."
@@ -11934,7 +12190,7 @@ async function handleTimeout(args, ctx) {
11934
12190
  const effectiveMs = ctx.runPolicies.get(ctx.scope) ?? ctx.defaultRunTimeoutMs;
11935
12191
  if (!input) {
11936
12192
  const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
11937
- await reply7(
12193
+ await reply8(
11938
12194
  ctx,
11939
12195
  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",
11940
12196
  minutes2 > 0 ? `Session idle timeout: stop after ${minutes2} minutes without activity events. Use \`/timeout <N|off|default>\` to change it.` : "Session idle timeout: off."
@@ -11943,27 +12199,27 @@ async function handleTimeout(args, ctx) {
11943
12199
  }
11944
12200
  if (input === "off") {
11945
12201
  ctx.runPolicies.set(ctx.scope, 0);
11946
- await reply7(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\u3002", "Disabled the session idle timeout.");
12202
+ await reply8(ctx, "\u5DF2\u5173\u95ED\u5F53\u524D\u4F1A\u8BDD\u7A7A\u95F2\u8D85\u65F6\u3002", "Disabled the session idle timeout.");
11947
12203
  return;
11948
12204
  }
11949
12205
  if (input === "default") {
11950
12206
  ctx.runPolicies.clear(ctx.scope);
11951
- await reply7(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u7A7A\u95F2\u8D85\u65F6\u3002", "Restored the default idle timeout.");
12207
+ await reply8(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u7A7A\u95F2\u8D85\u65F6\u3002", "Restored the default idle timeout.");
11952
12208
  return;
11953
12209
  }
11954
12210
  const minutes = Number(input);
11955
12211
  if (!Number.isInteger(minutes) || minutes <= 0) {
11956
- 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.");
12212
+ 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.");
11957
12213
  return;
11958
12214
  }
11959
12215
  ctx.runPolicies.set(ctx.scope, minutes * 6e4);
11960
- 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.`);
12216
+ 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.`);
11961
12217
  }
11962
12218
  async function handleConcurrency(args, ctx) {
11963
12219
  const input = args.trim();
11964
12220
  const effective = ctx.concurrencyStore.get(ctx.scope) ?? ctx.defaultScopeConcurrency;
11965
12221
  if (!input) {
11966
- await reply7(
12222
+ await reply8(
11967
12223
  ctx,
11968
12224
  `\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`,
11969
12225
  `Parallel tasks for this scope: **${String(effective)}**. Use \`/concurrency <N|default>\` to change it (N \u2265 1).`
@@ -11972,16 +12228,16 @@ async function handleConcurrency(args, ctx) {
11972
12228
  }
11973
12229
  if (input === "default") {
11974
12230
  ctx.concurrencyStore.clear(ctx.scope);
11975
- 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)}).`);
12231
+ 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)}).`);
11976
12232
  return;
11977
12233
  }
11978
12234
  const n = Number(input);
11979
12235
  if (!Number.isInteger(n) || n < 1) {
11980
- 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.");
12236
+ 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.");
11981
12237
  return;
11982
12238
  }
11983
12239
  ctx.concurrencyStore.set(ctx.scope, n);
11984
- 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)}**.`);
12240
+ 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)}**.`);
11985
12241
  }
11986
12242
  async function handlePermission(args, ctx) {
11987
12243
  const [rawPolicy = "", rawTarget = "", ...extra] = args.trim().split(/\s+/);
@@ -11993,7 +12249,7 @@ async function handlePermission(args, ctx) {
11993
12249
  };
11994
12250
  if (!input) {
11995
12251
  const current = ctx.permissionPolicies?.get(ctx.scope) ?? "ask";
11996
- await reply7(
12252
+ await reply8(
11997
12253
  ctx,
11998
12254
  `\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`,
11999
12255
  `Tool permission policy for this scope: **${labels[current].en}** (\`${current}\`). Use \`/permission ask|allow|deny\` to change it (admin only).`
@@ -12001,21 +12257,21 @@ async function handlePermission(args, ctx) {
12001
12257
  return;
12002
12258
  }
12003
12259
  if (input !== "ask" && input !== "allow" && input !== "deny" || extra.length > 0) {
12004
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/permission [ask|allow|deny] [scope]`", "Usage: `/permission [ask|allow|deny] [scope]`");
12260
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/permission [ask|allow|deny] [scope]`", "Usage: `/permission [ask|allow|deny] [scope]`");
12005
12261
  return;
12006
12262
  }
12007
- if (!requireAdmin3(ctx)) return;
12263
+ if (!requireAdmin4(ctx)) return;
12008
12264
  if (!ctx.permissionPolicies) {
12009
- 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.");
12265
+ 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.");
12010
12266
  return;
12011
12267
  }
12012
12268
  const targetScope = rawTarget || ctx.scope;
12013
12269
  if (targetScope !== ctx.chatId && !targetScope.startsWith(`${ctx.chatId}:`)) {
12014
- 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`.");
12270
+ 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`.");
12015
12271
  return;
12016
12272
  }
12017
12273
  await ctx.permissionPolicies.set(targetScope, input);
12018
- await reply7(
12274
+ await reply8(
12019
12275
  ctx,
12020
12276
  `\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`,
12021
12277
  `Set scope \`${targetScope}\`'s tool permission policy to **${labels[input].en}** (\`${input}\`). It applies to the next tool approval; plan approval remains enforced.`
@@ -12025,7 +12281,7 @@ async function handleDensity(args, ctx) {
12025
12281
  const input = args.trim().toLowerCase();
12026
12282
  if (!input) {
12027
12283
  const current = ctx.densityStore?.get(ctx.scope) ?? "standard";
12028
- await reply7(
12284
+ await reply8(
12029
12285
  ctx,
12030
12286
  `\u5F53\u524D\u5361\u7247\u5BC6\u5EA6\uFF1A**${current}**\u3002\u53EF\u7528 \`/density compact|standard|detailed\` \u8C03\u6574\u3002`,
12031
12287
  `Current card density: **${current}**. Use \`/density compact|standard|detailed\` to change it.`
@@ -12034,21 +12290,21 @@ async function handleDensity(args, ctx) {
12034
12290
  }
12035
12291
  if (input === "default") {
12036
12292
  ctx.densityStore?.clear(ctx.scope);
12037
- await reply7(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u5361\u7247\u5BC6\u5EA6\u3002", "Restored the default card density.");
12293
+ await reply8(ctx, "\u5DF2\u6062\u590D\u9ED8\u8BA4\u5361\u7247\u5BC6\u5EA6\u3002", "Restored the default card density.");
12038
12294
  return;
12039
12295
  }
12040
12296
  const density = parseCardDensity(input);
12041
12297
  if (!density) {
12042
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/density [compact|standard|detailed|default]`", "Usage: `/density [compact|standard|detailed|default]`");
12298
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/density [compact|standard|detailed|default]`", "Usage: `/density [compact|standard|detailed|default]`");
12043
12299
  return;
12044
12300
  }
12045
12301
  ctx.densityStore?.set(ctx.scope, density);
12046
- 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}**.`);
12302
+ 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}**.`);
12047
12303
  }
12048
12304
  async function handleExecutionMode(args, ctx) {
12049
12305
  const input = args.trim().toLowerCase();
12050
12306
  if (!ctx.executionModes) {
12051
- 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.");
12307
+ 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.");
12052
12308
  return;
12053
12309
  }
12054
12310
  if (!input) {
@@ -12065,7 +12321,7 @@ async function handleExecutionMode(args, ctx) {
12065
12321
  } catch {
12066
12322
  }
12067
12323
  }
12068
- await reply7(
12324
+ await reply8(
12069
12325
  ctx,
12070
12326
  `\u5F53\u524D\u6267\u884C\u6A21\u5F0F\uFF1A**${current}**\u3002\u53EF\u7528 \`/mode quick|balanced|deep\` \u5207\u6362\uFF1B\u4E0B\u4E00\u8F6E\u751F\u6548\u3002`,
12071
12327
  `Current execution mode: **${current}**. Use \`/mode quick|balanced|deep\`; changes apply next turn.`
@@ -12073,18 +12329,18 @@ async function handleExecutionMode(args, ctx) {
12073
12329
  return;
12074
12330
  }
12075
12331
  if (input !== "quick" && input !== "balanced" && input !== "deep") {
12076
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/mode [quick|balanced|deep]`", "Usage: `/mode [quick|balanced|deep]`");
12332
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/mode [quick|balanced|deep]`", "Usage: `/mode [quick|balanced|deep]`");
12077
12333
  return;
12078
12334
  }
12079
12335
  await ctx.executionModes.set(ctx.scope, input);
12080
- await reply7(
12336
+ await reply8(
12081
12337
  ctx,
12082
12338
  `\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`,
12083
12339
  `Set this session\u2019s execution mode to **${input}** for the next turn. Active work and existing context are unchanged.`
12084
12340
  );
12085
12341
  }
12086
12342
  async function handleEffortAlias(args, ctx) {
12087
- await reply7(
12343
+ await reply8(
12088
12344
  ctx,
12089
12345
  "`/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",
12090
12346
  "`/effort` is a compatibility alias for `/mode`; it controls task execution intensity, not the model reasoning-effort setting."
@@ -12094,11 +12350,11 @@ async function handleEffortAlias(args, ctx) {
12094
12350
  async function handleAsk(args, ctx) {
12095
12351
  const question = args.trim();
12096
12352
  if (!question) {
12097
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/ask <\u95EE\u9898>`", "Usage: `/ask <question>`");
12353
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/ask <\u95EE\u9898>`", "Usage: `/ask <question>`");
12098
12354
  return;
12099
12355
  }
12100
12356
  if (!ctx.questions) {
12101
- 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).");
12357
+ 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).");
12102
12358
  return;
12103
12359
  }
12104
12360
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -12120,18 +12376,18 @@ async function handleAsk(args, ctx) {
12120
12376
  if (answer !== void 0) {
12121
12377
  const text = Array.isArray(answer) ? answer.join("\u3001") : answer;
12122
12378
  ctx.sessions.recordExchange(ctx.scope, cwd, [text], void 0);
12123
- 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.");
12379
+ 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.");
12124
12380
  } else {
12125
- 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).");
12381
+ 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).");
12126
12382
  }
12127
12383
  }
12128
12384
  async function handleInvite(args, ctx) {
12129
12385
  const [kind, ...rest] = args.trim().split(/\s+/);
12130
12386
  const id = rest.join(" ").trim();
12131
- if (kind !== "list" && !requireAdmin3(ctx)) return;
12387
+ if (kind !== "list" && !requireAdmin4(ctx)) return;
12132
12388
  if (kind === "list") {
12133
12389
  const snapshot = ctx.accessManager.snapshot();
12134
- await reply7(
12390
+ await reply8(
12135
12391
  ctx,
12136
12392
  [
12137
12393
  "**\u8BBF\u95EE\u767D\u540D\u5355**",
@@ -12149,7 +12405,7 @@ async function handleInvite(args, ctx) {
12149
12405
  return;
12150
12406
  }
12151
12407
  if (!kind || !id) {
12152
- await reply7(
12408
+ await reply8(
12153
12409
  ctx,
12154
12410
  "\u7528\u6CD5\uFF1A`/invite user|admin|group <id>`\u3001`/invite list`\u3001`/invite remove user|group <id>`",
12155
12411
  "Usage: `/invite user|admin|group <id>`, `/invite list`, or `/invite remove user|group <id>`"
@@ -12158,39 +12414,39 @@ async function handleInvite(args, ctx) {
12158
12414
  }
12159
12415
  if (kind === "user") {
12160
12416
  await ctx.accessManager.addUser(id);
12161
- await reply7(ctx, `\u5DF2\u5141\u8BB8\u7528\u6237\uFF1A\`${id}\``, `Allowed user: \`${id}\``);
12417
+ await reply8(ctx, `\u5DF2\u5141\u8BB8\u7528\u6237\uFF1A\`${id}\``, `Allowed user: \`${id}\``);
12162
12418
  return;
12163
12419
  }
12164
12420
  if (kind === "admin") {
12165
12421
  await ctx.accessManager.addAdmin(id);
12166
- await reply7(ctx, `\u5DF2\u8BBE\u4E3A\u7BA1\u7406\u5458\uFF1A\`${id}\``, `Granted admin access to: \`${id}\``);
12422
+ await reply8(ctx, `\u5DF2\u8BBE\u4E3A\u7BA1\u7406\u5458\uFF1A\`${id}\``, `Granted admin access to: \`${id}\``);
12167
12423
  return;
12168
12424
  }
12169
12425
  if (kind === "group") {
12170
12426
  await ctx.accessManager.addChat(id);
12171
- await reply7(ctx, `\u5DF2\u5141\u8BB8\u7FA4\u804A\uFF1A\`${id}\``, `Allowed group chat: \`${id}\``);
12427
+ await reply8(ctx, `\u5DF2\u5141\u8BB8\u7FA4\u804A\uFF1A\`${id}\``, `Allowed group chat: \`${id}\``);
12172
12428
  return;
12173
12429
  }
12174
12430
  if (kind === "remove") {
12175
12431
  const [sub, target] = rest;
12176
12432
  if (sub === "user" && target) {
12177
12433
  await ctx.accessManager.removeUser(target);
12178
- await reply7(ctx, `\u5DF2\u79FB\u9664\u7528\u6237\uFF1A\`${target}\``, `Removed user: \`${target}\``);
12434
+ await reply8(ctx, `\u5DF2\u79FB\u9664\u7528\u6237\uFF1A\`${target}\``, `Removed user: \`${target}\``);
12179
12435
  return;
12180
12436
  }
12181
12437
  if (sub === "group" && target) {
12182
12438
  await ctx.accessManager.removeChat(target);
12183
- await reply7(ctx, `\u5DF2\u79FB\u9664\u7FA4\u804A\uFF1A\`${target}\``, `Removed group chat: \`${target}\``);
12439
+ await reply8(ctx, `\u5DF2\u79FB\u9664\u7FA4\u804A\uFF1A\`${target}\``, `Removed group chat: \`${target}\``);
12184
12440
  return;
12185
12441
  }
12186
- 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>`");
12442
+ 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>`");
12187
12443
  return;
12188
12444
  }
12189
- 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.");
12445
+ 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.");
12190
12446
  }
12191
- function requireAdmin3(ctx) {
12447
+ function requireAdmin4(ctx) {
12192
12448
  if (!ctx.accessManager.isAdmin(ctx.senderId)) {
12193
- void reply7(ctx, "\u4EC5\u7BA1\u7406\u5458\u53EF\u6267\u884C\u8BE5\u64CD\u4F5C\u3002", "Only admins can perform this operation.");
12449
+ void reply8(ctx, "\u4EC5\u7BA1\u7406\u5458\u53EF\u6267\u884C\u8BE5\u64CD\u4F5C\u3002", "Only admins can perform this operation.");
12194
12450
  return false;
12195
12451
  }
12196
12452
  return true;
@@ -12243,20 +12499,20 @@ async function handleKeyDispatch(args, ctx) {
12243
12499
  await handleKey(args, ctx);
12244
12500
  }
12245
12501
  async function handleHelp(_args, ctx) {
12246
- await reply7(ctx, renderCommandHelp("zh"), renderCommandHelp("en"));
12502
+ await reply8(ctx, renderCommandHelp("zh"), renderCommandHelp("en"));
12247
12503
  }
12248
12504
  async function handleLanguage(args, ctx) {
12249
12505
  if (!ctx.languagePolicies) {
12250
- await reply7(ctx, "\u5F53\u524D\u8FD0\u884C\u65F6\u672A\u542F\u7528\u8BED\u8A00\u7B56\u7565\u3002", "Language policy is unavailable in this runtime.");
12506
+ await reply8(ctx, "\u5F53\u524D\u8FD0\u884C\u65F6\u672A\u542F\u7528\u8BED\u8A00\u7B56\u7565\u3002", "Language policy is unavailable in this runtime.");
12251
12507
  return;
12252
12508
  }
12253
12509
  const [action = "show", field, value] = args.trim().split(/\s+/);
12254
12510
  if (action === "show") {
12255
12511
  const policy = ctx.languagePolicies.get();
12256
- 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}`);
12512
+ 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}`);
12257
12513
  return;
12258
12514
  }
12259
- if (!requireAdmin3(ctx)) return;
12515
+ if (!requireAdmin4(ctx)) return;
12260
12516
  if (action === "set" && field === "plain" && (value === "bilingual" || value === "zh" || value === "en")) {
12261
12517
  await ctx.languagePolicies.set({ plain: value });
12262
12518
  } else if (action === "set" && field === "agent" && (value === "auto" || value === "zh" || value === "en")) {
@@ -12264,7 +12520,7 @@ async function handleLanguage(args, ctx) {
12264
12520
  } else if (action === "reset" && (field === void 0 || field === "all" || field === "plain" || field === "agent")) {
12265
12521
  await ctx.languagePolicies.reset(field ?? "all");
12266
12522
  } else {
12267
- 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]`");
12523
+ 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]`");
12268
12524
  return;
12269
12525
  }
12270
12526
  process.env.DSH_LARK_REPLY_LANG = ctx.languagePolicies.get().plain;
@@ -12275,37 +12531,37 @@ function parseSecretTarget(value) {
12275
12531
  }
12276
12532
  async function handleSecret(args, ctx) {
12277
12533
  if (!ctx.secretService) {
12278
- 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.");
12534
+ 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.");
12279
12535
  return;
12280
12536
  }
12281
12537
  const [action, targetRaw, referenceRaw, ...purposeParts] = args.trim().split(/\s+/);
12282
12538
  const target = parseSecretTarget(targetRaw);
12283
12539
  const reference = referenceRaw?.trim();
12284
12540
  if (!target || !reference || !["status", "set", "remove"].includes(action ?? "")) {
12285
- 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>`");
12541
+ 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>`");
12286
12542
  return;
12287
12543
  }
12288
- if (action !== "status" && !requireAdmin3(ctx)) return;
12544
+ if (action !== "status" && !requireAdmin4(ctx)) return;
12289
12545
  if (action === "status") {
12290
12546
  const configured = await ctx.secretService.configured(target, reference);
12291
- await reply7(ctx, `${target} \`${reference}\`\uFF1A${configured ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`, `${target} \`${reference}\`: ${configured ? "configured" : "not configured"}`);
12547
+ await reply8(ctx, `${target} \`${reference}\`\uFF1A${configured ? "\u5DF2\u914D\u7F6E" : "\u672A\u914D\u7F6E"}`, `${target} \`${reference}\`: ${configured ? "configured" : "not configured"}`);
12292
12548
  return;
12293
12549
  }
12294
12550
  if (action === "remove") {
12295
12551
  const removed = await ctx.secretService.remove(target, reference);
12296
- 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.");
12552
+ 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.");
12297
12553
  return;
12298
12554
  }
12299
12555
  if (!ctx.senderId) {
12300
- 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.");
12556
+ 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.");
12301
12557
  return;
12302
12558
  }
12303
12559
  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" });
12304
- 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.");
12560
+ 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.");
12305
12561
  }
12306
12562
  async function handleSessionProjection(args, ctx) {
12307
12563
  if (!ctx.sessionProjection) {
12308
- await reply7(
12564
+ await reply8(
12309
12565
  ctx,
12310
12566
  "\u5F53\u524D adapter \u672A\u63D0\u4F9B DSH session \u6295\u5F71\uFF1B\u8BF7\u4F7F\u7528 `web` adapter \u5E76\u786E\u8BA4 Web host \u53EF\u7528\u3002",
12311
12567
  "The current adapter does not provide DSH session projection; use the `web` adapter and ensure the Web host is reachable."
@@ -12335,6 +12591,7 @@ var handlers = {
12335
12591
  "/role": handleRole,
12336
12592
  "/notify": handleNotify,
12337
12593
  "/notifications": handleNotifications,
12594
+ "/channels": handleChannels,
12338
12595
  "/replies": handleReplies,
12339
12596
  "/retention": handleRetention,
12340
12597
  "/archive": handleArchive,
@@ -12814,24 +13071,26 @@ var GroupMessagePoller = class {
12814
13071
 
12815
13072
  // src/bridge/reconnect-notifier.ts
12816
13073
  var ReconnectNotifier = class {
12817
- constructor(channel, directory, now = Date.now, reconciliation) {
13074
+ constructor(channel, directory, now = Date.now, reconciliation, onFault) {
12818
13075
  this.channel = channel;
12819
13076
  this.directory = directory;
12820
13077
  this.now = now;
12821
13078
  this.reconciliation = reconciliation;
13079
+ this.onFault = onFault;
12822
13080
  }
12823
13081
  channel;
12824
13082
  directory;
12825
13083
  now;
12826
13084
  reconciliation;
13085
+ onFault;
12827
13086
  startedAt;
12828
13087
  async reconnecting() {
12829
13088
  if (this.startedAt !== void 0) return;
12830
13089
  this.startedAt = this.now();
12831
- await this.send(
12832
- "\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",
12833
- "\u26A0\uFE0F The bot connection is unstable and reconnecting automatically. New messages may be delayed."
12834
- );
13090
+ 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";
13091
+ const enUs = "\u26A0\uFE0F The bot connection is unstable and reconnecting automatically. New messages may be delayed.";
13092
+ await this.send(zhCn, enUs);
13093
+ await this.fault(this.directory?.recentDestination()?.scope, { zh: zhCn, en: enUs });
12835
13094
  }
12836
13095
  async reconnected() {
12837
13096
  if (this.startedAt === void 0) return;
@@ -12839,12 +13098,12 @@ var ReconnectNotifier = class {
12839
13098
  this.startedAt = void 0;
12840
13099
  const target = this.directory?.recentDestination();
12841
13100
  const summary = target ? this.reconciliation?.(target.scope) : void 0;
12842
- await this.send(
12843
- `\u2705 \u673A\u5668\u4EBA\u8FDE\u63A5\u5DF2\u6062\u590D\uFF08\u4E2D\u65AD\u7EA6 ${formatDuration(elapsed)}\uFF09\u3002${summary ? `
12844
- ${summary.zhCn}` : ""}`,
12845
- `\u2705 The bot connection recovered after about ${formatDurationEnglish(elapsed)}.${summary ? `
12846
- ${summary.enUs}` : ""}`
12847
- );
13101
+ const zhCn = `\u2705 \u673A\u5668\u4EBA\u8FDE\u63A5\u5DF2\u6062\u590D\uFF08\u4E2D\u65AD\u7EA6 ${formatDuration(elapsed)}\uFF09\u3002${summary ? `
13102
+ ${summary.zhCn}` : ""}`;
13103
+ const enUs = `\u2705 The bot connection recovered after about ${formatDurationEnglish(elapsed)}.${summary ? `
13104
+ ${summary.enUs}` : ""}`;
13105
+ await this.send(zhCn, enUs);
13106
+ await this.fault(target?.scope, { zh: zhCn, en: enUs });
12848
13107
  }
12849
13108
  async send(zhCn, enUs) {
12850
13109
  const target = this.directory?.recentDestination();
@@ -12854,6 +13113,13 @@ ${summary.enUs}` : ""}`
12854
13113
  ...target.threadId ? { threadId: target.threadId } : {}
12855
13114
  });
12856
13115
  }
13116
+ async fault(scope, title) {
13117
+ if (!scope || !this.onFault) return;
13118
+ try {
13119
+ await this.onFault(scope, title);
13120
+ } catch (error) {
13121
+ }
13122
+ }
12857
13123
  };
12858
13124
  function formatDurationEnglish(ms) {
12859
13125
  if (ms < 1e3) return "less than 1 second";
@@ -14056,7 +14322,8 @@ async function startChannel(deps) {
14056
14322
  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`,
14057
14323
  enUs: `Ledger reconciliation: queued ${counts.queued} \xB7 running ${counts.running} \xB7 interrupted ${counts.interrupted} \xB7 failed ${counts.failed}; use \`/jobs\` for details.`
14058
14324
  };
14059
- } : void 0
14325
+ } : void 0,
14326
+ deps.faultNotifier
14060
14327
  );
14061
14328
  const isolationStore = deps.isolationStore ?? EMPTY_ISOLATION_STORE;
14062
14329
  let groupPoller;
@@ -14191,6 +14458,8 @@ async function startChannel(deps) {
14191
14458
  ...deps.permissionPolicies ? { permissionPolicies: deps.permissionPolicies } : {},
14192
14459
  ...deps.notificationPreferences ? { notificationPreferences: deps.notificationPreferences } : {},
14193
14460
  ...deps.defaultNotificationPreference ? { defaultNotificationPreference: deps.defaultNotificationPreference } : {},
14461
+ ...deps.notificationChannels ? { notificationChannels: deps.notificationChannels } : {},
14462
+ ...deps.notificationSinks ? { notificationSinks: deps.notificationSinks } : {},
14194
14463
  ...deps.replyPolicies ? { replyPolicies: deps.replyPolicies } : {},
14195
14464
  ...deps.executionModes ? { executionModes: deps.executionModes } : {},
14196
14465
  ...deps.languagePolicies ? { languagePolicies: deps.languagePolicies } : {},
@@ -15555,6 +15824,11 @@ var NotificationDispatcher = class {
15555
15824
  setDefaultPreference(preference) {
15556
15825
  this.defaultPreference = clonePreference(preference);
15557
15826
  }
15827
+ /**
15828
+ * Primary notification path: send to Feishu (default first-class route) and,
15829
+ * when the scope preference lists extra channels, fan out to those sinks.
15830
+ * Returns `false` when the event is disabled or no Feishu destination exists.
15831
+ */
15558
15832
  async notify(scope, event, detail) {
15559
15833
  const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
15560
15834
  if (!preference?.events.includes(event)) return false;
@@ -15563,21 +15837,52 @@ var NotificationDispatcher = class {
15563
15837
  log.warn("notification", "target-unavailable", { scope, target: preference.target });
15564
15838
  return false;
15565
15839
  }
15566
- 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"];
15840
+ const title = titleForEvent(event);
15567
15841
  try {
15568
- await this.deps.send(destination.chatId, bilingualMarkdown(
15569
- `${title[0]}\uFF08scope \`${scope}\`\uFF09${detail ? `\uFF1A${detail}` : ""}`,
15570
- `${title[1]} (scope \`${scope}\`)${detail ? `: ${detail}` : ""}`
15571
- ), {
15842
+ await this.deps.send(destination.chatId, renderFeishuBody(title, scope, detail), {
15572
15843
  ...destination.threadId ? { threadId: destination.threadId } : {},
15573
15844
  ...destination.messageId ? { replyTo: destination.messageId } : {},
15574
15845
  ...preference.mentionUserIds.length > 0 ? { mentions: preference.mentionUserIds.map((userId) => ({ userId })) } : {}
15575
15846
  });
15576
- return true;
15577
15847
  } catch (error) {
15578
15848
  log.warn("notification", "send-failed", { scope, event, error });
15579
15849
  return false;
15580
15850
  }
15851
+ if (preference.sinks.length > 0) {
15852
+ await this.broadcastSinks(preference.sinks, scope, event, title, detail);
15853
+ }
15854
+ return true;
15855
+ }
15856
+ /**
15857
+ * Safety-net / fault class notification (issue #113): broadcast to every
15858
+ * enabled outbound sink regardless of the per-scope opt-in, so a crash /
15859
+ * reconnect / heartbeat anomaly is seen even when a scope has not enabled
15860
+ * proactive Feishu reminders. When the scope preference explicitly enables
15861
+ * the `urgent` event it is also sent to Feishu.
15862
+ */
15863
+ async notifyUrgent(scope, title, detail) {
15864
+ const channels = this.deps.sinks?.enabledChannels() ?? [];
15865
+ if (channels.length > 0) {
15866
+ await this.broadcastSinks(
15867
+ channels.map((channel) => channel.id),
15868
+ scope,
15869
+ "urgent",
15870
+ title,
15871
+ detail
15872
+ );
15873
+ }
15874
+ const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
15875
+ if (!preference?.events.includes("urgent")) return;
15876
+ const destination = preference.target ? this.deps.scopeDirectory.resolve(preference.target) ?? this.deps.scopeDirectory.resolveChat(preference.target) : this.deps.scopeDirectory.resolve(scope);
15877
+ if (!destination) return;
15878
+ try {
15879
+ await this.deps.send(destination.chatId, renderFeishuBody(title, scope, detail), {
15880
+ ...destination.threadId ? { threadId: destination.threadId } : {},
15881
+ ...destination.messageId ? { replyTo: destination.messageId } : {}
15882
+ });
15883
+ } catch (error) {
15884
+ log.warn("notification", "urgent-send-failed", { scope, error });
15885
+ }
15581
15886
  }
15582
15887
  scheduleApprovalReminder(scope, toolName) {
15583
15888
  const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
@@ -15588,10 +15893,196 @@ var NotificationDispatcher = class {
15588
15893
  timer.unref?.();
15589
15894
  return () => clearTimeout(timer);
15590
15895
  }
15896
+ async broadcastSinks(channelIds, scope, event, title, detail) {
15897
+ if (!this.deps.sinks || channelIds.length === 0) return;
15898
+ try {
15899
+ const outcome = await this.deps.sinks.broadcast(channelIds, {
15900
+ scope,
15901
+ event,
15902
+ title,
15903
+ ...detail === void 0 ? {} : { detail }
15904
+ });
15905
+ if (outcome.failures.length > 0) {
15906
+ log.warn("notification", "sink-failures", { scope, event, failures: outcome.failures });
15907
+ }
15908
+ } catch (error) {
15909
+ log.warn("notification", "sink-broadcast-error", { scope, event, error });
15910
+ }
15911
+ }
15591
15912
  };
15913
+ function titleForEvent(event) {
15914
+ switch (event) {
15915
+ case "completed":
15916
+ return { zh: "\u2705 \u4EFB\u52A1\u5DF2\u5B8C\u6210", en: "\u2705 Task completed" };
15917
+ case "failed":
15918
+ return { zh: "\u26A0\uFE0F \u4EFB\u52A1\u6267\u884C\u5931\u8D25", en: "\u26A0\uFE0F Task failed" };
15919
+ case "approval":
15920
+ return { zh: "\u23F3 \u4EFB\u52A1\u4ECD\u5728\u7B49\u5F85\u64CD\u4F5C\u5BA1\u6279", en: "\u23F3 Task is still waiting for tool approval" };
15921
+ case "urgent":
15922
+ return { zh: "\u{1F534} \u7A81\u53D1 / \u6545\u969C", en: "\u{1F534} Urgent / fault" };
15923
+ }
15924
+ }
15925
+ function renderFeishuBody(title, scope, detail) {
15926
+ return bilingualMarkdown(
15927
+ `${title.zh}\uFF08scope \`${scope}\`\uFF09${detail ? `\uFF1A${detail}` : ""}`,
15928
+ `${title.en} (scope \`${scope}\`)${detail ? `: ${detail}` : ""}`
15929
+ );
15930
+ }
15592
15931
  function clonePreference(preference) {
15593
- return preference ? { ...preference, events: [...preference.events], mentionUserIds: [...preference.mentionUserIds] } : void 0;
15932
+ return preference ? { ...preference, events: [...preference.events], mentionUserIds: [...preference.mentionUserIds], sinks: [...preference.sinks] } : void 0;
15933
+ }
15934
+
15935
+ // src/notify/sinks/http.ts
15936
+ async function postJson(url, body3) {
15937
+ const controller = new AbortController();
15938
+ const timer = setTimeout(() => controller.abort(), SINK_HTTP_TIMEOUT_MS);
15939
+ timer.unref?.();
15940
+ try {
15941
+ return await fetch(url, {
15942
+ method: "POST",
15943
+ headers: { "content-type": "application/json" },
15944
+ body: body3,
15945
+ signal: controller.signal
15946
+ });
15947
+ } finally {
15948
+ clearTimeout(timer);
15949
+ }
15950
+ }
15951
+
15952
+ // src/notify/sinks/text.ts
15953
+ function renderSinkText(message) {
15954
+ const zhLine = `${message.title.zh}\uFF08scope \`${message.scope}\`\uFF09${message.detail ? `\uFF1A${message.detail}` : ""}`;
15955
+ const enLine = `${message.title.en} (scope \`${message.scope}\`)${message.detail ? `: ${message.detail}` : ""}`;
15956
+ return `${stripMarkdown(zhLine)}
15957
+
15958
+ ${stripMarkdown(enLine)}`;
15959
+ }
15960
+ function escapeHtml(value) {
15961
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
15594
15962
  }
15963
+ function stripMarkdown(value) {
15964
+ return value.replaceAll("`", "").replaceAll("**", "").replaceAll("*", "");
15965
+ }
15966
+
15967
+ // src/notify/sinks/telegram.ts
15968
+ var TelegramSink = class {
15969
+ constructor(baseUrl = "https://api.telegram.org") {
15970
+ this.baseUrl = baseUrl;
15971
+ }
15972
+ baseUrl;
15973
+ type = "telegram";
15974
+ async send(channel, message) {
15975
+ const token = channel.secret;
15976
+ if (!token) {
15977
+ log.warn("sink:telegram", "missing-token", { channel: channel.id });
15978
+ return false;
15979
+ }
15980
+ const url = `${this.baseUrl}/bot${encodeURIComponent(token)}/sendMessage`;
15981
+ const body3 = JSON.stringify({
15982
+ chat_id: channel.destination,
15983
+ text: escapeHtml(renderSinkText(message)),
15984
+ parse_mode: "HTML",
15985
+ disable_web_page_preview: true
15986
+ });
15987
+ try {
15988
+ const response = await postJson(url, body3);
15989
+ const payload = await response.json();
15990
+ if (!response.ok || payload.ok !== true) {
15991
+ log.warn("sink:telegram", "send-failed", {
15992
+ channel: channel.id,
15993
+ error: payload.description ?? `http ${response.status}`
15994
+ });
15995
+ return false;
15996
+ }
15997
+ return true;
15998
+ } catch (error) {
15999
+ log.warn("sink:telegram", "send-error", { channel: channel.id, error });
16000
+ return false;
16001
+ }
16002
+ }
16003
+ };
16004
+
16005
+ // src/notify/sinks/wecom.ts
16006
+ var WeComSink = class {
16007
+ constructor(baseUrl = "https://qyapi.weixin.qq.com") {
16008
+ this.baseUrl = baseUrl;
16009
+ }
16010
+ baseUrl;
16011
+ type = "wecom";
16012
+ async send(channel, message) {
16013
+ const key = channel.destination;
16014
+ if (!key) {
16015
+ log.warn("sink:wecom", "missing-key", { channel: channel.id });
16016
+ return false;
16017
+ }
16018
+ const url = `${this.baseUrl}/cgi-bin/webhook/send?key=${encodeURIComponent(key)}`;
16019
+ const body3 = JSON.stringify({
16020
+ msgtype: "text",
16021
+ text: { content: renderSinkText(message) }
16022
+ });
16023
+ try {
16024
+ const response = await postJson(url, body3);
16025
+ const payload = await response.json();
16026
+ if (!response.ok || payload.errcode !== 0) {
16027
+ log.warn("sink:wecom", "send-failed", {
16028
+ channel: channel.id,
16029
+ error: payload.errmsg ?? `http ${response.status}`
16030
+ });
16031
+ return false;
16032
+ }
16033
+ return true;
16034
+ } catch (error) {
16035
+ log.warn("sink:wecom", "send-error", { channel: channel.id, error });
16036
+ return false;
16037
+ }
16038
+ }
16039
+ };
16040
+
16041
+ // src/notify/sinks/registry.ts
16042
+ var OutboundSinkRegistry = class {
16043
+ constructor(store, sinks = [new TelegramSink(), new WeComSink()]) {
16044
+ this.store = store;
16045
+ this.sinks = sinks;
16046
+ }
16047
+ store;
16048
+ sinks;
16049
+ /** Resolve only the enabled channels referenced by `channelIds`. */
16050
+ channelsForIds(channelIds) {
16051
+ const seen = new Set(channelIds);
16052
+ return this.store.list().filter((channel) => channel.enabled && seen.has(channel.id));
16053
+ }
16054
+ /** Channels currently enabled (for `/status` and `/channels`). */
16055
+ enabledChannels() {
16056
+ return this.store.list().filter((channel) => channel.enabled);
16057
+ }
16058
+ /** Best-effort fan-out; a failing channel never blocks the others. */
16059
+ async broadcast(channelIds, message) {
16060
+ const channelIdsToSend = [...new Set(channelIds)];
16061
+ const channels = this.channelsForIds(channelIdsToSend);
16062
+ const failures = [];
16063
+ let delivered = 0;
16064
+ for (const channel of channels) {
16065
+ const sink = this.sinkFor(channel.type);
16066
+ if (!sink) {
16067
+ log.warn("sinks", "unsupported-type", { channel: channel.id, type: channel.type });
16068
+ failures.push(channel.id);
16069
+ continue;
16070
+ }
16071
+ let ok = false;
16072
+ try {
16073
+ ok = await sink.send(channel, message);
16074
+ } catch (error) {
16075
+ log.warn("sinks", "channel-send-error", { channel: channel.id, error });
16076
+ }
16077
+ if (ok) delivered += 1;
16078
+ else failures.push(channel.id);
16079
+ }
16080
+ return { delivered, failures, total: channels.length };
16081
+ }
16082
+ sinkFor(type) {
16083
+ return this.sinks.find((sink) => sink.type === type);
16084
+ }
16085
+ };
15595
16086
 
15596
16087
  // src/bridge/reply-dispatcher.ts
15597
16088
  var ReplyDispatcher = class {
@@ -15651,8 +16142,8 @@ var ReplyDispatcher = class {
15651
16142
  }
15652
16143
  }
15653
16144
  };
15654
- function replyRoute(reply8) {
15655
- return `${reply8.chatId}\0${reply8.options?.threadId ?? ""}`;
16145
+ function replyRoute(reply9) {
16146
+ return `${reply9.chatId}\0${reply9.options?.threadId ?? ""}`;
15656
16147
  }
15657
16148
  function mergedMarkdown(batch) {
15658
16149
  if (batch.length === 1) return batch[0].markdown;
@@ -15787,7 +16278,7 @@ async function onboardPersonalAgent(deps = {}) {
15787
16278
  }
15788
16279
 
15789
16280
  // src/media/attachments.ts
15790
- import { mkdir as mkdir16, readFile as readFile23, rename as rename3, rm as rm9, stat as stat9 } from "fs/promises";
16281
+ import { mkdir as mkdir16, readFile as readFile24, rename as rename3, rm as rm9, stat as stat9 } from "fs/promises";
15791
16282
  import { join as join23 } from "path";
15792
16283
  var MAX_TEXT_FILE_BYTES = 256e3;
15793
16284
  function assertSafeMediaName(mediaDir, destination) {
@@ -15828,7 +16319,7 @@ async function prepareAttachments(channel, message, mediaDir) {
15828
16319
  result.textFileNotes.push(`[attachment: ${resource.fileName ?? resource.fileKey}] ${destination}`);
15829
16320
  continue;
15830
16321
  }
15831
- const content = await readFile23(destination, "utf8");
16322
+ const content = await readFile24(destination, "utf8");
15832
16323
  const safeContent = truncateUtf8Safe(content, MAX_TEXT_FILE_BYTES);
15833
16324
  result.textFileNotes.push(
15834
16325
  `[attachment: ${resource.fileName ?? resource.fileKey}]
@@ -15839,7 +16330,7 @@ ${safeContent}`
15839
16330
  }
15840
16331
 
15841
16332
  // src/session/store.ts
15842
- import { readFile as readFile24 } from "fs/promises";
16333
+ import { readFile as readFile25 } from "fs/promises";
15843
16334
  var MAX_CONTEXT_SNAPSHOTS_PER_WORKSPACE = 32;
15844
16335
  var SessionStore = class {
15845
16336
  constructor(path) {
@@ -15854,7 +16345,7 @@ var SessionStore = class {
15854
16345
  pendingArchive = Promise.resolve();
15855
16346
  async load() {
15856
16347
  try {
15857
- const raw = JSON.parse(await readFile24(this.path, "utf8"));
16348
+ const raw = JSON.parse(await readFile25(this.path, "utf8"));
15858
16349
  const legacy = raw.schema !== 2 || !isObject(raw.workspaces);
15859
16350
  this.data = legacy ? migrateLegacy(raw) : normalizeV2(raw.workspaces);
15860
16351
  this.legacyScopes = legacy ? new Set(Object.keys(this.data.workspaces)) : /* @__PURE__ */ new Set();
@@ -15951,13 +16442,13 @@ var SessionStore = class {
15951
16442
  fullHistoryFor(scopeId, cwd) {
15952
16443
  return this.historyFor(scopeId, cwd);
15953
16444
  }
15954
- recordUsage(scopeId, cwd, usage3) {
16445
+ recordUsage(scopeId, cwd, usage4) {
15955
16446
  const state = this.ensureState(scopeId, cwd);
15956
16447
  const next = { ...state.metrics };
15957
- addMetric(next, "inputTokens", usage3.inputTokens);
15958
- addMetric(next, "outputTokens", usage3.outputTokens);
15959
- addMetric(next, "cacheReadTokens", usage3.cacheReadTokens);
15960
- addMetric(next, "cacheWriteTokens", usage3.cacheWriteTokens);
16448
+ addMetric(next, "inputTokens", usage4.inputTokens);
16449
+ addMetric(next, "outputTokens", usage4.outputTokens);
16450
+ addMetric(next, "cacheReadTokens", usage4.cacheReadTokens);
16451
+ addMetric(next, "cacheWriteTokens", usage4.cacheWriteTokens);
15961
16452
  state.metrics = next;
15962
16453
  this.schedulePersist();
15963
16454
  }
@@ -16173,7 +16664,7 @@ function normalizeContextSnapshot(value) {
16173
16664
  }
16174
16665
 
16175
16666
  // src/session/projection-store.ts
16176
- import { readFile as readFile25 } from "fs/promises";
16667
+ import { readFile as readFile26 } from "fs/promises";
16177
16668
  import { randomUUID as randomUUID11 } from "crypto";
16178
16669
  var MAX_RECENT_MESSAGES = 64;
16179
16670
  var MAX_PROMPT_CORRELATIONS = 64;
@@ -16187,7 +16678,7 @@ var SessionProjectionStore = class {
16187
16678
  saving = Promise.resolve();
16188
16679
  async load() {
16189
16680
  try {
16190
- const parsed = JSON.parse(await readFile25(this.path, "utf8"));
16681
+ const parsed = JSON.parse(await readFile26(this.path, "utf8"));
16191
16682
  const bindings = Object.fromEntries(
16192
16683
  Object.values(parsed.bindings ?? {}).flatMap((value) => {
16193
16684
  const normalized = normalizeBinding(value);
@@ -16399,7 +16890,7 @@ function normalizeCorrelation(value) {
16399
16890
  // src/session/archive.ts
16400
16891
  import { execFile as execFile2 } from "child_process";
16401
16892
  import { randomBytes as randomBytes3 } from "crypto";
16402
- import { access, mkdir as mkdir17, readdir as readdir3, readFile as readFile26, unlink, writeFile as writeFile8 } from "fs/promises";
16893
+ import { access, mkdir as mkdir17, readdir as readdir3, readFile as readFile27, unlink, writeFile as writeFile8 } from "fs/promises";
16403
16894
  import { join as join24 } from "path";
16404
16895
  import { promisify } from "util";
16405
16896
  var execFileAsync = promisify(execFile2);
@@ -16581,8 +17072,8 @@ var SessionArchive = class {
16581
17072
  const jsonlPath = join24(scopeDir, `${id}.jsonl`);
16582
17073
  const markdownPath = join24(scopeDir, `${id}.md`);
16583
17074
  const [jsonl, markdown2] = await Promise.all([
16584
- readFile26(jsonlPath, "utf8"),
16585
- readFile26(markdownPath, "utf8")
17075
+ readFile27(jsonlPath, "utf8"),
17076
+ readFile27(markdownPath, "utf8")
16586
17077
  ]);
16587
17078
  const [first = "", ...rest] = jsonl.split("\n");
16588
17079
  const header = JSON.parse(first);
@@ -16697,7 +17188,7 @@ async function defaultRunGit(args, cwd) {
16697
17188
  }
16698
17189
  async function readArchiveHeader(jsonlPath) {
16699
17190
  try {
16700
- const raw = await readFile26(jsonlPath, "utf8");
17191
+ const raw = await readFile27(jsonlPath, "utf8");
16701
17192
  const firstLine = raw.split("\n", 1)[0];
16702
17193
  if (!firstLine) return void 0;
16703
17194
  const header = JSON.parse(firstLine);
@@ -16747,7 +17238,7 @@ async function defaultCopyRulesFile(source, target) {
16747
17238
  await mkdir18(dirname16(target), { recursive: true });
16748
17239
  await copyFile(source, target);
16749
17240
  }
16750
- function slugify2(scope) {
17241
+ function slugify3(scope) {
16751
17242
  const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[.]+/, "").replace(/^-+|-+$/g, "").slice(0, 64);
16752
17243
  return slug || "session";
16753
17244
  }
@@ -16763,7 +17254,7 @@ var GitWorktreeManager = class {
16763
17254
  async ensure(scope, base) {
16764
17255
  const isGit = await this.isGitRepository(base);
16765
17256
  if (!isGit) return { cwd: base, created: false };
16766
- const scopeSlug = slugify2(scope);
17257
+ const scopeSlug = slugify3(scope);
16767
17258
  const project = createHash2("sha256").update(base).digest("hex").slice(0, 10);
16768
17259
  const slug = `${scopeSlug}-${project}`;
16769
17260
  const target = join25(this.options.worktreesRoot, slug);
@@ -16800,7 +17291,7 @@ var GitWorktreeManager = class {
16800
17291
  }
16801
17292
  /** Owning main worktree for the legacy scope-only linked worktree. */
16802
17293
  async legacyWorkspaceBase(scope) {
16803
- const legacyTarget = join25(this.options.worktreesRoot, slugify2(scope));
17294
+ const legacyTarget = join25(this.options.worktreesRoot, slugify3(scope));
16804
17295
  if (!await exists2(legacyTarget)) return void 0;
16805
17296
  try {
16806
17297
  const output = await this.runGit(["worktree", "list", "--porcelain"], legacyTarget);
@@ -16832,7 +17323,7 @@ async function samePath(left, right) {
16832
17323
  }
16833
17324
 
16834
17325
  // src/workspace/store.ts
16835
- import { readFile as readFile27 } from "fs/promises";
17326
+ import { readFile as readFile28 } from "fs/promises";
16836
17327
  var WorkspaceStore = class {
16837
17328
  data = { chats: {}, named: {}, lastUsed: {} };
16838
17329
  saving = Promise.resolve();
@@ -16842,7 +17333,7 @@ var WorkspaceStore = class {
16842
17333
  }
16843
17334
  async load() {
16844
17335
  try {
16845
- const raw = await readFile27(this.path, "utf8");
17336
+ const raw = await readFile28(this.path, "utf8");
16846
17337
  const parsed = JSON.parse(raw);
16847
17338
  this.data = {
16848
17339
  chats: parsed.chats ?? {},
@@ -16961,7 +17452,7 @@ var UpdateNotifier = class {
16961
17452
  };
16962
17453
 
16963
17454
  // src/bot/fleet-store.ts
16964
- import { mkdir as mkdir19, readFile as readFile28 } from "fs/promises";
17455
+ import { mkdir as mkdir19, readFile as readFile29 } from "fs/promises";
16965
17456
  import { dirname as dirname17 } from "path";
16966
17457
  function validBotInstanceName(name) {
16967
17458
  return /^[a-z][a-z0-9-]{0,31}$/u.test(name);
@@ -16974,7 +17465,7 @@ var BotFleetStore = class {
16974
17465
  data = { schemaVersion: 1, bots: {} };
16975
17466
  async load() {
16976
17467
  try {
16977
- const parsed = JSON.parse(await readFile28(this.path, "utf8"));
17468
+ const parsed = JSON.parse(await readFile29(this.path, "utf8"));
16978
17469
  this.data = {
16979
17470
  schemaVersion: 1,
16980
17471
  bots: Object.fromEntries(
@@ -17102,7 +17593,7 @@ function isFleetEntry(value) {
17102
17593
  }
17103
17594
 
17104
17595
  // src/bot/handoff-guard.ts
17105
- import { mkdir as mkdir20, readFile as readFile29 } from "fs/promises";
17596
+ import { mkdir as mkdir20, readFile as readFile30 } from "fs/promises";
17106
17597
  import { dirname as dirname18 } from "path";
17107
17598
  var BotHandoffGuard = class {
17108
17599
  constructor(path) {
@@ -17143,7 +17634,7 @@ var BotHandoffGuard = class {
17143
17634
  }
17144
17635
  async read() {
17145
17636
  try {
17146
- const parsed = JSON.parse(await readFile29(this.path, "utf8"));
17637
+ const parsed = JSON.parse(await readFile30(this.path, "utf8"));
17147
17638
  return { schemaVersion: 1, chats: parsed.chats ?? {} };
17148
17639
  } catch (error) {
17149
17640
  if (error.code !== "ENOENT") throw error;
@@ -17169,7 +17660,7 @@ init_dsh_runtime();
17169
17660
  import { homedir as homedir16 } from "os";
17170
17661
 
17171
17662
  // src/bot/job-ledger.ts
17172
- import { readFile as readFile30, rename as rename4 } from "fs/promises";
17663
+ import { readFile as readFile31, rename as rename4 } from "fs/promises";
17173
17664
  var EMPTY_COUNTS = {
17174
17665
  queued: 0,
17175
17666
  running: 0,
@@ -17192,7 +17683,7 @@ var JobLedger = class {
17192
17683
  if (this.path === ":memory:") return;
17193
17684
  let raw;
17194
17685
  try {
17195
- raw = await readFile30(this.path, "utf8");
17686
+ raw = await readFile31(this.path, "utf8");
17196
17687
  } catch (error) {
17197
17688
  if (error.code === "ENOENT") return;
17198
17689
  throw error;
@@ -17933,7 +18424,7 @@ init_own_package();
17933
18424
  // src/guardian/update-handoff.ts
17934
18425
  import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
17935
18426
  import { spawn as spawn7 } from "child_process";
17936
- import { chmod, mkdir as mkdir21, readFile as readFile31, rm as rm10 } from "fs/promises";
18427
+ import { chmod, mkdir as mkdir21, readFile as readFile32, rm as rm10 } from "fs/promises";
17937
18428
  import { dirname as dirname19, join as join26 } from "path";
17938
18429
  init_own_package();
17939
18430
  init_process();
@@ -17963,7 +18454,7 @@ function guardianUpdateFailureHint(code) {
17963
18454
  }
17964
18455
  async function loadState(file) {
17965
18456
  try {
17966
- const parsed = JSON.parse(await readFile31(file, "utf8"));
18457
+ const parsed = JSON.parse(await readFile32(file, "utf8"));
17967
18458
  return parsed.schemaVersion === 1 && typeof parsed.id === "string" ? parsed : void 0;
17968
18459
  } catch (error) {
17969
18460
  if (error.code === "ENOENT") return void 0;
@@ -18359,6 +18850,8 @@ async function startBridgeEngine(options) {
18359
18850
  const isolationStore = new IsolationStore(paths.profilePath(profileName, "isolation.json"));
18360
18851
  const permissionPolicies = new PermissionPolicyStore(paths.permissionPoliciesFile(profileName));
18361
18852
  const notificationPreferences = new NotificationPreferenceStore(paths.notificationPreferencesFile(profileName));
18853
+ const notificationChannels = new NotificationChannelStore(paths.notificationChannelsFile(profileName));
18854
+ const notificationSinks = new OutboundSinkRegistry(notificationChannels);
18362
18855
  const replyPolicies = new ReplyPolicyStore(paths.replyPoliciesFile(profileName));
18363
18856
  const executionModes = new ExecutionModeStore(paths.executionModesFile(profileName));
18364
18857
  const languagePolicies = new LanguagePolicyStore(paths.languagePoliciesFile(profileName));
@@ -18381,6 +18874,7 @@ async function startBridgeEngine(options) {
18381
18874
  isolationStore.load(),
18382
18875
  permissionPolicies.load(),
18383
18876
  notificationPreferences.load(),
18877
+ notificationChannels.load(),
18384
18878
  replyPolicies.load(),
18385
18879
  executionModes.load(),
18386
18880
  languagePolicies.load()
@@ -18450,7 +18944,8 @@ async function startBridgeEngine(options) {
18450
18944
  send: async (chatId, markdown2, options2) => {
18451
18945
  if (!streaming) throw new Error("bridge channel is not ready");
18452
18946
  await streaming.sendMarkdown(chatId, markdown2, options2);
18453
- }
18947
+ },
18948
+ sinks: notificationSinks
18454
18949
  });
18455
18950
  const replyDispatcher = new ReplyDispatcher({
18456
18951
  policies: replyPolicies,
@@ -18741,6 +19236,9 @@ async function startBridgeEngine(options) {
18741
19236
  approvals,
18742
19237
  permissionPolicies,
18743
19238
  notificationPreferences,
19239
+ notificationChannels,
19240
+ notificationSinks,
19241
+ faultNotifier: (scope, title, detail) => notificationDispatcher.notifyUrgent(scope, title, detail),
18744
19242
  ...defaultNotificationPreference ? { defaultNotificationPreference: { ...defaultNotificationPreference, events: [...defaultNotificationPreference.events] } } : {},
18745
19243
  replyPolicies,
18746
19244
  executionModes,
@@ -19003,6 +19501,7 @@ function notificationPreferenceFor(value) {
19003
19501
  return {
19004
19502
  events: value === "all" ? ["completed", "failed", "approval"] : ["completed", "failed"],
19005
19503
  mentionUserIds: [],
19504
+ sinks: [],
19006
19505
  approvalReminderMs: 10 * 6e4
19007
19506
  };
19008
19507
  }
@@ -19038,7 +19537,7 @@ function waitForShutdown() {
19038
19537
  init_dsh_runtime();
19039
19538
  init_own_package();
19040
19539
  import { spawn as spawn8 } from "child_process";
19041
- import { mkdir as mkdir23, readFile as readFile32, writeFile as writeFile9 } from "fs/promises";
19540
+ import { mkdir as mkdir23, readFile as readFile33, writeFile as writeFile9 } from "fs/promises";
19042
19541
  import { homedir as homedir17 } from "os";
19043
19542
  import { join as join28 } from "path";
19044
19543
  import { parse } from "yaml";
@@ -19119,7 +19618,7 @@ async function approveBuilds(profileDir) {
19119
19618
  const workspaceFile = join28(profileDir, "pnpm-workspace.yaml");
19120
19619
  let existing = "";
19121
19620
  try {
19122
- existing = await readFile32(workspaceFile, "utf8");
19621
+ existing = await readFile33(workspaceFile, "utf8");
19123
19622
  } catch {
19124
19623
  }
19125
19624
  const allowBuilds = [
@@ -19147,8 +19646,8 @@ async function preserveInstalledPnpmVersion(profileDir) {
19147
19646
  let modulesText;
19148
19647
  try {
19149
19648
  [packageText, modulesText] = await Promise.all([
19150
- readFile32(packageFile, "utf8"),
19151
- readFile32(modulesFile, "utf8")
19649
+ readFile33(packageFile, "utf8"),
19650
+ readFile33(modulesFile, "utf8")
19152
19651
  ]);
19153
19652
  } catch {
19154
19653
  return;