koishi-plugin-rocom 1.0.18 → 1.0.19

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 (57) hide show
  1. package/lib/announcement-service.d.ts +21 -0
  2. package/lib/announcement-service.js +63 -0
  3. package/lib/async-control.d.ts +3 -0
  4. package/lib/async-control.js +48 -0
  5. package/lib/client.d.ts +48 -7
  6. package/lib/client.js +244 -125
  7. package/lib/commands/egg.js +66 -4
  8. package/lib/commands/merchant.js +144 -146
  9. package/lib/commands/query.js +148 -117
  10. package/lib/commands/ranking.d.ts +2 -0
  11. package/lib/commands/ranking.js +31 -0
  12. package/lib/commands/share-code.d.ts +2 -0
  13. package/lib/commands/share-code.js +66 -0
  14. package/lib/commands/tools.js +47 -59
  15. package/lib/egg-service.d.ts +82 -8
  16. package/lib/egg-service.js +235 -19
  17. package/lib/img//345/274/202/350/211/262.png +0 -0
  18. package/lib/img//347/202/253/345/275/251.png +0 -0
  19. package/lib/index.d.ts +5 -0
  20. package/lib/index.js +16 -3
  21. package/lib/merchant-clock.d.ts +22 -0
  22. package/lib/merchant-clock.js +95 -0
  23. package/lib/merchant-service.d.ts +24 -0
  24. package/lib/merchant-service.js +148 -0
  25. package/lib/permissions.d.ts +16 -0
  26. package/lib/permissions.js +66 -0
  27. package/lib/pet-data-service.js +7 -9
  28. package/lib/pet-size.d.ts +13 -0
  29. package/lib/pet-size.js +54 -0
  30. package/lib/player-service.d.ts +37 -0
  31. package/lib/player-service.js +201 -0
  32. package/lib/ranking-service.d.ts +46 -0
  33. package/lib/ranking-service.js +137 -0
  34. package/lib/render-templates/announcement/detail.html +8 -0
  35. package/lib/render-templates/announcement/list.html +5 -0
  36. package/lib/render-templates/announcement/style.css +1 -0
  37. package/lib/render-templates/pet-ranking/index.html +99 -0
  38. package/lib/render-templates/pet-ranking/style.css +338 -0
  39. package/lib/render-templates/player-search/index.html +7 -0
  40. package/lib/render-templates/searcheggs/index.html +1 -1
  41. package/lib/render-templates/searcheggs/size.html +70 -68
  42. package/lib/render-templates/share-code-team/index.html +105 -0
  43. package/lib/render-templates/share-code-team/style.css +345 -0
  44. package/lib/render.d.ts +15 -2
  45. package/lib/render.js +120 -37
  46. package/lib/send-image.d.ts +1 -1
  47. package/lib/send-image.js +12 -1
  48. package/lib/share-code-service.d.ts +32 -0
  49. package/lib/share-code-service.js +184 -0
  50. package/lib/subscription-runner.d.ts +6 -0
  51. package/lib/subscription-runner.js +21 -0
  52. package/lib/subscription-send.d.ts +4 -2
  53. package/lib/subscription-send.js +45 -20
  54. package/lib/types.d.ts +5 -0
  55. package/lib/user.d.ts +5 -0
  56. package/package.json +4 -1
  57. package/readme.md +145 -124
@@ -9,6 +9,7 @@ exports.formatEggGroups = formatEggGroups;
9
9
  const koishi_1 = require("koishi");
10
10
  const node_fs_1 = __importDefault(require("node:fs"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
+ const pet_size_1 = require("./pet-size");
12
13
  const logger = new koishi_1.Logger('rocom-egg');
13
14
  exports.EGG_GROUP_META = {
14
15
  1: { label: '未发现', desc: '不能和任何精灵生蛋' },
@@ -70,6 +71,8 @@ function fmtRange(lo, hi, u) {
70
71
  return `${lo ?? hi}${u}`;
71
72
  }
72
73
  function num(value) {
74
+ if (value === null || value === undefined || value === '')
75
+ return null;
73
76
  const parsed = Number(value);
74
77
  return Number.isFinite(parsed) ? parsed : null;
75
78
  }
@@ -275,33 +278,60 @@ class EggService {
275
278
  weight_min: weightMin,
276
279
  weight_max: weightMax,
277
280
  weight_label: fmtRange(weightMin, weightMax, 'kg'),
281
+ ...(0, pet_size_1.sizeVariantPayload)(queryWeight, pet?.breeding?.weight_low == null ? null : pet.breeding.weight_low / 1000, pet?.breeding?.weight_high == null ? null : pet.breeding.weight_high / 1000),
278
282
  probability,
279
283
  match_count: matchCount,
280
284
  match_info_label: this.formatMatchSummary(probability, matchCount),
281
285
  };
282
286
  }
283
- formatSizeApiCard(item) {
284
- // 新版 /wiki/pet-size/query 返回 items[].pet / egg_size / match 结构;旧接口是平铺字段。
287
+ formatSizeApiCard(item, queryWeight) {
288
+ // 兼容 /wiki/pet-size/query 的 items[].pet/egg_size/match 结构与 /egg/search 的
289
+ // height_range_m/cm、weight_range_kg/g、r_value/range_area 平铺结构。
285
290
  const petInfo = item?.pet && typeof item.pet === 'object' ? item.pet : {};
286
291
  const eggSize = item?.egg_size && typeof item.egg_size === 'object' ? item.egg_size : {};
287
292
  const heightRange = eggSize?.height && typeof eggSize.height === 'object' ? eggSize.height : {};
288
293
  const weightRange = eggSize?.weight && typeof eggSize.weight === 'object' ? eggSize.weight : {};
289
294
  const match = item?.match && typeof item.match === 'object' ? item.match : {};
290
- const petId = petInfo?.pet_id || item?.petId || item?.pet_id || '-';
291
- const petName = petInfo?.name || (typeof item?.pet === 'string' ? item.pet : '') || item?.name || '未知精灵';
295
+ const petId = petInfo?.pet_id || petInfo?.id || item?.petId || item?.pet_id || item?.id || '-';
296
+ const petForm = String(petInfo?.form || item?.form || '').trim();
297
+ let petName = petInfo?.name || (typeof item?.pet === 'string' ? item.pet : '') || item?.name || item?.pet_name || '未知精灵';
298
+ if (petForm && !String(petName).includes(petForm))
299
+ petName = `${petName}(${petForm})`;
292
300
  const typeNames = Array.isArray(petInfo?.type_names) ? petInfo.type_names.filter(Boolean) : [];
293
301
  const eggGroupNames = Array.isArray(petInfo?.egg_group_names) ? petInfo.egg_group_names.filter(Boolean) : [];
302
+ const [heightRangeMMin, heightRangeMMax] = this.rangePair(item?.height_range_m);
303
+ const [heightRangeCmMin, heightRangeCmMax] = this.rangePair(item?.height_range_cm);
304
+ const [weightRangeKgMin, weightRangeKgMax] = this.rangePair(item?.weight_range_kg);
305
+ const [weightRangeGMin, weightRangeGMax] = this.rangePair(item?.weight_range_g);
294
306
  const probability = num(item?.probability) ?? num(match?.percent);
295
307
  const matchCount = num(item?.matchCount);
296
- const heightMin = num(heightRange?.min_m ?? item?.diameterMin);
297
- const heightMax = num(heightRange?.max_m ?? item?.diameterMax);
298
- const weightMin = num(weightRange?.min_kg ?? item?.weightMin);
299
- const weightMax = num(weightRange?.max_kg ?? item?.weightMax);
308
+ const heightMin = num(heightRange?.min_m) ?? heightRangeMMin
309
+ ?? (heightRangeCmMin == null ? null : heightRangeCmMin / 100)
310
+ ?? num(item?.diameterMin);
311
+ const heightMax = num(heightRange?.max_m) ?? heightRangeMMax
312
+ ?? (heightRangeCmMax == null ? null : heightRangeCmMax / 100)
313
+ ?? num(item?.diameterMax);
314
+ const weightMin = num(weightRange?.min_kg) ?? weightRangeKgMin
315
+ ?? (weightRangeGMin == null ? null : weightRangeGMin / 1000)
316
+ ?? num(item?.weightMin);
317
+ const weightMax = num(weightRange?.max_kg) ?? weightRangeKgMax
318
+ ?? (weightRangeGMax == null ? null : weightRangeGMax / 1000)
319
+ ?? num(item?.weightMax);
320
+ const rValue = num(item?.r_value);
321
+ const rangeArea = num(item?.range_area);
322
+ const sizeVariant = (0, pet_size_1.sizeVariantPayload)(queryWeight, weightMin, weightMax, fmtRange(weightMin, weightMax, 'kg'));
323
+ const fallbackMatchLabel = [
324
+ rValue != null ? `R值 ${formatNumber(rValue, 3)}` : '',
325
+ rangeArea != null ? `范围面积 ${formatNumber(rangeArea, 0)}` : '',
326
+ ].filter(Boolean).join(' / ');
327
+ const matchInfoLabel = String(match?.match_percent_text || '')
328
+ || fallbackMatchLabel
329
+ || this.formatMatchSummary(probability, matchCount);
300
330
  return {
301
331
  id: petId,
302
332
  name: petName,
303
- icon: petInfo?.icon || item?.petIcon || petIconUrl(petId),
304
- image: petInfo?.small_icon || item?.petImage || petImageUrl(petId),
333
+ icon: petInfo?.icon || item?.pet_icon_url || item?.petIcon || petIconUrl(petId),
334
+ image: petInfo?.small_icon || item?.pet_img_url || item?.petImage || petImageUrl(petId),
305
335
  type_label: typeNames.length ? typeNames.join(' / ') : '后端未提供',
306
336
  egg_group_ids: [],
307
337
  egg_groups_label: eggGroupNames.length ? eggGroupNames.join(' / ') : '后端未提供',
@@ -311,11 +341,18 @@ class EggService {
311
341
  weight_min: weightMin,
312
342
  weight_max: weightMax,
313
343
  weight_label: fmtRange(weightMin, weightMax, 'kg'),
344
+ query_weight: queryWeight,
345
+ ...sizeVariant,
314
346
  probability,
315
347
  match_count: matchCount,
316
- match_info_label: String(match?.match_percent_text || '') || this.formatMatchSummary(probability, matchCount),
348
+ match_info_label: matchInfoLabel,
317
349
  };
318
350
  }
351
+ rangePair(value) {
352
+ if (Array.isArray(value) && value.length >= 2)
353
+ return [num(value[0]), num(value[1])];
354
+ return [null, null];
355
+ }
319
356
  // 新版尺寸反查按 match.layer 划分完美/范围匹配。
320
357
  splitNewSizeApiItems(items) {
321
358
  const perfect = [];
@@ -473,7 +510,7 @@ class EggService {
473
510
  lines.push(`✅ 完美匹配 ${condStr} 的精灵(共 ${results.perfect.length} 只):`);
474
511
  results.perfect.slice(0, 10).forEach((p, i) => {
475
512
  const br = p.breeding || {};
476
- lines.push(` ${i + 1}. ${petName(p)} (#${p.id}) — ${fmtRange(ht(br.height_low), ht(br.height_high), 'm')} / ${fmtRange(wt(br.weight_low), wt(br.weight_high), 'kg')} · ${formatEggGroups(this.getEggGroups(p))}`);
513
+ lines.push(` ${i + 1}. ${petName(p)} (#${p.id}) — ${fmtRange(ht(br.height_low), ht(br.height_high), 'm')} / ${fmtRange(wt(br.weight_low), wt(br.weight_high), 'kg')} · ${formatEggGroups(this.getEggGroups(p))}${this.formatPetCard(p, height, weight).size_variant_label ? ' · 【' + this.formatPetCard(p, height, weight).size_variant_label + '】' : ''}`);
477
514
  });
478
515
  }
479
516
  if (results.range.length) {
@@ -482,7 +519,7 @@ class EggService {
482
519
  lines.push(`🔍 范围匹配 ${condStr} 的精灵(共 ${results.range.length} 只,容差±15%):`);
483
520
  results.range.slice(0, 10).forEach((p, i) => {
484
521
  const br = p.breeding || {};
485
- lines.push(` ${i + 1}. ${petName(p)} (#${p.id}) — ${fmtRange(ht(br.height_low), ht(br.height_high), 'm')} / ${fmtRange(wt(br.weight_low), wt(br.weight_high), 'kg')} · ${formatEggGroups(this.getEggGroups(p))}`);
522
+ lines.push(` ${i + 1}. ${petName(p)} (#${p.id}) — ${fmtRange(ht(br.height_low), ht(br.height_high), 'm')} / ${fmtRange(wt(br.weight_low), wt(br.weight_high), 'kg')} · ${formatEggGroups(this.getEggGroups(p))}${this.formatPetCard(p, height, weight).size_variant_label ? ' · 【' + this.formatPetCard(p, height, weight).size_variant_label + '】' : ''}`);
486
523
  });
487
524
  }
488
525
  return lines.join('\n');
@@ -501,8 +538,8 @@ class EggService {
501
538
  if (exact.length) {
502
539
  lines.push(`✅ 完美匹配 ${condStr} 的精灵(共 ${exact.length} 只):`);
503
540
  exact.slice(0, 10).forEach((item, i) => {
504
- const card = this.formatSizeApiCard(item);
505
- lines.push(` ${i + 1}. ${card.name} (#${card.id}) — ${card.height_label} / ${card.weight_label}`);
541
+ const card = this.formatSizeApiCard(item, weight);
542
+ lines.push(` ${i + 1}. ${card.name} (#${card.id}) — ${card.height_label} / ${card.weight_label}${card.size_variant_label ? ` · 【${card.size_variant_label}】` : ''}`);
506
543
  });
507
544
  }
508
545
  if (candidates.length) {
@@ -510,8 +547,8 @@ class EggService {
510
547
  lines.push('');
511
548
  lines.push(`🔍 范围匹配 ${condStr} 的精灵(共 ${candidates.length} 只):`);
512
549
  candidates.slice(0, 10).forEach((item, i) => {
513
- const card = this.formatSizeApiCard(item);
514
- lines.push(` ${i + 1}. ${card.name} (#${card.id}) — ${card.height_label} / ${card.weight_label}`);
550
+ const card = this.formatSizeApiCard(item, weight);
551
+ lines.push(` ${i + 1}. ${card.name} (#${card.id}) — ${card.height_label} / ${card.weight_label}${card.size_variant_label ? ` · 【${card.size_variant_label}】` : ''}`);
515
552
  });
516
553
  }
517
554
  return lines.join('\n');
@@ -534,7 +571,7 @@ class EggService {
534
571
  `🥚 ${data?.pet_name || '未知精灵'} (#${data?.pet_id || '-'})`,
535
572
  `属性:${data?.type_label || '未知'}`,
536
573
  `蛋组:${data?.egg_groups_label || '暂无蛋组数据'}`,
537
- `可配种精灵数:${data?.total_compatible || 0}`,
574
+ `${data?.compatible_total_known === false ? '当前预览精灵数' : '可配种精灵数'}:${data?.total_compatible || 0}`,
538
575
  ];
539
576
  if (data?.is_undiscovered)
540
577
  lines.push('⚠️ 该精灵属于「未发现」蛋组,无法配种。');
@@ -792,6 +829,185 @@ class EggService {
792
829
  copyright: 'Koishi & WeGame 洛克王国插件',
793
830
  };
794
831
  }
832
+ // ─── 后端 Egg API 转换(上游 v3.8.0)───
833
+ buildCandidatesFromEggApi(keyword, candidates) {
834
+ return {
835
+ keyword,
836
+ count: candidates.length,
837
+ candidates: candidates.map(item => this.formatEggApiCard(item)),
838
+ commandHint: '请使用更精确的名称重新查询',
839
+ copyright: 'Koishi & WeGame 洛克王国插件',
840
+ };
841
+ }
842
+ buildSearchDataFromEggApi(pet, compatibleByGroup = {}) {
843
+ const groups = this.eggApiGroups(pet);
844
+ const groupIds = [];
845
+ const groupLabels = {};
846
+ for (const group of groups) {
847
+ const id = this.eggGroupId(group);
848
+ if (id === null || groupIds.includes(id))
849
+ continue;
850
+ groupIds.push(id);
851
+ groupLabels[id] = this.eggGroupLabel(group);
852
+ }
853
+ const seen = new Set();
854
+ let totalCompatible = 0;
855
+ const sections = groups.map((group) => {
856
+ const gid = this.eggGroupId(group);
857
+ const label = this.eggGroupLabel(group);
858
+ if (gid === null)
859
+ return null;
860
+ if (this.isUndiscoveredEggGroup(gid, label)) {
861
+ return {
862
+ id: gid,
863
+ label: label || '未发现',
864
+ desc: '不能和任何精灵生蛋,多用于传说中的精灵',
865
+ count: 0,
866
+ members: [],
867
+ has_more: false,
868
+ total: 0,
869
+ };
870
+ }
871
+ const groupResult = compatibleByGroup[String(gid)] ?? compatibleByGroup[gid] ?? {};
872
+ const rawMembers = Array.isArray(groupResult)
873
+ ? groupResult
874
+ : (Array.isArray(groupResult?.items) ? groupResult.items : []);
875
+ const members = [];
876
+ for (const item of rawMembers) {
877
+ const itemId = item?.id ?? item?.pet_id;
878
+ if (String(itemId ?? '') === String(pet?.id ?? pet?.pet_id ?? ''))
879
+ continue;
880
+ const key = String(itemId ?? item?.name ?? '');
881
+ if (key && !seen.has(key)) {
882
+ seen.add(key);
883
+ totalCompatible += 1;
884
+ }
885
+ members.push(this.formatEggApiMember(item));
886
+ }
887
+ const total = num(groupResult?.total) ?? members.length;
888
+ return {
889
+ id: gid,
890
+ label,
891
+ desc: this.eggGroupDesc(group),
892
+ count: total,
893
+ members: members.slice(0, 30),
894
+ has_more: total > 30 || members.length > 30,
895
+ total,
896
+ };
897
+ }).filter(Boolean);
898
+ const [heightMin, heightMax] = this.eggHeightRangeM(pet);
899
+ const [weightMin, weightMax] = this.eggWeightRangeKg(pet);
900
+ const petId = pet?.id ?? pet?.pet_id ?? '-';
901
+ return {
902
+ pet_name: this.eggPetDisplayName(pet),
903
+ pet_id: petId,
904
+ pet_icon: pet?.pet_icon_url || pet?.icon_url || petIconUrl(petId),
905
+ pet_image: pet?.pet_img_url || pet?.image_url || petImageUrl(petId),
906
+ type_label: this.eggApiTypeLabel(pet),
907
+ egg_groups_label: groupIds.map(id => groupLabels[id] || String(id)).join(' / ') || '暂无蛋组数据',
908
+ egg_groups: groupIds,
909
+ egg_group_labels: groupLabels,
910
+ male_rate: null,
911
+ female_rate: null,
912
+ hatch_label: '后端未提供',
913
+ weight_label: fmtRange(weightMin, weightMax, 'kg'),
914
+ height_label: fmtRange(heightMin, heightMax, 'm'),
915
+ total_compatible: num(compatibleByGroup.__all__?.total) ?? totalCompatible,
916
+ compatible_total_known: num(compatibleByGroup.__all__?.total) !== null,
917
+ is_undiscovered: groups.some(group => this.isUndiscoveredEggGroup(this.eggGroupId(group), this.eggGroupLabel(group))),
918
+ egg_group_sections: sections,
919
+ total_stats: '后端未提供',
920
+ egg_details: { has_data: false },
921
+ commandHint: '数据来自后端查蛋;接口不可用时自动回退 Wiki / 本地查蛋' + (num(compatibleByGroup.__all__?.total) === null ? '。总数不可用,当前数量仅为已加载预览' : ''),
922
+ copyright: 'Koishi & WeGame 洛克王国插件',
923
+ };
924
+ }
925
+ formatEggApiCard(item) {
926
+ const petId = item?.id ?? item?.pet_id ?? '-';
927
+ const [heightMin, heightMax] = this.eggHeightRangeM(item);
928
+ const [weightMin, weightMax] = this.eggWeightRangeKg(item);
929
+ return {
930
+ id: petId,
931
+ name: this.eggPetDisplayName(item),
932
+ icon: item?.pet_icon_url || item?.icon_url || petIconUrl(petId),
933
+ image: item?.pet_img_url || item?.image_url || petImageUrl(petId),
934
+ type_label: this.eggApiTypeLabel(item),
935
+ egg_group_ids: [],
936
+ egg_groups_label: this.eggGroupsLabel(item),
937
+ height_min: heightMin,
938
+ height_max: heightMax,
939
+ height_label: fmtRange(heightMin, heightMax, 'm'),
940
+ weight_min: weightMin,
941
+ weight_max: weightMax,
942
+ weight_label: fmtRange(weightMin, weightMax, 'kg'),
943
+ probability: null,
944
+ match_count: null,
945
+ match_info_label: '后端命中',
946
+ };
947
+ }
948
+ formatEggApiMember(item) {
949
+ return {
950
+ name: this.eggPetDisplayName(item),
951
+ id: item?.id ?? item?.pet_id ?? '-',
952
+ type_label: this.eggApiTypeLabel(item),
953
+ egg_groups_label: this.eggGroupsLabel(item),
954
+ };
955
+ }
956
+ eggApiGroups(item) {
957
+ const groups = item?.egg_groups ?? item?.egg_group ?? [];
958
+ const list = Array.isArray(groups) ? groups : [groups];
959
+ return list
960
+ .filter(Boolean)
961
+ .map(group => (group && typeof group === 'object' ? group : { name: String(group), official_name: String(group) }));
962
+ }
963
+ eggGroupId(group) {
964
+ return num(group?.group_id ?? group?.id);
965
+ }
966
+ eggGroupLabel(group) {
967
+ return String(group?.official_name || group?.name || group?.display_name || '').trim();
968
+ }
969
+ eggGroupDesc(group) {
970
+ const label = this.eggGroupLabel(group);
971
+ const display = String(group?.display_name || '').trim();
972
+ return display && display !== label ? display : '';
973
+ }
974
+ isUndiscoveredEggGroup(id, label) {
975
+ const text = String(label || '');
976
+ return String(id) === '1' || text.includes('未发现') || text.includes('无法孵蛋');
977
+ }
978
+ eggPetDisplayName(item) {
979
+ const name = String(item?.name || item?.pet_name || '未知精灵').trim() || '未知精灵';
980
+ const form = String(item?.form || '').trim();
981
+ return form && !name.includes(form) ? `${name}(${form})` : name;
982
+ }
983
+ eggApiTypeLabel(item) {
984
+ const types = item?.unit_type;
985
+ if (Array.isArray(types) && types.length)
986
+ return types.filter(Boolean).join(' / ') || '未知';
987
+ return String(item?.type || item?.attribute_name || '未知');
988
+ }
989
+ eggGroupsLabel(item) {
990
+ const labels = this.eggApiGroups(item).map(group => this.eggGroupLabel(group)).filter(Boolean);
991
+ return labels.length ? labels.join(' / ') : '暂无蛋组数据';
992
+ }
993
+ eggHeightRangeM(item) {
994
+ const direct = this.rangePair(item?.height_range_m);
995
+ if (direct[0] != null || direct[1] != null)
996
+ return direct;
997
+ const cm = this.rangePair(item?.height_range_cm);
998
+ if (cm[0] != null || cm[1] != null)
999
+ return [cm[0] == null ? null : cm[0] / 100, cm[1] == null ? null : cm[1] / 100];
1000
+ return [null, null];
1001
+ }
1002
+ eggWeightRangeKg(item) {
1003
+ const direct = this.rangePair(item?.weight_range_kg);
1004
+ if (direct[0] != null || direct[1] != null)
1005
+ return direct;
1006
+ const g = this.rangePair(item?.weight_range_g);
1007
+ if (g[0] != null || g[1] != null)
1008
+ return [g[0] == null ? null : g[0] / 1000, g[1] == null ? null : g[1] / 1000];
1009
+ return [null, null];
1010
+ }
795
1011
  buildSizeSearchData(height, weight, results, heightDisplay) {
796
1012
  const conditions = [];
797
1013
  if (height != null)
@@ -816,7 +1032,7 @@ class EggService {
816
1032
  if (weight != null)
817
1033
  conditions.push(`体重 ${weight} kg`);
818
1034
  const groups = this.sizeApiResultGroups(results);
819
- const [perfect, ranged] = this.mergeCardsByName(groups.exact.map((item) => this.formatSizeApiCard(item)), groups.candidates.map((item) => this.formatSizeApiCard(item)));
1035
+ const [perfect, ranged] = this.mergeCardsByName(groups.exact.map((item) => this.formatSizeApiCard(item, weight)), groups.candidates.map((item) => this.formatSizeApiCard(item, weight)));
820
1036
  const poolName = results?.query?.pool && typeof results.query.pool === 'object'
821
1037
  ? results.query.pool.name
822
1038
  : '';
package/lib/index.d.ts CHANGED
@@ -17,6 +17,10 @@ export interface Config {
17
17
  merchantCheckMode: 'interval' | 'times';
18
18
  merchantCheckInterval: number;
19
19
  merchantCheckTimes: string[];
20
+ merchantTimezone: string;
21
+ subscriptionGroupAdminEnabled: boolean;
22
+ subscriptionBotAdminEnabled: boolean;
23
+ subscriptionBotAdminAuthority: number;
20
24
  homeSubscriptionEnabled: boolean;
21
25
  homeSubscriptionIntervalMinutes: number;
22
26
  announcementSubscriptionEnabled: boolean;
@@ -25,6 +29,7 @@ export interface Config {
25
29
  homeQueryPollIntervalMs: number;
26
30
  homeQueryTimeoutMs: number;
27
31
  lowBandwidthMode: boolean;
32
+ renderTimeout: number;
28
33
  atlasZipUrls: string[];
29
34
  atlasGitUrl: string;
30
35
  imageCompressionEnabled: boolean;
package/lib/index.js CHANGED
@@ -22,6 +22,8 @@ const wiki_1 = require("./commands/wiki");
22
22
  const egg_1 = require("./commands/egg");
23
23
  const tools_1 = require("./commands/tools");
24
24
  const admin_1 = require("./commands/admin");
25
+ const ranking_1 = require("./commands/ranking");
26
+ const share_code_1 = require("./commands/share-code");
25
27
  const send_image_1 = require("./send-image");
26
28
  exports.name = 'rocom';
27
29
  exports.inject = { required: ['puppeteer', 'database'] };
@@ -51,10 +53,10 @@ const MENU_GROUPS = [
51
53
  { cmd: '洛克.阵容', desc: '查看阵容推荐' },
52
54
  { cmd: '查看阵容', desc: '查看阵容详情' },
53
55
  { cmd: '洛克.交换大厅', desc: '查看交换大厅' },
54
- { cmd: '洛克.玩家', desc: '查询 ingame 玩家资料' },
56
+ { cmd: '洛克.玩家', desc: '查询 ingame 玩家资料(可选 UID)' },
55
57
  { cmd: '洛克.家园', desc: '查询家园菜园' },
56
58
  { cmd: '洛克.家园详情', desc: '家园精灵完整数据' },
57
- { cmd: '洛克.商店', desc: '查询 ingame 商店' },
59
+ { cmd: '洛克.商店', desc: '查询 ingame 商店(默认 3009)' },
58
60
  { cmd: '洛克.日历', desc: '查看活动日历' },
59
61
  { cmd: '洛克.公告', desc: '查看公告列表' },
60
62
  { cmd: '洛克.好友关系', desc: '查询好友关系' },
@@ -84,6 +86,10 @@ const MENU_GROUPS = [
84
86
  { cmd: '图鉴下载', desc: '下载图鉴缓存(管理员)' },
85
87
  { cmd: '洛克.查蛋', desc: '精灵查蛋 / 尺寸反查' },
86
88
  { cmd: '洛克.配种', desc: '配种查询' },
89
+ { cmd: '异色排行榜', desc: '异色精灵收集排行 [UID] [数量]' },
90
+ { cmd: '炫彩排行榜', desc: '炫彩精灵收集排行 [UID] [数量]' },
91
+ { cmd: '阵容码 解析', desc: '解析阵容分享码或链接' },
92
+ { cmd: '阵容码 查询', desc: '查询阵容分享码历史' },
87
93
  ],
88
94
  },
89
95
  ];
@@ -126,7 +132,11 @@ exports.Config = koishi_1.Schema.intersect([
126
132
  merchantCheckMode: koishi_1.Schema.union(['interval', 'times']).default('interval').description('商人检查模式:interval 定期轮询 / times 指定时间点'),
127
133
  merchantCheckInterval: koishi_1.Schema.number().default(300000).description('商人检查间隔,单位毫秒(仅 interval 模式生效)'),
128
134
  merchantCheckTimes: koishi_1.Schema.array(String).default([]).description('商人定时检查时间点(HH:MM 格式,如 08:00,仅 times 模式生效)'),
135
+ merchantTimezone: koishi_1.Schema.string().default('Asia/Shanghai').description('远行商人时区(IANA 名称,如 Asia/Shanghai;默认 UTC+8)'),
129
136
  merchantPrivateSubscriptionEnabled: koishi_1.Schema.boolean().default(true).description('允许个人私聊订阅远行商人推送'),
137
+ subscriptionGroupAdminEnabled: koishi_1.Schema.boolean().default(true).description('允许群主/管理员管理当前群订阅(对应上游 merchant_group_admin_enabled)'),
138
+ subscriptionBotAdminEnabled: koishi_1.Schema.boolean().default(true).description('允许白名单/高权限 Bot 管理员管理订阅(对应上游 merchant_bot_admin_enabled)'),
139
+ subscriptionBotAdminAuthority: koishi_1.Schema.number().default(4).description('Bot 管理员 authority 阈值;达到该值即视为 Bot 管理员'),
130
140
  homeSubscriptionEnabled: koishi_1.Schema.boolean().default(true).description('启用家园菜园和灵感订阅推送'),
131
141
  homeSubscriptionIntervalMinutes: koishi_1.Schema.number().default(5).description('家园订阅检查间隔,单位分钟'),
132
142
  announcementSubscriptionEnabled: koishi_1.Schema.boolean().default(true).description('启用洛克公告订阅推送'),
@@ -137,6 +147,7 @@ exports.Config = koishi_1.Schema.intersect([
137
147
  homeQueryPollIntervalMs: koishi_1.Schema.number().default(3000).description('家园查询进入排队后的轮询间隔,单位毫秒'),
138
148
  homeQueryTimeoutMs: koishi_1.Schema.number().default(180000).description('家园查询排队等候的总超时,单位毫秒,超时后提示稍后重试'),
139
149
  lowBandwidthMode: koishi_1.Schema.boolean().default(false).description('低带宽模式:家园详情不再加载技能图标,降低长图生成压力'),
150
+ renderTimeout: koishi_1.Schema.number().default(30000).description('单次渲染总超时,单位毫秒(导航、资源等待与截图共享截止时间)'),
140
151
  }).description('家园查询排队设置'),
141
152
  koishi_1.Schema.object({
142
153
  atlasZipUrls: koishi_1.Schema.array(String).default([
@@ -155,7 +166,7 @@ function apply(ctx, config) {
155
166
  const homeSubMgr = new user_1.HomeSubscriptionManager(dataDir);
156
167
  const announcementSubMgr = new user_1.AnnouncementSubscriptionManager(dataDir);
157
168
  const resPath = node_path_1.default.resolve(__dirname, '..');
158
- const renderer = new render_1.Renderer(resPath);
169
+ const renderer = new render_1.Renderer(resPath, { timeoutMs: config.renderTimeout || 30000 });
159
170
  const renderTemplateRoot = node_fs_1.default.existsSync(node_path_1.default.join(resPath, 'lib', 'render-templates'))
160
171
  ? node_path_1.default.join(resPath, 'lib', 'render-templates')
161
172
  : node_path_1.default.join(resPath, 'src', 'render-templates');
@@ -186,4 +197,6 @@ function apply(ctx, config) {
186
197
  (0, egg_1.register)(deps);
187
198
  (0, tools_1.register)(deps);
188
199
  (0, admin_1.register)(deps);
200
+ (0, ranking_1.register)(deps);
201
+ (0, share_code_1.register)(deps);
189
202
  }
@@ -0,0 +1,22 @@
1
+ export interface ClockParts {
2
+ date: string;
3
+ hour: number;
4
+ minute: number;
5
+ second: number;
6
+ }
7
+ export declare class MerchantClock {
8
+ private formatter;
9
+ private fixedOffset;
10
+ readonly zone: string;
11
+ constructor(requested?: string);
12
+ parts(nowMs?: number): ClockParts;
13
+ date(nowMs?: number): string;
14
+ /** Boundaries in absolute time, including 23/25-hour DST days. */
15
+ dayBounds(nowMs?: number): {
16
+ start: number;
17
+ end: number;
18
+ };
19
+ minuteOfDay(nowMs?: number): number;
20
+ timeText(nowMs?: number): string;
21
+ nextTimeFor(times: string[], nowMs?: number): number | null;
22
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MerchantClock = void 0;
4
+ function pad(value, width = 2) {
5
+ return String(value).padStart(width, '0');
6
+ }
7
+ // 统一时钟:默认上海时区失败时固定 +08:00,避免 UTC 服务器上商品日期/轮次错位。
8
+ class MerchantClock {
9
+ formatter;
10
+ fixedOffset = false;
11
+ zone;
12
+ constructor(requested = 'Asia/Shanghai') {
13
+ const wanted = requested || 'Asia/Shanghai';
14
+ let zone = wanted;
15
+ try {
16
+ new Intl.DateTimeFormat('en-CA', { timeZone: zone }).format(0);
17
+ }
18
+ catch {
19
+ if (wanted === 'Asia/Shanghai') {
20
+ zone = 'UTC';
21
+ this.fixedOffset = true;
22
+ }
23
+ else {
24
+ zone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
25
+ }
26
+ }
27
+ this.zone = this.fixedOffset ? 'UTC+08:00' : zone;
28
+ this.formatter = new Intl.DateTimeFormat('en-CA', {
29
+ timeZone: zone,
30
+ hourCycle: 'h23',
31
+ year: 'numeric',
32
+ month: '2-digit',
33
+ day: '2-digit',
34
+ hour: '2-digit',
35
+ minute: '2-digit',
36
+ second: '2-digit',
37
+ });
38
+ }
39
+ parts(nowMs = Date.now()) {
40
+ const timestamp = nowMs + (this.fixedOffset ? 8 * 3600_000 : 0);
41
+ const values = {};
42
+ for (const part of this.formatter.formatToParts(timestamp)) {
43
+ if (part.type !== 'literal')
44
+ values[part.type] = part.value;
45
+ }
46
+ return {
47
+ date: `${values.year}-${values.month}-${values.day}`,
48
+ hour: Number(values.hour),
49
+ minute: Number(values.minute),
50
+ second: Number(values.second),
51
+ };
52
+ }
53
+ date(nowMs = Date.now()) {
54
+ return this.parts(nowMs).date;
55
+ }
56
+ /** Boundaries in absolute time, including 23/25-hour DST days. */
57
+ dayBounds(nowMs = Date.now()) {
58
+ const day = this.date(nowMs);
59
+ const edge = (before) => {
60
+ let low = before ? nowMs - 48 * 3600_000 : nowMs;
61
+ let high = before ? nowMs : nowMs + 48 * 3600_000;
62
+ while (high - low > 1) {
63
+ const mid = Math.floor((low + high) / 2);
64
+ if (before ? this.date(mid) < day : this.date(mid) === day)
65
+ low = mid;
66
+ else
67
+ high = mid;
68
+ }
69
+ return high;
70
+ };
71
+ return { start: edge(true), end: edge(false) };
72
+ }
73
+ minuteOfDay(nowMs = Date.now()) {
74
+ const p = this.parts(nowMs);
75
+ return p.hour * 60 + p.minute;
76
+ }
77
+ timeText(nowMs = Date.now()) {
78
+ const p = this.parts(nowMs);
79
+ return `${pad(p.hour)}:${pad(p.minute)}`;
80
+ }
81
+ // 下一个匹配给定 HH:MM 墙钟列表的时刻(毫秒),扫描精度 1 分钟。
82
+ nextTimeFor(times, nowMs = Date.now()) {
83
+ const targets = new Set(times.map(item => String(item || '').trim()).filter(Boolean));
84
+ if (!targets.size)
85
+ return null;
86
+ let cursor = Math.floor(nowMs / 60000) * 60000 + 60000;
87
+ for (let i = 0; i < 60 * 24 * 8; i++) {
88
+ if (targets.has(this.timeText(cursor)))
89
+ return cursor;
90
+ cursor += 60000;
91
+ }
92
+ return null;
93
+ }
94
+ }
95
+ exports.MerchantClock = MerchantClock;
@@ -0,0 +1,24 @@
1
+ export interface MerchantItem {
2
+ id: string;
3
+ parentId?: string;
4
+ name: string;
5
+ icon: string;
6
+ price: number | null;
7
+ limit: number | null;
8
+ startsAt: number | null;
9
+ endsAt: number | null;
10
+ active: boolean;
11
+ category: string;
12
+ source: 'live' | 'legacy';
13
+ }
14
+ export interface NormalizedMerchant {
15
+ source: 'live' | 'legacy';
16
+ items: MerchantItem[];
17
+ }
18
+ export declare const numberOrNull: (v: unknown) => number | null;
19
+ export declare function livePrice(value: any): number | null;
20
+ export declare function merchantTimestampMs(value: any): number | null;
21
+ export declare function normalizeLiveMerchant(payload: any, nowMs?: number): MerchantItem[];
22
+ export declare function normalizeLegacyMerchant(payload: any, nowMs?: number): MerchantItem[];
23
+ export declare function isLiveMerchantPayload(payload: any): boolean;
24
+ export declare function normalizeMerchant(payload: any, nowMs?: number): NormalizedMerchant;