koishi-plugin-bns-rate 1.2.1 → 1.2.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 +30 -10
  2. package/index.js +94 -27
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  从 [dd373.com](https://www.dd373.com/s-d5gqt8.html) 获取剑灵怀旧服**神石**实时汇率,输出当前时段最低比例前 N 条。
4
4
 
5
- 支持**官方 QQ 机器人 Markdown 卡片**回复:标题 + 横幅图 + 红绿报价 + 跳转按钮,全部可在插件设置页配置。
5
+ 支持**官方 QQ 机器人 Markdown 卡片**回复:标题 + 横幅图 + 灰框报价 + 跳转按钮,全部可在插件设置页配置。
6
6
 
7
7
  ## 命令
8
8
 
@@ -10,25 +10,44 @@
10
10
  | --- | --- |
11
11
  | `bnsrate` / `神石` / `shenshi` | 查询当前时段最低神石比例 |
12
12
  | `bnsrate -f` | 忽略缓存,强制刷新 |
13
+ | `bnsrate.box` | **报价区排版对照**:一条消息里给出 code / quote / list / none 四种样式,在手机上挑一个填进 `ratesBox` |
13
14
  | `bnsrate.preview` | 只输出卡片 Markdown 源码(不发送卡片),调试用 |
14
15
 
15
16
  ## 卡片效果
16
17
 
17
18
  官方 QQ 机器人的**群聊 / 私聊**下,命令回复会变成 Markdown 卡片,形如:
18
19
 
19
- ```
20
+ ````
20
21
  # 神石比例实时播报
21
22
 
22
23
  ![DD373 DATA #560px #120px](<横幅图片直链>)
23
24
 
24
- 大家关注的 DD373 最低神石比例前5条来啦!当前时段(2026/9/10 10:46)最优价格如下:
25
+ 大家关注的 DD373 最低神石比例前5条来啦!当前时段(2026/9/10 11:03)最优价格如下:
26
+
27
+ ```
28
+ 1元 = 151.51 神石 | 1神石 = 0.0066元
29
+ 1元 = 149.54 神石 | 1神石 = 0.0067元
30
+ 1元 = 149.40 神石 | 1神石 = 0.0067元
31
+ ```
25
32
 
26
- 1元 = <font color="#E60012">151.51</font>神石 | 1神石 = <font color="#00A650">0.0066</font>元
27
- ...
28
- ***
29
33
  如需查看完整详情或进行交易,请点击下方按钮直接跳转至 DD373 官网。
30
34
  [ 点此直达DD373网站 ]
31
- ```
35
+ ````
36
+
37
+ 报价区默认用**代码块**包起来,在客户端渲染成**灰色框**(`ratesBox: code`)。代码块是等宽字体,插件会用空格把两列**对齐**(数字位数不同也对齐)。
38
+
39
+ ### 报价区排版可选四种(`ratesBox`)
40
+
41
+ | 值 | 效果 | QQ 官方文档支持 | 颜色 |
42
+ | --- | --- | --- | --- |
43
+ | **`code`(默认)** | 代码块 → 均匀灰底圆角框、等宽字体、两列对齐 | ❌ 不在[支持清单](https://bot.qq.com/wiki/develop/api-v2/server-inter/message/type/markdown.html)(客户端能力,安卓新版本 / iOS 才有) | ❌ 代码块内不支持 |
44
+ | `quote` | 块引用 → 灰底 + 左侧竖条 | ✅ 明确支持 | ✅ 桌面端 |
45
+ | `list` | 无序列表 → 无底色,缩进 + 圆点 | ✅ 明确支持 | ✅ 桌面端 |
46
+ | `none` | 不包裹,纯文本行 | — | ✅ 桌面端 |
47
+
48
+ 拿不准就在群里发 **`bnsrate.box`**:它会发一条含四种样式的对照卡片,挑一个填进设置页即可。若手机上 ` ``` ` 显示成了原文,说明该客户端不支持代码块,换成 `quote`。
49
+
50
+ > ⚠️ 手机端(安卓 / iOS)**不渲染 HTML 标签**:`<font color="#E60012">` 会**原样显示成一串字符**。因此默认的 `code` 模式**不输出任何颜色标签**;`quote` / `list` / `none` 会输出颜色(桌面端有颜色,手机端会露出标签,自行取舍)。
32
51
 
33
52
  其它平台(OneBot / Telegram 等)、QQ 频道(`qqguild`)、以及卡片发送失败时,都会**自动回退为纯文本**,不会发不出消息。
34
53
 
@@ -71,14 +90,15 @@
71
90
  | `button` | `true` | 是否显示底部跳转按钮 |
72
91
  | `buttonText` | `点此直达DD373网站` | 按钮文字 |
73
92
  | `buttonUrl` | `https://www.dd373.com/s-d5gqt8.html` | 按钮跳转链接 |
74
- | `colorForward` | `#E60012` | 「1元=xx神石」数字颜色,留空不染色 |
75
- | `colorReverse` | `#00A650` | 「1神石=xx元」数字颜色,留空不染色 |
93
+ | `ratesBox` | `code` | 报价区排版:`code` 代码块(灰框/等宽/对齐/无颜色)/`quote` 块引用/`list` 列表/`none` 不包裹。发 `bnsrate.box` 可看四种对照 |
94
+ | `colorForward` | `#E60012` | 「1元=xx神石」数字颜色,留空不染色。**仅 `ratesBox` 不是 `code` 时才生效** |
95
+ | `colorReverse` | `#00A650` | 「1神石=xx元」数字颜色,留空不染色。同上 |
76
96
  | `decimalsForward` | `2` | 「1元=xx神石」保留小数位(**截断**,不四舍五入),`0` = 原始值 |
77
97
  | `decimalsReverse` | `4` | 「1神石=xx元」保留小数位(截断),`0` = 原始值 |
78
98
 
79
99
  ## ⚠️ 注意事项
80
100
 
81
- 1. **颜色只在桌面端生效**。QQ 官方 Markdown 的[支持格式](https://bot.qq.com/wiki/develop/api-v2/server-inter/message/type/markdown.html)中没有字体颜色,颜色依赖 HTML 标签,**只有桌面端 QQNT 会渲染**;安卓 / iOS 会显示为普通文字(卡片本身不会出错)。
101
+ 1. **手机端不渲染 HTML 标签**。QQ 官方 Markdown 的[支持格式](https://bot.qq.com/wiki/develop/api-v2/server-inter/message/type/markdown.html)里没有字体颜色,颜色靠 HTML 标签实现,而**安卓 / iOS 会把这串标签当纯文本原样显示出来**(实测:手机上会看到 `<font color="#E60012">151.51</font>` 这样的字符)。所以默认 `ratesBox: code` 完全不输出颜色标签,用**代码块灰框 + 等宽对齐**来排版;想要桌面端红绿数字就把 `ratesBox` 改成 `quote` 或 `none`,代价是手机端会露出标签。
82
102
  2. **横幅图片必须是公网直链**,QQ 会下载并转存该资源。请勿使用官方的 `server-temp`(首次访问后即删除文件,群里多人同时查看会出现白图)。
83
103
  3. 群聊/单聊自定义 Markdown 自 2026/04/23 起已开放给所有机器人,**无需申请 Markdown 模板**;QQ 频道(`qqguild`)场景仍需内邀开通,本插件在频道下自动回退纯文本。
84
104
  4. 命令回复属被动消息,不占用主动消息配额。
package/index.js CHANGED
@@ -68,10 +68,13 @@ const Config = Schema.object({
68
68
 
69
69
  colorForward: Schema.string()
70
70
  .default('#E60012')
71
- .description('「1元=xx神石」数字的颜色,如 #E60012(红)。留空 = 不染色。⚠️ 颜色仅在桌面端 QQ 显示,安卓/iOS 会显示为普通文字'),
71
+ .description('「1元=xx神石」数字颜色,如 #E60012,留空即不染色。⚠️ 仅当 ratesBox 不是 code 时才有意义;手机端不渲染颜色,会把这串标签当文字显示出来'),
72
72
  colorReverse: Schema.string()
73
73
  .default('#00A650')
74
- .description('「1神石=xx元」数字的颜色,如 #00A650(绿)。留空 = 不染色'),
74
+ .description('「1神石=xx元」数字颜色,如 #00A650,留空即不染色。同上'),
75
+ ratesBox: Schema.union(['code', 'quote', 'list', 'none'])
76
+ .default('code')
77
+ .description('📦 报价区排版:code = 代码块(灰色框、等宽、两列对齐,但不支持颜色,且代码块不在 QQ 官方支持清单里);quote = 块引用(官方支持,灰底+左侧竖条,桌面端有颜色);list = 无序列表(官方支持,无底色);none = 不包裹。</br>拿不准就在群里发 bnsrate.box 看对照图,挑一个填这里'),
75
78
  decimalsForward: Schema.number()
76
79
  .default(2)
77
80
  .min(0)
@@ -191,19 +194,42 @@ function buildCardMarkdown(rates, timestamp, config) {
191
194
  }).trim()
192
195
  if (intro) lines.push('', intro)
193
196
 
194
- lines.push('', ...rates.map(r => (
195
- `1元 = ${colorize(truncate(r.forward, config.decimalsForward), config.colorForward)}神石 | `
196
- + `1神石 = ${colorize(truncate(r.reverse, config.decimalsReverse), config.colorReverse)}元`
197
- )))
197
+ lines.push('', ...buildRateBlock(rates, config))
198
198
 
199
199
  const footer = String(config.footer || '').trim()
200
- if (config.button && footer) {
201
- lines.push('', '***', '', footer)
202
- }
200
+ if (config.button && footer) lines.push('', footer)
203
201
 
204
202
  return lines.join('\n')
205
203
  }
206
204
 
205
+ // 报价区排版。box 省略时取 config.ratesBox
206
+ // code = 代码块(灰色框 / 等宽 / 两列对齐,代码块内不支持颜色)
207
+ // quote = 块引用(官方支持,灰底 + 左侧竖条,桌面端可上色)
208
+ // list = 无序列表(官方支持,无底色,缩进 + 圆点)
209
+ // none = 不加任何包裹
210
+ function buildRateBlock(rates, config, box) {
211
+ const mode = box || String(config.ratesBox || 'code')
212
+ const rows = rates.map(r => ({
213
+ fwd: truncate(r.forward, config.decimalsForward),
214
+ rev: truncate(r.reverse, config.decimalsReverse),
215
+ }))
216
+
217
+ if (mode === 'code') {
218
+ const wf = Math.max(...rows.map(r => r.fwd.length))
219
+ const wr = Math.max(...rows.map(r => r.rev.length))
220
+ const body = rows.map(r => `1元 = ${r.fwd.padEnd(wf)} 神石 | 1神石 = ${r.rev.padEnd(wr)}元`)
221
+ return ['```', ...body, '```']
222
+ }
223
+
224
+ const body = rows.map(r => (
225
+ `1元 = ${colorize(r.fwd, config.colorForward)}神石 | `
226
+ + `1神石 = ${colorize(r.rev, config.colorReverse)}元`
227
+ ))
228
+ if (mode === 'quote') return body.map(l => '> ' + l)
229
+ if (mode === 'list') return body.map(l => '- ' + l)
230
+ return body
231
+ }
232
+
207
233
  // —— QQ 官方 internal API 发送(单条 payload 内同时带 markdown 与 keyboard,避免被拆成两条消息)——
208
234
  // 与 koishi-plugin-ll-group-welcome / ll-schedule 用的是同一套已验证写法
209
235
  const KNOWN_IGNORE_CODES = new Set([11293, 40034101, 40034105, 304101, 304102])
@@ -228,12 +254,13 @@ function toPlainText(markdown, buttonUrl) {
228
254
  .replace(/!\[[^\]]*\]\([^)]*\)/g, '') // 去掉图片(含横幅)
229
255
  .replace(/<[^>]+>/g, '') // 去掉 <font color="..."> 等标签,保留其中文字
230
256
  .replace(/^#{1,6}\s*/gm, '')
257
+ .replace(/^\s*```.*$/gm, '') // 去掉代码块围栏
231
258
  .replace(/^\s*\*\*\*\s*$/gm, '') // 去掉水平分割线
232
259
  .replace(/\*\*([^*]+)\*\*/g, '$1')
233
260
  .replace(/\*([^*]+)\*/g, '$1')
234
261
  .replace(/`([^`]+)`/g, '$1')
235
262
  .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1: $2')
236
- .replace(/^>\s?/gm, '')
263
+ .replace(/^\s*>\s?/gm, '')
237
264
  .replace(/[ \t]+\n/g, '\n')
238
265
  .replace(/\n{3,}/g, '\n\n')
239
266
  .trim()
@@ -335,6 +362,27 @@ function apply(ctx, config) {
335
362
  }
336
363
  }
337
364
 
365
+ // 发卡片:带按钮 → 不带按钮(按钮能力/权限不可用时仍出卡片)→ 返回 false 让调用方回退纯文本
366
+ async function sendCard(session, markdown, cfg) {
367
+ const plain = toPlainText(markdown, cfg.button ? cfg.buttonUrl : '')
368
+ const passive = passiveMeta(session)
369
+ const hasButtons = !!(cfg.button && String(cfg.buttonUrl || '').trim())
370
+ for (const withButtons of (hasButtons ? [true, false] : [false])) {
371
+ try {
372
+ await sendQqCard(session, buildCardPayload(markdown, plain, cfg, passive, withButtons))
373
+ logger.debug(`已发送 Markdown 卡片到 ${session.channelId}${withButtons ? '(含按钮)' : '(无按钮)'}`)
374
+ return true
375
+ } catch (e) {
376
+ const code = errCode(e)
377
+ const detail = code !== undefined ? `code=${code}` : ((e && e.message) || e)
378
+ const level = isKnownIgnoreError(e) ? 'debug' : 'warn'
379
+ logger[level](`Markdown 卡片发送失败(${withButtons ? '含按钮' : '无按钮'}): ${detail}`)
380
+ }
381
+ }
382
+ logger.warn('Markdown 卡片全部尝试失败,回退纯文本')
383
+ return false
384
+ }
385
+
338
386
  ctx.command('bnsrate', '查询剑灵怀旧服神石汇率')
339
387
  .alias('神石')
340
388
  .alias('shenshi')
@@ -351,23 +399,7 @@ function apply(ctx, config) {
351
399
  // 官方QQ机器人的群聊/私聊:发 Markdown 卡片(markdown + 按钮在同一条消息里)
352
400
  if (config.markdown && session && supportsCard(session)) {
353
401
  const markdown = buildCardMarkdown(data.rates, data.timestamp, config)
354
- const plain = toPlainText(markdown, config.button ? config.buttonUrl : '')
355
- const passive = passiveMeta(session)
356
- const hasButtons = !!(config.button && String(config.buttonUrl || '').trim())
357
- // 依次尝试:带按钮 → 不带按钮(按钮能力/权限不可用时仍出卡片),都失败才回退纯文本
358
- for (const withButtons of (hasButtons ? [true, false] : [false])) {
359
- try {
360
- await sendQqCard(session, buildCardPayload(markdown, plain, config, passive, withButtons))
361
- logger.debug(`已发送 Markdown 卡片到 ${session.channelId}${withButtons ? '(含按钮)' : '(无按钮)'}`)
362
- return
363
- } catch (e) {
364
- const code = errCode(e)
365
- const detail = code !== undefined ? `code=${code}` : ((e && e.message) || e)
366
- const level = isKnownIgnoreError(e) ? 'debug' : 'warn'
367
- logger[level](`Markdown 卡片发送失败(${withButtons ? '含按钮' : '无按钮'}): ${detail}`)
368
- }
369
- }
370
- logger.warn('Markdown 卡片全部尝试失败,回退纯文本')
402
+ if (await sendCard(session, markdown, config)) return
371
403
  }
372
404
 
373
405
  return formatText(data.rates, data.timestamp, { stale: data.stale, error: data.error })
@@ -384,6 +416,40 @@ function apply(ctx, config) {
384
416
  return '获取汇率失败: ' + message
385
417
  }
386
418
  })
419
+
420
+ // 排版对照:一条消息里同时给出 4 种报价区排版,在手机上挑一个填进 ratesBox
421
+ ctx.command('bnsrate.box', '报价区排版对照:一条消息看 4 种样式,挑一个填到 ratesBox')
422
+ .action(async ({ session }) => {
423
+ let data
424
+ try {
425
+ data = await resolveRates(false)
426
+ } catch (e) {
427
+ const message = e instanceof Error ? e.message : String(e)
428
+ return '获取汇率失败: ' + message
429
+ }
430
+ const sample = data.rates.slice(0, 2)
431
+ const parts = [
432
+ '# 报价区排版对照',
433
+ '',
434
+ '把下面最好看的那一项填到插件设置页的 ratesBox:',
435
+ ]
436
+ const labels = {
437
+ code: 'A. code —— 代码块(灰色框 / 等宽 / 两列对齐 / 无颜色)',
438
+ quote: 'B. quote —— 块引用(灰底 + 左侧竖条 / 桌面端有颜色)',
439
+ list: 'C. list —— 无序列表(无底色 / 缩进 + 圆点)',
440
+ none: 'D. none —— 不包裹(纯文本行)',
441
+ }
442
+ for (const box of ['code', 'quote', 'list', 'none']) {
443
+ parts.push('', `## ${labels[box]}`, '', ...buildRateBlock(sample, config, box))
444
+ }
445
+
446
+ const markdown = parts.join('\n')
447
+ // 对照图不需要按钮,避免误触
448
+ if (session && supportsCard(session)) {
449
+ if (await sendCard(session, markdown, { ...config, button: false })) return
450
+ }
451
+ return markdown
452
+ })
387
453
  }
388
454
 
389
455
  module.exports = {
@@ -394,6 +460,7 @@ module.exports = {
394
460
  formatText,
395
461
  buildCardMarkdown,
396
462
  buildCardPayload,
463
+ buildRateBlock,
397
464
  buildKeyboard,
398
465
  toPlainText,
399
466
  passiveMeta,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-bns-rate",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Fetch BNS Classic Divine Stone exchange rate from dd373.com, with QQ official Markdown card support",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -17,8 +17,8 @@
17
17
  },
18
18
  "koishi": {
19
19
  "description": {
20
- "zh": "从 dd373.com 获取剑灵怀旧服神石实时汇率,输入 神石/bnsrate/shenshi 即可查询最低比例。支持官方QQ机器人 Markdown 卡片(标题+横幅图+红绿报价+跳转按钮),横幅图片与配色均可在插件设置页配置。",
21
- "en": "Fetch BNS Classic Divine Stone exchange rate from dd373.com. Supports QQ official bot Markdown cards (title, banner image, colored rates, jump button), all configurable."
20
+ "zh": "从 dd373.com 获取剑灵怀旧服神石实时汇率,输入 神石/bnsrate/shenshi 即可查询最低比例。支持官方QQ机器人 Markdown 卡片:标题 + 横幅图 + 代码块灰框报价(等宽对齐)+ 跳转按钮,正文与按钮同一条消息;横幅图片、外框样式、配色、小数位均可在插件设置页配置。",
21
+ "en": "Fetch BNS Classic Divine Stone exchange rate from dd373.com. QQ official bot Markdown card: title, banner image, gray code-box rate table, jump button all in a single message and fully configurable."
22
22
  },
23
23
  "service": {
24
24
  "required": ["http"]