@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/policy.ts CHANGED
@@ -1,13 +1,39 @@
1
- import type {
2
- AllowlistMatch,
3
- ChannelGroupContext,
4
- GroupToolPolicyConfig,
5
- } from "openclaw/plugin-sdk/feishu";
6
- import { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/feishu";
7
- import { normalizeFeishuTarget } from "./targets.js";
8
- import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
1
+ import {
2
+ normalizeAccountId,
3
+ resolveMergedAccountConfig,
4
+ } from "openclaw/plugin-sdk/account-resolution";
5
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
6
+ import { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/group-access";
7
+ import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
8
+ import type { AllowlistMatch, ChannelGroupContext } from "../runtime-api.js";
9
+ import { detectIdType } from "./targets.js";
10
+ import type { FeishuConfig } from "./types.js";
11
+
12
+ type FeishuAllowlistMatch = AllowlistMatch<"wildcard" | "id">;
13
+
14
+ const FEISHU_PROVIDER_PREFIX_RE = /^(feishu|lark):/i;
15
+
16
+ function stripRepeatedFeishuProviderPrefixes(raw: string): string {
17
+ let normalized = raw.trim();
18
+ while (FEISHU_PROVIDER_PREFIX_RE.test(normalized)) {
19
+ normalized = normalized.replace(FEISHU_PROVIDER_PREFIX_RE, "").trim();
20
+ }
21
+ return normalized;
22
+ }
9
23
 
10
- export type FeishuAllowlistMatch = AllowlistMatch<"wildcard" | "id">;
24
+ function canonicalizeFeishuAllowlistKey(params: { kind: "chat" | "user"; value: string }): string {
25
+ const value = params.value.trim();
26
+ if (!value) {
27
+ return "";
28
+ }
29
+ // A typed wildcard (`chat:*`, `user:*`, `open_id:*`, `dm:*`, `group:*`,
30
+ // `channel:*`) collapses to the bare wildcard so it keeps matching across
31
+ // both kinds, preserving the prior `normalizeFeishuTarget`-based behavior.
32
+ if (value === "*") {
33
+ return "*";
34
+ }
35
+ return `${params.kind}:${value}`;
36
+ }
11
37
 
12
38
  function normalizeFeishuAllowEntry(raw: string): string {
13
39
  const trimmed = raw.trim();
@@ -17,9 +43,56 @@ function normalizeFeishuAllowEntry(raw: string): string {
17
43
  if (trimmed === "*") {
18
44
  return "*";
19
45
  }
20
- const withoutProviderPrefix = trimmed.replace(/^feishu:/i, "");
21
- const normalized = normalizeFeishuTarget(withoutProviderPrefix) ?? withoutProviderPrefix;
22
- return normalized.trim().toLowerCase();
46
+
47
+ const withoutProviderPrefix = stripRepeatedFeishuProviderPrefixes(trimmed);
48
+ if (withoutProviderPrefix === "*") {
49
+ return "*";
50
+ }
51
+ const lowered = normalizeOptionalLowercaseString(withoutProviderPrefix) ?? "";
52
+ if (!lowered) {
53
+ return "";
54
+ }
55
+ // Lowercase for prefix detection only; preserve the original ID casing in the
56
+ // canonicalized key. Sender candidates pass through this same path so allowlist
57
+ // entries and runtime IDs stay normalized symmetrically.
58
+ if (
59
+ lowered.startsWith("chat:") ||
60
+ lowered.startsWith("group:") ||
61
+ lowered.startsWith("channel:")
62
+ ) {
63
+ return canonicalizeFeishuAllowlistKey({
64
+ kind: "chat",
65
+ value: withoutProviderPrefix.slice(withoutProviderPrefix.indexOf(":") + 1),
66
+ });
67
+ }
68
+ if (lowered.startsWith("user:") || lowered.startsWith("dm:")) {
69
+ return canonicalizeFeishuAllowlistKey({
70
+ kind: "user",
71
+ value: withoutProviderPrefix.slice(withoutProviderPrefix.indexOf(":") + 1),
72
+ });
73
+ }
74
+ if (lowered.startsWith("open_id:")) {
75
+ return canonicalizeFeishuAllowlistKey({
76
+ kind: "user",
77
+ value: withoutProviderPrefix.slice(withoutProviderPrefix.indexOf(":") + 1),
78
+ });
79
+ }
80
+
81
+ const detectedType = detectIdType(withoutProviderPrefix);
82
+ if (detectedType === "chat_id") {
83
+ return canonicalizeFeishuAllowlistKey({
84
+ kind: "chat",
85
+ value: withoutProviderPrefix,
86
+ });
87
+ }
88
+ if (detectedType === "open_id" || detectedType === "user_id") {
89
+ return canonicalizeFeishuAllowlistKey({
90
+ kind: "user",
91
+ value: withoutProviderPrefix,
92
+ });
93
+ }
94
+
95
+ return "";
23
96
  }
24
97
 
25
98
  export function resolveFeishuAllowlistMatch(params: {
@@ -40,7 +113,7 @@ export function resolveFeishuAllowlistMatch(params: {
40
113
 
41
114
  // Feishu allowlists are ID-based; mutable display names must never grant access.
42
115
  const senderCandidates = [params.senderId, ...(params.senderIds ?? [])]
43
- .map((entry) => normalizeFeishuAllowEntry(String(entry ?? "")))
116
+ .map((entry) => normalizeFeishuAllowEntry(entry ?? ""))
44
117
  .filter(Boolean);
45
118
 
46
119
  for (const senderId of senderCandidates) {
@@ -52,10 +125,7 @@ export function resolveFeishuAllowlistMatch(params: {
52
125
  return { allowed: false };
53
126
  }
54
127
 
55
- export function resolveFeishuGroupConfig(params: {
56
- cfg?: FeishuConfig;
57
- groupId?: string | null;
58
- }): FeishuGroupConfig | undefined {
128
+ export function resolveFeishuGroupConfig(params: { cfg?: FeishuConfig; groupId?: string | null }) {
59
129
  const groups = params.cfg?.groups ?? {};
60
130
  const wildcard = groups["*"];
61
131
  const groupId = params.groupId?.trim();
@@ -68,18 +138,37 @@ export function resolveFeishuGroupConfig(params: {
68
138
  return direct;
69
139
  }
70
140
 
71
- const lowered = groupId.toLowerCase();
72
- const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
141
+ const lowered = normalizeOptionalLowercaseString(groupId) ?? "";
142
+ const matchKey = Object.keys(groups).find(
143
+ (key) => normalizeOptionalLowercaseString(key) === lowered,
144
+ );
73
145
  if (matchKey) {
74
146
  return groups[matchKey];
75
147
  }
76
148
  return wildcard;
77
149
  }
78
150
 
79
- export function resolveFeishuGroupToolPolicy(
80
- params: ChannelGroupContext,
81
- ): GroupToolPolicyConfig | undefined {
82
- const cfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
151
+ export function hasExplicitFeishuGroupConfig(params: {
152
+ cfg?: FeishuConfig;
153
+ groupId?: string | null;
154
+ }): boolean {
155
+ const groups = params.cfg?.groups ?? {};
156
+ const groupId = params.groupId?.trim();
157
+ if (!groupId) {
158
+ return false;
159
+ }
160
+ if (Object.prototype.hasOwnProperty.call(groups, groupId) && groupId !== "*") {
161
+ return true;
162
+ }
163
+
164
+ const lowered = normalizeOptionalLowercaseString(groupId) ?? "";
165
+ return Object.keys(groups).some(
166
+ (key) => key !== "*" && normalizeOptionalLowercaseString(key) === lowered,
167
+ );
168
+ }
169
+
170
+ export function resolveFeishuGroupToolPolicy(params: ChannelGroupContext) {
171
+ const cfg = params.cfg.channels?.feishu;
83
172
  if (!cfg) {
84
173
  return undefined;
85
174
  }
@@ -109,15 +198,39 @@ export function isFeishuGroupAllowed(params: {
109
198
 
110
199
  export function resolveFeishuReplyPolicy(params: {
111
200
  isDirectMessage: boolean;
112
- globalConfig?: FeishuConfig;
113
- groupConfig?: FeishuGroupConfig;
201
+ cfg: OpenClawConfig;
202
+ accountId?: string | null;
203
+ groupId?: string | null;
204
+ /**
205
+ * Effective group policy resolved for this chat. When "open", requireMention
206
+ * defaults to false so that non-text messages (e.g. images) that cannot carry
207
+ * @-mentions are still delivered to the agent.
208
+ */
209
+ groupPolicy?: "open" | "allowlist" | "disabled" | "allowall";
114
210
  }): { requireMention: boolean } {
115
211
  if (params.isDirectMessage) {
116
212
  return { requireMention: false };
117
213
  }
118
214
 
119
- const requireMention =
120
- params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? true;
121
-
122
- return { requireMention };
215
+ const feishuCfg = params.cfg.channels?.feishu;
216
+ const resolvedCfg = resolveMergedAccountConfig<FeishuConfig>({
217
+ channelConfig: feishuCfg,
218
+ accounts: feishuCfg?.accounts as Record<string, Partial<FeishuConfig>> | undefined,
219
+ accountId: normalizeAccountId(params.accountId),
220
+ normalizeAccountId,
221
+ omitKeys: ["defaultAccount"],
222
+ });
223
+ const groupRequireMention = resolveFeishuGroupConfig({
224
+ cfg: resolvedCfg,
225
+ groupId: params.groupId,
226
+ })?.requireMention;
227
+
228
+ return {
229
+ requireMention:
230
+ typeof groupRequireMention === "boolean"
231
+ ? groupRequireMention
232
+ : typeof resolvedCfg.requireMention === "boolean"
233
+ ? resolvedCfg.requireMention
234
+ : params.groupPolicy !== "open",
235
+ };
123
236
  }
package/src/post.ts CHANGED
@@ -1,7 +1,9 @@
1
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
2
+ import { isRecord } from "./comment-shared.js";
1
3
  import { normalizeFeishuExternalKey } from "./external-keys.js";
2
4
 
3
5
  const FALLBACK_POST_TEXT = "[Rich text message]";
4
- const MARKDOWN_SPECIAL_CHARS = /([\\`*_{}\[\]()#+\-!|>~])/g;
6
+ const MARKDOWN_SPECIAL_CHARS = /([\\`*_{}[\]()#+\-!|>~])/g;
5
7
 
6
8
  type PostParseResult = {
7
9
  textContent: string;
@@ -15,10 +17,6 @@ type PostPayload = {
15
17
  content: unknown[];
16
18
  };
17
19
 
18
- function isRecord(value: unknown): value is Record<string, unknown> {
19
- return typeof value === "object" && value !== null;
20
- }
21
-
22
20
  function toStringOrEmpty(value: unknown): string {
23
21
  return typeof value === "string" ? value : "";
24
22
  }
@@ -136,7 +134,7 @@ function renderElement(
136
134
  return escapeMarkdownText(toStringOrEmpty(element));
137
135
  }
138
136
 
139
- const tag = toStringOrEmpty(element.tag).toLowerCase();
137
+ const tag = normalizeLowercaseStringOrEmpty(toStringOrEmpty(element.tag));
140
138
  switch (tag) {
141
139
  case "text":
142
140
  return renderTextElement(element);
@@ -168,6 +166,9 @@ function renderElement(
168
166
  }
169
167
  case "emotion":
170
168
  return renderEmotionElement(element);
169
+ case "md":
170
+ case "lark_md":
171
+ return toStringOrEmpty(element.text) || toStringOrEmpty(element.content);
171
172
  case "br":
172
173
  return "\n";
173
174
  case "hr":
package/src/probe.test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { clearProbeCache, FEISHU_PROBE_REQUEST_TIMEOUT_MS, probeFeishu } from "./probe.js";
2
3
 
3
4
  const createFeishuClientMock = vi.hoisted(() => vi.fn());
4
5
 
@@ -6,12 +7,10 @@ vi.mock("./client.js", () => ({
6
7
  createFeishuClient: createFeishuClientMock,
7
8
  }));
8
9
 
9
- import { FEISHU_PROBE_REQUEST_TIMEOUT_MS, probeFeishu, clearProbeCache } from "./probe.js";
10
-
11
10
  const DEFAULT_CREDS = { appId: "cli_123", appSecret: "secret" } as const; // pragma: allowlist secret
12
11
  const DEFAULT_SUCCESS_RESPONSE = {
13
12
  code: 0,
14
- bot: { bot_name: "TestBot", open_id: "ou_abc123" },
13
+ data: { pingBotInfo: { botName: "TestBot", botID: "ou_abc123" } },
15
14
  } as const;
16
15
  const DEFAULT_SUCCESS_RESULT = {
17
16
  ok: true,
@@ -21,7 +20,7 @@ const DEFAULT_SUCCESS_RESULT = {
21
20
  } as const;
22
21
  const BOT1_RESPONSE = {
23
22
  code: 0,
24
- bot: { bot_name: "Bot1", open_id: "ou_1" },
23
+ data: { pingBotInfo: { botName: "Bot1", botID: "ou_1" } },
25
24
  } as const;
26
25
 
27
26
  function makeRequestFn(response: Record<string, unknown>) {
@@ -40,7 +39,12 @@ function setupSuccessClient() {
40
39
 
41
40
  async function expectDefaultSuccessResult(
42
41
  creds = DEFAULT_CREDS,
43
- expected: Awaited<ReturnType<typeof probeFeishu>> = DEFAULT_SUCCESS_RESULT,
42
+ expected: {
43
+ ok: true;
44
+ appId: string;
45
+ botName: string;
46
+ botOpenId: string;
47
+ } = DEFAULT_SUCCESS_RESULT,
44
48
  ) {
45
49
  const result = await probeFeishu(creds);
46
50
  expect(result).toEqual(expected);
@@ -131,8 +135,9 @@ describe("probeFeishu", () => {
131
135
 
132
136
  expect(requestFn).toHaveBeenCalledWith(
133
137
  expect.objectContaining({
134
- method: "GET",
135
- url: "/open-apis/bot/v3/info",
138
+ method: "POST",
139
+ url: "/open-apis/bot/v1/openclaw_bot/ping",
140
+ data: { needBotInfo: true },
136
141
  timeout: FEISHU_PROBE_REQUEST_TIMEOUT_MS,
137
142
  }),
138
143
  );
@@ -255,10 +260,10 @@ describe("probeFeishu", () => {
255
260
  });
256
261
  });
257
262
 
258
- it("handles response.data.bot fallback path", async () => {
263
+ it("handles response with pingBotInfo in data", async () => {
259
264
  setupClient({
260
265
  code: 0,
261
- data: { bot: { bot_name: "DataBot", open_id: "ou_data" } },
266
+ data: { pingBotInfo: { botName: "DataBot", botID: "ou_data" } },
262
267
  });
263
268
 
264
269
  await expectDefaultSuccessResult(DEFAULT_CREDS, {
package/src/probe.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
1
2
  import { raceWithTimeoutAndAbort } from "./async.js";
2
3
  import { createFeishuClient, type FeishuClientCredentials } from "./client.js";
3
4
  import type { FeishuProbeResult } from "./types.js";
@@ -17,11 +18,19 @@ export type ProbeFeishuOptions = {
17
18
  abortSignal?: AbortSignal;
18
19
  };
19
20
 
20
- type FeishuBotInfoResponse = {
21
+ type FeishuPingResponse = {
21
22
  code: number;
22
23
  msg?: string;
23
- bot?: { bot_name?: string; open_id?: string };
24
- data?: { bot?: { bot_name?: string; open_id?: string } };
24
+ data?: { pingBotInfo?: { botID?: string; botName?: string } };
25
+ };
26
+
27
+ type FeishuRequestClient = ReturnType<typeof createFeishuClient> & {
28
+ request(params: {
29
+ method: "POST";
30
+ url: string;
31
+ data: Record<string, unknown>;
32
+ timeout: number;
33
+ }): Promise<FeishuPingResponse>;
25
34
  };
26
35
 
27
36
  function setCachedProbeResult(
@@ -70,16 +79,17 @@ export async function probeFeishu(
70
79
  }
71
80
 
72
81
  try {
73
- const client = createFeishuClient(creds);
74
- // Use bot/v3/info API to get bot information
75
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK generic request method
76
- const responseResult = await raceWithTimeoutAndAbort<FeishuBotInfoResponse>(
77
- (client as any).request({
78
- method: "GET",
79
- url: "/open-apis/bot/v3/info",
80
- data: {},
82
+ const client = createFeishuClient(creds) as FeishuRequestClient;
83
+ // Feishu-provided endpoint for OpenClaw, supported on both Feishu (CN)
84
+ // and Lark (international). No OAuth scopes required. Validates
85
+ // credentials and registers the app as an AI agent (智能体).
86
+ const responseResult = await raceWithTimeoutAndAbort<FeishuPingResponse>(
87
+ client.request({
88
+ method: "POST",
89
+ url: "/open-apis/bot/v1/openclaw_bot/ping",
90
+ data: { needBotInfo: true },
81
91
  timeout: timeoutMs,
82
- }) as Promise<FeishuBotInfoResponse>,
92
+ }),
83
93
  {
84
94
  timeoutMs,
85
95
  abortSignal: options.abortSignal,
@@ -126,14 +136,14 @@ export async function probeFeishu(
126
136
  );
127
137
  }
128
138
 
129
- const bot = response.bot || response.data?.bot;
139
+ const botInfo = response.data?.pingBotInfo;
130
140
  return setCachedProbeResult(
131
141
  cacheKey,
132
142
  {
133
143
  ok: true,
134
144
  appId: creds.appId,
135
- botName: bot?.bot_name,
136
- botOpenId: bot?.open_id,
145
+ botName: botInfo?.botName,
146
+ botOpenId: botInfo?.botID,
137
147
  },
138
148
  PROBE_SUCCESS_TTL_MS,
139
149
  );
@@ -143,7 +153,7 @@ export async function probeFeishu(
143
153
  {
144
154
  ok: false,
145
155
  appId: creds.appId,
146
- error: err instanceof Error ? err.message : String(err),
156
+ error: formatErrorMessage(err),
147
157
  },
148
158
  PROBE_ERROR_TTL_MS,
149
159
  );
@@ -0,0 +1,59 @@
1
+ const EVENT_DEDUP_TTL_MS = 5 * 60 * 1000;
2
+ const EVENT_MEMORY_MAX_SIZE = 2_000;
3
+
4
+ const processingClaims = new Map<string, number>();
5
+
6
+ function resolveEventDedupeKey(
7
+ namespace: string,
8
+ messageId: string | undefined | null,
9
+ ): string | null {
10
+ const trimmed = messageId?.trim();
11
+ return trimmed ? `${namespace}:${trimmed}` : null;
12
+ }
13
+
14
+ function pruneProcessingClaims(now: number): void {
15
+ const cutoff = now - EVENT_DEDUP_TTL_MS;
16
+ for (const [key, seenAt] of processingClaims) {
17
+ if (seenAt < cutoff) {
18
+ processingClaims.delete(key);
19
+ }
20
+ }
21
+ while (processingClaims.size > EVENT_MEMORY_MAX_SIZE) {
22
+ const oldestKey = processingClaims.keys().next().value;
23
+ if (!oldestKey) {
24
+ return;
25
+ }
26
+ processingClaims.delete(oldestKey);
27
+ }
28
+ }
29
+
30
+ export function tryBeginFeishuMessageProcessing(
31
+ messageId: string | undefined | null,
32
+ namespace = "global",
33
+ ): boolean {
34
+ const key = resolveEventDedupeKey(namespace, messageId);
35
+ if (!key) {
36
+ return true;
37
+ }
38
+ const now = Date.now();
39
+ pruneProcessingClaims(now);
40
+ if (processingClaims.has(key)) {
41
+ processingClaims.delete(key);
42
+ processingClaims.set(key, now);
43
+ pruneProcessingClaims(now);
44
+ return false;
45
+ }
46
+ processingClaims.set(key, now);
47
+ pruneProcessingClaims(now);
48
+ return true;
49
+ }
50
+
51
+ export function releaseFeishuMessageProcessing(
52
+ messageId: string | undefined | null,
53
+ namespace = "global",
54
+ ): void {
55
+ const key = resolveEventDedupeKey(namespace, messageId);
56
+ if (key) {
57
+ processingClaims.delete(key);
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export { renderQrTerminal } from "openclaw/plugin-sdk/media-runtime";
package/src/reactions.ts CHANGED
@@ -1,8 +1,8 @@
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
 
5
- export type FeishuReaction = {
5
+ type FeishuReaction = {
6
6
  reactionId: string;
7
7
  emojiType: string;
8
8
  operatorType: "app" | "user";
@@ -10,7 +10,7 @@ export type FeishuReaction = {
10
10
  };
11
11
 
12
12
  function resolveConfiguredFeishuClient(params: { cfg: ClawdbotConfig; accountId?: string }) {
13
- const account = resolveFeishuAccount(params);
13
+ const account = resolveFeishuRuntimeAccount(params);
14
14
  if (!account.configured) {
15
15
  throw new Error(`Feishu account "${account.accountId}" not configured`);
16
16
  }
@@ -121,33 +121,3 @@ export async function listReactionsFeishu(params: {
121
121
  item.operator_id?.open_id ?? item.operator_id?.user_id ?? item.operator_id?.union_id ?? "",
122
122
  }));
123
123
  }
124
-
125
- /**
126
- * Common Feishu emoji types for convenience.
127
- * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
128
- */
129
- export const FeishuEmoji = {
130
- // Common reactions
131
- THUMBSUP: "THUMBSUP",
132
- THUMBSDOWN: "THUMBSDOWN",
133
- HEART: "HEART",
134
- SMILE: "SMILE",
135
- GRINNING: "GRINNING",
136
- LAUGHING: "LAUGHING",
137
- CRY: "CRY",
138
- ANGRY: "ANGRY",
139
- SURPRISED: "SURPRISED",
140
- THINKING: "THINKING",
141
- CLAP: "CLAP",
142
- OK: "OK",
143
- FIST: "FIST",
144
- PRAY: "PRAY",
145
- FIRE: "FIRE",
146
- PARTY: "PARTY",
147
- CHECK: "CHECK",
148
- CROSS: "CROSS",
149
- QUESTION: "QUESTION",
150
- EXCLAMATION: "EXCLAMATION",
151
- } as const;
152
-
153
- export type FeishuEmojiType = (typeof FeishuEmoji)[keyof typeof FeishuEmoji];
@@ -0,0 +1,59 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { resolveFeishuReasoningPreviewEnabled } from "./reasoning-preview.js";
3
+
4
+ const { loadSessionStoreMock } = vi.hoisted(() => ({
5
+ loadSessionStoreMock: vi.fn(),
6
+ }));
7
+
8
+ vi.mock("./bot-runtime-api.js", async () => {
9
+ const actual =
10
+ await vi.importActual<typeof import("./bot-runtime-api.js")>("./bot-runtime-api.js");
11
+ return {
12
+ ...actual,
13
+ loadSessionStore: loadSessionStoreMock,
14
+ };
15
+ });
16
+
17
+ describe("resolveFeishuReasoningPreviewEnabled", () => {
18
+ beforeEach(() => {
19
+ vi.clearAllMocks();
20
+ });
21
+
22
+ it("enables previews only for stream reasoning sessions", () => {
23
+ loadSessionStoreMock.mockReturnValue({
24
+ "agent:main:feishu:dm:ou_sender_1": { reasoningLevel: "stream" },
25
+ "agent:main:feishu:dm:ou_sender_2": { reasoningLevel: "on" },
26
+ });
27
+
28
+ expect(
29
+ resolveFeishuReasoningPreviewEnabled({
30
+ storePath: "/tmp/feishu-sessions.json",
31
+ sessionKey: "agent:main:feishu:dm:ou_sender_1",
32
+ }),
33
+ ).toBe(true);
34
+ expect(
35
+ resolveFeishuReasoningPreviewEnabled({
36
+ storePath: "/tmp/feishu-sessions.json",
37
+ sessionKey: "agent:main:feishu:dm:ou_sender_2",
38
+ }),
39
+ ).toBe(false);
40
+ });
41
+
42
+ it("returns false for missing sessions or load failures", () => {
43
+ loadSessionStoreMock.mockImplementationOnce(() => {
44
+ throw new Error("disk unavailable");
45
+ });
46
+
47
+ expect(
48
+ resolveFeishuReasoningPreviewEnabled({
49
+ storePath: "/tmp/feishu-sessions.json",
50
+ sessionKey: "agent:main:feishu:dm:ou_sender_1",
51
+ }),
52
+ ).toBe(false);
53
+ expect(
54
+ resolveFeishuReasoningPreviewEnabled({
55
+ storePath: "/tmp/feishu-sessions.json",
56
+ }),
57
+ ).toBe(false);
58
+ });
59
+ });
@@ -0,0 +1,20 @@
1
+ import { loadSessionStore, resolveSessionStoreEntry } from "./bot-runtime-api.js";
2
+
3
+ export function resolveFeishuReasoningPreviewEnabled(params: {
4
+ storePath: string;
5
+ sessionKey?: string;
6
+ }): boolean {
7
+ if (!params.sessionKey) {
8
+ return false;
9
+ }
10
+
11
+ try {
12
+ const store = loadSessionStore(params.storePath, { skipCache: true });
13
+ return (
14
+ resolveSessionStoreEntry({ store, sessionKey: params.sessionKey }).existing
15
+ ?.reasoningLevel === "stream"
16
+ );
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ createReplyPrefixContext,
3
+ type ClawdbotConfig,
4
+ type OutboundIdentity,
5
+ type ReplyPayload,
6
+ type RuntimeEnv,
7
+ } from "../runtime-api.js";