koishi-plugin-vsinger-gacha 1.1.1 → 1.1.3

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.
Files changed (3) hide show
  1. package/README.md +31 -25
  2. package/lib/index.js +41 -20
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -19,14 +19,12 @@ VSinger 收藏集抽卡插件(Koishi 4.x):六位角色的官方收藏集
19
19
  - **图鉴长图**:液态玻璃背景 + 官方「大隐藏/小隐藏」角标 + 镭射标,超大图自动分张防 OOM
20
20
  - **管理指令**:给卡(管理员可凭空发,普通用户须自己拥有)、设/加积分、查积分
21
21
 
22
- ## 安装
22
+ ## 安装(选择一种)
23
23
 
24
- 插件**自带全部素材**(卡池数据 + 423 张缩略图 + 官方角标),装完即用;图鉴渲染是唯一可选依赖。
25
-
26
- ### 方式 A:一条命令装(不用 npm 帐号)
24
+ ### 方式 A:npm 一行装(推荐)
27
25
 
28
26
  ```bash
29
- npm i https://github.com/zb23333ww/koishi-plugin-vsinger-gacha/releases/download/v1.0.0/koishi-plugin-vsinger-gacha-1.0.3.tgz
27
+ npm i koishi-plugin-vsinger-gacha
30
28
  ```
31
29
 
32
30
  `koishi.yml` 加一行后重启:
@@ -36,16 +34,16 @@ plugins:
36
34
  vsinger-gacha: {}
37
35
  ```
38
36
 
39
- ### 方式 B:下载 zip 手动放(完全不用网络命令)
40
-
41
- 下载 [koishi-plugin-vsinger-gacha.zip](https://github.com/zb23333ww/koishi-plugin-vsinger-gacha/releases/download/v1.0.0/koishi-plugin-vsinger-gacha.zip) → 解压 → 整个文件夹放进 Koishi 项目的 `node_modules/` → `koishi.yml` 加 `vsinger-gacha: {}` → 重启。
42
-
43
- ### 方式 C:npm 正式包(发布后可用)
37
+ ### 方式 B:下载 tgz 直装(npm 不可用时)
44
38
 
45
39
  ```bash
46
- npm i koishi-plugin-vsinger-gacha
40
+ npm i https://github.com/zb23333ww/koishi-plugin-vsinger-gacha/releases/latest/download/koishi-plugin-vsinger-gacha.tgz
47
41
  ```
48
42
 
43
+ ### 方式 C:下载 zip 手动放
44
+
45
+ [koishi-plugin-vsinger-gacha.zip](https://github.com/zb23333ww/koishi-plugin-vsinger-gacha/releases/latest/download/koishi-plugin-vsinger-gacha.zip) → 解压 → 整个文件夹放进 Koishi 的 `node_modules/` → 配置 → 重启。
46
+
49
47
  ### 图鉴(可选)
50
48
 
51
49
  `/抽卡`、`/签到`、积分、管理指令**零依赖**即可用。
@@ -63,6 +61,19 @@ npm i @napi-rs/canvas
63
61
  - 想一次性缓存全部:`npm run fetch-assets -- --dir data/vsinger-gacha/cards`
64
62
  - 要动态卡 mp4(1.1GB):下载 Release 的 `assets-videos.tar` 解压到 `cards/`
65
63
 
64
+ ## 兼容性
65
+
66
+ | 项 | 说明 |
67
+ | --- | --- |
68
+ | **Koishi** | `>=4.17.0 <6`(4.18 与 5.x RC 均验证过) |
69
+ | **Node** | `>=18` |
70
+ | **适配器** | 任意 Satori 适配器可用;OneBot 额外支持**合并转发**,其它适配器自动退回普通消息 |
71
+ | **发图** | 默认 `buffer`(base64,跨容器最稳);可切 `file`(适配器能读同机文件时更省内存) |
72
+ | **数据库** | 不需要,数据存 `data/vsinger-gacha/*.json` |
73
+ | **图鉴** | 需要可选依赖 `@napi-rs/canvas`;没装时其它功能不受影响 |
74
+ | **字体** | 包内自带中文子集,不依赖系统字体 |
75
+ | **合并转发** | 配置 `forwardMode: auto / off` |
76
+
66
77
  ## 指令
67
78
 
68
79
  | 指令 | 说明 |
@@ -119,25 +130,20 @@ npm i @napi-rs/canvas
119
130
  MIT
120
131
 
121
132
 
122
- ## 素材包(可选)
133
+ ## 素材库(卡图 + 动态卡)
123
134
 
124
- 仓库不含 1.37GB 原图与 1.06GB 动态卡,放在 Releases:
135
+ 插件包只带**缩略图**(图鉴用)+ 卡池数据,原图和动态卡单独放在 Releases 的「素材库」:
125
136
 
126
- | Release 附件 | 大小 | 内容 |
137
+ | 附件 | 大小 | 内容 |
127
138
  | --- | --- | --- |
128
- | `assets-cards.tar` | 1.6 GB | 423 张 PNG(1242×1863)+ 274 张镭射版 JPG |
129
- | `assets-videos.tar` | 1.1 GB | 145 个动态卡 mp4 |
130
-
131
- 解压到 `cardsDir` 即可,或用 `npm run fetch-assets` 重新抓取。
139
+ | [`assets-cards.tar`](https://github.com/zb23333ww/koishi-plugin-vsinger-gacha/releases/download/v1.0.0/assets-cards.tar) | 1.6 GB | 423 张官方原图(1242×1863)+ 274 张镭射版 |
140
+ | [`assets-videos.tar`](https://github.com/zb23333ww/koishi-plugin-vsinger-gacha/releases/download/v1.0.0/assets-videos.tar) | 1.1 GB | 145 个动态卡 mp4 |
132
141
 
133
- ## 开发
142
+ 解压到 `cardsDir`(默认 `data/vsinger-gacha/cards`):
134
143
 
135
144
  ```bash
136
- npm i
137
- node test/e2e.cjs # 需要 @koishijs/plugin-mock + @napi-rs/canvas
145
+ tar xf assets-cards.tar -C data/vsinger-gacha/cards
146
+ tar xf assets-videos.tar -C data/vsinger-gacha/cards
138
147
  ```
139
148
 
140
- ## 免责声明
141
-
142
- 卡图、卡池信息来自哔哩哔哩「收藏集」公开接口,版权归哔哩哔哩及原作者所有;
143
- 本插件仅用于个人学习与同好群内的收藏展示,请勿用于商业用途。
149
+ **不装也能用**:卡图按需单张自动下载(约 1MB/张),图鉴直接用包内缩略图。
package/lib/index.js CHANGED
@@ -31,6 +31,10 @@ const Config = Schema.intersect([
31
31
  .description('可选:openid ↔ QQ 映射表(如 /root/qqbot/data/id-map.json),用于统一用户标识'),
32
32
  debug: Schema.boolean().default(false)
33
33
  .description('调试模式:把收到的每条消息打到日志里'),
34
+ forwardMode: Schema.union(['auto', 'off']).default('auto')
35
+ .description('合并转发:auto=支持就转发、不支持自动退回普通消息;off=永远普通消息'),
36
+ imageMode: Schema.union(['auto', 'buffer', 'file']).default('auto')
37
+ .description('发图方式:auto=buffer(base64,跨容器最稳);file=发本地路径(需适配器能读同机文件)'),
34
38
  sendVideos: Schema.boolean().default(true)
35
39
  .description('动态卡单独发送 mp4(需要本地 videos 目录有对应文件)'),
36
40
  autoDownload: Schema.boolean().default(true)
@@ -114,16 +118,32 @@ const normUser = (raw, session) => {
114
118
  * ★ 不用 file:// 路径:机器人框架 / OneBot 实现在容器里读不到宿主机路径
115
119
  * → 直接用 buffer(Koishi 会编码成 data/base64,跨进程可用)
116
120
  */
117
- const fileEl = (file, mime) => {
121
+ /** 机器人自身 uin / 显示名(不同适配器字段不一样) */
122
+ function botUin(session) {
123
+ return String(session.selfId || (session.bot && session.bot.selfId) || (session.bot && session.bot.config && session.bot.config.selfId) || '');
124
+ }
125
+ function botName(session) {
126
+ return String((session.bot && (session.bot.username || session.bot.name)) || 'Bot');
127
+ }
128
+
129
+ function fileEl(file, mime, mode) {
118
130
  const mt = mime || (/\.jpe?g$/i.test(file) ? 'image/jpeg' : 'image/png');
131
+ if (mode === 'file') {
132
+ try {
133
+ return h.image(require('node:url').pathToFileURL(file).href);
134
+ } catch { /* 退回 buffer */ }
135
+ }
119
136
  return h.image(fs.readFileSync(file), mt);
120
- };
137
+ }
121
138
 
122
139
 
123
140
  /** 发合并转发(OneBot 私有 API);失败返回 false */
124
141
  async function sendForward(session, nodes) {
125
142
  const bot = session.bot;
126
- if (!bot || !bot.internal) return false;
143
+ if (!bot || !bot.internal) {
144
+ sendForward.lastError = 'adapter 不支持 internal API';
145
+ return false;
146
+ }
127
147
  const gid = session.guildId || session.channelId;
128
148
  if (session.isDirect || !gid) return false;
129
149
  const internal = bot.internal;
@@ -151,9 +171,10 @@ async function sendForward(session, nodes) {
151
171
  /** 本地视频 → 消息元素(OneBot 要求视频单独一条消息) */
152
172
  function videoEl(file) {
153
173
  const buf = fs.readFileSync(file);
154
- const b64 = 'data:video/mp4;base64,' + buf.toString('base64');
155
- if (typeof h.video === 'function') return h.video(buf, 'video/mp4');
156
- return h('video', { src: b64 });
174
+ if (typeof h.video === 'function') {
175
+ try { return h.video(buf, 'video/mp4'); } catch { /* 退回通用元素 */ }
176
+ }
177
+ return h('video', { src: 'data:video/mp4;base64,' + buf.toString('base64') });
157
178
  }
158
179
 
159
180
  /** 本地图片 → OneBot base64 段 */
@@ -294,17 +315,17 @@ function apply(ctx, config) {
294
315
  if (session.isDirect === false || session.guildId) {
295
316
  nodes.push({
296
317
  type: 'node',
297
- data: { name: ctx.bots?.[0]?.username || '洛天依', uin: String(session.selfId || ''), content: [{ type: 'text', data: { text: title } }] },
318
+ data: { name: botName(session), uin: botUin(session), content: [{ type: 'text', data: { text: title } }] },
298
319
  });
299
320
  results.forEach((r, i) => {
300
321
  const rate = r.card.rate_pct == null ? '' : ` ${r.card.rate_pct}%`;
301
322
  const desc = `${r.tier}|${r.card.card_name}${rate}${r.laser ? '|✦镭射版' : ''}${r.card.video ? '|动态' : ''}`;
302
323
  const content = [{ type: 'text', data: { text: desc } }];
303
324
  if (pics[i]) content.push(imgSeg(pics[i]));
304
- nodes.push({ type: 'node', data: { name: '洛天依', uin: String(session.selfId || ''), content } });
325
+ nodes.push({ type: 'node', data: { name: botName(session), uin: botUin(session), content } });
305
326
  });
306
327
  }
307
- const ok = nodes.length && await sendForward(session, nodes);
328
+ const ok = config.forwardMode !== 'off' && nodes.length && await sendForward(session, nodes);
308
329
  if (!ok && nodes.length) logger.warn('合并转发不可用(%s),退回普通发送', sendForward.lastError || 'unknown');
309
330
 
310
331
  // ★ 动态卡:单独发 mp4(OneBot 要求视频独占一条消息)
@@ -327,7 +348,7 @@ function apply(ctx, config) {
327
348
  await session.send(title + '\n' + lines.join('\n'));
328
349
  for (const f of pics) {
329
350
  if (!f) continue;
330
- try { await session.send(fileEl(f)); } catch (e) { logger.warn('发卡图失败:%s', e.message); }
351
+ try { await session.send(fileEl(f, null, config.imageMode)); } catch (e) { logger.warn('发卡图失败:%s', e.message); }
331
352
  }
332
353
  }
333
354
  return;
@@ -359,18 +380,18 @@ function apply(ctx, config) {
359
380
  }
360
381
  lines.push('', '切换:/换池 序号(00 = 全部卡池)', '说明:/抽卡帮助');
361
382
 
362
- const uin = String(session.selfId || '');
383
+ const uin = botUin(session);
363
384
  const nodes = [];
364
385
  const chunk = 18;
365
386
  for (let k = 0; k < lines.length; k += chunk) {
366
- nodes.push({ type: 'node', data: { name: '洛天依', uin, content: [{ type: 'text', data: { text: lines.slice(k, k + chunk).join('\n') } }] } });
387
+ nodes.push({ type: 'node', data: { name: botName(session), uin, content: [{ type: 'text', data: { text: lines.slice(k, k + chunk).join('\n') } }] } });
367
388
  }
368
389
  if (nodes.length < 2) {
369
- nodes.push({ type: 'node', data: { name: '洛天依', uin, content: [{ type: 'text', data: { text: '切换:/换池 序号(00 = 全部卡池)' } }] } });
390
+ nodes.push({ type: 'node', data: { name: botName(session), uin, content: [{ type: 'text', data: { text: '切换:/换池 序号(00 = 全部卡池)' } }] } });
370
391
  }
371
- const ok = await sendForward(session, nodes);
392
+ const ok = config.forwardMode !== 'off' && await sendForward(session, nodes);
372
393
  if (!ok) {
373
- logger.warn('卡池合并转发失败(%s),改发文本', sendForward.lastError);
394
+ if (config.forwardMode !== 'off') logger.warn('卡池合并转发失败(%s),改发文本', sendForward.lastError);
374
395
  return lines.join('\n');
375
396
  }
376
397
  return;
@@ -414,7 +435,7 @@ function apply(ctx, config) {
414
435
  const f = game.holoFile(c)
415
436
  || (config.autoDownload !== false ? await game.ensureImage(c) : (fs.existsSync(game.cardFile(c)) ? game.cardFile(c) : null));
416
437
  if (!f) return `「${c.v.card_name}」的卡图下载失败(网络问题),稍后再试`;
417
- await session.send(fileEl(f, /\.jpe?g$/i.test(f) ? 'image/jpeg' : 'image/png'));
438
+ await session.send(fileEl(f, /\.jpe?g$/i.test(f) ? 'image/jpeg' : 'image/png', config.imageMode));
418
439
  return `🎴 ${c.v.card_name}\n档位:${tierName(c.v)} | 概率:${c.v.rate_pct}% | 你拥有 ×${u.owned[c.h]}${c.v.video ? ' | 动态卡' : ''}`;
419
440
  });
420
441
 
@@ -431,10 +452,10 @@ function apply(ctx, config) {
431
452
  tagsDir,
432
453
  thumbsDir: path.join(assetsDir, 'thumbs'),
433
454
  });
434
- const nodes = [{ type: 'node', data: { name: '洛天依', uin: String(session.selfId || ''), content: [{ type: 'text', data: { text: `${session.username || '你'} 的收藏集图鉴(${files.length} 张)` } }] } }];
435
- for (const f of files) nodes.push({ type: 'node', data: { name: '洛天依', uin: String(session.selfId || ''), content: [imgSeg(f)] } });
436
- const ok = await sendForward(session, nodes);
437
- if (!ok) for (const f of files) await session.send(fileEl(f, 'image/jpeg'));
455
+ const nodes = [{ type: 'node', data: { name: botName(session), uin: botUin(session), content: [{ type: 'text', data: { text: `${session.username || '你'} 的收藏集图鉴(${files.length} 张)` } }] } }];
456
+ for (const f of files) nodes.push({ type: 'node', data: { name: botName(session), uin: botUin(session), content: [imgSeg(f)] } });
457
+ const ok = config.forwardMode !== 'off' && await sendForward(session, nodes);
458
+ if (!ok) for (const f of files) await session.send(fileEl(f, 'image/jpeg', config.imageMode));
438
459
  } catch (e) {
439
460
  logger.warn('图鉴失败:%s', e.stack || e.message);
440
461
  return `图鉴生成失败:${String(e.message).slice(0, 120)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-vsinger-gacha",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "VSinger 收藏集抽卡 / 签到积分 / 图鉴(Koishi 插件,一次性安装)",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -22,7 +22,7 @@
22
22
  ],
23
23
  "license": "MIT",
24
24
  "peerDependencies": {
25
- "koishi": "^4.17.0"
25
+ "koishi": ">=4.17.0 <6"
26
26
  },
27
27
  "koishi": {
28
28
  "description": {
@@ -47,4 +47,4 @@
47
47
  "optionalDependencies": {
48
48
  "@napi-rs/canvas": "^0.1.52"
49
49
  }
50
- }
50
+ }