@openclaw-plugins/feishu-plus 0.1.7-fork.1

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/perm.ts ADDED
@@ -0,0 +1,166 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { createFeishuClient } from "./client.js";
3
+ import { listEnabledFeishuAccounts } from "./accounts.js";
4
+ import type * as Lark from "@larksuiteoapi/node-sdk";
5
+ import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
6
+ import { resolveToolsConfig } from "./tools-config.js";
7
+
8
+ // ============ Helpers ============
9
+
10
+ function json(data: unknown) {
11
+ return {
12
+ content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
13
+ details: data,
14
+ };
15
+ }
16
+
17
+ type ListTokenType =
18
+ | "doc"
19
+ | "sheet"
20
+ | "file"
21
+ | "wiki"
22
+ | "bitable"
23
+ | "docx"
24
+ | "mindnote"
25
+ | "minutes"
26
+ | "slides";
27
+ type CreateTokenType =
28
+ | "doc"
29
+ | "sheet"
30
+ | "file"
31
+ | "wiki"
32
+ | "bitable"
33
+ | "docx"
34
+ | "folder"
35
+ | "mindnote"
36
+ | "minutes"
37
+ | "slides";
38
+ type MemberType =
39
+ | "email"
40
+ | "openid"
41
+ | "unionid"
42
+ | "openchat"
43
+ | "opendepartmentid"
44
+ | "userid"
45
+ | "groupid"
46
+ | "wikispaceid";
47
+ type PermType = "view" | "edit" | "full_access";
48
+
49
+ // ============ Actions ============
50
+
51
+ async function listMembers(client: Lark.Client, token: string, type: string) {
52
+ const res = await client.drive.permissionMember.list({
53
+ path: { token },
54
+ params: { type: type as ListTokenType },
55
+ });
56
+ if (res.code !== 0) throw new Error(res.msg);
57
+
58
+ return {
59
+ members:
60
+ res.data?.items?.map((m) => ({
61
+ member_type: m.member_type,
62
+ member_id: m.member_id,
63
+ perm: m.perm,
64
+ name: m.name,
65
+ })) ?? [],
66
+ };
67
+ }
68
+
69
+ async function addMember(
70
+ client: Lark.Client,
71
+ token: string,
72
+ type: string,
73
+ memberType: string,
74
+ memberId: string,
75
+ perm: string,
76
+ ) {
77
+ const res = await client.drive.permissionMember.create({
78
+ path: { token },
79
+ params: { type: type as CreateTokenType, need_notification: false },
80
+ data: {
81
+ member_type: memberType as MemberType,
82
+ member_id: memberId,
83
+ perm: perm as PermType,
84
+ },
85
+ });
86
+ if (res.code !== 0) throw new Error(res.msg);
87
+
88
+ return {
89
+ success: true,
90
+ member: res.data?.member,
91
+ };
92
+ }
93
+
94
+ async function removeMember(
95
+ client: Lark.Client,
96
+ token: string,
97
+ type: string,
98
+ memberType: string,
99
+ memberId: string,
100
+ ) {
101
+ const res = await client.drive.permissionMember.delete({
102
+ path: { token, member_id: memberId },
103
+ params: { type: type as CreateTokenType, member_type: memberType as MemberType },
104
+ });
105
+ if (res.code !== 0) throw new Error(res.msg);
106
+
107
+ return {
108
+ success: true,
109
+ };
110
+ }
111
+
112
+ // ============ Tool Registration ============
113
+
114
+ export function registerFeishuPermTools(api: OpenClawPluginApi) {
115
+ if (!api.config) {
116
+ api.logger.debug?.("feishu_perm: No config available, skipping perm tools");
117
+ return;
118
+ }
119
+
120
+ const accounts = listEnabledFeishuAccounts(api.config);
121
+ if (accounts.length === 0) {
122
+ api.logger.debug?.("feishu_perm: No Feishu accounts configured, skipping perm tools");
123
+ return;
124
+ }
125
+
126
+ const firstAccount = accounts[0];
127
+ const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
128
+ if (!toolsCfg.perm) {
129
+ api.logger.debug?.("feishu_perm: perm tool disabled in config (default: false)");
130
+ return;
131
+ }
132
+
133
+ const getClient = () => createFeishuClient(firstAccount);
134
+
135
+ api.registerTool(
136
+ {
137
+ name: "feishu_perm",
138
+ label: "Feishu Perm",
139
+ description: "Feishu permission management. Actions: list, add, remove",
140
+ parameters: FeishuPermSchema,
141
+ async execute(_toolCallId, params) {
142
+ const p = params as FeishuPermParams;
143
+ try {
144
+ const client = getClient();
145
+ switch (p.action) {
146
+ case "list":
147
+ return json(await listMembers(client, p.token, p.type));
148
+ case "add":
149
+ return json(
150
+ await addMember(client, p.token, p.type, p.member_type, p.member_id, p.perm),
151
+ );
152
+ case "remove":
153
+ return json(await removeMember(client, p.token, p.type, p.member_type, p.member_id));
154
+ default:
155
+ return json({ error: `Unknown action: ${(p as any).action}` });
156
+ }
157
+ } catch (err) {
158
+ return json({ error: err instanceof Error ? err.message : String(err) });
159
+ }
160
+ },
161
+ },
162
+ { name: "feishu_perm" },
163
+ );
164
+
165
+ api.logger.info?.(`feishu_perm: Registered feishu_perm tool`);
166
+ }
package/src/policy.ts ADDED
@@ -0,0 +1,92 @@
1
+ import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
2
+ import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
3
+
4
+ export type FeishuAllowlistMatch = {
5
+ allowed: boolean;
6
+ matchKey?: string;
7
+ matchSource?: "wildcard" | "id" | "name";
8
+ };
9
+
10
+ export function resolveFeishuAllowlistMatch(params: {
11
+ allowFrom: Array<string | number>;
12
+ senderId: string;
13
+ senderName?: string | null;
14
+ }): FeishuAllowlistMatch {
15
+ const allowFrom = params.allowFrom
16
+ .map((entry) => String(entry).trim().toLowerCase())
17
+ .filter(Boolean);
18
+
19
+ if (allowFrom.length === 0) return { allowed: false };
20
+ if (allowFrom.includes("*")) {
21
+ return { allowed: true, matchKey: "*", matchSource: "wildcard" };
22
+ }
23
+
24
+ const senderId = params.senderId.toLowerCase();
25
+ if (allowFrom.includes(senderId)) {
26
+ return { allowed: true, matchKey: senderId, matchSource: "id" };
27
+ }
28
+
29
+ const senderName = params.senderName?.toLowerCase();
30
+ if (senderName && allowFrom.includes(senderName)) {
31
+ return { allowed: true, matchKey: senderName, matchSource: "name" };
32
+ }
33
+
34
+ return { allowed: false };
35
+ }
36
+
37
+ export function resolveFeishuGroupConfig(params: {
38
+ cfg?: FeishuConfig;
39
+ groupId?: string | null;
40
+ }): FeishuGroupConfig | undefined {
41
+ const groups = params.cfg?.groups ?? {};
42
+ const groupId = params.groupId?.trim();
43
+ if (!groupId) return undefined;
44
+
45
+ const direct = groups[groupId] as FeishuGroupConfig | undefined;
46
+ if (direct) return direct;
47
+
48
+ const lowered = groupId.toLowerCase();
49
+ const matchKey = Object.keys(groups).find((key) => key.toLowerCase() === lowered);
50
+ return matchKey ? (groups[matchKey] as FeishuGroupConfig | undefined) : undefined;
51
+ }
52
+
53
+ export function resolveFeishuGroupToolPolicy(
54
+ params: ChannelGroupContext,
55
+ ): GroupToolPolicyConfig | undefined {
56
+ const cfg = params.cfg.channels?.feishu as FeishuConfig | undefined;
57
+ if (!cfg) return undefined;
58
+
59
+ const groupConfig = resolveFeishuGroupConfig({
60
+ cfg,
61
+ groupId: params.groupId,
62
+ });
63
+
64
+ return groupConfig?.tools;
65
+ }
66
+
67
+ export function isFeishuGroupAllowed(params: {
68
+ groupPolicy: "open" | "allowlist" | "disabled";
69
+ allowFrom: Array<string | number>;
70
+ senderId: string;
71
+ senderName?: string | null;
72
+ }): boolean {
73
+ const { groupPolicy } = params;
74
+ if (groupPolicy === "disabled") return false;
75
+ if (groupPolicy === "open") return true;
76
+ return resolveFeishuAllowlistMatch(params).allowed;
77
+ }
78
+
79
+ export function resolveFeishuReplyPolicy(params: {
80
+ isDirectMessage: boolean;
81
+ globalConfig?: FeishuConfig;
82
+ groupConfig?: FeishuGroupConfig;
83
+ }): { requireMention: boolean } {
84
+ if (params.isDirectMessage) {
85
+ return { requireMention: false };
86
+ }
87
+
88
+ const requireMention =
89
+ params.groupConfig?.requireMention ?? params.globalConfig?.requireMention ?? true;
90
+
91
+ return { requireMention };
92
+ }
package/src/probe.ts ADDED
@@ -0,0 +1,115 @@
1
+ import type { FeishuProbeResult } from "./types.js";
2
+ import { createFeishuClient, type FeishuClientCredentials } from "./client.js";
3
+
4
+ // Cache configuration
5
+ const DEFAULT_CACHE_TTL_MINUTES = 15;
6
+ const ERROR_CACHE_TTL_MINUTES = 5;
7
+
8
+ // Cache entry type
9
+ interface CacheEntry {
10
+ result: FeishuProbeResult;
11
+ timestamp: number;
12
+ }
13
+
14
+ // In-memory cache for probe results
15
+ const probeCache = new Map<string, CacheEntry>();
16
+
17
+ /**
18
+ * Get cache TTL in milliseconds
19
+ */
20
+ function getCacheTtlMs(): number {
21
+ const envTtl = process.env.FEISHU_PROBE_CACHE_TTL_MINUTES;
22
+ const minutes = envTtl ? parseInt(envTtl, 10) : DEFAULT_CACHE_TTL_MINUTES;
23
+ return (isNaN(minutes) ? DEFAULT_CACHE_TTL_MINUTES : minutes) * 60 * 1000;
24
+ }
25
+
26
+ /**
27
+ * Get cache key for credentials
28
+ */
29
+ function getCacheKey(creds: FeishuClientCredentials): string {
30
+ return `${creds.appId}:${creds.appSecret}`;
31
+ }
32
+
33
+ /**
34
+ * Clear probe cache for a specific account or all accounts
35
+ * @param accountId - Optional account ID to clear. If not provided, clears all cache.
36
+ */
37
+ export function clearProbeCache(accountId?: string): void {
38
+ if (accountId) {
39
+ // Clear cache entries matching the account ID prefix
40
+ for (const [key, entry] of probeCache.entries()) {
41
+ if (key.startsWith(`${accountId}:`)) {
42
+ probeCache.delete(key);
43
+ }
44
+ }
45
+ } else {
46
+ probeCache.clear();
47
+ }
48
+ }
49
+
50
+ export async function probeFeishu(creds?: FeishuClientCredentials): Promise<FeishuProbeResult> {
51
+ if (!creds?.appId || !creds?.appSecret) {
52
+ return {
53
+ ok: false,
54
+ error: "missing credentials (appId, appSecret)",
55
+ };
56
+ }
57
+
58
+ // Check cache
59
+ const cacheKey = getCacheKey(creds);
60
+ const cached = probeCache.get(cacheKey);
61
+ const now = Date.now();
62
+ const cacheTtlMs = getCacheTtlMs();
63
+
64
+ if (cached) {
65
+ const isSuccess = cached.result.ok;
66
+ const effectiveTtl = isSuccess ? cacheTtlMs : ERROR_CACHE_TTL_MINUTES * 60 * 1000;
67
+
68
+ if (now - cached.timestamp < effectiveTtl) {
69
+ return cached.result;
70
+ }
71
+ // Cache expired, remove it
72
+ probeCache.delete(cacheKey);
73
+ }
74
+
75
+ try {
76
+ const client = createFeishuClient(creds);
77
+ // Use bot/v3/info API to get bot information
78
+ const response = await (client as any).request({
79
+ method: "GET",
80
+ url: "/open-apis/bot/v3/info",
81
+ data: {},
82
+ });
83
+
84
+ if (response.code !== 0) {
85
+ const result: FeishuProbeResult = {
86
+ ok: false,
87
+ appId: creds.appId,
88
+ error: `API error: ${response.msg || `code ${response.code}`}`,
89
+ };
90
+ // Cache error result with shorter TTL
91
+ probeCache.set(cacheKey, { result, timestamp: now });
92
+ return result;
93
+ }
94
+
95
+ const bot = response.bot || response.data?.bot;
96
+ const result: FeishuProbeResult = {
97
+ ok: true,
98
+ appId: creds.appId,
99
+ botName: bot?.bot_name,
100
+ botOpenId: bot?.open_id,
101
+ };
102
+ // Cache successful result
103
+ probeCache.set(cacheKey, { result, timestamp: now });
104
+ return result;
105
+ } catch (err) {
106
+ const result: FeishuProbeResult = {
107
+ ok: false,
108
+ appId: creds.appId,
109
+ error: err instanceof Error ? err.message : String(err),
110
+ };
111
+ // Cache error result with shorter TTL
112
+ probeCache.set(cacheKey, { result, timestamp: now });
113
+ return result;
114
+ }
115
+ }
@@ -0,0 +1,160 @@
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import { createFeishuClient } from "./client.js";
3
+ import { resolveFeishuAccount } from "./accounts.js";
4
+
5
+ export type FeishuReaction = {
6
+ reactionId: string;
7
+ emojiType: string;
8
+ operatorType: "app" | "user";
9
+ operatorId: string;
10
+ };
11
+
12
+ /**
13
+ * Add a reaction (emoji) to a message.
14
+ * @param emojiType - Feishu emoji type, e.g., "SMILE", "THUMBSUP", "HEART"
15
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
16
+ */
17
+ export async function addReactionFeishu(params: {
18
+ cfg: ClawdbotConfig;
19
+ messageId: string;
20
+ emojiType: string;
21
+ accountId?: string;
22
+ }): Promise<{ reactionId: string }> {
23
+ const { cfg, messageId, emojiType, accountId } = params;
24
+ const account = resolveFeishuAccount({ cfg, accountId });
25
+ if (!account.configured) {
26
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
27
+ }
28
+
29
+ const client = createFeishuClient(account);
30
+
31
+ const response = (await client.im.messageReaction.create({
32
+ path: { message_id: messageId },
33
+ data: {
34
+ reaction_type: {
35
+ emoji_type: emojiType,
36
+ },
37
+ },
38
+ })) as {
39
+ code?: number;
40
+ msg?: string;
41
+ data?: { reaction_id?: string };
42
+ };
43
+
44
+ if (response.code !== 0) {
45
+ throw new Error(`Feishu add reaction failed: ${response.msg || `code ${response.code}`}`);
46
+ }
47
+
48
+ const reactionId = response.data?.reaction_id;
49
+ if (!reactionId) {
50
+ throw new Error("Feishu add reaction failed: no reaction_id returned");
51
+ }
52
+
53
+ return { reactionId };
54
+ }
55
+
56
+ /**
57
+ * Remove a reaction from a message.
58
+ */
59
+ export async function removeReactionFeishu(params: {
60
+ cfg: ClawdbotConfig;
61
+ messageId: string;
62
+ reactionId: string;
63
+ accountId?: string;
64
+ }): Promise<void> {
65
+ const { cfg, messageId, reactionId, accountId } = params;
66
+ const account = resolveFeishuAccount({ cfg, accountId });
67
+ if (!account.configured) {
68
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
69
+ }
70
+
71
+ const client = createFeishuClient(account);
72
+
73
+ const response = (await client.im.messageReaction.delete({
74
+ path: {
75
+ message_id: messageId,
76
+ reaction_id: reactionId,
77
+ },
78
+ })) as { code?: number; msg?: string };
79
+
80
+ if (response.code !== 0) {
81
+ throw new Error(`Feishu remove reaction failed: ${response.msg || `code ${response.code}`}`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * List all reactions for a message.
87
+ */
88
+ export async function listReactionsFeishu(params: {
89
+ cfg: ClawdbotConfig;
90
+ messageId: string;
91
+ emojiType?: string;
92
+ accountId?: string;
93
+ }): Promise<FeishuReaction[]> {
94
+ const { cfg, messageId, emojiType, accountId } = params;
95
+ const account = resolveFeishuAccount({ cfg, accountId });
96
+ if (!account.configured) {
97
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
98
+ }
99
+
100
+ const client = createFeishuClient(account);
101
+
102
+ const response = (await client.im.messageReaction.list({
103
+ path: { message_id: messageId },
104
+ params: emojiType ? { reaction_type: emojiType } : undefined,
105
+ })) as {
106
+ code?: number;
107
+ msg?: string;
108
+ data?: {
109
+ items?: Array<{
110
+ reaction_id?: string;
111
+ reaction_type?: { emoji_type?: string };
112
+ operator_type?: string;
113
+ operator_id?: { open_id?: string; user_id?: string; union_id?: string };
114
+ }>;
115
+ };
116
+ };
117
+
118
+ if (response.code !== 0) {
119
+ throw new Error(`Feishu list reactions failed: ${response.msg || `code ${response.code}`}`);
120
+ }
121
+
122
+ const items = response.data?.items ?? [];
123
+ return items.map((item) => ({
124
+ reactionId: item.reaction_id ?? "",
125
+ emojiType: item.reaction_type?.emoji_type ?? "",
126
+ operatorType: item.operator_type === "app" ? "app" : "user",
127
+ operatorId:
128
+ item.operator_id?.open_id ?? item.operator_id?.user_id ?? item.operator_id?.union_id ?? "",
129
+ }));
130
+ }
131
+
132
+ /**
133
+ * Common Feishu emoji types for convenience.
134
+ * @see https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce
135
+ */
136
+ export const FeishuEmoji = {
137
+ // Common reactions
138
+ THUMBSUP: "THUMBSUP",
139
+ THUMBSDOWN: "THUMBSDOWN",
140
+ HEART: "HEART",
141
+ SMILE: "SMILE",
142
+ GRINNING: "GRINNING",
143
+ LAUGHING: "LAUGHING",
144
+ CRY: "CRY",
145
+ ANGRY: "ANGRY",
146
+ SURPRISED: "SURPRISED",
147
+ THINKING: "THINKING",
148
+ CLAP: "CLAP",
149
+ OK: "OK",
150
+ FIST: "FIST",
151
+ PRAY: "PRAY",
152
+ FIRE: "FIRE",
153
+ PARTY: "PARTY",
154
+ CHECK: "CHECK",
155
+ CROSS: "CROSS",
156
+ QUESTION: "QUESTION",
157
+ EXCLAMATION: "EXCLAMATION",
158
+ } as const;
159
+
160
+ export type FeishuEmojiType = (typeof FeishuEmoji)[keyof typeof FeishuEmoji];