@openclaw/feishu 2026.2.2 → 2026.2.9

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/src/onboarding.ts CHANGED
@@ -1,124 +1,113 @@
1
1
  import type {
2
2
  ChannelOnboardingAdapter,
3
3
  ChannelOnboardingDmPolicy,
4
+ ClawdbotConfig,
4
5
  DmPolicy,
5
- OpenClawConfig,
6
6
  WizardPrompter,
7
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
- import {
16
- listFeishuAccountIds,
17
- resolveDefaultFeishuAccountId,
18
- resolveFeishuAccount,
19
- } from "openclaw/plugin-sdk";
8
+ import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "openclaw/plugin-sdk";
9
+ import type { FeishuConfig } from "./types.js";
10
+ import { resolveFeishuCredentials } from "./accounts.js";
11
+ import { probeFeishu } from "./probe.js";
20
12
 
21
13
  const channel = "feishu" as const;
22
14
 
23
- function setFeishuDmPolicy(cfg: OpenClawConfig, policy: DmPolicy): OpenClawConfig {
15
+ function setFeishuDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
24
16
  const allowFrom =
25
- policy === "open" ? addWildcardAllowFrom(cfg.channels?.feishu?.allowFrom) : undefined;
17
+ dmPolicy === "open"
18
+ ? addWildcardAllowFrom(cfg.channels?.feishu?.allowFrom)?.map((entry) => String(entry))
19
+ : undefined;
26
20
  return {
27
21
  ...cfg,
28
22
  channels: {
29
23
  ...cfg.channels,
30
24
  feishu: {
31
25
  ...cfg.channels?.feishu,
32
- enabled: true,
33
- dmPolicy: policy,
26
+ dmPolicy,
34
27
  ...(allowFrom ? { allowFrom } : {}),
35
28
  },
36
29
  },
37
30
  };
38
31
  }
39
32
 
40
- async function noteFeishuSetup(prompter: WizardPrompter): Promise<void> {
41
- await prompter.note(
42
- [
43
- "Create a Feishu/Lark app and enable Bot + Event Subscription (WebSocket).",
44
- "Copy the App ID and App Secret from the app credentials page.",
45
- 'Lark (global): use open.larksuite.com and set domain="lark".',
46
- `Docs: ${formatDocsLink("/channels/feishu", "channels/feishu")}`,
47
- ].join("\n"),
48
- "Feishu setup",
49
- );
50
- }
51
-
52
- function normalizeAllowEntry(entry: string): string {
53
- return entry.replace(/^(feishu|lark):/i, "").trim();
33
+ function setFeishuAllowFrom(cfg: ClawdbotConfig, allowFrom: string[]): ClawdbotConfig {
34
+ return {
35
+ ...cfg,
36
+ channels: {
37
+ ...cfg.channels,
38
+ feishu: {
39
+ ...cfg.channels?.feishu,
40
+ allowFrom,
41
+ },
42
+ },
43
+ };
54
44
  }
55
45
 
56
- function resolveDomainChoice(domain?: string | null): "feishu" | "lark" {
57
- const normalized = String(domain ?? "").toLowerCase();
58
- if (normalized.includes("lark") || normalized.includes("larksuite")) {
59
- return "lark";
60
- }
61
- return "feishu";
46
+ function parseAllowFromInput(raw: string): string[] {
47
+ return raw
48
+ .split(/[\n,;]+/g)
49
+ .map((entry) => entry.trim())
50
+ .filter(Boolean);
62
51
  }
63
52
 
64
53
  async function promptFeishuAllowFrom(params: {
65
- cfg: OpenClawConfig;
54
+ cfg: ClawdbotConfig;
66
55
  prompter: WizardPrompter;
67
- accountId?: string | null;
68
- }): Promise<OpenClawConfig> {
69
- const { cfg, prompter } = params;
70
- const accountId = normalizeAccountId(params.accountId);
71
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
72
- const existingAllowFrom = isDefault
73
- ? (cfg.channels?.feishu?.allowFrom ?? [])
74
- : (cfg.channels?.feishu?.accounts?.[accountId]?.allowFrom ?? []);
75
-
76
- const entry = await prompter.text({
77
- message: "Feishu allowFrom (open_id or union_id)",
78
- placeholder: "ou_xxx",
79
- initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
80
- validate: (value) => {
81
- const raw = String(value ?? "").trim();
82
- if (!raw) {
83
- return "Required";
84
- }
85
- const entries = raw
86
- .split(/[\n,;]+/g)
87
- .map((item) => normalizeAllowEntry(item))
88
- .filter(Boolean);
89
- const invalid = entries.filter((item) => item !== "*" && !/^o[un]_[a-zA-Z0-9]+$/.test(item));
90
- if (invalid.length > 0) {
91
- return `Invalid Feishu ids: ${invalid.join(", ")}`;
92
- }
93
- return undefined;
94
- },
95
- });
56
+ }): Promise<ClawdbotConfig> {
57
+ const existing = params.cfg.channels?.feishu?.allowFrom ?? [];
58
+ await params.prompter.note(
59
+ [
60
+ "Allowlist Feishu DMs by open_id or user_id.",
61
+ "You can find user open_id in Feishu admin console or via API.",
62
+ "Examples:",
63
+ "- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
64
+ "- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
65
+ ].join("\n"),
66
+ "Feishu allowlist",
67
+ );
96
68
 
97
- const parsed = String(entry)
98
- .split(/[\n,;]+/g)
99
- .map((item) => normalizeAllowEntry(item))
100
- .filter(Boolean);
101
- const merged = [
102
- ...existingAllowFrom.map((item) => normalizeAllowEntry(String(item))),
103
- ...parsed,
104
- ].filter(Boolean);
105
- const unique = Array.from(new Set(merged));
69
+ while (true) {
70
+ const entry = await params.prompter.text({
71
+ message: "Feishu allowFrom (user open_ids)",
72
+ placeholder: "ou_xxxxx, ou_yyyyy",
73
+ initialValue: existing[0] ? String(existing[0]) : undefined,
74
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
75
+ });
76
+ const parts = parseAllowFromInput(String(entry));
77
+ if (parts.length === 0) {
78
+ await params.prompter.note("Enter at least one user.", "Feishu allowlist");
79
+ continue;
80
+ }
106
81
 
107
- if (isDefault) {
108
- return {
109
- ...cfg,
110
- channels: {
111
- ...cfg.channels,
112
- feishu: {
113
- ...cfg.channels?.feishu,
114
- enabled: true,
115
- dmPolicy: "allowlist",
116
- allowFrom: unique,
117
- },
118
- },
119
- };
82
+ const unique = [
83
+ ...new Set([
84
+ ...existing.map((v: string | number) => String(v).trim()).filter(Boolean),
85
+ ...parts,
86
+ ]),
87
+ ];
88
+ return setFeishuAllowFrom(params.cfg, unique);
120
89
  }
90
+ }
91
+
92
+ async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void> {
93
+ await prompter.note(
94
+ [
95
+ "1) Go to Feishu Open Platform (open.feishu.cn)",
96
+ "2) Create a self-built app",
97
+ "3) Get App ID and App Secret from Credentials page",
98
+ "4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
99
+ "5) Publish the app or add it to a test group",
100
+ "Tip: you can also set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.",
101
+ `Docs: ${formatDocsLink("/channels/feishu", "feishu")}`,
102
+ ].join("\n"),
103
+ "Feishu credentials",
104
+ );
105
+ }
121
106
 
107
+ function setFeishuGroupPolicy(
108
+ cfg: ClawdbotConfig,
109
+ groupPolicy: "open" | "allowlist" | "disabled",
110
+ ): ClawdbotConfig {
122
111
  return {
123
112
  ...cfg,
124
113
  channels: {
@@ -126,15 +115,20 @@ async function promptFeishuAllowFrom(params: {
126
115
  feishu: {
127
116
  ...cfg.channels?.feishu,
128
117
  enabled: true,
129
- accounts: {
130
- ...cfg.channels?.feishu?.accounts,
131
- [accountId]: {
132
- ...cfg.channels?.feishu?.accounts?.[accountId],
133
- enabled: cfg.channels?.feishu?.accounts?.[accountId]?.enabled ?? true,
134
- dmPolicy: "allowlist",
135
- allowFrom: unique,
136
- },
137
- },
118
+ groupPolicy,
119
+ },
120
+ },
121
+ };
122
+ }
123
+
124
+ function setFeishuGroupAllowFrom(cfg: ClawdbotConfig, groupAllowFrom: string[]): ClawdbotConfig {
125
+ return {
126
+ ...cfg,
127
+ channels: {
128
+ ...cfg.channels,
129
+ feishu: {
130
+ ...cfg.channels?.feishu,
131
+ groupAllowFrom,
138
132
  },
139
133
  },
140
134
  };
@@ -145,134 +139,221 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
145
139
  channel,
146
140
  policyKey: "channels.feishu.dmPolicy",
147
141
  allowFromKey: "channels.feishu.allowFrom",
148
- getCurrent: (cfg) => cfg.channels?.feishu?.dmPolicy ?? "pairing",
142
+ getCurrent: (cfg) => (cfg.channels?.feishu as FeishuConfig | undefined)?.dmPolicy ?? "pairing",
149
143
  setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy),
150
144
  promptAllowFrom: promptFeishuAllowFrom,
151
145
  };
152
146
 
153
- function updateFeishuConfig(
154
- cfg: OpenClawConfig,
155
- accountId: string,
156
- updates: { appId?: string; appSecret?: string; domain?: string; enabled?: boolean },
157
- ): OpenClawConfig {
158
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
159
- const next = { ...cfg } as OpenClawConfig;
160
- const feishu = { ...next.channels?.feishu } as Record<string, unknown>;
161
- const accounts = feishu.accounts
162
- ? { ...(feishu.accounts as Record<string, unknown>) }
163
- : undefined;
164
-
165
- if (isDefault && !accounts) {
166
- return {
167
- ...next,
168
- channels: {
169
- ...next.channels,
170
- feishu: {
171
- ...feishu,
172
- ...updates,
173
- enabled: updates.enabled ?? true,
174
- },
175
- },
176
- };
177
- }
178
-
179
- const resolvedAccounts = accounts ?? {};
180
- const existing = (resolvedAccounts[accountId] as Record<string, unknown>) ?? {};
181
- resolvedAccounts[accountId] = {
182
- ...existing,
183
- ...updates,
184
- enabled: updates.enabled ?? true,
185
- };
186
-
187
- return {
188
- ...next,
189
- channels: {
190
- ...next.channels,
191
- feishu: {
192
- ...feishu,
193
- accounts: resolvedAccounts,
194
- },
195
- },
196
- };
197
- }
198
-
199
147
  export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
200
148
  channel,
201
- dmPolicy,
202
149
  getStatus: async ({ cfg }) => {
203
- const configured = listFeishuAccountIds(cfg).some((id) => {
204
- const acc = resolveFeishuAccount({ cfg, accountId: id });
205
- return acc.tokenSource !== "none";
206
- });
150
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
151
+ const configured = Boolean(resolveFeishuCredentials(feishuCfg));
152
+
153
+ // Try to probe if configured
154
+ let probeResult = null;
155
+ if (configured && feishuCfg) {
156
+ try {
157
+ probeResult = await probeFeishu(feishuCfg);
158
+ } catch {
159
+ // Ignore probe errors
160
+ }
161
+ }
162
+
163
+ const statusLines: string[] = [];
164
+ if (!configured) {
165
+ statusLines.push("Feishu: needs app credentials");
166
+ } else if (probeResult?.ok) {
167
+ statusLines.push(
168
+ `Feishu: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`,
169
+ );
170
+ } else {
171
+ statusLines.push("Feishu: configured (connection not verified)");
172
+ }
173
+
207
174
  return {
208
175
  channel,
209
176
  configured,
210
- statusLines: [`Feishu: ${configured ? "configured" : "needs app credentials"}`],
211
- selectionHint: configured ? "configured" : "requires app credentials",
212
- quickstartScore: configured ? 1 : 10,
177
+ statusLines,
178
+ selectionHint: configured ? "configured" : "needs app creds",
179
+ quickstartScore: configured ? 2 : 0,
213
180
  };
214
181
  },
215
- configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
182
+
183
+ configure: async ({ cfg, prompter }) => {
184
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
185
+ const resolved = resolveFeishuCredentials(feishuCfg);
186
+ const hasConfigCreds = Boolean(feishuCfg?.appId?.trim() && feishuCfg?.appSecret?.trim());
187
+ const canUseEnv = Boolean(
188
+ !hasConfigCreds && process.env.FEISHU_APP_ID?.trim() && process.env.FEISHU_APP_SECRET?.trim(),
189
+ );
190
+
216
191
  let next = cfg;
217
- const override = accountOverrides.feishu?.trim();
218
- const defaultId = resolveDefaultFeishuAccountId(next);
219
- let accountId = override ? normalizeAccountId(override) : defaultId;
192
+ let appId: string | null = null;
193
+ let appSecret: string | null = null;
194
+
195
+ if (!resolved) {
196
+ await noteFeishuCredentialHelp(prompter);
197
+ }
220
198
 
221
- if (shouldPromptAccountIds && !override) {
222
- accountId = await promptAccountId({
223
- cfg: next,
224
- prompter,
225
- label: "Feishu",
226
- currentId: accountId,
227
- listAccountIds: listFeishuAccountIds,
228
- defaultAccountId: defaultId,
199
+ if (canUseEnv) {
200
+ const keepEnv = await prompter.confirm({
201
+ message: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
202
+ initialValue: true,
229
203
  });
204
+ if (keepEnv) {
205
+ next = {
206
+ ...next,
207
+ channels: {
208
+ ...next.channels,
209
+ feishu: { ...next.channels?.feishu, enabled: true },
210
+ },
211
+ };
212
+ } else {
213
+ appId = String(
214
+ await prompter.text({
215
+ message: "Enter Feishu App ID",
216
+ validate: (value) => (value?.trim() ? undefined : "Required"),
217
+ }),
218
+ ).trim();
219
+ appSecret = String(
220
+ await prompter.text({
221
+ message: "Enter Feishu App Secret",
222
+ validate: (value) => (value?.trim() ? undefined : "Required"),
223
+ }),
224
+ ).trim();
225
+ }
226
+ } else if (hasConfigCreds) {
227
+ const keep = await prompter.confirm({
228
+ message: "Feishu credentials already configured. Keep them?",
229
+ initialValue: true,
230
+ });
231
+ if (!keep) {
232
+ appId = String(
233
+ await prompter.text({
234
+ message: "Enter Feishu App ID",
235
+ validate: (value) => (value?.trim() ? undefined : "Required"),
236
+ }),
237
+ ).trim();
238
+ appSecret = String(
239
+ await prompter.text({
240
+ message: "Enter Feishu App Secret",
241
+ validate: (value) => (value?.trim() ? undefined : "Required"),
242
+ }),
243
+ ).trim();
244
+ }
245
+ } else {
246
+ appId = String(
247
+ await prompter.text({
248
+ message: "Enter Feishu App ID",
249
+ validate: (value) => (value?.trim() ? undefined : "Required"),
250
+ }),
251
+ ).trim();
252
+ appSecret = String(
253
+ await prompter.text({
254
+ message: "Enter Feishu App Secret",
255
+ validate: (value) => (value?.trim() ? undefined : "Required"),
256
+ }),
257
+ ).trim();
230
258
  }
231
259
 
232
- await noteFeishuSetup(prompter);
260
+ if (appId && appSecret) {
261
+ next = {
262
+ ...next,
263
+ channels: {
264
+ ...next.channels,
265
+ feishu: {
266
+ ...next.channels?.feishu,
267
+ enabled: true,
268
+ appId,
269
+ appSecret,
270
+ },
271
+ },
272
+ };
273
+
274
+ // Test connection
275
+ const testCfg = next.channels?.feishu as FeishuConfig;
276
+ try {
277
+ const probe = await probeFeishu(testCfg);
278
+ if (probe.ok) {
279
+ await prompter.note(
280
+ `Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`,
281
+ "Feishu connection test",
282
+ );
283
+ } else {
284
+ await prompter.note(
285
+ `Connection failed: ${probe.error ?? "unknown error"}`,
286
+ "Feishu connection test",
287
+ );
288
+ }
289
+ } catch (err) {
290
+ await prompter.note(`Connection test failed: ${String(err)}`, "Feishu connection test");
291
+ }
292
+ }
293
+
294
+ // Domain selection
295
+ const currentDomain = (next.channels?.feishu as FeishuConfig | undefined)?.domain ?? "feishu";
296
+ const domain = await prompter.select({
297
+ message: "Which Feishu domain?",
298
+ options: [
299
+ { value: "feishu", label: "Feishu (feishu.cn) - China" },
300
+ { value: "lark", label: "Lark (larksuite.com) - International" },
301
+ ],
302
+ initialValue: currentDomain,
303
+ });
304
+ if (domain) {
305
+ next = {
306
+ ...next,
307
+ channels: {
308
+ ...next.channels,
309
+ feishu: {
310
+ ...next.channels?.feishu,
311
+ domain: domain as "feishu" | "lark",
312
+ },
313
+ },
314
+ };
315
+ }
233
316
 
234
- const resolved = resolveFeishuAccount({ cfg: next, accountId });
235
- const domainChoice = await prompter.select({
236
- message: "Feishu domain",
317
+ // Group policy
318
+ const groupPolicy = await prompter.select({
319
+ message: "Group chat policy",
237
320
  options: [
238
- { value: "feishu", label: "Feishu (China) open.feishu.cn" },
239
- { value: "lark", label: "Lark (global) — open.larksuite.com" },
321
+ { value: "allowlist", label: "Allowlist - only respond in specific groups" },
322
+ { value: "open", label: "Open - respond in all groups (requires mention)" },
323
+ { value: "disabled", label: "Disabled - don't respond in groups" },
240
324
  ],
241
- initialValue: resolveDomainChoice(resolved.config.domain),
325
+ initialValue: (next.channels?.feishu as FeishuConfig | undefined)?.groupPolicy ?? "allowlist",
242
326
  });
243
- const domain = domainChoice === "lark" ? "lark" : "feishu";
327
+ if (groupPolicy) {
328
+ next = setFeishuGroupPolicy(next, groupPolicy as "open" | "allowlist" | "disabled");
329
+ }
244
330
 
245
- const isDefault = accountId === DEFAULT_ACCOUNT_ID;
246
- const envAppId = process.env.FEISHU_APP_ID?.trim();
247
- const envSecret = process.env.FEISHU_APP_SECRET?.trim();
248
- if (isDefault && envAppId && envSecret) {
249
- const useEnv = await prompter.confirm({
250
- message: "FEISHU_APP_ID/FEISHU_APP_SECRET detected. Use env vars?",
251
- initialValue: true,
331
+ // Group allowlist if needed
332
+ if (groupPolicy === "allowlist") {
333
+ const existing = (next.channels?.feishu as FeishuConfig | undefined)?.groupAllowFrom ?? [];
334
+ const entry = await prompter.text({
335
+ message: "Group chat allowlist (chat_ids)",
336
+ placeholder: "oc_xxxxx, oc_yyyyy",
337
+ initialValue: existing.length > 0 ? existing.map(String).join(", ") : undefined,
252
338
  });
253
- if (useEnv) {
254
- next = updateFeishuConfig(next, accountId, { enabled: true, domain });
255
- return { cfg: next, accountId };
339
+ if (entry) {
340
+ const parts = parseAllowFromInput(String(entry));
341
+ if (parts.length > 0) {
342
+ next = setFeishuGroupAllowFrom(next, parts);
343
+ }
256
344
  }
257
345
  }
258
- const appId = String(
259
- await prompter.text({
260
- message: "Feishu App ID (cli_...)",
261
- initialValue: resolved.config.appId?.trim() || undefined,
262
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
263
- }),
264
- ).trim();
265
346
 
266
- const appSecret = String(
267
- await prompter.text({
268
- message: "Feishu App Secret",
269
- initialValue: resolved.config.appSecret?.trim() || undefined,
270
- validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
271
- }),
272
- ).trim();
347
+ return { cfg: next, accountId: DEFAULT_ACCOUNT_ID };
348
+ },
273
349
 
274
- next = updateFeishuConfig(next, accountId, { appId, appSecret, domain, enabled: true });
350
+ dmPolicy,
275
351
 
276
- return { cfg: next, accountId };
277
- },
352
+ disable: (cfg) => ({
353
+ ...cfg,
354
+ channels: {
355
+ ...cfg.channels,
356
+ feishu: { ...cfg.channels?.feishu, enabled: false },
357
+ },
358
+ }),
278
359
  };
@@ -0,0 +1,55 @@
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
2
+ import { sendMediaFeishu } from "./media.js";
3
+ import { getFeishuRuntime } from "./runtime.js";
4
+ import { sendMessageFeishu } from "./send.js";
5
+
6
+ export const feishuOutbound: ChannelOutboundAdapter = {
7
+ deliveryMode: "direct",
8
+ chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
9
+ chunkerMode: "markdown",
10
+ textChunkLimit: 4000,
11
+ sendText: async ({ cfg, to, text, accountId }) => {
12
+ const result = await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
13
+ return { channel: "feishu", ...result };
14
+ },
15
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
16
+ // Send text first if provided
17
+ if (text?.trim()) {
18
+ await sendMessageFeishu({ cfg, to, text, accountId: accountId ?? undefined });
19
+ }
20
+
21
+ // Upload and send media if URL provided
22
+ if (mediaUrl) {
23
+ try {
24
+ const result = await sendMediaFeishu({
25
+ cfg,
26
+ to,
27
+ mediaUrl,
28
+ accountId: accountId ?? undefined,
29
+ });
30
+ return { channel: "feishu", ...result };
31
+ } catch (err) {
32
+ // Log the error for debugging
33
+ console.error(`[feishu] sendMediaFeishu failed:`, err);
34
+ // Fallback to URL link if upload fails
35
+ const fallbackText = `📎 ${mediaUrl}`;
36
+ const result = await sendMessageFeishu({
37
+ cfg,
38
+ to,
39
+ text: fallbackText,
40
+ accountId: accountId ?? undefined,
41
+ });
42
+ return { channel: "feishu", ...result };
43
+ }
44
+ }
45
+
46
+ // No media URL, just return text result
47
+ const result = await sendMessageFeishu({
48
+ cfg,
49
+ to,
50
+ text: text ?? "",
51
+ accountId: accountId ?? undefined,
52
+ });
53
+ return { channel: "feishu", ...result };
54
+ },
55
+ };
@@ -0,0 +1,52 @@
1
+ import { Type, type Static } from "@sinclair/typebox";
2
+
3
+ const TokenType = Type.Union([
4
+ Type.Literal("doc"),
5
+ Type.Literal("docx"),
6
+ Type.Literal("sheet"),
7
+ Type.Literal("bitable"),
8
+ Type.Literal("folder"),
9
+ Type.Literal("file"),
10
+ Type.Literal("wiki"),
11
+ Type.Literal("mindnote"),
12
+ ]);
13
+
14
+ const MemberType = Type.Union([
15
+ Type.Literal("email"),
16
+ Type.Literal("openid"),
17
+ Type.Literal("userid"),
18
+ Type.Literal("unionid"),
19
+ Type.Literal("openchat"),
20
+ Type.Literal("opendepartmentid"),
21
+ ]);
22
+
23
+ const Permission = Type.Union([
24
+ Type.Literal("view"),
25
+ Type.Literal("edit"),
26
+ Type.Literal("full_access"),
27
+ ]);
28
+
29
+ export const FeishuPermSchema = Type.Union([
30
+ Type.Object({
31
+ action: Type.Literal("list"),
32
+ token: Type.String({ description: "File token" }),
33
+ type: TokenType,
34
+ }),
35
+ Type.Object({
36
+ action: Type.Literal("add"),
37
+ token: Type.String({ description: "File token" }),
38
+ type: TokenType,
39
+ member_type: MemberType,
40
+ member_id: Type.String({ description: "Member ID (email, open_id, user_id, etc.)" }),
41
+ perm: Permission,
42
+ }),
43
+ Type.Object({
44
+ action: Type.Literal("remove"),
45
+ token: Type.String({ description: "File token" }),
46
+ type: TokenType,
47
+ member_type: MemberType,
48
+ member_id: Type.String({ description: "Member ID to remove" }),
49
+ }),
50
+ ]);
51
+
52
+ export type FeishuPermParams = Static<typeof FeishuPermSchema>;