koishi-plugin-rocom 1.0.12 → 1.0.17

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 (69) hide show
  1. package/lib/atlas-service.d.ts +33 -0
  2. package/lib/atlas-service.js +439 -0
  3. package/lib/client.d.ts +36 -1
  4. package/lib/client.js +221 -21
  5. package/lib/commands/account.js +1 -0
  6. package/lib/commands/merchant.js +351 -91
  7. package/lib/commands/query.js +162 -14
  8. package/lib/commands/tools.js +104 -0
  9. package/lib/commands/wiki.js +167 -6
  10. package/lib/egg-service.d.ts +2 -0
  11. package/lib/egg-service.js +50 -15
  12. package/lib/index.d.ts +9 -0
  13. package/lib/index.js +3626 -303
  14. package/lib/pet-data-service.d.ts +62 -0
  15. package/lib/pet-data-service.js +468 -0
  16. package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
  17. package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
  18. package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
  19. package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
  20. package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
  21. package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
  22. package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
  23. package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
  24. package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
  25. package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
  26. package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
  27. package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
  28. package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
  29. package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
  30. package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
  31. package/lib/render-templates/home/index.html +81 -97
  32. package/lib/render-templates/home/style.css +316 -375
  33. package/lib/render-templates/pet-data/index.html +172 -0
  34. package/lib/render-templates/pet-data/style.css +553 -0
  35. package/lib/render-templates/wiki/detail/index.html +84 -0
  36. package/lib/render-templates/wiki/list/index.html +84 -0
  37. package/lib/render-templates/wiki/menu/index.html +65 -0
  38. package/lib/render-templates/wiki/pet/index.html +158 -0
  39. package/lib/render-templates/wiki/pet/style.css +393 -0
  40. package/lib/render-templates/wiki/shared/style.css +431 -0
  41. package/lib/render-templates/wiki/skill/index.html +79 -0
  42. package/lib/render-templates/wiki/skill/style.css +233 -0
  43. package/lib/render-templates/wiki/suggestions/index.html +84 -0
  44. package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
  45. package/lib/render-templates/yuanxing-shangren/README.md +86 -0
  46. package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
  47. 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
  48. package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
  49. package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
  50. 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
  51. package/lib/render-templates/yuanxing-shangren/index.html +198 -170
  52. package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
  53. package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
  54. package/lib/render-templates/yuanxing-shangren/style.css +201 -256
  55. package/lib/render-templates/yuanxing-shangren/today.html +461 -0
  56. package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
  57. package/lib/render.js +48 -12
  58. package/lib/subscription-send.js +4 -5
  59. package/lib/ttf/rocom_skill_origin.ttf +0 -0
  60. 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
  61. package/lib/types.d.ts +13 -1
  62. package/lib/user.d.ts +23 -1
  63. package/lib/user.js +32 -1
  64. package/lib/wiki-catalog.d.ts +19 -0
  65. package/lib/wiki-catalog.js +124 -0
  66. package/lib/wiki-service.d.ts +220 -0
  67. package/lib/wiki-service.js +1463 -0
  68. package/package.json +1 -1
  69. package/readme.md +4 -5
@@ -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(timestampMs) {
42
+ function getChinaParts(input = Date.now()) {
35
43
  const parts = {};
36
- for (const item of chinaPartsFormatter.formatToParts(new Date(timestampMs))) {
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 date = new Date(timestamp);
76
- const month = String(date.getMonth() + 1).padStart(2, '0');
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 date = new Date(timestamp);
82
- const hour = String(date.getHours()).padStart(2, '0');
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 getActiveProducts(res) {
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 getCurrentMerchantRound() {
130
- const now = new Date();
131
- const hour = now.getHours();
132
- const rounds = [8, 12, 16, 20];
133
- let currentRound = null;
134
- let nextRound = null;
135
- for (let i = 0; i < rounds.length; i++) {
136
- if (hour >= rounds[i] && (i === rounds.length - 1 || hour < rounds[i + 1])) {
137
- currentRound = i + 1;
138
- nextRound = i < rounds.length - 1 ? rounds[i + 1] : rounds[0];
139
- break;
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
- if (currentRound === null)
143
- nextRound = rounds[0];
144
- const nextTime = new Date(now);
145
- if (nextRound <= hour)
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: rounds.length,
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 buildMerchantRenderPayload(res) {
196
- const products = getActiveProducts(res);
197
- const roundInfo = getCurrentMerchantRound();
198
- const activity = getMerchantActivity(res);
199
- const renderedProducts = products.map((p) => ({
200
- name: p.name || TEXT.unknown,
201
- image: p.icon_url || '',
202
- time_label: formatProductWindow(p),
203
- category: classifyMerchantItem(p),
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 categoryMap = {
206
- normal: [],
207
- round: [],
208
- weekend: [],
209
- };
210
- for (const product of renderedProducts) {
211
- categoryMap[product.category].push(product);
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
- const categories = CATEGORY_ORDER
214
- .filter(key => categoryMap[key].length > 0)
215
- .map(key => ({
216
- key,
217
- label: CATEGORY_LABELS[key],
218
- products: categoryMap[key],
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: activity.name || TEXT.merchant,
223
- subtitle: activity.start_date || '\u6bcf\u65e5 08:00 / 12:00 / 16:00 / 20:00 \u5237\u65b0',
224
- titleIcon: true,
225
- product_count: renderedProducts.length,
226
- round_info: roundInfo,
415
+ title: getMerchantActivityTitle(res),
416
+ subtitle: getMerchantActivitySubtitle(res),
227
417
  categories,
228
- products: renderedProducts,
418
+ roundGroups,
419
+ total_products: products.length,
229
420
  };
230
- const fallbackLines = [];
231
- if (renderedProducts.length) {
232
- fallbackLines.push(`\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff08\u5171 ${renderedProducts.length} \u4ef6\uff09`);
233
- fallbackLines.push(`\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\uff0c\u5269\u4f59\uff1a${roundInfo.countdown}`);
234
- fallbackLines.push('');
235
- for (const cat of categories) {
236
- fallbackLines.push(`\u3010${cat.label}\u3011`);
237
- cat.products.forEach((product, index) => {
238
- const tail = product.time_label ? ` (${product.time_label})` : '';
239
- fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
240
- });
241
- fallbackLines.push('');
242
- }
243
- }
244
- else {
245
- fallbackLines.push('\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002');
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 fallback = fallbackLines.join('\n').trimEnd();
248
- return { products: renderedProducts, roundInfo, data, fallback };
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 } = buildMerchantRenderPayload(res);
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, 'yuanxing-shangren', data);
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;
@@ -307,14 +549,24 @@ async function checkMerchantSubscriptions(deps) {
307
549
  function register(deps) {
308
550
  const { ctx, config, client, merchantSubMgr } = deps;
309
551
  ctx.command(TEXT.merchant, '\u67e5\u770b\u8fdc\u884c\u5546\u4eba\u5546\u54c1')
552
+ .alias('yxsr')
310
553
  .action(async ({ session }) => {
311
554
  const res = await client.getMerchantInfo(ctx, true);
312
555
  if (!res)
313
556
  return `\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\uff1a${client.getLastErrorBrief()}`;
314
- const { data, fallback } = buildMerchantRenderPayload(res);
315
- const png = await deps.renderer.renderHtml(ctx, 'yuanxing-shangren', data);
557
+ const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
558
+ const png = await deps.renderer.renderHtml(ctx, templateName, data);
316
559
  await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
317
560
  });
561
+ ctx.command(TEXT.todayMerchant, '查看今日远行商人全部商品')
562
+ .action(async ({ session }) => {
563
+ const res = await client.getMerchantInfo(ctx, true);
564
+ if (!res)
565
+ return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
566
+ const { data, fallback, templateName } = buildConfiguredTodayMerchantRenderPayload(deps, res);
567
+ const png = await deps.renderer.renderHtml(ctx, templateName, data);
568
+ await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren:today', deps.config);
569
+ });
318
570
  ctx.command(`${TEXT.subscribe} [args:text]`, '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\u63d0\u9192')
319
571
  .action(async ({ session }, args) => {
320
572
  const target = getSubscriptionTarget(session);
@@ -368,6 +620,14 @@ function register(deps) {
368
620
  return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
369
621
  });
370
622
  if (config.merchantSubscriptionEnabled) {
623
+ // 上游 v3.1.0:检查前加入 ±30s 随机延迟,降低刷新窗口和并发请求冲突。
624
+ const MERCHANT_JITTER_MS = 30000;
625
+ const runCheckWithJitter = () => {
626
+ const jitter = Math.floor(Math.random() * MERCHANT_JITTER_MS);
627
+ setTimeout(() => {
628
+ checkMerchantSubscriptions(deps).catch(err => logger.warn(`远行商人订阅检查失败: ${err}`));
629
+ }, jitter);
630
+ };
371
631
  if (config.merchantCheckMode === 'times' && config.merchantCheckTimes.length > 0) {
372
632
  let lastMerchantCheckKey = '';
373
633
  ctx.setInterval(async () => {
@@ -379,12 +639,12 @@ function register(deps) {
379
639
  if (checkKey === lastMerchantCheckKey)
380
640
  return;
381
641
  lastMerchantCheckKey = checkKey;
382
- await checkMerchantSubscriptions(deps);
642
+ runCheckWithJitter();
383
643
  }, 60000);
384
644
  }
385
645
  else {
386
646
  ctx.setInterval(async () => {
387
- await checkMerchantSubscriptions(deps);
647
+ runCheckWithJitter();
388
648
  }, config.merchantCheckInterval);
389
649
  }
390
650
  }