koishi-plugin-dwrgbwiki 2.2.0 → 2.2.2
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 +164 -13
- package/package.json +1 -1
- package/readme.md +10 -1
package/index.js
CHANGED
|
@@ -188,6 +188,9 @@ module.exports = {
|
|
|
188
188
|
feedbackChannelId: Schema.string()
|
|
189
189
|
.description('接收反馈的群/频道 id(推送反馈到该群,QQ 平台推荐使用群推送以避免私聊限制),优先于私聊推送')
|
|
190
190
|
.default(''),
|
|
191
|
+
feedbackAdminIds: Schema.array(Schema.string())
|
|
192
|
+
.description('反馈管理账号 id 列表(可回复反馈 / 拉黑 / 解封 / 查看黑名单)。koishi 默认新用户权限只有 1 级,不满足回复所需的 2 级,请在这里填入你自己的用户 id(如 QQ 号);留空则仅允许 koishi 权限 ≥2 的账号')
|
|
193
|
+
.default([]),
|
|
191
194
|
noticeEnable: Schema.boolean()
|
|
192
195
|
.description('启用网易公告变动监听(正式服+共研服)')
|
|
193
196
|
.default(false),
|
|
@@ -573,23 +576,77 @@ module.exports = {
|
|
|
573
576
|
|
|
574
577
|
ctx.command('反馈 <内容:text>', '反馈问题或建议(会私聊推送给管理员)')
|
|
575
578
|
.usage('反馈 <内容>')
|
|
579
|
+
.usage('反馈 <回复内容> -i <编号>(管理员:回复该反馈)')
|
|
580
|
+
.usage('反馈 拉黑 <用户id> [原因] / 反馈 拉黑 -i <编号> [原因](管理员:拉黑用户)')
|
|
581
|
+
.usage('反馈 解封 <用户id> / 反馈 黑名单(管理员)')
|
|
576
582
|
.example('反馈 抽卡图片显示异常')
|
|
577
583
|
.example('反馈 问题已处理 -i 12')
|
|
578
584
|
.check(async ({ session }) => {
|
|
579
|
-
// 含 -i
|
|
585
|
+
// 含 -i 的回复操作以及 拉黑/解封/黑名单 管理操作需管理员权限
|
|
586
|
+
// (feedbackAdminIds 中配置的用户 id,或 koishi 权限 ≥2;koishi 新用户默认权限只有 1)
|
|
580
587
|
const body = session.content ? String(session.content).replace(/^反馈\s*/, '') : ''
|
|
581
|
-
|
|
588
|
+
const clean = body.trim()
|
|
589
|
+
const isAdminOp = /^(拉黑|解封|黑名单)(?=\s|$)/.test(clean) || /\s+-i\s+\d+\s*$/.test(clean) || /^-i\s+\d+/.test(clean)
|
|
590
|
+
if (isAdminOp) {
|
|
582
591
|
const user = await session.observeUser(['authority']).catch(() => null)
|
|
583
|
-
|
|
592
|
+
const adminIds = Array.isArray(config.feedbackAdminIds) ? config.feedbackAdminIds : []
|
|
593
|
+
const isAdmin = (user && (user.authority || 0) >= 2) || adminIds.includes(String(session.userId))
|
|
594
|
+
if (!isAdmin) return '权限不足:回复反馈/拉黑/解封/黑名单需要管理员权限。请在插件配置「feedbackAdminIds」中添加你的用户 id,或设置 koishi 用户权限 ≥2'
|
|
584
595
|
}
|
|
585
596
|
})
|
|
586
597
|
.action(async ({ session, options }, content) => {
|
|
587
598
|
const raw0 = session.content ? String(session.content).trim() : ''
|
|
588
|
-
const
|
|
589
|
-
|
|
599
|
+
const clean0 = raw0.replace(/^反馈\s*/, '').trim()
|
|
600
|
+
const isReply = /\s+-i\s+\d+\s*$/.test(clean0) || /^-i\s+\d+/.test(clean0)
|
|
601
|
+
const adminOp = (clean0.match(/^(拉黑|解封|黑名单)(?=\s|$)/) || [])[1] || ''
|
|
602
|
+
logCmd(ctx, session, '反馈' + (adminOp ? '(' + adminOp + ')' : (isReply ? '(回复)' : '')), { content })
|
|
590
603
|
// 从原始消息提取完整反馈文字(保留空格;text 类型会去掉空格)
|
|
591
604
|
const raw = session.content ? String(session.content).trim() : ''
|
|
592
605
|
const body = raw.replace(/^反馈\s*/, '').trim()
|
|
606
|
+
// 黑名单管理(权限已在 check 校验):反馈 拉黑 <id> [原因] / 反馈 拉黑 -i <编号> [原因] / 反馈 解封 <id> / 反馈 黑名单
|
|
607
|
+
if (adminOp) {
|
|
608
|
+
if (adminOp === '黑名单') {
|
|
609
|
+
const list = await feedbackStore.listBlacklist()
|
|
610
|
+
if (!list.length) return '反馈黑名单为空'
|
|
611
|
+
const lines = list.map(e => '· ' + e.platform + ' / ' + e.userId + (e.reason ? '(' + e.reason + ')' : '') + ' — ' + e.time + (e.operator ? '(操作者 ' + e.operator + ')' : ''))
|
|
612
|
+
return '反馈黑名单(' + list.length + ' 人):\n' + lines.join('\n')
|
|
613
|
+
}
|
|
614
|
+
if (adminOp === '拉黑') {
|
|
615
|
+
const rest = clean0.replace(/^拉黑\s*/, '').trim()
|
|
616
|
+
if (!rest) return '请指定要拉黑的用户,格式:反馈 拉黑 <用户id> [原因],或 反馈 拉黑 -i <反馈编号> [原因]'
|
|
617
|
+
let target = null
|
|
618
|
+
let reason = ''
|
|
619
|
+
const viaFeedback = rest.match(/^-i\s+(\d+)(?:[\s ]*(.*))?$/)
|
|
620
|
+
if (viaFeedback) {
|
|
621
|
+
const record = await feedbackStore.get(viaFeedback[1])
|
|
622
|
+
if (!record) return '未找到编号 ' + viaFeedback[1] + ' 的反馈'
|
|
623
|
+
target = { platform: record.platform, userId: record.userId }
|
|
624
|
+
reason = (viaFeedback[2] || '').trim()
|
|
625
|
+
} else {
|
|
626
|
+
const direct = rest.match(/^(\S+)(?:[\s ]*(.*))?$/)
|
|
627
|
+
if (!direct) return '无法识别的拉黑目标'
|
|
628
|
+
target = { platform: session.platform, userId: direct[1] }
|
|
629
|
+
reason = (direct[2] || '').trim()
|
|
630
|
+
}
|
|
631
|
+
// 防止误拉管理员:检查被拉黑目标的 koishi 权限与反馈管理员配置
|
|
632
|
+
const adminIds = Array.isArray(config.feedbackAdminIds) ? config.feedbackAdminIds : []
|
|
633
|
+
if (adminIds.includes(String(target.userId))) return '不能拉黑反馈管理员账号'
|
|
634
|
+
if (ctx.database) {
|
|
635
|
+
const targetUser = await ctx.database.getUser(target.platform, target.userId, ['authority']).catch(() => null)
|
|
636
|
+
if (targetUser && (targetUser.authority || 0) >= 2) return '不能拉黑管理员账号'
|
|
637
|
+
}
|
|
638
|
+
const ok = await feedbackStore.addBlacklist(target.platform, target.userId, reason, session.userId)
|
|
639
|
+
if (!ok) return '该用户已在反馈黑名单中'
|
|
640
|
+
return '已将 ' + target.platform + '/' + target.userId + ' 加入反馈黑名单' + (reason ? '(原因:' + reason + ')' : '')
|
|
641
|
+
}
|
|
642
|
+
if (adminOp === '解封') {
|
|
643
|
+
const id = clean0.replace(/^解封\s*/, '').trim()
|
|
644
|
+
if (!id) return '请指定要解封的用户 id,格式:反馈 解封 <用户id>'
|
|
645
|
+
const ok = await feedbackStore.removeBlacklist(session.platform, id)
|
|
646
|
+
if (!ok) return '该用户不在反馈黑名单中'
|
|
647
|
+
return '已解除 ' + session.platform + '/' + id + ' 的反馈拉黑'
|
|
648
|
+
}
|
|
649
|
+
}
|
|
593
650
|
// 解析回复编号:反馈 <回复内容> -i <编号>
|
|
594
651
|
const optMatch = body.match(/\s+-i\s+(\d+)\s*$/)
|
|
595
652
|
const replyId = optMatch ? optMatch[1] : (options && options.i ? String(options.i).trim() : '')
|
|
@@ -612,7 +669,7 @@ module.exports = {
|
|
|
612
669
|
await feedbackStore.markReplied(replyId)
|
|
613
670
|
return '已回复编号 ' + replyId + '(已发到原反馈群)'
|
|
614
671
|
} catch (e) {
|
|
615
|
-
ctx.logger('dwrgbwiki').warn('回复反馈到原群失败,尝试私聊: ' + (e
|
|
672
|
+
ctx.logger('dwrgbwiki').warn('回复反馈到原群失败,尝试私聊: ' + errDetail(e) + ' | channelId=' + record.channelId + ' guildId=' + record.guildId)
|
|
616
673
|
}
|
|
617
674
|
}
|
|
618
675
|
try {
|
|
@@ -627,18 +684,24 @@ module.exports = {
|
|
|
627
684
|
await feedbackStore.markReplied(replyId)
|
|
628
685
|
return '已回复编号 ' + replyId + '(私聊失败,已转发到反馈群)'
|
|
629
686
|
} catch (e2) {
|
|
630
|
-
ctx.logger('dwrgbwiki').warn('
|
|
631
|
-
return '回复失败:' + (e2
|
|
687
|
+
ctx.logger('dwrgbwiki').warn('回复反馈兜底转发失败: ' + errDetail(e2))
|
|
688
|
+
return '回复失败:' + errDetail(e2)
|
|
632
689
|
}
|
|
633
690
|
}
|
|
634
|
-
ctx.logger('dwrgbwiki').warn('回复反馈失败: ' + (e
|
|
635
|
-
return '回复失败:' + (e
|
|
691
|
+
ctx.logger('dwrgbwiki').warn('回复反馈失败: ' + errDetail(e) + ' | target: ' + record.platform + '/' + record.userId + ' | bot: ' + (bot.platform || '?') + ':' + (bot.selfId || bot.sid) + ' status=' + bot.status)
|
|
692
|
+
return '回复失败:' + errDetail(e)
|
|
636
693
|
}
|
|
637
694
|
}
|
|
638
695
|
// 提交反馈模式
|
|
639
696
|
let text = body
|
|
640
697
|
if (!text && content) text = String(content).trim()
|
|
641
698
|
if (!text) return '请发送要反馈的内容,格式:反馈 <内容>'
|
|
699
|
+
// 黑名单拦截:被拉黑用户提交时直接告知并拒绝(不记录、不推送)
|
|
700
|
+
const banned = await feedbackStore.isBlacklisted(session.platform, session.userId)
|
|
701
|
+
if (banned) {
|
|
702
|
+
ctx.logger('dwrgbwiki').warn('反馈拦截: 黑名单用户 ' + session.platform + '/' + session.userId + ' 尝试提交反馈: ' + String(text).slice(0, 200))
|
|
703
|
+
return '你已被加入反馈黑名单,无法使用反馈功能' + (banned.reason ? '(原因:' + banned.reason + ')' : '')
|
|
704
|
+
}
|
|
642
705
|
const time = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
643
706
|
const senderName = session.username || session.user?.name || session.userId || '未知用户'
|
|
644
707
|
const record = await feedbackStore.create({
|
|
@@ -665,15 +728,15 @@ module.exports = {
|
|
|
665
728
|
} else {
|
|
666
729
|
await bot.sendPrivateMessage(config.feedbackUserId, feedbackMsg)
|
|
667
730
|
}
|
|
668
|
-
return '已收到反馈(编号 #' + record.id + '
|
|
731
|
+
return '已收到反馈(编号 #' + record.id + ')。如需收到回复,请联系群主打开机器人主动在群聊发言功能。本机器人由玩家制作,并非网易第五人格官方机器人,请勿反馈第五人格相关问题。'
|
|
669
732
|
} catch (e) {
|
|
670
|
-
ctx.logger('dwrgbwiki').warn('反馈推送失败: ' + (e
|
|
733
|
+
ctx.logger('dwrgbwiki').warn('反馈推送失败: ' + errDetail(e) + ' | target: ' + config.feedbackPlatform + '/' + (config.feedbackChannelId || config.feedbackUserId) + ' | bot: ' + (bot.platform || '?') + ':' + (bot.selfId || bot.sid) + ' status=' + bot.status)
|
|
671
734
|
return '已收到反馈(编号 #' + record.id + ',但推送给管理员失败,请稍后重试)'
|
|
672
735
|
}
|
|
673
736
|
}
|
|
674
737
|
return '已收到反馈(编号 #' + record.id + ',但未找到可用的推送机器人,请稍后重试)'
|
|
675
738
|
}
|
|
676
|
-
return '已收到反馈(编号 #' + record.id + '
|
|
739
|
+
return '已收到反馈(编号 #' + record.id + ')。如需收到回复,请联系群主打开机器人主动在群聊发言功能。本机器人由玩家制作,并非网易第五人格官方机器人,请勿反馈第五人格相关问题。'
|
|
677
740
|
})
|
|
678
741
|
},
|
|
679
742
|
}
|
|
@@ -692,6 +755,29 @@ function logCmd(ctx, session, name, args, extra) {
|
|
|
692
755
|
} catch (e) { /* 日志失败不影响主流程 */ }
|
|
693
756
|
}
|
|
694
757
|
|
|
758
|
+
// 提取错误详情(解包 AggregateError 与 HTTP 响应体,便于定位 QQ 等平台 API 报错)
|
|
759
|
+
function errDetail(e) {
|
|
760
|
+
try {
|
|
761
|
+
if (!e) return 'unknown'
|
|
762
|
+
const parts = []
|
|
763
|
+
const msg = e.message ? String(e.message) : ''
|
|
764
|
+
if (msg) parts.push(msg)
|
|
765
|
+
if (e.errors && e.errors.length) {
|
|
766
|
+
parts.push('inner[' + e.errors.length + ']: ' + e.errors.map(er => {
|
|
767
|
+
let s = (er && er.message) ? String(er.message) : String(er)
|
|
768
|
+
const body = er && er.response && er.response.data ? JSON.stringify(er.response.data) : ''
|
|
769
|
+
if (body) s += ' [' + body.slice(0, 400) + ']'
|
|
770
|
+
return s
|
|
771
|
+
}).join(' || '))
|
|
772
|
+
}
|
|
773
|
+
const resp = e.response && e.response.data ? JSON.stringify(e.response.data).slice(0, 400) : ''
|
|
774
|
+
if (resp) parts.push('resp: ' + resp)
|
|
775
|
+
return parts.join(' | ') || 'Error'
|
|
776
|
+
} catch (_) {
|
|
777
|
+
return 'Error'
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
695
781
|
// 指令宽度表:旧配置键 -> 表格中的指令名(commandWidths)
|
|
696
782
|
const WIDTH_COMMANDS = {
|
|
697
783
|
winrateWidth: '胜率',
|
|
@@ -1242,6 +1328,8 @@ class FeedbackStore {
|
|
|
1242
1328
|
this.ctx = ctx || null
|
|
1243
1329
|
this.cache = new Map() // id -> 反馈记录
|
|
1244
1330
|
this.next = null
|
|
1331
|
+
this.blacklist = [] // 黑名单:[{ platform, userId, reason, time, operator }]
|
|
1332
|
+
this.blacklistLoaded = false
|
|
1245
1333
|
}
|
|
1246
1334
|
|
|
1247
1335
|
async loadNext() {
|
|
@@ -1264,6 +1352,69 @@ class FeedbackStore {
|
|
|
1264
1352
|
} catch (e) { /* 忽略 */ }
|
|
1265
1353
|
}
|
|
1266
1354
|
|
|
1355
|
+
// ===== 黑名单(持久化在 feedback_data 表的 _blacklist 记录中) =====
|
|
1356
|
+
|
|
1357
|
+
async loadBlacklist() {
|
|
1358
|
+
if (this.blacklistLoaded) return
|
|
1359
|
+
this.blacklist = []
|
|
1360
|
+
this.blacklistLoaded = true
|
|
1361
|
+
if (this.ctx && this.ctx.database) {
|
|
1362
|
+
try {
|
|
1363
|
+
const rows = await this.ctx.database.get('feedback_data', { id: '_blacklist' })
|
|
1364
|
+
if (rows && rows.length && rows[0].data) {
|
|
1365
|
+
const parsed = JSON.parse(rows[0].data)
|
|
1366
|
+
if (Array.isArray(parsed.entries)) this.blacklist = parsed.entries
|
|
1367
|
+
}
|
|
1368
|
+
} catch (e) { /* 忽略 */ }
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
async saveBlacklist() {
|
|
1373
|
+
if (!this.ctx || !this.ctx.database) return
|
|
1374
|
+
try {
|
|
1375
|
+
await this.ctx.database.upsert('feedback_data', [{ id: '_blacklist', data: JSON.stringify({ entries: this.blacklist }) }])
|
|
1376
|
+
} catch (e) { /* 忽略 */ }
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// 返回命中项(含 reason),未命中返回 null
|
|
1380
|
+
async isBlacklisted(platform, userId) {
|
|
1381
|
+
await this.loadBlacklist()
|
|
1382
|
+
const id = String(userId == null ? '' : userId)
|
|
1383
|
+
if (!id) return null
|
|
1384
|
+
return this.blacklist.find(e => e.platform === platform && String(e.userId) === id) || null
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
async addBlacklist(platform, userId, reason, operator) {
|
|
1388
|
+
await this.loadBlacklist()
|
|
1389
|
+
const id = String(userId == null ? '' : userId)
|
|
1390
|
+
if (!id) return false
|
|
1391
|
+
if (this.blacklist.some(e => e.platform === platform && String(e.userId) === id)) return false
|
|
1392
|
+
this.blacklist.push({
|
|
1393
|
+
platform,
|
|
1394
|
+
userId: id,
|
|
1395
|
+
reason: reason || '',
|
|
1396
|
+
time: new Date().toLocaleString('zh-CN', { hour12: false }),
|
|
1397
|
+
operator: operator || '',
|
|
1398
|
+
})
|
|
1399
|
+
await this.saveBlacklist()
|
|
1400
|
+
return true
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
async removeBlacklist(platform, userId) {
|
|
1404
|
+
await this.loadBlacklist()
|
|
1405
|
+
const id = String(userId == null ? '' : userId)
|
|
1406
|
+
const before = this.blacklist.length
|
|
1407
|
+
this.blacklist = this.blacklist.filter(e => !(e.platform === platform && String(e.userId) === id))
|
|
1408
|
+
if (this.blacklist.length === before) return false
|
|
1409
|
+
await this.saveBlacklist()
|
|
1410
|
+
return true
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
async listBlacklist() {
|
|
1414
|
+
await this.loadBlacklist()
|
|
1415
|
+
return this.blacklist.slice()
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1267
1418
|
async get(id) {
|
|
1268
1419
|
const key = String(id)
|
|
1269
1420
|
if (this.cache.has(key)) return this.cache.get(key)
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -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
|
## 配置
|
|
@@ -291,6 +298,8 @@ WIKI页面 <页面名>
|
|
|
291
298
|
| `wecomPushDelay` | 断连宽限期(秒):断开不超过该时长不会推送,期间恢复则静默 | `60` |
|
|
292
299
|
| `feedbackPlatform` | 接收反馈的平台(如 `qq`),留空禁用反馈推送 | 空 |
|
|
293
300
|
| `feedbackUserId` | 接收反馈的账号 id | 空 |
|
|
301
|
+
| `feedbackChannelId` | 接收反馈的群/频道 id(优先于私聊推送) | 空 |
|
|
302
|
+
| `feedbackAdminIds` | 反馈管理账号 id 列表(可回复/拉黑/解封/查看黑名单)。koishi 新用户默认权限只有 1 级,达不到回复要求的 2 级,请在此填入你自己的用户 id;留空则仅允许 koishi 权限 ≥2 的账号 | 空 |
|
|
294
303
|
| `noticeEnable` | 启用网易公告变动监听 | `false` |
|
|
295
304
|
| `noticePlatform` | 公告推送的平台(如 `qq`),留空禁用公告推送 | `qq` |
|
|
296
305
|
| `noticeTargets` | 公告推送目标列表,以表格形式通过「添加项目」新增(每行一个目标:类型 channel=群/频道 或 private=私聊、目标 id),可添加多个,群/频道与私聊可同时推送 | 空 |
|