opencode-feishu 1.2.0 → 1.3.1

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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import fs, { existsSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import url, { fileURLToPath } from 'url';
3
4
  import { homedir } from 'os';
4
- import crypto2 from 'crypto';
5
- import url from 'url';
5
+ import crypto2, { randomUUID } from 'crypto';
6
6
  import http from 'http';
7
7
  import https from 'https';
8
8
  import http2 from 'http2';
@@ -13,6 +13,8 @@ import { EventEmitter } from 'events';
13
13
  import qs from 'querystring';
14
14
  import WebSocket from 'ws';
15
15
  import { HttpsProxyAgent } from 'https-proxy-agent';
16
+ import { tool } from '@opencode-ai/plugin';
17
+ import { createOpencodeClient } from '@opencode-ai/sdk/v2/client';
16
18
 
17
19
  var __create = Object.create;
18
20
  var __defProp = Object.defineProperty;
@@ -112523,6 +112525,7 @@ var FeishuConfigSchema = external_exports.object({
112523
112525
  pollInterval: external_exports.number().int().positive().default(1e3),
112524
112526
  stablePolls: external_exports.number().int().positive().default(3),
112525
112527
  dedupTtl: external_exports.number().int().positive().default(10 * 60 * 1e3),
112528
+ maxResourceSize: external_exports.number().int().positive().max(500 * 1024 * 1024).default(500 * 1024 * 1024),
112526
112529
  directory: external_exports.string().optional(),
112527
112530
  autoPrompt: AutoPromptSchema.default(() => AutoPromptSchema.parse({}))
112528
112531
  });
@@ -112600,6 +112603,29 @@ var CardKitClient = class {
112600
112603
  });
112601
112604
  }
112602
112605
  }
112606
+ /**
112607
+ * 在卡片末尾追加新组件(用于流式卡片动态添加元素)
112608
+ */
112609
+ async addElement(cardId, elements, sequence) {
112610
+ const res = await this.larkClient.cardkit.v1.cardElement.create({
112611
+ data: {
112612
+ type: "append",
112613
+ elements: JSON.stringify(elements),
112614
+ sequence
112615
+ },
112616
+ path: {
112617
+ card_id: cardId
112618
+ }
112619
+ });
112620
+ if (res?.code !== 0) {
112621
+ this.log?.("warn", "CardKit addElement \u5931\u8D25", {
112622
+ cardId,
112623
+ code: res?.code,
112624
+ msg: res?.msg
112625
+ });
112626
+ throw new Error(`CardKit addElement \u5931\u8D25: ${res?.msg ?? "unknown"} (code: ${res?.code})`);
112627
+ }
112628
+ }
112603
112629
  /**
112604
112630
  * 关闭卡片流式模式
112605
112631
  */
@@ -112623,94 +112649,49 @@ var CardKitClient = class {
112623
112649
  }
112624
112650
  };
112625
112651
 
112626
- // src/feishu/card-builder.ts
112627
- function buildPermissionCard(request) {
112628
- const permission = String(request.permission ?? "unknown");
112629
- const patterns = Array.isArray(request.patterns) ? request.patterns.map(String) : [];
112630
- const requestId = String(request.id ?? "");
112631
- const patternsText = patterns.length > 0 ? patterns.map((p) => `- ${p}`).join("\n") : "\uFF08\u65E0\u5177\u4F53\u8DEF\u5F84\uFF09";
112632
- return {
112633
- type: "card_kit",
112634
- data: {
112635
- schema: "2.0",
112636
- config: { wide_screen_mode: true },
112637
- header: {
112638
- title: { tag: "plain_text", content: `\u{1F510} \u6743\u9650\u8BF7\u6C42: ${permission}` },
112639
- template: "orange"
112640
- },
112641
- body: {
112642
- elements: [
112643
- {
112644
- tag: "markdown",
112645
- content: `AI \u8BF7\u6C42\u4EE5\u4E0B\u6743\u9650:
112646
-
112647
- ${patternsText}`
112648
- },
112649
- {
112650
- tag: "action",
112651
- actions: [
112652
- {
112653
- tag: "button",
112654
- text: { tag: "plain_text", content: "\u2705 \u5141\u8BB8\u4E00\u6B21" },
112655
- type: "primary",
112656
- value: JSON.stringify({ action: "permission_reply", requestId, reply: "once" })
112657
- },
112658
- {
112659
- tag: "button",
112660
- text: { tag: "plain_text", content: "\u{1F513} \u59CB\u7EC8\u5141\u8BB8" },
112661
- type: "default",
112662
- value: JSON.stringify({ action: "permission_reply", requestId, reply: "always" })
112663
- },
112664
- {
112665
- tag: "button",
112666
- text: { tag: "plain_text", content: "\u274C \u62D2\u7EDD" },
112667
- type: "danger",
112668
- value: JSON.stringify({ action: "permission_reply", requestId, reply: "reject" })
112669
- }
112670
- ]
112671
- }
112672
- ]
112673
- }
112652
+ // src/utils/ttl-map.ts
112653
+ var TtlMap = class {
112654
+ constructor(defaultTtlMs) {
112655
+ this.defaultTtlMs = defaultTtlMs;
112656
+ }
112657
+ data = /* @__PURE__ */ new Map();
112658
+ timers = /* @__PURE__ */ new Map();
112659
+ get(key) {
112660
+ return this.data.get(key);
112661
+ }
112662
+ has(key) {
112663
+ return this.data.has(key);
112664
+ }
112665
+ set(key, value, ttlMs) {
112666
+ this.delete(key);
112667
+ this.data.set(key, value);
112668
+ const timer = setTimeout(() => {
112669
+ this.data.delete(key);
112670
+ this.timers.delete(key);
112671
+ }, ttlMs ?? this.defaultTtlMs);
112672
+ timer.unref();
112673
+ this.timers.set(key, timer);
112674
+ }
112675
+ delete(key) {
112676
+ const timer = this.timers.get(key);
112677
+ if (timer) {
112678
+ clearTimeout(timer);
112679
+ this.timers.delete(key);
112674
112680
  }
112675
- };
112681
+ this.data.delete(key);
112682
+ }
112683
+ };
112684
+
112685
+ // src/feishu/session-chat-map.ts
112686
+ var sessionToChat = new TtlMap(24 * 60 * 60 * 1e3);
112687
+ function registerSessionChat(sessionId, chatId, chatType) {
112688
+ sessionToChat.set(sessionId, { chatId, chatType });
112676
112689
  }
112677
- function buildQuestionCard(request) {
112678
- const questions = Array.isArray(request.questions) ? request.questions : [];
112679
- const requestId = String(request.id ?? "");
112680
- const q = questions[0];
112681
- const header = String(q?.header ?? "AI \u63D0\u95EE");
112682
- const questionText = String(q?.question ?? "\u8BF7\u9009\u62E9");
112683
- const options = Array.isArray(q?.options) ? q.options : [];
112684
- const buttons = options.map((opt, idx) => ({
112685
- tag: "button",
112686
- text: { tag: "plain_text", content: String(opt.label ?? opt.value ?? `\u9009\u9879 ${idx + 1}`) },
112687
- type: idx === 0 ? "primary" : "default",
112688
- value: JSON.stringify({
112689
- action: "question_reply",
112690
- requestId,
112691
- answers: [[String(opt.value ?? opt.label ?? "")]]
112692
- })
112693
- }));
112694
- return {
112695
- type: "card_kit",
112696
- data: {
112697
- schema: "2.0",
112698
- config: { wide_screen_mode: true },
112699
- header: {
112700
- title: { tag: "plain_text", content: header },
112701
- template: "blue"
112702
- },
112703
- body: {
112704
- elements: [
112705
- {
112706
- tag: "markdown",
112707
- content: questionText
112708
- },
112709
- ...buttons.length > 0 ? [{ tag: "action", actions: buttons }] : []
112710
- ]
112711
- }
112712
- }
112713
- };
112690
+ function getChatIdBySession(sessionId) {
112691
+ return sessionToChat.get(sessionId)?.chatId;
112692
+ }
112693
+ function getChatInfoBySession(sessionId) {
112694
+ return sessionToChat.get(sessionId);
112714
112695
  }
112715
112696
 
112716
112697
  // src/feishu/sender.ts
@@ -112786,38 +112767,150 @@ async function sendCardMessage(client, chatId, cardId) {
112786
112767
  );
112787
112768
  }
112788
112769
 
112789
- // src/utils/ttl-map.ts
112790
- var TtlMap = class {
112791
- constructor(defaultTtlMs) {
112792
- this.defaultTtlMs = defaultTtlMs;
112793
- }
112794
- data = /* @__PURE__ */ new Map();
112795
- timers = /* @__PURE__ */ new Map();
112796
- get(key) {
112797
- return this.data.get(key);
112798
- }
112799
- has(key) {
112800
- return this.data.has(key);
112770
+ // src/feishu/markdown.ts
112771
+ var MAX_CARD_BYTES = 28 * 1024;
112772
+ var TRUNCATION_SUFFIX = "\n\n*\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD*";
112773
+ var TRUNCATION_SUFFIX_BYTES = new TextEncoder().encode(TRUNCATION_SUFFIX).length;
112774
+ var CODE_FENCE_BYTES = 4;
112775
+ var HTML_TAG_RE = /<\/?\w+(?:\s[^>]*)?\/?>/g;
112776
+ function cleanMarkdown(text) {
112777
+ let result = text.replace(/<br\s*\/?>/gi, "\n");
112778
+ const { segments, codeBlocks } = extractCodeBlocks(result);
112779
+ result = segments.map((seg) => seg.replace(HTML_TAG_RE, "")).join("\0");
112780
+ let idx = 0;
112781
+ result = result.replace(/\0/g, () => codeBlocks[idx++] ?? "");
112782
+ result = closeCodeBlocks(result);
112783
+ return result;
112784
+ }
112785
+ function truncateMarkdown(text, limit = MAX_CARD_BYTES) {
112786
+ const bytes = new TextEncoder().encode(text);
112787
+ if (bytes.length <= limit) return text;
112788
+ const effectiveLimit = limit - TRUNCATION_SUFFIX_BYTES - CODE_FENCE_BYTES;
112789
+ if (effectiveLimit <= 0) return TRUNCATION_SUFFIX;
112790
+ const truncated = new TextDecoder().decode(bytes.slice(0, effectiveLimit));
112791
+ const lastNewline = truncated.lastIndexOf("\n");
112792
+ const cutPoint = lastNewline > effectiveLimit * 0.8 ? lastNewline : truncated.length;
112793
+ let result = truncated.slice(0, cutPoint);
112794
+ result = closeCodeBlocks(result);
112795
+ return result + TRUNCATION_SUFFIX;
112796
+ }
112797
+ function extractCodeBlocks(text) {
112798
+ const segments = [];
112799
+ const codeBlocks = [];
112800
+ const re = /```[\s\S]*?```/g;
112801
+ let lastIndex = 0;
112802
+ let match;
112803
+ while ((match = re.exec(text)) !== null) {
112804
+ segments.push(text.slice(lastIndex, match.index));
112805
+ codeBlocks.push(match[0]);
112806
+ lastIndex = match.index + match[0].length;
112801
112807
  }
112802
- set(key, value, ttlMs) {
112803
- this.delete(key);
112804
- this.data.set(key, value);
112805
- const timer = setTimeout(() => {
112806
- this.data.delete(key);
112807
- this.timers.delete(key);
112808
- }, ttlMs ?? this.defaultTtlMs);
112809
- timer.unref();
112810
- this.timers.set(key, timer);
112808
+ segments.push(text.slice(lastIndex));
112809
+ return { segments, codeBlocks };
112810
+ }
112811
+ function closeCodeBlocks(text) {
112812
+ const matches = text.match(/```/g);
112813
+ if (matches && matches.length % 2 !== 0) {
112814
+ return text + "\n```";
112811
112815
  }
112812
- delete(key) {
112813
- const timer = this.timers.get(key);
112814
- if (timer) {
112815
- clearTimeout(timer);
112816
- this.timers.delete(key);
112816
+ return text;
112817
+ }
112818
+
112819
+ // src/tools/send-card.ts
112820
+ var z2 = tool.schema;
112821
+ var TEMPLATE_COLORS = ["blue", "green", "orange", "red", "purple", "grey"];
112822
+ function createSendCardTool(deps) {
112823
+ return tool({
112824
+ description: "\u53D1\u9001\u683C\u5F0F\u5316\u5361\u7247\u6D88\u606F\u5230\u5F53\u524D\u98DE\u4E66\u4F1A\u8BDD\u3002\u652F\u6301 markdown \u6B63\u6587\u3001\u5206\u5272\u7EBF\u3001\u5907\u6CE8\u548C\u4EA4\u4E92\u6309\u94AE\u3002\u6309\u94AE\u70B9\u51FB\u7B49\u540C\u7528\u6237\u53D1\u9001\u6D88\u606F\u3002\u5361\u7247\u4F5C\u4E3A\u72EC\u7ACB\u6D88\u606F\u53D1\u9001\uFF0C\u4E0D\u5F71\u54CD\u6D41\u5F0F\u56DE\u590D\u3002",
112825
+ args: {
112826
+ title: z2.string().describe("\u5361\u7247\u6807\u9898"),
112827
+ template: z2.enum(TEMPLATE_COLORS).default("blue").describe("\u6807\u9898\u989C\u8272\u4E3B\u9898"),
112828
+ sections: z2.array(
112829
+ z2.object({
112830
+ type: z2.enum(["markdown", "divider", "note", "actions"]).default("markdown").describe("\u533A\u5757\u7C7B\u578B\uFF1Amarkdown\uFF08\u6B63\u6587\uFF09\u3001divider\uFF08\u5206\u5272\u7EBF\uFF09\u3001note\uFF08\u5907\u6CE8\uFF09\u3001actions\uFF08\u6309\u94AE\u7EC4\uFF09"),
112831
+ content: z2.string().optional().describe("\u533A\u5757\u5185\u5BB9\uFF08markdown \u683C\u5F0F\uFF0Cdivider/actions \u7C7B\u578B\u65E0\u9700\u6B64\u5B57\u6BB5\uFF09"),
112832
+ buttons: z2.array(
112833
+ z2.object({
112834
+ text: z2.string().describe("\u6309\u94AE\u663E\u793A\u6587\u672C\uFF082-6\u5B57\uFF09"),
112835
+ value: z2.string().describe("\u70B9\u51FB\u540E\u4F5C\u4E3A\u7528\u6237\u6D88\u606F\u53D1\u9001\u7684\u5185\u5BB9"),
112836
+ style: z2.enum(["primary", "default", "danger"]).default("default").describe("\u6309\u94AE\u6837\u5F0F")
112837
+ })
112838
+ ).optional().describe("\u6309\u94AE\u5217\u8868\uFF08\u4EC5 actions \u7C7B\u578B\u4F7F\u7528\uFF09")
112839
+ })
112840
+ ).min(1).describe("\u5361\u7247\u6B63\u6587\u533A\u5757\u5217\u8868")
112841
+ },
112842
+ async execute(args, context) {
112843
+ const chatId = getChatIdBySession(context.sessionID);
112844
+ if (!chatId) {
112845
+ return "\u9519\u8BEF\uFF1A\u5F53\u524D\u4F1A\u8BDD\u4E0D\u5173\u8054\u98DE\u4E66\u804A\u5929\uFF0C\u65E0\u6CD5\u53D1\u9001\u5361\u7247";
112846
+ }
112847
+ const chatInfo = getChatInfoBySession(context.sessionID);
112848
+ const card = buildCardFromDSL(args, chatId, chatInfo?.chatType ?? "p2p");
112849
+ const result = await sendInteractiveCard(deps.feishuClient, chatId, card);
112850
+ if (result.ok) {
112851
+ deps.log("info", "Agent \u5361\u7247\u5DF2\u53D1\u9001", {
112852
+ sessionId: context.sessionID,
112853
+ chatId,
112854
+ title: args.title,
112855
+ messageId: result.messageId
112856
+ });
112857
+ return `\u5361\u7247\u5DF2\u53D1\u9001\uFF1A\u300C${args.title}\u300D`;
112858
+ }
112859
+ deps.log("warn", "Agent \u5361\u7247\u53D1\u9001\u5931\u8D25", {
112860
+ sessionId: context.sessionID,
112861
+ chatId,
112862
+ title: args.title,
112863
+ error: result.error
112864
+ });
112865
+ return `\u5361\u7247\u53D1\u9001\u5931\u8D25\uFF1A${result.error}`;
112817
112866
  }
112818
- this.data.delete(key);
112819
- }
112820
- };
112867
+ });
112868
+ }
112869
+ function buildCardFromDSL(args, chatId, chatType) {
112870
+ return {
112871
+ schema: "2.0",
112872
+ config: { wide_screen_mode: true },
112873
+ header: {
112874
+ title: { tag: "plain_text", content: args.title },
112875
+ template: args.template
112876
+ },
112877
+ body: {
112878
+ elements: args.sections.map((s) => {
112879
+ switch (s.type) {
112880
+ case "divider":
112881
+ return { tag: "hr" };
112882
+ case "note":
112883
+ return {
112884
+ tag: "note",
112885
+ elements: [{ tag: "plain_text", content: s.content ?? "" }]
112886
+ };
112887
+ case "actions":
112888
+ if (!s.buttons?.length) return null;
112889
+ return {
112890
+ tag: "action",
112891
+ actions: s.buttons.map((btn) => ({
112892
+ tag: "button",
112893
+ text: { tag: "plain_text", content: btn.text },
112894
+ type: btn.style,
112895
+ value: JSON.stringify(btn.actionPayload ?? {
112896
+ action: "send_message",
112897
+ chatId,
112898
+ chatType,
112899
+ text: btn.value
112900
+ })
112901
+ }))
112902
+ };
112903
+ case "markdown":
112904
+ default:
112905
+ return {
112906
+ tag: "markdown",
112907
+ content: truncateMarkdown(s.content ?? "", 28e3)
112908
+ };
112909
+ }
112910
+ }).filter(Boolean)
112911
+ }
112912
+ };
112913
+ }
112821
112914
 
112822
112915
  // src/handler/interactive.ts
112823
112916
  var seenIds = new TtlMap(10 * 60 * 1e3);
@@ -112826,14 +112919,14 @@ function markSeen(requestId) {
112826
112919
  seenIds.set(requestId, true);
112827
112920
  return true;
112828
112921
  }
112829
- function handlePermissionRequested(request, chatId, deps) {
112922
+ function handlePermissionRequested(request, chatId, deps, chatType = "p2p") {
112830
112923
  if (!deps.v2Client) {
112831
112924
  deps.log("warn", "v2Client \u672A\u914D\u7F6E\uFF0C\u8DF3\u8FC7\u6743\u9650\u5361\u7247\u53D1\u9001", { requestId: String(request.id ?? "") });
112832
112925
  return;
112833
112926
  }
112834
112927
  const requestId = String(request.id ?? "");
112835
112928
  if (!requestId || !markSeen(requestId)) return;
112836
- const card = buildPermissionCard(request);
112929
+ const card = buildPermissionCardDSL(request, chatId, chatType);
112837
112930
  sendInteractiveCard(deps.feishuClient, chatId, card).catch((err) => {
112838
112931
  deps.log("warn", "\u53D1\u9001\u6743\u9650\u5361\u7247\u5931\u8D25", {
112839
112932
  requestId,
@@ -112841,14 +112934,14 @@ function handlePermissionRequested(request, chatId, deps) {
112841
112934
  });
112842
112935
  });
112843
112936
  }
112844
- function handleQuestionRequested(request, chatId, deps) {
112937
+ function handleQuestionRequested(request, chatId, deps, chatType = "p2p") {
112845
112938
  if (!deps.v2Client) {
112846
112939
  deps.log("warn", "v2Client \u672A\u914D\u7F6E\uFF0C\u8DF3\u8FC7\u95EE\u7B54\u5361\u7247\u53D1\u9001", { requestId: String(request.id ?? "") });
112847
112940
  return;
112848
112941
  }
112849
112942
  const requestId = String(request.id ?? "");
112850
112943
  if (!requestId || !markSeen(requestId)) return;
112851
- const card = buildQuestionCard(request);
112944
+ const card = buildQuestionCardDSL(request, chatId, chatType);
112852
112945
  sendInteractiveCard(deps.feishuClient, chatId, card).catch((err) => {
112853
112946
  deps.log("warn", "\u53D1\u9001\u95EE\u7B54\u5361\u7247\u5931\u8D25", {
112854
112947
  requestId,
@@ -112858,12 +112951,39 @@ function handleQuestionRequested(request, chatId, deps) {
112858
112951
  }
112859
112952
  async function handleCardAction(action, deps) {
112860
112953
  if (!action.actionValue) return;
112861
- {
112954
+ if (!deps.v2Client) {
112862
112955
  deps.log("warn", "v2Client \u672A\u914D\u7F6E\uFF0C\u4EA4\u4E92\u56DE\u8C03\u88AB\u5FFD\u7565\uFF08\u6309\u94AE\u70B9\u51FB\u4E0D\u4F1A\u8F6C\u53D1\u5230 OpenCode\uFF09", {
112863
112956
  actionValue: action.actionValue
112864
112957
  });
112865
112958
  return;
112866
112959
  }
112960
+ let value;
112961
+ try {
112962
+ value = JSON.parse(action.actionValue);
112963
+ } catch {
112964
+ return;
112965
+ }
112966
+ const requestId = value.requestId;
112967
+ if (!requestId) return;
112968
+ try {
112969
+ if (value.action === "permission_reply" && "reply" in value) {
112970
+ await deps.v2Client.permission.reply({
112971
+ requestID: requestId,
112972
+ reply: value.reply
112973
+ });
112974
+ } else if (value.action === "question_reply" && "answers" in value) {
112975
+ await deps.v2Client.question.reply({
112976
+ requestID: requestId,
112977
+ answers: value.answers
112978
+ });
112979
+ }
112980
+ } catch (err) {
112981
+ deps.log("error", "\u4EA4\u4E92\u56DE\u8C03\u5904\u7406\u5931\u8D25", {
112982
+ action: value.action,
112983
+ requestId,
112984
+ error: err instanceof Error ? err.message : String(err)
112985
+ });
112986
+ }
112867
112987
  }
112868
112988
  function buildCallbackResponse(action) {
112869
112989
  if (!action.actionValue) return {};
@@ -112887,8 +113007,71 @@ function buildCallbackResponse(action) {
112887
113007
  toast: { type: "success", content: "\u2705 \u5DF2\u56DE\u7B54" }
112888
113008
  };
112889
113009
  }
113010
+ if (value.action === "send_message") {
113011
+ return {
113012
+ toast: { type: "info", content: "\u{1F4E8} \u5DF2\u53D1\u9001" }
113013
+ };
113014
+ }
112890
113015
  return {};
112891
113016
  }
113017
+ function buildPermissionCardDSL(request, chatId, chatType) {
113018
+ const permission = String(request.permission ?? "unknown");
113019
+ const patterns = Array.isArray(request.patterns) ? request.patterns.map(String) : [];
113020
+ const requestId = String(request.id ?? "");
113021
+ const patternsText = patterns.length > 0 ? patterns.map((p) => `- \`${p}\``).join("\n") : "\uFF08\u65E0\u5177\u4F53\u8DEF\u5F84\uFF09";
113022
+ const buttons = [
113023
+ {
113024
+ text: "\u2705 \u5141\u8BB8\u4E00\u6B21",
113025
+ value: "",
113026
+ style: "primary",
113027
+ actionPayload: { action: "permission_reply", requestId, reply: "once" }
113028
+ },
113029
+ {
113030
+ text: "\u{1F513} \u59CB\u7EC8\u5141\u8BB8",
113031
+ value: "",
113032
+ style: "default",
113033
+ actionPayload: { action: "permission_reply", requestId, reply: "always" }
113034
+ },
113035
+ {
113036
+ text: "\u274C \u62D2\u7EDD",
113037
+ value: "",
113038
+ style: "danger",
113039
+ actionPayload: { action: "permission_reply", requestId, reply: "reject" }
113040
+ }
113041
+ ];
113042
+ const sections = [
113043
+ { type: "markdown", content: `AI \u8BF7\u6C42\u4EE5\u4E0B\u6743\u9650:
113044
+
113045
+ ${patternsText}` },
113046
+ { type: "actions", buttons }
113047
+ ];
113048
+ const dsl = { title: `\u{1F510} \u6743\u9650\u8BF7\u6C42: ${permission}`, template: "orange", sections };
113049
+ return { type: "card_kit", data: buildCardFromDSL(dsl, chatId, chatType) };
113050
+ }
113051
+ function buildQuestionCardDSL(request, chatId, chatType) {
113052
+ const questions = request.questions ?? [];
113053
+ const requestId = String(request.id ?? "");
113054
+ const q = questions[0];
113055
+ const header = String(q?.header ?? "AI \u63D0\u95EE");
113056
+ const questionText = String(q?.question ?? "\u8BF7\u9009\u62E9");
113057
+ const options = Array.isArray(q?.options) ? q.options : [];
113058
+ const buttons = options.map((opt, idx) => ({
113059
+ text: String(opt.label ?? opt.value ?? `\u9009\u9879 ${idx + 1}`),
113060
+ value: "",
113061
+ style: idx === 0 ? "primary" : "default",
113062
+ actionPayload: {
113063
+ action: "question_reply",
113064
+ requestId,
113065
+ answers: [[String(opt.value ?? opt.label ?? "")]]
113066
+ }
113067
+ }));
113068
+ const sections = [
113069
+ { type: "markdown", content: questionText },
113070
+ ...buttons.length > 0 ? [{ type: "actions", buttons }] : []
113071
+ ];
113072
+ const dsl = { title: header, template: "blue", sections };
113073
+ return { type: "card_kit", data: buildCardFromDSL(dsl, chatId, chatType) };
113074
+ }
112892
113075
 
112893
113076
  // src/feishu/dedup.ts
112894
113077
  var dedup = new TtlMap(10 * 60 * 1e3);
@@ -112903,8 +113086,7 @@ function isDuplicate(messageId) {
112903
113086
  }
112904
113087
 
112905
113088
  // src/feishu/resource.ts
112906
- var MAX_RESOURCE_SIZE = 10 * 1024 * 1024;
112907
- async function downloadMessageResource(client, messageId, fileKey, type, log) {
113089
+ async function downloadMessageResource(client, messageId, fileKey, type, log, maxSize) {
112908
113090
  try {
112909
113091
  const res = await client.im.messageResource.get({
112910
113092
  path: { message_id: messageId, file_key: fileKey },
@@ -112912,7 +113094,7 @@ async function downloadMessageResource(client, messageId, fileKey, type, log) {
112912
113094
  });
112913
113095
  if (!res) {
112914
113096
  log("warn", "\u8D44\u6E90\u4E0B\u8F7D\u8FD4\u56DE\u7A7A\u6570\u636E", { messageId, fileKey, type });
112915
- return null;
113097
+ return { resource: null, reason: "error" };
112916
113098
  }
112917
113099
  const stream4 = res.getReadableStream();
112918
113100
  const chunks = [];
@@ -112920,10 +113102,10 @@ async function downloadMessageResource(client, messageId, fileKey, type, log) {
112920
113102
  for await (const chunk of stream4) {
112921
113103
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
112922
113104
  totalSize += buf.length;
112923
- if (totalSize > MAX_RESOURCE_SIZE) {
112924
- log("warn", "\u8D44\u6E90\u8FC7\u5927\uFF0C\u8DF3\u8FC7\u4E0B\u8F7D", { messageId, fileKey, totalSize });
113105
+ if (totalSize > maxSize) {
113106
+ log("warn", "\u8D44\u6E90\u8FC7\u5927\uFF0C\u8DF3\u8FC7\u4E0B\u8F7D", { messageId, fileKey, totalSize, maxSize });
112925
113107
  stream4.destroy();
112926
- return null;
113108
+ return { resource: null, reason: "too_large", totalSize };
112927
113109
  }
112928
113110
  chunks.push(buf);
112929
113111
  }
@@ -112932,7 +113114,7 @@ async function downloadMessageResource(client, messageId, fileKey, type, log) {
112932
113114
  const contentType = headers?.["content-type"] ?? guessMimeByType(type);
112933
113115
  const base643 = buffer.toString("base64");
112934
113116
  const dataUrl = `data:${contentType};base64,${base643}`;
112935
- return { dataUrl, mime: contentType };
113117
+ return { resource: { dataUrl, mime: contentType }, reason: "ok" };
112936
113118
  } catch (err) {
112937
113119
  log("warn", "\u8D44\u6E90\u4E0B\u8F7D\u5931\u8D25", {
112938
113120
  messageId,
@@ -112940,7 +113122,7 @@ async function downloadMessageResource(client, messageId, fileKey, type, log) {
112940
113122
  type,
112941
113123
  error: err instanceof Error ? err.message : String(err)
112942
113124
  });
112943
- return null;
113125
+ return { resource: null, reason: "error" };
112944
113126
  }
112945
113127
  }
112946
113128
  function guessMimeByType(type) {
@@ -112980,19 +113162,19 @@ function guessMimeByFilename(filename) {
112980
113162
  }
112981
113163
 
112982
113164
  // src/feishu/content-extractor.ts
112983
- async function extractParts(feishuClient, messageId, messageType, rawContent, log) {
113165
+ async function extractParts(feishuClient, messageId, messageType, rawContent, log, maxResourceSize) {
112984
113166
  try {
112985
113167
  switch (messageType) {
112986
113168
  case "text":
112987
113169
  return extractText(rawContent);
112988
113170
  case "image":
112989
- return await extractImage(feishuClient, messageId, rawContent, log);
113171
+ return await extractImage(feishuClient, messageId, rawContent, log, maxResourceSize);
112990
113172
  case "post":
112991
113173
  return extractPost(rawContent);
112992
113174
  case "file":
112993
- return await extractFile(feishuClient, messageId, rawContent, log);
113175
+ return await extractFile(feishuClient, messageId, rawContent, log, maxResourceSize);
112994
113176
  case "audio":
112995
- return await extractAudio(feishuClient, messageId, rawContent, log);
113177
+ return await extractAudio(feishuClient, messageId, rawContent, log, maxResourceSize);
112996
113178
  case "media":
112997
113179
  return extractMediaFallback();
112998
113180
  case "sticker":
@@ -113063,13 +113245,15 @@ function extractText(rawContent) {
113063
113245
  if (!text) return [];
113064
113246
  return [{ type: "text", text }];
113065
113247
  }
113066
- async function extractImage(client, messageId, rawContent, log) {
113248
+ async function extractImage(client, messageId, rawContent, log, maxResourceSize) {
113067
113249
  const parsed = JSON.parse(rawContent);
113068
113250
  const imageKey = parsed.image_key;
113069
113251
  if (!imageKey) return [{ type: "text", text: "[\u56FE\u7247: \u65E0\u6CD5\u83B7\u53D6]" }];
113070
- const resource = await downloadMessageResource(client, messageId, imageKey, "image", log);
113071
- if (!resource) return [{ type: "text", text: "[\u56FE\u7247: \u4E0B\u8F7D\u5931\u8D25]" }];
113072
- return [{ type: "file", mime: resource.mime, url: resource.dataUrl }];
113252
+ const result = await downloadMessageResource(client, messageId, imageKey, "image", log, maxResourceSize);
113253
+ if (!result.resource) {
113254
+ return [{ type: "text", text: formatDownloadFailure("\u56FE\u7247", result, maxResourceSize) }];
113255
+ }
113256
+ return [{ type: "file", mime: result.resource.mime, url: result.resource.dataUrl }];
113073
113257
  }
113074
113258
  function extractPost(rawContent) {
113075
113259
  const text = extractPostText(rawContent);
@@ -113104,27 +113288,39 @@ function extractPostText(rawContent) {
113104
113288
  return "";
113105
113289
  }
113106
113290
  }
113107
- async function extractFile(client, messageId, rawContent, log) {
113291
+ async function extractFile(client, messageId, rawContent, log, maxResourceSize) {
113108
113292
  const parsed = JSON.parse(rawContent);
113109
113293
  const fileKey = parsed.file_key;
113110
113294
  const fileName = parsed.file_name ?? "\u672A\u77E5\u6587\u4EF6";
113111
113295
  if (!fileKey) return [{ type: "text", text: `[\u6587\u4EF6: ${fileName}]` }];
113112
113296
  const mime = guessMimeByFilename(fileName);
113113
- const resource = await downloadMessageResource(client, messageId, fileKey, "file", log);
113114
- if (!resource) return [{ type: "text", text: `[\u6587\u4EF6\u4E0B\u8F7D\u5931\u8D25: ${fileName}]` }];
113115
- return [{ type: "file", mime: resource.mime || mime, url: resource.dataUrl, filename: fileName }];
113297
+ const result = await downloadMessageResource(client, messageId, fileKey, "file", log, maxResourceSize);
113298
+ if (!result.resource) {
113299
+ return [{ type: "text", text: formatDownloadFailure(fileName, result, maxResourceSize) }];
113300
+ }
113301
+ return [{ type: "file", mime: result.resource.mime || mime, url: result.resource.dataUrl, filename: fileName }];
113116
113302
  }
113117
- async function extractAudio(client, messageId, rawContent, log) {
113303
+ async function extractAudio(client, messageId, rawContent, log, maxResourceSize) {
113118
113304
  const parsed = JSON.parse(rawContent);
113119
113305
  const fileKey = parsed.file_key;
113120
113306
  if (!fileKey) return [{ type: "text", text: "[\u8BED\u97F3: \u65E0\u6CD5\u83B7\u53D6]" }];
113121
- const resource = await downloadMessageResource(client, messageId, fileKey, "file", log);
113122
- if (!resource) return [{ type: "text", text: "[\u8BED\u97F3: \u4E0B\u8F7D\u5931\u8D25]" }];
113123
- return [{ type: "file", mime: resource.mime || "audio/opus", url: resource.dataUrl }];
113307
+ const result = await downloadMessageResource(client, messageId, fileKey, "file", log, maxResourceSize);
113308
+ if (!result.resource) {
113309
+ return [{ type: "text", text: formatDownloadFailure("\u8BED\u97F3", result, maxResourceSize) }];
113310
+ }
113311
+ return [{ type: "file", mime: result.resource.mime || "audio/opus", url: result.resource.dataUrl }];
113124
113312
  }
113125
113313
  function extractMediaFallback() {
113126
113314
  return [{ type: "text", text: "[\u89C6\u9891\u6D88\u606F]" }];
113127
113315
  }
113316
+ function formatDownloadFailure(label, result, maxSize) {
113317
+ if (result.reason === "too_large" && result.totalSize) {
113318
+ const sizeMB = (result.totalSize / (1024 * 1024)).toFixed(1);
113319
+ const limitMB = (maxSize / (1024 * 1024)).toFixed(0);
113320
+ return `[\u6587\u4EF6\u8FC7\u5927: ${label}, \u5DF2\u4E0B\u8F7D ${sizeMB}MB \u65F6\u8D85\u51FA ${limitMB}MB \u9650\u5236]`;
113321
+ }
113322
+ return `[\u4E0B\u8F7D\u5931\u8D25: ${label}]`;
113323
+ }
113128
113324
  function extractInteractive(rawContent) {
113129
113325
  try {
113130
113326
  const parsed = JSON.parse(rawContent);
@@ -113255,6 +113451,25 @@ function startFeishuGateway(options) {
113255
113451
  chatId: String(evt.context?.open_chat_id ?? evt.open_chat_id ?? ""),
113256
113452
  operatorId: String(evt.operator?.open_id ?? "")
113257
113453
  };
113454
+ const sendMsg = parseSendMessageAction(action);
113455
+ if (sendMsg) {
113456
+ const syntheticCtx = {
113457
+ chatId: sendMsg.chatId,
113458
+ messageId: `btn-${randomUUID()}`,
113459
+ messageType: "text",
113460
+ content: sendMsg.text,
113461
+ rawContent: JSON.stringify({ text: sendMsg.text }),
113462
+ chatType: sendMsg.chatType,
113463
+ senderId: action.operatorId ?? "",
113464
+ shouldReply: true
113465
+ };
113466
+ void Promise.resolve(onMessage(syntheticCtx)).catch((err) => {
113467
+ log("error", "send_message \u6309\u94AE\u5904\u7406\u5931\u8D25", {
113468
+ error: err instanceof Error ? err.message : String(err)
113469
+ });
113470
+ });
113471
+ return buildCallbackResponse(action);
113472
+ }
113258
113473
  if (onCardAction) {
113259
113474
  void onCardAction(action).catch((err) => {
113260
113475
  log("error", "card action \u5904\u7406\u5931\u8D25", {
@@ -113302,6 +113517,20 @@ function startFeishuGateway(options) {
113302
113517
  };
113303
113518
  return { client: larkClient, stop };
113304
113519
  }
113520
+ function parseSendMessageAction(action) {
113521
+ if (!action.actionValue) return void 0;
113522
+ try {
113523
+ const value = JSON.parse(action.actionValue);
113524
+ if (value.action !== "send_message") return void 0;
113525
+ const text = typeof value.text === "string" ? value.text : "";
113526
+ const chatId = typeof value.chatId === "string" ? value.chatId : "";
113527
+ if (!text || !chatId) return void 0;
113528
+ const chatType = value.chatType === "group" ? "group" : "p2p";
113529
+ return { chatId, chatType, text };
113530
+ } catch {
113531
+ return void 0;
113532
+ }
113533
+ }
113305
113534
 
113306
113535
  // src/handler/action-bus.ts
113307
113536
  var subscribers = /* @__PURE__ */ new Map();
@@ -113698,55 +113927,6 @@ async function getOrCreateSession(client, sessionKey, directory) {
113698
113927
  return session;
113699
113928
  }
113700
113929
 
113701
- // src/feishu/markdown.ts
113702
- var MAX_CARD_BYTES = 28 * 1024;
113703
- var TRUNCATION_SUFFIX = "\n\n*\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD*";
113704
- var TRUNCATION_SUFFIX_BYTES = new TextEncoder().encode(TRUNCATION_SUFFIX).length;
113705
- var CODE_FENCE_BYTES = 4;
113706
- var HTML_TAG_RE = /<\/?\w+(?:\s[^>]*)?\/?>/g;
113707
- function cleanMarkdown(text) {
113708
- let result = text.replace(/<br\s*\/?>/gi, "\n");
113709
- const { segments, codeBlocks } = extractCodeBlocks(result);
113710
- result = segments.map((seg) => seg.replace(HTML_TAG_RE, "")).join("\0");
113711
- let idx = 0;
113712
- result = result.replace(/\0/g, () => codeBlocks[idx++] ?? "");
113713
- result = closeCodeBlocks(result);
113714
- return result;
113715
- }
113716
- function truncateMarkdown(text, limit = MAX_CARD_BYTES) {
113717
- const bytes = new TextEncoder().encode(text);
113718
- if (bytes.length <= limit) return text;
113719
- const effectiveLimit = limit - TRUNCATION_SUFFIX_BYTES - CODE_FENCE_BYTES;
113720
- if (effectiveLimit <= 0) return TRUNCATION_SUFFIX;
113721
- const truncated = new TextDecoder().decode(bytes.slice(0, effectiveLimit));
113722
- const lastNewline = truncated.lastIndexOf("\n");
113723
- const cutPoint = lastNewline > effectiveLimit * 0.8 ? lastNewline : truncated.length;
113724
- let result = truncated.slice(0, cutPoint);
113725
- result = closeCodeBlocks(result);
113726
- return result + TRUNCATION_SUFFIX;
113727
- }
113728
- function extractCodeBlocks(text) {
113729
- const segments = [];
113730
- const codeBlocks = [];
113731
- const re = /```[\s\S]*?```/g;
113732
- let lastIndex = 0;
113733
- let match;
113734
- while ((match = re.exec(text)) !== null) {
113735
- segments.push(text.slice(lastIndex, match.index));
113736
- codeBlocks.push(match[0]);
113737
- lastIndex = match.index + match[0].length;
113738
- }
113739
- segments.push(text.slice(lastIndex));
113740
- return { segments, codeBlocks };
113741
- }
113742
- function closeCodeBlocks(text) {
113743
- const matches = text.match(/```/g);
113744
- if (matches && matches.length % 2 !== 0) {
113745
- return text + "\n```";
113746
- }
113747
- return text;
113748
- }
113749
-
113750
113930
  // src/feishu/streaming-card.ts
113751
113931
  var StreamingCard = class {
113752
113932
  constructor(cardkit, feishuClient, chatId, log) {
@@ -113762,6 +113942,7 @@ var StreamingCard = class {
113762
113942
  textBuffer = "";
113763
113943
  toolStates = /* @__PURE__ */ new Map();
113764
113944
  closed = false;
113945
+ toolsElementAdded = false;
113765
113946
  /**
113766
113947
  * 创建卡片 + 发送 interactive 消息 → messageId
113767
113948
  */
@@ -113769,15 +113950,14 @@ var StreamingCard = class {
113769
113950
  const schema = {
113770
113951
  data: {
113771
113952
  schema: "2.0",
113772
- config: { streaming_mode: true, summary: { content: "\u6B63\u5728\u601D\u8003..." } },
113953
+ config: { streaming_mode: true },
113773
113954
  header: {
113774
113955
  title: { tag: "plain_text", content: "AI \u56DE\u590D" },
113775
113956
  template: "blue"
113776
113957
  },
113777
113958
  body: {
113778
113959
  elements: [
113779
- { tag: "markdown", element_id: "content", content: "\u6B63\u5728\u601D\u8003..." },
113780
- { tag: "markdown", element_id: "tools", content: "" }
113960
+ { tag: "markdown", element_id: "content", content: "\u6B63\u5728\u601D\u8003..." }
113781
113961
  ]
113782
113962
  }
113783
113963
  }
@@ -113809,9 +113989,9 @@ var StreamingCard = class {
113809
113989
  /**
113810
113990
  * 更新工具状态到 tools 元素
113811
113991
  */
113812
- async setToolStatus(callID, tool, state) {
113992
+ async setToolStatus(callID, tool2, state) {
113813
113993
  if (this.closed || !this.cardId) return;
113814
- this.toolStates.set(callID, { tool, state });
113994
+ this.toolStates.set(callID, { tool: tool2, state });
113815
113995
  this.enqueue(() => this.doUpdateTools());
113816
113996
  }
113817
113997
  /**
@@ -113867,12 +114047,22 @@ var StreamingCard = class {
113867
114047
  const icon = ts.state === "completed" ? "\u2705" : ts.state === "error" ? "\u274C" : "\u{1F504}";
113868
114048
  lines.push(`${icon} ${ts.tool}`);
113869
114049
  }
113870
- await this.cardkit.updateElement(
113871
- this.cardId,
113872
- "tools",
113873
- lines.join("\n"),
113874
- ++this.seq
113875
- );
114050
+ const content = lines.join("\n");
114051
+ if (!this.toolsElementAdded) {
114052
+ this.toolsElementAdded = true;
114053
+ await this.cardkit.addElement(
114054
+ this.cardId,
114055
+ [{ tag: "markdown", element_id: "tools", content }],
114056
+ ++this.seq
114057
+ );
114058
+ } else {
114059
+ await this.cardkit.updateElement(
114060
+ this.cardId,
114061
+ "tools",
114062
+ content,
114063
+ ++this.seq
114064
+ );
114065
+ }
113876
114066
  }
113877
114067
  };
113878
114068
 
@@ -113899,7 +114089,8 @@ async function handleChat(ctx, deps, signal) {
113899
114089
  const query = directory ? { directory } : void 0;
113900
114090
  const sessionKey = buildSessionKey(chatType, chatType === "p2p" ? senderId : chatId);
113901
114091
  const session = await getOrCreateSession(client, sessionKey, directory);
113902
- const parts = await buildPromptParts(feishuClient, messageId, messageType, rawContent, content, chatType, senderId, log);
114092
+ registerSessionChat(session.id, chatId, chatType);
114093
+ const parts = await buildPromptParts(feishuClient, messageId, messageType, rawContent, content, chatType, senderId, log, config2.maxResourceSize);
113903
114094
  if (!parts.length) return void 0;
113904
114095
  log("info", "\u6536\u5230\u7528\u6237\u6D88\u606F", {
113905
114096
  sessionKey,
@@ -113984,12 +114175,12 @@ async function handleChat(ctx, deps, signal) {
113984
114175
  break;
113985
114176
  case "permission-requested":
113986
114177
  if (deps.interactiveDeps) {
113987
- handlePermissionRequested(action.request, chatId, deps.interactiveDeps);
114178
+ handlePermissionRequested(action.request, chatId, deps.interactiveDeps, chatType);
113988
114179
  }
113989
114180
  break;
113990
114181
  case "question-requested":
113991
114182
  if (deps.interactiveDeps) {
113992
- handleQuestionRequested(action.request, chatId, deps.interactiveDeps);
114183
+ handleQuestionRequested(action.request, chatId, deps.interactiveDeps, chatType);
113993
114184
  }
113994
114185
  break;
113995
114186
  }
@@ -114073,7 +114264,7 @@ async function handleChat(ctx, deps, signal) {
114073
114264
  unregisterPending(activeSessionId);
114074
114265
  }
114075
114266
  }
114076
- async function buildPromptParts(feishuClient, messageId, messageType, rawContent, textContent, chatType, senderId, log) {
114267
+ async function buildPromptParts(feishuClient, messageId, messageType, rawContent, textContent, chatType, senderId, log, maxResourceSize) {
114077
114268
  if (messageType === "text") {
114078
114269
  let promptText = textContent;
114079
114270
  if (chatType === "group" && senderId) {
@@ -114081,7 +114272,7 @@ async function buildPromptParts(feishuClient, messageId, messageType, rawContent
114081
114272
  }
114082
114273
  return [{ type: "text", text: promptText }];
114083
114274
  }
114084
- const parts = await extractParts(feishuClient, messageId, messageType, rawContent, log);
114275
+ const parts = await extractParts(feishuClient, messageId, messageType, rawContent, log, maxResourceSize);
114085
114276
  if (chatType === "group" && senderId && parts.length > 0) {
114086
114277
  return [{ type: "text", text: `[${senderId}]:` }, ...parts];
114087
114278
  }
@@ -114494,11 +114685,17 @@ function formatHistoryAsContext(messages) {
114494
114685
  return `${header}
114495
114686
  ${body}`;
114496
114687
  }
114497
-
114498
- // src/index.ts
114499
114688
  var SERVICE_NAME = "opencode-feishu";
114500
114689
  var LOG_PREFIX = "[feishu]";
114501
114690
  var isDebug = !!process.env.FEISHU_DEBUG;
114691
+ function loadFeishuSkill() {
114692
+ const skillPath = join(fileURLToPath(import.meta.url), "../../skills/feishu-card-interaction.md");
114693
+ if (existsSync(skillPath)) {
114694
+ return readFileSync(skillPath, "utf-8");
114695
+ }
114696
+ return "\u5F53\u524D\u7528\u6237\u901A\u8FC7\u98DE\u4E66\uFF08Feishu/Lark\uFF09\u4E0E\u4F60\u5BF9\u8BDD\u3002\u4F60\u53EF\u4EE5\u4F7F\u7528 feishu_send_card \u5DE5\u5177\u53D1\u9001\u683C\u5F0F\u5316\u5361\u7247\u6D88\u606F\uFF08\u652F\u6301\u6309\u94AE\u4EA4\u4E92\uFF09\u3002";
114697
+ }
114698
+ var feishuSystemPrompt = loadFeishuSkill();
114502
114699
  var FeishuPlugin = async (ctx) => {
114503
114700
  const { client } = ctx;
114504
114701
  let gateway = null;
@@ -114541,7 +114738,7 @@ ${details}`);
114541
114738
  });
114542
114739
  const cardkit = new CardKitClient(larkClient, log);
114543
114740
  const botOpenId = await fetchBotOpenId(larkClient, log);
114544
- const v2Client = void 0;
114741
+ const v2Client = createOpencodeClient({ directory: resolvedConfig.directory || void 0 });
114545
114742
  gateway = startFeishuGateway({
114546
114743
  config: resolvedConfig,
114547
114744
  larkClient,
@@ -114595,6 +114792,13 @@ ${details}`);
114595
114792
  event: async ({ event }) => {
114596
114793
  if (!gateway) return;
114597
114794
  await handleEvent(event, { log, directory: resolvedConfig.directory });
114795
+ },
114796
+ tool: {
114797
+ feishu_send_card: createSendCardTool({ feishuClient: larkClient, log })
114798
+ },
114799
+ "experimental.chat.system.transform": async (input, output) => {
114800
+ if (!input.sessionID || !getChatIdBySession(input.sessionID)) return;
114801
+ output.system.push(feishuSystemPrompt);
114598
114802
  }
114599
114803
  };
114600
114804
  return hooks;