koishi-plugin-rocom 1.0.11 → 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.
Files changed (42) hide show
  1. package/lib/client.d.ts +17 -1
  2. package/lib/client.js +121 -21
  3. package/lib/commands/merchant.js +409 -61
  4. package/lib/commands/query.js +95 -27
  5. package/lib/index.d.ts +4 -0
  6. package/lib/index.js +600 -127
  7. package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
  8. package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
  9. package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
  10. package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
  11. package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
  12. package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
  13. package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
  14. package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
  15. package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
  16. package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
  17. package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
  18. package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
  19. package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
  20. package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
  21. package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
  22. package/lib/render-templates/home/index.html +81 -97
  23. package/lib/render-templates/home/style.css +316 -375
  24. package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
  25. package/lib/render-templates/yuanxing-shangren/README.md +86 -0
  26. package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
  27. 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
  28. package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
  29. package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
  30. 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
  31. package/lib/render-templates/yuanxing-shangren/index.html +233 -148
  32. package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
  33. package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
  34. package/lib/render-templates/yuanxing-shangren/style.css +201 -256
  35. package/lib/render-templates/yuanxing-shangren/today.html +461 -0
  36. package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
  37. package/lib/render.js +11 -4
  38. package/lib/subscription-send.js +4 -5
  39. package/lib/ttf/rocom_skill_origin.ttf +0 -0
  40. 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
  41. package/lib/types.d.ts +4 -0
  42. package/package.json +1 -1
@@ -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',
@@ -15,6 +16,72 @@ const TEXT = {
15
16
  defaultSource: '\u9ed8\u8ba4',
16
17
  customSource: '\u81ea\u5b9a\u4e49',
17
18
  };
19
+ const CATEGORY_ORDER = ['normal', 'round', 'weekend'];
20
+ const CATEGORY_LABELS = {
21
+ normal: '热销商品',
22
+ round: '常规商品',
23
+ weekend: '周末限定',
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
+ ];
31
+ const CHINA_TIMEZONE = 'Asia/Shanghai';
32
+ const chinaPartsFormatter = new Intl.DateTimeFormat('zh-CN', {
33
+ timeZone: CHINA_TIMEZONE,
34
+ year: 'numeric',
35
+ month: '2-digit',
36
+ day: '2-digit',
37
+ hour: '2-digit',
38
+ minute: '2-digit',
39
+ second: '2-digit',
40
+ hour12: false,
41
+ });
42
+ function getChinaParts(input = Date.now()) {
43
+ const parts = {};
44
+ const date = input instanceof Date ? input : new Date(input);
45
+ for (const item of chinaPartsFormatter.formatToParts(date)) {
46
+ if (item.type !== 'literal')
47
+ parts[item.type] = item.value;
48
+ }
49
+ return {
50
+ year: Number(parts.year || '0'),
51
+ month: Number(parts.month || '0'),
52
+ day: Number(parts.day || '0'),
53
+ hour: Number(parts.hour || '0'),
54
+ minute: Number(parts.minute || '0'),
55
+ second: Number(parts.second || '0'),
56
+ };
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
+ }
69
+ function classifyMerchantItem(item) {
70
+ const start = normalizeTimestamp(item?.start_time);
71
+ const end = normalizeTimestamp(item?.end_time);
72
+ if (!start || !end)
73
+ return 'normal';
74
+ const durationDays = (end - start) / (1000 * 60 * 60 * 24);
75
+ if (durationDays >= 2)
76
+ return 'weekend';
77
+ const startParts = getChinaParts(start);
78
+ const endParts = getChinaParts(end);
79
+ const startHour = startParts.hour + startParts.minute / 60;
80
+ const endHour = endParts.hour + endParts.minute / 60;
81
+ if (startHour <= 8 && endHour >= 23.5)
82
+ return 'normal';
83
+ return 'round';
84
+ }
18
85
  function normalizeTimestamp(value) {
19
86
  if (value === null || value === undefined || value === '')
20
87
  return null;
@@ -29,16 +96,12 @@ function formatProductWindow(product) {
29
96
  if (!start && !end)
30
97
  return '';
31
98
  const formatDate = (timestamp) => {
32
- const date = new Date(timestamp);
33
- const month = String(date.getMonth() + 1).padStart(2, '0');
34
- const day = String(date.getDate()).padStart(2, '0');
35
- return `${month}-${day}`;
99
+ const parts = getChinaParts(timestamp);
100
+ return `${padNumber(parts.month)}-${padNumber(parts.day)}`;
36
101
  };
37
102
  const formatTime = (timestamp) => {
38
- const date = new Date(timestamp);
39
- const hour = String(date.getHours()).padStart(2, '0');
40
- const minute = String(date.getMinutes()).padStart(2, '0');
41
- return `${hour}:${minute}`;
103
+ const parts = getChinaParts(timestamp);
104
+ return `${padNumber(parts.hour)}:${padNumber(parts.minute)}`;
42
105
  };
43
106
  if (start && end) {
44
107
  const datePart = formatDate(start);
@@ -52,46 +115,86 @@ function getMerchantActivity(res) {
52
115
  const activities = res?.merchantActivities || res?.merchant_activities || [];
53
116
  return activities[0] || {};
54
117
  }
55
- function getActiveProducts(res) {
118
+ function getMerchantProducts(res) {
56
119
  const activity = getMerchantActivity(res);
57
- const products = activity?.products || activity?.product_list || activity?.get_props || [];
58
- return products.filter((p) => {
59
- const now = Date.now();
60
- const start = normalizeTimestamp(p.start_time) ?? 0;
61
- const end = normalizeTimestamp(p.end_time) ?? Infinity;
62
- return now >= start && now < end;
63
- });
64
- }
65
- function getCurrentMerchantRound() {
66
- const now = new Date();
67
- const hour = now.getHours();
68
- const rounds = [8, 12, 16, 20];
69
- let currentRound = null;
70
- let nextRound = null;
71
- for (let i = 0; i < rounds.length; i++) {
72
- if (hour >= rounds[i] && (i === rounds.length - 1 || hour < rounds[i + 1])) {
73
- currentRound = i + 1;
74
- nextRound = i < rounds.length - 1 ? rounds[i + 1] : rounds[0];
75
- break;
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);
76
142
  }
77
143
  }
78
- if (currentRound === null)
79
- nextRound = rounds[0];
80
- const nextTime = new Date(now);
81
- if (nextRound <= hour)
82
- nextTime.setDate(nextTime.getDate() + 1);
83
- nextTime.setHours(nextRound, 0, 0, 0);
84
- const diff = nextTime.getTime() - now.getTime();
144
+ return merged;
145
+ }
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
+ };
188
+ }
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);
85
193
  const hours = Math.floor(diff / 3600000);
86
194
  const mins = Math.floor((diff % 3600000) / 60000);
87
- const datePart = [
88
- now.getFullYear(),
89
- String(now.getMonth() + 1).padStart(2, '0'),
90
- String(now.getDate()).padStart(2, '0'),
91
- ].join('-');
92
195
  return {
93
196
  current: currentRound,
94
- total: rounds.length,
197
+ total: ROUND_WINDOWS.length,
95
198
  countdown: `${hours}\u5c0f\u65f6${mins}\u5206\u949f`,
96
199
  is_open: currentRound !== null,
97
200
  round_id: `${datePart}-${currentRound || 'closed'}`,
@@ -128,36 +231,272 @@ function isBotAdmin(session, adminUserIds) {
128
231
  function sameStringArray(left, right) {
129
232
  return JSON.stringify(left) === JSON.stringify(right);
130
233
  }
131
- function buildMerchantRenderPayload(res) {
132
- const products = getActiveProducts(res);
133
- const roundInfo = getCurrentMerchantRound();
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),
310
+ }));
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('');
346
+ }
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) {
134
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: [],
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);
135
413
  const data = {
136
414
  background: '',
137
- title: activity.name || TEXT.merchant,
138
- subtitle: activity.start_date || '\u6bcf\u65e5 08:00 / 12:00 / 16:00 / 20:00 \u5237\u65b0',
139
- titleIcon: true,
140
- product_count: products.length,
141
- round_info: roundInfo,
142
- products: products.map((p) => ({
143
- name: p.name || TEXT.unknown,
144
- image: p.icon_url || '',
145
- time_label: formatProductWindow(p),
146
- })),
415
+ title: getMerchantActivityTitle(res),
416
+ subtitle: getMerchantActivitySubtitle(res),
417
+ categories,
418
+ roundGroups,
419
+ total_products: products.length,
147
420
  };
148
- const fallback = products.length
149
- ? `\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff1a${products.map((p) => p.name || TEXT.unknown).join('\u3001')}\n\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\n\u5269\u4f59\uff1a${roundInfo.countdown}`
150
- : '\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002';
421
+ const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
151
422
  return { products, roundInfo, data, fallback };
152
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);
458
+ }
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
+ };
491
+ }
153
492
  async function checkMerchantSubscriptions(deps) {
154
493
  const { ctx, client, merchantSubMgr, renderer, config } = deps;
155
494
  const res = await client.getMerchantInfo(ctx, true);
156
495
  if (!res)
157
496
  return { subscriptions: 0, matched: 0, pushed: 0 };
158
- const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
497
+ const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
159
498
  const productNames = products.map((p) => p.name || '').filter(Boolean);
160
- const rendered = await renderer.renderHtml(ctx, 'yuanxing-shangren', data);
499
+ const rendered = await renderer.renderHtml(ctx, templateName, data);
161
500
  const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
162
501
  const subs = merchantSubMgr.getAll();
163
502
  let matchedCount = 0;
@@ -214,10 +553,19 @@ function register(deps) {
214
553
  const res = await client.getMerchantInfo(ctx, true);
215
554
  if (!res)
216
555
  return `\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\uff1a${client.getLastErrorBrief()}`;
217
- const { data, fallback } = buildMerchantRenderPayload(res);
218
- const png = await deps.renderer.renderHtml(ctx, 'yuanxing-shangren', data);
556
+ const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
557
+ const png = await deps.renderer.renderHtml(ctx, templateName, data);
219
558
  await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
220
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
+ });
221
569
  ctx.command(`${TEXT.subscribe} [args:text]`, '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\u63d0\u9192')
222
570
  .action(async ({ session }, args) => {
223
571
  const target = getSubscriptionTarget(session);