koishi-plugin-dwrgbwiki 2.1.3 → 2.2.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 +248 -71
- package/notice.js +4 -1
- package/package.json +1 -1
- package/readme.md +16 -14
package/index.js
CHANGED
|
@@ -117,36 +117,20 @@ module.exports = {
|
|
|
117
117
|
width: Schema.number().min(400).max(4000)
|
|
118
118
|
.description('图片宽度(px),其他指令未单独设置时使用该值')
|
|
119
119
|
.default(1300),
|
|
120
|
-
|
|
121
|
-
.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.
|
|
129
|
-
|
|
130
|
-
.
|
|
131
|
-
.default(0),
|
|
120
|
+
commandWidths: Schema.array(Schema.object({
|
|
121
|
+
command: Schema.union(['胜率', '地图', '模仿者', '赛事', '勋章', '基础数据', '抽卡', '图鉴', 'WIKI'])
|
|
122
|
+
.required()
|
|
123
|
+
.description('指令名称'),
|
|
124
|
+
width: Schema.number().min(0).max(4000)
|
|
125
|
+
.description('该指令图片宽度(px),0=使用默认宽度')
|
|
126
|
+
.default(0),
|
|
127
|
+
}))
|
|
128
|
+
.role('table')
|
|
129
|
+
.description('各指令图片宽度设置(px),以表格形式通过「添加项目」新增;0=使用默认宽度')
|
|
130
|
+
.default([]),
|
|
132
131
|
matchMerge: Schema.union(['team', 'player', 'all', 'none'])
|
|
133
132
|
.description('赛事指令默认合并模式:team=合并队伍前身,player=合并选手多个id,all=两者都合并,none=完全不合并')
|
|
134
133
|
.default('team'),
|
|
135
|
-
badgeWidth: Schema.number().min(0).max(4000)
|
|
136
|
-
.description('勋章指令图片宽度(px),0=使用默认宽度')
|
|
137
|
-
.default(0),
|
|
138
|
-
basicWidth: Schema.number().min(0).max(4000)
|
|
139
|
-
.description('基础数据指令图片宽度(px),0=使用默认宽度')
|
|
140
|
-
.default(0),
|
|
141
|
-
gachaWidth: Schema.number().min(0).max(4000)
|
|
142
|
-
.description('抽卡指令图片宽度(px),0=使用默认宽度')
|
|
143
|
-
.default(0),
|
|
144
|
-
codexWidth: Schema.number().min(0).max(4000)
|
|
145
|
-
.description('图鉴指令图片宽度(px),0=使用默认宽度')
|
|
146
|
-
.default(0),
|
|
147
|
-
wikiWidth: Schema.number().min(400).max(4000)
|
|
148
|
-
.description('WIKI页面指令截图宽度(px)')
|
|
149
|
-
.default(1300),
|
|
150
134
|
wikiMaxHeight: Schema.number().min(200).max(12000)
|
|
151
135
|
.description('WIKI页面指令截图最大高度(px)')
|
|
152
136
|
.default(5000),
|
|
@@ -204,18 +188,25 @@ module.exports = {
|
|
|
204
188
|
feedbackChannelId: Schema.string()
|
|
205
189
|
.description('接收反馈的群/频道 id(推送反馈到该群,QQ 平台推荐使用群推送以避免私聊限制),优先于私聊推送')
|
|
206
190
|
.default(''),
|
|
191
|
+
feedbackAdminIds: Schema.array(Schema.string())
|
|
192
|
+
.description('反馈管理账号 id 列表(可回复反馈 / 拉黑 / 解封 / 查看黑名单)。koishi 默认新用户权限只有 1 级,不满足回复所需的 2 级,请在这里填入你自己的用户 id(如 QQ 号);留空则仅允许 koishi 权限 ≥2 的账号')
|
|
193
|
+
.default([]),
|
|
207
194
|
noticeEnable: Schema.boolean()
|
|
208
195
|
.description('启用网易公告变动监听(正式服+共研服)')
|
|
209
196
|
.default(false),
|
|
210
197
|
noticePlatform: Schema.string()
|
|
211
198
|
.description('公告推送的平台(如 qq),留空禁用公告推送')
|
|
212
199
|
.default('qq'),
|
|
213
|
-
|
|
214
|
-
.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.
|
|
218
|
-
|
|
200
|
+
noticeTargets: Schema.array(Schema.object({
|
|
201
|
+
type: Schema.union(['channel', 'private'])
|
|
202
|
+
.description('目标类型:channel=群/频道,private=私聊')
|
|
203
|
+
.default('channel'),
|
|
204
|
+
id: Schema.string().required()
|
|
205
|
+
.description('群/频道 id(channel 类型)或用户 id(private 类型)'),
|
|
206
|
+
}))
|
|
207
|
+
.role('table')
|
|
208
|
+
.description('公告推送目标列表,以表格形式通过「添加项目」新增推送目标(每行一个目标),可添加多个,群/频道与私聊可同时推送')
|
|
209
|
+
.default([]),
|
|
219
210
|
noticeInterval: Schema.number().min(1).max(1440)
|
|
220
211
|
.description('公告检查间隔(分钟)')
|
|
221
212
|
.default(10),
|
|
@@ -233,10 +224,10 @@ module.exports = {
|
|
|
233
224
|
key: Schema.string().required()
|
|
234
225
|
.description('模板参数名'),
|
|
235
226
|
label: Schema.string()
|
|
236
|
-
.description('
|
|
237
|
-
.default(''),
|
|
227
|
+
.description('显示名,留空则显示参数名'),
|
|
238
228
|
}))
|
|
239
|
-
.
|
|
229
|
+
.role('table')
|
|
230
|
+
.description('要解析展示的参数,以表格形式通过「添加项目」新增(每行一个参数);新增时默认只填 key,label 可留空或按需填写')
|
|
240
231
|
.default([]),
|
|
241
232
|
image: Schema.string()
|
|
242
233
|
.description('配图文件名模板(用 {{参数名}} 占位,如 {{角色}}{{品质}}品质时装_{{名称}}.png),留空不显示图片')
|
|
@@ -253,17 +244,17 @@ module.exports = {
|
|
|
253
244
|
templates: ['单时装', '商城信息'],
|
|
254
245
|
image: '{{角色}}{{品质}}品质时装_{{名称}}.png',
|
|
255
246
|
fields: [
|
|
256
|
-
{ key: '名称'
|
|
257
|
-
{ key: '角色'
|
|
258
|
-
{ key: '品质'
|
|
259
|
-
{ key: '获取方式'
|
|
260
|
-
{ key: '价格'
|
|
261
|
-
{ key: '上架时间'
|
|
262
|
-
{ key: '下架时间'
|
|
263
|
-
{ key: '具体描述'
|
|
264
|
-
{ key: '备注'
|
|
265
|
-
{ key: '限时限定'
|
|
266
|
-
{ key: '英文名称'
|
|
247
|
+
{ key: '名称' },
|
|
248
|
+
{ key: '角色' },
|
|
249
|
+
{ key: '品质' },
|
|
250
|
+
{ key: '获取方式' },
|
|
251
|
+
{ key: '价格' },
|
|
252
|
+
{ key: '上架时间' },
|
|
253
|
+
{ key: '下架时间' },
|
|
254
|
+
{ key: '具体描述' },
|
|
255
|
+
{ key: '备注' },
|
|
256
|
+
{ key: '限时限定' },
|
|
257
|
+
{ key: '英文名称' },
|
|
267
258
|
],
|
|
268
259
|
},
|
|
269
260
|
]),
|
|
@@ -588,20 +579,71 @@ module.exports = {
|
|
|
588
579
|
.example('反馈 抽卡图片显示异常')
|
|
589
580
|
.example('反馈 问题已处理 -i 12')
|
|
590
581
|
.check(async ({ session }) => {
|
|
591
|
-
// 含 -i
|
|
582
|
+
// 含 -i 的回复操作以及 拉黑/解封/黑名单 管理操作需管理员权限
|
|
583
|
+
// (feedbackAdminIds 中配置的用户 id,或 koishi 权限 ≥2;koishi 新用户默认权限只有 1)
|
|
592
584
|
const body = session.content ? String(session.content).replace(/^反馈\s*/, '') : ''
|
|
593
|
-
|
|
585
|
+
const clean = body.trim()
|
|
586
|
+
const isAdminOp = /^(拉黑|解封|黑名单)\b/.test(clean) || /\s+-i\s+\d+\s*$/.test(clean) || /^-i\s+\d+/.test(clean)
|
|
587
|
+
if (isAdminOp) {
|
|
594
588
|
const user = await session.observeUser(['authority']).catch(() => null)
|
|
595
|
-
|
|
589
|
+
const adminIds = Array.isArray(config.feedbackAdminIds) ? config.feedbackAdminIds : []
|
|
590
|
+
const isAdmin = (user && (user.authority || 0) >= 2) || adminIds.includes(String(session.userId))
|
|
591
|
+
if (!isAdmin) return '权限不足:回复反馈/拉黑/解封/黑名单需要管理员权限。请在插件配置「feedbackAdminIds」中添加你的用户 id,或设置 koishi 用户权限 ≥2'
|
|
596
592
|
}
|
|
597
593
|
})
|
|
598
594
|
.action(async ({ session, options }, content) => {
|
|
599
595
|
const raw0 = session.content ? String(session.content).trim() : ''
|
|
600
|
-
const
|
|
601
|
-
|
|
596
|
+
const clean0 = raw0.replace(/^反馈\s*/, '').trim()
|
|
597
|
+
const isReply = /\s+-i\s+\d+\s*$/.test(clean0) || /^-i\s+\d+/.test(clean0)
|
|
598
|
+
const adminOp = (clean0.match(/^(拉黑|解封|黑名单)\b/) || [])[1] || ''
|
|
599
|
+
logCmd(ctx, session, '反馈' + (adminOp ? '(' + adminOp + ')' : (isReply ? '(回复)' : '')), { content })
|
|
602
600
|
// 从原始消息提取完整反馈文字(保留空格;text 类型会去掉空格)
|
|
603
601
|
const raw = session.content ? String(session.content).trim() : ''
|
|
604
602
|
const body = raw.replace(/^反馈\s*/, '').trim()
|
|
603
|
+
// 黑名单管理(权限已在 check 校验):反馈 拉黑 <id> [原因] / 反馈 拉黑 -i <编号> [原因] / 反馈 解封 <id> / 反馈 黑名单
|
|
604
|
+
if (adminOp) {
|
|
605
|
+
if (adminOp === '黑名单') {
|
|
606
|
+
const list = await feedbackStore.listBlacklist()
|
|
607
|
+
if (!list.length) return '反馈黑名单为空'
|
|
608
|
+
const lines = list.map(e => '· ' + e.platform + ' / ' + e.userId + (e.reason ? '(' + e.reason + ')' : '') + ' — ' + e.time + (e.operator ? '(操作者 ' + e.operator + ')' : ''))
|
|
609
|
+
return '反馈黑名单(' + list.length + ' 人):\n' + lines.join('\n')
|
|
610
|
+
}
|
|
611
|
+
if (adminOp === '拉黑') {
|
|
612
|
+
const rest = clean0.replace(/^拉黑\b/, '').trim()
|
|
613
|
+
if (!rest) return '请指定要拉黑的用户,格式:反馈 拉黑 <用户id> [原因],或 反馈 拉黑 -i <反馈编号> [原因]'
|
|
614
|
+
let target = null
|
|
615
|
+
let reason = ''
|
|
616
|
+
const viaFeedback = rest.match(/^-i\s+(\d+)(?:[\s ]*(.*))?$/)
|
|
617
|
+
if (viaFeedback) {
|
|
618
|
+
const record = await feedbackStore.get(viaFeedback[1])
|
|
619
|
+
if (!record) return '未找到编号 ' + viaFeedback[1] + ' 的反馈'
|
|
620
|
+
target = { platform: record.platform, userId: record.userId }
|
|
621
|
+
reason = (viaFeedback[2] || '').trim()
|
|
622
|
+
} else {
|
|
623
|
+
const direct = rest.match(/^(\S+)(?:[\s ]*(.*))?$/)
|
|
624
|
+
if (!direct) return '无法识别的拉黑目标'
|
|
625
|
+
target = { platform: session.platform, userId: direct[1] }
|
|
626
|
+
reason = (direct[2] || '').trim()
|
|
627
|
+
}
|
|
628
|
+
// 防止误拉管理员:检查被拉黑目标的 koishi 权限与反馈管理员配置
|
|
629
|
+
const adminIds = Array.isArray(config.feedbackAdminIds) ? config.feedbackAdminIds : []
|
|
630
|
+
if (adminIds.includes(String(target.userId))) return '不能拉黑反馈管理员账号'
|
|
631
|
+
if (ctx.database) {
|
|
632
|
+
const targetUser = await ctx.database.getUser(target.platform, target.userId, ['authority']).catch(() => null)
|
|
633
|
+
if (targetUser && (targetUser.authority || 0) >= 2) return '不能拉黑管理员账号'
|
|
634
|
+
}
|
|
635
|
+
const ok = await feedbackStore.addBlacklist(target.platform, target.userId, reason, session.userId)
|
|
636
|
+
if (!ok) return '该用户已在反馈黑名单中'
|
|
637
|
+
return '已将 ' + target.platform + '/' + target.userId + ' 加入反馈黑名单' + (reason ? '(原因:' + reason + ')' : '')
|
|
638
|
+
}
|
|
639
|
+
if (adminOp === '解封') {
|
|
640
|
+
const id = clean0.replace(/^解封\b/, '').trim()
|
|
641
|
+
if (!id) return '请指定要解封的用户 id,格式:反馈 解封 <用户id>'
|
|
642
|
+
const ok = await feedbackStore.removeBlacklist(session.platform, id)
|
|
643
|
+
if (!ok) return '该用户不在反馈黑名单中'
|
|
644
|
+
return '已解除 ' + session.platform + '/' + id + ' 的反馈拉黑'
|
|
645
|
+
}
|
|
646
|
+
}
|
|
605
647
|
// 解析回复编号:反馈 <回复内容> -i <编号>
|
|
606
648
|
const optMatch = body.match(/\s+-i\s+(\d+)\s*$/)
|
|
607
649
|
const replyId = optMatch ? optMatch[1] : (options && options.i ? String(options.i).trim() : '')
|
|
@@ -624,7 +666,7 @@ module.exports = {
|
|
|
624
666
|
await feedbackStore.markReplied(replyId)
|
|
625
667
|
return '已回复编号 ' + replyId + '(已发到原反馈群)'
|
|
626
668
|
} catch (e) {
|
|
627
|
-
ctx.logger('dwrgbwiki').warn('回复反馈到原群失败,尝试私聊: ' + (e
|
|
669
|
+
ctx.logger('dwrgbwiki').warn('回复反馈到原群失败,尝试私聊: ' + errDetail(e) + ' | channelId=' + record.channelId + ' guildId=' + record.guildId)
|
|
628
670
|
}
|
|
629
671
|
}
|
|
630
672
|
try {
|
|
@@ -639,18 +681,24 @@ module.exports = {
|
|
|
639
681
|
await feedbackStore.markReplied(replyId)
|
|
640
682
|
return '已回复编号 ' + replyId + '(私聊失败,已转发到反馈群)'
|
|
641
683
|
} catch (e2) {
|
|
642
|
-
ctx.logger('dwrgbwiki').warn('
|
|
643
|
-
return '回复失败:' + (e2
|
|
684
|
+
ctx.logger('dwrgbwiki').warn('回复反馈兜底转发失败: ' + errDetail(e2))
|
|
685
|
+
return '回复失败:' + errDetail(e2)
|
|
644
686
|
}
|
|
645
687
|
}
|
|
646
|
-
ctx.logger('dwrgbwiki').warn('回复反馈失败: ' + (e
|
|
647
|
-
return '回复失败:' + (e
|
|
688
|
+
ctx.logger('dwrgbwiki').warn('回复反馈失败: ' + errDetail(e) + ' | target: ' + record.platform + '/' + record.userId + ' | bot: ' + (bot.platform || '?') + ':' + (bot.selfId || bot.sid) + ' status=' + bot.status)
|
|
689
|
+
return '回复失败:' + errDetail(e)
|
|
648
690
|
}
|
|
649
691
|
}
|
|
650
692
|
// 提交反馈模式
|
|
651
693
|
let text = body
|
|
652
694
|
if (!text && content) text = String(content).trim()
|
|
653
695
|
if (!text) return '请发送要反馈的内容,格式:反馈 <内容>'
|
|
696
|
+
// 黑名单拦截:被拉黑用户提交时直接告知并拒绝(不记录、不推送)
|
|
697
|
+
const banned = await feedbackStore.isBlacklisted(session.platform, session.userId)
|
|
698
|
+
if (banned) {
|
|
699
|
+
ctx.logger('dwrgbwiki').warn('反馈拦截: 黑名单用户 ' + session.platform + '/' + session.userId + ' 尝试提交反馈: ' + String(text).slice(0, 200))
|
|
700
|
+
return '你已被加入反馈黑名单,无法使用反馈功能' + (banned.reason ? '(原因:' + banned.reason + ')' : '')
|
|
701
|
+
}
|
|
654
702
|
const time = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
655
703
|
const senderName = session.username || session.user?.name || session.userId || '未知用户'
|
|
656
704
|
const record = await feedbackStore.create({
|
|
@@ -677,15 +725,15 @@ module.exports = {
|
|
|
677
725
|
} else {
|
|
678
726
|
await bot.sendPrivateMessage(config.feedbackUserId, feedbackMsg)
|
|
679
727
|
}
|
|
680
|
-
return '已收到反馈(编号 #' + record.id + '
|
|
728
|
+
return '已收到反馈(编号 #' + record.id + ')。如需收到回复,请联系群主打开机器人主动在群聊发言功能。本机器人由玩家制作,并非网易第五人格官方机器人,请勿反馈第五人格相关问题。'
|
|
681
729
|
} catch (e) {
|
|
682
|
-
ctx.logger('dwrgbwiki').warn('反馈推送失败: ' + (e
|
|
730
|
+
ctx.logger('dwrgbwiki').warn('反馈推送失败: ' + errDetail(e) + ' | target: ' + config.feedbackPlatform + '/' + (config.feedbackChannelId || config.feedbackUserId) + ' | bot: ' + (bot.platform || '?') + ':' + (bot.selfId || bot.sid) + ' status=' + bot.status)
|
|
683
731
|
return '已收到反馈(编号 #' + record.id + ',但推送给管理员失败,请稍后重试)'
|
|
684
732
|
}
|
|
685
733
|
}
|
|
686
734
|
return '已收到反馈(编号 #' + record.id + ',但未找到可用的推送机器人,请稍后重试)'
|
|
687
735
|
}
|
|
688
|
-
return '已收到反馈(编号 #' + record.id + '
|
|
736
|
+
return '已收到反馈(编号 #' + record.id + ')。如需收到回复,请联系群主打开机器人主动在群聊发言功能。本机器人由玩家制作,并非网易第五人格官方机器人,请勿反馈第五人格相关问题。'
|
|
689
737
|
})
|
|
690
738
|
},
|
|
691
739
|
}
|
|
@@ -704,7 +752,48 @@ function logCmd(ctx, session, name, args, extra) {
|
|
|
704
752
|
} catch (e) { /* 日志失败不影响主流程 */ }
|
|
705
753
|
}
|
|
706
754
|
|
|
755
|
+
// 提取错误详情(解包 AggregateError 与 HTTP 响应体,便于定位 QQ 等平台 API 报错)
|
|
756
|
+
function errDetail(e) {
|
|
757
|
+
try {
|
|
758
|
+
if (!e) return 'unknown'
|
|
759
|
+
const parts = []
|
|
760
|
+
const msg = e.message ? String(e.message) : ''
|
|
761
|
+
if (msg) parts.push(msg)
|
|
762
|
+
if (e.errors && e.errors.length) {
|
|
763
|
+
parts.push('inner[' + e.errors.length + ']: ' + e.errors.map(er => {
|
|
764
|
+
let s = (er && er.message) ? String(er.message) : String(er)
|
|
765
|
+
const body = er && er.response && er.response.data ? JSON.stringify(er.response.data) : ''
|
|
766
|
+
if (body) s += ' [' + body.slice(0, 400) + ']'
|
|
767
|
+
return s
|
|
768
|
+
}).join(' || '))
|
|
769
|
+
}
|
|
770
|
+
const resp = e.response && e.response.data ? JSON.stringify(e.response.data).slice(0, 400) : ''
|
|
771
|
+
if (resp) parts.push('resp: ' + resp)
|
|
772
|
+
return parts.join(' | ') || 'Error'
|
|
773
|
+
} catch (_) {
|
|
774
|
+
return 'Error'
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// 指令宽度表:旧配置键 -> 表格中的指令名(commandWidths)
|
|
779
|
+
const WIDTH_COMMANDS = {
|
|
780
|
+
winrateWidth: '胜率',
|
|
781
|
+
mapWidth: '地图',
|
|
782
|
+
mimicWidth: '模仿者',
|
|
783
|
+
matchWidth: '赛事',
|
|
784
|
+
badgeWidth: '勋章',
|
|
785
|
+
basicWidth: '基础数据',
|
|
786
|
+
gachaWidth: '抽卡',
|
|
787
|
+
codexWidth: '图鉴',
|
|
788
|
+
wikiWidth: 'WIKI',
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// 读取指令图片宽度:优先 commandWidths 表格,其次回退旧版独立配置键,最后使用默认宽度
|
|
707
792
|
function cmdWidth(config, key) {
|
|
793
|
+
const command = WIDTH_COMMANDS[key]
|
|
794
|
+
const list = Array.isArray(config.commandWidths) ? config.commandWidths : []
|
|
795
|
+
const row = command && list.find(t => t && t.command === command)
|
|
796
|
+
if (row && row.width > 0) return row.width
|
|
708
797
|
const v = config[key]
|
|
709
798
|
return (v && v > 0) ? v : (config.width || 1300)
|
|
710
799
|
}
|
|
@@ -881,6 +970,7 @@ async function codexImage(ctx, config, rest) {
|
|
|
881
970
|
ctx.logger('dwrgbwiki').warn(e)
|
|
882
971
|
const msg = e && e.message ? e.message : String(e)
|
|
883
972
|
if (String(msg).includes('页面不存在')) {
|
|
973
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
884
974
|
try {
|
|
885
975
|
const results = await codex.searchPages(q, config)
|
|
886
976
|
if (results.length) {
|
|
@@ -974,7 +1064,7 @@ async function wikiPageImage(ctx, config, wikiStore, userId, name) {
|
|
|
974
1064
|
} catch (e) {
|
|
975
1065
|
ctx.logger('dwrgbwiki').warn('WIKI页面解析标题失败,按原样截图: ' + (e && e.message ? e.message : e))
|
|
976
1066
|
}
|
|
977
|
-
const width = config
|
|
1067
|
+
const width = cmdWidth(config, 'wikiWidth')
|
|
978
1068
|
const maxHeight = config.wikiMaxHeight || 5000
|
|
979
1069
|
const image = await wiki.screenshot(ctx, target, {
|
|
980
1070
|
width,
|
|
@@ -1235,6 +1325,8 @@ class FeedbackStore {
|
|
|
1235
1325
|
this.ctx = ctx || null
|
|
1236
1326
|
this.cache = new Map() // id -> 反馈记录
|
|
1237
1327
|
this.next = null
|
|
1328
|
+
this.blacklist = [] // 黑名单:[{ platform, userId, reason, time, operator }]
|
|
1329
|
+
this.blacklistLoaded = false
|
|
1238
1330
|
}
|
|
1239
1331
|
|
|
1240
1332
|
async loadNext() {
|
|
@@ -1257,6 +1349,69 @@ class FeedbackStore {
|
|
|
1257
1349
|
} catch (e) { /* 忽略 */ }
|
|
1258
1350
|
}
|
|
1259
1351
|
|
|
1352
|
+
// ===== 黑名单(持久化在 feedback_data 表的 _blacklist 记录中) =====
|
|
1353
|
+
|
|
1354
|
+
async loadBlacklist() {
|
|
1355
|
+
if (this.blacklistLoaded) return
|
|
1356
|
+
this.blacklist = []
|
|
1357
|
+
this.blacklistLoaded = true
|
|
1358
|
+
if (this.ctx && this.ctx.database) {
|
|
1359
|
+
try {
|
|
1360
|
+
const rows = await this.ctx.database.get('feedback_data', { id: '_blacklist' })
|
|
1361
|
+
if (rows && rows.length && rows[0].data) {
|
|
1362
|
+
const parsed = JSON.parse(rows[0].data)
|
|
1363
|
+
if (Array.isArray(parsed.entries)) this.blacklist = parsed.entries
|
|
1364
|
+
}
|
|
1365
|
+
} catch (e) { /* 忽略 */ }
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async saveBlacklist() {
|
|
1370
|
+
if (!this.ctx || !this.ctx.database) return
|
|
1371
|
+
try {
|
|
1372
|
+
await this.ctx.database.upsert('feedback_data', [{ id: '_blacklist', data: JSON.stringify({ entries: this.blacklist }) }])
|
|
1373
|
+
} catch (e) { /* 忽略 */ }
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// 返回命中项(含 reason),未命中返回 null
|
|
1377
|
+
async isBlacklisted(platform, userId) {
|
|
1378
|
+
await this.loadBlacklist()
|
|
1379
|
+
const id = String(userId == null ? '' : userId)
|
|
1380
|
+
if (!id) return null
|
|
1381
|
+
return this.blacklist.find(e => e.platform === platform && String(e.userId) === id) || null
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
async addBlacklist(platform, userId, reason, operator) {
|
|
1385
|
+
await this.loadBlacklist()
|
|
1386
|
+
const id = String(userId == null ? '' : userId)
|
|
1387
|
+
if (!id) return false
|
|
1388
|
+
if (this.blacklist.some(e => e.platform === platform && String(e.userId) === id)) return false
|
|
1389
|
+
this.blacklist.push({
|
|
1390
|
+
platform,
|
|
1391
|
+
userId: id,
|
|
1392
|
+
reason: reason || '',
|
|
1393
|
+
time: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
1394
|
+
operator: operator || '',
|
|
1395
|
+
})
|
|
1396
|
+
await this.saveBlacklist()
|
|
1397
|
+
return true
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
async removeBlacklist(platform, userId) {
|
|
1401
|
+
await this.loadBlacklist()
|
|
1402
|
+
const id = String(userId == null ? '' : userId)
|
|
1403
|
+
const before = this.blacklist.length
|
|
1404
|
+
this.blacklist = this.blacklist.filter(e => !(e.platform === platform && String(e.userId) === id))
|
|
1405
|
+
if (this.blacklist.length === before) return false
|
|
1406
|
+
await this.saveBlacklist()
|
|
1407
|
+
return true
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
async listBlacklist() {
|
|
1411
|
+
await this.loadBlacklist()
|
|
1412
|
+
return this.blacklist.slice()
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1260
1415
|
async get(id) {
|
|
1261
1416
|
const key = String(id)
|
|
1262
1417
|
if (this.cache.has(key)) return this.cache.get(key)
|
|
@@ -1334,21 +1489,43 @@ async function fetchNotice(url, timeout) {
|
|
|
1334
1489
|
}
|
|
1335
1490
|
}
|
|
1336
1491
|
|
|
1492
|
+
// 汇总公告推送目标列表(noticeTargets:多目标,群/频道与私聊可混用)
|
|
1493
|
+
function buildNoticeTargets(config) {
|
|
1494
|
+
const list = Array.isArray(config.noticeTargets) ? config.noticeTargets : []
|
|
1495
|
+
const targets = []
|
|
1496
|
+
const seen = new Set()
|
|
1497
|
+
for (const t of list) {
|
|
1498
|
+
const id = String(t && t.id ? t.id : '').trim()
|
|
1499
|
+
if (!id) continue
|
|
1500
|
+
const type = t.type === 'private' ? 'private' : 'channel'
|
|
1501
|
+
const key = type + ':' + id
|
|
1502
|
+
if (seen.has(key)) continue
|
|
1503
|
+
seen.add(key)
|
|
1504
|
+
targets.push({ type, id })
|
|
1505
|
+
}
|
|
1506
|
+
return targets
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// 推送到全部目标,任一目标成功即视为成功(其余目标失败仅记录日志)
|
|
1337
1510
|
async function sendNoticeMessage(ctx, config, content, logger) {
|
|
1338
|
-
|
|
1511
|
+
const targets = buildNoticeTargets(config)
|
|
1512
|
+
if (!config.noticePlatform || !targets.length) return false
|
|
1339
1513
|
const bot = ctx.bots && ctx.bots.find(b => b.platform === config.noticePlatform && b.status === 1)
|
|
1340
1514
|
if (!bot) return false
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1515
|
+
let ok = false
|
|
1516
|
+
for (const t of targets) {
|
|
1517
|
+
try {
|
|
1518
|
+
if (t.type === 'private') {
|
|
1519
|
+
await bot.sendPrivateMessage(t.id, content)
|
|
1520
|
+
} else {
|
|
1521
|
+
await bot.sendMessage(t.id, content)
|
|
1522
|
+
}
|
|
1523
|
+
ok = true
|
|
1524
|
+
} catch (e) {
|
|
1525
|
+
logger.warn('公告推送失败(' + t.type + ':' + t.id + '): ' + (e && e.message ? e.message : e))
|
|
1346
1526
|
}
|
|
1347
|
-
return true
|
|
1348
|
-
} catch (e) {
|
|
1349
|
-
logger.warn('公告推送失败: ' + (e && e.message ? e.message : e))
|
|
1350
|
-
return false
|
|
1351
1527
|
}
|
|
1528
|
+
return ok
|
|
1352
1529
|
}
|
|
1353
1530
|
|
|
1354
1531
|
// 公告渲染为图片推送;失败(无 puppeteer / 渲染出错)返回 false,由调用方回退文本
|
package/notice.js
CHANGED
|
@@ -216,7 +216,10 @@ function buildNoticeHtml(parsed, config, changed) {
|
|
|
216
216
|
const width = config.width || 1300
|
|
217
217
|
const sourceName = config.sourceName || '官方'
|
|
218
218
|
const title = parsed.title || '更新公告'
|
|
219
|
-
|
|
219
|
+
// 增量推送(changed 非空)时,引言(第 1 个板块前的“致诸位尊敬的访客”等段落)未变动则不渲染,
|
|
220
|
+
// 避免每次推送都把固定引言带进图里;完整渲染(changed 为空)时始终展示
|
|
221
|
+
const showIntro = !changed || changed.intro
|
|
222
|
+
const introLines = showIntro ? parsed.intro.split('\n').map(l => l.trim()).filter(Boolean) : []
|
|
220
223
|
const introHtml = introLines.length
|
|
221
224
|
? '<div class="intro' + (changed ? ' update' : '') + '">' + introLines.map(l => inlineHtml(l)).join('<br>') + '</div>'
|
|
222
225
|
: ''
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -115,7 +115,7 @@ WIKI页面 <页面名>
|
|
|
115
115
|
| `图鉴 时装 招风旗` | 查询时装「招风旗」并生成图片卡片 |
|
|
116
116
|
|
|
117
117
|
- 数据来自第五人格 BWIKI(`wiki.biligame.com/dwrg`),通过 `query&prop=revisions` 获取页面 wikitext 后按模板解析
|
|
118
|
-
-
|
|
118
|
+
- 二级指令、模板名(前缀匹配,可多个)、解析字段、配图文件名模板与输出宽度均可在插件配置「图鉴二级指令配置」中自定义增减;字段以表格形式通过「添加项目」新增(每行一个参数),新增时默认只填 `key`,`label` 留空则显示参数名
|
|
119
119
|
- 配图文件名模板用 `{{参数名}}` 占位,如 `{{角色}}{{品质}}品质时装_{{名称}}.png`,插件会按该文件名在 wiki 上查找图片并显示在卡片左侧(找不到则不显示);参数值会自动清洗 wiki 语法(`[[链接]]`、`[[File:..]]`、`<br>` 等)
|
|
120
120
|
- 每个二级指令可单独设置 `width`(输出图片宽度,0=使用默认宽度),卡片为左侧图片、右侧字段表格的版式
|
|
121
121
|
- **时装专属**:`图鉴 时装 <名称>` 会额外读取 `/WIKI/时装稀有度/` 的 `latest_price.csv`(最新价格),在卡片底部显示该时装的**占有率**(含数据更新日期,占有率为动态数据);当品质为「稀世」时额外显示**单金价格**与**多金价格**
|
|
@@ -253,10 +253,17 @@ WIKI页面 <页面名>
|
|
|
253
253
|
| 指令 | 含义 |
|
|
254
254
|
| --- | --- |
|
|
255
255
|
| `反馈 抽卡图片显示异常` | 提交反馈并私聊推送给管理员 |
|
|
256
|
-
| `反馈 问题已处理 -i 12` |
|
|
256
|
+
| `反馈 问题已处理 -i 12` | 管理员按编号回复反馈者 |
|
|
257
|
+
| `反馈 拉黑 <用户id> [原因]` | 管理员拉黑用户,被拉黑者提交反馈时会收到拉黑提示 |
|
|
258
|
+
| `反馈 拉黑 -i <编号> [原因]` | 管理员直接拉黑某条反馈的作者 |
|
|
259
|
+
| `反馈 解封 <用户id>` | 管理员解除用户的拉黑 |
|
|
260
|
+
| `反馈 黑名单` | 管理员查看当前反馈黑名单列表 |
|
|
257
261
|
|
|
258
262
|
- 提交反馈后生成递增编号,回复「已收到反馈(编号 #N)」
|
|
263
|
+
- 管理员判定:用户 id 在配置项 `feedbackAdminIds` 中,或 koishi 权限 ≥2。**注意:koishi 新用户默认权限只有 1 级**,请在插件配置 `feedbackAdminIds` 中填入你自己的用户 id(如 QQ 号),否则回复/拉黑操作会提示权限不足
|
|
259
264
|
- 管理员用 `反馈 <回复内容> -i <编号>` 通过机器人**私聊**回复反馈者,并标记该反馈已回复
|
|
265
|
+
- **QQ 官方机器人注意**:回复/推送属于机器人**主动发消息**,需在 QQ 开放平台的机器人设置中开通「主动消息」能力(群主动消息/私聊主动消息),否则会提示回复失败(日志中的错误码可通过插件日志查看);若私聊受限,回复会兜底转发到配置的反馈群
|
|
266
|
+
- 被拉黑的用户提交反馈时会被拦截并直接告知「你已被加入反馈黑名单」(不生成记录、不推送给管理员);黑名单按「平台 + 用户 id」匹配,持久化到数据库(`feedback_data` 表),管理员账号(`feedbackAdminIds` 或权限 ≥2)不能被拉黑
|
|
260
267
|
- 反馈记录持久化到数据库(`feedback_data` 表)
|
|
261
268
|
|
|
262
269
|
## 配置
|
|
@@ -270,20 +277,12 @@ WIKI页面 <页面名>
|
|
|
270
277
|
| `showBadge` | 显示勋章分数据 | `true` |
|
|
271
278
|
| `badgeDays` | 勋章日增长率天数 | `7` |
|
|
272
279
|
| `width` | 图片宽度(px),其他指令未单独设置时使用该值 | `1300` |
|
|
273
|
-
| `
|
|
274
|
-
| `mapWidth` | 地图指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
275
|
-
| `mimicWidth` | 模仿者指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
276
|
-
| `badgeWidth` | 勋章指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
277
|
-
| `basicWidth` | 基础数据指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
278
|
-
| `gachaWidth` | 抽卡/探索/资源指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
279
|
-
| `codexWidth` | 图鉴指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
280
|
+
| `commandWidths` | 各指令图片宽度表(表格形式,每行:指令名 + 宽度,0=使用默认宽度),可配置胜率/地图/模仿者/赛事/勋章/基础数据/抽卡/图鉴/WIKI | 空 |
|
|
280
281
|
| `codexCommands` | 图鉴二级指令配置(名称/说明/模板名/解析字段,可增减) | 预置「时装」 |
|
|
281
|
-
| `wikiWidth` | WIKI页面指令截图宽度(px) | `1300` |
|
|
282
282
|
| `wikiMaxHeight` | WIKI页面指令截图最大高度(px) | `5000` |
|
|
283
283
|
| `wikiDailyLimit` | WIKI页面指令每日使用次数上限 | `5` |
|
|
284
284
|
| `cangtouDefaultMode` | 藏头指令默认模式 | `谐音` |
|
|
285
285
|
| `cangtouDefaultType` | 藏头指令默认物品类型 | `所有` |
|
|
286
|
-
| `matchWidth` | 赛事指令图片宽度(px),0=使用默认宽度 | `0` |
|
|
287
286
|
| `matchMerge` | 赛事指令默认合并模式(team/player/all/none) | `team` |
|
|
288
287
|
| `gachaDailyLimit` | 抽卡/探索每日次数上限 | `2` |
|
|
289
288
|
| `gachaDefaultJinghua` | 抽卡-精华 默认卡池编号 | 空 |
|
|
@@ -299,10 +298,11 @@ WIKI页面 <页面名>
|
|
|
299
298
|
| `wecomPushDelay` | 断连宽限期(秒):断开不超过该时长不会推送,期间恢复则静默 | `60` |
|
|
300
299
|
| `feedbackPlatform` | 接收反馈的平台(如 `qq`),留空禁用反馈推送 | 空 |
|
|
301
300
|
| `feedbackUserId` | 接收反馈的账号 id | 空 |
|
|
301
|
+
| `feedbackChannelId` | 接收反馈的群/频道 id(优先于私聊推送) | 空 |
|
|
302
|
+
| `feedbackAdminIds` | 反馈管理账号 id 列表(可回复/拉黑/解封/查看黑名单)。koishi 新用户默认权限只有 1 级,达不到回复要求的 2 级,请在此填入你自己的用户 id;留空则仅允许 koishi 权限 ≥2 的账号 | 空 |
|
|
302
303
|
| `noticeEnable` | 启用网易公告变动监听 | `false` |
|
|
303
304
|
| `noticePlatform` | 公告推送的平台(如 `qq`),留空禁用公告推送 | `qq` |
|
|
304
|
-
| `
|
|
305
|
-
| `noticeChannelId` | 公告推送目标:群/频道 id 或用户 id | 空 |
|
|
305
|
+
| `noticeTargets` | 公告推送目标列表,以表格形式通过「添加项目」新增(每行一个目标:类型 channel=群/频道 或 private=私聊、目标 id),可添加多个,群/频道与私聊可同时推送 | 空 |
|
|
306
306
|
| `noticeInterval` | 公告检查间隔(分钟) | `10` |
|
|
307
307
|
|
|
308
308
|
数据源两个镜像:`https://website.homeworkkun.top` 与 `https://web.homeworkkun.top`,可直接在配置中切换。
|
|
@@ -320,7 +320,9 @@ WIKI页面 <页面名>
|
|
|
320
320
|
- 正式服:`https://h55.update.netease.com/h55_notice_version2.txt`
|
|
321
321
|
- 共研服:`https://h55.update.netease.com/h55_notice_gongyan_version2.txt`
|
|
322
322
|
|
|
323
|
-
每次内容变动(以内容哈希判断)时,通过 `noticePlatform` 平台的机器人推送到 `
|
|
323
|
+
每次内容变动(以内容哈希判断)时,通过 `noticePlatform` 平台的机器人推送到 `noticeTargets` 列表中的全部目标(每条目标可单独指定 `channel` 群/频道或 `private` 私聊,可同时推送)。推送内容为「公告标题」+ 正文摘要(去除 HTML 标签与颜色代码,截断 500 字)。首次启动只记录基线,不推送。
|
|
324
|
+
|
|
325
|
+
变更推送优先渲染为图片(仅包含发生变化的部分):当「致诸位尊敬的访客」等引言段落没有变动时,不再将其渲染进图中,避免每次推送都带上固定引言;若引言本身有变动则会正常展示。图片渲染失败时自动回退为纯文本摘要推送。
|
|
324
326
|
|
|
325
327
|
## 图片内容
|
|
326
328
|
|