koishi-plugin-rocom 1.0.13 → 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 (36) 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 +19 -0
  4. package/lib/client.js +101 -1
  5. package/lib/commands/account.js +1 -0
  6. package/lib/commands/merchant.js +11 -2
  7. package/lib/commands/query.js +141 -9
  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 +5 -0
  13. package/lib/index.js +3153 -188
  14. package/lib/pet-data-service.d.ts +62 -0
  15. package/lib/pet-data-service.js +468 -0
  16. package/lib/render-templates/pet-data/index.html +172 -0
  17. package/lib/render-templates/pet-data/style.css +553 -0
  18. package/lib/render-templates/wiki/detail/index.html +84 -0
  19. package/lib/render-templates/wiki/list/index.html +84 -0
  20. package/lib/render-templates/wiki/menu/index.html +65 -0
  21. package/lib/render-templates/wiki/pet/index.html +158 -0
  22. package/lib/render-templates/wiki/pet/style.css +393 -0
  23. package/lib/render-templates/wiki/shared/style.css +431 -0
  24. package/lib/render-templates/wiki/skill/index.html +79 -0
  25. package/lib/render-templates/wiki/skill/style.css +233 -0
  26. package/lib/render-templates/wiki/suggestions/index.html +84 -0
  27. package/lib/render.js +37 -8
  28. package/lib/types.d.ts +9 -1
  29. package/lib/user.d.ts +23 -1
  30. package/lib/user.js +32 -1
  31. package/lib/wiki-catalog.d.ts +19 -0
  32. package/lib/wiki-catalog.js +124 -0
  33. package/lib/wiki-service.d.ts +220 -0
  34. package/lib/wiki-service.js +1463 -0
  35. package/package.json +1 -1
  36. package/readme.md +4 -5
package/lib/index.js CHANGED
@@ -36,9 +36,9 @@ __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");
@@ -174,14 +174,14 @@ var RocomClient = class _RocomClient {
174
174
  return String(value);
175
175
  }
176
176
  }
177
- logRequestFailureDetails(method, path6, headers, params, body, errorOrResponse) {
177
+ logRequestFailureDetails(method, path7, headers, params, body, errorOrResponse) {
178
178
  const err = errorOrResponse;
179
179
  const response = err?.response;
180
180
  const details = {
181
181
  request: {
182
182
  method,
183
- url: `${this.baseUrl}${path6}`,
184
- path: path6,
183
+ url: `${this.baseUrl}${path7}`,
184
+ path: path7,
185
185
  params: this.sanitizeForLog(params),
186
186
  body: this.sanitizeForLog(body),
187
187
  headers: this.sanitizeForLog(headers),
@@ -201,21 +201,21 @@ var RocomClient = class _RocomClient {
201
201
  stack: err?.stack
202
202
  } : void 0
203
203
  };
204
- logger.warn(`${method} ${path6} detailed failure
204
+ logger.warn(`${method} ${path7} detailed failure
205
205
  ${this.stringifyForLog(details)}`);
206
206
  }
207
- async get(ctx, path6, headers, params, options) {
207
+ async get(ctx, path7, headers, params, options) {
208
208
  try {
209
- const resp = await ctx.http.get(`${this.baseUrl}${path6}`, {
209
+ const resp = await ctx.http.get(`${this.baseUrl}${path7}`, {
210
210
  headers,
211
211
  params,
212
212
  timeout: this.timeout
213
213
  });
214
214
  if (resp?.code !== 0) {
215
215
  this.setLastError(resp?.message || "接口返回异常");
216
- logger.warn(path6 + " error: " + (resp?.message || "unknown"));
216
+ logger.warn(path7 + " error: " + (resp?.message || "unknown"));
217
217
  if (!options?.silentFailureDetails) {
218
- this.logRequestFailureDetails("GET", path6, headers, params, void 0, resp);
218
+ this.logRequestFailureDetails("GET", path7, headers, params, void 0, resp);
219
219
  }
220
220
  return null;
221
221
  }
@@ -224,40 +224,40 @@ ${this.stringifyForLog(details)}`);
224
224
  const message = this.formatHttpError(e);
225
225
  this.setLastError(message);
226
226
  if (!options?.silentFailureDetails) {
227
- this.logRequestFailureDetails("GET", path6, headers, params, void 0, e);
227
+ this.logRequestFailureDetails("GET", path7, headers, params, void 0, e);
228
228
  }
229
229
  const err = e;
230
230
  if (err?.response) {
231
- logger.warn("GET " + path6 + " failed: " + message);
231
+ logger.warn("GET " + path7 + " failed: " + message);
232
232
  } else {
233
- logger.error("GET " + path6 + " failed: " + message);
233
+ logger.error("GET " + path7 + " failed: " + message);
234
234
  }
235
235
  return null;
236
236
  }
237
237
  }
238
- async post(ctx, path6, headers, json, params) {
238
+ async post(ctx, path7, headers, json, params) {
239
239
  try {
240
- const resp = await ctx.http.post(`${this.baseUrl}${path6}`, json, {
240
+ const resp = await ctx.http.post(`${this.baseUrl}${path7}`, json, {
241
241
  headers,
242
242
  params,
243
243
  timeout: this.timeout
244
244
  });
245
245
  if (resp?.code !== 0) {
246
246
  this.setLastError(resp?.message || "接口返回异常");
247
- logger.warn(path6 + " error: " + (resp?.message || "unknown"));
248
- 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);
249
249
  return null;
250
250
  }
251
251
  return resp?.data ?? {};
252
252
  } catch (e) {
253
253
  const message = this.formatHttpError(e);
254
254
  this.setLastError(message);
255
- this.logRequestFailureDetails("POST", path6, headers, params, json, e);
255
+ this.logRequestFailureDetails("POST", path7, headers, params, json, e);
256
256
  const err = e;
257
257
  if (err?.response) {
258
- logger.warn("POST " + path6 + " failed: " + message);
258
+ logger.warn("POST " + path7 + " failed: " + message);
259
259
  } else {
260
- logger.error("POST " + path6 + " failed: " + message);
260
+ logger.error("POST " + path7 + " failed: " + message);
261
261
  }
262
262
  return null;
263
263
  }
@@ -267,34 +267,34 @@ ${this.stringifyForLog(details)}`);
267
267
  if (userIdentifier) result.user_identifier = this.sanitizeUid(userIdentifier);
268
268
  return result;
269
269
  }
270
- async delete(ctx, path6, headers, params) {
270
+ async delete(ctx, path7, headers, params) {
271
271
  try {
272
- 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 });
273
273
  if (resp?.code !== 0) {
274
274
  this.setLastError(resp?.message || "接口返回异常");
275
- logger.warn(path6 + " error: " + (resp?.message || "unknown"));
276
- 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);
277
277
  return null;
278
278
  }
279
279
  return resp?.data ?? {};
280
280
  } catch (e) {
281
281
  const message = this.formatHttpError(e);
282
282
  this.setLastError(message);
283
- this.logRequestFailureDetails("DELETE", path6, headers, params, void 0, e);
283
+ this.logRequestFailureDetails("DELETE", path7, headers, params, void 0, e);
284
284
  const err = e;
285
285
  if (err?.response) {
286
- logger.warn("DELETE " + path6 + " failed: " + message);
286
+ logger.warn("DELETE " + path7 + " failed: " + message);
287
287
  } else {
288
- logger.error("DELETE " + path6 + " failed: " + message);
288
+ logger.error("DELETE " + path7 + " failed: " + message);
289
289
  }
290
290
  return null;
291
291
  }
292
292
  }
293
293
  // Login and binding APIs.
294
- async requestWithStatus(ctx, method, path6, headers, options = {}) {
294
+ async requestWithStatus(ctx, method, path7, headers, options = {}) {
295
295
  const acceptedStatuses = options.acceptedStatuses || [200];
296
296
  try {
297
- const response = await ctx.http(method, `${this.baseUrl}${path6}`, {
297
+ const response = await ctx.http(method, `${this.baseUrl}${path7}`, {
298
298
  headers,
299
299
  params: options.params,
300
300
  data: options.json,
@@ -308,7 +308,7 @@ ${this.stringifyForLog(details)}`);
308
308
  const message = !acceptedStatuses.includes(status) ? `HTTP ${status}: ${rawMessage}` : rawMessage;
309
309
  this.setLastError(message);
310
310
  if (!options.silentFailureDetails) {
311
- this.logRequestFailureDetails(method, path6, headers, options.params, options.json, body);
311
+ this.logRequestFailureDetails(method, path7, headers, options.params, options.json, body);
312
312
  }
313
313
  return { status: null, data: null };
314
314
  }
@@ -316,7 +316,7 @@ ${this.stringifyForLog(details)}`);
316
316
  if (!acceptedStatuses.includes(status)) {
317
317
  this.setLastError(`HTTP ${status}`);
318
318
  if (!options.silentFailureDetails) {
319
- this.logRequestFailureDetails(method, path6, headers, options.params, options.json, response);
319
+ this.logRequestFailureDetails(method, path7, headers, options.params, options.json, response);
320
320
  }
321
321
  return { status: null, data: null };
322
322
  }
@@ -325,22 +325,22 @@ ${this.stringifyForLog(details)}`);
325
325
  const message = this.formatHttpError(e);
326
326
  this.setLastError(message);
327
327
  if (!options.silentFailureDetails) {
328
- this.logRequestFailureDetails(method, path6, headers, options.params, options.json, e);
328
+ this.logRequestFailureDetails(method, path7, headers, options.params, options.json, e);
329
329
  }
330
330
  return { status: null, data: null };
331
331
  }
332
332
  }
333
- async requestIngameWithFallback(ctx, path6, payload, options = {}) {
333
+ async requestIngameWithFallback(ctx, path7, payload, options = {}) {
334
334
  const acceptedStatuses = options.acceptedStatuses || [200, 202];
335
335
  const requestOnce = /* @__PURE__ */ __name(async (includeApiKey, silentFailureDetails) => {
336
336
  const headers = this.wegameHeaders("", "", "", "", includeApiKey);
337
- let result2 = await this.requestWithStatus(ctx, "POST", path6, headers, {
337
+ let result2 = await this.requestWithStatus(ctx, "POST", path7, headers, {
338
338
  json: payload,
339
339
  acceptedStatuses,
340
340
  silentFailureDetails
341
341
  });
342
342
  if (result2.status === null) {
343
- result2 = await this.requestWithStatus(ctx, "GET", path6, headers, {
343
+ result2 = await this.requestWithStatus(ctx, "GET", path7, headers, {
344
344
  params: payload,
345
345
  acceptedStatuses,
346
346
  silentFailureDetails
@@ -372,12 +372,15 @@ ${this.stringifyForLog(details)}`);
372
372
  return String(payload?.task_id || payload?.taskId || payload?.taskID || "").trim();
373
373
  }
374
374
  // 判断一份 payload 是否为“已完成的业务结果”(而非排队占位)。
375
- // 家园接口完成时返回 rows/home_info;玩家、商店完成时返回 source/title/rows。
375
+ // 家园接口完成时返回 rows/home_info;玩家、商店完成时返回 source/title/rows;
376
+ // 家园详情(pet/data)完成时返回 npc_pets/npc_pet。
376
377
  static isCompletedGatewayPayload(payload) {
377
378
  if (!payload || typeof payload !== "object") return false;
378
379
  if (Array.isArray(payload.rows)) return true;
379
380
  if (payload.home_info !== void 0) return true;
380
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;
381
384
  if (String(payload.title || "").trim()) return true;
382
385
  return false;
383
386
  }
@@ -563,9 +566,9 @@ ${this.stringifyForLog(details)}`);
563
566
  this.setLastError("UID 不能为空");
564
567
  return null;
565
568
  }
566
- const path6 = "/api/v1/games/rocom/ingame/player/search";
569
+ const path7 = "/api/v1/games/rocom/ingame/player/search";
567
570
  const payload = { uid: sanitizedUid, wait_ms: 5e3 };
568
- const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path6, payload);
571
+ const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path7, payload);
569
572
  if (status === 200 && data && this.isIngamePlayerPayload(data)) return data;
570
573
  if (!data) return null;
571
574
  if (this.isIngamePlayerPayload(data)) return data;
@@ -603,6 +606,22 @@ ${this.stringifyForLog(details)}`);
603
606
  });
604
607
  }
605
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;
606
625
  const params = this.scopedParams({ diameter, weight }, userIdentifier);
607
626
  if (sameRideEgg) params.sameRideEgg = 1;
608
627
  return this.get(ctx, "/api/v1/games/rocom/pet/size-query", this.wegameHeaders("", userIdentifier, "bot", "koishi"), params);
@@ -765,9 +784,9 @@ ${this.stringifyForLog(details)}`);
765
784
  return null;
766
785
  }
767
786
  const waitMs = Number(options.waitMs) || 5e3;
768
- const path6 = "/api/v1/games/rocom/ingame/home/info";
787
+ const path7 = "/api/v1/games/rocom/ingame/home/info";
769
788
  const payload = { uid: sanitizedUid, wait_ms: waitMs };
770
- const first = await this.requestIngameWithFallback(ctx, path6, payload);
789
+ const first = await this.requestIngameWithFallback(ctx, path7, payload);
771
790
  return this.pollIngameTask(ctx, first, options, {
772
791
  queuedNoTaskId: "家园查询任务已入队,但未返回 task_id",
773
792
  stillQueued: /* @__PURE__ */ __name((taskId) => `家园查询任务仍在排队,请稍后重试(task_id: ${taskId})`, "stillQueued")
@@ -782,6 +801,25 @@ ${this.stringifyForLog(details)}`);
782
801
  );
783
802
  return status === null ? null : data;
784
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
+ }
785
823
  async getFriendship(ctx, fwToken, userIds, userIdentifier = "") {
786
824
  return this.get(
787
825
  ctx,
@@ -813,6 +851,66 @@ ${this.stringifyForLog(details)}`);
813
851
  if (options.name) params.name = options.name;
814
852
  return this.get(ctx, "/api/v1/games/rocom/pet/detail", this.wegameHeaders(), params);
815
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
+ }
816
914
  };
817
915
 
818
916
  // src/user.ts
@@ -985,6 +1083,35 @@ var HomeSubscriptionManager = class {
985
1083
  return JSON.parse(JSON.stringify(this.store.get()));
986
1084
  }
987
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
+ };
988
1115
 
989
1116
  // src/egg-service.ts
990
1117
  var import_koishi3 = require("koishi");
@@ -1033,8 +1160,8 @@ __name(petType, "petType");
1033
1160
  function fmtDur(s) {
1034
1161
  if (!s || s <= 0) return "暂无数据";
1035
1162
  if (s % 86400 === 0) return `${s / 86400} 天`;
1036
- const h4 = s / 3600;
1037
- return h4 === Math.floor(h4) ? `${h4} 小时` : `${h4.toFixed(1)} 小时`;
1163
+ const h5 = s / 3600;
1164
+ return h5 === Math.floor(h5) ? `${h5} 小时` : `${h5.toFixed(1)} 小时`;
1038
1165
  }
1039
1166
  __name(fmtDur, "fmtDur");
1040
1167
  function wt(v) {
@@ -1240,20 +1367,29 @@ var EggService = class {
1240
1367
  };
1241
1368
  }
1242
1369
  formatSizeApiCard(item) {
1243
- 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);
1244
1380
  const matchCount = num(item?.matchCount);
1245
- const heightMin = num(item?.diameterMin);
1246
- const heightMax = num(item?.diameterMax);
1247
- const weightMin = num(item?.weightMin);
1248
- 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);
1249
1385
  return {
1250
- id: item?.petId || "-",
1251
- name: item?.pet || "未知精灵",
1252
- icon: item?.petIcon || petIconUrl(item?.petId),
1253
- image: item?.petImage || petImageUrl(item?.petId),
1254
- 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(" / ") : "后端未提供",
1255
1391
  egg_group_ids: [],
1256
- egg_groups_label: "后端未提供",
1392
+ egg_groups_label: eggGroupNames.length ? eggGroupNames.join(" / ") : "后端未提供",
1257
1393
  height_min: heightMin,
1258
1394
  height_max: heightMax,
1259
1395
  height_label: fmtRange(heightMin, heightMax, "m"),
@@ -1262,7 +1398,30 @@ var EggService = class {
1262
1398
  weight_label: fmtRange(weightMin, weightMax, "kg"),
1263
1399
  probability,
1264
1400
  match_count: matchCount,
1265
- 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 : []
1266
1425
  };
1267
1426
  }
1268
1427
  formatEggSearchCard(item) {
@@ -1421,8 +1580,7 @@ var EggService = class {
1421
1580
  if (height != null) cond.push(`身高=${heightDisplay || fmtRange(ht(height), ht(height), "m")}`);
1422
1581
  if (weight != null) cond.push(`体重=${weight}kg`);
1423
1582
  const condStr = cond.join(" + ") || "当前条件";
1424
- const exact = results?.exactResults || [];
1425
- const candidates = results?.candidates || [];
1583
+ const { exact, candidates } = this.sizeApiResultGroups(results);
1426
1584
  if (!exact.length && !candidates.length) return `❌ 未找到符合 ${condStr} 的精灵。`;
1427
1585
  const lines = [];
1428
1586
  if (exact.length) {
@@ -1635,9 +1793,10 @@ var EggService = class {
1635
1793
  const conditions = [];
1636
1794
  if (height != null) conditions.push(`身高 ${heightDisplay || fmtRange(ht(height), ht(height), "m")}`);
1637
1795
  if (weight != null) conditions.push(`体重 ${weight} kg`);
1796
+ const groups = this.sizeApiResultGroups(results);
1638
1797
  const [perfect, ranged] = this.mergeCardsByName(
1639
- (results?.exactResults || []).map((item) => this.formatSizeApiCard(item)),
1640
- (results?.candidates || []).map((item) => this.formatSizeApiCard(item))
1798
+ groups.exact.map((item) => this.formatSizeApiCard(item)),
1799
+ groups.candidates.map((item) => this.formatSizeApiCard(item))
1641
1800
  );
1642
1801
  const searchMode = results?.searchMode || "";
1643
1802
  const queryLabel = `${conditions.join(" / ") || "尺寸反查"}${searchMode ? ` · 模式 ${searchMode}` : ""}`;
@@ -1818,13 +1977,16 @@ var Renderer = class {
1818
1977
  try {
1819
1978
  await page.evaluate(async () => {
1820
1979
  const images = Array.from(document.images);
1821
- await Promise.all(images.map((img) => {
1822
- if (img.complete) return Promise.resolve();
1823
- return new Promise((resolve) => {
1824
- img.onload = () => resolve();
1825
- img.onerror = () => resolve();
1826
- });
1827
- }));
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
+ ]);
1828
1990
  const fonts = document.fonts;
1829
1991
  if (fonts?.ready) {
1830
1992
  await fonts.ready;
@@ -1841,6 +2003,8 @@ var Renderer = class {
1841
2003
  ".searcheggs-cont",
1842
2004
  ".bwiki-shell",
1843
2005
  ".skill-shell",
2006
+ ".wiki-page",
2007
+ ".pet-data-page",
1844
2008
  ".lineup-page",
1845
2009
  ".lineup-detail-page",
1846
2010
  ".page-section-main",
@@ -1888,6 +2052,26 @@ var Renderer = class {
1888
2052
  deviceScaleFactor: initialViewport.deviceScaleFactor
1889
2053
  });
1890
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
+ }
1891
2075
  const hasOverflow = elementMetrics.width > box.width + 0.5 || elementMetrics.height > box.height + 0.5;
1892
2076
  if (capturePadding.left || capturePadding.right || capturePadding.top || capturePadding.bottom || hasOverflow) {
1893
2077
  const clipX = Math.max(0, elementMetrics.x - capturePadding.left);
@@ -1925,6 +2109,1981 @@ var Renderer = class {
1925
2109
  }
1926
2110
  };
1927
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;
4034
+ }
4035
+ }
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);
4060
+ }
4061
+ }
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");
4086
+
1928
4087
  // src/role-token.ts
1929
4088
  function normalize(value) {
1930
4089
  return String(value ?? "").trim();
@@ -2059,12 +4218,12 @@ async function migrateLegacyFrameworkTokens(ctx, userMgr) {
2059
4218
  __name(migrateLegacyFrameworkTokens, "migrateLegacyFrameworkTokens");
2060
4219
 
2061
4220
  // src/commands/account.ts
2062
- var import_koishi6 = require("koishi");
4221
+ var import_koishi8 = require("koishi");
2063
4222
 
2064
4223
  // src/send-image.ts
2065
- var import_koishi5 = require("koishi");
2066
- var import_node_zlib = __toESM(require("node:zlib"));
2067
- 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");
2068
4227
  var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2069
4228
  var JPEG_SIGNATURE = Buffer.from([255, 216, 255]);
2070
4229
  function formatSessionContext(session) {
@@ -2139,9 +4298,9 @@ function compressPngImage(image, config) {
2139
4298
  if (type === "IEND") break;
2140
4299
  }
2141
4300
  if (!idatChunks.length) return image;
2142
- const rawImageData = import_node_zlib.default.inflateSync(Buffer.concat(idatChunks));
4301
+ const rawImageData = import_node_zlib2.default.inflateSync(Buffer.concat(idatChunks));
2143
4302
  const level = Math.max(0, Math.min(9, Math.floor(config.imageCompressionLevel ?? 9)));
2144
- const compressedIdat = import_node_zlib.default.deflateSync(rawImageData, { level });
4303
+ const compressedIdat = import_node_zlib2.default.deflateSync(rawImageData, { level });
2145
4304
  const rebuiltChunks = [];
2146
4305
  let wroteIdat = false;
2147
4306
  for (const chunk of chunks) {
@@ -2157,7 +4316,7 @@ function compressPngImage(image, config) {
2157
4316
  const compressed = Buffer.concat([PNG_SIGNATURE, ...rebuiltChunks]);
2158
4317
  if (compressed.length < image.length) return compressed;
2159
4318
  } catch (err) {
2160
- logger5.warn(`PNG compression failed, sending original image: ${err}`);
4319
+ logger7.warn(`PNG compression failed, sending original image: ${err}`);
2161
4320
  }
2162
4321
  return image;
2163
4322
  }
@@ -2165,36 +4324,36 @@ __name(compressPngImage, "compressPngImage");
2165
4324
  async function sendImageWithFallback(session, image, fallbackText, scene, compressionConfig) {
2166
4325
  const ctxInfo = formatSessionContext(session);
2167
4326
  if (!image) {
2168
- logger5.warn(`[${scene}] render returned empty image, fallback to text | ${ctxInfo}`);
4327
+ logger7.warn(`[${scene}] render returned empty image, fallback to text | ${ctxInfo}`);
2169
4328
  try {
2170
4329
  await session.send(fallbackText);
2171
4330
  } catch (e) {
2172
- logger5.error(`[${scene}] text fallback send failed | ${ctxInfo} | ${e}`);
4331
+ logger7.error(`[${scene}] text fallback send failed | ${ctxInfo} | ${e}`);
2173
4332
  }
2174
4333
  return;
2175
4334
  }
2176
4335
  const outputImage = compressionConfig ? compressPngImage(image, compressionConfig) : image;
2177
4336
  if (outputImage.length < image.length) {
2178
- 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}`);
2179
4338
  }
2180
4339
  try {
2181
- 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)));
2182
4341
  if (!hasSendResult(result)) {
2183
- 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}`);
2184
4343
  }
2185
4344
  } catch (e) {
2186
- 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}`);
2187
4346
  try {
2188
4347
  await session.send(fallbackText);
2189
4348
  } catch (fallbackErr) {
2190
- 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}`);
2191
4350
  }
2192
4351
  }
2193
4352
  }
2194
4353
  __name(sendImageWithFallback, "sendImageWithFallback");
2195
4354
 
2196
4355
  // src/commands/account.ts
2197
- var logger6 = new import_koishi6.Logger("rocom-account");
4356
+ var logger8 = new import_koishi8.Logger("rocom-account");
2198
4357
  async function recallMessages(session, messageIds) {
2199
4358
  if (!session?.bot?.deleteMessage || !session.channelId) return;
2200
4359
  for (const id of messageIds) {
@@ -2202,7 +4361,7 @@ async function recallMessages(session, messageIds) {
2202
4361
  try {
2203
4362
  await session.bot.deleteMessage(session.channelId, id);
2204
4363
  } catch (err) {
2205
- logger6.warn(`撤回消息 ${id} 失败:${err}`);
4364
+ logger8.warn(`撤回消息 ${id} 失败:${err}`);
2206
4365
  }
2207
4366
  }
2208
4367
  }
@@ -2282,19 +4441,19 @@ async function saveBindingWithRoleInfo(deps, session, fwToken, loginType, userId
2282
4441
  __name(saveBindingWithRoleInfo, "saveBindingWithRoleInfo");
2283
4442
  function register(deps) {
2284
4443
  const { ctx, client, userMgr } = deps;
2285
- ctx.command("洛克").subcommand(".QQ登录", "QQ 扫码登录").alias("洛克QQ登录").action(async ({ session }) => {
4444
+ ctx.command("洛克").subcommand(".QQ登录", "QQ 扫码登录").alias("洛克QQ登录").alias("洛克qq登录").action(async ({ session }) => {
2286
4445
  const userId = session.userId;
2287
4446
  const qrData = await client.qqQrLogin(ctx, userId);
2288
4447
  if (!qrData?.qr_image) return "获取 QQ 二维码失败。";
2289
4448
  const fwToken = qrData.frameworkToken;
2290
4449
  const qrB64 = qrData.qr_image;
2291
4450
  const imgData = qrB64.includes(",") ? qrB64.split(",")[1] : qrB64;
2292
- const qrSendResult = await session.send((0, import_koishi6.h)(
4451
+ const qrSendResult = await session.send((0, import_koishi8.h)(
2293
4452
  "message",
2294
4453
  {},
2295
- import_koishi6.h.at(userId),
2296
- import_koishi6.h.text("\n请使用 QQ 扫描二维码登录(有效时间 2 分钟)\n注意需要双设备扫码。\n"),
2297
- 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}`)
2298
4457
  ));
2299
4458
  const qrMessageIds = Array.isArray(qrSendResult) ? qrSendResult.filter(Boolean).map(String) : qrSendResult ? [String(qrSendResult)] : [];
2300
4459
  const startTime = Date.now();
@@ -2481,11 +4640,11 @@ ${qrUrl}`);
2481
4640
  __name(register, "register");
2482
4641
 
2483
4642
  // src/commands/query.ts
2484
- var import_koishi8 = require("koishi");
4643
+ var import_koishi10 = require("koishi");
2485
4644
 
2486
4645
  // src/subscription-send.ts
2487
- var import_koishi7 = require("koishi");
2488
- 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");
2489
4648
  function findBot(ctx, platform = "") {
2490
4649
  if (!ctx.bots?.length) return null;
2491
4650
  if (!platform) return ctx.bots[0];
@@ -2498,7 +4657,7 @@ async function sendScheduledMessage(ctx, target, message) {
2498
4657
  const userId = target.userId || "";
2499
4658
  const bot = findBot(ctx, platform);
2500
4659
  if (!bot) {
2501
- logger7.warn("no available bot, skip scheduled push");
4660
+ logger9.warn("no available bot, skip scheduled push");
2502
4661
  return false;
2503
4662
  }
2504
4663
  try {
@@ -2521,29 +4680,458 @@ async function sendScheduledMessage(ctx, target, message) {
2521
4680
  return true;
2522
4681
  }
2523
4682
  } catch (err) {
2524
- logger7.warn(`scheduled push failed: ${err}`);
4683
+ logger9.warn(`scheduled push failed: ${err}`);
2525
4684
  return false;
2526
4685
  }
2527
- logger7.warn(`scheduled push target is incomplete: ${JSON.stringify(target)}`);
4686
+ logger9.warn(`scheduled push target is incomplete: ${JSON.stringify(target)}`);
2528
4687
  return false;
2529
4688
  }
2530
4689
  __name(sendScheduledMessage, "sendScheduledMessage");
2531
4690
  async function sendScheduledImageWithFallback(ctx, target, image, fallbackText, mentionAll = false) {
2532
- const buildContent = /* @__PURE__ */ __name((body) => mentionAll ? (0, import_koishi7.h)("message", {}, (0, import_koishi7.h)("at", { type: "all" }), import_koishi7.h.text("\n"), body) : body, "buildContent");
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");
2533
4692
  if (!image) {
2534
- return sendScheduledMessage(ctx, target, buildContent(import_koishi7.h.text(fallbackText)));
4693
+ return sendScheduledMessage(ctx, target, buildContent(import_koishi9.h.text(fallbackText)));
2535
4694
  }
2536
- const imageSegment = import_koishi7.h.image(image, detectImageMime(image));
4695
+ const imageSegment = import_koishi9.h.image(image, detectImageMime(image));
2537
4696
  const sent = await sendScheduledMessage(ctx, target, buildContent(imageSegment));
2538
4697
  if (sent) return true;
2539
- return sendScheduledMessage(ctx, target, buildContent(import_koishi7.h.text(fallbackText)));
4698
+ return sendScheduledMessage(ctx, target, buildContent(import_koishi9.h.text(fallbackText)));
2540
4699
  }
2541
4700
  __name(sendScheduledImageWithFallback, "sendScheduledImageWithFallback");
2542
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
+
2543
5131
  // src/commands/query.ts
2544
- var import_node_fs4 = __toESM(require("node:fs"));
2545
- var import_node_path4 = __toESM(require("node:path"));
2546
- 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");
2547
5135
  async function sendImage(deps, session, templateName, data, fallback) {
2548
5136
  const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
2549
5137
  await sendImageWithFallback(session, png, fallback, `query:${templateName}`, deps.config);
@@ -2753,14 +5341,14 @@ function stringifyInspectValue(value) {
2753
5341
  return String(value);
2754
5342
  }
2755
5343
  __name(stringifyInspectValue, "stringifyInspectValue");
2756
- function normalizeEpochSeconds(value) {
5344
+ function normalizeEpochSeconds2(value) {
2757
5345
  const ts = Number(value);
2758
5346
  if (!Number.isFinite(ts)) return 0;
2759
5347
  if (ts > 1e13) return Math.floor(ts / 1e6);
2760
5348
  if (ts > 1e10) return Math.floor(ts / 1e3);
2761
5349
  return Math.floor(ts);
2762
5350
  }
2763
- __name(normalizeEpochSeconds, "normalizeEpochSeconds");
5351
+ __name(normalizeEpochSeconds2, "normalizeEpochSeconds");
2764
5352
  function formatHomeRemaining(targetTs, nowTs = Math.floor(Date.now() / 1e3)) {
2765
5353
  if (!targetTs) return "未开始";
2766
5354
  if (nowTs >= targetTs) return "已完成";
@@ -2824,12 +5412,12 @@ var PANEL_PET_STAT_DEFS = [
2824
5412
  { key: "special_defense", label: "魔防", addiType: 5, aliases: ["special_defense", "magic_defense", "sp_defense"] },
2825
5413
  { key: "speed", label: "速度", addiType: 6, aliases: ["speed", "spd"] }
2826
5414
  ];
2827
- 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");
2828
5416
  var PANEL_EMPTY_FEATURE = { id: "", name: "暂无特性", desc: "当前接口未返回特性描述。" };
2829
5417
  var PANEL_EMPTY_SKILL_HINT = "当前接口未返回技能详情。";
2830
5418
  var panelPetCacheMem = null;
2831
5419
  function panelPetCacheFilePath(deps) {
2832
- 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);
2833
5421
  }
2834
5422
  __name(panelPetCacheFilePath, "panelPetCacheFilePath");
2835
5423
  function ensureBloodlineLabel(raw, mutationLabel) {
@@ -2841,15 +5429,15 @@ function loadPanelPetCache(deps) {
2841
5429
  if (panelPetCacheMem) return panelPetCacheMem;
2842
5430
  const filePath = panelPetCacheFilePath(deps);
2843
5431
  try {
2844
- if (!import_node_fs4.default.existsSync(filePath)) {
5432
+ if (!import_node_fs5.default.existsSync(filePath)) {
2845
5433
  panelPetCacheMem = {};
2846
5434
  return panelPetCacheMem;
2847
5435
  }
2848
- const text = import_node_fs4.default.readFileSync(filePath, "utf-8");
5436
+ const text = import_node_fs5.default.readFileSync(filePath, "utf-8");
2849
5437
  const parsed = JSON.parse(text);
2850
5438
  panelPetCacheMem = parsed && typeof parsed === "object" ? parsed : {};
2851
5439
  } catch (err) {
2852
- logger8.warn(`加载精灵面板缓存失败: ${err}`);
5440
+ logger10.warn(`加载精灵面板缓存失败: ${err}`);
2853
5441
  panelPetCacheMem = {};
2854
5442
  }
2855
5443
  return panelPetCacheMem;
@@ -2857,11 +5445,11 @@ function loadPanelPetCache(deps) {
2857
5445
  __name(loadPanelPetCache, "loadPanelPetCache");
2858
5446
  function savePanelPetCache(deps, data) {
2859
5447
  const filePath = panelPetCacheFilePath(deps);
2860
- const dir = import_node_path4.default.dirname(filePath);
2861
- 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 });
2862
5450
  const tempPath = `${filePath}.tmp`;
2863
- import_node_fs4.default.writeFileSync(tempPath, JSON.stringify(data, null, 2), "utf-8");
2864
- 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);
2865
5453
  panelPetCacheMem = data;
2866
5454
  }
2867
5455
  __name(savePanelPetCache, "savePanelPetCache");
@@ -3358,11 +5946,22 @@ function extractHomePet(deps, raw, index, guard = false) {
3358
5946
  if (["", "0"].includes(String(petId || "0")) && !guard) return null;
3359
5947
  const nowTs = Math.floor(Date.now() / 1e3);
3360
5948
  const hasEgg = Boolean(raw.have_egg);
3361
- const predictedEggTime = normalizeEpochSeconds(raw.predicted_egg_time);
5949
+ const predictedEggTime = normalizeEpochSeconds2(raw.predicted_egg_time);
3362
5950
  const eggReady = hasEgg || predictedEggTime > 0 && nowTs >= predictedEggTime;
3363
5951
  const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
3364
5952
  const gender = Number(display.gender || raw.gender || 0) || 0;
3365
- const mutationType = Number(display.mutation_type || raw.mutation_type || homePet.mutation_type || 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
+ }
3366
5965
  const isMale = gender === 1;
3367
5966
  const status = homePet.status ?? raw.status;
3368
5967
  const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ["2", "guard", "守卫"].includes(String(status).toLowerCase());
@@ -3397,6 +5996,9 @@ function extractHomePet(deps, raw, index, guard = false) {
3397
5996
  statusText,
3398
5997
  statusClass,
3399
5998
  note,
5999
+ hasEgg,
6000
+ eggReady,
6001
+ eggTime: predictedEggTime || 0,
3400
6002
  inspireReady: eggReady,
3401
6003
  readyAt: predictedEggTime || 0,
3402
6004
  gender
@@ -3436,23 +6038,23 @@ function homePlantIcon(deps, iconId) {
3436
6038
  }
3437
6039
  __name(homePlantIcon, "homePlantIcon");
3438
6040
  var homePlantMapCache = null;
3439
- 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");
3440
6042
  function resolveHomePlantMapPath() {
3441
6043
  const candidates = [
3442
- import_node_path4.default.resolve(__dirname, "..", HOME_PLANT_MAP_RELATIVE_PATH),
3443
- 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)
3444
6046
  ];
3445
- 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];
3446
6048
  }
3447
6049
  __name(resolveHomePlantMapPath, "resolveHomePlantMapPath");
3448
6050
  function loadHomePlantMap() {
3449
6051
  if (homePlantMapCache) return homePlantMapCache;
3450
6052
  const filePath = resolveHomePlantMapPath();
3451
6053
  try {
3452
- 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"));
3453
6055
  homePlantMapCache = data && typeof data === "object" ? data : {};
3454
6056
  } catch (err) {
3455
- logger8.warn(`加载家园作物映射失败: ${err}`);
6057
+ logger10.warn(`加载家园作物映射失败: ${err}`);
3456
6058
  homePlantMapCache = {};
3457
6059
  }
3458
6060
  return homePlantMapCache;
@@ -3474,7 +6076,7 @@ function extractHomePlants(deps, homeInfo) {
3474
6076
  if (["", "0"].includes(String(plantId || "0"))) return null;
3475
6077
  const mappedPlant = plantMap[String(plantId)] || {};
3476
6078
  const iconId = plantData.icon_url || plantData.iconUrl || raw.icon_url || raw.iconUrl || plantData.iconid || raw.iconid || raw.icon_id || mappedPlant.iconid;
3477
- 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);
3478
6080
  const leftTime = Number(raw.left_time || 0);
3479
6081
  if (!readyAt && leftTime > 0) readyAt = nowTs + leftTime;
3480
6082
  const ready = Boolean(readyAt && nowTs >= readyAt) || ["2", "ready", "mature"].includes(String(raw.status));
@@ -3527,7 +6129,7 @@ function buildHomeRenderData(deps, res, uid) {
3527
6129
  return 0;
3528
6130
  });
3529
6131
  const gardenPlots = extractHomePlants(deps, homeInfo);
3530
- const createdAt = normalizeEpochSeconds(res?.meta?.created_at);
6132
+ const createdAt = normalizeEpochSeconds2(res?.meta?.created_at);
3531
6133
  return {
3532
6134
  title: "洛克家园",
3533
6135
  subtitle: "Home Information",
@@ -3688,11 +6290,31 @@ async function resolveHomeUid(deps, session, uid = "") {
3688
6290
  return String(deps.userMgr.getPrimaryBinding(session?.userId || "")?.role_id || "");
3689
6291
  }
3690
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
+ };
3691
6313
  async function subscribeHome(deps, session, uid, kind) {
3692
6314
  const target = sessionTarget(session);
3693
6315
  if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds)) return "此指令仅限管理员使用。";
3694
6316
  const targetUid = await resolveHomeUid(deps, session, uid);
3695
- if (!targetUid) return kind === "garden" ? "请提供玩家 UID,或先完成绑定后再订阅家园菜园。" : "请提供玩家 UID,或先完成绑定后再订阅家园灵感。";
6317
+ if (!targetUid) return HOME_SUB_TEXT[kind].needUidHint;
3696
6318
  const key = homeSubscriptionKey(session, targetUid, kind);
3697
6319
  deps.homeSubMgr.upsert(key, {
3698
6320
  key,
@@ -3706,7 +6328,7 @@ async function subscribeHome(deps, session, uid, kind) {
3706
6328
  notify_state: {},
3707
6329
  updated_at: Math.floor(Date.now() / 1e3)
3708
6330
  });
3709
- return kind === "garden" ? `已订阅 UID ${targetUid} 的家园菜园提醒:首个成熟和全部成熟时各推送一次。` : `已订阅 UID ${targetUid} 的家园精灵灵感提醒:首个完成和全部完成时各推送一次。`;
6331
+ return `已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`;
3710
6332
  }
3711
6333
  __name(subscribeHome, "subscribeHome");
3712
6334
  function homeSubscriptionState(data, kind) {
@@ -3716,6 +6338,12 @@ function homeSubscriptionState(data, kind) {
3716
6338
  const names2 = readyItems2.map((item) => `田地${item.landIndex} ${item.plantName}`);
3717
6339
  return { items: items2, readyItems: readyItems2, names: names2 };
3718
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
+ }
3719
6347
  const items = [...data.indoorPets || [], ...data.guardPets || []].filter((item) => item.readyAt);
3720
6348
  const readyItems = items.filter((item) => item.inspireReady);
3721
6349
  const names = readyItems.map((item) => item.name || "未知精灵");
@@ -3723,8 +6351,7 @@ function homeSubscriptionState(data, kind) {
3723
6351
  }
3724
6352
  __name(homeSubscriptionState, "homeSubscriptionState");
3725
6353
  function homeSubscriptionMessage(uid, kind, level, totalCount, readyItems, names) {
3726
- const kindText = kind === "garden" ? "菜园作物" : "精灵灵感";
3727
- const actionText = kind === "garden" ? "成熟" : "完成";
6354
+ const { kindText, actionText } = HOME_SUB_TEXT[kind];
3728
6355
  const levelText = level === "first" ? "首个" : "全部";
3729
6356
  return [
3730
6357
  `家园${kindText}${levelText}${actionText}提醒:${uid}`,
@@ -3739,7 +6366,7 @@ async function checkHomeSubscriptions(deps) {
3739
6366
  let checkedCount = 0;
3740
6367
  let pushedCount = 0;
3741
6368
  for (const [key, sub] of Object.entries(subs)) {
3742
- if (!sub.uid || !["garden", "inspiration"].includes(sub.kind)) continue;
6369
+ if (!sub.uid || !["garden", "inspiration", "egg"].includes(sub.kind)) continue;
3743
6370
  checkedCount++;
3744
6371
  if (!cache.has(sub.uid)) {
3745
6372
  cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 3e4 }));
@@ -3774,7 +6401,7 @@ async function checkHomeSubscriptions(deps) {
3774
6401
  }, messages.join("\n\n"));
3775
6402
  if (!sent) continue;
3776
6403
  } catch (e) {
3777
- logger8.warn(`家园订阅推送失败: ${e}`);
6404
+ logger10.warn(`家园订阅推送失败: ${e}`);
3778
6405
  continue;
3779
6406
  }
3780
6407
  for (const level of pushLevels) notifyState[level] = true;
@@ -3830,19 +6457,19 @@ function register2(deps) {
3830
6457
  const hasExtraProfileData = Boolean(profileSignature || profileHomeItems.length || profileCardItems.length || profileCardImage);
3831
6458
  let degraded = false;
3832
6459
  if (!sm) {
3833
- logger8.warn("[Rocom] 洛克.档案:pet-summary 接口不可用,已降级为基础档案渲染");
6460
+ logger10.warn("[Rocom] 洛克.档案:pet-summary 接口不可用,已降级为基础档案渲染");
3834
6461
  degraded = true;
3835
6462
  }
3836
6463
  if (!ev) {
3837
- logger8.warn("[Rocom] 洛克.档案:evaluation 接口不可用,已降级为基础档案渲染");
6464
+ logger10.warn("[Rocom] 洛克.档案:evaluation 接口不可用,已降级为基础档案渲染");
3838
6465
  degraded = true;
3839
6466
  }
3840
6467
  if (!cl) {
3841
- logger8.warn("[Rocom] 洛克.档案:collection 接口不可用,已降级为基础档案渲染");
6468
+ logger10.warn("[Rocom] 洛克.档案:collection 接口不可用,已降级为基础档案渲染");
3842
6469
  degraded = true;
3843
6470
  }
3844
6471
  if (!bo) {
3845
- logger8.warn("[Rocom] 洛克.档案:battle-overview 接口不可用,已降级为基础档案渲染");
6472
+ logger10.warn("[Rocom] 洛克.档案:battle-overview 接口不可用,已降级为基础档案渲染");
3846
6473
  degraded = true;
3847
6474
  }
3848
6475
  if (degraded) {
@@ -4194,6 +6821,81 @@ function register2(deps) {
4194
6821
  if (!res) return `家园查询失败:${client.getLastErrorBrief()}`;
4195
6822
  await sendImage(deps, session, "home", buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
4196
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
+ });
4197
6899
  ctx.command("洛克").subcommand(".刷新面板 [uid:string]", "刷新指定 UID 的精灵面板缓存").alias("洛克刷新面板").alias("刷新面板").alias("刷新精灵").alias("强制刷新").action(async ({ session }, uid = "") => {
4198
6900
  const targetUid = panelUidFromBinding(deps, session, uid);
4199
6901
  if (!targetUid) return "请提供 UID,或先绑定账号后再使用 洛克.刷新面板。";
@@ -4271,10 +6973,11 @@ function register2(deps) {
4271
6973
  });
4272
6974
  ctx.command("订阅家园菜园 [uid:string]", "订阅指定 UID 的家园菜园成熟提醒").action(async ({ session }, uid = "") => subscribeHome(deps, session, uid, "garden"));
4273
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"));
4274
6977
  ctx.command("取消订阅家园 [kind:string] [uid:string]", "取消当前会话的家园订阅").action(async ({ session }, kind = "全部", uid = "") => {
4275
6978
  const target = sessionTarget(session);
4276
6979
  if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds)) return "此指令仅限管理员使用。";
4277
- const kindMap = { "菜园": "garden", "灵感": "inspiration", "全部": "", all: "", garden: "garden", inspiration: "inspiration" };
6980
+ const kindMap = { "菜园": "garden", "灵感": "inspiration", "生蛋": "egg", "全部": "", all: "", garden: "garden", inspiration: "inspiration", egg: "egg" };
4278
6981
  const deleted = deps.homeSubMgr.deleteMatching(target, kindMap[String(kind || "全部")] ?? "", String(uid || "").trim());
4279
6982
  return deleted ? `已取消 ${deleted} 条家园订阅。` : "当前会话没有匹配的家园订阅。";
4280
6983
  });
@@ -4284,14 +6987,14 @@ function register2(deps) {
4284
6987
  return `家园订阅检查完成:订阅 ${result.subscriptions} 条,检查 ${result.checked} 条,推送 ${result.pushed} 档提醒。`;
4285
6988
  });
4286
6989
  if (deps.config.homeSubscriptionEnabled) {
4287
- 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);
4288
6991
  }
4289
6992
  }
4290
6993
  __name(register2, "register");
4291
6994
 
4292
6995
  // src/commands/merchant.ts
4293
- var import_koishi9 = require("koishi");
4294
- var logger9 = new import_koishi9.Logger("rocom-merchant");
6996
+ var import_koishi11 = require("koishi");
6997
+ var logger11 = new import_koishi11.Logger("rocom-merchant");
4295
6998
  var TEXT = {
4296
6999
  merchant: "远行商人",
4297
7000
  todayMerchant: "今日远行商人",
@@ -4792,7 +7495,7 @@ async function checkMerchantSubscriptions(deps) {
4792
7495
  const platform = sub.platform || ctx.bots[0]?.platform;
4793
7496
  const channelId = sub.channel_id || sub.group_id || sub.user_id || key;
4794
7497
  if (!platform || !channelId) {
4795
- logger9.warn(`推送失败 ${key}: 无法确定平台或频道`);
7498
+ logger11.warn(`推送失败 ${key}: 无法确定平台或频道`);
4796
7499
  continue;
4797
7500
  }
4798
7501
  const fallbackText = `${msg}
@@ -4816,7 +7519,7 @@ ${fallback}`;
4816
7519
  __name(checkMerchantSubscriptions, "checkMerchantSubscriptions");
4817
7520
  function register3(deps) {
4818
7521
  const { ctx, config, client, merchantSubMgr } = deps;
4819
- ctx.command(TEXT.merchant, "查看远行商人商品").action(async ({ session }) => {
7522
+ ctx.command(TEXT.merchant, "查看远行商人商品").alias("yxsr").action(async ({ session }) => {
4820
7523
  const res = await client.getMerchantInfo(ctx, true);
4821
7524
  if (!res) return `获取远行商人数据失败:${client.getLastErrorBrief()}`;
4822
7525
  const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
@@ -4875,6 +7578,13 @@ function register3(deps) {
4875
7578
  return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
4876
7579
  });
4877
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");
4878
7588
  if (config.merchantCheckMode === "times" && config.merchantCheckTimes.length > 0) {
4879
7589
  let lastMerchantCheckKey = "";
4880
7590
  ctx.setInterval(async () => {
@@ -4884,11 +7594,11 @@ function register3(deps) {
4884
7594
  const checkKey = `${now.toDateString()}-${timeStr}`;
4885
7595
  if (checkKey === lastMerchantCheckKey) return;
4886
7596
  lastMerchantCheckKey = checkKey;
4887
- await checkMerchantSubscriptions(deps);
7597
+ runCheckWithJitter();
4888
7598
  }, 6e4);
4889
7599
  } else {
4890
7600
  ctx.setInterval(async () => {
4891
- await checkMerchantSubscriptions(deps);
7601
+ runCheckWithJitter();
4892
7602
  }, config.merchantCheckInterval);
4893
7603
  }
4894
7604
  }
@@ -4896,11 +7606,156 @@ function register3(deps) {
4896
7606
  __name(register3, "register");
4897
7607
 
4898
7608
  // src/commands/wiki.ts
4899
- 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");
4900
7710
  function register4(deps) {
4901
- const { ctx } = deps;
4902
- ctx.command("洛克").subcommand(".wiki <name:text>", "查询精灵 wiki").alias("洛克wiki").action(() => WIKI_CLOSED_MESSAGE);
4903
- 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
+ });
4904
7759
  }
4905
7760
  __name(register4, "register");
4906
7761
 
@@ -5095,7 +7950,7 @@ function register5(deps) {
5095
7950
  __name(register5, "register");
5096
7951
 
5097
7952
  // src/commands/tools.ts
5098
- var import_koishi10 = require("koishi");
7953
+ var import_koishi13 = require("koishi");
5099
7954
 
5100
7955
  // src/activities-service.ts
5101
7956
  var CHINA_TIMEZONE2 = "Asia/Shanghai";
@@ -5383,7 +8238,7 @@ var ActivitiesService = class {
5383
8238
  };
5384
8239
 
5385
8240
  // src/commands/tools.ts
5386
- var logger10 = new import_koishi10.Logger("rocom-tools");
8241
+ var logger13 = new import_koishi13.Logger("rocom-tools");
5387
8242
  var activitiesService = new ActivitiesService();
5388
8243
  function trimText2(value) {
5389
8244
  return String(value ?? "").trim();
@@ -5422,6 +8277,63 @@ function announcementId(item) {
5422
8277
  return trimText2(item?.thread_id || item?.id);
5423
8278
  }
5424
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");
5425
8337
  function buildAnnouncementListText(data, page) {
5426
8338
  const list = firstArray(data, ["list", "items"]);
5427
8339
  if (!list.length) return page > 1 ? "该页没有更多公告。" : "当前没有公告数据。";
@@ -5497,11 +8409,45 @@ function register6(deps) {
5497
8409
  if (!data) return `公告详情查询失败:${client.getLastErrorBrief()}`;
5498
8410
  return buildAnnouncementDetailText(data);
5499
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
+ }
5500
8446
  ctx.command("洛克").subcommand(".同步配置", "手动同步 RoCom 远端配置").alias("洛克同步配置").action(async ({ session }) => {
5501
8447
  if (!config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
5502
8448
  const data = await client.syncConfig(ctx, session?.userId || "");
5503
8449
  if (!data) return `同步配置失败:${client.getLastErrorBrief()}`;
5504
- logger10.info(`manual config sync requested by ${session?.userId || "unknown"}`);
8450
+ logger13.info(`manual config sync requested by ${session?.userId || "unknown"}`);
5505
8451
  const skipped = Array.isArray(data?.skipped_resources) ? data.skipped_resources.length : 0;
5506
8452
  return skipped ? `配置同步完成,但有 ${skipped} 个资源跳过。` : "配置同步完成。";
5507
8453
  });
@@ -5509,8 +8455,8 @@ function register6(deps) {
5509
8455
  __name(register6, "register");
5510
8456
 
5511
8457
  // src/commands/admin.ts
5512
- var import_koishi11 = require("koishi");
5513
- var logger11 = new import_koishi11.Logger("rocom-admin");
8458
+ var import_koishi14 = require("koishi");
8459
+ var logger14 = new import_koishi14.Logger("rocom-admin");
5514
8460
  function register7(deps) {
5515
8461
  const { ctx, config, client, userMgr } = deps;
5516
8462
  function isAdmin(userId) {
@@ -5556,7 +8502,7 @@ function register7(deps) {
5556
8502
  for (const [userId, bindings] of Object.entries(allUsers)) {
5557
8503
  const token = await getRoleToken(ctx, userId);
5558
8504
  if (!token?.fwt) {
5559
- logger11.warn(`用户 ${userId} 没有可用的 fwt,跳过失效检测`);
8505
+ logger14.warn(`用户 ${userId} 没有可用的 fwt,跳过失效检测`);
5560
8506
  totalSkipped += bindings.length;
5561
8507
  continue;
5562
8508
  }
@@ -5583,7 +8529,7 @@ function register7(deps) {
5583
8529
  try {
5584
8530
  await client.deleteBinding(ctx, binding.binding_id, userId);
5585
8531
  } catch (e) {
5586
- logger11.warn(`删除用户 ${userId} 服务端绑定 ${binding.binding_id} 失败:${e}`);
8532
+ logger14.warn(`删除用户 ${userId} 服务端绑定 ${binding.binding_id} 失败:${e}`);
5587
8533
  }
5588
8534
  }
5589
8535
  totalInvalid++;
@@ -5622,7 +8568,7 @@ function register7(deps) {
5622
8568
  });
5623
8569
  }
5624
8570
  } catch (e) {
5625
- logger11.warn(`自动刷新用户 ${userId} 失败: ${e}`);
8571
+ logger14.warn(`自动刷新用户 ${userId} 失败: ${e}`);
5626
8572
  }
5627
8573
  }
5628
8574
  }, 3e4);
@@ -5633,7 +8579,7 @@ __name(register7, "register");
5633
8579
  // src/index.ts
5634
8580
  var name = "rocom";
5635
8581
  var inject = { required: ["puppeteer", "database"] };
5636
- var logger12 = new import_koishi12.Logger("rocom");
8582
+ var logger15 = new import_koishi15.Logger("rocom");
5637
8583
  var MENU_PAGE_TITLE = "洛克王国帮助菜单";
5638
8584
  var MENU_PAGE_SUBTITLE = "输入对应命令可查看功能,部分数据查询需要先完成账号绑定。";
5639
8585
  var MENU_GROUPS = [
@@ -5661,6 +8607,7 @@ var MENU_GROUPS = [
5661
8607
  { cmd: "洛克.交换大厅", desc: "查看交换大厅" },
5662
8608
  { cmd: "洛克.玩家", desc: "查询 ingame 玩家资料" },
5663
8609
  { cmd: "洛克.家园", desc: "查询家园菜园" },
8610
+ { cmd: "洛克.家园详情", desc: "家园精灵完整数据" },
5664
8611
  { cmd: "洛克.商店", desc: "查询 ingame 商店" },
5665
8612
  { cmd: "洛克.日历", desc: "查看活动日历" },
5666
8613
  { cmd: "洛克.公告", desc: "查看公告列表" },
@@ -5676,14 +8623,19 @@ var MENU_GROUPS = [
5676
8623
  { cmd: "取消订阅远行商人", desc: "取消商人提醒" },
5677
8624
  { cmd: "订阅家园菜园", desc: "菜园成熟提醒" },
5678
8625
  { cmd: "订阅家园灵感", desc: "精灵灵感提醒" },
5679
- { cmd: "取消订阅家园", desc: "取消家园订阅" }
8626
+ { cmd: "订阅家园生蛋", desc: "精灵生蛋提醒" },
8627
+ { cmd: "取消订阅家园", desc: "取消家园订阅" },
8628
+ { cmd: "订阅洛克公告", desc: "新公告推送提醒" },
8629
+ { cmd: "取消订阅洛克公告", desc: "取消公告订阅" }
5680
8630
  ]
5681
8631
  },
5682
8632
  {
5683
8633
  groupTitle: "百科与查蛋",
5684
8634
  menuItems: [
5685
- { cmd: "洛克.wiki", desc: "精灵 Wiki 查询" },
8635
+ { cmd: "洛克.wiki", desc: "Wiki 全局/分类查询" },
5686
8636
  { cmd: "洛克.技能", desc: "技能 Wiki 查询" },
8637
+ { cmd: "精灵图鉴", desc: "本地图鉴查图" },
8638
+ { cmd: "图鉴下载", desc: "下载图鉴缓存(管理员)" },
5687
8639
  { cmd: "洛克.查蛋", desc: "精灵查蛋 / 尺寸反查" },
5688
8640
  { cmd: "洛克.配种", desc: "配种查询" }
5689
8641
  ]
@@ -5709,56 +8661,69 @@ function buildMenuFallbackText() {
5709
8661
  return lines.join("\n").trimEnd();
5710
8662
  }
5711
8663
  __name(buildMenuFallbackText, "buildMenuFallbackText");
5712
- var Config = import_koishi12.Schema.intersect([
5713
- import_koishi12.Schema.object({
5714
- apiBaseUrl: import_koishi12.Schema.string().default("https://wegame.shallow.ink").description("API 基础地址"),
5715
- wegameApiKey: import_koishi12.Schema.string().default("").description("WeGame API Key(获取 key 查看 Github:https://github.com/Entropy-Increase-Team/koishi-plugin-rocom )"),
5716
- qqLoginDebugMode: import_koishi12.Schema.boolean().default(false).description("QQ 扫码登录调试模式,仅调试时开启"),
5717
- adminUserIds: import_koishi12.Schema.array(String).default([]).description("管理员用户 ID 列表"),
5718
- autoRefreshEnabled: import_koishi12.Schema.boolean().default(false).description("启用自动刷新凭证"),
5719
- 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("自动刷新时间")
5720
8672
  }).description("基础设置"),
5721
- import_koishi12.Schema.object({
5722
- imageCompressionEnabled: import_koishi12.Schema.boolean().default(true).description("发送图片前启用 PNG 无损压缩"),
5723
- imageCompressionMinBytes: import_koishi12.Schema.number().default(262144).description("触发压缩的最小图片大小,单位字节"),
5724
- 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")
5725
8677
  }).description("图片压缩设置"),
5726
- import_koishi12.Schema.object({
5727
- merchantSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("启用远行商人订阅"),
5728
- merchantSubscriptionItems: import_koishi12.Schema.array(String).default(["国王球", "棱镜球", "炫彩精灵蛋"]).description("默认订阅商品"),
5729
- merchantUiStyle: import_koishi12.Schema.union(["new", "old"]).default("new").description("远行商人 UI 样式:new 为新版大图卡片,old 为旧版列表卡片"),
5730
- merchantCheckMode: import_koishi12.Schema.union(["interval", "times"]).default("interval").description("商人检查模式:interval 定期轮询 / times 指定时间点"),
5731
- merchantCheckInterval: import_koishi12.Schema.number().default(3e5).description("商人检查间隔,单位毫秒(仅 interval 模式生效)"),
5732
- merchantCheckTimes: import_koishi12.Schema.array(String).default([]).description("商人定时检查时间点(HH:MM 格式,如 08:00,仅 times 模式生效)"),
5733
- merchantPrivateSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("允许个人私聊订阅远行商人推送"),
5734
- homeSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("启用家园菜园和灵感订阅推送"),
5735
- homeSubscriptionIntervalMinutes: import_koishi12.Schema.number().default(5).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("公告订阅检查间隔,单位分钟")
5736
8690
  }).description("订阅推送设置"),
5737
- import_koishi12.Schema.object({
5738
- homeQueryWaitMs: import_koishi12.Schema.number().default(5e3).description("家园查询服务端同步等待毫秒(long-poll,超过此时间未出结果则转入排队)"),
5739
- homeQueryPollIntervalMs: import_koishi12.Schema.number().default(3e3).description("家园查询进入排队后的轮询间隔,单位毫秒"),
5740
- homeQueryTimeoutMs: import_koishi12.Schema.number().default(18e4).description("家园查询排队等候的总超时,单位毫秒,超时后提示稍后重试")
5741
- }).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("图鉴下载设置")
5742
8704
  ]);
5743
8705
  function apply(ctx, config) {
5744
8706
  setupRoleTokenModel(ctx);
5745
8707
  const client = new RocomClient(config.apiBaseUrl, config.wegameApiKey);
5746
- const dataDir = import_node_path5.default.join(ctx.baseDir, "data", "rocom");
8708
+ const dataDir = import_node_path6.default.join(ctx.baseDir, "data", "rocom");
5747
8709
  const userMgr = new UserManager(dataDir);
5748
8710
  const merchantSubMgr = new MerchantSubscriptionManager(dataDir);
5749
8711
  const homeSubMgr = new HomeSubscriptionManager(dataDir);
5750
- const resPath = import_node_path5.default.resolve(__dirname, "..");
8712
+ const announcementSubMgr = new AnnouncementSubscriptionManager(dataDir);
8713
+ const resPath = import_node_path6.default.resolve(__dirname, "..");
5751
8714
  const renderer = new Renderer(resPath);
5752
- 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");
5753
- 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");
5754
8717
  const eggService = new EggService(searcheggsDir);
5755
- 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 };
5756
8721
  ctx.on("ready", () => {
5757
8722
  migrateRoleTokensToUserId(ctx).catch((err) => {
5758
- logger12.warn(`role token migration failed: ${err}`);
8723
+ logger15.warn(`role token migration failed: ${err}`);
5759
8724
  });
5760
8725
  migrateLegacyFrameworkTokens(ctx, userMgr).catch((err) => {
5761
- logger12.warn(`legacy framework token migration failed: ${err}`);
8726
+ logger15.warn(`legacy framework token migration failed: ${err}`);
5762
8727
  });
5763
8728
  });
5764
8729
  ctx.command("洛克", "洛克王国帮助菜单").action(async ({ session }) => {