@openclaw/feishu 2026.3.1 → 2026.3.7
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/index.ts +2 -2
- package/package.json +1 -1
- package/src/accounts.test.ts +268 -11
- package/src/accounts.ts +101 -14
- package/src/bitable.ts +40 -28
- package/src/bot.checkBotMentioned.test.ts +9 -1
- package/src/bot.stripBotMention.test.ts +118 -22
- package/src/bot.test.ts +945 -77
- package/src/bot.ts +492 -165
- package/src/card-action.ts +1 -1
- package/src/channel.test.ts +1 -1
- package/src/channel.ts +72 -68
- package/src/chat.test.ts +2 -2
- package/src/chat.ts +1 -1
- package/src/client.test.ts +221 -4
- package/src/client.ts +70 -5
- package/src/config-schema.test.ts +33 -6
- package/src/config-schema.ts +18 -10
- package/src/dedup.ts +47 -1
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +29 -50
- package/src/doc-schema.ts +16 -22
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +8 -11
- package/src/docx.account-selection.test.ts +10 -16
- package/src/docx.test.ts +41 -189
- package/src/docx.ts +1 -1
- package/src/drive.ts +13 -17
- package/src/dynamic-agent.ts +1 -1
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +164 -14
- package/src/media.ts +44 -10
- package/src/mention.ts +1 -1
- package/src/monitor.account.ts +284 -25
- package/src/monitor.reaction.test.ts +395 -46
- package/src/monitor.startup.test.ts +25 -8
- package/src/monitor.startup.ts +20 -7
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +88 -9
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +4 -1
- package/src/monitor.ts +4 -4
- package/src/monitor.webhook-security.test.ts +13 -11
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +213 -106
- package/src/outbound.test.ts +178 -0
- package/src/outbound.ts +39 -6
- package/src/perm.ts +11 -15
- package/src/policy.test.ts +40 -0
- package/src/policy.ts +9 -10
- package/src/probe.test.ts +54 -36
- package/src/probe.ts +57 -37
- package/src/reactions.ts +1 -1
- package/src/reply-dispatcher.test.ts +216 -0
- package/src/reply-dispatcher.ts +89 -22
- package/src/runtime.ts +1 -1
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +7 -3
- package/src/send.reply-fallback.test.ts +74 -0
- package/src/send.test.ts +1 -1
- package/src/send.ts +88 -49
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +96 -28
- package/src/targets.test.ts +29 -0
- package/src/targets.ts +25 -1
- package/src/tool-account-routing.test.ts +3 -3
- package/src/tool-account.ts +1 -1
- package/src/tool-factory-test-harness.ts +1 -1
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/types.ts +11 -4
- package/src/typing.ts +1 -1
- package/src/wiki.ts +15 -19
package/src/onboarding.ts
CHANGED
|
@@ -3,51 +3,49 @@ import type {
|
|
|
3
3
|
ChannelOnboardingDmPolicy,
|
|
4
4
|
ClawdbotConfig,
|
|
5
5
|
DmPolicy,
|
|
6
|
+
SecretInput,
|
|
6
7
|
WizardPrompter,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
8
|
-
import {
|
|
8
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
9
|
+
import {
|
|
10
|
+
buildSingleChannelSecretPromptState,
|
|
11
|
+
DEFAULT_ACCOUNT_ID,
|
|
12
|
+
formatDocsLink,
|
|
13
|
+
hasConfiguredSecretInput,
|
|
14
|
+
mergeAllowFromEntries,
|
|
15
|
+
promptSingleChannelSecretInput,
|
|
16
|
+
setTopLevelChannelAllowFrom,
|
|
17
|
+
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
18
|
+
setTopLevelChannelGroupPolicy,
|
|
19
|
+
splitOnboardingEntries,
|
|
20
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
9
21
|
import { resolveFeishuCredentials } from "./accounts.js";
|
|
10
22
|
import { probeFeishu } from "./probe.js";
|
|
11
23
|
import type { FeishuConfig } from "./types.js";
|
|
12
24
|
|
|
13
25
|
const channel = "feishu" as const;
|
|
14
26
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
...cfg,
|
|
22
|
-
channels: {
|
|
23
|
-
...cfg.channels,
|
|
24
|
-
feishu: {
|
|
25
|
-
...cfg.channels?.feishu,
|
|
26
|
-
dmPolicy,
|
|
27
|
-
...(allowFrom ? { allowFrom } : {}),
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
};
|
|
27
|
+
function normalizeString(value: unknown): string | undefined {
|
|
28
|
+
if (typeof value !== "string") {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const trimmed = value.trim();
|
|
32
|
+
return trimmed || undefined;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
function
|
|
34
|
-
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
...cfg.channels?.feishu,
|
|
40
|
-
allowFrom,
|
|
41
|
-
},
|
|
42
|
-
},
|
|
43
|
-
};
|
|
35
|
+
function setFeishuDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
|
|
36
|
+
return setTopLevelChannelDmPolicyWithAllowFrom({
|
|
37
|
+
cfg,
|
|
38
|
+
channel: "feishu",
|
|
39
|
+
dmPolicy,
|
|
40
|
+
}) as ClawdbotConfig;
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
function
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
|
|
44
|
+
return setTopLevelChannelAllowFrom({
|
|
45
|
+
cfg,
|
|
46
|
+
channel: "feishu",
|
|
47
|
+
allowFrom,
|
|
48
|
+
}) as ClawdbotConfig;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
async function promptFeishuAllowFrom(params: {
|
|
@@ -73,18 +71,13 @@ async function promptFeishuAllowFrom(params: {
|
|
|
73
71
|
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
74
72
|
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
75
73
|
});
|
|
76
|
-
const parts =
|
|
74
|
+
const parts = splitOnboardingEntries(String(entry));
|
|
77
75
|
if (parts.length === 0) {
|
|
78
76
|
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
|
79
77
|
continue;
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
const unique =
|
|
83
|
-
...new Set([
|
|
84
|
-
...existing.map((v: string | number) => String(v).trim()).filter(Boolean),
|
|
85
|
-
...parts,
|
|
86
|
-
]),
|
|
87
|
-
];
|
|
80
|
+
const unique = mergeAllowFromEntries(existing, parts);
|
|
88
81
|
return setFeishuAllowFrom(params.cfg, unique);
|
|
89
82
|
}
|
|
90
83
|
}
|
|
@@ -104,40 +97,30 @@ async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void>
|
|
|
104
97
|
);
|
|
105
98
|
}
|
|
106
99
|
|
|
107
|
-
async function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}> {
|
|
100
|
+
async function promptFeishuAppId(params: {
|
|
101
|
+
prompter: WizardPrompter;
|
|
102
|
+
initialValue?: string;
|
|
103
|
+
}): Promise<string> {
|
|
111
104
|
const appId = String(
|
|
112
|
-
await prompter.text({
|
|
105
|
+
await params.prompter.text({
|
|
113
106
|
message: "Enter Feishu App ID",
|
|
107
|
+
initialValue: params.initialValue,
|
|
114
108
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
115
109
|
}),
|
|
116
110
|
).trim();
|
|
117
|
-
|
|
118
|
-
await prompter.text({
|
|
119
|
-
message: "Enter Feishu App Secret",
|
|
120
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
121
|
-
}),
|
|
122
|
-
).trim();
|
|
123
|
-
return { appId, appSecret };
|
|
111
|
+
return appId;
|
|
124
112
|
}
|
|
125
113
|
|
|
126
114
|
function setFeishuGroupPolicy(
|
|
127
115
|
cfg: ClawdbotConfig,
|
|
128
116
|
groupPolicy: "open" | "allowlist" | "disabled",
|
|
129
117
|
): ClawdbotConfig {
|
|
130
|
-
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
enabled: true,
|
|
137
|
-
groupPolicy,
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
};
|
|
118
|
+
return setTopLevelChannelGroupPolicy({
|
|
119
|
+
cfg,
|
|
120
|
+
channel: "feishu",
|
|
121
|
+
groupPolicy,
|
|
122
|
+
enabled: true,
|
|
123
|
+
}) as ClawdbotConfig;
|
|
141
124
|
}
|
|
142
125
|
|
|
143
126
|
function setFeishuGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig {
|
|
@@ -167,13 +150,53 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
167
150
|
channel,
|
|
168
151
|
getStatus: async ({ cfg }) => {
|
|
169
152
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
170
|
-
|
|
153
|
+
|
|
154
|
+
const isAppIdConfigured = (value: unknown): boolean => {
|
|
155
|
+
const asString = normalizeString(value);
|
|
156
|
+
if (asString) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (!value || typeof value !== "object") {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const rec = value as Record<string, unknown>;
|
|
163
|
+
const source = normalizeString(rec.source)?.toLowerCase();
|
|
164
|
+
const id = normalizeString(rec.id);
|
|
165
|
+
if (source === "env" && id) {
|
|
166
|
+
return Boolean(normalizeString(process.env[id]));
|
|
167
|
+
}
|
|
168
|
+
return hasConfiguredSecretInput(value);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const topLevelConfigured = Boolean(
|
|
172
|
+
isAppIdConfigured(feishuCfg?.appId) && hasConfiguredSecretInput(feishuCfg?.appSecret),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const accountConfigured = Object.values(feishuCfg?.accounts ?? {}).some((account) => {
|
|
176
|
+
if (!account || typeof account !== "object") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const hasOwnAppId = Object.prototype.hasOwnProperty.call(account, "appId");
|
|
180
|
+
const hasOwnAppSecret = Object.prototype.hasOwnProperty.call(account, "appSecret");
|
|
181
|
+
const accountAppIdConfigured = hasOwnAppId
|
|
182
|
+
? isAppIdConfigured((account as Record<string, unknown>).appId)
|
|
183
|
+
: isAppIdConfigured(feishuCfg?.appId);
|
|
184
|
+
const accountSecretConfigured = hasOwnAppSecret
|
|
185
|
+
? hasConfiguredSecretInput((account as Record<string, unknown>).appSecret)
|
|
186
|
+
: hasConfiguredSecretInput(feishuCfg?.appSecret);
|
|
187
|
+
return Boolean(accountAppIdConfigured && accountSecretConfigured);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const configured = topLevelConfigured || accountConfigured;
|
|
191
|
+
const resolvedCredentials = resolveFeishuCredentials(feishuCfg, {
|
|
192
|
+
allowUnresolvedSecretRef: true,
|
|
193
|
+
});
|
|
171
194
|
|
|
172
195
|
// Try to probe if configured
|
|
173
196
|
let probeResult = null;
|
|
174
|
-
if (configured &&
|
|
197
|
+
if (configured && resolvedCredentials) {
|
|
175
198
|
try {
|
|
176
|
-
probeResult = await probeFeishu(
|
|
199
|
+
probeResult = await probeFeishu(resolvedCredentials);
|
|
177
200
|
} catch {
|
|
178
201
|
// Ignore probe errors
|
|
179
202
|
}
|
|
@@ -201,52 +224,59 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
201
224
|
|
|
202
225
|
configure: async ({ cfg, prompter }) => {
|
|
203
226
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
204
|
-
const resolved = resolveFeishuCredentials(feishuCfg
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
227
|
+
const resolved = resolveFeishuCredentials(feishuCfg, {
|
|
228
|
+
allowUnresolvedSecretRef: true,
|
|
229
|
+
});
|
|
230
|
+
const hasConfigSecret = hasConfiguredSecretInput(feishuCfg?.appSecret);
|
|
231
|
+
const hasConfigCreds = Boolean(
|
|
232
|
+
typeof feishuCfg?.appId === "string" && feishuCfg.appId.trim() && hasConfigSecret,
|
|
208
233
|
);
|
|
234
|
+
const appSecretPromptState = buildSingleChannelSecretPromptState({
|
|
235
|
+
accountConfigured: Boolean(resolved),
|
|
236
|
+
hasConfigToken: hasConfigSecret,
|
|
237
|
+
allowEnv: !hasConfigCreds && Boolean(process.env.FEISHU_APP_ID?.trim()),
|
|
238
|
+
envValue: process.env.FEISHU_APP_SECRET,
|
|
239
|
+
});
|
|
209
240
|
|
|
210
241
|
let next = cfg;
|
|
211
242
|
let appId: string | null = null;
|
|
212
|
-
let appSecret:
|
|
243
|
+
let appSecret: SecretInput | null = null;
|
|
244
|
+
let appSecretProbeValue: string | null = null;
|
|
213
245
|
|
|
214
246
|
if (!resolved) {
|
|
215
247
|
await noteFeishuCredentialHelp(prompter);
|
|
216
248
|
}
|
|
217
249
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
const appSecretResult = await promptSingleChannelSecretInput({
|
|
251
|
+
cfg: next,
|
|
252
|
+
prompter,
|
|
253
|
+
providerHint: "feishu",
|
|
254
|
+
credentialLabel: "App Secret",
|
|
255
|
+
accountConfigured: appSecretPromptState.accountConfigured,
|
|
256
|
+
canUseEnv: appSecretPromptState.canUseEnv,
|
|
257
|
+
hasConfigToken: appSecretPromptState.hasConfigToken,
|
|
258
|
+
envPrompt: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
259
|
+
keepPrompt: "Feishu App Secret already configured. Keep it?",
|
|
260
|
+
inputPrompt: "Enter Feishu App Secret",
|
|
261
|
+
preferredEnvVar: "FEISHU_APP_SECRET",
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
if (appSecretResult.action === "use-env") {
|
|
265
|
+
next = {
|
|
266
|
+
...next,
|
|
267
|
+
channels: {
|
|
268
|
+
...next.channels,
|
|
269
|
+
feishu: { ...next.channels?.feishu, enabled: true },
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
} else if (appSecretResult.action === "set") {
|
|
273
|
+
appSecret = appSecretResult.value;
|
|
274
|
+
appSecretProbeValue = appSecretResult.resolvedValue;
|
|
275
|
+
appId = await promptFeishuAppId({
|
|
276
|
+
prompter,
|
|
277
|
+
initialValue:
|
|
278
|
+
normalizeString(feishuCfg?.appId) ?? normalizeString(process.env.FEISHU_APP_ID),
|
|
240
279
|
});
|
|
241
|
-
if (!keep) {
|
|
242
|
-
const entered = await promptFeishuCredentials(prompter);
|
|
243
|
-
appId = entered.appId;
|
|
244
|
-
appSecret = entered.appSecret;
|
|
245
|
-
}
|
|
246
|
-
} else {
|
|
247
|
-
const entered = await promptFeishuCredentials(prompter);
|
|
248
|
-
appId = entered.appId;
|
|
249
|
-
appSecret = entered.appSecret;
|
|
250
280
|
}
|
|
251
281
|
|
|
252
282
|
if (appId && appSecret) {
|
|
@@ -264,9 +294,12 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
264
294
|
};
|
|
265
295
|
|
|
266
296
|
// Test connection
|
|
267
|
-
const testCfg = next.channels?.feishu as FeishuConfig;
|
|
268
297
|
try {
|
|
269
|
-
const probe = await probeFeishu(
|
|
298
|
+
const probe = await probeFeishu({
|
|
299
|
+
appId,
|
|
300
|
+
appSecret: appSecretProbeValue ?? undefined,
|
|
301
|
+
domain: (next.channels?.feishu as FeishuConfig | undefined)?.domain,
|
|
302
|
+
});
|
|
270
303
|
if (probe.ok) {
|
|
271
304
|
await prompter.note(
|
|
272
305
|
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
@@ -283,6 +316,80 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
283
316
|
}
|
|
284
317
|
}
|
|
285
318
|
|
|
319
|
+
const currentMode =
|
|
320
|
+
(next.channels?.feishu as FeishuConfig | undefined)?.connectionMode ?? "websocket";
|
|
321
|
+
const connectionMode = (await prompter.select({
|
|
322
|
+
message: "Feishu connection mode",
|
|
323
|
+
options: [
|
|
324
|
+
{ value: "websocket", label: "WebSocket (default)" },
|
|
325
|
+
{ value: "webhook", label: "Webhook" },
|
|
326
|
+
],
|
|
327
|
+
initialValue: currentMode,
|
|
328
|
+
})) as "websocket" | "webhook";
|
|
329
|
+
next = {
|
|
330
|
+
...next,
|
|
331
|
+
channels: {
|
|
332
|
+
...next.channels,
|
|
333
|
+
feishu: {
|
|
334
|
+
...next.channels?.feishu,
|
|
335
|
+
connectionMode,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (connectionMode === "webhook") {
|
|
341
|
+
const currentVerificationToken = (next.channels?.feishu as FeishuConfig | undefined)
|
|
342
|
+
?.verificationToken;
|
|
343
|
+
const verificationTokenPromptState = buildSingleChannelSecretPromptState({
|
|
344
|
+
accountConfigured: hasConfiguredSecretInput(currentVerificationToken),
|
|
345
|
+
hasConfigToken: hasConfiguredSecretInput(currentVerificationToken),
|
|
346
|
+
allowEnv: false,
|
|
347
|
+
});
|
|
348
|
+
const verificationTokenResult = await promptSingleChannelSecretInput({
|
|
349
|
+
cfg: next,
|
|
350
|
+
prompter,
|
|
351
|
+
providerHint: "feishu-webhook",
|
|
352
|
+
credentialLabel: "verification token",
|
|
353
|
+
accountConfigured: verificationTokenPromptState.accountConfigured,
|
|
354
|
+
canUseEnv: verificationTokenPromptState.canUseEnv,
|
|
355
|
+
hasConfigToken: verificationTokenPromptState.hasConfigToken,
|
|
356
|
+
envPrompt: "",
|
|
357
|
+
keepPrompt: "Feishu verification token already configured. Keep it?",
|
|
358
|
+
inputPrompt: "Enter Feishu verification token",
|
|
359
|
+
preferredEnvVar: "FEISHU_VERIFICATION_TOKEN",
|
|
360
|
+
});
|
|
361
|
+
if (verificationTokenResult.action === "set") {
|
|
362
|
+
next = {
|
|
363
|
+
...next,
|
|
364
|
+
channels: {
|
|
365
|
+
...next.channels,
|
|
366
|
+
feishu: {
|
|
367
|
+
...next.channels?.feishu,
|
|
368
|
+
verificationToken: verificationTokenResult.value,
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath;
|
|
374
|
+
const webhookPath = String(
|
|
375
|
+
await prompter.text({
|
|
376
|
+
message: "Feishu webhook path",
|
|
377
|
+
initialValue: currentWebhookPath ?? "/feishu/events",
|
|
378
|
+
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
379
|
+
}),
|
|
380
|
+
).trim();
|
|
381
|
+
next = {
|
|
382
|
+
...next,
|
|
383
|
+
channels: {
|
|
384
|
+
...next.channels,
|
|
385
|
+
feishu: {
|
|
386
|
+
...next.channels?.feishu,
|
|
387
|
+
webhookPath,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
286
393
|
// Domain selection
|
|
287
394
|
const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu";
|
|
288
395
|
const domain = await prompter.select({
|
|
@@ -329,7 +436,7 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
329
436
|
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
330
437
|
});
|
|
331
438
|
if (entry) {
|
|
332
|
-
const parts =
|
|
439
|
+
const parts = splitOnboardingEntries(String(entry));
|
|
333
440
|
if (parts.length > 0) {
|
|
334
441
|
next = setFeishuGroupAllowFrom(next, parts);
|
|
335
442
|
}
|
package/src/outbound.test.ts
CHANGED
|
@@ -136,6 +136,156 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
|
|
136
136
|
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
137
137
|
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "card_msg" }));
|
|
138
138
|
});
|
|
139
|
+
|
|
140
|
+
it("forwards replyToId as replyToMessageId on sendText", async () => {
|
|
141
|
+
await sendText({
|
|
142
|
+
cfg: {} as any,
|
|
143
|
+
to: "chat_1",
|
|
144
|
+
text: "hello",
|
|
145
|
+
replyToId: "om_reply_1",
|
|
146
|
+
accountId: "main",
|
|
147
|
+
} as any);
|
|
148
|
+
|
|
149
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
150
|
+
expect.objectContaining({
|
|
151
|
+
to: "chat_1",
|
|
152
|
+
text: "hello",
|
|
153
|
+
replyToMessageId: "om_reply_1",
|
|
154
|
+
accountId: "main",
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("falls back to threadId when replyToId is empty on sendText", async () => {
|
|
160
|
+
await sendText({
|
|
161
|
+
cfg: {} as any,
|
|
162
|
+
to: "chat_1",
|
|
163
|
+
text: "hello",
|
|
164
|
+
replyToId: " ",
|
|
165
|
+
threadId: "om_thread_2",
|
|
166
|
+
accountId: "main",
|
|
167
|
+
} as any);
|
|
168
|
+
|
|
169
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
170
|
+
expect.objectContaining({
|
|
171
|
+
to: "chat_1",
|
|
172
|
+
text: "hello",
|
|
173
|
+
replyToMessageId: "om_thread_2",
|
|
174
|
+
accountId: "main",
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("feishuOutbound.sendText replyToId forwarding", () => {
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
vi.clearAllMocks();
|
|
183
|
+
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
184
|
+
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
185
|
+
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("forwards replyToId as replyToMessageId to sendMessageFeishu", async () => {
|
|
189
|
+
await sendText({
|
|
190
|
+
cfg: {} as any,
|
|
191
|
+
to: "chat_1",
|
|
192
|
+
text: "hello",
|
|
193
|
+
replyToId: "om_reply_target",
|
|
194
|
+
accountId: "main",
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
198
|
+
expect.objectContaining({
|
|
199
|
+
to: "chat_1",
|
|
200
|
+
text: "hello",
|
|
201
|
+
replyToMessageId: "om_reply_target",
|
|
202
|
+
accountId: "main",
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("forwards replyToId to sendMarkdownCardFeishu when renderMode=card", async () => {
|
|
208
|
+
await sendText({
|
|
209
|
+
cfg: {
|
|
210
|
+
channels: {
|
|
211
|
+
feishu: {
|
|
212
|
+
renderMode: "card",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
} as any,
|
|
216
|
+
to: "chat_1",
|
|
217
|
+
text: "```code```",
|
|
218
|
+
replyToId: "om_reply_target",
|
|
219
|
+
accountId: "main",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(sendMarkdownCardFeishuMock).toHaveBeenCalledWith(
|
|
223
|
+
expect.objectContaining({
|
|
224
|
+
replyToMessageId: "om_reply_target",
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("does not pass replyToMessageId when replyToId is absent", async () => {
|
|
230
|
+
await sendText({
|
|
231
|
+
cfg: {} as any,
|
|
232
|
+
to: "chat_1",
|
|
233
|
+
text: "hello",
|
|
234
|
+
accountId: "main",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
to: "chat_1",
|
|
240
|
+
text: "hello",
|
|
241
|
+
accountId: "main",
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
expect(sendMessageFeishuMock.mock.calls[0][0].replyToMessageId).toBeUndefined();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("feishuOutbound.sendMedia replyToId forwarding", () => {
|
|
249
|
+
beforeEach(() => {
|
|
250
|
+
vi.clearAllMocks();
|
|
251
|
+
sendMessageFeishuMock.mockResolvedValue({ messageId: "text_msg" });
|
|
252
|
+
sendMarkdownCardFeishuMock.mockResolvedValue({ messageId: "card_msg" });
|
|
253
|
+
sendMediaFeishuMock.mockResolvedValue({ messageId: "media_msg" });
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("forwards replyToId to sendMediaFeishu", async () => {
|
|
257
|
+
await feishuOutbound.sendMedia?.({
|
|
258
|
+
cfg: {} as any,
|
|
259
|
+
to: "chat_1",
|
|
260
|
+
text: "",
|
|
261
|
+
mediaUrl: "https://example.com/image.png",
|
|
262
|
+
replyToId: "om_reply_target",
|
|
263
|
+
accountId: "main",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
267
|
+
expect.objectContaining({
|
|
268
|
+
replyToMessageId: "om_reply_target",
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("forwards replyToId to text caption send", async () => {
|
|
274
|
+
await feishuOutbound.sendMedia?.({
|
|
275
|
+
cfg: {} as any,
|
|
276
|
+
to: "chat_1",
|
|
277
|
+
text: "caption text",
|
|
278
|
+
mediaUrl: "https://example.com/image.png",
|
|
279
|
+
replyToId: "om_reply_target",
|
|
280
|
+
accountId: "main",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
284
|
+
expect.objectContaining({
|
|
285
|
+
replyToMessageId: "om_reply_target",
|
|
286
|
+
}),
|
|
287
|
+
);
|
|
288
|
+
});
|
|
139
289
|
});
|
|
140
290
|
|
|
141
291
|
describe("feishuOutbound.sendMedia renderMode", () => {
|
|
@@ -178,4 +328,32 @@ describe("feishuOutbound.sendMedia renderMode", () => {
|
|
|
178
328
|
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
|
179
329
|
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "media_msg" }));
|
|
180
330
|
});
|
|
331
|
+
|
|
332
|
+
it("uses threadId fallback as replyToMessageId on sendMedia", async () => {
|
|
333
|
+
await feishuOutbound.sendMedia?.({
|
|
334
|
+
cfg: {} as any,
|
|
335
|
+
to: "chat_1",
|
|
336
|
+
text: "caption",
|
|
337
|
+
mediaUrl: "https://example.com/image.png",
|
|
338
|
+
threadId: "om_thread_1",
|
|
339
|
+
accountId: "main",
|
|
340
|
+
} as any);
|
|
341
|
+
|
|
342
|
+
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
|
|
343
|
+
expect.objectContaining({
|
|
344
|
+
to: "chat_1",
|
|
345
|
+
mediaUrl: "https://example.com/image.png",
|
|
346
|
+
replyToMessageId: "om_thread_1",
|
|
347
|
+
accountId: "main",
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
|
351
|
+
expect.objectContaining({
|
|
352
|
+
to: "chat_1",
|
|
353
|
+
text: "caption",
|
|
354
|
+
replyToMessageId: "om_thread_1",
|
|
355
|
+
accountId: "main",
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
358
|
+
});
|
|
181
359
|
});
|