@nextclaw/channel-plugin-feishu 0.2.16 → 0.2.18

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/index.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import type { OpenClawPluginApi } from "./src/nextclaw-sdk/feishu.js";
2
+ import { registerFeishuCalendarTools } from "./src/calendar.js";
2
3
  import { emptyPluginConfigSchema } from "./src/nextclaw-sdk/feishu.js";
3
4
  import { registerFeishuBitableTools } from "./src/bitable.js";
4
5
  import { feishuPlugin } from "./src/channel.js";
5
6
  import { registerFeishuChatTools } from "./src/chat.js";
6
7
  import { registerFeishuDocTools } from "./src/docx.js";
7
8
  import { registerFeishuDriveTools } from "./src/drive.js";
9
+ import { registerFeishuIdentityTools } from "./src/identity.js";
10
+ import { registerFeishuOAuthTool } from "./src/oauth.js";
8
11
  import { registerFeishuPermTools } from "./src/perm.js";
9
12
  import { setFeishuRuntime } from "./src/runtime.js";
13
+ import { registerFeishuSheetsTools } from "./src/sheets.js";
14
+ import { registerFeishuTaskTools } from "./src/task.js";
10
15
  import { registerFeishuWikiTools } from "./src/wiki.js";
11
16
 
12
17
  export { monitorFeishuProvider } from "./src/monitor.js";
@@ -59,6 +64,11 @@ const plugin = {
59
64
  registerFeishuDriveTools(api);
60
65
  registerFeishuPermTools(api);
61
66
  registerFeishuBitableTools(api);
67
+ registerFeishuCalendarTools(api);
68
+ registerFeishuTaskTools(api);
69
+ registerFeishuSheetsTools(api);
70
+ registerFeishuIdentityTools(api);
71
+ registerFeishuOAuthTool(api);
62
72
  },
63
73
  };
64
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/channel-plugin-feishu",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "private": false,
5
5
  "description": "NextClaw Feishu/Lark channel plugin with doc/wiki/drive tools.",
6
6
  "type": "module",
@@ -0,0 +1,75 @@
1
+ import type * as Lark from "@larksuiteoapi/node-sdk";
2
+ import { AppScopeCheckFailedError } from "./auth-errors.js";
3
+
4
+ type CacheEntry = {
5
+ rawScopes: Array<{ scope: string; token_types?: string[] }>;
6
+ fetchedAt: number;
7
+ };
8
+
9
+ const cache = new Map<string, CacheEntry>();
10
+ const CACHE_TTL_MS = 30 * 1000;
11
+
12
+ export function invalidateAppScopeCache(appId: string): void {
13
+ cache.delete(appId);
14
+ }
15
+
16
+ export async function getAppGrantedScopes(
17
+ sdk: Lark.Client,
18
+ appId: string,
19
+ tokenType?: "user" | "tenant",
20
+ ): Promise<string[]> {
21
+ const cached = cache.get(appId);
22
+ if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
23
+ return cached.rawScopes
24
+ .filter((scope) => !tokenType || !scope.token_types || scope.token_types.includes(tokenType))
25
+ .map((scope) => scope.scope);
26
+ }
27
+
28
+ try {
29
+ const response = await (sdk as unknown as {
30
+ request: (params: {
31
+ method: string;
32
+ url: string;
33
+ params: Record<string, string>;
34
+ }) => Promise<{ code?: number; data?: { app?: { scopes?: Array<{ scope?: string; token_types?: string[] }> } } }>;
35
+ }).request({
36
+ method: "GET",
37
+ url: `/open-apis/application/v6/applications/${appId}`,
38
+ params: { lang: "zh_cn" },
39
+ });
40
+
41
+ if (response.code !== 0) {
42
+ throw new AppScopeCheckFailedError(appId);
43
+ }
44
+
45
+ const rawScopes =
46
+ response.data?.app?.scopes
47
+ ?.filter((scope): scope is { scope: string; token_types?: string[] } =>
48
+ typeof scope.scope === "string" && scope.scope.length > 0,
49
+ ) ?? [];
50
+ cache.set(appId, { rawScopes, fetchedAt: Date.now() });
51
+
52
+ return rawScopes
53
+ .filter((scope) => !tokenType || !scope.token_types || scope.token_types.includes(tokenType))
54
+ .map((scope) => scope.scope);
55
+ } catch (error) {
56
+ if (error instanceof AppScopeCheckFailedError) {
57
+ throw error;
58
+ }
59
+
60
+ const statusCode =
61
+ (error as { response?: { status?: number }; status?: number; statusCode?: number }).response
62
+ ?.status ??
63
+ (error as { status?: number }).status ??
64
+ (error as { statusCode?: number }).statusCode;
65
+ if (statusCode === 400 || statusCode === 403) {
66
+ throw new AppScopeCheckFailedError(appId);
67
+ }
68
+ return [];
69
+ }
70
+ }
71
+
72
+ export function missingScopes(appGranted: string[], apiRequired: string[]): string[] {
73
+ const granted = new Set(appGranted);
74
+ return apiRequired.filter((scope) => !granted.has(scope));
75
+ }
@@ -0,0 +1,90 @@
1
+ export const LARK_ERROR = {
2
+ APP_SCOPE_MISSING: 99991672,
3
+ USER_SCOPE_INSUFFICIENT: 99991679,
4
+ TOKEN_INVALID: 99991668,
5
+ TOKEN_EXPIRED: 99991677,
6
+ REFRESH_TOKEN_INVALID: 20026,
7
+ REFRESH_TOKEN_EXPIRED: 20037,
8
+ REFRESH_TOKEN_REVOKED: 20064,
9
+ REFRESH_TOKEN_ALREADY_USED: 20073,
10
+ REFRESH_SERVER_ERROR: 20050,
11
+ } as const;
12
+
13
+ export const REFRESH_TOKEN_RETRYABLE = new Set<number>([LARK_ERROR.REFRESH_SERVER_ERROR]);
14
+ export const TOKEN_RETRY_CODES = new Set<number>([
15
+ LARK_ERROR.TOKEN_INVALID,
16
+ LARK_ERROR.TOKEN_EXPIRED,
17
+ ]);
18
+
19
+ export type ScopeErrorInfo = {
20
+ apiName: string;
21
+ scopes: string[];
22
+ appScopeVerified?: boolean;
23
+ appId?: string;
24
+ };
25
+
26
+ export class NeedAuthorizationError extends Error {
27
+ readonly userOpenId: string;
28
+
29
+ constructor(userOpenId: string) {
30
+ super("need_user_authorization");
31
+ this.name = "NeedAuthorizationError";
32
+ this.userOpenId = userOpenId;
33
+ }
34
+ }
35
+
36
+ export class AppScopeCheckFailedError extends Error {
37
+ readonly appId?: string;
38
+
39
+ constructor(appId?: string) {
40
+ super("应用缺少 application:application:self_manage 权限,无法查询应用权限配置。");
41
+ this.name = "AppScopeCheckFailedError";
42
+ this.appId = appId;
43
+ }
44
+ }
45
+
46
+ export class AppScopeMissingError extends Error {
47
+ readonly apiName: string;
48
+ readonly missingScopes: string[];
49
+ readonly appId?: string;
50
+
51
+ constructor(info: ScopeErrorInfo) {
52
+ super(`应用缺少权限 [${info.scopes.join(", ")}],请管理员在飞书开放平台开通。`);
53
+ this.name = "AppScopeMissingError";
54
+ this.apiName = info.apiName;
55
+ this.missingScopes = info.scopes;
56
+ this.appId = info.appId;
57
+ }
58
+ }
59
+
60
+ export class UserAuthRequiredError extends Error {
61
+ readonly userOpenId: string;
62
+ readonly apiName: string;
63
+ readonly requiredScopes: string[];
64
+ readonly appScopeVerified: boolean;
65
+ readonly appId?: string;
66
+
67
+ constructor(userOpenId: string, info: ScopeErrorInfo) {
68
+ super("need_user_authorization");
69
+ this.name = "UserAuthRequiredError";
70
+ this.userOpenId = userOpenId;
71
+ this.apiName = info.apiName;
72
+ this.requiredScopes = info.scopes;
73
+ this.appScopeVerified = info.appScopeVerified ?? true;
74
+ this.appId = info.appId;
75
+ }
76
+ }
77
+
78
+ export class UserScopeInsufficientError extends Error {
79
+ readonly userOpenId: string;
80
+ readonly apiName: string;
81
+ readonly missingScopes: string[];
82
+
83
+ constructor(userOpenId: string, info: ScopeErrorInfo) {
84
+ super("user_scope_insufficient");
85
+ this.name = "UserScopeInsufficientError";
86
+ this.userOpenId = userOpenId;
87
+ this.apiName = info.apiName;
88
+ this.missingScopes = info.scopes;
89
+ }
90
+ }
@@ -0,0 +1,72 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi } from "./nextclaw-sdk/feishu.js";
3
+ import { assertLarkOk, createToolContext, handleInvokeError, json, registerTool } from "./user-tool-helpers.js";
4
+
5
+ const CalendarCalendarSchema = Type.Union([
6
+ Type.Object({ action: Type.Literal("list"), page_size: Type.Optional(Type.Number()), page_token: Type.Optional(Type.String()) }),
7
+ Type.Object({ action: Type.Literal("get"), calendar_id: Type.String() }),
8
+ Type.Object({ action: Type.Literal("primary") }),
9
+ ]);
10
+
11
+ export function registerFeishuCalendarCalendarTool(api: OpenClawPluginApi) {
12
+ const { toolClient } = createToolContext(api, "feishu_calendar_calendar");
13
+ registerTool(
14
+ api,
15
+ {
16
+ name: "feishu_calendar_calendar",
17
+ label: "Feishu Calendar",
18
+ description: "按本人身份查询飞书日历列表、主日历和指定日历详情。",
19
+ parameters: CalendarCalendarSchema,
20
+ async execute(_toolCallId, params) {
21
+ const payload = params as {
22
+ action: "list" | "get" | "primary";
23
+ page_size?: number;
24
+ page_token?: string;
25
+ calendar_id?: string;
26
+ };
27
+ try {
28
+ const client = toolClient();
29
+ if (payload.action === "list") {
30
+ const response = await client.invoke(
31
+ "feishu_calendar_calendar.list",
32
+ (sdk, opts) =>
33
+ sdk.calendar.calendar.list(
34
+ { params: { page_size: payload.page_size, page_token: payload.page_token } },
35
+ opts,
36
+ ),
37
+ { as: "user" },
38
+ );
39
+ assertLarkOk(response);
40
+ return json({
41
+ calendars: (response.data as { calendar_list?: unknown[] } | undefined)?.calendar_list ?? [],
42
+ has_more: (response.data as { has_more?: boolean } | undefined)?.has_more ?? false,
43
+ page_token: (response.data as { page_token?: string } | undefined)?.page_token,
44
+ });
45
+ }
46
+ if (payload.action === "primary") {
47
+ const response = await client.invoke(
48
+ "feishu_calendar_calendar.primary",
49
+ (sdk, opts) => sdk.calendar.calendar.primary({}, opts),
50
+ { as: "user" },
51
+ );
52
+ assertLarkOk(response);
53
+ return json({
54
+ calendars: (response.data as { calendars?: unknown[] } | undefined)?.calendars ?? [],
55
+ });
56
+ }
57
+ const response = await client.invoke(
58
+ "feishu_calendar_calendar.get",
59
+ (sdk, opts) =>
60
+ sdk.calendar.calendar.get({ path: { calendar_id: payload.calendar_id! } }, opts),
61
+ { as: "user" },
62
+ );
63
+ assertLarkOk(response);
64
+ return json({ calendar: (response.data as { calendar?: unknown } | undefined)?.calendar ?? response.data });
65
+ } catch (error) {
66
+ return handleInvokeError(error, api);
67
+ }
68
+ },
69
+ },
70
+ { name: "feishu_calendar_calendar" },
71
+ );
72
+ }
@@ -0,0 +1,96 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi } from "./nextclaw-sdk/feishu.js";
3
+ import { assertLarkOk, createToolContext, handleInvokeError, json, registerTool, StringEnum } from "./user-tool-helpers.js";
4
+
5
+ const CalendarAttendeeSchema = Type.Union([
6
+ Type.Object({
7
+ action: Type.Literal("create"),
8
+ calendar_id: Type.String(),
9
+ event_id: Type.String(),
10
+ attendees: Type.Array(Type.Object({ type: StringEnum(["user", "chat", "resource", "third_party"]), attendee_id: Type.String() })),
11
+ need_notification: Type.Optional(Type.Boolean()),
12
+ }),
13
+ Type.Object({
14
+ action: Type.Literal("list"),
15
+ calendar_id: Type.String(),
16
+ event_id: Type.String(),
17
+ page_size: Type.Optional(Type.Number()),
18
+ page_token: Type.Optional(Type.String()),
19
+ user_id_type: Type.Optional(StringEnum(["open_id", "union_id", "user_id"])),
20
+ }),
21
+ ]);
22
+
23
+ export function registerFeishuCalendarEventAttendeeTool(api: OpenClawPluginApi) {
24
+ const { toolClient } = createToolContext(api, "feishu_calendar_event_attendee");
25
+ registerTool(api, {
26
+ name: "feishu_calendar_event_attendee",
27
+ label: "Feishu Calendar Event Attendee",
28
+ description: "按本人身份添加日程参会人或查看参会人列表。",
29
+ parameters: CalendarAttendeeSchema,
30
+ async execute(_toolCallId, params) {
31
+ const payload = params as {
32
+ action: "create" | "list";
33
+ calendar_id: string;
34
+ event_id: string;
35
+ attendees?: Array<{ type: "user" | "chat" | "resource" | "third_party"; attendee_id: string }>;
36
+ need_notification?: boolean;
37
+ page_size?: number;
38
+ page_token?: string;
39
+ user_id_type?: "open_id" | "union_id" | "user_id";
40
+ };
41
+ try {
42
+ const client = toolClient();
43
+ if (payload.action === "create") {
44
+ const attendeePayload = (payload.attendees ?? []).map((attendee) => {
45
+ if (attendee.type === "user") return { type: attendee.type, user_id: attendee.attendee_id };
46
+ if (attendee.type === "chat") return { type: attendee.type, chat_id: attendee.attendee_id };
47
+ if (attendee.type === "resource") return { type: attendee.type, room_id: attendee.attendee_id };
48
+ return { type: attendee.type, third_party_email: attendee.attendee_id };
49
+ });
50
+ const response = await client.invoke(
51
+ "feishu_calendar_event_attendee.create",
52
+ (sdk, opts) =>
53
+ sdk.calendar.calendarEventAttendee.create(
54
+ {
55
+ path: { calendar_id: payload.calendar_id, event_id: payload.event_id },
56
+ params: { user_id_type: "open_id" as never },
57
+ data: {
58
+ attendees: attendeePayload,
59
+ need_notification: payload.need_notification ?? true,
60
+ },
61
+ },
62
+ opts,
63
+ ),
64
+ { as: "user" },
65
+ );
66
+ assertLarkOk(response);
67
+ return json({ attendees: (response.data as { attendees?: unknown[] } | undefined)?.attendees ?? [] });
68
+ }
69
+ const response = await client.invoke(
70
+ "feishu_calendar_event_attendee.list",
71
+ (sdk, opts) =>
72
+ sdk.calendar.calendarEventAttendee.list(
73
+ {
74
+ path: { calendar_id: payload.calendar_id, event_id: payload.event_id },
75
+ params: {
76
+ page_size: payload.page_size,
77
+ page_token: payload.page_token,
78
+ user_id_type: payload.user_id_type ?? "open_id",
79
+ },
80
+ },
81
+ opts,
82
+ ),
83
+ { as: "user" },
84
+ );
85
+ assertLarkOk(response);
86
+ return json({
87
+ attendees: (response.data as { items?: unknown[] } | undefined)?.items ?? [],
88
+ has_more: (response.data as { has_more?: boolean } | undefined)?.has_more ?? false,
89
+ page_token: (response.data as { page_token?: string } | undefined)?.page_token,
90
+ });
91
+ } catch (error) {
92
+ return handleInvokeError(error, api);
93
+ }
94
+ },
95
+ }, { name: "feishu_calendar_event_attendee" });
96
+ }
@@ -0,0 +1,236 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi } from "./nextclaw-sdk/feishu.js";
3
+ import { normalizeEventTimes, resolveCalendarId } from "./calendar-shared.js";
4
+ import { assertLarkOk, createToolContext, handleInvokeError, json, parseTimeToTimestamp, registerTool, StringEnum } from "./user-tool-helpers.js";
5
+
6
+ const CalendarEventSchema = Type.Union([
7
+ Type.Object({
8
+ action: Type.Literal("create"),
9
+ start_time: Type.String(),
10
+ end_time: Type.String(),
11
+ summary: Type.Optional(Type.String()),
12
+ calendar_id: Type.Optional(Type.String()),
13
+ description: Type.Optional(Type.String()),
14
+ attendees: Type.Optional(Type.Array(Type.Object({ type: StringEnum(["user", "chat", "resource", "third_party"]), id: Type.String() }))),
15
+ user_open_id: Type.Optional(Type.String()),
16
+ need_notification: Type.Optional(Type.Boolean()),
17
+ location: Type.Optional(Type.String()),
18
+ }),
19
+ Type.Object({
20
+ action: Type.Literal("list"),
21
+ start_time: Type.String(),
22
+ end_time: Type.String(),
23
+ calendar_id: Type.Optional(Type.String()),
24
+ page_size: Type.Optional(Type.Number()),
25
+ page_token: Type.Optional(Type.String()),
26
+ }),
27
+ Type.Object({ action: Type.Literal("get"), event_id: Type.String(), calendar_id: Type.Optional(Type.String()) }),
28
+ Type.Object({
29
+ action: Type.Literal("patch"),
30
+ event_id: Type.String(),
31
+ calendar_id: Type.Optional(Type.String()),
32
+ summary: Type.Optional(Type.String()),
33
+ description: Type.Optional(Type.String()),
34
+ start_time: Type.Optional(Type.String()),
35
+ end_time: Type.Optional(Type.String()),
36
+ location: Type.Optional(Type.String()),
37
+ }),
38
+ Type.Object({ action: Type.Literal("delete"), event_id: Type.String(), calendar_id: Type.Optional(Type.String()), need_notification: Type.Optional(Type.Boolean()) }),
39
+ ]);
40
+
41
+ function buildAttendees(payload: {
42
+ attendees?: Array<{ type: "user" | "chat" | "resource" | "third_party"; id: string }>;
43
+ user_open_id?: string;
44
+ }) {
45
+ const attendees = [
46
+ ...(payload.user_open_id ? [{ type: "user" as const, id: payload.user_open_id }] : []),
47
+ ...(payload.attendees ?? []),
48
+ ];
49
+ return attendees.map((attendee) => {
50
+ if (attendee.type === "user") return { type: attendee.type, user_id: attendee.id };
51
+ if (attendee.type === "chat") return { type: attendee.type, chat_id: attendee.id };
52
+ if (attendee.type === "resource") return { type: attendee.type, room_id: attendee.id };
53
+ return { type: attendee.type, third_party_email: attendee.id };
54
+ });
55
+ }
56
+
57
+ export function registerFeishuCalendarEventTool(api: OpenClawPluginApi) {
58
+ const { toolClient } = createToolContext(api, "feishu_calendar_event");
59
+ registerTool(
60
+ api,
61
+ {
62
+ name: "feishu_calendar_event",
63
+ label: "Feishu Calendar Event",
64
+ description: "按本人身份创建、查询、修改、删除飞书日程。",
65
+ parameters: CalendarEventSchema,
66
+ async execute(_toolCallId, params) {
67
+ const payload = params as {
68
+ action: "create" | "list" | "get" | "patch" | "delete";
69
+ calendar_id?: string;
70
+ event_id?: string;
71
+ start_time?: string;
72
+ end_time?: string;
73
+ summary?: string;
74
+ description?: string;
75
+ attendees?: Array<{ type: "user" | "chat" | "resource" | "third_party"; id: string }>;
76
+ user_open_id?: string;
77
+ need_notification?: boolean;
78
+ location?: string;
79
+ page_size?: number;
80
+ page_token?: string;
81
+ };
82
+ try {
83
+ const client = toolClient();
84
+ const calendarId = await resolveCalendarId(client, payload.calendar_id);
85
+
86
+ if (payload.action === "create") {
87
+ const startTs = payload.start_time ? parseTimeToTimestamp(payload.start_time) : null;
88
+ const endTs = payload.end_time ? parseTimeToTimestamp(payload.end_time) : null;
89
+ if (!startTs || !endTs) {
90
+ return json({ error: "start_time 和 end_time 必须为带时区的 ISO 8601 / RFC 3339 时间。" });
91
+ }
92
+ const response = await client.invoke(
93
+ "feishu_calendar_event.create",
94
+ (sdk, opts) =>
95
+ sdk.calendar.calendarEvent.create(
96
+ {
97
+ path: { calendar_id: calendarId },
98
+ data: {
99
+ summary: payload.summary,
100
+ description: payload.description,
101
+ start_time: { timestamp: startTs },
102
+ end_time: { timestamp: endTs },
103
+ ...(payload.location ? { location: { name: payload.location } } : {}),
104
+ },
105
+ },
106
+ opts,
107
+ ),
108
+ { as: "user" },
109
+ );
110
+ assertLarkOk(response);
111
+ const event = (response.data as { event?: { event_id?: string } } | undefined)?.event;
112
+ const attendeePayload = buildAttendees(payload);
113
+ if (event?.event_id && attendeePayload.length > 0) {
114
+ await client.invoke(
115
+ "feishu_calendar_event.create",
116
+ (sdk, opts) =>
117
+ sdk.calendar.calendarEventAttendee.create(
118
+ {
119
+ path: { calendar_id: calendarId, event_id: event.event_id! },
120
+ params: { user_id_type: "open_id" as never },
121
+ data: {
122
+ attendees: attendeePayload,
123
+ need_notification: payload.need_notification ?? true,
124
+ },
125
+ },
126
+ opts,
127
+ ),
128
+ { as: "user" },
129
+ );
130
+ }
131
+ return json({ event: normalizeEventTimes((response.data as { event?: Record<string, unknown> } | undefined)?.event) });
132
+ }
133
+
134
+ if (payload.action === "list") {
135
+ const startTs = payload.start_time ? parseTimeToTimestamp(payload.start_time) : null;
136
+ const endTs = payload.end_time ? parseTimeToTimestamp(payload.end_time) : null;
137
+ if (!startTs || !endTs) {
138
+ return json({ error: "start_time 和 end_time 必须为带时区的 ISO 8601 / RFC 3339 时间。" });
139
+ }
140
+ const response = await client.invoke(
141
+ "feishu_calendar_event.instance_view",
142
+ (sdk, opts) =>
143
+ sdk.calendar.calendarEvent.instanceView(
144
+ {
145
+ path: { calendar_id: calendarId },
146
+ params: {
147
+ start_time: startTs,
148
+ end_time: endTs,
149
+ page_size: payload.page_size,
150
+ page_token: payload.page_token,
151
+ user_id_type: "open_id" as never,
152
+ },
153
+ },
154
+ opts,
155
+ ),
156
+ { as: "user" },
157
+ );
158
+ assertLarkOk(response);
159
+ const items =
160
+ ((response.data as { items?: Array<Record<string, unknown>> } | undefined)?.items ?? []).map(
161
+ normalizeEventTimes,
162
+ );
163
+ return json({
164
+ events: items,
165
+ has_more: (response.data as { has_more?: boolean } | undefined)?.has_more ?? false,
166
+ page_token: (response.data as { page_token?: string } | undefined)?.page_token,
167
+ });
168
+ }
169
+
170
+ if (payload.action === "get") {
171
+ const response = await client.invoke(
172
+ "feishu_calendar_event.get",
173
+ (sdk, opts) =>
174
+ sdk.calendar.calendarEvent.get(
175
+ { path: { calendar_id: calendarId, event_id: payload.event_id! } },
176
+ opts,
177
+ ),
178
+ { as: "user" },
179
+ );
180
+ assertLarkOk(response);
181
+ return json({ event: normalizeEventTimes((response.data as { event?: Record<string, unknown> } | undefined)?.event) });
182
+ }
183
+
184
+ if (payload.action === "patch") {
185
+ const updateData: Record<string, unknown> = {};
186
+ if (payload.summary) updateData.summary = payload.summary;
187
+ if (payload.description) updateData.description = payload.description;
188
+ if (payload.location) updateData.location = { name: payload.location };
189
+ if (payload.start_time) {
190
+ const startTs = parseTimeToTimestamp(payload.start_time);
191
+ if (!startTs) return json({ error: "start_time 必须为带时区的 ISO 8601 / RFC 3339 时间。" });
192
+ updateData.start_time = { timestamp: startTs };
193
+ }
194
+ if (payload.end_time) {
195
+ const endTs = parseTimeToTimestamp(payload.end_time);
196
+ if (!endTs) return json({ error: "end_time 必须为带时区的 ISO 8601 / RFC 3339 时间。" });
197
+ updateData.end_time = { timestamp: endTs };
198
+ }
199
+ const response = await client.invoke(
200
+ "feishu_calendar_event.patch",
201
+ (sdk, opts) =>
202
+ sdk.calendar.calendarEvent.patch(
203
+ {
204
+ path: { calendar_id: calendarId, event_id: payload.event_id! },
205
+ data: updateData,
206
+ },
207
+ opts,
208
+ ),
209
+ { as: "user" },
210
+ );
211
+ assertLarkOk(response);
212
+ return json({ event: normalizeEventTimes((response.data as { event?: Record<string, unknown> } | undefined)?.event) });
213
+ }
214
+
215
+ const response = await client.invoke(
216
+ "feishu_calendar_event.delete",
217
+ (sdk, opts) =>
218
+ sdk.calendar.calendarEvent.delete(
219
+ {
220
+ path: { calendar_id: calendarId, event_id: payload.event_id! },
221
+ params: { need_notification: payload.need_notification ?? true },
222
+ },
223
+ opts,
224
+ ),
225
+ { as: "user" },
226
+ );
227
+ assertLarkOk(response);
228
+ return json({ success: true, event_id: payload.event_id });
229
+ } catch (error) {
230
+ return handleInvokeError(error, api);
231
+ }
232
+ },
233
+ },
234
+ { name: "feishu_calendar_event" },
235
+ );
236
+ }
@@ -0,0 +1,58 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { OpenClawPluginApi } from "./nextclaw-sdk/feishu.js";
3
+ import { assertLarkOk, createToolContext, handleInvokeError, json, parseTimeToRFC3339, registerTool } from "./user-tool-helpers.js";
4
+
5
+ const CalendarFreebusySchema = Type.Object({
6
+ action: Type.Literal("list"),
7
+ user_ids: Type.Array(Type.String()),
8
+ time_min: Type.String(),
9
+ time_max: Type.String(),
10
+ });
11
+
12
+ export function registerFeishuCalendarFreebusyTool(api: OpenClawPluginApi) {
13
+ const { toolClient } = createToolContext(api, "feishu_calendar_freebusy");
14
+ registerTool(api, {
15
+ name: "feishu_calendar_freebusy",
16
+ label: "Feishu Calendar Freebusy",
17
+ description: "按本人身份查询 1-10 位用户在一段时间内的忙闲状态。",
18
+ parameters: CalendarFreebusySchema,
19
+ async execute(_toolCallId, params) {
20
+ const payload = params as { action: "list"; user_ids: string[]; time_min: string; time_max: string };
21
+ try {
22
+ if (!payload.user_ids?.length || payload.user_ids.length > 10) {
23
+ return json({ error: "user_ids 必须是 1-10 个用户 open_id。" });
24
+ }
25
+ const timeMin = parseTimeToRFC3339(payload.time_min);
26
+ const timeMax = parseTimeToRFC3339(payload.time_max);
27
+ if (!timeMin || !timeMax) {
28
+ return json({ error: "time_min 和 time_max 必须是带时区的 RFC 3339 时间。" });
29
+ }
30
+ const client = toolClient();
31
+ const response = await client.invoke(
32
+ "feishu_calendar_freebusy.list",
33
+ (sdk, opts) =>
34
+ sdk.calendar.freebusy.batch(
35
+ {
36
+ data: {
37
+ time_min: timeMin,
38
+ time_max: timeMax,
39
+ user_ids: payload.user_ids,
40
+ include_external_calendar: true,
41
+ only_busy: true,
42
+ } as never,
43
+ },
44
+ opts,
45
+ ),
46
+ { as: "user" },
47
+ );
48
+ assertLarkOk(response);
49
+ return json({
50
+ freebusy_lists:
51
+ (response.data as { freebusy_lists?: unknown[] } | undefined)?.freebusy_lists ?? [],
52
+ });
53
+ } catch (error) {
54
+ return handleInvokeError(error, api);
55
+ }
56
+ },
57
+ }, { name: "feishu_calendar_freebusy" });
58
+ }