koishi-plugin-bilibili-notify 3.2.8-alpha.2 → 3.2.9-alpha.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/lib/index.js CHANGED
@@ -39,6 +39,7 @@ const path = __toESM$1(require("path"));
39
39
  const qrcode = __toESM$1(require("qrcode"));
40
40
  const cron = __toESM$1(require("cron"));
41
41
  const luxon = __toESM$1(require("luxon"));
42
+ const segmentit = __toESM$1(require("segmentit"));
42
43
  const __satorijs_element_jsx_runtime = __toESM$1(require("@satorijs/element/jsx-runtime"));
43
44
  const node_path = __toESM$1(require("node:path"));
44
45
  const node_url = __toESM$1(require("node:url"));
@@ -646,6 +647,7 @@ var ComRegister = class {
646
647
  privateBot;
647
648
  dynamicJob;
648
649
  liveJob;
650
+ _segmentit = (0, segmentit.useDefault)(new segmentit.Segment());
649
651
  constructor(ctx, config) {
650
652
  this.ctx = ctx;
651
653
  this.init(config);
@@ -1221,15 +1223,26 @@ var ComRegister = class {
1221
1223
  const msg = /* @__PURE__ */ (0, __satorijs_element_jsx_runtime.jsxs)("message", { children: [koishi.h.image(buffer, "image/jpeg"), liveNotifyMsg || ""] });
1222
1224
  return await this.broadcastToTargets(uid, msg, liveType === LiveType.StartBroadcasting ? PushType.StartBroadcasting : PushType.Live);
1223
1225
  }
1226
+ async segmentDanmaku(danmaku, danmakuWeightRecord) {
1227
+ this._segmentit.doSegment(danmaku).map(({ w: w$3, p: p$1 }) => {
1228
+ if (p$1 && p$1 === 2048) return;
1229
+ danmakuWeightRecord[w$3] = (danmakuWeightRecord[w$3] || 0) + 1;
1230
+ });
1231
+ }
1224
1232
  async liveDetectWithListener(roomId, uid, cardStyle) {
1225
1233
  let liveTime;
1226
1234
  let pushAtTimeTimer;
1227
- const currentLiveDanmakuArr = [];
1235
+ const danmakuWeightRecord = {};
1228
1236
  let liveStatus = false;
1229
1237
  let liveRoomInfo;
1230
1238
  let masterInfo;
1231
1239
  let watchedNum;
1232
1240
  const liveMsgObj = this.liveMsgManager.get(uid);
1241
+ const sendDanmakuWordCloud = async () => {
1242
+ const top50Words = Object.entries(danmakuWeightRecord).sort((a$1, b$2) => b$2[1] - a$1[1]).slice(0, 50);
1243
+ const buffer = await this.ctx.gi.generateWordCloudImg(top50Words, masterInfo.username);
1244
+ await this.broadcastToTargets(uid, koishi.h.image(buffer, "image/jpeg"), PushType.Live);
1245
+ };
1233
1246
  const pushAtTimeFunc = async () => {
1234
1247
  if (!await useMasterAndLiveRoomInfo(LiveType.LiveBroadcast)) {
1235
1248
  await this.sendPrivateMsg("获取直播间信息失败,推送直播卡片失败!");
@@ -1275,10 +1288,10 @@ var ComRegister = class {
1275
1288
  this.logger.error(`[${roomId}]直播间连接发生错误!`);
1276
1289
  },
1277
1290
  onIncomeDanmu: ({ body }) => {
1278
- currentLiveDanmakuArr.push(body.content);
1291
+ this.segmentDanmaku(body.content, danmakuWeightRecord);
1279
1292
  },
1280
1293
  onIncomeSuperChat: ({ body }) => {
1281
- currentLiveDanmakuArr.push(body.content);
1294
+ this.segmentDanmaku(body.content, danmakuWeightRecord);
1282
1295
  },
1283
1296
  onWatchedChange: ({ body }) => {
1284
1297
  watchedNum = body.text_small;
@@ -1333,6 +1346,10 @@ var ComRegister = class {
1333
1346
  }, uid, liveEndMsg);
1334
1347
  pushAtTimeTimer();
1335
1348
  pushAtTimeTimer = null;
1349
+ const words = Object.entries(danmakuWeightRecord);
1350
+ const buffer = await this.ctx.gi.generateWordCloudImg(words, masterInfo.username);
1351
+ await this.broadcastToTargets(uid, koishi.h.image(buffer, "image/jpeg"), PushType.Live);
1352
+ await sendDanmakuWordCloud();
1336
1353
  }
1337
1354
  };
1338
1355
  await this.ctx.bl.startLiveRoomListener(roomId, handler);
@@ -94312,6 +94329,133 @@ var GenerateImg = class extends koishi.Service {
94312
94329
  throw new Error(`生成图片失败!错误: ${e$1.toString()}`);
94313
94330
  });
94314
94331
  }
94332
+ async generateWordCloudImg(words, masterName) {
94333
+ const html = `
94334
+ <!DOCTYPE html>
94335
+ <html lang="zh-CN">
94336
+
94337
+ <head>
94338
+ <meta charset="UTF-8">
94339
+ <title>高清词云展示</title>
94340
+ <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500&display=swap" rel="stylesheet">
94341
+ <style>
94342
+ * {
94343
+ margin: 0;
94344
+ padding: 0;
94345
+ box-sizing: border-box;
94346
+ }
94347
+
94348
+ html {
94349
+ width: 720px;
94350
+ height: 520px;
94351
+ }
94352
+
94353
+ .wordcloud-bg {
94354
+ width: 720px;
94355
+ height: 520px;
94356
+ background: linear-gradient(to right, #e0eafc, #cfdef3);
94357
+ font-family: 'Quicksand', sans-serif;
94358
+ display: flex;
94359
+ justify-content: center;
94360
+ align-items: center;
94361
+ }
94362
+
94363
+ .wordcloud-card {
94364
+ width: 700px;
94365
+ height: 500px;
94366
+ backdrop-filter: blur(10px);
94367
+ background: rgba(255, 255, 255, 0.25);
94368
+ border-radius: 20px;
94369
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
94370
+ padding: 20px;
94371
+ display: flex;
94372
+ flex-direction: column;
94373
+ align-items: center;
94374
+ justify-content: center;
94375
+ }
94376
+
94377
+ h2 {
94378
+ margin: 0 0 10px;
94379
+ color: #333;
94380
+ font-size: 24px;
94381
+ }
94382
+
94383
+ canvas {
94384
+ width: 100%;
94385
+ height: 100%;
94386
+ display: block;
94387
+ }
94388
+ </style>
94389
+ </head>
94390
+
94391
+ <body>
94392
+ <div class="wordcloud-bg">
94393
+ <div class="wordcloud-card">
94394
+ <h2>${masterName}直播弹幕词云</h2>
94395
+ <canvas id="wordCloudCanvas"></canvas>
94396
+ </div>
94397
+ </div>
94398
+
94399
+ <script src="https://cdn.jsdelivr.net/npm/wordcloud@1.1.2/src/wordcloud2.min.js"></script>
94400
+ <script>
94401
+ const canvas = document.getElementById('wordCloudCanvas');
94402
+ const ctx = canvas.getContext('2d');
94403
+
94404
+ // 获取 CSS 大小
94405
+ const style = getComputedStyle(canvas);
94406
+ const cssWidth = parseInt(style.width);
94407
+ const cssHeight = parseInt(style.height);
94408
+ const ratio = window.devicePixelRatio || 1;
94409
+
94410
+ // 设置 canvas 分辨率 & 缩放
94411
+ canvas.width = cssWidth * ratio;
94412
+ canvas.height = cssHeight * ratio;
94413
+ ctx.scale(ratio, ratio);
94414
+
94415
+ const words = ${JSON.stringify(words)}
94416
+
94417
+ // 词云大小缩放
94418
+ const maxWeight = Math.max(...words.map(w => w[1]));
94419
+ const minWeight = Math.min(...words.map(w => w[1]));
94420
+
94421
+ // 设置最大字体大小、最小字体大小(以像素为单位)
94422
+ const maxFontSize = 60;
94423
+ const minFontSize = 14;
94424
+
94425
+ // 用映射函数代替 weightFactor
94426
+ function getWeightFactor(size) {
94427
+ if (maxWeight === minWeight) return maxFontSize; // 防止除0
94428
+ const ratio = (size - minWeight) / (maxWeight - minWeight);
94429
+ return minFontSize + (maxFontSize - minFontSize) * ratio;
94430
+ }
94431
+
94432
+ WordCloud(canvas, {
94433
+ list: words,
94434
+ gridSize: Math.round(8 * (cssWidth / 1024)), // 自动适配大小
94435
+ weightFactor: getWeightFactor,
94436
+ fontFamily: 'Quicksand, sans-serif',
94437
+ color: () => {
94438
+ const colors = ['#007CF0', '#00DFD8', '#7928CA', '#FF0080', '#FF4D4D', '#F9CB28'];
94439
+ return colors[Math.floor(Math.random() * colors.length)];
94440
+ },
94441
+ rotateRatio: 0.5,
94442
+ rotationSteps: 2,
94443
+ backgroundColor: 'transparent',
94444
+ drawOutOfBound: false,
94445
+ origin: [cssWidth / 2, cssHeight / 2], // 居中关键点
94446
+ // 明确告诉 wordcloud2 使用这个宽高(以 CSS 尺寸为准)
94447
+ width: cssWidth,
94448
+ height: cssHeight,
94449
+ });
94450
+ </script>
94451
+ </body>
94452
+
94453
+ </html>
94454
+ `;
94455
+ return await withRetry(() => this.imgHandler(html)).catch((e$1) => {
94456
+ throw new Error(`生成图片失败!错误: ${e$1.toString()}`);
94457
+ });
94458
+ }
94315
94459
  async getLiveStatus(time, liveStatus) {
94316
94460
  let titleStatus;
94317
94461
  let liveTime;
@@ -94675,7 +94819,10 @@ var BiliAPI = class extends koishi.Service {
94675
94819
  return data$1;
94676
94820
  }
94677
94821
  async getCookieInfo(refreshToken) {
94678
- const { data: data$1 } = await this.client.get(`${GET_COOKIES_INFO}?csrf=${refreshToken}`);
94822
+ const { data: data$1 } = await this.client.get(`${GET_COOKIES_INFO}?csrf=${refreshToken}`).catch((e$1) => {
94823
+ this.logger.info(e$1.message);
94824
+ return null;
94825
+ });
94679
94826
  return data$1;
94680
94827
  }
94681
94828
  async getUserInfo(mid) {
@@ -94898,7 +95045,7 @@ var BiliAPI = class extends koishi.Service {
94898
95045
  };
94899
95046
  try {
94900
95047
  const { data: data$1 } = await this.getCookieInfo(refreshToken);
94901
- if (!data$1.refresh) return;
95048
+ if (!data$1?.refresh) return;
94902
95049
  } catch (_$2) {
94903
95050
  if (times$1 >= 1) this.ctx.setTimeout(() => {
94904
95051
  this.checkIfTokenNeedRefresh(refreshToken, csrf, times$1 - 1);
package/lib/index.mjs CHANGED
@@ -4,6 +4,7 @@ import { resolve } from "path";
4
4
  import QRCode from "qrcode";
5
5
  import { CronJob } from "cron";
6
6
  import { DateTime } from "luxon";
7
+ import { Segment, useDefault } from "segmentit";
7
8
  import { Fragment, jsx, jsxs } from "@satorijs/element/jsx-runtime";
8
9
  import { resolve as resolve$1 } from "node:path";
9
10
  import { pathToFileURL } from "node:url";
@@ -648,6 +649,7 @@ var ComRegister = class {
648
649
  privateBot;
649
650
  dynamicJob;
650
651
  liveJob;
652
+ _segmentit = useDefault(new Segment());
651
653
  constructor(ctx, config) {
652
654
  this.ctx = ctx;
653
655
  this.init(config);
@@ -1223,15 +1225,26 @@ var ComRegister = class {
1223
1225
  const msg = /* @__PURE__ */ jsxs("message", { children: [h.image(buffer, "image/jpeg"), liveNotifyMsg || ""] });
1224
1226
  return await this.broadcastToTargets(uid, msg, liveType === LiveType.StartBroadcasting ? PushType.StartBroadcasting : PushType.Live);
1225
1227
  }
1228
+ async segmentDanmaku(danmaku, danmakuWeightRecord) {
1229
+ this._segmentit.doSegment(danmaku).map(({ w: w$3, p: p$1 }) => {
1230
+ if (p$1 && p$1 === 2048) return;
1231
+ danmakuWeightRecord[w$3] = (danmakuWeightRecord[w$3] || 0) + 1;
1232
+ });
1233
+ }
1226
1234
  async liveDetectWithListener(roomId, uid, cardStyle) {
1227
1235
  let liveTime;
1228
1236
  let pushAtTimeTimer;
1229
- const currentLiveDanmakuArr = [];
1237
+ const danmakuWeightRecord = {};
1230
1238
  let liveStatus = false;
1231
1239
  let liveRoomInfo;
1232
1240
  let masterInfo;
1233
1241
  let watchedNum;
1234
1242
  const liveMsgObj = this.liveMsgManager.get(uid);
1243
+ const sendDanmakuWordCloud = async () => {
1244
+ const top50Words = Object.entries(danmakuWeightRecord).sort((a$1, b$2) => b$2[1] - a$1[1]).slice(0, 50);
1245
+ const buffer = await this.ctx.gi.generateWordCloudImg(top50Words, masterInfo.username);
1246
+ await this.broadcastToTargets(uid, h.image(buffer, "image/jpeg"), PushType.Live);
1247
+ };
1235
1248
  const pushAtTimeFunc = async () => {
1236
1249
  if (!await useMasterAndLiveRoomInfo(LiveType.LiveBroadcast)) {
1237
1250
  await this.sendPrivateMsg("获取直播间信息失败,推送直播卡片失败!");
@@ -1277,10 +1290,10 @@ var ComRegister = class {
1277
1290
  this.logger.error(`[${roomId}]直播间连接发生错误!`);
1278
1291
  },
1279
1292
  onIncomeDanmu: ({ body }) => {
1280
- currentLiveDanmakuArr.push(body.content);
1293
+ this.segmentDanmaku(body.content, danmakuWeightRecord);
1281
1294
  },
1282
1295
  onIncomeSuperChat: ({ body }) => {
1283
- currentLiveDanmakuArr.push(body.content);
1296
+ this.segmentDanmaku(body.content, danmakuWeightRecord);
1284
1297
  },
1285
1298
  onWatchedChange: ({ body }) => {
1286
1299
  watchedNum = body.text_small;
@@ -1335,6 +1348,10 @@ var ComRegister = class {
1335
1348
  }, uid, liveEndMsg);
1336
1349
  pushAtTimeTimer();
1337
1350
  pushAtTimeTimer = null;
1351
+ const words = Object.entries(danmakuWeightRecord);
1352
+ const buffer = await this.ctx.gi.generateWordCloudImg(words, masterInfo.username);
1353
+ await this.broadcastToTargets(uid, h.image(buffer, "image/jpeg"), PushType.Live);
1354
+ await sendDanmakuWordCloud();
1338
1355
  }
1339
1356
  };
1340
1357
  await this.ctx.bl.startLiveRoomListener(roomId, handler);
@@ -94314,6 +94331,133 @@ var GenerateImg = class extends Service {
94314
94331
  throw new Error(`生成图片失败!错误: ${e$1.toString()}`);
94315
94332
  });
94316
94333
  }
94334
+ async generateWordCloudImg(words, masterName) {
94335
+ const html = `
94336
+ <!DOCTYPE html>
94337
+ <html lang="zh-CN">
94338
+
94339
+ <head>
94340
+ <meta charset="UTF-8">
94341
+ <title>高清词云展示</title>
94342
+ <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500&display=swap" rel="stylesheet">
94343
+ <style>
94344
+ * {
94345
+ margin: 0;
94346
+ padding: 0;
94347
+ box-sizing: border-box;
94348
+ }
94349
+
94350
+ html {
94351
+ width: 720px;
94352
+ height: 520px;
94353
+ }
94354
+
94355
+ .wordcloud-bg {
94356
+ width: 720px;
94357
+ height: 520px;
94358
+ background: linear-gradient(to right, #e0eafc, #cfdef3);
94359
+ font-family: 'Quicksand', sans-serif;
94360
+ display: flex;
94361
+ justify-content: center;
94362
+ align-items: center;
94363
+ }
94364
+
94365
+ .wordcloud-card {
94366
+ width: 700px;
94367
+ height: 500px;
94368
+ backdrop-filter: blur(10px);
94369
+ background: rgba(255, 255, 255, 0.25);
94370
+ border-radius: 20px;
94371
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
94372
+ padding: 20px;
94373
+ display: flex;
94374
+ flex-direction: column;
94375
+ align-items: center;
94376
+ justify-content: center;
94377
+ }
94378
+
94379
+ h2 {
94380
+ margin: 0 0 10px;
94381
+ color: #333;
94382
+ font-size: 24px;
94383
+ }
94384
+
94385
+ canvas {
94386
+ width: 100%;
94387
+ height: 100%;
94388
+ display: block;
94389
+ }
94390
+ </style>
94391
+ </head>
94392
+
94393
+ <body>
94394
+ <div class="wordcloud-bg">
94395
+ <div class="wordcloud-card">
94396
+ <h2>${masterName}直播弹幕词云</h2>
94397
+ <canvas id="wordCloudCanvas"></canvas>
94398
+ </div>
94399
+ </div>
94400
+
94401
+ <script src="https://cdn.jsdelivr.net/npm/wordcloud@1.1.2/src/wordcloud2.min.js"></script>
94402
+ <script>
94403
+ const canvas = document.getElementById('wordCloudCanvas');
94404
+ const ctx = canvas.getContext('2d');
94405
+
94406
+ // 获取 CSS 大小
94407
+ const style = getComputedStyle(canvas);
94408
+ const cssWidth = parseInt(style.width);
94409
+ const cssHeight = parseInt(style.height);
94410
+ const ratio = window.devicePixelRatio || 1;
94411
+
94412
+ // 设置 canvas 分辨率 & 缩放
94413
+ canvas.width = cssWidth * ratio;
94414
+ canvas.height = cssHeight * ratio;
94415
+ ctx.scale(ratio, ratio);
94416
+
94417
+ const words = ${JSON.stringify(words)}
94418
+
94419
+ // 词云大小缩放
94420
+ const maxWeight = Math.max(...words.map(w => w[1]));
94421
+ const minWeight = Math.min(...words.map(w => w[1]));
94422
+
94423
+ // 设置最大字体大小、最小字体大小(以像素为单位)
94424
+ const maxFontSize = 60;
94425
+ const minFontSize = 14;
94426
+
94427
+ // 用映射函数代替 weightFactor
94428
+ function getWeightFactor(size) {
94429
+ if (maxWeight === minWeight) return maxFontSize; // 防止除0
94430
+ const ratio = (size - minWeight) / (maxWeight - minWeight);
94431
+ return minFontSize + (maxFontSize - minFontSize) * ratio;
94432
+ }
94433
+
94434
+ WordCloud(canvas, {
94435
+ list: words,
94436
+ gridSize: Math.round(8 * (cssWidth / 1024)), // 自动适配大小
94437
+ weightFactor: getWeightFactor,
94438
+ fontFamily: 'Quicksand, sans-serif',
94439
+ color: () => {
94440
+ const colors = ['#007CF0', '#00DFD8', '#7928CA', '#FF0080', '#FF4D4D', '#F9CB28'];
94441
+ return colors[Math.floor(Math.random() * colors.length)];
94442
+ },
94443
+ rotateRatio: 0.5,
94444
+ rotationSteps: 2,
94445
+ backgroundColor: 'transparent',
94446
+ drawOutOfBound: false,
94447
+ origin: [cssWidth / 2, cssHeight / 2], // 居中关键点
94448
+ // 明确告诉 wordcloud2 使用这个宽高(以 CSS 尺寸为准)
94449
+ width: cssWidth,
94450
+ height: cssHeight,
94451
+ });
94452
+ </script>
94453
+ </body>
94454
+
94455
+ </html>
94456
+ `;
94457
+ return await withRetry(() => this.imgHandler(html)).catch((e$1) => {
94458
+ throw new Error(`生成图片失败!错误: ${e$1.toString()}`);
94459
+ });
94460
+ }
94317
94461
  async getLiveStatus(time, liveStatus) {
94318
94462
  let titleStatus;
94319
94463
  let liveTime;
@@ -94677,7 +94821,10 @@ var BiliAPI = class extends Service {
94677
94821
  return data$1;
94678
94822
  }
94679
94823
  async getCookieInfo(refreshToken) {
94680
- const { data: data$1 } = await this.client.get(`${GET_COOKIES_INFO}?csrf=${refreshToken}`);
94824
+ const { data: data$1 } = await this.client.get(`${GET_COOKIES_INFO}?csrf=${refreshToken}`).catch((e$1) => {
94825
+ this.logger.info(e$1.message);
94826
+ return null;
94827
+ });
94681
94828
  return data$1;
94682
94829
  }
94683
94830
  async getUserInfo(mid) {
@@ -94900,7 +95047,7 @@ var BiliAPI = class extends Service {
94900
95047
  };
94901
95048
  try {
94902
95049
  const { data: data$1 } = await this.getCookieInfo(refreshToken);
94903
- if (!data$1.refresh) return;
95050
+ if (!data$1?.refresh) return;
94904
95051
  } catch (_$2) {
94905
95052
  if (times$1 >= 1) this.ctx.setTimeout(() => {
94906
95053
  this.checkIfTokenNeedRefresh(refreshToken, csrf, times$1 - 1);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-bilibili-notify",
3
3
  "description": "Koishi bilibili notify plugin",
4
- "version": "3.2.8-alpha.2",
4
+ "version": "3.2.9-alpha.1",
5
5
  "contributors": [
6
6
  "Akokko <admin@akokko.com>"
7
7
  ],
@@ -45,6 +45,7 @@
45
45
  "luxon": "^3.6.1",
46
46
  "md5": "^2.3.0",
47
47
  "qrcode": "^1.5.4",
48
+ "segmentit": "^2.0.3",
48
49
  "tough-cookie": "^5.1.2"
49
50
  },
50
51
  "devDependencies": {
package/readme.md CHANGED
@@ -299,6 +299,11 @@ uid为必填参数,为要推送的UP主的UID,index为可选参数,为要
299
299
  > - ver 3.2.8-alpha.1 修复:直播推送没有推送语;
300
300
  > - ver 3.2.8-alpha.2 优化:直播推送语中,会换行所有换行符而不是第一个,其余参数仍只会替换第一个
301
301
 
302
+ > [!CAUTION]
303
+ > - ver 3.2.9-alpha.0 新增:弹幕词云; 不建议更新,目前仅做测试用!
304
+ > - ver 3.2.9-alpha.1 修复:弹幕词云显示问题,弹幕过多导致插件爆炸; 不建议更新,目前仅做测试用!
305
+
306
+
302
307
  ## 交流群
303
308
 
304
309
  > [!TIP]