koishi-plugin-bns-rate 1.2.2 → 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 +16 -6
  2. package/index.js +85 -37
  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,6 +10,7 @@
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
  ## 卡片效果
@@ -33,11 +34,20 @@
33
34
  [ 点此直达DD373网站 ]
34
35
  ````
35
36
 
36
- 报价列表默认用**代码块**包起来,在客户端渲染成**灰色框**(`ratesBox: code`)。代码块是等宽字体,插件会用空格把两列**对齐**(数字位数不同也对齐)。
37
+ 报价区默认用**代码块**包起来,在客户端渲染成**灰色框**(`ratesBox: code`)。代码块是等宽字体,插件会用空格把两列**对齐**(数字位数不同也对齐)。
37
38
 
38
- > ⚠️ 手机端(安卓 / iOS)**不渲染 HTML 标签**:`<font color="#E60012">` 会**原样显示成一串字符**。因此默认的 `code` 模式**不输出任何颜色标签**;只有把 `ratesBox` 改成 `quote` / `none` 时才会输出颜色(桌面端有颜色,手机端会露出标签,自行取舍)。
39
+ ### 报价区排版可选四种(`ratesBox`)
39
40
 
40
- 若手机上把 ` ``` ` 显示成了原文(不支持代码块),把 `ratesBox` 改成 `quote`,即可换成引用样式的外框。
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` 会输出颜色(桌面端有颜色,手机端会露出标签,自行取舍)。
41
51
 
42
52
  其它平台(OneBot / Telegram 等)、QQ 频道(`qqguild`)、以及卡片发送失败时,都会**自动回退为纯文本**,不会发不出消息。
43
53
 
@@ -80,8 +90,8 @@
80
90
  | `button` | `true` | 是否显示底部跳转按钮 |
81
91
  | `buttonText` | `点此直达DD373网站` | 按钮文字 |
82
92
  | `buttonUrl` | `https://www.dd373.com/s-d5gqt8.html` | 按钮跳转链接 |
83
- | `ratesBox` | `code` | 报价列表外框:`code` 代码块(灰色框、等宽、两列对齐、**无颜色**)/`quote` 引用(灰底+左侧竖条,桌面端可上色)/`none` 不加框 |
84
- | `colorForward` | `#E60012` | 「1元=xx神石」数字颜色,留空不染色。**仅在 `ratesBox != code` 时生效** |
93
+ | `ratesBox` | `code` | 报价区排版:`code` 代码块(灰框/等宽/对齐/无颜色)/`quote` 块引用/`list` 列表/`none` 不包裹。发 `bnsrate.box` 可看四种对照 |
94
+ | `colorForward` | `#E60012` | 「1元=xx神石」数字颜色,留空不染色。**仅 `ratesBox` 不是 `code` 时才生效** |
85
95
  | `colorReverse` | `#00A650` | 「1神石=xx元」数字颜色,留空不染色。同上 |
86
96
  | `decimalsForward` | `2` | 「1元=xx神石」保留小数位(**截断**,不四舍五入),`0` = 原始值 |
87
97
  | `decimalsReverse` | `4` | 「1神石=xx元」保留小数位(截断),`0` = 原始值 |
package/index.js CHANGED
@@ -68,13 +68,13 @@ const Config = Schema.object({
68
68
 
69
69
  colorForward: Schema.string()
70
70
  .default('#E60012')
71
- .description('「1元=xx神石」数字的颜色,如 #E60012(红)。留空 = 不染色。⚠️ ① 颜色只在桌面端 QQ 渲染;② 安卓/iOS 会把 <font> 标签当纯文本显示出来,所以仅在 ratesBox != code 时才有意义;③ 代码块内不支持颜色'),
71
+ .description('「1元=xx神石」数字颜色,如 #E60012,留空即不染色。⚠️ 仅当 ratesBox 不是 code 时才有意义;手机端不渲染颜色,会把这串标签当文字显示出来'),
72
72
  colorReverse: Schema.string()
73
73
  .default('#00A650')
74
- .description('「1神石=xx元」数字的颜色,如 #00A650(绿)。留空 = 不染色'),
75
- ratesBox: Schema.union(['code', 'quote', 'none'])
74
+ .description('「1神石=xx元」数字颜色,如 #00A650,留空即不染色。同上'),
75
+ ratesBox: Schema.union(['code', 'quote', 'list', 'none'])
76
76
  .default('code')
77
- .description('📦 报价列表的外框样式:code = 代码块(灰色框,等宽字体,行列对齐,不支持颜色);quote = 引用(灰底 + 左侧竖条,桌面端可显示颜色);none = 不加框。</br>若手机上把 ``` 显示成了原文,改成 quote 即可'),
77
+ .description('📦 报价区排版:code = 代码块(灰色框、等宽、两列对齐,但不支持颜色,且代码块不在 QQ 官方支持清单里);quote = 块引用(官方支持,灰底+左侧竖条,桌面端有颜色);list = 无序列表(官方支持,无底色);none = 不包裹。</br>拿不准就在群里发 bnsrate.box 看对照图,挑一个填这里'),
78
78
  decimalsForward: Schema.number()
79
79
  .default(2)
80
80
  .min(0)
@@ -194,32 +194,40 @@ function buildCardMarkdown(rates, timestamp, config) {
194
194
  }).trim()
195
195
  if (intro) lines.push('', intro)
196
196
 
197
- // 报价列表
198
- const box = String(config.ratesBox || 'code')
197
+ lines.push('', ...buildRateBlock(rates, config))
198
+
199
+ const footer = String(config.footer || '').trim()
200
+ if (config.button && footer) lines.push('', footer)
201
+
202
+ return lines.join('\n')
203
+ }
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')
199
212
  const rows = rates.map(r => ({
200
213
  fwd: truncate(r.forward, config.decimalsForward),
201
214
  rev: truncate(r.reverse, config.decimalsReverse),
202
215
  }))
203
216
 
204
- if (box === 'code') {
205
- // 代码块 = 灰色框(等宽字体,正好可以用空格把两列对齐)
206
- // ⚠️ 代码块内部不支持颜色,塞 <font> 标签只会被原样显示,所以这里不加颜色
217
+ if (mode === 'code') {
207
218
  const wf = Math.max(...rows.map(r => r.fwd.length))
208
219
  const wr = Math.max(...rows.map(r => r.rev.length))
209
220
  const body = rows.map(r => `1元 = ${r.fwd.padEnd(wf)} 神石 | 1神石 = ${r.rev.padEnd(wr)}元`)
210
- lines.push('', '```', ...body, '```')
211
- } else {
212
- const body = rows.map(r => (
213
- `1元 = ${colorize(r.fwd, config.colorForward)}神石 | `
214
- + `1神石 = ${colorize(r.rev, config.colorReverse)}元`
215
- ))
216
- lines.push('', ...(box === 'quote' ? body.map(l => '> ' + l) : body))
221
+ return ['```', ...body, '```']
217
222
  }
218
223
 
219
- const footer = String(config.footer || '').trim()
220
- if (config.button && footer) lines.push('', footer)
221
-
222
- return lines.join('\n')
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
223
231
  }
224
232
 
225
233
  // —— QQ 官方 internal API 发送(单条 payload 内同时带 markdown 与 keyboard,避免被拆成两条消息)——
@@ -354,6 +362,27 @@ function apply(ctx, config) {
354
362
  }
355
363
  }
356
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
+
357
386
  ctx.command('bnsrate', '查询剑灵怀旧服神石汇率')
358
387
  .alias('神石')
359
388
  .alias('shenshi')
@@ -370,23 +399,7 @@ function apply(ctx, config) {
370
399
  // 官方QQ机器人的群聊/私聊:发 Markdown 卡片(markdown + 按钮在同一条消息里)
371
400
  if (config.markdown && session && supportsCard(session)) {
372
401
  const markdown = buildCardMarkdown(data.rates, data.timestamp, config)
373
- const plain = toPlainText(markdown, config.button ? config.buttonUrl : '')
374
- const passive = passiveMeta(session)
375
- const hasButtons = !!(config.button && String(config.buttonUrl || '').trim())
376
- // 依次尝试:带按钮 → 不带按钮(按钮能力/权限不可用时仍出卡片),都失败才回退纯文本
377
- for (const withButtons of (hasButtons ? [true, false] : [false])) {
378
- try {
379
- await sendQqCard(session, buildCardPayload(markdown, plain, config, passive, withButtons))
380
- logger.debug(`已发送 Markdown 卡片到 ${session.channelId}${withButtons ? '(含按钮)' : '(无按钮)'}`)
381
- return
382
- } catch (e) {
383
- const code = errCode(e)
384
- const detail = code !== undefined ? `code=${code}` : ((e && e.message) || e)
385
- const level = isKnownIgnoreError(e) ? 'debug' : 'warn'
386
- logger[level](`Markdown 卡片发送失败(${withButtons ? '含按钮' : '无按钮'}): ${detail}`)
387
- }
388
- }
389
- logger.warn('Markdown 卡片全部尝试失败,回退纯文本')
402
+ if (await sendCard(session, markdown, config)) return
390
403
  }
391
404
 
392
405
  return formatText(data.rates, data.timestamp, { stale: data.stale, error: data.error })
@@ -403,6 +416,40 @@ function apply(ctx, config) {
403
416
  return '获取汇率失败: ' + message
404
417
  }
405
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
+ })
406
453
  }
407
454
 
408
455
  module.exports = {
@@ -413,6 +460,7 @@ module.exports = {
413
460
  formatText,
414
461
  buildCardMarkdown,
415
462
  buildCardPayload,
463
+ buildRateBlock,
416
464
  buildKeyboard,
417
465
  toPlainText,
418
466
  passiveMeta,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-bns-rate",
3
- "version": "1.2.2",
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"]