@m1heng-clawd/feishu 0.1.17 → 0.1.19
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/package.json +2 -2
- package/src/accounts.ts +28 -6
- package/src/bot.ts +18 -7
- package/src/channel.ts +13 -10
- package/src/config-schema.ts +20 -0
- package/src/media.ts +8 -1
- package/src/monitor.ts +2 -2
- package/src/outbound.ts +1 -1
- package/src/policy.ts +1 -1
- package/src/reply-dispatcher.ts +5 -3
- package/src/sdk-compat.ts +13 -0
- package/src/setup-wizard.ts +171 -0
- package/src/text/markdown-links.ts +16 -1
- package/src/onboarding.ts +0 -449
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m1heng-clawd/feishu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw Feishu/Lark channel plugin",
|
|
6
6
|
"scripts": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/node": "^25.0.10",
|
|
65
65
|
"@vitest/coverage-v8": "^2.1.8",
|
|
66
|
-
"openclaw": "2026.3.
|
|
66
|
+
"openclaw": "2026.3.24",
|
|
67
67
|
"tsx": "^4.21.0",
|
|
68
68
|
"typescript": "^5.7.0",
|
|
69
69
|
"vitest": "^2.1.8"
|
package/src/accounts.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "./sdk-compat.js";
|
|
3
3
|
import type { FeishuConfig, FeishuAccountConfig, FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -10,7 +10,11 @@ function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
|
|
10
10
|
if (!accounts || typeof accounts !== "object") {
|
|
11
11
|
return [];
|
|
12
12
|
}
|
|
13
|
-
return
|
|
13
|
+
return [...new Set(
|
|
14
|
+
Object.keys(accounts)
|
|
15
|
+
.filter((accountId) => accountId.trim())
|
|
16
|
+
.map((accountId) => normalizeAccountId(accountId)),
|
|
17
|
+
)];
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/**
|
|
@@ -38,17 +42,35 @@ export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
|
-
*
|
|
45
|
+
* Resolve the configured account key for a normalized account id.
|
|
46
|
+
* Preserves the original config key casing so write paths can avoid shadow entries.
|
|
42
47
|
*/
|
|
43
|
-
function
|
|
48
|
+
export function resolveConfiguredFeishuAccountKey(
|
|
44
49
|
cfg: ClawdbotConfig,
|
|
45
50
|
accountId: string,
|
|
46
|
-
):
|
|
51
|
+
): string | undefined {
|
|
47
52
|
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
|
|
48
53
|
if (!accounts || typeof accounts !== "object") {
|
|
49
54
|
return undefined;
|
|
50
55
|
}
|
|
51
|
-
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(accounts, accountId)) {
|
|
57
|
+
return accountId;
|
|
58
|
+
}
|
|
59
|
+
const normalizedId = normalizeAccountId(accountId);
|
|
60
|
+
return Object.keys(accounts).find((key) => normalizeAccountId(key) === normalizedId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the raw account-specific config.
|
|
65
|
+
* Preserves mixed-case config keys by resolving through the normalized account id.
|
|
66
|
+
*/
|
|
67
|
+
function resolveAccountConfig(
|
|
68
|
+
cfg: ClawdbotConfig,
|
|
69
|
+
accountId: string,
|
|
70
|
+
): FeishuAccountConfig | undefined {
|
|
71
|
+
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
|
|
72
|
+
const matchedKey = resolveConfiguredFeishuAccountKey(cfg, accountId);
|
|
73
|
+
return matchedKey && accounts ? accounts[matchedKey] : undefined;
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
/**
|
package/src/bot.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { ClawdbotConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { HistoryEntry } from "openclaw/plugin-sdk/feishu";
|
|
2
3
|
import {
|
|
3
4
|
buildPendingHistoryContextFromMap,
|
|
4
5
|
recordPendingHistoryEntryIfEnabled,
|
|
5
6
|
clearHistoryEntriesIfEnabled,
|
|
6
7
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
7
8
|
resolveMentionGatingWithBypass,
|
|
8
|
-
|
|
9
|
-
} from "openclaw/plugin-sdk";
|
|
9
|
+
} from "./sdk-compat.js";
|
|
10
10
|
import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
|
|
11
11
|
import { getFeishuRuntime } from "./runtime.js";
|
|
12
12
|
import { createFeishuClient } from "./client.js";
|
|
@@ -94,6 +94,7 @@ const senderNameCache = new Map<string, { name: string; expireAt: number }>();
|
|
|
94
94
|
// Key: appId or "default", Value: timestamp of last notification
|
|
95
95
|
const permissionErrorNotifiedAt = new Map<string, number>();
|
|
96
96
|
const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
97
|
+
const DEFAULT_MAX_MESSAGE_AGE_MS = 300_000; // 5 minutes
|
|
97
98
|
|
|
98
99
|
type SenderNameResult = {
|
|
99
100
|
name?: string;
|
|
@@ -918,13 +919,20 @@ export async function handleFeishuMessage(params: {
|
|
|
918
919
|
return;
|
|
919
920
|
}
|
|
920
921
|
|
|
921
|
-
let ctx = parseFeishuMessageEvent(event, botOpenId);
|
|
922
|
-
|
|
923
922
|
// Parse message create_time (Feishu uses millisecond epoch string).
|
|
924
923
|
const messageCreateTimeMs = event.message.create_time
|
|
925
924
|
? parseInt(event.message.create_time, 10)
|
|
926
925
|
: undefined;
|
|
927
926
|
|
|
927
|
+
// Discard stale messages (e.g. replayed after gateway restart).
|
|
928
|
+
const maxAgeMs = feishuCfg.maxMessageAgeMs ?? DEFAULT_MAX_MESSAGE_AGE_MS;
|
|
929
|
+
if (messageCreateTimeMs && Date.now() - messageCreateTimeMs > maxAgeMs) {
|
|
930
|
+
log(`feishu[${account.accountId}]: discarding stale message ${messageId} (age: ${Math.round((Date.now() - messageCreateTimeMs) / 1000)}s, max: ${maxAgeMs / 1000}s)`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
let ctx = parseFeishuMessageEvent(event, botOpenId);
|
|
935
|
+
|
|
928
936
|
const messageType = event.message.message_type;
|
|
929
937
|
const isForwardedInbound = isForwardedMessageType(messageType);
|
|
930
938
|
const forwardedDispatchKey = getForwardedKey({
|
|
@@ -1304,9 +1312,12 @@ export async function handleFeishuMessage(params: {
|
|
|
1304
1312
|
}
|
|
1305
1313
|
}
|
|
1306
1314
|
|
|
1307
|
-
// In group chats,
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1315
|
+
// In group chats, From must be group-scoped so the core's resolveGroupSessionKey can
|
|
1316
|
+
// detect group context and route replies back to the group — not to a user's DM session.
|
|
1317
|
+
// We include senderOpenId to preserve per-speaker identity within the group.
|
|
1318
|
+
const feishuFrom = isGroup
|
|
1319
|
+
? `feishu:${ctx.chatId}:${ctx.senderOpenId}`
|
|
1320
|
+
: `feishu:${ctx.senderOpenId}`;
|
|
1310
1321
|
const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
|
|
1311
1322
|
|
|
1312
1323
|
// Resolve peer ID for session routing
|
package/src/channel.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, PAIRING_APPROVED_MESSAGE } from "
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, PAIRING_APPROVED_MESSAGE } from "./sdk-compat.js";
|
|
3
|
+
import { feishuSetupWizardWithAccounts } from "./setup-wizard.js";
|
|
3
4
|
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
|
4
5
|
import {
|
|
6
|
+
resolveConfiguredFeishuAccountKey,
|
|
5
7
|
resolveFeishuAccount,
|
|
6
8
|
resolveFeishuCredentials,
|
|
7
9
|
listFeishuAccountIds,
|
|
8
10
|
resolveDefaultFeishuAccountId,
|
|
9
11
|
} from "./accounts.js";
|
|
12
|
+
import { monitorFeishuProvider } from "./monitor.js";
|
|
10
13
|
import { feishuOutbound } from "./outbound.js";
|
|
11
14
|
import { probeFeishu } from "./probe.js";
|
|
12
15
|
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
|
@@ -18,7 +21,6 @@ import {
|
|
|
18
21
|
listFeishuDirectoryPeersLive,
|
|
19
22
|
listFeishuDirectoryGroupsLive,
|
|
20
23
|
} from "./directory.js";
|
|
21
|
-
import { feishuOnboardingAdapter } from "./onboarding.js";
|
|
22
24
|
|
|
23
25
|
const meta = {
|
|
24
26
|
id: "feishu",
|
|
@@ -129,7 +131,6 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
129
131
|
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
|
130
132
|
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
|
131
133
|
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
132
|
-
const account = resolveFeishuAccount({ cfg, accountId });
|
|
133
134
|
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
134
135
|
|
|
135
136
|
if (isDefault) {
|
|
@@ -148,6 +149,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
148
149
|
|
|
149
150
|
// For named accounts, set enabled in accounts[accountId]
|
|
150
151
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
152
|
+
const accountKey = resolveConfiguredFeishuAccountKey(cfg, accountId) ?? accountId;
|
|
151
153
|
return {
|
|
152
154
|
...cfg,
|
|
153
155
|
channels: {
|
|
@@ -156,8 +158,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
156
158
|
...feishuCfg,
|
|
157
159
|
accounts: {
|
|
158
160
|
...feishuCfg?.accounts,
|
|
159
|
-
[
|
|
160
|
-
...feishuCfg?.accounts?.[
|
|
161
|
+
[accountKey]: {
|
|
162
|
+
...feishuCfg?.accounts?.[accountKey],
|
|
161
163
|
enabled,
|
|
162
164
|
},
|
|
163
165
|
},
|
|
@@ -184,7 +186,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
184
186
|
// Delete specific account from accounts
|
|
185
187
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
186
188
|
const accounts = { ...feishuCfg?.accounts };
|
|
187
|
-
|
|
189
|
+
const accountKey = resolveConfiguredFeishuAccountKey(cfg, accountId) ?? accountId;
|
|
190
|
+
delete accounts[accountKey];
|
|
188
191
|
|
|
189
192
|
return {
|
|
190
193
|
...cfg,
|
|
@@ -247,6 +250,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
253
|
+
const accountKey = resolveConfiguredFeishuAccountKey(cfg, accountId) ?? accountId;
|
|
250
254
|
return {
|
|
251
255
|
...cfg,
|
|
252
256
|
channels: {
|
|
@@ -255,8 +259,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
255
259
|
...feishuCfg,
|
|
256
260
|
accounts: {
|
|
257
261
|
...feishuCfg?.accounts,
|
|
258
|
-
[
|
|
259
|
-
...feishuCfg?.accounts?.[
|
|
262
|
+
[accountKey]: {
|
|
263
|
+
...feishuCfg?.accounts?.[accountKey],
|
|
260
264
|
enabled: true,
|
|
261
265
|
},
|
|
262
266
|
},
|
|
@@ -265,7 +269,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
265
269
|
};
|
|
266
270
|
},
|
|
267
271
|
},
|
|
268
|
-
|
|
272
|
+
setupWizard: feishuSetupWizardWithAccounts,
|
|
269
273
|
messaging: {
|
|
270
274
|
normalizeTarget: normalizeFeishuTarget,
|
|
271
275
|
targetResolver: {
|
|
@@ -324,7 +328,6 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
324
328
|
},
|
|
325
329
|
gateway: {
|
|
326
330
|
startAccount: async (ctx) => {
|
|
327
|
-
const { monitorFeishuProvider } = await import("./monitor.js");
|
|
328
331
|
const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
329
332
|
const port = account.config?.webhookPort ?? null;
|
|
330
333
|
ctx.setStatus({ accountId: ctx.accountId, port });
|
package/src/config-schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeAccountId } from "./sdk-compat.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
export { z };
|
|
3
4
|
|
|
@@ -157,6 +158,7 @@ export const FeishuAccountConfigSchema = z
|
|
|
157
158
|
mediaMaxMb: z.number().positive().optional(),
|
|
158
159
|
mediaLocalRoots: z.array(z.string()).optional(),
|
|
159
160
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
161
|
+
maxMessageAgeMs: z.number().positive().optional(),
|
|
160
162
|
renderMode: RenderModeSchema,
|
|
161
163
|
streaming: StreamingModeSchema,
|
|
162
164
|
tools: FeishuToolsConfigSchema,
|
|
@@ -196,6 +198,7 @@ export const FeishuConfigSchema = z
|
|
|
196
198
|
mediaMaxMb: z.number().positive().optional(),
|
|
197
199
|
mediaLocalRoots: z.array(z.string()).optional(),
|
|
198
200
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
201
|
+
maxMessageAgeMs: z.number().positive().optional(),
|
|
199
202
|
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
|
200
203
|
streaming: StreamingModeSchema,
|
|
201
204
|
tools: FeishuToolsConfigSchema,
|
|
@@ -206,6 +209,23 @@ export const FeishuConfigSchema = z
|
|
|
206
209
|
})
|
|
207
210
|
.strict()
|
|
208
211
|
.superRefine((value, ctx) => {
|
|
212
|
+
const normalizedAccountIds = new Map<string, string>();
|
|
213
|
+
for (const accountId of Object.keys(value.accounts ?? {})) {
|
|
214
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
215
|
+
const previousAccountId = normalizedAccountIds.get(normalizedAccountId);
|
|
216
|
+
if (previousAccountId && previousAccountId !== accountId) {
|
|
217
|
+
ctx.addIssue({
|
|
218
|
+
code: z.ZodIssueCode.custom,
|
|
219
|
+
path: ["accounts", accountId],
|
|
220
|
+
message:
|
|
221
|
+
`channels.feishu.accounts contains duplicate account ids after normalization: ` +
|
|
222
|
+
`"${previousAccountId}" and "${accountId}" both normalize to "${normalizedAccountId}"`,
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
normalizedAccountIds.set(normalizedAccountId, accountId);
|
|
227
|
+
}
|
|
228
|
+
|
|
209
229
|
if (value.dmPolicy === "open") {
|
|
210
230
|
const allowFrom = value.allowFrom ?? [];
|
|
211
231
|
const hasWildcard = allowFrom.some((entry) => String(entry).trim() === "*");
|
package/src/media.ts
CHANGED
|
@@ -514,7 +514,14 @@ export async function sendMediaFeishu(params: {
|
|
|
514
514
|
...(mergedLocalRoots.length > 0 ? { localRoots: mergedLocalRoots } : {}),
|
|
515
515
|
});
|
|
516
516
|
buffer = loaded.buffer;
|
|
517
|
-
|
|
517
|
+
const loadedFileName = loaded.fileName ?? "file";
|
|
518
|
+
let decoded: string;
|
|
519
|
+
try {
|
|
520
|
+
decoded = decodeURIComponent(loadedFileName);
|
|
521
|
+
} catch {
|
|
522
|
+
decoded = loadedFileName;
|
|
523
|
+
}
|
|
524
|
+
name = fileName ?? decoded;
|
|
518
525
|
contentType = loaded.contentType;
|
|
519
526
|
} else {
|
|
520
527
|
throw new Error("Either mediaUrl or mediaBuffer must be provided");
|
package/src/monitor.ts
CHANGED
|
@@ -3,9 +3,9 @@ import * as http from "http";
|
|
|
3
3
|
import {
|
|
4
4
|
type ClawdbotConfig,
|
|
5
5
|
type RuntimeEnv,
|
|
6
|
-
type HistoryEntry,
|
|
7
|
-
installRequestBodyLimitGuard,
|
|
8
6
|
} from "openclaw/plugin-sdk";
|
|
7
|
+
import type { HistoryEntry } from "openclaw/plugin-sdk/feishu";
|
|
8
|
+
import { installRequestBodyLimitGuard } from "./sdk-compat.js";
|
|
9
9
|
import type { ResolvedFeishuAccount } from "./types.js";
|
|
10
10
|
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
|
|
11
11
|
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
|
package/src/outbound.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import { getFeishuRuntime } from "./runtime.js";
|
|
3
3
|
import { sendMessageFeishu } from "./send.js";
|
|
4
4
|
import { sendMediaFeishu } from "./media.js";
|
package/src/policy.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
|
|
1
|
+
import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk/feishu";
|
|
2
2
|
import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export type FeishuGroupCommandMentionBypass = "never" | "single_bot" | "always";
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createReplyPrefixContext,
|
|
3
|
-
createTypingCallbacks,
|
|
4
|
-
logTypingFailure,
|
|
5
2
|
type ClawdbotConfig,
|
|
6
3
|
type ReplyPayload,
|
|
7
4
|
type RuntimeEnv,
|
|
8
5
|
} from "openclaw/plugin-sdk";
|
|
6
|
+
import {
|
|
7
|
+
createReplyPrefixContext,
|
|
8
|
+
createTypingCallbacks,
|
|
9
|
+
logTypingFailure,
|
|
10
|
+
} from "./sdk-compat.js";
|
|
9
11
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
10
12
|
import { createFeishuClient } from "./client.js";
|
|
11
13
|
import { buildMentionedCardContent, type MentionTarget } from "./mention.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
export {
|
|
3
|
+
PAIRING_APPROVED_MESSAGE,
|
|
4
|
+
buildPendingHistoryContextFromMap,
|
|
5
|
+
clearHistoryEntriesIfEnabled,
|
|
6
|
+
createReplyPrefixContext,
|
|
7
|
+
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
8
|
+
installRequestBodyLimitGuard,
|
|
9
|
+
logTypingFailure,
|
|
10
|
+
recordPendingHistoryEntryIfEnabled,
|
|
11
|
+
} from "openclaw/plugin-sdk/feishu";
|
|
12
|
+
export { resolveMentionGatingWithBypass } from "openclaw/plugin-sdk/channel-inbound";
|
|
13
|
+
export { createTypingCallbacks, promptAccountId } from "openclaw/plugin-sdk/matrix";
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import type { ClawdbotConfig, ChannelPlugin } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { ChannelSetupWizard } from "openclaw/plugin-sdk/setup";
|
|
3
|
+
import { feishuSetupWizard } from "openclaw/plugin-sdk/feishu";
|
|
4
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, promptAccountId } from "./sdk-compat.js";
|
|
5
|
+
import {
|
|
6
|
+
listFeishuAccountIds,
|
|
7
|
+
resolveConfiguredFeishuAccountKey,
|
|
8
|
+
resolveDefaultFeishuAccountId,
|
|
9
|
+
resolveFeishuAccount,
|
|
10
|
+
resolveFeishuCredentials,
|
|
11
|
+
} from "./accounts.js";
|
|
12
|
+
import { probeFeishu } from "./probe.js";
|
|
13
|
+
import type { FeishuConfig } from "./types.js";
|
|
14
|
+
|
|
15
|
+
function patchFeishuAccountConfig(
|
|
16
|
+
cfg: ClawdbotConfig,
|
|
17
|
+
accountId: string,
|
|
18
|
+
patch: Record<string, unknown>,
|
|
19
|
+
): ClawdbotConfig {
|
|
20
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
21
|
+
|
|
22
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
23
|
+
return {
|
|
24
|
+
...cfg,
|
|
25
|
+
channels: {
|
|
26
|
+
...cfg.channels,
|
|
27
|
+
feishu: { ...feishuCfg, ...patch },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const accountKey = resolveConfiguredFeishuAccountKey(cfg, accountId) ?? accountId;
|
|
33
|
+
return {
|
|
34
|
+
...cfg,
|
|
35
|
+
channels: {
|
|
36
|
+
...cfg.channels,
|
|
37
|
+
feishu: {
|
|
38
|
+
...feishuCfg,
|
|
39
|
+
accounts: {
|
|
40
|
+
...feishuCfg?.accounts,
|
|
41
|
+
[accountKey]: { ...feishuCfg?.accounts?.[accountKey], ...patch },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const accountScopedFinalize: NonNullable<ChannelSetupWizard["finalize"]> = async ({
|
|
49
|
+
cfg,
|
|
50
|
+
accountId,
|
|
51
|
+
prompter,
|
|
52
|
+
}) => {
|
|
53
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
54
|
+
const resolved = resolveFeishuCredentials(account.config);
|
|
55
|
+
const hasConfigCreds = Boolean(account.config?.appId?.trim() && account.config?.appSecret?.trim());
|
|
56
|
+
const accountLabel = accountId === DEFAULT_ACCOUNT_ID ? "default" : accountId;
|
|
57
|
+
|
|
58
|
+
let next = cfg;
|
|
59
|
+
let appId: string | null = null;
|
|
60
|
+
let appSecret: string | null = null;
|
|
61
|
+
|
|
62
|
+
if (!resolved) {
|
|
63
|
+
await prompter.note(
|
|
64
|
+
[
|
|
65
|
+
"1) Go to Feishu Open Platform (open.feishu.cn)",
|
|
66
|
+
"2) Create a self-built app",
|
|
67
|
+
"3) Get App ID and App Secret from Credentials page",
|
|
68
|
+
"4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
|
|
69
|
+
"5) Publish the app or add it to a test group",
|
|
70
|
+
].join("\n"),
|
|
71
|
+
`Feishu credentials (${accountLabel})`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (hasConfigCreds) {
|
|
76
|
+
const keep = await prompter.confirm({
|
|
77
|
+
message: `Feishu credentials already configured for "${accountLabel}". Keep them?`,
|
|
78
|
+
initialValue: true,
|
|
79
|
+
});
|
|
80
|
+
if (!keep) {
|
|
81
|
+
appId = String(await prompter.text({
|
|
82
|
+
message: `Enter Feishu App ID for "${accountLabel}"`,
|
|
83
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
84
|
+
})).trim();
|
|
85
|
+
appSecret = String(await prompter.text({
|
|
86
|
+
message: `Enter Feishu App Secret for "${accountLabel}"`,
|
|
87
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
88
|
+
})).trim();
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
appId = String(await prompter.text({
|
|
92
|
+
message: `Enter Feishu App ID for "${accountLabel}"`,
|
|
93
|
+
initialValue: account.config?.appId?.trim() || undefined,
|
|
94
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
95
|
+
})).trim();
|
|
96
|
+
appSecret = String(await prompter.text({
|
|
97
|
+
message: `Enter Feishu App Secret for "${accountLabel}"`,
|
|
98
|
+
validate: (v) => (v?.trim() ? undefined : "Required"),
|
|
99
|
+
})).trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (appId && appSecret) {
|
|
103
|
+
next = patchFeishuAccountConfig(next, accountId, { enabled: true, appId, appSecret });
|
|
104
|
+
try {
|
|
105
|
+
const testAccount = resolveFeishuAccount({ cfg: next, accountId });
|
|
106
|
+
const probe = await probeFeishu(testAccount);
|
|
107
|
+
if (probe.ok) {
|
|
108
|
+
await prompter.note(
|
|
109
|
+
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
110
|
+
`Feishu connection test (${accountLabel})`,
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
await prompter.note(
|
|
114
|
+
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
115
|
+
`Feishu connection test (${accountLabel})`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
await prompter.note(`Connection test failed: ${String(err)}`, `Feishu connection test (${accountLabel})`);
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
next = patchFeishuAccountConfig(next, accountId, { enabled: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const currentDomain = resolveFeishuAccount({ cfg: next, accountId }).config?.domain ?? "feishu";
|
|
126
|
+
const domain = await prompter.select({
|
|
127
|
+
message: `Which Feishu domain? (${accountLabel})`,
|
|
128
|
+
options: [
|
|
129
|
+
{ value: "feishu", label: "Feishu (feishu.cn) - China" },
|
|
130
|
+
{ value: "lark", label: "Lark (larksuite.com) - International" },
|
|
131
|
+
],
|
|
132
|
+
initialValue: currentDomain,
|
|
133
|
+
});
|
|
134
|
+
if (domain) {
|
|
135
|
+
next = patchFeishuAccountConfig(next, accountId, { domain });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { cfg: next };
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const feishuSetupWizardWithAccounts: NonNullable<ChannelPlugin["setupWizard"]> = {
|
|
142
|
+
...feishuSetupWizard,
|
|
143
|
+
|
|
144
|
+
resolveShouldPromptAccountIds: ({ cfg }) => listFeishuAccountIds(cfg).length > 1,
|
|
145
|
+
|
|
146
|
+
resolveAccountIdForConfigure: async ({
|
|
147
|
+
cfg,
|
|
148
|
+
prompter,
|
|
149
|
+
accountOverride,
|
|
150
|
+
shouldPromptAccountIds,
|
|
151
|
+
}) => {
|
|
152
|
+
if (accountOverride?.trim()) return normalizeAccountId(accountOverride);
|
|
153
|
+
const defaultId = resolveDefaultFeishuAccountId(cfg);
|
|
154
|
+
if (!shouldPromptAccountIds) return defaultId;
|
|
155
|
+
return promptAccountId({
|
|
156
|
+
cfg,
|
|
157
|
+
prompter,
|
|
158
|
+
label: "Feishu",
|
|
159
|
+
currentId: defaultId,
|
|
160
|
+
listAccountIds: listFeishuAccountIds,
|
|
161
|
+
defaultAccountId: defaultId,
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
finalize: async (params) => {
|
|
166
|
+
if (params.accountId === DEFAULT_ACCOUNT_ID) {
|
|
167
|
+
return feishuSetupWizard.finalize!(params);
|
|
168
|
+
}
|
|
169
|
+
return accountScopedFinalize(params);
|
|
170
|
+
},
|
|
171
|
+
};
|
|
@@ -91,8 +91,23 @@ function normalizeNonCodeSegments(text: string): string {
|
|
|
91
91
|
.join("");
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function normalizeCodeBlockFences(block: string): string {
|
|
95
|
+
const lines = block.split("\n");
|
|
96
|
+
return lines
|
|
97
|
+
.map((line) => {
|
|
98
|
+
// Fix opening and closing fences: remove leading whitespace
|
|
99
|
+
if (line.match(/^\s*```/)) {
|
|
100
|
+
return line.replace(/^\s+/, "");
|
|
101
|
+
}
|
|
102
|
+
return line;
|
|
103
|
+
})
|
|
104
|
+
.join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
export function normalizeFeishuMarkdownLinks(text: string): string {
|
|
95
|
-
if (!text
|
|
108
|
+
if (!text) return text;
|
|
109
|
+
text = normalizeCodeBlockFences(text);
|
|
110
|
+
if (!text.includes("http://") && !text.includes("https://")) {
|
|
96
111
|
return text;
|
|
97
112
|
}
|
|
98
113
|
|
package/src/onboarding.ts
DELETED
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ChannelOnboardingAdapter,
|
|
3
|
-
ChannelOnboardingDmPolicy,
|
|
4
|
-
ClawdbotConfig,
|
|
5
|
-
DmPolicy,
|
|
6
|
-
WizardPrompter,
|
|
7
|
-
} from "openclaw/plugin-sdk";
|
|
8
|
-
import {
|
|
9
|
-
addWildcardAllowFrom,
|
|
10
|
-
DEFAULT_ACCOUNT_ID,
|
|
11
|
-
formatDocsLink,
|
|
12
|
-
normalizeAccountId,
|
|
13
|
-
promptAccountId,
|
|
14
|
-
} from "openclaw/plugin-sdk";
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
listFeishuAccountIds,
|
|
18
|
-
resolveDefaultFeishuAccountId,
|
|
19
|
-
resolveFeishuAccount,
|
|
20
|
-
resolveFeishuCredentials,
|
|
21
|
-
} from "./accounts.js";
|
|
22
|
-
import { probeFeishu } from "./probe.js";
|
|
23
|
-
import type { FeishuConfig } from "./types.js";
|
|
24
|
-
|
|
25
|
-
const channel = "feishu" as const;
|
|
26
|
-
let onboardingDmPolicyAccountId: string | undefined;
|
|
27
|
-
|
|
28
|
-
function resolveOnboardingAccountId(cfg: ClawdbotConfig, accountId?: string | null): string {
|
|
29
|
-
const raw = accountId?.trim();
|
|
30
|
-
if (raw) {
|
|
31
|
-
return normalizeAccountId(raw);
|
|
32
|
-
}
|
|
33
|
-
return resolveDefaultFeishuAccountId(cfg);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function resolveDmPolicyAccountId(cfg: ClawdbotConfig, accountId?: string | null): string {
|
|
37
|
-
return resolveOnboardingAccountId(cfg, accountId ?? onboardingDmPolicyAccountId);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function upsertFeishuAccountConfig(
|
|
41
|
-
cfg: ClawdbotConfig,
|
|
42
|
-
accountId: string,
|
|
43
|
-
patch: Partial<FeishuConfig>,
|
|
44
|
-
): ClawdbotConfig {
|
|
45
|
-
const normalizedAccountId = normalizeAccountId(accountId);
|
|
46
|
-
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
47
|
-
|
|
48
|
-
if (normalizedAccountId === DEFAULT_ACCOUNT_ID) {
|
|
49
|
-
return {
|
|
50
|
-
...cfg,
|
|
51
|
-
channels: {
|
|
52
|
-
...cfg.channels,
|
|
53
|
-
feishu: {
|
|
54
|
-
...feishuCfg,
|
|
55
|
-
...patch,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const existingAccount = feishuCfg?.accounts?.[normalizedAccountId];
|
|
62
|
-
return {
|
|
63
|
-
...cfg,
|
|
64
|
-
channels: {
|
|
65
|
-
...cfg.channels,
|
|
66
|
-
feishu: {
|
|
67
|
-
...feishuCfg,
|
|
68
|
-
enabled: true,
|
|
69
|
-
accounts: {
|
|
70
|
-
...feishuCfg?.accounts,
|
|
71
|
-
[normalizedAccountId]: {
|
|
72
|
-
...existingAccount,
|
|
73
|
-
enabled: existingAccount?.enabled ?? true,
|
|
74
|
-
...patch,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function setFeishuDmPolicy(
|
|
83
|
-
cfg: ClawdbotConfig,
|
|
84
|
-
dmPolicy: DmPolicy,
|
|
85
|
-
accountId?: string,
|
|
86
|
-
): ClawdbotConfig {
|
|
87
|
-
const resolvedAccountId = resolveDmPolicyAccountId(cfg, accountId);
|
|
88
|
-
const account = resolveFeishuAccount({ cfg, accountId: resolvedAccountId });
|
|
89
|
-
// Feishu channel config does not support "disabled" as a dmPolicy value.
|
|
90
|
-
const effectiveDmPolicy = dmPolicy === "disabled" ? "pairing" : dmPolicy;
|
|
91
|
-
const allowFrom =
|
|
92
|
-
effectiveDmPolicy === "open"
|
|
93
|
-
? addWildcardAllowFrom(account.config.allowFrom)?.map((entry) => String(entry))
|
|
94
|
-
: undefined;
|
|
95
|
-
return upsertFeishuAccountConfig(cfg, resolvedAccountId, {
|
|
96
|
-
dmPolicy: effectiveDmPolicy,
|
|
97
|
-
...(allowFrom ? { allowFrom } : {}),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[], accountId?: string): ClawdbotConfig {
|
|
102
|
-
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
103
|
-
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { allowFrom });
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function parseAllowFromInput(raw: string): string[] {
|
|
107
|
-
return raw
|
|
108
|
-
.split(/[\n,;]+/g)
|
|
109
|
-
.map((entry) => entry.trim())
|
|
110
|
-
.filter(Boolean);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function promptFeishuAllowFrom(params: {
|
|
114
|
-
cfg: ClawdbotConfig;
|
|
115
|
-
prompter: WizardPrompter;
|
|
116
|
-
accountId?: string;
|
|
117
|
-
}): Promise<ClawdbotConfig> {
|
|
118
|
-
const accountId = resolveDmPolicyAccountId(params.cfg, params.accountId);
|
|
119
|
-
const existing = resolveFeishuAccount({ cfg: params.cfg, accountId }).config.allowFrom ?? [];
|
|
120
|
-
const accountLabel = accountId === DEFAULT_ACCOUNT_ID ? "default" : accountId;
|
|
121
|
-
await params.prompter.note(
|
|
122
|
-
[
|
|
123
|
-
`Account: ${accountLabel}`,
|
|
124
|
-
"Allowlist Feishu DMs by open_id or user_id.",
|
|
125
|
-
"You can find user open_id in Feishu admin console or via API.",
|
|
126
|
-
"Examples:",
|
|
127
|
-
"- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
128
|
-
"- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
129
|
-
].join("\n"),
|
|
130
|
-
"Feishu allowlist",
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
while (true) {
|
|
134
|
-
const entry = await params.prompter.text({
|
|
135
|
-
message: "Feishu allowFrom (user open_ids)",
|
|
136
|
-
placeholder: "ou_xxxxx, ou_yyyyy",
|
|
137
|
-
initialValue: existing[0] ? String(existing[0]) : undefined,
|
|
138
|
-
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
|
139
|
-
});
|
|
140
|
-
const parts = parseAllowFromInput(String(entry));
|
|
141
|
-
if (parts.length === 0) {
|
|
142
|
-
await params.prompter.note("Enter at least one user.", "Feishu allowlist");
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const unique = [
|
|
147
|
-
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts]),
|
|
148
|
-
];
|
|
149
|
-
return setFeishuAllowFrom(params.cfg, unique, accountId);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void> {
|
|
154
|
-
await prompter.note(
|
|
155
|
-
[
|
|
156
|
-
"1) Go to Feishu Open Platform (open.feishu.cn)",
|
|
157
|
-
"2) Create a self-built app",
|
|
158
|
-
"3) Get App ID and App Secret from Credentials page",
|
|
159
|
-
"4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
|
|
160
|
-
"5) Publish the app or add it to a test group",
|
|
161
|
-
"Tip: you can also set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.",
|
|
162
|
-
`Docs: ${formatDocsLink("/channels/feishu", "feishu")}`,
|
|
163
|
-
].join("\n"),
|
|
164
|
-
"Feishu credentials",
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function setFeishuGroupPolicy(
|
|
169
|
-
cfg: ClawdbotConfig,
|
|
170
|
-
groupPolicy: "open" | "allowlist" | "disabled",
|
|
171
|
-
accountId?: string,
|
|
172
|
-
): ClawdbotConfig {
|
|
173
|
-
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
174
|
-
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { enabled: true, groupPolicy });
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function setFeishuGroupAllowFrom(
|
|
178
|
-
cfg: ClawdbotConfig,
|
|
179
|
-
groupAllowFrom: string[],
|
|
180
|
-
accountId?: string,
|
|
181
|
-
): ClawdbotConfig {
|
|
182
|
-
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
183
|
-
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { groupAllowFrom });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function setFeishuDomain(cfg: ClawdbotConfig, domain: "feishu" | "lark", accountId?: string): ClawdbotConfig {
|
|
187
|
-
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
188
|
-
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { domain });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
192
|
-
label: "Feishu",
|
|
193
|
-
channel,
|
|
194
|
-
policyKey: "channels.feishu.dmPolicy",
|
|
195
|
-
allowFromKey: "channels.feishu.allowFrom",
|
|
196
|
-
getCurrent: (cfg) => {
|
|
197
|
-
const accountId = resolveDmPolicyAccountId(cfg);
|
|
198
|
-
return resolveFeishuAccount({ cfg, accountId }).config.dmPolicy ?? "pairing";
|
|
199
|
-
},
|
|
200
|
-
setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy, onboardingDmPolicyAccountId),
|
|
201
|
-
promptAllowFrom: promptFeishuAllowFrom,
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
205
|
-
channel,
|
|
206
|
-
getStatus: async ({ cfg, accountOverrides }) => {
|
|
207
|
-
const override = accountOverrides?.feishu?.trim();
|
|
208
|
-
const accountIds = override
|
|
209
|
-
? [resolveOnboardingAccountId(cfg, override)]
|
|
210
|
-
: listFeishuAccountIds(cfg);
|
|
211
|
-
const accounts = accountIds.map((accountId) => resolveFeishuAccount({ cfg, accountId }));
|
|
212
|
-
const configuredAccounts = accounts.filter((account) => account.configured);
|
|
213
|
-
const configured = configuredAccounts.length > 0;
|
|
214
|
-
|
|
215
|
-
// Try to probe if configured
|
|
216
|
-
let probeResult = null;
|
|
217
|
-
if (configuredAccounts[0]) {
|
|
218
|
-
try {
|
|
219
|
-
probeResult = await probeFeishu(configuredAccounts[0]);
|
|
220
|
-
} catch {
|
|
221
|
-
// Ignore probe errors
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const statusLines: string[] = [];
|
|
226
|
-
if (!configured) {
|
|
227
|
-
statusLines.push("Feishu: needs app credentials");
|
|
228
|
-
} else if (probeResult?.ok) {
|
|
229
|
-
const probeAccountId = configuredAccounts[0]?.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
230
|
-
const accountNote = probeAccountId === DEFAULT_ACCOUNT_ID ? "" : ` [${probeAccountId}]`;
|
|
231
|
-
statusLines.push(
|
|
232
|
-
`Feishu${accountNote}: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`,
|
|
233
|
-
);
|
|
234
|
-
if (!override && configuredAccounts.length > 1) {
|
|
235
|
-
statusLines.push(`Feishu: ${configuredAccounts.length} account(s) configured`);
|
|
236
|
-
}
|
|
237
|
-
} else if (!override && configuredAccounts.length > 1) {
|
|
238
|
-
statusLines.push(`Feishu: configured (${configuredAccounts.length} account(s), connection not verified)`);
|
|
239
|
-
} else {
|
|
240
|
-
statusLines.push("Feishu: configured (connection not verified)");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return {
|
|
244
|
-
channel,
|
|
245
|
-
configured,
|
|
246
|
-
statusLines,
|
|
247
|
-
selectionHint: configured ? "configured" : "needs app creds",
|
|
248
|
-
quickstartScore: configured ? 2 : 0,
|
|
249
|
-
};
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
|
|
253
|
-
const feishuOverride = accountOverrides?.feishu?.trim();
|
|
254
|
-
const defaultFeishuAccountId = resolveDefaultFeishuAccountId(cfg);
|
|
255
|
-
let feishuAccountId = feishuOverride
|
|
256
|
-
? normalizeAccountId(feishuOverride)
|
|
257
|
-
: defaultFeishuAccountId;
|
|
258
|
-
if (shouldPromptAccountIds && !feishuOverride) {
|
|
259
|
-
feishuAccountId = await promptAccountId({
|
|
260
|
-
cfg,
|
|
261
|
-
prompter,
|
|
262
|
-
label: "Feishu",
|
|
263
|
-
currentId: feishuAccountId,
|
|
264
|
-
listAccountIds: listFeishuAccountIds,
|
|
265
|
-
defaultAccountId: defaultFeishuAccountId,
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
onboardingDmPolicyAccountId = feishuAccountId;
|
|
269
|
-
const accountLabel = feishuAccountId === DEFAULT_ACCOUNT_ID ? "default" : feishuAccountId;
|
|
270
|
-
const currentAccount = resolveFeishuAccount({ cfg, accountId: feishuAccountId });
|
|
271
|
-
const resolved = resolveFeishuCredentials(currentAccount.config);
|
|
272
|
-
const hasConfigCreds = Boolean(
|
|
273
|
-
currentAccount.config.appId?.trim() && currentAccount.config.appSecret?.trim(),
|
|
274
|
-
);
|
|
275
|
-
const canUseEnv = Boolean(
|
|
276
|
-
feishuAccountId === DEFAULT_ACCOUNT_ID &&
|
|
277
|
-
!hasConfigCreds &&
|
|
278
|
-
process.env.FEISHU_APP_ID?.trim() &&
|
|
279
|
-
process.env.FEISHU_APP_SECRET?.trim(),
|
|
280
|
-
);
|
|
281
|
-
|
|
282
|
-
let next = cfg;
|
|
283
|
-
let appId: string | null = null;
|
|
284
|
-
let appSecret: string | null = null;
|
|
285
|
-
const appIdPrompt =
|
|
286
|
-
feishuAccountId === DEFAULT_ACCOUNT_ID
|
|
287
|
-
? "Enter Feishu App ID"
|
|
288
|
-
: `Enter Feishu App ID for account "${accountLabel}"`;
|
|
289
|
-
const appSecretPrompt =
|
|
290
|
-
feishuAccountId === DEFAULT_ACCOUNT_ID
|
|
291
|
-
? "Enter Feishu App Secret"
|
|
292
|
-
: `Enter Feishu App Secret for account "${accountLabel}"`;
|
|
293
|
-
|
|
294
|
-
if (!resolved) {
|
|
295
|
-
await noteFeishuCredentialHelp(prompter);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (canUseEnv) {
|
|
299
|
-
const keepEnv = await prompter.confirm({
|
|
300
|
-
message: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
|
|
301
|
-
initialValue: true,
|
|
302
|
-
});
|
|
303
|
-
if (keepEnv) {
|
|
304
|
-
next = upsertFeishuAccountConfig(next, feishuAccountId, { enabled: true });
|
|
305
|
-
} else {
|
|
306
|
-
appId = String(
|
|
307
|
-
await prompter.text({
|
|
308
|
-
message: appIdPrompt,
|
|
309
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
310
|
-
}),
|
|
311
|
-
).trim();
|
|
312
|
-
appSecret = String(
|
|
313
|
-
await prompter.text({
|
|
314
|
-
message: appSecretPrompt,
|
|
315
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
316
|
-
}),
|
|
317
|
-
).trim();
|
|
318
|
-
}
|
|
319
|
-
} else if (hasConfigCreds) {
|
|
320
|
-
const keep = await prompter.confirm({
|
|
321
|
-
message: `Feishu credentials already configured for account "${accountLabel}". Keep them?`,
|
|
322
|
-
initialValue: true,
|
|
323
|
-
});
|
|
324
|
-
if (!keep) {
|
|
325
|
-
appId = String(
|
|
326
|
-
await prompter.text({
|
|
327
|
-
message: appIdPrompt,
|
|
328
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
329
|
-
}),
|
|
330
|
-
).trim();
|
|
331
|
-
appSecret = String(
|
|
332
|
-
await prompter.text({
|
|
333
|
-
message: appSecretPrompt,
|
|
334
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
335
|
-
}),
|
|
336
|
-
).trim();
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
appId = String(
|
|
340
|
-
await prompter.text({
|
|
341
|
-
message: appIdPrompt,
|
|
342
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
343
|
-
}),
|
|
344
|
-
).trim();
|
|
345
|
-
appSecret = String(
|
|
346
|
-
await prompter.text({
|
|
347
|
-
message: appSecretPrompt,
|
|
348
|
-
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
349
|
-
}),
|
|
350
|
-
).trim();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (appId && appSecret) {
|
|
354
|
-
next = upsertFeishuAccountConfig(next, feishuAccountId, {
|
|
355
|
-
enabled: true,
|
|
356
|
-
appId,
|
|
357
|
-
appSecret,
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
// Test connection
|
|
361
|
-
const testAccount = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId });
|
|
362
|
-
try {
|
|
363
|
-
const probe = await probeFeishu(testAccount);
|
|
364
|
-
if (probe.ok) {
|
|
365
|
-
await prompter.note(
|
|
366
|
-
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
367
|
-
`Feishu connection test (${accountLabel})`,
|
|
368
|
-
);
|
|
369
|
-
} else {
|
|
370
|
-
await prompter.note(
|
|
371
|
-
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
372
|
-
`Feishu connection test (${accountLabel})`,
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
} catch (err) {
|
|
376
|
-
await prompter.note(
|
|
377
|
-
`Connection test failed: ${String(err)}`,
|
|
378
|
-
`Feishu connection test (${accountLabel})`,
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Domain selection
|
|
384
|
-
const currentDomain = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId }).config.domain ?? "feishu";
|
|
385
|
-
const domain = await prompter.select({
|
|
386
|
-
message: "Which Feishu domain?",
|
|
387
|
-
options: [
|
|
388
|
-
{ value: "feishu", label: "Feishu (feishu.cn) - China" },
|
|
389
|
-
{ value: "lark", label: "Lark (larksuite.com) - International" },
|
|
390
|
-
],
|
|
391
|
-
initialValue: currentDomain,
|
|
392
|
-
});
|
|
393
|
-
if (domain) {
|
|
394
|
-
next = setFeishuDomain(next, domain as "feishu" | "lark", feishuAccountId);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Group policy
|
|
398
|
-
const groupPolicyAccount = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId });
|
|
399
|
-
const groupPolicy = await prompter.select({
|
|
400
|
-
message: "Group chat policy",
|
|
401
|
-
options: [
|
|
402
|
-
{ value: "allowlist", label: "Allowlist - only respond in specific groups" },
|
|
403
|
-
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
404
|
-
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
405
|
-
],
|
|
406
|
-
initialValue: groupPolicyAccount.config.groupPolicy ?? "allowlist",
|
|
407
|
-
});
|
|
408
|
-
if (groupPolicy) {
|
|
409
|
-
next = setFeishuGroupPolicy(
|
|
410
|
-
next,
|
|
411
|
-
groupPolicy as "open" | "allowlist" | "disabled",
|
|
412
|
-
feishuAccountId,
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Group allowlist if needed
|
|
417
|
-
if (groupPolicy === "allowlist") {
|
|
418
|
-
const existing =
|
|
419
|
-
resolveFeishuAccount({ cfg: next, accountId: feishuAccountId }).config.groupAllowFrom ?? [];
|
|
420
|
-
const entry = await prompter.text({
|
|
421
|
-
message: "Group chat allowlist (chat_ids)",
|
|
422
|
-
placeholder: "oc_xxxxx, oc_yyyyy",
|
|
423
|
-
initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
|
|
424
|
-
});
|
|
425
|
-
if (entry) {
|
|
426
|
-
const parts = parseAllowFromInput(String(entry));
|
|
427
|
-
if (parts.length > 0) {
|
|
428
|
-
next = setFeishuGroupAllowFrom(next, parts, feishuAccountId);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
return { cfg: next, accountId: feishuAccountId };
|
|
434
|
-
},
|
|
435
|
-
|
|
436
|
-
dmPolicy,
|
|
437
|
-
|
|
438
|
-
onAccountRecorded: (accountId) => {
|
|
439
|
-
onboardingDmPolicyAccountId = normalizeAccountId(accountId);
|
|
440
|
-
},
|
|
441
|
-
|
|
442
|
-
disable: (cfg) => ({
|
|
443
|
-
...cfg,
|
|
444
|
-
channels: {
|
|
445
|
-
...cfg.channels,
|
|
446
|
-
feishu: { ...cfg.channels?.feishu, enabled: false },
|
|
447
|
-
},
|
|
448
|
-
}),
|
|
449
|
-
};
|