feishu-mcp 0.0.6 → 0.0.7

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.
@@ -0,0 +1,221 @@
1
+ import { Config } from './config.js';
2
+ import { Logger } from './logger.js';
3
+ /**
4
+ * 缓存管理器类
5
+ * 提供内存缓存功能,支持TTL和最大容量限制
6
+ * 只用于缓存用户token和wiki转docid结果
7
+ */
8
+ export class CacheManager {
9
+ /**
10
+ * 私有构造函数,用于单例模式
11
+ */
12
+ constructor() {
13
+ Object.defineProperty(this, "cache", {
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true,
17
+ value: void 0
18
+ });
19
+ Object.defineProperty(this, "config", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: void 0
24
+ });
25
+ this.cache = new Map();
26
+ this.config = Config.getInstance();
27
+ // 定期清理过期缓存
28
+ setInterval(() => {
29
+ this.cleanExpiredCache();
30
+ }, 60000); // 每分钟清理一次过期缓存
31
+ }
32
+ /**
33
+ * 获取缓存管理器实例
34
+ * @returns 缓存管理器实例
35
+ */
36
+ static getInstance() {
37
+ if (!CacheManager.instance) {
38
+ CacheManager.instance = new CacheManager();
39
+ }
40
+ return CacheManager.instance;
41
+ }
42
+ /**
43
+ * 设置缓存
44
+ * @param key 缓存键
45
+ * @param data 缓存数据
46
+ * @param ttl 缓存生存时间(秒),默认使用配置中的TTL
47
+ * @returns 是否成功设置缓存
48
+ */
49
+ set(key, data, ttl) {
50
+ if (!this.config.cache.enabled) {
51
+ return false;
52
+ }
53
+ // 如果缓存已达到最大容量,清理最早的条目
54
+ if (this.cache.size >= this.config.cache.maxSize) {
55
+ this.cleanOldestCache();
56
+ }
57
+ const now = Date.now();
58
+ const actualTtl = ttl || this.config.cache.ttl;
59
+ this.cache.set(key, {
60
+ data,
61
+ timestamp: now,
62
+ expiresAt: now + (actualTtl * 1000)
63
+ });
64
+ Logger.debug(`缓存设置: ${key} (TTL: ${actualTtl}秒)`);
65
+ return true;
66
+ }
67
+ /**
68
+ * 获取缓存
69
+ * @param key 缓存键
70
+ * @returns 缓存数据,如果未找到或已过期则返回null
71
+ */
72
+ get(key) {
73
+ if (!this.config.cache.enabled) {
74
+ return null;
75
+ }
76
+ const cacheItem = this.cache.get(key);
77
+ if (!cacheItem) {
78
+ Logger.debug(`缓存未命中: ${key}`);
79
+ return null;
80
+ }
81
+ // 检查是否过期
82
+ if (Date.now() > cacheItem.expiresAt) {
83
+ Logger.debug(`缓存已过期: ${key}`);
84
+ this.cache.delete(key);
85
+ return null;
86
+ }
87
+ Logger.debug(`缓存命中: ${key}`);
88
+ return cacheItem.data;
89
+ }
90
+ /**
91
+ * 删除缓存
92
+ * @param key 缓存键
93
+ * @returns 是否成功删除
94
+ */
95
+ delete(key) {
96
+ if (!this.config.cache.enabled) {
97
+ return false;
98
+ }
99
+ const result = this.cache.delete(key);
100
+ if (result) {
101
+ Logger.debug(`缓存删除: ${key}`);
102
+ }
103
+ return result;
104
+ }
105
+ /**
106
+ * 清空所有缓存
107
+ */
108
+ clear() {
109
+ if (!this.config.cache.enabled) {
110
+ return;
111
+ }
112
+ const size = this.cache.size;
113
+ this.cache.clear();
114
+ Logger.debug(`清空全部缓存,删除了 ${size} 条记录`);
115
+ }
116
+ /**
117
+ * 根据前缀清除缓存
118
+ * @param prefix 缓存键前缀
119
+ * @returns 清除的缓存数量
120
+ */
121
+ clearByPrefix(prefix) {
122
+ if (!this.config.cache.enabled) {
123
+ return 0;
124
+ }
125
+ let count = 0;
126
+ for (const key of this.cache.keys()) {
127
+ if (key.startsWith(prefix)) {
128
+ this.cache.delete(key);
129
+ count++;
130
+ }
131
+ }
132
+ if (count > 0) {
133
+ Logger.debug(`按前缀清除缓存: ${prefix}, 删除了 ${count} 条记录`);
134
+ }
135
+ return count;
136
+ }
137
+ /**
138
+ * 清理过期缓存
139
+ * @returns 清理的缓存数量
140
+ */
141
+ cleanExpiredCache() {
142
+ if (!this.config.cache.enabled) {
143
+ return 0;
144
+ }
145
+ const now = Date.now();
146
+ let count = 0;
147
+ for (const [key, item] of this.cache.entries()) {
148
+ if (now > item.expiresAt) {
149
+ this.cache.delete(key);
150
+ count++;
151
+ }
152
+ }
153
+ if (count > 0) {
154
+ Logger.debug(`清理过期缓存,删除了 ${count} 条记录`);
155
+ }
156
+ return count;
157
+ }
158
+ /**
159
+ * 清理最旧的缓存
160
+ * @param count 要清理的条目数,默认为1
161
+ */
162
+ cleanOldestCache(count = 1) {
163
+ if (!this.config.cache.enabled || this.cache.size === 0) {
164
+ return;
165
+ }
166
+ // 按时间戳排序
167
+ const entries = Array.from(this.cache.entries())
168
+ .sort((a, b) => a[1].timestamp - b[1].timestamp);
169
+ // 删除最早的几条记录
170
+ const toDelete = Math.min(count, entries.length);
171
+ for (let i = 0; i < toDelete; i++) {
172
+ this.cache.delete(entries[i][0]);
173
+ }
174
+ Logger.debug(`清理最旧缓存,删除了 ${toDelete} 条记录`);
175
+ }
176
+ /**
177
+ * 获取缓存统计信息
178
+ * @returns 缓存统计信息对象
179
+ */
180
+ getStats() {
181
+ return {
182
+ size: this.cache.size,
183
+ enabled: this.config.cache.enabled,
184
+ maxSize: this.config.cache.maxSize,
185
+ ttl: this.config.cache.ttl
186
+ };
187
+ }
188
+ /**
189
+ * 缓存访问令牌
190
+ * @param token 访问令牌
191
+ * @param expiresInSeconds 过期时间(秒)
192
+ * @returns 是否成功设置缓存
193
+ */
194
+ cacheToken(token, expiresInSeconds) {
195
+ return this.set('access_token', token, expiresInSeconds);
196
+ }
197
+ /**
198
+ * 获取缓存的访问令牌
199
+ * @returns 访问令牌,如果未找到或已过期则返回null
200
+ */
201
+ getToken() {
202
+ return this.get('access_token');
203
+ }
204
+ /**
205
+ * 缓存Wiki到文档ID的转换结果
206
+ * @param wikiToken Wiki Token
207
+ * @param documentId 文档ID
208
+ * @returns 是否成功设置缓存
209
+ */
210
+ cacheWikiToDocId(wikiToken, documentId) {
211
+ return this.set(`wiki:${wikiToken}`, documentId);
212
+ }
213
+ /**
214
+ * 获取缓存的Wiki转换结果
215
+ * @param wikiToken Wiki Token
216
+ * @returns 文档ID,如果未找到或已过期则返回null
217
+ */
218
+ getWikiToDocId(wikiToken) {
219
+ return this.get(`wiki:${wikiToken}`);
220
+ }
221
+ }
@@ -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,74 @@
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
+ }