opencode-feishu 1.2.0 → 1.3.0

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;
@@ -112623,94 +112625,49 @@ var CardKitClient = class {
112623
112625
  }
112624
112626
  };
112625
112627
 
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
- }
112628
+ // src/utils/ttl-map.ts
112629
+ var TtlMap = class {
112630
+ constructor(defaultTtlMs) {
112631
+ this.defaultTtlMs = defaultTtlMs;
112632
+ }
112633
+ data = /* @__PURE__ */ new Map();
112634
+ timers = /* @__PURE__ */ new Map();
112635
+ get(key) {
112636
+ return this.data.get(key);
112637
+ }
112638
+ has(key) {
112639
+ return this.data.has(key);
112640
+ }
112641
+ set(key, value, ttlMs) {
112642
+ this.delete(key);
112643
+ this.data.set(key, value);
112644
+ const timer = setTimeout(() => {
112645
+ this.data.delete(key);
112646
+ this.timers.delete(key);
112647
+ }, ttlMs ?? this.defaultTtlMs);
112648
+ timer.unref();
112649
+ this.timers.set(key, timer);
112650
+ }
112651
+ delete(key) {
112652
+ const timer = this.timers.get(key);
112653
+ if (timer) {
112654
+ clearTimeout(timer);
112655
+ this.timers.delete(key);
112674
112656
  }
112675
- };
112657
+ this.data.delete(key);
112658
+ }
112659
+ };
112660
+
112661
+ // src/feishu/session-chat-map.ts
112662
+ var sessionToChat = new TtlMap(24 * 60 * 60 * 1e3);
112663
+ function registerSessionChat(sessionId, chatId, chatType) {
112664
+ sessionToChat.set(sessionId, { chatId, chatType });
112676
112665
  }
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
- };
112666
+ function getChatIdBySession(sessionId) {
112667
+ return sessionToChat.get(sessionId)?.chatId;
112668
+ }
112669
+ function getChatInfoBySession(sessionId) {
112670
+ return sessionToChat.get(sessionId);
112714
112671
  }
112715
112672
 
112716
112673
  // src/feishu/sender.ts
@@ -112786,38 +112743,150 @@ async function sendCardMessage(client, chatId, cardId) {
112786
112743
  );
112787
112744
  }
112788
112745
 
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);
112746
+ // src/feishu/markdown.ts
112747
+ var MAX_CARD_BYTES = 28 * 1024;
112748
+ var TRUNCATION_SUFFIX = "\n\n*\u5185\u5BB9\u8FC7\u957F\uFF0C\u5DF2\u622A\u65AD*";
112749
+ var TRUNCATION_SUFFIX_BYTES = new TextEncoder().encode(TRUNCATION_SUFFIX).length;
112750
+ var CODE_FENCE_BYTES = 4;
112751
+ var HTML_TAG_RE = /<\/?\w+(?:\s[^>]*)?\/?>/g;
112752
+ function cleanMarkdown(text) {
112753
+ let result = text.replace(/<br\s*\/?>/gi, "\n");
112754
+ const { segments, codeBlocks } = extractCodeBlocks(result);
112755
+ result = segments.map((seg) => seg.replace(HTML_TAG_RE, "")).join("\0");
112756
+ let idx = 0;
112757
+ result = result.replace(/\0/g, () => codeBlocks[idx++] ?? "");
112758
+ result = closeCodeBlocks(result);
112759
+ return result;
112760
+ }
112761
+ function truncateMarkdown(text, limit = MAX_CARD_BYTES) {
112762
+ const bytes = new TextEncoder().encode(text);
112763
+ if (bytes.length <= limit) return text;
112764
+ const effectiveLimit = limit - TRUNCATION_SUFFIX_BYTES - CODE_FENCE_BYTES;
112765
+ if (effectiveLimit <= 0) return TRUNCATION_SUFFIX;
112766
+ const truncated = new TextDecoder().decode(bytes.slice(0, effectiveLimit));
112767
+ const lastNewline = truncated.lastIndexOf("\n");
112768
+ const cutPoint = lastNewline > effectiveLimit * 0.8 ? lastNewline : truncated.length;
112769
+ let result = truncated.slice(0, cutPoint);
112770
+ result = closeCodeBlocks(result);
112771
+ return result + TRUNCATION_SUFFIX;
112772
+ }
112773
+ function extractCodeBlocks(text) {
112774
+ const segments = [];
112775
+ const codeBlocks = [];
112776
+ const re = /```[\s\S]*?```/g;
112777
+ let lastIndex = 0;
112778
+ let match;
112779
+ while ((match = re.exec(text)) !== null) {
112780
+ segments.push(text.slice(lastIndex, match.index));
112781
+ codeBlocks.push(match[0]);
112782
+ lastIndex = match.index + match[0].length;
112801
112783
  }
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);
112784
+ segments.push(text.slice(lastIndex));
112785
+ return { segments, codeBlocks };
112786
+ }
112787
+ function closeCodeBlocks(text) {
112788
+ const matches = text.match(/```/g);
112789
+ if (matches && matches.length % 2 !== 0) {
112790
+ return text + "\n```";
112811
112791
  }
112812
- delete(key) {
112813
- const timer = this.timers.get(key);
112814
- if (timer) {
112815
- clearTimeout(timer);
112816
- this.timers.delete(key);
112792
+ return text;
112793
+ }
112794
+
112795
+ // src/tools/send-card.ts
112796
+ var z2 = tool.schema;
112797
+ var TEMPLATE_COLORS = ["blue", "green", "orange", "red", "purple", "grey"];
112798
+ function createSendCardTool(deps) {
112799
+ return tool({
112800
+ 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",
112801
+ args: {
112802
+ title: z2.string().describe("\u5361\u7247\u6807\u9898"),
112803
+ template: z2.enum(TEMPLATE_COLORS).default("blue").describe("\u6807\u9898\u989C\u8272\u4E3B\u9898"),
112804
+ sections: z2.array(
112805
+ z2.object({
112806
+ 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"),
112807
+ content: z2.string().optional().describe("\u533A\u5757\u5185\u5BB9\uFF08markdown \u683C\u5F0F\uFF0Cdivider/actions \u7C7B\u578B\u65E0\u9700\u6B64\u5B57\u6BB5\uFF09"),
112808
+ buttons: z2.array(
112809
+ z2.object({
112810
+ text: z2.string().describe("\u6309\u94AE\u663E\u793A\u6587\u672C\uFF082-6\u5B57\uFF09"),
112811
+ value: z2.string().describe("\u70B9\u51FB\u540E\u4F5C\u4E3A\u7528\u6237\u6D88\u606F\u53D1\u9001\u7684\u5185\u5BB9"),
112812
+ style: z2.enum(["primary", "default", "danger"]).default("default").describe("\u6309\u94AE\u6837\u5F0F")
112813
+ })
112814
+ ).optional().describe("\u6309\u94AE\u5217\u8868\uFF08\u4EC5 actions \u7C7B\u578B\u4F7F\u7528\uFF09")
112815
+ })
112816
+ ).min(1).describe("\u5361\u7247\u6B63\u6587\u533A\u5757\u5217\u8868")
112817
+ },
112818
+ async execute(args, context) {
112819
+ const chatId = getChatIdBySession(context.sessionID);
112820
+ if (!chatId) {
112821
+ return "\u9519\u8BEF\uFF1A\u5F53\u524D\u4F1A\u8BDD\u4E0D\u5173\u8054\u98DE\u4E66\u804A\u5929\uFF0C\u65E0\u6CD5\u53D1\u9001\u5361\u7247";
112822
+ }
112823
+ const chatInfo = getChatInfoBySession(context.sessionID);
112824
+ const card = buildCardFromDSL(args, chatId, chatInfo?.chatType ?? "p2p");
112825
+ const result = await sendInteractiveCard(deps.feishuClient, chatId, card);
112826
+ if (result.ok) {
112827
+ deps.log("info", "Agent \u5361\u7247\u5DF2\u53D1\u9001", {
112828
+ sessionId: context.sessionID,
112829
+ chatId,
112830
+ title: args.title,
112831
+ messageId: result.messageId
112832
+ });
112833
+ return `\u5361\u7247\u5DF2\u53D1\u9001\uFF1A\u300C${args.title}\u300D`;
112834
+ }
112835
+ deps.log("warn", "Agent \u5361\u7247\u53D1\u9001\u5931\u8D25", {
112836
+ sessionId: context.sessionID,
112837
+ chatId,
112838
+ title: args.title,
112839
+ error: result.error
112840
+ });
112841
+ return `\u5361\u7247\u53D1\u9001\u5931\u8D25\uFF1A${result.error}`;
112817
112842
  }
112818
- this.data.delete(key);
112819
- }
112820
- };
112843
+ });
112844
+ }
112845
+ function buildCardFromDSL(args, chatId, chatType) {
112846
+ return {
112847
+ schema: "2.0",
112848
+ config: { wide_screen_mode: true },
112849
+ header: {
112850
+ title: { tag: "plain_text", content: args.title },
112851
+ template: args.template
112852
+ },
112853
+ body: {
112854
+ elements: args.sections.map((s) => {
112855
+ switch (s.type) {
112856
+ case "divider":
112857
+ return { tag: "hr" };
112858
+ case "note":
112859
+ return {
112860
+ tag: "note",
112861
+ elements: [{ tag: "plain_text", content: s.content ?? "" }]
112862
+ };
112863
+ case "actions":
112864
+ if (!s.buttons?.length) return null;
112865
+ return {
112866
+ tag: "action",
112867
+ actions: s.buttons.map((btn) => ({
112868
+ tag: "button",
112869
+ text: { tag: "plain_text", content: btn.text },
112870
+ type: btn.style,
112871
+ value: JSON.stringify(btn.actionPayload ?? {
112872
+ action: "send_message",
112873
+ chatId,
112874
+ chatType,
112875
+ text: btn.value
112876
+ })
112877
+ }))
112878
+ };
112879
+ case "markdown":
112880
+ default:
112881
+ return {
112882
+ tag: "markdown",
112883
+ content: truncateMarkdown(s.content ?? "", 28e3)
112884
+ };
112885
+ }
112886
+ }).filter(Boolean)
112887
+ }
112888
+ };
112889
+ }
112821
112890
 
112822
112891
  // src/handler/interactive.ts
112823
112892
  var seenIds = new TtlMap(10 * 60 * 1e3);
@@ -112826,14 +112895,14 @@ function markSeen(requestId) {
112826
112895
  seenIds.set(requestId, true);
112827
112896
  return true;
112828
112897
  }
112829
- function handlePermissionRequested(request, chatId, deps) {
112898
+ function handlePermissionRequested(request, chatId, deps, chatType = "p2p") {
112830
112899
  if (!deps.v2Client) {
112831
112900
  deps.log("warn", "v2Client \u672A\u914D\u7F6E\uFF0C\u8DF3\u8FC7\u6743\u9650\u5361\u7247\u53D1\u9001", { requestId: String(request.id ?? "") });
112832
112901
  return;
112833
112902
  }
112834
112903
  const requestId = String(request.id ?? "");
112835
112904
  if (!requestId || !markSeen(requestId)) return;
112836
- const card = buildPermissionCard(request);
112905
+ const card = buildPermissionCardDSL(request, chatId, chatType);
112837
112906
  sendInteractiveCard(deps.feishuClient, chatId, card).catch((err) => {
112838
112907
  deps.log("warn", "\u53D1\u9001\u6743\u9650\u5361\u7247\u5931\u8D25", {
112839
112908
  requestId,
@@ -112841,14 +112910,14 @@ function handlePermissionRequested(request, chatId, deps) {
112841
112910
  });
112842
112911
  });
112843
112912
  }
112844
- function handleQuestionRequested(request, chatId, deps) {
112913
+ function handleQuestionRequested(request, chatId, deps, chatType = "p2p") {
112845
112914
  if (!deps.v2Client) {
112846
112915
  deps.log("warn", "v2Client \u672A\u914D\u7F6E\uFF0C\u8DF3\u8FC7\u95EE\u7B54\u5361\u7247\u53D1\u9001", { requestId: String(request.id ?? "") });
112847
112916
  return;
112848
112917
  }
112849
112918
  const requestId = String(request.id ?? "");
112850
112919
  if (!requestId || !markSeen(requestId)) return;
112851
- const card = buildQuestionCard(request);
112920
+ const card = buildQuestionCardDSL(request, chatId, chatType);
112852
112921
  sendInteractiveCard(deps.feishuClient, chatId, card).catch((err) => {
112853
112922
  deps.log("warn", "\u53D1\u9001\u95EE\u7B54\u5361\u7247\u5931\u8D25", {
112854
112923
  requestId,
@@ -112858,12 +112927,39 @@ function handleQuestionRequested(request, chatId, deps) {
112858
112927
  }
112859
112928
  async function handleCardAction(action, deps) {
112860
112929
  if (!action.actionValue) return;
112861
- {
112930
+ if (!deps.v2Client) {
112862
112931
  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
112932
  actionValue: action.actionValue
112864
112933
  });
112865
112934
  return;
112866
112935
  }
112936
+ let value;
112937
+ try {
112938
+ value = JSON.parse(action.actionValue);
112939
+ } catch {
112940
+ return;
112941
+ }
112942
+ const requestId = value.requestId;
112943
+ if (!requestId) return;
112944
+ try {
112945
+ if (value.action === "permission_reply" && "reply" in value) {
112946
+ await deps.v2Client.permission.reply({
112947
+ requestID: requestId,
112948
+ reply: value.reply
112949
+ });
112950
+ } else if (value.action === "question_reply" && "answers" in value) {
112951
+ await deps.v2Client.question.reply({
112952
+ requestID: requestId,
112953
+ answers: value.answers
112954
+ });
112955
+ }
112956
+ } catch (err) {
112957
+ deps.log("error", "\u4EA4\u4E92\u56DE\u8C03\u5904\u7406\u5931\u8D25", {
112958
+ action: value.action,
112959
+ requestId,
112960
+ error: err instanceof Error ? err.message : String(err)
112961
+ });
112962
+ }
112867
112963
  }
112868
112964
  function buildCallbackResponse(action) {
112869
112965
  if (!action.actionValue) return {};
@@ -112887,8 +112983,71 @@ function buildCallbackResponse(action) {
112887
112983
  toast: { type: "success", content: "\u2705 \u5DF2\u56DE\u7B54" }
112888
112984
  };
112889
112985
  }
112986
+ if (value.action === "send_message") {
112987
+ return {
112988
+ toast: { type: "info", content: "\u{1F4E8} \u5DF2\u53D1\u9001" }
112989
+ };
112990
+ }
112890
112991
  return {};
112891
112992
  }
112993
+ function buildPermissionCardDSL(request, chatId, chatType) {
112994
+ const permission = String(request.permission ?? "unknown");
112995
+ const patterns = Array.isArray(request.patterns) ? request.patterns.map(String) : [];
112996
+ const requestId = String(request.id ?? "");
112997
+ const patternsText = patterns.length > 0 ? patterns.map((p) => `- \`${p}\``).join("\n") : "\uFF08\u65E0\u5177\u4F53\u8DEF\u5F84\uFF09";
112998
+ const buttons = [
112999
+ {
113000
+ text: "\u2705 \u5141\u8BB8\u4E00\u6B21",
113001
+ value: "",
113002
+ style: "primary",
113003
+ actionPayload: { action: "permission_reply", requestId, reply: "once" }
113004
+ },
113005
+ {
113006
+ text: "\u{1F513} \u59CB\u7EC8\u5141\u8BB8",
113007
+ value: "",
113008
+ style: "default",
113009
+ actionPayload: { action: "permission_reply", requestId, reply: "always" }
113010
+ },
113011
+ {
113012
+ text: "\u274C \u62D2\u7EDD",
113013
+ value: "",
113014
+ style: "danger",
113015
+ actionPayload: { action: "permission_reply", requestId, reply: "reject" }
113016
+ }
113017
+ ];
113018
+ const sections = [
113019
+ { type: "markdown", content: `AI \u8BF7\u6C42\u4EE5\u4E0B\u6743\u9650:
113020
+
113021
+ ${patternsText}` },
113022
+ { type: "actions", buttons }
113023
+ ];
113024
+ const dsl = { title: `\u{1F510} \u6743\u9650\u8BF7\u6C42: ${permission}`, template: "orange", sections };
113025
+ return { type: "card_kit", data: buildCardFromDSL(dsl, chatId, chatType) };
113026
+ }
113027
+ function buildQuestionCardDSL(request, chatId, chatType) {
113028
+ const questions = request.questions ?? [];
113029
+ const requestId = String(request.id ?? "");
113030
+ const q = questions[0];
113031
+ const header = String(q?.header ?? "AI \u63D0\u95EE");
113032
+ const questionText = String(q?.question ?? "\u8BF7\u9009\u62E9");
113033
+ const options = Array.isArray(q?.options) ? q.options : [];
113034
+ const buttons = options.map((opt, idx) => ({
113035
+ text: String(opt.label ?? opt.value ?? `\u9009\u9879 ${idx + 1}`),
113036
+ value: "",
113037
+ style: idx === 0 ? "primary" : "default",
113038
+ actionPayload: {
113039
+ action: "question_reply",
113040
+ requestId,
113041
+ answers: [[String(opt.value ?? opt.label ?? "")]]
113042
+ }
113043
+ }));
113044
+ const sections = [
113045
+ { type: "markdown", content: questionText },
113046
+ ...buttons.length > 0 ? [{ type: "actions", buttons }] : []
113047
+ ];
113048
+ const dsl = { title: header, template: "blue", sections };
113049
+ return { type: "card_kit", data: buildCardFromDSL(dsl, chatId, chatType) };
113050
+ }
112892
113051
 
112893
113052
  // src/feishu/dedup.ts
112894
113053
  var dedup = new TtlMap(10 * 60 * 1e3);
@@ -113255,6 +113414,25 @@ function startFeishuGateway(options) {
113255
113414
  chatId: String(evt.context?.open_chat_id ?? evt.open_chat_id ?? ""),
113256
113415
  operatorId: String(evt.operator?.open_id ?? "")
113257
113416
  };
113417
+ const sendMsg = parseSendMessageAction(action);
113418
+ if (sendMsg) {
113419
+ const syntheticCtx = {
113420
+ chatId: sendMsg.chatId,
113421
+ messageId: `btn-${randomUUID()}`,
113422
+ messageType: "text",
113423
+ content: sendMsg.text,
113424
+ rawContent: JSON.stringify({ text: sendMsg.text }),
113425
+ chatType: sendMsg.chatType,
113426
+ senderId: action.operatorId ?? "",
113427
+ shouldReply: true
113428
+ };
113429
+ void Promise.resolve(onMessage(syntheticCtx)).catch((err) => {
113430
+ log("error", "send_message \u6309\u94AE\u5904\u7406\u5931\u8D25", {
113431
+ error: err instanceof Error ? err.message : String(err)
113432
+ });
113433
+ });
113434
+ return buildCallbackResponse(action);
113435
+ }
113258
113436
  if (onCardAction) {
113259
113437
  void onCardAction(action).catch((err) => {
113260
113438
  log("error", "card action \u5904\u7406\u5931\u8D25", {
@@ -113302,6 +113480,20 @@ function startFeishuGateway(options) {
113302
113480
  };
113303
113481
  return { client: larkClient, stop };
113304
113482
  }
113483
+ function parseSendMessageAction(action) {
113484
+ if (!action.actionValue) return void 0;
113485
+ try {
113486
+ const value = JSON.parse(action.actionValue);
113487
+ if (value.action !== "send_message") return void 0;
113488
+ const text = typeof value.text === "string" ? value.text : "";
113489
+ const chatId = typeof value.chatId === "string" ? value.chatId : "";
113490
+ if (!text || !chatId) return void 0;
113491
+ const chatType = value.chatType === "group" ? "group" : "p2p";
113492
+ return { chatId, chatType, text };
113493
+ } catch {
113494
+ return void 0;
113495
+ }
113496
+ }
113305
113497
 
113306
113498
  // src/handler/action-bus.ts
113307
113499
  var subscribers = /* @__PURE__ */ new Map();
@@ -113698,55 +113890,6 @@ async function getOrCreateSession(client, sessionKey, directory) {
113698
113890
  return session;
113699
113891
  }
113700
113892
 
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
113893
  // src/feishu/streaming-card.ts
113751
113894
  var StreamingCard = class {
113752
113895
  constructor(cardkit, feishuClient, chatId, log) {
@@ -113809,9 +113952,9 @@ var StreamingCard = class {
113809
113952
  /**
113810
113953
  * 更新工具状态到 tools 元素
113811
113954
  */
113812
- async setToolStatus(callID, tool, state) {
113955
+ async setToolStatus(callID, tool2, state) {
113813
113956
  if (this.closed || !this.cardId) return;
113814
- this.toolStates.set(callID, { tool, state });
113957
+ this.toolStates.set(callID, { tool: tool2, state });
113815
113958
  this.enqueue(() => this.doUpdateTools());
113816
113959
  }
113817
113960
  /**
@@ -113899,6 +114042,7 @@ async function handleChat(ctx, deps, signal) {
113899
114042
  const query = directory ? { directory } : void 0;
113900
114043
  const sessionKey = buildSessionKey(chatType, chatType === "p2p" ? senderId : chatId);
113901
114044
  const session = await getOrCreateSession(client, sessionKey, directory);
114045
+ registerSessionChat(session.id, chatId, chatType);
113902
114046
  const parts = await buildPromptParts(feishuClient, messageId, messageType, rawContent, content, chatType, senderId, log);
113903
114047
  if (!parts.length) return void 0;
113904
114048
  log("info", "\u6536\u5230\u7528\u6237\u6D88\u606F", {
@@ -113984,12 +114128,12 @@ async function handleChat(ctx, deps, signal) {
113984
114128
  break;
113985
114129
  case "permission-requested":
113986
114130
  if (deps.interactiveDeps) {
113987
- handlePermissionRequested(action.request, chatId, deps.interactiveDeps);
114131
+ handlePermissionRequested(action.request, chatId, deps.interactiveDeps, chatType);
113988
114132
  }
113989
114133
  break;
113990
114134
  case "question-requested":
113991
114135
  if (deps.interactiveDeps) {
113992
- handleQuestionRequested(action.request, chatId, deps.interactiveDeps);
114136
+ handleQuestionRequested(action.request, chatId, deps.interactiveDeps, chatType);
113993
114137
  }
113994
114138
  break;
113995
114139
  }
@@ -114494,11 +114638,17 @@ function formatHistoryAsContext(messages) {
114494
114638
  return `${header}
114495
114639
  ${body}`;
114496
114640
  }
114497
-
114498
- // src/index.ts
114499
114641
  var SERVICE_NAME = "opencode-feishu";
114500
114642
  var LOG_PREFIX = "[feishu]";
114501
114643
  var isDebug = !!process.env.FEISHU_DEBUG;
114644
+ function loadFeishuSkill() {
114645
+ const skillPath = join(fileURLToPath(import.meta.url), "../../skills/feishu-card-interaction.md");
114646
+ if (existsSync(skillPath)) {
114647
+ return readFileSync(skillPath, "utf-8");
114648
+ }
114649
+ 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";
114650
+ }
114651
+ var feishuSystemPrompt = loadFeishuSkill();
114502
114652
  var FeishuPlugin = async (ctx) => {
114503
114653
  const { client } = ctx;
114504
114654
  let gateway = null;
@@ -114541,7 +114691,7 @@ ${details}`);
114541
114691
  });
114542
114692
  const cardkit = new CardKitClient(larkClient, log);
114543
114693
  const botOpenId = await fetchBotOpenId(larkClient, log);
114544
- const v2Client = void 0;
114694
+ const v2Client = createOpencodeClient({ directory: resolvedConfig.directory || void 0 });
114545
114695
  gateway = startFeishuGateway({
114546
114696
  config: resolvedConfig,
114547
114697
  larkClient,
@@ -114595,6 +114745,13 @@ ${details}`);
114595
114745
  event: async ({ event }) => {
114596
114746
  if (!gateway) return;
114597
114747
  await handleEvent(event, { log, directory: resolvedConfig.directory });
114748
+ },
114749
+ tool: {
114750
+ feishu_send_card: createSendCardTool({ feishuClient: larkClient, log })
114751
+ },
114752
+ "experimental.chat.system.transform": async (input, output) => {
114753
+ if (!input.sessionID || !getChatIdBySession(input.sessionID)) return;
114754
+ output.system.push(feishuSystemPrompt);
114598
114755
  }
114599
114756
  };
114600
114757
  return hooks;