koishi-plugin-dwrgbwiki 2.2.0 → 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 +161 -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),
|
|
@@ -576,20 +579,71 @@ module.exports = {
|
|
|
576
579
|
.example('反馈 抽卡图片显示异常')
|
|
577
580
|
.example('反馈 问题已处理 -i 12')
|
|
578
581
|
.check(async ({ session }) => {
|
|
579
|
-
// 含 -i
|
|
582
|
+
// 含 -i 的回复操作以及 拉黑/解封/黑名单 管理操作需管理员权限
|
|
583
|
+
// (feedbackAdminIds 中配置的用户 id,或 koishi 权限 ≥2;koishi 新用户默认权限只有 1)
|
|
580
584
|
const body = session.content ? String(session.content).replace(/^反馈\s*/, '') : ''
|
|
581
|
-
|
|
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) {
|
|
582
588
|
const user = await session.observeUser(['authority']).catch(() => null)
|
|
583
|
-
|
|
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'
|
|
584
592
|
}
|
|
585
593
|
})
|
|
586
594
|
.action(async ({ session, options }, content) => {
|
|
587
595
|
const raw0 = session.content ? String(session.content).trim() : ''
|
|
588
|
-
const
|
|
589
|
-
|
|
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 })
|
|
590
600
|
// 从原始消息提取完整反馈文字(保留空格;text 类型会去掉空格)
|
|
591
601
|
const raw = session.content ? String(session.content).trim() : ''
|
|
592
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
|
+
}
|
|
593
647
|
// 解析回复编号:反馈 <回复内容> -i <编号>
|
|
594
648
|
const optMatch = body.match(/\s+-i\s+(\d+)\s*$/)
|
|
595
649
|
const replyId = optMatch ? optMatch[1] : (options && options.i ? String(options.i).trim() : '')
|
|
@@ -612,7 +666,7 @@ module.exports = {
|
|
|
612
666
|
await feedbackStore.markReplied(replyId)
|
|
613
667
|
return '已回复编号 ' + replyId + '(已发到原反馈群)'
|
|
614
668
|
} catch (e) {
|
|
615
|
-
ctx.logger('dwrgbwiki').warn('回复反馈到原群失败,尝试私聊: ' + (e
|
|
669
|
+
ctx.logger('dwrgbwiki').warn('回复反馈到原群失败,尝试私聊: ' + errDetail(e) + ' | channelId=' + record.channelId + ' guildId=' + record.guildId)
|
|
616
670
|
}
|
|
617
671
|
}
|
|
618
672
|
try {
|
|
@@ -627,18 +681,24 @@ module.exports = {
|
|
|
627
681
|
await feedbackStore.markReplied(replyId)
|
|
628
682
|
return '已回复编号 ' + replyId + '(私聊失败,已转发到反馈群)'
|
|
629
683
|
} catch (e2) {
|
|
630
|
-
ctx.logger('dwrgbwiki').warn('
|
|
631
|
-
return '回复失败:' + (e2
|
|
684
|
+
ctx.logger('dwrgbwiki').warn('回复反馈兜底转发失败: ' + errDetail(e2))
|
|
685
|
+
return '回复失败:' + errDetail(e2)
|
|
632
686
|
}
|
|
633
687
|
}
|
|
634
|
-
ctx.logger('dwrgbwiki').warn('回复反馈失败: ' + (e
|
|
635
|
-
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)
|
|
636
690
|
}
|
|
637
691
|
}
|
|
638
692
|
// 提交反馈模式
|
|
639
693
|
let text = body
|
|
640
694
|
if (!text && content) text = String(content).trim()
|
|
641
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
|
+
}
|
|
642
702
|
const time = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
643
703
|
const senderName = session.username || session.user?.name || session.userId || '未知用户'
|
|
644
704
|
const record = await feedbackStore.create({
|
|
@@ -665,15 +725,15 @@ module.exports = {
|
|
|
665
725
|
} else {
|
|
666
726
|
await bot.sendPrivateMessage(config.feedbackUserId, feedbackMsg)
|
|
667
727
|
}
|
|
668
|
-
return '已收到反馈(编号 #' + record.id + '
|
|
728
|
+
return '已收到反馈(编号 #' + record.id + ')。如需收到回复,请联系群主打开机器人主动在群聊发言功能。本机器人由玩家制作,并非网易第五人格官方机器人,请勿反馈第五人格相关问题。'
|
|
669
729
|
} catch (e) {
|
|
670
|
-
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)
|
|
671
731
|
return '已收到反馈(编号 #' + record.id + ',但推送给管理员失败,请稍后重试)'
|
|
672
732
|
}
|
|
673
733
|
}
|
|
674
734
|
return '已收到反馈(编号 #' + record.id + ',但未找到可用的推送机器人,请稍后重试)'
|
|
675
735
|
}
|
|
676
|
-
return '已收到反馈(编号 #' + record.id + '
|
|
736
|
+
return '已收到反馈(编号 #' + record.id + ')。如需收到回复,请联系群主打开机器人主动在群聊发言功能。本机器人由玩家制作,并非网易第五人格官方机器人,请勿反馈第五人格相关问题。'
|
|
677
737
|
})
|
|
678
738
|
},
|
|
679
739
|
}
|
|
@@ -692,6 +752,29 @@ function logCmd(ctx, session, name, args, extra) {
|
|
|
692
752
|
} catch (e) { /* 日志失败不影响主流程 */ }
|
|
693
753
|
}
|
|
694
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
|
+
|
|
695
778
|
// 指令宽度表:旧配置键 -> 表格中的指令名(commandWidths)
|
|
696
779
|
const WIDTH_COMMANDS = {
|
|
697
780
|
winrateWidth: '胜率',
|
|
@@ -1242,6 +1325,8 @@ class FeedbackStore {
|
|
|
1242
1325
|
this.ctx = ctx || null
|
|
1243
1326
|
this.cache = new Map() // id -> 反馈记录
|
|
1244
1327
|
this.next = null
|
|
1328
|
+
this.blacklist = [] // 黑名单:[{ platform, userId, reason, time, operator }]
|
|
1329
|
+
this.blacklistLoaded = false
|
|
1245
1330
|
}
|
|
1246
1331
|
|
|
1247
1332
|
async loadNext() {
|
|
@@ -1264,6 +1349,69 @@ class FeedbackStore {
|
|
|
1264
1349
|
} catch (e) { /* 忽略 */ }
|
|
1265
1350
|
}
|
|
1266
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
|
+
|
|
1267
1415
|
async get(id) {
|
|
1268
1416
|
const key = String(id)
|
|
1269
1417
|
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),可添加多个,群/频道与私聊可同时推送 | 空 |
|