@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
package/src/sheets.ts
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "./nextclaw-sdk/feishu.js";
|
|
3
|
+
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
|
+
import { wwwDomain } from "./domains.js";
|
|
5
|
+
import { resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
|
|
6
|
+
import {
|
|
7
|
+
assertLarkOk,
|
|
8
|
+
createToolContext,
|
|
9
|
+
handleInvokeError,
|
|
10
|
+
json,
|
|
11
|
+
registerTool,
|
|
12
|
+
StringEnum,
|
|
13
|
+
} from "./user-tool-helpers.js";
|
|
14
|
+
|
|
15
|
+
const MAX_READ_ROWS = 200;
|
|
16
|
+
const MAX_WRITE_ROWS = 5000;
|
|
17
|
+
const MAX_WRITE_COLS = 100;
|
|
18
|
+
|
|
19
|
+
type SheetParams = {
|
|
20
|
+
action: "info" | "read" | "write" | "append" | "find" | "create" | "export";
|
|
21
|
+
url?: string;
|
|
22
|
+
spreadsheet_token?: string;
|
|
23
|
+
range?: string;
|
|
24
|
+
sheet_id?: string;
|
|
25
|
+
value_render_option?: "FormattedValue" | "UnformattedValue" | "Formula" | "ToString";
|
|
26
|
+
values?: unknown[][];
|
|
27
|
+
find?: string;
|
|
28
|
+
match_case?: boolean;
|
|
29
|
+
match_entire_cell?: boolean;
|
|
30
|
+
search_by_regex?: boolean;
|
|
31
|
+
include_formulas?: boolean;
|
|
32
|
+
title?: string;
|
|
33
|
+
folder_token?: string;
|
|
34
|
+
headers?: unknown[];
|
|
35
|
+
data?: unknown[][];
|
|
36
|
+
file_extension?: "xlsx" | "csv";
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const SheetSchema = Type.Union([
|
|
40
|
+
Type.Object({ action: Type.Literal("info"), url: Type.Optional(Type.String()), spreadsheet_token: Type.Optional(Type.String()) }),
|
|
41
|
+
Type.Object({
|
|
42
|
+
action: Type.Literal("read"),
|
|
43
|
+
url: Type.Optional(Type.String()),
|
|
44
|
+
spreadsheet_token: Type.Optional(Type.String()),
|
|
45
|
+
range: Type.Optional(Type.String()),
|
|
46
|
+
sheet_id: Type.Optional(Type.String()),
|
|
47
|
+
value_render_option: Type.Optional(StringEnum(["FormattedValue", "UnformattedValue", "Formula", "ToString"])),
|
|
48
|
+
}),
|
|
49
|
+
Type.Object({
|
|
50
|
+
action: Type.Literal("write"),
|
|
51
|
+
url: Type.Optional(Type.String()),
|
|
52
|
+
spreadsheet_token: Type.Optional(Type.String()),
|
|
53
|
+
range: Type.Optional(Type.String()),
|
|
54
|
+
sheet_id: Type.Optional(Type.String()),
|
|
55
|
+
values: Type.Array(Type.Array(Type.Any())),
|
|
56
|
+
}),
|
|
57
|
+
Type.Object({
|
|
58
|
+
action: Type.Literal("append"),
|
|
59
|
+
url: Type.Optional(Type.String()),
|
|
60
|
+
spreadsheet_token: Type.Optional(Type.String()),
|
|
61
|
+
range: Type.Optional(Type.String()),
|
|
62
|
+
sheet_id: Type.Optional(Type.String()),
|
|
63
|
+
values: Type.Array(Type.Array(Type.Any())),
|
|
64
|
+
}),
|
|
65
|
+
Type.Object({
|
|
66
|
+
action: Type.Literal("find"),
|
|
67
|
+
url: Type.Optional(Type.String()),
|
|
68
|
+
spreadsheet_token: Type.Optional(Type.String()),
|
|
69
|
+
sheet_id: Type.String(),
|
|
70
|
+
range: Type.Optional(Type.String()),
|
|
71
|
+
find: Type.String(),
|
|
72
|
+
match_case: Type.Optional(Type.Boolean()),
|
|
73
|
+
match_entire_cell: Type.Optional(Type.Boolean()),
|
|
74
|
+
search_by_regex: Type.Optional(Type.Boolean()),
|
|
75
|
+
include_formulas: Type.Optional(Type.Boolean()),
|
|
76
|
+
}),
|
|
77
|
+
Type.Object({
|
|
78
|
+
action: Type.Literal("create"),
|
|
79
|
+
title: Type.String(),
|
|
80
|
+
folder_token: Type.Optional(Type.String()),
|
|
81
|
+
headers: Type.Optional(Type.Array(Type.Any())),
|
|
82
|
+
data: Type.Optional(Type.Array(Type.Array(Type.Any()))),
|
|
83
|
+
}),
|
|
84
|
+
Type.Object({
|
|
85
|
+
action: Type.Literal("export"),
|
|
86
|
+
url: Type.Optional(Type.String()),
|
|
87
|
+
spreadsheet_token: Type.Optional(Type.String()),
|
|
88
|
+
sheet_id: Type.Optional(Type.String()),
|
|
89
|
+
file_extension: StringEnum(["xlsx", "csv"]),
|
|
90
|
+
}),
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
function parseSheetUrl(url: string): { token: string; sheetId?: string } | null {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(url);
|
|
96
|
+
const match = parsed.pathname.match(/\/sheets\/([^/?#]+)/);
|
|
97
|
+
if (!match) return null;
|
|
98
|
+
return { token: match[1], sheetId: parsed.searchParams.get("sheet") || undefined };
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveToken(params: SheetParams) {
|
|
105
|
+
if (params.spreadsheet_token) return { token: params.spreadsheet_token };
|
|
106
|
+
if (params.url) return parseSheetUrl(params.url);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function colLetter(n: number): string {
|
|
111
|
+
let result = "";
|
|
112
|
+
let value = n;
|
|
113
|
+
while (value > 0) {
|
|
114
|
+
value -= 1;
|
|
115
|
+
result = String.fromCharCode(65 + (value % 26)) + result;
|
|
116
|
+
value = Math.floor(value / 26);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function flattenCellValue(cell: unknown): unknown {
|
|
122
|
+
if (!Array.isArray(cell)) return cell;
|
|
123
|
+
if (cell.length > 0 && cell.every((segment) => segment && typeof segment === "object" && "text" in (segment as object))) {
|
|
124
|
+
return cell.map((segment) => String((segment as { text?: unknown }).text ?? "")).join("");
|
|
125
|
+
}
|
|
126
|
+
return cell;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function resolveSheetRange(
|
|
130
|
+
client: ReturnType<typeof createToolContext>["toolClient"] extends () => infer T ? T : never,
|
|
131
|
+
token: string,
|
|
132
|
+
range?: string,
|
|
133
|
+
sheetId?: string,
|
|
134
|
+
) {
|
|
135
|
+
if (range) return range;
|
|
136
|
+
if (sheetId) return sheetId;
|
|
137
|
+
const response = await client.invoke(
|
|
138
|
+
"feishu_sheet.info",
|
|
139
|
+
(sdk, opts) => sdk.sheets.spreadsheetSheet.query({ path: { spreadsheet_token: token } }, opts),
|
|
140
|
+
{ as: "user" },
|
|
141
|
+
);
|
|
142
|
+
assertLarkOk(response);
|
|
143
|
+
const firstSheet = (response.data as { sheets?: Array<{ sheet_id?: string }> } | undefined)?.sheets?.[0];
|
|
144
|
+
if (!firstSheet?.sheet_id) {
|
|
145
|
+
throw new Error("Spreadsheet has no worksheets.");
|
|
146
|
+
}
|
|
147
|
+
return firstSheet.sheet_id;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function handleCreate(
|
|
151
|
+
client: ReturnType<typeof createToolContext>["toolClient"] extends () => infer T ? T : never,
|
|
152
|
+
payload: SheetParams,
|
|
153
|
+
) {
|
|
154
|
+
const createResponse = await client.invoke(
|
|
155
|
+
"feishu_sheet.create",
|
|
156
|
+
(sdk, opts) =>
|
|
157
|
+
sdk.sheets.spreadsheet.create(
|
|
158
|
+
{ data: { title: payload.title, folder_token: payload.folder_token } },
|
|
159
|
+
opts,
|
|
160
|
+
),
|
|
161
|
+
{ as: "user" },
|
|
162
|
+
);
|
|
163
|
+
assertLarkOk(createResponse);
|
|
164
|
+
const spreadsheet = (createResponse.data as { spreadsheet?: { spreadsheet_token?: string; title?: string } } | undefined)?.spreadsheet;
|
|
165
|
+
const token = spreadsheet?.spreadsheet_token;
|
|
166
|
+
if (!token) {
|
|
167
|
+
return json({ error: "创建电子表格失败,未返回 spreadsheet_token。" });
|
|
168
|
+
}
|
|
169
|
+
const allRows = [
|
|
170
|
+
...(payload.headers ? [payload.headers] : []),
|
|
171
|
+
...((payload.data as unknown[][] | undefined) ?? []),
|
|
172
|
+
];
|
|
173
|
+
if (allRows.length > 0) {
|
|
174
|
+
const sheetsResponse = await client.invoke(
|
|
175
|
+
"feishu_sheet.create",
|
|
176
|
+
(sdk, opts) => sdk.sheets.spreadsheetSheet.query({ path: { spreadsheet_token: token } }, opts),
|
|
177
|
+
{ as: "user" },
|
|
178
|
+
);
|
|
179
|
+
assertLarkOk(sheetsResponse);
|
|
180
|
+
const firstSheet = (sheetsResponse.data as { sheets?: Array<{ sheet_id?: string }> } | undefined)?.sheets?.[0];
|
|
181
|
+
if (firstSheet?.sheet_id) {
|
|
182
|
+
const numCols = Math.max(...allRows.map((row) => row.length), 1);
|
|
183
|
+
const range = `${firstSheet.sheet_id}!A1:${colLetter(numCols)}${allRows.length}`;
|
|
184
|
+
await client.invokeByPath("feishu_sheet.create", `/open-apis/sheets/v2/spreadsheets/${token}/values`, {
|
|
185
|
+
method: "PUT",
|
|
186
|
+
body: { valueRange: { range, values: allRows } },
|
|
187
|
+
as: "user",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return json({
|
|
192
|
+
spreadsheet_token: token,
|
|
193
|
+
title: spreadsheet?.title ?? payload.title,
|
|
194
|
+
url: `${wwwDomain(client.account.domain)}/sheets/${token}`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function handleInfo(
|
|
199
|
+
client: ReturnType<typeof createToolContext>["toolClient"] extends () => infer T ? T : never,
|
|
200
|
+
token: string,
|
|
201
|
+
) {
|
|
202
|
+
const [spreadsheetRes, sheetsRes] = await Promise.all([
|
|
203
|
+
client.invoke(
|
|
204
|
+
"feishu_sheet.info",
|
|
205
|
+
(sdk, opts) => sdk.sheets.spreadsheet.get({ path: { spreadsheet_token: token } }, opts),
|
|
206
|
+
{ as: "user" },
|
|
207
|
+
),
|
|
208
|
+
client.invoke(
|
|
209
|
+
"feishu_sheet.info",
|
|
210
|
+
(sdk, opts) => sdk.sheets.spreadsheetSheet.query({ path: { spreadsheet_token: token } }, opts),
|
|
211
|
+
{ as: "user" },
|
|
212
|
+
),
|
|
213
|
+
]);
|
|
214
|
+
assertLarkOk(spreadsheetRes);
|
|
215
|
+
assertLarkOk(sheetsRes);
|
|
216
|
+
return json({
|
|
217
|
+
title: (spreadsheetRes.data as { spreadsheet?: { title?: string } } | undefined)?.spreadsheet?.title,
|
|
218
|
+
spreadsheet_token: token,
|
|
219
|
+
url: `${wwwDomain(client.account.domain)}/sheets/${token}`,
|
|
220
|
+
sheets:
|
|
221
|
+
((sheetsRes.data as { sheets?: Array<Record<string, unknown>> } | undefined)?.sheets ?? []).map((sheet) => ({
|
|
222
|
+
sheet_id: sheet.sheet_id,
|
|
223
|
+
title: sheet.title,
|
|
224
|
+
index: sheet.index,
|
|
225
|
+
row_count: (sheet.grid_properties as { row_count?: number } | undefined)?.row_count,
|
|
226
|
+
column_count: (sheet.grid_properties as { column_count?: number } | undefined)?.column_count,
|
|
227
|
+
})),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function handleRead(
|
|
232
|
+
client: ReturnType<typeof createToolContext>["toolClient"] extends () => infer T ? T : never,
|
|
233
|
+
tokenInfo: { token: string; sheetId?: string },
|
|
234
|
+
payload: SheetParams,
|
|
235
|
+
) {
|
|
236
|
+
const range = await resolveSheetRange(client, tokenInfo.token, payload.range, payload.sheet_id ?? tokenInfo.sheetId);
|
|
237
|
+
const response = await client.invokeByPath<{
|
|
238
|
+
data?: { valueRange?: { range?: string; values?: unknown[][] } };
|
|
239
|
+
}>("feishu_sheet.read", `/open-apis/sheets/v2/spreadsheets/${tokenInfo.token}/values/${encodeURIComponent(range)}`, {
|
|
240
|
+
method: "GET",
|
|
241
|
+
query: {
|
|
242
|
+
valueRenderOption: payload.value_render_option ?? "ToString",
|
|
243
|
+
dateTimeRenderOption: "FormattedString",
|
|
244
|
+
},
|
|
245
|
+
as: "user",
|
|
246
|
+
});
|
|
247
|
+
const values = (response.data?.valueRange?.values ?? []).map((row) => row.map(flattenCellValue));
|
|
248
|
+
return json({
|
|
249
|
+
range: response.data?.valueRange?.range,
|
|
250
|
+
values: values.slice(0, MAX_READ_ROWS),
|
|
251
|
+
...(values.length > MAX_READ_ROWS
|
|
252
|
+
? {
|
|
253
|
+
truncated: true,
|
|
254
|
+
total_rows: values.length,
|
|
255
|
+
hint: `结果超过 ${MAX_READ_ROWS} 行,已截断。请缩小 range 后重试。`,
|
|
256
|
+
}
|
|
257
|
+
: {}),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function handleWrite(
|
|
262
|
+
client: ReturnType<typeof createToolContext>["toolClient"] extends () => infer T ? T : never,
|
|
263
|
+
tokenInfo: { token: string; sheetId?: string },
|
|
264
|
+
payload: SheetParams,
|
|
265
|
+
) {
|
|
266
|
+
if ((payload.values?.length ?? 0) > MAX_WRITE_ROWS) {
|
|
267
|
+
return json({ error: `写入行数超过限制 ${MAX_WRITE_ROWS}` });
|
|
268
|
+
}
|
|
269
|
+
if ((payload.values ?? []).some((row) => row.length > MAX_WRITE_COLS)) {
|
|
270
|
+
return json({ error: `写入列数超过限制 ${MAX_WRITE_COLS}` });
|
|
271
|
+
}
|
|
272
|
+
const range = await resolveSheetRange(client, tokenInfo.token, payload.range, payload.sheet_id ?? tokenInfo.sheetId);
|
|
273
|
+
const response = await client.invokeByPath<{
|
|
274
|
+
data?: {
|
|
275
|
+
updatedRange?: string;
|
|
276
|
+
updatedRows?: number;
|
|
277
|
+
updatedColumns?: number;
|
|
278
|
+
updatedCells?: number;
|
|
279
|
+
revision?: number;
|
|
280
|
+
updates?: {
|
|
281
|
+
updatedRange?: string;
|
|
282
|
+
updatedRows?: number;
|
|
283
|
+
updatedColumns?: number;
|
|
284
|
+
updatedCells?: number;
|
|
285
|
+
revision?: number;
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
}>(
|
|
289
|
+
payload.action === "write" ? "feishu_sheet.write" : "feishu_sheet.append",
|
|
290
|
+
`/open-apis/sheets/v2/spreadsheets/${tokenInfo.token}/${payload.action === "write" ? "values" : "values_append"}`,
|
|
291
|
+
{
|
|
292
|
+
method: payload.action === "write" ? "PUT" : "POST",
|
|
293
|
+
body: { valueRange: { range, values: payload.values } },
|
|
294
|
+
as: "user",
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
const updates = response.data?.updates ?? response.data;
|
|
298
|
+
return json({
|
|
299
|
+
updated_range: updates?.updatedRange,
|
|
300
|
+
updated_rows: updates?.updatedRows,
|
|
301
|
+
updated_columns: updates?.updatedColumns,
|
|
302
|
+
updated_cells: updates?.updatedCells,
|
|
303
|
+
revision: updates?.revision,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function handleFind(
|
|
308
|
+
client: ReturnType<typeof createToolContext>["toolClient"] extends () => infer T ? T : never,
|
|
309
|
+
token: string,
|
|
310
|
+
payload: SheetParams,
|
|
311
|
+
) {
|
|
312
|
+
const response = await client.invoke(
|
|
313
|
+
"feishu_sheet.find",
|
|
314
|
+
(sdk, opts) =>
|
|
315
|
+
sdk.sheets.spreadsheetSheet.find(
|
|
316
|
+
{
|
|
317
|
+
path: { spreadsheet_token: token, sheet_id: payload.sheet_id! },
|
|
318
|
+
data: {
|
|
319
|
+
find: payload.find,
|
|
320
|
+
find_condition: {
|
|
321
|
+
range: payload.range ? `${payload.sheet_id}!${payload.range}` : payload.sheet_id,
|
|
322
|
+
...(payload.match_case !== undefined ? { match_case: !payload.match_case } : {}),
|
|
323
|
+
...(payload.match_entire_cell !== undefined ? { match_entire_cell: payload.match_entire_cell } : {}),
|
|
324
|
+
...(payload.search_by_regex !== undefined ? { search_by_regex: payload.search_by_regex } : {}),
|
|
325
|
+
...(payload.include_formulas !== undefined ? { include_formulas: payload.include_formulas } : {}),
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
opts,
|
|
330
|
+
),
|
|
331
|
+
{ as: "user" },
|
|
332
|
+
);
|
|
333
|
+
assertLarkOk(response);
|
|
334
|
+
return json({
|
|
335
|
+
matched_cells: (response.data as { find_result?: { matched_cells?: unknown[] } } | undefined)?.find_result?.matched_cells ?? [],
|
|
336
|
+
matched_formula_cells:
|
|
337
|
+
(response.data as { find_result?: { matched_formula_cells?: unknown[] } } | undefined)?.find_result?.matched_formula_cells ?? [],
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function handleExport(
|
|
342
|
+
client: ReturnType<typeof createToolContext>["toolClient"] extends () => infer T ? T : never,
|
|
343
|
+
token: string,
|
|
344
|
+
payload: SheetParams,
|
|
345
|
+
) {
|
|
346
|
+
const exportCreate = await client.invoke(
|
|
347
|
+
"feishu_sheet.export",
|
|
348
|
+
(sdk, opts) =>
|
|
349
|
+
sdk.drive.exportTask.create(
|
|
350
|
+
{
|
|
351
|
+
data: {
|
|
352
|
+
file_extension: payload.file_extension,
|
|
353
|
+
token,
|
|
354
|
+
type: "sheet",
|
|
355
|
+
sub_id: payload.sheet_id,
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
opts,
|
|
359
|
+
),
|
|
360
|
+
{ as: "user" },
|
|
361
|
+
);
|
|
362
|
+
assertLarkOk(exportCreate);
|
|
363
|
+
const ticket = (exportCreate.data as { ticket?: string } | undefined)?.ticket;
|
|
364
|
+
if (!ticket) {
|
|
365
|
+
return json({ error: "导出任务创建失败,未返回 ticket。" });
|
|
366
|
+
}
|
|
367
|
+
for (let i = 0; i < 30; i += 1) {
|
|
368
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
369
|
+
const exportStatus = await client.invoke(
|
|
370
|
+
"feishu_sheet.export",
|
|
371
|
+
(sdk, opts) =>
|
|
372
|
+
sdk.drive.exportTask.get({ path: { ticket }, params: { token } }, opts),
|
|
373
|
+
{ as: "user" },
|
|
374
|
+
);
|
|
375
|
+
assertLarkOk(exportStatus);
|
|
376
|
+
const result = (exportStatus.data as {
|
|
377
|
+
result?: {
|
|
378
|
+
job_status?: number;
|
|
379
|
+
file_token?: string;
|
|
380
|
+
file_name?: string;
|
|
381
|
+
file_size?: number;
|
|
382
|
+
job_error_msg?: string;
|
|
383
|
+
};
|
|
384
|
+
} | undefined)?.result;
|
|
385
|
+
if (result?.job_status === 0) {
|
|
386
|
+
return json({
|
|
387
|
+
file_token: result.file_token,
|
|
388
|
+
file_name: result.file_name,
|
|
389
|
+
file_size: result.file_size,
|
|
390
|
+
file_extension: payload.file_extension,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
if ((result?.job_status ?? 0) >= 3) {
|
|
394
|
+
return json({ error: result?.job_error_msg ?? `导出失败 (status=${result?.job_status})` });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return json({ error: "导出任务超时,请稍后重试。" });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function registerFeishuSheetsTools(api: OpenClawPluginApi) {
|
|
401
|
+
if (!api.config) return;
|
|
402
|
+
const accounts = listEnabledFeishuAccounts(api.config);
|
|
403
|
+
if (accounts.length === 0) return;
|
|
404
|
+
if (!resolveAnyEnabledFeishuToolsConfig(accounts).sheets) return;
|
|
405
|
+
|
|
406
|
+
registerTool(api, {
|
|
407
|
+
name: "feishu_sheet",
|
|
408
|
+
label: "Feishu Sheet",
|
|
409
|
+
description: "按本人身份读取、写入、创建、查找和导出飞书电子表格。",
|
|
410
|
+
parameters: SheetSchema,
|
|
411
|
+
async execute(_toolCallId, params) {
|
|
412
|
+
const payload = params as SheetParams;
|
|
413
|
+
try {
|
|
414
|
+
const client = createToolContext(api, "feishu_sheet").toolClient();
|
|
415
|
+
if (payload.action === "create") return handleCreate(client, payload);
|
|
416
|
+
|
|
417
|
+
const tokenInfo = resolveToken(payload);
|
|
418
|
+
if (!tokenInfo?.token) {
|
|
419
|
+
return json({ error: "请传 spreadsheet_token 或 /sheets/TOKEN 形式的 url。" });
|
|
420
|
+
}
|
|
421
|
+
if (payload.action === "info") return handleInfo(client, tokenInfo.token);
|
|
422
|
+
if (payload.action === "read") return handleRead(client, tokenInfo, payload);
|
|
423
|
+
if (payload.action === "write" || payload.action === "append") return handleWrite(client, tokenInfo, payload);
|
|
424
|
+
if (payload.action === "find") return handleFind(client, tokenInfo.token, payload);
|
|
425
|
+
return handleExport(client, tokenInfo.token, payload);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return handleInvokeError(error, api);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
}, { name: "feishu_sheet" });
|
|
431
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
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 CommentSchema = Type.Union([
|
|
6
|
+
Type.Object({ action: Type.Literal("create"), task_guid: Type.String(), content: Type.String(), reply_to_comment_id: Type.Optional(Type.String()) }),
|
|
7
|
+
Type.Object({ action: Type.Literal("list"), resource_id: Type.String(), direction: Type.Optional(StringEnum(["asc", "desc"])), page_size: Type.Optional(Type.Number()), page_token: Type.Optional(Type.String()) }),
|
|
8
|
+
Type.Object({ action: Type.Literal("get"), comment_id: Type.String() }),
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function registerFeishuTaskCommentTool(api: OpenClawPluginApi) {
|
|
12
|
+
const { toolClient } = createToolContext(api, "feishu_task_comment");
|
|
13
|
+
registerTool(api, {
|
|
14
|
+
name: "feishu_task_comment",
|
|
15
|
+
label: "Feishu Task Comment",
|
|
16
|
+
description: "按本人身份创建、获取、列出任务评论。",
|
|
17
|
+
parameters: CommentSchema,
|
|
18
|
+
async execute(_toolCallId, params) {
|
|
19
|
+
const payload = params as {
|
|
20
|
+
action: "create" | "get" | "list";
|
|
21
|
+
task_guid?: string;
|
|
22
|
+
content?: string;
|
|
23
|
+
reply_to_comment_id?: string;
|
|
24
|
+
resource_id?: string;
|
|
25
|
+
direction?: "asc" | "desc";
|
|
26
|
+
page_size?: number;
|
|
27
|
+
page_token?: string;
|
|
28
|
+
comment_id?: string;
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
const client = toolClient();
|
|
32
|
+
if (payload.action === "create") {
|
|
33
|
+
const response = await client.invoke(
|
|
34
|
+
"feishu_task_comment.create",
|
|
35
|
+
(sdk, opts) =>
|
|
36
|
+
sdk.task.v2.comment.create(
|
|
37
|
+
{
|
|
38
|
+
params: { user_id_type: "open_id" as never },
|
|
39
|
+
data: {
|
|
40
|
+
content: payload.content,
|
|
41
|
+
resource_type: "task",
|
|
42
|
+
resource_id: payload.task_guid,
|
|
43
|
+
reply_to_comment_id: payload.reply_to_comment_id,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
opts,
|
|
47
|
+
),
|
|
48
|
+
{ as: "user" },
|
|
49
|
+
);
|
|
50
|
+
assertLarkOk(response);
|
|
51
|
+
return json({ comment: (response.data as { comment?: unknown } | undefined)?.comment });
|
|
52
|
+
}
|
|
53
|
+
if (payload.action === "get") {
|
|
54
|
+
const response = await client.invoke(
|
|
55
|
+
"feishu_task_comment.get",
|
|
56
|
+
(sdk, opts) =>
|
|
57
|
+
sdk.task.v2.comment.get(
|
|
58
|
+
{ path: { comment_id: payload.comment_id! }, params: { user_id_type: "open_id" as never } },
|
|
59
|
+
opts,
|
|
60
|
+
),
|
|
61
|
+
{ as: "user" },
|
|
62
|
+
);
|
|
63
|
+
assertLarkOk(response);
|
|
64
|
+
return json({ comment: (response.data as { comment?: unknown } | undefined)?.comment });
|
|
65
|
+
}
|
|
66
|
+
const response = await client.invoke(
|
|
67
|
+
"feishu_task_comment.list",
|
|
68
|
+
(sdk, opts) =>
|
|
69
|
+
sdk.task.v2.comment.list(
|
|
70
|
+
{
|
|
71
|
+
params: {
|
|
72
|
+
resource_type: "task",
|
|
73
|
+
resource_id: payload.resource_id,
|
|
74
|
+
direction: payload.direction,
|
|
75
|
+
page_size: payload.page_size,
|
|
76
|
+
page_token: payload.page_token,
|
|
77
|
+
user_id_type: "open_id" as never,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
opts,
|
|
81
|
+
),
|
|
82
|
+
{ as: "user" },
|
|
83
|
+
);
|
|
84
|
+
assertLarkOk(response);
|
|
85
|
+
return json({
|
|
86
|
+
comments: (response.data as { items?: unknown[] } | undefined)?.items ?? [],
|
|
87
|
+
has_more: (response.data as { has_more?: boolean } | undefined)?.has_more ?? false,
|
|
88
|
+
page_token: (response.data as { page_token?: string } | undefined)?.page_token,
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
return handleInvokeError(error, api);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
}, { name: "feishu_task_comment" });
|
|
95
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parseTimeToTimestampMs } from "./user-tool-helpers.js";
|
|
2
|
+
|
|
3
|
+
export function normalizeTaskTime(input?: { timestamp?: string; is_all_day?: boolean }) {
|
|
4
|
+
if (!input) return undefined;
|
|
5
|
+
const ts = parseTimeToTimestampMs(input.timestamp);
|
|
6
|
+
if (!ts) {
|
|
7
|
+
throw new Error("任务时间字段必须是带时区的 ISO 8601 / RFC 3339 时间。");
|
|
8
|
+
}
|
|
9
|
+
return { timestamp: ts, is_all_day: input.is_all_day ?? false };
|
|
10
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "./nextclaw-sdk/feishu.js";
|
|
3
|
+
import { normalizeTaskTime } from "./task-shared.js";
|
|
4
|
+
import { assertLarkOk, createToolContext, handleInvokeError, json, registerTool, StringEnum } from "./user-tool-helpers.js";
|
|
5
|
+
|
|
6
|
+
const SubtaskSchema = Type.Union([
|
|
7
|
+
Type.Object({
|
|
8
|
+
action: Type.Literal("create"),
|
|
9
|
+
task_guid: Type.String(),
|
|
10
|
+
summary: Type.String(),
|
|
11
|
+
description: Type.Optional(Type.String()),
|
|
12
|
+
due: Type.Optional(Type.Object({ timestamp: Type.String(), is_all_day: Type.Optional(Type.Boolean()) })),
|
|
13
|
+
start: Type.Optional(Type.Object({ timestamp: Type.String(), is_all_day: Type.Optional(Type.Boolean()) })),
|
|
14
|
+
members: Type.Optional(Type.Array(Type.Object({ id: Type.String(), role: Type.Optional(StringEnum(["assignee", "follower"])) }))),
|
|
15
|
+
}),
|
|
16
|
+
Type.Object({ action: Type.Literal("list"), task_guid: Type.String(), page_size: Type.Optional(Type.Number()), page_token: Type.Optional(Type.String()) }),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export function registerFeishuTaskSubtaskTool(api: OpenClawPluginApi) {
|
|
20
|
+
const { toolClient } = createToolContext(api, "feishu_task_subtask");
|
|
21
|
+
registerTool(api, {
|
|
22
|
+
name: "feishu_task_subtask",
|
|
23
|
+
label: "Feishu Task Subtask",
|
|
24
|
+
description: "按本人身份创建、列出任务的子任务。",
|
|
25
|
+
parameters: SubtaskSchema,
|
|
26
|
+
async execute(_toolCallId, params) {
|
|
27
|
+
const payload = params as {
|
|
28
|
+
action: "create" | "list";
|
|
29
|
+
task_guid: string;
|
|
30
|
+
summary?: string;
|
|
31
|
+
description?: string;
|
|
32
|
+
due?: { timestamp?: string; is_all_day?: boolean };
|
|
33
|
+
start?: { timestamp?: string; is_all_day?: boolean };
|
|
34
|
+
members?: Array<{ id: string; role?: string }>;
|
|
35
|
+
page_size?: number;
|
|
36
|
+
page_token?: string;
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
const client = toolClient();
|
|
40
|
+
if (payload.action === "create") {
|
|
41
|
+
const response = await client.invoke(
|
|
42
|
+
"feishu_task_subtask.create",
|
|
43
|
+
(sdk, opts) =>
|
|
44
|
+
sdk.task.v2.taskSubtask.create(
|
|
45
|
+
{
|
|
46
|
+
path: { task_guid: payload.task_guid },
|
|
47
|
+
params: { user_id_type: "open_id" as never },
|
|
48
|
+
data: {
|
|
49
|
+
summary: payload.summary,
|
|
50
|
+
description: payload.description,
|
|
51
|
+
due: normalizeTaskTime(payload.due),
|
|
52
|
+
start: normalizeTaskTime(payload.start),
|
|
53
|
+
members: payload.members?.map((member) => ({
|
|
54
|
+
id: member.id,
|
|
55
|
+
type: "user",
|
|
56
|
+
role: member.role ?? "assignee",
|
|
57
|
+
})),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
opts,
|
|
61
|
+
),
|
|
62
|
+
{ as: "user" },
|
|
63
|
+
);
|
|
64
|
+
assertLarkOk(response);
|
|
65
|
+
return json({ subtask: (response.data as { subtask?: unknown } | undefined)?.subtask });
|
|
66
|
+
}
|
|
67
|
+
const response = await client.invoke(
|
|
68
|
+
"feishu_task_subtask.list",
|
|
69
|
+
(sdk, opts) =>
|
|
70
|
+
sdk.task.v2.taskSubtask.list(
|
|
71
|
+
{
|
|
72
|
+
path: { task_guid: payload.task_guid },
|
|
73
|
+
params: {
|
|
74
|
+
page_size: payload.page_size,
|
|
75
|
+
page_token: payload.page_token,
|
|
76
|
+
user_id_type: "open_id" as never,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
opts,
|
|
80
|
+
),
|
|
81
|
+
{ as: "user" },
|
|
82
|
+
);
|
|
83
|
+
assertLarkOk(response);
|
|
84
|
+
return json({
|
|
85
|
+
subtasks: (response.data as { items?: unknown[] } | undefined)?.items ?? [],
|
|
86
|
+
has_more: (response.data as { has_more?: boolean } | undefined)?.has_more ?? false,
|
|
87
|
+
page_token: (response.data as { page_token?: string } | undefined)?.page_token,
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return handleInvokeError(error, api);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
}, { name: "feishu_task_subtask" });
|
|
94
|
+
}
|