@kodelyth/feishu 2026.5.39 → 2026.5.42

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 (238) hide show
  1. package/api.ts +32 -0
  2. package/channel-entry.ts +20 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +16 -0
  5. package/dist/accounts-D0ow-lRb.js +429 -0
  6. package/dist/api.js +2308 -0
  7. package/dist/app-registration-DBSnysKJ.js +184 -0
  8. package/dist/audio-preflight.runtime-Dpjbn-7r.js +7 -0
  9. package/dist/channel-13WQvQ0u.js +2115 -0
  10. package/dist/channel-entry.js +22 -0
  11. package/dist/channel-plugin-api.js +2 -0
  12. package/dist/channel.runtime-JMJonrJ4.js +729 -0
  13. package/dist/client-D1pzbBGo.js +157 -0
  14. package/dist/contract-api.js +9 -0
  15. package/dist/conversation-id-_58ecqlx.js +139 -0
  16. package/dist/drive-CgHOluXx.js +883 -0
  17. package/dist/index.js +68 -0
  18. package/dist/monitor-oWptK0zL.js +60 -0
  19. package/dist/monitor.account-DHaWlslg.js +5207 -0
  20. package/dist/monitor.state-C211a4tX.js +100 -0
  21. package/dist/probe-CF4duEpK.js +149 -0
  22. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  23. package/dist/runtime-DSh5rL_d.js +8 -0
  24. package/dist/runtime-api.js +14 -0
  25. package/dist/secret-contract-NSee-WzN.js +119 -0
  26. package/dist/secret-contract-api.js +2 -0
  27. package/dist/security-audit-DWVC0vSK.js +11 -0
  28. package/dist/security-audit-shared-Dpcwxeft.js +38 -0
  29. package/dist/security-contract-api.js +2 -0
  30. package/dist/send-DfZuV4Fi.js +1212 -0
  31. package/dist/session-conversation-Duaukbnl.js +27 -0
  32. package/dist/session-key-api.js +2 -0
  33. package/dist/setup-api.js +2 -0
  34. package/dist/setup-entry.js +15 -0
  35. package/dist/subagent-hooks-Dtegs0kh.js +235 -0
  36. package/dist/subagent-hooks-api.js +23 -0
  37. package/dist/targets-DFskxX4p.js +48 -0
  38. package/dist/thread-bindings-DI7lVSOE.js +222 -0
  39. package/index.ts +82 -0
  40. package/klaw.plugin.json +47 -1712
  41. package/package.json +4 -4
  42. package/runtime-api.ts +52 -0
  43. package/secret-contract-api.ts +5 -0
  44. package/security-contract-api.ts +1 -0
  45. package/session-key-api.ts +1 -0
  46. package/setup-api.ts +3 -0
  47. package/setup-entry.test.ts +19 -0
  48. package/setup-entry.ts +13 -0
  49. package/src/accounts.test.ts +480 -0
  50. package/src/accounts.ts +333 -0
  51. package/src/agent-config.ts +21 -0
  52. package/src/app-registration.ts +331 -0
  53. package/src/approval-auth.test.ts +24 -0
  54. package/src/approval-auth.ts +25 -0
  55. package/src/async.test.ts +35 -0
  56. package/src/async.ts +104 -0
  57. package/src/audio-preflight.runtime.ts +9 -0
  58. package/src/bitable.test.ts +136 -0
  59. package/src/bitable.ts +762 -0
  60. package/src/bot-content.ts +485 -0
  61. package/src/bot-group-name.test.ts +116 -0
  62. package/src/bot-runtime-api.ts +12 -0
  63. package/src/bot-sender-name.ts +125 -0
  64. package/src/bot.broadcast.test.ts +523 -0
  65. package/src/bot.card-action.test.ts +552 -0
  66. package/src/bot.checkBotMentioned.test.ts +265 -0
  67. package/src/bot.helpers.test.ts +135 -0
  68. package/src/bot.stripBotMention.test.ts +126 -0
  69. package/src/bot.test.ts +3671 -0
  70. package/src/bot.ts +1703 -0
  71. package/src/card-action.ts +447 -0
  72. package/src/card-interaction.test.ts +131 -0
  73. package/src/card-interaction.ts +159 -0
  74. package/src/card-test-helpers.ts +54 -0
  75. package/src/card-ux-approval.ts +65 -0
  76. package/src/card-ux-launcher.test.ts +106 -0
  77. package/src/card-ux-launcher.ts +121 -0
  78. package/src/card-ux-shared.ts +33 -0
  79. package/src/channel-runtime-api.ts +16 -0
  80. package/src/channel.runtime.ts +47 -0
  81. package/src/channel.test.ts +1151 -0
  82. package/src/channel.ts +1423 -0
  83. package/src/chat-schema.ts +25 -0
  84. package/src/chat.test.ts +240 -0
  85. package/src/chat.ts +188 -0
  86. package/src/client-timeout.ts +42 -0
  87. package/src/client.test.ts +447 -0
  88. package/src/client.ts +262 -0
  89. package/src/comment-dispatcher-runtime-api.ts +6 -0
  90. package/src/comment-dispatcher.test.ts +185 -0
  91. package/src/comment-dispatcher.ts +107 -0
  92. package/src/comment-handler-runtime-api.ts +3 -0
  93. package/src/comment-handler.test.ts +592 -0
  94. package/src/comment-handler.ts +303 -0
  95. package/src/comment-reaction.test.ts +138 -0
  96. package/src/comment-reaction.ts +259 -0
  97. package/src/comment-shared.test.ts +183 -0
  98. package/src/comment-shared.ts +406 -0
  99. package/src/comment-target.ts +44 -0
  100. package/src/config-schema.test.ts +326 -0
  101. package/src/config-schema.ts +335 -0
  102. package/src/conversation-id.test.ts +18 -0
  103. package/src/conversation-id.ts +199 -0
  104. package/src/dedup-runtime-api.ts +1 -0
  105. package/src/dedup.ts +141 -0
  106. package/src/dedupe-key.ts +72 -0
  107. package/src/directory.static.ts +61 -0
  108. package/src/directory.test.ts +141 -0
  109. package/src/directory.ts +124 -0
  110. package/src/doc-schema.ts +182 -0
  111. package/src/docx-batch-insert.test.ts +116 -0
  112. package/src/docx-batch-insert.ts +223 -0
  113. package/src/docx-color-text.ts +154 -0
  114. package/src/docx-table-ops.test.ts +53 -0
  115. package/src/docx-table-ops.ts +316 -0
  116. package/src/docx-types.ts +38 -0
  117. package/src/docx.account-selection.test.ts +95 -0
  118. package/src/docx.test.ts +701 -0
  119. package/src/docx.ts +1596 -0
  120. package/src/drive-schema.ts +92 -0
  121. package/src/drive.test.ts +1237 -0
  122. package/src/drive.ts +829 -0
  123. package/src/dynamic-agent.test.ts +155 -0
  124. package/src/dynamic-agent.ts +143 -0
  125. package/src/event-types.ts +45 -0
  126. package/src/external-keys.test.ts +20 -0
  127. package/src/external-keys.ts +19 -0
  128. package/src/lifecycle.test-support.ts +220 -0
  129. package/src/media.test.ts +955 -0
  130. package/src/media.ts +1105 -0
  131. package/src/mention-target.types.ts +5 -0
  132. package/src/mention.ts +114 -0
  133. package/src/message-action-contract.ts +13 -0
  134. package/src/monitor-state-runtime-api.ts +7 -0
  135. package/src/monitor-transport-runtime-api.ts +10 -0
  136. package/src/monitor.account.ts +492 -0
  137. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  138. package/src/monitor.bot-identity.ts +86 -0
  139. package/src/monitor.bot-menu-handler.ts +165 -0
  140. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  141. package/src/monitor.bot-menu.test.ts +188 -0
  142. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  143. package/src/monitor.card-action.lifecycle.test-support.ts +421 -0
  144. package/src/monitor.cleanup.test.ts +383 -0
  145. package/src/monitor.comment-notice-handler.ts +105 -0
  146. package/src/monitor.comment.test.ts +967 -0
  147. package/src/monitor.comment.ts +1386 -0
  148. package/src/monitor.lifecycle.test.ts +4 -0
  149. package/src/monitor.message-handler.ts +350 -0
  150. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  151. package/src/monitor.reaction.test.ts +739 -0
  152. package/src/monitor.startup.test.ts +213 -0
  153. package/src/monitor.startup.ts +74 -0
  154. package/src/monitor.state.defaults.test.ts +46 -0
  155. package/src/monitor.state.ts +170 -0
  156. package/src/monitor.synthetic-error.ts +18 -0
  157. package/src/monitor.test-mocks.ts +46 -0
  158. package/src/monitor.transport.ts +451 -0
  159. package/src/monitor.ts +100 -0
  160. package/src/monitor.webhook-e2e.test.ts +279 -0
  161. package/src/monitor.webhook-security.test.ts +389 -0
  162. package/src/monitor.webhook.test-helpers.ts +116 -0
  163. package/src/outbound-runtime-api.ts +1 -0
  164. package/src/outbound.test.ts +1118 -0
  165. package/src/outbound.ts +785 -0
  166. package/src/perm-schema.ts +52 -0
  167. package/src/perm.ts +170 -0
  168. package/src/pins.ts +108 -0
  169. package/src/policy.test.ts +223 -0
  170. package/src/policy.ts +318 -0
  171. package/src/post.test.ts +105 -0
  172. package/src/post.ts +275 -0
  173. package/src/probe.test.ts +283 -0
  174. package/src/probe.ts +166 -0
  175. package/src/processing-claims.ts +59 -0
  176. package/src/qr-terminal.ts +1 -0
  177. package/src/reactions.ts +123 -0
  178. package/src/reasoning-preview.test.ts +113 -0
  179. package/src/reasoning-preview.ts +28 -0
  180. package/src/reply-dispatcher-runtime-api.ts +7 -0
  181. package/src/reply-dispatcher.test.ts +1513 -0
  182. package/src/reply-dispatcher.ts +748 -0
  183. package/src/runtime.ts +9 -0
  184. package/src/secret-contract.ts +145 -0
  185. package/src/secret-input.ts +1 -0
  186. package/src/security-audit-shared.ts +69 -0
  187. package/src/security-audit.test.ts +59 -0
  188. package/src/security-audit.ts +1 -0
  189. package/src/send-result.ts +80 -0
  190. package/src/send-target.test.ts +86 -0
  191. package/src/send-target.ts +35 -0
  192. package/src/send.reply-fallback.test.ts +417 -0
  193. package/src/send.test.ts +621 -0
  194. package/src/send.ts +861 -0
  195. package/src/sequential-key.test.ts +72 -0
  196. package/src/sequential-key.ts +25 -0
  197. package/src/sequential-queue.test.ts +165 -0
  198. package/src/sequential-queue.ts +86 -0
  199. package/src/session-conversation.ts +42 -0
  200. package/src/session-route.ts +48 -0
  201. package/src/setup-core.ts +51 -0
  202. package/src/setup-surface.test.ts +484 -0
  203. package/src/setup-surface.ts +618 -0
  204. package/src/streaming-card.test.ts +397 -0
  205. package/src/streaming-card.ts +571 -0
  206. package/src/subagent-hooks.test.ts +627 -0
  207. package/src/subagent-hooks.ts +413 -0
  208. package/src/targets.ts +97 -0
  209. package/src/test-support/lifecycle-test-support.ts +454 -0
  210. package/src/thread-bindings.test.ts +180 -0
  211. package/src/thread-bindings.ts +331 -0
  212. package/src/tool-account-routing.test.ts +250 -0
  213. package/src/tool-account.test.ts +44 -0
  214. package/src/tool-account.ts +93 -0
  215. package/src/tool-factory-test-harness.ts +79 -0
  216. package/src/tool-result.test.ts +32 -0
  217. package/src/tool-result.ts +16 -0
  218. package/src/tools-config.test.ts +21 -0
  219. package/src/tools-config.ts +22 -0
  220. package/src/types.ts +106 -0
  221. package/src/typing.test.ts +144 -0
  222. package/src/typing.ts +214 -0
  223. package/src/wiki-schema.ts +69 -0
  224. package/src/wiki.ts +270 -0
  225. package/subagent-hooks-api.ts +31 -0
  226. package/tsconfig.json +16 -0
  227. package/api.js +0 -7
  228. package/channel-entry.js +0 -7
  229. package/channel-plugin-api.js +0 -7
  230. package/contract-api.js +0 -7
  231. package/index.js +0 -7
  232. package/runtime-api.js +0 -7
  233. package/secret-contract-api.js +0 -7
  234. package/security-contract-api.js +0 -7
  235. package/session-key-api.js +0 -7
  236. package/setup-api.js +0 -7
  237. package/setup-entry.js +0 -7
  238. package/subagent-hooks-api.js +0 -7
@@ -0,0 +1,72 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { FeishuMessageEvent } from "./bot.js";
3
+ import { getFeishuSequentialKey } from "./sequential-key.js";
4
+
5
+ function createTextEvent(params: {
6
+ text: string;
7
+ messageId?: string;
8
+ chatId?: string;
9
+ }): FeishuMessageEvent {
10
+ return {
11
+ sender: {
12
+ sender_id: {
13
+ open_id: "ou_sender_1",
14
+ user_id: "ou_user_1",
15
+ },
16
+ sender_type: "user",
17
+ },
18
+ message: {
19
+ message_id: params.messageId ?? "om_message_1",
20
+ chat_id: params.chatId ?? "oc_dm_chat",
21
+ chat_type: "p2p",
22
+ message_type: "text",
23
+ content: JSON.stringify({ text: params.text }),
24
+ },
25
+ } as FeishuMessageEvent;
26
+ }
27
+
28
+ describe("getFeishuSequentialKey", () => {
29
+ it.each([
30
+ [createTextEvent({ text: "hello" }), "feishu:default:oc_dm_chat"],
31
+ [createTextEvent({ text: "/status" }), "feishu:default:oc_dm_chat"],
32
+ [createTextEvent({ text: "/stop" }), "feishu:default:oc_dm_chat:control"],
33
+ [createTextEvent({ text: "/btw what changed?" }), "feishu:default:oc_dm_chat:btw"],
34
+ ])("resolves sequential key %#", (event, expected) => {
35
+ expect(
36
+ getFeishuSequentialKey({
37
+ accountId: "default",
38
+ event,
39
+ }),
40
+ ).toBe(expected);
41
+ });
42
+
43
+ it("keeps /btw on a stable per-chat lane across different message ids", () => {
44
+ const first = createTextEvent({ text: "/btw one", messageId: "om_message_1" });
45
+ const second = createTextEvent({ text: "/btw two", messageId: "om_message_2" });
46
+
47
+ expect(
48
+ getFeishuSequentialKey({
49
+ accountId: "default",
50
+ event: first,
51
+ }),
52
+ ).toBe("feishu:default:oc_dm_chat:btw");
53
+ expect(
54
+ getFeishuSequentialKey({
55
+ accountId: "default",
56
+ event: second,
57
+ }),
58
+ ).toBe("feishu:default:oc_dm_chat:btw");
59
+ });
60
+
61
+ it("falls back to a stable btw lane when the message id is unavailable", () => {
62
+ const event = createTextEvent({ text: "/btw what changed?" });
63
+ delete (event.message as { message_id?: string }).message_id;
64
+
65
+ expect(
66
+ getFeishuSequentialKey({
67
+ accountId: "default",
68
+ event,
69
+ }),
70
+ ).toBe("feishu:default:oc_dm_chat:btw");
71
+ });
72
+ });
@@ -0,0 +1,25 @@
1
+ import { isAbortRequestText, isBtwRequestText } from "klaw/plugin-sdk/command-primitives-runtime";
2
+ import { parseFeishuMessageEvent, type FeishuMessageEvent } from "./bot.js";
3
+
4
+ export function getFeishuSequentialKey(params: {
5
+ accountId: string;
6
+ event: FeishuMessageEvent;
7
+ botOpenId?: string;
8
+ botName?: string;
9
+ }): string {
10
+ const { accountId, event, botOpenId, botName } = params;
11
+ const chatId = event.message.chat_id?.trim() || "unknown";
12
+ const baseKey = `feishu:${accountId}:${chatId}`;
13
+ const parsed = parseFeishuMessageEvent(event, botOpenId, botName);
14
+ const text = parsed.content.trim();
15
+
16
+ if (isAbortRequestText(text)) {
17
+ return `${baseKey}:control`;
18
+ }
19
+
20
+ if (isBtwRequestText(text)) {
21
+ return `${baseKey}:btw`;
22
+ }
23
+
24
+ return baseKey;
25
+ }
@@ -0,0 +1,165 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createSequentialQueue } from "./sequential-queue.js";
3
+
4
+ function createDeferred() {
5
+ let resolve: (() => void) | undefined;
6
+ const promise = new Promise<void>((res) => {
7
+ resolve = res;
8
+ });
9
+ if (!resolve) {
10
+ throw new Error("Expected deferred resolver to be initialized");
11
+ }
12
+ return { promise, resolve };
13
+ }
14
+
15
+ describe("createSequentialQueue", () => {
16
+ afterEach(() => {
17
+ vi.useRealTimers();
18
+ });
19
+
20
+ it("serializes tasks for the same key", async () => {
21
+ const enqueue = createSequentialQueue();
22
+ const gate = createDeferred();
23
+ const order: string[] = [];
24
+
25
+ const first = enqueue("feishu:default:chat-1", async () => {
26
+ order.push("first:start");
27
+ await gate.promise;
28
+ order.push("first:end");
29
+ });
30
+ const second = enqueue("feishu:default:chat-1", async () => {
31
+ order.push("second:start");
32
+ order.push("second:end");
33
+ });
34
+
35
+ await Promise.resolve();
36
+ expect(order).toEqual(["first:start"]);
37
+
38
+ gate.resolve();
39
+ await Promise.all([first, second]);
40
+
41
+ expect(order).toEqual(["first:start", "first:end", "second:start", "second:end"]);
42
+ });
43
+
44
+ it("allows different keys to run concurrently", async () => {
45
+ const enqueue = createSequentialQueue();
46
+ const gateA = createDeferred();
47
+ const gateB = createDeferred();
48
+ const order: string[] = [];
49
+
50
+ const first = enqueue("feishu:default:chat-1", async () => {
51
+ order.push("chat-1:start");
52
+ await gateA.promise;
53
+ order.push("chat-1:end");
54
+ });
55
+ const second = enqueue("feishu:default:chat-1:btw:om_2", async () => {
56
+ order.push("btw:start");
57
+ await gateB.promise;
58
+ order.push("btw:end");
59
+ });
60
+
61
+ await Promise.resolve();
62
+ expect(order).toEqual(["chat-1:start", "btw:start"]);
63
+
64
+ gateA.resolve();
65
+ gateB.resolve();
66
+ await Promise.all([first, second]);
67
+
68
+ expect(order).toContain("chat-1:end");
69
+ expect(order).toContain("btw:end");
70
+ });
71
+
72
+ it("does not leak unhandled rejections when a queued task fails", async () => {
73
+ const enqueue = createSequentialQueue();
74
+ const unhandled: unknown[] = [];
75
+ const onUnhandledRejection = (reason: unknown) => {
76
+ unhandled.push(reason);
77
+ };
78
+ process.on("unhandledRejection", onUnhandledRejection);
79
+
80
+ try {
81
+ await expect(
82
+ enqueue("feishu:default:chat-1", async () => {
83
+ throw new Error("boom");
84
+ }),
85
+ ).rejects.toThrow("boom");
86
+
87
+ await new Promise<void>((resolve) => setImmediate(resolve));
88
+ expect(unhandled).toStrictEqual([]);
89
+
90
+ await expect(
91
+ enqueue("feishu:default:chat-1", async () => {
92
+ return;
93
+ }),
94
+ ).resolves.toBeUndefined();
95
+ } finally {
96
+ process.off("unhandledRejection", onUnhandledRejection);
97
+ }
98
+ });
99
+
100
+ it("evicts a stuck task after taskTimeoutMs so newer same-key work proceeds", async () => {
101
+ vi.useFakeTimers();
102
+ const timeouts: Array<{ key: string; timeoutMs: number }> = [];
103
+ const enqueue = createSequentialQueue({
104
+ taskTimeoutMs: 25,
105
+ onTaskTimeout: (key, timeoutMs) => {
106
+ timeouts.push({ key, timeoutMs });
107
+ },
108
+ });
109
+ const order: string[] = [];
110
+
111
+ // Stuck task — never resolves until the test cleans up.
112
+ const stuckGate = createDeferred();
113
+ const stuck = enqueue("feishu:default:chat-stuck", async () => {
114
+ order.push("stuck:start");
115
+ await stuckGate.promise;
116
+ order.push("stuck:end");
117
+ });
118
+
119
+ // Second same-key task — would be starved indefinitely without the cap.
120
+ const followUp = enqueue("feishu:default:chat-stuck", async () => {
121
+ order.push("follow-up:ran");
122
+ });
123
+
124
+ await vi.advanceTimersByTimeAsync(25);
125
+ await followUp;
126
+
127
+ expect(order).toEqual(["stuck:start", "follow-up:ran"]);
128
+ expect(timeouts).toEqual([{ key: "feishu:default:chat-stuck", timeoutMs: 25 }]);
129
+
130
+ // Drain the leaked stuck task so it doesn't trip the unhandled-rejection guard.
131
+ stuckGate.resolve();
132
+ await stuck;
133
+ });
134
+
135
+ it("disables the timeout cap when taskTimeoutMs is 0 (legacy behavior)", async () => {
136
+ vi.useFakeTimers();
137
+ const timeouts: Array<{ key: string; timeoutMs: number }> = [];
138
+ const enqueue = createSequentialQueue({
139
+ taskTimeoutMs: 0,
140
+ onTaskTimeout: (key, timeoutMs) => {
141
+ timeouts.push({ key, timeoutMs });
142
+ },
143
+ });
144
+ const gate = createDeferred();
145
+ const order: string[] = [];
146
+
147
+ const first = enqueue("feishu:default:chat-1", async () => {
148
+ order.push("first:start");
149
+ await gate.promise;
150
+ order.push("first:end");
151
+ });
152
+ const second = enqueue("feishu:default:chat-1", async () => {
153
+ order.push("second:ran");
154
+ });
155
+
156
+ // Wait long enough that a timeout would have fired if it were active.
157
+ await vi.advanceTimersByTimeAsync(30);
158
+ expect(order).toEqual(["first:start"]);
159
+ expect(timeouts).toStrictEqual([]);
160
+
161
+ gate.resolve();
162
+ await Promise.all([first, second]);
163
+ expect(order).toEqual(["first:start", "first:end", "second:ran"]);
164
+ });
165
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Per-key serial task queue for Feishu inbound message handling.
3
+ *
4
+ * Tasks enqueued under the same key run in FIFO order. Different keys run
5
+ * concurrently. This preserves the channel's same-chat ordering contract
6
+ * (see #64324) while letting cross-chat work proceed in parallel.
7
+ *
8
+ * `taskTimeoutMs` bounds how long the queue will block subsequent same-key
9
+ * tasks behind a single in-flight task. After the cap, the in-flight task
10
+ * is evicted from the blocking chain so newer messages for the same key
11
+ * can proceed. The original task is NOT aborted — it continues running in
12
+ * the background; it just stops starving the queue.
13
+ *
14
+ * Without this cap, a single hung dispatch (e.g. an agent call that never
15
+ * resolves) keeps later same-chat messages in `queued` state until the
16
+ * gateway is restarted. See #70133.
17
+ */
18
+
19
+ const DEFAULT_TASK_TIMEOUT_MS = 5 * 60 * 1000;
20
+
21
+ export interface SequentialQueueOptions {
22
+ /**
23
+ * Maximum time (ms) to block subsequent same-key tasks behind a single
24
+ * in-flight task. Pass 0 (or a non-finite value) to disable the cap and
25
+ * restore unbounded legacy behavior.
26
+ *
27
+ * Default: 5 minutes.
28
+ */
29
+ taskTimeoutMs?: number;
30
+
31
+ /**
32
+ * Optional callback fired when a task exceeds `taskTimeoutMs`. The task
33
+ * itself is not awaited further; this callback is the only signal the
34
+ * caller gets that the queue moved on without it.
35
+ */
36
+ onTaskTimeout?: (key: string, timeoutMs: number) => void;
37
+ }
38
+
39
+ export function createSequentialQueue(options: SequentialQueueOptions = {}) {
40
+ const queues = new Map<string, Promise<void>>();
41
+ const taskTimeoutMs = options.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
42
+ const onTaskTimeout = options.onTaskTimeout;
43
+
44
+ return (key: string, task: () => Promise<void>): Promise<void> => {
45
+ const previous = queues.get(key) ?? Promise.resolve();
46
+ const wrapped = () => boundedRun(key, task, taskTimeoutMs, onTaskTimeout);
47
+ const next = previous.then(wrapped, wrapped);
48
+ queues.set(key, next);
49
+ const cleanup = () => {
50
+ if (queues.get(key) === next) {
51
+ queues.delete(key);
52
+ }
53
+ };
54
+ next.then(cleanup, cleanup);
55
+ return next;
56
+ };
57
+ }
58
+
59
+ async function boundedRun(
60
+ key: string,
61
+ task: () => Promise<void>,
62
+ timeoutMs: number,
63
+ onTaskTimeout: ((key: string, timeoutMs: number) => void) | undefined,
64
+ ): Promise<void> {
65
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
66
+ return task();
67
+ }
68
+ let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
69
+ const timeoutPromise = new Promise<void>((resolve) => {
70
+ timeoutHandle = setTimeout(() => {
71
+ try {
72
+ onTaskTimeout?.(key, timeoutMs);
73
+ } catch {
74
+ // Swallow logging errors so they cannot poison the queue chain.
75
+ }
76
+ resolve();
77
+ }, timeoutMs);
78
+ });
79
+ try {
80
+ await Promise.race([task(), timeoutPromise]);
81
+ } finally {
82
+ if (timeoutHandle) {
83
+ clearTimeout(timeoutHandle);
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,42 @@
1
+ import { buildFeishuConversationId, parseFeishuConversationId } from "./conversation-id.js";
2
+
3
+ function resolveFeishuParentConversationCandidates(rawId: string): string[] {
4
+ const parsed = parseFeishuConversationId({ conversationId: rawId });
5
+ if (!parsed) {
6
+ return [];
7
+ }
8
+ switch (parsed.scope) {
9
+ case "group_topic_sender":
10
+ return [
11
+ buildFeishuConversationId({
12
+ chatId: parsed.chatId,
13
+ scope: "group_topic",
14
+ topicId: parsed.topicId,
15
+ }),
16
+ parsed.chatId,
17
+ ];
18
+ case "group_topic":
19
+ case "group_sender":
20
+ return [parsed.chatId];
21
+ case "group":
22
+ default:
23
+ return [];
24
+ }
25
+ }
26
+
27
+ export function resolveFeishuSessionConversation(params: {
28
+ kind: "group" | "channel";
29
+ rawId: string;
30
+ }) {
31
+ const parsed = parseFeishuConversationId({ conversationId: params.rawId });
32
+ if (!parsed) {
33
+ return null;
34
+ }
35
+ return {
36
+ id: parsed.canonicalConversationId,
37
+ baseConversationId: parsed.chatId,
38
+ parentConversationCandidates: resolveFeishuParentConversationCandidates(
39
+ parsed.canonicalConversationId,
40
+ ),
41
+ };
42
+ }
@@ -0,0 +1,48 @@
1
+ import {
2
+ buildChannelOutboundSessionRoute,
3
+ stripChannelTargetPrefix,
4
+ type ChannelOutboundSessionRouteParams,
5
+ } from "klaw/plugin-sdk/channel-core";
6
+ import { normalizeLowercaseStringOrEmpty } from "klaw/plugin-sdk/string-coerce-runtime";
7
+
8
+ export function resolveFeishuOutboundSessionRoute(params: ChannelOutboundSessionRouteParams) {
9
+ let trimmed = stripChannelTargetPrefix(params.target, "feishu", "lark");
10
+ if (!trimmed) {
11
+ return null;
12
+ }
13
+
14
+ const lower = normalizeLowercaseStringOrEmpty(trimmed);
15
+ let isGroup = false;
16
+ let typeExplicit = false;
17
+
18
+ if (lower.startsWith("group:") || lower.startsWith("chat:") || lower.startsWith("channel:")) {
19
+ trimmed = trimmed.replace(/^(group|chat|channel):/i, "").trim();
20
+ isGroup = true;
21
+ typeExplicit = true;
22
+ } else if (lower.startsWith("user:") || lower.startsWith("dm:")) {
23
+ trimmed = trimmed.replace(/^(user|dm):/i, "").trim();
24
+ isGroup = false;
25
+ typeExplicit = true;
26
+ }
27
+
28
+ if (!typeExplicit) {
29
+ const idLower = normalizeLowercaseStringOrEmpty(trimmed);
30
+ if (idLower.startsWith("ou_") || idLower.startsWith("on_")) {
31
+ isGroup = false;
32
+ }
33
+ }
34
+
35
+ return buildChannelOutboundSessionRoute({
36
+ cfg: params.cfg,
37
+ agentId: params.agentId,
38
+ channel: "feishu",
39
+ accountId: params.accountId,
40
+ peer: {
41
+ kind: isGroup ? "group" : "direct",
42
+ id: trimmed,
43
+ },
44
+ chatType: isGroup ? "group" : "direct",
45
+ from: isGroup ? `feishu:group:${trimmed}` : `feishu:${trimmed}`,
46
+ to: trimmed,
47
+ });
48
+ }
@@ -0,0 +1,51 @@
1
+ import {
2
+ DEFAULT_ACCOUNT_ID,
3
+ type ChannelSetupAdapter,
4
+ type KlawConfig,
5
+ } from "klaw/plugin-sdk/setup";
6
+ import { resolveDefaultFeishuAccountId } from "./accounts.js";
7
+ import type { FeishuConfig } from "./types.js";
8
+
9
+ export function setFeishuNamedAccountEnabled(
10
+ cfg: KlawConfig,
11
+ accountId: string,
12
+ enabled: boolean,
13
+ ): KlawConfig {
14
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
15
+ return {
16
+ ...cfg,
17
+ channels: {
18
+ ...cfg.channels,
19
+ feishu: {
20
+ ...feishuCfg,
21
+ accounts: {
22
+ ...feishuCfg?.accounts,
23
+ [accountId]: {
24
+ ...feishuCfg?.accounts?.[accountId],
25
+ enabled,
26
+ },
27
+ },
28
+ },
29
+ },
30
+ };
31
+ }
32
+
33
+ export const feishuSetupAdapter: ChannelSetupAdapter = {
34
+ resolveAccountId: ({ cfg, accountId }) => accountId?.trim() || resolveDefaultFeishuAccountId(cfg),
35
+ applyAccountConfig: ({ cfg, accountId }) => {
36
+ const isDefault = !accountId || accountId === DEFAULT_ACCOUNT_ID;
37
+ if (isDefault) {
38
+ return {
39
+ ...cfg,
40
+ channels: {
41
+ ...cfg.channels,
42
+ feishu: {
43
+ ...cfg.channels?.feishu,
44
+ enabled: true,
45
+ },
46
+ },
47
+ };
48
+ }
49
+ return setFeishuNamedAccountEnabled(cfg, accountId, true);
50
+ },
51
+ };