@m1heng-clawd/feishu 0.1.9 → 0.1.11
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/README.md +199 -31
- package/index.ts +3 -1
- package/package.json +4 -4
- package/skills/feishu-doc/SKILL.md +24 -0
- package/src/bitable-tools/actions.ts +199 -0
- package/src/bitable-tools/common.ts +90 -0
- package/src/bitable-tools/meta.ts +80 -0
- package/src/bitable-tools/register.ts +195 -0
- package/src/bitable-tools/schemas.ts +221 -0
- package/src/bitable.ts +1 -441
- package/src/bot.ts +130 -68
- package/src/channel.ts +6 -8
- package/src/config-schema.ts +7 -0
- package/src/dedup.ts +31 -0
- package/src/doc-schema.ts +8 -0
- package/src/doc-write-service.ts +711 -0
- package/src/docx.ts +83 -287
- package/src/drive-schema.ts +20 -0
- package/src/drive.ts +57 -27
- package/src/dynamic-agent.ts +8 -4
- package/src/media.ts +25 -40
- package/src/monitor.ts +29 -2
- package/src/onboarding.ts +196 -105
- package/src/perm.ts +23 -24
- package/src/probe.ts +108 -4
- package/src/reply-dispatcher.ts +137 -73
- package/src/streaming-card.ts +211 -0
- package/src/targets.ts +1 -1
- package/src/task-tools/actions.ts +137 -0
- package/src/task-tools/common.ts +18 -0
- package/src/task-tools/register.ts +101 -0
- package/src/task-tools/schemas.ts +138 -0
- package/src/task.ts +1 -0
- package/src/tools-common/feishu-api.ts +184 -0
- package/src/tools-common/tool-context.ts +23 -0
- package/src/tools-common/tool-exec.ts +73 -0
- package/src/tools-config.ts +2 -1
- package/src/types.ts +1 -0
- package/src/wiki.ts +42 -43
package/src/media.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
2
|
import { createFeishuClient } from "./client.js";
|
|
3
3
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
4
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
4
5
|
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
|
5
6
|
import fs from "fs";
|
|
6
7
|
import path from "path";
|
|
@@ -353,10 +354,12 @@ export async function sendFileFeishu(params: {
|
|
|
353
354
|
cfg: ClawdbotConfig;
|
|
354
355
|
to: string;
|
|
355
356
|
fileKey: string;
|
|
357
|
+
msgType?: "file" | "media";
|
|
356
358
|
replyToMessageId?: string;
|
|
357
359
|
accountId?: string;
|
|
358
360
|
}): Promise<SendMediaResult> {
|
|
359
361
|
const { cfg, to, fileKey, replyToMessageId, accountId } = params;
|
|
362
|
+
const msgType = params.msgType ?? "file";
|
|
360
363
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
361
364
|
if (!account.configured) {
|
|
362
365
|
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
@@ -376,7 +379,7 @@ export async function sendFileFeishu(params: {
|
|
|
376
379
|
path: { message_id: replyToMessageId },
|
|
377
380
|
data: {
|
|
378
381
|
content,
|
|
379
|
-
msg_type:
|
|
382
|
+
msg_type: msgType,
|
|
380
383
|
},
|
|
381
384
|
});
|
|
382
385
|
|
|
@@ -395,7 +398,7 @@ export async function sendFileFeishu(params: {
|
|
|
395
398
|
data: {
|
|
396
399
|
receive_id: receiveId,
|
|
397
400
|
content,
|
|
398
|
-
msg_type:
|
|
401
|
+
msg_type: msgType,
|
|
399
402
|
},
|
|
400
403
|
});
|
|
401
404
|
|
|
@@ -440,23 +443,6 @@ export function detectFileType(
|
|
|
440
443
|
}
|
|
441
444
|
}
|
|
442
445
|
|
|
443
|
-
/**
|
|
444
|
-
* Check if a string is a local file path (not a URL)
|
|
445
|
-
*/
|
|
446
|
-
function isLocalPath(urlOrPath: string): boolean {
|
|
447
|
-
// Starts with / or ~ or drive letter (Windows)
|
|
448
|
-
if (urlOrPath.startsWith("/") || urlOrPath.startsWith("~") || /^[a-zA-Z]:/.test(urlOrPath)) {
|
|
449
|
-
return true;
|
|
450
|
-
}
|
|
451
|
-
// Try to parse as URL - if it fails or has no protocol, it's likely a local path
|
|
452
|
-
try {
|
|
453
|
-
const url = new URL(urlOrPath);
|
|
454
|
-
return url.protocol === "file:";
|
|
455
|
-
} catch {
|
|
456
|
-
return true; // Not a valid URL, treat as local path
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
446
|
/**
|
|
461
447
|
* Upload and send media (image or file) from URL, local path, or buffer
|
|
462
448
|
*/
|
|
@@ -470,6 +456,11 @@ export async function sendMediaFeishu(params: {
|
|
|
470
456
|
accountId?: string;
|
|
471
457
|
}): Promise<SendMediaResult> {
|
|
472
458
|
const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
|
|
459
|
+
const account = resolveFeishuAccount({ cfg, accountId });
|
|
460
|
+
if (!account.configured) {
|
|
461
|
+
throw new Error(`Feishu account "${account.accountId}" not configured`);
|
|
462
|
+
}
|
|
463
|
+
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
473
464
|
|
|
474
465
|
let buffer: Buffer;
|
|
475
466
|
let name: string;
|
|
@@ -478,26 +469,12 @@ export async function sendMediaFeishu(params: {
|
|
|
478
469
|
buffer = mediaBuffer;
|
|
479
470
|
name = fileName ?? "file";
|
|
480
471
|
} else if (mediaUrl) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
if (!fs.existsSync(filePath)) {
|
|
488
|
-
throw new Error(`Local file not found: ${filePath}`);
|
|
489
|
-
}
|
|
490
|
-
buffer = fs.readFileSync(filePath);
|
|
491
|
-
name = fileName ?? path.basename(filePath);
|
|
492
|
-
} else {
|
|
493
|
-
// Remote URL - fetch
|
|
494
|
-
const response = await fetch(mediaUrl);
|
|
495
|
-
if (!response.ok) {
|
|
496
|
-
throw new Error(`Failed to fetch media from URL: ${response.status}`);
|
|
497
|
-
}
|
|
498
|
-
buffer = Buffer.from(await response.arrayBuffer());
|
|
499
|
-
name = fileName ?? (path.basename(new URL(mediaUrl).pathname) || "file");
|
|
500
|
-
}
|
|
472
|
+
const loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
|
|
473
|
+
maxBytes: mediaMaxBytes,
|
|
474
|
+
optimizeImages: false,
|
|
475
|
+
});
|
|
476
|
+
buffer = loaded.buffer;
|
|
477
|
+
name = fileName ?? loaded.fileName ?? "file";
|
|
501
478
|
} else {
|
|
502
479
|
throw new Error("Either mediaUrl or mediaBuffer must be provided");
|
|
503
480
|
}
|
|
@@ -518,6 +495,14 @@ export async function sendMediaFeishu(params: {
|
|
|
518
495
|
fileType,
|
|
519
496
|
accountId,
|
|
520
497
|
});
|
|
521
|
-
|
|
498
|
+
const isMedia = fileType === "mp4" || fileType === "opus";
|
|
499
|
+
return sendFileFeishu({
|
|
500
|
+
cfg,
|
|
501
|
+
to,
|
|
502
|
+
fileKey,
|
|
503
|
+
msgType: isMedia ? "media" : "file",
|
|
504
|
+
replyToMessageId,
|
|
505
|
+
accountId,
|
|
506
|
+
});
|
|
522
507
|
}
|
|
523
508
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
2
|
import * as http from "http";
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
type ClawdbotConfig,
|
|
5
|
+
type RuntimeEnv,
|
|
6
|
+
type HistoryEntry,
|
|
7
|
+
installRequestBodyLimitGuard,
|
|
8
|
+
} from "openclaw/plugin-sdk";
|
|
4
9
|
import type { ResolvedFeishuAccount } from "./types.js";
|
|
5
10
|
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
|
|
6
11
|
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
|
|
@@ -18,6 +23,8 @@ export type MonitorFeishuOpts = {
|
|
|
18
23
|
const wsClients = new Map<string, Lark.WSClient>();
|
|
19
24
|
const httpServers = new Map<string, http.Server>();
|
|
20
25
|
const botOpenIds = new Map<string, string>();
|
|
26
|
+
const FEISHU_WEBHOOK_MAX_BODY_BYTES = 1024 * 1024;
|
|
27
|
+
const FEISHU_WEBHOOK_BODY_TIMEOUT_MS = 30_000;
|
|
21
28
|
|
|
22
29
|
async function fetchBotOpenId(
|
|
23
30
|
account: ResolvedFeishuAccount,
|
|
@@ -191,7 +198,27 @@ async function monitorWebhook({ params, accountId, eventDispatcher }: Connection
|
|
|
191
198
|
log(`feishu[${accountId}]: starting Webhook server on port ${port}, path ${path}...`);
|
|
192
199
|
|
|
193
200
|
const server = http.createServer();
|
|
194
|
-
|
|
201
|
+
const webhookHandler = Lark.adaptDefault(path, eventDispatcher, { autoChallenge: true });
|
|
202
|
+
server.on("request", (req, res) => {
|
|
203
|
+
const guard = installRequestBodyLimitGuard(req, res, {
|
|
204
|
+
maxBytes: FEISHU_WEBHOOK_MAX_BODY_BYTES,
|
|
205
|
+
timeoutMs: FEISHU_WEBHOOK_BODY_TIMEOUT_MS,
|
|
206
|
+
responseFormat: "text",
|
|
207
|
+
});
|
|
208
|
+
if (guard.isTripped()) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
void Promise.resolve(webhookHandler(req, res))
|
|
213
|
+
.catch((err) => {
|
|
214
|
+
if (!guard.isTripped()) {
|
|
215
|
+
error(`feishu[${accountId}]: webhook handler error: ${String(err)}`);
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
.finally(() => {
|
|
219
|
+
guard.dispose();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
195
222
|
httpServers.set(accountId, server);
|
|
196
223
|
|
|
197
224
|
return new Promise((resolve, reject) => {
|
package/src/onboarding.ts
CHANGED
|
@@ -5,45 +5,104 @@ import type {
|
|
|
5
5
|
DmPolicy,
|
|
6
6
|
WizardPrompter,
|
|
7
7
|
} from "openclaw/plugin-sdk";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
addWildcardAllowFrom,
|
|
10
|
+
DEFAULT_ACCOUNT_ID,
|
|
11
|
+
formatDocsLink,
|
|
12
|
+
normalizeAccountId,
|
|
13
|
+
promptAccountId,
|
|
14
|
+
} from "openclaw/plugin-sdk";
|
|
9
15
|
|
|
10
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
listFeishuAccountIds,
|
|
18
|
+
resolveDefaultFeishuAccountId,
|
|
19
|
+
resolveFeishuAccount,
|
|
20
|
+
resolveFeishuCredentials,
|
|
21
|
+
} from "./accounts.js";
|
|
11
22
|
import { probeFeishu } from "./probe.js";
|
|
12
23
|
import type { FeishuConfig } from "./types.js";
|
|
13
24
|
|
|
14
25
|
const channel = "feishu" as const;
|
|
26
|
+
let onboardingDmPolicyAccountId: string | undefined;
|
|
15
27
|
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
...cfg.channels?.feishu,
|
|
27
|
-
dmPolicy,
|
|
28
|
-
...(allowFrom ? { allowFrom } : {}),
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
};
|
|
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);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
function
|
|
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];
|
|
35
62
|
return {
|
|
36
63
|
...cfg,
|
|
37
64
|
channels: {
|
|
38
65
|
...cfg.channels,
|
|
39
66
|
feishu: {
|
|
40
|
-
...
|
|
41
|
-
|
|
67
|
+
...feishuCfg,
|
|
68
|
+
enabled: true,
|
|
69
|
+
accounts: {
|
|
70
|
+
...feishuCfg?.accounts,
|
|
71
|
+
[normalizedAccountId]: {
|
|
72
|
+
...existingAccount,
|
|
73
|
+
enabled: existingAccount?.enabled ?? true,
|
|
74
|
+
...patch,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
42
77
|
},
|
|
43
78
|
},
|
|
44
79
|
};
|
|
45
80
|
}
|
|
46
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
|
+
|
|
47
106
|
function parseAllowFromInput(raw: string): string[] {
|
|
48
107
|
return raw
|
|
49
108
|
.split(/[\n,;]+/g)
|
|
@@ -54,10 +113,14 @@ function parseAllowFromInput(raw: string): string[] {
|
|
|
54
113
|
async function promptFeishuAllowFrom(params: {
|
|
55
114
|
cfg: ClawdbotConfig;
|
|
56
115
|
prompter: WizardPrompter;
|
|
116
|
+
accountId?: string;
|
|
57
117
|
}): Promise<ClawdbotConfig> {
|
|
58
|
-
const
|
|
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;
|
|
59
121
|
await params.prompter.note(
|
|
60
122
|
[
|
|
123
|
+
`Account: ${accountLabel}`,
|
|
61
124
|
"Allowlist Feishu DMs by open_id or user_id.",
|
|
62
125
|
"You can find user open_id in Feishu admin console or via API.",
|
|
63
126
|
"Examples:",
|
|
@@ -83,7 +146,7 @@ async function promptFeishuAllowFrom(params: {
|
|
|
83
146
|
const unique = [
|
|
84
147
|
...new Set([...existing.map((v) => String(v).trim()).filter(Boolean), ...parts]),
|
|
85
148
|
];
|
|
86
|
-
return setFeishuAllowFrom(params.cfg, unique);
|
|
149
|
+
return setFeishuAllowFrom(params.cfg, unique, accountId);
|
|
87
150
|
}
|
|
88
151
|
}
|
|
89
152
|
|
|
@@ -105,31 +168,24 @@ async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void>
|
|
|
105
168
|
function setFeishuGroupPolicy(
|
|
106
169
|
cfg: ClawdbotConfig,
|
|
107
170
|
groupPolicy: "open" | "allowlist" | "disabled",
|
|
171
|
+
accountId?: string,
|
|
108
172
|
): ClawdbotConfig {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
channels: {
|
|
112
|
-
...cfg.channels,
|
|
113
|
-
feishu: {
|
|
114
|
-
...cfg.channels?.feishu,
|
|
115
|
-
enabled: true,
|
|
116
|
-
groupPolicy,
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
};
|
|
173
|
+
const resolvedAccountId = resolveOnboardingAccountId(cfg, accountId);
|
|
174
|
+
return upsertFeishuAccountConfig(cfg, resolvedAccountId, { enabled: true, groupPolicy });
|
|
120
175
|
}
|
|
121
176
|
|
|
122
|
-
function setFeishuGroupAllowFrom(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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 });
|
|
133
189
|
}
|
|
134
190
|
|
|
135
191
|
const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
@@ -137,22 +193,30 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
|
|
|
137
193
|
channel,
|
|
138
194
|
policyKey: "channels.feishu.dmPolicy",
|
|
139
195
|
allowFromKey: "channels.feishu.allowFrom",
|
|
140
|
-
getCurrent: (cfg) =>
|
|
141
|
-
|
|
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),
|
|
142
201
|
promptAllowFrom: promptFeishuAllowFrom,
|
|
143
202
|
};
|
|
144
203
|
|
|
145
204
|
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
146
205
|
channel,
|
|
147
|
-
getStatus: async ({ cfg }) => {
|
|
148
|
-
const
|
|
149
|
-
const
|
|
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;
|
|
150
214
|
|
|
151
215
|
// Try to probe if configured
|
|
152
216
|
let probeResult = null;
|
|
153
|
-
if (
|
|
217
|
+
if (configuredAccounts[0]) {
|
|
154
218
|
try {
|
|
155
|
-
probeResult = await probeFeishu(
|
|
219
|
+
probeResult = await probeFeishu(configuredAccounts[0]);
|
|
156
220
|
} catch {
|
|
157
221
|
// Ignore probe errors
|
|
158
222
|
}
|
|
@@ -162,7 +226,16 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
162
226
|
if (!configured) {
|
|
163
227
|
statusLines.push("Feishu: needs app credentials");
|
|
164
228
|
} else if (probeResult?.ok) {
|
|
165
|
-
|
|
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)`);
|
|
166
239
|
} else {
|
|
167
240
|
statusLines.push("Feishu: configured (connection not verified)");
|
|
168
241
|
}
|
|
@@ -176,11 +249,31 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
176
249
|
};
|
|
177
250
|
},
|
|
178
251
|
|
|
179
|
-
configure: async ({ cfg, prompter }) => {
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
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
|
+
);
|
|
183
275
|
const canUseEnv = Boolean(
|
|
276
|
+
feishuAccountId === DEFAULT_ACCOUNT_ID &&
|
|
184
277
|
!hasConfigCreds &&
|
|
185
278
|
process.env.FEISHU_APP_ID?.trim() &&
|
|
186
279
|
process.env.FEISHU_APP_SECRET?.trim(),
|
|
@@ -189,6 +282,14 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
189
282
|
let next = cfg;
|
|
190
283
|
let appId: string | null = null;
|
|
191
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}"`;
|
|
192
293
|
|
|
193
294
|
if (!resolved) {
|
|
194
295
|
await noteFeishuCredentialHelp(prompter);
|
|
@@ -200,42 +301,36 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
200
301
|
initialValue: true,
|
|
201
302
|
});
|
|
202
303
|
if (keepEnv) {
|
|
203
|
-
next = {
|
|
204
|
-
...next,
|
|
205
|
-
channels: {
|
|
206
|
-
...next.channels,
|
|
207
|
-
feishu: { ...next.channels?.feishu, enabled: true },
|
|
208
|
-
},
|
|
209
|
-
};
|
|
304
|
+
next = upsertFeishuAccountConfig(next, feishuAccountId, { enabled: true });
|
|
210
305
|
} else {
|
|
211
306
|
appId = String(
|
|
212
307
|
await prompter.text({
|
|
213
|
-
message:
|
|
308
|
+
message: appIdPrompt,
|
|
214
309
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
215
310
|
}),
|
|
216
311
|
).trim();
|
|
217
312
|
appSecret = String(
|
|
218
313
|
await prompter.text({
|
|
219
|
-
message:
|
|
314
|
+
message: appSecretPrompt,
|
|
220
315
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
221
316
|
}),
|
|
222
317
|
).trim();
|
|
223
318
|
}
|
|
224
319
|
} else if (hasConfigCreds) {
|
|
225
320
|
const keep = await prompter.confirm({
|
|
226
|
-
message:
|
|
321
|
+
message: `Feishu credentials already configured for account "${accountLabel}". Keep them?`,
|
|
227
322
|
initialValue: true,
|
|
228
323
|
});
|
|
229
324
|
if (!keep) {
|
|
230
325
|
appId = String(
|
|
231
326
|
await prompter.text({
|
|
232
|
-
message:
|
|
327
|
+
message: appIdPrompt,
|
|
233
328
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
234
329
|
}),
|
|
235
330
|
).trim();
|
|
236
331
|
appSecret = String(
|
|
237
332
|
await prompter.text({
|
|
238
|
-
message:
|
|
333
|
+
message: appSecretPrompt,
|
|
239
334
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
240
335
|
}),
|
|
241
336
|
).trim();
|
|
@@ -243,54 +338,50 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
243
338
|
} else {
|
|
244
339
|
appId = String(
|
|
245
340
|
await prompter.text({
|
|
246
|
-
message:
|
|
341
|
+
message: appIdPrompt,
|
|
247
342
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
248
343
|
}),
|
|
249
344
|
).trim();
|
|
250
345
|
appSecret = String(
|
|
251
346
|
await prompter.text({
|
|
252
|
-
message:
|
|
347
|
+
message: appSecretPrompt,
|
|
253
348
|
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
254
349
|
}),
|
|
255
350
|
).trim();
|
|
256
351
|
}
|
|
257
352
|
|
|
258
353
|
if (appId && appSecret) {
|
|
259
|
-
next = {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
...next.channels?.feishu,
|
|
265
|
-
enabled: true,
|
|
266
|
-
appId,
|
|
267
|
-
appSecret,
|
|
268
|
-
},
|
|
269
|
-
},
|
|
270
|
-
};
|
|
354
|
+
next = upsertFeishuAccountConfig(next, feishuAccountId, {
|
|
355
|
+
enabled: true,
|
|
356
|
+
appId,
|
|
357
|
+
appSecret,
|
|
358
|
+
});
|
|
271
359
|
|
|
272
360
|
// Test connection
|
|
273
|
-
const
|
|
361
|
+
const testAccount = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId });
|
|
274
362
|
try {
|
|
275
|
-
const probe = await probeFeishu(
|
|
363
|
+
const probe = await probeFeishu(testAccount);
|
|
276
364
|
if (probe.ok) {
|
|
277
365
|
await prompter.note(
|
|
278
366
|
`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
|
|
279
|
-
|
|
367
|
+
`Feishu connection test (${accountLabel})`,
|
|
280
368
|
);
|
|
281
369
|
} else {
|
|
282
370
|
await prompter.note(
|
|
283
371
|
`Connection failed: ${probe.error ?? "unknown error"}`,
|
|
284
|
-
|
|
372
|
+
`Feishu connection test (${accountLabel})`,
|
|
285
373
|
);
|
|
286
374
|
}
|
|
287
375
|
} catch (err) {
|
|
288
|
-
await prompter.note(
|
|
376
|
+
await prompter.note(
|
|
377
|
+
`Connection test failed: ${String(err)}`,
|
|
378
|
+
`Feishu connection test (${accountLabel})`,
|
|
379
|
+
);
|
|
289
380
|
}
|
|
290
381
|
}
|
|
291
382
|
|
|
292
383
|
// Domain selection
|
|
293
|
-
const currentDomain = (next
|
|
384
|
+
const currentDomain = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId }).config.domain ?? "feishu";
|
|
294
385
|
const domain = await prompter.select({
|
|
295
386
|
message: "Which Feishu domain?",
|
|
296
387
|
options: [
|
|
@@ -300,19 +391,11 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
300
391
|
initialValue: currentDomain,
|
|
301
392
|
});
|
|
302
393
|
if (domain) {
|
|
303
|
-
next =
|
|
304
|
-
...next,
|
|
305
|
-
channels: {
|
|
306
|
-
...next.channels,
|
|
307
|
-
feishu: {
|
|
308
|
-
...next.channels?.feishu,
|
|
309
|
-
domain: domain as "feishu" | "lark",
|
|
310
|
-
},
|
|
311
|
-
},
|
|
312
|
-
};
|
|
394
|
+
next = setFeishuDomain(next, domain as "feishu" | "lark", feishuAccountId);
|
|
313
395
|
}
|
|
314
396
|
|
|
315
397
|
// Group policy
|
|
398
|
+
const groupPolicyAccount = resolveFeishuAccount({ cfg: next, accountId: feishuAccountId });
|
|
316
399
|
const groupPolicy = await prompter.select({
|
|
317
400
|
message: "Group chat policy",
|
|
318
401
|
options: [
|
|
@@ -320,16 +403,20 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
320
403
|
{ value: "open", label: "Open - respond in all groups (requires mention)" },
|
|
321
404
|
{ value: "disabled", label: "Disabled - don't respond in groups" },
|
|
322
405
|
],
|
|
323
|
-
initialValue:
|
|
324
|
-
(next.channels?.feishu as FeishuConfig | undefined)?.groupPolicy ?? "allowlist",
|
|
406
|
+
initialValue: groupPolicyAccount.config.groupPolicy ?? "allowlist",
|
|
325
407
|
});
|
|
326
408
|
if (groupPolicy) {
|
|
327
|
-
next = setFeishuGroupPolicy(
|
|
409
|
+
next = setFeishuGroupPolicy(
|
|
410
|
+
next,
|
|
411
|
+
groupPolicy as "open" | "allowlist" | "disabled",
|
|
412
|
+
feishuAccountId,
|
|
413
|
+
);
|
|
328
414
|
}
|
|
329
415
|
|
|
330
416
|
// Group allowlist if needed
|
|
331
417
|
if (groupPolicy === "allowlist") {
|
|
332
|
-
const existing =
|
|
418
|
+
const existing =
|
|
419
|
+
resolveFeishuAccount({ cfg: next, accountId: feishuAccountId }).config.groupAllowFrom ?? [];
|
|
333
420
|
const entry = await prompter.text({
|
|
334
421
|
message: "Group chat allowlist (chat_ids)",
|
|
335
422
|
placeholder: "oc_xxxxx, oc_yyyyy",
|
|
@@ -338,16 +425,20 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
|
338
425
|
if (entry) {
|
|
339
426
|
const parts = parseAllowFromInput(String(entry));
|
|
340
427
|
if (parts.length > 0) {
|
|
341
|
-
next = setFeishuGroupAllowFrom(next, parts);
|
|
428
|
+
next = setFeishuGroupAllowFrom(next, parts, feishuAccountId);
|
|
342
429
|
}
|
|
343
430
|
}
|
|
344
431
|
}
|
|
345
432
|
|
|
346
|
-
return { cfg: next, accountId:
|
|
433
|
+
return { cfg: next, accountId: feishuAccountId };
|
|
347
434
|
},
|
|
348
435
|
|
|
349
436
|
dmPolicy,
|
|
350
437
|
|
|
438
|
+
onAccountRecorded: (accountId) => {
|
|
439
|
+
onboardingDmPolicyAccountId = normalizeAccountId(accountId);
|
|
440
|
+
},
|
|
441
|
+
|
|
351
442
|
disable: (cfg) => ({
|
|
352
443
|
...cfg,
|
|
353
444
|
channels: {
|