koishi-plugin-kkk 3.10.0 → 3.10.2

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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # 更新日志
2
2
 
3
+ ## 3.10.2
4
+
5
+ > 修小红书图文笔记的图集一次解析发两遍的问题。
6
+
7
+ ### 修小红书图集发两遍
8
+
9
+ - 图文笔记的图集有**两个发送点**:一个不看「发送内容」开关、直接发一条 markdown 全套图,
10
+ 另一个按 `sendContent` 里的「图片」发(含实况图处理)。两个都命中时就是**两套图集**。
11
+ - 删掉前面那个不看开关的,图集统一由「图片」那处发送;关掉「图片」现在是真的不发了
12
+ (以前关掉照样会发一份)。
13
+ - 顺带给图集发送加了失败兜底:发不出去只记一步失败,后面的视频和错误卡片照常,
14
+ 不再把整条解析掀翻。
15
+
16
+ ## 3.10.1
17
+
18
+ > B站的复制跳转修一下:3.10.0 改成用用户发的那条链接后,B站 App 复制了反而不跳。
19
+
20
+ ### B站跳转链接改回标准视频页
21
+
22
+ - 3.10.0 让跳转块统一用用户发出来的那条链接,B站照做之后反而不能用:
23
+ 用户发的多半是 `b23.tv/xxx` 短链,**B站 App 复制了不跳转**。
24
+ - B站的跳转块固定用 `https://www.bilibili.com/video/<bvid>`(分 P 带 `?p=N`),
25
+ 不再用用户发的那条。卡片右下角的二维码同步改成这条,两边一致。
26
+ - 抖音、快手、小红书仍按 3.10.0 的规则:用用户原样发的那条短链。
27
+
3
28
  ## 3.10.0
4
29
 
5
30
  > 解析消息底部的「打开原站」链接换成了**代码块形式的跳转卡片**:
@@ -1,10 +1,10 @@
1
1
  {
2
- "version": "3.10.0",
3
- "buildTime": "2026-09-29T18:43:09.611Z",
4
- "buildTimestamp": 1790707389633,
2
+ "version": "3.10.2",
3
+ "buildTime": "2026-09-30T06:37:26.725Z",
4
+ "buildTimestamp": 1790750246750,
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": "ec43387a40b337a0c1430c7b2eef36668fa92fc5",
9
- "shortCommitHash": "ec43387"
8
+ "commitHash": "146e95e8c0f183d42d08beba60a079b6d0b81c07",
9
+ "shortCommitHash": "146e95e"
10
10
  }
@@ -326,12 +326,11 @@ const handleBilibili = (0, ParseForward_1.withParseForward)((0, ErrorHandler_1.w
326
326
  platform: 'bilibili',
327
327
  url: 'https://www.bilibili.com/video/' + iddata.bvid + (iddata.p ? '?p=' + iddata.p : ''),
328
328
  id: String(iddata.bvid),
329
- page: iddata.p,
329
+ page: iddata.p
330
330
  /**
331
- * 跳转块用**用户发出来的那条**(通常是 b23.tv 短链):
332
- * 上面那条规范链接带 ?p= 参数,长度上去了 App 剪贴板就认不出来。
331
+ * B站**不传 jumpUrl**:跳转块固定用 `www.bilibili.com/video/<bvid>` 这种标准形态
332
+ * (b23.tv 短链 App 复制了不跳转),由面板自己按 bvid + 分P 拼,见 `bilibiliShareUrl`。
333
333
  */
334
- jumpUrl: String(url)
335
334
  }, flags)) {
336
335
  return;
337
336
  }
@@ -401,13 +401,18 @@ function douyinShareUrl(awemeId, detailShareUrl) {
401
401
  return String(detailShareUrl ?? '') || ('https://www.douyin.com/video/' + String(awemeId));
402
402
  }
403
403
  /**
404
- * B站作品的跳转链接。
404
+ * B站的跳转链接:**固定用标准视频页** `https://www.bilibili.com/video/<bvid>`。
405
405
  *
406
- * 用 **b23.tv 短链**(`https://b23.tv/<bvid>`):卡片右下角二维码走的就是这条,
407
- * 扫二维码和复制跳转是同一件事,两处链接必须一致。
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,不用带参数)
408
412
  */
409
- function bilibiliShareUrl(bvid) {
410
- return 'https://b23.tv/' + String(bvid);
413
+ function bilibiliShareUrl(bvid, page) {
414
+ const url = 'https://www.bilibili.com/video/' + String(bvid);
415
+ return page && page > 1 ? url + '?p=' + page : url;
411
416
  }
412
417
  /** 快手作品页链接(App 认的是自家域名下的链接,短链最终也是跳到这里) */
413
418
  function kuaishouShareUrl(photoId) {
@@ -1178,18 +1183,20 @@ async function sendQqParsePanel(e, request) {
1178
1183
  * B站 / 抖音给的是「复制后打开 XX 自动跳转」代码块:各家 App 都认剪贴板里的自家链接,
1179
1184
  * 不依赖 QQ 会不会把 markdown 链接渲染成可点的东西。快手之类仍给普通链接。
1180
1185
  *
1181
- * 链接**用用户原样发的那条**(`request.jumpUrl`),拼不出来才退回 `request.url`。
1182
- * 之前按「和卡片二维码一致」去用 `bilibiliShareUrl` / `douyinShareUrl`,实测行不通:
1183
- * 那种链接带一串 `xsec_token` / `u_code` 之类的参数,长到 App 的剪贴板识别压根不认。
1184
- * 反过来用户发出来的 `b23.tv/xxx`、`v.douyin.com/xxx` 本来就是 App 自己生成的分享形态,
1185
- * 最短也最容易被识别。二维码那边不受影响(扫码不吃 URL 长度),保持原来的链接不动。
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
+ * 二维码(卡片右下角)不受影响,和上面各平台一样保持自己的链接。
1186
1193
  */
1187
1194
  const linkStart = lines.length;
1188
1195
  if (runtime.config.qqPanelSourceLink !== false && request.url) {
1189
- // 优先用户原样发的那条:我们拼的链接偏长,App 剪贴板识别不了;App 自家分享的短链才稳
1190
1196
  const jumpUrl = request.jumpUrl || request.url;
1191
1197
  const block = request.platform === 'bilibili'
1192
- ? copyJumpBlock('b站', jumpUrl)
1198
+ // B站固定用 BV 规范链接(短链 App 不识别),分 P 靠 request.page 带上
1199
+ ? copyJumpBlock('b站', bilibiliShareUrl(String(request.id), request.page))
1193
1200
  : request.platform === 'douyin'
1194
1201
  ? copyJumpBlock('抖音', jumpUrl, DOUYIN_JUMP_TOKEN)
1195
1202
  : [sourceLink('打开原站', jumpUrl)];
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.XiaohongshuEmoji = exports.xiaohongshuProcessVideos = exports.xiaohongshuGetLivePhotoVideo = exports.Xiaohongshu = void 0;
7
7
  const node_fs_1 = __importDefault(require("node:fs"));
8
- const ImageSlice_1 = require("../../module/utils/ImageSlice");
9
8
  const QqPanel_1 = require("../../module/utils/QqPanel");
10
9
  const parseTip_1 = require("../../module/utils/parseTip");
11
10
  const date_fns_1 = require("date-fns");
@@ -227,33 +226,12 @@ class Xiaohongshu extends module_1.Base {
227
226
  node_karin_2.logger.mark('[小红书] 详情卡片已发送');
228
227
  }
229
228
  /**
230
- * **图文笔记的图片要发出来**(用户要求)。
229
+ * **图集只发一次,由下面 `sendContent.includes('image')` 那块统一负责**(含实况图处理)。
231
230
  *
232
- * 之前只发了详情卡片,正文里的图片一张都没发 —— 用户看到的就是
233
- * 「解析出来了图文,但图呢?」。这里和抖音图集一样:合并成**一条 markdown**
234
- * (图片紧贴渲染,一条消息装完整套图)。
231
+ * 这里以前还有一段「图文图片合并成一条 markdown 发出去」的兜底,它**不看 sendContent 开关**,
232
+ * 于是勾了「图片」的部署会收到**两套图集**:先一条 markdown 全套图,紧接着又一条合并转发。
233
+ * 现在删掉了 —— 图集由下面那块按开关发,关掉「图片」就真的不发了(原来关掉照样发一份)。
235
234
  */
236
- const noteImages = (noteCard.image_list ?? [])
237
- .map((image) => String(image?.url_default ?? image?.url ?? ''))
238
- .filter(Boolean);
239
- if (!noteCard.video && noteImages.length) {
240
- try {
241
- const mdMessage = await (0, QqPanel_1.buildMarkdownImageMessage)(noteImages, 420, (0, ImageSlice_1.platformOf)(this.e));
242
- if (mdMessage) {
243
- await this.e.reply(mdMessage);
244
- node_karin_2.logger.mark('[小红书] 图文图片已用一条 markdown 发送,共 ' + noteImages.length + ' 张');
245
- }
246
- else {
247
- // md 生成失败就逐张发,总比一张不发好
248
- for (const imageUrl of noteImages)
249
- await this.e.reply(node_karin_1.segment.image(imageUrl));
250
- node_karin_2.logger.mark('[小红书] md 生成失败,图文图片改为逐张发送,共 ' + noteImages.length + ' 张');
251
- }
252
- }
253
- catch (error) {
254
- node_karin_2.logger.warn('[小红书] 发送图文图片失败: ' + String(error?.message ?? error).slice(0, 120));
255
- }
256
- }
257
235
  /**
258
236
  * 带「先提示、再宽限」的超时包装(和图片发送那边同一套口径,见 ImageSlice 的 SEND_* 常量)。
259
237
  *
@@ -517,11 +495,15 @@ class Xiaohongshu extends module_1.Base {
517
495
  processedImages.push(...tipImg);
518
496
  }
519
497
  const res = node_karin_1.common.makeForward(processedImages, Config_1.Config.app.fakeForward ? this.e.sender.userId : this.e.bot.account.selfId, Config_1.Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name);
520
- if (processedImages.length === 1) {
521
- await this.e.reply(processedImages[0]);
522
- }
523
- else if (processedImages.length > 1) {
524
- try {
498
+ /**
499
+ * 发图集是**唯一一次**发送图集的地方(另一处已删,见上面注释),所以失败要记进 steps:
500
+ * 让它只跳过这一步、照常走后面的视频 / 错误卡片,而不是把整条解析掀翻。
501
+ */
502
+ try {
503
+ if (processedImages.length === 1) {
504
+ await this.e.reply(processedImages[0]);
505
+ }
506
+ else if (processedImages.length > 1) {
525
507
  await this.e.bot.sendForwardMsg(this.e.contact, res, {
526
508
  source: '图片合集',
527
509
  summary: `查看${res.length}张图片/视频消息`,
@@ -529,10 +511,15 @@ class Xiaohongshu extends module_1.Base {
529
511
  news: [{ text: '点击查看解析结果' }]
530
512
  });
531
513
  }
532
- finally {
533
- for (const item of temp) {
534
- await module_1.Common.removeFile(item.filepath, true);
535
- }
514
+ node_karin_2.logger.mark('[小红书] 图集已发送,共 ' + processedImages.length + ' 张');
515
+ }
516
+ catch (error) {
517
+ steps.fail('发送图集', error);
518
+ }
519
+ finally {
520
+ // 临时文件(实况图生成的视频 / 静态图 / 合成封面)用完就删,失败也要删
521
+ for (const item of temp) {
522
+ await module_1.Common.removeFile(item.filepath, true);
536
523
  }
537
524
  }
538
525
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-kkk",
3
- "version": "3.10.0",
3
+ "version": "3.10.2",
4
4
  "description": "抖音 / B站 / 快手 / 小红书 视频解析与动态推送,带 QQ 交互面板与弹幕烧录(karin-plugin-kkk 的 Koishi 移植版)",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -319,12 +319,11 @@ const handleBilibili = withParseForward(wrapWithErrorHandler(
319
319
  platform: 'bilibili',
320
320
  url: 'https://www.bilibili.com/video/' + iddata.bvid + (iddata.p ? '?p=' + iddata.p : ''),
321
321
  id: String(iddata.bvid),
322
- page: iddata.p,
322
+ page: iddata.p
323
323
  /**
324
- * 跳转块用**用户发出来的那条**(通常是 b23.tv 短链):
325
- * 上面那条规范链接带 ?p= 参数,长度上去了 App 剪贴板就认不出来。
324
+ * B站**不传 jumpUrl**:跳转块固定用 `www.bilibili.com/video/<bvid>` 这种标准形态
325
+ * (b23.tv 短链 App 复制了不跳转),由面板自己按 bvid + 分P 拼,见 `bilibiliShareUrl`。
326
326
  */
327
- jumpUrl: String(url)
328
327
  }, flags)
329
328
  ) {
330
329
  return
@@ -407,13 +407,18 @@ export function douyinShareUrl (awemeId: string, detailShareUrl?: string): strin
407
407
  }
408
408
 
409
409
  /**
410
- * B站作品的跳转链接。
410
+ * B站的跳转链接:**固定用标准视频页** `https://www.bilibili.com/video/<bvid>`。
411
411
  *
412
- * 用 **b23.tv 短链**(`https://b23.tv/<bvid>`):卡片右下角二维码走的就是这条,
413
- * 扫二维码和复制跳转是同一件事,两处链接必须一致。
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,不用带参数)
414
418
  */
415
- export function bilibiliShareUrl (bvid: string): string {
416
- return 'https://b23.tv/' + String(bvid)
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
417
422
  }
418
423
 
419
424
  /** 快手作品页链接(App 认的是自家域名下的链接,短链最终也是跳到这里) */
@@ -1185,18 +1190,20 @@ export async function sendQqParsePanel (e: Message, request: PanelRequest): Prom
1185
1190
  * B站 / 抖音给的是「复制后打开 XX 自动跳转」代码块:各家 App 都认剪贴板里的自家链接,
1186
1191
  * 不依赖 QQ 会不会把 markdown 链接渲染成可点的东西。快手之类仍给普通链接。
1187
1192
  *
1188
- * 链接**用用户原样发的那条**(`request.jumpUrl`),拼不出来才退回 `request.url`。
1189
- * 之前按「和卡片二维码一致」去用 `bilibiliShareUrl` / `douyinShareUrl`,实测行不通:
1190
- * 那种链接带一串 `xsec_token` / `u_code` 之类的参数,长到 App 的剪贴板识别压根不认。
1191
- * 反过来用户发出来的 `b23.tv/xxx`、`v.douyin.com/xxx` 本来就是 App 自己生成的分享形态,
1192
- * 最短也最容易被识别。二维码那边不受影响(扫码不吃 URL 长度),保持原来的链接不动。
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
+ * 二维码(卡片右下角)不受影响,和上面各平台一样保持自己的链接。
1193
1200
  */
1194
1201
  const linkStart = lines.length
1195
1202
  if (runtime.config.qqPanelSourceLink !== false && request.url) {
1196
- // 优先用户原样发的那条:我们拼的链接偏长,App 剪贴板识别不了;App 自家分享的短链才稳
1197
1203
  const jumpUrl = request.jumpUrl || request.url
1198
1204
  const block = request.platform === 'bilibili'
1199
- ? copyJumpBlock('b站', jumpUrl)
1205
+ // B站固定用 BV 规范链接(短链 App 不识别),分 P 靠 request.page 带上
1206
+ ? copyJumpBlock('b站', bilibiliShareUrl(String(request.id), request.page))
1200
1207
  : request.platform === 'douyin'
1201
1208
  ? copyJumpBlock('抖音', jumpUrl, DOUYIN_JUMP_TOKEN)
1202
1209
  : [sourceLink('打开原站', jumpUrl)]
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs'
2
- import { platformOf } from '@/module/utils/ImageSlice'
3
- import { buildMarkdownImageMessage, sendCopyJumpMessage, xiaohongshuShareUrl } from '@/module/utils/QqPanel'
2
+ import { sendCopyJumpMessage, xiaohongshuShareUrl } from '@/module/utils/QqPanel'
4
3
  import { sendParseTip } from '@/module/utils/parseTip'
5
4
 
6
5
  import type { NoteComments, XiaohongshuEmojiListResponse } from '@ikenxuan/amagi'
@@ -274,30 +273,12 @@ export class Xiaohongshu extends Base {
274
273
  }
275
274
 
276
275
  /**
277
- * **图文笔记的图片要发出来**(用户要求)。
276
+ * **图集只发一次,由下面 `sendContent.includes('image')` 那块统一负责**(含实况图处理)。
278
277
  *
279
- * 之前只发了详情卡片,正文里的图片一张都没发 —— 用户看到的就是
280
- * 「解析出来了图文,但图呢?」。这里和抖音图集一样:合并成**一条 markdown**
281
- * (图片紧贴渲染,一条消息装完整套图)。
278
+ * 这里以前还有一段「图文图片合并成一条 markdown 发出去」的兜底,它**不看 sendContent 开关**,
279
+ * 于是勾了「图片」的部署会收到**两套图集**:先一条 markdown 全套图,紧接着又一条合并转发。
280
+ * 现在删掉了 —— 图集由下面那块按开关发,关掉「图片」就真的不发了(原来关掉照样发一份)。
282
281
  */
283
- const noteImages: string[] = (noteCard.image_list ?? [])
284
- .map((image: any) => String(image?.url_default ?? image?.url ?? ''))
285
- .filter(Boolean)
286
- if (!noteCard.video && noteImages.length) {
287
- try {
288
- const mdMessage = await buildMarkdownImageMessage(noteImages, 420, platformOf(this.e))
289
- if (mdMessage) {
290
- await this.e.reply(mdMessage)
291
- logger.mark('[小红书] 图文图片已用一条 markdown 发送,共 ' + noteImages.length + ' 张')
292
- } else {
293
- // md 生成失败就逐张发,总比一张不发好
294
- for (const imageUrl of noteImages) await this.e.reply(segment.image(imageUrl))
295
- logger.mark('[小红书] md 生成失败,图文图片改为逐张发送,共 ' + noteImages.length + ' 张')
296
- }
297
- } catch (error: any) {
298
- logger.warn('[小红书] 发送图文图片失败: ' + String(error?.message ?? error).slice(0, 120))
299
- }
300
- }
301
282
 
302
283
  /**
303
284
  * 带「先提示、再宽限」的超时包装(和图片发送那边同一套口径,见 ImageSlice 的 SEND_* 常量)。
@@ -585,20 +566,28 @@ export class Xiaohongshu extends Base {
585
566
  Config.app.fakeForward ? this.e.sender.nick : this.e.bot.account.name
586
567
  )
587
568
 
588
- if (processedImages.length === 1) {
589
- await this.e.reply(processedImages[0])
590
- } else if (processedImages.length > 1) {
591
- try {
569
+ /**
570
+ * 发图集是**唯一一次**发送图集的地方(另一处已删,见上面注释),所以失败要记进 steps:
571
+ * 让它只跳过这一步、照常走后面的视频 / 错误卡片,而不是把整条解析掀翻。
572
+ */
573
+ try {
574
+ if (processedImages.length === 1) {
575
+ await this.e.reply(processedImages[0])
576
+ } else if (processedImages.length > 1) {
592
577
  await this.e.bot.sendForwardMsg(this.e.contact, res, {
593
578
  source: '图片合集',
594
579
  summary: `查看${res.length}张图片/视频消息`,
595
580
  prompt: '小红书图集解析结果',
596
581
  news: [{ text: '点击查看解析结果' }]
597
582
  })
598
- } finally {
599
- for (const item of temp) {
600
- await Common.removeFile(item.filepath, true)
601
- }
583
+ }
584
+ logger.mark('[小红书] 图集已发送,共 ' + processedImages.length + ' 张')
585
+ } catch (error: any) {
586
+ steps.fail('发送图集', error)
587
+ } finally {
588
+ // 临时文件(实况图生成的视频 / 静态图 / 合成封面)用完就删,失败也要删
589
+ for (const item of temp) {
590
+ await Common.removeFile(item.filepath, true)
602
591
  }
603
592
  }
604
593
  }