@nextclaw/channel-plugin-feishu 0.2.12 → 0.2.14

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 (102) hide show
  1. package/README.md +3 -1
  2. package/index.ts +65 -0
  3. package/openclaw.plugin.json +3 -7
  4. package/package.json +33 -9
  5. package/skills/feishu-doc/SKILL.md +211 -0
  6. package/skills/feishu-doc/references/block-types.md +103 -0
  7. package/skills/feishu-drive/SKILL.md +97 -0
  8. package/skills/feishu-perm/SKILL.md +119 -0
  9. package/skills/feishu-wiki/SKILL.md +111 -0
  10. package/src/accounts.test.ts +371 -0
  11. package/src/accounts.ts +244 -0
  12. package/src/async.ts +62 -0
  13. package/src/bitable.ts +725 -0
  14. package/src/bot.card-action.test.ts +63 -0
  15. package/src/bot.checkBotMentioned.test.ts +193 -0
  16. package/src/bot.stripBotMention.test.ts +134 -0
  17. package/src/bot.test.ts +2107 -0
  18. package/src/bot.ts +1556 -0
  19. package/src/card-action.ts +79 -0
  20. package/src/channel.test.ts +48 -0
  21. package/src/channel.ts +369 -0
  22. package/src/chat-schema.ts +24 -0
  23. package/src/chat.test.ts +89 -0
  24. package/src/chat.ts +130 -0
  25. package/src/client.test.ts +324 -0
  26. package/src/client.ts +196 -0
  27. package/src/config-schema.test.ts +247 -0
  28. package/src/config-schema.ts +306 -0
  29. package/src/dedup.ts +203 -0
  30. package/src/directory.test.ts +40 -0
  31. package/src/directory.ts +156 -0
  32. package/src/doc-schema.ts +182 -0
  33. package/src/docx-batch-insert.test.ts +90 -0
  34. package/src/docx-batch-insert.ts +187 -0
  35. package/src/docx-color-text.ts +149 -0
  36. package/src/docx-table-ops.ts +298 -0
  37. package/src/docx.account-selection.test.ts +70 -0
  38. package/src/docx.test.ts +445 -0
  39. package/src/docx.ts +1460 -0
  40. package/src/drive-schema.ts +46 -0
  41. package/src/drive.ts +228 -0
  42. package/src/dynamic-agent.ts +131 -0
  43. package/src/external-keys.test.ts +20 -0
  44. package/src/external-keys.ts +19 -0
  45. package/src/feishu-command-handler.ts +59 -0
  46. package/src/media.test.ts +523 -0
  47. package/src/media.ts +484 -0
  48. package/src/mention.ts +133 -0
  49. package/src/monitor.account.ts +562 -0
  50. package/src/monitor.reaction.test.ts +653 -0
  51. package/src/monitor.startup.test.ts +190 -0
  52. package/src/monitor.startup.ts +64 -0
  53. package/src/monitor.state.defaults.test.ts +46 -0
  54. package/src/monitor.state.ts +155 -0
  55. package/src/monitor.test-mocks.ts +45 -0
  56. package/src/monitor.transport.ts +264 -0
  57. package/src/monitor.ts +95 -0
  58. package/src/monitor.webhook-e2e.test.ts +214 -0
  59. package/src/monitor.webhook-security.test.ts +142 -0
  60. package/src/monitor.webhook.test-helpers.ts +98 -0
  61. package/src/onboarding.status.test.ts +25 -0
  62. package/src/onboarding.test.ts +143 -0
  63. package/src/onboarding.ts +489 -0
  64. package/src/outbound.test.ts +356 -0
  65. package/src/outbound.ts +176 -0
  66. package/src/perm-schema.ts +52 -0
  67. package/src/perm.ts +176 -0
  68. package/src/policy.test.ts +154 -0
  69. package/src/policy.ts +123 -0
  70. package/src/post.test.ts +105 -0
  71. package/src/post.ts +274 -0
  72. package/src/probe.test.ts +270 -0
  73. package/src/probe.ts +156 -0
  74. package/src/reactions.ts +153 -0
  75. package/src/reply-dispatcher.test.ts +513 -0
  76. package/src/reply-dispatcher.ts +397 -0
  77. package/src/runtime.ts +6 -0
  78. package/src/secret-input.ts +13 -0
  79. package/src/send-message.ts +71 -0
  80. package/src/send-result.ts +29 -0
  81. package/src/send-target.test.ts +74 -0
  82. package/src/send-target.ts +29 -0
  83. package/src/send.reply-fallback.test.ts +189 -0
  84. package/src/send.test.ts +168 -0
  85. package/src/send.ts +481 -0
  86. package/src/streaming-card.test.ts +54 -0
  87. package/src/streaming-card.ts +374 -0
  88. package/src/targets.test.ts +70 -0
  89. package/src/targets.ts +107 -0
  90. package/src/tool-account-routing.test.ts +129 -0
  91. package/src/tool-account.ts +70 -0
  92. package/src/tool-factory-test-harness.ts +76 -0
  93. package/src/tool-result.test.ts +32 -0
  94. package/src/tool-result.ts +14 -0
  95. package/src/tools-config.test.ts +21 -0
  96. package/src/tools-config.ts +22 -0
  97. package/src/types.ts +103 -0
  98. package/src/typing.test.ts +144 -0
  99. package/src/typing.ts +210 -0
  100. package/src/wiki-schema.ts +55 -0
  101. package/src/wiki.ts +233 -0
  102. package/index.js +0 -27
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: feishu-wiki
3
+ description: |
4
+ Feishu knowledge base navigation. Activate when user mentions knowledge base, wiki, or wiki links.
5
+ ---
6
+
7
+ # Feishu Wiki Tool
8
+
9
+ Single tool `feishu_wiki` for knowledge base operations.
10
+
11
+ ## Token Extraction
12
+
13
+ From URL `https://xxx.feishu.cn/wiki/ABC123def` → `token` = `ABC123def`
14
+
15
+ ## Actions
16
+
17
+ ### List Knowledge Spaces
18
+
19
+ ```json
20
+ { "action": "spaces" }
21
+ ```
22
+
23
+ Returns all accessible wiki spaces.
24
+
25
+ ### List Nodes
26
+
27
+ ```json
28
+ { "action": "nodes", "space_id": "7xxx" }
29
+ ```
30
+
31
+ With parent:
32
+
33
+ ```json
34
+ { "action": "nodes", "space_id": "7xxx", "parent_node_token": "wikcnXXX" }
35
+ ```
36
+
37
+ ### Get Node Details
38
+
39
+ ```json
40
+ { "action": "get", "token": "ABC123def" }
41
+ ```
42
+
43
+ Returns: `node_token`, `obj_token`, `obj_type`, etc. Use `obj_token` with `feishu_doc` to read/write the document.
44
+
45
+ ### Create Node
46
+
47
+ ```json
48
+ { "action": "create", "space_id": "7xxx", "title": "New Page" }
49
+ ```
50
+
51
+ With type and parent:
52
+
53
+ ```json
54
+ {
55
+ "action": "create",
56
+ "space_id": "7xxx",
57
+ "title": "Sheet",
58
+ "obj_type": "sheet",
59
+ "parent_node_token": "wikcnXXX"
60
+ }
61
+ ```
62
+
63
+ `obj_type`: `docx` (default), `sheet`, `bitable`, `mindnote`, `file`, `doc`, `slides`
64
+
65
+ ### Move Node
66
+
67
+ ```json
68
+ { "action": "move", "space_id": "7xxx", "node_token": "wikcnXXX" }
69
+ ```
70
+
71
+ To different location:
72
+
73
+ ```json
74
+ {
75
+ "action": "move",
76
+ "space_id": "7xxx",
77
+ "node_token": "wikcnXXX",
78
+ "target_space_id": "7yyy",
79
+ "target_parent_token": "wikcnYYY"
80
+ }
81
+ ```
82
+
83
+ ### Rename Node
84
+
85
+ ```json
86
+ { "action": "rename", "space_id": "7xxx", "node_token": "wikcnXXX", "title": "New Title" }
87
+ ```
88
+
89
+ ## Wiki-Doc Workflow
90
+
91
+ To edit a wiki page:
92
+
93
+ 1. Get node: `{ "action": "get", "token": "wiki_token" }` → returns `obj_token`
94
+ 2. Read doc: `feishu_doc { "action": "read", "doc_token": "obj_token" }`
95
+ 3. Write doc: `feishu_doc { "action": "write", "doc_token": "obj_token", "content": "..." }`
96
+
97
+ ## Configuration
98
+
99
+ ```yaml
100
+ channels:
101
+ feishu:
102
+ tools:
103
+ wiki: true # default: true
104
+ doc: true # required - wiki content uses feishu_doc
105
+ ```
106
+
107
+ **Dependency:** This tool requires `feishu_doc` to be enabled. Wiki pages are documents - use `feishu_wiki` to navigate, then `feishu_doc` to read/edit content.
108
+
109
+ ## Permissions
110
+
111
+ Required: `wiki:wiki` or `wiki:wiki:readonly`
@@ -0,0 +1,371 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ resolveDefaultFeishuAccountId,
4
+ resolveDefaultFeishuAccountSelection,
5
+ resolveFeishuAccount,
6
+ resolveFeishuCredentials,
7
+ } from "./accounts.js";
8
+ import type { FeishuConfig } from "./types.js";
9
+
10
+ const asConfig = (value: Partial<FeishuConfig>) => value as FeishuConfig;
11
+
12
+ function makeDefaultAndRouterAccounts() {
13
+ return {
14
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
15
+ "router-d": { appId: "cli_router", appSecret: "secret_router" }, // pragma: allowlist secret
16
+ };
17
+ }
18
+
19
+ function expectExplicitDefaultAccountSelection(
20
+ account: ReturnType<typeof resolveFeishuAccount>,
21
+ appId: string,
22
+ ) {
23
+ expect(account.accountId).toBe("router-d");
24
+ expect(account.selectionSource).toBe("explicit-default");
25
+ expect(account.configured).toBe(true);
26
+ expect(account.appId).toBe(appId);
27
+ }
28
+
29
+ function withEnvVar(key: string, value: string | undefined, run: () => void) {
30
+ const prev = process.env[key];
31
+ if (value === undefined) {
32
+ delete process.env[key];
33
+ } else {
34
+ process.env[key] = value;
35
+ }
36
+ try {
37
+ run();
38
+ } finally {
39
+ if (prev === undefined) {
40
+ delete process.env[key];
41
+ } else {
42
+ process.env[key] = prev;
43
+ }
44
+ }
45
+ }
46
+
47
+ function expectUnresolvedEnvSecretRefError(key: string) {
48
+ expect(() =>
49
+ resolveFeishuCredentials(
50
+ asConfig({
51
+ appId: "cli_123",
52
+ appSecret: { source: "env", provider: "default", id: key } as never,
53
+ }),
54
+ ),
55
+ ).toThrow(/unresolved SecretRef/i);
56
+ }
57
+
58
+ describe("resolveDefaultFeishuAccountId", () => {
59
+ it("prefers channels.feishu.defaultAccount when configured", () => {
60
+ const cfg = {
61
+ channels: {
62
+ feishu: {
63
+ defaultAccount: "router-d",
64
+ accounts: makeDefaultAndRouterAccounts(),
65
+ },
66
+ },
67
+ };
68
+
69
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
70
+ });
71
+
72
+ it("normalizes configured defaultAccount before lookup", () => {
73
+ const cfg = {
74
+ channels: {
75
+ feishu: {
76
+ defaultAccount: "Router D",
77
+ accounts: {
78
+ "router-d": { appId: "cli_router", appSecret: "secret_router" }, // pragma: allowlist secret
79
+ },
80
+ },
81
+ },
82
+ };
83
+
84
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
85
+ });
86
+
87
+ it("keeps configured defaultAccount even when not present in accounts map", () => {
88
+ const cfg = {
89
+ channels: {
90
+ feishu: {
91
+ defaultAccount: "router-d",
92
+ accounts: {
93
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
94
+ zeta: { appId: "cli_zeta", appSecret: "secret_zeta" }, // pragma: allowlist secret
95
+ },
96
+ },
97
+ },
98
+ };
99
+
100
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("router-d");
101
+ });
102
+
103
+ it("falls back to literal default account id when present", () => {
104
+ const cfg = {
105
+ channels: {
106
+ feishu: {
107
+ accounts: {
108
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
109
+ zeta: { appId: "cli_zeta", appSecret: "secret_zeta" }, // pragma: allowlist secret
110
+ },
111
+ },
112
+ },
113
+ };
114
+
115
+ expect(resolveDefaultFeishuAccountId(cfg as never)).toBe("default");
116
+ });
117
+
118
+ it("reports selection source for configured defaults and mapped defaults", () => {
119
+ const explicitDefaultCfg = {
120
+ channels: {
121
+ feishu: {
122
+ defaultAccount: "router-d",
123
+ accounts: {},
124
+ },
125
+ },
126
+ };
127
+ expect(resolveDefaultFeishuAccountSelection(explicitDefaultCfg as never)).toEqual({
128
+ accountId: "router-d",
129
+ source: "explicit-default",
130
+ });
131
+
132
+ const mappedDefaultCfg = {
133
+ channels: {
134
+ feishu: {
135
+ accounts: {
136
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
137
+ },
138
+ },
139
+ },
140
+ };
141
+ expect(resolveDefaultFeishuAccountSelection(mappedDefaultCfg as never)).toEqual({
142
+ accountId: "default",
143
+ source: "mapped-default",
144
+ });
145
+ });
146
+ });
147
+
148
+ describe("resolveFeishuCredentials", () => {
149
+ it("throws unresolved SecretRef errors by default for unsupported secret sources", () => {
150
+ expect(() =>
151
+ resolveFeishuCredentials(
152
+ asConfig({
153
+ appId: "cli_123",
154
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" } as never,
155
+ }),
156
+ ),
157
+ ).toThrow(/unresolved SecretRef/i);
158
+ });
159
+
160
+ it("returns null (without throwing) when unresolved SecretRef is allowed", () => {
161
+ const creds = resolveFeishuCredentials(
162
+ asConfig({
163
+ appId: "cli_123",
164
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" } as never,
165
+ }),
166
+ { allowUnresolvedSecretRef: true },
167
+ );
168
+
169
+ expect(creds).toBeNull();
170
+ });
171
+
172
+ it("throws unresolved SecretRef error when env SecretRef points to missing env var", () => {
173
+ const key = "FEISHU_APP_SECRET_MISSING_TEST";
174
+ withEnvVar(key, undefined, () => {
175
+ expectUnresolvedEnvSecretRefError(key);
176
+ });
177
+ });
178
+
179
+ it("resolves env SecretRef objects when unresolved refs are allowed", () => {
180
+ const key = "FEISHU_APP_SECRET_TEST";
181
+ const prev = process.env[key];
182
+ process.env[key] = " secret_from_env ";
183
+
184
+ try {
185
+ const creds = resolveFeishuCredentials(
186
+ asConfig({
187
+ appId: "cli_123",
188
+ appSecret: { source: "env", provider: "default", id: key } as never,
189
+ }),
190
+ { allowUnresolvedSecretRef: true },
191
+ );
192
+
193
+ expect(creds).toEqual({
194
+ appId: "cli_123",
195
+ appSecret: "secret_from_env", // pragma: allowlist secret
196
+ encryptKey: undefined,
197
+ verificationToken: undefined,
198
+ domain: "feishu",
199
+ });
200
+ } finally {
201
+ if (prev === undefined) {
202
+ delete process.env[key];
203
+ } else {
204
+ process.env[key] = prev;
205
+ }
206
+ }
207
+ });
208
+
209
+ it("resolves env SecretRef with custom provider alias when unresolved refs are allowed", () => {
210
+ const key = "FEISHU_APP_SECRET_CUSTOM_PROVIDER_TEST";
211
+ const prev = process.env[key];
212
+ process.env[key] = " secret_from_env_alias ";
213
+
214
+ try {
215
+ const creds = resolveFeishuCredentials(
216
+ asConfig({
217
+ appId: "cli_123",
218
+ appSecret: { source: "env", provider: "corp-env", id: key } as never,
219
+ }),
220
+ { allowUnresolvedSecretRef: true },
221
+ );
222
+
223
+ expect(creds?.appSecret).toBe("secret_from_env_alias");
224
+ } finally {
225
+ if (prev === undefined) {
226
+ delete process.env[key];
227
+ } else {
228
+ process.env[key] = prev;
229
+ }
230
+ }
231
+ });
232
+
233
+ it("preserves unresolved SecretRef diagnostics for env refs in default mode", () => {
234
+ const key = "FEISHU_APP_SECRET_POLICY_TEST";
235
+ withEnvVar(key, "secret_from_env", () => {
236
+ expectUnresolvedEnvSecretRefError(key);
237
+ });
238
+ });
239
+
240
+ it("trims and returns credentials when values are valid strings", () => {
241
+ const creds = resolveFeishuCredentials(
242
+ asConfig({
243
+ appId: " cli_123 ",
244
+ appSecret: " secret_456 ",
245
+ encryptKey: " enc ",
246
+ verificationToken: " vt ",
247
+ }),
248
+ );
249
+
250
+ expect(creds).toEqual({
251
+ appId: "cli_123",
252
+ appSecret: "secret_456", // pragma: allowlist secret
253
+ encryptKey: "enc",
254
+ verificationToken: "vt",
255
+ domain: "feishu",
256
+ });
257
+ });
258
+
259
+ it("does not resolve encryptKey SecretRefs outside webhook mode", () => {
260
+ const creds = resolveFeishuCredentials(
261
+ asConfig({
262
+ connectionMode: "websocket",
263
+ appId: "cli_123",
264
+ appSecret: "secret_456",
265
+ encryptKey: { source: "file", provider: "default", id: "path/to/secret" } as never,
266
+ }),
267
+ );
268
+
269
+ expect(creds).toEqual({
270
+ appId: "cli_123",
271
+ appSecret: "secret_456", // pragma: allowlist secret
272
+ encryptKey: undefined,
273
+ verificationToken: undefined,
274
+ domain: "feishu",
275
+ });
276
+ });
277
+ });
278
+
279
+ describe("resolveFeishuAccount", () => {
280
+ it("uses top-level credentials with configured default account id even without account map entry", () => {
281
+ const cfg = {
282
+ channels: {
283
+ feishu: {
284
+ defaultAccount: "router-d",
285
+ appId: "top_level_app",
286
+ appSecret: "top_level_secret", // pragma: allowlist secret
287
+ accounts: {
288
+ default: { appId: "cli_default", appSecret: "secret_default" }, // pragma: allowlist secret
289
+ },
290
+ },
291
+ },
292
+ };
293
+
294
+ const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
295
+ expectExplicitDefaultAccountSelection(account, "top_level_app");
296
+ });
297
+
298
+ it("uses configured default account when accountId is omitted", () => {
299
+ const cfg = {
300
+ channels: {
301
+ feishu: {
302
+ defaultAccount: "router-d",
303
+ accounts: {
304
+ default: { enabled: true },
305
+ "router-d": { appId: "cli_router", appSecret: "secret_router", enabled: true }, // pragma: allowlist secret
306
+ },
307
+ },
308
+ },
309
+ };
310
+
311
+ const account = resolveFeishuAccount({ cfg: cfg as never, accountId: undefined });
312
+ expectExplicitDefaultAccountSelection(account, "cli_router");
313
+ });
314
+
315
+ it("keeps explicit accountId selection", () => {
316
+ const cfg = {
317
+ channels: {
318
+ feishu: {
319
+ defaultAccount: "router-d",
320
+ accounts: makeDefaultAndRouterAccounts(),
321
+ },
322
+ },
323
+ };
324
+
325
+ const account = resolveFeishuAccount({ cfg: cfg as never, accountId: "default" });
326
+ expect(account.accountId).toBe("default");
327
+ expect(account.selectionSource).toBe("explicit");
328
+ expect(account.appId).toBe("cli_default");
329
+ });
330
+
331
+ it("surfaces unresolved SecretRef errors in account resolution", () => {
332
+ expect(() =>
333
+ resolveFeishuAccount({
334
+ cfg: {
335
+ channels: {
336
+ feishu: {
337
+ accounts: {
338
+ main: {
339
+ appId: "cli_123",
340
+ appSecret: { source: "file", provider: "default", id: "path/to/secret" },
341
+ } as never,
342
+ },
343
+ },
344
+ },
345
+ } as never,
346
+ accountId: "main",
347
+ }),
348
+ ).toThrow(/unresolved SecretRef/i);
349
+ });
350
+
351
+ it("does not throw when account name is non-string", () => {
352
+ expect(() =>
353
+ resolveFeishuAccount({
354
+ cfg: {
355
+ channels: {
356
+ feishu: {
357
+ accounts: {
358
+ main: {
359
+ name: { bad: true },
360
+ appId: "cli_123",
361
+ appSecret: "secret_456", // pragma: allowlist secret
362
+ } as never,
363
+ },
364
+ },
365
+ },
366
+ } as never,
367
+ accountId: "main",
368
+ }),
369
+ ).not.toThrow();
370
+ });
371
+ });