koishi-plugin-ets2-tools-tmp 3.4.2 → 3.4.3
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/lib/command/ets-app/queryPoint/queryPoint.js +8 -14
- package/lib/command/tmpBind.js +7 -2
- package/lib/command/tmpFootprint.js +35 -18
- package/lib/command/tmpMileageRanking.js +19 -11
- package/lib/command/tmpPosition.js +39 -19
- package/lib/command/tmpQuery/tmpQueryImg.js +46 -14
- package/lib/command/tmpQuery/tmpQueryText.js +46 -16
- package/lib/command/tmpVtcMileageRanking.js +19 -11
- package/lib/database/guildBind.js +100 -17
- package/lib/database/model.js +6 -0
- package/lib/index.js +48 -17
- package/lib/util/tmpIdPicker.js +157 -5
- package/package.json +1 -1
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
const queryPointV1 = require("./queryPointV1");
|
|
2
|
+
const tmpIdPicker = require("../../../util/tmpIdPicker");
|
|
2
3
|
|
|
3
4
|
module.exports = async (ctx, cfg, session, targetQQ) => {
|
|
4
5
|
let queryQQ = targetQQ;
|
|
5
6
|
if (!queryQQ) {
|
|
6
7
|
queryQQ = session.userId;
|
|
7
8
|
} else {
|
|
8
|
-
if (queryQQ.
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
if (String(queryQQ).includes("<at ")) {
|
|
10
|
+
const ats = tmpIdPicker.parseAt(queryQQ);
|
|
11
|
+
const atUser = (Array.isArray(ats) && ats.length > 0) ? ats[0] : null;
|
|
12
|
+
if (!atUser || !atUser.id) {
|
|
13
|
+
return "无法识别@的用户,请输入纯数字QQ号";
|
|
11
14
|
}
|
|
12
|
-
|
|
13
|
-
const idStart = queryQQ.indexOf('id="');
|
|
14
|
-
if (idStart !== -1) {
|
|
15
|
-
const valueStart = idStart + 4;
|
|
16
|
-
const valueEnd = queryQQ.indexOf('"', valueStart);
|
|
17
|
-
if (valueEnd !== -1) {
|
|
18
|
-
id = queryQQ.substring(valueStart, valueEnd);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
queryQQ = id;
|
|
15
|
+
queryQQ = atUser.id;
|
|
22
16
|
}
|
|
23
|
-
if (!/^\d+$/.test(queryQQ)) {
|
|
17
|
+
if (!/^\d+$/.test(String(queryQQ))) {
|
|
24
18
|
return "QQ号格式不正确,请输入纯数字QQ号";
|
|
25
19
|
}
|
|
26
20
|
}
|
package/lib/command/tmpBind.js
CHANGED
|
@@ -16,8 +16,13 @@ module.exports = async (ctx, cfg, session, tmpId) => {
|
|
|
16
16
|
// 追加绑定(同一 tmpId 已绑定过则提示,不重复入库)
|
|
17
17
|
const result = await guildBind.add(ctx.database, session.platform, session.userId, playerInfo.data.tmpId, playerInfo.data.name);
|
|
18
18
|
if (result.exists) {
|
|
19
|
-
|
|
19
|
+
const hint = result.isDefault ? '(该编号已是你的默认绑定)' : '';
|
|
20
|
+
return `该编号已在绑定列表中 ( ${playerInfo.data.name} )${hint},无需重复绑定`;
|
|
20
21
|
}
|
|
21
22
|
const count = (await guildBind.list(ctx.database, session.platform, session.userId)).length;
|
|
22
|
-
|
|
23
|
+
if (count === 1) {
|
|
24
|
+
return `绑定成功 ( ${playerInfo.data.name} ),当前已绑定 ${count} 个编号(已自动设为默认绑定)`;
|
|
25
|
+
}
|
|
26
|
+
const defaultTip = result.isDefault ? '(已设为默认绑定)' : '';
|
|
27
|
+
return `绑定成功 ( ${playerInfo.data.name} )${defaultTip},当前已绑定 ${count} 个编号`;
|
|
23
28
|
};
|
|
@@ -57,38 +57,55 @@ module.exports = async (ctx, session, serverType, tmpId, date) => {
|
|
|
57
57
|
return '未启用 puppeteer 服务'
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
if (tmpId && tmpId.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const valueEnd = queryQQ.indexOf('"', valueStart);
|
|
69
|
-
if (valueEnd !== -1) {
|
|
70
|
-
id = queryQQ.substring(valueStart, valueEnd);
|
|
60
|
+
if (tmpId && typeof tmpId === 'string' && tmpId.includes('<at ')) {
|
|
61
|
+
const ats = tmpIdPicker.parseAt(tmpId);
|
|
62
|
+
let atUser = null;
|
|
63
|
+
if (Array.isArray(ats) && ats.length > 0) atUser = ats[0];
|
|
64
|
+
else if (ats && ats.failed) atUser = { id: null, name: '' };
|
|
65
|
+
if (atUser) {
|
|
66
|
+
if (!atUser.id) {
|
|
67
|
+
return `无法识别@的用户`;
|
|
71
68
|
}
|
|
69
|
+
if (atUser.name) tmpIdPicker.cacheAtName(session.platform, atUser.id, atUser.name);
|
|
70
|
+
const atBindings = await guildBind.listOf(ctx.database, session.platform, atUser.id);
|
|
71
|
+
if (atBindings.length === 0) {
|
|
72
|
+
return `该用户没有绑定玩家编号`;
|
|
73
|
+
}
|
|
74
|
+
// 多个绑定时挂起提问(@别人 也走多选);足迹不出图量大,禁用 all
|
|
75
|
+
const picked = await tmpIdPicker.pick(ctx, session, atBindings, {
|
|
76
|
+
ownerKey: atUser.id,
|
|
77
|
+
ownerName: atUser.name || '',
|
|
78
|
+
allowAll: false
|
|
79
|
+
});
|
|
80
|
+
if (picked === null) {
|
|
81
|
+
return // 已列出序号等待用户回复
|
|
82
|
+
}
|
|
83
|
+
if (picked.length > 1) {
|
|
84
|
+
const results = []
|
|
85
|
+
for (const id of picked) {
|
|
86
|
+
const r = await module.exports(ctx, session, serverType, id, date)
|
|
87
|
+
if (r != null) results.push(r)
|
|
88
|
+
}
|
|
89
|
+
return results.length === 1 ? results[0] : results
|
|
90
|
+
}
|
|
91
|
+
tmpId = picked[0];
|
|
72
92
|
}
|
|
73
|
-
queryQQ = id;
|
|
74
|
-
let guildBindData = await guildBind.get(ctx.database, session.platform, queryQQ);
|
|
75
|
-
if (!guildBindData) {
|
|
76
|
-
return `该用户没有绑定玩家编号`;
|
|
77
|
-
}
|
|
78
|
-
tmpId = guildBindData.tmp_id;
|
|
79
93
|
}
|
|
80
94
|
|
|
95
|
+
// 注意:足迹出图量大(Puppeteer 渲染 + 大量历史点位),刻意**不支持 all**,
|
|
96
|
+
// 避免一次请求打满服务器;两处 pick 都传 allowAll:false 关掉挂起期的 all。
|
|
81
97
|
if (tmpId && isNaN(tmpId)) {
|
|
82
98
|
return `请输入正确的玩家编号,或绑定玩家编号`
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
// 如果没有传入tmpId,尝试从绑定记录获取(多绑定时会列出序号供选择,支持多选=发多张足迹图)
|
|
102
|
+
// 足迹不出图量大,禁用 all(避免一次请求打满服务器)
|
|
86
103
|
if (!tmpId) {
|
|
87
104
|
const bindings = await guildBind.list(ctx.database, session.platform, session.userId)
|
|
88
105
|
if (bindings.length === 0) {
|
|
89
106
|
return `请输入正确的玩家编号,或绑定玩家编号`
|
|
90
107
|
}
|
|
91
|
-
const picked = await tmpIdPicker.pick(ctx, session, bindings)
|
|
108
|
+
const picked = await tmpIdPicker.pick(ctx, session, bindings, { allowAll: false })
|
|
92
109
|
if (picked === null) {
|
|
93
110
|
return // 已列出序号等待用户回复
|
|
94
111
|
}
|
|
@@ -4,7 +4,7 @@ const common = require('../util/common');
|
|
|
4
4
|
const evmOpenApi = require('../api/evmOpenApi');
|
|
5
5
|
const guildBind = require('../database/guildBind');
|
|
6
6
|
const tmpIdPicker = require('../util/tmpIdPicker');
|
|
7
|
-
module.exports = async (ctx, session, rankingType) => {
|
|
7
|
+
module.exports = async (ctx, session, rankingType, target) => {
|
|
8
8
|
if (!ctx.puppeteer) {
|
|
9
9
|
return '未启用 Puppeteer 功能';
|
|
10
10
|
}
|
|
@@ -16,17 +16,25 @@ module.exports = async (ctx, session, rankingType) => {
|
|
|
16
16
|
else if (mileageRankingList.data.length === 0) {
|
|
17
17
|
return '暂无数据';
|
|
18
18
|
}
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
let
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return; // 已列出序号等待用户回复
|
|
19
|
+
// 支持 @别人 查询:拿对方的绑定来高亮(排行榜只高亮一个人)
|
|
20
|
+
let ownerKey = session.userId;
|
|
21
|
+
let ownerName = '';
|
|
22
|
+
if (target && typeof target === 'string' && target.includes('<at ')) {
|
|
23
|
+
const ats = tmpIdPicker.parseAt(target);
|
|
24
|
+
const atUser = (Array.isArray(ats) && ats.length > 0) ? ats[0] : null;
|
|
25
|
+
if (!atUser || !atUser.id) {
|
|
26
|
+
return `无法识别@的用户`;
|
|
28
27
|
}
|
|
29
|
-
|
|
28
|
+
ownerKey = atUser.id;
|
|
29
|
+
ownerName = atUser.name || '';
|
|
30
|
+
if (ownerName) tmpIdPicker.cacheAtName(session.platform, ownerKey, ownerName);
|
|
31
|
+
}
|
|
32
|
+
// 排行榜只高亮一个人:直接用默认绑定,不弹序号询问
|
|
33
|
+
const bindings = await guildBind.listOf(ctx.database, session.platform, ownerKey);
|
|
34
|
+
let bindTmpId = null;
|
|
35
|
+
if (bindings.length > 0) {
|
|
36
|
+
await guildBind.ensureNames(ctx, bindings);
|
|
37
|
+
bindTmpId = String((bindings.find(b => guildBind.isDefault(b)) || bindings[0]).tmp_id);
|
|
30
38
|
}
|
|
31
39
|
let playerMileageRanking = null;
|
|
32
40
|
if (bindTmpId) {
|
|
@@ -14,25 +14,40 @@ const common = require('../util/common')
|
|
|
14
14
|
*/
|
|
15
15
|
module.exports = async (ctx, cfg, session, tmpId) => {
|
|
16
16
|
if (ctx.puppeteer) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
17
|
+
let atUser = null;
|
|
18
|
+
if (tmpId && typeof tmpId === 'string' && tmpId.includes('<at ')) {
|
|
19
|
+
const ats = tmpIdPicker.parseAt(tmpId);
|
|
20
|
+
if (Array.isArray(ats) && ats.length > 0) atUser = ats[0];
|
|
21
|
+
else if (ats && ats.failed) atUser = { id: null, name: '' };
|
|
22
|
+
if (atUser && atUser.name) tmpIdPicker.cacheAtName(session.platform, atUser.id, atUser.name);
|
|
23
|
+
}
|
|
24
|
+
if (atUser) {
|
|
25
|
+
const queryQQ = atUser.id;
|
|
26
|
+
if (!queryQQ) {
|
|
27
|
+
return `无法识别@的用户`;
|
|
28
|
+
}
|
|
29
|
+
const atBindings = await guildBind.listOf(ctx.database, session.platform, queryQQ);
|
|
30
|
+
if (atBindings.length === 0) {
|
|
31
|
+
return `该用户没有绑定玩家编号`;
|
|
32
|
+
}
|
|
33
|
+
// 多个绑定时挂起提问(是谁 @ 的不影响回复的人;被 @ 的人回复即可);定位不支持 all
|
|
34
|
+
const picked = await tmpIdPicker.pick(ctx, session, atBindings, {
|
|
35
|
+
ownerKey: queryQQ,
|
|
36
|
+
ownerName: atUser.name || '',
|
|
37
|
+
allowAll: false
|
|
38
|
+
});
|
|
39
|
+
if (picked === null) {
|
|
40
|
+
return; // 已列出序号等待用户回复
|
|
41
|
+
}
|
|
42
|
+
if (picked.length > 1) {
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const id of picked) {
|
|
45
|
+
const r = await module.exports(ctx, cfg, session, id);
|
|
46
|
+
if (r != null) results.push(r);
|
|
34
47
|
}
|
|
35
|
-
|
|
48
|
+
return results.length === 1 ? results[0] : results;
|
|
49
|
+
}
|
|
50
|
+
tmpId = picked[0];
|
|
36
51
|
}
|
|
37
52
|
// 如果没有传入tmpId,尝试从绑定记录获取(多绑定时会列出序号供选择,支持多选=发多张定位图)
|
|
38
53
|
if (!tmpId) {
|
|
@@ -40,7 +55,7 @@ module.exports = async (ctx, cfg, session, tmpId) => {
|
|
|
40
55
|
if (bindings.length === 0) {
|
|
41
56
|
return `请输入正确的玩家编号`
|
|
42
57
|
}
|
|
43
|
-
const picked = await tmpIdPicker.pick(ctx, session, bindings)
|
|
58
|
+
const picked = await tmpIdPicker.pick(ctx, session, bindings, { allowAll: false })
|
|
44
59
|
if (picked === null) {
|
|
45
60
|
return // 已列出序号等待用户回复
|
|
46
61
|
}
|
|
@@ -54,6 +69,11 @@ module.exports = async (ctx, cfg, session, tmpId) => {
|
|
|
54
69
|
}
|
|
55
70
|
tmpId = picked[0]
|
|
56
71
|
}
|
|
72
|
+
// 注意:定位刻意**不支持 all**(每个绑定都要各查一次玩家信息+线上位置,会放大请求量),
|
|
73
|
+
// 两处 pick 都传 allowAll:false 关掉挂起期的 all。
|
|
74
|
+
if (tmpId && isNaN(tmpId)) {
|
|
75
|
+
return `请输入正确的玩家编号,或绑定玩家编号`
|
|
76
|
+
}
|
|
57
77
|
|
|
58
78
|
// 查询玩家信息
|
|
59
79
|
let playerInfo = await truckersMpApi.player(ctx.http, tmpId)
|
|
@@ -64,23 +64,38 @@ module.exports = async (ctx, cfg, session, tmpId) => {
|
|
|
64
64
|
if (!ctx.puppeteer) {
|
|
65
65
|
return '未启用 puppeteer 服务';
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
let atUser = null;
|
|
68
|
+
if (tmpId && typeof tmpId === 'string' && tmpId.includes('<at ')) {
|
|
69
|
+
const ats = tmpIdPicker.parseAt(tmpId);
|
|
70
|
+
if (Array.isArray(ats) && ats.length > 0) atUser = ats[0];
|
|
71
|
+
else if (ats && ats.failed) atUser = { id: null, name: '' };
|
|
72
|
+
if (atUser && atUser.name) tmpIdPicker.cacheAtName(session.platform, atUser.id, atUser.name);
|
|
73
|
+
}
|
|
74
|
+
if (atUser) {
|
|
75
|
+
if (!atUser.id) {
|
|
76
|
+
return `无法识别@的用户`;
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!guildBindData) {
|
|
78
|
+
const atBindings = await guildBind.listOf(ctx.database, session.platform, atUser.id);
|
|
79
|
+
if (atBindings.length === 0) {
|
|
81
80
|
return `该用户没有绑定玩家编号`;
|
|
82
81
|
}
|
|
83
|
-
|
|
82
|
+
// 多个绑定时挂起提问(@别人 也走多选)
|
|
83
|
+
const picked = await tmpIdPicker.pick(ctx, session, atBindings, {
|
|
84
|
+
ownerKey: atUser.id,
|
|
85
|
+
ownerName: atUser.name || ''
|
|
86
|
+
});
|
|
87
|
+
if (picked === null) {
|
|
88
|
+
return; // 已列出序号等待用户回复
|
|
89
|
+
}
|
|
90
|
+
if (picked.length > 1) {
|
|
91
|
+
const results = [];
|
|
92
|
+
for (const id of picked) {
|
|
93
|
+
const r = await module.exports(ctx, cfg, session, id);
|
|
94
|
+
if (r != null) results.push(r);
|
|
95
|
+
}
|
|
96
|
+
return results.length === 1 ? results[0] : results;
|
|
97
|
+
}
|
|
98
|
+
tmpId = picked[0];
|
|
84
99
|
}
|
|
85
100
|
// 如果没有传入tmpId,尝试从绑定记录获取(多绑定时会列出序号供选择,支持多选)
|
|
86
101
|
if (!tmpId) {
|
|
@@ -102,6 +117,23 @@ module.exports = async (ctx, cfg, session, tmpId) => {
|
|
|
102
117
|
}
|
|
103
118
|
tmpId = picked[0];
|
|
104
119
|
}
|
|
120
|
+
// 「all / 全部」写法:一次性查询全部绑定(受多选上限截断)
|
|
121
|
+
if (tmpIdPicker.isAllArg(tmpId)) {
|
|
122
|
+
const bindings = await guildBind.list(ctx.database, session.platform, session.userId);
|
|
123
|
+
const { ids, hint } = await tmpIdPicker.pickAll(ctx, session, bindings);
|
|
124
|
+
if (ids.length === 0) {
|
|
125
|
+
return `请输入正确的玩家编号`;
|
|
126
|
+
}
|
|
127
|
+
const results = [];
|
|
128
|
+
for (const id of ids) {
|
|
129
|
+
const r = await module.exports(ctx, cfg, session, id);
|
|
130
|
+
if (r != null) results.push(r);
|
|
131
|
+
}
|
|
132
|
+
if (hint) {
|
|
133
|
+
await session.send(hint).catch(() => { });
|
|
134
|
+
}
|
|
135
|
+
return results.length === 1 ? results[0] : results;
|
|
136
|
+
}
|
|
105
137
|
// 并行请求玩家信息与线上状态(互不依赖,避免串行等待)
|
|
106
138
|
const [playerInfo, playerMapInfo] = await Promise.all([
|
|
107
139
|
evmOpenApi.playerInfo(ctx.http, tmpId),
|
|
@@ -50,25 +50,38 @@ const userGroup = {
|
|
|
50
50
|
*/
|
|
51
51
|
module.exports = async (ctx, cfg, session, tmpId) => {
|
|
52
52
|
const { vtcId } = cfg.tmpActivityService?.api || {};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
id = queryQQ.substring(valueStart, valueEnd);
|
|
64
|
-
}
|
|
53
|
+
let atUser = null;
|
|
54
|
+
if (tmpId && typeof tmpId === 'string' && tmpId.includes('<at ')) {
|
|
55
|
+
const ats = tmpIdPicker.parseAt(tmpId);
|
|
56
|
+
if (Array.isArray(ats) && ats.length > 0) atUser = ats[0];
|
|
57
|
+
else if (ats && ats.failed) atUser = { id: null, name: '' };
|
|
58
|
+
if (atUser && atUser.name) tmpIdPicker.cacheAtName(session.platform, atUser.id, atUser.name);
|
|
59
|
+
}
|
|
60
|
+
if (atUser) {
|
|
61
|
+
if (!atUser.id) {
|
|
62
|
+
return `无法识别@的用户`;
|
|
65
63
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!guildBindData) {
|
|
64
|
+
const atBindings = await guildBind.listOf(ctx.database, session.platform, atUser.id);
|
|
65
|
+
if (atBindings.length === 0) {
|
|
69
66
|
return `该用户没有绑定玩家编号`;
|
|
70
67
|
}
|
|
71
|
-
|
|
68
|
+
// 多个绑定时挂起提问(@别人 也走多选)
|
|
69
|
+
const picked = await tmpIdPicker.pick(ctx, session, atBindings, {
|
|
70
|
+
ownerKey: atUser.id,
|
|
71
|
+
ownerName: atUser.name || ''
|
|
72
|
+
});
|
|
73
|
+
if (picked === null) {
|
|
74
|
+
return; // 已列出序号等待用户回复
|
|
75
|
+
}
|
|
76
|
+
if (picked.length > 1) {
|
|
77
|
+
const results = [];
|
|
78
|
+
for (const id of picked) {
|
|
79
|
+
const r = await module.exports(ctx, cfg, session, id);
|
|
80
|
+
if (r != null) results.push(r);
|
|
81
|
+
}
|
|
82
|
+
return results.length === 1 ? results[0] : results;
|
|
83
|
+
}
|
|
84
|
+
tmpId = picked[0];
|
|
72
85
|
}
|
|
73
86
|
// 如果没有传入tmpId,尝试从绑定记录获取(多绑定时会列出序号供选择,支持多选)
|
|
74
87
|
if (!tmpId) {
|
|
@@ -90,6 +103,23 @@ module.exports = async (ctx, cfg, session, tmpId) => {
|
|
|
90
103
|
}
|
|
91
104
|
tmpId = picked[0];
|
|
92
105
|
}
|
|
106
|
+
// 「all / 全部」写法:一次性查询全部绑定(受多选上限截断)
|
|
107
|
+
if (tmpIdPicker.isAllArg(tmpId)) {
|
|
108
|
+
const bindings = await guildBind.list(ctx.database, session.platform, session.userId);
|
|
109
|
+
const { ids, hint } = await tmpIdPicker.pickAll(ctx, session, bindings);
|
|
110
|
+
if (ids.length === 0) {
|
|
111
|
+
return `请输入正确的玩家编号`;
|
|
112
|
+
}
|
|
113
|
+
const results = [];
|
|
114
|
+
for (const id of ids) {
|
|
115
|
+
const r = await module.exports(ctx, cfg, session, id);
|
|
116
|
+
if (r != null) results.push(r);
|
|
117
|
+
}
|
|
118
|
+
if (hint) {
|
|
119
|
+
await session.send(hint).catch(() => { });
|
|
120
|
+
}
|
|
121
|
+
return results.length === 1 ? results[0] : results;
|
|
122
|
+
}
|
|
93
123
|
// 并行请求玩家信息与线上状态(互不依赖,避免串行等待)
|
|
94
124
|
const [playerInfo, playerMapInfo] = await Promise.all([
|
|
95
125
|
evmOpenApi.playerInfo(ctx.http, tmpId),
|
|
@@ -4,7 +4,7 @@ const common = require('../util/common');
|
|
|
4
4
|
const evmOpenApi = require('../api/evmOpenApi');
|
|
5
5
|
const guildBind = require('../database/guildBind');
|
|
6
6
|
const tmpIdPicker = require('../util/tmpIdPicker');
|
|
7
|
-
module.exports = async (ctx, cfg, session, rankingType) => {
|
|
7
|
+
module.exports = async (ctx, cfg, session, rankingType, target) => {
|
|
8
8
|
if (!ctx.puppeteer) {
|
|
9
9
|
return '未启用 Puppeteer 功能';
|
|
10
10
|
}
|
|
@@ -20,17 +20,25 @@ module.exports = async (ctx, cfg, session, rankingType) => {
|
|
|
20
20
|
else if (mileageRankingList.data.length === 0) {
|
|
21
21
|
return '暂无数据';
|
|
22
22
|
}
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
let
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return; // 已列出序号等待用户回复
|
|
23
|
+
// 支持 @别人 查询:拿对方的绑定来高亮(排行榜只高亮一个人)
|
|
24
|
+
let ownerKey = session.userId;
|
|
25
|
+
let ownerName = '';
|
|
26
|
+
if (target && typeof target === 'string' && target.includes('<at ')) {
|
|
27
|
+
const ats = tmpIdPicker.parseAt(target);
|
|
28
|
+
const atUser = (Array.isArray(ats) && ats.length > 0) ? ats[0] : null;
|
|
29
|
+
if (!atUser || !atUser.id) {
|
|
30
|
+
return `无法识别@的用户`;
|
|
32
31
|
}
|
|
33
|
-
|
|
32
|
+
ownerKey = atUser.id;
|
|
33
|
+
ownerName = atUser.name || '';
|
|
34
|
+
if (ownerName) tmpIdPicker.cacheAtName(session.platform, ownerKey, ownerName);
|
|
35
|
+
}
|
|
36
|
+
// 排行榜只高亮一个人:直接用默认绑定,不弹序号询问
|
|
37
|
+
const bindings = await guildBind.listOf(ctx.database, session.platform, ownerKey);
|
|
38
|
+
let bindTmpId = null;
|
|
39
|
+
if (bindings.length > 0) {
|
|
40
|
+
await guildBind.ensureNames(ctx, bindings);
|
|
41
|
+
bindTmpId = String((bindings.find(b => guildBind.isDefault(b)) || bindings[0]).tmp_id);
|
|
34
42
|
}
|
|
35
43
|
let playerMileageRanking = null;
|
|
36
44
|
if (bindTmpId) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const evmOpenApi = require('../api/evmOpenApi');
|
|
2
2
|
/**
|
|
3
|
-
* TMP ID
|
|
3
|
+
* TMP ID 绑定(支持一人绑定多个 + 默认绑定)
|
|
4
4
|
* 表 tmp_guild_bind:一个绑定一行,同一 (platform, user_id) 可有多行;
|
|
5
|
-
*
|
|
5
|
+
* is_default=1 的那行是默认绑定(同一用户至多一个),排行榜等指令优先使用它。
|
|
6
|
+
* get() 返回默认绑定(无默认时退回第一行),保持与旧版行为兼容。
|
|
6
7
|
*/
|
|
7
8
|
module.exports = {
|
|
8
9
|
/**
|
|
@@ -26,21 +27,23 @@ module.exports = {
|
|
|
26
27
|
}));
|
|
27
28
|
return rows;
|
|
28
29
|
},
|
|
30
|
+
/** 是否默认绑定(is_default 兼容 1 / '1' / true 存法) */
|
|
31
|
+
isDefault(row) {
|
|
32
|
+
if (!row) return false;
|
|
33
|
+
return row.is_default === 1 || row.is_default === '1' || row.is_default === true;
|
|
34
|
+
},
|
|
29
35
|
/**
|
|
30
|
-
*
|
|
36
|
+
* 获取默认绑定:优先 is_default=1 的那行,没有则退回第一行(兼容旧数据)。
|
|
31
37
|
* @param db 数据源
|
|
32
38
|
* @param platform 平台
|
|
33
39
|
* @param userId 用户编号
|
|
34
40
|
*/
|
|
35
41
|
async get(db, platform, userId) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
if (guildBindList && guildBindList.length > 0) {
|
|
41
|
-
return guildBindList[0];
|
|
42
|
+
const rows = await this.list(db, platform, userId);
|
|
43
|
+
if (rows.length === 0) {
|
|
44
|
+
return null;
|
|
42
45
|
}
|
|
43
|
-
return
|
|
46
|
+
return rows.find(r => this.isDefault(r)) || rows[0];
|
|
44
47
|
},
|
|
45
48
|
/**
|
|
46
49
|
* 获取全部绑定(按绑定先后排序)
|
|
@@ -56,34 +59,114 @@ module.exports = {
|
|
|
56
59
|
}
|
|
57
60
|
return guildBindList.sort((a, b) => a.id - b.id);
|
|
58
61
|
},
|
|
62
|
+
/**
|
|
63
|
+
* 获取指定用户(可含平台前缀,如 qq:123)的全部绑定。
|
|
64
|
+
* 用于 @别人 查询:内部统一去掉 `qq:` 这类前缀再匹配,兼容库里两种存法。
|
|
65
|
+
*/
|
|
66
|
+
async listOf(db, platform, userId) {
|
|
67
|
+
const bare = String(userId == null ? '' : userId).replace(/^[a-z]+:/i, '');
|
|
68
|
+
const rows = await db.get('tmp_guild_bind', { platform });
|
|
69
|
+
return (rows || [])
|
|
70
|
+
.filter(r => {
|
|
71
|
+
const uid = String(r.user_id == null ? '' : r.user_id).replace(/^[a-z]+:/i, '');
|
|
72
|
+
return uid === bare;
|
|
73
|
+
})
|
|
74
|
+
.sort((a, b) => a.id - b.id);
|
|
75
|
+
},
|
|
76
|
+
/**
|
|
77
|
+
* 取默认绑定的 tmpId(无绑定时返回 null)
|
|
78
|
+
* @returns {string|null}
|
|
79
|
+
*/
|
|
80
|
+
async getDefaultTmpId(db, platform, userId) {
|
|
81
|
+
const row = await this.get(db, platform, userId);
|
|
82
|
+
return row ? String(row.tmp_id) : null;
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
85
|
+
* 把某个绑定设为该用户的默认绑定。同一用户的其他行会被清掉 is_default,
|
|
86
|
+
* 保证「至多一个默认」;若此前没有任何默认,默认绑定位本身就包含「第一条」。
|
|
87
|
+
* @param id 绑定行主键
|
|
88
|
+
* @returns { row, changed } changed=false 表示本来就是默认
|
|
89
|
+
*/
|
|
90
|
+
async setDefault(ctx, db, platform, userId, id) {
|
|
91
|
+
const rows = await this.list(db, platform, userId);
|
|
92
|
+
const target = rows.find(r => r.id === id);
|
|
93
|
+
if (!target) {
|
|
94
|
+
return { row: null, changed: false };
|
|
95
|
+
}
|
|
96
|
+
if (this.isDefault(target)) {
|
|
97
|
+
return { row: target, changed: false };
|
|
98
|
+
}
|
|
99
|
+
// 并发清掉其他行的默认标记(含历史脏数据里可能存在的多个默认)
|
|
100
|
+
await Promise.all(rows
|
|
101
|
+
.filter(r => r.id !== id && this.isDefault(r))
|
|
102
|
+
.map(r => db.set('tmp_guild_bind', r.id, { is_default: 0 })));
|
|
103
|
+
await db.set('tmp_guild_bind', id, { is_default: 1 });
|
|
104
|
+
target.is_default = 1;
|
|
105
|
+
for (const r of rows) {
|
|
106
|
+
if (r.id !== id) r.is_default = 0;
|
|
107
|
+
}
|
|
108
|
+
return { row: target, changed: true };
|
|
109
|
+
},
|
|
110
|
+
/**
|
|
111
|
+
* 按序号把该用户的某个绑定设为默认(序号即「我的绑定」列表里的序号)
|
|
112
|
+
* @returns { row, changed } row=null 表示序号越界
|
|
113
|
+
*/
|
|
114
|
+
async setDefaultByIndex(ctx, db, platform, userId, index) {
|
|
115
|
+
const rows = await this.list(db, platform, userId);
|
|
116
|
+
const idx = parseInt(index, 10);
|
|
117
|
+
if (isNaN(idx) || idx < 1 || idx > rows.length) {
|
|
118
|
+
return { row: null, changed: false, total: rows.length };
|
|
119
|
+
}
|
|
120
|
+
const result = await this.setDefault(ctx, db, platform, userId, rows[idx - 1].id);
|
|
121
|
+
return { ...result, total: rows.length };
|
|
122
|
+
},
|
|
59
123
|
/**
|
|
60
124
|
* 追加绑定(同一 tmpId 重复绑定时不会重复入库)
|
|
125
|
+
* 第一个绑定自动成为默认绑定。
|
|
61
126
|
* @param tmpId TMP ID
|
|
62
127
|
* @param tmpName TMP 玩家名称(用于多绑定列表展示,可为 null)
|
|
63
|
-
* @returns { exists, row } exists=true 表示该 tmpId 已绑定过
|
|
128
|
+
* @returns { exists, row, isDefault } exists=true 表示该 tmpId 已绑定过
|
|
64
129
|
*/
|
|
65
130
|
async add(db, platform, userId, tmpId, tmpName) {
|
|
66
131
|
const guildBindList = await this.list(db, platform, userId);
|
|
67
132
|
const existed = guildBindList.find(row => String(row.tmp_id) === String(tmpId));
|
|
68
133
|
if (existed) {
|
|
69
|
-
return { exists: true, row: existed };
|
|
134
|
+
return { exists: true, row: existed, isDefault: this.isDefault(existed) };
|
|
70
135
|
}
|
|
136
|
+
// 还没有任何默认绑定(含首次绑定、以及历史数据全无标记)时,新绑定即默认
|
|
137
|
+
const needDefault = !guildBindList.some(r => this.isDefault(r));
|
|
71
138
|
const row = await db.create('tmp_guild_bind', {
|
|
72
139
|
platform: platform,
|
|
73
140
|
user_id: userId,
|
|
74
141
|
tmp_id: tmpId,
|
|
75
|
-
tmp_name: tmpName || null
|
|
142
|
+
tmp_name: tmpName || null,
|
|
143
|
+
is_default: needDefault ? 1 : 0
|
|
76
144
|
});
|
|
77
|
-
return { exists: false, row };
|
|
145
|
+
return { exists: false, row, isDefault: needDefault };
|
|
78
146
|
},
|
|
79
147
|
/**
|
|
80
|
-
*
|
|
148
|
+
* 删除指定主键的绑定;若删掉的正是默认绑定,自动把剩下的第一条顶为默认,
|
|
149
|
+
* 避免「有绑定却没有默认」的空档。
|
|
150
|
+
* @returns { removedDefault, promoted } promoted=被顶为默认的那行(无则 null)
|
|
81
151
|
*/
|
|
82
|
-
async remove(db, id) {
|
|
152
|
+
async remove(db, platform, userId, id) {
|
|
153
|
+
const rows = await this.list(db, platform, userId);
|
|
154
|
+
const target = rows.find(r => r.id === id);
|
|
155
|
+
const wasDefault = this.isDefault(target);
|
|
83
156
|
await db.remove('tmp_guild_bind', id);
|
|
157
|
+
if (!wasDefault) {
|
|
158
|
+
return { removedDefault: false, promoted: null };
|
|
159
|
+
}
|
|
160
|
+
const rest = rows.filter(r => r.id !== id);
|
|
161
|
+
if (rest.length === 0) {
|
|
162
|
+
return { removedDefault: true, promoted: null };
|
|
163
|
+
}
|
|
164
|
+
await db.set('tmp_guild_bind', rest[0].id, { is_default: 1 });
|
|
165
|
+
rest[0].is_default = 1;
|
|
166
|
+
return { removedDefault: true, promoted: rest[0] };
|
|
84
167
|
},
|
|
85
168
|
/**
|
|
86
|
-
*
|
|
169
|
+
* 删除指定用户的全部绑定(默认标记随行一起删掉,无需额外清理)
|
|
87
170
|
*/
|
|
88
171
|
async removeAll(db, platform, userId) {
|
|
89
172
|
await db.remove('tmp_guild_bind', {
|
package/lib/database/model.js
CHANGED
|
@@ -32,6 +32,12 @@ const modelArray = {
|
|
|
32
32
|
length: 100,
|
|
33
33
|
nullable: true,
|
|
34
34
|
comment: 'TMP玩家名称(绑定时记录,用于多绑定列表展示)'
|
|
35
|
+
},
|
|
36
|
+
is_default: {
|
|
37
|
+
type: 'unsigned',
|
|
38
|
+
length: 1,
|
|
39
|
+
nullable: true,
|
|
40
|
+
comment: '是否默认绑定:1=默认(排行榜等用到),0/null=普通'
|
|
35
41
|
}
|
|
36
42
|
},
|
|
37
43
|
tmp_translate_cache: {
|
package/lib/index.js
CHANGED
|
@@ -264,7 +264,7 @@ function registerBaseCommands(ctx, cfg) {
|
|
|
264
264
|
.action(async ({ session }, tmpId) => await commands.tmpBind(ctx, cfg, session, tmpId));
|
|
265
265
|
|
|
266
266
|
ctx.command('我的绑定')
|
|
267
|
-
.usage("查看已绑定的全部TMP
|
|
267
|
+
.usage("查看已绑定的全部TMP编号(★ 为默认绑定)")
|
|
268
268
|
.action(async ({ session }) => {
|
|
269
269
|
const bindings = await guildBind.list(ctx.database, session.platform, session.userId);
|
|
270
270
|
if (bindings.length === 0) {
|
|
@@ -272,8 +272,35 @@ function registerBaseCommands(ctx, cfg) {
|
|
|
272
272
|
}
|
|
273
273
|
// 旧绑定缺名字的先补查并写回
|
|
274
274
|
await guildBind.ensureNames(ctx, bindings);
|
|
275
|
-
const lines = bindings.map((b, i) =>
|
|
276
|
-
|
|
275
|
+
const lines = bindings.map((b, i) => {
|
|
276
|
+
const mark = guildBind.isDefault(b) ? '★ ' : ' ';
|
|
277
|
+
return `${mark}${i + 1}. ${b.tmp_name || '未知'} (${b.tmp_id})`;
|
|
278
|
+
});
|
|
279
|
+
if (bindings.length === 1) {
|
|
280
|
+
return `已绑定 ${bindings.length} 个编号:\n${lines.join('\n')}`;
|
|
281
|
+
}
|
|
282
|
+
return `已绑定 ${bindings.length} 个编号:\n${lines.join('\n')}\n★ 为默认绑定(排行榜使用);「设为默认 序号」可切换,「解绑 序号」可删除\n查询回复序号选择,回复 all 一次性查全部(定位/足迹除外)`;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
ctx.command('设为默认 <index:string>')
|
|
286
|
+
.usage("把某个绑定设为默认(序号见「我的绑定」),排行榜等指令将使用默认绑定")
|
|
287
|
+
.example("设为默认 2")
|
|
288
|
+
.action(async ({ session }, index) => {
|
|
289
|
+
if (!index || !index.trim()) {
|
|
290
|
+
return '请输入要设为默认的序号(序号见「我的绑定」)';
|
|
291
|
+
}
|
|
292
|
+
await guildBind.ensureNames(ctx, await guildBind.list(ctx.database, session.platform, session.userId));
|
|
293
|
+
const { row, changed, total } = await guildBind.setDefaultByIndex(
|
|
294
|
+
ctx, ctx.database, session.platform, session.userId, index.trim()
|
|
295
|
+
);
|
|
296
|
+
if (!row) {
|
|
297
|
+
if (!total) {
|
|
298
|
+
return '还没有绑定编号,使用「绑定 tmpId」添加';
|
|
299
|
+
}
|
|
300
|
+
return `序号无效,可用范围 1-${total}(序号见「我的绑定」)`;
|
|
301
|
+
}
|
|
302
|
+
const label = `${row.tmp_name || '未知'} (${row.tmp_id})`;
|
|
303
|
+
return changed ? `已将 ${label} 设为默认绑定` : `${label} 已经是默认绑定`;
|
|
277
304
|
});
|
|
278
305
|
|
|
279
306
|
ctx.command('解绑 <index:string>')
|
|
@@ -298,8 +325,12 @@ function registerBaseCommands(ctx, cfg) {
|
|
|
298
325
|
return `序号无效,可用范围 1-${bindings.length}(序号见「我的绑定」)`;
|
|
299
326
|
}
|
|
300
327
|
const row = bindings[idx - 1];
|
|
301
|
-
await guildBind.remove(ctx.database, row.id);
|
|
302
|
-
|
|
328
|
+
const { promoted } = await guildBind.remove(ctx.database, session.platform, session.userId, row.id);
|
|
329
|
+
let msg = `已解绑 ${idx}. ${row.tmp_name || '未知'} (${row.tmp_id})`;
|
|
330
|
+
if (promoted) {
|
|
331
|
+
msg += `\n默认绑定已自动切换到 ${promoted.tmp_name || '未知'} (${promoted.tmp_id})`;
|
|
332
|
+
}
|
|
333
|
+
return msg;
|
|
303
334
|
});
|
|
304
335
|
}
|
|
305
336
|
|
|
@@ -329,23 +360,23 @@ function registerBaseCommands(ctx, cfg) {
|
|
|
329
360
|
}
|
|
330
361
|
|
|
331
362
|
if (cfg.commands?.tmpMileageRanking) {
|
|
332
|
-
ctx.command('里程排行榜')
|
|
333
|
-
.usage("查询欧洲卡车模拟2
|
|
334
|
-
.action(async ({ session }) => await commands.tmpMileageRanking(ctx, session, MileageRankingType.total));
|
|
363
|
+
ctx.command('里程排行榜 [target]')
|
|
364
|
+
.usage("查询欧洲卡车模拟2里程排行榜(可 @某人 查看对方的排名)")
|
|
365
|
+
.action(async ({ session }, target) => await commands.tmpMileageRanking(ctx, session, MileageRankingType.total, target));
|
|
335
366
|
|
|
336
|
-
ctx.command('今日里程排行榜')
|
|
337
|
-
.usage("查询欧洲卡车模拟2
|
|
338
|
-
.action(async ({ session }) => await commands.tmpMileageRanking(ctx, session, MileageRankingType.today));
|
|
367
|
+
ctx.command('今日里程排行榜 [target]')
|
|
368
|
+
.usage("查询欧洲卡车模拟2今日里程排行榜(可 @某人 查看对方的排名)")
|
|
369
|
+
.action(async ({ session }, target) => await commands.tmpMileageRanking(ctx, session, MileageRankingType.today, target));
|
|
339
370
|
}
|
|
340
371
|
|
|
341
372
|
if (cfg.commands?.tmpVtcMileageRanking) {
|
|
342
|
-
ctx.command('vtc里程排行榜')
|
|
343
|
-
.usage("
|
|
344
|
-
.action(async ({ session }) => await commands.tmpVtcMileageRanking(ctx, cfg, session, MileageRankingType.total));
|
|
373
|
+
ctx.command('vtc里程排行榜 [target]')
|
|
374
|
+
.usage("查询车队总里程排行榜(可 @某人 查看对方的排名)")
|
|
375
|
+
.action(async ({ session }, target) => await commands.tmpVtcMileageRanking(ctx, cfg, session, MileageRankingType.total, target));
|
|
345
376
|
|
|
346
|
-
ctx.command('vtc今日里程排行榜')
|
|
347
|
-
.usage("
|
|
348
|
-
.action(async ({ session }) => await commands.tmpVtcMileageRanking(ctx, cfg, session, MileageRankingType.today));
|
|
377
|
+
ctx.command('vtc今日里程排行榜 [target]')
|
|
378
|
+
.usage("查询车队今日里程排行榜(可 @某人 查看对方的排名)")
|
|
379
|
+
.action(async ({ session }, target) => await commands.tmpVtcMileageRanking(ctx, cfg, session, MileageRankingType.today, target));
|
|
349
380
|
}
|
|
350
381
|
|
|
351
382
|
if (cfg.commands?.pointRanking) {
|
package/lib/util/tmpIdPicker.js
CHANGED
|
@@ -1,24 +1,138 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 多绑定选择器:需要选 tmpId 时列出序号让用户回复选择(支持多选)。
|
|
3
3
|
* 原理:需要选择的指令先回复编号列表并挂起(pending),由前置中间件捕获用户下一条
|
|
4
4
|
* "纯序号"消息后解析并继续执行;超时/取消/其他内容均视为放弃。
|
|
5
|
+
*
|
|
6
|
+
* 归属:挂起只认**发起指令的人**。@别人 查询时列表显示被 @ 的人(以及对方的绑定),
|
|
7
|
+
* 但回复序号仍必须由发起者本人来发;被 @ 的人或群里其他人回复不会被当作选择。
|
|
5
8
|
*/
|
|
6
9
|
const MAX_PICK = 5; // 单次多选上限
|
|
7
10
|
const PICK_TIMEOUT = 60_000; // 等待回复时长(毫秒)
|
|
11
|
+
/** 「全部」哨兵:回复 all / 全部,或把 all 当作参数传入时使用 */
|
|
12
|
+
const ALL = Symbol('tmpIdPicker.ALL');
|
|
8
13
|
const guildBind = require('../database/guildBind');
|
|
9
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 判断一个参数是不是「全部」写法(all 大小写不敏感,也接受「全部」)
|
|
17
|
+
*/
|
|
18
|
+
function isAllArg(value) {
|
|
19
|
+
if (typeof value !== 'string') return false;
|
|
20
|
+
const v = value.trim();
|
|
21
|
+
return /^all$/i.test(v) || v === '全部';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 把「全部绑定」解析成 tmpId 数组。截断规则与多选一致(最多 MAX_PICK 个)。
|
|
26
|
+
* @returns { ids: string[], total: number, truncated: boolean }
|
|
27
|
+
*/
|
|
28
|
+
function resolveAll(bindings, max = MAX_PICK) {
|
|
29
|
+
const list = bindings || [];
|
|
30
|
+
const ids = list.slice(0, max).map(b => String(b.tmp_id));
|
|
31
|
+
return { ids, total: list.length, truncated: list.length > ids.length };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 供指令层处理 `xxx all` 写法:统一取「本人全部绑定」并补全名字。
|
|
36
|
+
* @returns {
|
|
37
|
+
* ids: string[], 要逐个查询的 tmpId(已截断到 MAX_PICK)
|
|
38
|
+
* total: number, 绑定总数
|
|
39
|
+
* truncated: boolean, 是否因上限被截断
|
|
40
|
+
* hint: string|null, 需要附加提示时的文案(截断 / 无绑定),无则 null
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
async function pickAll(ctx, session, bindingRows) {
|
|
44
|
+
const list = bindingRows || [];
|
|
45
|
+
if (list.length === 0) {
|
|
46
|
+
return { ids: [], total: 0, truncated: false, hint: null };
|
|
47
|
+
}
|
|
48
|
+
// 补全旧数据缺失的玩家名(复用列表展示那套逻辑,保证提示文案准确)
|
|
49
|
+
await guildBind.ensureNames(ctx, list);
|
|
50
|
+
const { ids, total, truncated } = resolveAll(list);
|
|
51
|
+
return {
|
|
52
|
+
ids,
|
|
53
|
+
total,
|
|
54
|
+
truncated,
|
|
55
|
+
hint: truncated ? `共 ${total} 个绑定,本次查询前 ${ids.length} 个` : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
10
59
|
/**
|
|
11
60
|
* key: `${platform}:${channelId}:${userId}` → { bindings, allowMulti, max, finish }
|
|
12
61
|
* 按平台+频道+用户隔离,避免群里其他人误触发。
|
|
13
62
|
*/
|
|
14
63
|
const pending = new Map();
|
|
15
64
|
|
|
65
|
+
/**
|
|
66
|
+
* 被 @ 的成员 id → { platform:id → 展示名 }。会话文本里的 @ 段通常带 name,
|
|
67
|
+
* 缺失时(纯 <at id="..."/>)从事件成员列表兜底拿昵称。
|
|
68
|
+
*/
|
|
69
|
+
const atNameCache = new Map();
|
|
70
|
+
|
|
71
|
+
/** QQ 号与 koishi 的 `qq:` 前缀可能不一致,统一成纯数字做 key */
|
|
72
|
+
function normalizeId(id) {
|
|
73
|
+
return String(id == null ? '' : id).replace(/^[a-z]+:/i, '');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 从消息文本/元素数组里遍历出所有被 @ 的人。
|
|
78
|
+
* 解析失败时返回 { failed: true },由调用方决定是否降级为默认绑定。
|
|
79
|
+
* @returns {Array<{ id: string, name: string }> | { failed: true }}
|
|
80
|
+
*/
|
|
81
|
+
function parseAt(target) {
|
|
82
|
+
if (target == null) return { failed: true };
|
|
83
|
+
if (Array.isArray(target)) {
|
|
84
|
+
const els = target.filter(el => el && typeof el === 'object' && el.type === 'at' && el.attrs && el.attrs.id != null);
|
|
85
|
+
if (els.length === 0) return { failed: true };
|
|
86
|
+
return els.map(el => ({ id: String(el.attrs.id), name: el.attrs.name || '' }));
|
|
87
|
+
}
|
|
88
|
+
const text = String(target);
|
|
89
|
+
if (!text.includes('<at ')) return [];
|
|
90
|
+
const out = [];
|
|
91
|
+
const re = /<at\s+([^>]*)>/g;
|
|
92
|
+
let m;
|
|
93
|
+
while ((m = re.exec(text)) !== null) {
|
|
94
|
+
const attrs = m[1];
|
|
95
|
+
const idm = /\bid\s*=\s*"([^"]*)"/.exec(attrs);
|
|
96
|
+
const nm = /\bname\s*=\s*"([^"]*)"/.exec(attrs);
|
|
97
|
+
if (idm && idm[1]) out.push({ id: idm[1], name: nm ? nm[1] : '' });
|
|
98
|
+
}
|
|
99
|
+
// 有 <at 标记却一个都没解析出来 → 格式异常,交调用方降级
|
|
100
|
+
if (out.length === 0) return { failed: true };
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 取 @ 对象的展示名(仅用于 @别人 时把对方名字写进列表标题)
|
|
106
|
+
*/
|
|
107
|
+
function displayName(session, at) {
|
|
108
|
+
if (at.name) return at.name;
|
|
109
|
+
for (const key of ['member', 'author', 'user']) {
|
|
110
|
+
const obj = session[key];
|
|
111
|
+
if (obj && normalizeId(obj.id) === normalizeId(at.id) && (obj.nick || obj.name)) {
|
|
112
|
+
return obj.nick || obj.name;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (session.event && Array.isArray(session.event.member)) {
|
|
116
|
+
const hit = session.event.member.find(mm => normalizeId(mm.user && mm.user.id) === normalizeId(at.id));
|
|
117
|
+
if (hit && hit.user && (hit.user.nick || hit.user.name)) {
|
|
118
|
+
return hit.user.nick || hit.user.name;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const sk = session.platform + ':' + normalizeId(at.id);
|
|
122
|
+
const cached = atNameCache.get(sk);
|
|
123
|
+
return cached || normalizeId(at.id);
|
|
124
|
+
}
|
|
125
|
+
|
|
16
126
|
function keyOf(session) {
|
|
17
127
|
return `${session.platform}:${session.channelId || 'private'}:${session.userId}`;
|
|
18
128
|
}
|
|
19
129
|
|
|
20
130
|
/**
|
|
21
131
|
* 注册选择器中间件(在插件 apply 中调用一次,前置执行)
|
|
132
|
+
*
|
|
133
|
+
* 归属规则:挂起记录只挂在**发起指令的那个人**的 key 上(keyOf 里的 session.userId)。
|
|
134
|
+
* 所以「A 发 查询 @B」时,只有 A 回复序号才算数;B 或群里其他人回复一律放行给原指令,
|
|
135
|
+
* 不会替 A 做选择。
|
|
22
136
|
*/
|
|
23
137
|
function register(ctx) {
|
|
24
138
|
ctx.middleware((session, next) => {
|
|
@@ -31,6 +145,21 @@ function register(ctx) {
|
|
|
31
145
|
record.finish(null);
|
|
32
146
|
return;
|
|
33
147
|
}
|
|
148
|
+
// 「全部」:选中全部绑定(同样受 max 截断,超出时提示)
|
|
149
|
+
// 不发图/重负载的指令(如足迹)会把 allowAll 置 false,此分支退回按普通内容处理
|
|
150
|
+
if (record.allowAll !== false && isAllArg(content)) {
|
|
151
|
+
const { ids, total, truncated } = resolveAll(record.bindings, record.allowMulti ? record.max : 1);
|
|
152
|
+
if (ids.length === 0) {
|
|
153
|
+
record.finish(null);
|
|
154
|
+
session.send('没有可查询的绑定,已取消').catch(() => { });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (truncated) {
|
|
158
|
+
session.send(`共 ${total} 个绑定,本次查询前 ${ids.length} 个`).catch(() => { });
|
|
159
|
+
}
|
|
160
|
+
record.finish(ids);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
34
163
|
// 纯序号消息(支持 1,3 / 1 3 / 1、3 等分隔写法)
|
|
35
164
|
if (/^\d{1,6}(?:\s*[,,、 ]\s*\d{1,6})*$/.test(content)) {
|
|
36
165
|
const indexes = [...content.matchAll(/\d{1,6}/g)].map(m => parseInt(m[0], 10));
|
|
@@ -54,8 +183,14 @@ function register(ctx) {
|
|
|
54
183
|
|
|
55
184
|
/**
|
|
56
185
|
* 需要绑定 tmpId 的指令调用:绑定数 ≤1 时直接返回不打扰,≥2 时列出序号并等待选择。
|
|
57
|
-
*
|
|
58
|
-
* @param
|
|
186
|
+
* 挂起只认调用本方法的会话(发起者),别人回复无效。
|
|
187
|
+
* @param bindings guildBind.list() / listOf() 的结果
|
|
188
|
+
* @param opts {
|
|
189
|
+
* allowMulti?: boolean, 是否允许多选(默认允许)
|
|
190
|
+
* allowAll?: boolean, 是否允许回复 all 一次选全部(默认允许);
|
|
191
|
+
* 出图/重负载指令(足迹)传 false —— 避免一次请求打满服务器
|
|
192
|
+
* ownerName?: string, 列表标题里显示的名字(@别人 查询时用;仅影响文案)
|
|
193
|
+
* }
|
|
59
194
|
* @returns 选中的 tmpId 字符串数组;返回 null 表示已在等待用户回复,调用方应直接 return
|
|
60
195
|
*/
|
|
61
196
|
async function pick(ctx, session, bindings, opts = {}) {
|
|
@@ -64,17 +199,27 @@ async function pick(ctx, session, bindings, opts = {}) {
|
|
|
64
199
|
await guildBind.ensureNames(ctx, bindings);
|
|
65
200
|
if (bindings.length === 1) return [String(bindings[0].tmp_id)];
|
|
66
201
|
const allowMulti = opts.allowMulti !== false;
|
|
202
|
+
const allowAll = opts.allowAll !== false;
|
|
67
203
|
const max = allowMulti ? MAX_PICK : 1;
|
|
204
|
+
// @别人 查询:对方的名字(提到过就复用,否则用 id 代替)
|
|
205
|
+
let ownerName = opts.ownerName || '';
|
|
206
|
+
if (!ownerName && opts.ownerKey && String(opts.ownerKey) !== session.userId) {
|
|
207
|
+
ownerName = atNameCache.get(session.platform + ':' + normalizeId(opts.ownerKey)) || normalizeId(opts.ownerKey);
|
|
208
|
+
}
|
|
68
209
|
const lines = bindings.map((b, i) => `${i + 1}. ${b.tmp_name || '未知'} (${b.tmp_id})`);
|
|
69
210
|
const tip = allowMulti
|
|
70
211
|
? `请回复序号选择(最多 ${max} 个,多选用逗号分隔,如 1,3)`
|
|
212
|
+
+ (allowAll ? ',或回复 all 查询全部' : '')
|
|
71
213
|
: '请回复一个序号';
|
|
72
|
-
|
|
214
|
+
const prefix = ownerName ? `@${ownerName} 绑定了多个编号:` : '您绑定了多个编号:';
|
|
215
|
+
await session.send(`${prefix}\n${lines.join('\n')}\n${tip}\n回复"取消"放弃,60 秒内有效`);
|
|
73
216
|
return await new Promise(resolve => {
|
|
217
|
+
// key 里带的是 session.userId(发起者),所以只有发起者能回复
|
|
74
218
|
const key = keyOf(session);
|
|
75
219
|
const record = {
|
|
76
220
|
bindings,
|
|
77
221
|
allowMulti,
|
|
222
|
+
allowAll,
|
|
78
223
|
max,
|
|
79
224
|
timer: null,
|
|
80
225
|
finish(result) {
|
|
@@ -94,4 +239,11 @@ async function pick(ctx, session, bindings, opts = {}) {
|
|
|
94
239
|
});
|
|
95
240
|
}
|
|
96
241
|
|
|
97
|
-
module.exports = {
|
|
242
|
+
module.exports = {
|
|
243
|
+
register, pick, MAX_PICK, PICK_TIMEOUT,
|
|
244
|
+
parseAt, normalizeId,
|
|
245
|
+
isAllArg, resolveAll, ALL, pickAll,
|
|
246
|
+
cacheAtName(platform, id, name) {
|
|
247
|
+
if (platform && id != null && name) atNameCache.set(platform + ':' + normalizeId(id), name);
|
|
248
|
+
},
|
|
249
|
+
};
|