@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,52 @@
1
+ import { Type, type Static } from "typebox";
2
+
3
+ const TokenType = Type.Union([
4
+ Type.Literal("doc"),
5
+ Type.Literal("docx"),
6
+ Type.Literal("sheet"),
7
+ Type.Literal("bitable"),
8
+ Type.Literal("folder"),
9
+ Type.Literal("file"),
10
+ Type.Literal("wiki"),
11
+ Type.Literal("mindnote"),
12
+ ]);
13
+
14
+ const MemberType = Type.Union([
15
+ Type.Literal("email"),
16
+ Type.Literal("openid"),
17
+ Type.Literal("userid"),
18
+ Type.Literal("unionid"),
19
+ Type.Literal("openchat"),
20
+ Type.Literal("opendepartmentid"),
21
+ ]);
22
+
23
+ const Permission = Type.Union([
24
+ Type.Literal("view"),
25
+ Type.Literal("edit"),
26
+ Type.Literal("full_access"),
27
+ ]);
28
+
29
+ export const FeishuPermSchema = Type.Union([
30
+ Type.Object({
31
+ action: Type.Literal("list"),
32
+ token: Type.String({ description: "File token" }),
33
+ type: TokenType,
34
+ }),
35
+ Type.Object({
36
+ action: Type.Literal("add"),
37
+ token: Type.String({ description: "File token" }),
38
+ type: TokenType,
39
+ member_type: MemberType,
40
+ member_id: Type.String({ description: "Member ID (email, open_id, user_id, etc.)" }),
41
+ perm: Permission,
42
+ }),
43
+ Type.Object({
44
+ action: Type.Literal("remove"),
45
+ token: Type.String({ description: "File token" }),
46
+ type: TokenType,
47
+ member_type: MemberType,
48
+ member_id: Type.String({ description: "Member ID to remove" }),
49
+ }),
50
+ ]);
51
+
52
+ export type FeishuPermParams = Static<typeof FeishuPermSchema>;
package/src/perm.ts ADDED
@@ -0,0 +1,170 @@
1
+ import type * as Lark from "@larksuiteoapi/node-sdk";
2
+ import type { KlawPluginApi } from "../runtime-api.js";
3
+ import { listEnabledFeishuAccounts } from "./accounts.js";
4
+ import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
5
+ import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
6
+ import {
7
+ jsonToolResult,
8
+ toolExecutionErrorResult,
9
+ unknownToolActionResult,
10
+ } from "./tool-result.js";
11
+
12
+ type ListTokenType =
13
+ | "doc"
14
+ | "sheet"
15
+ | "file"
16
+ | "wiki"
17
+ | "bitable"
18
+ | "docx"
19
+ | "mindnote"
20
+ | "minutes"
21
+ | "slides";
22
+ type CreateTokenType =
23
+ | "doc"
24
+ | "sheet"
25
+ | "file"
26
+ | "wiki"
27
+ | "bitable"
28
+ | "docx"
29
+ | "folder"
30
+ | "mindnote"
31
+ | "minutes"
32
+ | "slides";
33
+ type MemberType =
34
+ | "email"
35
+ | "openid"
36
+ | "unionid"
37
+ | "openchat"
38
+ | "opendepartmentid"
39
+ | "userid"
40
+ | "groupid"
41
+ | "wikispaceid";
42
+ type PermType = "view" | "edit" | "full_access";
43
+
44
+ // ============ Actions ============
45
+
46
+ async function listMembers(client: Lark.Client, token: string, type: string) {
47
+ const res = await client.drive.permissionMember.list({
48
+ path: { token },
49
+ params: { type: type as ListTokenType },
50
+ });
51
+ if (res.code !== 0) {
52
+ throw new Error(res.msg);
53
+ }
54
+
55
+ return {
56
+ members:
57
+ res.data?.items?.map((m) => ({
58
+ member_type: m.member_type,
59
+ member_id: m.member_id,
60
+ perm: m.perm,
61
+ name: m.name,
62
+ })) ?? [],
63
+ };
64
+ }
65
+
66
+ async function addMember(
67
+ client: Lark.Client,
68
+ token: string,
69
+ type: string,
70
+ memberType: string,
71
+ memberId: string,
72
+ perm: string,
73
+ ) {
74
+ const res = await client.drive.permissionMember.create({
75
+ path: { token },
76
+ params: { type: type as CreateTokenType, need_notification: false },
77
+ data: {
78
+ member_type: memberType as MemberType,
79
+ member_id: memberId,
80
+ perm: perm as PermType,
81
+ },
82
+ });
83
+ if (res.code !== 0) {
84
+ throw new Error(res.msg);
85
+ }
86
+
87
+ return {
88
+ success: true,
89
+ member: res.data?.member,
90
+ };
91
+ }
92
+
93
+ async function removeMember(
94
+ client: Lark.Client,
95
+ token: string,
96
+ type: string,
97
+ memberType: string,
98
+ memberId: string,
99
+ ) {
100
+ const res = await client.drive.permissionMember.delete({
101
+ path: { token, member_id: memberId },
102
+ params: { type: type as CreateTokenType, member_type: memberType as MemberType },
103
+ });
104
+ if (res.code !== 0) {
105
+ throw new Error(res.msg);
106
+ }
107
+
108
+ return {
109
+ success: true,
110
+ };
111
+ }
112
+
113
+ // ============ Tool Registration ============
114
+
115
+ export function registerFeishuPermTools(api: KlawPluginApi) {
116
+ if (!api.config) {
117
+ return;
118
+ }
119
+
120
+ const accounts = listEnabledFeishuAccounts(api.config);
121
+ if (accounts.length === 0) {
122
+ return;
123
+ }
124
+
125
+ const toolsCfg = resolveAnyEnabledFeishuToolsConfig(accounts);
126
+ if (!toolsCfg.perm) {
127
+ return;
128
+ }
129
+
130
+ type FeishuPermExecuteParams = FeishuPermParams & { accountId?: string };
131
+
132
+ api.registerTool(
133
+ (ctx) => {
134
+ const defaultAccountId = ctx.agentAccountId;
135
+ return {
136
+ name: "feishu_perm",
137
+ label: "Feishu Perm",
138
+ description: "Feishu permission management. Actions: list, add, remove",
139
+ parameters: FeishuPermSchema,
140
+ async execute(_toolCallId, params) {
141
+ const p = params as FeishuPermExecuteParams;
142
+ try {
143
+ const client = createFeishuToolClient({
144
+ api,
145
+ executeParams: p,
146
+ defaultAccountId,
147
+ });
148
+ switch (p.action) {
149
+ case "list":
150
+ return jsonToolResult(await listMembers(client, p.token, p.type));
151
+ case "add":
152
+ return jsonToolResult(
153
+ await addMember(client, p.token, p.type, p.member_type, p.member_id, p.perm),
154
+ );
155
+ case "remove":
156
+ return jsonToolResult(
157
+ await removeMember(client, p.token, p.type, p.member_type, p.member_id),
158
+ );
159
+ default:
160
+ return unknownToolActionResult((p as { action?: unknown }).action);
161
+ }
162
+ } catch (err) {
163
+ return toolExecutionErrorResult(err);
164
+ }
165
+ },
166
+ };
167
+ },
168
+ { name: "feishu_perm" },
169
+ );
170
+ }
package/src/pins.ts ADDED
@@ -0,0 +1,108 @@
1
+ import type { ClawdbotConfig } from "../runtime-api.js";
2
+ import { resolveFeishuRuntimeAccount } from "./accounts.js";
3
+ import { createFeishuClient } from "./client.js";
4
+
5
+ type FeishuPin = {
6
+ messageId: string;
7
+ chatId?: string;
8
+ operatorId?: string;
9
+ operatorIdType?: string;
10
+ createTime?: string;
11
+ };
12
+
13
+ function assertFeishuPinApiSuccess(response: { code?: number; msg?: string }, action: string) {
14
+ if (response.code !== 0) {
15
+ throw new Error(`Feishu ${action} failed: ${response.msg || `code ${response.code}`}`);
16
+ }
17
+ }
18
+
19
+ function normalizePin(pin: {
20
+ message_id: string;
21
+ chat_id?: string;
22
+ operator_id?: string;
23
+ operator_id_type?: string;
24
+ create_time?: string;
25
+ }): FeishuPin {
26
+ return {
27
+ messageId: pin.message_id,
28
+ chatId: pin.chat_id,
29
+ operatorId: pin.operator_id,
30
+ operatorIdType: pin.operator_id_type,
31
+ createTime: pin.create_time,
32
+ };
33
+ }
34
+
35
+ export async function createPinFeishu(params: {
36
+ cfg: ClawdbotConfig;
37
+ messageId: string;
38
+ accountId?: string;
39
+ }): Promise<FeishuPin | null> {
40
+ const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
41
+ if (!account.configured) {
42
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
43
+ }
44
+
45
+ const client = createFeishuClient(account);
46
+ const response = await client.im.pin.create({
47
+ data: {
48
+ message_id: params.messageId,
49
+ },
50
+ });
51
+ assertFeishuPinApiSuccess(response, "pin create");
52
+ return response.data?.pin ? normalizePin(response.data.pin) : null;
53
+ }
54
+
55
+ export async function removePinFeishu(params: {
56
+ cfg: ClawdbotConfig;
57
+ messageId: string;
58
+ accountId?: string;
59
+ }): Promise<void> {
60
+ const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
61
+ if (!account.configured) {
62
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
63
+ }
64
+
65
+ const client = createFeishuClient(account);
66
+ const response = await client.im.pin.delete({
67
+ path: {
68
+ message_id: params.messageId,
69
+ },
70
+ });
71
+ assertFeishuPinApiSuccess(response, "pin delete");
72
+ }
73
+
74
+ export async function listPinsFeishu(params: {
75
+ cfg: ClawdbotConfig;
76
+ chatId: string;
77
+ startTime?: string;
78
+ endTime?: string;
79
+ pageSize?: number;
80
+ pageToken?: string;
81
+ accountId?: string;
82
+ }): Promise<{ chatId: string; pins: FeishuPin[]; hasMore: boolean; pageToken?: string }> {
83
+ const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
84
+ if (!account.configured) {
85
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
86
+ }
87
+
88
+ const client = createFeishuClient(account);
89
+ const response = await client.im.pin.list({
90
+ params: {
91
+ chat_id: params.chatId,
92
+ ...(params.startTime ? { start_time: params.startTime } : {}),
93
+ ...(params.endTime ? { end_time: params.endTime } : {}),
94
+ ...(typeof params.pageSize === "number"
95
+ ? { page_size: Math.max(1, Math.min(100, Math.floor(params.pageSize))) }
96
+ : {}),
97
+ ...(params.pageToken ? { page_token: params.pageToken } : {}),
98
+ },
99
+ });
100
+ assertFeishuPinApiSuccess(response, "pin list");
101
+
102
+ return {
103
+ chatId: params.chatId,
104
+ pins: (response.data?.items ?? []).map(normalizePin),
105
+ hasMore: response.data?.has_more === true,
106
+ pageToken: response.data?.page_token,
107
+ };
108
+ }
@@ -0,0 +1,223 @@
1
+ import type { KlawConfig } from "klaw/plugin-sdk/core";
2
+ import { describe, expect, it } from "vitest";
3
+ import { FeishuConfigSchema } from "./config-schema.js";
4
+ import {
5
+ hasExplicitFeishuGroupConfig,
6
+ resolveFeishuGroupConfig,
7
+ resolveFeishuGroupSenderActivationIngressAccess,
8
+ resolveFeishuReplyPolicy,
9
+ } from "./policy.js";
10
+ import type { FeishuConfig } from "./types.js";
11
+
12
+ function createCfg(feishu: Record<string, unknown>): KlawConfig {
13
+ return {
14
+ channels: {
15
+ feishu,
16
+ },
17
+ } as KlawConfig;
18
+ }
19
+
20
+ function createFeishuConfig(overrides: Partial<FeishuConfig>): FeishuConfig {
21
+ return FeishuConfigSchema.parse(overrides);
22
+ }
23
+
24
+ describe("resolveFeishuReplyPolicy", () => {
25
+ it("defaults open groups to no mention when unset", () => {
26
+ expect(
27
+ resolveFeishuReplyPolicy({
28
+ isDirectMessage: false,
29
+ cfg: createCfg({ groupPolicy: "open" }),
30
+ groupPolicy: "open",
31
+ groupId: "oc_1",
32
+ }),
33
+ ).toEqual({ requireMention: false });
34
+ });
35
+
36
+ it("keeps explicit top-level mention gating in open groups", () => {
37
+ expect(
38
+ resolveFeishuReplyPolicy({
39
+ isDirectMessage: false,
40
+ cfg: createCfg({ groupPolicy: "open", requireMention: true }),
41
+ groupPolicy: "open",
42
+ groupId: "oc_1",
43
+ }),
44
+ ).toEqual({ requireMention: true });
45
+ });
46
+
47
+ it("keeps explicit account mention gating in open groups", () => {
48
+ expect(
49
+ resolveFeishuReplyPolicy({
50
+ isDirectMessage: false,
51
+ cfg: createCfg({
52
+ groupPolicy: "allowlist",
53
+ requireMention: false,
54
+ accounts: {
55
+ work: {
56
+ groupPolicy: "open",
57
+ requireMention: true,
58
+ },
59
+ },
60
+ }),
61
+ accountId: "work",
62
+ groupPolicy: "open",
63
+ groupId: "oc_1",
64
+ }),
65
+ ).toEqual({ requireMention: true });
66
+ });
67
+
68
+ it("keeps explicit per-group mention gating in open groups", () => {
69
+ expect(
70
+ resolveFeishuReplyPolicy({
71
+ isDirectMessage: false,
72
+ cfg: createCfg({
73
+ groupPolicy: "open",
74
+ groups: { oc_1: { requireMention: true } },
75
+ }),
76
+ groupPolicy: "open",
77
+ groupId: "oc_1",
78
+ }),
79
+ ).toEqual({ requireMention: true });
80
+ });
81
+
82
+ it("defaults allowlist groups to require mentions", () => {
83
+ expect(
84
+ resolveFeishuReplyPolicy({
85
+ isDirectMessage: false,
86
+ cfg: createCfg({ groupPolicy: "allowlist" }),
87
+ groupPolicy: "allowlist",
88
+ groupId: "oc_1",
89
+ }),
90
+ ).toEqual({ requireMention: true });
91
+ });
92
+ });
93
+
94
+ describe("resolveFeishuGroupConfig", () => {
95
+ it("falls back to wildcard group config when direct match is missing", () => {
96
+ const cfg = createFeishuConfig({
97
+ groups: {
98
+ "*": { requireMention: false },
99
+ "oc-explicit": { requireMention: true },
100
+ },
101
+ });
102
+
103
+ const resolved = resolveFeishuGroupConfig({
104
+ cfg,
105
+ groupId: "oc-missing",
106
+ });
107
+
108
+ expect(resolved).toEqual({ requireMention: false });
109
+ });
110
+
111
+ it("prefers exact group config over wildcard", () => {
112
+ const cfg = createFeishuConfig({
113
+ groups: {
114
+ "*": { requireMention: false },
115
+ "oc-explicit": { requireMention: true },
116
+ },
117
+ });
118
+
119
+ const resolved = resolveFeishuGroupConfig({
120
+ cfg,
121
+ groupId: "oc-explicit",
122
+ });
123
+
124
+ expect(resolved).toEqual({ requireMention: true });
125
+ });
126
+
127
+ it("keeps case-insensitive matching for explicit group ids", () => {
128
+ const cfg = createFeishuConfig({
129
+ groups: {
130
+ "*": { requireMention: false },
131
+ OC_UPPER: { requireMention: true },
132
+ },
133
+ });
134
+
135
+ const resolved = resolveFeishuGroupConfig({
136
+ cfg,
137
+ groupId: "oc_upper",
138
+ });
139
+
140
+ expect(resolved).toEqual({ requireMention: true });
141
+ });
142
+ });
143
+
144
+ describe("hasExplicitFeishuGroupConfig", () => {
145
+ it("matches direct and case-insensitive group ids", () => {
146
+ const cfg = createFeishuConfig({
147
+ groups: {
148
+ OC_UPPER: { requireMention: true },
149
+ },
150
+ });
151
+
152
+ expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "OC_UPPER" })).toBe(true);
153
+ expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "oc_upper" })).toBe(true);
154
+ });
155
+
156
+ it("does not treat wildcard group defaults as explicit admission", () => {
157
+ const cfg = createFeishuConfig({
158
+ groups: {
159
+ "*": { requireMention: false },
160
+ },
161
+ });
162
+
163
+ expect(hasExplicitFeishuGroupConfig({ cfg, groupId: "oc_any" })).toBe(false);
164
+ });
165
+ });
166
+
167
+ describe("resolveFeishuGroupSenderActivationIngressAccess", () => {
168
+ async function senderDecision(params: {
169
+ allowFrom: Array<string | number>;
170
+ senderOpenId: string;
171
+ senderUserId?: string;
172
+ }) {
173
+ return (
174
+ await resolveFeishuGroupSenderActivationIngressAccess({
175
+ cfg: createCfg({}),
176
+ accountId: "default",
177
+ chatId: "oc_group",
178
+ allowFrom: params.allowFrom,
179
+ senderOpenId: params.senderOpenId,
180
+ senderUserId: params.senderUserId,
181
+ requireMention: false,
182
+ mentionedBot: true,
183
+ })
184
+ ).senderAccess.decision;
185
+ }
186
+
187
+ it("allows provider-prefixed wildcard entries", async () => {
188
+ await expect(
189
+ senderDecision({
190
+ allowFrom: ["feishu:*", "lark:*"],
191
+ senderOpenId: "ou_anyone",
192
+ }),
193
+ ).resolves.toBe("allow");
194
+ });
195
+
196
+ it("matches normalized immutable user ID entries", async () => {
197
+ await expect(
198
+ senderDecision({
199
+ allowFrom: ["feishu:feishu:user:ou_ALLOWED"],
200
+ senderOpenId: "ou_ALLOWED",
201
+ }),
202
+ ).resolves.toBe("allow");
203
+ });
204
+
205
+ it("keeps user and chat allowlist namespaces distinct", async () => {
206
+ await expect(
207
+ senderDecision({
208
+ allowFrom: ["user:oc_group_123"],
209
+ senderOpenId: "oc_group_123",
210
+ }),
211
+ ).resolves.toBe("block");
212
+ });
213
+
214
+ it("supports user_id as an additional immutable sender candidate", async () => {
215
+ await expect(
216
+ senderDecision({
217
+ allowFrom: ["on_user_123"],
218
+ senderOpenId: "ou_other",
219
+ senderUserId: "on_user_123",
220
+ }),
221
+ ).resolves.toBe("allow");
222
+ });
223
+ });