@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.2

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.
Files changed (114) hide show
  1. package/dist/index.d.ts +23 -0
  2. package/dist/index.js +45 -0
  3. package/dist/src/accounts.js +141 -0
  4. package/dist/src/app-scope-checker.js +36 -0
  5. package/dist/src/async.js +34 -0
  6. package/dist/src/auth-errors.js +72 -0
  7. package/dist/src/bitable.js +495 -0
  8. package/dist/src/bot.d.ts +35 -0
  9. package/dist/src/bot.js +941 -0
  10. package/dist/src/calendar-calendar.js +54 -0
  11. package/dist/src/calendar-event-attendee.js +98 -0
  12. package/dist/src/calendar-event.js +193 -0
  13. package/dist/src/calendar-freebusy.js +40 -0
  14. package/dist/src/calendar-shared.js +23 -0
  15. package/dist/src/calendar.js +16 -0
  16. package/dist/src/card-action.js +49 -0
  17. package/dist/src/channel.d.ts +7 -0
  18. package/dist/src/channel.js +413 -0
  19. package/dist/src/chat-schema.js +25 -0
  20. package/dist/src/chat.js +87 -0
  21. package/dist/src/client.d.ts +16 -0
  22. package/dist/src/client.js +112 -0
  23. package/dist/src/config-schema.d.ts +357 -0
  24. package/dist/src/dedup.js +126 -0
  25. package/dist/src/device-flow.js +109 -0
  26. package/dist/src/directory.js +101 -0
  27. package/dist/src/doc-schema.js +148 -0
  28. package/dist/src/docx-batch-insert.js +104 -0
  29. package/dist/src/docx-color-text.js +80 -0
  30. package/dist/src/docx-table-ops.js +197 -0
  31. package/dist/src/docx.js +858 -0
  32. package/dist/src/domains.js +14 -0
  33. package/dist/src/drive-schema.js +41 -0
  34. package/dist/src/drive.js +126 -0
  35. package/dist/src/dynamic-agent.js +93 -0
  36. package/dist/src/external-keys.js +13 -0
  37. package/dist/src/feishu-fetch.js +12 -0
  38. package/dist/src/identity.js +92 -0
  39. package/dist/src/lark-ticket.js +11 -0
  40. package/dist/src/media.d.ts +75 -0
  41. package/dist/src/media.js +304 -0
  42. package/dist/src/mention.d.ts +52 -0
  43. package/dist/src/mention.js +82 -0
  44. package/dist/src/monitor.account.d.ts +1 -0
  45. package/dist/src/monitor.account.js +393 -0
  46. package/dist/src/monitor.d.ts +11 -0
  47. package/dist/src/monitor.js +58 -0
  48. package/dist/src/monitor.startup.js +24 -0
  49. package/dist/src/monitor.state.d.ts +1 -0
  50. package/dist/src/monitor.state.js +80 -0
  51. package/dist/src/monitor.transport.js +167 -0
  52. package/dist/src/nextclaw-sdk/account-id.js +15 -0
  53. package/dist/src/nextclaw-sdk/core-channel.js +150 -0
  54. package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
  55. package/dist/src/nextclaw-sdk/dedupe.js +164 -0
  56. package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
  57. package/dist/src/nextclaw-sdk/feishu.js +14 -0
  58. package/dist/src/nextclaw-sdk/history.js +69 -0
  59. package/dist/src/nextclaw-sdk/network-body.js +180 -0
  60. package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
  61. package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
  62. package/dist/src/nextclaw-sdk/network.js +4 -0
  63. package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
  64. package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
  65. package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
  66. package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
  67. package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
  68. package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
  69. package/dist/src/nextclaw-sdk/secrets.js +4 -0
  70. package/dist/src/nextclaw-sdk/types.d.ts +242 -0
  71. package/dist/src/oauth.js +171 -0
  72. package/dist/src/onboarding.js +381 -0
  73. package/dist/src/outbound.js +150 -0
  74. package/dist/src/perm-schema.js +49 -0
  75. package/dist/src/perm.js +90 -0
  76. package/dist/src/policy.js +61 -0
  77. package/dist/src/post.js +160 -0
  78. package/dist/src/probe.d.ts +11 -0
  79. package/dist/src/probe.js +85 -0
  80. package/dist/src/raw-request.js +24 -0
  81. package/dist/src/reactions.d.ts +67 -0
  82. package/dist/src/reactions.js +91 -0
  83. package/dist/src/reply-dispatcher.js +250 -0
  84. package/dist/src/runtime.js +5 -0
  85. package/dist/src/secret-input.js +3 -0
  86. package/dist/src/send-result.js +12 -0
  87. package/dist/src/send-target.js +22 -0
  88. package/dist/src/send.d.ts +51 -0
  89. package/dist/src/send.js +265 -0
  90. package/dist/src/sheets-shared.js +193 -0
  91. package/dist/src/sheets.js +95 -0
  92. package/dist/src/streaming-card.js +263 -0
  93. package/dist/src/targets.js +39 -0
  94. package/dist/src/task-comment.js +76 -0
  95. package/dist/src/task-shared.js +13 -0
  96. package/dist/src/task-subtask.js +79 -0
  97. package/dist/src/task-task.js +144 -0
  98. package/dist/src/task-tasklist.js +136 -0
  99. package/dist/src/task.js +16 -0
  100. package/dist/src/token-store.js +154 -0
  101. package/dist/src/tool-account.js +65 -0
  102. package/dist/src/tool-result.js +18 -0
  103. package/dist/src/tool-scopes.js +62 -0
  104. package/dist/src/tools-config.js +30 -0
  105. package/dist/src/types.d.ts +43 -0
  106. package/dist/src/typing.js +145 -0
  107. package/dist/src/uat-client.js +102 -0
  108. package/dist/src/user-tool-client.js +132 -0
  109. package/dist/src/user-tool-helpers.js +110 -0
  110. package/dist/src/user-tool-result.js +10 -0
  111. package/dist/src/wiki-schema.js +45 -0
  112. package/dist/src/wiki.js +144 -0
  113. package/package.json +8 -4
  114. package/index.ts +0 -75
@@ -0,0 +1,381 @@
1
+ import { DEFAULT_ACCOUNT_ID } from "./nextclaw-sdk/account-id.js";
2
+ import { formatDocsLink } from "./nextclaw-sdk/core-pairing.js";
3
+ import { hasConfiguredSecretInput } from "./nextclaw-sdk/secrets-core.js";
4
+ import { buildSingleChannelSecretPromptState, mergeAllowFromEntries, setTopLevelChannelAllowFrom, setTopLevelChannelDmPolicyWithAllowFrom, setTopLevelChannelGroupPolicy, splitOnboardingEntries } from "./nextclaw-sdk/secrets-config.js";
5
+ import { promptSingleChannelSecretInput } from "./nextclaw-sdk/secrets-prompt.js";
6
+ import "./nextclaw-sdk/feishu.js";
7
+ import { resolveFeishuCredentials } from "./accounts.js";
8
+ import { probeFeishu } from "./probe.js";
9
+ //#region src/onboarding.ts
10
+ const channel = "feishu";
11
+ function normalizeString(value) {
12
+ if (typeof value !== "string") return;
13
+ return value.trim() || void 0;
14
+ }
15
+ function setFeishuDmPolicy(cfg, dmPolicy) {
16
+ return setTopLevelChannelDmPolicyWithAllowFrom({
17
+ cfg,
18
+ channel: "feishu",
19
+ dmPolicy
20
+ });
21
+ }
22
+ function setFeishuAllowFrom(cfg, allowFrom) {
23
+ return setTopLevelChannelAllowFrom({
24
+ cfg,
25
+ channel: "feishu",
26
+ allowFrom
27
+ });
28
+ }
29
+ async function promptFeishuAllowFrom(params) {
30
+ const existing = params.cfg.channels?.feishu?.allowFrom ?? [];
31
+ await params.prompter.note([
32
+ "Allowlist Feishu DMs by open_id or user_id.",
33
+ "You can find user open_id in Feishu admin console or via API.",
34
+ "Examples:",
35
+ "- ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
36
+ "- on_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
37
+ ].join("\n"), "Feishu allowlist");
38
+ while (true) {
39
+ const entry = await params.prompter.text({
40
+ message: "Feishu allowFrom (user open_ids)",
41
+ placeholder: "ou_xxxxx, ou_yyyyy",
42
+ initialValue: existing[0] ? String(existing[0]) : void 0,
43
+ validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
44
+ });
45
+ const parts = splitOnboardingEntries(String(entry));
46
+ if (parts.length === 0) {
47
+ await params.prompter.note("Enter at least one user.", "Feishu allowlist");
48
+ continue;
49
+ }
50
+ const unique = mergeAllowFromEntries(existing, parts);
51
+ return setFeishuAllowFrom(params.cfg, unique);
52
+ }
53
+ }
54
+ async function noteFeishuCredentialHelp(prompter) {
55
+ await prompter.note([
56
+ "1) Go to Feishu Open Platform (open.feishu.cn)",
57
+ "2) Create a self-built app",
58
+ "3) Get App ID and App Secret from Credentials page",
59
+ "4) Enable required permissions: im:message, im:chat, contact:user.base:readonly",
60
+ "5) Publish the app or add it to a test group",
61
+ "Tip: you can also set FEISHU_APP_ID / FEISHU_APP_SECRET env vars.",
62
+ `Docs: ${formatDocsLink("/channels/feishu", "feishu")}`
63
+ ].join("\n"), "Feishu credentials");
64
+ }
65
+ async function promptFeishuAppId(params) {
66
+ return String(await params.prompter.text({
67
+ message: "Enter Feishu App ID",
68
+ initialValue: params.initialValue,
69
+ validate: (value) => value?.trim() ? void 0 : "Required"
70
+ })).trim();
71
+ }
72
+ function setFeishuGroupPolicy(cfg, groupPolicy) {
73
+ return setTopLevelChannelGroupPolicy({
74
+ cfg,
75
+ channel: "feishu",
76
+ groupPolicy,
77
+ enabled: true
78
+ });
79
+ }
80
+ function setFeishuGroupAllowFrom(cfg, groupAllowFrom) {
81
+ return {
82
+ ...cfg,
83
+ channels: {
84
+ ...cfg.channels,
85
+ feishu: {
86
+ ...cfg.channels?.feishu,
87
+ groupAllowFrom
88
+ }
89
+ }
90
+ };
91
+ }
92
+ const feishuOnboardingAdapter = {
93
+ channel,
94
+ getStatus: async ({ cfg }) => {
95
+ const feishuCfg = cfg.channels?.feishu;
96
+ const isAppIdConfigured = (value) => {
97
+ if (normalizeString(value)) return true;
98
+ if (!value || typeof value !== "object") return false;
99
+ const rec = value;
100
+ const source = normalizeString(rec.source)?.toLowerCase();
101
+ const id = normalizeString(rec.id);
102
+ if (source === "env" && id) return Boolean(normalizeString(process.env[id]));
103
+ return hasConfiguredSecretInput(value);
104
+ };
105
+ const topLevelConfigured = Boolean(isAppIdConfigured(feishuCfg?.appId) && hasConfiguredSecretInput(feishuCfg?.appSecret));
106
+ const accountConfigured = Object.values(feishuCfg?.accounts ?? {}).some((account) => {
107
+ if (!account || typeof account !== "object") return false;
108
+ const hasOwnAppId = Object.prototype.hasOwnProperty.call(account, "appId");
109
+ const hasOwnAppSecret = Object.prototype.hasOwnProperty.call(account, "appSecret");
110
+ const accountAppIdConfigured = hasOwnAppId ? isAppIdConfigured(account.appId) : isAppIdConfigured(feishuCfg?.appId);
111
+ const accountSecretConfigured = hasOwnAppSecret ? hasConfiguredSecretInput(account.appSecret) : hasConfiguredSecretInput(feishuCfg?.appSecret);
112
+ return Boolean(accountAppIdConfigured && accountSecretConfigured);
113
+ });
114
+ const configured = topLevelConfigured || accountConfigured;
115
+ const resolvedCredentials = resolveFeishuCredentials(feishuCfg, { allowUnresolvedSecretRef: true });
116
+ let probeResult = null;
117
+ if (configured && resolvedCredentials) try {
118
+ probeResult = await probeFeishu(resolvedCredentials);
119
+ } catch {}
120
+ const statusLines = [];
121
+ if (!configured) statusLines.push("Feishu: needs app credentials");
122
+ else if (probeResult?.ok) statusLines.push(`Feishu: connected as ${probeResult.botName ?? probeResult.botOpenId ?? "bot"}`);
123
+ else statusLines.push("Feishu: configured (connection not verified)");
124
+ return {
125
+ channel,
126
+ configured,
127
+ statusLines,
128
+ selectionHint: configured ? "configured" : "needs app creds",
129
+ quickstartScore: configured ? 2 : 0
130
+ };
131
+ },
132
+ configure: async ({ cfg, prompter }) => {
133
+ const feishuCfg = cfg.channels?.feishu;
134
+ const resolved = resolveFeishuCredentials(feishuCfg, { allowUnresolvedSecretRef: true });
135
+ const hasConfigSecret = hasConfiguredSecretInput(feishuCfg?.appSecret);
136
+ const hasConfigCreds = Boolean(typeof feishuCfg?.appId === "string" && feishuCfg.appId.trim() && hasConfigSecret);
137
+ const appSecretPromptState = buildSingleChannelSecretPromptState({
138
+ accountConfigured: Boolean(resolved),
139
+ hasConfigToken: hasConfigSecret,
140
+ allowEnv: !hasConfigCreds && Boolean(process.env.FEISHU_APP_ID?.trim()),
141
+ envValue: process.env.FEISHU_APP_SECRET
142
+ });
143
+ let next = cfg;
144
+ let appId = null;
145
+ let appSecret = null;
146
+ let appSecretProbeValue = null;
147
+ if (!resolved) await noteFeishuCredentialHelp(prompter);
148
+ const appSecretResult = await promptSingleChannelSecretInput({
149
+ cfg: next,
150
+ prompter,
151
+ providerHint: "feishu",
152
+ credentialLabel: "App Secret",
153
+ accountConfigured: appSecretPromptState.accountConfigured,
154
+ canUseEnv: appSecretPromptState.canUseEnv,
155
+ hasConfigToken: appSecretPromptState.hasConfigToken,
156
+ envPrompt: "FEISHU_APP_ID + FEISHU_APP_SECRET detected. Use env vars?",
157
+ keepPrompt: "Feishu App Secret already configured. Keep it?",
158
+ inputPrompt: "Enter Feishu App Secret",
159
+ preferredEnvVar: "FEISHU_APP_SECRET"
160
+ });
161
+ if (appSecretResult.action === "use-env") next = {
162
+ ...next,
163
+ channels: {
164
+ ...next.channels,
165
+ feishu: {
166
+ ...next.channels?.feishu,
167
+ enabled: true
168
+ }
169
+ }
170
+ };
171
+ else if (appSecretResult.action === "set") {
172
+ appSecret = appSecretResult.value;
173
+ appSecretProbeValue = appSecretResult.resolvedValue;
174
+ appId = await promptFeishuAppId({
175
+ prompter,
176
+ initialValue: normalizeString(feishuCfg?.appId) ?? normalizeString(process.env.FEISHU_APP_ID)
177
+ });
178
+ }
179
+ if (appId && appSecret) {
180
+ next = {
181
+ ...next,
182
+ channels: {
183
+ ...next.channels,
184
+ feishu: {
185
+ ...next.channels?.feishu,
186
+ enabled: true,
187
+ appId,
188
+ appSecret
189
+ }
190
+ }
191
+ };
192
+ try {
193
+ const probe = await probeFeishu({
194
+ appId,
195
+ appSecret: appSecretProbeValue ?? void 0,
196
+ domain: (next.channels?.feishu)?.domain
197
+ });
198
+ if (probe.ok) await prompter.note(`Connected as ${probe.botName ?? probe.botOpenId ?? "bot"}`, "Feishu connection test");
199
+ else await prompter.note(`Connection failed: ${probe.error ?? "unknown error"}`, "Feishu connection test");
200
+ } catch (err) {
201
+ await prompter.note(`Connection test failed: ${String(err)}`, "Feishu connection test");
202
+ }
203
+ }
204
+ const currentMode = (next.channels?.feishu)?.connectionMode ?? "websocket";
205
+ const connectionMode = await prompter.select({
206
+ message: "Feishu connection mode",
207
+ options: [{
208
+ value: "websocket",
209
+ label: "WebSocket (default)"
210
+ }, {
211
+ value: "webhook",
212
+ label: "Webhook"
213
+ }],
214
+ initialValue: currentMode
215
+ });
216
+ next = {
217
+ ...next,
218
+ channels: {
219
+ ...next.channels,
220
+ feishu: {
221
+ ...next.channels?.feishu,
222
+ connectionMode
223
+ }
224
+ }
225
+ };
226
+ if (connectionMode === "webhook") {
227
+ const currentVerificationToken = (next.channels?.feishu)?.verificationToken;
228
+ const verificationTokenPromptState = buildSingleChannelSecretPromptState({
229
+ accountConfigured: hasConfiguredSecretInput(currentVerificationToken),
230
+ hasConfigToken: hasConfiguredSecretInput(currentVerificationToken),
231
+ allowEnv: false
232
+ });
233
+ const verificationTokenResult = await promptSingleChannelSecretInput({
234
+ cfg: next,
235
+ prompter,
236
+ providerHint: "feishu-webhook",
237
+ credentialLabel: "verification token",
238
+ accountConfigured: verificationTokenPromptState.accountConfigured,
239
+ canUseEnv: verificationTokenPromptState.canUseEnv,
240
+ hasConfigToken: verificationTokenPromptState.hasConfigToken,
241
+ envPrompt: "",
242
+ keepPrompt: "Feishu verification token already configured. Keep it?",
243
+ inputPrompt: "Enter Feishu verification token",
244
+ preferredEnvVar: "FEISHU_VERIFICATION_TOKEN"
245
+ });
246
+ if (verificationTokenResult.action === "set") next = {
247
+ ...next,
248
+ channels: {
249
+ ...next.channels,
250
+ feishu: {
251
+ ...next.channels?.feishu,
252
+ verificationToken: verificationTokenResult.value
253
+ }
254
+ }
255
+ };
256
+ const currentEncryptKey = (next.channels?.feishu)?.encryptKey;
257
+ const encryptKeyPromptState = buildSingleChannelSecretPromptState({
258
+ accountConfigured: hasConfiguredSecretInput(currentEncryptKey),
259
+ hasConfigToken: hasConfiguredSecretInput(currentEncryptKey),
260
+ allowEnv: false
261
+ });
262
+ const encryptKeyResult = await promptSingleChannelSecretInput({
263
+ cfg: next,
264
+ prompter,
265
+ providerHint: "feishu-webhook",
266
+ credentialLabel: "encrypt key",
267
+ accountConfigured: encryptKeyPromptState.accountConfigured,
268
+ canUseEnv: encryptKeyPromptState.canUseEnv,
269
+ hasConfigToken: encryptKeyPromptState.hasConfigToken,
270
+ envPrompt: "",
271
+ keepPrompt: "Feishu encrypt key already configured. Keep it?",
272
+ inputPrompt: "Enter Feishu encrypt key",
273
+ preferredEnvVar: "FEISHU_ENCRYPT_KEY"
274
+ });
275
+ if (encryptKeyResult.action === "set") next = {
276
+ ...next,
277
+ channels: {
278
+ ...next.channels,
279
+ feishu: {
280
+ ...next.channels?.feishu,
281
+ encryptKey: encryptKeyResult.value
282
+ }
283
+ }
284
+ };
285
+ const currentWebhookPath = (next.channels?.feishu)?.webhookPath;
286
+ const webhookPath = String(await prompter.text({
287
+ message: "Feishu webhook path",
288
+ initialValue: currentWebhookPath ?? "/feishu/events",
289
+ validate: (value) => String(value ?? "").trim() ? void 0 : "Required"
290
+ })).trim();
291
+ next = {
292
+ ...next,
293
+ channels: {
294
+ ...next.channels,
295
+ feishu: {
296
+ ...next.channels?.feishu,
297
+ webhookPath
298
+ }
299
+ }
300
+ };
301
+ }
302
+ const currentDomain = (next.channels?.feishu)?.domain ?? "feishu";
303
+ const domain = await prompter.select({
304
+ message: "Which Feishu domain?",
305
+ options: [{
306
+ value: "feishu",
307
+ label: "Feishu (feishu.cn) - China"
308
+ }, {
309
+ value: "lark",
310
+ label: "Lark (larksuite.com) - International"
311
+ }],
312
+ initialValue: currentDomain
313
+ });
314
+ if (domain) next = {
315
+ ...next,
316
+ channels: {
317
+ ...next.channels,
318
+ feishu: {
319
+ ...next.channels?.feishu,
320
+ domain
321
+ }
322
+ }
323
+ };
324
+ const groupPolicy = await prompter.select({
325
+ message: "Group chat policy",
326
+ options: [
327
+ {
328
+ value: "allowlist",
329
+ label: "Allowlist - only respond in specific groups"
330
+ },
331
+ {
332
+ value: "open",
333
+ label: "Open - respond in all groups (requires mention)"
334
+ },
335
+ {
336
+ value: "disabled",
337
+ label: "Disabled - don't respond in groups"
338
+ }
339
+ ],
340
+ initialValue: (next.channels?.feishu)?.groupPolicy ?? "allowlist"
341
+ });
342
+ if (groupPolicy) next = setFeishuGroupPolicy(next, groupPolicy);
343
+ if (groupPolicy === "allowlist") {
344
+ const existing = (next.channels?.feishu)?.groupAllowFrom ?? [];
345
+ const entry = await prompter.text({
346
+ message: "Group chat allowlist (chat_ids)",
347
+ placeholder: "oc_xxxxx, oc_yyyyy",
348
+ initialValue: existing.length > 0 ? existing.map(String).join(", ") : void 0
349
+ });
350
+ if (entry) {
351
+ const parts = splitOnboardingEntries(String(entry));
352
+ if (parts.length > 0) next = setFeishuGroupAllowFrom(next, parts);
353
+ }
354
+ }
355
+ return {
356
+ cfg: next,
357
+ accountId: DEFAULT_ACCOUNT_ID
358
+ };
359
+ },
360
+ dmPolicy: {
361
+ label: "Feishu",
362
+ channel,
363
+ policyKey: "channels.feishu.dmPolicy",
364
+ allowFromKey: "channels.feishu.allowFrom",
365
+ getCurrent: (cfg) => (cfg.channels?.feishu)?.dmPolicy ?? "pairing",
366
+ setPolicy: (cfg, policy) => setFeishuDmPolicy(cfg, policy),
367
+ promptAllowFrom: promptFeishuAllowFrom
368
+ },
369
+ disable: (cfg) => ({
370
+ ...cfg,
371
+ channels: {
372
+ ...cfg.channels,
373
+ feishu: {
374
+ ...cfg.channels?.feishu,
375
+ enabled: false
376
+ }
377
+ }
378
+ })
379
+ };
380
+ //#endregion
381
+ export { feishuOnboardingAdapter };
@@ -0,0 +1,150 @@
1
+ import { resolveFeishuAccount } from "./accounts.js";
2
+ import { getFeishuRuntime } from "./runtime.js";
3
+ import { sendMediaFeishu } from "./media.js";
4
+ import { sendMarkdownCardFeishu, sendMessageFeishu } from "./send.js";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ //#region src/outbound.ts
8
+ function normalizePossibleLocalImagePath(text) {
9
+ const raw = text?.trim();
10
+ if (!raw) return null;
11
+ if (/\s/.test(raw)) return null;
12
+ if (/^(https?:\/\/|data:|file:\/\/)/i.test(raw)) return null;
13
+ const ext = path.extname(raw).toLowerCase();
14
+ if (![
15
+ ".jpg",
16
+ ".jpeg",
17
+ ".png",
18
+ ".gif",
19
+ ".webp",
20
+ ".bmp",
21
+ ".ico",
22
+ ".tiff"
23
+ ].includes(ext)) return null;
24
+ if (!path.isAbsolute(raw)) return null;
25
+ if (!fs.existsSync(raw)) return null;
26
+ try {
27
+ if (!fs.statSync(raw).isFile()) return null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ return raw;
32
+ }
33
+ function shouldUseCard(text) {
34
+ return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
35
+ }
36
+ function resolveReplyToMessageId(params) {
37
+ const replyToId = params.replyToId?.trim();
38
+ if (replyToId) return replyToId;
39
+ if (params.threadId == null) return;
40
+ return String(params.threadId).trim() || void 0;
41
+ }
42
+ async function sendOutboundText(params) {
43
+ const { cfg, to, text, accountId, replyToMessageId } = params;
44
+ const renderMode = resolveFeishuAccount({
45
+ cfg,
46
+ accountId
47
+ }).config?.renderMode ?? "auto";
48
+ if (renderMode === "card" || renderMode === "auto" && shouldUseCard(text)) return sendMarkdownCardFeishu({
49
+ cfg,
50
+ to,
51
+ text,
52
+ accountId,
53
+ replyToMessageId
54
+ });
55
+ return sendMessageFeishu({
56
+ cfg,
57
+ to,
58
+ text,
59
+ accountId,
60
+ replyToMessageId
61
+ });
62
+ }
63
+ const feishuOutbound = {
64
+ deliveryMode: "direct",
65
+ chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
66
+ chunkerMode: "markdown",
67
+ textChunkLimit: 4e3,
68
+ sendText: async ({ cfg, to, text, accountId, replyToId, threadId, mediaLocalRoots }) => {
69
+ const replyToMessageId = resolveReplyToMessageId({
70
+ replyToId,
71
+ threadId
72
+ });
73
+ const localImagePath = normalizePossibleLocalImagePath(text);
74
+ if (localImagePath) try {
75
+ return {
76
+ channel: "feishu",
77
+ ...await sendMediaFeishu({
78
+ cfg,
79
+ to,
80
+ mediaUrl: localImagePath,
81
+ accountId: accountId ?? void 0,
82
+ replyToMessageId,
83
+ mediaLocalRoots
84
+ })
85
+ };
86
+ } catch (err) {
87
+ console.error(`[feishu] local image path auto-send failed:`, err);
88
+ }
89
+ return {
90
+ channel: "feishu",
91
+ ...await sendOutboundText({
92
+ cfg,
93
+ to,
94
+ text,
95
+ accountId: accountId ?? void 0,
96
+ replyToMessageId
97
+ })
98
+ };
99
+ },
100
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots, replyToId, threadId }) => {
101
+ const replyToMessageId = resolveReplyToMessageId({
102
+ replyToId,
103
+ threadId
104
+ });
105
+ if (text?.trim()) await sendOutboundText({
106
+ cfg,
107
+ to,
108
+ text,
109
+ accountId: accountId ?? void 0,
110
+ replyToMessageId
111
+ });
112
+ if (mediaUrl) try {
113
+ return {
114
+ channel: "feishu",
115
+ ...await sendMediaFeishu({
116
+ cfg,
117
+ to,
118
+ mediaUrl,
119
+ accountId: accountId ?? void 0,
120
+ mediaLocalRoots,
121
+ replyToMessageId
122
+ })
123
+ };
124
+ } catch (err) {
125
+ console.error(`[feishu] sendMediaFeishu failed:`, err);
126
+ return {
127
+ channel: "feishu",
128
+ ...await sendOutboundText({
129
+ cfg,
130
+ to,
131
+ text: `📎 ${mediaUrl}`,
132
+ accountId: accountId ?? void 0,
133
+ replyToMessageId
134
+ })
135
+ };
136
+ }
137
+ return {
138
+ channel: "feishu",
139
+ ...await sendOutboundText({
140
+ cfg,
141
+ to,
142
+ text: text ?? "",
143
+ accountId: accountId ?? void 0,
144
+ replyToMessageId
145
+ })
146
+ };
147
+ }
148
+ };
149
+ //#endregion
150
+ export { feishuOutbound };
@@ -0,0 +1,49 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ //#region src/perm-schema.ts
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
+ const MemberType = Type.Union([
14
+ Type.Literal("email"),
15
+ Type.Literal("openid"),
16
+ Type.Literal("userid"),
17
+ Type.Literal("unionid"),
18
+ Type.Literal("openchat"),
19
+ Type.Literal("opendepartmentid")
20
+ ]);
21
+ const Permission = Type.Union([
22
+ Type.Literal("view"),
23
+ Type.Literal("edit"),
24
+ Type.Literal("full_access")
25
+ ]);
26
+ const FeishuPermSchema = Type.Union([
27
+ Type.Object({
28
+ action: Type.Literal("list"),
29
+ token: Type.String({ description: "File token" }),
30
+ type: TokenType
31
+ }),
32
+ Type.Object({
33
+ action: Type.Literal("add"),
34
+ token: Type.String({ description: "File token" }),
35
+ type: TokenType,
36
+ member_type: MemberType,
37
+ member_id: Type.String({ description: "Member ID (email, open_id, user_id, etc.)" }),
38
+ perm: Permission
39
+ }),
40
+ Type.Object({
41
+ action: Type.Literal("remove"),
42
+ token: Type.String({ description: "File token" }),
43
+ type: TokenType,
44
+ member_type: MemberType,
45
+ member_id: Type.String({ description: "Member ID to remove" })
46
+ })
47
+ ]);
48
+ //#endregion
49
+ export { FeishuPermSchema };
@@ -0,0 +1,90 @@
1
+ import { jsonToolResult, toolExecutionErrorResult, unknownToolActionResult } from "./tool-result.js";
2
+ import { createFeishuToolClient, resolveRegisteredFeishuToolsConfig } from "./tool-account.js";
3
+ import { FeishuPermSchema } from "./perm-schema.js";
4
+ //#region src/perm.ts
5
+ async function listMembers(client, token, type) {
6
+ const res = await client.drive.permissionMember.list({
7
+ path: { token },
8
+ params: { type }
9
+ });
10
+ if (res.code !== 0) throw new Error(res.msg);
11
+ return { members: res.data?.items?.map((m) => ({
12
+ member_type: m.member_type,
13
+ member_id: m.member_id,
14
+ perm: m.perm,
15
+ name: m.name
16
+ })) ?? [] };
17
+ }
18
+ async function addMember(client, token, type, memberType, memberId, perm) {
19
+ const res = await client.drive.permissionMember.create({
20
+ path: { token },
21
+ params: {
22
+ type,
23
+ need_notification: false
24
+ },
25
+ data: {
26
+ member_type: memberType,
27
+ member_id: memberId,
28
+ perm
29
+ }
30
+ });
31
+ if (res.code !== 0) throw new Error(res.msg);
32
+ return {
33
+ success: true,
34
+ member: res.data?.member
35
+ };
36
+ }
37
+ async function removeMember(client, token, type, memberType, memberId) {
38
+ const res = await client.drive.permissionMember.delete({
39
+ path: {
40
+ token,
41
+ member_id: memberId
42
+ },
43
+ params: {
44
+ type,
45
+ member_type: memberType
46
+ }
47
+ });
48
+ if (res.code !== 0) throw new Error(res.msg);
49
+ return { success: true };
50
+ }
51
+ function registerFeishuPermTools(api) {
52
+ if (!api.config) {
53
+ api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
54
+ return;
55
+ }
56
+ if (!resolveRegisteredFeishuToolsConfig(api.config).perm) {
57
+ api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
58
+ return;
59
+ }
60
+ api.registerTool((ctx) => {
61
+ const defaultAccountId = ctx.agentAccountId;
62
+ return {
63
+ name: "feishu_perm",
64
+ label: "Feishu Perm",
65
+ description: "Feishu permission management. Actions: list, add, remove",
66
+ parameters: FeishuPermSchema,
67
+ async execute(_toolCallId, params) {
68
+ const p = params;
69
+ try {
70
+ const client = createFeishuToolClient({
71
+ api,
72
+ executeParams: p,
73
+ defaultAccountId
74
+ });
75
+ switch (p.action) {
76
+ case "list": return jsonToolResult(await listMembers(client, p.token, p.type));
77
+ case "add": return jsonToolResult(await addMember(client, p.token, p.type, p.member_type, p.member_id, p.perm));
78
+ case "remove": return jsonToolResult(await removeMember(client, p.token, p.type, p.member_type, p.member_id));
79
+ default: return unknownToolActionResult(p.action);
80
+ }
81
+ } catch (err) {
82
+ return toolExecutionErrorResult(err);
83
+ }
84
+ }
85
+ };
86
+ }, { name: "feishu_perm" });
87
+ api.logger.info?.(`feishu_perm: Registered feishu_perm tool`);
88
+ }
89
+ //#endregion
90
+ export { registerFeishuPermTools };