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 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
- console.warn(`[rocom-client] ${method} ${path} detailed failure\n${this.stringifyForLog(details)}`);
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
- this.logRequestFailureDetails(method, path, headers, options.params, options.json, body);
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
- this.logRequestFailureDetails(method, path, headers, options.params, options.json, response);
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
- this.logRequestFailureDetails(method, path, headers, options.params, options.json, e);
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
- let { status, data } = await this.requestWithStatus(ctx, 'POST', path, headers, {
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, includeApiKey);
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
- let { status, data } = await this.requestWithStatus(ctx, 'POST', path, headers, {
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, includeApiKey);
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
- let headers = this.wegameHeaders();
498
- const data = await this.get(ctx, '/api/v1/games/rocom/ingame/merchant/info', headers, params, { silentFailureDetails: true });
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 });
@@ -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
- return userMgr.switchPrimary(session.userId, index)
160
- ? `成功切换到序号 ${index} 账号。`
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);
@@ -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 roleRes = await client.getRole(ctx, fwToken, undefined, userId);
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
- return totalInvalid > 0
85
- ? `清理完成:移除 ${totalInvalid} 个无效绑定,剩余 ${totalValid} 个有效绑定。`
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
- }, 60000);
139
+ }, 30000);
116
140
  }
117
141
  }
@@ -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 [
@@ -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
- return `${formatTime(start)}-${formatTime(end)}`;
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 = getActiveProducts(res);
153
+ const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
123
154
  const productNames = products.map((p) => p.name || '').filter(Boolean);
124
- const roundInfo = getCurrentMerchantRound();
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 sent = await (0, subscription_send_1.sendScheduledMessage)(ctx, {
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 ? `@\u5168\u4f53\n${msg}` : msg);
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 products = getActiveProducts(res);
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 '此指令仅限管理员使用。';
@@ -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 '此指令仅限管理员使用。';