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 +7 -3
- package/README.md +7 -3
- package/dist/index.js +190 -18
- 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 `<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.
|
|
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,
|
|
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
|
|
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,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
|
|
148384
|
+
const stat2 = deps.statSync ?? statSync;
|
|
148241
148385
|
const realpath = deps.realpathSync ?? realpathSync;
|
|
148242
|
-
const
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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({
|
|
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.
|
|
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",
|