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.
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 +340 -89
  4. package/lib/commands/query.js +22 -6
  5. package/lib/index.d.ts +4 -0
  6. package/lib/index.js +480 -122
  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 +198 -170
  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',
@@ -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;
@@ -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 } = buildMerchantRenderPayload(res);
315
- 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);
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);
@@ -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
- const res = await client.ingameHomeInfo(ctx, targetUid);
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;