feishu-mcp 0.0.6 → 0.0.8
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/README.md +1 -6
- package/dist/config.js +22 -100
- package/dist/index.js +14 -13
- package/dist/server.js +512 -449
- package/dist/services/baseService.js +204 -0
- package/dist/services/blockFactory.js +184 -0
- package/dist/services/feishuApiService.js +630 -0
- package/dist/services/feishuBlockService.js +179 -0
- package/dist/services/feishuService.js +475 -0
- package/dist/types/feishuSchema.js +119 -0
- package/dist/utils/cache.js +221 -0
- package/dist/utils/config.js +363 -0
- package/dist/utils/document.js +112 -0
- package/dist/utils/error.js +154 -0
- package/dist/utils/logger.js +257 -0
- package/dist/utils/paramUtils.js +193 -0
- package/package.json +1 -1
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { config as loadDotEnv } from 'dotenv';
|
|
2
|
+
import { hideBin } from 'yargs/helpers';
|
|
3
|
+
import yargs from 'yargs';
|
|
4
|
+
import { Logger, LogLevel } from './logger.js';
|
|
5
|
+
/**
|
|
6
|
+
* 配置来源枚举
|
|
7
|
+
*/
|
|
8
|
+
export var ConfigSource;
|
|
9
|
+
(function (ConfigSource) {
|
|
10
|
+
ConfigSource["DEFAULT"] = "default";
|
|
11
|
+
ConfigSource["ENV"] = "env";
|
|
12
|
+
ConfigSource["CLI"] = "cli";
|
|
13
|
+
ConfigSource["FILE"] = "file";
|
|
14
|
+
})(ConfigSource || (ConfigSource = {}));
|
|
15
|
+
/**
|
|
16
|
+
* 应用配置管理类
|
|
17
|
+
* 统一管理所有配置,支持环境变量、命令行参数和默认值
|
|
18
|
+
*/
|
|
19
|
+
export class Config {
|
|
20
|
+
/**
|
|
21
|
+
* 私有构造函数,用于单例模式
|
|
22
|
+
*/
|
|
23
|
+
constructor() {
|
|
24
|
+
Object.defineProperty(this, "server", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: void 0
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(this, "feishu", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: void 0
|
|
35
|
+
});
|
|
36
|
+
Object.defineProperty(this, "log", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
value: void 0
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(this, "cache", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: void 0
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(this, "configSources", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: void 0
|
|
53
|
+
});
|
|
54
|
+
// 确保在任何配置读取前加载.env文件
|
|
55
|
+
loadDotEnv();
|
|
56
|
+
// 解析命令行参数
|
|
57
|
+
const argv = this.parseCommandLineArgs();
|
|
58
|
+
// 初始化配置来源记录
|
|
59
|
+
this.configSources = {};
|
|
60
|
+
// 配置服务器
|
|
61
|
+
this.server = this.initServerConfig(argv);
|
|
62
|
+
// 配置飞书
|
|
63
|
+
this.feishu = this.initFeishuConfig(argv);
|
|
64
|
+
// 配置日志
|
|
65
|
+
this.log = this.initLogConfig(argv);
|
|
66
|
+
// 配置缓存
|
|
67
|
+
this.cache = this.initCacheConfig(argv);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 获取配置单例
|
|
71
|
+
* @returns 配置实例
|
|
72
|
+
*/
|
|
73
|
+
static getInstance() {
|
|
74
|
+
if (!Config.instance) {
|
|
75
|
+
Config.instance = new Config();
|
|
76
|
+
}
|
|
77
|
+
return Config.instance;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 解析命令行参数
|
|
81
|
+
* @returns 解析后的参数对象
|
|
82
|
+
*/
|
|
83
|
+
parseCommandLineArgs() {
|
|
84
|
+
return yargs(hideBin(process.argv))
|
|
85
|
+
.options({
|
|
86
|
+
port: {
|
|
87
|
+
type: 'number',
|
|
88
|
+
description: '服务器监听端口'
|
|
89
|
+
},
|
|
90
|
+
'log-level': {
|
|
91
|
+
type: 'string',
|
|
92
|
+
description: '日志级别 (debug, info, log, warn, error, none)'
|
|
93
|
+
},
|
|
94
|
+
'feishu-app-id': {
|
|
95
|
+
type: 'string',
|
|
96
|
+
description: '飞书应用ID'
|
|
97
|
+
},
|
|
98
|
+
'feishu-app-secret': {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: '飞书应用密钥'
|
|
101
|
+
},
|
|
102
|
+
'feishu-base-url': {
|
|
103
|
+
type: 'string',
|
|
104
|
+
description: '飞书API基础URL'
|
|
105
|
+
},
|
|
106
|
+
'cache-enabled': {
|
|
107
|
+
type: 'boolean',
|
|
108
|
+
description: '是否启用缓存'
|
|
109
|
+
},
|
|
110
|
+
'cache-ttl': {
|
|
111
|
+
type: 'number',
|
|
112
|
+
description: '缓存生存时间(秒)'
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
.help()
|
|
116
|
+
.parseSync();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 初始化服务器配置
|
|
120
|
+
* @param argv 命令行参数
|
|
121
|
+
* @returns 服务器配置
|
|
122
|
+
*/
|
|
123
|
+
initServerConfig(argv) {
|
|
124
|
+
const serverConfig = {
|
|
125
|
+
port: 3333,
|
|
126
|
+
};
|
|
127
|
+
// 处理PORT
|
|
128
|
+
if (argv.port) {
|
|
129
|
+
serverConfig.port = argv.port;
|
|
130
|
+
this.configSources['server.port'] = ConfigSource.CLI;
|
|
131
|
+
}
|
|
132
|
+
else if (process.env.PORT) {
|
|
133
|
+
serverConfig.port = parseInt(process.env.PORT, 10);
|
|
134
|
+
this.configSources['server.port'] = ConfigSource.ENV;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
this.configSources['server.port'] = ConfigSource.DEFAULT;
|
|
138
|
+
}
|
|
139
|
+
return serverConfig;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* 初始化飞书配置
|
|
143
|
+
* @param argv 命令行参数
|
|
144
|
+
* @returns 飞书配置
|
|
145
|
+
*/
|
|
146
|
+
initFeishuConfig(argv) {
|
|
147
|
+
const feishuConfig = {
|
|
148
|
+
appId: '',
|
|
149
|
+
appSecret: '',
|
|
150
|
+
baseUrl: 'https://open.feishu.cn/open-apis',
|
|
151
|
+
tokenLifetime: 7200000 // 2小时,单位:毫秒
|
|
152
|
+
};
|
|
153
|
+
// 处理App ID
|
|
154
|
+
if (argv['feishu-app-id']) {
|
|
155
|
+
feishuConfig.appId = argv['feishu-app-id'];
|
|
156
|
+
this.configSources['feishu.appId'] = ConfigSource.CLI;
|
|
157
|
+
}
|
|
158
|
+
else if (process.env.FEISHU_APP_ID) {
|
|
159
|
+
feishuConfig.appId = process.env.FEISHU_APP_ID;
|
|
160
|
+
this.configSources['feishu.appId'] = ConfigSource.ENV;
|
|
161
|
+
}
|
|
162
|
+
// 处理App Secret
|
|
163
|
+
if (argv['feishu-app-secret']) {
|
|
164
|
+
feishuConfig.appSecret = argv['feishu-app-secret'];
|
|
165
|
+
this.configSources['feishu.appSecret'] = ConfigSource.CLI;
|
|
166
|
+
}
|
|
167
|
+
else if (process.env.FEISHU_APP_SECRET) {
|
|
168
|
+
feishuConfig.appSecret = process.env.FEISHU_APP_SECRET;
|
|
169
|
+
this.configSources['feishu.appSecret'] = ConfigSource.ENV;
|
|
170
|
+
}
|
|
171
|
+
// 处理Base URL
|
|
172
|
+
if (argv['feishu-base-url']) {
|
|
173
|
+
feishuConfig.baseUrl = argv['feishu-base-url'];
|
|
174
|
+
this.configSources['feishu.baseUrl'] = ConfigSource.CLI;
|
|
175
|
+
}
|
|
176
|
+
else if (process.env.FEISHU_BASE_URL) {
|
|
177
|
+
feishuConfig.baseUrl = process.env.FEISHU_BASE_URL;
|
|
178
|
+
this.configSources['feishu.baseUrl'] = ConfigSource.ENV;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.configSources['feishu.baseUrl'] = ConfigSource.DEFAULT;
|
|
182
|
+
}
|
|
183
|
+
// 处理token生命周期
|
|
184
|
+
if (process.env.FEISHU_TOKEN_LIFETIME) {
|
|
185
|
+
feishuConfig.tokenLifetime = parseInt(process.env.FEISHU_TOKEN_LIFETIME, 10) * 1000;
|
|
186
|
+
this.configSources['feishu.tokenLifetime'] = ConfigSource.ENV;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this.configSources['feishu.tokenLifetime'] = ConfigSource.DEFAULT;
|
|
190
|
+
}
|
|
191
|
+
return feishuConfig;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 初始化日志配置
|
|
195
|
+
* @param argv 命令行参数
|
|
196
|
+
* @returns 日志配置
|
|
197
|
+
*/
|
|
198
|
+
initLogConfig(argv) {
|
|
199
|
+
const logConfig = {
|
|
200
|
+
level: LogLevel.INFO,
|
|
201
|
+
showTimestamp: true,
|
|
202
|
+
showLevel: true,
|
|
203
|
+
timestampFormat: 'yyyy-MM-dd HH:mm:ss.SSS'
|
|
204
|
+
};
|
|
205
|
+
// 处理日志级别
|
|
206
|
+
if (argv['log-level']) {
|
|
207
|
+
logConfig.level = this.getLogLevelFromString(argv['log-level']);
|
|
208
|
+
this.configSources['log.level'] = ConfigSource.CLI;
|
|
209
|
+
}
|
|
210
|
+
else if (process.env.LOG_LEVEL) {
|
|
211
|
+
logConfig.level = this.getLogLevelFromString(process.env.LOG_LEVEL);
|
|
212
|
+
this.configSources['log.level'] = ConfigSource.ENV;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
this.configSources['log.level'] = ConfigSource.DEFAULT;
|
|
216
|
+
}
|
|
217
|
+
// 处理时间戳显示
|
|
218
|
+
if (process.env.LOG_SHOW_TIMESTAMP) {
|
|
219
|
+
logConfig.showTimestamp = process.env.LOG_SHOW_TIMESTAMP.toLowerCase() === 'true';
|
|
220
|
+
this.configSources['log.showTimestamp'] = ConfigSource.ENV;
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
this.configSources['log.showTimestamp'] = ConfigSource.DEFAULT;
|
|
224
|
+
}
|
|
225
|
+
// 处理级别显示
|
|
226
|
+
if (process.env.LOG_SHOW_LEVEL) {
|
|
227
|
+
logConfig.showLevel = process.env.LOG_SHOW_LEVEL.toLowerCase() === 'true';
|
|
228
|
+
this.configSources['log.showLevel'] = ConfigSource.ENV;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.configSources['log.showLevel'] = ConfigSource.DEFAULT;
|
|
232
|
+
}
|
|
233
|
+
// 处理时间戳格式
|
|
234
|
+
if (process.env.LOG_TIMESTAMP_FORMAT) {
|
|
235
|
+
logConfig.timestampFormat = process.env.LOG_TIMESTAMP_FORMAT;
|
|
236
|
+
this.configSources['log.timestampFormat'] = ConfigSource.ENV;
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
this.configSources['log.timestampFormat'] = ConfigSource.DEFAULT;
|
|
240
|
+
}
|
|
241
|
+
return logConfig;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* 初始化缓存配置
|
|
245
|
+
* @param argv 命令行参数
|
|
246
|
+
* @returns 缓存配置
|
|
247
|
+
*/
|
|
248
|
+
initCacheConfig(argv) {
|
|
249
|
+
const cacheConfig = {
|
|
250
|
+
enabled: true,
|
|
251
|
+
ttl: 300, // 5分钟,单位:秒
|
|
252
|
+
maxSize: 100
|
|
253
|
+
};
|
|
254
|
+
// 处理缓存启用
|
|
255
|
+
if (argv['cache-enabled'] !== undefined) {
|
|
256
|
+
cacheConfig.enabled = argv['cache-enabled'];
|
|
257
|
+
this.configSources['cache.enabled'] = ConfigSource.CLI;
|
|
258
|
+
}
|
|
259
|
+
else if (process.env.CACHE_ENABLED) {
|
|
260
|
+
cacheConfig.enabled = process.env.CACHE_ENABLED.toLowerCase() === 'true';
|
|
261
|
+
this.configSources['cache.enabled'] = ConfigSource.ENV;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
this.configSources['cache.enabled'] = ConfigSource.DEFAULT;
|
|
265
|
+
}
|
|
266
|
+
// 处理TTL
|
|
267
|
+
if (argv['cache-ttl']) {
|
|
268
|
+
cacheConfig.ttl = argv['cache-ttl'];
|
|
269
|
+
this.configSources['cache.ttl'] = ConfigSource.CLI;
|
|
270
|
+
}
|
|
271
|
+
else if (process.env.CACHE_TTL) {
|
|
272
|
+
cacheConfig.ttl = parseInt(process.env.CACHE_TTL, 10);
|
|
273
|
+
this.configSources['cache.ttl'] = ConfigSource.ENV;
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
this.configSources['cache.ttl'] = ConfigSource.DEFAULT;
|
|
277
|
+
}
|
|
278
|
+
// 处理最大缓存大小
|
|
279
|
+
if (process.env.CACHE_MAX_SIZE) {
|
|
280
|
+
cacheConfig.maxSize = parseInt(process.env.CACHE_MAX_SIZE, 10);
|
|
281
|
+
this.configSources['cache.maxSize'] = ConfigSource.ENV;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
this.configSources['cache.maxSize'] = ConfigSource.DEFAULT;
|
|
285
|
+
}
|
|
286
|
+
return cacheConfig;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 从字符串获取日志级别
|
|
290
|
+
* @param levelStr 日志级别字符串
|
|
291
|
+
* @returns 日志级别枚举值
|
|
292
|
+
*/
|
|
293
|
+
getLogLevelFromString(levelStr) {
|
|
294
|
+
switch (levelStr.toLowerCase()) {
|
|
295
|
+
case 'debug': return LogLevel.DEBUG;
|
|
296
|
+
case 'info': return LogLevel.INFO;
|
|
297
|
+
case 'log': return LogLevel.LOG;
|
|
298
|
+
case 'warn': return LogLevel.WARN;
|
|
299
|
+
case 'error': return LogLevel.ERROR;
|
|
300
|
+
case 'none': return LogLevel.NONE;
|
|
301
|
+
default: return LogLevel.INFO;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 打印当前配置信息
|
|
306
|
+
* @param isStdioMode 是否在stdio模式下
|
|
307
|
+
*/
|
|
308
|
+
printConfig(isStdioMode = false) {
|
|
309
|
+
if (isStdioMode)
|
|
310
|
+
return;
|
|
311
|
+
Logger.info('当前配置:');
|
|
312
|
+
Logger.info('服务器配置:');
|
|
313
|
+
Logger.info(`- 端口: ${this.server.port} (来源: ${this.configSources['server.port']})`);
|
|
314
|
+
Logger.info('飞书配置:');
|
|
315
|
+
if (this.feishu.appId) {
|
|
316
|
+
Logger.info(`- App ID: ${this.maskApiKey(this.feishu.appId)} (来源: ${this.configSources['feishu.appId']})`);
|
|
317
|
+
}
|
|
318
|
+
if (this.feishu.appSecret) {
|
|
319
|
+
Logger.info(`- App Secret: ${this.maskApiKey(this.feishu.appSecret)} (来源: ${this.configSources['feishu.appSecret']})`);
|
|
320
|
+
}
|
|
321
|
+
Logger.info(`- API URL: ${this.feishu.baseUrl} (来源: ${this.configSources['feishu.baseUrl']})`);
|
|
322
|
+
Logger.info(`- Token生命周期: ${this.feishu.tokenLifetime / 1000}秒 (来源: ${this.configSources['feishu.tokenLifetime']})`);
|
|
323
|
+
Logger.info('日志配置:');
|
|
324
|
+
Logger.info(`- 日志级别: ${LogLevel[this.log.level]} (来源: ${this.configSources['log.level']})`);
|
|
325
|
+
Logger.info(`- 显示时间戳: ${this.log.showTimestamp} (来源: ${this.configSources['log.showTimestamp']})`);
|
|
326
|
+
Logger.info(`- 显示日志级别: ${this.log.showLevel} (来源: ${this.configSources['log.showLevel']})`);
|
|
327
|
+
Logger.info('缓存配置:');
|
|
328
|
+
Logger.info(`- 启用缓存: ${this.cache.enabled} (来源: ${this.configSources['cache.enabled']})`);
|
|
329
|
+
Logger.info(`- 缓存TTL: ${this.cache.ttl}秒 (来源: ${this.configSources['cache.ttl']})`);
|
|
330
|
+
Logger.info(`- 最大缓存条目: ${this.cache.maxSize} (来源: ${this.configSources['cache.maxSize']})`);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 掩盖API密钥
|
|
334
|
+
* @param key API密钥
|
|
335
|
+
* @returns 掩盖后的密钥字符串
|
|
336
|
+
*/
|
|
337
|
+
maskApiKey(key) {
|
|
338
|
+
if (!key || key.length <= 4)
|
|
339
|
+
return '****';
|
|
340
|
+
return `${key.substring(0, 2)}****${key.substring(key.length - 2)}`;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* 验证配置是否完整有效
|
|
344
|
+
* @returns 是否验证成功
|
|
345
|
+
*/
|
|
346
|
+
validate() {
|
|
347
|
+
// 验证服务器配置
|
|
348
|
+
if (!this.server.port || this.server.port <= 0) {
|
|
349
|
+
Logger.error('无效的服务器端口配置');
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
// 验证飞书配置
|
|
353
|
+
if (!this.feishu.appId) {
|
|
354
|
+
Logger.error('缺少飞书应用ID,请通过环境变量FEISHU_APP_ID或命令行参数--feishu-app-id提供');
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
if (!this.feishu.appSecret) {
|
|
358
|
+
Logger.error('缺少飞书应用Secret,请通过环境变量FEISHU_APP_SECRET或命令行参数--feishu-app-secret提供');
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从URL或ID中提取飞书文档ID
|
|
3
|
+
* 支持多种格式:
|
|
4
|
+
* 1. 标准文档URL: https://xxx.feishu.cn/docs/xxx 或 https://xxx.feishu.cn/docx/xxx
|
|
5
|
+
* 2. API URL: https://open.feishu.cn/open-apis/docx/v1/documents/xxx
|
|
6
|
+
* 3. 直接ID: JcKbdlokYoPIe0xDzJ1cduRXnRf
|
|
7
|
+
*
|
|
8
|
+
* @param input 文档URL或ID
|
|
9
|
+
* @returns 提取的文档ID或null
|
|
10
|
+
*/
|
|
11
|
+
export function extractDocumentId(input) {
|
|
12
|
+
// 移除首尾空白
|
|
13
|
+
input = input.trim();
|
|
14
|
+
// 处理各种URL格式
|
|
15
|
+
const docxMatch = input.match(/\/docx\/([a-zA-Z0-9_-]+)/i);
|
|
16
|
+
const docsMatch = input.match(/\/docs\/([a-zA-Z0-9_-]+)/i);
|
|
17
|
+
const apiMatch = input.match(/\/documents\/([a-zA-Z0-9_-]+)/i);
|
|
18
|
+
const directIdMatch = input.match(/^([a-zA-Z0-9_-]{10,})$/); // 假设ID至少10个字符
|
|
19
|
+
// 按优先级返回匹配结果
|
|
20
|
+
const match = docxMatch || docsMatch || apiMatch || directIdMatch;
|
|
21
|
+
return match ? match[1] : null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 从URL或Token中提取Wiki节点ID
|
|
25
|
+
* 支持多种格式:
|
|
26
|
+
* 1. Wiki URL: https://xxx.feishu.cn/wiki/xxx
|
|
27
|
+
* 2. 直接Token: xxx
|
|
28
|
+
*
|
|
29
|
+
* @param input Wiki URL或Token
|
|
30
|
+
* @returns 提取的Wiki Token或null
|
|
31
|
+
*/
|
|
32
|
+
export function extractWikiToken(input) {
|
|
33
|
+
// 移除首尾空白
|
|
34
|
+
input = input.trim();
|
|
35
|
+
// 处理Wiki URL格式
|
|
36
|
+
const wikiMatch = input.match(/\/wiki\/([a-zA-Z0-9_-]+)/i);
|
|
37
|
+
const directMatch = input.match(/^([a-zA-Z0-9_-]{10,})$/); // 假设Token至少10个字符
|
|
38
|
+
// 提取Token,如果存在查询参数,去掉它们
|
|
39
|
+
let token = wikiMatch ? wikiMatch[1] : (directMatch ? directMatch[1] : null);
|
|
40
|
+
if (token && token.includes('?')) {
|
|
41
|
+
token = token.split('?')[0];
|
|
42
|
+
}
|
|
43
|
+
return token;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 规范化文档ID
|
|
47
|
+
* 提取输入中的文档ID,如果提取失败则返回原输入
|
|
48
|
+
*
|
|
49
|
+
* @param input 文档URL或ID
|
|
50
|
+
* @returns 规范化的文档ID
|
|
51
|
+
* @throws 如果无法提取有效ID则抛出错误
|
|
52
|
+
*/
|
|
53
|
+
export function normalizeDocumentId(input) {
|
|
54
|
+
const id = extractDocumentId(input);
|
|
55
|
+
if (!id) {
|
|
56
|
+
throw new Error(`无法从 "${input}" 提取有效的文档ID`);
|
|
57
|
+
}
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 规范化Wiki Token
|
|
62
|
+
* 提取输入中的Wiki Token,如果提取失败则返回原输入
|
|
63
|
+
*
|
|
64
|
+
* @param input Wiki URL或Token
|
|
65
|
+
* @returns 规范化的Wiki Token
|
|
66
|
+
* @throws 如果无法提取有效Token则抛出错误
|
|
67
|
+
*/
|
|
68
|
+
export function normalizeWikiToken(input) {
|
|
69
|
+
const token = extractWikiToken(input);
|
|
70
|
+
if (!token) {
|
|
71
|
+
throw new Error(`无法从 "${input}" 提取有效的Wiki Token`);
|
|
72
|
+
}
|
|
73
|
+
return token;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 根据图片二进制数据检测MIME类型
|
|
77
|
+
* @param buffer 图片二进制数据
|
|
78
|
+
* @returns MIME类型字符串
|
|
79
|
+
*/
|
|
80
|
+
export function detectMimeType(buffer) {
|
|
81
|
+
// 简单的图片格式检测,根据文件头进行判断
|
|
82
|
+
if (buffer.length < 4) {
|
|
83
|
+
return 'application/octet-stream';
|
|
84
|
+
}
|
|
85
|
+
// JPEG格式
|
|
86
|
+
if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) {
|
|
87
|
+
return 'image/jpeg';
|
|
88
|
+
}
|
|
89
|
+
// PNG格式
|
|
90
|
+
else if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) {
|
|
91
|
+
return 'image/png';
|
|
92
|
+
}
|
|
93
|
+
// GIF格式
|
|
94
|
+
else if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
|
|
95
|
+
return 'image/gif';
|
|
96
|
+
}
|
|
97
|
+
// SVG格式 - 检查字符串前缀
|
|
98
|
+
else if (buffer.length > 5 && buffer.toString('ascii', 0, 5).toLowerCase() === '<?xml' ||
|
|
99
|
+
buffer.toString('ascii', 0, 4).toLowerCase() === '<svg') {
|
|
100
|
+
return 'image/svg+xml';
|
|
101
|
+
}
|
|
102
|
+
// WebP格式
|
|
103
|
+
else if (buffer.length > 12 &&
|
|
104
|
+
buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 &&
|
|
105
|
+
buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50) {
|
|
106
|
+
return 'image/webp';
|
|
107
|
+
}
|
|
108
|
+
// 默认二进制流
|
|
109
|
+
else {
|
|
110
|
+
return 'application/octet-stream';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Logger } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* 错误排查指南映射
|
|
4
|
+
*/
|
|
5
|
+
const errorGuides = {
|
|
6
|
+
// 飞书API标准错误码
|
|
7
|
+
'1770002': '资源未找到。请检查文档ID/块ID是否正确,并确保您有权限访问该资源。',
|
|
8
|
+
'1770001': '权限不足。请确保应用有足够的权限访问此资源。',
|
|
9
|
+
'1770003': '内部服务错误。请稍后重试。',
|
|
10
|
+
'1770004': '参数格式错误。请检查API请求参数是否正确。',
|
|
11
|
+
'1770005': '请求频率限制。请减少请求频率后重试。',
|
|
12
|
+
'1770006': '操作冲突。可能有其他用户正在编辑同一资源。',
|
|
13
|
+
'1770007': '资源已被删除。请检查资源是否存在。',
|
|
14
|
+
'1770008': '资源已被归档。请检查资源状态。',
|
|
15
|
+
'1770015': '文档或文件夹已被移动。请使用新的位置访问。',
|
|
16
|
+
// 身份验证和通用错误
|
|
17
|
+
'99991671': '飞书应用身份验证失败。请检查App ID和App Secret是否正确,或者重新注册飞书应用。',
|
|
18
|
+
'99991663': '权限不足。请确保:\n1. 应用已获得正确的权限范围\n2. 文档已与应用共享\n3. 您有访问该文档的权限',
|
|
19
|
+
'99991672': '请求频率超过限制。请稍后再试或优化代码减少请求次数。',
|
|
20
|
+
'99991661': '资源不存在。请检查文档ID/块ID是否正确,并确保资源仍然存在。',
|
|
21
|
+
'99991648': '文档ID格式不正确。请检查ID格式,应为标准飞书文档ID、URL或Token。',
|
|
22
|
+
'token_invalid': '访问令牌无效。请尝试刷新访问令牌。',
|
|
23
|
+
'invalid_token': '访问令牌无效。请尝试刷新访问令牌。',
|
|
24
|
+
'404': '资源未找到。请检查URL或ID是否正确。',
|
|
25
|
+
'403': '访问被拒绝。请检查权限设置并确保您有足够的访问权限。',
|
|
26
|
+
'401': '未授权。请检查认证凭据或尝试重新获取访问令牌。',
|
|
27
|
+
'400': '请求参数有误。请检查提供的参数格式和值是否正确。',
|
|
28
|
+
'500': '服务器内部错误。请稍后重试或联系飞书支持团队。'
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 格式化错误消息
|
|
32
|
+
* 对飞书API各种错误响应格式进行统一处理
|
|
33
|
+
*
|
|
34
|
+
* @param error 原始错误
|
|
35
|
+
* @param context 错误上下文(可选)
|
|
36
|
+
* @returns 格式化的错误消息
|
|
37
|
+
*/
|
|
38
|
+
export function formatErrorMessage(error, context) {
|
|
39
|
+
try {
|
|
40
|
+
// 预处理错误对象
|
|
41
|
+
if (!error) {
|
|
42
|
+
return '发生未知错误';
|
|
43
|
+
}
|
|
44
|
+
// 确定错误类型
|
|
45
|
+
let errorCode;
|
|
46
|
+
let errorMsg = '';
|
|
47
|
+
let fieldViolations = [];
|
|
48
|
+
let troubleshooter = '';
|
|
49
|
+
let logId = '';
|
|
50
|
+
// 处理飞书API标准错误格式
|
|
51
|
+
if (error.apiError) {
|
|
52
|
+
const apiError = error.apiError;
|
|
53
|
+
errorCode = apiError.code;
|
|
54
|
+
errorMsg = apiError.msg || '';
|
|
55
|
+
if (apiError.error) {
|
|
56
|
+
fieldViolations = apiError.error.field_violations || [];
|
|
57
|
+
troubleshooter = apiError.error.troubleshooter || '';
|
|
58
|
+
logId = apiError.error.log_id || '';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 处理直接包含code和msg的格式
|
|
62
|
+
else if (error.code !== undefined && error.msg !== undefined) {
|
|
63
|
+
errorCode = error.code;
|
|
64
|
+
errorMsg = error.msg;
|
|
65
|
+
if (error.error) {
|
|
66
|
+
fieldViolations = error.error.field_violations || [];
|
|
67
|
+
troubleshooter = error.error.troubleshooter || '';
|
|
68
|
+
logId = error.error.log_id || '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// 处理HTTP类错误
|
|
72
|
+
else if (error.status) {
|
|
73
|
+
errorCode = error.status;
|
|
74
|
+
errorMsg = error.statusText || error.err || '请求失败';
|
|
75
|
+
}
|
|
76
|
+
// 处理标准Error对象
|
|
77
|
+
else if (error instanceof Error) {
|
|
78
|
+
errorMsg = error.message;
|
|
79
|
+
}
|
|
80
|
+
// 处理字符串错误
|
|
81
|
+
else if (typeof error === 'string') {
|
|
82
|
+
errorMsg = error;
|
|
83
|
+
}
|
|
84
|
+
// 处理其他对象类型的错误
|
|
85
|
+
else if (typeof error === 'object') {
|
|
86
|
+
errorMsg = error.message || error.error || JSON.stringify(error);
|
|
87
|
+
}
|
|
88
|
+
// 构建基本错误消息
|
|
89
|
+
let formattedMessage = '';
|
|
90
|
+
if (context) {
|
|
91
|
+
formattedMessage += `${context}: `;
|
|
92
|
+
}
|
|
93
|
+
if (errorCode !== undefined) {
|
|
94
|
+
formattedMessage += `${errorMsg} (错误码: ${errorCode})`;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
formattedMessage += errorMsg;
|
|
98
|
+
}
|
|
99
|
+
// 添加日志ID
|
|
100
|
+
if (logId) {
|
|
101
|
+
formattedMessage += `\n日志ID: ${logId}`;
|
|
102
|
+
}
|
|
103
|
+
// 添加字段验证错误信息
|
|
104
|
+
if (fieldViolations && fieldViolations.length > 0) {
|
|
105
|
+
formattedMessage += '\n字段验证错误:';
|
|
106
|
+
fieldViolations.forEach((violation) => {
|
|
107
|
+
let detail = `\n - ${violation.field}`;
|
|
108
|
+
if (violation.description) {
|
|
109
|
+
detail += `: ${violation.description}`;
|
|
110
|
+
}
|
|
111
|
+
if (violation.value !== undefined) {
|
|
112
|
+
detail += `,提供的值: ${violation.value}`;
|
|
113
|
+
}
|
|
114
|
+
formattedMessage += detail;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// 添加排查建议
|
|
118
|
+
if (troubleshooter) {
|
|
119
|
+
formattedMessage += `\n\n排查建议:\n${troubleshooter}`;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// 尝试添加预定义的错误指南
|
|
123
|
+
const errorCodeStr = String(errorCode);
|
|
124
|
+
if (errorGuides[errorCodeStr]) {
|
|
125
|
+
formattedMessage += `\n\n排查建议:\n${errorGuides[errorCodeStr]}`;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// 如果没有精确匹配,尝试通过错误消息内容模糊匹配
|
|
129
|
+
for (const [key, guide] of Object.entries(errorGuides)) {
|
|
130
|
+
if (errorMsg.toLowerCase().includes(key.toLowerCase())) {
|
|
131
|
+
formattedMessage += `\n\n排查建议:\n${guide}`;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return formattedMessage;
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
Logger.error("格式化错误消息时发生错误:", e);
|
|
141
|
+
return typeof error === 'string' ? error : '发生未知错误';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 包装错误为标准格式
|
|
146
|
+
*
|
|
147
|
+
* @param message 错误消息前缀
|
|
148
|
+
* @param originalError 原始错误
|
|
149
|
+
* @returns 包装后的错误对象
|
|
150
|
+
*/
|
|
151
|
+
export function wrapError(message, originalError) {
|
|
152
|
+
const errorMessage = formatErrorMessage(originalError);
|
|
153
|
+
return new Error(`${message}: ${errorMessage}`);
|
|
154
|
+
}
|