dsh-lark-bot 0.19.11 → 0.19.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -296,6 +296,16 @@ var init_profile_package = __esm({
296
296
  }
297
297
  });
298
298
 
299
+ // src/adapters/model-choice.ts
300
+ function resolveModelChoice(preferenceModel, envModel) {
301
+ return preferenceModel?.trim() || envModel;
302
+ }
303
+ var init_model_choice = __esm({
304
+ "src/adapters/model-choice.ts"() {
305
+ "use strict";
306
+ }
307
+ });
308
+
299
309
  // src/adapters/dsh/acp-runtime.ts
300
310
  import { spawn as spawn4 } from "cross-spawn";
301
311
  import { existsSync as existsSync4, realpathSync as realpathSync3 } from "fs";
@@ -666,7 +676,7 @@ async function buildAcpAgentAdapter(env, preferences) {
666
676
  home: homedir2(),
667
677
  env: process.env,
668
678
  provider: env.provider,
669
- model: preferences.model ?? env.model,
679
+ model: resolveModelChoice(preferences.model, env.model),
670
680
  ...env.dshExplicit ? { command: env.dshCommand, args: env.dshArgs } : {}
671
681
  };
672
682
  const ensure = await ensureAcpProfile(runtimeOptions);
@@ -679,7 +689,7 @@ async function buildAcpAgentAdapter(env, preferences) {
679
689
  return new AcpDshAdapter({
680
690
  launch,
681
691
  provider: env.provider,
682
- model: preferences.model ?? env.model
692
+ model: resolveModelChoice(preferences.model, env.model)
683
693
  });
684
694
  }
685
695
  var AcpDshAdapter;
@@ -688,6 +698,7 @@ var init_acp_adapter = __esm({
688
698
  "use strict";
689
699
  init_acp_runtime();
690
700
  init_event_channel();
701
+ init_model_choice();
691
702
  AcpDshAdapter = class {
692
703
  id = "dsh-acp";
693
704
  displayName = "DeepSeek Harness (ACP)";
@@ -814,14 +825,14 @@ var init_acp_adapter = __esm({
814
825
  return;
815
826
  }
816
827
  if (response.usage) {
817
- const usage3 = {
828
+ const usage4 = {
818
829
  type: "usage",
819
830
  ...typeof response.usage.inputTokens === "number" ? { inputTokens: response.usage.inputTokens } : {},
820
831
  ...typeof response.usage.outputTokens === "number" ? { outputTokens: response.usage.outputTokens } : {},
821
832
  ...typeof response.usage.cachedReadTokens === "number" ? { cacheReadTokens: response.usage.cachedReadTokens } : {},
822
833
  ...typeof response.usage.cachedWriteTokens === "number" ? { cacheWriteTokens: response.usage.cachedWriteTokens } : {}
823
834
  };
824
- channel.push(usage3);
835
+ channel.push(usage4);
825
836
  }
826
837
  })().catch((error) => {
827
838
  if (!stopRequested.value) {
@@ -972,12 +983,18 @@ async function listProcesses(platform = process.platform, run = captureOutput) {
972
983
  return listProcessesPosix(run);
973
984
  }
974
985
  async function listProcessesPosix(run) {
975
- const { stdout } = await run("ps", ["-axo", "pid=,args="], 1e4);
986
+ const { stdout } = await run("ps", ["-axo", "pid=,ppid=,args="], 1e4);
976
987
  const result = [];
977
988
  for (const line of stdout.split("\n")) {
978
- const match = /^\s*(\d+)\s+(.+)$/.exec(line);
979
- if (match) {
980
- result.push({ pid: Number(match[1]), cmdline: match[2] ?? "" });
989
+ const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line);
990
+ if (!match) continue;
991
+ const pid = Number(match[1]);
992
+ const ppid = Number(match[2]);
993
+ if (!Number.isInteger(pid) || pid <= 0) continue;
994
+ if (Number.isInteger(ppid) && ppid > 0) {
995
+ result.push({ pid, ppid, cmdline: match[3] ?? "" });
996
+ } else {
997
+ result.push({ pid, cmdline: match[3] ?? "" });
981
998
  }
982
999
  }
983
1000
  return result;
@@ -989,7 +1006,7 @@ async function listProcessesWindows(run) {
989
1006
  "-NoProfile",
990
1007
  "-NonInteractive",
991
1008
  "-Command",
992
- "Get-CimInstance Win32_Process | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress"
1009
+ "Get-CimInstance Win32_Process | Select-Object ProcessId, ParentProcessId, CommandLine | ConvertTo-Json -Compress"
993
1010
  ],
994
1011
  1e4
995
1012
  );
@@ -1000,8 +1017,11 @@ async function listProcessesWindows(run) {
1000
1017
  if (typeof row !== "object" || row === null) return [];
1001
1018
  const record = row;
1002
1019
  const pid = Number(record.ProcessId);
1020
+ const ppid = Number(record.ParentProcessId);
1003
1021
  const cmdline = record.CommandLine;
1004
- return Number.isInteger(pid) && pid > 0 && typeof cmdline === "string" ? [{ pid, cmdline }] : [];
1022
+ if (!Number.isInteger(pid) || pid <= 0 || typeof cmdline !== "string") return [];
1023
+ if (Number.isInteger(ppid) && ppid > 0) return [{ pid, ppid, cmdline }];
1024
+ return [{ pid, cmdline }];
1005
1025
  });
1006
1026
  } catch {
1007
1027
  return [];
@@ -1703,7 +1723,7 @@ var DshProviderManager = class {
1703
1723
  try {
1704
1724
  catalogProviders = await this.catalog.listProviders();
1705
1725
  } catch (error) {
1706
- log.warn("model-catalog", "refresh-failed", {
1726
+ log.info("model-catalog", "refresh-failed", {
1707
1727
  error: error instanceof Error ? error.message : String(error)
1708
1728
  });
1709
1729
  }
@@ -2482,11 +2502,11 @@ function translateToolResult(data) {
2482
2502
  }
2483
2503
  function translateAssistantMessage(data) {
2484
2504
  if (!isRecord3(data) || !isRecord3(data.usage)) return [];
2485
- const usage3 = data.usage;
2486
- const inputTokens = typeof usage3.inputTokens === "number" ? usage3.inputTokens : void 0;
2487
- const outputTokens = typeof usage3.outputTokens === "number" ? usage3.outputTokens : void 0;
2488
- const cacheReadTokens = typeof usage3.cacheReadTokens === "number" ? usage3.cacheReadTokens : void 0;
2489
- 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;
2490
2510
  if (inputTokens === void 0 && outputTokens === void 0 && cacheReadTokens === void 0 && cacheWriteTokens === void 0) return [];
2491
2511
  return [
2492
2512
  {
@@ -3442,6 +3462,7 @@ var WebDshAdapter = class {
3442
3462
  };
3443
3463
 
3444
3464
  // src/adapters/index.ts
3465
+ init_model_choice();
3445
3466
  async function resolveAdapterRoute(input, source) {
3446
3467
  const provider = input.provider?.trim() || void 0;
3447
3468
  const model = input.model?.trim() || void 0;
@@ -3461,7 +3482,7 @@ async function resolveAdapterRoute(input, source) {
3461
3482
  return void 0;
3462
3483
  }
3463
3484
  async function buildAgentAdapter(env, preferences = { stopGraceMs: void 0, model: void 0 }) {
3464
- const configuredModel = preferences.model ?? env.model;
3485
+ const configuredModel = resolveModelChoice(preferences.model, env.model);
3465
3486
  const managedRoute = env.adapterMode === "sdk" || env.adapterMode === "acp" ? await resolveAdapterRoute(
3466
3487
  { provider: env.provider, model: configuredModel },
3467
3488
  new DshProviderManager({ env: process.env })
@@ -3493,7 +3514,7 @@ async function buildAgentAdapter(env, preferences = { stopGraceMs: void 0, model
3493
3514
  return new WebDshAdapter({
3494
3515
  baseUrl: env.webBaseUrl,
3495
3516
  provider: env.provider,
3496
- model: preferences.model ?? env.model
3517
+ model: resolveModelChoice(preferences.model, env.model)
3497
3518
  });
3498
3519
  }
3499
3520
  case "sdk":
@@ -3661,6 +3682,7 @@ function resolveAppPaths(root = defaultHome()) {
3661
3682
  jobsFile: (profile) => profilePath(profile, "jobs.json"),
3662
3683
  permissionPoliciesFile: (profile) => profilePath(profile, "permission-policies.json"),
3663
3684
  notificationPreferencesFile: (profile) => profilePath(profile, "notification-preferences.json"),
3685
+ notificationChannelsFile: (profile) => profilePath(profile, "notification-channels.json"),
3664
3686
  replyPoliciesFile: (profile) => profilePath(profile, "reply-policies.json"),
3665
3687
  executionModesFile: (profile) => profilePath(profile, "execution-modes.json"),
3666
3688
  languagePoliciesFile: (profile) => profilePath(profile, "language-policy.json"),
@@ -4189,17 +4211,36 @@ var ConfigStore = class {
4189
4211
  };
4190
4212
 
4191
4213
  // src/guardian/install.ts
4192
- init_own_package();
4193
- init_dsh_runtime();
4194
- init_process();
4195
4214
  import { mkdir as mkdir8, readFile as readFile8, rm as rm3, writeFile as writeFile5 } from "fs/promises";
4196
4215
  import { existsSync as existsSync5 } from "fs";
4197
4216
  import { homedir as homedir6 } from "os";
4198
- import { delimiter, dirname as dirname9, join as join11 } from "path";
4217
+ import { dirname as dirname10, join as join11 } from "path";
4218
+
4219
+ // src/platform/path.ts
4220
+ import { delimiter, dirname as dirname8 } from "path";
4221
+ function sanitizeServicePath(nodeBin, inheritedPath = process.env.PATH) {
4222
+ const entries = [dirname8(nodeBin), ...inheritedPath?.split(delimiter) ?? []];
4223
+ const seen = /* @__PURE__ */ new Set();
4224
+ return entries.filter((entry) => {
4225
+ if (!entry) return false;
4226
+ const normalized = entry.replaceAll("\\", "/");
4227
+ if (normalized.includes("/node_modules/.bin") || /\/_npx(?:\/|$)/.test(normalized) || /\/guardian\/update-worker\/npm-cache(?:\/|$)/.test(normalized)) {
4228
+ return false;
4229
+ }
4230
+ if (seen.has(entry)) return false;
4231
+ seen.add(entry);
4232
+ return true;
4233
+ }).join(delimiter);
4234
+ }
4235
+
4236
+ // src/guardian/install.ts
4237
+ init_own_package();
4238
+ init_dsh_runtime();
4239
+ init_process();
4199
4240
 
4200
4241
  // src/guardian/state.ts
4201
4242
  import { readFile as readFile7 } from "fs/promises";
4202
- import { dirname as dirname8 } from "path";
4243
+ import { dirname as dirname9 } from "path";
4203
4244
  import { mkdir as mkdir7 } from "fs/promises";
4204
4245
  var DEFAULT_DSH_PROFILE = "dsh-lark";
4205
4246
  var DEFAULT_BRIDGE_PROFILE = "default";
@@ -4241,7 +4282,7 @@ async function loadGuardianState(file, fallback) {
4241
4282
  }
4242
4283
  async function saveGuardianState(file, state) {
4243
4284
  const next = { ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
4244
- await mkdir7(dirname8(file), { recursive: true });
4285
+ await mkdir7(dirname9(file), { recursive: true });
4245
4286
  await writeFileAtomic(file, `${JSON.stringify(next, null, 2)}
4246
4287
  `, { mode: 384 });
4247
4288
  }
@@ -4295,7 +4336,7 @@ function guardianServiceFilePath(platform, root) {
4295
4336
  }
4296
4337
  function systemdUnit(nodeBin, cliEntry, env = {}) {
4297
4338
  const renderedEnv = {
4298
- PATH: env.PATH ?? dirname9(nodeBin),
4339
+ PATH: env.PATH ?? dirname10(nodeBin),
4299
4340
  ...env
4300
4341
  };
4301
4342
  const envLines = Object.entries(renderedEnv).map(([key, value]) => `Environment=${key}=${value}`).join("\n");
@@ -4318,18 +4359,7 @@ function systemdUnit(nodeBin, cliEntry, env = {}) {
4318
4359
  ].join("\n");
4319
4360
  }
4320
4361
  function stableGuardianServicePath(nodeBin, inheritedPath = process.env.PATH) {
4321
- const entries = [dirname9(nodeBin), ...inheritedPath?.split(delimiter) ?? []];
4322
- const seen = /* @__PURE__ */ new Set();
4323
- return entries.filter((entry) => {
4324
- if (!entry) return false;
4325
- const normalized = entry.replaceAll("\\", "/");
4326
- if (normalized.includes("/node_modules/.bin") || /\/_npx(?:\/|$)/.test(normalized) || /\/guardian\/update-worker\/npm-cache(?:\/|$)/.test(normalized)) {
4327
- return false;
4328
- }
4329
- if (seen.has(entry)) return false;
4330
- seen.add(entry);
4331
- return true;
4332
- }).join(delimiter);
4362
+ return sanitizeServicePath(nodeBin, inheritedPath);
4333
4363
  }
4334
4364
  function launchdPlist(nodeBin, cliEntry, label2 = `io.dsh-lark.${GUARDIAN_LABEL}`, logPath = "/tmp/dsh-lark-guardian.log") {
4335
4365
  return [
@@ -4666,7 +4696,7 @@ async function detectUpgradeState(options = {}) {
4666
4696
 
4667
4697
  // src/upgrade/state.ts
4668
4698
  import { mkdir as mkdir9, readFile as readFile11 } from "fs/promises";
4669
- import { dirname as dirname10, join as join13 } from "path";
4699
+ import { dirname as dirname11, join as join13 } from "path";
4670
4700
  function upgradeStatePath(root) {
4671
4701
  return join13(root, "upgrade-state.json");
4672
4702
  }
@@ -4682,7 +4712,7 @@ async function loadUpgradeState(file) {
4682
4712
  }
4683
4713
  }
4684
4714
  async function saveUpgradeState(file, state) {
4685
- await mkdir9(dirname10(file), { recursive: true });
4715
+ await mkdir9(dirname11(file), { recursive: true });
4686
4716
  await writeFileAtomic(file, JSON.stringify(state, null, 2));
4687
4717
  }
4688
4718
 
@@ -4765,13 +4795,13 @@ function packageSpecFor(name, version) {
4765
4795
 
4766
4796
  // src/service/manager.ts
4767
4797
  import { mkdir as mkdir14, readFile as readFile14, rename as rename2, rm as rm7, stat as stat5 } from "fs/promises";
4768
- import { dirname as dirname13, join as join19 } from "path";
4798
+ import { dirname as dirname14, join as join19 } from "path";
4769
4799
  import { homedir as homedir11 } from "os";
4770
4800
  init_dsh_runtime();
4771
4801
 
4772
4802
  // src/service/command.ts
4773
4803
  import { execFile } from "child_process";
4774
- import { basename as basename3, dirname as dirname11, join as join14 } from "path";
4804
+ import { basename as basename3, dirname as dirname12, join as join14 } from "path";
4775
4805
  import { fileURLToPath as fileURLToPath2 } from "url";
4776
4806
  var runCommand = (command, args, options) => new Promise((resolve6) => {
4777
4807
  execFile(
@@ -4797,7 +4827,7 @@ function resolveCliJsPath(metaUrl = import.meta.url) {
4797
4827
  const current = fileURLToPath2(metaUrl);
4798
4828
  const name = basename3(current);
4799
4829
  if (name === "cli.js" || name === "cli.mjs" || name === "cli.cjs") return current;
4800
- return join14(dirname11(current), "..", "dist", "cli.js");
4830
+ return join14(dirname12(current), "..", "dist", "cli.js");
4801
4831
  }
4802
4832
  function slugify(value) {
4803
4833
  const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -4840,6 +4870,9 @@ function snapshotServiceEnv(source = process.env, extraKeys = [], inherited = {}
4840
4870
  }
4841
4871
  }
4842
4872
  }
4873
+ if (typeof env.PATH === "string" && env.PATH.length > 0) {
4874
+ env.PATH = sanitizeServicePath(process.execPath, env.PATH);
4875
+ }
4843
4876
  return env;
4844
4877
  }
4845
4878
  function formatEnvFile(env) {
@@ -5425,7 +5458,7 @@ var PortableServiceController = class {
5425
5458
 
5426
5459
  // src/service/windows-task.ts
5427
5460
  import { mkdir as mkdir13, writeFile as writeFile7 } from "fs/promises";
5428
- import { dirname as dirname12, join as join18 } from "path";
5461
+ import { dirname as dirname13, join as join18 } from "path";
5429
5462
  function psQuote(value) {
5430
5463
  return `'${value.replace(/'/g, "''")}'`;
5431
5464
  }
@@ -5436,7 +5469,7 @@ function buildScheduledTaskScript(action, spec) {
5436
5469
  `$arguments = ${psQuote(spec.commandArgs.map((arg) => `"${arg.replace(/"/g, '\\"')}"`).join(" "))}`,
5437
5470
  `$log = ${psQuote(spec.logFile)}`,
5438
5471
  `$profile = ${psQuote(spec.profile)}`,
5439
- `$workDir = ${psQuote(dirname12(spec.commandPath))}`
5472
+ `$workDir = ${psQuote(dirname13(spec.commandPath))}`
5440
5473
  ].join("\n");
5441
5474
  const install = [
5442
5475
  `$inner = '"' + $command + '" ' + $arguments + ' >> "' + $log + '" 2>&1'`,
@@ -5655,7 +5688,7 @@ var ServiceManager = class {
5655
5688
  }
5656
5689
  async withLifecycleLock(operation) {
5657
5690
  const lockDir = this.paths.serviceLockDir(this.profile);
5658
- await mkdir14(dirname13(lockDir), { recursive: true });
5691
+ await mkdir14(dirname14(lockDir), { recursive: true });
5659
5692
  try {
5660
5693
  await mkdir14(lockDir);
5661
5694
  } catch (error) {
@@ -5695,11 +5728,39 @@ var ServiceManager = class {
5695
5728
  if (serviceStatus.state === "running") return;
5696
5729
  const process2 = await this.findProcess(this.profile);
5697
5730
  if (process2) {
5731
+ if (await this.isGuardianSpawned(process2)) {
5732
+ await this.stopGuardianSpawnedProcess(process2.pid);
5733
+ return;
5734
+ }
5698
5735
  throw new Error(
5699
5736
  `\u68C0\u6D4B\u5230\u672A\u53D7\u7BA1\u7684 dsh --profile ${this.profile}\uFF08pid ${process2.pid}\uFF09\u3002\u8BF7\u5148\u5728\u539F\u7EC8\u7AEF\u505C\u6B62\u5B83\uFF0C\u518D\u91CD\u8BD5\u3002`
5700
5737
  );
5701
5738
  }
5702
5739
  }
5740
+ /** True when the profile process was spawned by the resident guardian. */
5741
+ async isGuardianSpawned(process2) {
5742
+ const ppid = process2.ppid;
5743
+ if (!ppid) return false;
5744
+ const parent = (await listProcesses()).find((entry) => entry.pid === ppid);
5745
+ return parent !== void 0 && matchGuardianProcess(parent.cmdline);
5746
+ }
5747
+ /** Best-effort stop of a guardian-spawned profile process (grace, then kill). */
5748
+ async stopGuardianSpawnedProcess(pid) {
5749
+ try {
5750
+ process.kill(pid, "SIGTERM");
5751
+ } catch {
5752
+ return;
5753
+ }
5754
+ const deadline = Date.now() + 5e3;
5755
+ while (Date.now() < deadline && isPidAlive2(pid)) {
5756
+ await new Promise((resolve6) => setTimeout(resolve6, 100));
5757
+ }
5758
+ if (!isPidAlive2(pid)) return;
5759
+ try {
5760
+ process.kill(pid, "SIGKILL");
5761
+ } catch {
5762
+ }
5763
+ }
5703
5764
  async buildSpec(requireProfile = false) {
5704
5765
  const previous = await this.readMetadata();
5705
5766
  const dshBin = this.dshBinOverride ?? discoverDshBin(this.home, this.sourceEnv) ?? previous?.dshBin;
@@ -5796,8 +5857,8 @@ var ServiceManager = class {
5796
5857
  }
5797
5858
  }
5798
5859
  async prepareFiles(spec, platform) {
5799
- await mkdir14(dirname13(spec.logFile), { recursive: true });
5800
- await mkdir14(dirname13(spec.envFile), { recursive: true });
5860
+ await mkdir14(dirname14(spec.logFile), { recursive: true });
5861
+ await mkdir14(dirname14(spec.envFile), { recursive: true });
5801
5862
  await writeServiceEnv(spec.envFile, spec.env);
5802
5863
  if (platform === "win32-task") {
5803
5864
  try {
@@ -6850,7 +6911,7 @@ var PermissionPolicyStore = class {
6850
6911
 
6851
6912
  // src/bot/notification-preference-store.ts
6852
6913
  import { readFile as readFile19 } from "fs/promises";
6853
- var EVENTS = /* @__PURE__ */ new Set(["completed", "failed", "approval"]);
6914
+ var EVENTS = /* @__PURE__ */ new Set(["completed", "failed", "approval", "urgent"]);
6854
6915
  var NotificationPreferenceStore = class {
6855
6916
  constructor(path) {
6856
6917
  this.path = path;
@@ -6879,7 +6940,7 @@ var NotificationPreferenceStore = class {
6879
6940
  get(scope) {
6880
6941
  const value = this.data.scopes[scope];
6881
6942
  if (value === void 0 || value === false) return void 0;
6882
- return { ...value, events: [...value.events], mentionUserIds: [...value.mentionUserIds] };
6943
+ return { ...value, events: [...value.events], mentionUserIds: [...value.mentionUserIds], sinks: [...value.sinks] };
6883
6944
  }
6884
6945
  resolve(scope, fallback) {
6885
6946
  const value = this.data.scopes[scope];
@@ -6924,6 +6985,7 @@ function normalizePreference(value) {
6924
6985
  ...typeof input.target === "string" && input.target.trim() ? { target: input.target.trim() } : {},
6925
6986
  events,
6926
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()))],
6927
6989
  approvalReminderMs
6928
6990
  };
6929
6991
  }
@@ -7400,6 +7462,7 @@ function statusCardMarkdownFor(input, locale) {
7400
7462
  zh ? `\u{1F39A}\uFE0F **\u6267\u884C\u6A21\u5F0F**\uFF1A\`${input.executionMode ?? "balanced"}\`` : `\u{1F39A}\uFE0F **Execution mode**: \`${input.executionMode ?? "balanced"}\``,
7401
7463
  zh ? `\u{1F6E1}\uFE0F **\u5DE5\u5177\u6743\u9650**\uFF1A\`${input.permissionPolicy ?? "ask"}\`` : `\u{1F6E1}\uFE0F **Tool permission**: \`${input.permissionPolicy ?? "ask"}\``,
7402
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(",")}\``] : [],
7403
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`] : [],
7404
7467
  ...input.role ? [`\u{1F3AD} **role**\uFF1A${input.role}`] : [],
7405
7468
  `\u{1F516} **version**\uFF1A\`${input.version}\``,
@@ -7816,8 +7879,8 @@ function renderStandard(state, now, locale, maxTools) {
7816
7879
  } else if (state.terminal === "done" && !hasAnswer(state)) {
7817
7880
  elements.push(noteMd(zh ? "_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_" : "_(No content returned)_"));
7818
7881
  } else if (state.terminal === "done") {
7819
- const usage3 = usageLine(state);
7820
- if (usage3) elements.push(noteMd(usage3));
7882
+ const usage4 = usageLine(state);
7883
+ if (usage4) elements.push(noteMd(usage4));
7821
7884
  }
7822
7885
  if (state.finalDeliveryError) {
7823
7886
  elements.push(finalDeliveryFailureLine(locale));
@@ -7883,8 +7946,8 @@ function renderDetailed(state, now, locale, maxTools) {
7883
7946
  } else if (state.terminal === "error") {
7884
7947
  elements.push(runFailureLine(locale));
7885
7948
  } else if (state.terminal === "done") {
7886
- const usage3 = usageLine(state);
7887
- if (usage3) elements.push(noteMd(usage3));
7949
+ const usage4 = usageLine(state);
7950
+ if (usage4) elements.push(noteMd(usage4));
7888
7951
  if (!hasAnswer(state)) elements.push(noteMd(zh ? "_\uFF08\u672A\u8FD4\u56DE\u5185\u5BB9\uFF09_" : "_(No content returned)_"));
7889
7952
  }
7890
7953
  if (state.finalDeliveryError) {
@@ -10461,7 +10524,7 @@ async function handleConfigHubAction(action, ctx, value = {}) {
10461
10524
  }
10462
10525
 
10463
10526
  // src/commands/archive.ts
10464
- import { dirname as dirname14 } from "path";
10527
+ import { dirname as dirname15 } from "path";
10465
10528
 
10466
10529
  // src/media/outbound-files.ts
10467
10530
  import { constants } from "fs";
@@ -10657,7 +10720,7 @@ async function sendArchiveFiles(ctx, record, destination = {
10657
10720
  [record.jsonlPath, `${record.archiveId}.jsonl`]
10658
10721
  ]) {
10659
10722
  try {
10660
- const prepared = await prepareOutboundFile({ path, baseDir: dirname14(path), allowedRoots: [dirname14(path)], fileName });
10723
+ const prepared = await prepareOutboundFile({ path, baseDir: dirname15(path), allowedRoots: [dirname15(path)], fileName });
10661
10724
  await ctx.channel.sendFile(destination.chatId, prepared.fileName, prepared.content, {
10662
10725
  ...destination.messageId ? { replyTo: destination.messageId } : {},
10663
10726
  ...destination.threadId ? { threadId: destination.threadId } : {}
@@ -10907,7 +10970,7 @@ async function handleNotify(args, ctx) {
10907
10970
  }
10908
10971
 
10909
10972
  // src/commands/notifications.ts
10910
- var ALL_EVENTS = ["completed", "failed", "approval"];
10973
+ var ALL_EVENTS = ["completed", "failed", "approval", "urgent"];
10911
10974
  async function reply5(ctx, zh, en) {
10912
10975
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
10913
10976
  }
@@ -10945,6 +11008,7 @@ async function handleNotifications(args, ctx) {
10945
11008
  let events = [...ALL_EVENTS];
10946
11009
  let mentionUserIds = ctx.senderId ? [ctx.senderId] : [];
10947
11010
  let reminderMinutes = 10;
11011
+ let sinks = [];
10948
11012
  for (const token of tokens.slice(1)) {
10949
11013
  if (token.startsWith("events=")) {
10950
11014
  const requested = token.slice(7).split(",").filter(Boolean);
@@ -10956,6 +11020,8 @@ async function handleNotifications(args, ctx) {
10956
11020
  } else if (token.startsWith("remind=")) {
10957
11021
  reminderMinutes = Number(token.slice(7));
10958
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);
10959
11025
  } else if (!target) {
10960
11026
  target = token === "current" ? void 0 : token;
10961
11027
  } else return usage(ctx);
@@ -10974,50 +11040,297 @@ async function handleNotifications(args, ctx) {
10974
11040
  ...target ? { target } : {},
10975
11041
  events,
10976
11042
  mentionUserIds: [...new Set(mentionUserIds)],
10977
- approvalReminderMs: reminderMinutes * 6e4
11043
+ approvalReminderMs: reminderMinutes * 6e4,
11044
+ sinks: [...new Set(sinks)]
10978
11045
  };
10979
11046
  await store.set(ctx.scope, value);
10980
11047
  await reply5(ctx, `\u5DF2\u5F00\u542F\u3002${describe(value, true)}`, `Enabled. ${describe(value, false)}`);
10981
11048
  }
10982
11049
  function describe(value, zh) {
10983
11050
  const minutes = value.approvalReminderMs / 6e4;
10984
- 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.`;
10985
11053
  }
10986
11054
  async function usage(ctx) {
10987
11055
  await reply5(
10988
11056
  ctx,
10989
- "\u7528\u6CD5\uFF1A`/notifications [show|off|default|on [current|scope|chatId] [events=completed,failed,approval] [mentions=self,ou_x|none] [remind=10]]`",
10990
- "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]]`"
11059
+ );
11060
+ }
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>`"
10991
11286
  );
10992
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
+ }
10993
11306
 
10994
11307
  // src/commands/replies.ts
10995
11308
  async function handleReplies(args, ctx) {
10996
11309
  const store = ctx.replyPolicies;
10997
- 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.");
10998
11311
  const tokens = args.trim().split(/\s+/).filter(Boolean);
10999
- 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));
11000
- 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.");
11001
11314
  if (tokens[0] === "default") {
11002
11315
  await store.set(ctx.scope, void 0);
11003
- 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.");
11004
11317
  }
11005
- if (tokens[0] !== "set") return usage2(ctx);
11318
+ if (tokens[0] !== "set") return usage3(ctx);
11006
11319
  const policy = { ...store.get(ctx.scope) };
11007
11320
  for (const token of tokens.slice(1)) {
11008
11321
  const [key, raw, extra] = token.split("=");
11009
- if (!raw || extra !== void 0) return usage2(ctx);
11322
+ if (!raw || extra !== void 0) return usage3(ctx);
11010
11323
  const seconds = Number(raw);
11011
- if (!Number.isInteger(seconds)) return usage2(ctx);
11324
+ if (!Number.isInteger(seconds)) return usage3(ctx);
11012
11325
  if (key === "merge" && seconds >= 0 && seconds <= 300) policy.mergeWindowMs = seconds * 1e3;
11013
11326
  else if (key === "batch" && seconds >= 1 && seconds <= 20) policy.maxBatchSize = seconds;
11014
11327
  else if (key === "interval" && seconds >= 0 && seconds <= 3600) policy.minIntervalMs = seconds * 1e3;
11015
11328
  else if (key === "dedupe" && seconds >= 0 && seconds <= 3600) policy.dedupeWindowMs = seconds * 1e3;
11016
- else return usage2(ctx);
11329
+ else return usage3(ctx);
11017
11330
  }
11018
- if (tokens.length === 1) return usage2(ctx);
11331
+ if (tokens.length === 1) return usage3(ctx);
11019
11332
  await store.set(ctx.scope, policy);
11020
- 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)}`);
11021
11334
  }
11022
11335
  async function canManageReplies(ctx) {
11023
11336
  if (ctx.accessManager.isAdmin(ctx.senderId)) return true;
@@ -11031,10 +11344,10 @@ async function canManageReplies(ctx) {
11031
11344
  function describe2(policy, configured, zh) {
11032
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.`;
11033
11346
  }
11034
- async function usage2(ctx) {
11035
- 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]`");
11036
11349
  }
11037
- async function reply6(ctx, zh, en) {
11350
+ async function reply7(ctx, zh, en) {
11038
11351
  await ctx.channel.sendMarkdown(ctx.chatId, bilingualMarkdown(zh, en), { replyTo: ctx.messageId });
11039
11352
  }
11040
11353
 
@@ -11221,7 +11534,8 @@ var COMMAND_CATALOG = [
11221
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)" },
11222
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" },
11223
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)" },
11224
- { 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)" },
11225
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" },
11226
11540
  { names: ["/retention"], area: "session", zh: "`[N|default]` \u2014 \u4F1A\u8BDD\u4FDD\u7559\u6761\u6570", en: "`[N|default]` \u2014 retained live-message count" },
11227
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" },
@@ -11299,6 +11613,7 @@ var HELP = [
11299
11613
  "- `/notify <scope|chatId> <text>` \u2014 \u8DE8\u4F1A\u8BDD\u53D1\u9001\u901A\u77E5\uFF08\u7BA1\u7406\u5458\uFF09",
11300
11614
  "- `/notify list` \u2014 \u67E5\u770B\u5DF2\u6CE8\u518C scope",
11301
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",
11302
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",
11303
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",
11304
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",
@@ -11360,7 +11675,7 @@ var HELP_EN = [
11360
11675
  "- `/invite user|admin|group <id>` \u2014 manage access allowlists",
11361
11676
  "- `/help` \u2014 show this help"
11362
11677
  ].join("\n");
11363
- async function reply7(ctx, zhCn, enUs) {
11678
+ async function reply8(ctx, zhCn, enUs) {
11364
11679
  await ctx.channel.sendMarkdown(ctx.chatId, enUs === void 0 ? zhCn : bilingualMarkdown(zhCn, enUs, ctx.languagePolicies?.get().plain), {
11365
11680
  replyTo: ctx.messageId
11366
11681
  });
@@ -11369,7 +11684,7 @@ async function handleNew(_args, ctx) {
11369
11684
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11370
11685
  const interrupted = await ctx.activeRuns.interruptWorkspace(ctx.scope, cwd);
11371
11686
  ctx.sessions.clear(ctx.scope, cwd);
11372
- await reply7(
11687
+ await reply8(
11373
11688
  ctx,
11374
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",
11375
11690
  interrupted > 0 ? `Interrupted ${String(interrupted)} task(s) and started a new session.` : "Started a new session."
@@ -11398,11 +11713,11 @@ function groupAppLink(chatId) {
11398
11713
  async function handleNewGroup(args, ctx) {
11399
11714
  const name = args.trim();
11400
11715
  if (!name) {
11401
- 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");
11402
11717
  return;
11403
11718
  }
11404
11719
  if (name.length > MAX_GROUP_NAME_LENGTH) {
11405
- await reply7(
11720
+ await reply8(
11406
11721
  ctx,
11407
11722
  `\u7FA4\u540D\u8FC7\u957F\uFF08\u4E0A\u9650 ${String(MAX_GROUP_NAME_LENGTH)} \u5B57\u7B26\uFF0C\u5F53\u524D ${String(name.length)}\uFF09\u3002`,
11408
11723
  `The group name is too long (limit ${String(MAX_GROUP_NAME_LENGTH)} characters; received ${String(name.length)}).`
@@ -11410,11 +11725,11 @@ async function handleNewGroup(args, ctx) {
11410
11725
  return;
11411
11726
  }
11412
11727
  if (!ctx.channel.createChat) {
11413
- 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.");
11414
11729
  return;
11415
11730
  }
11416
11731
  if (!ctx.senderId) {
11417
- 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.");
11418
11733
  return;
11419
11734
  }
11420
11735
  try {
@@ -11425,7 +11740,7 @@ async function handleNewGroup(args, ctx) {
11425
11740
  inviteUserIds: [ctx.senderId],
11426
11741
  userIdType: "open_id"
11427
11742
  });
11428
- await reply7(
11743
+ await reply8(
11429
11744
  ctx,
11430
11745
  [
11431
11746
  `\u2705 \u5DF2\u521B\u5EFA\u7FA4\u804A\uFF1A**${name}**`,
@@ -11444,13 +11759,13 @@ async function handleNewGroup(args, ctx) {
11444
11759
  );
11445
11760
  } catch (error) {
11446
11761
  const message = error instanceof Error ? error.message : String(error);
11447
- 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}\``);
11448
11763
  }
11449
11764
  }
11450
11765
  async function handleCd(args, ctx) {
11451
11766
  const path = args.trim();
11452
11767
  if (!path) {
11453
- await reply7(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`", "Usage: `/cd <path>`");
11768
+ await reply8(ctx, "\u7528\u6CD5\uFF1A`/cd <path>`", "Usage: `/cd <path>`");
11454
11769
  return;
11455
11770
  }
11456
11771
  const normalized = path.replace(/^~/u, "~");
@@ -11458,13 +11773,13 @@ async function handleCd(args, ctx) {
11458
11773
  const cwd = resolve5(expanded);
11459
11774
  const target = await stat8(cwd).catch(() => void 0);
11460
11775
  if (!target?.isDirectory()) {
11461
- 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}\`.`);
11462
11777
  return;
11463
11778
  }
11464
11779
  const previous = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11465
11780
  const interrupted = cwd === previous ? 0 : await ctx.activeRuns.interruptWorkspace(ctx.scope, previous);
11466
11781
  ctx.workspaces.setCwd(ctx.scope, cwd);
11467
- await reply7(
11782
+ await reply8(
11468
11783
  ctx,
11469
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` : ""}`,
11470
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).` : ""}`
@@ -11484,7 +11799,7 @@ async function handleWs(args, ctx) {
11484
11799
  const lines = Object.entries(named).map(
11485
11800
  ([key, value]) => `- **${key}** \u2192 \`${value}\`${value === current ? " \u2190 \u5F53\u524D" : ""}`
11486
11801
  );
11487
- await reply7(
11802
+ await reply8(
11488
11803
  ctx,
11489
11804
  [
11490
11805
  `\u5F53\u524D cwd\uFF1A\`${current}\``,
@@ -11501,29 +11816,29 @@ async function handleWs(args, ctx) {
11501
11816
  }
11502
11817
  if (sub === "save") {
11503
11818
  if (!name) {
11504
- 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>`");
11505
11820
  return;
11506
11821
  }
11507
11822
  const current = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11508
11823
  ctx.workspaces.saveNamed(name, current);
11509
- 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}\``);
11510
11825
  return;
11511
11826
  }
11512
11827
  if (sub === "use") {
11513
11828
  if (!name) {
11514
- 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>`");
11515
11830
  return;
11516
11831
  }
11517
11832
  const cwd = ctx.workspaces.getNamed(name);
11518
11833
  if (!cwd) {
11519
- 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}**`);
11520
11835
  return;
11521
11836
  }
11522
11837
  const previous = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11523
11838
  const interrupted = cwd === previous ? 0 : await ctx.activeRuns.interruptWorkspace(ctx.scope, previous);
11524
11839
  ctx.workspaces.setCwd(ctx.scope, cwd);
11525
11840
  ctx.workspaces.touchNamed(name);
11526
- await reply7(
11841
+ await reply8(
11527
11842
  ctx,
11528
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` : ""}`,
11529
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).` : ""}`
@@ -11532,14 +11847,14 @@ async function handleWs(args, ctx) {
11532
11847
  }
11533
11848
  if (sub === "remove") {
11534
11849
  if (!name) {
11535
- 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>`");
11536
11851
  return;
11537
11852
  }
11538
11853
  const removed = ctx.workspaces.removeNamed(name);
11539
- 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}**`);
11540
11855
  return;
11541
11856
  }
11542
- 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.");
11543
11858
  }
11544
11859
  async function statusCardInputFor(ctx) {
11545
11860
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -11586,6 +11901,7 @@ async function statusCardInputFor(ctx) {
11586
11901
  permissionPolicy: ctx.permissionPolicies?.get(ctx.scope) ?? "ask",
11587
11902
  executionMode: ctx.executionModes?.get(ctx.scope) ?? "balanced",
11588
11903
  ...notificationPreference ? { notificationPreference } : {},
11904
+ ...ctx.notificationSinks ? { notificationChannels: ctx.notificationSinks.enabledChannels().map((channel) => channel.id) } : {},
11589
11905
  ...replyPolicy ? { replyPolicy, replyPolicyConfigured: ctx.replyPolicies?.isConfigured(ctx.scope) ?? false } : {},
11590
11906
  role: role ? `\`${role.id}\` (${role.name})` : void 0,
11591
11907
  metrics,
@@ -11628,16 +11944,16 @@ async function handleStatus(_args, ctx) {
11628
11944
  } catch {
11629
11945
  }
11630
11946
  }
11631
- await reply7(ctx, bilingualMarkdown(statusCardMarkdown(input), statusCardMarkdownEnglish(input)));
11947
+ await reply8(ctx, bilingualMarkdown(statusCardMarkdown(input), statusCardMarkdownEnglish(input)));
11632
11948
  }
11633
11949
  async function handleIsolation(args, ctx) {
11634
11950
  if (ctx.chatMode === "p2p") {
11635
- 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.");
11636
11952
  return;
11637
11953
  }
11638
11954
  const mode = args.trim().toLowerCase();
11639
11955
  if (!mode) {
11640
- await reply7(
11956
+ await reply8(
11641
11957
  ctx,
11642
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`,
11643
11959
  `Current group isolation mode: **${ctx.isolationStore?.get(ctx.chatId) ?? "topic"}**. Use \`/isolation group|topic|member\` to switch.`
@@ -11645,16 +11961,16 @@ async function handleIsolation(args, ctx) {
11645
11961
  return;
11646
11962
  }
11647
11963
  if (mode !== "group" && mode !== "topic" && mode !== "member") {
11648
- 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`");
11649
11965
  return;
11650
11966
  }
11651
- if (!requireAdmin3(ctx)) return;
11967
+ if (!requireAdmin4(ctx)) return;
11652
11968
  if (!ctx.isolationStore) {
11653
- 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.");
11654
11970
  return;
11655
11971
  }
11656
11972
  ctx.isolationStore.set(ctx.chatId, mode);
11657
- await reply7(
11973
+ await reply8(
11658
11974
  ctx,
11659
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`,
11660
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.`
@@ -11681,25 +11997,25 @@ async function handleVersion(_args, ctx) {
11681
11997
  lines.push("\u6700\u65B0\u7248\u672C\u67E5\u8BE2\u6682\u4E0D\u53EF\u7528\uFF08\u7F51\u7EDC / registry \u5F02\u5E38\uFF09\u3002");
11682
11998
  english.push("Latest-version lookup is unavailable (network or registry error).");
11683
11999
  }
11684
- await reply7(ctx, lines.join("\n"), english.join("\n"));
12000
+ await reply8(ctx, lines.join("\n"), english.join("\n"));
11685
12001
  }
11686
12002
  async function handleUpgrade(_args, ctx) {
11687
- if (!requireAdmin3(ctx)) return;
12003
+ if (!requireAdmin4(ctx)) return;
11688
12004
  if (!ctx.senderId || !ctx.channelUpdates) {
11689
- 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.");
11690
12006
  return;
11691
12007
  }
11692
12008
  const result = await ctx.channelUpdates.check({ scope: ctx.scope, actorId: ctx.senderId });
11693
12009
  if (result.kind === "unavailable") {
11694
- 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.");
11695
12011
  return;
11696
12012
  }
11697
12013
  if (result.kind === "current") {
11698
- 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}\`.`);
11699
12015
  return;
11700
12016
  }
11701
12017
  if (!ctx.channel.sendCard) {
11702
- 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.");
11703
12019
  return;
11704
12020
  }
11705
12021
  await ctx.channel.sendCard(ctx.chatId, renderUpdateCard({
@@ -11714,9 +12030,9 @@ async function handleUpgrade(_args, ctx) {
11714
12030
  });
11715
12031
  }
11716
12032
  async function handleDoctor(_args, ctx) {
11717
- if (!requireAdmin3(ctx)) return;
12033
+ if (!requireAdmin4(ctx)) return;
11718
12034
  if (!ctx.createDiagnosticBundle || !ctx.channel.sendFile) {
11719
- 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.");
11720
12036
  return;
11721
12037
  }
11722
12038
  let file;
@@ -11733,7 +12049,7 @@ async function handleDoctor(_args, ctx) {
11733
12049
  ...status.jobs ? { jobs: status.jobs } : {}
11734
12050
  }), ctx.diagnosticTimeoutMs?.generate ?? DOCTOR_GENERATE_TIMEOUT_MS);
11735
12051
  } catch {
11736
- await reply7(
12052
+ await reply8(
11737
12053
  ctx,
11738
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",
11739
12055
  "\u26A0\uFE0F Failed to generate the diagnostic bundle. Retry later; if it keeps failing, run `dsh-lark-bot doctor` locally."
@@ -11747,21 +12063,21 @@ async function handleDoctor(_args, ctx) {
11747
12063
  }), ctx.diagnosticTimeoutMs?.upload ?? DOCTOR_UPLOAD_TIMEOUT_MS);
11748
12064
  } catch (error) {
11749
12065
  if (error instanceof OperationTimeoutError) {
11750
- await reply7(
12066
+ await reply8(
11751
12067
  ctx,
11752
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",
11753
12069
  "\u26A0\uFE0F Diagnostic upload timed out with an unknown result: the file may still arrive later. Check this chat before retrying."
11754
12070
  );
11755
12071
  return;
11756
12072
  }
11757
- await reply7(
12073
+ await reply8(
11758
12074
  ctx,
11759
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",
11760
12076
  "\u26A0\uFE0F Failed to send the diagnostic bundle. Retry later; if it keeps failing, run `dsh-lark-bot doctor` locally."
11761
12077
  );
11762
12078
  return;
11763
12079
  }
11764
- await reply7(
12080
+ await reply8(
11765
12081
  ctx,
11766
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",
11767
12083
  "\u2705 The redacted diagnostic bundle was generated and sent. You can download and forward it to a maintainer."
@@ -11771,18 +12087,18 @@ async function handleResume(_args, ctx) {
11771
12087
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
11772
12088
  const history = ctx.sessions.historyFor(ctx.scope, cwd);
11773
12089
  if (history.length === 0) {
11774
- 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.");
11775
12091
  return;
11776
12092
  }
11777
12093
  const recent = history.slice(-6).map((message) => {
11778
12094
  const speaker = message.role === "user" ? "\u{1F464}" : "\u{1F916}";
11779
12095
  return `${speaker} ${message.content.slice(0, 300)}`;
11780
12096
  });
11781
- 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"));
11782
12098
  }
11783
12099
  async function handleJobs(args, ctx) {
11784
12100
  if (!ctx.jobs) {
11785
- 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.");
11786
12102
  return;
11787
12103
  }
11788
12104
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -11790,11 +12106,11 @@ async function handleJobs(args, ctx) {
11790
12106
  const records = ctx.jobs.list(ctx.scope, cwd, 20);
11791
12107
  if (sub === "retry") {
11792
12108
  if (!id) {
11793
- 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>`");
11794
12110
  return;
11795
12111
  }
11796
12112
  const queued = await ctx.requeueJob?.(id, ctx.scope, cwd);
11797
- await reply7(
12113
+ await reply8(
11798
12114
  ctx,
11799
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`,
11800
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.`
@@ -11804,19 +12120,19 @@ async function handleJobs(args, ctx) {
11804
12120
  if (sub === "show") {
11805
12121
  const record = id ? ctx.jobs.get(id, ctx.scope, cwd) : void 0;
11806
12122
  if (!record) {
11807
- 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 ?? ""}**`);
11808
12124
  return;
11809
12125
  }
11810
- await reply7(ctx, jobDetail(record, "zh"), jobDetail(record, "en"));
12126
+ await reply8(ctx, jobDetail(record, "zh"), jobDetail(record, "en"));
11811
12127
  return;
11812
12128
  }
11813
12129
  if (sub && sub !== "list") {
11814
- 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>]`");
11815
12131
  return;
11816
12132
  }
11817
12133
  const counts = ctx.jobs.counts(ctx.scope, cwd);
11818
12134
  const rows = records.slice(0, 10);
11819
- await reply7(
12135
+ await reply8(
11820
12136
  ctx,
11821
12137
  [
11822
12138
  "**\u4EFB\u52A1\u5BF9\u8D26**",
@@ -11863,7 +12179,7 @@ async function handleStop(_args, ctx) {
11863
12179
  });
11864
12180
  let stopped = 0;
11865
12181
  for (const scope of scopes) stopped += await ctx.activeRuns.interrupt(scope);
11866
- await reply7(
12182
+ await reply8(
11867
12183
  ctx,
11868
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",
11869
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."
@@ -11874,7 +12190,7 @@ async function handleTimeout(args, ctx) {
11874
12190
  const effectiveMs = ctx.runPolicies.get(ctx.scope) ?? ctx.defaultRunTimeoutMs;
11875
12191
  if (!input) {
11876
12192
  const minutes2 = effectiveMs > 0 ? Math.round(effectiveMs / 6e4) : 0;
11877
- await reply7(
12193
+ await reply8(
11878
12194
  ctx,
11879
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",
11880
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."
@@ -11883,27 +12199,27 @@ async function handleTimeout(args, ctx) {
11883
12199
  }
11884
12200
  if (input === "off") {
11885
12201
  ctx.runPolicies.set(ctx.scope, 0);
11886
- 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.");
11887
12203
  return;
11888
12204
  }
11889
12205
  if (input === "default") {
11890
12206
  ctx.runPolicies.clear(ctx.scope);
11891
- 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.");
11892
12208
  return;
11893
12209
  }
11894
12210
  const minutes = Number(input);
11895
12211
  if (!Number.isInteger(minutes) || minutes <= 0) {
11896
- 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.");
11897
12213
  return;
11898
12214
  }
11899
12215
  ctx.runPolicies.set(ctx.scope, minutes * 6e4);
11900
- 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.`);
11901
12217
  }
11902
12218
  async function handleConcurrency(args, ctx) {
11903
12219
  const input = args.trim();
11904
12220
  const effective = ctx.concurrencyStore.get(ctx.scope) ?? ctx.defaultScopeConcurrency;
11905
12221
  if (!input) {
11906
- await reply7(
12222
+ await reply8(
11907
12223
  ctx,
11908
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`,
11909
12225
  `Parallel tasks for this scope: **${String(effective)}**. Use \`/concurrency <N|default>\` to change it (N \u2265 1).`
@@ -11912,16 +12228,16 @@ async function handleConcurrency(args, ctx) {
11912
12228
  }
11913
12229
  if (input === "default") {
11914
12230
  ctx.concurrencyStore.clear(ctx.scope);
11915
- 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)}).`);
11916
12232
  return;
11917
12233
  }
11918
12234
  const n = Number(input);
11919
12235
  if (!Number.isInteger(n) || n < 1) {
11920
- 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.");
11921
12237
  return;
11922
12238
  }
11923
12239
  ctx.concurrencyStore.set(ctx.scope, n);
11924
- 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)}**.`);
11925
12241
  }
11926
12242
  async function handlePermission(args, ctx) {
11927
12243
  const [rawPolicy = "", rawTarget = "", ...extra] = args.trim().split(/\s+/);
@@ -11933,7 +12249,7 @@ async function handlePermission(args, ctx) {
11933
12249
  };
11934
12250
  if (!input) {
11935
12251
  const current = ctx.permissionPolicies?.get(ctx.scope) ?? "ask";
11936
- await reply7(
12252
+ await reply8(
11937
12253
  ctx,
11938
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`,
11939
12255
  `Tool permission policy for this scope: **${labels[current].en}** (\`${current}\`). Use \`/permission ask|allow|deny\` to change it (admin only).`
@@ -11941,21 +12257,21 @@ async function handlePermission(args, ctx) {
11941
12257
  return;
11942
12258
  }
11943
12259
  if (input !== "ask" && input !== "allow" && input !== "deny" || extra.length > 0) {
11944
- 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]`");
11945
12261
  return;
11946
12262
  }
11947
- if (!requireAdmin3(ctx)) return;
12263
+ if (!requireAdmin4(ctx)) return;
11948
12264
  if (!ctx.permissionPolicies) {
11949
- 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.");
11950
12266
  return;
11951
12267
  }
11952
12268
  const targetScope = rawTarget || ctx.scope;
11953
12269
  if (targetScope !== ctx.chatId && !targetScope.startsWith(`${ctx.chatId}:`)) {
11954
- 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`.");
11955
12271
  return;
11956
12272
  }
11957
12273
  await ctx.permissionPolicies.set(targetScope, input);
11958
- await reply7(
12274
+ await reply8(
11959
12275
  ctx,
11960
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`,
11961
12277
  `Set scope \`${targetScope}\`'s tool permission policy to **${labels[input].en}** (\`${input}\`). It applies to the next tool approval; plan approval remains enforced.`
@@ -11965,7 +12281,7 @@ async function handleDensity(args, ctx) {
11965
12281
  const input = args.trim().toLowerCase();
11966
12282
  if (!input) {
11967
12283
  const current = ctx.densityStore?.get(ctx.scope) ?? "standard";
11968
- await reply7(
12284
+ await reply8(
11969
12285
  ctx,
11970
12286
  `\u5F53\u524D\u5361\u7247\u5BC6\u5EA6\uFF1A**${current}**\u3002\u53EF\u7528 \`/density compact|standard|detailed\` \u8C03\u6574\u3002`,
11971
12287
  `Current card density: **${current}**. Use \`/density compact|standard|detailed\` to change it.`
@@ -11974,21 +12290,21 @@ async function handleDensity(args, ctx) {
11974
12290
  }
11975
12291
  if (input === "default") {
11976
12292
  ctx.densityStore?.clear(ctx.scope);
11977
- 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.");
11978
12294
  return;
11979
12295
  }
11980
12296
  const density = parseCardDensity(input);
11981
12297
  if (!density) {
11982
- 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]`");
11983
12299
  return;
11984
12300
  }
11985
12301
  ctx.densityStore?.set(ctx.scope, density);
11986
- 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}**.`);
11987
12303
  }
11988
12304
  async function handleExecutionMode(args, ctx) {
11989
12305
  const input = args.trim().toLowerCase();
11990
12306
  if (!ctx.executionModes) {
11991
- 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.");
11992
12308
  return;
11993
12309
  }
11994
12310
  if (!input) {
@@ -12005,7 +12321,7 @@ async function handleExecutionMode(args, ctx) {
12005
12321
  } catch {
12006
12322
  }
12007
12323
  }
12008
- await reply7(
12324
+ await reply8(
12009
12325
  ctx,
12010
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`,
12011
12327
  `Current execution mode: **${current}**. Use \`/mode quick|balanced|deep\`; changes apply next turn.`
@@ -12013,18 +12329,18 @@ async function handleExecutionMode(args, ctx) {
12013
12329
  return;
12014
12330
  }
12015
12331
  if (input !== "quick" && input !== "balanced" && input !== "deep") {
12016
- 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]`");
12017
12333
  return;
12018
12334
  }
12019
12335
  await ctx.executionModes.set(ctx.scope, input);
12020
- await reply7(
12336
+ await reply8(
12021
12337
  ctx,
12022
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`,
12023
12339
  `Set this session\u2019s execution mode to **${input}** for the next turn. Active work and existing context are unchanged.`
12024
12340
  );
12025
12341
  }
12026
12342
  async function handleEffortAlias(args, ctx) {
12027
- await reply7(
12343
+ await reply8(
12028
12344
  ctx,
12029
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",
12030
12346
  "`/effort` is a compatibility alias for `/mode`; it controls task execution intensity, not the model reasoning-effort setting."
@@ -12034,11 +12350,11 @@ async function handleEffortAlias(args, ctx) {
12034
12350
  async function handleAsk(args, ctx) {
12035
12351
  const question = args.trim();
12036
12352
  if (!question) {
12037
- 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>`");
12038
12354
  return;
12039
12355
  }
12040
12356
  if (!ctx.questions) {
12041
- 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).");
12042
12358
  return;
12043
12359
  }
12044
12360
  const cwd = ctx.workspaces.cwdFor(ctx.scope) ?? ctx.defaultWorkspace;
@@ -12060,18 +12376,18 @@ async function handleAsk(args, ctx) {
12060
12376
  if (answer !== void 0) {
12061
12377
  const text = Array.isArray(answer) ? answer.join("\u3001") : answer;
12062
12378
  ctx.sessions.recordExchange(ctx.scope, cwd, [text], void 0);
12063
- 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.");
12064
12380
  } else {
12065
- 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).");
12066
12382
  }
12067
12383
  }
12068
12384
  async function handleInvite(args, ctx) {
12069
12385
  const [kind, ...rest] = args.trim().split(/\s+/);
12070
12386
  const id = rest.join(" ").trim();
12071
- if (kind !== "list" && !requireAdmin3(ctx)) return;
12387
+ if (kind !== "list" && !requireAdmin4(ctx)) return;
12072
12388
  if (kind === "list") {
12073
12389
  const snapshot = ctx.accessManager.snapshot();
12074
- await reply7(
12390
+ await reply8(
12075
12391
  ctx,
12076
12392
  [
12077
12393
  "**\u8BBF\u95EE\u767D\u540D\u5355**",
@@ -12089,7 +12405,7 @@ async function handleInvite(args, ctx) {
12089
12405
  return;
12090
12406
  }
12091
12407
  if (!kind || !id) {
12092
- await reply7(
12408
+ await reply8(
12093
12409
  ctx,
12094
12410
  "\u7528\u6CD5\uFF1A`/invite user|admin|group <id>`\u3001`/invite list`\u3001`/invite remove user|group <id>`",
12095
12411
  "Usage: `/invite user|admin|group <id>`, `/invite list`, or `/invite remove user|group <id>`"
@@ -12098,39 +12414,39 @@ async function handleInvite(args, ctx) {
12098
12414
  }
12099
12415
  if (kind === "user") {
12100
12416
  await ctx.accessManager.addUser(id);
12101
- 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}\``);
12102
12418
  return;
12103
12419
  }
12104
12420
  if (kind === "admin") {
12105
12421
  await ctx.accessManager.addAdmin(id);
12106
- 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}\``);
12107
12423
  return;
12108
12424
  }
12109
12425
  if (kind === "group") {
12110
12426
  await ctx.accessManager.addChat(id);
12111
- 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}\``);
12112
12428
  return;
12113
12429
  }
12114
12430
  if (kind === "remove") {
12115
12431
  const [sub, target] = rest;
12116
12432
  if (sub === "user" && target) {
12117
12433
  await ctx.accessManager.removeUser(target);
12118
- 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}\``);
12119
12435
  return;
12120
12436
  }
12121
12437
  if (sub === "group" && target) {
12122
12438
  await ctx.accessManager.removeChat(target);
12123
- 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}\``);
12124
12440
  return;
12125
12441
  }
12126
- 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>`");
12127
12443
  return;
12128
12444
  }
12129
- 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.");
12130
12446
  }
12131
- function requireAdmin3(ctx) {
12447
+ function requireAdmin4(ctx) {
12132
12448
  if (!ctx.accessManager.isAdmin(ctx.senderId)) {
12133
- 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.");
12134
12450
  return false;
12135
12451
  }
12136
12452
  return true;
@@ -12183,20 +12499,20 @@ async function handleKeyDispatch(args, ctx) {
12183
12499
  await handleKey(args, ctx);
12184
12500
  }
12185
12501
  async function handleHelp(_args, ctx) {
12186
- await reply7(ctx, renderCommandHelp("zh"), renderCommandHelp("en"));
12502
+ await reply8(ctx, renderCommandHelp("zh"), renderCommandHelp("en"));
12187
12503
  }
12188
12504
  async function handleLanguage(args, ctx) {
12189
12505
  if (!ctx.languagePolicies) {
12190
- 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.");
12191
12507
  return;
12192
12508
  }
12193
12509
  const [action = "show", field, value] = args.trim().split(/\s+/);
12194
12510
  if (action === "show") {
12195
12511
  const policy = ctx.languagePolicies.get();
12196
- 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}`);
12197
12513
  return;
12198
12514
  }
12199
- if (!requireAdmin3(ctx)) return;
12515
+ if (!requireAdmin4(ctx)) return;
12200
12516
  if (action === "set" && field === "plain" && (value === "bilingual" || value === "zh" || value === "en")) {
12201
12517
  await ctx.languagePolicies.set({ plain: value });
12202
12518
  } else if (action === "set" && field === "agent" && (value === "auto" || value === "zh" || value === "en")) {
@@ -12204,7 +12520,7 @@ async function handleLanguage(args, ctx) {
12204
12520
  } else if (action === "reset" && (field === void 0 || field === "all" || field === "plain" || field === "agent")) {
12205
12521
  await ctx.languagePolicies.reset(field ?? "all");
12206
12522
  } else {
12207
- 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]`");
12208
12524
  return;
12209
12525
  }
12210
12526
  process.env.DSH_LARK_REPLY_LANG = ctx.languagePolicies.get().plain;
@@ -12215,37 +12531,37 @@ function parseSecretTarget(value) {
12215
12531
  }
12216
12532
  async function handleSecret(args, ctx) {
12217
12533
  if (!ctx.secretService) {
12218
- 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.");
12219
12535
  return;
12220
12536
  }
12221
12537
  const [action, targetRaw, referenceRaw, ...purposeParts] = args.trim().split(/\s+/);
12222
12538
  const target = parseSecretTarget(targetRaw);
12223
12539
  const reference = referenceRaw?.trim();
12224
12540
  if (!target || !reference || !["status", "set", "remove"].includes(action ?? "")) {
12225
- 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>`");
12226
12542
  return;
12227
12543
  }
12228
- if (action !== "status" && !requireAdmin3(ctx)) return;
12544
+ if (action !== "status" && !requireAdmin4(ctx)) return;
12229
12545
  if (action === "status") {
12230
12546
  const configured = await ctx.secretService.configured(target, reference);
12231
- 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"}`);
12232
12548
  return;
12233
12549
  }
12234
12550
  if (action === "remove") {
12235
12551
  const removed = await ctx.secretService.remove(target, reference);
12236
- 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.");
12237
12553
  return;
12238
12554
  }
12239
12555
  if (!ctx.senderId) {
12240
- 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.");
12241
12557
  return;
12242
12558
  }
12243
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" });
12244
- 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.");
12245
12561
  }
12246
12562
  async function handleSessionProjection(args, ctx) {
12247
12563
  if (!ctx.sessionProjection) {
12248
- await reply7(
12564
+ await reply8(
12249
12565
  ctx,
12250
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",
12251
12567
  "The current adapter does not provide DSH session projection; use the `web` adapter and ensure the Web host is reachable."
@@ -12275,6 +12591,7 @@ var handlers = {
12275
12591
  "/role": handleRole,
12276
12592
  "/notify": handleNotify,
12277
12593
  "/notifications": handleNotifications,
12594
+ "/channels": handleChannels,
12278
12595
  "/replies": handleReplies,
12279
12596
  "/retention": handleRetention,
12280
12597
  "/archive": handleArchive,
@@ -12754,24 +13071,26 @@ var GroupMessagePoller = class {
12754
13071
 
12755
13072
  // src/bridge/reconnect-notifier.ts
12756
13073
  var ReconnectNotifier = class {
12757
- constructor(channel, directory, now = Date.now, reconciliation) {
13074
+ constructor(channel, directory, now = Date.now, reconciliation, onFault) {
12758
13075
  this.channel = channel;
12759
13076
  this.directory = directory;
12760
13077
  this.now = now;
12761
13078
  this.reconciliation = reconciliation;
13079
+ this.onFault = onFault;
12762
13080
  }
12763
13081
  channel;
12764
13082
  directory;
12765
13083
  now;
12766
13084
  reconciliation;
13085
+ onFault;
12767
13086
  startedAt;
12768
13087
  async reconnecting() {
12769
13088
  if (this.startedAt !== void 0) return;
12770
13089
  this.startedAt = this.now();
12771
- await this.send(
12772
- "\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",
12773
- "\u26A0\uFE0F The bot connection is unstable and reconnecting automatically. New messages may be delayed."
12774
- );
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 });
12775
13094
  }
12776
13095
  async reconnected() {
12777
13096
  if (this.startedAt === void 0) return;
@@ -12779,12 +13098,12 @@ var ReconnectNotifier = class {
12779
13098
  this.startedAt = void 0;
12780
13099
  const target = this.directory?.recentDestination();
12781
13100
  const summary = target ? this.reconciliation?.(target.scope) : void 0;
12782
- await this.send(
12783
- `\u2705 \u673A\u5668\u4EBA\u8FDE\u63A5\u5DF2\u6062\u590D\uFF08\u4E2D\u65AD\u7EA6 ${formatDuration(elapsed)}\uFF09\u3002${summary ? `
12784
- ${summary.zhCn}` : ""}`,
12785
- `\u2705 The bot connection recovered after about ${formatDurationEnglish(elapsed)}.${summary ? `
12786
- ${summary.enUs}` : ""}`
12787
- );
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 });
12788
13107
  }
12789
13108
  async send(zhCn, enUs) {
12790
13109
  const target = this.directory?.recentDestination();
@@ -12794,6 +13113,13 @@ ${summary.enUs}` : ""}`
12794
13113
  ...target.threadId ? { threadId: target.threadId } : {}
12795
13114
  });
12796
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
+ }
12797
13123
  };
12798
13124
  function formatDurationEnglish(ms) {
12799
13125
  if (ms < 1e3) return "less than 1 second";
@@ -13996,7 +14322,8 @@ async function startChannel(deps) {
13996
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`,
13997
14323
  enUs: `Ledger reconciliation: queued ${counts.queued} \xB7 running ${counts.running} \xB7 interrupted ${counts.interrupted} \xB7 failed ${counts.failed}; use \`/jobs\` for details.`
13998
14324
  };
13999
- } : void 0
14325
+ } : void 0,
14326
+ deps.faultNotifier
14000
14327
  );
14001
14328
  const isolationStore = deps.isolationStore ?? EMPTY_ISOLATION_STORE;
14002
14329
  let groupPoller;
@@ -14131,6 +14458,8 @@ async function startChannel(deps) {
14131
14458
  ...deps.permissionPolicies ? { permissionPolicies: deps.permissionPolicies } : {},
14132
14459
  ...deps.notificationPreferences ? { notificationPreferences: deps.notificationPreferences } : {},
14133
14460
  ...deps.defaultNotificationPreference ? { defaultNotificationPreference: deps.defaultNotificationPreference } : {},
14461
+ ...deps.notificationChannels ? { notificationChannels: deps.notificationChannels } : {},
14462
+ ...deps.notificationSinks ? { notificationSinks: deps.notificationSinks } : {},
14134
14463
  ...deps.replyPolicies ? { replyPolicies: deps.replyPolicies } : {},
14135
14464
  ...deps.executionModes ? { executionModes: deps.executionModes } : {},
14136
14465
  ...deps.languagePolicies ? { languagePolicies: deps.languagePolicies } : {},
@@ -15495,6 +15824,11 @@ var NotificationDispatcher = class {
15495
15824
  setDefaultPreference(preference) {
15496
15825
  this.defaultPreference = clonePreference(preference);
15497
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
+ */
15498
15832
  async notify(scope, event, detail) {
15499
15833
  const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
15500
15834
  if (!preference?.events.includes(event)) return false;
@@ -15503,21 +15837,52 @@ var NotificationDispatcher = class {
15503
15837
  log.warn("notification", "target-unavailable", { scope, target: preference.target });
15504
15838
  return false;
15505
15839
  }
15506
- 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);
15507
15841
  try {
15508
- await this.deps.send(destination.chatId, bilingualMarkdown(
15509
- `${title[0]}\uFF08scope \`${scope}\`\uFF09${detail ? `\uFF1A${detail}` : ""}`,
15510
- `${title[1]} (scope \`${scope}\`)${detail ? `: ${detail}` : ""}`
15511
- ), {
15842
+ await this.deps.send(destination.chatId, renderFeishuBody(title, scope, detail), {
15512
15843
  ...destination.threadId ? { threadId: destination.threadId } : {},
15513
15844
  ...destination.messageId ? { replyTo: destination.messageId } : {},
15514
15845
  ...preference.mentionUserIds.length > 0 ? { mentions: preference.mentionUserIds.map((userId) => ({ userId })) } : {}
15515
15846
  });
15516
- return true;
15517
15847
  } catch (error) {
15518
15848
  log.warn("notification", "send-failed", { scope, event, error });
15519
15849
  return false;
15520
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
+ }
15521
15886
  }
15522
15887
  scheduleApprovalReminder(scope, toolName) {
15523
15888
  const preference = this.deps.preferences.resolve(scope, this.defaultPreference);
@@ -15528,11 +15893,197 @@ var NotificationDispatcher = class {
15528
15893
  timer.unref?.();
15529
15894
  return () => clearTimeout(timer);
15530
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
+ }
15531
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
+ }
15532
15931
  function clonePreference(preference) {
15533
- 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;
15534
15933
  }
15535
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;");
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
+ };
16086
+
15536
16087
  // src/bridge/reply-dispatcher.ts
15537
16088
  var ReplyDispatcher = class {
15538
16089
  constructor(deps) {
@@ -15591,8 +16142,8 @@ var ReplyDispatcher = class {
15591
16142
  }
15592
16143
  }
15593
16144
  };
15594
- function replyRoute(reply8) {
15595
- return `${reply8.chatId}\0${reply8.options?.threadId ?? ""}`;
16145
+ function replyRoute(reply9) {
16146
+ return `${reply9.chatId}\0${reply9.options?.threadId ?? ""}`;
15596
16147
  }
15597
16148
  function mergedMarkdown(batch) {
15598
16149
  if (batch.length === 1) return batch[0].markdown;
@@ -15727,7 +16278,7 @@ async function onboardPersonalAgent(deps = {}) {
15727
16278
  }
15728
16279
 
15729
16280
  // src/media/attachments.ts
15730
- 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";
15731
16282
  import { join as join23 } from "path";
15732
16283
  var MAX_TEXT_FILE_BYTES = 256e3;
15733
16284
  function assertSafeMediaName(mediaDir, destination) {
@@ -15768,7 +16319,7 @@ async function prepareAttachments(channel, message, mediaDir) {
15768
16319
  result.textFileNotes.push(`[attachment: ${resource.fileName ?? resource.fileKey}] ${destination}`);
15769
16320
  continue;
15770
16321
  }
15771
- const content = await readFile23(destination, "utf8");
16322
+ const content = await readFile24(destination, "utf8");
15772
16323
  const safeContent = truncateUtf8Safe(content, MAX_TEXT_FILE_BYTES);
15773
16324
  result.textFileNotes.push(
15774
16325
  `[attachment: ${resource.fileName ?? resource.fileKey}]
@@ -15779,7 +16330,7 @@ ${safeContent}`
15779
16330
  }
15780
16331
 
15781
16332
  // src/session/store.ts
15782
- import { readFile as readFile24 } from "fs/promises";
16333
+ import { readFile as readFile25 } from "fs/promises";
15783
16334
  var MAX_CONTEXT_SNAPSHOTS_PER_WORKSPACE = 32;
15784
16335
  var SessionStore = class {
15785
16336
  constructor(path) {
@@ -15794,7 +16345,7 @@ var SessionStore = class {
15794
16345
  pendingArchive = Promise.resolve();
15795
16346
  async load() {
15796
16347
  try {
15797
- const raw = JSON.parse(await readFile24(this.path, "utf8"));
16348
+ const raw = JSON.parse(await readFile25(this.path, "utf8"));
15798
16349
  const legacy = raw.schema !== 2 || !isObject(raw.workspaces);
15799
16350
  this.data = legacy ? migrateLegacy(raw) : normalizeV2(raw.workspaces);
15800
16351
  this.legacyScopes = legacy ? new Set(Object.keys(this.data.workspaces)) : /* @__PURE__ */ new Set();
@@ -15891,13 +16442,13 @@ var SessionStore = class {
15891
16442
  fullHistoryFor(scopeId, cwd) {
15892
16443
  return this.historyFor(scopeId, cwd);
15893
16444
  }
15894
- recordUsage(scopeId, cwd, usage3) {
16445
+ recordUsage(scopeId, cwd, usage4) {
15895
16446
  const state = this.ensureState(scopeId, cwd);
15896
16447
  const next = { ...state.metrics };
15897
- addMetric(next, "inputTokens", usage3.inputTokens);
15898
- addMetric(next, "outputTokens", usage3.outputTokens);
15899
- addMetric(next, "cacheReadTokens", usage3.cacheReadTokens);
15900
- 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);
15901
16452
  state.metrics = next;
15902
16453
  this.schedulePersist();
15903
16454
  }
@@ -16113,7 +16664,7 @@ function normalizeContextSnapshot(value) {
16113
16664
  }
16114
16665
 
16115
16666
  // src/session/projection-store.ts
16116
- import { readFile as readFile25 } from "fs/promises";
16667
+ import { readFile as readFile26 } from "fs/promises";
16117
16668
  import { randomUUID as randomUUID11 } from "crypto";
16118
16669
  var MAX_RECENT_MESSAGES = 64;
16119
16670
  var MAX_PROMPT_CORRELATIONS = 64;
@@ -16127,7 +16678,7 @@ var SessionProjectionStore = class {
16127
16678
  saving = Promise.resolve();
16128
16679
  async load() {
16129
16680
  try {
16130
- const parsed = JSON.parse(await readFile25(this.path, "utf8"));
16681
+ const parsed = JSON.parse(await readFile26(this.path, "utf8"));
16131
16682
  const bindings = Object.fromEntries(
16132
16683
  Object.values(parsed.bindings ?? {}).flatMap((value) => {
16133
16684
  const normalized = normalizeBinding(value);
@@ -16339,7 +16890,7 @@ function normalizeCorrelation(value) {
16339
16890
  // src/session/archive.ts
16340
16891
  import { execFile as execFile2 } from "child_process";
16341
16892
  import { randomBytes as randomBytes3 } from "crypto";
16342
- 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";
16343
16894
  import { join as join24 } from "path";
16344
16895
  import { promisify } from "util";
16345
16896
  var execFileAsync = promisify(execFile2);
@@ -16521,8 +17072,8 @@ var SessionArchive = class {
16521
17072
  const jsonlPath = join24(scopeDir, `${id}.jsonl`);
16522
17073
  const markdownPath = join24(scopeDir, `${id}.md`);
16523
17074
  const [jsonl, markdown2] = await Promise.all([
16524
- readFile26(jsonlPath, "utf8"),
16525
- readFile26(markdownPath, "utf8")
17075
+ readFile27(jsonlPath, "utf8"),
17076
+ readFile27(markdownPath, "utf8")
16526
17077
  ]);
16527
17078
  const [first = "", ...rest] = jsonl.split("\n");
16528
17079
  const header = JSON.parse(first);
@@ -16637,7 +17188,7 @@ async function defaultRunGit(args, cwd) {
16637
17188
  }
16638
17189
  async function readArchiveHeader(jsonlPath) {
16639
17190
  try {
16640
- const raw = await readFile26(jsonlPath, "utf8");
17191
+ const raw = await readFile27(jsonlPath, "utf8");
16641
17192
  const firstLine = raw.split("\n", 1)[0];
16642
17193
  if (!firstLine) return void 0;
16643
17194
  const header = JSON.parse(firstLine);
@@ -16665,7 +17216,7 @@ async function safeRemove(path) {
16665
17216
  import { execFile as execFile3 } from "child_process";
16666
17217
  import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
16667
17218
  import { access as access2, copyFile, mkdir as mkdir18, realpath as realpath2 } from "fs/promises";
16668
- import { dirname as dirname15, join as join25 } from "path";
17219
+ import { dirname as dirname16, join as join25 } from "path";
16669
17220
  import { promisify as promisify2 } from "util";
16670
17221
  var execFileAsync2 = promisify2(execFile3);
16671
17222
  async function defaultRunGit2(args, cwd) {
@@ -16684,10 +17235,10 @@ async function exists2(path) {
16684
17235
  }
16685
17236
  }
16686
17237
  async function defaultCopyRulesFile(source, target) {
16687
- await mkdir18(dirname15(target), { recursive: true });
17238
+ await mkdir18(dirname16(target), { recursive: true });
16688
17239
  await copyFile(source, target);
16689
17240
  }
16690
- function slugify2(scope) {
17241
+ function slugify3(scope) {
16691
17242
  const slug = scope.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[.]+/, "").replace(/^-+|-+$/g, "").slice(0, 64);
16692
17243
  return slug || "session";
16693
17244
  }
@@ -16703,7 +17254,7 @@ var GitWorktreeManager = class {
16703
17254
  async ensure(scope, base) {
16704
17255
  const isGit = await this.isGitRepository(base);
16705
17256
  if (!isGit) return { cwd: base, created: false };
16706
- const scopeSlug = slugify2(scope);
17257
+ const scopeSlug = slugify3(scope);
16707
17258
  const project = createHash2("sha256").update(base).digest("hex").slice(0, 10);
16708
17259
  const slug = `${scopeSlug}-${project}`;
16709
17260
  const target = join25(this.options.worktreesRoot, slug);
@@ -16740,7 +17291,7 @@ var GitWorktreeManager = class {
16740
17291
  }
16741
17292
  /** Owning main worktree for the legacy scope-only linked worktree. */
16742
17293
  async legacyWorkspaceBase(scope) {
16743
- const legacyTarget = join25(this.options.worktreesRoot, slugify2(scope));
17294
+ const legacyTarget = join25(this.options.worktreesRoot, slugify3(scope));
16744
17295
  if (!await exists2(legacyTarget)) return void 0;
16745
17296
  try {
16746
17297
  const output = await this.runGit(["worktree", "list", "--porcelain"], legacyTarget);
@@ -16772,7 +17323,7 @@ async function samePath(left, right) {
16772
17323
  }
16773
17324
 
16774
17325
  // src/workspace/store.ts
16775
- import { readFile as readFile27 } from "fs/promises";
17326
+ import { readFile as readFile28 } from "fs/promises";
16776
17327
  var WorkspaceStore = class {
16777
17328
  data = { chats: {}, named: {}, lastUsed: {} };
16778
17329
  saving = Promise.resolve();
@@ -16782,7 +17333,7 @@ var WorkspaceStore = class {
16782
17333
  }
16783
17334
  async load() {
16784
17335
  try {
16785
- const raw = await readFile27(this.path, "utf8");
17336
+ const raw = await readFile28(this.path, "utf8");
16786
17337
  const parsed = JSON.parse(raw);
16787
17338
  this.data = {
16788
17339
  chats: parsed.chats ?? {},
@@ -16901,8 +17452,8 @@ var UpdateNotifier = class {
16901
17452
  };
16902
17453
 
16903
17454
  // src/bot/fleet-store.ts
16904
- import { mkdir as mkdir19, readFile as readFile28 } from "fs/promises";
16905
- import { dirname as dirname16 } from "path";
17455
+ import { mkdir as mkdir19, readFile as readFile29 } from "fs/promises";
17456
+ import { dirname as dirname17 } from "path";
16906
17457
  function validBotInstanceName(name) {
16907
17458
  return /^[a-z][a-z0-9-]{0,31}$/u.test(name);
16908
17459
  }
@@ -16914,7 +17465,7 @@ var BotFleetStore = class {
16914
17465
  data = { schemaVersion: 1, bots: {} };
16915
17466
  async load() {
16916
17467
  try {
16917
- const parsed = JSON.parse(await readFile28(this.path, "utf8"));
17468
+ const parsed = JSON.parse(await readFile29(this.path, "utf8"));
16918
17469
  this.data = {
16919
17470
  schemaVersion: 1,
16920
17471
  bots: Object.fromEntries(
@@ -17023,7 +17574,7 @@ var BotFleetStore = class {
17023
17574
  })).sort((a, b) => a.name.localeCompare(b.name));
17024
17575
  }
17025
17576
  async persist() {
17026
- await mkdir19(dirname16(this.path), { recursive: true });
17577
+ await mkdir19(dirname17(this.path), { recursive: true });
17027
17578
  await writeFileAtomic(this.path, `${JSON.stringify(this.data, null, 2)}
17028
17579
  `, { mode: 384 });
17029
17580
  }
@@ -17042,8 +17593,8 @@ function isFleetEntry(value) {
17042
17593
  }
17043
17594
 
17044
17595
  // src/bot/handoff-guard.ts
17045
- import { mkdir as mkdir20, readFile as readFile29 } from "fs/promises";
17046
- import { dirname as dirname17 } from "path";
17596
+ import { mkdir as mkdir20, readFile as readFile30 } from "fs/promises";
17597
+ import { dirname as dirname18 } from "path";
17047
17598
  var BotHandoffGuard = class {
17048
17599
  constructor(path) {
17049
17600
  this.path = path;
@@ -17083,7 +17634,7 @@ var BotHandoffGuard = class {
17083
17634
  }
17084
17635
  async read() {
17085
17636
  try {
17086
- const parsed = JSON.parse(await readFile29(this.path, "utf8"));
17637
+ const parsed = JSON.parse(await readFile30(this.path, "utf8"));
17087
17638
  return { schemaVersion: 1, chats: parsed.chats ?? {} };
17088
17639
  } catch (error) {
17089
17640
  if (error.code !== "ENOENT") throw error;
@@ -17091,7 +17642,7 @@ var BotHandoffGuard = class {
17091
17642
  }
17092
17643
  }
17093
17644
  async write(data) {
17094
- await mkdir20(dirname17(this.path), { recursive: true });
17645
+ await mkdir20(dirname18(this.path), { recursive: true });
17095
17646
  await writeFileAtomic(this.path, `${JSON.stringify(data, null, 2)}
17096
17647
  `, { mode: 384 });
17097
17648
  }
@@ -17109,7 +17660,7 @@ init_dsh_runtime();
17109
17660
  import { homedir as homedir16 } from "os";
17110
17661
 
17111
17662
  // src/bot/job-ledger.ts
17112
- import { readFile as readFile30, rename as rename4 } from "fs/promises";
17663
+ import { readFile as readFile31, rename as rename4 } from "fs/promises";
17113
17664
  var EMPTY_COUNTS = {
17114
17665
  queued: 0,
17115
17666
  running: 0,
@@ -17132,7 +17683,7 @@ var JobLedger = class {
17132
17683
  if (this.path === ":memory:") return;
17133
17684
  let raw;
17134
17685
  try {
17135
- raw = await readFile30(this.path, "utf8");
17686
+ raw = await readFile31(this.path, "utf8");
17136
17687
  } catch (error) {
17137
17688
  if (error.code === "ENOENT") return;
17138
17689
  throw error;
@@ -17873,8 +18424,8 @@ init_own_package();
17873
18424
  // src/guardian/update-handoff.ts
17874
18425
  import { createHash as createHash3, randomUUID as randomUUID13 } from "crypto";
17875
18426
  import { spawn as spawn7 } from "child_process";
17876
- import { chmod, mkdir as mkdir21, readFile as readFile31, rm as rm10 } from "fs/promises";
17877
- import { dirname as dirname18, join as join26 } from "path";
18427
+ import { chmod, mkdir as mkdir21, readFile as readFile32, rm as rm10 } from "fs/promises";
18428
+ import { dirname as dirname19, join as join26 } from "path";
17878
18429
  init_own_package();
17879
18430
  init_process();
17880
18431
  function guardianUpdateFailureHint(code) {
@@ -17903,7 +18454,7 @@ function guardianUpdateFailureHint(code) {
17903
18454
  }
17904
18455
  async function loadState(file) {
17905
18456
  try {
17906
- const parsed = JSON.parse(await readFile31(file, "utf8"));
18457
+ const parsed = JSON.parse(await readFile32(file, "utf8"));
17907
18458
  return parsed.schemaVersion === 1 && typeof parsed.id === "string" ? parsed : void 0;
17908
18459
  } catch (error) {
17909
18460
  if (error.code === "ENOENT") return void 0;
@@ -17911,7 +18462,7 @@ async function loadState(file) {
17911
18462
  }
17912
18463
  }
17913
18464
  async function saveState(file, state) {
17914
- await mkdir21(dirname18(file), { recursive: true });
18465
+ await mkdir21(dirname19(file), { recursive: true });
17915
18466
  await writeFileAtomic(file, `${JSON.stringify(state, null, 2)}
17916
18467
  `, { mode: 384 });
17917
18468
  }
@@ -17986,7 +18537,7 @@ async function runGuardianUpdateWorker(request, options = {}) {
17986
18537
  if (delayMs > 0) await new Promise((resolve6) => setTimeout(resolve6, delayMs));
17987
18538
  const spec = `${state.packageName}@${state.targetVersion}`;
17988
18539
  const run = options.run ?? captureOutput;
17989
- const workerRoot = join26(dirname18(request.stateFile), "update-worker");
18540
+ const workerRoot = join26(dirname19(request.stateFile), "update-worker");
17990
18541
  const cacheRoot = join26(workerRoot, "npm-cache");
17991
18542
  const cacheDir = join26(
17992
18543
  cacheRoot,
@@ -18299,6 +18850,8 @@ async function startBridgeEngine(options) {
18299
18850
  const isolationStore = new IsolationStore(paths.profilePath(profileName, "isolation.json"));
18300
18851
  const permissionPolicies = new PermissionPolicyStore(paths.permissionPoliciesFile(profileName));
18301
18852
  const notificationPreferences = new NotificationPreferenceStore(paths.notificationPreferencesFile(profileName));
18853
+ const notificationChannels = new NotificationChannelStore(paths.notificationChannelsFile(profileName));
18854
+ const notificationSinks = new OutboundSinkRegistry(notificationChannels);
18302
18855
  const replyPolicies = new ReplyPolicyStore(paths.replyPoliciesFile(profileName));
18303
18856
  const executionModes = new ExecutionModeStore(paths.executionModesFile(profileName));
18304
18857
  const languagePolicies = new LanguagePolicyStore(paths.languagePoliciesFile(profileName));
@@ -18321,6 +18874,7 @@ async function startBridgeEngine(options) {
18321
18874
  isolationStore.load(),
18322
18875
  permissionPolicies.load(),
18323
18876
  notificationPreferences.load(),
18877
+ notificationChannels.load(),
18324
18878
  replyPolicies.load(),
18325
18879
  executionModes.load(),
18326
18880
  languagePolicies.load()
@@ -18390,7 +18944,8 @@ async function startBridgeEngine(options) {
18390
18944
  send: async (chatId, markdown2, options2) => {
18391
18945
  if (!streaming) throw new Error("bridge channel is not ready");
18392
18946
  await streaming.sendMarkdown(chatId, markdown2, options2);
18393
- }
18947
+ },
18948
+ sinks: notificationSinks
18394
18949
  });
18395
18950
  const replyDispatcher = new ReplyDispatcher({
18396
18951
  policies: replyPolicies,
@@ -18681,6 +19236,9 @@ async function startBridgeEngine(options) {
18681
19236
  approvals,
18682
19237
  permissionPolicies,
18683
19238
  notificationPreferences,
19239
+ notificationChannels,
19240
+ notificationSinks,
19241
+ faultNotifier: (scope, title, detail) => notificationDispatcher.notifyUrgent(scope, title, detail),
18684
19242
  ...defaultNotificationPreference ? { defaultNotificationPreference: { ...defaultNotificationPreference, events: [...defaultNotificationPreference.events] } } : {},
18685
19243
  replyPolicies,
18686
19244
  executionModes,
@@ -18943,6 +19501,7 @@ function notificationPreferenceFor(value) {
18943
19501
  return {
18944
19502
  events: value === "all" ? ["completed", "failed", "approval"] : ["completed", "failed"],
18945
19503
  mentionUserIds: [],
19504
+ sinks: [],
18946
19505
  approvalReminderMs: 10 * 6e4
18947
19506
  };
18948
19507
  }
@@ -18978,7 +19537,7 @@ function waitForShutdown() {
18978
19537
  init_dsh_runtime();
18979
19538
  init_own_package();
18980
19539
  import { spawn as spawn8 } from "child_process";
18981
- 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";
18982
19541
  import { homedir as homedir17 } from "os";
18983
19542
  import { join as join28 } from "path";
18984
19543
  import { parse } from "yaml";
@@ -19059,7 +19618,7 @@ async function approveBuilds(profileDir) {
19059
19618
  const workspaceFile = join28(profileDir, "pnpm-workspace.yaml");
19060
19619
  let existing = "";
19061
19620
  try {
19062
- existing = await readFile32(workspaceFile, "utf8");
19621
+ existing = await readFile33(workspaceFile, "utf8");
19063
19622
  } catch {
19064
19623
  }
19065
19624
  const allowBuilds = [
@@ -19087,8 +19646,8 @@ async function preserveInstalledPnpmVersion(profileDir) {
19087
19646
  let modulesText;
19088
19647
  try {
19089
19648
  [packageText, modulesText] = await Promise.all([
19090
- readFile32(packageFile, "utf8"),
19091
- readFile32(modulesFile, "utf8")
19649
+ readFile33(packageFile, "utf8"),
19650
+ readFile33(modulesFile, "utf8")
19092
19651
  ]);
19093
19652
  } catch {
19094
19653
  return;
@@ -21166,7 +21725,7 @@ function followLogFile(path, lines) {
21166
21725
  // src/cli/commands/supervise.ts
21167
21726
  import { spawn as spawn10 } from "child_process";
21168
21727
  import { closeSync, mkdirSync, openSync } from "fs";
21169
- import { dirname as dirname19 } from "path";
21728
+ import { dirname as dirname20 } from "path";
21170
21729
  import { homedir as homedir22 } from "os";
21171
21730
  init_dsh_runtime();
21172
21731
  var BASE_BACKOFF_MS = 5e3;
@@ -21200,8 +21759,8 @@ async function runSupervise(options, deps = {}) {
21200
21759
  const logFile = paths.serviceLogFile(profile);
21201
21760
  const dshBin = deps.dshBin ?? discoverDshBin(homedir22(), childEnv);
21202
21761
  if (!dshBin) throw new Error("\u672A\u627E\u5230 dsh CLI\uFF0Cportable supervisor \u65E0\u6CD5\u542F\u52A8 profile\u3002");
21203
- mkdirSync(dirname19(statusFile), { recursive: true });
21204
- mkdirSync(dirname19(logFile), { recursive: true });
21762
+ mkdirSync(dirname20(statusFile), { recursive: true });
21763
+ mkdirSync(dirname20(logFile), { recursive: true });
21205
21764
  const logFd = openSync(logFile, "a");
21206
21765
  const writeStatus = async (state, childPid, restarts2) => {
21207
21766
  const processIdentity = await (deps.readProcessIdentity ?? readLinuxProcessIdentity)(process.pid);