@openclaw/feishu 2026.3.13 → 2026.5.2-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 +1827 -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 +1253 -309
- package/src/chat-schema.ts +5 -4
- package/src/chat.test.ts +135 -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 +406 -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 +33 -95
- package/src/directory.static.ts +61 -0
- package/src/directory.test.ts +116 -20
- package/src/directory.ts +60 -92
- 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 +403 -26
- package/src/media.ts +509 -132
- 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.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 +105 -2
- package/src/send.test.ts +386 -4
- package/src/send.ts +414 -95
- 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 +453 -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/runtime.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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>(
|
|
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
|
+
};
|
package/src/secret-input.ts
CHANGED
|
@@ -1,13 +1 @@
|
|
|
1
|
-
|
|
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";
|
package/src/send-result.ts
CHANGED
package/src/send-target.test.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
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",
|
package/src/send-target.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
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
|
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 =
|
|
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
|
-
|
|
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({
|
|
@@ -51,6 +57,34 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
|
51
57
|
});
|
|
52
58
|
});
|
|
53
59
|
|
|
60
|
+
it("preserves Feishu diagnostics when direct sends reject before response checks", async () => {
|
|
61
|
+
const apiError = Object.assign(new Error("Request failed with status code 400"), {
|
|
62
|
+
response: {
|
|
63
|
+
status: 400,
|
|
64
|
+
data: {
|
|
65
|
+
code: 9499,
|
|
66
|
+
msg: "Bad Request",
|
|
67
|
+
error: {
|
|
68
|
+
log_id: "202604291247104BEF4C42D2420A9AD569",
|
|
69
|
+
troubleshooter:
|
|
70
|
+
"https://open.feishu.cn/search?log_id=202604291247104BEF4C42D2420A9AD569",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
createMock.mockRejectedValue(apiError);
|
|
76
|
+
|
|
77
|
+
await expect(
|
|
78
|
+
sendMessageFeishu({
|
|
79
|
+
cfg: {} as never,
|
|
80
|
+
to: "user:ou_target",
|
|
81
|
+
text: "hello",
|
|
82
|
+
}),
|
|
83
|
+
).rejects.toThrow(
|
|
84
|
+
/Feishu send failed: .*"http_status":400.*"feishu_code":9499.*"feishu_msg":"Bad Request".*"feishu_log_id":"202604291247104BEF4C42D2420A9AD569".*"feishu_troubleshooter":"https:\/\/open\.feishu\.cn\/search\?log_id=202604291247104BEF4C42D2420A9AD569"/,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
54
88
|
it("falls back to create for withdrawn post replies", async () => {
|
|
55
89
|
replyMock.mockResolvedValue({
|
|
56
90
|
code: 230011,
|
|
@@ -171,6 +205,75 @@ describe("Feishu reply fallback for withdrawn/deleted targets", () => {
|
|
|
171
205
|
expect(createMock).not.toHaveBeenCalled();
|
|
172
206
|
});
|
|
173
207
|
|
|
208
|
+
it("fails thread replies instead of falling back to a top-level send", async () => {
|
|
209
|
+
replyMock.mockResolvedValue({
|
|
210
|
+
code: 230011,
|
|
211
|
+
msg: "The message was withdrawn.",
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await expect(
|
|
215
|
+
sendMessageFeishu({
|
|
216
|
+
cfg: {} as never,
|
|
217
|
+
to: "chat:oc_group_1",
|
|
218
|
+
text: "hello",
|
|
219
|
+
replyToMessageId: "om_parent",
|
|
220
|
+
replyInThread: true,
|
|
221
|
+
}),
|
|
222
|
+
).rejects.toThrow(
|
|
223
|
+
"Feishu thread reply failed: reply target is unavailable and cannot safely fall back to a top-level send.",
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
227
|
+
expect(replyMock).toHaveBeenCalledWith({
|
|
228
|
+
path: { message_id: "om_parent" },
|
|
229
|
+
data: expect.objectContaining({
|
|
230
|
+
reply_in_thread: true,
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("fails thrown withdrawn thread replies instead of falling back to create", async () => {
|
|
236
|
+
const sdkError = Object.assign(new Error("request failed"), { code: 230011 });
|
|
237
|
+
replyMock.mockRejectedValue(sdkError);
|
|
238
|
+
|
|
239
|
+
await expect(
|
|
240
|
+
sendMessageFeishu({
|
|
241
|
+
cfg: {} as never,
|
|
242
|
+
to: "chat:oc_group_1",
|
|
243
|
+
text: "hello",
|
|
244
|
+
replyToMessageId: "om_parent",
|
|
245
|
+
replyInThread: true,
|
|
246
|
+
}),
|
|
247
|
+
).rejects.toThrow(
|
|
248
|
+
"Feishu thread reply failed: reply target is unavailable and cannot safely fall back to a top-level send.",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(createMock).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("still falls back for non-thread replies to withdrawn targets", async () => {
|
|
255
|
+
replyMock.mockResolvedValue({
|
|
256
|
+
code: 230011,
|
|
257
|
+
msg: "The message was withdrawn.",
|
|
258
|
+
});
|
|
259
|
+
createMock.mockResolvedValue({
|
|
260
|
+
code: 0,
|
|
261
|
+
data: { message_id: "om_non_thread_fallback" },
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
await expectFallbackResult(
|
|
265
|
+
() =>
|
|
266
|
+
sendMessageFeishu({
|
|
267
|
+
cfg: {} as never,
|
|
268
|
+
to: "user:ou_target",
|
|
269
|
+
text: "hello",
|
|
270
|
+
replyToMessageId: "om_parent",
|
|
271
|
+
replyInThread: false,
|
|
272
|
+
}),
|
|
273
|
+
"om_non_thread_fallback",
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
174
277
|
it("re-throws non-withdrawn thrown errors for card messages", async () => {
|
|
175
278
|
const sdkError = Object.assign(new Error("permission denied"), { code: 99991401 });
|
|
176
279
|
replyMock.mockRejectedValue(sdkError);
|