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.
- package/lib/client.d.ts +17 -1
- package/lib/client.js +121 -21
- package/lib/commands/merchant.js +409 -61
- package/lib/commands/query.js +95 -27
- package/lib/index.d.ts +4 -0
- package/lib/index.js +600 -127
- package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
- package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
- package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
- package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
- package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
- package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
- package/lib/render-templates/home/index.html +81 -97
- package/lib/render-templates/home/style.css +316 -375
- package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
- package/lib/render-templates/yuanxing-shangren/README.md +86 -0
- package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
- 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
- package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
- 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
- package/lib/render-templates/yuanxing-shangren/index.html +233 -148
- package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
- package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
- package/lib/render-templates/yuanxing-shangren/style.css +201 -256
- package/lib/render-templates/yuanxing-shangren/today.html +461 -0
- package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
- package/lib/render.js +11 -4
- package/lib/subscription-send.js +4 -5
- package/lib/ttf/rocom_skill_origin.ttf +0 -0
- 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
- package/lib/types.d.ts +4 -0
- 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
|
|
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(), {
|
|
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,
|
|
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
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3269
|
-
const
|
|
3270
|
-
const
|
|
3271
|
-
const
|
|
3272
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
4181
|
-
|
|
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
|
|
4187
|
-
|
|
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
|
|
4405
|
+
function getMerchantProducts(res) {
|
|
4205
4406
|
const activity = getMerchantActivity(res);
|
|
4206
|
-
const
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
const
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
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:
|
|
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
|
|
4284
|
-
const
|
|
4285
|
-
const
|
|
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
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
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 =
|
|
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 } =
|
|
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,
|
|
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 } =
|
|
4363
|
-
const png = await deps.renderer.renderHtml(ctx,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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);
|