@openclaw/feishu 2026.3.13 → 2026.5.1-beta.1

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 (188) hide show
  1. package/api.ts +31 -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/index.ts +70 -53
  6. package/openclaw.plugin.json +1653 -4
  7. package/package.json +32 -7
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.test.ts +14 -0
  14. package/setup-entry.ts +13 -0
  15. package/src/accounts.test.ts +95 -7
  16. package/src/accounts.ts +199 -117
  17. package/src/app-registration.ts +331 -0
  18. package/src/approval-auth.test.ts +24 -0
  19. package/src/approval-auth.ts +25 -0
  20. package/src/async.test.ts +35 -0
  21. package/src/async.ts +43 -1
  22. package/src/audio-preflight.runtime.ts +9 -0
  23. package/src/bitable.test.ts +131 -0
  24. package/src/bitable.ts +59 -22
  25. package/src/bot-content.ts +474 -0
  26. package/src/bot-group-name.test.ts +108 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.broadcast.test.ts +463 -0
  30. package/src/bot.card-action.test.ts +519 -5
  31. package/src/bot.checkBotMentioned.test.ts +92 -20
  32. package/src/bot.helpers.test.ts +118 -0
  33. package/src/bot.stripBotMention.test.ts +13 -21
  34. package/src/bot.test.ts +1334 -401
  35. package/src/bot.ts +778 -775
  36. package/src/card-action.ts +408 -40
  37. package/src/card-interaction.test.ts +129 -0
  38. package/src/card-interaction.ts +159 -0
  39. package/src/card-test-helpers.ts +47 -0
  40. package/src/card-ux-approval.ts +65 -0
  41. package/src/card-ux-launcher.test.ts +99 -0
  42. package/src/card-ux-launcher.ts +121 -0
  43. package/src/card-ux-shared.ts +33 -0
  44. package/src/channel-runtime-api.ts +16 -0
  45. package/src/channel.runtime.ts +47 -0
  46. package/src/channel.test.ts +914 -3
  47. package/src/channel.ts +1252 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +84 -28
  50. package/src/chat.ts +68 -10
  51. package/src/client.test.ts +212 -103
  52. package/src/client.ts +115 -21
  53. package/src/comment-dispatcher-runtime-api.ts +6 -0
  54. package/src/comment-dispatcher.test.ts +169 -0
  55. package/src/comment-dispatcher.ts +107 -0
  56. package/src/comment-handler-runtime-api.ts +3 -0
  57. package/src/comment-handler.test.ts +486 -0
  58. package/src/comment-handler.ts +309 -0
  59. package/src/comment-reaction.test.ts +166 -0
  60. package/src/comment-reaction.ts +259 -0
  61. package/src/comment-shared.test.ts +182 -0
  62. package/src/comment-shared.ts +365 -0
  63. package/src/comment-target.ts +44 -0
  64. package/src/config-schema.test.ts +63 -1
  65. package/src/config-schema.ts +31 -4
  66. package/src/conversation-id.test.ts +18 -0
  67. package/src/conversation-id.ts +199 -0
  68. package/src/dedup-runtime-api.ts +1 -0
  69. package/src/dedup.ts +32 -94
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +119 -20
  72. package/src/directory.ts +61 -91
  73. package/src/doc-schema.ts +1 -1
  74. package/src/docx-batch-insert.test.ts +39 -38
  75. package/src/docx-batch-insert.ts +55 -19
  76. package/src/docx-color-text.ts +9 -4
  77. package/src/docx-table-ops.test.ts +53 -0
  78. package/src/docx-table-ops.ts +52 -34
  79. package/src/docx-types.ts +38 -0
  80. package/src/docx.account-selection.test.ts +12 -3
  81. package/src/docx.test.ts +314 -74
  82. package/src/docx.ts +278 -122
  83. package/src/drive-schema.ts +47 -1
  84. package/src/drive.test.ts +1219 -0
  85. package/src/drive.ts +614 -13
  86. package/src/dynamic-agent.ts +10 -4
  87. package/src/event-types.ts +45 -0
  88. package/src/external-keys.ts +1 -1
  89. package/src/lifecycle.test-support.ts +220 -0
  90. package/src/media.test.ts +375 -26
  91. package/src/media.ts +434 -88
  92. package/src/mention-target.types.ts +5 -0
  93. package/src/mention.ts +32 -51
  94. package/src/message-action-contract.ts +13 -0
  95. package/src/monitor-state-runtime-api.ts +7 -0
  96. package/src/monitor-transport-runtime-api.ts +7 -0
  97. package/src/monitor.account.ts +218 -312
  98. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  99. package/src/monitor.bot-identity.ts +86 -0
  100. package/src/monitor.bot-menu-handler.ts +165 -0
  101. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  102. package/src/monitor.bot-menu.test.ts +178 -0
  103. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  104. package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
  105. package/src/monitor.cleanup.test.ts +376 -0
  106. package/src/monitor.comment-notice-handler.ts +105 -0
  107. package/src/monitor.comment.test.ts +937 -0
  108. package/src/monitor.comment.ts +1386 -0
  109. package/src/monitor.lifecycle.test.ts +4 -0
  110. package/src/monitor.message-handler.ts +339 -0
  111. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  112. package/src/monitor.reaction.test.ts +108 -48
  113. package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
  114. package/src/monitor.startup.test.ts +11 -9
  115. package/src/monitor.startup.ts +26 -16
  116. package/src/monitor.state.ts +20 -5
  117. package/src/monitor.synthetic-error.ts +18 -0
  118. package/src/monitor.test-mocks.ts +2 -2
  119. package/src/monitor.transport.ts +220 -60
  120. package/src/monitor.ts +15 -10
  121. package/src/monitor.webhook-e2e.test.ts +65 -7
  122. package/src/monitor.webhook-security.test.ts +122 -0
  123. package/src/monitor.webhook.test-helpers.ts +44 -26
  124. package/src/outbound-runtime-api.ts +1 -0
  125. package/src/outbound.test.ts +616 -37
  126. package/src/outbound.ts +623 -81
  127. package/src/perm-schema.ts +1 -1
  128. package/src/perm.ts +1 -7
  129. package/src/pins.ts +108 -0
  130. package/src/policy.test.ts +297 -117
  131. package/src/policy.ts +142 -29
  132. package/src/post.ts +7 -6
  133. package/src/probe.test.ts +14 -9
  134. package/src/probe.ts +26 -16
  135. package/src/processing-claims.ts +59 -0
  136. package/src/qr-terminal.ts +1 -0
  137. package/src/reactions.ts +4 -34
  138. package/src/reasoning-preview.test.ts +59 -0
  139. package/src/reasoning-preview.ts +20 -0
  140. package/src/reply-dispatcher-runtime-api.ts +7 -0
  141. package/src/reply-dispatcher.test.ts +660 -29
  142. package/src/reply-dispatcher.ts +407 -154
  143. package/src/runtime.ts +6 -3
  144. package/src/secret-contract.ts +145 -0
  145. package/src/secret-input.ts +1 -13
  146. package/src/security-audit-shared.ts +69 -0
  147. package/src/security-audit.test.ts +61 -0
  148. package/src/security-audit.ts +1 -0
  149. package/src/send-result.ts +1 -1
  150. package/src/send-target.test.ts +9 -3
  151. package/src/send-target.ts +10 -4
  152. package/src/send.reply-fallback.test.ts +77 -2
  153. package/src/send.test.ts +386 -4
  154. package/src/send.ts +399 -86
  155. package/src/sequential-key.test.ts +72 -0
  156. package/src/sequential-key.ts +28 -0
  157. package/src/sequential-queue.test.ts +92 -0
  158. package/src/sequential-queue.ts +16 -0
  159. package/src/session-conversation.ts +42 -0
  160. package/src/session-route.ts +48 -0
  161. package/src/setup-core.ts +51 -0
  162. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  163. package/src/setup-surface.ts +581 -0
  164. package/src/streaming-card.test.ts +138 -2
  165. package/src/streaming-card.ts +134 -18
  166. package/src/subagent-hooks.test.ts +603 -0
  167. package/src/subagent-hooks.ts +397 -0
  168. package/src/targets.ts +3 -13
  169. package/src/test-support/lifecycle-test-support.ts +479 -0
  170. package/src/thread-bindings.test.ts +143 -0
  171. package/src/thread-bindings.ts +330 -0
  172. package/src/tool-account-routing.test.ts +66 -8
  173. package/src/tool-account.test.ts +44 -0
  174. package/src/tool-account.ts +40 -17
  175. package/src/tool-factory-test-harness.ts +11 -8
  176. package/src/tool-result.ts +3 -1
  177. package/src/tools-config.ts +1 -1
  178. package/src/types.ts +16 -15
  179. package/src/typing.ts +10 -6
  180. package/src/wiki-schema.ts +1 -1
  181. package/src/wiki.ts +1 -7
  182. package/subagent-hooks-api.ts +31 -0
  183. package/tsconfig.json +16 -0
  184. package/src/feishu-command-handler.ts +0 -59
  185. package/src/onboarding.status.test.ts +0 -25
  186. package/src/onboarding.ts +0 -489
  187. package/src/send-message.ts +0 -71
  188. package/src/targets.test.ts +0 -70
package/src/runtime.ts CHANGED
@@ -1,6 +1,9 @@
1
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/compat";
2
- import type { PluginRuntime } from "openclaw/plugin-sdk/feishu";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk/core";
2
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
3
3
 
4
4
  const { setRuntime: setFeishuRuntime, getRuntime: getFeishuRuntime } =
5
- createPluginRuntimeStore<PluginRuntime>("Feishu runtime not initialized");
5
+ createPluginRuntimeStore<PluginRuntime>({
6
+ pluginId: "feishu",
7
+ errorMessage: "Feishu runtime not initialized",
8
+ });
6
9
  export { getFeishuRuntime, setFeishuRuntime };
@@ -0,0 +1,145 @@
1
+ import {
2
+ collectConditionalChannelFieldAssignments,
3
+ collectSimpleChannelFieldAssignments,
4
+ getChannelSurface,
5
+ hasOwnProperty,
6
+ normalizeSecretStringValue,
7
+ type ResolverContext,
8
+ type SecretDefaults,
9
+ type SecretTargetRegistryEntry,
10
+ } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
11
+
12
+ export const secretTargetRegistryEntries: SecretTargetRegistryEntry[] = [
13
+ {
14
+ id: "channels.feishu.accounts.*.appSecret",
15
+ targetType: "channels.feishu.accounts.*.appSecret",
16
+ configFile: "openclaw.json",
17
+ pathPattern: "channels.feishu.accounts.*.appSecret",
18
+ secretShape: "secret_input",
19
+ expectedResolvedValue: "string",
20
+ includeInPlan: true,
21
+ includeInConfigure: true,
22
+ includeInAudit: true,
23
+ },
24
+ {
25
+ id: "channels.feishu.accounts.*.encryptKey",
26
+ targetType: "channels.feishu.accounts.*.encryptKey",
27
+ configFile: "openclaw.json",
28
+ pathPattern: "channels.feishu.accounts.*.encryptKey",
29
+ secretShape: "secret_input",
30
+ expectedResolvedValue: "string",
31
+ includeInPlan: true,
32
+ includeInConfigure: true,
33
+ includeInAudit: true,
34
+ },
35
+ {
36
+ id: "channels.feishu.accounts.*.verificationToken",
37
+ targetType: "channels.feishu.accounts.*.verificationToken",
38
+ configFile: "openclaw.json",
39
+ pathPattern: "channels.feishu.accounts.*.verificationToken",
40
+ secretShape: "secret_input",
41
+ expectedResolvedValue: "string",
42
+ includeInPlan: true,
43
+ includeInConfigure: true,
44
+ includeInAudit: true,
45
+ },
46
+ {
47
+ id: "channels.feishu.appSecret",
48
+ targetType: "channels.feishu.appSecret",
49
+ configFile: "openclaw.json",
50
+ pathPattern: "channels.feishu.appSecret",
51
+ secretShape: "secret_input",
52
+ expectedResolvedValue: "string",
53
+ includeInPlan: true,
54
+ includeInConfigure: true,
55
+ includeInAudit: true,
56
+ },
57
+ {
58
+ id: "channels.feishu.encryptKey",
59
+ targetType: "channels.feishu.encryptKey",
60
+ configFile: "openclaw.json",
61
+ pathPattern: "channels.feishu.encryptKey",
62
+ secretShape: "secret_input",
63
+ expectedResolvedValue: "string",
64
+ includeInPlan: true,
65
+ includeInConfigure: true,
66
+ includeInAudit: true,
67
+ },
68
+ {
69
+ id: "channels.feishu.verificationToken",
70
+ targetType: "channels.feishu.verificationToken",
71
+ configFile: "openclaw.json",
72
+ pathPattern: "channels.feishu.verificationToken",
73
+ secretShape: "secret_input",
74
+ expectedResolvedValue: "string",
75
+ includeInPlan: true,
76
+ includeInConfigure: true,
77
+ includeInAudit: true,
78
+ },
79
+ ];
80
+
81
+ export function collectRuntimeConfigAssignments(params: {
82
+ config: { channels?: Record<string, unknown> };
83
+ defaults?: SecretDefaults;
84
+ context: ResolverContext;
85
+ }): void {
86
+ const resolved = getChannelSurface(params.config, "feishu");
87
+ if (!resolved) {
88
+ return;
89
+ }
90
+ const { channel: feishu, surface } = resolved;
91
+ collectSimpleChannelFieldAssignments({
92
+ channelKey: "feishu",
93
+ field: "appSecret",
94
+ channel: feishu,
95
+ surface,
96
+ defaults: params.defaults,
97
+ context: params.context,
98
+ topInactiveReason: "no enabled account inherits this top-level Feishu appSecret.",
99
+ accountInactiveReason: "Feishu account is disabled.",
100
+ });
101
+ const baseConnectionMode =
102
+ normalizeSecretStringValue(feishu.connectionMode) === "webhook" ? "webhook" : "websocket";
103
+ const resolveAccountMode = (account: Record<string, unknown>) =>
104
+ hasOwnProperty(account, "connectionMode")
105
+ ? normalizeSecretStringValue(account.connectionMode)
106
+ : baseConnectionMode;
107
+ collectConditionalChannelFieldAssignments({
108
+ channelKey: "feishu",
109
+ field: "encryptKey",
110
+ channel: feishu,
111
+ surface,
112
+ defaults: params.defaults,
113
+ context: params.context,
114
+ topLevelActiveWithoutAccounts: baseConnectionMode === "webhook",
115
+ topLevelInheritedAccountActive: ({ account, enabled }) =>
116
+ enabled &&
117
+ !hasOwnProperty(account, "encryptKey") &&
118
+ resolveAccountMode(account) === "webhook",
119
+ accountActive: ({ account, enabled }) => enabled && resolveAccountMode(account) === "webhook",
120
+ topInactiveReason: "no enabled Feishu webhook-mode surface inherits this top-level encryptKey.",
121
+ accountInactiveReason: "Feishu account is disabled or not running in webhook mode.",
122
+ });
123
+ collectConditionalChannelFieldAssignments({
124
+ channelKey: "feishu",
125
+ field: "verificationToken",
126
+ channel: feishu,
127
+ surface,
128
+ defaults: params.defaults,
129
+ context: params.context,
130
+ topLevelActiveWithoutAccounts: baseConnectionMode === "webhook",
131
+ topLevelInheritedAccountActive: ({ account, enabled }) =>
132
+ enabled &&
133
+ !hasOwnProperty(account, "verificationToken") &&
134
+ resolveAccountMode(account) === "webhook",
135
+ accountActive: ({ account, enabled }) => enabled && resolveAccountMode(account) === "webhook",
136
+ topInactiveReason:
137
+ "no enabled Feishu webhook-mode surface inherits this top-level verificationToken.",
138
+ accountInactiveReason: "Feishu account is disabled or not running in webhook mode.",
139
+ });
140
+ }
141
+
142
+ export const channelSecrets = {
143
+ secretTargetRegistryEntries,
144
+ collectRuntimeConfigAssignments,
145
+ };
@@ -1,13 +1 @@
1
- import {
2
- buildSecretInputSchema,
3
- hasConfiguredSecretInput,
4
- normalizeResolvedSecretInputString,
5
- normalizeSecretInputString,
6
- } from "openclaw/plugin-sdk/feishu";
7
-
8
- export {
9
- buildSecretInputSchema,
10
- hasConfiguredSecretInput,
11
- normalizeResolvedSecretInputString,
12
- normalizeSecretInputString,
13
- };
1
+ export { buildSecretInputSchema, hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
@@ -0,0 +1,69 @@
1
+ import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
2
+ import type { OpenClawConfig } from "../runtime-api.js";
3
+
4
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
5
+ return value && typeof value === "object" && !Array.isArray(value)
6
+ ? (value as Record<string, unknown>)
7
+ : undefined;
8
+ }
9
+
10
+ function hasNonEmptyString(value: unknown): boolean {
11
+ return typeof value === "string" && value.trim().length > 0;
12
+ }
13
+
14
+ function isFeishuDocToolEnabled(cfg: OpenClawConfig): boolean {
15
+ const channels = asRecord(cfg.channels);
16
+ const feishu = asRecord(channels?.feishu);
17
+ if (!feishu || feishu.enabled === false) {
18
+ return false;
19
+ }
20
+
21
+ const baseTools = asRecord(feishu.tools);
22
+ const baseDocEnabled = baseTools?.doc !== false;
23
+ const baseAppId = hasNonEmptyString(feishu.appId);
24
+ const baseAppSecret = hasConfiguredSecretInput(feishu.appSecret, cfg.secrets?.defaults);
25
+ const baseConfigured = baseAppId && baseAppSecret;
26
+
27
+ const accounts = asRecord(feishu.accounts);
28
+ if (!accounts || Object.keys(accounts).length === 0) {
29
+ return baseDocEnabled && baseConfigured;
30
+ }
31
+
32
+ for (const accountValue of Object.values(accounts)) {
33
+ const account = asRecord(accountValue) ?? {};
34
+ if (account.enabled === false) {
35
+ continue;
36
+ }
37
+ const accountTools = asRecord(account.tools);
38
+ const effectiveTools = accountTools ?? baseTools;
39
+ const docEnabled = effectiveTools?.doc !== false;
40
+ if (!docEnabled) {
41
+ continue;
42
+ }
43
+ const accountConfigured =
44
+ (hasNonEmptyString(account.appId) || baseAppId) &&
45
+ (hasConfiguredSecretInput(account.appSecret, cfg.secrets?.defaults) || baseAppSecret);
46
+ if (accountConfigured) {
47
+ return true;
48
+ }
49
+ }
50
+
51
+ return false;
52
+ }
53
+
54
+ export function collectFeishuSecurityAuditFindings(params: { cfg: OpenClawConfig }) {
55
+ if (!isFeishuDocToolEnabled(params.cfg)) {
56
+ return [];
57
+ }
58
+ return [
59
+ {
60
+ checkId: "channels.feishu.doc_owner_open_id",
61
+ severity: "warn" as const,
62
+ title: "Feishu doc create can grant requester permissions",
63
+ detail:
64
+ 'channels.feishu tools include "doc"; feishu_doc action "create" can grant document access to the trusted requesting Feishu user.',
65
+ remediation:
66
+ "Disable channels.feishu.tools.doc when not needed, and restrict tool access for untrusted prompts.",
67
+ },
68
+ ];
69
+ }
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { OpenClawConfig } from "../runtime-api.js";
3
+ import { collectFeishuSecurityAuditFindings } from "./security-audit.js";
4
+
5
+ describe("Feishu security audit findings", () => {
6
+ it.each([
7
+ {
8
+ name: "warns when doc tool is enabled because create can grant requester access",
9
+ cfg: {
10
+ channels: {
11
+ feishu: {
12
+ appId: "cli_test",
13
+ appSecret: "secret_test",
14
+ },
15
+ },
16
+ } satisfies OpenClawConfig,
17
+ expectedFinding: "channels.feishu.doc_owner_open_id",
18
+ },
19
+ {
20
+ name: "treats SecretRef appSecret as configured for doc tool risk detection",
21
+ cfg: {
22
+ channels: {
23
+ feishu: {
24
+ appId: "cli_test",
25
+ appSecret: {
26
+ source: "env",
27
+ provider: "default",
28
+ id: "FEISHU_APP_SECRET",
29
+ },
30
+ },
31
+ },
32
+ } satisfies OpenClawConfig,
33
+ expectedFinding: "channels.feishu.doc_owner_open_id",
34
+ },
35
+ {
36
+ name: "does not warn for doc grant risk when doc tools are disabled",
37
+ cfg: {
38
+ channels: {
39
+ feishu: {
40
+ appId: "cli_test",
41
+ appSecret: "secret_test",
42
+ tools: { doc: false },
43
+ },
44
+ },
45
+ } satisfies OpenClawConfig,
46
+ expectedNoFinding: "channels.feishu.doc_owner_open_id",
47
+ },
48
+ ])("$name", ({ cfg, expectedFinding, expectedNoFinding }) => {
49
+ const findings = collectFeishuSecurityAuditFindings({ cfg });
50
+ if (expectedFinding) {
51
+ expect(
52
+ findings.some(
53
+ (finding) => finding.checkId === expectedFinding && finding.severity === "warn",
54
+ ),
55
+ ).toBe(true);
56
+ }
57
+ if (expectedNoFinding) {
58
+ expect(findings.some((finding) => finding.checkId === expectedNoFinding)).toBe(false);
59
+ }
60
+ });
61
+ });
@@ -0,0 +1 @@
1
+ export { collectFeishuSecurityAuditFindings } from "./security-audit-shared.js";
@@ -1,4 +1,4 @@
1
- export type FeishuMessageApiResponse = {
1
+ type FeishuMessageApiResponse = {
2
2
  code?: number;
3
3
  msg?: string;
4
4
  data?: {
@@ -1,22 +1,28 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import { resolveFeishuSendTarget } from "./send-target.js";
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { ClawdbotConfig } from "../runtime-api.js";
4
3
 
5
4
  const resolveFeishuAccountMock = vi.hoisted(() => vi.fn());
6
5
  const createFeishuClientMock = vi.hoisted(() => vi.fn());
7
6
 
8
7
  vi.mock("./accounts.js", () => ({
9
8
  resolveFeishuAccount: resolveFeishuAccountMock,
9
+ resolveFeishuRuntimeAccount: resolveFeishuAccountMock,
10
10
  }));
11
11
 
12
12
  vi.mock("./client.js", () => ({
13
13
  createFeishuClient: createFeishuClientMock,
14
14
  }));
15
15
 
16
+ let resolveFeishuSendTarget: typeof import("./send-target.js").resolveFeishuSendTarget;
17
+
16
18
  describe("resolveFeishuSendTarget", () => {
17
19
  const cfg = {} as ClawdbotConfig;
18
20
  const client = { id: "client" };
19
21
 
22
+ beforeAll(async () => {
23
+ ({ resolveFeishuSendTarget } = await import("./send-target.js"));
24
+ });
25
+
20
26
  beforeEach(() => {
21
27
  resolveFeishuAccountMock.mockReset().mockReturnValue({
22
28
  accountId: "default",
@@ -1,15 +1,21 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
2
- import { resolveFeishuAccount } from "./accounts.js";
1
+ import type { ClawdbotConfig } from "../runtime-api.js";
2
+ import { resolveFeishuRuntimeAccount } from "./accounts.js";
3
3
  import { createFeishuClient } from "./client.js";
4
4
  import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
5
5
 
6
+ type FeishuSendTarget = {
7
+ client: ReturnType<typeof createFeishuClient>;
8
+ receiveId: string;
9
+ receiveIdType: ReturnType<typeof resolveReceiveIdType>;
10
+ };
11
+
6
12
  export function resolveFeishuSendTarget(params: {
7
13
  cfg: ClawdbotConfig;
8
14
  to: string;
9
15
  accountId?: string;
10
- }) {
16
+ }): FeishuSendTarget {
11
17
  const target = params.to.trim();
12
- const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
18
+ const account = resolveFeishuRuntimeAccount({ cfg: params.cfg, accountId: params.accountId });
13
19
  if (!account.configured) {
14
20
  throw new Error(`Feishu account "${account.accountId}" not configured`);
15
21
  }
@@ -1,4 +1,4 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  const resolveFeishuSendTargetMock = vi.hoisted(() => vi.fn());
4
4
  const resolveMarkdownTableModeMock = vi.hoisted(() => vi.fn(() => "preserve"));
@@ -9,6 +9,7 @@ vi.mock("./send-target.js", () => ({
9
9
  }));
10
10
 
11
11
  vi.mock("./runtime.js", () => ({
12
+ setFeishuRuntime: vi.fn(),
12
13
  getFeishuRuntime: () => ({
13
14
  channel: {
14
15
  text: {
@@ -19,7 +20,8 @@ vi.mock("./runtime.js", () => ({
19
20
  }),
20
21
  }));
21
22
 
22
- import { sendCardFeishu, sendMessageFeishu } from "./send.js";
23
+ let sendCardFeishu: typeof import("./send.js").sendCardFeishu;
24
+ let sendMessageFeishu: typeof import("./send.js").sendMessageFeishu;
23
25
 
24
26
  describe("Feishu reply fallback for withdrawn/deleted targets", () => {
25
27
  const replyMock = vi.fn();
@@ -35,6 +37,10 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
35
37
  expect(result.messageId).toBe(expectedMessageId);
36
38
  }
37
39
 
40
+ beforeAll(async () => {
41
+ ({ sendCardFeishu, sendMessageFeishu } = await import("./send.js"));
42
+ });
43
+
38
44
  beforeEach(() => {
39
45
  vi.clearAllMocks();
40
46
  resolveFeishuSendTargetMock.mockReturnValue({
@@ -171,6 +177,75 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
171
177
  expect(createMock).not.toHaveBeenCalled();
172
178
  });
173
179
 
180
+ it("fails thread replies instead of falling back to a top-level send", async () => {
181
+ replyMock.mockResolvedValue({
182
+ code: 230011,
183
+ msg: "The message was withdrawn.",
184
+ });
185
+
186
+ await expect(
187
+ sendMessageFeishu({
188
+ cfg: {} as never,
189
+ to: "chat:oc_group_1",
190
+ text: "hello",
191
+ replyToMessageId: "om_parent",
192
+ replyInThread: true,
193
+ }),
194
+ ).rejects.toThrow(
195
+ "Feishu thread reply failed: reply target is unavailable and cannot safely fall back to a top-level send.",
196
+ );
197
+
198
+ expect(createMock).not.toHaveBeenCalled();
199
+ expect(replyMock).toHaveBeenCalledWith({
200
+ path: { message_id: "om_parent" },
201
+ data: expect.objectContaining({
202
+ reply_in_thread: true,
203
+ }),
204
+ });
205
+ });
206
+
207
+ it("fails thrown withdrawn thread replies instead of falling back to create", async () => {
208
+ const sdkError = Object.assign(new Error("request failed"), { code: 230011 });
209
+ replyMock.mockRejectedValue(sdkError);
210
+
211
+ await expect(
212
+ sendMessageFeishu({
213
+ cfg: {} as never,
214
+ to: "chat:oc_group_1",
215
+ text: "hello",
216
+ replyToMessageId: "om_parent",
217
+ replyInThread: true,
218
+ }),
219
+ ).rejects.toThrow(
220
+ "Feishu thread reply failed: reply target is unavailable and cannot safely fall back to a top-level send.",
221
+ );
222
+
223
+ expect(createMock).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it("still falls back for non-thread replies to withdrawn targets", async () => {
227
+ replyMock.mockResolvedValue({
228
+ code: 230011,
229
+ msg: "The message was withdrawn.",
230
+ });
231
+ createMock.mockResolvedValue({
232
+ code: 0,
233
+ data: { message_id: "om_non_thread_fallback" },
234
+ });
235
+
236
+ await expectFallbackResult(
237
+ () =>
238
+ sendMessageFeishu({
239
+ cfg: {} as never,
240
+ to: "user:ou_target",
241
+ text: "hello",
242
+ replyToMessageId: "om_parent",
243
+ replyInThread: false,
244
+ }),
245
+ "om_non_thread_fallback",
246
+ );
247
+ });
248
+
174
249
  it("re-throws non-withdrawn thrown errors for card messages", async () => {
175
250
  const sdkError = Object.assign(new Error("permission denied"), { code: 99991401 });
176
251
  replyMock.mockRejectedValue(sdkError);