lark-remote 0.1.5 → 0.1.7

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 (190) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.en.md +5 -1
  3. package/README.md +10 -2
  4. package/dist/bridge/approval-coordinator.d.ts +131 -0
  5. package/dist/bridge/approval-coordinator.js +409 -0
  6. package/dist/bridge/inbound-media.d.ts +62 -0
  7. package/dist/bridge/inbound-media.js +282 -0
  8. package/dist/bridge/index.d.ts +138 -5
  9. package/dist/bridge/index.js +569 -65
  10. package/dist/bridge/queue-manager.d.ts +11 -1
  11. package/dist/bridge/queue-manager.js +20 -11
  12. package/dist/card/approval-render.d.ts +16 -0
  13. package/dist/card/approval-render.js +363 -0
  14. package/dist/card/bash-card-session.d.ts +2 -24
  15. package/dist/card/bash-card-session.js +6 -73
  16. package/dist/card/bash-renderer.js +2 -3
  17. package/dist/card/card-budget.js +29 -75
  18. package/dist/card/card-session.d.ts +48 -0
  19. package/dist/card/card-session.js +86 -0
  20. package/dist/card/card-shared.d.ts +13 -10
  21. package/dist/card/card-shared.js +27 -24
  22. package/dist/card/run-card-session.d.ts +2 -55
  23. package/dist/card/run-card-session.js +6 -104
  24. package/dist/card/run-renderer.js +140 -202
  25. package/dist/card/run-state.d.ts +38 -3
  26. package/dist/card/run-state.js +221 -11
  27. package/dist/card/text-truncate.d.ts +13 -12
  28. package/dist/card/text-truncate.js +13 -63
  29. package/dist/common/fs.d.ts +8 -0
  30. package/dist/common/fs.js +17 -0
  31. package/dist/common/guards.d.ts +0 -20
  32. package/dist/common/guards.js +0 -43
  33. package/dist/common/sleep.d.ts +2 -0
  34. package/dist/common/sleep.js +5 -0
  35. package/dist/common/truncate.d.ts +39 -0
  36. package/dist/common/truncate.js +92 -0
  37. package/dist/config/codex-config.d.ts +1 -1
  38. package/dist/config/codex-config.js +1 -1
  39. package/dist/config/dir.js +1 -1
  40. package/dist/config/index.d.ts +116 -9
  41. package/dist/config/index.js +122 -29
  42. package/dist/connector/index.d.ts +74 -2
  43. package/dist/connector/index.js +119 -1
  44. package/dist/index.js +139 -99
  45. package/dist/instance-lock.js +4 -12
  46. package/dist/order/alias-resolve.d.ts +13 -0
  47. package/dist/order/alias-resolve.js +22 -0
  48. package/dist/order/alias-store.d.ts +38 -0
  49. package/dist/order/alias-store.js +106 -0
  50. package/dist/persistence/atomic-write.d.ts +8 -2
  51. package/dist/persistence/atomic-write.js +23 -7
  52. package/dist/restart.js +2 -1
  53. package/dist/router/card-helpers.d.ts +63 -0
  54. package/dist/router/card-helpers.js +146 -0
  55. package/dist/router/config/claude.js +25 -1
  56. package/dist/router/config/codex.d.ts +1 -1
  57. package/dist/router/config/codex.js +33 -5
  58. package/dist/router/config/common/model-patch.d.ts +22 -0
  59. package/dist/router/config/common/model-patch.js +26 -0
  60. package/dist/router/config/common/render.js +9 -4
  61. package/dist/router/config/index.d.ts +12 -0
  62. package/dist/router/config/index.js +25 -1
  63. package/dist/router/config/kimi.d.ts +4 -1
  64. package/dist/router/config/kimi.js +27 -0
  65. package/dist/router/config/opencode.js +18 -5
  66. package/dist/router/config/pi.js +4 -5
  67. package/dist/router/config/types.d.ts +12 -3
  68. package/dist/router/index.d.ts +77 -17
  69. package/dist/router/index.js +455 -549
  70. package/dist/router/utils.d.ts +4 -0
  71. package/dist/router/utils.js +9 -2
  72. package/dist/runner/agent-choices-common.d.ts +16 -0
  73. package/dist/runner/agent-choices-common.js +31 -0
  74. package/dist/runner/claude/runner.d.ts +47 -10
  75. package/dist/runner/claude/runner.js +117 -46
  76. package/dist/runner/claude/session.d.ts +166 -0
  77. package/dist/runner/claude/session.js +749 -0
  78. package/dist/runner/codex/app-server/protocol-types.d.ts +519 -0
  79. package/dist/runner/codex/app-server/protocol-types.js +56 -0
  80. package/dist/runner/codex/app-server/runner.d.ts +104 -0
  81. package/dist/runner/codex/app-server/runner.js +377 -0
  82. package/dist/runner/codex/app-server/translator.d.ts +89 -0
  83. package/dist/runner/codex/app-server/translator.js +716 -0
  84. package/dist/runner/codex/index.d.ts +1 -1
  85. package/dist/runner/codex/index.js +1 -1
  86. package/dist/runner/common/acp/protocol-helpers.d.ts +14 -0
  87. package/dist/runner/common/acp/protocol-helpers.js +21 -0
  88. package/dist/runner/common/acp/protocol-types.d.ts +238 -0
  89. package/dist/runner/common/acp/protocol-types.js +75 -0
  90. package/dist/runner/common/connection-based-runner.d.ts +111 -0
  91. package/dist/runner/common/connection-based-runner.js +261 -0
  92. package/dist/runner/common/jsonrpc/client.d.ts +89 -0
  93. package/dist/runner/common/jsonrpc/client.js +243 -0
  94. package/dist/runner/common/jsonrpc/connection-manager.d.ts +125 -0
  95. package/dist/runner/common/jsonrpc/connection-manager.js +205 -0
  96. package/dist/runner/common/jsonrpc/transport.d.ts +50 -0
  97. package/dist/runner/common/jsonrpc/transport.js +230 -0
  98. package/dist/runner/common/jsonrpc/types.d.ts +32 -0
  99. package/dist/runner/common/jsonrpc/types.js +9 -0
  100. package/dist/runner/common/process-stopper.d.ts +2 -1
  101. package/dist/runner/common/process-stopper.js +1 -0
  102. package/dist/runner/common/runner-utils.d.ts +6 -13
  103. package/dist/runner/common/runner-utils.js +6 -47
  104. package/dist/runner/common/spawning-runner.d.ts +24 -70
  105. package/dist/runner/common/spawning-runner.js +98 -152
  106. package/dist/runner/common/usage.d.ts +2 -0
  107. package/dist/runner/common/usage.js +2 -0
  108. package/dist/runner/index.d.ts +5 -5
  109. package/dist/runner/index.js +4 -4
  110. package/dist/runner/kimi/acp/runner.d.ts +106 -0
  111. package/dist/runner/kimi/acp/runner.js +448 -0
  112. package/dist/runner/kimi/acp/translator.d.ts +124 -0
  113. package/dist/runner/kimi/acp/translator.js +336 -0
  114. package/dist/runner/kimi/index.d.ts +1 -1
  115. package/dist/runner/kimi/index.js +1 -1
  116. package/dist/runner/opencode/acp/index.d.ts +1 -0
  117. package/dist/runner/opencode/acp/index.js +2 -0
  118. package/dist/runner/opencode/acp/runner.d.ts +121 -0
  119. package/dist/runner/opencode/acp/runner.js +429 -0
  120. package/dist/runner/opencode/acp/translator.d.ts +119 -0
  121. package/dist/runner/opencode/acp/translator.js +345 -0
  122. package/dist/runner/opencode/index.d.ts +1 -1
  123. package/dist/runner/opencode/index.js +1 -1
  124. package/dist/runner/pi/index.d.ts +1 -1
  125. package/dist/runner/pi/index.js +1 -1
  126. package/dist/runner/pi/rpc/client.d.ts +52 -0
  127. package/dist/runner/pi/rpc/client.js +114 -0
  128. package/dist/runner/pi/rpc/index.d.ts +4 -0
  129. package/dist/runner/pi/rpc/index.js +4 -0
  130. package/dist/runner/pi/rpc/protocol-types.d.ts +127 -0
  131. package/dist/runner/pi/rpc/protocol-types.js +21 -0
  132. package/dist/runner/pi/rpc/runner.d.ts +80 -0
  133. package/dist/runner/pi/rpc/runner.js +199 -0
  134. package/dist/runner/pi/rpc/translator.d.ts +55 -0
  135. package/dist/runner/pi/rpc/translator.js +258 -0
  136. package/dist/runner/registry.d.ts +7 -8
  137. package/dist/runner/registry.js +3 -4
  138. package/dist/runner/resolve-agent-choices.js +18 -59
  139. package/dist/runner/sync-agent-choices.js +16 -67
  140. package/dist/runner/types.d.ts +153 -2
  141. package/dist/session/claude/session-index.js +2 -3
  142. package/dist/session/claude/session-reader.js +3 -6
  143. package/dist/session/claude/sessions.js +6 -13
  144. package/dist/session/codex/rollout-reader.d.ts +2 -2
  145. package/dist/session/codex/rollout-reader.js +57 -17
  146. package/dist/session/codex/sessions.d.ts +1 -1
  147. package/dist/session/codex/sessions.js +2 -2
  148. package/dist/session/common/content-blocks.d.ts +7 -0
  149. package/dist/session/common/content-blocks.js +13 -9
  150. package/dist/session/common/jsonl.d.ts +8 -25
  151. package/dist/session/common/jsonl.js +126 -261
  152. package/dist/session/common/pagination.d.ts +28 -0
  153. package/dist/session/common/pagination.js +33 -0
  154. package/dist/session/kimi/sessions.d.ts +34 -1
  155. package/dist/session/kimi/sessions.js +208 -98
  156. package/dist/session/opencode/sessions.d.ts +17 -3
  157. package/dist/session/opencode/sessions.js +29 -29
  158. package/dist/session/pi/sessions.d.ts +5 -1
  159. package/dist/session/pi/sessions.js +12 -16
  160. package/dist/session/session-store.d.ts +11 -1
  161. package/dist/session/session-store.js +75 -178
  162. package/dist/update/index.d.ts +2 -2
  163. package/dist/update/index.js +2 -2
  164. package/dist/update/install.d.ts +20 -2
  165. package/dist/update/install.js +51 -7
  166. package/package.json +1 -1
  167. package/dist/runner/codex/argv.d.ts +0 -33
  168. package/dist/runner/codex/argv.js +0 -40
  169. package/dist/runner/codex/jsonl.d.ts +0 -37
  170. package/dist/runner/codex/jsonl.js +0 -221
  171. package/dist/runner/codex/runner.d.ts +0 -63
  172. package/dist/runner/codex/runner.js +0 -124
  173. package/dist/runner/common/exec-translator.d.ts +0 -73
  174. package/dist/runner/common/exec-translator.js +0 -105
  175. package/dist/runner/kimi/jsonl.d.ts +0 -28
  176. package/dist/runner/kimi/jsonl.js +0 -143
  177. package/dist/runner/kimi/runner.d.ts +0 -49
  178. package/dist/runner/kimi/runner.js +0 -105
  179. package/dist/runner/opencode/argv.d.ts +0 -32
  180. package/dist/runner/opencode/argv.js +0 -35
  181. package/dist/runner/opencode/jsonl.d.ts +0 -68
  182. package/dist/runner/opencode/jsonl.js +0 -295
  183. package/dist/runner/opencode/runner.d.ts +0 -61
  184. package/dist/runner/opencode/runner.js +0 -93
  185. package/dist/runner/pi/jsonl.d.ts +0 -128
  186. package/dist/runner/pi/jsonl.js +0 -249
  187. package/dist/runner/pi/runner.d.ts +0 -36
  188. package/dist/runner/pi/runner.js +0 -76
  189. package/dist/session/common/catch-up.d.ts +0 -12
  190. package/dist/session/common/catch-up.js +0 -31
@@ -1,4 +1,4 @@
1
- import { type CardActionEvent, type CardActionResponse, type CardStreamProducer } from '@larksuite/channel';
1
+ import { type CardActionEvent, type CardActionResponse, type CardStreamProducer, type LarkChannel } from '@larksuite/channel';
2
2
  import type { AppConfig } from '../config/index.js';
3
3
  /**
4
4
  * dedup 缓存 TTL(毫秒)。控制飞书事件去重窗口,全局作用于 message + cardAction。
@@ -26,6 +26,54 @@ interface FeishuMessage {
26
26
  content: string;
27
27
  }
28
28
  type MessageHandler = (msg: FeishuMessage) => void;
29
+ /**
30
+ * 入站媒体(image/file 消息的 resource)下载结果,交给 bridge 落盘。
31
+ * media 为通过大小限制、可落盘的项;failures 为下载失败或超限的项
32
+ * (bridge 负责把这些失败提示回用户)。
33
+ */
34
+ export interface InboundMediaItem {
35
+ /** 资源类型:image 消息按 MIME 生成 image_<HHmmss>_<n>.<ext>,file 保留文件名。 */
36
+ type: 'image' | 'file';
37
+ /** 原始文件名(file 消息有;image 消息无,由 bridge 按 MIME 生成)。 */
38
+ fileName?: string;
39
+ /** 服务端 content-type(参数已剥离;可能缺失,bridge 有兜底)。 */
40
+ mimeType?: string;
41
+ /**
42
+ * 下载内容所在临时文件(流式落盘,避免大文件全量进堆内存)。
43
+ * 由 bridge 移动到最终位置;任何未移动路径都必须清理该文件。
44
+ */
45
+ tempPath: string;
46
+ }
47
+ export interface InboundMediaFailure {
48
+ fileName?: string;
49
+ reason: string;
50
+ }
51
+ export interface InboundMediaPayload {
52
+ userId: string;
53
+ chatId: string;
54
+ messageId: string;
55
+ /** 引用上下文:不同 replyTo 强制拆批。 */
56
+ replyToMessageId?: string;
57
+ media: InboundMediaItem[];
58
+ failures: InboundMediaFailure[];
59
+ }
60
+ /**
61
+ * 媒体消息到达的轻量描述(未下载):供上层先做 owner/配置闸门,
62
+ * 通过后再调 downloadInboundMedia —— 下载发生在认证之后(P1 review 修复)。
63
+ */
64
+ export interface InboundMediaMessage {
65
+ userId: string;
66
+ chatId: string;
67
+ messageId: string;
68
+ /** 引用上下文:不同 replyTo 强制拆批。 */
69
+ replyToMessageId?: string;
70
+ resources: Array<{
71
+ type: 'image' | 'file';
72
+ fileKey: string;
73
+ fileName?: string;
74
+ }>;
75
+ }
76
+ type InboundMediaDetectedHandler = (msg: InboundMediaMessage) => void | Promise<void>;
29
77
  /**
30
78
  * CardAction handler may return a CardActionResponse (e.g. `{ toast: {...} }`)
31
79
  * to give the clicking user native immediate feedback. The SDK passes the
@@ -34,9 +82,11 @@ type MessageHandler = (msg: FeishuMessage) => void;
34
82
  */
35
83
  type CardActionHandler = (action: CardActionEvent) => void | CardActionResponse | Promise<void | CardActionResponse>;
36
84
  export declare class FeishuConnector {
37
- private channel;
85
+ /** 飞书 channel 实例(public:测试直接访问/注入 mock,替代 as unknown as)。 */
86
+ channel: LarkChannel;
38
87
  private onMessage?;
39
88
  private onCardAction?;
89
+ private onInboundMediaDetected?;
40
90
  private isConnected;
41
91
  private appId;
42
92
  private appSecret;
@@ -44,6 +94,23 @@ export declare class FeishuConnector {
44
94
  get connected(): boolean;
45
95
  setMessageHandler(handler: MessageHandler): void;
46
96
  setCardActionHandler(handler: CardActionHandler): void;
97
+ setInboundMediaDetectedHandler(handler: InboundMediaDetectedHandler): void;
98
+ /**
99
+ * 上报媒体消息到达(轻量,不含下载内容)。上层认证/配置闸门通过后
100
+ * 再调 downloadInboundMedia。
101
+ */
102
+ private handleInboundMediaDetected;
103
+ /**
104
+ * 下载消息里的全部资源(流式写临时文件,避免大文件全量进堆内存)。
105
+ * 逐资源 try/catch:单个失败不影响其他资源;超限项不进入 media(不落盘)。
106
+ * 下载失败/超限都随 failures 交给 bridge 提示用户(对齐"发送文件失败"文案风格)。
107
+ * 调用方必须已通过 owner/配置闸门(index.ts);maxFileSizeMb 由调用方从
108
+ * 当前配置传入(避免 connector 持有过期的 config 快照)。
109
+ */
110
+ downloadInboundMedia(msg: InboundMediaMessage, opts?: {
111
+ maxFileSizeMb?: number;
112
+ downloadTimeoutMs?: number;
113
+ }): Promise<InboundMediaPayload>;
47
114
  connect(): Promise<void>;
48
115
  disconnect(): Promise<void>;
49
116
  reconnect(): Promise<void>;
@@ -68,6 +135,11 @@ export declare class FeishuConnector {
68
135
  /** Format error for logging without causing circular serialization */
69
136
  private formatError;
70
137
  addReaction(messageId: string, emoji: string): Promise<void>;
138
+ /**
139
+ * Remove a reaction by emoji (e.g. retract the Typing indicator).
140
+ * The SDK resolves the reaction id internally; missing reaction is a no-op.
141
+ */
142
+ removeReactionByEmoji(messageId: string, emoji: string): Promise<void>;
71
143
  /**
72
144
  * Upload a file to Feishu and send it to the specified chat.
73
145
  * Uses the im/v1/files upload API.
@@ -1,9 +1,15 @@
1
1
  import { createLarkChannel, } from '@larksuite/channel';
2
2
  import { getLogger } from '../logger/index.js';
3
3
  import { MAX_FILE_UPLOAD_SIZE } from './file-limits.js';
4
+ import { DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB } from '../config/index.js';
5
+ import { sleep } from '../common/sleep.js';
4
6
  import axios from 'axios';
5
7
  import fs from 'node:fs';
8
+ import { silentlyUnlink } from '../common/fs.js';
6
9
  import https from 'node:https';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { randomUUID } from 'node:crypto';
7
13
  import FormData from 'form-data';
8
14
  /**
9
15
  * Dedicated https.Agent with keepAlive disabled for sendFile's three axios
@@ -36,6 +42,20 @@ const noKeepAliveAgent = new https.Agent({ keepAlive: false });
36
42
  * 详见 design.md §9.8。原地更新逻辑新增按钮时参考该节去重风险分类。
37
43
  */
38
44
  export const DEDUP_TTL_MS = 300;
45
+ /** 单资源下载超时(毫秒):SDK 无内置超时,挂起会导致提示丢失 + 残留临时文件。 */
46
+ const RESOURCE_DOWNLOAD_TIMEOUT_MS = 5 * 60 * 1000;
47
+ function withTimeout(promise, ms, label) {
48
+ return new Promise((resolve, reject) => {
49
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
50
+ promise.then((value) => {
51
+ clearTimeout(timer);
52
+ resolve(value);
53
+ }, (err) => {
54
+ clearTimeout(timer);
55
+ reject(err);
56
+ });
57
+ });
58
+ }
39
59
  /**
40
60
  * 判断 send 失败是否值得重试一次(§9.5 限流重试口径,§P1-3 修正)。
41
61
  *
@@ -75,9 +95,11 @@ function tryGetPatchService(channel) {
75
95
  return c?.im?.v1?.message;
76
96
  }
77
97
  export class FeishuConnector {
98
+ /** 飞书 channel 实例(public:测试直接访问/注入 mock,替代 as unknown as)。 */
78
99
  channel;
79
100
  onMessage;
80
101
  onCardAction;
102
+ onInboundMediaDetected;
81
103
  isConnected = false;
82
104
  appId;
83
105
  appSecret;
@@ -125,6 +147,15 @@ export class FeishuConnector {
125
147
  this.channel.on('message', (msg) => {
126
148
  if (msg.chatType !== 'p2p')
127
149
  return;
150
+ // 入站图片/文件:不转发文本内容(图片消息的 content 只是占位文案),
151
+ // 而是下载资源后交给 bridge 落盘(runner 零改动)。
152
+ if ((msg.rawContentType === 'image' || msg.rawContentType === 'file') &&
153
+ msg.resources.length > 0) {
154
+ // 只上报"媒体到达",不在此下载:owner/配置闸门在 index.ts 里先执行,
155
+ // 通过后才调用 downloadInboundMedia(避免未认证大文件下载打爆内存)。
156
+ void this.handleInboundMediaDetected(msg);
157
+ return;
158
+ }
128
159
  this.onMessage?.({
129
160
  userId: msg.senderId,
130
161
  messageId: msg.messageId,
@@ -158,6 +189,81 @@ export class FeishuConnector {
158
189
  setCardActionHandler(handler) {
159
190
  this.onCardAction = handler;
160
191
  }
192
+ setInboundMediaDetectedHandler(handler) {
193
+ this.onInboundMediaDetected = handler;
194
+ }
195
+ /**
196
+ * 上报媒体消息到达(轻量,不含下载内容)。上层认证/配置闸门通过后
197
+ * 再调 downloadInboundMedia。
198
+ */
199
+ async handleInboundMediaDetected(msg) {
200
+ if (!this.onInboundMediaDetected) {
201
+ getLogger().warn('[feishu] inbound media received but no detected handler registered');
202
+ return;
203
+ }
204
+ try {
205
+ await this.onInboundMediaDetected({
206
+ userId: msg.senderId,
207
+ chatId: msg.chatId,
208
+ messageId: msg.messageId,
209
+ replyToMessageId: msg.replyToMessageId,
210
+ resources: msg.resources.map((r) => ({
211
+ type: r.type === 'image' ? 'image' : 'file',
212
+ fileKey: r.fileKey,
213
+ fileName: r.fileName,
214
+ })),
215
+ });
216
+ }
217
+ catch (err) {
218
+ getLogger().error('[feishu] inbound media detected handler failed:', err.message);
219
+ }
220
+ }
221
+ /**
222
+ * 下载消息里的全部资源(流式写临时文件,避免大文件全量进堆内存)。
223
+ * 逐资源 try/catch:单个失败不影响其他资源;超限项不进入 media(不落盘)。
224
+ * 下载失败/超限都随 failures 交给 bridge 提示用户(对齐"发送文件失败"文案风格)。
225
+ * 调用方必须已通过 owner/配置闸门(index.ts);maxFileSizeMb 由调用方从
226
+ * 当前配置传入(避免 connector 持有过期的 config 快照)。
227
+ */
228
+ async downloadInboundMedia(msg, opts) {
229
+ const maxBytes = (opts?.maxFileSizeMb ?? DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB) * 1024 * 1024;
230
+ const timeoutMs = opts?.downloadTimeoutMs ?? RESOURCE_DOWNLOAD_TIMEOUT_MS;
231
+ const media = [];
232
+ const failures = [];
233
+ for (const res of msg.resources) {
234
+ const tmpPath = path.join(os.tmpdir(), `lark-remote-inbound-${randomUUID()}`);
235
+ try {
236
+ const { contentType, bytesWritten } = await withTimeout(this.channel.downloadResourceToFile(msg.messageId, res.fileKey, res.type, tmpPath), timeoutMs, `downloadResource fileKey=${res.fileKey}`);
237
+ if (bytesWritten > maxBytes) {
238
+ silentlyUnlink(tmpPath);
239
+ failures.push({
240
+ fileName: res.fileName,
241
+ reason: `超过 ${maxBytes / (1024 * 1024)}MB 大小限制`,
242
+ });
243
+ continue;
244
+ }
245
+ media.push({
246
+ type: res.type,
247
+ fileName: res.fileName,
248
+ mimeType: contentType,
249
+ tempPath: tmpPath,
250
+ });
251
+ }
252
+ catch (err) {
253
+ silentlyUnlink(tmpPath);
254
+ getLogger().warn(`[feishu] downloadResource failed fileKey=${res.fileKey} type=${res.type}:`, err.message);
255
+ failures.push({ fileName: res.fileName, reason: `下载失败: ${err.message}` });
256
+ }
257
+ }
258
+ return {
259
+ userId: msg.userId,
260
+ chatId: msg.chatId,
261
+ messageId: msg.messageId,
262
+ replyToMessageId: msg.replyToMessageId,
263
+ media,
264
+ failures,
265
+ };
266
+ }
161
267
  async connect() {
162
268
  try {
163
269
  await this.channel.connect();
@@ -200,7 +306,7 @@ export class FeishuConnector {
200
306
  catch (err) {
201
307
  if (shouldRetrySendError(err)) {
202
308
  getLogger().warn('[feishu] rate limited, retrying in 200ms...');
203
- await new Promise((r) => setTimeout(r, 200));
309
+ await sleep(200);
204
310
  try {
205
311
  const result = await this.channel.send(chatId, input, { replyTo: opts?.replyTo });
206
312
  return result.messageId;
@@ -264,6 +370,18 @@ export class FeishuConnector {
264
370
  getLogger().error('[feishu] addReaction failed:', this.formatError(err));
265
371
  }
266
372
  }
373
+ /**
374
+ * Remove a reaction by emoji (e.g. retract the Typing indicator).
375
+ * The SDK resolves the reaction id internally; missing reaction is a no-op.
376
+ */
377
+ async removeReactionByEmoji(messageId, emoji) {
378
+ try {
379
+ await this.channel.removeReactionByEmoji(messageId, emoji);
380
+ }
381
+ catch (err) {
382
+ getLogger().error('[feishu] removeReactionByEmoji failed:', this.formatError(err));
383
+ }
384
+ }
267
385
  /**
268
386
  * Upload a file to Feishu and send it to the specified chat.
269
387
  * Uses the im/v1/files upload API.