koishi-plugin-bns-rate 1.2.2 → 1.2.4

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 +94 -41
  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
@@ -28,7 +28,7 @@ const Config = Schema.object({
28
28
  .description('💬 用 QQ官方 Markdown 卡片回复(仅官方QQ机器人「群/私聊」生效;频道机器人、OneBot 等其它平台自动回退纯文本,发送失败也会回退)'),
29
29
  title: Schema.string()
30
30
  .default('神石比例实时播报')
31
- .description('卡片顶部标题(# 一级标题)'),
31
+ .description('卡片顶部标题(# 一级标题)。留空则不输出标题——若横幅图上已经有标题文字,把这里清空即可'),
32
32
 
33
33
  bannerUrl: Schema.string()
34
34
  .default('')
@@ -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)
@@ -176,15 +176,20 @@ function formatText(rates, timestamp, opts = {}) {
176
176
  }
177
177
 
178
178
  // QQ官方 Markdown 卡片源码
179
+ // 按"块"拼装、块之间用空行分隔:任何一块留空/关闭就整块不输出,
180
+ // 这样不会出现空标题(`# `)、也不会在开头/结尾留下多余空行
179
181
  function buildCardMarkdown(rates, timestamp, config) {
180
182
  const time = formatTime(timestamp, false)
181
- const lines = [`# ${config.title}`]
183
+ const blocks = []
184
+
185
+ const title = String(config.title || '').trim()
186
+ if (title) blocks.push(`# ${title}`)
182
187
 
183
188
  const banner = String(config.bannerUrl || '').trim()
184
189
  if (banner) {
185
190
  const w = Math.max(0, Math.round(Number(config.bannerWidth) || 0))
186
191
  const ht = Math.max(0, Math.round(Number(config.bannerHeight) || 0))
187
- lines.push('', `![${config.bannerAlt} #${w}px #${ht}px](${banner})`)
192
+ blocks.push(`![${String(config.bannerAlt || '').trim()} #${w}px #${ht}px](${banner})`)
188
193
  }
189
194
 
190
195
  const intro = fillTemplate(config.intro, {
@@ -192,34 +197,42 @@ function buildCardMarkdown(rates, timestamp, config) {
192
197
  time,
193
198
  date: time.split(' ')[0],
194
199
  }).trim()
195
- if (intro) lines.push('', intro)
200
+ if (intro) blocks.push(intro)
201
+
202
+ blocks.push(buildRateBlock(rates, config).join('\n'))
203
+
204
+ const footer = String(config.footer || '').trim()
205
+ if (config.button && footer) blocks.push(footer)
196
206
 
197
- // 报价列表
198
- const box = String(config.ratesBox || 'code')
207
+ return blocks.join('\n\n').trim()
208
+ }
209
+
210
+ // 报价区排版。box 省略时取 config.ratesBox
211
+ // code = 代码块(灰色框 / 等宽 / 两列对齐,代码块内不支持颜色)
212
+ // quote = 块引用(官方支持,灰底 + 左侧竖条,桌面端可上色)
213
+ // list = 无序列表(官方支持,无底色,缩进 + 圆点)
214
+ // none = 不加任何包裹
215
+ function buildRateBlock(rates, config, box) {
216
+ const mode = box || String(config.ratesBox || 'code')
199
217
  const rows = rates.map(r => ({
200
218
  fwd: truncate(r.forward, config.decimalsForward),
201
219
  rev: truncate(r.reverse, config.decimalsReverse),
202
220
  }))
203
221
 
204
- if (box === 'code') {
205
- // 代码块 = 灰色框(等宽字体,正好可以用空格把两列对齐)
206
- // ⚠️ 代码块内部不支持颜色,塞 <font> 标签只会被原样显示,所以这里不加颜色
222
+ if (mode === 'code') {
207
223
  const wf = Math.max(...rows.map(r => r.fwd.length))
208
224
  const wr = Math.max(...rows.map(r => r.rev.length))
209
225
  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))
226
+ return ['```', ...body, '```']
217
227
  }
218
228
 
219
- const footer = String(config.footer || '').trim()
220
- if (config.button && footer) lines.push('', footer)
221
-
222
- return lines.join('\n')
229
+ const body = rows.map(r => (
230
+ `1元 = ${colorize(r.fwd, config.colorForward)}神石 | `
231
+ + `1神石 = ${colorize(r.rev, config.colorReverse)}元`
232
+ ))
233
+ if (mode === 'quote') return body.map(l => '> ' + l)
234
+ if (mode === 'list') return body.map(l => '- ' + l)
235
+ return body
223
236
  }
224
237
 
225
238
  // —— QQ 官方 internal API 发送(单条 payload 内同时带 markdown 与 keyboard,避免被拆成两条消息)——
@@ -354,6 +367,27 @@ function apply(ctx, config) {
354
367
  }
355
368
  }
356
369
 
370
+ // 发卡片:带按钮 → 不带按钮(按钮能力/权限不可用时仍出卡片)→ 返回 false 让调用方回退纯文本
371
+ async function sendCard(session, markdown, cfg) {
372
+ const plain = toPlainText(markdown, cfg.button ? cfg.buttonUrl : '')
373
+ const passive = passiveMeta(session)
374
+ const hasButtons = !!(cfg.button && String(cfg.buttonUrl || '').trim())
375
+ for (const withButtons of (hasButtons ? [true, false] : [false])) {
376
+ try {
377
+ await sendQqCard(session, buildCardPayload(markdown, plain, cfg, passive, withButtons))
378
+ logger.debug(`已发送 Markdown 卡片到 ${session.channelId}${withButtons ? '(含按钮)' : '(无按钮)'}`)
379
+ return true
380
+ } catch (e) {
381
+ const code = errCode(e)
382
+ const detail = code !== undefined ? `code=${code}` : ((e && e.message) || e)
383
+ const level = isKnownIgnoreError(e) ? 'debug' : 'warn'
384
+ logger[level](`Markdown 卡片发送失败(${withButtons ? '含按钮' : '无按钮'}): ${detail}`)
385
+ }
386
+ }
387
+ logger.warn('Markdown 卡片全部尝试失败,回退纯文本')
388
+ return false
389
+ }
390
+
357
391
  ctx.command('bnsrate', '查询剑灵怀旧服神石汇率')
358
392
  .alias('神石')
359
393
  .alias('shenshi')
@@ -370,23 +404,7 @@ function apply(ctx, config) {
370
404
  // 官方QQ机器人的群聊/私聊:发 Markdown 卡片(markdown + 按钮在同一条消息里)
371
405
  if (config.markdown && session && supportsCard(session)) {
372
406
  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 卡片全部尝试失败,回退纯文本')
407
+ if (await sendCard(session, markdown, config)) return
390
408
  }
391
409
 
392
410
  return formatText(data.rates, data.timestamp, { stale: data.stale, error: data.error })
@@ -403,6 +421,40 @@ function apply(ctx, config) {
403
421
  return '获取汇率失败: ' + message
404
422
  }
405
423
  })
424
+
425
+ // 排版对照:一条消息里同时给出 4 种报价区排版,在手机上挑一个填进 ratesBox
426
+ ctx.command('bnsrate.box', '报价区排版对照:一条消息看 4 种样式,挑一个填到 ratesBox')
427
+ .action(async ({ session }) => {
428
+ let data
429
+ try {
430
+ data = await resolveRates(false)
431
+ } catch (e) {
432
+ const message = e instanceof Error ? e.message : String(e)
433
+ return '获取汇率失败: ' + message
434
+ }
435
+ const sample = data.rates.slice(0, 2)
436
+ const parts = [
437
+ '# 报价区排版对照',
438
+ '',
439
+ '把下面最好看的那一项填到插件设置页的 ratesBox:',
440
+ ]
441
+ const labels = {
442
+ code: 'A. code —— 代码块(灰色框 / 等宽 / 两列对齐 / 无颜色)',
443
+ quote: 'B. quote —— 块引用(灰底 + 左侧竖条 / 桌面端有颜色)',
444
+ list: 'C. list —— 无序列表(无底色 / 缩进 + 圆点)',
445
+ none: 'D. none —— 不包裹(纯文本行)',
446
+ }
447
+ for (const box of ['code', 'quote', 'list', 'none']) {
448
+ parts.push('', `## ${labels[box]}`, '', ...buildRateBlock(sample, config, box))
449
+ }
450
+
451
+ const markdown = parts.join('\n')
452
+ // 对照图不需要按钮,避免误触
453
+ if (session && supportsCard(session)) {
454
+ if (await sendCard(session, markdown, { ...config, button: false })) return
455
+ }
456
+ return markdown
457
+ })
406
458
  }
407
459
 
408
460
  module.exports = {
@@ -413,6 +465,7 @@ module.exports = {
413
465
  formatText,
414
466
  buildCardMarkdown,
415
467
  buildCardPayload,
468
+ buildRateBlock,
416
469
  buildKeyboard,
417
470
  toPlainText,
418
471
  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.4",
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"]