@ycodium-ai/plugin-im 0.2.3192 → 0.2.3208

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
@@ -137,7 +137,7 @@ var require_punycode = __commonJS({
137
137
  }
138
138
  return floor(k + (baseMinusTMin + 1) * delta / (delta + skew));
139
139
  };
140
- var decode2 = function(input) {
140
+ var decode3 = function(input) {
141
141
  const output = [];
142
142
  const inputLength = input.length;
143
143
  let i = 0;
@@ -249,7 +249,7 @@ var require_punycode = __commonJS({
249
249
  };
250
250
  var toUnicode = function(input) {
251
251
  return mapDomain(input, function(string) {
252
- return regexPunycode.test(string) ? decode2(string.slice(4).toLowerCase()) : string;
252
+ return regexPunycode.test(string) ? decode3(string.slice(4).toLowerCase()) : string;
253
253
  });
254
254
  };
255
255
  var toASCII = function(input) {
@@ -275,7 +275,7 @@ var require_punycode = __commonJS({
275
275
  "decode": ucs2decode,
276
276
  "encode": ucs2encode
277
277
  },
278
- "decode": decode2,
278
+ "decode": decode3,
279
279
  "encode": encode2,
280
280
  "toASCII": toASCII,
281
281
  "toUnicode": toUnicode
@@ -18475,7 +18475,7 @@ var require_utils = __commonJS({
18475
18475
  return acc;
18476
18476
  }, target2);
18477
18477
  };
18478
- var decode2 = function(str2, defaultDecoder, charset) {
18478
+ var decode3 = function(str2, defaultDecoder, charset) {
18479
18479
  var strWithoutPlus = str2.replace(/\+/g, " ");
18480
18480
  if (charset === "iso-8859-1") {
18481
18481
  return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape);
@@ -18601,7 +18601,7 @@ var require_utils = __commonJS({
18601
18601
  assign,
18602
18602
  combine,
18603
18603
  compact,
18604
- decode: decode2,
18604
+ decode: decode3,
18605
18605
  encode: encode2,
18606
18606
  isBuffer,
18607
18607
  isOverflow,
@@ -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: createHash7 } = __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 = createHash7("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: createHash7 } = __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 = createHash7("sha1").update(key + GUID).digest("base64");
24708
24708
  const headers = [
24709
24709
  "HTTP/1.1 101 Switching Protocols",
24710
24710
  "Upgrade: websocket",
@@ -24912,7 +24912,7 @@ var require_base64 = __commonJS({
24912
24912
  return String.fromCharCode.apply(String, chunk.slice(0, i2));
24913
24913
  };
24914
24914
  var invalidEncoding = "invalid encoding";
24915
- base64.decode = function decode2(string, buffer, offset) {
24915
+ base64.decode = function decode3(string, buffer, offset) {
24916
24916
  var start = offset;
24917
24917
  var j = 0, t;
24918
24918
  for (var i2 = 0; i2 < string.length; ) {
@@ -121305,7 +121305,7 @@ var require_lib2 = __commonJS({
121305
121305
  Header.encodeDelimited = function encodeDelimited(message, writer) {
121306
121306
  return this.encode(message, writer).ldelim();
121307
121307
  };
121308
- Header.decode = function decode3(reader, length) {
121308
+ Header.decode = function decode4(reader, length) {
121309
121309
  if (!(reader instanceof $Reader))
121310
121310
  reader = $Reader.create(reader);
121311
121311
  let end = length === void 0 ? reader.len : reader.pos + length, message = new $root.pbbp2.Header();
@@ -121451,7 +121451,7 @@ var require_lib2 = __commonJS({
121451
121451
  Frame.encodeDelimited = function encodeDelimited(message, writer) {
121452
121452
  return this.encode(message, writer).ldelim();
121453
121453
  };
121454
- Frame.decode = function decode3(reader, length) {
121454
+ Frame.decode = function decode4(reader, length) {
121455
121455
  if (!(reader instanceof $Reader))
121456
121456
  reader = $Reader.create(reader);
121457
121457
  let end = length === void 0 ? reader.len : reader.pos + length, message = new $root.pbbp2.Frame();
@@ -121678,7 +121678,7 @@ var require_lib2 = __commonJS({
121678
121678
  })();
121679
121679
  return pbbp22;
121680
121680
  })();
121681
- var decode2 = (buffer) => {
121681
+ var decode3 = (buffer) => {
121682
121682
  return pbbp2.Frame.decode(buffer);
121683
121683
  };
121684
121684
  var WSConfig = class {
@@ -122177,7 +122177,7 @@ var require_lib2 = __commonJS({
122177
122177
  const wsInstance = this.wsConfig.getWSInstance();
122178
122178
  wsInstance === null || wsInstance === void 0 ? void 0 : wsInstance.on("message", (buffer) => __awaiter(this, void 0, void 0, function* () {
122179
122179
  this.clearLiveness();
122180
- const data = decode2(buffer);
122180
+ const data = decode3(buffer);
122181
122181
  const { method } = data;
122182
122182
  if (method === FrameType.control) {
122183
122183
  yield this.handleControlData(data);
@@ -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 = [
@@ -127410,6 +127416,31 @@ var makeChatQueue = (core) => {
127410
127416
  // src/chatSetup.ts
127411
127417
  import { threadOrNull } from "@ycodium-ai/plugin-api";
127412
127418
 
127419
+ // src/cooldown.ts
127420
+ var makeCooldown = (windowMs, now, maxKeys = Number.POSITIVE_INFINITY) => {
127421
+ const lastAt = /* @__PURE__ */ new Map();
127422
+ return {
127423
+ tryAcquire: (key) => {
127424
+ const current = now();
127425
+ const previous = lastAt.get(key);
127426
+ if (previous !== void 0 && current - previous < windowMs) return false;
127427
+ lastAt.delete(key);
127428
+ lastAt.set(key, current);
127429
+ if (lastAt.size > maxKeys) {
127430
+ for (const [candidate, at] of lastAt) {
127431
+ if (current - at >= windowMs) lastAt.delete(candidate);
127432
+ }
127433
+ while (lastAt.size > maxKeys) {
127434
+ const oldest = lastAt.keys().next().value;
127435
+ if (oldest === void 0) break;
127436
+ lastAt.delete(oldest);
127437
+ }
127438
+ }
127439
+ return true;
127440
+ }
127441
+ };
127442
+ };
127443
+
127413
127444
  // src/projectList.ts
127414
127445
  var PROJECT_LIST_MAX = 20;
127415
127446
  var projectLine = (row, position) => `${String(position)}. ${row.title.replace(/\s+/g, " ").trim()}${row.current ? " \u2605" : ""}`;
@@ -127498,16 +127529,21 @@ var pickSession = (arg, listed, rows) => {
127498
127529
  var makeChatSetup = (core, deps) => {
127499
127530
  const { host, t, adapterFor, commit, deliver, bindingOf: bindingOf2, hasPanel } = core;
127500
127531
  const { projectRowsFor, listedProjectsByChat } = deps.rows;
127501
- const promptProject = async (binding) => {
127502
- const chatKey = chatKeyOf(binding);
127503
- let rows;
127532
+ const pendingMessages = makeBoundedMap(100);
127533
+ const projectPromptCooldown = makeCooldown(3e4, () => Date.now());
127534
+ const loadProjectRows = async (binding) => {
127504
127535
  try {
127505
- rows = await projectRowsFor(chatKey);
127536
+ return await projectRowsFor(chatKeyOf(binding));
127506
127537
  } catch (error) {
127507
127538
  host.log.warn("chat could not list the projects", { error: describeError(error) });
127508
127539
  await deliver(binding, t("panel.loadFailed"));
127509
- return;
127540
+ return null;
127510
127541
  }
127542
+ };
127543
+ const promptProject = async (binding, available) => {
127544
+ const chatKey = chatKeyOf(binding);
127545
+ const rows = available ?? await loadProjectRows(binding);
127546
+ if (rows === null) return;
127511
127547
  listedProjectsByChat.set(chatKey, listedProjectIds(rows));
127512
127548
  if (rows.length > 0 && hasPanel(binding)) {
127513
127549
  try {
@@ -127527,14 +127563,8 @@ var makeChatSetup = (core, deps) => {
127527
127563
  return;
127528
127564
  }
127529
127565
  const chatKey = chatKeyOf(binding);
127530
- let rows;
127531
- try {
127532
- rows = await projectRowsFor(chatKey);
127533
- } catch (error) {
127534
- host.log.warn("chat could not list the projects", { error: describeError(error) });
127535
- await deliver(binding, t("panel.loadFailed"));
127536
- return;
127537
- }
127566
+ const rows = await loadProjectRows(binding);
127567
+ if (rows === null) return;
127538
127568
  const picked = pickProject(arg, listedProjectsByChat.get(chatKey), rows);
127539
127569
  if (picked === null) {
127540
127570
  await deliver(binding, t("repo.notFound", { query: arg }));
@@ -127566,6 +127596,9 @@ var makeChatSetup = (core, deps) => {
127566
127596
  listedProjectsByChat.delete(chatKey);
127567
127597
  host.log.info("chat picked a project", { bot: binding.displayName, projectId: picked });
127568
127598
  await deliver(binding, t("repo.done", { title: target2.title }));
127599
+ const pending = pendingMessages.get(chatKey);
127600
+ pendingMessages.forget((key) => key === chatKey);
127601
+ if (pending !== void 0) await deps.resumeMessage(binding, pending);
127569
127602
  };
127570
127603
  const handleGroup = async (binding, name, senderId) => {
127571
127604
  const adapter = adapterFor(binding.platform);
@@ -127593,7 +127626,27 @@ var makeChatSetup = (core, deps) => {
127593
127626
  await deliver(binding, t("group.created", { name }));
127594
127627
  await promptProject(bindingOf2(binding, chatId));
127595
127628
  };
127596
- return { promptProject, handleRepo, handleGroup };
127629
+ const prepareMessage = async (binding, message) => {
127630
+ const chatKey = chatKeyOf(binding);
127631
+ const asked = message.text.trim();
127632
+ if (/^[1-9]\d{0,2}$/u.test(asked) && listedProjectsByChat.has(chatKey)) {
127633
+ await handleRepo(binding, asked);
127634
+ return false;
127635
+ }
127636
+ const rows = await loadProjectRows(binding);
127637
+ if (rows === null) return false;
127638
+ const only = rows.length === 1 ? rows[0] : void 0;
127639
+ if (only !== void 0) {
127640
+ commit("chat-updated", { chatKey, projectId: only.projectId });
127641
+ pendingMessages.forget((key) => key === chatKey);
127642
+ await deliver(binding, t("repo.autoSelected", { title: only.title }));
127643
+ return true;
127644
+ }
127645
+ pendingMessages.set(chatKey, message);
127646
+ if (projectPromptCooldown.tryAcquire(chatKey)) await promptProject(binding, rows);
127647
+ return false;
127648
+ };
127649
+ return { promptProject, handleRepo, handleGroup, prepareMessage };
127597
127650
  };
127598
127651
 
127599
127652
  // src/commands.ts
@@ -127686,7 +127739,6 @@ var parseChatCommand = (input) => {
127686
127739
  return query === "" ? { kind: "selectTeam" } : { kind: "help" };
127687
127740
  }
127688
127741
  if (isThreadCommand(input.text, input.chinese)) return { kind: "selectThread" };
127689
- if (/^\/[a-z][a-z0-9_-]*/i.test(trimmed)) return { kind: "help" };
127690
127742
  if (input.chinese) {
127691
127743
  const alias = ZH_ALIASES[trimmed];
127692
127744
  if (alias !== void 0) return { kind: alias };
@@ -127943,13 +127995,58 @@ var makeCommands = (core, deps) => {
127943
127995
  };
127944
127996
 
127945
127997
  // 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);
127998
+ var makeKeyedQueue = (onError, options = {}) => {
127999
+ const queues = /* @__PURE__ */ new Map();
128000
+ const run = (key, work, kind = {}) => {
128001
+ const queue = queues.get(key) ?? { tail: Promise.resolve(), size: 0, droppable: 0 };
128002
+ const droppable = kind.droppable === true;
128003
+ if (droppable && options.limit !== void 0 && queue.droppable >= options.limit) {
128004
+ options.onFull?.(key, options.limit);
128005
+ return false;
128006
+ }
128007
+ queue.size += 1;
128008
+ if (droppable) queue.droppable += 1;
128009
+ queue.tail = queue.tail.then(work).catch(onError).then(() => {
128010
+ queue.size -= 1;
128011
+ if (droppable) queue.droppable -= 1;
128012
+ if (queue.size === 0 && queues.get(key) === queue) queues.delete(key);
127951
128013
  });
127952
- tails.set(key, run);
128014
+ queues.set(key, queue);
128015
+ return true;
128016
+ };
128017
+ return Object.assign(run, { activeKeys: () => queues.size });
128018
+ };
128019
+
128020
+ // src/redact.ts
128021
+ var DETAIL_MAX_CHARS = 200;
128022
+ var MASK = "***";
128023
+ var TOKEN_LIKE = /[A-Za-z0-9_=-]{24,}/g;
128024
+ var MIN_SECRET_LENGTH = 6;
128025
+ var botRedactor = (columns, values) => {
128026
+ const redactor = makeRedactor();
128027
+ for (const column of columns) {
128028
+ if (column.kind !== "password") continue;
128029
+ const value2 = values[column.key];
128030
+ redactor.register(value2);
128031
+ if (value2 !== void 0) redactor.register(encodeURIComponent(value2));
128032
+ }
128033
+ return redactor;
128034
+ };
128035
+ var makeRedactor = () => {
128036
+ const secrets = /* @__PURE__ */ new Set();
128037
+ const redact = (text4) => {
128038
+ let out = text4;
128039
+ for (const secret of [...secrets].sort((a, b) => b.length - a.length)) {
128040
+ out = out.split(secret).join(MASK);
128041
+ }
128042
+ return out.replace(TOKEN_LIKE, MASK);
128043
+ };
128044
+ return {
128045
+ register: (secret) => {
128046
+ if (typeof secret === "string" && secret.length >= MIN_SECRET_LENGTH) secrets.add(secret);
128047
+ },
128048
+ redact,
128049
+ detail: (text4) => redact(text4).slice(0, DETAIL_MAX_CHARS)
127953
128050
  };
127954
128051
  };
127955
128052
 
@@ -127967,26 +128064,61 @@ var connectionKey = (adapter, bot) => JSON.stringify([
127967
128064
  var startConnections = (core, inboundHandlersFor) => {
127968
128065
  const { host, adapterFor, ready } = core;
127969
128066
  const live = /* @__PURE__ */ new Map();
128067
+ const statuses = /* @__PURE__ */ new Map();
128068
+ const listeners = /* @__PURE__ */ new Set();
128069
+ const notify = () => {
128070
+ const snapshot = Object.fromEntries(statuses);
128071
+ for (const listener of [...listeners]) listener(snapshot);
128072
+ };
128073
+ const record3 = (rowId, status) => {
128074
+ const previous = statuses.get(rowId);
128075
+ if (previous?.state === status.state && previous.error === status.error && previous.reason === status.reason && previous.duplicateOf === status.duplicateOf && previous.hint === status.hint)
128076
+ return;
128077
+ statuses.set(rowId, status);
128078
+ notify();
128079
+ };
128080
+ const remove = (rowId) => {
128081
+ if (statuses.delete(rowId)) notify();
128082
+ };
127970
128083
  const enqueueForRow = makeKeyedQueue((error) => {
127971
128084
  host.log.warn("chat could not start an inbound connection", { error: describeError(error) });
127972
128085
  });
127973
128086
  let disposed = false;
127974
128087
  const open = (bot, key) => {
127975
128088
  const adapter = adapterFor(bot.platform);
128089
+ const redactor = botRedactor(adapter?.columns ?? [], bot.values);
127976
128090
  let closed = false;
127977
128091
  let closeConnection;
128092
+ record3(bot.rowId, { state: "connecting" });
127978
128093
  enqueueForRow(bot.rowId, async () => {
127979
128094
  try {
127980
128095
  await ready;
127981
128096
  if (closed || adapter === void 0) return;
127982
- const connection = await adapter.connect(bot, inboundHandlersFor(bot));
128097
+ const connection = await adapter.connect(bot, {
128098
+ ...inboundHandlersFor(bot),
128099
+ onConnectionState: (status) => {
128100
+ if (closed) return;
128101
+ if (status.state === "connected") {
128102
+ record3(bot.rowId, status);
128103
+ return;
128104
+ }
128105
+ record3(bot.rowId, {
128106
+ state: "failed",
128107
+ ...status.reason === void 0 ? {} : { reason: status.reason },
128108
+ ...status.error === void 0 ? {} : { error: redactor.redact(describeError(status.error)) },
128109
+ ...status.hint === void 0 ? {} : { hint: status.hint }
128110
+ });
128111
+ }
128112
+ });
127983
128113
  if (closed) connection.close();
127984
128114
  else closeConnection = () => connection.close();
127985
128115
  } catch (error) {
128116
+ const detail = redactor.redact(describeError(error));
128117
+ if (!closed) record3(bot.rowId, { state: "failed", error: detail });
127986
128118
  host.log.warn("chat could not start an inbound connection", {
127987
128119
  bot: bot.displayName,
127988
128120
  platform: bot.platform,
127989
- error: describeError(error)
128121
+ error: detail
127990
128122
  });
127991
128123
  }
127992
128124
  });
@@ -128001,19 +128133,25 @@ var startConnections = (core, inboundHandlersFor) => {
128001
128133
  const reconcile = () => {
128002
128134
  const bots = disposed ? [] : core.bots();
128003
128135
  const wanted = /* @__PURE__ */ new Map();
128004
- const credentials3 = /* @__PURE__ */ new Set();
128136
+ const credentials3 = /* @__PURE__ */ new Map();
128137
+ const duplicates = /* @__PURE__ */ new Map();
128005
128138
  for (const bot of bots) {
128006
128139
  const adapter = adapterFor(bot.platform);
128007
128140
  if (adapter === void 0) continue;
128008
128141
  const fingerprint = `${bot.platform}:${adapter.columns.map((column) => bot.values[column.key] ?? "").join(" ")}`;
128009
- if (credentials3.has(fingerprint)) {
128142
+ const duplicateOf = credentials3.get(fingerprint);
128143
+ if (duplicateOf !== void 0) {
128144
+ duplicates.set(bot.rowId, duplicateOf);
128145
+ if (statuses.get(bot.rowId)?.duplicateOf !== duplicateOf) {
128146
+ record3(bot.rowId, { state: "duplicate", duplicateOf });
128147
+ }
128010
128148
  host.log.warn("chat did not connect a bot that repeats another bot's credentials", {
128011
128149
  bot: bot.displayName,
128012
128150
  platform: bot.platform
128013
128151
  });
128014
128152
  continue;
128015
128153
  }
128016
- credentials3.add(fingerprint);
128154
+ credentials3.set(fingerprint, bot.rowId);
128017
128155
  wanted.set(bot.rowId, { bot, key: connectionKey(adapter, bot) });
128018
128156
  }
128019
128157
  for (const [rowId, connection] of live) {
@@ -128021,6 +128159,9 @@ var startConnections = (core, inboundHandlersFor) => {
128021
128159
  connection.close();
128022
128160
  live.delete(rowId);
128023
128161
  }
128162
+ for (const rowId of statuses.keys()) {
128163
+ if (!wanted.has(rowId) && !duplicates.has(rowId)) remove(rowId);
128164
+ }
128024
128165
  for (const [rowId, { bot, key }] of wanted) {
128025
128166
  if (!live.has(rowId)) live.set(rowId, open(bot, key));
128026
128167
  }
@@ -128029,8 +128170,18 @@ var startConnections = (core, inboundHandlersFor) => {
128029
128170
  host.onDispose(() => {
128030
128171
  disposed = true;
128031
128172
  reconcile();
128173
+ listeners.clear();
128032
128174
  });
128033
- return { reconcile };
128175
+ return {
128176
+ reconcile,
128177
+ subscribe: (listener) => {
128178
+ if (!disposed) listeners.add(listener);
128179
+ listener(Object.fromEntries(statuses));
128180
+ return () => {
128181
+ listeners.delete(listener);
128182
+ };
128183
+ }
128184
+ };
128034
128185
  };
128035
128186
 
128036
128187
  // src/deliveryHttp.ts
@@ -128279,6 +128430,7 @@ ${token}` : void 0;
128279
128430
  };
128280
128431
 
128281
128432
  // src/core.ts
128433
+ var OUTBOUND_QUEUE_LIMIT = 100;
128282
128434
  var makeImCore = (input) => {
128283
128435
  const { host, t, adapters } = input;
128284
128436
  const adapterFor = (platform) => adapters.find((adapter) => adapter.platform === platform);
@@ -128290,7 +128442,7 @@ var makeImCore = (input) => {
128290
128442
  init: emptyState,
128291
128443
  apply: foldPluginEvent(input.pluginId)
128292
128444
  });
128293
- const ready = stateHandle.get().then(
128445
+ const chatReady = stateHandle.get().then(
128294
128446
  (restored) => {
128295
128447
  state = restored;
128296
128448
  },
@@ -128298,6 +128450,7 @@ var makeImCore = (input) => {
128298
128450
  host.log.warn("chat could not restore its saved state", { error: describeError(error) });
128299
128451
  }
128300
128452
  );
128453
+ const ready = Promise.all([chatReady, input.inboundReady]).then(() => void 0);
128301
128454
  let persisting = Promise.resolve();
128302
128455
  const commit = (name, data) => {
128303
128456
  state = foldImEvent(state, name, data);
@@ -128313,10 +128466,21 @@ var makeImCore = (input) => {
128313
128466
  const bindChat = (chatKey, threadId) => {
128314
128467
  commit("chat-updated", { chatKey, threadId });
128315
128468
  };
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) => {
128469
+ const outbound = makeKeyedQueue(
128470
+ (error) => {
128471
+ host.log.warn("chat could not deliver queued work", { error: describeError(error) });
128472
+ },
128473
+ {
128474
+ limit: OUTBOUND_QUEUE_LIMIT,
128475
+ onFull: (chatKey, limit) => {
128476
+ host.log.warn("chat queue is full; refused a conclusion until restart", {
128477
+ chatKey,
128478
+ limit
128479
+ });
128480
+ }
128481
+ }
128482
+ );
128483
+ const postImages = async (binding, attachments, onFailure) => {
128320
128484
  const adapter = adapterFor(binding.platform);
128321
128485
  if (adapter?.sendImage === void 0) return;
128322
128486
  for (const attachment of attachments) {
@@ -128333,19 +128497,22 @@ var makeImCore = (input) => {
128333
128497
  name: attachment.name,
128334
128498
  error: describeError(error)
128335
128499
  });
128500
+ await onFailure?.(error);
128336
128501
  }
128337
128502
  }
128338
128503
  };
128339
- const deliver = async (binding, text4, attachments = []) => {
128340
- await postImages(binding, attachments);
128504
+ const deliver = async (binding, text4, attachments = [], options = {}) => {
128505
+ const { retry, onFailure } = options;
128506
+ await postImages(binding, attachments, onFailure);
128341
128507
  if (text4.trim() === "") return true;
128342
128508
  const adapter = adapterFor(binding.platform);
128343
128509
  if (adapter === void 0) return false;
128344
128510
  try {
128345
- await adapter.sendText(binding, text4);
128511
+ await adapter.sendText(binding, text4, { retry });
128346
128512
  return true;
128347
128513
  } catch (error) {
128348
128514
  host.log.warn("chat could not deliver", { error: describeError(error) });
128515
+ await onFailure?.(error);
128349
128516
  return false;
128350
128517
  }
128351
128518
  };
@@ -128377,6 +128544,92 @@ var makeImCore = (input) => {
128377
128544
  // src/conclusionDelivery.ts
128378
128545
  import { threadOrNull as threadOrNull3 } from "@ycodium-ai/plugin-api";
128379
128546
 
128547
+ // src/abortableSleep.ts
128548
+ var abortableSleep = (ms, signal) => new Promise((resolve, reject) => {
128549
+ if (signal?.aborted === true) {
128550
+ reject(new Error("aborted"));
128551
+ return;
128552
+ }
128553
+ const onAbort = () => {
128554
+ clearTimeout(timer);
128555
+ reject(new Error("aborted"));
128556
+ };
128557
+ const timer = setTimeout(() => {
128558
+ signal?.removeEventListener("abort", onAbort);
128559
+ resolve();
128560
+ }, ms);
128561
+ signal?.addEventListener("abort", onAbort, { once: true });
128562
+ });
128563
+
128564
+ // src/deliveryFailure.ts
128565
+ import * as NodeCrypto2 from "node:crypto";
128566
+
128567
+ // src/diagnostics.ts
128568
+ var PREFIX = {
128569
+ conn: "IM-CONN",
128570
+ msg: "IM-MSG",
128571
+ setup: "IM-SETUP"
128572
+ };
128573
+ var shortSalt = () => {
128574
+ const bytes = new Uint8Array(2);
128575
+ crypto.getRandomValues(bytes);
128576
+ return Array.from(bytes, (b) => b.toString(16).toUpperCase().padStart(2, "0")).join("");
128577
+ };
128578
+ var stableDiagnosticRef = (kind, digest) => `${PREFIX[kind]}-${digest.slice(0, 16).toUpperCase()}`;
128579
+ var diagnosticRef = (kind, nowMs = Date.now()) => {
128580
+ const stamp = nowMs.toString(36).toUpperCase();
128581
+ return `${PREFIX[kind]}-${stamp}-${shortSalt()}`;
128582
+ };
128583
+ var formatDiagnosticCopy = (report) => [
128584
+ `ok=${report.ok ? "yes" : "no"}`,
128585
+ `platform=${report.platform}`,
128586
+ `bot=${report.botName}`,
128587
+ `row=${report.rowId}`,
128588
+ `stage=${report.stage}`,
128589
+ `at=${report.at}`,
128590
+ `detail=${report.detail}`
128591
+ ].join("\n");
128592
+
128593
+ // src/deliveryFailure.ts
128594
+ var deliveryFailure = (core, binding, threadId, turnId) => async (error) => {
128595
+ const rejection = core.adapterFor(binding.platform)?.describeDeliveryRejection?.(error, binding);
128596
+ if (rejection === void 0) return;
128597
+ const { recipients } = rejection;
128598
+ const digest = NodeCrypto2.createHash("sha256").update(JSON.stringify([threadId, turnId, binding.platform, binding.rowId, recipients])).digest("hex");
128599
+ const id = `im.delivery-failed:${digest}`;
128600
+ const reference2 = stableDiagnosticRef("msg", digest);
128601
+ const payload = {
128602
+ platform: binding.platform,
128603
+ recipients,
128604
+ hint: rejection.hint,
128605
+ providerCode: rejection.providerCode,
128606
+ reference: reference2,
128607
+ sourceTurnId: turnId
128608
+ };
128609
+ core.host.log.warn("chat recipient rejected delivery", { threadId, ...payload });
128610
+ if (core.state.opened[threadId] === void 0) return;
128611
+ try {
128612
+ await core.host.threads.externalMessages.append({
128613
+ requestId: id,
128614
+ threadId,
128615
+ messages: [],
128616
+ activities: [
128617
+ {
128618
+ id,
128619
+ tone: "error",
128620
+ kind: "im.delivery-failed",
128621
+ summary: rejection.summary,
128622
+ payload,
128623
+ turnId: null,
128624
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
128625
+ }
128626
+ ]
128627
+ });
128628
+ } catch {
128629
+ core.host.log.warn("chat could not append delivery failure notice", { threadId, reference: reference2 });
128630
+ }
128631
+ };
128632
+
128380
128633
  // src/threadMessages.ts
128381
128634
  import {
128382
128635
  isThreadNotFound,
@@ -128428,20 +128681,30 @@ async function messagesAfterLastUser(threads, threadId) {
128428
128681
  }
128429
128682
 
128430
128683
  // src/conclusionDelivery.ts
128684
+ var ALWAYS_OWED = () => true;
128431
128685
  var makeConclusionDelivery = (core, anchors) => {
128432
128686
  let disposed = false;
128687
+ const stopped = new AbortController();
128433
128688
  core.host.onDispose(() => {
128434
128689
  disposed = true;
128690
+ stopped.abort();
128435
128691
  });
128436
128692
  const marked = (text4, recovered) => recovered ? `${core.t("reply.conclusionRecovered")}
128437
128693
  ${text4}` : text4;
128694
+ const sleep3 = (ms) => abortableSleep(ms, stopped.signal).catch(() => void 0);
128695
+ const retryFor = (threadId, owed = ALWAYS_OWED) => ({
128696
+ sleep: sleep3,
128697
+ shouldContinue: async () => !disposed && owed() && await threadOrNull3(core.host.threads, threadId) !== null
128698
+ });
128438
128699
  return {
128700
+ retryFor,
128439
128701
  prepareConclusion: async (threadId, turnId = null) => {
128440
128702
  const thread = await threadOrNull3(core.host.threads, threadId);
128441
128703
  const final = thread === null ? void 0 : await latestMessage(
128442
128704
  core.host.threads,
128443
128705
  threadId,
128444
- (message) => message.role === "assistant" && message.text.trim() !== ""
128706
+ // 给了轮次就只取这一轮的正文:排队等着的结论不能拿到下一轮的话。没记轮次的老消息照旧认。
128707
+ (message) => message.role === "assistant" && message.text.trim() !== "" && (turnId === null || message.turnId === null || message.turnId === turnId)
128445
128708
  );
128446
128709
  const attachments = final === void 0 ? [] : await attachmentsOf(core.host.threads, threadId, final);
128447
128710
  return async (binding, options) => {
@@ -128451,12 +128714,22 @@ ${text4}` : text4;
128451
128714
 
128452
128715
  ${final.text}`, options.recovered);
128453
128716
  const anchored = await anchors.anchored(binding, threadId, turnId ?? final.turnId);
128454
- return !disposed && core.deliver(anchored, text4, attachments);
128717
+ return !disposed && core.deliver(anchored, text4, attachments, {
128718
+ retry: retryFor(threadId, options.owed),
128719
+ onFailure: deliveryFailure(core, binding, threadId, turnId ?? final.turnId)
128720
+ });
128455
128721
  };
128456
128722
  },
128457
- failure: async (binding, threadId, notice, recovered, turnId = null) => {
128723
+ /**
128724
+ * 发一段已经写好的结论文字(失败通知、卡片定稿失败时的文字兜底):锚点、停用检查、重发与
128725
+ * 拒收提醒都在这里,调用方只管写文字。
128726
+ */
128727
+ sendText: async (binding, threadId, notice, recovered, turnId = null, owed = ALWAYS_OWED) => {
128458
128728
  const anchored = await anchors.anchored(binding, threadId, turnId);
128459
- return !disposed && core.deliver(anchored, marked(notice, recovered));
128729
+ return !disposed && core.deliver(anchored, marked(notice, recovered), [], {
128730
+ retry: retryFor(threadId, owed),
128731
+ onFailure: deliveryFailure(core, binding, threadId, turnId)
128732
+ });
128460
128733
  }
128461
128734
  };
128462
128735
  };
@@ -128709,63 +128982,15 @@ var makeInboundAttachments = (core) => {
128709
128982
 
128710
128983
  // src/inbound.ts
128711
128984
  import { projectOrNull } from "@ycodium-ai/plugin-api";
128712
-
128713
- // src/boundedSeen.ts
128714
- var makeBoundedSeen = (limit) => {
128715
- const seen = /* @__PURE__ */ new Set();
128716
- return {
128717
- has: (key) => seen.has(key),
128718
- /** 记下这个键:第一次见返回 true;已经记着返回 false(不重排,最早记的先被忘)。 */
128719
- claim: (key) => {
128720
- if (seen.has(key)) return false;
128721
- seen.add(key);
128722
- if (seen.size > limit) {
128723
- const oldest = seen.values().next().value;
128724
- if (oldest !== void 0) seen.delete(oldest);
128725
- }
128726
- return true;
128727
- }
128728
- };
128729
- };
128730
-
128731
- // src/cooldown.ts
128732
- var makeCooldown = (windowMs, now, maxKeys = Number.POSITIVE_INFINITY) => {
128733
- const lastAt = /* @__PURE__ */ new Map();
128734
- return {
128735
- tryAcquire: (key) => {
128736
- const current = now();
128737
- const previous = lastAt.get(key);
128738
- if (previous !== void 0 && current - previous < windowMs) return false;
128739
- lastAt.delete(key);
128740
- lastAt.set(key, current);
128741
- if (lastAt.size > maxKeys) {
128742
- for (const [candidate, at] of lastAt) {
128743
- if (current - at >= windowMs) lastAt.delete(candidate);
128744
- }
128745
- while (lastAt.size > maxKeys) {
128746
- const oldest = lastAt.keys().next().value;
128747
- if (oldest === void 0) break;
128748
- lastAt.delete(oldest);
128749
- }
128750
- }
128751
- return true;
128752
- }
128753
- };
128754
- };
128755
-
128756
- // src/inbound.ts
128757
- var SEEN_MESSAGE_LIMIT = 256;
128758
128985
  var ARCHIVED_REPLY_COOLDOWN_MS = 10 * 6e4;
128759
128986
  var UNSUPPORTED_REPLY_COOLDOWN_MS = 6e4;
128760
128987
  var NOT_ALLOWED_REPLY_COOLDOWN_MS = 10 * 6e4;
128761
128988
  var NOT_ALLOWED_REPLY_KEY_LIMIT = 1e3;
128762
- var PROJECT_PROMPT_COOLDOWN_MS = 3e4;
128763
128989
  var makeInbound = (core, deps) => {
128764
128990
  const { host, t, adapterFor, commit, deliver, bindingOf: bindingOf2, bots } = core;
128765
128991
  const { enqueueForChat } = deps;
128766
128992
  const { handleCommand, targetOf } = deps.commands;
128767
- const { promptProject, handleRepo } = deps.chatSetup;
128768
- const { listedProjectsByChat } = deps.rows;
128993
+ const { prepareMessage } = deps.chatSetup;
128769
128994
  const { questionFlow, handleButtonClick } = deps.interactions;
128770
128995
  const { answerByToken } = deps.approvals;
128771
128996
  const { handlePanelClick } = deps.panelWiring;
@@ -128779,10 +129004,9 @@ var makeInbound = (core, deps) => {
128779
129004
  () => Date.now(),
128780
129005
  NOT_ALLOWED_REPLY_KEY_LIMIT
128781
129006
  );
128782
- const projectPromptCooldown = makeCooldown(PROJECT_PROMPT_COOLDOWN_MS, () => Date.now());
128783
- const handledMessages = makeBoundedSeen(SEEN_MESSAGE_LIMIT);
128784
- const alreadyHandled = (key) => !handledMessages.claim(key);
128785
- const claimCallback = (rowId, callbackId) => callbackId === void 0 || callbackId === "" || !alreadyHandled(`c:${rowId}:${callbackId}`);
129007
+ const { inboundState } = deps;
129008
+ const alreadyHandled = (rowId, key) => !inboundState.claim(rowId, key);
129009
+ const claimCallback = (rowId, callbackId) => callbackId === void 0 || callbackId === "" || !alreadyHandled(rowId, `c:${callbackId}`);
128786
129010
  const turnAwayStranger = (target2, sender) => {
128787
129011
  host.log.warn("chat ignored a message from someone not on the allow list", {
128788
129012
  bot: target2.displayName,
@@ -128822,6 +129046,61 @@ var makeInbound = (core, deps) => {
128822
129046
  const threadId = await startTurn(speaker, message);
128823
129047
  if (threadId !== null) await markWorking(speaker, threadId, message.messageId);
128824
129048
  };
129049
+ const resumeMessage = async (current, message) => {
129050
+ inheritFromParent(current, message.chatId, message.parentChatId);
129051
+ let target2 = {
129052
+ ...bindingOf2(current, message.chatId),
129053
+ reply: anchorOfMessage(adapterFor(current.platform), message)
129054
+ };
129055
+ const chatKey = chatKeyOf(target2);
129056
+ if (await handleCommand(target2, message.text, message.senderId)) return;
129057
+ if (target2.projectId === "") {
129058
+ if (!await prepareMessage(target2, message)) return;
129059
+ target2 = { ...bindingOf2(current, message.chatId), reply: target2.reply };
129060
+ }
129061
+ const boundThread = chatOf(core.state, chatKey).threadId;
129062
+ if (boundThread !== null && message.text !== "" && await questionFlow.answerByText(chatKey, boundThread, message.text)) {
129063
+ return;
129064
+ }
129065
+ const project = await projectOrNull(host.projects, target2.projectId);
129066
+ if (project === null || project.archivedAt != null) {
129067
+ if (project === null) {
129068
+ host.log.warn("chat cannot start work: the chat's project is gone", {
129069
+ bot: target2.displayName,
129070
+ projectId: target2.projectId
129071
+ });
129072
+ }
129073
+ if (archivedReplyCooldown.tryAcquire(chatKey)) {
129074
+ await deliver(
129075
+ target2,
129076
+ project === null ? t("reply.projectMissing") : t("reply.projectArchived", { title: project.title })
129077
+ );
129078
+ }
129079
+ return;
129080
+ }
129081
+ const routed = targetOf(target2);
129082
+ const attachmentsOnly = message.text.trim() === "" && message.resources.length > 0;
129083
+ if (routed.kind === "team") {
129084
+ if (message.resources.length > 0) await deliver(target2, t("reply.attachmentsNoTeam"));
129085
+ if (attachmentsOnly) return;
129086
+ const leadThreadId = await startTeamGoal(target2, message);
129087
+ if (leadThreadId !== null) await markWorking(target2, leadThreadId, message.messageId);
129088
+ return;
129089
+ }
129090
+ if (attachmentsOnly) {
129091
+ await receiveAttachmentsOnly(target2, message);
129092
+ return;
129093
+ }
129094
+ const branchChatId = hasWaitingAttachments(chatKey) ? void 0 : message.branchChatId;
129095
+ if (branchChatId === void 0) {
129096
+ await openTurn(target2, message);
129097
+ return;
129098
+ }
129099
+ enqueueForChat(chatKeyOf({ rowId: current.rowId, chatId: branchChatId }), async () => {
129100
+ inheritFromParent(current, branchChatId, message.chatId);
129101
+ await openTurn({ ...bindingOf2(current, branchChatId), reply: target2.reply }, message);
129102
+ });
129103
+ };
128825
129104
  const inboundHandlersFor = (bot) => ({
128826
129105
  onError: (message) => host.log.warn("chat inbound connection failed", {
128827
129106
  platform: bot.platform,
@@ -128844,7 +129123,7 @@ var makeInbound = (core, deps) => {
128844
129123
  if (current === void 0) return;
128845
129124
  const message = settleChat(current, received);
128846
129125
  const arrived = bindingOf2(current, message.chatId);
128847
- if (alreadyHandled(`m:${chatKeyOf(arrived)}:${message.messageId}`)) return;
129126
+ if (alreadyHandled(current.rowId, `m:${message.chatId}:${message.messageId}`)) return;
128848
129127
  if (message.chatKind !== void 0 && chatOf(core.state, chatKeyOf(arrived)).chatKind !== message.chatKind) {
128849
129128
  commit("chat-updated", { chatKey: chatKeyOf(arrived), chatKind: message.chatKind });
128850
129129
  }
@@ -128871,69 +129150,129 @@ var makeInbound = (core, deps) => {
128871
129150
  });
128872
129151
  return;
128873
129152
  }
128874
- enqueueForChat(chatKeyOf(arrived), async () => {
128875
- inheritFromParent(current, message.chatId, message.parentChatId);
128876
- const target2 = {
128877
- ...bindingOf2(current, message.chatId),
128878
- reply: anchorOfMessage(adapterFor(current.platform), message)
128879
- };
128880
- const chatKey = chatKeyOf(target2);
128881
- if (await handleCommand(target2, message.text, message.senderId)) return;
128882
- if (target2.projectId === "") {
128883
- const asked = message.text.trim();
128884
- if (/^[1-9]\d{0,2}$/u.test(asked) && listedProjectsByChat.has(chatKey)) {
128885
- await handleRepo(target2, asked);
128886
- } else if (projectPromptCooldown.tryAcquire(chatKey)) {
128887
- await promptProject(target2);
128888
- }
128889
- return;
128890
- }
128891
- const boundThread = chatOf(core.state, chatKey).threadId;
128892
- if (boundThread !== null && message.text !== "" && await questionFlow.answerByText(chatKey, boundThread, message.text)) {
128893
- return;
129153
+ enqueueForChat(chatKeyOf(arrived), () => resumeMessage(current, message));
129154
+ }
129155
+ });
129156
+ return { inboundHandlersFor, resumeMessage };
129157
+ };
129158
+
129159
+ // src/inboundStore.ts
129160
+ import * as NodeFSP from "node:fs/promises";
129161
+ import * as NodePath from "node:path";
129162
+ var SEEN_LIMIT = 1e3;
129163
+ var fileIo = {
129164
+ readFile: (path) => NodeFSP.readFile(path, "utf8"),
129165
+ mkdir: async (path) => {
129166
+ await NodeFSP.mkdir(path, { recursive: true });
129167
+ },
129168
+ writeFile: (path, contents) => NodeFSP.writeFile(path, contents, "utf8"),
129169
+ rename: (from, to) => NodeFSP.rename(from, to)
129170
+ };
129171
+ var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
129172
+ var decode = (contents) => {
129173
+ const value2 = JSON.parse(contents);
129174
+ if (!isRecord2(value2) || value2["version"] !== 1 || !isRecord2(value2["bots"]))
129175
+ throw new Error("invalid inbound store");
129176
+ const bots = /* @__PURE__ */ new Map();
129177
+ for (const [rowId, row] of Object.entries(value2["bots"])) {
129178
+ if (!isRecord2(row) || !Array.isArray(row["seenIds"]) || !row["seenIds"].every((id) => typeof id === "string"))
129179
+ throw new Error("invalid inbound bot row");
129180
+ const telegram = row["telegram"];
129181
+ if (telegram !== void 0 && (!isRecord2(telegram) || typeof telegram["tokenHash"] !== "string" || telegram["tokenHash"] === "" || typeof telegram["offset"] !== "number" || !Number.isSafeInteger(telegram["offset"]) || telegram["offset"] < 0))
129182
+ throw new Error("invalid telegram cursor");
129183
+ bots.set(rowId, {
129184
+ seenIds: row["seenIds"].slice(-SEEN_LIMIT),
129185
+ ...telegram === void 0 ? {} : {
129186
+ telegram: {
129187
+ tokenHash: telegram["tokenHash"],
129188
+ offset: telegram["offset"]
128894
129189
  }
128895
- const project = await projectOrNull(host.projects, target2.projectId);
128896
- if (project === null || project.archivedAt != null) {
128897
- if (project === null) {
128898
- host.log.warn("chat cannot start work: the chat's project is gone", {
128899
- bot: target2.displayName,
128900
- projectId: target2.projectId
128901
- });
128902
- }
128903
- if (archivedReplyCooldown.tryAcquire(chatKey)) {
128904
- await deliver(
128905
- target2,
128906
- project === null ? t("reply.projectMissing") : t("reply.projectArchived", { title: project.title })
128907
- );
129190
+ }
129191
+ });
129192
+ }
129193
+ return bots;
129194
+ };
129195
+ var makeInboundStore = (input) => {
129196
+ const io = input.io ?? fileIo;
129197
+ const path = NodePath.join(input.dataDir, "inbound.json");
129198
+ const temporary = `${path}.tmp`;
129199
+ let bots = /* @__PURE__ */ new Map();
129200
+ let dirty = false;
129201
+ let writing;
129202
+ const warn = (message, error) => input.log.warn(message, { error: describeError(error) });
129203
+ const ready = io.readFile(path).then(
129204
+ (contents) => {
129205
+ try {
129206
+ bots = decode(contents);
129207
+ } catch (error) {
129208
+ warn("chat could not restore inbound state", error);
129209
+ }
129210
+ },
129211
+ (error) => {
129212
+ if (!isRecord2(error) || error["code"] !== "ENOENT")
129213
+ warn("chat could not restore inbound state", error);
129214
+ }
129215
+ );
129216
+ const write = () => {
129217
+ writing ??= Promise.resolve().then(async () => {
129218
+ try {
129219
+ while (dirty) {
129220
+ dirty = false;
129221
+ const contents = JSON.stringify({ version: 1, bots: Object.fromEntries(bots) });
129222
+ try {
129223
+ await io.mkdir(input.dataDir);
129224
+ await io.writeFile(temporary, contents);
129225
+ await io.rename(temporary, path);
129226
+ } catch (error) {
129227
+ dirty = true;
129228
+ warn("chat could not save inbound state", error);
129229
+ break;
128908
129230
  }
128909
- return;
128910
- }
128911
- const routed = targetOf(target2);
128912
- const attachmentsOnly = message.text.trim() === "" && message.resources.length > 0;
128913
- if (routed.kind === "team") {
128914
- if (message.resources.length > 0) await deliver(target2, t("reply.attachmentsNoTeam"));
128915
- if (attachmentsOnly) return;
128916
- const leadThreadId = await startTeamGoal(target2, message);
128917
- if (leadThreadId !== null) await markWorking(target2, leadThreadId, message.messageId);
128918
- return;
128919
- }
128920
- if (attachmentsOnly) {
128921
- await receiveAttachmentsOnly(target2, message);
128922
- return;
128923
129231
  }
128924
- const branchChatId = hasWaitingAttachments(chatKey) ? void 0 : message.branchChatId;
128925
- if (branchChatId === void 0) {
128926
- await openTurn(target2, message);
128927
- return;
128928
- }
128929
- enqueueForChat(chatKeyOf({ rowId: current.rowId, chatId: branchChatId }), async () => {
128930
- inheritFromParent(current, branchChatId, message.chatId);
128931
- await openTurn({ ...bindingOf2(current, branchChatId), reply: target2.reply }, message);
128932
- });
128933
- });
129232
+ } finally {
129233
+ writing = void 0;
129234
+ }
129235
+ });
129236
+ return writing;
129237
+ };
129238
+ const changed = () => {
129239
+ dirty = true;
129240
+ void write();
129241
+ };
129242
+ const rowOf = (rowId) => {
129243
+ let row = bots.get(rowId);
129244
+ if (row === void 0) {
129245
+ row = { seenIds: [] };
129246
+ bots.set(rowId, row);
128934
129247
  }
128935
- });
128936
- return { inboundHandlersFor };
129248
+ return row;
129249
+ };
129250
+ return {
129251
+ ready,
129252
+ claim: (rowId, id) => {
129253
+ const row = rowOf(rowId);
129254
+ if (row.seenIds.includes(id)) return false;
129255
+ row.seenIds.push(id);
129256
+ if (row.seenIds.length > SEEN_LIMIT) row.seenIds.shift();
129257
+ changed();
129258
+ return true;
129259
+ },
129260
+ readCursor: (rowId, tokenHash) => {
129261
+ const cursor = bots.get(rowId)?.telegram;
129262
+ return cursor?.tokenHash === tokenHash ? cursor.offset : void 0;
129263
+ },
129264
+ saveCursor: (rowId, tokenHash, offset) => {
129265
+ const row = rowOf(rowId);
129266
+ if (row.telegram?.tokenHash === tokenHash && row.telegram.offset === offset) return;
129267
+ if (row.telegram?.tokenHash !== tokenHash) row.seenIds = [];
129268
+ row.telegram = { tokenHash, offset };
129269
+ changed();
129270
+ },
129271
+ flush: async () => {
129272
+ await ready;
129273
+ if (dirty || writing) await write();
129274
+ }
129275
+ };
128937
129276
  };
128938
129277
 
128939
129278
  // src/interactions.ts
@@ -130583,29 +130922,6 @@ var sleep = async (deps) => {
130583
130922
  }
130584
130923
  };
130585
130924
 
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
130925
  // src/lark/setup/cookieJar.ts
130610
130926
  var MAX_REDIRECT_HOPS = 10;
130611
130927
  var parseSetCookie = (responseUrl, header) => {
@@ -131323,21 +131639,7 @@ var buildChecklist = () => ({
131323
131639
  });
131324
131640
  var makeRealClock = () => ({
131325
131641
  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
- })
131642
+ sleep: abortableSleep
131341
131643
  });
131342
131644
  var saveAndConclude = async (ctx, updateSettings, result) => {
131343
131645
  try {
@@ -131429,6 +131731,9 @@ var registerLarkSetup = (ctx, options = {}) => {
131429
131731
  return runner;
131430
131732
  };
131431
131733
 
131734
+ // src/larkApi.ts
131735
+ import { parseRetryAfterMs } from "@ycodium-ai/plugin-api";
131736
+
131432
131737
  // src/larkPost.ts
131433
131738
  var styledText = (text4, style) => {
131434
131739
  let out = text4;
@@ -131608,6 +131913,33 @@ var parseLarkReply = (message) => {
131608
131913
  }
131609
131914
  };
131610
131915
 
131916
+ // src/sendRetry.ts
131917
+ var NO_RETRY = { retry: false };
131918
+ var SEND_RETRY_DELAYS_MS = [1e3, 4e3, 15e3];
131919
+ var MAX_WAIT_MS = 6e4;
131920
+ var SendStoppedError = class extends Error {
131921
+ constructor(cause) {
131922
+ super("send stopped before retrying", { cause });
131923
+ this.name = "SendStoppedError";
131924
+ }
131925
+ };
131926
+ var sendWithRetry = async (request, verdictOf, retry) => {
131927
+ for (let attempt = 0; ; attempt += 1) {
131928
+ try {
131929
+ return await request();
131930
+ } catch (error) {
131931
+ const delayMs = SEND_RETRY_DELAYS_MS[attempt];
131932
+ if (retry === void 0 || delayMs === void 0) throw error;
131933
+ const verdict = verdictOf(error);
131934
+ if (!verdict.retry) throw error;
131935
+ const waitMs = verdict.afterMs ?? delayMs;
131936
+ if (waitMs > MAX_WAIT_MS) throw error;
131937
+ await retry.sleep(waitMs);
131938
+ if (!await retry.shouldContinue()) throw new SendStoppedError(error);
131939
+ }
131940
+ }
131941
+ };
131942
+
131611
131943
  // src/splitMessage.ts
131612
131944
  var splitMessage = (text4, limit) => {
131613
131945
  if (text4.length <= limit) return [text4];
@@ -131640,19 +131972,39 @@ var LARK_TEXT_MAX_CHARS = 3500;
131640
131972
  var LarkApiError = class extends Error {
131641
131973
  operation;
131642
131974
  code;
131643
- constructor(operation, code2, message) {
131975
+ status;
131976
+ retryAfterMs;
131977
+ constructor(operation, code2, message, http = {}) {
131644
131978
  super(`lark ${operation} failed (code ${String(code2 ?? "?")}): ${message}`);
131645
131979
  this.name = "LarkApiError";
131646
131980
  this.operation = operation;
131647
131981
  this.code = code2;
131982
+ this.status = http.status;
131983
+ this.retryAfterMs = http.retryAfterMs;
131648
131984
  }
131649
131985
  };
131650
- var isLarkPermissionError = (error) => error instanceof LarkApiError && (error.code === 99991672 || error.code === 99991679 || "status" in error && (error.status === 401 || error.status === 403));
131986
+ var isLarkPermissionError = (error) => error instanceof LarkApiError && (error.code === 99991672 || error.code === 99991679 || error.status === 401 || error.status === 403);
131987
+ var LARK_RATE_LIMITED = 99991400;
131988
+ var isReplyRejected = (error) => error instanceof LarkApiError && error.status === void 0 && error.code !== LARK_RATE_LIMITED;
131989
+ var larkSendVerdict = (error) => {
131990
+ if (!(error instanceof LarkApiError)) return NO_RETRY;
131991
+ if (error.status === 429 || error.code === LARK_RATE_LIMITED) {
131992
+ return { retry: true, afterMs: error.retryAfterMs };
131993
+ }
131994
+ if (error.status !== void 0 && error.status >= 500) {
131995
+ return { retry: true, afterMs: error.retryAfterMs };
131996
+ }
131997
+ return NO_RETRY;
131998
+ };
131651
131999
  var readEnvelope = async (operation, response) => {
131652
132000
  if (!response.ok) {
131653
132001
  await response.body?.cancel().catch(() => void 0);
131654
- throw Object.assign(new LarkApiError(operation, void 0, `HTTP ${String(response.status)}`), {
131655
- status: response.status
132002
+ throw new LarkApiError(operation, void 0, `HTTP ${String(response.status)}`, {
132003
+ status: response.status,
132004
+ retryAfterMs: parseRetryAfterMs(
132005
+ response.headers.get("retry-after") ?? response.headers.get("x-ogw-ratelimit-reset"),
132006
+ Date.now()
132007
+ ) ?? void 0
131656
132008
  });
131657
132009
  }
131658
132010
  const body = await response.json();
@@ -131679,35 +132031,37 @@ var postMessage = async (input) => {
131679
132031
  authorization: `Bearer ${input.token}`,
131680
132032
  "content-type": "application/json; charset=utf-8"
131681
132033
  };
131682
- const message = { msg_type: input.msgType, content: JSON.stringify(input.content) };
131683
- const sentOf = async (response2) => (await readEnvelope(input.operation, response2)).data;
132034
+ const message = {
132035
+ msg_type: input.msgType,
132036
+ content: JSON.stringify(input.content),
132037
+ ...input.retry === void 0 ? {} : { uuid: crypto.randomUUID() }
132038
+ };
132039
+ const post = (url, body) => sendWithRetry(
132040
+ async () => (await readEnvelope(
132041
+ input.operation,
132042
+ await input.http(url, { method: "POST", headers, body: JSON.stringify(body) })
132043
+ )).data,
132044
+ larkSendVerdict,
132045
+ input.retry
132046
+ );
131684
132047
  if (input.reply !== void 0) {
131685
132048
  try {
131686
- const response2 = await input.http(
132049
+ const sent2 = await post(
131687
132050
  `${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
- }
132051
+ { ...message, ...input.reply.inThread ? { reply_in_thread: true } : {} }
131696
132052
  );
131697
- const sent = await sentOf(response2);
131698
- if (input.reply.inThread) input.onThreadReply?.(sent?.thread_id || void 0);
131699
- return sent?.message_id ?? null;
132053
+ if (input.reply.inThread) input.onThreadReply?.(sent2?.thread_id || void 0);
132054
+ return sent2?.message_id ?? null;
131700
132055
  } catch (error) {
131701
- if (!(error instanceof LarkApiError)) throw error;
132056
+ if (!isReplyRejected(error)) throw error;
131702
132057
  }
131703
132058
  }
131704
132059
  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 })
132060
+ const sent = await post(`${LARK_BASE}/im/v1/messages?receive_id_type=${idType}`, {
132061
+ receive_id: input.chatId,
132062
+ ...message
131709
132063
  });
131710
- return (await sentOf(response))?.message_id ?? null;
132064
+ return sent?.message_id ?? null;
131711
132065
  };
131712
132066
  var readMessage = async (input) => {
131713
132067
  const response = await input.http(
@@ -131962,9 +132316,9 @@ function getDecodeCache(exclude) {
131962
132316
  }
131963
132317
  return cache;
131964
132318
  }
131965
- function decode(string, exclude) {
132319
+ function decode2(string, exclude) {
131966
132320
  if (typeof exclude !== "string") {
131967
- exclude = decode.defaultChars;
132321
+ exclude = decode2.defaultChars;
131968
132322
  }
131969
132323
  const cache = getDecodeCache(exclude);
131970
132324
  return string.replace(/(%[a-f0-9]{2})+/gi, function(seq) {
@@ -132023,9 +132377,9 @@ function decode(string, exclude) {
132023
132377
  return result;
132024
132378
  });
132025
132379
  }
132026
- decode.defaultChars = ";/?:@&=+$,#";
132027
- decode.componentChars = "";
132028
- var decode_default = decode;
132380
+ decode2.defaultChars = ";/?:@&=+$,#";
132381
+ decode2.componentChars = "";
132382
+ var decode_default = decode2;
132029
132383
 
132030
132384
  // ../../node_modules/.pnpm/mdurl@2.1.0/node_modules/mdurl/lib/encode.mjs
132031
132385
  var encodeCache = {};
@@ -133046,12 +133400,12 @@ var LinkifyIt = class {
133046
133400
  *
133047
133401
  * See [twitter mentions example](https://github.com/markdown-it/linkify-it/blob/master/examples/twitter.mjs).
133048
133402
  */
133049
- add(schema, definition = null) {
133050
- if (!definition) delete this.__schemas__[schema];
133403
+ add(schema, definition2 = null) {
133404
+ if (!definition2) delete this.__schemas__[schema];
133051
133405
  else {
133052
133406
  const def = {
133053
133407
  normalize: (match, self2) => self2.normalize(match),
133054
- ...definition
133408
+ ...definition2
133055
133409
  };
133056
133410
  this.__schemas__[schema] = def;
133057
133411
  }
@@ -137310,7 +137664,7 @@ var UNSUPPORTED_MESSAGE_TYPES = /* @__PURE__ */ new Set([
137310
137664
  "share_chat",
137311
137665
  "share_user",
137312
137666
  "system",
137313
- "merge",
137667
+ "merge_forward",
137314
137668
  "interactive"
137315
137669
  ]);
137316
137670
  var botIdsOf = (mentions, appId) => {
@@ -137482,6 +137836,7 @@ var cardActionOperator = (event) => {
137482
137836
  }
137483
137837
  return null;
137484
137838
  };
137839
+ var isLarkAppId = (appId) => /^cli_[0-9a-fA-F]{16}$/u.test(appId);
137485
137840
  var startLarkInbound = (input) => {
137486
137841
  const dispatcher = new import_node_sdk.EventDispatcher({}).register({
137487
137842
  "im.message.receive_v1": (event) => {
@@ -137523,19 +137878,59 @@ var startLarkInbound = (input) => {
137523
137878
  return notice === null ? {} : { toast: { type: "info", content: notice } };
137524
137879
  }
137525
137880
  });
137881
+ if (!isLarkAppId(input.appId)) {
137882
+ input.onConnectionState?.({ state: "failed", reason: "invalid-app-id" });
137883
+ return { close: () => void 0 };
137884
+ }
137885
+ let closed = false;
137886
+ const connected = () => {
137887
+ if (!closed) input.onConnectionState?.({ state: "connected" });
137888
+ };
137889
+ const failed = (error) => {
137890
+ if (!closed) {
137891
+ input.onConnectionState?.({ state: "failed", reason: "disconnected", error });
137892
+ }
137893
+ };
137526
137894
  const client = new import_node_sdk.WSClient({
137527
137895
  appId: input.appId,
137528
137896
  appSecret: input.appSecret,
137529
137897
  ...input.domain === void 0 ? {} : { domain: input.domain },
137530
137898
  autoReconnect: true,
137531
- onError: (cause) => input.onError(cause.message)
137899
+ onReady: connected,
137900
+ onReconnected: connected,
137901
+ onReconnecting: () => failed(),
137902
+ onError: (cause) => {
137903
+ if (closed) return;
137904
+ failed(cause);
137905
+ input.onError(cause.message);
137906
+ }
137532
137907
  });
137533
- void client.start({ eventDispatcher: dispatcher });
137534
- return { close: () => client.close({ force: true }) };
137908
+ void client.start({ eventDispatcher: dispatcher }).catch(failed);
137909
+ return {
137910
+ close: () => {
137911
+ closed = true;
137912
+ client.close({ force: true });
137913
+ }
137914
+ };
137535
137915
  };
137536
137916
 
137537
- // src/replyReference.ts
137538
- import * as NodeCrypto2 from "node:crypto";
137917
+ // src/boundedSeen.ts
137918
+ var makeBoundedSeen = (limit) => {
137919
+ const seen = /* @__PURE__ */ new Set();
137920
+ return {
137921
+ has: (key) => seen.has(key),
137922
+ /** 记下这个键:第一次见返回 true;已经记着返回 false(不重排,最早记的先被忘)。 */
137923
+ claim: (key) => {
137924
+ if (seen.has(key)) return false;
137925
+ seen.add(key);
137926
+ if (seen.size > limit) {
137927
+ const oldest = seen.values().next().value;
137928
+ if (oldest !== void 0) seen.delete(oldest);
137929
+ }
137930
+ return true;
137931
+ }
137932
+ };
137933
+ };
137539
137934
 
137540
137935
  // src/deadline.ts
137541
137936
  var withDeadline2 = async (timeoutMs, load) => {
@@ -137554,6 +137949,7 @@ var withDeadline2 = async (timeoutMs, load) => {
137554
137949
  };
137555
137950
 
137556
137951
  // src/replyReference.ts
137952
+ import * as NodeCrypto3 from "node:crypto";
137557
137953
  var CONTENT_MAX = 8e3;
137558
137954
  var ATTACHMENTS_MAX = 20;
137559
137955
  var AUTHOR_MAX = 256;
@@ -137626,15 +138022,16 @@ var formatReplyReference = (reference2) => {
137626
138022
  ...lines.map((line) => `> ${line}`)
137627
138023
  ].join("\n");
137628
138024
  };
137629
- var textWithInboundContext = (message, rowId, t) => {
137630
- const reply = formatReplyReference(message.replyTo);
137631
- const text4 = reply === null ? message.text : `${reply}
137632
-
137633
- ${message.text}`;
138025
+ var textWithInboundContext = (message, rowId, t, topicRoot) => {
138026
+ const text4 = [
138027
+ formatReplyReference(topicRoot),
138028
+ formatReplyReference(message.replyTo),
138029
+ message.text
138030
+ ].filter((part) => part !== null).join("\n\n");
137634
138031
  if (message.chatKind !== "group" || message.senderId === null) return text4;
137635
138032
  let name = cleanString(message.senderName, AUTHOR_MAX).value;
137636
138033
  if (!name) {
137637
- const code2 = NodeCrypto2.createHash("sha256").update(JSON.stringify([rowId, message.senderId])).digest("hex").slice(0, 4);
138034
+ const code2 = NodeCrypto3.createHash("sha256").update(JSON.stringify([rowId, message.senderId])).digest("hex").slice(0, 4);
137638
138035
  name = t("source.member", { code: code2 });
137639
138036
  }
137640
138037
  const chat = cleanString(message.chatTitle, AUTHOR_MAX).value;
@@ -137687,6 +138084,26 @@ var makeLookup = (rowId, load) => {
137687
138084
  };
137688
138085
  var makeLarkMessageContext = (deps) => {
137689
138086
  let contactDenied = false;
138087
+ const deliveredRoots = makeBoundedSeen(CACHE_LIMIT);
138088
+ const loadReference = (messageId) => loadReplyReference(async (signal) => {
138089
+ const token = await deps.tenantToken();
138090
+ signal.throwIfAborted();
138091
+ return fetchMessageReference({ http: deps.http, token, messageId, signal });
138092
+ });
138093
+ const topicRootForSession = (message) => {
138094
+ const topic = message.topic;
138095
+ if (topic === void 0 || topic.rootGiven === false) return void 0;
138096
+ return async (recipientId) => {
138097
+ const key = JSON.stringify([message.chatId, topic.rootMessageId, recipientId]);
138098
+ if (deliveredRoots.has(key)) return void 0;
138099
+ return {
138100
+ reference: topic.rootMessageId === message.messageId ? void 0 : await loadReference(topic.rootMessageId),
138101
+ confirmConsumed: () => {
138102
+ deliveredRoots.claim(key);
138103
+ }
138104
+ };
138105
+ };
138106
+ };
137690
138107
  const userName = makeLookup(deps.rowId, async (openId, signal) => {
137691
138108
  try {
137692
138109
  const token = await deps.tenantToken();
@@ -137713,7 +138130,7 @@ var makeLarkMessageContext = (deps) => {
137713
138130
  });
137714
138131
  return name instanceof Promise || chat instanceof Promise ? Promise.all([name, chat]).then(([senderName, info]) => fields(senderName, info)) : fields(name, chat);
137715
138132
  };
137716
- return { chatInfo, forMessage };
138133
+ return { chatInfo, forMessage, loadReference, topicRootForSession };
137717
138134
  };
137718
138135
 
137719
138136
  // src/larkAdapter.ts
@@ -137755,6 +138172,7 @@ var larkConfigIssues = (binding) => {
137755
138172
  const { appId, appSecret } = credentials(binding);
137756
138173
  const issues = [];
137757
138174
  if (appId === "") issues.push({ column: "larkAppId", code: "required" });
138175
+ else if (!isLarkAppId(appId)) issues.push({ column: "larkAppId", code: "format" });
137758
138176
  if (appSecret === "") issues.push({ column: "larkAppSecret", code: "required" });
137759
138177
  return issues;
137760
138178
  };
@@ -137842,7 +138260,7 @@ var makeLarkAdapter = (deps) => {
137842
138260
  return rootMessageId === void 0 ? void 0 : groupId;
137843
138261
  },
137844
138262
  validate: larkConfigIssues,
137845
- isConfigured: (binding) => larkConfigIssues(binding).length === 0,
138263
+ isConfigured: (binding) => larkConfigIssues(binding).every((issue) => issue.code !== "required"),
137846
138264
  connect: (binding, handlers) => {
137847
138265
  const { appId, appSecret } = credentials(binding);
137848
138266
  const port = {
@@ -137871,6 +138289,7 @@ var makeLarkAdapter = (deps) => {
137871
138289
  topicOnMention: topicOnMentionOf(binding),
137872
138290
  isSoloGroup: soloGroupFor(context),
137873
138291
  onError: handlers.onError,
138292
+ ...handlers.onConnectionState === void 0 ? {} : { onConnectionState: handlers.onConnectionState },
137874
138293
  onCardAction: handlers.onButton,
137875
138294
  onPanelClick: (click, operatorOpenId) => handlers.onPanelClick?.({
137876
138295
  chatId: click.chatId,
@@ -137883,14 +138302,7 @@ var makeLarkAdapter = (deps) => {
137883
138302
  const { topic } = message;
137884
138303
  const source = context.forMessage(message);
137885
138304
  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
- );
138305
+ const reference2 = parentMessageId === void 0 ? null : context.loadReference(parentMessageId);
137894
138306
  const deliver = (names, replyTo) => handlers.onMessage({
137895
138307
  messageId: message.messageId,
137896
138308
  // 已经开出来的话题就是它自己那个聊天;将要开出来的(主窗口里 @ 机器人)先算群的,
@@ -137906,6 +138318,7 @@ var makeLarkAdapter = (deps) => {
137906
138318
  senderId: message.senderOpenId,
137907
138319
  ...names,
137908
138320
  text: message.text,
138321
+ topicRootForSession: context.topicRootForSession(message),
137909
138322
  ...replyTo === void 0 ? {} : { replyTo },
137910
138323
  chatKind: message.chatType === "p2p" ? "private" : "group",
137911
138324
  replyInThread: message.inThread,
@@ -137960,7 +138373,8 @@ var makeLarkAdapter = (deps) => {
137960
138373
  const messageId = await sendChatText({
137961
138374
  ...target2,
137962
138375
  receiveIdType,
137963
- text: chunk
138376
+ text: chunk,
138377
+ retry: options?.retry
137964
138378
  });
137965
138379
  deps.log.info("lark delivered a message", { messageId, bot: binding.displayName });
137966
138380
  }
@@ -137974,6 +138388,9 @@ var makeLarkAdapter = (deps) => {
137974
138388
  detail: "missing app id or secret in saved config"
137975
138389
  };
137976
138390
  }
138391
+ if (!isLarkAppId(appId)) {
138392
+ return { stage: "credentials", ok: false, detail: "malformed app id in saved config" };
138393
+ }
137977
138394
  try {
137978
138395
  const { expiresInSeconds } = await fetchTenantToken({ http: deps.http, appId, appSecret });
137979
138396
  return {
@@ -138295,7 +138712,7 @@ var en_default = {
138295
138712
  },
138296
138713
  wecomAppListenPort: {
138297
138714
  label: "Local callback port (optional)",
138298
- description: "Defaults to 30987. Self-built apps listening on the same address share one port.",
138715
+ 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
138716
  placeholder: "30987"
138300
138717
  },
138301
138718
  wecomAppListenHost: {
@@ -138313,6 +138730,7 @@ var en_default = {
138313
138730
  }
138314
138731
  },
138315
138732
  reply: {
138733
+ wecomDeliveryFailed: "This reply didn't reach {{recipient}} in WeCom: they are outside this app's visible range. Add them to the app's visible range in the WeCom admin console, then ask the agent to send it again.",
138316
138734
  conclusionRecovered: "(Finished while restarting; delivered now)",
138317
138735
  notAllowed: "You are not on this bot's allow list, so this tap had no effect.",
138318
138736
  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.',
@@ -138641,6 +139059,7 @@ var en_default = {
138641
139059
  }
138642
139060
  },
138643
139061
  repo: {
139062
+ autoSelected: "Using project {{title}}.",
138644
139063
  needProject: "This chat has no project yet, so I won't start any work. Pick one and I will.",
138645
139064
  currentLine: 'This chat works on project "{{title}}".',
138646
139065
  noProjects: "There are no projects on this machine yet. Add one in Ycodium first.",
@@ -138668,13 +139087,16 @@ var en_default = {
138668
139087
  description: "Verify platform credentials still work; does not open a session or call a model.",
138669
139088
  running: "Checking {{rowId}}\u2026",
138670
139089
  done: "Connection OK",
138671
- failed: "Connection failed (ref {{ref}})",
139090
+ failed: "Connection failed",
138672
139091
  notSaved: "Save this row first (credentials are read from saved config only).",
138673
139092
  hint: {
138674
- badToken: "This does not look like a bot token. Copy it again from BotFather: digits, a colon, then the key (ref {{ref}})",
138675
- unauthorized: "Telegram does not accept this token. It may have been revoked; get the current one from BotFather (ref {{ref}})",
138676
- notBot: "This token does not belong to a bot. Use the token BotFather gave you (ref {{ref}})",
138677
- webhookActive: "This bot has a webhook set by another service, so it cannot receive messages here. Remove the webhook there, then check again (ref {{ref}})"
139093
+ badToken: "This does not look like a bot token. Copy it again from BotFather: digits, a colon, then the key.",
139094
+ unauthorized: "Telegram does not accept this token. It may have been revoked; get the current one from BotFather.",
139095
+ notBot: "This token does not belong to a bot. Use the token BotFather gave you.",
139096
+ webhookActive: "This bot has a webhook set by another service, so it cannot receive messages here. Remove the webhook there, then check again.",
139097
+ wecomTrustedIp: "Add this machine's outbound public IP to the app's trusted IP list in the WeCom admin console.",
139098
+ wecomInvalidUser: "Check that the recipient, department or tag exists and is in this app's visible range.",
139099
+ wecomInvalidAgent: "Check the AgentId against this self-built application in the WeCom admin console."
138678
139100
  }
138679
139101
  },
138680
139102
  copyDiagnostics: {
@@ -138682,24 +139104,24 @@ var en_default = {
138682
139104
  description: "Run a connection probe and produce copy-paste diagnostics (no secrets).",
138683
139105
  running: "Building diagnostics for {{rowId}}\u2026",
138684
139106
  done: "Diagnostics ready \u2014 see details below",
138685
- failed: "Diagnostics failed (ref {{ref}})",
139107
+ failed: "Diagnostics failed",
138686
139108
  notSaved: "Save this row first before copying diagnostics."
138687
139109
  },
138688
139110
  diagnostic: {
138689
- ref: "Ref {{ref}}\n{{detail}}"
139111
+ ref: "Reference: {{ref}}\n{{detail}}"
138690
139112
  },
138691
139113
  larkRepair: {
138692
139114
  label: "Repair scopes",
138693
- description: "Verify the Lark app can still mint a token and list scope / publish steps.",
139115
+ description: "Check whether the saved credentials can mint a token; scopes, event subscriptions and publishing are not checked automatically.",
138694
139116
  step: {
138695
- scopes: "Confirm message / resource / panel scopes",
138696
- events: "Confirm long connection and event subscriptions",
138697
- publish: "Publish if scopes were added",
139117
+ scopes: "Scopes \u2014 not checked",
139118
+ events: "Event subscriptions \u2014 not checked",
139119
+ publish: "Publishing \u2014 not checked",
138698
139120
  verify: "Verify the app can still mint a tenant token"
138699
139121
  },
138700
- done: "Credentials work; add any missing scopes in the Feishu console and publish",
138701
- hint: "Ref {{ref}}\n{{detail}}\nIf scopes are missing, request them on the Feishu open platform and publish; a chat /repair command will follow.",
138702
- failed: "Could not verify the Lark app (ref {{ref}})",
139122
+ done: "Only token issuance was verified. Scopes, event subscriptions and publishing were not checked automatically; review them on the Feishu open platform.",
139123
+ hint: "Reference: {{ref}}\n{{detail}}\nReview scopes, event subscriptions and publishing on the Feishu open platform; none were checked automatically.",
139124
+ failed: "Could not mint a token with the saved credentials. Scopes, event subscriptions and publishing were not checked automatically; review them on the Feishu open platform.",
138703
139125
  notLark: "This row is not a saved Lark bot."
138704
139126
  }
138705
139127
  },
@@ -138738,7 +139160,23 @@ var en_default = {
138738
139160
  removeFailed: "Couldn't delete this bot.",
138739
139161
  status: {
138740
139162
  ready: "Ready",
138741
- incomplete: "Incomplete"
139163
+ incomplete: "Incomplete",
139164
+ connecting: "Connecting",
139165
+ connected: "Connected",
139166
+ notConnected: "Not connected"
139167
+ },
139168
+ connection: {
139169
+ failed: "The last connection attempt failed.",
139170
+ details: "Details: {{error}}",
139171
+ reason: {
139172
+ "not-available": "This platform can't be connected yet.",
139173
+ "invalid-app-id": "The App ID isn't in the right format.",
139174
+ disconnected: "The connection to the platform is down.",
139175
+ "subscription-failed": "The platform didn't confirm the subscription.",
139176
+ "token-unavailable": "Couldn't get an access token.",
139177
+ "credentials-rejected": "The platform rejected the credentials."
139178
+ },
139179
+ duplicate: "Uses the same credentials as \u201C{{name}}\u201D. Only that row connects."
138742
139180
  },
138743
139181
  secret: {
138744
139182
  saved: "Saved. Leave blank to keep it.",
@@ -138977,7 +139415,7 @@ var zh_CN_default = {
138977
139415
  },
138978
139416
  wecomAppListenPort: {
138979
139417
  label: "\u672C\u673A\u56DE\u8C03\u7AEF\u53E3\uFF08\u53EF\u9009\uFF09",
138980
- description: "\u9ED8\u8BA4 30987\u3002\u76D1\u542C\u5730\u5740\u76F8\u540C\u7684\u81EA\u5EFA\u5E94\u7528\u5171\u7528\u4E00\u4E2A\u7AEF\u53E3\u3002",
139418
+ description: "\u9ED8\u8BA4 30987\u3002\u76D1\u542C\u5730\u5740\u76F8\u540C\u7684\u81EA\u5EFA\u5E94\u7528\u5171\u7528\u4E00\u4E2A\u7AEF\u53E3\uFF1B\u76D1\u542C\u5730\u5740\u4E0D\u540C\u7684\u4E24\u884C\uFF08\u6BD4\u5982 0.0.0.0 \u548C 127.0.0.1\uFF09\u8981\u7528\u4E0D\u540C\u7AEF\u53E3\uFF0C\u5426\u5219\u540E\u4E00\u4E2A\u76D1\u542C\u4E0D\u8D77\u6765\u3002",
138981
139419
  placeholder: "30987"
138982
139420
  },
138983
139421
  wecomAppListenHost: {
@@ -138995,6 +139433,7 @@ var zh_CN_default = {
138995
139433
  }
138996
139434
  },
138997
139435
  reply: {
139436
+ wecomDeliveryFailed: "\u8FD9\u6761\u56DE\u590D\u6CA1\u9001\u5230\u4F01\u4E1A\u5FAE\u4FE1\u91CC\u7684 {{recipient}}\uFF1A\u5BF9\u65B9\u4E0D\u5728\u8FD9\u4E2A\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\u3002\u5230\u4F01\u4E1A\u5FAE\u4FE1\u7BA1\u7406\u540E\u53F0\u628A\u4ED6\u4EEC\u52A0\u8FDB\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\uFF0C\u518D\u8BA9 agent \u91CD\u53D1\u4E00\u6B21\u3002",
138998
139437
  conclusionRecovered: "\uFF08\u91CD\u542F\u671F\u95F4\u5B8C\u6210\uFF0C\u73B0\u5728\u8865\u53D1\uFF09",
138999
139438
  notAllowed: "\u4F60\u4E0D\u5728\u8FD9\u4E2A\u673A\u5668\u4EBA\u7684\u5141\u8BB8\u540D\u5355\u91CC\uFF0C\u8FD9\u6B21\u70B9\u51FB\u6CA1\u6709\u751F\u6548\u3002",
139000
139439
  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",
@@ -139323,6 +139762,7 @@ var zh_CN_default = {
139323
139762
  }
139324
139763
  },
139325
139764
  repo: {
139765
+ autoSelected: "\u4F7F\u7528\u9879\u76EE\u300C{{title}}\u300D\u3002",
139326
139766
  needProject: "\u8FD9\u4E2A\u804A\u5929\u8FD8\u6CA1\u9009\u9879\u76EE\uFF0C\u9009\u5B9A\u4E4B\u524D\u6211\u4E0D\u5F00\u5DE5\u3002\u9009\u4E00\u4E2A\u5C31\u5F00\u59CB\u3002",
139327
139767
  currentLine: "\u8FD9\u4E2A\u804A\u5929\u73B0\u5728\u5728\u9879\u76EE\u300C{{title}}\u300D\u4E0A\u3002",
139328
139768
  noProjects: "\u8FD9\u53F0\u673A\u5668\u4E0A\u8FD8\u6CA1\u6709\u9879\u76EE\uFF0C\u5148\u5230 Ycodium \u91CC\u6DFB\u52A0\u4E00\u4E2A\u3002",
@@ -139350,13 +139790,16 @@ var zh_CN_default = {
139350
139790
  description: "\u9A8C\u8BC1\u5E73\u53F0\u51ED\u636E\u662F\u5426\u4ECD\u53EF\u7528\uFF1B\u4E0D\u4F1A\u5F00\u4F1A\u8BDD\uFF0C\u4E5F\u4E0D\u4F1A\u8C03\u6A21\u578B\u3002",
139351
139791
  running: "\u6B63\u5728\u68C0\u67E5 {{rowId}} \u2026",
139352
139792
  done: "\u8FDE\u63A5\u6B63\u5E38",
139353
- failed: "\u8FDE\u63A5\u5931\u8D25\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139793
+ failed: "\u8FDE\u63A5\u5931\u8D25",
139354
139794
  notSaved: "\u8BF7\u5148\u4FDD\u5B58\u8FD9\u4E00\u884C\u518D\u68C0\u67E5\u8FDE\u63A5\uFF08\u51ED\u636E\u53EA\u4ECE\u5DF2\u4FDD\u5B58\u914D\u7F6E\u8BFB\u53D6\uFF09\u3002",
139355
139795
  hint: {
139356
- badToken: "\u8FD9\u4E0D\u50CF\u673A\u5668\u4EBA\u7684 token\u3002\u5230 BotFather \u91CD\u65B0\u590D\u5236\u4E00\u4EFD\uFF1A\u6570\u5B57\u3001\u5192\u53F7\uFF0C\u540E\u9762\u662F\u5BC6\u94A5\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139357
- unauthorized: "Telegram \u4E0D\u8BA4\u8FD9\u4E2A token\uFF0C\u53EF\u80FD\u5DF2\u7ECF\u4F5C\u5E9F\u3002\u5230 BotFather \u53D6\u73B0\u5728\u7684\u90A3\u4E00\u4EFD\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139358
- notBot: "\u8FD9\u4E2A token \u4E0D\u662F\u673A\u5668\u4EBA\u7684\u3002\u8BF7\u7528 BotFather \u7ED9\u7684\u90A3\u4E00\u4EFD\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139359
- webhookActive: "\u8FD9\u4E2A\u673A\u5668\u4EBA\u88AB\u522B\u7684\u670D\u52A1\u8BBE\u4E86 webhook\uFF0C\u5728\u8FD9\u91CC\u6536\u4E0D\u5230\u6D88\u606F\u3002\u5148\u5230\u90A3\u8FB9\u64A4\u6389 webhook\uFF0C\u518D\u68C0\u67E5\u4E00\u6B21\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09"
139796
+ 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",
139797
+ 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",
139798
+ notBot: "\u8FD9\u4E2A token \u4E0D\u662F\u673A\u5668\u4EBA\u7684\u3002\u8BF7\u7528 BotFather \u7ED9\u7684\u90A3\u4E00\u4EFD\u3002",
139799
+ 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",
139800
+ 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",
139801
+ 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",
139802
+ wecomInvalidAgent: "\u6838\u5BF9\u4F01\u4E1A\u5FAE\u4FE1\u540E\u53F0\u8FD9\u4E2A\u81EA\u5EFA\u5E94\u7528\u7684 AgentId\u3002"
139360
139803
  }
139361
139804
  },
139362
139805
  copyDiagnostics: {
@@ -139364,24 +139807,24 @@ var zh_CN_default = {
139364
139807
  description: "\u8DD1\u4E00\u6B21\u8FDE\u63A5\u63A2\u6D4B\uFF0C\u751F\u6210\u53EF\u590D\u5236\u7684\u8BCA\u65AD\u6587\u672C\uFF08\u4E0D\u542B\u5BC6\u94A5\uFF09\u3002",
139365
139808
  running: "\u6B63\u5728\u751F\u6210\u8BCA\u65AD {{rowId}} \u2026",
139366
139809
  done: "\u8BCA\u65AD\u5DF2\u751F\u6210\uFF0C\u89C1\u4E0B\u65B9\u8BE6\u60C5",
139367
- failed: "\u8BCA\u65AD\u5931\u8D25\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139810
+ failed: "\u8BCA\u65AD\u5931\u8D25",
139368
139811
  notSaved: "\u8BF7\u5148\u4FDD\u5B58\u8FD9\u4E00\u884C\u518D\u590D\u5236\u8BCA\u65AD\u3002"
139369
139812
  },
139370
139813
  diagnostic: {
139371
- ref: "\u53C2\u8003\u53F7 {{ref}}\n{{detail}}"
139814
+ ref: "\u53C2\u8003\u53F7\uFF1A{{ref}}\n{{detail}}"
139372
139815
  },
139373
139816
  larkRepair: {
139374
139817
  label: "\u8865\u5168\u6743\u9650",
139375
- description: "\u6838\u5BF9\u98DE\u4E66\u5E94\u7528\u4ECD\u80FD\u6362 token\uFF0C\u5E76\u63D0\u793A\u9700\u8981\u8865\u7684\u6743\u9650\u4E0E\u53D1\u5E03\u6B65\u9AA4\u3002",
139818
+ description: "\u68C0\u67E5\u5DF2\u4FDD\u5B58\u7684\u51ED\u636E\u80FD\u5426\u6362\u53D6 token\uFF1B\u6743\u9650\u3001\u4E8B\u4EF6\u8BA2\u9605\u548C\u53D1\u5E03\u4E0D\u4F1A\u81EA\u52A8\u68C0\u67E5\u3002",
139376
139819
  step: {
139377
- scopes: "\u786E\u8BA4\u6D88\u606F / \u8D44\u6E90 / \u9762\u677F\u7B49\u6743\u9650",
139378
- events: "\u786E\u8BA4\u957F\u8FDE\u63A5\u4E0E\u4E8B\u4EF6\u8BA2\u9605",
139379
- publish: "\u5982\u6709\u589E\u91CF\u6743\u9650\uFF0C\u5B8C\u6210\u53D1\u5E03\u5BA1\u6279",
139820
+ scopes: "\u6743\u9650\uFF1A\u672A\u68C0\u67E5",
139821
+ events: "\u4E8B\u4EF6\u8BA2\u9605\uFF1A\u672A\u68C0\u67E5",
139822
+ publish: "\u53D1\u5E03\uFF1A\u672A\u68C0\u67E5",
139380
139823
  verify: "\u9A8C\u8BC1\u5E94\u7528\u4ECD\u53EF\u6362\u53D6 tenant token"
139381
139824
  },
139382
- done: "\u51ED\u636E\u53EF\u7528\uFF1B\u82E5\u6743\u9650\u4E0D\u8DB3\u8BF7\u6309\u98DE\u4E66\u63A7\u5236\u53F0\u8865\u9F50\u540E\u53D1\u5E03",
139383
- hint: "\u53C2\u8003\u53F7 {{ref}}\n{{detail}}\n\u7F3A\u6743\u9650\u65F6\u53EF\u5728\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u5BF9\u8BE5\u5E94\u7528\u589E\u91CF\u7533\u8BF7\u5E76\u53D1\u5E03\uFF1B\u804A\u5929\u4FA7 /repair \u547D\u4EE4\u540E\u7EED\u8865\u9F50\u3002",
139384
- failed: "\u65E0\u6CD5\u9A8C\u8BC1\u98DE\u4E66\u5E94\u7528\uFF08\u53C2\u8003\u53F7 {{ref}}\uFF09",
139825
+ 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",
139826
+ 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",
139827
+ failed: "\u5DF2\u4FDD\u5B58\u7684\u51ED\u636E\u672A\u80FD\u6362\u53D6 token\uFF1B\u6743\u9650\u3001\u4E8B\u4EF6\u8BA2\u9605\u548C\u53D1\u5E03\u6CA1\u6709\u81EA\u52A8\u68C0\u67E5\uFF0C\u8BF7\u5230\u98DE\u4E66\u5F00\u653E\u5E73\u53F0\u6838\u5BF9\u3002",
139385
139828
  notLark: "\u8FD9\u4E00\u884C\u4E0D\u662F\u5DF2\u4FDD\u5B58\u7684\u98DE\u4E66\u673A\u5668\u4EBA\u3002"
139386
139829
  }
139387
139830
  },
@@ -139419,8 +139862,24 @@ var zh_CN_default = {
139419
139862
  copyFailed: "\u6CA1\u590D\u5236\u4E0A\u3002",
139420
139863
  removeFailed: "\u6CA1\u80FD\u5220\u6389\u8FD9\u4E2A\u673A\u5668\u4EBA\u3002",
139421
139864
  status: {
139422
- ready: "\u53EF\u7528",
139423
- incomplete: "\u8FD8\u6CA1\u586B\u5B8C"
139865
+ ready: "\u5C31\u7EEA",
139866
+ incomplete: "\u5F85\u8865\u5168",
139867
+ connecting: "\u8FDE\u63A5\u4E2D",
139868
+ connected: "\u5DF2\u8FDE\u63A5",
139869
+ notConnected: "\u672A\u8FDE\u63A5"
139870
+ },
139871
+ connection: {
139872
+ failed: "\u4E0A\u6B21\u8FDE\u63A5\u6CA1\u6709\u6210\u529F\u3002",
139873
+ details: "\u8BE6\u60C5\uFF1A{{error}}",
139874
+ reason: {
139875
+ "not-available": "\u8FD9\u4E2A\u5E73\u53F0\u8FD8\u4E0D\u80FD\u8FDE\u63A5\u3002",
139876
+ "invalid-app-id": "App ID \u683C\u5F0F\u4E0D\u6B63\u786E\u3002",
139877
+ disconnected: "\u548C\u5E73\u53F0\u7684\u8FDE\u63A5\u65AD\u4E86\u3002",
139878
+ "subscription-failed": "\u5E73\u53F0\u6CA1\u6709\u786E\u8BA4\u8BA2\u9605\u3002",
139879
+ "token-unavailable": "\u6CA1\u80FD\u6362\u5230\u8BBF\u95EE\u4EE4\u724C\u3002",
139880
+ "credentials-rejected": "\u5E73\u53F0\u62D2\u7EDD\u4E86\u51ED\u636E\u3002"
139881
+ },
139882
+ duplicate: "\u548C\u300C{{name}}\u300D\u7528\u7684\u662F\u540C\u4E00\u4EFD\u51ED\u636E\uFF0C\u53EA\u8FDE\u90A3\u4E00\u884C\u3002"
139424
139883
  },
139425
139884
  secret: {
139426
139885
  saved: "\u5DF2\u7ECF\u5B58\u8FC7\uFF0C\u7559\u7A7A\u5C31\u4FDD\u6301\u539F\u503C\u3002",
@@ -139601,6 +140060,16 @@ var makeModelPlan = (core, deps) => {
139601
140060
  // src/notifications.ts
139602
140061
  import { readThreadFailed, threadOrNull as threadOrNull5 } from "@ycodium-ai/plugin-api";
139603
140062
 
140063
+ // src/conclusionOutbox.ts
140064
+ var enqueueConclusion = (core, ledger, binding, debt, send) => core.outbound(
140065
+ chatKeyOf(binding),
140066
+ async () => {
140067
+ if (debt === void 0) await send();
140068
+ else await ledger.deliver(debt, send);
140069
+ },
140070
+ { droppable: debt !== void 0 }
140071
+ );
140072
+
139604
140073
  // src/conclusionRecovery.ts
139605
140074
  var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
139606
140075
  var recoverConclusions = async (core, ledger, delivery, failures) => {
@@ -139608,6 +140077,12 @@ var recoverConclusions = async (core, ledger, delivery, failures) => {
139608
140077
  core.host.onDispose(() => {
139609
140078
  disposed = true;
139610
140079
  });
140080
+ const couldNotRecover = (debt, error) => {
140081
+ core.host.log.warn("chat could not recover a conclusion", {
140082
+ ...debt,
140083
+ error: describeError(error)
140084
+ });
140085
+ };
139611
140086
  await core.ready;
139612
140087
  for (const debt of Object.values(conclusionDebtsOf(core.state))) {
139613
140088
  if (disposed) return;
@@ -139634,24 +140109,27 @@ var recoverConclusions = async (core, ledger, delivery, failures) => {
139634
140109
  }
139635
140110
  const binding = core.bindingForChatKey(debt.chatKey);
139636
140111
  if (binding === void 0) continue;
139637
- await ledger.deliver(debt, async () => {
139638
- if (latest.state === "error") {
139639
- return delivery.failure(
139640
- binding,
139641
- debt.threadId,
139642
- failures.forFailure(debt.threadId, execution.session?.lastError),
139643
- true,
139644
- latest.turnId
139645
- );
140112
+ enqueueConclusion(core, ledger, binding, debt, async (owed) => {
140113
+ try {
140114
+ if (latest.state === "error") {
140115
+ return delivery.sendText(
140116
+ binding,
140117
+ debt.threadId,
140118
+ failures.forFailure(debt.threadId, execution.session?.lastError),
140119
+ true,
140120
+ latest.turnId,
140121
+ owed
140122
+ );
140123
+ }
140124
+ const send = await delivery.prepareConclusion(debt.threadId, latest.turnId);
140125
+ return send(binding, { recovered: true, streaming: false, owed });
140126
+ } catch (error) {
140127
+ couldNotRecover(debt, error);
140128
+ return false;
139646
140129
  }
139647
- const send = await delivery.prepareConclusion(debt.threadId, latest.turnId);
139648
- return send(binding, { recovered: true, streaming: false });
139649
140130
  });
139650
140131
  } catch (error) {
139651
- core.host.log.warn("chat could not recover a conclusion", {
139652
- ...debt,
139653
- error: describeError(error)
139654
- });
140132
+ couldNotRecover(debt, error);
139655
140133
  }
139656
140134
  }
139657
140135
  };
@@ -139674,7 +140152,7 @@ var outcomeOfSession = (status) => {
139674
140152
  var THREAD_PROJECT_CACHE_LIMIT = 256;
139675
140153
  var makeNotifications = (core, deps) => {
139676
140154
  const { host, adapterFor, bindingForChatKey, ready } = core;
139677
- const { clearWorking } = deps;
140155
+ const { takeWorking } = deps;
139678
140156
  const { turnCards } = deps.replyCards;
139679
140157
  const { failures, conclusions, delivery } = deps;
139680
140158
  void recoverConclusions(core, conclusions, delivery, failures);
@@ -139697,6 +140175,11 @@ var makeNotifications = (core, deps) => {
139697
140175
  });
139698
140176
  const streamsTo = (binding, threadId) => adapterFor(binding.platform)?.turnCards !== void 0 && owns(binding, threadId);
139699
140177
  const owns = (binding, threadId) => chatOfThread(core.state, threadId) === chatKeyOf(binding);
140178
+ const later = (binding, work) => {
140179
+ core.outbound(chatKeyOf(binding), async () => {
140180
+ await work();
140181
+ });
140182
+ };
139700
140183
  const threadsWithSomethingToReport = /* @__PURE__ */ new Set();
139701
140184
  host.reactor({
139702
140185
  name: "im.assistant-messages",
@@ -139722,29 +140205,38 @@ var makeNotifications = (core, deps) => {
139722
140205
  const owes = Object.values(conclusionDebtsOf(core.state)).some(
139723
140206
  (debt2) => debt2.threadId === threadId
139724
140207
  );
139725
- const { debt, handled, execution } = said || owes ? await conclusions.forTurn(threadId) : { debt: void 0, handled: false, execution: null };
140208
+ const { debt, handled, execution } = said || owes || targets.length > 0 ? await conclusions.forTurn(threadId) : { debt: void 0, handled: false, execution: null };
139726
140209
  if (debt !== void 0) {
139727
140210
  const binding = bindingForChatKey(debt.chatKey);
139728
140211
  if (binding !== void 0 && !streamsTo(binding, threadId) && execution?.session?.activeTurnId == null && execution?.latestTurn?.state !== "running" && execution?.latestTurn?.state !== "error" && payload.session?.status !== "error") {
139729
- await conclusions.deliver(debt, async () => {
139730
- const send = await delivery.prepareConclusion(
139731
- threadId,
139732
- execution?.latestTurn?.turnId ?? null
139733
- );
139734
- return send(binding, { recovered: false, streaming: false });
140212
+ const turnId = execution?.latestTurn?.turnId ?? null;
140213
+ enqueueConclusion(core, conclusions, binding, debt, async (owed) => {
140214
+ const send = await delivery.prepareConclusion(threadId, turnId);
140215
+ return send(binding, { recovered: false, streaming: false, owed });
139735
140216
  });
139736
140217
  }
139737
140218
  } else if (said && !handled) {
139738
140219
  const send = await delivery.prepareConclusion(threadId);
139739
140220
  for (const binding of targets) {
139740
- await send(binding, {
139741
- recovered: false,
139742
- streaming: streamsTo(binding, threadId)
139743
- });
140221
+ const streaming = streamsTo(binding, threadId);
140222
+ enqueueConclusion(
140223
+ core,
140224
+ conclusions,
140225
+ binding,
140226
+ void 0,
140227
+ () => send(binding, { recovered: false, streaming })
140228
+ );
139744
140229
  }
139745
140230
  }
139746
140231
  const outcome = outcomeOfSession(payload.session?.status);
139747
- for (const binding of targets) await clearWorking(binding, threadId, outcome);
140232
+ for (const binding of targets) {
140233
+ const withdraw = takeWorking(binding, threadId, outcome);
140234
+ turnCards.afterTurn(
140235
+ threadId,
140236
+ () => later(binding, withdraw),
140237
+ execution?.latestTurn?.turnId ?? null
140238
+ );
140239
+ }
139748
140240
  }
139749
140241
  });
139750
140242
  host.events.on("thread.failed", async (event) => {
@@ -139753,7 +140245,7 @@ var makeNotifications = (core, deps) => {
139753
140245
  threadsWithSomethingToReport.delete(threadId);
139754
140246
  await ready;
139755
140247
  const targets = targetsForProject(threadId, await projectOfThread(threadId));
139756
- const { debt, handled } = await conclusions.forTurn(threadId);
140248
+ const { debt, handled, execution } = await conclusions.forTurn(threadId);
139757
140249
  const debtBinding = debt === void 0 ? void 0 : bindingForChatKey(debt.chatKey);
139758
140250
  const recipients = debt !== void 0 ? debtBinding === void 0 ? [] : [debtBinding] : targets;
139759
140251
  const textTargets = handled ? [] : recipients.filter(
@@ -139763,16 +140255,20 @@ var makeNotifications = (core, deps) => {
139763
140255
  const notice = textTargets.length > 0 || drawingTurn !== null ? failures.forFailure(threadId, readThreadFailed(event)?.detail) : null;
139764
140256
  if (notice !== null && drawingTurn !== null)
139765
140257
  failures.holdForCard(threadId, drawingTurn, notice);
139766
- for (const binding of targets) {
139767
- await clearWorking(binding, threadId, "failed");
139768
- }
139769
140258
  if (notice !== null) {
139770
140259
  for (const binding of textTargets) {
139771
- const send = () => delivery.failure(binding, threadId, notice, false);
139772
- if (debt !== void 0) await conclusions.deliver(debt, send);
139773
- else await send();
140260
+ const send = (owed) => delivery.sendText(binding, threadId, notice, false, null, owed);
140261
+ enqueueConclusion(core, conclusions, binding, debt, send);
139774
140262
  }
139775
140263
  }
140264
+ for (const binding of targets) {
140265
+ const withdraw = takeWorking(binding, threadId, "failed");
140266
+ turnCards.afterTurn(
140267
+ threadId,
140268
+ () => later(binding, withdraw),
140269
+ execution?.latestTurn?.turnId ?? null
140270
+ );
140271
+ }
139776
140272
  });
139777
140273
  const rememberThreadProject = (threadId, projectId) => {
139778
140274
  projectByThread.set(threadId, projectId);
@@ -140333,32 +140829,6 @@ var makeProjectRows = (core) => {
140333
140829
  return { projectRowsFor, listedProjectsByChat };
140334
140830
  };
140335
140831
 
140336
- // src/diagnostics.ts
140337
- var PREFIX = {
140338
- conn: "IM-CONN",
140339
- msg: "IM-MSG",
140340
- setup: "IM-SETUP"
140341
- };
140342
- var shortSalt = () => {
140343
- const bytes = new Uint8Array(2);
140344
- crypto.getRandomValues(bytes);
140345
- return Array.from(bytes, (b) => b.toString(16).toUpperCase().padStart(2, "0")).join("");
140346
- };
140347
- var diagnosticRef = (kind, nowMs = Date.now()) => {
140348
- const stamp = nowMs.toString(36).toUpperCase();
140349
- return `${PREFIX[kind]}-${stamp}-${shortSalt()}`;
140350
- };
140351
- var formatDiagnosticCopy = (report) => [
140352
- `ref=${report.ref}`,
140353
- `ok=${report.ok ? "yes" : "no"}`,
140354
- `platform=${report.platform}`,
140355
- `bot=${report.botName}`,
140356
- `row=${report.rowId}`,
140357
- `stage=${report.stage}`,
140358
- `at=${report.at}`,
140359
- `detail=${report.detail}`
140360
- ].join("\n");
140361
-
140362
140832
  // src/failureNotices.ts
140363
140833
  var makeFailureNotices = (core) => {
140364
140834
  const heldForCard = /* @__PURE__ */ new Map();
@@ -140450,20 +140920,12 @@ var trailingReply = (messages) => {
140450
140920
  var ROTATE = "rotate";
140451
140921
  var isDrawing = (live) => live.card !== null || live.reopen;
140452
140922
  var RECENT_ENDED_LIMIT = 256;
140923
+ var turnKey = (threadId, turnId) => JSON.stringify([threadId, turnId]);
140453
140924
  var makeTurnCards = (deps) => {
140454
140925
  const slots = /* @__PURE__ */ new Map();
140455
- const recentEnded = /* @__PURE__ */ new Map();
140926
+ const recentEnded = makeBoundedSeen(RECENT_ENDED_LIMIT);
140456
140927
  const erroredThreads = /* @__PURE__ */ new Set();
140457
140928
  let disposed = false;
140458
- const rememberEnded = (threadId, turnId) => {
140459
- if (turnId === null) return;
140460
- recentEnded.delete(threadId);
140461
- recentEnded.set(threadId, turnId);
140462
- if (recentEnded.size > RECENT_ENDED_LIMIT) {
140463
- const oldest = recentEnded.keys().next().value;
140464
- if (oldest !== void 0) recentEnded.delete(oldest);
140465
- }
140466
- };
140467
140929
  const readTexts = async (live, progress) => {
140468
140930
  const texts = /* @__PURE__ */ new Map();
140469
140931
  for (const segment of progress.segments) {
@@ -140515,29 +140977,28 @@ var makeTurnCards = (deps) => {
140515
140977
  const turnId = progress.turnId ?? live?.turnId ?? null;
140516
140978
  const target2 = live?.target ?? deps.resolveTarget(threadId, turnId);
140517
140979
  if (target2 === null) return;
140518
- rememberEnded(threadId, turnId);
140519
140980
  if (end.status === "error") erroredThreads.add(threadId);
140520
140981
  const title = live?.title ?? await deps.titleOf(threadId);
140521
140982
  const view = await finalView(title, progress, live, end);
140522
- if (live?.card != null) {
140523
- const card = live.card;
140524
- const sent = await card.finish(view);
140525
- deps.onClosed(card.cardId);
140526
- if (deps.finalize !== void 0)
140527
- await deps.finalize(threadId, turnId, target2.chatKey, async () => sent);
140528
- return;
140529
- }
140530
- const send = async () => {
140531
- const card = await openCard(target2, view);
140532
- return card !== null || await deps.fallback(threadId, turnId, target2.chatKey, view);
140533
- };
140534
- if (deps.finalize === void 0) await send();
140535
- else await deps.finalize(threadId, turnId, target2.chatKey, send);
140983
+ const card = live?.card ?? null;
140984
+ const finished = card !== null && await card.finish(view);
140985
+ if (card !== null) deps.onClosed(card.cardId);
140986
+ const send = async () => finished || card === null && await openCard(target2, view) !== null;
140987
+ const sent = deps.finalize === void 0 ? await send() : await deps.finalize(threadId, turnId, target2.chatKey, send);
140988
+ if (!sent) await deps.fallback(threadId, turnId, target2.chatKey, view);
140989
+ if (turnId !== null) recentEnded.claim(turnKey(threadId, turnId));
140990
+ const callbacks = slot.afterTurn.get(turnId) ?? [];
140991
+ slot.afterTurn.delete(turnId);
140992
+ if (turnId !== null) {
140993
+ callbacks.push(...slot.afterTurn.get(null) ?? []);
140994
+ slot.afterTurn.delete(null);
140995
+ }
140996
+ for (const after of callbacks) after();
140536
140997
  };
140537
140998
  const handle = async (slot, threadId, item) => {
140538
140999
  await deps.ready;
140539
141000
  if (disposed) return;
140540
- if (item.turnId !== null && recentEnded.get(threadId) === item.turnId) return;
141001
+ if (item.turnId !== null && recentEnded.has(turnKey(threadId, item.turnId))) return;
140541
141002
  let live = slot.live;
140542
141003
  if (live !== null) {
140543
141004
  const sameTurn = item.turnId === null || live.turnId === null || live.turnId === item.turnId;
@@ -140621,7 +141082,11 @@ var makeTurnCards = (deps) => {
140621
141082
  }
140622
141083
  } finally {
140623
141084
  slot.running = false;
140624
- if (slot.live === null && slot.queue.length === 0) slots.delete(threadId);
141085
+ if (slot.live === null && slot.queue.length === 0) {
141086
+ slots.delete(threadId);
141087
+ for (const callbacks of slot.afterTurn.values()) for (const after of callbacks) after();
141088
+ slot.afterTurn.clear();
141089
+ }
140625
141090
  }
140626
141091
  };
140627
141092
  return {
@@ -140630,7 +141095,7 @@ var makeTurnCards = (deps) => {
140630
141095
  if (disposed) return;
140631
141096
  let slot = slots.get(threadId);
140632
141097
  if (slot === void 0) {
140633
- slot = { queue: [], running: false, live: null };
141098
+ slot = { queue: [], running: false, live: null, afterTurn: /* @__PURE__ */ new Map() };
140634
141099
  slots.set(threadId, slot);
140635
141100
  }
140636
141101
  const last = slot.queue.at(-1);
@@ -140644,6 +141109,18 @@ var makeTurnCards = (deps) => {
140644
141109
  void pump(threadId, slot);
140645
141110
  }
140646
141111
  },
141112
+ /** 撤回执只等所属轮次收尾;没有轮次 id 时仍等当前线程槽,不能在出站队列里等自己。 */
141113
+ afterTurn: (threadId, work, turnId = null) => {
141114
+ if (disposed) return;
141115
+ const slot = slots.get(threadId);
141116
+ if (slot === void 0 || turnId !== null && recentEnded.has(turnKey(threadId, turnId)))
141117
+ work();
141118
+ else {
141119
+ const callbacks = slot.afterTurn.get(turnId) ?? [];
141120
+ callbacks.push(work);
141121
+ slot.afterTurn.set(turnId, callbacks);
141122
+ }
141123
+ },
140647
141124
  /**
140648
141125
  * 审批或提问答完了:封存这条会话开着的卡,下一拍另开一张。立刻返回;没有在画的卡、或
140649
141126
  * 平台不换卡,就什么都不做。
@@ -140670,7 +141147,10 @@ var makeTurnCards = (deps) => {
140670
141147
  /** 插件停用:放下所有在画的卡,不再发任何更新。卡会停在最后一拍,重启时被标「已中断」。 */
140671
141148
  dispose: () => {
140672
141149
  disposed = true;
140673
- for (const slot of slots.values()) slot.live?.card?.abandon();
141150
+ for (const slot of slots.values()) {
141151
+ slot.live?.card?.abandon();
141152
+ slot.afterTurn.clear();
141153
+ }
140674
141154
  slots.clear();
140675
141155
  }
140676
141156
  };
@@ -140678,7 +141158,7 @@ var makeTurnCards = (deps) => {
140678
141158
 
140679
141159
  // src/replyCards.ts
140680
141160
  var makeReplyCards = (core, failures, anchors, conclusions, delivery) => {
140681
- const { host, adapterFor, commit, deliver, bindingForChatKey, bindingOfThread, ready } = core;
141161
+ const { host, adapterFor, commit, bindingForChatKey, bindingOfThread, ready } = core;
140682
141162
  const stopRequested = /* @__PURE__ */ new Set();
140683
141163
  const turnCards = makeTurnCards({
140684
141164
  ready,
@@ -140737,22 +141217,28 @@ var makeReplyCards = (core, failures, anchors, conclusions, delivery) => {
140737
141217
  onClosed: (cardId) => commit("turn-card-closed", { cardId }),
140738
141218
  finalize: async (threadId, turnId, chatKey, send) => {
140739
141219
  const { debt, handled } = await conclusions.forTurn(threadId, turnId);
140740
- if (handled) return;
140741
- if (debt === void 0) {
140742
- await send();
140743
- return;
140744
- }
140745
- if (!await conclusions.deliver(debt, send)) return;
141220
+ if (handled) return true;
141221
+ if (debt === void 0) return send();
141222
+ if (!await conclusions.deliver(debt, send)) return false;
140746
141223
  const binding = bindingForChatKey(chatKey);
140747
- if (binding === void 0) return;
141224
+ if (binding === void 0) return true;
140748
141225
  const sendConclusion = await delivery.prepareConclusion(threadId, turnId);
140749
141226
  await sendConclusion(binding, { recovered: false, streaming: true });
141227
+ return true;
140750
141228
  },
140751
141229
  fallback: async (threadId, turnId, chatKey, view) => {
141230
+ const { debt, handled } = await conclusions.forTurn(threadId, turnId);
141231
+ if (handled) return;
140752
141232
  const binding = bindingForChatKey(chatKey);
140753
- if (binding === void 0) return false;
141233
+ if (binding === void 0) return;
140754
141234
  const text4 = [view.body, view.errorText ?? ""].filter((part) => part !== "").join("\n\n");
140755
- return deliver(await anchors.anchored(binding, threadId, turnId), text4);
141235
+ enqueueConclusion(
141236
+ core,
141237
+ conclusions,
141238
+ binding,
141239
+ debt,
141240
+ (owed) => delivery.sendText(binding, threadId, text4, false, turnId, owed)
141241
+ );
140756
141242
  },
140757
141243
  schedule: (run, delayMs) => {
140758
141244
  const timer = setTimeout(run, delayMs);
@@ -141122,8 +141608,8 @@ var makeSessions = (core) => {
141122
141608
  };
141123
141609
 
141124
141610
  // src/teamGoals.ts
141125
- import * as NodeCrypto3 from "node:crypto";
141126
- var isRecord2 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
141611
+ import * as NodeCrypto4 from "node:crypto";
141612
+ var isRecord3 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
141127
141613
  var teamGoalRequestId = (binding, messageId) => {
141128
141614
  const identity = JSON.stringify([
141129
141615
  "im",
@@ -141132,7 +141618,7 @@ var teamGoalRequestId = (binding, messageId) => {
141132
141618
  binding.chatId,
141133
141619
  messageId
141134
141620
  ]);
141135
- return NodeCrypto3.createHash("sha256").update(identity).digest("hex");
141621
+ return NodeCrypto4.createHash("sha256").update(identity).digest("hex");
141136
141622
  };
141137
141623
  var makeTeamGoals = (core) => {
141138
141624
  const { host, t, deliver, commit } = core;
@@ -141153,17 +141639,19 @@ var makeTeamGoals = (core) => {
141153
141639
  };
141154
141640
  const startTeamGoal = async (binding, message) => {
141155
141641
  try {
141642
+ const requestId = teamGoalRequestId(binding, message.messageId);
141643
+ const topicRoot = await message.topicRootForSession?.(requestId);
141156
141644
  const result = await host.plugins.invoke({
141157
141645
  pluginId: "team",
141158
141646
  command: "startGoal",
141159
141647
  payload: {
141160
141648
  projectId: binding.projectId,
141161
- goal: textWithInboundContext(message, binding.rowId, t),
141649
+ goal: textWithInboundContext(message, binding.rowId, t, topicRoot?.reference),
141162
141650
  titleSeed: message.text,
141163
- requestId: teamGoalRequestId(binding, message.messageId)
141651
+ requestId
141164
141652
  }
141165
141653
  });
141166
- if (isRecord2(result) && result._tag === "TeamGoalRejection") {
141654
+ if (isRecord3(result) && result._tag === "TeamGoalRejection") {
141167
141655
  await deliver(
141168
141656
  binding,
141169
141657
  t(
@@ -141172,9 +141660,10 @@ var makeTeamGoals = (core) => {
141172
141660
  );
141173
141661
  return null;
141174
141662
  }
141175
- if (!isRecord2(result) || typeof result.leadThreadId !== "string" || result.leadThreadId === "" || result.leadName !== null && typeof result.leadName !== "string") {
141663
+ if (!isRecord3(result) || typeof result.leadThreadId !== "string" || result.leadThreadId === "" || result.leadName !== null && typeof result.leadName !== "string") {
141176
141664
  throw new Error("team startGoal returned an invalid result");
141177
141665
  }
141666
+ topicRoot?.confirmConsumed();
141178
141667
  commit("team-goal-started", { chatKey: chatKeyOf(binding), threadId: result.leadThreadId });
141179
141668
  await deliver(
141180
141669
  binding,
@@ -141194,6 +141683,9 @@ var makeTeamGoals = (core) => {
141194
141683
  return { hasRosterFor, startTeamGoal };
141195
141684
  };
141196
141685
 
141686
+ // src/telegramAdapter.ts
141687
+ import * as NodeCrypto5 from "node:crypto";
141688
+
141197
141689
  // src/telegramApi.ts
141198
141690
  var API_BASE = "https://api.telegram.org";
141199
141691
  var POLL_TIMEOUT_SECONDS = 45;
@@ -141218,6 +141710,10 @@ var target = (chatId) => {
141218
141710
  const { chat, thread } = splitChatId(chatId);
141219
141711
  return { chat_id: chat, ...thread === null ? {} : { message_thread_id: thread } };
141220
141712
  };
141713
+ var replyParameters = (messageId) => {
141714
+ const id = Number(messageId);
141715
+ return messageId === void 0 || !Number.isSafeInteger(id) ? {} : { reply_parameters: { message_id: id, allow_sending_without_reply: true } };
141716
+ };
141221
141717
  var chatOnly = (chatId) => ({ chat_id: splitChatId(chatId).chat });
141222
141718
  var TelegramApiError = class extends Error {
141223
141719
  code;
@@ -141230,6 +141726,10 @@ var TelegramApiError = class extends Error {
141230
141726
  this.retryAfterSeconds = retryAfterSeconds;
141231
141727
  }
141232
141728
  };
141729
+ var telegramSendVerdict = (error) => error instanceof TelegramApiError && (error.code === 429 || error.code === 503 && error.retryAfterSeconds !== void 0) ? {
141730
+ retry: true,
141731
+ afterMs: error.retryAfterSeconds === void 0 ? void 0 : error.retryAfterSeconds * 1e3
141732
+ } : NO_RETRY;
141233
141733
  var isNotModified = (error) => error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("message is not modified");
141234
141734
  var PHOTO_NAME = "photo.jpg";
141235
141735
  var PHOTO_MIME = "image/jpeg";
@@ -141345,13 +141845,15 @@ var makeTelegramApi = (input) => {
141345
141845
  }
141346
141846
  return envelope.result;
141347
141847
  };
141348
- const upload = async (method, field, chatId, file) => {
141848
+ const upload = async (method, field, chatId, file, replyMessageId) => {
141349
141849
  const form = new FormData();
141350
141850
  const where = target(chatId);
141351
141851
  form.set("chat_id", where.chat_id);
141352
141852
  if (where.message_thread_id !== void 0) {
141353
141853
  form.set("message_thread_id", String(where.message_thread_id));
141354
141854
  }
141855
+ const reply = replyParameters(replyMessageId).reply_parameters;
141856
+ if (reply !== void 0) form.set("reply_parameters", JSON.stringify(reply));
141355
141857
  form.set(field, new Blob([new Uint8Array(file.bytes)], { type: file.mimeType }), file.name);
141356
141858
  const response = await send(
141357
141859
  method,
@@ -141374,9 +141876,10 @@ var makeTelegramApi = (input) => {
141374
141876
  (row) => row.map((button) => ({ text: button.text, callback_data: button.data }))
141375
141877
  )
141376
141878
  });
141377
- const sendSingle = async (method, chatId, body, rows) => {
141879
+ const sendSingle = async (method, chatId, body, rows, replyMessageId) => {
141378
141880
  const sent = await call(method, {
141379
141881
  ...target(chatId),
141882
+ ...replyParameters(replyMessageId),
141380
141883
  ...body,
141381
141884
  ...rows === void 0 ? {} : { reply_markup: keyboard(rows) }
141382
141885
  });
@@ -141436,21 +141939,26 @@ var makeTelegramApi = (input) => {
141436
141939
  },
141437
141940
  POLL_CALL_TIMEOUT_MS
141438
141941
  ),
141439
- sendMessage: async (chatId, text4) => {
141440
- for (const chunk of splitForTelegram(text4)) {
141441
- await call("sendMessage", { ...target(chatId), text: chunk });
141942
+ sendMessage: async (chatId, text4, retry, replyMessageId) => {
141943
+ for (const [index, chunk] of splitForTelegram(text4).entries()) {
141944
+ const reply = index === 0 ? replyMessageId : void 0;
141945
+ await sendWithRetry(
141946
+ () => sendSingle("sendMessage", chatId, { text: chunk }, void 0, reply),
141947
+ telegramSendVerdict,
141948
+ retry
141949
+ );
141442
141950
  }
141443
141951
  },
141444
141952
  /**
141445
141953
  * 发一条消息,回它的 id。调用方保证 `text` 不超过单条上限(边写边更新的回复要自己
141446
141954
  * 在上限处翻篇,这里不替它切)。带 `rows` 就挂一块内联键盘。
141447
141955
  */
141448
- sendOne: (chatId, text4, rows) => sendSingle("sendMessage", chatId, { text: text4 }, rows),
141956
+ sendOne: (chatId, text4, rows, replyMessageId) => sendSingle("sendMessage", chatId, { text: text4 }, rows, replyMessageId),
141449
141957
  /**
141450
141958
  * 发一条富文本,回它的 id。`markdown` 已是平台要的样子。带 `rows` 就挂一块内联键盘。
141451
141959
  * 调用方保证不超富文本单条上限(这里不替它切)。
141452
141960
  */
141453
- sendRichOne: (chatId, markdown, rows) => sendSingle("sendRichMessage", chatId, { rich_message: { markdown } }, rows),
141961
+ sendRichOne: (chatId, markdown, rows, replyMessageId) => sendSingle("sendRichMessage", chatId, { rich_message: { markdown } }, rows, replyMessageId),
141454
141962
  /**
141455
141963
  * 改一条已发出的消息。内容没变(`message is not modified`)当成功。`rows` 不给就是
141456
141964
  * 去掉按钮:改了文字又要保留按钮的调用方得自己再传一次。
@@ -141462,20 +141970,21 @@ var makeTelegramApi = (input) => {
141462
141970
  */
141463
141971
  editRich: (chatId, messageId, markdown, rows = []) => editSingle(chatId, messageId, { rich_message: { markdown } }, rows),
141464
141972
  /** 带按钮的审批消息,一行一个按钮;回消息 id,之后才改得了它(平台没给就是 null)。 */
141465
- sendButtons: (chatId, text4, buttons) => sendSingle(
141973
+ sendButtons: (chatId, text4, buttons, replyMessageId) => sendSingle(
141466
141974
  "sendMessage",
141467
141975
  chatId,
141468
141976
  { text: text4 },
141469
- buttons.map((button) => [{ text: button.label, data: button.token }])
141977
+ buttons.map((button) => [{ text: button.label, data: button.token }]),
141978
+ replyMessageId
141470
141979
  ),
141471
141980
  /** 答完点击,不然客户端上那个按钮一直转圈。带 `text` 时点击者会看到一句提示。 */
141472
141981
  answerCallback: (callbackQueryId, text4) => call("answerCallbackQuery", {
141473
141982
  callback_query_id: callbackQueryId,
141474
141983
  ...text4 === void 0 ? {} : { text: text4 }
141475
141984
  }),
141476
- sendPhoto: (chatId, photo) => upload("sendPhoto", "photo", chatId, photo),
141985
+ sendPhoto: (chatId, photo, replyMessageId) => upload("sendPhoto", "photo", chatId, photo, replyMessageId),
141477
141986
  /** 以文件形式发(不压缩、不限尺寸比例):什么类型都行,音频与视频也走这条。 */
141478
- sendDocument: (chatId, file) => upload("sendDocument", "document", chatId, file),
141987
+ sendDocument: (chatId, file, replyMessageId) => upload("sendDocument", "document", chatId, file, replyMessageId),
141479
141988
  /** 取一个文件的字节。要两步:先换路径,再按路径下载。 */
141480
141989
  fetchFile: async (fileId, maxBytes) => {
141481
141990
  const file = await call("getFile", {
@@ -141687,7 +142196,7 @@ var makeTurnWriter = (deps) => {
141687
142196
  const put = async (text4) => {
141688
142197
  const bounded = text4.length <= deps.limit ? text4 : text4.slice(0, deps.limit);
141689
142198
  if (live === null) {
141690
- const id = await deps.messages.send(bounded);
142199
+ const id = await deps.messages.send(bounded, first === null);
141691
142200
  if (id === null) throw new Error("telegram sent a message without an id");
141692
142201
  live = id;
141693
142202
  first ??= id;
@@ -141827,19 +142336,18 @@ var makeTelegramAdapter = (deps) => {
141827
142336
  limit: TELEGRAM_TEXT_MAX_CHARS,
141828
142337
  ...resume === void 0 ? {} : { resume },
141829
142338
  messages: {
141830
- send: (text4) => withRichFallback(
141831
- toTelegramRichMarkdown(text4),
141832
- async (markdown) => (await apiFor(slot.binding)).sendRichOne(chatId, markdown),
141833
- async () => (await apiFor(slot.binding)).sendOne(chatId, text4)
141834
- ),
142339
+ send: (text4, first) => {
142340
+ const reply = first ? slot.binding.reply?.messageId : void 0;
142341
+ return withRichFallback(
142342
+ toTelegramRichMarkdown(text4),
142343
+ async (markdown) => (await apiFor(slot.binding)).sendRichOne(chatId, markdown, void 0, reply),
142344
+ async () => (await apiFor(slot.binding)).sendOne(chatId, text4, void 0, reply)
142345
+ );
142346
+ },
141835
142347
  edit: (messageId, text4) => withRichFallback(
141836
142348
  toTelegramRichMarkdown(text4),
141837
- async (markdown) => {
141838
- await (await apiFor(slot.binding)).editRich(chatId, messageId, markdown);
141839
- },
141840
- async () => {
141841
- await (await apiFor(slot.binding)).editMessage(chatId, messageId, text4);
141842
- }
142349
+ async (markdown) => (await apiFor(slot.binding)).editRich(chatId, messageId, markdown),
142350
+ async () => (await apiFor(slot.binding)).editMessage(chatId, messageId, text4)
141843
142351
  )
141844
142352
  }
141845
142353
  });
@@ -141847,6 +142355,7 @@ var makeTelegramAdapter = (deps) => {
141847
142355
  platform: "telegram",
141848
142356
  icon: "telegram",
141849
142357
  label: "Telegram",
142358
+ quotesReplies: true,
141850
142359
  columns: COLUMNS2,
141851
142360
  validate: telegramConfigIssues,
141852
142361
  isConfigured: (binding) => telegramConfigIssues(binding).length === 0,
@@ -141855,7 +142364,12 @@ var makeTelegramAdapter = (deps) => {
141855
142364
  };
141856
142365
  const stopped = new Promise((resolve) => stop = resolve);
141857
142366
  let running = true;
141858
- let offset = 0;
142367
+ const tokenHash = NodeCrypto5.createHash("sha256").update(tokenOf(binding)).digest("hex");
142368
+ let offset = deps.readCursor(binding.rowId, tokenHash);
142369
+ const advanceOffset = (update) => {
142370
+ offset = nextOffset([update], offset ?? 0);
142371
+ deps.saveCursor(binding.rowId, tokenHash, offset);
142372
+ };
141859
142373
  let acknowledged = 0;
141860
142374
  let backoff = BACKOFF_START_MS;
141861
142375
  let botUsername;
@@ -141868,7 +142382,7 @@ var makeTelegramAdapter = (deps) => {
141868
142382
  });
141869
142383
  const publishAck = () => {
141870
142384
  const api = liveApi;
141871
- if (api === void 0 || offset <= acknowledged) return;
142385
+ if (api === void 0 || offset === void 0 || offset <= acknowledged) return;
141872
142386
  const next = offset;
141873
142387
  acknowledged = next;
141874
142388
  void api.getUpdates(next, 0).catch(() => void 0);
@@ -141876,6 +142390,7 @@ var makeTelegramAdapter = (deps) => {
141876
142390
  const poll = async () => {
141877
142391
  for (; ; ) {
141878
142392
  if (!running) return;
142393
+ let checkingConnection = true;
141879
142394
  try {
141880
142395
  const api = await apiFor(binding);
141881
142396
  liveApi = api;
@@ -141884,6 +142399,7 @@ var makeTelegramAdapter = (deps) => {
141884
142399
  const own = await api.getMe();
141885
142400
  botUsername = own.username ?? null;
141886
142401
  botId = own.id;
142402
+ if (running) handlers.onConnectionState?.({ state: "connected" });
141887
142403
  }
141888
142404
  if (!running) return;
141889
142405
  if (!menuSent) {
@@ -141895,9 +142411,17 @@ var makeTelegramAdapter = (deps) => {
141895
142411
  });
141896
142412
  });
141897
142413
  }
142414
+ if (offset === void 0) {
142415
+ const backlog = await api.getUpdates(-1, 0);
142416
+ if (!running) return;
142417
+ offset = nextOffset(backlog, 0);
142418
+ deps.saveCursor(binding.rowId, tokenHash, offset);
142419
+ }
141898
142420
  acknowledged = offset;
141899
142421
  const updates = await api.getUpdates(offset);
141900
142422
  if (!running) return;
142423
+ handlers.onConnectionState?.({ state: "connected" });
142424
+ checkingConnection = false;
141901
142425
  backoff = BACKOFF_START_MS;
141902
142426
  for (const update of updates) {
141903
142427
  if (!running) break;
@@ -141905,13 +142429,13 @@ var makeTelegramAdapter = (deps) => {
141905
142429
  if (callback?.data !== void 0) {
141906
142430
  const operatorId = callback.from === void 0 ? null : String(callback.from.id);
141907
142431
  const notice = callback.data.startsWith(PANEL_DATA_PREFIX) ? panelClick(binding.rowId, callback, operatorId, handlers) : handlers.onButton(callback.data, operatorId, callback.id);
141908
- offset = nextOffset([update], offset);
142432
+ advanceOffset(update);
141909
142433
  await api.answerCallback(callback.id, notice ?? void 0).catch(() => void 0);
141910
142434
  continue;
141911
142435
  }
141912
142436
  const message = update.message;
141913
142437
  if (message === void 0) {
141914
- offset = nextOffset([update], offset);
142438
+ advanceOffset(update);
141915
142439
  continue;
141916
142440
  }
141917
142441
  const resource = resourceOf2(message);
@@ -141920,7 +142444,7 @@ var makeTelegramAdapter = (deps) => {
141920
142444
  botUsername
141921
142445
  );
141922
142446
  if (text4 === null) {
141923
- offset = nextOffset([update], offset);
142447
+ advanceOffset(update);
141924
142448
  continue;
141925
142449
  }
141926
142450
  const gated = !passesGroupGate2({
@@ -141934,12 +142458,12 @@ var makeTelegramAdapter = (deps) => {
141934
142458
  const solo = binding.groupMention === "always" && await isSoloGroup(String(message.chat.id));
141935
142459
  if (!running) break;
141936
142460
  if (!solo) {
141937
- offset = nextOffset([update], offset);
142461
+ advanceOffset(update);
141938
142462
  continue;
141939
142463
  }
141940
142464
  }
141941
142465
  if (!running) break;
141942
- offset = nextOffset([update], offset);
142466
+ advanceOffset(update);
141943
142467
  const senderId = message.from === void 0 ? null : String(message.from.id);
141944
142468
  const senderName = telegramDisplayName(message.from);
141945
142469
  const chatKind = message.chat.type === "private" ? "private" : "group";
@@ -141982,6 +142506,7 @@ var makeTelegramAdapter = (deps) => {
141982
142506
  }
141983
142507
  } catch (error) {
141984
142508
  if (!running) return;
142509
+ if (checkingConnection) handlers.onConnectionState?.({ state: "failed", error });
141985
142510
  handlers.onError(error instanceof Error ? error.message : String(error));
141986
142511
  await Promise.race([(deps.sleep ?? defaultSleep)(backoff), stopped]);
141987
142512
  backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
@@ -141999,9 +142524,9 @@ var makeTelegramAdapter = (deps) => {
141999
142524
  }
142000
142525
  };
142001
142526
  },
142002
- sendText: async (binding, text4) => {
142527
+ sendText: async (binding, text4, options) => {
142003
142528
  const api = await apiFor(binding);
142004
- await api.sendMessage(binding.chatId, text4);
142529
+ await api.sendMessage(binding.chatId, text4, options?.retry, binding.reply?.messageId);
142005
142530
  },
142006
142531
  checkConnection: async (bot) => {
142007
142532
  const token = tokenOf(bot);
@@ -142070,25 +142595,30 @@ var makeTelegramAdapter = (deps) => {
142070
142595
  sendImage: async (binding, image2) => {
142071
142596
  const api = await apiFor(binding);
142072
142597
  try {
142073
- await api.sendPhoto(binding.chatId, image2);
142598
+ await api.sendPhoto(binding.chatId, image2, binding.reply?.messageId);
142074
142599
  } catch (error) {
142075
142600
  if (!(error instanceof TelegramApiError && error.code === 400)) throw error;
142076
142601
  deps.log.info("telegram could not send the picture as a photo, sending it as a file", {
142077
142602
  name: image2.name,
142078
142603
  reason: error.message
142079
142604
  });
142080
- await api.sendDocument(binding.chatId, image2);
142605
+ await api.sendDocument(binding.chatId, image2, binding.reply?.messageId);
142081
142606
  }
142082
142607
  },
142083
142608
  sendFile: async (binding, file) => {
142084
142609
  const api = await apiFor(binding);
142085
- await api.sendDocument(binding.chatId, file);
142610
+ await api.sendDocument(binding.chatId, file, binding.reply?.messageId);
142086
142611
  },
142087
142612
  // 单张照片 10 MB、单个文件 50 MB。这两个数来自 Bot API 文档的记忆,本仓库里没有核对过。
142088
142613
  outboundLimits: { imageBytes: 10 * 1024 * 1024, fileBytes: 50 * 1024 * 1024 },
142089
142614
  askApproval: async (binding, request) => {
142090
142615
  const api = await apiFor(binding);
142091
- return api.sendButtons(binding.chatId, request.body, request.options);
142616
+ return api.sendButtons(
142617
+ binding.chatId,
142618
+ request.body,
142619
+ request.options,
142620
+ binding.reply?.messageId
142621
+ );
142092
142622
  },
142093
142623
  settleApproval: async (binding, cardId, settlement) => {
142094
142624
  const api = await apiFor(binding);
@@ -142140,7 +142670,7 @@ ${line}`
142140
142670
  const api = await apiFor(binding);
142141
142671
  const rendered = renderPanel(card, 1);
142142
142672
  const cardId = await withFloodWait(
142143
- () => api.sendOne(binding.chatId, rendered.text, rendered.rows)
142673
+ () => api.sendOne(binding.chatId, rendered.text, rendered.rows, binding.reply?.messageId)
142144
142674
  );
142145
142675
  if (cardId !== null) {
142146
142676
  panelKeyboards.set(`${binding.rowId}:${binding.chatId}:${cardId}`, {
@@ -142165,7 +142695,9 @@ ${line}`
142165
142695
  open: async (binding, card) => {
142166
142696
  const api = await apiFor(binding);
142167
142697
  const rendered = renderChoiceCard(card);
142168
- return withFloodWait(() => api.sendOne(binding.chatId, rendered.text, rendered.rows));
142698
+ return withFloodWait(
142699
+ () => api.sendOne(binding.chatId, rendered.text, rendered.rows, binding.reply?.messageId)
142700
+ );
142169
142701
  },
142170
142702
  update: async (binding, cardId, card) => {
142171
142703
  const api = await apiFor(binding);
@@ -142260,6 +142792,7 @@ var makeTurns = (core, deps) => {
142260
142792
  const text4 = message.text;
142261
142793
  const threadId = await ensureThread(binding, text4 || message.resources[0]?.name || "Chat");
142262
142794
  if (threadId === null) return null;
142795
+ const topicRoot = await message.topicRootForSession?.(threadId);
142263
142796
  const carried = waitingFor(chatKey, threadId);
142264
142797
  pendingByChat.delete(chatKey);
142265
142798
  const own = await collectResources(
@@ -142297,7 +142830,10 @@ var makeTurns = (core, deps) => {
142297
142830
  message: {
142298
142831
  messageId: userMessageId,
142299
142832
  role: "user",
142300
- text: [textWithInboundContext(message, binding.rowId, t), ...notes].join("\n"),
142833
+ text: [
142834
+ textWithInboundContext(message, binding.rowId, t, topicRoot?.reference),
142835
+ ...notes
142836
+ ].join("\n"),
142301
142837
  attachments: attachments.map(
142302
142838
  (attachment) => attachment.type === "image" ? { ...attachment, type: "image" } : { ...attachment, type: "file" }
142303
142839
  )
@@ -142307,6 +142843,7 @@ var makeTurns = (core, deps) => {
142307
142843
  interactionMode: interactionModeOf(chatKey),
142308
142844
  ...intentAction !== void 0 ? { intentAction } : {}
142309
142845
  });
142846
+ topicRoot?.confirmConsumed();
142310
142847
  if (intentAction?.kind !== "steer") {
142311
142848
  deps.conclusions.owe({
142312
142849
  chatKey,
@@ -142435,8 +142972,15 @@ var ROW_ACTION_ITEM_SCHEMA = makeFlowItemSchema({
142435
142972
  doneRequired: ["messageKey", "fills"]
142436
142973
  });
142437
142974
  var botOf = (config2, adapters, rowId) => {
142438
- const adapterFor = (platform) => adapters.find((adapter) => adapter.platform === platform);
142439
- return readBots(config2, adapterFor).find((bot) => bot.rowId === rowId);
142975
+ const table2 = config2[BOTS_FIELD];
142976
+ if (typeof table2 !== "object" || table2 === null || Array.isArray(table2) || !Object.hasOwn(table2, rowId))
142977
+ return void 0;
142978
+ const row = table2[rowId];
142979
+ if (typeof row !== "object" || row === null || Array.isArray(row)) return void 0;
142980
+ const raw = row;
142981
+ const platform = platformOf(raw);
142982
+ const adapter = adapters.find((candidate) => candidate.platform === platform);
142983
+ return { ...readBotView(rowId, raw, adapter ?? { label: platform }), platform };
142440
142984
  };
142441
142985
  var reportOf = (bot, stage, ok, detail, hint) => ({
142442
142986
  ref: diagnosticRef("conn"),
@@ -142452,12 +142996,9 @@ var reportOf = (bot, stage, ok, detail, hint) => ({
142452
142996
  var probe = async (adapters, bot) => {
142453
142997
  const adapter = adapters.find((candidate) => candidate.platform === bot.platform);
142454
142998
  if (adapter?.checkConnection === void 0) {
142455
- return reportOf(bot, "platform", false, `unsupported platform ${bot.platform}`);
142456
- }
142457
- const redactor = makeRedactor();
142458
- for (const column of adapter.columns) {
142459
- if (column.kind === "password") redactor.register(bot.values[column.key]);
142999
+ return reportOf(bot, "platform", false, `unsupported platform ${bot.platform}`, "notAvailable");
142460
143000
  }
143001
+ const redactor = botRedactor(adapter.columns, bot.values);
142461
143002
  try {
142462
143003
  const result = await adapter.checkConnection(bot);
142463
143004
  return reportOf(
@@ -142482,10 +143023,15 @@ var emitReport = (emit, report, action) => {
142482
143023
  });
142483
143024
  return;
142484
143025
  }
143026
+ const suggested = report.hint !== void 0 && action.keyPrefix === "rowAction.checkConnection";
142485
143027
  emit({
142486
143028
  kind: "failed",
142487
- reasonKey: report.hint !== void 0 && action.keyPrefix === "rowAction.checkConnection" ? `rowAction.checkConnection.hint.${report.hint}` : `${action.keyPrefix}.failed`,
142488
- params: { ref: report.ref },
143029
+ reasonKey: suggested && report.hint !== void 0 ? connectionHintKey(report.hint) : `${action.keyPrefix}.failed`,
143030
+ // 参考号由界面写在细节第一行(「参考号:…」),结论句里不带。
143031
+ params: {
143032
+ ref: report.ref,
143033
+ ...suggested && report.hint !== void 0 ? { hint: report.hint } : {}
143034
+ },
142489
143035
  detail
142490
143036
  });
142491
143037
  };
@@ -142537,9 +143083,9 @@ var registerSettingsRowActions = (ctx, adapters) => {
142537
143083
  send({
142538
143084
  kind: "steps",
142539
143085
  steps: [
142540
- { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "pending" },
142541
- { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "pending" },
142542
- { id: "publish", labelKey: "rowAction.larkRepair.step.publish", state: "pending" },
143086
+ { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "skipped" },
143087
+ { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "skipped" },
143088
+ { id: "publish", labelKey: "rowAction.larkRepair.step.publish", state: "skipped" },
142543
143089
  { id: "verify", labelKey: "rowAction.larkRepair.step.verify", state: "running" }
142544
143090
  ]
142545
143091
  });
@@ -142556,9 +143102,9 @@ var registerSettingsRowActions = (ctx, adapters) => {
142556
143102
  send({
142557
143103
  kind: "steps",
142558
143104
  steps: [
142559
- { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "done" },
142560
- { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "done" },
142561
- { id: "publish", labelKey: "rowAction.larkRepair.step.publish", state: "done" },
143105
+ { id: "scopes", labelKey: "rowAction.larkRepair.step.scopes", state: "skipped" },
143106
+ { id: "events", labelKey: "rowAction.larkRepair.step.events", state: "skipped" },
143107
+ { id: "publish", labelKey: "rowAction.larkRepair.step.publish", state: "skipped" },
142562
143108
  {
142563
143109
  id: "verify",
142564
143110
  labelKey: "rowAction.larkRepair.step.verify",
@@ -142574,7 +143120,7 @@ var registerSettingsRowActions = (ctx, adapters) => {
142574
143120
  warnings: [
142575
143121
  {
142576
143122
  key: "rowAction.larkRepair.hint",
142577
- params: { ref, detail: formatDiagnosticCopy({ ...report, ref }) }
143123
+ params: { ref, detail: formatDiagnosticCopy(report) }
142578
143124
  }
142579
143125
  ]
142580
143126
  });
@@ -142584,14 +143130,14 @@ var registerSettingsRowActions = (ctx, adapters) => {
142584
143130
  kind: "failed",
142585
143131
  reasonKey: "rowAction.larkRepair.failed",
142586
143132
  params: { ref },
142587
- detail: formatDiagnosticCopy({ ...report, ref })
143133
+ detail: formatDiagnosticCopy(report)
142588
143134
  });
142589
143135
  }
142590
143136
  });
142591
143137
  };
142592
143138
 
142593
143139
  // src/settingsView.ts
142594
- var isRecord3 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
143140
+ var isRecord4 = (value2) => typeof value2 === "object" && value2 !== null && !Array.isArray(value2);
142595
143141
  var hasStoredSecret = (row, key) => {
142596
143142
  const value2 = row[key];
142597
143143
  return typeof value2 === "string" && value2 !== "";
@@ -142609,10 +143155,10 @@ var targetTable = () => ({
142609
143155
  });
142610
143156
  var targetViews = (config2) => {
142611
143157
  const table2 = config2[DELIVERY_TARGETS_FIELD];
142612
- if (!isRecord3(table2)) return [];
143158
+ if (!isRecord4(table2)) return [];
142613
143159
  const columns = deliveryTargetColumns();
142614
143160
  return Object.entries(table2).flatMap(([rowId, raw]) => {
142615
- if (!isRecord3(raw)) return [];
143161
+ if (!isRecord4(raw)) return [];
142616
143162
  const issues = targetIssues(raw);
142617
143163
  const values = Object.fromEntries(
142618
143164
  columns.map((column) => [column.key, readString(raw, column.key)])
@@ -142624,9 +143170,9 @@ var buildSettingsView = (config2, adapters) => {
142624
143170
  const columns = buildBotColumns(adapters);
142625
143171
  const table2 = config2[BOTS_FIELD];
142626
143172
  const bots = [];
142627
- if (isRecord3(table2)) {
143173
+ if (isRecord4(table2)) {
142628
143174
  for (const [rowId, raw] of Object.entries(table2)) {
142629
- if (!isRecord3(raw)) continue;
143175
+ if (!isRecord4(raw)) continue;
142630
143176
  const platform = platformOf(raw);
142631
143177
  const values = {};
142632
143178
  const secrets = {};
@@ -142689,10 +143235,10 @@ var applyRowEdit = (config2, { field, columns }, edit) => {
142689
143235
  for (const key of clearSecrets) {
142690
143236
  if (!secretColumns.has(key)) return columnError("not-a-secret", key);
142691
143237
  }
142692
- const table2 = isRecord3(config2[field]) ? config2[field] : {};
143238
+ const table2 = isRecord4(config2[field]) ? config2[field] : {};
142693
143239
  const requested = edit.rowId;
142694
143240
  const current = requested === void 0 ? {} : Object.hasOwn(table2, requested) ? table2[requested] : void 0;
142695
- if (!isRecord3(current)) return { ok: false, code: "unknown-row" };
143241
+ if (!isRecord4(current)) return { ok: false, code: "unknown-row" };
142696
143242
  const rowId = requested ?? nextRowId(table2);
142697
143243
  const nextRow = { ...current, ...edit.values };
142698
143244
  const clearing = clearSecrets.filter((key) => !Object.hasOwn(secrets, key));
@@ -142710,7 +143256,7 @@ var applyBotEdit = (config2, adapters, edit) => applyRowEdit(config2, { field: B
142710
143256
  var applyTargetEdit = (config2, edit) => applyRowEdit(config2, targetTable(), edit);
142711
143257
  var removeRow = (config2, field, rowId) => {
142712
143258
  const table2 = config2[field];
142713
- if (!isRecord3(table2) || !Object.hasOwn(table2, rowId)) return null;
143259
+ if (!isRecord4(table2) || !Object.hasOwn(table2, rowId)) return null;
142714
143260
  const nextTable = { ...table2 };
142715
143261
  delete nextTable[rowId];
142716
143262
  return { ...config2, [field]: nextTable };
@@ -142784,8 +143330,29 @@ var generalWriteOptions = (payload, next) => {
142784
143330
  }
142785
143331
  return void 0;
142786
143332
  };
142787
- var viewAfterWrite = async (ctx, adapters) => buildSettingsView(await ctx.settings.read(), adapters);
142788
- var registerSettingsCommands = (ctx, adapters, updateSettings) => {
143333
+ var registerSettingsCommands = (ctx, adapters, updateSettings, connections) => {
143334
+ const viewAfterWrite = async () => buildSettingsView(await ctx.settings.read(), adapters);
143335
+ ctx.stream({
143336
+ name: SETTINGS_CONNECTIONS_COMMAND,
143337
+ description: "Read bot connection states as an initial snapshot and subsequent changes.",
143338
+ scope: "read",
143339
+ payloadSchema: EMPTY_PAYLOAD,
143340
+ buffer: { latest: 1 },
143341
+ handle: async (_payload, emit, signal) => {
143342
+ if (signal.aborted) return;
143343
+ const unsubscribe = connections.subscribe((snapshot) => {
143344
+ if (!signal.aborted) emit(snapshot);
143345
+ });
143346
+ try {
143347
+ await new Promise((resolve) => {
143348
+ if (signal.aborted) resolve();
143349
+ else signal.addEventListener("abort", () => resolve(), { once: true });
143350
+ });
143351
+ } finally {
143352
+ unsubscribe();
143353
+ }
143354
+ }
143355
+ });
142789
143356
  ctx.command({
142790
143357
  name: SETTINGS_VIEW_COMMAND,
142791
143358
  description: "Read the chat-bridge settings view, bot columns, and this machine's proxies.",
@@ -142812,7 +143379,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142812
143379
  return { result, write: { config: next, options: { secrets, clearSecrets } } };
142813
143380
  });
142814
143381
  if (!edited.ok) return edited;
142815
- return { ok: true, rowId: edited.rowId, view: await viewAfterWrite(ctx, adapters) };
143382
+ return { ok: true, rowId: edited.rowId, view: await viewAfterWrite() };
142816
143383
  };
142817
143384
  const removeRow2 = async (remove) => {
142818
143385
  const removed = await updateSettings((config2) => {
@@ -142820,7 +143387,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142820
143387
  return next === null ? { result: false } : { result: true, write: { config: next } };
142821
143388
  });
142822
143389
  if (!removed) return { ok: false, code: "unknown-row" };
142823
- return { ok: true, view: await viewAfterWrite(ctx, adapters) };
143390
+ return { ok: true, view: await viewAfterWrite() };
142824
143391
  };
142825
143392
  ctx.command({
142826
143393
  name: SETTINGS_SAVE_BOT_COMMAND,
@@ -142871,7 +143438,7 @@ var registerSettingsCommands = (ctx, adapters, updateSettings) => {
142871
143438
  write: options === void 0 ? { config: next } : { config: next, options }
142872
143439
  };
142873
143440
  });
142874
- return { ok: true, view: await viewAfterWrite(ctx, adapters) };
143441
+ return { ok: true, view: await viewAfterWrite() };
142875
143442
  }
142876
143443
  });
142877
143444
  };
@@ -142899,12 +143466,13 @@ var makeWechatAdapter = (deps) => {
142899
143466
  columns: COLUMNS3,
142900
143467
  validate: wechatConfigIssues,
142901
143468
  isConfigured: (bot) => wechatConfigIssues(bot).length === 0,
142902
- connect: (bot, _handlers) => {
143469
+ connect: (bot, handlers) => {
142903
143470
  deps.log.warn("wechat iLink connect is stubbed pending security-review gate (YCO-236)", {
142904
143471
  bot: bot.displayName,
142905
143472
  hasToken: tokenOf2(bot) !== "",
142906
143473
  textLimit: WECHAT_TEXT_MAX_CHARS
142907
143474
  });
143475
+ handlers.onConnectionState?.({ state: "failed", reason: "not-available" });
142908
143476
  return { close: () => void 0 };
142909
143477
  },
142910
143478
  sendText: async (binding, _text) => {
@@ -142927,7 +143495,7 @@ var makeWechatAdapter = (deps) => {
142927
143495
  };
142928
143496
 
142929
143497
  // src/wecomUpload.ts
142930
- import * as NodeCrypto4 from "node:crypto";
143498
+ import * as NodeCrypto6 from "node:crypto";
142931
143499
  var WECOM_CHUNK_BYTES = 512 * 1024;
142932
143500
  var WECOM_MAX_CHUNKS = 100;
142933
143501
  var WECOM_MIN_UPLOAD_BYTES = 5;
@@ -142960,7 +143528,7 @@ var uploadWecomMedia = async (input) => {
142960
143528
  filename: clampName(input.filename),
142961
143529
  total_size: bytes.byteLength,
142962
143530
  total_chunks: totalChunks,
142963
- md5: NodeCrypto4.createHash("md5").update(bytes).digest("hex")
143531
+ md5: NodeCrypto6.createHash("md5").update(bytes).digest("hex")
142964
143532
  });
142965
143533
  const uploadId = init.upload_id;
142966
143534
  if (typeof uploadId !== "string" || uploadId === "") {
@@ -143047,7 +143615,7 @@ var wecomReplyReference = (quote) => {
143047
143615
  };
143048
143616
 
143049
143617
  // src/wecomApi.ts
143050
- var DEFAULT_WECOM_ENDPOINT = "wss://openws.work.weixin.qq.com/wssvr/";
143618
+ var DEFAULT_WECOM_ENDPOINT = "wss://openws.work.weixin.qq.com";
143051
143619
  var PING_INTERVAL_MS = 3e4;
143052
143620
  var MAX_MISSED_PONGS = 2;
143053
143621
  var SUBSCRIBE_TIMEOUT_MS = 1e4;
@@ -143184,9 +143752,10 @@ var runWecomClient = (input) => {
143184
143752
  const message = parseWecomMessage(frame, input.botId);
143185
143753
  if (message !== null) input.handlers.onMessage(message);
143186
143754
  };
143187
- const finish = () => {
143755
+ const finish = (error = new Error("WeCom connection closed"), reason = "disconnected") => {
143188
143756
  if (finished) return;
143189
143757
  finished = true;
143758
+ if (!closed) input.handlers.onConnectionState?.({ state: "failed", error, reason });
143190
143759
  clearInterval(heartbeat);
143191
143760
  openStreams.clear();
143192
143761
  void input.socket.close().catch(() => void 0);
@@ -143204,6 +143773,7 @@ var runWecomClient = (input) => {
143204
143773
  if (!closed) {
143205
143774
  input.handlers.onError(error instanceof Error ? error.message : String(error));
143206
143775
  }
143776
+ finish(error);
143207
143777
  }
143208
143778
  finish();
143209
143779
  };
@@ -143211,7 +143781,7 @@ var runWecomClient = (input) => {
143211
143781
  if (finished) return;
143212
143782
  if (missedPong >= MAX_MISSED_PONGS) {
143213
143783
  input.handlers.onError("socket hang up: missed pong");
143214
- finish();
143784
+ finish(new Error("socket hang up: missed pong"));
143215
143785
  return;
143216
143786
  }
143217
143787
  missedPong += 1;
@@ -143232,10 +143802,13 @@ var runWecomClient = (input) => {
143232
143802
  settleAck(subscribeId, error instanceof Error ? error : new Error(String(error)));
143233
143803
  });
143234
143804
  const ready = subscribed.then(
143235
- () => true,
143805
+ () => {
143806
+ if (!finished && !closed) input.handlers.onConnectionState?.({ state: "connected" });
143807
+ return !finished && !closed;
143808
+ },
143236
143809
  (error) => {
143237
143810
  if (!closed) input.handlers.onError(error instanceof Error ? error.message : String(error));
143238
- finish();
143811
+ finish(error, "subscription-failed");
143239
143812
  return false;
143240
143813
  }
143241
143814
  );
@@ -143370,7 +143943,7 @@ var runWecomClient = (input) => {
143370
143943
  };
143371
143944
 
143372
143945
  // src/wecomMedia.ts
143373
- import * as NodeCrypto5 from "node:crypto";
143946
+ import * as NodeCrypto7 from "node:crypto";
143374
143947
 
143375
143948
  // src/wecomPadding.ts
143376
143949
  var PADDING_BLOCK = 32;
@@ -143401,7 +143974,7 @@ var decryptWecomMedia = (cipher, aesKey) => {
143401
143974
  if (cipher.byteLength === 0 || cipher.byteLength % AES_BLOCK !== 0) {
143402
143975
  throw new WecomDecryptError("the ciphertext is not a whole number of blocks");
143403
143976
  }
143404
- const decipher = NodeCrypto5.createDecipheriv("aes-256-cbc", key, key.subarray(0, IV_BYTES));
143977
+ const decipher = NodeCrypto7.createDecipheriv("aes-256-cbc", key, key.subarray(0, IV_BYTES));
143405
143978
  decipher.setAutoPadding(false);
143406
143979
  const plain = Buffer.concat([decipher.update(cipher), decipher.final()]);
143407
143980
  const unpadded = stripPadding(plain);
@@ -143544,6 +144117,9 @@ ${kept}`;
143544
144117
  botSecret,
143545
144118
  handlers: {
143546
144119
  onError: handlers.onError,
144120
+ onConnectionState: (state) => {
144121
+ if (!stopped) handlers.onConnectionState?.(state);
144122
+ },
143547
144123
  onMessage: (message) => {
143548
144124
  if (message.chatType !== "single") return;
143549
144125
  client.rememberRequest(message.chatId, message.requestId);
@@ -143585,6 +144161,8 @@ ${kept}`;
143585
144161
  try {
143586
144162
  if (await runOnce()) backoff = BACKOFF_START_MS2;
143587
144163
  } catch (error) {
144164
+ if (stopped) return;
144165
+ handlers.onConnectionState?.({ state: "failed", error, reason: "disconnected" });
143588
144166
  handlers.onError(error instanceof Error ? error.message : String(error));
143589
144167
  }
143590
144168
  if (stopped) return;
@@ -143602,6 +144180,7 @@ ${kept}`;
143602
144180
  }
143603
144181
  };
143604
144182
  },
144183
+ // 不看 `options.retry`:长连接的回执没有核实过的限流码,也没有幂等键,失败一律说不清发没发出去。
143605
144184
  sendText: async (binding, text4) => {
143606
144185
  const client = clients.get(binding.rowId);
143607
144186
  if (client === void 0) {
@@ -143618,12 +144197,36 @@ ${kept}`;
143618
144197
  detail: "missing bot id or secret in saved config"
143619
144198
  };
143620
144199
  }
144200
+ let stage = "websocket";
143621
144201
  try {
143622
144202
  const socket = await deps.connectSocket(endpoint);
143623
- await socket.close();
143624
- return { stage: "websocket", ok: true, detail: `endpoint reachable (${endpoint})` };
144203
+ stage = "aibot_subscribe";
144204
+ let detail = "WeCom connection closed before subscribe ack";
144205
+ let closing;
144206
+ const client = runWecomClient({
144207
+ socket: {
144208
+ send: (data) => socket.send(data),
144209
+ messages: socket.messages,
144210
+ close: () => closing ??= socket.close()
144211
+ },
144212
+ botId,
144213
+ botSecret,
144214
+ handlers: {
144215
+ onMessage: () => void 0,
144216
+ onError: (error) => {
144217
+ detail = error;
144218
+ }
144219
+ }
144220
+ });
144221
+ try {
144222
+ const ok = await client.ready;
144223
+ return { stage, ok, detail: ok ? `subscription confirmed (${endpoint})` : detail };
144224
+ } finally {
144225
+ client.close();
144226
+ await closing;
144227
+ }
143625
144228
  } catch (error) {
143626
- return { stage: "websocket", ok: false, detail: describeError(error) };
144229
+ return { stage, ok: false, detail: describeError(error) };
143627
144230
  }
143628
144231
  },
143629
144232
  askApproval: async (binding, request) => {
@@ -143680,18 +144283,25 @@ ${kept}`;
143680
144283
  // src/wecomAppApi.ts
143681
144284
  var WECOM_APP_DEFAULT_API_BASE = "https://qyapi.weixin.qq.com";
143682
144285
  var WECOM_APP_TEXT_MAX_BYTES = 2048;
144286
+ var REQUEST_TIMEOUT_MS = 15e3;
144287
+ var MEDIA_TIMEOUT_MS = 6e4;
143683
144288
  var WecomAppApiError = class extends Error {
143684
144289
  code;
143685
144290
  providerCode;
143686
144291
  hint;
144292
+ invalidUsers;
143687
144293
  constructor(code2, message, extras) {
143688
144294
  super(message);
143689
144295
  this.name = "WecomAppApiError";
143690
144296
  this.code = code2;
143691
144297
  this.providerCode = extras?.providerCode;
143692
144298
  this.hint = extras?.hint;
144299
+ this.invalidUsers = extras?.invalidUsers ?? [];
143693
144300
  }
143694
144301
  };
144302
+ var TOKEN_INVALID_CODES = /* @__PURE__ */ new Set(["40014", "42001"]);
144303
+ var RATE_LIMITED_CODES = /* @__PURE__ */ new Set(["45009", "45033"]);
144304
+ var wecomAppSendVerdict = (error) => error instanceof WecomAppApiError && error.providerCode !== void 0 && (RATE_LIMITED_CODES.has(error.providerCode) || TOKEN_INVALID_CODES.has(error.providerCode)) ? { retry: true } : NO_RETRY;
143695
144305
  var cleanString2 = (value2) => {
143696
144306
  if (typeof value2 !== "string") return null;
143697
144307
  const trimmed = value2.trim();
@@ -143727,6 +144337,7 @@ var splitUtf8ByBytes = (text4, maxBytes = WECOM_APP_TEXT_MAX_BYTES) => {
143727
144337
  if (current.length > 0) chunks.push(current);
143728
144338
  return chunks;
143729
144339
  };
144340
+ var parseInvalidUsers = (value2) => typeof value2 === "string" ? value2.split("|").map((user) => user.trim()).filter(Boolean) : [];
143730
144341
  var WecomAppApi = class {
143731
144342
  corpId;
143732
144343
  corpSecret;
@@ -143778,9 +144389,13 @@ var WecomAppApi = class {
143778
144389
  async sendText(input) {
143779
144390
  const chunks = splitUtf8ByBytes(input.content);
143780
144391
  for (const chunk of chunks) {
143781
- await this.#send(
143782
- { touser: input.userId, msgtype: "text", text: { content: chunk } },
143783
- input.signal
144392
+ await sendWithRetry(
144393
+ () => this.#send(
144394
+ { touser: input.userId, msgtype: "text", text: { content: chunk } },
144395
+ input.signal
144396
+ ),
144397
+ wecomAppSendVerdict,
144398
+ input.retry
143784
144399
  );
143785
144400
  }
143786
144401
  }
@@ -143807,12 +144422,16 @@ var WecomAppApi = class {
143807
144422
  new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }),
143808
144423
  input.name
143809
144424
  );
143810
- const payload = await this.#requestJson(url, {
143811
- method: "POST",
143812
- // 不设 content-type:multipart 的 boundary 要让 fetch 自己写。
143813
- body: form,
143814
- ...input.signal === void 0 ? {} : { signal: input.signal }
143815
- });
144425
+ const payload = await this.#requestJson(
144426
+ url,
144427
+ {
144428
+ method: "POST",
144429
+ // 不设 content-type:multipart 的 boundary 要让 fetch 自己写。
144430
+ body: form,
144431
+ ...input.signal === void 0 ? {} : { signal: input.signal }
144432
+ },
144433
+ MEDIA_TIMEOUT_MS
144434
+ );
143816
144435
  const mediaId = payload["media_id"];
143817
144436
  if (typeof mediaId === "string" && mediaId !== "") return mediaId;
143818
144437
  throw this.#apiError(Number(payload["errcode"] ?? -1), payload["errmsg"]);
@@ -143826,10 +144445,17 @@ var WecomAppApi = class {
143826
144445
  const url = new URL("/cgi-bin/media/get", this.apiBaseUrl);
143827
144446
  url.searchParams.set("access_token", token);
143828
144447
  url.searchParams.set("media_id", input.mediaId);
143829
- const response = await this.#fetch(url, {
143830
- method: "GET",
143831
- ...input.signal === void 0 ? {} : { signal: input.signal }
143832
- });
144448
+ return this.#withRequest(
144449
+ url,
144450
+ {
144451
+ method: "GET",
144452
+ ...input.signal === void 0 ? {} : { signal: input.signal }
144453
+ },
144454
+ MEDIA_TIMEOUT_MS,
144455
+ (response) => this.#readMedia(response, input.maxBytes)
144456
+ );
144457
+ }
144458
+ async #readMedia(response, maxBytes) {
143833
144459
  const contentType = response.headers.get("content-type") ?? "";
143834
144460
  if (contentType.includes("application/json") || contentType.includes("text/plain")) {
143835
144461
  let payload = {};
@@ -143849,7 +144475,7 @@ var WecomAppApi = class {
143849
144475
  `WeCom media/get failed (HTTP ${String(response.status)}).`
143850
144476
  );
143851
144477
  }
143852
- const bytes = await readBodyLimited(response, input.maxBytes);
144478
+ const bytes = await readBodyLimited(response, maxBytes);
143853
144479
  const name = filenameFromContentDisposition(response.headers.get("content-disposition"));
143854
144480
  const mimeType = contentType.split(";")[0]?.trim();
143855
144481
  return {
@@ -143868,55 +144494,76 @@ var WecomAppApi = class {
143868
144494
  body: JSON.stringify({ ...message, agentid: Number.parseInt(this.agentId, 10) }),
143869
144495
  ...signal === void 0 ? {} : { signal }
143870
144496
  });
143871
- const errcode = Number(payload["errcode"] ?? 0);
143872
- if (errcode !== 0) throw this.#apiError(errcode, payload["errmsg"]);
143873
144497
  const invalid = payload["invaliduser"];
143874
144498
  if (typeof invalid === "string" && invalid.trim() !== "") {
143875
144499
  throw new WecomAppApiError("invalid-user", `WeCom did not deliver to: ${invalid.trim()}.`, {
143876
144500
  providerCode: "81013",
143877
- hint: "\u786E\u8BA4\u63A5\u6536\u4EBA\u4ECD\u5728\u8FD9\u4E2A\u81EA\u5EFA\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\u5185\u3002"
144501
+ hint: "wecomInvalidUser",
144502
+ invalidUsers: parseInvalidUsers(invalid)
143878
144503
  });
143879
144504
  }
143880
144505
  }
143881
- #apiError(errcode, errmsg) {
144506
+ #apiError(errcode, errmsg, invalidUsers) {
143882
144507
  const code2 = String(errcode);
143883
144508
  const message = typeof errmsg === "string" && errmsg !== "" ? `WeCom API failed (${code2}): ${errmsg}` : `WeCom API failed (${code2}).`;
143884
- if (errcode === 40014 || errcode === 42001) this.invalidateToken();
144509
+ if (TOKEN_INVALID_CODES.has(code2)) this.invalidateToken();
143885
144510
  if (errcode === 60020) {
143886
144511
  return new WecomAppApiError("trusted-ip", "WeCom rejected the outbound IP (errcode 60020).", {
143887
144512
  providerCode: code2,
143888
- hint: "\u628A\u51FA\u7AD9\u516C\u7F51 IP \u52A0\u8FDB\u4F01\u4E1A\u5FAE\u4FE1\u540E\u53F0\u7684\u300C\u4F01\u4E1A\u53EF\u4FE1 IP\u300D\u3002"
144513
+ hint: "wecomTrustedIp"
143889
144514
  });
143890
144515
  }
143891
144516
  if (errcode === 81013) {
143892
144517
  return new WecomAppApiError("invalid-user", message, {
143893
144518
  providerCode: code2,
143894
- hint: "\u6536\u4EF6\u4EBA\u3001\u90E8\u95E8\u6216\u6807\u7B7E\u4E0D\u5B58\u5728\uFF0C\u6216\u4E0D\u5728\u8FD9\u4E2A\u81EA\u5EFA\u5E94\u7528\u7684\u53EF\u89C1\u8303\u56F4\u5185\u3002"
144519
+ hint: "wecomInvalidUser",
144520
+ invalidUsers: parseInvalidUsers(invalidUsers)
143895
144521
  });
143896
144522
  }
143897
144523
  if (errcode === 40056) {
143898
144524
  return new WecomAppApiError("invalid-agent", message, {
143899
144525
  providerCode: code2,
143900
- hint: "AgentId \u4E0D\u5BF9\uFF0C\u6838\u5BF9\u81EA\u5EFA\u5E94\u7528\u7684 AgentId\u3002"
144526
+ hint: "wecomInvalidAgent"
143901
144527
  });
143902
144528
  }
143903
144529
  return new WecomAppApiError("api-failed", message, { providerCode: code2 });
143904
144530
  }
143905
- async #requestJson(url, init) {
143906
- const response = await this.#fetch(url, init);
143907
- let payload;
144531
+ async #withRequest(url, init, timeoutMs, read) {
144532
+ const deadline = new AbortController();
144533
+ const signal = AbortSignal.any([deadline.signal, ...init.signal ? [init.signal] : []]);
144534
+ const timeout = new WecomAppApiError(
144535
+ "timeout",
144536
+ `WeCom ${url.pathname} timed out after ${timeoutMs}ms.`
144537
+ );
144538
+ const timer = setTimeout(() => deadline.abort(timeout), timeoutMs);
143908
144539
  try {
143909
- payload = await response.json();
143910
- } catch {
143911
- throw new WecomAppApiError(
143912
- "bad-response",
143913
- `WeCom returned non-JSON (HTTP ${String(response.status)}).`
143914
- );
143915
- }
143916
- if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
143917
- throw new WecomAppApiError("bad-response", "WeCom returned a non-object JSON body.");
144540
+ return await read(await this.#fetch(url, { ...init, signal }));
144541
+ } catch (error) {
144542
+ if (signal.aborted) throw signal.reason;
144543
+ throw error;
144544
+ } finally {
144545
+ clearTimeout(timer);
143918
144546
  }
143919
- return payload;
144547
+ }
144548
+ async #requestJson(url, init, timeoutMs = REQUEST_TIMEOUT_MS) {
144549
+ return this.#withRequest(url, init, timeoutMs, async (response) => {
144550
+ let payload;
144551
+ try {
144552
+ payload = await response.json();
144553
+ } catch {
144554
+ throw new WecomAppApiError(
144555
+ "bad-response",
144556
+ `WeCom returned non-JSON (HTTP ${String(response.status)}).`
144557
+ );
144558
+ }
144559
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
144560
+ throw new WecomAppApiError("bad-response", "WeCom returned a non-object JSON body.");
144561
+ }
144562
+ const result = payload;
144563
+ const errcode = Number(result["errcode"] ?? 0);
144564
+ if (errcode !== 0) throw this.#apiError(errcode, result["errmsg"], result["invaliduser"]);
144565
+ return result;
144566
+ });
143920
144567
  }
143921
144568
  };
143922
144569
 
@@ -143925,7 +144572,7 @@ import * as NodeHttp2 from "node:http";
143925
144572
  import * as NodeNet from "node:net";
143926
144573
 
143927
144574
  // src/wecomAppCrypto.ts
143928
- import * as NodeCrypto6 from "node:crypto";
144575
+ import * as NodeCrypto8 from "node:crypto";
143929
144576
  var AES_KEY_BYTES = 32;
143930
144577
  var RANDOM_BYTES = 16;
143931
144578
  var WecomAppCryptoError = class extends Error {
@@ -143948,13 +144595,13 @@ var timingSafeEqualString2 = (a, b) => {
143948
144595
  const left = Buffer.from(a);
143949
144596
  const right = Buffer.from(b);
143950
144597
  if (left.length === 0 || left.length !== right.length) return false;
143951
- return NodeCrypto6.timingSafeEqual(left, right);
144598
+ return NodeCrypto8.timingSafeEqual(left, right);
143952
144599
  };
143953
- var computeSignature = (token, timestamp, nonce, encrypt) => NodeCrypto6.createHash("sha1").update([token, timestamp, nonce, encrypt].sort().join("")).digest("hex");
144600
+ var computeSignature = (token, timestamp, nonce, encrypt) => NodeCrypto8.createHash("sha1").update([token, timestamp, nonce, encrypt].sort().join("")).digest("hex");
143954
144601
  var verifySignature = (token, timestamp, nonce, encrypt, signature) => timingSafeEqualString2(computeSignature(token, timestamp, nonce, encrypt), signature);
143955
144602
  var decrypt = (encodingAESKey, encryptText, expectReceiveId) => {
143956
144603
  const { key, iv } = parseEncodingAESKey(encodingAESKey);
143957
- const decipher = NodeCrypto6.createDecipheriv("aes-256-cbc", key, iv);
144604
+ const decipher = NodeCrypto8.createDecipheriv("aes-256-cbc", key, iv);
143958
144605
  decipher.setAutoPadding(false);
143959
144606
  let deciphered;
143960
144607
  try {
@@ -144013,10 +144660,10 @@ var createWecomAppCrypto = (input) => {
144013
144660
  decrypt: (encryptText) => decrypt(encodingAESKey, encryptText, corpId).message
144014
144661
  };
144015
144662
  };
144016
- var deriveCallbackPathSecret = (token, encodingAESKey) => NodeCrypto6.createHash("sha256").update(`${token.trim()}:${encodingAESKey.trim()}`).digest("hex").slice(0, 24);
144663
+ var deriveCallbackPathSecret = (token, encodingAESKey) => NodeCrypto8.createHash("sha256").update(`${token.trim()}:${encodingAESKey.trim()}`).digest("hex").slice(0, 24);
144017
144664
 
144018
144665
  // src/wecomAppParse.ts
144019
- var isRecord4 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
144666
+ var isRecord5 = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
144020
144667
  var isXmlFormat = (raw) => {
144021
144668
  const trimmed = raw.trim();
144022
144669
  return trimmed.startsWith("<") && trimmed.endsWith(">");
@@ -144062,7 +144709,7 @@ var parseWecomAppPlainMessage = (raw) => {
144062
144709
  } catch {
144063
144710
  return null;
144064
144711
  }
144065
- if (!isRecord4(data)) return null;
144712
+ if (!isRecord5(data)) return null;
144066
144713
  const fromRaw = data["from"];
144067
144714
  const textRaw = data["text"];
144068
144715
  const imageRaw = data["image"];
@@ -144071,10 +144718,10 @@ var parseWecomAppPlainMessage = (raw) => {
144071
144718
  msgtype: String(data["msgtype"] ?? data["MsgType"] ?? "").toLowerCase(),
144072
144719
  createTime: data["createTime"] !== void 0 ? String(data["createTime"]) : data["CreateTime"] !== void 0 ? String(data["CreateTime"]) : void 0,
144073
144720
  agentId: data["agentid"] !== void 0 ? String(data["agentid"]) : data["AgentID"] !== void 0 ? String(data["AgentID"]) : void 0,
144074
- from: isRecord4(fromRaw) ? { userid: typeof fromRaw["userid"] === "string" ? fromRaw["userid"] : void 0 } : typeof data["FromUserName"] === "string" ? { userid: data["FromUserName"] } : void 0,
144721
+ from: isRecord5(fromRaw) ? { userid: typeof fromRaw["userid"] === "string" ? fromRaw["userid"] : void 0 } : typeof data["FromUserName"] === "string" ? { userid: data["FromUserName"] } : void 0,
144075
144722
  to: typeof data["to"] === "string" ? data["to"] : void 0,
144076
- text: isRecord4(textRaw) ? { content: typeof textRaw["content"] === "string" ? textRaw["content"] : void 0 } : void 0,
144077
- image: isRecord4(imageRaw) ? { url: typeof imageRaw["url"] === "string" ? imageRaw["url"] : void 0 } : void 0,
144723
+ text: isRecord5(textRaw) ? { content: typeof textRaw["content"] === "string" ? textRaw["content"] : void 0 } : void 0,
144724
+ image: isRecord5(imageRaw) ? { url: typeof imageRaw["url"] === "string" ? imageRaw["url"] : void 0 } : void 0,
144078
144725
  mediaId: typeof data["mediaid"] === "string" ? data["mediaid"] : void 0,
144079
144726
  recognition: typeof data["recognition"] === "string" ? data["recognition"] : void 0,
144080
144727
  event: String(data["event"] ?? data["eventtype"] ?? "").toLowerCase() || void 0,
@@ -144460,6 +145107,7 @@ var WECOM_APP_IMAGE_BYTES = 10 * 1024 * 1024;
144460
145107
  var WECOM_APP_FILE_BYTES = 20 * 1024 * 1024;
144461
145108
  var WECOM_APP_MIN_BYTES = 5;
144462
145109
  var WECOM_APP_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png"]);
145110
+ var CREDENTIAL_ERROR_CODES = /* @__PURE__ */ new Set(["40001", "40013", "40014", "40056", "42001", "60020"]);
144463
145111
  var joinCallbackUrl = (baseOrOrigin, path) => `${baseOrOrigin}${path}`;
144464
145112
  var wecomAppPublicFields = (bot) => {
144465
145113
  const c = wecomAppCredentials(bot);
@@ -144485,6 +145133,8 @@ var OUTBOUND_COLUMNS = /* @__PURE__ */ new Set([
144485
145133
  "wecomAppSecret",
144486
145134
  "wecomAppApiBaseUrl"
144487
145135
  ]);
145136
+ var PROBE_BACKOFF_START_MS = 1e3;
145137
+ var PROBE_BACKOFF_MAX_MS = 6e4;
144488
145138
  var makeWecomAppAdapter = (deps) => {
144489
145139
  const apis = /* @__PURE__ */ new Map();
144490
145140
  const newApi = (c) => new WecomAppApi({
@@ -144495,17 +145145,30 @@ var makeWecomAppAdapter = (deps) => {
144495
145145
  fetch: deps.fetch
144496
145146
  });
144497
145147
  const callbackHub = makeWecomAppCallbackHub();
144498
- const apiFor = (binding) => {
145148
+ const connectionFor = (binding) => {
144499
145149
  const api = apis.get(binding.rowId);
144500
145150
  if (api === void 0) {
144501
145151
  throw new ImNotConnectedError(`wecom-app bot '${binding.displayName}' is not connected.`);
144502
145152
  }
144503
145153
  return api;
144504
145154
  };
145155
+ const deliver = async (binding, send) => {
145156
+ const connection = connectionFor(binding);
145157
+ try {
145158
+ await send(connection.api);
145159
+ connection.report({ state: "connected" });
145160
+ } catch (error) {
145161
+ if (error instanceof WecomAppApiError && error.providerCode !== void 0 && CREDENTIAL_ERROR_CODES.has(error.providerCode)) {
145162
+ connection.report({ state: "failed", error, reason: "credentials-rejected" });
145163
+ }
145164
+ throw error;
145165
+ }
145166
+ };
144505
145167
  const sendMedia = async (binding, mediaType, file) => {
144506
- const api = apiFor(binding);
144507
- const mediaId = await api.uploadMedia({ mediaType, ...file });
144508
- await api.sendMedia({ userId: binding.chatId, mediaType, mediaId });
145168
+ await deliver(binding, async (api) => {
145169
+ const mediaId = await api.uploadMedia({ mediaType, ...file });
145170
+ await api.sendMedia({ userId: binding.chatId, mediaType, mediaId });
145171
+ });
144509
145172
  deps.log.info(`wecom-app delivered ${mediaType === "image" ? "an image" : "a file"}`, {
144510
145173
  name: file.name
144511
145174
  });
@@ -144556,6 +145219,19 @@ var makeWecomAppAdapter = (deps) => {
144556
145219
  (issue) => issue.code !== "required" && issue.column !== "wecomAppEncodingAESKey"
144557
145220
  ),
144558
145221
  publicFields: wecomAppPublicFields,
145222
+ // 收件人不在应用可见范围(invaliduser / 81013):说清是谁、为什么、怎么办。
145223
+ describeDeliveryRejection: (error, binding) => {
145224
+ if (!(error instanceof WecomAppApiError) || error.code !== "invalid-user") return void 0;
145225
+ const recipients = [
145226
+ ...new Set(error.invalidUsers.length > 0 ? error.invalidUsers : [binding.chatId])
145227
+ ].sort();
145228
+ return {
145229
+ recipients,
145230
+ summary: deps.translate("reply.wecomDeliveryFailed", { recipient: recipients.join(", ") }),
145231
+ hint: error.hint,
145232
+ providerCode: error.providerCode
145233
+ };
145234
+ },
144559
145235
  /**
144560
145236
  * 自建应用是回调式渠道:入站由企微推到我们的 listener,没有可探的长连接;能探的是出站
144561
145237
  * 凭据——和真实发送同一条路,向 gettoken 换一次 access_token。
@@ -144586,7 +145262,12 @@ var makeWecomAppAdapter = (deps) => {
144586
145262
  detail: "access_token ok (callback reachability is not probed)"
144587
145263
  };
144588
145264
  } catch (error) {
144589
- return { stage: "gettoken", ok: false, detail: describeError(error) };
145265
+ return {
145266
+ stage: "gettoken",
145267
+ ok: false,
145268
+ detail: describeError(error),
145269
+ ...error instanceof WecomAppApiError && error.hint !== void 0 ? { hint: error.hint } : {}
145270
+ };
144590
145271
  }
144591
145272
  },
144592
145273
  connect: async (bot, handlers) => {
@@ -144600,7 +145281,14 @@ var makeWecomAppAdapter = (deps) => {
144600
145281
  const lease = await callbackHub.acquire({ host: config2.listenHost, port: config2.listenPort });
144601
145282
  const server = lease.server;
144602
145283
  const api = newApi(c);
144603
- apis.set(bot.rowId, api);
145284
+ let closed = false;
145285
+ const report = (state) => {
145286
+ if (closed) return;
145287
+ handlers.onConnectionState?.(
145288
+ state.state === "failed" && state.error instanceof WecomAppApiError && state.error.hint !== void 0 ? { ...state, hint: state.error.hint } : state
145289
+ );
145290
+ };
145291
+ apis.set(bot.rowId, { api, report });
144604
145292
  let path;
144605
145293
  try {
144606
145294
  path = server.register({
@@ -144616,7 +145304,7 @@ var makeWecomAppAdapter = (deps) => {
144616
145304
  await lease.release();
144617
145305
  throw error;
144618
145306
  }
144619
- deps.log.info("wecom-app connected", {
145307
+ deps.log.info("wecom-app callback listening", {
144620
145308
  bot: bot.displayName,
144621
145309
  callbackPath: path,
144622
145310
  callbackUrl: joinCallbackUrl(
@@ -144625,8 +145313,26 @@ var makeWecomAppAdapter = (deps) => {
144625
145313
  ),
144626
145314
  listen: `${server.host}:${String(server.port)}`
144627
145315
  });
145316
+ const probe2 = new AbortController();
145317
+ void (async () => {
145318
+ let backoff = PROBE_BACKOFF_START_MS;
145319
+ while (!probe2.signal.aborted) {
145320
+ try {
145321
+ await api.getAccessToken(probe2.signal);
145322
+ report({ state: "connected" });
145323
+ return;
145324
+ } catch (error) {
145325
+ if (probe2.signal.aborted) return;
145326
+ report({ state: "failed", error, reason: "token-unavailable" });
145327
+ await (deps.sleep ?? abortableSleep)(backoff, probe2.signal).catch(() => void 0);
145328
+ backoff = Math.min(backoff * 2, PROBE_BACKOFF_MAX_MS);
145329
+ }
145330
+ }
145331
+ })();
144628
145332
  return {
144629
145333
  close: () => {
145334
+ closed = true;
145335
+ probe2.abort();
144630
145336
  server.unregister(bot.rowId);
144631
145337
  apis.delete(bot.rowId);
144632
145338
  void lease.release().catch((error) => {
@@ -144642,15 +145348,18 @@ var makeWecomAppAdapter = (deps) => {
144642
145348
  if (options?.targetKind === "group") {
144643
145349
  throw new Error("wecom-app can only deliver to a user; group targets are not supported.");
144644
145350
  }
144645
- await apiFor(binding).sendText({ userId: binding.chatId, content: text4 });
145351
+ await deliver(
145352
+ binding,
145353
+ (api) => api.sendText({ userId: binding.chatId, content: text4, retry: options?.retry })
145354
+ );
144646
145355
  },
144647
145356
  // D376:企业微信自建应用没有表情回执也没有流式,开工时先回一句文字,结论照常另发。没有要撤的
144648
145357
  // 东西,回 null;发不出去由核心记一条 warn,不拦这一轮。
144649
145358
  acknowledge: async (binding) => {
144650
- await apiFor(binding).sendText({
144651
- userId: binding.chatId,
144652
- content: deps.translate("reply.received")
144653
- });
145359
+ await deliver(
145360
+ binding,
145361
+ (api) => api.sendText({ userId: binding.chatId, content: deps.translate("reply.received") })
145362
+ );
144654
145363
  return null;
144655
145364
  },
144656
145365
  sendImage: (binding, image2) => sendMedia(binding, "image", image2),
@@ -144661,11 +145370,16 @@ var makeWecomAppAdapter = (deps) => {
144661
145370
  minBytes: WECOM_APP_MIN_BYTES
144662
145371
  },
144663
145372
  imageMimeTypes: WECOM_APP_IMAGE_TYPES,
144664
- fetchResource: (binding, input) => apiFor(binding).downloadMedia({ mediaId: input.resource.key, maxBytes: input.maxBytes }),
145373
+ fetchResource: (binding, input) => connectionFor(binding).api.downloadMedia({
145374
+ mediaId: input.resource.key,
145375
+ maxBytes: input.maxBytes
145376
+ }),
144665
145377
  askApproval: async (binding, request) => {
144666
- const api = apis.get(binding.rowId);
144667
- if (api === void 0) return null;
144668
- await api.sendText({ userId: binding.chatId, content: numberedApprovalText(request) });
145378
+ if (!apis.has(binding.rowId)) return null;
145379
+ await deliver(
145380
+ binding,
145381
+ (api) => api.sendText({ userId: binding.chatId, content: numberedApprovalText(request) })
145382
+ );
144669
145383
  return null;
144670
145384
  },
144671
145385
  answerByText: (_binding, text4, options) => answerByNumber(text4, options)
@@ -144673,10 +145387,16 @@ var makeWecomAppAdapter = (deps) => {
144673
145387
  };
144674
145388
 
144675
145389
  // src/index.ts
144676
- var imPlugin = definePlugin({
145390
+ var definition = {
144677
145391
  id: "im",
144678
- apiVersion: 17,
144679
- activate: async (ctx) => {
145392
+ apiVersion: 19,
145393
+ activate: async (ctx, inboundIo) => {
145394
+ const inboundState = makeInboundStore({
145395
+ dataDir: ctx.paths.dataDir,
145396
+ log: ctx.log,
145397
+ io: inboundIo
145398
+ });
145399
+ ctx.onDispose(inboundState.flush);
144680
145400
  const { t, setLanguage } = makeLiveTranslate(
144681
145401
  parseReplyLanguage(ctx.config[REPLY_LANGUAGE_FIELD])
144682
145402
  );
@@ -144698,6 +145418,8 @@ var imPlugin = definePlugin({
144698
145418
  // Telegram 在不少网络里要绕代理,而绕不绕、绕哪个是按行配的 —— 这是插件面上
144699
145419
  // `ctx.proxies` 的第一个真实消费者。
144700
145420
  fetchVia: (proxy) => ctx.proxies.fetch(proxy),
145421
+ readCursor: inboundState.readCursor,
145422
+ saveCursor: inboundState.saveCursor,
144701
145423
  translate: t,
144702
145424
  log: {
144703
145425
  info: (message, fields) => ctx.log.info(message, fields),
@@ -144736,7 +145458,6 @@ var imPlugin = definePlugin({
144736
145458
  const updateSettings = makeSettingsUpdater(ctx.settings);
144737
145459
  registerLarkSetup(ctx, { updateSettings });
144738
145460
  registerSettingsRowActions(ctx, adapters);
144739
- registerSettingsCommands(ctx, adapters, updateSettings);
144740
145461
  const core = makeImCore({
144741
145462
  host: {
144742
145463
  log: ctx.log,
@@ -144755,7 +145476,8 @@ var imPlugin = definePlugin({
144755
145476
  config: () => ctx.config,
144756
145477
  derivedState: ctx.derivedState,
144757
145478
  t,
144758
- adapters
145479
+ adapters,
145480
+ inboundReady: inboundState.ready
144759
145481
  });
144760
145482
  const acknowledgements = makeAcknowledgements(core);
144761
145483
  const failures = makeFailureNotices(core);
@@ -144771,7 +145493,7 @@ var imPlugin = definePlugin({
144771
145493
  );
144772
145494
  const teams = makeTeamGoals(core);
144773
145495
  const notifications = makeNotifications(core, {
144774
- clearWorking: acknowledgements.clearWorking,
145496
+ takeWorking: acknowledgements.takeWorking,
144775
145497
  replyCards,
144776
145498
  failures,
144777
145499
  conclusions,
@@ -144806,6 +145528,7 @@ var imPlugin = definePlugin({
144806
145528
  });
144807
145529
  const chatSetup = makeChatSetup(core, {
144808
145530
  rows: projectRows,
145531
+ resumeMessage: (bot, message) => inbound.resumeMessage(bot, message),
144809
145532
  openPicker: (chatKey, rows, reply) => panelWiring.panel.openProjects(chatKey, rows, reply)
144810
145533
  });
144811
145534
  const commands = makeCommands(core, {
@@ -144818,10 +145541,10 @@ var imPlugin = definePlugin({
144818
145541
  openPanel: (chatKey, reply) => panelWiring.panel.open(chatKey, reply)
144819
145542
  });
144820
145543
  const inbound = makeInbound(core, {
145544
+ inboundState,
144821
145545
  enqueueForChat: chatQueue.enqueueForChat,
144822
145546
  commands,
144823
145547
  chatSetup,
144824
- rows: projectRows,
144825
145548
  interactions,
144826
145549
  approvals,
144827
145550
  panelWiring,
@@ -144847,6 +145570,7 @@ var imPlugin = definePlugin({
144847
145570
  ]
144848
145571
  });
144849
145572
  const connections = startConnections(core, inbound.inboundHandlersFor);
145573
+ registerSettingsCommands(ctx, adapters, updateSettings, connections);
144850
145574
  const { bots, connecting } = connections.reconcile();
144851
145575
  const deliveryHttp = makeDeliveryHttp({
144852
145576
  // 每次请求按此刻的设置找目标:投递目标表改了不用重开监听。
@@ -144864,8 +145588,11 @@ var imPlugin = definePlugin({
144864
145588
  }
144865
145589
  ctx.log.info("chat plugin active", { bots, connecting });
144866
145590
  }
144867
- });
145591
+ };
145592
+ var activateImPlugin = definition.activate;
145593
+ var imPlugin = definePlugin(definition);
144868
145594
  var index_default = imPlugin;
144869
145595
  export {
145596
+ activateImPlugin,
144870
145597
  index_default as default
144871
145598
  };