opencode-feishu-plugin 0.2.8 → 0.2.9

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/README.en.md CHANGED
@@ -21,15 +21,19 @@ Connect [OpenCode](https://opencode.ai) to Feishu/Lark: **one Feishu topic = one
21
21
  | ✅ **Card approvals** | Permission requests become cards: allow once / always / this session only / deny — signed tokens prevent forgery and replay |
22
22
  | 📊 **Real-time visible** | "Thinking" receipt → live tool-call cards → streaming text updates; footer shows the current model |
23
23
  | ⏹ **Controllable** | Every reply card has a "force stop" button; a watchdog auto-interrupts stuck sessions; native queue with `/steer` `/now` to cut in |
24
+ | 📎 **Images / files** | Images and files sent in Feishu are downloaded and attached to the session, so vision/file-capable models can see and read them |
24
25
  | 🚫 **No ports** | Full long-connection; no inbound port needed on the server |
25
26
 
26
27
  ## 1. Feishu app setup (~3 minutes)
27
28
 
28
29
  1. Open the [Feishu Open Platform](https://open.feishu.cn/app) → **Create an enterprise self-built app**.
29
30
  2. **Add app capability → Bot**.
30
- 3. **Permission management** — enable only these two scopes:
31
+ 3. **Permission management** — enable these two minimal scopes:
31
32
  - `im:message.p2p_msg:readonly` — read messages users send to the bot in p2p chat
32
33
  - `im:message:send_as_bot` — send messages as the app (also used to update cards)
34
+
35
+ To **receive images / files** (downloaded and attached to sessions), add one more:
36
+ - `im:message:readonly` — fetch message resources (required to download images / files)
33
37
  4. **Events & Callbacks → Event configuration**: subscription method **"Use long connection to receive events"** (do **not** pick Webhook), add event `im.message.receive_v1`.
34
38
  5. **Events & Callbacks → Callback configuration**: same long-connection method, add callback `card.action.trigger` (zero permission requirement).
35
39
  6. **Version management & release**: availability = **only yourself**, create a version and **publish**. ⚠️ Without publishing the app stays in "development" state and the long connection cannot connect — the bot will never respond.
@@ -167,7 +171,7 @@ Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepa
167
171
 
168
172
  ## 6. Known limitations
169
173
 
170
- - Only **p2p text** (incl. rich text); images / files / audio-video get a text placeholder and aren't downloaded.
174
+ - Image / file messages are **downloaded and attached to the session** (requires `im:message:readonly`); audio / video / stickers still get a text placeholder.
171
175
  - Only takes over approvals for **Feishu-originated sessions**; local TUI sessions are unaffected.
172
176
  - One main path to create a session: the `/new` / `/form` form card.
173
177
  - Forms are JSON 2.0; old clients need ≥ V3.7.0 for `select_static`.
@@ -178,7 +182,7 @@ Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepa
178
182
 
179
183
  Iterating from real usage feedback; current plan:
180
184
 
181
- - [ ] **Accept images / files**: currently only p2p text is handled — images / files get a text placeholder. Planned: download images / files and **attach them to the session** so vision / file-capable models can see the images and read the file contents.
185
+ - [x] **Accept images / files**: done — downloaded automatically into `<configDir>/plugins/feishu-files/` and attached to the session (requires `im:message:readonly`; default max 20MB per attachment).
182
186
  - [ ] **New messages cut in by default when busy**: currently new messages queue natively while a session is busy (manual cut-in via `/steer`, `/now`). Planned: new messages default to **cutting in immediately**, interrupting the current step to run first.
183
187
 
184
188
  ## Advanced topics & development
package/README.md CHANGED
@@ -21,15 +21,19 @@
21
21
  | ✅ **卡片审批** | 权限请求变飞书卡片:允许一次 / 始终允许 / 本会话内允许 / 拒绝,自签 token 防伪防重放 |
22
22
  | 📊 **实时可见** | 「思考中」回执 → 工具调用实时上卡 → 文本流式更新,页脚显示当前模型 |
23
23
  | ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时原生排队,`/steer` `/now` 可插队 |
24
+ | 📎 **图片 / 文件** | 飞书里的图片 / 文件自动下载并挂进会话,支持视觉 / 文件的模型直接看图、读文件 |
24
25
  | 🚫 **无端口** | 全程长连接,服务器无需开放任何入站端口 |
25
26
 
26
27
  ## 一、飞书后台配置(约 3 分钟)
27
28
 
28
29
  1. 打开 [飞书开放平台](https://open.feishu.cn/app) → **创建企业自建应用**。
29
30
  2. **添加应用能力 → 机器人**。
30
- 3. **权限管理**,只开通这两个 scope:
31
+ 3. **权限管理**,开通这两个最小 scope:
31
32
  - `im:message.p2p_msg:readonly` —— 读取用户发给机器人的单聊消息
32
33
  - `im:message:send_as_bot` —— 以应用身份发消息(也用于更新卡片)
34
+
35
+ 如需**接收图片 / 文件**(下载后挂进会话),再加开一个:
36
+ - `im:message:readonly` —— 获取消息中的资源文件(图片 / 文件下载的必要条件)
33
37
  4. **事件与回调 → 事件配置**:订阅方式选**「使用长连接接收事件」**(不要选 Webhook),添加事件 `im.message.receive_v1`。
34
38
  5. **事件与回调 → 回调配置**:订阅方式同样选**长连接**,添加回调 `card.action.trigger`(零权限要求)。
35
39
  6. **版本管理与发布**:可用范围 = **仅本人**,创建版本并**发布**。⚠️ 不发布就是开发态,长连接连不上,机器人不会有任何反应。
@@ -167,7 +171,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
167
171
 
168
172
  ## 六、已知限制
169
173
 
170
- - 只处理**单聊文本**(含富文本);图片 / 文件 / 音视频只给文字占位,不下载。
174
+ - 图片 / 文件消息会**下载到本地并挂进会话**(需开 `im:message:readonly`);音频 / 视频 / 表情包仍只给文字占位。
171
175
  - 只接管**从飞书发起的会话**的审批;本地 TUI 会话不受影响。
172
176
  - 建会话只有一条主路径:`/new` 与 `/form` 等价的表单卡。
173
177
  - 表单为 JSON 2.0,老客户端对 `select_static` 有最低版本要求(≥ V3.7.0)。
@@ -178,7 +182,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
178
182
 
179
183
  按真实使用反馈迭代,当前规划:
180
184
 
181
- - [ ] **接收图片 / 文件**:目前只处理单聊文本,图片 / 文件仅显示文字占位。规划:下载图片 / 文件并作为**附件挂进会话**,让支持视觉 / 文件的模型直接看图、读文件内容。
185
+ - [x] **接收图片 / 文件**:已支持——自动下载到 `<configDir>/plugins/feishu-files/` 并作为附件挂进会话(需开 `im:message:readonly`;单附件默认 ≤20MB)。
182
186
  - [ ] **忙时新消息默认插队**:目前会话忙时新消息默认排队(可用 `/steer`、`/now` 手动插队)。规划:忙时你发的新消息**默认直接插队**,立即打断当前步骤优先执行。
183
187
 
184
188
  ## 高级主题与开发
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import crypto3, { randomBytes, createHash, createHmac, timingSafeEqual } from 'crypto';
2
- import url from 'url';
2
+ import url, { pathToFileURL } from 'url';
3
3
  import http from 'http';
4
4
  import https from 'https';
5
5
  import http2 from 'http2';
6
6
  import util from 'util';
7
- import path, { dirname, isAbsolute, sep, resolve as resolve$1, join, basename } from 'path';
7
+ import path, { dirname, isAbsolute, join, sep, resolve as resolve$1, basename } from 'path';
8
8
  import zlib from 'zlib';
9
9
  import stream3, { Readable } from 'stream';
10
10
  import { EventEmitter } from 'events';
11
11
  import fs, { mkdirSync, createWriteStream, readFileSync, realpathSync, statSync } from 'fs';
12
12
  import qs from 'querystring';
13
13
  import { homedir } from 'os';
14
- import { readFile, readdir } from 'fs/promises';
14
+ import { mkdir, stat, rm, readFile, readdir } from 'fs/promises';
15
15
 
16
16
  var __create = Object.create;
17
17
  var __defProp = Object.defineProperty;
@@ -8845,7 +8845,7 @@ var require_mime_db = __commonJS({
8845
8845
  var require_mime_types = __commonJS({
8846
8846
  "node_modules/form-data/node_modules/mime-types/index.js"(exports) {
8847
8847
  var db = require_mime_db();
8848
- var extname = __require("path").extname;
8848
+ var extname2 = __require("path").extname;
8849
8849
  var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
8850
8850
  var TEXT_TYPE_REGEXP = /^text\//i;
8851
8851
  exports.charset = charset;
@@ -8899,7 +8899,7 @@ var require_mime_types = __commonJS({
8899
8899
  if (!path2 || typeof path2 !== "string") {
8900
8900
  return false;
8901
8901
  }
8902
- var extension3 = extname("x." + path2).toLowerCase().substr(1);
8902
+ var extension3 = extname2("x." + path2).toLowerCase().substr(1);
8903
8903
  if (!extension3) {
8904
8904
  return false;
8905
8905
  }
@@ -10040,12 +10040,12 @@ var require_form_data = __commonJS({
10040
10040
  if (value3.end != void 0 && value3.end != Infinity && value3.start != void 0) {
10041
10041
  callback3(null, value3.end + 1 - (value3.start ? value3.start : 0));
10042
10042
  } else {
10043
- fs2.stat(value3.path, function(err2, stat) {
10043
+ fs2.stat(value3.path, function(err2, stat2) {
10044
10044
  if (err2) {
10045
10045
  callback3(err2);
10046
10046
  return;
10047
10047
  }
10048
- var fileSize = stat.size - (value3.start ? value3.start : 0);
10048
+ var fileSize = stat2.size - (value3.start ? value3.start : 0);
10049
10049
  callback3(null, fileSize);
10050
10050
  });
10051
10051
  }
@@ -144029,6 +144029,15 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144029
144029
  const runnerCardTextMax = clamp4(asNumber(merged.runnerCardTextMax, 2048), 512, 8192);
144030
144030
  const finalAnswerMinChars = clamp4(asNumber(merged.finalAnswerMinChars, 600), 0, 2e4);
144031
144031
  const finalAnswerFileMinBytes = clamp4(asNumber(merged.finalAnswerFileMinBytes, 20 * 1024), 8192, 102400);
144032
+ const acceptAttachments = asBoolean(merged.acceptAttachments, true);
144033
+ const attachmentMaxBytes = clamp4(
144034
+ asNumber(merged.attachmentMaxBytes, 20 * 1024 * 1024),
144035
+ 1024 * 1024,
144036
+ 100 * 1024 * 1024
144037
+ );
144038
+ const attachmentTimeoutMs = clamp4(asNumber(merged.attachmentTimeoutMs, 3e4), 5e3, 12e4);
144039
+ const attachmentsDirRaw = asString(merged.attachmentsDir).trim();
144040
+ const attachmentsDir = attachmentsDirRaw ? attachmentsDirRaw : join(resolveConfigDir(env, deps.configDir), "plugins", "feishu-files");
144032
144041
  const keepaliveIntervalMs = clamp4(
144033
144042
  asNumber(merged.keepaliveIntervalMs, 20 * 60 * 1e3),
144034
144043
  5 * 60 * 1e3,
@@ -144089,7 +144098,11 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144089
144098
  runnerCardMaxTools,
144090
144099
  runnerCardTextMax,
144091
144100
  finalAnswerMinChars,
144092
- finalAnswerFileMinBytes
144101
+ finalAnswerFileMinBytes,
144102
+ acceptAttachments,
144103
+ attachmentMaxBytes,
144104
+ attachmentTimeoutMs,
144105
+ attachmentsDir
144093
144106
  };
144094
144107
  }
144095
144108
  function loadConfigFile(env, deps, warnings) {
@@ -144706,6 +144719,27 @@ function extractPostText(rawContent) {
144706
144719
  return "";
144707
144720
  }
144708
144721
  }
144722
+ function parseIncomingAttachment(messageType, rawContent) {
144723
+ if (messageType !== "image" && messageType !== "file") return void 0;
144724
+ let parsed;
144725
+ try {
144726
+ parsed = JSON.parse(rawContent);
144727
+ } catch {
144728
+ return void 0;
144729
+ }
144730
+ if (!isRecordLike(parsed)) return void 0;
144731
+ if (messageType === "image") {
144732
+ const imageKey = str(parsed.image_key);
144733
+ return imageKey ? { kind: "image", fileKey: imageKey } : void 0;
144734
+ }
144735
+ const fileKey = str(parsed.file_key);
144736
+ if (!fileKey) return void 0;
144737
+ const fileName = str(parsed.file_name);
144738
+ return { kind: "file", fileKey, ...fileName ? { fileName } : {} };
144739
+ }
144740
+ function isRecordLike(value3) {
144741
+ return typeof value3 === "object" && value3 !== null && !Array.isArray(value3);
144742
+ }
144709
144743
  function parseIncomingMessage(data) {
144710
144744
  if (!isRecord2(data)) return void 0;
144711
144745
  const message = data.message;
@@ -144718,6 +144752,7 @@ function parseIncomingMessage(data) {
144718
144752
  if (!rawContent) return void 0;
144719
144753
  const sender = data.sender;
144720
144754
  const senderOpenId = isRecord2(sender) && isRecord2(sender.sender_id) ? str(sender.sender_id.open_id) : "";
144755
+ const attachment = parseIncomingAttachment(messageType, rawContent);
144721
144756
  return {
144722
144757
  eventId: str(data.event_id),
144723
144758
  messageId,
@@ -144725,6 +144760,7 @@ function parseIncomingMessage(data) {
144725
144760
  chatType: str(message.chat_type) || "p2p",
144726
144761
  messageType,
144727
144762
  text: extractMessageText(messageType, rawContent),
144763
+ ...attachment ? { attachment } : {},
144728
144764
  senderOpenId,
144729
144765
  ...str(message.create_time) ? { createTime: str(message.create_time) } : {},
144730
144766
  ...str(message.thread_id) ? { threadId: str(message.thread_id) } : {},
@@ -145221,6 +145257,114 @@ function createFeishuSender(client, log2, options2 = {}) {
145221
145257
  }
145222
145258
  };
145223
145259
  }
145260
+ var AttachmentTimeoutError = class extends Error {
145261
+ constructor() {
145262
+ super("attachment-timeout");
145263
+ this.name = "AttachmentTimeoutError";
145264
+ }
145265
+ };
145266
+ function withTimeout(promise3, ms) {
145267
+ return new Promise((resolve4, reject) => {
145268
+ const timer = setTimeout(() => reject(new AttachmentTimeoutError()), Math.max(1, ms));
145269
+ timer.unref?.();
145270
+ promise3.then(
145271
+ (value3) => {
145272
+ clearTimeout(timer);
145273
+ resolve4(value3);
145274
+ },
145275
+ (err2) => {
145276
+ clearTimeout(timer);
145277
+ reject(err2);
145278
+ }
145279
+ );
145280
+ });
145281
+ }
145282
+ function sanitizeAttachmentName(raw, fallback = "attachment") {
145283
+ const base2 = (raw ?? "").trim();
145284
+ const cleaned = base2.replace(/[\\/]/g, "_").replace(/[\u0000-\u001f\u007f]/g, "").replace(/^\.+/, "").slice(0, 120);
145285
+ return cleaned || fallback;
145286
+ }
145287
+ var IMAGE_EXT_BY_MIME = {
145288
+ "image/png": ".png",
145289
+ "image/jpeg": ".jpg",
145290
+ "image/jpg": ".jpg",
145291
+ "image/gif": ".gif",
145292
+ "image/webp": ".webp",
145293
+ "image/bmp": ".bmp",
145294
+ "image/tiff": ".tiff"
145295
+ };
145296
+ function imageExtFromHeaders(headers) {
145297
+ const raw = headers?.["content-type"] ?? headers?.["Content-Type"];
145298
+ const mime = typeof raw === "string" ? raw.split(";")[0].trim().toLowerCase() : "";
145299
+ return IMAGE_EXT_BY_MIME[mime] ?? ".png";
145300
+ }
145301
+ function buildAttachmentFileName(attachment, headers) {
145302
+ if (attachment.kind === "image") {
145303
+ return sanitizeAttachmentName(`image${imageExtFromHeaders(headers)}`, "image.png");
145304
+ }
145305
+ const raw = sanitizeAttachmentName(attachment.fileName, "file");
145306
+ return raw;
145307
+ }
145308
+ function formatBytes(bytes) {
145309
+ if (!Number.isFinite(bytes) || bytes < 0) return "0 B";
145310
+ if (bytes < 1024) return `${bytes} B`;
145311
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
145312
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
145313
+ }
145314
+ function attachmentSavedPrompt(input) {
145315
+ const size6 = formatBytes(input.size);
145316
+ return input.kind === "image" ? `[\u9644\u4EF6] \u56FE\u7247\u300C${input.name}\u300D\u5DF2\u4FDD\u5B58\u5230 ${input.path}\uFF08${size6}\uFF09\uFF0C\u53EF\u76F4\u63A5\u67E5\u770B\u3002` : `[\u9644\u4EF6] \u6587\u4EF6\u300C${input.name}\u300D\u5DF2\u4FDD\u5B58\u5230 ${input.path}\uFF08${size6}\uFF09\u3002`;
145317
+ }
145318
+ async function downloadAttachment(input) {
145319
+ const { client, messageId, attachment, dir, maxBytes, timeoutMs, log: log2 } = input;
145320
+ const startedAt = Date.now();
145321
+ let target;
145322
+ try {
145323
+ await mkdir(dir, { recursive: true });
145324
+ const res = await withTimeout(
145325
+ client.im.messageResource.get({
145326
+ params: { type: attachment.kind },
145327
+ path: { message_id: messageId, file_key: attachment.fileKey }
145328
+ }),
145329
+ timeoutMs
145330
+ );
145331
+ if (!res || typeof res.writeFile !== "function") {
145332
+ return { ok: false, reason: "\u8D44\u6E90\u63A5\u53E3\u672A\u8FD4\u56DE\u6587\u4EF6\u6D41" };
145333
+ }
145334
+ const name = buildAttachmentFileName(attachment, res.headers);
145335
+ target = join(dir, `${messageId}-${name}`);
145336
+ await res.writeFile(target);
145337
+ const info = await stat(target);
145338
+ if (info.size > maxBytes) {
145339
+ await rm(target, { force: true });
145340
+ return {
145341
+ ok: false,
145342
+ reason: `\u9644\u4EF6\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(info.size)} > ${formatBytes(maxBytes)}\uFF09`
145343
+ };
145344
+ }
145345
+ log2.info("\u9644\u4EF6\u5DF2\u4E0B\u8F7D", {
145346
+ messageId,
145347
+ kind: attachment.kind,
145348
+ name,
145349
+ size: info.size,
145350
+ ms: Date.now() - startedAt
145351
+ });
145352
+ return { ok: true, path: target, name, size: info.size };
145353
+ } catch (err2) {
145354
+ if (target) await rm(target, { force: true }).catch(() => void 0);
145355
+ const reason = err2 instanceof AttachmentTimeoutError ? `\u4E0B\u8F7D\u8D85\u65F6\uFF08>${Math.round(timeoutMs / 1e3)}s\uFF09` : errorMessage(err2);
145356
+ log2.warn("\u9644\u4EF6\u4E0B\u8F7D\u5931\u8D25", { messageId, kind: attachment.kind, error: reason });
145357
+ return { ok: false, reason };
145358
+ }
145359
+ }
145360
+ function downloadedAttachmentPrompt(attachment, outcome) {
145361
+ return attachmentSavedPrompt({
145362
+ kind: attachment.kind,
145363
+ name: outcome.name,
145364
+ path: outcome.path,
145365
+ size: outcome.size
145366
+ });
145367
+ }
145224
145368
 
145225
145369
  // src/feishu/session-map.ts
145226
145370
  var CHAT_KEY_PREFIX = "feishu:v2:chat:";
@@ -148237,9 +148381,9 @@ function validateDirectory(rawPath, allowedRoots, deps = {}) {
148237
148381
  if (!isAbsolute(raw)) return err("not_absolute", "\u76EE\u5F55\u5FC5\u987B\u662F**\u7EDD\u5BF9\u8DEF\u5F84**\uFF08\u4EE5 `/` \u5F00\u5934\uFF09\u3002");
148238
148382
  target = resolve$1(raw);
148239
148383
  }
148240
- const stat = deps.statSync ?? statSync;
148384
+ const stat2 = deps.statSync ?? statSync;
148241
148385
  const realpath = deps.realpathSync ?? realpathSync;
148242
- const mkdir = deps.mkdirSync ?? mkdirSync;
148386
+ const mkdir2 = deps.mkdirSync ?? mkdirSync;
148243
148387
  const blocked = forbiddenReason(target);
148244
148388
  if (blocked) return blocked;
148245
148389
  if (!roots.some((root) => isUnder(target, root))) {
@@ -148247,13 +148391,13 @@ function validateDirectory(rawPath, allowedRoots, deps = {}) {
148247
148391
  }
148248
148392
  let exists2 = true;
148249
148393
  try {
148250
- if (!stat(target).isDirectory()) return err("not_dir", `\u4E0D\u662F\u76EE\u5F55\uFF1A\`${target}\``);
148394
+ if (!stat2(target).isDirectory()) return err("not_dir", `\u4E0D\u662F\u76EE\u5F55\uFF1A\`${target}\``);
148251
148395
  } catch {
148252
148396
  exists2 = false;
148253
148397
  }
148254
148398
  if (!exists2) {
148255
148399
  try {
148256
- mkdir(target, { recursive: true });
148400
+ mkdir2(target, { recursive: true });
148257
148401
  } catch {
148258
148402
  return err("create_failed", `\u76EE\u5F55\u4E0D\u5B58\u5728\u4E14\u65E0\u6CD5\u521B\u5EFA\uFF1A\`${target}\`\u3002\u8BF7\u68C0\u67E5\u8DEF\u5F84\u4E0E\u6743\u9650\u3002`);
148259
148403
  }
@@ -150731,7 +150875,7 @@ var SummaryTimeoutError = class extends Error {
150731
150875
  this.name = "SummaryTimeoutError";
150732
150876
  }
150733
150877
  };
150734
- function withTimeout(promise3, ms) {
150878
+ function withTimeout2(promise3, ms) {
150735
150879
  return new Promise((resolve4, reject) => {
150736
150880
  const timer = setTimeout(() => reject(new SummaryTimeoutError()), Math.max(1, ms));
150737
150881
  timer.unref?.();
@@ -150814,7 +150958,7 @@ async function summarizeSession(deps, input) {
150814
150958
  let messages;
150815
150959
  if (deps.readMessages) {
150816
150960
  try {
150817
- messages = await withTimeout(deps.readMessages(input.sessionID, input.directory), timeoutMs);
150961
+ messages = await withTimeout2(deps.readMessages(input.sessionID, input.directory), timeoutMs);
150818
150962
  const reused = extractLatestSummary(messages);
150819
150963
  if (reused) return { summary: reused, source: "reused" };
150820
150964
  } catch (err2) {
@@ -150827,7 +150971,7 @@ async function summarizeSession(deps, input) {
150827
150971
  if (!deps.generateText) return { source: "none" };
150828
150972
  try {
150829
150973
  const transcript = buildTranscript(messages);
150830
- const raw = await withTimeout(
150974
+ const raw = await withTimeout2(
150831
150975
  deps.generateText(buildSummaryPrompt(transcript), input.directory),
150832
150976
  timeoutMs
150833
150977
  );
@@ -151827,8 +151971,31 @@ async function start(ctx, config, log2, logSink) {
151827
151971
  ...model ? { model } : {}
151828
151972
  });
151829
151973
  if (!receipt.ok) log2.warn("\u56DE\u6267\u5361\u672A\u53D1\u9001\uFF0C\u4ECD\u7EE7\u7EED prompt", { sessionID, delivery });
151974
+ let promptText = message.text;
151975
+ const files = [];
151976
+ if (config.acceptAttachments && message.attachment) {
151977
+ const outcome = await downloadAttachment({
151978
+ client,
151979
+ messageId: message.messageId,
151980
+ attachment: message.attachment,
151981
+ dir: config.attachmentsDir,
151982
+ maxBytes: config.attachmentMaxBytes,
151983
+ timeoutMs: config.attachmentTimeoutMs,
151984
+ log: log2
151985
+ });
151986
+ if (outcome.ok) {
151987
+ files.push({ uri: pathToFileURL(outcome.path).href });
151988
+ promptText = `${promptText}
151989
+
151990
+ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
151991
+ } else {
151992
+ promptText = `${promptText}
151993
+
151994
+ [\u9644\u4EF6] \u4E0B\u8F7D\u5931\u8D25\uFF1A${outcome.reason}`;
151995
+ }
151996
+ }
151830
151997
  try {
151831
- await promptSession(ctx, sessionID, message.text, delivery);
151998
+ await promptSession(ctx, sessionID, promptText, delivery, files);
151832
151999
  } catch (err2) {
151833
152000
  log2.warn("prompt \u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
151834
152001
  runs.apply(sessionID, { type: "execution.failed", error: errorMessage(err2) });
@@ -152066,9 +152233,14 @@ async function replyPermission(ctx, input) {
152066
152233
  throw err2;
152067
152234
  }
152068
152235
  }
152069
- async function promptSession(ctx, sessionID, text, delivery) {
152236
+ async function promptSession(ctx, sessionID, text, delivery, files) {
152070
152237
  const api = ctx.session.prompt;
152071
- await api({ sessionID, text, delivery });
152238
+ await api({
152239
+ sessionID,
152240
+ text,
152241
+ delivery,
152242
+ ...files && files.length > 0 ? { files } : {}
152243
+ });
152072
152244
  }
152073
152245
  async function readSessionMessages(ctx, sessionID, directory, log2) {
152074
152246
  const options2 = directory ? { headers: { "x-opencode-directory": directory } } : void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-feishu-plugin",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Feishu/Lark plugin for OpenCode V2 — single-user topics as sessions, in-card permission approvals, streaming cards. Minimal permissions, no public endpoint.",
5
5
  "type": "module",
6
6
  "main": "index.js",