koishi-plugin-rocom 1.0.11 → 1.0.12

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.
@@ -15,6 +15,49 @@ const TEXT = {
15
15
  defaultSource: '\u9ed8\u8ba4',
16
16
  customSource: '\u81ea\u5b9a\u4e49',
17
17
  };
18
+ const CATEGORY_ORDER = ['normal', 'round', 'weekend'];
19
+ const CATEGORY_LABELS = {
20
+ normal: '热销商品',
21
+ round: '常规商品',
22
+ weekend: '周末限定',
23
+ };
24
+ const CHINA_TIMEZONE = 'Asia/Shanghai';
25
+ const chinaPartsFormatter = new Intl.DateTimeFormat('zh-CN', {
26
+ timeZone: CHINA_TIMEZONE,
27
+ year: 'numeric',
28
+ month: '2-digit',
29
+ day: '2-digit',
30
+ hour: '2-digit',
31
+ minute: '2-digit',
32
+ hour12: false,
33
+ });
34
+ function getChinaParts(timestampMs) {
35
+ const parts = {};
36
+ for (const item of chinaPartsFormatter.formatToParts(new Date(timestampMs))) {
37
+ if (item.type !== 'literal')
38
+ parts[item.type] = item.value;
39
+ }
40
+ return {
41
+ hour: Number(parts.hour || '0'),
42
+ minute: Number(parts.minute || '0'),
43
+ };
44
+ }
45
+ function classifyMerchantItem(item) {
46
+ const start = normalizeTimestamp(item?.start_time);
47
+ const end = normalizeTimestamp(item?.end_time);
48
+ if (!start || !end)
49
+ return 'normal';
50
+ const durationDays = (end - start) / (1000 * 60 * 60 * 24);
51
+ if (durationDays >= 2)
52
+ return 'weekend';
53
+ const startParts = getChinaParts(start);
54
+ const endParts = getChinaParts(end);
55
+ const startHour = startParts.hour + startParts.minute / 60;
56
+ const endHour = endParts.hour + endParts.minute / 60;
57
+ if (startHour <= 8 && endHour >= 23.5)
58
+ return 'normal';
59
+ return 'round';
60
+ }
18
61
  function normalizeTimestamp(value) {
19
62
  if (value === null || value === undefined || value === '')
20
63
  return null;
@@ -54,13 +97,34 @@ function getMerchantActivity(res) {
54
97
  }
55
98
  function getActiveProducts(res) {
56
99
  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
- });
100
+ const groups = [];
101
+ if (Array.isArray(activity?.products))
102
+ groups.push(activity.products);
103
+ if (Array.isArray(activity?.product_list))
104
+ groups.push(activity.product_list);
105
+ if (Array.isArray(activity?.get_props))
106
+ groups.push(activity.get_props);
107
+ if (Array.isArray(activity?.get_extra_props))
108
+ groups.push(activity.get_extra_props);
109
+ if (Array.isArray(activity?.get_pets))
110
+ groups.push(activity.get_pets);
111
+ const merged = [];
112
+ const seen = new Set();
113
+ const now = Date.now();
114
+ for (const list of groups) {
115
+ for (const item of list) {
116
+ const start = normalizeTimestamp(item?.start_time) ?? 0;
117
+ const end = normalizeTimestamp(item?.end_time) ?? Infinity;
118
+ if (now < start || now >= end)
119
+ continue;
120
+ const key = `${item?.id ?? ''}|${item?.name ?? ''}|${start}|${end === Infinity ? 'inf' : end}`;
121
+ if (seen.has(key))
122
+ continue;
123
+ seen.add(key);
124
+ merged.push(item);
125
+ }
126
+ }
127
+ return merged;
64
128
  }
65
129
  function getCurrentMerchantRound() {
66
130
  const now = new Date();
@@ -132,23 +196,56 @@ function buildMerchantRenderPayload(res) {
132
196
  const products = getActiveProducts(res);
133
197
  const roundInfo = getCurrentMerchantRound();
134
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),
204
+ }));
205
+ const categoryMap = {
206
+ normal: [],
207
+ round: [],
208
+ weekend: [],
209
+ };
210
+ for (const product of renderedProducts) {
211
+ categoryMap[product.category].push(product);
212
+ }
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],
219
+ }));
135
220
  const data = {
136
221
  background: '',
137
222
  title: activity.name || TEXT.merchant,
138
223
  subtitle: activity.start_date || '\u6bcf\u65e5 08:00 / 12:00 / 16:00 / 20:00 \u5237\u65b0',
139
224
  titleIcon: true,
140
- product_count: products.length,
225
+ product_count: renderedProducts.length,
141
226
  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
- })),
227
+ categories,
228
+ products: renderedProducts,
147
229
  };
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';
151
- return { products, roundInfo, data, fallback };
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');
246
+ }
247
+ const fallback = fallbackLines.join('\n').trimEnd();
248
+ return { products: renderedProducts, roundInfo, data, fallback };
152
249
  }
153
250
  async function checkMerchantSubscriptions(deps) {
154
251
  const { ctx, client, merchantSubMgr, renderer, config } = deps;
@@ -258,16 +258,28 @@ function normalizeDurationSeconds(value) {
258
258
  function formatHomeRemaining(targetTs, nowTs = Math.floor(Date.now() / 1000)) {
259
259
  if (!targetTs)
260
260
  return '未开始';
261
- const remain = Math.max(0, targetTs - nowTs);
262
- if (remain <= 0)
261
+ if (nowTs >= targetTs)
263
262
  return '已完成';
264
- const hours = Math.floor(remain / 3600);
263
+ const remain = Math.max(0, targetTs - nowTs);
264
+ const days = Math.floor(remain / 86400);
265
+ const hours = Math.floor((remain % 86400) / 3600);
265
266
  const minutes = Math.floor((remain % 3600) / 60);
266
- if (hours >= 24)
267
- return `${Math.floor(hours / 24)}天${hours % 24}小时`;
267
+ const seconds = remain % 60;
268
+ if (days > 0)
269
+ return hours > 0 ? `${days}天${hours}小时` : `${days}天`;
268
270
  if (hours > 0)
269
271
  return `${hours}小时${minutes}分钟`;
270
- return `${minutes}分钟`;
272
+ if (minutes > 0)
273
+ return `${minutes}分${seconds}秒`;
274
+ return `${seconds}秒`;
275
+ }
276
+ function formatEggRemaining(targetTs, nowTs = Math.floor(Date.now() / 1000)) {
277
+ if (!targetTs || nowTs >= targetTs)
278
+ return '0分钟';
279
+ const remain = Math.max(0, targetTs - nowTs);
280
+ const totalHours = Math.floor(remain / 3600);
281
+ const minutes = Math.floor((remain % 3600) / 60);
282
+ return `${totalHours}小时${minutes}分钟`;
271
283
  }
272
284
  function homeInfoPayload(res) {
273
285
  const payload = res || {};
@@ -856,31 +868,59 @@ function extractHomePet(raw, index, guard = false) {
856
868
  const petId = homePet.pet_cfg_id || homePet.pet_id || homePet.pet_base_id || raw.pet_cfg_id || raw.pet_id || raw.id;
857
869
  if (['', '0'].includes(String(petId || '0')) && !guard)
858
870
  return null;
859
- const feedInfo = homePet.feed_info && typeof homePet.feed_info === 'object' ? homePet.feed_info : {};
860
- const beginTime = normalizeEpochSeconds(feedInfo.begin_time);
861
- const timeCost = normalizeDurationSeconds(feedInfo.time_cost);
862
- let readyAt = normalizeEpochSeconds(homePet.pet_rip_time || raw.pet_rip_time || raw.rip_time);
863
- if (!readyAt && beginTime && timeCost)
864
- readyAt = beginTime + timeCost;
865
871
  const nowTs = Math.floor(Date.now() / 1000);
866
- const hasInspiration = Boolean(readyAt);
867
- const inspireReady = hasInspiration && nowTs >= readyAt;
868
- const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ['2', 'guard', '守卫'].includes(String(raw.status).toLowerCase());
869
- const statusText = isGuard && !hasInspiration ? '守卫中' : inspireReady ? '灵感已完成' : hasInspiration ? '灵感收集中' : '未喂食';
870
- const statusClass = isGuard && !hasInspiration ? 'guard' : inspireReady ? 'ready' : hasInspiration ? 'progress' : 'idle';
872
+ const hasEgg = Boolean(raw.have_egg);
873
+ const predictedEggTime = normalizeEpochSeconds(raw.predicted_egg_time);
874
+ const eggReady = hasEgg || (predictedEggTime > 0 && nowTs >= predictedEggTime);
875
+ const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
876
+ const gender = Number(display.gender || raw.gender || 0) || 0;
877
+ const isMale = gender === 1;
878
+ const status = homePet.status ?? raw.status;
879
+ const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ['2', 'guard', '守卫'].includes(String(status).toLowerCase());
880
+ const hasInspiration = feedRound > 0;
881
+ const inspireReady = hasInspiration;
882
+ const statusText = isGuard && !hasInspiration ? '守卫中'
883
+ : (inspireReady ? '可收取灵感'
884
+ : (hasInspiration ? '灵感收集中'
885
+ : '未喂食'));
886
+ const statusClass = isGuard && !hasInspiration ? 'guard'
887
+ : (eggReady ? 'ready'
888
+ : (inspireReady ? 'progress'
889
+ : (hasInspiration ? 'progress'
890
+ : 'idle')));
891
+ let note;
892
+ if (isGuard && String(petId) === '0') {
893
+ note = '家园守卫位';
894
+ }
895
+ else if (eggReady) {
896
+ note = '可收取';
897
+ }
898
+ else if (predictedEggTime > 0) {
899
+ note = `${formatEggRemaining(predictedEggTime, nowTs)}后生蛋`;
900
+ }
901
+ else if (feedRound > 0) {
902
+ note = isMale ? '' : '等待生蛋';
903
+ }
904
+ else if (isGuard) {
905
+ note = '家园守卫位';
906
+ }
907
+ else {
908
+ note = '未喂食';
909
+ }
871
910
  return {
872
911
  id: String(petId || ''),
873
912
  pos: raw.pos || raw.position || index + 1,
874
913
  name: String(homePet.name || homePet.pet_name || raw.name || raw.pet_name || `精灵 ${petId || ''}`),
875
914
  level: display.level || raw.level || homePet.level || '--',
876
915
  iconUrl: homePetIcon(petId, raw.icon_url || raw.pet_img_url || raw.petIcon || ''),
877
- badge: isGuard ? '守' : '',
916
+ badge: isGuard ? '守' : (hasEgg ? '' : ''),
878
917
  isGuard,
879
918
  statusText,
880
919
  statusClass,
881
- note: hasInspiration ? formatHomeRemaining(readyAt, nowTs) : (isGuard ? '家园守卫位' : '暂无灵感倒计时'),
882
- inspireReady,
883
- readyAt,
920
+ note,
921
+ inspireReady: eggReady,
922
+ readyAt: predictedEggTime || 0,
923
+ gender,
884
924
  };
885
925
  }
886
926
  function homePetSources(homeInfo) {
@@ -1011,6 +1051,18 @@ function buildHomeRenderData(deps, res, uid) {
1011
1051
  if (item)
1012
1052
  guardPets.push(item);
1013
1053
  });
1054
+ indoorPets.sort((a, b) => {
1055
+ if (a.gender === 2 && b.gender !== 2)
1056
+ return -1;
1057
+ if (a.gender !== 2 && b.gender === 2)
1058
+ return 1;
1059
+ if (a.gender === 2) {
1060
+ const ta = a.readyAt || Number.MAX_SAFE_INTEGER;
1061
+ const tb = b.readyAt || Number.MAX_SAFE_INTEGER;
1062
+ return ta - tb;
1063
+ }
1064
+ return 0;
1065
+ });
1014
1066
  const gardenPlots = extractHomePlants(deps, homeInfo);
1015
1067
  const createdAt = normalizeEpochSeconds(res?.meta?.created_at);
1016
1068
  return {
package/lib/index.js CHANGED
@@ -2667,25 +2667,28 @@ function normalizeEpochSeconds(value) {
2667
2667
  return Math.floor(ts);
2668
2668
  }
2669
2669
  __name(normalizeEpochSeconds, "normalizeEpochSeconds");
2670
- function normalizeDurationSeconds(value) {
2671
- const seconds = Number(value);
2672
- if (!Number.isFinite(seconds)) return 0;
2673
- if (seconds > 1e9) return Math.floor(seconds / 1e6);
2674
- if (seconds > 1e6) return Math.floor(seconds / 1e3);
2675
- return Math.floor(seconds);
2676
- }
2677
- __name(normalizeDurationSeconds, "normalizeDurationSeconds");
2678
2670
  function formatHomeRemaining(targetTs, nowTs = Math.floor(Date.now() / 1e3)) {
2679
2671
  if (!targetTs) return "未开始";
2672
+ if (nowTs >= targetTs) return "已完成";
2680
2673
  const remain = Math.max(0, targetTs - nowTs);
2681
- if (remain <= 0) return "已完成";
2682
- const hours = Math.floor(remain / 3600);
2674
+ const days = Math.floor(remain / 86400);
2675
+ const hours = Math.floor(remain % 86400 / 3600);
2683
2676
  const minutes = Math.floor(remain % 3600 / 60);
2684
- if (hours >= 24) return `${Math.floor(hours / 24)}天${hours % 24}小时`;
2677
+ const seconds = remain % 60;
2678
+ if (days > 0) return hours > 0 ? `${days}天${hours}小时` : `${days}天`;
2685
2679
  if (hours > 0) return `${hours}小时${minutes}分钟`;
2686
- return `${minutes}分钟`;
2680
+ if (minutes > 0) return `${minutes}分${seconds}秒`;
2681
+ return `${seconds}秒`;
2687
2682
  }
2688
2683
  __name(formatHomeRemaining, "formatHomeRemaining");
2684
+ function formatEggRemaining(targetTs, nowTs = Math.floor(Date.now() / 1e3)) {
2685
+ if (!targetTs || nowTs >= targetTs) return "0分钟";
2686
+ const remain = Math.max(0, targetTs - nowTs);
2687
+ const totalHours = Math.floor(remain / 3600);
2688
+ const minutes = Math.floor(remain % 3600 / 60);
2689
+ return `${totalHours}小时${minutes}分钟`;
2690
+ }
2691
+ __name(formatEggRemaining, "formatEggRemaining");
2689
2692
  function homeInfoPayload(res) {
2690
2693
  const payload = res || {};
2691
2694
  if (payload.result?.home_info) return payload.result.home_info;
@@ -3259,30 +3262,47 @@ function extractHomePet(raw, index, guard = false) {
3259
3262
  const display = raw.display_info && typeof raw.display_info === "object" ? raw.display_info : {};
3260
3263
  const petId = homePet.pet_cfg_id || homePet.pet_id || homePet.pet_base_id || raw.pet_cfg_id || raw.pet_id || raw.id;
3261
3264
  if (["", "0"].includes(String(petId || "0")) && !guard) return null;
3262
- const feedInfo = homePet.feed_info && typeof homePet.feed_info === "object" ? homePet.feed_info : {};
3263
- const beginTime = normalizeEpochSeconds(feedInfo.begin_time);
3264
- const timeCost = normalizeDurationSeconds(feedInfo.time_cost);
3265
- let readyAt = normalizeEpochSeconds(homePet.pet_rip_time || raw.pet_rip_time || raw.rip_time);
3266
- if (!readyAt && beginTime && timeCost) readyAt = beginTime + timeCost;
3267
3265
  const nowTs = Math.floor(Date.now() / 1e3);
3268
- const hasInspiration = Boolean(readyAt);
3269
- const inspireReady = hasInspiration && nowTs >= readyAt;
3270
- const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ["2", "guard", "守卫"].includes(String(raw.status).toLowerCase());
3271
- const statusText = isGuard && !hasInspiration ? "守卫中" : inspireReady ? "灵感已完成" : hasInspiration ? "灵感收集中" : "未喂食";
3272
- const statusClass = isGuard && !hasInspiration ? "guard" : inspireReady ? "ready" : hasInspiration ? "progress" : "idle";
3266
+ const hasEgg = Boolean(raw.have_egg);
3267
+ const predictedEggTime = normalizeEpochSeconds(raw.predicted_egg_time);
3268
+ const eggReady = hasEgg || predictedEggTime > 0 && nowTs >= predictedEggTime;
3269
+ const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
3270
+ const gender = Number(display.gender || raw.gender || 0) || 0;
3271
+ const isMale = gender === 1;
3272
+ const status = homePet.status ?? raw.status;
3273
+ const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ["2", "guard", "守卫"].includes(String(status).toLowerCase());
3274
+ const hasInspiration = feedRound > 0;
3275
+ const inspireReady = hasInspiration;
3276
+ const statusText = isGuard && !hasInspiration ? "守卫中" : inspireReady ? "可收取灵感" : hasInspiration ? "灵感收集中" : "未喂食";
3277
+ const statusClass = isGuard && !hasInspiration ? "guard" : eggReady ? "ready" : inspireReady ? "progress" : hasInspiration ? "progress" : "idle";
3278
+ let note;
3279
+ if (isGuard && String(petId) === "0") {
3280
+ note = "家园守卫位";
3281
+ } else if (eggReady) {
3282
+ note = "可收取";
3283
+ } else if (predictedEggTime > 0) {
3284
+ note = `${formatEggRemaining(predictedEggTime, nowTs)}后生蛋`;
3285
+ } else if (feedRound > 0) {
3286
+ note = isMale ? "" : "等待生蛋";
3287
+ } else if (isGuard) {
3288
+ note = "家园守卫位";
3289
+ } else {
3290
+ note = "未喂食";
3291
+ }
3273
3292
  return {
3274
3293
  id: String(petId || ""),
3275
3294
  pos: raw.pos || raw.position || index + 1,
3276
3295
  name: String(homePet.name || homePet.pet_name || raw.name || raw.pet_name || `精灵 ${petId || ""}`),
3277
3296
  level: display.level || raw.level || homePet.level || "--",
3278
3297
  iconUrl: homePetIcon(petId, raw.icon_url || raw.pet_img_url || raw.petIcon || ""),
3279
- badge: isGuard ? "守" : "",
3298
+ badge: isGuard ? "守" : hasEgg ? "" : "",
3280
3299
  isGuard,
3281
3300
  statusText,
3282
3301
  statusClass,
3283
- note: hasInspiration ? formatHomeRemaining(readyAt, nowTs) : isGuard ? "家园守卫位" : "暂无灵感倒计时",
3284
- inspireReady,
3285
- readyAt
3302
+ note,
3303
+ inspireReady: eggReady,
3304
+ readyAt: predictedEggTime || 0,
3305
+ gender
3286
3306
  };
3287
3307
  }
3288
3308
  __name(extractHomePet, "extractHomePet");
@@ -3399,6 +3419,16 @@ function buildHomeRenderData(deps, res, uid) {
3399
3419
  const item = extractHomePet(raw, index, true);
3400
3420
  if (item) guardPets.push(item);
3401
3421
  });
3422
+ indoorPets.sort((a, b) => {
3423
+ if (a.gender === 2 && b.gender !== 2) return -1;
3424
+ if (a.gender !== 2 && b.gender === 2) return 1;
3425
+ if (a.gender === 2) {
3426
+ const ta = a.readyAt || Number.MAX_SAFE_INTEGER;
3427
+ const tb = b.readyAt || Number.MAX_SAFE_INTEGER;
3428
+ return ta - tb;
3429
+ }
3430
+ return 0;
3431
+ });
3402
3432
  const gardenPlots = extractHomePlants(deps, homeInfo);
3403
3433
  const createdAt = normalizeEpochSeconds(res?.meta?.created_at);
3404
3434
  return {
@@ -4165,6 +4195,47 @@ var TEXT = {
4165
4195
  defaultSource: "默认",
4166
4196
  customSource: "自定义"
4167
4197
  };
4198
+ var CATEGORY_ORDER = ["normal", "round", "weekend"];
4199
+ var CATEGORY_LABELS = {
4200
+ normal: "热销商品",
4201
+ round: "常规商品",
4202
+ weekend: "周末限定"
4203
+ };
4204
+ var CHINA_TIMEZONE = "Asia/Shanghai";
4205
+ var chinaPartsFormatter = new Intl.DateTimeFormat("zh-CN", {
4206
+ timeZone: CHINA_TIMEZONE,
4207
+ year: "numeric",
4208
+ month: "2-digit",
4209
+ day: "2-digit",
4210
+ hour: "2-digit",
4211
+ minute: "2-digit",
4212
+ hour12: false
4213
+ });
4214
+ function getChinaParts(timestampMs) {
4215
+ const parts = {};
4216
+ for (const item of chinaPartsFormatter.formatToParts(new Date(timestampMs))) {
4217
+ if (item.type !== "literal") parts[item.type] = item.value;
4218
+ }
4219
+ return {
4220
+ hour: Number(parts.hour || "0"),
4221
+ minute: Number(parts.minute || "0")
4222
+ };
4223
+ }
4224
+ __name(getChinaParts, "getChinaParts");
4225
+ function classifyMerchantItem(item) {
4226
+ const start = normalizeTimestamp(item?.start_time);
4227
+ const end = normalizeTimestamp(item?.end_time);
4228
+ if (!start || !end) return "normal";
4229
+ const durationDays = (end - start) / (1e3 * 60 * 60 * 24);
4230
+ if (durationDays >= 2) return "weekend";
4231
+ const startParts = getChinaParts(start);
4232
+ const endParts = getChinaParts(end);
4233
+ const startHour = startParts.hour + startParts.minute / 60;
4234
+ const endHour = endParts.hour + endParts.minute / 60;
4235
+ if (startHour <= 8 && endHour >= 23.5) return "normal";
4236
+ return "round";
4237
+ }
4238
+ __name(classifyMerchantItem, "classifyMerchantItem");
4168
4239
  function normalizeTimestamp(value) {
4169
4240
  if (value === null || value === void 0 || value === "") return null;
4170
4241
  const timestamp = Number(value);
@@ -4203,13 +4274,27 @@ function getMerchantActivity(res) {
4203
4274
  __name(getMerchantActivity, "getMerchantActivity");
4204
4275
  function getActiveProducts(res) {
4205
4276
  const activity = getMerchantActivity(res);
4206
- const products = activity?.products || activity?.product_list || activity?.get_props || [];
4207
- return products.filter((p) => {
4208
- const now = Date.now();
4209
- const start = normalizeTimestamp(p.start_time) ?? 0;
4210
- const end = normalizeTimestamp(p.end_time) ?? Infinity;
4211
- return now >= start && now < end;
4212
- });
4277
+ const groups = [];
4278
+ if (Array.isArray(activity?.products)) groups.push(activity.products);
4279
+ if (Array.isArray(activity?.product_list)) groups.push(activity.product_list);
4280
+ if (Array.isArray(activity?.get_props)) groups.push(activity.get_props);
4281
+ if (Array.isArray(activity?.get_extra_props)) groups.push(activity.get_extra_props);
4282
+ if (Array.isArray(activity?.get_pets)) groups.push(activity.get_pets);
4283
+ const merged = [];
4284
+ const seen = /* @__PURE__ */ new Set();
4285
+ const now = Date.now();
4286
+ for (const list of groups) {
4287
+ for (const item of list) {
4288
+ const start = normalizeTimestamp(item?.start_time) ?? 0;
4289
+ const end = normalizeTimestamp(item?.end_time) ?? Infinity;
4290
+ if (now < start || now >= end) continue;
4291
+ const key = `${item?.id ?? ""}|${item?.name ?? ""}|${start}|${end === Infinity ? "inf" : end}`;
4292
+ if (seen.has(key)) continue;
4293
+ seen.add(key);
4294
+ merged.push(item);
4295
+ }
4296
+ }
4297
+ return merged;
4213
4298
  }
4214
4299
  __name(getActiveProducts, "getActiveProducts");
4215
4300
  function getCurrentMerchantRound() {
@@ -4284,23 +4369,53 @@ function buildMerchantRenderPayload(res) {
4284
4369
  const products = getActiveProducts(res);
4285
4370
  const roundInfo = getCurrentMerchantRound();
4286
4371
  const activity = getMerchantActivity(res);
4372
+ const renderedProducts = products.map((p) => ({
4373
+ name: p.name || TEXT.unknown,
4374
+ image: p.icon_url || "",
4375
+ time_label: formatProductWindow(p),
4376
+ category: classifyMerchantItem(p)
4377
+ }));
4378
+ const categoryMap = {
4379
+ normal: [],
4380
+ round: [],
4381
+ weekend: []
4382
+ };
4383
+ for (const product of renderedProducts) {
4384
+ categoryMap[product.category].push(product);
4385
+ }
4386
+ const categories = CATEGORY_ORDER.filter((key) => categoryMap[key].length > 0).map((key) => ({
4387
+ key,
4388
+ label: CATEGORY_LABELS[key],
4389
+ products: categoryMap[key]
4390
+ }));
4287
4391
  const data = {
4288
4392
  background: "",
4289
4393
  title: activity.name || TEXT.merchant,
4290
4394
  subtitle: activity.start_date || "每日 08:00 / 12:00 / 16:00 / 20:00 刷新",
4291
4395
  titleIcon: true,
4292
- product_count: products.length,
4396
+ product_count: renderedProducts.length,
4293
4397
  round_info: roundInfo,
4294
- products: products.map((p) => ({
4295
- name: p.name || TEXT.unknown,
4296
- image: p.icon_url || "",
4297
- time_label: formatProductWindow(p)
4298
- }))
4398
+ categories,
4399
+ products: renderedProducts
4299
4400
  };
4300
- const fallback = products.length ? `远行商人当前商品:${products.map((p) => p.name || TEXT.unknown).join("、")}
4301
- 轮次:${roundInfo.current || TEXT.notOpen}
4302
- 剩余:${roundInfo.countdown}` : "当前远行商人暂无商品。";
4303
- return { products, roundInfo, data, fallback };
4401
+ const fallbackLines = [];
4402
+ if (renderedProducts.length) {
4403
+ fallbackLines.push(`远行商人当前商品(共 ${renderedProducts.length} 件)`);
4404
+ fallbackLines.push(`轮次:${roundInfo.current || TEXT.notOpen},剩余:${roundInfo.countdown}`);
4405
+ fallbackLines.push("");
4406
+ for (const cat of categories) {
4407
+ fallbackLines.push(`【${cat.label}】`);
4408
+ cat.products.forEach((product, index) => {
4409
+ const tail = product.time_label ? ` (${product.time_label})` : "";
4410
+ fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
4411
+ });
4412
+ fallbackLines.push("");
4413
+ }
4414
+ } else {
4415
+ fallbackLines.push("当前远行商人暂无商品。");
4416
+ }
4417
+ const fallback = fallbackLines.join("\n").trimEnd();
4418
+ return { products: renderedProducts, roundInfo, data, fallback };
4304
4419
  }
4305
4420
  __name(buildMerchantRenderPayload, "buildMerchantRenderPayload");
4306
4421
  async function checkMerchantSubscriptions(deps) {
@@ -4631,7 +4746,7 @@ __name(register5, "register");
4631
4746
  var import_koishi10 = require("koishi");
4632
4747
 
4633
4748
  // src/activities-service.ts
4634
- var CHINA_TIMEZONE = "Asia/Shanghai";
4749
+ var CHINA_TIMEZONE2 = "Asia/Shanghai";
4635
4750
  var DAY_MS = 24 * 60 * 60 * 1e3;
4636
4751
  var ACTIVITY_THEMES = ["gold", "green", "brown"];
4637
4752
  var LOOKBACK_DAYS = 10;
@@ -4639,12 +4754,12 @@ var MAX_LOOKAHEAD_DAYS = 50;
4639
4754
  var TRAILING_DAYS_AFTER_LAST_ACTIVITY = 3;
4640
4755
  var MIN_LOOKAHEAD_DAYS = 7;
4641
4756
  var chinaDateFormatter = new Intl.DateTimeFormat("zh-CN", {
4642
- timeZone: CHINA_TIMEZONE,
4757
+ timeZone: CHINA_TIMEZONE2,
4643
4758
  month: "2-digit",
4644
4759
  day: "2-digit"
4645
4760
  });
4646
4761
  var chinaDateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
4647
- timeZone: CHINA_TIMEZONE,
4762
+ timeZone: CHINA_TIMEZONE2,
4648
4763
  month: "2-digit",
4649
4764
  day: "2-digit",
4650
4765
  hour: "2-digit",
@@ -4652,7 +4767,7 @@ var chinaDateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
4652
4767
  hour12: false
4653
4768
  });
4654
4769
  var chinaDatePartsFormatter = new Intl.DateTimeFormat("zh-CN", {
4655
- timeZone: CHINA_TIMEZONE,
4770
+ timeZone: CHINA_TIMEZONE2,
4656
4771
  year: "numeric",
4657
4772
  month: "2-digit",
4658
4773
  day: "2-digit"
@@ -31,6 +31,16 @@
31
31
  --card-bg: rgba(255, 252, 247, 0.82);
32
32
  --tag-bg: rgba(255, 214, 153, 0.4);
33
33
  --tag-text: #9a5f19;
34
+ --accent-color: #a0631d;
35
+ --warning-color: #bf5f3f;
36
+ --border-color: rgba(179, 123, 45, 0.2);
37
+ --border-color-light: rgba(118, 97, 74, 0.14);
38
+ --weekend-color: #c04030;
39
+ --normal-color: #2a8040;
40
+ --round-color: #2a60b0;
41
+ --weekend-bg: rgba(220, 80, 60, 0.15);
42
+ --normal-bg: rgba(60, 160, 80, 0.15);
43
+ --round-bg: rgba(60, 120, 220, 0.15);
34
44
  }
35
45
 
36
46
  html,
@@ -39,7 +49,7 @@ body {
39
49
  height: auto;
40
50
  font-family: var(--font-family);
41
51
  color: var(--text-1);
42
- background: #ece3d3;
52
+ background: linear-gradient(135deg, #fff8eb, #fff0d2);
43
53
  }
44
54
 
45
55
  .merchant-page {
@@ -47,7 +57,7 @@ body {
47
57
  width: min(1280px, 100%);
48
58
  margin: 0 auto;
49
59
  overflow: hidden;
50
- background: #ece3d3;
60
+ background: linear-gradient(135deg, #fff8eb, #fff0d2);
51
61
  }
52
62
 
53
63
  .background {
@@ -133,14 +143,14 @@ body {
133
143
  padding: 10px 16px;
134
144
  border-radius: 999px;
135
145
  background: rgba(255,255,255,0.72);
136
- border: 1px solid rgba(179, 123, 45, 0.2);
146
+ border: 1px solid var(--border-color);
137
147
  color: var(--text-2);
138
148
  font-size: 18px;
139
149
  }
140
150
 
141
151
  .summary-chip strong {
142
152
  font-family: var(--font-family-accent);
143
- color: #a0631d;
153
+ color: var(--accent-color);
144
154
  font-size: 28px;
145
155
  margin-left: 8px;
146
156
  }
@@ -163,14 +173,14 @@ body {
163
173
  }
164
174
 
165
175
  .countdown-pill {
166
- color: #bf5f3f;
176
+ color: var(--warning-color);
167
177
  font-weight: 700;
168
178
  }
169
179
 
170
180
  .products-grid {
171
- display: grid;
172
- grid-template-columns: 1fr;
173
- gap: 16px;
181
+ display: flex;
182
+ flex-direction: column;
183
+ gap: 20px;
174
184
  margin-top: 18px;
175
185
  }
176
186
 
@@ -181,17 +191,17 @@ body {
181
191
  align-items: center;
182
192
  min-height: 148px;
183
193
  padding: 18px 28px;
184
- border: 1px solid rgba(118, 97, 74, 0.14);
194
+ border: 1px solid rgba(200, 140, 40, 0.4);
185
195
  border-radius: 22px;
186
- background: linear-gradient(135deg, rgba(255,255,255,0.72), rgba(244,236,224,0.88));
187
- box-shadow: 0 10px 26px rgba(58, 39, 21, 0.06);
196
+ background: linear-gradient(135deg, rgba(255,248,235,0.88), rgba(255,240,210,0.9));
197
+ box-shadow: 0 6px 22px rgba(180, 120, 20, 0.08);
188
198
  }
189
199
 
190
200
  .product-image-container {
191
201
  width: 120px;
192
202
  height: 120px;
193
203
  border-radius: 50%;
194
- background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.94), rgba(239,228,210,0.92));
204
+ background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.94), rgba(255,240,210,0.92));
195
205
  display: flex;
196
206
  align-items: center;
197
207
  justify-content: center;
@@ -244,8 +254,8 @@ body {
244
254
  padding: 10px 14px;
245
255
  border-radius: 16px;
246
256
  background: rgba(255,255,255,0.72);
247
- border: 1px solid rgba(179, 123, 45, 0.18);
248
- color: #8b5e2b;
257
+ border: 1px solid var(--border-color);
258
+ color: var(--accent-color);
249
259
  font-size: 18px;
250
260
  font-weight: 700;
251
261
  }
@@ -254,10 +264,53 @@ body {
254
264
  text-align: center;
255
265
  padding: 30px 18px;
256
266
  border-radius: 22px;
257
- border: 1px dashed rgba(120, 102, 81, 0.35);
267
+ border: 1px dashed rgba(200, 140, 40, 0.35);
258
268
  color: #5c4a37;
259
269
  font-size: 22px;
260
- background: rgba(255,255,255,0.52);
270
+ background: rgba(255,248,235,0.6);
271
+ }
272
+
273
+ .category-section {
274
+ margin-top: 16px;
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 14px;
278
+ }
279
+
280
+ .category-header {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 10px;
284
+ padding: 0 4px;
285
+ }
286
+
287
+ .category-title {
288
+ font-family: var(--font-family-accent);
289
+ font-size: 22px;
290
+ font-weight: 800;
291
+ color: var(--text-1);
292
+ }
293
+
294
+ .category-tag {
295
+ padding: 3px 10px;
296
+ border-radius: 999px;
297
+ font-size: 14px;
298
+ font-weight: 700;
299
+ }
300
+
301
+ .category-tag.weekend {
302
+ background: var(--weekend-bg);
303
+ color: var(--weekend-color);
304
+ }
305
+
306
+ .category-tag.normal {
307
+ background: var(--normal-bg);
308
+ color: var(--normal-color);
309
+ }
310
+
311
+ .category-tag.round {
312
+ background: var(--round-bg);
313
+ color: var(--round-color);
261
314
  }
262
315
 
263
316
  @media (max-width: 760px) {
@@ -345,20 +398,24 @@ body {
345
398
  </div>
346
399
 
347
400
  <div class="products-grid">
348
- {{if products && products.length > 0}}
349
- {{each products product}}
350
- <div class="product-card">
351
- <div class="product-image-container">
352
- <img class="product-image" src="{{product.image}}" alt="{{product.name}}">
353
- </div>
354
- <div class="product-main">
355
- <div class="product-name">{{product.name}}</div>
356
- <div class="product-sub">远行商人当前轮次商品</div>
357
- <div class="product-time">北京时间 {{product.time_label}}</div>
401
+ {{if categories && categories.length > 0}}
402
+ {{each categories cat}}
403
+ <div class="category-section">
404
+ <div class="category-header">
405
+ <span class="category-title">{{cat.label}}</span>
406
+ <span class="category-tag {{cat.key}}">{{cat.products.length}} 件</span>
358
407
  </div>
359
- <div class="product-side">
360
- <div class="product-slot">本轮商品</div>
408
+ {{each cat.products product}}
409
+ <div class="product-card">
410
+ <div class="product-image-container">
411
+ <img class="product-image" src="{{product.image}}" alt="{{product.name}}">
412
+ </div>
413
+ <div class="product-main">
414
+ <div class="product-name">{{product.name}}</div>
415
+ <div class="product-time">北京时间 {{product.time_label}}</div>
416
+ </div>
361
417
  </div>
418
+ {{/each}}
362
419
  </div>
363
420
  {{/each}}
364
421
  {{else}}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-rocom",
3
3
  "description": "洛克王国查询与订阅插件",
4
- "version": "1.0.11",
4
+ "version": "1.0.12",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "homepage": "https://github.com/staytomorrow/koishi-plugin-rocom",