@nextclaw/channel-plugin-feishu 0.2.12 → 0.2.14
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 +3 -1
- package/index.ts +65 -0
- package/openclaw.plugin.json +3 -7
- package/package.json +33 -9
- package/skills/feishu-doc/SKILL.md +211 -0
- package/skills/feishu-doc/references/block-types.md +103 -0
- package/skills/feishu-drive/SKILL.md +97 -0
- package/skills/feishu-perm/SKILL.md +119 -0
- package/skills/feishu-wiki/SKILL.md +111 -0
- package/src/accounts.test.ts +371 -0
- package/src/accounts.ts +244 -0
- package/src/async.ts +62 -0
- package/src/bitable.ts +725 -0
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +193 -0
- package/src/bot.stripBotMention.test.ts +134 -0
- package/src/bot.test.ts +2107 -0
- package/src/bot.ts +1556 -0
- package/src/card-action.ts +79 -0
- package/src/channel.test.ts +48 -0
- package/src/channel.ts +369 -0
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +324 -0
- package/src/client.ts +196 -0
- package/src/config-schema.test.ts +247 -0
- package/src/config-schema.ts +306 -0
- package/src/dedup.ts +203 -0
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +156 -0
- package/src/doc-schema.ts +182 -0
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +187 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +70 -0
- package/src/docx.test.ts +445 -0
- package/src/docx.ts +1460 -0
- package/src/drive-schema.ts +46 -0
- package/src/drive.ts +228 -0
- package/src/dynamic-agent.ts +131 -0
- package/src/external-keys.test.ts +20 -0
- package/src/external-keys.ts +19 -0
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +523 -0
- package/src/media.ts +484 -0
- package/src/mention.ts +133 -0
- package/src/monitor.account.ts +562 -0
- package/src/monitor.reaction.test.ts +653 -0
- package/src/monitor.startup.test.ts +190 -0
- package/src/monitor.startup.ts +64 -0
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +155 -0
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +264 -0
- package/src/monitor.ts +95 -0
- package/src/monitor.webhook-e2e.test.ts +214 -0
- package/src/monitor.webhook-security.test.ts +142 -0
- package/src/monitor.webhook.test-helpers.ts +98 -0
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +489 -0
- package/src/outbound.test.ts +356 -0
- package/src/outbound.ts +176 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +176 -0
- package/src/policy.test.ts +154 -0
- package/src/policy.ts +123 -0
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +270 -0
- package/src/probe.ts +156 -0
- package/src/reactions.ts +153 -0
- package/src/reply-dispatcher.test.ts +513 -0
- package/src/reply-dispatcher.ts +397 -0
- package/src/runtime.ts +6 -0
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-result.ts +29 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +29 -0
- package/src/send.reply-fallback.test.ts +189 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +481 -0
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +374 -0
- package/src/targets.test.ts +70 -0
- package/src/targets.ts +107 -0
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +103 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +210 -0
- package/src/wiki-schema.ts +55 -0
- package/src/wiki.ts +233 -0
- package/index.js +0 -27
package/src/accounts.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
|
2
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
|
|
3
|
+
import { normalizeResolvedSecretInputString, normalizeSecretInputString } from "./secret-input.js";
|
|
4
|
+
import type {
|
|
5
|
+
FeishuConfig,
|
|
6
|
+
FeishuAccountConfig,
|
|
7
|
+
FeishuDefaultAccountSelectionSource,
|
|
8
|
+
FeishuDomain,
|
|
9
|
+
ResolvedFeishuAccount,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* List all configured account IDs from the accounts field.
|
|
14
|
+
*/
|
|
15
|
+
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
|
16
|
+
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
|
|
17
|
+
if (!accounts || typeof accounts !== "object") {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
return Object.keys(accounts).filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* List all Feishu account IDs.
|
|
25
|
+
* If no accounts are configured, returns [DEFAULT_ACCOUNT_ID] for backward compatibility.
|
|
26
|
+
*/
|
|
27
|
+
export function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
|
|
28
|
+
const ids = listConfiguredAccountIds(cfg);
|
|
29
|
+
if (ids.length === 0) {
|
|
30
|
+
// Backward compatibility: no accounts configured, use default
|
|
31
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
32
|
+
}
|
|
33
|
+
return [...ids].toSorted((a, b) => a.localeCompare(b));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the default account selection and its source.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveDefaultFeishuAccountSelection(cfg: ClawdbotConfig): {
|
|
40
|
+
accountId: string;
|
|
41
|
+
source: FeishuDefaultAccountSelectionSource;
|
|
42
|
+
} {
|
|
43
|
+
const preferredRaw = (cfg.channels?.feishu as FeishuConfig | undefined)?.defaultAccount?.trim();
|
|
44
|
+
const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : undefined;
|
|
45
|
+
if (preferred) {
|
|
46
|
+
return {
|
|
47
|
+
accountId: preferred,
|
|
48
|
+
source: "explicit-default",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const ids = listFeishuAccountIds(cfg);
|
|
52
|
+
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
53
|
+
return {
|
|
54
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
55
|
+
source: "mapped-default",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
accountId: ids[0] ?? DEFAULT_ACCOUNT_ID,
|
|
60
|
+
source: "fallback",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the default account ID.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
|
|
68
|
+
return resolveDefaultFeishuAccountSelection(cfg).accountId;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get the raw account-specific config.
|
|
73
|
+
*/
|
|
74
|
+
function resolveAccountConfig(
|
|
75
|
+
cfg: ClawdbotConfig,
|
|
76
|
+
accountId: string,
|
|
77
|
+
): FeishuAccountConfig | undefined {
|
|
78
|
+
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
|
|
79
|
+
if (!accounts || typeof accounts !== "object") {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return accounts[accountId];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Merge top-level config with account-specific config.
|
|
87
|
+
* Account-specific fields override top-level fields.
|
|
88
|
+
*/
|
|
89
|
+
function mergeFeishuAccountConfig(cfg: ClawdbotConfig, accountId: string): FeishuConfig {
|
|
90
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
91
|
+
|
|
92
|
+
// Extract base config (exclude accounts field to avoid recursion)
|
|
93
|
+
const { accounts: _ignored, defaultAccount: _ignoredDefaultAccount, ...base } = feishuCfg ?? {};
|
|
94
|
+
|
|
95
|
+
// Get account-specific overrides
|
|
96
|
+
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
|
97
|
+
|
|
98
|
+
// Merge: account config overrides base config
|
|
99
|
+
return { ...base, ...account } as FeishuConfig;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolve Feishu credentials from a config.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveFeishuCredentials(cfg?: FeishuConfig): {
|
|
106
|
+
appId: string;
|
|
107
|
+
appSecret: string;
|
|
108
|
+
encryptKey?: string;
|
|
109
|
+
verificationToken?: string;
|
|
110
|
+
domain: FeishuDomain;
|
|
111
|
+
} | null;
|
|
112
|
+
export function resolveFeishuCredentials(
|
|
113
|
+
cfg: FeishuConfig | undefined,
|
|
114
|
+
options: { allowUnresolvedSecretRef?: boolean },
|
|
115
|
+
): {
|
|
116
|
+
appId: string;
|
|
117
|
+
appSecret: string;
|
|
118
|
+
encryptKey?: string;
|
|
119
|
+
verificationToken?: string;
|
|
120
|
+
domain: FeishuDomain;
|
|
121
|
+
} | null;
|
|
122
|
+
export function resolveFeishuCredentials(
|
|
123
|
+
cfg?: FeishuConfig,
|
|
124
|
+
options?: { allowUnresolvedSecretRef?: boolean },
|
|
125
|
+
): {
|
|
126
|
+
appId: string;
|
|
127
|
+
appSecret: string;
|
|
128
|
+
encryptKey?: string;
|
|
129
|
+
verificationToken?: string;
|
|
130
|
+
domain: FeishuDomain;
|
|
131
|
+
} | null {
|
|
132
|
+
const normalizeString = (value: unknown): string | undefined => {
|
|
133
|
+
if (typeof value !== "string") {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const trimmed = value.trim();
|
|
137
|
+
return trimmed ? trimmed : undefined;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const resolveSecretLike = (value: unknown, path: string): string | undefined => {
|
|
141
|
+
const asString = normalizeString(value);
|
|
142
|
+
if (asString) {
|
|
143
|
+
return asString;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// In relaxed/onboarding paths only: allow direct env SecretRef reads for UX.
|
|
147
|
+
// Default resolution path must preserve unresolved-ref diagnostics/policy semantics.
|
|
148
|
+
if (options?.allowUnresolvedSecretRef && typeof value === "object" && value !== null) {
|
|
149
|
+
const rec = value as Record<string, unknown>;
|
|
150
|
+
const source = normalizeString(rec.source)?.toLowerCase();
|
|
151
|
+
const id = normalizeString(rec.id);
|
|
152
|
+
if (source === "env" && id) {
|
|
153
|
+
const envValue = normalizeString(process.env[id]);
|
|
154
|
+
if (envValue) {
|
|
155
|
+
return envValue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options?.allowUnresolvedSecretRef) {
|
|
161
|
+
return normalizeSecretInputString(value);
|
|
162
|
+
}
|
|
163
|
+
return normalizeResolvedSecretInputString({ value, path });
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const appId = resolveSecretLike(cfg?.appId, "channels.feishu.appId");
|
|
167
|
+
const appSecret = resolveSecretLike(cfg?.appSecret, "channels.feishu.appSecret");
|
|
168
|
+
|
|
169
|
+
if (!appId || !appSecret) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const connectionMode = cfg?.connectionMode ?? "websocket";
|
|
173
|
+
return {
|
|
174
|
+
appId,
|
|
175
|
+
appSecret,
|
|
176
|
+
encryptKey:
|
|
177
|
+
connectionMode === "webhook"
|
|
178
|
+
? resolveSecretLike(cfg?.encryptKey, "channels.feishu.encryptKey")
|
|
179
|
+
: normalizeString(cfg?.encryptKey),
|
|
180
|
+
verificationToken: resolveSecretLike(
|
|
181
|
+
cfg?.verificationToken,
|
|
182
|
+
"channels.feishu.verificationToken",
|
|
183
|
+
),
|
|
184
|
+
domain: cfg?.domain ?? "feishu",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Resolve a complete Feishu account with merged config.
|
|
190
|
+
*/
|
|
191
|
+
export function resolveFeishuAccount(params: {
|
|
192
|
+
cfg: ClawdbotConfig;
|
|
193
|
+
accountId?: string | null;
|
|
194
|
+
}): ResolvedFeishuAccount {
|
|
195
|
+
const hasExplicitAccountId =
|
|
196
|
+
typeof params.accountId === "string" && params.accountId.trim() !== "";
|
|
197
|
+
const defaultSelection = hasExplicitAccountId
|
|
198
|
+
? null
|
|
199
|
+
: resolveDefaultFeishuAccountSelection(params.cfg);
|
|
200
|
+
const accountId = hasExplicitAccountId
|
|
201
|
+
? normalizeAccountId(params.accountId)
|
|
202
|
+
: (defaultSelection?.accountId ?? DEFAULT_ACCOUNT_ID);
|
|
203
|
+
const selectionSource = hasExplicitAccountId
|
|
204
|
+
? "explicit"
|
|
205
|
+
: (defaultSelection?.source ?? "fallback");
|
|
206
|
+
const feishuCfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
|
|
207
|
+
|
|
208
|
+
// Base enabled state (top-level)
|
|
209
|
+
const baseEnabled = feishuCfg?.enabled !== false;
|
|
210
|
+
|
|
211
|
+
// Merge configs
|
|
212
|
+
const merged = mergeFeishuAccountConfig(params.cfg, accountId);
|
|
213
|
+
|
|
214
|
+
// Account-level enabled state
|
|
215
|
+
const accountEnabled = merged.enabled !== false;
|
|
216
|
+
const enabled = baseEnabled && accountEnabled;
|
|
217
|
+
|
|
218
|
+
// Resolve credentials from merged config
|
|
219
|
+
const creds = resolveFeishuCredentials(merged);
|
|
220
|
+
const accountName = (merged as FeishuAccountConfig).name;
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
accountId,
|
|
224
|
+
selectionSource,
|
|
225
|
+
enabled,
|
|
226
|
+
configured: Boolean(creds),
|
|
227
|
+
name: typeof accountName === "string" ? accountName.trim() || undefined : undefined,
|
|
228
|
+
appId: creds?.appId,
|
|
229
|
+
appSecret: creds?.appSecret,
|
|
230
|
+
encryptKey: creds?.encryptKey,
|
|
231
|
+
verificationToken: creds?.verificationToken,
|
|
232
|
+
domain: creds?.domain ?? "feishu",
|
|
233
|
+
config: merged,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* List all enabled and configured accounts.
|
|
239
|
+
*/
|
|
240
|
+
export function listEnabledFeishuAccounts(cfg: ClawdbotConfig): ResolvedFeishuAccount[] {
|
|
241
|
+
return listFeishuAccountIds(cfg)
|
|
242
|
+
.map((accountId) => resolveFeishuAccount({ cfg, accountId }))
|
|
243
|
+
.filter((account) => account.enabled && account.configured);
|
|
244
|
+
}
|
package/src/async.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const RACE_TIMEOUT = Symbol("race-timeout");
|
|
2
|
+
const RACE_ABORT = Symbol("race-abort");
|
|
3
|
+
|
|
4
|
+
export type RaceWithTimeoutAndAbortResult<T> =
|
|
5
|
+
| { status: "resolved"; value: T }
|
|
6
|
+
| { status: "timeout" }
|
|
7
|
+
| { status: "aborted" };
|
|
8
|
+
|
|
9
|
+
export async function raceWithTimeoutAndAbort<T>(
|
|
10
|
+
promise: Promise<T>,
|
|
11
|
+
options: {
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
abortSignal?: AbortSignal;
|
|
14
|
+
} = {},
|
|
15
|
+
): Promise<RaceWithTimeoutAndAbortResult<T>> {
|
|
16
|
+
if (options.abortSignal?.aborted) {
|
|
17
|
+
return { status: "aborted" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (options.timeoutMs === undefined && !options.abortSignal) {
|
|
21
|
+
return { status: "resolved", value: await promise };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
25
|
+
let abortHandler: (() => void) | undefined;
|
|
26
|
+
const contenders: Array<Promise<T | typeof RACE_TIMEOUT | typeof RACE_ABORT>> = [promise];
|
|
27
|
+
|
|
28
|
+
if (options.timeoutMs !== undefined) {
|
|
29
|
+
contenders.push(
|
|
30
|
+
new Promise((resolve) => {
|
|
31
|
+
timeoutHandle = setTimeout(() => resolve(RACE_TIMEOUT), options.timeoutMs);
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (options.abortSignal) {
|
|
37
|
+
contenders.push(
|
|
38
|
+
new Promise((resolve) => {
|
|
39
|
+
abortHandler = () => resolve(RACE_ABORT);
|
|
40
|
+
options.abortSignal?.addEventListener("abort", abortHandler, { once: true });
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const result = await Promise.race(contenders);
|
|
47
|
+
if (result === RACE_TIMEOUT) {
|
|
48
|
+
return { status: "timeout" };
|
|
49
|
+
}
|
|
50
|
+
if (result === RACE_ABORT) {
|
|
51
|
+
return { status: "aborted" };
|
|
52
|
+
}
|
|
53
|
+
return { status: "resolved", value: result };
|
|
54
|
+
} finally {
|
|
55
|
+
if (timeoutHandle) {
|
|
56
|
+
clearTimeout(timeoutHandle);
|
|
57
|
+
}
|
|
58
|
+
if (abortHandler) {
|
|
59
|
+
options.abortSignal?.removeEventListener("abort", abortHandler);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|