@ycodium-ai/plugin-im 0.2.2596 → 0.2.2658

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23360,7 +23360,7 @@ var require_websocket = __commonJS({
23360
23360
  var http = __require("http");
23361
23361
  var net = __require("net");
23362
23362
  var tls = __require("tls");
23363
- var { randomBytes, createHash } = __require("crypto");
23363
+ var { randomBytes, createHash: createHash2 } = __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 = createHash("sha1").update(key + GUID).digest("base64");
24031
+ const digest = createHash2("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 } = __require("crypto");
24400
+ var { createHash: createHash2 } = __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 = createHash("sha1").update(key + GUID).digest("base64");
24707
+ const digest = createHash2("sha1").update(key + GUID).digest("base64");
24708
24708
  const headers = [
24709
24709
  "HTTP/1.1 101 Switching Protocols",
24710
24710
  "Upgrade: websocket",
@@ -25198,9 +25198,9 @@ var require_float = __commonJS({
25198
25198
  var require_utf8 = __commonJS({
25199
25199
  "../../node_modules/.pnpm/@protobufjs+utf8@1.1.2/node_modules/@protobufjs/utf8/index.js"(exports) {
25200
25200
  "use strict";
25201
- var utf8 = exports;
25201
+ var utf83 = exports;
25202
25202
  var replacementCharCode = 65533;
25203
- utf8.length = function utf8_length(string) {
25203
+ utf83.length = function utf8_length(string) {
25204
25204
  var len = 0, c = 0;
25205
25205
  for (var i = 0; i < string.length; ++i) {
25206
25206
  c = string.charCodeAt(i);
@@ -25216,7 +25216,7 @@ var require_utf8 = __commonJS({
25216
25216
  }
25217
25217
  return len;
25218
25218
  };
25219
- utf8.read = function utf8_read(buffer, start, end) {
25219
+ utf83.read = function utf8_read(buffer, start, end) {
25220
25220
  if (end - start < 1)
25221
25221
  return "";
25222
25222
  var parts = null, chunk = [], i = 0, t, t2, c2, c3;
@@ -25252,7 +25252,7 @@ var require_utf8 = __commonJS({
25252
25252
  }
25253
25253
  return String.fromCharCode.apply(String, chunk.slice(0, i));
25254
25254
  };
25255
- utf8.write = function utf8_write(string, buffer, offset) {
25255
+ utf83.write = function utf8_write(string, buffer, offset) {
25256
25256
  var start = offset, c12, c2;
25257
25257
  for (var i = 0; i < string.length; ++i) {
25258
25258
  c12 = string.charCodeAt(i);
@@ -26595,7 +26595,7 @@ var require_writer = __commonJS({
26595
26595
  var BufferWriter;
26596
26596
  var LongBits = util.LongBits;
26597
26597
  var base64 = util.base64;
26598
- var utf8 = util.utf8;
26598
+ var utf83 = util.utf8;
26599
26599
  function Op(fn, len, val) {
26600
26600
  this.fn = fn;
26601
26601
  this.len = len;
@@ -26730,8 +26730,8 @@ var require_writer = __commonJS({
26730
26730
  return this.uint32(len)._push(writeBytes, len, value);
26731
26731
  };
26732
26732
  Writer.prototype.string = function write_string(value) {
26733
- var len = utf8.length(value);
26734
- return len ? this.uint32(len)._push(utf8.write, len, value) : this._push(writeByte, 1, 0);
26733
+ var len = utf83.length(value);
26734
+ return len ? this.uint32(len)._push(utf83.write, len, value) : this._push(writeByte, 1, 0);
26735
26735
  };
26736
26736
  Writer.prototype.fork = function fork() {
26737
26737
  this.states = new State(this);
@@ -26836,7 +26836,7 @@ var require_reader = __commonJS({
26836
26836
  var util = require_minimal();
26837
26837
  var BufferReader;
26838
26838
  var LongBits = util.LongBits;
26839
- var utf8 = util.utf8;
26839
+ var utf83 = util.utf8;
26840
26840
  function indexOutOfRange(reader, writeLength) {
26841
26841
  return RangeError("index out of range: " + reader.pos + " + " + (writeLength || 1) + " > " + reader.len);
26842
26842
  }
@@ -27001,7 +27001,7 @@ var require_reader = __commonJS({
27001
27001
  };
27002
27002
  Reader.prototype.string = function read_string() {
27003
27003
  var bytes = this.bytes();
27004
- return utf8.read(bytes, 0, bytes.length);
27004
+ return utf83.read(bytes, 0, bytes.length);
27005
27005
  };
27006
27006
  Reader.prototype.skip = function skip(length) {
27007
27007
  if (typeof length === "number") {
@@ -123921,13 +123921,13 @@ ${block2}
123921
123921
  throw err;
123922
123922
  }
123923
123923
  const delay = base * Math.pow(3, attempt - 1);
123924
- yield sleep(delay);
123924
+ yield sleep2(delay);
123925
123925
  }
123926
123926
  }
123927
123927
  throw lastErr;
123928
123928
  });
123929
123929
  }
123930
- function sleep(ms) {
123930
+ function sleep2(ms) {
123931
123931
  return new Promise((r) => setTimeout(r, ms));
123932
123932
  }
123933
123933
  var DEFAULT_CHUNK_LIMIT = 3500;
@@ -126163,9 +126163,155 @@ import {
126163
126163
  readRequestResolved,
126164
126164
  readTurnProgress,
126165
126165
  REQUEST_RESOLVED_TRIGGER_KIND,
126166
- TURN_PROGRESS_TRIGGER_KIND
126166
+ TURN_PROGRESS_TRIGGER_KIND,
126167
+ USER_INPUT_PENDING_TRIGGER_KIND
126167
126168
  } from "@ycodium-ai/plugin-api";
126168
126169
 
126170
+ // src/attachments.ts
126171
+ var IMAGE_LIMIT_BYTES = 10 * 1024 * 1024;
126172
+ var FILE_LIMIT_BYTES = 25 * 1024 * 1024;
126173
+ var MAX_ATTACHMENTS_PER_TURN = 20;
126174
+ var INLINE_IMAGE_MIMES = /* @__PURE__ */ new Set([
126175
+ "image/gif",
126176
+ "image/jpeg",
126177
+ "image/png",
126178
+ "image/webp"
126179
+ ]);
126180
+ var isInlineImageMime = (mimeType) => INLINE_IMAGE_MIMES.has(mimeType.toLowerCase());
126181
+ var limitForMime = (mimeType) => isInlineImageMime(mimeType) ? IMAGE_LIMIT_BYTES : FILE_LIMIT_BYTES;
126182
+ var limitForKind = (kind) => kind === "image" ? IMAGE_LIMIT_BYTES : FILE_LIMIT_BYTES;
126183
+ var formatMegabytes = (bytes) => {
126184
+ const megabytes = bytes / (1024 * 1024);
126185
+ return `${Number.isInteger(megabytes) ? String(megabytes) : megabytes.toFixed(1)} MB`;
126186
+ };
126187
+ var ImResourceTooLargeError = class extends Error {
126188
+ limitBytes;
126189
+ constructor(limitBytes) {
126190
+ super(`the resource is over the ${String(limitBytes)} byte limit`);
126191
+ this.name = "ImResourceTooLargeError";
126192
+ this.limitBytes = limitBytes;
126193
+ }
126194
+ };
126195
+ var readBodyLimited = async (response, maxBytes) => {
126196
+ const declared = Number(response.headers.get("content-length"));
126197
+ if (Number.isFinite(declared) && declared > maxBytes) {
126198
+ await response.body?.cancel().catch(() => void 0);
126199
+ throw new ImResourceTooLargeError(maxBytes);
126200
+ }
126201
+ const reader = response.body?.getReader();
126202
+ if (reader === void 0) return new Uint8Array(0);
126203
+ const chunks = [];
126204
+ let total = 0;
126205
+ for (; ; ) {
126206
+ const { done, value } = await reader.read();
126207
+ if (done) break;
126208
+ total += value.byteLength;
126209
+ if (total > maxBytes) {
126210
+ await reader.cancel().catch(() => void 0);
126211
+ throw new ImResourceTooLargeError(maxBytes);
126212
+ }
126213
+ chunks.push(value);
126214
+ }
126215
+ const bytes = new Uint8Array(total);
126216
+ let offset = 0;
126217
+ for (const chunk of chunks) {
126218
+ bytes.set(chunk, offset);
126219
+ offset += chunk.byteLength;
126220
+ }
126221
+ return bytes;
126222
+ };
126223
+ var startsWith = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
126224
+ var ascii = (bytes, from, to) => String.fromCharCode(...bytes.subarray(from, to));
126225
+ var sniffMime = (bytes) => {
126226
+ if (startsWith(bytes, [137, 80, 78, 71, 13, 10, 26, 10])) return "image/png";
126227
+ if (startsWith(bytes, [255, 216, 255])) return "image/jpeg";
126228
+ const head = ascii(bytes, 0, 6);
126229
+ if (head === "GIF87a" || head === "GIF89a") return "image/gif";
126230
+ if (startsWith(bytes, [37, 80, 68, 70])) return "application/pdf";
126231
+ if (startsWith(bytes, [80, 75, 3, 4]) || startsWith(bytes, [80, 75, 5, 6])) {
126232
+ return "application/zip";
126233
+ }
126234
+ if (ascii(bytes, 0, 4) === "RIFF") {
126235
+ const form = ascii(bytes, 8, 12);
126236
+ if (form === "WEBP") return "image/webp";
126237
+ if (form === "AVI ") return "video/x-msvideo";
126238
+ if (form === "WAVE") return "audio/wav";
126239
+ }
126240
+ if (ascii(bytes, 4, 8) === "ftyp") {
126241
+ const brand = ascii(bytes, 8, 12);
126242
+ if (brand === "qt ") return "video/quicktime";
126243
+ if (brand === "M4A ") return "audio/mp4";
126244
+ return "video/mp4";
126245
+ }
126246
+ if (startsWith(bytes, [26, 69, 223, 163])) return "video/webm";
126247
+ if (ascii(bytes, 0, 4) === "OggS") return "audio/ogg";
126248
+ if (ascii(bytes, 0, 3) === "ID3") return "audio/mpeg";
126249
+ return void 0;
126250
+ };
126251
+ var resolveMime = (claimed, bytes) => {
126252
+ const sniffed = sniffMime(bytes);
126253
+ const generic = claimed === "" || claimed === "application/octet-stream";
126254
+ if (sniffed === void 0) {
126255
+ return isInlineImageMime(claimed) ? "application/octet-stream" : claimed || "application/octet-stream";
126256
+ }
126257
+ if (isInlineImageMime(sniffed) || generic || isInlineImageMime(claimed)) return sniffed;
126258
+ return claimed;
126259
+ };
126260
+ var IMAGE_EXTENSIONS = {
126261
+ "image/png": ["png"],
126262
+ "image/jpeg": ["jpg", "jpeg"],
126263
+ "image/gif": ["gif"],
126264
+ "image/webp": ["webp"]
126265
+ };
126266
+ var withImageExtension = (name, mimeType) => {
126267
+ const extensions = IMAGE_EXTENSIONS[mimeType.toLowerCase()];
126268
+ if (extensions === void 0) return name;
126269
+ const dot = name.lastIndexOf(".");
126270
+ const current = dot > 0 ? name.slice(dot + 1).toLowerCase() : "";
126271
+ if (extensions.includes(current)) return name;
126272
+ const stem = dot > 0 ? name.slice(0, dot) : name;
126273
+ return `${stem}.${extensions[0] ?? "bin"}`;
126274
+ };
126275
+ var NAME_MAX_BYTES = 200;
126276
+ var utf8 = new TextEncoder();
126277
+ var sanitizeAttachmentName = (name, fallback) => {
126278
+ const cleaned = name.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}/\\<>:"|?*]/gu, "_").replace(/_+/g, "_").trim();
126279
+ if (cleaned === "" || /^\.+$/.test(cleaned) || cleaned === "_") return fallback;
126280
+ if (utf8.encode(cleaned).byteLength <= NAME_MAX_BYTES) return cleaned;
126281
+ const dot = cleaned.lastIndexOf(".");
126282
+ const extension = dot > 0 && cleaned.length - dot <= 17 ? cleaned.slice(dot) : "";
126283
+ const stem = [...extension === "" ? cleaned : cleaned.slice(0, dot)];
126284
+ while (stem.length > 1 && utf8.encode(stem.join("") + extension).byteLength > NAME_MAX_BYTES) {
126285
+ stem.pop();
126286
+ }
126287
+ return stem.join("") + extension;
126288
+ };
126289
+ var mimeFromName = (name) => {
126290
+ const ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
126291
+ const known = {
126292
+ png: "image/png",
126293
+ jpg: "image/jpeg",
126294
+ jpeg: "image/jpeg",
126295
+ gif: "image/gif",
126296
+ webp: "image/webp",
126297
+ pdf: "application/pdf",
126298
+ txt: "text/plain",
126299
+ md: "text/markdown",
126300
+ json: "application/json",
126301
+ csv: "text/csv",
126302
+ zip: "application/zip",
126303
+ mp4: "video/mp4",
126304
+ mov: "video/quicktime",
126305
+ webm: "video/webm",
126306
+ mp3: "audio/mpeg",
126307
+ wav: "audio/wav",
126308
+ ogg: "audio/ogg",
126309
+ opus: "audio/ogg",
126310
+ m4a: "audio/mp4"
126311
+ };
126312
+ return known[ext] ?? "application/octet-stream";
126313
+ };
126314
+
126169
126315
  // src/chatCommand.ts
126170
126316
  var teamArgument = (text2, chinese) => {
126171
126317
  const match = text2.match(/^\/team(?:\s+([\s\S]*))?$/i);
@@ -126205,6 +126351,8 @@ var isThreadCommand = (text2, chinese) => /^\/thread$/i.test(text2.trim()) || ch
126205
126351
  var ZH_ALIASES = {
126206
126352
  \u505C: "stop",
126207
126353
  \u505C\u6B62: "stop",
126354
+ // 不用单独的「继续」:那是人对 agent 说的话,不该被当成命令吞掉。
126355
+ \u7EE7\u7EED\u53D1\u9001: "continue",
126208
126356
  \u72B6\u6001: "status",
126209
126357
  \u65B0\u5BF9\u8BDD: "new",
126210
126358
  \u5E2E\u52A9: "help",
@@ -126223,6 +126371,7 @@ var resolveTeam = (query, teams) => {
126223
126371
  var parseChatCommand = (input) => {
126224
126372
  const trimmed = input.text.trim();
126225
126373
  if (/^\/stop$/i.test(trimmed)) return { kind: "stop" };
126374
+ if (/^\/continue$/i.test(trimmed)) return { kind: "continue" };
126226
126375
  if (/^\/status$/i.test(trimmed)) return { kind: "status" };
126227
126376
  if (/^\/new$/i.test(trimmed)) return { kind: "new" };
126228
126377
  if (/^\/help$/i.test(trimmed)) return { kind: "help" };
@@ -126254,6 +126403,7 @@ var parseChatCommand = (input) => {
126254
126403
  var helpText = (t, options = {}) => [
126255
126404
  t("help.title"),
126256
126405
  t("help.stop"),
126406
+ t("help.continue"),
126257
126407
  t("help.status"),
126258
126408
  t("help.new"),
126259
126409
  t("help.sessions"),
@@ -126278,9 +126428,20 @@ var IM_STATE_KEY = "chats";
126278
126428
  var IM_STATE_VERSION = 1;
126279
126429
  var OPENED_THREAD_LIMIT = 200;
126280
126430
  var APPROVAL_RECORD_LIMIT = 100;
126281
- var emptyState = () => ({ chats: {}, opened: {}, approvals: {}, turnCards: {} });
126431
+ var emptyState = () => ({
126432
+ chats: {},
126433
+ opened: {},
126434
+ approvals: {},
126435
+ turnCards: {},
126436
+ questions: {},
126437
+ queue: {}
126438
+ });
126439
+ var questionsOf = (state) => state.questions ?? {};
126440
+ var queueOf = (state) => state.queue ?? {};
126282
126441
  var openTurnCardsOf = (state) => state.turnCards ?? {};
126283
126442
  var TURN_CARD_RECORD_LIMIT = 50;
126443
+ var QUESTION_RECORD_LIMIT = 50;
126444
+ var QUEUE_RECORD_LIMIT = 50;
126284
126445
  var DEFAULT_CHAT_RECORD = {
126285
126446
  threadId: null,
126286
126447
  target: { kind: "thread" },
@@ -126301,6 +126462,31 @@ var findApprovalByToken = (state, token) => {
126301
126462
  var openApprovalOf = (state, chatKey) => Object.values(state.approvals).findLast(
126302
126463
  (approval) => approval.chatKey === chatKey && !approval.resolved
126303
126464
  );
126465
+ var findQuestionByToken = (state, token) => {
126466
+ for (const record of Object.values(questionsOf(state))) {
126467
+ for (const item of record.items) {
126468
+ const option = item.options.find((candidate) => candidate.token === token);
126469
+ if (option !== void 0) return { record, item, click: { kind: "option", option } };
126470
+ if (item.submitToken === token) return { record, item, click: { kind: "submit" } };
126471
+ }
126472
+ }
126473
+ return void 0;
126474
+ };
126475
+ var openQuestionOf = (state, chatKey, threadId) => Object.values(questionsOf(state)).findLast(
126476
+ (record) => record.chatKey === chatKey && record.threadId === threadId && !record.resolved
126477
+ );
126478
+ var isUnanswered = (item) => item.answer === void 0;
126479
+ var findQueueByToken = (state, token) => {
126480
+ for (const record of Object.values(queueOf(state))) {
126481
+ if (record.steerToken === token) return { record, op: "steer" };
126482
+ if (record.withdrawToken === token) return { record, op: "withdraw" };
126483
+ if (record.resumeToken === token) return { record, op: "resume" };
126484
+ }
126485
+ return void 0;
126486
+ };
126487
+ var openQueueOfThread = (state, threadId) => Object.values(queueOf(state)).filter(
126488
+ (record) => record.threadId === threadId && record.settled === void 0
126489
+ );
126304
126490
  var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
126305
126491
  var nonEmpty = (value) => typeof value === "string" && value !== "" ? value : null;
126306
126492
  var readTarget = (value) => {
@@ -126322,6 +126508,76 @@ var readButtons = (value) => {
126322
126508
  }
126323
126509
  return buttons.length === 0 ? null : buttons;
126324
126510
  };
126511
+ var readQuestionOptions = (value) => {
126512
+ if (!Array.isArray(value)) return null;
126513
+ const options = [];
126514
+ for (const entry of value) {
126515
+ if (!isRecord(entry)) return null;
126516
+ const token = nonEmpty(entry["token"]);
126517
+ const optionValue = nonEmpty(entry["value"]);
126518
+ if (token === null || optionValue === null || typeof entry["label"] !== "string") return null;
126519
+ const planAction = entry["planAction"];
126520
+ options.push({
126521
+ token,
126522
+ value: optionValue,
126523
+ label: entry["label"],
126524
+ description: typeof entry["description"] === "string" ? entry["description"] : "",
126525
+ ...planAction === "approve" || planAction === "revise" || planAction === "stop" ? { planAction } : {}
126526
+ });
126527
+ }
126528
+ return options.length === 0 ? null : options;
126529
+ };
126530
+ var readQuestionItems = (value) => {
126531
+ if (!Array.isArray(value)) return null;
126532
+ const items = [];
126533
+ for (const entry of value) {
126534
+ if (!isRecord(entry)) return null;
126535
+ const id = nonEmpty(entry["id"]);
126536
+ const options = readQuestionOptions(entry["options"]);
126537
+ if (id === null || options === null || typeof entry["question"] !== "string") return null;
126538
+ const submitToken = nonEmpty(entry["submitToken"]);
126539
+ items.push({
126540
+ id,
126541
+ header: typeof entry["header"] === "string" ? entry["header"] : "",
126542
+ question: entry["question"],
126543
+ kind: entry["kind"] === "plan" ? "plan" : "question",
126544
+ multiSelect: entry["multiSelect"] === true,
126545
+ // 老事实没写这个字段时按「允许」处理:与宿主「没明说算 true」一致。
126546
+ allowFreeform: entry["allowFreeform"] !== false,
126547
+ options,
126548
+ ...submitToken === null ? {} : { submitToken },
126549
+ selected: []
126550
+ });
126551
+ }
126552
+ return items.length === 0 ? null : items;
126553
+ };
126554
+ var readAnswer = (value) => {
126555
+ if (typeof value === "string") return value;
126556
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
126557
+ return value;
126558
+ }
126559
+ return null;
126560
+ };
126561
+ var withQuestionItem = (state, requestId, questionId, patch) => {
126562
+ const record = requestId === null ? void 0 : questionsOf(state)[requestId];
126563
+ if (requestId === null || questionId === null || record === void 0) return state;
126564
+ if (!record.items.some((item) => item.id === questionId)) return state;
126565
+ return {
126566
+ ...state,
126567
+ questions: {
126568
+ ...questionsOf(state),
126569
+ [requestId]: {
126570
+ ...record,
126571
+ items: record.items.map((item) => item.id === questionId ? patch(item) : item)
126572
+ }
126573
+ }
126574
+ };
126575
+ };
126576
+ var withQueueRecord = (state, intentId, patch) => {
126577
+ const record = intentId === null ? void 0 : queueOf(state)[intentId];
126578
+ if (intentId === null || record === void 0) return state;
126579
+ return { ...state, queue: { ...queueOf(state), [intentId]: patch(record) } };
126580
+ };
126325
126581
  var keepNewest = (record, limit) => {
126326
126582
  const entries = Object.entries(record);
126327
126583
  return entries.length <= limit ? record : Object.fromEntries(entries.slice(entries.length - limit));
@@ -126405,6 +126661,124 @@ var foldImEvent = (state, name, data) => {
126405
126661
  approvals: { ...state.approvals, [requestId]: { ...existing, cardId } }
126406
126662
  };
126407
126663
  }
126664
+ case "question-offered": {
126665
+ const chatKey = nonEmpty(data["chatKey"]);
126666
+ const threadId = nonEmpty(data["threadId"]);
126667
+ const requestId = nonEmpty(data["requestId"]);
126668
+ const items = readQuestionItems(data["items"]);
126669
+ if (chatKey === null || threadId === null || requestId === null || items === null) {
126670
+ return state;
126671
+ }
126672
+ return {
126673
+ ...state,
126674
+ questions: keepNewest(
126675
+ {
126676
+ ...questionsOf(state),
126677
+ [requestId]: { chatKey, threadId, requestId, items, resolved: false }
126678
+ },
126679
+ QUESTION_RECORD_LIMIT
126680
+ )
126681
+ };
126682
+ }
126683
+ case "question-carded": {
126684
+ const cardId = nonEmpty(data["cardId"]);
126685
+ if (cardId === null) return state;
126686
+ return withQuestionItem(
126687
+ state,
126688
+ nonEmpty(data["requestId"]),
126689
+ nonEmpty(data["questionId"]),
126690
+ (item) => ({ ...item, cardId })
126691
+ );
126692
+ }
126693
+ case "question-selected": {
126694
+ const selected = readAnswer(data["selected"]);
126695
+ if (!Array.isArray(selected)) return state;
126696
+ return withQuestionItem(
126697
+ state,
126698
+ nonEmpty(data["requestId"]),
126699
+ nonEmpty(data["questionId"]),
126700
+ (item) => ({ ...item, selected })
126701
+ );
126702
+ }
126703
+ case "question-answered": {
126704
+ const answer = readAnswer(data["answer"]);
126705
+ if (answer === null) return state;
126706
+ const label = typeof data["label"] === "string" ? data["label"] : "";
126707
+ return withQuestionItem(
126708
+ state,
126709
+ nonEmpty(data["requestId"]),
126710
+ nonEmpty(data["questionId"]),
126711
+ (item) => ({ ...item, answer, answerLabel: label })
126712
+ );
126713
+ }
126714
+ case "question-resolved": {
126715
+ const requestId = nonEmpty(data["requestId"]);
126716
+ const existing = requestId === null ? void 0 : questionsOf(state)[requestId];
126717
+ if (requestId === null || existing === void 0) return state;
126718
+ const ending = data["ending"];
126719
+ return {
126720
+ ...state,
126721
+ questions: {
126722
+ ...questionsOf(state),
126723
+ [requestId]: {
126724
+ ...existing,
126725
+ resolved: true,
126726
+ ending: ending === "elsewhere" || ending === "cancelled" ? ending : "here"
126727
+ }
126728
+ }
126729
+ };
126730
+ }
126731
+ case "queue-offered": {
126732
+ const chatKey = nonEmpty(data["chatKey"]);
126733
+ const threadId = nonEmpty(data["threadId"]);
126734
+ const intentId = nonEmpty(data["intentId"]);
126735
+ const steerToken = nonEmpty(data["steerToken"]);
126736
+ const withdrawToken = nonEmpty(data["withdrawToken"]);
126737
+ const resumeToken = nonEmpty(data["resumeToken"]);
126738
+ if (chatKey === null || threadId === null || intentId === null || steerToken === null || withdrawToken === null || resumeToken === null) {
126739
+ return state;
126740
+ }
126741
+ return {
126742
+ ...state,
126743
+ queue: keepNewest(
126744
+ {
126745
+ ...queueOf(state),
126746
+ [intentId]: {
126747
+ chatKey,
126748
+ threadId,
126749
+ intentId,
126750
+ text: typeof data["text"] === "string" ? data["text"] : "",
126751
+ steerToken,
126752
+ withdrawToken,
126753
+ resumeToken,
126754
+ steering: false
126755
+ }
126756
+ },
126757
+ QUEUE_RECORD_LIMIT
126758
+ )
126759
+ };
126760
+ }
126761
+ case "queue-carded": {
126762
+ const cardId = nonEmpty(data["cardId"]);
126763
+ if (cardId === null) return state;
126764
+ return withQueueRecord(state, nonEmpty(data["intentId"]), (record) => ({
126765
+ ...record,
126766
+ cardId
126767
+ }));
126768
+ }
126769
+ case "queue-steering":
126770
+ return withQueueRecord(state, nonEmpty(data["intentId"]), (record) => ({
126771
+ ...record,
126772
+ steering: data["steering"] === true
126773
+ }));
126774
+ case "queue-settled": {
126775
+ const outcome = data["outcome"];
126776
+ if (outcome !== "steered" && outcome !== "sent" && outcome !== "removed") return state;
126777
+ return withQueueRecord(state, nonEmpty(data["intentId"]), (record) => ({
126778
+ ...record,
126779
+ settled: outcome
126780
+ }));
126781
+ }
126408
126782
  case "turn-card-opened": {
126409
126783
  const cardId = nonEmpty(data["cardId"]);
126410
126784
  const chatKey = nonEmpty(data["chatKey"]);
@@ -126580,6 +126954,39 @@ var uploadImage = async (input) => {
126580
126954
  }
126581
126955
  return body.data.image_key;
126582
126956
  };
126957
+ var uploadFile = async (input) => {
126958
+ const form = new FormData();
126959
+ form.set("file_type", "stream");
126960
+ form.set("file_name", input.name);
126961
+ form.set("file", new Blob([new Uint8Array(input.bytes)], { type: input.mimeType }), input.name);
126962
+ const response = await input.http(`${LARK_BASE}/im/v1/files`, {
126963
+ method: "POST",
126964
+ // 不设 content-type:multipart 的 boundary 要让 fetch 自己写。
126965
+ headers: { authorization: `Bearer ${input.token}` },
126966
+ body: form
126967
+ });
126968
+ const body = await readEnvelope("files.create", response);
126969
+ if (typeof body.data?.file_key !== "string") {
126970
+ throw new LarkApiError("files.create", 0, "the response carried no file_key");
126971
+ }
126972
+ return body.data.file_key;
126973
+ };
126974
+ var sendChatFile = async (input) => {
126975
+ const response = await input.http(`${LARK_BASE}/im/v1/messages?receive_id_type=chat_id`, {
126976
+ method: "POST",
126977
+ headers: {
126978
+ authorization: `Bearer ${input.token}`,
126979
+ "content-type": "application/json; charset=utf-8"
126980
+ },
126981
+ body: JSON.stringify({
126982
+ receive_id: input.chatId,
126983
+ msg_type: "file",
126984
+ content: JSON.stringify({ file_key: input.fileKey })
126985
+ })
126986
+ });
126987
+ const body = await readEnvelope("send.file", response);
126988
+ return body.data?.message_id ?? null;
126989
+ };
126583
126990
  var sendChatImage = async (input) => {
126584
126991
  const response = await input.http(`${LARK_BASE}/im/v1/messages?receive_id_type=chat_id`, {
126585
126992
  method: "POST",
@@ -132009,85 +132416,215 @@ var buildPanelCard = (card) => {
132009
132416
  elements
132010
132417
  };
132011
132418
  };
132419
+ var CHOICE_TEMPLATE = {
132420
+ ask: "wathet",
132421
+ queue: "wathet",
132422
+ done: "grey"
132423
+ };
132424
+ var buildChoiceCard = (card) => {
132425
+ const elements = [];
132426
+ const lead = [
132427
+ card.heading === null || card.heading === "" ? "" : `**${stripAngleBrackets(card.heading).replaceAll("*", "")}**`,
132428
+ stripAngleBrackets(card.body)
132429
+ ].filter((part) => part !== "");
132430
+ if (lead.length > 0) elements.push({ tag: "markdown", content: lead.join("\n\n") });
132431
+ for (const row of card.rows) {
132432
+ if (row.length === 0) continue;
132433
+ elements.push({
132434
+ tag: "action",
132435
+ actions: row.map((button) => ({
132436
+ tag: "button",
132437
+ text: { tag: "plain_text", content: button.label },
132438
+ type: button.style,
132439
+ value: { tk: button.token }
132440
+ }))
132441
+ });
132442
+ }
132443
+ if (card.note !== null && card.note !== "") {
132444
+ if (elements.length > 0) elements.push({ tag: "hr" });
132445
+ elements.push({
132446
+ tag: "note",
132447
+ elements: [{ tag: "plain_text", content: card.note }]
132448
+ });
132449
+ }
132450
+ if (elements.length === 0) elements.push({ tag: "markdown", content: " " });
132451
+ return {
132452
+ config: { wide_screen_mode: true },
132453
+ header: {
132454
+ title: { tag: "plain_text", content: card.title },
132455
+ template: CHOICE_TEMPLATE[card.tone]
132456
+ },
132457
+ elements
132458
+ };
132459
+ };
132012
132460
 
132013
132461
  // src/larkInbound.ts
132014
132462
  var import_node_sdk = __toESM(require_lib2(), 1);
132015
- var mentionsApp = (event, appId) => (event.message?.mentions ?? []).some(
132016
- (mention) => mention.id?.app_id === appId || mention.mentioned_type === "bot"
132017
- );
132018
- var mimeFromName = (name) => {
132019
- const ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
132020
- const known = {
132021
- png: "image/png",
132022
- jpg: "image/jpeg",
132023
- jpeg: "image/jpeg",
132024
- gif: "image/gif",
132025
- webp: "image/webp",
132026
- pdf: "application/pdf",
132027
- txt: "text/plain",
132028
- md: "text/markdown",
132029
- json: "application/json",
132030
- csv: "text/csv"
132031
- };
132032
- return known[ext] ?? "application/octet-stream";
132463
+
132464
+ // src/larkPost.ts
132465
+ var styledText = (text2, style) => {
132466
+ let out = text2;
132467
+ if (style?.includes("bold")) out = `**${out}**`;
132468
+ if (style?.includes("italic")) out = `*${out}*`;
132469
+ if (style?.includes("lineThrough")) out = `~~${out}~~`;
132470
+ if (style?.includes("underline")) out = `<u>${out}</u>`;
132471
+ return out;
132033
132472
  };
132034
- var readPost = (content) => {
132035
- if (content === void 0) return null;
132473
+ var nonEmpty2 = (value) => typeof value === "string" && value !== "";
132474
+ var resourceOf = (element, index) => {
132475
+ if (element.tag === "img" && nonEmpty2(element.image_key)) {
132476
+ return {
132477
+ fileKey: element.image_key,
132478
+ kind: "image",
132479
+ name: `image-${String(index)}.png`,
132480
+ mimeType: "image/png"
132481
+ };
132482
+ }
132483
+ if ((element.tag === "media" || element.tag === "file") && nonEmpty2(element.file_key)) {
132484
+ const video = element.tag === "media";
132485
+ const name = nonEmpty2(element.file_name) ? element.file_name : video ? `video-${String(index)}.mp4` : `file-${String(index)}`;
132486
+ return {
132487
+ fileKey: element.file_key,
132488
+ kind: "file",
132489
+ name,
132490
+ mimeType: video && !nonEmpty2(element.file_name) ? "video/mp4" : mimeFromName(name)
132491
+ };
132492
+ }
132493
+ return null;
132494
+ };
132495
+ var placeholderOf = (element, index) => `<${element.tag === "img" ? "image" : element.tag === "media" ? "video" : "file"} ${String(index)}>`;
132496
+ var serializeElement = (element, index, botIds) => {
132497
+ switch (element.tag) {
132498
+ case "text":
132499
+ return styledText(element.text ?? "", element.style);
132500
+ case "a": {
132501
+ const label = element.text ?? element.href ?? "";
132502
+ return nonEmpty2(element.href) ? `[${label}](${element.href})` : label;
132503
+ }
132504
+ case "at": {
132505
+ if (element.user_id !== void 0 && botIds.has(element.user_id)) return "";
132506
+ return nonEmpty2(element.user_name) ? `@${element.user_name}` : "";
132507
+ }
132508
+ case "code_block":
132509
+ return `\`\`\`${element.language ?? ""}
132510
+ ${element.text ?? ""}
132511
+ \`\`\``;
132512
+ case "hr":
132513
+ return "---";
132514
+ case "emotion":
132515
+ return element.emoji_type ?? "";
132516
+ case "img":
132517
+ case "media":
132518
+ case "file":
132519
+ return index === null ? "" : placeholderOf(element, index);
132520
+ default:
132521
+ return "";
132522
+ }
132523
+ };
132524
+ var serializePost = (content, botIds = /* @__PURE__ */ new Set()) => {
132036
132525
  let parsed;
132037
132526
  try {
132038
132527
  parsed = JSON.parse(content);
132039
132528
  } catch {
132040
- return null;
132529
+ return void 0;
132041
132530
  }
132042
- const rows = parsed.content ?? [];
132043
- const lines = [];
132044
- let imageKey = null;
132045
- for (const row of rows) {
132046
- const parts = [];
132047
- for (const segment of row) {
132048
- if (segment.tag === "text" && typeof segment.text === "string") parts.push(segment.text);
132049
- if (segment.tag === "img" && typeof segment.image_key === "string" && imageKey === null) {
132050
- imageKey = segment.image_key;
132051
- }
132052
- }
132053
- const line = parts.join("").trim();
132054
- if (line.length > 0) lines.push(line);
132531
+ const elements = (parsed.content ?? []).flat();
132532
+ const resources = [];
132533
+ const indexOf = /* @__PURE__ */ new Map();
132534
+ for (const element of elements) {
132535
+ const resource = resourceOf(element, resources.length + 1);
132536
+ if (resource === null) continue;
132537
+ resources.push(resource);
132538
+ indexOf.set(element, resources.length);
132055
132539
  }
132056
- return {
132057
- text: lines.join("\n"),
132058
- resource: imageKey === null ? null : { fileKey: imageKey, kind: "image", name: "image.png", mimeType: "image/png" }
132059
- };
132540
+ const md2 = parsed.md;
132541
+ if (typeof md2 === "string" && md2.trim() !== "") {
132542
+ const placeholderByKey = /* @__PURE__ */ new Map();
132543
+ for (const element of elements) {
132544
+ const index = indexOf.get(element);
132545
+ const key = element.tag === "img" ? element.image_key : element.file_key;
132546
+ if (index !== void 0 && nonEmpty2(key)) {
132547
+ placeholderByKey.set(key, placeholderOf(element, index));
132548
+ }
132549
+ }
132550
+ const text3 = md2.replace(
132551
+ /!\[(?:img|video|file)\]\(([^)]+)\)/g,
132552
+ (whole, rawKey) => placeholderByKey.get(rawKey.trim()) ?? whole
132553
+ );
132554
+ return { text: text3, resources };
132555
+ }
132556
+ const paragraphs = (parsed.content ?? []).map(
132557
+ (row) => row.map((element) => serializeElement(element, indexOf.get(element) ?? null, botIds)).join("")
132558
+ );
132559
+ const text2 = paragraphs.map((line) => line.trim()).filter((line) => line !== "").join("\n");
132560
+ if (text2 === "" && resources.length === 0) return void 0;
132561
+ return { text: text2, resources };
132060
132562
  };
132061
- var readResource = (messageType, content) => {
132563
+
132564
+ // src/larkInbound.ts
132565
+ var mentionsApp = (event, appId) => (event.message?.mentions ?? []).some(
132566
+ (mention) => mention.id?.app_id === appId || mention.mentioned_type === "bot"
132567
+ );
132568
+ var RESOURCE_MESSAGE_TYPES = /* @__PURE__ */ new Set(["image", "file", "audio", "media"]);
132569
+ var UNSUPPORTED_MESSAGE_TYPES = /* @__PURE__ */ new Set([
132570
+ "sticker",
132571
+ "folder",
132572
+ "share_chat",
132573
+ "share_user",
132574
+ "system",
132575
+ "merge",
132576
+ "interactive"
132577
+ ]);
132578
+ var botIdsOf = (mentions, appId) => {
132579
+ const ids = /* @__PURE__ */ new Set();
132580
+ for (const mention of mentions) {
132581
+ if (mention.id?.app_id !== appId && mention.mentioned_type !== "bot") continue;
132582
+ ids.add(mention.key);
132583
+ for (const id of [mention.id?.open_id, mention.id?.app_id]) if (id !== void 0) ids.add(id);
132584
+ }
132585
+ return ids;
132586
+ };
132587
+ var readJson = (content) => {
132062
132588
  if (content === void 0) return null;
132063
- let parsed;
132064
132589
  try {
132065
- parsed = JSON.parse(content);
132590
+ const parsed = JSON.parse(content);
132591
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
132066
132592
  } catch {
132067
132593
  return null;
132068
132594
  }
132595
+ };
132596
+ var readResource = (messageType, content) => {
132597
+ const parsed = readJson(content);
132598
+ if (parsed === null) return null;
132599
+ const key = (field) => {
132600
+ const value = parsed[field];
132601
+ return typeof value === "string" && value !== "" ? value : null;
132602
+ };
132069
132603
  if (messageType === "image") {
132070
- const key = parsed["image_key"];
132071
- return typeof key !== "string" ? null : { fileKey: key, kind: "image", name: "image.png", mimeType: "image/png" };
132604
+ const imageKey = key("image_key");
132605
+ return imageKey === null ? null : { fileKey: imageKey, kind: "image", name: "image.png", mimeType: "image/png" };
132072
132606
  }
132073
132607
  if (messageType === "file") {
132074
- const key = parsed["file_key"];
132075
- if (typeof key !== "string") return null;
132076
- const rawName = parsed["file_name"];
132077
- const name = typeof rawName === "string" && rawName.length > 0 ? rawName : "file";
132078
- return { fileKey: key, kind: "file", name, mimeType: mimeFromName(name) };
132608
+ const fileKey = key("file_key");
132609
+ if (fileKey === null) return null;
132610
+ const name = key("file_name") ?? "file";
132611
+ return { fileKey, kind: "file", name, mimeType: mimeFromName(name) };
132612
+ }
132613
+ if (messageType === "audio") {
132614
+ const fileKey = key("file_key");
132615
+ return fileKey === null ? null : { fileKey, kind: "file", name: "voice.opus", mimeType: "audio/ogg" };
132616
+ }
132617
+ if (messageType === "media") {
132618
+ const fileKey = key("file_key");
132619
+ if (fileKey === null) return null;
132620
+ const name = key("file_name") ?? "video.mp4";
132621
+ return { fileKey, kind: "file", name, mimeType: mimeFromName(name) };
132079
132622
  }
132080
132623
  return null;
132081
132624
  };
132082
132625
  var readText = (content) => {
132083
- if (content === void 0) return null;
132084
- try {
132085
- const parsed = JSON.parse(content);
132086
- const text2 = parsed.text;
132087
- return typeof text2 === "string" ? text2 : null;
132088
- } catch {
132089
- return null;
132090
- }
132626
+ const text2 = readJson(content)?.["text"];
132627
+ return typeof text2 === "string" ? text2 : null;
132091
132628
  };
132092
132629
  var parseInboundMessage = (event, appId) => {
132093
132630
  if (event.sender?.sender_type === "app") return null;
@@ -132096,33 +132633,52 @@ var parseInboundMessage = (event, appId) => {
132096
132633
  const { message_id: messageId, chat_id: chatId } = message;
132097
132634
  if (typeof messageId !== "string" || typeof chatId !== "string") return null;
132098
132635
  const chatType = message.chat_type ?? "group";
132099
- if (chatType !== "p2p" && !mentionsApp(event, appId)) return null;
132100
132636
  const messageType = message.message_type ?? "";
132101
- const post = messageType === "post" ? readPost(message.content) : null;
132102
- const resource = post !== null ? post.resource : readResource(messageType, message.content);
132103
- const raw = post !== null ? post.text : messageType === "text" ? readText(message.content) : null;
132104
- const text2 = (message.mentions ?? []).reduce((value, mention) => value.replaceAll(mention.key, ""), raw ?? "").trim();
132105
- if (text2.length === 0 && resource === null) return null;
132106
- return {
132107
- messageId,
132108
- chatId,
132109
- chatType,
132110
- senderOpenId: event.sender?.sender_id?.open_id ?? null,
132111
- text: text2,
132112
- resource
132113
- };
132637
+ if (chatType !== "p2p" && !mentionsApp(event, appId) && !RESOURCE_MESSAGE_TYPES.has(messageType)) {
132638
+ return null;
132639
+ }
132640
+ const senderOpenId = event.sender?.sender_id?.open_id ?? null;
132641
+ if (UNSUPPORTED_MESSAGE_TYPES.has(messageType)) {
132642
+ return {
132643
+ messageId,
132644
+ chatId,
132645
+ chatType,
132646
+ senderOpenId,
132647
+ text: "",
132648
+ resources: [],
132649
+ unsupportedType: messageType
132650
+ };
132651
+ }
132652
+ const mentions = message.mentions ?? [];
132653
+ const post = messageType === "post" ? serializePost(message.content ?? "", botIdsOf(mentions, appId)) : void 0;
132654
+ const single = post === void 0 ? readResource(messageType, message.content) : null;
132655
+ const resources = post !== void 0 ? post.resources : single === null ? [] : [single];
132656
+ const raw = post !== void 0 ? post.text : messageType === "text" ? readText(message.content) : null;
132657
+ const text2 = mentions.reduce((value, mention) => value.replaceAll(mention.key, ""), raw ?? "").trim();
132658
+ if (text2.length === 0 && resources.length === 0) return null;
132659
+ return { messageId, chatId, chatType, senderOpenId, text: text2, resources };
132114
132660
  };
132115
132661
  var fetchMessageResource = async (input) => {
132116
- const url = `https://open.feishu.cn/open-apis/im/v1/messages/${encodeURIComponent(
132662
+ const url = `${LARK_BASE}/im/v1/messages/${encodeURIComponent(
132117
132663
  input.messageId
132118
132664
  )}/resources/${encodeURIComponent(input.resource.fileKey)}?type=${input.resource.kind}`;
132119
132665
  const response = await input.http(url, {
132120
132666
  headers: { authorization: `Bearer ${input.token}` }
132121
132667
  });
132122
132668
  if (!response.ok) {
132669
+ await response.body?.cancel().catch(() => void 0);
132123
132670
  throw new Error(`lark resource fetch failed: HTTP ${String(response.status)}`);
132124
132671
  }
132125
- return new Uint8Array(await response.arrayBuffer());
132672
+ const bytes = await readBodyLimited(response, input.maxBytes);
132673
+ if ((response.headers.get("content-type") ?? "").includes("application/json")) {
132674
+ const envelope = readJson(new TextDecoder().decode(bytes.subarray(0, 4096)));
132675
+ const code2 = envelope?.["code"];
132676
+ if (typeof code2 === "number" && code2 !== 0) {
132677
+ const detail = envelope?.["msg"];
132678
+ throw new LarkApiError("resource", code2, typeof detail === "string" ? detail : "no message");
132679
+ }
132680
+ }
132681
+ return bytes;
132126
132682
  };
132127
132683
  var cardActionValue = (event) => {
132128
132684
  const roots = [event, event?.event];
@@ -132244,7 +132800,7 @@ var makeLarkAdapter = (deps) => {
132244
132800
  appId,
132245
132801
  appSecret,
132246
132802
  onError: handlers.onError,
132247
- onCardAction: handlers.onApproval,
132803
+ onCardAction: handlers.onButton,
132248
132804
  onPanelClick: (click, operatorOpenId) => handlers.onPanelClick?.({
132249
132805
  chatId: click.chatId,
132250
132806
  cardId: click.cardId,
@@ -132258,12 +132814,13 @@ var makeLarkAdapter = (deps) => {
132258
132814
  chatId: message.chatId,
132259
132815
  senderId: message.senderOpenId,
132260
132816
  text: message.text,
132261
- resource: message.resource === null ? null : {
132262
- key: message.resource.fileKey,
132263
- name: message.resource.name,
132264
- mimeType: message.resource.mimeType,
132265
- kind: message.resource.kind
132266
- }
132817
+ resources: message.resources.map((resource) => ({
132818
+ key: resource.fileKey,
132819
+ name: resource.name,
132820
+ mimeType: resource.mimeType,
132821
+ kind: resource.kind
132822
+ })),
132823
+ ...message.unsupportedType === void 0 ? {} : { unsupportedType: message.unsupportedType }
132267
132824
  });
132268
132825
  }
132269
132826
  });
@@ -132292,6 +132849,20 @@ var makeLarkAdapter = (deps) => {
132292
132849
  await sendChatImage({ http: deps.http, token, chatId: binding.chatId, imageKey });
132293
132850
  deps.log.info("lark delivered an image", { name: image2.name });
132294
132851
  },
132852
+ sendFile: async (binding, file) => {
132853
+ const token = await tenantToken(binding);
132854
+ const fileKey = await uploadFile({
132855
+ http: deps.http,
132856
+ token,
132857
+ bytes: file.bytes,
132858
+ name: file.name,
132859
+ mimeType: file.mimeType
132860
+ });
132861
+ await sendChatFile({ http: deps.http, token, chatId: binding.chatId, fileKey });
132862
+ deps.log.info("lark delivered a file", { name: file.name });
132863
+ },
132864
+ // 图 10 MB、文件 30 MB(飞书开放平台文档:上传图片接口、上传文件接口;空文件会被拒)。
132865
+ outboundLimits: { imageBytes: 10 * 1024 * 1024, fileBytes: 30 * 1024 * 1024 },
132295
132866
  askApproval: async (binding, request) => sendCard({
132296
132867
  http: deps.http,
132297
132868
  token: await tenantToken(binding),
@@ -132346,6 +132917,22 @@ var makeLarkAdapter = (deps) => {
132346
132917
  });
132347
132918
  }
132348
132919
  },
132920
+ choiceCards: {
132921
+ open: async (binding, card) => sendCard({
132922
+ http: deps.http,
132923
+ token: await tenantToken(binding),
132924
+ chatId: binding.chatId,
132925
+ card: buildChoiceCard(card)
132926
+ }),
132927
+ update: async (binding, cardId, card) => {
132928
+ await patchCard({
132929
+ http: deps.http,
132930
+ token: await tenantToken(binding),
132931
+ messageId: cardId,
132932
+ card: buildChoiceCard(card)
132933
+ });
132934
+ }
132935
+ },
132349
132936
  acknowledge: async (binding, messageId) => addReaction({
132350
132937
  http: deps.http,
132351
132938
  token: await tenantToken(binding),
@@ -132366,6 +132953,7 @@ var makeLarkAdapter = (deps) => {
132366
132953
  http: deps.http,
132367
132954
  token: await tenantToken(binding),
132368
132955
  messageId: input.messageId,
132956
+ maxBytes: input.maxBytes,
132369
132957
  resource: {
132370
132958
  fileKey: resource.key,
132371
132959
  kind: resource.kind,
@@ -132462,7 +133050,20 @@ var en_default = {
132462
133050
  projectArchived: 'Project "{{title}}" is archived. Restore it, then message again.',
132463
133051
  projectMissing: `The project bound to "{{group}}" is no longer on this machine (it may have been deleted), so a thread cannot be started. In Ycodium's chat settings, pick a project for this group again.`,
132464
133052
  threadGone: "The earlier session was deleted or archived, so this group now starts a new one.",
132465
- teamGone: "The team this group was sending to no longer exists, so messages go to a normal session again."
133053
+ teamGone: "The team this group was sending to no longer exists, so messages go to a normal session again.",
133054
+ attachmentReceived: "Got {{names}} ({{count}} waiting). Tell me what to do with them.",
133055
+ attachmentFailed: "Could not take {{name}}: {{reason}}.",
133056
+ attachmentReason: {
133057
+ tooLarge: "it is over the {{limit}} limit",
133058
+ downloadFailed: "the download failed",
133059
+ storeFailed: "it could not be saved",
133060
+ empty: "it is empty"
133061
+ },
133062
+ attachmentLimit: "At most {{max}} attachments go with one message, so {{name}} was left out. Send a message about the current ones first.",
133063
+ attachmentsNoTeam: "Files cannot be sent to a team yet, so they were left out. Send /thread to go back to a normal session.",
133064
+ unsupportedType: "I can't process {{type}} messages yet.",
133065
+ unsupportedFolder: "Folders can't be downloaded. Zip it and send the archive instead.",
133066
+ sendingFile: "Sending {{name}}:"
132466
133067
  },
132467
133068
  approval: {
132468
133069
  allow: "Allow",
@@ -132470,6 +133071,50 @@ var en_default = {
132470
133071
  alreadyHandled: "Already handled",
132471
133072
  replyHint: "Reply with the number."
132472
133073
  },
133074
+ question: {
133075
+ title: "\u2753 Question",
133076
+ planTitle: "\u{1F4DD} Plan review",
133077
+ planApprove: "Approve plan",
133078
+ planApproveHint: "Let the agent start implementing this plan.",
133079
+ planRevise: "Revise plan",
133080
+ planReviseHint: "Ask the agent to rework the plan before going ahead.",
133081
+ planStop: "Stop here",
133082
+ planStopHint: "Leave plan mode without implementing this plan.",
133083
+ freeformHint: "Or reply with a message to answer in your own words.",
133084
+ replyOne: "Reply with the number.",
133085
+ replyMany: "Reply with the numbers, separated by commas.",
133086
+ submit: "\u2705 Submit",
133087
+ pickFirst: "Pick at least one option first.",
133088
+ answered: "Answer: {{answer}}",
133089
+ elsewhere: "Already answered elsewhere (the web app or another client).",
133090
+ cancelled: "This question was closed without an answer.",
133091
+ alreadyHandled: "Already handled",
133092
+ sendFailed: "Could not hand your answer to the agent. The question may have been answered elsewhere or already closed."
133093
+ },
133094
+ queue: {
133095
+ title: "\u23F3 Queued",
133096
+ waitRunning: "It goes out when the current reply finishes. To add it to the reply in progress now, tap Steer.",
133097
+ waitPaused: "Sending is paused (the last reply was stopped or failed). Tap Resume sending to carry on.",
133098
+ waitIdle: "It goes out shortly.",
133099
+ steer: "\u27A1\uFE0F Steer",
133100
+ withdraw: "\u21A9\uFE0F Withdraw",
133101
+ resume: "\u25B6\uFE0F Resume sending",
133102
+ steeringTitle: "\u23F3 Steering\u2026",
133103
+ steering: "\u{1F4AC} Adding it to the current reply\u2026",
133104
+ sendingTitle: "\u{1F4E4} Sending\u2026",
133105
+ sending: "Sending it now.",
133106
+ steeredTitle: "\u2705 Steered",
133107
+ steered: "Added to the current reply.",
133108
+ sentTitle: "\u{1F4E4} Sent",
133109
+ sent: "It went out as its own turn.",
133110
+ removedTitle: "\u{1F5D1}\uFE0F Withdrawn",
133111
+ removed: "Withdrawn. It will not be sent.",
133112
+ steerFailed: "Could not add it to the current reply (the reply may have just ended or changed). It stays queued and goes out when the reply finishes.",
133113
+ withdrawFailed: "Could not withdraw it: it has already started sending.",
133114
+ resumeFailed: "Could not resume sending. Try again in a moment.",
133115
+ alreadyHandled: "Already handled",
133116
+ plainNotice: "Queued: this message goes out when the current reply finishes."
133117
+ },
132473
133118
  command: {
132474
133119
  teamSelected: 'OK. From now on messages go to "{{title}}".',
132475
133120
  threadSelected: "OK. From now on messages start a normal session.",
@@ -132478,6 +133123,9 @@ var en_default = {
132478
133123
  newSession: "OK. The next message starts a new session.",
132479
133124
  stopNoSession: "Nothing is running in this group.",
132480
133125
  stopNoTurn: "No turn is running right now.",
133126
+ continued: "Resumed. The waiting messages are being sent.",
133127
+ continueNothing: "Nothing to resume: sending is not paused in this group.",
133128
+ continueFailed: "Could not resume sending. Try again in a moment.",
132481
133129
  stopped: "Stopped. The session is still there \u2014 just keep talking."
132482
133130
  },
132483
133131
  status: {
@@ -132499,12 +133147,13 @@ var en_default = {
132499
133147
  },
132500
133148
  help: {
132501
133149
  title: "Commands:",
133150
+ continue: "\xB7 /continue \u2014 resume sending after /stop or a failure paused it; the waiting messages go out",
132502
133151
  stop: "\xB7 /stop \u2014 stop the turn that is running; the session stays",
132503
133152
  status: "\xB7 /status \u2014 what is going on right now",
132504
133153
  new: "\xB7 /new \u2014 forget the current session; the next message starts a new one",
132505
133154
  team: "\xB7 /team name \u2014 send the next messages to a team; /team alone lists the teams",
132506
133155
  thread: "\xB7 /thread \u2014 go back to a normal session",
132507
- aliases: "Chinese shortcuts (\u505C / \u72B6\u6001 / \u65B0\u5BF9\u8BDD / \u4F1A\u8BDD\u5217\u8868 / \u5207\u6362\u4F1A\u8BDD / \u6362\u6A21\u578B / \u8BA1\u5212\u6A21\u5F0F / \u6362\u56E2\u961F / \u6362\u5BF9\u8BDD) work when the reply language is Chinese.",
133156
+ aliases: "Chinese shortcuts (\u505C / \u7EE7\u7EED\u53D1\u9001 / \u72B6\u6001 / \u65B0\u5BF9\u8BDD / \u4F1A\u8BDD\u5217\u8868 / \u5207\u6362\u4F1A\u8BDD / \u6362\u6A21\u578B / \u8BA1\u5212\u6A21\u5F0F / \u6362\u56E2\u961F / \u6362\u5BF9\u8BDD) work when the reply language is Chinese.",
132508
133157
  sessions: "\xB7 /sessions \u2014 list the sessions this bot opened for this group's project",
132509
133158
  resume: "\xB7 /resume number \u2014 switch to one of them (a number from /sessions, or the start of its id)",
132510
133159
  model: "\xB7 /model \u2014 see the model this group uses; /model number or /model instance/model switches it",
@@ -132593,6 +133242,7 @@ var en_default = {
132593
133242
  },
132594
133243
  cmd: {
132595
133244
  new: "New session",
133245
+ continue: "Resume sending",
132596
133246
  stop: "Stop this turn",
132597
133247
  sessions: "Sessions",
132598
133248
  team: "Send to a team",
@@ -132712,7 +133362,20 @@ var zh_CN_default = {
132712
133362
  projectArchived: "\u9879\u76EE\u300C{{title}}\u300D\u5DF2\u5F52\u6863\uFF0C\u6062\u590D\u540E\u518D\u53D1\u6D88\u606F\u3002",
132713
133363
  projectMissing: "\u300C{{group}}\u300D\u7ED1\u7684\u9879\u76EE\u5DF2\u7ECF\u4E0D\u5728\u8FD9\u53F0\u673A\u5668\u4E0A\u4E86\uFF08\u53EF\u80FD\u88AB\u5220\u4E86\uFF09\uFF0C\u5F00\u4E0D\u4E86\u4F1A\u8BDD\u3002\u5230 Ycodium \u7684\u804A\u5929\u8BBE\u7F6E\u91CC\u7ED9\u8FD9\u4E2A\u7FA4\u91CD\u65B0\u9009\u4E00\u4E2A\u9879\u76EE\u3002",
132714
133364
  threadGone: "\u4E4B\u524D\u90A3\u6761\u4F1A\u8BDD\u5DF2\u88AB\u5220\u9664\u6216\u5F52\u6863\uFF0C\u8FD9\u4E2A\u7FA4\u6539\u4E3A\u5F00\u4E00\u6761\u65B0\u4F1A\u8BDD\u3002",
132715
- teamGone: "\u8FD9\u4E2A\u7FA4\u539F\u6765\u6D3E\u7ED9\u7684\u961F\u5DF2\u7ECF\u4E0D\u5728\u4E86\uFF0C\u63A5\u4E0B\u6765\u6539\u56DE\u666E\u901A\u4F1A\u8BDD\u3002"
133365
+ teamGone: "\u8FD9\u4E2A\u7FA4\u539F\u6765\u6D3E\u7ED9\u7684\u961F\u5DF2\u7ECF\u4E0D\u5728\u4E86\uFF0C\u63A5\u4E0B\u6765\u6539\u56DE\u666E\u901A\u4F1A\u8BDD\u3002",
133366
+ attachmentReceived: "\u6536\u5230 {{names}}\uFF08\u73B0\u6709 {{count}} \u4E2A\u5F85\u5904\u7406\uFF09\u3002\u8BF4\u4E00\u53E5\u60F3\u8BA9\u6211\u600E\u4E48\u5904\u7406\u3002",
133367
+ attachmentFailed: "\u6CA1\u80FD\u6536\u4E0B {{name}}\uFF1A{{reason}}\u3002",
133368
+ attachmentReason: {
133369
+ tooLarge: "\u8D85\u8FC7\u4E86 {{limit}} \u7684\u4E0A\u9650",
133370
+ downloadFailed: "\u4E0B\u8F7D\u5931\u8D25\u4E86",
133371
+ storeFailed: "\u4FDD\u5B58\u4E0D\u4E86",
133372
+ empty: "\u6587\u4EF6\u662F\u7A7A\u7684"
133373
+ },
133374
+ attachmentLimit: "\u4E00\u6761\u6D88\u606F\u6700\u591A\u5E26 {{max}} \u4E2A\u9644\u4EF6\uFF0C\u6240\u4EE5 {{name}} \u6CA1\u6536\u3002\u5148\u53D1\u4E00\u53E5\u8BDD\u5904\u7406\u624B\u4E0A\u8FD9\u4E9B\u3002",
133375
+ attachmentsNoTeam: "\u6587\u4EF6\u8FD8\u53D1\u4E0D\u5230\u56E2\u961F\uFF0C\u8FD9\u6B21\u6CA1\u5E26\u4E0A\u3002\u53D1 /thread \u56DE\u5230\u666E\u901A\u4F1A\u8BDD\u3002",
133376
+ unsupportedType: "\u6211\u8FD8\u4E0D\u80FD\u5904\u7406 {{type}} \u7C7B\u578B\u7684\u6D88\u606F\u3002",
133377
+ unsupportedFolder: "\u6587\u4EF6\u5939\u6CA1\u6CD5\u4E0B\u8F7D\uFF0C\u8BF7\u6253\u5305\u6210\u538B\u7F29\u6587\u4EF6\u518D\u53D1\u3002",
133378
+ sendingFile: "\u6B63\u5728\u53D1\u9001 {{name}}\uFF1A"
132716
133379
  },
132717
133380
  approval: {
132718
133381
  allow: "\u5141\u8BB8",
@@ -132720,6 +133383,50 @@ var zh_CN_default = {
132720
133383
  alreadyHandled: "\u5DF2\u5904\u7406",
132721
133384
  replyHint: "\u56DE\u590D\u7F16\u53F7\u5373\u53EF\u3002"
132722
133385
  },
133386
+ question: {
133387
+ title: "\u2753 \u63D0\u95EE",
133388
+ planTitle: "\u{1F4DD} \u8BA1\u5212\u786E\u8BA4",
133389
+ planApprove: "\u6279\u51C6\u8BA1\u5212",
133390
+ planApproveHint: "\u8BA9 agent \u6309\u8FD9\u4EFD\u8BA1\u5212\u5F00\u59CB\u5B9E\u65BD\u3002",
133391
+ planRevise: "\u4FEE\u6539\u8BA1\u5212",
133392
+ planReviseHint: "\u8BA9 agent \u5148\u4FEE\u6539\u8BA1\u5212\uFF0C\u518D\u7EE7\u7EED\u5F80\u4E0B\u505A\u3002",
133393
+ planStop: "\u5230\u6B64\u4E3A\u6B62",
133394
+ planStopHint: "\u9000\u51FA\u8BA1\u5212\u6A21\u5F0F\uFF0C\u4E0D\u5B9E\u65BD\u8FD9\u4EFD\u8BA1\u5212\u3002",
133395
+ freeformHint: "\u4E5F\u53EF\u4EE5\u76F4\u63A5\u56DE\u590D\u4E00\u6761\u6D88\u606F\uFF0C\u7528\u81EA\u5DF1\u7684\u8BDD\u4F5C\u7B54\u3002",
133396
+ replyOne: "\u56DE\u590D\u7F16\u53F7\u5373\u53EF\u3002",
133397
+ replyMany: "\u56DE\u590D\u7F16\u53F7\uFF0C\u591A\u4E2A\u7528\u9017\u53F7\u9694\u5F00\u3002",
133398
+ submit: "\u2705 \u63D0\u4EA4",
133399
+ pickFirst: "\u5148\u9009\u4E00\u9879\u518D\u63D0\u4EA4\u3002",
133400
+ answered: "\u56DE\u7B54\uFF1A{{answer}}",
133401
+ elsewhere: "\u5DF2\u5728\u522B\u5904\u56DE\u7B54\uFF08\u7F51\u9875\u6216\u5176\u4ED6\u5BA2\u6237\u7AEF\uFF09\u3002",
133402
+ cancelled: "\u8FD9\u9053\u63D0\u95EE\u5DF2\u7ED3\u675F\uFF0C\u6CA1\u6709\u6536\u5230\u56DE\u7B54\u3002",
133403
+ alreadyHandled: "\u5DF2\u5904\u7406",
133404
+ sendFailed: "\u6CA1\u80FD\u628A\u4F60\u7684\u56DE\u7B54\u4EA4\u7ED9 agent\u3002\u8FD9\u9053\u63D0\u95EE\u53EF\u80FD\u5DF2\u5728\u522B\u5904\u56DE\u7B54\uFF0C\u6216\u5DF2\u7ECF\u7ED3\u675F\u3002"
133405
+ },
133406
+ queue: {
133407
+ title: "\u23F3 \u5DF2\u6392\u961F",
133408
+ waitRunning: "\u5F53\u524D\u8FD9\u4E00\u8F6E\u56DE\u590D\u7ED3\u675F\u540E\u81EA\u52A8\u53D1\u51FA\u3002\u60F3\u73B0\u5728\u5C31\u9001\u8FDB\u6B63\u5728\u8FDB\u884C\u7684\u8FD9\u4E00\u8F6E\uFF0C\u70B9\u300C\u63D2\u8BDD\u300D\u3002",
133409
+ waitPaused: "\u53D1\u9001\u5DF2\u6682\u505C\uFF08\u4E0A\u4E00\u8F6E\u88AB\u505C\u6B62\u6216\u51FA\u9519\u4E86\uFF09\u3002\u70B9\u300C\u6062\u590D\u53D1\u9001\u300D\u7EE7\u7EED\u3002",
133410
+ waitIdle: "\u9A6C\u4E0A\u53D1\u51FA\u3002",
133411
+ steer: "\u27A1\uFE0F \u63D2\u8BDD",
133412
+ withdraw: "\u21A9\uFE0F \u64A4\u56DE",
133413
+ resume: "\u25B6\uFE0F \u6062\u590D\u53D1\u9001",
133414
+ steeringTitle: "\u23F3 \u63D2\u8BDD\u4E2D\u2026",
133415
+ steering: "\u{1F4AC} \u6B63\u5728\u9001\u8FDB\u5F53\u524D\u8FD9\u4E00\u8F6E\u2026",
133416
+ sendingTitle: "\u{1F4E4} \u53D1\u9001\u4E2D\u2026",
133417
+ sending: "\u6B63\u5728\u53D1\u51FA\u3002",
133418
+ steeredTitle: "\u2705 \u5DF2\u63D2\u8BDD",
133419
+ steered: "\u5DF2\u9001\u8FDB\u5F53\u524D\u8FD9\u4E00\u8F6E\u3002",
133420
+ sentTitle: "\u{1F4E4} \u5DF2\u53D1\u9001",
133421
+ sent: "\u5DF2\u4F5C\u4E3A\u5355\u72EC\u7684\u4E00\u8F6E\u53D1\u51FA\u3002",
133422
+ removedTitle: "\u{1F5D1}\uFE0F \u5DF2\u64A4\u56DE",
133423
+ removed: "\u5DF2\u64A4\u56DE\uFF0C\u4E0D\u4F1A\u53D1\u51FA\u3002",
133424
+ steerFailed: "\u6CA1\u80FD\u9001\u8FDB\u5F53\u524D\u8FD9\u4E00\u8F6E\uFF08\u8FD9\u4E00\u8F6E\u53EF\u80FD\u521A\u7ED3\u675F\u6216\u5DF2\u7ECF\u6362\u4E86\uFF09\u3002\u6D88\u606F\u4ECD\u5728\u6392\u961F\uFF0C\u8FD9\u4E00\u8F6E\u7ED3\u675F\u540E\u53D1\u51FA\u3002",
133425
+ withdrawFailed: "\u6CA1\u80FD\u64A4\u56DE\uFF1A\u8FD9\u6761\u6D88\u606F\u5DF2\u7ECF\u5F00\u59CB\u53D1\u9001\u4E86\u3002",
133426
+ resumeFailed: "\u6CA1\u80FD\u6062\u590D\u53D1\u9001\uFF0C\u7A0D\u540E\u518D\u8BD5\u3002",
133427
+ alreadyHandled: "\u5DF2\u5904\u7406",
133428
+ plainNotice: "\u5DF2\u6392\u961F\uFF1A\u5F53\u524D\u8FD9\u4E00\u8F6E\u56DE\u590D\u7ED3\u675F\u540E\u53D1\u51FA\u3002"
133429
+ },
132723
133430
  command: {
132724
133431
  teamSelected: "\u597D\uFF0C\u63A5\u4E0B\u6765\u6D3E\u7ED9\u300C{{title}}\u300D\u3002",
132725
133432
  threadSelected: "\u597D\uFF0C\u63A5\u4E0B\u6765\u5F00\u666E\u901A\u4F1A\u8BDD\u3002",
@@ -132728,6 +133435,9 @@ var zh_CN_default = {
132728
133435
  newSession: "\u597D\uFF0C\u4E0B\u4E00\u53E5\u8BDD\u5F00\u4E00\u6761\u65B0\u4F1A\u8BDD\u3002",
132729
133436
  stopNoSession: "\u8FD9\u4E2A\u7FA4\u73B0\u5728\u6CA1\u6709\u5728\u8DD1\u7684\u6D3B\u513F\u3002",
132730
133437
  stopNoTurn: "\u73B0\u5728\u6CA1\u6709\u5728\u8DD1\u7684\u4E00\u8F6E\u3002",
133438
+ continued: "\u5DF2\u6062\u590D\uFF0C\u6392\u7740\u7684\u6D88\u606F\u5F00\u59CB\u53D1\u9001\u3002",
133439
+ continueNothing: "\u6CA1\u6709\u53EF\u6062\u590D\u7684\uFF1A\u8FD9\u4E2A\u7FA4\u7684\u53D1\u9001\u6CA1\u6709\u88AB\u6682\u505C\u3002",
133440
+ continueFailed: "\u6CA1\u80FD\u6062\u590D\u53D1\u9001\uFF0C\u7A0D\u540E\u518D\u8BD5\u3002",
132731
133441
  stopped: "\u505C\u4E86\u3002\u4F1A\u8BDD\u8FD8\u5728\uFF0C\u63A5\u7740\u8BF4\u5C31\u884C\u3002"
132732
133442
  },
132733
133443
  status: {
@@ -132749,12 +133459,13 @@ var zh_CN_default = {
132749
133459
  },
132750
133460
  help: {
132751
133461
  title: "\u80FD\u7528\u7684\u547D\u4EE4\uFF1A",
133462
+ continue: "\xB7 /continue \u2014\u2014 /stop \u6216\u51FA\u9519\u6682\u505C\u4E86\u53D1\u9001\u4E4B\u540E\uFF0C\u7528\u5B83\u6062\u590D\uFF0C\u6392\u7740\u7684\u6D88\u606F\u7EE7\u7EED\u53D1\u51FA",
132752
133463
  stop: "\xB7 /stop \u2014\u2014 \u505C\u6389\u6B63\u5728\u8DD1\u7684\u8FD9\u4E00\u8F6E\uFF0C\u4F1A\u8BDD\u7559\u7740",
132753
133464
  status: "\xB7 /status \u2014\u2014 \u73B0\u5728\u5728\u5E72\u4EC0\u4E48",
132754
133465
  new: "\xB7 /new \u2014\u2014 \u5FD8\u6389\u5F53\u524D\u4F1A\u8BDD\uFF0C\u4E0B\u4E00\u53E5\u5F00\u65B0\u7684",
132755
133466
  team: "\xB7 /team \u961F\u540D \u2014\u2014 \u628A\u63A5\u4E0B\u6765\u7684\u6D3B\u6D3E\u7ED9\u4E00\u652F\u961F\uFF1B/team \u4E0D\u5E26\u540D\u5B57\u770B\u6709\u54EA\u4E9B\u961F",
132756
133467
  thread: "\xB7 /thread \u2014\u2014 \u5207\u56DE\u666E\u901A\u4F1A\u8BDD",
132757
- aliases: "\u4E2D\u6587\u4E5F\u884C\uFF1A\u505C / \u72B6\u6001 / \u65B0\u5BF9\u8BDD / \u4F1A\u8BDD\u5217\u8868 / \u5207\u6362\u4F1A\u8BDD / \u6362\u6A21\u578B / \u8BA1\u5212\u6A21\u5F0F / \u6362\u56E2\u961F / \u6362\u5BF9\u8BDD\u3002",
133468
+ aliases: "\u4E2D\u6587\u4E5F\u884C\uFF1A\u505C / \u7EE7\u7EED\u53D1\u9001 / \u72B6\u6001 / \u65B0\u5BF9\u8BDD / \u4F1A\u8BDD\u5217\u8868 / \u5207\u6362\u4F1A\u8BDD / \u6362\u6A21\u578B / \u8BA1\u5212\u6A21\u5F0F / \u6362\u56E2\u961F / \u6362\u5BF9\u8BDD\u3002",
132758
133469
  sessions: "\xB7 /sessions \u2014\u2014 \u5217\u51FA\u672C\u673A\u5668\u4EBA\u4E3A\u8FD9\u4E2A\u7FA4\u7684\u9879\u76EE\u5F00\u8FC7\u7684\u4F1A\u8BDD",
132759
133470
  resume: "\xB7 /resume \u7F16\u53F7 \u2014\u2014 \u5207\u5230\u5176\u4E2D\u4E00\u6761\uFF08/sessions \u91CC\u7684\u7F16\u53F7\uFF0C\u6216\u4F1A\u8BDD id \u7684\u5F00\u5934\uFF09",
132760
133471
  model: "\xB7 /model \u2014\u2014 \u770B\u8FD9\u4E2A\u7FA4\u7528\u7684\u6A21\u578B\uFF1B/model \u7F16\u53F7 \u6216 /model \u5B9E\u4F8B/\u6A21\u578B \u5207\u6362",
@@ -132843,6 +133554,7 @@ var zh_CN_default = {
132843
133554
  },
132844
133555
  cmd: {
132845
133556
  new: "\u65B0\u4F1A\u8BDD",
133557
+ continue: "\u6062\u590D\u53D1\u9001",
132846
133558
  stop: "\u505C\u6B62\u672C\u8F6E",
132847
133559
  sessions: "\u4F1A\u8BDD\u5217\u8868",
132848
133560
  team: "\u6D3E\u7ED9\u56E2\u961F",
@@ -133033,6 +133745,14 @@ var PANEL_COMMANDS = [
133033
133745
  labelKey: "panel.cmd.stop",
133034
133746
  action: { op: "run", arg: "stop" }
133035
133747
  },
133748
+ {
133749
+ id: "continue",
133750
+ category: "session",
133751
+ line: "/continue",
133752
+ kind: "continue",
133753
+ labelKey: "panel.cmd.continue",
133754
+ action: { op: "run", arg: "continue" }
133755
+ },
133036
133756
  {
133037
133757
  id: "sessions",
133038
133758
  category: "session",
@@ -133393,6 +134113,724 @@ var makePanelController = (deps) => {
133393
134113
  };
133394
134114
  };
133395
134115
 
134116
+ // src/flowHost.ts
134117
+ var NOT_MINE = { handled: false };
134118
+ var handledWith = (notice) => ({ handled: true, notice });
134119
+
134120
+ // src/questionCards.ts
134121
+ var PLAN_ACTION_TEXT = {
134122
+ approve: { label: "question.planApprove", hint: "question.planApproveHint" },
134123
+ revise: { label: "question.planRevise", hint: "question.planReviseHint" },
134124
+ stop: { label: "question.planStop", hint: "question.planStopHint" }
134125
+ };
134126
+ var planTextOf = (item, option) => item.kind === "plan" && option.planAction !== void 0 ? PLAN_ACTION_TEXT[option.planAction] : void 0;
134127
+ var optionLabelOf = (item, option, t) => {
134128
+ const plan = planTextOf(item, option);
134129
+ return plan === void 0 ? option.label : t(plan.label);
134130
+ };
134131
+ var optionDescriptionOf = (item, option, t) => {
134132
+ const plan = planTextOf(item, option);
134133
+ return (plan === void 0 ? option.description : t(plan.hint)).trim();
134134
+ };
134135
+ var QUESTION_TEXT_MAX_CHARS = 1500;
134136
+ var ANSWER_ECHO_MAX_CHARS = 200;
134137
+ var clip2 = (text2, limit) => {
134138
+ const chars = Array.from(text2.trim());
134139
+ return chars.length <= limit ? chars.join("") : `${chars.slice(0, limit).join("")}\u2026`;
134140
+ };
134141
+ var titleOf = (item, index, total, t) => {
134142
+ const base = item.kind === "plan" ? t("question.planTitle") : t("question.title");
134143
+ return total > 1 ? `${base} ${String(index + 1)}/${String(total)}` : base;
134144
+ };
134145
+ var bodyOf = (item, t) => clip2(
134146
+ item.options.filter((option) => optionDescriptionOf(item, option, t) !== "").map(
134147
+ (option) => `\xB7 ${optionLabelOf(item, option, t)}: ${optionDescriptionOf(item, option, t)}`
134148
+ ).join("\n"),
134149
+ QUESTION_TEXT_MAX_CHARS
134150
+ );
134151
+ var headingOf = (item) => clip2(
134152
+ [item.header, item.question].filter((part) => part.trim() !== "").join("\n"),
134153
+ QUESTION_TEXT_MAX_CHARS
134154
+ );
134155
+ var questionCardOf = (item, index, total, t) => ({
134156
+ tone: "ask",
134157
+ title: titleOf(item, index, total, t),
134158
+ heading: headingOf(item),
134159
+ body: bodyOf(item, t),
134160
+ note: item.allowFreeform ? t("question.freeformHint") : null,
134161
+ rows: [
134162
+ ...item.options.map((option) => {
134163
+ const ticked = item.multiSelect && item.selected.includes(option.value);
134164
+ return [
134165
+ {
134166
+ label: ticked ? `\u2705 ${optionLabelOf(item, option, t)}` : optionLabelOf(item, option, t),
134167
+ token: option.token,
134168
+ style: ticked ? "primary" : "default"
134169
+ }
134170
+ ];
134171
+ }),
134172
+ ...item.multiSelect && item.submitToken !== void 0 ? [[{ label: t("question.submit"), token: item.submitToken, style: "primary" }]] : []
134173
+ ]
134174
+ });
134175
+ var questionSettledCardOf = (item, index, total, ending, t) => ({
134176
+ tone: "done",
134177
+ title: titleOf(item, index, total, t),
134178
+ heading: headingOf(item),
134179
+ body: "",
134180
+ note: item.answer !== void 0 ? t("question.answered", { answer: item.answerLabel ?? "" }) : ending === "cancelled" ? t("question.cancelled") : t("question.elsewhere"),
134181
+ rows: []
134182
+ });
134183
+ var questionPlainText = (item, index, total, t) => [
134184
+ titleOf(item, index, total, t),
134185
+ headingOf(item),
134186
+ ...item.options.map((option, position) => {
134187
+ const description = optionDescriptionOf(item, option, t);
134188
+ return `${String(position + 1)}. ${optionLabelOf(item, option, t)}${description === "" ? "" : ` \u2014 ${description}`}`;
134189
+ }),
134190
+ "",
134191
+ item.multiSelect ? t("question.replyMany") : t("question.replyOne"),
134192
+ ...item.allowFreeform ? [t("question.freeformHint")] : []
134193
+ ].join("\n");
134194
+ var parseNumberedReply = (item, text2, t) => {
134195
+ const trimmed = text2.trim();
134196
+ const pattern = item.multiSelect ? /^\d+(?:[\s,,、]+\d+)*$/ : /^\d+$/;
134197
+ if (!pattern.test(trimmed)) return null;
134198
+ const numbers = [...new Set(trimmed.split(/[\s,,、]+/).map(Number))];
134199
+ const picked = numbers.map((number) => item.options[number - 1]);
134200
+ if (picked.some((option) => option === void 0)) return null;
134201
+ const options = item.options.filter((option) => picked.includes(option));
134202
+ if (item.multiSelect) {
134203
+ return {
134204
+ answer: options.map((option) => option.value),
134205
+ label: options.map((option) => optionLabelOf(item, option, t)).join(", ")
134206
+ };
134207
+ }
134208
+ const only = options[0];
134209
+ return only === void 0 ? null : { answer: only.value, label: optionLabelOf(item, only, t) };
134210
+ };
134211
+
134212
+ // src/questionFlow.ts
134213
+ var makeQuestionFlow = (host) => {
134214
+ const { t } = host;
134215
+ const recordOf = (requestId) => questionsOf(host.state())[requestId];
134216
+ const itemOf = (requestId, questionId) => recordOf(requestId)?.items.find((item) => item.id === questionId);
134217
+ const hasButtons = (chatKey) => host.target(chatKey)?.adapter.choiceCards !== void 0;
134218
+ const redraw = async (requestId, questionId) => {
134219
+ const record = recordOf(requestId);
134220
+ const item = itemOf(requestId, questionId);
134221
+ const target = record === void 0 ? void 0 : host.target(record.chatKey);
134222
+ if (record === void 0 || item?.cardId === void 0 || target === void 0) return;
134223
+ if (record.resolved || item.answer !== void 0) return;
134224
+ try {
134225
+ await target.adapter.choiceCards?.update(
134226
+ target.binding,
134227
+ item.cardId,
134228
+ questionCardOf(item, record.items.indexOf(item), record.items.length, t)
134229
+ );
134230
+ } catch (error) {
134231
+ host.warn("chat could not update the question card", error);
134232
+ }
134233
+ };
134234
+ const freeze = async (requestId, questionId) => {
134235
+ const record = recordOf(requestId);
134236
+ const item = itemOf(requestId, questionId);
134237
+ const target = record === void 0 ? void 0 : host.target(record.chatKey);
134238
+ if (record === void 0 || item?.cardId === void 0 || target === void 0) return;
134239
+ try {
134240
+ await target.adapter.choiceCards?.update(
134241
+ target.binding,
134242
+ item.cardId,
134243
+ questionSettledCardOf(
134244
+ item,
134245
+ record.items.indexOf(item),
134246
+ record.items.length,
134247
+ record.ending ?? "here",
134248
+ t
134249
+ )
134250
+ );
134251
+ } catch (error) {
134252
+ host.warn("chat could not update the question card", error);
134253
+ }
134254
+ };
134255
+ const sayNext = async (requestId) => {
134256
+ const record = recordOf(requestId);
134257
+ if (record === void 0 || record.resolved) return;
134258
+ const item = record.items.find(isUnanswered);
134259
+ if (item === void 0) return;
134260
+ await host.say(
134261
+ record.chatKey,
134262
+ questionPlainText(item, record.items.indexOf(item), record.items.length, t)
134263
+ );
134264
+ };
134265
+ const post = async (requestId) => {
134266
+ const first = recordOf(requestId);
134267
+ const target = first === void 0 ? void 0 : host.target(first.chatKey);
134268
+ if (first === void 0 || target === void 0) return;
134269
+ const cards = target.adapter.choiceCards;
134270
+ if (cards === void 0) {
134271
+ await sayNext(requestId);
134272
+ return;
134273
+ }
134274
+ for (const questionId of first.items.map((item) => item.id)) {
134275
+ const record = recordOf(requestId);
134276
+ const item = itemOf(requestId, questionId);
134277
+ if (record === void 0 || record.resolved || item === void 0) return;
134278
+ try {
134279
+ const cardId = await cards.open(
134280
+ target.binding,
134281
+ questionCardOf(item, record.items.indexOf(item), record.items.length, t)
134282
+ );
134283
+ if (cardId === null) continue;
134284
+ host.commit("question-carded", { requestId, questionId, cardId });
134285
+ const after = recordOf(requestId);
134286
+ if (after?.resolved === true || itemOf(requestId, questionId)?.answer !== void 0) {
134287
+ await freeze(requestId, questionId);
134288
+ }
134289
+ } catch (error) {
134290
+ host.warn("chat could not ask the question", error);
134291
+ }
134292
+ }
134293
+ };
134294
+ const offer = (chatKey, threadId, pending) => {
134295
+ const target = host.target(chatKey);
134296
+ if (target === void 0) return;
134297
+ const buttons = target.adapter.choiceCards !== void 0;
134298
+ if (!buttons && target.adapter.answerByText === void 0) {
134299
+ host.warn("chat cannot show questions on this platform");
134300
+ return;
134301
+ }
134302
+ const fresh = pending.filter((request) => recordOf(request.requestId) === void 0);
134303
+ for (const request of buttons ? fresh : fresh.slice(0, 1)) {
134304
+ host.commit("question-offered", {
134305
+ chatKey,
134306
+ threadId,
134307
+ requestId: request.requestId,
134308
+ items: request.questions.map((question) => ({
134309
+ id: question.id,
134310
+ header: question.header,
134311
+ question: question.question,
134312
+ kind: question.kind,
134313
+ multiSelect: question.multiSelect,
134314
+ allowFreeform: question.allowFreeform,
134315
+ options: question.options.map((option) => ({
134316
+ token: host.newToken(),
134317
+ label: option.label,
134318
+ value: option.value,
134319
+ description: option.description,
134320
+ ...option.planAction === void 0 ? {} : { planAction: option.planAction }
134321
+ })),
134322
+ ...question.multiSelect ? { submitToken: host.newToken() } : {}
134323
+ }))
134324
+ });
134325
+ if (recordOf(request.requestId) === void 0) continue;
134326
+ host.outbound(chatKey, () => post(request.requestId));
134327
+ }
134328
+ };
134329
+ const answer = async (requestId, questionId, value, label) => {
134330
+ const before = recordOf(requestId);
134331
+ const item = itemOf(requestId, questionId);
134332
+ if (before === void 0 || before.resolved || item === void 0 || !isUnanswered(item)) {
134333
+ return false;
134334
+ }
134335
+ host.commit("question-answered", { requestId, questionId, answer: value, label });
134336
+ const after = recordOf(requestId);
134337
+ if (after === void 0) return false;
134338
+ const complete = after.items.every((entry) => !isUnanswered(entry));
134339
+ if (complete) host.commit("question-resolved", { requestId, ending: "here" });
134340
+ host.outbound(after.chatKey, () => freeze(requestId, questionId));
134341
+ if (!complete) {
134342
+ if (!hasButtons(after.chatKey)) host.outbound(after.chatKey, () => sayNext(requestId));
134343
+ return true;
134344
+ }
134345
+ try {
134346
+ await host.dispatch({
134347
+ type: "thread.user-input.respond",
134348
+ commandId: host.newId(),
134349
+ threadId: after.threadId,
134350
+ requestId,
134351
+ answers: Object.fromEntries(after.items.map((entry) => [entry.id, entry.answer])),
134352
+ createdAt: host.now()
134353
+ });
134354
+ } catch (error) {
134355
+ host.warn("chat could not hand the answer to the agent", error);
134356
+ await host.say(after.chatKey, t("question.sendFailed"));
134357
+ }
134358
+ return true;
134359
+ };
134360
+ const click = (token, operatorId) => {
134361
+ const found = findQuestionByToken(host.state(), token);
134362
+ if (found === void 0) return NOT_MINE;
134363
+ const { record, item } = found;
134364
+ if (record.resolved || !isUnanswered(item)) return handledWith(t("question.alreadyHandled"));
134365
+ if (!host.allowed(record.chatKey, operatorId)) return handledWith(t("reply.notAllowed"));
134366
+ const requestId = record.requestId;
134367
+ if (found.click.kind === "option") {
134368
+ const option = found.click.option;
134369
+ if (!item.multiSelect) {
134370
+ void answer(requestId, item.id, option.value, optionLabelOf(item, option, t));
134371
+ return handledWith(null);
134372
+ }
134373
+ const selected = item.selected.includes(option.value) ? item.selected.filter((value) => value !== option.value) : [...item.selected, option.value];
134374
+ host.commit("question-selected", { requestId, questionId: item.id, selected });
134375
+ host.outbound(record.chatKey, () => redraw(requestId, item.id));
134376
+ return handledWith(null);
134377
+ }
134378
+ const chosen = item.options.filter((option) => item.selected.includes(option.value));
134379
+ if (chosen.length === 0) return handledWith(t("question.pickFirst"));
134380
+ void answer(
134381
+ requestId,
134382
+ item.id,
134383
+ chosen.map((option) => option.value),
134384
+ chosen.map((option) => optionLabelOf(item, option, t)).join(", ")
134385
+ );
134386
+ return handledWith(null);
134387
+ };
134388
+ const answerByText = async (chatKey, threadId, text2) => {
134389
+ const record = openQuestionOf(host.state(), chatKey, threadId);
134390
+ const trimmed = text2.trim();
134391
+ if (record === void 0 || trimmed === "") return false;
134392
+ const unanswered = record.items.filter(isUnanswered);
134393
+ const first = unanswered[0];
134394
+ if (first === void 0) return false;
134395
+ if (!hasButtons(chatKey)) {
134396
+ const numbered = parseNumberedReply(first, trimmed, t);
134397
+ if (numbered !== null) {
134398
+ return answer(record.requestId, first.id, numbered.answer, numbered.label);
134399
+ }
134400
+ }
134401
+ const freeform = unanswered.find((item) => item.allowFreeform);
134402
+ if (freeform === void 0) return false;
134403
+ return answer(record.requestId, freeform.id, trimmed, clip2(trimmed, ANSWER_ECHO_MAX_CHARS));
134404
+ };
134405
+ const resolved = (requestId, outcome) => {
134406
+ const record = recordOf(requestId);
134407
+ if (record === void 0 || record.resolved) return;
134408
+ const ending = outcome === "answered" ? "elsewhere" : "cancelled";
134409
+ host.commit("question-resolved", { requestId, ending });
134410
+ for (const item of record.items) {
134411
+ if (isUnanswered(item)) host.outbound(record.chatKey, () => freeze(requestId, item.id));
134412
+ }
134413
+ };
134414
+ return { offer, click, answerByText, resolved };
134415
+ };
134416
+
134417
+ // src/sendFile.ts
134418
+ var failure = (code2, text2) => ({
134419
+ text: text2,
134420
+ isError: true,
134421
+ errorCode: code2
134422
+ });
134423
+ var describeError = (error) => error instanceof Error ? error.message : String(error);
134424
+ var reasonOf = (error) => {
134425
+ const reason = error?.reason;
134426
+ return typeof reason === "string" ? reason : void 0;
134427
+ };
134428
+ var formatSize = (bytes) => bytes < 1024 * 1024 ? `${String(Math.max(1, Math.round(bytes / 1024)))} KB` : formatMegabytes(bytes);
134429
+ var baseName = (path) => path.split(/[\\/]/).filter(Boolean).at(-1) ?? "";
134430
+ var readFailure = (error, path) => {
134431
+ switch (reasonOf(error)) {
134432
+ case "outside-workspace":
134433
+ return failure(
134434
+ "outside-workspace",
134435
+ `"${path}" is outside this conversation's working folder. Only files inside the project can be sent.`
134436
+ );
134437
+ case "not-a-file":
134438
+ return failure("not-a-file", `"${path}" is not a regular file (a folder cannot be sent).`);
134439
+ case "not-found":
134440
+ return failure("not-found", `"${path}" does not exist. Check the path and try again.`);
134441
+ case "too-large":
134442
+ return failure("too-large", `"${path}" is too large to send.`);
134443
+ case "invocation-ended":
134444
+ return failure(
134445
+ "invocation-ended",
134446
+ "The file could not be read: the tool call had already ended."
134447
+ );
134448
+ default:
134449
+ return failure("unreadable", `"${path}" could not be read: ${describeError(error)}`);
134450
+ }
134451
+ };
134452
+ var makeSendFileTool = (deps) => ({
134453
+ name: "send_file",
134454
+ description: "Send a file from this project to the chat this conversation is connected to (Lark, Telegram or WeCom), as a native image or file message. Use it when the user asked for a concrete deliverable, or would clearly benefit from one: a chart, a screenshot, a generated report, a CSV, an archive. Do not use it for text you can just say. `path` is relative to your working folder, or an absolute path inside the project; folders, and files outside the project, are refused. Images are sent as pictures when the platform allows it, everything else (documents, audio, video) as a file. Size limits are per platform (about 10 MB for a picture and 20 to 50 MB for a file); a too-large file is refused, so compress or split it. The chat first gets one line of caption, then the file. It only works in a conversation that was started from a chat; elsewhere it fails with not-in-chat and sends nothing.",
134455
+ inputSchema: {
134456
+ type: "object",
134457
+ properties: {
134458
+ path: {
134459
+ type: "string",
134460
+ description: "The file to send: a path relative to your working folder, or an absolute path inside the project"
134461
+ },
134462
+ description: {
134463
+ type: "string",
134464
+ description: "Optional caption: one short sentence, in the language the user writes in, saying what is being sent. Shown in the chat before the file. Leave it out to use a default line."
134465
+ }
134466
+ },
134467
+ required: ["path"]
134468
+ },
134469
+ handle: async (input, invocation) => {
134470
+ const path = typeof input["path"] === "string" ? input["path"].trim() : "";
134471
+ if (path === "") return failure("unreadable", "`path` is required.");
134472
+ const binding = invocation.threadId === null ? void 0 : deps.boundChat(invocation.threadId);
134473
+ if (binding === void 0) {
134474
+ return failure(
134475
+ "not-in-chat",
134476
+ "This conversation is not connected to a chat, so there is nowhere to send the file. Nothing was sent."
134477
+ );
134478
+ }
134479
+ const adapter = deps.adapterFor(binding);
134480
+ if (adapter?.sendFile === void 0 || adapter.outboundLimits === void 0) {
134481
+ return failure(
134482
+ "unsupported-platform",
134483
+ `${adapter?.label ?? "This chat platform"} cannot receive files from the agent yet. Nothing was sent.`
134484
+ );
134485
+ }
134486
+ const workspace = invocation.workspace;
134487
+ if (workspace === void 0) {
134488
+ return failure(
134489
+ "no-workspace",
134490
+ "Files cannot be read for this call (it did not come from a conversation)."
134491
+ );
134492
+ }
134493
+ const limits = adapter.outboundLimits;
134494
+ let size;
134495
+ try {
134496
+ const info = await workspace.stat(path);
134497
+ if (!info.isFile) {
134498
+ return failure("not-a-file", `"${path}" is not a regular file (a folder cannot be sent).`);
134499
+ }
134500
+ size = info.size;
134501
+ } catch (error) {
134502
+ return readFailure(error, path);
134503
+ }
134504
+ const minBytes = limits.minBytes ?? 1;
134505
+ if (size < minBytes) {
134506
+ return failure(
134507
+ "empty",
134508
+ size === 0 ? `"${path}" is empty, and an empty file cannot be sent.` : `"${path}" is only ${String(size)} bytes, and ${adapter.label} cannot send a file smaller than ${String(minBytes)} bytes.`
134509
+ );
134510
+ }
134511
+ const tooLarge = (bytes2) => failure(
134512
+ "too-large",
134513
+ `"${path}" is ${formatSize(bytes2)}, over the ${formatMegabytes(limits.fileBytes)} limit for ${adapter.label}. Compress or split it and try again.`
134514
+ );
134515
+ if (size > limits.fileBytes) return tooLarge(size);
134516
+ let bytes;
134517
+ try {
134518
+ bytes = (await workspace.readFile(path, { maxBytes: limits.fileBytes })).bytes;
134519
+ } catch (error) {
134520
+ return reasonOf(error) === "too-large" ? tooLarge(size) : readFailure(error, path);
134521
+ }
134522
+ if (bytes.byteLength === 0)
134523
+ return failure("empty", `"${path}" is empty, and an empty file cannot be sent.`);
134524
+ if (bytes.byteLength > limits.fileBytes) return tooLarge(bytes.byteLength);
134525
+ const name = sanitizeAttachmentName(baseName(path), "file");
134526
+ const mimeType = resolveMime(mimeFromName(name), bytes);
134527
+ const asImage = isInlineImageMime(mimeType) && (adapter.imageMimeTypes?.has(mimeType) ?? true) && bytes.byteLength <= limits.imageBytes && adapter.sendImage !== void 0;
134528
+ const caption = typeof input["description"] === "string" && input["description"].trim() !== "" ? input["description"].trim() : deps.translate("reply.sendingFile", { name });
134529
+ try {
134530
+ await adapter.sendText(binding, caption);
134531
+ } catch (error) {
134532
+ deps.log.warn("chat could not send the caption of a file", { error: describeError(error) });
134533
+ }
134534
+ try {
134535
+ const file = { bytes, name, mimeType };
134536
+ if (asImage && adapter.sendImage !== void 0) await adapter.sendImage(binding, file);
134537
+ else await adapter.sendFile(binding, file);
134538
+ } catch (error) {
134539
+ deps.log.warn("chat could not deliver a file", { name, error: describeError(error) });
134540
+ return failure(
134541
+ "upload-failed",
134542
+ `${adapter.label} did not accept "${name}": ${describeError(error)}. The caption line may already be in the chat; nothing else was sent.`
134543
+ );
134544
+ }
134545
+ deps.log.info("chat delivered a file from the agent", {
134546
+ name,
134547
+ sizeBytes: bytes.byteLength,
134548
+ asImage
134549
+ });
134550
+ return {
134551
+ text: `Sent "${name}" (${formatSize(bytes.byteLength)}) to ${adapter.label}.`
134552
+ };
134553
+ }
134554
+ });
134555
+
134556
+ // src/queueCards.ts
134557
+ var QUEUE_PREVIEW_MAX_CHARS = 200;
134558
+ var WAITING_STATUSES = /* @__PURE__ */ new Set(["queued", "blocked"]);
134559
+ var IN_FLIGHT_STATUSES = /* @__PURE__ */ new Set([
134560
+ "preparing",
134561
+ "dispatching",
134562
+ "reconciling"
134563
+ ]);
134564
+ var WITHDRAWN_STATUSES = /* @__PURE__ */ new Set(["cancelled", "abandoned"]);
134565
+ var queueViewOf = (record, facts, released) => {
134566
+ const status = facts.intent?.status;
134567
+ if (status === void 0) return null;
134568
+ if (WITHDRAWN_STATUSES.has(status)) return { kind: "settled", outcome: "removed" };
134569
+ if (IN_FLIGHT_STATUSES.has(status))
134570
+ return record.steering ? { kind: "steering" } : { kind: "sending" };
134571
+ if (!WAITING_STATUSES.has(status)) {
134572
+ return { kind: "settled", outcome: record.steering ? "steered" : "sent" };
134573
+ }
134574
+ if (record.steering && !released) return { kind: "steering" };
134575
+ return { kind: "queued", running: facts.activeTurnId !== null, paused: facts.queuePaused };
134576
+ };
134577
+ var SETTLED_TITLE = {
134578
+ steered: "queue.steeredTitle",
134579
+ sent: "queue.sentTitle",
134580
+ removed: "queue.removedTitle"
134581
+ };
134582
+ var SETTLED_NOTE = {
134583
+ steered: "queue.steered",
134584
+ sent: "queue.sent",
134585
+ removed: "queue.removed"
134586
+ };
134587
+ var queueCardOf = (record, view, note, t) => {
134588
+ const body = clip2(record.text, QUEUE_PREVIEW_MAX_CHARS);
134589
+ if (view.kind === "steering") {
134590
+ return {
134591
+ tone: "queue",
134592
+ title: t("queue.steeringTitle"),
134593
+ heading: null,
134594
+ body,
134595
+ note: t("queue.steering"),
134596
+ rows: []
134597
+ };
134598
+ }
134599
+ if (view.kind === "sending") {
134600
+ return {
134601
+ tone: "queue",
134602
+ title: t("queue.sendingTitle"),
134603
+ heading: null,
134604
+ body,
134605
+ note: t("queue.sending"),
134606
+ rows: []
134607
+ };
134608
+ }
134609
+ if (view.kind === "settled") {
134610
+ return {
134611
+ tone: "done",
134612
+ title: t(SETTLED_TITLE[view.outcome]),
134613
+ heading: null,
134614
+ body,
134615
+ note: t(SETTLED_NOTE[view.outcome]),
134616
+ rows: []
134617
+ };
134618
+ }
134619
+ const steerable = view.running && !view.paused;
134620
+ const state = view.paused ? t("queue.waitPaused") : view.running ? t("queue.waitRunning") : t("queue.waitIdle");
134621
+ return {
134622
+ tone: "queue",
134623
+ title: t("queue.title"),
134624
+ heading: null,
134625
+ body,
134626
+ note: note ?? state,
134627
+ rows: [
134628
+ [
134629
+ ...steerable ? [{ label: t("queue.steer"), token: record.steerToken, style: "primary" }] : [],
134630
+ { label: t("queue.withdraw"), token: record.withdrawToken, style: "default" }
134631
+ ],
134632
+ ...view.paused ? [[{ label: t("queue.resume"), token: record.resumeToken, style: "primary" }]] : []
134633
+ ]
134634
+ };
134635
+ };
134636
+ var queueViewKey = (view, note) => JSON.stringify([view, note]);
134637
+
134638
+ // src/queueFlow.ts
134639
+ var LIVE_STATUSES = /* @__PURE__ */ new Set([
134640
+ "queued",
134641
+ "preparing",
134642
+ "dispatching",
134643
+ "running",
134644
+ "reconciling",
134645
+ "blocked"
134646
+ ]);
134647
+ var makeQueueFlow = (host, deps) => {
134648
+ const { t } = host;
134649
+ const shown = /* @__PURE__ */ new Map();
134650
+ const notes = /* @__PURE__ */ new Map();
134651
+ const recordOf = (intentId) => queueOf(host.state())[intentId];
134652
+ const factsOf = (execution, intentId) => ({
134653
+ activeTurnId: execution.activeTurnId,
134654
+ queuePaused: execution.queuePaused,
134655
+ intent: execution.intents.find((intent) => intent.intentId === intentId)
134656
+ });
134657
+ const noteQueued = async (chatKey, threadId, messageId, text2) => {
134658
+ const target = host.target(chatKey);
134659
+ if (target === void 0) return;
134660
+ let execution;
134661
+ try {
134662
+ execution = await deps.readExecution(threadId);
134663
+ } catch (error) {
134664
+ host.warn("chat could not read the turn queue", error);
134665
+ return;
134666
+ }
134667
+ const intent = execution?.intents.find((entry) => entry.messageId === messageId);
134668
+ if (execution === null || intent === void 0 || intent.status !== "queued") return;
134669
+ const waiting = execution.activeTurnId !== null || execution.queuePaused || execution.intents.some(
134670
+ (other) => other.intentId !== intent.intentId && LIVE_STATUSES.has(other.status) && other.sequence < intent.sequence
134671
+ );
134672
+ if (!waiting) return;
134673
+ const cards = target.adapter.choiceCards;
134674
+ if (cards === void 0) {
134675
+ await host.say(chatKey, t("queue.plainNotice"));
134676
+ return;
134677
+ }
134678
+ const intentId = intent.intentId;
134679
+ host.commit("queue-offered", {
134680
+ chatKey,
134681
+ threadId,
134682
+ intentId,
134683
+ text: text2,
134684
+ steerToken: host.newToken(),
134685
+ withdrawToken: host.newToken(),
134686
+ resumeToken: host.newToken()
134687
+ });
134688
+ host.outbound(chatKey, async () => {
134689
+ const record = recordOf(intentId);
134690
+ if (record === void 0) return;
134691
+ try {
134692
+ const latest = await deps.readExecution(threadId) ?? execution;
134693
+ const view = queueViewOf(record, factsOf(latest, intentId), false) ?? {
134694
+ kind: "queued",
134695
+ running: latest.activeTurnId !== null,
134696
+ paused: latest.queuePaused
134697
+ };
134698
+ if (view.kind === "settled") {
134699
+ host.commit("queue-settled", { intentId, outcome: view.outcome });
134700
+ return;
134701
+ }
134702
+ const cardId = await cards.open(target.binding, queueCardOf(record, view, null, t));
134703
+ if (cardId === null) return;
134704
+ host.commit("queue-carded", { intentId, cardId });
134705
+ shown.set(intentId, queueViewKey(view, null));
134706
+ } catch (error) {
134707
+ host.warn("chat could not show the queued message", error);
134708
+ }
134709
+ });
134710
+ };
134711
+ const redraw = async (intentId, released) => {
134712
+ const record = recordOf(intentId);
134713
+ const target = record === void 0 ? void 0 : host.target(record.chatKey);
134714
+ if (record === void 0 || record.settled !== void 0 || target === void 0) return;
134715
+ if (record.cardId === void 0) return;
134716
+ let execution;
134717
+ try {
134718
+ execution = await deps.readExecution(record.threadId);
134719
+ } catch (error) {
134720
+ host.warn("chat could not read the turn queue", error);
134721
+ return;
134722
+ }
134723
+ if (execution === null) return;
134724
+ const view = queueViewOf(record, factsOf(execution, intentId), released);
134725
+ if (view === null) return;
134726
+ if (view.kind === "queued" && record.steering) {
134727
+ host.commit("queue-steering", { intentId, steering: false });
134728
+ notes.set(intentId, t("queue.steerFailed"));
134729
+ }
134730
+ if (view.kind === "settled") notes.delete(intentId);
134731
+ await paint(record, view);
134732
+ if (view.kind === "settled") {
134733
+ host.commit("queue-settled", { intentId, outcome: view.outcome });
134734
+ shown.delete(intentId);
134735
+ }
134736
+ };
134737
+ const paint = async (record, view) => {
134738
+ const target = host.target(record.chatKey);
134739
+ const cards = target?.adapter.choiceCards;
134740
+ if (target === void 0 || cards === void 0 || record.cardId === void 0) return;
134741
+ const note = notes.get(record.intentId) ?? null;
134742
+ const key = queueViewKey(view, note);
134743
+ if (shown.get(record.intentId) === key) return;
134744
+ try {
134745
+ await cards.update(target.binding, record.cardId, queueCardOf(record, view, note, t));
134746
+ shown.set(record.intentId, key);
134747
+ } catch (error) {
134748
+ host.warn("chat could not update the queued message card", error);
134749
+ }
134750
+ };
134751
+ const refresh = (threadId, released = null) => {
134752
+ for (const record of openQueueOfThread(host.state(), threadId)) {
134753
+ host.outbound(record.chatKey, () => redraw(record.intentId, record.intentId === released));
134754
+ }
134755
+ };
134756
+ const run = async (record, work, failure2) => {
134757
+ notes.delete(record.intentId);
134758
+ let ok = false;
134759
+ try {
134760
+ ok = await work();
134761
+ } catch (error) {
134762
+ host.warn("chat could not change the queue", error);
134763
+ }
134764
+ if (!ok) notes.set(record.intentId, failure2);
134765
+ host.outbound(record.chatKey, () => redraw(record.intentId, false));
134766
+ };
134767
+ const steer = async (record) => {
134768
+ const execution = await deps.readExecution(record.threadId);
134769
+ if (execution?.activeTurnId == null) return false;
134770
+ host.commit("queue-steering", { intentId: record.intentId, steering: true });
134771
+ try {
134772
+ await host.dispatch({
134773
+ type: "thread.turn-intent.run",
134774
+ commandId: host.newId(),
134775
+ threadId: record.threadId,
134776
+ intentId: record.intentId,
134777
+ mode: "steer",
134778
+ expectedTurnId: execution.activeTurnId,
134779
+ createdAt: host.now()
134780
+ });
134781
+ return true;
134782
+ } catch (error) {
134783
+ host.commit("queue-steering", { intentId: record.intentId, steering: false });
134784
+ throw error;
134785
+ }
134786
+ };
134787
+ const click = (token, operatorId) => {
134788
+ const found = findQueueByToken(host.state(), token);
134789
+ if (found === void 0) return NOT_MINE;
134790
+ const { record, op } = found;
134791
+ if (record.settled !== void 0) return handledWith(t("queue.alreadyHandled"));
134792
+ if (!host.allowed(record.chatKey, operatorId)) return handledWith(t("reply.notAllowed"));
134793
+ if (op === "steer") {
134794
+ if (record.steering) return handledWith(t("queue.alreadyHandled"));
134795
+ void run(record, () => steer(record), t("queue.steerFailed"));
134796
+ } else if (op === "withdraw") {
134797
+ void run(
134798
+ record,
134799
+ async () => {
134800
+ await host.dispatch({
134801
+ type: "thread.turn-intent.cancel",
134802
+ commandId: host.newId(),
134803
+ threadId: record.threadId,
134804
+ intentId: record.intentId,
134805
+ // 只撤还没开始发送的:已经在路上的由宿主拒绝,而不是被当成「已放弃」。
134806
+ expectedStatus: "queued",
134807
+ expectedDeliveryCertainty: "not-dispatched",
134808
+ createdAt: host.now()
134809
+ });
134810
+ return true;
134811
+ },
134812
+ t("queue.withdrawFailed")
134813
+ );
134814
+ } else {
134815
+ void run(
134816
+ record,
134817
+ async () => {
134818
+ await host.dispatch({
134819
+ type: "thread.turn-queue.resume",
134820
+ commandId: host.newId(),
134821
+ threadId: record.threadId,
134822
+ createdAt: host.now()
134823
+ });
134824
+ return true;
134825
+ },
134826
+ t("queue.resumeFailed")
134827
+ );
134828
+ }
134829
+ return handledWith(null);
134830
+ };
134831
+ return { noteQueued, refresh, click };
134832
+ };
134833
+
133396
134834
  // src/telegramApi.ts
133397
134835
  var API_BASE = "https://api.telegram.org";
133398
134836
  var POLL_TIMEOUT_SECONDS = 45;
@@ -133411,28 +134849,19 @@ var TelegramApiError = class extends Error {
133411
134849
  var isNotModified = (error) => error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("message is not modified");
133412
134850
  var PHOTO_NAME = "photo.jpg";
133413
134851
  var PHOTO_MIME = "image/jpeg";
133414
- var mimeFromName2 = (name) => {
133415
- const ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
133416
- const known = {
133417
- png: "image/png",
133418
- jpg: "image/jpeg",
133419
- jpeg: "image/jpeg",
133420
- gif: "image/gif",
133421
- webp: "image/webp",
133422
- pdf: "application/pdf",
133423
- txt: "text/plain",
133424
- md: "text/markdown",
133425
- json: "application/json",
133426
- csv: "text/csv"
133427
- };
133428
- return known[ext] ?? "application/octet-stream";
133429
- };
133430
- var resourceOf = (message) => {
134852
+ var withSize = (size) => size === void 0 ? {} : { sizeBytes: size };
134853
+ var resourceOf2 = (message) => {
133431
134854
  const photo = message.photo;
133432
134855
  if (photo !== void 0 && photo.length > 0) {
133433
134856
  const largest = photo[photo.length - 1];
133434
134857
  if (largest !== void 0) {
133435
- return { fileId: largest.file_id, name: PHOTO_NAME, mimeType: PHOTO_MIME, kind: "image" };
134858
+ return {
134859
+ fileId: largest.file_id,
134860
+ name: PHOTO_NAME,
134861
+ mimeType: PHOTO_MIME,
134862
+ kind: "image",
134863
+ ...withSize(largest.file_size)
134864
+ };
133436
134865
  }
133437
134866
  }
133438
134867
  const document2 = message.document;
@@ -133441,12 +134870,38 @@ var resourceOf = (message) => {
133441
134870
  return {
133442
134871
  fileId: document2.file_id,
133443
134872
  name,
133444
- mimeType: document2.mime_type ?? mimeFromName2(name),
133445
- kind: "file"
134873
+ mimeType: document2.mime_type ?? mimeFromName(name),
134874
+ kind: "file",
134875
+ ...withSize(document2.file_size)
134876
+ };
134877
+ }
134878
+ const media = [
134879
+ [message.voice, "voice.ogg", "audio/ogg"],
134880
+ [message.audio, "audio", "application/octet-stream"],
134881
+ [message.video, "video.mp4", "video/mp4"],
134882
+ [message.video_note, "video-note.mp4", "video/mp4"],
134883
+ [message.animation, "animation.mp4", "video/mp4"]
134884
+ ];
134885
+ for (const [entry, fallbackName, fallbackMime] of media) {
134886
+ if (entry === void 0) continue;
134887
+ const name = entry.file_name ?? fallbackName;
134888
+ return {
134889
+ fileId: entry.file_id,
134890
+ name,
134891
+ mimeType: entry.mime_type ?? (entry.file_name === void 0 ? fallbackMime : mimeFromName(name)),
134892
+ kind: "file",
134893
+ ...withSize(entry.file_size)
133446
134894
  };
133447
134895
  }
133448
134896
  return null;
133449
134897
  };
134898
+ var unsupportedTypeOf = (message) => {
134899
+ for (const type of ["sticker", "location", "venue", "contact", "poll", "dice"]) {
134900
+ if (message[type] !== void 0) return type;
134901
+ }
134902
+ return null;
134903
+ };
134904
+ var TELEGRAM_DOWNLOAD_LIMIT_BYTES = 20 * 1024 * 1024;
133450
134905
  var splitForTelegram = (text2, limit = TELEGRAM_TEXT_MAX_CHARS) => splitMessage(text2, limit);
133451
134906
  var nextOffset = (updates, current) => updates.reduce((offset, update) => Math.max(offset, update.update_id + 1), current);
133452
134907
  var commandForBot = (text2, botUsername) => {
@@ -133476,6 +134931,24 @@ var makeTelegramApi = (input) => {
133476
134931
  }
133477
134932
  return envelope.result;
133478
134933
  };
134934
+ const upload = async (method, field, chatId, file) => {
134935
+ const form = new FormData();
134936
+ form.set("chat_id", chatId);
134937
+ form.set(field, new Blob([new Uint8Array(file.bytes)], { type: file.mimeType }), file.name);
134938
+ const response = await input.http(`${API_BASE}/bot${input.token}/${method}`, {
134939
+ method: "POST",
134940
+ body: form
134941
+ });
134942
+ const envelope = await response.json();
134943
+ if (envelope.ok !== true) {
134944
+ throw new TelegramApiError(
134945
+ method,
134946
+ envelope.error_code,
134947
+ envelope.description ?? "no reason",
134948
+ envelope.parameters?.retry_after
134949
+ );
134950
+ }
134951
+ };
133479
134952
  const keyboard = (rows) => ({
133480
134953
  inline_keyboard: rows.map(
133481
134954
  (row) => row.map((button) => ({ text: button.text, callback_data: button.data }))
@@ -133544,41 +135017,38 @@ var makeTelegramApi = (input) => {
133544
135017
  callback_query_id: callbackQueryId,
133545
135018
  ...text2 === void 0 ? {} : { text: text2 }
133546
135019
  }),
133547
- sendPhoto: async (chatId, photo) => {
133548
- const form = new FormData();
133549
- form.set("chat_id", chatId);
133550
- form.set(
133551
- "photo",
133552
- new Blob([new Uint8Array(photo.bytes)], { type: photo.mimeType }),
133553
- photo.name
133554
- );
133555
- const response = await input.http(`${API_BASE}/bot${input.token}/sendPhoto`, {
133556
- method: "POST",
133557
- body: form
133558
- });
133559
- const envelope = await response.json();
133560
- if (envelope.ok !== true) {
133561
- throw new TelegramApiError(
133562
- "sendPhoto",
133563
- envelope.error_code,
133564
- envelope.description ?? "no reason"
133565
- );
133566
- }
133567
- },
135020
+ sendPhoto: (chatId, photo) => upload("sendPhoto", "photo", chatId, photo),
135021
+ /** 以文件形式发(不压缩、不限尺寸比例):什么类型都行,音频与视频也走这条。 */
135022
+ sendDocument: (chatId, file) => upload("sendDocument", "document", chatId, file),
133568
135023
  /** 取一个文件的字节。要两步:先换路径,再按路径下载。 */
133569
- fetchFile: async (fileId) => {
133570
- const file = await call("getFile", { file_id: fileId });
135024
+ fetchFile: async (fileId, maxBytes) => {
135025
+ const file = await call("getFile", {
135026
+ file_id: fileId
135027
+ }).catch((error) => {
135028
+ if (error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("too big")) {
135029
+ throw new ImResourceTooLargeError(TELEGRAM_DOWNLOAD_LIMIT_BYTES);
135030
+ }
135031
+ throw error;
135032
+ });
133571
135033
  const path = file.file_path;
133572
135034
  if (path === void 0) throw new TelegramApiError("getFile", void 0, "no file_path");
133573
135035
  const response = await input.http(`${API_BASE}/file/bot${input.token}/${path}`);
133574
135036
  if (!response.ok) {
133575
135037
  throw new TelegramApiError("file", response.status, "download failed");
133576
135038
  }
133577
- return new Uint8Array(await response.arrayBuffer());
135039
+ return readBodyLimited(response, maxBytes);
133578
135040
  }
133579
135041
  };
133580
135042
  };
133581
135043
 
135044
+ // src/telegramChoice.ts
135045
+ var renderChoiceCard = (card) => ({
135046
+ text: [card.title, card.heading ?? "", card.body, card.note ?? ""].filter((part) => part !== "").join("\n\n"),
135047
+ rows: card.rows.filter((row) => row.length > 0).map(
135048
+ (row) => row.map((button) => ({ text: button.label, data: button.token }))
135049
+ )
135050
+ });
135051
+
133582
135052
  // src/telegramPanel.ts
133583
135053
  var PANEL_DATA_PREFIX = "pn:";
133584
135054
  var renderPanel = (card, version) => {
@@ -133609,7 +135079,7 @@ var parsePanelData = (data) => {
133609
135079
 
133610
135080
  // src/plainTurnText.ts
133611
135081
  var RUNNING_LABEL_MAX_CHARS = 200;
133612
- var clip2 = (text2, max) => {
135082
+ var clip3 = (text2, max) => {
133613
135083
  const chars = Array.from(text2);
133614
135084
  return chars.length <= max ? text2 : `${chars.slice(0, max - 1).join("")}\u2026`;
133615
135085
  };
@@ -133618,7 +135088,7 @@ var turnTextParts = (view, t) => {
133618
135088
  if (view.omitted > 0) names.unshift(t("card.step.omitted", { count: view.omitted }));
133619
135089
  const headLines = [view.title === "" ? t("card.untitled") : view.title];
133620
135090
  if (names.length > 0) headLines.push(names.join(" \u2192 "));
133621
- if (view.running !== null) headLines.push(`\u{1F527} ${clip2(view.running, RUNNING_LABEL_MAX_CHARS)}`);
135091
+ if (view.running !== null) headLines.push(`\u{1F527} ${clip3(view.running, RUNNING_LABEL_MAX_CHARS)}`);
133622
135092
  const body = view.body.trim();
133623
135093
  const footLines = [];
133624
135094
  if (view.status === "working") {
@@ -133695,14 +135165,14 @@ var tokenOf = (binding) => binding.values["telegramToken"] ?? "";
133695
135165
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
133696
135166
  var makeTelegramAdapter = (deps) => {
133697
135167
  const apis = /* @__PURE__ */ new Map();
133698
- const sleep = deps.sleep ?? defaultSleep;
135168
+ const sleep2 = deps.sleep ?? defaultSleep;
133699
135169
  const t = deps.translate;
133700
135170
  const withFloodWait = async (run) => {
133701
135171
  try {
133702
135172
  return await run();
133703
135173
  } catch (error) {
133704
135174
  if (error instanceof TelegramApiError && error.code === 429 && error.retryAfterSeconds !== void 0 && error.retryAfterSeconds <= FLOOD_WAIT_MAX_SECONDS) {
133705
- await sleep(error.retryAfterSeconds * 1e3);
135175
+ await sleep2(error.retryAfterSeconds * 1e3);
133706
135176
  return run();
133707
135177
  }
133708
135178
  throw error;
@@ -133786,29 +135256,45 @@ var makeTelegramAdapter = (deps) => {
133786
135256
  const callback = update.callback_query;
133787
135257
  if (callback?.data !== void 0) {
133788
135258
  const operatorId = callback.from === void 0 ? null : String(callback.from.id);
133789
- const notice = callback.data.startsWith(PANEL_DATA_PREFIX) ? panelClick(callback, operatorId, handlers) : handlers.onApproval(callback.data, operatorId);
135259
+ const notice = callback.data.startsWith(PANEL_DATA_PREFIX) ? panelClick(callback, operatorId, handlers) : handlers.onButton(callback.data, operatorId);
133790
135260
  await api.answerCallback(callback.id, notice ?? void 0).catch(() => void 0);
133791
135261
  continue;
133792
135262
  }
133793
135263
  const message = update.message;
133794
135264
  if (message === void 0) continue;
133795
- const resource = resourceOf(message);
135265
+ const resource = resourceOf2(message);
133796
135266
  const text2 = commandForBot(
133797
135267
  (message.text ?? message.caption ?? "").trim(),
133798
135268
  botUsername
133799
135269
  );
133800
- if (text2 === null || text2 === "" && resource === null) continue;
135270
+ if (text2 === null) continue;
135271
+ if (text2 === "" && resource === null) {
135272
+ const unsupported = unsupportedTypeOf(message);
135273
+ if (unsupported === null) continue;
135274
+ handlers.onMessage({
135275
+ messageId: String(message.message_id),
135276
+ chatId: String(message.chat.id),
135277
+ senderId: message.from === void 0 ? null : String(message.from.id),
135278
+ text: "",
135279
+ resources: [],
135280
+ unsupportedType: unsupported
135281
+ });
135282
+ continue;
135283
+ }
133801
135284
  handlers.onMessage({
133802
135285
  messageId: String(message.message_id),
133803
135286
  chatId: String(message.chat.id),
133804
135287
  senderId: message.from === void 0 ? null : String(message.from.id),
133805
135288
  text: text2,
133806
- resource: resource === null ? null : {
133807
- key: resource.fileId,
133808
- name: resource.name,
133809
- mimeType: resource.mimeType,
133810
- kind: resource.kind
133811
- }
135289
+ resources: resource === null ? [] : [
135290
+ {
135291
+ key: resource.fileId,
135292
+ name: resource.name,
135293
+ mimeType: resource.mimeType,
135294
+ kind: resource.kind,
135295
+ ...resource.sizeBytes === void 0 ? {} : { sizeBytes: resource.sizeBytes }
135296
+ }
135297
+ ]
133812
135298
  });
133813
135299
  }
133814
135300
  } catch (error) {
@@ -133835,8 +135321,23 @@ var makeTelegramAdapter = (deps) => {
133835
135321
  },
133836
135322
  sendImage: async (binding, image2) => {
133837
135323
  const api = await apiFor(binding);
133838
- await api.sendPhoto(binding.chatId, image2);
135324
+ try {
135325
+ await api.sendPhoto(binding.chatId, image2);
135326
+ } catch (error) {
135327
+ if (!(error instanceof TelegramApiError && error.code === 400)) throw error;
135328
+ deps.log.info("telegram could not send the picture as a photo, sending it as a file", {
135329
+ name: image2.name,
135330
+ reason: error.message
135331
+ });
135332
+ await api.sendDocument(binding.chatId, image2);
135333
+ }
133839
135334
  },
135335
+ sendFile: async (binding, file) => {
135336
+ const api = await apiFor(binding);
135337
+ await api.sendDocument(binding.chatId, file);
135338
+ },
135339
+ // 单张照片 10 MB、单个文件 50 MB。这两个数来自 Bot API 文档的记忆,本仓库里没有核对过。
135340
+ outboundLimits: { imageBytes: 10 * 1024 * 1024, fileBytes: 50 * 1024 * 1024 },
133840
135341
  askApproval: async (binding, request) => {
133841
135342
  const api = await apiFor(binding);
133842
135343
  return api.sendButtons(binding.chatId, request.body, request.options);
@@ -133906,9 +135407,24 @@ ${line}`
133906
135407
  panelKeyboards.set(key, { version, actions: rendered.actions });
133907
135408
  }
133908
135409
  },
135410
+ choiceCards: {
135411
+ open: async (binding, card) => {
135412
+ const api = await apiFor(binding);
135413
+ const rendered = renderChoiceCard(card);
135414
+ return withFloodWait(() => api.sendOne(binding.chatId, rendered.text, rendered.rows));
135415
+ },
135416
+ update: async (binding, cardId, card) => {
135417
+ const api = await apiFor(binding);
135418
+ const rendered = renderChoiceCard(card);
135419
+ await withFloodWait(
135420
+ () => api.editMessage(binding.chatId, cardId, rendered.text, rendered.rows)
135421
+ );
135422
+ }
135423
+ },
135424
+ downloadLimitBytes: TELEGRAM_DOWNLOAD_LIMIT_BYTES,
133909
135425
  fetchResource: async (binding, input) => {
133910
135426
  const api = await apiFor(binding);
133911
- return api.fetchFile(input.resource.key);
135427
+ return api.fetchFile(input.resource.key, input.maxBytes);
133912
135428
  }
133913
135429
  };
133914
135430
  };
@@ -134137,34 +135653,177 @@ var makeTurnCards = (deps) => {
134137
135653
  };
134138
135654
  };
134139
135655
 
135656
+ // src/wecomUpload.ts
135657
+ import * as NodeCrypto from "node:crypto";
135658
+ var WECOM_CHUNK_BYTES = 512 * 1024;
135659
+ var WECOM_MAX_CHUNKS = 100;
135660
+ var WECOM_MIN_UPLOAD_BYTES = 5;
135661
+ var FILENAME_MAX_BYTES = 256;
135662
+ var CHUNK_RETRY_BACKOFF_MS = [500, 1e3];
135663
+ var utf82 = new TextEncoder();
135664
+ var concurrencyFor = (chunks) => chunks <= 4 ? chunks : chunks <= 10 ? 3 : 2;
135665
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
135666
+ var clampName = (name) => {
135667
+ if (utf82.encode(name).byteLength <= FILENAME_MAX_BYTES) return name;
135668
+ const chars = [...name];
135669
+ while (chars.length > 1 && utf82.encode(chars.join("")).byteLength > FILENAME_MAX_BYTES) {
135670
+ chars.pop();
135671
+ }
135672
+ return chars.join("");
135673
+ };
135674
+ var uploadWecomMedia = async (input) => {
135675
+ const { bytes } = input;
135676
+ if (bytes.byteLength < WECOM_MIN_UPLOAD_BYTES) {
135677
+ throw new Error(`WeCom needs a file of at least ${String(WECOM_MIN_UPLOAD_BYTES)} bytes`);
135678
+ }
135679
+ const totalChunks = Math.ceil(bytes.byteLength / WECOM_CHUNK_BYTES);
135680
+ if (totalChunks > WECOM_MAX_CHUNKS) {
135681
+ throw new Error(
135682
+ `the file needs ${String(totalChunks)} chunks, WeCom allows ${String(WECOM_MAX_CHUNKS)}`
135683
+ );
135684
+ }
135685
+ const init = await input.request("aibot_upload_media_init", {
135686
+ type: input.type,
135687
+ filename: clampName(input.filename),
135688
+ total_size: bytes.byteLength,
135689
+ total_chunks: totalChunks,
135690
+ md5: NodeCrypto.createHash("md5").update(bytes).digest("hex")
135691
+ });
135692
+ const uploadId = init.upload_id;
135693
+ if (typeof uploadId !== "string" || uploadId === "") {
135694
+ throw new Error("WeCom upload init returned no upload_id");
135695
+ }
135696
+ const sendChunk = async (index) => {
135697
+ const part = bytes.subarray(index * WECOM_CHUNK_BYTES, (index + 1) * WECOM_CHUNK_BYTES);
135698
+ const body = {
135699
+ upload_id: uploadId,
135700
+ chunk_index: index,
135701
+ base64_data: Buffer.from(part).toString("base64")
135702
+ };
135703
+ for (let attempt = 0; ; attempt += 1) {
135704
+ try {
135705
+ await input.request("aibot_upload_media_chunk", body);
135706
+ return;
135707
+ } catch (error) {
135708
+ const backoff = CHUNK_RETRY_BACKOFF_MS[attempt];
135709
+ if (backoff === void 0) throw error;
135710
+ await sleep(backoff);
135711
+ }
135712
+ }
135713
+ };
135714
+ let next = 0;
135715
+ let failed = null;
135716
+ const worker = async () => {
135717
+ while (failed === null) {
135718
+ const index = next;
135719
+ next += 1;
135720
+ if (index >= totalChunks) return;
135721
+ try {
135722
+ await sendChunk(index);
135723
+ } catch (error) {
135724
+ failed ??= error;
135725
+ return;
135726
+ }
135727
+ }
135728
+ };
135729
+ await Promise.all(Array.from({ length: concurrencyFor(totalChunks) }, () => worker()));
135730
+ if (failed !== null) throw failed;
135731
+ const finish = await input.request("aibot_upload_media_finish", { upload_id: uploadId });
135732
+ const mediaId = finish.media_id;
135733
+ if (typeof mediaId !== "string" || mediaId === "") {
135734
+ throw new Error("WeCom upload finish returned no media_id");
135735
+ }
135736
+ return mediaId;
135737
+ };
135738
+
134140
135739
  // src/wecomApi.ts
134141
135740
  var DEFAULT_WECOM_ENDPOINT = "wss://openws.work.weixin.qq.com/wssvr/";
134142
135741
  var PING_INTERVAL_MS = 3e4;
134143
135742
  var MAX_MISSED_PONGS = 2;
134144
135743
  var SUBSCRIBE_TIMEOUT_MS = 1e4;
134145
135744
  var SEND_ACK_TIMEOUT_MS = 5e3;
135745
+ var UPLOAD_ACK_TIMEOUT_MS = 15e3;
134146
135746
  var WECOM_TEXT_MAX_CHARS = 4e3;
134147
135747
  var normalizeWecomEndpoint = (endpoint) => `${endpoint.replace(/\/+$/, "")}/`;
135748
+ var mediaResource = (payload, defaults) => typeof payload?.url === "string" && payload.url !== "" ? {
135749
+ url: payload.url,
135750
+ aesKey: typeof payload.aeskey === "string" && payload.aeskey !== "" ? payload.aeskey : void 0,
135751
+ ...defaults
135752
+ } : null;
134148
135753
  var parseWecomMessage = (frame, botId) => {
134149
135754
  const body = frame.body;
134150
135755
  if (body === void 0) return null;
134151
135756
  const messageId = body.msgid;
134152
135757
  const chatId = body.chatid ?? body.from?.userid;
134153
135758
  if (typeof messageId !== "string" || typeof chatId !== "string") return null;
134154
- if (body.msgtype !== "text") return null;
134155
- const raw = body.text?.content;
134156
- if (typeof raw !== "string") return null;
134157
135759
  const ownIds = [...new Set([body.aibotid, botId].filter((id) => Boolean(id)))];
135760
+ const stripOwn = (value) => ownIds.reduce((rest, id) => rest.replaceAll(`@${id}`, ""), value);
135761
+ let mentionSource = "";
135762
+ let text2 = "";
135763
+ const resources = [];
135764
+ switch (body.msgtype) {
135765
+ case "text": {
135766
+ const raw = body.text?.content;
135767
+ if (typeof raw !== "string") return null;
135768
+ mentionSource = raw;
135769
+ text2 = stripOwn(raw).trim();
135770
+ break;
135771
+ }
135772
+ case "voice": {
135773
+ const transcript = body.voice?.content;
135774
+ if (typeof transcript !== "string" || transcript.trim() === "") return null;
135775
+ mentionSource = transcript;
135776
+ text2 = stripOwn(transcript).trim();
135777
+ break;
135778
+ }
135779
+ case "image":
135780
+ case "file":
135781
+ case "video": {
135782
+ const resource = body.msgtype === "image" ? mediaResource(body.image, { name: "image.png", mimeType: "image/png", kind: "image" }) : body.msgtype === "file" ? mediaResource(body.file, {
135783
+ name: "file",
135784
+ mimeType: "application/octet-stream",
135785
+ kind: "file"
135786
+ }) : mediaResource(body.video, { name: "video.mp4", mimeType: "video/mp4", kind: "file" });
135787
+ if (resource === null) return null;
135788
+ resources.push(resource);
135789
+ break;
135790
+ }
135791
+ case "mixed": {
135792
+ const parts = [];
135793
+ for (const item of body.mixed?.msg_item ?? []) {
135794
+ if (item.msgtype === "text" && typeof item.text?.content === "string") {
135795
+ mentionSource += item.text.content;
135796
+ const part = stripOwn(item.text.content).trim();
135797
+ if (part !== "") parts.push(part);
135798
+ } else if (item.msgtype === "image") {
135799
+ const resource = mediaResource(item.image, {
135800
+ name: `image-${String(resources.length + 1)}.png`,
135801
+ mimeType: "image/png",
135802
+ kind: "image"
135803
+ });
135804
+ if (resource === null) continue;
135805
+ resources.push(resource);
135806
+ parts.push(`<image ${String(resources.length)}>`);
135807
+ }
135808
+ }
135809
+ if (parts.length === 0) return null;
135810
+ text2 = parts.join("\n");
135811
+ break;
135812
+ }
135813
+ default:
135814
+ return null;
135815
+ }
135816
+ const mentioned = ownIds.some((id) => mentionSource.includes(`@${id}`));
134158
135817
  const chatType = body.chattype ?? "single";
134159
- const mentioned = ownIds.some((id) => raw.includes(`@${id}`));
134160
- const text2 = ownIds.reduce((value, id) => value.replaceAll(`@${id}`, ""), raw).trim();
134161
135818
  return {
134162
135819
  messageId,
134163
135820
  chatId,
134164
135821
  senderUserId: body.from?.userid ?? null,
134165
135822
  text: text2,
135823
+ resources,
134166
135824
  requestId: frame.headers?.req_id ?? null,
134167
- chatType: mentioned ? "single" : chatType
135825
+ chatType: mentioned ? "single" : chatType,
135826
+ conversation: chatType === "group" ? "group" : "single"
134168
135827
  };
134169
135828
  };
134170
135829
  var splitForWecom = (text2, limit = WECOM_TEXT_MAX_CHARS) => splitMessage(text2, limit);
@@ -134173,6 +135832,7 @@ var runWecomClient = (input) => {
134173
135832
  let sequence = 0;
134174
135833
  const nextRequestId = (kind) => `${kind}_${++sequence}`;
134175
135834
  const pendingAcks = /* @__PURE__ */ new Map();
135835
+ const conversations = /* @__PURE__ */ new Map();
134176
135836
  const pendingRequests = /* @__PURE__ */ new Map();
134177
135837
  const openStreams = /* @__PURE__ */ new Map();
134178
135838
  const subscribeId = nextRequestId("aibot_subscribe");
@@ -134181,11 +135841,11 @@ var runWecomClient = (input) => {
134181
135841
  let finished = false;
134182
135842
  const ended = Promise.withResolvers();
134183
135843
  const send = (frame) => input.socket.send(JSON.stringify(frame));
134184
- const settleAck = (requestId, error) => {
135844
+ const settleAck = (requestId, error, frame) => {
134185
135845
  const pending = pendingAcks.get(requestId);
134186
135846
  if (pending === void 0) return;
134187
135847
  pendingAcks.delete(requestId);
134188
- if (error === null) pending.resolve();
135848
+ if (error === null) pending.resolve(frame);
134189
135849
  else pending.reject(error);
134190
135850
  };
134191
135851
  const handleFrame = (raw) => {
@@ -134206,7 +135866,7 @@ var runWecomClient = (input) => {
134206
135866
  const error = typeof frame.errcode === "number" && frame.errcode !== 0 ? new Error(`WeCom ack failed: ${String(frame.errcode)} ${frame.errmsg ?? ""}`) : null;
134207
135867
  const stream = openStreams.get(requestId);
134208
135868
  if (stream !== void 0 && error !== null) stream.failed = true;
134209
- settleAck(requestId, error);
135869
+ settleAck(requestId, error, frame);
134210
135870
  return;
134211
135871
  }
134212
135872
  if (frame.cmd !== "aibot_msg_callback") return;
@@ -134248,7 +135908,7 @@ var runWecomClient = (input) => {
134248
135908
  }, PING_INTERVAL_MS);
134249
135909
  void readLoop();
134250
135910
  const subscribed = new Promise((resolve, reject) => {
134251
- pendingAcks.set(subscribeId, { resolve, reject });
135911
+ pendingAcks.set(subscribeId, { resolve: () => resolve(), reject });
134252
135912
  setTimeout(() => {
134253
135913
  if (pendingAcks.delete(subscribeId)) reject(new Error("ETIMEDOUT: WeCom subscribe ack"));
134254
135914
  }, SUBSCRIBE_TIMEOUT_MS);
@@ -134285,7 +135945,7 @@ var runWecomClient = (input) => {
134285
135945
  }
134286
135946
  const requestId = nextRequestId("aibot_send_msg");
134287
135947
  const ack = new Promise((resolve, reject) => {
134288
- pendingAcks.set(requestId, { resolve, reject });
135948
+ pendingAcks.set(requestId, { resolve: () => resolve(), reject });
134289
135949
  setTimeout(() => {
134290
135950
  if (pendingAcks.delete(requestId)) resolve();
134291
135951
  }, SEND_ACK_TIMEOUT_MS);
@@ -134305,6 +135965,50 @@ var runWecomClient = (input) => {
134305
135965
  }
134306
135966
  }
134307
135967
  };
135968
+ const request = async (cmd, body) => {
135969
+ const requestId = nextRequestId(cmd);
135970
+ let timer;
135971
+ const ack = new Promise((resolve, reject) => {
135972
+ pendingAcks.set(requestId, { resolve, reject });
135973
+ timer = setTimeout(() => {
135974
+ if (pendingAcks.delete(requestId)) {
135975
+ reject(
135976
+ new Error(
135977
+ `WeCom did not acknowledge ${cmd} within ${String(UPLOAD_ACK_TIMEOUT_MS / 1e3)} s`
135978
+ )
135979
+ );
135980
+ }
135981
+ }, UPLOAD_ACK_TIMEOUT_MS);
135982
+ });
135983
+ try {
135984
+ const [, frame] = await Promise.all([
135985
+ send({ cmd, headers: { req_id: requestId }, body }),
135986
+ ack
135987
+ ]);
135988
+ return frame?.body ?? {};
135989
+ } catch (error) {
135990
+ pendingAcks.delete(requestId);
135991
+ throw error;
135992
+ } finally {
135993
+ clearTimeout(timer);
135994
+ }
135995
+ };
135996
+ const sendMedia = async (chatId, kind, media) => {
135997
+ const mediaId = await uploadWecomMedia({
135998
+ request,
135999
+ type: kind,
136000
+ filename: media.name,
136001
+ bytes: media.bytes
136002
+ });
136003
+ const conversation = conversations.get(chatId);
136004
+ await request("aibot_send_msg", {
136005
+ chatid: chatId,
136006
+ // 不知道(重启后还没收到过这个会话的消息)就不填:发文字也是只给 chatid。
136007
+ ...conversation === void 0 ? {} : { chat_type: conversation === "group" ? 2 : 1 },
136008
+ msgtype: kind,
136009
+ [kind]: { media_id: mediaId }
136010
+ });
136011
+ };
134308
136012
  const openStream = (chatId) => {
134309
136013
  const requestId = pendingRequests.get(chatId);
134310
136014
  if (requestId === void 0) return null;
@@ -134336,6 +136040,10 @@ var runWecomClient = (input) => {
134336
136040
  };
134337
136041
  return {
134338
136042
  sendText,
136043
+ sendMedia,
136044
+ rememberConversation: (chatId, conversation) => {
136045
+ conversations.set(chatId, conversation);
136046
+ },
134339
136047
  openStream,
134340
136048
  rememberRequest: (chatId, requestId) => {
134341
136049
  if (requestId !== null) pendingRequests.set(chatId, requestId);
@@ -134350,6 +136058,83 @@ var runWecomClient = (input) => {
134350
136058
  };
134351
136059
  };
134352
136060
 
136061
+ // src/wecomMedia.ts
136062
+ import * as NodeCrypto2 from "node:crypto";
136063
+ var PADDING_BLOCK = 32;
136064
+ var KEY_BYTES = 32;
136065
+ var IV_BYTES = 16;
136066
+ var AES_BLOCK = 16;
136067
+ var WecomDecryptError = class extends Error {
136068
+ constructor(reason) {
136069
+ super(`wecom media could not be decrypted: ${reason}`);
136070
+ this.name = "WecomDecryptError";
136071
+ }
136072
+ };
136073
+ var decryptWecomMedia = (cipher, aesKey) => {
136074
+ const key = Buffer.from(aesKey, "base64");
136075
+ if (key.byteLength !== KEY_BYTES) throw new WecomDecryptError("the key is not 32 bytes");
136076
+ if (cipher.byteLength === 0 || cipher.byteLength % AES_BLOCK !== 0) {
136077
+ throw new WecomDecryptError("the ciphertext is not a whole number of blocks");
136078
+ }
136079
+ const decipher = NodeCrypto2.createDecipheriv("aes-256-cbc", key, key.subarray(0, IV_BYTES));
136080
+ decipher.setAutoPadding(false);
136081
+ const plain = Buffer.concat([decipher.update(cipher), decipher.final()]);
136082
+ const pad = plain[plain.byteLength - 1] ?? 0;
136083
+ if (pad < 1 || pad > PADDING_BLOCK || pad > plain.byteLength) {
136084
+ throw new WecomDecryptError("the padding length is out of range");
136085
+ }
136086
+ for (let index = plain.byteLength - pad; index < plain.byteLength; index += 1) {
136087
+ if (plain[index] !== pad) throw new WecomDecryptError("the padding bytes are not all equal");
136088
+ }
136089
+ return new Uint8Array(plain.subarray(0, plain.byteLength - pad));
136090
+ };
136091
+ var decodeQuietly = (value) => {
136092
+ try {
136093
+ return decodeURIComponent(value);
136094
+ } catch {
136095
+ return value;
136096
+ }
136097
+ };
136098
+ var filenameFromContentDisposition = (header) => {
136099
+ if (header === null) return void 0;
136100
+ const extended = /filename\*\s*=\s*(?:UTF-8|utf-8)''([^;]+)/i.exec(header);
136101
+ if (extended?.[1] !== void 0) {
136102
+ const name = decodeQuietly(extended[1].trim());
136103
+ if (name !== "") return name;
136104
+ }
136105
+ const quoted = /filename\s*=\s*"([^"]*)"/i.exec(header);
136106
+ const plain = quoted === null ? /filename\s*=\s*([^;]+)/i.exec(header) : null;
136107
+ const raw = quoted?.[1] ?? plain?.[1]?.trim();
136108
+ if (raw === void 0 || raw === "") return void 0;
136109
+ return decodeQuietly(raw);
136110
+ };
136111
+ var fetchWecomMedia = async (input) => {
136112
+ const response = await input.http(input.url);
136113
+ if (!response.ok) {
136114
+ await response.body?.cancel().catch(() => void 0);
136115
+ throw new Error(`wecom media fetch failed: HTTP ${String(response.status)}`);
136116
+ }
136117
+ const name = filenameFromContentDisposition(response.headers.get("content-disposition"));
136118
+ const encrypted = input.aesKey !== void 0 && input.aesKey !== "";
136119
+ let body;
136120
+ try {
136121
+ body = await readBodyLimited(
136122
+ response,
136123
+ encrypted ? input.maxBytes + PADDING_BLOCK : input.maxBytes
136124
+ );
136125
+ } catch (error) {
136126
+ if (error instanceof ImResourceTooLargeError) throw new ImResourceTooLargeError(input.maxBytes);
136127
+ throw error;
136128
+ }
136129
+ if (!encrypted) {
136130
+ input.onUnencrypted?.();
136131
+ return { bytes: body, name };
136132
+ }
136133
+ const bytes = decryptWecomMedia(body, input.aesKey ?? "");
136134
+ if (bytes.byteLength > input.maxBytes) throw new ImResourceTooLargeError(input.maxBytes);
136135
+ return { bytes, name };
136136
+ };
136137
+
134353
136138
  // src/wecomAdapter.ts
134354
136139
  var WECOM_CARD_THROTTLE_MS = 400;
134355
136140
  var LIVE_STREAM_LIMIT = 200;
@@ -134403,6 +136188,12 @@ ${kept}`;
134403
136188
  await entry.client.sendText(entry.chatId, chunk);
134404
136189
  }
134405
136190
  };
136191
+ const sendMedia = async (binding, kind, media) => {
136192
+ const client = clients.get(binding.rowId);
136193
+ if (client === void 0) throw new Error("WeCom is not connected right now");
136194
+ await client.sendMedia(binding.chatId, kind, media);
136195
+ deps.log.info("wecom delivered a file", { name: media.name, as: kind });
136196
+ };
134406
136197
  return {
134407
136198
  platform: "wecom",
134408
136199
  icon: "wecom",
@@ -134436,13 +136227,19 @@ ${kept}`;
134436
136227
  onMessage: (message) => {
134437
136228
  if (message.chatType !== "single") return;
134438
136229
  client.rememberRequest(message.chatId, message.requestId);
136230
+ client.rememberConversation(message.chatId, message.conversation);
134439
136231
  handlers.onMessage({
134440
136232
  messageId: message.messageId,
134441
136233
  chatId: message.chatId,
134442
136234
  senderId: message.senderUserId,
134443
136235
  text: message.text,
134444
- // 企微这一版不收附件,先不假装支持。
134445
- resource: null
136236
+ // 下载地址与密钥打成一个键:核心只管把它原样交回来。
136237
+ resources: message.resources.map((resource) => ({
136238
+ key: JSON.stringify({ url: resource.url, aesKey: resource.aesKey ?? null }),
136239
+ name: resource.name,
136240
+ mimeType: resource.mimeType,
136241
+ kind: resource.kind
136242
+ }))
134446
136243
  });
134447
136244
  }
134448
136245
  }
@@ -134520,9 +136317,33 @@ ${kept}`;
134520
136317
  if (view.status !== "working") liveStreams.delete(key);
134521
136318
  }
134522
136319
  },
136320
+ sendImage: (binding, image2) => sendMedia(binding, "image", image2),
136321
+ sendFile: (binding, file) => sendMedia(binding, "file", file),
136322
+ // 图 10 MB(只认 png / jpg / jpeg / gif)、文件 20 MB,来自企微官方文档「智能机器人长连接」。
136323
+ outboundLimits: { imageBytes: 10 * 1024 * 1024, fileBytes: 20 * 1024 * 1024, minBytes: 5 },
136324
+ imageMimeTypes: /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif"]),
134523
136325
  answerByText: (_binding, text2, options) => {
134524
136326
  const index = optionIndexFrom(text2, options.length);
134525
136327
  return index === null ? null : options[index - 1]?.token ?? null;
136328
+ },
136329
+ fetchResource: async (binding, input) => {
136330
+ const parsed = JSON.parse(input.resource.key);
136331
+ const media = await fetchWecomMedia({
136332
+ http: deps.http,
136333
+ url: parsed.url,
136334
+ aesKey: parsed.aesKey ?? void 0,
136335
+ maxBytes: input.maxBytes,
136336
+ onUnencrypted: () => {
136337
+ deps.log.info("wecom media arrived without a key, used as is", {
136338
+ group: binding.displayName
136339
+ });
136340
+ }
136341
+ });
136342
+ return {
136343
+ bytes: media.bytes,
136344
+ name: media.name,
136345
+ mimeType: media.name === void 0 ? void 0 : mimeFromName(media.name)
136346
+ };
134526
136347
  }
134527
136348
  };
134528
136349
  };
@@ -134536,6 +136357,7 @@ var TITLE_MAX_CHARS = 72;
134536
136357
  var DEFAULT_PLATFORM = "lark";
134537
136358
  var SESSION_SCAN_LIMIT = 50;
134538
136359
  var ARCHIVED_REPLY_COOLDOWN_MS = 10 * 6e4;
136360
+ var UNSUPPORTED_REPLY_COOLDOWN_MS = 6e4;
134539
136361
  var INBOUND_RUNTIME_MODES = /* @__PURE__ */ new Set([
134540
136362
  "approval-required",
134541
136363
  "auto-accept-edits",
@@ -134548,7 +136370,7 @@ var readString = (row, key) => {
134548
136370
  const value = row[key];
134549
136371
  return typeof value === "string" ? value.trim() : "";
134550
136372
  };
134551
- var describeError = (error) => error instanceof Error ? error.message : String(error);
136373
+ var describeError2 = (error) => error instanceof Error ? error.message : String(error);
134552
136374
  var titleFrom = (text2) => text2.replace(/\s+/g, " ").slice(0, TITLE_MAX_CHARS) || "Chat thread";
134553
136375
  var imPlugin = definePlugin({
134554
136376
  id: "im",
@@ -134582,6 +136404,7 @@ var imPlugin = definePlugin({
134582
136404
  makeWecomAdapter({
134583
136405
  // 企微不需要厂商 SDK:全程 JSON over WebSocket,走插件面那条出网长连接。
134584
136406
  connectSocket: (url) => ctx.socket.connect({ url }),
136407
+ http: ctx.http,
134585
136408
  translate: t,
134586
136409
  log: {
134587
136410
  info: (message, fields) => ctx.log.info(message, fields),
@@ -134753,7 +136576,7 @@ var imPlugin = definePlugin({
134753
136576
  } catch (error) {
134754
136577
  ctx.log.warn("chat could not deliver an image", {
134755
136578
  name: attachment.name,
134756
- error: describeError(error)
136579
+ error: describeError2(error)
134757
136580
  });
134758
136581
  }
134759
136582
  }
@@ -134764,7 +136587,7 @@ var imPlugin = definePlugin({
134764
136587
  try {
134765
136588
  await adapterFor(binding.platform)?.sendText(binding, text2);
134766
136589
  } catch (error) {
134767
- ctx.log.warn("chat could not deliver", { error: describeError(error) });
136590
+ ctx.log.warn("chat could not deliver", { error: describeError2(error) });
134768
136591
  }
134769
136592
  };
134770
136593
  const projectByThread = /* @__PURE__ */ new Map();
@@ -134798,7 +136621,7 @@ var imPlugin = definePlugin({
134798
136621
  acknowledgements.set(threadId, pending);
134799
136622
  ctx.log.info("chat acknowledged a message", { threadId, messageId });
134800
136623
  } catch (error) {
134801
- ctx.log.warn("chat could not acknowledge the message", { error: describeError(error) });
136624
+ ctx.log.warn("chat could not acknowledge the message", { error: describeError2(error) });
134802
136625
  }
134803
136626
  };
134804
136627
  const clearWorking = async (binding, threadId) => {
@@ -134811,7 +136634,7 @@ var imPlugin = definePlugin({
134811
136634
  await adapter.clearAcknowledge(binding, entry.messageId, entry.ackId);
134812
136635
  } catch (error) {
134813
136636
  ctx.log.warn("chat could not clear the acknowledgement", {
134814
- error: describeError(error)
136637
+ error: describeError2(error)
134815
136638
  });
134816
136639
  }
134817
136640
  }
@@ -134898,7 +136721,7 @@ ${final.text}`;
134898
136721
  state = restored;
134899
136722
  },
134900
136723
  (error) => {
134901
- ctx.log.warn("chat could not restore its saved state", { error: describeError(error) });
136724
+ ctx.log.warn("chat could not restore its saved state", { error: describeError2(error) });
134902
136725
  }
134903
136726
  );
134904
136727
  let persisting = Promise.resolve();
@@ -134907,7 +136730,7 @@ ${final.text}`;
134907
136730
  persisting = persisting.then(() => ctx.events.append({ name, aggregate: { kind: "plugin", id: ctx.id }, data })).then(
134908
136731
  () => void 0,
134909
136732
  (error) => {
134910
- ctx.log.warn("chat could not save its state", { name, error: describeError(error) });
136733
+ ctx.log.warn("chat could not save its state", { name, error: describeError2(error) });
134911
136734
  }
134912
136735
  );
134913
136736
  };
@@ -134915,7 +136738,7 @@ ${final.text}`;
134915
136738
  commit("chat-updated", { chatKey, threadId });
134916
136739
  };
134917
136740
  const outbound = makeKeyedQueue((error) => {
134918
- ctx.log.warn("chat could not deliver a card", { error: describeError(error) });
136741
+ ctx.log.warn("chat could not deliver a card", { error: describeError2(error) });
134919
136742
  });
134920
136743
  const settleApprovalCard = async (approval, settlement) => {
134921
136744
  if (approval.cardId === void 0) return;
@@ -134925,7 +136748,7 @@ ${final.text}`;
134925
136748
  try {
134926
136749
  await settle(binding, approval.cardId, settlement);
134927
136750
  } catch (error) {
134928
- ctx.log.warn("chat could not update the approval card", { error: describeError(error) });
136751
+ ctx.log.warn("chat could not update the approval card", { error: describeError2(error) });
134929
136752
  }
134930
136753
  };
134931
136754
  const answerApproval = async (approval, decision, label) => {
@@ -134977,12 +136800,12 @@ ${final.text}`;
134977
136800
  },
134978
136801
  endOf: async (threadId) => {
134979
136802
  const stopped = stopRequested.delete(threadId);
134980
- const failure = failureSummaries.get(threadId);
136803
+ const failure2 = failureSummaries.get(threadId);
134981
136804
  failureSummaries.delete(threadId);
134982
136805
  const thread = await ctx.projections.getThread(threadId).catch(() => null);
134983
136806
  const session = thread?.session;
134984
136807
  if (session?.status === "error") {
134985
- return { status: "error", errorText: session.lastError ?? failure ?? null };
136808
+ return { status: "error", errorText: session.lastError ?? failure2 ?? null };
134986
136809
  }
134987
136810
  if (stopped || session?.status === "interrupted" || session?.status === "stopped") {
134988
136811
  return { status: "interrupted", errorText: null };
@@ -135002,7 +136825,7 @@ ${final.text}`;
135002
136825
  return () => clearTimeout(timer);
135003
136826
  },
135004
136827
  warn: (message, error) => {
135005
- ctx.log.warn(`chat ${message}`, { error: describeError(error) });
136828
+ ctx.log.warn(`chat ${message}`, { error: describeError2(error) });
135006
136829
  }
135007
136830
  });
135008
136831
  ctx.events.on(TURN_PROGRESS_TRIGGER_KIND, (event) => {
@@ -135028,7 +136851,7 @@ ${final.text}`;
135028
136851
  );
135029
136852
  } catch (error) {
135030
136853
  ctx.log.warn("chat could not mark a stale card as interrupted", {
135031
- error: describeError(error)
136854
+ error: describeError2(error)
135032
136855
  });
135033
136856
  }
135034
136857
  }
@@ -135049,7 +136872,7 @@ ${final.text}`;
135049
136872
  const record = state.approvals[offer.requestId];
135050
136873
  if (record?.resolved === true) await settleApprovalCard(record, { kind: "elsewhere" });
135051
136874
  } catch (error) {
135052
- ctx.log.warn("chat could not ask for the approval", { error: describeError(error) });
136875
+ ctx.log.warn("chat could not ask for the approval", { error: describeError2(error) });
135053
136876
  }
135054
136877
  };
135055
136878
  ctx.events.on("thread.approval-pending", async (event) => {
@@ -135097,13 +136920,116 @@ ${final.text}`;
135097
136920
  });
135098
136921
  ctx.events.on(REQUEST_RESOLVED_TRIGGER_KIND, async (event) => {
135099
136922
  const resolved = readRequestResolved(event);
135100
- if (resolved === null || resolved.requestKind !== "approval") return;
136923
+ if (resolved === null) return;
135101
136924
  await ready;
136925
+ if (resolved.requestKind === "user-input") {
136926
+ questionFlow.resolved(
136927
+ resolved.requestId,
136928
+ resolved.outcome === "cancelled" ? "cancelled" : "answered"
136929
+ );
136930
+ return;
136931
+ }
135102
136932
  const record = state.approvals[resolved.requestId];
135103
136933
  if (record === void 0 || record.resolved) return;
135104
136934
  commit("approval-resolved", { requestId: resolved.requestId });
135105
136935
  outbound(record.chatKey, () => settleApprovalCard(record, { kind: "elsewhere" }));
135106
136936
  });
136937
+ const bindingOfThread = (threadId) => readBindings().find((candidate) => chatOf(state, chatKeyOf(candidate)).threadId === threadId);
136938
+ const flowHost = {
136939
+ t,
136940
+ state: () => state,
136941
+ commit,
136942
+ // 改卡必须排在回调应答之后:按钮点击的回调要先同步应答,飞书在应答不带改卡时会把卡
136943
+ // 恢复成点击前的样子;所以这两条流的发卡与改卡都推迟一个宏任务再入群队列。
136944
+ outbound: (chatKey, job) => {
136945
+ setTimeout(() => outbound(chatKey, job), 0);
136946
+ },
136947
+ target: (chatKey) => {
136948
+ const binding = bindingOfChat(chatKey);
136949
+ const adapter = binding === void 0 ? void 0 : adapterFor(binding.platform);
136950
+ return binding === void 0 || adapter === void 0 ? void 0 : { binding, adapter };
136951
+ },
136952
+ allowed: (chatKey, operatorId) => {
136953
+ const binding = bindingOfChat(chatKey);
136954
+ if (binding !== void 0 && isAllowedSender(binding, operatorId)) return true;
136955
+ ctx.log.warn("chat ignored a button click from someone not on the allow list", {
136956
+ group: binding?.displayName ?? chatKey,
136957
+ operator: operatorId
136958
+ });
136959
+ return false;
136960
+ },
136961
+ dispatch: (command) => ctx.commands.dispatch(command),
136962
+ say: async (chatKey, text2) => {
136963
+ const binding = bindingOfChat(chatKey);
136964
+ if (binding !== void 0) await deliver(binding, text2);
136965
+ },
136966
+ newToken: () => crypto.randomUUID().slice(0, 8),
136967
+ newId: () => crypto.randomUUID(),
136968
+ now: () => (/* @__PURE__ */ new Date()).toISOString(),
136969
+ warn: (message, error) => {
136970
+ ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError2(error) });
136971
+ }
136972
+ };
136973
+ const questionFlow = makeQuestionFlow(flowHost);
136974
+ const queueFlow = makeQueueFlow(flowHost, {
136975
+ readExecution: async (threadId) => {
136976
+ const execution = await ctx.projections.getThreadExecution(threadId);
136977
+ if (execution === null) return null;
136978
+ return {
136979
+ activeTurnId: execution.session?.activeTurnId ?? null,
136980
+ queuePaused: execution.queuePaused,
136981
+ intents: execution.turnIntents.map((intent) => ({
136982
+ intentId: intent.intentId,
136983
+ messageId: intent.messageId,
136984
+ status: intent.status,
136985
+ sequence: intent.sequence
136986
+ }))
136987
+ };
136988
+ }
136989
+ });
136990
+ ctx.events.on(USER_INPUT_PENDING_TRIGGER_KIND, async (event) => {
136991
+ const threadId = event.threadId;
136992
+ if (threadId === null) return;
136993
+ await ready;
136994
+ const binding = bindingOfThread(threadId);
136995
+ if (binding === void 0) return;
136996
+ if (ctx.projections.listPendingUserInputs === void 0) {
136997
+ ctx.log.warn("chat cannot show questions: the host cannot list them");
136998
+ return;
136999
+ }
137000
+ try {
137001
+ questionFlow.offer(
137002
+ chatKeyOf(binding),
137003
+ threadId,
137004
+ await ctx.projections.listPendingUserInputs(threadId)
137005
+ );
137006
+ } catch (error) {
137007
+ ctx.log.warn("chat could not read the pending questions", { error: describeError2(error) });
137008
+ }
137009
+ });
137010
+ ctx.reactor({
137011
+ name: "im.queue-cards",
137012
+ events: [
137013
+ "thread.session-set",
137014
+ "thread.turn-intent-transitioned",
137015
+ "thread.turn-queue-updated",
137016
+ "thread.turn-queue-paused"
137017
+ ],
137018
+ handle: async (event) => {
137019
+ const payload = event.payload;
137020
+ if (typeof payload.threadId !== "string") return;
137021
+ await ready;
137022
+ const released = event.type === "thread.turn-intent-transitioned" && payload.status === "queued" && (payload.fromStatus !== "queued" || typeof payload.blockedReason === "string") && typeof payload.intentId === "string" ? payload.intentId : null;
137023
+ queueFlow.refresh(payload.threadId, released);
137024
+ }
137025
+ });
137026
+ const handleButtonClick = (token, operatorId) => {
137027
+ const question = questionFlow.click(token, operatorId);
137028
+ if (question.handled) return question.notice;
137029
+ const queued = queueFlow.click(token, operatorId);
137030
+ if (queued.handled) return queued.notice;
137031
+ return handleApprovalClick(token, operatorId);
137032
+ };
135107
137033
  const isAllowedSender = (binding, sender) => binding.allowedSenders.size === 0 || sender !== null && binding.allowedSenders.has(sender);
135108
137034
  const handleApprovalClick = (token, operatorId) => {
135109
137035
  const found = findApprovalByToken(state, token);
@@ -135121,12 +137047,13 @@ ${final.text}`;
135121
137047
  }
135122
137048
  void answerApproval(found.approval, found.button.decision, found.button.label).catch(
135123
137049
  (error) => {
135124
- ctx.log.warn("chat could not answer the approval", { error: describeError(error) });
137050
+ ctx.log.warn("chat could not answer the approval", { error: describeError2(error) });
135125
137051
  }
135126
137052
  );
135127
137053
  return null;
135128
137054
  };
135129
137055
  const archivedReplyCooldown = makeCooldown(ARCHIVED_REPLY_COOLDOWN_MS, () => Date.now());
137056
+ const unsupportedReplyCooldown = makeCooldown(UNSUPPORTED_REPLY_COOLDOWN_MS, () => Date.now());
135130
137057
  const alreadyHandled = (target, messageId) => {
135131
137058
  const key = `${chatKeyOf(target)}:${messageId}`;
135132
137059
  if (seen.has(key)) return true;
@@ -135138,33 +137065,103 @@ ${final.text}`;
135138
137065
  }
135139
137066
  return false;
135140
137067
  };
135141
- const collectAttachment = async (binding, threadId, message) => {
135142
- const resource = message.resource;
137068
+ const pendingByChat = /* @__PURE__ */ new Map();
137069
+ const pendingFor = (chatKey, threadId) => {
137070
+ const existing = pendingByChat.get(chatKey);
137071
+ if (existing !== void 0 && existing.threadId === threadId) return existing;
137072
+ const fresh = { threadId, attachments: [], notes: [] };
137073
+ pendingByChat.set(chatKey, fresh);
137074
+ return fresh;
137075
+ };
137076
+ const failureForUser = (failure2) => failure2.kind === "tooLarge" ? t("reply.attachmentReason.tooLarge", { limit: formatMegabytes(failure2.limitBytes) }) : t(`reply.attachmentReason.${failure2.kind}`);
137077
+ const failureForAgent = (failure2) => failure2.kind === "tooLarge" ? `it is over the ${formatMegabytes(failure2.limitBytes)} limit` : failure2.kind === "downloadFailed" ? "the download failed" : failure2.kind === "storeFailed" ? "it could not be saved" : "it is empty";
137078
+ const collectResources = async (binding, threadId, messageId, resources, room) => {
135143
137079
  const adapter = adapterFor(binding.platform);
135144
- if (resource === null || adapter?.fetchResource === void 0) return [];
135145
- try {
135146
- const bytes = await adapter.fetchResource(binding, {
135147
- messageId: message.messageId,
135148
- resource
135149
- });
135150
- const stored = await ctx.attachments.store({
135151
- threadId,
135152
- name: resource.name,
135153
- mimeType: resource.mimeType,
135154
- bytes
135155
- });
135156
- ctx.log.info("chat stored an inbound attachment", {
135157
- name: stored.name,
135158
- sizeBytes: stored.sizeBytes
135159
- });
135160
- return [stored];
135161
- } catch (error) {
135162
- ctx.log.warn("chat dropped an inbound attachment", {
135163
- name: resource.name,
135164
- error: describeError(error)
135165
- });
135166
- return [];
137080
+ const attachments = [];
137081
+ const notes = [];
137082
+ const reject = async (name, failure2) => {
137083
+ notes.push(
137084
+ `[the user also sent "${name}", which could not be taken: ${failureForAgent(failure2)}]`
137085
+ );
137086
+ await deliver(
137087
+ binding,
137088
+ t("reply.attachmentFailed", { name, reason: failureForUser(failure2) })
137089
+ );
137090
+ };
137091
+ for (const resource of resources) {
137092
+ const name = sanitizeAttachmentName(resource.name, "file");
137093
+ if (attachments.length >= room) {
137094
+ notes.push(`[the user also sent "${name}", which was left out: too many attachments]`);
137095
+ await deliver(
137096
+ binding,
137097
+ t("reply.attachmentLimit", { max: MAX_ATTACHMENTS_PER_TURN, name })
137098
+ );
137099
+ continue;
137100
+ }
137101
+ const limitBytes = Math.min(
137102
+ limitForKind(resource.kind),
137103
+ adapter?.downloadLimitBytes ?? Number.POSITIVE_INFINITY
137104
+ );
137105
+ if (resource.sizeBytes !== void 0 && resource.sizeBytes > limitBytes) {
137106
+ await reject(name, { kind: "tooLarge", limitBytes });
137107
+ continue;
137108
+ }
137109
+ if (adapter?.fetchResource === void 0) {
137110
+ await reject(name, { kind: "downloadFailed" });
137111
+ continue;
137112
+ }
137113
+ let fetched;
137114
+ try {
137115
+ fetched = await adapter.fetchResource(binding, {
137116
+ messageId,
137117
+ resource,
137118
+ maxBytes: limitBytes
137119
+ });
137120
+ } catch (error) {
137121
+ ctx.log.warn("chat dropped an inbound attachment", {
137122
+ name,
137123
+ error: describeError2(error)
137124
+ });
137125
+ await reject(
137126
+ name,
137127
+ error instanceof ImResourceTooLargeError ? { kind: "tooLarge", limitBytes: error.limitBytes } : { kind: "downloadFailed" }
137128
+ );
137129
+ continue;
137130
+ }
137131
+ const bytes = fetched instanceof Uint8Array ? fetched : fetched.bytes;
137132
+ const knownName = fetched instanceof Uint8Array ? void 0 : fetched.name;
137133
+ const finalName = knownName === void 0 ? name : sanitizeAttachmentName(knownName, name);
137134
+ const claimedMime = (fetched instanceof Uint8Array ? void 0 : fetched.mimeType) ?? resource.mimeType;
137135
+ if (bytes.byteLength === 0) {
137136
+ await reject(finalName, { kind: "empty" });
137137
+ continue;
137138
+ }
137139
+ const mimeType = resolveMime(claimedMime, bytes);
137140
+ if (bytes.byteLength > limitForMime(mimeType)) {
137141
+ await reject(finalName, { kind: "tooLarge", limitBytes: limitForMime(mimeType) });
137142
+ continue;
137143
+ }
137144
+ try {
137145
+ const stored = await ctx.attachments.store({
137146
+ threadId,
137147
+ name: withImageExtension(finalName, mimeType),
137148
+ mimeType,
137149
+ bytes
137150
+ });
137151
+ ctx.log.info("chat stored an inbound attachment", {
137152
+ name: stored.name,
137153
+ sizeBytes: stored.sizeBytes
137154
+ });
137155
+ attachments.push(stored);
137156
+ } catch (error) {
137157
+ ctx.log.warn("chat dropped an inbound attachment", {
137158
+ name: finalName,
137159
+ error: describeError2(error)
137160
+ });
137161
+ await reject(finalName, { kind: "storeFailed" });
137162
+ }
135167
137163
  }
137164
+ return { attachments, notes };
135168
137165
  };
135169
137166
  const targetOf = (binding) => chatOf(state, chatKeyOf(binding)).target;
135170
137167
  const chatByRun = /* @__PURE__ */ new Map();
@@ -135282,7 +137279,7 @@ ${final.text}`;
135282
137279
  modelSelection: selection
135283
137280
  });
135284
137281
  } catch (error) {
135285
- ctx.log.warn("chat could not switch the thread model", { error: describeError(error) });
137282
+ ctx.log.warn("chat could not switch the thread model", { error: describeError2(error) });
135286
137283
  await deliver(binding, t("model.setFailed"));
135287
137284
  return;
135288
137285
  }
@@ -135362,7 +137359,7 @@ ${final.text}`;
135362
137359
  return true;
135363
137360
  } catch (error) {
135364
137361
  ctx.log.warn("chat could not open the command panel", {
135365
- error: describeError(error)
137362
+ error: describeError2(error)
135366
137363
  });
135367
137364
  }
135368
137365
  }
@@ -135409,6 +137406,37 @@ ${final.text}`;
135409
137406
  );
135410
137407
  return true;
135411
137408
  }
137409
+ case "continue": {
137410
+ const threadId = chatOf(state, chatKey).threadId ?? void 0;
137411
+ let paused = false;
137412
+ if (threadId !== void 0) {
137413
+ try {
137414
+ paused = (await ctx.projections.getThreadExecution(threadId))?.queuePaused === true;
137415
+ } catch (error) {
137416
+ ctx.log.warn("chat could not read the turn queue", { error: describeError2(error) });
137417
+ await deliver(binding, t("command.continueFailed"));
137418
+ return true;
137419
+ }
137420
+ }
137421
+ if (threadId === void 0 || !paused) {
137422
+ await deliver(binding, t("command.continueNothing"));
137423
+ return true;
137424
+ }
137425
+ try {
137426
+ await ctx.commands.dispatch({
137427
+ type: "thread.turn-queue.resume",
137428
+ commandId: crypto.randomUUID(),
137429
+ threadId,
137430
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
137431
+ });
137432
+ } catch (error) {
137433
+ ctx.log.warn("chat could not resume the turn queue", { error: describeError2(error) });
137434
+ await deliver(binding, t("command.continueFailed"));
137435
+ return true;
137436
+ }
137437
+ await deliver(binding, t("command.continued"));
137438
+ return true;
137439
+ }
135412
137440
  case "stop": {
135413
137441
  const threadId = chatOf(state, chatKey).threadId ?? void 0;
135414
137442
  if (threadId === void 0) {
@@ -135465,9 +137493,8 @@ ${final.text}`;
135465
137493
  }
135466
137494
  };
135467
137495
  const interactionModeOf = (chatKey) => chatOf(state, chatKey).plan ? "plan" : "default";
135468
- const startTurn = async (binding, message) => {
137496
+ const ensureThread = async (binding, titleSeed) => {
135469
137497
  const chatKey = chatKeyOf(binding);
135470
- const text2 = message.text;
135471
137498
  let threadId = chatOf(state, chatKey).threadId ?? void 0;
135472
137499
  if (threadId !== void 0 && !await threadStillUsable(binding, threadId)) {
135473
137500
  ctx.log.info("chat dropped a saved thread that is no longer usable", {
@@ -135478,70 +137505,136 @@ ${final.text}`;
135478
137505
  threadId = void 0;
135479
137506
  await deliver(binding, t("reply.threadGone"));
135480
137507
  }
135481
- if (threadId === void 0) {
135482
- const project = await ctx.projections.getProject(binding.projectId);
135483
- if (project === null) {
135484
- ctx.log.warn("chat cannot start a thread: the configured project is gone", {
135485
- group: binding.displayName,
135486
- projectId: binding.projectId
135487
- });
135488
- return null;
135489
- }
135490
- const modelSelection = chatOf(state, chatKey).model ?? project.defaultModelSelection;
135491
- if (modelSelection === null) {
135492
- ctx.log.warn("chat cannot start a thread: the project has no default model", {
135493
- group: binding.displayName,
135494
- projectId: binding.projectId
135495
- });
135496
- await deliver(binding, t("reply.noDefaultModel", { title: project.title }));
135497
- return null;
135498
- }
135499
- const createdThreadId = crypto.randomUUID();
135500
- threadId = createdThreadId;
135501
- await ctx.commands.dispatch({
135502
- type: "thread.create",
135503
- commandId: crypto.randomUUID(),
135504
- threadId,
135505
- projectId: binding.projectId,
135506
- title: titleFrom(text2 || message.resource?.name || "Chat"),
135507
- modelSelection,
135508
- runtimeMode: binding.runtimeMode,
135509
- interactionMode: interactionModeOf(chatKey),
135510
- branch: null,
135511
- worktreePath: null,
135512
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
137508
+ if (threadId !== void 0) return threadId;
137509
+ const project = await ctx.projections.getProject(binding.projectId);
137510
+ if (project === null) {
137511
+ ctx.log.warn("chat cannot start a thread: the configured project is gone", {
137512
+ group: binding.displayName,
137513
+ projectId: binding.projectId
135513
137514
  });
135514
- commit("thread-opened", {
135515
- chatKey,
135516
- threadId: createdThreadId,
135517
- projectId: binding.projectId,
135518
- openedAt: (/* @__PURE__ */ new Date()).toISOString()
137515
+ return null;
137516
+ }
137517
+ const modelSelection = chatOf(state, chatKey).model ?? project.defaultModelSelection;
137518
+ if (modelSelection === null) {
137519
+ ctx.log.warn("chat cannot start a thread: the project has no default model", {
137520
+ group: binding.displayName,
137521
+ projectId: binding.projectId
135519
137522
  });
135520
- projectByThread.set(createdThreadId, binding.projectId);
137523
+ await deliver(binding, t("reply.noDefaultModel", { title: project.title }));
137524
+ return null;
135521
137525
  }
135522
- const attachments = await collectAttachment(binding, threadId, message);
137526
+ const createdThreadId = crypto.randomUUID();
135523
137527
  await ctx.commands.dispatch({
135524
- type: "thread.turn.start",
137528
+ type: "thread.create",
135525
137529
  commandId: crypto.randomUUID(),
135526
- threadId,
135527
- message: {
135528
- messageId: crypto.randomUUID(),
135529
- role: "user",
135530
- text: text2,
135531
- attachments
135532
- },
135533
- titleSeed: titleFrom(text2 || attachments[0]?.name || "Chat"),
137530
+ threadId: createdThreadId,
137531
+ projectId: binding.projectId,
137532
+ title: titleFrom(titleSeed),
137533
+ modelSelection,
135534
137534
  runtimeMode: binding.runtimeMode,
135535
137535
  interactionMode: interactionModeOf(chatKey),
137536
+ branch: null,
137537
+ worktreePath: null,
135536
137538
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
135537
137539
  });
137540
+ commit("thread-opened", {
137541
+ chatKey,
137542
+ threadId: createdThreadId,
137543
+ projectId: binding.projectId,
137544
+ openedAt: (/* @__PURE__ */ new Date()).toISOString()
137545
+ });
137546
+ projectByThread.set(createdThreadId, binding.projectId);
137547
+ return createdThreadId;
137548
+ };
137549
+ const startTurn = async (binding, message) => {
137550
+ const chatKey = chatKeyOf(binding);
137551
+ const text2 = message.text;
137552
+ const threadId = await ensureThread(binding, text2 || message.resources[0]?.name || "Chat");
137553
+ if (threadId === null) return null;
137554
+ const waiting = pendingByChat.get(chatKey);
137555
+ pendingByChat.delete(chatKey);
137556
+ const carried = waiting?.threadId === threadId ? waiting : void 0;
137557
+ const own = await collectResources(
137558
+ binding,
137559
+ threadId,
137560
+ message.messageId,
137561
+ message.resources,
137562
+ MAX_ATTACHMENTS_PER_TURN - (carried?.attachments.length ?? 0)
137563
+ );
137564
+ const attachments = [...carried?.attachments ?? [], ...own.attachments];
137565
+ const notes = [...carried?.notes ?? [], ...own.notes];
137566
+ const userMessageId = crypto.randomUUID();
137567
+ try {
137568
+ await ctx.commands.dispatch({
137569
+ type: "thread.turn.start",
137570
+ commandId: crypto.randomUUID(),
137571
+ threadId,
137572
+ message: {
137573
+ messageId: userMessageId,
137574
+ role: "user",
137575
+ text: notes.length === 0 ? text2 : [text2, ...notes].join("\n"),
137576
+ attachments
137577
+ },
137578
+ titleSeed: titleFrom(text2 || attachments[0]?.name || "Chat"),
137579
+ runtimeMode: binding.runtimeMode,
137580
+ interactionMode: interactionModeOf(chatKey),
137581
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
137582
+ });
137583
+ } catch (error) {
137584
+ if (carried !== void 0) pendingByChat.set(chatKey, carried);
137585
+ throw error;
137586
+ }
135538
137587
  ctx.log.info("chat started a turn from a message", {
135539
137588
  group: binding.displayName,
135540
137589
  chatId: binding.chatId,
135541
137590
  threadId
135542
137591
  });
137592
+ await queueFlow.noteQueued(
137593
+ chatKey,
137594
+ threadId,
137595
+ userMessageId,
137596
+ text2.trim() || attachments[0]?.name || message.resources[0]?.name || ""
137597
+ );
135543
137598
  return threadId;
135544
137599
  };
137600
+ const receiveAttachmentsOnly = async (binding, message) => {
137601
+ const threadId = await ensureThread(binding, message.resources[0]?.name || "Chat");
137602
+ if (threadId === null) return;
137603
+ const waiting = pendingFor(chatKeyOf(binding), threadId);
137604
+ const collected = await collectResources(
137605
+ binding,
137606
+ threadId,
137607
+ message.messageId,
137608
+ message.resources,
137609
+ MAX_ATTACHMENTS_PER_TURN - waiting.attachments.length
137610
+ );
137611
+ waiting.attachments.push(...collected.attachments);
137612
+ waiting.notes.push(...collected.notes.slice(0, MAX_ATTACHMENTS_PER_TURN));
137613
+ if (collected.attachments.length === 0) return;
137614
+ await deliver(
137615
+ binding,
137616
+ t("reply.attachmentReceived", {
137617
+ names: collected.attachments.map((attachment) => attachment.name).join(", "),
137618
+ count: waiting.attachments.length
137619
+ })
137620
+ );
137621
+ };
137622
+ ctx.toolkit({
137623
+ name: "im",
137624
+ tools: [
137625
+ makeSendFileTool({
137626
+ boundChat: (threadId) => readBindings().find(
137627
+ (binding) => binding.projectId !== "" && chatOf(state, chatKeyOf(binding)).threadId === threadId
137628
+ ),
137629
+ adapterFor: (binding) => adapterFor(binding.platform),
137630
+ translate: t,
137631
+ log: {
137632
+ info: (message, fields) => ctx.log.info(message, fields),
137633
+ warn: (message, fields) => ctx.log.warn(message, fields)
137634
+ }
137635
+ })
137636
+ ]
137637
+ });
135545
137638
  const receiving = readBindings().filter((binding) => binding.projectId !== "");
135546
137639
  const connections = [];
135547
137640
  const started = /* @__PURE__ */ new Set();
@@ -135549,7 +137642,7 @@ ${final.text}`;
135549
137642
  const chatQueues = /* @__PURE__ */ new Map();
135550
137643
  const enqueueForChat = (chatKey, work) => {
135551
137644
  const run = (chatQueues.get(chatKey) ?? Promise.resolve()).then(work).catch((error) => {
135552
- ctx.log.warn("chat could not handle the message", { error: describeError(error) });
137645
+ ctx.log.warn("chat could not handle the message", { error: describeError2(error) });
135553
137646
  }).then(() => {
135554
137647
  if (chatQueues.get(chatKey) === run) chatQueues.delete(chatKey);
135555
137648
  });
@@ -135600,7 +137693,7 @@ ${final.text}`;
135600
137693
  },
135601
137694
  now: () => Date.now(),
135602
137695
  warn: (message, error) => {
135603
- ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError(error) });
137696
+ ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError2(error) });
135604
137697
  }
135605
137698
  });
135606
137699
  const handlePanelClick = (platform, click) => {
@@ -135622,7 +137715,7 @@ ${final.text}`;
135622
137715
  };
135623
137716
  const inboundHandlersFor = (platform) => ({
135624
137717
  onError: (message) => ctx.log.warn("chat inbound connection failed", { platform, message }),
135625
- onApproval: handleApprovalClick,
137718
+ onButton: handleButtonClick,
135626
137719
  onPanelClick: (click) => handlePanelClick(platform, click),
135627
137720
  onMessage: (message) => {
135628
137721
  const target = bindingForChat(platform, message.chatId);
@@ -135635,6 +137728,17 @@ ${final.text}`;
135635
137728
  });
135636
137729
  return;
135637
137730
  }
137731
+ if (message.unsupportedType !== void 0) {
137732
+ const unsupported = message.unsupportedType;
137733
+ enqueueForChat(chatKeyOf(target), async () => {
137734
+ if (!unsupportedReplyCooldown.tryAcquire(chatKeyOf(target))) return;
137735
+ await deliver(
137736
+ target,
137737
+ unsupported === "folder" ? t("reply.unsupportedFolder") : t("reply.unsupportedType", { type: unsupported })
137738
+ );
137739
+ });
137740
+ return;
137741
+ }
135638
137742
  const open = openApprovalOf(state, chatKeyOf(target));
135639
137743
  const answerToken = open === void 0 ? null : adapterFor(target.platform)?.answerByText?.(
135640
137744
  target,
@@ -135643,13 +137747,17 @@ ${final.text}`;
135643
137747
  ) ?? null;
135644
137748
  if (answerToken !== null) {
135645
137749
  void answerByToken(answerToken).catch((error) => {
135646
- ctx.log.warn("chat could not answer the approval", { error: describeError(error) });
137750
+ ctx.log.warn("chat could not answer the approval", { error: describeError2(error) });
135647
137751
  });
135648
137752
  return;
135649
137753
  }
135650
137754
  enqueueForChat(chatKeyOf(target), async () => {
135651
137755
  const teams = await fetchTeams(target);
135652
137756
  if (await handleCommand(target, message.text, teams ?? [])) return;
137757
+ const boundThread = chatOf(state, chatKeyOf(target)).threadId;
137758
+ if (boundThread !== null && message.text !== "" && await questionFlow.answerByText(chatKeyOf(target), boundThread, message.text)) {
137759
+ return;
137760
+ }
135653
137761
  const project = await ctx.projections.getProject(target.projectId);
135654
137762
  if (project === null || project.archivedAt != null) {
135655
137763
  if (project === null) {
@@ -135667,6 +137775,7 @@ ${final.text}`;
135667
137775
  return;
135668
137776
  }
135669
137777
  const routed = targetOf(target);
137778
+ const attachmentsOnly = message.text.trim() === "" && message.resources.length > 0;
135670
137779
  if (routed.kind === "team") {
135671
137780
  if (teams !== null && !teams.some((team) => team.teamId === routed.teamId)) {
135672
137781
  commit("chat-updated", {
@@ -135676,11 +137785,17 @@ ${final.text}`;
135676
137785
  });
135677
137786
  await deliver(target, t("reply.teamGone"));
135678
137787
  } else {
137788
+ if (message.resources.length > 0) await deliver(target, t("reply.attachmentsNoTeam"));
137789
+ if (attachmentsOnly) return;
135679
137790
  await startTeamRun(target, routed.teamId, message.text);
135680
137791
  await markWorking(target, chatKeyOf(target), message.messageId);
135681
137792
  return;
135682
137793
  }
135683
137794
  }
137795
+ if (attachmentsOnly) {
137796
+ await receiveAttachmentsOnly(target, message);
137797
+ return;
137798
+ }
135684
137799
  const threadId = await startTurn(target, message);
135685
137800
  if (threadId !== null) await markWorking(target, threadId, message.messageId);
135686
137801
  });
@@ -135699,7 +137814,7 @@ ${final.text}`;
135699
137814
  ctx.log.warn("chat could not start an inbound connection", {
135700
137815
  group: binding.displayName,
135701
137816
  platform: binding.platform,
135702
- error: describeError(error)
137817
+ error: describeError2(error)
135703
137818
  });
135704
137819
  });
135705
137820
  }