koishi-plugin-dwrgbwiki 2.3.0 → 2.4.0

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
@@ -235,20 +235,11 @@ module.exports = {
235
235
  .description('BWIKI 更新检查间隔(分钟)')
236
236
  .default(10),
237
237
  wikiWatchLimit: Schema.number().min(1).max(100)
238
- .description('每次拉取的最近变更条数(用于发现新变更)')
238
+ .description('每次拉取的最近变更条数(同时决定每次推送最多列出的页面变更数)')
239
239
  .default(50),
240
- wikiWatchDetailLimit: Schema.number().min(0).max(20)
241
- .description('每次推送中获取详细更新内容(diff/新页面内容)的最大页面数,其余只推送标题与编辑摘要')
242
- .default(5),
243
- wikiWatchMaxItems: Schema.number().min(1).max(30)
244
- .description('每次推送最多列出的页面变更数(超出部分仅提示数量)')
245
- .default(15),
246
240
  wikiWatchMaxChars: Schema.number().min(100).max(3000)
247
241
  .description('单页更新内容摘要最大字符数')
248
242
  .default(500),
249
- wikiWatchSkipBots: Schema.boolean()
250
- .description('过滤机器人编辑(rcshow=!bot)')
251
- .default(true),
252
243
  wikiWatchNamespaces: Schema.array(Schema.number())
253
244
  .description('监听的命名空间 id 列表(主命名空间=0;留空仅监听主命名空间)')
254
245
  .default([0]),
@@ -407,7 +398,9 @@ module.exports = {
407
398
  if (r.error) return '检查失败:' + r.error
408
399
  if (r.first) return '首次检查:已记录当前更新位置(本次不推送),之后的页面更新会自动推送到配置的目标'
409
400
  if (r.pushed) {
410
- return '检查完成:检测到 ' + r.pushed + ' 个页面变更' + (r.sent === false ? '(但未推送:未配置目标或对应平台机器人不可用)' : ',已推送')
401
+ const msg = Array.isArray(r.message) ? r.message.slice() : [String(r.message || '检查完成')]
402
+ if (r.sent === false) msg.push('\n(未推送:未配置目标或对应平台机器人不可用,以下仅展示)')
403
+ return msg
411
404
  }
412
405
  return '检查完成:没有新的页面变更'
413
406
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-dwrgbwiki",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "第五人格BWIKI编辑组开发,所有代码由DeepSeek V4 Flash 0731编写。支持胜率/地图/模仿者/勋章/基础数据查询、图鉴查询、wiki页面截图、藏头诗、抽卡模拟、加页手记探索、赛事数据(选手/对决)、公告变动推送、BWIKI页面更新推送、反馈系统等功能。",
5
5
  "main": "index.js",
6
6
  "files": [
package/readme.md CHANGED
@@ -309,11 +309,8 @@ BWIKI更新监控 [检查]
309
309
  | `wikiWatchPlatform` | BWIKI 更新推送的平台(如 `qq`),留空禁用推送 | `qq` |
310
310
  | `wikiWatchTargets` | BWIKI 更新推送目标列表(与 `noticeTargets` 同构:每行一个目标,类型 channel=群/频道 或 private=私聊、目标 id),可添加多个 | 空 |
311
311
  | `wikiWatchInterval` | BWIKI 更新检查间隔(分钟) | `10` |
312
- | `wikiWatchLimit` | 每次拉取的最近变更条数(用于发现新变更) | `50` |
313
- | `wikiWatchDetailLimit` | 每次推送中获取详细更新内容(diff/新页面内容)的最大页面数,其余只推送标题与编辑摘要 | `5` |
314
- | `wikiWatchMaxItems` | 每次推送最多列出的页面变更数(超出部分仅提示数量) | `15` |
312
+ | `wikiWatchLimit` | 每次拉取的最近变更条数(同时决定每次推送最多列出的页面变更数) | `50` |
315
313
  | `wikiWatchMaxChars` | 单页更新内容摘要最大字符数 | `500` |
316
- | `wikiWatchSkipBots` | 过滤机器人编辑(`rcshow=!bot`) | `true` |
317
314
  | `wikiWatchNamespaces` | 监听的命名空间 id 列表(主命名空间=0;留空仅监听主命名空间) | `[0]` |
318
315
 
319
316
  数据源两个镜像:`https://website.homeworkkun.top` 与 `https://web.homeworkkun.top`,可直接在配置中切换。
@@ -343,15 +340,16 @@ BWIKI更新监控 [检查]
343
340
 
344
341
  启用 `wikiWatchEnable` 后,插件会定时(`wikiWatchInterval` 分钟)调用第五人格 BWIKI 的 MediaWiki API(`https://wiki.biligame.com/dwrg/api.php`):
345
342
 
346
- 1. `action=query&list=recentchanges` 获取最近更新的页面(默认只监听主命名空间,可配置 `wikiWatchNamespaces`;`wikiWatchSkipBots` 可过滤机器人编辑)
343
+ 1. `action=query&list=recentchanges` 获取最近更新的页面(默认只监听主命名空间,可配置 `wikiWatchNamespaces`;**不过滤机器人编辑**)
347
344
  2. 对每个新变更调用 `action=compare` 拉取该次编辑的差异(纯文本 unified diff)作为「最后更新内容」;新创建的页面则拉取最新内容前段
345
+ 3. 编辑者的 bilibili 数字 uid 通过 bilibili 空间接口解析为真实用户名(如 `2086555` → 摸鱼达人作业君),解析失败时回退显示 uid
348
346
 
349
- 汇总为一条文字消息,通过 `wikiWatchPlatform` 平台的机器人推送到 `wikiWatchTargets` 列表中的全部目标(每行一个目标,可混用 `channel` 群/频道与 `private` 私聊),格式为:页面标题(新页面标 🆕)、编辑者、时间、编辑摘要、大小变化、更新内容摘要(`wikiWatchMaxChars` 截断)、页面链接。
347
+ 汇总后渲染为一张图片(每轮最多 `wikiWatchLimit` 个页面,全部附更新内容),通过 `wikiWatchPlatform` 平台的机器人推送到 `wikiWatchTargets` 列表中的全部目标(每行一个目标,可混用 `channel` 群/频道与 `private` 私聊)。图片内容为:页面标题(新页面标 🆕)、编辑者昵称、时间、编辑摘要、大小变化、更新内容(diff 的 `+` 行绿、`-` 行红);不包含页面链接。
350
348
 
351
349
  - 首次启动只记录基线(当前最新变更位置),不推送;之后检测到新变更才推送
352
350
  - 已处理的最新变更 id 持久化到数据库(`wiki_data` 表),机器人重启后从上次位置继续,不会重复推送
353
- - 每次推送最多列出 `wikiWatchMaxItems` 个变更,其中最新的 `wikiWatchDetailLimit` 个会附详细更新内容,其余仅标题与摘要
354
- - 任何成员可发送 `BWIKI更新监控` 查看监控状态,发送 `BWIKI更新监控 检查` 立即执行一次检查(首次检查只记录基线)
351
+ - 渲染失败或没有 puppeteer 时自动回退为纯文本推送
352
+ - 任何成员可发送 `BWIKI更新监控` 查看监控状态,发送 `BWIKI更新监控 检查` 立即执行一次检查(返回图片,首次检查只记录基线)
355
353
  - 请求带浏览器 User-Agent,遇到 BWIKI 限流(HTTP 567/429/503)自动重试,检查失败只记录日志,不影响下次轮询
356
354
 
357
355
  ## 图片内容
package/wikiwatch.js CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  // ===== 第五人格 BWIKI 页面更新监听 =====
4
4
  // 定时调用 MediaWiki API(wiki.biligame.com/dwrg/api.php):
5
- // 1. list=recentchanges 获取最近更新的页面(默认主命名空间,可过滤机器人编辑)
5
+ // 1. list=recentchanges 获取最近更新的页面(默认主命名空间,不过滤机器人编辑)
6
6
  // 2. 对每个新变更 action=compare 拉取该次编辑的差异(difftype=unified 纯文本 diff);
7
7
  // 新页面则拉取最新版本内容的前段
8
- // 汇总成文字消息,通过配置的机器人平台(wikiWatchPlatform)推送到目标群聊/私聊列表
9
- // (wikiWatchTargets,与公告推送同构,每行一个目标:类型 + id)。
8
+ // 3. 编辑者的 bilibili 数字 uid 通过 bilibili 空间接口解析为真实用户名
9
+ // 汇总后渲染为一张图片推送到配置的目标群聊/私聊列表(wikiWatchTargets)。
10
10
  // 首次启动只记录基线不推送;已处理的最新 rcid 持久化到数据库(wiki_data 表),
11
11
  // 重启后从上次位置继续,不会重复推送。
12
12
 
13
+ const { h } = require('koishi')
13
14
  const API_BASE = 'https://wiki.biligame.com/dwrg'
14
15
  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'
15
16
  const RETRY_WAIT = [3000, 8000] // BWIKI 限流/安全策略(567/429/503)时的重试间隔
@@ -55,7 +56,7 @@ function buildNamespaceParam(config) {
55
56
  return encodeURIComponent(list.join('|'))
56
57
  }
57
58
 
58
- // 拉取最近变更(返回 recentchanges 记录数组,按时间倒序)
59
+ // 拉取最近变更(返回 recentchanges 记录数组,按时间倒序;不过滤机器人编辑)
59
60
  async function fetchRecentChanges(config) {
60
61
  const timeout = Math.max(config.timeout || 15000, 20000)
61
62
  const limit = clamp(config.wikiWatchLimit, 1, 100, 50)
@@ -64,7 +65,6 @@ async function fetchRecentChanges(config) {
64
65
  '&rclimit=' + limit +
65
66
  '&rcprop=' + encodeURIComponent('title|ids|timestamp|user|comment|flags|sizes') +
66
67
  '&rctype=' + encodeURIComponent('edit|new') +
67
- (config.wikiWatchSkipBots === false ? '' : '&rcshow=!bot') +
68
68
  '&format=json&formatversion=2'
69
69
  const data = await fetchJson(url, timeout)
70
70
  return (data.query && data.query.recentchanges) || []
@@ -136,40 +136,218 @@ function fmtTime(ts) {
136
136
  }
137
137
  }
138
138
 
139
- function indentText(text, prefix) {
140
- return String(text).split('\n').map(l => prefix + l).join('\n')
139
+ // ===== bilibili uid → 用户名解析 =====
140
+ // BWIKI 账号绑定 bilibili,MediaWiki 返回的 user 是数字 uid。通过 bilibili 空间接口
141
+ // 解析真实用户名;失败时回退显示 uid。结果在进程内缓存,避免频繁请求。
142
+ const NAME_CACHE = new Map()
143
+
144
+ async function resolveUserName(logger, uid) {
145
+ 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) }
174
+ }
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) }
191
+ }
192
+ const result = name || key
193
+ NAME_CACHE.set(key, result)
194
+ return result
141
195
  }
142
196
 
143
- // 汇总推送消息(单页内容摘要按 wikiWatchMaxChars 截断,总长度 4000 字符防刷屏)
144
- function buildWatchMessage(items, maxChars) {
145
- const cap = clamp(maxChars, 100, 3000, 500)
146
- const lines = []
147
- lines.push('【第五人格 BWIKI 更新】检测到 ' + items.length + ' 个页面变更')
148
- for (const it of items) {
149
- lines.push('')
150
- lines.push((it.isNew ? '🆕 ' : '📄 ') + it.title)
197
+ // ===== 文本渲染为图片 =====
198
+
199
+ function escHtml(s) {
200
+ return String(s == null ? '' : s)
201
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
202
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;')
203
+ }
204
+
205
+ // 更新内容块:diff 的 + 行标绿、- 行标红;普通行中性色
206
+ function renderContentHtml(text, maxChars) {
207
+ let t = String(text || '').replace(/\n{2,}/g, '\n').trim()
208
+ if (t.length > maxChars) t = t.slice(0, maxChars) + '…'
209
+ const lines = t.split('\n').map(l => {
210
+ const esc = escHtml(l)
211
+ if (l.charAt(0) === '+') return '<div class="ln add">' + esc + '</div>'
212
+ if (l.charAt(0) === '-') return '<div class="ln del">' + esc + '</div>'
213
+ return '<div class="ln">' + esc + '</div>'
214
+ }).join('')
215
+ return '<div class="content">' + lines + '</div>'
216
+ }
217
+
218
+ function buildWatchHtml(items, config) {
219
+ const width = config.width || 1300
220
+ const title = escHtml(config.summaryTitle || ('检测到 ' + items.length + ' 个页面变更'))
221
+ const cards = items.map(it => {
222
+ const tag = it.isNew ? '<span class="tag-new">🆕 新页面</span>' : '<span class="tag-edit">✏️ 编辑</span>'
151
223
  const meta = []
152
- if (it.user) meta.push(it.user)
153
- meta.push(fmtTime(it.time))
224
+ if (it.user) meta.push('<span class="m-user">👤 ' + escHtml(it.user) + '</span>')
225
+ meta.push('<span>🕒 ' + escHtml(fmtTime(it.time)) + '</span>')
226
+ if (it.comment) meta.push('<span class="m-comment">💬 “' + escHtml(it.comment) + '”</span>')
227
+ if (it.minor) meta.push('<span class="m-minor">(小编辑)</span>')
228
+ const size = it.sizeDelta != null
229
+ ? '<div class="size">大小:' + (it.sizeDelta >= 0 ? '+' : '') + it.sizeDelta + ' 字节</div>'
230
+ : ''
231
+ const content = it.detail && it.detail.text
232
+ ? renderContentHtml(it.detail.text, clamp(config.wikiWatchMaxChars, 100, 3000, 500))
233
+ : ''
234
+ return '<div class="card">' +
235
+ '<div class="c-head">' + tag + '<span class="c-title">' + escHtml(it.title) + '</span></div>' +
236
+ '<div class="c-meta">' + meta.join('<span class="sep">·</span>') + '</div>' +
237
+ size +
238
+ content +
239
+ '</div>'
240
+ }).join('')
241
+
242
+ return `<!DOCTYPE html>
243
+ <html lang="zh-CN">
244
+ <head>
245
+ <meta charset="UTF-8">
246
+ <style>
247
+ * { margin:0; padding:0; box-sizing:border-box; }
248
+ body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; color:#e6edf3;
249
+ background:radial-gradient(1400px 700px at 50% -10%, #1b2a4a 0%, #10182b 45%, #0a0e17 100%); padding:24px 20px; }
250
+ .page { max-width:${width}px; margin:0 auto; }
251
+ .hero { text-align:center; padding:24px 22px 20px; border-radius:18px; margin-bottom:18px;
252
+ background:linear-gradient(135deg, #667eea 0%, #764ba2 55%, #a4508b 100%); box-shadow:0 8px 28px rgba(0,0,0,.4); position:relative; overflow:hidden; }
253
+ .hero::after { content:''; position:absolute; inset:0; background:radial-gradient(600px 200px at 80% -20%, rgba(255,255,255,.18), transparent 60%); }
254
+ .badge { display:inline-flex; font-size:22px; font-weight:700; color:#ffd76d; background:rgba(0,0,0,.28);
255
+ border:1px solid rgba(255,215,109,.4); border-radius:999px; padding:4px 14px; margin-bottom:10px; letter-spacing:1px; }
256
+ .title { font-size:32px; font-weight:800; color:#fff; letter-spacing:2px; text-shadow:0 2px 10px rgba(0,0,0,.35); }
257
+ .sub { font-size:22px; color:rgba(255,255,255,.8); margin-top:8px; letter-spacing:.5px; }
258
+ .card { background:rgba(22,27,34,.8); border:1px solid rgba(255,255,255,.09); border-radius:16px; padding:16px 18px; margin-bottom:14px; }
259
+ .c-head { display:flex; align-items:center; gap:10px; font-size:24px; margin-bottom:8px; }
260
+ .tag-new { flex-shrink:0; font-size:20px; font-weight:700; color:#0d1117; background:linear-gradient(135deg,#3fb950,#2ea043); border-radius:999px; padding:2px 10px; }
261
+ .tag-edit { flex-shrink:0; font-size:20px; font-weight:700; color:#0d1117; background:linear-gradient(135deg,#58a6ff,#4c8dff); border-radius:999px; padding:2px 10px; }
262
+ .c-title { font-weight:800; color:#fff; letter-spacing:.5px; word-break:break-all; }
263
+ .c-meta { display:flex; align-items:center; flex-wrap:wrap; gap:10px; font-size:21px; color:#79c0ff; margin-bottom:6px; }
264
+ .c-meta .sep { color:rgba(255,255,255,.3); }
265
+ .m-user { color:#ffd76d; font-weight:700; }
266
+ .m-comment { color:#c9d1d9; }
267
+ .m-minor { color:#8b949e; font-size:19px; }
268
+ .size { font-size:20px; color:#8b949e; margin-bottom:6px; }
269
+ .content { background:rgba(0,0,0,.25); border:1px solid rgba(255,255,255,.07); border-radius:10px; padding:10px 12px; }
270
+ .ln { font-size:21px; line-height:1.7; color:#c9d1d9; white-space:pre-wrap; word-break:break-all; font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace; }
271
+ .ln.add { color:#3fb950; }
272
+ .ln.del { color:#f85149; }
273
+ .foot { text-align:center; color:#8b949e; font-size:20px; margin-top:12px; opacity:.75; letter-spacing:.5px; }
274
+ </style>
275
+ </head>
276
+ <body>
277
+ <div class="page">
278
+ <div class="hero">
279
+ <div class="badge">🛰️ 第五人格 BWIKI 更新</div>
280
+ <div class="title">${title}</div>
281
+ <div class="sub">数据来源:第五人格 BWIKI · 最近更改</div>
282
+ </div>
283
+ ${cards}
284
+ <div class="foot">数据来源:第五人格 BWIKI</div>
285
+ </div>
286
+ </body>
287
+ </html>`
288
+ }
289
+
290
+ // 通过 puppeteer 把 HTML 渲染为 PNG;无 puppeteer / 渲染失败返回 null(调用方回退文本)
291
+ async function renderWatchHtml(ctx, html, config) {
292
+ if (!ctx.puppeteer) return null
293
+ try {
294
+ const width = config.width || 1300
295
+ const page = await ctx.puppeteer.page()
296
+ try {
297
+ await page.setViewport({ width, height: 400, deviceScaleFactor: config.scale || 2 })
298
+ await page.setContent(html, { waitUntil: 'load' })
299
+ await page.waitForFunction(() => Array.from(document.images).every(i => i.complete), {
300
+ timeout: 15000, polling: 200,
301
+ }).catch(() => {})
302
+ return await page.screenshot({ type: 'png', fullPage: true })
303
+ } finally {
304
+ await page.close().catch(() => {})
305
+ }
306
+ } catch (e) {
307
+ return null
308
+ }
309
+ }
310
+
311
+ // 纯文本回退(无 puppeteer / 渲染失败时使用;不含标题行,标题由 buildWatchOutput 统一给出)
312
+ function buildWatchText(items, maxChars) {
313
+ const blocks = []
314
+ for (const it of items) {
315
+ const b = []
316
+ b.push((it.isNew ? '🆕 ' : '📄 ') + it.title)
317
+ const meta = [it.user, fmtTime(it.time)].filter(Boolean)
154
318
  if (it.comment) meta.push('“' + it.comment + '”')
155
319
  if (it.minor) meta.push('(小编辑)')
156
- lines.push(' ' + meta.join(' · '))
157
- if (it.sizeDelta != null) {
158
- lines.push(' 大小:' + (it.sizeDelta >= 0 ? '+' : '') + it.sizeDelta + ' 字节')
159
- }
320
+ b.push(' ' + meta.join(' · '))
321
+ if (it.sizeDelta != null) b.push(' 大小:' + (it.sizeDelta >= 0 ? '+' : '') + it.sizeDelta + ' 字节')
160
322
  if (it.detail && it.detail.text) {
161
- let text = String(it.detail.text).replace(/\n{2,}/g, '\n').trim()
162
- if (text.length > cap) text = text.slice(0, cap) + '…'
163
- if (text) lines.push(' 更新内容:\n' + indentText(text, ' '))
323
+ let t = String(it.detail.text).replace(/\n{2,}/g, '\n').trim()
324
+ if (t.length > maxChars) t = t.slice(0, maxChars) + '…'
325
+ if (t) b.push(' 更新内容:\n' + indentText(t, ' '))
164
326
  }
165
- lines.push(' 🔗 https://wiki.biligame.com/dwrg/' + encodeURIComponent(it.title))
327
+ blocks.push(b.join('\n'))
166
328
  }
167
- let msg = lines.join('\n')
168
- const MAX_TOTAL = 4000
169
- if (msg.length > MAX_TOTAL) msg = msg.slice(0, MAX_TOTAL) + '\n…(内容过长已截断)'
329
+ let msg = blocks.join('\n\n')
330
+ if (msg.length > 4000) msg = msg.slice(0, 4000) + '\n…(内容过长已截断)'
170
331
  return msg
171
332
  }
172
333
 
334
+ function indentText(text, prefix) {
335
+ return String(text).split('\n').map(l => prefix + l).join('\n')
336
+ }
337
+
338
+ // 汇总输出:优先渲染为图片,失败回退纯文本。返回 koishi 元素数组
339
+ async function buildWatchOutput(ctx, config, items, logger) {
340
+ const summary = '检测到 ' + items.length + ' 个页面变更'
341
+ try {
342
+ const html = buildWatchHtml(items, { ...config, summaryTitle: summary })
343
+ const img = await renderWatchHtml(ctx, html, config)
344
+ if (img) return [h.image(img, 'image/png')]
345
+ } catch (e) {
346
+ logger.warn('BWIKI更新图片渲染失败,回退纯文本: ' + (e && e.message ? e.message : e))
347
+ }
348
+ return ['【第五人格 BWIKI 更新】' + summary, '\n' + buildWatchText(items, clamp(config.wikiWatchMaxChars, 100, 3000, 500))]
349
+ }
350
+
173
351
  // 汇总推送目标列表(wikiWatchTargets:多目标,群/频道与私聊可混用;与公告推送一致)
174
352
  function buildWatchTargets(config) {
175
353
  const list = Array.isArray(config.wikiWatchTargets) ? config.wikiWatchTargets : []
@@ -187,7 +365,7 @@ function buildWatchTargets(config) {
187
365
  return targets
188
366
  }
189
367
 
190
- // 推送到全部目标,任一目标成功即视为成功(其余目标失败仅记录日志)
368
+ // 推送到全部目标,任一目标成功即视为成功(其余目标失败仅记录日志)。content 为元素数组或字符串。
191
369
  async function sendWatchMessage(ctx, config, content, logger) {
192
370
  const targets = buildWatchTargets(config)
193
371
  if (!config.wikiWatchPlatform || !targets.length) return false
@@ -243,7 +421,7 @@ function startWikiWatchMonitor(ctx, config) {
243
421
  } catch (e) { /* 忽略 */ }
244
422
  }
245
423
 
246
- // 执行一次检查。返回 { pushed, first, sent } 或 { error }
424
+ // 执行一次检查。返回 { pushed, first, sent, message } 或 { error }
247
425
  const check = async () => {
248
426
  if (state.running) return { error: '正在检查中,请稍后再试' }
249
427
  state.running = true
@@ -252,7 +430,7 @@ function startWikiWatchMonitor(ctx, config) {
252
430
  state.lastCheck = fmtTime(new Date().toISOString())
253
431
  state.lastError = null
254
432
  const changes = await fetchRecentChanges(config)
255
- if (!changes.length) return { pushed: 0, first: state.lastRcid == null }
433
+ if (!changes.length) return { pushed: 0, first: state.lastRcid == null, message: null }
256
434
 
257
435
  const firstRun = state.lastRcid == null
258
436
  const fresh = [] // 相对上次位置新增的变更(倒序)
@@ -269,12 +447,10 @@ function startWikiWatchMonitor(ctx, config) {
269
447
  state.lastRcid = newest
270
448
  await saveState()
271
449
  }
272
- if (firstRun || !fresh.length) return { pushed: 0, first: firstRun }
450
+ if (firstRun || !fresh.length) return { pushed: 0, first: firstRun, message: null }
273
451
 
274
452
  fresh.reverse() // 转为时间正序(旧 → 新)
275
453
 
276
- const maxItems = clamp(config.wikiWatchMaxItems, 1, 30, 15)
277
- const detailLimit = clamp(config.wikiWatchDetailLimit, 0, 20, 5)
278
454
  const items = []
279
455
  for (let i = 0; i < fresh.length; i++) {
280
456
  const c = fresh[i]
@@ -289,23 +465,22 @@ function startWikiWatchMonitor(ctx, config) {
289
465
  isNew: c.type === 'new' || !!c.new,
290
466
  sizeDelta: !isNaN(nl) && !isNaN(ol) && nl !== undefined && ol !== undefined ? nl - ol : null,
291
467
  }
292
- // 只对最新的 detailLimit 条拉取详细内容(其余仅摘要),控制 API 请求量
293
- if (i >= fresh.length - detailLimit) {
294
- try {
295
- item.detail = await fetchDiffDetail(config, c)
296
- } catch (e) {
297
- logger.warn('BWIKI更新详情获取失败(' + item.title + '): ' + (e && e.message ? e.message : e))
298
- }
468
+ try {
469
+ item.detail = await fetchDiffDetail(config, c)
470
+ } catch (e) {
471
+ logger.warn('BWIKI更新详情获取失败(' + item.title + '): ' + (e && e.message ? e.message : e))
299
472
  }
300
473
  items.push(item)
301
474
  }
302
475
 
303
- const shown = items.slice(-maxItems) // 保留最新的 maxItems 条
304
- const omitted = items.length - shown.length
305
- let msg = buildWatchMessage(shown, config.wikiWatchMaxChars)
306
- if (omitted > 0) msg += '\n…另有 ' + omitted + ' 个变更未列出(可调大 wikiWatchMaxItems)'
307
- const sent = await sendWatchMessage(ctx, config, msg, logger)
308
- return { pushed: shown.length, first: false, sent }
476
+ // 解析编辑者昵称:bilibili 数字 uid → 真实用户名
477
+ for (const it of items) {
478
+ if (it.user) it.user = await resolveUserName(logger, it.user)
479
+ }
480
+
481
+ const message = await buildWatchOutput(ctx, config, items, logger)
482
+ const sent = await sendWatchMessage(ctx, config, message, logger)
483
+ return { pushed: items.length, first: false, sent, message }
309
484
  } catch (e) {
310
485
  state.lastError = (e && e.message ? e.message : String(e))
311
486
  logger.warn('BWIKI更新检查失败: ' + state.lastError)
@@ -333,5 +508,7 @@ module.exports = {
333
508
  buildWatchTargets,
334
509
  fetchRecentChanges,
335
510
  fetchDiffDetail,
336
- buildWatchMessage,
511
+ buildWatchHtml,
512
+ buildWatchText,
513
+ resolveUserName,
337
514
  }