@keyou007/agent-bot 0.1.14-alpha.0 → 0.1.14-alpha.2

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 (113) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +82 -55
  3. package/README.zh.md +82 -55
  4. package/config.example.yaml +5 -1
  5. package/dist/cli/AutostartManager.js +431 -0
  6. package/dist/cli/AutostartManager.js.map +1 -0
  7. package/dist/cli/FeishuAppConfiguration.js +17 -13
  8. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  9. package/dist/cli/Initializer.js +35 -0
  10. package/dist/cli/Initializer.js.map +1 -1
  11. package/dist/cli/SelfUpdateRunner.js +552 -0
  12. package/dist/cli/SelfUpdateRunner.js.map +1 -0
  13. package/dist/cli/SelfUpdater.js +431 -0
  14. package/dist/cli/SelfUpdater.js.map +1 -0
  15. package/dist/cli/autostartOutput.js +63 -0
  16. package/dist/cli/autostartOutput.js.map +1 -0
  17. package/dist/cli/controlProtocol.js.map +1 -1
  18. package/dist/cli/help.js +92 -18
  19. package/dist/cli/help.js.map +1 -1
  20. package/dist/cli/taskGroupOptions.js +14 -11
  21. package/dist/cli/taskGroupOptions.js.map +1 -1
  22. package/dist/cli/taskListOutput.js +3 -0
  23. package/dist/cli/taskListOutput.js.map +1 -1
  24. package/dist/cli/taskTarget.js +99 -0
  25. package/dist/cli/taskTarget.js.map +1 -0
  26. package/dist/cli.js +643 -58
  27. package/dist/cli.js.map +1 -1
  28. package/dist/codex/CodexRuntime.js +16 -5
  29. package/dist/codex/CodexRuntime.js.map +1 -1
  30. package/dist/commands/CommandRouter.js +20 -1
  31. package/dist/commands/CommandRouter.js.map +1 -1
  32. package/dist/config/loadConfig.js +2 -0
  33. package/dist/config/loadConfig.js.map +1 -1
  34. package/dist/config/schema.js +2 -0
  35. package/dist/config/schema.js.map +1 -1
  36. package/dist/console/ConsoleTurnPresenter.js +2 -0
  37. package/dist/console/ConsoleTurnPresenter.js.map +1 -1
  38. package/dist/feishu/CardRenderer.js +584 -16
  39. package/dist/feishu/CardRenderer.js.map +1 -1
  40. package/dist/feishu/FeishuConnector.js +29 -8
  41. package/dist/feishu/FeishuConnector.js.map +1 -1
  42. package/dist/feishu/FeishuMarkdown.js +25 -1
  43. package/dist/feishu/FeishuMarkdown.js.map +1 -1
  44. package/dist/feishu/FeishuMessageClient.js +43 -0
  45. package/dist/feishu/FeishuMessageClient.js.map +1 -1
  46. package/dist/feishu/FeishuTurnPresenter.js +29 -1
  47. package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
  48. package/dist/feishu/ownerAccess.js +7 -0
  49. package/dist/feishu/ownerAccess.js.map +1 -0
  50. package/dist/index.js +148 -19
  51. package/dist/index.js.map +1 -1
  52. package/dist/presentation/OutboundRouter.js +19 -1
  53. package/dist/presentation/OutboundRouter.js.map +1 -1
  54. package/dist/proxy/ProxySessionController.js +1444 -66
  55. package/dist/proxy/ProxySessionController.js.map +1 -1
  56. package/dist/startup/StartupNotifier.js +65 -16
  57. package/dist/startup/StartupNotifier.js.map +1 -1
  58. package/dist/startup/startFeishu.js +2 -2
  59. package/dist/startup/startFeishu.js.map +1 -1
  60. package/dist/state/StateStore.js +268 -67
  61. package/dist/state/StateStore.js.map +1 -1
  62. package/dist/state/migrations.js +26 -0
  63. package/dist/state/migrations.js.map +1 -1
  64. package/dist/supervision/SafeRestartNotifier.js +17 -16
  65. package/dist/supervision/SafeRestartNotifier.js.map +1 -1
  66. package/dist/supervision/SafeRestartScheduler.js +12 -7
  67. package/dist/supervision/SafeRestartScheduler.js.map +1 -1
  68. package/dist/supervision/replacementSupervisor.js +45 -4
  69. package/dist/supervision/replacementSupervisor.js.map +1 -1
  70. package/dist/supervisor.js +30 -18
  71. package/dist/supervisor.js.map +1 -1
  72. package/docs/technical-reference.md +33 -10
  73. package/docs/technical-reference.zh.md +33 -10
  74. package/npm-shrinkwrap.json +2 -2
  75. package/package.json +2 -2
  76. package/skills/agent-bot/SKILL.md +107 -182
  77. package/src/cli/AutostartManager.ts +579 -0
  78. package/src/cli/FeishuAppConfiguration.ts +22 -7
  79. package/src/cli/Initializer.ts +57 -0
  80. package/src/cli/SelfUpdateRunner.ts +817 -0
  81. package/src/cli/SelfUpdater.ts +688 -0
  82. package/src/cli/autostartOutput.ts +75 -0
  83. package/src/cli/controlProtocol.ts +111 -1
  84. package/src/cli/help.ts +92 -18
  85. package/src/cli/taskGroupOptions.ts +27 -17
  86. package/src/cli/taskListOutput.ts +10 -0
  87. package/src/cli/taskTarget.ts +168 -0
  88. package/src/cli.ts +781 -65
  89. package/src/codex/CodexRuntime.ts +17 -5
  90. package/src/commands/CommandRouter.ts +19 -1
  91. package/src/commands/commandTypes.ts +3 -0
  92. package/src/config/loadConfig.ts +2 -0
  93. package/src/config/schema.ts +2 -0
  94. package/src/console/ConsoleTurnPresenter.ts +4 -1
  95. package/src/feishu/CardRenderer.ts +716 -16
  96. package/src/feishu/FeishuConnector.ts +44 -10
  97. package/src/feishu/FeishuMarkdown.ts +27 -1
  98. package/src/feishu/FeishuMessageClient.ts +58 -0
  99. package/src/feishu/FeishuTurnPresenter.ts +35 -2
  100. package/src/feishu/ownerAccess.ts +7 -0
  101. package/src/feishu/types.ts +7 -0
  102. package/src/index.ts +192 -22
  103. package/src/presentation/OutboundRouter.ts +28 -3
  104. package/src/proxy/ProxySessionController.ts +1707 -70
  105. package/src/runtime/types.ts +1 -0
  106. package/src/startup/StartupNotifier.ts +67 -17
  107. package/src/startup/startFeishu.ts +5 -3
  108. package/src/state/StateStore.ts +413 -65
  109. package/src/state/migrations.ts +26 -0
  110. package/src/supervision/SafeRestartNotifier.ts +17 -17
  111. package/src/supervision/SafeRestartScheduler.ts +13 -6
  112. package/src/supervision/replacementSupervisor.ts +50 -4
  113. package/src/supervisor.ts +35 -16
@@ -2,6 +2,7 @@ import type { Logger } from "pino";
2
2
  import type { AppConfig } from "../config/schema.js";
3
3
  import { threadContextKey } from "./contextKey.js";
4
4
  import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
5
+ import { allowsFeishuUser } from "./ownerAccess.js";
5
6
  import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
6
7
 
7
8
  type BotOpenIdResolver = (appId: string, appSecret: string) => Promise<string>;
@@ -33,18 +34,31 @@ export class FeishuConnector {
33
34
  this.logger.warn({ error }, "Failed to resolve the Lark bot Open ID for the Agent environment.");
34
35
  }
35
36
  }
36
- await this.startFeishuWs(appId, appSecret, respondToAllGroupMessages ? undefined : botOpenId);
37
+ await this.startFeishuWs(appId, appSecret, botOpenId, !respondToAllGroupMessages);
37
38
  }
38
39
 
39
40
  stop(): void {
40
41
  // SDK connector currently relies on process lifetime.
41
42
  }
42
43
 
43
- private async startFeishuWs(appId: string, appSecret: string, requiredMentionOpenId?: string): Promise<void> {
44
+ private async startFeishuWs(
45
+ appId: string,
46
+ appSecret: string,
47
+ botOpenId?: string,
48
+ requireMention = false,
49
+ ): Promise<void> {
44
50
  const lark = (await import("@larksuiteoapi/node-sdk")) as Record<string, any>;
45
51
  const eventDispatcher = new lark.EventDispatcher({}).register({
46
52
  "im.message.receive_v1": async (data: unknown) => {
47
- const message = toIncomingMessage(data, requiredMentionOpenId);
53
+ const senderOpenId = getFeishuEvent(data)?.sender?.sender_id?.open_id;
54
+ if (!allowsFeishuUser(this.config, senderOpenId)) {
55
+ this.logger.debug(
56
+ { messageId: getFeishuEvent(data)?.message?.message_id },
57
+ "Ignored a Feishu message from a non-owner.",
58
+ );
59
+ return;
60
+ }
61
+ const message = toIncomingMessage(data, botOpenId, requireMention);
48
62
  if (!message) {
49
63
  this.logger.debug({ data }, "Ignored unsupported Feishu message event.");
50
64
  return;
@@ -74,6 +88,14 @@ export class FeishuConnector {
74
88
  });
75
89
  },
76
90
  "card.action.trigger": async (data: unknown) => {
91
+ const operatorOpenId = getFeishuEvent(data)?.operator?.open_id;
92
+ if (!allowsFeishuUser(this.config, operatorOpenId)) {
93
+ this.logger.debug(
94
+ { actionId: getEventId(data) },
95
+ "Ignored a Feishu card action from a non-owner.",
96
+ );
97
+ return {};
98
+ }
77
99
  const action = toCardAction(data);
78
100
  if (!action) {
79
101
  this.logger.debug({ data }, "Ignored unsupported Feishu card action.");
@@ -115,7 +137,11 @@ function toChatUpdatedEvent(data: unknown): ChatUpdatedEvent | undefined {
115
137
  };
116
138
  }
117
139
 
118
- function toIncomingMessage(data: unknown, requiredMentionOpenId?: string): IncomingMessage | undefined {
140
+ function toIncomingMessage(
141
+ data: unknown,
142
+ botOpenId?: string,
143
+ requireMention = false,
144
+ ): IncomingMessage | undefined {
119
145
  const event = getFeishuEvent(data);
120
146
  const message = event?.message;
121
147
  if (!message) {
@@ -128,11 +154,10 @@ function toIncomingMessage(data: unknown, requiredMentionOpenId?: string): Incom
128
154
  if (!parsed) return undefined;
129
155
  const chatId = message.chat_id;
130
156
  const chatType = message.chat_type === "group" ? "group" : "p2p";
131
- if (
132
- chatType === "group" &&
133
- requiredMentionOpenId &&
134
- !mentionsOpenId(message.mentions, requiredMentionOpenId)
135
- ) {
157
+ const mentionedBot = chatType === "group"
158
+ && Boolean(botOpenId)
159
+ && mentionsOpenId(message.mentions, botOpenId!);
160
+ if (chatType === "group" && requireMention && !mentionedBot) {
136
161
  return undefined;
137
162
  }
138
163
  const threadId = typeof message.thread_id === "string" && message.thread_id ? message.thread_id : undefined;
@@ -161,6 +186,7 @@ function toIncomingMessage(data: unknown, requiredMentionOpenId?: string): Incom
161
186
  ...(threadId ? { threadId } : {}),
162
187
  ...(rootMessageId ? { rootMessageId } : {}),
163
188
  ...(parentMessageId ? { parentMessageId } : {}),
189
+ ...(mentionedBot ? { mentionedBot: true as const } : {}),
164
190
  text: parsed.text,
165
191
  ...(parsed.images.length > 0 ? { images: parsed.images.map((imageKey) => ({ imageKey })) } : {}),
166
192
  };
@@ -250,10 +276,18 @@ function toCardAction(data: unknown) {
250
276
  contextKey: chatId ? `chat_id:${chatId}` : `open_id:${openId}`,
251
277
  userId: openId,
252
278
  ...(typeof event.context?.open_message_id === "string" ? { messageId: event.context.open_message_id } : {}),
253
- value: isRecord(event.action.value) ? event.action.value : {},
279
+ value: getCardActionValue(event.action),
254
280
  };
255
281
  }
256
282
 
283
+ function getCardActionValue(action: Record<string, unknown>): Record<string, unknown> {
284
+ if (action.tag === "overflow" && typeof action.option === "string") {
285
+ const option = parseJsonObject(action.option);
286
+ if (Object.keys(option).length > 0) return option;
287
+ }
288
+ return isRecord(action.value) ? action.value : {};
289
+ }
290
+
257
291
  function getEventId(data: unknown): string | undefined {
258
292
  const value = getNested<unknown>(data, ["header", "event_id"]) ?? getNested(data, ["data", "header", "event_id"]);
259
293
  return typeof value === "string" ? value : undefined;
@@ -6,6 +6,8 @@ interface FenceState {
6
6
  }
7
7
 
8
8
  const FENCE_OPENER = /^([ \t]*)((?:`{3,})|(?:~{3,}))(.*)$/;
9
+ const MARKDOWN_LINK = /(!?)\[([^\]\r\n]*)\]\((<?[^)\r\n]+>?)\)/g;
10
+ const LOCAL_FILE_LINE_REFERENCE = /(:\d+(?::\d+)?)$/;
9
11
 
10
12
  export function normalizeFeishuMarkdown(markdown: string): string {
11
13
  const lines = markdown.match(/[^\r\n]*(?:\r\n|\n)|[^\r\n]+$/g) ?? [];
@@ -17,7 +19,7 @@ export function normalizeFeishuMarkdown(markdown: string): string {
17
19
 
18
20
  if (!fence) {
19
21
  const opener = FENCE_OPENER.exec(body);
20
- if (!opener?.[2]) return line;
22
+ if (!opener?.[2]) return `${includeLocalFileLineReferences(body)}${ending}`;
21
23
 
22
24
  const indent = opener[1] ?? "";
23
25
  const delimiter = opener[2];
@@ -38,6 +40,30 @@ export function normalizeFeishuMarkdown(markdown: string): string {
38
40
  }).join("");
39
41
  }
40
42
 
43
+ function includeLocalFileLineReferences(markdown: string): string {
44
+ return markdown.replace(MARKDOWN_LINK, (link, imageMarker: string, label: string, rawTarget: string) => {
45
+ if (imageMarker) return link;
46
+
47
+ const target = unwrapMarkdownTarget(rawTarget.trim());
48
+ if (!isLocalFileTarget(target)) return link;
49
+
50
+ const reference = LOCAL_FILE_LINE_REFERENCE.exec(target)?.[1];
51
+ if (!reference || label.endsWith(reference)) return link;
52
+ return `[${label}${reference}](${rawTarget})`;
53
+ });
54
+ }
55
+
56
+ function unwrapMarkdownTarget(target: string): string {
57
+ return target.startsWith("<") && target.endsWith(">") ? target.slice(1, -1) : target;
58
+ }
59
+
60
+ function isLocalFileTarget(target: string): boolean {
61
+ return /^file:\/\//i.test(target)
62
+ || /^\/?[a-z]:[\\/]/i.test(target)
63
+ || /^\\\\/.test(target)
64
+ || /^\/(?!\/)/.test(target);
65
+ }
66
+
41
67
  function isFenceCloser(line: string, fence: FenceState): boolean {
42
68
  const match = /^[ \t]*(`+|~+)[ \t]*$/.exec(line);
43
69
  const delimiter = match?.[1];
@@ -48,6 +48,15 @@ interface UploadImageResponse {
48
48
  error?: unknown;
49
49
  }
50
50
 
51
+ interface UploadFileResponse {
52
+ code: number;
53
+ msg: string;
54
+ data?: {
55
+ file_key?: string;
56
+ };
57
+ error?: unknown;
58
+ }
59
+
51
60
  interface ReactionResponse {
52
61
  code: number;
53
62
  msg: string;
@@ -178,6 +187,11 @@ export class FeishuMessageClient implements FeishuOutbound {
178
187
  return this.sendMessage(contextKey, "text", { text });
179
188
  }
180
189
 
190
+ async sendFile(contextKey: string, filePath: string): Promise<string | undefined> {
191
+ const fileKey = await this.uploadFile(filePath);
192
+ return this.sendMessage(contextKey, "file", { file_key: fileKey });
193
+ }
194
+
181
195
  async sendMarkdown(contextKey: string, markdown: string, idempotencyKey?: string): Promise<string | undefined> {
182
196
  const normalizedMarkdown = normalizeFeishuMarkdown(markdown);
183
197
  const elements = await renderMarkdownWithLocalImages(
@@ -210,6 +224,15 @@ export class FeishuMessageClient implements FeishuOutbound {
210
224
  return this.replyMessage(contextKey, target, "text", { text }, idempotencyKey);
211
225
  }
212
226
 
227
+ async replyFile(
228
+ contextKey: string,
229
+ target: MessageReplyTarget,
230
+ filePath: string,
231
+ ): Promise<string | undefined> {
232
+ const fileKey = await this.uploadFile(filePath);
233
+ return this.replyMessage(contextKey, target, "file", { file_key: fileKey });
234
+ }
235
+
213
236
  async replyMarkdown(
214
237
  contextKey: string,
215
238
  target: MessageReplyTarget,
@@ -554,6 +577,40 @@ export class FeishuMessageClient implements FeishuOutbound {
554
577
  }
555
578
  }
556
579
 
580
+ private async uploadFile(filePath: string): Promise<string> {
581
+ const fileName = path.basename(filePath);
582
+ try {
583
+ const fileStats = await stat(filePath);
584
+ if (!fileStats.isFile()) throw new Error(`Not a regular file: ${fileName}`);
585
+ if (fileStats.size === 0) throw new Error(`Feishu does not accept empty files: ${fileName}`);
586
+ if (fileStats.size > 30 * 1024 * 1024) {
587
+ throw new Error(`File exceeds Feishu's 30 MiB limit: ${fileName}`);
588
+ }
589
+
590
+ const contents = await readFile(filePath);
591
+ const token = await this.getTenantAccessToken();
592
+ const form = new FormData();
593
+ form.append("file_type", "stream");
594
+ form.append("file_name", fileName);
595
+ const bytes = new ArrayBuffer(contents.byteLength);
596
+ new Uint8Array(bytes).set(contents);
597
+ form.append("file", new Blob([bytes]), fileName);
598
+ const response = await fetch("https://open.feishu.cn/open-apis/im/v1/files", {
599
+ method: "POST",
600
+ headers: { Authorization: `Bearer ${token}` },
601
+ body: form,
602
+ });
603
+ const payload = (await response.json()) as UploadFileResponse;
604
+ const fileKey = payload.data?.file_key;
605
+ if (!response.ok || payload.code !== 0 || !fileKey) {
606
+ throw new FeishuApiError(payload.msg || response.statusText, payload.code, payload, "upload file", response.status);
607
+ }
608
+ return fileKey;
609
+ } catch (error) {
610
+ throw normalizeTransportError(error, "upload file");
611
+ }
612
+ }
613
+
557
614
  private async uploadImageBytes(
558
615
  contents: Uint8Array,
559
616
  imageType: "message" | "avatar",
@@ -675,6 +732,7 @@ export class FeishuApiError extends Error {
675
732
  | SendMessageResponse
676
733
  | CreateGroupResponse
677
734
  | UploadImageResponse
735
+ | UploadFileResponse
678
736
  | ReactionResponse
679
737
  | DownloadImageErrorResponse,
680
738
  operation = "send",
@@ -116,7 +116,7 @@ export class FeishuTurnPresenter {
116
116
  taskTitle?: string,
117
117
  replyTarget?: MessageReplyTarget,
118
118
  prompt?: string,
119
- ): Promise<void> {
119
+ ): Promise<string | undefined> {
120
120
  const existing = this.pendingEntries.get(sessionId);
121
121
  if (existing) {
122
122
  await existing.initializing;
@@ -125,7 +125,7 @@ export class FeishuTurnPresenter {
125
125
  this.store.saveTurnSnapshot(existing.state.turnId, sessionId, existing.state, existing.contextKey);
126
126
  if (!existing.historySnapshot) existing.scheduler?.update(existing.state, "critical");
127
127
  }
128
- return;
128
+ return existing.state.turnId;
129
129
  }
130
130
  this.sessionContexts.set(sessionId, contextKey);
131
131
  if (taskTitle) this.sessionTitles.set(sessionId, taskTitle);
@@ -146,6 +146,7 @@ export class FeishuTurnPresenter {
146
146
  entry.initializing = this.initializeEntry(entry);
147
147
  try {
148
148
  await entry.initializing;
149
+ return state.turnId;
149
150
  } catch (error) {
150
151
  if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
151
152
  this.entries.delete(state.turnId);
@@ -169,6 +170,34 @@ export class FeishuTurnPresenter {
169
170
  await this.syncThinkingCardReaction(entry);
170
171
  }
171
172
 
173
+ async interruptTurnForRecovery(
174
+ sessionId: string,
175
+ contextKey: string,
176
+ turnId: string,
177
+ message: string,
178
+ ): Promise<void> {
179
+ const snapshot = this.store.getTurnSnapshot(turnId);
180
+ if (!isTurnViewState(snapshot) || snapshot.sessionId !== sessionId || isTerminalViewStatus(snapshot.status)) return;
181
+ const state: TurnViewState = {
182
+ ...snapshot,
183
+ status: "cancelled",
184
+ progressText: message,
185
+ activeTool: undefined,
186
+ approval: undefined,
187
+ completedAt: Date.now(),
188
+ };
189
+ this.store.saveTurnSnapshot(turnId, sessionId, state, contextKey);
190
+ const delivery = this.store.getTurnDelivery(turnId);
191
+ if (delivery?.progressMessageId) {
192
+ await this.outbound.updateInteractiveCard(delivery.progressMessageId, this.renderer.renderTurn(state));
193
+ }
194
+ const entry = this.entries.get(turnId);
195
+ if (entry) {
196
+ this.entries.delete(turnId);
197
+ if (this.pendingEntries.get(sessionId) === entry) this.pendingEntries.delete(sessionId);
198
+ }
199
+ }
200
+
172
201
  async appendSteerMessage(
173
202
  sessionId: string,
174
203
  turnId: string,
@@ -519,6 +548,10 @@ function isTerminalEvent(event: AgentEvent): boolean {
519
548
  return event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed";
520
549
  }
521
550
 
551
+ function isTerminalViewStatus(status: TurnViewState["status"]): boolean {
552
+ return status === "completed" || status === "cancelled" || status === "failed";
553
+ }
554
+
522
555
  function thinkingCardReactionForStatus(status: TurnViewState["status"]): string | undefined {
523
556
  if (status === "completed") return undefined;
524
557
  if (status === "failed") return THINKING_CARD_FAILED_REACTION;
@@ -0,0 +1,7 @@
1
+ import type { AppConfig } from "../config/schema.js";
2
+
3
+ export function allowsFeishuUser(config: AppConfig, userOpenId: string | undefined): boolean {
4
+ if (config.feishu?.respondToOwnerOnly !== true) return true;
5
+ const ownerOpenId = config.feishu.userOpenId?.trim();
6
+ return Boolean(ownerOpenId && userOpenId === ownerOpenId);
7
+ }
@@ -9,6 +9,7 @@ export interface IncomingMessage {
9
9
  threadId?: string;
10
10
  rootMessageId?: string;
11
11
  parentMessageId?: string;
12
+ mentionedBot?: true;
12
13
  text: string;
13
14
  images?: IncomingImageReference[];
14
15
  }
@@ -53,6 +54,7 @@ export interface FeishuOutbound {
53
54
  deleteReaction?(messageId: string, reactionId: string): Promise<void>;
54
55
  downloadImage?(messageId: string, imageKey: string): Promise<string>;
55
56
  sendText(contextKey: string, text: string): Promise<string | undefined>;
57
+ sendFile?(contextKey: string, filePath: string): Promise<string | undefined>;
56
58
  sendMarkdown(contextKey: string, markdown: string, idempotencyKey?: string): Promise<string | undefined>;
57
59
  sendInteractiveCard(contextKey: string, card: Record<string, unknown>): Promise<string | undefined>;
58
60
  replyText?(
@@ -61,6 +63,11 @@ export interface FeishuOutbound {
61
63
  text: string,
62
64
  idempotencyKey?: string,
63
65
  ): Promise<string | undefined>;
66
+ replyFile?(
67
+ contextKey: string,
68
+ target: MessageReplyTarget,
69
+ filePath: string,
70
+ ): Promise<string | undefined>;
64
71
  replyMarkdown?(
65
72
  contextKey: string,
66
73
  target: MessageReplyTarget,