koishi-plugin-rocom 1.0.19 → 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 (183) hide show
  1. package/lib/atlas-service.js +1 -1
  2. package/lib/client.d.ts +10 -0
  3. package/lib/client.js +44 -8
  4. package/lib/commands/egg.js +1 -1
  5. package/lib/commands/merchant.d.ts +105 -0
  6. package/lib/commands/merchant.js +474 -54
  7. package/lib/commands/query.d.ts +18 -0
  8. package/lib/commands/query.js +446 -37
  9. package/lib/commands/share-code.js +47 -2
  10. package/lib/commands/tools.js +1 -1
  11. package/lib/commands/wiki.js +1 -1
  12. package/lib/egg-service.js +1 -1
  13. package/lib/element-colors.d.ts +3 -0
  14. package/lib/element-colors.js +34 -0
  15. package/lib/icon-metrics.d.ts +32 -0
  16. package/lib/icon-metrics.js +255 -0
  17. package/lib/index.d.ts +5 -0
  18. package/lib/index.js +11857 -188
  19. package/lib/merchant-catalog.d.ts +47 -0
  20. package/lib/merchant-catalog.js +193 -0
  21. package/lib/merchant-price-log.d.ts +63 -0
  22. package/lib/merchant-price-log.js +187 -0
  23. package/lib/merchant-service.d.ts +17 -3
  24. package/lib/merchant-service.js +79 -11
  25. package/lib/pet-data-service.d.ts +13 -0
  26. package/lib/pet-data-service.js +111 -2
  27. package/lib/player-service.js +2 -2
  28. package/lib/ranking-service.js +1 -1
  29. package/lib/render-templates/home/style.css +15 -14
  30. package/lib/render-templates/home-pets/index.html +80 -0
  31. package/lib/render-templates/home-pets/style.css +338 -0
  32. package/lib/render-templates/pet-detail-classic/index.html +151 -0
  33. package/lib/render-templates/pet-detail-classic/style.css +503 -0
  34. package/lib/render-templates/pet-detail-classic/texture2D/a_title.png +0 -0
  35. package/lib/render-templates/pet-detail-classic/texture2D/bg.jpg +0 -0
  36. package/lib/render-templates/pet-detail-classic/texture2D/footer.png +0 -0
  37. package/lib/render-templates/pet-detail-classic/texture2D/icon.png +0 -0
  38. package/lib/render-templates/pet-detail-classic/texture2D/img_head.png +0 -0
  39. package/lib/render-templates/pet-detail-classic/texture2D/jinhua_bg.png +0 -0
  40. package/lib/render-templates/pet-detail-classic/texture2D/level_icon.png +0 -0
  41. package/lib/render-templates/pet-detail-classic/texture2D/line.png +0 -0
  42. package/lib/render-templates/pet-detail-classic/texture2D/mask_bar.png +0 -0
  43. package/lib/render-templates/pet-detail-classic/texture2D/pet_bg.png +0 -0
  44. package/lib/render-templates/pet-detail-classic/texture2D/pet_rocom.png +0 -0
  45. package/lib/render-templates/pet-detail-classic/texture2D/pet_rocom_bg.png +0 -0
  46. package/lib/render-templates/pet-detail-classic/texture2D/poro_bar.png +0 -0
  47. package/lib/render-templates/pet-detail-classic/texture2D/right_jinhua.png +0 -0
  48. package/lib/render-templates/pet-detail-classic/texture2D/skill_bg.png +0 -0
  49. package/lib/render-templates/pet-detail-classic/texture2D/skill_mask.png +0 -0
  50. package/lib/render-templates/pet-detail-classic/texture2D/star.png +0 -0
  51. package/lib/render-templates/pet-detail-classic/texture2D/star_1.png +0 -0
  52. package/lib/render-templates/pet-detail-classic/texture2D/star_8.png +0 -0
  53. package/lib/render-templates/pet-detail-classic/texture2D/star_9.png +0 -0
  54. package/lib/render-templates/pet-detail-classic/texture2D/table.png +0 -0
  55. package/lib/render-templates/pet-detail-classic/texture2D/tags.png +0 -0
  56. package/lib/render-templates/pet-detail-classic/texture2D/title.png +0 -0
  57. package/lib/render-templates/pet-detail-classic/texture2D/top_bg.png +0 -0
  58. package/lib/render-templates/pet-detail-classic/texture2D/xuancai_overlay.png +0 -0
  59. package/lib/render-templates/pet-detail-classic/texture2D/yise_overlay.png +0 -0
  60. package/lib/render-templates/pet-detail-classic/texture2D/ys_bar.png +0 -0
  61. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/10.png +0 -0
  62. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/11.png +0 -0
  63. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/12.png +0 -0
  64. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/13.png +0 -0
  65. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/14.png +0 -0
  66. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/15.png +0 -0
  67. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/16.png +0 -0
  68. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/17.png +0 -0
  69. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/18.png +0 -0
  70. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/19.png +0 -0
  71. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/2.png +0 -0
  72. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/20.png +0 -0
  73. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/23.png +0 -0
  74. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/3.png +0 -0
  75. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/4.png +0 -0
  76. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/5.png +0 -0
  77. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/6.png +0 -0
  78. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/8.png +0 -0
  79. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/9.png +0 -0
  80. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/205/211.png +0 -0
  81. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/206/260.png +0 -0
  82. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/234/260.png +0 -0
  83. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/271/273.png +0 -0
  84. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/271/275.png +0 -0
  85. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/201/266.png +0 -0
  86. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/227/240.png +0 -0
  87. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/231/256/351/200/232.png +0 -0
  88. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/234/272/346/242/260.png +0 -0
  89. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/255/246.png +0 -0
  90. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/257/222.png +0 -0
  91. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/260/264.png +0 -0
  92. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/201/253.png +0 -0
  93. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/224/265.png +0 -0
  94. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/277/274.png +0 -0
  95. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/215/211.png +0 -0
  96. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/220/214.png +0 -0
  97. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/231/253.png +0 -0
  98. package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//351/276/231.png +0 -0
  99. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/1.png +0 -0
  100. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/10.png +0 -0
  101. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/11.png +0 -0
  102. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/12.png +0 -0
  103. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/13.png +0 -0
  104. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/14.png +0 -0
  105. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/15.png +0 -0
  106. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/16.png +0 -0
  107. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/17.png +0 -0
  108. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/18.png +0 -0
  109. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/19.png +0 -0
  110. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/2.png +0 -0
  111. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/21.png +0 -0
  112. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/23.png +0 -0
  113. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/24.png +0 -0
  114. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/3.png +0 -0
  115. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/4.png +0 -0
  116. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/5.png +0 -0
  117. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/6.png +0 -0
  118. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/7.png +0 -0
  119. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/8.png +0 -0
  120. package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/9.png +0 -0
  121. package/lib/render-templates/share-code-classic/assets/eit-logo.png +0 -0
  122. package/lib/render-templates/share-code-classic/assets/img_xingxingdi.png +0 -0
  123. package/lib/render-templates/share-code-classic/assets/roco_icon.png +0 -0
  124. package/lib/render-templates/share-code-classic/assets//345/255/227/344/275/223/Rocofontblack-Regular .ttf +0 -0
  125. 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
  126. 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
  127. 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
  128. 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
  129. 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
  130. 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
  131. 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
  132. 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
  133. 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
  134. 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
  135. 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
  136. 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
  137. 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
  138. 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
  139. 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
  140. 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
  141. 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
  142. 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
  143. 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
  144. package/lib/render-templates/share-code-classic/assets//347/201/253/346/274/206/345/215/260.png +0 -0
  145. 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
  146. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/205/211.png +0 -0
  147. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/206/260.png +0 -0
  148. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/234/260.png +0 -0
  149. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/245/207/345/274/202.png +0 -0
  150. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/271/273.png +0 -0
  151. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/271/275.png +0 -0
  152. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/201/266.png +0 -0
  153. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/231/256/351/200/232.png +0 -0
  154. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/234/272/346/242/260.png +0 -0
  155. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/255/246.png +0 -0
  156. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/257/222.png +0 -0
  157. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/260/264.png +0 -0
  158. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/261/241/346/237/223.png +0 -0
  159. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/201/253.png +0 -0
  160. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/224/265.png +0 -0
  161. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/277/274.png +0 -0
  162. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/215/211.png +0 -0
  163. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/220/214.png +0 -0
  164. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/231/253.png +0 -0
  165. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//351/246/226/351/242/206.png +0 -0
  166. package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//351/276/231.png +0 -0
  167. package/lib/render-templates/share-code-classic/index.html +119 -0
  168. package/lib/render-templates/share-code-classic/style.css +646 -0
  169. package/lib/render-templates/yuanxing-shangren/merchant.html +479 -461
  170. package/lib/render-templates/yuanxing-shangren/today.html +407 -461
  171. package/lib/render.d.ts +3 -0
  172. package/lib/render.js +19 -3
  173. package/lib/send-image.js +10 -0
  174. package/lib/share-code-service.d.ts +36 -0
  175. package/lib/share-code-service.js +73 -0
  176. package/lib/ttf/rocom_origin.ttf +0 -0
  177. package/lib/types.d.ts +9 -1
  178. package/lib/user.d.ts +6 -0
  179. package/lib/wiki-catalog.js +1 -1
  180. package/lib/wiki-service.js +1 -1
  181. package/package.json +2 -1
  182. package/readme.md +3 -23
  183. package/lib/ttf/rocom_skill_origin.ttf +0 -0
@@ -1,15 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildMerchantRenderParams = buildMerchantRenderParams;
4
+ exports.buildMerchantCardItems = buildMerchantCardItems;
5
+ exports.buildTodayRows = buildTodayRows;
3
6
  exports.register = register;
4
7
  const koishi_1 = require("koishi");
5
8
  const send_image_1 = require("../send-image");
6
9
  const subscription_send_1 = require("../subscription-send");
7
10
  const merchant_service_1 = require("../merchant-service");
11
+ const merchant_catalog_1 = require("../merchant-catalog");
8
12
  const merchant_clock_1 = require("../merchant-clock");
9
13
  const permissions_1 = require("../permissions");
10
14
  const subscription_runner_1 = require("../subscription-runner");
11
15
  const node_async_hooks_1 = require("node:async_hooks");
12
16
  const async_control_1 = require("../async-control");
17
+ const icon_metrics_1 = require("../icon-metrics");
13
18
  const logger = new koishi_1.Logger('rocom-merchant');
14
19
  const TEXT = {
15
20
  merchant: '\u8fdc\u884c\u5546\u4eba',
@@ -34,6 +39,52 @@ const ROUND_WINDOWS = [
34
39
  { id: 3, label: '16:00-20:00', startHour: 16, endHour: 20 },
35
40
  { id: 4, label: '20:00-24:00', startHour: 20, endHour: 24 },
36
41
  ];
42
+ // 商人卡片按原生尺寸渲染:1080 设计稿缩到 1/4 排版,再整体 zoom 放大;
43
+ // 视口宽 960 + deviceScaleFactor 4 → 3840 物理像素输出。
44
+ const MERCHANT_CARD_SOURCE_WIDTH = 1080;
45
+ const MERCHANT_CARD_LAYOUT_SCALE = 0.25;
46
+ const MERCHANT_CARD_OUTPUT_WIDTH = 3840;
47
+ const MERCHANT_CARD_DEVICE_SCALE_FACTOR = 4;
48
+ const MERCHANT_CARD_RENDER_SCALE = MERCHANT_CARD_LAYOUT_SCALE;
49
+ const MERCHANT_CARD_RENDER_WIDTH = Math.ceil(MERCHANT_CARD_OUTPUT_WIDTH / MERCHANT_CARD_DEVICE_SCALE_FACTOR);
50
+ const MERCHANT_CARD_OUTPUT_SCALE = MERCHANT_CARD_RENDER_WIDTH / MERCHANT_CARD_SOURCE_WIDTH;
51
+ const MERCHANT_CARD_RENDER_BASE_WIDTH = MERCHANT_CARD_SOURCE_WIDTH * MERCHANT_CARD_LAYOUT_SCALE;
52
+ const MERCHANT_CARD_RENDER_ZOOM = MERCHANT_CARD_RENDER_WIDTH / MERCHANT_CARD_RENDER_BASE_WIDTH;
53
+ const MERCHANT_CARD_STROKE_11 = 11 * MERCHANT_CARD_RENDER_SCALE;
54
+ const MERCHANT_CARD_STROKE_10 = 10 * MERCHANT_CARD_RENDER_SCALE;
55
+ const MERCHANT_CARD_OFFSET_3 = 3 * MERCHANT_CARD_RENDER_SCALE;
56
+ // 今日卡片商品名只有 8.5px,沿用 11 倍描边半径会把字糊成一坨,单独用小半径
57
+ const MERCHANT_TODAY_NAME_STROKE = 3 * MERCHANT_CARD_RENDER_SCALE;
58
+ // 单件商品卡里 .goods-img-wrap 的尺寸(模板基准 px),图标等面积归一的框;
59
+ // 「不透明内容」统一缩放到的面积约等于 55×55 方图,再大细长商品会溢出图标框。
60
+ const MERCHANT_ICON_BOX_WIDTH = 74;
61
+ const MERCHANT_ICON_BOX_HEIGHT = 61.5;
62
+ const MERCHANT_ICON_TARGET_AREA = 3000;
63
+ // 单个图标取图超时;取不到就按原样渲染,不影响出卡片
64
+ const MERCHANT_ICON_FETCH_TIMEOUT_MS = 8000;
65
+ const MERCHANT_CARD_ICON_BOX = {
66
+ width: MERCHANT_ICON_BOX_WIDTH,
67
+ height: MERCHANT_ICON_BOX_HEIGHT,
68
+ targetArea: MERCHANT_ICON_TARGET_AREA,
69
+ };
70
+ // 今日卡片一轮一行:每行左侧轮次标签,右侧最多并排 5 件商品。
71
+ // 列宽固定(不随该轮商品数变化),否则同一张卡片里图标大小又会参差;
72
+ // 一轮超过 5 件时整行一起收窄、图标框跟着缩,保证不溢出行宽。
73
+ const MERCHANT_TODAY_START_Y = 592;
74
+ const MERCHANT_TODAY_ROW_HEIGHT = 368;
75
+ const MERCHANT_TODAY_ROW_GAP = 32;
76
+ // 底部信息框压在最后一行下沿 21px(沿用旧版 bottomFrameTop = lastTop + 287 算法),
77
+ // 写成正间距会在末尾留出一大片空白。
78
+ const MERCHANT_TODAY_FRAME_OVERLAP = 21;
79
+ const MERCHANT_TODAY_ROW_WIDTH = 260.75;
80
+ const MERCHANT_TODAY_TAB_WIDTH = 38;
81
+ const MERCHANT_TODAY_MAX_COLUMNS = 5;
82
+ const MERCHANT_TODAY_CELL_WIDTH = (MERCHANT_TODAY_ROW_WIDTH - MERCHANT_TODAY_TAB_WIDTH) / MERCHANT_TODAY_MAX_COLUMNS;
83
+ // 今日卡片的商品图标框(模板 .item-icon-wrap);等面积归一基准,行收窄时按面积比缩小
84
+ const MERCHANT_TODAY_ICON_SIZE = 40;
85
+ const MERCHANT_TODAY_ICON_TARGET_AREA = 1150;
86
+ // 图标尺寸按「去掉签名参数的地址」缓存,避免每次出卡片重复取图;失败缓存 null 不再重试。
87
+ const iconMetricsCache = new Map();
37
88
  const defaultClock = new merchant_clock_1.MerchantClock('Asia/Shanghai');
38
89
  // Scoped to each synchronous view build; plugin instances never overwrite another clock.
39
90
  const clockScope = new node_async_hooks_1.AsyncLocalStorage();
@@ -163,23 +214,27 @@ function getCurrentMerchantRound(now = new Date()) {
163
214
  round_id: `${datePart}-${currentRound || 'closed'}`,
164
215
  };
165
216
  }
217
+ // @全体 的写法。
218
+ const MENTION_ALL_ALIASES = new Set(['@全体', '全体', '@所有人', 'at全体', 'at所有人']);
166
219
  function parseMerchantSubscriptionArgs(args, defaultItems) {
167
220
  const parts = (args || '').split(/\s+/).filter(Boolean);
168
- let mentionAll = false;
221
+ let mentionAll = null;
169
222
  if (parts[0] === '1' || parts[0] === '0') {
170
223
  mentionAll = parts.shift() === '1';
171
224
  }
172
- const matchAll = parts.length === 1 && parts[0] === '全部';
225
+ // 支持「订阅远行商人 @全体 1 3」写法。
226
+ for (let i = parts.length - 1; i >= 0; i--) {
227
+ if (MENTION_ALL_ALIASES.has(parts[i].toLowerCase())) {
228
+ mentionAll = true;
229
+ parts.splice(i, 1);
230
+ }
231
+ }
232
+ const matchAll = parts.length === 1 && (0, merchant_catalog_1.isAllAlias)(parts[0]);
173
233
  if (matchAll)
174
234
  parts.shift();
175
235
  const items = matchAll ? [] : (parts.length ? parts : defaultItems);
176
236
  const source = matchAll ? '全部商品' : (parts.length ? TEXT.customSource : TEXT.defaultSource);
177
- return {
178
- mention_all: mentionAll,
179
- match_all: matchAll,
180
- items,
181
- source,
182
- };
237
+ return { mentionAll, match_all: matchAll, tokens: parts, items, source };
183
238
  }
184
239
  function getSubscriptionTarget(session) {
185
240
  const platform = session.platform || session.bot?.platform || '';
@@ -191,16 +246,16 @@ function getSubscriptionTarget(session) {
191
246
  function isBotAdmin(session, adminUserIds) {
192
247
  return adminUserIds.includes(session?.userId || '');
193
248
  }
194
- // 订阅管理权限:群主/管理员与 Bot 管理员按独立开关生效(上游 v4.0)。
249
+ // 订阅管理权限:群主/管理员与 Bot 管理员按独立开关生效。
195
250
  function canManageGroupSubscription(deps, session) {
196
251
  return (0, permissions_1.canManageSubscription)(deps.config, session);
197
252
  }
198
253
  function sameStringArray(left, right) {
199
254
  return JSON.stringify(left) === JSON.stringify(right);
200
255
  }
201
- function normalizeMerchantProducts(res, now = new Date()) {
256
+ function normalizeMerchantProducts(res, now = new Date(), options) {
202
257
  const nowMs = now.getTime();
203
- const { items } = (0, merchant_service_1.normalizeMerchant)(res, nowMs);
258
+ const { items } = (0, merchant_service_1.normalizeMerchant)(res, nowMs, options);
204
259
  return items.map((item) => {
205
260
  const source = { start_time: item.startsAt, end_time: item.endsAt };
206
261
  return {
@@ -215,14 +270,171 @@ function normalizeMerchantProducts(res, now = new Date()) {
215
270
  price: item.price,
216
271
  limit: item.limit,
217
272
  item_category: item.category,
273
+ allDay: item.allDay,
274
+ goods_id: item.id,
218
275
  };
219
276
  });
220
277
  }
221
- function buildMerchantCardItems(products, options) {
278
+ // 实时接口只给当前轮次的价格,轮次一过就拿不到了;
279
+ // 每次拿到实时数据先按 goods_id + 轮次落盘,供当天后续轮次补价。
280
+ function recordLiveMerchantPrices(deps, res, now = new Date()) {
281
+ const priceLog = deps.merchantPriceLog;
282
+ if (!priceLog || !res || res._source !== 'live' || !Array.isArray(res.goods))
283
+ return;
284
+ const nowMs = now.getTime();
285
+ const currentRound = getCurrentMerchantRound(now).current ?? 0;
286
+ const mapping = new Map();
287
+ for (const meta of Array.isArray(res.goods_mapping) ? res.goods_mapping : []) {
288
+ if (meta && meta.goods_id !== undefined && meta.goods_id !== null)
289
+ mapping.set(String(meta.goods_id), meta);
290
+ }
291
+ for (const meta of Array.isArray(res._goods_mapping) ? res._goods_mapping : []) {
292
+ if (meta && meta.goods_id !== undefined && meta.goods_id !== null && !mapping.has(String(meta.goods_id)))
293
+ mapping.set(String(meta.goods_id), meta);
294
+ }
295
+ const priceOf = (value, key) => {
296
+ if (value && typeof value === 'object') {
297
+ const target = value[key] ?? (key === 'real' ? value.origin : value.real);
298
+ const num = target && typeof target === 'object' ? Number(target.amount) : Number(target);
299
+ return Number.isFinite(num) && num > 0 ? num : 0;
300
+ }
301
+ const num = Number(value);
302
+ return Number.isFinite(num) && num > 0 ? num : 0;
303
+ };
304
+ // 轮次判定:带 next_refresh_time 的随轮次刷新,缺省视为全天(round 0)。
305
+ const roundOf = (raw) => {
306
+ const refreshAt = (0, merchant_service_1.merchantTimestampMs)(raw?.next_refresh_time);
307
+ if (!refreshAt)
308
+ return 0;
309
+ return currentRound;
310
+ };
311
+ const grouped = new Map();
312
+ for (const raw of res.goods) {
313
+ if (!raw || typeof raw !== 'object')
314
+ continue;
315
+ const goodsId = raw.goods_id ?? raw.id;
316
+ const meta = mapping.get(String(goodsId)) || {};
317
+ const round = roundOf(raw);
318
+ const list = grouped.get(round) || [];
319
+ list.push({
320
+ goods_id: goodsId,
321
+ goods_name: meta.goods_name || raw.goods_name || raw.name,
322
+ item_id: meta.item_id,
323
+ item_num: meta.item_num,
324
+ buy_limit_num: raw.limit_buy_num,
325
+ price: priceOf(raw.price, 'real'),
326
+ origin_price: priceOf(raw.price, 'origin'),
327
+ });
328
+ grouped.set(round, list);
329
+ }
330
+ for (const [round, list] of grouped) {
331
+ priceLog.record(list, { now, round });
332
+ }
333
+ }
334
+ // 今日远行商人:历史轮次商品在实时接口拿不到价格(旧接口 random_goods 恒为 0),
335
+ // 用当天记录下来的实时价格补上。
336
+ function backfillTodayPrices(priceLog, products, now = new Date()) {
337
+ if (!priceLog)
338
+ return products;
339
+ for (const product of products) {
340
+ if (product.price && product.price > 0)
341
+ continue;
342
+ const round = product.round_id ?? getRoundForItem({ start_time: product.start_time, end_time: product.end_time }, now) ?? 0;
343
+ const found = priceLog.lookup(product.goods_id, product.name, round);
344
+ if (!found)
345
+ continue;
346
+ product.price = found.price;
347
+ if (product.limit === null || product.limit === undefined)
348
+ product.limit = found.buy_limit_num || null;
349
+ }
350
+ return products;
351
+ }
352
+ function buildMerchantRenderParams(pageHeight) {
353
+ return {
354
+ pageHeight,
355
+ renderWidth: MERCHANT_CARD_RENDER_WIDTH,
356
+ renderHeight: Math.ceil(pageHeight * MERCHANT_CARD_OUTPUT_SCALE),
357
+ renderBaseWidth: MERCHANT_CARD_RENDER_BASE_WIDTH,
358
+ renderBaseHeight: pageHeight * MERCHANT_CARD_LAYOUT_SCALE,
359
+ renderScale: MERCHANT_CARD_RENDER_SCALE,
360
+ renderZoom: MERCHANT_CARD_RENDER_ZOOM,
361
+ renderDeviceScaleFactor: MERCHANT_CARD_DEVICE_SCALE_FACTOR,
362
+ renderStroke11: MERCHANT_CARD_STROKE_11,
363
+ renderStroke10: MERCHANT_CARD_STROKE_10,
364
+ renderStrokeName: MERCHANT_TODAY_NAME_STROKE,
365
+ renderOffset3: MERCHANT_CARD_OFFSET_3,
366
+ };
367
+ }
368
+ /** 把图标换算成模板用的 transform;没有测量结果就原样渲染。 */
369
+ function buildMerchantIconStyle(iconUrl, iconMetrics, box = MERCHANT_CARD_ICON_BOX) {
370
+ const fallback = { iconScale: 1, iconOffsetX: 0, iconOffsetY: 0 };
371
+ const url = String(iconUrl || '').trim();
372
+ if (!url || !iconMetrics)
373
+ return fallback;
374
+ const fit = (0, icon_metrics_1.buildIconFit)(iconMetrics.get(url), box);
375
+ return fit
376
+ ? { iconScale: fit.scale, iconOffsetX: fit.offsetX, iconOffsetY: fit.offsetY }
377
+ : fallback;
378
+ }
379
+ async function measureMerchantIcon(deps, url) {
380
+ const cacheKey = String(url || '').trim().split('?')[0];
381
+ if (!cacheKey)
382
+ return null;
383
+ if (iconMetricsCache.has(cacheKey))
384
+ return iconMetricsCache.get(cacheKey) ?? null;
385
+ let metrics = null;
386
+ try {
387
+ if (!deps.ctx?.http)
388
+ throw new Error('http service unavailable');
389
+ const response = await deps.ctx.http('GET', url, {
390
+ responseType: 'arraybuffer',
391
+ timeout: MERCHANT_ICON_FETCH_TIMEOUT_MS,
392
+ });
393
+ const status = Number(response?.status) || 0;
394
+ if (status >= 400)
395
+ throw new Error(`HTTP ${status}`);
396
+ const body = response?.data;
397
+ metrics = (0, icon_metrics_1.readImageMetrics)(Buffer.isBuffer(body) ? body : Buffer.from(body || []));
398
+ }
399
+ catch (error) {
400
+ logger.warn(`读取商品图标失败(${cacheKey}):${error?.message || error}`);
401
+ }
402
+ iconMetricsCache.set(cacheKey, metrics);
403
+ return metrics;
404
+ }
405
+ /** 批量量图标,返回 Map<icon_url, metrics>;个别失败只是缺那一条。 */
406
+ async function collectMerchantIconMetrics(deps, urls) {
407
+ const unique = [...new Set([...urls].map(url => String(url || '').trim()).filter(Boolean))];
408
+ const metrics = new Map();
409
+ await Promise.all(unique.map(async (url) => {
410
+ const measured = await measureMerchantIcon(deps, url);
411
+ if (measured)
412
+ metrics.set(url, measured);
413
+ }));
414
+ return metrics;
415
+ }
416
+ /** 收集商人响应里出现过的图标地址(去重);图标只挂在旧接口的 get_props / get_extra_props / get_pets 上。 */
417
+ function collectMerchantIconUrls(res) {
418
+ let root = res || {};
419
+ if (root.result && typeof root.result === 'object')
420
+ root = root.result;
421
+ const merchantActivities = Array.isArray(root.merchantActivities)
422
+ ? root.merchantActivities
423
+ : Array.isArray(root.merchant_activities) ? root.merchant_activities : [];
424
+ const urls = new Set();
425
+ for (const activity of merchantActivities) {
426
+ for (const key of ['get_props', 'get_extra_props', 'get_pets']) {
427
+ for (const item of Array.isArray(activity?.[key]) ? activity[key] : []) {
428
+ const url = String(item?.icon_url || '').trim();
429
+ if (url)
430
+ urls.add(url);
431
+ }
432
+ }
433
+ }
434
+ return [...urls];
435
+ }
436
+ function buildMerchantGoodsAll(products, options) {
222
437
  const catOrder = { round: 0, normal: 1, weekend: 2 };
223
- const startY = 592;
224
- const cardHeight = 308;
225
- const gap = 43;
226
438
  const goodsAll = products.map((product) => {
227
439
  const limit = product.limit;
228
440
  const limitText = limit === undefined || limit === null ? '--' : String(limit);
@@ -254,15 +466,65 @@ function buildMerchantCardItems(products, options) {
254
466
  return catOrder[a.category] - catOrder[b.category];
255
467
  return Number(b.price || 0) - Number(a.price || 0);
256
468
  });
469
+ return goodsAll;
470
+ }
471
+ function buildMerchantCardItems(products, options) {
472
+ const startY = 592;
473
+ const cardHeight = 308;
474
+ const gap = 43;
475
+ const goodsAll = buildMerchantGoodsAll(products, options);
257
476
  const goods = goodsAll.map((item, index) => ({
258
477
  ...item,
478
+ ...buildMerchantIconStyle(item.iconUrl, options.iconMetrics),
259
479
  num: String(index + 1).padStart(2, '0'),
260
480
  top: startY + index * (cardHeight + gap),
261
481
  }));
262
482
  const lastCardTop = goods.length > 0 ? goods[goods.length - 1].top : startY;
263
483
  const bottomFrameTop = lastCardTop + 287;
264
484
  const pageHeight = bottomFrameTop + 160;
265
- return { goods, bottomFrameTop, pageHeight };
485
+ return { goods, bottomFrameTop, ...buildMerchantRenderParams(pageHeight) };
486
+ }
487
+ /**
488
+ * 今日卡片按轮次分行:热销(roundId 0,全天/跨天在架)置顶,第 1~4 轮升序。
489
+ * 每件商品落在哪一轮由商品数据的 roundId 事先算好。
490
+ */
491
+ function buildTodayRows(goods, currentRoundId, iconMetrics) {
492
+ const grouped = new Map();
493
+ for (const item of goods) {
494
+ const roundId = Number(item.roundId) || 0;
495
+ if (!grouped.has(roundId))
496
+ grouped.set(roundId, []);
497
+ grouped.get(roundId).push(item);
498
+ }
499
+ const roundIds = [...grouped.keys()].sort((a, b) => {
500
+ if (a === 0)
501
+ return -1;
502
+ if (b === 0)
503
+ return 1;
504
+ return a - b;
505
+ });
506
+ const available = MERCHANT_TODAY_ROW_WIDTH - MERCHANT_TODAY_TAB_WIDTH;
507
+ return roundIds.map((roundId, index) => {
508
+ const win = ROUND_WINDOWS.find(item => item.id === roundId);
509
+ const list = grouped.get(roundId);
510
+ const cellWidth = Math.min(MERCHANT_TODAY_CELL_WIDTH, available / Math.max(list.length, 1));
511
+ const iconSize = Math.min(MERCHANT_TODAY_ICON_SIZE, cellWidth - 4);
512
+ const iconBox = {
513
+ width: iconSize,
514
+ height: iconSize,
515
+ targetArea: MERCHANT_TODAY_ICON_TARGET_AREA * ((iconSize / MERCHANT_TODAY_ICON_SIZE) ** 2),
516
+ };
517
+ return {
518
+ roundId,
519
+ label: win ? `第${roundId}轮` : '热销',
520
+ timeLabel: win ? `${padNumber(win.startHour)}-${padNumber(win.endHour)}` : '全天',
521
+ isCurrent: Boolean(currentRoundId) && roundId === currentRoundId,
522
+ top: MERCHANT_TODAY_START_Y + (index * (MERCHANT_TODAY_ROW_HEIGHT + MERCHANT_TODAY_ROW_GAP)),
523
+ cellWidth,
524
+ iconSize,
525
+ items: list.map(item => ({ ...item, ...buildMerchantIconStyle(item.iconUrl, iconMetrics, iconBox) })),
526
+ };
527
+ });
266
528
  }
267
529
  function getCurrentMerchantProducts(products, roundInfo) {
268
530
  return products.filter((product) => {
@@ -337,8 +599,20 @@ function getMerchantDateStr(now = new Date()) {
337
599
  const parts = getChinaParts(now);
338
600
  return `${parts.month}.${parts.day}`;
339
601
  }
340
- function buildMerchantRenderPayload(res, now = new Date()) {
341
- const allProducts = normalizeMerchantProducts(res, now);
602
+ // 商品缺图标时按 item_id/item_kind 拼 Wiki 物品图,仍缺失时用本地 logo 兜底。
603
+ function merchantViewOptions(deps) {
604
+ const secondsOfDay = (ms) => {
605
+ const parts = getChinaParts(ms);
606
+ return parts.hour * 3600 + parts.minute * 60 + parts.second;
607
+ };
608
+ return {
609
+ secondsOfDay,
610
+ assetBaseUrl: deps.client?.wikiAssetBaseUrl,
611
+ fallbackIcon: deps.renderer?.resourceUrl?.('img/logo.cVSpb3sL.png') || '',
612
+ };
613
+ }
614
+ function buildMerchantRenderPayload(res, now = new Date(), options, iconMetrics) {
615
+ const allProducts = normalizeMerchantProducts(res, now, options);
342
616
  const roundInfo = getCurrentMerchantRound(now);
343
617
  const products = getCurrentMerchantProducts(allProducts, roundInfo);
344
618
  const currentWindow = ROUND_WINDOWS.find(win => win.id === roundInfo.current);
@@ -348,13 +622,13 @@ function buildMerchantRenderPayload(res, now = new Date()) {
348
622
  const data = {
349
623
  dateStr: getMerchantDateStr(now),
350
624
  timeRange,
351
- ...buildMerchantCardItems(products, { includeEnded: false }),
625
+ ...buildMerchantCardItems(products, { includeEnded: false, iconMetrics }),
352
626
  };
353
627
  const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
354
628
  return { products, roundInfo, data, fallback };
355
629
  }
356
- function buildLegacyMerchantRenderPayload(res, now = new Date()) {
357
- const allProducts = normalizeMerchantProducts(res, now);
630
+ function buildLegacyMerchantRenderPayload(res, now = new Date(), options) {
631
+ const allProducts = normalizeMerchantProducts(res, now, options);
358
632
  const roundInfo = getCurrentMerchantRound(now);
359
633
  const products = getCurrentMerchantProducts(allProducts, roundInfo);
360
634
  const roundGroups = getLegacyRoundGroups(now, roundInfo);
@@ -372,31 +646,48 @@ function buildLegacyMerchantRenderPayload(res, now = new Date()) {
372
646
  const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
373
647
  return { products, roundInfo, data, fallback };
374
648
  }
375
- function buildTodayMerchantRenderPayload(res, now = new Date()) {
376
- const products = normalizeMerchantProducts(res, now)
649
+ function buildTodayMerchantRenderPayload(res, now = new Date(), options, priceLog, iconMetrics) {
650
+ const products = backfillTodayPrices(priceLog, normalizeMerchantProducts(res, now, options)
377
651
  .filter(product => {
378
652
  const source = {
379
653
  start_time: product.start_time,
380
654
  end_time: product.end_time,
381
655
  };
382
656
  return isMerchantItemToday(source, now);
383
- });
657
+ }), now);
658
+ const roundInfo = getCurrentMerchantRound(now);
659
+ const goodsAll = buildMerchantGoodsAll(products, { includeEnded: true });
660
+ const rows = buildTodayRows(goodsAll, roundInfo.current, iconMetrics);
661
+ // 扁平 goods 仍按行序编号,方便日志与订阅侧复用
662
+ const goods = [];
663
+ for (const row of rows) {
664
+ for (const item of row.items) {
665
+ goods.push({ ...item, num: String(goods.length + 1).padStart(2, '0') });
666
+ }
667
+ }
668
+ const lastRowTop = rows.length > 0 ? rows[rows.length - 1].top : MERCHANT_TODAY_START_Y;
669
+ const bottomFrameTop = (lastRowTop + MERCHANT_TODAY_ROW_HEIGHT) - MERCHANT_TODAY_FRAME_OVERLAP;
670
+ const pageHeight = bottomFrameTop + 160;
384
671
  const data = {
385
672
  dateStr: getMerchantDateStr(now),
386
- ...buildMerchantCardItems(products, { includeEnded: true }),
673
+ goods,
674
+ rows,
675
+ startTop: MERCHANT_TODAY_START_Y,
676
+ bottomFrameTop,
677
+ ...buildMerchantRenderParams(pageHeight),
387
678
  };
388
679
  const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, products);
389
680
  return { products, data, fallback };
390
681
  }
391
- function buildLegacyTodayMerchantRenderPayload(res, now = new Date()) {
392
- const allProducts = normalizeMerchantProducts(res, now)
682
+ function buildLegacyTodayMerchantRenderPayload(res, now = new Date(), options, priceLog) {
683
+ const allProducts = backfillTodayPrices(priceLog, normalizeMerchantProducts(res, now, options)
393
684
  .filter(product => {
394
685
  const source = {
395
686
  start_time: product.start_time,
396
687
  end_time: product.end_time,
397
688
  };
398
689
  return isMerchantItemToday(source, now);
399
- });
690
+ }), now);
400
691
  const roundInfo = getCurrentMerchantRound(now);
401
692
  const roundGroups = getLegacyRoundGroups(now, roundInfo);
402
693
  for (const product of allProducts) {
@@ -422,28 +713,54 @@ function buildLegacyTodayMerchantRenderPayload(res, now = new Date()) {
422
713
  function useLegacyMerchantUi(deps) {
423
714
  return deps.config.merchantUiStyle === 'old';
424
715
  }
425
- function buildConfiguredMerchantRenderPayload(deps, res, now = new Date()) {
426
- return clockScope.run(new merchant_clock_1.MerchantClock(deps.config.merchantTimezone || 'Asia/Shanghai'), () => {
427
- const payload = useLegacyMerchantUi(deps)
428
- ? buildLegacyMerchantRenderPayload(res, now)
429
- : buildMerchantRenderPayload(res, now);
716
+ async function buildConfiguredMerchantRenderPayload(deps, res, now = new Date()) {
717
+ const options = merchantViewOptions(deps);
718
+ return clockScope.run(new merchant_clock_1.MerchantClock(deps.config.merchantTimezone || 'Asia/Shanghai'), async () => {
719
+ if (useLegacyMerchantUi(deps)) {
720
+ return {
721
+ ...buildLegacyMerchantRenderPayload(res, now, options),
722
+ templateName: 'yuanxing-shangren',
723
+ };
724
+ }
725
+ const iconMetrics = await collectMerchantIconMetrics(deps, collectMerchantIconUrls(res));
430
726
  return {
431
- ...payload,
432
- templateName: useLegacyMerchantUi(deps) ? 'yuanxing-shangren' : 'yuanxing-shangren/merchant',
727
+ ...buildMerchantRenderPayload(res, now, options, iconMetrics),
728
+ templateName: 'yuanxing-shangren/merchant',
433
729
  };
434
730
  });
435
731
  }
436
- function buildConfiguredTodayMerchantRenderPayload(deps, res, now = new Date()) {
437
- return clockScope.run(new merchant_clock_1.MerchantClock(deps.config.merchantTimezone || 'Asia/Shanghai'), () => {
438
- const payload = useLegacyMerchantUi(deps)
439
- ? buildLegacyTodayMerchantRenderPayload(res, now)
440
- : buildTodayMerchantRenderPayload(res, now);
732
+ async function buildConfiguredTodayMerchantRenderPayload(deps, res, now = new Date()) {
733
+ const options = merchantViewOptions(deps);
734
+ return clockScope.run(new merchant_clock_1.MerchantClock(deps.config.merchantTimezone || 'Asia/Shanghai'), async () => {
735
+ if (useLegacyMerchantUi(deps)) {
736
+ return {
737
+ ...buildLegacyTodayMerchantRenderPayload(res, now, options, deps.merchantPriceLog),
738
+ templateName: 'yuanxing-shangren',
739
+ };
740
+ }
741
+ const iconMetrics = await collectMerchantIconMetrics(deps, collectMerchantIconUrls(res));
441
742
  return {
442
- ...payload,
443
- templateName: useLegacyMerchantUi(deps) ? 'yuanxing-shangren' : 'yuanxing-shangren/today',
743
+ ...buildTodayMerchantRenderPayload(res, now, options, deps.merchantPriceLog, iconMetrics),
744
+ templateName: 'yuanxing-shangren/today',
444
745
  };
445
746
  });
446
747
  }
748
+ // 读取订阅当日已推送的全天商品名集合(跨天自动失效)。
749
+ function getAlldayPushedNames(sub, todayKey) {
750
+ const state = sub.allday_pushed;
751
+ if (!state || state.date !== todayKey)
752
+ return new Set();
753
+ return new Set((state.names || []).map(String));
754
+ }
755
+ // 把本次推送命中的全天商品记入当日已推送状态。
756
+ function recordAlldayPushed(sub, todayKey, names) {
757
+ const existing = sub.allday_pushed?.date === todayKey ? [...(sub.allday_pushed.names || [])] : [];
758
+ for (const name of names) {
759
+ if (!existing.includes(name))
760
+ existing.push(name);
761
+ }
762
+ return existing.length ? { date: todayKey, names: existing } : undefined;
763
+ }
447
764
  async function checkMerchantSubscriptions(deps, signal) {
448
765
  const { ctx, client, merchantSubMgr, renderer, config } = deps;
449
766
  const subs = merchantSubMgr.getAll();
@@ -454,12 +771,20 @@ async function checkMerchantSubscriptions(deps, signal) {
454
771
  const res = await client.getMerchantInfo(ctx, true, { signal });
455
772
  if (signal?.aborted)
456
773
  return { subscriptions: 0, matched: 0, pushed: 0, kind: 'cancelled' };
457
- if (!res)
774
+ // 接口失败视为空结果等待稍后重试,不再发送空商品图片。
775
+ if (!res) {
776
+ logger.warn(`远行商人订阅查询失败:${client.getLastErrorBrief?.() || '接口无返回'},等待重试`);
458
777
  return { subscriptions: Object.keys(subs).length, matched: 0, pushed: 0, kind: 'error' };
459
- const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
778
+ }
779
+ recordLiveMerchantPrices(deps, res);
780
+ const { products, roundInfo, data, fallback, templateName } = await buildConfiguredMerchantRenderPayload(deps, res);
460
781
  if (!products.length)
461
782
  return { subscriptions: Object.keys(subs).length, matched: 0, pushed: 0, kind: 'empty' };
462
783
  const productNames = products.map((p) => p.name || '').filter(Boolean);
784
+ // 开启后全天商品每个订阅每天只推送一次;仅作用于按商品名订阅的推送,
785
+ // “全部商品”订阅仍是每轮推送整张商人图。
786
+ const alldayNames = config.merchantAllDayOnceEnabled ? (0, merchant_service_1.collectAllDayProductNames)(products) : new Set();
787
+ const todayKey = new merchant_clock_1.MerchantClock(config.merchantTimezone || 'Asia/Shanghai').date();
463
788
  const rendered = await renderer.renderHtml(ctx, templateName, data, { signal });
464
789
  const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
465
790
  let matchedCount = 0;
@@ -470,13 +795,29 @@ async function checkMerchantSubscriptions(deps, signal) {
470
795
  if (JSON.stringify(merchantSubMgr.getAll()[key]) !== JSON.stringify(sub))
471
796
  continue;
472
797
  const matchAll = !!sub.match_all;
473
- const matched = matchAll
798
+ let matched = matchAll
474
799
  ? productNames
475
800
  : sub.items.filter((item) => productNames.some(n => n.includes(item)));
476
801
  if (!matchAll && !matched.length)
477
802
  continue;
478
803
  if (matchAll && !products.length)
479
804
  continue;
805
+ // 当天已推送过的全天商品不再计入本轮命中,全部被剔除则本轮不推送。
806
+ // 订阅词条按子串匹配商品名,只有词条命中的商品全部为全天时才按全天去重。
807
+ const onlyAlldayItem = (item) => {
808
+ if (!alldayNames.size)
809
+ return false;
810
+ const hits = productNames.filter(n => n.includes(item));
811
+ return hits.length > 0 && hits.every(n => alldayNames.has(n));
812
+ };
813
+ let pushItems = matched;
814
+ if (!matchAll && alldayNames.size) {
815
+ const pushedToday = getAlldayPushedNames(sub, todayKey);
816
+ if (pushedToday.size)
817
+ pushItems = matched.filter(item => !(onlyAlldayItem(item) && pushedToday.has(item)));
818
+ }
819
+ if (!matchAll && !pushItems.length)
820
+ continue;
480
821
  matchedCount++;
481
822
  if (matchAll) {
482
823
  if (sub.last_push_round === roundInfo.round_id)
@@ -488,7 +829,7 @@ async function checkMerchantSubscriptions(deps, signal) {
488
829
  }
489
830
  const msg = matchAll
490
831
  ? `\ud83d\udd14 \u8fdc\u884c\u5546\u4eba\u5237\u65b0\u63d0\u9192\n\u5f53\u524d\u5546\u54c1\uff1a${productNames.join('\u3001')}`
491
- : `\ud83d\udd14 \u8fdc\u884c\u5546\u4eba\u5237\u65b0\u63d0\u9192\n\u5f53\u524d\u5546\u54c1\uff1a${productNames.join('\u3001')}\n\u5339\u914d\u8ba2\u9605\uff1a${matched.join('\u3001')}`;
832
+ : `\ud83d\udd14 \u8fdc\u884c\u5546\u4eba\u5237\u65b0\u63d0\u9192\n\u5f53\u524d\u5546\u54c1\uff1a${productNames.join('\u3001')}\n\u5339\u914d\u8ba2\u9605\uff1a${pushItems.join('\u3001')}`;
492
833
  const platform = sub.platform || ctx.bots[0]?.platform;
493
834
  const channelId = sub.channel_id || sub.group_id || sub.user_id || key;
494
835
  if (!platform || !channelId) {
@@ -510,6 +851,9 @@ async function checkMerchantSubscriptions(deps, signal) {
510
851
  ...sub,
511
852
  last_push_round: roundInfo.round_id,
512
853
  last_matched_items: matched,
854
+ allday_pushed: matchAll || !alldayNames.size
855
+ ? sub.allday_pushed
856
+ : recordAlldayPushed(sub, todayKey, pushItems.filter(onlyAlldayItem)),
513
857
  });
514
858
  }
515
859
  return { subscriptions: Object.keys(subs).length, matched: matchedCount, pushed: pushedCount, kind: 'success' };
@@ -525,7 +869,8 @@ function register(deps) {
525
869
  const res = await client.getMerchantInfo(ctx, true);
526
870
  if (!res)
527
871
  return `\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\uff1a${client.getLastErrorBrief()}`;
528
- const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
872
+ recordLiveMerchantPrices(deps, res);
873
+ const { data, fallback, templateName } = await buildConfiguredMerchantRenderPayload(deps, res);
529
874
  const png = await deps.renderer.renderHtml(ctx, templateName, data);
530
875
  await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
531
876
  });
@@ -535,10 +880,29 @@ function register(deps) {
535
880
  const res = await client.getMerchantInfo(ctx, true);
536
881
  if (!res)
537
882
  return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
538
- const { data, fallback, templateName } = buildConfiguredTodayMerchantRenderPayload(deps, res);
883
+ recordLiveMerchantPrices(deps, res);
884
+ const { data, fallback, templateName } = await buildConfiguredTodayMerchantRenderPayload(deps, res);
539
885
  const png = await deps.renderer.renderHtml(ctx, templateName, data);
540
886
  await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren:today', deps.config);
541
887
  });
888
+ // 查看商品目录与订阅序号。
889
+ ctx.command('商人商品', '查看远行商人商品列表与订阅序号')
890
+ .alias('洛克.商人商品')
891
+ .alias('洛克商人商品')
892
+ .userFields(['authority'])
893
+ .action(async () => {
894
+ const catalog = await deps.merchantCatalog.getCatalog();
895
+ if (!catalog.length)
896
+ return '暂时取不到远行商人商品列表,请稍后再试。';
897
+ const lines = [
898
+ `远行商人商品列表(共 ${catalog.length} 件)`,
899
+ `订阅写法:${TEXT.subscribe} 1 3 5,也可以用商品名`,
900
+ '',
901
+ ];
902
+ for (const item of catalog)
903
+ lines.push(`${item.index}. ${item.name}`);
904
+ return lines.join('\n');
905
+ });
542
906
  ctx.command(`${TEXT.subscribe} [args:text]`, '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\u63d0\u9192')
543
907
  .userFields(['authority'])
544
908
  .action(async ({ session }, args) => {
@@ -549,6 +913,42 @@ function register(deps) {
549
913
  return '个人私聊订阅功能已被禁用,请联系机器人管理员。';
550
914
  const parsed = parseMerchantSubscriptionArgs(args, config.merchantSubscriptionItems);
551
915
  const existing = merchantSubMgr.get(target.key);
916
+ // 序号 / 商品名 / 全部 都经商品目录解析,序号命中后换成规范名落盘。
917
+ const catalog = await deps.merchantCatalog.getCatalog();
918
+ const resolved = (0, merchant_catalog_1.resolveMerchantSelection)(parsed.tokens, catalog);
919
+ const matchAll = parsed.match_all || resolved.all;
920
+ const hasInput = parsed.match_all || parsed.tokens.length > 0;
921
+ if (hasInput && !matchAll && !resolved.names.length) {
922
+ return [
923
+ `没有识别到可订阅的商品:${resolved.unknownIndexes.join('、') || parsed.tokens.join('、')}`,
924
+ `先用 商人商品 查看商品序号,再按序号或名称订阅。`,
925
+ ].join('\n');
926
+ }
927
+ // 不带商品参数重复订阅时,沿用上次配置(全部模式或商品列表),
928
+ // 而不是悄悄重置成配置默认商品。
929
+ let sourceHint;
930
+ let items;
931
+ if (matchAll) {
932
+ items = [];
933
+ sourceHint = '全部商品(每轮推送)';
934
+ }
935
+ else if (resolved.names.length) {
936
+ items = resolved.names;
937
+ sourceHint = '本次指定';
938
+ }
939
+ else if (existing && (existing.match_all || (existing.items || []).length > 0)) {
940
+ items = existing.items || [];
941
+ sourceHint = '沿用上次配置';
942
+ }
943
+ else {
944
+ items = config.merchantSubscriptionItems;
945
+ sourceHint = '默认商品配置';
946
+ }
947
+ const matchAllFinal = matchAll || (resolved.names.length === 0 && !hasInput && existing?.match_all === true);
948
+ // 未显式指定 1/0 或 @全体 时沿用上次设置。
949
+ const mentionAll = target.privateChat
950
+ ? false
951
+ : (parsed.mentionAll === null ? (existing?.mention_all ?? false) : parsed.mentionAll);
552
952
  merchantSubMgr.upsert(target.key, {
553
953
  group_id: session.guildId || '',
554
954
  user_id: target.privateChat ? session.userId : '',
@@ -556,14 +956,32 @@ function register(deps) {
556
956
  channel_id: target.channelId,
557
957
  platform: target.platform,
558
958
  self_id: session.selfId || session.bot?.selfId || '',
559
- items: parsed.items,
560
- match_all: parsed.match_all,
561
- mention_all: target.privateChat ? false : parsed.mention_all,
959
+ items,
960
+ match_all: matchAllFinal,
961
+ mention_all: mentionAll,
562
962
  last_push_round: existing?.last_push_round ?? null,
563
963
  last_matched_items: existing?.last_matched_items ?? [],
964
+ allday_pushed: existing?.allday_pushed,
564
965
  updated_by: session.userId,
565
966
  });
566
- return `\u2705 \u5df2\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\uff1a${parsed.match_all ? '\u5168\u90e8\u5546\u54c1\uff08\u6bcf\u8f6e\u63a8\u9001\uff09' : parsed.items.join('\u3001')}\uff08${parsed.source}\uff09\uff1b${target.privateChat ? '个人订阅' : (parsed.mention_all ? '\u547d\u4e2d\u540e\u4f1a @\u5168\u4f53' : '\u547d\u4e2d\u540e\u4e0d @\u5168\u4f53')}`;
967
+ const lines = [
968
+ `\u2705 \u5df2\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\uff08${target.privateChat ? '个人订阅' : '本群订阅'}):${matchAllFinal ? '全部商品(每轮推送整张商人图)' : `监听 ${items.length} 件商品`}`,
969
+ ];
970
+ if (!matchAllFinal)
971
+ lines.push(`监听商品:${(0, merchant_catalog_1.describeMerchantItems)(items, catalog)}`);
972
+ lines.push(`商品来源:${sourceHint}。`);
973
+ lines.push(mentionAll ? '命中后会尝试 @全体。' : '命中后不会 @全体。');
974
+ if (resolved.unknownIndexes.length)
975
+ lines.push(`未找到的序号:${resolved.unknownIndexes.join('、')}(已忽略)。`);
976
+ if (resolved.unrecognizedNames.length)
977
+ lines.push(`商品目录里没有:${resolved.unrecognizedNames.join('、')}(已按原名订阅)。`);
978
+ lines.push('');
979
+ lines.push('可用格式:');
980
+ lines.push(`${TEXT.subscribe} 1 3 国王球 按序号或名称订阅`);
981
+ lines.push(`${TEXT.subscribe} 全部 订阅全部商品,每轮推送整张商人图`);
982
+ lines.push(`${TEXT.subscribe} @全体 1 3 命中后尝试 @全体`);
983
+ lines.push(`商人商品 查看商品序号`);
984
+ return lines.join('\n');
567
985
  });
568
986
  ctx.command(TEXT.viewSubscribe, '\u67e5\u770b\u5f53\u524d\u4f1a\u8bdd\u7684\u8fdc\u884c\u5546\u4eba\u8ba2\u9605')
569
987
  .userFields(['authority'])
@@ -575,7 +993,8 @@ function register(deps) {
575
993
  const scopeName = target.privateChat ? '你' : '当前群组';
576
994
  if (!sub)
577
995
  return `${scopeName}未订阅远行商人。\n用法:${TEXT.subscribe} [1/0] [商品名1] [商品名2] ...\n或:${TEXT.subscribe} 全部(每轮直接推送整张商人图)`;
578
- const itemsText = sub.match_all ? '全部商品(每轮推送)' : sub.items.join('、');
996
+ const catalog = await deps.merchantCatalog.getCatalog();
997
+ const itemsText = sub.match_all ? '全部商品(每轮推送)' : (0, merchant_catalog_1.describeMerchantItems)(sub.items || [], catalog) || (sub.items || []).join('、');
579
998
  return `${scopeName}订阅商品:${itemsText}\n提醒方式:${target.privateChat ? '私聊提醒' : (sub.mention_all ? '@全体' : '普通提醒')}`;
580
999
  });
581
1000
  ctx.command(TEXT.unsubscribe, '\u53d6\u6d88\u8fdc\u884c\u5546\u4eba\u8ba2\u9605')
@@ -610,7 +1029,8 @@ function register(deps) {
610
1029
  if (!Object.keys(merchantSubMgr.getAll()).length)
611
1030
  return;
612
1031
  const result = await checkMerchantSubscriptions(deps, signal);
613
- if (result.kind !== 'empty' || retry === 3)
1032
+ // 接口失败与空结果一样等待稍后重试,避免直接放弃本轮推送。
1033
+ if ((result.kind !== 'empty' && result.kind !== 'error') || retry === 3)
614
1034
  return;
615
1035
  await (0, async_control_1.delay)(240000 + Math.floor(Math.random() * 60001) - 30000, signal);
616
1036
  }