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.
- package/lib/atlas-service.js +1 -1
- package/lib/client.d.ts +10 -0
- package/lib/client.js +44 -8
- package/lib/commands/egg.js +1 -1
- package/lib/commands/merchant.d.ts +105 -0
- package/lib/commands/merchant.js +474 -54
- package/lib/commands/query.d.ts +18 -0
- package/lib/commands/query.js +446 -37
- package/lib/commands/share-code.js +47 -2
- package/lib/commands/tools.js +1 -1
- package/lib/commands/wiki.js +1 -1
- package/lib/egg-service.js +1 -1
- 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/index.d.ts +5 -0
- package/lib/index.js +11857 -188
- package/lib/merchant-catalog.d.ts +47 -0
- package/lib/merchant-catalog.js +193 -0
- package/lib/merchant-price-log.d.ts +63 -0
- package/lib/merchant-price-log.js +187 -0
- package/lib/merchant-service.d.ts +17 -3
- package/lib/merchant-service.js +79 -11
- package/lib/pet-data-service.d.ts +13 -0
- package/lib/pet-data-service.js +111 -2
- package/lib/player-service.js +2 -2
- package/lib/ranking-service.js +1 -1
- 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/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/yuanxing-shangren/merchant.html +479 -461
- package/lib/render-templates/yuanxing-shangren/today.html +407 -461
- package/lib/render.d.ts +3 -0
- package/lib/render.js +19 -3
- package/lib/send-image.js +10 -0
- package/lib/share-code-service.d.ts +36 -0
- package/lib/share-code-service.js +73 -0
- package/lib/ttf/rocom_origin.ttf +0 -0
- package/lib/types.d.ts +9 -1
- package/lib/user.d.ts +6 -0
- package/lib/wiki-catalog.js +1 -1
- package/lib/wiki-service.js +1 -1
- package/package.json +2 -1
- package/readme.md +3 -23
- package/lib/ttf/rocom_skill_origin.ttf +0 -0
package/lib/commands/merchant.js
CHANGED
|
@@ -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 =
|
|
221
|
+
let mentionAll = null;
|
|
169
222
|
if (parts[0] === '1' || parts[0] === '0') {
|
|
170
223
|
mentionAll = parts.shift() === '1';
|
|
171
224
|
}
|
|
172
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
...
|
|
432
|
-
templateName:
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
...
|
|
443
|
-
templateName:
|
|
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
|
-
|
|
774
|
+
// 接口失败视为空结果等待稍后重试,不再发送空商品图片。
|
|
775
|
+
if (!res) {
|
|
776
|
+
logger.warn(`远行商人订阅查询失败:${client.getLastErrorBrief?.() || '接口无返回'},等待重试`);
|
|
458
777
|
return { subscriptions: Object.keys(subs).length, matched: 0, pushed: 0, kind: 'error' };
|
|
459
|
-
|
|
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
|
-
|
|
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${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
560
|
-
match_all:
|
|
561
|
-
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
}
|