opencode-feishu-plugin 0.2.6 → 0.2.8

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 +111 -446
  2. package/README.md +78 -254
  3. package/dist/index.js +252 -28
  4. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -144025,6 +144025,10 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144025
144025
  const topicStatusThrottleMs = clamp4(asNumber(merged.topicStatusThrottleMs, 1e3), 500, 1e4);
144026
144026
  const cardMaxTables = clamp4(asNumber(merged.cardMaxTables, 4), 1, 5);
144027
144027
  const keepalive = asBoolean(merged.keepalive, true);
144028
+ const runnerCardMaxTools = clamp4(asNumber(merged.runnerCardMaxTools, 12), 1, 50);
144029
+ const runnerCardTextMax = clamp4(asNumber(merged.runnerCardTextMax, 2048), 512, 8192);
144030
+ const finalAnswerMinChars = clamp4(asNumber(merged.finalAnswerMinChars, 600), 0, 2e4);
144031
+ const finalAnswerFileMinBytes = clamp4(asNumber(merged.finalAnswerFileMinBytes, 20 * 1024), 8192, 102400);
144028
144032
  const keepaliveIntervalMs = clamp4(
144029
144033
  asNumber(merged.keepaliveIntervalMs, 20 * 60 * 1e3),
144030
144034
  5 * 60 * 1e3,
@@ -144081,7 +144085,11 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
144081
144085
  topicStatusThrottleMs,
144082
144086
  cardMaxTables,
144083
144087
  keepalive,
144084
- keepaliveIntervalMs
144088
+ keepaliveIntervalMs,
144089
+ runnerCardMaxTools,
144090
+ runnerCardTextMax,
144091
+ finalAnswerMinChars,
144092
+ finalAnswerFileMinBytes
144085
144093
  };
144086
144094
  }
144087
144095
  function loadConfigFile(env, deps, warnings) {
@@ -145173,6 +145181,29 @@ function createFeishuSender(client, log2, options2 = {}) {
145173
145181
  return void 0;
145174
145182
  }
145175
145183
  },
145184
+ async sendFile(chatId, fileName, content, replyToMessageId) {
145185
+ try {
145186
+ const uploaded = await client.im.file.create({
145187
+ data: { file_type: "stream", file_name: fileName, file: content }
145188
+ });
145189
+ const fileKey = uploaded?.file_key ?? "";
145190
+ if (!fileKey) return { ok: false, error: "missing file_key" };
145191
+ const payload = { msg_type: "file", content: JSON.stringify({ file_key: fileKey }) };
145192
+ const res = replyToMessageId ? await client.im.message.reply({ path: { message_id: replyToMessageId }, data: payload }) : await client.im.message.create({
145193
+ params: { receive_id_type: "chat_id" },
145194
+ data: { receive_id: chatId, ...payload }
145195
+ });
145196
+ if (res?.code && res.code !== 0) {
145197
+ log2.warn("\u53D1\u9001\u6587\u4EF6\u5931\u8D25", { chatId, fileName, code: res.code, msg: res.msg });
145198
+ return { ok: false, error: `code=${res.code} msg=${res.msg ?? ""}` };
145199
+ }
145200
+ const messageId = res?.data?.message_id ?? "";
145201
+ return messageId ? { ok: true, messageId } : { ok: true };
145202
+ } catch (err2) {
145203
+ log2.warn("\u53D1\u9001\u6587\u4EF6\u5F02\u5E38", { chatId, fileName, error: describeLarkError(err2) });
145204
+ return { ok: false, error: describeLarkError(err2) };
145205
+ }
145206
+ },
145176
145207
  async deleteMessage(messageId) {
145177
145208
  if (!messageId) return { ok: false, error: "missing messageId" };
145178
145209
  try {
@@ -145567,7 +145598,8 @@ function parseRootCard(value3) {
145567
145598
  ...value3.compactPending === true ? { compactPending: true } : {},
145568
145599
  ...compactError ? { compactError } : {},
145569
145600
  ...value3.compactButton === true ? { compactButton: true } : {},
145570
- ...note2 ? { note: note2 } : {}
145601
+ ...note2 ? { note: note2 } : {},
145602
+ ...value3.openedTopic === true ? { openedTopic: true } : {}
145571
145603
  };
145572
145604
  }
145573
145605
  function parseStringArray(value3) {
@@ -145885,6 +145917,8 @@ function reduce6(state, event) {
145885
145917
  status: "error",
145886
145918
  output: event.output
145887
145919
  }));
145920
+ case "final.separated":
145921
+ return { ...state, finalSeparated: true };
145888
145922
  default:
145889
145923
  return state;
145890
145924
  }
@@ -146061,6 +146095,14 @@ function closeCodeFences(text) {
146061
146095
  if (fences && fences.length % 2 !== 0) return `${text}${CODE_FENCE}`;
146062
146096
  return text;
146063
146097
  }
146098
+ function buildFinalAnswerCard(text) {
146099
+ return {
146100
+ schema: "2.0",
146101
+ config: { update_multi: true },
146102
+ header: { title: { tag: "plain_text", content: "\u2705 \u5B8C\u6574\u56DE\u7B54" }, template: "green" },
146103
+ body: { elements: [{ tag: "markdown", content: truncateCardContent(text, 24 * 1024) }] }
146104
+ };
146105
+ }
146064
146106
 
146065
146107
  // src/feishu/run-renderer.ts
146066
146108
  var COLLAPSE_TOOL_THRESHOLD = 3;
@@ -146068,11 +146110,30 @@ var HEADER_SUMMARY_MAX = 80;
146068
146110
  var TOOL_INPUT_MAX = 600;
146069
146111
  var OUTPUT_FIRST_LINE_MAX = 200;
146070
146112
  var TEXT_ELEMENT_MAX = 8 * 1024;
146113
+ var DEFAULT_MAX_TOOLS = 12;
146114
+ var DEFAULT_TEXT_MAX = 2 * 1024;
146071
146115
  function renderRunCard(state, stopValue, options2 = {}) {
146072
146116
  const elements = [];
146073
- for (const group of groupBlocks(state.blocks)) {
146117
+ const maxTools = options2.maxTools ?? DEFAULT_MAX_TOOLS;
146118
+ const textMax = options2.textMax ?? DEFAULT_TEXT_MAX;
146119
+ const capped = capToolBlocks(state.blocks, maxTools);
146120
+ if (capped.omitted > 0) {
146121
+ elements.push(note(`\u2026\u5DF2\u7701\u7565\u524D ${capped.omitted} \u6B21\u5DE5\u5177\u8C03\u7528\uFF08\u5B8C\u6574\u8BB0\u5F55\u89C1 opencode \u4F1A\u8BDD\uFF09`));
146122
+ }
146123
+ const groups = groupBlocks(capped.blocks);
146124
+ const lastTextIndex = groups.reduce(
146125
+ (acc, g, i) => g.kind === "text" && g.content.trim() ? i : acc,
146126
+ -1
146127
+ );
146128
+ for (let i = 0; i < groups.length; i += 1) {
146129
+ const group = groups[i];
146074
146130
  if (group.kind === "text") {
146075
- if (group.content.trim()) elements.push(markdown(truncateCardContent(group.content, TEXT_ELEMENT_MAX)));
146131
+ if (!group.content.trim()) continue;
146132
+ if (state.finalSeparated && i === lastTextIndex) {
146133
+ elements.push(note("\u2705 \u5B8C\u6574\u56DE\u7B54\u5DF2\u5355\u72EC\u53D1\u9001\uFF08\u89C1\u4E0B\u65B9\u5361\u7247 / \u9644\u4EF6\uFF09"));
146134
+ continue;
146135
+ }
146136
+ elements.push(markdown(truncateCardContent(group.content, Math.min(textMax, TEXT_ELEMENT_MAX))));
146076
146137
  } else {
146077
146138
  elements.push(...renderToolGroup(group.tools, state.terminal !== "running"));
146078
146139
  }
@@ -146109,6 +146170,20 @@ function renderRunCard(state, stopValue, options2 = {}) {
146109
146170
  }
146110
146171
  return enforceSize(guarded);
146111
146172
  }
146173
+ function capToolBlocks(blocks, maxTools) {
146174
+ const total2 = blocks.reduce((n, b) => n + (b.kind === "tool" ? 1 : 0), 0);
146175
+ if (maxTools <= 0 || total2 <= maxTools) return { blocks: [...blocks], omitted: 0 };
146176
+ let toDrop = total2 - maxTools;
146177
+ const out = [];
146178
+ for (const block of blocks) {
146179
+ if (block.kind === "tool" && toDrop > 0) {
146180
+ toDrop -= 1;
146181
+ continue;
146182
+ }
146183
+ out.push(block);
146184
+ }
146185
+ return { blocks: out, omitted: total2 - maxTools };
146186
+ }
146112
146187
  function groupBlocks(blocks) {
146113
146188
  const groups = [];
146114
146189
  let toolBuf = [];
@@ -146295,6 +146370,19 @@ function byteLength(value3) {
146295
146370
  }
146296
146371
 
146297
146372
  // src/feishu/run-controller.ts
146373
+ function trailingText(blocks) {
146374
+ const parts = [];
146375
+ for (let i = blocks.length - 1; i >= 0; i -= 1) {
146376
+ const block = blocks[i];
146377
+ if (block.kind === "tool") {
146378
+ if (parts.length > 0) break;
146379
+ continue;
146380
+ }
146381
+ if (block.content.trim()) parts.unshift(block.content);
146382
+ else if (parts.length > 0) break;
146383
+ }
146384
+ return parts.join("\n\n").trim();
146385
+ }
146298
146386
  function createRunController(deps) {
146299
146387
  const sessions = /* @__PURE__ */ new Map();
146300
146388
  const throttlers = /* @__PURE__ */ new Map();
@@ -146312,6 +146400,8 @@ function createRunController(deps) {
146312
146400
  };
146313
146401
  const renderCard = (sessionID, state) => renderRunCard(state, deps.buildStopValue?.(sessionID), {
146314
146402
  maxTables: deps.cardMaxTables,
146403
+ maxTools: deps.runnerCardMaxTools,
146404
+ textMax: deps.runnerCardTextMax,
146315
146405
  onLimit: (report) => {
146316
146406
  deps.log.warn("\u8FD0\u884C\u5361\u5185\u5BB9\u8D85\u9650\uFF0C\u5DF2\u964D\u7EA7", {
146317
146407
  sessionID,
@@ -146389,7 +146479,8 @@ function createRunController(deps) {
146389
146479
  state,
146390
146480
  chain: Promise.resolve(),
146391
146481
  finalized: false,
146392
- queuedAt: Date.now()
146482
+ queuedAt: Date.now(),
146483
+ ...input.replyToMessageId ? { replyToMessageId: input.replyToMessageId } : {}
146393
146484
  };
146394
146485
  if (input.delivery === "queue") {
146395
146486
  runs.queued.push(card);
@@ -146419,7 +146510,29 @@ function createRunController(deps) {
146419
146510
  }
146420
146511
  const card = runs.active;
146421
146512
  if (!card) return;
146422
- if (event.type === "execution.succeeded" || event.type === "execution.failed") {
146513
+ if (event.type === "execution.succeeded") {
146514
+ const text = trailingText(card.state.blocks);
146515
+ const long = deps.sendFinalAnswer !== void 0 && deps.finalAnswer !== void 0 && text.length >= deps.finalAnswer.minChars;
146516
+ if (long) {
146517
+ card.state = reduce6(reduce6(card.state, { type: "final.separated" }), event);
146518
+ finalize2(card);
146519
+ runs.active = void 0;
146520
+ void deps.sendFinalAnswer({
146521
+ sessionID,
146522
+ chatId: card.chatId,
146523
+ ...card.replyToMessageId ? { replyToMessageId: card.replyToMessageId } : {},
146524
+ text
146525
+ }).catch((err2) => {
146526
+ deps.log.warn("\u6700\u7EC8\u7B54\u6848\u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
146527
+ });
146528
+ return;
146529
+ }
146530
+ update2(card, event, true);
146531
+ finalize2(card);
146532
+ runs.active = void 0;
146533
+ return;
146534
+ }
146535
+ if (event.type === "execution.failed") {
146423
146536
  update2(card, event, true);
146424
146537
  finalize2(card);
146425
146538
  runs.active = void 0;
@@ -147971,7 +148084,10 @@ function renderResumedBody(title, base2, now3) {
147971
148084
  setup.push(`- \u6700\u8FD1\u6D3B\u52A8\uFF1A${relativeTime(base2.updatedAt, now3)}`);
147972
148085
  }
147973
148086
  if (setup.length > 0) lines.push("", ...setup);
147974
- 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");
148087
+ lines.push(
148088
+ "",
148089
+ 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"
148090
+ );
147975
148091
  if (base2.summary || base2.summaryPending || base2.compactPending || base2.compactError) {
147976
148092
  lines.push("", `**${base2.summaryLabel ?? "\u6458\u8981"}**\uFF1A`);
147977
148093
  if (base2.summaryPending) lines.push("\u23F3 \u6B63\u5728\u603B\u7ED3\u8BE5\u4F1A\u8BDD\u2026");
@@ -148010,7 +148126,8 @@ function buildSessionOpenedCard(input) {
148010
148126
  ...input.summaryPending ? { summaryPending: true } : {},
148011
148127
  ...input.compactPending ? { compactPending: true } : {},
148012
148128
  ...input.compactError ? { compactError: input.compactError } : {},
148013
- ...input.compactButton ? { compactButton: true } : {}
148129
+ ...input.compactButton ? { compactButton: true } : {},
148130
+ ...input.openedTopic ? { openedTopic: true } : {}
148014
148131
  };
148015
148132
  return buildSessionRootCard(base2, void 0, {
148016
148133
  now: input.now ?? Date.now(),
@@ -149934,21 +150051,20 @@ async function enterSessionThread(ctx, input) {
149934
150051
  const onLimit = (report) => {
149935
150052
  ctx.deps.log.warn("\u4F1A\u8BDD\u6062\u590D\u5361\u5185\u5BB9\u8D85\u9650\uFF0C\u5DF2\u964D\u7EA7", { sessionID, ...report });
149936
150053
  };
149937
- const cardInput = {
150054
+ const newNow = ctx.now();
150055
+ const baseInput = {
149938
150056
  title: info?.title ?? "",
149939
150057
  sessionID,
149940
150058
  ...dir ? { dir } : {},
149941
150059
  ...link2?.model ? { model: modelLabel(link2.model) } : {},
149942
150060
  ...info?.updatedAt ? { updatedAt: info.updatedAt } : {},
149943
- now: ctx.now(),
150061
+ now: newNow,
149944
150062
  ...ctx.deps.cardMaxTables !== void 0 ? { maxTables: ctx.deps.cardMaxTables } : {},
149945
150063
  onLimit,
149946
- ...showSummary ? { summaryPending: true } : {},
149947
- // 「🗜 压缩并总结」只在开关开启且运行时装配了签名时渲染。
149948
- ...showSummary && ctx.deps.signCompact ? { compactButton: { sessionID, token: ctx.deps.signCompact(sessionID) } } : {}
150064
+ openedTopic: true
149949
150065
  };
149950
- const card = buildSessionOpenedCard(cardInput);
149951
- const res = await ctx.deps.sender.sendCard(chatId, card);
150066
+ const rootCard = buildSessionOpenedCard(baseInput);
150067
+ const res = await ctx.deps.sender.sendCard(chatId, rootCard);
149952
150068
  ctx.deps.log.info("\u53D1\u9001\u4F1A\u8BDD\u6062\u590D\u5361", {
149953
150069
  sessionID,
149954
150070
  source: input.source,
@@ -149970,26 +150086,63 @@ async function enterSessionThread(ctx, input) {
149970
150086
  }
149971
150087
  await ctx.deps.sessionMap.bindRoot(res.messageId, sessionID);
149972
150088
  ctx.deps.log.info("resume card bound root to session", { sessionID, rootId: res.messageId });
149973
- await ctx.deps.sessionMap.setRootCard(sessionID, {
150089
+ const summaryBlock = {
150090
+ ...showSummary ? { summaryPending: true } : {},
150091
+ ...showSummary && ctx.deps.signCompact ? { compactButton: { sessionID, token: ctx.deps.signCompact(sessionID) } } : {}
150092
+ };
150093
+ const openerInput = { ...baseInput, ...summaryBlock };
150094
+ const openerRes = await ctx.deps.sender.replyCard(res.messageId, buildSessionOpenedCard(openerInput), {
150095
+ replyInThread: true
150096
+ });
150097
+ const openerMessageId = openerRes.ok ? openerRes.messageId : void 0;
150098
+ let threadId = openerRes.threadId;
150099
+ if (!threadId && openerMessageId) {
150100
+ threadId = (await ctx.deps.sender.getMessageMeta(openerMessageId))?.threadId;
150101
+ }
150102
+ const rootCardBase = {
149974
150103
  style: "resumed",
149975
150104
  sessionID,
149976
150105
  title: info?.title ?? "",
149977
150106
  ...dir ? { dir } : {},
149978
150107
  ...link2?.model ? { model: modelLabel(link2.model) } : {},
149979
- ...info?.updatedAt ? { updatedAt: info.updatedAt } : {},
149980
- ...showSummary ? { summaryPending: true, compactButton: Boolean(cardInput.compactButton) } : {}
150108
+ ...info?.updatedAt ? { updatedAt: info.updatedAt } : {}
150109
+ };
150110
+ if (threadId) {
150111
+ await ctx.deps.sessionMap.bindThread(threadId, sessionID, chatId, operatorOpenId, res.messageId);
150112
+ ctx.deps.log.info("\u6062\u590D\u4F1A\u8BDD\u5DF2\u81EA\u52A8\u5F00\u8BDD\u9898", {
150113
+ sessionID,
150114
+ threadId,
150115
+ rootMessageId: res.messageId,
150116
+ openerMessageId
150117
+ });
150118
+ await ctx.deps.sessionMap.setRootCard(sessionID, { ...rootCardBase, openedTopic: true });
150119
+ if (showSummary && openerMessageId) {
150120
+ void patchResumeSummary(ctx, sessionID, dir, openerMessageId, openerInput, false);
150121
+ }
150122
+ return { ok: true, threadId, messageId: res.messageId };
150123
+ }
150124
+ ctx.deps.log.warn("\u6062\u590D\u4F1A\u8BDD\u81EA\u52A8\u5F00\u8BDD\u9898\u5931\u8D25\uFF0C\u56DE\u9000\u4E3A\u56DE\u590D\u6839\u5361", {
150125
+ sessionID,
150126
+ openError: openerRes.error ?? "unknown"
150127
+ });
150128
+ const fallbackInput = { ...baseInput, openedTopic: false, ...summaryBlock };
150129
+ await ctx.deps.sender.patchCard(res.messageId, buildSessionOpenedCard(fallbackInput));
150130
+ await ctx.deps.sessionMap.setRootCard(sessionID, {
150131
+ ...rootCardBase,
150132
+ ...showSummary ? { summaryPending: true, compactButton: Boolean(fallbackInput.compactButton) } : {}
149981
150133
  });
149982
150134
  if (showSummary) {
149983
- void patchResumeSummary(ctx, sessionID, dir, res.messageId, cardInput);
150135
+ void patchResumeSummary(ctx, sessionID, dir, res.messageId, fallbackInput, true);
149984
150136
  }
149985
150137
  return { ok: true, messageId: res.messageId };
149986
150138
  }
149987
150139
  var SUMMARY_LABEL = {
149988
150140
  reused: "\u4F1A\u8BDD\u6458\u8981",
149989
150141
  generated: "\u6458\u8981\uFF08\u5FEB\u6458\u8981\uFF09",
150142
+ excerpt: "\u6700\u8FD1\u4F1A\u8BDD\u8BB0\u5F55\uFF08\u622A\u53D6\u6458\u8981\uFF09",
149990
150143
  compacted: "\u5DF2\u538B\u7F29 \xB7 \u4F1A\u8BDD\u6458\u8981"
149991
150144
  };
149992
- async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput) {
150145
+ async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput, persistBase) {
149993
150146
  try {
149994
150147
  const outcome = await ctx.deps.summarizeSession({
149995
150148
  sessionID,
@@ -149998,9 +150151,11 @@ async function patchResumeSummary(ctx, sessionID, dir, messageId, cardInput) {
149998
150151
  });
149999
150152
  const summary = outcome.summary ?? "\uFF08\u6458\u8981\u751F\u6210\u5931\u8D25\uFF0C\u53EF\u76F4\u63A5\u53D1\u6D88\u606F\u7EE7\u7EED\uFF09";
150000
150153
  const summaryLabel = SUMMARY_LABEL[outcome.source] ?? "\u6458\u8981";
150001
- const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
150002
- if (base2) {
150003
- await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false, summary, summaryLabel });
150154
+ if (persistBase) {
150155
+ const base2 = await ctx.deps.sessionMap.getRootCard(sessionID);
150156
+ if (base2) {
150157
+ await ctx.deps.sessionMap.setRootCard(sessionID, { ...base2, summaryPending: false, summary, summaryLabel });
150158
+ }
150004
150159
  }
150005
150160
  const res = await ctx.deps.sender.patchCard(
150006
150161
  messageId,
@@ -150640,6 +150795,20 @@ function buildSummaryPrompt(transcript) {
150640
150795
  \u4F1A\u8BDD\u6700\u8FD1\u8BB0\u5F55\uFF1A
150641
150796
  ${transcript}` : RESUME_SUMMARY_PROMPT;
150642
150797
  }
150798
+ function buildExcerptSummary(transcript, maxLines = 5, maxChars = 900) {
150799
+ if (!transcript) return void 0;
150800
+ const lines = transcript.split("\n").filter(Boolean);
150801
+ if (lines.length === 0) return void 0;
150802
+ const out = [];
150803
+ let bytes = 0;
150804
+ for (const line of lines) {
150805
+ const size6 = Buffer.byteLength(line, "utf8");
150806
+ if (out.length >= maxLines || bytes + size6 > maxChars) break;
150807
+ out.push(line);
150808
+ bytes += size6;
150809
+ }
150810
+ return out.length > 0 ? out.join("\n") : void 0;
150811
+ }
150643
150812
  async function summarizeSession(deps, input) {
150644
150813
  const timeoutMs = Math.max(1, Math.floor(input.timeoutMs));
150645
150814
  let messages;
@@ -150664,6 +150833,8 @@ async function summarizeSession(deps, input) {
150664
150833
  );
150665
150834
  const generated = extractGeneratedText(raw);
150666
150835
  if (generated) return { summary: generated, source: "generated" };
150836
+ const excerpt = buildExcerptSummary(transcript);
150837
+ if (excerpt) return { summary: excerpt, source: "excerpt" };
150667
150838
  return { source: "none", error: "empty-summary" };
150668
150839
  } catch (err2) {
150669
150840
  const error = errorMessage(err2);
@@ -150672,6 +150843,11 @@ async function summarizeSession(deps, input) {
150672
150843
  timedOut: err2 instanceof SummaryTimeoutError,
150673
150844
  error
150674
150845
  });
150846
+ try {
150847
+ const excerpt = buildExcerptSummary(buildTranscript(messages));
150848
+ if (excerpt) return { summary: excerpt, source: "excerpt" };
150849
+ } catch {
150850
+ }
150675
150851
  return { source: "none", error };
150676
150852
  }
150677
150853
  }
@@ -151194,9 +151370,38 @@ async function start(ctx, config, log2, logSink) {
151194
151370
  enabled: config.stream,
151195
151371
  throttleMs: config.streamThrottleMs,
151196
151372
  cardMaxTables: config.cardMaxTables,
151373
+ runnerCardMaxTools: config.runnerCardMaxTools,
151374
+ runnerCardTextMax: config.runnerCardTextMax,
151375
+ finalAnswer: { minChars: config.finalAnswerMinChars },
151376
+ // 长回答单独成卡/成文件:运行卡只留「完整回答已单独发送」提示。
151377
+ sendFinalAnswer: (input) => sendFinalAnswer(input),
151197
151378
  // 每次 patch 重签强停 token(`stop` 在下方定义,闭包运行时才求值)。
151198
151379
  buildStopValue: (sessionID) => stop.buildStopValue(sessionID)
151199
151380
  });
151381
+ async function sendFinalAnswer(input) {
151382
+ const bytes = Buffer.byteLength(input.text, "utf8");
151383
+ if (bytes > config.finalAnswerFileMinBytes) {
151384
+ const fileName = `opencode-${input.sessionID.slice(-6)}.md`;
151385
+ const res2 = await sender.sendFile(
151386
+ input.chatId,
151387
+ fileName,
151388
+ Buffer.from(input.text, "utf8"),
151389
+ input.replyToMessageId
151390
+ );
151391
+ if (res2.ok) {
151392
+ log2.info("\u6700\u7EC8\u7B54\u6848\u5DF2\u8F6C\u4E3A\u6587\u4EF6\u53D1\u9001", { sessionID: input.sessionID, bytes, fileName });
151393
+ return;
151394
+ }
151395
+ log2.warn("\u6700\u7EC8\u7B54\u6848\u8F6C\u6587\u4EF6\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u5361\u7247", { sessionID: input.sessionID, error: res2.error ?? "unknown" });
151396
+ }
151397
+ const card = buildFinalAnswerCard(input.text);
151398
+ const res = input.replyToMessageId ? await sender.replyCard(input.replyToMessageId, card) : await sender.sendCard(input.chatId, card);
151399
+ if (!res.ok) {
151400
+ log2.warn("\u6700\u7EC8\u7B54\u6848\u5361\u7247\u53D1\u9001\u5931\u8D25", { sessionID: input.sessionID, error: res.error ?? "unknown" });
151401
+ return;
151402
+ }
151403
+ log2.info("\u6700\u7EC8\u7B54\u6848\u5DF2\u5355\u72EC\u6210\u5361", { sessionID: input.sessionID, bytes });
151404
+ }
151200
151405
  const recovery = createSessionRecovery({
151201
151406
  log: log2,
151202
151407
  resolveLink: (sessionID) => sessionMap.resolveBySession(sessionID),
@@ -151647,13 +151852,32 @@ async function start(ctx, config, log2, logSink) {
151647
151852
  })
151648
151853
  });
151649
151854
  const abort = new AbortController();
151855
+ const EVENT_RETRY_BASE_MS = 1e3;
151856
+ const EVENT_RETRY_MAX_MS = 3e4;
151857
+ const sleep3 = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
151650
151858
  const subscription = (async () => {
151651
- try {
151652
- for await (const event of ctx.event.subscribe({ signal: abort.signal })) {
151653
- await handleEvent(event);
151859
+ let attempt = 0;
151860
+ while (!abort.signal.aborted) {
151861
+ try {
151862
+ for await (const event of ctx.event.subscribe({ signal: abort.signal })) {
151863
+ if (attempt > 0) {
151864
+ log2.info("\u4E8B\u4EF6\u8BA2\u9605\u5DF2\u91CD\u8FDE", { attempt });
151865
+ }
151866
+ attempt = 0;
151867
+ await handleEvent(event);
151868
+ }
151869
+ if (abort.signal.aborted) break;
151870
+ attempt += 1;
151871
+ const delay3 = Math.min(EVENT_RETRY_MAX_MS, EVENT_RETRY_BASE_MS * 2 ** Math.min(attempt, 5));
151872
+ log2.warn("\u4E8B\u4EF6\u8BA2\u9605\u6D41\u5DF2\u7ED3\u675F\uFF0C\u51C6\u5907\u91CD\u8FDE", { attempt, delayMs: delay3 });
151873
+ await sleep3(delay3);
151874
+ } catch (err2) {
151875
+ if (abort.signal.aborted) break;
151876
+ attempt += 1;
151877
+ const delay3 = Math.min(EVENT_RETRY_MAX_MS, EVENT_RETRY_BASE_MS * 2 ** Math.min(attempt, 5));
151878
+ log2.error("\u4E8B\u4EF6\u8BA2\u9605\u5F02\u5E38\uFF0C\u51C6\u5907\u91CD\u8FDE", { attempt, delayMs: delay3, error: errorMessage(err2) });
151879
+ await sleep3(delay3);
151654
151880
  }
151655
- } catch (err2) {
151656
- if (!abort.signal.aborted) log2.error("\u4E8B\u4EF6\u8BA2\u9605\u5F02\u5E38\u9000\u51FA", { error: errorMessage(err2) });
151657
151881
  }
151658
151882
  })();
151659
151883
  const eventDeps = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-feishu-plugin",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",