koishi-plugin-video-parser-all 0.5.2 → 0.5.4

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/lib/index.d.ts CHANGED
@@ -61,4 +61,32 @@ export declare const Config: Schema<{
61
61
  } & {
62
62
  autoClearCacheInterval: number;
63
63
  }>;
64
+ export declare enum ErrorCode {
65
+ SUCCESS = 0,
66
+ UNKNOWN_ERROR = 1000,
67
+ UNSUPPORTED_PLATFORM = 1001,
68
+ PLATFORM_API_NOT_CONFIGURED = 1002,
69
+ REQUEST_TIMEOUT = 1003,
70
+ NETWORK_ERROR = 1004,
71
+ DUPLICATE_PARSE = 1005,
72
+ INVALID_URL = 1006,
73
+ API_RETURN_ERROR = 2000,
74
+ API_DATA_PARSE_FAILED = 2001,
75
+ API_EMPTY_RESPONSE = 2002,
76
+ API_INVALID_RESPONSE = 2003,
77
+ VIDEO_DOWNLOAD_FAILED = 3000,
78
+ VIDEO_SIZE_EXCEEDED = 3001,
79
+ UNSUPPORTED_CONTENT_TYPE = 3002,
80
+ NO_VIDEO_FOUND = 3003,
81
+ NO_IMAGE_FOUND = 3004,
82
+ MESSAGE_SEND_FAILED = 4000,
83
+ MESSAGE_SEND_TIMEOUT = 4001,
84
+ FORWARD_MESSAGE_FAILED = 4002,
85
+ DOUYIN_PARSE_FAILED = 5001,
86
+ XIAOHONGSHU_PARSE_FAILED = 5002,
87
+ BILIBILI_PARSE_FAILED = 5003,
88
+ KUAISHOU_PARSE_FAILED = 5004,
89
+ WEIBO_PARSE_FAILED = 5005
90
+ }
91
+ export declare const ErrorMessageMap: Record<ErrorCode, string>;
64
92
  export declare function apply(ctx: Context, config: any): void;
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.Config = exports.name = void 0;
6
+ exports.ErrorMessageMap = exports.ErrorCode = exports.Config = exports.name = void 0;
7
7
  exports.apply = apply;
8
8
  const koishi_1 = require("koishi");
9
9
  const axios_1 = __importDefault(require("axios"));
@@ -22,7 +22,7 @@ exports.Config = koishi_1.Schema.intersect([
22
22
  sameLinkInterval: koishi_1.Schema.number().min(0).default(180).description('相同链接重复解析间隔(秒)'),
23
23
  }).description('基础设置'),
24
24
  koishi_1.Schema.object({
25
- unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n音乐:${音乐名}').description('统一消息格式(B站会显示投币,其他平台自动隐藏;无法获取的变量会自动隐藏)'),
25
+ unifiedMessageFormat: koishi_1.Schema.string().role('textarea').default('标题:${标题}\n作者:${作者}\n简介:${简介}\n时长:${视频时长}\n点赞:${点赞数}\n投币:${投币数}\n收藏:${收藏数}\n转发:${转发数}\n播放:${播放数}\n评论:${评论数}\n音乐:${音乐名}').description('统一消息格式(无法获取的变量会自动隐藏)\n变量介绍:\n${标题} - 内容标题\n${作者} - 作者名称\n${简介} - 内容简介\n${视频时长} - 视频时长\n${点赞数} - 点赞数量\n${投币数} - 投币数量(仅B站)\n${收藏数} - 收藏数量\n${转发数} - 转发/分享数量\n${播放数} - 播放数量\n${评论数} - 评论数量\n${音乐名} - 背景音乐名称'),
26
26
  }).description('统一消息格式'),
27
27
  koishi_1.Schema.object({
28
28
  showImageText: koishi_1.Schema.boolean().default(true).description('显示图文内容'),
@@ -38,29 +38,84 @@ exports.Config = koishi_1.Schema.intersect([
38
38
  }).description('网络与API设置'),
39
39
  koishi_1.Schema.object({
40
40
  ignoreSendError: koishi_1.Schema.boolean().default(true).description('忽略发送失败错误'),
41
- retryTimes: koishi_1.Schema.number().min(0).default(0).description('API请求重试次数'),
42
- retryInterval: koishi_1.Schema.number().min(0).default(0).description('重试间隔时间(毫秒)'),
41
+ retryTimes: koishi_1.Schema.number().min(0).default(3).description('API请求重试次数'),
42
+ retryInterval: koishi_1.Schema.number().min(0).default(1000).description('重试间隔时间(毫秒)'),
43
43
  }).description('错误与重试设置'),
44
44
  koishi_1.Schema.object({
45
45
  enableForward: koishi_1.Schema.boolean().default(false).description('启用合并转发(仅OneBot平台)'),
46
- downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频(避免链接失效)'),
46
+ downloadVideoBeforeSend: koishi_1.Schema.boolean().default(false).description('发送前先下载视频'),
47
47
  maxVideoSize: koishi_1.Schema.number().min(0).default(0).description('最大视频大小限制(MB,0为不限制)'),
48
48
  downloadThreads: koishi_1.Schema.number().min(0).max(10).default(0).description('多线程下载线程数(0为不使用多线程,1-10为启用对应线程数)'),
49
- }).description('发送方式设置(说明:视频大小超过限制将只发送链接;多线程下载可提升速度但可能增加服务器负载)'),
49
+ }).description('发送方式设置'),
50
50
  koishi_1.Schema.object({
51
51
  messageBufferDelay: koishi_1.Schema.number().min(0).default(0).description('消息缓冲延迟(毫秒,批量处理链接)'),
52
52
  }).description('消息处理设置'),
53
53
  koishi_1.Schema.object({
54
54
  autoClearCacheInterval: koishi_1.Schema.number().min(0).default(0).description('自动清理缓存间隔(分钟,0为关闭)'),
55
- }).description('缓存清理设置(说明:开启自动清理可定期删除过期的临时视频文件和解析缓存)'),
55
+ }).description('缓存清理设置'),
56
56
  ]);
57
+ var ErrorCode;
58
+ (function (ErrorCode) {
59
+ ErrorCode[ErrorCode["SUCCESS"] = 0] = "SUCCESS";
60
+ ErrorCode[ErrorCode["UNKNOWN_ERROR"] = 1000] = "UNKNOWN_ERROR";
61
+ ErrorCode[ErrorCode["UNSUPPORTED_PLATFORM"] = 1001] = "UNSUPPORTED_PLATFORM";
62
+ ErrorCode[ErrorCode["PLATFORM_API_NOT_CONFIGURED"] = 1002] = "PLATFORM_API_NOT_CONFIGURED";
63
+ ErrorCode[ErrorCode["REQUEST_TIMEOUT"] = 1003] = "REQUEST_TIMEOUT";
64
+ ErrorCode[ErrorCode["NETWORK_ERROR"] = 1004] = "NETWORK_ERROR";
65
+ ErrorCode[ErrorCode["DUPLICATE_PARSE"] = 1005] = "DUPLICATE_PARSE";
66
+ ErrorCode[ErrorCode["INVALID_URL"] = 1006] = "INVALID_URL";
67
+ ErrorCode[ErrorCode["API_RETURN_ERROR"] = 2000] = "API_RETURN_ERROR";
68
+ ErrorCode[ErrorCode["API_DATA_PARSE_FAILED"] = 2001] = "API_DATA_PARSE_FAILED";
69
+ ErrorCode[ErrorCode["API_EMPTY_RESPONSE"] = 2002] = "API_EMPTY_RESPONSE";
70
+ ErrorCode[ErrorCode["API_INVALID_RESPONSE"] = 2003] = "API_INVALID_RESPONSE";
71
+ ErrorCode[ErrorCode["VIDEO_DOWNLOAD_FAILED"] = 3000] = "VIDEO_DOWNLOAD_FAILED";
72
+ ErrorCode[ErrorCode["VIDEO_SIZE_EXCEEDED"] = 3001] = "VIDEO_SIZE_EXCEEDED";
73
+ ErrorCode[ErrorCode["UNSUPPORTED_CONTENT_TYPE"] = 3002] = "UNSUPPORTED_CONTENT_TYPE";
74
+ ErrorCode[ErrorCode["NO_VIDEO_FOUND"] = 3003] = "NO_VIDEO_FOUND";
75
+ ErrorCode[ErrorCode["NO_IMAGE_FOUND"] = 3004] = "NO_IMAGE_FOUND";
76
+ ErrorCode[ErrorCode["MESSAGE_SEND_FAILED"] = 4000] = "MESSAGE_SEND_FAILED";
77
+ ErrorCode[ErrorCode["MESSAGE_SEND_TIMEOUT"] = 4001] = "MESSAGE_SEND_TIMEOUT";
78
+ ErrorCode[ErrorCode["FORWARD_MESSAGE_FAILED"] = 4002] = "FORWARD_MESSAGE_FAILED";
79
+ ErrorCode[ErrorCode["DOUYIN_PARSE_FAILED"] = 5001] = "DOUYIN_PARSE_FAILED";
80
+ ErrorCode[ErrorCode["XIAOHONGSHU_PARSE_FAILED"] = 5002] = "XIAOHONGSHU_PARSE_FAILED";
81
+ ErrorCode[ErrorCode["BILIBILI_PARSE_FAILED"] = 5003] = "BILIBILI_PARSE_FAILED";
82
+ ErrorCode[ErrorCode["KUAISHOU_PARSE_FAILED"] = 5004] = "KUAISHOU_PARSE_FAILED";
83
+ ErrorCode[ErrorCode["WEIBO_PARSE_FAILED"] = 5005] = "WEIBO_PARSE_FAILED";
84
+ })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
85
+ exports.ErrorMessageMap = {
86
+ [ErrorCode.SUCCESS]: '操作成功',
87
+ [ErrorCode.UNKNOWN_ERROR]: '未知错误',
88
+ [ErrorCode.UNSUPPORTED_PLATFORM]: '不支持该平台链接',
89
+ [ErrorCode.PLATFORM_API_NOT_CONFIGURED]: '该平台暂未配置解析接口',
90
+ [ErrorCode.REQUEST_TIMEOUT]: '请求超时',
91
+ [ErrorCode.NETWORK_ERROR]: '网络请求失败',
92
+ [ErrorCode.DUPLICATE_PARSE]: '请勿重复解析(相同链接解析间隔未到)',
93
+ [ErrorCode.INVALID_URL]: '无效的链接格式',
94
+ [ErrorCode.API_RETURN_ERROR]: 'API返回错误',
95
+ [ErrorCode.API_DATA_PARSE_FAILED]: '数据解析异常',
96
+ [ErrorCode.API_EMPTY_RESPONSE]: 'API返回空数据',
97
+ [ErrorCode.API_INVALID_RESPONSE]: 'API返回无效格式',
98
+ [ErrorCode.VIDEO_DOWNLOAD_FAILED]: '视频下载失败',
99
+ [ErrorCode.VIDEO_SIZE_EXCEEDED]: '视频大小超过限制',
100
+ [ErrorCode.UNSUPPORTED_CONTENT_TYPE]: '不支持的内容类型',
101
+ [ErrorCode.NO_VIDEO_FOUND]: '未找到视频内容',
102
+ [ErrorCode.NO_IMAGE_FOUND]: '未找到图片内容',
103
+ [ErrorCode.MESSAGE_SEND_FAILED]: '消息发送失败',
104
+ [ErrorCode.MESSAGE_SEND_TIMEOUT]: '消息发送超时',
105
+ [ErrorCode.FORWARD_MESSAGE_FAILED]: '合并转发失败',
106
+ [ErrorCode.DOUYIN_PARSE_FAILED]: '抖音链接解析失败',
107
+ [ErrorCode.XIAOHONGSHU_PARSE_FAILED]: '小红书链接解析失败',
108
+ [ErrorCode.BILIBILI_PARSE_FAILED]: 'B站链接解析失败',
109
+ [ErrorCode.KUAISHOU_PARSE_FAILED]: '快手链接解析失败',
110
+ [ErrorCode.WEIBO_PARSE_FAILED]: '微博链接解析失败',
111
+ };
57
112
  const processed = new Map();
58
113
  const linkBuffer = new Map();
59
114
  const logger = new koishi_1.Logger(exports.name);
60
115
  const PLATFORM_KEYWORDS = {
61
116
  bilibili: ['bilibili', 'b23', 'B站', 'www.bilibili.com', 'm.bilibili.com', '哔哩哔哩', 'bilibili.com/opus', 'bilibili.com/video', 'b23.tv', 't.bilibili.com', 'bilibili.com/bangumi'],
62
117
  kuaishou: ['kuaishou', '快手', 'v.kuaishou.com', 'www.kuaishou.com', 'kwimgs.com', 'kuaishou.com/app'],
63
- xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com', 'xiaohongshu.com/explore', 'xhslink.com/'],
118
+ xiaohongshu: ['xiaohongshu', '小红书', 'xhslink.com', 'xiaohongshu.com', 'xhscdn.com', 'xiaohongshu.com/explore', 'xhslink.com/', 'xiaohongshu.com/discovery/item'],
64
119
  weibo: ['weibo', '微博', 'weibo.com', 'video.weibo.com', 'm.weibo.cn', 'weibo.com/tv/show', 'weibo.com/feed'],
65
120
  toutiao: ['toutiao', '今日头条', 'm.toutiao.com', 'toutiao.com', 'ixigua.com', 'toutiao.com/video', 'ixigua.com/i'],
66
121
  pipigx: ['pipigx', '皮皮搞笑', 'h5.pipigx.com', 'ippzone.com', 'pipigx.com/share'],
@@ -79,9 +134,20 @@ const API_CONFIG = {
79
134
  pipixia: 'https://api.bugpk.com/api/ppx',
80
135
  zuiyou: 'https://api.bugpk.com/api/zuiyou'
81
136
  };
137
+ const PLATFORM_ERROR_CODE_MAP = {
138
+ douyin: ErrorCode.DOUYIN_PARSE_FAILED,
139
+ xiaohongshu: ErrorCode.XIAOHONGSHU_PARSE_FAILED,
140
+ bilibili: ErrorCode.BILIBILI_PARSE_FAILED,
141
+ kuaishou: ErrorCode.KUAISHOU_PARSE_FAILED,
142
+ weibo: ErrorCode.WEIBO_PARSE_FAILED,
143
+ toutiao: ErrorCode.API_RETURN_ERROR,
144
+ pipigx: ErrorCode.API_RETURN_ERROR,
145
+ pipixia: ErrorCode.API_RETURN_ERROR,
146
+ zuiyou: ErrorCode.API_RETURN_ERROR
147
+ };
82
148
  const VARIABLE_MAPPING = {
83
149
  '标题': ['title', 'Title', 'TITLE'],
84
- '作者': ['author', 'name', 'Author', 'Name'],
150
+ '作者': ['author.name', 'author', 'name', 'Author', 'Name'],
85
151
  '简介': ['desc', 'description', 'Desc', 'Description', 'content', 'Content'],
86
152
  '视频时长': ['duration', 'Duration', 'time', 'Time'],
87
153
  '点赞数': ['like', 'Like', 'attitudes_count', 'digg_count', 'praise'],
@@ -92,6 +158,10 @@ const VARIABLE_MAPPING = {
92
158
  '评论数': ['comment', 'Comment', 'comments_count', 'comment_count', 'discuss'],
93
159
  '音乐名': ['music.title', 'music_name', 'audio_name', 'sound_name']
94
160
  };
161
+ function getErrorInfo(code, detail) {
162
+ const baseMsg = exports.ErrorMessageMap[code] || exports.ErrorMessageMap[ErrorCode.UNKNOWN_ERROR];
163
+ return detail ? `[错误码: ${code}] ${baseMsg}:${detail}` : `[错误码: ${code}] ${baseMsg}`;
164
+ }
95
165
  function getErrorMessage(error) {
96
166
  if (error instanceof Error)
97
167
  return error.message;
@@ -156,11 +226,11 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
156
226
  const filePath = path_1.default.join(dir, `${filename}.mp4`);
157
227
  try {
158
228
  if (url.endsWith('.m4a') || url.endsWith('.mp3')) {
159
- throw new Error('不支持音频');
229
+ return { filePath: '', code: ErrorCode.UNSUPPORTED_CONTENT_TYPE };
160
230
  }
161
231
  const fileSize = await getFileSize(url, userAgent);
162
232
  if (maxSize > 0 && fileSize > maxSize) {
163
- throw new Error(`视频大小${fileSize}MB超过限制${maxSize}MB`);
233
+ return { filePath: '', code: ErrorCode.VIDEO_SIZE_EXCEEDED };
164
234
  }
165
235
  if (threads <= 0 || fileSize === 0) {
166
236
  const response = await (0, axios_1.default)({
@@ -174,7 +244,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
174
244
  });
175
245
  const writeStream = fs_1.default.createWriteStream(filePath);
176
246
  await (0, promises_1.pipeline)(response.data, writeStream);
177
- return filePath;
247
+ return { filePath, code: ErrorCode.SUCCESS };
178
248
  }
179
249
  const totalSize = fileSize * 1024 * 1024;
180
250
  const chunkSize = Math.ceil(totalSize / threads);
@@ -200,7 +270,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
200
270
  fs_1.default.unlinkSync(result.filePath);
201
271
  }
202
272
  writeStream.end();
203
- return filePath;
273
+ return { filePath, code: ErrorCode.SUCCESS };
204
274
  }
205
275
  catch (error) {
206
276
  if (fs_1.default.existsSync(filePath)) {
@@ -213,7 +283,7 @@ async function downloadVideo(url, filename, userAgent, maxSize, threads) {
213
283
  }
214
284
  catch (e) { }
215
285
  });
216
- throw error;
286
+ return { filePath: '', code: ErrorCode.VIDEO_DOWNLOAD_FAILED };
217
287
  }
218
288
  }
219
289
  function extractUrl(content) {
@@ -249,19 +319,41 @@ function getPlatformType(url) {
249
319
  return 'zuiyou';
250
320
  return null;
251
321
  }
322
+ function cleanUrl(url) {
323
+ try {
324
+ // 处理HTML实体编码
325
+ url = url.replace(/&amp;/g, '&');
326
+ const urlObj = new URL(url);
327
+ if (urlObj.hostname.includes('xiaohongshu.com')) {
328
+ urlObj.searchParams.delete('source');
329
+ urlObj.searchParams.delete('xhsshare');
330
+ urlObj.searchParams.delete('xsec_token');
331
+ urlObj.searchParams.delete('xsec_source');
332
+ return urlObj.origin + urlObj.pathname + urlObj.search;
333
+ }
334
+ if (urlObj.hostname.includes('douyin.com') || urlObj.hostname.includes('v.douyin.com')) {
335
+ return urlObj.origin + urlObj.pathname;
336
+ }
337
+ return url;
338
+ }
339
+ catch (e) {
340
+ // 处理HTML实体编码
341
+ return url.replace(/&amp;/g, '&');
342
+ }
343
+ }
252
344
  async function resolveShortUrl(url) {
253
345
  try {
254
346
  const res = await axios_1.default.head(url, {
255
- timeout: 5000,
256
- maxRedirects: 5,
347
+ timeout: 10000,
348
+ maxRedirects: 10,
257
349
  headers: {
258
350
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
259
351
  }
260
352
  });
261
- return res.request.res?.responseUrl || url;
353
+ return cleanUrl(res.request.res?.responseUrl || url);
262
354
  }
263
355
  catch (e) {
264
- return url;
356
+ return cleanUrl(url);
265
357
  }
266
358
  }
267
359
  function formatDuration(input) {
@@ -328,9 +420,9 @@ function parseData(rawResponse, maxDescLength, platform) {
328
420
  stat[varName] = value;
329
421
  }
330
422
  });
331
- let type = 'video';
423
+ let type = data.type || 'video';
332
424
  const title = findValueInObject(data, ['title']) || '无标题';
333
- const author = findValueInObject(data, ['author', 'name']) || '未知作者';
425
+ const author = findValueInObject(data, ['author.name', 'author']) || '未知作者';
334
426
  const desc = (findValueInObject(data, ['desc', 'description', 'content']) || title).toString().slice(0, maxDescLength);
335
427
  const cover = findValueInObject(data, ['cover', 'imgurl', 'pic', 'thumbnail']) || '';
336
428
  let images = findValueInObject(data, ['imgurl', 'images', 'pics']) || [];
@@ -344,24 +436,23 @@ function parseData(rawResponse, maxDescLength, platform) {
344
436
  findValueInObject(data, ['video_url'])
345
437
  ];
346
438
  let video = '';
347
- for (const url of videoUrls) {
348
- if (url && typeof url === 'string' && url.trim() !== '') {
349
- video = url;
350
- break;
439
+ if (Array.isArray(videoUrls[2])) {
440
+ video = videoUrls[2][0]?.url || '';
441
+ }
442
+ else {
443
+ for (const url of videoUrls) {
444
+ if (url && typeof url === 'string' && url.trim() !== '') {
445
+ video = url;
446
+ break;
447
+ }
351
448
  }
352
449
  }
353
450
  const durationValue = findValueInObject(data, ['duration']);
354
451
  const duration = typeof durationValue === 'number' ? durationValue : parseInt(durationValue) || 0;
355
452
  const durationFormatted = formatDuration(durationValue || 0);
356
- const dataType = findValueInObject(data, ['type']);
357
- if (dataType === 'image' || (images.length > 0 && !video)) {
358
- type = 'image';
359
- }
360
- if (video && (video.endsWith('.m4a') || video.endsWith('.mp3'))) {
361
- video = '';
362
- }
453
+ const live_photo = data.live_photo || [];
363
454
  return {
364
- type,
455
+ type: type,
365
456
  rawData: rawResponse,
366
457
  title,
367
458
  author,
@@ -371,7 +462,8 @@ function parseData(rawResponse, maxDescLength, platform) {
371
462
  video,
372
463
  duration,
373
464
  durationFormatted,
374
- stat
465
+ stat,
466
+ live_photo
375
467
  };
376
468
  }
377
469
  function generateFormattedText(platform, parseData, config) {
@@ -401,6 +493,9 @@ function generateFormattedText(platform, parseData, config) {
401
493
  }
402
494
  });
403
495
  result = validLines.join('\n').trim();
496
+ if (!result) {
497
+ result = `标题:${parseData.title}\n作者:${parseData.author}\n简介:${parseData.desc}`;
498
+ }
404
499
  return result;
405
500
  }
406
501
  function clearAllCache() {
@@ -443,58 +538,139 @@ function apply(ctx, config) {
443
538
  timeout: config.timeout,
444
539
  headers: { 'User-Agent': config.userAgent || 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' }
445
540
  });
541
+ async function parseWithRetry(url, platform, retryTimes) {
542
+ let lastError = null;
543
+ for (let i = 0; i <= retryTimes; i++) {
544
+ try {
545
+ const res = await http.get(API_CONFIG[platform], {
546
+ params: { url },
547
+ timeout: config.timeout
548
+ });
549
+ return res.data;
550
+ }
551
+ catch (error) {
552
+ lastError = error;
553
+ if (i < retryTimes) {
554
+ await delay(config.retryInterval);
555
+ }
556
+ }
557
+ }
558
+ throw lastError;
559
+ }
446
560
  async function parse(url) {
447
- const realUrl = await resolveShortUrl(url);
561
+ let realUrl = await resolveShortUrl(url);
562
+ realUrl = cleanUrl(realUrl);
448
563
  const platform = getPlatformType(realUrl);
449
564
  if (!platform) {
450
- logger.error(`不支持该平台: ${platform || '未知'}, URL: ${url}`);
451
- return { data: null, msg: '不支持该平台链接' };
565
+ const code = ErrorCode.UNSUPPORTED_PLATFORM;
566
+ const msg = getErrorInfo(code, url);
567
+ logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${url}`);
568
+ return { data: null, code, msg };
452
569
  }
453
570
  const apiUrl = API_CONFIG[platform];
454
571
  if (!apiUrl) {
455
- logger.error(`未配置该平台API: ${platform}, URL: ${url}`);
456
- return { data: null, msg: '该平台暂未配置解析接口' };
572
+ const code = ErrorCode.PLATFORM_API_NOT_CONFIGURED;
573
+ const msg = getErrorInfo(code, platform);
574
+ logger.error(`[${code}] ${exports.ErrorMessageMap[code]}: ${platform}`);
575
+ return { data: null, code, msg };
457
576
  }
458
577
  try {
459
- const res = await http.get(apiUrl, {
460
- params: { url: realUrl },
461
- timeout: config.timeout
462
- });
463
- if (res.data.code !== 200 && res.data.code !== 0) {
464
- logger.error(`API返回错误: ${platform}, URL: ${url}, 错误: ${res.data.msg || '未知错误'}`);
465
- return { data: null, msg: res.data.msg || '解析失败' };
578
+ const resData = await parseWithRetry(realUrl, platform, config.retryTimes);
579
+ // 正确的成功判断逻辑:code为200或0,或者msg包含"解析成功"
580
+ const isSuccess = resData.code === 200 || resData.code === 0 ||
581
+ (resData.msg && resData.msg.includes('解析成功'));
582
+ if (!isSuccess) {
583
+ const apiErrorMsg = resData.msg || '解析失败';
584
+ const platformCode = PLATFORM_ERROR_CODE_MAP[platform] || ErrorCode.API_RETURN_ERROR;
585
+ let detailedMsg = apiErrorMsg;
586
+ if (apiErrorMsg.includes('无法识别解析类型') || apiErrorMsg.includes('未找到有效内容')) {
587
+ detailedMsg = `链接格式不支持或内容已失效:${apiErrorMsg}`;
588
+ }
589
+ const code = platformCode;
590
+ const msg = getErrorInfo(code, detailedMsg);
591
+ logger.error(`[${code}] API返回错误: ${platform}, URL: ${url}, 错误: ${apiErrorMsg}`);
592
+ return { data: null, code, msg };
593
+ }
594
+ try {
595
+ const parseResult = parseData(resData, config.maxDescLength, platform);
596
+ // 修正内容判断逻辑:支持live类型和live_photo
597
+ const hasValidContent = parseResult.video ||
598
+ (parseResult.images && parseResult.images.length > 0) ||
599
+ (parseResult.live_photo && parseResult.live_photo.length > 0) ||
600
+ parseResult.type === 'live';
601
+ if (!hasValidContent) {
602
+ const code = ErrorCode.NO_VIDEO_FOUND;
603
+ const msg = getErrorInfo(code, '链接有效但未找到视频/图片内容(可能是直播、私密内容或已删除)');
604
+ logger.warn(`[${code}] 解析成功但无有效内容: ${platform}, URL: ${url}`);
605
+ return { data: null, code, msg };
606
+ }
607
+ logger.info(`[${ErrorCode.SUCCESS}] ${platform}解析成功: ${url}`);
608
+ return {
609
+ data: parseResult,
610
+ code: ErrorCode.SUCCESS,
611
+ msg: getErrorInfo(ErrorCode.SUCCESS)
612
+ };
613
+ }
614
+ catch (parseError) {
615
+ const errorMsg = getErrorMessage(parseError);
616
+ const code = ErrorCode.API_DATA_PARSE_FAILED;
617
+ const msg = getErrorInfo(code, errorMsg);
618
+ logger.error(`[${code}] 解析数据失败: ${platform}, URL: ${url}, 错误: ${errorMsg}`);
619
+ return { data: null, code, msg };
466
620
  }
467
- const parseResult = parseData(res.data, config.maxDescLength, platform);
468
- return { data: parseResult, msg: `${platform}解析成功` };
469
621
  }
470
622
  catch (error) {
471
- logger.error(`解析请求失败: ${platform}, URL: ${url}, 错误: ${getErrorMessage(error)}`);
472
- return { data: null, msg: '解析失败,请稍后重试' };
623
+ const errorMsg = getErrorMessage(error);
624
+ let code = ErrorCode.UNKNOWN_ERROR;
625
+ if (errorMsg.includes('timeout')) {
626
+ code = ErrorCode.REQUEST_TIMEOUT;
627
+ }
628
+ else if (errorMsg.includes('Network') || errorMsg.includes('network')) {
629
+ code = ErrorCode.NETWORK_ERROR;
630
+ }
631
+ else {
632
+ code = ErrorCode.NETWORK_ERROR;
633
+ }
634
+ const msg = getErrorInfo(code, errorMsg);
635
+ logger.error(`[${code}] 解析请求失败: ${platform}, URL: ${url}, 错误: ${errorMsg}`);
636
+ return { data: null, code, msg };
473
637
  }
474
638
  }
475
639
  async function processSingleUrl(session, url) {
476
640
  const hash = crypto_1.default.createHash('md5').update(url).digest('hex');
477
641
  const now = Date.now();
478
642
  if (processed.get(hash) && now - processed.get(hash) < config.sameLinkInterval * 1000) {
479
- return { data: null, msg: '请勿重复解析' };
643
+ const code = ErrorCode.DUPLICATE_PARSE;
644
+ const msg = getErrorInfo(code);
645
+ return { data: null, code, msg };
480
646
  }
481
647
  processed.set(hash, now);
482
648
  const result = await parse(url);
483
649
  if (!result.data)
484
- return { data: null, msg: result.msg };
650
+ return { data: null, code: result.code, msg: result.msg };
485
651
  const parseData = result.data;
486
652
  const platform = getPlatformType(url);
487
653
  const text = generateFormattedText(platform, parseData, config);
488
654
  return {
489
- data: { text, cover: parseData.cover, images: parseData.images, video: parseData.video, type: parseData.type },
490
- msg: 'ok'
655
+ data: {
656
+ text,
657
+ cover: parseData.cover,
658
+ images: parseData.images,
659
+ video: parseData.video,
660
+ type: parseData.type,
661
+ live_photo: parseData.live_photo
662
+ },
663
+ code: ErrorCode.SUCCESS,
664
+ msg: getErrorInfo(ErrorCode.SUCCESS)
491
665
  };
492
666
  }
493
667
  async function sendTimeout(session, content) {
494
668
  if (config.videoSendTimeout <= 0) {
495
669
  return session.send(content).catch((err) => {
670
+ const errorMsg = getErrorMessage(err);
671
+ logger.error(`[${ErrorCode.MESSAGE_SEND_FAILED}] 发送消息失败: ${errorMsg}`);
496
672
  if (!config.ignoreSendError)
497
- logger.error('发送消息失败:', err.message);
673
+ return null;
498
674
  return null;
499
675
  });
500
676
  }
@@ -502,8 +678,11 @@ function apply(ctx, config) {
502
678
  session.send(content),
503
679
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), config.videoSendTimeout))
504
680
  ]).catch((err) => {
681
+ const errorMsg = getErrorMessage(err);
682
+ const code = errorMsg.includes('timeout') ? ErrorCode.MESSAGE_SEND_TIMEOUT : ErrorCode.MESSAGE_SEND_FAILED;
683
+ logger.error(`[${code}] 发送消息失败: ${errorMsg}`);
505
684
  if (!config.ignoreSendError)
506
- logger.error('发送消息超时:', err.message);
685
+ return null;
507
686
  return null;
508
687
  });
509
688
  }
@@ -516,43 +695,32 @@ function apply(ctx, config) {
516
695
  linkBuffer.delete(key);
517
696
  }
518
697
  const items = [];
519
- const errs = [];
698
+ const errors = [];
520
699
  for (const url of urls) {
521
700
  const result = await processSingleUrl(session, url);
522
701
  if (result.data) {
523
702
  items.push(result.data);
524
703
  }
525
704
  else {
526
- errs.push(`【${url.slice(0, 22)}...】:${result.msg}`);
527
- logger.error(`解析失败: ${url}, 原因: ${result.msg}`);
705
+ errors.push({ url, code: result.code, msg: result.msg });
706
+ logger.error(`[${result.code}] 解析失败: ${url}, 原因: ${result.msg}`);
528
707
  }
529
708
  }
530
- if (errs.length) {
531
- logger.error(`解析失败数量: ${errs.length}, 示例: ${errs[0]}`);
532
- }
533
- const enableForward = config.enableForward && session.platform === 'onebot';
534
- const forwardMessages = [];
535
- const botName = config.botName || '视频解析机器人';
536
- if (errs.length) {
537
- const errorMsg = `⚠ 部分解析失败\n${errs.join('\n')}`;
538
- if (enableForward) {
539
- forwardMessages.push(buildForwardNode(session, errorMsg, botName));
540
- }
541
- else {
542
- await sendTimeout(session, errorMsg);
543
- await delay(600);
544
- }
709
+ if (errors.length > 0) {
710
+ const errorLines = errors.map(err => `【${err.url}】: ${err.msg}`);
711
+ const errorMsg = `❌ 解析失败列表(共${errors.length}个链接):\n${errorLines.join('\n')}`;
712
+ logger.error(`解析失败数量: ${errors.length}, 错误码列表: ${errors.map(e => e.code).join(', ')}`);
713
+ await sendTimeout(session, errorMsg);
714
+ await delay(500);
545
715
  }
546
716
  if (items.length === 0) {
547
- const failMsg = `❌ 全部解析失败\n${errs.join('\n')}`;
548
- if (enableForward) {
549
- forwardMessages.push(buildForwardNode(session, failMsg, botName));
550
- }
551
- else {
552
- await sendTimeout(session, failMsg);
553
- }
717
+ const failMsg = getErrorInfo(ErrorCode.UNKNOWN_ERROR, '所有链接均解析失败,请检查链接是否有效或稍后重试');
718
+ await sendTimeout(session, `⚠ ${failMsg}`);
554
719
  return;
555
720
  }
721
+ const enableForward = config.enableForward && session.platform === 'onebot';
722
+ const forwardMessages = [];
723
+ const botName = config.botName || '视频解析机器人';
556
724
  for (const item of items) {
557
725
  try {
558
726
  if (enableForward) {
@@ -565,28 +733,60 @@ function apply(ctx, config) {
565
733
  forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.images[i]), botName));
566
734
  }
567
735
  }
736
+ if (item.type === 'live' || item.type === 'live_photo') {
737
+ if (item.live_photo?.length) {
738
+ for (let i = 0; i < item.live_photo.length && forwardMessages.length < 100; i++) {
739
+ const liveItem = item.live_photo[i];
740
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(liveItem.image), botName));
741
+ if (liveItem.video) {
742
+ try {
743
+ const videoElem = koishi_1.h.video(liveItem.video);
744
+ forwardMessages.push(buildForwardNode(session, videoElem, botName));
745
+ }
746
+ catch (e) {
747
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`视频链接: ${liveItem.video}`), botName));
748
+ }
749
+ }
750
+ }
751
+ }
752
+ else if (item.images?.length) {
753
+ for (let i = 0; i < item.images.length && forwardMessages.length < 100; i++) {
754
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.image(item.images[i]), botName));
755
+ }
756
+ }
757
+ }
568
758
  if (item.video && config.showVideoFile && forwardMessages.length < 100) {
569
759
  let videoElem;
570
760
  try {
571
761
  if (config.downloadVideoBeforeSend) {
572
762
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
573
- const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
574
- videoElem = koishi_1.h.file(filePath);
763
+ const downloadResult = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
764
+ if (downloadResult.code !== ErrorCode.SUCCESS) {
765
+ const errorMsg = getErrorInfo(downloadResult.code);
766
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`${errorMsg}\n链接:${item.video}`), botName));
767
+ }
768
+ else {
769
+ videoElem = koishi_1.h.file(downloadResult.filePath);
770
+ forwardMessages.push(buildForwardNode(session, videoElem, botName));
771
+ }
575
772
  }
576
773
  else {
577
774
  const fileSize = await getFileSize(item.video, config.userAgent);
578
775
  if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
579
- videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
776
+ const code = ErrorCode.VIDEO_SIZE_EXCEEDED;
777
+ const errorMsg = getErrorInfo(code, `${fileSize}MB超过限制${config.maxVideoSize}MB`);
778
+ videoElem = koishi_1.h.text(`${errorMsg},仅发送链接:${item.video}`);
580
779
  }
581
780
  else {
582
781
  videoElem = koishi_1.h.video(item.video);
583
782
  }
584
783
  }
585
- forwardMessages.push(buildForwardNode(session, videoElem, botName));
586
784
  }
587
785
  catch (error) {
588
- logger.error(`视频处理失败: ${getErrorMessage(error)}`);
589
- forwardMessages.push(buildForwardNode(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`), botName));
786
+ const errorMsg = getErrorMessage(error);
787
+ const code = ErrorCode.VIDEO_DOWNLOAD_FAILED;
788
+ logger.error(`[${code}] 视频处理失败: ${errorMsg}`);
789
+ forwardMessages.push(buildForwardNode(session, koishi_1.h.text(getErrorInfo(code, errorMsg) + `\n链接:${item.video}`), botName));
590
790
  }
591
791
  }
592
792
  }
@@ -595,7 +795,28 @@ function apply(ctx, config) {
595
795
  await sendTimeout(session, item.text);
596
796
  await delay(300);
597
797
  }
598
- if (item.type === 'image' && item.images?.length) {
798
+ if (item.type === 'live' || item.type === 'live_photo') {
799
+ if (item.live_photo?.length) {
800
+ for (const liveItem of item.live_photo) {
801
+ await sendTimeout(session, koishi_1.h.image(liveItem.image));
802
+ await delay(200);
803
+ if (liveItem.video) {
804
+ try {
805
+ await sendTimeout(session, koishi_1.h.video(liveItem.video));
806
+ }
807
+ catch (e) {
808
+ await sendTimeout(session, koishi_1.h.text(`Live Photo 视频链接: ${liveItem.video}`));
809
+ }
810
+ await delay(300);
811
+ }
812
+ }
813
+ }
814
+ else if (item.images?.length) {
815
+ const imgMsg = (0, koishi_1.h)('message', ...item.images.map((url) => koishi_1.h.image(url)));
816
+ await sendTimeout(session, imgMsg);
817
+ }
818
+ }
819
+ else if (item.type === 'image' && item.images?.length) {
599
820
  const imgMsg = (0, koishi_1.h)('message', ...item.images.map((url) => koishi_1.h.image(url)));
600
821
  await sendTimeout(session, imgMsg);
601
822
  }
@@ -609,13 +830,22 @@ function apply(ctx, config) {
609
830
  let videoElem;
610
831
  if (config.downloadVideoBeforeSend) {
611
832
  const filename = crypto_1.default.createHash('md5').update(item.video).digest('hex');
612
- const filePath = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
613
- videoElem = koishi_1.h.file(filePath);
833
+ const downloadResult = await downloadVideo(item.video, filename, config.userAgent, config.maxVideoSize, config.downloadThreads);
834
+ if (downloadResult.code !== ErrorCode.SUCCESS) {
835
+ const errorMsg = getErrorInfo(downloadResult.code);
836
+ await sendTimeout(session, koishi_1.h.text(`${errorMsg}\n链接:${item.video}`));
837
+ continue;
838
+ }
839
+ else {
840
+ videoElem = koishi_1.h.file(downloadResult.filePath);
841
+ }
614
842
  }
615
843
  else {
616
844
  const fileSize = await getFileSize(item.video, config.userAgent);
617
845
  if (config.maxVideoSize > 0 && fileSize > config.maxVideoSize) {
618
- videoElem = koishi_1.h.text(`视频大小${fileSize}MB超过限制${config.maxVideoSize}MB,仅发送链接:${item.video}`);
846
+ const code = ErrorCode.VIDEO_SIZE_EXCEEDED;
847
+ const errorMsg = getErrorInfo(code, `${fileSize}MB超过限制${config.maxVideoSize}MB`);
848
+ videoElem = koishi_1.h.text(`${errorMsg},仅发送链接:${item.video}`);
619
849
  }
620
850
  else {
621
851
  videoElem = koishi_1.h.video(item.video);
@@ -624,8 +854,10 @@ function apply(ctx, config) {
624
854
  await sendTimeout(session, videoElem);
625
855
  }
626
856
  catch (error) {
627
- logger.error(`视频处理失败: ${getErrorMessage(error)}`);
628
- await sendTimeout(session, koishi_1.h.text(`视频处理失败:${getErrorMessage(error)}\n链接:${item.video}`));
857
+ const errorMsg = getErrorMessage(error);
858
+ const code = ErrorCode.VIDEO_DOWNLOAD_FAILED;
859
+ logger.error(`[${code}] 视频处理失败: ${errorMsg}`);
860
+ await sendTimeout(session, koishi_1.h.text(getErrorInfo(code, errorMsg) + `\n链接:${item.video}`));
629
861
  }
630
862
  }
631
863
  }
@@ -633,8 +865,10 @@ function apply(ctx, config) {
633
865
  }
634
866
  }
635
867
  catch (error) {
636
- logger.error(`处理内容失败: ${getErrorMessage(error)}`);
637
- await sendTimeout(session, `❌ 处理${item.type}内容失败: ${getErrorMessage(error)}`);
868
+ const errorMsg = getErrorMessage(error);
869
+ const code = ErrorCode.UNKNOWN_ERROR;
870
+ logger.error(`[${code}] 处理内容失败: ${errorMsg}`);
871
+ await sendTimeout(session, koishi_1.h.text(getErrorInfo(code, `处理${item.type}内容失败: ${errorMsg}`)));
638
872
  }
639
873
  }
640
874
  if (enableForward && forwardMessages.length) {
@@ -644,7 +878,9 @@ function apply(ctx, config) {
644
878
  await sendTimeout(session, forwardMsg);
645
879
  }
646
880
  catch (error) {
647
- logger.error(`合并转发失败: ${getErrorMessage(error)}`);
881
+ const errorMsg = getErrorMessage(error);
882
+ const code = ErrorCode.FORWARD_MESSAGE_FAILED;
883
+ logger.error(`[${code}] 合并转发失败: ${errorMsg}`);
648
884
  for (const node of forwardMessages) {
649
885
  await sendTimeout(session, node.data.content);
650
886
  await delay(500);
@@ -687,21 +923,25 @@ function apply(ctx, config) {
687
923
  });
688
924
  ctx.command('parse <url>', '手动解析视频链接')
689
925
  .action(async ({ session }, url) => {
690
- if (!url)
691
- return '请输入视频链接';
926
+ if (!url) {
927
+ const code = ErrorCode.INVALID_URL;
928
+ return getErrorInfo(code, '请输入视频链接');
929
+ }
692
930
  let urls = extractUrl(url);
693
931
  if (urls.length === 0 && hasPlatformKeyword(url)) {
694
932
  const allLinks = url.match(/https?:\/\/[^\s\"\'\>\]]+/gi) || [];
695
933
  urls = allLinks.filter((u) => getPlatformType(u));
696
934
  }
697
- if (urls.length === 0)
698
- return '不支持该链接';
935
+ if (urls.length === 0) {
936
+ const code = ErrorCode.UNSUPPORTED_PLATFORM;
937
+ return getErrorInfo(code, '不支持该链接');
938
+ }
699
939
  await flush(session, urls);
700
940
  });
701
941
  ctx.command('clear-cache', '清空解析缓存与临时文件')
702
942
  .action(() => {
703
943
  clearAllCache();
704
- return '解析缓存已清空';
944
+ return getErrorInfo(ErrorCode.SUCCESS, '解析缓存已清空');
705
945
  });
706
946
  setInterval(() => {
707
947
  const now = Date.now();
@@ -720,18 +960,24 @@ function apply(ctx, config) {
720
960
  const stat = fs_1.default.statSync(path_1.default.join(tempDir, file));
721
961
  if (now - stat.mtimeMs > 3600000) {
722
962
  fs_1.default.unlinkSync(path_1.default.join(tempDir, file));
963
+ logger.info(`清理过期临时文件: ${file}`);
723
964
  }
724
965
  }
725
966
  catch (error) {
726
- logger.error(`清理临时文件失败: ${file}, ${getErrorMessage(error)}`);
967
+ const errorMsg = getErrorMessage(error);
968
+ logger.error(`[${ErrorCode.UNKNOWN_ERROR}] 清理临时文件失败: ${file}, ${errorMsg}`);
727
969
  }
728
970
  });
729
971
  }, 1800000);
730
972
  if (config.autoClearCacheInterval > 0) {
731
973
  setInterval(() => {
732
974
  clearAllCache();
975
+ logger.info(getErrorInfo(ErrorCode.SUCCESS, '自动清理缓存完成'));
733
976
  }, config.autoClearCacheInterval * 60000);
734
977
  }
735
- process.on('exit', clearAllCache);
736
- logger.info('视频解析插件已加载');
978
+ process.on('exit', () => {
979
+ clearAllCache();
980
+ logger.info(getErrorInfo(ErrorCode.SUCCESS, '进程退出,已清理缓存'));
981
+ });
982
+ logger.info(getErrorInfo(ErrorCode.SUCCESS, '视频解析插件已加载'));
737
983
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-video-parser-all",
3
3
  "description": "Koishi 全平台视频解析插件,支持抖音/快手/B站/小红书/微博/今日头条/皮皮搞笑/皮皮虾/最右视频链接解析",
4
- "version": "0.5.2",
4
+ "version": "0.5.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/readme.md CHANGED
@@ -8,7 +8,6 @@
8
8
  - 🎨 自定义解析结果格式、返回内容类型(封面/链接/视频)
9
9
  - ⚡ 内置防重复解析、接口重试、自动缓存清理等实用功能
10
10
  - 📤 支持 OneBot 平台消息合并转发,优化展示体验
11
- - 🔌 内置多套解析 API,自动降级容错,提升解析成功率
12
11
 
13
12
  ### English
14
13
  This is a **video parsing plugin** developed for the Koishi bot framework, supporting automatic recognition and parsing of short video links from mainstream platforms such as Douyin, Kuaishou, Bilibili, Xiaohongshu, Weibo, Toutiao, Pipi Funny, Pipi Shrimp, and Zuiyou. Core features:
@@ -16,7 +15,6 @@ This is a **video parsing plugin** developed for the Koishi bot framework, suppo
16
15
  - 🎨 Customize the parsing result format and return content type (cover/link/video)
17
16
  - ⚡ Built-in duplicate prevention, retry logic, auto cache cleanup
18
17
  - 📤 Support OneBot message forwarding for better display experience
19
- - 🔌 Multiple built-in parsing APIs with automatic failover
20
18
 
21
19
  ## 项目仓库 (Repository)
22
20
  - GitHub: `https://github.com/Minecraft-1314/koishi-plugin-video-parser-all`
@@ -76,8 +74,6 @@ This is a **video parsing plugin** developed for the Koishi bot framework, suppo
76
74
  |----------------------|-------------------------|
77
75
  | Minecraft-1314 | 插件完整开发 (Complete plugin development) |
78
76
  | JH-Ahua | BugPk-Api 支持 |
79
- | 素颜API | 素颜API 支持 |
80
- | 星之阁API | 星之阁API 支持 |
81
77
  | (欢迎提交 PR 加入贡献者列表) | (Welcome to submit PR to join the contributor list) |
82
78
 
83
79
  ## 许可协议 (License)