koishi-plugin-rocom 1.0.18 → 1.0.24

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 (216) 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/atlas-service.js +1 -1
  6. package/lib/client.d.ts +58 -7
  7. package/lib/client.js +282 -127
  8. package/lib/commands/egg.js +66 -4
  9. package/lib/commands/merchant.d.ts +105 -0
  10. package/lib/commands/merchant.js +600 -182
  11. package/lib/commands/query.d.ts +18 -0
  12. package/lib/commands/query.js +585 -145
  13. package/lib/commands/ranking.d.ts +2 -0
  14. package/lib/commands/ranking.js +31 -0
  15. package/lib/commands/share-code.d.ts +2 -0
  16. package/lib/commands/share-code.js +111 -0
  17. package/lib/commands/tools.js +48 -60
  18. package/lib/commands/wiki.js +1 -1
  19. package/lib/egg-service.d.ts +82 -8
  20. package/lib/egg-service.js +235 -19
  21. package/lib/element-colors.d.ts +3 -0
  22. package/lib/element-colors.js +34 -0
  23. package/lib/icon-metrics.d.ts +32 -0
  24. package/lib/icon-metrics.js +255 -0
  25. package/lib/img//345/274/202/350/211/262.png +0 -0
  26. package/lib/img//347/202/253/345/275/251.png +0 -0
  27. package/lib/index.d.ts +10 -0
  28. package/lib/index.js +11857 -175
  29. package/lib/merchant-catalog.d.ts +47 -0
  30. package/lib/merchant-catalog.js +193 -0
  31. package/lib/merchant-clock.d.ts +22 -0
  32. package/lib/merchant-clock.js +95 -0
  33. package/lib/merchant-price-log.d.ts +63 -0
  34. package/lib/merchant-price-log.js +187 -0
  35. package/lib/merchant-service.d.ts +38 -0
  36. package/lib/merchant-service.js +216 -0
  37. package/lib/permissions.d.ts +16 -0
  38. package/lib/permissions.js +66 -0
  39. package/lib/pet-data-service.d.ts +13 -0
  40. package/lib/pet-data-service.js +117 -10
  41. package/lib/pet-size.d.ts +13 -0
  42. package/lib/pet-size.js +54 -0
  43. package/lib/player-service.d.ts +37 -0
  44. package/lib/player-service.js +201 -0
  45. package/lib/ranking-service.d.ts +46 -0
  46. package/lib/ranking-service.js +137 -0
  47. package/lib/render-templates/announcement/detail.html +8 -0
  48. package/lib/render-templates/announcement/list.html +5 -0
  49. package/lib/render-templates/announcement/style.css +1 -0
  50. package/lib/render-templates/home/style.css +15 -14
  51. package/lib/render-templates/home-pets/index.html +80 -0
  52. package/lib/render-templates/home-pets/style.css +338 -0
  53. package/lib/render-templates/pet-detail-classic/index.html +151 -0
  54. package/lib/render-templates/pet-detail-classic/style.css +503 -0
  55. package/lib/render-templates/pet-detail-classic/texture2D/a_title.png +0 -0
  56. package/lib/render-templates/pet-detail-classic/texture2D/bg.jpg +0 -0
  57. package/lib/render-templates/pet-detail-classic/texture2D/footer.png +0 -0
  58. package/lib/render-templates/pet-detail-classic/texture2D/icon.png +0 -0
  59. package/lib/render-templates/pet-detail-classic/texture2D/img_head.png +0 -0
  60. package/lib/render-templates/pet-detail-classic/texture2D/jinhua_bg.png +0 -0
  61. package/lib/render-templates/pet-detail-classic/texture2D/level_icon.png +0 -0
  62. package/lib/render-templates/pet-detail-classic/texture2D/line.png +0 -0
  63. package/lib/render-templates/pet-detail-classic/texture2D/mask_bar.png +0 -0
  64. package/lib/render-templates/pet-detail-classic/texture2D/pet_bg.png +0 -0
  65. package/lib/render-templates/pet-detail-classic/texture2D/pet_rocom.png +0 -0
  66. package/lib/render-templates/pet-detail-classic/texture2D/pet_rocom_bg.png +0 -0
  67. package/lib/render-templates/pet-detail-classic/texture2D/poro_bar.png +0 -0
  68. package/lib/render-templates/pet-detail-classic/texture2D/right_jinhua.png +0 -0
  69. package/lib/render-templates/pet-detail-classic/texture2D/skill_bg.png +0 -0
  70. package/lib/render-templates/pet-detail-classic/texture2D/skill_mask.png +0 -0
  71. package/lib/render-templates/pet-detail-classic/texture2D/star.png +0 -0
  72. package/lib/render-templates/pet-detail-classic/texture2D/star_1.png +0 -0
  73. package/lib/render-templates/pet-detail-classic/texture2D/star_8.png +0 -0
  74. package/lib/render-templates/pet-detail-classic/texture2D/star_9.png +0 -0
  75. package/lib/render-templates/pet-detail-classic/texture2D/table.png +0 -0
  76. package/lib/render-templates/pet-detail-classic/texture2D/tags.png +0 -0
  77. package/lib/render-templates/pet-detail-classic/texture2D/title.png +0 -0
  78. package/lib/render-templates/pet-detail-classic/texture2D/top_bg.png +0 -0
  79. package/lib/render-templates/pet-detail-classic/texture2D/xuancai_overlay.png +0 -0
  80. package/lib/render-templates/pet-detail-classic/texture2D/yise_overlay.png +0 -0
  81. package/lib/render-templates/pet-detail-classic/texture2D/ys_bar.png +0 -0
  82. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/10.png +0 -0
  83. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/11.png +0 -0
  84. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/12.png +0 -0
  85. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/13.png +0 -0
  86. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/14.png +0 -0
  87. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/15.png +0 -0
  88. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/16.png +0 -0
  89. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/17.png +0 -0
  90. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/18.png +0 -0
  91. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/19.png +0 -0
  92. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/2.png +0 -0
  93. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/20.png +0 -0
  94. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/23.png +0 -0
  95. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/3.png +0 -0
  96. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/4.png +0 -0
  97. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/5.png +0 -0
  98. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/6.png +0 -0
  99. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/8.png +0 -0
  100. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/9.png +0 -0
  101. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/205/211.png +0 -0
  102. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/206/260.png +0 -0
  103. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/234/260.png +0 -0
  104. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/271/273.png +0 -0
  105. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/271/275.png +0 -0
  106. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/201/266.png +0 -0
  107. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/227/240.png +0 -0
  108. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/231/256/351/200/232.png +0 -0
  109. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/234/272/346/242/260.png +0 -0
  110. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/255/246.png +0 -0
  111. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/257/222.png +0 -0
  112. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/260/264.png +0 -0
  113. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/201/253.png +0 -0
  114. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/224/265.png +0 -0
  115. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/277/274.png +0 -0
  116. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/215/211.png +0 -0
  117. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/220/214.png +0 -0
  118. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/231/253.png +0 -0
  119. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//351/276/231.png +0 -0
  120. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/1.png +0 -0
  121. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/10.png +0 -0
  122. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/11.png +0 -0
  123. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/12.png +0 -0
  124. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/13.png +0 -0
  125. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/14.png +0 -0
  126. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/15.png +0 -0
  127. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/16.png +0 -0
  128. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/17.png +0 -0
  129. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/18.png +0 -0
  130. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/19.png +0 -0
  131. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/2.png +0 -0
  132. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/21.png +0 -0
  133. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/23.png +0 -0
  134. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/24.png +0 -0
  135. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/3.png +0 -0
  136. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/4.png +0 -0
  137. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/5.png +0 -0
  138. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/6.png +0 -0
  139. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/7.png +0 -0
  140. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/8.png +0 -0
  141. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/9.png +0 -0
  142. package/lib/render-templates/pet-ranking/index.html +99 -0
  143. package/lib/render-templates/pet-ranking/style.css +338 -0
  144. package/lib/render-templates/player-search/index.html +7 -0
  145. package/lib/render-templates/searcheggs/index.html +1 -1
  146. package/lib/render-templates/searcheggs/size.html +70 -68
  147. package/lib/render-templates/share-code-classic/assets/eit-logo.png +0 -0
  148. package/lib/render-templates/share-code-classic/assets/img_xingxingdi.png +0 -0
  149. package/lib/render-templates/share-code-classic/assets/roco_icon.png +0 -0
  150. package/lib/render-templates/share-code-classic/assets//345/255/227/344/275/223/Rocofontblack-Regular .ttf +0 -0
  151. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//345/205/211.png +0 -0
  152. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//345/206/260.png +0 -0
  153. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//345/234/260.png +0 -0
  154. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//345/271/273.png +0 -0
  155. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//345/271/275.png +0 -0
  156. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//346/201/266.png +0 -0
  157. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//346/227/240.png +0 -0
  158. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//346/231/256/351/200/232.png +0 -0
  159. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//346/234/272/346/242/260.png +0 -0
  160. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//346/255/246.png +0 -0
  161. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//346/257/222.png +0 -0
  162. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//346/260/264.png +0 -0
  163. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//347/201/253.png +0 -0
  164. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//347/224/265.png +0 -0
  165. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//347/277/274.png +0 -0
  166. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//350/215/211.png +0 -0
  167. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//350/220/214.png +0 -0
  168. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//350/231/253.png +0 -0
  169. package/lib/render-templates/share-code-classic/assets//345/256/240/347/211/251/345/261/236/346/200/247//351/276/231.png +0 -0
  170. package/lib/render-templates/share-code-classic/assets//347/201/253/346/274/206/345/215/260.png +0 -0
  171. package/lib/render-templates/share-code-classic/assets//347/253/213/347/273/230/350/203/214/346/231/257/346/241/206.png +0 -0
  172. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/205/211.png +0 -0
  173. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/206/260.png +0 -0
  174. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/234/260.png +0 -0
  175. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/245/207/345/274/202.png +0 -0
  176. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/271/273.png +0 -0
  177. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/271/275.png +0 -0
  178. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/201/266.png +0 -0
  179. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/231/256/351/200/232.png +0 -0
  180. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/234/272/346/242/260.png +0 -0
  181. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/255/246.png +0 -0
  182. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/257/222.png +0 -0
  183. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/260/264.png +0 -0
  184. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/261/241/346/237/223.png +0 -0
  185. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/201/253.png +0 -0
  186. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/224/265.png +0 -0
  187. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/277/274.png +0 -0
  188. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/215/211.png +0 -0
  189. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/220/214.png +0 -0
  190. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/231/253.png +0 -0
  191. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//351/246/226/351/242/206.png +0 -0
  192. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//351/276/231.png +0 -0
  193. package/lib/render-templates/share-code-classic/index.html +119 -0
  194. package/lib/render-templates/share-code-classic/style.css +646 -0
  195. package/lib/render-templates/share-code-team/index.html +105 -0
  196. package/lib/render-templates/share-code-team/style.css +345 -0
  197. package/lib/render-templates/yuanxing-shangren/merchant.html +479 -461
  198. package/lib/render-templates/yuanxing-shangren/today.html +407 -461
  199. package/lib/render.d.ts +18 -2
  200. package/lib/render.js +138 -39
  201. package/lib/send-image.d.ts +1 -1
  202. package/lib/send-image.js +22 -1
  203. package/lib/share-code-service.d.ts +68 -0
  204. package/lib/share-code-service.js +257 -0
  205. package/lib/subscription-runner.d.ts +6 -0
  206. package/lib/subscription-runner.js +21 -0
  207. package/lib/subscription-send.d.ts +4 -2
  208. package/lib/subscription-send.js +45 -20
  209. package/lib/ttf/rocom_origin.ttf +0 -0
  210. package/lib/types.d.ts +14 -1
  211. package/lib/user.d.ts +11 -0
  212. package/lib/wiki-catalog.js +1 -1
  213. package/lib/wiki-service.js +1 -1
  214. package/package.json +5 -1
  215. package/readme.md +125 -124
  216. package/lib/ttf/rocom_skill_origin.ttf +0 -0
@@ -3,69 +3,25 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extractHomeEggStatus = extractHomeEggStatus;
6
7
  exports.register = register;
7
8
  const koishi_1 = require("koishi");
8
9
  const account_1 = require("./account");
9
10
  const send_image_1 = require("../send-image");
10
11
  const subscription_send_1 = require("../subscription-send");
11
12
  const pet_data_service_1 = require("../pet-data-service");
13
+ const player_service_1 = require("../player-service");
14
+ const merchant_service_1 = require("../merchant-service");
15
+ const permissions_1 = require("../permissions");
16
+ const render_1 = require("../render");
17
+ const subscription_runner_1 = require("../subscription-runner");
12
18
  const node_fs_1 = __importDefault(require("node:fs"));
13
19
  const node_path_1 = __importDefault(require("node:path"));
14
20
  const logger = new koishi_1.Logger('rocom-query');
15
- async function sendImage(deps, session, templateName, data, fallback) {
16
- const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
21
+ async function sendImage(deps, session, templateName, data, fallback, options) {
22
+ const png = await deps.renderer.renderPages(deps.ctx, templateName, data, options);
17
23
  await (0, send_image_1.sendImageWithFallback)(session, png, fallback, `query:${templateName}`, deps.config);
18
24
  }
19
- function cleanPlayerFieldValue(field, value) {
20
- const text = String(value ?? '').trim().replace(/^'+|'+$/g, '');
21
- if (!text || ['<0B>', '<0b>', '<0B >', '<0b >'].includes(text))
22
- return '未设置';
23
- if (['is_online', 'online', 'chat_top_unlock', 'is_friend', 'is_black', 'is_black_role', 'is_chat_node_unlock'].includes(field)) {
24
- return ['1', 'true', 'True', '是'].includes(text) ? '是' : '否';
25
- }
26
- if (['sex', 'gender'].includes(field)) {
27
- return { '0': '未知', '1': '男', '2': '女' }[text] || text;
28
- }
29
- if (field === 'friend_type') {
30
- return { '0': '默认', '1': '特殊' }[text] || text;
31
- }
32
- if (field === 'battle_state') {
33
- return { '0': '空闲', '1': '对战中' }[text] || text;
34
- }
35
- return text;
36
- }
37
- function parseIngamePlayerPayload(payload, uid) {
38
- const rows = payload?.rows || [];
39
- const rowMap = {};
40
- const labelMap = {};
41
- for (const row of rows) {
42
- const field = String(row.field || '');
43
- if (!field)
44
- continue;
45
- rowMap[field] = String(row.value ?? '');
46
- labelMap[field] = String(row.label || field);
47
- }
48
- const playerUid = cleanPlayerFieldValue('uin', rowMap.uin || uid);
49
- const signature = cleanPlayerFieldValue('signature', rowMap.signature || '');
50
- return {
51
- title: String(payload?.title || '玩家搜索'),
52
- nickname: cleanPlayerFieldValue('name', rowMap.name || '-'),
53
- uid: playerUid,
54
- level: cleanPlayerFieldValue('level', rowMap.level || '-'),
55
- signature: signature === '未设置' ? '' : signature,
56
- rowMap,
57
- labelMap,
58
- };
59
- }
60
- function playerField(parsed, field, defaultValue = '未设置') {
61
- if (!parsed)
62
- return defaultValue;
63
- const raw = parsed.rowMap[field];
64
- if (raw == null || raw === '')
65
- return defaultValue;
66
- const value = cleanPlayerFieldValue(field, raw);
67
- return value && value !== '-' && value !== '未设置' ? value : defaultValue;
68
- }
69
25
  function tryParseJson(text) {
70
26
  try {
71
27
  return JSON.parse(text);
@@ -876,7 +832,7 @@ function extractHomePet(deps, raw, index, guard = false) {
876
832
  const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
877
833
  const gender = Number(display.gender || raw.gender || 0) || 0;
878
834
  let mutationType = Number(display.mutation_type || raw.mutation_type || homePet.mutation_type || 0) || 0;
879
- // 上游 v3.4.0:mutation_type 缺失时按 real_speciality_ids 识别变体(103=异色、502=炫彩、两者兼有=异色炫彩)。
835
+ // mutation_type 缺失时按 real_speciality_ids 识别变体(103=异色、502=炫彩、两者兼有=异色炫彩)。
880
836
  if (!mutationType) {
881
837
  const specialityValues = [];
882
838
  for (const value of [homePet.real_speciality_ids, raw.real_speciality_ids, homePet.speciality_id, raw.speciality_id]) {
@@ -1112,8 +1068,8 @@ function buildHomeRenderData(deps, res, uid) {
1112
1068
  updatedAt: new Date(createdAt ? createdAt * 1000 : Date.now()).toLocaleString('zh-CN'),
1113
1069
  };
1114
1070
  }
1115
- function buildPlayerSearchRenderData(payload, uid) {
1116
- const parsed = parseIngamePlayerPayload(payload, uid);
1071
+ function buildPlayerSearchRenderData(payload, uid, cardImageUrl = '') {
1072
+ const parsed = (0, player_service_1.parseIngamePlayerPayload)(payload, uid);
1117
1073
  const pack = (title, pairs) => {
1118
1074
  const items = pairs
1119
1075
  .filter(([, value]) => value && value !== '-' && value !== '未设置')
@@ -1123,27 +1079,27 @@ function buildPlayerSearchRenderData(payload, uid) {
1123
1079
  const sections = [
1124
1080
  pack('核心档案', [
1125
1081
  ['等级', parsed.level],
1126
- ['在线状态', playerField(parsed, 'online', playerField(parsed, 'is_online'))],
1127
- ['性别', playerField(parsed, 'gender', playerField(parsed, 'sex'))],
1128
- ['世界等级', playerField(parsed, 'world_level')],
1129
- ['图鉴收集', playerField(parsed, 'card_handbook_collect_num')],
1130
- ['最后离线', playerField(parsed, 'last_logout_time')],
1082
+ ['在线状态', (0, player_service_1.playerField)(parsed, 'online', (0, player_service_1.playerField)(parsed, 'is_online'))],
1083
+ ['性别', (0, player_service_1.playerField)(parsed, 'gender', (0, player_service_1.playerField)(parsed, 'sex'))],
1084
+ ['世界等级', (0, player_service_1.playerField)(parsed, 'world_level')],
1085
+ ['图鉴收集', (0, player_service_1.playerField)(parsed, 'card_handbook_collect_num')],
1086
+ ['最后离线', (0, player_service_1.playerField)(parsed, 'last_logout_time')],
1131
1087
  ]),
1132
1088
  pack('家园信息', [
1133
- ['家园名称', playerField(parsed, 'home_name')],
1134
- ['家园等级', playerField(parsed, 'home_level')],
1135
- ['家园经验', playerField(parsed, 'home_experience')],
1136
- ['舒适度', playerField(parsed, 'home_comfort_level')],
1137
- ['访客数量', playerField(parsed, 'visitor_num')],
1089
+ ['家园名称', (0, player_service_1.playerField)(parsed, 'home_name')],
1090
+ ['家园等级', (0, player_service_1.playerField)(parsed, 'home_level')],
1091
+ ['家园经验', (0, player_service_1.playerField)(parsed, 'home_experience')],
1092
+ ['舒适度', (0, player_service_1.playerField)(parsed, 'home_comfort_level')],
1093
+ ['访客数量', (0, player_service_1.playerField)(parsed, 'visitor_num')],
1138
1094
  ]),
1139
1095
  ].filter(Boolean);
1140
1096
  const summaryCards = [
1141
1097
  { label: '等级', value: parsed.level },
1142
- { label: '在线状态', value: playerField(parsed, 'online', playerField(parsed, 'is_online')) },
1143
- { label: '世界等级', value: playerField(parsed, 'world_level') },
1144
- { label: '图鉴收集', value: playerField(parsed, 'card_handbook_collect_num') },
1145
- { label: '家园等级', value: playerField(parsed, 'home_level') },
1146
- { label: '舒适度', value: playerField(parsed, 'home_comfort_level') },
1098
+ { label: '在线状态', value: (0, player_service_1.playerField)(parsed, 'online', (0, player_service_1.playerField)(parsed, 'is_online')) },
1099
+ { label: '世界等级', value: (0, player_service_1.playerField)(parsed, 'world_level') },
1100
+ { label: '图鉴收集', value: (0, player_service_1.playerField)(parsed, 'card_handbook_collect_num') },
1101
+ { label: '家园等级', value: (0, player_service_1.playerField)(parsed, 'home_level') },
1102
+ { label: '舒适度', value: (0, player_service_1.playerField)(parsed, 'home_comfort_level') },
1147
1103
  ].filter(item => item.value && item.value !== '-');
1148
1104
  const signature = parsed.signature && parsed.signature !== '未设置' ? parsed.signature : '';
1149
1105
  return {
@@ -1155,15 +1111,38 @@ function buildPlayerSearchRenderData(payload, uid) {
1155
1111
  signature,
1156
1112
  showSignature: Boolean(signature),
1157
1113
  sections,
1158
- commandHint: '洛克.玩家 <UID>',
1114
+ cardImageUrl: cardImageUrl || '',
1115
+ collectedShining: (0, player_service_1.playerField)(parsed, 'collected_shining_pet_count', '未知'),
1116
+ collectedGlass: (0, player_service_1.playerField)(parsed, 'collected_glass_pet_count', '未知'),
1117
+ commandHint: '洛克.玩家 [UID]',
1159
1118
  copyright: 'Koishi & WeGame Locke Kingdom Plugin',
1160
1119
  };
1161
1120
  }
1162
- function buildShopRenderData(payload, shopId) {
1121
+ function buildShopRenderData(payload, shopId, options = {}) {
1163
1122
  const sections = [];
1164
1123
  const detailItems = [];
1165
1124
  const summaryCards = [{ label: '商店 ID', value: shopId }];
1166
- if (Array.isArray(payload?.rows)) {
1125
+ const shop = payload?.shop || {};
1126
+ if (Array.isArray(payload?.goods)) {
1127
+ const items = (0, merchant_service_1.normalizeLiveMerchant)(payload, Date.now(), { assetBaseUrl: options.assetBaseUrl });
1128
+ summaryCards.push({ label: '商品数量', value: String(items.length) });
1129
+ const shopName = String(shop.name || shop.title || '').trim();
1130
+ if (shopName)
1131
+ summaryCards.push({ label: '商店名称', value: shopName });
1132
+ sections.push({
1133
+ title: '商品列表',
1134
+ cards: items.map((item) => ({
1135
+ title: item.name,
1136
+ image: item.icon,
1137
+ meta: [
1138
+ item.price !== null ? { label: '价格', value: String(item.price) } : null,
1139
+ item.limit !== null ? { label: '限购', value: String(item.limit) } : null,
1140
+ item.active ? null : { label: '状态', value: '非在售' },
1141
+ ].filter(Boolean),
1142
+ })),
1143
+ });
1144
+ }
1145
+ else if (Array.isArray(payload?.rows)) {
1167
1146
  detailItems.push(...payload.rows.filter((row) => Number(row.level || 0) === 0).map((row) => ({
1168
1147
  label: row.label || row.field || '-',
1169
1148
  value: stringifyInspectValue(row.value),
@@ -1171,6 +1150,8 @@ function buildShopRenderData(payload, shopId) {
1171
1150
  }
1172
1151
  else {
1173
1152
  for (const [key, value] of Object.entries(payload || {})) {
1153
+ if (['_source', 'goods', 'shop', 'meta', '_goods_mapping', 'goods_mapping'].includes(key))
1154
+ continue;
1174
1155
  if (Array.isArray(value)) {
1175
1156
  summaryCards.push({ label: key, value: String(value.length) });
1176
1157
  sections.push({
@@ -1194,12 +1175,12 @@ function buildShopRenderData(payload, shopId) {
1194
1175
  title: '洛克商店',
1195
1176
  subtitle: `shop_id = ${shopId}`,
1196
1177
  heroTitle: '商店查询',
1197
- heroValue: detailItems.find(item => ['name', 'title', '名称', '标题'].includes(item.label))?.value || shopId,
1178
+ heroValue: String(shop.name || shop.title || '') || detailItems.find(item => ['name', 'title', '名称', '标题'].includes(item.label))?.value || shopId,
1198
1179
  heroSubvalue: `shop_id = ${shopId}`,
1199
1180
  summaryCards: summaryCards.slice(0, 3),
1200
1181
  sections,
1201
1182
  detailItems: detailItems.slice(0, 18),
1202
- commandHint: '洛克.商店 <shop_id>',
1183
+ commandHint: '洛克.商店 [shop_id]',
1203
1184
  copyright: 'Koishi & WeGame Locke Kingdom Plugin',
1204
1185
  };
1205
1186
  }
@@ -1274,7 +1255,7 @@ function sessionTarget(session) {
1274
1255
  return {
1275
1256
  platform: session?.platform || session?.bot?.platform || '',
1276
1257
  channelId: session?.channelId || session?.guildId || '',
1277
- userId: session?.guildId ? '' : (session?.userId || ''),
1258
+ userId: (0, permissions_1.isDirectSession)(session) ? (session?.userId || '') : '',
1278
1259
  };
1279
1260
  }
1280
1261
  function homeSubscriptionKey(session, uid, kind) {
@@ -1284,6 +1265,9 @@ function homeSubscriptionKey(session, uid, kind) {
1284
1265
  function isBotAdmin(session, adminUserIds) {
1285
1266
  return adminUserIds.includes(session?.userId || '');
1286
1267
  }
1268
+ function canManageGroupSubscription(deps, session) {
1269
+ return (0, permissions_1.canManageSubscription)(deps.config, session);
1270
+ }
1287
1271
  async function resolveHomeUid(deps, session, uid = '') {
1288
1272
  const targetUid = String(uid || '').trim();
1289
1273
  if (targetUid)
@@ -1310,27 +1294,297 @@ const HOME_SUB_TEXT = {
1310
1294
  needUidHint: '请提供玩家 UID,或先完成绑定后再订阅家园生蛋。',
1311
1295
  },
1312
1296
  };
1313
- async function subscribeHome(deps, session, uid, kind) {
1297
+ async function subscribeHome(deps, session, uid, kind, mentionAll = null) {
1314
1298
  const target = sessionTarget(session);
1315
- if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds))
1316
- return '此指令仅限管理员使用。';
1299
+ if (!target.userId && !canManageGroupSubscription(deps, session))
1300
+ return '此指令仅限群管理员或机器人管理员使用。';
1317
1301
  const targetUid = await resolveHomeUid(deps, session, uid);
1318
1302
  if (!targetUid)
1319
1303
  return HOME_SUB_TEXT[kind].needUidHint;
1320
1304
  const key = homeSubscriptionKey(session, targetUid, kind);
1305
+ const own = deps.userMgr.getUserBindings(session?.userId || '').find(binding => binding.role_id === targetUid);
1306
+ const existing = deps.homeSubMgr.getAll()[key];
1321
1307
  deps.homeSubMgr.upsert(key, {
1322
1308
  key,
1323
1309
  kind,
1324
1310
  uid: targetUid,
1311
+ notify_user_id: own ? session.userId : '',
1312
+ nickname: own?.nickname || '',
1325
1313
  platform: target.platform,
1314
+ self_id: session?.selfId || session?.bot?.selfId || '',
1326
1315
  channel_id: target.channelId,
1327
1316
  guild_id: session?.guildId || '',
1328
1317
  user_id: target.userId,
1329
1318
  updated_by: session?.userId || '',
1330
- notify_state: {},
1319
+ // 群订阅支持命中后 @全体;未显式指定时沿用上次设置。
1320
+ mention_all: target.userId ? false : (mentionAll ?? existing?.mention_all ?? false),
1321
+ notify_state: existing?.notify_state || {},
1331
1322
  updated_at: Math.floor(Date.now() / 1000),
1332
1323
  });
1333
- return `已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`;
1324
+ const finalMentionAll = target.userId ? false : (mentionAll ?? existing?.mention_all ?? false);
1325
+ const mentionHint = target.userId ? '' : (finalMentionAll ? '命中后会尝试 @全体。' : '命中后不会 @全体。');
1326
+ return [`已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`, mentionHint].filter(Boolean).join('');
1327
+ }
1328
+ // 订阅家园 的 @全体 写法与 UID 解析。
1329
+ const HOME_MENTION_ALL_ALIASES = new Set(['@全体', '全体', '@所有人', 'at全体', 'at所有人']);
1330
+ function parseHomeSubscribeArgs(args) {
1331
+ const tokens = String(args || '').split(/\s+/).filter(Boolean);
1332
+ let mentionAll = null;
1333
+ const uids = [];
1334
+ for (const token of tokens) {
1335
+ if (HOME_MENTION_ALL_ALIASES.has(token.toLowerCase())) {
1336
+ mentionAll = true;
1337
+ continue;
1338
+ }
1339
+ uids.push(token);
1340
+ }
1341
+ return { mentionAll, uid: uids[0] || '' };
1342
+ }
1343
+ function pickEggStatusPayload(payload) {
1344
+ const candidates = [payload?.data, payload?.result, payload?.data?.data, payload?.result?.data, payload];
1345
+ for (const candidate of candidates) {
1346
+ if (candidate && typeof candidate === 'object' && !Array.isArray(candidate))
1347
+ return candidate;
1348
+ }
1349
+ return {};
1350
+ }
1351
+ function extractHomeEggPet(item, nowTs) {
1352
+ const petInfo = item?.home_pet_info && typeof item.home_pet_info === 'object' ? item.home_pet_info : item;
1353
+ const petId = petInfo?.pet_cfg_id || petInfo?.pet_id || item?.pet_cfg_id || item?.pet_id;
1354
+ const name = String(petInfo?.name || petInfo?.pet_name || item?.name || item?.pet_name || '').trim();
1355
+ if (!Number(petId) && !name)
1356
+ return null;
1357
+ const hasEgg = Boolean(item?.have_egg ?? petInfo?.have_egg);
1358
+ const predictedEggTime = normalizeEpochSeconds(item?.predicted_egg_time ?? petInfo?.predicted_egg_time ?? item?.egg_ready_time);
1359
+ const ready = hasEgg || (predictedEggTime > 0 && nowTs >= predictedEggTime);
1360
+ const remaining = ready ? '' : (predictedEggTime > 0 ? formatEggRemaining(predictedEggTime, nowTs) : '未知');
1361
+ return {
1362
+ petId: String(petId || ''),
1363
+ name: name || `精灵 ${petId || ''}`,
1364
+ ready,
1365
+ remaining,
1366
+ predictedEggTime,
1367
+ hasEgg,
1368
+ };
1369
+ }
1370
+ /**
1371
+ * 从 home/eggs 轻量接口响应中提取蛋状态。
1372
+ * 接口仍在开发中,这里对多种可能的字段名做兼容。
1373
+ */
1374
+ function extractHomeEggStatus(payload) {
1375
+ const data = pickEggStatusPayload(payload);
1376
+ const nowTs = Math.floor(Date.now() / 1000);
1377
+ const eggLists = [
1378
+ data?.eggs,
1379
+ data?.egg_list,
1380
+ data?.egg_info,
1381
+ data?.home_eggs,
1382
+ data?.home_egg_list,
1383
+ data?.home_pets,
1384
+ data?.pet_list,
1385
+ ].filter(Array.isArray);
1386
+ const pets = [];
1387
+ for (const list of eggLists) {
1388
+ for (const item of list) {
1389
+ const pet = extractHomeEggPet(item, nowTs);
1390
+ if (pet)
1391
+ pets.push(pet);
1392
+ }
1393
+ }
1394
+ const readyCount = pets.length > 0
1395
+ ? pets.filter(pet => pet.ready).length
1396
+ : Number(data?.ready_egg_count ?? data?.readyEggCount ?? data?.egg_ready_count ?? 0) || 0;
1397
+ const totalEggs = pets.length > 0
1398
+ ? pets.length
1399
+ : Number(data?.total_egg_count ?? data?.totalEggCount ?? data?.egg_count ?? 0) || 0;
1400
+ return { readyCount, totalEggs, pets };
1401
+ }
1402
+ // 与全量家园信息的蛋状态口径一致:只统计参与生蛋的精灵(有蛋或已排期)。
1403
+ function homeEggSubscriptionState(payload) {
1404
+ const status = extractHomeEggStatus(payload);
1405
+ const items = status.pets.filter(pet => pet.hasEgg || pet.predictedEggTime > 0);
1406
+ const readyItems = items.filter(pet => pet.ready);
1407
+ return { items, readyItems, names: readyItems.map(pet => pet.name) };
1408
+ }
1409
+ function buildHomeEggStatusText(uid, status) {
1410
+ const lines = [
1411
+ `家园蛋状态 - UID ${uid}`,
1412
+ `可收蛋精灵:${status.readyCount} 只 / 共检测 ${status.totalEggs} 只`,
1413
+ ];
1414
+ const tracked = status.pets.filter(pet => pet.hasEgg || pet.predictedEggTime > 0);
1415
+ if (tracked.length > 0) {
1416
+ lines.push('');
1417
+ for (const pet of tracked.slice(0, 30)) {
1418
+ lines.push(`${pet.name} ${pet.ready ? '✅ 可收蛋' : `⏳ ${pet.remaining}`}`);
1419
+ }
1420
+ if (tracked.length > 30)
1421
+ lines.push(`…等共 ${tracked.length} 只`);
1422
+ }
1423
+ lines.push('');
1424
+ lines.push(`更新时间:${new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' })}`);
1425
+ return lines.join('\n');
1426
+ }
1427
+ // 家园精灵一览(home/pet-data),汇总体重、声音、变异与生蛋状态。
1428
+ function collectHomePetsViewSources(homeInfo, payload) {
1429
+ const containers = [homeInfo?.friend_cell_home_brief_info, homeCellInfo(homeInfo), homeInfo?.cell_info, homeInfo, payload?.friend_cell_home_brief_info];
1430
+ const result = [];
1431
+ const seen = new Set();
1432
+ for (const container of containers) {
1433
+ if (!container || typeof container !== 'object' || !Array.isArray(container.home_pets))
1434
+ continue;
1435
+ for (const item of container.home_pets) {
1436
+ const info = item?.home_pet_info && typeof item.home_pet_info === 'object' ? item.home_pet_info : item;
1437
+ const key = String(info?.pet_gid || info?.furniture_guid || `${info?.pet_cfg_id || ''}:${info?.name || ''}`);
1438
+ if (key && seen.has(key))
1439
+ continue;
1440
+ if (key)
1441
+ seen.add(key);
1442
+ result.push(item);
1443
+ }
1444
+ }
1445
+ return result;
1446
+ }
1447
+ function homePetsViewPetFromNpc(raw) {
1448
+ const npcPet = raw?.npc_pet && typeof raw.npc_pet === 'object' ? raw.npc_pet : {};
1449
+ const pet = npcPet?.pet && typeof npcPet.pet === 'object' ? npcPet.pet : (raw?.pet && typeof raw.pet === 'object' ? raw.pet : raw?.display_info);
1450
+ const petId = raw?.pet_cfg_id || raw?.pet_id || pet?.base_conf_id || pet?.pet_id || pet?.id;
1451
+ return {
1452
+ home_pet_info: {
1453
+ pet_gid: raw?.pet_gid || pet?.pet_gid,
1454
+ npc_id: raw?.npc_id || raw?.npcId,
1455
+ pet_cfg_id: petId,
1456
+ furniture_guid: raw?.furniture_guid,
1457
+ name: raw?.name || pet?.name,
1458
+ speciality_id: raw?.speciality_id || pet?.speciality_id,
1459
+ real_speciality_ids: raw?.real_speciality_ids || pet?.real_speciality_ids || [],
1460
+ status: raw?.status,
1461
+ },
1462
+ display_info: pet,
1463
+ };
1464
+ }
1465
+ function isHomePetsViewGuard(homePetInfo, displayInfo, pet) {
1466
+ const status = String(homePetInfo?.status ?? pet?.status ?? displayInfo?.status ?? '').toLowerCase();
1467
+ const zeroCfgWithName = String(homePetInfo?.pet_cfg_id || '0') === '0'
1468
+ && Boolean(String(homePetInfo?.name || homePetInfo?.pet_name || '').trim());
1469
+ return zeroCfgWithName
1470
+ || Boolean(homePetInfo?.is_guard || homePetInfo?.guard || pet?.is_guard || pet?.guard || displayInfo?.is_guard || displayInfo?.guard)
1471
+ || status === '2' || status === 'guard';
1472
+ }
1473
+ // ingame 数据体重可能以克或千克记录:大数值按克换算成千克展示。
1474
+ function homePetsWeightText(value) {
1475
+ const num = Number(value);
1476
+ if (!Number.isFinite(num) || num <= 0)
1477
+ return '--';
1478
+ const kg = num >= 1000 ? num / 1000 : num;
1479
+ const text = kg.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
1480
+ return `${text}kg`;
1481
+ }
1482
+ function buildHomePetsViewData(deps, res, uid) {
1483
+ const raw = res || {};
1484
+ const payload = raw.result && typeof raw.result === 'object' ? raw.result : raw;
1485
+ const homeInfo = homeInfoPayload(raw);
1486
+ const homePets = collectHomePetsViewSources(homeInfo, payload);
1487
+ const npcPets = Array.isArray(payload.npc_pets) ? payload.npc_pets : [];
1488
+ const npcMap = new Map();
1489
+ for (const entry of npcPets) {
1490
+ const npcPet = entry?.npc_pet && typeof entry.npc_pet === 'object' ? entry.npc_pet : {};
1491
+ const pet = npcPet?.pet && typeof npcPet.pet === 'object' ? npcPet.pet : entry?.pet;
1492
+ const gid = String(entry?.pet_gid || entry?.petGid || npcPet?.pet_gid || pet?.pet_gid || '').trim();
1493
+ if (gid)
1494
+ npcMap.set(gid, entry);
1495
+ }
1496
+ const nowTs = Math.floor(Date.now() / 1000);
1497
+ const petSources = homePets.length > 0 ? homePets : npcPets.map(entry => homePetsViewPetFromNpc(entry));
1498
+ const pets = [];
1499
+ for (const hp of petSources) {
1500
+ const homePetInfo = hp?.home_pet_info && typeof hp.home_pet_info === 'object' ? hp.home_pet_info : hp;
1501
+ const displayInfo = hp?.display_info && typeof hp.display_info === 'object' ? hp.display_info : {};
1502
+ const petGid = String(homePetInfo?.pet_gid || displayInfo?.pet_gid || '').trim();
1503
+ const npcRaw = npcMap.get(petGid) || {};
1504
+ const npcPetObj = npcRaw?.npc_pet && typeof npcRaw.npc_pet === 'object' ? npcRaw.npc_pet : {};
1505
+ const npcFullPet = npcPetObj?.pet && typeof npcPetObj.pet === 'object' ? npcPetObj.pet
1506
+ : (npcRaw?.pet && typeof npcRaw.pet === 'object' ? npcRaw.pet : npcRaw?.display_info);
1507
+ const npcStatus = String(npcRaw?.status || '');
1508
+ const hasFull = npcFullPet && typeof npcFullPet === 'object' && Object.keys(npcFullPet).length > 0 && npcStatus !== 'error';
1509
+ const pet = hasFull ? npcFullPet : displayInfo;
1510
+ const petId = pet?.base_conf_id || pet?.pet_id || pet?.id || displayInfo?.base_conf_id || homePetInfo?.pet_cfg_id;
1511
+ if (isHomePetsViewGuard(homePetInfo, displayInfo, pet) || !Number(petId))
1512
+ continue;
1513
+ const name = String(pet?.name || homePetInfo?.name || homePetInfo?.pet_name || `精灵 ${petId}`).trim();
1514
+ const gender = Number(pet?.gender ?? displayInfo?.gender ?? 0) || 0;
1515
+ const genderText = gender === 1 ? '♂ 雄性' : gender === 2 ? '♀ 雌性' : '未知';
1516
+ let mutationType = Number(pet?.mutation_type ?? displayInfo?.mutation_type ?? 0) || 0;
1517
+ if (!mutationType) {
1518
+ // 与 extractHomePet 相同的变体兜底(real_speciality_ids 103=异色、502=炫彩)。
1519
+ const specialityValues = [];
1520
+ for (const value of [homePetInfo.real_speciality_ids, npcRaw.real_speciality_ids, homePetInfo.speciality_id, npcRaw.speciality_id]) {
1521
+ if (Array.isArray(value))
1522
+ specialityValues.push(...value);
1523
+ else if (value !== undefined && value !== null && value !== '')
1524
+ specialityValues.push(value);
1525
+ }
1526
+ const specialityIds = new Set(specialityValues.map(value => String(value).trim()).filter(Boolean));
1527
+ if (specialityIds.has('103') && specialityIds.has('502'))
1528
+ mutationType = 9;
1529
+ else if (specialityIds.has('103'))
1530
+ mutationType = 1;
1531
+ else if (specialityIds.has('502'))
1532
+ mutationType = 8;
1533
+ }
1534
+ const variantText = String(pet?.mutation_name || displayInfo?.mutation_name || '')
1535
+ || (mutationType === 9 ? '异色炫彩' : mutationType === 1 ? '异色' : mutationType === 8 ? '炫彩' : '');
1536
+ const hasEgg = Boolean(hp?.have_egg ?? pet?.have_egg);
1537
+ const predictedEggTime = normalizeEpochSeconds(hp?.predicted_egg_time ?? pet?.predicted_egg_time);
1538
+ const eggReady = hasEgg || (predictedEggTime > 0 && nowTs >= predictedEggTime);
1539
+ const eggText = eggReady ? '可收蛋' : (predictedEggTime > 0 ? `${formatEggRemaining(predictedEggTime, nowTs)}后生蛋` : '');
1540
+ pets.push({
1541
+ id: String(petId || ''),
1542
+ name,
1543
+ level: pet?.level ?? displayInfo?.level ?? homePetInfo?.level ?? '--',
1544
+ genderText,
1545
+ weightText: homePetsWeightText(pet?.weight ?? displayInfo?.weight ?? homePetInfo?.weight ?? pet?.pet_weight),
1546
+ voiceText: (0, pet_data_service_1.petDataVoiceInfo)(pet?.voice ?? displayInfo?.voice).value,
1547
+ variantText,
1548
+ eggReady,
1549
+ eggText,
1550
+ iconUrl: homePetIcon(petId, pet?.pet_icon_url || displayInfo?.pet_icon_url || npcRaw?.icon_url || ''),
1551
+ starIconUrl: [1, 8, 9].includes(mutationType)
1552
+ ? deps.renderer.resourceUrl(`render-templates/home/img/rocomuid/star_${mutationType}.png`)
1553
+ : '',
1554
+ });
1555
+ }
1556
+ pets.sort((a, b) => (a.eggReady === b.eggReady ? 0 : (a.eggReady ? -1 : 1)));
1557
+ const brief = homeBriefInfo(homeInfo);
1558
+ const homeName = String(brief?.home_name || brief?.name || payload?.player_info?.name || '').trim() || '洛克玩家';
1559
+ return {
1560
+ title: '家园精灵',
1561
+ homeName,
1562
+ uid,
1563
+ pets,
1564
+ total: pets.length,
1565
+ eggCount: pets.filter(pet => pet.eggReady).length,
1566
+ emptyText: '未获取到家园精灵信息。请确认目标玩家在线、家园可访问,或稍后重试。',
1567
+ updatedAt: new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }),
1568
+ };
1569
+ }
1570
+ function buildHomePetsViewText(data) {
1571
+ const lines = [
1572
+ `【家园精灵】UID ${data.uid}(${data.homeName})`,
1573
+ `共 ${data.total} 只精灵,可收蛋 ${data.eggCount} 只`,
1574
+ ];
1575
+ for (const pet of (data.pets || []).slice(0, 12)) {
1576
+ const bits = [pet.name, `Lv.${pet.level}`, pet.genderText, pet.weightText, pet.voiceText];
1577
+ if (pet.variantText)
1578
+ bits.push(pet.variantText);
1579
+ if (pet.eggText)
1580
+ bits.push(pet.eggText);
1581
+ lines.push(`- ${bits.join(' ')}`);
1582
+ }
1583
+ if ((data.pets || []).length > 12)
1584
+ lines.push(`…等共 ${data.pets.length} 只`);
1585
+ if (!data.pets?.length)
1586
+ lines.push(data.emptyText);
1587
+ return lines.join('\n');
1334
1588
  }
1335
1589
  function homeSubscriptionState(data, kind) {
1336
1590
  if (kind === 'garden') {
@@ -1359,27 +1613,55 @@ function homeSubscriptionMessage(uid, kind, level, totalCount, readyItems, names
1359
1613
  names.length ? `已完成:${names.slice(0, 8).join('、')}` : '',
1360
1614
  ].filter(Boolean).join('\n');
1361
1615
  }
1362
- async function checkHomeSubscriptions(deps) {
1616
+ async function checkHomeSubscriptions(deps, signal) {
1363
1617
  const subs = deps.homeSubMgr.getAll();
1364
- const cache = new Map();
1618
+ const fullCache = new Map();
1619
+ const eggsCache = new Map();
1620
+ const nowSec = Math.floor(Date.now() / 1000);
1365
1621
  let checkedCount = 0;
1366
1622
  let pushedCount = 0;
1623
+ const fetchFull = async (uid) => {
1624
+ if (!fullCache.has(uid)) {
1625
+ fullCache.set(uid, await deps.client.ingameHomeInfo(deps.ctx, uid, { timeoutMs: 30000, signal }).catch(() => null));
1626
+ }
1627
+ return fullCache.get(uid);
1628
+ };
1367
1629
  for (const [key, sub] of Object.entries(subs)) {
1630
+ if (signal?.aborted)
1631
+ break;
1368
1632
  if (!sub.uid || !['garden', 'inspiration', 'egg'].includes(sub.kind))
1369
1633
  continue;
1370
1634
  checkedCount++;
1371
- if (!cache.has(sub.uid)) {
1372
- cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 30000 }));
1635
+ const modified = () => JSON.stringify(deps.homeSubMgr.getAll()[key]) !== JSON.stringify(sub);
1636
+ let state = null;
1637
+ if (sub.kind === 'egg') {
1638
+ // 蛋状态订阅走轻量 home/eggs 接口,降低轮询开销。
1639
+ if (!eggsCache.has(sub.uid)) {
1640
+ eggsCache.set(sub.uid, await deps.client.ingameHomeEggs(deps.ctx, sub.uid, { timeoutMs: 30000, signal }).catch(() => null));
1641
+ }
1642
+ if (signal?.aborted)
1643
+ break;
1644
+ if (modified())
1645
+ continue;
1646
+ const payload = eggsCache.get(sub.uid);
1647
+ if (payload)
1648
+ state = homeEggSubscriptionState(payload);
1373
1649
  }
1374
- const res = cache.get(sub.uid);
1375
- if (!res)
1376
- continue;
1377
- const data = buildHomeRenderData(deps, res, sub.uid);
1378
- const { items, readyItems, names } = homeSubscriptionState(data, sub.kind);
1650
+ if (!state) {
1651
+ const res = await fetchFull(sub.uid);
1652
+ if (signal?.aborted)
1653
+ break;
1654
+ if (modified())
1655
+ continue;
1656
+ if (!res)
1657
+ continue;
1658
+ state = homeSubscriptionState(buildHomeRenderData(deps, res, sub.uid), sub.kind);
1659
+ }
1660
+ const { items, readyItems, names } = state;
1379
1661
  const totalCount = items.length;
1380
1662
  if (totalCount <= 0)
1381
1663
  continue;
1382
- const notifyState = sub.notify_state || {};
1664
+ const notifyState = { ...sub.notify_state };
1383
1665
  const pushLevels = [];
1384
1666
  if (!readyItems.length) {
1385
1667
  notifyState.first = false;
@@ -1394,18 +1676,27 @@ async function checkHomeSubscriptions(deps) {
1394
1676
  notifyState.all = false;
1395
1677
  }
1396
1678
  if (!pushLevels.length) {
1397
- deps.homeSubMgr.upsert(key, { ...sub, notify_state: notifyState });
1679
+ deps.homeSubMgr.upsert(key, { ...sub, notify_state: notifyState, last_check_time: nowSec });
1398
1680
  continue;
1399
1681
  }
1400
- const messages = pushLevels.map(level => homeSubscriptionMessage(sub.uid, sub.kind, level, totalCount, readyItems, names));
1682
+ const owned = deps.userMgr.getUserBindings(sub.updated_by).find(binding => binding.role_id === sub.uid);
1683
+ const label = owned?.nickname || sub.uid;
1684
+ const notifyUser = owned && sub.notify_user_id === sub.updated_by ? sub.notify_user_id : '';
1685
+ const messages = pushLevels.map(level => homeSubscriptionMessage(label, sub.kind, level, totalCount, readyItems, names));
1401
1686
  try {
1687
+ const body = messages.join('\n\n');
1688
+ // 群订阅 mention_all 开启时命中后 @全体。
1689
+ const content = !sub.user_id && sub.mention_all
1690
+ ? [(0, koishi_1.h)('at', { type: 'all' }), koishi_1.h.text('\n' + body)]
1691
+ : (!sub.user_id && notifyUser ? [koishi_1.h.at(notifyUser), koishi_1.h.text('\n' + body)] : body);
1402
1692
  const sent = await (0, subscription_send_1.sendScheduledMessage)(deps.ctx, {
1403
1693
  platform: sub.platform,
1404
1694
  channelId: sub.channel_id || sub.guild_id || sub.user_id || '',
1405
1695
  guildId: sub.guild_id || '',
1406
1696
  userId: sub.user_id || '',
1407
- }, messages.join('\n\n'));
1408
- if (!sent)
1697
+ selfId: sub.self_id || '',
1698
+ }, content, signal);
1699
+ if (!sent || signal?.aborted || JSON.stringify(deps.homeSubMgr.getAll()[key]) !== JSON.stringify(sub))
1409
1700
  continue;
1410
1701
  }
1411
1702
  catch (e) {
@@ -1415,12 +1706,13 @@ async function checkHomeSubscriptions(deps) {
1415
1706
  for (const level of pushLevels)
1416
1707
  notifyState[level] = true;
1417
1708
  pushedCount += pushLevels.length;
1418
- deps.homeSubMgr.upsert(key, { ...sub, notify_state: notifyState, last_push_time: Math.floor(Date.now() / 1000) });
1709
+ deps.homeSubMgr.upsert(key, { ...sub, notify_state: notifyState, last_push_time: Math.floor(Date.now() / 1000), last_check_time: nowSec });
1419
1710
  }
1420
1711
  return { subscriptions: Object.keys(subs).length, checked: checkedCount, pushed: pushedCount };
1421
1712
  }
1422
1713
  function register(deps) {
1423
1714
  const { ctx, client } = deps;
1715
+ const homeRunner = (0, subscription_runner_1.createRunner)(ctx);
1424
1716
  ctx.command('洛克').subcommand('.档案', '查看个人档案')
1425
1717
  .alias('洛克档案')
1426
1718
  .action(async ({ session }) => {
@@ -1444,28 +1736,30 @@ function register(deps) {
1444
1736
  const cl = collRes || {};
1445
1737
  const bo = boRes || {};
1446
1738
  const recentBattle = blRes?.battles?.[0];
1447
- const playerSearchRes = role?.id ? await client.ingamePlayerSearch(ctx, String(role.id)) : null;
1448
- const playerSearchData = parseIngamePlayerPayload(playerSearchRes, String(role.id || ''));
1739
+ const auth = { fwToken, userIdentifier };
1740
+ const playerSearchRes = role?.id ? await client.ingamePlayerSearch(ctx, String(role.id), { auth }) : null;
1741
+ const playerCardRes = role?.id ? await client.ingamePlayerCard(ctx, String(role.id), { auth }).catch(() => null) : null;
1742
+ const playerSearchData = (0, player_service_1.parseIngamePlayerPayload)((0, player_service_1.mergePlayerPayloads)(playerSearchRes, playerCardRes), String(role.id || ''));
1449
1743
  const profileSignature = playerSearchData?.signature || '';
1450
1744
  const profileHeadTags = playerSearchData ? [
1451
- { label: '在线', value: playerField(playerSearchData, 'online', '未知') },
1452
- { label: '性别', value: playerField(playerSearchData, 'gender', playerField(playerSearchData, 'sex', '未知')) },
1453
- { label: '世界等级', value: playerField(playerSearchData, 'world_level') },
1454
- { label: '家园等级', value: playerField(playerSearchData, 'home_level') },
1745
+ { label: '在线', value: (0, player_service_1.playerField)(playerSearchData, 'online', '未知') },
1746
+ { label: '性别', value: (0, player_service_1.playerField)(playerSearchData, 'gender', (0, player_service_1.playerField)(playerSearchData, 'sex', '未知')) },
1747
+ { label: '世界等级', value: (0, player_service_1.playerField)(playerSearchData, 'world_level') },
1748
+ { label: '家园等级', value: (0, player_service_1.playerField)(playerSearchData, 'home_level') },
1455
1749
  ].filter((item) => item.value && item.value !== '-' && item.value !== '未设置').slice(0, 4) : [];
1456
1750
  const profileHomeItems = playerSearchData ? [
1457
- { label: '家园名称', value: playerField(playerSearchData, 'home_name') },
1458
- { label: '家园等级', value: playerField(playerSearchData, 'home_level') },
1459
- { label: '家园经验', value: playerField(playerSearchData, 'home_experience') },
1460
- { label: '舒适度', value: playerField(playerSearchData, 'home_comfort_level') },
1461
- { label: '访客数量', value: playerField(playerSearchData, 'visitor_num') },
1751
+ { label: '家园名称', value: (0, player_service_1.playerField)(playerSearchData, 'home_name') },
1752
+ { label: '家园等级', value: (0, player_service_1.playerField)(playerSearchData, 'home_level') },
1753
+ { label: '家园经验', value: (0, player_service_1.playerField)(playerSearchData, 'home_experience') },
1754
+ { label: '舒适度', value: (0, player_service_1.playerField)(playerSearchData, 'home_comfort_level') },
1755
+ { label: '访客数量', value: (0, player_service_1.playerField)(playerSearchData, 'visitor_num') },
1462
1756
  ].filter((item) => item.value && item.value !== '-' && item.value !== '未设置') : [];
1463
1757
  const profileCardItems = playerSearchData ? [
1464
- { label: '名片皮肤', value: playerField(playerSearchData, 'card_skin_selected') },
1465
- { label: '名片头像', value: playerField(playerSearchData, 'card_icon_selected') },
1758
+ { label: '名片皮肤', value: (0, player_service_1.playerField)(playerSearchData, 'card_skin_selected') },
1759
+ { label: '名片头像', value: (0, player_service_1.playerField)(playerSearchData, 'card_icon_selected') },
1466
1760
  ].filter((item) => item.value && item.value !== '-' && item.value !== '未设置') : [];
1467
- const profileCardImage = playerSearchData ? playerField(playerSearchData, 'card_bussiness_card_url', '') : '';
1468
- const profileStatusText = playerSearchData ? playerField(playerSearchData, 'online', '未知') : '未知';
1761
+ const profileCardImage = playerSearchData ? (0, player_service_1.resourceUrl)((0, player_service_1.playerField)(playerSearchData, 'card_bussiness_card_url', ''), deps.config.apiBaseUrl) : '';
1762
+ const profileStatusText = playerSearchData ? (0, player_service_1.playerField)(playerSearchData, 'online', '未知') : '未知';
1469
1763
  const hasExtraProfileData = Boolean(profileSignature || profileHomeItems.length || profileCardItems.length || profileCardImage);
1470
1764
  let degraded = false;
1471
1765
  if (!sm) {
@@ -1579,7 +1873,7 @@ function register(deps) {
1579
1873
  profileStatusText,
1580
1874
  profileStatusClass: profileStatusText === '是' ? 'online' : 'offline',
1581
1875
  commandHint: '洛克.背包 <筛选> <页码> | 洛克.战绩 <页码> | 洛克 查看菜单',
1582
- copyright: 'AstrBot & WeGame Locke Kingdom Plugin',
1876
+ copyright: 'Koishi & WeGame Roco Kingdom Plugin',
1583
1877
  };
1584
1878
  if (recentBattle) {
1585
1879
  data.hasBattleData = true;
@@ -1593,7 +1887,7 @@ function register(deps) {
1593
1887
  icon: pet.pet_img_url?.replace('/image.png', '/icon.png') || '',
1594
1888
  }));
1595
1889
  }
1596
- const fallback = `【${role.name}的档案】Lv.${role.level} UID:${role.id}\n评分:${ev.score || '0'} 收藏:${cl.current_collection_count || 0}/${cl.total_collection_count || 0}`;
1890
+ const fallback = `【${role.name || '洛克玩家'}的档案】Lv.${role.level || '--'} UID:${role.id || '--'}\n评分:${ev.score || '0'} 收藏:${cl.current_collection_count || 0}/${cl.total_collection_count || 0}`;
1597
1891
  await sendImage(deps, session, 'personal-card', data, fallback);
1598
1892
  });
1599
1893
  ctx.command('洛克').subcommand('.战绩 [page:number]', '查看对战战绩')
@@ -1633,7 +1927,7 @@ function register(deps) {
1633
1927
  });
1634
1928
  const data = {
1635
1929
  userAvatarDisplay: role.avatar_url || '',
1636
- userName: role.name,
1930
+ userName: role.name || '洛克玩家',
1637
1931
  userLevel: role.level || 1,
1638
1932
  userUid: role.id || '',
1639
1933
  winRate: `${bo.win_rate || 0}%`,
@@ -1642,7 +1936,7 @@ function register(deps) {
1642
1936
  commandHint: '洛克.战绩 <页码>',
1643
1937
  copyright: 'Koishi & WeGame 洛克王国插件',
1644
1938
  };
1645
- await sendImage(deps, session, 'record', data, `【${role.name}的战绩】胜率:${bo.win_rate || 0}% 场次:${bo.total_match || 0}`);
1939
+ await sendImage(deps, session, 'record', data, `【${role.name || '洛克玩家'}的战绩】胜率:${bo.win_rate || 0}% 场次:${bo.total_match || 0}`);
1646
1940
  });
1647
1941
  ctx.command('洛克').subcommand('.背包 [arg1:string] [arg2:string]', '查看精灵背包')
1648
1942
  .alias('洛克背包')
@@ -1838,15 +2132,33 @@ function register(deps) {
1838
2132
  };
1839
2133
  await sendImage(deps, session, 'exchange-hall', data, `【交换大厅】第${page}页`);
1840
2134
  });
1841
- ctx.command('洛克').subcommand('.玩家 <uid:string>', '通过 ingame 接口查询玩家基础资料')
2135
+ ctx.command('洛克').subcommand('.玩家 [uid:string]', '通过 ingame 接口查询玩家资料')
1842
2136
  .alias('洛克玩家')
1843
- .action(async ({ session }, uid) => {
1844
- if (!uid)
1845
- return '请提供玩家 UID。用法:洛克.玩家 <UID>';
1846
- const res = await client.ingamePlayerSearch(ctx, uid);
1847
- if (!res)
2137
+ .action(async ({ session }, uid = '') => {
2138
+ const userId = session.userId;
2139
+ const explicitUid = String(uid || '').trim();
2140
+ const binding = deps.userMgr.getPrimaryBinding(userId);
2141
+ const targetUid = explicitUid || String(binding?.role_id || '');
2142
+ const fwToken = await (0, account_1.getPrimaryToken)(deps, userId);
2143
+ if (!targetUid && !fwToken) {
2144
+ return '请提供玩家 UID,或先使用“洛克.QQ登录 / 洛克.微信登录”完成绑定。用法:洛克.玩家 [UID]';
2145
+ }
2146
+ const auth = { fwToken, userIdentifier: userId };
2147
+ const search = await client.ingamePlayerSearch(ctx, targetUid, { auth });
2148
+ if (!search)
1848
2149
  return `玩家搜索失败:${client.getLastErrorBrief()}`;
1849
- await sendImage(deps, session, 'player-search', buildPlayerSearchRenderData(res, uid), `【洛克玩家】UID ${uid}`);
2150
+ // 显式 UID 与搜索返回 UID 不一致时不合并名片,避免混用两个角色数据。
2151
+ const returnedUid = String(search?.player_info?.uin ?? search?.player_info?.uid ?? search?.uin ?? search?.uid ?? '');
2152
+ const uidConsistent = !explicitUid || !returnedUid || returnedUid === explicitUid;
2153
+ let card = uidConsistent
2154
+ ? await client.ingamePlayerCard(ctx, targetUid, { auth }).catch(() => null)
2155
+ : null;
2156
+ const cardUid = (0, player_service_1.playerPayloadUid)(card);
2157
+ if (cardUid && (targetUid || returnedUid) && cardUid !== (targetUid || returnedUid))
2158
+ card = null;
2159
+ const view = (0, player_service_1.buildPlayerView)(search, card, targetUid, deps.config.apiBaseUrl);
2160
+ const merged = (0, player_service_1.mergePlayerPayloads)(search, card);
2161
+ await sendImage(deps, session, 'player-search', buildPlayerSearchRenderData(merged, targetUid || view.parsed.uid, view.cardImageUrl), (0, player_service_1.buildPlayerText)(merged, targetUid || view.parsed.uid));
1850
2162
  });
1851
2163
  ctx.command('洛克').subcommand('.家园 [uid:string]', '通过 UID 查询家园菜园、守卫和室内精灵')
1852
2164
  .alias('洛克家园')
@@ -1856,10 +2168,12 @@ function register(deps) {
1856
2168
  const binding = deps.userMgr.getPrimaryBinding(session.userId);
1857
2169
  targetUid = String(binding?.role_id || '');
1858
2170
  }
1859
- if (!targetUid)
2171
+ const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
2172
+ if (!targetUid && !fwToken)
1860
2173
  return '请提供玩家 UID,或先完成绑定后使用 洛克.家园。';
1861
2174
  let queuedNotified = false;
1862
2175
  const res = await client.ingameHomeInfo(ctx, targetUid, {
2176
+ auth: { fwToken, userIdentifier: session.userId },
1863
2177
  waitMs: deps.config.homeQueryWaitMs,
1864
2178
  intervalMs: deps.config.homeQueryPollIntervalMs,
1865
2179
  timeoutMs: deps.config.homeQueryTimeoutMs,
@@ -1914,7 +2228,9 @@ function register(deps) {
1914
2228
  ].join('\n');
1915
2229
  }
1916
2230
  const lowBandwidth = Boolean(deps.config.lowBandwidthMode);
1917
- // Wiki 补全(技能详情/精灵体重范围);低带宽模式跳过技能图标但仍取技能名。
2231
+ // classic 样式走 pet-detail-classic 模板(立绘 + 属性表 + 技能栏),
2232
+ // 不需要精灵体重范围(sizeLookup)。
2233
+ const useClassic = deps.config.homeDetailUiStyle !== 'panel';
1918
2234
  const [optionsPayload, skillLookup, sizeLookup] = await Promise.all([
1919
2235
  deps.wikiService.getOptionsPayload().catch(() => ({})),
1920
2236
  (async () => {
@@ -1928,6 +2244,8 @@ function register(deps) {
1928
2244
  return lookup;
1929
2245
  })(),
1930
2246
  (async () => {
2247
+ if (useClassic)
2248
+ return {};
1931
2249
  const lookup = {};
1932
2250
  const ids = (0, pet_data_service_1.petDataPetIdsFromPayload)(res);
1933
2251
  await Promise.all(ids.map(async (id) => {
@@ -1938,7 +2256,16 @@ function register(deps) {
1938
2256
  return lookup;
1939
2257
  })(),
1940
2258
  ]);
1941
- const data = (0, pet_data_service_1.buildPetDataRenderData)(res, targetUid || '当前绑定', {
2259
+ const classicData = useClassic
2260
+ ? (0, pet_data_service_1.buildPetDetailClassicView)(res, targetUid || '当前绑定', {
2261
+ optionsPayload,
2262
+ skillLookup,
2263
+ baseUrl: deps.config.apiBaseUrl.replace(/\/$/, ''),
2264
+ loadSkillIcons: !lowBandwidth,
2265
+ variantIconPath: mutationType => `render-templates/pet-detail-classic/texture2D/star_${mutationType}.png`,
2266
+ })
2267
+ : null;
2268
+ const data = classicData || (0, pet_data_service_1.buildPetDataRenderData)(res, targetUid || '当前绑定', {
1942
2269
  optionsPayload,
1943
2270
  skillLookup,
1944
2271
  sizeLookup,
@@ -1947,18 +2274,94 @@ function register(deps) {
1947
2274
  baseUrl: deps.config.apiBaseUrl.replace(/\/$/, ''),
1948
2275
  });
1949
2276
  const fallbackLines = [
1950
- `家园详情 - UID ${data.uid}(${data.onlineText})`,
1951
- data.notice,
2277
+ `家园详情 - UID ${data.uid}${data.onlineText ? `(${data.onlineText})` : ''}`,
2278
+ data.notice || '',
1952
2279
  ];
1953
2280
  for (const pet of (data.pets || []).slice(0, 8)) {
1954
- fallbackLines.push(`${pet.index}. ${pet.name} Lv.${pet.level} #${pet.baseId} ${pet.variantText} 分贝 ${pet.voiceText || '--'}`);
2281
+ const bits = [
2282
+ pet.index ? `${pet.index}.` : '',
2283
+ `${pet.name} Lv.${pet.level}`,
2284
+ pet.variantText,
2285
+ `分贝 ${pet.voiceText || '--'}`,
2286
+ ].filter(Boolean);
2287
+ fallbackLines.push(bits.join(' '));
1955
2288
  }
1956
2289
  if (!data.pets?.length)
1957
2290
  fallbackLines.push(data.emptyText);
1958
2291
  fallbackLines.push('');
1959
2292
  fallbackLines.push('若服务器带宽过小导致生成超时,请在配置项打开低带宽模式。');
1960
2293
  fallbackLines.push('低带宽模式开启后,家园详情将不再加载技能图标。');
1961
- await sendImage(deps, session, 'pet-data', data, fallbackLines.join('\n'));
2294
+ await sendImage(deps, session, classicData ? 'pet-detail-classic' : 'pet-data', data, fallbackLines.join('\n'), {
2295
+ ...(lowBandwidth ? render_1.lowBandwidthRenderOptions : {}),
2296
+ timeoutMs: deps.config.renderTimeout || 30000,
2297
+ });
2298
+ });
2299
+ // 家园精灵一览(home/pet-data),体重、声音、变异与生蛋状态。
2300
+ ctx.command('洛克').subcommand('.家园精灵 [uid:string]', '查看家园精灵列表(体重、声音、生蛋状态)')
2301
+ .alias('洛克家园精灵')
2302
+ .alias('家园精灵')
2303
+ .action(async ({ session }, uid = '') => {
2304
+ let targetUid = String(uid || '').trim();
2305
+ if (!targetUid) {
2306
+ const binding = deps.userMgr.getPrimaryBinding(session.userId);
2307
+ targetUid = String(binding?.role_id || '');
2308
+ }
2309
+ const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
2310
+ if (!targetUid && !fwToken)
2311
+ return '请提供玩家 UID,或先完成绑定后使用 洛克.家园精灵。';
2312
+ let queuedNotified = false;
2313
+ const res = await client.ingameHomePetData(ctx, targetUid, {
2314
+ auth: { fwToken, userIdentifier: session.userId },
2315
+ waitMs: deps.config.homeQueryWaitMs,
2316
+ intervalMs: deps.config.homeQueryPollIntervalMs,
2317
+ timeoutMs: deps.config.homeQueryTimeoutMs,
2318
+ onQueued: async () => {
2319
+ if (queuedNotified)
2320
+ return;
2321
+ queuedNotified = true;
2322
+ await session?.send?.(`UID ${targetUid} 的家园精灵查询已进入队列,正在等待游戏侧返回,请稍候…`);
2323
+ },
2324
+ });
2325
+ if (!res) {
2326
+ return [
2327
+ `家园精灵查询失败:${client.getLastErrorBrief()}`,
2328
+ '提示:该接口需要目标玩家在线,且目标家园可访问。',
2329
+ ].join('\n');
2330
+ }
2331
+ const data = buildHomePetsViewData(deps, res, targetUid || '当前绑定');
2332
+ await sendImage(deps, session, 'home-pets', data, buildHomePetsViewText(data), {
2333
+ timeoutMs: deps.config.renderTimeout || 30000,
2334
+ });
2335
+ });
2336
+ // 立即查询指定 UID 的家园蛋状态。
2337
+ ctx.command('洛克').subcommand('.家园状态 [uid:string]', '查询家园蛋状态(可收蛋精灵与倒计时)')
2338
+ .alias('洛克家园状态')
2339
+ .alias('家园状态')
2340
+ .action(async ({ session }, uid = '') => {
2341
+ let targetUid = String(uid || '').trim();
2342
+ if (!targetUid) {
2343
+ const binding = deps.userMgr.getPrimaryBinding(session.userId);
2344
+ targetUid = String(binding?.role_id || '');
2345
+ }
2346
+ const fwToken = await (0, account_1.getPrimaryToken)(deps, session.userId);
2347
+ if (!targetUid && !fwToken)
2348
+ return `请提供玩家 UID,或先完成绑定后使用 洛克.家园状态。`;
2349
+ let queuedNotified = false;
2350
+ const res = await client.ingameHomeEggs(ctx, targetUid, {
2351
+ auth: { fwToken, userIdentifier: session.userId },
2352
+ waitMs: deps.config.homeQueryWaitMs,
2353
+ intervalMs: deps.config.homeQueryPollIntervalMs,
2354
+ timeoutMs: deps.config.homeQueryTimeoutMs,
2355
+ onQueued: async () => {
2356
+ if (queuedNotified)
2357
+ return;
2358
+ queuedNotified = true;
2359
+ await session?.send?.(`UID ${targetUid} 的家园蛋状态查询已进入队列,正在等待游戏侧返回,请稍候…`);
2360
+ },
2361
+ });
2362
+ if (!res)
2363
+ return `家园蛋状态查询失败:${client.getLastErrorBrief()}`;
2364
+ return buildHomeEggStatusText(targetUid, extractHomeEggStatus(res));
1962
2365
  });
1963
2366
  ctx.command('洛克').subcommand('.刷新面板 [uid:string]', '刷新指定 UID 的精灵面板缓存')
1964
2367
  .alias('洛克刷新面板')
@@ -2041,15 +2444,16 @@ function register(deps) {
2041
2444
  const fallback = panelPetDetailFallback(targetUid, selected, updatedAtText);
2042
2445
  await sendImage(deps, session, 'pet-panel-detail', data, fallback);
2043
2446
  });
2044
- ctx.command('洛克').subcommand('.商店 <shopId:string>', '通过 ingame 接口查询商店信息')
2447
+ ctx.command('洛克').subcommand('.商店 [shopId:string]', '通过 ingame 接口查询商店信息,默认商店 3009')
2045
2448
  .alias('洛克商店')
2046
- .action(async ({ session }, shopId) => {
2047
- if (!shopId)
2048
- return '请提供商店 ID。用法:洛克.商店 <shop_id>';
2049
- const res = await client.ingameMerchantInfo(ctx, shopId);
2449
+ .action(async ({ session }, shopId = '3009') => {
2450
+ const targetShopId = String(shopId || '3009').trim() || '3009';
2451
+ if (!/^\d+$/.test(targetShopId))
2452
+ return '商店 ID 必须是数字。用法:洛克.商店 [shop_id],默认 3009';
2453
+ const res = await client.ingameMerchantInfo(ctx, targetShopId);
2050
2454
  if (!res)
2051
2455
  return `商店查询失败:${client.getLastErrorBrief()}`;
2052
- await sendImage(deps, session, 'ingame-shop', buildShopRenderData(res, shopId), `【洛克商店】shop_id=${shopId}`);
2456
+ await sendImage(deps, session, 'ingame-shop', buildShopRenderData(res, targetShopId, { assetBaseUrl: client.wikiAssetBaseUrl }), `【洛克商店】shop_id=${targetShopId}`);
2053
2457
  });
2054
2458
  ctx.command('洛克').subcommand('.好友关系 <userIds:string>', '查询好友关系')
2055
2459
  .alias('洛克好友关系')
@@ -2064,17 +2468,51 @@ function register(deps) {
2064
2468
  return `好友关系查询失败:${client.getLastErrorBrief()}`;
2065
2469
  await sendImage(deps, session, 'friendship', buildFriendshipRenderData(res, userIds), `【好友关系】${userIds}`);
2066
2470
  });
2067
- ctx.command('订阅家园菜园 [uid:string]', '订阅指定 UID 的家园菜园成熟提醒')
2068
- .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'garden'));
2069
- ctx.command('订阅家园灵感 [uid:string]', '订阅指定 UID 的家园精灵灵感完成提醒')
2070
- .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'inspiration'));
2071
- ctx.command('订阅家园生蛋 [uid:string]', '订阅指定 UID 的家园精灵生蛋提醒')
2072
- .action(async ({ session }, uid = '') => subscribeHome(deps, session, uid, 'egg'));
2471
+ ctx.command('订阅家园菜园 [args:text]', '订阅指定 UID 的家园菜园成熟提醒')
2472
+ .userFields(['authority'])
2473
+ .action(async ({ session }, args = '') => {
2474
+ const parsed = parseHomeSubscribeArgs(args);
2475
+ return subscribeHome(deps, session, parsed.uid, 'garden', parsed.mentionAll);
2476
+ });
2477
+ ctx.command('订阅家园灵感 [args:text]', '订阅指定 UID 的家园精灵灵感完成提醒')
2478
+ .userFields(['authority'])
2479
+ .action(async ({ session }, args = '') => {
2480
+ const parsed = parseHomeSubscribeArgs(args);
2481
+ return subscribeHome(deps, session, parsed.uid, 'inspiration', parsed.mentionAll);
2482
+ });
2483
+ ctx.command('订阅家园生蛋 [args:text]', '订阅指定 UID 的家园精灵生蛋提醒')
2484
+ .userFields(['authority'])
2485
+ .action(async ({ session }, args = '') => {
2486
+ const parsed = parseHomeSubscribeArgs(args);
2487
+ return subscribeHome(deps, session, parsed.uid, 'egg', parsed.mentionAll);
2488
+ });
2489
+ // 查看当前会话的家园订阅列表。
2490
+ ctx.command('我的家园订阅', '查看当前会话的家园订阅列表')
2491
+ .userFields(['authority'])
2492
+ .action(async ({ session }) => {
2493
+ const target = sessionTarget(session);
2494
+ const kindLabel = { garden: '菜园', inspiration: '灵感', egg: '生蛋' };
2495
+ const subs = Object.values(deps.homeSubMgr.getAll()).filter(sub => sub.platform === target.platform
2496
+ && (sub.channel_id || '') === (target.channelId || '')
2497
+ && (sub.user_id || '') === (target.userId || ''));
2498
+ if (!subs.length)
2499
+ return (0, permissions_1.isDirectSession)(session) ? '你当前没有家园订阅。' : '本群当前没有家园订阅。';
2500
+ const formatTime = (ts) => (ts ? new Date(ts * 1000).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) : '从未');
2501
+ const lines = [`当前家园订阅(共 ${subs.length} 个):`, ''];
2502
+ for (const sub of subs) {
2503
+ const mentionText = sub.mention_all && !sub.user_id ? ',命中后 @全体' : '';
2504
+ lines.push(`UID ${sub.uid}(${kindLabel[sub.kind] || sub.kind}${mentionText}):上次检查 ${formatTime(sub.last_check_time)},上次推送 ${formatTime(sub.last_push_time)}`);
2505
+ }
2506
+ lines.push('');
2507
+ lines.push('发送 取消订阅家园 [菜园/灵感/生蛋] [UID] 取消订阅');
2508
+ return lines.join('\n');
2509
+ });
2073
2510
  ctx.command('取消订阅家园 [kind:string] [uid:string]', '取消当前会话的家园订阅')
2511
+ .userFields(['authority'])
2074
2512
  .action(async ({ session }, kind = '全部', uid = '') => {
2075
2513
  const target = sessionTarget(session);
2076
- if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds))
2077
- return '此指令仅限管理员使用。';
2514
+ if (!target.userId && !canManageGroupSubscription(deps, session))
2515
+ return '此指令仅限群管理员或机器人管理员使用。';
2078
2516
  const kindMap = { '菜园': 'garden', '灵感': 'inspiration', '生蛋': 'egg', '全部': '', all: '', garden: 'garden', inspiration: 'inspiration', egg: 'egg' };
2079
2517
  const deleted = deps.homeSubMgr.deleteMatching(target, kindMap[String(kind || '全部')] ?? '', String(uid || '').trim());
2080
2518
  return deleted ? `已取消 ${deleted} 条家园订阅。` : '当前会话没有匹配的家园订阅。';
@@ -2084,10 +2522,12 @@ function register(deps) {
2084
2522
  .action(async ({ session }) => {
2085
2523
  if (!isBotAdmin(session, deps.config.adminUserIds))
2086
2524
  return '此指令仅限管理员使用。';
2087
- const result = await checkHomeSubscriptions(deps);
2525
+ const result = await homeRunner(signal => checkHomeSubscriptions(deps, signal));
2526
+ if (!result)
2527
+ return '订阅检查正在运行或插件已停止。';
2088
2528
  return `家园订阅检查完成:订阅 ${result.subscriptions} 条,检查 ${result.checked} 条,推送 ${result.pushed} 档提醒。`;
2089
2529
  });
2090
2530
  if (deps.config.homeSubscriptionEnabled) {
2091
- ctx.setInterval(() => checkHomeSubscriptions(deps).catch(err => logger.warn(`家园订阅检查失败: ${err}`)), Math.max(1, deps.config.homeSubscriptionIntervalMinutes || 5) * 60000);
2531
+ ctx.setInterval(() => homeRunner(signal => checkHomeSubscriptions(deps, signal)).catch(err => logger.warn(`家园订阅检查失败: ${err}`)), Math.max(1, deps.config.homeSubscriptionIntervalMinutes || 5) * 60000);
2092
2532
  }
2093
2533
  }