koishi-plugin-rocom 1.0.12 → 1.0.13
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/client.d.ts +17 -1
- package/lib/client.js +121 -21
- package/lib/commands/merchant.js +340 -89
- package/lib/commands/query.js +22 -6
- package/lib/index.d.ts +4 -0
- package/lib/index.js +480 -122
- package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
- package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
- package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
- package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
- package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
- package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
- package/lib/render-templates/home/index.html +81 -97
- package/lib/render-templates/home/style.css +316 -375
- package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
- package/lib/render-templates/yuanxing-shangren/README.md +86 -0
- package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//345/272/225/351/203/250/344/277/241/346/201/257/346/241/206.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/274/226/345/217/267/350/203/214/346/231/257/346/241/206.png +0 -0
- package/lib/render-templates/yuanxing-shangren/index.html +198 -170
- package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
- package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
- package/lib/render-templates/yuanxing-shangren/style.css +201 -256
- package/lib/render-templates/yuanxing-shangren/today.html +461 -0
- package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
- package/lib/render.js +11 -4
- package/lib/subscription-send.js +4 -5
- package/lib/ttf/rocom_skill_origin.ttf +0 -0
- package/lib/ttf//351/200/240/345/255/227/345/267/245/346/210/277/345/220/257/351/273/221/344/275/223.ttf +0 -0
- package/lib/types.d.ts +4 -0
- package/package.json +1 -1
package/lib/commands/merchant.js
CHANGED
|
@@ -7,6 +7,7 @@ const subscription_send_1 = require("../subscription-send");
|
|
|
7
7
|
const logger = new koishi_1.Logger('rocom-merchant');
|
|
8
8
|
const TEXT = {
|
|
9
9
|
merchant: '\u8fdc\u884c\u5546\u4eba',
|
|
10
|
+
todayMerchant: '今日远行商人',
|
|
10
11
|
subscribe: '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba',
|
|
11
12
|
unsubscribe: '\u53d6\u6d88\u8ba2\u9605\u8fdc\u884c\u5546\u4eba',
|
|
12
13
|
viewSubscribe: '\u67e5\u770b\u8fdc\u884c\u5546\u4eba\u8ba2\u9605',
|
|
@@ -21,6 +22,12 @@ const CATEGORY_LABELS = {
|
|
|
21
22
|
round: '常规商品',
|
|
22
23
|
weekend: '周末限定',
|
|
23
24
|
};
|
|
25
|
+
const ROUND_WINDOWS = [
|
|
26
|
+
{ id: 1, label: '08:00-12:00', startHour: 8, endHour: 12 },
|
|
27
|
+
{ id: 2, label: '12:00-16:00', startHour: 12, endHour: 16 },
|
|
28
|
+
{ id: 3, label: '16:00-20:00', startHour: 16, endHour: 20 },
|
|
29
|
+
{ id: 4, label: '20:00-24:00', startHour: 20, endHour: 24 },
|
|
30
|
+
];
|
|
24
31
|
const CHINA_TIMEZONE = 'Asia/Shanghai';
|
|
25
32
|
const chinaPartsFormatter = new Intl.DateTimeFormat('zh-CN', {
|
|
26
33
|
timeZone: CHINA_TIMEZONE,
|
|
@@ -29,19 +36,36 @@ const chinaPartsFormatter = new Intl.DateTimeFormat('zh-CN', {
|
|
|
29
36
|
day: '2-digit',
|
|
30
37
|
hour: '2-digit',
|
|
31
38
|
minute: '2-digit',
|
|
39
|
+
second: '2-digit',
|
|
32
40
|
hour12: false,
|
|
33
41
|
});
|
|
34
|
-
function getChinaParts(
|
|
42
|
+
function getChinaParts(input = Date.now()) {
|
|
35
43
|
const parts = {};
|
|
36
|
-
|
|
44
|
+
const date = input instanceof Date ? input : new Date(input);
|
|
45
|
+
for (const item of chinaPartsFormatter.formatToParts(date)) {
|
|
37
46
|
if (item.type !== 'literal')
|
|
38
47
|
parts[item.type] = item.value;
|
|
39
48
|
}
|
|
40
49
|
return {
|
|
50
|
+
year: Number(parts.year || '0'),
|
|
51
|
+
month: Number(parts.month || '0'),
|
|
52
|
+
day: Number(parts.day || '0'),
|
|
41
53
|
hour: Number(parts.hour || '0'),
|
|
42
54
|
minute: Number(parts.minute || '0'),
|
|
55
|
+
second: Number(parts.second || '0'),
|
|
43
56
|
};
|
|
44
57
|
}
|
|
58
|
+
function padNumber(value) {
|
|
59
|
+
return String(value).padStart(2, '0');
|
|
60
|
+
}
|
|
61
|
+
function getChinaDateText(input = Date.now()) {
|
|
62
|
+
const parts = getChinaParts(input);
|
|
63
|
+
return `${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
64
|
+
}
|
|
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
|
+
}
|
|
45
69
|
function classifyMerchantItem(item) {
|
|
46
70
|
const start = normalizeTimestamp(item?.start_time);
|
|
47
71
|
const end = normalizeTimestamp(item?.end_time);
|
|
@@ -72,16 +96,12 @@ function formatProductWindow(product) {
|
|
|
72
96
|
if (!start && !end)
|
|
73
97
|
return '';
|
|
74
98
|
const formatDate = (timestamp) => {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
78
|
-
return `${month}-${day}`;
|
|
99
|
+
const parts = getChinaParts(timestamp);
|
|
100
|
+
return `${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
79
101
|
};
|
|
80
102
|
const formatTime = (timestamp) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
84
|
-
return `${hour}:${minute}`;
|
|
103
|
+
const parts = getChinaParts(timestamp);
|
|
104
|
+
return `${padNumber(parts.hour)}:${padNumber(parts.minute)}`;
|
|
85
105
|
};
|
|
86
106
|
if (start && end) {
|
|
87
107
|
const datePart = formatDate(start);
|
|
@@ -95,7 +115,7 @@ function getMerchantActivity(res) {
|
|
|
95
115
|
const activities = res?.merchantActivities || res?.merchant_activities || [];
|
|
96
116
|
return activities[0] || {};
|
|
97
117
|
}
|
|
98
|
-
function
|
|
118
|
+
function getMerchantProducts(res) {
|
|
99
119
|
const activity = getMerchantActivity(res);
|
|
100
120
|
const groups = [];
|
|
101
121
|
if (Array.isArray(activity?.products))
|
|
@@ -110,13 +130,10 @@ function getActiveProducts(res) {
|
|
|
110
130
|
groups.push(activity.get_pets);
|
|
111
131
|
const merged = [];
|
|
112
132
|
const seen = new Set();
|
|
113
|
-
const now = Date.now();
|
|
114
133
|
for (const list of groups) {
|
|
115
134
|
for (const item of list) {
|
|
116
135
|
const start = normalizeTimestamp(item?.start_time) ?? 0;
|
|
117
136
|
const end = normalizeTimestamp(item?.end_time) ?? Infinity;
|
|
118
|
-
if (now < start || now >= end)
|
|
119
|
-
continue;
|
|
120
137
|
const key = `${item?.id ?? ''}|${item?.name ?? ''}|${start}|${end === Infinity ? 'inf' : end}`;
|
|
121
138
|
if (seen.has(key))
|
|
122
139
|
continue;
|
|
@@ -126,36 +143,58 @@ function getActiveProducts(res) {
|
|
|
126
143
|
}
|
|
127
144
|
return merged;
|
|
128
145
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
function isMerchantItemActive(item, now = Date.now()) {
|
|
147
|
+
const nowMs = now instanceof Date ? now.getTime() : now;
|
|
148
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
149
|
+
const end = normalizeTimestamp(item?.end_time);
|
|
150
|
+
return (start === null || nowMs >= start) && (end === null || nowMs < end);
|
|
151
|
+
}
|
|
152
|
+
function isMerchantItemToday(item, now = Date.now()) {
|
|
153
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
154
|
+
const end = normalizeTimestamp(item?.end_time);
|
|
155
|
+
if (start === null || end === null)
|
|
156
|
+
return true;
|
|
157
|
+
const startOfDay = getChinaDayStartMs(now);
|
|
158
|
+
const endOfDay = startOfDay + 24 * 60 * 60 * 1000;
|
|
159
|
+
return start < endOfDay && end > startOfDay;
|
|
160
|
+
}
|
|
161
|
+
function getRoundForItem(item, now = Date.now()) {
|
|
162
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
163
|
+
if (start === null)
|
|
164
|
+
return null;
|
|
165
|
+
const startParts = getChinaParts(start);
|
|
166
|
+
const nowParts = getChinaParts(now);
|
|
167
|
+
if (startParts.year !== nowParts.year || startParts.month !== nowParts.month || startParts.day !== nowParts.day) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const startHour = startParts.hour + startParts.minute / 60;
|
|
171
|
+
const round = ROUND_WINDOWS.find(win => startHour >= win.startHour && startHour < win.endHour);
|
|
172
|
+
return round?.id ?? null;
|
|
173
|
+
}
|
|
174
|
+
function getCurrentMerchantRound(now = new Date()) {
|
|
175
|
+
const parts = getChinaParts(now);
|
|
176
|
+
const secondsOfDay = parts.hour * 3600 + parts.minute * 60 + parts.second;
|
|
177
|
+
const marketStartSeconds = 8 * 3600;
|
|
178
|
+
const marketEndSeconds = 24 * 3600;
|
|
179
|
+
const datePart = getChinaDateText(now);
|
|
180
|
+
if (secondsOfDay < marketStartSeconds || secondsOfDay >= marketEndSeconds) {
|
|
181
|
+
return {
|
|
182
|
+
current: null,
|
|
183
|
+
total: ROUND_WINDOWS.length,
|
|
184
|
+
countdown: TEXT.notOpen,
|
|
185
|
+
is_open: false,
|
|
186
|
+
round_id: `${datePart}-closed`,
|
|
187
|
+
};
|
|
141
188
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
nextTime.setDate(nextTime.getDate() + 1);
|
|
147
|
-
nextTime.setHours(nextRound, 0, 0, 0);
|
|
148
|
-
const diff = nextTime.getTime() - now.getTime();
|
|
189
|
+
const currentWindow = ROUND_WINDOWS.find(win => parts.hour >= win.startHour && parts.hour < win.endHour);
|
|
190
|
+
const currentRound = currentWindow?.id ?? null;
|
|
191
|
+
const roundEndSeconds = (currentWindow?.endHour ?? 24) * 3600;
|
|
192
|
+
const diff = Math.max(0, (roundEndSeconds - secondsOfDay) * 1000);
|
|
149
193
|
const hours = Math.floor(diff / 3600000);
|
|
150
194
|
const mins = Math.floor((diff % 3600000) / 60000);
|
|
151
|
-
const datePart = [
|
|
152
|
-
now.getFullYear(),
|
|
153
|
-
String(now.getMonth() + 1).padStart(2, '0'),
|
|
154
|
-
String(now.getDate()).padStart(2, '0'),
|
|
155
|
-
].join('-');
|
|
156
195
|
return {
|
|
157
196
|
current: currentRound,
|
|
158
|
-
total:
|
|
197
|
+
total: ROUND_WINDOWS.length,
|
|
159
198
|
countdown: `${hours}\u5c0f\u65f6${mins}\u5206\u949f`,
|
|
160
199
|
is_open: currentRound !== null,
|
|
161
200
|
round_id: `${datePart}-${currentRound || 'closed'}`,
|
|
@@ -192,69 +231,272 @@ function isBotAdmin(session, adminUserIds) {
|
|
|
192
231
|
function sameStringArray(left, right) {
|
|
193
232
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
194
233
|
}
|
|
195
|
-
function
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
function getRandomGoodsMaps(res) {
|
|
235
|
+
const priceMap = new Map();
|
|
236
|
+
const limitMap = new Map();
|
|
237
|
+
const randomGoods = Array.isArray(res?.random_goods)
|
|
238
|
+
? res.random_goods
|
|
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;
|
|
257
|
+
return {
|
|
258
|
+
name,
|
|
259
|
+
image: String(item?.icon_url || item?.iconUrl || ''),
|
|
260
|
+
time_label: formatProductWindow(item),
|
|
261
|
+
category: classifyMerchantItem(item),
|
|
262
|
+
round_id: getRoundForItem(item, now),
|
|
263
|
+
is_active: isMerchantItemActive(item, now),
|
|
264
|
+
start_time: normalizeTimestamp(item?.start_time),
|
|
265
|
+
end_time: normalizeTimestamp(item?.end_time),
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function buildMerchantCardItems(products, res, options) {
|
|
270
|
+
const { priceMap, limitMap } = getRandomGoodsMaps(res);
|
|
271
|
+
const catOrder = { round: 0, normal: 1, weekend: 2 };
|
|
272
|
+
const startY = 592;
|
|
273
|
+
const cardHeight = 308;
|
|
274
|
+
const gap = 43;
|
|
275
|
+
const goodsAll = products.map((product) => {
|
|
276
|
+
const limit = limitMap.get(product.name);
|
|
277
|
+
const limitText = limit === undefined || limit === null || limit === '' ? '--' : String(limit);
|
|
278
|
+
const isHot = product.category !== 'round';
|
|
279
|
+
const isEnded = options.includeEnded ? !product.is_active : false;
|
|
280
|
+
const roundPrefix = product.round_id ? `第${product.round_id}轮·` : '';
|
|
281
|
+
const remainingStr = product.category === 'normal'
|
|
282
|
+
? `本日限购${limitText}个`
|
|
283
|
+
: product.category === 'weekend'
|
|
284
|
+
? `活动期间限购${limitText}个`
|
|
285
|
+
: `${isEnded ? roundPrefix : ''}本轮限购${limitText}个`;
|
|
286
|
+
return {
|
|
287
|
+
goods_name: product.name,
|
|
288
|
+
iconUrl: product.image,
|
|
289
|
+
price: priceMap.get(product.name) ?? 0,
|
|
290
|
+
num: '',
|
|
291
|
+
category: product.category,
|
|
292
|
+
roundId: product.round_id || 0,
|
|
293
|
+
isHot,
|
|
294
|
+
isEnded,
|
|
295
|
+
remainingStr,
|
|
296
|
+
top: 0,
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
goodsAll.sort((a, b) => {
|
|
300
|
+
if (options.includeEnded && a.isEnded !== b.isEnded)
|
|
301
|
+
return a.isEnded ? 1 : -1;
|
|
302
|
+
if (a.category !== b.category)
|
|
303
|
+
return catOrder[a.category] - catOrder[b.category];
|
|
304
|
+
return Number(b.price || 0) - Number(a.price || 0);
|
|
305
|
+
});
|
|
306
|
+
const goods = goodsAll.map((item, index) => ({
|
|
307
|
+
...item,
|
|
308
|
+
num: String(index + 1).padStart(2, '0'),
|
|
309
|
+
top: startY + index * (cardHeight + gap),
|
|
204
310
|
}));
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
311
|
+
const lastCardTop = goods.length > 0 ? goods[goods.length - 1].top : startY;
|
|
312
|
+
const bottomFrameTop = lastCardTop + 287;
|
|
313
|
+
const pageHeight = bottomFrameTop + 160;
|
|
314
|
+
return { goods, bottomFrameTop, pageHeight };
|
|
315
|
+
}
|
|
316
|
+
function getCurrentMerchantProducts(products, roundInfo) {
|
|
317
|
+
return products.filter((product) => {
|
|
318
|
+
if (!product.is_active)
|
|
319
|
+
return false;
|
|
320
|
+
if (product.category === 'round')
|
|
321
|
+
return !!roundInfo.current && product.round_id === roundInfo.current;
|
|
322
|
+
return true;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function buildMerchantFallbackText(title, products, roundInfo) {
|
|
326
|
+
const fallbackLines = [title];
|
|
327
|
+
if (roundInfo) {
|
|
328
|
+
fallbackLines.push(`轮次:第 ${roundInfo.current || TEXT.notOpen} / ${roundInfo.total} 轮`);
|
|
329
|
+
fallbackLines.push(`剩余:${roundInfo.countdown}`);
|
|
330
|
+
}
|
|
331
|
+
fallbackLines.push('');
|
|
332
|
+
if (!products.length) {
|
|
333
|
+
fallbackLines.push(roundInfo ? '当前轮次暂无商品。' : '今日暂无已公布的远行商人商品。');
|
|
334
|
+
return fallbackLines.join('\n').trimEnd();
|
|
335
|
+
}
|
|
336
|
+
for (const key of CATEGORY_ORDER) {
|
|
337
|
+
const group = products.filter(product => product.category === key);
|
|
338
|
+
if (!group.length)
|
|
339
|
+
continue;
|
|
340
|
+
fallbackLines.push(`【${CATEGORY_LABELS[key]}】`);
|
|
341
|
+
group.forEach((product, index) => {
|
|
342
|
+
const tail = product.time_label ? ` (${product.time_label})` : '';
|
|
343
|
+
fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
|
|
344
|
+
});
|
|
345
|
+
fallbackLines.push('');
|
|
212
346
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
347
|
+
return fallbackLines.join('\n').trimEnd();
|
|
348
|
+
}
|
|
349
|
+
function getMerchantActivityTitle(res) {
|
|
350
|
+
const activity = getMerchantActivity(res);
|
|
351
|
+
return String(activity?.name || TEXT.merchant).trim() || TEXT.merchant;
|
|
352
|
+
}
|
|
353
|
+
function getMerchantActivitySubtitle(res) {
|
|
354
|
+
const activity = getMerchantActivity(res);
|
|
355
|
+
return String(activity?.start_date || '每日 08:00 / 12:00 / 16:00 / 20:00 刷新').trim();
|
|
356
|
+
}
|
|
357
|
+
function getLegacyRoundGroups(now, roundInfo) {
|
|
358
|
+
return ROUND_WINDOWS.map(win => ({
|
|
359
|
+
round_id: win.id,
|
|
360
|
+
label: `${padNumber(win.startHour)}:00 - ${padNumber(win.endHour)}:00`,
|
|
361
|
+
is_current: roundInfo.current === win.id,
|
|
362
|
+
products: [],
|
|
219
363
|
}));
|
|
364
|
+
}
|
|
365
|
+
function buildLegacyCategoryGroups(products, roundGroups) {
|
|
366
|
+
return CATEGORY_ORDER
|
|
367
|
+
.map((key) => {
|
|
368
|
+
const groups = roundGroups
|
|
369
|
+
.map(group => ({
|
|
370
|
+
round_id: group.round_id,
|
|
371
|
+
label: group.label,
|
|
372
|
+
is_current: group.is_current,
|
|
373
|
+
products: group.products.filter(product => product.category === key),
|
|
374
|
+
}))
|
|
375
|
+
.filter(group => group.products.length > 0);
|
|
376
|
+
return {
|
|
377
|
+
key,
|
|
378
|
+
label: CATEGORY_LABELS[key],
|
|
379
|
+
roundGroups: groups,
|
|
380
|
+
product_count: groups.reduce((sum, group) => sum + group.products.length, 0),
|
|
381
|
+
};
|
|
382
|
+
})
|
|
383
|
+
.filter(category => category.product_count > 0);
|
|
384
|
+
}
|
|
385
|
+
function getMerchantDateStr(now = new Date()) {
|
|
386
|
+
const parts = getChinaParts(now);
|
|
387
|
+
return `${parts.month}.${parts.day}`;
|
|
388
|
+
}
|
|
389
|
+
function buildMerchantRenderPayload(res, now = new Date()) {
|
|
390
|
+
const allProducts = normalizeMerchantProducts(res, now);
|
|
391
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
392
|
+
const products = getCurrentMerchantProducts(allProducts, roundInfo);
|
|
393
|
+
const currentWindow = ROUND_WINDOWS.find(win => win.id === roundInfo.current);
|
|
394
|
+
const timeRange = currentWindow
|
|
395
|
+
? `${padNumber(currentWindow.startHour)}:00-${padNumber(currentWindow.endHour)}:00`
|
|
396
|
+
: '--:--~--:--';
|
|
397
|
+
const data = {
|
|
398
|
+
dateStr: getMerchantDateStr(now),
|
|
399
|
+
timeRange,
|
|
400
|
+
...buildMerchantCardItems(products, res, { includeEnded: false }),
|
|
401
|
+
};
|
|
402
|
+
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
403
|
+
return { products, roundInfo, data, fallback };
|
|
404
|
+
}
|
|
405
|
+
function buildLegacyMerchantRenderPayload(res, now = new Date()) {
|
|
406
|
+
const allProducts = normalizeMerchantProducts(res, now);
|
|
407
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
408
|
+
const products = getCurrentMerchantProducts(allProducts, roundInfo);
|
|
409
|
+
const roundGroups = getLegacyRoundGroups(now, roundInfo);
|
|
410
|
+
const currentGroup = roundGroups.find(group => group.round_id === roundInfo.current) || roundGroups[0];
|
|
411
|
+
currentGroup.products.push(...products);
|
|
412
|
+
const categories = buildLegacyCategoryGroups(products, roundGroups);
|
|
220
413
|
const data = {
|
|
221
414
|
background: '',
|
|
222
|
-
title:
|
|
223
|
-
subtitle:
|
|
224
|
-
titleIcon: true,
|
|
225
|
-
product_count: renderedProducts.length,
|
|
226
|
-
round_info: roundInfo,
|
|
415
|
+
title: getMerchantActivityTitle(res),
|
|
416
|
+
subtitle: getMerchantActivitySubtitle(res),
|
|
227
417
|
categories,
|
|
228
|
-
|
|
418
|
+
roundGroups,
|
|
419
|
+
total_products: products.length,
|
|
229
420
|
};
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
421
|
+
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
422
|
+
return { products, roundInfo, data, fallback };
|
|
423
|
+
}
|
|
424
|
+
function buildTodayMerchantRenderPayload(res, now = new Date()) {
|
|
425
|
+
const products = normalizeMerchantProducts(res, now)
|
|
426
|
+
.filter(product => {
|
|
427
|
+
const source = {
|
|
428
|
+
start_time: product.start_time,
|
|
429
|
+
end_time: product.end_time,
|
|
430
|
+
};
|
|
431
|
+
return isMerchantItemToday(source, now);
|
|
432
|
+
});
|
|
433
|
+
const data = {
|
|
434
|
+
dateStr: getMerchantDateStr(now),
|
|
435
|
+
...buildMerchantCardItems(products, res, { includeEnded: true }),
|
|
436
|
+
};
|
|
437
|
+
const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, products);
|
|
438
|
+
return { products, data, fallback };
|
|
439
|
+
}
|
|
440
|
+
function buildLegacyTodayMerchantRenderPayload(res, now = new Date()) {
|
|
441
|
+
const allProducts = normalizeMerchantProducts(res, now)
|
|
442
|
+
.filter(product => {
|
|
443
|
+
const source = {
|
|
444
|
+
start_time: product.start_time,
|
|
445
|
+
end_time: product.end_time,
|
|
446
|
+
};
|
|
447
|
+
return isMerchantItemToday(source, now);
|
|
448
|
+
});
|
|
449
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
450
|
+
const roundGroups = getLegacyRoundGroups(now, roundInfo);
|
|
451
|
+
for (const product of allProducts) {
|
|
452
|
+
const roundId = product.category === 'round' ? product.round_id : null;
|
|
453
|
+
const group = roundId
|
|
454
|
+
? roundGroups.find(item => item.round_id === roundId)
|
|
455
|
+
: roundGroups[0];
|
|
456
|
+
if (group)
|
|
457
|
+
group.products.push(product);
|
|
246
458
|
}
|
|
247
|
-
const
|
|
248
|
-
|
|
459
|
+
const categories = buildLegacyCategoryGroups(allProducts, roundGroups);
|
|
460
|
+
const data = {
|
|
461
|
+
background: '',
|
|
462
|
+
title: TEXT.todayMerchant,
|
|
463
|
+
subtitle: `${getChinaDateText(now)} · 每日 08:00 / 12:00 / 16:00 / 20:00 刷新`,
|
|
464
|
+
categories,
|
|
465
|
+
roundGroups,
|
|
466
|
+
total_products: allProducts.length,
|
|
467
|
+
};
|
|
468
|
+
const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, allProducts);
|
|
469
|
+
return { products: allProducts, data, fallback };
|
|
470
|
+
}
|
|
471
|
+
function useLegacyMerchantUi(deps) {
|
|
472
|
+
return deps.config.merchantUiStyle === 'old';
|
|
473
|
+
}
|
|
474
|
+
function buildConfiguredMerchantRenderPayload(deps, res, now = new Date()) {
|
|
475
|
+
const payload = useLegacyMerchantUi(deps)
|
|
476
|
+
? buildLegacyMerchantRenderPayload(res, now)
|
|
477
|
+
: buildMerchantRenderPayload(res, now);
|
|
478
|
+
return {
|
|
479
|
+
...payload,
|
|
480
|
+
templateName: useLegacyMerchantUi(deps) ? 'yuanxing-shangren' : 'yuanxing-shangren/merchant',
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function buildConfiguredTodayMerchantRenderPayload(deps, res, now = new Date()) {
|
|
484
|
+
const payload = useLegacyMerchantUi(deps)
|
|
485
|
+
? buildLegacyTodayMerchantRenderPayload(res, now)
|
|
486
|
+
: buildTodayMerchantRenderPayload(res, now);
|
|
487
|
+
return {
|
|
488
|
+
...payload,
|
|
489
|
+
templateName: useLegacyMerchantUi(deps) ? 'yuanxing-shangren' : 'yuanxing-shangren/today',
|
|
490
|
+
};
|
|
249
491
|
}
|
|
250
492
|
async function checkMerchantSubscriptions(deps) {
|
|
251
493
|
const { ctx, client, merchantSubMgr, renderer, config } = deps;
|
|
252
494
|
const res = await client.getMerchantInfo(ctx, true);
|
|
253
495
|
if (!res)
|
|
254
496
|
return { subscriptions: 0, matched: 0, pushed: 0 };
|
|
255
|
-
const { products, roundInfo, data, fallback } =
|
|
497
|
+
const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
256
498
|
const productNames = products.map((p) => p.name || '').filter(Boolean);
|
|
257
|
-
const rendered = await renderer.renderHtml(ctx,
|
|
499
|
+
const rendered = await renderer.renderHtml(ctx, templateName, data);
|
|
258
500
|
const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
|
|
259
501
|
const subs = merchantSubMgr.getAll();
|
|
260
502
|
let matchedCount = 0;
|
|
@@ -311,10 +553,19 @@ function register(deps) {
|
|
|
311
553
|
const res = await client.getMerchantInfo(ctx, true);
|
|
312
554
|
if (!res)
|
|
313
555
|
return `\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\uff1a${client.getLastErrorBrief()}`;
|
|
314
|
-
const { data, fallback } =
|
|
315
|
-
const png = await deps.renderer.renderHtml(ctx,
|
|
556
|
+
const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
557
|
+
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
316
558
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
|
|
317
559
|
});
|
|
560
|
+
ctx.command(TEXT.todayMerchant, '查看今日远行商人全部商品')
|
|
561
|
+
.action(async ({ session }) => {
|
|
562
|
+
const res = await client.getMerchantInfo(ctx, true);
|
|
563
|
+
if (!res)
|
|
564
|
+
return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
|
|
565
|
+
const { data, fallback, templateName } = buildConfiguredTodayMerchantRenderPayload(deps, res);
|
|
566
|
+
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
567
|
+
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren:today', deps.config);
|
|
568
|
+
});
|
|
318
569
|
ctx.command(`${TEXT.subscribe} [args:text]`, '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\u63d0\u9192')
|
|
319
570
|
.action(async ({ session }, args) => {
|
|
320
571
|
const target = getSubscriptionTarget(session);
|
package/lib/commands/query.js
CHANGED
|
@@ -827,7 +827,7 @@ function panelPetDetailData(uid, pet, updatedAtText) {
|
|
|
827
827
|
};
|
|
828
828
|
}
|
|
829
829
|
async function refreshPanelPets(deps, uid, userId = '') {
|
|
830
|
-
const res = await deps.client.ingameHomeInfo(deps.ctx, uid, 20000);
|
|
830
|
+
const res = await deps.client.ingameHomeInfo(deps.ctx, uid, { waitMs: 20000, timeoutMs: 30000 });
|
|
831
831
|
if (res) {
|
|
832
832
|
const homeInfo = homeInfoPayload(res);
|
|
833
833
|
const pets = buildPanelPetList(homeInfo);
|
|
@@ -860,7 +860,7 @@ function formatPanelUpdatedAt(ts) {
|
|
|
860
860
|
return new Date().toLocaleString('zh-CN');
|
|
861
861
|
return new Date(value * 1000).toLocaleString('zh-CN');
|
|
862
862
|
}
|
|
863
|
-
function extractHomePet(raw, index, guard = false) {
|
|
863
|
+
function extractHomePet(deps, raw, index, guard = false) {
|
|
864
864
|
if (!raw || typeof raw !== 'object')
|
|
865
865
|
return null;
|
|
866
866
|
const homePet = raw.home_pet_info && typeof raw.home_pet_info === 'object' ? raw.home_pet_info : raw;
|
|
@@ -874,6 +874,7 @@ function extractHomePet(raw, index, guard = false) {
|
|
|
874
874
|
const eggReady = hasEgg || (predictedEggTime > 0 && nowTs >= predictedEggTime);
|
|
875
875
|
const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
|
|
876
876
|
const gender = Number(display.gender || raw.gender || 0) || 0;
|
|
877
|
+
const mutationType = Number(display.mutation_type || raw.mutation_type || homePet.mutation_type || 0) || 0;
|
|
877
878
|
const isMale = gender === 1;
|
|
878
879
|
const status = homePet.status ?? raw.status;
|
|
879
880
|
const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ['2', 'guard', '守卫'].includes(String(status).toLowerCase());
|
|
@@ -913,7 +914,11 @@ function extractHomePet(raw, index, guard = false) {
|
|
|
913
914
|
name: String(homePet.name || homePet.pet_name || raw.name || raw.pet_name || `精灵 ${petId || ''}`),
|
|
914
915
|
level: display.level || raw.level || homePet.level || '--',
|
|
915
916
|
iconUrl: homePetIcon(petId, raw.icon_url || raw.pet_img_url || raw.petIcon || ''),
|
|
917
|
+
starIconUrl: [1, 8, 9].includes(mutationType)
|
|
918
|
+
? deps.renderer.resourceUrl(`render-templates/home/img/rocomuid/star_${mutationType}.png`)
|
|
919
|
+
: '',
|
|
916
920
|
badge: isGuard ? '守' : (hasEgg ? '蛋' : ''),
|
|
921
|
+
mutationType,
|
|
917
922
|
isGuard,
|
|
918
923
|
statusText,
|
|
919
924
|
statusClass,
|
|
@@ -1038,7 +1043,7 @@ function buildHomeRenderData(deps, res, uid) {
|
|
|
1038
1043
|
const indoorPets = [];
|
|
1039
1044
|
const guardPets = [];
|
|
1040
1045
|
indoorSources.forEach((raw, index) => {
|
|
1041
|
-
const item = extractHomePet(raw, index);
|
|
1046
|
+
const item = extractHomePet(deps, raw, index);
|
|
1042
1047
|
if (!item)
|
|
1043
1048
|
return;
|
|
1044
1049
|
if (item.isGuard)
|
|
@@ -1047,7 +1052,7 @@ function buildHomeRenderData(deps, res, uid) {
|
|
|
1047
1052
|
indoorPets.push(item);
|
|
1048
1053
|
});
|
|
1049
1054
|
guardSources.forEach((raw, index) => {
|
|
1050
|
-
const item = extractHomePet(raw, index, true);
|
|
1055
|
+
const item = extractHomePet(deps, raw, index, true);
|
|
1051
1056
|
if (item)
|
|
1052
1057
|
guardPets.push(item);
|
|
1053
1058
|
});
|
|
@@ -1320,7 +1325,7 @@ async function checkHomeSubscriptions(deps) {
|
|
|
1320
1325
|
continue;
|
|
1321
1326
|
checkedCount++;
|
|
1322
1327
|
if (!cache.has(sub.uid)) {
|
|
1323
|
-
cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid));
|
|
1328
|
+
cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 30000 }));
|
|
1324
1329
|
}
|
|
1325
1330
|
const res = cache.get(sub.uid);
|
|
1326
1331
|
if (!res)
|
|
@@ -1809,7 +1814,18 @@ function register(deps) {
|
|
|
1809
1814
|
}
|
|
1810
1815
|
if (!targetUid)
|
|
1811
1816
|
return '请提供玩家 UID,或先完成绑定后使用 洛克.家园。';
|
|
1812
|
-
|
|
1817
|
+
let queuedNotified = false;
|
|
1818
|
+
const res = await client.ingameHomeInfo(ctx, targetUid, {
|
|
1819
|
+
waitMs: deps.config.homeQueryWaitMs,
|
|
1820
|
+
intervalMs: deps.config.homeQueryPollIntervalMs,
|
|
1821
|
+
timeoutMs: deps.config.homeQueryTimeoutMs,
|
|
1822
|
+
onQueued: async () => {
|
|
1823
|
+
if (queuedNotified)
|
|
1824
|
+
return;
|
|
1825
|
+
queuedNotified = true;
|
|
1826
|
+
await session?.send?.(`UID ${targetUid} 的家园查询已进入队列,正在等待游戏侧返回,请稍候…`);
|
|
1827
|
+
},
|
|
1828
|
+
});
|
|
1813
1829
|
if (!res)
|
|
1814
1830
|
return `家园查询失败:${client.getLastErrorBrief()}`;
|
|
1815
1831
|
await sendImage(deps, session, 'home', buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
|
package/lib/index.d.ts
CHANGED
|
@@ -12,12 +12,16 @@ export interface Config {
|
|
|
12
12
|
autoRefreshTime: string[];
|
|
13
13
|
merchantSubscriptionEnabled: boolean;
|
|
14
14
|
merchantSubscriptionItems: string[];
|
|
15
|
+
merchantUiStyle: 'new' | 'old';
|
|
15
16
|
merchantPrivateSubscriptionEnabled: boolean;
|
|
16
17
|
merchantCheckMode: 'interval' | 'times';
|
|
17
18
|
merchantCheckInterval: number;
|
|
18
19
|
merchantCheckTimes: string[];
|
|
19
20
|
homeSubscriptionEnabled: boolean;
|
|
20
21
|
homeSubscriptionIntervalMinutes: number;
|
|
22
|
+
homeQueryWaitMs: number;
|
|
23
|
+
homeQueryPollIntervalMs: number;
|
|
24
|
+
homeQueryTimeoutMs: number;
|
|
21
25
|
imageCompressionEnabled: boolean;
|
|
22
26
|
imageCompressionMinBytes: number;
|
|
23
27
|
imageCompressionLevel: number;
|