opencode-feishu-plugin 0.2.8 → 0.2.10
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 +7 -3
- package/README.md +7 -3
- package/dist/index.js +205 -19
- package/package.json +1 -1
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
|
|
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
|
-
-
|
|
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
|
-
- [
|
|
185
|
+
- [x] **Accept images / files**: done — downloaded automatically into the session working directory at `.opencode/temp/opencode-feishu-plugin/` (with a built-in `.gitignore`, so `git status` stays clean; override with `attachmentsDir`; 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.
|
|
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] **接收图片 / 文件**:已支持——自动下载到**会话工作目录**下的 `.opencode/temp/opencode-feishu-plugin/`(内置 `.gitignore`,不污染 `git status`;可用 `attachmentsDir` 覆盖;单附件默认 ≤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,
|
|
7
|
+
import path, { dirname, isAbsolute, sep, join, 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
|
-
import { homedir } from 'os';
|
|
14
|
-
import { readFile, readdir } from 'fs/promises';
|
|
13
|
+
import { homedir, tmpdir } from 'os';
|
|
14
|
+
import { mkdir, stat, rm, writeFile, 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
|
|
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 =
|
|
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,
|
|
10043
|
+
fs2.stat(value3.path, function(err2, stat2) {
|
|
10044
10044
|
if (err2) {
|
|
10045
10045
|
callback3(err2);
|
|
10046
10046
|
return;
|
|
10047
10047
|
}
|
|
10048
|
-
var fileSize =
|
|
10048
|
+
var fileSize = stat2.size - (value3.start ? value3.start : 0);
|
|
10049
10049
|
callback3(null, fileSize);
|
|
10050
10050
|
});
|
|
10051
10051
|
}
|
|
@@ -144029,6 +144029,14 @@ 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();
|
|
144032
144040
|
const keepaliveIntervalMs = clamp4(
|
|
144033
144041
|
asNumber(merged.keepaliveIntervalMs, 20 * 60 * 1e3),
|
|
144034
144042
|
5 * 60 * 1e3,
|
|
@@ -144089,7 +144097,11 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
|
|
|
144089
144097
|
runnerCardMaxTools,
|
|
144090
144098
|
runnerCardTextMax,
|
|
144091
144099
|
finalAnswerMinChars,
|
|
144092
|
-
finalAnswerFileMinBytes
|
|
144100
|
+
finalAnswerFileMinBytes,
|
|
144101
|
+
acceptAttachments,
|
|
144102
|
+
attachmentMaxBytes,
|
|
144103
|
+
attachmentTimeoutMs,
|
|
144104
|
+
...attachmentsDirRaw ? { attachmentsDir: attachmentsDirRaw } : {}
|
|
144093
144105
|
};
|
|
144094
144106
|
}
|
|
144095
144107
|
function loadConfigFile(env, deps, warnings) {
|
|
@@ -144706,6 +144718,27 @@ function extractPostText(rawContent) {
|
|
|
144706
144718
|
return "";
|
|
144707
144719
|
}
|
|
144708
144720
|
}
|
|
144721
|
+
function parseIncomingAttachment(messageType, rawContent) {
|
|
144722
|
+
if (messageType !== "image" && messageType !== "file") return void 0;
|
|
144723
|
+
let parsed;
|
|
144724
|
+
try {
|
|
144725
|
+
parsed = JSON.parse(rawContent);
|
|
144726
|
+
} catch {
|
|
144727
|
+
return void 0;
|
|
144728
|
+
}
|
|
144729
|
+
if (!isRecordLike(parsed)) return void 0;
|
|
144730
|
+
if (messageType === "image") {
|
|
144731
|
+
const imageKey = str(parsed.image_key);
|
|
144732
|
+
return imageKey ? { kind: "image", fileKey: imageKey } : void 0;
|
|
144733
|
+
}
|
|
144734
|
+
const fileKey = str(parsed.file_key);
|
|
144735
|
+
if (!fileKey) return void 0;
|
|
144736
|
+
const fileName = str(parsed.file_name);
|
|
144737
|
+
return { kind: "file", fileKey, ...fileName ? { fileName } : {} };
|
|
144738
|
+
}
|
|
144739
|
+
function isRecordLike(value3) {
|
|
144740
|
+
return typeof value3 === "object" && value3 !== null && !Array.isArray(value3);
|
|
144741
|
+
}
|
|
144709
144742
|
function parseIncomingMessage(data) {
|
|
144710
144743
|
if (!isRecord2(data)) return void 0;
|
|
144711
144744
|
const message = data.message;
|
|
@@ -144718,6 +144751,7 @@ function parseIncomingMessage(data) {
|
|
|
144718
144751
|
if (!rawContent) return void 0;
|
|
144719
144752
|
const sender = data.sender;
|
|
144720
144753
|
const senderOpenId = isRecord2(sender) && isRecord2(sender.sender_id) ? str(sender.sender_id.open_id) : "";
|
|
144754
|
+
const attachment = parseIncomingAttachment(messageType, rawContent);
|
|
144721
144755
|
return {
|
|
144722
144756
|
eventId: str(data.event_id),
|
|
144723
144757
|
messageId,
|
|
@@ -144725,6 +144759,7 @@ function parseIncomingMessage(data) {
|
|
|
144725
144759
|
chatType: str(message.chat_type) || "p2p",
|
|
144726
144760
|
messageType,
|
|
144727
144761
|
text: extractMessageText(messageType, rawContent),
|
|
144762
|
+
...attachment ? { attachment } : {},
|
|
144728
144763
|
senderOpenId,
|
|
144729
144764
|
...str(message.create_time) ? { createTime: str(message.create_time) } : {},
|
|
144730
144765
|
...str(message.thread_id) ? { threadId: str(message.thread_id) } : {},
|
|
@@ -145221,6 +145256,127 @@ function createFeishuSender(client, log2, options2 = {}) {
|
|
|
145221
145256
|
}
|
|
145222
145257
|
};
|
|
145223
145258
|
}
|
|
145259
|
+
var ATTACHMENT_DIR_RELATIVE = [".opencode", "temp", "opencode-feishu-plugin"];
|
|
145260
|
+
function resolveAttachmentDir(override, sessionDir) {
|
|
145261
|
+
if (override && override.trim()) return override.trim();
|
|
145262
|
+
if (sessionDir && sessionDir.trim()) return join(sessionDir.trim(), ...ATTACHMENT_DIR_RELATIVE);
|
|
145263
|
+
return join(tmpdir(), "opencode-feishu-plugin");
|
|
145264
|
+
}
|
|
145265
|
+
async function ensureAttachmentDirGitIgnored(dir) {
|
|
145266
|
+
try {
|
|
145267
|
+
await writeFile(join(dir, ".gitignore"), "*\n", { flag: "wx" });
|
|
145268
|
+
} catch {
|
|
145269
|
+
}
|
|
145270
|
+
}
|
|
145271
|
+
var AttachmentTimeoutError = class extends Error {
|
|
145272
|
+
constructor() {
|
|
145273
|
+
super("attachment-timeout");
|
|
145274
|
+
this.name = "AttachmentTimeoutError";
|
|
145275
|
+
}
|
|
145276
|
+
};
|
|
145277
|
+
function withTimeout(promise3, ms) {
|
|
145278
|
+
return new Promise((resolve4, reject) => {
|
|
145279
|
+
const timer = setTimeout(() => reject(new AttachmentTimeoutError()), Math.max(1, ms));
|
|
145280
|
+
timer.unref?.();
|
|
145281
|
+
promise3.then(
|
|
145282
|
+
(value3) => {
|
|
145283
|
+
clearTimeout(timer);
|
|
145284
|
+
resolve4(value3);
|
|
145285
|
+
},
|
|
145286
|
+
(err2) => {
|
|
145287
|
+
clearTimeout(timer);
|
|
145288
|
+
reject(err2);
|
|
145289
|
+
}
|
|
145290
|
+
);
|
|
145291
|
+
});
|
|
145292
|
+
}
|
|
145293
|
+
function sanitizeAttachmentName(raw, fallback = "attachment") {
|
|
145294
|
+
const base2 = (raw ?? "").trim();
|
|
145295
|
+
const cleaned = base2.replace(/[\\/]/g, "_").replace(/[\u0000-\u001f\u007f]/g, "").replace(/^\.+/, "").slice(0, 120);
|
|
145296
|
+
return cleaned || fallback;
|
|
145297
|
+
}
|
|
145298
|
+
var IMAGE_EXT_BY_MIME = {
|
|
145299
|
+
"image/png": ".png",
|
|
145300
|
+
"image/jpeg": ".jpg",
|
|
145301
|
+
"image/jpg": ".jpg",
|
|
145302
|
+
"image/gif": ".gif",
|
|
145303
|
+
"image/webp": ".webp",
|
|
145304
|
+
"image/bmp": ".bmp",
|
|
145305
|
+
"image/tiff": ".tiff"
|
|
145306
|
+
};
|
|
145307
|
+
function imageExtFromHeaders(headers) {
|
|
145308
|
+
const raw = headers?.["content-type"] ?? headers?.["Content-Type"];
|
|
145309
|
+
const mime = typeof raw === "string" ? raw.split(";")[0].trim().toLowerCase() : "";
|
|
145310
|
+
return IMAGE_EXT_BY_MIME[mime] ?? ".png";
|
|
145311
|
+
}
|
|
145312
|
+
function buildAttachmentFileName(attachment, headers) {
|
|
145313
|
+
if (attachment.kind === "image") {
|
|
145314
|
+
return sanitizeAttachmentName(`image${imageExtFromHeaders(headers)}`, "image.png");
|
|
145315
|
+
}
|
|
145316
|
+
const raw = sanitizeAttachmentName(attachment.fileName, "file");
|
|
145317
|
+
return raw;
|
|
145318
|
+
}
|
|
145319
|
+
function formatBytes(bytes) {
|
|
145320
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "0 B";
|
|
145321
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
145322
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
145323
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
145324
|
+
}
|
|
145325
|
+
function attachmentSavedPrompt(input) {
|
|
145326
|
+
const size6 = formatBytes(input.size);
|
|
145327
|
+
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`;
|
|
145328
|
+
}
|
|
145329
|
+
async function downloadAttachment(input) {
|
|
145330
|
+
const { client, messageId, attachment, dir, maxBytes, timeoutMs, log: log2, gitIgnore } = input;
|
|
145331
|
+
const startedAt = Date.now();
|
|
145332
|
+
let target;
|
|
145333
|
+
try {
|
|
145334
|
+
await mkdir(dir, { recursive: true });
|
|
145335
|
+
if (gitIgnore) await ensureAttachmentDirGitIgnored(dir);
|
|
145336
|
+
const res = await withTimeout(
|
|
145337
|
+
client.im.messageResource.get({
|
|
145338
|
+
params: { type: attachment.kind },
|
|
145339
|
+
path: { message_id: messageId, file_key: attachment.fileKey }
|
|
145340
|
+
}),
|
|
145341
|
+
timeoutMs
|
|
145342
|
+
);
|
|
145343
|
+
if (!res || typeof res.writeFile !== "function") {
|
|
145344
|
+
return { ok: false, reason: "\u8D44\u6E90\u63A5\u53E3\u672A\u8FD4\u56DE\u6587\u4EF6\u6D41" };
|
|
145345
|
+
}
|
|
145346
|
+
const name = buildAttachmentFileName(attachment, res.headers);
|
|
145347
|
+
target = join(dir, `${messageId}-${name}`);
|
|
145348
|
+
await res.writeFile(target);
|
|
145349
|
+
const info = await stat(target);
|
|
145350
|
+
if (info.size > maxBytes) {
|
|
145351
|
+
await rm(target, { force: true });
|
|
145352
|
+
return {
|
|
145353
|
+
ok: false,
|
|
145354
|
+
reason: `\u9644\u4EF6\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(info.size)} > ${formatBytes(maxBytes)}\uFF09`
|
|
145355
|
+
};
|
|
145356
|
+
}
|
|
145357
|
+
log2.info("\u9644\u4EF6\u5DF2\u4E0B\u8F7D", {
|
|
145358
|
+
messageId,
|
|
145359
|
+
kind: attachment.kind,
|
|
145360
|
+
name,
|
|
145361
|
+
size: info.size,
|
|
145362
|
+
ms: Date.now() - startedAt
|
|
145363
|
+
});
|
|
145364
|
+
return { ok: true, path: target, name, size: info.size };
|
|
145365
|
+
} catch (err2) {
|
|
145366
|
+
if (target) await rm(target, { force: true }).catch(() => void 0);
|
|
145367
|
+
const reason = err2 instanceof AttachmentTimeoutError ? `\u4E0B\u8F7D\u8D85\u65F6\uFF08>${Math.round(timeoutMs / 1e3)}s\uFF09` : errorMessage(err2);
|
|
145368
|
+
log2.warn("\u9644\u4EF6\u4E0B\u8F7D\u5931\u8D25", { messageId, kind: attachment.kind, error: reason });
|
|
145369
|
+
return { ok: false, reason };
|
|
145370
|
+
}
|
|
145371
|
+
}
|
|
145372
|
+
function downloadedAttachmentPrompt(attachment, outcome) {
|
|
145373
|
+
return attachmentSavedPrompt({
|
|
145374
|
+
kind: attachment.kind,
|
|
145375
|
+
name: outcome.name,
|
|
145376
|
+
path: outcome.path,
|
|
145377
|
+
size: outcome.size
|
|
145378
|
+
});
|
|
145379
|
+
}
|
|
145224
145380
|
|
|
145225
145381
|
// src/feishu/session-map.ts
|
|
145226
145382
|
var CHAT_KEY_PREFIX = "feishu:v2:chat:";
|
|
@@ -148237,9 +148393,9 @@ function validateDirectory(rawPath, allowedRoots, deps = {}) {
|
|
|
148237
148393
|
if (!isAbsolute(raw)) return err("not_absolute", "\u76EE\u5F55\u5FC5\u987B\u662F**\u7EDD\u5BF9\u8DEF\u5F84**\uFF08\u4EE5 `/` \u5F00\u5934\uFF09\u3002");
|
|
148238
148394
|
target = resolve$1(raw);
|
|
148239
148395
|
}
|
|
148240
|
-
const
|
|
148396
|
+
const stat2 = deps.statSync ?? statSync;
|
|
148241
148397
|
const realpath = deps.realpathSync ?? realpathSync;
|
|
148242
|
-
const
|
|
148398
|
+
const mkdir2 = deps.mkdirSync ?? mkdirSync;
|
|
148243
148399
|
const blocked = forbiddenReason(target);
|
|
148244
148400
|
if (blocked) return blocked;
|
|
148245
148401
|
if (!roots.some((root) => isUnder(target, root))) {
|
|
@@ -148247,13 +148403,13 @@ function validateDirectory(rawPath, allowedRoots, deps = {}) {
|
|
|
148247
148403
|
}
|
|
148248
148404
|
let exists2 = true;
|
|
148249
148405
|
try {
|
|
148250
|
-
if (!
|
|
148406
|
+
if (!stat2(target).isDirectory()) return err("not_dir", `\u4E0D\u662F\u76EE\u5F55\uFF1A\`${target}\``);
|
|
148251
148407
|
} catch {
|
|
148252
148408
|
exists2 = false;
|
|
148253
148409
|
}
|
|
148254
148410
|
if (!exists2) {
|
|
148255
148411
|
try {
|
|
148256
|
-
|
|
148412
|
+
mkdir2(target, { recursive: true });
|
|
148257
148413
|
} catch {
|
|
148258
148414
|
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
148415
|
}
|
|
@@ -150731,7 +150887,7 @@ var SummaryTimeoutError = class extends Error {
|
|
|
150731
150887
|
this.name = "SummaryTimeoutError";
|
|
150732
150888
|
}
|
|
150733
150889
|
};
|
|
150734
|
-
function
|
|
150890
|
+
function withTimeout2(promise3, ms) {
|
|
150735
150891
|
return new Promise((resolve4, reject) => {
|
|
150736
150892
|
const timer = setTimeout(() => reject(new SummaryTimeoutError()), Math.max(1, ms));
|
|
150737
150893
|
timer.unref?.();
|
|
@@ -150814,7 +150970,7 @@ async function summarizeSession(deps, input) {
|
|
|
150814
150970
|
let messages;
|
|
150815
150971
|
if (deps.readMessages) {
|
|
150816
150972
|
try {
|
|
150817
|
-
messages = await
|
|
150973
|
+
messages = await withTimeout2(deps.readMessages(input.sessionID, input.directory), timeoutMs);
|
|
150818
150974
|
const reused = extractLatestSummary(messages);
|
|
150819
150975
|
if (reused) return { summary: reused, source: "reused" };
|
|
150820
150976
|
} catch (err2) {
|
|
@@ -150827,7 +150983,7 @@ async function summarizeSession(deps, input) {
|
|
|
150827
150983
|
if (!deps.generateText) return { source: "none" };
|
|
150828
150984
|
try {
|
|
150829
150985
|
const transcript = buildTranscript(messages);
|
|
150830
|
-
const raw = await
|
|
150986
|
+
const raw = await withTimeout2(
|
|
150831
150987
|
deps.generateText(buildSummaryPrompt(transcript), input.directory),
|
|
150832
150988
|
timeoutMs
|
|
150833
150989
|
);
|
|
@@ -151827,8 +151983,33 @@ async function start(ctx, config, log2, logSink) {
|
|
|
151827
151983
|
...model ? { model } : {}
|
|
151828
151984
|
});
|
|
151829
151985
|
if (!receipt.ok) log2.warn("\u56DE\u6267\u5361\u672A\u53D1\u9001\uFF0C\u4ECD\u7EE7\u7EED prompt", { sessionID, delivery });
|
|
151986
|
+
let promptText = message.text;
|
|
151987
|
+
const files = [];
|
|
151988
|
+
if (config.acceptAttachments && message.attachment) {
|
|
151989
|
+
const outcome = await downloadAttachment({
|
|
151990
|
+
client,
|
|
151991
|
+
messageId: message.messageId,
|
|
151992
|
+
attachment: message.attachment,
|
|
151993
|
+
dir: resolveAttachmentDir(config.attachmentsDir, link2?.dir),
|
|
151994
|
+
maxBytes: config.attachmentMaxBytes,
|
|
151995
|
+
timeoutMs: config.attachmentTimeoutMs,
|
|
151996
|
+
log: log2,
|
|
151997
|
+
// 显式配置的目录不动;默认的会话内目录顺手放 `.gitignore`,避免污染 git status。
|
|
151998
|
+
...config.attachmentsDir ? {} : { gitIgnore: true }
|
|
151999
|
+
});
|
|
152000
|
+
if (outcome.ok) {
|
|
152001
|
+
files.push({ uri: pathToFileURL(outcome.path).href });
|
|
152002
|
+
promptText = `${promptText}
|
|
152003
|
+
|
|
152004
|
+
${downloadedAttachmentPrompt(message.attachment, outcome)}`;
|
|
152005
|
+
} else {
|
|
152006
|
+
promptText = `${promptText}
|
|
152007
|
+
|
|
152008
|
+
[\u9644\u4EF6] \u4E0B\u8F7D\u5931\u8D25\uFF1A${outcome.reason}`;
|
|
152009
|
+
}
|
|
152010
|
+
}
|
|
151830
152011
|
try {
|
|
151831
|
-
await promptSession(ctx, sessionID,
|
|
152012
|
+
await promptSession(ctx, sessionID, promptText, delivery, files);
|
|
151832
152013
|
} catch (err2) {
|
|
151833
152014
|
log2.warn("prompt \u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
|
|
151834
152015
|
runs.apply(sessionID, { type: "execution.failed", error: errorMessage(err2) });
|
|
@@ -152066,9 +152247,14 @@ async function replyPermission(ctx, input) {
|
|
|
152066
152247
|
throw err2;
|
|
152067
152248
|
}
|
|
152068
152249
|
}
|
|
152069
|
-
async function promptSession(ctx, sessionID, text, delivery) {
|
|
152250
|
+
async function promptSession(ctx, sessionID, text, delivery, files) {
|
|
152070
152251
|
const api = ctx.session.prompt;
|
|
152071
|
-
await api({
|
|
152252
|
+
await api({
|
|
152253
|
+
sessionID,
|
|
152254
|
+
text,
|
|
152255
|
+
delivery,
|
|
152256
|
+
...files && files.length > 0 ? { files } : {}
|
|
152257
|
+
});
|
|
152072
152258
|
}
|
|
152073
152259
|
async function readSessionMessages(ctx, sessionID, directory, log2) {
|
|
152074
152260
|
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.
|
|
3
|
+
"version": "0.2.10",
|
|
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",
|