@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 +10 -0
- package/package.json +1 -1
- package/src/app-scope-checker.ts +75 -0
- package/src/auth-errors.ts +90 -0
- package/src/calendar-calendar.ts +72 -0
- package/src/calendar-event-attendee.ts +96 -0
- package/src/calendar-event.ts +236 -0
- package/src/calendar-freebusy.ts +58 -0
- package/src/calendar-shared.ts +33 -0
- package/src/calendar.ts +18 -0
- package/src/card-action.ts +19 -7
- package/src/config-schema.ts +5 -0
- package/src/device-flow.ts +188 -0
- package/src/domains.ts +19 -0
- package/src/feishu-fetch.ts +9 -0
- package/src/identity.ts +160 -0
- package/src/lark-ticket.ts +26 -0
- package/src/monitor.account.ts +48 -20
- package/src/oauth.ts +248 -0
- package/src/raw-request.ts +45 -0
- package/src/sheets.ts +431 -0
- package/src/task-comment.ts +95 -0
- package/src/task-shared.ts +10 -0
- package/src/task-subtask.ts +94 -0
- package/src/task-task.ts +172 -0
- package/src/task-tasklist.ts +174 -0
- package/src/task.ts +18 -0
- package/src/token-store.ts +211 -0
- package/src/tool-account-routing.test.ts +19 -0
- package/src/tool-account.ts +16 -0
- package/src/tool-scopes.ts +102 -0
- package/src/tools-config.test.ts +19 -0
- package/src/tools-config.ts +5 -0
- package/src/types.ts +5 -0
- package/src/uat-client.ts +159 -0
- package/src/user-tool-client.ts +224 -0
- package/src/user-tool-helpers.ts +157 -0
- package/src/user-tool-result.ts +22 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { createToolContext } from "./user-tool-helpers.js";
|
|
2
|
+
import { assertLarkOk, unixTimestampToISO8601 } from "./user-tool-helpers.js";
|
|
3
|
+
|
|
4
|
+
export function normalizeEventTimes<T extends Record<string, unknown> | undefined>(event: T): T {
|
|
5
|
+
if (!event) return event;
|
|
6
|
+
const typed = event as Record<string, unknown>;
|
|
7
|
+
const start = typed.start_time as { timestamp?: string | number } | undefined;
|
|
8
|
+
const end = typed.end_time as { timestamp?: string | number } | undefined;
|
|
9
|
+
return {
|
|
10
|
+
...typed,
|
|
11
|
+
start_time_iso8601: unixTimestampToISO8601(start?.timestamp),
|
|
12
|
+
end_time_iso8601: unixTimestampToISO8601(end?.timestamp),
|
|
13
|
+
} as T;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function resolveCalendarId(
|
|
17
|
+
client: ReturnType<typeof createToolContext>["toolClient"] extends () => infer T ? T : never,
|
|
18
|
+
calendarId?: string,
|
|
19
|
+
) {
|
|
20
|
+
if (calendarId) return calendarId;
|
|
21
|
+
const response = await client.invoke(
|
|
22
|
+
"feishu_calendar_calendar.primary",
|
|
23
|
+
(sdk, opts) => sdk.calendar.calendar.primary({}, opts),
|
|
24
|
+
{ as: "user" },
|
|
25
|
+
);
|
|
26
|
+
assertLarkOk(response);
|
|
27
|
+
const calendars = (response.data as { calendars?: Array<{ calendar_id?: string }> } | undefined)?.calendars;
|
|
28
|
+
const primaryId = calendars?.[0]?.calendar_id;
|
|
29
|
+
if (!primaryId) {
|
|
30
|
+
throw new Error("No primary calendar found for current user.");
|
|
31
|
+
}
|
|
32
|
+
return primaryId;
|
|
33
|
+
}
|
package/src/calendar.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "./nextclaw-sdk/feishu.js";
|
|
2
|
+
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
3
|
+
import { registerFeishuCalendarCalendarTool } from "./calendar-calendar.js";
|
|
4
|
+
import { registerFeishuCalendarEventTool } from "./calendar-event.js";
|
|
5
|
+
import { registerFeishuCalendarEventAttendeeTool } from "./calendar-event-attendee.js";
|
|
6
|
+
import { registerFeishuCalendarFreebusyTool } from "./calendar-freebusy.js";
|
|
7
|
+
import { resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
8
|
+
|
|
9
|
+
export function registerFeishuCalendarTools(api: OpenClawPluginApi) {
|
|
10
|
+
if (!api.config) return;
|
|
11
|
+
const accounts = listEnabledFeishuAccounts(api.config);
|
|
12
|
+
if (accounts.length === 0) return;
|
|
13
|
+
if (!resolveAnyEnabledFeishuToolsConfig(accounts).calendar) return;
|
|
14
|
+
registerFeishuCalendarCalendarTool(api);
|
|
15
|
+
registerFeishuCalendarEventTool(api);
|
|
16
|
+
registerFeishuCalendarEventAttendeeTool(api);
|
|
17
|
+
registerFeishuCalendarFreebusyTool(api);
|
|
18
|
+
}
|
package/src/card-action.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ClawdbotConfig, RuntimeEnv } from "./nextclaw-sdk/feishu.js";
|
|
2
2
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
3
3
|
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
|
|
4
|
+
import { withTicket } from "./lark-ticket.js";
|
|
4
5
|
|
|
5
6
|
export type FeishuCardActionEvent = {
|
|
6
7
|
operator: {
|
|
@@ -69,11 +70,22 @@ export async function handleFeishuCardAction(params: {
|
|
|
69
70
|
);
|
|
70
71
|
|
|
71
72
|
// Dispatch as normal message
|
|
72
|
-
await
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
await withTicket(
|
|
74
|
+
{
|
|
75
|
+
accountId: account.accountId,
|
|
76
|
+
messageId: messageEvent.message.message_id,
|
|
77
|
+
chatId: messageEvent.message.chat_id,
|
|
78
|
+
senderOpenId: event.operator.open_id,
|
|
79
|
+
chatType: messageEvent.message.chat_type,
|
|
80
|
+
startTime: Date.now(),
|
|
81
|
+
},
|
|
82
|
+
() =>
|
|
83
|
+
handleFeishuMessage({
|
|
84
|
+
cfg,
|
|
85
|
+
event: messageEvent,
|
|
86
|
+
botOpenId: params.botOpenId,
|
|
87
|
+
runtime,
|
|
88
|
+
accountId,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
79
91
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -92,6 +92,11 @@ const FeishuToolsConfigSchema = z
|
|
|
92
92
|
drive: z.boolean().optional(), // Cloud storage operations (default: true)
|
|
93
93
|
perm: z.boolean().optional(), // Permission management (default: false, sensitive)
|
|
94
94
|
scopes: z.boolean().optional(), // App scopes diagnostic (default: true)
|
|
95
|
+
calendar: z.boolean().optional(), // Calendar operations (default: true)
|
|
96
|
+
task: z.boolean().optional(), // Task operations (default: true)
|
|
97
|
+
sheets: z.boolean().optional(), // Sheets operations (default: true)
|
|
98
|
+
oauth: z.boolean().optional(), // OAuth operations (default: true)
|
|
99
|
+
identity: z.boolean().optional(), // Identity lookup operations (default: true)
|
|
95
100
|
})
|
|
96
101
|
.strict()
|
|
97
102
|
.optional();
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { feishuFetch } from "./feishu-fetch.js";
|
|
2
|
+
import type { FeishuDomain } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export type DeviceAuthResponse = {
|
|
5
|
+
deviceCode: string;
|
|
6
|
+
userCode: string;
|
|
7
|
+
verificationUri: string;
|
|
8
|
+
verificationUriComplete: string;
|
|
9
|
+
expiresIn: number;
|
|
10
|
+
interval: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DeviceFlowTokenData = {
|
|
14
|
+
accessToken: string;
|
|
15
|
+
refreshToken: string;
|
|
16
|
+
expiresIn: number;
|
|
17
|
+
refreshExpiresIn: number;
|
|
18
|
+
scope: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type DeviceFlowResult =
|
|
22
|
+
| { ok: true; token: DeviceFlowTokenData }
|
|
23
|
+
| {
|
|
24
|
+
ok: false;
|
|
25
|
+
error: "authorization_pending" | "slow_down" | "access_denied" | "expired_token";
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export function resolveOAuthEndpoints(domain: FeishuDomain): {
|
|
30
|
+
deviceAuthorization: string;
|
|
31
|
+
token: string;
|
|
32
|
+
} {
|
|
33
|
+
if (!domain || domain === "feishu") {
|
|
34
|
+
return {
|
|
35
|
+
deviceAuthorization: "https://accounts.feishu.cn/oauth/v1/device_authorization",
|
|
36
|
+
token: "https://open.feishu.cn/open-apis/authen/v2/oauth/token",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (domain === "lark") {
|
|
40
|
+
return {
|
|
41
|
+
deviceAuthorization: "https://accounts.larksuite.com/oauth/v1/device_authorization",
|
|
42
|
+
token: "https://open.larksuite.com/open-apis/authen/v2/oauth/token",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const base = domain.replace(/\/+$/, "");
|
|
47
|
+
let accountsBase = base;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(base);
|
|
50
|
+
if (parsed.hostname.startsWith("open.")) {
|
|
51
|
+
accountsBase = `${parsed.protocol}//${parsed.hostname.replace(/^open\./, "accounts.")}`;
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
deviceAuthorization: `${accountsBase}/oauth/v1/device_authorization`,
|
|
59
|
+
token: `${base}/open-apis/authen/v2/oauth/token`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function requestDeviceAuthorization(params: {
|
|
64
|
+
appId: string;
|
|
65
|
+
appSecret: string;
|
|
66
|
+
domain: FeishuDomain;
|
|
67
|
+
scope?: string;
|
|
68
|
+
}): Promise<DeviceAuthResponse> {
|
|
69
|
+
const endpoints = resolveOAuthEndpoints(params.domain);
|
|
70
|
+
let scope = params.scope?.trim() ?? "";
|
|
71
|
+
if (!scope.includes("offline_access")) {
|
|
72
|
+
scope = scope ? `${scope} offline_access` : "offline_access";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const basicAuth = Buffer.from(`${params.appId}:${params.appSecret}`).toString("base64");
|
|
76
|
+
const body = new URLSearchParams();
|
|
77
|
+
body.set("client_id", params.appId);
|
|
78
|
+
body.set("scope", scope);
|
|
79
|
+
|
|
80
|
+
const response = await feishuFetch(endpoints.deviceAuthorization, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
84
|
+
Authorization: `Basic ${basicAuth}`,
|
|
85
|
+
},
|
|
86
|
+
body: body.toString(),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
90
|
+
if (!response.ok || data.error) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
String(data.error_description ?? data.error ?? `HTTP ${response.status}`),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
deviceCode: String(data.device_code ?? ""),
|
|
98
|
+
userCode: String(data.user_code ?? ""),
|
|
99
|
+
verificationUri: String(data.verification_uri ?? ""),
|
|
100
|
+
verificationUriComplete: String(data.verification_uri_complete ?? data.verification_uri ?? ""),
|
|
101
|
+
expiresIn: Number(data.expires_in ?? 240),
|
|
102
|
+
interval: Number(data.interval ?? 5),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const timer = setTimeout(resolve, ms);
|
|
109
|
+
signal?.addEventListener(
|
|
110
|
+
"abort",
|
|
111
|
+
() => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
114
|
+
},
|
|
115
|
+
{ once: true },
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function pollDeviceToken(params: {
|
|
121
|
+
appId: string;
|
|
122
|
+
appSecret: string;
|
|
123
|
+
domain: FeishuDomain;
|
|
124
|
+
deviceCode: string;
|
|
125
|
+
interval: number;
|
|
126
|
+
expiresIn: number;
|
|
127
|
+
signal?: AbortSignal;
|
|
128
|
+
}): Promise<DeviceFlowResult> {
|
|
129
|
+
const endpoints = resolveOAuthEndpoints(params.domain);
|
|
130
|
+
const deadline = Date.now() + params.expiresIn * 1000;
|
|
131
|
+
let interval = params.interval;
|
|
132
|
+
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
if (params.signal?.aborted) {
|
|
135
|
+
return { ok: false, error: "expired_token", message: "Polling was cancelled" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await sleep(interval * 1000, params.signal);
|
|
139
|
+
|
|
140
|
+
const response = await feishuFetch(endpoints.token, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
143
|
+
body: new URLSearchParams({
|
|
144
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
145
|
+
device_code: params.deviceCode,
|
|
146
|
+
client_id: params.appId,
|
|
147
|
+
client_secret: params.appSecret,
|
|
148
|
+
}).toString(),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
152
|
+
const error = typeof data.error === "string" ? data.error : undefined;
|
|
153
|
+
|
|
154
|
+
if (!error && data.access_token) {
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
token: {
|
|
158
|
+
accessToken: String(data.access_token),
|
|
159
|
+
refreshToken: String(data.refresh_token ?? ""),
|
|
160
|
+
expiresIn: Number(data.expires_in ?? 7200),
|
|
161
|
+
refreshExpiresIn: Number(data.refresh_token_expires_in ?? data.expires_in ?? 7200),
|
|
162
|
+
scope: String(data.scope ?? ""),
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (error === "authorization_pending") {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (error === "slow_down") {
|
|
171
|
+
interval = Math.min(interval + 5, 60);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (error === "access_denied" || error === "expired_token") {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error,
|
|
178
|
+
message: String(data.error_description ?? error),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: "expired_token",
|
|
186
|
+
message: "Device authorization expired before approval finished.",
|
|
187
|
+
};
|
|
188
|
+
}
|
package/src/domains.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FeishuDomain } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export function openPlatformDomain(domain?: FeishuDomain): string {
|
|
4
|
+
return domain === "lark" ? "https://open.larksuite.com" : "https://open.feishu.cn";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function wwwDomain(domain?: FeishuDomain): string {
|
|
8
|
+
return domain === "lark" ? "https://www.larksuite.com" : "https://www.feishu.cn";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveDomainUrl(domain: FeishuDomain): string {
|
|
12
|
+
if (domain === "feishu") {
|
|
13
|
+
return "https://open.feishu.cn";
|
|
14
|
+
}
|
|
15
|
+
if (domain === "lark") {
|
|
16
|
+
return "https://open.larksuite.com";
|
|
17
|
+
}
|
|
18
|
+
return domain.replace(/\/+$/, "");
|
|
19
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const FEISHU_USER_AGENT = "nextclaw-feishu-plugin/0.2";
|
|
2
|
+
|
|
3
|
+
export function feishuFetch(url: string | URL | Request, init?: RequestInit): Promise<Response> {
|
|
4
|
+
const headers = new Headers(init?.headers);
|
|
5
|
+
if (!headers.has("User-Agent")) {
|
|
6
|
+
headers.set("User-Agent", FEISHU_USER_AGENT);
|
|
7
|
+
}
|
|
8
|
+
return fetch(url, { ...init, headers });
|
|
9
|
+
}
|
package/src/identity.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "./nextclaw-sdk/feishu.js";
|
|
3
|
+
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
|
+
import { resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
5
|
+
import {
|
|
6
|
+
assertLarkOk,
|
|
7
|
+
createToolContext,
|
|
8
|
+
handleInvokeError,
|
|
9
|
+
json,
|
|
10
|
+
registerTool,
|
|
11
|
+
StringEnum,
|
|
12
|
+
} from "./user-tool-helpers.js";
|
|
13
|
+
|
|
14
|
+
const GetUserSchema = Type.Object({
|
|
15
|
+
user_id: Type.Optional(
|
|
16
|
+
Type.String({
|
|
17
|
+
description: "用户 ID(如 ou_xxx)。不传时获取当前消息发送者本人信息。",
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
user_id_type: Type.Optional(StringEnum(["open_id", "union_id", "user_id"])),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const SearchUserSchema = Type.Object({
|
|
24
|
+
query: Type.String({
|
|
25
|
+
description: "搜索关键词,可匹配姓名、邮箱、手机号等。",
|
|
26
|
+
}),
|
|
27
|
+
page_size: Type.Optional(
|
|
28
|
+
Type.Integer({
|
|
29
|
+
description: "分页大小,默认 20,最大 200。",
|
|
30
|
+
minimum: 1,
|
|
31
|
+
maximum: 200,
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
page_token: Type.Optional(
|
|
35
|
+
Type.String({
|
|
36
|
+
description: "翻页 token。",
|
|
37
|
+
}),
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export function registerFeishuIdentityTools(api: OpenClawPluginApi) {
|
|
42
|
+
if (!api.config) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const accounts = listEnabledFeishuAccounts(api.config);
|
|
47
|
+
if (accounts.length === 0) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const toolsConfig = resolveAnyEnabledFeishuToolsConfig(accounts);
|
|
52
|
+
if (!toolsConfig.identity) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
registerFeishuGetUserTool(api);
|
|
57
|
+
registerFeishuSearchUserTool(api);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function registerFeishuGetUserTool(api: OpenClawPluginApi) {
|
|
61
|
+
const { toolClient, log } = createToolContext(api, "feishu_get_user");
|
|
62
|
+
|
|
63
|
+
registerTool(
|
|
64
|
+
api,
|
|
65
|
+
{
|
|
66
|
+
name: "feishu_get_user",
|
|
67
|
+
label: "Feishu: Get User",
|
|
68
|
+
description:
|
|
69
|
+
"获取飞书用户信息。不传 user_id 时默认获取当前消息发送者本人信息;传 user_id 时获取指定用户信息。",
|
|
70
|
+
parameters: GetUserSchema,
|
|
71
|
+
async execute(_toolCallId, params) {
|
|
72
|
+
const payload = params as {
|
|
73
|
+
user_id?: string;
|
|
74
|
+
user_id_type?: "open_id" | "union_id" | "user_id";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const client = toolClient();
|
|
79
|
+
if (!payload.user_id) {
|
|
80
|
+
log.info("fetching current user info");
|
|
81
|
+
const response = await client.invoke(
|
|
82
|
+
"feishu_get_user.default",
|
|
83
|
+
(sdk, opts) => sdk.authen.userInfo.get({}, opts),
|
|
84
|
+
{ as: "user" },
|
|
85
|
+
);
|
|
86
|
+
assertLarkOk(response);
|
|
87
|
+
return json({ user: response.data });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
log.info(`fetching user ${payload.user_id}`);
|
|
91
|
+
const response = await client.invoke(
|
|
92
|
+
"feishu_get_user.default",
|
|
93
|
+
(sdk, opts) =>
|
|
94
|
+
sdk.contact.user.get(
|
|
95
|
+
{
|
|
96
|
+
path: { user_id: payload.user_id! },
|
|
97
|
+
params: {
|
|
98
|
+
user_id_type: payload.user_id_type ?? "open_id",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
opts,
|
|
102
|
+
),
|
|
103
|
+
{ as: "user" },
|
|
104
|
+
);
|
|
105
|
+
assertLarkOk(response);
|
|
106
|
+
return json({ user: response.data?.user });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
return handleInvokeError(error, api);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{ name: "feishu_get_user" },
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function registerFeishuSearchUserTool(api: OpenClawPluginApi) {
|
|
117
|
+
const { toolClient, log } = createToolContext(api, "feishu_search_user");
|
|
118
|
+
|
|
119
|
+
registerTool(
|
|
120
|
+
api,
|
|
121
|
+
{
|
|
122
|
+
name: "feishu_search_user",
|
|
123
|
+
label: "Feishu: Search User",
|
|
124
|
+
description: "搜索飞书员工信息,返回姓名、部门、open_id 等结果。",
|
|
125
|
+
parameters: SearchUserSchema,
|
|
126
|
+
async execute(_toolCallId, params) {
|
|
127
|
+
const payload = params as {
|
|
128
|
+
query: string;
|
|
129
|
+
page_size?: number;
|
|
130
|
+
page_token?: string;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const client = toolClient();
|
|
135
|
+
log.info(`search query="${payload.query}"`);
|
|
136
|
+
const response = await client.invokeByPath<{
|
|
137
|
+
data?: { users?: unknown[]; has_more?: boolean; page_token?: string };
|
|
138
|
+
}>("feishu_search_user.default", "/open-apis/search/v1/user", {
|
|
139
|
+
method: "GET",
|
|
140
|
+
query: {
|
|
141
|
+
query: payload.query,
|
|
142
|
+
page_size: String(payload.page_size ?? 20),
|
|
143
|
+
...(payload.page_token ? { page_token: payload.page_token } : {}),
|
|
144
|
+
},
|
|
145
|
+
as: "user",
|
|
146
|
+
});
|
|
147
|
+
assertLarkOk(response as { code?: number; msg?: string });
|
|
148
|
+
return json({
|
|
149
|
+
users: response.data?.users ?? [],
|
|
150
|
+
has_more: response.data?.has_more ?? false,
|
|
151
|
+
page_token: response.data?.page_token,
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return handleInvokeError(error, api);
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{ name: "feishu_search_user" },
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
export type LarkTicket = {
|
|
4
|
+
messageId: string;
|
|
5
|
+
chatId: string;
|
|
6
|
+
accountId: string;
|
|
7
|
+
startTime: number;
|
|
8
|
+
senderOpenId?: string;
|
|
9
|
+
chatType?: "p2p" | "group" | "private";
|
|
10
|
+
threadId?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const store = new AsyncLocalStorage<LarkTicket>();
|
|
14
|
+
|
|
15
|
+
export function withTicket<T>(ticket: LarkTicket, fn: () => T | Promise<T>): T | Promise<T> {
|
|
16
|
+
return store.run(ticket, fn);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getTicket(): LarkTicket | undefined {
|
|
20
|
+
return store.getStore();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ticketElapsed(): number {
|
|
24
|
+
const ticket = getTicket();
|
|
25
|
+
return ticket ? Date.now() - ticket.startTime : 0;
|
|
26
|
+
}
|
package/src/monitor.account.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as crypto from "crypto";
|
|
2
|
-
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
+
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
3
3
|
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "./nextclaw-sdk/feishu.js";
|
|
4
4
|
import { resolveFeishuAccount } from "./accounts.js";
|
|
5
5
|
import { raceWithTimeoutAndAbort } from "./async.js";
|
|
@@ -24,6 +24,7 @@ import { botNames, botOpenIds } from "./monitor.state.js";
|
|
|
24
24
|
import { monitorWebhook, monitorWebSocket } from "./monitor.transport.js";
|
|
25
25
|
import { getFeishuRuntime } from "./runtime.js";
|
|
26
26
|
import { getMessageFeishu } from "./send.js";
|
|
27
|
+
import { withTicket } from "./lark-ticket.js";
|
|
27
28
|
import type { FeishuChatType, ResolvedFeishuAccount } from "./types.js";
|
|
28
29
|
|
|
29
30
|
const FEISHU_REACTION_VERIFY_TIMEOUT_MS = 1_500;
|
|
@@ -256,16 +257,28 @@ function registerEventHandlers(
|
|
|
256
257
|
const dispatchFeishuMessage = async (event: FeishuMessageEvent) => {
|
|
257
258
|
const chatId = event.message.chat_id?.trim() || "unknown";
|
|
258
259
|
const task = () =>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
withTicket(
|
|
261
|
+
{
|
|
262
|
+
accountId,
|
|
263
|
+
messageId: event.message.message_id,
|
|
264
|
+
chatId: event.message.chat_id,
|
|
265
|
+
senderOpenId: event.sender.sender_id.open_id?.trim() || undefined,
|
|
266
|
+
chatType: event.message.chat_type,
|
|
267
|
+
threadId: event.message.thread_id?.trim() || event.message.root_id?.trim() || undefined,
|
|
268
|
+
startTime: Date.now(),
|
|
269
|
+
},
|
|
270
|
+
() =>
|
|
271
|
+
handleFeishuMessage({
|
|
272
|
+
cfg,
|
|
273
|
+
event,
|
|
274
|
+
botOpenId: botOpenIds.get(accountId),
|
|
275
|
+
botName: botNames.get(accountId),
|
|
276
|
+
runtime,
|
|
277
|
+
chatHistories,
|
|
278
|
+
accountId,
|
|
279
|
+
processingClaimHeld: true,
|
|
280
|
+
}),
|
|
281
|
+
);
|
|
269
282
|
await enqueue(chatId, task);
|
|
270
283
|
};
|
|
271
284
|
const resolveSenderDebounceId = (event: FeishuMessageEvent): string | undefined => {
|
|
@@ -441,15 +454,30 @@ function registerEventHandlers(
|
|
|
441
454
|
if (!syntheticEvent) {
|
|
442
455
|
return;
|
|
443
456
|
}
|
|
444
|
-
const promise =
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
457
|
+
const promise = withTicket(
|
|
458
|
+
{
|
|
459
|
+
accountId,
|
|
460
|
+
messageId: syntheticEvent.message.message_id,
|
|
461
|
+
chatId: syntheticEvent.message.chat_id,
|
|
462
|
+
senderOpenId: syntheticEvent.sender.sender_id.open_id?.trim() || undefined,
|
|
463
|
+
chatType: syntheticEvent.message.chat_type,
|
|
464
|
+
threadId:
|
|
465
|
+
syntheticEvent.message.thread_id?.trim() ||
|
|
466
|
+
syntheticEvent.message.root_id?.trim() ||
|
|
467
|
+
undefined,
|
|
468
|
+
startTime: Date.now(),
|
|
469
|
+
},
|
|
470
|
+
() =>
|
|
471
|
+
handleFeishuMessage({
|
|
472
|
+
cfg,
|
|
473
|
+
event: syntheticEvent,
|
|
474
|
+
botOpenId: myBotId,
|
|
475
|
+
botName: botNames.get(accountId),
|
|
476
|
+
runtime,
|
|
477
|
+
chatHistories,
|
|
478
|
+
accountId,
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
453
481
|
if (fireAndForget) {
|
|
454
482
|
promise.catch((err) => {
|
|
455
483
|
error(`feishu[${accountId}]: error handling reaction: ${String(err)}`);
|