koishi-plugin-dwrgbwiki 2.4.0 → 2.4.1
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/index.js +3 -1
- package/match.js +7 -0
- package/package.json +1 -1
- package/wikiwatch.js +128 -49
package/index.js
CHANGED
|
@@ -525,10 +525,11 @@ module.exports = {
|
|
|
525
525
|
if (subFn === '选手') {
|
|
526
526
|
if (!p1 || !String(p1).trim()) return '请提供选手 id,格式:赛事 选手 <选手id>'
|
|
527
527
|
const target = String(p1).trim()
|
|
528
|
+
const notFoundText = '当前赛事数据系统未收录【' + target + '】相关数据,请检查选手id是否输入正确。目前仅支持使用选手英文id进行查询。'
|
|
528
529
|
try {
|
|
529
530
|
const result = await match.loadPlayerData(config, target, mode)
|
|
530
531
|
if (!result.data || (!result.data.hunter && !result.data.survivor)) {
|
|
531
|
-
return
|
|
532
|
+
return notFoundText
|
|
532
533
|
}
|
|
533
534
|
const width = cmdWidth(config, 'matchWidth')
|
|
534
535
|
const updateTime = await match.loadAnnounceTime(config)
|
|
@@ -537,6 +538,7 @@ module.exports = {
|
|
|
537
538
|
return updateTime ? [h.image(image, 'image/png'), '本页面数据上次更新时间:' + updateTime] : [h.image(image, 'image/png')]
|
|
538
539
|
} catch (e) {
|
|
539
540
|
ctx.logger('dwrgbwiki').warn('赛事查询失败: ' + (e && e.message ? e.message : e))
|
|
541
|
+
if (e && e.code === 'PLAYER_NOT_FOUND') return notFoundText
|
|
540
542
|
const msg = (e && e.message || '').includes('404')
|
|
541
543
|
? '未找到选手「' + target + '」的数据,请检查 id 是否正确(大小写、别名)'
|
|
542
544
|
: '赛事查询失败:' + (e && e.message ? e.message : e)
|
package/match.js
CHANGED
|
@@ -76,6 +76,13 @@ async function loadPlayerData(config, playerId, mergeMode) {
|
|
|
76
76
|
if (key.toLowerCase() === lower) { mainId = key; break }
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
// 选手未收录:index 中不存在该 id(大小写不敏感)时直接抛专用错误,
|
|
80
|
+
// 避免对不存在的文件发起请求而报出「Unexpected token '<'」之类的晦涩 JSON 解析错误
|
|
81
|
+
if (!index[mainId]) {
|
|
82
|
+
const err = new Error('PLAYER_NOT_FOUND:' + String(playerId))
|
|
83
|
+
err.code = 'PLAYER_NOT_FOUND'
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
79
86
|
// 收集候选文件:精确匹配 + 所有大小写变体
|
|
80
87
|
const candidates = new Set()
|
|
81
88
|
if (index[mainId]) candidates.add(index[mainId].file)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-dwrgbwiki",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "第五人格BWIKI编辑组开发,所有代码由DeepSeek V4 Flash 0731编写。支持胜率/地图/模仿者/勋章/基础数据查询、图鉴查询、wiki页面截图、藏头诗、抽卡模拟、加页手记探索、赛事数据(选手/对决)、公告变动推送、BWIKI页面更新推送、反馈系统等功能。",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
package/wikiwatch.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// 重启后从上次位置继续,不会重复推送。
|
|
12
12
|
|
|
13
13
|
const { h } = require('koishi')
|
|
14
|
+
const crypto = require('crypto')
|
|
14
15
|
const API_BASE = 'https://wiki.biligame.com/dwrg'
|
|
15
16
|
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
|
16
17
|
const RETRY_WAIT = [3000, 8000] // BWIKI 限流/安全策略(567/429/503)时的重试间隔
|
|
@@ -137,61 +138,139 @@ function fmtTime(ts) {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
// ===== bilibili uid → 用户名解析 =====
|
|
140
|
-
// BWIKI 账号绑定 bilibili,MediaWiki 返回的 user 是数字 uid。通过 bilibili
|
|
141
|
-
//
|
|
141
|
+
// BWIKI 账号绑定 bilibili,MediaWiki 返回的 user 是数字 uid。通过 bilibili 多个来源
|
|
142
|
+
// 解析真实用户名:
|
|
143
|
+
// 1. WBI 签名后的 x/space/wbi/acc/info(官方推荐,需动态取 wbi 密钥)
|
|
144
|
+
// 2. 未签名 x/space/acc/info、x/web-interface/card(部分环境仍可用)
|
|
145
|
+
// 3. 空间页 <title>(服务端渲染,含用户名)
|
|
146
|
+
// 全部失败时回退显示 uid。
|
|
147
|
+
// 成功结果永久缓存;失败结果缓存 10 分钟后自动重试(避免首轮风控导致一直显示 uid)。
|
|
148
|
+
// 每个 uid 首次失败会打一条 warn 日志,便于排查具体原因。
|
|
149
|
+
|
|
142
150
|
const NAME_CACHE = new Map()
|
|
143
151
|
|
|
152
|
+
// wbi 签名混盐索引表(bilibili 官方 hash 算法,公开常量)
|
|
153
|
+
const MIXIN_KEY_ENC_TAB = [46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52]
|
|
154
|
+
|
|
155
|
+
const NEG_CACHE_TTL = 10 * 60 * 1000 // 失败缓存有效期 10 分钟
|
|
156
|
+
const BILI_TIMEOUT = 6000
|
|
157
|
+
|
|
158
|
+
let biliMixinKey = null
|
|
159
|
+
let biliMixinTs = 0
|
|
160
|
+
|
|
161
|
+
async function fetchBiliJson(url, timeout) {
|
|
162
|
+
const ctrl = new AbortController()
|
|
163
|
+
const timer = setTimeout(() => ctrl.abort(), timeout)
|
|
164
|
+
try {
|
|
165
|
+
const headers = {
|
|
166
|
+
'User-Agent': UA,
|
|
167
|
+
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
168
|
+
'Accept': 'application/json, text/plain, */*',
|
|
169
|
+
'Referer': 'https://space.bilibili.com/',
|
|
170
|
+
}
|
|
171
|
+
const res = await fetch(url, { signal: ctrl.signal, headers })
|
|
172
|
+
if (!res.ok) return { __status: res.status }
|
|
173
|
+
return await res.json()
|
|
174
|
+
} catch (e) {
|
|
175
|
+
return { __error: e && e.message ? e.message : String(e) }
|
|
176
|
+
} finally {
|
|
177
|
+
clearTimeout(timer)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 动态获取 wbi 混盐密钥(48 位原始 key → 32 位 mixin key),缓存 4 小时
|
|
182
|
+
async function ensureBiliMixinKey() {
|
|
183
|
+
if (biliMixinKey && Date.now() - biliMixinTs < 4 * 60 * 60 * 1000) return biliMixinKey
|
|
184
|
+
const data = await fetchBiliJson('https://api.bilibili.com/x/web-interface/nav', BILI_TIMEOUT)
|
|
185
|
+
const img = data && data.data && data.data.wbi_img && data.data.wbi_img.img_url
|
|
186
|
+
const sub = data && data.data && data.data.wbi_img && data.data.wbi_img.sub_url
|
|
187
|
+
if (!img || !sub) return null
|
|
188
|
+
const raw = (String(img).split('/').pop() + String(sub).split('/').pop()).replace(/\.\w+$/, '')
|
|
189
|
+
if (raw.length < 64) return null
|
|
190
|
+
let mixin = ''
|
|
191
|
+
for (const i of MIXIN_KEY_ENC_TAB) mixin += raw[i]
|
|
192
|
+
biliMixinKey = mixin.slice(0, 32)
|
|
193
|
+
biliMixinTs = Date.now()
|
|
194
|
+
return biliMixinKey
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 生成 wbi 签名后的完整接口 URL
|
|
198
|
+
function wbiSignUrl(endpoint, params, mixinKey) {
|
|
199
|
+
const p = Object.assign({}, params)
|
|
200
|
+
p.wts = Math.round(Date.now() / 1000)
|
|
201
|
+
const query = Object.keys(p).sort().map(k => k + '=' + encodeURIComponent(p[k])).join('&')
|
|
202
|
+
const wRid = crypto.createHash('md5').update(query + mixinKey).digest('hex')
|
|
203
|
+
return endpoint + '?' + query + '&w_rid=' + wRid
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 从 bilibili 返回体提取用户名:acc/info 是 data.name,card 是 data.card.name
|
|
207
|
+
function extractBiliName(d) {
|
|
208
|
+
if (!d || d.code !== 0 || !d.data) return null
|
|
209
|
+
if (d.data.name && String(d.data.name).trim()) return String(d.data.name).trim()
|
|
210
|
+
if (d.data.card && d.data.card.name && String(d.data.card.name).trim()) return String(d.data.card.name).trim()
|
|
211
|
+
return null
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 尝试解析 uid 对应的用户名;失败返回 null
|
|
215
|
+
async function tryResolveName(uid) {
|
|
216
|
+
// 1) WBI 签名接口(最可靠)
|
|
217
|
+
const mixinKey = await ensureBiliMixinKey()
|
|
218
|
+
if (mixinKey) {
|
|
219
|
+
const url = wbiSignUrl('https://api.bilibili.com/x/space/wbi/acc/info', { mid: uid }, mixinKey)
|
|
220
|
+
const d = await fetchBiliJson(url, BILI_TIMEOUT)
|
|
221
|
+
const n = extractBiliName(d)
|
|
222
|
+
if (n) return n
|
|
223
|
+
}
|
|
224
|
+
// 2) 未签名接口(部分环境仍可用,card 结构为 data.card.name)
|
|
225
|
+
for (const ep of [
|
|
226
|
+
'https://api.bilibili.com/x/space/acc/info?mid=' + uid,
|
|
227
|
+
'https://api.bilibili.com/x/web-interface/card?mid=' + uid,
|
|
228
|
+
]) {
|
|
229
|
+
const d = await fetchBiliJson(ep, BILI_TIMEOUT)
|
|
230
|
+
const n = extractBiliName(d)
|
|
231
|
+
if (n) return n
|
|
232
|
+
}
|
|
233
|
+
// 3) 空间页 <title>(服务端渲染,含用户名)
|
|
234
|
+
const ctrl = new AbortController()
|
|
235
|
+
const timer = setTimeout(() => ctrl.abort(), BILI_TIMEOUT)
|
|
236
|
+
try {
|
|
237
|
+
const res = await fetch('https://space.bilibili.com/' + uid, {
|
|
238
|
+
signal: ctrl.signal,
|
|
239
|
+
headers: { 'User-Agent': UA, 'Accept-Language': 'zh-CN,zh;q=0.9' },
|
|
240
|
+
})
|
|
241
|
+
if (res.ok) {
|
|
242
|
+
const t = await res.text()
|
|
243
|
+
const m = t.match(/<title>([^<]+)<\/title>/)
|
|
244
|
+
if (m && m[1]) {
|
|
245
|
+
let s = String(m[1]).replace(/[_\- ]\s*哔哩哔哩.*$/i, '').replace(/\s*[-_]\s*bilibili.*$/i, '').trim()
|
|
246
|
+
// 过滤占位/错误标题,避免把无意义文本当用户名
|
|
247
|
+
if (s && !/^[\d\s]+$/.test(s) && !/^(加载中|请稍候|没有找到|错误|验证|访问|403|404|412|哔哩哔哩)/.test(s)) return s
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (e) { /* 忽略 */ }
|
|
251
|
+
finally { clearTimeout(timer) }
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
|
|
144
255
|
async function resolveUserName(logger, uid) {
|
|
145
256
|
const key = String(uid == null ? '' : uid).trim()
|
|
146
|
-
if (!/^\d+$/.test(key)) return key || '' // 非数字 uid
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const apis = [
|
|
152
|
-
'https://api.bilibili.com/x/space/acc/info?mid=' + key,
|
|
153
|
-
'https://api.bilibili.com/x/web-interface/card?mid=' + key,
|
|
154
|
-
]
|
|
155
|
-
for (const url of apis) {
|
|
156
|
-
if (name) break
|
|
157
|
-
const ctrl = new AbortController()
|
|
158
|
-
const timer = setTimeout(() => ctrl.abort(), timeout)
|
|
159
|
-
try {
|
|
160
|
-
const res = await fetch(url, {
|
|
161
|
-
signal: ctrl.signal,
|
|
162
|
-
headers: {
|
|
163
|
-
'User-Agent': UA,
|
|
164
|
-
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
165
|
-
'Referer': 'https://space.bilibili.com/' + key,
|
|
166
|
-
},
|
|
167
|
-
})
|
|
168
|
-
if (res.ok) {
|
|
169
|
-
const data = await res.json()
|
|
170
|
-
if (data && data.code === 0 && data.data && data.data.name) name = String(data.data.name).trim()
|
|
171
|
-
}
|
|
172
|
-
} catch (e) { /* 尝试下一个 */ }
|
|
173
|
-
finally { clearTimeout(timer) }
|
|
257
|
+
if (!/^\d+$/.test(key)) return key || '' // 非数字 uid(普通用户名)原样返回
|
|
258
|
+
const cached = NAME_CACHE.get(key)
|
|
259
|
+
if (cached) {
|
|
260
|
+
if (cached.name) return cached.name // 已解析成功
|
|
261
|
+
if (Date.now() - cached.ts < NEG_CACHE_TTL) return key // 失败缓存期内
|
|
174
262
|
}
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (res.ok) {
|
|
185
|
-
const t = await res.text()
|
|
186
|
-
const m = t.match(/<title>([^<]+)<\/title>/)
|
|
187
|
-
if (m && m[1]) name = String(m[1]).replace(/[_\-]\s*哔哩哔哩.*$/i, '').trim()
|
|
188
|
-
}
|
|
189
|
-
} catch (e) { /* 忽略 */ }
|
|
190
|
-
finally { clearTimeout(timer) }
|
|
263
|
+
const found = await tryResolveName(key)
|
|
264
|
+
if (found) {
|
|
265
|
+
NAME_CACHE.set(key, { name: found, ts: Date.now() })
|
|
266
|
+
return found
|
|
267
|
+
}
|
|
268
|
+
// 解析失败:短时缓存(10 分钟)以便自动重试;每个 uid 每进程只打一次日志
|
|
269
|
+
NAME_CACHE.set(key, { name: null, ts: Date.now(), logged: true })
|
|
270
|
+
if (!cached || !cached.logged) {
|
|
271
|
+
logger.warn('BWIKI更新昵称解析失败(显示 uid),请检查 bilibili 接口可达性: ' + key)
|
|
191
272
|
}
|
|
192
|
-
|
|
193
|
-
NAME_CACHE.set(key, result)
|
|
194
|
-
return result
|
|
273
|
+
return key
|
|
195
274
|
}
|
|
196
275
|
|
|
197
276
|
// ===== 文本渲染为图片 =====
|