koishi-plugin-rocom 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/lib/client.d.ts +17 -1
  2. package/lib/client.js +121 -21
  3. package/lib/commands/merchant.js +409 -61
  4. package/lib/commands/query.js +95 -27
  5. package/lib/index.d.ts +4 -0
  6. package/lib/index.js +600 -127
  7. package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
  8. package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
  9. package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
  10. package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
  11. package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
  12. package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
  13. package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
  14. package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
  15. package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
  16. package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
  17. package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
  18. package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
  19. package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
  20. package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
  21. package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
  22. package/lib/render-templates/home/index.html +81 -97
  23. package/lib/render-templates/home/style.css +316 -375
  24. package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
  25. package/lib/render-templates/yuanxing-shangren/README.md +86 -0
  26. package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
  27. package/lib/render-templates/yuanxing-shangren/img//345/272/225/351/203/250/344/277/241/346/201/257/346/241/206.png +0 -0
  28. package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
  29. package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
  30. package/lib/render-templates/yuanxing-shangren/img//347/274/226/345/217/267/350/203/214/346/231/257/346/241/206.png +0 -0
  31. package/lib/render-templates/yuanxing-shangren/index.html +233 -148
  32. package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
  33. package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
  34. package/lib/render-templates/yuanxing-shangren/style.css +201 -256
  35. package/lib/render-templates/yuanxing-shangren/today.html +461 -0
  36. package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
  37. package/lib/render.js +11 -4
  38. package/lib/subscription-send.js +4 -5
  39. package/lib/ttf/rocom_skill_origin.ttf +0 -0
  40. package/lib/ttf//351/200/240/345/255/227/345/267/245/346/210/277/345/220/257/351/273/221/344/275/223.ttf +0 -0
  41. package/lib/types.d.ts +4 -0
  42. package/package.json +1 -1
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
 
@@ -2667,25 +2761,28 @@ function normalizeEpochSeconds(value) {
2667
2761
  return Math.floor(ts);
2668
2762
  }
2669
2763
  __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
2764
  function formatHomeRemaining(targetTs, nowTs = Math.floor(Date.now() / 1e3)) {
2679
2765
  if (!targetTs) return "未开始";
2766
+ if (nowTs >= targetTs) return "已完成";
2680
2767
  const remain = Math.max(0, targetTs - nowTs);
2681
- if (remain <= 0) return "已完成";
2682
- const hours = Math.floor(remain / 3600);
2768
+ const days = Math.floor(remain / 86400);
2769
+ const hours = Math.floor(remain % 86400 / 3600);
2683
2770
  const minutes = Math.floor(remain % 3600 / 60);
2684
- if (hours >= 24) return `${Math.floor(hours / 24)}天${hours % 24}小时`;
2771
+ const seconds = remain % 60;
2772
+ if (days > 0) return hours > 0 ? `${days}天${hours}小时` : `${days}天`;
2685
2773
  if (hours > 0) return `${hours}小时${minutes}分钟`;
2686
- return `${minutes}分钟`;
2774
+ if (minutes > 0) return `${minutes}分${seconds}秒`;
2775
+ return `${seconds}秒`;
2687
2776
  }
2688
2777
  __name(formatHomeRemaining, "formatHomeRemaining");
2778
+ function formatEggRemaining(targetTs, nowTs = Math.floor(Date.now() / 1e3)) {
2779
+ if (!targetTs || nowTs >= targetTs) return "0分钟";
2780
+ const remain = Math.max(0, targetTs - nowTs);
2781
+ const totalHours = Math.floor(remain / 3600);
2782
+ const minutes = Math.floor(remain % 3600 / 60);
2783
+ return `${totalHours}小时${minutes}分钟`;
2784
+ }
2785
+ __name(formatEggRemaining, "formatEggRemaining");
2689
2786
  function homeInfoPayload(res) {
2690
2787
  const payload = res || {};
2691
2788
  if (payload.result?.home_info) return payload.result.home_info;
@@ -3221,7 +3318,7 @@ function panelPetDetailData(uid, pet, updatedAtText) {
3221
3318
  }
3222
3319
  __name(panelPetDetailData, "panelPetDetailData");
3223
3320
  async function refreshPanelPets(deps, uid, userId = "") {
3224
- const res = await deps.client.ingameHomeInfo(deps.ctx, uid, 2e4);
3321
+ const res = await deps.client.ingameHomeInfo(deps.ctx, uid, { waitMs: 2e4, timeoutMs: 3e4 });
3225
3322
  if (res) {
3226
3323
  const homeInfo = homeInfoPayload(res);
3227
3324
  const pets = buildPanelPetList(homeInfo);
@@ -3253,36 +3350,56 @@ function formatPanelUpdatedAt(ts) {
3253
3350
  return new Date(value * 1e3).toLocaleString("zh-CN");
3254
3351
  }
3255
3352
  __name(formatPanelUpdatedAt, "formatPanelUpdatedAt");
3256
- function extractHomePet(raw, index, guard = false) {
3353
+ function extractHomePet(deps, raw, index, guard = false) {
3257
3354
  if (!raw || typeof raw !== "object") return null;
3258
3355
  const homePet = raw.home_pet_info && typeof raw.home_pet_info === "object" ? raw.home_pet_info : raw;
3259
3356
  const display = raw.display_info && typeof raw.display_info === "object" ? raw.display_info : {};
3260
3357
  const petId = homePet.pet_cfg_id || homePet.pet_id || homePet.pet_base_id || raw.pet_cfg_id || raw.pet_id || raw.id;
3261
3358
  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
3359
  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";
3360
+ const hasEgg = Boolean(raw.have_egg);
3361
+ const predictedEggTime = normalizeEpochSeconds(raw.predicted_egg_time);
3362
+ const eggReady = hasEgg || predictedEggTime > 0 && nowTs >= predictedEggTime;
3363
+ const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
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;
3366
+ const isMale = gender === 1;
3367
+ const status = homePet.status ?? raw.status;
3368
+ const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ["2", "guard", "守卫"].includes(String(status).toLowerCase());
3369
+ const hasInspiration = feedRound > 0;
3370
+ const inspireReady = hasInspiration;
3371
+ const statusText = isGuard && !hasInspiration ? "守卫中" : inspireReady ? "可收取灵感" : hasInspiration ? "灵感收集中" : "未喂食";
3372
+ const statusClass = isGuard && !hasInspiration ? "guard" : eggReady ? "ready" : inspireReady ? "progress" : hasInspiration ? "progress" : "idle";
3373
+ let note;
3374
+ if (isGuard && String(petId) === "0") {
3375
+ note = "家园守卫位";
3376
+ } else if (eggReady) {
3377
+ note = "可收取";
3378
+ } else if (predictedEggTime > 0) {
3379
+ note = `${formatEggRemaining(predictedEggTime, nowTs)}后生蛋`;
3380
+ } else if (feedRound > 0) {
3381
+ note = isMale ? "" : "等待生蛋";
3382
+ } else if (isGuard) {
3383
+ note = "家园守卫位";
3384
+ } else {
3385
+ note = "未喂食";
3386
+ }
3273
3387
  return {
3274
3388
  id: String(petId || ""),
3275
3389
  pos: raw.pos || raw.position || index + 1,
3276
3390
  name: String(homePet.name || homePet.pet_name || raw.name || raw.pet_name || `精灵 ${petId || ""}`),
3277
3391
  level: display.level || raw.level || homePet.level || "--",
3278
3392
  iconUrl: homePetIcon(petId, raw.icon_url || raw.pet_img_url || raw.petIcon || ""),
3279
- badge: isGuard ? "守" : "",
3393
+ starIconUrl: [1, 8, 9].includes(mutationType) ? deps.renderer.resourceUrl(`render-templates/home/img/rocomuid/star_${mutationType}.png`) : "",
3394
+ badge: isGuard ? "守" : hasEgg ? "蛋" : "",
3395
+ mutationType,
3280
3396
  isGuard,
3281
3397
  statusText,
3282
3398
  statusClass,
3283
- note: hasInspiration ? formatHomeRemaining(readyAt, nowTs) : isGuard ? "家园守卫位" : "暂无灵感倒计时",
3284
- inspireReady,
3285
- readyAt
3399
+ note,
3400
+ inspireReady: eggReady,
3401
+ readyAt: predictedEggTime || 0,
3402
+ gender
3286
3403
  };
3287
3404
  }
3288
3405
  __name(extractHomePet, "extractHomePet");
@@ -3390,15 +3507,25 @@ function buildHomeRenderData(deps, res, uid) {
3390
3507
  const indoorPets = [];
3391
3508
  const guardPets = [];
3392
3509
  indoorSources.forEach((raw, index) => {
3393
- const item = extractHomePet(raw, index);
3510
+ const item = extractHomePet(deps, raw, index);
3394
3511
  if (!item) return;
3395
3512
  if (item.isGuard) guardPets.push(item);
3396
3513
  else indoorPets.push(item);
3397
3514
  });
3398
3515
  guardSources.forEach((raw, index) => {
3399
- const item = extractHomePet(raw, index, true);
3516
+ const item = extractHomePet(deps, raw, index, true);
3400
3517
  if (item) guardPets.push(item);
3401
3518
  });
3519
+ indoorPets.sort((a, b) => {
3520
+ if (a.gender === 2 && b.gender !== 2) return -1;
3521
+ if (a.gender !== 2 && b.gender === 2) return 1;
3522
+ if (a.gender === 2) {
3523
+ const ta = a.readyAt || Number.MAX_SAFE_INTEGER;
3524
+ const tb = b.readyAt || Number.MAX_SAFE_INTEGER;
3525
+ return ta - tb;
3526
+ }
3527
+ return 0;
3528
+ });
3402
3529
  const gardenPlots = extractHomePlants(deps, homeInfo);
3403
3530
  const createdAt = normalizeEpochSeconds(res?.meta?.created_at);
3404
3531
  return {
@@ -3615,7 +3742,7 @@ async function checkHomeSubscriptions(deps) {
3615
3742
  if (!sub.uid || !["garden", "inspiration"].includes(sub.kind)) continue;
3616
3743
  checkedCount++;
3617
3744
  if (!cache.has(sub.uid)) {
3618
- 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 }));
3619
3746
  }
3620
3747
  const res = cache.get(sub.uid);
3621
3748
  if (!res) continue;
@@ -4053,7 +4180,17 @@ function register2(deps) {
4053
4180
  targetUid = String(binding?.role_id || "");
4054
4181
  }
4055
4182
  if (!targetUid) return "请提供玩家 UID,或先完成绑定后使用 洛克.家园。";
4056
- 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
+ });
4057
4194
  if (!res) return `家园查询失败:${client.getLastErrorBrief()}`;
4058
4195
  await sendImage(deps, session, "home", buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
4059
4196
  });
@@ -4157,6 +4294,7 @@ var import_koishi9 = require("koishi");
4157
4294
  var logger9 = new import_koishi9.Logger("rocom-merchant");
4158
4295
  var TEXT = {
4159
4296
  merchant: "远行商人",
4297
+ todayMerchant: "今日远行商人",
4160
4298
  subscribe: "订阅远行商人",
4161
4299
  unsubscribe: "取消订阅远行商人",
4162
4300
  viewSubscribe: "查看远行商人订阅",
@@ -4165,6 +4303,73 @@ var TEXT = {
4165
4303
  defaultSource: "默认",
4166
4304
  customSource: "自定义"
4167
4305
  };
4306
+ var CATEGORY_ORDER = ["normal", "round", "weekend"];
4307
+ var CATEGORY_LABELS = {
4308
+ normal: "热销商品",
4309
+ round: "常规商品",
4310
+ weekend: "周末限定"
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
+ ];
4318
+ var CHINA_TIMEZONE = "Asia/Shanghai";
4319
+ var chinaPartsFormatter = new Intl.DateTimeFormat("zh-CN", {
4320
+ timeZone: CHINA_TIMEZONE,
4321
+ year: "numeric",
4322
+ month: "2-digit",
4323
+ day: "2-digit",
4324
+ hour: "2-digit",
4325
+ minute: "2-digit",
4326
+ second: "2-digit",
4327
+ hour12: false
4328
+ });
4329
+ function getChinaParts(input = Date.now()) {
4330
+ const parts = {};
4331
+ const date = input instanceof Date ? input : new Date(input);
4332
+ for (const item of chinaPartsFormatter.formatToParts(date)) {
4333
+ if (item.type !== "literal") parts[item.type] = item.value;
4334
+ }
4335
+ return {
4336
+ year: Number(parts.year || "0"),
4337
+ month: Number(parts.month || "0"),
4338
+ day: Number(parts.day || "0"),
4339
+ hour: Number(parts.hour || "0"),
4340
+ minute: Number(parts.minute || "0"),
4341
+ second: Number(parts.second || "0")
4342
+ };
4343
+ }
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");
4359
+ function classifyMerchantItem(item) {
4360
+ const start = normalizeTimestamp(item?.start_time);
4361
+ const end = normalizeTimestamp(item?.end_time);
4362
+ if (!start || !end) return "normal";
4363
+ const durationDays = (end - start) / (1e3 * 60 * 60 * 24);
4364
+ if (durationDays >= 2) return "weekend";
4365
+ const startParts = getChinaParts(start);
4366
+ const endParts = getChinaParts(end);
4367
+ const startHour = startParts.hour + startParts.minute / 60;
4368
+ const endHour = endParts.hour + endParts.minute / 60;
4369
+ if (startHour <= 8 && endHour >= 23.5) return "normal";
4370
+ return "round";
4371
+ }
4372
+ __name(classifyMerchantItem, "classifyMerchantItem");
4168
4373
  function normalizeTimestamp(value) {
4169
4374
  if (value === null || value === void 0 || value === "") return null;
4170
4375
  const timestamp = Number(value);
@@ -4177,16 +4382,12 @@ function formatProductWindow(product) {
4177
4382
  const end = normalizeTimestamp(product?.end_time);
4178
4383
  if (!start && !end) return "";
4179
4384
  const formatDate2 = /* @__PURE__ */ __name((timestamp) => {
4180
- const date = new Date(timestamp);
4181
- const month = String(date.getMonth() + 1).padStart(2, "0");
4182
- const day = String(date.getDate()).padStart(2, "0");
4183
- return `${month}-${day}`;
4385
+ const parts = getChinaParts(timestamp);
4386
+ return `${padNumber(parts.month)}-${padNumber(parts.day)}`;
4184
4387
  }, "formatDate");
4185
4388
  const formatTime = /* @__PURE__ */ __name((timestamp) => {
4186
- const date = new Date(timestamp);
4187
- const hour = String(date.getHours()).padStart(2, "0");
4188
- const minute = String(date.getMinutes()).padStart(2, "0");
4189
- return `${hour}:${minute}`;
4389
+ const parts = getChinaParts(timestamp);
4390
+ return `${padNumber(parts.hour)}:${padNumber(parts.minute)}`;
4190
4391
  }, "formatTime");
4191
4392
  if (start && end) {
4192
4393
  const datePart = formatDate2(start);
@@ -4201,45 +4402,82 @@ function getMerchantActivity(res) {
4201
4402
  return activities[0] || {};
4202
4403
  }
4203
4404
  __name(getMerchantActivity, "getMerchantActivity");
4204
- function getActiveProducts(res) {
4405
+ function getMerchantProducts(res) {
4205
4406
  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
- });
4213
- }
4214
- __name(getActiveProducts, "getActiveProducts");
4215
- function getCurrentMerchantRound() {
4216
- const now = /* @__PURE__ */ new Date();
4217
- const hour = now.getHours();
4218
- const rounds = [8, 12, 16, 20];
4219
- let currentRound = null;
4220
- let nextRound = null;
4221
- for (let i = 0; i < rounds.length; i++) {
4222
- if (hour >= rounds[i] && (i === rounds.length - 1 || hour < rounds[i + 1])) {
4223
- currentRound = i + 1;
4224
- nextRound = i < rounds.length - 1 ? rounds[i + 1] : rounds[0];
4225
- break;
4226
- }
4227
- }
4228
- if (currentRound === null) nextRound = rounds[0];
4229
- const nextTime = new Date(now);
4230
- if (nextRound <= hour) nextTime.setDate(nextTime.getDate() + 1);
4231
- nextTime.setHours(nextRound, 0, 0, 0);
4232
- const diff = nextTime.getTime() - now.getTime();
4407
+ const groups = [];
4408
+ if (Array.isArray(activity?.products)) groups.push(activity.products);
4409
+ if (Array.isArray(activity?.product_list)) groups.push(activity.product_list);
4410
+ if (Array.isArray(activity?.get_props)) groups.push(activity.get_props);
4411
+ if (Array.isArray(activity?.get_extra_props)) groups.push(activity.get_extra_props);
4412
+ if (Array.isArray(activity?.get_pets)) groups.push(activity.get_pets);
4413
+ const merged = [];
4414
+ const seen = /* @__PURE__ */ new Set();
4415
+ for (const list of groups) {
4416
+ for (const item of list) {
4417
+ const start = normalizeTimestamp(item?.start_time) ?? 0;
4418
+ const end = normalizeTimestamp(item?.end_time) ?? Infinity;
4419
+ const key = `${item?.id ?? ""}|${item?.name ?? ""}|${start}|${end === Infinity ? "inf" : end}`;
4420
+ if (seen.has(key)) continue;
4421
+ seen.add(key);
4422
+ merged.push(item);
4423
+ }
4424
+ }
4425
+ return merged;
4426
+ }
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);
4233
4476
  const hours = Math.floor(diff / 36e5);
4234
4477
  const mins = Math.floor(diff % 36e5 / 6e4);
4235
- const datePart = [
4236
- now.getFullYear(),
4237
- String(now.getMonth() + 1).padStart(2, "0"),
4238
- String(now.getDate()).padStart(2, "0")
4239
- ].join("-");
4240
4478
  return {
4241
4479
  current: currentRound,
4242
- total: rounds.length,
4480
+ total: ROUND_WINDOWS.length,
4243
4481
  countdown: `${hours}小时${mins}分钟`,
4244
4482
  is_open: currentRound !== null,
4245
4483
  round_id: `${datePart}-${currentRound || "closed"}`
@@ -4280,36 +4518,258 @@ function sameStringArray(left, right) {
4280
4518
  return JSON.stringify(left) === JSON.stringify(right);
4281
4519
  }
4282
4520
  __name(sameStringArray, "sameStringArray");
4283
- function buildMerchantRenderPayload(res) {
4284
- const products = getActiveProducts(res);
4285
- const roundInfo = getCurrentMerchantRound();
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)
4587
+ }));
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("");
4622
+ }
4623
+ return fallbackLines.join("\n").trimEnd();
4624
+ }
4625
+ __name(buildMerchantFallbackText, "buildMerchantFallbackText");
4626
+ function getMerchantActivityTitle(res) {
4286
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: []
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` : "--:--~--:--";
4287
4673
  const data = {
4288
- background: "",
4289
- title: activity.name || TEXT.merchant,
4290
- subtitle: activity.start_date || "每日 08:00 / 12:00 / 16:00 / 20:00 刷新",
4291
- titleIcon: true,
4292
- product_count: products.length,
4293
- 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
- }))
4674
+ dateStr: getMerchantDateStr(now),
4675
+ timeRange,
4676
+ ...buildMerchantCardItems(products, res, { includeEnded: false })
4299
4677
  };
4300
- const fallback = products.length ? `远行商人当前商品:${products.map((p) => p.name || TEXT.unknown).join("、")}
4301
- 轮次:${roundInfo.current || TEXT.notOpen}
4302
- 剩余:${roundInfo.countdown}` : "当前远行商人暂无商品。";
4678
+ const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
4303
4679
  return { products, roundInfo, data, fallback };
4304
4680
  }
4305
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);
4690
+ const data = {
4691
+ background: "",
4692
+ title: getMerchantActivityTitle(res),
4693
+ subtitle: getMerchantActivitySubtitle(res),
4694
+ categories,
4695
+ roundGroups,
4696
+ total_products: products.length
4697
+ };
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 };
4744
+ }
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");
4306
4766
  async function checkMerchantSubscriptions(deps) {
4307
4767
  const { ctx, client, merchantSubMgr, renderer, config } = deps;
4308
4768
  const res = await client.getMerchantInfo(ctx, true);
4309
4769
  if (!res) return { subscriptions: 0, matched: 0, pushed: 0 };
4310
- const { products, roundInfo, data, fallback } = buildMerchantRenderPayload(res);
4770
+ const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
4311
4771
  const productNames = products.map((p) => p.name || "").filter(Boolean);
4312
- const rendered = await renderer.renderHtml(ctx, "yuanxing-shangren", data);
4772
+ const rendered = await renderer.renderHtml(ctx, templateName, data);
4313
4773
  const renderedImage = rendered ? compressPngImage(rendered, config) : null;
4314
4774
  const subs = merchantSubMgr.getAll();
4315
4775
  let matchedCount = 0;
@@ -4359,10 +4819,17 @@ function register3(deps) {
4359
4819
  ctx.command(TEXT.merchant, "查看远行商人商品").action(async ({ session }) => {
4360
4820
  const res = await client.getMerchantInfo(ctx, true);
4361
4821
  if (!res) return `获取远行商人数据失败:${client.getLastErrorBrief()}`;
4362
- const { data, fallback } = buildMerchantRenderPayload(res);
4363
- 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);
4364
4824
  await sendImageWithFallback(session, png, fallback, "merchant:yuanxing-shangren", deps.config);
4365
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
+ });
4366
4833
  ctx.command(`${TEXT.subscribe} [args:text]`, "订阅远行商人商品提醒").action(async ({ session }, args) => {
4367
4834
  const target = getSubscriptionTarget(session);
4368
4835
  if (!target.privateChat && !isBotAdmin2(session, config.adminUserIds)) return "此指令仅限管理员使用。";
@@ -4631,7 +5098,7 @@ __name(register5, "register");
4631
5098
  var import_koishi10 = require("koishi");
4632
5099
 
4633
5100
  // src/activities-service.ts
4634
- var CHINA_TIMEZONE = "Asia/Shanghai";
5101
+ var CHINA_TIMEZONE2 = "Asia/Shanghai";
4635
5102
  var DAY_MS = 24 * 60 * 60 * 1e3;
4636
5103
  var ACTIVITY_THEMES = ["gold", "green", "brown"];
4637
5104
  var LOOKBACK_DAYS = 10;
@@ -4639,12 +5106,12 @@ var MAX_LOOKAHEAD_DAYS = 50;
4639
5106
  var TRAILING_DAYS_AFTER_LAST_ACTIVITY = 3;
4640
5107
  var MIN_LOOKAHEAD_DAYS = 7;
4641
5108
  var chinaDateFormatter = new Intl.DateTimeFormat("zh-CN", {
4642
- timeZone: CHINA_TIMEZONE,
5109
+ timeZone: CHINA_TIMEZONE2,
4643
5110
  month: "2-digit",
4644
5111
  day: "2-digit"
4645
5112
  });
4646
5113
  var chinaDateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
4647
- timeZone: CHINA_TIMEZONE,
5114
+ timeZone: CHINA_TIMEZONE2,
4648
5115
  month: "2-digit",
4649
5116
  day: "2-digit",
4650
5117
  hour: "2-digit",
@@ -4652,7 +5119,7 @@ var chinaDateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
4652
5119
  hour12: false
4653
5120
  });
4654
5121
  var chinaDatePartsFormatter = new Intl.DateTimeFormat("zh-CN", {
4655
- timeZone: CHINA_TIMEZONE,
5122
+ timeZone: CHINA_TIMEZONE2,
4656
5123
  year: "numeric",
4657
5124
  month: "2-digit",
4658
5125
  day: "2-digit"
@@ -5259,13 +5726,19 @@ var Config = import_koishi12.Schema.intersect([
5259
5726
  import_koishi12.Schema.object({
5260
5727
  merchantSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("启用远行商人订阅"),
5261
5728
  merchantSubscriptionItems: import_koishi12.Schema.array(String).default(["国王球", "棱镜球", "炫彩精灵蛋"]).description("默认订阅商品"),
5729
+ merchantUiStyle: import_koishi12.Schema.union(["new", "old"]).default("new").description("远行商人 UI 样式:new 为新版大图卡片,old 为旧版列表卡片"),
5262
5730
  merchantCheckMode: import_koishi12.Schema.union(["interval", "times"]).default("interval").description("商人检查模式:interval 定期轮询 / times 指定时间点"),
5263
5731
  merchantCheckInterval: import_koishi12.Schema.number().default(3e5).description("商人检查间隔,单位毫秒(仅 interval 模式生效)"),
5264
5732
  merchantCheckTimes: import_koishi12.Schema.array(String).default([]).description("商人定时检查时间点(HH:MM 格式,如 08:00,仅 times 模式生效)"),
5265
5733
  merchantPrivateSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("允许个人私聊订阅远行商人推送"),
5266
5734
  homeSubscriptionEnabled: import_koishi12.Schema.boolean().default(true).description("启用家园菜园和灵感订阅推送"),
5267
5735
  homeSubscriptionIntervalMinutes: import_koishi12.Schema.number().default(5).description("家园订阅检查间隔,单位分钟")
5268
- }).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("家园查询排队设置")
5269
5742
  ]);
5270
5743
  function apply(ctx, config) {
5271
5744
  setupRoleTokenModel(ctx);