cc-lark 0.1.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/.github/workflows/ci.yml +47 -0
- package/.github/workflows/release.yml +47 -0
- package/.github/workflows/sync-upstream.yml +127 -0
- package/.prettierrc.json +7 -0
- package/README.md +214 -0
- package/dist/core/api-error.d.ts +193 -0
- package/dist/core/api-error.d.ts.map +1 -0
- package/dist/core/api-error.js +263 -0
- package/dist/core/api-error.js.map +1 -0
- package/dist/core/auth-errors.d.ts +13 -0
- package/dist/core/auth-errors.d.ts.map +1 -0
- package/dist/core/auth-errors.js +14 -0
- package/dist/core/auth-errors.js.map +1 -0
- package/dist/core/config.d.ts +60 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +115 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/device-flow.d.ts +80 -0
- package/dist/core/device-flow.d.ts.map +1 -0
- package/dist/core/device-flow.js +231 -0
- package/dist/core/device-flow.js.map +1 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +16 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/lark-client.d.ts +136 -0
- package/dist/core/lark-client.d.ts.map +1 -0
- package/dist/core/lark-client.js +315 -0
- package/dist/core/lark-client.js.map +1 -0
- package/dist/core/token-store.d.ts +67 -0
- package/dist/core/token-store.d.ts.map +1 -0
- package/dist/core/token-store.js +215 -0
- package/dist/core/token-store.js.map +1 -0
- package/dist/core/types.d.ts +286 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +11 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/uat-client.d.ts +64 -0
- package/dist/core/uat-client.d.ts.map +1 -0
- package/dist/core/uat-client.js +227 -0
- package/dist/core/uat-client.js.map +1 -0
- package/dist/core/version.d.ts +26 -0
- package/dist/core/version.d.ts.map +1 -0
- package/dist/core/version.js +50 -0
- package/dist/core/version.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/bitable/app.d.ts +20 -0
- package/dist/tools/bitable/app.d.ts.map +1 -0
- package/dist/tools/bitable/app.js +301 -0
- package/dist/tools/bitable/app.js.map +1 -0
- package/dist/tools/bitable/field.d.ts +19 -0
- package/dist/tools/bitable/field.d.ts.map +1 -0
- package/dist/tools/bitable/field.js +315 -0
- package/dist/tools/bitable/field.js.map +1 -0
- package/dist/tools/bitable/index.d.ts +21 -0
- package/dist/tools/bitable/index.d.ts.map +1 -0
- package/dist/tools/bitable/index.js +39 -0
- package/dist/tools/bitable/index.js.map +1 -0
- package/dist/tools/bitable/record.d.ts +22 -0
- package/dist/tools/bitable/record.d.ts.map +1 -0
- package/dist/tools/bitable/record.js +434 -0
- package/dist/tools/bitable/record.js.map +1 -0
- package/dist/tools/bitable/table.d.ts +21 -0
- package/dist/tools/bitable/table.d.ts.map +1 -0
- package/dist/tools/bitable/table.js +361 -0
- package/dist/tools/bitable/table.js.map +1 -0
- package/dist/tools/calendar/calendar.d.ts +18 -0
- package/dist/tools/calendar/calendar.d.ts.map +1 -0
- package/dist/tools/calendar/calendar.js +192 -0
- package/dist/tools/calendar/calendar.js.map +1 -0
- package/dist/tools/calendar/event.d.ts +20 -0
- package/dist/tools/calendar/event.d.ts.map +1 -0
- package/dist/tools/calendar/event.js +465 -0
- package/dist/tools/calendar/event.js.map +1 -0
- package/dist/tools/calendar/index.d.ts +19 -0
- package/dist/tools/calendar/index.d.ts.map +1 -0
- package/dist/tools/calendar/index.js +37 -0
- package/dist/tools/calendar/index.js.map +1 -0
- package/dist/tools/chat/chat.d.ts +11 -0
- package/dist/tools/chat/chat.d.ts.map +1 -0
- package/dist/tools/chat/chat.js +106 -0
- package/dist/tools/chat/chat.js.map +1 -0
- package/dist/tools/chat/index.d.ts +11 -0
- package/dist/tools/chat/index.d.ts.map +1 -0
- package/dist/tools/chat/index.js +20 -0
- package/dist/tools/chat/index.js.map +1 -0
- package/dist/tools/chat/members.d.ts +9 -0
- package/dist/tools/chat/members.d.ts.map +1 -0
- package/dist/tools/chat/members.js +80 -0
- package/dist/tools/chat/members.js.map +1 -0
- package/dist/tools/common/get-user.d.ts +11 -0
- package/dist/tools/common/get-user.d.ts.map +1 -0
- package/dist/tools/common/get-user.js +112 -0
- package/dist/tools/common/get-user.js.map +1 -0
- package/dist/tools/common/index.d.ts +11 -0
- package/dist/tools/common/index.d.ts.map +1 -0
- package/dist/tools/common/index.js +20 -0
- package/dist/tools/common/index.js.map +1 -0
- package/dist/tools/common/search-user.d.ts +9 -0
- package/dist/tools/common/search-user.d.ts.map +1 -0
- package/dist/tools/common/search-user.js +88 -0
- package/dist/tools/common/search-user.js.map +1 -0
- package/dist/tools/doc/create.d.ts +17 -0
- package/dist/tools/doc/create.d.ts.map +1 -0
- package/dist/tools/doc/create.js +159 -0
- package/dist/tools/doc/create.js.map +1 -0
- package/dist/tools/doc/fetch.d.ts +17 -0
- package/dist/tools/doc/fetch.d.ts.map +1 -0
- package/dist/tools/doc/fetch.js +123 -0
- package/dist/tools/doc/fetch.js.map +1 -0
- package/dist/tools/doc/index.d.ts +21 -0
- package/dist/tools/doc/index.d.ts.map +1 -0
- package/dist/tools/doc/index.js +33 -0
- package/dist/tools/doc/index.js.map +1 -0
- package/dist/tools/doc/shared.d.ts +69 -0
- package/dist/tools/doc/shared.d.ts.map +1 -0
- package/dist/tools/doc/shared.js +172 -0
- package/dist/tools/doc/shared.js.map +1 -0
- package/dist/tools/doc/update.d.ts +25 -0
- package/dist/tools/doc/update.d.ts.map +1 -0
- package/dist/tools/doc/update.js +208 -0
- package/dist/tools/doc/update.js.map +1 -0
- package/dist/tools/drive/file.d.ts +13 -0
- package/dist/tools/drive/file.d.ts.map +1 -0
- package/dist/tools/drive/file.js +212 -0
- package/dist/tools/drive/file.js.map +1 -0
- package/dist/tools/drive/index.d.ts +12 -0
- package/dist/tools/drive/index.d.ts.map +1 -0
- package/dist/tools/drive/index.js +25 -0
- package/dist/tools/drive/index.js.map +1 -0
- package/dist/tools/im/format-messages.d.ts +99 -0
- package/dist/tools/im/format-messages.d.ts.map +1 -0
- package/dist/tools/im/format-messages.js +277 -0
- package/dist/tools/im/format-messages.js.map +1 -0
- package/dist/tools/im/helpers.d.ts +53 -0
- package/dist/tools/im/helpers.d.ts.map +1 -0
- package/dist/tools/im/helpers.js +85 -0
- package/dist/tools/im/helpers.js.map +1 -0
- package/dist/tools/im/index.d.ts +25 -0
- package/dist/tools/im/index.d.ts.map +1 -0
- package/dist/tools/im/index.js +44 -0
- package/dist/tools/im/index.js.map +1 -0
- package/dist/tools/im/message-read.d.ts +19 -0
- package/dist/tools/im/message-read.d.ts.map +1 -0
- package/dist/tools/im/message-read.js +526 -0
- package/dist/tools/im/message-read.js.map +1 -0
- package/dist/tools/im/message.d.ts +22 -0
- package/dist/tools/im/message.d.ts.map +1 -0
- package/dist/tools/im/message.js +233 -0
- package/dist/tools/im/message.js.map +1 -0
- package/dist/tools/im/resource.d.ts +19 -0
- package/dist/tools/im/resource.d.ts.map +1 -0
- package/dist/tools/im/resource.js +185 -0
- package/dist/tools/im/resource.js.map +1 -0
- package/dist/tools/im/time-utils.d.ts +70 -0
- package/dist/tools/im/time-utils.d.ts.map +1 -0
- package/dist/tools/im/time-utils.js +277 -0
- package/dist/tools/im/time-utils.js.map +1 -0
- package/dist/tools/index.d.ts +85 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +135 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/oauth.d.ts +15 -0
- package/dist/tools/oauth.d.ts.map +1 -0
- package/dist/tools/oauth.js +379 -0
- package/dist/tools/oauth.js.map +1 -0
- package/dist/tools/search/doc-search.d.ts +9 -0
- package/dist/tools/search/doc-search.d.ts.map +1 -0
- package/dist/tools/search/doc-search.js +219 -0
- package/dist/tools/search/doc-search.js.map +1 -0
- package/dist/tools/search/index.d.ts +11 -0
- package/dist/tools/search/index.d.ts.map +1 -0
- package/dist/tools/search/index.js +18 -0
- package/dist/tools/search/index.js.map +1 -0
- package/dist/tools/sheets/index.d.ts +11 -0
- package/dist/tools/sheets/index.d.ts.map +1 -0
- package/dist/tools/sheets/index.js +18 -0
- package/dist/tools/sheets/index.js.map +1 -0
- package/dist/tools/sheets/sheet.d.ts +11 -0
- package/dist/tools/sheets/sheet.d.ts.map +1 -0
- package/dist/tools/sheets/sheet.js +332 -0
- package/dist/tools/sheets/sheet.js.map +1 -0
- package/dist/tools/task/index.d.ts +12 -0
- package/dist/tools/task/index.d.ts.map +1 -0
- package/dist/tools/task/index.js +30 -0
- package/dist/tools/task/index.js.map +1 -0
- package/dist/tools/task/task.d.ts +13 -0
- package/dist/tools/task/task.d.ts.map +1 -0
- package/dist/tools/task/task.js +225 -0
- package/dist/tools/task/task.js.map +1 -0
- package/dist/tools/task/tasklist.d.ts +13 -0
- package/dist/tools/task/tasklist.d.ts.map +1 -0
- package/dist/tools/task/tasklist.js +206 -0
- package/dist/tools/task/tasklist.js.map +1 -0
- package/dist/tools/wiki/index.d.ts +11 -0
- package/dist/tools/wiki/index.d.ts.map +1 -0
- package/dist/tools/wiki/index.js +20 -0
- package/dist/tools/wiki/index.js.map +1 -0
- package/dist/tools/wiki/node.d.ts +11 -0
- package/dist/tools/wiki/node.d.ts.map +1 -0
- package/dist/tools/wiki/node.js +112 -0
- package/dist/tools/wiki/node.js.map +1 -0
- package/dist/tools/wiki/space.d.ts +11 -0
- package/dist/tools/wiki/space.d.ts.map +1 -0
- package/dist/tools/wiki/space.js +125 -0
- package/dist/tools/wiki/space.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +101 -0
- package/dist/utils/logger.js.map +1 -0
- package/eslint.config.js +13 -0
- package/package.json +54 -0
- package/skills/feishu-bitable/SKILL.md +248 -0
- package/skills/feishu-bitable/references/examples.md +813 -0
- package/skills/feishu-bitable/references/field-properties.md +763 -0
- package/skills/feishu-bitable/references/record-values.md +911 -0
- package/skills/feishu-calendar/SKILL.md +244 -0
- package/skills/feishu-channel-rules/SKILL.md +18 -0
- package/skills/feishu-channel-rules/references/markdown-syntax.md +138 -0
- package/skills/feishu-create-doc/SKILL.md +719 -0
- package/skills/feishu-fetch-doc/SKILL.md +93 -0
- package/skills/feishu-im-read/SKILL.md +163 -0
- package/skills/feishu-task/SKILL.md +293 -0
- package/skills/feishu-troubleshoot/SKILL.md +70 -0
- package/skills/feishu-update-doc/SKILL.md +285 -0
- package/src/core/api-error.ts +342 -0
- package/src/core/auth-errors.ts +27 -0
- package/src/core/config.ts +134 -0
- package/src/core/device-flow.ts +314 -0
- package/src/core/index.ts +16 -0
- package/src/core/lark-client.ts +391 -0
- package/src/core/token-store.ts +249 -0
- package/src/core/types.ts +302 -0
- package/src/core/uat-client.ts +298 -0
- package/src/core/version.ts +53 -0
- package/src/index.ts +138 -0
- package/src/tools/bitable/app.ts +390 -0
- package/src/tools/bitable/field.ts +406 -0
- package/src/tools/bitable/index.ts +43 -0
- package/src/tools/bitable/record.ts +559 -0
- package/src/tools/bitable/table.ts +472 -0
- package/src/tools/calendar/calendar.ts +254 -0
- package/src/tools/calendar/event.ts +606 -0
- package/src/tools/calendar/index.ts +41 -0
- package/src/tools/chat/chat.ts +127 -0
- package/src/tools/chat/index.ts +24 -0
- package/src/tools/chat/members.ts +93 -0
- package/src/tools/common/get-user.ts +127 -0
- package/src/tools/common/index.ts +24 -0
- package/src/tools/common/search-user.ts +99 -0
- package/src/tools/doc/create.ts +184 -0
- package/src/tools/doc/fetch.ts +149 -0
- package/src/tools/doc/index.ts +38 -0
- package/src/tools/doc/shared.ts +228 -0
- package/src/tools/doc/update.ts +240 -0
- package/src/tools/drive/file.ts +265 -0
- package/src/tools/drive/index.ts +29 -0
- package/src/tools/im/format-messages.ts +391 -0
- package/src/tools/im/helpers.ts +109 -0
- package/src/tools/im/index.ts +49 -0
- package/src/tools/im/message-read.ts +676 -0
- package/src/tools/im/message.ts +303 -0
- package/src/tools/im/resource.ts +225 -0
- package/src/tools/im/time-utils.ts +347 -0
- package/src/tools/index.ts +205 -0
- package/src/tools/oauth.ts +460 -0
- package/src/tools/search/doc-search.ts +250 -0
- package/src/tools/search/index.ts +22 -0
- package/src/tools/sheets/index.ts +22 -0
- package/src/tools/sheets/sheet.ts +382 -0
- package/src/tools/task/index.ts +34 -0
- package/src/tools/task/task.ts +265 -0
- package/src/tools/task/tasklist.ts +262 -0
- package/src/tools/wiki/index.ts +24 -0
- package/src/tools/wiki/node.ts +131 -0
- package/src/tools/wiki/space.ts +152 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/logger.ts +132 -0
- package/tests/core/config.test.ts +238 -0
- package/tests/core/device-flow.test.ts +490 -0
- package/tests/core/lark-client.test.ts +378 -0
- package/tests/core/token-store.test.ts +438 -0
- package/tests/index.test.ts +360 -0
- package/tests/tools/doc/create.test.ts +224 -0
- package/tests/tools/doc/fetch.test.ts +182 -0
- package/tests/tools/doc/shared.test.ts +183 -0
- package/tests/tools/doc/update.test.ts +330 -0
- package/tests/tools/im/format-messages.test.ts +184 -0
- package/tests/tools/im/time-utils.test.ts +178 -0
- package/tests/utils/logger.test.ts +140 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* feishu_drive_file tool - Manage Feishu Drive files.
|
|
6
|
+
*
|
|
7
|
+
* Actions: list, get_meta, copy, move, delete
|
|
8
|
+
*
|
|
9
|
+
* Adapted from openclaw-lark for MCP Server architecture.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import type { ToolRegistry } from '../index.js';
|
|
14
|
+
import { LarkClient } from '../../core/lark-client.js';
|
|
15
|
+
import { getValidAccessToken, NeedAuthorizationError } from '../../core/uat-client.js';
|
|
16
|
+
import { assertLarkOk } from '../../core/api-error.js';
|
|
17
|
+
import { json, jsonError, type ToolResult } from '../im/helpers.js';
|
|
18
|
+
import { logger } from '../../utils/logger.js';
|
|
19
|
+
|
|
20
|
+
const log = logger('tools:drive:file');
|
|
21
|
+
|
|
22
|
+
// Schemas
|
|
23
|
+
const listActionSchema = {
|
|
24
|
+
action: z.literal('list').describe('List files in a folder'),
|
|
25
|
+
folder_token: z.string().optional().describe('Folder token (optional, defaults to root)'),
|
|
26
|
+
page_size: z.number().min(1).max(200).optional().describe('Page size (default 200)'),
|
|
27
|
+
page_token: z.string().optional().describe('Pagination token'),
|
|
28
|
+
order_by: z.enum(['EditedTime', 'CreatedTime']).optional().describe('Sort order'),
|
|
29
|
+
direction: z.enum(['ASC', 'DESC']).optional().describe('Sort direction'),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const docTypeEnum = z.enum(['doc', 'sheet', 'file', 'bitable', 'docx', 'folder', 'mindnote', 'slides']);
|
|
33
|
+
|
|
34
|
+
const getMetaActionSchema = {
|
|
35
|
+
action: z.literal('get_meta').describe('Get file metadata'),
|
|
36
|
+
request_docs: z.array(z.object({
|
|
37
|
+
doc_token: z.string().describe('Document token'),
|
|
38
|
+
doc_type: docTypeEnum.describe('Document type'),
|
|
39
|
+
})).min(1).max(50).describe('Documents to query'),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const copyActionSchema = {
|
|
43
|
+
action: z.literal('copy').describe('Copy a file'),
|
|
44
|
+
file_token: z.string().describe('File token'),
|
|
45
|
+
name: z.string().describe('New file name'),
|
|
46
|
+
type: docTypeEnum.describe('Document type'),
|
|
47
|
+
folder_token: z.string().optional().describe('Target folder token'),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const moveActionSchema = {
|
|
51
|
+
action: z.literal('move').describe('Move a file'),
|
|
52
|
+
file_token: z.string().describe('File token'),
|
|
53
|
+
type: docTypeEnum.describe('Document type'),
|
|
54
|
+
folder_token: z.string().describe('Target folder token'),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const deleteActionSchema = {
|
|
58
|
+
action: z.literal('delete').describe('Delete a file'),
|
|
59
|
+
file_token: z.string().describe('File token'),
|
|
60
|
+
type: docTypeEnum.describe('Document type'),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
async function getAccessToken(context: { larkClient: LarkClient | null; config: import('../../core/types.js').FeishuConfig }): Promise<string | ToolResult> {
|
|
64
|
+
const { larkClient, config } = context;
|
|
65
|
+
if (!larkClient) return jsonError('LarkClient not initialized.');
|
|
66
|
+
const { appId, appSecret, brand } = config;
|
|
67
|
+
if (!appId || !appSecret) return jsonError('Missing FEISHU_APP_ID or FEISHU_APP_SECRET.');
|
|
68
|
+
|
|
69
|
+
const { listStoredTokens } = await import('../../core/token-store.js');
|
|
70
|
+
const tokens = await listStoredTokens(appId);
|
|
71
|
+
if (tokens.length === 0) return jsonError('No user authorization found. Use feishu_oauth tool first.');
|
|
72
|
+
const userOpenId = tokens[0].userOpenId;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
return await getValidAccessToken({ userOpenId, appId, appSecret, domain: brand ?? 'feishu' });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof NeedAuthorizationError) return jsonError('User authorization expired. Re-authorize with feishu_oauth.');
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function registerDriveFileTool(registry: ToolRegistry): void {
|
|
83
|
+
// List files
|
|
84
|
+
registry.register({
|
|
85
|
+
name: 'feishu_drive_file_list',
|
|
86
|
+
description: 'List files in a Feishu Drive folder.\n\nRequires OAuth authorization.',
|
|
87
|
+
inputSchema: listActionSchema,
|
|
88
|
+
handler: async (args, context) => {
|
|
89
|
+
const p = args as z.infer<ReturnType<typeof z.object<typeof listActionSchema>>>;
|
|
90
|
+
const { larkClient } = context;
|
|
91
|
+
|
|
92
|
+
const tokenResult = await getAccessToken(context);
|
|
93
|
+
if (typeof tokenResult === 'object' && 'content' in tokenResult) return tokenResult;
|
|
94
|
+
const accessToken = tokenResult;
|
|
95
|
+
|
|
96
|
+
log.info(`list: folder_token=${p.folder_token || '(root)'}, page_size=${p.page_size ?? 200}`);
|
|
97
|
+
|
|
98
|
+
const Lark = await import('@larksuiteoapi/node-sdk');
|
|
99
|
+
const opts = Lark.withUserAccessToken(accessToken);
|
|
100
|
+
|
|
101
|
+
const res = await larkClient!.sdk.drive.v1.file.list(
|
|
102
|
+
{
|
|
103
|
+
params: {
|
|
104
|
+
folder_token: p.folder_token || '',
|
|
105
|
+
page_size: p.page_size,
|
|
106
|
+
page_token: p.page_token,
|
|
107
|
+
order_by: p.order_by,
|
|
108
|
+
direction: p.direction,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
opts
|
|
112
|
+
);
|
|
113
|
+
assertLarkOk(res);
|
|
114
|
+
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
const data = res.data as any;
|
|
117
|
+
|
|
118
|
+
return json({
|
|
119
|
+
files: data?.files,
|
|
120
|
+
has_more: data?.has_more,
|
|
121
|
+
page_token: data?.next_page_token,
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Get metadata
|
|
127
|
+
registry.register({
|
|
128
|
+
name: 'feishu_drive_file_get_meta',
|
|
129
|
+
description: 'Get metadata for Feishu documents.\n\nRequires OAuth authorization.',
|
|
130
|
+
inputSchema: getMetaActionSchema,
|
|
131
|
+
handler: async (args, context) => {
|
|
132
|
+
const p = args as z.infer<ReturnType<typeof z.object<typeof getMetaActionSchema>>>;
|
|
133
|
+
const { larkClient } = context;
|
|
134
|
+
|
|
135
|
+
if (!p.request_docs || p.request_docs.length === 0) {
|
|
136
|
+
return jsonError('request_docs must be a non-empty array');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const tokenResult = await getAccessToken(context);
|
|
140
|
+
if (typeof tokenResult === 'object' && 'content' in tokenResult) return tokenResult;
|
|
141
|
+
const accessToken = tokenResult;
|
|
142
|
+
|
|
143
|
+
log.info(`get_meta: querying ${p.request_docs.length} documents`);
|
|
144
|
+
|
|
145
|
+
const Lark = await import('@larksuiteoapi/node-sdk');
|
|
146
|
+
const opts = Lark.withUserAccessToken(accessToken);
|
|
147
|
+
|
|
148
|
+
const res = await larkClient!.sdk.drive.meta.batchQuery(
|
|
149
|
+
{ data: { request_docs: p.request_docs } },
|
|
150
|
+
opts
|
|
151
|
+
);
|
|
152
|
+
assertLarkOk(res);
|
|
153
|
+
|
|
154
|
+
return json({ metas: res.data?.metas ?? [] });
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Copy file
|
|
159
|
+
registry.register({
|
|
160
|
+
name: 'feishu_drive_file_copy',
|
|
161
|
+
description: 'Copy a Feishu Drive file.\n\nRequires OAuth authorization.',
|
|
162
|
+
inputSchema: copyActionSchema,
|
|
163
|
+
handler: async (args, context) => {
|
|
164
|
+
const p = args as z.infer<ReturnType<typeof z.object<typeof copyActionSchema>>>;
|
|
165
|
+
const { larkClient } = context;
|
|
166
|
+
|
|
167
|
+
const tokenResult = await getAccessToken(context);
|
|
168
|
+
if (typeof tokenResult === 'object' && 'content' in tokenResult) return tokenResult;
|
|
169
|
+
const accessToken = tokenResult;
|
|
170
|
+
|
|
171
|
+
log.info(`copy: file_token=${p.file_token}, name=${p.name}, type=${p.type}`);
|
|
172
|
+
|
|
173
|
+
const Lark = await import('@larksuiteoapi/node-sdk');
|
|
174
|
+
const opts = Lark.withUserAccessToken(accessToken);
|
|
175
|
+
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
177
|
+
const data: any = { name: p.name, type: p.type };
|
|
178
|
+
if (p.folder_token) data.folder_token = p.folder_token;
|
|
179
|
+
|
|
180
|
+
const res = await larkClient!.sdk.drive.file.copy(
|
|
181
|
+
{ path: { file_token: p.file_token }, data },
|
|
182
|
+
opts
|
|
183
|
+
);
|
|
184
|
+
assertLarkOk(res);
|
|
185
|
+
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
187
|
+
const fileData = res.data as any;
|
|
188
|
+
|
|
189
|
+
return json({ file: fileData?.file });
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Move file
|
|
194
|
+
registry.register({
|
|
195
|
+
name: 'feishu_drive_file_move',
|
|
196
|
+
description: 'Move a Feishu Drive file.\n\nRequires OAuth authorization.',
|
|
197
|
+
inputSchema: moveActionSchema,
|
|
198
|
+
handler: async (args, context) => {
|
|
199
|
+
const p = args as z.infer<ReturnType<typeof z.object<typeof moveActionSchema>>>;
|
|
200
|
+
const { larkClient } = context;
|
|
201
|
+
|
|
202
|
+
const tokenResult = await getAccessToken(context);
|
|
203
|
+
if (typeof tokenResult === 'object' && 'content' in tokenResult) return tokenResult;
|
|
204
|
+
const accessToken = tokenResult;
|
|
205
|
+
|
|
206
|
+
log.info(`move: file_token=${p.file_token}, type=${p.type}, folder_token=${p.folder_token}`);
|
|
207
|
+
|
|
208
|
+
const Lark = await import('@larksuiteoapi/node-sdk');
|
|
209
|
+
const opts = Lark.withUserAccessToken(accessToken);
|
|
210
|
+
|
|
211
|
+
const res = await larkClient!.sdk.drive.file.move(
|
|
212
|
+
{ path: { file_token: p.file_token }, data: { type: p.type as any, folder_token: p.folder_token } },
|
|
213
|
+
opts
|
|
214
|
+
);
|
|
215
|
+
assertLarkOk(res);
|
|
216
|
+
|
|
217
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
218
|
+
const data = res.data as any;
|
|
219
|
+
|
|
220
|
+
return json({
|
|
221
|
+
success: true,
|
|
222
|
+
task_id: data?.task_id,
|
|
223
|
+
file_token: p.file_token,
|
|
224
|
+
target_folder_token: p.folder_token,
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Delete file
|
|
230
|
+
registry.register({
|
|
231
|
+
name: 'feishu_drive_file_delete',
|
|
232
|
+
description: 'Delete a Feishu Drive file.\n\nRequires OAuth authorization.',
|
|
233
|
+
inputSchema: deleteActionSchema,
|
|
234
|
+
handler: async (args, context) => {
|
|
235
|
+
const p = args as z.infer<ReturnType<typeof z.object<typeof deleteActionSchema>>>;
|
|
236
|
+
const { larkClient } = context;
|
|
237
|
+
|
|
238
|
+
const tokenResult = await getAccessToken(context);
|
|
239
|
+
if (typeof tokenResult === 'object' && 'content' in tokenResult) return tokenResult;
|
|
240
|
+
const accessToken = tokenResult;
|
|
241
|
+
|
|
242
|
+
log.info(`delete: file_token=${p.file_token}, type=${p.type}`);
|
|
243
|
+
|
|
244
|
+
const Lark = await import('@larksuiteoapi/node-sdk');
|
|
245
|
+
const opts = Lark.withUserAccessToken(accessToken);
|
|
246
|
+
|
|
247
|
+
const res = await larkClient!.sdk.drive.file.delete(
|
|
248
|
+
{ path: { file_token: p.file_token }, params: { type: p.type as any } },
|
|
249
|
+
opts
|
|
250
|
+
);
|
|
251
|
+
assertLarkOk(res);
|
|
252
|
+
|
|
253
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
254
|
+
const data = res.data as any;
|
|
255
|
+
|
|
256
|
+
return json({
|
|
257
|
+
success: true,
|
|
258
|
+
task_id: data?.task_id,
|
|
259
|
+
file_token: p.file_token,
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
log.debug('feishu_drive_file tools registered');
|
|
265
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Drive Tools Index
|
|
6
|
+
*
|
|
7
|
+
* Drive/file management tools for Feishu/Lark.
|
|
8
|
+
* Adapted from openclaw-lark for MCP Server architecture.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ToolRegistry } from '../index.js';
|
|
12
|
+
import { registerDriveFileTool } from './file.js';
|
|
13
|
+
import { logger } from '../../utils/logger.js';
|
|
14
|
+
|
|
15
|
+
const log = logger('tools:drive');
|
|
16
|
+
|
|
17
|
+
export function registerDriveTools(registry: ToolRegistry): void {
|
|
18
|
+
registerDriveFileTool(registry);
|
|
19
|
+
|
|
20
|
+
log.info('Drive tools registered', {
|
|
21
|
+
tools: [
|
|
22
|
+
'feishu_drive_file_list',
|
|
23
|
+
'feishu_drive_file_get_meta',
|
|
24
|
+
'feishu_drive_file_copy',
|
|
25
|
+
'feishu_drive_file_move',
|
|
26
|
+
'feishu_drive_file_delete',
|
|
27
|
+
].join(', '),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Message formatting utilities for IM tools.
|
|
6
|
+
*
|
|
7
|
+
* Converts raw Feishu IM API message objects to AI-readable JSON format.
|
|
8
|
+
* Adapted from openclaw-lark for MCP Server architecture.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { millisStringToDateTime } from './time-utils.js';
|
|
12
|
+
import { getCachedUserName, setCachedUserNames } from './helpers.js';
|
|
13
|
+
import { logger } from '../../utils/logger.js';
|
|
14
|
+
|
|
15
|
+
const log = logger('tools:im:format-messages');
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Shape of a message item returned by the Feishu IM API.
|
|
23
|
+
*/
|
|
24
|
+
export interface ApiMessageItem {
|
|
25
|
+
message_id?: string;
|
|
26
|
+
msg_type?: string;
|
|
27
|
+
create_time?: string;
|
|
28
|
+
upper_message_id?: string;
|
|
29
|
+
body?: { content?: string };
|
|
30
|
+
sender?: {
|
|
31
|
+
id?: string;
|
|
32
|
+
sender_type?: string;
|
|
33
|
+
};
|
|
34
|
+
mentions?: Array<{
|
|
35
|
+
key: string;
|
|
36
|
+
id: unknown;
|
|
37
|
+
name?: string;
|
|
38
|
+
}>;
|
|
39
|
+
parent_id?: string;
|
|
40
|
+
thread_id?: string;
|
|
41
|
+
deleted?: boolean;
|
|
42
|
+
updated?: boolean;
|
|
43
|
+
chat_id?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Formatted message structure for AI consumption.
|
|
48
|
+
*/
|
|
49
|
+
export interface FormattedMessage {
|
|
50
|
+
message_id: string;
|
|
51
|
+
msg_type: string;
|
|
52
|
+
content: string;
|
|
53
|
+
sender: { id: string; sender_type: string; name?: string };
|
|
54
|
+
create_time: string;
|
|
55
|
+
/** Reply message ID (parent_id). Omitted when thread_id exists since thread context is inferable */
|
|
56
|
+
reply_to?: string;
|
|
57
|
+
thread_id?: string;
|
|
58
|
+
mentions?: Array<{ key: string; id: string; name: string }>;
|
|
59
|
+
deleted: boolean;
|
|
60
|
+
updated: boolean;
|
|
61
|
+
chat_id?: string;
|
|
62
|
+
chat_type?: 'p2p' | 'group';
|
|
63
|
+
chat_name?: string;
|
|
64
|
+
chat_partner?: { open_id: string; name?: string };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Context for message content conversion.
|
|
69
|
+
*/
|
|
70
|
+
export interface ConvertContext {
|
|
71
|
+
messageId: string;
|
|
72
|
+
accountId?: string;
|
|
73
|
+
resolveUserName?: (openId: string) => string | undefined;
|
|
74
|
+
batchResolveNames?: (openIds: string[]) => Promise<void>;
|
|
75
|
+
fetchSubMessages?: (messageId: string) => Promise<ApiMessageItem[]>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Mention extraction
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/** Extract open_id from mention's id field (handles both object and string formats) */
|
|
83
|
+
export function extractMentionOpenId(id: unknown): string {
|
|
84
|
+
if (typeof id === 'string') return id;
|
|
85
|
+
if (id != null && typeof id === 'object' && 'open_id' in id) {
|
|
86
|
+
const openId = (id as Record<string, unknown>).open_id;
|
|
87
|
+
return typeof openId === 'string' ? openId : '';
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Message content conversion
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Convert raw message content to AI-readable text.
|
|
98
|
+
* This is a simplified version - full converter handles all message types.
|
|
99
|
+
*/
|
|
100
|
+
export function convertMessageContent(
|
|
101
|
+
raw: string,
|
|
102
|
+
msgType: string,
|
|
103
|
+
_ctx: ConvertContext
|
|
104
|
+
): string {
|
|
105
|
+
if (!raw) return '';
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
|
|
110
|
+
switch (msgType) {
|
|
111
|
+
case 'text':
|
|
112
|
+
return parsed.text || raw;
|
|
113
|
+
|
|
114
|
+
case 'post':
|
|
115
|
+
case 'rich_text': {
|
|
116
|
+
// Rich text - extract text from content blocks
|
|
117
|
+
return extractPostContent(parsed);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case 'image':
|
|
121
|
+
return `[Image: ${parsed.image_key || 'unknown'}]`;
|
|
122
|
+
|
|
123
|
+
case 'file':
|
|
124
|
+
return `[File: ${parsed.file_name || parsed.file_key || 'unknown'}]`;
|
|
125
|
+
|
|
126
|
+
case 'audio':
|
|
127
|
+
return `[Audio: ${parsed.file_name || parsed.audio_key || 'unknown'}]`;
|
|
128
|
+
|
|
129
|
+
case 'media':
|
|
130
|
+
return `[Media: ${parsed.file_name || parsed.media_key || 'unknown'}]`;
|
|
131
|
+
|
|
132
|
+
case 'sticker':
|
|
133
|
+
return `[Sticker: ${parsed.file_key || 'unknown'}]`;
|
|
134
|
+
|
|
135
|
+
case 'interactive':
|
|
136
|
+
return '[Interactive Card]';
|
|
137
|
+
|
|
138
|
+
case 'share_chat':
|
|
139
|
+
return `[Share Chat: ${parsed.chat_id || 'unknown'}]`;
|
|
140
|
+
|
|
141
|
+
case 'share_user':
|
|
142
|
+
return `[Share User: ${parsed.user_id || 'unknown'}]`;
|
|
143
|
+
|
|
144
|
+
case 'merge_forward':
|
|
145
|
+
return '[Merged Forwarded Messages]';
|
|
146
|
+
|
|
147
|
+
default:
|
|
148
|
+
return raw;
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// Not valid JSON, return as-is
|
|
152
|
+
return raw;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Extract text content from a post/rich_text message.
|
|
158
|
+
*/
|
|
159
|
+
function extractPostContent(parsed: Record<string, unknown>): string {
|
|
160
|
+
const sections: string[] = [];
|
|
161
|
+
|
|
162
|
+
// Handle multi-language content (zh_cn, en_us, etc.)
|
|
163
|
+
for (const lang of ['zh_cn', 'en_us']) {
|
|
164
|
+
const content = parsed[lang] as Record<string, unknown> | undefined;
|
|
165
|
+
if (!content) continue;
|
|
166
|
+
|
|
167
|
+
// Title
|
|
168
|
+
if (content.title) {
|
|
169
|
+
sections.push(String(content.title));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Content lines
|
|
173
|
+
if (Array.isArray(content.content)) {
|
|
174
|
+
for (const line of content.content) {
|
|
175
|
+
if (Array.isArray(line)) {
|
|
176
|
+
const lineText = extractLineText(line);
|
|
177
|
+
if (lineText) sections.push(lineText);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return sections.join('\n') || JSON.stringify(parsed);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extract text from a rich text line.
|
|
188
|
+
*/
|
|
189
|
+
function extractLineText(line: unknown[]): string {
|
|
190
|
+
const parts: string[] = [];
|
|
191
|
+
|
|
192
|
+
for (const element of line) {
|
|
193
|
+
if (!element || typeof element !== 'object') continue;
|
|
194
|
+
const el = element as Record<string, unknown>;
|
|
195
|
+
|
|
196
|
+
if (el.text_run) {
|
|
197
|
+
const tr = el.text_run as Record<string, unknown>;
|
|
198
|
+
if (tr.content) parts.push(String(tr.content));
|
|
199
|
+
} else if (el.mention_run) {
|
|
200
|
+
const mr = el.mention_run as Record<string, unknown>;
|
|
201
|
+
if (mr.text) parts.push(String(mr.text));
|
|
202
|
+
} else if (el.equation_run) {
|
|
203
|
+
const er = el.equation_run as Record<string, unknown>;
|
|
204
|
+
if (er.content) parts.push(`$${er.content}$`);
|
|
205
|
+
} else if (el.text) {
|
|
206
|
+
parts.push(String(el.text));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return parts.join('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Message formatting
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Build ConvertContext from a raw API message item.
|
|
219
|
+
*/
|
|
220
|
+
export function buildConvertContextFromItem(
|
|
221
|
+
item: ApiMessageItem,
|
|
222
|
+
fallbackMessageId: string,
|
|
223
|
+
accountId?: string
|
|
224
|
+
): ConvertContext {
|
|
225
|
+
const mentions = new Map<string, { key: string; openId: string; name: string }>();
|
|
226
|
+
const mentionsByOpenId = new Map<string, { key: string; openId: string; name: string }>();
|
|
227
|
+
|
|
228
|
+
for (const m of item.mentions ?? []) {
|
|
229
|
+
const openId = extractMentionOpenId(m.id);
|
|
230
|
+
if (!openId) continue;
|
|
231
|
+
|
|
232
|
+
const info = {
|
|
233
|
+
key: m.key,
|
|
234
|
+
openId,
|
|
235
|
+
name: m.name ?? '',
|
|
236
|
+
};
|
|
237
|
+
mentions.set(m.key, info);
|
|
238
|
+
mentionsByOpenId.set(openId, info);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
messageId: item.message_id ?? fallbackMessageId,
|
|
243
|
+
accountId,
|
|
244
|
+
resolveUserName: accountId ? (openId) => getCachedUserName(openId) : undefined,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Format a single message item.
|
|
250
|
+
*/
|
|
251
|
+
export async function formatMessageItem(
|
|
252
|
+
item: ApiMessageItem,
|
|
253
|
+
accountId: string,
|
|
254
|
+
nameResolver: (openId: string) => string | undefined,
|
|
255
|
+
ctxOverrides?: Partial<ConvertContext>
|
|
256
|
+
): Promise<FormattedMessage> {
|
|
257
|
+
const messageId = item.message_id ?? '';
|
|
258
|
+
const msgType = item.msg_type ?? 'unknown';
|
|
259
|
+
|
|
260
|
+
// Convert message content
|
|
261
|
+
let content = '';
|
|
262
|
+
try {
|
|
263
|
+
const rawContent = item.body?.content ?? '';
|
|
264
|
+
if (rawContent) {
|
|
265
|
+
const ctx = {
|
|
266
|
+
...buildConvertContextFromItem(item, messageId, accountId),
|
|
267
|
+
...ctxOverrides,
|
|
268
|
+
};
|
|
269
|
+
content = convertMessageContent(rawContent, msgType, ctx);
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
log.warn('Content conversion failed, using raw content', {
|
|
273
|
+
messageId,
|
|
274
|
+
msgType,
|
|
275
|
+
error: err instanceof Error ? err.message : String(err),
|
|
276
|
+
});
|
|
277
|
+
content = item.body?.content ?? '';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Build sender info
|
|
281
|
+
const senderId = item.sender?.id ?? '';
|
|
282
|
+
const senderType = item.sender?.sender_type ?? 'unknown';
|
|
283
|
+
let senderName: string | undefined;
|
|
284
|
+
if (senderId && senderType === 'user') {
|
|
285
|
+
senderName = nameResolver(senderId);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const sender: FormattedMessage['sender'] = {
|
|
289
|
+
id: senderId,
|
|
290
|
+
sender_type: senderType,
|
|
291
|
+
};
|
|
292
|
+
if (senderName) {
|
|
293
|
+
sender.name = senderName;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Build mentions
|
|
297
|
+
let mentions: FormattedMessage['mentions'];
|
|
298
|
+
if (item.mentions && item.mentions.length > 0) {
|
|
299
|
+
mentions = item.mentions.map((m) => ({
|
|
300
|
+
key: m.key ?? '',
|
|
301
|
+
id: extractMentionOpenId(m.id),
|
|
302
|
+
name: m.name ?? '',
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Convert create_time (milliseconds string to ISO 8601 +08:00)
|
|
307
|
+
const createTime = item.create_time ? millisStringToDateTime(item.create_time) : '';
|
|
308
|
+
|
|
309
|
+
const formatted: FormattedMessage = {
|
|
310
|
+
message_id: messageId,
|
|
311
|
+
msg_type: msgType,
|
|
312
|
+
content,
|
|
313
|
+
sender,
|
|
314
|
+
create_time: createTime,
|
|
315
|
+
deleted: item.deleted ?? false,
|
|
316
|
+
updated: item.updated ?? false,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Optional fields
|
|
320
|
+
// reply_to (parent_id) and thread_id display logic:
|
|
321
|
+
// - If thread_id exists, only show thread_id (omit reply_to, thread context is inferable)
|
|
322
|
+
// - If no thread_id but has parent_id, show as reply_to
|
|
323
|
+
if (item.thread_id) {
|
|
324
|
+
formatted.thread_id = item.thread_id;
|
|
325
|
+
} else if (item.parent_id) {
|
|
326
|
+
formatted.reply_to = item.parent_id;
|
|
327
|
+
}
|
|
328
|
+
if (mentions) {
|
|
329
|
+
formatted.mentions = mentions;
|
|
330
|
+
}
|
|
331
|
+
if (item.chat_id) {
|
|
332
|
+
formatted.chat_id = item.chat_id;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return formatted;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Batch format message list.
|
|
340
|
+
*
|
|
341
|
+
* First batch resolves all sender names (writes to cache),
|
|
342
|
+
* then formats each message individually.
|
|
343
|
+
*/
|
|
344
|
+
export async function formatMessageList(
|
|
345
|
+
items: ApiMessageItem[],
|
|
346
|
+
accountId: string,
|
|
347
|
+
nameResolver: (openId: string) => string | undefined,
|
|
348
|
+
batchResolver: (openIds: string[]) => Promise<void>
|
|
349
|
+
): Promise<FormattedMessage[]> {
|
|
350
|
+
// 1. Cache mention names (free info from API)
|
|
351
|
+
const mentionNames = new Map<string, string>();
|
|
352
|
+
for (const item of items) {
|
|
353
|
+
for (const m of item.mentions ?? []) {
|
|
354
|
+
const openId = extractMentionOpenId(m.id);
|
|
355
|
+
if (openId && m.name) {
|
|
356
|
+
mentionNames.set(openId, m.name);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (mentionNames.size > 0) {
|
|
361
|
+
setCachedUserNames(mentionNames);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// 2. Collect all user sender open_ids
|
|
365
|
+
const senderIds = [
|
|
366
|
+
...new Set(
|
|
367
|
+
items
|
|
368
|
+
.map((item) => (item.sender?.sender_type === 'user' ? item.sender.id : undefined))
|
|
369
|
+
.filter((id): id is string => !!id)
|
|
370
|
+
),
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
// 3. Batch resolve missing names
|
|
374
|
+
if (senderIds.length > 0) {
|
|
375
|
+
const missing = senderIds.filter((id) => nameResolver(id) === undefined);
|
|
376
|
+
if (missing.length > 0) {
|
|
377
|
+
await batchResolver(missing);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 4. Format each message
|
|
382
|
+
const ctxOverrides: Partial<ConvertContext> = {
|
|
383
|
+
accountId,
|
|
384
|
+
resolveUserName: nameResolver,
|
|
385
|
+
batchResolveNames: batchResolver,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
return Promise.all(
|
|
389
|
+
items.map((item) => formatMessageItem(item, accountId, nameResolver, ctxOverrides))
|
|
390
|
+
);
|
|
391
|
+
}
|