opencode-feishu-plugin 0.2.7 → 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.
Files changed (4) hide show
  1. package/README.en.md +114 -457
  2. package/README.md +82 -268
  3. package/dist/index.js +296 -41
  4. package/package.json +1 -1
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:";
@@ -145598,7 +145742,8 @@ function parseRootCard(value3) {
145598
145742
  ...value3.compactPending === true ? { compactPending: true } : {},
145599
145743
  ...compactError ? { compactError } : {},
145600
145744
  ...value3.compactButton === true ? { compactButton: true } : {},
145601
- ...note2 ? { note: note2 } : {}
145745
+ ...note2 ? { note: note2 } : {},
145746
+ ...value3.openedTopic === true ? { openedTopic: true } : {}
145602
145747
  };
145603
145748
  }
145604
145749
  function parseStringArray(value3) {
@@ -148083,7 +148228,10 @@ function renderResumedBody(title, base2, now3) {
148083
148228
  setup.push(`- \u6700\u8FD1\u6D3B\u52A8\uFF1A${relativeTime(base2.updatedAt, now3)}`);
148084
148229
  }
148085
148230
  if (setup.length > 0) lines.push("", ...setup);
148086
- lines.push("", "**\u56DE\u590D\u672C\u5361\u7247**\u5373\u53EF\u7EE7\u7EED\u8FD9\u4E2A\u5386\u53F2\u4F1A\u8BDD\uFF08\u98DE\u4E66\u56DE\u590D\u4F1A\u5728\u672C\u5361\u4E0B\u5F62\u6210\u8BDD\u9898\uFF09\u3002");
148231
+ lines.push(
148232
+ "",
148233
+ base2.openedTopic ? "\u2705 **\u8BDD\u9898\u5DF2\u6253\u5F00**\uFF1A\u76F4\u63A5\u5728\u672C\u8BDD\u9898\u5185\u56DE\u590D\u5373\u53EF\u7EE7\u7EED\u8FD9\u4E2A\u5386\u53F2\u4F1A\u8BDD\u3002" : "**\u56DE\u590D\u672C\u5361\u7247**\u5373\u53EF\u7EE7\u7EED\u8FD9\u4E2A\u5386\u53F2\u4F1A\u8BDD\uFF08\u98DE\u4E66\u56DE\u590D\u4F1A\u5728\u672C\u5361\u4E0B\u5F62\u6210\u8BDD\u9898\uFF09\u3002"
148234
+ );
148087
148235
  if (base2.summary || base2.summaryPending || base2.compactPending || base2.compactError) {
148088
148236
  lines.push("", `**${base2.summaryLabel ?? "\u6458\u8981"}**\uFF1A`);
148089
148237
  if (base2.summaryPending) lines.push("\u23F3 \u6B63\u5728\u603B\u7ED3\u8BE5\u4F1A\u8BDD\u2026");
@@ -148122,7 +148270,8 @@ function buildSessionOpenedCard(input) {
148122
148270
  ...input.summaryPending ? { summaryPending: true } : {},
148123
148271
  ...input.compactPending ? { compactPending: true } : {},
148124
148272
  ...input.compactError ? { compactError: input.compactError } : {},
148125
- ...input.compactButton ? { compactButton: true } : {}
148273
+ ...input.compactButton ? { compactButton: true } : {},
148274
+ ...input.openedTopic ? { openedTopic: true } : {}
148126
148275
  };
148127
148276
  return buildSessionRootCard(base2, void 0, {
148128
148277
  now: input.now ?? Date.now(),
@@ -148232,9 +148381,9 @@ function validateDirectory(rawPath, allowedRoots, deps = {}) {
148232
148381
  if (!isAbsolute(raw)) return err("not_absolute", "\u76EE\u5F55\u5FC5\u987B\u662F**\u7EDD\u5BF9\u8DEF\u5F84**\uFF08\u4EE5 `/` \u5F00\u5934\uFF09\u3002");
148233
148382
  target = resolve$1(raw);
148234
148383
  }
148235
- const stat = deps.statSync ?? statSync;
148384
+ const stat2 = deps.statSync ?? statSync;
148236
148385
  const realpath = deps.realpathSync ?? realpathSync;
148237
- const mkdir = deps.mkdirSync ?? mkdirSync;
148386
+ const mkdir2 = deps.mkdirSync ?? mkdirSync;
148238
148387
  const blocked = forbiddenReason(target);
148239
148388
  if (blocked) return blocked;
148240
148389
  if (!roots.some((root) => isUnder(target, root))) {
@@ -148242,13 +148391,13 @@ function validateDirectory(rawPath, allowedRoots, deps = {}) {
148242
148391
  }
148243
148392
  let exists2 = true;
148244
148393
  try {
148245
- 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}\``);
148246
148395
  } catch {
148247
148396
  exists2 = false;
148248
148397
  }
148249
148398
  if (!exists2) {
148250
148399
  try {
148251
- mkdir(target, { recursive: true });
148400
+ mkdir2(target, { recursive: true });
148252
148401
  } catch {
148253
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`);
148254
148403
  }
@@ -150046,21 +150195,20 @@ async function enterSessionThread(ctx, input) {
150046
150195
  const onLimit = (report) => {
150047
150196
  ctx.deps.log.warn("\u4F1A\u8BDD\u6062\u590D\u5361\u5185\u5BB9\u8D85\u9650\uFF0C\u5DF2\u964D\u7EA7", { sessionID, ...report });
150048
150197
  };
150049
- const cardInput = {
150198
+ const newNow = ctx.now();
150199
+ const baseInput = {
150050
150200
  title: info?.title ?? "",
150051
150201
  sessionID,
150052
150202
  ...dir ? { dir } : {},
150053
150203
  ...link2?.model ? { model: modelLabel(link2.model) } : {},
150054
150204
  ...info?.updatedAt ? { updatedAt: info.updatedAt } : {},
150055
- now: ctx.now(),
150205
+ now: newNow,
150056
150206
  ...ctx.deps.cardMaxTables !== void 0 ? { maxTables: ctx.deps.cardMaxTables } : {},
150057
150207
  onLimit,
150058
- ...showSummary ? { summaryPending: true } : {},
150059
- // 「🗜 压缩并总结」只在开关开启且运行时装配了签名时渲染。
150060
- ...showSummary && ctx.deps.signCompact ? { compactButton: { sessionID, token: ctx.deps.signCompact(sessionID) } } : {}
150208
+ openedTopic: true
150061
150209
  };
150062
- const card = buildSessionOpenedCard(cardInput);
150063
- const res = await ctx.deps.sender.sendCard(chatId, card);
150210
+ const rootCard = buildSessionOpenedCard(baseInput);
150211
+ const res = await ctx.deps.sender.sendCard(chatId, rootCard);
150064
150212
  ctx.deps.log.info("\u53D1\u9001\u4F1A\u8BDD\u6062\u590D\u5361", {
150065
150213
  sessionID,
150066
150214
  source: input.source,
@@ -150082,26 +150230,63 @@ async function enterSessionThread(ctx, input) {
150082
150230
  }
150083
150231
  await ctx.deps.sessionMap.bindRoot(res.messageId, sessionID);
150084
150232
  ctx.deps.log.info("resume card bound root to session", { sessionID, rootId: res.messageId });
150085
- await ctx.deps.sessionMap.setRootCard(sessionID, {
150233
+ const summaryBlock = {
150234
+ ...showSummary ? { summaryPending: true } : {},
150235
+ ...showSummary && ctx.deps.signCompact ? { compactButton: { sessionID, token: ctx.deps.signCompact(sessionID) } } : {}
150236
+ };
150237
+ const openerInput = { ...baseInput, ...summaryBlock };
150238
+ const openerRes = await ctx.deps.sender.replyCard(res.messageId, buildSessionOpenedCard(openerInput), {
150239
+ replyInThread: true
150240
+ });
150241
+ const openerMessageId = openerRes.ok ? openerRes.messageId : void 0;
150242
+ let threadId = openerRes.threadId;
150243
+ if (!threadId && openerMessageId) {
150244
+ threadId = (await ctx.deps.sender.getMessageMeta(openerMessageId))?.threadId;
150245
+ }
150246
+ const rootCardBase = {
150086
150247
  style: "resumed",
150087
150248
  sessionID,
150088
150249
  title: info?.title ?? "",
150089
150250
  ...dir ? { dir } : {},
150090
150251
  ...link2?.model ? { model: modelLabel(link2.model) } : {},
150091
- ...info?.updatedAt ? { updatedAt: info.updatedAt } : {},
150092
- ...showSummary ? { summaryPending: true, compactButton: Boolean(cardInput.compactButton) } : {}
150252
+ ...info?.updatedAt ? { updatedAt: info.updatedAt } : {}
150253
+ };
150254
+ if (threadId) {
150255
+ await ctx.deps.sessionMap.bindThread(threadId, sessionID, chatId, operatorOpenId, res.messageId);
150256
+ ctx.deps.log.info("\u6062\u590D\u4F1A\u8BDD\u5DF2\u81EA\u52A8\u5F00\u8BDD\u9898", {
150257
+ sessionID,
150258
+ threadId,
150259
+ rootMessageId: res.messageId,
150260
+ openerMessageId
150261
+ });
150262
+ await ctx.deps.sessionMap.setRootCard(sessionID, { ...rootCardBase, openedTopic: true });
150263
+ if (showSummary && openerMessageId) {
150264
+ void patchResumeSummary(ctx, sessionID, dir, openerMessageId, openerInput, false);
150265
+ }
150266
+ return { ok: true, threadId, messageId: res.messageId };
150267
+ }
150268
+ ctx.deps.log.warn("\u6062\u590D\u4F1A\u8BDD\u81EA\u52A8\u5F00\u8BDD\u9898\u5931\u8D25\uFF0C\u56DE\u9000\u4E3A\u56DE\u590D\u6839\u5361", {
150269
+ sessionID,
150270
+ openError: openerRes.error ?? "unknown"
150271
+ });
150272
+ const fallbackInput = { ...baseInput, openedTopic: false, ...summaryBlock };
150273
+ await ctx.deps.sender.patchCard(res.messageId, buildSessionOpenedCard(fallbackInput));
150274
+ await ctx.deps.sessionMap.setRootCard(sessionID, {
150275
+ ...rootCardBase,
150276
+ ...showSummary ? { summaryPending: true, compactButton: Boolean(fallbackInput.compactButton) } : {}
150093
150277
  });
150094
150278
  if (showSummary) {
150095
- void patchResumeSummary(ctx, sessionID, dir, res.messageId, cardInput);
150279
+ void patchResumeSummary(ctx, sessionID, dir, res.messageId, fallbackInput, true);
150096
150280
  }
150097
150281
  return { ok: true, messageId: res.messageId };
150098
150282
  }
150099
150283
  var SUMMARY_LABEL = {
150100
150284
  reused: "\u4F1A\u8BDD\u6458\u8981",
150101
150285
  generated: "\u6458\u8981\uFF08\u5FEB\u6458\u8981\uFF09",
150286
+ excerpt: "\u6700\u8FD1\u4F1A\u8BDD\u8BB0\u5F55\uFF08\u622A\u53D6\u6458\u8981\uFF09",
150102
150287
  compacted: "\u5DF2\u538B\u7F29 \xB7 \u4F1A\u8BDD\u6458\u8981"
150103
150288
  };
150104
- async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput) {
150289
+ async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput, persistBase) {
150105
150290
  try {
150106
150291
  const outcome = await ctx.deps.summarizeSession({
150107
150292
  sessionID,
@@ -150110,9 +150295,11 @@ async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput) {
150110
150295
  });
150111
150296
  const summary = outcome.summary ?? "\uFF08\u6458\u8981\u751F\u6210\u5931\u8D25\uFF0C\u53EF\u76F4\u63A5\u53D1\u6D88\u606F\u7EE7\u7EED\uFF09";
150112
150297
  const summaryLabel = SUMMARY_LABEL[outcome.source] ?? "\u6458\u8981";
150113
- const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
150114
- if (base2) {
150115
- await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false, summary, summaryLabel });
150298
+ if (persistBase) {
150299
+ const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
150300
+ if (base2) {
150301
+ await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false, summary, summaryLabel });
150302
+ }
150116
150303
  }
150117
150304
  const res = await ctx.deps.sender.patchCard(
150118
150305
  messageId,
@@ -150688,7 +150875,7 @@ var SummaryTimeoutError = class extends Error {
150688
150875
  this.name = "SummaryTimeoutError";
150689
150876
  }
150690
150877
  };
150691
- function withTimeout(promise3, ms) {
150878
+ function withTimeout2(promise3, ms) {
150692
150879
  return new Promise((resolve4, reject) => {
150693
150880
  const timer = setTimeout(() => reject(new SummaryTimeoutError()), Math.max(1, ms));
150694
150881
  timer.unref?.();
@@ -150752,12 +150939,26 @@ function buildSummaryPrompt(transcript) {
150752
150939
  \u4F1A\u8BDD\u6700\u8FD1\u8BB0\u5F55\uFF1A
150753
150940
  ${transcript}` : RESUME_SUMMARY_PROMPT;
150754
150941
  }
150942
+ function buildExcerptSummary(transcript, maxLines = 5, maxChars = 900) {
150943
+ if (!transcript) return void 0;
150944
+ const lines = transcript.split("\n").filter(Boolean);
150945
+ if (lines.length === 0) return void 0;
150946
+ const out = [];
150947
+ let bytes = 0;
150948
+ for (const line of lines) {
150949
+ const size6 = Buffer.byteLength(line, "utf8");
150950
+ if (out.length >= maxLines || bytes + size6 > maxChars) break;
150951
+ out.push(line);
150952
+ bytes += size6;
150953
+ }
150954
+ return out.length > 0 ? out.join("\n") : void 0;
150955
+ }
150755
150956
  async function summarizeSession(deps, input) {
150756
150957
  const timeoutMs = Math.max(1, Math.floor(input.timeoutMs));
150757
150958
  let messages;
150758
150959
  if (deps.readMessages) {
150759
150960
  try {
150760
- messages = await withTimeout(deps.readMessages(input.sessionID, input.directory), timeoutMs);
150961
+ messages = await withTimeout2(deps.readMessages(input.sessionID, input.directory), timeoutMs);
150761
150962
  const reused = extractLatestSummary(messages);
150762
150963
  if (reused) return { summary: reused, source: "reused" };
150763
150964
  } catch (err2) {
@@ -150770,12 +150971,14 @@ async function summarizeSession(deps, input) {
150770
150971
  if (!deps.generateText) return { source: "none" };
150771
150972
  try {
150772
150973
  const transcript = buildTranscript(messages);
150773
- const raw = await withTimeout(
150974
+ const raw = await withTimeout2(
150774
150975
  deps.generateText(buildSummaryPrompt(transcript), input.directory),
150775
150976
  timeoutMs
150776
150977
  );
150777
150978
  const generated = extractGeneratedText(raw);
150778
150979
  if (generated) return { summary: generated, source: "generated" };
150980
+ const excerpt = buildExcerptSummary(transcript);
150981
+ if (excerpt) return { summary: excerpt, source: "excerpt" };
150779
150982
  return { source: "none", error: "empty-summary" };
150780
150983
  } catch (err2) {
150781
150984
  const error = errorMessage(err2);
@@ -150784,6 +150987,11 @@ async function summarizeSession(deps, input) {
150784
150987
  timedOut: err2 instanceof SummaryTimeoutError,
150785
150988
  error
150786
150989
  });
150990
+ try {
150991
+ const excerpt = buildExcerptSummary(buildTranscript(messages));
150992
+ if (excerpt) return { summary: excerpt, source: "excerpt" };
150993
+ } catch {
150994
+ }
150787
150995
  return { source: "none", error };
150788
150996
  }
150789
150997
  }
@@ -151763,8 +151971,31 @@ async function start(ctx, config, log2, logSink) {
151763
151971
  ...model ? { model } : {}
151764
151972
  });
151765
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
+ }
151766
151997
  try {
151767
- await promptSession(ctx, sessionID, message.text, delivery);
151998
+ await promptSession(ctx, sessionID, promptText, delivery, files);
151768
151999
  } catch (err2) {
151769
152000
  log2.warn("prompt \u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
151770
152001
  runs.apply(sessionID, { type: "execution.failed", error: errorMessage(err2) });
@@ -151788,13 +152019,32 @@ async function start(ctx, config, log2, logSink) {
151788
152019
  })
151789
152020
  });
151790
152021
  const abort = new AbortController();
152022
+ const EVENT_RETRY_BASE_MS = 1e3;
152023
+ const EVENT_RETRY_MAX_MS = 3e4;
152024
+ const sleep3 = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
151791
152025
  const subscription = (async () => {
151792
- try {
151793
- for await (const event of ctx.event.subscribe({ signal: abort.signal })) {
151794
- await handleEvent(event);
152026
+ let attempt = 0;
152027
+ while (!abort.signal.aborted) {
152028
+ try {
152029
+ for await (const event of ctx.event.subscribe({ signal: abort.signal })) {
152030
+ if (attempt > 0) {
152031
+ log2.info("\u4E8B\u4EF6\u8BA2\u9605\u5DF2\u91CD\u8FDE", { attempt });
152032
+ }
152033
+ attempt = 0;
152034
+ await handleEvent(event);
152035
+ }
152036
+ if (abort.signal.aborted) break;
152037
+ attempt += 1;
152038
+ const delay3 = Math.min(EVENT_RETRY_MAX_MS, EVENT_RETRY_BASE_MS * 2 ** Math.min(attempt, 5));
152039
+ log2.warn("\u4E8B\u4EF6\u8BA2\u9605\u6D41\u5DF2\u7ED3\u675F\uFF0C\u51C6\u5907\u91CD\u8FDE", { attempt, delayMs: delay3 });
152040
+ await sleep3(delay3);
152041
+ } catch (err2) {
152042
+ if (abort.signal.aborted) break;
152043
+ attempt += 1;
152044
+ const delay3 = Math.min(EVENT_RETRY_MAX_MS, EVENT_RETRY_BASE_MS * 2 ** Math.min(attempt, 5));
152045
+ log2.error("\u4E8B\u4EF6\u8BA2\u9605\u5F02\u5E38\uFF0C\u51C6\u5907\u91CD\u8FDE", { attempt, delayMs: delay3, error: errorMessage(err2) });
152046
+ await sleep3(delay3);
151795
152047
  }
151796
- } catch (err2) {
151797
- if (!abort.signal.aborted) log2.error("\u4E8B\u4EF6\u8BA2\u9605\u5F02\u5E38\u9000\u51FA", { error: errorMessage(err2) });
151798
152048
  }
151799
152049
  })();
151800
152050
  const eventDeps = {
@@ -151983,9 +152233,14 @@ async function replyPermission(ctx, input) {
151983
152233
  throw err2;
151984
152234
  }
151985
152235
  }
151986
- async function promptSession(ctx, sessionID, text, delivery) {
152236
+ async function promptSession(ctx, sessionID, text, delivery, files) {
151987
152237
  const api = ctx.session.prompt;
151988
- await api({ sessionID, text, delivery });
152238
+ await api({
152239
+ sessionID,
152240
+ text,
152241
+ delivery,
152242
+ ...files && files.length > 0 ? { files } : {}
152243
+ });
151989
152244
  }
151990
152245
  async function readSessionMessages(ctx, sessionID, directory, log2) {
151991
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.7",
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",