koishi-plugin-rocom 1.0.12 → 1.0.17

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.
Files changed (69) hide show
  1. package/lib/atlas-service.d.ts +33 -0
  2. package/lib/atlas-service.js +439 -0
  3. package/lib/client.d.ts +36 -1
  4. package/lib/client.js +221 -21
  5. package/lib/commands/account.js +1 -0
  6. package/lib/commands/merchant.js +351 -91
  7. package/lib/commands/query.js +162 -14
  8. package/lib/commands/tools.js +104 -0
  9. package/lib/commands/wiki.js +167 -6
  10. package/lib/egg-service.d.ts +2 -0
  11. package/lib/egg-service.js +50 -15
  12. package/lib/index.d.ts +9 -0
  13. package/lib/index.js +3626 -303
  14. package/lib/pet-data-service.d.ts +62 -0
  15. package/lib/pet-data-service.js +468 -0
  16. package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
  17. package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
  18. package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
  19. package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
  20. package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
  21. package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
  22. package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
  23. package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
  24. package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
  25. package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
  26. package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
  27. package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
  28. package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
  29. package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
  30. package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
  31. package/lib/render-templates/home/index.html +81 -97
  32. package/lib/render-templates/home/style.css +316 -375
  33. package/lib/render-templates/pet-data/index.html +172 -0
  34. package/lib/render-templates/pet-data/style.css +553 -0
  35. package/lib/render-templates/wiki/detail/index.html +84 -0
  36. package/lib/render-templates/wiki/list/index.html +84 -0
  37. package/lib/render-templates/wiki/menu/index.html +65 -0
  38. package/lib/render-templates/wiki/pet/index.html +158 -0
  39. package/lib/render-templates/wiki/pet/style.css +393 -0
  40. package/lib/render-templates/wiki/shared/style.css +431 -0
  41. package/lib/render-templates/wiki/skill/index.html +79 -0
  42. package/lib/render-templates/wiki/skill/style.css +233 -0
  43. package/lib/render-templates/wiki/suggestions/index.html +84 -0
  44. package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
  45. package/lib/render-templates/yuanxing-shangren/README.md +86 -0
  46. package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
  47. package/lib/render-templates/yuanxing-shangren/img//345/272/225/351/203/250/344/277/241/346/201/257/346/241/206.png +0 -0
  48. package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
  49. package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
  50. package/lib/render-templates/yuanxing-shangren/img//347/274/226/345/217/267/350/203/214/346/231/257/346/241/206.png +0 -0
  51. package/lib/render-templates/yuanxing-shangren/index.html +198 -170
  52. package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
  53. package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
  54. package/lib/render-templates/yuanxing-shangren/style.css +201 -256
  55. package/lib/render-templates/yuanxing-shangren/today.html +461 -0
  56. package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
  57. package/lib/render.js +48 -12
  58. package/lib/subscription-send.js +4 -5
  59. package/lib/ttf/rocom_skill_origin.ttf +0 -0
  60. package/lib/ttf//351/200/240/345/255/227/345/267/245/346/210/277/345/220/257/351/273/221/344/275/223.ttf +0 -0
  61. package/lib/types.d.ts +13 -1
  62. package/lib/user.d.ts +23 -1
  63. package/lib/user.js +32 -1
  64. package/lib/wiki-catalog.d.ts +19 -0
  65. package/lib/wiki-catalog.js +124 -0
  66. package/lib/wiki-service.d.ts +220 -0
  67. package/lib/wiki-service.js +1463 -0
  68. package/package.json +1 -1
  69. package/readme.md +4 -5
package/lib/index.js CHANGED
@@ -36,14 +36,17 @@ __export(src_exports, {
36
36
  name: () => name
37
37
  });
38
38
  module.exports = __toCommonJS(src_exports);
39
- var import_koishi12 = require("koishi");
40
- var import_node_fs5 = __toESM(require("node:fs"));
41
- var import_node_path5 = __toESM(require("node:path"));
39
+ var import_koishi15 = require("koishi");
40
+ var import_node_fs7 = __toESM(require("node:fs"));
41
+ var import_node_path6 = __toESM(require("node:path"));
42
42
 
43
43
  // src/client.ts
44
44
  var import_koishi = require("koishi");
45
45
  var logger = new import_koishi.Logger("rocom-client");
46
- var RocomClient = class {
46
+ var INGAME_PENDING_STATUSES = ["queued", "pending", "running", "processing", "accepted"];
47
+ var INGAME_FAILED_STATUSES = ["failed", "error", "timeout", "cancelled", "canceled"];
48
+ var INGAME_COMPLETED_STATUSES = ["done", "success", "succeeded", "completed", "finished"];
49
+ var RocomClient = class _RocomClient {
47
50
  static {
48
51
  __name(this, "RocomClient");
49
52
  }
@@ -171,14 +174,14 @@ var RocomClient = class {
171
174
  return String(value);
172
175
  }
173
176
  }
174
- logRequestFailureDetails(method, path6, headers, params, body, errorOrResponse) {
177
+ logRequestFailureDetails(method, path7, headers, params, body, errorOrResponse) {
175
178
  const err = errorOrResponse;
176
179
  const response = err?.response;
177
180
  const details = {
178
181
  request: {
179
182
  method,
180
- url: `${this.baseUrl}${path6}`,
181
- path: path6,
183
+ url: `${this.baseUrl}${path7}`,
184
+ path: path7,
182
185
  params: this.sanitizeForLog(params),
183
186
  body: this.sanitizeForLog(body),
184
187
  headers: this.sanitizeForLog(headers),
@@ -198,21 +201,21 @@ var RocomClient = class {
198
201
  stack: err?.stack
199
202
  } : void 0
200
203
  };
201
- logger.warn(`${method} ${path6} detailed failure
204
+ logger.warn(`${method} ${path7} detailed failure
202
205
  ${this.stringifyForLog(details)}`);
203
206
  }
204
- async get(ctx, path6, headers, params, options) {
207
+ async get(ctx, path7, headers, params, options) {
205
208
  try {
206
- const resp = await ctx.http.get(`${this.baseUrl}${path6}`, {
209
+ const resp = await ctx.http.get(`${this.baseUrl}${path7}`, {
207
210
  headers,
208
211
  params,
209
212
  timeout: this.timeout
210
213
  });
211
214
  if (resp?.code !== 0) {
212
215
  this.setLastError(resp?.message || "接口返回异常");
213
- logger.warn(path6 + " error: " + (resp?.message || "unknown"));
216
+ logger.warn(path7 + " error: " + (resp?.message || "unknown"));
214
217
  if (!options?.silentFailureDetails) {
215
- this.logRequestFailureDetails("GET", path6, headers, params, void 0, resp);
218
+ this.logRequestFailureDetails("GET", path7, headers, params, void 0, resp);
216
219
  }
217
220
  return null;
218
221
  }
@@ -221,40 +224,40 @@ ${this.stringifyForLog(details)}`);
221
224
  const message = this.formatHttpError(e);
222
225
  this.setLastError(message);
223
226
  if (!options?.silentFailureDetails) {
224
- this.logRequestFailureDetails("GET", path6, headers, params, void 0, e);
227
+ this.logRequestFailureDetails("GET", path7, headers, params, void 0, e);
225
228
  }
226
229
  const err = e;
227
230
  if (err?.response) {
228
- logger.warn("GET " + path6 + " failed: " + message);
231
+ logger.warn("GET " + path7 + " failed: " + message);
229
232
  } else {
230
- logger.error("GET " + path6 + " failed: " + message);
233
+ logger.error("GET " + path7 + " failed: " + message);
231
234
  }
232
235
  return null;
233
236
  }
234
237
  }
235
- async post(ctx, path6, headers, json, params) {
238
+ async post(ctx, path7, headers, json, params) {
236
239
  try {
237
- const resp = await ctx.http.post(`${this.baseUrl}${path6}`, json, {
240
+ const resp = await ctx.http.post(`${this.baseUrl}${path7}`, json, {
238
241
  headers,
239
242
  params,
240
243
  timeout: this.timeout
241
244
  });
242
245
  if (resp?.code !== 0) {
243
246
  this.setLastError(resp?.message || "接口返回异常");
244
- logger.warn(path6 + " error: " + (resp?.message || "unknown"));
245
- this.logRequestFailureDetails("POST", path6, headers, params, json, resp);
247
+ logger.warn(path7 + " error: " + (resp?.message || "unknown"));
248
+ this.logRequestFailureDetails("POST", path7, headers, params, json, resp);
246
249
  return null;
247
250
  }
248
251
  return resp?.data ?? {};
249
252
  } catch (e) {
250
253
  const message = this.formatHttpError(e);
251
254
  this.setLastError(message);
252
- this.logRequestFailureDetails("POST", path6, headers, params, json, e);
255
+ this.logRequestFailureDetails("POST", path7, headers, params, json, e);
253
256
  const err = e;
254
257
  if (err?.response) {
255
- logger.warn("POST " + path6 + " failed: " + message);
258
+ logger.warn("POST " + path7 + " failed: " + message);
256
259
  } else {
257
- logger.error("POST " + path6 + " failed: " + message);
260
+ logger.error("POST " + path7 + " failed: " + message);
258
261
  }
259
262
  return null;
260
263
  }
@@ -264,34 +267,34 @@ ${this.stringifyForLog(details)}`);
264
267
  if (userIdentifier) result.user_identifier = this.sanitizeUid(userIdentifier);
265
268
  return result;
266
269
  }
267
- async delete(ctx, path6, headers, params) {
270
+ async delete(ctx, path7, headers, params) {
268
271
  try {
269
- const resp = await ctx.http("DELETE", `${this.baseUrl}${path6}`, { headers, params, timeout: this.timeout });
272
+ const resp = await ctx.http("DELETE", `${this.baseUrl}${path7}`, { headers, params, timeout: this.timeout });
270
273
  if (resp?.code !== 0) {
271
274
  this.setLastError(resp?.message || "接口返回异常");
272
- logger.warn(path6 + " error: " + (resp?.message || "unknown"));
273
- this.logRequestFailureDetails("DELETE", path6, headers, params, void 0, resp);
275
+ logger.warn(path7 + " error: " + (resp?.message || "unknown"));
276
+ this.logRequestFailureDetails("DELETE", path7, headers, params, void 0, resp);
274
277
  return null;
275
278
  }
276
279
  return resp?.data ?? {};
277
280
  } catch (e) {
278
281
  const message = this.formatHttpError(e);
279
282
  this.setLastError(message);
280
- this.logRequestFailureDetails("DELETE", path6, headers, params, void 0, e);
283
+ this.logRequestFailureDetails("DELETE", path7, headers, params, void 0, e);
281
284
  const err = e;
282
285
  if (err?.response) {
283
- logger.warn("DELETE " + path6 + " failed: " + message);
286
+ logger.warn("DELETE " + path7 + " failed: " + message);
284
287
  } else {
285
- logger.error("DELETE " + path6 + " failed: " + message);
288
+ logger.error("DELETE " + path7 + " failed: " + message);
286
289
  }
287
290
  return null;
288
291
  }
289
292
  }
290
293
  // Login and binding APIs.
291
- async requestWithStatus(ctx, method, path6, headers, options = {}) {
294
+ async requestWithStatus(ctx, method, path7, headers, options = {}) {
292
295
  const acceptedStatuses = options.acceptedStatuses || [200];
293
296
  try {
294
- const response = await ctx.http(method, `${this.baseUrl}${path6}`, {
297
+ const response = await ctx.http(method, `${this.baseUrl}${path7}`, {
295
298
  headers,
296
299
  params: options.params,
297
300
  data: options.json,
@@ -305,7 +308,7 @@ ${this.stringifyForLog(details)}`);
305
308
  const message = !acceptedStatuses.includes(status) ? `HTTP ${status}: ${rawMessage}` : rawMessage;
306
309
  this.setLastError(message);
307
310
  if (!options.silentFailureDetails) {
308
- this.logRequestFailureDetails(method, path6, headers, options.params, options.json, body);
311
+ this.logRequestFailureDetails(method, path7, headers, options.params, options.json, body);
309
312
  }
310
313
  return { status: null, data: null };
311
314
  }
@@ -313,7 +316,7 @@ ${this.stringifyForLog(details)}`);
313
316
  if (!acceptedStatuses.includes(status)) {
314
317
  this.setLastError(`HTTP ${status}`);
315
318
  if (!options.silentFailureDetails) {
316
- this.logRequestFailureDetails(method, path6, headers, options.params, options.json, response);
319
+ this.logRequestFailureDetails(method, path7, headers, options.params, options.json, response);
317
320
  }
318
321
  return { status: null, data: null };
319
322
  }
@@ -322,22 +325,22 @@ ${this.stringifyForLog(details)}`);
322
325
  const message = this.formatHttpError(e);
323
326
  this.setLastError(message);
324
327
  if (!options.silentFailureDetails) {
325
- this.logRequestFailureDetails(method, path6, headers, options.params, options.json, e);
328
+ this.logRequestFailureDetails(method, path7, headers, options.params, options.json, e);
326
329
  }
327
330
  return { status: null, data: null };
328
331
  }
329
332
  }
330
- async requestIngameWithFallback(ctx, path6, payload, options = {}) {
333
+ async requestIngameWithFallback(ctx, path7, payload, options = {}) {
331
334
  const acceptedStatuses = options.acceptedStatuses || [200, 202];
332
335
  const requestOnce = /* @__PURE__ */ __name(async (includeApiKey, silentFailureDetails) => {
333
336
  const headers = this.wegameHeaders("", "", "", "", includeApiKey);
334
- let result2 = await this.requestWithStatus(ctx, "POST", path6, headers, {
337
+ let result2 = await this.requestWithStatus(ctx, "POST", path7, headers, {
335
338
  json: payload,
336
339
  acceptedStatuses,
337
340
  silentFailureDetails
338
341
  });
339
342
  if (result2.status === null) {
340
- result2 = await this.requestWithStatus(ctx, "GET", path6, headers, {
343
+ result2 = await this.requestWithStatus(ctx, "GET", path7, headers, {
341
344
  params: payload,
342
345
  acceptedStatuses,
343
346
  silentFailureDetails
@@ -362,6 +365,100 @@ ${this.stringifyForLog(details)}`);
362
365
  { acceptedStatuses: [200, 202] }
363
366
  );
364
367
  }
368
+ static normalizeTaskStatus(value) {
369
+ return String(value ?? "").trim().toLowerCase();
370
+ }
371
+ static extractTaskId(payload) {
372
+ return String(payload?.task_id || payload?.taskId || payload?.taskID || "").trim();
373
+ }
374
+ // 判断一份 payload 是否为“已完成的业务结果”(而非排队占位)。
375
+ // 家园接口完成时返回 rows/home_info;玩家、商店完成时返回 source/title/rows;
376
+ // 家园详情(pet/data)完成时返回 npc_pets/npc_pet。
377
+ static isCompletedGatewayPayload(payload) {
378
+ if (!payload || typeof payload !== "object") return false;
379
+ if (Array.isArray(payload.rows)) return true;
380
+ if (payload.home_info !== void 0) return true;
381
+ if (payload.source !== void 0) return true;
382
+ if (Array.isArray(payload.npc_pets)) return true;
383
+ if (payload.npc_pet && typeof payload.npc_pet === "object") return true;
384
+ if (String(payload.title || "").trim()) return true;
385
+ return false;
386
+ }
387
+ static extractCompletedIngamePayload(payload) {
388
+ if (!payload || typeof payload !== "object") return null;
389
+ if (_RocomClient.isCompletedGatewayPayload(payload)) return payload;
390
+ if (_RocomClient.isCompletedGatewayPayload(payload.result)) return payload.result;
391
+ if (_RocomClient.isCompletedGatewayPayload(payload.data)) return payload.data;
392
+ return null;
393
+ }
394
+ static taskErrorMessage(payload, fallbackStatus = "") {
395
+ const candidates = [
396
+ payload?.message,
397
+ payload?.error,
398
+ payload?.error_message,
399
+ payload?.reason,
400
+ payload?.result?.message,
401
+ payload?.result?.error,
402
+ payload?.result?.error_message,
403
+ payload?.data?.message,
404
+ payload?.data?.error
405
+ ];
406
+ for (const candidate of candidates) {
407
+ const text = String(candidate ?? "").trim();
408
+ if (text && text.toLowerCase() !== "failed") return text;
409
+ }
410
+ return String(payload?.status || fallbackStatus || "").trim() || "任务执行失败";
411
+ }
412
+ // 排队等候轮询:把 ingame 接口提交后返回的 202(task_id/status=queued)轮询到出结果或超时。
413
+ // 入参 first 是首请求拿到的 { status, data, usedApiKey }。
414
+ async pollIngameTask(ctx, first, options, labels) {
415
+ const { status, data, usedApiKey } = first;
416
+ const initialCompletedPayload = _RocomClient.extractCompletedIngamePayload(data);
417
+ if (status !== null && initialCompletedPayload) return initialCompletedPayload;
418
+ const taskId = _RocomClient.extractTaskId(data);
419
+ if (!taskId) {
420
+ if (status === 202) this.setLastError(labels.queuedNoTaskId);
421
+ return status === null ? null : data;
422
+ }
423
+ if (typeof options.onQueued === "function") {
424
+ try {
425
+ await options.onQueued(taskId);
426
+ } catch (e) {
427
+ logger.warn(`ingame 排队提示回调失败: ${e}`);
428
+ }
429
+ }
430
+ const intervalMs = Math.max(300, Number(options.intervalMs) || 3e3);
431
+ const timeoutMs = Math.max(intervalMs, Number(options.timeoutMs) || 18e4);
432
+ const startedAt = Date.now();
433
+ let lastStatus = _RocomClient.normalizeTaskStatus(data?.status);
434
+ while (Date.now() - startedAt < timeoutMs) {
435
+ if (lastStatus && INGAME_FAILED_STATUSES.includes(lastStatus)) {
436
+ this.setLastError(_RocomClient.taskErrorMessage(data, lastStatus));
437
+ return null;
438
+ }
439
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
440
+ const task = await this.getIngameTask(ctx, taskId, usedApiKey);
441
+ if (task.status === null) return null;
442
+ const completedPayload = _RocomClient.extractCompletedIngamePayload(task.data);
443
+ if (completedPayload) return completedPayload;
444
+ lastStatus = _RocomClient.normalizeTaskStatus(task.data?.status);
445
+ if (INGAME_FAILED_STATUSES.includes(lastStatus)) {
446
+ this.setLastError(_RocomClient.taskErrorMessage(task.data, lastStatus));
447
+ return null;
448
+ }
449
+ if (INGAME_COMPLETED_STATUSES.includes(lastStatus)) {
450
+ this.setLastError(`Ingame 任务已完成但未返回可解析结果:${taskId}`);
451
+ return null;
452
+ }
453
+ if (lastStatus && !INGAME_PENDING_STATUSES.includes(lastStatus)) {
454
+ this.setLastError(`Ingame 任务状态异常:${lastStatus}`);
455
+ return null;
456
+ }
457
+ if (!_RocomClient.extractTaskId(task.data) && !lastStatus) return task.data;
458
+ }
459
+ this.setLastError(labels.stillQueued(taskId));
460
+ return null;
461
+ }
365
462
  async qqQrLogin(ctx, userIdentifier) {
366
463
  const params = { client_type: "bot", client_id: "koishi", provider: "rocom" };
367
464
  if (userIdentifier) params.user_identifier = this.sanitizeUid(userIdentifier);
@@ -469,9 +566,9 @@ ${this.stringifyForLog(details)}`);
469
566
  this.setLastError("UID 不能为空");
470
567
  return null;
471
568
  }
472
- const path6 = "/api/v1/games/rocom/ingame/player/search";
569
+ const path7 = "/api/v1/games/rocom/ingame/player/search";
473
570
  const payload = { uid: sanitizedUid, wait_ms: 5e3 };
474
- const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path6, payload);
571
+ const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path7, payload);
475
572
  if (status === 200 && data && this.isIngamePlayerPayload(data)) return data;
476
573
  if (!data) return null;
477
574
  if (this.isIngamePlayerPayload(data)) return data;
@@ -503,9 +600,28 @@ ${this.stringifyForLog(details)}`);
503
600
  return this.get(ctx, "/api/v1/games/rocom/exchange/posters", this.wegameHeaders(fwToken, userIdentifier), params);
504
601
  }
505
602
  async getMerchantInfo(ctx, refresh = false) {
506
- return this.get(ctx, "/api/v1/games/rocom/merchant/info", this.wegameHeaders(), { refresh: refresh ? "true" : "false" });
603
+ return this.get(ctx, "/api/v1/games/rocom/merchant/info", this.wegameHeaders(), {
604
+ refresh: refresh ? "true" : "false",
605
+ random_goods: "all"
606
+ });
507
607
  }
508
608
  async queryPetSize(ctx, diameter, weight, sameRideEgg = false, userIdentifier = "") {
609
+ const newParams = this.scopedParams({
610
+ diameter,
611
+ weight,
612
+ pool: sameRideEgg ? "ride" : "magic",
613
+ include_display_only: "false",
614
+ page_no: 1,
615
+ page_size: 30
616
+ }, userIdentifier);
617
+ const newRes = await this.get(
618
+ ctx,
619
+ "/api/v1/games/rocom/wiki/pet-size/query",
620
+ this.wegameHeaders("", userIdentifier, "bot", "koishi"),
621
+ newParams,
622
+ { silentFailureDetails: true }
623
+ );
624
+ if (newRes && Array.isArray(newRes.items)) return newRes;
509
625
  const params = this.scopedParams({ diameter, weight }, userIdentifier);
510
626
  if (sameRideEgg) params.sameRideEgg = 1;
511
627
  return this.get(ctx, "/api/v1/games/rocom/pet/size-query", this.wegameHeaders("", userIdentifier, "bot", "koishi"), params);
@@ -661,29 +777,20 @@ ${this.stringifyForLog(details)}`);
661
777
  this.scopedParams(params, userIdentifier)
662
778
  );
663
779
  }
664
- async ingameHomeInfo(ctx, uid, waitMs = 5e3) {
780
+ async ingameHomeInfo(ctx, uid, options = {}) {
665
781
  const sanitizedUid = this.sanitizeUid(uid);
666
782
  if (!sanitizedUid) {
667
783
  this.setLastError("UID 不能为空");
668
784
  return null;
669
785
  }
670
- const path6 = "/api/v1/games/rocom/ingame/home/info";
786
+ const waitMs = Number(options.waitMs) || 5e3;
787
+ const path7 = "/api/v1/games/rocom/ingame/home/info";
671
788
  const payload = { uid: sanitizedUid, wait_ms: waitMs };
672
- const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path6, payload);
673
- if (status === 200 && data && !(data.task_id || data.taskId || data.taskID)) return data;
674
- const taskId = data?.task_id || data?.taskId || data?.taskID;
675
- if (!taskId) {
676
- if (status === 202) this.setLastError("家园查询任务已入队,但未返回 task_id");
677
- return null;
678
- }
679
- for (let i = 0; i < 10; i++) {
680
- await new Promise((resolve) => setTimeout(resolve, 1e3));
681
- const task = await this.getIngameTask(ctx, taskId, usedApiKey);
682
- if (task.status === 200) return task.data;
683
- if (task.status === null) return null;
684
- }
685
- this.setLastError(`Home query task is still queued, please retry later (task_id: ${taskId})`);
686
- return null;
789
+ const first = await this.requestIngameWithFallback(ctx, path7, payload);
790
+ return this.pollIngameTask(ctx, first, options, {
791
+ queuedNoTaskId: "家园查询任务已入队,但未返回 task_id",
792
+ stillQueued: /* @__PURE__ */ __name((taskId) => `家园查询任务仍在排队,请稍后重试(task_id: ${taskId})`, "stillQueued")
793
+ });
687
794
  }
688
795
  async ingameMerchantInfo(ctx, shopId) {
689
796
  const params = { shop_id: shopId };
@@ -694,6 +801,25 @@ ${this.stringifyForLog(details)}`);
694
801
  );
695
802
  return status === null ? null : data;
696
803
  }
804
+ // 家园详情:批量或单只查询家园摆放精灵的完整 ingame 数据(上游 v3.6.1)。
805
+ // UID 参数名为 target_uin;单只查询需同时提供 pet_gid 和 npc_id。
806
+ async ingamePetData(ctx, uid, extras = {}, options = {}) {
807
+ const sanitizedUid = this.sanitizeUid(uid);
808
+ if (!sanitizedUid) {
809
+ this.setLastError("UID 不能为空");
810
+ return null;
811
+ }
812
+ const waitMs = Number(options.waitMs) || 2e4;
813
+ const payload = { target_uin: sanitizedUid, wait_ms: waitMs };
814
+ if (extras.petGid !== void 0 && extras.petGid !== null && String(extras.petGid) !== "") payload.pet_gid = extras.petGid;
815
+ if (extras.npcId !== void 0 && extras.npcId !== null && String(extras.npcId) !== "") payload.npc_id = extras.npcId;
816
+ const path7 = "/api/v1/games/rocom/ingame/pet/data";
817
+ const first = await this.requestIngameWithFallback(ctx, path7, payload);
818
+ return this.pollIngameTask(ctx, first, options, {
819
+ queuedNoTaskId: "家园详情任务已入队,但未返回 task_id",
820
+ stillQueued: /* @__PURE__ */ __name((taskId) => `家园详情任务仍在排队,请稍后重试(task_id: ${taskId})`, "stillQueued")
821
+ });
822
+ }
697
823
  async getFriendship(ctx, fwToken, userIds, userIdentifier = "") {
698
824
  return this.get(
699
825
  ctx,
@@ -725,6 +851,66 @@ ${this.stringifyForLog(details)}`);
725
851
  if (options.name) params.name = options.name;
726
852
  return this.get(ctx, "/api/v1/games/rocom/pet/detail", this.wegameHeaders(), params);
727
853
  }
854
+ // ===== 新版 RoCom Wiki API(上游 v3.6.0)=====
855
+ wikiPagedParams(q = "", pageNo = 1, pageSize = 10, filters = {}) {
856
+ const params = {
857
+ page_no: Math.max(Number(pageNo) || 1, 1),
858
+ page_size: Math.min(Math.max(Number(pageSize) || 10, 1), 100)
859
+ };
860
+ if (q) params.q = q;
861
+ for (const [key, value] of Object.entries(filters)) {
862
+ if (value !== void 0 && value !== null && value !== "") params[key] = value;
863
+ }
864
+ return params;
865
+ }
866
+ async listWikiPets(ctx, q = "", pageNo = 1, pageSize = 10, filters = {}) {
867
+ return this.get(ctx, "/api/v1/games/rocom/wiki/pets", this.wegameHeaders(), this.wikiPagedParams(q, pageNo, pageSize, filters));
868
+ }
869
+ async getWikiPet(ctx, petId) {
870
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}`, this.wegameHeaders());
871
+ }
872
+ async getWikiPetProfile(ctx, petId) {
873
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/profile`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
874
+ }
875
+ async getWikiPetSkills(ctx, petId) {
876
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/skills`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
877
+ }
878
+ async getWikiPetFamily(ctx, petId) {
879
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/family`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
880
+ }
881
+ async getWikiPetHandbook(ctx, petId) {
882
+ return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/handbook`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
883
+ }
884
+ async listWikiSkills(ctx, q = "", pageNo = 1, pageSize = 10, filters = {}) {
885
+ return this.get(ctx, "/api/v1/games/rocom/wiki/skills", this.wegameHeaders(), this.wikiPagedParams(q, pageNo, pageSize, filters));
886
+ }
887
+ async getWikiSkill(ctx, skillId) {
888
+ return this.get(ctx, `/api/v1/games/rocom/wiki/skills/${skillId}`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
889
+ }
890
+ async getWikiSkillPets(ctx, skillId) {
891
+ return this.get(ctx, `/api/v1/games/rocom/wiki/skills/${skillId}/pets`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
892
+ }
893
+ async getWikiCatalogs(ctx) {
894
+ return this.get(ctx, "/api/v1/games/rocom/wiki/catalogs", this.wegameHeaders());
895
+ }
896
+ async getWikiOptions(ctx) {
897
+ return this.get(ctx, "/api/v1/games/rocom/wiki/options", this.wegameHeaders(), void 0, { silentFailureDetails: true });
898
+ }
899
+ async getWikiPath(ctx, path7, params = {}) {
900
+ const trimmed = String(path7 || "").trim();
901
+ if (!trimmed.startsWith("/api/v1/games/rocom/wiki/")) {
902
+ this.setLastError("非法 Wiki 路径");
903
+ return null;
904
+ }
905
+ return this.get(ctx, trimmed, this.wegameHeaders(), params);
906
+ }
907
+ async listWikiCatalogItems(ctx, path7, q = "", pageNo = 1, pageSize = 10, search = true) {
908
+ const params = this.wikiPagedParams(search ? q : "", pageNo, pageSize);
909
+ return this.getWikiPath(ctx, path7, params);
910
+ }
911
+ get wikiAssetBaseUrl() {
912
+ return this.baseUrl;
913
+ }
728
914
  };
729
915
 
730
916
  // src/user.ts
@@ -897,6 +1083,35 @@ var HomeSubscriptionManager = class {
897
1083
  return JSON.parse(JSON.stringify(this.store.get()));
898
1084
  }
899
1085
  };
1086
+ var AnnouncementSubscriptionManager = class {
1087
+ static {
1088
+ __name(this, "AnnouncementSubscriptionManager");
1089
+ }
1090
+ store;
1091
+ constructor(dataDir) {
1092
+ this.store = new JsonStore(dataDir, "rocom_announcement_subscriptions.json", {});
1093
+ }
1094
+ upsert(key, sub) {
1095
+ const data = this.store.get();
1096
+ data[key] = { ...sub };
1097
+ this.store.set(data);
1098
+ }
1099
+ deleteMatching(target) {
1100
+ const data = this.store.get();
1101
+ let deleted = 0;
1102
+ for (const [key, sub] of Object.entries(data)) {
1103
+ const sameTarget = sub.platform === target.platform && (sub.channel_id || "") === (target.channelId || "") && (sub.user_id || "") === (target.userId || "");
1104
+ if (!sameTarget) continue;
1105
+ delete data[key];
1106
+ deleted++;
1107
+ }
1108
+ if (deleted) this.store.set(data);
1109
+ return deleted;
1110
+ }
1111
+ getAll() {
1112
+ return JSON.parse(JSON.stringify(this.store.get()));
1113
+ }
1114
+ };
900
1115
 
901
1116
  // src/egg-service.ts
902
1117
  var import_koishi3 = require("koishi");
@@ -945,8 +1160,8 @@ __name(petType, "petType");
945
1160
  function fmtDur(s) {
946
1161
  if (!s || s <= 0) return "暂无数据";
947
1162
  if (s % 86400 === 0) return `${s / 86400} 天`;
948
- const h4 = s / 3600;
949
- return h4 === Math.floor(h4) ? `${h4} 小时` : `${h4.toFixed(1)} 小时`;
1163
+ const h5 = s / 3600;
1164
+ return h5 === Math.floor(h5) ? `${h5} 小时` : `${h5.toFixed(1)} 小时`;
950
1165
  }
951
1166
  __name(fmtDur, "fmtDur");
952
1167
  function wt(v) {
@@ -1152,20 +1367,29 @@ var EggService = class {
1152
1367
  };
1153
1368
  }
1154
1369
  formatSizeApiCard(item) {
1155
- const probability = num(item?.probability);
1370
+ const petInfo = item?.pet && typeof item.pet === "object" ? item.pet : {};
1371
+ const eggSize = item?.egg_size && typeof item.egg_size === "object" ? item.egg_size : {};
1372
+ const heightRange = eggSize?.height && typeof eggSize.height === "object" ? eggSize.height : {};
1373
+ const weightRange = eggSize?.weight && typeof eggSize.weight === "object" ? eggSize.weight : {};
1374
+ const match = item?.match && typeof item.match === "object" ? item.match : {};
1375
+ const petId = petInfo?.pet_id || item?.petId || item?.pet_id || "-";
1376
+ const petName3 = petInfo?.name || (typeof item?.pet === "string" ? item.pet : "") || item?.name || "未知精灵";
1377
+ const typeNames = Array.isArray(petInfo?.type_names) ? petInfo.type_names.filter(Boolean) : [];
1378
+ const eggGroupNames = Array.isArray(petInfo?.egg_group_names) ? petInfo.egg_group_names.filter(Boolean) : [];
1379
+ const probability = num(item?.probability) ?? num(match?.percent);
1156
1380
  const matchCount = num(item?.matchCount);
1157
- const heightMin = num(item?.diameterMin);
1158
- const heightMax = num(item?.diameterMax);
1159
- const weightMin = num(item?.weightMin);
1160
- const weightMax = num(item?.weightMax);
1381
+ const heightMin = num(heightRange?.min_m ?? item?.diameterMin);
1382
+ const heightMax = num(heightRange?.max_m ?? item?.diameterMax);
1383
+ const weightMin = num(weightRange?.min_kg ?? item?.weightMin);
1384
+ const weightMax = num(weightRange?.max_kg ?? item?.weightMax);
1161
1385
  return {
1162
- id: item?.petId || "-",
1163
- name: item?.pet || "未知精灵",
1164
- icon: item?.petIcon || petIconUrl(item?.petId),
1165
- image: item?.petImage || petImageUrl(item?.petId),
1166
- type_label: "后端未提供",
1386
+ id: petId,
1387
+ name: petName3,
1388
+ icon: petInfo?.icon || item?.petIcon || petIconUrl(petId),
1389
+ image: petInfo?.small_icon || item?.petImage || petImageUrl(petId),
1390
+ type_label: typeNames.length ? typeNames.join(" / ") : "后端未提供",
1167
1391
  egg_group_ids: [],
1168
- egg_groups_label: "后端未提供",
1392
+ egg_groups_label: eggGroupNames.length ? eggGroupNames.join(" / ") : "后端未提供",
1169
1393
  height_min: heightMin,
1170
1394
  height_max: heightMax,
1171
1395
  height_label: fmtRange(heightMin, heightMax, "m"),
@@ -1174,7 +1398,30 @@ var EggService = class {
1174
1398
  weight_label: fmtRange(weightMin, weightMax, "kg"),
1175
1399
  probability,
1176
1400
  match_count: matchCount,
1177
- match_info_label: this.formatMatchSummary(probability, matchCount)
1401
+ match_info_label: String(match?.match_percent_text || "") || this.formatMatchSummary(probability, matchCount)
1402
+ };
1403
+ }
1404
+ // 新版尺寸反查按 match.layer 划分完美/范围匹配。
1405
+ splitNewSizeApiItems(items) {
1406
+ const perfect = [];
1407
+ const ranged = [];
1408
+ for (const item of items) {
1409
+ const match = item?.match && typeof item.match === "object" ? item.match : {};
1410
+ const layer = String(match?.layer || "").toLowerCase();
1411
+ const displayOnly = Boolean(match?.display_only);
1412
+ if (["strict", "exact"].includes(layer) && !displayOnly) perfect.push(item);
1413
+ else ranged.push(item);
1414
+ }
1415
+ return [perfect, ranged];
1416
+ }
1417
+ sizeApiResultGroups(results) {
1418
+ if (Array.isArray(results?.items)) {
1419
+ const [perfect, ranged] = this.splitNewSizeApiItems(results.items);
1420
+ return { exact: perfect, candidates: ranged };
1421
+ }
1422
+ return {
1423
+ exact: Array.isArray(results?.exactResults) ? results.exactResults : [],
1424
+ candidates: Array.isArray(results?.candidates) ? results.candidates : []
1178
1425
  };
1179
1426
  }
1180
1427
  formatEggSearchCard(item) {
@@ -1333,8 +1580,7 @@ var EggService = class {
1333
1580
  if (height != null) cond.push(`身高=${heightDisplay || fmtRange(ht(height), ht(height), "m")}`);
1334
1581
  if (weight != null) cond.push(`体重=${weight}kg`);
1335
1582
  const condStr = cond.join(" + ") || "当前条件";
1336
- const exact = results?.exactResults || [];
1337
- const candidates = results?.candidates || [];
1583
+ const { exact, candidates } = this.sizeApiResultGroups(results);
1338
1584
  if (!exact.length && !candidates.length) return `❌ 未找到符合 ${condStr} 的精灵。`;
1339
1585
  const lines = [];
1340
1586
  if (exact.length) {
@@ -1547,9 +1793,10 @@ var EggService = class {
1547
1793
  const conditions = [];
1548
1794
  if (height != null) conditions.push(`身高 ${heightDisplay || fmtRange(ht(height), ht(height), "m")}`);
1549
1795
  if (weight != null) conditions.push(`体重 ${weight} kg`);
1796
+ const groups = this.sizeApiResultGroups(results);
1550
1797
  const [perfect, ranged] = this.mergeCardsByName(
1551
- (results?.exactResults || []).map((item) => this.formatSizeApiCard(item)),
1552
- (results?.candidates || []).map((item) => this.formatSizeApiCard(item))
1798
+ groups.exact.map((item) => this.formatSizeApiCard(item)),
1799
+ groups.candidates.map((item) => this.formatSizeApiCard(item))
1553
1800
  );
1554
1801
  const searchMode = results?.searchMode || "";
1555
1802
  const queryLabel = `${conditions.join(" / ") || "尺寸反查"}${searchMode ? ` · 模式 ${searchMode}` : ""}`;
@@ -1634,6 +1881,11 @@ var TEMPLATE_CAPTURE_PADDING = {
1634
1881
  var TEMPLATE_VIEWPORTS = {
1635
1882
  activities: { width: 1600, height: 1200, deviceScaleFactor: 2 }
1636
1883
  };
1884
+ var VISUAL_BOUNDS_TEMPLATES = /* @__PURE__ */ new Set([
1885
+ "home",
1886
+ "yuanxing-shangren/merchant",
1887
+ "yuanxing-shangren/today"
1888
+ ]);
1637
1889
  var DEFAULT_SCREENSHOT_OPTIONS = {
1638
1890
  type: "jpeg",
1639
1891
  quality: 82
@@ -1725,13 +1977,16 @@ var Renderer = class {
1725
1977
  try {
1726
1978
  await page.evaluate(async () => {
1727
1979
  const images = Array.from(document.images);
1728
- await Promise.all(images.map((img) => {
1729
- if (img.complete) return Promise.resolve();
1730
- return new Promise((resolve) => {
1731
- img.onload = () => resolve();
1732
- img.onerror = () => resolve();
1733
- });
1734
- }));
1980
+ await Promise.race([
1981
+ Promise.all(images.map((img) => {
1982
+ if (img.complete) return Promise.resolve();
1983
+ return new Promise((resolve) => {
1984
+ img.onload = () => resolve();
1985
+ img.onerror = () => resolve();
1986
+ });
1987
+ })),
1988
+ new Promise((resolve) => setTimeout(resolve, 1e4))
1989
+ ]);
1735
1990
  const fonts = document.fonts;
1736
1991
  if (fonts?.ready) {
1737
1992
  await fonts.ready;
@@ -1748,6 +2003,8 @@ var Renderer = class {
1748
2003
  ".searcheggs-cont",
1749
2004
  ".bwiki-shell",
1750
2005
  ".skill-shell",
2006
+ ".wiki-page",
2007
+ ".pet-data-page",
1751
2008
  ".lineup-page",
1752
2009
  ".lineup-detail-page",
1753
2010
  ".page-section-main",
@@ -1760,6 +2017,7 @@ var Renderer = class {
1760
2017
  ".student-perks-page",
1761
2018
  ".student-page",
1762
2019
  ".merchant-page",
2020
+ ".page",
1763
2021
  ".home-page",
1764
2022
  ".pet-panel-page",
1765
2023
  ".pet-detail-page",
@@ -1776,16 +2034,17 @@ var Renderer = class {
1776
2034
  if (target) {
1777
2035
  const box = await target.boundingBox();
1778
2036
  if (box && box.width > 0 && box.height > 0) {
1779
- const elementMetrics = await page.evaluate((el) => {
2037
+ const useVisualBounds = VISUAL_BOUNDS_TEMPLATES.has(templateName);
2038
+ const elementMetrics = await page.evaluate((el, visualBounds) => {
1780
2039
  const rect = el.getBoundingClientRect();
1781
2040
  const element = el;
1782
2041
  return {
1783
2042
  x: rect.left + window.scrollX,
1784
2043
  y: rect.top + window.scrollY,
1785
- width: Math.max(rect.width, element.scrollWidth, element.offsetWidth),
1786
- height: Math.max(rect.height, element.scrollHeight, element.offsetHeight)
2044
+ width: visualBounds ? rect.width : Math.max(rect.width, element.scrollWidth, element.offsetWidth),
2045
+ height: visualBounds ? rect.height : Math.max(rect.height, element.scrollHeight, element.offsetHeight)
1787
2046
  };
1788
- }, target);
2047
+ }, target, useVisualBounds);
1789
2048
  const capturePadding = TEMPLATE_CAPTURE_PADDING[templateName] || { left: 0, right: 0, top: 0, bottom: 0 };
1790
2049
  await page.setViewport({
1791
2050
  width: Math.max(Math.ceil(elementMetrics.x + elementMetrics.width + capturePadding.right) + 8, 200),
@@ -1793,6 +2052,26 @@ var Renderer = class {
1793
2052
  deviceScaleFactor: initialViewport.deviceScaleFactor
1794
2053
  });
1795
2054
  await new Promise((resolve) => setTimeout(resolve, 100));
2055
+ try {
2056
+ const remeasured = await page.evaluate((el, visualBounds) => {
2057
+ const rect = el.getBoundingClientRect();
2058
+ const element = el;
2059
+ return {
2060
+ x: rect.left + window.scrollX,
2061
+ y: rect.top + window.scrollY,
2062
+ width: visualBounds ? rect.width : Math.max(rect.width, element.scrollWidth, element.offsetWidth),
2063
+ height: visualBounds ? rect.height : Math.max(rect.height, element.scrollHeight, element.offsetHeight)
2064
+ };
2065
+ }, target, useVisualBounds);
2066
+ if (remeasured && remeasured.width > 0 && remeasured.height > 0) {
2067
+ elementMetrics.x = remeasured.x;
2068
+ elementMetrics.y = remeasured.y;
2069
+ elementMetrics.width = remeasured.width;
2070
+ elementMetrics.height = remeasured.height;
2071
+ }
2072
+ } catch (err) {
2073
+ logger4.warn(`element remeasure failed for ${templateName}: ${err}`);
2074
+ }
1796
2075
  const hasOverflow = elementMetrics.width > box.width + 0.5 || elementMetrics.height > box.height + 0.5;
1797
2076
  if (capturePadding.left || capturePadding.right || capturePadding.top || capturePadding.bottom || hasOverflow) {
1798
2077
  const clipX = Math.max(0, elementMetrics.x - capturePadding.left);
@@ -1818,17 +2097,1992 @@ var Renderer = class {
1818
2097
  return Buffer.isBuffer(screenshot) ? screenshot : Buffer.from(screenshot);
1819
2098
  } finally {
1820
2099
  try {
1821
- await page.close();
1822
- } catch {
2100
+ await page.close();
2101
+ } catch {
2102
+ }
2103
+ import_node_fs3.default.rmSync(tempDir, { recursive: true, force: true });
2104
+ }
2105
+ } catch (e) {
2106
+ logger4.error(`render failed: ${e}`);
2107
+ return null;
2108
+ }
2109
+ }
2110
+ };
2111
+
2112
+ // src/wiki-service.ts
2113
+ var import_koishi5 = require("koishi");
2114
+
2115
+ // src/wiki-catalog.ts
2116
+ function route(key, title, aliases, detailPath = "", idFields = [], search = null) {
2117
+ return { key, title, aliases, detail_path: detailPath, id_fields: idFields, search };
2118
+ }
2119
+ __name(route, "route");
2120
+ var WIKI_CATALOG_ROUTES = [
2121
+ route("pets", "精灵", ["精灵", "宠物", "pet", "pets"], "/api/v1/games/rocom/wiki/pets/{pet_id}", ["pet_id"]),
2122
+ route("pet-skills", "精灵技能", ["精灵技能", "宠物技能"], "/api/v1/games/rocom/wiki/pets/{pet_id}/skills", ["pet_id"], false),
2123
+ route("pet-features", "精灵特性", ["精灵特性", "宠物特性"], "/api/v1/games/rocom/wiki/pets/{pet_id}/features", ["pet_id"], false),
2124
+ route("pet-family", "精灵家族", ["精灵家族", "进化链", "形态链"], "/api/v1/games/rocom/wiki/pets/{pet_id}/family", ["pet_id"], false),
2125
+ route("pet-egg", "精灵蛋详情", ["精灵蛋详情", "宠物蛋详情"], "/api/v1/games/rocom/wiki/pets/{pet_id}/egg", ["pet_id"], false),
2126
+ route("pet-handbook", "精灵图鉴课题", ["精灵图鉴课题", "图鉴课题"], "/api/v1/games/rocom/wiki/pets/{pet_id}/handbook", ["pet_id"], false),
2127
+ route("pet-talents", "精灵天赋", ["精灵天赋", "同乘天赋"], "/api/v1/games/rocom/wiki/pets/{pet_id}/talents", ["pet_id"], false),
2128
+ route("pet-natures", "精灵性格", ["精灵性格", "性格"], "/api/v1/games/rocom/wiki/pets/{pet_id}/natures", ["pet_id"], false),
2129
+ route("pet-profile", "精灵资料", ["精灵资料", "资料页"], "/api/v1/games/rocom/wiki/pets/{pet_id}/profile", ["pet_id"], false),
2130
+ route("pet-fruit-section", "精灵关联果实", ["精灵关联果实"], "/api/v1/games/rocom/wiki/pets/{pet_id}/fruit", ["pet_id"], false),
2131
+ route("pet-ride", "精灵骑乘", ["精灵骑乘", "骑乘"], "/api/v1/games/rocom/wiki/pets/{pet_id}/ride", ["pet_id"], false),
2132
+ route("pet-ecology", "精灵生态", ["精灵生态", "生态"], "/api/v1/games/rocom/wiki/pets/{pet_id}/ecology", ["pet_id"], false),
2133
+ route("pet-stories", "精灵故事", ["精灵故事", "故事"], "/api/v1/games/rocom/wiki/pets/{pet_id}/stories", ["pet_id"], false),
2134
+ route("pet-acquisition", "精灵获取方式", ["精灵获取", "获取方式"], "/api/v1/games/rocom/wiki/pets/{pet_id}/acquisition", ["pet_id"], false),
2135
+ route("skills", "技能", ["技能", "skill", "skills"], "/api/v1/games/rocom/wiki/skills/{skill_id}", ["skill_id"]),
2136
+ route("skill-pets", "技能可用精灵", ["技能可用精灵", "技能精灵"], "/api/v1/games/rocom/wiki/skills/{skill_id}/pets", ["skill_id"], false),
2137
+ route("skill-stones", "技能石", ["技能石", "skillstone", "skill-stone"], "/api/v1/games/rocom/wiki/skill-stones/{item_id}", ["item_id"]),
2138
+ route("skill-stone-recipes", "技能石配方", ["技能石配方", "技能配方", "skillstonerecipe"], "/api/v1/games/rocom/wiki/skill-stone-recipes/{item_id}", ["item_id"]),
2139
+ route("items", "物品", ["物品", "道具", "item", "items"], "/api/v1/games/rocom/wiki/items/{item_kind}/{item_id}", ["item_kind", "item_id"]),
2140
+ route("egg-items", "蛋物品", ["蛋物品", "蛋道具", "egg-item"], "/api/v1/games/rocom/wiki/egg-items/{item_id}", ["item_id"]),
2141
+ route("pet-eggs", "精灵蛋", ["精灵蛋", "宠物蛋", "蛋图鉴", "pet-egg"], "/api/v1/games/rocom/wiki/pet-eggs/{egg_conf_id}", ["egg_conf_id"]),
2142
+ route("random-eggs", "随机蛋", ["随机蛋", "随机蛋池", "random-egg"], "/api/v1/games/rocom/wiki/random-eggs/{random_egg_id}", ["random_egg_id"]),
2143
+ route("pet-fruits", "精灵果实", ["精灵果实", "果实", "pet-fruit"], "/api/v1/games/rocom/wiki/pet-fruits/{item_id}", ["item_id"]),
2144
+ route("pet-foods", "精灵食物", ["精灵食物", "食物", "pet-food"], "/api/v1/games/rocom/wiki/pet-foods/{item_id}", ["item_id"]),
2145
+ route("pet-gifts", "精灵礼物", ["精灵礼物", "礼物", "pet-gift"], "/api/v1/games/rocom/wiki/pet-gifts/{item_id}", ["item_id"]),
2146
+ route("pet-close-level-effects", "亲密等级效果", ["亲密等级", "亲密效果"], "", ["level"], false),
2147
+ route("pet-action-close-exp", "行为亲密经验", ["行为亲密", "亲密经验"], "", [], false),
2148
+ route("home-egg-lay-rates", "家园产蛋概率", ["家园产蛋", "产蛋概率"], "", [], false),
2149
+ route("home-progression", "家园成长", ["家园成长", "家园升级"], "", [], false),
2150
+ route("pet-levels", "精灵等级经验", ["精灵等级", "等级经验", "pet-level"], "/api/v1/games/rocom/wiki/pet-levels/{level}", ["level"], false),
2151
+ route("handbook-areas", "图鉴区域", ["图鉴区域", "区域图鉴"], "", ["id"], false),
2152
+ route("fruit-trees", "果实树", ["果实树", "树"], "/api/v1/games/rocom/wiki/fruit-trees/{tree_id}", ["tree_id"]),
2153
+ route("fruit-refresh", "果实刷新精灵", ["果实刷新", "刷新精灵"], "/api/v1/games/rocom/wiki/fruit-refresh/{fruit_item_id}", ["fruit_item_id"], false),
2154
+ route("medals", "奖章", ["奖章", "勋章", "medal"], "/api/v1/games/rocom/wiki/medals/{medal_id}", ["medal_id"]),
2155
+ route("recipes", "食谱", ["食谱", "料理", "recipe"], "/api/v1/games/rocom/wiki/recipes/{item_id}", ["item_id"]),
2156
+ route("balls", "咕噜球", ["咕噜球", "球", "ball"], "/api/v1/games/rocom/wiki/balls/{ball_id}", ["ball_id"]),
2157
+ route("plants", "种植", ["种植", "作物", "植物", "plant"], "/api/v1/games/rocom/wiki/plants/{plant_id}", ["plant_id"]),
2158
+ route("pet-carryons", "精灵携带物", ["精灵携带物", "携带物", "carryon"], "/api/v1/games/rocom/wiki/pet-carryons/{carryon_id}", ["carryon_id"]),
2159
+ route("furniture", "家具", ["家具", "furniture"], "/api/v1/games/rocom/wiki/furniture/{furniture_id}", ["furniture_id"]),
2160
+ route("fashion", "服装套装", ["服装", "套装", "fashion"], "/api/v1/games/rocom/wiki/fashion/{suit_id}", ["suit_id"]),
2161
+ route("regions", "地区", ["地区", "区域", "region"], "/api/v1/games/rocom/wiki/regions/{region_id}", ["region_id"]),
2162
+ route("dungeons", "副本", ["副本", "dungeon"], "/api/v1/games/rocom/wiki/dungeons/{dungeon_id}", ["dungeon_id"]),
2163
+ route("mails", "邮件模板", ["邮件", "邮件模板", "mail"], "/api/v1/games/rocom/wiki/mails/{mail_id}", ["mail_id"]),
2164
+ route("music", "音乐", ["音乐", "music"], "/api/v1/games/rocom/wiki/music/{music_id}", ["music_id"]),
2165
+ route("profile-assets", "个人名片资产", ["名片", "名片资产", "profile-asset"], "/api/v1/games/rocom/wiki/profile-assets/{asset_type}/{asset_id}", ["asset_type", "asset_id"]),
2166
+ route("chat-emojis", "聊天表情", ["表情", "聊天表情", "emoji"], "/api/v1/games/rocom/wiki/chat-emojis/{emoji_id}", ["emoji_id"]),
2167
+ route("photo-actions", "拍照动作", ["拍照动作", "动作", "photo-action"], "/api/v1/games/rocom/wiki/photo-actions/{action_type}/{action_id}", ["action_type", "action_id"]),
2168
+ route("tasks", "任务", ["任务", "task"], "/api/v1/games/rocom/wiki/tasks/{task_id}", ["task_id"]),
2169
+ route("task-summaries", "剧情摘要", ["剧情", "剧情摘要", "task-summary"], "/api/v1/games/rocom/wiki/task-summaries/{summary_id}", ["summary_id"]),
2170
+ route("shops", "商店", ["商店", "shop"], "/api/v1/games/rocom/wiki/shops/{shop_id}", ["shop_id"]),
2171
+ route("exchanges", "兑换", ["兑换", "exchange"], "/api/v1/games/rocom/wiki/exchanges/{exchange_id}", ["exchange_id"])
2172
+ ];
2173
+ var WIKI_CATALOG_ROUTES_BY_KEY = new Map(WIKI_CATALOG_ROUTES.map((item) => [item.key, item]));
2174
+ var WIKI_CATALOG_ROUTES_BY_ALIAS = /* @__PURE__ */ new Map();
2175
+ for (const item of WIKI_CATALOG_ROUTES) {
2176
+ for (const alias of [item.key, item.title, ...item.aliases]) {
2177
+ WIKI_CATALOG_ROUTES_BY_ALIAS.set(alias.toLowerCase(), item);
2178
+ }
2179
+ }
2180
+ var WIKI_GLOBAL_PRIORITIES = {
2181
+ "pets": 0,
2182
+ "skills": 0,
2183
+ "balls": 1,
2184
+ "pet-carryons": 1,
2185
+ "medals": 1,
2186
+ "plants": 1,
2187
+ "pet-fruits": 1,
2188
+ "recipes": 1,
2189
+ "pet-eggs": 1,
2190
+ "random-eggs": 1,
2191
+ "egg-items": 1,
2192
+ "skill-stones": 1,
2193
+ "skill-stone-recipes": 1,
2194
+ "pet-foods": 1,
2195
+ "pet-gifts": 1,
2196
+ "furniture": 1,
2197
+ "fashion": 1,
2198
+ "regions": 1,
2199
+ "dungeons": 1,
2200
+ "tasks": 1,
2201
+ "task-summaries": 1,
2202
+ "shops": 1,
2203
+ "exchanges": 1,
2204
+ "mails": 2,
2205
+ "music": 2,
2206
+ "chat-emojis": 2,
2207
+ "photo-actions": 2,
2208
+ "items": 3,
2209
+ "profile-assets": 4
2210
+ };
2211
+ function wikiGlobalCatalogPriority(catalog) {
2212
+ const key = String(catalog?.key || "");
2213
+ return WIKI_GLOBAL_PRIORITIES[key] ?? 2;
2214
+ }
2215
+ __name(wikiGlobalCatalogPriority, "wikiGlobalCatalogPriority");
2216
+ var WIKI_KEY_LABELS = {
2217
+ pet_id: "精灵ID",
2218
+ skill_id: "技能ID",
2219
+ item_id: "物品ID",
2220
+ item_kind: "物品类型",
2221
+ egg_conf_id: "蛋配置ID",
2222
+ random_egg_id: "随机蛋ID",
2223
+ tree_id: "果实树ID",
2224
+ medal_id: "奖章ID",
2225
+ ball_id: "咕噜球ID",
2226
+ plant_id: "种植ID",
2227
+ carryon_id: "携带物ID",
2228
+ furniture_id: "家具ID",
2229
+ suit_id: "套装ID",
2230
+ region_id: "地区ID",
2231
+ dungeon_id: "副本ID",
2232
+ mail_id: "邮件ID",
2233
+ music_id: "音乐ID",
2234
+ asset_type: "资产类型",
2235
+ asset_id: "资产ID",
2236
+ emoji_id: "表情ID",
2237
+ action_type: "动作类型",
2238
+ action_id: "动作ID",
2239
+ task_id: "任务ID",
2240
+ summary_id: "剧情ID",
2241
+ shop_id: "商店ID",
2242
+ exchange_id: "兑换ID",
2243
+ handbook_no: "图鉴编号",
2244
+ stage: "阶段",
2245
+ quality: "品质",
2246
+ cost: "能量",
2247
+ power: "威力",
2248
+ level: "等级",
2249
+ total_exp: "累计经验",
2250
+ next_level_exp: "升级所需",
2251
+ next_level_total_exp: "下级累计",
2252
+ hatch_label: "孵化时间",
2253
+ hatch_seconds: "孵化秒数",
2254
+ egg_type: "蛋类型",
2255
+ egg_size: "蛋尺寸",
2256
+ body_size: "体型",
2257
+ type: "类型",
2258
+ skill_type: "技能类型",
2259
+ damage_type: "伤害类型",
2260
+ element_type: "属性",
2261
+ label_type: "标签",
2262
+ families: "技能族",
2263
+ pet_count: "可用精灵",
2264
+ learnable_pet_count: "可学习精灵",
2265
+ food_close_exp: "亲密经验",
2266
+ close_exp: "亲密经验",
2267
+ home_exp_num: "家园经验",
2268
+ furniture_coin_num: "家具币",
2269
+ need_time_label: "制作时间",
2270
+ interaction_count: "交互次数",
2271
+ comfort: "舒适度",
2272
+ footprint: "占地",
2273
+ gender: "性别",
2274
+ grade: "品级",
2275
+ bond: "羁绊",
2276
+ category: "分类",
2277
+ area: "区域",
2278
+ source: "来源",
2279
+ source_label: "来源",
2280
+ summary: "摘要",
2281
+ description: "说明",
2282
+ flavor_text: "背景说明",
2283
+ enabled: "启用",
2284
+ banned: "禁用",
2285
+ shareable: "可分享",
2286
+ common: "通用",
2287
+ nest_num: "巢穴数",
2288
+ egg_laying_nest_num: "产蛋巢数",
2289
+ pet_lay_egg_rate_percent: "产蛋概率",
2290
+ close_level: "亲密等级",
2291
+ action_label: "行为"
2292
+ };
2293
+ function wikiLabelForKey(key) {
2294
+ return WIKI_KEY_LABELS[String(key || "")] || String(key || "").replace(/_/g, " ");
2295
+ }
2296
+ __name(wikiLabelForKey, "wikiLabelForKey");
2297
+ var WIKI_SECTION_TITLES = {
2298
+ items: "条目",
2299
+ members: "家族成员",
2300
+ probabilities: "概率信息",
2301
+ variants: "蛋型变体",
2302
+ voice_percent: "语音概率",
2303
+ topics: "图鉴课题",
2304
+ rewards: "奖励",
2305
+ reward_items: "奖励",
2306
+ acquire_methods: "获取方式",
2307
+ acquisition: "获取方式",
2308
+ methods: "获取方式",
2309
+ sources: "来源",
2310
+ world_maps: "世界地图",
2311
+ habitats: "栖息地",
2312
+ effects: "效果",
2313
+ rules: "规则",
2314
+ parts: "套装部件",
2315
+ tags: "标签",
2316
+ goods: "商品",
2317
+ mall_items: "商城商品",
2318
+ get_items: "获得物品",
2319
+ cost_items: "消耗物品",
2320
+ comfort_levels: "舒适度收益",
2321
+ levels: "等级成长",
2322
+ pet_bond_counts: "精灵羁绊",
2323
+ pet_home_limits: "入驻上限",
2324
+ settled_bonuses: "入驻加成",
2325
+ enjoy_field_types: "喜欢场景",
2326
+ hate_field_types: "讨厌场景",
2327
+ npc_reactions: "NPC 反应",
2328
+ movements: "骑乘动作",
2329
+ blood: "血脉技能",
2330
+ level: "升级习得",
2331
+ machine: "技能石习得",
2332
+ field: "场景习得"
2333
+ };
2334
+ function wikiSectionTitle(key) {
2335
+ return WIKI_SECTION_TITLES[String(key || "")] || wikiLabelForKey(key);
2336
+ }
2337
+ __name(wikiSectionTitle, "wikiSectionTitle");
2338
+
2339
+ // src/wiki-service.ts
2340
+ var logger5 = new import_koishi5.Logger("rocom-wiki");
2341
+ var COPYRIGHT = "Koishi & WeGame 洛克王国插件";
2342
+ function normalizeQueryText(text) {
2343
+ return String(text ?? "").replace(/\s+/g, "").trim().toLowerCase();
2344
+ }
2345
+ __name(normalizeQueryText, "normalizeQueryText");
2346
+ function wikiNamedValue(value) {
2347
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2348
+ return String(value.name || value.label || value.value || "");
2349
+ }
2350
+ return String(value ?? "");
2351
+ }
2352
+ __name(wikiNamedValue, "wikiNamedValue");
2353
+ function wikiNames(values) {
2354
+ if (!values) return [];
2355
+ const list = Array.isArray(values) ? values : [values];
2356
+ const result = [];
2357
+ for (const value of list) {
2358
+ const text = wikiNamedValue(value).trim();
2359
+ if (text) result.push(text);
2360
+ }
2361
+ return result;
2362
+ }
2363
+ __name(wikiNames, "wikiNames");
2364
+ function wikiRangeLabel(data, keyMin, keyMax, unit) {
2365
+ if (!data || typeof data !== "object") return "暂无";
2366
+ const low = data[keyMin];
2367
+ const high = data[keyMax];
2368
+ const empty = /* @__PURE__ */ __name((v) => v === void 0 || v === null || v === "", "empty");
2369
+ if (empty(low) && empty(high)) return "暂无";
2370
+ if (empty(low)) return `${high}${unit}`;
2371
+ if (empty(high) || low === high) return `${low}${unit}`;
2372
+ return `${low}-${high}${unit}`;
2373
+ }
2374
+ __name(wikiRangeLabel, "wikiRangeLabel");
2375
+ function similarityRatio(a, b) {
2376
+ if (!a || !b) return 0;
2377
+ if (a === b) return 1;
2378
+ const lenA = a.length;
2379
+ const lenB = b.length;
2380
+ const dp = Array.from({ length: lenB + 1 }, (_, j) => j);
2381
+ for (let i = 1; i <= lenA; i++) {
2382
+ let prev = dp[0];
2383
+ dp[0] = i;
2384
+ for (let j = 1; j <= lenB; j++) {
2385
+ const temp = dp[j];
2386
+ dp[j] = a[i - 1] === b[j - 1] ? prev : Math.min(prev, dp[j - 1], dp[j]) + 1;
2387
+ prev = temp;
2388
+ }
2389
+ }
2390
+ return 1 - dp[lenB] / Math.max(lenA, lenB);
2391
+ }
2392
+ __name(similarityRatio, "similarityRatio");
2393
+ function wikiSimilarityScore(query, item) {
2394
+ const normalizedQuery = normalizeQueryText(query);
2395
+ if (!normalizedQuery) return 0;
2396
+ const candidates = [
2397
+ wikiTitleForItem(item, ""),
2398
+ item?.name || "",
2399
+ item?.summary || "",
2400
+ item?.description || item?.desc || ""
2401
+ ];
2402
+ let best = 0;
2403
+ for (const text of candidates) {
2404
+ const normalizedText = normalizeQueryText(text);
2405
+ if (!normalizedText) continue;
2406
+ if (normalizedQuery === normalizedText) best = Math.max(best, 1);
2407
+ else if (normalizedText.includes(normalizedQuery) || normalizedQuery.includes(normalizedText)) best = Math.max(best, 0.82);
2408
+ else best = Math.max(best, similarityRatio(normalizedQuery, normalizedText));
2409
+ }
2410
+ return best;
2411
+ }
2412
+ __name(wikiSimilarityScore, "wikiSimilarityScore");
2413
+ function wikiGenericValue(value, depth = 0) {
2414
+ if (value === void 0 || value === null || value === "") return "";
2415
+ if (typeof value === "boolean") return value ? "是" : "否";
2416
+ if (typeof value === "number") return String(value);
2417
+ if (Array.isArray(value)) {
2418
+ const texts = value.slice(0, 6).map((item) => wikiGenericValue(item, depth + 1)).filter(Boolean);
2419
+ const suffix = value.length > 6 ? " ..." : "";
2420
+ return texts.join("、") + suffix;
2421
+ }
2422
+ if (typeof value === "object") {
2423
+ for (const key of ["name", "label", "title", "summary", "description", "text"]) {
2424
+ const text = String(value[key] || "").trim();
2425
+ if (text) return text;
2426
+ }
2427
+ if ("min_m" in value || "max_m" in value) return wikiRangeLabel(value, "min_m", "max_m", "m");
2428
+ if ("min_kg" in value || "max_kg" in value) return wikiRangeLabel(value, "min_kg", "max_kg", "kg");
2429
+ if (depth >= 1) {
2430
+ const pairs = [];
2431
+ for (const [key, item] of Object.entries(value).slice(0, 4)) {
2432
+ const text = wikiGenericValue(item, depth + 1);
2433
+ if (text) pairs.push(`${wikiLabelForKey(key)}:${text}`);
2434
+ }
2435
+ return pairs.join(";");
2436
+ }
2437
+ return "";
2438
+ }
2439
+ return String(value);
2440
+ }
2441
+ __name(wikiGenericValue, "wikiGenericValue");
2442
+ function wikiSizeLabel(value) {
2443
+ if (!value || typeof value !== "object" || Array.isArray(value)) return wikiGenericValue(value);
2444
+ const parts = [];
2445
+ if (value.height && typeof value.height === "object") {
2446
+ const text = wikiRangeLabel(value.height, "min_m", "max_m", "m");
2447
+ if (text && text !== "暂无") parts.push(`高 ${text}`);
2448
+ }
2449
+ if (value.weight && typeof value.weight === "object") {
2450
+ const text = wikiRangeLabel(value.weight, "min_kg", "max_kg", "kg");
2451
+ if (text && text !== "暂无") parts.push(`重 ${text}`);
2452
+ }
2453
+ return parts.join(" / ");
2454
+ }
2455
+ __name(wikiSizeLabel, "wikiSizeLabel");
2456
+ function wikiFootprintLabel(value) {
2457
+ if (!value || typeof value !== "object" || Array.isArray(value)) return wikiGenericValue(value);
2458
+ const width = value.width ?? value.x ?? value.cols;
2459
+ const height = value.height ?? value.y ?? value.rows;
2460
+ if (width !== void 0 && width !== null && width !== "" && height !== void 0 && height !== null && height !== "") {
2461
+ return `${width} x ${height}`;
2462
+ }
2463
+ return wikiGenericValue(value, 1);
2464
+ }
2465
+ __name(wikiFootprintLabel, "wikiFootprintLabel");
2466
+ function wikiCountLabel(value, unit = "个") {
2467
+ if (Array.isArray(value)) return `${value.length}${unit}`;
2468
+ if (value === void 0 || value === null || value === "") return "";
2469
+ return typeof value === "number" ? `${value}${unit}` : String(value);
2470
+ }
2471
+ __name(wikiCountLabel, "wikiCountLabel");
2472
+ function wikiPickImage(item) {
2473
+ const imageKeys = [
2474
+ "big_icon",
2475
+ "image",
2476
+ "cover_image",
2477
+ "preview_image",
2478
+ "checkout_icon",
2479
+ "icon",
2480
+ "small_icon",
2481
+ "background_image",
2482
+ "display_image",
2483
+ "package_bg",
2484
+ "package_cover"
2485
+ ];
2486
+ for (const key of imageKeys) {
2487
+ const value = item?.[key];
2488
+ if (typeof value === "string" && /^(https?:\/\/|\{\{)/.test(value)) return value;
2489
+ if (value && typeof value === "object") {
2490
+ const url = value.url || value.image || value.icon;
2491
+ if (typeof url === "string" && /^(https?:\/\/|\{\{)/.test(url)) return url;
2492
+ }
2493
+ }
2494
+ return "";
2495
+ }
2496
+ __name(wikiPickImage, "wikiPickImage");
2497
+ function wikiTitleForItem(item, fallback = "Wiki 条目") {
2498
+ for (const key of ["name", "title", "display_name", "summary", "description"]) {
2499
+ const value = String(item?.[key] || "").trim();
2500
+ if (value) return value.slice(0, 48);
2501
+ }
2502
+ for (const key of [
2503
+ "pet_id",
2504
+ "skill_id",
2505
+ "item_id",
2506
+ "egg_conf_id",
2507
+ "random_egg_id",
2508
+ "tree_id",
2509
+ "medal_id",
2510
+ "ball_id",
2511
+ "plant_id",
2512
+ "furniture_id",
2513
+ "suit_id",
2514
+ "region_id",
2515
+ "dungeon_id",
2516
+ "mail_id",
2517
+ "music_id",
2518
+ "task_id",
2519
+ "shop_id",
2520
+ "exchange_id",
2521
+ "id"
2522
+ ]) {
2523
+ const value = item?.[key];
2524
+ if (value !== void 0 && value !== null && value !== "") return `${fallback} #${value}`;
2525
+ }
2526
+ return fallback;
2527
+ }
2528
+ __name(wikiTitleForItem, "wikiTitleForItem");
2529
+ function wikiSummaryForItem(item) {
2530
+ for (const key of ["summary", "description", "desc", "effect", "flavor_text", "text", "content"]) {
2531
+ const value = wikiGenericValue(item?.[key]);
2532
+ if (value) return value.slice(0, 180);
2533
+ }
2534
+ return "";
2535
+ }
2536
+ __name(wikiSummaryForItem, "wikiSummaryForItem");
2537
+ function wikiBadgesForItem(item) {
2538
+ const badges = [];
2539
+ for (const key of ["type", "skill_type", "damage_type", "element_type", "category", "quality", "rarity", "label_type", "egg_type", "grade", "gender"]) {
2540
+ const value = wikiGenericValue(item?.[key]);
2541
+ if (value) badges.push(value);
2542
+ }
2543
+ for (const badge of item?.badges || []) {
2544
+ const value = wikiGenericValue(badge);
2545
+ if (value) badges.push(value);
2546
+ }
2547
+ for (const key of ["type_names", "egg_group_names", "tags", "source_counts"]) {
2548
+ badges.push(...wikiNames(item?.[key]).slice(0, 4));
2549
+ }
2550
+ const output = [];
2551
+ const seen = /* @__PURE__ */ new Set();
2552
+ for (const badge of badges) {
2553
+ const key = String(badge);
2554
+ if (key && !seen.has(key)) {
2555
+ output.push(key);
2556
+ seen.add(key);
2557
+ }
2558
+ }
2559
+ return output.slice(0, 8);
2560
+ }
2561
+ __name(wikiBadgesForItem, "wikiBadgesForItem");
2562
+ function wikiAddFact(rows, label, value) {
2563
+ const text = wikiGenericValue(value);
2564
+ if (text) rows.push({ label, value: text.slice(0, 96) });
2565
+ }
2566
+ __name(wikiAddFact, "wikiAddFact");
2567
+ function wikiMetaForItem(item, limit = 12, catalogKey = "") {
2568
+ const rows = [];
2569
+ if (catalogKey === "pets") {
2570
+ wikiAddFact(rows, "图鉴编号", item?.handbook_no);
2571
+ wikiAddFact(rows, "精灵ID", item?.pet_id);
2572
+ wikiAddFact(rows, "属性", item?.type_names || item?.types);
2573
+ wikiAddFact(rows, "蛋组", item?.egg_group_names || item?.egg_groups);
2574
+ } else if (catalogKey === "skills") {
2575
+ wikiAddFact(rows, "技能ID", item?.skill_id);
2576
+ wikiAddFact(rows, "属性", item?.element_type || item?.type);
2577
+ wikiAddFact(rows, "类型", item?.skill_type || item?.damage_type);
2578
+ wikiAddFact(rows, "威力 / 能量", `${item?.power ?? "-"} / ${item?.cost ?? "-"}`);
2579
+ wikiAddFact(rows, "可用精灵", item?.pet_count);
2580
+ } else if (["pet-eggs", "egg-items", "random-eggs", "pet-egg"].includes(catalogKey)) {
2581
+ wikiAddFact(rows, "蛋ID", item?.egg_conf_id || item?.random_egg_id || item?.item_id);
2582
+ wikiAddFact(rows, "蛋类型", item?.egg_type);
2583
+ wikiAddFact(rows, "孵化时间", item?.hatch_label);
2584
+ wikiAddFact(rows, "尺寸", wikiSizeLabel(item?.egg_size));
2585
+ wikiAddFact(rows, "关联精灵", item?.pet);
2586
+ } else if (["pet-foods", "pet-gifts"].includes(catalogKey)) {
2587
+ wikiAddFact(rows, "物品ID", item?.item_id);
2588
+ wikiAddFact(rows, "亲密经验", item?.food_close_exp || item?.close_exp);
2589
+ wikiAddFact(rows, "家园经验", item?.home_exp_num);
2590
+ wikiAddFact(rows, "制作时间", item?.need_time_label);
2591
+ wikiAddFact(rows, "交互次数", item?.interaction_count);
2592
+ } else if (catalogKey === "home-egg-lay-rates") {
2593
+ wikiAddFact(rows, "巢穴数", item?.nest_num);
2594
+ wikiAddFact(rows, "产蛋巢", item?.egg_laying_nest_num);
2595
+ const rate = item?.pet_lay_egg_rate_percent;
2596
+ wikiAddFact(rows, "产蛋概率", rate !== void 0 && rate !== null && rate !== "" ? `${rate}%` : "");
2597
+ } else if (catalogKey === "pet-levels") {
2598
+ wikiAddFact(rows, "等级", item?.level);
2599
+ wikiAddFact(rows, "累计经验", item?.total_exp);
2600
+ wikiAddFact(rows, "下级所需", item?.next_level_exp);
2601
+ } else if (catalogKey === "furniture") {
2602
+ wikiAddFact(rows, "家具ID", item?.furniture_id);
2603
+ wikiAddFact(rows, "分类", item?.category);
2604
+ wikiAddFact(rows, "舒适度", item?.comfort);
2605
+ wikiAddFact(rows, "占地", wikiFootprintLabel(item?.footprint));
2606
+ } else if (catalogKey === "fashion") {
2607
+ wikiAddFact(rows, "套装ID", item?.suit_id);
2608
+ wikiAddFact(rows, "性别", item?.gender);
2609
+ wikiAddFact(rows, "品级", item?.grade);
2610
+ wikiAddFact(rows, "部件", wikiCountLabel(item?.parts));
2611
+ } else if (catalogKey === "exchanges") {
2612
+ wikiAddFact(rows, "兑换ID", item?.exchange_id);
2613
+ wikiAddFact(rows, "获得", item?.get_items);
2614
+ wikiAddFact(rows, "消耗", item?.cost_items);
2615
+ } else if (catalogKey === "shops") {
2616
+ wikiAddFact(rows, "商店ID", item?.shop_id);
2617
+ wikiAddFact(rows, "页签", item?.tab_name);
2618
+ wikiAddFact(rows, "商品数", wikiCountLabel(item?.goods));
2619
+ } else if (["skill-stones", "skill-stone-recipes", "items", "pet-fruits", "recipes", "balls", "plants", "pet-carryons", "medals"].includes(catalogKey)) {
2620
+ wikiAddFact(rows, "物品ID", item?.item_id || item?.medal_id || item?.ball_id || item?.plant_id || item?.carryon_id);
2621
+ wikiAddFact(rows, "类型", item?.type || item?.label_type);
2622
+ wikiAddFact(rows, "品质", item?.quality);
2623
+ wikiAddFact(rows, "效果", item?.summary || item?.catch_effect || item?.carryon);
2624
+ } else {
2625
+ for (const key of [
2626
+ "pet_id",
2627
+ "skill_id",
2628
+ "item_id",
2629
+ "tree_id",
2630
+ "level",
2631
+ "close_level",
2632
+ "action_label",
2633
+ "region_id",
2634
+ "dungeon_id",
2635
+ "mail_id",
2636
+ "music_id",
2637
+ "emoji_id",
2638
+ "task_id",
2639
+ "summary_id",
2640
+ "asset_type",
2641
+ "asset_id",
2642
+ "action_type",
2643
+ "action_id",
2644
+ "quality",
2645
+ "type",
2646
+ "category"
2647
+ ]) {
2648
+ if (item && key in item) wikiAddFact(rows, wikiLabelForKey(key), item[key]);
2649
+ }
2650
+ }
2651
+ if (rows.length) return rows.slice(0, limit);
2652
+ const skip = /* @__PURE__ */ new Set([
2653
+ "name",
2654
+ "title",
2655
+ "description",
2656
+ "desc",
2657
+ "summary",
2658
+ "icon",
2659
+ "small_icon",
2660
+ "big_icon",
2661
+ "image",
2662
+ "cover_image",
2663
+ "preview_image",
2664
+ "background_image",
2665
+ "items",
2666
+ "rewards",
2667
+ "reward_items",
2668
+ "tags",
2669
+ "badges",
2670
+ "catalog"
2671
+ ]);
2672
+ for (const [key, value] of Object.entries(item || {})) {
2673
+ if (skip.has(key)) continue;
2674
+ const text = wikiGenericValue(value);
2675
+ if (!text) continue;
2676
+ rows.push({ label: wikiLabelForKey(key), value: text.slice(0, 80) });
2677
+ if (rows.length >= limit) break;
2678
+ }
2679
+ return rows;
2680
+ }
2681
+ __name(wikiMetaForItem, "wikiMetaForItem");
2682
+ function wikiCardForItem(item, fallback, catalog) {
2683
+ const catalogKey = String(catalog?.key || "");
2684
+ return {
2685
+ title: wikiTitleForItem(item, fallback),
2686
+ image: wikiPickImage(item),
2687
+ summary: wikiSummaryForItem(item),
2688
+ badges: wikiBadgesForItem(item),
2689
+ meta: wikiMetaForItem(item, 5, catalogKey)
2690
+ };
2691
+ }
2692
+ __name(wikiCardForItem, "wikiCardForItem");
2693
+ function wikiRowsFromDict(value, limit = 16) {
2694
+ const rows = [];
2695
+ for (const [key, item] of Object.entries(value)) {
2696
+ if (["icon", "small_icon", "big_icon", "image", "display_image"].includes(key)) continue;
2697
+ const text = wikiGenericValue(item);
2698
+ if (text) rows.push({ label: wikiLabelForKey(key), value: text.slice(0, 120) });
2699
+ if (rows.length >= limit) break;
2700
+ }
2701
+ return rows;
2702
+ }
2703
+ __name(wikiRowsFromDict, "wikiRowsFromDict");
2704
+ function wikiSectionsForPayload(payload, catalog) {
2705
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return [];
2706
+ const sections = [];
2707
+ const addList = /* @__PURE__ */ __name((title, value) => {
2708
+ if (!Array.isArray(value) || !value.length) return;
2709
+ const dictItems = value.filter((item) => item && typeof item === "object" && !Array.isArray(item));
2710
+ if (dictItems.length) {
2711
+ sections.push({
2712
+ title,
2713
+ cards: dictItems.slice(0, 12).map((item) => wikiCardForItem(item, title, catalog)),
2714
+ rows: [],
2715
+ total: dictItems.length
2716
+ });
2717
+ return;
2718
+ }
2719
+ const texts = value.slice(0, 16).map((item) => wikiGenericValue(item)).filter(Boolean);
2720
+ if (!texts.length) return;
2721
+ sections.push({
2722
+ title,
2723
+ cards: [],
2724
+ rows: texts.map((text, idx) => ({ label: String(idx + 1), value: text })),
2725
+ total: texts.length
2726
+ });
2727
+ }, "addList");
2728
+ for (const [key, value] of Object.entries(payload)) {
2729
+ const title = wikiSectionTitle(key);
2730
+ if (value && typeof value === "object" && !Array.isArray(value) && ["probabilities", "npc_reactions", "catch_effect", "carryon", "planting", "limit", "bond", "mall_package"].includes(key)) {
2731
+ const rows = wikiRowsFromDict(value);
2732
+ if (rows.length) sections.push({ title, cards: [], rows, total: rows.length });
2733
+ continue;
2734
+ }
2735
+ addList(title, value);
2736
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2737
+ for (const [subkey, subvalue] of Object.entries(value)) {
2738
+ addList(`${title} / ${wikiSectionTitle(subkey)}`, subvalue);
2739
+ }
2740
+ }
2741
+ }
2742
+ return sections.slice(0, 8);
2743
+ }
2744
+ __name(wikiSectionsForPayload, "wikiSectionsForPayload");
2745
+ function findExactWikiMatch(results, query) {
2746
+ const normalizedQuery = normalizeQueryText(query);
2747
+ if (!normalizedQuery) return null;
2748
+ for (const item of results) {
2749
+ const name2 = String(item?.name || "");
2750
+ const form = String(item?.form || "");
2751
+ const candidates = [
2752
+ normalizeQueryText(name2),
2753
+ normalizeQueryText(`${name2}${form}`),
2754
+ normalizeQueryText(`${name2} ${form}`),
2755
+ normalizeQueryText(`${form}${name2}`)
2756
+ ];
2757
+ if (candidates.includes(normalizedQuery)) return item;
2758
+ }
2759
+ return null;
2760
+ }
2761
+ __name(findExactWikiMatch, "findExactWikiMatch");
2762
+ function wikiCandidateText(query, items, kind) {
2763
+ const lines = [`找到多个${kind}候选,请使用更精确名称:`];
2764
+ items.slice(0, 10).forEach((item, idx) => {
2765
+ const name2 = item?.name || "未知";
2766
+ const form = item?.form || "";
2767
+ const itemId = item?.pet_id || item?.skill_id || item?.id || "-";
2768
+ const suffix = form && form !== "普通" ? `(${form})` : "";
2769
+ lines.push(`${idx + 1}. ${name2}${suffix} #${itemId}`);
2770
+ });
2771
+ lines.push(`
2772
+ 你查询的是:${query}`);
2773
+ return lines.join("\n");
2774
+ }
2775
+ __name(wikiCandidateText, "wikiCandidateText");
2776
+ function wikiBodySize(...sources) {
2777
+ let bodySize = {};
2778
+ for (const source of sources) {
2779
+ if (source && typeof source === "object" && source.body_size && typeof source.body_size === "object") {
2780
+ bodySize = source.body_size;
2781
+ break;
2782
+ }
2783
+ }
2784
+ return {
2785
+ height: wikiRangeLabel(bodySize?.height, "min_m", "max_m", "m"),
2786
+ weight: wikiRangeLabel(bodySize?.weight, "min_kg", "max_kg", "kg")
2787
+ };
2788
+ }
2789
+ __name(wikiBodySize, "wikiBodySize");
2790
+ function wikiGenderLabel(value) {
2791
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2792
+ const parts = [];
2793
+ if (value.male_percent !== void 0 && value.male_percent !== null && value.male_percent !== "") parts.push(`雄 ${value.male_percent}%`);
2794
+ if (value.female_percent !== void 0 && value.female_percent !== null && value.female_percent !== "") parts.push(`雌 ${value.female_percent}%`);
2795
+ return parts.length ? parts.join(" / ") : "暂无";
2796
+ }
2797
+ return String(value || "暂无");
2798
+ }
2799
+ __name(wikiGenderLabel, "wikiGenderLabel");
2800
+ function wikiEcologyLabel(value, fallback = "") {
2801
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2802
+ const parts = [];
2803
+ for (const key of ["pet_text", "pet_style"]) {
2804
+ const text = String(value[key] || "").trim();
2805
+ if (text) parts.push(text);
2806
+ }
2807
+ const habitats = wikiNames(value.habitats);
2808
+ if (habitats.length) parts.push("栖息:" + habitats.join(" / "));
2809
+ return parts.join(";");
2810
+ }
2811
+ return String(value || fallback || "").trim();
2812
+ }
2813
+ __name(wikiEcologyLabel, "wikiEcologyLabel");
2814
+ function wikiSourceLabel(source, fallback = "") {
2815
+ const mapping = { level: "升级", machine: "技能石", blood: "血脉", field: "特殊来源" };
2816
+ const text = String(source || "").trim();
2817
+ return String(fallback || mapping[text] || text || "");
2818
+ }
2819
+ __name(wikiSourceLabel, "wikiSourceLabel");
2820
+ function wikiTypeEffectiveness(profile) {
2821
+ const data = profile?.type_effectiveness;
2822
+ if (!data || typeof data !== "object") return [];
2823
+ const rows = [];
2824
+ const definitions = [
2825
+ ["攻击克制", ["attack", "strong"]],
2826
+ ["攻击弱效", ["attack", "weak"]],
2827
+ ["防御弱点", ["defense", "weak"]],
2828
+ ["防御抵抗", ["defense", "resist"]]
2829
+ ];
2830
+ for (const [label, [groupKey, itemKey]] of definitions) {
2831
+ const group = data[groupKey] && typeof data[groupKey] === "object" ? data[groupKey] : {};
2832
+ const names = wikiNames(group?.[itemKey]);
2833
+ if (names.length) rows.push({ label, value: names.join(" / ") });
2834
+ }
2835
+ return rows;
2836
+ }
2837
+ __name(wikiTypeEffectiveness, "wikiTypeEffectiveness");
2838
+ function buildPetStats(profile) {
2839
+ const attrs = profile?.attributes && typeof profile.attributes === "object" ? profile.attributes : {};
2840
+ const defs = [
2841
+ ["精力", "hp", "#42b883"],
2842
+ ["物攻", "physical_attack", "#e86452"],
2843
+ ["魔攻", "magic_attack", "#5987f5"],
2844
+ ["物防", "physical_defense", "#d69a32"],
2845
+ ["魔防", "magic_defense", "#25a6a6"],
2846
+ ["速度", "speed", "#8d62d9"]
2847
+ ];
2848
+ return defs.map(([label, key, color]) => {
2849
+ const value = Number(attrs[key]) || 0;
2850
+ return { label, value, color, percent: Math.min(value / 160 * 100, 100) };
2851
+ });
2852
+ }
2853
+ __name(buildPetStats, "buildPetStats");
2854
+ function buildPetSkillGroups(skillsData) {
2855
+ const groups = [];
2856
+ const sourceDefs = [
2857
+ ["level", "升级习得"],
2858
+ ["machine", "技能机"],
2859
+ ["blood", "血脉技能"],
2860
+ ["field", "场景习得"]
2861
+ ];
2862
+ for (const [key, label] of sourceDefs) {
2863
+ const rawItems = Array.isArray(skillsData?.[key]) ? skillsData[key] : [];
2864
+ const items = rawItems.slice(0, 10).map((skill) => ({
2865
+ name: skill?.name || "未知技能",
2866
+ icon: skill?.icon || "",
2867
+ level: skill?.level || skill?.source_label || "",
2868
+ type: wikiNamedValue(skill?.element_type || skill?.type) || "未知",
2869
+ category: wikiNamedValue(skill?.skill_type) || wikiNamedValue(skill?.damage_type) || "未知",
2870
+ cost: skill?.cost ?? "?",
2871
+ power: skill?.power ?? "?",
2872
+ desc: skill?.desc || skill?.description || skill?.flavor_text || "暂无说明"
2873
+ }));
2874
+ if (items.length) groups.push({ label, items });
2875
+ }
2876
+ return groups;
2877
+ }
2878
+ __name(buildPetSkillGroups, "buildPetSkillGroups");
2879
+ function buildPetFamily(familyData, currentId) {
2880
+ let items = [];
2881
+ if (familyData && typeof familyData === "object" && !Array.isArray(familyData)) {
2882
+ if (Array.isArray(familyData.members)) items.push(...familyData.members);
2883
+ for (const formGroup of familyData.forms || []) {
2884
+ for (const member of formGroup?.members || []) {
2885
+ const merged = { ...member };
2886
+ if (formGroup?.name && !merged.form_group) merged.form_group = formGroup.name;
2887
+ items.push(merged);
2888
+ }
2889
+ }
2890
+ if (!items.length) {
2891
+ for (const key of ["items", "family", "evolutions"]) {
2892
+ if (Array.isArray(familyData[key])) {
2893
+ items.push(...familyData[key]);
2894
+ break;
2895
+ }
2896
+ }
2897
+ }
2898
+ } else if (Array.isArray(familyData)) {
2899
+ items = familyData;
2900
+ }
2901
+ const result = [];
2902
+ const seen = /* @__PURE__ */ new Set();
2903
+ for (const item of items.slice(0, 12)) {
2904
+ const petId = item?.pet_id ?? item?.id;
2905
+ const key = String(petId ?? item?.name ?? "");
2906
+ if (key && seen.has(key)) continue;
2907
+ seen.add(key);
2908
+ const conditionTexts = Array.isArray(item?.condition_texts) ? item.condition_texts : [];
2909
+ result.push({
2910
+ pet_id: petId ?? "-",
2911
+ name: item?.name || "未知精灵",
2912
+ form: item?.form || item?.form_group || "",
2913
+ icon: item?.icon || item?.small_icon || "",
2914
+ condition: item?.condition_summary || conditionTexts.filter(Boolean).join(" / ") || item?.evolution_description || item?.condition || item?.evolve_condition || item?.relation || "",
2915
+ is_current: String(petId ?? "") === String(currentId ?? "")
2916
+ });
2917
+ }
2918
+ return result;
2919
+ }
2920
+ __name(buildPetFamily, "buildPetFamily");
2921
+ function buildHandbookTopics(handbookData) {
2922
+ const topics = Array.isArray(handbookData?.topics) ? handbookData.topics : [];
2923
+ const result = [];
2924
+ for (const item of topics.slice(0, 8)) {
2925
+ const rewards = [];
2926
+ let rewardItems = item?.rewards || item?.reward_items;
2927
+ if (!rewardItems && item?.reward && typeof item.reward === "object") rewardItems = item.reward.items;
2928
+ for (const reward of rewardItems || []) {
2929
+ rewards.push({
2930
+ name: reward?.name || "奖励",
2931
+ count: reward?.count || "",
2932
+ icon: reward?.icon || ""
2933
+ });
2934
+ }
2935
+ result.push({
2936
+ name: item?.name || item?.title || `课题 ${item?.topic_id || ""}`.trim(),
2937
+ desc: item?.description || item?.desc || "",
2938
+ rewards: rewards.slice(0, 4)
2939
+ });
2940
+ }
2941
+ return result;
2942
+ }
2943
+ __name(buildHandbookTopics, "buildHandbookTopics");
2944
+ function buildWikiPetRenderData(overview, profile, skills, family, handbook, query) {
2945
+ profile = profile || {};
2946
+ skills = skills || {};
2947
+ family = family || {};
2948
+ handbook = handbook || {};
2949
+ const petId = overview?.pet_id ?? profile?.pet_id;
2950
+ const typeNames = wikiNames(overview?.type_names || overview?.types);
2951
+ const eggGroups = wikiNames(overview?.egg_group_names || overview?.egg_groups);
2952
+ const feature = overview?.feature || {};
2953
+ const bodySize = wikiBodySize(profile, overview);
2954
+ const stats = buildPetStats(profile);
2955
+ const totalStats = profile?.attributes?.sum || stats.reduce((sum, item) => sum + item.value, 0);
2956
+ const form = overview?.form || profile?.form || "";
2957
+ const classis = wikiNamedValue(overview?.classis) || "暂无";
2958
+ const habitats = wikiNames(profile?.habitats);
2959
+ const areas = wikiNames(handbook?.areas);
2960
+ return {
2961
+ name: overview?.name || profile?.name || query,
2962
+ query,
2963
+ pet_id: petId ?? "-",
2964
+ number: overview?.handbook_no || profile?.handbook_no || "---",
2965
+ form,
2966
+ formLine: `${form || "普通"}${classis && classis !== "暂无" ? ` · ${classis}` : ""}`,
2967
+ quality: overview?.quality || profile?.quality || "",
2968
+ stage: overview?.stage || profile?.stage || "",
2969
+ pet_icon: overview?.icon || overview?.small_icon || "",
2970
+ main_image: profile?.small_icon || overview?.small_icon || profile?.icon || overview?.icon || "",
2971
+ type_names: typeNames,
2972
+ egg_groups: eggGroups,
2973
+ description: profile?.description || overview?.description || "暂无图鉴描述",
2974
+ classis,
2975
+ feature_name: feature?.name || "暂无",
2976
+ feature_desc: feature?.desc || "暂无特性说明",
2977
+ ride_talent: overview?.has_ride_talent ? "支持" : "不支持",
2978
+ height_label: bodySize.height,
2979
+ weight_label: bodySize.weight,
2980
+ gender_ratio: wikiGenderLabel(profile?.gender_ratio),
2981
+ move_type: profile?.move_type || "暂无",
2982
+ habitats,
2983
+ habitatsLabel: habitats.length ? habitats.join(" / ") : "暂无",
2984
+ ecology: wikiEcologyLabel(profile?.ecology, profile?.pet_style),
2985
+ type_effectiveness: wikiTypeEffectiveness(profile),
2986
+ total_stats: totalStats,
2987
+ pet_stats: stats,
2988
+ skill_groups: buildPetSkillGroups(skills),
2989
+ family_members: buildPetFamily(family, petId),
2990
+ handbook_topics: buildHandbookTopics(handbook),
2991
+ areas,
2992
+ areasLabel: areas.join(" / "),
2993
+ commandHint: "💡 洛克wiki <类型> <关键词或ID> | 示例:洛克wiki 技能 圣光斩",
2994
+ copyright: COPYRIGHT
2995
+ };
2996
+ }
2997
+ __name(buildWikiPetRenderData, "buildWikiPetRenderData");
2998
+ function buildWikiSkillRenderData(item, petsData, query) {
2999
+ const pets = ((petsData || {}).items || []).slice(0, 30).map((pet) => {
3000
+ const source = wikiSourceLabel(pet?.source, pet?.source_label);
3001
+ const level = pet?.level || "";
3002
+ return {
3003
+ name: pet?.name || "未知精灵",
3004
+ pet_id: pet?.pet_id ?? "-",
3005
+ icon: pet?.icon || pet?.small_icon || "",
3006
+ source,
3007
+ level,
3008
+ sourceLine: source || level ? `${source}${level ? ` · Lv.${level}` : ""}` : ""
3009
+ };
3010
+ });
3011
+ const tags = (item?.tags || []).map((tag) => ({ name: tag?.name || "", icon: tag?.icon || "" }));
3012
+ return {
3013
+ name: item?.name || query,
3014
+ skill_id: item?.skill_id ?? "-",
3015
+ query,
3016
+ icon: item?.icon || "",
3017
+ type: wikiNamedValue(item?.type) || "未知",
3018
+ skill_type: wikiNamedValue(item?.skill_type) || "未知",
3019
+ damage_type: wikiNamedValue(item?.damage_type) || "未知",
3020
+ element_type: wikiNamedValue(item?.element_type) || "未知",
3021
+ cost: item?.cost ?? "?",
3022
+ power: item?.power ?? "?",
3023
+ families: item?.families || "暂无",
3024
+ description: item?.description || "暂无说明",
3025
+ flavor_text: item?.flavor_text || "",
3026
+ tags,
3027
+ pets,
3028
+ pet_total: (petsData || {}).total || pets.length,
3029
+ commandHint: "💡 洛克wiki 技能 <技能名或ID>",
3030
+ copyright: COPYRIGHT
3031
+ };
3032
+ }
3033
+ __name(buildWikiSkillRenderData, "buildWikiSkillRenderData");
3034
+ function splitWikiCommandParts(text) {
3035
+ const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
3036
+ let pageNo = 1;
3037
+ if (parts.length) {
3038
+ const tail = parts[parts.length - 1];
3039
+ const pageMatch = /^(?:(?:p|P|页|第)(\d+)(?:页)?|(\d+)页)$/.exec(tail);
3040
+ if (pageMatch && parts.length > 1) {
3041
+ const pageValue = pageMatch[1] || pageMatch[2];
3042
+ pageNo = Math.max(parseInt(pageValue, 10), 1);
3043
+ parts.pop();
3044
+ }
3045
+ }
3046
+ return { parts, pageNo };
3047
+ }
3048
+ __name(splitWikiCommandParts, "splitWikiCommandParts");
3049
+ function wikiCatalogByToken(token) {
3050
+ const text = String(token || "").trim().toLowerCase();
3051
+ return WIKI_CATALOG_ROUTES_BY_ALIAS.get(text) || null;
3052
+ }
3053
+ __name(wikiCatalogByToken, "wikiCatalogByToken");
3054
+ var WikiService = class {
3055
+ constructor(ctx, client) {
3056
+ this.ctx = ctx;
3057
+ this.client = client;
3058
+ }
3059
+ static {
3060
+ __name(this, "WikiService");
3061
+ }
3062
+ catalogsCache = null;
3063
+ catalogsCacheTs = 0;
3064
+ optionsCache = null;
3065
+ optionsCacheTs = 0;
3066
+ skillDetailCache = /* @__PURE__ */ new Map();
3067
+ petDetailCache = /* @__PURE__ */ new Map();
3068
+ async getCatalogsPayload(force = false) {
3069
+ const now = Date.now();
3070
+ if (!force && this.catalogsCache !== null && now - this.catalogsCacheTs < 3e5) {
3071
+ return this.catalogsCache;
3072
+ }
3073
+ const payload = await this.client.getWikiCatalogs(this.ctx);
3074
+ if (payload && typeof payload === "object") {
3075
+ this.catalogsCache = payload;
3076
+ this.catalogsCacheTs = now;
3077
+ return payload;
3078
+ }
3079
+ return this.catalogsCache || {};
3080
+ }
3081
+ async getOptionsPayload(force = false) {
3082
+ const now = Date.now();
3083
+ if (!force && this.optionsCache !== null && now - this.optionsCacheTs < 3e5) {
3084
+ return this.optionsCache;
3085
+ }
3086
+ const payload = await this.client.getWikiOptions(this.ctx);
3087
+ if (payload && typeof payload === "object") {
3088
+ this.optionsCache = payload;
3089
+ this.optionsCacheTs = now;
3090
+ return payload;
3091
+ }
3092
+ return this.optionsCache || {};
3093
+ }
3094
+ backendCatalogItems(payload) {
3095
+ if (!payload || typeof payload !== "object") return [];
3096
+ const items = payload.items;
3097
+ if (!Array.isArray(items)) return [];
3098
+ return items.filter((item) => item && typeof item === "object");
3099
+ }
3100
+ catalogFromBackendItem(item) {
3101
+ const key = String(item?.key || "").trim();
3102
+ const base = WIKI_CATALOG_ROUTES_BY_KEY.get(key);
3103
+ const catalog = base ? { ...base, aliases: [...base.aliases] } : { key, title: String(item?.name || key || "Wiki"), aliases: [], list_path: "", detail_path: "", id_fields: [], search: null };
3104
+ const backendName = String(item?.name || "").trim();
3105
+ if (backendName && !base) catalog.title = backendName;
3106
+ for (const alias of [key, backendName, backendName.replace(/图鉴/g, "").trim()]) {
3107
+ if (alias && !catalog.aliases.includes(alias)) catalog.aliases.push(alias);
3108
+ }
3109
+ const filters = Array.isArray(item?.filters) ? item.filters : [];
3110
+ const hasQFilter = filters.some((f) => f && typeof f === "object" && f.key === "q");
3111
+ const routeSearch = catalog.search;
3112
+ catalog.search = routeSearch === null || routeSearch === void 0 ? hasQFilter : Boolean(routeSearch);
3113
+ if (item?.path) catalog.list_path = String(item.path);
3114
+ catalog._backend = item;
3115
+ return catalog;
3116
+ }
3117
+ catalogForKeyFromPayload(key, catalogsPayload) {
3118
+ const item = this.backendCatalogItems(catalogsPayload).find((it) => String(it?.key || "") === String(key || ""));
3119
+ if (item) return this.catalogFromBackendItem(item);
3120
+ const route2 = WIKI_CATALOG_ROUTES_BY_KEY.get(String(key || ""));
3121
+ return route2 ? { ...route2, aliases: [...route2.aliases] } : null;
3122
+ }
3123
+ catalogsFromPayload(catalogsPayload) {
3124
+ return this.backendCatalogItems(catalogsPayload).map((item) => this.catalogFromBackendItem(item));
3125
+ }
3126
+ async getCatalogByKey(key) {
3127
+ return this.catalogForKeyFromPayload(key, await this.getCatalogsPayload());
3128
+ }
3129
+ dynamicCatalogByToken(token, catalogsPayload) {
3130
+ const text = normalizeQueryText(token);
3131
+ if (!text) return null;
3132
+ for (const item of this.backendCatalogItems(catalogsPayload)) {
3133
+ const candidates = [
3134
+ item?.key,
3135
+ item?.name,
3136
+ String(item?.name || "").replace(/图鉴/g, "").trim()
3137
+ ];
3138
+ if (candidates.some((candidate) => candidate && normalizeQueryText(candidate) === text)) {
3139
+ return this.catalogFromBackendItem(item);
3140
+ }
3141
+ }
3142
+ return null;
3143
+ }
3144
+ parseWikiCommand(text) {
3145
+ const trimmed = String(text || "").trim();
3146
+ if (!trimmed) return { catalog: null, query: "", pageNo: 1 };
3147
+ const { parts, pageNo } = splitWikiCommandParts(trimmed);
3148
+ const catalog = parts.length ? wikiCatalogByToken(parts[0]) : null;
3149
+ if (catalog) return { catalog: { ...catalog, aliases: [...catalog.aliases] }, query: parts.slice(1).join(" ").trim(), pageNo };
3150
+ return { catalog: null, query: trimmed, pageNo };
3151
+ }
3152
+ catalogUsageText() {
3153
+ const catalogs = this.catalogsFromPayload(this.catalogsCache || {});
3154
+ const names = catalogs.slice(0, 24).map((item) => item.title || item.key).join("、");
3155
+ return [
3156
+ "Wiki 用法:",
3157
+ " 洛克wiki <类型> [关键词或ID]",
3158
+ " 洛克wiki <关键词或ID>",
3159
+ "示例:洛克wiki 水灵、洛克wiki 技能 圣光斩、洛克wiki 物品 xx球、洛克wiki 种植 食谱名",
3160
+ `后端目录:${names || "请稍后重试"}`
3161
+ ].join("\n");
3162
+ }
3163
+ buildCatalogRenderData(catalogsPayload, optionsPayload) {
3164
+ const catalogs = this.catalogsFromPayload(catalogsPayload);
3165
+ const topicCard = /* @__PURE__ */ __name((catalog) => {
3166
+ const backend = catalog._backend && typeof catalog._backend === "object" ? catalog._backend : {};
3167
+ const filters = Array.isArray(backend.filters) ? backend.filters : [];
3168
+ const filterText = filters.slice(0, 6).filter((item) => item && typeof item === "object").map((item) => String(item.label || item.key || "")).join("、");
3169
+ const title = String(catalog.title || backend.name || catalog.key || "Wiki");
3170
+ const key = String(catalog.key || "");
3171
+ const count = backend.count;
3172
+ let hint = `洛克wiki ${title} <关键词或ID>`;
3173
+ if (key) hint = `${hint} / 洛克wiki ${key} <关键词或ID>`;
3174
+ let coverage = String(backend.description || "").trim();
3175
+ if (filterText) coverage = `${coverage} 筛选:${filterText}`.trim();
3176
+ return {
3177
+ title,
3178
+ key,
3179
+ summary: count !== void 0 && count !== null && count !== "" ? `${hint} · 条目 ${count}` : hint,
3180
+ desc: key,
3181
+ children: coverage
3182
+ };
3183
+ }, "topicCard");
3184
+ const groups = [{
3185
+ title: "后端 Wiki 图鉴入口",
3186
+ desc: "",
3187
+ items: catalogs.map(topicCard),
3188
+ total: catalogs.length
3189
+ }];
3190
+ return {
3191
+ title: "洛克 Wiki",
3192
+ subtitle: "统一资料查询入口",
3193
+ summary: "",
3194
+ badges: [],
3195
+ facts: [],
3196
+ primary: [],
3197
+ groups,
3198
+ commandHint: "💡 示例:洛克wiki 水灵 | 洛克wiki 技能 圣光斩 | 洛克wiki 物品 国王球",
3199
+ copyright: COPYRIGHT
3200
+ };
3201
+ }
3202
+ globalSearchCatalogs(catalogsPayload) {
3203
+ const catalogs = [];
3204
+ const seen = /* @__PURE__ */ new Set();
3205
+ for (const catalog of this.catalogsFromPayload(catalogsPayload)) {
3206
+ const key = String(catalog.key || "");
3207
+ if (seen.has(key) || !catalog.list_path || catalog.search === false) continue;
3208
+ catalogs.push(catalog);
3209
+ seen.add(key);
3210
+ }
3211
+ return catalogs;
3212
+ }
3213
+ resultCommandExamples(items, catalog, limit = 3) {
3214
+ const examples = [];
3215
+ const seen = /* @__PURE__ */ new Set();
3216
+ for (const item of items) {
3217
+ if (!item || typeof item !== "object") continue;
3218
+ const child = this.catalogForKeyFromPayload(String(item._catalog_key || ""), this.catalogsCache || {}) || catalog || null;
3219
+ const category = String(item._catalog_title || child?.title || "").trim();
3220
+ const title = wikiTitleForItem(item, "").trim();
3221
+ if (!category || !title) continue;
3222
+ const command = `洛克wiki ${category} ${title}`;
3223
+ if (seen.has(command)) continue;
3224
+ examples.push(command);
3225
+ seen.add(command);
3226
+ if (examples.length >= limit) break;
3227
+ }
3228
+ return examples;
3229
+ }
3230
+ wikiPathParamsFromItem(catalog, item, rawText = "") {
3231
+ const tokens = String(rawText || "").split(/\s+/).filter(Boolean);
3232
+ const params = {};
3233
+ const idFields = catalog.id_fields || [];
3234
+ idFields.forEach((field, idx) => {
3235
+ let value = item?.[field];
3236
+ const empty = /* @__PURE__ */ __name((v) => v === void 0 || v === null || v === "", "empty");
3237
+ if (empty(value) && ["item_id", "asset_id", "action_id"].includes(field)) value = item?.id;
3238
+ if (empty(value) && ["ball_id", "plant_id", "carryon_id"].includes(field)) value = item?.item_id ?? item?.id;
3239
+ if (empty(value) && idx < tokens.length) value = tokens[idx];
3240
+ if (!empty(value)) params[field] = String(value);
3241
+ });
3242
+ return params;
3243
+ }
3244
+ fillDetailPath(catalog, params) {
3245
+ let path7 = String(catalog.detail_path || "");
3246
+ for (const field of catalog.id_fields || []) {
3247
+ const value = params[field];
3248
+ if (value === void 0 || value === null || value === "") return "";
3249
+ path7 = path7.replace(`{${field}}`, String(value));
3250
+ }
3251
+ return path7;
3252
+ }
3253
+ itemMatchesQueryExact(catalog, item, query) {
3254
+ const trimmed = String(query || "").trim();
3255
+ if (!trimmed) return false;
3256
+ const normalizedQuery = normalizeQueryText(trimmed);
3257
+ for (const field of catalog.id_fields || []) {
3258
+ let value = item?.[field];
3259
+ if ((value === void 0 || value === null || value === "") && ["ball_id", "plant_id", "carryon_id"].includes(field)) {
3260
+ value = item?.item_id ?? item?.id;
3261
+ }
3262
+ if (String(value ?? "") === trimmed) return true;
3263
+ }
3264
+ return normalizeQueryText(wikiTitleForItem(item, "")) === normalizedQuery;
3265
+ }
3266
+ findCatalogMatch(catalog, items, query, allowSingle = true) {
3267
+ const trimmed = String(query || "").trim();
3268
+ if (!trimmed) return null;
3269
+ for (const item of items) {
3270
+ if (this.itemMatchesQueryExact(catalog, item, trimmed)) return item;
3271
+ }
3272
+ if (allowSingle && items.length === 1) return items[0];
3273
+ return null;
3274
+ }
3275
+ async suggestCatalogItems(catalog, query, limit = 10) {
3276
+ if (!catalog || !catalog.list_path) return [];
3277
+ const trimmed = String(query || "").trim();
3278
+ if (!trimmed) return [];
3279
+ const terms = [];
3280
+ for (const term of [trimmed, trimmed.slice(0, 2), trimmed.slice(0, 1), trimmed.slice(0, -1)]) {
3281
+ const cleaned = String(term || "").trim();
3282
+ if (cleaned && !terms.includes(cleaned)) terms.push(cleaned);
3283
+ }
3284
+ const collected = /* @__PURE__ */ new Map();
3285
+ for (const term of terms.slice(0, 4)) {
3286
+ const res = await this.client.listWikiCatalogItems(
3287
+ this.ctx,
3288
+ catalog.list_path,
3289
+ term,
3290
+ 1,
3291
+ 30,
3292
+ Boolean(term && catalog.search !== false)
3293
+ );
3294
+ const items = res && typeof res === "object" ? res.items : [];
3295
+ for (const item of items || []) {
3296
+ if (!item || typeof item !== "object") continue;
3297
+ const title = wikiTitleForItem(item, "");
3298
+ const keyParts = [catalog.key || "", title];
3299
+ for (const field of catalog.id_fields || []) {
3300
+ if (item[field] !== void 0 && item[field] !== null && item[field] !== "") keyParts.push(String(item[field]));
3301
+ }
3302
+ const dedupeKey = keyParts.join("|");
3303
+ if (!collected.has(dedupeKey)) collected.set(dedupeKey, item);
3304
+ }
3305
+ if (collected.size >= limit * 3) break;
3306
+ }
3307
+ const scored = [];
3308
+ for (const item of collected.values()) {
3309
+ const score = wikiSimilarityScore(trimmed, item);
3310
+ if (score >= 0.18) scored.push([score, item]);
3311
+ }
3312
+ scored.sort((a, b) => b[0] - a[0]);
3313
+ if (!scored.length) return [...collected.values()].slice(0, limit);
3314
+ return scored.slice(0, limit).map(([, item]) => item);
3315
+ }
3316
+ buildGenericRenderData(catalog, payload, query, mode, pageNo = 1) {
3317
+ payload = payload || {};
3318
+ const fullCatalog = catalog;
3319
+ if (mode === "global-search") {
3320
+ const items = Array.isArray(payload?.items) ? payload.items : [];
3321
+ const cards = [];
3322
+ for (const item2 of items) {
3323
+ if (!item2 || typeof item2 !== "object") continue;
3324
+ const child = this.catalogForKeyFromPayload(String(item2._catalog_key || ""), this.catalogsCache || {}) || fullCatalog;
3325
+ const card = wikiCardForItem(item2, child?.title || "Wiki", child);
3326
+ if (item2._catalog_title) card.badges = [item2._catalog_title, ...card.badges];
3327
+ cards.push(card);
3328
+ }
3329
+ return {
3330
+ title: `全局搜索:${query}`,
3331
+ subtitle: "Wiki / 全局搜索",
3332
+ image: "",
3333
+ summary: "未指定分类时,会在所有可搜索的大分类接口中使用后端 q 参数查询。",
3334
+ badges: ["全局搜索", "可继续指定分类"],
3335
+ facts: [
3336
+ { label: "查询词", value: query || "-" },
3337
+ { label: "结果数", value: String(payload?.total ?? cards.length) },
3338
+ { label: "搜索接口", value: String(this.globalSearchCatalogs(this.catalogsCache || {}).length) }
3339
+ ],
3340
+ actionHint: "要查看具体条目,请按卡片绿色分类 tag 加名称继续查询。",
3341
+ actionExamples: this.resultCommandExamples(items, fullCatalog),
3342
+ cards,
3343
+ sections: [],
3344
+ commandHint: "💡 结果过多时可指定分类,例如 洛克wiki 技能 水花 或 洛克wiki 物品 国王球",
3345
+ copyright: COPYRIGHT
3346
+ };
3347
+ }
3348
+ if (mode === "suggestions") {
3349
+ const items = Array.isArray(payload?.items) ? payload.items : [];
3350
+ const cards = [];
3351
+ for (const item2 of items) {
3352
+ if (!item2 || typeof item2 !== "object") continue;
3353
+ const child = this.catalogForKeyFromPayload(String(item2._catalog_key || ""), this.catalogsCache || {}) || fullCatalog;
3354
+ const card = wikiCardForItem(item2, child?.title || fullCatalog.title, child);
3355
+ if (item2._catalog_title) card.badges = [item2._catalog_title, ...card.badges];
3356
+ cards.push(card);
3357
+ }
3358
+ return {
3359
+ title: `没有找到「${query}」`,
3360
+ subtitle: `Wiki / ${fullCatalog.title} / 联想结果`,
3361
+ summary: "接口没有返回精确结果,下面是按名称、摘要和相似度整理的候选。",
3362
+ image: "",
3363
+ badges: [fullCatalog.title, "联想"],
3364
+ facts: [
3365
+ { label: "查询词", value: query || "-" },
3366
+ { label: "候选数", value: String(cards.length) }
3367
+ ],
3368
+ actionHint: "接口没有精确命中时,可按候选卡片的绿色分类 tag 加名称继续查询。",
3369
+ actionExamples: this.resultCommandExamples(items, fullCatalog),
3370
+ cards,
3371
+ sections: [],
3372
+ commandHint: `💡 可使用 洛克wiki ${fullCatalog.title} <候选名称或ID> 继续查询`,
3373
+ copyright: COPYRIGHT
3374
+ };
3375
+ }
3376
+ if (mode === "list") {
3377
+ const items = Array.isArray(payload?.items) ? payload.items : [];
3378
+ const cards = items.filter((item2) => item2 && typeof item2 === "object").map((item2) => wikiCardForItem(item2, fullCatalog.title, fullCatalog));
3379
+ const catalogInfo = payload?.catalog && typeof payload.catalog === "object" ? payload.catalog : {};
3380
+ return {
3381
+ title: `${fullCatalog.title}列表`,
3382
+ subtitle: `Wiki / ${fullCatalog.title} / 第 ${pageNo} 页`,
3383
+ image: "",
3384
+ summary: catalogInfo.description || `共 ${payload?.total ?? cards.length} 条`,
3385
+ badges: [fullCatalog.title, "列表"],
3386
+ facts: [
3387
+ { label: "当前页", value: String(payload?.page_no ?? pageNo) },
3388
+ { label: "总页数", value: String(payload?.total_pages ?? "-") },
3389
+ { label: "总数", value: String(payload?.total ?? cards.length) }
3390
+ ],
3391
+ actionHint: query ? "搜索结果较多时,可按分类加名称继续查询具体条目。" : "",
3392
+ actionExamples: query ? this.resultCommandExamples(items, fullCatalog) : [],
3393
+ cards,
3394
+ sections: [],
3395
+ commandHint: "💡 洛克wiki <类型> <关键词或ID> | 洛克wiki 查看支持类型",
3396
+ copyright: COPYRIGHT
3397
+ };
3398
+ }
3399
+ const item = payload && typeof payload === "object" ? payload : {};
3400
+ return {
3401
+ title: wikiTitleForItem(item, fullCatalog.title),
3402
+ subtitle: `Wiki / ${fullCatalog.title}`,
3403
+ image: wikiPickImage(item),
3404
+ summary: wikiSummaryForItem(item),
3405
+ badges: [fullCatalog.title, ...wikiBadgesForItem(item)],
3406
+ facts: wikiMetaForItem(item, 16, String(fullCatalog.key || "")),
3407
+ cards: [],
3408
+ sections: wikiSectionsForPayload(item, fullCatalog),
3409
+ commandHint: "💡 洛克wiki <类型> <关键词或ID> | 洛克wiki 查看支持类型",
3410
+ copyright: COPYRIGHT
3411
+ };
3412
+ }
3413
+ mergeDetailPayloads(base, extra) {
3414
+ const merged = { ...base || {} };
3415
+ for (const [key, value] of Object.entries(extra || {})) {
3416
+ if (value === void 0 || value === null || value === "" || Array.isArray(value) && !value.length || typeof value === "object" && !Array.isArray(value) && !Object.keys(value).length) continue;
3417
+ const oldValue = merged[key];
3418
+ if (oldValue && typeof oldValue === "object" && !Array.isArray(oldValue) && value && typeof value === "object" && !Array.isArray(value)) {
3419
+ merged[key] = this.mergeDetailPayloads(oldValue, value);
3420
+ } else if (Array.isArray(oldValue) && Array.isArray(value)) {
3421
+ const seen = /* @__PURE__ */ new Set();
3422
+ const combined = [];
3423
+ for (const item of [...oldValue, ...value]) {
3424
+ const marker = typeof item === "object" ? JSON.stringify(item) : String(item);
3425
+ if (seen.has(marker)) continue;
3426
+ seen.add(marker);
3427
+ combined.push(item);
3428
+ }
3429
+ merged[key] = combined;
3430
+ } else {
3431
+ merged[key] = value;
3432
+ }
3433
+ }
3434
+ return merged;
3435
+ }
3436
+ itemDetailCompanionKeys(catalog, detail) {
3437
+ const catalogKey = String(catalog?.key || "");
3438
+ const itemId = detail?.item_id;
3439
+ if (itemId === void 0 || itemId === null || itemId === "") return [];
3440
+ if (["skill-stones", "skill-stone-recipes", "balls", "medals", "pet-carryons"].includes(catalogKey)) {
3441
+ return detail?.item_kind ? ["items"] : [];
3442
+ }
3443
+ if (catalogKey !== "items") return [];
3444
+ const flags = detail?.flags && typeof detail.flags === "object" ? detail.flags : {};
3445
+ const typeLabel = wikiGenericValue(detail?.type || detail?.label_type);
3446
+ if (flags.is_skill_stone || typeLabel === "技能石") return ["skill-stones"];
3447
+ if (flags.is_skill_stone_recipe || String(detail?.name || "").startsWith("配方-")) return ["skill-stone-recipes"];
3448
+ if (typeLabel === "咕噜球") return ["balls"];
3449
+ if (["奖章", "勋章"].includes(typeLabel)) return ["medals"];
3450
+ if (["携带物", "精灵携带物"].includes(typeLabel)) return ["pet-carryons"];
3451
+ return [];
3452
+ }
3453
+ async enrichItemDetail(catalog, detail) {
3454
+ if (!detail || typeof detail !== "object") return detail;
3455
+ const itemId = detail.item_id;
3456
+ if (itemId === void 0 || itemId === null || itemId === "") return detail;
3457
+ let merged = { ...detail };
3458
+ for (const key of this.itemDetailCompanionKeys(catalog, detail)) {
3459
+ const companion = this.catalogForKeyFromPayload(key, this.catalogsCache || {});
3460
+ if (!companion) continue;
3461
+ if (key === "items") {
3462
+ const itemKind = detail.item_kind;
3463
+ if (itemKind === void 0 || itemKind === null || itemKind === "") continue;
3464
+ const companionDetail2 = await this.client.getWikiPath(this.ctx, `/api/v1/games/rocom/wiki/items/${itemKind}/${itemId}`);
3465
+ if (companionDetail2) merged = this.mergeDetailPayloads(companionDetail2, merged);
3466
+ continue;
3467
+ }
3468
+ const companionDetail = await this.client.getWikiPath(this.ctx, `/api/v1/games/rocom/wiki/${key}/${itemId}`);
3469
+ if (companionDetail) merged = this.mergeDetailPayloads(merged, companionDetail);
3470
+ }
3471
+ return merged;
3472
+ }
3473
+ async resolveWikiPet(query) {
3474
+ const trimmed = String(query || "").trim();
3475
+ if (!trimmed) return { detail: null, candidates: [], error: "请输入精灵名称或 ID。用法:洛克wiki 精灵 <精灵名或ID>" };
3476
+ if (/^\d+$/.test(trimmed)) {
3477
+ const detail2 = await this.client.getWikiPet(this.ctx, trimmed);
3478
+ if (detail2) return { detail: detail2, candidates: [], error: "" };
3479
+ return { detail: null, candidates: [], error: `获取 Wiki 精灵详情失败:${this.client.getLastError()}` };
3480
+ }
3481
+ const searchRes = await this.client.listWikiPets(this.ctx, trimmed, 1, 10);
3482
+ const items = (searchRes || {}).items || [];
3483
+ if (!items.length) {
3484
+ const suggestions = await this.suggestCatalogItems(await this.getCatalogByKey("pets"), trimmed, 10);
3485
+ if (suggestions.length) return { detail: null, candidates: suggestions, error: "" };
3486
+ return { detail: null, candidates: [], error: `未找到「${trimmed}」的 Wiki 精灵资料:${this.client.getLastError("无匹配结果")}` };
3487
+ }
3488
+ let selected = findExactWikiMatch(items, trimmed);
3489
+ if (!selected && items.length === 1) selected = items[0];
3490
+ if (!selected) return { detail: null, candidates: items, error: "" };
3491
+ const detail = await this.client.getWikiPet(this.ctx, selected.pet_id);
3492
+ if (detail) return { detail, candidates: [], error: "" };
3493
+ return { detail: null, candidates: [], error: `获取 Wiki 精灵详情失败:${this.client.getLastError()}` };
3494
+ }
3495
+ async resolveWikiSkill(query) {
3496
+ const trimmed = String(query || "").trim();
3497
+ if (!trimmed) return { detail: null, candidates: [], error: "请输入技能名称或 ID。用法:洛克wiki 技能 <技能名或ID>" };
3498
+ if (/^\d+$/.test(trimmed)) {
3499
+ const detail2 = await this.client.getWikiSkill(this.ctx, trimmed);
3500
+ if (detail2) return { detail: detail2, candidates: [], error: "" };
3501
+ return { detail: null, candidates: [], error: `获取技能详情失败:${this.client.getLastError()}` };
3502
+ }
3503
+ const searchRes = await this.client.listWikiSkills(this.ctx, trimmed, 1, 10);
3504
+ const items = (searchRes || {}).items || [];
3505
+ if (!items.length) {
3506
+ const suggestions = await this.suggestCatalogItems(await this.getCatalogByKey("skills"), trimmed, 10);
3507
+ if (suggestions.length) return { detail: null, candidates: suggestions, error: "" };
3508
+ return { detail: null, candidates: [], error: `未找到「${trimmed}」的技能 Wiki 资料:${this.client.getLastError("无匹配结果")}。用法:洛克wiki 技能 <技能名或ID>` };
3509
+ }
3510
+ let selected = findExactWikiMatch(items, trimmed);
3511
+ if (!selected && items.length === 1) selected = items[0];
3512
+ if (!selected) return { detail: null, candidates: items, error: "" };
3513
+ const detail = await this.client.getWikiSkill(this.ctx, selected.skill_id);
3514
+ if (detail) return { detail, candidates: [], error: "" };
3515
+ return { detail: null, candidates: [], error: `获取技能详情失败:${this.client.getLastError()}` };
3516
+ }
3517
+ async fetchWikiPetSections(petId) {
3518
+ const [profile, skills, family, handbook] = await Promise.all([
3519
+ this.client.getWikiPetProfile(this.ctx, petId).catch(() => null),
3520
+ this.client.getWikiPetSkills(this.ctx, petId).catch(() => null),
3521
+ this.client.getWikiPetFamily(this.ctx, petId).catch(() => null),
3522
+ this.client.getWikiPetHandbook(this.ctx, petId).catch(() => null)
3523
+ ]);
3524
+ return {
3525
+ profile: profile && typeof profile === "object" ? profile : {},
3526
+ skills: skills && typeof skills === "object" ? skills : {},
3527
+ family: family && typeof family === "object" ? family : {},
3528
+ handbook: handbook && typeof handbook === "object" ? handbook : {}
3529
+ };
3530
+ }
3531
+ // 通过缓存查询技能详情(供家园详情技能补全使用)
3532
+ async getSkillDetailCached(skillId) {
3533
+ const key = String(skillId || "").trim();
3534
+ if (!key) return null;
3535
+ if (this.skillDetailCache.has(key)) return this.skillDetailCache.get(key);
3536
+ const detail = await this.client.getWikiSkill(this.ctx, key);
3537
+ if (detail && typeof detail === "object") {
3538
+ this.skillDetailCache.set(key, detail);
3539
+ return detail;
3540
+ }
3541
+ return null;
3542
+ }
3543
+ // 通过缓存查询精灵详情(供家园详情体重范围使用)
3544
+ async getPetDetailCached(petId) {
3545
+ const key = String(petId || "").trim();
3546
+ if (!key) return null;
3547
+ if (this.petDetailCache.has(key)) return this.petDetailCache.get(key);
3548
+ const detail = await this.client.getWikiPet(this.ctx, key);
3549
+ if (detail && typeof detail === "object") {
3550
+ this.petDetailCache.set(key, detail);
3551
+ return detail;
3552
+ }
3553
+ return null;
3554
+ }
3555
+ async fetchGenericCatalog(catalog, query, pageNo) {
3556
+ const trimmed = String(query || "").trim();
3557
+ const detailPath = String(catalog.detail_path || "");
3558
+ const listPath = String(catalog.list_path || "");
3559
+ if (detailPath && trimmed) {
3560
+ const directParams = this.wikiPathParamsFromItem(catalog, {}, trimmed);
3561
+ const idFields = catalog.id_fields || [];
3562
+ if (idFields.length === 1 && idFields[0] === "pet_id" && !/^\d+$/.test(trimmed)) {
3563
+ const { detail: pet } = await this.resolveWikiPet(trimmed);
3564
+ if (pet?.pet_id !== void 0 && pet?.pet_id !== null && pet?.pet_id !== "") directParams.pet_id = String(pet.pet_id);
3565
+ }
3566
+ if (idFields.length === 1 && idFields[0] === "skill_id" && !/^\d+$/.test(trimmed)) {
3567
+ const { detail: skill } = await this.resolveWikiSkill(trimmed);
3568
+ if (skill?.skill_id !== void 0 && skill?.skill_id !== null && skill?.skill_id !== "") directParams.skill_id = String(skill.skill_id);
3569
+ }
3570
+ const directPath = this.fillDetailPath(catalog, directParams);
3571
+ const typeFields = /* @__PURE__ */ new Set(["item_kind", "asset_type", "action_type"]);
3572
+ let directAllowed = Boolean(directPath) && Object.keys(directParams).length >= idFields.length;
3573
+ for (const field of idFields) {
3574
+ if (typeFields.has(field)) continue;
3575
+ if (!/^\d+$/.test(String(directParams[field] || ""))) {
3576
+ directAllowed = false;
3577
+ break;
3578
+ }
3579
+ }
3580
+ if (directAllowed) {
3581
+ const detail = await this.client.getWikiPath(this.ctx, directPath);
3582
+ if (detail) return { payload: await this.enrichItemDetail(catalog, detail), mode: "detail", error: "" };
3583
+ }
3584
+ }
3585
+ if (!listPath) {
3586
+ return { payload: null, mode: "detail", error: `${catalog.title} 需要提供 ID。用法:洛克wiki ${catalog.title} <ID>` };
3587
+ }
3588
+ const listRes = await this.client.listWikiCatalogItems(this.ctx, listPath, trimmed, pageNo, 12, catalog.search !== false);
3589
+ if (listRes === null) {
3590
+ return { payload: null, mode: "list", error: `获取 ${catalog.title} 失败:${this.client.getLastError()}` };
3591
+ }
3592
+ if (!listRes || typeof listRes !== "object" || !("items" in listRes)) {
3593
+ return { payload: listRes, mode: "detail", error: "" };
3594
+ }
3595
+ const items = listRes.items || [];
3596
+ if (trimmed && !items.length) {
3597
+ const suggestions = await this.suggestCatalogItems(catalog, trimmed, 10);
3598
+ if (suggestions.length) return { payload: { items: suggestions, query: trimmed }, mode: "suggestions", error: "" };
3599
+ }
3600
+ if (detailPath && trimmed && items.length) {
3601
+ const selected = this.findCatalogMatch(catalog, items, trimmed);
3602
+ if (selected) {
3603
+ const params = this.wikiPathParamsFromItem(catalog, selected, trimmed);
3604
+ const selectedPath = this.fillDetailPath(catalog, params);
3605
+ if (selectedPath) {
3606
+ const detail = await this.client.getWikiPath(this.ctx, selectedPath);
3607
+ if (detail) return { payload: await this.enrichItemDetail(catalog, detail), mode: "detail", error: "" };
3608
+ }
3609
+ }
3610
+ }
3611
+ return { payload: listRes, mode: "list", error: "" };
3612
+ }
3613
+ async fetchDetailForCatalogItem(catalog, item, query) {
3614
+ if (!catalog.detail_path) return null;
3615
+ const params = this.wikiPathParamsFromItem(catalog, item, query);
3616
+ const detailPath = this.fillDetailPath(catalog, params);
3617
+ if (!detailPath) return null;
3618
+ const detail = await this.client.getWikiPath(this.ctx, detailPath);
3619
+ return this.enrichItemDetail(catalog, detail);
3620
+ }
3621
+ async fetchGlobalSearch(query, pageNo) {
3622
+ const trimmed = String(query || "").trim();
3623
+ if (!trimmed) return { payload: null, mode: "global-search", error: "请输入 Wiki 关键词。" };
3624
+ const catalogsPayload = await this.getCatalogsPayload();
3625
+ const catalogs = this.globalSearchCatalogs(catalogsPayload);
3626
+ if (!catalogs.length) return { payload: null, mode: "global-search", error: "暂无可全局搜索的 Wiki 接口。" };
3627
+ const results = [];
3628
+ const concurrency = 8;
3629
+ let index = 0;
3630
+ const worker = /* @__PURE__ */ __name(async () => {
3631
+ while (index < catalogs.length) {
3632
+ const catalog = catalogs[index++];
3633
+ try {
3634
+ const res = await this.client.listWikiCatalogItems(this.ctx, catalog.list_path, trimmed, pageNo, 6, true);
3635
+ if (res === null) {
3636
+ results.push({ catalog, items: [], error: this.client.getLastError("") });
3637
+ } else {
3638
+ const items2 = res && typeof res === "object" ? res.items : [];
3639
+ results.push({ catalog, items: (items2 || []).filter((item) => item && typeof item === "object"), error: "" });
3640
+ }
3641
+ } catch (e) {
3642
+ results.push({ catalog, items: [], error: String(e) });
3643
+ }
3644
+ }
3645
+ }, "worker");
3646
+ await Promise.all(Array.from({ length: Math.min(concurrency, catalogs.length) }, worker));
3647
+ const collected = /* @__PURE__ */ new Map();
3648
+ const exactKeys = /* @__PURE__ */ new Set();
3649
+ const errors = [];
3650
+ for (const { catalog, items: items2, error } of results) {
3651
+ if (error) {
3652
+ errors.push(`${catalog.title || catalog.key}:${error}`);
3653
+ continue;
3654
+ }
3655
+ for (const item of items2) {
3656
+ const merged = { ...item, _catalog_key: catalog.key, _catalog_title: catalog.title };
3657
+ const title = wikiTitleForItem(item, "");
3658
+ const keyParts = [String(catalog.key || ""), title];
3659
+ for (const field of catalog.id_fields || []) {
3660
+ let value = item[field];
3661
+ if ((value === void 0 || value === null || value === "") && ["ball_id", "plant_id", "carryon_id"].includes(field)) {
3662
+ value = item.item_id ?? item.id;
3663
+ }
3664
+ if (value !== void 0 && value !== null && value !== "") keyParts.push(String(value));
3665
+ }
3666
+ const dedupeKey = keyParts.join("|");
3667
+ if (collected.has(dedupeKey)) continue;
3668
+ let score = wikiSimilarityScore(trimmed, merged);
3669
+ if (this.itemMatchesQueryExact(catalog, item, trimmed)) {
3670
+ score += 10 - wikiGlobalCatalogPriority(catalog) * 0.01;
3671
+ exactKeys.add(dedupeKey);
3672
+ }
3673
+ merged._match_score = score;
3674
+ collected.set(dedupeKey, merged);
3675
+ }
3676
+ }
3677
+ const items = [...collected.values()];
3678
+ items.sort((a, b) => (b._match_score || 0) - (a._match_score || 0));
3679
+ const exactItems = [...exactKeys].filter((key) => collected.has(key)).map((key) => collected.get(key));
3680
+ if (exactItems.length) {
3681
+ const priorityOf = /* @__PURE__ */ __name((item) => wikiGlobalCatalogPriority(
3682
+ this.catalogForKeyFromPayload(String(item._catalog_key || ""), catalogsPayload)
3683
+ ), "priorityOf");
3684
+ exactItems.sort((a, b) => priorityOf(a) - priorityOf(b) || (b._match_score || 0) - (a._match_score || 0));
3685
+ const bestPriority = priorityOf(exactItems[0]);
3686
+ const bestItems = exactItems.filter((item) => priorityOf(item) === bestPriority);
3687
+ if (bestItems.length === 1) {
3688
+ const exactItem = bestItems[0];
3689
+ const catalog = this.catalogForKeyFromPayload(String(exactItem._catalog_key || ""), catalogsPayload);
3690
+ if (catalog) {
3691
+ const detail = await this.fetchDetailForCatalogItem(catalog, exactItem, trimmed);
3692
+ if (detail) return { payload: { item: detail, _catalog: catalog }, mode: "global-detail", error: "" };
3693
+ }
3694
+ }
3695
+ }
3696
+ if (items.length) {
3697
+ return { payload: { items: items.slice(0, 12), query: trimmed, total: items.length, _global: true }, mode: "global-search", error: "" };
3698
+ }
3699
+ if (errors.length) return { payload: null, mode: "global-search", error: `全局搜索失败:${errors[0]}` };
3700
+ return { payload: { items: [], query: trimmed, total: 0, _global: true }, mode: "global-search", error: "" };
3701
+ }
3702
+ };
3703
+
3704
+ // src/atlas-service.ts
3705
+ var import_koishi6 = require("koishi");
3706
+ var import_node_child_process = require("node:child_process");
3707
+ var import_node_fs4 = __toESM(require("node:fs"));
3708
+ var import_node_path4 = __toESM(require("node:path"));
3709
+ var import_node_zlib = __toESM(require("node:zlib"));
3710
+ var logger6 = new import_koishi6.Logger("rocom-atlas");
3711
+ var DEFAULT_ATLAS_ZIP_URLS = [
3712
+ "https://codeload.github.com/Entropy-Increase-Team/Rocom-Atlas/zip/refs/heads/main",
3713
+ "https://github.com/Entropy-Increase-Team/Rocom-Atlas/archive/refs/heads/main.zip"
3714
+ ];
3715
+ var DEFAULT_ATLAS_GIT_URL = "https://github.com/Entropy-Increase-Team/Rocom-Atlas.git";
3716
+ function extractZip(buffer, destDir) {
3717
+ let eocd = -1;
3718
+ const minEocd = Math.max(0, buffer.length - 65557);
3719
+ for (let i = buffer.length - 22; i >= minEocd; i--) {
3720
+ if (buffer.readUInt32LE(i) === 101010256) {
3721
+ eocd = i;
3722
+ break;
3723
+ }
3724
+ }
3725
+ if (eocd < 0) throw new Error("ZIP 结构异常:找不到中央目录结尾");
3726
+ const entryCount = buffer.readUInt16LE(eocd + 10);
3727
+ let offset = buffer.readUInt32LE(eocd + 16);
3728
+ for (let i = 0; i < entryCount; i++) {
3729
+ if (buffer.readUInt32LE(offset) !== 33639248) throw new Error("ZIP 结构异常:中央目录签名错误");
3730
+ const compressionMethod = buffer.readUInt16LE(offset + 10);
3731
+ const compressedSize = buffer.readUInt32LE(offset + 20);
3732
+ const nameLength = buffer.readUInt16LE(offset + 28);
3733
+ const extraLength = buffer.readUInt16LE(offset + 30);
3734
+ const commentLength = buffer.readUInt16LE(offset + 32);
3735
+ const localHeaderOffset = buffer.readUInt32LE(offset + 42);
3736
+ const name2 = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
3737
+ offset += 46 + nameLength + extraLength + commentLength;
3738
+ const normalized = name2.replace(/\\/g, "/");
3739
+ if (normalized.includes("..")) continue;
3740
+ const target = import_node_path4.default.join(destDir, ...normalized.split("/"));
3741
+ if (!import_node_path4.default.resolve(target).startsWith(import_node_path4.default.resolve(destDir))) continue;
3742
+ if (normalized.endsWith("/")) {
3743
+ import_node_fs4.default.mkdirSync(target, { recursive: true });
3744
+ continue;
3745
+ }
3746
+ if (buffer.readUInt32LE(localHeaderOffset) !== 67324752) throw new Error("ZIP 结构异常:本地文件头签名错误");
3747
+ const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
3748
+ const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
3749
+ const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength;
3750
+ const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
3751
+ let content;
3752
+ if (compressionMethod === 0) content = Buffer.from(compressed);
3753
+ else if (compressionMethod === 8) content = import_node_zlib.default.inflateRawSync(compressed);
3754
+ else throw new Error(`ZIP 不支持的压缩方式: ${compressionMethod}`);
3755
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
3756
+ import_node_fs4.default.writeFileSync(target, content);
3757
+ }
3758
+ }
3759
+ __name(extractZip, "extractZip");
3760
+ function gitClone(dst, gitUrl) {
3761
+ return new Promise((resolve, reject) => {
3762
+ const child = (0, import_node_child_process.spawn)(
3763
+ "git",
3764
+ ["clone", "--depth", "1", "--single-branch", "--progress", gitUrl, dst],
3765
+ { env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }
3766
+ );
3767
+ const stderrLines = [];
3768
+ let stderrTail = "";
3769
+ child.stderr.on("data", (chunk) => {
3770
+ stderrTail += chunk.toString("utf8");
3771
+ const lines = stderrTail.split(/[\r\n]+/);
3772
+ stderrTail = lines.pop() || "";
3773
+ for (const line of lines) {
3774
+ const text = line.trim();
3775
+ if (!text) continue;
3776
+ stderrLines.push(text);
3777
+ logger6.info(`git clone: ${text}`);
3778
+ }
3779
+ });
3780
+ const timer = setTimeout(() => {
3781
+ child.kill();
3782
+ reject(new Error("git clone Rocom-Atlas 超时(180 秒)"));
3783
+ }, 18e4);
3784
+ child.on("error", (error) => {
3785
+ clearTimeout(timer);
3786
+ reject(new Error(`git clone Rocom-Atlas 失败:${error.message}`));
3787
+ });
3788
+ child.on("close", (code) => {
3789
+ clearTimeout(timer);
3790
+ if (code === 0) resolve();
3791
+ else reject(new Error(`git clone Rocom-Atlas 失败(exit ${code}):${stderrLines.slice(-3).join(" | ")}`));
3792
+ });
3793
+ });
3794
+ }
3795
+ __name(gitClone, "gitClone");
3796
+ var AtlasService = class {
3797
+ constructor(dataDir, options = {}) {
3798
+ this.dataDir = dataDir;
3799
+ this.zipUrls = options.zipUrls?.length ? options.zipUrls : DEFAULT_ATLAS_ZIP_URLS;
3800
+ this.gitUrl = options.gitUrl || DEFAULT_ATLAS_GIT_URL;
3801
+ }
3802
+ static {
3803
+ __name(this, "AtlasService");
3804
+ }
3805
+ zipUrls;
3806
+ gitUrl;
3807
+ get atlasDir() {
3808
+ return import_node_path4.default.join(this.dataDir, "rocom_atlas");
3809
+ }
3810
+ indexPath() {
3811
+ return import_node_path4.default.join(this.atlasDir, "path.json");
3812
+ }
3813
+ petsDir() {
3814
+ return import_node_path4.default.join(this.atlasDir, "pets");
3815
+ }
3816
+ aliasPath() {
3817
+ return import_node_path4.default.join(this.atlasDir, "othername", "pets.yaml");
3818
+ }
3819
+ isReady() {
3820
+ return import_node_fs4.default.existsSync(this.indexPath()) && import_node_fs4.default.existsSync(this.petsDir());
3821
+ }
3822
+ loadIndex() {
3823
+ try {
3824
+ const payload = JSON.parse(import_node_fs4.default.readFileSync(this.indexPath(), "utf-8"));
3825
+ const pets = payload?.pets;
3826
+ return pets && typeof pets === "object" ? pets : {};
3827
+ } catch (e) {
3828
+ logger6.warn(`读取本地图鉴索引失败: ${e}`);
3829
+ return {};
3830
+ }
3831
+ }
3832
+ stripYamlValue(value) {
3833
+ const text = String(value || "").trim();
3834
+ if (text.length >= 2 && text[0] === text[text.length - 1] && ["'", '"'].includes(text[0])) {
3835
+ return text.slice(1, -1).trim();
3836
+ }
3837
+ return text;
3838
+ }
3839
+ // 解析上游简单结构的 pets.yaml:`正式名:` + 缩进 `- 别名` 列表。
3840
+ loadPetAliases() {
3841
+ const filePath = this.aliasPath();
3842
+ if (!import_node_fs4.default.existsSync(filePath)) return {};
3843
+ const aliases = {};
3844
+ let currentName = "";
3845
+ try {
3846
+ const lines = import_node_fs4.default.readFileSync(filePath, "utf-8").split(/\r?\n/);
3847
+ for (const line of lines) {
3848
+ const stripped = line.trim();
3849
+ if (!stripped || stripped.startsWith("#")) continue;
3850
+ if (!/^\s/.test(line) && stripped.endsWith(":")) {
3851
+ currentName = this.stripYamlValue(stripped.slice(0, -1));
3852
+ if (currentName) {
3853
+ const key = normalizeQueryText(currentName);
3854
+ aliases[key] = aliases[key] || [];
3855
+ if (!aliases[key].includes(currentName)) aliases[key].push(currentName);
3856
+ }
3857
+ continue;
3858
+ }
3859
+ if (currentName && stripped.startsWith("-")) {
3860
+ const alias = this.stripYamlValue(stripped.slice(1));
3861
+ if (!alias) continue;
3862
+ const key = normalizeQueryText(alias);
3863
+ aliases[key] = aliases[key] || [];
3864
+ if (!aliases[key].includes(currentName)) aliases[key].push(currentName);
3865
+ }
3866
+ }
3867
+ } catch (e) {
3868
+ logger6.warn(`读取本地图鉴别名失败: ${e}`);
3869
+ return {};
3870
+ }
3871
+ return aliases;
3872
+ }
3873
+ localImagePath(atlasRelPath) {
3874
+ const rel = String(atlasRelPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
3875
+ if (!rel) return "";
3876
+ const candidate = import_node_path4.default.resolve(this.atlasDir, ...rel.split("/"));
3877
+ const root = import_node_path4.default.resolve(this.atlasDir);
3878
+ if (!candidate.startsWith(root)) return "";
3879
+ return candidate;
3880
+ }
3881
+ existingImagePath(index, name2) {
3882
+ const filePath = this.localImagePath(index[name2] || "");
3883
+ return filePath && import_node_fs4.default.existsSync(filePath) ? filePath : "";
3884
+ }
3885
+ findMatch(query) {
3886
+ const trimmed = String(query || "").trim();
3887
+ if (!trimmed) return { name: "", imagePath: "", candidates: [] };
3888
+ if (/^\d+$/.test(trimmed)) {
3889
+ const filePath = import_node_path4.default.join(this.petsDir(), `${trimmed}.png`);
3890
+ if (import_node_fs4.default.existsSync(filePath)) return { name: `#${trimmed}`, imagePath: filePath, candidates: [] };
3891
+ }
3892
+ const index = this.loadIndex();
3893
+ if (!Object.keys(index).length) return { name: "", imagePath: "", candidates: [] };
3894
+ const normalizedQuery = normalizeQueryText(trimmed);
3895
+ let exactKey = "";
3896
+ for (const name2 of Object.keys(index)) {
3897
+ if (normalizeQueryText(name2) === normalizedQuery) {
3898
+ exactKey = name2;
3899
+ break;
3900
+ }
3901
+ }
3902
+ if (exactKey) {
3903
+ const filePath = this.existingImagePath(index, exactKey);
3904
+ if (filePath) return { name: exactKey, imagePath: filePath, candidates: [] };
3905
+ }
3906
+ const aliasMap = this.loadPetAliases();
3907
+ const exactAliasCandidates = (aliasMap[normalizedQuery] || []).filter((name2) => this.existingImagePath(index, name2));
3908
+ if (exactAliasCandidates.length) {
3909
+ const only = exactAliasCandidates[0];
3910
+ return { name: only, imagePath: this.existingImagePath(index, only), candidates: [] };
3911
+ }
3912
+ const candidates = [];
3913
+ const addCandidate = /* @__PURE__ */ __name((name2) => {
3914
+ if (!candidates.includes(name2) && this.existingImagePath(index, name2)) candidates.push(name2);
3915
+ }, "addCandidate");
3916
+ for (const name2 of Object.keys(index)) {
3917
+ const normalizedName = normalizeQueryText(name2);
3918
+ if (normalizedQuery && (normalizedName.includes(normalizedQuery) || normalizedQuery.includes(normalizedName))) {
3919
+ addCandidate(name2);
3920
+ }
3921
+ }
3922
+ for (const [alias, names] of Object.entries(aliasMap)) {
3923
+ if (normalizedQuery && (alias.includes(normalizedQuery) || normalizedQuery.includes(alias))) {
3924
+ for (const name2 of names) addCandidate(name2);
3925
+ }
3926
+ }
3927
+ if (candidates.length === 1) {
3928
+ const only = candidates[0];
3929
+ return { name: only, imagePath: this.existingImagePath(index, only), candidates: [] };
3930
+ }
3931
+ return { name: "", imagePath: "", candidates: candidates.slice(0, 10) };
3932
+ }
3933
+ prepareAtlasDir(sourceRoot, preparedDir) {
3934
+ import_node_fs4.default.mkdirSync(preparedDir, { recursive: true });
3935
+ for (const name2 of ["path.json", "index", "othername", "pets"]) {
3936
+ const src = import_node_path4.default.join(sourceRoot, name2);
3937
+ const dst = import_node_path4.default.join(preparedDir, name2);
3938
+ if (import_node_fs4.default.existsSync(src)) {
3939
+ import_node_fs4.default.cpSync(src, dst, { recursive: true });
3940
+ }
3941
+ }
3942
+ if (!import_node_fs4.default.existsSync(import_node_path4.default.join(preparedDir, "path.json"))) throw new Error("Atlas 缺少 path.json");
3943
+ if (!import_node_fs4.default.existsSync(import_node_path4.default.join(preparedDir, "othername", "pets.yaml"))) throw new Error("Atlas 缺少 othername/pets.yaml");
3944
+ const petsDir = import_node_path4.default.join(preparedDir, "pets");
3945
+ if (!import_node_fs4.default.existsSync(petsDir)) throw new Error("Atlas 缺少 pets 图片目录");
3946
+ const imageCount = import_node_fs4.default.readdirSync(petsDir).filter((name2) => name2.toLowerCase().endsWith(".png")).length;
3947
+ let totalBytes = 0;
3948
+ const walk = /* @__PURE__ */ __name((dir) => {
3949
+ for (const entry of import_node_fs4.default.readdirSync(dir, { withFileTypes: true })) {
3950
+ const entryPath = import_node_path4.default.join(dir, entry.name);
3951
+ if (entry.isDirectory()) walk(entryPath);
3952
+ else totalBytes += import_node_fs4.default.statSync(entryPath).size;
3953
+ }
3954
+ }, "walk");
3955
+ walk(preparedDir);
3956
+ return { imageCount, totalBytes };
3957
+ }
3958
+ replaceAtlasDir(preparedDir) {
3959
+ const target = import_node_path4.default.resolve(this.atlasDir);
3960
+ const dataRoot = import_node_path4.default.resolve(this.dataDir);
3961
+ if (!target.startsWith(dataRoot)) throw new Error("图鉴目标目录不在插件数据目录内");
3962
+ if (import_node_fs4.default.existsSync(target)) import_node_fs4.default.rmSync(target, { recursive: true, force: true });
3963
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
3964
+ try {
3965
+ import_node_fs4.default.renameSync(preparedDir, target);
3966
+ } catch (e) {
3967
+ if (["EXDEV", "EPERM"].includes(e?.code)) {
3968
+ logger6.info(`rename 失败(${e.code}),回退为复制方式迁移图鉴目录`);
3969
+ import_node_fs4.default.cpSync(preparedDir, target, { recursive: true });
3970
+ } else {
3971
+ throw e;
3972
+ }
3973
+ }
3974
+ }
3975
+ // 流式下载压缩包:每 2 秒输出一次已下载字节数/速度,避免大文件下载期间毫无动静。
3976
+ async downloadZipWithProgress(ctx, url) {
3977
+ const started = Date.now();
3978
+ const res = await ctx.http(url, { responseType: "stream", timeout: 18e4 });
3979
+ const totalHeader = Number(res.headers?.get?.("content-length"));
3980
+ const totalBytes = Number.isFinite(totalHeader) && totalHeader > 0 ? totalHeader : 0;
3981
+ const totalText = totalBytes ? `${(totalBytes / 1024 / 1024).toFixed(1)} MB` : "未知大小";
3982
+ logger6.info(`开始下载图鉴压缩包:${url}(${totalText})`);
3983
+ const chunks = [];
3984
+ let received = 0;
3985
+ let lastLogAt = Date.now();
3986
+ const reader = res.data.getReader();
3987
+ while (true) {
3988
+ const { done, value } = await reader.read();
3989
+ if (done) break;
3990
+ if (value) {
3991
+ chunks.push(Buffer.from(value));
3992
+ received += value.byteLength;
3993
+ }
3994
+ const now = Date.now();
3995
+ if (now - lastLogAt >= 2e3) {
3996
+ lastLogAt = now;
3997
+ const mb = (received / 1024 / 1024).toFixed(1);
3998
+ const speed = (received / 1024 / (now - started) * 1e3 / 1024).toFixed(2);
3999
+ const percentText = totalBytes ? `${Math.min(99, Math.round(received / totalBytes * 100))}%,` : "";
4000
+ logger6.info(`图鉴压缩包下载中:${percentText}已收到 ${mb} MB,平均 ${speed} MB/s`);
4001
+ }
4002
+ }
4003
+ const elapsed = ((Date.now() - started) / 1e3).toFixed(1);
4004
+ logger6.info(`图鉴压缩包下载完成:共 ${(received / 1024 / 1024).toFixed(1)} MB,耗时 ${elapsed} 秒`);
4005
+ return Buffer.concat(chunks);
4006
+ }
4007
+ async download(ctx, progressCb) {
4008
+ const emit = /* @__PURE__ */ __name(async (percent, stage) => {
4009
+ if (progressCb) await progressCb(Math.max(0, Math.min(100, Math.round(percent))), stage);
4010
+ }, "emit");
4011
+ import_node_fs4.default.mkdirSync(this.dataDir, { recursive: true });
4012
+ for (const entry of import_node_fs4.default.readdirSync(this.dataDir, { withFileTypes: true })) {
4013
+ if (entry.isDirectory() && entry.name.startsWith(".atlas_tmp_")) {
4014
+ cleanupTempDir(import_node_path4.default.join(this.dataDir, entry.name));
4015
+ }
4016
+ }
4017
+ const tmpRoot = import_node_fs4.default.mkdtempSync(import_node_path4.default.join(this.dataDir, ".atlas_tmp_"));
4018
+ const extractDir = import_node_path4.default.join(tmpRoot, "extract");
4019
+ let preparedDir = import_node_path4.default.join(tmpRoot, "rocom_atlas");
4020
+ const cloneDir = import_node_path4.default.join(tmpRoot, "clone");
4021
+ try {
4022
+ let zipBuffer = null;
4023
+ const zipErrors = [];
4024
+ for (const url of this.zipUrls) {
4025
+ try {
4026
+ await emit(10, "正在下载图鉴压缩包");
4027
+ zipBuffer = await this.downloadZipWithProgress(ctx, url);
4028
+ await emit(80, "图鉴压缩包下载完成");
4029
+ break;
4030
+ } catch (e) {
4031
+ logger6.warn(`图鉴压缩包下载失败:${url}: ${e}`);
4032
+ zipErrors.push(`${url}: ${e}`);
4033
+ zipBuffer = null;
1823
4034
  }
1824
- import_node_fs3.default.rmSync(tempDir, { recursive: true, force: true });
1825
4035
  }
1826
- } catch (e) {
1827
- logger4.error(`render failed: ${e}`);
1828
- return null;
4036
+ let stats;
4037
+ if (zipBuffer) {
4038
+ await emit(80, "正在解压图鉴文件");
4039
+ logger6.info("正在解压图鉴压缩包…");
4040
+ import_node_fs4.default.mkdirSync(extractDir, { recursive: true });
4041
+ extractZip(zipBuffer, extractDir);
4042
+ logger6.info("图鉴压缩包解压完成");
4043
+ const roots = import_node_fs4.default.readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => import_node_path4.default.join(extractDir, entry.name));
4044
+ if (!roots.length) throw new Error("Atlas 压缩包结构异常");
4045
+ stats = this.prepareAtlasDir(roots[0], preparedDir);
4046
+ } else {
4047
+ logger6.warn(`GitHub zip 下载失败,尝试 git clone:${zipErrors.join(" | ")}`);
4048
+ await emit(20, "正在 git clone 图鉴仓库");
4049
+ await gitClone(cloneDir, this.gitUrl);
4050
+ await emit(80, "正在整理图鉴文件");
4051
+ stats = this.prepareAtlasDir(cloneDir, preparedDir);
4052
+ }
4053
+ this.replaceAtlasDir(preparedDir);
4054
+ preparedDir = "";
4055
+ logger6.info(`本地图鉴缓存已更新:${stats.imageCount} 张图片,${(stats.totalBytes / 1024 / 1024).toFixed(1)} MB`);
4056
+ await emit(100, "本地图鉴缓存已更新");
4057
+ return stats;
4058
+ } finally {
4059
+ cleanupTempDir(tmpRoot);
1829
4060
  }
1830
4061
  }
1831
4062
  };
4063
+ function cleanupTempDir(tmpRoot) {
4064
+ try {
4065
+ import_node_fs4.default.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
4066
+ } catch {
4067
+ try {
4068
+ const clearReadonly = /* @__PURE__ */ __name((dir) => {
4069
+ for (const entry of import_node_fs4.default.readdirSync(dir, { withFileTypes: true })) {
4070
+ const entryPath = import_node_path4.default.join(dir, entry.name);
4071
+ try {
4072
+ import_node_fs4.default.chmodSync(entryPath, 438);
4073
+ } catch {
4074
+ }
4075
+ if (entry.isDirectory()) clearReadonly(entryPath);
4076
+ }
4077
+ }, "clearReadonly");
4078
+ clearReadonly(tmpRoot);
4079
+ import_node_fs4.default.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
4080
+ } catch (e) {
4081
+ logger6.warn(`清理图鉴临时目录失败(不影响下载结果):${tmpRoot}: ${e}`);
4082
+ }
4083
+ }
4084
+ }
4085
+ __name(cleanupTempDir, "cleanupTempDir");
1832
4086
 
1833
4087
  // src/role-token.ts
1834
4088
  function normalize(value) {
@@ -1964,12 +4218,12 @@ async function migrateLegacyFrameworkTokens(ctx, userMgr) {
1964
4218
  __name(migrateLegacyFrameworkTokens, "migrateLegacyFrameworkTokens");
1965
4219
 
1966
4220
  // src/commands/account.ts
1967
- var import_koishi6 = require("koishi");
4221
+ var import_koishi8 = require("koishi");
1968
4222
 
1969
4223
  // src/send-image.ts
1970
- var import_koishi5 = require("koishi");
1971
- var import_node_zlib = __toESM(require("node:zlib"));
1972
- var logger5 = new import_koishi5.Logger("rocom-send");
4224
+ var import_koishi7 = require("koishi");
4225
+ var import_node_zlib2 = __toESM(require("node:zlib"));
4226
+ var logger7 = new import_koishi7.Logger("rocom-send");
1973
4227
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
1974
4228
  var JPEG_SIGNATURE = Buffer.from([255, 216, 255]);
1975
4229
  function formatSessionContext(session) {
@@ -2044,9 +4298,9 @@ function compressPngImage(image, config) {
2044
4298
  if (type === "IEND") break;
2045
4299
  }
2046
4300
  if (!idatChunks.length) return image;
2047
- const rawImageData = import_node_zlib.default.inflateSync(Buffer.concat(idatChunks));
4301
+ const rawImageData = import_node_zlib2.default.inflateSync(Buffer.concat(idatChunks));
2048
4302
  const level = Math.max(0, Math.min(9, Math.floor(config.imageCompressionLevel ?? 9)));
2049
- const compressedIdat = import_node_zlib.default.deflateSync(rawImageData, { level });
4303
+ const compressedIdat = import_node_zlib2.default.deflateSync(rawImageData, { level });
2050
4304
  const rebuiltChunks = [];
2051
4305
  let wroteIdat = false;
2052
4306
  for (const chunk of chunks) {
@@ -2062,7 +4316,7 @@ function compressPngImage(image, config) {
2062
4316
  const compressed = Buffer.concat([PNG_SIGNATURE, ...rebuiltChunks]);
2063
4317
  if (compressed.length < image.length) return compressed;
2064
4318
  } catch (err) {
2065
- logger5.warn(`PNG compression failed, sending original image: ${err}`);
4319
+ logger7.warn(`PNG compression failed, sending original image: ${err}`);
2066
4320
  }
2067
4321
  return image;
2068
4322
  }
@@ -2070,36 +4324,36 @@ __name(compressPngImage, "compressPngImage");
2070
4324
  async function sendImageWithFallback(session, image, fallbackText, scene, compressionConfig) {
2071
4325
  const ctxInfo = formatSessionContext(session);
2072
4326
  if (!image) {
2073
- logger5.warn(`[${scene}] render returned empty image, fallback to text | ${ctxInfo}`);
4327
+ logger7.warn(`[${scene}] render returned empty image, fallback to text | ${ctxInfo}`);
2074
4328
  try {
2075
4329
  await session.send(fallbackText);
2076
4330
  } catch (e) {
2077
- logger5.error(`[${scene}] text fallback send failed | ${ctxInfo} | ${e}`);
4331
+ logger7.error(`[${scene}] text fallback send failed | ${ctxInfo} | ${e}`);
2078
4332
  }
2079
4333
  return;
2080
4334
  }
2081
4335
  const outputImage = compressionConfig ? compressPngImage(image, compressionConfig) : image;
2082
4336
  if (outputImage.length < image.length) {
2083
- logger5.info(`[${scene}] compressed image ${image.length}B -> ${outputImage.length}B | ${ctxInfo}`);
4337
+ logger7.info(`[${scene}] compressed image ${image.length}B -> ${outputImage.length}B | ${ctxInfo}`);
2084
4338
  }
2085
4339
  try {
2086
- const result = await session.send(import_koishi5.h.image(outputImage, detectImageMime(outputImage)));
4340
+ const result = await session.send(import_koishi7.h.image(outputImage, detectImageMime(outputImage)));
2087
4341
  if (!hasSendResult(result)) {
2088
- logger5.warn(`[${scene}] image send returned empty result | size=${outputImage.length}B | ${ctxInfo}`);
4342
+ logger7.warn(`[${scene}] image send returned empty result | size=${outputImage.length}B | ${ctxInfo}`);
2089
4343
  }
2090
4344
  } catch (e) {
2091
- logger5.error(`[${scene}] image send failed | size=${outputImage.length}B | ${ctxInfo} | ${e}`);
4345
+ logger7.error(`[${scene}] image send failed | size=${outputImage.length}B | ${ctxInfo} | ${e}`);
2092
4346
  try {
2093
4347
  await session.send(fallbackText);
2094
4348
  } catch (fallbackErr) {
2095
- logger5.error(`[${scene}] text fallback send failed after image send error | ${ctxInfo} | ${fallbackErr}`);
4349
+ logger7.error(`[${scene}] text fallback send failed after image send error | ${ctxInfo} | ${fallbackErr}`);
2096
4350
  }
2097
4351
  }
2098
4352
  }
2099
4353
  __name(sendImageWithFallback, "sendImageWithFallback");
2100
4354
 
2101
4355
  // src/commands/account.ts
2102
- var logger6 = new import_koishi6.Logger("rocom-account");
4356
+ var logger8 = new import_koishi8.Logger("rocom-account");
2103
4357
  async function recallMessages(session, messageIds) {
2104
4358
  if (!session?.bot?.deleteMessage || !session.channelId) return;
2105
4359
  for (const id of messageIds) {
@@ -2107,7 +4361,7 @@ async function recallMessages(session, messageIds) {
2107
4361
  try {
2108
4362
  await session.bot.deleteMessage(session.channelId, id);
2109
4363
  } catch (err) {
2110
- logger6.warn(`撤回消息 ${id} 失败:${err}`);
4364
+ logger8.warn(`撤回消息 ${id} 失败:${err}`);
2111
4365
  }
2112
4366
  }
2113
4367
  }
@@ -2187,19 +4441,19 @@ async function saveBindingWithRoleInfo(deps, session, fwToken, loginType, userId
2187
4441
  __name(saveBindingWithRoleInfo, "saveBindingWithRoleInfo");
2188
4442
  function register(deps) {
2189
4443
  const { ctx, client, userMgr } = deps;
2190
- ctx.command("洛克").subcommand(".QQ登录", "QQ 扫码登录").alias("洛克QQ登录").action(async ({ session }) => {
4444
+ ctx.command("洛克").subcommand(".QQ登录", "QQ 扫码登录").alias("洛克QQ登录").alias("洛克qq登录").action(async ({ session }) => {
2191
4445
  const userId = session.userId;
2192
4446
  const qrData = await client.qqQrLogin(ctx, userId);
2193
4447
  if (!qrData?.qr_image) return "获取 QQ 二维码失败。";
2194
4448
  const fwToken = qrData.frameworkToken;
2195
4449
  const qrB64 = qrData.qr_image;
2196
4450
  const imgData = qrB64.includes(",") ? qrB64.split(",")[1] : qrB64;
2197
- const qrSendResult = await session.send((0, import_koishi6.h)(
4451
+ const qrSendResult = await session.send((0, import_koishi8.h)(
2198
4452
  "message",
2199
4453
  {},
2200
- import_koishi6.h.at(userId),
2201
- import_koishi6.h.text("\n请使用 QQ 扫描二维码登录(有效时间 2 分钟)\n注意需要双设备扫码。\n"),
2202
- import_koishi6.h.image(`data:image/png;base64,${imgData}`)
4454
+ import_koishi8.h.at(userId),
4455
+ import_koishi8.h.text("\n请使用 QQ 扫描二维码登录(有效时间 2 分钟)\n注意需要双设备扫码。\n"),
4456
+ import_koishi8.h.image(`data:image/png;base64,${imgData}`)
2203
4457
  ));
2204
4458
  const qrMessageIds = Array.isArray(qrSendResult) ? qrSendResult.filter(Boolean).map(String) : qrSendResult ? [String(qrSendResult)] : [];
2205
4459
  const startTime = Date.now();
@@ -2386,11 +4640,11 @@ ${qrUrl}`);
2386
4640
  __name(register, "register");
2387
4641
 
2388
4642
  // src/commands/query.ts
2389
- var import_koishi8 = require("koishi");
4643
+ var import_koishi10 = require("koishi");
2390
4644
 
2391
4645
  // src/subscription-send.ts
2392
- var import_koishi7 = require("koishi");
2393
- var logger7 = new import_koishi7.Logger("rocom-subscription-send");
4646
+ var import_koishi9 = require("koishi");
4647
+ var logger9 = new import_koishi9.Logger("rocom-subscription-send");
2394
4648
  function findBot(ctx, platform = "") {
2395
4649
  if (!ctx.bots?.length) return null;
2396
4650
  if (!platform) return ctx.bots[0];
@@ -2403,7 +4657,7 @@ async function sendScheduledMessage(ctx, target, message) {
2403
4657
  const userId = target.userId || "";
2404
4658
  const bot = findBot(ctx, platform);
2405
4659
  if (!bot) {
2406
- logger7.warn("no available bot, skip scheduled push");
4660
+ logger9.warn("no available bot, skip scheduled push");
2407
4661
  return false;
2408
4662
  }
2409
4663
  try {
@@ -2426,30 +4680,458 @@ async function sendScheduledMessage(ctx, target, message) {
2426
4680
  return true;
2427
4681
  }
2428
4682
  } catch (err) {
2429
- logger7.warn(`scheduled push failed: ${err}`);
4683
+ logger9.warn(`scheduled push failed: ${err}`);
2430
4684
  return false;
2431
4685
  }
2432
- logger7.warn(`scheduled push target is incomplete: ${JSON.stringify(target)}`);
4686
+ logger9.warn(`scheduled push target is incomplete: ${JSON.stringify(target)}`);
2433
4687
  return false;
2434
4688
  }
2435
4689
  __name(sendScheduledMessage, "sendScheduledMessage");
2436
4690
  async function sendScheduledImageWithFallback(ctx, target, image, fallbackText, mentionAll = false) {
2437
- const prefix = mentionAll ? "@全体\n" : "";
4691
+ const buildContent = /* @__PURE__ */ __name((body) => mentionAll ? (0, import_koishi9.h)("message", {}, (0, import_koishi9.h)("at", { type: "all" }), import_koishi9.h.text("\n"), body) : body, "buildContent");
2438
4692
  if (!image) {
2439
- return sendScheduledMessage(ctx, target, `${prefix}${fallbackText}`);
4693
+ return sendScheduledMessage(ctx, target, buildContent(import_koishi9.h.text(fallbackText)));
2440
4694
  }
2441
- const imageSegment = import_koishi7.h.image(image, detectImageMime(image));
2442
- const content = mentionAll ? `${prefix}${imageSegment}` : imageSegment;
2443
- const sent = await sendScheduledMessage(ctx, target, content);
4695
+ const imageSegment = import_koishi9.h.image(image, detectImageMime(image));
4696
+ const sent = await sendScheduledMessage(ctx, target, buildContent(imageSegment));
2444
4697
  if (sent) return true;
2445
- return sendScheduledMessage(ctx, target, `${prefix}${fallbackText}`);
4698
+ return sendScheduledMessage(ctx, target, buildContent(import_koishi9.h.text(fallbackText)));
2446
4699
  }
2447
4700
  __name(sendScheduledImageWithFallback, "sendScheduledImageWithFallback");
2448
4701
 
4702
+ // src/pet-data-service.ts
4703
+ var CN_TIME_ZONE = "Asia/Shanghai";
4704
+ function petDataDisplay(value, defaultText = "--") {
4705
+ if (value === void 0 || value === null || value === "") return defaultText;
4706
+ if (typeof value === "boolean") return value ? "是" : "否";
4707
+ return String(value);
4708
+ }
4709
+ __name(petDataDisplay, "petDataDisplay");
4710
+ function petDataImageUrl(petId, imageType = "image") {
4711
+ const assetIdRaw = parseInt(String(petId), 10);
4712
+ if (!Number.isFinite(assetIdRaw) || assetIdRaw <= 0) return "";
4713
+ const assetId = assetIdRaw < 3e3 ? assetIdRaw + 3e3 : assetIdRaw;
4714
+ const type = imageType === "icon" ? "icon" : "image";
4715
+ return `https://game.gtimg.cn/images/rocom/rocodata/jingling/${assetId}/${type}.png`;
4716
+ }
4717
+ __name(petDataImageUrl, "petDataImageUrl");
4718
+ function petDataWikiPetId(value) {
4719
+ const petId = parseInt(String(value), 10);
4720
+ if (!Number.isFinite(petId) || petId <= 0) return "";
4721
+ return String(petId < 3e3 ? petId + 3e3 : petId);
4722
+ }
4723
+ __name(petDataWikiPetId, "petDataWikiPetId");
4724
+ function normalizeEpochSeconds(value) {
4725
+ const num2 = Number(value);
4726
+ if (!Number.isFinite(num2) || num2 <= 0) return 0;
4727
+ return num2 > 1e10 ? Math.floor(num2 / 1e3) : Math.floor(num2);
4728
+ }
4729
+ __name(normalizeEpochSeconds, "normalizeEpochSeconds");
4730
+ function petDataTimeText(value) {
4731
+ const ts = normalizeEpochSeconds(value);
4732
+ if (!ts) return "--";
4733
+ const date = new Date(ts * 1e3);
4734
+ const formatter = new Intl.DateTimeFormat("zh-CN", {
4735
+ timeZone: CN_TIME_ZONE,
4736
+ year: "numeric",
4737
+ month: "2-digit",
4738
+ day: "2-digit",
4739
+ hour: "2-digit",
4740
+ minute: "2-digit",
4741
+ hour12: false
4742
+ });
4743
+ const parts = Object.fromEntries(formatter.formatToParts(date).map((p) => [p.type, p.value]));
4744
+ return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`;
4745
+ }
4746
+ __name(petDataTimeText, "petDataTimeText");
4747
+ function petDataSizeText(value, unit) {
4748
+ const number = Number(value);
4749
+ if (!Number.isFinite(number)) return "--";
4750
+ if (unit === "g") return `${(number / 1e3).toFixed(2)} kg`;
4751
+ if (unit === "cm") return `${number} cm`;
4752
+ return `${number} ${unit}`.trim();
4753
+ }
4754
+ __name(petDataSizeText, "petDataSizeText");
4755
+ function petDataVoiceText(value) {
4756
+ if (value === void 0 || value === null || value === "") return "--";
4757
+ const number = Number(value);
4758
+ if (!Number.isFinite(number)) return petDataDisplay(value);
4759
+ return Number.isInteger(number) ? `${number} dB` : `${number} dB`;
4760
+ }
4761
+ __name(petDataVoiceText, "petDataVoiceText");
4762
+ function petDataVoiceInfo(value) {
4763
+ const text = petDataVoiceText(value);
4764
+ const number = Number(value);
4765
+ if (!Number.isFinite(number)) return { value: text, hint: "", className: "" };
4766
+ const intNumber = Math.trunc(number);
4767
+ if (intNumber >= 96 && intNumber <= 100) {
4768
+ return { value: `${text} · 婉转声`, hint: "婉转声 96~100", className: "voice-soft" };
4769
+ }
4770
+ if (intNumber >= -100 && intNumber <= -96) {
4771
+ return { value: `${text} · 粗嗓门`, hint: "粗嗓门 -96~-100", className: "voice-rough" };
4772
+ }
4773
+ return { value: text, hint: "婉转声 96~100 / 粗嗓门 -96~-100", className: "" };
4774
+ }
4775
+ __name(petDataVoiceInfo, "petDataVoiceInfo");
4776
+ function petDataKgCompact(grams) {
4777
+ const number = Number(grams);
4778
+ if (!Number.isFinite(number)) return "--";
4779
+ const text = (number / 1e3).toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
4780
+ return `${text}kg`;
4781
+ }
4782
+ __name(petDataKgCompact, "petDataKgCompact");
4783
+ function petDataWeightSizeInfo(pet, wikiPet) {
4784
+ const bodySize = wikiPet && typeof wikiPet === "object" ? wikiPet.body_size : null;
4785
+ const weightRange = bodySize && typeof bodySize === "object" ? bodySize.weight : null;
4786
+ if (!weightRange || typeof weightRange !== "object") return {};
4787
+ const current = Number(pet?.weight);
4788
+ const low = Number(weightRange.min_g);
4789
+ const high = Number(weightRange.max_g);
4790
+ if (!Number.isFinite(current) || !Number.isFinite(low) || !Number.isFinite(high)) return {};
4791
+ if (high <= low) return {};
4792
+ const span = high - low;
4793
+ const smallCut = low + span * 0.05;
4794
+ const largeCut = high - span * 0.05;
4795
+ const rangeText = `${petDataKgCompact(low)}-${petDataKgCompact(high)}`;
4796
+ if (current <= smallCut) {
4797
+ return { label: "小块头", className: "size-small", hint: `小块头 · ≤${petDataKgCompact(smallCut)} · 范围 ${rangeText}` };
4798
+ }
4799
+ if (current >= largeCut) {
4800
+ return { label: "大块头", className: "size-large", hint: `大块头 · ≥${petDataKgCompact(largeCut)} · 范围 ${rangeText}` };
4801
+ }
4802
+ return { label: "", className: "", hint: `范围 ${rangeText}` };
4803
+ }
4804
+ __name(petDataWeightSizeInfo, "petDataWeightSizeInfo");
4805
+ function petDataOptionMaps(options) {
4806
+ const petOptions = options && typeof options === "object" && options.pet && typeof options.pet === "object" ? options.pet : {};
4807
+ const buildMap = /* @__PURE__ */ __name((key, preferShort = false) => {
4808
+ const result = {};
4809
+ const values = petOptions[key];
4810
+ if (!Array.isArray(values)) return result;
4811
+ for (const item of values) {
4812
+ if (!item || typeof item !== "object") continue;
4813
+ const itemId = item.id;
4814
+ if (itemId === void 0 || itemId === null || itemId === "") continue;
4815
+ const name2 = preferShort && item.short_name ? item.short_name : item.name;
4816
+ if (name2) result[String(itemId)] = String(name2);
4817
+ }
4818
+ return result;
4819
+ }, "buildMap");
4820
+ const natures = {};
4821
+ for (const item of petOptions.natures || []) {
4822
+ if (!item || typeof item !== "object" || item.id === void 0 || item.id === null || item.id === "") continue;
4823
+ const name2 = String(item.name || item.id);
4824
+ const summary = String(item.summary || "").trim();
4825
+ natures[String(item.id)] = summary ? `${name2}(${summary})` : name2;
4826
+ }
4827
+ return {
4828
+ natures,
4829
+ bloodlines: buildMap("bloodlines", true),
4830
+ types: buildMap("types"),
4831
+ talent_ratings: buildMap("talent_ratings")
4832
+ };
4833
+ }
4834
+ __name(petDataOptionMaps, "petDataOptionMaps");
4835
+ function petDataLookup(mapping, value, defaultText = "--") {
4836
+ const key = String(value ?? "").trim();
4837
+ if (!key) return defaultText;
4838
+ return mapping[key] || key;
4839
+ }
4840
+ __name(petDataLookup, "petDataLookup");
4841
+ function petDataVariant(pet, fallback = {}) {
4842
+ fallback = fallback || {};
4843
+ const mutationName = String(
4844
+ pet?.mutation_name || fallback?.mutation_name || fallback?.pet_mutation_name || ""
4845
+ ).trim();
4846
+ const mutationType = String(pet?.mutation_type ?? fallback?.mutation_type ?? "").trim();
4847
+ const specialityValues = [];
4848
+ for (const value of [pet?.real_speciality_ids, fallback?.real_speciality_ids, pet?.speciality_id, fallback?.speciality_id]) {
4849
+ if (Array.isArray(value)) specialityValues.push(...value);
4850
+ else if (value !== void 0 && value !== null && value !== "") specialityValues.push(value);
4851
+ }
4852
+ const specialityIds = new Set(specialityValues.map((value) => String(value).trim()).filter(Boolean));
4853
+ if (mutationType === "9" || mutationName.includes("异色") && mutationName.includes("炫彩") || specialityIds.has("103") && specialityIds.has("502")) {
4854
+ return { variantText: "异色炫彩", variantIcon: "异色炫彩.png" };
4855
+ }
4856
+ if (mutationType === "1" || mutationName.includes("异色") || specialityIds.has("103")) {
4857
+ return { variantText: "异色", variantIcon: "异色.png" };
4858
+ }
4859
+ if (mutationType === "8" || mutationName.includes("炫彩") || specialityIds.has("502")) {
4860
+ return { variantText: "炫彩", variantIcon: "炫彩.png" };
4861
+ }
4862
+ return { variantText: mutationName || "普通", variantIcon: "" };
4863
+ }
4864
+ __name(petDataVariant, "petDataVariant");
4865
+ function petDataAttributes(pet) {
4866
+ const attributeInfo = pet?.attribute_info && typeof pet.attribute_info === "object" ? pet.attribute_info : {};
4867
+ const fields = [
4868
+ ["hp", "生命"],
4869
+ ["attack", "物攻"],
4870
+ ["special_attack", "魔攻"],
4871
+ ["defense", "物防"],
4872
+ ["special_defense", "魔防"],
4873
+ ["speed", "速度"]
4874
+ ];
4875
+ return fields.map(([key, label]) => {
4876
+ const raw = attributeInfo[key] && typeof attributeInfo[key] === "object" ? attributeInfo[key] : {};
4877
+ let percent = 6;
4878
+ const base = Number(raw.base_value);
4879
+ if (Number.isFinite(base)) percent = Math.trunc(Math.max(6, Math.min(100, base / 200 * 100)));
4880
+ return {
4881
+ label,
4882
+ value: petDataDisplay(raw.base_value),
4883
+ race: petDataDisplay(raw.total_race),
4884
+ talent: petDataDisplay(raw.talent),
4885
+ percent
4886
+ };
4887
+ });
4888
+ }
4889
+ __name(petDataAttributes, "petDataAttributes");
4890
+ function petDataSkillItems(pet) {
4891
+ const skillRoot = pet?.skill && typeof pet.skill === "object" ? pet.skill : {};
4892
+ const skills = Array.isArray(skillRoot.skill_data) ? skillRoot.skill_data : [];
4893
+ const sortKey = /* @__PURE__ */ __name((item) => {
4894
+ const equippedRank = item?.is_equipped ? 0 : 1;
4895
+ const learnedRank = item?.is_learned ? 0 : 1;
4896
+ let pos = parseInt(String(item?.pos ?? 99), 10);
4897
+ if (!Number.isFinite(pos)) pos = 99;
4898
+ return [equippedRank, learnedRank, pos];
4899
+ }, "sortKey");
4900
+ return skills.filter((s) => s && typeof s === "object").sort((a, b) => {
4901
+ const ka = sortKey(a);
4902
+ const kb = sortKey(b);
4903
+ return ka[0] - kb[0] || ka[1] - kb[1] || ka[2] - kb[2];
4904
+ });
4905
+ }
4906
+ __name(petDataSkillItems, "petDataSkillItems");
4907
+ function extractRawItems(payload) {
4908
+ const rawItems = [];
4909
+ if (Array.isArray(payload?.npc_pets)) {
4910
+ rawItems.push(...payload.npc_pets.filter((item) => item && typeof item === "object"));
4911
+ } else if (payload?.npc_pet && typeof payload.npc_pet === "object") {
4912
+ const query = payload?.query && typeof payload.query === "object" ? payload.query : {};
4913
+ rawItems.push({
4914
+ status: "ok",
4915
+ npc_pet: payload.npc_pet,
4916
+ pet_gid: query.pet_gid,
4917
+ furniture_guid: query.npc_id || query.furniture_guid,
4918
+ npc_id_source: "query"
4919
+ });
4920
+ }
4921
+ return rawItems;
4922
+ }
4923
+ __name(extractRawItems, "extractRawItems");
4924
+ function petDataSkillIdsFromPayload(payload) {
4925
+ let root = payload || {};
4926
+ if (root.result && typeof root.result === "object") root = root.result;
4927
+ const ids = [];
4928
+ for (const raw of extractRawItems(root)) {
4929
+ const npcPet = raw?.npc_pet && typeof raw.npc_pet === "object" ? raw.npc_pet : {};
4930
+ const pet = npcPet?.pet && typeof npcPet.pet === "object" ? npcPet.pet : {};
4931
+ for (const item of petDataSkillItems(pet).slice(0, 8)) {
4932
+ const skillId = String(item?.id ?? "").trim();
4933
+ if (skillId && !ids.includes(skillId)) ids.push(skillId);
4934
+ }
4935
+ }
4936
+ return ids;
4937
+ }
4938
+ __name(petDataSkillIdsFromPayload, "petDataSkillIdsFromPayload");
4939
+ function petDataPetIdsFromPayload(payload) {
4940
+ let root = payload || {};
4941
+ if (root.result && typeof root.result === "object") root = root.result;
4942
+ const ids = [];
4943
+ for (const raw of extractRawItems(root)) {
4944
+ const npcPet = raw?.npc_pet && typeof raw.npc_pet === "object" ? raw.npc_pet : {};
4945
+ const pet = npcPet?.pet && typeof npcPet.pet === "object" ? npcPet.pet : {};
4946
+ const petId = petDataWikiPetId(pet.base_conf_id || raw.pet_cfg_id || pet.catch_base_id || pet.conf_id);
4947
+ if (petId && !ids.includes(petId)) ids.push(petId);
4948
+ }
4949
+ return ids;
4950
+ }
4951
+ __name(petDataPetIdsFromPayload, "petDataPetIdsFromPayload");
4952
+ function petDataSkillIconUrl(baseUrl, skillId, detail = null) {
4953
+ const icon = String(detail?.icon || "").trim();
4954
+ if (icon) return icon;
4955
+ const skillText = String(skillId ?? "").trim();
4956
+ if (!skillText) return "";
4957
+ return `${baseUrl}/api/v1/resources/wiki/assets/skills/${skillText}.png`;
4958
+ }
4959
+ __name(petDataSkillIconUrl, "petDataSkillIconUrl");
4960
+ function wikiNamedValue2(value) {
4961
+ if (value && typeof value === "object" && !Array.isArray(value)) {
4962
+ return String(value.name || value.label || value.value || "");
4963
+ }
4964
+ return String(value ?? "");
4965
+ }
4966
+ __name(wikiNamedValue2, "wikiNamedValue");
4967
+ function petDataSkills(pet, skillLookup = {}, options = {}) {
4968
+ const loadSkillIcons = options.loadSkillIcons !== false;
4969
+ const baseUrl = options.baseUrl || "";
4970
+ const result = [];
4971
+ for (const item of petDataSkillItems(pet).slice(0, 8)) {
4972
+ const skillId = petDataDisplay(item?.id);
4973
+ const detail = skillId !== "--" ? skillLookup[String(skillId)] : null;
4974
+ let status;
4975
+ let statusClass;
4976
+ if (item?.is_equipped) {
4977
+ status = "已装备";
4978
+ statusClass = "equipped";
4979
+ } else if (item?.is_learned) {
4980
+ status = "已学会";
4981
+ statusClass = "learned";
4982
+ } else {
4983
+ status = "未学会";
4984
+ statusClass = "locked";
4985
+ }
4986
+ const element = detail ? wikiNamedValue2(detail.element_type) : "";
4987
+ const skillType = detail ? wikiNamedValue2(detail.skill_type || detail.damage_type) : "";
4988
+ result.push({
4989
+ id: skillId,
4990
+ name: String(detail?.name || skillId),
4991
+ icon: loadSkillIcons ? petDataSkillIconUrl(baseUrl, skillId, detail) : "",
4992
+ element: element || "未知",
4993
+ type: skillType || "技能",
4994
+ power: petDataDisplay(detail?.power),
4995
+ cost: petDataDisplay(detail?.cost),
4996
+ pos: petDataDisplay(item?.pos),
4997
+ status,
4998
+ statusClass,
4999
+ unlock: item?.unlock_need_lv !== void 0 && item?.unlock_need_lv !== null && item?.unlock_need_lv !== "" && item?.unlock_need_lv !== 0 ? `Lv.${item.unlock_need_lv}` : "--",
5000
+ description: String(detail?.description || detail?.desc || "")
5001
+ });
5002
+ }
5003
+ return result;
5004
+ }
5005
+ __name(petDataSkills, "petDataSkills");
5006
+ function petDataCardItems(pet, optionMaps, sizeInfo = {}) {
5007
+ const genderMap = { "0": "未知", "1": "雄性", "2": "雌性" };
5008
+ const gender = genderMap[String(pet?.gender ?? "")] || petDataDisplay(pet?.gender);
5009
+ const nature = petDataLookup(optionMaps.natures, pet?.nature);
5010
+ const blood = petDataLookup(optionMaps.bloodlines, pet?.blood_id);
5011
+ const talentRank = petDataLookup(optionMaps.talent_ratings, pet?.talent_rank, petDataDisplay(pet?.talent_rank));
5012
+ const voiceInfo = petDataVoiceInfo(pet?.voice);
5013
+ let weightValue = petDataSizeText(pet?.weight, "g");
5014
+ if (sizeInfo.label) weightValue = `${weightValue} · ${sizeInfo.label}`;
5015
+ return [
5016
+ { label: "等级", value: petDataDisplay(pet?.level) },
5017
+ { label: "性别", value: gender },
5018
+ { label: "分贝", ...voiceInfo },
5019
+ { label: "性格", value: nature },
5020
+ { label: "血脉", value: blood },
5021
+ { label: "天赋评级", value: talentRank },
5022
+ { label: "身高", value: petDataSizeText(pet?.height, "cm") },
5023
+ {
5024
+ label: "体重",
5025
+ value: weightValue,
5026
+ hint: sizeInfo.hint || "",
5027
+ className: sizeInfo.className || ""
5028
+ }
5029
+ ];
5030
+ }
5031
+ __name(petDataCardItems, "petDataCardItems");
5032
+ function petDataExtractItems(payload, optionMaps, skillLookup = {}, sizeLookup = {}, options = {}) {
5033
+ const rawItems = extractRawItems(payload);
5034
+ const pets = [];
5035
+ rawItems.forEach((raw, index) => {
5036
+ const npcPet = raw?.npc_pet && typeof raw.npc_pet === "object" ? raw.npc_pet : {};
5037
+ const pet = npcPet?.pet && typeof npcPet.pet === "object" ? npcPet.pet : {};
5038
+ const status = String(raw?.status || (Object.keys(pet).length ? "ok" : "unknown"));
5039
+ const { variantText, variantIcon } = petDataVariant(pet, raw);
5040
+ const baseId = pet.base_conf_id || raw.pet_cfg_id || pet.catch_base_id || pet.conf_id;
5041
+ const wikiPetId = petDataWikiPetId(baseId);
5042
+ const sizeInfo = petDataWeightSizeInfo(pet, sizeLookup[wikiPetId]);
5043
+ const name2 = pet.name || pet.pet_default_name || raw.pet_default_name || raw.name || `精灵 ${baseId || index + 1}`;
5044
+ const defaultName = pet.pet_default_name || raw.pet_default_name || "";
5045
+ const displayDefault = defaultName && defaultName !== name2 ? defaultName : "";
5046
+ const petGid = pet.gid || raw.pet_gid;
5047
+ const sceneInfo = pet.scene_info && typeof pet.scene_info === "object" ? pet.scene_info : null;
5048
+ const furnitureGuid = raw.furniture_guid ?? (sceneInfo ? sceneInfo.npc_id : void 0);
5049
+ pets.push({
5050
+ index: index + 1,
5051
+ status,
5052
+ statusText: status === "ok" ? "成功" : status,
5053
+ retCode: petDataDisplay(npcPet.ret_code),
5054
+ name: String(name2),
5055
+ defaultName: String(displayDefault),
5056
+ level: petDataDisplay(pet.level),
5057
+ baseId: petDataDisplay(baseId),
5058
+ confId: petDataDisplay(pet.conf_id),
5059
+ petGid: petDataDisplay(petGid),
5060
+ npcId: petDataDisplay(furnitureGuid),
5061
+ npcIdSource: petDataDisplay(raw.npc_id_source),
5062
+ imageUrl: petDataImageUrl(baseId, "image"),
5063
+ iconUrl: petDataImageUrl(baseId, "icon"),
5064
+ variantText,
5065
+ variantIcon,
5066
+ voiceText: petDataVoiceText(pet.voice),
5067
+ cards: petDataCardItems(pet, optionMaps, sizeInfo),
5068
+ attributes: petDataAttributes(pet),
5069
+ skills: petDataSkills(pet, skillLookup, options),
5070
+ catchItems: [
5071
+ { label: "捕捉等级", value: petDataDisplay(pet.catch_lv) },
5072
+ { label: "捕捉方式", value: petDataDisplay(pet.catch_way) },
5073
+ { label: "捕捉营地", value: petDataDisplay(pet.caught_camp) },
5074
+ { label: "获得时间", value: petDataTimeText(pet.add_time) }
5075
+ ],
5076
+ specialityIds: (Array.isArray(pet.real_speciality_ids) ? pet.real_speciality_ids : []).map((x) => String(x)).join(" / ") || petDataDisplay(pet.speciality_id),
5077
+ relationshipType: petDataDisplay(npcPet.relationship_type),
5078
+ errorText: petDataDisplay(raw.error || raw.message || npcPet.error_message, "")
5079
+ });
5080
+ });
5081
+ return pets;
5082
+ }
5083
+ __name(petDataExtractItems, "petDataExtractItems");
5084
+ function buildPetDataRenderData(res, uid, options = {}) {
5085
+ let payload = res || {};
5086
+ if (payload.result && typeof payload.result === "object") payload = payload.result;
5087
+ const optionMaps = petDataOptionMaps(options.optionsPayload);
5088
+ let playerInfo = payload.player_info && typeof payload.player_info === "object" ? payload.player_info : {};
5089
+ if (!Object.keys(playerInfo).length && payload.npc_pet && typeof payload.npc_pet === "object") {
5090
+ playerInfo = payload.npc_pet.player_info && typeof payload.npc_pet.player_info === "object" ? payload.npc_pet.player_info : {};
5091
+ }
5092
+ const pets = petDataExtractItems(payload, optionMaps, options.skillLookup || {}, options.sizeLookup || {}, {
5093
+ loadSkillIcons: !options.lowBandwidthMode,
5094
+ baseUrl: options.baseUrl || ""
5095
+ });
5096
+ const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : {};
5097
+ const finishedAt = normalizeEpochSeconds(meta.finished_at || meta.created_at);
5098
+ const updatedAt = petDataTimeText(finishedAt || Math.floor(Date.now() / 1e3));
5099
+ const online = playerInfo.online;
5100
+ const onlineText = online === true ? "在线" : online === false ? "离线" : "未知";
5101
+ const okCount = payload.npc_pet_ok_count;
5102
+ const errorCount = payload.npc_pet_error_count;
5103
+ const skippedCount = payload.npc_pet_skipped_count;
5104
+ return {
5105
+ title: "家园详情",
5106
+ subtitle: "Ingame Pet Data",
5107
+ uid: petDataDisplay(payload.uin || playerInfo.uin || uid),
5108
+ playerName: petDataDisplay(playerInfo.name, "未知玩家"),
5109
+ playerLevel: petDataDisplay(playerInfo.level),
5110
+ worldLevel: petDataDisplay(playerInfo.world_level),
5111
+ onlineText,
5112
+ isOnline: online === true,
5113
+ queryMode: options.singleQuery ? "单只精灵" : "家园批量",
5114
+ lowBandwidthMode: Boolean(options.lowBandwidthMode),
5115
+ summaryCards: [
5116
+ { label: "目标状态", value: onlineText },
5117
+ { label: "返回精灵", value: String(pets.length) },
5118
+ { label: "成功/失败", value: `${petDataDisplay(okCount, String(pets.length))} / ${petDataDisplay(errorCount, "0")}` },
5119
+ { label: "跳过", value: petDataDisplay(skippedCount, "0") }
5120
+ ],
5121
+ pets,
5122
+ updatedAt,
5123
+ notice: "该接口依赖目标玩家在线且家园可访问;离线或隐私/上游不可达时可能失败。",
5124
+ emptyText: "未获取到家园精灵完整数据。请确认目标玩家在线、家园可访问,或稍后重试。",
5125
+ commandHint: "💡 洛克.家园详情 <UID> | 洛克.家园详情 <UID> <pet_gid> <npc_id>",
5126
+ copyright: "Koishi & WeGame 洛克王国插件"
5127
+ };
5128
+ }
5129
+ __name(buildPetDataRenderData, "buildPetDataRenderData");
5130
+
2449
5131
  // src/commands/query.ts
2450
- var import_node_fs4 = __toESM(require("node:fs"));
2451
- var import_node_path4 = __toESM(require("node:path"));
2452
- var logger8 = new import_koishi8.Logger("rocom-query");
5132
+ var import_node_fs5 = __toESM(require("node:fs"));
5133
+ var import_node_path5 = __toESM(require("node:path"));
5134
+ var logger10 = new import_koishi10.Logger("rocom-query");
2453
5135
  async function sendImage(deps, session, templateName, data, fallback) {
2454
5136
  const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
2455
5137
  await sendImageWithFallback(session, png, fallback, `query:${templateName}`, deps.config);
@@ -2659,14 +5341,14 @@ function stringifyInspectValue(value) {
2659
5341
  return String(value);
2660
5342
  }
2661
5343
  __name(stringifyInspectValue, "stringifyInspectValue");
2662
- function normalizeEpochSeconds(value) {
5344
+ function normalizeEpochSeconds2(value) {
2663
5345
  const ts = Number(value);
2664
5346
  if (!Number.isFinite(ts)) return 0;
2665
5347
  if (ts > 1e13) return Math.floor(ts / 1e6);
2666
5348
  if (ts > 1e10) return Math.floor(ts / 1e3);
2667
5349
  return Math.floor(ts);
2668
5350
  }
2669
- __name(normalizeEpochSeconds, "normalizeEpochSeconds");
5351
+ __name(normalizeEpochSeconds2, "normalizeEpochSeconds");
2670
5352
  function formatHomeRemaining(targetTs, nowTs = Math.floor(Date.now() / 1e3)) {
2671
5353
  if (!targetTs) return "未开始";
2672
5354
  if (nowTs >= targetTs) return "已完成";
@@ -2730,12 +5412,12 @@ var PANEL_PET_STAT_DEFS = [
2730
5412
  { key: "special_defense", label: "魔防", addiType: 5, aliases: ["special_defense", "magic_defense", "sp_defense"] },
2731
5413
  { key: "speed", label: "速度", addiType: 6, aliases: ["speed", "spd"] }
2732
5414
  ];
2733
- var PANEL_PET_CACHE_PATH = import_node_path4.default.join("data", "rocom", "rocom_pet_panel_cache.json");
5415
+ var PANEL_PET_CACHE_PATH = import_node_path5.default.join("data", "rocom", "rocom_pet_panel_cache.json");
2734
5416
  var PANEL_EMPTY_FEATURE = { id: "", name: "暂无特性", desc: "当前接口未返回特性描述。" };
2735
5417
  var PANEL_EMPTY_SKILL_HINT = "当前接口未返回技能详情。";
2736
5418
  var panelPetCacheMem = null;
2737
5419
  function panelPetCacheFilePath(deps) {
2738
- return import_node_path4.default.join(deps.ctx.baseDir, PANEL_PET_CACHE_PATH);
5420
+ return import_node_path5.default.join(deps.ctx.baseDir, PANEL_PET_CACHE_PATH);
2739
5421
  }
2740
5422
  __name(panelPetCacheFilePath, "panelPetCacheFilePath");
2741
5423
  function ensureBloodlineLabel(raw, mutationLabel) {
@@ -2747,15 +5429,15 @@ function loadPanelPetCache(deps) {
2747
5429
  if (panelPetCacheMem) return panelPetCacheMem;
2748
5430
  const filePath = panelPetCacheFilePath(deps);
2749
5431
  try {
2750
- if (!import_node_fs4.default.existsSync(filePath)) {
5432
+ if (!import_node_fs5.default.existsSync(filePath)) {
2751
5433
  panelPetCacheMem = {};
2752
5434
  return panelPetCacheMem;
2753
5435
  }
2754
- const text = import_node_fs4.default.readFileSync(filePath, "utf-8");
5436
+ const text = import_node_fs5.default.readFileSync(filePath, "utf-8");
2755
5437
  const parsed = JSON.parse(text);
2756
5438
  panelPetCacheMem = parsed && typeof parsed === "object" ? parsed : {};
2757
5439
  } catch (err) {
2758
- logger8.warn(`加载精灵面板缓存失败: ${err}`);
5440
+ logger10.warn(`加载精灵面板缓存失败: ${err}`);
2759
5441
  panelPetCacheMem = {};
2760
5442
  }
2761
5443
  return panelPetCacheMem;
@@ -2763,11 +5445,11 @@ function loadPanelPetCache(deps) {
2763
5445
  __name(loadPanelPetCache, "loadPanelPetCache");
2764
5446
  function savePanelPetCache(deps, data) {
2765
5447
  const filePath = panelPetCacheFilePath(deps);
2766
- const dir = import_node_path4.default.dirname(filePath);
2767
- import_node_fs4.default.mkdirSync(dir, { recursive: true });
5448
+ const dir = import_node_path5.default.dirname(filePath);
5449
+ import_node_fs5.default.mkdirSync(dir, { recursive: true });
2768
5450
  const tempPath = `${filePath}.tmp`;
2769
- import_node_fs4.default.writeFileSync(tempPath, JSON.stringify(data, null, 2), "utf-8");
2770
- import_node_fs4.default.renameSync(tempPath, filePath);
5451
+ import_node_fs5.default.writeFileSync(tempPath, JSON.stringify(data, null, 2), "utf-8");
5452
+ import_node_fs5.default.renameSync(tempPath, filePath);
2771
5453
  panelPetCacheMem = data;
2772
5454
  }
2773
5455
  __name(savePanelPetCache, "savePanelPetCache");
@@ -3224,7 +5906,7 @@ function panelPetDetailData(uid, pet, updatedAtText) {
3224
5906
  }
3225
5907
  __name(panelPetDetailData, "panelPetDetailData");
3226
5908
  async function refreshPanelPets(deps, uid, userId = "") {
3227
- const res = await deps.client.ingameHomeInfo(deps.ctx, uid, 2e4);
5909
+ const res = await deps.client.ingameHomeInfo(deps.ctx, uid, { waitMs: 2e4, timeoutMs: 3e4 });
3228
5910
  if (res) {
3229
5911
  const homeInfo = homeInfoPayload(res);
3230
5912
  const pets = buildPanelPetList(homeInfo);
@@ -3256,7 +5938,7 @@ function formatPanelUpdatedAt(ts) {
3256
5938
  return new Date(value * 1e3).toLocaleString("zh-CN");
3257
5939
  }
3258
5940
  __name(formatPanelUpdatedAt, "formatPanelUpdatedAt");
3259
- function extractHomePet(raw, index, guard = false) {
5941
+ function extractHomePet(deps, raw, index, guard = false) {
3260
5942
  if (!raw || typeof raw !== "object") return null;
3261
5943
  const homePet = raw.home_pet_info && typeof raw.home_pet_info === "object" ? raw.home_pet_info : raw;
3262
5944
  const display = raw.display_info && typeof raw.display_info === "object" ? raw.display_info : {};
@@ -3264,10 +5946,22 @@ function extractHomePet(raw, index, guard = false) {
3264
5946
  if (["", "0"].includes(String(petId || "0")) && !guard) return null;
3265
5947
  const nowTs = Math.floor(Date.now() / 1e3);
3266
5948
  const hasEgg = Boolean(raw.have_egg);
3267
- const predictedEggTime = normalizeEpochSeconds(raw.predicted_egg_time);
5949
+ const predictedEggTime = normalizeEpochSeconds2(raw.predicted_egg_time);
3268
5950
  const eggReady = hasEgg || predictedEggTime > 0 && nowTs >= predictedEggTime;
3269
5951
  const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
3270
5952
  const gender = Number(display.gender || raw.gender || 0) || 0;
5953
+ let mutationType = Number(display.mutation_type || raw.mutation_type || homePet.mutation_type || 0) || 0;
5954
+ if (!mutationType) {
5955
+ const specialityValues = [];
5956
+ for (const value of [homePet.real_speciality_ids, raw.real_speciality_ids, homePet.speciality_id, raw.speciality_id]) {
5957
+ if (Array.isArray(value)) specialityValues.push(...value);
5958
+ else if (value !== void 0 && value !== null && value !== "") specialityValues.push(value);
5959
+ }
5960
+ const specialityIds = new Set(specialityValues.map((value) => String(value).trim()).filter(Boolean));
5961
+ if (specialityIds.has("103") && specialityIds.has("502")) mutationType = 9;
5962
+ else if (specialityIds.has("103")) mutationType = 1;
5963
+ else if (specialityIds.has("502")) mutationType = 8;
5964
+ }
3271
5965
  const isMale = gender === 1;
3272
5966
  const status = homePet.status ?? raw.status;
3273
5967
  const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ["2", "guard", "守卫"].includes(String(status).toLowerCase());
@@ -3295,11 +5989,16 @@ function extractHomePet(raw, index, guard = false) {
3295
5989
  name: String(homePet.name || homePet.pet_name || raw.name || raw.pet_name || `精灵 ${petId || ""}`),
3296
5990
  level: display.level || raw.level || homePet.level || "--",
3297
5991
  iconUrl: homePetIcon(petId, raw.icon_url || raw.pet_img_url || raw.petIcon || ""),
5992
+ starIconUrl: [1, 8, 9].includes(mutationType) ? deps.renderer.resourceUrl(`render-templates/home/img/rocomuid/star_${mutationType}.png`) : "",
3298
5993
  badge: isGuard ? "守" : hasEgg ? "蛋" : "",
5994
+ mutationType,
3299
5995
  isGuard,
3300
5996
  statusText,
3301
5997
  statusClass,
3302
5998
  note,
5999
+ hasEgg,
6000
+ eggReady,
6001
+ eggTime: predictedEggTime || 0,
3303
6002
  inspireReady: eggReady,
3304
6003
  readyAt: predictedEggTime || 0,
3305
6004
  gender
@@ -3339,23 +6038,23 @@ function homePlantIcon(deps, iconId) {
3339
6038
  }
3340
6039
  __name(homePlantIcon, "homePlantIcon");
3341
6040
  var homePlantMapCache = null;
3342
- var HOME_PLANT_MAP_RELATIVE_PATH = import_node_path4.default.join("render-templates", "home", "data", "home_item_list.json");
6041
+ var HOME_PLANT_MAP_RELATIVE_PATH = import_node_path5.default.join("render-templates", "home", "data", "home_item_list.json");
3343
6042
  function resolveHomePlantMapPath() {
3344
6043
  const candidates = [
3345
- import_node_path4.default.resolve(__dirname, "..", HOME_PLANT_MAP_RELATIVE_PATH),
3346
- import_node_path4.default.resolve(__dirname, HOME_PLANT_MAP_RELATIVE_PATH)
6044
+ import_node_path5.default.resolve(__dirname, "..", HOME_PLANT_MAP_RELATIVE_PATH),
6045
+ import_node_path5.default.resolve(__dirname, HOME_PLANT_MAP_RELATIVE_PATH)
3347
6046
  ];
3348
- return candidates.find((candidate) => import_node_fs4.default.existsSync(candidate)) || candidates[0];
6047
+ return candidates.find((candidate) => import_node_fs5.default.existsSync(candidate)) || candidates[0];
3349
6048
  }
3350
6049
  __name(resolveHomePlantMapPath, "resolveHomePlantMapPath");
3351
6050
  function loadHomePlantMap() {
3352
6051
  if (homePlantMapCache) return homePlantMapCache;
3353
6052
  const filePath = resolveHomePlantMapPath();
3354
6053
  try {
3355
- const data = JSON.parse(import_node_fs4.default.readFileSync(filePath, "utf-8"));
6054
+ const data = JSON.parse(import_node_fs5.default.readFileSync(filePath, "utf-8"));
3356
6055
  homePlantMapCache = data && typeof data === "object" ? data : {};
3357
6056
  } catch (err) {
3358
- logger8.warn(`加载家园作物映射失败: ${err}`);
6057
+ logger10.warn(`加载家园作物映射失败: ${err}`);
3359
6058
  homePlantMapCache = {};
3360
6059
  }
3361
6060
  return homePlantMapCache;
@@ -3377,7 +6076,7 @@ function extractHomePlants(deps, homeInfo) {
3377
6076
  if (["", "0"].includes(String(plantId || "0"))) return null;
3378
6077
  const mappedPlant = plantMap[String(plantId)] || {};
3379
6078
  const iconId = plantData.icon_url || plantData.iconUrl || raw.icon_url || raw.iconUrl || plantData.iconid || raw.iconid || raw.icon_id || mappedPlant.iconid;
3380
- let readyAt = normalizeEpochSeconds(raw.plant_rip_time || raw.rip_time || raw.end_time);
6079
+ let readyAt = normalizeEpochSeconds2(raw.plant_rip_time || raw.rip_time || raw.end_time);
3381
6080
  const leftTime = Number(raw.left_time || 0);
3382
6081
  if (!readyAt && leftTime > 0) readyAt = nowTs + leftTime;
3383
6082
  const ready = Boolean(readyAt && nowTs >= readyAt) || ["2", "ready", "mature"].includes(String(raw.status));
@@ -3410,13 +6109,13 @@ function buildHomeRenderData(deps, res, uid) {
3410
6109
  const indoorPets = [];
3411
6110
  const guardPets = [];
3412
6111
  indoorSources.forEach((raw, index) => {
3413
- const item = extractHomePet(raw, index);
6112
+ const item = extractHomePet(deps, raw, index);
3414
6113
  if (!item) return;
3415
6114
  if (item.isGuard) guardPets.push(item);
3416
6115
  else indoorPets.push(item);
3417
6116
  });
3418
6117
  guardSources.forEach((raw, index) => {
3419
- const item = extractHomePet(raw, index, true);
6118
+ const item = extractHomePet(deps, raw, index, true);
3420
6119
  if (item) guardPets.push(item);
3421
6120
  });
3422
6121
  indoorPets.sort((a, b) => {
@@ -3430,7 +6129,7 @@ function buildHomeRenderData(deps, res, uid) {
3430
6129
  return 0;
3431
6130
  });
3432
6131
  const gardenPlots = extractHomePlants(deps, homeInfo);
3433
- const createdAt = normalizeEpochSeconds(res?.meta?.created_at);
6132
+ const createdAt = normalizeEpochSeconds2(res?.meta?.created_at);
3434
6133
  return {
3435
6134
  title: "洛克家园",
3436
6135
  subtitle: "Home Information",
@@ -3591,11 +6290,31 @@ async function resolveHomeUid(deps, session, uid = "") {
3591
6290
  return String(deps.userMgr.getPrimaryBinding(session?.userId || "")?.role_id || "");
3592
6291
  }
3593
6292
  __name(resolveHomeUid, "resolveHomeUid");
6293
+ var HOME_SUB_TEXT = {
6294
+ garden: {
6295
+ kindText: "菜园作物",
6296
+ actionText: "成熟",
6297
+ subscribedHint: "的家园菜园提醒:首个成熟和全部成熟时各推送一次。",
6298
+ needUidHint: "请提供玩家 UID,或先完成绑定后再订阅家园菜园。"
6299
+ },
6300
+ inspiration: {
6301
+ kindText: "精灵灵感",
6302
+ actionText: "完成",
6303
+ subscribedHint: "的家园精灵灵感提醒:首个完成和全部完成时各推送一次。",
6304
+ needUidHint: "请提供玩家 UID,或先完成绑定后再订阅家园灵感。"
6305
+ },
6306
+ egg: {
6307
+ kindText: "精灵生蛋",
6308
+ actionText: "可领取",
6309
+ subscribedHint: "的家园精灵生蛋提醒:首个可领取和全部可领取时各推送一次。",
6310
+ needUidHint: "请提供玩家 UID,或先完成绑定后再订阅家园生蛋。"
6311
+ }
6312
+ };
3594
6313
  async function subscribeHome(deps, session, uid, kind) {
3595
6314
  const target = sessionTarget(session);
3596
6315
  if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds)) return "此指令仅限管理员使用。";
3597
6316
  const targetUid = await resolveHomeUid(deps, session, uid);
3598
- if (!targetUid) return kind === "garden" ? "请提供玩家 UID,或先完成绑定后再订阅家园菜园。" : "请提供玩家 UID,或先完成绑定后再订阅家园灵感。";
6317
+ if (!targetUid) return HOME_SUB_TEXT[kind].needUidHint;
3599
6318
  const key = homeSubscriptionKey(session, targetUid, kind);
3600
6319
  deps.homeSubMgr.upsert(key, {
3601
6320
  key,
@@ -3609,7 +6328,7 @@ async function subscribeHome(deps, session, uid, kind) {
3609
6328
  notify_state: {},
3610
6329
  updated_at: Math.floor(Date.now() / 1e3)
3611
6330
  });
3612
- return kind === "garden" ? `已订阅 UID ${targetUid} 的家园菜园提醒:首个成熟和全部成熟时各推送一次。` : `已订阅 UID ${targetUid} 的家园精灵灵感提醒:首个完成和全部完成时各推送一次。`;
6331
+ return `已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`;
3613
6332
  }
3614
6333
  __name(subscribeHome, "subscribeHome");
3615
6334
  function homeSubscriptionState(data, kind) {
@@ -3619,6 +6338,12 @@ function homeSubscriptionState(data, kind) {
3619
6338
  const names2 = readyItems2.map((item) => `田地${item.landIndex} ${item.plantName}`);
3620
6339
  return { items: items2, readyItems: readyItems2, names: names2 };
3621
6340
  }
6341
+ if (kind === "egg") {
6342
+ const items2 = [...data.indoorPets || [], ...data.guardPets || []].filter((item) => item.eggTime || item.hasEgg);
6343
+ const readyItems2 = items2.filter((item) => item.eggReady);
6344
+ const names2 = readyItems2.map((item) => item.name || "未知精灵");
6345
+ return { items: items2, readyItems: readyItems2, names: names2 };
6346
+ }
3622
6347
  const items = [...data.indoorPets || [], ...data.guardPets || []].filter((item) => item.readyAt);
3623
6348
  const readyItems = items.filter((item) => item.inspireReady);
3624
6349
  const names = readyItems.map((item) => item.name || "未知精灵");
@@ -3626,8 +6351,7 @@ function homeSubscriptionState(data, kind) {
3626
6351
  }
3627
6352
  __name(homeSubscriptionState, "homeSubscriptionState");
3628
6353
  function homeSubscriptionMessage(uid, kind, level, totalCount, readyItems, names) {
3629
- const kindText = kind === "garden" ? "菜园作物" : "精灵灵感";
3630
- const actionText = kind === "garden" ? "成熟" : "完成";
6354
+ const { kindText, actionText } = HOME_SUB_TEXT[kind];
3631
6355
  const levelText = level === "first" ? "首个" : "全部";
3632
6356
  return [
3633
6357
  `家园${kindText}${levelText}${actionText}提醒:${uid}`,
@@ -3642,10 +6366,10 @@ async function checkHomeSubscriptions(deps) {
3642
6366
  let checkedCount = 0;
3643
6367
  let pushedCount = 0;
3644
6368
  for (const [key, sub] of Object.entries(subs)) {
3645
- if (!sub.uid || !["garden", "inspiration"].includes(sub.kind)) continue;
6369
+ if (!sub.uid || !["garden", "inspiration", "egg"].includes(sub.kind)) continue;
3646
6370
  checkedCount++;
3647
6371
  if (!cache.has(sub.uid)) {
3648
- cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid));
6372
+ cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 3e4 }));
3649
6373
  }
3650
6374
  const res = cache.get(sub.uid);
3651
6375
  if (!res) continue;
@@ -3677,7 +6401,7 @@ async function checkHomeSubscriptions(deps) {
3677
6401
  }, messages.join("\n\n"));
3678
6402
  if (!sent) continue;
3679
6403
  } catch (e) {
3680
- logger8.warn(`家园订阅推送失败: ${e}`);
6404
+ logger10.warn(`家园订阅推送失败: ${e}`);
3681
6405
  continue;
3682
6406
  }
3683
6407
  for (const level of pushLevels) notifyState[level] = true;
@@ -3733,19 +6457,19 @@ function register2(deps) {
3733
6457
  const hasExtraProfileData = Boolean(profileSignature || profileHomeItems.length || profileCardItems.length || profileCardImage);
3734
6458
  let degraded = false;
3735
6459
  if (!sm) {
3736
- logger8.warn("[Rocom] 洛克.档案:pet-summary 接口不可用,已降级为基础档案渲染");
6460
+ logger10.warn("[Rocom] 洛克.档案:pet-summary 接口不可用,已降级为基础档案渲染");
3737
6461
  degraded = true;
3738
6462
  }
3739
6463
  if (!ev) {
3740
- logger8.warn("[Rocom] 洛克.档案:evaluation 接口不可用,已降级为基础档案渲染");
6464
+ logger10.warn("[Rocom] 洛克.档案:evaluation 接口不可用,已降级为基础档案渲染");
3741
6465
  degraded = true;
3742
6466
  }
3743
6467
  if (!cl) {
3744
- logger8.warn("[Rocom] 洛克.档案:collection 接口不可用,已降级为基础档案渲染");
6468
+ logger10.warn("[Rocom] 洛克.档案:collection 接口不可用,已降级为基础档案渲染");
3745
6469
  degraded = true;
3746
6470
  }
3747
6471
  if (!bo) {
3748
- logger8.warn("[Rocom] 洛克.档案:battle-overview 接口不可用,已降级为基础档案渲染");
6472
+ logger10.warn("[Rocom] 洛克.档案:battle-overview 接口不可用,已降级为基础档案渲染");
3749
6473
  degraded = true;
3750
6474
  }
3751
6475
  if (degraded) {
@@ -4083,10 +6807,95 @@ function register2(deps) {
4083
6807
  targetUid = String(binding?.role_id || "");
4084
6808
  }
4085
6809
  if (!targetUid) return "请提供玩家 UID,或先完成绑定后使用 洛克.家园。";
4086
- const res = await client.ingameHomeInfo(ctx, targetUid);
6810
+ let queuedNotified = false;
6811
+ const res = await client.ingameHomeInfo(ctx, targetUid, {
6812
+ waitMs: deps.config.homeQueryWaitMs,
6813
+ intervalMs: deps.config.homeQueryPollIntervalMs,
6814
+ timeoutMs: deps.config.homeQueryTimeoutMs,
6815
+ onQueued: /* @__PURE__ */ __name(async () => {
6816
+ if (queuedNotified) return;
6817
+ queuedNotified = true;
6818
+ await session?.send?.(`UID ${targetUid} 的家园查询已进入队列,正在等待游戏侧返回,请稍候…`);
6819
+ }, "onQueued")
6820
+ });
4087
6821
  if (!res) return `家园查询失败:${client.getLastErrorBrief()}`;
4088
6822
  await sendImage(deps, session, "home", buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
4089
6823
  });
6824
+ ctx.command("洛克").subcommand(".家园详情 [uid:string] [petGid:string] [npcId:string]", "查询家园摆放精灵的完整 ingame 数据").alias("洛克家园详情").alias("家园详情").alias("洛克精灵数据").alias("精灵数据").action(async ({ session }, uid = "", petGid = "", npcId = "") => {
6825
+ const petGidText = String(petGid || "").trim();
6826
+ const npcIdText = String(npcId || "").trim();
6827
+ if (petGidText && !npcIdText || npcIdText && !petGidText) {
6828
+ return [
6829
+ "请同时提供 pet_gid 和 npc_id。用法:洛克.家园详情 <UID> 或 洛克.家园详情 <UID> <pet_gid> <npc_id>",
6830
+ "提示:该接口依赖目标玩家在线,npc_id 也可以使用 furniture_guid。"
6831
+ ].join("\n");
6832
+ }
6833
+ let targetUid = String(uid || "").trim();
6834
+ if (!targetUid) {
6835
+ const binding = deps.userMgr.getPrimaryBinding(session.userId);
6836
+ targetUid = String(binding?.role_id || "");
6837
+ }
6838
+ if (!targetUid) return "请提供玩家 UID,或先完成绑定后使用 洛克.家园详情。目标玩家需要在线。";
6839
+ let queuedNotified = false;
6840
+ const res = await client.ingamePetData(ctx, targetUid, { petGid: petGidText, npcId: npcIdText }, {
6841
+ waitMs: 2e4,
6842
+ intervalMs: deps.config.homeQueryPollIntervalMs,
6843
+ timeoutMs: deps.config.homeQueryTimeoutMs,
6844
+ onQueued: /* @__PURE__ */ __name(async () => {
6845
+ if (queuedNotified) return;
6846
+ queuedNotified = true;
6847
+ await session?.send?.(`UID ${targetUid} 的家园详情查询已进入队列,正在等待游戏侧返回,请稍候…`);
6848
+ }, "onQueued")
6849
+ });
6850
+ if (!res) {
6851
+ return [
6852
+ `家园详情查询失败:${client.getLastErrorBrief()}`,
6853
+ "提示:该接口需要目标玩家在线,且目标家园可访问;离线时通常无法获取完整数据。"
6854
+ ].join("\n");
6855
+ }
6856
+ const lowBandwidth = Boolean(deps.config.lowBandwidthMode);
6857
+ const [optionsPayload, skillLookup, sizeLookup] = await Promise.all([
6858
+ deps.wikiService.getOptionsPayload().catch(() => ({})),
6859
+ (async () => {
6860
+ const lookup = {};
6861
+ const ids = petDataSkillIdsFromPayload(res);
6862
+ await Promise.all(ids.map(async (id) => {
6863
+ const detail = await deps.wikiService.getSkillDetailCached(id).catch(() => null);
6864
+ if (detail) lookup[id] = detail;
6865
+ }));
6866
+ return lookup;
6867
+ })(),
6868
+ (async () => {
6869
+ const lookup = {};
6870
+ const ids = petDataPetIdsFromPayload(res);
6871
+ await Promise.all(ids.map(async (id) => {
6872
+ const detail = await deps.wikiService.getPetDetailCached(id).catch(() => null);
6873
+ if (detail) lookup[id] = detail;
6874
+ }));
6875
+ return lookup;
6876
+ })()
6877
+ ]);
6878
+ const data = buildPetDataRenderData(res, targetUid || "当前绑定", {
6879
+ optionsPayload,
6880
+ skillLookup,
6881
+ sizeLookup,
6882
+ singleQuery: Boolean(petGidText && npcIdText),
6883
+ lowBandwidthMode: lowBandwidth,
6884
+ baseUrl: deps.config.apiBaseUrl.replace(/\/$/, "")
6885
+ });
6886
+ const fallbackLines = [
6887
+ `家园详情 - UID ${data.uid}(${data.onlineText})`,
6888
+ data.notice
6889
+ ];
6890
+ for (const pet of (data.pets || []).slice(0, 8)) {
6891
+ fallbackLines.push(`${pet.index}. ${pet.name} Lv.${pet.level} #${pet.baseId} ${pet.variantText} 分贝 ${pet.voiceText || "--"}`);
6892
+ }
6893
+ if (!data.pets?.length) fallbackLines.push(data.emptyText);
6894
+ fallbackLines.push("");
6895
+ fallbackLines.push("若服务器带宽过小导致生成超时,请在配置项打开低带宽模式。");
6896
+ fallbackLines.push("低带宽模式开启后,家园详情将不再加载技能图标。");
6897
+ await sendImage(deps, session, "pet-data", data, fallbackLines.join("\n"));
6898
+ });
4090
6899
  ctx.command("洛克").subcommand(".刷新面板 [uid:string]", "刷新指定 UID 的精灵面板缓存").alias("洛克刷新面板").alias("刷新面板").alias("刷新精灵").alias("强制刷新").action(async ({ session }, uid = "") => {
4091
6900
  const targetUid = panelUidFromBinding(deps, session, uid);
4092
6901
  if (!targetUid) return "请提供 UID,或先绑定账号后再使用 洛克.刷新面板。";
@@ -4164,10 +6973,11 @@ function register2(deps) {
4164
6973
  });
4165
6974
  ctx.command("订阅家园菜园 [uid:string]", "订阅指定 UID 的家园菜园成熟提醒").action(async ({ session }, uid = "") => subscribeHome(deps, session, uid, "garden"));
4166
6975
  ctx.command("订阅家园灵感 [uid:string]", "订阅指定 UID 的家园精灵灵感完成提醒").action(async ({ session }, uid = "") => subscribeHome(deps, session, uid, "inspiration"));
6976
+ ctx.command("订阅家园生蛋 [uid:string]", "订阅指定 UID 的家园精灵生蛋提醒").action(async ({ session }, uid = "") => subscribeHome(deps, session, uid, "egg"));
4167
6977
  ctx.command("取消订阅家园 [kind:string] [uid:string]", "取消当前会话的家园订阅").action(async ({ session }, kind = "全部", uid = "") => {
4168
6978
  const target = sessionTarget(session);
4169
6979
  if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds)) return "此指令仅限管理员使用。";
4170
- const kindMap = { "菜园": "garden", "灵感": "inspiration", "全部": "", all: "", garden: "garden", inspiration: "inspiration" };
6980
+ const kindMap = { "菜园": "garden", "灵感": "inspiration", "生蛋": "egg", "全部": "", all: "", garden: "garden", inspiration: "inspiration", egg: "egg" };
4171
6981
  const deleted = deps.homeSubMgr.deleteMatching(target, kindMap[String(kind || "全部")] ?? "", String(uid || "").trim());
4172
6982
  return deleted ? `已取消 ${deleted} 条家园订阅。` : "当前会话没有匹配的家园订阅。";
4173
6983
  });
@@ -4177,16 +6987,17 @@ function register2(deps) {
4177
6987
  return `家园订阅检查完成:订阅 ${result.subscriptions} 条,检查 ${result.checked} 条,推送 ${result.pushed} 档提醒。`;
4178
6988
  });
4179
6989
  if (deps.config.homeSubscriptionEnabled) {
4180
- ctx.setInterval(() => checkHomeSubscriptions(deps).catch((err) => logger8.warn(`家园订阅检查失败: ${err}`)), Math.max(1, deps.config.homeSubscriptionIntervalMinutes || 5) * 6e4);
6990
+ ctx.setInterval(() => checkHomeSubscriptions(deps).catch((err) => logger10.warn(`家园订阅检查失败: ${err}`)), Math.max(1, deps.config.homeSubscriptionIntervalMinutes || 5) * 6e4);
4181
6991
  }
4182
6992
  }
4183
6993
  __name(register2, "register");
4184
6994
 
4185
6995
  // src/commands/merchant.ts
4186
- var import_koishi9 = require("koishi");
4187
- var logger9 = new import_koishi9.Logger("rocom-merchant");
6996
+ var import_koishi11 = require("koishi");
6997
+ var logger11 = new import_koishi11.Logger("rocom-merchant");
4188
6998
  var TEXT = {
4189
6999
  merchant: "远行商人",
7000
+ todayMerchant: "今日远行商人",
4190
7001
  subscribe: "订阅远行商人",
4191
7002
  unsubscribe: "取消订阅远行商人",
4192
7003
  viewSubscribe: "查看远行商人订阅",
@@ -4201,6 +7012,12 @@ var CATEGORY_LABELS = {
4201
7012
  round: "常规商品",
4202
7013
  weekend: "周末限定"
4203
7014
  };
7015
+ var ROUND_WINDOWS = [
7016
+ { id: 1, label: "08:00-12:00", startHour: 8, endHour: 12 },
7017
+ { id: 2, label: "12:00-16:00", startHour: 12, endHour: 16 },
7018
+ { id: 3, label: "16:00-20:00", startHour: 16, endHour: 20 },
7019
+ { id: 4, label: "20:00-24:00", startHour: 20, endHour: 24 }
7020
+ ];
4204
7021
  var CHINA_TIMEZONE = "Asia/Shanghai";
4205
7022
  var chinaPartsFormatter = new Intl.DateTimeFormat("zh-CN", {
4206
7023
  timeZone: CHINA_TIMEZONE,
@@ -4209,19 +7026,39 @@ var chinaPartsFormatter = new Intl.DateTimeFormat("zh-CN", {
4209
7026
  day: "2-digit",
4210
7027
  hour: "2-digit",
4211
7028
  minute: "2-digit",
7029
+ second: "2-digit",
4212
7030
  hour12: false
4213
7031
  });
4214
- function getChinaParts(timestampMs) {
7032
+ function getChinaParts(input = Date.now()) {
4215
7033
  const parts = {};
4216
- for (const item of chinaPartsFormatter.formatToParts(new Date(timestampMs))) {
7034
+ const date = input instanceof Date ? input : new Date(input);
7035
+ for (const item of chinaPartsFormatter.formatToParts(date)) {
4217
7036
  if (item.type !== "literal") parts[item.type] = item.value;
4218
7037
  }
4219
7038
  return {
7039
+ year: Number(parts.year || "0"),
7040
+ month: Number(parts.month || "0"),
7041
+ day: Number(parts.day || "0"),
4220
7042
  hour: Number(parts.hour || "0"),
4221
- minute: Number(parts.minute || "0")
7043
+ minute: Number(parts.minute || "0"),
7044
+ second: Number(parts.second || "0")
4222
7045
  };
4223
7046
  }
4224
7047
  __name(getChinaParts, "getChinaParts");
7048
+ function padNumber(value) {
7049
+ return String(value).padStart(2, "0");
7050
+ }
7051
+ __name(padNumber, "padNumber");
7052
+ function getChinaDateText(input = Date.now()) {
7053
+ const parts = getChinaParts(input);
7054
+ return `${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}`;
7055
+ }
7056
+ __name(getChinaDateText, "getChinaDateText");
7057
+ function getChinaDayStartMs(input = Date.now()) {
7058
+ const parts = getChinaParts(input);
7059
+ return (/* @__PURE__ */ new Date(`${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}T00:00:00+08:00`)).getTime();
7060
+ }
7061
+ __name(getChinaDayStartMs, "getChinaDayStartMs");
4225
7062
  function classifyMerchantItem(item) {
4226
7063
  const start = normalizeTimestamp(item?.start_time);
4227
7064
  const end = normalizeTimestamp(item?.end_time);
@@ -4248,16 +7085,12 @@ function formatProductWindow(product) {
4248
7085
  const end = normalizeTimestamp(product?.end_time);
4249
7086
  if (!start && !end) return "";
4250
7087
  const formatDate2 = /* @__PURE__ */ __name((timestamp) => {
4251
- const date = new Date(timestamp);
4252
- const month = String(date.getMonth() + 1).padStart(2, "0");
4253
- const day = String(date.getDate()).padStart(2, "0");
4254
- return `${month}-${day}`;
7088
+ const parts = getChinaParts(timestamp);
7089
+ return `${padNumber(parts.month)}-${padNumber(parts.day)}`;
4255
7090
  }, "formatDate");
4256
7091
  const formatTime = /* @__PURE__ */ __name((timestamp) => {
4257
- const date = new Date(timestamp);
4258
- const hour = String(date.getHours()).padStart(2, "0");
4259
- const minute = String(date.getMinutes()).padStart(2, "0");
4260
- return `${hour}:${minute}`;
7092
+ const parts = getChinaParts(timestamp);
7093
+ return `${padNumber(parts.hour)}:${padNumber(parts.minute)}`;
4261
7094
  }, "formatTime");
4262
7095
  if (start && end) {
4263
7096
  const datePart = formatDate2(start);
@@ -4272,7 +7105,7 @@ function getMerchantActivity(res) {
4272
7105
  return activities[0] || {};
4273
7106
  }
4274
7107
  __name(getMerchantActivity, "getMerchantActivity");
4275
- function getActiveProducts(res) {
7108
+ function getMerchantProducts(res) {
4276
7109
  const activity = getMerchantActivity(res);
4277
7110
  const groups = [];
4278
7111
  if (Array.isArray(activity?.products)) groups.push(activity.products);
@@ -4282,12 +7115,10 @@ function getActiveProducts(res) {
4282
7115
  if (Array.isArray(activity?.get_pets)) groups.push(activity.get_pets);
4283
7116
  const merged = [];
4284
7117
  const seen = /* @__PURE__ */ new Set();
4285
- const now = Date.now();
4286
7118
  for (const list of groups) {
4287
7119
  for (const item of list) {
4288
7120
  const start = normalizeTimestamp(item?.start_time) ?? 0;
4289
7121
  const end = normalizeTimestamp(item?.end_time) ?? Infinity;
4290
- if (now < start || now >= end) continue;
4291
7122
  const key = `${item?.id ?? ""}|${item?.name ?? ""}|${start}|${end === Infinity ? "inf" : end}`;
4292
7123
  if (seen.has(key)) continue;
4293
7124
  seen.add(key);
@@ -4296,35 +7127,60 @@ function getActiveProducts(res) {
4296
7127
  }
4297
7128
  return merged;
4298
7129
  }
4299
- __name(getActiveProducts, "getActiveProducts");
4300
- function getCurrentMerchantRound() {
4301
- const now = /* @__PURE__ */ new Date();
4302
- const hour = now.getHours();
4303
- const rounds = [8, 12, 16, 20];
4304
- let currentRound = null;
4305
- let nextRound = null;
4306
- for (let i = 0; i < rounds.length; i++) {
4307
- if (hour >= rounds[i] && (i === rounds.length - 1 || hour < rounds[i + 1])) {
4308
- currentRound = i + 1;
4309
- nextRound = i < rounds.length - 1 ? rounds[i + 1] : rounds[0];
4310
- break;
4311
- }
7130
+ __name(getMerchantProducts, "getMerchantProducts");
7131
+ function isMerchantItemActive(item, now = Date.now()) {
7132
+ const nowMs = now instanceof Date ? now.getTime() : now;
7133
+ const start = normalizeTimestamp(item?.start_time);
7134
+ const end = normalizeTimestamp(item?.end_time);
7135
+ return (start === null || nowMs >= start) && (end === null || nowMs < end);
7136
+ }
7137
+ __name(isMerchantItemActive, "isMerchantItemActive");
7138
+ function isMerchantItemToday(item, now = Date.now()) {
7139
+ const start = normalizeTimestamp(item?.start_time);
7140
+ const end = normalizeTimestamp(item?.end_time);
7141
+ if (start === null || end === null) return true;
7142
+ const startOfDay = getChinaDayStartMs(now);
7143
+ const endOfDay = startOfDay + 24 * 60 * 60 * 1e3;
7144
+ return start < endOfDay && end > startOfDay;
7145
+ }
7146
+ __name(isMerchantItemToday, "isMerchantItemToday");
7147
+ function getRoundForItem(item, now = Date.now()) {
7148
+ const start = normalizeTimestamp(item?.start_time);
7149
+ if (start === null) return null;
7150
+ const startParts = getChinaParts(start);
7151
+ const nowParts = getChinaParts(now);
7152
+ if (startParts.year !== nowParts.year || startParts.month !== nowParts.month || startParts.day !== nowParts.day) {
7153
+ return null;
7154
+ }
7155
+ const startHour = startParts.hour + startParts.minute / 60;
7156
+ const round = ROUND_WINDOWS.find((win) => startHour >= win.startHour && startHour < win.endHour);
7157
+ return round?.id ?? null;
7158
+ }
7159
+ __name(getRoundForItem, "getRoundForItem");
7160
+ function getCurrentMerchantRound(now = /* @__PURE__ */ new Date()) {
7161
+ const parts = getChinaParts(now);
7162
+ const secondsOfDay = parts.hour * 3600 + parts.minute * 60 + parts.second;
7163
+ const marketStartSeconds = 8 * 3600;
7164
+ const marketEndSeconds = 24 * 3600;
7165
+ const datePart = getChinaDateText(now);
7166
+ if (secondsOfDay < marketStartSeconds || secondsOfDay >= marketEndSeconds) {
7167
+ return {
7168
+ current: null,
7169
+ total: ROUND_WINDOWS.length,
7170
+ countdown: TEXT.notOpen,
7171
+ is_open: false,
7172
+ round_id: `${datePart}-closed`
7173
+ };
4312
7174
  }
4313
- if (currentRound === null) nextRound = rounds[0];
4314
- const nextTime = new Date(now);
4315
- if (nextRound <= hour) nextTime.setDate(nextTime.getDate() + 1);
4316
- nextTime.setHours(nextRound, 0, 0, 0);
4317
- const diff = nextTime.getTime() - now.getTime();
7175
+ const currentWindow = ROUND_WINDOWS.find((win) => parts.hour >= win.startHour && parts.hour < win.endHour);
7176
+ const currentRound = currentWindow?.id ?? null;
7177
+ const roundEndSeconds = (currentWindow?.endHour ?? 24) * 3600;
7178
+ const diff = Math.max(0, (roundEndSeconds - secondsOfDay) * 1e3);
4318
7179
  const hours = Math.floor(diff / 36e5);
4319
7180
  const mins = Math.floor(diff % 36e5 / 6e4);
4320
- const datePart = [
4321
- now.getFullYear(),
4322
- String(now.getMonth() + 1).padStart(2, "0"),
4323
- String(now.getDate()).padStart(2, "0")
4324
- ].join("-");
4325
7181
  return {
4326
7182
  current: currentRound,
4327
- total: rounds.length,
7183
+ total: ROUND_WINDOWS.length,
4328
7184
  countdown: `${hours}小时${mins}分钟`,
4329
7185
  is_open: currentRound !== null,
4330
7186
  round_id: `${datePart}-${currentRound || "closed"}`
@@ -4365,66 +7221,258 @@ function sameStringArray(left, right) {
4365
7221
  return JSON.stringify(left) === JSON.stringify(right);
4366
7222
  }
4367
7223
  __name(sameStringArray, "sameStringArray");
4368
- function buildMerchantRenderPayload(res) {
4369
- const products = getActiveProducts(res);
4370
- const roundInfo = getCurrentMerchantRound();
4371
- const activity = getMerchantActivity(res);
4372
- const renderedProducts = products.map((p) => ({
4373
- name: p.name || TEXT.unknown,
4374
- image: p.icon_url || "",
4375
- time_label: formatProductWindow(p),
4376
- category: classifyMerchantItem(p)
7224
+ function getRandomGoodsMaps(res) {
7225
+ const priceMap = /* @__PURE__ */ new Map();
7226
+ const limitMap = /* @__PURE__ */ new Map();
7227
+ const randomGoods = Array.isArray(res?.random_goods) ? res.random_goods : Array.isArray(res?.randomGoods) ? res.randomGoods : [];
7228
+ for (const item of randomGoods) {
7229
+ const name2 = String(item?.goods_name || item?.name || "").trim();
7230
+ if (!name2) continue;
7231
+ if (item?.price !== void 0 && item?.price !== null && item.price !== "") priceMap.set(name2, item.price);
7232
+ if (item?.buy_limit_num !== void 0 && item?.buy_limit_num !== null && item.buy_limit_num !== "") {
7233
+ limitMap.set(name2, item.buy_limit_num);
7234
+ }
7235
+ }
7236
+ return { priceMap, limitMap };
7237
+ }
7238
+ __name(getRandomGoodsMaps, "getRandomGoodsMaps");
7239
+ function normalizeMerchantProducts(res, now = /* @__PURE__ */ new Date()) {
7240
+ return getMerchantProducts(res).map((item) => {
7241
+ const name2 = String(item?.name || item?.goods_name || TEXT.unknown).trim() || TEXT.unknown;
7242
+ return {
7243
+ name: name2,
7244
+ image: String(item?.icon_url || item?.iconUrl || ""),
7245
+ time_label: formatProductWindow(item),
7246
+ category: classifyMerchantItem(item),
7247
+ round_id: getRoundForItem(item, now),
7248
+ is_active: isMerchantItemActive(item, now),
7249
+ start_time: normalizeTimestamp(item?.start_time),
7250
+ end_time: normalizeTimestamp(item?.end_time)
7251
+ };
7252
+ });
7253
+ }
7254
+ __name(normalizeMerchantProducts, "normalizeMerchantProducts");
7255
+ function buildMerchantCardItems(products, res, options) {
7256
+ const { priceMap, limitMap } = getRandomGoodsMaps(res);
7257
+ const catOrder = { round: 0, normal: 1, weekend: 2 };
7258
+ const startY = 592;
7259
+ const cardHeight = 308;
7260
+ const gap = 43;
7261
+ const goodsAll = products.map((product) => {
7262
+ const limit = limitMap.get(product.name);
7263
+ const limitText = limit === void 0 || limit === null || limit === "" ? "--" : String(limit);
7264
+ const isHot = product.category !== "round";
7265
+ const isEnded = options.includeEnded ? !product.is_active : false;
7266
+ const roundPrefix = product.round_id ? `第${product.round_id}轮·` : "";
7267
+ const remainingStr = product.category === "normal" ? `本日限购${limitText}个` : product.category === "weekend" ? `活动期间限购${limitText}个` : `${isEnded ? roundPrefix : ""}本轮限购${limitText}个`;
7268
+ return {
7269
+ goods_name: product.name,
7270
+ iconUrl: product.image,
7271
+ price: priceMap.get(product.name) ?? 0,
7272
+ num: "",
7273
+ category: product.category,
7274
+ roundId: product.round_id || 0,
7275
+ isHot,
7276
+ isEnded,
7277
+ remainingStr,
7278
+ top: 0
7279
+ };
7280
+ });
7281
+ goodsAll.sort((a, b) => {
7282
+ if (options.includeEnded && a.isEnded !== b.isEnded) return a.isEnded ? 1 : -1;
7283
+ if (a.category !== b.category) return catOrder[a.category] - catOrder[b.category];
7284
+ return Number(b.price || 0) - Number(a.price || 0);
7285
+ });
7286
+ const goods = goodsAll.map((item, index) => ({
7287
+ ...item,
7288
+ num: String(index + 1).padStart(2, "0"),
7289
+ top: startY + index * (cardHeight + gap)
4377
7290
  }));
4378
- const categoryMap = {
4379
- normal: [],
4380
- round: [],
4381
- weekend: []
4382
- };
4383
- for (const product of renderedProducts) {
4384
- categoryMap[product.category].push(product);
7291
+ const lastCardTop = goods.length > 0 ? goods[goods.length - 1].top : startY;
7292
+ const bottomFrameTop = lastCardTop + 287;
7293
+ const pageHeight = bottomFrameTop + 160;
7294
+ return { goods, bottomFrameTop, pageHeight };
7295
+ }
7296
+ __name(buildMerchantCardItems, "buildMerchantCardItems");
7297
+ function getCurrentMerchantProducts(products, roundInfo) {
7298
+ return products.filter((product) => {
7299
+ if (!product.is_active) return false;
7300
+ if (product.category === "round") return !!roundInfo.current && product.round_id === roundInfo.current;
7301
+ return true;
7302
+ });
7303
+ }
7304
+ __name(getCurrentMerchantProducts, "getCurrentMerchantProducts");
7305
+ function buildMerchantFallbackText(title, products, roundInfo) {
7306
+ const fallbackLines = [title];
7307
+ if (roundInfo) {
7308
+ fallbackLines.push(`轮次:第 ${roundInfo.current || TEXT.notOpen} / ${roundInfo.total} 轮`);
7309
+ fallbackLines.push(`剩余:${roundInfo.countdown}`);
7310
+ }
7311
+ fallbackLines.push("");
7312
+ if (!products.length) {
7313
+ fallbackLines.push(roundInfo ? "当前轮次暂无商品。" : "今日暂无已公布的远行商人商品。");
7314
+ return fallbackLines.join("\n").trimEnd();
7315
+ }
7316
+ for (const key of CATEGORY_ORDER) {
7317
+ const group = products.filter((product) => product.category === key);
7318
+ if (!group.length) continue;
7319
+ fallbackLines.push(`【${CATEGORY_LABELS[key]}】`);
7320
+ group.forEach((product, index) => {
7321
+ const tail = product.time_label ? ` (${product.time_label})` : "";
7322
+ fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
7323
+ });
7324
+ fallbackLines.push("");
4385
7325
  }
4386
- const categories = CATEGORY_ORDER.filter((key) => categoryMap[key].length > 0).map((key) => ({
4387
- key,
4388
- label: CATEGORY_LABELS[key],
4389
- products: categoryMap[key]
7326
+ return fallbackLines.join("\n").trimEnd();
7327
+ }
7328
+ __name(buildMerchantFallbackText, "buildMerchantFallbackText");
7329
+ function getMerchantActivityTitle(res) {
7330
+ const activity = getMerchantActivity(res);
7331
+ return String(activity?.name || TEXT.merchant).trim() || TEXT.merchant;
7332
+ }
7333
+ __name(getMerchantActivityTitle, "getMerchantActivityTitle");
7334
+ function getMerchantActivitySubtitle(res) {
7335
+ const activity = getMerchantActivity(res);
7336
+ return String(activity?.start_date || "每日 08:00 / 12:00 / 16:00 / 20:00 刷新").trim();
7337
+ }
7338
+ __name(getMerchantActivitySubtitle, "getMerchantActivitySubtitle");
7339
+ function getLegacyRoundGroups(now, roundInfo) {
7340
+ return ROUND_WINDOWS.map((win) => ({
7341
+ round_id: win.id,
7342
+ label: `${padNumber(win.startHour)}:00 - ${padNumber(win.endHour)}:00`,
7343
+ is_current: roundInfo.current === win.id,
7344
+ products: []
4390
7345
  }));
7346
+ }
7347
+ __name(getLegacyRoundGroups, "getLegacyRoundGroups");
7348
+ function buildLegacyCategoryGroups(products, roundGroups) {
7349
+ return CATEGORY_ORDER.map((key) => {
7350
+ const groups = roundGroups.map((group) => ({
7351
+ round_id: group.round_id,
7352
+ label: group.label,
7353
+ is_current: group.is_current,
7354
+ products: group.products.filter((product) => product.category === key)
7355
+ })).filter((group) => group.products.length > 0);
7356
+ return {
7357
+ key,
7358
+ label: CATEGORY_LABELS[key],
7359
+ roundGroups: groups,
7360
+ product_count: groups.reduce((sum, group) => sum + group.products.length, 0)
7361
+ };
7362
+ }).filter((category) => category.product_count > 0);
7363
+ }
7364
+ __name(buildLegacyCategoryGroups, "buildLegacyCategoryGroups");
7365
+ function getMerchantDateStr(now = /* @__PURE__ */ new Date()) {
7366
+ const parts = getChinaParts(now);
7367
+ return `${parts.month}.${parts.day}`;
7368
+ }
7369
+ __name(getMerchantDateStr, "getMerchantDateStr");
7370
+ function buildMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
7371
+ const allProducts = normalizeMerchantProducts(res, now);
7372
+ const roundInfo = getCurrentMerchantRound(now);
7373
+ const products = getCurrentMerchantProducts(allProducts, roundInfo);
7374
+ const currentWindow = ROUND_WINDOWS.find((win) => win.id === roundInfo.current);
7375
+ const timeRange = currentWindow ? `${padNumber(currentWindow.startHour)}:00-${padNumber(currentWindow.endHour)}:00` : "--:--~--:--";
7376
+ const data = {
7377
+ dateStr: getMerchantDateStr(now),
7378
+ timeRange,
7379
+ ...buildMerchantCardItems(products, res, { includeEnded: false })
7380
+ };
7381
+ const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
7382
+ return { products, roundInfo, data, fallback };
7383
+ }
7384
+ __name(buildMerchantRenderPayload, "buildMerchantRenderPayload");
7385
+ function buildLegacyMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
7386
+ const allProducts = normalizeMerchantProducts(res, now);
7387
+ const roundInfo = getCurrentMerchantRound(now);
7388
+ const products = getCurrentMerchantProducts(allProducts, roundInfo);
7389
+ const roundGroups = getLegacyRoundGroups(now, roundInfo);
7390
+ const currentGroup = roundGroups.find((group) => group.round_id === roundInfo.current) || roundGroups[0];
7391
+ currentGroup.products.push(...products);
7392
+ const categories = buildLegacyCategoryGroups(products, roundGroups);
4391
7393
  const data = {
4392
7394
  background: "",
4393
- title: activity.name || TEXT.merchant,
4394
- subtitle: activity.start_date || "每日 08:00 / 12:00 / 16:00 / 20:00 刷新",
4395
- titleIcon: true,
4396
- product_count: renderedProducts.length,
4397
- round_info: roundInfo,
7395
+ title: getMerchantActivityTitle(res),
7396
+ subtitle: getMerchantActivitySubtitle(res),
4398
7397
  categories,
4399
- products: renderedProducts
7398
+ roundGroups,
7399
+ total_products: products.length
4400
7400
  };
4401
- const fallbackLines = [];
4402
- if (renderedProducts.length) {
4403
- fallbackLines.push(`远行商人当前商品(共 ${renderedProducts.length} 件)`);
4404
- fallbackLines.push(`轮次:${roundInfo.current || TEXT.notOpen},剩余:${roundInfo.countdown}`);
4405
- fallbackLines.push("");
4406
- for (const cat of categories) {
4407
- fallbackLines.push(`【${cat.label}】`);
4408
- cat.products.forEach((product, index) => {
4409
- const tail = product.time_label ? ` (${product.time_label})` : "";
4410
- fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
4411
- });
4412
- fallbackLines.push("");
4413
- }
4414
- } else {
4415
- fallbackLines.push("当前远行商人暂无商品。");
4416
- }
4417
- const fallback = fallbackLines.join("\n").trimEnd();
4418
- return { products: renderedProducts, roundInfo, data, fallback };
7401
+ const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
7402
+ return { products, roundInfo, data, fallback };
7403
+ }
7404
+ __name(buildLegacyMerchantRenderPayload, "buildLegacyMerchantRenderPayload");
7405
+ function buildTodayMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
7406
+ const products = normalizeMerchantProducts(res, now).filter((product) => {
7407
+ const source = {
7408
+ start_time: product.start_time,
7409
+ end_time: product.end_time
7410
+ };
7411
+ return isMerchantItemToday(source, now);
7412
+ });
7413
+ const data = {
7414
+ dateStr: getMerchantDateStr(now),
7415
+ ...buildMerchantCardItems(products, res, { includeEnded: true })
7416
+ };
7417
+ const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, products);
7418
+ return { products, data, fallback };
7419
+ }
7420
+ __name(buildTodayMerchantRenderPayload, "buildTodayMerchantRenderPayload");
7421
+ function buildLegacyTodayMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
7422
+ const allProducts = normalizeMerchantProducts(res, now).filter((product) => {
7423
+ const source = {
7424
+ start_time: product.start_time,
7425
+ end_time: product.end_time
7426
+ };
7427
+ return isMerchantItemToday(source, now);
7428
+ });
7429
+ const roundInfo = getCurrentMerchantRound(now);
7430
+ const roundGroups = getLegacyRoundGroups(now, roundInfo);
7431
+ for (const product of allProducts) {
7432
+ const roundId = product.category === "round" ? product.round_id : null;
7433
+ const group = roundId ? roundGroups.find((item) => item.round_id === roundId) : roundGroups[0];
7434
+ if (group) group.products.push(product);
7435
+ }
7436
+ const categories = buildLegacyCategoryGroups(allProducts, roundGroups);
7437
+ const data = {
7438
+ background: "",
7439
+ title: TEXT.todayMerchant,
7440
+ subtitle: `${getChinaDateText(now)} · 每日 08:00 / 12:00 / 16:00 / 20:00 刷新`,
7441
+ categories,
7442
+ roundGroups,
7443
+ total_products: allProducts.length
7444
+ };
7445
+ const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, allProducts);
7446
+ return { products: allProducts, data, fallback };
4419
7447
  }
4420
- __name(buildMerchantRenderPayload, "buildMerchantRenderPayload");
7448
+ __name(buildLegacyTodayMerchantRenderPayload, "buildLegacyTodayMerchantRenderPayload");
7449
+ function useLegacyMerchantUi(deps) {
7450
+ return deps.config.merchantUiStyle === "old";
7451
+ }
7452
+ __name(useLegacyMerchantUi, "useLegacyMerchantUi");
7453
+ function buildConfiguredMerchantRenderPayload(deps, res, now = /* @__PURE__ */ new Date()) {
7454
+ const payload = useLegacyMerchantUi(deps) ? buildLegacyMerchantRenderPayload(res, now) : buildMerchantRenderPayload(res, now);
7455
+ return {
7456
+ ...payload,
7457
+ templateName: useLegacyMerchantUi(deps) ? "yuanxing-shangren" : "yuanxing-shangren/merchant"
7458
+ };
7459
+ }
7460
+ __name(buildConfiguredMerchantRenderPayload, "buildConfiguredMerchantRenderPayload");
7461
+ function buildConfiguredTodayMerchantRenderPayload(deps, res, now = /* @__PURE__ */ new Date()) {
7462
+ const payload = useLegacyMerchantUi(deps) ? buildLegacyTodayMerchantRenderPayload(res, now) : buildTodayMerchantRenderPayload(res, now);
7463
+ return {
7464
+ ...payload,
7465
+ templateName: useLegacyMerchantUi(deps) ? "yuanxing-shangren" : "yuanxing-shangren/today"
7466
+ };
7467
+ }
7468
+ __name(buildConfiguredTodayMerchantRenderPayload, "buildConfiguredTodayMerchantRenderPayload");
4421
7469
  async function checkMerchantSubscriptions(deps) {
4422
7470
  const { ctx, client, merchantSubMgr, renderer, config } = deps;
4423
7471
  const res = await client.getMerchantInfo(ctx, true);
4424
7472
  if (!res) return { subscriptions: 0, matched: 0, pushed: 0 };
4425
- const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
7473
+ const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
4426
7474
  const productNames = products.map((p) => p.name || "").filter(Boolean);
4427
- const rendered = await renderer.renderHtml(ctx, "yuanxing-shangren", data);
7475
+ const rendered = await renderer.renderHtml(ctx, templateName, data);
4428
7476
  const renderedImage = rendered ? compressPngImage(rendered, config) : null;
4429
7477
  const subs = merchantSubMgr.getAll();
4430
7478
  let matchedCount = 0;
@@ -4447,7 +7495,7 @@ async function checkMerchantSubscriptions(deps) {
4447
7495
  const platform = sub.platform || ctx.bots[0]?.platform;
4448
7496
  const channelId = sub.channel_id || sub.group_id || sub.user_id || key;
4449
7497
  if (!platform || !channelId) {
4450
- logger9.warn(`推送失败 ${key}: 无法确定平台或频道`);
7498
+ logger11.warn(`推送失败 ${key}: 无法确定平台或频道`);
4451
7499
  continue;
4452
7500
  }
4453
7501
  const fallbackText = `${msg}
@@ -4471,13 +7519,20 @@ ${fallback}`;
4471
7519
  __name(checkMerchantSubscriptions, "checkMerchantSubscriptions");
4472
7520
  function register3(deps) {
4473
7521
  const { ctx, config, client, merchantSubMgr } = deps;
4474
- ctx.command(TEXT.merchant, "查看远行商人商品").action(async ({ session }) => {
7522
+ ctx.command(TEXT.merchant, "查看远行商人商品").alias("yxsr").action(async ({ session }) => {
4475
7523
  const res = await client.getMerchantInfo(ctx, true);
4476
7524
  if (!res) return `获取远行商人数据失败:${client.getLastErrorBrief()}`;
4477
- const { data, fallback } = buildMerchantRenderPayload(res);
4478
- const png = await deps.renderer.renderHtml(ctx, "yuanxing-shangren", data);
7525
+ const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
7526
+ const png = await deps.renderer.renderHtml(ctx, templateName, data);
4479
7527
  await sendImageWithFallback(session, png, fallback, "merchant:yuanxing-shangren", deps.config);
4480
7528
  });
7529
+ ctx.command(TEXT.todayMerchant, "查看今日远行商人全部商品").action(async ({ session }) => {
7530
+ const res = await client.getMerchantInfo(ctx, true);
7531
+ if (!res) return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
7532
+ const { data, fallback, templateName } = buildConfiguredTodayMerchantRenderPayload(deps, res);
7533
+ const png = await deps.renderer.renderHtml(ctx, templateName, data);
7534
+ await sendImageWithFallback(session, png, fallback, "merchant:yuanxing-shangren:today", deps.config);
7535
+ });
4481
7536
  ctx.command(`${TEXT.subscribe} [args:text]`, "订阅远行商人商品提醒").action(async ({ session }, args) => {
4482
7537
  const target = getSubscriptionTarget(session);
4483
7538
  if (!target.privateChat && !isBotAdmin2(session, config.adminUserIds)) return "此指令仅限管理员使用。";
@@ -4523,6 +7578,13 @@ function register3(deps) {
4523
7578
  return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
4524
7579
  });
4525
7580
  if (config.merchantSubscriptionEnabled) {
7581
+ const MERCHANT_JITTER_MS = 3e4;
7582
+ const runCheckWithJitter = /* @__PURE__ */ __name(() => {
7583
+ const jitter = Math.floor(Math.random() * MERCHANT_JITTER_MS);
7584
+ setTimeout(() => {
7585
+ checkMerchantSubscriptions(deps).catch((err) => logger11.warn(`远行商人订阅检查失败: ${err}`));
7586
+ }, jitter);
7587
+ }, "runCheckWithJitter");
4526
7588
  if (config.merchantCheckMode === "times" && config.merchantCheckTimes.length > 0) {
4527
7589
  let lastMerchantCheckKey = "";
4528
7590
  ctx.setInterval(async () => {
@@ -4532,11 +7594,11 @@ function register3(deps) {
4532
7594
  const checkKey = `${now.toDateString()}-${timeStr}`;
4533
7595
  if (checkKey === lastMerchantCheckKey) return;
4534
7596
  lastMerchantCheckKey = checkKey;
4535
- await checkMerchantSubscriptions(deps);
7597
+ runCheckWithJitter();
4536
7598
  }, 6e4);
4537
7599
  } else {
4538
7600
  ctx.setInterval(async () => {
4539
- await checkMerchantSubscriptions(deps);
7601
+ runCheckWithJitter();
4540
7602
  }, config.merchantCheckInterval);
4541
7603
  }
4542
7604
  }
@@ -4544,11 +7606,156 @@ function register3(deps) {
4544
7606
  __name(register3, "register");
4545
7607
 
4546
7608
  // src/commands/wiki.ts
4547
- var WIKI_CLOSED_MESSAGE = "该功能暂时关闭";
7609
+ var import_koishi12 = require("koishi");
7610
+ var import_node_fs6 = __toESM(require("node:fs"));
7611
+ var logger12 = new import_koishi12.Logger("rocom-wiki-cmd");
7612
+ async function sendImage2(deps, session, templateName, data, fallback) {
7613
+ const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
7614
+ await sendImageWithFallback(session, png, fallback, `wiki:${templateName}`, deps.config);
7615
+ }
7616
+ __name(sendImage2, "sendImage");
7617
+ async function handleWikiQuery(deps, session, rawText) {
7618
+ const { ctx, wikiService } = deps;
7619
+ const trimmed = String(rawText || "").trim();
7620
+ const rawKey = trimmed.toLowerCase();
7621
+ const catalogsMeta = await wikiService.getCatalogsPayload();
7622
+ if (!trimmed || ["帮助", "help", "类型", "目录", "专题"].includes(rawKey)) {
7623
+ const optionsMeta = await wikiService.getOptionsPayload();
7624
+ const data2 = wikiService.buildCatalogRenderData(catalogsMeta, optionsMeta);
7625
+ await sendImage2(deps, session, "wiki/menu", data2, wikiService.catalogUsageText());
7626
+ return;
7627
+ }
7628
+ let { catalog, query, pageNo } = wikiService.parseWikiCommand(trimmed);
7629
+ if (catalog) {
7630
+ catalog = wikiService.catalogForKeyFromPayload(String(catalog.key || ""), catalogsMeta) || catalog;
7631
+ }
7632
+ if (!catalog) {
7633
+ const { parts, pageNo: parsedPageNo } = splitWikiCommandParts(trimmed);
7634
+ if (parts.length) {
7635
+ const dynamicCatalog = wikiService.dynamicCatalogByToken(parts[0], catalogsMeta);
7636
+ if (dynamicCatalog) {
7637
+ catalog = dynamicCatalog;
7638
+ query = parts.slice(1).join(" ").trim();
7639
+ pageNo = parsedPageNo;
7640
+ }
7641
+ }
7642
+ }
7643
+ if (!catalog) {
7644
+ const { payload: payload2, mode: mode2, error: error2 } = await wikiService.fetchGlobalSearch(query, pageNo);
7645
+ if (error2) return error2;
7646
+ if (mode2 === "global-detail") {
7647
+ const sourceCatalog = payload2?._catalog || {};
7648
+ const detail = payload2?.item && typeof payload2.item === "object" ? payload2.item : {};
7649
+ const sourceKey = String(sourceCatalog?.key || "");
7650
+ if (sourceKey === "pets") {
7651
+ const sections = await wikiService.fetchWikiPetSections(detail.pet_id);
7652
+ const data4 = buildWikiPetRenderData(detail, sections.profile, sections.skills, sections.family, sections.handbook, query);
7653
+ await sendImage2(deps, session, "wiki/pet", data4, [
7654
+ `${data4.name} #${data4.number}`,
7655
+ `属性:${data4.type_names.join(" / ") || "暂无"}`,
7656
+ `蛋组:${data4.egg_groups.join(" / ") || "暂无"}`,
7657
+ data4.description
7658
+ ].join("\n"));
7659
+ return;
7660
+ }
7661
+ if (sourceKey === "skills") {
7662
+ const pets = await deps.client.getWikiSkillPets(ctx, detail.skill_id);
7663
+ const data4 = buildWikiSkillRenderData(detail, pets && typeof pets === "object" ? pets : {}, query);
7664
+ await sendImage2(deps, session, "wiki/skill", data4, `${data4.name} #${data4.skill_id}
7665
+ ${data4.description}`);
7666
+ return;
7667
+ }
7668
+ const data3 = wikiService.buildGenericRenderData(sourceCatalog?.key ? sourceCatalog : { title: "全局搜索", key: "global" }, detail, query, "detail", pageNo);
7669
+ await sendImage2(deps, session, "wiki/detail", data3, `${data3.title}
7670
+ ${data3.summary || ""}`);
7671
+ return;
7672
+ }
7673
+ const data2 = wikiService.buildGenericRenderData({ title: "全局搜索", key: "global" }, payload2, query, "global-search", pageNo);
7674
+ await sendImage2(deps, session, "wiki/list", data2, `${data2.title}
7675
+ ${data2.summary || ""}`);
7676
+ return;
7677
+ }
7678
+ if (["pets", "pet"].includes(catalog.key) && query) {
7679
+ const { detail: overview, candidates, error: error2 } = await wikiService.resolveWikiPet(query);
7680
+ if (error2) return error2;
7681
+ if (candidates.length) return wikiCandidateText(query, candidates, "精灵");
7682
+ if (!overview) return "获取 Wiki 精灵详情失败:接口未返回有效数据。";
7683
+ const sections = await wikiService.fetchWikiPetSections(overview.pet_id);
7684
+ const data2 = buildWikiPetRenderData(overview, sections.profile, sections.skills, sections.family, sections.handbook, query);
7685
+ await sendImage2(deps, session, "wiki/pet", data2, `${data2.name} #${data2.number}
7686
+ ${data2.description}`);
7687
+ return;
7688
+ }
7689
+ if (["skills", "skill"].includes(catalog.key) && query) {
7690
+ const { detail, candidates, error: error2 } = await wikiService.resolveWikiSkill(query);
7691
+ if (error2) return error2;
7692
+ if (candidates.length) return wikiCandidateText(query, candidates, "技能");
7693
+ if (!detail) return "获取技能详情失败:接口未返回有效数据。";
7694
+ const pets = await deps.client.getWikiSkillPets(ctx, detail.skill_id);
7695
+ const data2 = buildWikiSkillRenderData(detail, pets && typeof pets === "object" ? pets : {}, query);
7696
+ await sendImage2(deps, session, "wiki/skill", data2, `${data2.name} #${data2.skill_id}
7697
+ ${data2.description}`);
7698
+ return;
7699
+ }
7700
+ const { payload, mode, error } = await wikiService.fetchGenericCatalog(catalog, query, pageNo);
7701
+ if (error) return error;
7702
+ const data = wikiService.buildGenericRenderData(catalog, payload, query, mode, pageNo);
7703
+ let templateName = "wiki/detail";
7704
+ if (mode === "list") templateName = "wiki/list";
7705
+ else if (mode === "suggestions") templateName = "wiki/suggestions";
7706
+ await sendImage2(deps, session, templateName, data, `${data.title}
7707
+ ${data.summary || ""}`);
7708
+ }
7709
+ __name(handleWikiQuery, "handleWikiQuery");
4548
7710
  function register4(deps) {
4549
- const { ctx } = deps;
4550
- ctx.command("洛克").subcommand(".wiki <name:text>", "查询精灵 wiki").alias("洛克wiki").action(() => WIKI_CLOSED_MESSAGE);
4551
- ctx.command("洛克").subcommand(".技能 <name:text>", "查询技能 wiki").alias("洛克技能").action(() => WIKI_CLOSED_MESSAGE);
7711
+ const { ctx, atlasService } = deps;
7712
+ ctx.command("洛克").subcommand(".wiki [name:text]", "查询洛克 Wiki(精灵/技能/物品等,支持全局搜索)").alias("洛克wiki").alias("洛克百科").action(async ({ session }, name2 = "") => handleWikiQuery(deps, session, name2));
7713
+ ctx.command("洛克").subcommand(".技能 <name:text>", "查询技能 Wiki").alias("洛克技能").action(async ({ session }, name2 = "") => handleWikiQuery(deps, session, `技能 ${String(name2 || "").trim()}`));
7714
+ ctx.command("图鉴下载", "下载 Rocom-Atlas 精灵图鉴到本地缓存").alias("洛克图鉴下载").action(async ({ session }) => {
7715
+ if (!deps.config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
7716
+ await session?.send?.("开始下载 Rocom-Atlas 图鉴,请稍候…");
7717
+ let lastPercent = -20;
7718
+ try {
7719
+ const { imageCount, totalBytes } = await atlasService.download(ctx, async (percent, stage) => {
7720
+ if (percent - lastPercent >= 20 || percent >= 100) {
7721
+ lastPercent = percent;
7722
+ await session?.send?.(`图鉴下载进度 ${percent}%:${stage}`);
7723
+ }
7724
+ });
7725
+ const sizeMb = (totalBytes / 1024 / 1024).toFixed(1);
7726
+ return `图鉴下载完成:共 ${imageCount} 张精灵图片,占用约 ${sizeMb} MB。
7727
+ 使用:精灵图鉴 <精灵名>`;
7728
+ } catch (e) {
7729
+ logger12.warn(`图鉴下载失败: ${e}`);
7730
+ return `图鉴下载失败:${e instanceof Error ? e.message : e}`;
7731
+ }
7732
+ });
7733
+ ctx.command("精灵图鉴 <name:text>", "使用本地 Rocom-Atlas 查询精灵图鉴图片").alias("洛克精灵图鉴").action(async ({ session }, name2 = "") => {
7734
+ const query = String(name2 || "").trim();
7735
+ if (!query) return "请输入精灵名称。用法:精灵图鉴 <精灵名>";
7736
+ if (!atlasService.isReady()) {
7737
+ return "本地图鉴尚未下载。请管理员先执行:图鉴下载";
7738
+ }
7739
+ const { name: matchedName, imagePath, candidates } = atlasService.findMatch(query);
7740
+ if (imagePath) {
7741
+ try {
7742
+ const image = import_node_fs6.default.readFileSync(imagePath);
7743
+ await session?.send?.((0, import_koishi12.h)("message", {}, import_koishi12.h.text(`【精灵图鉴】${matchedName}
7744
+ `), import_koishi12.h.image(image, "image/png")));
7745
+ return;
7746
+ } catch (e) {
7747
+ logger12.warn(`读取图鉴图片失败: ${e}`);
7748
+ return `读取图鉴图片失败:${e instanceof Error ? e.message : e}`;
7749
+ }
7750
+ }
7751
+ if (candidates.length) {
7752
+ const lines = [`未精确匹配「${query}」,可能是:`];
7753
+ candidates.forEach((item, idx) => lines.push(`${idx + 1}. ${item}`));
7754
+ lines.push("请使用更精确名称重新查询。");
7755
+ return lines.join("\n");
7756
+ }
7757
+ return `未在本地图鉴中找到「${query}」。可尝试其他名称,或联系管理员更新图鉴(图鉴下载)。`;
7758
+ });
4552
7759
  }
4553
7760
  __name(register4, "register");
4554
7761
 
@@ -4743,7 +7950,7 @@ function register5(deps) {
4743
7950
  __name(register5, "register");
4744
7951
 
4745
7952
  // src/commands/tools.ts
4746
- var import_koishi10 = require("koishi");
7953
+ var import_koishi13 = require("koishi");
4747
7954
 
4748
7955
  // src/activities-service.ts
4749
7956
  var CHINA_TIMEZONE2 = "Asia/Shanghai";
@@ -5031,7 +8238,7 @@ var ActivitiesService = class {
5031
8238
  };
5032
8239
 
5033
8240
  // src/commands/tools.ts
5034
- var logger10 = new import_koishi10.Logger("rocom-tools");
8241
+ var logger13 = new import_koishi13.Logger("rocom-tools");
5035
8242
  var activitiesService = new ActivitiesService();
5036
8243
  function trimText2(value) {
5037
8244
  return String(value ?? "").trim();
@@ -5070,6 +8277,63 @@ function announcementId(item) {
5070
8277
  return trimText2(item?.thread_id || item?.id);
5071
8278
  }
5072
8279
  __name(announcementId, "announcementId");
8280
+ function announcementTimestamp(item) {
8281
+ for (const key of ["published_at_ts", "publish_at_ts", "created_at_ts"]) {
8282
+ const value = Number(item?.[key]);
8283
+ if (Number.isFinite(value) && value > 0) return value;
8284
+ }
8285
+ for (const key of ["publishAt", "published_at", "createdAt"]) {
8286
+ const text = trimText2(item?.[key]);
8287
+ if (!text) continue;
8288
+ const date = new Date(text);
8289
+ if (!Number.isNaN(date.getTime())) return Math.floor(date.getTime() / 1e3);
8290
+ }
8291
+ return 0;
8292
+ }
8293
+ __name(announcementTimestamp, "announcementTimestamp");
8294
+ function announcementSubscriptionKey(session) {
8295
+ const privateChat = !session?.guildId;
8296
+ return [session?.platform || "", session?.channelId || "private", privateChat ? session?.userId || "" : "", "announcement"].join(":");
8297
+ }
8298
+ __name(announcementSubscriptionKey, "announcementSubscriptionKey");
8299
+ async function checkAnnouncementSubscriptions(deps) {
8300
+ const { ctx, client, announcementSubMgr } = deps;
8301
+ const subs = announcementSubMgr.getAll();
8302
+ const subEntries = Object.entries(subs);
8303
+ if (!subEntries.length) return { subscriptions: 0, pushed: 0 };
8304
+ const latest = await client.getLatestAnnouncement(ctx, { category_id: 99 });
8305
+ const item = latest?.detail || latest?.announcement || latest;
8306
+ const latestId = announcementId(item);
8307
+ if (!latestId) return { subscriptions: subEntries.length, pushed: 0 };
8308
+ const latestTs = announcementTimestamp(item);
8309
+ let pushed = 0;
8310
+ for (const [key, sub] of subEntries) {
8311
+ if (latestId === String(sub.last_id || "")) continue;
8312
+ if (latestTs && sub.since_ts && latestTs <= sub.since_ts) continue;
8313
+ const message = [`【洛克王国新公告】`, buildAnnouncementDetailText(latest)].join("\n");
8314
+ try {
8315
+ const sent = await sendScheduledMessage(ctx, {
8316
+ platform: sub.platform,
8317
+ channelId: sub.channel_id || sub.guild_id || sub.user_id || "",
8318
+ guildId: sub.guild_id || "",
8319
+ userId: sub.user_id || ""
8320
+ }, message);
8321
+ if (!sent) continue;
8322
+ } catch (e) {
8323
+ logger13.warn(`公告订阅推送失败: ${e}`);
8324
+ continue;
8325
+ }
8326
+ pushed++;
8327
+ announcementSubMgr.upsert(key, {
8328
+ ...sub,
8329
+ last_id: latestId,
8330
+ since_ts: latestTs || Math.floor(Date.now() / 1e3),
8331
+ updated_at: Math.floor(Date.now() / 1e3)
8332
+ });
8333
+ }
8334
+ return { subscriptions: subEntries.length, pushed };
8335
+ }
8336
+ __name(checkAnnouncementSubscriptions, "checkAnnouncementSubscriptions");
5073
8337
  function buildAnnouncementListText(data, page) {
5074
8338
  const list = firstArray(data, ["list", "items"]);
5075
8339
  if (!list.length) return page > 1 ? "该页没有更多公告。" : "当前没有公告数据。";
@@ -5145,11 +8409,45 @@ function register6(deps) {
5145
8409
  if (!data) return `公告详情查询失败:${client.getLastErrorBrief()}`;
5146
8410
  return buildAnnouncementDetailText(data);
5147
8411
  });
8412
+ ctx.command("订阅洛克公告", "订阅洛克王国新公告推送").action(async ({ session }) => {
8413
+ const privateChat = !session?.guildId;
8414
+ if (!privateChat && !config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
8415
+ const key = announcementSubscriptionKey(session);
8416
+ const latest = await client.getLatestAnnouncement(ctx, { category_id: 99 }, session?.userId || "");
8417
+ const item = latest?.detail || latest?.announcement || latest;
8418
+ deps.announcementSubMgr.upsert(key, {
8419
+ key,
8420
+ platform: session?.platform || "",
8421
+ channel_id: session?.channelId || "",
8422
+ guild_id: session?.guildId || "",
8423
+ user_id: privateChat ? session?.userId || "" : "",
8424
+ updated_by: session?.userId || "",
8425
+ last_id: announcementId(item),
8426
+ since_ts: announcementTimestamp(item) || Math.floor(Date.now() / 1e3),
8427
+ updated_at: Math.floor(Date.now() / 1e3)
8428
+ });
8429
+ return "已订阅洛克王国新公告推送,检测到新公告时会在当前会话提醒。";
8430
+ });
8431
+ ctx.command("取消订阅洛克公告", "取消洛克王国新公告推送").action(async ({ session }) => {
8432
+ const privateChat = !session?.guildId;
8433
+ if (!privateChat && !config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
8434
+ const deleted = deps.announcementSubMgr.deleteMatching({
8435
+ platform: session?.platform || "",
8436
+ channelId: session?.channelId || "",
8437
+ userId: privateChat ? session?.userId || "" : ""
8438
+ });
8439
+ return deleted ? "已取消洛克公告订阅。" : "当前会话没有洛克公告订阅。";
8440
+ });
8441
+ if (config.announcementSubscriptionEnabled) {
8442
+ ctx.setInterval(() => {
8443
+ checkAnnouncementSubscriptions(deps).catch((err) => logger13.warn(`公告订阅检查失败: ${err}`));
8444
+ }, Math.max(1, config.announcementPollIntervalMinutes || 10) * 6e4);
8445
+ }
5148
8446
  ctx.command("洛克").subcommand(".同步配置", "手动同步 RoCom 远端配置").alias("洛克同步配置").action(async ({ session }) => {
5149
8447
  if (!config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
5150
8448
  const data = await client.syncConfig(ctx, session?.userId || "");
5151
8449
  if (!data) return `同步配置失败:${client.getLastErrorBrief()}`;
5152
- logger10.info(`manual config sync requested by ${session?.userId || "unknown"}`);
8450
+ logger13.info(`manual config sync requested by ${session?.userId || "unknown"}`);
5153
8451
  const skipped = Array.isArray(data?.skipped_resources) ? data.skipped_resources.length : 0;
5154
8452
  return skipped ? `配置同步完成,但有 ${skipped} 个资源跳过。` : "配置同步完成。";
5155
8453
  });
@@ -5157,8 +8455,8 @@ function register6(deps) {
5157
8455
  __name(register6, "register");
5158
8456
 
5159
8457
  // src/commands/admin.ts
5160
- var import_koishi11 = require("koishi");
5161
- var logger11 = new import_koishi11.Logger("rocom-admin");
8458
+ var import_koishi14 = require("koishi");
8459
+ var logger14 = new import_koishi14.Logger("rocom-admin");
5162
8460
  function register7(deps) {
5163
8461
  const { ctx, config, client, userMgr } = deps;
5164
8462
  function isAdmin(userId) {
@@ -5204,7 +8502,7 @@ function register7(deps) {
5204
8502
  for (const [userId, bindings] of Object.entries(allUsers)) {
5205
8503
  const token = await getRoleToken(ctx, userId);
5206
8504
  if (!token?.fwt) {
5207
- logger11.warn(`用户 ${userId} 没有可用的 fwt,跳过失效检测`);
8505
+ logger14.warn(`用户 ${userId} 没有可用的 fwt,跳过失效检测`);
5208
8506
  totalSkipped += bindings.length;
5209
8507
  continue;
5210
8508
  }
@@ -5231,7 +8529,7 @@ function register7(deps) {
5231
8529
  try {
5232
8530
  await client.deleteBinding(ctx, binding.binding_id, userId);
5233
8531
  } catch (e) {
5234
- logger11.warn(`删除用户 ${userId} 服务端绑定 ${binding.binding_id} 失败:${e}`);
8532
+ logger14.warn(`删除用户 ${userId} 服务端绑定 ${binding.binding_id} 失败:${e}`);
5235
8533
  }
5236
8534
  }
5237
8535
  totalInvalid++;
@@ -5270,7 +8568,7 @@ function register7(deps) {
5270
8568
  });
5271
8569
  }
5272
8570
  } catch (e) {
5273
- logger11.warn(`自动刷新用户 ${userId} 失败: ${e}`);
8571
+ logger14.warn(`自动刷新用户 ${userId} 失败: ${e}`);
5274
8572
  }
5275
8573
  }
5276
8574
  }, 3e4);
@@ -5281,7 +8579,7 @@ __name(register7, "register");
5281
8579
  // src/index.ts
5282
8580
  var name = "rocom";
5283
8581
  var inject = { required: ["puppeteer", "database"] };
5284
- var logger12 = new import_koishi12.Logger("rocom");
8582
+ var logger15 = new import_koishi15.Logger("rocom");
5285
8583
  var MENU_PAGE_TITLE = "洛克王国帮助菜单";
5286
8584
  var MENU_PAGE_SUBTITLE = "输入对应命令可查看功能,部分数据查询需要先完成账号绑定。";
5287
8585
  var MENU_GROUPS = [
@@ -5309,6 +8607,7 @@ var MENU_GROUPS = [
5309
8607
  { cmd: "洛克.交换大厅", desc: "查看交换大厅" },
5310
8608
  { cmd: "洛克.玩家", desc: "查询 ingame 玩家资料" },
5311
8609
  { cmd: "洛克.家园", desc: "查询家园菜园" },
8610
+ { cmd: "洛克.家园详情", desc: "家园精灵完整数据" },
5312
8611
  { cmd: "洛克.商店", desc: "查询 ingame 商店" },
5313
8612
  { cmd: "洛克.日历", desc: "查看活动日历" },
5314
8613
  { cmd: "洛克.公告", desc: "查看公告列表" },
@@ -5324,14 +8623,19 @@ var MENU_GROUPS = [
5324
8623
  { cmd: "取消订阅远行商人", desc: "取消商人提醒" },
5325
8624
  { cmd: "订阅家园菜园", desc: "菜园成熟提醒" },
5326
8625
  { cmd: "订阅家园灵感", desc: "精灵灵感提醒" },
5327
- { cmd: "取消订阅家园", desc: "取消家园订阅" }
8626
+ { cmd: "订阅家园生蛋", desc: "精灵生蛋提醒" },
8627
+ { cmd: "取消订阅家园", desc: "取消家园订阅" },
8628
+ { cmd: "订阅洛克公告", desc: "新公告推送提醒" },
8629
+ { cmd: "取消订阅洛克公告", desc: "取消公告订阅" }
5328
8630
  ]
5329
8631
  },
5330
8632
  {
5331
8633
  groupTitle: "百科与查蛋",
5332
8634
  menuItems: [
5333
- { cmd: "洛克.wiki", desc: "精灵 Wiki 查询" },
8635
+ { cmd: "洛克.wiki", desc: "Wiki 全局/分类查询" },
5334
8636
  { cmd: "洛克.技能", desc: "技能 Wiki 查询" },
8637
+ { cmd: "精灵图鉴", desc: "本地图鉴查图" },
8638
+ { cmd: "图鉴下载", desc: "下载图鉴缓存(管理员)" },
5335
8639
  { cmd: "洛克.查蛋", desc: "精灵查蛋 / 尺寸反查" },
5336
8640
  { cmd: "洛克.配种", desc: "配种查询" }
5337
8641
  ]
@@ -5357,50 +8661,69 @@ function buildMenuFallbackText() {
5357
8661
  return lines.join("\n").trimEnd();
5358
8662
  }
5359
8663
  __name(buildMenuFallbackText, "buildMenuFallbackText");
5360
- var Config = import_koishi12.Schema.intersect([
5361
- import_koishi12.Schema.object({
5362
- apiBaseUrl: import_koishi12.Schema.string().default("https://wegame.shallow.ink").description("API 基础地址"),
5363
- wegameApiKey: import_koishi12.Schema.string().default("").description("WeGame API Key(获取 key 查看 Github:https://github.com/Entropy-Increase-Team/koishi-plugin-rocom )"),
5364
- qqLoginDebugMode: import_koishi12.Schema.boolean().default(false).description("QQ 扫码登录调试模式,仅调试时开启"),
5365
- adminUserIds: import_koishi12.Schema.array(String).default([]).description("管理员用户 ID 列表"),
5366
- autoRefreshEnabled: import_koishi12.Schema.boolean().default(false).description("启用自动刷新凭证"),
5367
- autoRefreshTime: import_koishi12.Schema.array(String).default(["00:00", "12:00"]).description("自动刷新时间")
8664
+ var Config = import_koishi15.Schema.intersect([
8665
+ import_koishi15.Schema.object({
8666
+ apiBaseUrl: import_koishi15.Schema.string().default("https://wegame.shallow.ink").description("API 基础地址"),
8667
+ wegameApiKey: import_koishi15.Schema.string().default("").description("WeGame API Key(获取 key 查看 Github:https://github.com/Entropy-Increase-Team/koishi-plugin-rocom )"),
8668
+ qqLoginDebugMode: import_koishi15.Schema.boolean().default(false).description("QQ 扫码登录调试模式,仅调试时开启"),
8669
+ adminUserIds: import_koishi15.Schema.array(String).default([]).description("管理员用户 ID 列表"),
8670
+ autoRefreshEnabled: import_koishi15.Schema.boolean().default(false).description("启用自动刷新凭证"),
8671
+ autoRefreshTime: import_koishi15.Schema.array(String).default(["00:00", "12:00"]).description("自动刷新时间")
5368
8672
  }).description("基础设置"),
5369
- import_koishi12.Schema.object({
5370
- imageCompressionEnabled: import_koishi12.Schema.boolean().default(true).description("发送图片前启用 PNG 无损压缩"),
5371
- imageCompressionMinBytes: import_koishi12.Schema.number().default(262144).description("触发压缩的最小图片大小,单位字节"),
5372
- imageCompressionLevel: import_koishi12.Schema.number().min(0).max(9).default(9).description("PNG zlib 压缩等级 0-9")
8673
+ import_koishi15.Schema.object({
8674
+ imageCompressionEnabled: import_koishi15.Schema.boolean().default(true).description("发送图片前启用 PNG 无损压缩"),
8675
+ imageCompressionMinBytes: import_koishi15.Schema.number().default(262144).description("触发压缩的最小图片大小,单位字节"),
8676
+ imageCompressionLevel: import_koishi15.Schema.number().min(0).max(9).default(9).description("PNG zlib 压缩等级 0-9")
5373
8677
  }).description("图片压缩设置"),
5374
- import_koishi12.Schema.object({
5375
- merchantSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("启用远行商人订阅"),
5376
- merchantSubscriptionItems: import_koishi12.Schema.array(String).default(["国王球", "棱镜球", "炫彩精灵蛋"]).description("默认订阅商品"),
5377
- merchantCheckMode: import_koishi12.Schema.union(["interval", "times"]).default("interval").description("商人检查模式:interval 定期轮询 / times 指定时间点"),
5378
- merchantCheckInterval: import_koishi12.Schema.number().default(3e5).description("商人检查间隔,单位毫秒(仅 interval 模式生效)"),
5379
- merchantCheckTimes: import_koishi12.Schema.array(String).default([]).description("商人定时检查时间点(HH:MM 格式,如 08:00,仅 times 模式生效)"),
5380
- merchantPrivateSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("允许个人私聊订阅远行商人推送"),
5381
- homeSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("启用家园菜园和灵感订阅推送"),
5382
- homeSubscriptionIntervalMinutes: import_koishi12.Schema.number().default(5).description("家园订阅检查间隔,单位分钟")
5383
- }).description("订阅推送设置")
8678
+ import_koishi15.Schema.object({
8679
+ merchantSubscriptionEnabled: import_koishi15.Schema.boolean().default(true).description("启用远行商人订阅"),
8680
+ merchantSubscriptionItems: import_koishi15.Schema.array(String).default(["国王球", "棱镜球", "炫彩精灵蛋"]).description("默认订阅商品"),
8681
+ merchantUiStyle: import_koishi15.Schema.union(["new", "old"]).default("new").description("远行商人 UI 样式:new 为新版大图卡片,old 为旧版列表卡片"),
8682
+ merchantCheckMode: import_koishi15.Schema.union(["interval", "times"]).default("interval").description("商人检查模式:interval 定期轮询 / times 指定时间点"),
8683
+ merchantCheckInterval: import_koishi15.Schema.number().default(3e5).description("商人检查间隔,单位毫秒(仅 interval 模式生效)"),
8684
+ merchantCheckTimes: import_koishi15.Schema.array(String).default([]).description("商人定时检查时间点(HH:MM 格式,如 08:00,仅 times 模式生效)"),
8685
+ merchantPrivateSubscriptionEnabled: import_koishi15.Schema.boolean().default(true).description("允许个人私聊订阅远行商人推送"),
8686
+ homeSubscriptionEnabled: import_koishi15.Schema.boolean().default(true).description("启用家园菜园和灵感订阅推送"),
8687
+ homeSubscriptionIntervalMinutes: import_koishi15.Schema.number().default(5).description("家园订阅检查间隔,单位分钟"),
8688
+ announcementSubscriptionEnabled: import_koishi15.Schema.boolean().default(true).description("启用洛克公告订阅推送"),
8689
+ announcementPollIntervalMinutes: import_koishi15.Schema.number().default(10).description("公告订阅检查间隔,单位分钟")
8690
+ }).description("订阅推送设置"),
8691
+ import_koishi15.Schema.object({
8692
+ homeQueryWaitMs: import_koishi15.Schema.number().default(5e3).description("家园查询服务端同步等待毫秒(long-poll,超过此时间未出结果则转入排队)"),
8693
+ homeQueryPollIntervalMs: import_koishi15.Schema.number().default(3e3).description("家园查询进入排队后的轮询间隔,单位毫秒"),
8694
+ homeQueryTimeoutMs: import_koishi15.Schema.number().default(18e4).description("家园查询排队等候的总超时,单位毫秒,超时后提示稍后重试"),
8695
+ lowBandwidthMode: import_koishi15.Schema.boolean().default(false).description("低带宽模式:家园详情不再加载技能图标,降低长图生成压力")
8696
+ }).description("家园查询排队设置"),
8697
+ import_koishi15.Schema.object({
8698
+ atlasZipUrls: import_koishi15.Schema.array(String).default([
8699
+ "https://codeload.github.com/Entropy-Increase-Team/Rocom-Atlas/zip/refs/heads/main",
8700
+ "https://github.com/Entropy-Increase-Team/Rocom-Atlas/archive/refs/heads/main.zip"
8701
+ ]).description("图鉴压缩包下载地址列表(按顺序尝试,均失败后转 git clone)"),
8702
+ atlasGitUrl: import_koishi15.Schema.string().default("https://github.com/Entropy-Increase-Team/Rocom-Atlas.git").description("图鉴 git clone 地址(压缩包下载失败时回退使用)")
8703
+ }).description("图鉴下载设置")
5384
8704
  ]);
5385
8705
  function apply(ctx, config) {
5386
8706
  setupRoleTokenModel(ctx);
5387
8707
  const client = new RocomClient(config.apiBaseUrl, config.wegameApiKey);
5388
- const dataDir = import_node_path5.default.join(ctx.baseDir, "data", "rocom");
8708
+ const dataDir = import_node_path6.default.join(ctx.baseDir, "data", "rocom");
5389
8709
  const userMgr = new UserManager(dataDir);
5390
8710
  const merchantSubMgr = new MerchantSubscriptionManager(dataDir);
5391
8711
  const homeSubMgr = new HomeSubscriptionManager(dataDir);
5392
- const resPath = import_node_path5.default.resolve(__dirname, "..");
8712
+ const announcementSubMgr = new AnnouncementSubscriptionManager(dataDir);
8713
+ const resPath = import_node_path6.default.resolve(__dirname, "..");
5393
8714
  const renderer = new Renderer(resPath);
5394
- const renderTemplateRoot = import_node_fs5.default.existsSync(import_node_path5.default.join(resPath, "lib", "render-templates")) ? import_node_path5.default.join(resPath, "lib", "render-templates") : import_node_path5.default.join(resPath, "src", "render-templates");
5395
- const searcheggsDir = import_node_path5.default.join(renderTemplateRoot, "searcheggs");
8715
+ const renderTemplateRoot = import_node_fs7.default.existsSync(import_node_path6.default.join(resPath, "lib", "render-templates")) ? import_node_path6.default.join(resPath, "lib", "render-templates") : import_node_path6.default.join(resPath, "src", "render-templates");
8716
+ const searcheggsDir = import_node_path6.default.join(renderTemplateRoot, "searcheggs");
5396
8717
  const eggService = new EggService(searcheggsDir);
5397
- const deps = { ctx, config, client, userMgr, merchantSubMgr, homeSubMgr, eggService, renderer };
8718
+ const wikiService = new WikiService(ctx, client);
8719
+ const atlasService = new AtlasService(dataDir, { zipUrls: config.atlasZipUrls, gitUrl: config.atlasGitUrl });
8720
+ const deps = { ctx, config, client, userMgr, merchantSubMgr, homeSubMgr, announcementSubMgr, eggService, renderer, wikiService, atlasService };
5398
8721
  ctx.on("ready", () => {
5399
8722
  migrateRoleTokensToUserId(ctx).catch((err) => {
5400
- logger12.warn(`role token migration failed: ${err}`);
8723
+ logger15.warn(`role token migration failed: ${err}`);
5401
8724
  });
5402
8725
  migrateLegacyFrameworkTokens(ctx, userMgr).catch((err) => {
5403
- logger12.warn(`legacy framework token migration failed: ${err}`);
8726
+ logger15.warn(`legacy framework token migration failed: ${err}`);
5404
8727
  });
5405
8728
  });
5406
8729
  ctx.command("洛克", "洛克王国帮助菜单").action(async ({ session }) => {