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 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 errorImage(ctx, config, '未找到选手「' + target + '」的数据,请检查 id 是否正确', '赛事')
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.0",
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
- // 解析真实用户名;失败时回退显示 uid。结果在进程内缓存,避免频繁请求。
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
- if (NAME_CACHE.has(key)) return NAME_CACHE.get(key)
148
- const timeout = 10000
149
- let name = null
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 (!name) {
177
- const ctrl = new AbortController()
178
- const timer = setTimeout(() => ctrl.abort(), timeout)
179
- try {
180
- const res = await fetch('https://space.bilibili.com/' + key, {
181
- signal: ctrl.signal,
182
- headers: { 'User-Agent': UA, 'Accept-Language': 'zh-CN,zh;q=0.9' },
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
- const result = name || key
193
- NAME_CACHE.set(key, result)
194
- return result
273
+ return key
195
274
  }
196
275
 
197
276
  // ===== 文本渲染为图片 =====