koishi-plugin-share-links-analysis 0.13.2 → 0.14.0
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/LICENSE +21 -21
- package/README.md +71 -71
- package/lib/index.js +121 -6
- package/lib/parsers/twitter.js +15 -5
- package/lib/parsers/xiaoheihe.js +2 -2
- package/lib/utils.js +1 -1
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 shangxue
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 shangxue
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,71 +1,71 @@
|
|
|
1
|
-
# koishi-plugin-share-links-analysis
|
|
2
|
-
|
|
3
|
-
# 视频链接解析插件说明 📺✨
|
|
4
|
-
|
|
5
|
-
本插件为用户提供便捷的分享链接链接解析服务,让聊天体验更加丰富多彩。
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 鸣谢 💖
|
|
10
|
-
|
|
11
|
-
本插件的解析能力与稳定运行,离不开以下开源项目及社区文档的强大支撑。特此向这些项目及其维护者致以最诚挚的感谢:
|
|
12
|
-
|
|
13
|
-
- [koishi-plugin-bilibili-videolink-analysis](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/bilibili-videolink-analysis)
|
|
14
|
-
- [koishi-plugin-bili-parser](https://github.com/summonhim/koishi-plugin-bili-parser)
|
|
15
|
-
- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
|
|
16
|
-
- [koishi-plugin-xiaohongshu](https://www.npmjs.com/package/koishi-plugin-xiaohongshu)
|
|
17
|
-
以及解析方式原作者:[@MuJie](https://mu-jie.cc/)
|
|
18
|
-
- [BetterTwitFix](https://github.com/dylanpdx/BetterTwitFix)
|
|
19
|
-
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
|
20
|
-
- [NeteaseCloudMusicApiEnhanced](https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced)
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## 🚀 YouTube 解析后端部署指南 (必看)
|
|
25
|
-
|
|
26
|
-
由于 YouTube 引入了极其严苛的反爬机制,传统的直连解析已全部失效。为了保证解析的稳定,本插件采用了 **“前端 Koishi + 后端 Python 微服务”** 的分离架构。
|
|
27
|
-
|
|
28
|
-
Python 后端利用 `yt-dlp` 的最新特性,通过调用本地 Node.js 实时计算 YouTube 签名,并已内置自动热更新。
|
|
29
|
-
|
|
30
|
-
### 部署环境要求
|
|
31
|
-
- **Python 3.8+**
|
|
32
|
-
- **Node.js** (⚠️ 必须安装!`yt-dlp` 需要调用 Node 环境来执行 JS 脚本破解签名)
|
|
33
|
-
- **一个能够正常访问 YouTube 的网络代理** (强烈建议使用流媒体解锁节点)
|
|
34
|
-
|
|
35
|
-
### 部署步骤
|
|
36
|
-
|
|
37
|
-
**1. 下载后端脚本**
|
|
38
|
-
将本仓库内的 `yt_server.py` 下载到你的服务器上:
|
|
39
|
-
```bash
|
|
40
|
-
wget https://raw.githubusercontent.com/furryaxw/share-links-analysis/Master/yt_server.py
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**2. 安装 Python 依赖**
|
|
44
|
-
```bash
|
|
45
|
-
pip install httpx fastapi uvicorn yt-dlp
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
**3. 配置网络代理 (重要)**
|
|
49
|
-
请使用编辑器(如 vim 或 nano)打开 `yt_server.py`,在文件顶部找到代理配置区域,**将地址修改为你自己的真实代理**:
|
|
50
|
-
```python
|
|
51
|
-
# =====================================
|
|
52
|
-
# 配置代理
|
|
53
|
-
proxy = "http://127.0.0.1:7890" # 👈 在这里填入你的代理IP和端口
|
|
54
|
-
# =====================================
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**4. 启动微服务**
|
|
58
|
-
```bash
|
|
59
|
-
python yt_server.py
|
|
60
|
-
```
|
|
61
|
-
*(注:由于脚本内置了自动更新热重启逻辑,强烈建议直接使用上述命令启动,或者使用 `pm2 start yt_server.py --name yt-api --interpreter python3` 进行后台进程守护。)*
|
|
62
|
-
|
|
63
|
-
**5. 在 Koishi 中配置**
|
|
64
|
-
确保 Python 服务成功运行(默认监听 `12001` 端口)后,回到 Koishi 控制台的本插件配置页。
|
|
65
|
-
在 `youtube_pythonApiUrl` 配置项中填入你的后端地址:
|
|
66
|
-
```text
|
|
67
|
-
http://127.0.0.1:12001/api/parse
|
|
68
|
-
```
|
|
69
|
-
*(如果你的 Python 服务部署在其他服务器上,请将 `127.0.0.1` 替换为对应的服务器 IP)*
|
|
70
|
-
|
|
71
|
-
🎉 配置完成!现在你可以尽情享受丝滑的 YouTube 视频与 Shorts 解析了!
|
|
1
|
+
# koishi-plugin-share-links-analysis
|
|
2
|
+
|
|
3
|
+
# 视频链接解析插件说明 📺✨
|
|
4
|
+
|
|
5
|
+
本插件为用户提供便捷的分享链接链接解析服务,让聊天体验更加丰富多彩。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 鸣谢 💖
|
|
10
|
+
|
|
11
|
+
本插件的解析能力与稳定运行,离不开以下开源项目及社区文档的强大支撑。特此向这些项目及其维护者致以最诚挚的感谢:
|
|
12
|
+
|
|
13
|
+
- [koishi-plugin-bilibili-videolink-analysis](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/bilibili-videolink-analysis)
|
|
14
|
+
- [koishi-plugin-bili-parser](https://github.com/summonhim/koishi-plugin-bili-parser)
|
|
15
|
+
- [bilibili-API-collect](https://github.com/SocialSisterYi/bilibili-API-collect)
|
|
16
|
+
- [koishi-plugin-xiaohongshu](https://www.npmjs.com/package/koishi-plugin-xiaohongshu)
|
|
17
|
+
以及解析方式原作者:[@MuJie](https://mu-jie.cc/)
|
|
18
|
+
- [BetterTwitFix](https://github.com/dylanpdx/BetterTwitFix)
|
|
19
|
+
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
|
20
|
+
- [NeteaseCloudMusicApiEnhanced](https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 🚀 YouTube 解析后端部署指南 (必看)
|
|
25
|
+
|
|
26
|
+
由于 YouTube 引入了极其严苛的反爬机制,传统的直连解析已全部失效。为了保证解析的稳定,本插件采用了 **“前端 Koishi + 后端 Python 微服务”** 的分离架构。
|
|
27
|
+
|
|
28
|
+
Python 后端利用 `yt-dlp` 的最新特性,通过调用本地 Node.js 实时计算 YouTube 签名,并已内置自动热更新。
|
|
29
|
+
|
|
30
|
+
### 部署环境要求
|
|
31
|
+
- **Python 3.8+**
|
|
32
|
+
- **Node.js** (⚠️ 必须安装!`yt-dlp` 需要调用 Node 环境来执行 JS 脚本破解签名)
|
|
33
|
+
- **一个能够正常访问 YouTube 的网络代理** (强烈建议使用流媒体解锁节点)
|
|
34
|
+
|
|
35
|
+
### 部署步骤
|
|
36
|
+
|
|
37
|
+
**1. 下载后端脚本**
|
|
38
|
+
将本仓库内的 `yt_server.py` 下载到你的服务器上:
|
|
39
|
+
```bash
|
|
40
|
+
wget https://raw.githubusercontent.com/furryaxw/share-links-analysis/Master/yt_server.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. 安装 Python 依赖**
|
|
44
|
+
```bash
|
|
45
|
+
pip install httpx fastapi uvicorn yt-dlp
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**3. 配置网络代理 (重要)**
|
|
49
|
+
请使用编辑器(如 vim 或 nano)打开 `yt_server.py`,在文件顶部找到代理配置区域,**将地址修改为你自己的真实代理**:
|
|
50
|
+
```python
|
|
51
|
+
# =====================================
|
|
52
|
+
# 配置代理
|
|
53
|
+
proxy = "http://127.0.0.1:7890" # 👈 在这里填入你的代理IP和端口
|
|
54
|
+
# =====================================
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**4. 启动微服务**
|
|
58
|
+
```bash
|
|
59
|
+
python yt_server.py
|
|
60
|
+
```
|
|
61
|
+
*(注:由于脚本内置了自动更新热重启逻辑,强烈建议直接使用上述命令启动,或者使用 `pm2 start yt_server.py --name yt-api --interpreter python3` 进行后台进程守护。)*
|
|
62
|
+
|
|
63
|
+
**5. 在 Koishi 中配置**
|
|
64
|
+
确保 Python 服务成功运行(默认监听 `12001` 端口)后,回到 Koishi 控制台的本插件配置页。
|
|
65
|
+
在 `youtube_pythonApiUrl` 配置项中填入你的后端地址:
|
|
66
|
+
```text
|
|
67
|
+
http://127.0.0.1:12001/api/parse
|
|
68
|
+
```
|
|
69
|
+
*(如果你的 Python 服务部署在其他服务器上,请将 `127.0.0.1` 替换为对应的服务器 IP)*
|
|
70
|
+
|
|
71
|
+
🎉 配置完成!现在你可以尽情享受丝滑的 YouTube 视频与 Shorts 解析了!
|
package/lib/index.js
CHANGED
|
@@ -301,6 +301,98 @@ function apply(ctx, config) {
|
|
|
301
301
|
await ctx.database.remove('sla_file_cache', {});
|
|
302
302
|
return '缓存及对应文件已清理。';
|
|
303
303
|
});
|
|
304
|
+
cmd.subcommand('.checkcache <url:string>', '查看缓存数据状态', { authority: 2 })
|
|
305
|
+
.action(async ({ session }, url) => {
|
|
306
|
+
if (!session)
|
|
307
|
+
return '会话不可用。';
|
|
308
|
+
if (!config.enableCache)
|
|
309
|
+
return '缓存功能未启用。';
|
|
310
|
+
const links = await (0, core_1.resolveLinks)(url, ctx, config);
|
|
311
|
+
if (links.length === 0)
|
|
312
|
+
return '未在该链接中识别到支持的内容。';
|
|
313
|
+
const link = links[0];
|
|
314
|
+
const cacheKey = `${link.platform}:${link.id}`;
|
|
315
|
+
const cached = await ctx.database.get('sla_parse_cache', cacheKey);
|
|
316
|
+
if (cached.length === 0)
|
|
317
|
+
return `未找到缓存数据: ${cacheKey}`;
|
|
318
|
+
const entry = cached[0];
|
|
319
|
+
const ageMs = Date.now() - entry.created_at;
|
|
320
|
+
const isExpiredL1 = config.cacheExpiration > 0 && (ageMs > config.cacheExpiration * 60 * 60 * 1000);
|
|
321
|
+
const isExpiredL2 = config.optimisticExpiration > 0 && (ageMs > config.optimisticExpiration * 60 * 60 * 1000);
|
|
322
|
+
if (isExpiredL2)
|
|
323
|
+
return `缓存数据已完全过期: ${cacheKey}`;
|
|
324
|
+
const cacheTimeStr = new Date(entry.created_at).toLocaleString('zh-CN', { hour12: false });
|
|
325
|
+
const result = { ...entry.data };
|
|
326
|
+
if (isExpiredL1 && config.optimisticCache) {
|
|
327
|
+
result.mainbody = (result.mainbody || '') + `\n\n[📦 L2 缓存 | 缓存时间: ${cacheTimeStr}]`;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
result.mainbody = (result.mainbody || '') + `\n\n[📦 L1 缓存 | 缓存时间: ${cacheTimeStr}]`;
|
|
331
|
+
}
|
|
332
|
+
const sendStats = { downloadTime: 0, sendTime: 0, errors: [] };
|
|
333
|
+
await (0, utils_1.sendResult)(ctx, session, config, result, logger, sendStats);
|
|
334
|
+
return;
|
|
335
|
+
});
|
|
336
|
+
cmd.subcommand('.forceparse <url:string>', '忽略缓存强制解析链接', { authority: 2 })
|
|
337
|
+
.action(async ({ session }, url) => {
|
|
338
|
+
if (!session)
|
|
339
|
+
return '会话不可用。';
|
|
340
|
+
const links = await (0, core_1.resolveLinks)(url, ctx, config);
|
|
341
|
+
if (links.length === 0)
|
|
342
|
+
return '未在该链接中识别到支持的内容。';
|
|
343
|
+
const link = links[0];
|
|
344
|
+
if (config.waitTip_Switch)
|
|
345
|
+
await session.send(config.waitTip_Switch);
|
|
346
|
+
const result = await (0, core_1.processLink)(ctx, config, link, session);
|
|
347
|
+
if (!result)
|
|
348
|
+
return `解析失败: ${link.platform}/${link.type}:${link.id}`;
|
|
349
|
+
const cacheKey = `${link.platform}:${link.id}`;
|
|
350
|
+
if (config.enableCache) {
|
|
351
|
+
await ctx.database.upsert('sla_parse_cache', [{
|
|
352
|
+
key: cacheKey,
|
|
353
|
+
data: result,
|
|
354
|
+
created_at: Date.now()
|
|
355
|
+
}]);
|
|
356
|
+
}
|
|
357
|
+
const sendStats = { downloadTime: 0, sendTime: 0, errors: [] };
|
|
358
|
+
await (0, utils_1.sendResult)(ctx, session, config, result, logger, sendStats);
|
|
359
|
+
return;
|
|
360
|
+
});
|
|
361
|
+
cmd.subcommand('.directlink <url:string> [quality:string]', '获取视频/音频直链', { authority: 1 })
|
|
362
|
+
.action(async ({ session }, url, quality) => {
|
|
363
|
+
if (!session)
|
|
364
|
+
return '会话不可用。';
|
|
365
|
+
const links = await (0, core_1.resolveLinks)(url, ctx, config);
|
|
366
|
+
if (links.length === 0)
|
|
367
|
+
return '未在该链接中识别到支持的内容。';
|
|
368
|
+
const link = links[0];
|
|
369
|
+
let clarity = config.Video_ClarityPriority;
|
|
370
|
+
if (quality) {
|
|
371
|
+
const q = quality.trim().toLowerCase();
|
|
372
|
+
if (q === 'high' || q === 'h' || q === '高' || q === '高清晰度') {
|
|
373
|
+
clarity = '2';
|
|
374
|
+
}
|
|
375
|
+
else if (q === 'low' || q === 'l' || q === '低' || q === '低清晰度') {
|
|
376
|
+
clarity = '1';
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
return `无效的画质参数: ${quality}。可用: high/h/高清晰度, low/l/低清晰度`;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const modifiedConfig = { ...config, Video_ClarityPriority: clarity, Max_size: Number.MAX_SAFE_INTEGER };
|
|
383
|
+
const result = await (0, core_1.processLink)(ctx, modifiedConfig, link, session);
|
|
384
|
+
if (!result)
|
|
385
|
+
return `解析失败: ${link.platform}/${link.type}:${link.id}`;
|
|
386
|
+
if (result.files.length === 0) {
|
|
387
|
+
return `未获取到直链: ${link.platform}/${link.type}:${link.id}\n该内容类型可能不支持直链获取。`;
|
|
388
|
+
}
|
|
389
|
+
const clarityLabel = clarity === '2' ? '高清晰度优先' : '低清晰度优先';
|
|
390
|
+
let output = `直链结果 (${link.platform}/${link.type}:${link.id}):\n画质策略: ${clarityLabel}\n标题: ${result.title}\n`;
|
|
391
|
+
for (const [i, file] of result.files.entries()) {
|
|
392
|
+
output += `\n[${i + 1}] ${file.type}: ${file.url}`;
|
|
393
|
+
}
|
|
394
|
+
return output;
|
|
395
|
+
});
|
|
304
396
|
cmd.subcommand('.refresh', '强制刷新所有 Cookie', { authority: 3 })
|
|
305
397
|
.action(async ({ session }) => {
|
|
306
398
|
await session?.send('🔄 开始执行 Cookie 全量刷新流程...');
|
|
@@ -530,9 +622,15 @@ function apply(ctx, config) {
|
|
|
530
622
|
}
|
|
531
623
|
}
|
|
532
624
|
catch (e) {
|
|
625
|
+
let mergeErrDetail = e.message || String(e);
|
|
626
|
+
if (e.cause) {
|
|
627
|
+
const causeCode = e.cause.code || '';
|
|
628
|
+
const causeMsg = e.cause.message || String(e.cause);
|
|
629
|
+
mergeErrDetail += ` [cause: ${causeCode ? causeCode + ': ' : ''}${causeMsg}]`;
|
|
630
|
+
}
|
|
533
631
|
// 其他合并请求抛错时触发回退
|
|
534
632
|
if (optimisticData) {
|
|
535
|
-
logger.warn(`合并任务失败,触发乐观缓存回退: ${cacheKey}`);
|
|
633
|
+
logger.warn(`合并任务失败,触发乐观缓存回退: ${cacheKey} | Error: ${mergeErrDetail}`);
|
|
536
634
|
result = optimisticData;
|
|
537
635
|
const cacheTimeStr = new Date(optimisticTime).toLocaleString('zh-CN', { hour12: false });
|
|
538
636
|
result.mainbody = (result.mainbody || '') + `\n\n[⚠️ 并发请求异常,回退 L2 乐观缓存 | 缓存时间: ${cacheTimeStr}]`;
|
|
@@ -573,15 +671,22 @@ function apply(ctx, config) {
|
|
|
573
671
|
return res;
|
|
574
672
|
}
|
|
575
673
|
catch (e) {
|
|
674
|
+
// 构建包含根因的详细错误信息
|
|
675
|
+
let errDetail = e.message || String(e);
|
|
676
|
+
if (e.cause) {
|
|
677
|
+
const causeCode = e.cause.code || '';
|
|
678
|
+
const causeMsg = e.cause.message || String(e.cause);
|
|
679
|
+
errDetail += ` [cause: ${causeCode ? causeCode + ': ' : ''}${causeMsg}]`;
|
|
680
|
+
}
|
|
576
681
|
// 抛出异常(网络错误/封禁)时触发乐观回退
|
|
577
682
|
if (optimisticData) {
|
|
578
|
-
logger.warn(`解析抛出异常,触发乐观缓存回退: ${cacheKey} | Error: ${
|
|
683
|
+
logger.warn(`解析抛出异常,触发乐观缓存回退: ${cacheKey} | Error: ${errDetail}`);
|
|
579
684
|
const cacheTimeStr = new Date(optimisticTime).toLocaleString('zh-CN', { hour12: false });
|
|
580
685
|
optimisticData.mainbody = (optimisticData.mainbody || '') + `\n\n[⚠️ 接口触发异常,回退 L2 乐观缓存 | 缓存时间: ${cacheTimeStr}]`;
|
|
581
686
|
optimisticData._isOptimisticFallback = true;
|
|
582
687
|
return optimisticData;
|
|
583
688
|
}
|
|
584
|
-
logger.warn(`解析任务出错: ${
|
|
689
|
+
logger.warn(`解析任务出错: ${errDetail}`);
|
|
585
690
|
throw e;
|
|
586
691
|
}
|
|
587
692
|
finally {
|
|
@@ -619,9 +724,19 @@ function apply(ctx, config) {
|
|
|
619
724
|
}
|
|
620
725
|
catch (e) {
|
|
621
726
|
status = "error";
|
|
622
|
-
|
|
727
|
+
// 构建包含根因的详细错误信息
|
|
728
|
+
let details = e.message || String(e);
|
|
729
|
+
if (e.cause) {
|
|
730
|
+
const causeCode = e.cause.code || '';
|
|
731
|
+
const causeMsg = e.cause.message || String(e.cause);
|
|
732
|
+
details += ` [cause: ${causeCode ? causeCode + ': ' : ''}${causeMsg}]`;
|
|
733
|
+
}
|
|
734
|
+
if (e.response?.status) {
|
|
735
|
+
details += ` [HTTP ${e.response.status}]`;
|
|
736
|
+
}
|
|
737
|
+
errorMsg = details;
|
|
623
738
|
errorStack = e.stack || String(e);
|
|
624
|
-
logger.warn(`处理异常: ${
|
|
739
|
+
logger.warn(`处理异常: ${details}`);
|
|
625
740
|
}
|
|
626
741
|
finally {
|
|
627
742
|
// 4. 上报针对性排障数据
|
|
@@ -665,7 +780,7 @@ function apply(ctx, config) {
|
|
|
665
780
|
...resultFeatures,
|
|
666
781
|
// === 6. 错误追踪 ===
|
|
667
782
|
error_msg: errorMsg,
|
|
668
|
-
error_stack:
|
|
783
|
+
error_stack: truncatedStack,
|
|
669
784
|
// === 7. 耗时拆解 (性能瓶颈定位) ===
|
|
670
785
|
time_total_ms: totalTime,
|
|
671
786
|
time_parse_ms: parseTime, // 解析器发请求拉取数据的耗时
|
package/lib/parsers/twitter.js
CHANGED
|
@@ -238,7 +238,9 @@ async function handleSyndicationFallback(ctx, config, tweetId, session) {
|
|
|
238
238
|
const reqOptions = {
|
|
239
239
|
headers: {
|
|
240
240
|
'User-Agent': config.userAgent,
|
|
241
|
-
'Accept': '*/*'
|
|
241
|
+
'Accept': '*/*',
|
|
242
|
+
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
243
|
+
'Referer': 'https://platform.twitter.com/'
|
|
242
244
|
}
|
|
243
245
|
};
|
|
244
246
|
if (config.proxy)
|
|
@@ -298,7 +300,10 @@ async function handleSyndicationFallback(ctx, config, tweetId, session) {
|
|
|
298
300
|
};
|
|
299
301
|
}
|
|
300
302
|
catch (e) {
|
|
301
|
-
|
|
303
|
+
const causeDetail = e.cause ? ` (cause: ${e.cause.code || e.cause.message || e.cause})` : '';
|
|
304
|
+
logger.error(`Syndication fallback failed: ${e.message}${causeDetail}`);
|
|
305
|
+
if (e.response?.status)
|
|
306
|
+
logger.error(` HTTP status: ${e.response.status}`);
|
|
302
307
|
// 抛出错误让外部统一处理 404 等信息
|
|
303
308
|
throw e;
|
|
304
309
|
}
|
|
@@ -372,16 +377,21 @@ async function process(ctx, config, link, session) {
|
|
|
372
377
|
// 404 通常意味着推文真的没了
|
|
373
378
|
const isNotFound = error.response?.status === 404;
|
|
374
379
|
if (!isNotFound) {
|
|
375
|
-
|
|
380
|
+
const causeDetail = error.cause ? ` (cause: ${error.cause.code || error.cause.message || error.cause})` : '';
|
|
381
|
+
logger.warn(`VxTwitter API 失败 (${error.message}${causeDetail}),尝试 Syndication API Fallback...`);
|
|
376
382
|
try {
|
|
377
383
|
return await handleSyndicationFallback(ctx, config, tweetId, session);
|
|
378
384
|
}
|
|
379
385
|
catch (fallbackError) {
|
|
380
|
-
|
|
386
|
+
const fbCauseDetail = fallbackError.cause ? ` (cause: ${fallbackError.cause.code || fallbackError.cause.message || fallbackError.cause})` : '';
|
|
387
|
+
logger.error(`Fallback 失败: ${fallbackError.message}${fbCauseDetail}`);
|
|
388
|
+
logger.error(`Twitter 解析失败: 主 API 和 Fallback 均失败`);
|
|
381
389
|
}
|
|
382
390
|
}
|
|
391
|
+
else {
|
|
392
|
+
logger.error(`Twitter 解析失败: ${error.message}`);
|
|
393
|
+
}
|
|
383
394
|
// 最终错误处理
|
|
384
|
-
logger.error(`Twitter 解析失败: ${error.message}`);
|
|
385
395
|
if (error.response?.status === 404) {
|
|
386
396
|
await session.send('推文不存在或已被删除');
|
|
387
397
|
}
|
package/lib/parsers/xiaoheihe.js
CHANGED
|
@@ -9,11 +9,11 @@ const utils_1 = require("../utils");
|
|
|
9
9
|
exports.name = "xiaoheihe";
|
|
10
10
|
const linkRules = [
|
|
11
11
|
{
|
|
12
|
-
pattern: /https?:\/\/api\.xiaoheihe\.cn\/v3\/bbs\/app\/api\/web\/share\?[^"'\s]*?\blink_id=(
|
|
12
|
+
pattern: /https?:\/\/api\.xiaoheihe\.cn\/v3\/bbs\/app\/api\/web\/share\?[^"'\s]*?\blink_id=([a-zA-Z0-9]+)/gi,
|
|
13
13
|
type: "bbs_api",
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
|
-
pattern: /https?:\/\/www\.xiaoheihe\.cn\/app\/bbs\/link\/(
|
|
16
|
+
pattern: /https?:\/\/www\.xiaoheihe\.cn\/app\/bbs\/link\/([a-zA-Z0-9]+)/gi,
|
|
17
17
|
type: "bbs",
|
|
18
18
|
}
|
|
19
19
|
];
|
package/lib/utils.js
CHANGED
|
@@ -641,7 +641,7 @@ async function isUserAdmin(session, userId) {
|
|
|
641
641
|
if (!memberInfo)
|
|
642
642
|
return false;
|
|
643
643
|
const adminRoles = ["owner", "admin", "administrator"];
|
|
644
|
-
const memberRoles =
|
|
644
|
+
const memberRoles = (memberInfo.roles || []).map(r => r.name).filter((n) => !!n);
|
|
645
645
|
for (const role of memberRoles) {
|
|
646
646
|
if (adminRoles.includes(role.toLowerCase()))
|
|
647
647
|
return true;
|