@nextclaw/channel-plugin-feishu 0.2.13 → 0.2.15

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 (120) hide show
  1. package/README.md +3 -1
  2. package/index.ts +65 -0
  3. package/openclaw.plugin.json +3 -7
  4. package/package.json +32 -9
  5. package/skills/feishu-doc/SKILL.md +211 -0
  6. package/skills/feishu-doc/references/block-types.md +103 -0
  7. package/skills/feishu-drive/SKILL.md +97 -0
  8. package/skills/feishu-perm/SKILL.md +119 -0
  9. package/skills/feishu-wiki/SKILL.md +111 -0
  10. package/src/accounts.test.ts +371 -0
  11. package/src/accounts.ts +244 -0
  12. package/src/async.ts +62 -0
  13. package/src/bitable.ts +725 -0
  14. package/src/bot.card-action.test.ts +63 -0
  15. package/src/bot.checkBotMentioned.test.ts +193 -0
  16. package/src/bot.stripBotMention.test.ts +134 -0
  17. package/src/bot.test.ts +2107 -0
  18. package/src/bot.ts +1556 -0
  19. package/src/card-action.ts +79 -0
  20. package/src/channel.test.ts +48 -0
  21. package/src/channel.ts +369 -0
  22. package/src/chat-schema.ts +24 -0
  23. package/src/chat.test.ts +89 -0
  24. package/src/chat.ts +130 -0
  25. package/src/client.test.ts +324 -0
  26. package/src/client.ts +196 -0
  27. package/src/config-schema.test.ts +247 -0
  28. package/src/config-schema.ts +306 -0
  29. package/src/dedup.ts +203 -0
  30. package/src/directory.test.ts +40 -0
  31. package/src/directory.ts +156 -0
  32. package/src/doc-schema.ts +182 -0
  33. package/src/docx-batch-insert.test.ts +90 -0
  34. package/src/docx-batch-insert.ts +187 -0
  35. package/src/docx-color-text.ts +149 -0
  36. package/src/docx-table-ops.ts +298 -0
  37. package/src/docx.account-selection.test.ts +70 -0
  38. package/src/docx.test.ts +445 -0
  39. package/src/docx.ts +1460 -0
  40. package/src/drive-schema.ts +46 -0
  41. package/src/drive.ts +228 -0
  42. package/src/dynamic-agent.ts +131 -0
  43. package/src/external-keys.test.ts +20 -0
  44. package/src/external-keys.ts +19 -0
  45. package/src/feishu-command-handler.ts +59 -0
  46. package/src/media.test.ts +523 -0
  47. package/src/media.ts +484 -0
  48. package/src/mention.ts +133 -0
  49. package/src/monitor.account.ts +562 -0
  50. package/src/monitor.reaction.test.ts +653 -0
  51. package/src/monitor.startup.test.ts +190 -0
  52. package/src/monitor.startup.ts +64 -0
  53. package/src/monitor.state.defaults.test.ts +46 -0
  54. package/src/monitor.state.ts +155 -0
  55. package/src/monitor.test-mocks.ts +45 -0
  56. package/src/monitor.transport.ts +264 -0
  57. package/src/monitor.ts +95 -0
  58. package/src/monitor.webhook-e2e.test.ts +214 -0
  59. package/src/monitor.webhook-security.test.ts +142 -0
  60. package/src/monitor.webhook.test-helpers.ts +98 -0
  61. package/src/nextclaw-sdk/account-id.ts +31 -0
  62. package/src/nextclaw-sdk/compat.ts +8 -0
  63. package/src/nextclaw-sdk/core-channel.ts +296 -0
  64. package/src/nextclaw-sdk/core-pairing.ts +224 -0
  65. package/src/nextclaw-sdk/core.ts +26 -0
  66. package/src/nextclaw-sdk/dedupe.ts +246 -0
  67. package/src/nextclaw-sdk/feishu.ts +77 -0
  68. package/src/nextclaw-sdk/history.ts +127 -0
  69. package/src/nextclaw-sdk/network-body.ts +245 -0
  70. package/src/nextclaw-sdk/network-fetch.ts +129 -0
  71. package/src/nextclaw-sdk/network-webhook.ts +182 -0
  72. package/src/nextclaw-sdk/network.ts +13 -0
  73. package/src/nextclaw-sdk/runtime-store.ts +26 -0
  74. package/src/nextclaw-sdk/secrets-config.ts +109 -0
  75. package/src/nextclaw-sdk/secrets-core.ts +170 -0
  76. package/src/nextclaw-sdk/secrets-prompt.ts +305 -0
  77. package/src/nextclaw-sdk/secrets.ts +18 -0
  78. package/src/nextclaw-sdk/types.ts +300 -0
  79. package/src/onboarding.status.test.ts +25 -0
  80. package/src/onboarding.test.ts +143 -0
  81. package/src/onboarding.ts +489 -0
  82. package/src/outbound.test.ts +356 -0
  83. package/src/outbound.ts +176 -0
  84. package/src/perm-schema.ts +52 -0
  85. package/src/perm.ts +176 -0
  86. package/src/policy.test.ts +154 -0
  87. package/src/policy.ts +123 -0
  88. package/src/post.test.ts +105 -0
  89. package/src/post.ts +274 -0
  90. package/src/probe.test.ts +270 -0
  91. package/src/probe.ts +156 -0
  92. package/src/reactions.ts +153 -0
  93. package/src/reply-dispatcher.test.ts +513 -0
  94. package/src/reply-dispatcher.ts +397 -0
  95. package/src/runtime.ts +6 -0
  96. package/src/secret-input.ts +13 -0
  97. package/src/send-message.ts +71 -0
  98. package/src/send-result.ts +29 -0
  99. package/src/send-target.test.ts +74 -0
  100. package/src/send-target.ts +29 -0
  101. package/src/send.reply-fallback.test.ts +189 -0
  102. package/src/send.test.ts +168 -0
  103. package/src/send.ts +481 -0
  104. package/src/streaming-card.test.ts +54 -0
  105. package/src/streaming-card.ts +374 -0
  106. package/src/targets.test.ts +70 -0
  107. package/src/targets.ts +107 -0
  108. package/src/tool-account-routing.test.ts +129 -0
  109. package/src/tool-account.ts +70 -0
  110. package/src/tool-factory-test-harness.ts +76 -0
  111. package/src/tool-result.test.ts +32 -0
  112. package/src/tool-result.ts +14 -0
  113. package/src/tools-config.test.ts +21 -0
  114. package/src/tools-config.ts +22 -0
  115. package/src/types.ts +103 -0
  116. package/src/typing.test.ts +144 -0
  117. package/src/typing.ts +210 -0
  118. package/src/wiki-schema.ts +55 -0
  119. package/src/wiki.ts +233 -0
  120. package/index.js +0 -27
@@ -0,0 +1,300 @@
1
+ export type LogFn = (message: string) => void;
2
+
3
+ export type SecretRefSource = "env" | "file" | "exec";
4
+
5
+ export type SecretRef = {
6
+ source: SecretRefSource;
7
+ provider: string;
8
+ id: string;
9
+ };
10
+
11
+ export type SecretInput = string | SecretRef;
12
+
13
+ export type DmPolicy = "open" | "pairing" | "allowlist";
14
+ export type GroupPolicy = "open" | "allowlist" | "disabled";
15
+
16
+ export type AllowlistMatch<TSource extends string = string> = {
17
+ allowed: boolean;
18
+ matchKey?: string;
19
+ matchSource?: TSource;
20
+ };
21
+
22
+ export type BaseProbeResult<TTarget = string> = {
23
+ ok: boolean;
24
+ target?: TTarget;
25
+ error?: string;
26
+ [key: string]: unknown;
27
+ };
28
+
29
+ export type ReplyPayload = {
30
+ text?: string;
31
+ mediaUrl?: string;
32
+ mediaUrls?: string[];
33
+ [key: string]: unknown;
34
+ };
35
+
36
+ export type HistoryEntry = {
37
+ sender: string;
38
+ body: string;
39
+ timestamp?: number;
40
+ messageId?: string;
41
+ };
42
+
43
+ export type GroupToolPolicyConfig = Record<string, unknown>;
44
+
45
+ export type ChannelGroupContext = {
46
+ cfg: ClawdbotConfig;
47
+ groupId?: string | null;
48
+ [key: string]: unknown;
49
+ };
50
+
51
+ export type ChannelMeta = {
52
+ id: string;
53
+ label: string;
54
+ selectionLabel?: string;
55
+ docsPath?: string;
56
+ docsLabel?: string;
57
+ blurb?: string;
58
+ aliases?: string[];
59
+ order?: number;
60
+ [key: string]: unknown;
61
+ };
62
+
63
+ export type ChannelPlugin<ResolvedAccount = unknown> = {
64
+ id: string;
65
+ meta: ChannelMeta;
66
+ config?: Record<string, unknown>;
67
+ onboarding?: ChannelOnboardingAdapter;
68
+ [key: string]: unknown;
69
+ __resolvedAccountType__?: ResolvedAccount;
70
+ };
71
+
72
+ export type ChannelOutboundAdapter = Record<string, unknown>;
73
+
74
+ export type AnyAgentTool = {
75
+ name?: string;
76
+ description?: string;
77
+ parameters?: Record<string, unknown>;
78
+ execute?: (toolCallId: string, params: unknown) => Promise<unknown> | unknown;
79
+ [key: string]: unknown;
80
+ };
81
+
82
+ export type ResolvedAgentRoute = {
83
+ agentId?: string;
84
+ sessionKey?: string;
85
+ [key: string]: unknown;
86
+ };
87
+
88
+ export type InboundDebouncer<T> = {
89
+ run: (key: string, value: T, task: (value: T) => Promise<void>) => void;
90
+ clear: () => void;
91
+ };
92
+
93
+ export type PluginRuntime = {
94
+ version?: string;
95
+ config: {
96
+ loadConfig?: () => OpenClawConfig;
97
+ writeConfigFile: (next: OpenClawConfig) => Promise<void>;
98
+ };
99
+ logging: {
100
+ shouldLogVerbose: () => boolean;
101
+ };
102
+ media: {
103
+ detectMime: (params: { buffer: Buffer }) => Promise<string | undefined>;
104
+ loadWebMedia: (
105
+ url: string,
106
+ options?: Record<string, unknown>,
107
+ ) => Promise<Record<string, unknown>>;
108
+ };
109
+ channel: {
110
+ media: {
111
+ fetchRemoteMedia: (params: {
112
+ url: string;
113
+ maxBytes?: number;
114
+ }) => Promise<Record<string, unknown>>;
115
+ saveMediaBuffer: (
116
+ buffer: Buffer,
117
+ contentType?: string,
118
+ direction?: string,
119
+ maxBytes?: number,
120
+ ) => Promise<{ path: string; contentType?: string }>;
121
+ };
122
+ text: {
123
+ chunkMarkdownText: (text: string, limit: number) => string[];
124
+ resolveMarkdownTableMode: (params: Record<string, unknown>) => unknown;
125
+ convertMarkdownTables: (text: string, mode?: unknown) => string;
126
+ resolveTextChunkLimit: (
127
+ cfg: OpenClawConfig,
128
+ channel: string,
129
+ accountId?: string,
130
+ options?: Record<string, unknown>,
131
+ ) => number;
132
+ resolveChunkMode: (cfg: OpenClawConfig, channel: string) => string;
133
+ chunkTextWithMode: (text: string, limit: number, mode?: unknown) => string[];
134
+ hasControlCommand: (text: string, cfg: OpenClawConfig) => boolean;
135
+ };
136
+ reply: {
137
+ resolveEnvelopeFormatOptions: (cfg: OpenClawConfig) => Record<string, unknown>;
138
+ formatAgentEnvelope: (params: Record<string, unknown>) => string;
139
+ finalizeInboundContext: (params: Record<string, unknown>) => Record<string, unknown>;
140
+ withReplyDispatcher: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
141
+ dispatchReplyFromConfig: (params: Record<string, unknown>) => Promise<Record<string, unknown>>;
142
+ createReplyDispatcherWithTyping: (params: Record<string, unknown>) => {
143
+ dispatcher: unknown;
144
+ replyOptions: Record<string, unknown>;
145
+ markDispatchIdle: () => void;
146
+ };
147
+ resolveHumanDelayConfig: (cfg: OpenClawConfig, agentId: string) => unknown;
148
+ dispatchReplyWithBufferedBlockDispatcher?: (
149
+ params: Record<string, unknown>,
150
+ ) => Promise<void>;
151
+ };
152
+ routing: {
153
+ resolveAgentRoute: (params: Record<string, unknown>) => ResolvedAgentRoute | null;
154
+ };
155
+ pairing: {
156
+ readAllowFromStore: (params: { channel: string; accountId?: string }) => unknown;
157
+ upsertPairingRequest: (params: Record<string, unknown>) => Promise<{
158
+ code: string;
159
+ created: boolean;
160
+ }>;
161
+ };
162
+ commands: {
163
+ shouldComputeCommandAuthorized: (text: string) => boolean;
164
+ resolveCommandAuthorizedFromAuthorizers: (
165
+ params: Record<string, unknown>,
166
+ ) => Promise<boolean> | boolean;
167
+ };
168
+ debounce: {
169
+ resolveInboundDebounceMs: (params: Record<string, unknown>) => number;
170
+ createInboundDebouncer: <T>(params: Record<string, unknown>) => InboundDebouncer<T>;
171
+ };
172
+ };
173
+ [key: string]: unknown;
174
+ };
175
+
176
+ export type RuntimeEnv = {
177
+ log?: (message: string) => void;
178
+ error?: (message: string) => void;
179
+ warn?: (message: string) => void;
180
+ debug?: (message: string) => void;
181
+ info?: (message: string) => void;
182
+ [key: string]: unknown;
183
+ };
184
+
185
+ export type OpenClawPluginApi = {
186
+ config: ClawdbotConfig;
187
+ runtime: PluginRuntime;
188
+ logger: {
189
+ info: LogFn;
190
+ warn: LogFn;
191
+ error: LogFn;
192
+ debug?: LogFn;
193
+ };
194
+ registerTool: (
195
+ tool:
196
+ | AnyAgentTool
197
+ | ((ctx: Record<string, unknown>) => AnyAgentTool | AnyAgentTool[] | null | undefined),
198
+ opts?: { name?: string; names?: string[]; optional?: boolean },
199
+ ) => void;
200
+ registerChannel: (registration: unknown) => void;
201
+ [key: string]: unknown;
202
+ };
203
+
204
+ export type WizardSelectOption<T extends string = string> = {
205
+ value: T;
206
+ label: string;
207
+ hint?: string;
208
+ };
209
+
210
+ export type WizardPrompter = {
211
+ note: (message: string, title?: string) => Promise<void>;
212
+ text: (params: {
213
+ message: string;
214
+ placeholder?: string;
215
+ initialValue?: string;
216
+ validate?: (value: string) => string | undefined;
217
+ }) => Promise<string>;
218
+ confirm: (params: { message: string; initialValue?: boolean }) => Promise<boolean>;
219
+ select: <T extends string = string>(params: {
220
+ message: string;
221
+ options: WizardSelectOption<T>[];
222
+ initialValue?: T | string;
223
+ }) => Promise<T>;
224
+ };
225
+
226
+ export type ChannelOnboardingDmPolicy = {
227
+ label: string;
228
+ channel: string;
229
+ policyKey: string;
230
+ allowFromKey: string;
231
+ getCurrent: (cfg: ClawdbotConfig) => DmPolicy;
232
+ setPolicy: (cfg: ClawdbotConfig, policy: DmPolicy) => ClawdbotConfig;
233
+ promptAllowFrom: (params: {
234
+ cfg: ClawdbotConfig;
235
+ prompter: WizardPrompter;
236
+ }) => Promise<ClawdbotConfig>;
237
+ };
238
+
239
+ export type ChannelOnboardingAdapter = {
240
+ channel: string;
241
+ getStatus: (params: { cfg: ClawdbotConfig }) => Promise<{
242
+ channel: string;
243
+ configured: boolean;
244
+ statusLines: string[];
245
+ selectionHint?: string;
246
+ quickstartScore?: number;
247
+ }>;
248
+ configure: (params: {
249
+ cfg: ClawdbotConfig;
250
+ prompter: WizardPrompter;
251
+ }) => Promise<{ cfg: ClawdbotConfig; accountId?: string }>;
252
+ dmPolicy?: ChannelOnboardingDmPolicy;
253
+ disable?: (cfg: ClawdbotConfig) => ClawdbotConfig;
254
+ };
255
+
256
+ export type OpenClawConfig = {
257
+ channels?: Record<string, Record<string, unknown> | undefined>;
258
+ bindings?: Array<{
259
+ agentId?: string;
260
+ match?: {
261
+ channel?: string;
262
+ peer?: {
263
+ kind?: string;
264
+ id?: string;
265
+ };
266
+ };
267
+ }>;
268
+ agents?: {
269
+ list?: Array<{
270
+ id: string;
271
+ workspace?: string;
272
+ agentDir?: string;
273
+ [key: string]: unknown;
274
+ }>;
275
+ [key: string]: unknown;
276
+ };
277
+ broadcast?: Record<string, string[]>;
278
+ secrets?: {
279
+ defaults?: {
280
+ env?: string;
281
+ file?: string;
282
+ exec?: string;
283
+ };
284
+ providers?: Record<
285
+ string,
286
+ {
287
+ source?: SecretRefSource;
288
+ path?: string;
289
+ mode?: "singleValue" | "json";
290
+ command?: string;
291
+ args?: string[];
292
+ [key: string]: unknown;
293
+ }
294
+ >;
295
+ [key: string]: unknown;
296
+ };
297
+ [key: string]: unknown;
298
+ };
299
+
300
+ export type ClawdbotConfig = OpenClawConfig;
@@ -0,0 +1,25 @@
1
+ import type { OpenClawConfig } from "./nextclaw-sdk/feishu.js";
2
+ import { describe, expect, it } from "vitest";
3
+ import { feishuOnboardingAdapter } from "./onboarding.js";
4
+
5
+ describe("feishu onboarding status", () => {
6
+ it("treats SecretRef appSecret as configured when appId is present", async () => {
7
+ const status = await feishuOnboardingAdapter.getStatus({
8
+ cfg: {
9
+ channels: {
10
+ feishu: {
11
+ appId: "cli_a123456",
12
+ appSecret: {
13
+ source: "env",
14
+ provider: "default",
15
+ id: "FEISHU_APP_SECRET",
16
+ },
17
+ },
18
+ },
19
+ } as OpenClawConfig,
20
+ accountOverrides: {},
21
+ });
22
+
23
+ expect(status.configured).toBe(true);
24
+ });
25
+ });
@@ -0,0 +1,143 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("./probe.js", () => ({
4
+ probeFeishu: vi.fn(async () => ({ ok: false, error: "mocked" })),
5
+ }));
6
+
7
+ import { feishuOnboardingAdapter } from "./onboarding.js";
8
+
9
+ const baseConfigureContext = {
10
+ runtime: {} as never,
11
+ accountOverrides: {},
12
+ shouldPromptAccountIds: false,
13
+ forceAllowFrom: false,
14
+ };
15
+
16
+ const baseStatusContext = {
17
+ accountOverrides: {},
18
+ };
19
+
20
+ async function withEnvVars(values: Record<string, string | undefined>, run: () => Promise<void>) {
21
+ const previous = new Map<string, string | undefined>();
22
+ for (const [key, value] of Object.entries(values)) {
23
+ previous.set(key, process.env[key]);
24
+ if (value === undefined) {
25
+ delete process.env[key];
26
+ } else {
27
+ process.env[key] = value;
28
+ }
29
+ }
30
+
31
+ try {
32
+ await run();
33
+ } finally {
34
+ for (const [key, prior] of previous.entries()) {
35
+ if (prior === undefined) {
36
+ delete process.env[key];
37
+ } else {
38
+ process.env[key] = prior;
39
+ }
40
+ }
41
+ }
42
+ }
43
+
44
+ async function getStatusWithEnvRefs(params: { appIdKey: string; appSecretKey: string }) {
45
+ return await feishuOnboardingAdapter.getStatus({
46
+ cfg: {
47
+ channels: {
48
+ feishu: {
49
+ appId: { source: "env", id: params.appIdKey, provider: "default" },
50
+ appSecret: { source: "env", id: params.appSecretKey, provider: "default" },
51
+ },
52
+ },
53
+ } as never,
54
+ ...baseStatusContext,
55
+ });
56
+ }
57
+
58
+ describe("feishuOnboardingAdapter.configure", () => {
59
+ it("does not throw when config appId/appSecret are SecretRef objects", async () => {
60
+ const text = vi
61
+ .fn()
62
+ .mockResolvedValueOnce("cli_from_prompt")
63
+ .mockResolvedValueOnce("secret_from_prompt")
64
+ .mockResolvedValueOnce("oc_group_1");
65
+
66
+ const prompter = {
67
+ note: vi.fn(async () => undefined),
68
+ text,
69
+ confirm: vi.fn(async () => true),
70
+ select: vi.fn(
71
+ async ({ initialValue }: { initialValue?: string }) => initialValue ?? "allowlist",
72
+ ),
73
+ } as never;
74
+
75
+ await expect(
76
+ feishuOnboardingAdapter.configure({
77
+ cfg: {
78
+ channels: {
79
+ feishu: {
80
+ appId: { source: "env", id: "FEISHU_APP_ID", provider: "default" },
81
+ appSecret: { source: "env", id: "FEISHU_APP_SECRET", provider: "default" },
82
+ },
83
+ },
84
+ } as never,
85
+ prompter,
86
+ ...baseConfigureContext,
87
+ }),
88
+ ).resolves.toBeTruthy();
89
+ });
90
+ });
91
+
92
+ describe("feishuOnboardingAdapter.getStatus", () => {
93
+ it("does not fallback to top-level appId when account explicitly sets empty appId", async () => {
94
+ const status = await feishuOnboardingAdapter.getStatus({
95
+ cfg: {
96
+ channels: {
97
+ feishu: {
98
+ appId: "top_level_app",
99
+ accounts: {
100
+ main: {
101
+ appId: "",
102
+ appSecret: "sample-app-credential", // pragma: allowlist secret
103
+ },
104
+ },
105
+ },
106
+ },
107
+ } as never,
108
+ ...baseStatusContext,
109
+ });
110
+
111
+ expect(status.configured).toBe(false);
112
+ });
113
+
114
+ it("treats env SecretRef appId as not configured when env var is missing", async () => {
115
+ const appIdKey = "FEISHU_APP_ID_STATUS_MISSING_TEST";
116
+ const appSecretKey = "FEISHU_APP_CREDENTIAL_STATUS_MISSING_TEST"; // pragma: allowlist secret
117
+ await withEnvVars(
118
+ {
119
+ [appIdKey]: undefined,
120
+ [appSecretKey]: "env-credential-456", // pragma: allowlist secret
121
+ },
122
+ async () => {
123
+ const status = await getStatusWithEnvRefs({ appIdKey, appSecretKey });
124
+ expect(status.configured).toBe(false);
125
+ },
126
+ );
127
+ });
128
+
129
+ it("treats env SecretRef appId/appSecret as configured in status", async () => {
130
+ const appIdKey = "FEISHU_APP_ID_STATUS_TEST";
131
+ const appSecretKey = "FEISHU_APP_CREDENTIAL_STATUS_TEST"; // pragma: allowlist secret
132
+ await withEnvVars(
133
+ {
134
+ [appIdKey]: "cli_env_123",
135
+ [appSecretKey]: "env-credential-456", // pragma: allowlist secret
136
+ },
137
+ async () => {
138
+ const status = await getStatusWithEnvRefs({ appIdKey, appSecretKey });
139
+ expect(status.configured).toBe(true);
140
+ },
141
+ );
142
+ });
143
+ });