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 +4 -11
- package/package.json +1 -1
- package/readme.md +6 -8
- package/wikiwatch.js +225 -48
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
|
-
|
|
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
|
+
"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` |
|
|
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
|
|
343
|
+
1. `action=query&list=recentchanges` 获取最近更新的页面(默认只监听主命名空间,可配置 `wikiWatchNamespaces`;**不过滤机器人编辑**)
|
|
347
344
|
2. 对每个新变更调用 `action=compare` 拉取该次编辑的差异(纯文本 unified diff)作为「最后更新内容」;新创建的页面则拉取最新内容前段
|
|
345
|
+
3. 编辑者的 bilibili 数字 uid 通过 bilibili 空间接口解析为真实用户名(如 `2086555` → 摸鱼达人作业君),解析失败时回退显示 uid
|
|
348
346
|
|
|
349
|
-
|
|
347
|
+
汇总后渲染为一张图片(每轮最多 `wikiWatchLimit` 个页面,全部附更新内容),通过 `wikiWatchPlatform` 平台的机器人推送到 `wikiWatchTargets` 列表中的全部目标(每行一个目标,可混用 `channel` 群/频道与 `private` 私聊)。图片内容为:页面标题(新页面标 🆕)、编辑者昵称、时间、编辑摘要、大小变化、更新内容(diff 的 `+` 行绿、`-` 行红);不包含页面链接。
|
|
350
348
|
|
|
351
349
|
- 首次启动只记录基线(当前最新变更位置),不推送;之后检测到新变更才推送
|
|
352
350
|
- 已处理的最新变更 id 持久化到数据库(`wiki_data` 表),机器人重启后从上次位置继续,不会重复推送
|
|
353
|
-
-
|
|
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
|
-
//
|
|
9
|
-
//
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
197
|
+
// ===== 文本渲染为图片 =====
|
|
198
|
+
|
|
199
|
+
function escHtml(s) {
|
|
200
|
+
return String(s == null ? '' : s)
|
|
201
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
202
|
+
.replace(/"/g, '"').replace(/'/g, ''')
|
|
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
|
-
|
|
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
|
|
162
|
-
if (
|
|
163
|
-
if (
|
|
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
|
-
|
|
327
|
+
blocks.push(b.join('\n'))
|
|
166
328
|
}
|
|
167
|
-
let msg =
|
|
168
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
511
|
+
buildWatchHtml,
|
|
512
|
+
buildWatchText,
|
|
513
|
+
resolveUserName,
|
|
337
514
|
}
|