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,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Core type definitions for the cc-lark MCP Server.
|
|
6
|
+
*
|
|
7
|
+
* Contains Feishu/Lark API types, configuration interfaces, and MCP-specific types.
|
|
8
|
+
* Adapted from openclaw-lark for MCP Server architecture.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Domain & connection enums
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The Lark platform brand.
|
|
17
|
+
* - `"feishu"` targets the China-mainland Feishu service.
|
|
18
|
+
* - `"lark"` targets the international Lark service.
|
|
19
|
+
* - Any other string is treated as a custom base URL.
|
|
20
|
+
*/
|
|
21
|
+
export type LarkBrand = 'feishu' | 'lark' | (string & {});
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Feishu identifiers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/** The four ID types recognised by the Feishu API. */
|
|
28
|
+
export type FeishuIdType = 'open_id' | 'user_id' | 'union_id' | 'chat_id';
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// MCP-specific types
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** Standard MCP tool result structure. */
|
|
35
|
+
export interface ToolResult {
|
|
36
|
+
content: Array<{
|
|
37
|
+
type: 'text' | 'image' | 'resource';
|
|
38
|
+
text?: string;
|
|
39
|
+
data?: string;
|
|
40
|
+
mimeType?: string;
|
|
41
|
+
resource?: {
|
|
42
|
+
uri: string;
|
|
43
|
+
name: string;
|
|
44
|
+
mimeType?: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
blob?: string;
|
|
47
|
+
};
|
|
48
|
+
}>;
|
|
49
|
+
isError?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Tool definition for MCP server. */
|
|
53
|
+
export interface ToolDefinition {
|
|
54
|
+
name: string;
|
|
55
|
+
description: string;
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: 'object';
|
|
58
|
+
properties?: Record<string, unknown>;
|
|
59
|
+
required?: string[];
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Configuration types
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/** Feishu configuration loaded from environment variables. */
|
|
68
|
+
export interface FeishuConfig {
|
|
69
|
+
/** Feishu App ID */
|
|
70
|
+
appId: string;
|
|
71
|
+
/** Feishu App Secret */
|
|
72
|
+
appSecret: string;
|
|
73
|
+
/** User Access Token (optional, for user-authorized operations) */
|
|
74
|
+
userAccessToken?: string;
|
|
75
|
+
/** The Lark platform brand (feishu, lark, or custom URL) */
|
|
76
|
+
brand?: LarkBrand;
|
|
77
|
+
/** Encrypt key for webhook event decryption */
|
|
78
|
+
encryptKey?: string;
|
|
79
|
+
/** Verification token for webhook validation */
|
|
80
|
+
verificationToken?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Validation result for configuration. */
|
|
84
|
+
export interface ConfigValidationResult {
|
|
85
|
+
valid: boolean;
|
|
86
|
+
errors: string[];
|
|
87
|
+
config?: FeishuConfig;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Credentials
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/** The minimum credential set needed to interact with the Lark API. */
|
|
95
|
+
export interface LarkCredentials {
|
|
96
|
+
appId: string;
|
|
97
|
+
appSecret: string;
|
|
98
|
+
encryptKey?: string;
|
|
99
|
+
verificationToken?: string;
|
|
100
|
+
brand: LarkBrand;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Lark API response types
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/** Base response from Lark API. */
|
|
108
|
+
export interface LarkApiResponse<T = unknown> {
|
|
109
|
+
code: number;
|
|
110
|
+
msg: string;
|
|
111
|
+
data?: T;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Token response from Lark API. */
|
|
115
|
+
export interface LarkTokenResponse {
|
|
116
|
+
tenant_access_token: string;
|
|
117
|
+
expire: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** User info from Lark API. */
|
|
121
|
+
export interface LarkUserInfo {
|
|
122
|
+
open_id: string;
|
|
123
|
+
user_id?: string;
|
|
124
|
+
union_id?: string;
|
|
125
|
+
name?: string;
|
|
126
|
+
en_name?: string;
|
|
127
|
+
nickname?: string;
|
|
128
|
+
email?: string;
|
|
129
|
+
mobile?: string;
|
|
130
|
+
gender?: number;
|
|
131
|
+
avatar_url?: string;
|
|
132
|
+
status?: {
|
|
133
|
+
is_frozen?: boolean;
|
|
134
|
+
is_resigned?: boolean;
|
|
135
|
+
is_activated?: boolean;
|
|
136
|
+
};
|
|
137
|
+
department_ids?: string[];
|
|
138
|
+
leader_user_id?: string;
|
|
139
|
+
city?: string;
|
|
140
|
+
country?: string;
|
|
141
|
+
work_station?: string;
|
|
142
|
+
join_time?: number;
|
|
143
|
+
employee_no?: string;
|
|
144
|
+
employee_type?: number;
|
|
145
|
+
positions?: string[];
|
|
146
|
+
orders?: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Message content types. */
|
|
150
|
+
export type LarkMessageContent =
|
|
151
|
+
| { text: string }
|
|
152
|
+
| { image_key: string }
|
|
153
|
+
| { file_key: string }
|
|
154
|
+
| { audio_key: string }
|
|
155
|
+
| { media_key: string }
|
|
156
|
+
| { sticker_key: string }
|
|
157
|
+
| { rich_text: LarkRichTextContent };
|
|
158
|
+
|
|
159
|
+
/** Rich text content structure. */
|
|
160
|
+
export interface LarkRichTextContent {
|
|
161
|
+
zh_cn?: LarkRichTextSection[];
|
|
162
|
+
en_us?: LarkRichTextSection[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Rich text section. */
|
|
166
|
+
export interface LarkRichTextSection {
|
|
167
|
+
title?: string;
|
|
168
|
+
lines?: LarkRichTextLine[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Rich text line. */
|
|
172
|
+
export interface LarkRichTextLine {
|
|
173
|
+
text_run?: {
|
|
174
|
+
text: string;
|
|
175
|
+
bold?: boolean;
|
|
176
|
+
italic?: boolean;
|
|
177
|
+
underline?: boolean;
|
|
178
|
+
strikethrough?: boolean;
|
|
179
|
+
inline_code?: boolean;
|
|
180
|
+
color?: string;
|
|
181
|
+
link?: string;
|
|
182
|
+
};
|
|
183
|
+
mention_run?: {
|
|
184
|
+
text: string;
|
|
185
|
+
user_id: string;
|
|
186
|
+
};
|
|
187
|
+
equation_run?: {
|
|
188
|
+
text: string;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Document types
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
/** Docx document metadata. */
|
|
197
|
+
export interface LarkDocxDocument {
|
|
198
|
+
document_id: string;
|
|
199
|
+
revision_id: number;
|
|
200
|
+
title: string;
|
|
201
|
+
create_time: number;
|
|
202
|
+
update_time: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Docx block content. */
|
|
206
|
+
export interface LarkDocxBlock {
|
|
207
|
+
block_type: number;
|
|
208
|
+
block_id: string;
|
|
209
|
+
parent_id?: string;
|
|
210
|
+
children?: string[];
|
|
211
|
+
text?: {
|
|
212
|
+
initial_style: Record<string, unknown>;
|
|
213
|
+
elements: Array<{
|
|
214
|
+
text_run?: { content: string };
|
|
215
|
+
mention_run?: { user_id: string; text: string };
|
|
216
|
+
equation_run?: { content: string };
|
|
217
|
+
}>;
|
|
218
|
+
};
|
|
219
|
+
heading1?: { elements: unknown[] };
|
|
220
|
+
heading2?: { elements: unknown[] };
|
|
221
|
+
heading3?: { elements: unknown[] };
|
|
222
|
+
heading4?: { elements: unknown[] };
|
|
223
|
+
heading5?: { elements: unknown[] };
|
|
224
|
+
heading6?: { elements: unknown[] };
|
|
225
|
+
heading7?: { elements: unknown[] };
|
|
226
|
+
heading8?: { elements: unknown[] };
|
|
227
|
+
heading9?: { elements: unknown[] };
|
|
228
|
+
bullet?: { elements: unknown[] };
|
|
229
|
+
ordered?: { elements: unknown[] };
|
|
230
|
+
code?: { elements: unknown[] };
|
|
231
|
+
quote?: { elements: unknown[] };
|
|
232
|
+
table?: {
|
|
233
|
+
rows: number;
|
|
234
|
+
columns: number;
|
|
235
|
+
property: {
|
|
236
|
+
row_size: number[];
|
|
237
|
+
column_size: number[];
|
|
238
|
+
};
|
|
239
|
+
cells: string[];
|
|
240
|
+
};
|
|
241
|
+
table_cell?: {
|
|
242
|
+
property: Record<string, unknown>;
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// Sheet types
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
/** Spreadsheet metadata. */
|
|
251
|
+
export interface LarkSpreadsheet {
|
|
252
|
+
spreadsheet_token: string;
|
|
253
|
+
name: string;
|
|
254
|
+
revision: number;
|
|
255
|
+
owner_id: string;
|
|
256
|
+
create_time: number;
|
|
257
|
+
update_time: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Sheet (worksheet) metadata. */
|
|
261
|
+
export interface LarkSheet {
|
|
262
|
+
sheet_id: string;
|
|
263
|
+
title: string;
|
|
264
|
+
index: number;
|
|
265
|
+
row_count: number;
|
|
266
|
+
column_count: number;
|
|
267
|
+
frozen_row_count?: number;
|
|
268
|
+
frozen_column_count?: number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Sheet cell value. */
|
|
272
|
+
export interface LarkCellValue {
|
|
273
|
+
value: string;
|
|
274
|
+
type: number;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// Drive types
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
/** Drive file metadata. */
|
|
282
|
+
export interface LarkDriveFile {
|
|
283
|
+
token: string;
|
|
284
|
+
name: string;
|
|
285
|
+
type: string;
|
|
286
|
+
parent_token: string;
|
|
287
|
+
create_time: number;
|
|
288
|
+
update_time: number;
|
|
289
|
+
size: number;
|
|
290
|
+
creator_id?: string;
|
|
291
|
+
modifier_id?: string;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Drive folder metadata. */
|
|
295
|
+
export interface LarkDriveFolder {
|
|
296
|
+
token: string;
|
|
297
|
+
name: string;
|
|
298
|
+
parent_token: string;
|
|
299
|
+
create_time: number;
|
|
300
|
+
update_time: number;
|
|
301
|
+
creator_id?: string;
|
|
302
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* UAT (User Access Token) API call wrapper for cc-lark MCP Server.
|
|
6
|
+
*
|
|
7
|
+
* Provides a safe, auto-refreshing interface for making Lark API calls on
|
|
8
|
+
* behalf of a user. Tokens are read from file-based storage, refreshed
|
|
9
|
+
* transparently, and never exposed to the AI layer.
|
|
10
|
+
*
|
|
11
|
+
* Adapted from openclaw-lark for MCP Server architecture.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { LarkBrand } from './types.js';
|
|
15
|
+
import {
|
|
16
|
+
getStoredToken,
|
|
17
|
+
setStoredToken,
|
|
18
|
+
removeStoredToken,
|
|
19
|
+
tokenStatus,
|
|
20
|
+
maskToken,
|
|
21
|
+
type StoredUAToken,
|
|
22
|
+
} from './token-store.js';
|
|
23
|
+
import { resolveOAuthEndpoints } from './device-flow.js';
|
|
24
|
+
import { logger } from '../utils/logger.js';
|
|
25
|
+
import { getUserAgent } from './version.js';
|
|
26
|
+
import { REFRESH_TOKEN_IRRECOVERABLE, TOKEN_RETRY_CODES, NeedAuthorizationError } from './api-error.js';
|
|
27
|
+
|
|
28
|
+
const log = logger('uat-client');
|
|
29
|
+
|
|
30
|
+
// Re-export for backward compatibility
|
|
31
|
+
export { NeedAuthorizationError };
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Types
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
export interface UATCallOptions {
|
|
38
|
+
userOpenId: string;
|
|
39
|
+
appId: string;
|
|
40
|
+
appSecret: string;
|
|
41
|
+
domain: LarkBrand;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface UATStatus {
|
|
45
|
+
authorized: boolean;
|
|
46
|
+
userOpenId: string;
|
|
47
|
+
scope?: string;
|
|
48
|
+
expiresAt?: number;
|
|
49
|
+
refreshExpiresAt?: number;
|
|
50
|
+
grantedAt?: number;
|
|
51
|
+
tokenStatus?: 'valid' | 'needs_refresh' | 'expired';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// HTTP helper
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Header-aware fetch for Lark API calls.
|
|
60
|
+
*/
|
|
61
|
+
function larkFetch(url: string | URL | Request, init?: RequestInit): Promise<Response> {
|
|
62
|
+
const headers = {
|
|
63
|
+
...init?.headers,
|
|
64
|
+
'User-Agent': getUserAgent(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return fetch(url, { ...init, headers });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Per-user refresh lock
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Guards against concurrent refresh operations for the same user.
|
|
76
|
+
*
|
|
77
|
+
* refresh_token is single-use: if two requests trigger a refresh
|
|
78
|
+
* simultaneously, the second one would use an already-consumed token and
|
|
79
|
+
* fail. The lock ensures only one refresh runs at a time per user.
|
|
80
|
+
*/
|
|
81
|
+
const refreshLocks = new Map<string, Promise<StoredUAToken | null>>();
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Refresh implementation
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
async function doRefreshToken(opts: UATCallOptions, stored: StoredUAToken): Promise<StoredUAToken | null> {
|
|
88
|
+
// refresh_token already expired → can't refresh, need re-auth.
|
|
89
|
+
if (Date.now() >= stored.refreshExpiresAt) {
|
|
90
|
+
log.info(`refresh_token expired for ${opts.userOpenId}, clearing`);
|
|
91
|
+
await removeStoredToken(opts.appId, opts.userOpenId);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const endpoints = resolveOAuthEndpoints(opts.domain);
|
|
96
|
+
|
|
97
|
+
const resp = await larkFetch(endpoints.token, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
100
|
+
body: new URLSearchParams({
|
|
101
|
+
grant_type: 'refresh_token',
|
|
102
|
+
refresh_token: stored.refreshToken,
|
|
103
|
+
client_id: opts.appId,
|
|
104
|
+
client_secret: opts.appSecret,
|
|
105
|
+
}).toString(),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const data = (await resp.json()) as Record<string, unknown>;
|
|
109
|
+
|
|
110
|
+
// Lark v2 token endpoint returns `code: 0` on success.
|
|
111
|
+
// Some responses use `error` field instead (standard OAuth).
|
|
112
|
+
const code = data.code as number | undefined;
|
|
113
|
+
const error = data.error as string | undefined;
|
|
114
|
+
|
|
115
|
+
if ((code !== undefined && code !== 0) || error) {
|
|
116
|
+
const errCode = code ?? error;
|
|
117
|
+
const errMsg = (data.error_description as string) ?? (data.msg as string) ?? 'unknown';
|
|
118
|
+
|
|
119
|
+
// Known irrecoverable codes: invalid/expired/missing refresh_token
|
|
120
|
+
if (REFRESH_TOKEN_IRRECOVERABLE.has(code as number)) {
|
|
121
|
+
log.warn(`refresh failed (code=${errCode}), clearing token for ${opts.userOpenId}`);
|
|
122
|
+
await removeStoredToken(opts.appId, opts.userOpenId);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw new Error(`Token refresh failed (code=${errCode}): ${errMsg}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!data.access_token) {
|
|
130
|
+
throw new Error('Token refresh returned no access_token');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const now = Date.now();
|
|
134
|
+
const updated: StoredUAToken = {
|
|
135
|
+
userOpenId: stored.userOpenId,
|
|
136
|
+
appId: opts.appId,
|
|
137
|
+
accessToken: data.access_token as string,
|
|
138
|
+
// refresh_token is rotated – always use the new one.
|
|
139
|
+
refreshToken: (data.refresh_token as string) ?? stored.refreshToken,
|
|
140
|
+
expiresAt: now + ((data.expires_in as number) ?? 7200) * 1000,
|
|
141
|
+
refreshExpiresAt: data.refresh_token_expires_in
|
|
142
|
+
? now + (data.refresh_token_expires_in as number) * 1000
|
|
143
|
+
: stored.refreshExpiresAt,
|
|
144
|
+
scope: (data.scope as string) ?? stored.scope,
|
|
145
|
+
grantedAt: stored.grantedAt,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await setStoredToken(updated);
|
|
149
|
+
log.info(`refreshed UAT for ${opts.userOpenId} (at:${maskToken(updated.accessToken)})`);
|
|
150
|
+
return updated;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Refresh with per-user locking.
|
|
155
|
+
*/
|
|
156
|
+
async function refreshWithLock(opts: UATCallOptions, stored: StoredUAToken): Promise<StoredUAToken | null> {
|
|
157
|
+
const key = `${opts.appId}:${opts.userOpenId}`;
|
|
158
|
+
|
|
159
|
+
// Another refresh is already in-flight – wait for it and re-read.
|
|
160
|
+
const existing = refreshLocks.get(key);
|
|
161
|
+
if (existing) {
|
|
162
|
+
await existing;
|
|
163
|
+
return getStoredToken(opts.appId, opts.userOpenId);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const promise = doRefreshToken(opts, stored);
|
|
167
|
+
refreshLocks.set(key, promise);
|
|
168
|
+
try {
|
|
169
|
+
return await promise;
|
|
170
|
+
} finally {
|
|
171
|
+
refreshLocks.delete(key);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Public API
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Obtain a valid access_token for the given user.
|
|
181
|
+
*
|
|
182
|
+
* - Reads from storage.
|
|
183
|
+
* - Refreshes proactively if the token is about to expire.
|
|
184
|
+
* - Throws when no token exists or refresh fails irrecoverably.
|
|
185
|
+
*
|
|
186
|
+
* The returned token must never be exposed to the AI layer.
|
|
187
|
+
*/
|
|
188
|
+
export async function getValidAccessToken(opts: UATCallOptions): Promise<string> {
|
|
189
|
+
const stored = await getStoredToken(opts.appId, opts.userOpenId);
|
|
190
|
+
if (!stored) {
|
|
191
|
+
throw new NeedAuthorizationError(opts.userOpenId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const status = tokenStatus(stored);
|
|
195
|
+
|
|
196
|
+
if (status === 'valid') {
|
|
197
|
+
return stored.accessToken;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (status === 'needs_refresh') {
|
|
201
|
+
const refreshed = await refreshWithLock(opts, stored);
|
|
202
|
+
if (!refreshed) {
|
|
203
|
+
throw new NeedAuthorizationError(opts.userOpenId);
|
|
204
|
+
}
|
|
205
|
+
return refreshed.accessToken;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// expired
|
|
209
|
+
await removeStoredToken(opts.appId, opts.userOpenId);
|
|
210
|
+
throw new NeedAuthorizationError(opts.userOpenId);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Execute an API call with a valid UAT, retrying once on token-expiry errors.
|
|
215
|
+
*/
|
|
216
|
+
export async function callWithUAT<T>(
|
|
217
|
+
opts: UATCallOptions,
|
|
218
|
+
apiCall: (accessToken: string) => Promise<T>
|
|
219
|
+
): Promise<T> {
|
|
220
|
+
const accessToken = await getValidAccessToken(opts);
|
|
221
|
+
try {
|
|
222
|
+
return await apiCall(accessToken);
|
|
223
|
+
} catch (err: unknown) {
|
|
224
|
+
// Retry once if the server reports token invalid/expired.
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
226
|
+
const code = (err as any)?.code ?? (err as any)?.response?.data?.code;
|
|
227
|
+
if (TOKEN_RETRY_CODES.has(code as number)) {
|
|
228
|
+
log.warn(`API call failed (code=${code}), refreshing and retrying`);
|
|
229
|
+
const stored = await getStoredToken(opts.appId, opts.userOpenId);
|
|
230
|
+
if (!stored) throw new NeedAuthorizationError(opts.userOpenId);
|
|
231
|
+
const refreshed = await refreshWithLock(opts, stored);
|
|
232
|
+
if (!refreshed) throw new NeedAuthorizationError(opts.userOpenId);
|
|
233
|
+
return await apiCall(refreshed.accessToken);
|
|
234
|
+
}
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get the current UAT status for a user.
|
|
241
|
+
*/
|
|
242
|
+
export async function getUATStatus(opts: UATCallOptions): Promise<UATStatus> {
|
|
243
|
+
const stored = await getStoredToken(opts.appId, opts.userOpenId);
|
|
244
|
+
if (!stored) {
|
|
245
|
+
return {
|
|
246
|
+
authorized: false,
|
|
247
|
+
userOpenId: opts.userOpenId,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const status = tokenStatus(stored);
|
|
252
|
+
return {
|
|
253
|
+
authorized: status !== 'expired',
|
|
254
|
+
userOpenId: opts.userOpenId,
|
|
255
|
+
scope: stored.scope,
|
|
256
|
+
expiresAt: stored.expiresAt,
|
|
257
|
+
refreshExpiresAt: stored.refreshExpiresAt,
|
|
258
|
+
grantedAt: stored.grantedAt,
|
|
259
|
+
tokenStatus: status,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Save a new token from OAuth device flow.
|
|
265
|
+
*/
|
|
266
|
+
export async function saveTokenFromDeviceFlow(
|
|
267
|
+
opts: UATCallOptions,
|
|
268
|
+
tokenData: {
|
|
269
|
+
accessToken: string;
|
|
270
|
+
refreshToken: string;
|
|
271
|
+
expiresIn: number;
|
|
272
|
+
refreshExpiresIn: number;
|
|
273
|
+
scope: string;
|
|
274
|
+
}
|
|
275
|
+
): Promise<StoredUAToken> {
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
const stored: StoredUAToken = {
|
|
278
|
+
userOpenId: opts.userOpenId,
|
|
279
|
+
appId: opts.appId,
|
|
280
|
+
accessToken: tokenData.accessToken,
|
|
281
|
+
refreshToken: tokenData.refreshToken,
|
|
282
|
+
expiresAt: now + tokenData.expiresIn * 1000,
|
|
283
|
+
refreshExpiresAt: now + tokenData.refreshExpiresIn * 1000,
|
|
284
|
+
scope: tokenData.scope,
|
|
285
|
+
grantedAt: now,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
await setStoredToken(stored);
|
|
289
|
+
return stored;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Revoke a user's UAT by removing it from storage.
|
|
294
|
+
*/
|
|
295
|
+
export async function revokeUAT(appId: string, userOpenId: string): Promise<void> {
|
|
296
|
+
await removeStoredToken(appId, userOpenId);
|
|
297
|
+
log.info(`revoked UAT for ${userOpenId}`);
|
|
298
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*
|
|
5
|
+
* Version management for cc-lark MCP Server.
|
|
6
|
+
*
|
|
7
|
+
* Reads version from package.json and generates User-Agent string.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
|
|
14
|
+
/** Cached version string */
|
|
15
|
+
let cachedVersion: string | undefined;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the package version from package.json.
|
|
19
|
+
*
|
|
20
|
+
* @returns Version string (e.g., "0.1.0") or "unknown" if read fails
|
|
21
|
+
*/
|
|
22
|
+
export function getPackageVersion(): string {
|
|
23
|
+
if (cachedVersion) return cachedVersion;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Current file: src/core/version.ts -> up two levels to project root
|
|
27
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
28
|
+
const __dirname = dirname(__filename);
|
|
29
|
+
const packageJsonPath = join(__dirname, '..', '..', 'package.json');
|
|
30
|
+
|
|
31
|
+
const raw = readFileSync(packageJsonPath, 'utf8');
|
|
32
|
+
const pkg = JSON.parse(raw) as { version?: string };
|
|
33
|
+
cachedVersion = pkg.version ?? 'unknown';
|
|
34
|
+
return cachedVersion;
|
|
35
|
+
} catch {
|
|
36
|
+
cachedVersion = 'unknown';
|
|
37
|
+
return cachedVersion;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate User-Agent string for HTTP requests.
|
|
43
|
+
*
|
|
44
|
+
* @returns User-Agent string (e.g., "cc-lark/0.1.0")
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* getUserAgent() // => "cc-lark/0.1.0"
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function getUserAgent(): string {
|
|
52
|
+
return `cc-lark/${getPackageVersion()}`;
|
|
53
|
+
}
|