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.
@@ -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.startsWith("<at ")) {
9
- if (queryQQ.startsWith('<at ')) {
10
- queryQQ = queryQQ.replace('<at ', '');
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
- let id = '';
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
  }
@@ -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
- return `该编号已在绑定列表中 ( ${playerInfo.data.name} ),无需重复绑定`;
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
- return `绑定成功 ( ${playerInfo.data.name} ),当前已绑定 ${count} 个编号`;
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.startsWith("<at ")) {
61
- if (tmpId.startsWith('<at ')) {
62
- queryQQ = tmpId.replace('<at ', '');
63
- }
64
- let id = '';
65
- const idStart = queryQQ.indexOf('id="');
66
- if (idStart !== -1) {
67
- const valueStart = idStart + 4;
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
- const bindings = await guildBind.list(ctx.database, session.platform, session.userId);
21
- let bindTmpId = null;
22
- if (bindings.length === 1) {
23
- bindTmpId = String(bindings[0].tmp_id);
24
- } else if (bindings.length > 1) {
25
- const picked = await tmpIdPicker.pick(ctx, session, bindings, { allowMulti: false });
26
- if (picked === null) {
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
- bindTmpId = picked[0];
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
- if (tmpId && tmpId.startsWith("<at ")) {
18
- if (tmpId.startsWith('<at ')) {
19
- queryQQ = tmpId.replace('<at ', '');
20
- }
21
- let id = '';
22
- const idStart = queryQQ.indexOf('id="');
23
- if (idStart !== -1) {
24
- const valueStart = idStart + 4;
25
- const valueEnd = queryQQ.indexOf('"', valueStart);
26
- if (valueEnd !== -1) {
27
- id = queryQQ.substring(valueStart, valueEnd);
28
- }
29
- }
30
- queryQQ = id;
31
- let guildBindData = await guildBind.get(ctx.database, session.platform, queryQQ);
32
- if (!guildBindData) {
33
- return `该用户没有绑定玩家编号`;
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
- tmpId = guildBindData.tmp_id;
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
- if (tmpId && tmpId.startsWith("<at ")) {
68
- let queryQQ = tmpId.replace('<at ', '');
69
- let id = '';
70
- const idStart = queryQQ.indexOf('id="');
71
- if (idStart !== -1) {
72
- const valueStart = idStart + 4;
73
- const valueEnd = queryQQ.indexOf('"', valueStart);
74
- if (valueEnd !== -1) {
75
- id = queryQQ.substring(valueStart, valueEnd);
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
- queryQQ = id;
79
- let guildBindData = await guildBind.get(ctx.database, session.platform, queryQQ);
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
- tmpId = guildBindData.tmp_id;
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
- if (tmpId && tmpId.startsWith("<at ")) {
54
- if (tmpId.startsWith('<at ')) {
55
- queryQQ = tmpId.replace('<at ', '');
56
- }
57
- let id = '';
58
- const idStart = queryQQ.indexOf('id="');
59
- if (idStart !== -1) {
60
- const valueStart = idStart + 4;
61
- const valueEnd = queryQQ.indexOf('"', valueStart);
62
- if (valueEnd !== -1) {
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
- queryQQ = id;
67
- let guildBindData = await guildBind.get(ctx.database, session.platform, queryQQ);
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
- tmpId = guildBindData.tmp_id;
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
- const bindings = await guildBind.list(ctx.database, session.platform, session.userId);
25
- let bindTmpId = null;
26
- if (bindings.length === 1) {
27
- bindTmpId = String(bindings[0].tmp_id);
28
- } else if (bindings.length > 1) {
29
- const picked = await tmpIdPicker.pick(ctx, session, bindings, { allowMulti: false });
30
- if (picked === null) {
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
- bindTmpId = picked[0];
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
- * get() 返回第一行(默认绑定),保持与旧版行为兼容。
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 guildBindList = await db.get('tmp_guild_bind', {
37
- platform,
38
- user_id: userId
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 null;
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', {
@@ -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) => `${i + 1}. ${b.tmp_name || '未知'} (${b.tmp_id})`);
276
- return `已绑定 ${bindings.length} 个编号:\n${lines.join('\n')}${bindings.length > 1 ? '\n查询时会列出序号供选择;使用「解绑 序号」可删除' : ''}`;
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
- return `已解绑 ${idx}. ${row.tmp_name || '未知'} (${row.tmp_id})`;
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) {
@@ -1,24 +1,138 @@
1
1
  /**
2
- * 多绑定选择器:用户绑定了多个 tmpId 时,列出序号让用户回复选择(支持多选)。
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
- * @param bindings guildBind.list() 的结果
58
- * @param opts { allowMulti?: boolean } 是否允许多选(默认允许)
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
- await session.send(`您绑定了多个编号:\n${lines.join('\n')}\n${tip}\n回复"取消"放弃,60 秒内有效`);
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 = { register, pick, MAX_PICK, PICK_TIMEOUT };
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
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-ets2-tools-tmp",
3
3
  "description": "欧卡2 TruckersMP信息查询、车队平台查询及活动提醒",
4
- "version": "3.4.2",
4
+ "version": "3.4.3",
5
5
  "contributors": [
6
6
  "opwop <slhp1013@qq.com>"
7
7
  ],