@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.
- package/api.ts +31 -0
- package/channel-entry.ts +20 -0
- package/channel-plugin-api.ts +1 -0
- package/contract-api.ts +16 -0
- package/index.ts +70 -53
- package/openclaw.plugin.json +1653 -4
- package/package.json +32 -7
- package/runtime-api.ts +55 -0
- package/secret-contract-api.ts +5 -0
- package/security-contract-api.ts +1 -0
- package/session-key-api.ts +1 -0
- package/setup-api.ts +3 -0
- package/setup-entry.test.ts +14 -0
- package/setup-entry.ts +13 -0
- package/src/accounts.test.ts +95 -7
- package/src/accounts.ts +199 -117
- package/src/app-registration.ts +331 -0
- package/src/approval-auth.test.ts +24 -0
- package/src/approval-auth.ts +25 -0
- package/src/async.test.ts +35 -0
- package/src/async.ts +43 -1
- package/src/audio-preflight.runtime.ts +9 -0
- package/src/bitable.test.ts +131 -0
- package/src/bitable.ts +59 -22
- package/src/bot-content.ts +474 -0
- package/src/bot-group-name.test.ts +108 -0
- package/src/bot-runtime-api.ts +12 -0
- package/src/bot-sender-name.ts +125 -0
- package/src/bot.broadcast.test.ts +463 -0
- package/src/bot.card-action.test.ts +519 -5
- package/src/bot.checkBotMentioned.test.ts +92 -20
- package/src/bot.helpers.test.ts +118 -0
- package/src/bot.stripBotMention.test.ts +13 -21
- package/src/bot.test.ts +1334 -401
- package/src/bot.ts +778 -775
- package/src/card-action.ts +408 -40
- package/src/card-interaction.test.ts +129 -0
- package/src/card-interaction.ts +159 -0
- package/src/card-test-helpers.ts +47 -0
- package/src/card-ux-approval.ts +65 -0
- package/src/card-ux-launcher.test.ts +99 -0
- package/src/card-ux-launcher.ts +121 -0
- package/src/card-ux-shared.ts +33 -0
- package/src/channel-runtime-api.ts +16 -0
- package/src/channel.runtime.ts +47 -0
- package/src/channel.test.ts +914 -3
- package/src/channel.ts +1252 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +84 -28
- package/src/chat.ts +68 -10
- package/src/client.test.ts +212 -103
- package/src/client.ts +115 -21
- package/src/comment-dispatcher-runtime-api.ts +6 -0
- package/src/comment-dispatcher.test.ts +169 -0
- package/src/comment-dispatcher.ts +107 -0
- package/src/comment-handler-runtime-api.ts +3 -0
- package/src/comment-handler.test.ts +486 -0
- package/src/comment-handler.ts +309 -0
- package/src/comment-reaction.test.ts +166 -0
- package/src/comment-reaction.ts +259 -0
- package/src/comment-shared.test.ts +182 -0
- package/src/comment-shared.ts +365 -0
- package/src/comment-target.ts +44 -0
- package/src/config-schema.test.ts +63 -1
- package/src/config-schema.ts +31 -4
- package/src/conversation-id.test.ts +18 -0
- package/src/conversation-id.ts +199 -0
- package/src/dedup-runtime-api.ts +1 -0
- package/src/dedup.ts +32 -94
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +119 -20
- package/src/directory.ts +61 -91
- package/src/doc-schema.ts +1 -1
- package/src/docx-batch-insert.test.ts +39 -38
- package/src/docx-batch-insert.ts +55 -19
- package/src/docx-color-text.ts +9 -4
- package/src/docx-table-ops.test.ts +53 -0
- package/src/docx-table-ops.ts +52 -34
- package/src/docx-types.ts +38 -0
- package/src/docx.account-selection.test.ts +12 -3
- package/src/docx.test.ts +314 -74
- package/src/docx.ts +278 -122
- package/src/drive-schema.ts +47 -1
- package/src/drive.test.ts +1219 -0
- package/src/drive.ts +614 -13
- package/src/dynamic-agent.ts +10 -4
- package/src/event-types.ts +45 -0
- package/src/external-keys.ts +1 -1
- package/src/lifecycle.test-support.ts +220 -0
- package/src/media.test.ts +375 -26
- package/src/media.ts +434 -88
- package/src/mention-target.types.ts +5 -0
- package/src/mention.ts +32 -51
- package/src/message-action-contract.ts +13 -0
- package/src/monitor-state-runtime-api.ts +7 -0
- package/src/monitor-transport-runtime-api.ts +7 -0
- package/src/monitor.account.ts +218 -312
- package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
- package/src/monitor.bot-identity.ts +86 -0
- package/src/monitor.bot-menu-handler.ts +165 -0
- package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
- package/src/monitor.bot-menu.test.ts +178 -0
- package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
- package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
- package/src/monitor.cleanup.test.ts +376 -0
- package/src/monitor.comment-notice-handler.ts +105 -0
- package/src/monitor.comment.test.ts +937 -0
- package/src/monitor.comment.ts +1386 -0
- package/src/monitor.lifecycle.test.ts +4 -0
- package/src/monitor.message-handler.ts +339 -0
- package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
- package/src/monitor.reaction.test.ts +108 -48
- package/src/monitor.reply-once.lifecycle.test-support.ts +190 -0
- package/src/monitor.startup.test.ts +11 -9
- package/src/monitor.startup.ts +26 -16
- package/src/monitor.state.ts +20 -5
- package/src/monitor.synthetic-error.ts +18 -0
- package/src/monitor.test-mocks.ts +2 -2
- package/src/monitor.transport.ts +220 -60
- package/src/monitor.ts +15 -10
- package/src/monitor.webhook-e2e.test.ts +65 -7
- package/src/monitor.webhook-security.test.ts +122 -0
- package/src/monitor.webhook.test-helpers.ts +44 -26
- package/src/outbound-runtime-api.ts +1 -0
- package/src/outbound.test.ts +616 -37
- package/src/outbound.ts +623 -81
- package/src/perm-schema.ts +1 -1
- package/src/perm.ts +1 -7
- package/src/pins.ts +108 -0
- package/src/policy.test.ts +297 -117
- package/src/policy.ts +142 -29
- package/src/post.ts +7 -6
- package/src/probe.test.ts +14 -9
- package/src/probe.ts +26 -16
- package/src/processing-claims.ts +59 -0
- package/src/qr-terminal.ts +1 -0
- package/src/reactions.ts +4 -34
- package/src/reasoning-preview.test.ts +59 -0
- package/src/reasoning-preview.ts +20 -0
- package/src/reply-dispatcher-runtime-api.ts +7 -0
- package/src/reply-dispatcher.test.ts +660 -29
- package/src/reply-dispatcher.ts +407 -154
- package/src/runtime.ts +6 -3
- package/src/secret-contract.ts +145 -0
- package/src/secret-input.ts +1 -13
- package/src/security-audit-shared.ts +69 -0
- package/src/security-audit.test.ts +61 -0
- package/src/security-audit.ts +1 -0
- package/src/send-result.ts +1 -1
- package/src/send-target.test.ts +9 -3
- package/src/send-target.ts +10 -4
- package/src/send.reply-fallback.test.ts +77 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +399 -86
- package/src/sequential-key.test.ts +72 -0
- package/src/sequential-key.ts +28 -0
- package/src/sequential-queue.test.ts +92 -0
- package/src/sequential-queue.ts +16 -0
- package/src/session-conversation.ts +42 -0
- package/src/session-route.ts +48 -0
- package/src/setup-core.ts +51 -0
- package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
- package/src/setup-surface.ts +581 -0
- package/src/streaming-card.test.ts +138 -2
- package/src/streaming-card.ts +134 -18
- package/src/subagent-hooks.test.ts +603 -0
- package/src/subagent-hooks.ts +397 -0
- package/src/targets.ts +3 -13
- package/src/test-support/lifecycle-test-support.ts +479 -0
- package/src/thread-bindings.test.ts +143 -0
- package/src/thread-bindings.ts +330 -0
- package/src/tool-account-routing.test.ts +66 -8
- package/src/tool-account.test.ts +44 -0
- package/src/tool-account.ts +40 -17
- package/src/tool-factory-test-harness.ts +11 -8
- package/src/tool-result.ts +3 -1
- package/src/tools-config.ts +1 -1
- package/src/types.ts +16 -15
- package/src/typing.ts +10 -6
- package/src/wiki-schema.ts +1 -1
- package/src/wiki.ts +1 -7
- package/subagent-hooks-api.ts +31 -0
- package/tsconfig.json +16 -0
- package/src/feishu-command-handler.ts +0 -59
- package/src/onboarding.status.test.ts +0 -25
- package/src/onboarding.ts +0 -489
- package/src/send-message.ts +0 -71
- package/src/targets.test.ts +0 -70
package/src/policy.ts
CHANGED
|
@@ -1,13 +1,39 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
} from "openclaw/plugin-sdk/
|
|
6
|
-
import { evaluateSenderGroupAccessForPolicy } from "openclaw/plugin-sdk/
|
|
7
|
-
import {
|
|
8
|
-
import type {
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
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(
|
|
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
|
|
72
|
-
const matchKey = Object.keys(groups).find(
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 = /([\\`*_{}
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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: "
|
|
135
|
-
url: "/open-apis/bot/
|
|
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
|
|
263
|
+
it("handles response with pingBotInfo in data", async () => {
|
|
259
264
|
setupClient({
|
|
260
265
|
code: 0,
|
|
261
|
-
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
|
|
21
|
+
type FeishuPingResponse = {
|
|
21
22
|
code: number;
|
|
22
23
|
msg?: string;
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
})
|
|
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
|
|
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:
|
|
136
|
-
botOpenId:
|
|
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:
|
|
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 "
|
|
2
|
-
import {
|
|
1
|
+
import type { ClawdbotConfig } from "../runtime-api.js";
|
|
2
|
+
import { resolveFeishuRuntimeAccount } from "./accounts.js";
|
|
3
3
|
import { createFeishuClient } from "./client.js";
|
|
4
4
|
|
|
5
|
-
|
|
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 =
|
|
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
|
+
}
|