koishi-plugin-kkk 3.9.1 → 3.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/lib/build-metadata.json +5 -5
- package/lib/karin/apps/tools.js +19 -3
- package/lib/karin/module/utils/QqPanel.js +196 -8
- package/lib/karin/platform/kuaishou/kuaishou.js +33 -2
- package/lib/karin/platform/xiaohongshu/xiaohongshu.js +25 -4
- package/package.json +1 -1
- package/src/karin/apps/tools.ts +19 -3
- package/src/karin/module/utils/QqPanel.ts +206 -6
- package/src/karin/platform/kuaishou/kuaishou.ts +33 -2
- package/src/karin/platform/xiaohongshu/xiaohongshu.ts +26 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
## 3.10.1
|
|
4
|
+
|
|
5
|
+
> B站的复制跳转修一下:3.10.0 改成用用户发的那条链接后,B站 App 复制了反而不跳。
|
|
6
|
+
|
|
7
|
+
### B站跳转链接改回标准视频页
|
|
8
|
+
|
|
9
|
+
- 3.10.0 让跳转块统一用用户发出来的那条链接,B站照做之后反而不能用:
|
|
10
|
+
用户发的多半是 `b23.tv/xxx` 短链,**B站 App 复制了不跳转**。
|
|
11
|
+
- B站的跳转块固定用 `https://www.bilibili.com/video/<bvid>`(分 P 带 `?p=N`),
|
|
12
|
+
不再用用户发的那条。卡片右下角的二维码同步改成这条,两边一致。
|
|
13
|
+
- 抖音、快手、小红书仍按 3.10.0 的规则:用用户原样发的那条短链。
|
|
14
|
+
|
|
15
|
+
## 3.10.0
|
|
16
|
+
|
|
17
|
+
> 解析消息底部的「打开原站」链接换成了**代码块形式的跳转卡片**:
|
|
18
|
+
> 整段复制后打开对应 App 会直接跳到那条作品,不再依赖 QQ 会不会把链接渲染成可点的东西。
|
|
19
|
+
> 快手和小红书以前完全没有这条链接,现在补上了。
|
|
20
|
+
|
|
21
|
+
### 原站跳转改成「复制码」
|
|
22
|
+
|
|
23
|
+
- 发出的东西从 `[打开原站](链接)` 变成代码块:
|
|
24
|
+
|
|
25
|
+
````
|
|
26
|
+
```复制后打开抖音自动跳转 →
|
|
27
|
+
https://v.douyin.com/iQabcdef/ :1pm Mai:/ 2237886846@qq.com
|
|
28
|
+
```
|
|
29
|
+
````
|
|
30
|
+
|
|
31
|
+
各家 App 都认「剪贴板里有一条自家链接」——复制整段文本打开 App 会弹直达询问,
|
|
32
|
+
比点链接稳;官方 QQ 对 markdown 里的链接也经常直接拒收整条消息,代码块没有这个问题。
|
|
33
|
+
- 提示文案放在**围栏的语种位**(` ``` ` 后面那截),不占正文一行,
|
|
34
|
+
代码块里就只有链接本身,全选复制不会带上多余文字。
|
|
35
|
+
- **抖音附带唤起口令**:光有链接 App 不一定弹跳转,接一段口令才稳。
|
|
36
|
+
这是抖音 App 生成的固定串(每条作品都一样),不是模板。
|
|
37
|
+
- **快手、小红书单独发一条**:这两个平台没有清晰度面板——原来的跳转代码挂在画质表格下面,
|
|
38
|
+
它们压根走不到,等于一条都没发过。现在在解析内容发完后补一条同格式的消息。
|
|
39
|
+
- 开关沿用 WebUI 的「原站跳转」(QQ 适配器分组,`qqPanelSourceLink`,默认开);
|
|
40
|
+
跳转发送失败只记日志,**不影响解析结果**。
|
|
41
|
+
|
|
42
|
+
### 跳转链接用用户发的那条
|
|
43
|
+
|
|
44
|
+
- 一开始让跳转链接和卡片右下角的二维码保持一致(`b23.tv/<bvid>`、抖音 `share_url`),
|
|
45
|
+
实测行不通:那种链接带 `xsec_token` / `u_code` 一串参数,**太长,App 剪贴板识别不了**。
|
|
46
|
+
- 改成优先用用户原样发的那条(`b23.tv/xxx`、`v.douyin.com/xxx`、`v.kuaishou.com/xxx`、
|
|
47
|
+
`xhslink.com/o/xxx`)——那本来就是各家 App 自己生成的分享形态,最短也最容易被识别。
|
|
48
|
+
取不到时才退回插件拼的标准链接。
|
|
49
|
+
- 卡片二维码**没动**:扫码不吃 URL 长度,换个链接反而要重测,保持原样。
|
|
50
|
+
- 顺带修了快手卡片二维码:以前填的是视频 CDN 直链,扫出来是段播放地址而不是作品页,
|
|
51
|
+
现在指向作品页(`photoId` 取不到时仍退回原逻辑)。
|
|
52
|
+
|
|
53
|
+
### markdown 只对 QQ / QQ 频道发
|
|
54
|
+
|
|
55
|
+
- 能渲染 markdown 的只有官方 QQ 适配器和 QQ 频道,其它平台(OneBot / Discord / Telegram /
|
|
56
|
+
KOOK…)会把代码块的 ` ``` ` 围栏原样显示成一串反引号。
|
|
57
|
+
- 判定从 OneBot 黑名单改成**平台白名单**(`qq` / `qqguild`):这两个发 markdown 代码块,
|
|
58
|
+
其余一律发去围栏的纯文本(提示一行 + 链接一行),照样整段复制、App 一样认。
|
|
59
|
+
|
|
3
60
|
## 3.9.1
|
|
4
61
|
|
|
5
62
|
> 修 MySQL 上启动直接报 `ER_TOO_LONG_KEY`、插件起不来的问题。
|
package/lib/build-metadata.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "3.
|
|
3
|
-
"buildTime": "2026-09-
|
|
4
|
-
"buildTimestamp":
|
|
2
|
+
"version": "3.10.1",
|
|
3
|
+
"buildTime": "2026-09-30T02:30:42.608Z",
|
|
4
|
+
"buildTimestamp": 1790735442631,
|
|
5
5
|
"name": "koishi-plugin-kkk",
|
|
6
6
|
"description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
|
|
7
7
|
"homepage": "https://github.com/maimai993/koishi-plugin-kkk#readme",
|
|
8
|
-
"commitHash": "
|
|
9
|
-
"shortCommitHash": "
|
|
8
|
+
"commitHash": "faa50bc98b4adaa32a0dda5bdf68de39539794f1",
|
|
9
|
+
"shortCommitHash": "faa50bc"
|
|
10
10
|
}
|
package/lib/karin/apps/tools.js
CHANGED
|
@@ -229,7 +229,13 @@ const handleDouyin = (0, ParseForward_1.withParseForward)((0, ErrorHandler_1.wra
|
|
|
229
229
|
* 面板按钮里继续用**原始分享链接**:`getDouyinID` 是靠跟随跳转拿 aweme_id 的,
|
|
230
230
|
* 换成 `www.douyin.com/video/{id}` 反而可能撞上抖音的验证页。
|
|
231
231
|
*/
|
|
232
|
-
if (iddata.type === 'one_work' && await tryQqPanel(e, {
|
|
232
|
+
if (iddata.type === 'one_work' && await tryQqPanel(e, {
|
|
233
|
+
platform: 'douyin',
|
|
234
|
+
url,
|
|
235
|
+
id: String(iddata.aweme_id),
|
|
236
|
+
// 跳转块用用户原样发的那条(App 分享出来的短链,识别率最高)
|
|
237
|
+
jumpUrl: url
|
|
238
|
+
}, flags)) {
|
|
233
239
|
return;
|
|
234
240
|
}
|
|
235
241
|
// 真的开始解析了才提示「本部署烧不了弹幕」——切换面板时要重发面板,那时提示是多余的
|
|
@@ -321,6 +327,10 @@ const handleBilibili = (0, ParseForward_1.withParseForward)((0, ErrorHandler_1.w
|
|
|
321
327
|
url: 'https://www.bilibili.com/video/' + iddata.bvid + (iddata.p ? '?p=' + iddata.p : ''),
|
|
322
328
|
id: String(iddata.bvid),
|
|
323
329
|
page: iddata.p
|
|
330
|
+
/**
|
|
331
|
+
* B站**不传 jumpUrl**:跳转块固定用 `www.bilibili.com/video/<bvid>` 这种标准形态
|
|
332
|
+
* (b23.tv 短链 App 复制了不跳转),由面板自己按 bvid + 分P 拼,见 `bilibiliShareUrl`。
|
|
333
|
+
*/
|
|
324
334
|
}, flags)) {
|
|
325
335
|
return;
|
|
326
336
|
}
|
|
@@ -385,7 +395,13 @@ const handleKuaishou = (0, ParseForward_1.withParseForward)((0, ErrorHandler_1.w
|
|
|
385
395
|
}
|
|
386
396
|
const iddata = await (0, kuaishou_1.getKuaishouID)(String(kuaishouUrl));
|
|
387
397
|
const WorkData = await (0, kuaishou_1.fetchKuaishouData)(iddata.type, iddata);
|
|
388
|
-
|
|
398
|
+
/**
|
|
399
|
+
* 跳转块要用**用户发出来的那条**:`match(...g)` 拿到的是数组,
|
|
400
|
+
* `String(数组)` 会把多条链接用逗号拼成一串(一条链接时碰巧正确),
|
|
401
|
+
* 这里显式取第一个,保证只发出一条干净的原链接。
|
|
402
|
+
*/
|
|
403
|
+
const kuaishouOrigin = Array.isArray(kuaishouUrl) && kuaishouUrl.length ? String(kuaishouUrl[0]) : String(kuaishouUrl ?? '');
|
|
404
|
+
const kuaishou = new kuaishou_1.Kuaishou(e, iddata, kuaishouOrigin);
|
|
389
405
|
await (0, ParseOverride_1.runWithParseOverride)({
|
|
390
406
|
...flags.override,
|
|
391
407
|
/** 面板按钮点出来的:只回「收到请求,开始下载」,不再发「检测到链接,开始解析」 */
|
|
@@ -427,7 +443,7 @@ const handleXiaohongshu = (0, ParseForward_1.withParseForward)((0, ErrorHandler_
|
|
|
427
443
|
return;
|
|
428
444
|
}
|
|
429
445
|
const iddata = await (0, xiaohongshu_1.getXiaohongshuID)(url);
|
|
430
|
-
const xiaohongshu = new xiaohongshu_1.Xiaohongshu(e, iddata);
|
|
446
|
+
const xiaohongshu = new xiaohongshu_1.Xiaohongshu(e, iddata, url);
|
|
431
447
|
await (0, ParseOverride_1.runWithParseOverride)({
|
|
432
448
|
...flags.override,
|
|
433
449
|
fromPanel: flags.panelToken !== undefined || flags.panel !== undefined
|
|
@@ -36,12 +36,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.buildImageMessageFor = exports.buildMarkdownImageMessage = exports.toMarkdownImage = exports.sendParseTip = void 0;
|
|
39
|
+
exports.buildImageMessageFor = exports.buildMarkdownImageMessage = exports.toMarkdownImage = exports.sendParseTip = exports.supportsMarkdown = void 0;
|
|
40
40
|
exports.rememberPanelRequest = rememberPanelRequest;
|
|
41
41
|
exports.resolvePanelQualitySize = resolvePanelQualitySize;
|
|
42
42
|
exports.resolvePanelToken = resolvePanelToken;
|
|
43
43
|
exports.isQqPlatform = isQqPlatform;
|
|
44
44
|
exports.sourceLink = sourceLink;
|
|
45
|
+
exports.copyJumpBlock = copyJumpBlock;
|
|
46
|
+
exports.douyinShareUrl = douyinShareUrl;
|
|
47
|
+
exports.bilibiliShareUrl = bilibiliShareUrl;
|
|
48
|
+
exports.kuaishouShareUrl = kuaishouShareUrl;
|
|
49
|
+
exports.xiaohongshuShareUrl = xiaohongshuShareUrl;
|
|
50
|
+
exports.sendCopyJumpMessage = sendCopyJumpMessage;
|
|
45
51
|
exports.cmdInput = cmdInput;
|
|
46
52
|
exports.rememberPanelMessage = rememberPanelMessage;
|
|
47
53
|
exports.recallLastPanel = recallLastPanel;
|
|
@@ -341,6 +347,157 @@ function sourceLink(label, url) {
|
|
|
341
347
|
const safe = String(url).replace(/[()\s]/g, (char) => encodeURIComponent(char));
|
|
342
348
|
return '[' + label + '](' + safe + ')';
|
|
343
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* 「复制后打开 XX 自动跳转」代码块:
|
|
352
|
+
*
|
|
353
|
+
* ```复制后打开b站自动跳转 →
|
|
354
|
+
* https://b23.tv/BV…
|
|
355
|
+
* ```
|
|
356
|
+
*
|
|
357
|
+
* 提示文案放在**围栏的信息位**(` ``` ` 后面那段,也就是 markdown 写语言的地方),
|
|
358
|
+
* 不占正文一行 —— 代码块里只有那条链接,全选复制时不会带上多余文字,
|
|
359
|
+
* 渲染出来提示仍是显示在代码块顶上的。
|
|
360
|
+
*
|
|
361
|
+
* 不用 `[打开原站](url)` 是因为那种链接能不能点取决于 adapter —— 官方 QQ 接口实测会拒收
|
|
362
|
+
* (见 `sourceLink` 的说明)。而各家 App 都认「剪贴板里有一条自家链接」:
|
|
363
|
+
* 复制整段文本后打开 App 会弹出打开询问,比点链接稳。
|
|
364
|
+
* @param appName App 名字,写进提示文案里(b站 / 抖音)
|
|
365
|
+
* @param url 目标地址(放进代码块的是纯文本,不做 markdown 编码)
|
|
366
|
+
* @returns 代码块的行数组;没地址时是空数组,调用方直接展开即可
|
|
367
|
+
*/
|
|
368
|
+
/**
|
|
369
|
+
* 抖音 App 的唤起口令。
|
|
370
|
+
*
|
|
371
|
+
* 抖音认的是「剪贴板里有一段带口令的文本」:光有一条链接,App 不一定弹跳转询问,
|
|
372
|
+
* 链接后接着这段口令才能保证复制后打开抖音直达作品。
|
|
373
|
+
* 这是抖音 App 生成的固定串(每条作品都一样,接口里没有对应字段),别当成模板去替换。
|
|
374
|
+
*/
|
|
375
|
+
const DOUYIN_JUMP_TOKEN = ':1pm Mai:/ 2237886846@qq.com';
|
|
376
|
+
/**
|
|
377
|
+
* @param appName App 名字,写进提示文案里(b站 / 抖音)
|
|
378
|
+
* @param url 目标地址(放进代码块的是纯文本,不做 markdown 编码)
|
|
379
|
+
* @param tail 追加在链接后面的文字(抖音的唤起口令走这里)
|
|
380
|
+
*/
|
|
381
|
+
function copyJumpBlock(appName, url, tail = '') {
|
|
382
|
+
if (!url)
|
|
383
|
+
return [];
|
|
384
|
+
// 链接和口令之间留两个空格:抖音的口令是独立一段,贴着链接会被当成 URL 的一部分
|
|
385
|
+
// 提示末尾的箭头指向代码块右上角的「复制」按钮 —— 复制的是整段(这里只有链接),App 才会跳转
|
|
386
|
+
return ['```' + '复制后打开' + appName + '自动跳转 →', String(url) + (tail ? ' ' + tail : ''), '```'];
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* 抖音作品的分享链接(**给卡片右下角二维码用的**)。
|
|
390
|
+
*
|
|
391
|
+
* 优先取接口返回的 `share_url`:那是抖音 App 生成的分享链接,后面带一串参数,
|
|
392
|
+
* 扫码场景没问题(二维码不吃 URL 长度),比自己拼的裸链接更接近官方形态;
|
|
393
|
+
* 拿不到(接口改版 / 风控)才退回标准链接。
|
|
394
|
+
*
|
|
395
|
+
* 注意它**不适合作为「复制跳转」的链接** —— 参数太长,App 的剪贴板识别不了,
|
|
396
|
+
* 那种场景要用用户原样发的那条(见 `sendParsePanel` 里 `request.jumpUrl`)。
|
|
397
|
+
* @param awemeId 作品 id
|
|
398
|
+
* @param detailShareUrl 作品详情里的 `share_url`
|
|
399
|
+
*/
|
|
400
|
+
function douyinShareUrl(awemeId, detailShareUrl) {
|
|
401
|
+
return String(detailShareUrl ?? '') || ('https://www.douyin.com/video/' + String(awemeId));
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* B站的跳转链接:**固定用标准视频页** `https://www.bilibili.com/video/<bvid>`。
|
|
405
|
+
*
|
|
406
|
+
* 试过 b23.tv 短链(`https://b23.tv/<bvid>`),实测 B站 App 认不出来 —— 复制后打开 App
|
|
407
|
+
* 没反应,必须是完整的 `www.bilibili.com/video/BV…` 这种形态。分 P 走 `?p=N` 查询参数。
|
|
408
|
+
*
|
|
409
|
+
* 卡片右下角的二维码也走这条(扫码不受长度影响,和跳转块保持一致)。
|
|
410
|
+
* @param bvid BV 号
|
|
411
|
+
* @param page 分 P(1 表示第一 P,不用带参数)
|
|
412
|
+
*/
|
|
413
|
+
function bilibiliShareUrl(bvid, page) {
|
|
414
|
+
const url = 'https://www.bilibili.com/video/' + String(bvid);
|
|
415
|
+
return page && page > 1 ? url + '?p=' + page : url;
|
|
416
|
+
}
|
|
417
|
+
/** 快手作品页链接(App 认的是自家域名下的链接,短链最终也是跳到这里) */
|
|
418
|
+
function kuaishouShareUrl(photoId) {
|
|
419
|
+
return 'https://www.kuaishou.com/short-video/' + String(photoId);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 小红书笔记页链接。
|
|
423
|
+
*
|
|
424
|
+
* 和详情卡片的二维码**逐字符对齐**(含 `xsec_token` 那串参数)——
|
|
425
|
+
* 卡片那儿一直是这么拼的,跳转块换用同一个函数即可,别再单独拼一遍。
|
|
426
|
+
* @param noteId 笔记 id
|
|
427
|
+
* @param xsecToken 访问令牌;没有就退回到不带参数的短形式
|
|
428
|
+
*/
|
|
429
|
+
function xiaohongshuShareUrl(noteId, xsecToken = '') {
|
|
430
|
+
const base = 'https://www.xiaohongshu.com/discovery/item/' + String(noteId);
|
|
431
|
+
return xsecToken
|
|
432
|
+
? base + '?source=webshare&xhsshare=pc_web&xsec_token=' + String(xsecToken) + '&xsec_source=pc_share'
|
|
433
|
+
: base;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* **能真正渲染 markdown 的平台** —— 只有官方 QQ 和 QQ 频道,别的一律发纯文本。
|
|
437
|
+
*
|
|
438
|
+
* 白名单而不是黑名单:markdown 是腾讯那两个 adapter 的私货,
|
|
439
|
+
* OneBot(NapCat / Lagrange / go-cqhttp / Chronocat)、Discord、Telegram、KOOK…
|
|
440
|
+
* 都不认这东西。代码块的 ` ``` ` 围栏在它们那里会原样露出来,
|
|
441
|
+
* 用户看到的是一串反引号而不是代码块。
|
|
442
|
+
*
|
|
443
|
+
* 这些平台发**去围栏的纯文本**:提示一行 + 链接一行,照样能整段复制,App 一样认。
|
|
444
|
+
* @param platform `platformOf(e)` 拿到的适配器名
|
|
445
|
+
*/
|
|
446
|
+
const supportsMarkdown = (platform) => {
|
|
447
|
+
const name = String(platform ?? '').toLowerCase();
|
|
448
|
+
return name === 'qq' || name === 'qqguild';
|
|
449
|
+
};
|
|
450
|
+
exports.supportsMarkdown = supportsMarkdown;
|
|
451
|
+
/**
|
|
452
|
+
* **没有清晰度面板的平台**:单独发一条「复制后打开 XX 自动跳转」。
|
|
453
|
+
*
|
|
454
|
+
* B站 / 抖音那条 repeat 代码块挂在清晰度表格下面(`sendParsePanel`),
|
|
455
|
+
* 而快手、小红书压根没有可选画质的面板——本来也不发任何原站链接。
|
|
456
|
+
* 这里给它们补一条同格式的独立消息,行为和开关都跟面板里的那行一致。
|
|
457
|
+
*
|
|
458
|
+
* 发送失败**不影响解析**:跳转是锦上添花,主流程(卡片 / 图片 / 视频)都已经发出去了。
|
|
459
|
+
* @param appName App 名字(快手 / 小红书)
|
|
460
|
+
* @param url 目标地址;空则直接跳过
|
|
461
|
+
* @param tail 追加在链接后面的文字(抖音走 QqPanel 内部的口令,其它平台不用)
|
|
462
|
+
* @returns 是否发出去了
|
|
463
|
+
*/
|
|
464
|
+
async function sendCopyJumpMessage(e, appName, url, tail = '') {
|
|
465
|
+
if (!url)
|
|
466
|
+
return false;
|
|
467
|
+
if ((0, runtime_1.tryGetRuntime)()?.config.qqPanelSourceLink === false)
|
|
468
|
+
return false;
|
|
469
|
+
const lines = copyJumpBlock(appName, url, tail);
|
|
470
|
+
/**
|
|
471
|
+
* 去掉围栏就是同内容的纯文本 —— 给不渲染 markdown 的适配器兜底。
|
|
472
|
+
* 提示行现在是 ``` 开头的(信息位写法),去掉 ``` 正好还原成一句提示。
|
|
473
|
+
*/
|
|
474
|
+
const plain = lines
|
|
475
|
+
.map((line) => (line.startsWith('```') ? line.slice(3) : line))
|
|
476
|
+
.filter(Boolean)
|
|
477
|
+
.join(String.fromCharCode(10));
|
|
478
|
+
const content = (0, exports.supportsMarkdown)((0, ImageSlice_1.platformOf)(e))
|
|
479
|
+
? node_karin_1.segment.markdown(lines.join(String.fromCharCode(10)))
|
|
480
|
+
: plain;
|
|
481
|
+
try {
|
|
482
|
+
await e.reply(content);
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
/**
|
|
487
|
+
* 官方 QQ 有时因为消息里的链接形式拒收整条 markdown(见 `sourceLink` 的说明),
|
|
488
|
+
* 这里退回纯文本再试一次;连纯文本都发不出去就跳过,不影响主流程。
|
|
489
|
+
*/
|
|
490
|
+
node_karin_1.logger.debug('[原站跳转] ' + appName + ' markdown 发送失败,退回纯文本: ' + String(error?.message ?? error).slice(0, 120));
|
|
491
|
+
try {
|
|
492
|
+
await e.reply(plain);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
catch (retryError) {
|
|
496
|
+
node_karin_1.logger.warn('[原站跳转] ' + appName + ' 发送失败,已跳过: ' + String(retryError?.message ?? retryError).slice(0, 120));
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
344
501
|
function cmdInput(command, show) {
|
|
345
502
|
const text = encodeURIComponent(command).replace(/'/g, '%27');
|
|
346
503
|
const label = encodeURIComponent(show ?? command).replace(/'/g, '%27');
|
|
@@ -473,6 +630,10 @@ async function showLoadingTip(e) {
|
|
|
473
630
|
*
|
|
474
631
|
* 单独认出来是为了**发送失败时能摘掉它重发**:链接只是锦上添花,
|
|
475
632
|
* 不能因为它(不同 adapter / 版本的链接限制不一样)把整条面板甚至整个解析搞挂。
|
|
633
|
+
*
|
|
634
|
+
* 「复制跳转」代码块(`copyJumpBlock`)也是链接行,但它跨 4 行带 ``` 围栏,
|
|
635
|
+
* 按行内容匹配容易漏(漏掉围栏会把后面的文字全包进代码块),所以那几行由调用方
|
|
636
|
+
* 用**下标范围**传给 `sendPanelMarkdown`。
|
|
476
637
|
*/
|
|
477
638
|
const isSourceLinkLine = (line) => /^\[[^\]]+\]\([^)]+\)\s*$/.test(line.trim());
|
|
478
639
|
/**
|
|
@@ -484,14 +645,18 @@ const isSourceLinkLine = (line) => /^\[[^\]]+\]\([^)]+\)\s*$/.test(line.trim());
|
|
|
484
645
|
* 先照常发,失败且内容里确实有链接行时,去掉链接行再发一次,成功就当没事发生。
|
|
485
646
|
* @param lines 面板的 markdown 行
|
|
486
647
|
* @param send 真正发送的函数(第一次/重发都走它)
|
|
648
|
+
* @param linkRange 「打开原站」那几行在 lines 里的下标范围 [起, 止);
|
|
649
|
+
* 不传时退回到按内容匹配 `[label](url)` 那一种写法
|
|
487
650
|
* @returns 最后一次发送的结果
|
|
488
651
|
*/
|
|
489
|
-
async function sendPanelMarkdown(lines, send) {
|
|
652
|
+
async function sendPanelMarkdown(lines, send, linkRange) {
|
|
490
653
|
try {
|
|
491
654
|
return { sent: await send(node_karin_1.segment.markdown(lines.join(String.fromCharCode(10)))), droppedLink: false };
|
|
492
655
|
}
|
|
493
656
|
catch (error) {
|
|
494
|
-
const withoutLink =
|
|
657
|
+
const withoutLink = linkRange
|
|
658
|
+
? [...lines.slice(0, linkRange[0]), ...lines.slice(linkRange[1])]
|
|
659
|
+
: lines.filter((line) => !isSourceLinkLine(line));
|
|
495
660
|
if (withoutLink.length === lines.length)
|
|
496
661
|
throw error;
|
|
497
662
|
node_karin_1.logger.warn('[QQ面板] 带链接的面板发送失败(' + String(error?.message ?? error).slice(0, 120) + '),去掉链接行重发一次');
|
|
@@ -572,7 +737,8 @@ async function uploadPanelCard(e, request, detail, hotDanmaku = []) {
|
|
|
572
737
|
e,
|
|
573
738
|
Detail_Data: detail,
|
|
574
739
|
create_time: Number(detail?.create_time) || Math.floor(Date.now() / 1000),
|
|
575
|
-
|
|
740
|
+
// 和右下角二维码同一个链接:抖音那条带一串分享参数,App 才认
|
|
741
|
+
shareLink: douyinShareUrl(String(detail?.aweme_id ?? request.id), detail?.share_url),
|
|
576
742
|
// 还没选档,所以不显示分辨率块(和分享信息图一致)
|
|
577
743
|
videoSource: undefined
|
|
578
744
|
});
|
|
@@ -602,7 +768,7 @@ async function uploadPanelCard(e, request, detail, hotDanmaku = []) {
|
|
|
602
768
|
const route = 'bilibili/videoInfo';
|
|
603
769
|
const cardData = request.platform === 'bilibili'
|
|
604
770
|
? {
|
|
605
|
-
share_url:
|
|
771
|
+
share_url: bilibiliShareUrl(String(detail.bvid ?? request.id)),
|
|
606
772
|
title: detail.title,
|
|
607
773
|
desc: '',
|
|
608
774
|
stat: detail.stat,
|
|
@@ -1013,16 +1179,38 @@ async function sendQqParsePanel(e, request) {
|
|
|
1013
1179
|
* 「打开原站」:放在表格下方(用户要求的位置)——用户看完卡片和画质后,
|
|
1014
1180
|
* 想直接去平台看原作品时不用再翻聊天记录找链接。
|
|
1015
1181
|
* 开关在 WebUI 的 QQ 适配器分组(qqPanelSourceLink,默认开)。
|
|
1182
|
+
*
|
|
1183
|
+
* B站 / 抖音给的是「复制后打开 XX 自动跳转」代码块:各家 App 都认剪贴板里的自家链接,
|
|
1184
|
+
* 不依赖 QQ 会不会把 markdown 链接渲染成可点的东西。快手之类仍给普通链接。
|
|
1185
|
+
*
|
|
1186
|
+
* 链接**优先用用户原样发的那条**(`request.jumpUrl`):我们拼的链接往往偏长,
|
|
1187
|
+
* App 剪贴板识别不了;用户发出来的 `v.douyin.com/xxx`、`v.kuaishou.com/xxx` 本来就是
|
|
1188
|
+
* 各家 App 自己生成的分享形态,最短也最容易被识别。
|
|
1189
|
+
*
|
|
1190
|
+
* **B站是唯一的例外**:它反过来 —— b23.tv 短链 App 不认,必须是完整的
|
|
1191
|
+
* `www.bilibili.com/video/BV…`,所以那支固定走 `bilibiliShareUrl()`,不用用户发的那条。
|
|
1192
|
+
* 二维码(卡片右下角)不受影响,和上面各平台一样保持自己的链接。
|
|
1016
1193
|
*/
|
|
1017
|
-
|
|
1018
|
-
|
|
1194
|
+
const linkStart = lines.length;
|
|
1195
|
+
if (runtime.config.qqPanelSourceLink !== false && request.url) {
|
|
1196
|
+
const jumpUrl = request.jumpUrl || request.url;
|
|
1197
|
+
const block = request.platform === 'bilibili'
|
|
1198
|
+
// B站固定用 BV 规范链接(短链 App 不识别),分 P 靠 request.page 带上
|
|
1199
|
+
? copyJumpBlock('b站', bilibiliShareUrl(String(request.id), request.page))
|
|
1200
|
+
: request.platform === 'douyin'
|
|
1201
|
+
? copyJumpBlock('抖音', jumpUrl, DOUYIN_JUMP_TOKEN)
|
|
1202
|
+
: [sourceLink('打开原站', jumpUrl)];
|
|
1203
|
+
lines.push(...block);
|
|
1204
|
+
}
|
|
1019
1205
|
// 带链接发不出去时自动去掉链接行重发(不然整条面板、整个解析都会被一个链接拖死)
|
|
1206
|
+
// 代码块连围栏共 3 行,所以按**下标范围**整块摘,不能只删中间那行
|
|
1207
|
+
const linkRange = lines.length > linkStart ? [linkStart, lines.length] : undefined;
|
|
1020
1208
|
const { sent } = await sendPanelMarkdown(lines, async (content) => {
|
|
1021
1209
|
// 卡片已经撤过「加载中…」了;卡片没发出去(或渲染失败)时这里补撤一次
|
|
1022
1210
|
if (!cardSent)
|
|
1023
1211
|
await recallMessageById(e, loadingId);
|
|
1024
1212
|
return await e.reply(content);
|
|
1025
|
-
});
|
|
1213
|
+
}, linkRange);
|
|
1026
1214
|
rememberPanelMessage(e, sent?.messageId);
|
|
1027
1215
|
node_karin_1.logger.debug('[QQ面板] 已发送解析面板: ' + request.platform + ' ' + request.id + '(' + shown.length + '/' + info.options.length + ' 档画质)');
|
|
1028
1216
|
return true;
|
|
@@ -6,6 +6,7 @@ const module_1 = require("../../module");
|
|
|
6
6
|
const ParseSteps_1 = require("../../module/utils/ParseSteps");
|
|
7
7
|
// sendParseTip 单独导入:它在一个无依赖的叶子模块里,避免和平台模块形成循环 import
|
|
8
8
|
const parseTip_1 = require("../../module/utils/parseTip");
|
|
9
|
+
const QqPanel_1 = require("../../module/utils/QqPanel");
|
|
9
10
|
// 注意用相对写法:@/ 别名在仓库里指向 karin/,@/player 会被解析成不存在的 karin/player
|
|
10
11
|
const player_1 = require("../../../player");
|
|
11
12
|
const kuaishou_1 = require("../../platform/kuaishou");
|
|
@@ -35,10 +36,20 @@ class Kuaishou extends module_1.Base {
|
|
|
35
36
|
* 本插件只解析视频,图集/单图会在下面提前返回,那时保持 undefined。
|
|
36
37
|
*/
|
|
37
38
|
workType;
|
|
38
|
-
|
|
39
|
+
/** ID 解析结果:里面的 photoId 用来兜底拼作品页链接 */
|
|
40
|
+
iddata;
|
|
41
|
+
/**
|
|
42
|
+
* **用户消息里原样提取出来的链接**(`v.kuaishou.com/xxx` 这类 App 分享短链)。
|
|
43
|
+
* 「复制后打开快手自动跳转」优先用这条 —— 它是快手 App 自己生成的,识别率最高;
|
|
44
|
+
* 我们拼的长链接(哪怕只是 `short-video/<photoId>`)App 未必认。
|
|
45
|
+
*/
|
|
46
|
+
originUrl;
|
|
47
|
+
constructor(e, iddata, originUrl = '') {
|
|
39
48
|
super(e);
|
|
40
49
|
this.e = e;
|
|
41
50
|
this.type = iddata?.type;
|
|
51
|
+
this.iddata = iddata;
|
|
52
|
+
this.originUrl = String(originUrl ?? '');
|
|
42
53
|
}
|
|
43
54
|
async KuaishouHandler(data) {
|
|
44
55
|
// 入参是 fetchKuaishouData 的联合返回,本插件只解析视频,先收窄到 one_work 那一支:
|
|
@@ -92,6 +103,20 @@ class Kuaishou extends module_1.Base {
|
|
|
92
103
|
return true;
|
|
93
104
|
}
|
|
94
105
|
this.workType = 'video';
|
|
106
|
+
/**
|
|
107
|
+
* 卡片右下角二维码与「复制后打开快手自动跳转」共用**同一条作品页链接**。
|
|
108
|
+
*
|
|
109
|
+
* 原来这里给模板的是 `video_url`(CDN 视频直链),扫出来是一段播放地址、
|
|
110
|
+
* 不是作品页;快手 App 认的是自家域名下的链接,所以统一改成 `short-video/<photoId>`。
|
|
111
|
+
*/
|
|
112
|
+
const photoId = String(work?.photo?.id ?? this.iddata?.photoId ?? '');
|
|
113
|
+
/**
|
|
114
|
+
* 取不到 photoId 时**不能给空串**:模板拿不到二维码内容会整张卡渲染不出来,
|
|
115
|
+
* 那种情况下退回 CDN 直链(原来的行为),至少卡片照常出、只是二维码还是播放地址。
|
|
116
|
+
* 「复制跳转」不受影响——它用自己的那份 shareUrl,取不到就不发。
|
|
117
|
+
*/
|
|
118
|
+
const shareUrl = photoId ? (0, QqPanel_1.kuaishouShareUrl)(photoId) : '';
|
|
119
|
+
const cardShareUrl = shareUrl || video_url;
|
|
95
120
|
/** 本次解析的步骤容器:单步失败只跳过、不中断,最后统一渲染一张错误卡片(见 ParseSteps) */
|
|
96
121
|
const steps = new ParseSteps_1.ParseSteps();
|
|
97
122
|
/** 发送任务组:评论区与视频各走一条线,谁先就绪谁先发 */
|
|
@@ -128,7 +153,7 @@ class Kuaishou extends module_1.Base {
|
|
|
128
153
|
viewCount: work.photo.viewCount,
|
|
129
154
|
CommentsData,
|
|
130
155
|
CommentLength: CommentsData?.length ?? 0,
|
|
131
|
-
share_url:
|
|
156
|
+
share_url: cardShareUrl,
|
|
132
157
|
VideoSize: fileSizeInMB,
|
|
133
158
|
likeCount: work.photo.likeCount
|
|
134
159
|
});
|
|
@@ -147,6 +172,12 @@ class Kuaishou extends module_1.Base {
|
|
|
147
172
|
await (0, module_1.uploadFile)(this.e, downloadedVideo, video_url, { message_id: this.e.messageId });
|
|
148
173
|
});
|
|
149
174
|
await sends.settle();
|
|
175
|
+
/**
|
|
176
|
+
* 「打开原站」**单独发一条**:快手没有清晰度面板(那套面板只有 B站 / 抖音有,
|
|
177
|
+
* 它们的跳转块挂在画质表格下面),所以这里在所有内容发完之后补一条同格式的消息。
|
|
178
|
+
* 发不出去也不影响解析(详情见 sendCopyJumpMessage)。
|
|
179
|
+
*/
|
|
180
|
+
await (0, QqPanel_1.sendCopyJumpMessage)(this.e, '快手', this.originUrl || shareUrl);
|
|
150
181
|
steps.throwIfFailed();
|
|
151
182
|
return true;
|
|
152
183
|
}
|
|
@@ -25,10 +25,18 @@ class Xiaohongshu extends module_1.Base {
|
|
|
25
25
|
* 笔记详情拿到之前无法判定,所以留到 `XiaohongshuHandler` 里赋值。
|
|
26
26
|
*/
|
|
27
27
|
workType;
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* **用户消息里原样提取出来的链接**(`xhslink.com/o/xxx` 这类 App 分享短链)。
|
|
30
|
+
*
|
|
31
|
+
* 「复制后打开小红书自动跳转」优先用这条:卡片那条带 `xsec_token` 全参数的分享链接
|
|
32
|
+
* 实测**太长、App 剪贴板识别不了**,而用户发的是小红书 App 自己生成的,一定能识别。
|
|
33
|
+
*/
|
|
34
|
+
originUrl;
|
|
35
|
+
constructor(e, iddata, originUrl = '') {
|
|
29
36
|
super(e);
|
|
30
37
|
this.e = e;
|
|
31
38
|
this.type = iddata?.type;
|
|
39
|
+
this.originUrl = String(originUrl ?? '');
|
|
32
40
|
}
|
|
33
41
|
/**
|
|
34
42
|
* 取够配置条数的笔记评论。
|
|
@@ -84,6 +92,13 @@ class Xiaohongshu extends module_1.Base {
|
|
|
84
92
|
*/
|
|
85
93
|
const rawItem = Array.isArray(noteItems) ? noteItems[0] : undefined;
|
|
86
94
|
const noteCard = rawItem?.note_card ?? rawItem?.noteCard ?? rawItem;
|
|
95
|
+
/**
|
|
96
|
+
* **卡片右下角的二维码与「复制跳转」必须是同一条链接**。
|
|
97
|
+
*
|
|
98
|
+
* 以前 noteInfo / comment 两个模板各拼一遍同样的字符串,跳转块再拼第三遍,
|
|
99
|
+
* 三处任何一处改动都会让它们指向不同地址。这里统一从这里取值。
|
|
100
|
+
*/
|
|
101
|
+
const shareUrl = (0, QqPanel_1.xiaohongshuShareUrl)(String(data.note_id), String(data.xsec_token ?? ''));
|
|
87
102
|
node_karin_2.logger.mark('[小红书] 条目键名: ' + JSON.stringify(Object.keys(rawItem ?? {}).slice(0, 12)) +
|
|
88
103
|
' / note_card 键名: ' + JSON.stringify(Object.keys(rawItem?.note_card ?? {}).slice(0, 16)));
|
|
89
104
|
if (!noteCard) {
|
|
@@ -198,7 +213,7 @@ class Xiaohongshu extends module_1.Base {
|
|
|
198
213
|
?? '',
|
|
199
214
|
time: noteCard.time,
|
|
200
215
|
ip_location: noteCard.ip_location,
|
|
201
|
-
share_url:
|
|
216
|
+
share_url: shareUrl,
|
|
202
217
|
image_list: noteCard.image_list?.map((image) => image.url_default) ?? [],
|
|
203
218
|
is_video: Boolean(noteCard.video)
|
|
204
219
|
});
|
|
@@ -348,7 +363,7 @@ class Xiaohongshu extends module_1.Base {
|
|
|
348
363
|
ImageLength: noteCard.image_list?.length || 0,
|
|
349
364
|
// 提示直接写进卡片(模板的 ErrorText),不再单独发文字、也不带 emoji
|
|
350
365
|
ErrorText: '评论数据获取失败,稍后再试试',
|
|
351
|
-
share_url:
|
|
366
|
+
share_url: shareUrl
|
|
352
367
|
});
|
|
353
368
|
await this.e.reply(emptyCommentCard);
|
|
354
369
|
node_karin_2.logger.mark('[小红书] 已发出空的评论卡片(占位)');
|
|
@@ -370,7 +385,7 @@ class Xiaohongshu extends module_1.Base {
|
|
|
370
385
|
CommentsData: processedComments,
|
|
371
386
|
CommentLength: processedComments.length,
|
|
372
387
|
ImageLength: noteCard.image_list?.length || 0,
|
|
373
|
-
share_url:
|
|
388
|
+
share_url: shareUrl
|
|
374
389
|
});
|
|
375
390
|
this.e.reply(commentListImg);
|
|
376
391
|
}
|
|
@@ -552,6 +567,12 @@ class Xiaohongshu extends module_1.Base {
|
|
|
552
567
|
* 由 ErrorHandler 渲染**一张**错误卡片(此时能发的卡片/图片/视频都已经发出去了)。
|
|
553
568
|
*/
|
|
554
569
|
await sends.settle();
|
|
570
|
+
/**
|
|
571
|
+
* 「打开原站」**单独发一条**:小红书没有清晰度面板(那套面板只有 B站 / 抖音有,
|
|
572
|
+
* 它们的跳转块挂在画质表格下面),所以这里在所有内容发完之后补一条同格式的消息。
|
|
573
|
+
* 发不出去也不影响解析(详情见 sendCopyJumpMessage)。
|
|
574
|
+
*/
|
|
575
|
+
await (0, QqPanel_1.sendCopyJumpMessage)(this.e, '小红书', this.originUrl || shareUrl);
|
|
555
576
|
steps.throwIfFailed();
|
|
556
577
|
return true;
|
|
557
578
|
}
|
package/package.json
CHANGED
package/src/karin/apps/tools.ts
CHANGED
|
@@ -208,7 +208,13 @@ const handleDouyin = withParseForward(wrapWithErrorHandler(
|
|
|
208
208
|
* 面板按钮里继续用**原始分享链接**:`getDouyinID` 是靠跟随跳转拿 aweme_id 的,
|
|
209
209
|
* 换成 `www.douyin.com/video/{id}` 反而可能撞上抖音的验证页。
|
|
210
210
|
*/
|
|
211
|
-
if (iddata.type === 'one_work' && await tryQqPanel(e, {
|
|
211
|
+
if (iddata.type === 'one_work' && await tryQqPanel(e, {
|
|
212
|
+
platform: 'douyin',
|
|
213
|
+
url,
|
|
214
|
+
id: String(iddata.aweme_id),
|
|
215
|
+
// 跳转块用用户原样发的那条(App 分享出来的短链,识别率最高)
|
|
216
|
+
jumpUrl: url
|
|
217
|
+
}, flags)) {
|
|
212
218
|
return
|
|
213
219
|
}
|
|
214
220
|
|
|
@@ -314,6 +320,10 @@ const handleBilibili = withParseForward(wrapWithErrorHandler(
|
|
|
314
320
|
url: 'https://www.bilibili.com/video/' + iddata.bvid + (iddata.p ? '?p=' + iddata.p : ''),
|
|
315
321
|
id: String(iddata.bvid),
|
|
316
322
|
page: iddata.p
|
|
323
|
+
/**
|
|
324
|
+
* B站**不传 jumpUrl**:跳转块固定用 `www.bilibili.com/video/<bvid>` 这种标准形态
|
|
325
|
+
* (b23.tv 短链 App 复制了不跳转),由面板自己按 bvid + 分P 拼,见 `bilibiliShareUrl`。
|
|
326
|
+
*/
|
|
317
327
|
}, flags)
|
|
318
328
|
) {
|
|
319
329
|
return
|
|
@@ -388,7 +398,13 @@ const handleKuaishou = withParseForward(wrapWithErrorHandler(
|
|
|
388
398
|
}
|
|
389
399
|
const iddata = await getKuaishouID(String(kuaishouUrl))
|
|
390
400
|
const WorkData = await fetchKuaishouData(iddata.type, iddata)
|
|
391
|
-
|
|
401
|
+
/**
|
|
402
|
+
* 跳转块要用**用户发出来的那条**:`match(...g)` 拿到的是数组,
|
|
403
|
+
* `String(数组)` 会把多条链接用逗号拼成一串(一条链接时碰巧正确),
|
|
404
|
+
* 这里显式取第一个,保证只发出一条干净的原链接。
|
|
405
|
+
*/
|
|
406
|
+
const kuaishouOrigin = Array.isArray(kuaishouUrl) && kuaishouUrl.length ? String(kuaishouUrl[0]) : String(kuaishouUrl ?? '')
|
|
407
|
+
const kuaishou = new Kuaishou(e, iddata, kuaishouOrigin)
|
|
392
408
|
await runWithParseOverride(
|
|
393
409
|
{
|
|
394
410
|
...flags.override,
|
|
@@ -437,7 +453,7 @@ const handleXiaohongshu = withParseForward(wrapWithErrorHandler(
|
|
|
437
453
|
return
|
|
438
454
|
}
|
|
439
455
|
const iddata = await getXiaohongshuID(url)
|
|
440
|
-
const xiaohongshu = new Xiaohongshu(e, iddata)
|
|
456
|
+
const xiaohongshu = new Xiaohongshu(e, iddata, url)
|
|
441
457
|
await runWithParseOverride(
|
|
442
458
|
{
|
|
443
459
|
...flags.override,
|
|
@@ -48,6 +48,14 @@ export interface PanelRequest {
|
|
|
48
48
|
id: string
|
|
49
49
|
/** B站分P */
|
|
50
50
|
page?: number
|
|
51
|
+
/**
|
|
52
|
+
* **用户消息里原样提取出来的那条链接**,只用在「复制后打开 XX 自动跳转」里。
|
|
53
|
+
*
|
|
54
|
+
* 之前跳转块用的是这边构造的链接(`www.douyin.com/video/<id>`、带一长串参数的分享链接…),
|
|
55
|
+
* 实测**太长 App 剪贴板不认**,而用户发的那条(通常是 App 分享出来的短口令链接)
|
|
56
|
+
* 反而是 App 自家生成的、一定能识别。没有时退回 `url`。
|
|
57
|
+
*/
|
|
58
|
+
jumpUrl?: string
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
/** 一个可选画质 */
|
|
@@ -343,6 +351,165 @@ export function sourceLink (label: string, url: string): string {
|
|
|
343
351
|
return '[' + label + '](' + safe + ')'
|
|
344
352
|
}
|
|
345
353
|
|
|
354
|
+
/**
|
|
355
|
+
* 「复制后打开 XX 自动跳转」代码块:
|
|
356
|
+
*
|
|
357
|
+
* ```复制后打开b站自动跳转 →
|
|
358
|
+
* https://b23.tv/BV…
|
|
359
|
+
* ```
|
|
360
|
+
*
|
|
361
|
+
* 提示文案放在**围栏的信息位**(` ``` ` 后面那段,也就是 markdown 写语言的地方),
|
|
362
|
+
* 不占正文一行 —— 代码块里只有那条链接,全选复制时不会带上多余文字,
|
|
363
|
+
* 渲染出来提示仍是显示在代码块顶上的。
|
|
364
|
+
*
|
|
365
|
+
* 不用 `[打开原站](url)` 是因为那种链接能不能点取决于 adapter —— 官方 QQ 接口实测会拒收
|
|
366
|
+
* (见 `sourceLink` 的说明)。而各家 App 都认「剪贴板里有一条自家链接」:
|
|
367
|
+
* 复制整段文本后打开 App 会弹出打开询问,比点链接稳。
|
|
368
|
+
* @param appName App 名字,写进提示文案里(b站 / 抖音)
|
|
369
|
+
* @param url 目标地址(放进代码块的是纯文本,不做 markdown 编码)
|
|
370
|
+
* @returns 代码块的行数组;没地址时是空数组,调用方直接展开即可
|
|
371
|
+
*/
|
|
372
|
+
/**
|
|
373
|
+
* 抖音 App 的唤起口令。
|
|
374
|
+
*
|
|
375
|
+
* 抖音认的是「剪贴板里有一段带口令的文本」:光有一条链接,App 不一定弹跳转询问,
|
|
376
|
+
* 链接后接着这段口令才能保证复制后打开抖音直达作品。
|
|
377
|
+
* 这是抖音 App 生成的固定串(每条作品都一样,接口里没有对应字段),别当成模板去替换。
|
|
378
|
+
*/
|
|
379
|
+
const DOUYIN_JUMP_TOKEN = ':1pm Mai:/ 2237886846@qq.com'
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param appName App 名字,写进提示文案里(b站 / 抖音)
|
|
383
|
+
* @param url 目标地址(放进代码块的是纯文本,不做 markdown 编码)
|
|
384
|
+
* @param tail 追加在链接后面的文字(抖音的唤起口令走这里)
|
|
385
|
+
*/
|
|
386
|
+
export function copyJumpBlock (appName: string, url: string, tail = ''): string[] {
|
|
387
|
+
if (!url) return []
|
|
388
|
+
// 链接和口令之间留两个空格:抖音的口令是独立一段,贴着链接会被当成 URL 的一部分
|
|
389
|
+
// 提示末尾的箭头指向代码块右上角的「复制」按钮 —— 复制的是整段(这里只有链接),App 才会跳转
|
|
390
|
+
return ['```' + '复制后打开' + appName + '自动跳转 →', String(url) + (tail ? ' ' + tail : ''), '```']
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 抖音作品的分享链接(**给卡片右下角二维码用的**)。
|
|
395
|
+
*
|
|
396
|
+
* 优先取接口返回的 `share_url`:那是抖音 App 生成的分享链接,后面带一串参数,
|
|
397
|
+
* 扫码场景没问题(二维码不吃 URL 长度),比自己拼的裸链接更接近官方形态;
|
|
398
|
+
* 拿不到(接口改版 / 风控)才退回标准链接。
|
|
399
|
+
*
|
|
400
|
+
* 注意它**不适合作为「复制跳转」的链接** —— 参数太长,App 的剪贴板识别不了,
|
|
401
|
+
* 那种场景要用用户原样发的那条(见 `sendParsePanel` 里 `request.jumpUrl`)。
|
|
402
|
+
* @param awemeId 作品 id
|
|
403
|
+
* @param detailShareUrl 作品详情里的 `share_url`
|
|
404
|
+
*/
|
|
405
|
+
export function douyinShareUrl (awemeId: string, detailShareUrl?: string): string {
|
|
406
|
+
return String(detailShareUrl ?? '') || ('https://www.douyin.com/video/' + String(awemeId))
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* B站的跳转链接:**固定用标准视频页** `https://www.bilibili.com/video/<bvid>`。
|
|
411
|
+
*
|
|
412
|
+
* 试过 b23.tv 短链(`https://b23.tv/<bvid>`),实测 B站 App 认不出来 —— 复制后打开 App
|
|
413
|
+
* 没反应,必须是完整的 `www.bilibili.com/video/BV…` 这种形态。分 P 走 `?p=N` 查询参数。
|
|
414
|
+
*
|
|
415
|
+
* 卡片右下角的二维码也走这条(扫码不受长度影响,和跳转块保持一致)。
|
|
416
|
+
* @param bvid BV 号
|
|
417
|
+
* @param page 分 P(1 表示第一 P,不用带参数)
|
|
418
|
+
*/
|
|
419
|
+
export function bilibiliShareUrl (bvid: string, page?: number): string {
|
|
420
|
+
const url = 'https://www.bilibili.com/video/' + String(bvid)
|
|
421
|
+
return page && page > 1 ? url + '?p=' + page : url
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** 快手作品页链接(App 认的是自家域名下的链接,短链最终也是跳到这里) */
|
|
425
|
+
export function kuaishouShareUrl (photoId: string): string {
|
|
426
|
+
return 'https://www.kuaishou.com/short-video/' + String(photoId)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 小红书笔记页链接。
|
|
431
|
+
*
|
|
432
|
+
* 和详情卡片的二维码**逐字符对齐**(含 `xsec_token` 那串参数)——
|
|
433
|
+
* 卡片那儿一直是这么拼的,跳转块换用同一个函数即可,别再单独拼一遍。
|
|
434
|
+
* @param noteId 笔记 id
|
|
435
|
+
* @param xsecToken 访问令牌;没有就退回到不带参数的短形式
|
|
436
|
+
*/
|
|
437
|
+
export function xiaohongshuShareUrl (noteId: string, xsecToken = ''): string {
|
|
438
|
+
const base = 'https://www.xiaohongshu.com/discovery/item/' + String(noteId)
|
|
439
|
+
return xsecToken
|
|
440
|
+
? base + '?source=webshare&xhsshare=pc_web&xsec_token=' + String(xsecToken) + '&xsec_source=pc_share'
|
|
441
|
+
: base
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* **能真正渲染 markdown 的平台** —— 只有官方 QQ 和 QQ 频道,别的一律发纯文本。
|
|
446
|
+
*
|
|
447
|
+
* 白名单而不是黑名单:markdown 是腾讯那两个 adapter 的私货,
|
|
448
|
+
* OneBot(NapCat / Lagrange / go-cqhttp / Chronocat)、Discord、Telegram、KOOK…
|
|
449
|
+
* 都不认这东西。代码块的 ` ``` ` 围栏在它们那里会原样露出来,
|
|
450
|
+
* 用户看到的是一串反引号而不是代码块。
|
|
451
|
+
*
|
|
452
|
+
* 这些平台发**去围栏的纯文本**:提示一行 + 链接一行,照样能整段复制,App 一样认。
|
|
453
|
+
* @param platform `platformOf(e)` 拿到的适配器名
|
|
454
|
+
*/
|
|
455
|
+
export const supportsMarkdown = (platform: string): boolean => {
|
|
456
|
+
const name = String(platform ?? '').toLowerCase()
|
|
457
|
+
return name === 'qq' || name === 'qqguild'
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* **没有清晰度面板的平台**:单独发一条「复制后打开 XX 自动跳转」。
|
|
462
|
+
*
|
|
463
|
+
* B站 / 抖音那条 repeat 代码块挂在清晰度表格下面(`sendParsePanel`),
|
|
464
|
+
* 而快手、小红书压根没有可选画质的面板——本来也不发任何原站链接。
|
|
465
|
+
* 这里给它们补一条同格式的独立消息,行为和开关都跟面板里的那行一致。
|
|
466
|
+
*
|
|
467
|
+
* 发送失败**不影响解析**:跳转是锦上添花,主流程(卡片 / 图片 / 视频)都已经发出去了。
|
|
468
|
+
* @param appName App 名字(快手 / 小红书)
|
|
469
|
+
* @param url 目标地址;空则直接跳过
|
|
470
|
+
* @param tail 追加在链接后面的文字(抖音走 QqPanel 内部的口令,其它平台不用)
|
|
471
|
+
* @returns 是否发出去了
|
|
472
|
+
*/
|
|
473
|
+
export async function sendCopyJumpMessage (
|
|
474
|
+
e: Message,
|
|
475
|
+
appName: string,
|
|
476
|
+
url: string,
|
|
477
|
+
tail = ''
|
|
478
|
+
): Promise<boolean> {
|
|
479
|
+
if (!url) return false
|
|
480
|
+
if (tryGetRuntime()?.config.qqPanelSourceLink === false) return false
|
|
481
|
+
|
|
482
|
+
const lines = copyJumpBlock(appName, url, tail)
|
|
483
|
+
/**
|
|
484
|
+
* 去掉围栏就是同内容的纯文本 —— 给不渲染 markdown 的适配器兜底。
|
|
485
|
+
* 提示行现在是 ``` 开头的(信息位写法),去掉 ``` 正好还原成一句提示。
|
|
486
|
+
*/
|
|
487
|
+
const plain = lines
|
|
488
|
+
.map((line) => (line.startsWith('```') ? line.slice(3) : line))
|
|
489
|
+
.filter(Boolean)
|
|
490
|
+
.join(String.fromCharCode(10))
|
|
491
|
+
const content = supportsMarkdown(platformOf(e))
|
|
492
|
+
? segment.markdown(lines.join(String.fromCharCode(10)))
|
|
493
|
+
: plain
|
|
494
|
+
try {
|
|
495
|
+
await e.reply(content)
|
|
496
|
+
return true
|
|
497
|
+
} catch (error: any) {
|
|
498
|
+
/**
|
|
499
|
+
* 官方 QQ 有时因为消息里的链接形式拒收整条 markdown(见 `sourceLink` 的说明),
|
|
500
|
+
* 这里退回纯文本再试一次;连纯文本都发不出去就跳过,不影响主流程。
|
|
501
|
+
*/
|
|
502
|
+
logger.debug('[原站跳转] ' + appName + ' markdown 发送失败,退回纯文本: ' + String(error?.message ?? error).slice(0, 120))
|
|
503
|
+
try {
|
|
504
|
+
await e.reply(plain)
|
|
505
|
+
return true
|
|
506
|
+
} catch (retryError: any) {
|
|
507
|
+
logger.warn('[原站跳转] ' + appName + ' 发送失败,已跳过: ' + String(retryError?.message ?? retryError).slice(0, 120))
|
|
508
|
+
return false
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
346
513
|
export function cmdInput (command: string, show?: string): string {
|
|
347
514
|
const text = encodeURIComponent(command).replace(/'/g, '%27')
|
|
348
515
|
const label = encodeURIComponent(show ?? command).replace(/'/g, '%27')
|
|
@@ -465,6 +632,10 @@ async function showLoadingTip (e: Message): Promise<string | undefined> {
|
|
|
465
632
|
*
|
|
466
633
|
* 单独认出来是为了**发送失败时能摘掉它重发**:链接只是锦上添花,
|
|
467
634
|
* 不能因为它(不同 adapter / 版本的链接限制不一样)把整条面板甚至整个解析搞挂。
|
|
635
|
+
*
|
|
636
|
+
* 「复制跳转」代码块(`copyJumpBlock`)也是链接行,但它跨 4 行带 ``` 围栏,
|
|
637
|
+
* 按行内容匹配容易漏(漏掉围栏会把后面的文字全包进代码块),所以那几行由调用方
|
|
638
|
+
* 用**下标范围**传给 `sendPanelMarkdown`。
|
|
468
639
|
*/
|
|
469
640
|
const isSourceLinkLine = (line: string): boolean => /^\[[^\]]+\]\([^)]+\)\s*$/.test(line.trim())
|
|
470
641
|
|
|
@@ -477,16 +648,21 @@ const isSourceLinkLine = (line: string): boolean => /^\[[^\]]+\]\([^)]+\)\s*$/.t
|
|
|
477
648
|
* 先照常发,失败且内容里确实有链接行时,去掉链接行再发一次,成功就当没事发生。
|
|
478
649
|
* @param lines 面板的 markdown 行
|
|
479
650
|
* @param send 真正发送的函数(第一次/重发都走它)
|
|
651
|
+
* @param linkRange 「打开原站」那几行在 lines 里的下标范围 [起, 止);
|
|
652
|
+
* 不传时退回到按内容匹配 `[label](url)` 那一种写法
|
|
480
653
|
* @returns 最后一次发送的结果
|
|
481
654
|
*/
|
|
482
655
|
async function sendPanelMarkdown (
|
|
483
656
|
lines: string[],
|
|
484
|
-
send: (content: any) => Promise<any
|
|
657
|
+
send: (content: any) => Promise<any>,
|
|
658
|
+
linkRange?: [number, number]
|
|
485
659
|
): Promise<{ sent: any, droppedLink: boolean }> {
|
|
486
660
|
try {
|
|
487
661
|
return { sent: await send(segment.markdown(lines.join(String.fromCharCode(10)))), droppedLink: false }
|
|
488
662
|
} catch (error: any) {
|
|
489
|
-
const withoutLink =
|
|
663
|
+
const withoutLink = linkRange
|
|
664
|
+
? [...lines.slice(0, linkRange[0]), ...lines.slice(linkRange[1])]
|
|
665
|
+
: lines.filter((line) => !isSourceLinkLine(line))
|
|
490
666
|
if (withoutLink.length === lines.length) throw error
|
|
491
667
|
logger.warn('[QQ面板] 带链接的面板发送失败(' + String(error?.message ?? error).slice(0, 120) + '),去掉链接行重发一次')
|
|
492
668
|
return { sent: await send(segment.markdown(withoutLink.join(String.fromCharCode(10)))), droppedLink: true }
|
|
@@ -573,7 +749,8 @@ async function uploadPanelCard (
|
|
|
573
749
|
e,
|
|
574
750
|
Detail_Data: detail,
|
|
575
751
|
create_time: Number(detail?.create_time) || Math.floor(Date.now() / 1000),
|
|
576
|
-
|
|
752
|
+
// 和右下角二维码同一个链接:抖音那条带一串分享参数,App 才认
|
|
753
|
+
shareLink: douyinShareUrl(String(detail?.aweme_id ?? request.id), detail?.share_url),
|
|
577
754
|
// 还没选档,所以不显示分辨率块(和分享信息图一致)
|
|
578
755
|
videoSource: undefined
|
|
579
756
|
} as any)
|
|
@@ -602,7 +779,7 @@ async function uploadPanelCard (
|
|
|
602
779
|
const route = 'bilibili/videoInfo'
|
|
603
780
|
const cardData: any = request.platform === 'bilibili'
|
|
604
781
|
? {
|
|
605
|
-
share_url:
|
|
782
|
+
share_url: bilibiliShareUrl(String(detail.bvid ?? request.id)),
|
|
606
783
|
title: detail.title,
|
|
607
784
|
desc: '',
|
|
608
785
|
stat: detail.stat,
|
|
@@ -1009,14 +1186,37 @@ export async function sendQqParsePanel (e: Message, request: PanelRequest): Prom
|
|
|
1009
1186
|
* 「打开原站」:放在表格下方(用户要求的位置)——用户看完卡片和画质后,
|
|
1010
1187
|
* 想直接去平台看原作品时不用再翻聊天记录找链接。
|
|
1011
1188
|
* 开关在 WebUI 的 QQ 适配器分组(qqPanelSourceLink,默认开)。
|
|
1189
|
+
*
|
|
1190
|
+
* B站 / 抖音给的是「复制后打开 XX 自动跳转」代码块:各家 App 都认剪贴板里的自家链接,
|
|
1191
|
+
* 不依赖 QQ 会不会把 markdown 链接渲染成可点的东西。快手之类仍给普通链接。
|
|
1192
|
+
*
|
|
1193
|
+
* 链接**优先用用户原样发的那条**(`request.jumpUrl`):我们拼的链接往往偏长,
|
|
1194
|
+
* App 剪贴板识别不了;用户发出来的 `v.douyin.com/xxx`、`v.kuaishou.com/xxx` 本来就是
|
|
1195
|
+
* 各家 App 自己生成的分享形态,最短也最容易被识别。
|
|
1196
|
+
*
|
|
1197
|
+
* **B站是唯一的例外**:它反过来 —— b23.tv 短链 App 不认,必须是完整的
|
|
1198
|
+
* `www.bilibili.com/video/BV…`,所以那支固定走 `bilibiliShareUrl()`,不用用户发的那条。
|
|
1199
|
+
* 二维码(卡片右下角)不受影响,和上面各平台一样保持自己的链接。
|
|
1012
1200
|
*/
|
|
1013
|
-
|
|
1201
|
+
const linkStart = lines.length
|
|
1202
|
+
if (runtime.config.qqPanelSourceLink !== false && request.url) {
|
|
1203
|
+
const jumpUrl = request.jumpUrl || request.url
|
|
1204
|
+
const block = request.platform === 'bilibili'
|
|
1205
|
+
// B站固定用 BV 规范链接(短链 App 不识别),分 P 靠 request.page 带上
|
|
1206
|
+
? copyJumpBlock('b站', bilibiliShareUrl(String(request.id), request.page))
|
|
1207
|
+
: request.platform === 'douyin'
|
|
1208
|
+
? copyJumpBlock('抖音', jumpUrl, DOUYIN_JUMP_TOKEN)
|
|
1209
|
+
: [sourceLink('打开原站', jumpUrl)]
|
|
1210
|
+
lines.push(...block)
|
|
1211
|
+
}
|
|
1014
1212
|
// 带链接发不出去时自动去掉链接行重发(不然整条面板、整个解析都会被一个链接拖死)
|
|
1213
|
+
// 代码块连围栏共 3 行,所以按**下标范围**整块摘,不能只删中间那行
|
|
1214
|
+
const linkRange: [number, number] | undefined = lines.length > linkStart ? [linkStart, lines.length] : undefined
|
|
1015
1215
|
const { sent } = await sendPanelMarkdown(lines, async (content) => {
|
|
1016
1216
|
// 卡片已经撤过「加载中…」了;卡片没发出去(或渲染失败)时这里补撤一次
|
|
1017
1217
|
if (!cardSent) await recallMessageById(e, loadingId)
|
|
1018
1218
|
return await e.reply(content)
|
|
1019
|
-
})
|
|
1219
|
+
}, linkRange)
|
|
1020
1220
|
rememberPanelMessage(e, sent?.messageId)
|
|
1021
1221
|
logger.debug('[QQ面板] 已发送解析面板: ' + request.platform + ' ' + request.id + '(' + shown.length + '/' + info.options.length + ' 档画质)')
|
|
1022
1222
|
return true
|
|
@@ -6,6 +6,7 @@ import { ParseSteps, SendTasks } from '@/module/utils/ParseSteps'
|
|
|
6
6
|
// sendParseTip 单独导入:它在一个无依赖的叶子模块里,避免和平台模块形成循环 import
|
|
7
7
|
import { sendParseTip } from '@/module/utils/parseTip'
|
|
8
8
|
import type { ParseWorkType } from '@/module/db'
|
|
9
|
+
import { kuaishouShareUrl, sendCopyJumpMessage } from '@/module/utils/QqPanel'
|
|
9
10
|
import { Config } from '@/module/utils/Config'
|
|
10
11
|
// 注意用相对写法:@/ 别名在仓库里指向 karin/,@/player 会被解析成不存在的 karin/player
|
|
11
12
|
import { applyForceOnlinePlayer } from '../../../player'
|
|
@@ -39,10 +40,20 @@ export class Kuaishou extends Base {
|
|
|
39
40
|
* 本插件只解析视频,图集/单图会在下面提前返回,那时保持 undefined。
|
|
40
41
|
*/
|
|
41
42
|
workType?: ParseWorkType
|
|
42
|
-
|
|
43
|
+
/** ID 解析结果:里面的 photoId 用来兜底拼作品页链接 */
|
|
44
|
+
private readonly iddata: ExtendedKuaishouOptionsType
|
|
45
|
+
/**
|
|
46
|
+
* **用户消息里原样提取出来的链接**(`v.kuaishou.com/xxx` 这类 App 分享短链)。
|
|
47
|
+
* 「复制后打开快手自动跳转」优先用这条 —— 它是快手 App 自己生成的,识别率最高;
|
|
48
|
+
* 我们拼的长链接(哪怕只是 `short-video/<photoId>`)App 未必认。
|
|
49
|
+
*/
|
|
50
|
+
private readonly originUrl: string
|
|
51
|
+
constructor(e: Message, iddata: ExtendedKuaishouOptionsType, originUrl = '') {
|
|
43
52
|
super(e)
|
|
44
53
|
this.e = e
|
|
45
54
|
this.type = iddata?.type
|
|
55
|
+
this.iddata = iddata
|
|
56
|
+
this.originUrl = String(originUrl ?? '')
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
async KuaishouHandler(data: KuaishouDataResult) {
|
|
@@ -101,6 +112,20 @@ export class Kuaishou extends Base {
|
|
|
101
112
|
return true
|
|
102
113
|
}
|
|
103
114
|
this.workType = 'video'
|
|
115
|
+
/**
|
|
116
|
+
* 卡片右下角二维码与「复制后打开快手自动跳转」共用**同一条作品页链接**。
|
|
117
|
+
*
|
|
118
|
+
* 原来这里给模板的是 `video_url`(CDN 视频直链),扫出来是一段播放地址、
|
|
119
|
+
* 不是作品页;快手 App 认的是自家域名下的链接,所以统一改成 `short-video/<photoId>`。
|
|
120
|
+
*/
|
|
121
|
+
const photoId = String(work?.photo?.id ?? this.iddata?.photoId ?? '')
|
|
122
|
+
/**
|
|
123
|
+
* 取不到 photoId 时**不能给空串**:模板拿不到二维码内容会整张卡渲染不出来,
|
|
124
|
+
* 那种情况下退回 CDN 直链(原来的行为),至少卡片照常出、只是二维码还是播放地址。
|
|
125
|
+
* 「复制跳转」不受影响——它用自己的那份 shareUrl,取不到就不发。
|
|
126
|
+
*/
|
|
127
|
+
const shareUrl = photoId ? kuaishouShareUrl(photoId) : ''
|
|
128
|
+
const cardShareUrl = shareUrl || video_url
|
|
104
129
|
/** 本次解析的步骤容器:单步失败只跳过、不中断,最后统一渲染一张错误卡片(见 ParseSteps) */
|
|
105
130
|
const steps = new ParseSteps()
|
|
106
131
|
/** 发送任务组:评论区与视频各走一条线,谁先就绪谁先发 */
|
|
@@ -140,7 +165,7 @@ export class Kuaishou extends Base {
|
|
|
140
165
|
viewCount: work.photo.viewCount,
|
|
141
166
|
CommentsData,
|
|
142
167
|
CommentLength: CommentsData?.length ?? 0,
|
|
143
|
-
share_url:
|
|
168
|
+
share_url: cardShareUrl,
|
|
144
169
|
VideoSize: fileSizeInMB,
|
|
145
170
|
likeCount: work.photo.likeCount
|
|
146
171
|
})
|
|
@@ -161,6 +186,12 @@ export class Kuaishou extends Base {
|
|
|
161
186
|
})
|
|
162
187
|
|
|
163
188
|
await sends.settle()
|
|
189
|
+
/**
|
|
190
|
+
* 「打开原站」**单独发一条**:快手没有清晰度面板(那套面板只有 B站 / 抖音有,
|
|
191
|
+
* 它们的跳转块挂在画质表格下面),所以这里在所有内容发完之后补一条同格式的消息。
|
|
192
|
+
* 发不出去也不影响解析(详情见 sendCopyJumpMessage)。
|
|
193
|
+
*/
|
|
194
|
+
await sendCopyJumpMessage(this.e, '快手', this.originUrl || shareUrl)
|
|
164
195
|
steps.throwIfFailed()
|
|
165
196
|
return true
|
|
166
197
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import { platformOf } from '@/module/utils/ImageSlice'
|
|
3
|
-
import { buildMarkdownImageMessage } from '@/module/utils/QqPanel'
|
|
3
|
+
import { buildMarkdownImageMessage, sendCopyJumpMessage, xiaohongshuShareUrl } from '@/module/utils/QqPanel'
|
|
4
4
|
import { sendParseTip } from '@/module/utils/parseTip'
|
|
5
5
|
|
|
6
6
|
import type { NoteComments, XiaohongshuEmojiListResponse } from '@ikenxuan/amagi'
|
|
@@ -61,11 +61,19 @@ export class Xiaohongshu extends Base {
|
|
|
61
61
|
* 笔记详情拿到之前无法判定,所以留到 `XiaohongshuHandler` 里赋值。
|
|
62
62
|
*/
|
|
63
63
|
workType?: ParseWorkType
|
|
64
|
+
/**
|
|
65
|
+
* **用户消息里原样提取出来的链接**(`xhslink.com/o/xxx` 这类 App 分享短链)。
|
|
66
|
+
*
|
|
67
|
+
* 「复制后打开小红书自动跳转」优先用这条:卡片那条带 `xsec_token` 全参数的分享链接
|
|
68
|
+
* 实测**太长、App 剪贴板识别不了**,而用户发的是小红书 App 自己生成的,一定能识别。
|
|
69
|
+
*/
|
|
70
|
+
private readonly originUrl: string
|
|
64
71
|
|
|
65
|
-
constructor(e: Message, iddata: XiaohongshuIdData) {
|
|
72
|
+
constructor(e: Message, iddata: XiaohongshuIdData, originUrl = '') {
|
|
66
73
|
super(e)
|
|
67
74
|
this.e = e
|
|
68
75
|
this.type = iddata?.type
|
|
76
|
+
this.originUrl = String(originUrl ?? '')
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
/**
|
|
@@ -124,6 +132,13 @@ export class Xiaohongshu extends Base {
|
|
|
124
132
|
*/
|
|
125
133
|
const rawItem: any = Array.isArray(noteItems) ? noteItems[0] : undefined
|
|
126
134
|
const noteCard: any = rawItem?.note_card ?? rawItem?.noteCard ?? rawItem
|
|
135
|
+
/**
|
|
136
|
+
* **卡片右下角的二维码与「复制跳转」必须是同一条链接**。
|
|
137
|
+
*
|
|
138
|
+
* 以前 noteInfo / comment 两个模板各拼一遍同样的字符串,跳转块再拼第三遍,
|
|
139
|
+
* 三处任何一处改动都会让它们指向不同地址。这里统一从这里取值。
|
|
140
|
+
*/
|
|
141
|
+
const shareUrl = xiaohongshuShareUrl(String(data.note_id), String(data.xsec_token ?? ''))
|
|
127
142
|
logger.mark('[小红书] 条目键名: ' + JSON.stringify(Object.keys(rawItem ?? {}).slice(0, 12)) +
|
|
128
143
|
' / note_card 键名: ' + JSON.stringify(Object.keys(rawItem?.note_card ?? {}).slice(0, 16)))
|
|
129
144
|
if (!noteCard) {
|
|
@@ -245,7 +260,7 @@ export class Xiaohongshu extends Base {
|
|
|
245
260
|
?? '',
|
|
246
261
|
time: noteCard.time,
|
|
247
262
|
ip_location: noteCard.ip_location,
|
|
248
|
-
share_url:
|
|
263
|
+
share_url: shareUrl,
|
|
249
264
|
image_list: noteCard.image_list?.map((image) => image.url_default) ?? [],
|
|
250
265
|
is_video: Boolean(noteCard.video)
|
|
251
266
|
})
|
|
@@ -395,7 +410,7 @@ export class Xiaohongshu extends Base {
|
|
|
395
410
|
ImageLength: noteCard.image_list?.length || 0,
|
|
396
411
|
// 提示直接写进卡片(模板的 ErrorText),不再单独发文字、也不带 emoji
|
|
397
412
|
ErrorText: '评论数据获取失败,稍后再试试',
|
|
398
|
-
share_url:
|
|
413
|
+
share_url: shareUrl
|
|
399
414
|
})
|
|
400
415
|
await this.e.reply(emptyCommentCard)
|
|
401
416
|
logger.mark('[小红书] 已发出空的评论卡片(占位)')
|
|
@@ -415,7 +430,7 @@ export class Xiaohongshu extends Base {
|
|
|
415
430
|
CommentsData: processedComments,
|
|
416
431
|
CommentLength: processedComments.length,
|
|
417
432
|
ImageLength: noteCard.image_list?.length || 0,
|
|
418
|
-
share_url:
|
|
433
|
+
share_url: shareUrl
|
|
419
434
|
})
|
|
420
435
|
this.e.reply(commentListImg)
|
|
421
436
|
}
|
|
@@ -618,6 +633,12 @@ export class Xiaohongshu extends Base {
|
|
|
618
633
|
* 由 ErrorHandler 渲染**一张**错误卡片(此时能发的卡片/图片/视频都已经发出去了)。
|
|
619
634
|
*/
|
|
620
635
|
await sends.settle()
|
|
636
|
+
/**
|
|
637
|
+
* 「打开原站」**单独发一条**:小红书没有清晰度面板(那套面板只有 B站 / 抖音有,
|
|
638
|
+
* 它们的跳转块挂在画质表格下面),所以这里在所有内容发完之后补一条同格式的消息。
|
|
639
|
+
* 发不出去也不影响解析(详情见 sendCopyJumpMessage)。
|
|
640
|
+
*/
|
|
641
|
+
await sendCopyJumpMessage(this.e, '小红书', this.originUrl || shareUrl)
|
|
621
642
|
steps.throwIfFailed()
|
|
622
643
|
return true
|
|
623
644
|
}
|