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.
- package/README.md +16 -6
- package/index.js +94 -41
- 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
|
-
|
|
37
|
+
报价区默认用**代码块**包起来,在客户端渲染成**灰色框**(`ratesBox: code`)。代码块是等宽字体,插件会用空格把两列**对齐**(数字位数不同也对齐)。
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
### 报价区排版可选四种(`ratesBox`)
|
|
39
40
|
|
|
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` 会输出颜色(桌面端有颜色,手机端会露出标签,自行取舍)。
|
|
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` |
|
|
84
|
-
| `colorForward` | `#E60012` | 「1元=xx
|
|
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
|
|
71
|
+
.description('「1元=xx神石」数字颜色,如 #E60012,留空即不染色。⚠️ 仅当 ratesBox 不是 code 时才有意义;手机端不渲染颜色,会把这串标签当文字显示出来'),
|
|
72
72
|
colorReverse: Schema.string()
|
|
73
73
|
.default('#00A650')
|
|
74
|
-
.description('「1神石=xx
|
|
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('📦
|
|
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
|
|
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
|
-
|
|
192
|
+
blocks.push(``)
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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"]
|