@ycodium-ai/plugin-im 0.2.3192 → 0.2.3202

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/index.js CHANGED
@@ -23360,7 +23360,7 @@ var require_websocket = __commonJS({
23360
23360
  var http = __require("http");
23361
23361
  var net = __require("net");
23362
23362
  var tls = __require("tls");
23363
- var { randomBytes, createHash: createHash5 } = __require("crypto");
23363
+ var { randomBytes, createHash: createHash6 } = __require("crypto");
23364
23364
  var { Duplex, Readable } = __require("stream");
23365
23365
  var { URL: URL2 } = __require("url");
23366
23366
  var PerMessageDeflate = require_permessage_deflate();
@@ -24028,7 +24028,7 @@ var require_websocket = __commonJS({
24028
24028
  abortHandshake(websocket, socket, "Invalid Upgrade header");
24029
24029
  return;
24030
24030
  }
24031
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
24031
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
24032
24032
  if (res.headers["sec-websocket-accept"] !== digest) {
24033
24033
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
24034
24034
  return;
@@ -24397,7 +24397,7 @@ var require_websocket_server = __commonJS({
24397
24397
  var EventEmitter = __require("events");
24398
24398
  var http = __require("http");
24399
24399
  var { Duplex } = __require("stream");
24400
- var { createHash: createHash5 } = __require("crypto");
24400
+ var { createHash: createHash6 } = __require("crypto");
24401
24401
  var extension = require_extension();
24402
24402
  var PerMessageDeflate = require_permessage_deflate();
24403
24403
  var subprotocol = require_subprotocol();
@@ -24704,7 +24704,7 @@ var require_websocket_server = __commonJS({
24704
24704
  );
24705
24705
  }
24706
24706
  if (this._state > RUNNING) return abortHandshake(socket, 503);
24707
- const digest = createHash5("sha1").update(key + GUID).digest("base64");
24707
+ const digest = createHash6("sha1").update(key + GUID).digest("base64");
24708
24708
  const headers = [
24709
24709
  "HTTP/1.1 101 Switching Protocols",
24710
24710
  "Upgrade: websocket",
@@ -126185,22 +126185,26 @@ var makeAcknowledgements = (core) => {
126185
126185
  host.log.warn("chat could not acknowledge the message", { error: describeError(error) });
126186
126186
  }
126187
126187
  };
126188
- const clearWorking = async (binding, threadId, outcome) => {
126188
+ const takeWorking = (binding, threadId, outcome) => {
126189
126189
  const adapter = adapterFor(binding.platform);
126190
+ const clear = adapter?.clearAcknowledge?.bind(adapter);
126190
126191
  const pending = acknowledgements.get(threadId);
126191
- if (pending === void 0 || adapter?.clearAcknowledge === void 0) return;
126192
+ if (pending === void 0 || clear === void 0) return async () => void 0;
126192
126193
  acknowledgements.delete(threadId);
126193
- for (const entry of pending) {
126194
- try {
126195
- await adapter.clearAcknowledge(binding, entry.messageId, entry.ackId, outcome);
126196
- } catch (error) {
126197
- host.log.warn("chat could not clear the acknowledgement", {
126198
- error: describeError(error)
126199
- });
126194
+ return async () => {
126195
+ for (const entry of pending) {
126196
+ try {
126197
+ await clear(binding, entry.messageId, entry.ackId, outcome);
126198
+ } catch (error) {
126199
+ host.log.warn("chat could not clear the acknowledgement", {
126200
+ error: describeError(error)
126201
+ });
126202
+ }
126200
126203
  }
126201
- }
126204
+ };
126202
126205
  };
126203
- return { markWorking, clearWorking };
126206
+ const clearWorking = (binding, threadId, outcome) => takeWorking(binding, threadId, outcome)();
126207
+ return { markWorking, takeWorking, clearWorking };
126204
126208
  };
126205
126209
 
126206
126210
  // src/botConfig.ts
@@ -126401,7 +126405,7 @@ var makeConclusionLedger = (core) => {
126401
126405
  return false;
126402
126406
  inFlight.add(key);
126403
126407
  try {
126404
- const sent = await send();
126408
+ const sent = await send(() => conclusionDebtsOf(core.state)[key] !== void 0);
126405
126409
  if (sent) settle(debt);
126406
126410
  return sent;
126407
126411
  } finally {
@@ -126837,6 +126841,7 @@ var readString = (row, key) => {
126837
126841
 
126838
126842
  // src/settingsContract.ts
126839
126843
  var SETTINGS_VIEW_COMMAND = "settings.view";
126844
+ var SETTINGS_CONNECTIONS_COMMAND = "settings.connections";
126840
126845
  var SETTINGS_SAVE_BOT_COMMAND = "settings.saveBot";
126841
126846
  var SETTINGS_REMOVE_BOT_COMMAND = "settings.removeBot";
126842
126847
  var SETTINGS_SAVE_GENERAL_COMMAND = "settings.saveGeneral";
@@ -126877,6 +126882,7 @@ var foldFlowItem = (snapshot, item) => {
126877
126882
  }
126878
126883
  }
126879
126884
  };
126885
+ var connectionHintKey = (hint) => hint === "notAvailable" ? "settingsSection.connection.reason.not-available" : `rowAction.checkConnection.hint.${hint}`;
126880
126886
 
126881
126887
  // src/targetKind.ts
126882
126888
  var TARGET_KIND_OPTIONS = [
@@ -127943,13 +127949,58 @@ var makeCommands = (core, deps) => {
127943
127949
  };
127944
127950
 
127945
127951
  // src/keyedQueue.ts
127946
- var makeKeyedQueue = (onError) => {
127947
- const tails = /* @__PURE__ */ new Map();
127948
- return (key, work) => {
127949
- const run = (tails.get(key) ?? Promise.resolve()).then(work).catch(onError).then(() => {
127950
- if (tails.get(key) === run) tails.delete(key);
127952
+ var makeKeyedQueue = (onError, options = {}) => {
127953
+ const queues = /* @__PURE__ */ new Map();
127954
+ const run = (key, work, kind = {}) => {
127955
+ const queue = queues.get(key) ?? { tail: Promise.resolve(), size: 0, droppable: 0 };
127956
+ const droppable = kind.droppable === true;
127957
+ if (droppable && options.limit !== void 0 && queue.droppable >= options.limit) {
127958
+ options.onFull?.(key, options.limit);
127959
+ return false;
127960
+ }
127961
+ queue.size += 1;
127962
+ if (droppable) queue.droppable += 1;
127963
+ queue.tail = queue.tail.then(work).catch(onError).then(() => {
127964
+ queue.size -= 1;
127965
+ if (droppable) queue.droppable -= 1;
127966
+ if (queue.size === 0 && queues.get(key) === queue) queues.delete(key);
127951
127967
  });
127952
- tails.set(key, run);
127968
+ queues.set(key, queue);
127969
+ return true;
127970
+ };
127971
+ return Object.assign(run, { activeKeys: () => queues.size });
127972
+ };
127973
+
127974
+ // src/redact.ts
127975
+ var DETAIL_MAX_CHARS = 200;
127976
+ var MASK = "***";
127977
+ var TOKEN_LIKE = /[A-Za-z0-9_=-]{24,}/g;
127978
+ var MIN_SECRET_LENGTH = 6;
127979
+ var botRedactor = (columns, values) => {
127980
+ const redactor = makeRedactor();
127981
+ for (const column of columns) {
127982
+ if (column.kind !== "password") continue;
127983
+ const value2 = values[column.key];
127984
+ redactor.register(value2);
127985
+ if (value2 !== void 0) redactor.register(encodeURIComponent(value2));
127986
+ }
127987
+ return redactor;
127988
+ };
127989
+ var makeRedactor = () => {
127990
+ const secrets = /* @__PURE__ */ new Set();
127991
+ const redact = (text4) => {
127992
+ let out = text4;
127993
+ for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
127994
+ out = out.split(secret).join(MASK);
127995
+ }
127996
+ return out.replace(TOKEN_LIKE, MASK);
127997
+ };
127998
+ return {
127999
+ register: (secret) => {
128000
+ if (typeof secret === "string" && secret.length >= MIN_SECRET_LENGTH) secrets.add(secret);
128001
+ },
128002
+ redact,
128003
+ detail: (text4) => redact(text4).slice(0, DETAIL_MAX_CHARS)
127953
128004
  };
127954
128005
  };
127955
128006
 
@@ -127967,26 +128018,61 @@ var connectionKey = (adapter, bot) => JSON.stringify([
127967
128018
  var startConnections = (core, inboundHandlersFor) => {
127968
128019
  const { host, adapterFor, ready } = core;
127969
128020
  const live = /* @__PURE__ */ new Map();
128021
+ const statuses = /* @__PURE__ */ new Map();
128022
+ const listeners = /* @__PURE__ */ new Set();
128023
+ const notify = () => {
128024
+ const snapshot = Object.fromEntries(statuses);
128025
+ for (const listener of [...listeners]) listener(snapshot);
128026
+ };
128027
+ const record3 = (rowId, status) => {
128028
+ const previous = statuses.get(rowId);
128029
+ if (previous?.state === status.state && previous.error === status.error && previous.reason === status.reason && previous.duplicateOf === status.duplicateOf && previous.hint === status.hint)
128030
+ return;
128031
+ statuses.set(rowId, status);
128032
+ notify();
128033
+ };
128034
+ const remove = (rowId) => {
128035
+ if (statuses.delete(rowId)) notify();
128036
+ };
127970
128037
  const enqueueForRow = makeKeyedQueue((error) => {
127971
128038
  host.log.warn("chat could not start an inbound connection", { error: describeError(error) });
127972
128039
  });
127973
128040
  let disposed = false;
127974
128041
  const open = (bot, key) => {
127975
128042
  const adapter = adapterFor(bot.platform);
128043
+ const redactor = botRedactor(adapter?.columns ?? [], bot.values);
127976
128044
  let closed = false;
127977
128045
  let closeConnection;
128046
+ record3(bot.rowId, { state: "connecting" });
127978
128047
  enqueueForRow(bot.rowId, async () => {
127979
128048
  try {
127980
128049
  await ready;
127981
128050
  if (closed || adapter === void 0) return;
127982
- const connection = await adapter.connect(bot, inboundHandlersFor(bot));
128051
+ const connection = await adapter.connect(bot, {
128052
+ ...inboundHandlersFor(bot),
128053
+ onConnectionState: (status) => {
128054
+ if (closed) return;
128055
+ if (status.state === "connected") {
128056
+ record3(bot.rowId, status);
128057
+ return;
128058
+ }
128059
+ record3(bot.rowId, {
128060
+ state: "failed",
128061
+ ...status.reason === void 0 ? {} : { reason: status.reason },
128062
+ ...status.error === void 0 ? {} : { error: redactor.redact(describeError(status.error)) },
128063
+ ...status.hint === void 0 ? {} : { hint: status.hint }
128064
+ });
128065
+ }
128066
+ });
127983
128067
  if (closed) connection.close();
127984
128068
  else closeConnection = () => connection.close();
127985
128069
  } catch (error) {
128070
+ const detail = redactor.redact(describeError(error));
128071
+ if (!closed) record3(bot.rowId, { state: "failed", error: detail });
127986
128072
  host.log.warn("chat could not start an inbound connection", {
127987
128073
  bot: bot.displayName,
127988
128074
  platform: bot.platform,
127989
- error: describeError(error)
128075
+ error: detail
127990
128076
  });
127991
128077
  }
127992
128078
  });
@@ -128001,19 +128087,25 @@ var startConnections = (core, inboundHandlersFor) => {
128001
128087
  const reconcile = () => {
128002
128088
  const bots = disposed ? [] : core.bots();
128003
128089
  const wanted = /* @__PURE__ */ new Map();
128004
- const credentials3 = /* @__PURE__ */ new Set();
128090
+ const credentials3 = /* @__PURE__ */ new Map();
128091
+ const duplicates = /* @__PURE__ */ new Map();
128005
128092
  for (const bot of bots) {
128006
128093
  const adapter = adapterFor(bot.platform);
128007
128094
  if (adapter === void 0) continue;
128008
128095
  const fingerprint = `${bot.platform}:${adapter.columns.map((column) => bot.values[column.key] ?? "").join(" ")}`;
128009
- if (credentials3.has(fingerprint)) {
128096
+ const duplicateOf = credentials3.get(fingerprint);
128097
+ if (duplicateOf !== void 0) {
128098
+ duplicates.set(bot.rowId, duplicateOf);
128099
+ if (statuses.get(bot.rowId)?.duplicateOf !== duplicateOf) {
128100
+ record3(bot.rowId, { state: "duplicate", duplicateOf });
128101
+ }
128010
128102
  host.log.warn("chat did not connect a bot that repeats another bot's credentials", {
128011
128103
  bot: bot.displayName,
128012
128104
  platform: bot.platform
128013
128105
  });
128014
128106
  continue;
128015
128107
  }
128016
- credentials3.add(fingerprint);
128108
+ credentials3.set(fingerprint, bot.rowId);
128017
128109
  wanted.set(bot.rowId, { bot, key: connectionKey(adapter, bot) });
128018
128110
  }
128019
128111
  for (const [rowId, connection] of live) {
@@ -128021,6 +128113,9 @@ var startConnections = (core, inboundHandlersFor) => {
128021
128113
  connection.close();
128022
128114
  live.delete(rowId);
128023
128115
  }
128116
+ for (const rowId of statuses.keys()) {
128117
+ if (!wanted.has(rowId) && !duplicates.has(rowId)) remove(rowId);
128118
+ }
128024
128119
  for (const [rowId, { bot, key }] of wanted) {
128025
128120
  if (!live.has(rowId)) live.set(rowId, open(bot, key));
128026
128121
  }
@@ -128029,8 +128124,18 @@ var startConnections = (core, inboundHandlersFor) => {
128029
128124
  host.onDispose(() => {
128030
128125
  disposed = true;
128031
128126
  reconcile();
128127
+ listeners.clear();
128032
128128
  });
128033
- return { reconcile };
128129
+ return {
128130
+ reconcile,
128131
+ subscribe: (listener) => {
128132
+ if (!disposed) listeners.add(listener);
128133
+ listener(Object.fromEntries(statuses));
128134
+ return () => {
128135
+ listeners.delete(listener);
128136
+ };
128137
+ }
128138
+ };
128034
128139
  };
128035
128140
 
128036
128141
  // src/deliveryHttp.ts
@@ -128279,6 +128384,7 @@ ${token}` : void 0;
128279
128384
  };
128280
128385
 
128281
128386
  // src/core.ts
128387
+ var OUTBOUND_QUEUE_LIMIT = 100;
128282
128388
  var makeImCore = (input) => {
128283
128389
  const { host, t, adapters } = input;
128284
128390
  const adapterFor = (platform) => adapters.find((adapter) => adapter.platform === platform);
@@ -128313,10 +128419,21 @@ var makeImCore = (input) => {
128313
128419
  const bindChat = (chatKey, threadId) => {
128314
128420
  commit("chat-updated", { chatKey, threadId });
128315
128421
  };
128316
- const outbound = makeKeyedQueue((error) => {
128317
- host.log.warn("chat could not deliver a card", { error: describeError(error) });
128318
- });
128319
- const postImages = async (binding, attachments) => {
128422
+ const outbound = makeKeyedQueue(
128423
+ (error) => {
128424
+ host.log.warn("chat could not deliver queued work", { error: describeError(error) });
128425
+ },
128426
+ {
128427
+ limit: OUTBOUND_QUEUE_LIMIT,
128428
+ onFull: (chatKey, limit) => {
128429
+ host.log.warn("chat queue is full; refused a conclusion until restart", {
128430
+ chatKey,
128431
+ limit
128432
+ });
128433
+ }
128434
+ }
128435
+ );
128436
+ const postImages = async (binding, attachments, onFailure) => {
128320
128437
  const adapter = adapterFor(binding.platform);
128321
128438
  if (adapter?.sendImage === void 0) return;
128322
128439
  for (const attachment of attachments) {
@@ -128333,19 +128450,22 @@ var makeImCore = (input) => {
128333
128450
  name: attachment.name,
128334
128451
  error: describeError(error)
128335
128452
  });
128453
+ await onFailure?.(error);
128336
128454
  }
128337
128455
  }
128338
128456
  };
128339
- const deliver = async (binding, text4, attachments = []) => {
128340
- await postImages(binding, attachments);
128457
+ const deliver = async (binding, text4, attachments = [], options = {}) => {
128458
+ const { retry, onFailure } = options;
128459
+ await postImages(binding, attachments, onFailure);
128341
128460
  if (text4.trim() === "") return true;
128342
128461
  const adapter = adapterFor(binding.platform);
128343
128462
  if (adapter === void 0) return false;
128344
128463
  try {
128345
- await adapter.sendText(binding, text4);
128464
+ await adapter.sendText(binding, text4, { retry });
128346
128465
  return true;
128347
128466
  } catch (error) {
128348
128467
  host.log.warn("chat could not deliver", { error: describeError(error) });
128468
+ await onFailure?.(error);
128349
128469
  return false;
128350
128470
  }
128351
128471
  };
@@ -128377,6 +128497,92 @@ var makeImCore = (input) => {
128377
128497
  // src/conclusionDelivery.ts
128378
128498
  import { threadOrNull as threadOrNull3 } from "@ycodium-ai/plugin-api";
128379
128499
 
128500
+ // src/abortableSleep.ts
128501
+ var abortableSleep = (ms, signal) => new Promise((resolve, reject) => {
128502
+ if (signal?.aborted === true) {
128503
+ reject(new Error("aborted"));
128504
+ return;
128505
+ }
128506
+ const onAbort = () => {
128507
+ clearTimeout(timer);
128508
+ reject(new Error("aborted"));
128509
+ };
128510
+ const timer = setTimeout(() => {
128511
+ signal?.removeEventListener("abort", onAbort);
128512
+ resolve();
128513
+ }, ms);
128514
+ signal?.addEventListener("abort", onAbort, { once: true });
128515
+ });
128516
+
128517
+ // src/deliveryFailure.ts
128518
+ import * as NodeCrypto2 from "node:crypto";
128519
+
128520
+ // src/diagnostics.ts
128521
+ var PREFIX = {
128522
+ conn: "IM-CONN",
128523
+ msg: "IM-MSG",
128524
+ setup: "IM-SETUP"
128525
+ };
128526
+ var shortSalt = () => {
128527
+ const bytes = new Uint8Array(2);
128528
+ crypto.getRandomValues(bytes);
128529
+ return Array.from(bytes, (b) => b.toString(16).toUpperCase().padStart(2, "0")).join("");
128530
+ };
128531
+ var stableDiagnosticRef = (kind, digest) => `${PREFIX[kind]}-${digest.slice(0, 16).toUpperCase()}`;
128532
+ var diagnosticRef = (kind, nowMs = Date.now()) => {
128533
+ const stamp = nowMs.toString(36).toUpperCase();
128534
+ return `${PREFIX[kind]}-${stamp}-${shortSalt()}`;
128535
+ };
128536
+ var formatDiagnosticCopy = (report) => [
128537
+ `ok=${report.ok ? "yes" : "no"}`,
128538
+ `platform=${report.platform}`,
128539
+ `bot=${report.botName}`,
128540
+ `row=${report.rowId}`,
128541
+ `stage=${report.stage}`,
128542
+ `at=${report.at}`,
128543
+ `detail=${report.detail}`
128544
+ ].join("\n");
128545
+
128546
+ // src/deliveryFailure.ts
128547
+ var deliveryFailure = (core, binding, threadId, turnId) => async (error) => {
128548
+ const rejection = core.adapterFor(binding.platform)?.describeDeliveryRejection?.(error, binding);
128549
+ if (rejection === void 0) return;
128550
+ const { recipients } = rejection;
128551
+ const digest = NodeCrypto2.createHash("sha256").update(JSON.stringify([threadId, turnId, binding.platform, binding.rowId, recipients])).digest("hex");
128552
+ const id = `im.delivery-failed:${digest}`;
128553
+ const reference2 = stableDiagnosticRef("msg", digest);
128554
+ const payload = {
128555
+ platform: binding.platform,
128556
+ recipients,
128557
+ hint: rejection.hint,
128558
+ providerCode: rejection.providerCode,
128559
+ reference: reference2,
128560
+ sourceTurnId: turnId
128561
+ };
128562
+ core.host.log.warn("chat recipient rejected delivery", { threadId, ...payload });
128563
+ if (core.state.opened[threadId] === void 0) return;
128564
+ try {
128565
+ await core.host.threads.externalMessages.append({
128566
+ requestId: id,
128567
+ threadId,
128568
+ messages: [],
128569
+ activities: [
128570
+ {
128571
+ id,
128572
+ tone: "error",
128573
+ kind: "im.delivery-failed",
128574
+ summary: rejection.summary,
128575
+ payload,
128576
+ turnId: null,
128577
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
128578
+ }
128579
+ ]
128580
+ });
128581
+ } catch {
128582
+ core.host.log.warn("chat could not append delivery failure notice", { threadId, reference: reference2 });
128583
+ }
128584
+ };
128585
+
128380
128586
  // src/threadMessages.ts
128381
128587
  import {
128382
128588
  isThreadNotFound,
@@ -128428,20 +128634,30 @@ async function messagesAfterLastUser(threads, threadId) {
128428
128634
  }
128429
128635
 
128430
128636
  // src/conclusionDelivery.ts
128637
+ var ALWAYS_OWED = () => true;
128431
128638
  var makeConclusionDelivery = (core, anchors) => {
128432
128639
  let disposed = false;
128640
+ const stopped = new AbortController();
128433
128641
  core.host.onDispose(() => {
128434
128642
  disposed = true;
128643
+ stopped.abort();
128435
128644
  });
128436
128645
  const marked = (text4, recovered) => recovered ? `${core.t("reply.conclusionRecovered")}
128437
128646
  ${text4}` : text4;
128647
+ const sleep3 = (ms) => abortableSleep(ms, stopped.signal).catch(() => void 0);
128648
+ const retryFor = (threadId, owed = ALWAYS_OWED) => ({
128649
+ sleep: sleep3,
128650
+ shouldContinue: async () => !disposed && owed() && await threadOrNull3(core.host.threads, threadId) !== null
128651
+ });
128438
128652
  return {
128653
+ retryFor,
128439
128654
  prepareConclusion: async (threadId, turnId = null) => {
128440
128655
  const thread = await threadOrNull3(core.host.threads, threadId);
128441
128656
  const final = thread === null ? void 0 : await latestMessage(
128442
128657
  core.host.threads,
128443
128658
  threadId,
128444
- (message) => message.role === "assistant" && message.text.trim() !== ""
128659
+ // 给了轮次就只取这一轮的正文:排队等着的结论不能拿到下一轮的话。没记轮次的老消息照旧认。
128660
+ (message) => message.role === "assistant" && message.text.trim() !== "" && (turnId === null || message.turnId === null || message.turnId === turnId)
128445
128661
  );
128446
128662
  const attachments = final === void 0 ? [] : await attachmentsOf(core.host.threads, threadId, final);
128447
128663
  return async (binding, options) => {
@@ -128451,12 +128667,22 @@ ${text4}` : text4;
128451
128667
 
128452
128668
  ${final.text}`, options.recovered);
128453
128669
  const anchored = await anchors.anchored(binding, threadId, turnId ?? final.turnId);
128454
- return !disposed && core.deliver(anchored, text4, attachments);
128670
+ return !disposed && core.deliver(anchored, text4, attachments, {
128671
+ retry: retryFor(threadId, options.owed),
128672
+ onFailure: deliveryFailure(core, binding, threadId, turnId ?? final.turnId)
128673
+ });
128455
128674
  };
128456
128675
  },
128457
- failure: async (binding, threadId, notice, recovered, turnId = null) => {
128676
+ /**
128677
+ * 发一段已经写好的结论文字(失败通知、卡片定稿失败时的文字兜底):锚点、停用检查、重发与
128678
+ * 拒收提醒都在这里,调用方只管写文字。
128679
+ */
128680
+ sendText: async (binding, threadId, notice, recovered, turnId = null, owed = ALWAYS_OWED) => {
128458
128681
  const anchored = await anchors.anchored(binding, threadId, turnId);
128459
- return !disposed && core.deliver(anchored, marked(notice, recovered));
128682
+ return !disposed && core.deliver(anchored, marked(notice, recovered), [], {
128683
+ retry: retryFor(threadId, owed),
128684
+ onFailure: deliveryFailure(core, binding, threadId, turnId)
128685
+ });
128460
128686
  }
128461
128687
  };
128462
128688
  };
@@ -130583,29 +130809,6 @@ var sleep = async (deps) => {
130583
130809
  }
130584
130810
  };
130585
130811
 
130586
- // src/redact.ts
130587
- var DETAIL_MAX_CHARS = 200;
130588
- var MASK = "***";
130589
- var TOKEN_LIKE = /[A-Za-z0-9_=-]{24,}/g;
130590
- var MIN_SECRET_LENGTH = 6;
130591
- var makeRedactor = () => {
130592
- const secrets = /* @__PURE__ */ new Set();
130593
- const redact = (text4) => {
130594
- let out = text4;
130595
- for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
130596
- out = out.split(secret).join(MASK);
130597
- }
130598
- return out.replace(TOKEN_LIKE, MASK);
130599
- };
130600
- return {
130601
- register: (secret) => {
130602
- if (typeof secret === "string" && secret.length >= MIN_SECRET_LENGTH) secrets.add(secret);
130603
- },
130604
- redact,
130605
- detail: (text4) => redact(text4).slice(0, DETAIL_MAX_CHARS)
130606
- };
130607
- };
130608
-
130609
130812
  // src/lark/setup/cookieJar.ts
130610
130813
  var MAX_REDIRECT_HOPS = 10;
130611
130814
  var parseSetCookie = (responseUrl, header) => {
@@ -131323,21 +131526,7 @@ var buildChecklist = () => ({
131323
131526
  });
131324
131527
  var makeRealClock = () => ({
131325
131528
  now: () => Date.now(),
131326
- sleep: (ms, signal) => new Promise((resolve, reject) => {
131327
- if (signal?.aborted === true) {
131328
- reject(new Error("aborted"));
131329
- return;
131330
- }
131331
- const onAbort = () => {
131332
- clearTimeout(timer);
131333
- reject(new Error("aborted"));
131334
- };
131335
- const timer = setTimeout(() => {
131336
- signal?.removeEventListener("abort", onAbort);
131337
- resolve();
131338
- }, ms);
131339
- signal?.addEventListener("abort", onAbort, { once: true });
131340
- })
131529
+ sleep: abortableSleep
131341
131530
  });
131342
131531
  var saveAndConclude = async (ctx, updateSettings, result) => {
131343
131532
  try {
@@ -131429,6 +131618,9 @@ var registerLarkSetup = (ctx, options = {}) => {
131429
131618
  return runner;
131430
131619
  };
131431
131620
 
131621
+ // src/larkApi.ts
131622
+ import { parseRetryAfterMs } from "@ycodium-ai/plugin-api";
131623
+
131432
131624
  // src/larkPost.ts
131433
131625
  var styledText = (text4, style) => {
131434
131626
  let out = text4;
@@ -131608,6 +131800,33 @@ var parseLarkReply = (message) => {
131608
131800
  }
131609
131801
  };
131610
131802
 
131803
+ // src/sendRetry.ts
131804
+ var NO_RETRY = { retry: false };
131805
+ var SEND_RETRY_DELAYS_MS = [1e3, 4e3, 15e3];
131806
+ var MAX_WAIT_MS = 6e4;
131807
+ var SendStoppedError = class extends Error {
131808
+ constructor(cause) {
131809
+ super("send stopped before retrying", { cause });
131810
+ this.name = "SendStoppedError";
131811
+ }
131812
+ };
131813
+ var sendWithRetry = async (request, verdictOf, retry) => {
131814
+ for (let attempt = 0; ; attempt += 1) {
131815
+ try {
131816
+ return await request();
131817
+ } catch (error) {
131818
+ const delayMs = SEND_RETRY_DELAYS_MS[attempt];
131819
+ if (retry === void 0 || delayMs === void 0) throw error;
131820
+ const verdict = verdictOf(error);
131821
+ if (!verdict.retry) throw error;
131822
+ const waitMs = verdict.afterMs ?? delayMs;
131823
+ if (waitMs > MAX_WAIT_MS) throw error;
131824
+ await retry.sleep(waitMs);
131825
+ if (!await retry.shouldContinue()) throw new SendStoppedError(error);
131826
+ }
131827
+ }
131828
+ };
131829
+
131611
131830
  // src/splitMessage.ts
131612
131831
  var splitMessage = (text4, limit) => {
131613
131832
  if (text4.length <= limit) return [text4];
@@ -131640,19 +131859,39 @@ var LARK_TEXT_MAX_CHARS = 3500;
131640
131859
  var LarkApiError = class extends Error {
131641
131860
  operation;
131642
131861
  code;
131643
- constructor(operation, code2, message) {
131862
+ status;
131863
+ retryAfterMs;
131864
+ constructor(operation, code2, message, http = {}) {
131644
131865
  super(`lark ${operation} failed (code ${String(code2 ?? "?")}): ${message}`);
131645
131866
  this.name = "LarkApiError";
131646
131867
  this.operation = operation;
131647
131868
  this.code = code2;
131869
+ this.status = http.status;
131870
+ this.retryAfterMs = http.retryAfterMs;
131648
131871
  }
131649
131872
  };
131650
- var isLarkPermissionError = (error) => error instanceof LarkApiError && (error.code === 99991672 || error.code === 99991679 || "status" in error && (error.status === 401 || error.status === 403));
131873
+ var isLarkPermissionError = (error) => error instanceof LarkApiError && (error.code === 99991672 || error.code === 99991679 || error.status === 401 || error.status === 403);
131874
+ var LARK_RATE_LIMITED = 99991400;
131875
+ var isReplyRejected = (error) => error instanceof LarkApiError && error.status === void 0 && error.code !== LARK_RATE_LIMITED;
131876
+ var larkSendVerdict = (error) => {
131877
+ if (!(error instanceof LarkApiError)) return NO_RETRY;
131878
+ if (error.status === 429 || error.code === LARK_RATE_LIMITED) {
131879
+ return { retry: true, afterMs: error.retryAfterMs };
131880
+ }
131881
+ if (error.status !== void 0 && error.status >= 500) {
131882
+ return { retry: true, afterMs: error.retryAfterMs };
131883
+ }
131884
+ return NO_RETRY;
131885
+ };
131651
131886
  var readEnvelope = async (operation, response) => {
131652
131887
  if (!response.ok) {
131653
131888
  await response.body?.cancel().catch(() => void 0);
131654
- throw Object.assign(new LarkApiError(operation, void 0, `HTTP ${String(response.status)}`), {
131655
- status: response.status
131889
+ throw new LarkApiError(operation, void 0, `HTTP ${String(response.status)}`, {
131890
+ status: response.status,
131891
+ retryAfterMs: parseRetryAfterMs(
131892
+ response.headers.get("retry-after") ?? response.headers.get("x-ogw-ratelimit-reset"),
131893
+ Date.now()
131894
+ ) ?? void 0
131656
131895
  });
131657
131896
  }
131658
131897
  const body = await response.json();
@@ -131679,35 +131918,37 @@ var postMessage = async (input) => {
131679
131918
  authorization: `Bearer ${input.token}`,
131680
131919
  "content-type": "application/json; charset=utf-8"
131681
131920
  };
131682
- const message = { msg_type: input.msgType, content: JSON.stringify(input.content) };
131683
- const sentOf = async (response2) => (await readEnvelope(input.operation, response2)).data;
131921
+ const message = {
131922
+ msg_type: input.msgType,
131923
+ content: JSON.stringify(input.content),
131924
+ ...input.retry === void 0 ? {} : { uuid: crypto.randomUUID() }
131925
+ };
131926
+ const post = (url, body) => sendWithRetry(
131927
+ async () => (await readEnvelope(
131928
+ input.operation,
131929
+ await input.http(url, { method: "POST", headers, body: JSON.stringify(body) })
131930
+ )).data,
131931
+ larkSendVerdict,
131932
+ input.retry
131933
+ );
131684
131934
  if (input.reply !== void 0) {
131685
131935
  try {
131686
- const response2 = await input.http(
131936
+ const sent2 = await post(
131687
131937
  `${LARK_BASE}/im/v1/messages/${encodeURIComponent(input.reply.messageId)}/reply`,
131688
- {
131689
- method: "POST",
131690
- headers,
131691
- body: JSON.stringify({
131692
- ...message,
131693
- ...input.reply.inThread ? { reply_in_thread: true } : {}
131694
- })
131695
- }
131938
+ { ...message, ...input.reply.inThread ? { reply_in_thread: true } : {} }
131696
131939
  );
131697
- const sent = await sentOf(response2);
131698
- if (input.reply.inThread) input.onThreadReply?.(sent?.thread_id || void 0);
131699
- return sent?.message_id ?? null;
131940
+ if (input.reply.inThread) input.onThreadReply?.(sent2?.thread_id || void 0);
131941
+ return sent2?.message_id ?? null;
131700
131942
  } catch (error) {
131701
- if (!(error instanceof LarkApiError)) throw error;
131943
+ if (!isReplyRejected(error)) throw error;
131702
131944
  }
131703
131945
  }
131704
131946
  const idType = input.receiveIdType ?? "chat_id";
131705
- const response = await input.http(`${LARK_BASE}/im/v1/messages?receive_id_type=${idType}`, {
131706
- method: "POST",
131707
- headers,
131708
- body: JSON.stringify({ receive_id: input.chatId, ...message })
131947
+ const sent = await post(`${LARK_BASE}/im/v1/messages?receive_id_type=${idType}`, {
131948
+ receive_id: input.chatId,
131949
+ ...message
131709
131950
  });
131710
- return (await sentOf(response))?.message_id ?? null;
131951
+ return sent?.message_id ?? null;
131711
131952
  };
131712
131953
  var readMessage = async (input) => {
131713
131954
  const response = await input.http(
@@ -137310,7 +137551,7 @@ var UNSUPPORTED_MESSAGE_TYPES = /* @__PURE__ */ new Set([
137310
137551
  "share_chat",
137311
137552
  "share_user",
137312
137553
  "system",
137313
- "merge",
137554
+ "merge_forward",
137314
137555
  "interactive"
137315
137556
  ]);
137316
137557
  var botIdsOf = (mentions, appId) => {
@@ -137482,6 +137723,7 @@ var cardActionOperator = (event) => {
137482
137723
  }
137483
137724
  return null;
137484
137725
  };
137726
+ var isLarkAppId = (appId) => /^cli_[0-9a-fA-F]{16}$/u.test(appId);
137485
137727
  var startLarkInbound = (input) => {
137486
137728
  const dispatcher = new import_node_sdk.EventDispatcher({}).register({
137487
137729
  "im.message.receive_v1": (event) => {
@@ -137523,20 +137765,42 @@ var startLarkInbound = (input) => {
137523
137765
  return notice === null ? {} : { toast: { type: "info", content: notice } };
137524
137766
  }
137525
137767
  });
137768
+ if (!isLarkAppId(input.appId)) {
137769
+ input.onConnectionState?.({ state: "failed", reason: "invalid-app-id" });
137770
+ return { close: () => void 0 };
137771
+ }
137772
+ let closed = false;
137773
+ const connected = () => {
137774
+ if (!closed) input.onConnectionState?.({ state: "connected" });
137775
+ };
137776
+ const failed = (error) => {
137777
+ if (!closed) {
137778
+ input.onConnectionState?.({ state: "failed", reason: "disconnected", error });
137779
+ }
137780
+ };
137526
137781
  const client = new import_node_sdk.WSClient({
137527
137782
  appId: input.appId,
137528
137783
  appSecret: input.appSecret,
137529
137784
  ...input.domain === void 0 ? {} : { domain: input.domain },
137530
137785
  autoReconnect: true,
137531
- onError: (cause) => input.onError(cause.message)
137786
+ onReady: connected,
137787
+ onReconnected: connected,
137788
+ onReconnecting: () => failed(),
137789
+ onError: (cause) => {
137790
+ if (closed) return;
137791
+ failed(cause);
137792
+ input.onError(cause.message);
137793
+ }
137532
137794
  });
137533
- void client.start({ eventDispatcher: dispatcher });
137534
- return { close: () => client.close({ force: true }) };
137795
+ void client.start({ eventDispatcher: dispatcher }).catch(failed);
137796
+ return {
137797
+ close: () => {
137798
+ closed = true;
137799
+ client.close({ force: true });
137800
+ }
137801
+ };
137535
137802
  };
137536
137803
 
137537
- // src/replyReference.ts
137538
- import * as NodeCrypto2 from "node:crypto";
137539
-
137540
137804
  // src/deadline.ts
137541
137805
  var withDeadline2 = async (timeoutMs, load) => {
137542
137806
  const signal = AbortSignal.timeout(timeoutMs);
@@ -137554,6 +137818,7 @@ var withDeadline2 = async (timeoutMs, load) => {
137554
137818
  };
137555
137819
 
137556
137820
  // src/replyReference.ts
137821
+ import * as NodeCrypto3 from "node:crypto";
137557
137822
  var CONTENT_MAX = 8e3;
137558
137823
  var ATTACHMENTS_MAX = 20;
137559
137824
  var AUTHOR_MAX = 256;
@@ -137626,15 +137891,16 @@ var formatReplyReference = (reference2) => {
137626
137891
  ...lines.map((line) => `> ${line}`)
137627
137892
  ].join("\n");
137628
137893
  };
137629
- var textWithInboundContext = (message, rowId, t) => {
137630
- const reply = formatReplyReference(message.replyTo);
137631
- const text4 = reply === null ? message.text : `${reply}
137632
-
137633
- ${message.text}`;
137894
+ var textWithInboundContext = (message, rowId, t, topicRoot) => {
137895
+ const text4 = [
137896
+ formatReplyReference(topicRoot),
137897
+ formatReplyReference(message.replyTo),
137898
+ message.text
137899
+ ].filter((part) => part !== null).join("\n\n");
137634
137900
  if (message.chatKind !== "group" || message.senderId === null) return text4;
137635
137901
  let name = cleanString(message.senderName, AUTHOR_MAX).value;
137636
137902
  if (!name) {
137637
- const code2 = NodeCrypto2.createHash("sha256").update(JSON.stringify([rowId, message.senderId])).digest("hex").slice(0, 4);
137903
+ const code2 = NodeCrypto3.createHash("sha256").update(JSON.stringify([rowId, message.senderId])).digest("hex").slice(0, 4);
137638
137904
  name = t("source.member", { code: code2 });
137639
137905
  }
137640
137906
  const chat = cleanString(message.chatTitle, AUTHOR_MAX).value;
@@ -137687,6 +137953,26 @@ var makeLookup = (rowId, load) => {
137687
137953
  };
137688
137954
  var makeLarkMessageContext = (deps) => {
137689
137955
  let contactDenied = false;
137956
+ const deliveredRoots = makeBoundedSeen(CACHE_LIMIT);
137957
+ const loadReference = (messageId) => loadReplyReference(async (signal) => {
137958
+ const token = await deps.tenantToken();
137959
+ signal.throwIfAborted();
137960
+ return fetchMessageReference({ http: deps.http, token, messageId, signal });
137961
+ });
137962
+ const topicRootForSession = (message) => {
137963
+ const topic = message.topic;
137964
+ if (topic === void 0 || topic.rootGiven === false) return void 0;
137965
+ return async (recipientId) => {
137966
+ const key = JSON.stringify([message.chatId, topic.rootMessageId, recipientId]);
137967
+ if (deliveredRoots.has(key)) return void 0;
137968
+ return {
137969
+ reference: topic.rootMessageId === message.messageId ? void 0 : await loadReference(topic.rootMessageId),
137970
+ confirmConsumed: () => {
137971
+ deliveredRoots.claim(key);
137972
+ }
137973
+ };
137974
+ };
137975
+ };
137690
137976
  const userName = makeLookup(deps.rowId, async (openId, signal) => {
137691
137977
  try {
137692
137978
  const token = await deps.tenantToken();
@@ -137713,7 +137999,7 @@ var makeLarkMessageContext = (deps) => {
137713
137999
  });
137714
138000
  return name instanceof Promise || chat instanceof Promise ? Promise.all([name, chat]).then(([senderName, info]) => fields(senderName, info)) : fields(name, chat);
137715
138001
  };
137716
- return { chatInfo, forMessage };
138002
+ return { chatInfo, forMessage, loadReference, topicRootForSession };
137717
138003
  };
137718
138004
 
137719
138005
  // src/larkAdapter.ts
@@ -137755,6 +138041,7 @@ var larkConfigIssues = (binding) => {
137755
138041
  const { appId, appSecret } = credentials(binding);
137756
138042
  const issues = [];
137757
138043
  if (appId === "") issues.push({ column: "larkAppId", code: "required" });
138044
+ else if (!isLarkAppId(appId)) issues.push({ column: "larkAppId", code: "format" });
137758
138045
  if (appSecret === "") issues.push({ column: "larkAppSecret", code: "required" });
137759
138046
  return issues;
137760
138047
  };
@@ -137842,7 +138129,7 @@ var makeLarkAdapter = (deps) => {
137842
138129
  return rootMessageId === void 0 ? void 0 : groupId;
137843
138130
  },
137844
138131
  validate: larkConfigIssues,
137845
- isConfigured: (binding) => larkConfigIssues(binding).length === 0,
138132
+ isConfigured: (binding) => larkConfigIssues(binding).every((issue) => issue.code !== "required"),
137846
138133
  connect: (binding, handlers) => {
137847
138134
  const { appId, appSecret } = credentials(binding);
137848
138135
  const port = {
@@ -137871,6 +138158,7 @@ var makeLarkAdapter = (deps) => {
137871
138158
  topicOnMention: topicOnMentionOf(binding),
137872
138159
  isSoloGroup: soloGroupFor(context),
137873
138160
  onError: handlers.onError,
138161
+ ...handlers.onConnectionState === void 0 ? {} : { onConnectionState: handlers.onConnectionState },
137874
138162
  onCardAction: handlers.onButton,
137875
138163
  onPanelClick: (click, operatorOpenId) => handlers.onPanelClick?.({
137876
138164
  chatId: click.chatId,
@@ -137883,14 +138171,7 @@ var makeLarkAdapter = (deps) => {
137883
138171
  const { topic } = message;
137884
138172
  const source = context.forMessage(message);
137885
138173
  const parentMessageId = message.parentMessageId;
137886
- const reference2 = parentMessageId === void 0 ? null : loadReplyReference(
137887
- async (signal) => fetchMessageReference({
137888
- http: deps.http,
137889
- token: await tenantToken(binding),
137890
- messageId: parentMessageId,
137891
- signal
137892
- })
137893
- );
138174
+ const reference2 = parentMessageId === void 0 ? null : context.loadReference(parentMessageId);
137894
138175
  const deliver = (names, replyTo) => handlers.onMessage({
137895
138176
  messageId: message.messageId,
137896
138177
  // 已经开出来的话题就是它自己那个聊天;将要开出来的(主窗口里 @ 机器人)先算群的,
@@ -137906,6 +138187,7 @@ var makeLarkAdapter = (deps) => {
137906
138187
  senderId: message.senderOpenId,
137907
138188
  ...names,
137908
138189
  text: message.text,
138190
+ topicRootForSession: context.topicRootForSession(message),
137909
138191
  ...replyTo === void 0 ? {} : { replyTo },
137910
138192
  chatKind: message.chatType === "p2p" ? "private" : "group",
137911
138193
  replyInThread: message.inThread,
@@ -137960,7 +138242,8 @@ var makeLarkAdapter = (deps) => {
137960
138242
  const messageId = await sendChatText({
137961
138243
  ...target2,
137962
138244
  receiveIdType,
137963
- text: chunk
138245
+ text: chunk,
138246
+ retry: options?.retry
137964
138247
  });
137965
138248
  deps.log.info("lark delivered a message", { messageId, bot: binding.displayName });
137966
138249
  }
@@ -137974,6 +138257,9 @@ var makeLarkAdapter = (deps) => {
137974
138257
  detail: "missing app id or secret in saved config"
137975
138258
  };
137976
138259
  }
138260
+ if (!isLarkAppId(appId)) {
138261
+ return { stage: "credentials", ok: false, detail: "malformed app id in saved config" };
138262
+ }
137977
138263
  try {
137978
138264
  const { expiresInSeconds } = await fetchTenantToken({ http: deps.http, appId, appSecret });
137979
138265
  return {
@@ -138295,7 +138581,7 @@ var en_default = {
138295
138581
  },
138296
138582
  wecomAppListenPort: {
138297
138583
  label: "Local callback port (optional)",
138298
- description: "Defaults to 30987. Self-built apps listening on the same address share one port.",
138584
+ description: "Defaults to 30987. Self-built apps listening on the same address share one port; rows with different listen addresses (say 0.0.0.0 and 127.0.0.1) need different ports, or the second one fails to listen.",
138299
138585
  placeholder: "30987"
138300
138586
  },
138301
138587
  wecomAppListenHost: {
@@ -138313,6 +138599,7 @@ var en_default = {
138313
138599
  }
138314
138600
  },
138315
138601
  reply: {
138602
+ wecomDeliveryFailed: "This reply didn't reach {{recipient}} in WeCom: they are outside this app's visible range. Add them to the app's visible range in the WeCom admin console, then ask the agent to send it again.",
138316
138603
  conclusionRecovered: "(Finished while restarting; delivered now)",
138317
138604
  notAllowed: "You are not on this bot's allow list, so this tap had no effect.",
138318
138605
  noDefaultModel: 'Project "{{title}}" has no agent selected yet, so a thread cannot be started. Send /model to pick one, or set a default in the project settings, then message again.',
@@ -138668,13 +138955,16 @@ var en_default = {
138668
138955
  description: "Verify platform credentials still work; does not open a session or call a model.",
138669
138956
  running: "Checking {{rowId}}\u2026",
138670
138957
  done: "Connection OK",
138671
- failed: "Connection failed (ref {{ref}})",
138958
+ failed: "Connection failed",
138672
138959
  notSaved: "Save this row first (credentials are read from saved config only).",
138673
138960
  hint: {
138674
- badToken: "This does not look like a bot token. Copy it again from BotFather: digits, a colon, then the key (ref {{ref}})",
138675
- unauthorized: "Telegram does not accept this token. It may have been revoked; get the current one from BotFather (ref {{ref}})",
138676
- notBot: "This token does not belong to a bot. Use the token BotFather gave you (ref {{ref}})",
138677
- webhookActive: "This bot has a webhook set by another service, so it cannot receive messages here. Remove the webhook there, then check again (ref {{ref}})"
138961
+ badToken: "This does not look like a bot token. Copy it again from BotFather: digits, a colon, then the key.",
138962
+ unauthorized: "Telegram does not accept this token. It may have been revoked; get the current one from BotFather.",
138963
+ notBot: "This token does not belong to a bot. Use the token BotFather gave you.",
138964
+ webhookActive: "This bot has a webhook set by another service, so it cannot receive messages here. Remove the webhook there, then check again.",
138965
+ wecomTrustedIp: "Add this machine's outbound public IP to the app's trusted IP list in the WeCom admin console.",
138966
+ wecomInvalidUser: "Check that the recipient, department or tag exists and is in this app's visible range.",
138967
+ wecomInvalidAgent: "Check the AgentId against this self-built application in the WeCom admin console."
138678
138968
  }
138679
138969
  },
138680
138970
  copyDiagnostics: {
@@ -138682,24 +138972,24 @@ var en_default = {
138682
138972
  description: "Run a connection probe and produce copy-paste diagnostics (no secrets).",
138683
138973
  running: "Building diagnostics for {{rowId}}\u2026",
138684
138974
  done: "Diagnostics ready \u2014 see details below",
138685
- failed: "Diagnostics failed (ref {{ref}})",
138975
+ failed: "Diagnostics failed",
138686
138976
  notSaved: "Save this row first before copying diagnostics."
138687
138977
  },
138688
138978
  diagnostic: {
138689
- ref: "Ref {{ref}}\n{{detail}}"
138979
+ ref: "Reference: {{ref}}\n{{detail}}"
138690
138980
  },
138691
138981
  larkRepair: {
138692
138982
  label: "Repair scopes",
138693
- description: "Verify the Lark app can still mint a token and list scope / publish steps.",
138983
+ description: "Check whether the saved credentials can mint a token; scopes, event subscriptions and publishing are not checked automatically.",
138694
138984
  step: {
138695
- scopes: "Confirm message / resource / panel scopes",
138696
- events: "Confirm long connection and event subscriptions",
138697
- publish: "Publish if scopes were added",
138985
+ scopes: "Scopes \u2014 not checked",
138986
+ events: "Event subscriptions \u2014 not checked",
138987
+ publish: "Publishing \u2014 not checked",
138698
138988
  verify: "Verify the app can still mint a tenant token"
138699
138989
  },
138700
- done: "Credentials work; add any missing scopes in the Feishu console and publish",
138701
- hint: "Ref {{ref}}\n{{detail}}\nIf scopes are missing, request them on the Feishu open platform and publish; a chat /repair command will follow.",
138702
- failed: "Could not verify the Lark app (ref {{ref}})",
138990
+ done: "Only token issuance was verified. Scopes, event subscriptions and publishing were not checked automatically; review them on the Feishu open platform.",
138991
+ hint: "Reference: {{ref}}\n{{detail}}\nReview scopes, event subscriptions and publishing on the Feishu open platform; none were checked automatically.",
138992
+ failed: "Could not mint a token with the saved credentials. Scopes, event subscriptions and publishing were not checked automatically; review them on the Feishu open platform.",
138703
138993
  notLark: "This row is not a saved Lark bot."
138704
138994
  }
138705
138995
  },
@@ -138738,7 +139028,23 @@ var en_default = {
138738
139028
  removeFailed: "Couldn't delete this bot.",
138739
139029
  status: {
138740
139030
  ready: "Ready",
138741
- incomplete: "Incomplete"
139031
+ incomplete: "Incomplete",
139032
+ connecting: "Connecting",
139033
+ connected: "Connected",
139034
+ notConnected: "Not connected"
139035
+ },
139036
+ connection: {
139037
+ failed: "The last connection attempt failed.",
139038
+ details: "Details: {{error}}",
139039
+ reason: {
139040
+ "not-available": "This platform can't be connected yet.",
139041
+ "invalid-app-id": "The App ID isn't in the right format.",
139042
+ disconnected: "The connection to the platform is down.",
139043
+ "subscription-failed": "The platform didn't confirm the subscription.",
139044
+ "token-unavailable": "Couldn't get an access token.",
139045
+ "credentials-rejected": "The platform rejected the credentials."
139046
+ },
139047
+ duplicate: "Uses the same credentials as \u201C{{name}}\u201D. Only that row connects."
138742
139048
  },
138743
139049
  secret: {
138744
139050
  saved: "Saved. Leave blank to keep it.",
@@ -138977,7 +139283,7 @@ var zh_CN_default = {
138977
139283
  },
138978
139284
  wecomAppListenPort: {
138979
139285
  label: "\u672C\u673A\u56DE\u8C03\u7AEF\u53E3\uFF08\u53EF\u9009\uFF09",
138980
- description: "\u9ED8\u8BA4 30987\u3002\u76D1\u542C\u5730\u5740\u76F8\u540C\u7684\u81EA\u5EFA\u5E94\u7528\u5171\u7528\u4E00\u4E2A\u7AEF\u53E3\u3002",
139286
+ description: "\u9ED8\u8BA4 30987\u3002\u76D1\u542C\u5730\u5740\u76F8\u540C\u7684\u81EA\u5EFA\u5E94\u7528\u5171\u7528\u4E00\u4E2A\u7AEF\u53E3\uFF1B\u76D1\u542C\u5730\u5740\u4E0D\u540C\u7684\u4E24\u884C\uFF08\u6BD4\u5982 0.0.0.0 \u548C 127.0.0.1\uFF09\u8981\u7528\u4E0D\u540C\u7AEF\u53E3\uFF0C\u5426\u5219\u540E\u4E00\u4E2A\u76D1\u542C\u4E0D\u8D77\u6765\u3002",
138981
139287
  placeholder: "30987"
138982
139288
  },
138983
139289
  wecomAppListenHost: {
@@ -138995,6 +139301,7 @@ var zh_CN_default = {
138995
139301
  }
138996
139302
  },
138997
139303
  reply: {
139304
+ wecomDeliveryFailed: "\u8FD9\u6761\u56DE\u590D\u6CA1\u9001\u5230\u4F01\u4E1A\u5FAE\u4FE1\u91CC\u7684 {{recipient}}\uFF1A\u5BF9\u65B9\u4E0D\u5728\u8FD9\u4E2A\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\u3002\u5230\u4F01\u4E1A\u5FAE\u4FE1\u7BA1\u7406\u540E\u53F0\u628A\u4ED6\u4EEC\u52A0\u8FDB\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\uFF0C\u518D\u8BA9 agent \u91CD\u53D1\u4E00\u6B21\u3002",
138998
139305
  conclusionRecovered: "\uFF08\u91CD\u542F\u671F\u95F4\u5B8C\u6210\uFF0C\u73B0\u5728\u8865\u53D1\uFF09",
138999
139306
  notAllowed: "\u4F60\u4E0D\u5728\u8FD9\u4E2A\u673A\u5668\u4EBA\u7684\u5141\u8BB8\u540D\u5355\u91CC\uFF0C\u8FD9\u6B21\u70B9\u51FB\u6CA1\u6709\u751F\u6548\u3002",
139000
139307
  noDefaultModel: "\u9879\u76EE\u300C{{title}}\u300D\u8FD8\u6CA1\u9009 agent\uFF0C\u5F00\u4E0D\u4E86\u4F1A\u8BDD\u3002\u53D1 /model \u9009\u4E00\u4E2A\uFF0C\u6216\u8005\u5230\u9879\u76EE\u8BBE\u7F6E\u91CC\u8BBE\u4E2A\u9ED8\u8BA4\uFF0C\u518D\u53D1\u4E00\u6B21\u6D88\u606F\u3002",
@@ -139350,13 +139657,16 @@ var zh_CN_default = {
139350
139657
  description: "\u9A8C\u8BC1\u5E73\u53F0\u51ED\u636E\u662F\u5426\u4ECD\u53EF\u7528\uFF1B\u4E0D\u4F1A\u5F00\u4F1A\u8BDD\uFF0C\u4E5F\u4E0D\u4F1A\u8C03\u6A21\u578B\u3002",
139351
139658
  running: "\u6B63\u5728\u68C0\u67E5 {{rowId}} \u2026",
139352
139659
  done: "\u8FDE\u63A5\u6B63\u5E38",
139353
- failed: "\u8FDE\u63A5\u5931\u8D25\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139660
+ failed: "\u8FDE\u63A5\u5931\u8D25",
139354
139661
  notSaved: "\u8BF7\u5148\u4FDD\u5B58\u8FD9\u4E00\u884C\u518D\u68C0\u67E5\u8FDE\u63A5\uFF08\u51ED\u636E\u53EA\u4ECE\u5DF2\u4FDD\u5B58\u914D\u7F6E\u8BFB\u53D6\uFF09\u3002",
139355
139662
  hint: {
139356
- badToken: "\u8FD9\u4E0D\u50CF\u673A\u5668\u4EBA\u7684 token\u3002\u5230 BotFather \u91CD\u65B0\u590D\u5236\u4E00\u4EFD\uFF1A\u6570\u5B57\u3001\u5192\u53F7\uFF0C\u540E\u9762\u662F\u5BC6\u94A5\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139357
- unauthorized: "Telegram \u4E0D\u8BA4\u8FD9\u4E2A token\uFF0C\u53EF\u80FD\u5DF2\u7ECF\u4F5C\u5E9F\u3002\u5230 BotFather \u53D6\u73B0\u5728\u7684\u90A3\u4E00\u4EFD\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139358
- notBot: "\u8FD9\u4E2A token \u4E0D\u662F\u673A\u5668\u4EBA\u7684\u3002\u8BF7\u7528 BotFather \u7ED9\u7684\u90A3\u4E00\u4EFD\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139359
- webhookActive: "\u8FD9\u4E2A\u673A\u5668\u4EBA\u88AB\u522B\u7684\u670D\u52A1\u8BBE\u4E86 webhook\uFF0C\u5728\u8FD9\u91CC\u6536\u4E0D\u5230\u6D88\u606F\u3002\u5148\u5230\u90A3\u8FB9\u64A4\u6389 webhook\uFF0C\u518D\u68C0\u67E5\u4E00\u6B21\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09"
139663
+ badToken: "\u8FD9\u4E0D\u50CF\u673A\u5668\u4EBA\u7684 token\u3002\u5230 BotFather \u91CD\u65B0\u590D\u5236\u4E00\u4EFD\uFF1A\u6570\u5B57\u3001\u5192\u53F7\uFF0C\u540E\u9762\u662F\u5BC6\u94A5\u3002",
139664
+ unauthorized: "Telegram \u4E0D\u8BA4\u8FD9\u4E2A token\uFF0C\u53EF\u80FD\u5DF2\u7ECF\u4F5C\u5E9F\u3002\u5230 BotFather \u53D6\u73B0\u5728\u7684\u90A3\u4E00\u4EFD\u3002",
139665
+ notBot: "\u8FD9\u4E2A token \u4E0D\u662F\u673A\u5668\u4EBA\u7684\u3002\u8BF7\u7528 BotFather \u7ED9\u7684\u90A3\u4E00\u4EFD\u3002",
139666
+ webhookActive: "\u8FD9\u4E2A\u673A\u5668\u4EBA\u88AB\u522B\u7684\u670D\u52A1\u8BBE\u4E86 webhook\uFF0C\u5728\u8FD9\u91CC\u6536\u4E0D\u5230\u6D88\u606F\u3002\u5148\u5230\u90A3\u8FB9\u64A4\u6389 webhook\uFF0C\u518D\u68C0\u67E5\u4E00\u6B21\u3002",
139667
+ wecomTrustedIp: "\u628A\u8FD9\u53F0\u673A\u5668\u7684\u51FA\u7AD9\u516C\u7F51 IP \u52A0\u8FDB\u4F01\u4E1A\u5FAE\u4FE1\u540E\u53F0\u8FD9\u4E2A\u5E94\u7528\u7684\u4F01\u4E1A\u53EF\u4FE1 IP\u3002",
139668
+ wecomInvalidUser: "\u786E\u8BA4\u6536\u4EF6\u4EBA\u3001\u90E8\u95E8\u6216\u6807\u7B7E\u5B58\u5728\uFF0C\u5E76\u5728\u8FD9\u4E2A\u81EA\u5EFA\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\u5185\u3002",
139669
+ wecomInvalidAgent: "\u6838\u5BF9\u4F01\u4E1A\u5FAE\u4FE1\u540E\u53F0\u8FD9\u4E2A\u81EA\u5EFA\u5E94\u7528\u7684 AgentId\u3002"
139360
139670
  }
139361
139671
  },
139362
139672
  copyDiagnostics: {
@@ -139364,24 +139674,24 @@ var zh_CN_default = {
139364
139674
  description: "\u8DD1\u4E00\u6B21\u8FDE\u63A5\u63A2\u6D4B\uFF0C\u751F\u6210\u53EF\u590D\u5236\u7684\u8BCA\u65AD\u6587\u672C\uFF08\u4E0D\u542B\u5BC6\u94A5\uFF09\u3002",
139365
139675
  running: "\u6B63\u5728\u751F\u6210\u8BCA\u65AD {{rowId}} \u2026",
139366
139676
  done: "\u8BCA\u65AD\u5DF2\u751F\u6210\uFF0C\u89C1\u4E0B\u65B9\u8BE6\u60C5",
139367
- failed: "\u8BCA\u65AD\u5931\u8D25\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139677
+ failed: "\u8BCA\u65AD\u5931\u8D25",
139368
139678
  notSaved: "\u8BF7\u5148\u4FDD\u5B58\u8FD9\u4E00\u884C\u518D\u590D\u5236\u8BCA\u65AD\u3002"
139369
139679
  },
139370
139680
  diagnostic: {
139371
- ref: "\u53C2\u8003\u53F7 {{ref}}\n{{detail}}"
139681
+ ref: "\u53C2\u8003\u53F7\uFF1A{{ref}}\n{{detail}}"
139372
139682
  },
139373
139683
  larkRepair: {
139374
139684
  label: "\u8865\u5168\u6743\u9650",
139375
- description: "\u6838\u5BF9\u98DE\u4E66\u5E94\u7528\u4ECD\u80FD\u6362 token\uFF0C\u5E76\u63D0\u793A\u9700\u8981\u8865\u7684\u6743\u9650\u4E0E\u53D1\u5E03\u6B65\u9AA4\u3002",
139685
+ description: "\u68C0\u67E5\u5DF2\u4FDD\u5B58\u7684\u51ED\u636E\u80FD\u5426\u6362\u53D6 token\uFF1B\u6743\u9650\u3001\u4E8B\u4EF6\u8BA2\u9605\u548C\u53D1\u5E03\u4E0D\u4F1A\u81EA\u52A8\u68C0\u67E5\u3002",
139376
139686
  step: {
139377
- scopes: "\u786E\u8BA4\u6D88\u606F / \u8D44\u6E90 / \u9762\u677F\u7B49\u6743\u9650",
139378
- events: "\u786E\u8BA4\u957F\u8FDE\u63A5\u4E0E\u4E8B\u4EF6\u8BA2\u9605",
139379
- publish: "\u5982\u6709\u589E\u91CF\u6743\u9650\uFF0C\u5B8C\u6210\u53D1\u5E03\u5BA1\u6279",
139687
+ scopes: "\u6743\u9650\uFF1A\u672A\u68C0\u67E5",
139688
+ events: "\u4E8B\u4EF6\u8BA2\u9605\uFF1A\u672A\u68C0\u67E5",
139689
+ publish: "\u53D1\u5E03\uFF1A\u672A\u68C0\u67E5",
139380
139690
  verify: "\u9A8C\u8BC1\u5E94\u7528\u4ECD\u53EF\u6362\u53D6 tenant token"
139381
139691
  },
139382
- done: "\u51ED\u636E\u53EF\u7528\uFF1B\u82E5\u6743\u9650\u4E0D\u8DB3\u8BF7\u6309\u98DE\u4E66\u63A7\u5236\u53F0\u8865\u9F50\u540E\u53D1\u5E03",
139383
- hint: "\u53C2\u8003\u53F7 {{ref}}\n{{detail}}\n\u7F3A\u6743\u9650\u65F6\u53EF\u5728\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u5BF9\u8BE5\u5E94\u7528\u589E\u91CF\u7533\u8BF7\u5E76\u53D1\u5E03\uFF1B\u804A\u5929\u4FA7 /repair \u547D\u4EE4\u540E\u7EED\u8865\u9F50\u3002",
139384
- failed: "\u65E0\u6CD5\u9A8C\u8BC1\u98DE\u4E66\u5E94\u7528\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139692
+ done: "\u4EC5\u6838\u5B9E\u4E86\u51ED\u636E\u4ECD\u80FD\u6362\u53D6 token\uFF1B\u6743\u9650\u3001\u4E8B\u4EF6\u8BA2\u9605\u548C\u53D1\u5E03\u6CA1\u6709\u81EA\u52A8\u68C0\u67E5\uFF0C\u8BF7\u5230\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u6838\u5BF9\u3002",
139693
+ hint: "\u53C2\u8003\u53F7\uFF1A{{ref}}\n{{detail}}\n\u8BF7\u5230\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u6838\u5BF9\u6743\u9650\u3001\u4E8B\u4EF6\u8BA2\u9605\u548C\u53D1\u5E03\uFF1B\u8FD9\u4E09\u9879\u5747\u672A\u81EA\u52A8\u68C0\u67E5\u3002",
139694
+ failed: "\u5DF2\u4FDD\u5B58\u7684\u51ED\u636E\u672A\u80FD\u6362\u53D6 token\uFF1B\u6743\u9650\u3001\u4E8B\u4EF6\u8BA2\u9605\u548C\u53D1\u5E03\u6CA1\u6709\u81EA\u52A8\u68C0\u67E5\uFF0C\u8BF7\u5230\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u6838\u5BF9\u3002",
139385
139695
  notLark: "\u8FD9\u4E00\u884C\u4E0D\u662F\u5DF2\u4FDD\u5B58\u7684\u98DE\u4E66\u673A\u5668\u4EBA\u3002"
139386
139696
  }
139387
139697
  },
@@ -139419,8 +139729,24 @@ var zh_CN_default = {
139419
139729
  copyFailed: "\u6CA1\u590D\u5236\u4E0A\u3002",
139420
139730
  removeFailed: "\u6CA1\u80FD\u5220\u6389\u8FD9\u4E2A\u673A\u5668\u4EBA\u3002",
139421
139731
  status: {
139422
- ready: "\u53EF\u7528",
139423
- incomplete: "\u8FD8\u6CA1\u586B\u5B8C"
139732
+ ready: "\u5C31\u7EEA",
139733
+ incomplete: "\u5F85\u8865\u5168",
139734
+ connecting: "\u8FDE\u63A5\u4E2D",
139735
+ connected: "\u5DF2\u8FDE\u63A5",
139736
+ notConnected: "\u672A\u8FDE\u63A5"
139737
+ },
139738
+ connection: {
139739
+ failed: "\u4E0A\u6B21\u8FDE\u63A5\u6CA1\u6709\u6210\u529F\u3002",
139740
+ details: "\u8BE6\u60C5\uFF1A{{error}}",
139741
+ reason: {
139742
+ "not-available": "\u8FD9\u4E2A\u5E73\u53F0\u8FD8\u4E0D\u80FD\u8FDE\u63A5\u3002",
139743
+ "invalid-app-id": "App ID \u683C\u5F0F\u4E0D\u6B63\u786E\u3002",
139744
+ disconnected: "\u548C\u5E73\u53F0\u7684\u8FDE\u63A5\u65AD\u4E86\u3002",
139745
+ "subscription-failed": "\u5E73\u53F0\u6CA1\u6709\u786E\u8BA4\u8BA2\u9605\u3002",
139746
+ "token-unavailable": "\u6CA1\u80FD\u6362\u5230\u8BBF\u95EE\u4EE4\u724C\u3002",
139747
+ "credentials-rejected": "\u5E73\u53F0\u62D2\u7EDD\u4E86\u51ED\u636E\u3002"
139748
+ },
139749
+ duplicate: "\u548C\u300C{{name}}\u300D\u7528\u7684\u662F\u540C\u4E00\u4EFD\u51ED\u636E\uFF0C\u53EA\u8FDE\u90A3\u4E00\u884C\u3002"
139424
139750
  },
139425
139751
  secret: {
139426
139752
  saved: "\u5DF2\u7ECF\u5B58\u8FC7\uFF0C\u7559\u7A7A\u5C31\u4FDD\u6301\u539F\u503C\u3002",
@@ -139601,6 +139927,16 @@ var makeModelPlan = (core, deps) => {
139601
139927
  // src/notifications.ts
139602
139928
  import { readThreadFailed, threadOrNull as threadOrNull5 } from "@ycodium-ai/plugin-api";
139603
139929
 
139930
+ // src/conclusionOutbox.ts
139931
+ var enqueueConclusion = (core, ledger, binding, debt, send) => core.outbound(
139932
+ chatKeyOf(binding),
139933
+ async () => {
139934
+ if (debt === void 0) await send();
139935
+ else await ledger.deliver(debt, send);
139936
+ },
139937
+ { droppable: debt !== void 0 }
139938
+ );
139939
+
139604
139940
  // src/conclusionRecovery.ts
139605
139941
  var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
139606
139942
  var recoverConclusions = async (core, ledger, delivery, failures) => {
@@ -139608,6 +139944,12 @@ var recoverConclusions = async (core, ledger, delivery, failures) => {
139608
139944
  core.host.onDispose(() => {
139609
139945
  disposed = true;
139610
139946
  });
139947
+ const couldNotRecover = (debt, error) => {
139948
+ core.host.log.warn("chat could not recover a conclusion", {
139949
+ ...debt,
139950
+ error: describeError(error)
139951
+ });
139952
+ };
139611
139953
  await core.ready;
139612
139954
  for (const debt of Object.values(conclusionDebtsOf(core.state))) {
139613
139955
  if (disposed) return;
@@ -139634,24 +139976,27 @@ var recoverConclusions = async (core, ledger, delivery, failures) => {
139634
139976
  }
139635
139977
  const binding = core.bindingForChatKey(debt.chatKey);
139636
139978
  if (binding === void 0) continue;
139637
- await ledger.deliver(debt, async () => {
139638
- if (latest.state === "error") {
139639
- return delivery.failure(
139640
- binding,
139641
- debt.threadId,
139642
- failures.forFailure(debt.threadId, execution.session?.lastError),
139643
- true,
139644
- latest.turnId
139645
- );
139979
+ enqueueConclusion(core, ledger, binding, debt, async (owed) => {
139980
+ try {
139981
+ if (latest.state === "error") {
139982
+ return delivery.sendText(
139983
+ binding,
139984
+ debt.threadId,
139985
+ failures.forFailure(debt.threadId, execution.session?.lastError),
139986
+ true,
139987
+ latest.turnId,
139988
+ owed
139989
+ );
139990
+ }
139991
+ const send = await delivery.prepareConclusion(debt.threadId, latest.turnId);
139992
+ return send(binding, { recovered: true, streaming: false, owed });
139993
+ } catch (error) {
139994
+ couldNotRecover(debt, error);
139995
+ return false;
139646
139996
  }
139647
- const send = await delivery.prepareConclusion(debt.threadId, latest.turnId);
139648
- return send(binding, { recovered: true, streaming: false });
139649
139997
  });
139650
139998
  } catch (error) {
139651
- core.host.log.warn("chat could not recover a conclusion", {
139652
- ...debt,
139653
- error: describeError(error)
139654
- });
139999
+ couldNotRecover(debt, error);
139655
140000
  }
139656
140001
  }
139657
140002
  };
@@ -139674,7 +140019,7 @@ var outcomeOfSession = (status) => {
139674
140019
  var THREAD_PROJECT_CACHE_LIMIT = 256;
139675
140020
  var makeNotifications = (core, deps) => {
139676
140021
  const { host, adapterFor, bindingForChatKey, ready } = core;
139677
- const { clearWorking } = deps;
140022
+ const { takeWorking } = deps;
139678
140023
  const { turnCards } = deps.replyCards;
139679
140024
  const { failures, conclusions, delivery } = deps;
139680
140025
  void recoverConclusions(core, conclusions, delivery, failures);
@@ -139697,6 +140042,11 @@ var makeNotifications = (core, deps) => {
139697
140042
  });
139698
140043
  const streamsTo = (binding, threadId) => adapterFor(binding.platform)?.turnCards !== void 0 && owns(binding, threadId);
139699
140044
  const owns = (binding, threadId) => chatOfThread(core.state, threadId) === chatKeyOf(binding);
140045
+ const later = (binding, work) => {
140046
+ core.outbound(chatKeyOf(binding), async () => {
140047
+ await work();
140048
+ });
140049
+ };
139700
140050
  const threadsWithSomethingToReport = /* @__PURE__ */ new Set();
139701
140051
  host.reactor({
139702
140052
  name: "im.assistant-messages",
@@ -139719,32 +140069,38 @@ var makeNotifications = (core, deps) => {
139719
140069
  const said = threadsWithSomethingToReport.delete(threadId);
139720
140070
  await ready;
139721
140071
  const targets = targetsForProject(threadId, await projectOfThread(threadId));
139722
- const owes = Object.values(conclusionDebtsOf(core.state)).some(
139723
- (debt2) => debt2.threadId === threadId
139724
- );
139725
- const { debt, handled, execution } = said || owes ? await conclusions.forTurn(threadId) : { debt: void 0, handled: false, execution: null };
140072
+ const { debt, handled, execution } = await conclusions.forTurn(threadId);
139726
140073
  if (debt !== void 0) {
139727
140074
  const binding = bindingForChatKey(debt.chatKey);
139728
140075
  if (binding !== void 0 && !streamsTo(binding, threadId) && execution?.session?.activeTurnId == null && execution?.latestTurn?.state !== "running" && execution?.latestTurn?.state !== "error" && payload.session?.status !== "error") {
139729
- await conclusions.deliver(debt, async () => {
139730
- const send = await delivery.prepareConclusion(
139731
- threadId,
139732
- execution?.latestTurn?.turnId ?? null
139733
- );
139734
- return send(binding, { recovered: false, streaming: false });
140076
+ const turnId = execution?.latestTurn?.turnId ?? null;
140077
+ enqueueConclusion(core, conclusions, binding, debt, async (owed) => {
140078
+ const send = await delivery.prepareConclusion(threadId, turnId);
140079
+ return send(binding, { recovered: false, streaming: false, owed });
139735
140080
  });
139736
140081
  }
139737
140082
  } else if (said && !handled) {
139738
140083
  const send = await delivery.prepareConclusion(threadId);
139739
140084
  for (const binding of targets) {
139740
- await send(binding, {
139741
- recovered: false,
139742
- streaming: streamsTo(binding, threadId)
139743
- });
140085
+ const streaming = streamsTo(binding, threadId);
140086
+ enqueueConclusion(
140087
+ core,
140088
+ conclusions,
140089
+ binding,
140090
+ void 0,
140091
+ () => send(binding, { recovered: false, streaming })
140092
+ );
139744
140093
  }
139745
140094
  }
139746
140095
  const outcome = outcomeOfSession(payload.session?.status);
139747
- for (const binding of targets) await clearWorking(binding, threadId, outcome);
140096
+ for (const binding of targets) {
140097
+ const withdraw = takeWorking(binding, threadId, outcome);
140098
+ turnCards.afterTurn(
140099
+ threadId,
140100
+ () => later(binding, withdraw),
140101
+ execution?.latestTurn?.turnId ?? null
140102
+ );
140103
+ }
139748
140104
  }
139749
140105
  });
139750
140106
  host.events.on("thread.failed", async (event) => {
@@ -139753,7 +140109,7 @@ var makeNotifications = (core, deps) => {
139753
140109
  threadsWithSomethingToReport.delete(threadId);
139754
140110
  await ready;
139755
140111
  const targets = targetsForProject(threadId, await projectOfThread(threadId));
139756
- const { debt, handled } = await conclusions.forTurn(threadId);
140112
+ const { debt, handled, execution } = await conclusions.forTurn(threadId);
139757
140113
  const debtBinding = debt === void 0 ? void 0 : bindingForChatKey(debt.chatKey);
139758
140114
  const recipients = debt !== void 0 ? debtBinding === void 0 ? [] : [debtBinding] : targets;
139759
140115
  const textTargets = handled ? [] : recipients.filter(
@@ -139763,16 +140119,20 @@ var makeNotifications = (core, deps) => {
139763
140119
  const notice = textTargets.length > 0 || drawingTurn !== null ? failures.forFailure(threadId, readThreadFailed(event)?.detail) : null;
139764
140120
  if (notice !== null && drawingTurn !== null)
139765
140121
  failures.holdForCard(threadId, drawingTurn, notice);
139766
- for (const binding of targets) {
139767
- await clearWorking(binding, threadId, "failed");
139768
- }
139769
140122
  if (notice !== null) {
139770
140123
  for (const binding of textTargets) {
139771
- const send = () => delivery.failure(binding, threadId, notice, false);
139772
- if (debt !== void 0) await conclusions.deliver(debt, send);
139773
- else await send();
140124
+ const send = (owed) => delivery.sendText(binding, threadId, notice, false, null, owed);
140125
+ enqueueConclusion(core, conclusions, binding, debt, send);
139774
140126
  }
139775
140127
  }
140128
+ for (const binding of targets) {
140129
+ const withdraw = takeWorking(binding, threadId, "failed");
140130
+ turnCards.afterTurn(
140131
+ threadId,
140132
+ () => later(binding, withdraw),
140133
+ execution?.latestTurn?.turnId ?? null
140134
+ );
140135
+ }
139776
140136
  });
139777
140137
  const rememberThreadProject = (threadId, projectId) => {
139778
140138
  projectByThread.set(threadId, projectId);
@@ -140333,32 +140693,6 @@ var makeProjectRows = (core) => {
140333
140693
  return { projectRowsFor, listedProjectsByChat };
140334
140694
  };
140335
140695
 
140336
- // src/diagnostics.ts
140337
- var PREFIX = {
140338
- conn: "IM-CONN",
140339
- msg: "IM-MSG",
140340
- setup: "IM-SETUP"
140341
- };
140342
- var shortSalt = () => {
140343
- const bytes = new Uint8Array(2);
140344
- crypto.getRandomValues(bytes);
140345
- return Array.from(bytes, (b) => b.toString(16).toUpperCase().padStart(2, "0")).join("");
140346
- };
140347
- var diagnosticRef = (kind, nowMs = Date.now()) => {
140348
- const stamp = nowMs.toString(36).toUpperCase();
140349
- return `${PREFIX[kind]}-${stamp}-${shortSalt()}`;
140350
- };
140351
- var formatDiagnosticCopy = (report) => [
140352
- `ref=${report.ref}`,
140353
- `ok=${report.ok ? "yes" : "no"}`,
140354
- `platform=${report.platform}`,
140355
- `bot=${report.botName}`,
140356
- `row=${report.rowId}`,
140357
- `stage=${report.stage}`,
140358
- `at=${report.at}`,
140359
- `detail=${report.detail}`
140360
- ].join("\n");
140361
-
140362
140696
  // src/failureNotices.ts
140363
140697
  var makeFailureNotices = (core) => {
140364
140698
  const heldForCard = /* @__PURE__ */ new Map();
@@ -140450,20 +140784,12 @@ var trailingReply = (messages) => {
140450
140784
  var ROTATE = "rotate";
140451
140785
  var isDrawing = (live) => live.card !== null || live.reopen;
140452
140786
  var RECENT_ENDED_LIMIT = 256;
140787
+ var turnKey = (threadId, turnId) => JSON.stringify([threadId, turnId]);
140453
140788
  var makeTurnCards = (deps) => {
140454
140789
  const slots = /* @__PURE__ */ new Map();
140455
- const recentEnded = /* @__PURE__ */ new Map();
140790
+ const recentEnded = makeBoundedSeen(RECENT_ENDED_LIMIT);
140456
140791
  const erroredThreads = /* @__PURE__ */ new Set();
140457
140792
  let disposed = false;
140458
- const rememberEnded = (threadId, turnId) => {
140459
- if (turnId === null) return;
140460
- recentEnded.delete(threadId);
140461
- recentEnded.set(threadId, turnId);
140462
- if (recentEnded.size > RECENT_ENDED_LIMIT) {
140463
- const oldest = recentEnded.keys().next().value;
140464
- if (oldest !== void 0) recentEnded.delete(oldest);
140465
- }
140466
- };
140467
140793
  const readTexts = async (live, progress) => {
140468
140794
  const texts = /* @__PURE__ */ new Map();
140469
140795
  for (const segment of progress.segments) {
@@ -140515,29 +140841,28 @@ var makeTurnCards = (deps) => {
140515
140841
  const turnId = progress.turnId ?? live?.turnId ?? null;
140516
140842
  const target2 = live?.target ?? deps.resolveTarget(threadId, turnId);
140517
140843
  if (target2 === null) return;
140518
- rememberEnded(threadId, turnId);
140519
140844
  if (end.status === "error") erroredThreads.add(threadId);
140520
140845
  const title = live?.title ?? await deps.titleOf(threadId);
140521
140846
  const view = await finalView(title, progress, live, end);
140522
- if (live?.card != null) {
140523
- const card = live.card;
140524
- const sent = await card.finish(view);
140525
- deps.onClosed(card.cardId);
140526
- if (deps.finalize !== void 0)
140527
- await deps.finalize(threadId, turnId, target2.chatKey, async () => sent);
140528
- return;
140529
- }
140530
- const send = async () => {
140531
- const card = await openCard(target2, view);
140532
- return card !== null || await deps.fallback(threadId, turnId, target2.chatKey, view);
140533
- };
140534
- if (deps.finalize === void 0) await send();
140535
- else await deps.finalize(threadId, turnId, target2.chatKey, send);
140847
+ const card = live?.card ?? null;
140848
+ const finished = card !== null && await card.finish(view);
140849
+ if (card !== null) deps.onClosed(card.cardId);
140850
+ const send = async () => finished || card === null && await openCard(target2, view) !== null;
140851
+ const sent = deps.finalize === void 0 ? await send() : await deps.finalize(threadId, turnId, target2.chatKey, send);
140852
+ if (!sent) await deps.fallback(threadId, turnId, target2.chatKey, view);
140853
+ if (turnId !== null) recentEnded.claim(turnKey(threadId, turnId));
140854
+ const callbacks = slot.afterTurn.get(turnId) ?? [];
140855
+ slot.afterTurn.delete(turnId);
140856
+ if (turnId !== null) {
140857
+ callbacks.push(...slot.afterTurn.get(null) ?? []);
140858
+ slot.afterTurn.delete(null);
140859
+ }
140860
+ for (const after of callbacks) after();
140536
140861
  };
140537
140862
  const handle = async (slot, threadId, item) => {
140538
140863
  await deps.ready;
140539
140864
  if (disposed) return;
140540
- if (item.turnId !== null && recentEnded.get(threadId) === item.turnId) return;
140865
+ if (item.turnId !== null && recentEnded.has(turnKey(threadId, item.turnId))) return;
140541
140866
  let live = slot.live;
140542
140867
  if (live !== null) {
140543
140868
  const sameTurn = item.turnId === null || live.turnId === null || live.turnId === item.turnId;
@@ -140621,7 +140946,11 @@ var makeTurnCards = (deps) => {
140621
140946
  }
140622
140947
  } finally {
140623
140948
  slot.running = false;
140624
- if (slot.live === null && slot.queue.length === 0) slots.delete(threadId);
140949
+ if (slot.live === null && slot.queue.length === 0) {
140950
+ slots.delete(threadId);
140951
+ for (const callbacks of slot.afterTurn.values()) for (const after of callbacks) after();
140952
+ slot.afterTurn.clear();
140953
+ }
140625
140954
  }
140626
140955
  };
140627
140956
  return {
@@ -140630,7 +140959,7 @@ var makeTurnCards = (deps) => {
140630
140959
  if (disposed) return;
140631
140960
  let slot = slots.get(threadId);
140632
140961
  if (slot === void 0) {
140633
- slot = { queue: [], running: false, live: null };
140962
+ slot = { queue: [], running: false, live: null, afterTurn: /* @__PURE__ */ new Map() };
140634
140963
  slots.set(threadId, slot);
140635
140964
  }
140636
140965
  const last = slot.queue.at(-1);
@@ -140644,6 +140973,18 @@ var makeTurnCards = (deps) => {
140644
140973
  void pump(threadId, slot);
140645
140974
  }
140646
140975
  },
140976
+ /** 撤回执只等所属轮次收尾;没有轮次 id 时仍等当前线程槽,不能在出站队列里等自己。 */
140977
+ afterTurn: (threadId, work, turnId = null) => {
140978
+ if (disposed) return;
140979
+ const slot = slots.get(threadId);
140980
+ if (slot === void 0 || turnId !== null && recentEnded.has(turnKey(threadId, turnId)))
140981
+ work();
140982
+ else {
140983
+ const callbacks = slot.afterTurn.get(turnId) ?? [];
140984
+ callbacks.push(work);
140985
+ slot.afterTurn.set(turnId, callbacks);
140986
+ }
140987
+ },
140647
140988
  /**
140648
140989
  * 审批或提问答完了:封存这条会话开着的卡,下一拍另开一张。立刻返回;没有在画的卡、或
140649
140990
  * 平台不换卡,就什么都不做。
@@ -140670,7 +141011,10 @@ var makeTurnCards = (deps) => {
140670
141011
  /** 插件停用:放下所有在画的卡,不再发任何更新。卡会停在最后一拍,重启时被标「已中断」。 */
140671
141012
  dispose: () => {
140672
141013
  disposed = true;
140673
- for (const slot of slots.values()) slot.live?.card?.abandon();
141014
+ for (const slot of slots.values()) {
141015
+ slot.live?.card?.abandon();
141016
+ slot.afterTurn.clear();
141017
+ }
140674
141018
  slots.clear();
140675
141019
  }
140676
141020
  };
@@ -140678,7 +141022,7 @@ var makeTurnCards = (deps) => {
140678
141022
 
140679
141023
  // src/replyCards.ts
140680
141024
  var makeReplyCards = (core, failures, anchors, conclusions, delivery) => {
140681
- const { host, adapterFor, commit, deliver, bindingForChatKey, bindingOfThread, ready } = core;
141025
+ const { host, adapterFor, commit, bindingForChatKey, bindingOfThread, ready } = core;
140682
141026
  const stopRequested = /* @__PURE__ */ new Set();
140683
141027
  const turnCards = makeTurnCards({
140684
141028
  ready,
@@ -140737,22 +141081,28 @@ var makeReplyCards = (core, failures, anchors, conclusions, delivery) => {
140737
141081
  onClosed: (cardId) => commit("turn-card-closed", { cardId }),
140738
141082
  finalize: async (threadId, turnId, chatKey, send) => {
140739
141083
  const { debt, handled } = await conclusions.forTurn(threadId, turnId);
140740
- if (handled) return;
140741
- if (debt === void 0) {
140742
- await send();
140743
- return;
140744
- }
140745
- if (!await conclusions.deliver(debt, send)) return;
141084
+ if (handled) return true;
141085
+ if (debt === void 0) return send();
141086
+ if (!await conclusions.deliver(debt, send)) return false;
140746
141087
  const binding = bindingForChatKey(chatKey);
140747
- if (binding === void 0) return;
141088
+ if (binding === void 0) return true;
140748
141089
  const sendConclusion = await delivery.prepareConclusion(threadId, turnId);
140749
141090
  await sendConclusion(binding, { recovered: false, streaming: true });
141091
+ return true;
140750
141092
  },
140751
141093
  fallback: async (threadId, turnId, chatKey, view) => {
141094
+ const { debt, handled } = await conclusions.forTurn(threadId, turnId);
141095
+ if (handled) return;
140752
141096
  const binding = bindingForChatKey(chatKey);
140753
- if (binding === void 0) return false;
141097
+ if (binding === void 0) return;
140754
141098
  const text4 = [view.body, view.errorText ?? ""].filter((part) => part !== "").join("\n\n");
140755
- return deliver(await anchors.anchored(binding, threadId, turnId), text4);
141099
+ enqueueConclusion(
141100
+ core,
141101
+ conclusions,
141102
+ binding,
141103
+ debt,
141104
+ (owed) => delivery.sendText(binding, threadId, text4, false, turnId, owed)
141105
+ );
140756
141106
  },
140757
141107
  schedule: (run, delayMs) => {
140758
141108
  const timer = setTimeout(run, delayMs);
@@ -141122,7 +141472,7 @@ var makeSessions = (core) => {
141122
141472
  };
141123
141473
 
141124
141474
  // src/teamGoals.ts
141125
- import * as NodeCrypto3 from "node:crypto";
141475
+ import * as NodeCrypto4 from "node:crypto";
141126
141476
  var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
141127
141477
  var teamGoalRequestId = (binding, messageId) => {
141128
141478
  const identity = JSON.stringify([
@@ -141132,7 +141482,7 @@ var teamGoalRequestId = (binding, messageId) => {
141132
141482
  binding.chatId,
141133
141483
  messageId
141134
141484
  ]);
141135
- return NodeCrypto3.createHash("sha256").update(identity).digest("hex");
141485
+ return NodeCrypto4.createHash("sha256").update(identity).digest("hex");
141136
141486
  };
141137
141487
  var makeTeamGoals = (core) => {
141138
141488
  const { host, t, deliver, commit } = core;
@@ -141153,14 +141503,16 @@ var makeTeamGoals = (core) => {
141153
141503
  };
141154
141504
  const startTeamGoal = async (binding, message) => {
141155
141505
  try {
141506
+ const requestId = teamGoalRequestId(binding, message.messageId);
141507
+ const topicRoot = await message.topicRootForSession?.(requestId);
141156
141508
  const result = await host.plugins.invoke({
141157
141509
  pluginId: "team",
141158
141510
  command: "startGoal",
141159
141511
  payload: {
141160
141512
  projectId: binding.projectId,
141161
- goal: textWithInboundContext(message, binding.rowId, t),
141513
+ goal: textWithInboundContext(message, binding.rowId, t, topicRoot?.reference),
141162
141514
  titleSeed: message.text,
141163
- requestId: teamGoalRequestId(binding, message.messageId)
141515
+ requestId
141164
141516
  }
141165
141517
  });
141166
141518
  if (isRecord2(result) && result._tag === "TeamGoalRejection") {
@@ -141175,6 +141527,7 @@ var makeTeamGoals = (core) => {
141175
141527
  if (!isRecord2(result) || typeof result.leadThreadId !== "string" || result.leadThreadId === "" || result.leadName !== null && typeof result.leadName !== "string") {
141176
141528
  throw new Error("team startGoal returned an invalid result");
141177
141529
  }
141530
+ topicRoot?.confirmConsumed();
141178
141531
  commit("team-goal-started", { chatKey: chatKeyOf(binding), threadId: result.leadThreadId });
141179
141532
  await deliver(
141180
141533
  binding,
@@ -141230,6 +141583,10 @@ var TelegramApiError = class extends Error {
141230
141583
  this.retryAfterSeconds = retryAfterSeconds;
141231
141584
  }
141232
141585
  };
141586
+ var telegramSendVerdict = (error) => error instanceof TelegramApiError && (error.code === 429 || error.code === 503 && error.retryAfterSeconds !== void 0) ? {
141587
+ retry: true,
141588
+ afterMs: error.retryAfterSeconds === void 0 ? void 0 : error.retryAfterSeconds * 1e3
141589
+ } : NO_RETRY;
141233
141590
  var isNotModified = (error) => error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("message is not modified");
141234
141591
  var PHOTO_NAME = "photo.jpg";
141235
141592
  var PHOTO_MIME = "image/jpeg";
@@ -141436,9 +141793,13 @@ var makeTelegramApi = (input) => {
141436
141793
  },
141437
141794
  POLL_CALL_TIMEOUT_MS
141438
141795
  ),
141439
- sendMessage: async (chatId, text4) => {
141796
+ sendMessage: async (chatId, text4, retry) => {
141440
141797
  for (const chunk of splitForTelegram(text4)) {
141441
- await call("sendMessage", { ...target(chatId), text: chunk });
141798
+ await sendWithRetry(
141799
+ () => call("sendMessage", { ...target(chatId), text: chunk }),
141800
+ telegramSendVerdict,
141801
+ retry
141802
+ );
141442
141803
  }
141443
141804
  },
141444
141805
  /**
@@ -141876,6 +142237,7 @@ var makeTelegramAdapter = (deps) => {
141876
142237
  const poll = async () => {
141877
142238
  for (; ; ) {
141878
142239
  if (!running) return;
142240
+ let checkingConnection = true;
141879
142241
  try {
141880
142242
  const api = await apiFor(binding);
141881
142243
  liveApi = api;
@@ -141884,6 +142246,7 @@ var makeTelegramAdapter = (deps) => {
141884
142246
  const own = await api.getMe();
141885
142247
  botUsername = own.username ?? null;
141886
142248
  botId = own.id;
142249
+ if (running) handlers.onConnectionState?.({ state: "connected" });
141887
142250
  }
141888
142251
  if (!running) return;
141889
142252
  if (!menuSent) {
@@ -141898,6 +142261,8 @@ var makeTelegramAdapter = (deps) => {
141898
142261
  acknowledged = offset;
141899
142262
  const updates = await api.getUpdates(offset);
141900
142263
  if (!running) return;
142264
+ handlers.onConnectionState?.({ state: "connected" });
142265
+ checkingConnection = false;
141901
142266
  backoff = BACKOFF_START_MS;
141902
142267
  for (const update of updates) {
141903
142268
  if (!running) break;
@@ -141982,6 +142347,7 @@ var makeTelegramAdapter = (deps) => {
141982
142347
  }
141983
142348
  } catch (error) {
141984
142349
  if (!running) return;
142350
+ if (checkingConnection) handlers.onConnectionState?.({ state: "failed", error });
141985
142351
  handlers.onError(error instanceof Error ? error.message : String(error));
141986
142352
  await Promise.race([(deps.sleep ?? defaultSleep)(backoff), stopped]);
141987
142353
  backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
@@ -141999,9 +142365,9 @@ var makeTelegramAdapter = (deps) => {
141999
142365
  }
142000
142366
  };
142001
142367
  },
142002
- sendText: async (binding, text4) => {
142368
+ sendText: async (binding, text4, options) => {
142003
142369
  const api = await apiFor(binding);
142004
- await api.sendMessage(binding.chatId, text4);
142370
+ await api.sendMessage(binding.chatId, text4, options?.retry);
142005
142371
  },
142006
142372
  checkConnection: async (bot) => {
142007
142373
  const token = tokenOf(bot);
@@ -142260,6 +142626,7 @@ var makeTurns = (core, deps) => {
142260
142626
  const text4 = message.text;
142261
142627
  const threadId = await ensureThread(binding, text4 || message.resources[0]?.name || "Chat");
142262
142628
  if (threadId === null) return null;
142629
+ const topicRoot = await message.topicRootForSession?.(threadId);
142263
142630
  const carried = waitingFor(chatKey, threadId);
142264
142631
  pendingByChat.delete(chatKey);
142265
142632
  const own = await collectResources(
@@ -142297,7 +142664,10 @@ var makeTurns = (core, deps) => {
142297
142664
  message: {
142298
142665
  messageId: userMessageId,
142299
142666
  role: "user",
142300
- text: [textWithInboundContext(message, binding.rowId, t), ...notes].join("\n"),
142667
+ text: [
142668
+ textWithInboundContext(message, binding.rowId, t, topicRoot?.reference),
142669
+ ...notes
142670
+ ].join("\n"),
142301
142671
  attachments: attachments.map(
142302
142672
  (attachment) => attachment.type === "image" ? { ...attachment, type: "image" } : { ...attachment, type: "file" }
142303
142673
  )
@@ -142307,6 +142677,7 @@ var makeTurns = (core, deps) => {
142307
142677
  interactionMode: interactionModeOf(chatKey),
142308
142678
  ...intentAction !== void 0 ? { intentAction } : {}
142309
142679
  });
142680
+ topicRoot?.confirmConsumed();
142310
142681
  if (intentAction?.kind !== "steer") {
142311
142682
  deps.conclusions.owe({
142312
142683
  chatKey,
@@ -142435,8 +142806,15 @@ var ROW_ACTION_ITEM_SCHEMA = makeFlowItemSchema({
142435
142806
  doneRequired: ["messageKey", "fills"]
142436
142807
  });
142437
142808
  var botOf = (config2, adapters, rowId) => {
142438
- const adapterFor = (platform) => adapters.find((adapter) => adapter.platform === platform);
142439
- return readBots(config2, adapterFor).find((bot) => bot.rowId === rowId);
142809
+ const table2 = config2[BOTS_FIELD];
142810
+ if (typeof table2 !== "object" || table2 === null || Array.isArray(table2) || !Object.hasOwn(table2, rowId))
142811
+ return void 0;
142812
+ const row = table2[rowId];
142813
+ if (typeof row !== "object" || row === null || Array.isArray(row)) return void 0;
142814
+ const raw = row;
142815
+ const platform = platformOf(raw);
142816
+ const adapter = adapters.find((candidate) => candidate.platform === platform);
142817
+ return { ...readBotView(rowId, raw, adapter ?? { label: platform }), platform };
142440
142818
  };
142441
142819
  var reportOf = (bot, stage, ok, detail, hint) => ({
142442
142820
  ref: diagnosticRef("conn"),
@@ -142452,12 +142830,9 @@ var reportOf = (bot, stage, ok, detail, hint) => ({
142452
142830
  var probe = async (adapters, bot) => {
142453
142831
  const adapter = adapters.find((candidate) => candidate.platform === bot.platform);
142454
142832
  if (adapter?.checkConnection === void 0) {
142455
- return reportOf(bot, "platform", false, `unsupported platform ${bot.platform}`);
142456
- }
142457
- const redactor = makeRedactor();
142458
- for (const column of adapter.columns) {
142459
- if (column.kind === "password") redactor.register(bot.values[column.key]);
142833
+ return reportOf(bot, "platform", false, `unsupported platform ${bot.platform}`, "notAvailable");
142460
142834
  }
142835
+ const redactor = botRedactor(adapter.columns, bot.values);
142461
142836
  try {
142462
142837
  const result = await adapter.checkConnection(bot);
142463
142838
  return reportOf(
@@ -142482,10 +142857,15 @@ var emitReport = (emit, report, action) => {
142482
142857
  });
142483
142858
  return;
142484
142859
  }
142860
+ const suggested = report.hint !== void 0 && action.keyPrefix === "rowAction.checkConnection";
142485
142861
  emit({
142486
142862
  kind: "failed",
142487
- reasonKey: report.hint !== void 0 && action.keyPrefix === "rowAction.checkConnection" ? `rowAction.checkConnection.hint.${report.hint}` : `${action.keyPrefix}.failed`,
142488
- params: { ref: report.ref },
142863
+ reasonKey: suggested && report.hint !== void 0 ? connectionHintKey(report.hint) : `${action.keyPrefix}.failed`,
142864
+ // 参考号由界面写在细节第一行(「参考号:…」),结论句里不带。
142865
+ params: {
142866
+ ref: report.ref,
142867
+ ...suggested && report.hint !== void 0 ? { hint: report.hint } : {}
142868
+ },
142489
142869
  detail
142490
142870
  });
142491
142871
  };
@@ -142537,9 +142917,9 @@ var registerSettingsRowActions = (ctx, adapters) => {
142537
142917
  send({
142538
142918
  kind: "steps",
142539
142919
  steps: [
142540
- { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "pending" },
142541
- { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "pending" },
142542
- { id: "publish", labelKey: "rowAction.larkRepair.step.publish", state: "pending" },
142920
+ { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "skipped" },
142921
+ { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "skipped" },
142922
+ { id: "publish", labelKey: "rowAction.larkRepair.step.publish", state: "skipped" },
142543
142923
  { id: "verify", labelKey: "rowAction.larkRepair.step.verify", state: "running" }
142544
142924
  ]
142545
142925
  });
@@ -142556,9 +142936,9 @@ var registerSettingsRowActions = (ctx, adapters) => {
142556
142936
  send({
142557
142937
  kind: "steps",
142558
142938
  steps: [
142559
- { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "done" },
142560
- { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "done" },
142561
- { id: "publish", labelKey: "rowAction.larkRepair.step.publish", state: "done" },
142939
+ { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "skipped" },
142940
+ { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "skipped" },
142941
+ { id: "publish", labelKey: "rowAction.larkRepair.step.publish", state: "skipped" },
142562
142942
  {
142563
142943
  id: "verify",
142564
142944
  labelKey: "rowAction.larkRepair.step.verify",
@@ -142574,7 +142954,7 @@ var registerSettingsRowActions = (ctx, adapters) => {
142574
142954
  warnings: [
142575
142955
  {
142576
142956
  key: "rowAction.larkRepair.hint",
142577
- params: { ref, detail: formatDiagnosticCopy({ ...report, ref }) }
142957
+ params: { ref, detail: formatDiagnosticCopy(report) }
142578
142958
  }
142579
142959
  ]
142580
142960
  });
@@ -142584,7 +142964,7 @@ var registerSettingsRowActions = (ctx, adapters) => {
142584
142964
  kind: "failed",
142585
142965
  reasonKey: "rowAction.larkRepair.failed",
142586
142966
  params: { ref },
142587
- detail: formatDiagnosticCopy({ ...report, ref })
142967
+ detail: formatDiagnosticCopy(report)
142588
142968
  });
142589
142969
  }
142590
142970
  });
@@ -142784,8 +143164,29 @@ var generalWriteOptions = (payload, next) => {
142784
143164
  }
142785
143165
  return void 0;
142786
143166
  };
142787
- var viewAfterWrite = async (ctx, adapters) => buildSettingsView(await ctx.settings.read(), adapters);
142788
- var registerSettingsCommands = (ctx, adapters, updateSettings) => {
143167
+ var registerSettingsCommands = (ctx, adapters, updateSettings, connections) => {
143168
+ const viewAfterWrite = async () => buildSettingsView(await ctx.settings.read(), adapters);
143169
+ ctx.stream({
143170
+ name: SETTINGS_CONNECTIONS_COMMAND,
143171
+ description: "Read bot connection states as an initial snapshot and subsequent changes.",
143172
+ scope: "read",
143173
+ payloadSchema: EMPTY_PAYLOAD,
143174
+ buffer: { latest: 1 },
143175
+ handle: async (_payload, emit, signal) => {
143176
+ if (signal.aborted) return;
143177
+ const unsubscribe = connections.subscribe((snapshot) => {
143178
+ if (!signal.aborted) emit(snapshot);
143179
+ });
143180
+ try {
143181
+ await new Promise((resolve) => {
143182
+ if (signal.aborted) resolve();
143183
+ else signal.addEventListener("abort", () => resolve(), { once: true });
143184
+ });
143185
+ } finally {
143186
+ unsubscribe();
143187
+ }
143188
+ }
143189
+ });
142789
143190
  ctx.command({
142790
143191
  name: SETTINGS_VIEW_COMMAND,
142791
143192
  description: "Read the chat-bridge settings view, bot columns, and this machine's proxies.",
@@ -142812,7 +143213,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142812
143213
  return { result, write: { config: next, options: { secrets, clearSecrets } } };
142813
143214
  });
142814
143215
  if (!edited.ok) return edited;
142815
- return { ok: true, rowId: edited.rowId, view: await viewAfterWrite(ctx, adapters) };
143216
+ return { ok: true, rowId: edited.rowId, view: await viewAfterWrite() };
142816
143217
  };
142817
143218
  const removeRow2 = async (remove) => {
142818
143219
  const removed = await updateSettings((config2) => {
@@ -142820,7 +143221,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142820
143221
  return next === null ? { result: false } : { result: true, write: { config: next } };
142821
143222
  });
142822
143223
  if (!removed) return { ok: false, code: "unknown-row" };
142823
- return { ok: true, view: await viewAfterWrite(ctx, adapters) };
143224
+ return { ok: true, view: await viewAfterWrite() };
142824
143225
  };
142825
143226
  ctx.command({
142826
143227
  name: SETTINGS_SAVE_BOT_COMMAND,
@@ -142871,7 +143272,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142871
143272
  write: options === void 0 ? { config: next } : { config: next, options }
142872
143273
  };
142873
143274
  });
142874
- return { ok: true, view: await viewAfterWrite(ctx, adapters) };
143275
+ return { ok: true, view: await viewAfterWrite() };
142875
143276
  }
142876
143277
  });
142877
143278
  };
@@ -142899,12 +143300,13 @@ var makeWechatAdapter = (deps) => {
142899
143300
  columns: COLUMNS3,
142900
143301
  validate: wechatConfigIssues,
142901
143302
  isConfigured: (bot) => wechatConfigIssues(bot).length === 0,
142902
- connect: (bot, _handlers) => {
143303
+ connect: (bot, handlers) => {
142903
143304
  deps.log.warn("wechat iLink connect is stubbed pending security-review gate (YCO-236)", {
142904
143305
  bot: bot.displayName,
142905
143306
  hasToken: tokenOf2(bot) !== "",
142906
143307
  textLimit: WECHAT_TEXT_MAX_CHARS
142907
143308
  });
143309
+ handlers.onConnectionState?.({ state: "failed", reason: "not-available" });
142908
143310
  return { close: () => void 0 };
142909
143311
  },
142910
143312
  sendText: async (binding, _text) => {
@@ -142927,7 +143329,7 @@ var makeWechatAdapter = (deps) => {
142927
143329
  };
142928
143330
 
142929
143331
  // src/wecomUpload.ts
142930
- import * as NodeCrypto4 from "node:crypto";
143332
+ import * as NodeCrypto5 from "node:crypto";
142931
143333
  var WECOM_CHUNK_BYTES = 512 * 1024;
142932
143334
  var WECOM_MAX_CHUNKS = 100;
142933
143335
  var WECOM_MIN_UPLOAD_BYTES = 5;
@@ -142960,7 +143362,7 @@ var uploadWecomMedia = async (input) => {
142960
143362
  filename: clampName(input.filename),
142961
143363
  total_size: bytes.byteLength,
142962
143364
  total_chunks: totalChunks,
142963
- md5: NodeCrypto4.createHash("md5").update(bytes).digest("hex")
143365
+ md5: NodeCrypto5.createHash("md5").update(bytes).digest("hex")
142964
143366
  });
142965
143367
  const uploadId = init.upload_id;
142966
143368
  if (typeof uploadId !== "string" || uploadId === "") {
@@ -143047,7 +143449,7 @@ var wecomReplyReference = (quote) => {
143047
143449
  };
143048
143450
 
143049
143451
  // src/wecomApi.ts
143050
- var DEFAULT_WECOM_ENDPOINT = "wss://openws.work.weixin.qq.com/wssvr/";
143452
+ var DEFAULT_WECOM_ENDPOINT = "wss://openws.work.weixin.qq.com";
143051
143453
  var PING_INTERVAL_MS = 3e4;
143052
143454
  var MAX_MISSED_PONGS = 2;
143053
143455
  var SUBSCRIBE_TIMEOUT_MS = 1e4;
@@ -143184,9 +143586,10 @@ var runWecomClient = (input) => {
143184
143586
  const message = parseWecomMessage(frame, input.botId);
143185
143587
  if (message !== null) input.handlers.onMessage(message);
143186
143588
  };
143187
- const finish = () => {
143589
+ const finish = (error = new Error("WeCom connection closed"), reason = "disconnected") => {
143188
143590
  if (finished) return;
143189
143591
  finished = true;
143592
+ if (!closed) input.handlers.onConnectionState?.({ state: "failed", error, reason });
143190
143593
  clearInterval(heartbeat);
143191
143594
  openStreams.clear();
143192
143595
  void input.socket.close().catch(() => void 0);
@@ -143204,6 +143607,7 @@ var runWecomClient = (input) => {
143204
143607
  if (!closed) {
143205
143608
  input.handlers.onError(error instanceof Error ? error.message : String(error));
143206
143609
  }
143610
+ finish(error);
143207
143611
  }
143208
143612
  finish();
143209
143613
  };
@@ -143211,7 +143615,7 @@ var runWecomClient = (input) => {
143211
143615
  if (finished) return;
143212
143616
  if (missedPong >= MAX_MISSED_PONGS) {
143213
143617
  input.handlers.onError("socket hang up: missed pong");
143214
- finish();
143618
+ finish(new Error("socket hang up: missed pong"));
143215
143619
  return;
143216
143620
  }
143217
143621
  missedPong += 1;
@@ -143232,10 +143636,13 @@ var runWecomClient = (input) => {
143232
143636
  settleAck(subscribeId, error instanceof Error ? error : new Error(String(error)));
143233
143637
  });
143234
143638
  const ready = subscribed.then(
143235
- () => true,
143639
+ () => {
143640
+ if (!finished && !closed) input.handlers.onConnectionState?.({ state: "connected" });
143641
+ return !finished && !closed;
143642
+ },
143236
143643
  (error) => {
143237
143644
  if (!closed) input.handlers.onError(error instanceof Error ? error.message : String(error));
143238
- finish();
143645
+ finish(error, "subscription-failed");
143239
143646
  return false;
143240
143647
  }
143241
143648
  );
@@ -143370,7 +143777,7 @@ var runWecomClient = (input) => {
143370
143777
  };
143371
143778
 
143372
143779
  // src/wecomMedia.ts
143373
- import * as NodeCrypto5 from "node:crypto";
143780
+ import * as NodeCrypto6 from "node:crypto";
143374
143781
 
143375
143782
  // src/wecomPadding.ts
143376
143783
  var PADDING_BLOCK = 32;
@@ -143401,7 +143808,7 @@ var decryptWecomMedia = (cipher, aesKey) => {
143401
143808
  if (cipher.byteLength === 0 || cipher.byteLength % AES_BLOCK !== 0) {
143402
143809
  throw new WecomDecryptError("the ciphertext is not a whole number of blocks");
143403
143810
  }
143404
- const decipher = NodeCrypto5.createDecipheriv("aes-256-cbc", key, key.subarray(0, IV_BYTES));
143811
+ const decipher = NodeCrypto6.createDecipheriv("aes-256-cbc", key, key.subarray(0, IV_BYTES));
143405
143812
  decipher.setAutoPadding(false);
143406
143813
  const plain = Buffer.concat([decipher.update(cipher), decipher.final()]);
143407
143814
  const unpadded = stripPadding(plain);
@@ -143544,6 +143951,9 @@ ${kept}`;
143544
143951
  botSecret,
143545
143952
  handlers: {
143546
143953
  onError: handlers.onError,
143954
+ onConnectionState: (state) => {
143955
+ if (!stopped) handlers.onConnectionState?.(state);
143956
+ },
143547
143957
  onMessage: (message) => {
143548
143958
  if (message.chatType !== "single") return;
143549
143959
  client.rememberRequest(message.chatId, message.requestId);
@@ -143585,6 +143995,8 @@ ${kept}`;
143585
143995
  try {
143586
143996
  if (await runOnce()) backoff = BACKOFF_START_MS2;
143587
143997
  } catch (error) {
143998
+ if (stopped) return;
143999
+ handlers.onConnectionState?.({ state: "failed", error, reason: "disconnected" });
143588
144000
  handlers.onError(error instanceof Error ? error.message : String(error));
143589
144001
  }
143590
144002
  if (stopped) return;
@@ -143602,6 +144014,7 @@ ${kept}`;
143602
144014
  }
143603
144015
  };
143604
144016
  },
144017
+ // 不看 `options.retry`:长连接的回执没有核实过的限流码,也没有幂等键,失败一律说不清发没发出去。
143605
144018
  sendText: async (binding, text4) => {
143606
144019
  const client = clients.get(binding.rowId);
143607
144020
  if (client === void 0) {
@@ -143618,12 +144031,36 @@ ${kept}`;
143618
144031
  detail: "missing bot id or secret in saved config"
143619
144032
  };
143620
144033
  }
144034
+ let stage = "websocket";
143621
144035
  try {
143622
144036
  const socket = await deps.connectSocket(endpoint);
143623
- await socket.close();
143624
- return { stage: "websocket", ok: true, detail: `endpoint reachable (${endpoint})` };
144037
+ stage = "aibot_subscribe";
144038
+ let detail = "WeCom connection closed before subscribe ack";
144039
+ let closing;
144040
+ const client = runWecomClient({
144041
+ socket: {
144042
+ send: (data) => socket.send(data),
144043
+ messages: socket.messages,
144044
+ close: () => closing ??= socket.close()
144045
+ },
144046
+ botId,
144047
+ botSecret,
144048
+ handlers: {
144049
+ onMessage: () => void 0,
144050
+ onError: (error) => {
144051
+ detail = error;
144052
+ }
144053
+ }
144054
+ });
144055
+ try {
144056
+ const ok = await client.ready;
144057
+ return { stage, ok, detail: ok ? `subscription confirmed (${endpoint})` : detail };
144058
+ } finally {
144059
+ client.close();
144060
+ await closing;
144061
+ }
143625
144062
  } catch (error) {
143626
- return { stage: "websocket", ok: false, detail: describeError(error) };
144063
+ return { stage, ok: false, detail: describeError(error) };
143627
144064
  }
143628
144065
  },
143629
144066
  askApproval: async (binding, request) => {
@@ -143680,18 +144117,25 @@ ${kept}`;
143680
144117
  // src/wecomAppApi.ts
143681
144118
  var WECOM_APP_DEFAULT_API_BASE = "https://qyapi.weixin.qq.com";
143682
144119
  var WECOM_APP_TEXT_MAX_BYTES = 2048;
144120
+ var REQUEST_TIMEOUT_MS = 15e3;
144121
+ var MEDIA_TIMEOUT_MS = 6e4;
143683
144122
  var WecomAppApiError = class extends Error {
143684
144123
  code;
143685
144124
  providerCode;
143686
144125
  hint;
144126
+ invalidUsers;
143687
144127
  constructor(code2, message, extras) {
143688
144128
  super(message);
143689
144129
  this.name = "WecomAppApiError";
143690
144130
  this.code = code2;
143691
144131
  this.providerCode = extras?.providerCode;
143692
144132
  this.hint = extras?.hint;
144133
+ this.invalidUsers = extras?.invalidUsers ?? [];
143693
144134
  }
143694
144135
  };
144136
+ var TOKEN_INVALID_CODES = /* @__PURE__ */ new Set(["40014", "42001"]);
144137
+ var RATE_LIMITED_CODES = /* @__PURE__ */ new Set(["45009", "45033"]);
144138
+ var wecomAppSendVerdict = (error) => error instanceof WecomAppApiError && error.providerCode !== void 0 && (RATE_LIMITED_CODES.has(error.providerCode) || TOKEN_INVALID_CODES.has(error.providerCode)) ? { retry: true } : NO_RETRY;
143695
144139
  var cleanString2 = (value2) => {
143696
144140
  if (typeof value2 !== "string") return null;
143697
144141
  const trimmed = value2.trim();
@@ -143727,6 +144171,7 @@ var splitUtf8ByBytes = (text4, maxBytes = WECOM_APP_TEXT_MAX_BYTES) => {
143727
144171
  if (current.length > 0) chunks.push(current);
143728
144172
  return chunks;
143729
144173
  };
144174
+ var parseInvalidUsers = (value2) => typeof value2 === "string" ? value2.split("|").map((user) => user.trim()).filter(Boolean) : [];
143730
144175
  var WecomAppApi = class {
143731
144176
  corpId;
143732
144177
  corpSecret;
@@ -143778,9 +144223,13 @@ var WecomAppApi = class {
143778
144223
  async sendText(input) {
143779
144224
  const chunks = splitUtf8ByBytes(input.content);
143780
144225
  for (const chunk of chunks) {
143781
- await this.#send(
143782
- { touser: input.userId, msgtype: "text", text: { content: chunk } },
143783
- input.signal
144226
+ await sendWithRetry(
144227
+ () => this.#send(
144228
+ { touser: input.userId, msgtype: "text", text: { content: chunk } },
144229
+ input.signal
144230
+ ),
144231
+ wecomAppSendVerdict,
144232
+ input.retry
143784
144233
  );
143785
144234
  }
143786
144235
  }
@@ -143807,12 +144256,16 @@ var WecomAppApi = class {
143807
144256
  new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }),
143808
144257
  input.name
143809
144258
  );
143810
- const payload = await this.#requestJson(url, {
143811
- method: "POST",
143812
- // 不设 content-type:multipart 的 boundary 要让 fetch 自己写。
143813
- body: form,
143814
- ...input.signal === void 0 ? {} : { signal: input.signal }
143815
- });
144259
+ const payload = await this.#requestJson(
144260
+ url,
144261
+ {
144262
+ method: "POST",
144263
+ // 不设 content-type:multipart 的 boundary 要让 fetch 自己写。
144264
+ body: form,
144265
+ ...input.signal === void 0 ? {} : { signal: input.signal }
144266
+ },
144267
+ MEDIA_TIMEOUT_MS
144268
+ );
143816
144269
  const mediaId = payload["media_id"];
143817
144270
  if (typeof mediaId === "string" && mediaId !== "") return mediaId;
143818
144271
  throw this.#apiError(Number(payload["errcode"] ?? -1), payload["errmsg"]);
@@ -143826,10 +144279,17 @@ var WecomAppApi = class {
143826
144279
  const url = new URL("/cgi-bin/media/get", this.apiBaseUrl);
143827
144280
  url.searchParams.set("access_token", token);
143828
144281
  url.searchParams.set("media_id", input.mediaId);
143829
- const response = await this.#fetch(url, {
143830
- method: "GET",
143831
- ...input.signal === void 0 ? {} : { signal: input.signal }
143832
- });
144282
+ return this.#withRequest(
144283
+ url,
144284
+ {
144285
+ method: "GET",
144286
+ ...input.signal === void 0 ? {} : { signal: input.signal }
144287
+ },
144288
+ MEDIA_TIMEOUT_MS,
144289
+ (response) => this.#readMedia(response, input.maxBytes)
144290
+ );
144291
+ }
144292
+ async #readMedia(response, maxBytes) {
143833
144293
  const contentType = response.headers.get("content-type") ?? "";
143834
144294
  if (contentType.includes("application/json") || contentType.includes("text/plain")) {
143835
144295
  let payload = {};
@@ -143849,7 +144309,7 @@ var WecomAppApi = class {
143849
144309
  `WeCom media/get failed (HTTP ${String(response.status)}).`
143850
144310
  );
143851
144311
  }
143852
- const bytes = await readBodyLimited(response, input.maxBytes);
144312
+ const bytes = await readBodyLimited(response, maxBytes);
143853
144313
  const name = filenameFromContentDisposition(response.headers.get("content-disposition"));
143854
144314
  const mimeType = contentType.split(";")[0]?.trim();
143855
144315
  return {
@@ -143868,55 +144328,76 @@ var WecomAppApi = class {
143868
144328
  body: JSON.stringify({ ...message, agentid: Number.parseInt(this.agentId, 10) }),
143869
144329
  ...signal === void 0 ? {} : { signal }
143870
144330
  });
143871
- const errcode = Number(payload["errcode"] ?? 0);
143872
- if (errcode !== 0) throw this.#apiError(errcode, payload["errmsg"]);
143873
144331
  const invalid = payload["invaliduser"];
143874
144332
  if (typeof invalid === "string" && invalid.trim() !== "") {
143875
144333
  throw new WecomAppApiError("invalid-user", `WeCom did not deliver to: ${invalid.trim()}.`, {
143876
144334
  providerCode: "81013",
143877
- hint: "\u786E\u8BA4\u63A5\u6536\u4EBA\u4ECD\u5728\u8FD9\u4E2A\u81EA\u5EFA\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\u5185\u3002"
144335
+ hint: "wecomInvalidUser",
144336
+ invalidUsers: parseInvalidUsers(invalid)
143878
144337
  });
143879
144338
  }
143880
144339
  }
143881
- #apiError(errcode, errmsg) {
144340
+ #apiError(errcode, errmsg, invalidUsers) {
143882
144341
  const code2 = String(errcode);
143883
144342
  const message = typeof errmsg === "string" && errmsg !== "" ? `WeCom API failed (${code2}): ${errmsg}` : `WeCom API failed (${code2}).`;
143884
- if (errcode === 40014 || errcode === 42001) this.invalidateToken();
144343
+ if (TOKEN_INVALID_CODES.has(code2)) this.invalidateToken();
143885
144344
  if (errcode === 60020) {
143886
144345
  return new WecomAppApiError("trusted-ip", "WeCom rejected the outbound IP (errcode 60020).", {
143887
144346
  providerCode: code2,
143888
- hint: "\u628A\u51FA\u7AD9\u516C\u7F51 IP \u52A0\u8FDB\u4F01\u4E1A\u5FAE\u4FE1\u540E\u53F0\u7684\u300C\u4F01\u4E1A\u53EF\u4FE1 IP\u300D\u3002"
144347
+ hint: "wecomTrustedIp"
143889
144348
  });
143890
144349
  }
143891
144350
  if (errcode === 81013) {
143892
144351
  return new WecomAppApiError("invalid-user", message, {
143893
144352
  providerCode: code2,
143894
- hint: "\u6536\u4EF6\u4EBA\u3001\u90E8\u95E8\u6216\u6807\u7B7E\u4E0D\u5B58\u5728\uFF0C\u6216\u4E0D\u5728\u8FD9\u4E2A\u81EA\u5EFA\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\u5185\u3002"
144353
+ hint: "wecomInvalidUser",
144354
+ invalidUsers: parseInvalidUsers(invalidUsers)
143895
144355
  });
143896
144356
  }
143897
144357
  if (errcode === 40056) {
143898
144358
  return new WecomAppApiError("invalid-agent", message, {
143899
144359
  providerCode: code2,
143900
- hint: "AgentId \u4E0D\u5BF9\uFF0C\u6838\u5BF9\u81EA\u5EFA\u5E94\u7528\u7684 AgentId\u3002"
144360
+ hint: "wecomInvalidAgent"
143901
144361
  });
143902
144362
  }
143903
144363
  return new WecomAppApiError("api-failed", message, { providerCode: code2 });
143904
144364
  }
143905
- async #requestJson(url, init) {
143906
- const response = await this.#fetch(url, init);
143907
- let payload;
144365
+ async #withRequest(url, init, timeoutMs, read) {
144366
+ const deadline = new AbortController();
144367
+ const signal = AbortSignal.any([deadline.signal, ...init.signal ? [init.signal] : []]);
144368
+ const timeout = new WecomAppApiError(
144369
+ "timeout",
144370
+ `WeCom ${url.pathname} timed out after ${timeoutMs}ms.`
144371
+ );
144372
+ const timer = setTimeout(() => deadline.abort(timeout), timeoutMs);
143908
144373
  try {
143909
- payload = await response.json();
143910
- } catch {
143911
- throw new WecomAppApiError(
143912
- "bad-response",
143913
- `WeCom returned non-JSON (HTTP ${String(response.status)}).`
143914
- );
143915
- }
143916
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
143917
- throw new WecomAppApiError("bad-response", "WeCom returned a non-object JSON body.");
144374
+ return await read(await this.#fetch(url, { ...init, signal }));
144375
+ } catch (error) {
144376
+ if (signal.aborted) throw signal.reason;
144377
+ throw error;
144378
+ } finally {
144379
+ clearTimeout(timer);
143918
144380
  }
143919
- return payload;
144381
+ }
144382
+ async #requestJson(url, init, timeoutMs = REQUEST_TIMEOUT_MS) {
144383
+ return this.#withRequest(url, init, timeoutMs, async (response) => {
144384
+ let payload;
144385
+ try {
144386
+ payload = await response.json();
144387
+ } catch {
144388
+ throw new WecomAppApiError(
144389
+ "bad-response",
144390
+ `WeCom returned non-JSON (HTTP ${String(response.status)}).`
144391
+ );
144392
+ }
144393
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
144394
+ throw new WecomAppApiError("bad-response", "WeCom returned a non-object JSON body.");
144395
+ }
144396
+ const result = payload;
144397
+ const errcode = Number(result["errcode"] ?? 0);
144398
+ if (errcode !== 0) throw this.#apiError(errcode, result["errmsg"], result["invaliduser"]);
144399
+ return result;
144400
+ });
143920
144401
  }
143921
144402
  };
143922
144403
 
@@ -143925,7 +144406,7 @@ import * as NodeHttp2 from "node:http";
143925
144406
  import * as NodeNet from "node:net";
143926
144407
 
143927
144408
  // src/wecomAppCrypto.ts
143928
- import * as NodeCrypto6 from "node:crypto";
144409
+ import * as NodeCrypto7 from "node:crypto";
143929
144410
  var AES_KEY_BYTES = 32;
143930
144411
  var RANDOM_BYTES = 16;
143931
144412
  var WecomAppCryptoError = class extends Error {
@@ -143948,13 +144429,13 @@ var timingSafeEqualString2 = (a, b) => {
143948
144429
  const left = Buffer.from(a);
143949
144430
  const right = Buffer.from(b);
143950
144431
  if (left.length === 0 || left.length !== right.length) return false;
143951
- return NodeCrypto6.timingSafeEqual(left, right);
144432
+ return NodeCrypto7.timingSafeEqual(left, right);
143952
144433
  };
143953
- var computeSignature = (token, timestamp, nonce, encrypt) => NodeCrypto6.createHash("sha1").update([token, timestamp, nonce, encrypt].sort().join("")).digest("hex");
144434
+ var computeSignature = (token, timestamp, nonce, encrypt) => NodeCrypto7.createHash("sha1").update([token, timestamp, nonce, encrypt].sort().join("")).digest("hex");
143954
144435
  var verifySignature = (token, timestamp, nonce, encrypt, signature) => timingSafeEqualString2(computeSignature(token, timestamp, nonce, encrypt), signature);
143955
144436
  var decrypt = (encodingAESKey, encryptText, expectReceiveId) => {
143956
144437
  const { key, iv } = parseEncodingAESKey(encodingAESKey);
143957
- const decipher = NodeCrypto6.createDecipheriv("aes-256-cbc", key, iv);
144438
+ const decipher = NodeCrypto7.createDecipheriv("aes-256-cbc", key, iv);
143958
144439
  decipher.setAutoPadding(false);
143959
144440
  let deciphered;
143960
144441
  try {
@@ -144013,7 +144494,7 @@ var createWecomAppCrypto = (input) => {
144013
144494
  decrypt: (encryptText) => decrypt(encodingAESKey, encryptText, corpId).message
144014
144495
  };
144015
144496
  };
144016
- var deriveCallbackPathSecret = (token, encodingAESKey) => NodeCrypto6.createHash("sha256").update(`${token.trim()}:${encodingAESKey.trim()}`).digest("hex").slice(0, 24);
144497
+ var deriveCallbackPathSecret = (token, encodingAESKey) => NodeCrypto7.createHash("sha256").update(`${token.trim()}:${encodingAESKey.trim()}`).digest("hex").slice(0, 24);
144017
144498
 
144018
144499
  // src/wecomAppParse.ts
144019
144500
  var isRecord4 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
@@ -144460,6 +144941,7 @@ var WECOM_APP_IMAGE_BYTES = 10 * 1024 * 1024;
144460
144941
  var WECOM_APP_FILE_BYTES = 20 * 1024 * 1024;
144461
144942
  var WECOM_APP_MIN_BYTES = 5;
144462
144943
  var WECOM_APP_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png"]);
144944
+ var CREDENTIAL_ERROR_CODES = /* @__PURE__ */ new Set(["40001", "40013", "40014", "40056", "42001", "60020"]);
144463
144945
  var joinCallbackUrl = (baseOrOrigin, path) => `${baseOrOrigin}${path}`;
144464
144946
  var wecomAppPublicFields = (bot) => {
144465
144947
  const c = wecomAppCredentials(bot);
@@ -144485,6 +144967,8 @@ var OUTBOUND_COLUMNS = /* @__PURE__ */ new Set([
144485
144967
  "wecomAppSecret",
144486
144968
  "wecomAppApiBaseUrl"
144487
144969
  ]);
144970
+ var PROBE_BACKOFF_START_MS = 1e3;
144971
+ var PROBE_BACKOFF_MAX_MS = 6e4;
144488
144972
  var makeWecomAppAdapter = (deps) => {
144489
144973
  const apis = /* @__PURE__ */ new Map();
144490
144974
  const newApi = (c) => new WecomAppApi({
@@ -144495,17 +144979,30 @@ var makeWecomAppAdapter = (deps) => {
144495
144979
  fetch: deps.fetch
144496
144980
  });
144497
144981
  const callbackHub = makeWecomAppCallbackHub();
144498
- const apiFor = (binding) => {
144982
+ const connectionFor = (binding) => {
144499
144983
  const api = apis.get(binding.rowId);
144500
144984
  if (api === void 0) {
144501
144985
  throw new ImNotConnectedError(`wecom-app bot '${binding.displayName}' is not connected.`);
144502
144986
  }
144503
144987
  return api;
144504
144988
  };
144989
+ const deliver = async (binding, send) => {
144990
+ const connection = connectionFor(binding);
144991
+ try {
144992
+ await send(connection.api);
144993
+ connection.report({ state: "connected" });
144994
+ } catch (error) {
144995
+ if (error instanceof WecomAppApiError && error.providerCode !== void 0 && CREDENTIAL_ERROR_CODES.has(error.providerCode)) {
144996
+ connection.report({ state: "failed", error, reason: "credentials-rejected" });
144997
+ }
144998
+ throw error;
144999
+ }
145000
+ };
144505
145001
  const sendMedia = async (binding, mediaType, file) => {
144506
- const api = apiFor(binding);
144507
- const mediaId = await api.uploadMedia({ mediaType, ...file });
144508
- await api.sendMedia({ userId: binding.chatId, mediaType, mediaId });
145002
+ await deliver(binding, async (api) => {
145003
+ const mediaId = await api.uploadMedia({ mediaType, ...file });
145004
+ await api.sendMedia({ userId: binding.chatId, mediaType, mediaId });
145005
+ });
144509
145006
  deps.log.info(`wecom-app delivered ${mediaType === "image" ? "an image" : "a file"}`, {
144510
145007
  name: file.name
144511
145008
  });
@@ -144556,6 +145053,19 @@ var makeWecomAppAdapter = (deps) => {
144556
145053
  (issue) => issue.code !== "required" && issue.column !== "wecomAppEncodingAESKey"
144557
145054
  ),
144558
145055
  publicFields: wecomAppPublicFields,
145056
+ // 收件人不在应用可见范围(invaliduser / 81013):说清是谁、为什么、怎么办。
145057
+ describeDeliveryRejection: (error, binding) => {
145058
+ if (!(error instanceof WecomAppApiError) || error.code !== "invalid-user") return void 0;
145059
+ const recipients = [
145060
+ ...new Set(error.invalidUsers.length > 0 ? error.invalidUsers : [binding.chatId])
145061
+ ].sort();
145062
+ return {
145063
+ recipients,
145064
+ summary: deps.translate("reply.wecomDeliveryFailed", { recipient: recipients.join(", ") }),
145065
+ hint: error.hint,
145066
+ providerCode: error.providerCode
145067
+ };
145068
+ },
144559
145069
  /**
144560
145070
  * 自建应用是回调式渠道:入站由企微推到我们的 listener,没有可探的长连接;能探的是出站
144561
145071
  * 凭据——和真实发送同一条路,向 gettoken 换一次 access_token。
@@ -144586,7 +145096,12 @@ var makeWecomAppAdapter = (deps) => {
144586
145096
  detail: "access_token ok (callback reachability is not probed)"
144587
145097
  };
144588
145098
  } catch (error) {
144589
- return { stage: "gettoken", ok: false, detail: describeError(error) };
145099
+ return {
145100
+ stage: "gettoken",
145101
+ ok: false,
145102
+ detail: describeError(error),
145103
+ ...error instanceof WecomAppApiError && error.hint !== void 0 ? { hint: error.hint } : {}
145104
+ };
144590
145105
  }
144591
145106
  },
144592
145107
  connect: async (bot, handlers) => {
@@ -144600,7 +145115,14 @@ var makeWecomAppAdapter = (deps) => {
144600
145115
  const lease = await callbackHub.acquire({ host: config2.listenHost, port: config2.listenPort });
144601
145116
  const server = lease.server;
144602
145117
  const api = newApi(c);
144603
- apis.set(bot.rowId, api);
145118
+ let closed = false;
145119
+ const report = (state) => {
145120
+ if (closed) return;
145121
+ handlers.onConnectionState?.(
145122
+ state.state === "failed" && state.error instanceof WecomAppApiError && state.error.hint !== void 0 ? { ...state, hint: state.error.hint } : state
145123
+ );
145124
+ };
145125
+ apis.set(bot.rowId, { api, report });
144604
145126
  let path;
144605
145127
  try {
144606
145128
  path = server.register({
@@ -144616,7 +145138,7 @@ var makeWecomAppAdapter = (deps) => {
144616
145138
  await lease.release();
144617
145139
  throw error;
144618
145140
  }
144619
- deps.log.info("wecom-app connected", {
145141
+ deps.log.info("wecom-app callback listening", {
144620
145142
  bot: bot.displayName,
144621
145143
  callbackPath: path,
144622
145144
  callbackUrl: joinCallbackUrl(
@@ -144625,8 +145147,26 @@ var makeWecomAppAdapter = (deps) => {
144625
145147
  ),
144626
145148
  listen: `${server.host}:${String(server.port)}`
144627
145149
  });
145150
+ const probe2 = new AbortController();
145151
+ void (async () => {
145152
+ let backoff = PROBE_BACKOFF_START_MS;
145153
+ while (!probe2.signal.aborted) {
145154
+ try {
145155
+ await api.getAccessToken(probe2.signal);
145156
+ report({ state: "connected" });
145157
+ return;
145158
+ } catch (error) {
145159
+ if (probe2.signal.aborted) return;
145160
+ report({ state: "failed", error, reason: "token-unavailable" });
145161
+ await (deps.sleep ?? abortableSleep)(backoff, probe2.signal).catch(() => void 0);
145162
+ backoff = Math.min(backoff * 2, PROBE_BACKOFF_MAX_MS);
145163
+ }
145164
+ }
145165
+ })();
144628
145166
  return {
144629
145167
  close: () => {
145168
+ closed = true;
145169
+ probe2.abort();
144630
145170
  server.unregister(bot.rowId);
144631
145171
  apis.delete(bot.rowId);
144632
145172
  void lease.release().catch((error) => {
@@ -144642,15 +145182,18 @@ var makeWecomAppAdapter = (deps) => {
144642
145182
  if (options?.targetKind === "group") {
144643
145183
  throw new Error("wecom-app can only deliver to a user; group targets are not supported.");
144644
145184
  }
144645
- await apiFor(binding).sendText({ userId: binding.chatId, content: text4 });
145185
+ await deliver(
145186
+ binding,
145187
+ (api) => api.sendText({ userId: binding.chatId, content: text4, retry: options?.retry })
145188
+ );
144646
145189
  },
144647
145190
  // D376:企业微信自建应用没有表情回执也没有流式,开工时先回一句文字,结论照常另发。没有要撤的
144648
145191
  // 东西,回 null;发不出去由核心记一条 warn,不拦这一轮。
144649
145192
  acknowledge: async (binding) => {
144650
- await apiFor(binding).sendText({
144651
- userId: binding.chatId,
144652
- content: deps.translate("reply.received")
144653
- });
145193
+ await deliver(
145194
+ binding,
145195
+ (api) => api.sendText({ userId: binding.chatId, content: deps.translate("reply.received") })
145196
+ );
144654
145197
  return null;
144655
145198
  },
144656
145199
  sendImage: (binding, image2) => sendMedia(binding, "image", image2),
@@ -144661,11 +145204,16 @@ var makeWecomAppAdapter = (deps) => {
144661
145204
  minBytes: WECOM_APP_MIN_BYTES
144662
145205
  },
144663
145206
  imageMimeTypes: WECOM_APP_IMAGE_TYPES,
144664
- fetchResource: (binding, input) => apiFor(binding).downloadMedia({ mediaId: input.resource.key, maxBytes: input.maxBytes }),
145207
+ fetchResource: (binding, input) => connectionFor(binding).api.downloadMedia({
145208
+ mediaId: input.resource.key,
145209
+ maxBytes: input.maxBytes
145210
+ }),
144665
145211
  askApproval: async (binding, request) => {
144666
- const api = apis.get(binding.rowId);
144667
- if (api === void 0) return null;
144668
- await api.sendText({ userId: binding.chatId, content: numberedApprovalText(request) });
145212
+ if (!apis.has(binding.rowId)) return null;
145213
+ await deliver(
145214
+ binding,
145215
+ (api) => api.sendText({ userId: binding.chatId, content: numberedApprovalText(request) })
145216
+ );
144669
145217
  return null;
144670
145218
  },
144671
145219
  answerByText: (_binding, text4, options) => answerByNumber(text4, options)
@@ -144675,7 +145223,7 @@ var makeWecomAppAdapter = (deps) => {
144675
145223
  // src/index.ts
144676
145224
  var imPlugin = definePlugin({
144677
145225
  id: "im",
144678
- apiVersion: 17,
145226
+ apiVersion: 18,
144679
145227
  activate: async (ctx) => {
144680
145228
  const { t, setLanguage } = makeLiveTranslate(
144681
145229
  parseReplyLanguage(ctx.config[REPLY_LANGUAGE_FIELD])
@@ -144736,7 +145284,6 @@ var imPlugin = definePlugin({
144736
145284
  const updateSettings = makeSettingsUpdater(ctx.settings);
144737
145285
  registerLarkSetup(ctx, { updateSettings });
144738
145286
  registerSettingsRowActions(ctx, adapters);
144739
- registerSettingsCommands(ctx, adapters, updateSettings);
144740
145287
  const core = makeImCore({
144741
145288
  host: {
144742
145289
  log: ctx.log,
@@ -144771,7 +145318,7 @@ var imPlugin = definePlugin({
144771
145318
  );
144772
145319
  const teams = makeTeamGoals(core);
144773
145320
  const notifications = makeNotifications(core, {
144774
- clearWorking: acknowledgements.clearWorking,
145321
+ takeWorking: acknowledgements.takeWorking,
144775
145322
  replyCards,
144776
145323
  failures,
144777
145324
  conclusions,
@@ -144847,6 +145394,7 @@ var imPlugin = definePlugin({
144847
145394
  ]
144848
145395
  });
144849
145396
  const connections = startConnections(core, inbound.inboundHandlersFor);
145397
+ registerSettingsCommands(ctx, adapters, updateSettings, connections);
144850
145398
  const { bots, connecting } = connections.reconcile();
144851
145399
  const deliveryHttp = makeDeliveryHttp({
144852
145400
  // 每次请求按此刻的设置找目标:投递目标表改了不用重开监听。