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
package/lib/index.js CHANGED
@@ -43,7 +43,10 @@ var import_node_path5 = __toESM(require("node:path"));
43
43
  // src/client.ts
44
44
  var import_koishi = require("koishi");
45
45
  var logger = new import_koishi.Logger("rocom-client");
46
- var RocomClient = class {
46
+ var INGAME_PENDING_STATUSES = ["queued", "pending", "running", "processing", "accepted"];
47
+ var INGAME_FAILED_STATUSES = ["failed", "error", "timeout", "cancelled", "canceled"];
48
+ var INGAME_COMPLETED_STATUSES = ["done", "success", "succeeded", "completed", "finished"];
49
+ var RocomClient = class _RocomClient {
47
50
  static {
48
51
  __name(this, "RocomClient");
49
52
  }
@@ -362,6 +365,97 @@ ${this.stringifyForLog(details)}`);
362
365
  { acceptedStatuses: [200, 202] }
363
366
  );
364
367
  }
368
+ static normalizeTaskStatus(value) {
369
+ return String(value ?? "").trim().toLowerCase();
370
+ }
371
+ static extractTaskId(payload) {
372
+ return String(payload?.task_id || payload?.taskId || payload?.taskID || "").trim();
373
+ }
374
+ // 判断一份 payload 是否为“已完成的业务结果”(而非排队占位)。
375
+ // 家园接口完成时返回 rows/home_info;玩家、商店完成时返回 source/title/rows。
376
+ static isCompletedGatewayPayload(payload) {
377
+ if (!payload || typeof payload !== "object") return false;
378
+ if (Array.isArray(payload.rows)) return true;
379
+ if (payload.home_info !== void 0) return true;
380
+ if (payload.source !== void 0) return true;
381
+ if (String(payload.title || "").trim()) return true;
382
+ return false;
383
+ }
384
+ static extractCompletedIngamePayload(payload) {
385
+ if (!payload || typeof payload !== "object") return null;
386
+ if (_RocomClient.isCompletedGatewayPayload(payload)) return payload;
387
+ if (_RocomClient.isCompletedGatewayPayload(payload.result)) return payload.result;
388
+ if (_RocomClient.isCompletedGatewayPayload(payload.data)) return payload.data;
389
+ return null;
390
+ }
391
+ static taskErrorMessage(payload, fallbackStatus = "") {
392
+ const candidates = [
393
+ payload?.message,
394
+ payload?.error,
395
+ payload?.error_message,
396
+ payload?.reason,
397
+ payload?.result?.message,
398
+ payload?.result?.error,
399
+ payload?.result?.error_message,
400
+ payload?.data?.message,
401
+ payload?.data?.error
402
+ ];
403
+ for (const candidate of candidates) {
404
+ const text = String(candidate ?? "").trim();
405
+ if (text && text.toLowerCase() !== "failed") return text;
406
+ }
407
+ return String(payload?.status || fallbackStatus || "").trim() || "任务执行失败";
408
+ }
409
+ // 排队等候轮询:把 ingame 接口提交后返回的 202(task_id/status=queued)轮询到出结果或超时。
410
+ // 入参 first 是首请求拿到的 { status, data, usedApiKey }。
411
+ async pollIngameTask(ctx, first, options, labels) {
412
+ const { status, data, usedApiKey } = first;
413
+ const initialCompletedPayload = _RocomClient.extractCompletedIngamePayload(data);
414
+ if (status !== null && initialCompletedPayload) return initialCompletedPayload;
415
+ const taskId = _RocomClient.extractTaskId(data);
416
+ if (!taskId) {
417
+ if (status === 202) this.setLastError(labels.queuedNoTaskId);
418
+ return status === null ? null : data;
419
+ }
420
+ if (typeof options.onQueued === "function") {
421
+ try {
422
+ await options.onQueued(taskId);
423
+ } catch (e) {
424
+ logger.warn(`ingame 排队提示回调失败: ${e}`);
425
+ }
426
+ }
427
+ const intervalMs = Math.max(300, Number(options.intervalMs) || 3e3);
428
+ const timeoutMs = Math.max(intervalMs, Number(options.timeoutMs) || 18e4);
429
+ const startedAt = Date.now();
430
+ let lastStatus = _RocomClient.normalizeTaskStatus(data?.status);
431
+ while (Date.now() - startedAt < timeoutMs) {
432
+ if (lastStatus && INGAME_FAILED_STATUSES.includes(lastStatus)) {
433
+ this.setLastError(_RocomClient.taskErrorMessage(data, lastStatus));
434
+ return null;
435
+ }
436
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
437
+ const task = await this.getIngameTask(ctx, taskId, usedApiKey);
438
+ if (task.status === null) return null;
439
+ const completedPayload = _RocomClient.extractCompletedIngamePayload(task.data);
440
+ if (completedPayload) return completedPayload;
441
+ lastStatus = _RocomClient.normalizeTaskStatus(task.data?.status);
442
+ if (INGAME_FAILED_STATUSES.includes(lastStatus)) {
443
+ this.setLastError(_RocomClient.taskErrorMessage(task.data, lastStatus));
444
+ return null;
445
+ }
446
+ if (INGAME_COMPLETED_STATUSES.includes(lastStatus)) {
447
+ this.setLastError(`Ingame 任务已完成但未返回可解析结果:${taskId}`);
448
+ return null;
449
+ }
450
+ if (lastStatus && !INGAME_PENDING_STATUSES.includes(lastStatus)) {
451
+ this.setLastError(`Ingame 任务状态异常:${lastStatus}`);
452
+ return null;
453
+ }
454
+ if (!_RocomClient.extractTaskId(task.data) && !lastStatus) return task.data;
455
+ }
456
+ this.setLastError(labels.stillQueued(taskId));
457
+ return null;
458
+ }
365
459
  async qqQrLogin(ctx, userIdentifier) {
366
460
  const params = { client_type: "bot", client_id: "koishi", provider: "rocom" };
367
461
  if (userIdentifier) params.user_identifier = this.sanitizeUid(userIdentifier);
@@ -503,7 +597,10 @@ ${this.stringifyForLog(details)}`);
503
597
  return this.get(ctx, "/api/v1/games/rocom/exchange/posters", this.wegameHeaders(fwToken, userIdentifier), params);
504
598
  }
505
599
  async getMerchantInfo(ctx, refresh = false) {
506
- return this.get(ctx, "/api/v1/games/rocom/merchant/info", this.wegameHeaders(), { refresh: refresh ? "true" : "false" });
600
+ return this.get(ctx, "/api/v1/games/rocom/merchant/info", this.wegameHeaders(), {
601
+ refresh: refresh ? "true" : "false",
602
+ random_goods: "all"
603
+ });
507
604
  }
508
605
  async queryPetSize(ctx, diameter, weight, sameRideEgg = false, userIdentifier = "") {
509
606
  const params = this.scopedParams({ diameter, weight }, userIdentifier);
@@ -661,29 +758,20 @@ ${this.stringifyForLog(details)}`);
661
758
  this.scopedParams(params, userIdentifier)
662
759
  );
663
760
  }
664
- async ingameHomeInfo(ctx, uid, waitMs = 5e3) {
761
+ async ingameHomeInfo(ctx, uid, options = {}) {
665
762
  const sanitizedUid = this.sanitizeUid(uid);
666
763
  if (!sanitizedUid) {
667
764
  this.setLastError("UID 不能为空");
668
765
  return null;
669
766
  }
767
+ const waitMs = Number(options.waitMs) || 5e3;
670
768
  const path6 = "/api/v1/games/rocom/ingame/home/info";
671
769
  const payload = { uid: sanitizedUid, wait_ms: waitMs };
672
- const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path6, payload);
673
- if (status === 200 && data && !(data.task_id || data.taskId || data.taskID)) return data;
674
- const taskId = data?.task_id || data?.taskId || data?.taskID;
675
- if (!taskId) {
676
- if (status === 202) this.setLastError("家园查询任务已入队,但未返回 task_id");
677
- return null;
678
- }
679
- for (let i = 0; i < 10; i++) {
680
- await new Promise((resolve) => setTimeout(resolve, 1e3));
681
- const task = await this.getIngameTask(ctx, taskId, usedApiKey);
682
- if (task.status === 200) return task.data;
683
- if (task.status === null) return null;
684
- }
685
- this.setLastError(`Home query task is still queued, please retry later (task_id: ${taskId})`);
686
- return null;
770
+ const first = await this.requestIngameWithFallback(ctx, path6, payload);
771
+ return this.pollIngameTask(ctx, first, options, {
772
+ queuedNoTaskId: "家园查询任务已入队,但未返回 task_id",
773
+ stillQueued: /* @__PURE__ */ __name((taskId) => `家园查询任务仍在排队,请稍后重试(task_id: ${taskId})`, "stillQueued")
774
+ });
687
775
  }
688
776
  async ingameMerchantInfo(ctx, shopId) {
689
777
  const params = { shop_id: shopId };
@@ -1634,6 +1722,11 @@ var TEMPLATE_CAPTURE_PADDING = {
1634
1722
  var TEMPLATE_VIEWPORTS = {
1635
1723
  activities: { width: 1600, height: 1200, deviceScaleFactor: 2 }
1636
1724
  };
1725
+ var VISUAL_BOUNDS_TEMPLATES = /* @__PURE__ */ new Set([
1726
+ "home",
1727
+ "yuanxing-shangren/merchant",
1728
+ "yuanxing-shangren/today"
1729
+ ]);
1637
1730
  var DEFAULT_SCREENSHOT_OPTIONS = {
1638
1731
  type: "jpeg",
1639
1732
  quality: 82
@@ -1760,6 +1853,7 @@ var Renderer = class {
1760
1853
  ".student-perks-page",
1761
1854
  ".student-page",
1762
1855
  ".merchant-page",
1856
+ ".page",
1763
1857
  ".home-page",
1764
1858
  ".pet-panel-page",
1765
1859
  ".pet-detail-page",
@@ -1776,16 +1870,17 @@ var Renderer = class {
1776
1870
  if (target) {
1777
1871
  const box = await target.boundingBox();
1778
1872
  if (box && box.width > 0 && box.height > 0) {
1779
- const elementMetrics = await page.evaluate((el) => {
1873
+ const useVisualBounds = VISUAL_BOUNDS_TEMPLATES.has(templateName);
1874
+ const elementMetrics = await page.evaluate((el, visualBounds) => {
1780
1875
  const rect = el.getBoundingClientRect();
1781
1876
  const element = el;
1782
1877
  return {
1783
1878
  x: rect.left + window.scrollX,
1784
1879
  y: rect.top + window.scrollY,
1785
- width: Math.max(rect.width, element.scrollWidth, element.offsetWidth),
1786
- height: Math.max(rect.height, element.scrollHeight, element.offsetHeight)
1880
+ width: visualBounds ? rect.width : Math.max(rect.width, element.scrollWidth, element.offsetWidth),
1881
+ height: visualBounds ? rect.height : Math.max(rect.height, element.scrollHeight, element.offsetHeight)
1787
1882
  };
1788
- }, target);
1883
+ }, target, useVisualBounds);
1789
1884
  const capturePadding = TEMPLATE_CAPTURE_PADDING[templateName] || { left: 0, right: 0, top: 0, bottom: 0 };
1790
1885
  await page.setViewport({
1791
1886
  width: Math.max(Math.ceil(elementMetrics.x + elementMetrics.width + capturePadding.right) + 8, 200),
@@ -2434,15 +2529,14 @@ async function sendScheduledMessage(ctx, target, message) {
2434
2529
  }
2435
2530
  __name(sendScheduledMessage, "sendScheduledMessage");
2436
2531
  async function sendScheduledImageWithFallback(ctx, target, image, fallbackText, mentionAll = false) {
2437
- const prefix = mentionAll ? "@全体\n" : "";
2532
+ const buildContent = /* @__PURE__ */ __name((body) => mentionAll ? (0, import_koishi7.h)("message", {}, (0, import_koishi7.h)("at", { type: "all" }), import_koishi7.h.text("\n"), body) : body, "buildContent");
2438
2533
  if (!image) {
2439
- return sendScheduledMessage(ctx, target, `${prefix}${fallbackText}`);
2534
+ return sendScheduledMessage(ctx, target, buildContent(import_koishi7.h.text(fallbackText)));
2440
2535
  }
2441
2536
  const imageSegment = import_koishi7.h.image(image, detectImageMime(image));
2442
- const content = mentionAll ? `${prefix}${imageSegment}` : imageSegment;
2443
- const sent = await sendScheduledMessage(ctx, target, content);
2537
+ const sent = await sendScheduledMessage(ctx, target, buildContent(imageSegment));
2444
2538
  if (sent) return true;
2445
- return sendScheduledMessage(ctx, target, `${prefix}${fallbackText}`);
2539
+ return sendScheduledMessage(ctx, target, buildContent(import_koishi7.h.text(fallbackText)));
2446
2540
  }
2447
2541
  __name(sendScheduledImageWithFallback, "sendScheduledImageWithFallback");
2448
2542
 
@@ -3224,7 +3318,7 @@ function panelPetDetailData(uid, pet, updatedAtText) {
3224
3318
  }
3225
3319
  __name(panelPetDetailData, "panelPetDetailData");
3226
3320
  async function refreshPanelPets(deps, uid, userId = "") {
3227
- const res = await deps.client.ingameHomeInfo(deps.ctx, uid, 2e4);
3321
+ const res = await deps.client.ingameHomeInfo(deps.ctx, uid, { waitMs: 2e4, timeoutMs: 3e4 });
3228
3322
  if (res) {
3229
3323
  const homeInfo = homeInfoPayload(res);
3230
3324
  const pets = buildPanelPetList(homeInfo);
@@ -3256,7 +3350,7 @@ function formatPanelUpdatedAt(ts) {
3256
3350
  return new Date(value * 1e3).toLocaleString("zh-CN");
3257
3351
  }
3258
3352
  __name(formatPanelUpdatedAt, "formatPanelUpdatedAt");
3259
- function extractHomePet(raw, index, guard = false) {
3353
+ function extractHomePet(deps, raw, index, guard = false) {
3260
3354
  if (!raw || typeof raw !== "object") return null;
3261
3355
  const homePet = raw.home_pet_info && typeof raw.home_pet_info === "object" ? raw.home_pet_info : raw;
3262
3356
  const display = raw.display_info && typeof raw.display_info === "object" ? raw.display_info : {};
@@ -3268,6 +3362,7 @@ function extractHomePet(raw, index, guard = false) {
3268
3362
  const eggReady = hasEgg || predictedEggTime > 0 && nowTs >= predictedEggTime;
3269
3363
  const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
3270
3364
  const gender = Number(display.gender || raw.gender || 0) || 0;
3365
+ const mutationType = Number(display.mutation_type || raw.mutation_type || homePet.mutation_type || 0) || 0;
3271
3366
  const isMale = gender === 1;
3272
3367
  const status = homePet.status ?? raw.status;
3273
3368
  const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ["2", "guard", "守卫"].includes(String(status).toLowerCase());
@@ -3295,7 +3390,9 @@ function extractHomePet(raw, index, guard = false) {
3295
3390
  name: String(homePet.name || homePet.pet_name || raw.name || raw.pet_name || `精灵 ${petId || ""}`),
3296
3391
  level: display.level || raw.level || homePet.level || "--",
3297
3392
  iconUrl: homePetIcon(petId, raw.icon_url || raw.pet_img_url || raw.petIcon || ""),
3393
+ starIconUrl: [1, 8, 9].includes(mutationType) ? deps.renderer.resourceUrl(`render-templates/home/img/rocomuid/star_${mutationType}.png`) : "",
3298
3394
  badge: isGuard ? "守" : hasEgg ? "蛋" : "",
3395
+ mutationType,
3299
3396
  isGuard,
3300
3397
  statusText,
3301
3398
  statusClass,
@@ -3410,13 +3507,13 @@ function buildHomeRenderData(deps, res, uid) {
3410
3507
  const indoorPets = [];
3411
3508
  const guardPets = [];
3412
3509
  indoorSources.forEach((raw, index) => {
3413
- const item = extractHomePet(raw, index);
3510
+ const item = extractHomePet(deps, raw, index);
3414
3511
  if (!item) return;
3415
3512
  if (item.isGuard) guardPets.push(item);
3416
3513
  else indoorPets.push(item);
3417
3514
  });
3418
3515
  guardSources.forEach((raw, index) => {
3419
- const item = extractHomePet(raw, index, true);
3516
+ const item = extractHomePet(deps, raw, index, true);
3420
3517
  if (item) guardPets.push(item);
3421
3518
  });
3422
3519
  indoorPets.sort((a, b) => {
@@ -3645,7 +3742,7 @@ async function checkHomeSubscriptions(deps) {
3645
3742
  if (!sub.uid || !["garden", "inspiration"].includes(sub.kind)) continue;
3646
3743
  checkedCount++;
3647
3744
  if (!cache.has(sub.uid)) {
3648
- cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid));
3745
+ cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 3e4 }));
3649
3746
  }
3650
3747
  const res = cache.get(sub.uid);
3651
3748
  if (!res) continue;
@@ -4083,7 +4180,17 @@ function register2(deps) {
4083
4180
  targetUid = String(binding?.role_id || "");
4084
4181
  }
4085
4182
  if (!targetUid) return "请提供玩家 UID,或先完成绑定后使用 洛克.家园。";
4086
- const res = await client.ingameHomeInfo(ctx, targetUid);
4183
+ let queuedNotified = false;
4184
+ const res = await client.ingameHomeInfo(ctx, targetUid, {
4185
+ waitMs: deps.config.homeQueryWaitMs,
4186
+ intervalMs: deps.config.homeQueryPollIntervalMs,
4187
+ timeoutMs: deps.config.homeQueryTimeoutMs,
4188
+ onQueued: /* @__PURE__ */ __name(async () => {
4189
+ if (queuedNotified) return;
4190
+ queuedNotified = true;
4191
+ await session?.send?.(`UID ${targetUid} 的家园查询已进入队列,正在等待游戏侧返回,请稍候…`);
4192
+ }, "onQueued")
4193
+ });
4087
4194
  if (!res) return `家园查询失败:${client.getLastErrorBrief()}`;
4088
4195
  await sendImage(deps, session, "home", buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
4089
4196
  });
@@ -4187,6 +4294,7 @@ var import_koishi9 = require("koishi");
4187
4294
  var logger9 = new import_koishi9.Logger("rocom-merchant");
4188
4295
  var TEXT = {
4189
4296
  merchant: "远行商人",
4297
+ todayMerchant: "今日远行商人",
4190
4298
  subscribe: "订阅远行商人",
4191
4299
  unsubscribe: "取消订阅远行商人",
4192
4300
  viewSubscribe: "查看远行商人订阅",
@@ -4201,6 +4309,12 @@ var CATEGORY_LABELS = {
4201
4309
  round: "常规商品",
4202
4310
  weekend: "周末限定"
4203
4311
  };
4312
+ var ROUND_WINDOWS = [
4313
+ { id: 1, label: "08:00-12:00", startHour: 8, endHour: 12 },
4314
+ { id: 2, label: "12:00-16:00", startHour: 12, endHour: 16 },
4315
+ { id: 3, label: "16:00-20:00", startHour: 16, endHour: 20 },
4316
+ { id: 4, label: "20:00-24:00", startHour: 20, endHour: 24 }
4317
+ ];
4204
4318
  var CHINA_TIMEZONE = "Asia/Shanghai";
4205
4319
  var chinaPartsFormatter = new Intl.DateTimeFormat("zh-CN", {
4206
4320
  timeZone: CHINA_TIMEZONE,
@@ -4209,19 +4323,39 @@ var chinaPartsFormatter = new Intl.DateTimeFormat("zh-CN", {
4209
4323
  day: "2-digit",
4210
4324
  hour: "2-digit",
4211
4325
  minute: "2-digit",
4326
+ second: "2-digit",
4212
4327
  hour12: false
4213
4328
  });
4214
- function getChinaParts(timestampMs) {
4329
+ function getChinaParts(input = Date.now()) {
4215
4330
  const parts = {};
4216
- for (const item of chinaPartsFormatter.formatToParts(new Date(timestampMs))) {
4331
+ const date = input instanceof Date ? input : new Date(input);
4332
+ for (const item of chinaPartsFormatter.formatToParts(date)) {
4217
4333
  if (item.type !== "literal") parts[item.type] = item.value;
4218
4334
  }
4219
4335
  return {
4336
+ year: Number(parts.year || "0"),
4337
+ month: Number(parts.month || "0"),
4338
+ day: Number(parts.day || "0"),
4220
4339
  hour: Number(parts.hour || "0"),
4221
- minute: Number(parts.minute || "0")
4340
+ minute: Number(parts.minute || "0"),
4341
+ second: Number(parts.second || "0")
4222
4342
  };
4223
4343
  }
4224
4344
  __name(getChinaParts, "getChinaParts");
4345
+ function padNumber(value) {
4346
+ return String(value).padStart(2, "0");
4347
+ }
4348
+ __name(padNumber, "padNumber");
4349
+ function getChinaDateText(input = Date.now()) {
4350
+ const parts = getChinaParts(input);
4351
+ return `${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}`;
4352
+ }
4353
+ __name(getChinaDateText, "getChinaDateText");
4354
+ function getChinaDayStartMs(input = Date.now()) {
4355
+ const parts = getChinaParts(input);
4356
+ return (/* @__PURE__ */ new Date(`${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}T00:00:00+08:00`)).getTime();
4357
+ }
4358
+ __name(getChinaDayStartMs, "getChinaDayStartMs");
4225
4359
  function classifyMerchantItem(item) {
4226
4360
  const start = normalizeTimestamp(item?.start_time);
4227
4361
  const end = normalizeTimestamp(item?.end_time);
@@ -4248,16 +4382,12 @@ function formatProductWindow(product) {
4248
4382
  const end = normalizeTimestamp(product?.end_time);
4249
4383
  if (!start && !end) return "";
4250
4384
  const formatDate2 = /* @__PURE__ */ __name((timestamp) => {
4251
- const date = new Date(timestamp);
4252
- const month = String(date.getMonth() + 1).padStart(2, "0");
4253
- const day = String(date.getDate()).padStart(2, "0");
4254
- return `${month}-${day}`;
4385
+ const parts = getChinaParts(timestamp);
4386
+ return `${padNumber(parts.month)}-${padNumber(parts.day)}`;
4255
4387
  }, "formatDate");
4256
4388
  const formatTime = /* @__PURE__ */ __name((timestamp) => {
4257
- const date = new Date(timestamp);
4258
- const hour = String(date.getHours()).padStart(2, "0");
4259
- const minute = String(date.getMinutes()).padStart(2, "0");
4260
- return `${hour}:${minute}`;
4389
+ const parts = getChinaParts(timestamp);
4390
+ return `${padNumber(parts.hour)}:${padNumber(parts.minute)}`;
4261
4391
  }, "formatTime");
4262
4392
  if (start && end) {
4263
4393
  const datePart = formatDate2(start);
@@ -4272,7 +4402,7 @@ function getMerchantActivity(res) {
4272
4402
  return activities[0] || {};
4273
4403
  }
4274
4404
  __name(getMerchantActivity, "getMerchantActivity");
4275
- function getActiveProducts(res) {
4405
+ function getMerchantProducts(res) {
4276
4406
  const activity = getMerchantActivity(res);
4277
4407
  const groups = [];
4278
4408
  if (Array.isArray(activity?.products)) groups.push(activity.products);
@@ -4282,12 +4412,10 @@ function getActiveProducts(res) {
4282
4412
  if (Array.isArray(activity?.get_pets)) groups.push(activity.get_pets);
4283
4413
  const merged = [];
4284
4414
  const seen = /* @__PURE__ */ new Set();
4285
- const now = Date.now();
4286
4415
  for (const list of groups) {
4287
4416
  for (const item of list) {
4288
4417
  const start = normalizeTimestamp(item?.start_time) ?? 0;
4289
4418
  const end = normalizeTimestamp(item?.end_time) ?? Infinity;
4290
- if (now < start || now >= end) continue;
4291
4419
  const key = `${item?.id ?? ""}|${item?.name ?? ""}|${start}|${end === Infinity ? "inf" : end}`;
4292
4420
  if (seen.has(key)) continue;
4293
4421
  seen.add(key);
@@ -4296,35 +4424,60 @@ function getActiveProducts(res) {
4296
4424
  }
4297
4425
  return merged;
4298
4426
  }
4299
- __name(getActiveProducts, "getActiveProducts");
4300
- function getCurrentMerchantRound() {
4301
- const now = /* @__PURE__ */ new Date();
4302
- const hour = now.getHours();
4303
- const rounds = [8, 12, 16, 20];
4304
- let currentRound = null;
4305
- let nextRound = null;
4306
- for (let i = 0; i < rounds.length; i++) {
4307
- if (hour >= rounds[i] && (i === rounds.length - 1 || hour < rounds[i + 1])) {
4308
- currentRound = i + 1;
4309
- nextRound = i < rounds.length - 1 ? rounds[i + 1] : rounds[0];
4310
- break;
4311
- }
4312
- }
4313
- if (currentRound === null) nextRound = rounds[0];
4314
- const nextTime = new Date(now);
4315
- if (nextRound <= hour) nextTime.setDate(nextTime.getDate() + 1);
4316
- nextTime.setHours(nextRound, 0, 0, 0);
4317
- const diff = nextTime.getTime() - now.getTime();
4427
+ __name(getMerchantProducts, "getMerchantProducts");
4428
+ function isMerchantItemActive(item, now = Date.now()) {
4429
+ const nowMs = now instanceof Date ? now.getTime() : now;
4430
+ const start = normalizeTimestamp(item?.start_time);
4431
+ const end = normalizeTimestamp(item?.end_time);
4432
+ return (start === null || nowMs >= start) && (end === null || nowMs < end);
4433
+ }
4434
+ __name(isMerchantItemActive, "isMerchantItemActive");
4435
+ function isMerchantItemToday(item, now = Date.now()) {
4436
+ const start = normalizeTimestamp(item?.start_time);
4437
+ const end = normalizeTimestamp(item?.end_time);
4438
+ if (start === null || end === null) return true;
4439
+ const startOfDay = getChinaDayStartMs(now);
4440
+ const endOfDay = startOfDay + 24 * 60 * 60 * 1e3;
4441
+ return start < endOfDay && end > startOfDay;
4442
+ }
4443
+ __name(isMerchantItemToday, "isMerchantItemToday");
4444
+ function getRoundForItem(item, now = Date.now()) {
4445
+ const start = normalizeTimestamp(item?.start_time);
4446
+ if (start === null) return null;
4447
+ const startParts = getChinaParts(start);
4448
+ const nowParts = getChinaParts(now);
4449
+ if (startParts.year !== nowParts.year || startParts.month !== nowParts.month || startParts.day !== nowParts.day) {
4450
+ return null;
4451
+ }
4452
+ const startHour = startParts.hour + startParts.minute / 60;
4453
+ const round = ROUND_WINDOWS.find((win) => startHour >= win.startHour && startHour < win.endHour);
4454
+ return round?.id ?? null;
4455
+ }
4456
+ __name(getRoundForItem, "getRoundForItem");
4457
+ function getCurrentMerchantRound(now = /* @__PURE__ */ new Date()) {
4458
+ const parts = getChinaParts(now);
4459
+ const secondsOfDay = parts.hour * 3600 + parts.minute * 60 + parts.second;
4460
+ const marketStartSeconds = 8 * 3600;
4461
+ const marketEndSeconds = 24 * 3600;
4462
+ const datePart = getChinaDateText(now);
4463
+ if (secondsOfDay < marketStartSeconds || secondsOfDay >= marketEndSeconds) {
4464
+ return {
4465
+ current: null,
4466
+ total: ROUND_WINDOWS.length,
4467
+ countdown: TEXT.notOpen,
4468
+ is_open: false,
4469
+ round_id: `${datePart}-closed`
4470
+ };
4471
+ }
4472
+ const currentWindow = ROUND_WINDOWS.find((win) => parts.hour >= win.startHour && parts.hour < win.endHour);
4473
+ const currentRound = currentWindow?.id ?? null;
4474
+ const roundEndSeconds = (currentWindow?.endHour ?? 24) * 3600;
4475
+ const diff = Math.max(0, (roundEndSeconds - secondsOfDay) * 1e3);
4318
4476
  const hours = Math.floor(diff / 36e5);
4319
4477
  const mins = Math.floor(diff % 36e5 / 6e4);
4320
- const datePart = [
4321
- now.getFullYear(),
4322
- String(now.getMonth() + 1).padStart(2, "0"),
4323
- String(now.getDate()).padStart(2, "0")
4324
- ].join("-");
4325
4478
  return {
4326
4479
  current: currentRound,
4327
- total: rounds.length,
4480
+ total: ROUND_WINDOWS.length,
4328
4481
  countdown: `${hours}小时${mins}分钟`,
4329
4482
  is_open: currentRound !== null,
4330
4483
  round_id: `${datePart}-${currentRound || "closed"}`
@@ -4365,66 +4518,258 @@ function sameStringArray(left, right) {
4365
4518
  return JSON.stringify(left) === JSON.stringify(right);
4366
4519
  }
4367
4520
  __name(sameStringArray, "sameStringArray");
4368
- function buildMerchantRenderPayload(res) {
4369
- const products = getActiveProducts(res);
4370
- const roundInfo = getCurrentMerchantRound();
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)
4521
+ function getRandomGoodsMaps(res) {
4522
+ const priceMap = /* @__PURE__ */ new Map();
4523
+ const limitMap = /* @__PURE__ */ new Map();
4524
+ const randomGoods = Array.isArray(res?.random_goods) ? res.random_goods : Array.isArray(res?.randomGoods) ? res.randomGoods : [];
4525
+ for (const item of randomGoods) {
4526
+ const name2 = String(item?.goods_name || item?.name || "").trim();
4527
+ if (!name2) continue;
4528
+ if (item?.price !== void 0 && item?.price !== null && item.price !== "") priceMap.set(name2, item.price);
4529
+ if (item?.buy_limit_num !== void 0 && item?.buy_limit_num !== null && item.buy_limit_num !== "") {
4530
+ limitMap.set(name2, item.buy_limit_num);
4531
+ }
4532
+ }
4533
+ return { priceMap, limitMap };
4534
+ }
4535
+ __name(getRandomGoodsMaps, "getRandomGoodsMaps");
4536
+ function normalizeMerchantProducts(res, now = /* @__PURE__ */ new Date()) {
4537
+ return getMerchantProducts(res).map((item) => {
4538
+ const name2 = String(item?.name || item?.goods_name || TEXT.unknown).trim() || TEXT.unknown;
4539
+ return {
4540
+ name: name2,
4541
+ image: String(item?.icon_url || item?.iconUrl || ""),
4542
+ time_label: formatProductWindow(item),
4543
+ category: classifyMerchantItem(item),
4544
+ round_id: getRoundForItem(item, now),
4545
+ is_active: isMerchantItemActive(item, now),
4546
+ start_time: normalizeTimestamp(item?.start_time),
4547
+ end_time: normalizeTimestamp(item?.end_time)
4548
+ };
4549
+ });
4550
+ }
4551
+ __name(normalizeMerchantProducts, "normalizeMerchantProducts");
4552
+ function buildMerchantCardItems(products, res, options) {
4553
+ const { priceMap, limitMap } = getRandomGoodsMaps(res);
4554
+ const catOrder = { round: 0, normal: 1, weekend: 2 };
4555
+ const startY = 592;
4556
+ const cardHeight = 308;
4557
+ const gap = 43;
4558
+ const goodsAll = products.map((product) => {
4559
+ const limit = limitMap.get(product.name);
4560
+ const limitText = limit === void 0 || limit === null || limit === "" ? "--" : String(limit);
4561
+ const isHot = product.category !== "round";
4562
+ const isEnded = options.includeEnded ? !product.is_active : false;
4563
+ const roundPrefix = product.round_id ? `第${product.round_id}轮·` : "";
4564
+ const remainingStr = product.category === "normal" ? `本日限购${limitText}个` : product.category === "weekend" ? `活动期间限购${limitText}个` : `${isEnded ? roundPrefix : ""}本轮限购${limitText}个`;
4565
+ return {
4566
+ goods_name: product.name,
4567
+ iconUrl: product.image,
4568
+ price: priceMap.get(product.name) ?? 0,
4569
+ num: "",
4570
+ category: product.category,
4571
+ roundId: product.round_id || 0,
4572
+ isHot,
4573
+ isEnded,
4574
+ remainingStr,
4575
+ top: 0
4576
+ };
4577
+ });
4578
+ goodsAll.sort((a, b) => {
4579
+ if (options.includeEnded && a.isEnded !== b.isEnded) return a.isEnded ? 1 : -1;
4580
+ if (a.category !== b.category) return catOrder[a.category] - catOrder[b.category];
4581
+ return Number(b.price || 0) - Number(a.price || 0);
4582
+ });
4583
+ const goods = goodsAll.map((item, index) => ({
4584
+ ...item,
4585
+ num: String(index + 1).padStart(2, "0"),
4586
+ top: startY + index * (cardHeight + gap)
4377
4587
  }));
4378
- const categoryMap = {
4379
- normal: [],
4380
- round: [],
4381
- weekend: []
4382
- };
4383
- for (const product of renderedProducts) {
4384
- categoryMap[product.category].push(product);
4588
+ const lastCardTop = goods.length > 0 ? goods[goods.length - 1].top : startY;
4589
+ const bottomFrameTop = lastCardTop + 287;
4590
+ const pageHeight = bottomFrameTop + 160;
4591
+ return { goods, bottomFrameTop, pageHeight };
4592
+ }
4593
+ __name(buildMerchantCardItems, "buildMerchantCardItems");
4594
+ function getCurrentMerchantProducts(products, roundInfo) {
4595
+ return products.filter((product) => {
4596
+ if (!product.is_active) return false;
4597
+ if (product.category === "round") return !!roundInfo.current && product.round_id === roundInfo.current;
4598
+ return true;
4599
+ });
4600
+ }
4601
+ __name(getCurrentMerchantProducts, "getCurrentMerchantProducts");
4602
+ function buildMerchantFallbackText(title, products, roundInfo) {
4603
+ const fallbackLines = [title];
4604
+ if (roundInfo) {
4605
+ fallbackLines.push(`轮次:第 ${roundInfo.current || TEXT.notOpen} / ${roundInfo.total} 轮`);
4606
+ fallbackLines.push(`剩余:${roundInfo.countdown}`);
4607
+ }
4608
+ fallbackLines.push("");
4609
+ if (!products.length) {
4610
+ fallbackLines.push(roundInfo ? "当前轮次暂无商品。" : "今日暂无已公布的远行商人商品。");
4611
+ return fallbackLines.join("\n").trimEnd();
4612
+ }
4613
+ for (const key of CATEGORY_ORDER) {
4614
+ const group = products.filter((product) => product.category === key);
4615
+ if (!group.length) continue;
4616
+ fallbackLines.push(`【${CATEGORY_LABELS[key]}】`);
4617
+ group.forEach((product, index) => {
4618
+ const tail = product.time_label ? ` (${product.time_label})` : "";
4619
+ fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
4620
+ });
4621
+ fallbackLines.push("");
4385
4622
  }
4386
- const categories = CATEGORY_ORDER.filter((key) => categoryMap[key].length > 0).map((key) => ({
4387
- key,
4388
- label: CATEGORY_LABELS[key],
4389
- products: categoryMap[key]
4623
+ return fallbackLines.join("\n").trimEnd();
4624
+ }
4625
+ __name(buildMerchantFallbackText, "buildMerchantFallbackText");
4626
+ function getMerchantActivityTitle(res) {
4627
+ const activity = getMerchantActivity(res);
4628
+ return String(activity?.name || TEXT.merchant).trim() || TEXT.merchant;
4629
+ }
4630
+ __name(getMerchantActivityTitle, "getMerchantActivityTitle");
4631
+ function getMerchantActivitySubtitle(res) {
4632
+ const activity = getMerchantActivity(res);
4633
+ return String(activity?.start_date || "每日 08:00 / 12:00 / 16:00 / 20:00 刷新").trim();
4634
+ }
4635
+ __name(getMerchantActivitySubtitle, "getMerchantActivitySubtitle");
4636
+ function getLegacyRoundGroups(now, roundInfo) {
4637
+ return ROUND_WINDOWS.map((win) => ({
4638
+ round_id: win.id,
4639
+ label: `${padNumber(win.startHour)}:00 - ${padNumber(win.endHour)}:00`,
4640
+ is_current: roundInfo.current === win.id,
4641
+ products: []
4390
4642
  }));
4643
+ }
4644
+ __name(getLegacyRoundGroups, "getLegacyRoundGroups");
4645
+ function buildLegacyCategoryGroups(products, roundGroups) {
4646
+ return CATEGORY_ORDER.map((key) => {
4647
+ const groups = roundGroups.map((group) => ({
4648
+ round_id: group.round_id,
4649
+ label: group.label,
4650
+ is_current: group.is_current,
4651
+ products: group.products.filter((product) => product.category === key)
4652
+ })).filter((group) => group.products.length > 0);
4653
+ return {
4654
+ key,
4655
+ label: CATEGORY_LABELS[key],
4656
+ roundGroups: groups,
4657
+ product_count: groups.reduce((sum, group) => sum + group.products.length, 0)
4658
+ };
4659
+ }).filter((category) => category.product_count > 0);
4660
+ }
4661
+ __name(buildLegacyCategoryGroups, "buildLegacyCategoryGroups");
4662
+ function getMerchantDateStr(now = /* @__PURE__ */ new Date()) {
4663
+ const parts = getChinaParts(now);
4664
+ return `${parts.month}.${parts.day}`;
4665
+ }
4666
+ __name(getMerchantDateStr, "getMerchantDateStr");
4667
+ function buildMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
4668
+ const allProducts = normalizeMerchantProducts(res, now);
4669
+ const roundInfo = getCurrentMerchantRound(now);
4670
+ const products = getCurrentMerchantProducts(allProducts, roundInfo);
4671
+ const currentWindow = ROUND_WINDOWS.find((win) => win.id === roundInfo.current);
4672
+ const timeRange = currentWindow ? `${padNumber(currentWindow.startHour)}:00-${padNumber(currentWindow.endHour)}:00` : "--:--~--:--";
4673
+ const data = {
4674
+ dateStr: getMerchantDateStr(now),
4675
+ timeRange,
4676
+ ...buildMerchantCardItems(products, res, { includeEnded: false })
4677
+ };
4678
+ const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
4679
+ return { products, roundInfo, data, fallback };
4680
+ }
4681
+ __name(buildMerchantRenderPayload, "buildMerchantRenderPayload");
4682
+ function buildLegacyMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
4683
+ const allProducts = normalizeMerchantProducts(res, now);
4684
+ const roundInfo = getCurrentMerchantRound(now);
4685
+ const products = getCurrentMerchantProducts(allProducts, roundInfo);
4686
+ const roundGroups = getLegacyRoundGroups(now, roundInfo);
4687
+ const currentGroup = roundGroups.find((group) => group.round_id === roundInfo.current) || roundGroups[0];
4688
+ currentGroup.products.push(...products);
4689
+ const categories = buildLegacyCategoryGroups(products, roundGroups);
4391
4690
  const data = {
4392
4691
  background: "",
4393
- title: activity.name || TEXT.merchant,
4394
- subtitle: activity.start_date || "每日 08:00 / 12:00 / 16:00 / 20:00 刷新",
4395
- titleIcon: true,
4396
- product_count: renderedProducts.length,
4397
- round_info: roundInfo,
4692
+ title: getMerchantActivityTitle(res),
4693
+ subtitle: getMerchantActivitySubtitle(res),
4398
4694
  categories,
4399
- products: renderedProducts
4695
+ roundGroups,
4696
+ total_products: products.length
4400
4697
  };
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 };
4698
+ const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
4699
+ return { products, roundInfo, data, fallback };
4700
+ }
4701
+ __name(buildLegacyMerchantRenderPayload, "buildLegacyMerchantRenderPayload");
4702
+ function buildTodayMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
4703
+ const products = normalizeMerchantProducts(res, now).filter((product) => {
4704
+ const source = {
4705
+ start_time: product.start_time,
4706
+ end_time: product.end_time
4707
+ };
4708
+ return isMerchantItemToday(source, now);
4709
+ });
4710
+ const data = {
4711
+ dateStr: getMerchantDateStr(now),
4712
+ ...buildMerchantCardItems(products, res, { includeEnded: true })
4713
+ };
4714
+ const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, products);
4715
+ return { products, data, fallback };
4716
+ }
4717
+ __name(buildTodayMerchantRenderPayload, "buildTodayMerchantRenderPayload");
4718
+ function buildLegacyTodayMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
4719
+ const allProducts = normalizeMerchantProducts(res, now).filter((product) => {
4720
+ const source = {
4721
+ start_time: product.start_time,
4722
+ end_time: product.end_time
4723
+ };
4724
+ return isMerchantItemToday(source, now);
4725
+ });
4726
+ const roundInfo = getCurrentMerchantRound(now);
4727
+ const roundGroups = getLegacyRoundGroups(now, roundInfo);
4728
+ for (const product of allProducts) {
4729
+ const roundId = product.category === "round" ? product.round_id : null;
4730
+ const group = roundId ? roundGroups.find((item) => item.round_id === roundId) : roundGroups[0];
4731
+ if (group) group.products.push(product);
4732
+ }
4733
+ const categories = buildLegacyCategoryGroups(allProducts, roundGroups);
4734
+ const data = {
4735
+ background: "",
4736
+ title: TEXT.todayMerchant,
4737
+ subtitle: `${getChinaDateText(now)} · 每日 08:00 / 12:00 / 16:00 / 20:00 刷新`,
4738
+ categories,
4739
+ roundGroups,
4740
+ total_products: allProducts.length
4741
+ };
4742
+ const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, allProducts);
4743
+ return { products: allProducts, data, fallback };
4419
4744
  }
4420
- __name(buildMerchantRenderPayload, "buildMerchantRenderPayload");
4745
+ __name(buildLegacyTodayMerchantRenderPayload, "buildLegacyTodayMerchantRenderPayload");
4746
+ function useLegacyMerchantUi(deps) {
4747
+ return deps.config.merchantUiStyle === "old";
4748
+ }
4749
+ __name(useLegacyMerchantUi, "useLegacyMerchantUi");
4750
+ function buildConfiguredMerchantRenderPayload(deps, res, now = /* @__PURE__ */ new Date()) {
4751
+ const payload = useLegacyMerchantUi(deps) ? buildLegacyMerchantRenderPayload(res, now) : buildMerchantRenderPayload(res, now);
4752
+ return {
4753
+ ...payload,
4754
+ templateName: useLegacyMerchantUi(deps) ? "yuanxing-shangren" : "yuanxing-shangren/merchant"
4755
+ };
4756
+ }
4757
+ __name(buildConfiguredMerchantRenderPayload, "buildConfiguredMerchantRenderPayload");
4758
+ function buildConfiguredTodayMerchantRenderPayload(deps, res, now = /* @__PURE__ */ new Date()) {
4759
+ const payload = useLegacyMerchantUi(deps) ? buildLegacyTodayMerchantRenderPayload(res, now) : buildTodayMerchantRenderPayload(res, now);
4760
+ return {
4761
+ ...payload,
4762
+ templateName: useLegacyMerchantUi(deps) ? "yuanxing-shangren" : "yuanxing-shangren/today"
4763
+ };
4764
+ }
4765
+ __name(buildConfiguredTodayMerchantRenderPayload, "buildConfiguredTodayMerchantRenderPayload");
4421
4766
  async function checkMerchantSubscriptions(deps) {
4422
4767
  const { ctx, client, merchantSubMgr, renderer, config } = deps;
4423
4768
  const res = await client.getMerchantInfo(ctx, true);
4424
4769
  if (!res) return { subscriptions: 0, matched: 0, pushed: 0 };
4425
- const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
4770
+ const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
4426
4771
  const productNames = products.map((p) => p.name || "").filter(Boolean);
4427
- const rendered = await renderer.renderHtml(ctx, "yuanxing-shangren", data);
4772
+ const rendered = await renderer.renderHtml(ctx, templateName, data);
4428
4773
  const renderedImage = rendered ? compressPngImage(rendered, config) : null;
4429
4774
  const subs = merchantSubMgr.getAll();
4430
4775
  let matchedCount = 0;
@@ -4474,10 +4819,17 @@ function register3(deps) {
4474
4819
  ctx.command(TEXT.merchant, "查看远行商人商品").action(async ({ session }) => {
4475
4820
  const res = await client.getMerchantInfo(ctx, true);
4476
4821
  if (!res) return `获取远行商人数据失败:${client.getLastErrorBrief()}`;
4477
- const { data, fallback } = buildMerchantRenderPayload(res);
4478
- const png = await deps.renderer.renderHtml(ctx, "yuanxing-shangren", data);
4822
+ const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
4823
+ const png = await deps.renderer.renderHtml(ctx, templateName, data);
4479
4824
  await sendImageWithFallback(session, png, fallback, "merchant:yuanxing-shangren", deps.config);
4480
4825
  });
4826
+ ctx.command(TEXT.todayMerchant, "查看今日远行商人全部商品").action(async ({ session }) => {
4827
+ const res = await client.getMerchantInfo(ctx, true);
4828
+ if (!res) return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
4829
+ const { data, fallback, templateName } = buildConfiguredTodayMerchantRenderPayload(deps, res);
4830
+ const png = await deps.renderer.renderHtml(ctx, templateName, data);
4831
+ await sendImageWithFallback(session, png, fallback, "merchant:yuanxing-shangren:today", deps.config);
4832
+ });
4481
4833
  ctx.command(`${TEXT.subscribe} [args:text]`, "订阅远行商人商品提醒").action(async ({ session }, args) => {
4482
4834
  const target = getSubscriptionTarget(session);
4483
4835
  if (!target.privateChat && !isBotAdmin2(session, config.adminUserIds)) return "此指令仅限管理员使用。";
@@ -5374,13 +5726,19 @@ var Config = import_koishi12.Schema.intersect([
5374
5726
  import_koishi12.Schema.object({
5375
5727
  merchantSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("启用远行商人订阅"),
5376
5728
  merchantSubscriptionItems: import_koishi12.Schema.array(String).default(["国王球", "棱镜球", "炫彩精灵蛋"]).description("默认订阅商品"),
5729
+ merchantUiStyle: import_koishi12.Schema.union(["new", "old"]).default("new").description("远行商人 UI 样式:new 为新版大图卡片,old 为旧版列表卡片"),
5377
5730
  merchantCheckMode: import_koishi12.Schema.union(["interval", "times"]).default("interval").description("商人检查模式:interval 定期轮询 / times 指定时间点"),
5378
5731
  merchantCheckInterval: import_koishi12.Schema.number().default(3e5).description("商人检查间隔,单位毫秒(仅 interval 模式生效)"),
5379
5732
  merchantCheckTimes: import_koishi12.Schema.array(String).default([]).description("商人定时检查时间点(HH:MM 格式,如 08:00,仅 times 模式生效)"),
5380
5733
  merchantPrivateSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("允许个人私聊订阅远行商人推送"),
5381
5734
  homeSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("启用家园菜园和灵感订阅推送"),
5382
5735
  homeSubscriptionIntervalMinutes: import_koishi12.Schema.number().default(5).description("家园订阅检查间隔,单位分钟")
5383
- }).description("订阅推送设置")
5736
+ }).description("订阅推送设置"),
5737
+ import_koishi12.Schema.object({
5738
+ homeQueryWaitMs: import_koishi12.Schema.number().default(5e3).description("家园查询服务端同步等待毫秒(long-poll,超过此时间未出结果则转入排队)"),
5739
+ homeQueryPollIntervalMs: import_koishi12.Schema.number().default(3e3).description("家园查询进入排队后的轮询间隔,单位毫秒"),
5740
+ homeQueryTimeoutMs: import_koishi12.Schema.number().default(18e4).description("家园查询排队等候的总超时,单位毫秒,超时后提示稍后重试")
5741
+ }).description("家园查询排队设置")
5384
5742
  ]);
5385
5743
  function apply(ctx, config) {
5386
5744
  setupRoleTokenModel(ctx);