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.
- package/lib/announcement-service.d.ts +21 -0
- package/lib/announcement-service.js +63 -0
- package/lib/async-control.d.ts +3 -0
- package/lib/async-control.js +48 -0
- package/lib/atlas-service.js +1 -1
- package/lib/client.d.ts +58 -7
- package/lib/client.js +282 -127
- package/lib/commands/egg.js +66 -4
- package/lib/commands/merchant.d.ts +105 -0
- package/lib/commands/merchant.js +600 -182
- package/lib/commands/query.d.ts +18 -0
- package/lib/commands/query.js +585 -145
- package/lib/commands/ranking.d.ts +2 -0
- package/lib/commands/ranking.js +31 -0
- package/lib/commands/share-code.d.ts +2 -0
- package/lib/commands/share-code.js +111 -0
- package/lib/commands/tools.js +48 -60
- package/lib/commands/wiki.js +1 -1
- package/lib/egg-service.d.ts +82 -8
- package/lib/egg-service.js +235 -19
- package/lib/element-colors.d.ts +3 -0
- package/lib/element-colors.js +34 -0
- package/lib/icon-metrics.d.ts +32 -0
- package/lib/icon-metrics.js +255 -0
- package/lib/img//345/274/202/350/211/262.png +0 -0
- package/lib/img//347/202/253/345/275/251.png +0 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +11857 -175
- package/lib/merchant-catalog.d.ts +47 -0
- package/lib/merchant-catalog.js +193 -0
- package/lib/merchant-clock.d.ts +22 -0
- package/lib/merchant-clock.js +95 -0
- package/lib/merchant-price-log.d.ts +63 -0
- package/lib/merchant-price-log.js +187 -0
- package/lib/merchant-service.d.ts +38 -0
- package/lib/merchant-service.js +216 -0
- package/lib/permissions.d.ts +16 -0
- package/lib/permissions.js +66 -0
- package/lib/pet-data-service.d.ts +13 -0
- package/lib/pet-data-service.js +117 -10
- package/lib/pet-size.d.ts +13 -0
- package/lib/pet-size.js +54 -0
- package/lib/player-service.d.ts +37 -0
- package/lib/player-service.js +201 -0
- package/lib/ranking-service.d.ts +46 -0
- package/lib/ranking-service.js +137 -0
- package/lib/render-templates/announcement/detail.html +8 -0
- package/lib/render-templates/announcement/list.html +5 -0
- package/lib/render-templates/announcement/style.css +1 -0
- package/lib/render-templates/home/style.css +15 -14
- package/lib/render-templates/home-pets/index.html +80 -0
- package/lib/render-templates/home-pets/style.css +338 -0
- package/lib/render-templates/pet-detail-classic/index.html +151 -0
- package/lib/render-templates/pet-detail-classic/style.css +503 -0
- package/lib/render-templates/pet-detail-classic/texture2D/a_title.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/bg.jpg +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/footer.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/icon.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/img_head.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/jinhua_bg.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/level_icon.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/line.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/mask_bar.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/pet_bg.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/pet_rocom.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/pet_rocom_bg.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/poro_bar.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/right_jinhua.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/skill_bg.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/skill_mask.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/star.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/star_1.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/star_8.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/star_9.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/table.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/tags.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/title.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/top_bg.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/xuancai_overlay.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/yise_overlay.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D/ys_bar.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/10.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/11.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/12.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/13.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/14.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/15.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/16.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/17.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/18.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/19.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/2.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/20.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/23.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/3.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/4.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/5.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/6.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/8.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247/9.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/205/211.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/206/260.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/234/260.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/271/273.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//345/271/275.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/201/266.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/227/240.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/231/256/351/200/232.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/234/272/346/242/260.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/255/246.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/257/222.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//346/260/264.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/201/253.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/224/265.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//347/277/274.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/215/211.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/220/214.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//350/231/253.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//345/261/236/346/200/247//351/276/231.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/1.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/10.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/11.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/12.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/13.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/14.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/15.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/16.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/17.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/18.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/19.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/2.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/21.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/23.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/24.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/3.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/4.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/5.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/6.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/7.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/8.png +0 -0
- package/lib/render-templates/pet-detail-classic/texture2D//350/241/200/350/204/211/9.png +0 -0
- package/lib/render-templates/pet-ranking/index.html +99 -0
- package/lib/render-templates/pet-ranking/style.css +338 -0
- package/lib/render-templates/player-search/index.html +7 -0
- package/lib/render-templates/searcheggs/index.html +1 -1
- package/lib/render-templates/searcheggs/size.html +70 -68
- package/lib/render-templates/share-code-classic/assets/eit-logo.png +0 -0
- package/lib/render-templates/share-code-classic/assets/img_xingxingdi.png +0 -0
- package/lib/render-templates/share-code-classic/assets/roco_icon.png +0 -0
- package/lib/render-templates/share-code-classic/assets//345/255/227/344/275/223/Rocofontblack-Regular .ttf +0 -0
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- package/lib/render-templates/share-code-classic/assets//347/201/253/346/274/206/345/215/260.png +0 -0
- 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
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/205/211.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/206/260.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/234/260.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/245/207/345/274/202.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/271/273.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//345/271/275.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/201/266.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/231/256/351/200/232.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/234/272/346/242/260.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/255/246.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/257/222.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/260/264.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//346/261/241/346/237/223.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/201/253.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/224/265.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//347/277/274.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/215/211.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/220/214.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//350/231/253.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//351/246/226/351/242/206.png +0 -0
- package/lib/render-templates/share-code-classic/assets//350/241/200/350/204/211//351/276/231.png +0 -0
- package/lib/render-templates/share-code-classic/index.html +119 -0
- package/lib/render-templates/share-code-classic/style.css +646 -0
- package/lib/render-templates/share-code-team/index.html +105 -0
- package/lib/render-templates/share-code-team/style.css +345 -0
- package/lib/render-templates/yuanxing-shangren/merchant.html +479 -461
- package/lib/render-templates/yuanxing-shangren/today.html +407 -461
- package/lib/render.d.ts +18 -2
- package/lib/render.js +138 -39
- package/lib/send-image.d.ts +1 -1
- package/lib/send-image.js +22 -1
- package/lib/share-code-service.d.ts +68 -0
- package/lib/share-code-service.js +257 -0
- package/lib/subscription-runner.d.ts +6 -0
- package/lib/subscription-runner.js +21 -0
- package/lib/subscription-send.d.ts +4 -2
- package/lib/subscription-send.js +45 -20
- package/lib/ttf/rocom_origin.ttf +0 -0
- package/lib/types.d.ts +14 -1
- package/lib/user.d.ts +11 -0
- package/lib/wiki-catalog.js +1 -1
- package/lib/wiki-service.js +1 -1
- package/package.json +5 -1
- package/readme.md +125 -124
- package/lib/ttf/rocom_skill_origin.ttf +0 -0
package/lib/commands/merchant.js
CHANGED
|
@@ -1,9 +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");
|
|
10
|
+
const merchant_service_1 = require("../merchant-service");
|
|
11
|
+
const merchant_catalog_1 = require("../merchant-catalog");
|
|
12
|
+
const merchant_clock_1 = require("../merchant-clock");
|
|
13
|
+
const permissions_1 = require("../permissions");
|
|
14
|
+
const subscription_runner_1 = require("../subscription-runner");
|
|
15
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
16
|
+
const async_control_1 = require("../async-control");
|
|
17
|
+
const icon_metrics_1 = require("../icon-metrics");
|
|
7
18
|
const logger = new koishi_1.Logger('rocom-merchant');
|
|
8
19
|
const TEXT = {
|
|
9
20
|
merchant: '\u8fdc\u884c\u5546\u4eba',
|
|
@@ -28,31 +39,67 @@ const ROUND_WINDOWS = [
|
|
|
28
39
|
{ id: 3, label: '16:00-20:00', startHour: 16, endHour: 20 },
|
|
29
40
|
{ id: 4, label: '20:00-24:00', startHour: 20, endHour: 24 },
|
|
30
41
|
];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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();
|
|
88
|
+
const defaultClock = new merchant_clock_1.MerchantClock('Asia/Shanghai');
|
|
89
|
+
// Scoped to each synchronous view build; plugin instances never overwrite another clock.
|
|
90
|
+
const clockScope = new node_async_hooks_1.AsyncLocalStorage();
|
|
91
|
+
const currentClock = () => clockScope.getStore() || defaultClock;
|
|
42
92
|
function getChinaParts(input = Date.now()) {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
if (item.type !== 'literal')
|
|
47
|
-
parts[item.type] = item.value;
|
|
48
|
-
}
|
|
93
|
+
const ms = input instanceof Date ? input.getTime() : input;
|
|
94
|
+
const parts = currentClock().parts(ms);
|
|
95
|
+
const [year, month, day] = parts.date.split('-').map(Number);
|
|
49
96
|
return {
|
|
50
|
-
year:
|
|
51
|
-
month:
|
|
52
|
-
day:
|
|
53
|
-
hour:
|
|
54
|
-
minute:
|
|
55
|
-
second:
|
|
97
|
+
year: year || 0,
|
|
98
|
+
month: month || 0,
|
|
99
|
+
day: day || 0,
|
|
100
|
+
hour: parts.hour,
|
|
101
|
+
minute: parts.minute,
|
|
102
|
+
second: parts.second,
|
|
56
103
|
};
|
|
57
104
|
}
|
|
58
105
|
function padNumber(value) {
|
|
@@ -62,10 +109,6 @@ function getChinaDateText(input = Date.now()) {
|
|
|
62
109
|
const parts = getChinaParts(input);
|
|
63
110
|
return `${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
64
111
|
}
|
|
65
|
-
function getChinaDayStartMs(input = Date.now()) {
|
|
66
|
-
const parts = getChinaParts(input);
|
|
67
|
-
return new Date(`${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}T00:00:00+08:00`).getTime();
|
|
68
|
-
}
|
|
69
112
|
function classifyMerchantItem(item) {
|
|
70
113
|
const start = normalizeTimestamp(item?.start_time);
|
|
71
114
|
const end = normalizeTimestamp(item?.end_time);
|
|
@@ -115,34 +158,6 @@ function getMerchantActivity(res) {
|
|
|
115
158
|
const activities = res?.merchantActivities || res?.merchant_activities || [];
|
|
116
159
|
return activities[0] || {};
|
|
117
160
|
}
|
|
118
|
-
function getMerchantProducts(res) {
|
|
119
|
-
const activity = getMerchantActivity(res);
|
|
120
|
-
const groups = [];
|
|
121
|
-
if (Array.isArray(activity?.products))
|
|
122
|
-
groups.push(activity.products);
|
|
123
|
-
if (Array.isArray(activity?.product_list))
|
|
124
|
-
groups.push(activity.product_list);
|
|
125
|
-
if (Array.isArray(activity?.get_props))
|
|
126
|
-
groups.push(activity.get_props);
|
|
127
|
-
if (Array.isArray(activity?.get_extra_props))
|
|
128
|
-
groups.push(activity.get_extra_props);
|
|
129
|
-
if (Array.isArray(activity?.get_pets))
|
|
130
|
-
groups.push(activity.get_pets);
|
|
131
|
-
const merged = [];
|
|
132
|
-
const seen = new Set();
|
|
133
|
-
for (const list of groups) {
|
|
134
|
-
for (const item of list) {
|
|
135
|
-
const start = normalizeTimestamp(item?.start_time) ?? 0;
|
|
136
|
-
const end = normalizeTimestamp(item?.end_time) ?? Infinity;
|
|
137
|
-
const key = `${item?.id ?? ''}|${item?.name ?? ''}|${start}|${end === Infinity ? 'inf' : end}`;
|
|
138
|
-
if (seen.has(key))
|
|
139
|
-
continue;
|
|
140
|
-
seen.add(key);
|
|
141
|
-
merged.push(item);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return merged;
|
|
145
|
-
}
|
|
146
161
|
function isMerchantItemActive(item, now = Date.now()) {
|
|
147
162
|
const nowMs = now instanceof Date ? now.getTime() : now;
|
|
148
163
|
const start = normalizeTimestamp(item?.start_time);
|
|
@@ -154,8 +169,7 @@ function isMerchantItemToday(item, now = Date.now()) {
|
|
|
154
169
|
const end = normalizeTimestamp(item?.end_time);
|
|
155
170
|
if (start === null || end === null)
|
|
156
171
|
return true;
|
|
157
|
-
const startOfDay =
|
|
158
|
-
const endOfDay = startOfDay + 24 * 60 * 60 * 1000;
|
|
172
|
+
const { start: startOfDay, end: endOfDay } = currentClock().dayBounds(now instanceof Date ? now.getTime() : now);
|
|
159
173
|
return start < endOfDay && end > startOfDay;
|
|
160
174
|
}
|
|
161
175
|
function getRoundForItem(item, now = Date.now()) {
|
|
@@ -200,81 +214,230 @@ function getCurrentMerchantRound(now = new Date()) {
|
|
|
200
214
|
round_id: `${datePart}-${currentRound || 'closed'}`,
|
|
201
215
|
};
|
|
202
216
|
}
|
|
217
|
+
// @全体 的写法。
|
|
218
|
+
const MENTION_ALL_ALIASES = new Set(['@全体', '全体', '@所有人', 'at全体', 'at所有人']);
|
|
203
219
|
function parseMerchantSubscriptionArgs(args, defaultItems) {
|
|
204
220
|
const parts = (args || '').split(/\s+/).filter(Boolean);
|
|
205
|
-
let mentionAll =
|
|
221
|
+
let mentionAll = null;
|
|
206
222
|
if (parts[0] === '1' || parts[0] === '0') {
|
|
207
223
|
mentionAll = parts.shift() === '1';
|
|
208
224
|
}
|
|
209
|
-
|
|
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]);
|
|
210
233
|
if (matchAll)
|
|
211
234
|
parts.shift();
|
|
212
235
|
const items = matchAll ? [] : (parts.length ? parts : defaultItems);
|
|
213
236
|
const source = matchAll ? '全部商品' : (parts.length ? TEXT.customSource : TEXT.defaultSource);
|
|
214
|
-
return {
|
|
215
|
-
mention_all: mentionAll,
|
|
216
|
-
match_all: matchAll,
|
|
217
|
-
items,
|
|
218
|
-
source,
|
|
219
|
-
};
|
|
237
|
+
return { mentionAll, match_all: matchAll, tokens: parts, items, source };
|
|
220
238
|
}
|
|
221
239
|
function getSubscriptionTarget(session) {
|
|
222
240
|
const platform = session.platform || session.bot?.platform || '';
|
|
223
|
-
const privateChat =
|
|
241
|
+
const privateChat = (0, permissions_1.isDirectSession)(session);
|
|
224
242
|
const channelId = session.channelId || session.guildId || session.userId || '';
|
|
225
|
-
const key = privateChat ? `private_${session.userId}` : session.guildId;
|
|
243
|
+
const key = privateChat ? `private_${session.userId}` : (session.guildId || channelId);
|
|
226
244
|
return { key, platform, channelId, privateChat };
|
|
227
245
|
}
|
|
228
246
|
function isBotAdmin(session, adminUserIds) {
|
|
229
247
|
return adminUserIds.includes(session?.userId || '');
|
|
230
248
|
}
|
|
249
|
+
// 订阅管理权限:群主/管理员与 Bot 管理员按独立开关生效。
|
|
250
|
+
function canManageGroupSubscription(deps, session) {
|
|
251
|
+
return (0, permissions_1.canManageSubscription)(deps.config, session);
|
|
252
|
+
}
|
|
231
253
|
function sameStringArray(left, right) {
|
|
232
254
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
233
255
|
}
|
|
234
|
-
function
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
: Array.isArray(res?.randomGoods)
|
|
240
|
-
? res.randomGoods
|
|
241
|
-
: [];
|
|
242
|
-
for (const item of randomGoods) {
|
|
243
|
-
const name = String(item?.goods_name || item?.name || '').trim();
|
|
244
|
-
if (!name)
|
|
245
|
-
continue;
|
|
246
|
-
if (item?.price !== undefined && item?.price !== null && item.price !== '')
|
|
247
|
-
priceMap.set(name, item.price);
|
|
248
|
-
if (item?.buy_limit_num !== undefined && item?.buy_limit_num !== null && item.buy_limit_num !== '') {
|
|
249
|
-
limitMap.set(name, item.buy_limit_num);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return { priceMap, limitMap };
|
|
253
|
-
}
|
|
254
|
-
function normalizeMerchantProducts(res, now = new Date()) {
|
|
255
|
-
return getMerchantProducts(res).map((item) => {
|
|
256
|
-
const name = String(item?.name || item?.goods_name || TEXT.unknown).trim() || TEXT.unknown;
|
|
256
|
+
function normalizeMerchantProducts(res, now = new Date(), options) {
|
|
257
|
+
const nowMs = now.getTime();
|
|
258
|
+
const { items } = (0, merchant_service_1.normalizeMerchant)(res, nowMs, options);
|
|
259
|
+
return items.map((item) => {
|
|
260
|
+
const source = { start_time: item.startsAt, end_time: item.endsAt };
|
|
257
261
|
return {
|
|
258
|
-
name,
|
|
259
|
-
image:
|
|
260
|
-
time_label: formatProductWindow(
|
|
261
|
-
category: classifyMerchantItem(
|
|
262
|
-
round_id: getRoundForItem(
|
|
263
|
-
is_active:
|
|
264
|
-
start_time:
|
|
265
|
-
end_time:
|
|
262
|
+
name: item.name,
|
|
263
|
+
image: item.icon,
|
|
264
|
+
time_label: formatProductWindow(source),
|
|
265
|
+
category: classifyMerchantItem(source),
|
|
266
|
+
round_id: getRoundForItem(source, now),
|
|
267
|
+
is_active: item.active,
|
|
268
|
+
start_time: item.startsAt,
|
|
269
|
+
end_time: item.endsAt,
|
|
270
|
+
price: item.price,
|
|
271
|
+
limit: item.limit,
|
|
272
|
+
item_category: item.category,
|
|
273
|
+
allDay: item.allDay,
|
|
274
|
+
goods_id: item.id,
|
|
266
275
|
};
|
|
267
276
|
});
|
|
268
277
|
}
|
|
269
|
-
|
|
270
|
-
|
|
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) {
|
|
271
437
|
const catOrder = { round: 0, normal: 1, weekend: 2 };
|
|
272
|
-
const startY = 592;
|
|
273
|
-
const cardHeight = 308;
|
|
274
|
-
const gap = 43;
|
|
275
438
|
const goodsAll = products.map((product) => {
|
|
276
|
-
const limit =
|
|
277
|
-
const limitText = limit === undefined || limit === null
|
|
439
|
+
const limit = product.limit;
|
|
440
|
+
const limitText = limit === undefined || limit === null ? '--' : String(limit);
|
|
278
441
|
const isHot = product.category !== 'round';
|
|
279
442
|
const isEnded = options.includeEnded ? !product.is_active : false;
|
|
280
443
|
const roundPrefix = product.round_id ? `第${product.round_id}轮·` : '';
|
|
@@ -286,7 +449,7 @@ function buildMerchantCardItems(products, res, options) {
|
|
|
286
449
|
return {
|
|
287
450
|
goods_name: product.name,
|
|
288
451
|
iconUrl: product.image,
|
|
289
|
-
price:
|
|
452
|
+
price: product.price ?? 0,
|
|
290
453
|
num: '',
|
|
291
454
|
category: product.category,
|
|
292
455
|
roundId: product.round_id || 0,
|
|
@@ -303,15 +466,65 @@ function buildMerchantCardItems(products, res, options) {
|
|
|
303
466
|
return catOrder[a.category] - catOrder[b.category];
|
|
304
467
|
return Number(b.price || 0) - Number(a.price || 0);
|
|
305
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);
|
|
306
476
|
const goods = goodsAll.map((item, index) => ({
|
|
307
477
|
...item,
|
|
478
|
+
...buildMerchantIconStyle(item.iconUrl, options.iconMetrics),
|
|
308
479
|
num: String(index + 1).padStart(2, '0'),
|
|
309
480
|
top: startY + index * (cardHeight + gap),
|
|
310
481
|
}));
|
|
311
482
|
const lastCardTop = goods.length > 0 ? goods[goods.length - 1].top : startY;
|
|
312
483
|
const bottomFrameTop = lastCardTop + 287;
|
|
313
484
|
const pageHeight = bottomFrameTop + 160;
|
|
314
|
-
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
|
+
});
|
|
315
528
|
}
|
|
316
529
|
function getCurrentMerchantProducts(products, roundInfo) {
|
|
317
530
|
return products.filter((product) => {
|
|
@@ -386,8 +599,20 @@ function getMerchantDateStr(now = new Date()) {
|
|
|
386
599
|
const parts = getChinaParts(now);
|
|
387
600
|
return `${parts.month}.${parts.day}`;
|
|
388
601
|
}
|
|
389
|
-
|
|
390
|
-
|
|
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);
|
|
391
616
|
const roundInfo = getCurrentMerchantRound(now);
|
|
392
617
|
const products = getCurrentMerchantProducts(allProducts, roundInfo);
|
|
393
618
|
const currentWindow = ROUND_WINDOWS.find(win => win.id === roundInfo.current);
|
|
@@ -397,13 +622,13 @@ function buildMerchantRenderPayload(res, now = new Date()) {
|
|
|
397
622
|
const data = {
|
|
398
623
|
dateStr: getMerchantDateStr(now),
|
|
399
624
|
timeRange,
|
|
400
|
-
...buildMerchantCardItems(products,
|
|
625
|
+
...buildMerchantCardItems(products, { includeEnded: false, iconMetrics }),
|
|
401
626
|
};
|
|
402
627
|
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
403
628
|
return { products, roundInfo, data, fallback };
|
|
404
629
|
}
|
|
405
|
-
function buildLegacyMerchantRenderPayload(res, now = new Date()) {
|
|
406
|
-
const allProducts = normalizeMerchantProducts(res, now);
|
|
630
|
+
function buildLegacyMerchantRenderPayload(res, now = new Date(), options) {
|
|
631
|
+
const allProducts = normalizeMerchantProducts(res, now, options);
|
|
407
632
|
const roundInfo = getCurrentMerchantRound(now);
|
|
408
633
|
const products = getCurrentMerchantProducts(allProducts, roundInfo);
|
|
409
634
|
const roundGroups = getLegacyRoundGroups(now, roundInfo);
|
|
@@ -421,31 +646,48 @@ function buildLegacyMerchantRenderPayload(res, now = new Date()) {
|
|
|
421
646
|
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
422
647
|
return { products, roundInfo, data, fallback };
|
|
423
648
|
}
|
|
424
|
-
function buildTodayMerchantRenderPayload(res, now = new Date()) {
|
|
425
|
-
const products = normalizeMerchantProducts(res, now)
|
|
649
|
+
function buildTodayMerchantRenderPayload(res, now = new Date(), options, priceLog, iconMetrics) {
|
|
650
|
+
const products = backfillTodayPrices(priceLog, normalizeMerchantProducts(res, now, options)
|
|
426
651
|
.filter(product => {
|
|
427
652
|
const source = {
|
|
428
653
|
start_time: product.start_time,
|
|
429
654
|
end_time: product.end_time,
|
|
430
655
|
};
|
|
431
656
|
return isMerchantItemToday(source, now);
|
|
432
|
-
});
|
|
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;
|
|
433
671
|
const data = {
|
|
434
672
|
dateStr: getMerchantDateStr(now),
|
|
435
|
-
|
|
673
|
+
goods,
|
|
674
|
+
rows,
|
|
675
|
+
startTop: MERCHANT_TODAY_START_Y,
|
|
676
|
+
bottomFrameTop,
|
|
677
|
+
...buildMerchantRenderParams(pageHeight),
|
|
436
678
|
};
|
|
437
679
|
const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, products);
|
|
438
680
|
return { products, data, fallback };
|
|
439
681
|
}
|
|
440
|
-
function buildLegacyTodayMerchantRenderPayload(res, now = new Date()) {
|
|
441
|
-
const allProducts = normalizeMerchantProducts(res, now)
|
|
682
|
+
function buildLegacyTodayMerchantRenderPayload(res, now = new Date(), options, priceLog) {
|
|
683
|
+
const allProducts = backfillTodayPrices(priceLog, normalizeMerchantProducts(res, now, options)
|
|
442
684
|
.filter(product => {
|
|
443
685
|
const source = {
|
|
444
686
|
start_time: product.start_time,
|
|
445
687
|
end_time: product.end_time,
|
|
446
688
|
};
|
|
447
689
|
return isMerchantItemToday(source, now);
|
|
448
|
-
});
|
|
690
|
+
}), now);
|
|
449
691
|
const roundInfo = getCurrentMerchantRound(now);
|
|
450
692
|
const roundGroups = getLegacyRoundGroups(now, roundInfo);
|
|
451
693
|
for (const product of allProducts) {
|
|
@@ -471,45 +713,111 @@ function buildLegacyTodayMerchantRenderPayload(res, now = new Date()) {
|
|
|
471
713
|
function useLegacyMerchantUi(deps) {
|
|
472
714
|
return deps.config.merchantUiStyle === 'old';
|
|
473
715
|
}
|
|
474
|
-
function buildConfiguredMerchantRenderPayload(deps, res, now = new Date()) {
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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));
|
|
726
|
+
return {
|
|
727
|
+
...buildMerchantRenderPayload(res, now, options, iconMetrics),
|
|
728
|
+
templateName: 'yuanxing-shangren/merchant',
|
|
729
|
+
};
|
|
730
|
+
});
|
|
482
731
|
}
|
|
483
|
-
function buildConfiguredTodayMerchantRenderPayload(deps, res, now = new Date()) {
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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));
|
|
742
|
+
return {
|
|
743
|
+
...buildTodayMerchantRenderPayload(res, now, options, deps.merchantPriceLog, iconMetrics),
|
|
744
|
+
templateName: 'yuanxing-shangren/today',
|
|
745
|
+
};
|
|
746
|
+
});
|
|
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;
|
|
491
763
|
}
|
|
492
|
-
async function checkMerchantSubscriptions(deps) {
|
|
764
|
+
async function checkMerchantSubscriptions(deps, signal) {
|
|
493
765
|
const { ctx, client, merchantSubMgr, renderer, config } = deps;
|
|
494
|
-
const
|
|
495
|
-
if (!
|
|
496
|
-
return { subscriptions: 0, matched: 0, pushed: 0 };
|
|
497
|
-
|
|
766
|
+
const subs = merchantSubMgr.getAll();
|
|
767
|
+
if (!Object.keys(subs).length)
|
|
768
|
+
return { subscriptions: 0, matched: 0, pushed: 0, kind: 'no-subscriptions' };
|
|
769
|
+
if (signal?.aborted)
|
|
770
|
+
return { subscriptions: 0, matched: 0, pushed: 0, kind: 'cancelled' };
|
|
771
|
+
const res = await client.getMerchantInfo(ctx, true, { signal });
|
|
772
|
+
if (signal?.aborted)
|
|
773
|
+
return { subscriptions: 0, matched: 0, pushed: 0, kind: 'cancelled' };
|
|
774
|
+
// 接口失败视为空结果等待稍后重试,不再发送空商品图片。
|
|
775
|
+
if (!res) {
|
|
776
|
+
logger.warn(`远行商人订阅查询失败:${client.getLastErrorBrief?.() || '接口无返回'},等待重试`);
|
|
777
|
+
return { subscriptions: Object.keys(subs).length, matched: 0, pushed: 0, kind: 'error' };
|
|
778
|
+
}
|
|
779
|
+
recordLiveMerchantPrices(deps, res);
|
|
780
|
+
const { products, roundInfo, data, fallback, templateName } = await buildConfiguredMerchantRenderPayload(deps, res);
|
|
781
|
+
if (!products.length)
|
|
782
|
+
return { subscriptions: Object.keys(subs).length, matched: 0, pushed: 0, kind: 'empty' };
|
|
498
783
|
const productNames = products.map((p) => p.name || '').filter(Boolean);
|
|
499
|
-
|
|
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();
|
|
788
|
+
const rendered = await renderer.renderHtml(ctx, templateName, data, { signal });
|
|
500
789
|
const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
|
|
501
|
-
const subs = merchantSubMgr.getAll();
|
|
502
790
|
let matchedCount = 0;
|
|
503
791
|
let pushedCount = 0;
|
|
504
792
|
for (const [key, sub] of Object.entries(subs)) {
|
|
793
|
+
if (signal?.aborted)
|
|
794
|
+
break;
|
|
795
|
+
if (JSON.stringify(merchantSubMgr.getAll()[key]) !== JSON.stringify(sub))
|
|
796
|
+
continue;
|
|
505
797
|
const matchAll = !!sub.match_all;
|
|
506
|
-
|
|
798
|
+
let matched = matchAll
|
|
507
799
|
? productNames
|
|
508
800
|
: sub.items.filter((item) => productNames.some(n => n.includes(item)));
|
|
509
801
|
if (!matchAll && !matched.length)
|
|
510
802
|
continue;
|
|
511
803
|
if (matchAll && !products.length)
|
|
512
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;
|
|
513
821
|
matchedCount++;
|
|
514
822
|
if (matchAll) {
|
|
515
823
|
if (sub.last_push_round === roundInfo.round_id)
|
|
@@ -521,7 +829,7 @@ async function checkMerchantSubscriptions(deps) {
|
|
|
521
829
|
}
|
|
522
830
|
const msg = matchAll
|
|
523
831
|
? `\ud83d\udd14 \u8fdc\u884c\u5546\u4eba\u5237\u65b0\u63d0\u9192\n\u5f53\u524d\u5546\u54c1\uff1a${productNames.join('\u3001')}`
|
|
524
|
-
: `\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${
|
|
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')}`;
|
|
525
833
|
const platform = sub.platform || ctx.bots[0]?.platform;
|
|
526
834
|
const channelId = sub.channel_id || sub.group_id || sub.user_id || key;
|
|
527
835
|
if (!platform || !channelId) {
|
|
@@ -534,118 +842,228 @@ async function checkMerchantSubscriptions(deps) {
|
|
|
534
842
|
channelId,
|
|
535
843
|
guildId: sub.group_id || '',
|
|
536
844
|
userId: sub.user_id || '',
|
|
537
|
-
|
|
538
|
-
|
|
845
|
+
selfId: sub.self_id || '',
|
|
846
|
+
}, renderedImage, fallbackText, !!sub.mention_all, signal);
|
|
847
|
+
if (!sent || signal?.aborted || JSON.stringify(merchantSubMgr.getAll()[key]) !== JSON.stringify(sub))
|
|
539
848
|
continue;
|
|
540
849
|
pushedCount++;
|
|
541
850
|
merchantSubMgr.upsert(key, {
|
|
542
851
|
...sub,
|
|
543
852
|
last_push_round: roundInfo.round_id,
|
|
544
853
|
last_matched_items: matched,
|
|
854
|
+
allday_pushed: matchAll || !alldayNames.size
|
|
855
|
+
? sub.allday_pushed
|
|
856
|
+
: recordAlldayPushed(sub, todayKey, pushItems.filter(onlyAlldayItem)),
|
|
545
857
|
});
|
|
546
858
|
}
|
|
547
|
-
return { subscriptions: Object.keys(subs).length, matched: matchedCount, pushed: pushedCount };
|
|
859
|
+
return { subscriptions: Object.keys(subs).length, matched: matchedCount, pushed: pushedCount, kind: 'success' };
|
|
548
860
|
}
|
|
549
861
|
function register(deps) {
|
|
862
|
+
const merchantRunner = (0, subscription_runner_1.createRunner)(deps.ctx);
|
|
863
|
+
const merchantClock = new merchant_clock_1.MerchantClock(deps.config.merchantTimezone || 'Asia/Shanghai');
|
|
550
864
|
const { ctx, config, client, merchantSubMgr } = deps;
|
|
551
865
|
ctx.command(TEXT.merchant, '\u67e5\u770b\u8fdc\u884c\u5546\u4eba\u5546\u54c1')
|
|
552
866
|
.alias('yxsr')
|
|
867
|
+
.userFields(['authority'])
|
|
553
868
|
.action(async ({ session }) => {
|
|
554
869
|
const res = await client.getMerchantInfo(ctx, true);
|
|
555
870
|
if (!res)
|
|
556
871
|
return `\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\uff1a${client.getLastErrorBrief()}`;
|
|
557
|
-
|
|
872
|
+
recordLiveMerchantPrices(deps, res);
|
|
873
|
+
const { data, fallback, templateName } = await buildConfiguredMerchantRenderPayload(deps, res);
|
|
558
874
|
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
559
875
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
|
|
560
876
|
});
|
|
561
877
|
ctx.command(TEXT.todayMerchant, '查看今日远行商人全部商品')
|
|
878
|
+
.userFields(['authority'])
|
|
562
879
|
.action(async ({ session }) => {
|
|
563
880
|
const res = await client.getMerchantInfo(ctx, true);
|
|
564
881
|
if (!res)
|
|
565
882
|
return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
|
|
566
|
-
|
|
883
|
+
recordLiveMerchantPrices(deps, res);
|
|
884
|
+
const { data, fallback, templateName } = await buildConfiguredTodayMerchantRenderPayload(deps, res);
|
|
567
885
|
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
568
886
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren:today', deps.config);
|
|
569
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
|
+
});
|
|
570
906
|
ctx.command(`${TEXT.subscribe} [args:text]`, '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\u63d0\u9192')
|
|
907
|
+
.userFields(['authority'])
|
|
571
908
|
.action(async ({ session }, args) => {
|
|
572
909
|
const target = getSubscriptionTarget(session);
|
|
573
|
-
if (!target.privateChat && !
|
|
574
|
-
return '
|
|
910
|
+
if (!target.privateChat && !canManageGroupSubscription(deps, session))
|
|
911
|
+
return '此指令仅限群管理员或机器人管理员使用。';
|
|
575
912
|
if (target.privateChat && !config.merchantPrivateSubscriptionEnabled)
|
|
576
913
|
return '个人私聊订阅功能已被禁用,请联系机器人管理员。';
|
|
577
914
|
const parsed = parseMerchantSubscriptionArgs(args, config.merchantSubscriptionItems);
|
|
578
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);
|
|
579
952
|
merchantSubMgr.upsert(target.key, {
|
|
580
953
|
group_id: session.guildId || '',
|
|
581
954
|
user_id: target.privateChat ? session.userId : '',
|
|
582
955
|
type: target.privateChat ? '个人订阅' : '群订阅',
|
|
583
956
|
channel_id: target.channelId,
|
|
584
957
|
platform: target.platform,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
958
|
+
self_id: session.selfId || session.bot?.selfId || '',
|
|
959
|
+
items,
|
|
960
|
+
match_all: matchAllFinal,
|
|
961
|
+
mention_all: mentionAll,
|
|
588
962
|
last_push_round: existing?.last_push_round ?? null,
|
|
589
963
|
last_matched_items: existing?.last_matched_items ?? [],
|
|
964
|
+
allday_pushed: existing?.allday_pushed,
|
|
590
965
|
updated_by: session.userId,
|
|
591
966
|
});
|
|
592
|
-
|
|
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');
|
|
593
985
|
});
|
|
594
986
|
ctx.command(TEXT.viewSubscribe, '\u67e5\u770b\u5f53\u524d\u4f1a\u8bdd\u7684\u8fdc\u884c\u5546\u4eba\u8ba2\u9605')
|
|
987
|
+
.userFields(['authority'])
|
|
595
988
|
.action(async ({ session }) => {
|
|
596
989
|
const target = getSubscriptionTarget(session);
|
|
597
|
-
if (!target.privateChat && !
|
|
598
|
-
return '
|
|
990
|
+
if (!target.privateChat && !canManageGroupSubscription(deps, session))
|
|
991
|
+
return '此指令仅限群管理员或机器人管理员使用。';
|
|
599
992
|
const sub = merchantSubMgr.get(target.key);
|
|
600
993
|
const scopeName = target.privateChat ? '你' : '当前群组';
|
|
601
994
|
if (!sub)
|
|
602
995
|
return `${scopeName}未订阅远行商人。\n用法:${TEXT.subscribe} [1/0] [商品名1] [商品名2] ...\n或:${TEXT.subscribe} 全部(每轮直接推送整张商人图)`;
|
|
603
|
-
const
|
|
996
|
+
const catalog = await deps.merchantCatalog.getCatalog();
|
|
997
|
+
const itemsText = sub.match_all ? '全部商品(每轮推送)' : (0, merchant_catalog_1.describeMerchantItems)(sub.items || [], catalog) || (sub.items || []).join('、');
|
|
604
998
|
return `${scopeName}订阅商品:${itemsText}\n提醒方式:${target.privateChat ? '私聊提醒' : (sub.mention_all ? '@全体' : '普通提醒')}`;
|
|
605
999
|
});
|
|
606
1000
|
ctx.command(TEXT.unsubscribe, '\u53d6\u6d88\u8fdc\u884c\u5546\u4eba\u8ba2\u9605')
|
|
1001
|
+
.userFields(['authority'])
|
|
607
1002
|
.action(async ({ session }) => {
|
|
608
1003
|
const target = getSubscriptionTarget(session);
|
|
609
|
-
if (!target.privateChat && !
|
|
610
|
-
return '
|
|
1004
|
+
if (!target.privateChat && !canManageGroupSubscription(deps, session))
|
|
1005
|
+
return '此指令仅限群管理员或机器人管理员使用。';
|
|
611
1006
|
merchantSubMgr.delete(target.key);
|
|
612
1007
|
return '\u2705 \u5df2\u53d6\u6d88\u8fdc\u884c\u5546\u4eba\u8ba2\u9605\u3002';
|
|
613
1008
|
});
|
|
614
1009
|
ctx.command('洛克').subcommand('.调试远行商人订阅', '立即执行一次远行商人订阅检查')
|
|
615
1010
|
.alias('洛克调试远行商人订阅')
|
|
1011
|
+
.userFields(['authority'])
|
|
616
1012
|
.action(async ({ session }) => {
|
|
617
1013
|
if (!isBotAdmin(session, config.adminUserIds))
|
|
618
1014
|
return '此指令仅限管理员使用。';
|
|
619
|
-
const result = await checkMerchantSubscriptions(deps);
|
|
1015
|
+
const result = await merchantRunner(signal => checkMerchantSubscriptions(deps, signal));
|
|
1016
|
+
if (!result)
|
|
1017
|
+
return '订阅检查正在运行或插件已停止。';
|
|
620
1018
|
return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
|
|
621
1019
|
});
|
|
1020
|
+
const roundKey = () => {
|
|
1021
|
+
const p = merchantClock.parts();
|
|
1022
|
+
return p.date + ':' + Math.floor(p.hour / 4);
|
|
1023
|
+
};
|
|
1024
|
+
const runMerchantCheck = () => merchantRunner(async (signal) => {
|
|
1025
|
+
const round = roundKey();
|
|
1026
|
+
for (let retry = 0; retry <= 3; retry++) {
|
|
1027
|
+
if (signal.aborted || (retry > 0 && roundKey() !== round))
|
|
1028
|
+
return;
|
|
1029
|
+
if (!Object.keys(merchantSubMgr.getAll()).length)
|
|
1030
|
+
return;
|
|
1031
|
+
const result = await checkMerchantSubscriptions(deps, signal);
|
|
1032
|
+
// 接口失败与空结果一样等待稍后重试,避免直接放弃本轮推送。
|
|
1033
|
+
if ((result.kind !== 'empty' && result.kind !== 'error') || retry === 3)
|
|
1034
|
+
return;
|
|
1035
|
+
await (0, async_control_1.delay)(240000 + Math.floor(Math.random() * 60001) - 30000, signal);
|
|
1036
|
+
}
|
|
1037
|
+
}).catch(err => { if (!merchantRunner.aborted())
|
|
1038
|
+
logger.warn('远行商人订阅检查失败: ' + err); });
|
|
622
1039
|
if (config.merchantSubscriptionEnabled) {
|
|
623
|
-
// 上游 v3.1.0:检查前加入 ±30s 随机延迟,降低刷新窗口和并发请求冲突。
|
|
624
|
-
const MERCHANT_JITTER_MS = 30000;
|
|
625
|
-
const runCheckWithJitter = () => {
|
|
626
|
-
const jitter = Math.floor(Math.random() * MERCHANT_JITTER_MS);
|
|
627
|
-
setTimeout(() => {
|
|
628
|
-
checkMerchantSubscriptions(deps).catch(err => logger.warn(`远行商人订阅检查失败: ${err}`));
|
|
629
|
-
}, jitter);
|
|
630
|
-
};
|
|
631
1040
|
if (config.merchantCheckMode === 'times' && config.merchantCheckTimes.length > 0) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
1041
|
+
const times = config.merchantCheckTimes.filter(value => /^([01]\d|2[0-3]):[0-5]\d$/.test(value));
|
|
1042
|
+
let lastWallKey = '';
|
|
1043
|
+
const schedule = (after = Date.now()) => {
|
|
1044
|
+
if (merchantRunner.aborted())
|
|
1045
|
+
return;
|
|
1046
|
+
const target = merchantClock.nextTimeFor(times, after);
|
|
1047
|
+
if (target === null)
|
|
637
1048
|
return;
|
|
638
|
-
const
|
|
639
|
-
if (
|
|
1049
|
+
const wallKey = merchantClock.date(target) + ':' + merchantClock.timeText(target);
|
|
1050
|
+
if (wallKey === lastWallKey) {
|
|
1051
|
+
schedule(target);
|
|
640
1052
|
return;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1053
|
+
}
|
|
1054
|
+
const jitter = Math.floor(Math.random() * 60001) - 30000;
|
|
1055
|
+
ctx.setTimeout(() => {
|
|
1056
|
+
lastWallKey = wallKey;
|
|
1057
|
+
void runMerchantCheck();
|
|
1058
|
+
schedule(target);
|
|
1059
|
+
}, Math.max(0, target + jitter - Date.now()));
|
|
1060
|
+
};
|
|
1061
|
+
schedule();
|
|
644
1062
|
}
|
|
645
1063
|
else {
|
|
646
|
-
ctx.setInterval(
|
|
647
|
-
|
|
648
|
-
}, config.merchantCheckInterval);
|
|
1064
|
+
ctx.setInterval(() => {
|
|
1065
|
+
ctx.setTimeout(() => { void runMerchantCheck(); }, Math.floor(Math.random() * 30001));
|
|
1066
|
+
}, Math.max(30000, config.merchantCheckInterval || 300000));
|
|
649
1067
|
}
|
|
650
1068
|
}
|
|
651
1069
|
}
|