@ycodium-ai/plugin-im 0.2.2634 → 0.2.2664

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;
@@ -126167,6 +126167,151 @@ import {
126167
126167
  USER_INPUT_PENDING_TRIGGER_KIND
126168
126168
  } from "@ycodium-ai/plugin-api";
126169
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
+
126170
126315
  // src/chatCommand.ts
126171
126316
  var teamArgument = (text2, chinese) => {
126172
126317
  const match = text2.match(/^\/team(?:\s+([\s\S]*))?$/i);
@@ -126371,11 +126516,13 @@ var readQuestionOptions = (value) => {
126371
126516
  const token = nonEmpty(entry["token"]);
126372
126517
  const optionValue = nonEmpty(entry["value"]);
126373
126518
  if (token === null || optionValue === null || typeof entry["label"] !== "string") return null;
126519
+ const planAction = entry["planAction"];
126374
126520
  options.push({
126375
126521
  token,
126376
126522
  value: optionValue,
126377
126523
  label: entry["label"],
126378
- description: typeof entry["description"] === "string" ? entry["description"] : ""
126524
+ description: typeof entry["description"] === "string" ? entry["description"] : "",
126525
+ ...planAction === "approve" || planAction === "revise" || planAction === "stop" ? { planAction } : {}
126379
126526
  });
126380
126527
  }
126381
126528
  return options.length === 0 ? null : options;
@@ -126807,6 +126954,39 @@ var uploadImage = async (input) => {
126807
126954
  }
126808
126955
  return body.data.image_key;
126809
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
+ };
126810
126990
  var sendChatImage = async (input) => {
126811
126991
  const response = await input.http(`${LARK_BASE}/im/v1/messages?receive_id_type=chat_id`, {
126812
126992
  method: "POST",
@@ -132280,82 +132460,171 @@ var buildChoiceCard = (card) => {
132280
132460
 
132281
132461
  // src/larkInbound.ts
132282
132462
  var import_node_sdk = __toESM(require_lib2(), 1);
132283
- var mentionsApp = (event, appId) => (event.message?.mentions ?? []).some(
132284
- (mention) => mention.id?.app_id === appId || mention.mentioned_type === "bot"
132285
- );
132286
- var mimeFromName = (name) => {
132287
- const ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
132288
- const known = {
132289
- png: "image/png",
132290
- jpg: "image/jpeg",
132291
- jpeg: "image/jpeg",
132292
- gif: "image/gif",
132293
- webp: "image/webp",
132294
- pdf: "application/pdf",
132295
- txt: "text/plain",
132296
- md: "text/markdown",
132297
- json: "application/json",
132298
- csv: "text/csv"
132299
- };
132300
- 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;
132301
132472
  };
132302
- var readPost = (content) => {
132303
- 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()) => {
132304
132525
  let parsed;
132305
132526
  try {
132306
132527
  parsed = JSON.parse(content);
132307
132528
  } catch {
132308
- return null;
132529
+ return void 0;
132309
132530
  }
132310
- const rows = parsed.content ?? [];
132311
- const lines = [];
132312
- let imageKey = null;
132313
- for (const row of rows) {
132314
- const parts = [];
132315
- for (const segment of row) {
132316
- if (segment.tag === "text" && typeof segment.text === "string") parts.push(segment.text);
132317
- if (segment.tag === "img" && typeof segment.image_key === "string" && imageKey === null) {
132318
- imageKey = segment.image_key;
132319
- }
132320
- }
132321
- const line = parts.join("").trim();
132322
- 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);
132323
132539
  }
132324
- return {
132325
- text: lines.join("\n"),
132326
- resource: imageKey === null ? null : { fileKey: imageKey, kind: "image", name: "image.png", mimeType: "image/png" }
132327
- };
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 };
132328
132562
  };
132329
- 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) => {
132330
132588
  if (content === void 0) return null;
132331
- let parsed;
132332
132589
  try {
132333
- parsed = JSON.parse(content);
132590
+ const parsed = JSON.parse(content);
132591
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
132334
132592
  } catch {
132335
132593
  return null;
132336
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
+ };
132337
132603
  if (messageType === "image") {
132338
- const key = parsed["image_key"];
132339
- 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" };
132340
132606
  }
132341
132607
  if (messageType === "file") {
132342
- const key = parsed["file_key"];
132343
- if (typeof key !== "string") return null;
132344
- const rawName = parsed["file_name"];
132345
- const name = typeof rawName === "string" && rawName.length > 0 ? rawName : "file";
132346
- 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) };
132347
132622
  }
132348
132623
  return null;
132349
132624
  };
132350
132625
  var readText = (content) => {
132351
- if (content === void 0) return null;
132352
- try {
132353
- const parsed = JSON.parse(content);
132354
- const text2 = parsed.text;
132355
- return typeof text2 === "string" ? text2 : null;
132356
- } catch {
132357
- return null;
132358
- }
132626
+ const text2 = readJson(content)?.["text"];
132627
+ return typeof text2 === "string" ? text2 : null;
132359
132628
  };
132360
132629
  var parseInboundMessage = (event, appId) => {
132361
132630
  if (event.sender?.sender_type === "app") return null;
@@ -132364,33 +132633,52 @@ var parseInboundMessage = (event, appId) => {
132364
132633
  const { message_id: messageId, chat_id: chatId } = message;
132365
132634
  if (typeof messageId !== "string" || typeof chatId !== "string") return null;
132366
132635
  const chatType = message.chat_type ?? "group";
132367
- if (chatType !== "p2p" && !mentionsApp(event, appId)) return null;
132368
132636
  const messageType = message.message_type ?? "";
132369
- const post = messageType === "post" ? readPost(message.content) : null;
132370
- const resource = post !== null ? post.resource : readResource(messageType, message.content);
132371
- const raw = post !== null ? post.text : messageType === "text" ? readText(message.content) : null;
132372
- const text2 = (message.mentions ?? []).reduce((value, mention) => value.replaceAll(mention.key, ""), raw ?? "").trim();
132373
- if (text2.length === 0 && resource === null) return null;
132374
- return {
132375
- messageId,
132376
- chatId,
132377
- chatType,
132378
- senderOpenId: event.sender?.sender_id?.open_id ?? null,
132379
- text: text2,
132380
- resource
132381
- };
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 };
132382
132660
  };
132383
132661
  var fetchMessageResource = async (input) => {
132384
- const url = `https://open.feishu.cn/open-apis/im/v1/messages/${encodeURIComponent(
132662
+ const url = `${LARK_BASE}/im/v1/messages/${encodeURIComponent(
132385
132663
  input.messageId
132386
132664
  )}/resources/${encodeURIComponent(input.resource.fileKey)}?type=${input.resource.kind}`;
132387
132665
  const response = await input.http(url, {
132388
132666
  headers: { authorization: `Bearer ${input.token}` }
132389
132667
  });
132390
132668
  if (!response.ok) {
132669
+ await response.body?.cancel().catch(() => void 0);
132391
132670
  throw new Error(`lark resource fetch failed: HTTP ${String(response.status)}`);
132392
132671
  }
132393
- 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;
132394
132682
  };
132395
132683
  var cardActionValue = (event) => {
132396
132684
  const roots = [event, event?.event];
@@ -132526,12 +132814,13 @@ var makeLarkAdapter = (deps) => {
132526
132814
  chatId: message.chatId,
132527
132815
  senderId: message.senderOpenId,
132528
132816
  text: message.text,
132529
- resource: message.resource === null ? null : {
132530
- key: message.resource.fileKey,
132531
- name: message.resource.name,
132532
- mimeType: message.resource.mimeType,
132533
- kind: message.resource.kind
132534
- }
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 }
132535
132824
  });
132536
132825
  }
132537
132826
  });
@@ -132560,6 +132849,20 @@ var makeLarkAdapter = (deps) => {
132560
132849
  await sendChatImage({ http: deps.http, token, chatId: binding.chatId, imageKey });
132561
132850
  deps.log.info("lark delivered an image", { name: image2.name });
132562
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 },
132563
132866
  askApproval: async (binding, request) => sendCard({
132564
132867
  http: deps.http,
132565
132868
  token: await tenantToken(binding),
@@ -132650,6 +132953,7 @@ var makeLarkAdapter = (deps) => {
132650
132953
  http: deps.http,
132651
132954
  token: await tenantToken(binding),
132652
132955
  messageId: input.messageId,
132956
+ maxBytes: input.maxBytes,
132653
132957
  resource: {
132654
132958
  fileKey: resource.key,
132655
132959
  kind: resource.kind,
@@ -132746,7 +133050,20 @@ var en_default = {
132746
133050
  projectArchived: 'Project "{{title}}" is archived. Restore it, then message again.',
132747
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.`,
132748
133052
  threadGone: "The earlier session was deleted or archived, so this group now starts a new one.",
132749
- 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}}:"
132750
133067
  },
132751
133068
  approval: {
132752
133069
  allow: "Allow",
@@ -132757,6 +133074,12 @@ var en_default = {
132757
133074
  question: {
132758
133075
  title: "\u2753 Question",
132759
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.",
132760
133083
  freeformHint: "Or reply with a message to answer in your own words.",
132761
133084
  replyOne: "Reply with the number.",
132762
133085
  replyMany: "Reply with the numbers, separated by commas.",
@@ -133039,7 +133362,20 @@ var zh_CN_default = {
133039
133362
  projectArchived: "\u9879\u76EE\u300C{{title}}\u300D\u5DF2\u5F52\u6863\uFF0C\u6062\u590D\u540E\u518D\u53D1\u6D88\u606F\u3002",
133040
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",
133041
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",
133042
- 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"
133043
133379
  },
133044
133380
  approval: {
133045
133381
  allow: "\u5141\u8BB8",
@@ -133050,6 +133386,12 @@ var zh_CN_default = {
133050
133386
  question: {
133051
133387
  title: "\u2753 \u63D0\u95EE",
133052
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",
133053
133395
  freeformHint: "\u4E5F\u53EF\u4EE5\u76F4\u63A5\u56DE\u590D\u4E00\u6761\u6D88\u606F\uFF0C\u7528\u81EA\u5DF1\u7684\u8BDD\u4F5C\u7B54\u3002",
133054
133396
  replyOne: "\u56DE\u590D\u7F16\u53F7\u5373\u53EF\u3002",
133055
133397
  replyMany: "\u56DE\u590D\u7F16\u53F7\uFF0C\u591A\u4E2A\u7528\u9017\u53F7\u9694\u5F00\u3002",
@@ -133776,6 +134118,20 @@ var NOT_MINE = { handled: false };
133776
134118
  var handledWith = (notice) => ({ handled: true, notice });
133777
134119
 
133778
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
+ };
133779
134135
  var QUESTION_TEXT_MAX_CHARS = 1500;
133780
134136
  var ANSWER_ECHO_MAX_CHARS = 200;
133781
134137
  var clip2 = (text2, limit) => {
@@ -133786,8 +134142,10 @@ var titleOf = (item, index, total, t) => {
133786
134142
  const base = item.kind === "plan" ? t("question.planTitle") : t("question.title");
133787
134143
  return total > 1 ? `${base} ${String(index + 1)}/${String(total)}` : base;
133788
134144
  };
133789
- var bodyOf = (item) => clip2(
133790
- item.options.filter((option) => option.description.trim() !== "").map((option) => `\xB7 ${option.label}: ${option.description.trim()}`).join("\n"),
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"),
133791
134149
  QUESTION_TEXT_MAX_CHARS
133792
134150
  );
133793
134151
  var headingOf = (item) => clip2(
@@ -133798,14 +134156,14 @@ var questionCardOf = (item, index, total, t) => ({
133798
134156
  tone: "ask",
133799
134157
  title: titleOf(item, index, total, t),
133800
134158
  heading: headingOf(item),
133801
- body: bodyOf(item),
134159
+ body: bodyOf(item, t),
133802
134160
  note: item.allowFreeform ? t("question.freeformHint") : null,
133803
134161
  rows: [
133804
134162
  ...item.options.map((option) => {
133805
134163
  const ticked = item.multiSelect && item.selected.includes(option.value);
133806
134164
  return [
133807
134165
  {
133808
- label: ticked ? `\u2705 ${option.label}` : option.label,
134166
+ label: ticked ? `\u2705 ${optionLabelOf(item, option, t)}` : optionLabelOf(item, option, t),
133809
134167
  token: option.token,
133810
134168
  style: ticked ? "primary" : "default"
133811
134169
  }
@@ -133826,14 +134184,14 @@ var questionPlainText = (item, index, total, t) => [
133826
134184
  titleOf(item, index, total, t),
133827
134185
  headingOf(item),
133828
134186
  ...item.options.map((option, position) => {
133829
- const description = option.description.trim();
133830
- return `${String(position + 1)}. ${option.label}${description === "" ? "" : ` \u2014 ${description}`}`;
134187
+ const description = optionDescriptionOf(item, option, t);
134188
+ return `${String(position + 1)}. ${optionLabelOf(item, option, t)}${description === "" ? "" : ` \u2014 ${description}`}`;
133831
134189
  }),
133832
134190
  "",
133833
134191
  item.multiSelect ? t("question.replyMany") : t("question.replyOne"),
133834
134192
  ...item.allowFreeform ? [t("question.freeformHint")] : []
133835
134193
  ].join("\n");
133836
- var parseNumberedReply = (item, text2) => {
134194
+ var parseNumberedReply = (item, text2, t) => {
133837
134195
  const trimmed = text2.trim();
133838
134196
  const pattern = item.multiSelect ? /^\d+(?:[\s,,、]+\d+)*$/ : /^\d+$/;
133839
134197
  if (!pattern.test(trimmed)) return null;
@@ -133844,11 +134202,11 @@ var parseNumberedReply = (item, text2) => {
133844
134202
  if (item.multiSelect) {
133845
134203
  return {
133846
134204
  answer: options.map((option) => option.value),
133847
- label: options.map((option) => option.label).join(", ")
134205
+ label: options.map((option) => optionLabelOf(item, option, t)).join(", ")
133848
134206
  };
133849
134207
  }
133850
134208
  const only = options[0];
133851
- return only === void 0 ? null : { answer: only.value, label: only.label };
134209
+ return only === void 0 ? null : { answer: only.value, label: optionLabelOf(item, only, t) };
133852
134210
  };
133853
134211
 
133854
134212
  // src/questionFlow.ts
@@ -133958,7 +134316,8 @@ var makeQuestionFlow = (host) => {
133958
134316
  token: host.newToken(),
133959
134317
  label: option.label,
133960
134318
  value: option.value,
133961
- description: option.description
134319
+ description: option.description,
134320
+ ...option.planAction === void 0 ? {} : { planAction: option.planAction }
133962
134321
  })),
133963
134322
  ...question.multiSelect ? { submitToken: host.newToken() } : {}
133964
134323
  }))
@@ -134008,7 +134367,7 @@ var makeQuestionFlow = (host) => {
134008
134367
  if (found.click.kind === "option") {
134009
134368
  const option = found.click.option;
134010
134369
  if (!item.multiSelect) {
134011
- void answer(requestId, item.id, option.value, option.label);
134370
+ void answer(requestId, item.id, option.value, optionLabelOf(item, option, t));
134012
134371
  return handledWith(null);
134013
134372
  }
134014
134373
  const selected = item.selected.includes(option.value) ? item.selected.filter((value) => value !== option.value) : [...item.selected, option.value];
@@ -134022,7 +134381,7 @@ var makeQuestionFlow = (host) => {
134022
134381
  requestId,
134023
134382
  item.id,
134024
134383
  chosen.map((option) => option.value),
134025
- chosen.map((option) => option.label).join(", ")
134384
+ chosen.map((option) => optionLabelOf(item, option, t)).join(", ")
134026
134385
  );
134027
134386
  return handledWith(null);
134028
134387
  };
@@ -134034,7 +134393,7 @@ var makeQuestionFlow = (host) => {
134034
134393
  const first = unanswered[0];
134035
134394
  if (first === void 0) return false;
134036
134395
  if (!hasButtons(chatKey)) {
134037
- const numbered = parseNumberedReply(first, trimmed);
134396
+ const numbered = parseNumberedReply(first, trimmed, t);
134038
134397
  if (numbered !== null) {
134039
134398
  return answer(record.requestId, first.id, numbered.answer, numbered.label);
134040
134399
  }
@@ -134055,6 +134414,145 @@ var makeQuestionFlow = (host) => {
134055
134414
  return { offer, click, answerByText, resolved };
134056
134415
  };
134057
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
+
134058
134556
  // src/queueCards.ts
134059
134557
  var QUEUE_PREVIEW_MAX_CHARS = 200;
134060
134558
  var WAITING_STATUSES = /* @__PURE__ */ new Set(["queued", "blocked"]);
@@ -134255,7 +134753,7 @@ var makeQueueFlow = (host, deps) => {
134255
134753
  host.outbound(record.chatKey, () => redraw(record.intentId, record.intentId === released));
134256
134754
  }
134257
134755
  };
134258
- const run = async (record, work, failure) => {
134756
+ const run = async (record, work, failure2) => {
134259
134757
  notes.delete(record.intentId);
134260
134758
  let ok = false;
134261
134759
  try {
@@ -134263,7 +134761,7 @@ var makeQueueFlow = (host, deps) => {
134263
134761
  } catch (error) {
134264
134762
  host.warn("chat could not change the queue", error);
134265
134763
  }
134266
- if (!ok) notes.set(record.intentId, failure);
134764
+ if (!ok) notes.set(record.intentId, failure2);
134267
134765
  host.outbound(record.chatKey, () => redraw(record.intentId, false));
134268
134766
  };
134269
134767
  const steer = async (record) => {
@@ -134351,28 +134849,19 @@ var TelegramApiError = class extends Error {
134351
134849
  var isNotModified = (error) => error instanceof TelegramApiError && error.code === 400 && error.message.toLowerCase().includes("message is not modified");
134352
134850
  var PHOTO_NAME = "photo.jpg";
134353
134851
  var PHOTO_MIME = "image/jpeg";
134354
- var mimeFromName2 = (name) => {
134355
- const ext = name.slice(name.lastIndexOf(".") + 1).toLowerCase();
134356
- const known = {
134357
- png: "image/png",
134358
- jpg: "image/jpeg",
134359
- jpeg: "image/jpeg",
134360
- gif: "image/gif",
134361
- webp: "image/webp",
134362
- pdf: "application/pdf",
134363
- txt: "text/plain",
134364
- md: "text/markdown",
134365
- json: "application/json",
134366
- csv: "text/csv"
134367
- };
134368
- return known[ext] ?? "application/octet-stream";
134369
- };
134370
- var resourceOf = (message) => {
134852
+ var withSize = (size) => size === void 0 ? {} : { sizeBytes: size };
134853
+ var resourceOf2 = (message) => {
134371
134854
  const photo = message.photo;
134372
134855
  if (photo !== void 0 && photo.length > 0) {
134373
134856
  const largest = photo[photo.length - 1];
134374
134857
  if (largest !== void 0) {
134375
- 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
+ };
134376
134865
  }
134377
134866
  }
134378
134867
  const document2 = message.document;
@@ -134381,12 +134870,38 @@ var resourceOf = (message) => {
134381
134870
  return {
134382
134871
  fileId: document2.file_id,
134383
134872
  name,
134384
- mimeType: document2.mime_type ?? mimeFromName2(name),
134385
- kind: "file"
134873
+ mimeType: document2.mime_type ?? mimeFromName(name),
134874
+ kind: "file",
134875
+ ...withSize(document2.file_size)
134386
134876
  };
134387
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)
134894
+ };
134895
+ }
134896
+ return null;
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
+ }
134388
134902
  return null;
134389
134903
  };
134904
+ var TELEGRAM_DOWNLOAD_LIMIT_BYTES = 20 * 1024 * 1024;
134390
134905
  var splitForTelegram = (text2, limit = TELEGRAM_TEXT_MAX_CHARS) => splitMessage(text2, limit);
134391
134906
  var nextOffset = (updates, current) => updates.reduce((offset, update) => Math.max(offset, update.update_id + 1), current);
134392
134907
  var commandForBot = (text2, botUsername) => {
@@ -134416,6 +134931,24 @@ var makeTelegramApi = (input) => {
134416
134931
  }
134417
134932
  return envelope.result;
134418
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
+ };
134419
134952
  const keyboard = (rows) => ({
134420
134953
  inline_keyboard: rows.map(
134421
134954
  (row) => row.map((button) => ({ text: button.text, callback_data: button.data }))
@@ -134484,37 +135017,26 @@ var makeTelegramApi = (input) => {
134484
135017
  callback_query_id: callbackQueryId,
134485
135018
  ...text2 === void 0 ? {} : { text: text2 }
134486
135019
  }),
134487
- sendPhoto: async (chatId, photo) => {
134488
- const form = new FormData();
134489
- form.set("chat_id", chatId);
134490
- form.set(
134491
- "photo",
134492
- new Blob([new Uint8Array(photo.bytes)], { type: photo.mimeType }),
134493
- photo.name
134494
- );
134495
- const response = await input.http(`${API_BASE}/bot${input.token}/sendPhoto`, {
134496
- method: "POST",
134497
- body: form
134498
- });
134499
- const envelope = await response.json();
134500
- if (envelope.ok !== true) {
134501
- throw new TelegramApiError(
134502
- "sendPhoto",
134503
- envelope.error_code,
134504
- envelope.description ?? "no reason"
134505
- );
134506
- }
134507
- },
135020
+ sendPhoto: (chatId, photo) => upload("sendPhoto", "photo", chatId, photo),
135021
+ /** 以文件形式发(不压缩、不限尺寸比例):什么类型都行,音频与视频也走这条。 */
135022
+ sendDocument: (chatId, file) => upload("sendDocument", "document", chatId, file),
134508
135023
  /** 取一个文件的字节。要两步:先换路径,再按路径下载。 */
134509
- fetchFile: async (fileId) => {
134510
- 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
+ });
134511
135033
  const path = file.file_path;
134512
135034
  if (path === void 0) throw new TelegramApiError("getFile", void 0, "no file_path");
134513
135035
  const response = await input.http(`${API_BASE}/file/bot${input.token}/${path}`);
134514
135036
  if (!response.ok) {
134515
135037
  throw new TelegramApiError("file", response.status, "download failed");
134516
135038
  }
134517
- return new Uint8Array(await response.arrayBuffer());
135039
+ return readBodyLimited(response, maxBytes);
134518
135040
  }
134519
135041
  };
134520
135042
  };
@@ -134643,14 +135165,14 @@ var tokenOf = (binding) => binding.values["telegramToken"] ?? "";
134643
135165
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
134644
135166
  var makeTelegramAdapter = (deps) => {
134645
135167
  const apis = /* @__PURE__ */ new Map();
134646
- const sleep = deps.sleep ?? defaultSleep;
135168
+ const sleep2 = deps.sleep ?? defaultSleep;
134647
135169
  const t = deps.translate;
134648
135170
  const withFloodWait = async (run) => {
134649
135171
  try {
134650
135172
  return await run();
134651
135173
  } catch (error) {
134652
135174
  if (error instanceof TelegramApiError && error.code === 429 && error.retryAfterSeconds !== void 0 && error.retryAfterSeconds <= FLOOD_WAIT_MAX_SECONDS) {
134653
- await sleep(error.retryAfterSeconds * 1e3);
135175
+ await sleep2(error.retryAfterSeconds * 1e3);
134654
135176
  return run();
134655
135177
  }
134656
135178
  throw error;
@@ -134740,23 +135262,39 @@ var makeTelegramAdapter = (deps) => {
134740
135262
  }
134741
135263
  const message = update.message;
134742
135264
  if (message === void 0) continue;
134743
- const resource = resourceOf(message);
135265
+ const resource = resourceOf2(message);
134744
135266
  const text2 = commandForBot(
134745
135267
  (message.text ?? message.caption ?? "").trim(),
134746
135268
  botUsername
134747
135269
  );
134748
- 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
+ }
134749
135284
  handlers.onMessage({
134750
135285
  messageId: String(message.message_id),
134751
135286
  chatId: String(message.chat.id),
134752
135287
  senderId: message.from === void 0 ? null : String(message.from.id),
134753
135288
  text: text2,
134754
- resource: resource === null ? null : {
134755
- key: resource.fileId,
134756
- name: resource.name,
134757
- mimeType: resource.mimeType,
134758
- kind: resource.kind
134759
- }
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
+ ]
134760
135298
  });
134761
135299
  }
134762
135300
  } catch (error) {
@@ -134783,8 +135321,23 @@ var makeTelegramAdapter = (deps) => {
134783
135321
  },
134784
135322
  sendImage: async (binding, image2) => {
134785
135323
  const api = await apiFor(binding);
134786
- 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
+ }
135334
+ },
135335
+ sendFile: async (binding, file) => {
135336
+ const api = await apiFor(binding);
135337
+ await api.sendDocument(binding.chatId, file);
134787
135338
  },
135339
+ // 单张照片 10 MB、单个文件 50 MB。这两个数来自 Bot API 文档的记忆,本仓库里没有核对过。
135340
+ outboundLimits: { imageBytes: 10 * 1024 * 1024, fileBytes: 50 * 1024 * 1024 },
134788
135341
  askApproval: async (binding, request) => {
134789
135342
  const api = await apiFor(binding);
134790
135343
  return api.sendButtons(binding.chatId, request.body, request.options);
@@ -134868,9 +135421,10 @@ ${line}`
134868
135421
  );
134869
135422
  }
134870
135423
  },
135424
+ downloadLimitBytes: TELEGRAM_DOWNLOAD_LIMIT_BYTES,
134871
135425
  fetchResource: async (binding, input) => {
134872
135426
  const api = await apiFor(binding);
134873
- return api.fetchFile(input.resource.key);
135427
+ return api.fetchFile(input.resource.key, input.maxBytes);
134874
135428
  }
134875
135429
  };
134876
135430
  };
@@ -135099,34 +135653,177 @@ var makeTurnCards = (deps) => {
135099
135653
  };
135100
135654
  };
135101
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
+
135102
135739
  // src/wecomApi.ts
135103
135740
  var DEFAULT_WECOM_ENDPOINT = "wss://openws.work.weixin.qq.com/wssvr/";
135104
135741
  var PING_INTERVAL_MS = 3e4;
135105
135742
  var MAX_MISSED_PONGS = 2;
135106
135743
  var SUBSCRIBE_TIMEOUT_MS = 1e4;
135107
135744
  var SEND_ACK_TIMEOUT_MS = 5e3;
135745
+ var UPLOAD_ACK_TIMEOUT_MS = 15e3;
135108
135746
  var WECOM_TEXT_MAX_CHARS = 4e3;
135109
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;
135110
135753
  var parseWecomMessage = (frame, botId) => {
135111
135754
  const body = frame.body;
135112
135755
  if (body === void 0) return null;
135113
135756
  const messageId = body.msgid;
135114
135757
  const chatId = body.chatid ?? body.from?.userid;
135115
135758
  if (typeof messageId !== "string" || typeof chatId !== "string") return null;
135116
- if (body.msgtype !== "text") return null;
135117
- const raw = body.text?.content;
135118
- if (typeof raw !== "string") return null;
135119
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}`));
135120
135817
  const chatType = body.chattype ?? "single";
135121
- const mentioned = ownIds.some((id) => raw.includes(`@${id}`));
135122
- const text2 = ownIds.reduce((value, id) => value.replaceAll(`@${id}`, ""), raw).trim();
135123
135818
  return {
135124
135819
  messageId,
135125
135820
  chatId,
135126
135821
  senderUserId: body.from?.userid ?? null,
135127
135822
  text: text2,
135823
+ resources,
135128
135824
  requestId: frame.headers?.req_id ?? null,
135129
- chatType: mentioned ? "single" : chatType
135825
+ chatType: mentioned ? "single" : chatType,
135826
+ conversation: chatType === "group" ? "group" : "single"
135130
135827
  };
135131
135828
  };
135132
135829
  var splitForWecom = (text2, limit = WECOM_TEXT_MAX_CHARS) => splitMessage(text2, limit);
@@ -135135,6 +135832,7 @@ var runWecomClient = (input) => {
135135
135832
  let sequence = 0;
135136
135833
  const nextRequestId = (kind) => `${kind}_${++sequence}`;
135137
135834
  const pendingAcks = /* @__PURE__ */ new Map();
135835
+ const conversations = /* @__PURE__ */ new Map();
135138
135836
  const pendingRequests = /* @__PURE__ */ new Map();
135139
135837
  const openStreams = /* @__PURE__ */ new Map();
135140
135838
  const subscribeId = nextRequestId("aibot_subscribe");
@@ -135143,11 +135841,11 @@ var runWecomClient = (input) => {
135143
135841
  let finished = false;
135144
135842
  const ended = Promise.withResolvers();
135145
135843
  const send = (frame) => input.socket.send(JSON.stringify(frame));
135146
- const settleAck = (requestId, error) => {
135844
+ const settleAck = (requestId, error, frame) => {
135147
135845
  const pending = pendingAcks.get(requestId);
135148
135846
  if (pending === void 0) return;
135149
135847
  pendingAcks.delete(requestId);
135150
- if (error === null) pending.resolve();
135848
+ if (error === null) pending.resolve(frame);
135151
135849
  else pending.reject(error);
135152
135850
  };
135153
135851
  const handleFrame = (raw) => {
@@ -135168,7 +135866,7 @@ var runWecomClient = (input) => {
135168
135866
  const error = typeof frame.errcode === "number" && frame.errcode !== 0 ? new Error(`WeCom ack failed: ${String(frame.errcode)} ${frame.errmsg ?? ""}`) : null;
135169
135867
  const stream = openStreams.get(requestId);
135170
135868
  if (stream !== void 0 && error !== null) stream.failed = true;
135171
- settleAck(requestId, error);
135869
+ settleAck(requestId, error, frame);
135172
135870
  return;
135173
135871
  }
135174
135872
  if (frame.cmd !== "aibot_msg_callback") return;
@@ -135210,7 +135908,7 @@ var runWecomClient = (input) => {
135210
135908
  }, PING_INTERVAL_MS);
135211
135909
  void readLoop();
135212
135910
  const subscribed = new Promise((resolve, reject) => {
135213
- pendingAcks.set(subscribeId, { resolve, reject });
135911
+ pendingAcks.set(subscribeId, { resolve: () => resolve(), reject });
135214
135912
  setTimeout(() => {
135215
135913
  if (pendingAcks.delete(subscribeId)) reject(new Error("ETIMEDOUT: WeCom subscribe ack"));
135216
135914
  }, SUBSCRIBE_TIMEOUT_MS);
@@ -135247,7 +135945,7 @@ var runWecomClient = (input) => {
135247
135945
  }
135248
135946
  const requestId = nextRequestId("aibot_send_msg");
135249
135947
  const ack = new Promise((resolve, reject) => {
135250
- pendingAcks.set(requestId, { resolve, reject });
135948
+ pendingAcks.set(requestId, { resolve: () => resolve(), reject });
135251
135949
  setTimeout(() => {
135252
135950
  if (pendingAcks.delete(requestId)) resolve();
135253
135951
  }, SEND_ACK_TIMEOUT_MS);
@@ -135267,6 +135965,50 @@ var runWecomClient = (input) => {
135267
135965
  }
135268
135966
  }
135269
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
+ };
135270
136012
  const openStream = (chatId) => {
135271
136013
  const requestId = pendingRequests.get(chatId);
135272
136014
  if (requestId === void 0) return null;
@@ -135298,6 +136040,10 @@ var runWecomClient = (input) => {
135298
136040
  };
135299
136041
  return {
135300
136042
  sendText,
136043
+ sendMedia,
136044
+ rememberConversation: (chatId, conversation) => {
136045
+ conversations.set(chatId, conversation);
136046
+ },
135301
136047
  openStream,
135302
136048
  rememberRequest: (chatId, requestId) => {
135303
136049
  if (requestId !== null) pendingRequests.set(chatId, requestId);
@@ -135312,6 +136058,83 @@ var runWecomClient = (input) => {
135312
136058
  };
135313
136059
  };
135314
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
+
135315
136138
  // src/wecomAdapter.ts
135316
136139
  var WECOM_CARD_THROTTLE_MS = 400;
135317
136140
  var LIVE_STREAM_LIMIT = 200;
@@ -135365,6 +136188,12 @@ ${kept}`;
135365
136188
  await entry.client.sendText(entry.chatId, chunk);
135366
136189
  }
135367
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
+ };
135368
136197
  return {
135369
136198
  platform: "wecom",
135370
136199
  icon: "wecom",
@@ -135398,13 +136227,19 @@ ${kept}`;
135398
136227
  onMessage: (message) => {
135399
136228
  if (message.chatType !== "single") return;
135400
136229
  client.rememberRequest(message.chatId, message.requestId);
136230
+ client.rememberConversation(message.chatId, message.conversation);
135401
136231
  handlers.onMessage({
135402
136232
  messageId: message.messageId,
135403
136233
  chatId: message.chatId,
135404
136234
  senderId: message.senderUserId,
135405
136235
  text: message.text,
135406
- // 企微这一版不收附件,先不假装支持。
135407
- 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
+ }))
135408
136243
  });
135409
136244
  }
135410
136245
  }
@@ -135482,9 +136317,33 @@ ${kept}`;
135482
136317
  if (view.status !== "working") liveStreams.delete(key);
135483
136318
  }
135484
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"]),
135485
136325
  answerByText: (_binding, text2, options) => {
135486
136326
  const index = optionIndexFrom(text2, options.length);
135487
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
+ };
135488
136347
  }
135489
136348
  };
135490
136349
  };
@@ -135498,6 +136357,7 @@ var TITLE_MAX_CHARS = 72;
135498
136357
  var DEFAULT_PLATFORM = "lark";
135499
136358
  var SESSION_SCAN_LIMIT = 50;
135500
136359
  var ARCHIVED_REPLY_COOLDOWN_MS = 10 * 6e4;
136360
+ var UNSUPPORTED_REPLY_COOLDOWN_MS = 6e4;
135501
136361
  var INBOUND_RUNTIME_MODES = /* @__PURE__ */ new Set([
135502
136362
  "approval-required",
135503
136363
  "auto-accept-edits",
@@ -135510,7 +136370,7 @@ var readString = (row, key) => {
135510
136370
  const value = row[key];
135511
136371
  return typeof value === "string" ? value.trim() : "";
135512
136372
  };
135513
- var describeError = (error) => error instanceof Error ? error.message : String(error);
136373
+ var describeError2 = (error) => error instanceof Error ? error.message : String(error);
135514
136374
  var titleFrom = (text2) => text2.replace(/\s+/g, " ").slice(0, TITLE_MAX_CHARS) || "Chat thread";
135515
136375
  var imPlugin = definePlugin({
135516
136376
  id: "im",
@@ -135544,6 +136404,7 @@ var imPlugin = definePlugin({
135544
136404
  makeWecomAdapter({
135545
136405
  // 企微不需要厂商 SDK:全程 JSON over WebSocket,走插件面那条出网长连接。
135546
136406
  connectSocket: (url) => ctx.socket.connect({ url }),
136407
+ http: ctx.http,
135547
136408
  translate: t,
135548
136409
  log: {
135549
136410
  info: (message, fields) => ctx.log.info(message, fields),
@@ -135715,7 +136576,7 @@ var imPlugin = definePlugin({
135715
136576
  } catch (error) {
135716
136577
  ctx.log.warn("chat could not deliver an image", {
135717
136578
  name: attachment.name,
135718
- error: describeError(error)
136579
+ error: describeError2(error)
135719
136580
  });
135720
136581
  }
135721
136582
  }
@@ -135726,7 +136587,7 @@ var imPlugin = definePlugin({
135726
136587
  try {
135727
136588
  await adapterFor(binding.platform)?.sendText(binding, text2);
135728
136589
  } catch (error) {
135729
- ctx.log.warn("chat could not deliver", { error: describeError(error) });
136590
+ ctx.log.warn("chat could not deliver", { error: describeError2(error) });
135730
136591
  }
135731
136592
  };
135732
136593
  const projectByThread = /* @__PURE__ */ new Map();
@@ -135760,7 +136621,7 @@ var imPlugin = definePlugin({
135760
136621
  acknowledgements.set(threadId, pending);
135761
136622
  ctx.log.info("chat acknowledged a message", { threadId, messageId });
135762
136623
  } catch (error) {
135763
- 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) });
135764
136625
  }
135765
136626
  };
135766
136627
  const clearWorking = async (binding, threadId) => {
@@ -135773,7 +136634,7 @@ var imPlugin = definePlugin({
135773
136634
  await adapter.clearAcknowledge(binding, entry.messageId, entry.ackId);
135774
136635
  } catch (error) {
135775
136636
  ctx.log.warn("chat could not clear the acknowledgement", {
135776
- error: describeError(error)
136637
+ error: describeError2(error)
135777
136638
  });
135778
136639
  }
135779
136640
  }
@@ -135860,7 +136721,7 @@ ${final.text}`;
135860
136721
  state = restored;
135861
136722
  },
135862
136723
  (error) => {
135863
- 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) });
135864
136725
  }
135865
136726
  );
135866
136727
  let persisting = Promise.resolve();
@@ -135869,7 +136730,7 @@ ${final.text}`;
135869
136730
  persisting = persisting.then(() => ctx.events.append({ name, aggregate: { kind: "plugin", id: ctx.id }, data })).then(
135870
136731
  () => void 0,
135871
136732
  (error) => {
135872
- 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) });
135873
136734
  }
135874
136735
  );
135875
136736
  };
@@ -135877,7 +136738,7 @@ ${final.text}`;
135877
136738
  commit("chat-updated", { chatKey, threadId });
135878
136739
  };
135879
136740
  const outbound = makeKeyedQueue((error) => {
135880
- 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) });
135881
136742
  });
135882
136743
  const settleApprovalCard = async (approval, settlement) => {
135883
136744
  if (approval.cardId === void 0) return;
@@ -135887,7 +136748,7 @@ ${final.text}`;
135887
136748
  try {
135888
136749
  await settle(binding, approval.cardId, settlement);
135889
136750
  } catch (error) {
135890
- 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) });
135891
136752
  }
135892
136753
  };
135893
136754
  const answerApproval = async (approval, decision, label) => {
@@ -135939,12 +136800,12 @@ ${final.text}`;
135939
136800
  },
135940
136801
  endOf: async (threadId) => {
135941
136802
  const stopped = stopRequested.delete(threadId);
135942
- const failure = failureSummaries.get(threadId);
136803
+ const failure2 = failureSummaries.get(threadId);
135943
136804
  failureSummaries.delete(threadId);
135944
136805
  const thread = await ctx.projections.getThread(threadId).catch(() => null);
135945
136806
  const session = thread?.session;
135946
136807
  if (session?.status === "error") {
135947
- return { status: "error", errorText: session.lastError ?? failure ?? null };
136808
+ return { status: "error", errorText: session.lastError ?? failure2 ?? null };
135948
136809
  }
135949
136810
  if (stopped || session?.status === "interrupted" || session?.status === "stopped") {
135950
136811
  return { status: "interrupted", errorText: null };
@@ -135964,7 +136825,7 @@ ${final.text}`;
135964
136825
  return () => clearTimeout(timer);
135965
136826
  },
135966
136827
  warn: (message, error) => {
135967
- ctx.log.warn(`chat ${message}`, { error: describeError(error) });
136828
+ ctx.log.warn(`chat ${message}`, { error: describeError2(error) });
135968
136829
  }
135969
136830
  });
135970
136831
  ctx.events.on(TURN_PROGRESS_TRIGGER_KIND, (event) => {
@@ -135990,7 +136851,7 @@ ${final.text}`;
135990
136851
  );
135991
136852
  } catch (error) {
135992
136853
  ctx.log.warn("chat could not mark a stale card as interrupted", {
135993
- error: describeError(error)
136854
+ error: describeError2(error)
135994
136855
  });
135995
136856
  }
135996
136857
  }
@@ -136011,7 +136872,7 @@ ${final.text}`;
136011
136872
  const record = state.approvals[offer.requestId];
136012
136873
  if (record?.resolved === true) await settleApprovalCard(record, { kind: "elsewhere" });
136013
136874
  } catch (error) {
136014
- 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) });
136015
136876
  }
136016
136877
  };
136017
136878
  ctx.events.on("thread.approval-pending", async (event) => {
@@ -136106,7 +136967,7 @@ ${final.text}`;
136106
136967
  newId: () => crypto.randomUUID(),
136107
136968
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
136108
136969
  warn: (message, error) => {
136109
- ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError(error) });
136970
+ ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError2(error) });
136110
136971
  }
136111
136972
  };
136112
136973
  const questionFlow = makeQuestionFlow(flowHost);
@@ -136143,7 +137004,7 @@ ${final.text}`;
136143
137004
  await ctx.projections.listPendingUserInputs(threadId)
136144
137005
  );
136145
137006
  } catch (error) {
136146
- ctx.log.warn("chat could not read the pending questions", { error: describeError(error) });
137007
+ ctx.log.warn("chat could not read the pending questions", { error: describeError2(error) });
136147
137008
  }
136148
137009
  });
136149
137010
  ctx.reactor({
@@ -136186,12 +137047,13 @@ ${final.text}`;
136186
137047
  }
136187
137048
  void answerApproval(found.approval, found.button.decision, found.button.label).catch(
136188
137049
  (error) => {
136189
- 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) });
136190
137051
  }
136191
137052
  );
136192
137053
  return null;
136193
137054
  };
136194
137055
  const archivedReplyCooldown = makeCooldown(ARCHIVED_REPLY_COOLDOWN_MS, () => Date.now());
137056
+ const unsupportedReplyCooldown = makeCooldown(UNSUPPORTED_REPLY_COOLDOWN_MS, () => Date.now());
136195
137057
  const alreadyHandled = (target, messageId) => {
136196
137058
  const key = `${chatKeyOf(target)}:${messageId}`;
136197
137059
  if (seen.has(key)) return true;
@@ -136203,33 +137065,103 @@ ${final.text}`;
136203
137065
  }
136204
137066
  return false;
136205
137067
  };
136206
- const collectAttachment = async (binding, threadId, message) => {
136207
- 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) => {
136208
137079
  const adapter = adapterFor(binding.platform);
136209
- if (resource === null || adapter?.fetchResource === void 0) return [];
136210
- try {
136211
- const bytes = await adapter.fetchResource(binding, {
136212
- messageId: message.messageId,
136213
- resource
136214
- });
136215
- const stored = await ctx.attachments.store({
136216
- threadId,
136217
- name: resource.name,
136218
- mimeType: resource.mimeType,
136219
- bytes
136220
- });
136221
- ctx.log.info("chat stored an inbound attachment", {
136222
- name: stored.name,
136223
- sizeBytes: stored.sizeBytes
136224
- });
136225
- return [stored];
136226
- } catch (error) {
136227
- ctx.log.warn("chat dropped an inbound attachment", {
136228
- name: resource.name,
136229
- error: describeError(error)
136230
- });
136231
- 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
+ }
136232
137163
  }
137164
+ return { attachments, notes };
136233
137165
  };
136234
137166
  const targetOf = (binding) => chatOf(state, chatKeyOf(binding)).target;
136235
137167
  const chatByRun = /* @__PURE__ */ new Map();
@@ -136347,7 +137279,7 @@ ${final.text}`;
136347
137279
  modelSelection: selection
136348
137280
  });
136349
137281
  } catch (error) {
136350
- 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) });
136351
137283
  await deliver(binding, t("model.setFailed"));
136352
137284
  return;
136353
137285
  }
@@ -136427,7 +137359,7 @@ ${final.text}`;
136427
137359
  return true;
136428
137360
  } catch (error) {
136429
137361
  ctx.log.warn("chat could not open the command panel", {
136430
- error: describeError(error)
137362
+ error: describeError2(error)
136431
137363
  });
136432
137364
  }
136433
137365
  }
@@ -136481,7 +137413,7 @@ ${final.text}`;
136481
137413
  try {
136482
137414
  paused = (await ctx.projections.getThreadExecution(threadId))?.queuePaused === true;
136483
137415
  } catch (error) {
136484
- ctx.log.warn("chat could not read the turn queue", { error: describeError(error) });
137416
+ ctx.log.warn("chat could not read the turn queue", { error: describeError2(error) });
136485
137417
  await deliver(binding, t("command.continueFailed"));
136486
137418
  return true;
136487
137419
  }
@@ -136498,7 +137430,7 @@ ${final.text}`;
136498
137430
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
136499
137431
  });
136500
137432
  } catch (error) {
136501
- ctx.log.warn("chat could not resume the turn queue", { error: describeError(error) });
137433
+ ctx.log.warn("chat could not resume the turn queue", { error: describeError2(error) });
136502
137434
  await deliver(binding, t("command.continueFailed"));
136503
137435
  return true;
136504
137436
  }
@@ -136561,9 +137493,8 @@ ${final.text}`;
136561
137493
  }
136562
137494
  };
136563
137495
  const interactionModeOf = (chatKey) => chatOf(state, chatKey).plan ? "plan" : "default";
136564
- const startTurn = async (binding, message) => {
137496
+ const ensureThread = async (binding, titleSeed) => {
136565
137497
  const chatKey = chatKeyOf(binding);
136566
- const text2 = message.text;
136567
137498
  let threadId = chatOf(state, chatKey).threadId ?? void 0;
136568
137499
  if (threadId !== void 0 && !await threadStillUsable(binding, threadId)) {
136569
137500
  ctx.log.info("chat dropped a saved thread that is no longer usable", {
@@ -136574,64 +137505,85 @@ ${final.text}`;
136574
137505
  threadId = void 0;
136575
137506
  await deliver(binding, t("reply.threadGone"));
136576
137507
  }
136577
- if (threadId === void 0) {
136578
- const project = await ctx.projections.getProject(binding.projectId);
136579
- if (project === null) {
136580
- ctx.log.warn("chat cannot start a thread: the configured project is gone", {
136581
- group: binding.displayName,
136582
- projectId: binding.projectId
136583
- });
136584
- return null;
136585
- }
136586
- const modelSelection = chatOf(state, chatKey).model ?? project.defaultModelSelection;
136587
- if (modelSelection === null) {
136588
- ctx.log.warn("chat cannot start a thread: the project has no default model", {
136589
- group: binding.displayName,
136590
- projectId: binding.projectId
136591
- });
136592
- await deliver(binding, t("reply.noDefaultModel", { title: project.title }));
136593
- return null;
136594
- }
136595
- const createdThreadId = crypto.randomUUID();
136596
- threadId = createdThreadId;
136597
- await ctx.commands.dispatch({
136598
- type: "thread.create",
136599
- commandId: crypto.randomUUID(),
136600
- threadId,
136601
- projectId: binding.projectId,
136602
- title: titleFrom(text2 || message.resource?.name || "Chat"),
136603
- modelSelection,
136604
- runtimeMode: binding.runtimeMode,
136605
- interactionMode: interactionModeOf(chatKey),
136606
- branch: null,
136607
- worktreePath: null,
136608
- 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
136609
137514
  });
136610
- commit("thread-opened", {
136611
- chatKey,
136612
- threadId: createdThreadId,
136613
- projectId: binding.projectId,
136614
- 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
136615
137522
  });
136616
- projectByThread.set(createdThreadId, binding.projectId);
137523
+ await deliver(binding, t("reply.noDefaultModel", { title: project.title }));
137524
+ return null;
136617
137525
  }
136618
- const attachments = await collectAttachment(binding, threadId, message);
136619
- const userMessageId = crypto.randomUUID();
137526
+ const createdThreadId = crypto.randomUUID();
136620
137527
  await ctx.commands.dispatch({
136621
- type: "thread.turn.start",
137528
+ type: "thread.create",
136622
137529
  commandId: crypto.randomUUID(),
136623
- threadId,
136624
- message: {
136625
- messageId: userMessageId,
136626
- role: "user",
136627
- text: text2,
136628
- attachments
136629
- },
136630
- titleSeed: titleFrom(text2 || attachments[0]?.name || "Chat"),
137530
+ threadId: createdThreadId,
137531
+ projectId: binding.projectId,
137532
+ title: titleFrom(titleSeed),
137533
+ modelSelection,
136631
137534
  runtimeMode: binding.runtimeMode,
136632
137535
  interactionMode: interactionModeOf(chatKey),
137536
+ branch: null,
137537
+ worktreePath: null,
136633
137538
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
136634
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
+ }
136635
137587
  ctx.log.info("chat started a turn from a message", {
136636
137588
  group: binding.displayName,
136637
137589
  chatId: binding.chatId,
@@ -136641,10 +137593,48 @@ ${final.text}`;
136641
137593
  chatKey,
136642
137594
  threadId,
136643
137595
  userMessageId,
136644
- text2.trim() || attachments[0]?.name || message.resource?.name || ""
137596
+ text2.trim() || attachments[0]?.name || message.resources[0]?.name || ""
136645
137597
  );
136646
137598
  return threadId;
136647
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
+ });
136648
137638
  const receiving = readBindings().filter((binding) => binding.projectId !== "");
136649
137639
  const connections = [];
136650
137640
  const started = /* @__PURE__ */ new Set();
@@ -136652,7 +137642,7 @@ ${final.text}`;
136652
137642
  const chatQueues = /* @__PURE__ */ new Map();
136653
137643
  const enqueueForChat = (chatKey, work) => {
136654
137644
  const run = (chatQueues.get(chatKey) ?? Promise.resolve()).then(work).catch((error) => {
136655
- 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) });
136656
137646
  }).then(() => {
136657
137647
  if (chatQueues.get(chatKey) === run) chatQueues.delete(chatKey);
136658
137648
  });
@@ -136703,7 +137693,7 @@ ${final.text}`;
136703
137693
  },
136704
137694
  now: () => Date.now(),
136705
137695
  warn: (message, error) => {
136706
- ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError(error) });
137696
+ ctx.log.warn(`chat ${message}`, error === void 0 ? {} : { error: describeError2(error) });
136707
137697
  }
136708
137698
  });
136709
137699
  const handlePanelClick = (platform, click) => {
@@ -136738,6 +137728,17 @@ ${final.text}`;
136738
137728
  });
136739
137729
  return;
136740
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
+ }
136741
137742
  const open = openApprovalOf(state, chatKeyOf(target));
136742
137743
  const answerToken = open === void 0 ? null : adapterFor(target.platform)?.answerByText?.(
136743
137744
  target,
@@ -136746,7 +137747,7 @@ ${final.text}`;
136746
137747
  ) ?? null;
136747
137748
  if (answerToken !== null) {
136748
137749
  void answerByToken(answerToken).catch((error) => {
136749
- 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) });
136750
137751
  });
136751
137752
  return;
136752
137753
  }
@@ -136754,7 +137755,7 @@ ${final.text}`;
136754
137755
  const teams = await fetchTeams(target);
136755
137756
  if (await handleCommand(target, message.text, teams ?? [])) return;
136756
137757
  const boundThread = chatOf(state, chatKeyOf(target)).threadId;
136757
- if (boundThread !== null && await questionFlow.answerByText(chatKeyOf(target), boundThread, message.text)) {
137758
+ if (boundThread !== null && message.text !== "" && await questionFlow.answerByText(chatKeyOf(target), boundThread, message.text)) {
136758
137759
  return;
136759
137760
  }
136760
137761
  const project = await ctx.projections.getProject(target.projectId);
@@ -136774,6 +137775,7 @@ ${final.text}`;
136774
137775
  return;
136775
137776
  }
136776
137777
  const routed = targetOf(target);
137778
+ const attachmentsOnly = message.text.trim() === "" && message.resources.length > 0;
136777
137779
  if (routed.kind === "team") {
136778
137780
  if (teams !== null && !teams.some((team) => team.teamId === routed.teamId)) {
136779
137781
  commit("chat-updated", {
@@ -136783,11 +137785,17 @@ ${final.text}`;
136783
137785
  });
136784
137786
  await deliver(target, t("reply.teamGone"));
136785
137787
  } else {
137788
+ if (message.resources.length > 0) await deliver(target, t("reply.attachmentsNoTeam"));
137789
+ if (attachmentsOnly) return;
136786
137790
  await startTeamRun(target, routed.teamId, message.text);
136787
137791
  await markWorking(target, chatKeyOf(target), message.messageId);
136788
137792
  return;
136789
137793
  }
136790
137794
  }
137795
+ if (attachmentsOnly) {
137796
+ await receiveAttachmentsOnly(target, message);
137797
+ return;
137798
+ }
136791
137799
  const threadId = await startTurn(target, message);
136792
137800
  if (threadId !== null) await markWorking(target, threadId, message.messageId);
136793
137801
  });
@@ -136806,7 +137814,7 @@ ${final.text}`;
136806
137814
  ctx.log.warn("chat could not start an inbound connection", {
136807
137815
  group: binding.displayName,
136808
137816
  platform: binding.platform,
136809
- error: describeError(error)
137817
+ error: describeError2(error)
136810
137818
  });
136811
137819
  });
136812
137820
  }