@ycodium-ai/plugin-im 0.2.3180 → 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 {
@@ -138285,7 +138571,7 @@ var en_default = {
138285
138571
  },
138286
138572
  wecomAppCallbackBaseUrl: {
138287
138573
  label: "Public callback base URL (optional)",
138288
- description: "When set, logs print the full callback URL to paste into WeCom \u201CReceive messages \u2192 API receive\u201D. Default listener is 127.0.0.1; use a reverse proxy or tunnel.",
138574
+ description: "The public address that forwards to this machine's callback port. When set, the full callback URL to paste into WeCom \u201CReceive messages \u2192 API receive\u201D appears below; otherwise only the path does.",
138289
138575
  placeholder: "https://im.example.com or http://IP:30987"
138290
138576
  },
138291
138577
  wecomAppApiBaseUrl: {
@@ -138295,9 +138581,14 @@ var en_default = {
138295
138581
  },
138296
138582
  wecomAppListenPort: {
138297
138583
  label: "Local callback port (optional)",
138298
- description: "Defaults to 30987 on 127.0.0.1. Shared across self-built apps.",
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
  },
138587
+ wecomAppListenHost: {
138588
+ label: "Local listen address (optional)",
138589
+ description: "Defaults to 127.0.0.1: only this machine can reach the callback port, so put a reverse proxy or tunnel in front of it. 0.0.0.0 listens on every network interface, which opens the callback port to the network; only use it behind a firewall you control.",
138590
+ placeholder: "127.0.0.1"
138591
+ },
138301
138592
  wechatBotToken: {
138302
138593
  label: "WeChat bot token",
138303
138594
  description: "bot_token from QR binding. Live iLink login is gated by proposal 0069; filling this in does not connect yet."
@@ -138308,6 +138599,7 @@ var en_default = {
138308
138599
  }
138309
138600
  },
138310
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.",
138311
138603
  conclusionRecovered: "(Finished while restarting; delivered now)",
138312
138604
  notAllowed: "You are not on this bot's allow list, so this tap had no effect.",
138313
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.',
@@ -138331,7 +138623,8 @@ var en_default = {
138331
138623
  unsupportedType: "I can't process {{type}} messages yet.",
138332
138624
  unsupportedFolder: "Folders can't be downloaded. Zip it and send the archive instead.",
138333
138625
  sendingFile: "Sending {{name}}:",
138334
- notAllowedYet: `You are not on the allow list yet. Your user id is {{id}}. Add it under Settings \u2192 Plugins \u2192 Chat, in this bot's "Allowed people" box.`
138626
+ notAllowedYet: `You are not on the allow list yet. Your user id is {{id}}. Add it under Settings \u2192 Plugins \u2192 Chat, in this bot's "Allowed people" box.`,
138627
+ received: "Got it, working on it."
138335
138628
  },
138336
138629
  approval: {
138337
138630
  allow: "Allow",
@@ -138662,13 +138955,16 @@ var en_default = {
138662
138955
  description: "Verify platform credentials still work; does not open a session or call a model.",
138663
138956
  running: "Checking {{rowId}}\u2026",
138664
138957
  done: "Connection OK",
138665
- failed: "Connection failed (ref {{ref}})",
138958
+ failed: "Connection failed",
138666
138959
  notSaved: "Save this row first (credentials are read from saved config only).",
138667
138960
  hint: {
138668
- badToken: "This does not look like a bot token. Copy it again from BotFather: digits, a colon, then the key (ref {{ref}})",
138669
- unauthorized: "Telegram does not accept this token. It may have been revoked; get the current one from BotFather (ref {{ref}})",
138670
- notBot: "This token does not belong to a bot. Use the token BotFather gave you (ref {{ref}})",
138671
- 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."
138672
138968
  }
138673
138969
  },
138674
138970
  copyDiagnostics: {
@@ -138676,24 +138972,24 @@ var en_default = {
138676
138972
  description: "Run a connection probe and produce copy-paste diagnostics (no secrets).",
138677
138973
  running: "Building diagnostics for {{rowId}}\u2026",
138678
138974
  done: "Diagnostics ready \u2014 see details below",
138679
- failed: "Diagnostics failed (ref {{ref}})",
138975
+ failed: "Diagnostics failed",
138680
138976
  notSaved: "Save this row first before copying diagnostics."
138681
138977
  },
138682
138978
  diagnostic: {
138683
- ref: "Ref {{ref}}\n{{detail}}"
138979
+ ref: "Reference: {{ref}}\n{{detail}}"
138684
138980
  },
138685
138981
  larkRepair: {
138686
138982
  label: "Repair scopes",
138687
- 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.",
138688
138984
  step: {
138689
- scopes: "Confirm message / resource / panel scopes",
138690
- events: "Confirm long connection and event subscriptions",
138691
- 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",
138692
138988
  verify: "Verify the app can still mint a tenant token"
138693
138989
  },
138694
- done: "Credentials work; add any missing scopes in the Feishu console and publish",
138695
- hint: "Ref {{ref}}\n{{detail}}\nIf scopes are missing, request them on the Feishu open platform and publish; a chat /repair command will follow.",
138696
- 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.",
138697
138993
  notLark: "This row is not a saved Lark bot."
138698
138994
  }
138699
138995
  },
@@ -138732,7 +139028,23 @@ var en_default = {
138732
139028
  removeFailed: "Couldn't delete this bot.",
138733
139029
  status: {
138734
139030
  ready: "Ready",
138735
- 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."
138736
139048
  },
138737
139049
  secret: {
138738
139050
  saved: "Saved. Leave blank to keep it.",
@@ -138744,7 +139056,8 @@ var en_default = {
138744
139056
  format: "This format isn't right."
138745
139057
  },
138746
139058
  publicField: {
138747
- callbackUrl: "Callback URL"
139059
+ callbackUrl: "Callback URL",
139060
+ callbackPath: "Callback path"
138748
139061
  },
138749
139062
  action: {
138750
139063
  running: "Working\u2026",
@@ -138770,6 +139083,11 @@ var en_default = {
138770
139083
  untitled: "New target",
138771
139084
  removeConfirm: "Delete this target",
138772
139085
  removeFailed: "Couldn't delete this target."
139086
+ },
139087
+ publicFieldHint: {
139088
+ callbackNeedsKeys: "Fill in Token and EncodingAESKey and save; the callback URL is derived from them and appears here.",
139089
+ callbackPathNeedsBase: "Put the public address that forwards to this machine in front of this path, or fill in the public callback base URL above to get the full URL.",
139090
+ callbackFollowsKeys: "This URL changes when Token or EncodingAESKey changes. Paste it into WeCom again after changing them."
138773
139091
  }
138774
139092
  },
138775
139093
  team: {
@@ -138955,7 +139273,7 @@ var zh_CN_default = {
138955
139273
  },
138956
139274
  wecomAppCallbackBaseUrl: {
138957
139275
  label: "\u516C\u7F51\u56DE\u8C03\u57FA\u5740\uFF08\u53EF\u9009\uFF09",
138958
- description: "\u586B\u516C\u7F51\u53EF\u8FBE\u7684 http(s) \u57FA\u5740\u540E\uFF0C\u65E5\u5FD7\u4F1A\u6253\u5370\u5B8C\u6574\u56DE\u8C03 URL\uFF0C\u628A\u5B83\u8D34\u8FDB\u4F01\u5FAE\u300C\u63A5\u6536\u6D88\u606F \u2192 \u8BBE\u7F6E API \u63A5\u6536\u300D\u3002\u4E0D\u586B\u53EA\u663E\u793A\u672C\u673A\u8DEF\u5F84\u3002\u9ED8\u8BA4\u76D1\u542C 127.0.0.1\uFF0C\u9700\u53CD\u4EE3\u6216\u96A7\u9053\u3002",
139276
+ description: "\u8F6C\u53D1\u5230\u672C\u673A\u56DE\u8C03\u7AEF\u53E3\u7684\u516C\u7F51\u5730\u5740\u3002\u586B\u4E86\u4E0B\u9762\u5C31\u7ED9\u51FA\u5B8C\u6574\u56DE\u8C03\u5730\u5740\uFF0C\u8D34\u8FDB\u4F01\u4E1A\u5FAE\u4FE1\u300C\u63A5\u6536\u6D88\u606F \u2192 \u8BBE\u7F6E API \u63A5\u6536\u300D\uFF1B\u4E0D\u586B\u53EA\u7ED9\u8DEF\u5F84\u3002",
138959
139277
  placeholder: "https://im.example.com \u6216 http://IP:30987"
138960
139278
  },
138961
139279
  wecomAppApiBaseUrl: {
@@ -138965,9 +139283,14 @@ var zh_CN_default = {
138965
139283
  },
138966
139284
  wecomAppListenPort: {
138967
139285
  label: "\u672C\u673A\u56DE\u8C03\u7AEF\u53E3\uFF08\u53EF\u9009\uFF09",
138968
- description: "\u9ED8\u8BA4 30987\uFF0C\u53EA\u542C 127.0.0.1\u3002\u591A\u53F0\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",
138969
139287
  placeholder: "30987"
138970
139288
  },
139289
+ wecomAppListenHost: {
139290
+ label: "\u672C\u673A\u76D1\u542C\u5730\u5740\uFF08\u53EF\u9009\uFF09",
139291
+ description: "\u9ED8\u8BA4 127.0.0.1\uFF1A\u53EA\u6709\u672C\u673A\u8FDE\u5F97\u4E0A\u56DE\u8C03\u7AEF\u53E3\uFF0C\u9700\u8981\u5728\u524D\u9762\u653E\u53CD\u4EE3\u6216\u96A7\u9053\u3002\u586B 0.0.0.0 \u4F1A\u5728\u6240\u6709\u7F51\u5361\u4E0A\u76D1\u542C\uFF0C\u7B49\u4E8E\u628A\u56DE\u8C03\u7AEF\u53E3\u5BF9\u5916\u5F00\u653E\uFF0C\u8981\u81EA\u5DF1\u7528\u9632\u706B\u5899\u63A7\u597D\u3002",
139292
+ placeholder: "127.0.0.1"
139293
+ },
138971
139294
  wechatBotToken: {
138972
139295
  label: "\u5FAE\u4FE1\u673A\u5668\u4EBA\u4EE4\u724C",
138973
139296
  description: "\u626B\u7801\u7ED1\u5B9A\u540E\u5F97\u5230\u7684 bot_token\u3002\u771F\u767B\u5F55\u5C1A\u672A\u5F00\u653E\uFF1A\u586B\u4E86\u4E5F\u4E0D\u4F1A\u8FDE\u4E0A iLink\uFF0C\u89C1\u63D0\u6848 0069 \u5B89\u5168\u8BC4\u5BA1\u3002"
@@ -138978,6 +139301,7 @@ var zh_CN_default = {
138978
139301
  }
138979
139302
  },
138980
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",
138981
139305
  conclusionRecovered: "\uFF08\u91CD\u542F\u671F\u95F4\u5B8C\u6210\uFF0C\u73B0\u5728\u8865\u53D1\uFF09",
138982
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",
138983
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",
@@ -139001,7 +139325,8 @@ var zh_CN_default = {
139001
139325
  unsupportedType: "\u6211\u8FD8\u4E0D\u80FD\u5904\u7406 {{type}} \u7C7B\u578B\u7684\u6D88\u606F\u3002",
139002
139326
  unsupportedFolder: "\u6587\u4EF6\u5939\u6CA1\u6CD5\u4E0B\u8F7D\uFF0C\u8BF7\u6253\u5305\u6210\u538B\u7F29\u6587\u4EF6\u518D\u53D1\u3002",
139003
139327
  sendingFile: "\u6B63\u5728\u53D1\u9001 {{name}}\uFF1A",
139004
- notAllowedYet: "\u4F60\u8FD8\u4E0D\u5728\u5141\u8BB8\u540D\u5355\u91CC\uFF0C\u4F60\u7684\u7528\u6237 id \u662F {{id}}\uFF0C\u8BF7\u628A\u5B83\u586B\u8FDB \u8BBE\u7F6E \u2192 \u63D2\u4EF6 \u2192 \u804A\u5929 \u2192 \u8FD9\u4E2A\u673A\u5668\u4EBA\u7684\u300C\u5141\u8BB8\u7684\u4EBA\u300D\u3002"
139328
+ notAllowedYet: "\u4F60\u8FD8\u4E0D\u5728\u5141\u8BB8\u540D\u5355\u91CC\uFF0C\u4F60\u7684\u7528\u6237 id \u662F {{id}}\uFF0C\u8BF7\u628A\u5B83\u586B\u8FDB \u8BBE\u7F6E \u2192 \u63D2\u4EF6 \u2192 \u804A\u5929 \u2192 \u8FD9\u4E2A\u673A\u5668\u4EBA\u7684\u300C\u5141\u8BB8\u7684\u4EBA\u300D\u3002",
139329
+ received: "\u6536\u5230\uFF0C\u5728\u5904\u7406\u3002"
139005
139330
  },
139006
139331
  approval: {
139007
139332
  allow: "\u5141\u8BB8",
@@ -139332,13 +139657,16 @@ var zh_CN_default = {
139332
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",
139333
139658
  running: "\u6B63\u5728\u68C0\u67E5 {{rowId}} \u2026",
139334
139659
  done: "\u8FDE\u63A5\u6B63\u5E38",
139335
- failed: "\u8FDE\u63A5\u5931\u8D25\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139660
+ failed: "\u8FDE\u63A5\u5931\u8D25",
139336
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",
139337
139662
  hint: {
139338
- 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",
139339
- 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",
139340
- 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",
139341
- 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"
139342
139670
  }
139343
139671
  },
139344
139672
  copyDiagnostics: {
@@ -139346,24 +139674,24 @@ var zh_CN_default = {
139346
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",
139347
139675
  running: "\u6B63\u5728\u751F\u6210\u8BCA\u65AD {{rowId}} \u2026",
139348
139676
  done: "\u8BCA\u65AD\u5DF2\u751F\u6210\uFF0C\u89C1\u4E0B\u65B9\u8BE6\u60C5",
139349
- failed: "\u8BCA\u65AD\u5931\u8D25\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139677
+ failed: "\u8BCA\u65AD\u5931\u8D25",
139350
139678
  notSaved: "\u8BF7\u5148\u4FDD\u5B58\u8FD9\u4E00\u884C\u518D\u590D\u5236\u8BCA\u65AD\u3002"
139351
139679
  },
139352
139680
  diagnostic: {
139353
- ref: "\u53C2\u8003\u53F7 {{ref}}\n{{detail}}"
139681
+ ref: "\u53C2\u8003\u53F7\uFF1A{{ref}}\n{{detail}}"
139354
139682
  },
139355
139683
  larkRepair: {
139356
139684
  label: "\u8865\u5168\u6743\u9650",
139357
- 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",
139358
139686
  step: {
139359
- scopes: "\u786E\u8BA4\u6D88\u606F / \u8D44\u6E90 / \u9762\u677F\u7B49\u6743\u9650",
139360
- events: "\u786E\u8BA4\u957F\u8FDE\u63A5\u4E0E\u4E8B\u4EF6\u8BA2\u9605",
139361
- 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",
139362
139690
  verify: "\u9A8C\u8BC1\u5E94\u7528\u4ECD\u53EF\u6362\u53D6 tenant token"
139363
139691
  },
139364
- done: "\u51ED\u636E\u53EF\u7528\uFF1B\u82E5\u6743\u9650\u4E0D\u8DB3\u8BF7\u6309\u98DE\u4E66\u63A7\u5236\u53F0\u8865\u9F50\u540E\u53D1\u5E03",
139365
- 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",
139366
- 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",
139367
139695
  notLark: "\u8FD9\u4E00\u884C\u4E0D\u662F\u5DF2\u4FDD\u5B58\u7684\u98DE\u4E66\u673A\u5668\u4EBA\u3002"
139368
139696
  }
139369
139697
  },
@@ -139401,8 +139729,24 @@ var zh_CN_default = {
139401
139729
  copyFailed: "\u6CA1\u590D\u5236\u4E0A\u3002",
139402
139730
  removeFailed: "\u6CA1\u80FD\u5220\u6389\u8FD9\u4E2A\u673A\u5668\u4EBA\u3002",
139403
139731
  status: {
139404
- ready: "\u53EF\u7528",
139405
- 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"
139406
139750
  },
139407
139751
  secret: {
139408
139752
  saved: "\u5DF2\u7ECF\u5B58\u8FC7\uFF0C\u7559\u7A7A\u5C31\u4FDD\u6301\u539F\u503C\u3002",
@@ -139414,7 +139758,8 @@ var zh_CN_default = {
139414
139758
  format: "\u8FD9\u4E2A\u683C\u5F0F\u4E0D\u5BF9\u3002"
139415
139759
  },
139416
139760
  publicField: {
139417
- callbackUrl: "\u56DE\u8C03\u5730\u5740"
139761
+ callbackUrl: "\u56DE\u8C03\u5730\u5740",
139762
+ callbackPath: "\u56DE\u8C03\u8DEF\u5F84"
139418
139763
  },
139419
139764
  action: {
139420
139765
  running: "\u6B63\u5728\u5904\u7406\u2026",
@@ -139440,6 +139785,11 @@ var zh_CN_default = {
139440
139785
  untitled: "\u65B0\u76EE\u6807",
139441
139786
  removeConfirm: "\u5220\u9664\u8FD9\u4E2A\u76EE\u6807",
139442
139787
  removeFailed: "\u8FD9\u4E2A\u76EE\u6807\u6CA1\u5220\u6389\u3002"
139788
+ },
139789
+ publicFieldHint: {
139790
+ callbackNeedsKeys: "\u586B\u597D Token \u548C EncodingAESKey \u5E76\u4FDD\u5B58\u540E\uFF0C\u8FD9\u91CC\u7ED9\u51FA\u56DE\u8C03\u5730\u5740\uFF08\u5730\u5740\u7531\u5B83\u4FE9\u7B97\u51FA\uFF09\u3002",
139791
+ callbackPathNeedsBase: "\u628A\u8F6C\u53D1\u5230\u672C\u673A\u7684\u516C\u7F51\u5730\u5740\u62FC\u5728\u8FD9\u6BB5\u8DEF\u5F84\u524D\u9762\uFF0C\u6216\u5728\u4E0A\u9762\u586B\u516C\u7F51\u56DE\u8C03\u57FA\u5740\uFF0C\u8FD9\u91CC\u5C31\u7ED9\u51FA\u5B8C\u6574\u5730\u5740\u3002",
139792
+ callbackFollowsKeys: "\u6539\u4E86 Token \u6216 EncodingAESKey\uFF0C\u8FD9\u4E2A\u5730\u5740\u4F1A\u53D8\uFF0C\u8981\u91CD\u65B0\u8D34\u8FDB\u4F01\u4E1A\u5FAE\u4FE1\u3002"
139443
139793
  }
139444
139794
  },
139445
139795
  team: {
@@ -139577,6 +139927,16 @@ var makeModelPlan = (core, deps) => {
139577
139927
  // src/notifications.ts
139578
139928
  import { readThreadFailed, threadOrNull as threadOrNull5 } from "@ycodium-ai/plugin-api";
139579
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
+
139580
139940
  // src/conclusionRecovery.ts
139581
139941
  var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
139582
139942
  var recoverConclusions = async (core, ledger, delivery, failures) => {
@@ -139584,6 +139944,12 @@ var recoverConclusions = async (core, ledger, delivery, failures) => {
139584
139944
  core.host.onDispose(() => {
139585
139945
  disposed = true;
139586
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
+ };
139587
139953
  await core.ready;
139588
139954
  for (const debt of Object.values(conclusionDebtsOf(core.state))) {
139589
139955
  if (disposed) return;
@@ -139610,24 +139976,27 @@ var recoverConclusions = async (core, ledger, delivery, failures) => {
139610
139976
  }
139611
139977
  const binding = core.bindingForChatKey(debt.chatKey);
139612
139978
  if (binding === void 0) continue;
139613
- await ledger.deliver(debt, async () => {
139614
- if (latest.state === "error") {
139615
- return delivery.failure(
139616
- binding,
139617
- debt.threadId,
139618
- failures.forFailure(debt.threadId, execution.session?.lastError),
139619
- true,
139620
- latest.turnId
139621
- );
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;
139622
139996
  }
139623
- const send = await delivery.prepareConclusion(debt.threadId, latest.turnId);
139624
- return send(binding, { recovered: true, streaming: false });
139625
139997
  });
139626
139998
  } catch (error) {
139627
- core.host.log.warn("chat could not recover a conclusion", {
139628
- ...debt,
139629
- error: describeError(error)
139630
- });
139999
+ couldNotRecover(debt, error);
139631
140000
  }
139632
140001
  }
139633
140002
  };
@@ -139650,7 +140019,7 @@ var outcomeOfSession = (status) => {
139650
140019
  var THREAD_PROJECT_CACHE_LIMIT = 256;
139651
140020
  var makeNotifications = (core, deps) => {
139652
140021
  const { host, adapterFor, bindingForChatKey, ready } = core;
139653
- const { clearWorking } = deps;
140022
+ const { takeWorking } = deps;
139654
140023
  const { turnCards } = deps.replyCards;
139655
140024
  const { failures, conclusions, delivery } = deps;
139656
140025
  void recoverConclusions(core, conclusions, delivery, failures);
@@ -139673,6 +140042,11 @@ var makeNotifications = (core, deps) => {
139673
140042
  });
139674
140043
  const streamsTo = (binding, threadId) => adapterFor(binding.platform)?.turnCards !== void 0 && owns(binding, threadId);
139675
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
+ };
139676
140050
  const threadsWithSomethingToReport = /* @__PURE__ */ new Set();
139677
140051
  host.reactor({
139678
140052
  name: "im.assistant-messages",
@@ -139695,32 +140069,38 @@ var makeNotifications = (core, deps) => {
139695
140069
  const said = threadsWithSomethingToReport.delete(threadId);
139696
140070
  await ready;
139697
140071
  const targets = targetsForProject(threadId, await projectOfThread(threadId));
139698
- const owes = Object.values(conclusionDebtsOf(core.state)).some(
139699
- (debt2) => debt2.threadId === threadId
139700
- );
139701
- 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);
139702
140073
  if (debt !== void 0) {
139703
140074
  const binding = bindingForChatKey(debt.chatKey);
139704
140075
  if (binding !== void 0 && !streamsTo(binding, threadId) && execution?.session?.activeTurnId == null && execution?.latestTurn?.state !== "running" && execution?.latestTurn?.state !== "error" && payload.session?.status !== "error") {
139705
- await conclusions.deliver(debt, async () => {
139706
- const send = await delivery.prepareConclusion(
139707
- threadId,
139708
- execution?.latestTurn?.turnId ?? null
139709
- );
139710
- 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 });
139711
140080
  });
139712
140081
  }
139713
140082
  } else if (said && !handled) {
139714
140083
  const send = await delivery.prepareConclusion(threadId);
139715
140084
  for (const binding of targets) {
139716
- await send(binding, {
139717
- recovered: false,
139718
- streaming: streamsTo(binding, threadId)
139719
- });
140085
+ const streaming = streamsTo(binding, threadId);
140086
+ enqueueConclusion(
140087
+ core,
140088
+ conclusions,
140089
+ binding,
140090
+ void 0,
140091
+ () => send(binding, { recovered: false, streaming })
140092
+ );
139720
140093
  }
139721
140094
  }
139722
140095
  const outcome = outcomeOfSession(payload.session?.status);
139723
- 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
+ }
139724
140104
  }
139725
140105
  });
139726
140106
  host.events.on("thread.failed", async (event) => {
@@ -139729,7 +140109,7 @@ var makeNotifications = (core, deps) => {
139729
140109
  threadsWithSomethingToReport.delete(threadId);
139730
140110
  await ready;
139731
140111
  const targets = targetsForProject(threadId, await projectOfThread(threadId));
139732
- const { debt, handled } = await conclusions.forTurn(threadId);
140112
+ const { debt, handled, execution } = await conclusions.forTurn(threadId);
139733
140113
  const debtBinding = debt === void 0 ? void 0 : bindingForChatKey(debt.chatKey);
139734
140114
  const recipients = debt !== void 0 ? debtBinding === void 0 ? [] : [debtBinding] : targets;
139735
140115
  const textTargets = handled ? [] : recipients.filter(
@@ -139739,16 +140119,20 @@ var makeNotifications = (core, deps) => {
139739
140119
  const notice = textTargets.length > 0 || drawingTurn !== null ? failures.forFailure(threadId, readThreadFailed(event)?.detail) : null;
139740
140120
  if (notice !== null && drawingTurn !== null)
139741
140121
  failures.holdForCard(threadId, drawingTurn, notice);
139742
- for (const binding of targets) {
139743
- await clearWorking(binding, threadId, "failed");
139744
- }
139745
140122
  if (notice !== null) {
139746
140123
  for (const binding of textTargets) {
139747
- const send = () => delivery.failure(binding, threadId, notice, false);
139748
- if (debt !== void 0) await conclusions.deliver(debt, send);
139749
- else await send();
140124
+ const send = (owed) => delivery.sendText(binding, threadId, notice, false, null, owed);
140125
+ enqueueConclusion(core, conclusions, binding, debt, send);
139750
140126
  }
139751
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
+ }
139752
140136
  });
139753
140137
  const rememberThreadProject = (threadId, projectId) => {
139754
140138
  projectByThread.set(threadId, projectId);
@@ -140309,32 +140693,6 @@ var makeProjectRows = (core) => {
140309
140693
  return { projectRowsFor, listedProjectsByChat };
140310
140694
  };
140311
140695
 
140312
- // src/diagnostics.ts
140313
- var PREFIX = {
140314
- conn: "IM-CONN",
140315
- msg: "IM-MSG",
140316
- setup: "IM-SETUP"
140317
- };
140318
- var shortSalt = () => {
140319
- const bytes = new Uint8Array(2);
140320
- crypto.getRandomValues(bytes);
140321
- return Array.from(bytes, (b) => b.toString(16).toUpperCase().padStart(2, "0")).join("");
140322
- };
140323
- var diagnosticRef = (kind, nowMs = Date.now()) => {
140324
- const stamp = nowMs.toString(36).toUpperCase();
140325
- return `${PREFIX[kind]}-${stamp}-${shortSalt()}`;
140326
- };
140327
- var formatDiagnosticCopy = (report) => [
140328
- `ref=${report.ref}`,
140329
- `ok=${report.ok ? "yes" : "no"}`,
140330
- `platform=${report.platform}`,
140331
- `bot=${report.botName}`,
140332
- `row=${report.rowId}`,
140333
- `stage=${report.stage}`,
140334
- `at=${report.at}`,
140335
- `detail=${report.detail}`
140336
- ].join("\n");
140337
-
140338
140696
  // src/failureNotices.ts
140339
140697
  var makeFailureNotices = (core) => {
140340
140698
  const heldForCard = /* @__PURE__ */ new Map();
@@ -140426,20 +140784,12 @@ var trailingReply = (messages) => {
140426
140784
  var ROTATE = "rotate";
140427
140785
  var isDrawing = (live) => live.card !== null || live.reopen;
140428
140786
  var RECENT_ENDED_LIMIT = 256;
140787
+ var turnKey = (threadId, turnId) => JSON.stringify([threadId, turnId]);
140429
140788
  var makeTurnCards = (deps) => {
140430
140789
  const slots = /* @__PURE__ */ new Map();
140431
- const recentEnded = /* @__PURE__ */ new Map();
140790
+ const recentEnded = makeBoundedSeen(RECENT_ENDED_LIMIT);
140432
140791
  const erroredThreads = /* @__PURE__ */ new Set();
140433
140792
  let disposed = false;
140434
- const rememberEnded = (threadId, turnId) => {
140435
- if (turnId === null) return;
140436
- recentEnded.delete(threadId);
140437
- recentEnded.set(threadId, turnId);
140438
- if (recentEnded.size > RECENT_ENDED_LIMIT) {
140439
- const oldest = recentEnded.keys().next().value;
140440
- if (oldest !== void 0) recentEnded.delete(oldest);
140441
- }
140442
- };
140443
140793
  const readTexts = async (live, progress) => {
140444
140794
  const texts = /* @__PURE__ */ new Map();
140445
140795
  for (const segment of progress.segments) {
@@ -140491,29 +140841,28 @@ var makeTurnCards = (deps) => {
140491
140841
  const turnId = progress.turnId ?? live?.turnId ?? null;
140492
140842
  const target2 = live?.target ?? deps.resolveTarget(threadId, turnId);
140493
140843
  if (target2 === null) return;
140494
- rememberEnded(threadId, turnId);
140495
140844
  if (end.status === "error") erroredThreads.add(threadId);
140496
140845
  const title = live?.title ?? await deps.titleOf(threadId);
140497
140846
  const view = await finalView(title, progress, live, end);
140498
- if (live?.card != null) {
140499
- const card = live.card;
140500
- const sent = await card.finish(view);
140501
- deps.onClosed(card.cardId);
140502
- if (deps.finalize !== void 0)
140503
- await deps.finalize(threadId, turnId, target2.chatKey, async () => sent);
140504
- return;
140505
- }
140506
- const send = async () => {
140507
- const card = await openCard(target2, view);
140508
- return card !== null || await deps.fallback(threadId, turnId, target2.chatKey, view);
140509
- };
140510
- if (deps.finalize === void 0) await send();
140511
- 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();
140512
140861
  };
140513
140862
  const handle = async (slot, threadId, item) => {
140514
140863
  await deps.ready;
140515
140864
  if (disposed) return;
140516
- if (item.turnId !== null && recentEnded.get(threadId) === item.turnId) return;
140865
+ if (item.turnId !== null && recentEnded.has(turnKey(threadId, item.turnId))) return;
140517
140866
  let live = slot.live;
140518
140867
  if (live !== null) {
140519
140868
  const sameTurn = item.turnId === null || live.turnId === null || live.turnId === item.turnId;
@@ -140597,7 +140946,11 @@ var makeTurnCards = (deps) => {
140597
140946
  }
140598
140947
  } finally {
140599
140948
  slot.running = false;
140600
- 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
+ }
140601
140954
  }
140602
140955
  };
140603
140956
  return {
@@ -140606,7 +140959,7 @@ var makeTurnCards = (deps) => {
140606
140959
  if (disposed) return;
140607
140960
  let slot = slots.get(threadId);
140608
140961
  if (slot === void 0) {
140609
- slot = { queue: [], running: false, live: null };
140962
+ slot = { queue: [], running: false, live: null, afterTurn: /* @__PURE__ */ new Map() };
140610
140963
  slots.set(threadId, slot);
140611
140964
  }
140612
140965
  const last = slot.queue.at(-1);
@@ -140620,6 +140973,18 @@ var makeTurnCards = (deps) => {
140620
140973
  void pump(threadId, slot);
140621
140974
  }
140622
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
+ },
140623
140988
  /**
140624
140989
  * 审批或提问答完了:封存这条会话开着的卡,下一拍另开一张。立刻返回;没有在画的卡、或
140625
140990
  * 平台不换卡,就什么都不做。
@@ -140646,7 +141011,10 @@ var makeTurnCards = (deps) => {
140646
141011
  /** 插件停用:放下所有在画的卡,不再发任何更新。卡会停在最后一拍,重启时被标「已中断」。 */
140647
141012
  dispose: () => {
140648
141013
  disposed = true;
140649
- 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
+ }
140650
141018
  slots.clear();
140651
141019
  }
140652
141020
  };
@@ -140654,7 +141022,7 @@ var makeTurnCards = (deps) => {
140654
141022
 
140655
141023
  // src/replyCards.ts
140656
141024
  var makeReplyCards = (core, failures, anchors, conclusions, delivery) => {
140657
- const { host, adapterFor, commit, deliver, bindingForChatKey, bindingOfThread, ready } = core;
141025
+ const { host, adapterFor, commit, bindingForChatKey, bindingOfThread, ready } = core;
140658
141026
  const stopRequested = /* @__PURE__ */ new Set();
140659
141027
  const turnCards = makeTurnCards({
140660
141028
  ready,
@@ -140713,22 +141081,28 @@ var makeReplyCards = (core, failures, anchors, conclusions, delivery) => {
140713
141081
  onClosed: (cardId) => commit("turn-card-closed", { cardId }),
140714
141082
  finalize: async (threadId, turnId, chatKey, send) => {
140715
141083
  const { debt, handled } = await conclusions.forTurn(threadId, turnId);
140716
- if (handled) return;
140717
- if (debt === void 0) {
140718
- await send();
140719
- return;
140720
- }
140721
- 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;
140722
141087
  const binding = bindingForChatKey(chatKey);
140723
- if (binding === void 0) return;
141088
+ if (binding === void 0) return true;
140724
141089
  const sendConclusion = await delivery.prepareConclusion(threadId, turnId);
140725
141090
  await sendConclusion(binding, { recovered: false, streaming: true });
141091
+ return true;
140726
141092
  },
140727
141093
  fallback: async (threadId, turnId, chatKey, view) => {
141094
+ const { debt, handled } = await conclusions.forTurn(threadId, turnId);
141095
+ if (handled) return;
140728
141096
  const binding = bindingForChatKey(chatKey);
140729
- if (binding === void 0) return false;
141097
+ if (binding === void 0) return;
140730
141098
  const text4 = [view.body, view.errorText ?? ""].filter((part) => part !== "").join("\n\n");
140731
- 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
+ );
140732
141106
  },
140733
141107
  schedule: (run, delayMs) => {
140734
141108
  const timer = setTimeout(run, delayMs);
@@ -141098,7 +141472,7 @@ var makeSessions = (core) => {
141098
141472
  };
141099
141473
 
141100
141474
  // src/teamGoals.ts
141101
- import * as NodeCrypto3 from "node:crypto";
141475
+ import * as NodeCrypto4 from "node:crypto";
141102
141476
  var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
141103
141477
  var teamGoalRequestId = (binding, messageId) => {
141104
141478
  const identity = JSON.stringify([
@@ -141108,7 +141482,7 @@ var teamGoalRequestId = (binding, messageId) => {
141108
141482
  binding.chatId,
141109
141483
  messageId
141110
141484
  ]);
141111
- return NodeCrypto3.createHash("sha256").update(identity).digest("hex");
141485
+ return NodeCrypto4.createHash("sha256").update(identity).digest("hex");
141112
141486
  };
141113
141487
  var makeTeamGoals = (core) => {
141114
141488
  const { host, t, deliver, commit } = core;
@@ -141129,14 +141503,16 @@ var makeTeamGoals = (core) => {
141129
141503
  };
141130
141504
  const startTeamGoal = async (binding, message) => {
141131
141505
  try {
141506
+ const requestId = teamGoalRequestId(binding, message.messageId);
141507
+ const topicRoot = await message.topicRootForSession?.(requestId);
141132
141508
  const result = await host.plugins.invoke({
141133
141509
  pluginId: "team",
141134
141510
  command: "startGoal",
141135
141511
  payload: {
141136
141512
  projectId: binding.projectId,
141137
- goal: textWithInboundContext(message, binding.rowId, t),
141513
+ goal: textWithInboundContext(message, binding.rowId, t, topicRoot?.reference),
141138
141514
  titleSeed: message.text,
141139
- requestId: teamGoalRequestId(binding, message.messageId)
141515
+ requestId
141140
141516
  }
141141
141517
  });
141142
141518
  if (isRecord2(result) && result._tag === "TeamGoalRejection") {
@@ -141151,6 +141527,7 @@ var makeTeamGoals = (core) => {
141151
141527
  if (!isRecord2(result) || typeof result.leadThreadId !== "string" || result.leadThreadId === "" || result.leadName !== null && typeof result.leadName !== "string") {
141152
141528
  throw new Error("team startGoal returned an invalid result");
141153
141529
  }
141530
+ topicRoot?.confirmConsumed();
141154
141531
  commit("team-goal-started", { chatKey: chatKeyOf(binding), threadId: result.leadThreadId });
141155
141532
  await deliver(
141156
141533
  binding,
@@ -141206,6 +141583,10 @@ var TelegramApiError = class extends Error {
141206
141583
  this.retryAfterSeconds = retryAfterSeconds;
141207
141584
  }
141208
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;
141209
141590
  var isNotModified = (error) => error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("message is not modified");
141210
141591
  var PHOTO_NAME = "photo.jpg";
141211
141592
  var PHOTO_MIME = "image/jpeg";
@@ -141412,9 +141793,13 @@ var makeTelegramApi = (input) => {
141412
141793
  },
141413
141794
  POLL_CALL_TIMEOUT_MS
141414
141795
  ),
141415
- sendMessage: async (chatId, text4) => {
141796
+ sendMessage: async (chatId, text4, retry) => {
141416
141797
  for (const chunk of splitForTelegram(text4)) {
141417
- await call("sendMessage", { ...target(chatId), text: chunk });
141798
+ await sendWithRetry(
141799
+ () => call("sendMessage", { ...target(chatId), text: chunk }),
141800
+ telegramSendVerdict,
141801
+ retry
141802
+ );
141418
141803
  }
141419
141804
  },
141420
141805
  /**
@@ -141852,6 +142237,7 @@ var makeTelegramAdapter = (deps) => {
141852
142237
  const poll = async () => {
141853
142238
  for (; ; ) {
141854
142239
  if (!running) return;
142240
+ let checkingConnection = true;
141855
142241
  try {
141856
142242
  const api = await apiFor(binding);
141857
142243
  liveApi = api;
@@ -141860,6 +142246,7 @@ var makeTelegramAdapter = (deps) => {
141860
142246
  const own = await api.getMe();
141861
142247
  botUsername = own.username ?? null;
141862
142248
  botId = own.id;
142249
+ if (running) handlers.onConnectionState?.({ state: "connected" });
141863
142250
  }
141864
142251
  if (!running) return;
141865
142252
  if (!menuSent) {
@@ -141874,6 +142261,8 @@ var makeTelegramAdapter = (deps) => {
141874
142261
  acknowledged = offset;
141875
142262
  const updates = await api.getUpdates(offset);
141876
142263
  if (!running) return;
142264
+ handlers.onConnectionState?.({ state: "connected" });
142265
+ checkingConnection = false;
141877
142266
  backoff = BACKOFF_START_MS;
141878
142267
  for (const update of updates) {
141879
142268
  if (!running) break;
@@ -141958,6 +142347,7 @@ var makeTelegramAdapter = (deps) => {
141958
142347
  }
141959
142348
  } catch (error) {
141960
142349
  if (!running) return;
142350
+ if (checkingConnection) handlers.onConnectionState?.({ state: "failed", error });
141961
142351
  handlers.onError(error instanceof Error ? error.message : String(error));
141962
142352
  await Promise.race([(deps.sleep ?? defaultSleep)(backoff), stopped]);
141963
142353
  backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
@@ -141975,9 +142365,9 @@ var makeTelegramAdapter = (deps) => {
141975
142365
  }
141976
142366
  };
141977
142367
  },
141978
- sendText: async (binding, text4) => {
142368
+ sendText: async (binding, text4, options) => {
141979
142369
  const api = await apiFor(binding);
141980
- await api.sendMessage(binding.chatId, text4);
142370
+ await api.sendMessage(binding.chatId, text4, options?.retry);
141981
142371
  },
141982
142372
  checkConnection: async (bot) => {
141983
142373
  const token = tokenOf(bot);
@@ -142236,6 +142626,7 @@ var makeTurns = (core, deps) => {
142236
142626
  const text4 = message.text;
142237
142627
  const threadId = await ensureThread(binding, text4 || message.resources[0]?.name || "Chat");
142238
142628
  if (threadId === null) return null;
142629
+ const topicRoot = await message.topicRootForSession?.(threadId);
142239
142630
  const carried = waitingFor(chatKey, threadId);
142240
142631
  pendingByChat.delete(chatKey);
142241
142632
  const own = await collectResources(
@@ -142273,7 +142664,10 @@ var makeTurns = (core, deps) => {
142273
142664
  message: {
142274
142665
  messageId: userMessageId,
142275
142666
  role: "user",
142276
- text: [textWithInboundContext(message, binding.rowId, t), ...notes].join("\n"),
142667
+ text: [
142668
+ textWithInboundContext(message, binding.rowId, t, topicRoot?.reference),
142669
+ ...notes
142670
+ ].join("\n"),
142277
142671
  attachments: attachments.map(
142278
142672
  (attachment) => attachment.type === "image" ? { ...attachment, type: "image" } : { ...attachment, type: "file" }
142279
142673
  )
@@ -142283,6 +142677,7 @@ var makeTurns = (core, deps) => {
142283
142677
  interactionMode: interactionModeOf(chatKey),
142284
142678
  ...intentAction !== void 0 ? { intentAction } : {}
142285
142679
  });
142680
+ topicRoot?.confirmConsumed();
142286
142681
  if (intentAction?.kind !== "steer") {
142287
142682
  deps.conclusions.owe({
142288
142683
  chatKey,
@@ -142411,8 +142806,15 @@ var ROW_ACTION_ITEM_SCHEMA = makeFlowItemSchema({
142411
142806
  doneRequired: ["messageKey", "fills"]
142412
142807
  });
142413
142808
  var botOf = (config2, adapters, rowId) => {
142414
- const adapterFor = (platform) => adapters.find((adapter) => adapter.platform === platform);
142415
- 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 };
142416
142818
  };
142417
142819
  var reportOf = (bot, stage, ok, detail, hint) => ({
142418
142820
  ref: diagnosticRef("conn"),
@@ -142428,12 +142830,9 @@ var reportOf = (bot, stage, ok, detail, hint) => ({
142428
142830
  var probe = async (adapters, bot) => {
142429
142831
  const adapter = adapters.find((candidate) => candidate.platform === bot.platform);
142430
142832
  if (adapter?.checkConnection === void 0) {
142431
- return reportOf(bot, "platform", false, `unsupported platform ${bot.platform}`);
142432
- }
142433
- const redactor = makeRedactor();
142434
- for (const column of adapter.columns) {
142435
- if (column.kind === "password") redactor.register(bot.values[column.key]);
142833
+ return reportOf(bot, "platform", false, `unsupported platform ${bot.platform}`, "notAvailable");
142436
142834
  }
142835
+ const redactor = botRedactor(adapter.columns, bot.values);
142437
142836
  try {
142438
142837
  const result = await adapter.checkConnection(bot);
142439
142838
  return reportOf(
@@ -142458,10 +142857,15 @@ var emitReport = (emit, report, action) => {
142458
142857
  });
142459
142858
  return;
142460
142859
  }
142860
+ const suggested = report.hint !== void 0 && action.keyPrefix === "rowAction.checkConnection";
142461
142861
  emit({
142462
142862
  kind: "failed",
142463
- reasonKey: report.hint !== void 0 && action.keyPrefix === "rowAction.checkConnection" ? `rowAction.checkConnection.hint.${report.hint}` : `${action.keyPrefix}.failed`,
142464
- 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
+ },
142465
142869
  detail
142466
142870
  });
142467
142871
  };
@@ -142513,9 +142917,9 @@ var registerSettingsRowActions = (ctx, adapters) => {
142513
142917
  send({
142514
142918
  kind: "steps",
142515
142919
  steps: [
142516
- { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "pending" },
142517
- { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "pending" },
142518
- { 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" },
142519
142923
  { id: "verify", labelKey: "rowAction.larkRepair.step.verify", state: "running" }
142520
142924
  ]
142521
142925
  });
@@ -142532,9 +142936,9 @@ var registerSettingsRowActions = (ctx, adapters) => {
142532
142936
  send({
142533
142937
  kind: "steps",
142534
142938
  steps: [
142535
- { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "done" },
142536
- { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "done" },
142537
- { 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" },
142538
142942
  {
142539
142943
  id: "verify",
142540
142944
  labelKey: "rowAction.larkRepair.step.verify",
@@ -142550,7 +142954,7 @@ var registerSettingsRowActions = (ctx, adapters) => {
142550
142954
  warnings: [
142551
142955
  {
142552
142956
  key: "rowAction.larkRepair.hint",
142553
- params: { ref, detail: formatDiagnosticCopy({ ...report, ref }) }
142957
+ params: { ref, detail: formatDiagnosticCopy(report) }
142554
142958
  }
142555
142959
  ]
142556
142960
  });
@@ -142560,7 +142964,7 @@ var registerSettingsRowActions = (ctx, adapters) => {
142560
142964
  kind: "failed",
142561
142965
  reasonKey: "rowAction.larkRepair.failed",
142562
142966
  params: { ref },
142563
- detail: formatDiagnosticCopy({ ...report, ref })
142967
+ detail: formatDiagnosticCopy(report)
142564
142968
  });
142565
142969
  }
142566
142970
  });
@@ -142760,8 +143164,29 @@ var generalWriteOptions = (payload, next) => {
142760
143164
  }
142761
143165
  return void 0;
142762
143166
  };
142763
- var viewAfterWrite = async (ctx, adapters) => buildSettingsView(await ctx.settings.read(), adapters);
142764
- 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
+ });
142765
143190
  ctx.command({
142766
143191
  name: SETTINGS_VIEW_COMMAND,
142767
143192
  description: "Read the chat-bridge settings view, bot columns, and this machine's proxies.",
@@ -142788,7 +143213,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142788
143213
  return { result, write: { config: next, options: { secrets, clearSecrets } } };
142789
143214
  });
142790
143215
  if (!edited.ok) return edited;
142791
- return { ok: true, rowId: edited.rowId, view: await viewAfterWrite(ctx, adapters) };
143216
+ return { ok: true, rowId: edited.rowId, view: await viewAfterWrite() };
142792
143217
  };
142793
143218
  const removeRow2 = async (remove) => {
142794
143219
  const removed = await updateSettings((config2) => {
@@ -142796,7 +143221,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142796
143221
  return next === null ? { result: false } : { result: true, write: { config: next } };
142797
143222
  });
142798
143223
  if (!removed) return { ok: false, code: "unknown-row" };
142799
- return { ok: true, view: await viewAfterWrite(ctx, adapters) };
143224
+ return { ok: true, view: await viewAfterWrite() };
142800
143225
  };
142801
143226
  ctx.command({
142802
143227
  name: SETTINGS_SAVE_BOT_COMMAND,
@@ -142847,7 +143272,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142847
143272
  write: options === void 0 ? { config: next } : { config: next, options }
142848
143273
  };
142849
143274
  });
142850
- return { ok: true, view: await viewAfterWrite(ctx, adapters) };
143275
+ return { ok: true, view: await viewAfterWrite() };
142851
143276
  }
142852
143277
  });
142853
143278
  };
@@ -142875,12 +143300,13 @@ var makeWechatAdapter = (deps) => {
142875
143300
  columns: COLUMNS3,
142876
143301
  validate: wechatConfigIssues,
142877
143302
  isConfigured: (bot) => wechatConfigIssues(bot).length === 0,
142878
- connect: (bot, _handlers) => {
143303
+ connect: (bot, handlers) => {
142879
143304
  deps.log.warn("wechat iLink connect is stubbed pending security-review gate (YCO-236)", {
142880
143305
  bot: bot.displayName,
142881
143306
  hasToken: tokenOf2(bot) !== "",
142882
143307
  textLimit: WECHAT_TEXT_MAX_CHARS
142883
143308
  });
143309
+ handlers.onConnectionState?.({ state: "failed", reason: "not-available" });
142884
143310
  return { close: () => void 0 };
142885
143311
  },
142886
143312
  sendText: async (binding, _text) => {
@@ -142903,7 +143329,7 @@ var makeWechatAdapter = (deps) => {
142903
143329
  };
142904
143330
 
142905
143331
  // src/wecomUpload.ts
142906
- import * as NodeCrypto4 from "node:crypto";
143332
+ import * as NodeCrypto5 from "node:crypto";
142907
143333
  var WECOM_CHUNK_BYTES = 512 * 1024;
142908
143334
  var WECOM_MAX_CHUNKS = 100;
142909
143335
  var WECOM_MIN_UPLOAD_BYTES = 5;
@@ -142936,7 +143362,7 @@ var uploadWecomMedia = async (input) => {
142936
143362
  filename: clampName(input.filename),
142937
143363
  total_size: bytes.byteLength,
142938
143364
  total_chunks: totalChunks,
142939
- md5: NodeCrypto4.createHash("md5").update(bytes).digest("hex")
143365
+ md5: NodeCrypto5.createHash("md5").update(bytes).digest("hex")
142940
143366
  });
142941
143367
  const uploadId = init.upload_id;
142942
143368
  if (typeof uploadId !== "string" || uploadId === "") {
@@ -143023,7 +143449,7 @@ var wecomReplyReference = (quote) => {
143023
143449
  };
143024
143450
 
143025
143451
  // src/wecomApi.ts
143026
- var DEFAULT_WECOM_ENDPOINT = "wss://openws.work.weixin.qq.com/wssvr/";
143452
+ var DEFAULT_WECOM_ENDPOINT = "wss://openws.work.weixin.qq.com";
143027
143453
  var PING_INTERVAL_MS = 3e4;
143028
143454
  var MAX_MISSED_PONGS = 2;
143029
143455
  var SUBSCRIBE_TIMEOUT_MS = 1e4;
@@ -143160,9 +143586,10 @@ var runWecomClient = (input) => {
143160
143586
  const message = parseWecomMessage(frame, input.botId);
143161
143587
  if (message !== null) input.handlers.onMessage(message);
143162
143588
  };
143163
- const finish = () => {
143589
+ const finish = (error = new Error("WeCom connection closed"), reason = "disconnected") => {
143164
143590
  if (finished) return;
143165
143591
  finished = true;
143592
+ if (!closed) input.handlers.onConnectionState?.({ state: "failed", error, reason });
143166
143593
  clearInterval(heartbeat);
143167
143594
  openStreams.clear();
143168
143595
  void input.socket.close().catch(() => void 0);
@@ -143180,6 +143607,7 @@ var runWecomClient = (input) => {
143180
143607
  if (!closed) {
143181
143608
  input.handlers.onError(error instanceof Error ? error.message : String(error));
143182
143609
  }
143610
+ finish(error);
143183
143611
  }
143184
143612
  finish();
143185
143613
  };
@@ -143187,7 +143615,7 @@ var runWecomClient = (input) => {
143187
143615
  if (finished) return;
143188
143616
  if (missedPong >= MAX_MISSED_PONGS) {
143189
143617
  input.handlers.onError("socket hang up: missed pong");
143190
- finish();
143618
+ finish(new Error("socket hang up: missed pong"));
143191
143619
  return;
143192
143620
  }
143193
143621
  missedPong += 1;
@@ -143208,10 +143636,13 @@ var runWecomClient = (input) => {
143208
143636
  settleAck(subscribeId, error instanceof Error ? error : new Error(String(error)));
143209
143637
  });
143210
143638
  const ready = subscribed.then(
143211
- () => true,
143639
+ () => {
143640
+ if (!finished && !closed) input.handlers.onConnectionState?.({ state: "connected" });
143641
+ return !finished && !closed;
143642
+ },
143212
143643
  (error) => {
143213
143644
  if (!closed) input.handlers.onError(error instanceof Error ? error.message : String(error));
143214
- finish();
143645
+ finish(error, "subscription-failed");
143215
143646
  return false;
143216
143647
  }
143217
143648
  );
@@ -143346,7 +143777,7 @@ var runWecomClient = (input) => {
143346
143777
  };
143347
143778
 
143348
143779
  // src/wecomMedia.ts
143349
- import * as NodeCrypto5 from "node:crypto";
143780
+ import * as NodeCrypto6 from "node:crypto";
143350
143781
 
143351
143782
  // src/wecomPadding.ts
143352
143783
  var PADDING_BLOCK = 32;
@@ -143377,7 +143808,7 @@ var decryptWecomMedia = (cipher, aesKey) => {
143377
143808
  if (cipher.byteLength === 0 || cipher.byteLength % AES_BLOCK !== 0) {
143378
143809
  throw new WecomDecryptError("the ciphertext is not a whole number of blocks");
143379
143810
  }
143380
- 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));
143381
143812
  decipher.setAutoPadding(false);
143382
143813
  const plain = Buffer.concat([decipher.update(cipher), decipher.final()]);
143383
143814
  const unpadded = stripPadding(plain);
@@ -143520,6 +143951,9 @@ ${kept}`;
143520
143951
  botSecret,
143521
143952
  handlers: {
143522
143953
  onError: handlers.onError,
143954
+ onConnectionState: (state) => {
143955
+ if (!stopped) handlers.onConnectionState?.(state);
143956
+ },
143523
143957
  onMessage: (message) => {
143524
143958
  if (message.chatType !== "single") return;
143525
143959
  client.rememberRequest(message.chatId, message.requestId);
@@ -143561,6 +143995,8 @@ ${kept}`;
143561
143995
  try {
143562
143996
  if (await runOnce()) backoff = BACKOFF_START_MS2;
143563
143997
  } catch (error) {
143998
+ if (stopped) return;
143999
+ handlers.onConnectionState?.({ state: "failed", error, reason: "disconnected" });
143564
144000
  handlers.onError(error instanceof Error ? error.message : String(error));
143565
144001
  }
143566
144002
  if (stopped) return;
@@ -143578,6 +144014,7 @@ ${kept}`;
143578
144014
  }
143579
144015
  };
143580
144016
  },
144017
+ // 不看 `options.retry`:长连接的回执没有核实过的限流码,也没有幂等键,失败一律说不清发没发出去。
143581
144018
  sendText: async (binding, text4) => {
143582
144019
  const client = clients.get(binding.rowId);
143583
144020
  if (client === void 0) {
@@ -143594,12 +144031,36 @@ ${kept}`;
143594
144031
  detail: "missing bot id or secret in saved config"
143595
144032
  };
143596
144033
  }
144034
+ let stage = "websocket";
143597
144035
  try {
143598
144036
  const socket = await deps.connectSocket(endpoint);
143599
- await socket.close();
143600
- 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
+ }
143601
144062
  } catch (error) {
143602
- return { stage: "websocket", ok: false, detail: describeError(error) };
144063
+ return { stage, ok: false, detail: describeError(error) };
143603
144064
  }
143604
144065
  },
143605
144066
  askApproval: async (binding, request) => {
@@ -143656,18 +144117,25 @@ ${kept}`;
143656
144117
  // src/wecomAppApi.ts
143657
144118
  var WECOM_APP_DEFAULT_API_BASE = "https://qyapi.weixin.qq.com";
143658
144119
  var WECOM_APP_TEXT_MAX_BYTES = 2048;
144120
+ var REQUEST_TIMEOUT_MS = 15e3;
144121
+ var MEDIA_TIMEOUT_MS = 6e4;
143659
144122
  var WecomAppApiError = class extends Error {
143660
144123
  code;
143661
144124
  providerCode;
143662
144125
  hint;
144126
+ invalidUsers;
143663
144127
  constructor(code2, message, extras) {
143664
144128
  super(message);
143665
144129
  this.name = "WecomAppApiError";
143666
144130
  this.code = code2;
143667
144131
  this.providerCode = extras?.providerCode;
143668
144132
  this.hint = extras?.hint;
144133
+ this.invalidUsers = extras?.invalidUsers ?? [];
143669
144134
  }
143670
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;
143671
144139
  var cleanString2 = (value2) => {
143672
144140
  if (typeof value2 !== "string") return null;
143673
144141
  const trimmed = value2.trim();
@@ -143703,6 +144171,7 @@ var splitUtf8ByBytes = (text4, maxBytes = WECOM_APP_TEXT_MAX_BYTES) => {
143703
144171
  if (current.length > 0) chunks.push(current);
143704
144172
  return chunks;
143705
144173
  };
144174
+ var parseInvalidUsers = (value2) => typeof value2 === "string" ? value2.split("|").map((user) => user.trim()).filter(Boolean) : [];
143706
144175
  var WecomAppApi = class {
143707
144176
  corpId;
143708
144177
  corpSecret;
@@ -143754,9 +144223,13 @@ var WecomAppApi = class {
143754
144223
  async sendText(input) {
143755
144224
  const chunks = splitUtf8ByBytes(input.content);
143756
144225
  for (const chunk of chunks) {
143757
- await this.#send(
143758
- { touser: input.userId, msgtype: "text", text: { content: chunk } },
143759
- 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
143760
144233
  );
143761
144234
  }
143762
144235
  }
@@ -143783,12 +144256,16 @@ var WecomAppApi = class {
143783
144256
  new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }),
143784
144257
  input.name
143785
144258
  );
143786
- const payload = await this.#requestJson(url, {
143787
- method: "POST",
143788
- // 不设 content-type:multipart 的 boundary 要让 fetch 自己写。
143789
- body: form,
143790
- ...input.signal === void 0 ? {} : { signal: input.signal }
143791
- });
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
+ );
143792
144269
  const mediaId = payload["media_id"];
143793
144270
  if (typeof mediaId === "string" && mediaId !== "") return mediaId;
143794
144271
  throw this.#apiError(Number(payload["errcode"] ?? -1), payload["errmsg"]);
@@ -143802,10 +144279,17 @@ var WecomAppApi = class {
143802
144279
  const url = new URL("/cgi-bin/media/get", this.apiBaseUrl);
143803
144280
  url.searchParams.set("access_token", token);
143804
144281
  url.searchParams.set("media_id", input.mediaId);
143805
- const response = await this.#fetch(url, {
143806
- method: "GET",
143807
- ...input.signal === void 0 ? {} : { signal: input.signal }
143808
- });
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) {
143809
144293
  const contentType = response.headers.get("content-type") ?? "";
143810
144294
  if (contentType.includes("application/json") || contentType.includes("text/plain")) {
143811
144295
  let payload = {};
@@ -143825,7 +144309,7 @@ var WecomAppApi = class {
143825
144309
  `WeCom media/get failed (HTTP ${String(response.status)}).`
143826
144310
  );
143827
144311
  }
143828
- const bytes = await readBodyLimited(response, input.maxBytes);
144312
+ const bytes = await readBodyLimited(response, maxBytes);
143829
144313
  const name = filenameFromContentDisposition(response.headers.get("content-disposition"));
143830
144314
  const mimeType = contentType.split(";")[0]?.trim();
143831
144315
  return {
@@ -143844,63 +144328,85 @@ var WecomAppApi = class {
143844
144328
  body: JSON.stringify({ ...message, agentid: Number.parseInt(this.agentId, 10) }),
143845
144329
  ...signal === void 0 ? {} : { signal }
143846
144330
  });
143847
- const errcode = Number(payload["errcode"] ?? 0);
143848
- if (errcode !== 0) throw this.#apiError(errcode, payload["errmsg"]);
143849
144331
  const invalid = payload["invaliduser"];
143850
144332
  if (typeof invalid === "string" && invalid.trim() !== "") {
143851
144333
  throw new WecomAppApiError("invalid-user", `WeCom did not deliver to: ${invalid.trim()}.`, {
143852
144334
  providerCode: "81013",
143853
- 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)
143854
144337
  });
143855
144338
  }
143856
144339
  }
143857
- #apiError(errcode, errmsg) {
144340
+ #apiError(errcode, errmsg, invalidUsers) {
143858
144341
  const code2 = String(errcode);
143859
144342
  const message = typeof errmsg === "string" && errmsg !== "" ? `WeCom API failed (${code2}): ${errmsg}` : `WeCom API failed (${code2}).`;
143860
- if (errcode === 40014 || errcode === 42001) this.invalidateToken();
144343
+ if (TOKEN_INVALID_CODES.has(code2)) this.invalidateToken();
143861
144344
  if (errcode === 60020) {
143862
144345
  return new WecomAppApiError("trusted-ip", "WeCom rejected the outbound IP (errcode 60020).", {
143863
144346
  providerCode: code2,
143864
- 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"
143865
144348
  });
143866
144349
  }
143867
144350
  if (errcode === 81013) {
143868
144351
  return new WecomAppApiError("invalid-user", message, {
143869
144352
  providerCode: code2,
143870
- 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)
143871
144355
  });
143872
144356
  }
143873
144357
  if (errcode === 40056) {
143874
144358
  return new WecomAppApiError("invalid-agent", message, {
143875
144359
  providerCode: code2,
143876
- hint: "AgentId \u4E0D\u5BF9\uFF0C\u6838\u5BF9\u81EA\u5EFA\u5E94\u7528\u7684 AgentId\u3002"
144360
+ hint: "wecomInvalidAgent"
143877
144361
  });
143878
144362
  }
143879
144363
  return new WecomAppApiError("api-failed", message, { providerCode: code2 });
143880
144364
  }
143881
- async #requestJson(url, init) {
143882
- const response = await this.#fetch(url, init);
143883
- 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);
143884
144373
  try {
143885
- payload = await response.json();
143886
- } catch {
143887
- throw new WecomAppApiError(
143888
- "bad-response",
143889
- `WeCom returned non-JSON (HTTP ${String(response.status)}).`
143890
- );
143891
- }
143892
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
143893
- 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);
143894
144380
  }
143895
- 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
+ });
143896
144401
  }
143897
144402
  };
143898
144403
 
143899
144404
  // src/wecomAppCallback.ts
143900
144405
  import * as NodeHttp2 from "node:http";
144406
+ import * as NodeNet from "node:net";
143901
144407
 
143902
144408
  // src/wecomAppCrypto.ts
143903
- import * as NodeCrypto6 from "node:crypto";
144409
+ import * as NodeCrypto7 from "node:crypto";
143904
144410
  var AES_KEY_BYTES = 32;
143905
144411
  var RANDOM_BYTES = 16;
143906
144412
  var WecomAppCryptoError = class extends Error {
@@ -143923,13 +144429,13 @@ var timingSafeEqualString2 = (a, b) => {
143923
144429
  const left = Buffer.from(a);
143924
144430
  const right = Buffer.from(b);
143925
144431
  if (left.length === 0 || left.length !== right.length) return false;
143926
- return NodeCrypto6.timingSafeEqual(left, right);
144432
+ return NodeCrypto7.timingSafeEqual(left, right);
143927
144433
  };
143928
- 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");
143929
144435
  var verifySignature = (token, timestamp, nonce, encrypt, signature) => timingSafeEqualString2(computeSignature(token, timestamp, nonce, encrypt), signature);
143930
144436
  var decrypt = (encodingAESKey, encryptText, expectReceiveId) => {
143931
144437
  const { key, iv } = parseEncodingAESKey(encodingAESKey);
143932
- const decipher = NodeCrypto6.createDecipheriv("aes-256-cbc", key, iv);
144438
+ const decipher = NodeCrypto7.createDecipheriv("aes-256-cbc", key, iv);
143933
144439
  decipher.setAutoPadding(false);
143934
144440
  let deciphered;
143935
144441
  try {
@@ -143988,7 +144494,7 @@ var createWecomAppCrypto = (input) => {
143988
144494
  decrypt: (encryptText) => decrypt(encodingAESKey, encryptText, corpId).message
143989
144495
  };
143990
144496
  };
143991
- 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);
143992
144498
 
143993
144499
  // src/wecomAppParse.ts
143994
144500
  var isRecord4 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
@@ -144060,6 +144566,10 @@ var parseWecomAppPlainMessage = (raw) => {
144060
144566
 
144061
144567
  // src/wecomAppCallback.ts
144062
144568
  var WECOM_APP_DEFAULT_CALLBACK_HOST = "127.0.0.1";
144569
+ var parseListenHost = (raw) => {
144570
+ if (raw === "") return WECOM_APP_DEFAULT_CALLBACK_HOST;
144571
+ return NodeNet.isIPv4(raw) ? raw : null;
144572
+ };
144063
144573
  var WECOM_APP_DEFAULT_CALLBACK_PORT = 30987;
144064
144574
  var WECOM_APP_CALLBACK_PATH_PREFIX = "/wecom-app-callback";
144065
144575
  var MAX_BODY_BYTES2 = 1 * 1024 * 1024;
@@ -144318,7 +144828,8 @@ var wecomAppCredentials = (bot) => ({
144318
144828
  encodingAESKey: value(bot, "wecomAppEncodingAESKey"),
144319
144829
  callbackBaseUrl: value(bot, "wecomAppCallbackBaseUrl").replace(/\/+$/u, ""),
144320
144830
  apiBaseUrl: value(bot, "wecomAppApiBaseUrl"),
144321
- listenPortRaw: value(bot, "wecomAppListenPort")
144831
+ listenPortRaw: value(bot, "wecomAppListenPort"),
144832
+ listenHostRaw: value(bot, "wecomAppListenHost")
144322
144833
  });
144323
144834
  var validCorpId = (id) => /^[A-Za-z][A-Za-z0-9_-]{2,127}$/u.test(id);
144324
144835
  var validAgentId = (id) => /^\d{1,32}$/u.test(id) && Number(id) > 0;
@@ -144361,14 +144872,18 @@ var validateWecomAppBot = (bot) => {
144361
144872
  if (parseListenPort(c.listenPortRaw) === null) {
144362
144873
  issues.push({ column: "wecomAppListenPort", code: "format" });
144363
144874
  }
144875
+ if (parseListenHost(c.listenHostRaw) === null) {
144876
+ issues.push({ column: "wecomAppListenHost", code: "format" });
144877
+ }
144364
144878
  return issues;
144365
144879
  };
144366
144880
  var readWecomAppConnectConfig = (bot) => {
144367
144881
  const issues = validateWecomAppBot(bot);
144368
144882
  const credentials3 = wecomAppCredentials(bot);
144369
144883
  const listenPort = parseListenPort(credentials3.listenPortRaw);
144370
- if (issues.length > 0 || listenPort === null) return { ok: false, issues };
144371
- return { ok: true, credentials: credentials3, listenPort };
144884
+ const listenHost = parseListenHost(credentials3.listenHostRaw);
144885
+ if (issues.length > 0 || listenPort === null || listenHost === null) return { ok: false, issues };
144886
+ return { ok: true, credentials: credentials3, listenHost, listenPort };
144372
144887
  };
144373
144888
 
144374
144889
  // src/wecomAppAdapter.ts
@@ -144413,24 +144928,36 @@ var COLUMNS5 = [
144413
144928
  labelKey: "column.wecomAppListenPort.label",
144414
144929
  descriptionKey: "column.wecomAppListenPort.description",
144415
144930
  placeholderKey: "column.wecomAppListenPort.placeholder"
144931
+ },
144932
+ {
144933
+ key: "wecomAppListenHost",
144934
+ kind: "text",
144935
+ labelKey: "column.wecomAppListenHost.label",
144936
+ descriptionKey: "column.wecomAppListenHost.description",
144937
+ placeholderKey: "column.wecomAppListenHost.placeholder"
144416
144938
  }
144417
144939
  ];
144418
144940
  var WECOM_APP_IMAGE_BYTES = 10 * 1024 * 1024;
144419
144941
  var WECOM_APP_FILE_BYTES = 20 * 1024 * 1024;
144420
144942
  var WECOM_APP_MIN_BYTES = 5;
144421
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"]);
144422
144945
  var joinCallbackUrl = (baseOrOrigin, path) => `${baseOrOrigin}${path}`;
144423
144946
  var wecomAppPublicFields = (bot) => {
144424
144947
  const c = wecomAppCredentials(bot);
144425
- if (c.callbackBaseUrl === "") return [];
144948
+ if (c.token === "" || c.encodingAESKey === "") {
144949
+ return [{ key: "callbackUrl", value: "", hint: "callbackNeedsKeys" }];
144950
+ }
144951
+ const path = callbackRoutePath(bot.rowId, c.token, c.encodingAESKey);
144952
+ if (c.callbackBaseUrl === "") {
144953
+ return [{ key: "callbackPath", value: path, copyable: true, hint: "callbackPathNeedsBase" }];
144954
+ }
144426
144955
  return [
144427
144956
  {
144428
144957
  key: "callbackUrl",
144429
- value: joinCallbackUrl(
144430
- c.callbackBaseUrl,
144431
- callbackRoutePath(bot.rowId, c.token, c.encodingAESKey)
144432
- ),
144433
- copyable: true
144958
+ value: joinCallbackUrl(c.callbackBaseUrl, path),
144959
+ copyable: true,
144960
+ hint: "callbackFollowsKeys"
144434
144961
  }
144435
144962
  ];
144436
144963
  };
@@ -144440,6 +144967,8 @@ var OUTBOUND_COLUMNS = /* @__PURE__ */ new Set([
144440
144967
  "wecomAppSecret",
144441
144968
  "wecomAppApiBaseUrl"
144442
144969
  ]);
144970
+ var PROBE_BACKOFF_START_MS = 1e3;
144971
+ var PROBE_BACKOFF_MAX_MS = 6e4;
144443
144972
  var makeWecomAppAdapter = (deps) => {
144444
144973
  const apis = /* @__PURE__ */ new Map();
144445
144974
  const newApi = (c) => new WecomAppApi({
@@ -144450,17 +144979,30 @@ var makeWecomAppAdapter = (deps) => {
144450
144979
  fetch: deps.fetch
144451
144980
  });
144452
144981
  const callbackHub = makeWecomAppCallbackHub();
144453
- const apiFor = (binding) => {
144982
+ const connectionFor = (binding) => {
144454
144983
  const api = apis.get(binding.rowId);
144455
144984
  if (api === void 0) {
144456
144985
  throw new ImNotConnectedError(`wecom-app bot '${binding.displayName}' is not connected.`);
144457
144986
  }
144458
144987
  return api;
144459
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
+ };
144460
145001
  const sendMedia = async (binding, mediaType, file) => {
144461
- const api = apiFor(binding);
144462
- const mediaId = await api.uploadMedia({ mediaType, ...file });
144463
- 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
+ });
144464
145006
  deps.log.info(`wecom-app delivered ${mediaType === "image" ? "an image" : "a file"}`, {
144465
145007
  name: file.name
144466
145008
  });
@@ -144511,6 +145053,19 @@ var makeWecomAppAdapter = (deps) => {
144511
145053
  (issue) => issue.code !== "required" && issue.column !== "wecomAppEncodingAESKey"
144512
145054
  ),
144513
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
+ },
144514
145069
  /**
144515
145070
  * 自建应用是回调式渠道:入站由企微推到我们的 listener,没有可探的长连接;能探的是出站
144516
145071
  * 凭据——和真实发送同一条路,向 gettoken 换一次 access_token。
@@ -144541,7 +145096,12 @@ var makeWecomAppAdapter = (deps) => {
144541
145096
  detail: "access_token ok (callback reachability is not probed)"
144542
145097
  };
144543
145098
  } catch (error) {
144544
- 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
+ };
144545
145105
  }
144546
145106
  },
144547
145107
  connect: async (bot, handlers) => {
@@ -144552,10 +145112,17 @@ var makeWecomAppAdapter = (deps) => {
144552
145112
  );
144553
145113
  }
144554
145114
  const c = config2.credentials;
144555
- const lease = await callbackHub.acquire({ port: config2.listenPort });
145115
+ const lease = await callbackHub.acquire({ host: config2.listenHost, port: config2.listenPort });
144556
145116
  const server = lease.server;
144557
145117
  const api = newApi(c);
144558
- 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 });
144559
145126
  let path;
144560
145127
  try {
144561
145128
  path = server.register({
@@ -144571,7 +145138,7 @@ var makeWecomAppAdapter = (deps) => {
144571
145138
  await lease.release();
144572
145139
  throw error;
144573
145140
  }
144574
- deps.log.info("wecom-app connected", {
145141
+ deps.log.info("wecom-app callback listening", {
144575
145142
  bot: bot.displayName,
144576
145143
  callbackPath: path,
144577
145144
  callbackUrl: joinCallbackUrl(
@@ -144580,8 +145147,26 @@ var makeWecomAppAdapter = (deps) => {
144580
145147
  ),
144581
145148
  listen: `${server.host}:${String(server.port)}`
144582
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
+ })();
144583
145166
  return {
144584
145167
  close: () => {
145168
+ closed = true;
145169
+ probe2.abort();
144585
145170
  server.unregister(bot.rowId);
144586
145171
  apis.delete(bot.rowId);
144587
145172
  void lease.release().catch((error) => {
@@ -144597,7 +145182,19 @@ var makeWecomAppAdapter = (deps) => {
144597
145182
  if (options?.targetKind === "group") {
144598
145183
  throw new Error("wecom-app can only deliver to a user; group targets are not supported.");
144599
145184
  }
144600
- 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
+ );
145189
+ },
145190
+ // D376:企业微信自建应用没有表情回执也没有流式,开工时先回一句文字,结论照常另发。没有要撤的
145191
+ // 东西,回 null;发不出去由核心记一条 warn,不拦这一轮。
145192
+ acknowledge: async (binding) => {
145193
+ await deliver(
145194
+ binding,
145195
+ (api) => api.sendText({ userId: binding.chatId, content: deps.translate("reply.received") })
145196
+ );
145197
+ return null;
144601
145198
  },
144602
145199
  sendImage: (binding, image2) => sendMedia(binding, "image", image2),
144603
145200
  sendFile: (binding, file) => sendMedia(binding, "file", file),
@@ -144607,11 +145204,16 @@ var makeWecomAppAdapter = (deps) => {
144607
145204
  minBytes: WECOM_APP_MIN_BYTES
144608
145205
  },
144609
145206
  imageMimeTypes: WECOM_APP_IMAGE_TYPES,
144610
- 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
+ }),
144611
145211
  askApproval: async (binding, request) => {
144612
- const api = apis.get(binding.rowId);
144613
- if (api === void 0) return null;
144614
- 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
+ );
144615
145217
  return null;
144616
145218
  },
144617
145219
  answerByText: (_binding, text4, options) => answerByNumber(text4, options)
@@ -144621,7 +145223,7 @@ var makeWecomAppAdapter = (deps) => {
144621
145223
  // src/index.ts
144622
145224
  var imPlugin = definePlugin({
144623
145225
  id: "im",
144624
- apiVersion: 16,
145226
+ apiVersion: 18,
144625
145227
  activate: async (ctx) => {
144626
145228
  const { t, setLanguage } = makeLiveTranslate(
144627
145229
  parseReplyLanguage(ctx.config[REPLY_LANGUAGE_FIELD])
@@ -144682,7 +145284,6 @@ var imPlugin = definePlugin({
144682
145284
  const updateSettings = makeSettingsUpdater(ctx.settings);
144683
145285
  registerLarkSetup(ctx, { updateSettings });
144684
145286
  registerSettingsRowActions(ctx, adapters);
144685
- registerSettingsCommands(ctx, adapters, updateSettings);
144686
145287
  const core = makeImCore({
144687
145288
  host: {
144688
145289
  log: ctx.log,
@@ -144717,7 +145318,7 @@ var imPlugin = definePlugin({
144717
145318
  );
144718
145319
  const teams = makeTeamGoals(core);
144719
145320
  const notifications = makeNotifications(core, {
144720
- clearWorking: acknowledgements.clearWorking,
145321
+ takeWorking: acknowledgements.takeWorking,
144721
145322
  replyCards,
144722
145323
  failures,
144723
145324
  conclusions,
@@ -144793,6 +145394,7 @@ var imPlugin = definePlugin({
144793
145394
  ]
144794
145395
  });
144795
145396
  const connections = startConnections(core, inbound.inboundHandlersFor);
145397
+ registerSettingsCommands(ctx, adapters, updateSettings, connections);
144796
145398
  const { bots, connecting } = connections.reconcile();
144797
145399
  const deliveryHttp = makeDeliveryHttp({
144798
145400
  // 每次请求按此刻的设置找目标:投递目标表改了不用重开监听。