koishi-plugin-rocom 1.0.3 → 1.0.5
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/client.d.ts +1 -1
- package/lib/client.js +47 -107
- package/lib/commands/account.js +48 -3
- package/lib/commands/admin.js +29 -5
- package/lib/commands/egg.js +2 -0
- package/lib/commands/merchant.js +44 -28
- package/lib/commands/query.js +11 -0
- package/lib/commands/wiki.js +2 -0
- package/lib/index.js +145 -3872
- package/lib/render-templates/menu/index.html +5 -6
- package/lib/render-templates/menu/style.css +44 -46
- package/lib/render-templates/yuanxing-shangren/index.html +29 -29
- package/lib/render.js +1 -0
- package/lib/subscription-send.d.ts +2 -1
- package/lib/subscription-send.js +13 -0
- package/package.json +2 -2
package/lib/client.d.ts
CHANGED
|
@@ -14,12 +14,12 @@ export declare class RocomClient {
|
|
|
14
14
|
private sanitizeForLog;
|
|
15
15
|
private headersForLog;
|
|
16
16
|
private stringifyForLog;
|
|
17
|
-
private isApiKeyPermissionUndeclaredError;
|
|
18
17
|
private logRequestFailureDetails;
|
|
19
18
|
private get;
|
|
20
19
|
private post;
|
|
21
20
|
private delete;
|
|
22
21
|
private requestWithStatus;
|
|
22
|
+
private requestIngameWithFallback;
|
|
23
23
|
private getIngameTask;
|
|
24
24
|
qqQrLogin(ctx: Context, userIdentifier: string): Promise<any>;
|
|
25
25
|
qqQrStatus(ctx: Context, fwToken: string, userIdentifier: string): Promise<any>;
|
package/lib/client.js
CHANGED
|
@@ -99,11 +99,6 @@ class RocomClient {
|
|
|
99
99
|
return String(value);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
isApiKeyPermissionUndeclaredError(message) {
|
|
103
|
-
if (!message)
|
|
104
|
-
return false;
|
|
105
|
-
return /未声明\s*api\s*key\s*权限|api\s*key\s*permission|api key 权限/i.test(message);
|
|
106
|
-
}
|
|
107
102
|
logRequestFailureDetails(method, path, headers, params, body, errorOrResponse) {
|
|
108
103
|
const err = errorOrResponse;
|
|
109
104
|
const response = err?.response;
|
|
@@ -131,7 +126,7 @@ class RocomClient {
|
|
|
131
126
|
stack: err?.stack,
|
|
132
127
|
} : undefined,
|
|
133
128
|
};
|
|
134
|
-
|
|
129
|
+
logger.warn(`${method} ${path} detailed failure\n${this.stringifyForLog(details)}`);
|
|
135
130
|
}
|
|
136
131
|
async get(ctx, path, headers, params, options) {
|
|
137
132
|
try {
|
|
@@ -235,23 +230,58 @@ class RocomClient {
|
|
|
235
230
|
const body = response?.data !== undefined ? response.data : response;
|
|
236
231
|
if (body?.code !== undefined && body.code !== 0) {
|
|
237
232
|
this.setLastError(body.message || body.msg || '接口返回异常');
|
|
238
|
-
|
|
233
|
+
if (!options.silentFailureDetails) {
|
|
234
|
+
this.logRequestFailureDetails(method, path, headers, options.params, options.json, body);
|
|
235
|
+
}
|
|
239
236
|
return { status: null, data: null };
|
|
240
237
|
}
|
|
241
238
|
const data = body?.code !== undefined ? (body.data ?? {}) : (body ?? {});
|
|
242
239
|
if (!acceptedStatuses.includes(status)) {
|
|
243
240
|
this.setLastError(`HTTP ${status}`);
|
|
244
|
-
|
|
241
|
+
if (!options.silentFailureDetails) {
|
|
242
|
+
this.logRequestFailureDetails(method, path, headers, options.params, options.json, response);
|
|
243
|
+
}
|
|
244
|
+
return { status: null, data: null };
|
|
245
245
|
}
|
|
246
246
|
return { status, data };
|
|
247
247
|
}
|
|
248
248
|
catch (e) {
|
|
249
249
|
const message = this.formatHttpError(e);
|
|
250
250
|
this.setLastError(message);
|
|
251
|
-
|
|
251
|
+
if (!options.silentFailureDetails) {
|
|
252
|
+
this.logRequestFailureDetails(method, path, headers, options.params, options.json, e);
|
|
253
|
+
}
|
|
252
254
|
return { status: null, data: null };
|
|
253
255
|
}
|
|
254
256
|
}
|
|
257
|
+
async requestIngameWithFallback(ctx, path, payload, options = {}) {
|
|
258
|
+
const acceptedStatuses = options.acceptedStatuses || [200, 202];
|
|
259
|
+
const requestOnce = async (includeApiKey, silentFailureDetails) => {
|
|
260
|
+
const headers = this.wegameHeaders('', '', '', '', includeApiKey);
|
|
261
|
+
let result = await this.requestWithStatus(ctx, 'POST', path, headers, {
|
|
262
|
+
json: payload,
|
|
263
|
+
acceptedStatuses,
|
|
264
|
+
silentFailureDetails,
|
|
265
|
+
});
|
|
266
|
+
if (result.status === null) {
|
|
267
|
+
result = await this.requestWithStatus(ctx, 'GET', path, headers, {
|
|
268
|
+
params: payload,
|
|
269
|
+
acceptedStatuses,
|
|
270
|
+
silentFailureDetails,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
};
|
|
275
|
+
let result = await requestOnce(false, Boolean(this.apiKey));
|
|
276
|
+
if (result.status !== null)
|
|
277
|
+
return { ...result, usedApiKey: false };
|
|
278
|
+
if (this.apiKey) {
|
|
279
|
+
result = await requestOnce(true, false);
|
|
280
|
+
if (result.status !== null)
|
|
281
|
+
return { ...result, usedApiKey: true };
|
|
282
|
+
}
|
|
283
|
+
return { status: null, data: null, usedApiKey: false };
|
|
284
|
+
}
|
|
255
285
|
async getIngameTask(ctx, taskId, includeApiKey = true) {
|
|
256
286
|
return this.requestWithStatus(ctx, 'GET', `/api/v1/games/rocom/ingame/tasks/${taskId}`, this.wegameHeaders('', '', '', '', includeApiKey), { acceptedStatuses: [200, 202] });
|
|
257
287
|
}
|
|
@@ -339,52 +369,14 @@ class RocomClient {
|
|
|
339
369
|
async ingamePlayerSearch(ctx, uid) {
|
|
340
370
|
const sanitizedUid = this.sanitizeUid(uid);
|
|
341
371
|
if (!sanitizedUid) {
|
|
342
|
-
this.setLastError('UID
|
|
372
|
+
this.setLastError('UID 不能为空');
|
|
343
373
|
return null;
|
|
344
374
|
}
|
|
345
375
|
const path = '/api/v1/games/rocom/ingame/player/search';
|
|
346
|
-
let includeApiKey = true;
|
|
347
|
-
let headers = this.wegameHeaders();
|
|
348
376
|
const payload = { uid: sanitizedUid, wait_ms: 5000 };
|
|
349
|
-
|
|
350
|
-
json: payload,
|
|
351
|
-
acceptedStatuses: [200, 202],
|
|
352
|
-
});
|
|
377
|
+
const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path, payload);
|
|
353
378
|
if (status === 200 && data && this.isIngamePlayerPayload(data))
|
|
354
379
|
return data;
|
|
355
|
-
if (status === null) {
|
|
356
|
-
const fallback = await this.requestWithStatus(ctx, 'GET', path, headers, {
|
|
357
|
-
params: payload,
|
|
358
|
-
acceptedStatuses: [200, 202],
|
|
359
|
-
});
|
|
360
|
-
status = fallback.status;
|
|
361
|
-
data = fallback.data;
|
|
362
|
-
if (status === 200 && data && this.isIngamePlayerPayload(data))
|
|
363
|
-
return data;
|
|
364
|
-
}
|
|
365
|
-
if (status === null && includeApiKey && this.apiKey && this.isApiKeyPermissionUndeclaredError(this.getLastError(''))) {
|
|
366
|
-
logger.warn('ingame/player/search rejected X-API-Key, retrying without API key');
|
|
367
|
-
includeApiKey = false;
|
|
368
|
-
headers = this.wegameHeaders('', '', '', '', false);
|
|
369
|
-
const retry = await this.requestWithStatus(ctx, 'POST', path, headers, {
|
|
370
|
-
json: payload,
|
|
371
|
-
acceptedStatuses: [200, 202],
|
|
372
|
-
});
|
|
373
|
-
status = retry.status;
|
|
374
|
-
data = retry.data;
|
|
375
|
-
if (status === 200 && data && this.isIngamePlayerPayload(data))
|
|
376
|
-
return data;
|
|
377
|
-
if (status === null) {
|
|
378
|
-
const fallback = await this.requestWithStatus(ctx, 'GET', path, headers, {
|
|
379
|
-
params: payload,
|
|
380
|
-
acceptedStatuses: [200, 202],
|
|
381
|
-
});
|
|
382
|
-
status = fallback.status;
|
|
383
|
-
data = fallback.data;
|
|
384
|
-
if (status === 200 && data && this.isIngamePlayerPayload(data))
|
|
385
|
-
return data;
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
380
|
if (!data)
|
|
389
381
|
return null;
|
|
390
382
|
if (this.isIngamePlayerPayload(data))
|
|
@@ -397,7 +389,7 @@ class RocomClient {
|
|
|
397
389
|
}
|
|
398
390
|
for (let i = 0; i < 8; i++) {
|
|
399
391
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
400
|
-
const task = await this.getIngameTask(ctx, taskId,
|
|
392
|
+
const task = await this.getIngameTask(ctx, taskId, usedApiKey);
|
|
401
393
|
if (task.status === 200)
|
|
402
394
|
return task.data;
|
|
403
395
|
if (task.status === null)
|
|
@@ -429,52 +421,14 @@ class RocomClient {
|
|
|
429
421
|
async ingameHomeInfo(ctx, uid, waitMs = 5000) {
|
|
430
422
|
const sanitizedUid = this.sanitizeUid(uid);
|
|
431
423
|
if (!sanitizedUid) {
|
|
432
|
-
this.setLastError('UID
|
|
424
|
+
this.setLastError('UID 不能为空');
|
|
433
425
|
return null;
|
|
434
426
|
}
|
|
435
427
|
const path = '/api/v1/games/rocom/ingame/home/info';
|
|
436
|
-
let includeApiKey = true;
|
|
437
|
-
let headers = this.wegameHeaders();
|
|
438
428
|
const payload = { uid: sanitizedUid, wait_ms: waitMs };
|
|
439
|
-
|
|
440
|
-
json: payload,
|
|
441
|
-
acceptedStatuses: [200, 202],
|
|
442
|
-
});
|
|
429
|
+
const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path, payload);
|
|
443
430
|
if (status === 200 && data && !(data.task_id || data.taskId || data.taskID))
|
|
444
431
|
return data;
|
|
445
|
-
if (status === null) {
|
|
446
|
-
const fallback = await this.requestWithStatus(ctx, 'GET', path, headers, {
|
|
447
|
-
params: payload,
|
|
448
|
-
acceptedStatuses: [200, 202],
|
|
449
|
-
});
|
|
450
|
-
status = fallback.status;
|
|
451
|
-
data = fallback.data;
|
|
452
|
-
if (status === 200 && data && !(data.task_id || data.taskId || data.taskID))
|
|
453
|
-
return data;
|
|
454
|
-
}
|
|
455
|
-
if (status === null && includeApiKey && this.apiKey && this.isApiKeyPermissionUndeclaredError(this.getLastError(''))) {
|
|
456
|
-
logger.warn('ingame/home/info rejected X-API-Key, retrying without API key');
|
|
457
|
-
includeApiKey = false;
|
|
458
|
-
headers = this.wegameHeaders('', '', '', '', false);
|
|
459
|
-
const retry = await this.requestWithStatus(ctx, 'POST', path, headers, {
|
|
460
|
-
json: payload,
|
|
461
|
-
acceptedStatuses: [200, 202],
|
|
462
|
-
});
|
|
463
|
-
status = retry.status;
|
|
464
|
-
data = retry.data;
|
|
465
|
-
if (status === 200 && data && !(data.task_id || data.taskId || data.taskID))
|
|
466
|
-
return data;
|
|
467
|
-
if (status === null) {
|
|
468
|
-
const fallback = await this.requestWithStatus(ctx, 'GET', path, headers, {
|
|
469
|
-
params: payload,
|
|
470
|
-
acceptedStatuses: [200, 202],
|
|
471
|
-
});
|
|
472
|
-
status = fallback.status;
|
|
473
|
-
data = fallback.data;
|
|
474
|
-
if (status === 200 && data && !(data.task_id || data.taskId || data.taskID))
|
|
475
|
-
return data;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
432
|
const taskId = data?.task_id || data?.taskId || data?.taskID;
|
|
479
433
|
if (!taskId) {
|
|
480
434
|
if (status === 202)
|
|
@@ -483,7 +437,7 @@ class RocomClient {
|
|
|
483
437
|
}
|
|
484
438
|
for (let i = 0; i < 10; i++) {
|
|
485
439
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
486
|
-
const task = await this.getIngameTask(ctx, taskId,
|
|
440
|
+
const task = await this.getIngameTask(ctx, taskId, usedApiKey);
|
|
487
441
|
if (task.status === 200)
|
|
488
442
|
return task.data;
|
|
489
443
|
if (task.status === null)
|
|
@@ -494,22 +448,8 @@ class RocomClient {
|
|
|
494
448
|
}
|
|
495
449
|
async ingameMerchantInfo(ctx, shopId) {
|
|
496
450
|
const params = { shop_id: shopId };
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if (data)
|
|
500
|
-
return data;
|
|
501
|
-
const postData = await this.post(ctx, '/api/v1/games/rocom/ingame/merchant/info', headers, params);
|
|
502
|
-
if (postData)
|
|
503
|
-
return postData;
|
|
504
|
-
if (this.apiKey && this.isApiKeyPermissionUndeclaredError(this.getLastError(''))) {
|
|
505
|
-
logger.warn('ingame/merchant/info rejected X-API-Key, retrying without API key');
|
|
506
|
-
headers = this.wegameHeaders('', '', '', '', false);
|
|
507
|
-
const fallbackData = await this.get(ctx, '/api/v1/games/rocom/ingame/merchant/info', headers, params, { silentFailureDetails: true });
|
|
508
|
-
if (fallbackData)
|
|
509
|
-
return fallbackData;
|
|
510
|
-
return this.post(ctx, '/api/v1/games/rocom/ingame/merchant/info', headers, params);
|
|
511
|
-
}
|
|
512
|
-
return null;
|
|
451
|
+
const { status, data } = await this.requestIngameWithFallback(ctx, '/api/v1/games/rocom/ingame/merchant/info', params);
|
|
452
|
+
return status === null ? null : data;
|
|
513
453
|
}
|
|
514
454
|
async getFriendship(ctx, fwToken, userIds, userIdentifier = '') {
|
|
515
455
|
return this.get(ctx, '/api/v1/games/rocom/social/friendship', this.rocomHeaders(fwToken, userIdentifier), { user_ids: userIds });
|
package/lib/commands/account.js
CHANGED
|
@@ -66,6 +66,7 @@ async function saveBindingWithRoleInfo(deps, session, fwToken, loginType, userId
|
|
|
66
66
|
function register(deps) {
|
|
67
67
|
const { ctx, client, userMgr } = deps;
|
|
68
68
|
ctx.command('洛克').subcommand('.QQ登录', 'QQ 扫码登录')
|
|
69
|
+
.alias('洛克QQ登录')
|
|
69
70
|
.action(async ({ session }) => {
|
|
70
71
|
const userId = session.userId;
|
|
71
72
|
const qrData = await client.qqQrLogin(ctx, userId);
|
|
@@ -91,6 +92,7 @@ function register(deps) {
|
|
|
91
92
|
return '登录超时或失败,请重试。';
|
|
92
93
|
});
|
|
93
94
|
ctx.command('洛克').subcommand('.微信登录', '微信扫码登录')
|
|
95
|
+
.alias('洛克微信登录')
|
|
94
96
|
.action(async ({ session }) => {
|
|
95
97
|
const userId = session.userId;
|
|
96
98
|
const qrData = await client.wechatQrLogin(ctx, userId);
|
|
@@ -115,6 +117,7 @@ function register(deps) {
|
|
|
115
117
|
return '登录超时或失败,请重试。';
|
|
116
118
|
});
|
|
117
119
|
ctx.command('洛克').subcommand('.导入 <tgpId:string> <tgpTicket:string>', '导入 WeGame 凭证')
|
|
120
|
+
.alias('洛克导入')
|
|
118
121
|
.action(async ({ session }, tgpId, tgpTicket) => {
|
|
119
122
|
if (!tgpId || !tgpTicket)
|
|
120
123
|
return '用法:洛克.导入 <tgp_id> <tgp_ticket>';
|
|
@@ -125,6 +128,7 @@ function register(deps) {
|
|
|
125
128
|
await saveBindingWithRoleInfo(deps, session, res.frameworkToken, 'manual', userId);
|
|
126
129
|
});
|
|
127
130
|
ctx.command('洛克').subcommand('.绑定列表', '查看已绑定账号')
|
|
131
|
+
.alias('洛克绑定列表')
|
|
128
132
|
.action(async ({ session }) => {
|
|
129
133
|
const bindings = userMgr.getUserBindings(session.userId);
|
|
130
134
|
if (!bindings.length)
|
|
@@ -153,14 +157,33 @@ function register(deps) {
|
|
|
153
157
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallbackLines.join('\n'), 'account:bind-list', deps.config);
|
|
154
158
|
});
|
|
155
159
|
ctx.command('洛克').subcommand('.切换 <index:number>', '切换主账号')
|
|
160
|
+
.alias('洛克切换')
|
|
156
161
|
.action(async ({ session }, index) => {
|
|
157
162
|
if (!index)
|
|
158
163
|
return '用法:洛克.切换 <序号>';
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
164
|
+
const userId = session.userId;
|
|
165
|
+
if (!userMgr.switchPrimary(userId, index))
|
|
166
|
+
return '序号无效。';
|
|
167
|
+
const newPrimary = userMgr.getPrimaryBinding(userId);
|
|
168
|
+
if (newPrimary?.binding_id) {
|
|
169
|
+
const token = await (0, role_token_1.getRoleToken)(ctx, userId);
|
|
170
|
+
if (token && token.bindingId !== newPrimary.binding_id) {
|
|
171
|
+
const res = await client.refreshBinding(ctx, newPrimary.binding_id, userId);
|
|
172
|
+
if (res?.framework_token) {
|
|
173
|
+
await (0, role_token_1.upsertRoleToken)(ctx, {
|
|
174
|
+
userId,
|
|
175
|
+
fwt: res.framework_token,
|
|
176
|
+
bindingId: newPrimary.binding_id,
|
|
177
|
+
roleId: newPrimary.role_id,
|
|
178
|
+
loginType: newPrimary.login_type,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return `成功切换到序号 ${index} 账号:${newPrimary?.nickname || '未知'}`;
|
|
162
184
|
});
|
|
163
185
|
ctx.command('洛克').subcommand('.解绑 <index:number>', '解绑账号')
|
|
186
|
+
.alias('洛克解绑')
|
|
164
187
|
.action(async ({ session }, index) => {
|
|
165
188
|
if (!index)
|
|
166
189
|
return '用法:洛克.解绑 <序号>';
|
|
@@ -178,9 +201,31 @@ function register(deps) {
|
|
|
178
201
|
if (userMgr.getUserBindings(session.userId).length === 0) {
|
|
179
202
|
await (0, role_token_1.removeRoleToken)(ctx, session.userId);
|
|
180
203
|
}
|
|
204
|
+
else {
|
|
205
|
+
const token = await (0, role_token_1.getRoleToken)(ctx, session.userId);
|
|
206
|
+
if (token?.bindingId === removed.binding_id) {
|
|
207
|
+
const newPrimary = userMgr.getPrimaryBinding(session.userId);
|
|
208
|
+
if (newPrimary?.binding_id) {
|
|
209
|
+
const res = await client.refreshBinding(ctx, newPrimary.binding_id, session.userId);
|
|
210
|
+
if (res?.framework_token) {
|
|
211
|
+
await (0, role_token_1.upsertRoleToken)(ctx, {
|
|
212
|
+
userId: session.userId,
|
|
213
|
+
fwt: res.framework_token,
|
|
214
|
+
bindingId: newPrimary.binding_id,
|
|
215
|
+
roleId: newPrimary.role_id,
|
|
216
|
+
loginType: newPrimary.login_type,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
await (0, role_token_1.removeRoleToken)(ctx, session.userId);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
181
225
|
return `已解绑账号:${removed.nickname}`;
|
|
182
226
|
});
|
|
183
227
|
ctx.command('洛克').subcommand('.刷新', '刷新当前主账号凭证')
|
|
228
|
+
.alias('洛克刷新')
|
|
184
229
|
.action(async ({ session }) => {
|
|
185
230
|
const userId = session.userId;
|
|
186
231
|
const binding = userMgr.getPrimaryBinding(userId);
|
package/lib/commands/admin.js
CHANGED
|
@@ -10,6 +10,7 @@ function register(deps) {
|
|
|
10
10
|
return config.adminUserIds.includes(userId);
|
|
11
11
|
}
|
|
12
12
|
ctx.command('洛克').subcommand('.刷新所有凭证', '刷新所有用户凭证(管理员)')
|
|
13
|
+
.alias('洛克刷新所有凭证')
|
|
13
14
|
.action(async ({ session }) => {
|
|
14
15
|
if (!isAdmin(session.userId))
|
|
15
16
|
return '此指令仅限管理员使用。';
|
|
@@ -44,6 +45,7 @@ function register(deps) {
|
|
|
44
45
|
return `刷新完成:成功 ${success},失败 ${fail}`;
|
|
45
46
|
});
|
|
46
47
|
ctx.command('洛克').subcommand('.删除失效绑定', '清理失效绑定(管理员)')
|
|
48
|
+
.alias('洛克删除失效绑定')
|
|
47
49
|
.action(async ({ session }) => {
|
|
48
50
|
if (!isAdmin(session.userId))
|
|
49
51
|
return '此指令仅限管理员使用。';
|
|
@@ -51,19 +53,33 @@ function register(deps) {
|
|
|
51
53
|
const allUsers = userMgr.getAllUsersBindings();
|
|
52
54
|
let totalInvalid = 0;
|
|
53
55
|
let totalValid = 0;
|
|
56
|
+
let totalSkipped = 0;
|
|
54
57
|
for (const [userId, bindings] of Object.entries(allUsers)) {
|
|
55
58
|
const token = await (0, role_token_1.getRoleToken)(ctx, userId);
|
|
56
59
|
if (!token?.fwt) {
|
|
57
60
|
logger.warn(`用户 ${userId} 没有可用的 fwt,跳过失效检测`);
|
|
61
|
+
totalSkipped += bindings.length;
|
|
58
62
|
continue;
|
|
59
63
|
}
|
|
60
|
-
const fwToken = token.fwt;
|
|
61
64
|
const valid = [];
|
|
62
65
|
for (const binding of bindings) {
|
|
63
|
-
const
|
|
66
|
+
const refreshRes = binding.binding_id
|
|
67
|
+
? await client.refreshBinding(ctx, binding.binding_id, userId)
|
|
68
|
+
: null;
|
|
69
|
+
const fwt = refreshRes?.framework_token || token.fwt;
|
|
70
|
+
const roleRes = await client.getRole(ctx, fwt, undefined, userId);
|
|
64
71
|
if (roleRes?.role) {
|
|
65
72
|
valid.push(binding);
|
|
66
73
|
totalValid++;
|
|
74
|
+
if (refreshRes?.framework_token) {
|
|
75
|
+
await (0, role_token_1.upsertRoleToken)(ctx, {
|
|
76
|
+
userId,
|
|
77
|
+
fwt: refreshRes.framework_token,
|
|
78
|
+
bindingId: binding.binding_id,
|
|
79
|
+
roleId: binding.role_id,
|
|
80
|
+
loginType: binding.login_type,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
67
83
|
continue;
|
|
68
84
|
}
|
|
69
85
|
if (binding.binding_id) {
|
|
@@ -81,16 +97,24 @@ function register(deps) {
|
|
|
81
97
|
await (0, role_token_1.removeRoleToken)(ctx, userId);
|
|
82
98
|
}
|
|
83
99
|
}
|
|
84
|
-
|
|
85
|
-
|
|
100
|
+
const parts = [`清理完成:移除 ${totalInvalid} 个无效绑定,剩余 ${totalValid} 个有效绑定。`];
|
|
101
|
+
if (totalSkipped)
|
|
102
|
+
parts.push(`跳过 ${totalSkipped} 个(无可用凭证)。`);
|
|
103
|
+
return totalInvalid > 0 || totalSkipped > 0
|
|
104
|
+
? parts.join('')
|
|
86
105
|
: `所有绑定均有效,无需清理。共 ${totalValid} 个有效绑定。`;
|
|
87
106
|
});
|
|
88
107
|
if (config.autoRefreshEnabled) {
|
|
108
|
+
let lastRefreshKey = '';
|
|
89
109
|
ctx.setInterval(async () => {
|
|
90
110
|
const now = new Date();
|
|
91
111
|
const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
92
112
|
if (!config.autoRefreshTime.includes(timeStr))
|
|
93
113
|
return;
|
|
114
|
+
const refreshKey = `${now.toDateString()}-${timeStr}`;
|
|
115
|
+
if (refreshKey === lastRefreshKey)
|
|
116
|
+
return;
|
|
117
|
+
lastRefreshKey = refreshKey;
|
|
94
118
|
const allUsers = userMgr.getAllUsersBindings();
|
|
95
119
|
for (const [userId] of Object.entries(allUsers)) {
|
|
96
120
|
const binding = userMgr.getPrimaryBinding(userId);
|
|
@@ -112,6 +136,6 @@ function register(deps) {
|
|
|
112
136
|
logger.warn(`自动刷新用户 ${userId} 失败: ${e}`);
|
|
113
137
|
}
|
|
114
138
|
}
|
|
115
|
-
},
|
|
139
|
+
}, 30000);
|
|
116
140
|
}
|
|
117
141
|
}
|
package/lib/commands/egg.js
CHANGED
|
@@ -48,6 +48,7 @@ async function sendEggImage(deps, session, templateName, data, fallback) {
|
|
|
48
48
|
function register(deps) {
|
|
49
49
|
const { ctx, client, eggService } = deps;
|
|
50
50
|
ctx.command('洛克').subcommand('.查蛋 [arg1:string] [arg2:string]', '查询精灵蛋组')
|
|
51
|
+
.alias('洛克查蛋')
|
|
51
52
|
.action(async ({ session }, arg1, arg2) => {
|
|
52
53
|
if (!arg1) {
|
|
53
54
|
return [
|
|
@@ -143,6 +144,7 @@ function register(deps) {
|
|
|
143
144
|
await sendEggImage(deps, session, 'searcheggs', data, hint + eggService.buildSearchText(pet));
|
|
144
145
|
});
|
|
145
146
|
ctx.command('洛克').subcommand('.配种 <nameA:string> [nameB:string]', '配种查询')
|
|
147
|
+
.alias('洛克配种')
|
|
146
148
|
.action(async ({ session }, nameA, nameB) => {
|
|
147
149
|
if (!nameA) {
|
|
148
150
|
return [
|
package/lib/commands/merchant.js
CHANGED
|
@@ -28,17 +28,25 @@ function formatProductWindow(product) {
|
|
|
28
28
|
const end = normalizeTimestamp(product?.end_time);
|
|
29
29
|
if (!start && !end)
|
|
30
30
|
return '';
|
|
31
|
+
const formatDate = (timestamp) => {
|
|
32
|
+
const date = new Date(timestamp);
|
|
33
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
34
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
35
|
+
return `${month}-${day}`;
|
|
36
|
+
};
|
|
31
37
|
const formatTime = (timestamp) => {
|
|
32
38
|
const date = new Date(timestamp);
|
|
33
39
|
const hour = String(date.getHours()).padStart(2, '0');
|
|
34
40
|
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
35
41
|
return `${hour}:${minute}`;
|
|
36
42
|
};
|
|
37
|
-
if (start && end)
|
|
38
|
-
|
|
43
|
+
if (start && end) {
|
|
44
|
+
const datePart = formatDate(start);
|
|
45
|
+
return `${datePart} ${formatTime(start)} – ${formatTime(end)}`;
|
|
46
|
+
}
|
|
39
47
|
if (start)
|
|
40
|
-
return `${formatTime(start)}+`;
|
|
41
|
-
return `${formatTime(end)}-`;
|
|
48
|
+
return `${formatDate(start)} ${formatTime(start)}+`;
|
|
49
|
+
return `${formatDate(end)} ${formatTime(end)}-`;
|
|
42
50
|
}
|
|
43
51
|
function getMerchantActivity(res) {
|
|
44
52
|
const activities = res?.merchantActivities || res?.merchant_activities || [];
|
|
@@ -83,6 +91,7 @@ function getCurrentMerchantRound() {
|
|
|
83
91
|
].join('-');
|
|
84
92
|
return {
|
|
85
93
|
current: currentRound,
|
|
94
|
+
total: rounds.length,
|
|
86
95
|
countdown: `${hours}\u5c0f\u65f6${mins}\u5206\u949f`,
|
|
87
96
|
is_open: currentRound !== null,
|
|
88
97
|
round_id: `${datePart}-${currentRound || 'closed'}`,
|
|
@@ -114,14 +123,37 @@ function isBotAdmin(session, adminUserIds) {
|
|
|
114
123
|
function sameStringArray(left, right) {
|
|
115
124
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
116
125
|
}
|
|
126
|
+
function buildMerchantRenderPayload(res) {
|
|
127
|
+
const products = getActiveProducts(res);
|
|
128
|
+
const roundInfo = getCurrentMerchantRound();
|
|
129
|
+
const activity = getMerchantActivity(res);
|
|
130
|
+
const data = {
|
|
131
|
+
background: '',
|
|
132
|
+
title: activity.name || TEXT.merchant,
|
|
133
|
+
subtitle: activity.start_date || '\u6bcf\u65e5 08:00 / 12:00 / 16:00 / 20:00 \u5237\u65b0',
|
|
134
|
+
titleIcon: true,
|
|
135
|
+
product_count: products.length,
|
|
136
|
+
round_info: roundInfo,
|
|
137
|
+
products: products.map((p) => ({
|
|
138
|
+
name: p.name || TEXT.unknown,
|
|
139
|
+
image: p.icon_url || '',
|
|
140
|
+
time_label: formatProductWindow(p),
|
|
141
|
+
})),
|
|
142
|
+
};
|
|
143
|
+
const fallback = products.length
|
|
144
|
+
? `\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff1a${products.map((p) => p.name || TEXT.unknown).join('\u3001')}\n\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\n\u5269\u4f59\uff1a${roundInfo.countdown}`
|
|
145
|
+
: '\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002';
|
|
146
|
+
return { products, roundInfo, data, fallback };
|
|
147
|
+
}
|
|
117
148
|
async function checkMerchantSubscriptions(deps) {
|
|
118
|
-
const { ctx, client, merchantSubMgr } = deps;
|
|
149
|
+
const { ctx, client, merchantSubMgr, renderer, config } = deps;
|
|
119
150
|
const res = await client.getMerchantInfo(ctx, true);
|
|
120
151
|
if (!res)
|
|
121
152
|
return { subscriptions: 0, matched: 0, pushed: 0 };
|
|
122
|
-
const products =
|
|
153
|
+
const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
|
|
123
154
|
const productNames = products.map((p) => p.name || '').filter(Boolean);
|
|
124
|
-
const
|
|
155
|
+
const rendered = await renderer.renderHtml(ctx, 'yuanxing-shangren', data);
|
|
156
|
+
const renderedPng = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
|
|
125
157
|
const subs = merchantSubMgr.getAll();
|
|
126
158
|
let matchedCount = 0;
|
|
127
159
|
let pushedCount = 0;
|
|
@@ -139,12 +171,13 @@ async function checkMerchantSubscriptions(deps) {
|
|
|
139
171
|
logger.warn(`\u63a8\u9001\u5931\u8d25 ${key}: \u65e0\u6cd5\u786e\u5b9a\u5e73\u53f0\u6216\u9891\u9053`);
|
|
140
172
|
continue;
|
|
141
173
|
}
|
|
142
|
-
const
|
|
174
|
+
const fallbackText = `${msg}\n${fallback}`;
|
|
175
|
+
const sent = await (0, subscription_send_1.sendScheduledImageWithFallback)(ctx, {
|
|
143
176
|
platform,
|
|
144
177
|
channelId,
|
|
145
178
|
guildId: sub.group_id || '',
|
|
146
179
|
userId: sub.user_id || '',
|
|
147
|
-
}, sub.mention_all
|
|
180
|
+
}, renderedPng, fallbackText, !!sub.mention_all);
|
|
148
181
|
if (!sent)
|
|
149
182
|
continue;
|
|
150
183
|
pushedCount++;
|
|
@@ -163,25 +196,7 @@ function register(deps) {
|
|
|
163
196
|
const res = await client.getMerchantInfo(ctx, true);
|
|
164
197
|
if (!res)
|
|
165
198
|
return '\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\u3002';
|
|
166
|
-
const
|
|
167
|
-
const roundInfo = getCurrentMerchantRound();
|
|
168
|
-
const activity = getMerchantActivity(res);
|
|
169
|
-
const data = {
|
|
170
|
-
background: '',
|
|
171
|
-
title: activity.name || TEXT.merchant,
|
|
172
|
-
subtitle: activity.start_date || '\u6bcf\u65e5 08:00 / 12:00 / 16:00 / 20:00 \u5237\u65b0',
|
|
173
|
-
titleIcon: '',
|
|
174
|
-
product_count: products.length,
|
|
175
|
-
round_info: roundInfo,
|
|
176
|
-
products: products.map((p) => ({
|
|
177
|
-
name: p.name || TEXT.unknown,
|
|
178
|
-
image: p.icon_url || '',
|
|
179
|
-
time_label: formatProductWindow(p),
|
|
180
|
-
})),
|
|
181
|
-
};
|
|
182
|
-
const fallback = products.length
|
|
183
|
-
? `\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff1a${products.map((p) => p.name || TEXT.unknown).join('\u3001')}\n\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\n\u5269\u4f59\uff1a${roundInfo.countdown}`
|
|
184
|
-
: '\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002';
|
|
199
|
+
const { data, fallback } = buildMerchantRenderPayload(res);
|
|
185
200
|
const png = await deps.renderer.renderHtml(ctx, 'yuanxing-shangren', data);
|
|
186
201
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
|
|
187
202
|
});
|
|
@@ -228,6 +243,7 @@ function register(deps) {
|
|
|
228
243
|
return '\u2705 \u5df2\u53d6\u6d88\u8fdc\u884c\u5546\u4eba\u8ba2\u9605\u3002';
|
|
229
244
|
});
|
|
230
245
|
ctx.command('洛克').subcommand('.调试远行商人订阅', '立即执行一次远行商人订阅检查')
|
|
246
|
+
.alias('洛克调试远行商人订阅')
|
|
231
247
|
.action(async ({ session }) => {
|
|
232
248
|
if (!isBotAdmin(session, config.adminUserIds))
|
|
233
249
|
return '此指令仅限管理员使用。';
|
package/lib/commands/query.js
CHANGED
|
@@ -777,6 +777,7 @@ async function checkHomeSubscriptions(deps) {
|
|
|
777
777
|
function register(deps) {
|
|
778
778
|
const { ctx, client } = deps;
|
|
779
779
|
ctx.command('洛克').subcommand('.档案', '查看个人档案')
|
|
780
|
+
.alias('洛克档案')
|
|
780
781
|
.action(async ({ session }) => {
|
|
781
782
|
const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
|
|
782
783
|
if (!fwToken)
|
|
@@ -952,6 +953,7 @@ function register(deps) {
|
|
|
952
953
|
await sendImage(deps, session, 'personal-card', data, fallback);
|
|
953
954
|
});
|
|
954
955
|
ctx.command('洛克').subcommand('.战绩 [page:number]', '查看对战战绩')
|
|
956
|
+
.alias('洛克战绩')
|
|
955
957
|
.action(async ({ session }, _page = 1) => {
|
|
956
958
|
const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
|
|
957
959
|
if (!fwToken)
|
|
@@ -999,6 +1001,7 @@ function register(deps) {
|
|
|
999
1001
|
await sendImage(deps, session, 'record', data, `【${role.name}的战绩】胜率:${bo.win_rate || 0}% 场次:${bo.total_match || 0}`);
|
|
1000
1002
|
});
|
|
1001
1003
|
ctx.command('洛克').subcommand('.背包 [arg1:string] [arg2:string]', '查看精灵背包')
|
|
1004
|
+
.alias('洛克背包')
|
|
1002
1005
|
.action(async ({ session }, arg1, arg2) => {
|
|
1003
1006
|
const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
|
|
1004
1007
|
if (!fwToken)
|
|
@@ -1058,6 +1061,7 @@ function register(deps) {
|
|
|
1058
1061
|
await sendImage(deps, session, 'package', data, `【背包 - ${category}精灵】共${petRes.total || 0}只`);
|
|
1059
1062
|
});
|
|
1060
1063
|
ctx.command('洛克').subcommand('.阵容 [arg1:string] [arg2:string]', '查看阵容推荐')
|
|
1064
|
+
.alias('洛克阵容')
|
|
1061
1065
|
.action(async ({ session }, arg1, arg2) => {
|
|
1062
1066
|
const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
|
|
1063
1067
|
if (!fwToken)
|
|
@@ -1147,6 +1151,7 @@ function register(deps) {
|
|
|
1147
1151
|
await sendImage(deps, session, 'lineup-detail', data, fallback);
|
|
1148
1152
|
});
|
|
1149
1153
|
ctx.command('洛克').subcommand('.交换大厅 [page:number]', '查看交换大厅')
|
|
1154
|
+
.alias('洛克交换大厅')
|
|
1150
1155
|
.action(async ({ session }, page = 1) => {
|
|
1151
1156
|
const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
|
|
1152
1157
|
if (!fwToken)
|
|
@@ -1187,6 +1192,7 @@ function register(deps) {
|
|
|
1187
1192
|
await sendImage(deps, session, 'exchange-hall', data, `【交换大厅】第${page}页`);
|
|
1188
1193
|
});
|
|
1189
1194
|
ctx.command('洛克').subcommand('.玩家 <uid:string>', '通过 ingame 接口查询玩家基础资料')
|
|
1195
|
+
.alias('洛克玩家')
|
|
1190
1196
|
.action(async ({ session }, uid) => {
|
|
1191
1197
|
if (!uid)
|
|
1192
1198
|
return '请提供玩家 UID。用法:洛克.玩家 <UID>';
|
|
@@ -1196,6 +1202,7 @@ function register(deps) {
|
|
|
1196
1202
|
await sendImage(deps, session, 'player-search', buildPlayerSearchRenderData(res, uid), `【洛克玩家】UID ${uid}`);
|
|
1197
1203
|
});
|
|
1198
1204
|
ctx.command('洛克').subcommand('.家园 [uid:string]', '通过 UID 查询家园菜园、守卫和室内精灵')
|
|
1205
|
+
.alias('洛克家园')
|
|
1199
1206
|
.action(async ({ session }, uid = '') => {
|
|
1200
1207
|
let targetUid = String(uid || '').trim();
|
|
1201
1208
|
if (!targetUid) {
|
|
@@ -1210,6 +1217,7 @@ function register(deps) {
|
|
|
1210
1217
|
await sendImage(deps, session, 'home', buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
|
|
1211
1218
|
});
|
|
1212
1219
|
ctx.command('洛克').subcommand('.商店 <shopId:string>', '通过 ingame 接口查询商店信息')
|
|
1220
|
+
.alias('洛克商店')
|
|
1213
1221
|
.action(async ({ session }, shopId) => {
|
|
1214
1222
|
if (!shopId)
|
|
1215
1223
|
return '请提供商店 ID。用法:洛克.商店 <shop_id>';
|
|
@@ -1219,6 +1227,7 @@ function register(deps) {
|
|
|
1219
1227
|
await sendImage(deps, session, 'ingame-shop', buildShopRenderData(res, shopId), `【洛克商店】shop_id=${shopId}`);
|
|
1220
1228
|
});
|
|
1221
1229
|
ctx.command('洛克').subcommand('.好友关系 <userIds:string>', '查询好友关系')
|
|
1230
|
+
.alias('洛克好友关系')
|
|
1222
1231
|
.action(async ({ session }, userIds) => {
|
|
1223
1232
|
if (!userIds)
|
|
1224
1233
|
return '请提供要查询的用户 ID 列表。用法:洛克.好友关系 <id1,id2>';
|
|
@@ -1231,6 +1240,7 @@ function register(deps) {
|
|
|
1231
1240
|
await sendImage(deps, session, 'friendship', buildFriendshipRenderData(res, userIds), `【好友关系】${userIds}`);
|
|
1232
1241
|
});
|
|
1233
1242
|
ctx.command('洛克').subcommand('.学生 [area:number] [accountType:number]', '查询学生认证状态与学生活动福利')
|
|
1243
|
+
.alias('洛克学生')
|
|
1234
1244
|
.action(async ({ session }, area = 101, accountType = 0) => {
|
|
1235
1245
|
const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
|
|
1236
1246
|
if (!fwToken)
|
|
@@ -1260,6 +1270,7 @@ function register(deps) {
|
|
|
1260
1270
|
return deleted ? `已取消 ${deleted} 条家园订阅。` : '当前会话没有匹配的家园订阅。';
|
|
1261
1271
|
});
|
|
1262
1272
|
ctx.command('洛克').subcommand('.调试家园订阅', '立即执行一次家园订阅检查')
|
|
1273
|
+
.alias('洛克调试家园订阅')
|
|
1263
1274
|
.action(async ({ session }) => {
|
|
1264
1275
|
if (!isBotAdmin(session, deps.config.adminUserIds))
|
|
1265
1276
|
return '此指令仅限管理员使用。';
|