koishi-plugin-rocom 1.0.12 → 1.0.17
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/atlas-service.d.ts +33 -0
- package/lib/atlas-service.js +439 -0
- package/lib/client.d.ts +36 -1
- package/lib/client.js +221 -21
- package/lib/commands/account.js +1 -0
- package/lib/commands/merchant.js +351 -91
- package/lib/commands/query.js +162 -14
- package/lib/commands/tools.js +104 -0
- package/lib/commands/wiki.js +167 -6
- package/lib/egg-service.d.ts +2 -0
- package/lib/egg-service.js +50 -15
- package/lib/index.d.ts +9 -0
- package/lib/index.js +3626 -303
- package/lib/pet-data-service.d.ts +62 -0
- package/lib/pet-data-service.js +468 -0
- 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/pet-data/index.html +172 -0
- package/lib/render-templates/pet-data/style.css +553 -0
- package/lib/render-templates/wiki/detail/index.html +84 -0
- package/lib/render-templates/wiki/list/index.html +84 -0
- package/lib/render-templates/wiki/menu/index.html +65 -0
- package/lib/render-templates/wiki/pet/index.html +158 -0
- package/lib/render-templates/wiki/pet/style.css +393 -0
- package/lib/render-templates/wiki/shared/style.css +431 -0
- package/lib/render-templates/wiki/skill/index.html +79 -0
- package/lib/render-templates/wiki/skill/style.css +233 -0
- package/lib/render-templates/wiki/suggestions/index.html +84 -0
- 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 +198 -170
- 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 +48 -12
- 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 +13 -1
- package/lib/user.d.ts +23 -1
- package/lib/user.js +32 -1
- package/lib/wiki-catalog.d.ts +19 -0
- package/lib/wiki-catalog.js +124 -0
- package/lib/wiki-service.d.ts +220 -0
- package/lib/wiki-service.js +1463 -0
- package/package.json +1 -1
- package/readme.md +4 -5
package/lib/index.js
CHANGED
|
@@ -36,14 +36,17 @@ __export(src_exports, {
|
|
|
36
36
|
name: () => name
|
|
37
37
|
});
|
|
38
38
|
module.exports = __toCommonJS(src_exports);
|
|
39
|
-
var
|
|
40
|
-
var
|
|
41
|
-
var
|
|
39
|
+
var import_koishi15 = require("koishi");
|
|
40
|
+
var import_node_fs7 = __toESM(require("node:fs"));
|
|
41
|
+
var import_node_path6 = __toESM(require("node:path"));
|
|
42
42
|
|
|
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
|
}
|
|
@@ -171,14 +174,14 @@ var RocomClient = class {
|
|
|
171
174
|
return String(value);
|
|
172
175
|
}
|
|
173
176
|
}
|
|
174
|
-
logRequestFailureDetails(method,
|
|
177
|
+
logRequestFailureDetails(method, path7, headers, params, body, errorOrResponse) {
|
|
175
178
|
const err = errorOrResponse;
|
|
176
179
|
const response = err?.response;
|
|
177
180
|
const details = {
|
|
178
181
|
request: {
|
|
179
182
|
method,
|
|
180
|
-
url: `${this.baseUrl}${
|
|
181
|
-
path:
|
|
183
|
+
url: `${this.baseUrl}${path7}`,
|
|
184
|
+
path: path7,
|
|
182
185
|
params: this.sanitizeForLog(params),
|
|
183
186
|
body: this.sanitizeForLog(body),
|
|
184
187
|
headers: this.sanitizeForLog(headers),
|
|
@@ -198,21 +201,21 @@ var RocomClient = class {
|
|
|
198
201
|
stack: err?.stack
|
|
199
202
|
} : void 0
|
|
200
203
|
};
|
|
201
|
-
logger.warn(`${method} ${
|
|
204
|
+
logger.warn(`${method} ${path7} detailed failure
|
|
202
205
|
${this.stringifyForLog(details)}`);
|
|
203
206
|
}
|
|
204
|
-
async get(ctx,
|
|
207
|
+
async get(ctx, path7, headers, params, options) {
|
|
205
208
|
try {
|
|
206
|
-
const resp = await ctx.http.get(`${this.baseUrl}${
|
|
209
|
+
const resp = await ctx.http.get(`${this.baseUrl}${path7}`, {
|
|
207
210
|
headers,
|
|
208
211
|
params,
|
|
209
212
|
timeout: this.timeout
|
|
210
213
|
});
|
|
211
214
|
if (resp?.code !== 0) {
|
|
212
215
|
this.setLastError(resp?.message || "接口返回异常");
|
|
213
|
-
logger.warn(
|
|
216
|
+
logger.warn(path7 + " error: " + (resp?.message || "unknown"));
|
|
214
217
|
if (!options?.silentFailureDetails) {
|
|
215
|
-
this.logRequestFailureDetails("GET",
|
|
218
|
+
this.logRequestFailureDetails("GET", path7, headers, params, void 0, resp);
|
|
216
219
|
}
|
|
217
220
|
return null;
|
|
218
221
|
}
|
|
@@ -221,40 +224,40 @@ ${this.stringifyForLog(details)}`);
|
|
|
221
224
|
const message = this.formatHttpError(e);
|
|
222
225
|
this.setLastError(message);
|
|
223
226
|
if (!options?.silentFailureDetails) {
|
|
224
|
-
this.logRequestFailureDetails("GET",
|
|
227
|
+
this.logRequestFailureDetails("GET", path7, headers, params, void 0, e);
|
|
225
228
|
}
|
|
226
229
|
const err = e;
|
|
227
230
|
if (err?.response) {
|
|
228
|
-
logger.warn("GET " +
|
|
231
|
+
logger.warn("GET " + path7 + " failed: " + message);
|
|
229
232
|
} else {
|
|
230
|
-
logger.error("GET " +
|
|
233
|
+
logger.error("GET " + path7 + " failed: " + message);
|
|
231
234
|
}
|
|
232
235
|
return null;
|
|
233
236
|
}
|
|
234
237
|
}
|
|
235
|
-
async post(ctx,
|
|
238
|
+
async post(ctx, path7, headers, json, params) {
|
|
236
239
|
try {
|
|
237
|
-
const resp = await ctx.http.post(`${this.baseUrl}${
|
|
240
|
+
const resp = await ctx.http.post(`${this.baseUrl}${path7}`, json, {
|
|
238
241
|
headers,
|
|
239
242
|
params,
|
|
240
243
|
timeout: this.timeout
|
|
241
244
|
});
|
|
242
245
|
if (resp?.code !== 0) {
|
|
243
246
|
this.setLastError(resp?.message || "接口返回异常");
|
|
244
|
-
logger.warn(
|
|
245
|
-
this.logRequestFailureDetails("POST",
|
|
247
|
+
logger.warn(path7 + " error: " + (resp?.message || "unknown"));
|
|
248
|
+
this.logRequestFailureDetails("POST", path7, headers, params, json, resp);
|
|
246
249
|
return null;
|
|
247
250
|
}
|
|
248
251
|
return resp?.data ?? {};
|
|
249
252
|
} catch (e) {
|
|
250
253
|
const message = this.formatHttpError(e);
|
|
251
254
|
this.setLastError(message);
|
|
252
|
-
this.logRequestFailureDetails("POST",
|
|
255
|
+
this.logRequestFailureDetails("POST", path7, headers, params, json, e);
|
|
253
256
|
const err = e;
|
|
254
257
|
if (err?.response) {
|
|
255
|
-
logger.warn("POST " +
|
|
258
|
+
logger.warn("POST " + path7 + " failed: " + message);
|
|
256
259
|
} else {
|
|
257
|
-
logger.error("POST " +
|
|
260
|
+
logger.error("POST " + path7 + " failed: " + message);
|
|
258
261
|
}
|
|
259
262
|
return null;
|
|
260
263
|
}
|
|
@@ -264,34 +267,34 @@ ${this.stringifyForLog(details)}`);
|
|
|
264
267
|
if (userIdentifier) result.user_identifier = this.sanitizeUid(userIdentifier);
|
|
265
268
|
return result;
|
|
266
269
|
}
|
|
267
|
-
async delete(ctx,
|
|
270
|
+
async delete(ctx, path7, headers, params) {
|
|
268
271
|
try {
|
|
269
|
-
const resp = await ctx.http("DELETE", `${this.baseUrl}${
|
|
272
|
+
const resp = await ctx.http("DELETE", `${this.baseUrl}${path7}`, { headers, params, timeout: this.timeout });
|
|
270
273
|
if (resp?.code !== 0) {
|
|
271
274
|
this.setLastError(resp?.message || "接口返回异常");
|
|
272
|
-
logger.warn(
|
|
273
|
-
this.logRequestFailureDetails("DELETE",
|
|
275
|
+
logger.warn(path7 + " error: " + (resp?.message || "unknown"));
|
|
276
|
+
this.logRequestFailureDetails("DELETE", path7, headers, params, void 0, resp);
|
|
274
277
|
return null;
|
|
275
278
|
}
|
|
276
279
|
return resp?.data ?? {};
|
|
277
280
|
} catch (e) {
|
|
278
281
|
const message = this.formatHttpError(e);
|
|
279
282
|
this.setLastError(message);
|
|
280
|
-
this.logRequestFailureDetails("DELETE",
|
|
283
|
+
this.logRequestFailureDetails("DELETE", path7, headers, params, void 0, e);
|
|
281
284
|
const err = e;
|
|
282
285
|
if (err?.response) {
|
|
283
|
-
logger.warn("DELETE " +
|
|
286
|
+
logger.warn("DELETE " + path7 + " failed: " + message);
|
|
284
287
|
} else {
|
|
285
|
-
logger.error("DELETE " +
|
|
288
|
+
logger.error("DELETE " + path7 + " failed: " + message);
|
|
286
289
|
}
|
|
287
290
|
return null;
|
|
288
291
|
}
|
|
289
292
|
}
|
|
290
293
|
// Login and binding APIs.
|
|
291
|
-
async requestWithStatus(ctx, method,
|
|
294
|
+
async requestWithStatus(ctx, method, path7, headers, options = {}) {
|
|
292
295
|
const acceptedStatuses = options.acceptedStatuses || [200];
|
|
293
296
|
try {
|
|
294
|
-
const response = await ctx.http(method, `${this.baseUrl}${
|
|
297
|
+
const response = await ctx.http(method, `${this.baseUrl}${path7}`, {
|
|
295
298
|
headers,
|
|
296
299
|
params: options.params,
|
|
297
300
|
data: options.json,
|
|
@@ -305,7 +308,7 @@ ${this.stringifyForLog(details)}`);
|
|
|
305
308
|
const message = !acceptedStatuses.includes(status) ? `HTTP ${status}: ${rawMessage}` : rawMessage;
|
|
306
309
|
this.setLastError(message);
|
|
307
310
|
if (!options.silentFailureDetails) {
|
|
308
|
-
this.logRequestFailureDetails(method,
|
|
311
|
+
this.logRequestFailureDetails(method, path7, headers, options.params, options.json, body);
|
|
309
312
|
}
|
|
310
313
|
return { status: null, data: null };
|
|
311
314
|
}
|
|
@@ -313,7 +316,7 @@ ${this.stringifyForLog(details)}`);
|
|
|
313
316
|
if (!acceptedStatuses.includes(status)) {
|
|
314
317
|
this.setLastError(`HTTP ${status}`);
|
|
315
318
|
if (!options.silentFailureDetails) {
|
|
316
|
-
this.logRequestFailureDetails(method,
|
|
319
|
+
this.logRequestFailureDetails(method, path7, headers, options.params, options.json, response);
|
|
317
320
|
}
|
|
318
321
|
return { status: null, data: null };
|
|
319
322
|
}
|
|
@@ -322,22 +325,22 @@ ${this.stringifyForLog(details)}`);
|
|
|
322
325
|
const message = this.formatHttpError(e);
|
|
323
326
|
this.setLastError(message);
|
|
324
327
|
if (!options.silentFailureDetails) {
|
|
325
|
-
this.logRequestFailureDetails(method,
|
|
328
|
+
this.logRequestFailureDetails(method, path7, headers, options.params, options.json, e);
|
|
326
329
|
}
|
|
327
330
|
return { status: null, data: null };
|
|
328
331
|
}
|
|
329
332
|
}
|
|
330
|
-
async requestIngameWithFallback(ctx,
|
|
333
|
+
async requestIngameWithFallback(ctx, path7, payload, options = {}) {
|
|
331
334
|
const acceptedStatuses = options.acceptedStatuses || [200, 202];
|
|
332
335
|
const requestOnce = /* @__PURE__ */ __name(async (includeApiKey, silentFailureDetails) => {
|
|
333
336
|
const headers = this.wegameHeaders("", "", "", "", includeApiKey);
|
|
334
|
-
let result2 = await this.requestWithStatus(ctx, "POST",
|
|
337
|
+
let result2 = await this.requestWithStatus(ctx, "POST", path7, headers, {
|
|
335
338
|
json: payload,
|
|
336
339
|
acceptedStatuses,
|
|
337
340
|
silentFailureDetails
|
|
338
341
|
});
|
|
339
342
|
if (result2.status === null) {
|
|
340
|
-
result2 = await this.requestWithStatus(ctx, "GET",
|
|
343
|
+
result2 = await this.requestWithStatus(ctx, "GET", path7, headers, {
|
|
341
344
|
params: payload,
|
|
342
345
|
acceptedStatuses,
|
|
343
346
|
silentFailureDetails
|
|
@@ -362,6 +365,100 @@ ${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
|
+
// 家园详情(pet/data)完成时返回 npc_pets/npc_pet。
|
|
377
|
+
static isCompletedGatewayPayload(payload) {
|
|
378
|
+
if (!payload || typeof payload !== "object") return false;
|
|
379
|
+
if (Array.isArray(payload.rows)) return true;
|
|
380
|
+
if (payload.home_info !== void 0) return true;
|
|
381
|
+
if (payload.source !== void 0) return true;
|
|
382
|
+
if (Array.isArray(payload.npc_pets)) return true;
|
|
383
|
+
if (payload.npc_pet && typeof payload.npc_pet === "object") return true;
|
|
384
|
+
if (String(payload.title || "").trim()) return true;
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
static extractCompletedIngamePayload(payload) {
|
|
388
|
+
if (!payload || typeof payload !== "object") return null;
|
|
389
|
+
if (_RocomClient.isCompletedGatewayPayload(payload)) return payload;
|
|
390
|
+
if (_RocomClient.isCompletedGatewayPayload(payload.result)) return payload.result;
|
|
391
|
+
if (_RocomClient.isCompletedGatewayPayload(payload.data)) return payload.data;
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
static taskErrorMessage(payload, fallbackStatus = "") {
|
|
395
|
+
const candidates = [
|
|
396
|
+
payload?.message,
|
|
397
|
+
payload?.error,
|
|
398
|
+
payload?.error_message,
|
|
399
|
+
payload?.reason,
|
|
400
|
+
payload?.result?.message,
|
|
401
|
+
payload?.result?.error,
|
|
402
|
+
payload?.result?.error_message,
|
|
403
|
+
payload?.data?.message,
|
|
404
|
+
payload?.data?.error
|
|
405
|
+
];
|
|
406
|
+
for (const candidate of candidates) {
|
|
407
|
+
const text = String(candidate ?? "").trim();
|
|
408
|
+
if (text && text.toLowerCase() !== "failed") return text;
|
|
409
|
+
}
|
|
410
|
+
return String(payload?.status || fallbackStatus || "").trim() || "任务执行失败";
|
|
411
|
+
}
|
|
412
|
+
// 排队等候轮询:把 ingame 接口提交后返回的 202(task_id/status=queued)轮询到出结果或超时。
|
|
413
|
+
// 入参 first 是首请求拿到的 { status, data, usedApiKey }。
|
|
414
|
+
async pollIngameTask(ctx, first, options, labels) {
|
|
415
|
+
const { status, data, usedApiKey } = first;
|
|
416
|
+
const initialCompletedPayload = _RocomClient.extractCompletedIngamePayload(data);
|
|
417
|
+
if (status !== null && initialCompletedPayload) return initialCompletedPayload;
|
|
418
|
+
const taskId = _RocomClient.extractTaskId(data);
|
|
419
|
+
if (!taskId) {
|
|
420
|
+
if (status === 202) this.setLastError(labels.queuedNoTaskId);
|
|
421
|
+
return status === null ? null : data;
|
|
422
|
+
}
|
|
423
|
+
if (typeof options.onQueued === "function") {
|
|
424
|
+
try {
|
|
425
|
+
await options.onQueued(taskId);
|
|
426
|
+
} catch (e) {
|
|
427
|
+
logger.warn(`ingame 排队提示回调失败: ${e}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const intervalMs = Math.max(300, Number(options.intervalMs) || 3e3);
|
|
431
|
+
const timeoutMs = Math.max(intervalMs, Number(options.timeoutMs) || 18e4);
|
|
432
|
+
const startedAt = Date.now();
|
|
433
|
+
let lastStatus = _RocomClient.normalizeTaskStatus(data?.status);
|
|
434
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
435
|
+
if (lastStatus && INGAME_FAILED_STATUSES.includes(lastStatus)) {
|
|
436
|
+
this.setLastError(_RocomClient.taskErrorMessage(data, lastStatus));
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
440
|
+
const task = await this.getIngameTask(ctx, taskId, usedApiKey);
|
|
441
|
+
if (task.status === null) return null;
|
|
442
|
+
const completedPayload = _RocomClient.extractCompletedIngamePayload(task.data);
|
|
443
|
+
if (completedPayload) return completedPayload;
|
|
444
|
+
lastStatus = _RocomClient.normalizeTaskStatus(task.data?.status);
|
|
445
|
+
if (INGAME_FAILED_STATUSES.includes(lastStatus)) {
|
|
446
|
+
this.setLastError(_RocomClient.taskErrorMessage(task.data, lastStatus));
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
if (INGAME_COMPLETED_STATUSES.includes(lastStatus)) {
|
|
450
|
+
this.setLastError(`Ingame 任务已完成但未返回可解析结果:${taskId}`);
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
if (lastStatus && !INGAME_PENDING_STATUSES.includes(lastStatus)) {
|
|
454
|
+
this.setLastError(`Ingame 任务状态异常:${lastStatus}`);
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
if (!_RocomClient.extractTaskId(task.data) && !lastStatus) return task.data;
|
|
458
|
+
}
|
|
459
|
+
this.setLastError(labels.stillQueued(taskId));
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
365
462
|
async qqQrLogin(ctx, userIdentifier) {
|
|
366
463
|
const params = { client_type: "bot", client_id: "koishi", provider: "rocom" };
|
|
367
464
|
if (userIdentifier) params.user_identifier = this.sanitizeUid(userIdentifier);
|
|
@@ -469,9 +566,9 @@ ${this.stringifyForLog(details)}`);
|
|
|
469
566
|
this.setLastError("UID 不能为空");
|
|
470
567
|
return null;
|
|
471
568
|
}
|
|
472
|
-
const
|
|
569
|
+
const path7 = "/api/v1/games/rocom/ingame/player/search";
|
|
473
570
|
const payload = { uid: sanitizedUid, wait_ms: 5e3 };
|
|
474
|
-
const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx,
|
|
571
|
+
const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path7, payload);
|
|
475
572
|
if (status === 200 && data && this.isIngamePlayerPayload(data)) return data;
|
|
476
573
|
if (!data) return null;
|
|
477
574
|
if (this.isIngamePlayerPayload(data)) return data;
|
|
@@ -503,9 +600,28 @@ ${this.stringifyForLog(details)}`);
|
|
|
503
600
|
return this.get(ctx, "/api/v1/games/rocom/exchange/posters", this.wegameHeaders(fwToken, userIdentifier), params);
|
|
504
601
|
}
|
|
505
602
|
async getMerchantInfo(ctx, refresh = false) {
|
|
506
|
-
return this.get(ctx, "/api/v1/games/rocom/merchant/info", this.wegameHeaders(), {
|
|
603
|
+
return this.get(ctx, "/api/v1/games/rocom/merchant/info", this.wegameHeaders(), {
|
|
604
|
+
refresh: refresh ? "true" : "false",
|
|
605
|
+
random_goods: "all"
|
|
606
|
+
});
|
|
507
607
|
}
|
|
508
608
|
async queryPetSize(ctx, diameter, weight, sameRideEgg = false, userIdentifier = "") {
|
|
609
|
+
const newParams = this.scopedParams({
|
|
610
|
+
diameter,
|
|
611
|
+
weight,
|
|
612
|
+
pool: sameRideEgg ? "ride" : "magic",
|
|
613
|
+
include_display_only: "false",
|
|
614
|
+
page_no: 1,
|
|
615
|
+
page_size: 30
|
|
616
|
+
}, userIdentifier);
|
|
617
|
+
const newRes = await this.get(
|
|
618
|
+
ctx,
|
|
619
|
+
"/api/v1/games/rocom/wiki/pet-size/query",
|
|
620
|
+
this.wegameHeaders("", userIdentifier, "bot", "koishi"),
|
|
621
|
+
newParams,
|
|
622
|
+
{ silentFailureDetails: true }
|
|
623
|
+
);
|
|
624
|
+
if (newRes && Array.isArray(newRes.items)) return newRes;
|
|
509
625
|
const params = this.scopedParams({ diameter, weight }, userIdentifier);
|
|
510
626
|
if (sameRideEgg) params.sameRideEgg = 1;
|
|
511
627
|
return this.get(ctx, "/api/v1/games/rocom/pet/size-query", this.wegameHeaders("", userIdentifier, "bot", "koishi"), params);
|
|
@@ -661,29 +777,20 @@ ${this.stringifyForLog(details)}`);
|
|
|
661
777
|
this.scopedParams(params, userIdentifier)
|
|
662
778
|
);
|
|
663
779
|
}
|
|
664
|
-
async ingameHomeInfo(ctx, uid,
|
|
780
|
+
async ingameHomeInfo(ctx, uid, options = {}) {
|
|
665
781
|
const sanitizedUid = this.sanitizeUid(uid);
|
|
666
782
|
if (!sanitizedUid) {
|
|
667
783
|
this.setLastError("UID 不能为空");
|
|
668
784
|
return null;
|
|
669
785
|
}
|
|
670
|
-
const
|
|
786
|
+
const waitMs = Number(options.waitMs) || 5e3;
|
|
787
|
+
const path7 = "/api/v1/games/rocom/ingame/home/info";
|
|
671
788
|
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;
|
|
789
|
+
const first = await this.requestIngameWithFallback(ctx, path7, payload);
|
|
790
|
+
return this.pollIngameTask(ctx, first, options, {
|
|
791
|
+
queuedNoTaskId: "家园查询任务已入队,但未返回 task_id",
|
|
792
|
+
stillQueued: /* @__PURE__ */ __name((taskId) => `家园查询任务仍在排队,请稍后重试(task_id: ${taskId})`, "stillQueued")
|
|
793
|
+
});
|
|
687
794
|
}
|
|
688
795
|
async ingameMerchantInfo(ctx, shopId) {
|
|
689
796
|
const params = { shop_id: shopId };
|
|
@@ -694,6 +801,25 @@ ${this.stringifyForLog(details)}`);
|
|
|
694
801
|
);
|
|
695
802
|
return status === null ? null : data;
|
|
696
803
|
}
|
|
804
|
+
// 家园详情:批量或单只查询家园摆放精灵的完整 ingame 数据(上游 v3.6.1)。
|
|
805
|
+
// UID 参数名为 target_uin;单只查询需同时提供 pet_gid 和 npc_id。
|
|
806
|
+
async ingamePetData(ctx, uid, extras = {}, options = {}) {
|
|
807
|
+
const sanitizedUid = this.sanitizeUid(uid);
|
|
808
|
+
if (!sanitizedUid) {
|
|
809
|
+
this.setLastError("UID 不能为空");
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
const waitMs = Number(options.waitMs) || 2e4;
|
|
813
|
+
const payload = { target_uin: sanitizedUid, wait_ms: waitMs };
|
|
814
|
+
if (extras.petGid !== void 0 && extras.petGid !== null && String(extras.petGid) !== "") payload.pet_gid = extras.petGid;
|
|
815
|
+
if (extras.npcId !== void 0 && extras.npcId !== null && String(extras.npcId) !== "") payload.npc_id = extras.npcId;
|
|
816
|
+
const path7 = "/api/v1/games/rocom/ingame/pet/data";
|
|
817
|
+
const first = await this.requestIngameWithFallback(ctx, path7, payload);
|
|
818
|
+
return this.pollIngameTask(ctx, first, options, {
|
|
819
|
+
queuedNoTaskId: "家园详情任务已入队,但未返回 task_id",
|
|
820
|
+
stillQueued: /* @__PURE__ */ __name((taskId) => `家园详情任务仍在排队,请稍后重试(task_id: ${taskId})`, "stillQueued")
|
|
821
|
+
});
|
|
822
|
+
}
|
|
697
823
|
async getFriendship(ctx, fwToken, userIds, userIdentifier = "") {
|
|
698
824
|
return this.get(
|
|
699
825
|
ctx,
|
|
@@ -725,6 +851,66 @@ ${this.stringifyForLog(details)}`);
|
|
|
725
851
|
if (options.name) params.name = options.name;
|
|
726
852
|
return this.get(ctx, "/api/v1/games/rocom/pet/detail", this.wegameHeaders(), params);
|
|
727
853
|
}
|
|
854
|
+
// ===== 新版 RoCom Wiki API(上游 v3.6.0)=====
|
|
855
|
+
wikiPagedParams(q = "", pageNo = 1, pageSize = 10, filters = {}) {
|
|
856
|
+
const params = {
|
|
857
|
+
page_no: Math.max(Number(pageNo) || 1, 1),
|
|
858
|
+
page_size: Math.min(Math.max(Number(pageSize) || 10, 1), 100)
|
|
859
|
+
};
|
|
860
|
+
if (q) params.q = q;
|
|
861
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
862
|
+
if (value !== void 0 && value !== null && value !== "") params[key] = value;
|
|
863
|
+
}
|
|
864
|
+
return params;
|
|
865
|
+
}
|
|
866
|
+
async listWikiPets(ctx, q = "", pageNo = 1, pageSize = 10, filters = {}) {
|
|
867
|
+
return this.get(ctx, "/api/v1/games/rocom/wiki/pets", this.wegameHeaders(), this.wikiPagedParams(q, pageNo, pageSize, filters));
|
|
868
|
+
}
|
|
869
|
+
async getWikiPet(ctx, petId) {
|
|
870
|
+
return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}`, this.wegameHeaders());
|
|
871
|
+
}
|
|
872
|
+
async getWikiPetProfile(ctx, petId) {
|
|
873
|
+
return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/profile`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
|
|
874
|
+
}
|
|
875
|
+
async getWikiPetSkills(ctx, petId) {
|
|
876
|
+
return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/skills`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
|
|
877
|
+
}
|
|
878
|
+
async getWikiPetFamily(ctx, petId) {
|
|
879
|
+
return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/family`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
|
|
880
|
+
}
|
|
881
|
+
async getWikiPetHandbook(ctx, petId) {
|
|
882
|
+
return this.get(ctx, `/api/v1/games/rocom/wiki/pets/${petId}/handbook`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
|
|
883
|
+
}
|
|
884
|
+
async listWikiSkills(ctx, q = "", pageNo = 1, pageSize = 10, filters = {}) {
|
|
885
|
+
return this.get(ctx, "/api/v1/games/rocom/wiki/skills", this.wegameHeaders(), this.wikiPagedParams(q, pageNo, pageSize, filters));
|
|
886
|
+
}
|
|
887
|
+
async getWikiSkill(ctx, skillId) {
|
|
888
|
+
return this.get(ctx, `/api/v1/games/rocom/wiki/skills/${skillId}`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
|
|
889
|
+
}
|
|
890
|
+
async getWikiSkillPets(ctx, skillId) {
|
|
891
|
+
return this.get(ctx, `/api/v1/games/rocom/wiki/skills/${skillId}/pets`, this.wegameHeaders(), void 0, { silentFailureDetails: true });
|
|
892
|
+
}
|
|
893
|
+
async getWikiCatalogs(ctx) {
|
|
894
|
+
return this.get(ctx, "/api/v1/games/rocom/wiki/catalogs", this.wegameHeaders());
|
|
895
|
+
}
|
|
896
|
+
async getWikiOptions(ctx) {
|
|
897
|
+
return this.get(ctx, "/api/v1/games/rocom/wiki/options", this.wegameHeaders(), void 0, { silentFailureDetails: true });
|
|
898
|
+
}
|
|
899
|
+
async getWikiPath(ctx, path7, params = {}) {
|
|
900
|
+
const trimmed = String(path7 || "").trim();
|
|
901
|
+
if (!trimmed.startsWith("/api/v1/games/rocom/wiki/")) {
|
|
902
|
+
this.setLastError("非法 Wiki 路径");
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
return this.get(ctx, trimmed, this.wegameHeaders(), params);
|
|
906
|
+
}
|
|
907
|
+
async listWikiCatalogItems(ctx, path7, q = "", pageNo = 1, pageSize = 10, search = true) {
|
|
908
|
+
const params = this.wikiPagedParams(search ? q : "", pageNo, pageSize);
|
|
909
|
+
return this.getWikiPath(ctx, path7, params);
|
|
910
|
+
}
|
|
911
|
+
get wikiAssetBaseUrl() {
|
|
912
|
+
return this.baseUrl;
|
|
913
|
+
}
|
|
728
914
|
};
|
|
729
915
|
|
|
730
916
|
// src/user.ts
|
|
@@ -897,6 +1083,35 @@ var HomeSubscriptionManager = class {
|
|
|
897
1083
|
return JSON.parse(JSON.stringify(this.store.get()));
|
|
898
1084
|
}
|
|
899
1085
|
};
|
|
1086
|
+
var AnnouncementSubscriptionManager = class {
|
|
1087
|
+
static {
|
|
1088
|
+
__name(this, "AnnouncementSubscriptionManager");
|
|
1089
|
+
}
|
|
1090
|
+
store;
|
|
1091
|
+
constructor(dataDir) {
|
|
1092
|
+
this.store = new JsonStore(dataDir, "rocom_announcement_subscriptions.json", {});
|
|
1093
|
+
}
|
|
1094
|
+
upsert(key, sub) {
|
|
1095
|
+
const data = this.store.get();
|
|
1096
|
+
data[key] = { ...sub };
|
|
1097
|
+
this.store.set(data);
|
|
1098
|
+
}
|
|
1099
|
+
deleteMatching(target) {
|
|
1100
|
+
const data = this.store.get();
|
|
1101
|
+
let deleted = 0;
|
|
1102
|
+
for (const [key, sub] of Object.entries(data)) {
|
|
1103
|
+
const sameTarget = sub.platform === target.platform && (sub.channel_id || "") === (target.channelId || "") && (sub.user_id || "") === (target.userId || "");
|
|
1104
|
+
if (!sameTarget) continue;
|
|
1105
|
+
delete data[key];
|
|
1106
|
+
deleted++;
|
|
1107
|
+
}
|
|
1108
|
+
if (deleted) this.store.set(data);
|
|
1109
|
+
return deleted;
|
|
1110
|
+
}
|
|
1111
|
+
getAll() {
|
|
1112
|
+
return JSON.parse(JSON.stringify(this.store.get()));
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
900
1115
|
|
|
901
1116
|
// src/egg-service.ts
|
|
902
1117
|
var import_koishi3 = require("koishi");
|
|
@@ -945,8 +1160,8 @@ __name(petType, "petType");
|
|
|
945
1160
|
function fmtDur(s) {
|
|
946
1161
|
if (!s || s <= 0) return "暂无数据";
|
|
947
1162
|
if (s % 86400 === 0) return `${s / 86400} 天`;
|
|
948
|
-
const
|
|
949
|
-
return
|
|
1163
|
+
const h5 = s / 3600;
|
|
1164
|
+
return h5 === Math.floor(h5) ? `${h5} 小时` : `${h5.toFixed(1)} 小时`;
|
|
950
1165
|
}
|
|
951
1166
|
__name(fmtDur, "fmtDur");
|
|
952
1167
|
function wt(v) {
|
|
@@ -1152,20 +1367,29 @@ var EggService = class {
|
|
|
1152
1367
|
};
|
|
1153
1368
|
}
|
|
1154
1369
|
formatSizeApiCard(item) {
|
|
1155
|
-
const
|
|
1370
|
+
const petInfo = item?.pet && typeof item.pet === "object" ? item.pet : {};
|
|
1371
|
+
const eggSize = item?.egg_size && typeof item.egg_size === "object" ? item.egg_size : {};
|
|
1372
|
+
const heightRange = eggSize?.height && typeof eggSize.height === "object" ? eggSize.height : {};
|
|
1373
|
+
const weightRange = eggSize?.weight && typeof eggSize.weight === "object" ? eggSize.weight : {};
|
|
1374
|
+
const match = item?.match && typeof item.match === "object" ? item.match : {};
|
|
1375
|
+
const petId = petInfo?.pet_id || item?.petId || item?.pet_id || "-";
|
|
1376
|
+
const petName3 = petInfo?.name || (typeof item?.pet === "string" ? item.pet : "") || item?.name || "未知精灵";
|
|
1377
|
+
const typeNames = Array.isArray(petInfo?.type_names) ? petInfo.type_names.filter(Boolean) : [];
|
|
1378
|
+
const eggGroupNames = Array.isArray(petInfo?.egg_group_names) ? petInfo.egg_group_names.filter(Boolean) : [];
|
|
1379
|
+
const probability = num(item?.probability) ?? num(match?.percent);
|
|
1156
1380
|
const matchCount = num(item?.matchCount);
|
|
1157
|
-
const heightMin = num(item?.diameterMin);
|
|
1158
|
-
const heightMax = num(item?.diameterMax);
|
|
1159
|
-
const weightMin = num(item?.weightMin);
|
|
1160
|
-
const weightMax = num(item?.weightMax);
|
|
1381
|
+
const heightMin = num(heightRange?.min_m ?? item?.diameterMin);
|
|
1382
|
+
const heightMax = num(heightRange?.max_m ?? item?.diameterMax);
|
|
1383
|
+
const weightMin = num(weightRange?.min_kg ?? item?.weightMin);
|
|
1384
|
+
const weightMax = num(weightRange?.max_kg ?? item?.weightMax);
|
|
1161
1385
|
return {
|
|
1162
|
-
id:
|
|
1163
|
-
name:
|
|
1164
|
-
icon: item?.petIcon || petIconUrl(
|
|
1165
|
-
image: item?.petImage || petImageUrl(
|
|
1166
|
-
type_label: "后端未提供",
|
|
1386
|
+
id: petId,
|
|
1387
|
+
name: petName3,
|
|
1388
|
+
icon: petInfo?.icon || item?.petIcon || petIconUrl(petId),
|
|
1389
|
+
image: petInfo?.small_icon || item?.petImage || petImageUrl(petId),
|
|
1390
|
+
type_label: typeNames.length ? typeNames.join(" / ") : "后端未提供",
|
|
1167
1391
|
egg_group_ids: [],
|
|
1168
|
-
egg_groups_label: "后端未提供",
|
|
1392
|
+
egg_groups_label: eggGroupNames.length ? eggGroupNames.join(" / ") : "后端未提供",
|
|
1169
1393
|
height_min: heightMin,
|
|
1170
1394
|
height_max: heightMax,
|
|
1171
1395
|
height_label: fmtRange(heightMin, heightMax, "m"),
|
|
@@ -1174,7 +1398,30 @@ var EggService = class {
|
|
|
1174
1398
|
weight_label: fmtRange(weightMin, weightMax, "kg"),
|
|
1175
1399
|
probability,
|
|
1176
1400
|
match_count: matchCount,
|
|
1177
|
-
match_info_label: this.formatMatchSummary(probability, matchCount)
|
|
1401
|
+
match_info_label: String(match?.match_percent_text || "") || this.formatMatchSummary(probability, matchCount)
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
// 新版尺寸反查按 match.layer 划分完美/范围匹配。
|
|
1405
|
+
splitNewSizeApiItems(items) {
|
|
1406
|
+
const perfect = [];
|
|
1407
|
+
const ranged = [];
|
|
1408
|
+
for (const item of items) {
|
|
1409
|
+
const match = item?.match && typeof item.match === "object" ? item.match : {};
|
|
1410
|
+
const layer = String(match?.layer || "").toLowerCase();
|
|
1411
|
+
const displayOnly = Boolean(match?.display_only);
|
|
1412
|
+
if (["strict", "exact"].includes(layer) && !displayOnly) perfect.push(item);
|
|
1413
|
+
else ranged.push(item);
|
|
1414
|
+
}
|
|
1415
|
+
return [perfect, ranged];
|
|
1416
|
+
}
|
|
1417
|
+
sizeApiResultGroups(results) {
|
|
1418
|
+
if (Array.isArray(results?.items)) {
|
|
1419
|
+
const [perfect, ranged] = this.splitNewSizeApiItems(results.items);
|
|
1420
|
+
return { exact: perfect, candidates: ranged };
|
|
1421
|
+
}
|
|
1422
|
+
return {
|
|
1423
|
+
exact: Array.isArray(results?.exactResults) ? results.exactResults : [],
|
|
1424
|
+
candidates: Array.isArray(results?.candidates) ? results.candidates : []
|
|
1178
1425
|
};
|
|
1179
1426
|
}
|
|
1180
1427
|
formatEggSearchCard(item) {
|
|
@@ -1333,8 +1580,7 @@ var EggService = class {
|
|
|
1333
1580
|
if (height != null) cond.push(`身高=${heightDisplay || fmtRange(ht(height), ht(height), "m")}`);
|
|
1334
1581
|
if (weight != null) cond.push(`体重=${weight}kg`);
|
|
1335
1582
|
const condStr = cond.join(" + ") || "当前条件";
|
|
1336
|
-
const exact = results
|
|
1337
|
-
const candidates = results?.candidates || [];
|
|
1583
|
+
const { exact, candidates } = this.sizeApiResultGroups(results);
|
|
1338
1584
|
if (!exact.length && !candidates.length) return `❌ 未找到符合 ${condStr} 的精灵。`;
|
|
1339
1585
|
const lines = [];
|
|
1340
1586
|
if (exact.length) {
|
|
@@ -1547,9 +1793,10 @@ var EggService = class {
|
|
|
1547
1793
|
const conditions = [];
|
|
1548
1794
|
if (height != null) conditions.push(`身高 ${heightDisplay || fmtRange(ht(height), ht(height), "m")}`);
|
|
1549
1795
|
if (weight != null) conditions.push(`体重 ${weight} kg`);
|
|
1796
|
+
const groups = this.sizeApiResultGroups(results);
|
|
1550
1797
|
const [perfect, ranged] = this.mergeCardsByName(
|
|
1551
|
-
|
|
1552
|
-
|
|
1798
|
+
groups.exact.map((item) => this.formatSizeApiCard(item)),
|
|
1799
|
+
groups.candidates.map((item) => this.formatSizeApiCard(item))
|
|
1553
1800
|
);
|
|
1554
1801
|
const searchMode = results?.searchMode || "";
|
|
1555
1802
|
const queryLabel = `${conditions.join(" / ") || "尺寸反查"}${searchMode ? ` · 模式 ${searchMode}` : ""}`;
|
|
@@ -1634,6 +1881,11 @@ var TEMPLATE_CAPTURE_PADDING = {
|
|
|
1634
1881
|
var TEMPLATE_VIEWPORTS = {
|
|
1635
1882
|
activities: { width: 1600, height: 1200, deviceScaleFactor: 2 }
|
|
1636
1883
|
};
|
|
1884
|
+
var VISUAL_BOUNDS_TEMPLATES = /* @__PURE__ */ new Set([
|
|
1885
|
+
"home",
|
|
1886
|
+
"yuanxing-shangren/merchant",
|
|
1887
|
+
"yuanxing-shangren/today"
|
|
1888
|
+
]);
|
|
1637
1889
|
var DEFAULT_SCREENSHOT_OPTIONS = {
|
|
1638
1890
|
type: "jpeg",
|
|
1639
1891
|
quality: 82
|
|
@@ -1725,13 +1977,16 @@ var Renderer = class {
|
|
|
1725
1977
|
try {
|
|
1726
1978
|
await page.evaluate(async () => {
|
|
1727
1979
|
const images = Array.from(document.images);
|
|
1728
|
-
await Promise.
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1980
|
+
await Promise.race([
|
|
1981
|
+
Promise.all(images.map((img) => {
|
|
1982
|
+
if (img.complete) return Promise.resolve();
|
|
1983
|
+
return new Promise((resolve) => {
|
|
1984
|
+
img.onload = () => resolve();
|
|
1985
|
+
img.onerror = () => resolve();
|
|
1986
|
+
});
|
|
1987
|
+
})),
|
|
1988
|
+
new Promise((resolve) => setTimeout(resolve, 1e4))
|
|
1989
|
+
]);
|
|
1735
1990
|
const fonts = document.fonts;
|
|
1736
1991
|
if (fonts?.ready) {
|
|
1737
1992
|
await fonts.ready;
|
|
@@ -1748,6 +2003,8 @@ var Renderer = class {
|
|
|
1748
2003
|
".searcheggs-cont",
|
|
1749
2004
|
".bwiki-shell",
|
|
1750
2005
|
".skill-shell",
|
|
2006
|
+
".wiki-page",
|
|
2007
|
+
".pet-data-page",
|
|
1751
2008
|
".lineup-page",
|
|
1752
2009
|
".lineup-detail-page",
|
|
1753
2010
|
".page-section-main",
|
|
@@ -1760,6 +2017,7 @@ var Renderer = class {
|
|
|
1760
2017
|
".student-perks-page",
|
|
1761
2018
|
".student-page",
|
|
1762
2019
|
".merchant-page",
|
|
2020
|
+
".page",
|
|
1763
2021
|
".home-page",
|
|
1764
2022
|
".pet-panel-page",
|
|
1765
2023
|
".pet-detail-page",
|
|
@@ -1776,16 +2034,17 @@ var Renderer = class {
|
|
|
1776
2034
|
if (target) {
|
|
1777
2035
|
const box = await target.boundingBox();
|
|
1778
2036
|
if (box && box.width > 0 && box.height > 0) {
|
|
1779
|
-
const
|
|
2037
|
+
const useVisualBounds = VISUAL_BOUNDS_TEMPLATES.has(templateName);
|
|
2038
|
+
const elementMetrics = await page.evaluate((el, visualBounds) => {
|
|
1780
2039
|
const rect = el.getBoundingClientRect();
|
|
1781
2040
|
const element = el;
|
|
1782
2041
|
return {
|
|
1783
2042
|
x: rect.left + window.scrollX,
|
|
1784
2043
|
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)
|
|
2044
|
+
width: visualBounds ? rect.width : Math.max(rect.width, element.scrollWidth, element.offsetWidth),
|
|
2045
|
+
height: visualBounds ? rect.height : Math.max(rect.height, element.scrollHeight, element.offsetHeight)
|
|
1787
2046
|
};
|
|
1788
|
-
}, target);
|
|
2047
|
+
}, target, useVisualBounds);
|
|
1789
2048
|
const capturePadding = TEMPLATE_CAPTURE_PADDING[templateName] || { left: 0, right: 0, top: 0, bottom: 0 };
|
|
1790
2049
|
await page.setViewport({
|
|
1791
2050
|
width: Math.max(Math.ceil(elementMetrics.x + elementMetrics.width + capturePadding.right) + 8, 200),
|
|
@@ -1793,6 +2052,26 @@ var Renderer = class {
|
|
|
1793
2052
|
deviceScaleFactor: initialViewport.deviceScaleFactor
|
|
1794
2053
|
});
|
|
1795
2054
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2055
|
+
try {
|
|
2056
|
+
const remeasured = await page.evaluate((el, visualBounds) => {
|
|
2057
|
+
const rect = el.getBoundingClientRect();
|
|
2058
|
+
const element = el;
|
|
2059
|
+
return {
|
|
2060
|
+
x: rect.left + window.scrollX,
|
|
2061
|
+
y: rect.top + window.scrollY,
|
|
2062
|
+
width: visualBounds ? rect.width : Math.max(rect.width, element.scrollWidth, element.offsetWidth),
|
|
2063
|
+
height: visualBounds ? rect.height : Math.max(rect.height, element.scrollHeight, element.offsetHeight)
|
|
2064
|
+
};
|
|
2065
|
+
}, target, useVisualBounds);
|
|
2066
|
+
if (remeasured && remeasured.width > 0 && remeasured.height > 0) {
|
|
2067
|
+
elementMetrics.x = remeasured.x;
|
|
2068
|
+
elementMetrics.y = remeasured.y;
|
|
2069
|
+
elementMetrics.width = remeasured.width;
|
|
2070
|
+
elementMetrics.height = remeasured.height;
|
|
2071
|
+
}
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
logger4.warn(`element remeasure failed for ${templateName}: ${err}`);
|
|
2074
|
+
}
|
|
1796
2075
|
const hasOverflow = elementMetrics.width > box.width + 0.5 || elementMetrics.height > box.height + 0.5;
|
|
1797
2076
|
if (capturePadding.left || capturePadding.right || capturePadding.top || capturePadding.bottom || hasOverflow) {
|
|
1798
2077
|
const clipX = Math.max(0, elementMetrics.x - capturePadding.left);
|
|
@@ -1818,17 +2097,1992 @@ var Renderer = class {
|
|
|
1818
2097
|
return Buffer.isBuffer(screenshot) ? screenshot : Buffer.from(screenshot);
|
|
1819
2098
|
} finally {
|
|
1820
2099
|
try {
|
|
1821
|
-
await page.close();
|
|
1822
|
-
} catch {
|
|
2100
|
+
await page.close();
|
|
2101
|
+
} catch {
|
|
2102
|
+
}
|
|
2103
|
+
import_node_fs3.default.rmSync(tempDir, { recursive: true, force: true });
|
|
2104
|
+
}
|
|
2105
|
+
} catch (e) {
|
|
2106
|
+
logger4.error(`render failed: ${e}`);
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
// src/wiki-service.ts
|
|
2113
|
+
var import_koishi5 = require("koishi");
|
|
2114
|
+
|
|
2115
|
+
// src/wiki-catalog.ts
|
|
2116
|
+
function route(key, title, aliases, detailPath = "", idFields = [], search = null) {
|
|
2117
|
+
return { key, title, aliases, detail_path: detailPath, id_fields: idFields, search };
|
|
2118
|
+
}
|
|
2119
|
+
__name(route, "route");
|
|
2120
|
+
var WIKI_CATALOG_ROUTES = [
|
|
2121
|
+
route("pets", "精灵", ["精灵", "宠物", "pet", "pets"], "/api/v1/games/rocom/wiki/pets/{pet_id}", ["pet_id"]),
|
|
2122
|
+
route("pet-skills", "精灵技能", ["精灵技能", "宠物技能"], "/api/v1/games/rocom/wiki/pets/{pet_id}/skills", ["pet_id"], false),
|
|
2123
|
+
route("pet-features", "精灵特性", ["精灵特性", "宠物特性"], "/api/v1/games/rocom/wiki/pets/{pet_id}/features", ["pet_id"], false),
|
|
2124
|
+
route("pet-family", "精灵家族", ["精灵家族", "进化链", "形态链"], "/api/v1/games/rocom/wiki/pets/{pet_id}/family", ["pet_id"], false),
|
|
2125
|
+
route("pet-egg", "精灵蛋详情", ["精灵蛋详情", "宠物蛋详情"], "/api/v1/games/rocom/wiki/pets/{pet_id}/egg", ["pet_id"], false),
|
|
2126
|
+
route("pet-handbook", "精灵图鉴课题", ["精灵图鉴课题", "图鉴课题"], "/api/v1/games/rocom/wiki/pets/{pet_id}/handbook", ["pet_id"], false),
|
|
2127
|
+
route("pet-talents", "精灵天赋", ["精灵天赋", "同乘天赋"], "/api/v1/games/rocom/wiki/pets/{pet_id}/talents", ["pet_id"], false),
|
|
2128
|
+
route("pet-natures", "精灵性格", ["精灵性格", "性格"], "/api/v1/games/rocom/wiki/pets/{pet_id}/natures", ["pet_id"], false),
|
|
2129
|
+
route("pet-profile", "精灵资料", ["精灵资料", "资料页"], "/api/v1/games/rocom/wiki/pets/{pet_id}/profile", ["pet_id"], false),
|
|
2130
|
+
route("pet-fruit-section", "精灵关联果实", ["精灵关联果实"], "/api/v1/games/rocom/wiki/pets/{pet_id}/fruit", ["pet_id"], false),
|
|
2131
|
+
route("pet-ride", "精灵骑乘", ["精灵骑乘", "骑乘"], "/api/v1/games/rocom/wiki/pets/{pet_id}/ride", ["pet_id"], false),
|
|
2132
|
+
route("pet-ecology", "精灵生态", ["精灵生态", "生态"], "/api/v1/games/rocom/wiki/pets/{pet_id}/ecology", ["pet_id"], false),
|
|
2133
|
+
route("pet-stories", "精灵故事", ["精灵故事", "故事"], "/api/v1/games/rocom/wiki/pets/{pet_id}/stories", ["pet_id"], false),
|
|
2134
|
+
route("pet-acquisition", "精灵获取方式", ["精灵获取", "获取方式"], "/api/v1/games/rocom/wiki/pets/{pet_id}/acquisition", ["pet_id"], false),
|
|
2135
|
+
route("skills", "技能", ["技能", "skill", "skills"], "/api/v1/games/rocom/wiki/skills/{skill_id}", ["skill_id"]),
|
|
2136
|
+
route("skill-pets", "技能可用精灵", ["技能可用精灵", "技能精灵"], "/api/v1/games/rocom/wiki/skills/{skill_id}/pets", ["skill_id"], false),
|
|
2137
|
+
route("skill-stones", "技能石", ["技能石", "skillstone", "skill-stone"], "/api/v1/games/rocom/wiki/skill-stones/{item_id}", ["item_id"]),
|
|
2138
|
+
route("skill-stone-recipes", "技能石配方", ["技能石配方", "技能配方", "skillstonerecipe"], "/api/v1/games/rocom/wiki/skill-stone-recipes/{item_id}", ["item_id"]),
|
|
2139
|
+
route("items", "物品", ["物品", "道具", "item", "items"], "/api/v1/games/rocom/wiki/items/{item_kind}/{item_id}", ["item_kind", "item_id"]),
|
|
2140
|
+
route("egg-items", "蛋物品", ["蛋物品", "蛋道具", "egg-item"], "/api/v1/games/rocom/wiki/egg-items/{item_id}", ["item_id"]),
|
|
2141
|
+
route("pet-eggs", "精灵蛋", ["精灵蛋", "宠物蛋", "蛋图鉴", "pet-egg"], "/api/v1/games/rocom/wiki/pet-eggs/{egg_conf_id}", ["egg_conf_id"]),
|
|
2142
|
+
route("random-eggs", "随机蛋", ["随机蛋", "随机蛋池", "random-egg"], "/api/v1/games/rocom/wiki/random-eggs/{random_egg_id}", ["random_egg_id"]),
|
|
2143
|
+
route("pet-fruits", "精灵果实", ["精灵果实", "果实", "pet-fruit"], "/api/v1/games/rocom/wiki/pet-fruits/{item_id}", ["item_id"]),
|
|
2144
|
+
route("pet-foods", "精灵食物", ["精灵食物", "食物", "pet-food"], "/api/v1/games/rocom/wiki/pet-foods/{item_id}", ["item_id"]),
|
|
2145
|
+
route("pet-gifts", "精灵礼物", ["精灵礼物", "礼物", "pet-gift"], "/api/v1/games/rocom/wiki/pet-gifts/{item_id}", ["item_id"]),
|
|
2146
|
+
route("pet-close-level-effects", "亲密等级效果", ["亲密等级", "亲密效果"], "", ["level"], false),
|
|
2147
|
+
route("pet-action-close-exp", "行为亲密经验", ["行为亲密", "亲密经验"], "", [], false),
|
|
2148
|
+
route("home-egg-lay-rates", "家园产蛋概率", ["家园产蛋", "产蛋概率"], "", [], false),
|
|
2149
|
+
route("home-progression", "家园成长", ["家园成长", "家园升级"], "", [], false),
|
|
2150
|
+
route("pet-levels", "精灵等级经验", ["精灵等级", "等级经验", "pet-level"], "/api/v1/games/rocom/wiki/pet-levels/{level}", ["level"], false),
|
|
2151
|
+
route("handbook-areas", "图鉴区域", ["图鉴区域", "区域图鉴"], "", ["id"], false),
|
|
2152
|
+
route("fruit-trees", "果实树", ["果实树", "树"], "/api/v1/games/rocom/wiki/fruit-trees/{tree_id}", ["tree_id"]),
|
|
2153
|
+
route("fruit-refresh", "果实刷新精灵", ["果实刷新", "刷新精灵"], "/api/v1/games/rocom/wiki/fruit-refresh/{fruit_item_id}", ["fruit_item_id"], false),
|
|
2154
|
+
route("medals", "奖章", ["奖章", "勋章", "medal"], "/api/v1/games/rocom/wiki/medals/{medal_id}", ["medal_id"]),
|
|
2155
|
+
route("recipes", "食谱", ["食谱", "料理", "recipe"], "/api/v1/games/rocom/wiki/recipes/{item_id}", ["item_id"]),
|
|
2156
|
+
route("balls", "咕噜球", ["咕噜球", "球", "ball"], "/api/v1/games/rocom/wiki/balls/{ball_id}", ["ball_id"]),
|
|
2157
|
+
route("plants", "种植", ["种植", "作物", "植物", "plant"], "/api/v1/games/rocom/wiki/plants/{plant_id}", ["plant_id"]),
|
|
2158
|
+
route("pet-carryons", "精灵携带物", ["精灵携带物", "携带物", "carryon"], "/api/v1/games/rocom/wiki/pet-carryons/{carryon_id}", ["carryon_id"]),
|
|
2159
|
+
route("furniture", "家具", ["家具", "furniture"], "/api/v1/games/rocom/wiki/furniture/{furniture_id}", ["furniture_id"]),
|
|
2160
|
+
route("fashion", "服装套装", ["服装", "套装", "fashion"], "/api/v1/games/rocom/wiki/fashion/{suit_id}", ["suit_id"]),
|
|
2161
|
+
route("regions", "地区", ["地区", "区域", "region"], "/api/v1/games/rocom/wiki/regions/{region_id}", ["region_id"]),
|
|
2162
|
+
route("dungeons", "副本", ["副本", "dungeon"], "/api/v1/games/rocom/wiki/dungeons/{dungeon_id}", ["dungeon_id"]),
|
|
2163
|
+
route("mails", "邮件模板", ["邮件", "邮件模板", "mail"], "/api/v1/games/rocom/wiki/mails/{mail_id}", ["mail_id"]),
|
|
2164
|
+
route("music", "音乐", ["音乐", "music"], "/api/v1/games/rocom/wiki/music/{music_id}", ["music_id"]),
|
|
2165
|
+
route("profile-assets", "个人名片资产", ["名片", "名片资产", "profile-asset"], "/api/v1/games/rocom/wiki/profile-assets/{asset_type}/{asset_id}", ["asset_type", "asset_id"]),
|
|
2166
|
+
route("chat-emojis", "聊天表情", ["表情", "聊天表情", "emoji"], "/api/v1/games/rocom/wiki/chat-emojis/{emoji_id}", ["emoji_id"]),
|
|
2167
|
+
route("photo-actions", "拍照动作", ["拍照动作", "动作", "photo-action"], "/api/v1/games/rocom/wiki/photo-actions/{action_type}/{action_id}", ["action_type", "action_id"]),
|
|
2168
|
+
route("tasks", "任务", ["任务", "task"], "/api/v1/games/rocom/wiki/tasks/{task_id}", ["task_id"]),
|
|
2169
|
+
route("task-summaries", "剧情摘要", ["剧情", "剧情摘要", "task-summary"], "/api/v1/games/rocom/wiki/task-summaries/{summary_id}", ["summary_id"]),
|
|
2170
|
+
route("shops", "商店", ["商店", "shop"], "/api/v1/games/rocom/wiki/shops/{shop_id}", ["shop_id"]),
|
|
2171
|
+
route("exchanges", "兑换", ["兑换", "exchange"], "/api/v1/games/rocom/wiki/exchanges/{exchange_id}", ["exchange_id"])
|
|
2172
|
+
];
|
|
2173
|
+
var WIKI_CATALOG_ROUTES_BY_KEY = new Map(WIKI_CATALOG_ROUTES.map((item) => [item.key, item]));
|
|
2174
|
+
var WIKI_CATALOG_ROUTES_BY_ALIAS = /* @__PURE__ */ new Map();
|
|
2175
|
+
for (const item of WIKI_CATALOG_ROUTES) {
|
|
2176
|
+
for (const alias of [item.key, item.title, ...item.aliases]) {
|
|
2177
|
+
WIKI_CATALOG_ROUTES_BY_ALIAS.set(alias.toLowerCase(), item);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
var WIKI_GLOBAL_PRIORITIES = {
|
|
2181
|
+
"pets": 0,
|
|
2182
|
+
"skills": 0,
|
|
2183
|
+
"balls": 1,
|
|
2184
|
+
"pet-carryons": 1,
|
|
2185
|
+
"medals": 1,
|
|
2186
|
+
"plants": 1,
|
|
2187
|
+
"pet-fruits": 1,
|
|
2188
|
+
"recipes": 1,
|
|
2189
|
+
"pet-eggs": 1,
|
|
2190
|
+
"random-eggs": 1,
|
|
2191
|
+
"egg-items": 1,
|
|
2192
|
+
"skill-stones": 1,
|
|
2193
|
+
"skill-stone-recipes": 1,
|
|
2194
|
+
"pet-foods": 1,
|
|
2195
|
+
"pet-gifts": 1,
|
|
2196
|
+
"furniture": 1,
|
|
2197
|
+
"fashion": 1,
|
|
2198
|
+
"regions": 1,
|
|
2199
|
+
"dungeons": 1,
|
|
2200
|
+
"tasks": 1,
|
|
2201
|
+
"task-summaries": 1,
|
|
2202
|
+
"shops": 1,
|
|
2203
|
+
"exchanges": 1,
|
|
2204
|
+
"mails": 2,
|
|
2205
|
+
"music": 2,
|
|
2206
|
+
"chat-emojis": 2,
|
|
2207
|
+
"photo-actions": 2,
|
|
2208
|
+
"items": 3,
|
|
2209
|
+
"profile-assets": 4
|
|
2210
|
+
};
|
|
2211
|
+
function wikiGlobalCatalogPriority(catalog) {
|
|
2212
|
+
const key = String(catalog?.key || "");
|
|
2213
|
+
return WIKI_GLOBAL_PRIORITIES[key] ?? 2;
|
|
2214
|
+
}
|
|
2215
|
+
__name(wikiGlobalCatalogPriority, "wikiGlobalCatalogPriority");
|
|
2216
|
+
var WIKI_KEY_LABELS = {
|
|
2217
|
+
pet_id: "精灵ID",
|
|
2218
|
+
skill_id: "技能ID",
|
|
2219
|
+
item_id: "物品ID",
|
|
2220
|
+
item_kind: "物品类型",
|
|
2221
|
+
egg_conf_id: "蛋配置ID",
|
|
2222
|
+
random_egg_id: "随机蛋ID",
|
|
2223
|
+
tree_id: "果实树ID",
|
|
2224
|
+
medal_id: "奖章ID",
|
|
2225
|
+
ball_id: "咕噜球ID",
|
|
2226
|
+
plant_id: "种植ID",
|
|
2227
|
+
carryon_id: "携带物ID",
|
|
2228
|
+
furniture_id: "家具ID",
|
|
2229
|
+
suit_id: "套装ID",
|
|
2230
|
+
region_id: "地区ID",
|
|
2231
|
+
dungeon_id: "副本ID",
|
|
2232
|
+
mail_id: "邮件ID",
|
|
2233
|
+
music_id: "音乐ID",
|
|
2234
|
+
asset_type: "资产类型",
|
|
2235
|
+
asset_id: "资产ID",
|
|
2236
|
+
emoji_id: "表情ID",
|
|
2237
|
+
action_type: "动作类型",
|
|
2238
|
+
action_id: "动作ID",
|
|
2239
|
+
task_id: "任务ID",
|
|
2240
|
+
summary_id: "剧情ID",
|
|
2241
|
+
shop_id: "商店ID",
|
|
2242
|
+
exchange_id: "兑换ID",
|
|
2243
|
+
handbook_no: "图鉴编号",
|
|
2244
|
+
stage: "阶段",
|
|
2245
|
+
quality: "品质",
|
|
2246
|
+
cost: "能量",
|
|
2247
|
+
power: "威力",
|
|
2248
|
+
level: "等级",
|
|
2249
|
+
total_exp: "累计经验",
|
|
2250
|
+
next_level_exp: "升级所需",
|
|
2251
|
+
next_level_total_exp: "下级累计",
|
|
2252
|
+
hatch_label: "孵化时间",
|
|
2253
|
+
hatch_seconds: "孵化秒数",
|
|
2254
|
+
egg_type: "蛋类型",
|
|
2255
|
+
egg_size: "蛋尺寸",
|
|
2256
|
+
body_size: "体型",
|
|
2257
|
+
type: "类型",
|
|
2258
|
+
skill_type: "技能类型",
|
|
2259
|
+
damage_type: "伤害类型",
|
|
2260
|
+
element_type: "属性",
|
|
2261
|
+
label_type: "标签",
|
|
2262
|
+
families: "技能族",
|
|
2263
|
+
pet_count: "可用精灵",
|
|
2264
|
+
learnable_pet_count: "可学习精灵",
|
|
2265
|
+
food_close_exp: "亲密经验",
|
|
2266
|
+
close_exp: "亲密经验",
|
|
2267
|
+
home_exp_num: "家园经验",
|
|
2268
|
+
furniture_coin_num: "家具币",
|
|
2269
|
+
need_time_label: "制作时间",
|
|
2270
|
+
interaction_count: "交互次数",
|
|
2271
|
+
comfort: "舒适度",
|
|
2272
|
+
footprint: "占地",
|
|
2273
|
+
gender: "性别",
|
|
2274
|
+
grade: "品级",
|
|
2275
|
+
bond: "羁绊",
|
|
2276
|
+
category: "分类",
|
|
2277
|
+
area: "区域",
|
|
2278
|
+
source: "来源",
|
|
2279
|
+
source_label: "来源",
|
|
2280
|
+
summary: "摘要",
|
|
2281
|
+
description: "说明",
|
|
2282
|
+
flavor_text: "背景说明",
|
|
2283
|
+
enabled: "启用",
|
|
2284
|
+
banned: "禁用",
|
|
2285
|
+
shareable: "可分享",
|
|
2286
|
+
common: "通用",
|
|
2287
|
+
nest_num: "巢穴数",
|
|
2288
|
+
egg_laying_nest_num: "产蛋巢数",
|
|
2289
|
+
pet_lay_egg_rate_percent: "产蛋概率",
|
|
2290
|
+
close_level: "亲密等级",
|
|
2291
|
+
action_label: "行为"
|
|
2292
|
+
};
|
|
2293
|
+
function wikiLabelForKey(key) {
|
|
2294
|
+
return WIKI_KEY_LABELS[String(key || "")] || String(key || "").replace(/_/g, " ");
|
|
2295
|
+
}
|
|
2296
|
+
__name(wikiLabelForKey, "wikiLabelForKey");
|
|
2297
|
+
var WIKI_SECTION_TITLES = {
|
|
2298
|
+
items: "条目",
|
|
2299
|
+
members: "家族成员",
|
|
2300
|
+
probabilities: "概率信息",
|
|
2301
|
+
variants: "蛋型变体",
|
|
2302
|
+
voice_percent: "语音概率",
|
|
2303
|
+
topics: "图鉴课题",
|
|
2304
|
+
rewards: "奖励",
|
|
2305
|
+
reward_items: "奖励",
|
|
2306
|
+
acquire_methods: "获取方式",
|
|
2307
|
+
acquisition: "获取方式",
|
|
2308
|
+
methods: "获取方式",
|
|
2309
|
+
sources: "来源",
|
|
2310
|
+
world_maps: "世界地图",
|
|
2311
|
+
habitats: "栖息地",
|
|
2312
|
+
effects: "效果",
|
|
2313
|
+
rules: "规则",
|
|
2314
|
+
parts: "套装部件",
|
|
2315
|
+
tags: "标签",
|
|
2316
|
+
goods: "商品",
|
|
2317
|
+
mall_items: "商城商品",
|
|
2318
|
+
get_items: "获得物品",
|
|
2319
|
+
cost_items: "消耗物品",
|
|
2320
|
+
comfort_levels: "舒适度收益",
|
|
2321
|
+
levels: "等级成长",
|
|
2322
|
+
pet_bond_counts: "精灵羁绊",
|
|
2323
|
+
pet_home_limits: "入驻上限",
|
|
2324
|
+
settled_bonuses: "入驻加成",
|
|
2325
|
+
enjoy_field_types: "喜欢场景",
|
|
2326
|
+
hate_field_types: "讨厌场景",
|
|
2327
|
+
npc_reactions: "NPC 反应",
|
|
2328
|
+
movements: "骑乘动作",
|
|
2329
|
+
blood: "血脉技能",
|
|
2330
|
+
level: "升级习得",
|
|
2331
|
+
machine: "技能石习得",
|
|
2332
|
+
field: "场景习得"
|
|
2333
|
+
};
|
|
2334
|
+
function wikiSectionTitle(key) {
|
|
2335
|
+
return WIKI_SECTION_TITLES[String(key || "")] || wikiLabelForKey(key);
|
|
2336
|
+
}
|
|
2337
|
+
__name(wikiSectionTitle, "wikiSectionTitle");
|
|
2338
|
+
|
|
2339
|
+
// src/wiki-service.ts
|
|
2340
|
+
var logger5 = new import_koishi5.Logger("rocom-wiki");
|
|
2341
|
+
var COPYRIGHT = "Koishi & WeGame 洛克王国插件";
|
|
2342
|
+
function normalizeQueryText(text) {
|
|
2343
|
+
return String(text ?? "").replace(/\s+/g, "").trim().toLowerCase();
|
|
2344
|
+
}
|
|
2345
|
+
__name(normalizeQueryText, "normalizeQueryText");
|
|
2346
|
+
function wikiNamedValue(value) {
|
|
2347
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2348
|
+
return String(value.name || value.label || value.value || "");
|
|
2349
|
+
}
|
|
2350
|
+
return String(value ?? "");
|
|
2351
|
+
}
|
|
2352
|
+
__name(wikiNamedValue, "wikiNamedValue");
|
|
2353
|
+
function wikiNames(values) {
|
|
2354
|
+
if (!values) return [];
|
|
2355
|
+
const list = Array.isArray(values) ? values : [values];
|
|
2356
|
+
const result = [];
|
|
2357
|
+
for (const value of list) {
|
|
2358
|
+
const text = wikiNamedValue(value).trim();
|
|
2359
|
+
if (text) result.push(text);
|
|
2360
|
+
}
|
|
2361
|
+
return result;
|
|
2362
|
+
}
|
|
2363
|
+
__name(wikiNames, "wikiNames");
|
|
2364
|
+
function wikiRangeLabel(data, keyMin, keyMax, unit) {
|
|
2365
|
+
if (!data || typeof data !== "object") return "暂无";
|
|
2366
|
+
const low = data[keyMin];
|
|
2367
|
+
const high = data[keyMax];
|
|
2368
|
+
const empty = /* @__PURE__ */ __name((v) => v === void 0 || v === null || v === "", "empty");
|
|
2369
|
+
if (empty(low) && empty(high)) return "暂无";
|
|
2370
|
+
if (empty(low)) return `${high}${unit}`;
|
|
2371
|
+
if (empty(high) || low === high) return `${low}${unit}`;
|
|
2372
|
+
return `${low}-${high}${unit}`;
|
|
2373
|
+
}
|
|
2374
|
+
__name(wikiRangeLabel, "wikiRangeLabel");
|
|
2375
|
+
function similarityRatio(a, b) {
|
|
2376
|
+
if (!a || !b) return 0;
|
|
2377
|
+
if (a === b) return 1;
|
|
2378
|
+
const lenA = a.length;
|
|
2379
|
+
const lenB = b.length;
|
|
2380
|
+
const dp = Array.from({ length: lenB + 1 }, (_, j) => j);
|
|
2381
|
+
for (let i = 1; i <= lenA; i++) {
|
|
2382
|
+
let prev = dp[0];
|
|
2383
|
+
dp[0] = i;
|
|
2384
|
+
for (let j = 1; j <= lenB; j++) {
|
|
2385
|
+
const temp = dp[j];
|
|
2386
|
+
dp[j] = a[i - 1] === b[j - 1] ? prev : Math.min(prev, dp[j - 1], dp[j]) + 1;
|
|
2387
|
+
prev = temp;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return 1 - dp[lenB] / Math.max(lenA, lenB);
|
|
2391
|
+
}
|
|
2392
|
+
__name(similarityRatio, "similarityRatio");
|
|
2393
|
+
function wikiSimilarityScore(query, item) {
|
|
2394
|
+
const normalizedQuery = normalizeQueryText(query);
|
|
2395
|
+
if (!normalizedQuery) return 0;
|
|
2396
|
+
const candidates = [
|
|
2397
|
+
wikiTitleForItem(item, ""),
|
|
2398
|
+
item?.name || "",
|
|
2399
|
+
item?.summary || "",
|
|
2400
|
+
item?.description || item?.desc || ""
|
|
2401
|
+
];
|
|
2402
|
+
let best = 0;
|
|
2403
|
+
for (const text of candidates) {
|
|
2404
|
+
const normalizedText = normalizeQueryText(text);
|
|
2405
|
+
if (!normalizedText) continue;
|
|
2406
|
+
if (normalizedQuery === normalizedText) best = Math.max(best, 1);
|
|
2407
|
+
else if (normalizedText.includes(normalizedQuery) || normalizedQuery.includes(normalizedText)) best = Math.max(best, 0.82);
|
|
2408
|
+
else best = Math.max(best, similarityRatio(normalizedQuery, normalizedText));
|
|
2409
|
+
}
|
|
2410
|
+
return best;
|
|
2411
|
+
}
|
|
2412
|
+
__name(wikiSimilarityScore, "wikiSimilarityScore");
|
|
2413
|
+
function wikiGenericValue(value, depth = 0) {
|
|
2414
|
+
if (value === void 0 || value === null || value === "") return "";
|
|
2415
|
+
if (typeof value === "boolean") return value ? "是" : "否";
|
|
2416
|
+
if (typeof value === "number") return String(value);
|
|
2417
|
+
if (Array.isArray(value)) {
|
|
2418
|
+
const texts = value.slice(0, 6).map((item) => wikiGenericValue(item, depth + 1)).filter(Boolean);
|
|
2419
|
+
const suffix = value.length > 6 ? " ..." : "";
|
|
2420
|
+
return texts.join("、") + suffix;
|
|
2421
|
+
}
|
|
2422
|
+
if (typeof value === "object") {
|
|
2423
|
+
for (const key of ["name", "label", "title", "summary", "description", "text"]) {
|
|
2424
|
+
const text = String(value[key] || "").trim();
|
|
2425
|
+
if (text) return text;
|
|
2426
|
+
}
|
|
2427
|
+
if ("min_m" in value || "max_m" in value) return wikiRangeLabel(value, "min_m", "max_m", "m");
|
|
2428
|
+
if ("min_kg" in value || "max_kg" in value) return wikiRangeLabel(value, "min_kg", "max_kg", "kg");
|
|
2429
|
+
if (depth >= 1) {
|
|
2430
|
+
const pairs = [];
|
|
2431
|
+
for (const [key, item] of Object.entries(value).slice(0, 4)) {
|
|
2432
|
+
const text = wikiGenericValue(item, depth + 1);
|
|
2433
|
+
if (text) pairs.push(`${wikiLabelForKey(key)}:${text}`);
|
|
2434
|
+
}
|
|
2435
|
+
return pairs.join(";");
|
|
2436
|
+
}
|
|
2437
|
+
return "";
|
|
2438
|
+
}
|
|
2439
|
+
return String(value);
|
|
2440
|
+
}
|
|
2441
|
+
__name(wikiGenericValue, "wikiGenericValue");
|
|
2442
|
+
function wikiSizeLabel(value) {
|
|
2443
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return wikiGenericValue(value);
|
|
2444
|
+
const parts = [];
|
|
2445
|
+
if (value.height && typeof value.height === "object") {
|
|
2446
|
+
const text = wikiRangeLabel(value.height, "min_m", "max_m", "m");
|
|
2447
|
+
if (text && text !== "暂无") parts.push(`高 ${text}`);
|
|
2448
|
+
}
|
|
2449
|
+
if (value.weight && typeof value.weight === "object") {
|
|
2450
|
+
const text = wikiRangeLabel(value.weight, "min_kg", "max_kg", "kg");
|
|
2451
|
+
if (text && text !== "暂无") parts.push(`重 ${text}`);
|
|
2452
|
+
}
|
|
2453
|
+
return parts.join(" / ");
|
|
2454
|
+
}
|
|
2455
|
+
__name(wikiSizeLabel, "wikiSizeLabel");
|
|
2456
|
+
function wikiFootprintLabel(value) {
|
|
2457
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return wikiGenericValue(value);
|
|
2458
|
+
const width = value.width ?? value.x ?? value.cols;
|
|
2459
|
+
const height = value.height ?? value.y ?? value.rows;
|
|
2460
|
+
if (width !== void 0 && width !== null && width !== "" && height !== void 0 && height !== null && height !== "") {
|
|
2461
|
+
return `${width} x ${height}`;
|
|
2462
|
+
}
|
|
2463
|
+
return wikiGenericValue(value, 1);
|
|
2464
|
+
}
|
|
2465
|
+
__name(wikiFootprintLabel, "wikiFootprintLabel");
|
|
2466
|
+
function wikiCountLabel(value, unit = "个") {
|
|
2467
|
+
if (Array.isArray(value)) return `${value.length}${unit}`;
|
|
2468
|
+
if (value === void 0 || value === null || value === "") return "";
|
|
2469
|
+
return typeof value === "number" ? `${value}${unit}` : String(value);
|
|
2470
|
+
}
|
|
2471
|
+
__name(wikiCountLabel, "wikiCountLabel");
|
|
2472
|
+
function wikiPickImage(item) {
|
|
2473
|
+
const imageKeys = [
|
|
2474
|
+
"big_icon",
|
|
2475
|
+
"image",
|
|
2476
|
+
"cover_image",
|
|
2477
|
+
"preview_image",
|
|
2478
|
+
"checkout_icon",
|
|
2479
|
+
"icon",
|
|
2480
|
+
"small_icon",
|
|
2481
|
+
"background_image",
|
|
2482
|
+
"display_image",
|
|
2483
|
+
"package_bg",
|
|
2484
|
+
"package_cover"
|
|
2485
|
+
];
|
|
2486
|
+
for (const key of imageKeys) {
|
|
2487
|
+
const value = item?.[key];
|
|
2488
|
+
if (typeof value === "string" && /^(https?:\/\/|\{\{)/.test(value)) return value;
|
|
2489
|
+
if (value && typeof value === "object") {
|
|
2490
|
+
const url = value.url || value.image || value.icon;
|
|
2491
|
+
if (typeof url === "string" && /^(https?:\/\/|\{\{)/.test(url)) return url;
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return "";
|
|
2495
|
+
}
|
|
2496
|
+
__name(wikiPickImage, "wikiPickImage");
|
|
2497
|
+
function wikiTitleForItem(item, fallback = "Wiki 条目") {
|
|
2498
|
+
for (const key of ["name", "title", "display_name", "summary", "description"]) {
|
|
2499
|
+
const value = String(item?.[key] || "").trim();
|
|
2500
|
+
if (value) return value.slice(0, 48);
|
|
2501
|
+
}
|
|
2502
|
+
for (const key of [
|
|
2503
|
+
"pet_id",
|
|
2504
|
+
"skill_id",
|
|
2505
|
+
"item_id",
|
|
2506
|
+
"egg_conf_id",
|
|
2507
|
+
"random_egg_id",
|
|
2508
|
+
"tree_id",
|
|
2509
|
+
"medal_id",
|
|
2510
|
+
"ball_id",
|
|
2511
|
+
"plant_id",
|
|
2512
|
+
"furniture_id",
|
|
2513
|
+
"suit_id",
|
|
2514
|
+
"region_id",
|
|
2515
|
+
"dungeon_id",
|
|
2516
|
+
"mail_id",
|
|
2517
|
+
"music_id",
|
|
2518
|
+
"task_id",
|
|
2519
|
+
"shop_id",
|
|
2520
|
+
"exchange_id",
|
|
2521
|
+
"id"
|
|
2522
|
+
]) {
|
|
2523
|
+
const value = item?.[key];
|
|
2524
|
+
if (value !== void 0 && value !== null && value !== "") return `${fallback} #${value}`;
|
|
2525
|
+
}
|
|
2526
|
+
return fallback;
|
|
2527
|
+
}
|
|
2528
|
+
__name(wikiTitleForItem, "wikiTitleForItem");
|
|
2529
|
+
function wikiSummaryForItem(item) {
|
|
2530
|
+
for (const key of ["summary", "description", "desc", "effect", "flavor_text", "text", "content"]) {
|
|
2531
|
+
const value = wikiGenericValue(item?.[key]);
|
|
2532
|
+
if (value) return value.slice(0, 180);
|
|
2533
|
+
}
|
|
2534
|
+
return "";
|
|
2535
|
+
}
|
|
2536
|
+
__name(wikiSummaryForItem, "wikiSummaryForItem");
|
|
2537
|
+
function wikiBadgesForItem(item) {
|
|
2538
|
+
const badges = [];
|
|
2539
|
+
for (const key of ["type", "skill_type", "damage_type", "element_type", "category", "quality", "rarity", "label_type", "egg_type", "grade", "gender"]) {
|
|
2540
|
+
const value = wikiGenericValue(item?.[key]);
|
|
2541
|
+
if (value) badges.push(value);
|
|
2542
|
+
}
|
|
2543
|
+
for (const badge of item?.badges || []) {
|
|
2544
|
+
const value = wikiGenericValue(badge);
|
|
2545
|
+
if (value) badges.push(value);
|
|
2546
|
+
}
|
|
2547
|
+
for (const key of ["type_names", "egg_group_names", "tags", "source_counts"]) {
|
|
2548
|
+
badges.push(...wikiNames(item?.[key]).slice(0, 4));
|
|
2549
|
+
}
|
|
2550
|
+
const output = [];
|
|
2551
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2552
|
+
for (const badge of badges) {
|
|
2553
|
+
const key = String(badge);
|
|
2554
|
+
if (key && !seen.has(key)) {
|
|
2555
|
+
output.push(key);
|
|
2556
|
+
seen.add(key);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
return output.slice(0, 8);
|
|
2560
|
+
}
|
|
2561
|
+
__name(wikiBadgesForItem, "wikiBadgesForItem");
|
|
2562
|
+
function wikiAddFact(rows, label, value) {
|
|
2563
|
+
const text = wikiGenericValue(value);
|
|
2564
|
+
if (text) rows.push({ label, value: text.slice(0, 96) });
|
|
2565
|
+
}
|
|
2566
|
+
__name(wikiAddFact, "wikiAddFact");
|
|
2567
|
+
function wikiMetaForItem(item, limit = 12, catalogKey = "") {
|
|
2568
|
+
const rows = [];
|
|
2569
|
+
if (catalogKey === "pets") {
|
|
2570
|
+
wikiAddFact(rows, "图鉴编号", item?.handbook_no);
|
|
2571
|
+
wikiAddFact(rows, "精灵ID", item?.pet_id);
|
|
2572
|
+
wikiAddFact(rows, "属性", item?.type_names || item?.types);
|
|
2573
|
+
wikiAddFact(rows, "蛋组", item?.egg_group_names || item?.egg_groups);
|
|
2574
|
+
} else if (catalogKey === "skills") {
|
|
2575
|
+
wikiAddFact(rows, "技能ID", item?.skill_id);
|
|
2576
|
+
wikiAddFact(rows, "属性", item?.element_type || item?.type);
|
|
2577
|
+
wikiAddFact(rows, "类型", item?.skill_type || item?.damage_type);
|
|
2578
|
+
wikiAddFact(rows, "威力 / 能量", `${item?.power ?? "-"} / ${item?.cost ?? "-"}`);
|
|
2579
|
+
wikiAddFact(rows, "可用精灵", item?.pet_count);
|
|
2580
|
+
} else if (["pet-eggs", "egg-items", "random-eggs", "pet-egg"].includes(catalogKey)) {
|
|
2581
|
+
wikiAddFact(rows, "蛋ID", item?.egg_conf_id || item?.random_egg_id || item?.item_id);
|
|
2582
|
+
wikiAddFact(rows, "蛋类型", item?.egg_type);
|
|
2583
|
+
wikiAddFact(rows, "孵化时间", item?.hatch_label);
|
|
2584
|
+
wikiAddFact(rows, "尺寸", wikiSizeLabel(item?.egg_size));
|
|
2585
|
+
wikiAddFact(rows, "关联精灵", item?.pet);
|
|
2586
|
+
} else if (["pet-foods", "pet-gifts"].includes(catalogKey)) {
|
|
2587
|
+
wikiAddFact(rows, "物品ID", item?.item_id);
|
|
2588
|
+
wikiAddFact(rows, "亲密经验", item?.food_close_exp || item?.close_exp);
|
|
2589
|
+
wikiAddFact(rows, "家园经验", item?.home_exp_num);
|
|
2590
|
+
wikiAddFact(rows, "制作时间", item?.need_time_label);
|
|
2591
|
+
wikiAddFact(rows, "交互次数", item?.interaction_count);
|
|
2592
|
+
} else if (catalogKey === "home-egg-lay-rates") {
|
|
2593
|
+
wikiAddFact(rows, "巢穴数", item?.nest_num);
|
|
2594
|
+
wikiAddFact(rows, "产蛋巢", item?.egg_laying_nest_num);
|
|
2595
|
+
const rate = item?.pet_lay_egg_rate_percent;
|
|
2596
|
+
wikiAddFact(rows, "产蛋概率", rate !== void 0 && rate !== null && rate !== "" ? `${rate}%` : "");
|
|
2597
|
+
} else if (catalogKey === "pet-levels") {
|
|
2598
|
+
wikiAddFact(rows, "等级", item?.level);
|
|
2599
|
+
wikiAddFact(rows, "累计经验", item?.total_exp);
|
|
2600
|
+
wikiAddFact(rows, "下级所需", item?.next_level_exp);
|
|
2601
|
+
} else if (catalogKey === "furniture") {
|
|
2602
|
+
wikiAddFact(rows, "家具ID", item?.furniture_id);
|
|
2603
|
+
wikiAddFact(rows, "分类", item?.category);
|
|
2604
|
+
wikiAddFact(rows, "舒适度", item?.comfort);
|
|
2605
|
+
wikiAddFact(rows, "占地", wikiFootprintLabel(item?.footprint));
|
|
2606
|
+
} else if (catalogKey === "fashion") {
|
|
2607
|
+
wikiAddFact(rows, "套装ID", item?.suit_id);
|
|
2608
|
+
wikiAddFact(rows, "性别", item?.gender);
|
|
2609
|
+
wikiAddFact(rows, "品级", item?.grade);
|
|
2610
|
+
wikiAddFact(rows, "部件", wikiCountLabel(item?.parts));
|
|
2611
|
+
} else if (catalogKey === "exchanges") {
|
|
2612
|
+
wikiAddFact(rows, "兑换ID", item?.exchange_id);
|
|
2613
|
+
wikiAddFact(rows, "获得", item?.get_items);
|
|
2614
|
+
wikiAddFact(rows, "消耗", item?.cost_items);
|
|
2615
|
+
} else if (catalogKey === "shops") {
|
|
2616
|
+
wikiAddFact(rows, "商店ID", item?.shop_id);
|
|
2617
|
+
wikiAddFact(rows, "页签", item?.tab_name);
|
|
2618
|
+
wikiAddFact(rows, "商品数", wikiCountLabel(item?.goods));
|
|
2619
|
+
} else if (["skill-stones", "skill-stone-recipes", "items", "pet-fruits", "recipes", "balls", "plants", "pet-carryons", "medals"].includes(catalogKey)) {
|
|
2620
|
+
wikiAddFact(rows, "物品ID", item?.item_id || item?.medal_id || item?.ball_id || item?.plant_id || item?.carryon_id);
|
|
2621
|
+
wikiAddFact(rows, "类型", item?.type || item?.label_type);
|
|
2622
|
+
wikiAddFact(rows, "品质", item?.quality);
|
|
2623
|
+
wikiAddFact(rows, "效果", item?.summary || item?.catch_effect || item?.carryon);
|
|
2624
|
+
} else {
|
|
2625
|
+
for (const key of [
|
|
2626
|
+
"pet_id",
|
|
2627
|
+
"skill_id",
|
|
2628
|
+
"item_id",
|
|
2629
|
+
"tree_id",
|
|
2630
|
+
"level",
|
|
2631
|
+
"close_level",
|
|
2632
|
+
"action_label",
|
|
2633
|
+
"region_id",
|
|
2634
|
+
"dungeon_id",
|
|
2635
|
+
"mail_id",
|
|
2636
|
+
"music_id",
|
|
2637
|
+
"emoji_id",
|
|
2638
|
+
"task_id",
|
|
2639
|
+
"summary_id",
|
|
2640
|
+
"asset_type",
|
|
2641
|
+
"asset_id",
|
|
2642
|
+
"action_type",
|
|
2643
|
+
"action_id",
|
|
2644
|
+
"quality",
|
|
2645
|
+
"type",
|
|
2646
|
+
"category"
|
|
2647
|
+
]) {
|
|
2648
|
+
if (item && key in item) wikiAddFact(rows, wikiLabelForKey(key), item[key]);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (rows.length) return rows.slice(0, limit);
|
|
2652
|
+
const skip = /* @__PURE__ */ new Set([
|
|
2653
|
+
"name",
|
|
2654
|
+
"title",
|
|
2655
|
+
"description",
|
|
2656
|
+
"desc",
|
|
2657
|
+
"summary",
|
|
2658
|
+
"icon",
|
|
2659
|
+
"small_icon",
|
|
2660
|
+
"big_icon",
|
|
2661
|
+
"image",
|
|
2662
|
+
"cover_image",
|
|
2663
|
+
"preview_image",
|
|
2664
|
+
"background_image",
|
|
2665
|
+
"items",
|
|
2666
|
+
"rewards",
|
|
2667
|
+
"reward_items",
|
|
2668
|
+
"tags",
|
|
2669
|
+
"badges",
|
|
2670
|
+
"catalog"
|
|
2671
|
+
]);
|
|
2672
|
+
for (const [key, value] of Object.entries(item || {})) {
|
|
2673
|
+
if (skip.has(key)) continue;
|
|
2674
|
+
const text = wikiGenericValue(value);
|
|
2675
|
+
if (!text) continue;
|
|
2676
|
+
rows.push({ label: wikiLabelForKey(key), value: text.slice(0, 80) });
|
|
2677
|
+
if (rows.length >= limit) break;
|
|
2678
|
+
}
|
|
2679
|
+
return rows;
|
|
2680
|
+
}
|
|
2681
|
+
__name(wikiMetaForItem, "wikiMetaForItem");
|
|
2682
|
+
function wikiCardForItem(item, fallback, catalog) {
|
|
2683
|
+
const catalogKey = String(catalog?.key || "");
|
|
2684
|
+
return {
|
|
2685
|
+
title: wikiTitleForItem(item, fallback),
|
|
2686
|
+
image: wikiPickImage(item),
|
|
2687
|
+
summary: wikiSummaryForItem(item),
|
|
2688
|
+
badges: wikiBadgesForItem(item),
|
|
2689
|
+
meta: wikiMetaForItem(item, 5, catalogKey)
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
__name(wikiCardForItem, "wikiCardForItem");
|
|
2693
|
+
function wikiRowsFromDict(value, limit = 16) {
|
|
2694
|
+
const rows = [];
|
|
2695
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2696
|
+
if (["icon", "small_icon", "big_icon", "image", "display_image"].includes(key)) continue;
|
|
2697
|
+
const text = wikiGenericValue(item);
|
|
2698
|
+
if (text) rows.push({ label: wikiLabelForKey(key), value: text.slice(0, 120) });
|
|
2699
|
+
if (rows.length >= limit) break;
|
|
2700
|
+
}
|
|
2701
|
+
return rows;
|
|
2702
|
+
}
|
|
2703
|
+
__name(wikiRowsFromDict, "wikiRowsFromDict");
|
|
2704
|
+
function wikiSectionsForPayload(payload, catalog) {
|
|
2705
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return [];
|
|
2706
|
+
const sections = [];
|
|
2707
|
+
const addList = /* @__PURE__ */ __name((title, value) => {
|
|
2708
|
+
if (!Array.isArray(value) || !value.length) return;
|
|
2709
|
+
const dictItems = value.filter((item) => item && typeof item === "object" && !Array.isArray(item));
|
|
2710
|
+
if (dictItems.length) {
|
|
2711
|
+
sections.push({
|
|
2712
|
+
title,
|
|
2713
|
+
cards: dictItems.slice(0, 12).map((item) => wikiCardForItem(item, title, catalog)),
|
|
2714
|
+
rows: [],
|
|
2715
|
+
total: dictItems.length
|
|
2716
|
+
});
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
const texts = value.slice(0, 16).map((item) => wikiGenericValue(item)).filter(Boolean);
|
|
2720
|
+
if (!texts.length) return;
|
|
2721
|
+
sections.push({
|
|
2722
|
+
title,
|
|
2723
|
+
cards: [],
|
|
2724
|
+
rows: texts.map((text, idx) => ({ label: String(idx + 1), value: text })),
|
|
2725
|
+
total: texts.length
|
|
2726
|
+
});
|
|
2727
|
+
}, "addList");
|
|
2728
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
2729
|
+
const title = wikiSectionTitle(key);
|
|
2730
|
+
if (value && typeof value === "object" && !Array.isArray(value) && ["probabilities", "npc_reactions", "catch_effect", "carryon", "planting", "limit", "bond", "mall_package"].includes(key)) {
|
|
2731
|
+
const rows = wikiRowsFromDict(value);
|
|
2732
|
+
if (rows.length) sections.push({ title, cards: [], rows, total: rows.length });
|
|
2733
|
+
continue;
|
|
2734
|
+
}
|
|
2735
|
+
addList(title, value);
|
|
2736
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2737
|
+
for (const [subkey, subvalue] of Object.entries(value)) {
|
|
2738
|
+
addList(`${title} / ${wikiSectionTitle(subkey)}`, subvalue);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
return sections.slice(0, 8);
|
|
2743
|
+
}
|
|
2744
|
+
__name(wikiSectionsForPayload, "wikiSectionsForPayload");
|
|
2745
|
+
function findExactWikiMatch(results, query) {
|
|
2746
|
+
const normalizedQuery = normalizeQueryText(query);
|
|
2747
|
+
if (!normalizedQuery) return null;
|
|
2748
|
+
for (const item of results) {
|
|
2749
|
+
const name2 = String(item?.name || "");
|
|
2750
|
+
const form = String(item?.form || "");
|
|
2751
|
+
const candidates = [
|
|
2752
|
+
normalizeQueryText(name2),
|
|
2753
|
+
normalizeQueryText(`${name2}${form}`),
|
|
2754
|
+
normalizeQueryText(`${name2} ${form}`),
|
|
2755
|
+
normalizeQueryText(`${form}${name2}`)
|
|
2756
|
+
];
|
|
2757
|
+
if (candidates.includes(normalizedQuery)) return item;
|
|
2758
|
+
}
|
|
2759
|
+
return null;
|
|
2760
|
+
}
|
|
2761
|
+
__name(findExactWikiMatch, "findExactWikiMatch");
|
|
2762
|
+
function wikiCandidateText(query, items, kind) {
|
|
2763
|
+
const lines = [`找到多个${kind}候选,请使用更精确名称:`];
|
|
2764
|
+
items.slice(0, 10).forEach((item, idx) => {
|
|
2765
|
+
const name2 = item?.name || "未知";
|
|
2766
|
+
const form = item?.form || "";
|
|
2767
|
+
const itemId = item?.pet_id || item?.skill_id || item?.id || "-";
|
|
2768
|
+
const suffix = form && form !== "普通" ? `(${form})` : "";
|
|
2769
|
+
lines.push(`${idx + 1}. ${name2}${suffix} #${itemId}`);
|
|
2770
|
+
});
|
|
2771
|
+
lines.push(`
|
|
2772
|
+
你查询的是:${query}`);
|
|
2773
|
+
return lines.join("\n");
|
|
2774
|
+
}
|
|
2775
|
+
__name(wikiCandidateText, "wikiCandidateText");
|
|
2776
|
+
function wikiBodySize(...sources) {
|
|
2777
|
+
let bodySize = {};
|
|
2778
|
+
for (const source of sources) {
|
|
2779
|
+
if (source && typeof source === "object" && source.body_size && typeof source.body_size === "object") {
|
|
2780
|
+
bodySize = source.body_size;
|
|
2781
|
+
break;
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
height: wikiRangeLabel(bodySize?.height, "min_m", "max_m", "m"),
|
|
2786
|
+
weight: wikiRangeLabel(bodySize?.weight, "min_kg", "max_kg", "kg")
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
2789
|
+
__name(wikiBodySize, "wikiBodySize");
|
|
2790
|
+
function wikiGenderLabel(value) {
|
|
2791
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2792
|
+
const parts = [];
|
|
2793
|
+
if (value.male_percent !== void 0 && value.male_percent !== null && value.male_percent !== "") parts.push(`雄 ${value.male_percent}%`);
|
|
2794
|
+
if (value.female_percent !== void 0 && value.female_percent !== null && value.female_percent !== "") parts.push(`雌 ${value.female_percent}%`);
|
|
2795
|
+
return parts.length ? parts.join(" / ") : "暂无";
|
|
2796
|
+
}
|
|
2797
|
+
return String(value || "暂无");
|
|
2798
|
+
}
|
|
2799
|
+
__name(wikiGenderLabel, "wikiGenderLabel");
|
|
2800
|
+
function wikiEcologyLabel(value, fallback = "") {
|
|
2801
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2802
|
+
const parts = [];
|
|
2803
|
+
for (const key of ["pet_text", "pet_style"]) {
|
|
2804
|
+
const text = String(value[key] || "").trim();
|
|
2805
|
+
if (text) parts.push(text);
|
|
2806
|
+
}
|
|
2807
|
+
const habitats = wikiNames(value.habitats);
|
|
2808
|
+
if (habitats.length) parts.push("栖息:" + habitats.join(" / "));
|
|
2809
|
+
return parts.join(";");
|
|
2810
|
+
}
|
|
2811
|
+
return String(value || fallback || "").trim();
|
|
2812
|
+
}
|
|
2813
|
+
__name(wikiEcologyLabel, "wikiEcologyLabel");
|
|
2814
|
+
function wikiSourceLabel(source, fallback = "") {
|
|
2815
|
+
const mapping = { level: "升级", machine: "技能石", blood: "血脉", field: "特殊来源" };
|
|
2816
|
+
const text = String(source || "").trim();
|
|
2817
|
+
return String(fallback || mapping[text] || text || "");
|
|
2818
|
+
}
|
|
2819
|
+
__name(wikiSourceLabel, "wikiSourceLabel");
|
|
2820
|
+
function wikiTypeEffectiveness(profile) {
|
|
2821
|
+
const data = profile?.type_effectiveness;
|
|
2822
|
+
if (!data || typeof data !== "object") return [];
|
|
2823
|
+
const rows = [];
|
|
2824
|
+
const definitions = [
|
|
2825
|
+
["攻击克制", ["attack", "strong"]],
|
|
2826
|
+
["攻击弱效", ["attack", "weak"]],
|
|
2827
|
+
["防御弱点", ["defense", "weak"]],
|
|
2828
|
+
["防御抵抗", ["defense", "resist"]]
|
|
2829
|
+
];
|
|
2830
|
+
for (const [label, [groupKey, itemKey]] of definitions) {
|
|
2831
|
+
const group = data[groupKey] && typeof data[groupKey] === "object" ? data[groupKey] : {};
|
|
2832
|
+
const names = wikiNames(group?.[itemKey]);
|
|
2833
|
+
if (names.length) rows.push({ label, value: names.join(" / ") });
|
|
2834
|
+
}
|
|
2835
|
+
return rows;
|
|
2836
|
+
}
|
|
2837
|
+
__name(wikiTypeEffectiveness, "wikiTypeEffectiveness");
|
|
2838
|
+
function buildPetStats(profile) {
|
|
2839
|
+
const attrs = profile?.attributes && typeof profile.attributes === "object" ? profile.attributes : {};
|
|
2840
|
+
const defs = [
|
|
2841
|
+
["精力", "hp", "#42b883"],
|
|
2842
|
+
["物攻", "physical_attack", "#e86452"],
|
|
2843
|
+
["魔攻", "magic_attack", "#5987f5"],
|
|
2844
|
+
["物防", "physical_defense", "#d69a32"],
|
|
2845
|
+
["魔防", "magic_defense", "#25a6a6"],
|
|
2846
|
+
["速度", "speed", "#8d62d9"]
|
|
2847
|
+
];
|
|
2848
|
+
return defs.map(([label, key, color]) => {
|
|
2849
|
+
const value = Number(attrs[key]) || 0;
|
|
2850
|
+
return { label, value, color, percent: Math.min(value / 160 * 100, 100) };
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
__name(buildPetStats, "buildPetStats");
|
|
2854
|
+
function buildPetSkillGroups(skillsData) {
|
|
2855
|
+
const groups = [];
|
|
2856
|
+
const sourceDefs = [
|
|
2857
|
+
["level", "升级习得"],
|
|
2858
|
+
["machine", "技能机"],
|
|
2859
|
+
["blood", "血脉技能"],
|
|
2860
|
+
["field", "场景习得"]
|
|
2861
|
+
];
|
|
2862
|
+
for (const [key, label] of sourceDefs) {
|
|
2863
|
+
const rawItems = Array.isArray(skillsData?.[key]) ? skillsData[key] : [];
|
|
2864
|
+
const items = rawItems.slice(0, 10).map((skill) => ({
|
|
2865
|
+
name: skill?.name || "未知技能",
|
|
2866
|
+
icon: skill?.icon || "",
|
|
2867
|
+
level: skill?.level || skill?.source_label || "",
|
|
2868
|
+
type: wikiNamedValue(skill?.element_type || skill?.type) || "未知",
|
|
2869
|
+
category: wikiNamedValue(skill?.skill_type) || wikiNamedValue(skill?.damage_type) || "未知",
|
|
2870
|
+
cost: skill?.cost ?? "?",
|
|
2871
|
+
power: skill?.power ?? "?",
|
|
2872
|
+
desc: skill?.desc || skill?.description || skill?.flavor_text || "暂无说明"
|
|
2873
|
+
}));
|
|
2874
|
+
if (items.length) groups.push({ label, items });
|
|
2875
|
+
}
|
|
2876
|
+
return groups;
|
|
2877
|
+
}
|
|
2878
|
+
__name(buildPetSkillGroups, "buildPetSkillGroups");
|
|
2879
|
+
function buildPetFamily(familyData, currentId) {
|
|
2880
|
+
let items = [];
|
|
2881
|
+
if (familyData && typeof familyData === "object" && !Array.isArray(familyData)) {
|
|
2882
|
+
if (Array.isArray(familyData.members)) items.push(...familyData.members);
|
|
2883
|
+
for (const formGroup of familyData.forms || []) {
|
|
2884
|
+
for (const member of formGroup?.members || []) {
|
|
2885
|
+
const merged = { ...member };
|
|
2886
|
+
if (formGroup?.name && !merged.form_group) merged.form_group = formGroup.name;
|
|
2887
|
+
items.push(merged);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
if (!items.length) {
|
|
2891
|
+
for (const key of ["items", "family", "evolutions"]) {
|
|
2892
|
+
if (Array.isArray(familyData[key])) {
|
|
2893
|
+
items.push(...familyData[key]);
|
|
2894
|
+
break;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
} else if (Array.isArray(familyData)) {
|
|
2899
|
+
items = familyData;
|
|
2900
|
+
}
|
|
2901
|
+
const result = [];
|
|
2902
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2903
|
+
for (const item of items.slice(0, 12)) {
|
|
2904
|
+
const petId = item?.pet_id ?? item?.id;
|
|
2905
|
+
const key = String(petId ?? item?.name ?? "");
|
|
2906
|
+
if (key && seen.has(key)) continue;
|
|
2907
|
+
seen.add(key);
|
|
2908
|
+
const conditionTexts = Array.isArray(item?.condition_texts) ? item.condition_texts : [];
|
|
2909
|
+
result.push({
|
|
2910
|
+
pet_id: petId ?? "-",
|
|
2911
|
+
name: item?.name || "未知精灵",
|
|
2912
|
+
form: item?.form || item?.form_group || "",
|
|
2913
|
+
icon: item?.icon || item?.small_icon || "",
|
|
2914
|
+
condition: item?.condition_summary || conditionTexts.filter(Boolean).join(" / ") || item?.evolution_description || item?.condition || item?.evolve_condition || item?.relation || "",
|
|
2915
|
+
is_current: String(petId ?? "") === String(currentId ?? "")
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
return result;
|
|
2919
|
+
}
|
|
2920
|
+
__name(buildPetFamily, "buildPetFamily");
|
|
2921
|
+
function buildHandbookTopics(handbookData) {
|
|
2922
|
+
const topics = Array.isArray(handbookData?.topics) ? handbookData.topics : [];
|
|
2923
|
+
const result = [];
|
|
2924
|
+
for (const item of topics.slice(0, 8)) {
|
|
2925
|
+
const rewards = [];
|
|
2926
|
+
let rewardItems = item?.rewards || item?.reward_items;
|
|
2927
|
+
if (!rewardItems && item?.reward && typeof item.reward === "object") rewardItems = item.reward.items;
|
|
2928
|
+
for (const reward of rewardItems || []) {
|
|
2929
|
+
rewards.push({
|
|
2930
|
+
name: reward?.name || "奖励",
|
|
2931
|
+
count: reward?.count || "",
|
|
2932
|
+
icon: reward?.icon || ""
|
|
2933
|
+
});
|
|
2934
|
+
}
|
|
2935
|
+
result.push({
|
|
2936
|
+
name: item?.name || item?.title || `课题 ${item?.topic_id || ""}`.trim(),
|
|
2937
|
+
desc: item?.description || item?.desc || "",
|
|
2938
|
+
rewards: rewards.slice(0, 4)
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
return result;
|
|
2942
|
+
}
|
|
2943
|
+
__name(buildHandbookTopics, "buildHandbookTopics");
|
|
2944
|
+
function buildWikiPetRenderData(overview, profile, skills, family, handbook, query) {
|
|
2945
|
+
profile = profile || {};
|
|
2946
|
+
skills = skills || {};
|
|
2947
|
+
family = family || {};
|
|
2948
|
+
handbook = handbook || {};
|
|
2949
|
+
const petId = overview?.pet_id ?? profile?.pet_id;
|
|
2950
|
+
const typeNames = wikiNames(overview?.type_names || overview?.types);
|
|
2951
|
+
const eggGroups = wikiNames(overview?.egg_group_names || overview?.egg_groups);
|
|
2952
|
+
const feature = overview?.feature || {};
|
|
2953
|
+
const bodySize = wikiBodySize(profile, overview);
|
|
2954
|
+
const stats = buildPetStats(profile);
|
|
2955
|
+
const totalStats = profile?.attributes?.sum || stats.reduce((sum, item) => sum + item.value, 0);
|
|
2956
|
+
const form = overview?.form || profile?.form || "";
|
|
2957
|
+
const classis = wikiNamedValue(overview?.classis) || "暂无";
|
|
2958
|
+
const habitats = wikiNames(profile?.habitats);
|
|
2959
|
+
const areas = wikiNames(handbook?.areas);
|
|
2960
|
+
return {
|
|
2961
|
+
name: overview?.name || profile?.name || query,
|
|
2962
|
+
query,
|
|
2963
|
+
pet_id: petId ?? "-",
|
|
2964
|
+
number: overview?.handbook_no || profile?.handbook_no || "---",
|
|
2965
|
+
form,
|
|
2966
|
+
formLine: `${form || "普通"}${classis && classis !== "暂无" ? ` · ${classis}` : ""}`,
|
|
2967
|
+
quality: overview?.quality || profile?.quality || "",
|
|
2968
|
+
stage: overview?.stage || profile?.stage || "",
|
|
2969
|
+
pet_icon: overview?.icon || overview?.small_icon || "",
|
|
2970
|
+
main_image: profile?.small_icon || overview?.small_icon || profile?.icon || overview?.icon || "",
|
|
2971
|
+
type_names: typeNames,
|
|
2972
|
+
egg_groups: eggGroups,
|
|
2973
|
+
description: profile?.description || overview?.description || "暂无图鉴描述",
|
|
2974
|
+
classis,
|
|
2975
|
+
feature_name: feature?.name || "暂无",
|
|
2976
|
+
feature_desc: feature?.desc || "暂无特性说明",
|
|
2977
|
+
ride_talent: overview?.has_ride_talent ? "支持" : "不支持",
|
|
2978
|
+
height_label: bodySize.height,
|
|
2979
|
+
weight_label: bodySize.weight,
|
|
2980
|
+
gender_ratio: wikiGenderLabel(profile?.gender_ratio),
|
|
2981
|
+
move_type: profile?.move_type || "暂无",
|
|
2982
|
+
habitats,
|
|
2983
|
+
habitatsLabel: habitats.length ? habitats.join(" / ") : "暂无",
|
|
2984
|
+
ecology: wikiEcologyLabel(profile?.ecology, profile?.pet_style),
|
|
2985
|
+
type_effectiveness: wikiTypeEffectiveness(profile),
|
|
2986
|
+
total_stats: totalStats,
|
|
2987
|
+
pet_stats: stats,
|
|
2988
|
+
skill_groups: buildPetSkillGroups(skills),
|
|
2989
|
+
family_members: buildPetFamily(family, petId),
|
|
2990
|
+
handbook_topics: buildHandbookTopics(handbook),
|
|
2991
|
+
areas,
|
|
2992
|
+
areasLabel: areas.join(" / "),
|
|
2993
|
+
commandHint: "💡 洛克wiki <类型> <关键词或ID> | 示例:洛克wiki 技能 圣光斩",
|
|
2994
|
+
copyright: COPYRIGHT
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
__name(buildWikiPetRenderData, "buildWikiPetRenderData");
|
|
2998
|
+
function buildWikiSkillRenderData(item, petsData, query) {
|
|
2999
|
+
const pets = ((petsData || {}).items || []).slice(0, 30).map((pet) => {
|
|
3000
|
+
const source = wikiSourceLabel(pet?.source, pet?.source_label);
|
|
3001
|
+
const level = pet?.level || "";
|
|
3002
|
+
return {
|
|
3003
|
+
name: pet?.name || "未知精灵",
|
|
3004
|
+
pet_id: pet?.pet_id ?? "-",
|
|
3005
|
+
icon: pet?.icon || pet?.small_icon || "",
|
|
3006
|
+
source,
|
|
3007
|
+
level,
|
|
3008
|
+
sourceLine: source || level ? `${source}${level ? ` · Lv.${level}` : ""}` : ""
|
|
3009
|
+
};
|
|
3010
|
+
});
|
|
3011
|
+
const tags = (item?.tags || []).map((tag) => ({ name: tag?.name || "", icon: tag?.icon || "" }));
|
|
3012
|
+
return {
|
|
3013
|
+
name: item?.name || query,
|
|
3014
|
+
skill_id: item?.skill_id ?? "-",
|
|
3015
|
+
query,
|
|
3016
|
+
icon: item?.icon || "",
|
|
3017
|
+
type: wikiNamedValue(item?.type) || "未知",
|
|
3018
|
+
skill_type: wikiNamedValue(item?.skill_type) || "未知",
|
|
3019
|
+
damage_type: wikiNamedValue(item?.damage_type) || "未知",
|
|
3020
|
+
element_type: wikiNamedValue(item?.element_type) || "未知",
|
|
3021
|
+
cost: item?.cost ?? "?",
|
|
3022
|
+
power: item?.power ?? "?",
|
|
3023
|
+
families: item?.families || "暂无",
|
|
3024
|
+
description: item?.description || "暂无说明",
|
|
3025
|
+
flavor_text: item?.flavor_text || "",
|
|
3026
|
+
tags,
|
|
3027
|
+
pets,
|
|
3028
|
+
pet_total: (petsData || {}).total || pets.length,
|
|
3029
|
+
commandHint: "💡 洛克wiki 技能 <技能名或ID>",
|
|
3030
|
+
copyright: COPYRIGHT
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
__name(buildWikiSkillRenderData, "buildWikiSkillRenderData");
|
|
3034
|
+
function splitWikiCommandParts(text) {
|
|
3035
|
+
const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
|
|
3036
|
+
let pageNo = 1;
|
|
3037
|
+
if (parts.length) {
|
|
3038
|
+
const tail = parts[parts.length - 1];
|
|
3039
|
+
const pageMatch = /^(?:(?:p|P|页|第)(\d+)(?:页)?|(\d+)页)$/.exec(tail);
|
|
3040
|
+
if (pageMatch && parts.length > 1) {
|
|
3041
|
+
const pageValue = pageMatch[1] || pageMatch[2];
|
|
3042
|
+
pageNo = Math.max(parseInt(pageValue, 10), 1);
|
|
3043
|
+
parts.pop();
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
return { parts, pageNo };
|
|
3047
|
+
}
|
|
3048
|
+
__name(splitWikiCommandParts, "splitWikiCommandParts");
|
|
3049
|
+
function wikiCatalogByToken(token) {
|
|
3050
|
+
const text = String(token || "").trim().toLowerCase();
|
|
3051
|
+
return WIKI_CATALOG_ROUTES_BY_ALIAS.get(text) || null;
|
|
3052
|
+
}
|
|
3053
|
+
__name(wikiCatalogByToken, "wikiCatalogByToken");
|
|
3054
|
+
var WikiService = class {
|
|
3055
|
+
constructor(ctx, client) {
|
|
3056
|
+
this.ctx = ctx;
|
|
3057
|
+
this.client = client;
|
|
3058
|
+
}
|
|
3059
|
+
static {
|
|
3060
|
+
__name(this, "WikiService");
|
|
3061
|
+
}
|
|
3062
|
+
catalogsCache = null;
|
|
3063
|
+
catalogsCacheTs = 0;
|
|
3064
|
+
optionsCache = null;
|
|
3065
|
+
optionsCacheTs = 0;
|
|
3066
|
+
skillDetailCache = /* @__PURE__ */ new Map();
|
|
3067
|
+
petDetailCache = /* @__PURE__ */ new Map();
|
|
3068
|
+
async getCatalogsPayload(force = false) {
|
|
3069
|
+
const now = Date.now();
|
|
3070
|
+
if (!force && this.catalogsCache !== null && now - this.catalogsCacheTs < 3e5) {
|
|
3071
|
+
return this.catalogsCache;
|
|
3072
|
+
}
|
|
3073
|
+
const payload = await this.client.getWikiCatalogs(this.ctx);
|
|
3074
|
+
if (payload && typeof payload === "object") {
|
|
3075
|
+
this.catalogsCache = payload;
|
|
3076
|
+
this.catalogsCacheTs = now;
|
|
3077
|
+
return payload;
|
|
3078
|
+
}
|
|
3079
|
+
return this.catalogsCache || {};
|
|
3080
|
+
}
|
|
3081
|
+
async getOptionsPayload(force = false) {
|
|
3082
|
+
const now = Date.now();
|
|
3083
|
+
if (!force && this.optionsCache !== null && now - this.optionsCacheTs < 3e5) {
|
|
3084
|
+
return this.optionsCache;
|
|
3085
|
+
}
|
|
3086
|
+
const payload = await this.client.getWikiOptions(this.ctx);
|
|
3087
|
+
if (payload && typeof payload === "object") {
|
|
3088
|
+
this.optionsCache = payload;
|
|
3089
|
+
this.optionsCacheTs = now;
|
|
3090
|
+
return payload;
|
|
3091
|
+
}
|
|
3092
|
+
return this.optionsCache || {};
|
|
3093
|
+
}
|
|
3094
|
+
backendCatalogItems(payload) {
|
|
3095
|
+
if (!payload || typeof payload !== "object") return [];
|
|
3096
|
+
const items = payload.items;
|
|
3097
|
+
if (!Array.isArray(items)) return [];
|
|
3098
|
+
return items.filter((item) => item && typeof item === "object");
|
|
3099
|
+
}
|
|
3100
|
+
catalogFromBackendItem(item) {
|
|
3101
|
+
const key = String(item?.key || "").trim();
|
|
3102
|
+
const base = WIKI_CATALOG_ROUTES_BY_KEY.get(key);
|
|
3103
|
+
const catalog = base ? { ...base, aliases: [...base.aliases] } : { key, title: String(item?.name || key || "Wiki"), aliases: [], list_path: "", detail_path: "", id_fields: [], search: null };
|
|
3104
|
+
const backendName = String(item?.name || "").trim();
|
|
3105
|
+
if (backendName && !base) catalog.title = backendName;
|
|
3106
|
+
for (const alias of [key, backendName, backendName.replace(/图鉴/g, "").trim()]) {
|
|
3107
|
+
if (alias && !catalog.aliases.includes(alias)) catalog.aliases.push(alias);
|
|
3108
|
+
}
|
|
3109
|
+
const filters = Array.isArray(item?.filters) ? item.filters : [];
|
|
3110
|
+
const hasQFilter = filters.some((f) => f && typeof f === "object" && f.key === "q");
|
|
3111
|
+
const routeSearch = catalog.search;
|
|
3112
|
+
catalog.search = routeSearch === null || routeSearch === void 0 ? hasQFilter : Boolean(routeSearch);
|
|
3113
|
+
if (item?.path) catalog.list_path = String(item.path);
|
|
3114
|
+
catalog._backend = item;
|
|
3115
|
+
return catalog;
|
|
3116
|
+
}
|
|
3117
|
+
catalogForKeyFromPayload(key, catalogsPayload) {
|
|
3118
|
+
const item = this.backendCatalogItems(catalogsPayload).find((it) => String(it?.key || "") === String(key || ""));
|
|
3119
|
+
if (item) return this.catalogFromBackendItem(item);
|
|
3120
|
+
const route2 = WIKI_CATALOG_ROUTES_BY_KEY.get(String(key || ""));
|
|
3121
|
+
return route2 ? { ...route2, aliases: [...route2.aliases] } : null;
|
|
3122
|
+
}
|
|
3123
|
+
catalogsFromPayload(catalogsPayload) {
|
|
3124
|
+
return this.backendCatalogItems(catalogsPayload).map((item) => this.catalogFromBackendItem(item));
|
|
3125
|
+
}
|
|
3126
|
+
async getCatalogByKey(key) {
|
|
3127
|
+
return this.catalogForKeyFromPayload(key, await this.getCatalogsPayload());
|
|
3128
|
+
}
|
|
3129
|
+
dynamicCatalogByToken(token, catalogsPayload) {
|
|
3130
|
+
const text = normalizeQueryText(token);
|
|
3131
|
+
if (!text) return null;
|
|
3132
|
+
for (const item of this.backendCatalogItems(catalogsPayload)) {
|
|
3133
|
+
const candidates = [
|
|
3134
|
+
item?.key,
|
|
3135
|
+
item?.name,
|
|
3136
|
+
String(item?.name || "").replace(/图鉴/g, "").trim()
|
|
3137
|
+
];
|
|
3138
|
+
if (candidates.some((candidate) => candidate && normalizeQueryText(candidate) === text)) {
|
|
3139
|
+
return this.catalogFromBackendItem(item);
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
return null;
|
|
3143
|
+
}
|
|
3144
|
+
parseWikiCommand(text) {
|
|
3145
|
+
const trimmed = String(text || "").trim();
|
|
3146
|
+
if (!trimmed) return { catalog: null, query: "", pageNo: 1 };
|
|
3147
|
+
const { parts, pageNo } = splitWikiCommandParts(trimmed);
|
|
3148
|
+
const catalog = parts.length ? wikiCatalogByToken(parts[0]) : null;
|
|
3149
|
+
if (catalog) return { catalog: { ...catalog, aliases: [...catalog.aliases] }, query: parts.slice(1).join(" ").trim(), pageNo };
|
|
3150
|
+
return { catalog: null, query: trimmed, pageNo };
|
|
3151
|
+
}
|
|
3152
|
+
catalogUsageText() {
|
|
3153
|
+
const catalogs = this.catalogsFromPayload(this.catalogsCache || {});
|
|
3154
|
+
const names = catalogs.slice(0, 24).map((item) => item.title || item.key).join("、");
|
|
3155
|
+
return [
|
|
3156
|
+
"Wiki 用法:",
|
|
3157
|
+
" 洛克wiki <类型> [关键词或ID]",
|
|
3158
|
+
" 洛克wiki <关键词或ID>",
|
|
3159
|
+
"示例:洛克wiki 水灵、洛克wiki 技能 圣光斩、洛克wiki 物品 xx球、洛克wiki 种植 食谱名",
|
|
3160
|
+
`后端目录:${names || "请稍后重试"}`
|
|
3161
|
+
].join("\n");
|
|
3162
|
+
}
|
|
3163
|
+
buildCatalogRenderData(catalogsPayload, optionsPayload) {
|
|
3164
|
+
const catalogs = this.catalogsFromPayload(catalogsPayload);
|
|
3165
|
+
const topicCard = /* @__PURE__ */ __name((catalog) => {
|
|
3166
|
+
const backend = catalog._backend && typeof catalog._backend === "object" ? catalog._backend : {};
|
|
3167
|
+
const filters = Array.isArray(backend.filters) ? backend.filters : [];
|
|
3168
|
+
const filterText = filters.slice(0, 6).filter((item) => item && typeof item === "object").map((item) => String(item.label || item.key || "")).join("、");
|
|
3169
|
+
const title = String(catalog.title || backend.name || catalog.key || "Wiki");
|
|
3170
|
+
const key = String(catalog.key || "");
|
|
3171
|
+
const count = backend.count;
|
|
3172
|
+
let hint = `洛克wiki ${title} <关键词或ID>`;
|
|
3173
|
+
if (key) hint = `${hint} / 洛克wiki ${key} <关键词或ID>`;
|
|
3174
|
+
let coverage = String(backend.description || "").trim();
|
|
3175
|
+
if (filterText) coverage = `${coverage} 筛选:${filterText}`.trim();
|
|
3176
|
+
return {
|
|
3177
|
+
title,
|
|
3178
|
+
key,
|
|
3179
|
+
summary: count !== void 0 && count !== null && count !== "" ? `${hint} · 条目 ${count}` : hint,
|
|
3180
|
+
desc: key,
|
|
3181
|
+
children: coverage
|
|
3182
|
+
};
|
|
3183
|
+
}, "topicCard");
|
|
3184
|
+
const groups = [{
|
|
3185
|
+
title: "后端 Wiki 图鉴入口",
|
|
3186
|
+
desc: "",
|
|
3187
|
+
items: catalogs.map(topicCard),
|
|
3188
|
+
total: catalogs.length
|
|
3189
|
+
}];
|
|
3190
|
+
return {
|
|
3191
|
+
title: "洛克 Wiki",
|
|
3192
|
+
subtitle: "统一资料查询入口",
|
|
3193
|
+
summary: "",
|
|
3194
|
+
badges: [],
|
|
3195
|
+
facts: [],
|
|
3196
|
+
primary: [],
|
|
3197
|
+
groups,
|
|
3198
|
+
commandHint: "💡 示例:洛克wiki 水灵 | 洛克wiki 技能 圣光斩 | 洛克wiki 物品 国王球",
|
|
3199
|
+
copyright: COPYRIGHT
|
|
3200
|
+
};
|
|
3201
|
+
}
|
|
3202
|
+
globalSearchCatalogs(catalogsPayload) {
|
|
3203
|
+
const catalogs = [];
|
|
3204
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3205
|
+
for (const catalog of this.catalogsFromPayload(catalogsPayload)) {
|
|
3206
|
+
const key = String(catalog.key || "");
|
|
3207
|
+
if (seen.has(key) || !catalog.list_path || catalog.search === false) continue;
|
|
3208
|
+
catalogs.push(catalog);
|
|
3209
|
+
seen.add(key);
|
|
3210
|
+
}
|
|
3211
|
+
return catalogs;
|
|
3212
|
+
}
|
|
3213
|
+
resultCommandExamples(items, catalog, limit = 3) {
|
|
3214
|
+
const examples = [];
|
|
3215
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3216
|
+
for (const item of items) {
|
|
3217
|
+
if (!item || typeof item !== "object") continue;
|
|
3218
|
+
const child = this.catalogForKeyFromPayload(String(item._catalog_key || ""), this.catalogsCache || {}) || catalog || null;
|
|
3219
|
+
const category = String(item._catalog_title || child?.title || "").trim();
|
|
3220
|
+
const title = wikiTitleForItem(item, "").trim();
|
|
3221
|
+
if (!category || !title) continue;
|
|
3222
|
+
const command = `洛克wiki ${category} ${title}`;
|
|
3223
|
+
if (seen.has(command)) continue;
|
|
3224
|
+
examples.push(command);
|
|
3225
|
+
seen.add(command);
|
|
3226
|
+
if (examples.length >= limit) break;
|
|
3227
|
+
}
|
|
3228
|
+
return examples;
|
|
3229
|
+
}
|
|
3230
|
+
wikiPathParamsFromItem(catalog, item, rawText = "") {
|
|
3231
|
+
const tokens = String(rawText || "").split(/\s+/).filter(Boolean);
|
|
3232
|
+
const params = {};
|
|
3233
|
+
const idFields = catalog.id_fields || [];
|
|
3234
|
+
idFields.forEach((field, idx) => {
|
|
3235
|
+
let value = item?.[field];
|
|
3236
|
+
const empty = /* @__PURE__ */ __name((v) => v === void 0 || v === null || v === "", "empty");
|
|
3237
|
+
if (empty(value) && ["item_id", "asset_id", "action_id"].includes(field)) value = item?.id;
|
|
3238
|
+
if (empty(value) && ["ball_id", "plant_id", "carryon_id"].includes(field)) value = item?.item_id ?? item?.id;
|
|
3239
|
+
if (empty(value) && idx < tokens.length) value = tokens[idx];
|
|
3240
|
+
if (!empty(value)) params[field] = String(value);
|
|
3241
|
+
});
|
|
3242
|
+
return params;
|
|
3243
|
+
}
|
|
3244
|
+
fillDetailPath(catalog, params) {
|
|
3245
|
+
let path7 = String(catalog.detail_path || "");
|
|
3246
|
+
for (const field of catalog.id_fields || []) {
|
|
3247
|
+
const value = params[field];
|
|
3248
|
+
if (value === void 0 || value === null || value === "") return "";
|
|
3249
|
+
path7 = path7.replace(`{${field}}`, String(value));
|
|
3250
|
+
}
|
|
3251
|
+
return path7;
|
|
3252
|
+
}
|
|
3253
|
+
itemMatchesQueryExact(catalog, item, query) {
|
|
3254
|
+
const trimmed = String(query || "").trim();
|
|
3255
|
+
if (!trimmed) return false;
|
|
3256
|
+
const normalizedQuery = normalizeQueryText(trimmed);
|
|
3257
|
+
for (const field of catalog.id_fields || []) {
|
|
3258
|
+
let value = item?.[field];
|
|
3259
|
+
if ((value === void 0 || value === null || value === "") && ["ball_id", "plant_id", "carryon_id"].includes(field)) {
|
|
3260
|
+
value = item?.item_id ?? item?.id;
|
|
3261
|
+
}
|
|
3262
|
+
if (String(value ?? "") === trimmed) return true;
|
|
3263
|
+
}
|
|
3264
|
+
return normalizeQueryText(wikiTitleForItem(item, "")) === normalizedQuery;
|
|
3265
|
+
}
|
|
3266
|
+
findCatalogMatch(catalog, items, query, allowSingle = true) {
|
|
3267
|
+
const trimmed = String(query || "").trim();
|
|
3268
|
+
if (!trimmed) return null;
|
|
3269
|
+
for (const item of items) {
|
|
3270
|
+
if (this.itemMatchesQueryExact(catalog, item, trimmed)) return item;
|
|
3271
|
+
}
|
|
3272
|
+
if (allowSingle && items.length === 1) return items[0];
|
|
3273
|
+
return null;
|
|
3274
|
+
}
|
|
3275
|
+
async suggestCatalogItems(catalog, query, limit = 10) {
|
|
3276
|
+
if (!catalog || !catalog.list_path) return [];
|
|
3277
|
+
const trimmed = String(query || "").trim();
|
|
3278
|
+
if (!trimmed) return [];
|
|
3279
|
+
const terms = [];
|
|
3280
|
+
for (const term of [trimmed, trimmed.slice(0, 2), trimmed.slice(0, 1), trimmed.slice(0, -1)]) {
|
|
3281
|
+
const cleaned = String(term || "").trim();
|
|
3282
|
+
if (cleaned && !terms.includes(cleaned)) terms.push(cleaned);
|
|
3283
|
+
}
|
|
3284
|
+
const collected = /* @__PURE__ */ new Map();
|
|
3285
|
+
for (const term of terms.slice(0, 4)) {
|
|
3286
|
+
const res = await this.client.listWikiCatalogItems(
|
|
3287
|
+
this.ctx,
|
|
3288
|
+
catalog.list_path,
|
|
3289
|
+
term,
|
|
3290
|
+
1,
|
|
3291
|
+
30,
|
|
3292
|
+
Boolean(term && catalog.search !== false)
|
|
3293
|
+
);
|
|
3294
|
+
const items = res && typeof res === "object" ? res.items : [];
|
|
3295
|
+
for (const item of items || []) {
|
|
3296
|
+
if (!item || typeof item !== "object") continue;
|
|
3297
|
+
const title = wikiTitleForItem(item, "");
|
|
3298
|
+
const keyParts = [catalog.key || "", title];
|
|
3299
|
+
for (const field of catalog.id_fields || []) {
|
|
3300
|
+
if (item[field] !== void 0 && item[field] !== null && item[field] !== "") keyParts.push(String(item[field]));
|
|
3301
|
+
}
|
|
3302
|
+
const dedupeKey = keyParts.join("|");
|
|
3303
|
+
if (!collected.has(dedupeKey)) collected.set(dedupeKey, item);
|
|
3304
|
+
}
|
|
3305
|
+
if (collected.size >= limit * 3) break;
|
|
3306
|
+
}
|
|
3307
|
+
const scored = [];
|
|
3308
|
+
for (const item of collected.values()) {
|
|
3309
|
+
const score = wikiSimilarityScore(trimmed, item);
|
|
3310
|
+
if (score >= 0.18) scored.push([score, item]);
|
|
3311
|
+
}
|
|
3312
|
+
scored.sort((a, b) => b[0] - a[0]);
|
|
3313
|
+
if (!scored.length) return [...collected.values()].slice(0, limit);
|
|
3314
|
+
return scored.slice(0, limit).map(([, item]) => item);
|
|
3315
|
+
}
|
|
3316
|
+
buildGenericRenderData(catalog, payload, query, mode, pageNo = 1) {
|
|
3317
|
+
payload = payload || {};
|
|
3318
|
+
const fullCatalog = catalog;
|
|
3319
|
+
if (mode === "global-search") {
|
|
3320
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
3321
|
+
const cards = [];
|
|
3322
|
+
for (const item2 of items) {
|
|
3323
|
+
if (!item2 || typeof item2 !== "object") continue;
|
|
3324
|
+
const child = this.catalogForKeyFromPayload(String(item2._catalog_key || ""), this.catalogsCache || {}) || fullCatalog;
|
|
3325
|
+
const card = wikiCardForItem(item2, child?.title || "Wiki", child);
|
|
3326
|
+
if (item2._catalog_title) card.badges = [item2._catalog_title, ...card.badges];
|
|
3327
|
+
cards.push(card);
|
|
3328
|
+
}
|
|
3329
|
+
return {
|
|
3330
|
+
title: `全局搜索:${query}`,
|
|
3331
|
+
subtitle: "Wiki / 全局搜索",
|
|
3332
|
+
image: "",
|
|
3333
|
+
summary: "未指定分类时,会在所有可搜索的大分类接口中使用后端 q 参数查询。",
|
|
3334
|
+
badges: ["全局搜索", "可继续指定分类"],
|
|
3335
|
+
facts: [
|
|
3336
|
+
{ label: "查询词", value: query || "-" },
|
|
3337
|
+
{ label: "结果数", value: String(payload?.total ?? cards.length) },
|
|
3338
|
+
{ label: "搜索接口", value: String(this.globalSearchCatalogs(this.catalogsCache || {}).length) }
|
|
3339
|
+
],
|
|
3340
|
+
actionHint: "要查看具体条目,请按卡片绿色分类 tag 加名称继续查询。",
|
|
3341
|
+
actionExamples: this.resultCommandExamples(items, fullCatalog),
|
|
3342
|
+
cards,
|
|
3343
|
+
sections: [],
|
|
3344
|
+
commandHint: "💡 结果过多时可指定分类,例如 洛克wiki 技能 水花 或 洛克wiki 物品 国王球",
|
|
3345
|
+
copyright: COPYRIGHT
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
if (mode === "suggestions") {
|
|
3349
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
3350
|
+
const cards = [];
|
|
3351
|
+
for (const item2 of items) {
|
|
3352
|
+
if (!item2 || typeof item2 !== "object") continue;
|
|
3353
|
+
const child = this.catalogForKeyFromPayload(String(item2._catalog_key || ""), this.catalogsCache || {}) || fullCatalog;
|
|
3354
|
+
const card = wikiCardForItem(item2, child?.title || fullCatalog.title, child);
|
|
3355
|
+
if (item2._catalog_title) card.badges = [item2._catalog_title, ...card.badges];
|
|
3356
|
+
cards.push(card);
|
|
3357
|
+
}
|
|
3358
|
+
return {
|
|
3359
|
+
title: `没有找到「${query}」`,
|
|
3360
|
+
subtitle: `Wiki / ${fullCatalog.title} / 联想结果`,
|
|
3361
|
+
summary: "接口没有返回精确结果,下面是按名称、摘要和相似度整理的候选。",
|
|
3362
|
+
image: "",
|
|
3363
|
+
badges: [fullCatalog.title, "联想"],
|
|
3364
|
+
facts: [
|
|
3365
|
+
{ label: "查询词", value: query || "-" },
|
|
3366
|
+
{ label: "候选数", value: String(cards.length) }
|
|
3367
|
+
],
|
|
3368
|
+
actionHint: "接口没有精确命中时,可按候选卡片的绿色分类 tag 加名称继续查询。",
|
|
3369
|
+
actionExamples: this.resultCommandExamples(items, fullCatalog),
|
|
3370
|
+
cards,
|
|
3371
|
+
sections: [],
|
|
3372
|
+
commandHint: `💡 可使用 洛克wiki ${fullCatalog.title} <候选名称或ID> 继续查询`,
|
|
3373
|
+
copyright: COPYRIGHT
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
if (mode === "list") {
|
|
3377
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
3378
|
+
const cards = items.filter((item2) => item2 && typeof item2 === "object").map((item2) => wikiCardForItem(item2, fullCatalog.title, fullCatalog));
|
|
3379
|
+
const catalogInfo = payload?.catalog && typeof payload.catalog === "object" ? payload.catalog : {};
|
|
3380
|
+
return {
|
|
3381
|
+
title: `${fullCatalog.title}列表`,
|
|
3382
|
+
subtitle: `Wiki / ${fullCatalog.title} / 第 ${pageNo} 页`,
|
|
3383
|
+
image: "",
|
|
3384
|
+
summary: catalogInfo.description || `共 ${payload?.total ?? cards.length} 条`,
|
|
3385
|
+
badges: [fullCatalog.title, "列表"],
|
|
3386
|
+
facts: [
|
|
3387
|
+
{ label: "当前页", value: String(payload?.page_no ?? pageNo) },
|
|
3388
|
+
{ label: "总页数", value: String(payload?.total_pages ?? "-") },
|
|
3389
|
+
{ label: "总数", value: String(payload?.total ?? cards.length) }
|
|
3390
|
+
],
|
|
3391
|
+
actionHint: query ? "搜索结果较多时,可按分类加名称继续查询具体条目。" : "",
|
|
3392
|
+
actionExamples: query ? this.resultCommandExamples(items, fullCatalog) : [],
|
|
3393
|
+
cards,
|
|
3394
|
+
sections: [],
|
|
3395
|
+
commandHint: "💡 洛克wiki <类型> <关键词或ID> | 洛克wiki 查看支持类型",
|
|
3396
|
+
copyright: COPYRIGHT
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
const item = payload && typeof payload === "object" ? payload : {};
|
|
3400
|
+
return {
|
|
3401
|
+
title: wikiTitleForItem(item, fullCatalog.title),
|
|
3402
|
+
subtitle: `Wiki / ${fullCatalog.title}`,
|
|
3403
|
+
image: wikiPickImage(item),
|
|
3404
|
+
summary: wikiSummaryForItem(item),
|
|
3405
|
+
badges: [fullCatalog.title, ...wikiBadgesForItem(item)],
|
|
3406
|
+
facts: wikiMetaForItem(item, 16, String(fullCatalog.key || "")),
|
|
3407
|
+
cards: [],
|
|
3408
|
+
sections: wikiSectionsForPayload(item, fullCatalog),
|
|
3409
|
+
commandHint: "💡 洛克wiki <类型> <关键词或ID> | 洛克wiki 查看支持类型",
|
|
3410
|
+
copyright: COPYRIGHT
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
mergeDetailPayloads(base, extra) {
|
|
3414
|
+
const merged = { ...base || {} };
|
|
3415
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
3416
|
+
if (value === void 0 || value === null || value === "" || Array.isArray(value) && !value.length || typeof value === "object" && !Array.isArray(value) && !Object.keys(value).length) continue;
|
|
3417
|
+
const oldValue = merged[key];
|
|
3418
|
+
if (oldValue && typeof oldValue === "object" && !Array.isArray(oldValue) && value && typeof value === "object" && !Array.isArray(value)) {
|
|
3419
|
+
merged[key] = this.mergeDetailPayloads(oldValue, value);
|
|
3420
|
+
} else if (Array.isArray(oldValue) && Array.isArray(value)) {
|
|
3421
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3422
|
+
const combined = [];
|
|
3423
|
+
for (const item of [...oldValue, ...value]) {
|
|
3424
|
+
const marker = typeof item === "object" ? JSON.stringify(item) : String(item);
|
|
3425
|
+
if (seen.has(marker)) continue;
|
|
3426
|
+
seen.add(marker);
|
|
3427
|
+
combined.push(item);
|
|
3428
|
+
}
|
|
3429
|
+
merged[key] = combined;
|
|
3430
|
+
} else {
|
|
3431
|
+
merged[key] = value;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
return merged;
|
|
3435
|
+
}
|
|
3436
|
+
itemDetailCompanionKeys(catalog, detail) {
|
|
3437
|
+
const catalogKey = String(catalog?.key || "");
|
|
3438
|
+
const itemId = detail?.item_id;
|
|
3439
|
+
if (itemId === void 0 || itemId === null || itemId === "") return [];
|
|
3440
|
+
if (["skill-stones", "skill-stone-recipes", "balls", "medals", "pet-carryons"].includes(catalogKey)) {
|
|
3441
|
+
return detail?.item_kind ? ["items"] : [];
|
|
3442
|
+
}
|
|
3443
|
+
if (catalogKey !== "items") return [];
|
|
3444
|
+
const flags = detail?.flags && typeof detail.flags === "object" ? detail.flags : {};
|
|
3445
|
+
const typeLabel = wikiGenericValue(detail?.type || detail?.label_type);
|
|
3446
|
+
if (flags.is_skill_stone || typeLabel === "技能石") return ["skill-stones"];
|
|
3447
|
+
if (flags.is_skill_stone_recipe || String(detail?.name || "").startsWith("配方-")) return ["skill-stone-recipes"];
|
|
3448
|
+
if (typeLabel === "咕噜球") return ["balls"];
|
|
3449
|
+
if (["奖章", "勋章"].includes(typeLabel)) return ["medals"];
|
|
3450
|
+
if (["携带物", "精灵携带物"].includes(typeLabel)) return ["pet-carryons"];
|
|
3451
|
+
return [];
|
|
3452
|
+
}
|
|
3453
|
+
async enrichItemDetail(catalog, detail) {
|
|
3454
|
+
if (!detail || typeof detail !== "object") return detail;
|
|
3455
|
+
const itemId = detail.item_id;
|
|
3456
|
+
if (itemId === void 0 || itemId === null || itemId === "") return detail;
|
|
3457
|
+
let merged = { ...detail };
|
|
3458
|
+
for (const key of this.itemDetailCompanionKeys(catalog, detail)) {
|
|
3459
|
+
const companion = this.catalogForKeyFromPayload(key, this.catalogsCache || {});
|
|
3460
|
+
if (!companion) continue;
|
|
3461
|
+
if (key === "items") {
|
|
3462
|
+
const itemKind = detail.item_kind;
|
|
3463
|
+
if (itemKind === void 0 || itemKind === null || itemKind === "") continue;
|
|
3464
|
+
const companionDetail2 = await this.client.getWikiPath(this.ctx, `/api/v1/games/rocom/wiki/items/${itemKind}/${itemId}`);
|
|
3465
|
+
if (companionDetail2) merged = this.mergeDetailPayloads(companionDetail2, merged);
|
|
3466
|
+
continue;
|
|
3467
|
+
}
|
|
3468
|
+
const companionDetail = await this.client.getWikiPath(this.ctx, `/api/v1/games/rocom/wiki/${key}/${itemId}`);
|
|
3469
|
+
if (companionDetail) merged = this.mergeDetailPayloads(merged, companionDetail);
|
|
3470
|
+
}
|
|
3471
|
+
return merged;
|
|
3472
|
+
}
|
|
3473
|
+
async resolveWikiPet(query) {
|
|
3474
|
+
const trimmed = String(query || "").trim();
|
|
3475
|
+
if (!trimmed) return { detail: null, candidates: [], error: "请输入精灵名称或 ID。用法:洛克wiki 精灵 <精灵名或ID>" };
|
|
3476
|
+
if (/^\d+$/.test(trimmed)) {
|
|
3477
|
+
const detail2 = await this.client.getWikiPet(this.ctx, trimmed);
|
|
3478
|
+
if (detail2) return { detail: detail2, candidates: [], error: "" };
|
|
3479
|
+
return { detail: null, candidates: [], error: `获取 Wiki 精灵详情失败:${this.client.getLastError()}` };
|
|
3480
|
+
}
|
|
3481
|
+
const searchRes = await this.client.listWikiPets(this.ctx, trimmed, 1, 10);
|
|
3482
|
+
const items = (searchRes || {}).items || [];
|
|
3483
|
+
if (!items.length) {
|
|
3484
|
+
const suggestions = await this.suggestCatalogItems(await this.getCatalogByKey("pets"), trimmed, 10);
|
|
3485
|
+
if (suggestions.length) return { detail: null, candidates: suggestions, error: "" };
|
|
3486
|
+
return { detail: null, candidates: [], error: `未找到「${trimmed}」的 Wiki 精灵资料:${this.client.getLastError("无匹配结果")}` };
|
|
3487
|
+
}
|
|
3488
|
+
let selected = findExactWikiMatch(items, trimmed);
|
|
3489
|
+
if (!selected && items.length === 1) selected = items[0];
|
|
3490
|
+
if (!selected) return { detail: null, candidates: items, error: "" };
|
|
3491
|
+
const detail = await this.client.getWikiPet(this.ctx, selected.pet_id);
|
|
3492
|
+
if (detail) return { detail, candidates: [], error: "" };
|
|
3493
|
+
return { detail: null, candidates: [], error: `获取 Wiki 精灵详情失败:${this.client.getLastError()}` };
|
|
3494
|
+
}
|
|
3495
|
+
async resolveWikiSkill(query) {
|
|
3496
|
+
const trimmed = String(query || "").trim();
|
|
3497
|
+
if (!trimmed) return { detail: null, candidates: [], error: "请输入技能名称或 ID。用法:洛克wiki 技能 <技能名或ID>" };
|
|
3498
|
+
if (/^\d+$/.test(trimmed)) {
|
|
3499
|
+
const detail2 = await this.client.getWikiSkill(this.ctx, trimmed);
|
|
3500
|
+
if (detail2) return { detail: detail2, candidates: [], error: "" };
|
|
3501
|
+
return { detail: null, candidates: [], error: `获取技能详情失败:${this.client.getLastError()}` };
|
|
3502
|
+
}
|
|
3503
|
+
const searchRes = await this.client.listWikiSkills(this.ctx, trimmed, 1, 10);
|
|
3504
|
+
const items = (searchRes || {}).items || [];
|
|
3505
|
+
if (!items.length) {
|
|
3506
|
+
const suggestions = await this.suggestCatalogItems(await this.getCatalogByKey("skills"), trimmed, 10);
|
|
3507
|
+
if (suggestions.length) return { detail: null, candidates: suggestions, error: "" };
|
|
3508
|
+
return { detail: null, candidates: [], error: `未找到「${trimmed}」的技能 Wiki 资料:${this.client.getLastError("无匹配结果")}。用法:洛克wiki 技能 <技能名或ID>` };
|
|
3509
|
+
}
|
|
3510
|
+
let selected = findExactWikiMatch(items, trimmed);
|
|
3511
|
+
if (!selected && items.length === 1) selected = items[0];
|
|
3512
|
+
if (!selected) return { detail: null, candidates: items, error: "" };
|
|
3513
|
+
const detail = await this.client.getWikiSkill(this.ctx, selected.skill_id);
|
|
3514
|
+
if (detail) return { detail, candidates: [], error: "" };
|
|
3515
|
+
return { detail: null, candidates: [], error: `获取技能详情失败:${this.client.getLastError()}` };
|
|
3516
|
+
}
|
|
3517
|
+
async fetchWikiPetSections(petId) {
|
|
3518
|
+
const [profile, skills, family, handbook] = await Promise.all([
|
|
3519
|
+
this.client.getWikiPetProfile(this.ctx, petId).catch(() => null),
|
|
3520
|
+
this.client.getWikiPetSkills(this.ctx, petId).catch(() => null),
|
|
3521
|
+
this.client.getWikiPetFamily(this.ctx, petId).catch(() => null),
|
|
3522
|
+
this.client.getWikiPetHandbook(this.ctx, petId).catch(() => null)
|
|
3523
|
+
]);
|
|
3524
|
+
return {
|
|
3525
|
+
profile: profile && typeof profile === "object" ? profile : {},
|
|
3526
|
+
skills: skills && typeof skills === "object" ? skills : {},
|
|
3527
|
+
family: family && typeof family === "object" ? family : {},
|
|
3528
|
+
handbook: handbook && typeof handbook === "object" ? handbook : {}
|
|
3529
|
+
};
|
|
3530
|
+
}
|
|
3531
|
+
// 通过缓存查询技能详情(供家园详情技能补全使用)
|
|
3532
|
+
async getSkillDetailCached(skillId) {
|
|
3533
|
+
const key = String(skillId || "").trim();
|
|
3534
|
+
if (!key) return null;
|
|
3535
|
+
if (this.skillDetailCache.has(key)) return this.skillDetailCache.get(key);
|
|
3536
|
+
const detail = await this.client.getWikiSkill(this.ctx, key);
|
|
3537
|
+
if (detail && typeof detail === "object") {
|
|
3538
|
+
this.skillDetailCache.set(key, detail);
|
|
3539
|
+
return detail;
|
|
3540
|
+
}
|
|
3541
|
+
return null;
|
|
3542
|
+
}
|
|
3543
|
+
// 通过缓存查询精灵详情(供家园详情体重范围使用)
|
|
3544
|
+
async getPetDetailCached(petId) {
|
|
3545
|
+
const key = String(petId || "").trim();
|
|
3546
|
+
if (!key) return null;
|
|
3547
|
+
if (this.petDetailCache.has(key)) return this.petDetailCache.get(key);
|
|
3548
|
+
const detail = await this.client.getWikiPet(this.ctx, key);
|
|
3549
|
+
if (detail && typeof detail === "object") {
|
|
3550
|
+
this.petDetailCache.set(key, detail);
|
|
3551
|
+
return detail;
|
|
3552
|
+
}
|
|
3553
|
+
return null;
|
|
3554
|
+
}
|
|
3555
|
+
async fetchGenericCatalog(catalog, query, pageNo) {
|
|
3556
|
+
const trimmed = String(query || "").trim();
|
|
3557
|
+
const detailPath = String(catalog.detail_path || "");
|
|
3558
|
+
const listPath = String(catalog.list_path || "");
|
|
3559
|
+
if (detailPath && trimmed) {
|
|
3560
|
+
const directParams = this.wikiPathParamsFromItem(catalog, {}, trimmed);
|
|
3561
|
+
const idFields = catalog.id_fields || [];
|
|
3562
|
+
if (idFields.length === 1 && idFields[0] === "pet_id" && !/^\d+$/.test(trimmed)) {
|
|
3563
|
+
const { detail: pet } = await this.resolveWikiPet(trimmed);
|
|
3564
|
+
if (pet?.pet_id !== void 0 && pet?.pet_id !== null && pet?.pet_id !== "") directParams.pet_id = String(pet.pet_id);
|
|
3565
|
+
}
|
|
3566
|
+
if (idFields.length === 1 && idFields[0] === "skill_id" && !/^\d+$/.test(trimmed)) {
|
|
3567
|
+
const { detail: skill } = await this.resolveWikiSkill(trimmed);
|
|
3568
|
+
if (skill?.skill_id !== void 0 && skill?.skill_id !== null && skill?.skill_id !== "") directParams.skill_id = String(skill.skill_id);
|
|
3569
|
+
}
|
|
3570
|
+
const directPath = this.fillDetailPath(catalog, directParams);
|
|
3571
|
+
const typeFields = /* @__PURE__ */ new Set(["item_kind", "asset_type", "action_type"]);
|
|
3572
|
+
let directAllowed = Boolean(directPath) && Object.keys(directParams).length >= idFields.length;
|
|
3573
|
+
for (const field of idFields) {
|
|
3574
|
+
if (typeFields.has(field)) continue;
|
|
3575
|
+
if (!/^\d+$/.test(String(directParams[field] || ""))) {
|
|
3576
|
+
directAllowed = false;
|
|
3577
|
+
break;
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
if (directAllowed) {
|
|
3581
|
+
const detail = await this.client.getWikiPath(this.ctx, directPath);
|
|
3582
|
+
if (detail) return { payload: await this.enrichItemDetail(catalog, detail), mode: "detail", error: "" };
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
if (!listPath) {
|
|
3586
|
+
return { payload: null, mode: "detail", error: `${catalog.title} 需要提供 ID。用法:洛克wiki ${catalog.title} <ID>` };
|
|
3587
|
+
}
|
|
3588
|
+
const listRes = await this.client.listWikiCatalogItems(this.ctx, listPath, trimmed, pageNo, 12, catalog.search !== false);
|
|
3589
|
+
if (listRes === null) {
|
|
3590
|
+
return { payload: null, mode: "list", error: `获取 ${catalog.title} 失败:${this.client.getLastError()}` };
|
|
3591
|
+
}
|
|
3592
|
+
if (!listRes || typeof listRes !== "object" || !("items" in listRes)) {
|
|
3593
|
+
return { payload: listRes, mode: "detail", error: "" };
|
|
3594
|
+
}
|
|
3595
|
+
const items = listRes.items || [];
|
|
3596
|
+
if (trimmed && !items.length) {
|
|
3597
|
+
const suggestions = await this.suggestCatalogItems(catalog, trimmed, 10);
|
|
3598
|
+
if (suggestions.length) return { payload: { items: suggestions, query: trimmed }, mode: "suggestions", error: "" };
|
|
3599
|
+
}
|
|
3600
|
+
if (detailPath && trimmed && items.length) {
|
|
3601
|
+
const selected = this.findCatalogMatch(catalog, items, trimmed);
|
|
3602
|
+
if (selected) {
|
|
3603
|
+
const params = this.wikiPathParamsFromItem(catalog, selected, trimmed);
|
|
3604
|
+
const selectedPath = this.fillDetailPath(catalog, params);
|
|
3605
|
+
if (selectedPath) {
|
|
3606
|
+
const detail = await this.client.getWikiPath(this.ctx, selectedPath);
|
|
3607
|
+
if (detail) return { payload: await this.enrichItemDetail(catalog, detail), mode: "detail", error: "" };
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
return { payload: listRes, mode: "list", error: "" };
|
|
3612
|
+
}
|
|
3613
|
+
async fetchDetailForCatalogItem(catalog, item, query) {
|
|
3614
|
+
if (!catalog.detail_path) return null;
|
|
3615
|
+
const params = this.wikiPathParamsFromItem(catalog, item, query);
|
|
3616
|
+
const detailPath = this.fillDetailPath(catalog, params);
|
|
3617
|
+
if (!detailPath) return null;
|
|
3618
|
+
const detail = await this.client.getWikiPath(this.ctx, detailPath);
|
|
3619
|
+
return this.enrichItemDetail(catalog, detail);
|
|
3620
|
+
}
|
|
3621
|
+
async fetchGlobalSearch(query, pageNo) {
|
|
3622
|
+
const trimmed = String(query || "").trim();
|
|
3623
|
+
if (!trimmed) return { payload: null, mode: "global-search", error: "请输入 Wiki 关键词。" };
|
|
3624
|
+
const catalogsPayload = await this.getCatalogsPayload();
|
|
3625
|
+
const catalogs = this.globalSearchCatalogs(catalogsPayload);
|
|
3626
|
+
if (!catalogs.length) return { payload: null, mode: "global-search", error: "暂无可全局搜索的 Wiki 接口。" };
|
|
3627
|
+
const results = [];
|
|
3628
|
+
const concurrency = 8;
|
|
3629
|
+
let index = 0;
|
|
3630
|
+
const worker = /* @__PURE__ */ __name(async () => {
|
|
3631
|
+
while (index < catalogs.length) {
|
|
3632
|
+
const catalog = catalogs[index++];
|
|
3633
|
+
try {
|
|
3634
|
+
const res = await this.client.listWikiCatalogItems(this.ctx, catalog.list_path, trimmed, pageNo, 6, true);
|
|
3635
|
+
if (res === null) {
|
|
3636
|
+
results.push({ catalog, items: [], error: this.client.getLastError("") });
|
|
3637
|
+
} else {
|
|
3638
|
+
const items2 = res && typeof res === "object" ? res.items : [];
|
|
3639
|
+
results.push({ catalog, items: (items2 || []).filter((item) => item && typeof item === "object"), error: "" });
|
|
3640
|
+
}
|
|
3641
|
+
} catch (e) {
|
|
3642
|
+
results.push({ catalog, items: [], error: String(e) });
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
}, "worker");
|
|
3646
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, catalogs.length) }, worker));
|
|
3647
|
+
const collected = /* @__PURE__ */ new Map();
|
|
3648
|
+
const exactKeys = /* @__PURE__ */ new Set();
|
|
3649
|
+
const errors = [];
|
|
3650
|
+
for (const { catalog, items: items2, error } of results) {
|
|
3651
|
+
if (error) {
|
|
3652
|
+
errors.push(`${catalog.title || catalog.key}:${error}`);
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
for (const item of items2) {
|
|
3656
|
+
const merged = { ...item, _catalog_key: catalog.key, _catalog_title: catalog.title };
|
|
3657
|
+
const title = wikiTitleForItem(item, "");
|
|
3658
|
+
const keyParts = [String(catalog.key || ""), title];
|
|
3659
|
+
for (const field of catalog.id_fields || []) {
|
|
3660
|
+
let value = item[field];
|
|
3661
|
+
if ((value === void 0 || value === null || value === "") && ["ball_id", "plant_id", "carryon_id"].includes(field)) {
|
|
3662
|
+
value = item.item_id ?? item.id;
|
|
3663
|
+
}
|
|
3664
|
+
if (value !== void 0 && value !== null && value !== "") keyParts.push(String(value));
|
|
3665
|
+
}
|
|
3666
|
+
const dedupeKey = keyParts.join("|");
|
|
3667
|
+
if (collected.has(dedupeKey)) continue;
|
|
3668
|
+
let score = wikiSimilarityScore(trimmed, merged);
|
|
3669
|
+
if (this.itemMatchesQueryExact(catalog, item, trimmed)) {
|
|
3670
|
+
score += 10 - wikiGlobalCatalogPriority(catalog) * 0.01;
|
|
3671
|
+
exactKeys.add(dedupeKey);
|
|
3672
|
+
}
|
|
3673
|
+
merged._match_score = score;
|
|
3674
|
+
collected.set(dedupeKey, merged);
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
const items = [...collected.values()];
|
|
3678
|
+
items.sort((a, b) => (b._match_score || 0) - (a._match_score || 0));
|
|
3679
|
+
const exactItems = [...exactKeys].filter((key) => collected.has(key)).map((key) => collected.get(key));
|
|
3680
|
+
if (exactItems.length) {
|
|
3681
|
+
const priorityOf = /* @__PURE__ */ __name((item) => wikiGlobalCatalogPriority(
|
|
3682
|
+
this.catalogForKeyFromPayload(String(item._catalog_key || ""), catalogsPayload)
|
|
3683
|
+
), "priorityOf");
|
|
3684
|
+
exactItems.sort((a, b) => priorityOf(a) - priorityOf(b) || (b._match_score || 0) - (a._match_score || 0));
|
|
3685
|
+
const bestPriority = priorityOf(exactItems[0]);
|
|
3686
|
+
const bestItems = exactItems.filter((item) => priorityOf(item) === bestPriority);
|
|
3687
|
+
if (bestItems.length === 1) {
|
|
3688
|
+
const exactItem = bestItems[0];
|
|
3689
|
+
const catalog = this.catalogForKeyFromPayload(String(exactItem._catalog_key || ""), catalogsPayload);
|
|
3690
|
+
if (catalog) {
|
|
3691
|
+
const detail = await this.fetchDetailForCatalogItem(catalog, exactItem, trimmed);
|
|
3692
|
+
if (detail) return { payload: { item: detail, _catalog: catalog }, mode: "global-detail", error: "" };
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
if (items.length) {
|
|
3697
|
+
return { payload: { items: items.slice(0, 12), query: trimmed, total: items.length, _global: true }, mode: "global-search", error: "" };
|
|
3698
|
+
}
|
|
3699
|
+
if (errors.length) return { payload: null, mode: "global-search", error: `全局搜索失败:${errors[0]}` };
|
|
3700
|
+
return { payload: { items: [], query: trimmed, total: 0, _global: true }, mode: "global-search", error: "" };
|
|
3701
|
+
}
|
|
3702
|
+
};
|
|
3703
|
+
|
|
3704
|
+
// src/atlas-service.ts
|
|
3705
|
+
var import_koishi6 = require("koishi");
|
|
3706
|
+
var import_node_child_process = require("node:child_process");
|
|
3707
|
+
var import_node_fs4 = __toESM(require("node:fs"));
|
|
3708
|
+
var import_node_path4 = __toESM(require("node:path"));
|
|
3709
|
+
var import_node_zlib = __toESM(require("node:zlib"));
|
|
3710
|
+
var logger6 = new import_koishi6.Logger("rocom-atlas");
|
|
3711
|
+
var DEFAULT_ATLAS_ZIP_URLS = [
|
|
3712
|
+
"https://codeload.github.com/Entropy-Increase-Team/Rocom-Atlas/zip/refs/heads/main",
|
|
3713
|
+
"https://github.com/Entropy-Increase-Team/Rocom-Atlas/archive/refs/heads/main.zip"
|
|
3714
|
+
];
|
|
3715
|
+
var DEFAULT_ATLAS_GIT_URL = "https://github.com/Entropy-Increase-Team/Rocom-Atlas.git";
|
|
3716
|
+
function extractZip(buffer, destDir) {
|
|
3717
|
+
let eocd = -1;
|
|
3718
|
+
const minEocd = Math.max(0, buffer.length - 65557);
|
|
3719
|
+
for (let i = buffer.length - 22; i >= minEocd; i--) {
|
|
3720
|
+
if (buffer.readUInt32LE(i) === 101010256) {
|
|
3721
|
+
eocd = i;
|
|
3722
|
+
break;
|
|
3723
|
+
}
|
|
3724
|
+
}
|
|
3725
|
+
if (eocd < 0) throw new Error("ZIP 结构异常:找不到中央目录结尾");
|
|
3726
|
+
const entryCount = buffer.readUInt16LE(eocd + 10);
|
|
3727
|
+
let offset = buffer.readUInt32LE(eocd + 16);
|
|
3728
|
+
for (let i = 0; i < entryCount; i++) {
|
|
3729
|
+
if (buffer.readUInt32LE(offset) !== 33639248) throw new Error("ZIP 结构异常:中央目录签名错误");
|
|
3730
|
+
const compressionMethod = buffer.readUInt16LE(offset + 10);
|
|
3731
|
+
const compressedSize = buffer.readUInt32LE(offset + 20);
|
|
3732
|
+
const nameLength = buffer.readUInt16LE(offset + 28);
|
|
3733
|
+
const extraLength = buffer.readUInt16LE(offset + 30);
|
|
3734
|
+
const commentLength = buffer.readUInt16LE(offset + 32);
|
|
3735
|
+
const localHeaderOffset = buffer.readUInt32LE(offset + 42);
|
|
3736
|
+
const name2 = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
|
|
3737
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
3738
|
+
const normalized = name2.replace(/\\/g, "/");
|
|
3739
|
+
if (normalized.includes("..")) continue;
|
|
3740
|
+
const target = import_node_path4.default.join(destDir, ...normalized.split("/"));
|
|
3741
|
+
if (!import_node_path4.default.resolve(target).startsWith(import_node_path4.default.resolve(destDir))) continue;
|
|
3742
|
+
if (normalized.endsWith("/")) {
|
|
3743
|
+
import_node_fs4.default.mkdirSync(target, { recursive: true });
|
|
3744
|
+
continue;
|
|
3745
|
+
}
|
|
3746
|
+
if (buffer.readUInt32LE(localHeaderOffset) !== 67324752) throw new Error("ZIP 结构异常:本地文件头签名错误");
|
|
3747
|
+
const localNameLength = buffer.readUInt16LE(localHeaderOffset + 26);
|
|
3748
|
+
const localExtraLength = buffer.readUInt16LE(localHeaderOffset + 28);
|
|
3749
|
+
const dataStart = localHeaderOffset + 30 + localNameLength + localExtraLength;
|
|
3750
|
+
const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
|
|
3751
|
+
let content;
|
|
3752
|
+
if (compressionMethod === 0) content = Buffer.from(compressed);
|
|
3753
|
+
else if (compressionMethod === 8) content = import_node_zlib.default.inflateRawSync(compressed);
|
|
3754
|
+
else throw new Error(`ZIP 不支持的压缩方式: ${compressionMethod}`);
|
|
3755
|
+
import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
|
|
3756
|
+
import_node_fs4.default.writeFileSync(target, content);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
__name(extractZip, "extractZip");
|
|
3760
|
+
function gitClone(dst, gitUrl) {
|
|
3761
|
+
return new Promise((resolve, reject) => {
|
|
3762
|
+
const child = (0, import_node_child_process.spawn)(
|
|
3763
|
+
"git",
|
|
3764
|
+
["clone", "--depth", "1", "--single-branch", "--progress", gitUrl, dst],
|
|
3765
|
+
{ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }
|
|
3766
|
+
);
|
|
3767
|
+
const stderrLines = [];
|
|
3768
|
+
let stderrTail = "";
|
|
3769
|
+
child.stderr.on("data", (chunk) => {
|
|
3770
|
+
stderrTail += chunk.toString("utf8");
|
|
3771
|
+
const lines = stderrTail.split(/[\r\n]+/);
|
|
3772
|
+
stderrTail = lines.pop() || "";
|
|
3773
|
+
for (const line of lines) {
|
|
3774
|
+
const text = line.trim();
|
|
3775
|
+
if (!text) continue;
|
|
3776
|
+
stderrLines.push(text);
|
|
3777
|
+
logger6.info(`git clone: ${text}`);
|
|
3778
|
+
}
|
|
3779
|
+
});
|
|
3780
|
+
const timer = setTimeout(() => {
|
|
3781
|
+
child.kill();
|
|
3782
|
+
reject(new Error("git clone Rocom-Atlas 超时(180 秒)"));
|
|
3783
|
+
}, 18e4);
|
|
3784
|
+
child.on("error", (error) => {
|
|
3785
|
+
clearTimeout(timer);
|
|
3786
|
+
reject(new Error(`git clone Rocom-Atlas 失败:${error.message}`));
|
|
3787
|
+
});
|
|
3788
|
+
child.on("close", (code) => {
|
|
3789
|
+
clearTimeout(timer);
|
|
3790
|
+
if (code === 0) resolve();
|
|
3791
|
+
else reject(new Error(`git clone Rocom-Atlas 失败(exit ${code}):${stderrLines.slice(-3).join(" | ")}`));
|
|
3792
|
+
});
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
__name(gitClone, "gitClone");
|
|
3796
|
+
var AtlasService = class {
|
|
3797
|
+
constructor(dataDir, options = {}) {
|
|
3798
|
+
this.dataDir = dataDir;
|
|
3799
|
+
this.zipUrls = options.zipUrls?.length ? options.zipUrls : DEFAULT_ATLAS_ZIP_URLS;
|
|
3800
|
+
this.gitUrl = options.gitUrl || DEFAULT_ATLAS_GIT_URL;
|
|
3801
|
+
}
|
|
3802
|
+
static {
|
|
3803
|
+
__name(this, "AtlasService");
|
|
3804
|
+
}
|
|
3805
|
+
zipUrls;
|
|
3806
|
+
gitUrl;
|
|
3807
|
+
get atlasDir() {
|
|
3808
|
+
return import_node_path4.default.join(this.dataDir, "rocom_atlas");
|
|
3809
|
+
}
|
|
3810
|
+
indexPath() {
|
|
3811
|
+
return import_node_path4.default.join(this.atlasDir, "path.json");
|
|
3812
|
+
}
|
|
3813
|
+
petsDir() {
|
|
3814
|
+
return import_node_path4.default.join(this.atlasDir, "pets");
|
|
3815
|
+
}
|
|
3816
|
+
aliasPath() {
|
|
3817
|
+
return import_node_path4.default.join(this.atlasDir, "othername", "pets.yaml");
|
|
3818
|
+
}
|
|
3819
|
+
isReady() {
|
|
3820
|
+
return import_node_fs4.default.existsSync(this.indexPath()) && import_node_fs4.default.existsSync(this.petsDir());
|
|
3821
|
+
}
|
|
3822
|
+
loadIndex() {
|
|
3823
|
+
try {
|
|
3824
|
+
const payload = JSON.parse(import_node_fs4.default.readFileSync(this.indexPath(), "utf-8"));
|
|
3825
|
+
const pets = payload?.pets;
|
|
3826
|
+
return pets && typeof pets === "object" ? pets : {};
|
|
3827
|
+
} catch (e) {
|
|
3828
|
+
logger6.warn(`读取本地图鉴索引失败: ${e}`);
|
|
3829
|
+
return {};
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
stripYamlValue(value) {
|
|
3833
|
+
const text = String(value || "").trim();
|
|
3834
|
+
if (text.length >= 2 && text[0] === text[text.length - 1] && ["'", '"'].includes(text[0])) {
|
|
3835
|
+
return text.slice(1, -1).trim();
|
|
3836
|
+
}
|
|
3837
|
+
return text;
|
|
3838
|
+
}
|
|
3839
|
+
// 解析上游简单结构的 pets.yaml:`正式名:` + 缩进 `- 别名` 列表。
|
|
3840
|
+
loadPetAliases() {
|
|
3841
|
+
const filePath = this.aliasPath();
|
|
3842
|
+
if (!import_node_fs4.default.existsSync(filePath)) return {};
|
|
3843
|
+
const aliases = {};
|
|
3844
|
+
let currentName = "";
|
|
3845
|
+
try {
|
|
3846
|
+
const lines = import_node_fs4.default.readFileSync(filePath, "utf-8").split(/\r?\n/);
|
|
3847
|
+
for (const line of lines) {
|
|
3848
|
+
const stripped = line.trim();
|
|
3849
|
+
if (!stripped || stripped.startsWith("#")) continue;
|
|
3850
|
+
if (!/^\s/.test(line) && stripped.endsWith(":")) {
|
|
3851
|
+
currentName = this.stripYamlValue(stripped.slice(0, -1));
|
|
3852
|
+
if (currentName) {
|
|
3853
|
+
const key = normalizeQueryText(currentName);
|
|
3854
|
+
aliases[key] = aliases[key] || [];
|
|
3855
|
+
if (!aliases[key].includes(currentName)) aliases[key].push(currentName);
|
|
3856
|
+
}
|
|
3857
|
+
continue;
|
|
3858
|
+
}
|
|
3859
|
+
if (currentName && stripped.startsWith("-")) {
|
|
3860
|
+
const alias = this.stripYamlValue(stripped.slice(1));
|
|
3861
|
+
if (!alias) continue;
|
|
3862
|
+
const key = normalizeQueryText(alias);
|
|
3863
|
+
aliases[key] = aliases[key] || [];
|
|
3864
|
+
if (!aliases[key].includes(currentName)) aliases[key].push(currentName);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
} catch (e) {
|
|
3868
|
+
logger6.warn(`读取本地图鉴别名失败: ${e}`);
|
|
3869
|
+
return {};
|
|
3870
|
+
}
|
|
3871
|
+
return aliases;
|
|
3872
|
+
}
|
|
3873
|
+
localImagePath(atlasRelPath) {
|
|
3874
|
+
const rel = String(atlasRelPath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
3875
|
+
if (!rel) return "";
|
|
3876
|
+
const candidate = import_node_path4.default.resolve(this.atlasDir, ...rel.split("/"));
|
|
3877
|
+
const root = import_node_path4.default.resolve(this.atlasDir);
|
|
3878
|
+
if (!candidate.startsWith(root)) return "";
|
|
3879
|
+
return candidate;
|
|
3880
|
+
}
|
|
3881
|
+
existingImagePath(index, name2) {
|
|
3882
|
+
const filePath = this.localImagePath(index[name2] || "");
|
|
3883
|
+
return filePath && import_node_fs4.default.existsSync(filePath) ? filePath : "";
|
|
3884
|
+
}
|
|
3885
|
+
findMatch(query) {
|
|
3886
|
+
const trimmed = String(query || "").trim();
|
|
3887
|
+
if (!trimmed) return { name: "", imagePath: "", candidates: [] };
|
|
3888
|
+
if (/^\d+$/.test(trimmed)) {
|
|
3889
|
+
const filePath = import_node_path4.default.join(this.petsDir(), `${trimmed}.png`);
|
|
3890
|
+
if (import_node_fs4.default.existsSync(filePath)) return { name: `#${trimmed}`, imagePath: filePath, candidates: [] };
|
|
3891
|
+
}
|
|
3892
|
+
const index = this.loadIndex();
|
|
3893
|
+
if (!Object.keys(index).length) return { name: "", imagePath: "", candidates: [] };
|
|
3894
|
+
const normalizedQuery = normalizeQueryText(trimmed);
|
|
3895
|
+
let exactKey = "";
|
|
3896
|
+
for (const name2 of Object.keys(index)) {
|
|
3897
|
+
if (normalizeQueryText(name2) === normalizedQuery) {
|
|
3898
|
+
exactKey = name2;
|
|
3899
|
+
break;
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
if (exactKey) {
|
|
3903
|
+
const filePath = this.existingImagePath(index, exactKey);
|
|
3904
|
+
if (filePath) return { name: exactKey, imagePath: filePath, candidates: [] };
|
|
3905
|
+
}
|
|
3906
|
+
const aliasMap = this.loadPetAliases();
|
|
3907
|
+
const exactAliasCandidates = (aliasMap[normalizedQuery] || []).filter((name2) => this.existingImagePath(index, name2));
|
|
3908
|
+
if (exactAliasCandidates.length) {
|
|
3909
|
+
const only = exactAliasCandidates[0];
|
|
3910
|
+
return { name: only, imagePath: this.existingImagePath(index, only), candidates: [] };
|
|
3911
|
+
}
|
|
3912
|
+
const candidates = [];
|
|
3913
|
+
const addCandidate = /* @__PURE__ */ __name((name2) => {
|
|
3914
|
+
if (!candidates.includes(name2) && this.existingImagePath(index, name2)) candidates.push(name2);
|
|
3915
|
+
}, "addCandidate");
|
|
3916
|
+
for (const name2 of Object.keys(index)) {
|
|
3917
|
+
const normalizedName = normalizeQueryText(name2);
|
|
3918
|
+
if (normalizedQuery && (normalizedName.includes(normalizedQuery) || normalizedQuery.includes(normalizedName))) {
|
|
3919
|
+
addCandidate(name2);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
for (const [alias, names] of Object.entries(aliasMap)) {
|
|
3923
|
+
if (normalizedQuery && (alias.includes(normalizedQuery) || normalizedQuery.includes(alias))) {
|
|
3924
|
+
for (const name2 of names) addCandidate(name2);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
if (candidates.length === 1) {
|
|
3928
|
+
const only = candidates[0];
|
|
3929
|
+
return { name: only, imagePath: this.existingImagePath(index, only), candidates: [] };
|
|
3930
|
+
}
|
|
3931
|
+
return { name: "", imagePath: "", candidates: candidates.slice(0, 10) };
|
|
3932
|
+
}
|
|
3933
|
+
prepareAtlasDir(sourceRoot, preparedDir) {
|
|
3934
|
+
import_node_fs4.default.mkdirSync(preparedDir, { recursive: true });
|
|
3935
|
+
for (const name2 of ["path.json", "index", "othername", "pets"]) {
|
|
3936
|
+
const src = import_node_path4.default.join(sourceRoot, name2);
|
|
3937
|
+
const dst = import_node_path4.default.join(preparedDir, name2);
|
|
3938
|
+
if (import_node_fs4.default.existsSync(src)) {
|
|
3939
|
+
import_node_fs4.default.cpSync(src, dst, { recursive: true });
|
|
3940
|
+
}
|
|
3941
|
+
}
|
|
3942
|
+
if (!import_node_fs4.default.existsSync(import_node_path4.default.join(preparedDir, "path.json"))) throw new Error("Atlas 缺少 path.json");
|
|
3943
|
+
if (!import_node_fs4.default.existsSync(import_node_path4.default.join(preparedDir, "othername", "pets.yaml"))) throw new Error("Atlas 缺少 othername/pets.yaml");
|
|
3944
|
+
const petsDir = import_node_path4.default.join(preparedDir, "pets");
|
|
3945
|
+
if (!import_node_fs4.default.existsSync(petsDir)) throw new Error("Atlas 缺少 pets 图片目录");
|
|
3946
|
+
const imageCount = import_node_fs4.default.readdirSync(petsDir).filter((name2) => name2.toLowerCase().endsWith(".png")).length;
|
|
3947
|
+
let totalBytes = 0;
|
|
3948
|
+
const walk = /* @__PURE__ */ __name((dir) => {
|
|
3949
|
+
for (const entry of import_node_fs4.default.readdirSync(dir, { withFileTypes: true })) {
|
|
3950
|
+
const entryPath = import_node_path4.default.join(dir, entry.name);
|
|
3951
|
+
if (entry.isDirectory()) walk(entryPath);
|
|
3952
|
+
else totalBytes += import_node_fs4.default.statSync(entryPath).size;
|
|
3953
|
+
}
|
|
3954
|
+
}, "walk");
|
|
3955
|
+
walk(preparedDir);
|
|
3956
|
+
return { imageCount, totalBytes };
|
|
3957
|
+
}
|
|
3958
|
+
replaceAtlasDir(preparedDir) {
|
|
3959
|
+
const target = import_node_path4.default.resolve(this.atlasDir);
|
|
3960
|
+
const dataRoot = import_node_path4.default.resolve(this.dataDir);
|
|
3961
|
+
if (!target.startsWith(dataRoot)) throw new Error("图鉴目标目录不在插件数据目录内");
|
|
3962
|
+
if (import_node_fs4.default.existsSync(target)) import_node_fs4.default.rmSync(target, { recursive: true, force: true });
|
|
3963
|
+
import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(target), { recursive: true });
|
|
3964
|
+
try {
|
|
3965
|
+
import_node_fs4.default.renameSync(preparedDir, target);
|
|
3966
|
+
} catch (e) {
|
|
3967
|
+
if (["EXDEV", "EPERM"].includes(e?.code)) {
|
|
3968
|
+
logger6.info(`rename 失败(${e.code}),回退为复制方式迁移图鉴目录`);
|
|
3969
|
+
import_node_fs4.default.cpSync(preparedDir, target, { recursive: true });
|
|
3970
|
+
} else {
|
|
3971
|
+
throw e;
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
// 流式下载压缩包:每 2 秒输出一次已下载字节数/速度,避免大文件下载期间毫无动静。
|
|
3976
|
+
async downloadZipWithProgress(ctx, url) {
|
|
3977
|
+
const started = Date.now();
|
|
3978
|
+
const res = await ctx.http(url, { responseType: "stream", timeout: 18e4 });
|
|
3979
|
+
const totalHeader = Number(res.headers?.get?.("content-length"));
|
|
3980
|
+
const totalBytes = Number.isFinite(totalHeader) && totalHeader > 0 ? totalHeader : 0;
|
|
3981
|
+
const totalText = totalBytes ? `${(totalBytes / 1024 / 1024).toFixed(1)} MB` : "未知大小";
|
|
3982
|
+
logger6.info(`开始下载图鉴压缩包:${url}(${totalText})`);
|
|
3983
|
+
const chunks = [];
|
|
3984
|
+
let received = 0;
|
|
3985
|
+
let lastLogAt = Date.now();
|
|
3986
|
+
const reader = res.data.getReader();
|
|
3987
|
+
while (true) {
|
|
3988
|
+
const { done, value } = await reader.read();
|
|
3989
|
+
if (done) break;
|
|
3990
|
+
if (value) {
|
|
3991
|
+
chunks.push(Buffer.from(value));
|
|
3992
|
+
received += value.byteLength;
|
|
3993
|
+
}
|
|
3994
|
+
const now = Date.now();
|
|
3995
|
+
if (now - lastLogAt >= 2e3) {
|
|
3996
|
+
lastLogAt = now;
|
|
3997
|
+
const mb = (received / 1024 / 1024).toFixed(1);
|
|
3998
|
+
const speed = (received / 1024 / (now - started) * 1e3 / 1024).toFixed(2);
|
|
3999
|
+
const percentText = totalBytes ? `${Math.min(99, Math.round(received / totalBytes * 100))}%,` : "";
|
|
4000
|
+
logger6.info(`图鉴压缩包下载中:${percentText}已收到 ${mb} MB,平均 ${speed} MB/s`);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
const elapsed = ((Date.now() - started) / 1e3).toFixed(1);
|
|
4004
|
+
logger6.info(`图鉴压缩包下载完成:共 ${(received / 1024 / 1024).toFixed(1)} MB,耗时 ${elapsed} 秒`);
|
|
4005
|
+
return Buffer.concat(chunks);
|
|
4006
|
+
}
|
|
4007
|
+
async download(ctx, progressCb) {
|
|
4008
|
+
const emit = /* @__PURE__ */ __name(async (percent, stage) => {
|
|
4009
|
+
if (progressCb) await progressCb(Math.max(0, Math.min(100, Math.round(percent))), stage);
|
|
4010
|
+
}, "emit");
|
|
4011
|
+
import_node_fs4.default.mkdirSync(this.dataDir, { recursive: true });
|
|
4012
|
+
for (const entry of import_node_fs4.default.readdirSync(this.dataDir, { withFileTypes: true })) {
|
|
4013
|
+
if (entry.isDirectory() && entry.name.startsWith(".atlas_tmp_")) {
|
|
4014
|
+
cleanupTempDir(import_node_path4.default.join(this.dataDir, entry.name));
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
const tmpRoot = import_node_fs4.default.mkdtempSync(import_node_path4.default.join(this.dataDir, ".atlas_tmp_"));
|
|
4018
|
+
const extractDir = import_node_path4.default.join(tmpRoot, "extract");
|
|
4019
|
+
let preparedDir = import_node_path4.default.join(tmpRoot, "rocom_atlas");
|
|
4020
|
+
const cloneDir = import_node_path4.default.join(tmpRoot, "clone");
|
|
4021
|
+
try {
|
|
4022
|
+
let zipBuffer = null;
|
|
4023
|
+
const zipErrors = [];
|
|
4024
|
+
for (const url of this.zipUrls) {
|
|
4025
|
+
try {
|
|
4026
|
+
await emit(10, "正在下载图鉴压缩包");
|
|
4027
|
+
zipBuffer = await this.downloadZipWithProgress(ctx, url);
|
|
4028
|
+
await emit(80, "图鉴压缩包下载完成");
|
|
4029
|
+
break;
|
|
4030
|
+
} catch (e) {
|
|
4031
|
+
logger6.warn(`图鉴压缩包下载失败:${url}: ${e}`);
|
|
4032
|
+
zipErrors.push(`${url}: ${e}`);
|
|
4033
|
+
zipBuffer = null;
|
|
1823
4034
|
}
|
|
1824
|
-
import_node_fs3.default.rmSync(tempDir, { recursive: true, force: true });
|
|
1825
4035
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
4036
|
+
let stats;
|
|
4037
|
+
if (zipBuffer) {
|
|
4038
|
+
await emit(80, "正在解压图鉴文件");
|
|
4039
|
+
logger6.info("正在解压图鉴压缩包…");
|
|
4040
|
+
import_node_fs4.default.mkdirSync(extractDir, { recursive: true });
|
|
4041
|
+
extractZip(zipBuffer, extractDir);
|
|
4042
|
+
logger6.info("图鉴压缩包解压完成");
|
|
4043
|
+
const roots = import_node_fs4.default.readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => import_node_path4.default.join(extractDir, entry.name));
|
|
4044
|
+
if (!roots.length) throw new Error("Atlas 压缩包结构异常");
|
|
4045
|
+
stats = this.prepareAtlasDir(roots[0], preparedDir);
|
|
4046
|
+
} else {
|
|
4047
|
+
logger6.warn(`GitHub zip 下载失败,尝试 git clone:${zipErrors.join(" | ")}`);
|
|
4048
|
+
await emit(20, "正在 git clone 图鉴仓库");
|
|
4049
|
+
await gitClone(cloneDir, this.gitUrl);
|
|
4050
|
+
await emit(80, "正在整理图鉴文件");
|
|
4051
|
+
stats = this.prepareAtlasDir(cloneDir, preparedDir);
|
|
4052
|
+
}
|
|
4053
|
+
this.replaceAtlasDir(preparedDir);
|
|
4054
|
+
preparedDir = "";
|
|
4055
|
+
logger6.info(`本地图鉴缓存已更新:${stats.imageCount} 张图片,${(stats.totalBytes / 1024 / 1024).toFixed(1)} MB`);
|
|
4056
|
+
await emit(100, "本地图鉴缓存已更新");
|
|
4057
|
+
return stats;
|
|
4058
|
+
} finally {
|
|
4059
|
+
cleanupTempDir(tmpRoot);
|
|
1829
4060
|
}
|
|
1830
4061
|
}
|
|
1831
4062
|
};
|
|
4063
|
+
function cleanupTempDir(tmpRoot) {
|
|
4064
|
+
try {
|
|
4065
|
+
import_node_fs4.default.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
|
|
4066
|
+
} catch {
|
|
4067
|
+
try {
|
|
4068
|
+
const clearReadonly = /* @__PURE__ */ __name((dir) => {
|
|
4069
|
+
for (const entry of import_node_fs4.default.readdirSync(dir, { withFileTypes: true })) {
|
|
4070
|
+
const entryPath = import_node_path4.default.join(dir, entry.name);
|
|
4071
|
+
try {
|
|
4072
|
+
import_node_fs4.default.chmodSync(entryPath, 438);
|
|
4073
|
+
} catch {
|
|
4074
|
+
}
|
|
4075
|
+
if (entry.isDirectory()) clearReadonly(entryPath);
|
|
4076
|
+
}
|
|
4077
|
+
}, "clearReadonly");
|
|
4078
|
+
clearReadonly(tmpRoot);
|
|
4079
|
+
import_node_fs4.default.rmSync(tmpRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
|
|
4080
|
+
} catch (e) {
|
|
4081
|
+
logger6.warn(`清理图鉴临时目录失败(不影响下载结果):${tmpRoot}: ${e}`);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
__name(cleanupTempDir, "cleanupTempDir");
|
|
1832
4086
|
|
|
1833
4087
|
// src/role-token.ts
|
|
1834
4088
|
function normalize(value) {
|
|
@@ -1964,12 +4218,12 @@ async function migrateLegacyFrameworkTokens(ctx, userMgr) {
|
|
|
1964
4218
|
__name(migrateLegacyFrameworkTokens, "migrateLegacyFrameworkTokens");
|
|
1965
4219
|
|
|
1966
4220
|
// src/commands/account.ts
|
|
1967
|
-
var
|
|
4221
|
+
var import_koishi8 = require("koishi");
|
|
1968
4222
|
|
|
1969
4223
|
// src/send-image.ts
|
|
1970
|
-
var
|
|
1971
|
-
var
|
|
1972
|
-
var
|
|
4224
|
+
var import_koishi7 = require("koishi");
|
|
4225
|
+
var import_node_zlib2 = __toESM(require("node:zlib"));
|
|
4226
|
+
var logger7 = new import_koishi7.Logger("rocom-send");
|
|
1973
4227
|
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1974
4228
|
var JPEG_SIGNATURE = Buffer.from([255, 216, 255]);
|
|
1975
4229
|
function formatSessionContext(session) {
|
|
@@ -2044,9 +4298,9 @@ function compressPngImage(image, config) {
|
|
|
2044
4298
|
if (type === "IEND") break;
|
|
2045
4299
|
}
|
|
2046
4300
|
if (!idatChunks.length) return image;
|
|
2047
|
-
const rawImageData =
|
|
4301
|
+
const rawImageData = import_node_zlib2.default.inflateSync(Buffer.concat(idatChunks));
|
|
2048
4302
|
const level = Math.max(0, Math.min(9, Math.floor(config.imageCompressionLevel ?? 9)));
|
|
2049
|
-
const compressedIdat =
|
|
4303
|
+
const compressedIdat = import_node_zlib2.default.deflateSync(rawImageData, { level });
|
|
2050
4304
|
const rebuiltChunks = [];
|
|
2051
4305
|
let wroteIdat = false;
|
|
2052
4306
|
for (const chunk of chunks) {
|
|
@@ -2062,7 +4316,7 @@ function compressPngImage(image, config) {
|
|
|
2062
4316
|
const compressed = Buffer.concat([PNG_SIGNATURE, ...rebuiltChunks]);
|
|
2063
4317
|
if (compressed.length < image.length) return compressed;
|
|
2064
4318
|
} catch (err) {
|
|
2065
|
-
|
|
4319
|
+
logger7.warn(`PNG compression failed, sending original image: ${err}`);
|
|
2066
4320
|
}
|
|
2067
4321
|
return image;
|
|
2068
4322
|
}
|
|
@@ -2070,36 +4324,36 @@ __name(compressPngImage, "compressPngImage");
|
|
|
2070
4324
|
async function sendImageWithFallback(session, image, fallbackText, scene, compressionConfig) {
|
|
2071
4325
|
const ctxInfo = formatSessionContext(session);
|
|
2072
4326
|
if (!image) {
|
|
2073
|
-
|
|
4327
|
+
logger7.warn(`[${scene}] render returned empty image, fallback to text | ${ctxInfo}`);
|
|
2074
4328
|
try {
|
|
2075
4329
|
await session.send(fallbackText);
|
|
2076
4330
|
} catch (e) {
|
|
2077
|
-
|
|
4331
|
+
logger7.error(`[${scene}] text fallback send failed | ${ctxInfo} | ${e}`);
|
|
2078
4332
|
}
|
|
2079
4333
|
return;
|
|
2080
4334
|
}
|
|
2081
4335
|
const outputImage = compressionConfig ? compressPngImage(image, compressionConfig) : image;
|
|
2082
4336
|
if (outputImage.length < image.length) {
|
|
2083
|
-
|
|
4337
|
+
logger7.info(`[${scene}] compressed image ${image.length}B -> ${outputImage.length}B | ${ctxInfo}`);
|
|
2084
4338
|
}
|
|
2085
4339
|
try {
|
|
2086
|
-
const result = await session.send(
|
|
4340
|
+
const result = await session.send(import_koishi7.h.image(outputImage, detectImageMime(outputImage)));
|
|
2087
4341
|
if (!hasSendResult(result)) {
|
|
2088
|
-
|
|
4342
|
+
logger7.warn(`[${scene}] image send returned empty result | size=${outputImage.length}B | ${ctxInfo}`);
|
|
2089
4343
|
}
|
|
2090
4344
|
} catch (e) {
|
|
2091
|
-
|
|
4345
|
+
logger7.error(`[${scene}] image send failed | size=${outputImage.length}B | ${ctxInfo} | ${e}`);
|
|
2092
4346
|
try {
|
|
2093
4347
|
await session.send(fallbackText);
|
|
2094
4348
|
} catch (fallbackErr) {
|
|
2095
|
-
|
|
4349
|
+
logger7.error(`[${scene}] text fallback send failed after image send error | ${ctxInfo} | ${fallbackErr}`);
|
|
2096
4350
|
}
|
|
2097
4351
|
}
|
|
2098
4352
|
}
|
|
2099
4353
|
__name(sendImageWithFallback, "sendImageWithFallback");
|
|
2100
4354
|
|
|
2101
4355
|
// src/commands/account.ts
|
|
2102
|
-
var
|
|
4356
|
+
var logger8 = new import_koishi8.Logger("rocom-account");
|
|
2103
4357
|
async function recallMessages(session, messageIds) {
|
|
2104
4358
|
if (!session?.bot?.deleteMessage || !session.channelId) return;
|
|
2105
4359
|
for (const id of messageIds) {
|
|
@@ -2107,7 +4361,7 @@ async function recallMessages(session, messageIds) {
|
|
|
2107
4361
|
try {
|
|
2108
4362
|
await session.bot.deleteMessage(session.channelId, id);
|
|
2109
4363
|
} catch (err) {
|
|
2110
|
-
|
|
4364
|
+
logger8.warn(`撤回消息 ${id} 失败:${err}`);
|
|
2111
4365
|
}
|
|
2112
4366
|
}
|
|
2113
4367
|
}
|
|
@@ -2187,19 +4441,19 @@ async function saveBindingWithRoleInfo(deps, session, fwToken, loginType, userId
|
|
|
2187
4441
|
__name(saveBindingWithRoleInfo, "saveBindingWithRoleInfo");
|
|
2188
4442
|
function register(deps) {
|
|
2189
4443
|
const { ctx, client, userMgr } = deps;
|
|
2190
|
-
ctx.command("洛克").subcommand(".QQ登录", "QQ 扫码登录").alias("洛克QQ登录").action(async ({ session }) => {
|
|
4444
|
+
ctx.command("洛克").subcommand(".QQ登录", "QQ 扫码登录").alias("洛克QQ登录").alias("洛克qq登录").action(async ({ session }) => {
|
|
2191
4445
|
const userId = session.userId;
|
|
2192
4446
|
const qrData = await client.qqQrLogin(ctx, userId);
|
|
2193
4447
|
if (!qrData?.qr_image) return "获取 QQ 二维码失败。";
|
|
2194
4448
|
const fwToken = qrData.frameworkToken;
|
|
2195
4449
|
const qrB64 = qrData.qr_image;
|
|
2196
4450
|
const imgData = qrB64.includes(",") ? qrB64.split(",")[1] : qrB64;
|
|
2197
|
-
const qrSendResult = await session.send((0,
|
|
4451
|
+
const qrSendResult = await session.send((0, import_koishi8.h)(
|
|
2198
4452
|
"message",
|
|
2199
4453
|
{},
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
4454
|
+
import_koishi8.h.at(userId),
|
|
4455
|
+
import_koishi8.h.text("\n请使用 QQ 扫描二维码登录(有效时间 2 分钟)\n注意需要双设备扫码。\n"),
|
|
4456
|
+
import_koishi8.h.image(`data:image/png;base64,${imgData}`)
|
|
2203
4457
|
));
|
|
2204
4458
|
const qrMessageIds = Array.isArray(qrSendResult) ? qrSendResult.filter(Boolean).map(String) : qrSendResult ? [String(qrSendResult)] : [];
|
|
2205
4459
|
const startTime = Date.now();
|
|
@@ -2386,11 +4640,11 @@ ${qrUrl}`);
|
|
|
2386
4640
|
__name(register, "register");
|
|
2387
4641
|
|
|
2388
4642
|
// src/commands/query.ts
|
|
2389
|
-
var
|
|
4643
|
+
var import_koishi10 = require("koishi");
|
|
2390
4644
|
|
|
2391
4645
|
// src/subscription-send.ts
|
|
2392
|
-
var
|
|
2393
|
-
var
|
|
4646
|
+
var import_koishi9 = require("koishi");
|
|
4647
|
+
var logger9 = new import_koishi9.Logger("rocom-subscription-send");
|
|
2394
4648
|
function findBot(ctx, platform = "") {
|
|
2395
4649
|
if (!ctx.bots?.length) return null;
|
|
2396
4650
|
if (!platform) return ctx.bots[0];
|
|
@@ -2403,7 +4657,7 @@ async function sendScheduledMessage(ctx, target, message) {
|
|
|
2403
4657
|
const userId = target.userId || "";
|
|
2404
4658
|
const bot = findBot(ctx, platform);
|
|
2405
4659
|
if (!bot) {
|
|
2406
|
-
|
|
4660
|
+
logger9.warn("no available bot, skip scheduled push");
|
|
2407
4661
|
return false;
|
|
2408
4662
|
}
|
|
2409
4663
|
try {
|
|
@@ -2426,30 +4680,458 @@ async function sendScheduledMessage(ctx, target, message) {
|
|
|
2426
4680
|
return true;
|
|
2427
4681
|
}
|
|
2428
4682
|
} catch (err) {
|
|
2429
|
-
|
|
4683
|
+
logger9.warn(`scheduled push failed: ${err}`);
|
|
2430
4684
|
return false;
|
|
2431
4685
|
}
|
|
2432
|
-
|
|
4686
|
+
logger9.warn(`scheduled push target is incomplete: ${JSON.stringify(target)}`);
|
|
2433
4687
|
return false;
|
|
2434
4688
|
}
|
|
2435
4689
|
__name(sendScheduledMessage, "sendScheduledMessage");
|
|
2436
4690
|
async function sendScheduledImageWithFallback(ctx, target, image, fallbackText, mentionAll = false) {
|
|
2437
|
-
const
|
|
4691
|
+
const buildContent = /* @__PURE__ */ __name((body) => mentionAll ? (0, import_koishi9.h)("message", {}, (0, import_koishi9.h)("at", { type: "all" }), import_koishi9.h.text("\n"), body) : body, "buildContent");
|
|
2438
4692
|
if (!image) {
|
|
2439
|
-
return sendScheduledMessage(ctx, target,
|
|
4693
|
+
return sendScheduledMessage(ctx, target, buildContent(import_koishi9.h.text(fallbackText)));
|
|
2440
4694
|
}
|
|
2441
|
-
const imageSegment =
|
|
2442
|
-
const
|
|
2443
|
-
const sent = await sendScheduledMessage(ctx, target, content);
|
|
4695
|
+
const imageSegment = import_koishi9.h.image(image, detectImageMime(image));
|
|
4696
|
+
const sent = await sendScheduledMessage(ctx, target, buildContent(imageSegment));
|
|
2444
4697
|
if (sent) return true;
|
|
2445
|
-
return sendScheduledMessage(ctx, target,
|
|
4698
|
+
return sendScheduledMessage(ctx, target, buildContent(import_koishi9.h.text(fallbackText)));
|
|
2446
4699
|
}
|
|
2447
4700
|
__name(sendScheduledImageWithFallback, "sendScheduledImageWithFallback");
|
|
2448
4701
|
|
|
4702
|
+
// src/pet-data-service.ts
|
|
4703
|
+
var CN_TIME_ZONE = "Asia/Shanghai";
|
|
4704
|
+
function petDataDisplay(value, defaultText = "--") {
|
|
4705
|
+
if (value === void 0 || value === null || value === "") return defaultText;
|
|
4706
|
+
if (typeof value === "boolean") return value ? "是" : "否";
|
|
4707
|
+
return String(value);
|
|
4708
|
+
}
|
|
4709
|
+
__name(petDataDisplay, "petDataDisplay");
|
|
4710
|
+
function petDataImageUrl(petId, imageType = "image") {
|
|
4711
|
+
const assetIdRaw = parseInt(String(petId), 10);
|
|
4712
|
+
if (!Number.isFinite(assetIdRaw) || assetIdRaw <= 0) return "";
|
|
4713
|
+
const assetId = assetIdRaw < 3e3 ? assetIdRaw + 3e3 : assetIdRaw;
|
|
4714
|
+
const type = imageType === "icon" ? "icon" : "image";
|
|
4715
|
+
return `https://game.gtimg.cn/images/rocom/rocodata/jingling/${assetId}/${type}.png`;
|
|
4716
|
+
}
|
|
4717
|
+
__name(petDataImageUrl, "petDataImageUrl");
|
|
4718
|
+
function petDataWikiPetId(value) {
|
|
4719
|
+
const petId = parseInt(String(value), 10);
|
|
4720
|
+
if (!Number.isFinite(petId) || petId <= 0) return "";
|
|
4721
|
+
return String(petId < 3e3 ? petId + 3e3 : petId);
|
|
4722
|
+
}
|
|
4723
|
+
__name(petDataWikiPetId, "petDataWikiPetId");
|
|
4724
|
+
function normalizeEpochSeconds(value) {
|
|
4725
|
+
const num2 = Number(value);
|
|
4726
|
+
if (!Number.isFinite(num2) || num2 <= 0) return 0;
|
|
4727
|
+
return num2 > 1e10 ? Math.floor(num2 / 1e3) : Math.floor(num2);
|
|
4728
|
+
}
|
|
4729
|
+
__name(normalizeEpochSeconds, "normalizeEpochSeconds");
|
|
4730
|
+
function petDataTimeText(value) {
|
|
4731
|
+
const ts = normalizeEpochSeconds(value);
|
|
4732
|
+
if (!ts) return "--";
|
|
4733
|
+
const date = new Date(ts * 1e3);
|
|
4734
|
+
const formatter = new Intl.DateTimeFormat("zh-CN", {
|
|
4735
|
+
timeZone: CN_TIME_ZONE,
|
|
4736
|
+
year: "numeric",
|
|
4737
|
+
month: "2-digit",
|
|
4738
|
+
day: "2-digit",
|
|
4739
|
+
hour: "2-digit",
|
|
4740
|
+
minute: "2-digit",
|
|
4741
|
+
hour12: false
|
|
4742
|
+
});
|
|
4743
|
+
const parts = Object.fromEntries(formatter.formatToParts(date).map((p) => [p.type, p.value]));
|
|
4744
|
+
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`;
|
|
4745
|
+
}
|
|
4746
|
+
__name(petDataTimeText, "petDataTimeText");
|
|
4747
|
+
function petDataSizeText(value, unit) {
|
|
4748
|
+
const number = Number(value);
|
|
4749
|
+
if (!Number.isFinite(number)) return "--";
|
|
4750
|
+
if (unit === "g") return `${(number / 1e3).toFixed(2)} kg`;
|
|
4751
|
+
if (unit === "cm") return `${number} cm`;
|
|
4752
|
+
return `${number} ${unit}`.trim();
|
|
4753
|
+
}
|
|
4754
|
+
__name(petDataSizeText, "petDataSizeText");
|
|
4755
|
+
function petDataVoiceText(value) {
|
|
4756
|
+
if (value === void 0 || value === null || value === "") return "--";
|
|
4757
|
+
const number = Number(value);
|
|
4758
|
+
if (!Number.isFinite(number)) return petDataDisplay(value);
|
|
4759
|
+
return Number.isInteger(number) ? `${number} dB` : `${number} dB`;
|
|
4760
|
+
}
|
|
4761
|
+
__name(petDataVoiceText, "petDataVoiceText");
|
|
4762
|
+
function petDataVoiceInfo(value) {
|
|
4763
|
+
const text = petDataVoiceText(value);
|
|
4764
|
+
const number = Number(value);
|
|
4765
|
+
if (!Number.isFinite(number)) return { value: text, hint: "", className: "" };
|
|
4766
|
+
const intNumber = Math.trunc(number);
|
|
4767
|
+
if (intNumber >= 96 && intNumber <= 100) {
|
|
4768
|
+
return { value: `${text} · 婉转声`, hint: "婉转声 96~100", className: "voice-soft" };
|
|
4769
|
+
}
|
|
4770
|
+
if (intNumber >= -100 && intNumber <= -96) {
|
|
4771
|
+
return { value: `${text} · 粗嗓门`, hint: "粗嗓门 -96~-100", className: "voice-rough" };
|
|
4772
|
+
}
|
|
4773
|
+
return { value: text, hint: "婉转声 96~100 / 粗嗓门 -96~-100", className: "" };
|
|
4774
|
+
}
|
|
4775
|
+
__name(petDataVoiceInfo, "petDataVoiceInfo");
|
|
4776
|
+
function petDataKgCompact(grams) {
|
|
4777
|
+
const number = Number(grams);
|
|
4778
|
+
if (!Number.isFinite(number)) return "--";
|
|
4779
|
+
const text = (number / 1e3).toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
|
|
4780
|
+
return `${text}kg`;
|
|
4781
|
+
}
|
|
4782
|
+
__name(petDataKgCompact, "petDataKgCompact");
|
|
4783
|
+
function petDataWeightSizeInfo(pet, wikiPet) {
|
|
4784
|
+
const bodySize = wikiPet && typeof wikiPet === "object" ? wikiPet.body_size : null;
|
|
4785
|
+
const weightRange = bodySize && typeof bodySize === "object" ? bodySize.weight : null;
|
|
4786
|
+
if (!weightRange || typeof weightRange !== "object") return {};
|
|
4787
|
+
const current = Number(pet?.weight);
|
|
4788
|
+
const low = Number(weightRange.min_g);
|
|
4789
|
+
const high = Number(weightRange.max_g);
|
|
4790
|
+
if (!Number.isFinite(current) || !Number.isFinite(low) || !Number.isFinite(high)) return {};
|
|
4791
|
+
if (high <= low) return {};
|
|
4792
|
+
const span = high - low;
|
|
4793
|
+
const smallCut = low + span * 0.05;
|
|
4794
|
+
const largeCut = high - span * 0.05;
|
|
4795
|
+
const rangeText = `${petDataKgCompact(low)}-${petDataKgCompact(high)}`;
|
|
4796
|
+
if (current <= smallCut) {
|
|
4797
|
+
return { label: "小块头", className: "size-small", hint: `小块头 · ≤${petDataKgCompact(smallCut)} · 范围 ${rangeText}` };
|
|
4798
|
+
}
|
|
4799
|
+
if (current >= largeCut) {
|
|
4800
|
+
return { label: "大块头", className: "size-large", hint: `大块头 · ≥${petDataKgCompact(largeCut)} · 范围 ${rangeText}` };
|
|
4801
|
+
}
|
|
4802
|
+
return { label: "", className: "", hint: `范围 ${rangeText}` };
|
|
4803
|
+
}
|
|
4804
|
+
__name(petDataWeightSizeInfo, "petDataWeightSizeInfo");
|
|
4805
|
+
function petDataOptionMaps(options) {
|
|
4806
|
+
const petOptions = options && typeof options === "object" && options.pet && typeof options.pet === "object" ? options.pet : {};
|
|
4807
|
+
const buildMap = /* @__PURE__ */ __name((key, preferShort = false) => {
|
|
4808
|
+
const result = {};
|
|
4809
|
+
const values = petOptions[key];
|
|
4810
|
+
if (!Array.isArray(values)) return result;
|
|
4811
|
+
for (const item of values) {
|
|
4812
|
+
if (!item || typeof item !== "object") continue;
|
|
4813
|
+
const itemId = item.id;
|
|
4814
|
+
if (itemId === void 0 || itemId === null || itemId === "") continue;
|
|
4815
|
+
const name2 = preferShort && item.short_name ? item.short_name : item.name;
|
|
4816
|
+
if (name2) result[String(itemId)] = String(name2);
|
|
4817
|
+
}
|
|
4818
|
+
return result;
|
|
4819
|
+
}, "buildMap");
|
|
4820
|
+
const natures = {};
|
|
4821
|
+
for (const item of petOptions.natures || []) {
|
|
4822
|
+
if (!item || typeof item !== "object" || item.id === void 0 || item.id === null || item.id === "") continue;
|
|
4823
|
+
const name2 = String(item.name || item.id);
|
|
4824
|
+
const summary = String(item.summary || "").trim();
|
|
4825
|
+
natures[String(item.id)] = summary ? `${name2}(${summary})` : name2;
|
|
4826
|
+
}
|
|
4827
|
+
return {
|
|
4828
|
+
natures,
|
|
4829
|
+
bloodlines: buildMap("bloodlines", true),
|
|
4830
|
+
types: buildMap("types"),
|
|
4831
|
+
talent_ratings: buildMap("talent_ratings")
|
|
4832
|
+
};
|
|
4833
|
+
}
|
|
4834
|
+
__name(petDataOptionMaps, "petDataOptionMaps");
|
|
4835
|
+
function petDataLookup(mapping, value, defaultText = "--") {
|
|
4836
|
+
const key = String(value ?? "").trim();
|
|
4837
|
+
if (!key) return defaultText;
|
|
4838
|
+
return mapping[key] || key;
|
|
4839
|
+
}
|
|
4840
|
+
__name(petDataLookup, "petDataLookup");
|
|
4841
|
+
function petDataVariant(pet, fallback = {}) {
|
|
4842
|
+
fallback = fallback || {};
|
|
4843
|
+
const mutationName = String(
|
|
4844
|
+
pet?.mutation_name || fallback?.mutation_name || fallback?.pet_mutation_name || ""
|
|
4845
|
+
).trim();
|
|
4846
|
+
const mutationType = String(pet?.mutation_type ?? fallback?.mutation_type ?? "").trim();
|
|
4847
|
+
const specialityValues = [];
|
|
4848
|
+
for (const value of [pet?.real_speciality_ids, fallback?.real_speciality_ids, pet?.speciality_id, fallback?.speciality_id]) {
|
|
4849
|
+
if (Array.isArray(value)) specialityValues.push(...value);
|
|
4850
|
+
else if (value !== void 0 && value !== null && value !== "") specialityValues.push(value);
|
|
4851
|
+
}
|
|
4852
|
+
const specialityIds = new Set(specialityValues.map((value) => String(value).trim()).filter(Boolean));
|
|
4853
|
+
if (mutationType === "9" || mutationName.includes("异色") && mutationName.includes("炫彩") || specialityIds.has("103") && specialityIds.has("502")) {
|
|
4854
|
+
return { variantText: "异色炫彩", variantIcon: "异色炫彩.png" };
|
|
4855
|
+
}
|
|
4856
|
+
if (mutationType === "1" || mutationName.includes("异色") || specialityIds.has("103")) {
|
|
4857
|
+
return { variantText: "异色", variantIcon: "异色.png" };
|
|
4858
|
+
}
|
|
4859
|
+
if (mutationType === "8" || mutationName.includes("炫彩") || specialityIds.has("502")) {
|
|
4860
|
+
return { variantText: "炫彩", variantIcon: "炫彩.png" };
|
|
4861
|
+
}
|
|
4862
|
+
return { variantText: mutationName || "普通", variantIcon: "" };
|
|
4863
|
+
}
|
|
4864
|
+
__name(petDataVariant, "petDataVariant");
|
|
4865
|
+
function petDataAttributes(pet) {
|
|
4866
|
+
const attributeInfo = pet?.attribute_info && typeof pet.attribute_info === "object" ? pet.attribute_info : {};
|
|
4867
|
+
const fields = [
|
|
4868
|
+
["hp", "生命"],
|
|
4869
|
+
["attack", "物攻"],
|
|
4870
|
+
["special_attack", "魔攻"],
|
|
4871
|
+
["defense", "物防"],
|
|
4872
|
+
["special_defense", "魔防"],
|
|
4873
|
+
["speed", "速度"]
|
|
4874
|
+
];
|
|
4875
|
+
return fields.map(([key, label]) => {
|
|
4876
|
+
const raw = attributeInfo[key] && typeof attributeInfo[key] === "object" ? attributeInfo[key] : {};
|
|
4877
|
+
let percent = 6;
|
|
4878
|
+
const base = Number(raw.base_value);
|
|
4879
|
+
if (Number.isFinite(base)) percent = Math.trunc(Math.max(6, Math.min(100, base / 200 * 100)));
|
|
4880
|
+
return {
|
|
4881
|
+
label,
|
|
4882
|
+
value: petDataDisplay(raw.base_value),
|
|
4883
|
+
race: petDataDisplay(raw.total_race),
|
|
4884
|
+
talent: petDataDisplay(raw.talent),
|
|
4885
|
+
percent
|
|
4886
|
+
};
|
|
4887
|
+
});
|
|
4888
|
+
}
|
|
4889
|
+
__name(petDataAttributes, "petDataAttributes");
|
|
4890
|
+
function petDataSkillItems(pet) {
|
|
4891
|
+
const skillRoot = pet?.skill && typeof pet.skill === "object" ? pet.skill : {};
|
|
4892
|
+
const skills = Array.isArray(skillRoot.skill_data) ? skillRoot.skill_data : [];
|
|
4893
|
+
const sortKey = /* @__PURE__ */ __name((item) => {
|
|
4894
|
+
const equippedRank = item?.is_equipped ? 0 : 1;
|
|
4895
|
+
const learnedRank = item?.is_learned ? 0 : 1;
|
|
4896
|
+
let pos = parseInt(String(item?.pos ?? 99), 10);
|
|
4897
|
+
if (!Number.isFinite(pos)) pos = 99;
|
|
4898
|
+
return [equippedRank, learnedRank, pos];
|
|
4899
|
+
}, "sortKey");
|
|
4900
|
+
return skills.filter((s) => s && typeof s === "object").sort((a, b) => {
|
|
4901
|
+
const ka = sortKey(a);
|
|
4902
|
+
const kb = sortKey(b);
|
|
4903
|
+
return ka[0] - kb[0] || ka[1] - kb[1] || ka[2] - kb[2];
|
|
4904
|
+
});
|
|
4905
|
+
}
|
|
4906
|
+
__name(petDataSkillItems, "petDataSkillItems");
|
|
4907
|
+
function extractRawItems(payload) {
|
|
4908
|
+
const rawItems = [];
|
|
4909
|
+
if (Array.isArray(payload?.npc_pets)) {
|
|
4910
|
+
rawItems.push(...payload.npc_pets.filter((item) => item && typeof item === "object"));
|
|
4911
|
+
} else if (payload?.npc_pet && typeof payload.npc_pet === "object") {
|
|
4912
|
+
const query = payload?.query && typeof payload.query === "object" ? payload.query : {};
|
|
4913
|
+
rawItems.push({
|
|
4914
|
+
status: "ok",
|
|
4915
|
+
npc_pet: payload.npc_pet,
|
|
4916
|
+
pet_gid: query.pet_gid,
|
|
4917
|
+
furniture_guid: query.npc_id || query.furniture_guid,
|
|
4918
|
+
npc_id_source: "query"
|
|
4919
|
+
});
|
|
4920
|
+
}
|
|
4921
|
+
return rawItems;
|
|
4922
|
+
}
|
|
4923
|
+
__name(extractRawItems, "extractRawItems");
|
|
4924
|
+
function petDataSkillIdsFromPayload(payload) {
|
|
4925
|
+
let root = payload || {};
|
|
4926
|
+
if (root.result && typeof root.result === "object") root = root.result;
|
|
4927
|
+
const ids = [];
|
|
4928
|
+
for (const raw of extractRawItems(root)) {
|
|
4929
|
+
const npcPet = raw?.npc_pet && typeof raw.npc_pet === "object" ? raw.npc_pet : {};
|
|
4930
|
+
const pet = npcPet?.pet && typeof npcPet.pet === "object" ? npcPet.pet : {};
|
|
4931
|
+
for (const item of petDataSkillItems(pet).slice(0, 8)) {
|
|
4932
|
+
const skillId = String(item?.id ?? "").trim();
|
|
4933
|
+
if (skillId && !ids.includes(skillId)) ids.push(skillId);
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
return ids;
|
|
4937
|
+
}
|
|
4938
|
+
__name(petDataSkillIdsFromPayload, "petDataSkillIdsFromPayload");
|
|
4939
|
+
function petDataPetIdsFromPayload(payload) {
|
|
4940
|
+
let root = payload || {};
|
|
4941
|
+
if (root.result && typeof root.result === "object") root = root.result;
|
|
4942
|
+
const ids = [];
|
|
4943
|
+
for (const raw of extractRawItems(root)) {
|
|
4944
|
+
const npcPet = raw?.npc_pet && typeof raw.npc_pet === "object" ? raw.npc_pet : {};
|
|
4945
|
+
const pet = npcPet?.pet && typeof npcPet.pet === "object" ? npcPet.pet : {};
|
|
4946
|
+
const petId = petDataWikiPetId(pet.base_conf_id || raw.pet_cfg_id || pet.catch_base_id || pet.conf_id);
|
|
4947
|
+
if (petId && !ids.includes(petId)) ids.push(petId);
|
|
4948
|
+
}
|
|
4949
|
+
return ids;
|
|
4950
|
+
}
|
|
4951
|
+
__name(petDataPetIdsFromPayload, "petDataPetIdsFromPayload");
|
|
4952
|
+
function petDataSkillIconUrl(baseUrl, skillId, detail = null) {
|
|
4953
|
+
const icon = String(detail?.icon || "").trim();
|
|
4954
|
+
if (icon) return icon;
|
|
4955
|
+
const skillText = String(skillId ?? "").trim();
|
|
4956
|
+
if (!skillText) return "";
|
|
4957
|
+
return `${baseUrl}/api/v1/resources/wiki/assets/skills/${skillText}.png`;
|
|
4958
|
+
}
|
|
4959
|
+
__name(petDataSkillIconUrl, "petDataSkillIconUrl");
|
|
4960
|
+
function wikiNamedValue2(value) {
|
|
4961
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
4962
|
+
return String(value.name || value.label || value.value || "");
|
|
4963
|
+
}
|
|
4964
|
+
return String(value ?? "");
|
|
4965
|
+
}
|
|
4966
|
+
__name(wikiNamedValue2, "wikiNamedValue");
|
|
4967
|
+
function petDataSkills(pet, skillLookup = {}, options = {}) {
|
|
4968
|
+
const loadSkillIcons = options.loadSkillIcons !== false;
|
|
4969
|
+
const baseUrl = options.baseUrl || "";
|
|
4970
|
+
const result = [];
|
|
4971
|
+
for (const item of petDataSkillItems(pet).slice(0, 8)) {
|
|
4972
|
+
const skillId = petDataDisplay(item?.id);
|
|
4973
|
+
const detail = skillId !== "--" ? skillLookup[String(skillId)] : null;
|
|
4974
|
+
let status;
|
|
4975
|
+
let statusClass;
|
|
4976
|
+
if (item?.is_equipped) {
|
|
4977
|
+
status = "已装备";
|
|
4978
|
+
statusClass = "equipped";
|
|
4979
|
+
} else if (item?.is_learned) {
|
|
4980
|
+
status = "已学会";
|
|
4981
|
+
statusClass = "learned";
|
|
4982
|
+
} else {
|
|
4983
|
+
status = "未学会";
|
|
4984
|
+
statusClass = "locked";
|
|
4985
|
+
}
|
|
4986
|
+
const element = detail ? wikiNamedValue2(detail.element_type) : "";
|
|
4987
|
+
const skillType = detail ? wikiNamedValue2(detail.skill_type || detail.damage_type) : "";
|
|
4988
|
+
result.push({
|
|
4989
|
+
id: skillId,
|
|
4990
|
+
name: String(detail?.name || skillId),
|
|
4991
|
+
icon: loadSkillIcons ? petDataSkillIconUrl(baseUrl, skillId, detail) : "",
|
|
4992
|
+
element: element || "未知",
|
|
4993
|
+
type: skillType || "技能",
|
|
4994
|
+
power: petDataDisplay(detail?.power),
|
|
4995
|
+
cost: petDataDisplay(detail?.cost),
|
|
4996
|
+
pos: petDataDisplay(item?.pos),
|
|
4997
|
+
status,
|
|
4998
|
+
statusClass,
|
|
4999
|
+
unlock: item?.unlock_need_lv !== void 0 && item?.unlock_need_lv !== null && item?.unlock_need_lv !== "" && item?.unlock_need_lv !== 0 ? `Lv.${item.unlock_need_lv}` : "--",
|
|
5000
|
+
description: String(detail?.description || detail?.desc || "")
|
|
5001
|
+
});
|
|
5002
|
+
}
|
|
5003
|
+
return result;
|
|
5004
|
+
}
|
|
5005
|
+
__name(petDataSkills, "petDataSkills");
|
|
5006
|
+
function petDataCardItems(pet, optionMaps, sizeInfo = {}) {
|
|
5007
|
+
const genderMap = { "0": "未知", "1": "雄性", "2": "雌性" };
|
|
5008
|
+
const gender = genderMap[String(pet?.gender ?? "")] || petDataDisplay(pet?.gender);
|
|
5009
|
+
const nature = petDataLookup(optionMaps.natures, pet?.nature);
|
|
5010
|
+
const blood = petDataLookup(optionMaps.bloodlines, pet?.blood_id);
|
|
5011
|
+
const talentRank = petDataLookup(optionMaps.talent_ratings, pet?.talent_rank, petDataDisplay(pet?.talent_rank));
|
|
5012
|
+
const voiceInfo = petDataVoiceInfo(pet?.voice);
|
|
5013
|
+
let weightValue = petDataSizeText(pet?.weight, "g");
|
|
5014
|
+
if (sizeInfo.label) weightValue = `${weightValue} · ${sizeInfo.label}`;
|
|
5015
|
+
return [
|
|
5016
|
+
{ label: "等级", value: petDataDisplay(pet?.level) },
|
|
5017
|
+
{ label: "性别", value: gender },
|
|
5018
|
+
{ label: "分贝", ...voiceInfo },
|
|
5019
|
+
{ label: "性格", value: nature },
|
|
5020
|
+
{ label: "血脉", value: blood },
|
|
5021
|
+
{ label: "天赋评级", value: talentRank },
|
|
5022
|
+
{ label: "身高", value: petDataSizeText(pet?.height, "cm") },
|
|
5023
|
+
{
|
|
5024
|
+
label: "体重",
|
|
5025
|
+
value: weightValue,
|
|
5026
|
+
hint: sizeInfo.hint || "",
|
|
5027
|
+
className: sizeInfo.className || ""
|
|
5028
|
+
}
|
|
5029
|
+
];
|
|
5030
|
+
}
|
|
5031
|
+
__name(petDataCardItems, "petDataCardItems");
|
|
5032
|
+
function petDataExtractItems(payload, optionMaps, skillLookup = {}, sizeLookup = {}, options = {}) {
|
|
5033
|
+
const rawItems = extractRawItems(payload);
|
|
5034
|
+
const pets = [];
|
|
5035
|
+
rawItems.forEach((raw, index) => {
|
|
5036
|
+
const npcPet = raw?.npc_pet && typeof raw.npc_pet === "object" ? raw.npc_pet : {};
|
|
5037
|
+
const pet = npcPet?.pet && typeof npcPet.pet === "object" ? npcPet.pet : {};
|
|
5038
|
+
const status = String(raw?.status || (Object.keys(pet).length ? "ok" : "unknown"));
|
|
5039
|
+
const { variantText, variantIcon } = petDataVariant(pet, raw);
|
|
5040
|
+
const baseId = pet.base_conf_id || raw.pet_cfg_id || pet.catch_base_id || pet.conf_id;
|
|
5041
|
+
const wikiPetId = petDataWikiPetId(baseId);
|
|
5042
|
+
const sizeInfo = petDataWeightSizeInfo(pet, sizeLookup[wikiPetId]);
|
|
5043
|
+
const name2 = pet.name || pet.pet_default_name || raw.pet_default_name || raw.name || `精灵 ${baseId || index + 1}`;
|
|
5044
|
+
const defaultName = pet.pet_default_name || raw.pet_default_name || "";
|
|
5045
|
+
const displayDefault = defaultName && defaultName !== name2 ? defaultName : "";
|
|
5046
|
+
const petGid = pet.gid || raw.pet_gid;
|
|
5047
|
+
const sceneInfo = pet.scene_info && typeof pet.scene_info === "object" ? pet.scene_info : null;
|
|
5048
|
+
const furnitureGuid = raw.furniture_guid ?? (sceneInfo ? sceneInfo.npc_id : void 0);
|
|
5049
|
+
pets.push({
|
|
5050
|
+
index: index + 1,
|
|
5051
|
+
status,
|
|
5052
|
+
statusText: status === "ok" ? "成功" : status,
|
|
5053
|
+
retCode: petDataDisplay(npcPet.ret_code),
|
|
5054
|
+
name: String(name2),
|
|
5055
|
+
defaultName: String(displayDefault),
|
|
5056
|
+
level: petDataDisplay(pet.level),
|
|
5057
|
+
baseId: petDataDisplay(baseId),
|
|
5058
|
+
confId: petDataDisplay(pet.conf_id),
|
|
5059
|
+
petGid: petDataDisplay(petGid),
|
|
5060
|
+
npcId: petDataDisplay(furnitureGuid),
|
|
5061
|
+
npcIdSource: petDataDisplay(raw.npc_id_source),
|
|
5062
|
+
imageUrl: petDataImageUrl(baseId, "image"),
|
|
5063
|
+
iconUrl: petDataImageUrl(baseId, "icon"),
|
|
5064
|
+
variantText,
|
|
5065
|
+
variantIcon,
|
|
5066
|
+
voiceText: petDataVoiceText(pet.voice),
|
|
5067
|
+
cards: petDataCardItems(pet, optionMaps, sizeInfo),
|
|
5068
|
+
attributes: petDataAttributes(pet),
|
|
5069
|
+
skills: petDataSkills(pet, skillLookup, options),
|
|
5070
|
+
catchItems: [
|
|
5071
|
+
{ label: "捕捉等级", value: petDataDisplay(pet.catch_lv) },
|
|
5072
|
+
{ label: "捕捉方式", value: petDataDisplay(pet.catch_way) },
|
|
5073
|
+
{ label: "捕捉营地", value: petDataDisplay(pet.caught_camp) },
|
|
5074
|
+
{ label: "获得时间", value: petDataTimeText(pet.add_time) }
|
|
5075
|
+
],
|
|
5076
|
+
specialityIds: (Array.isArray(pet.real_speciality_ids) ? pet.real_speciality_ids : []).map((x) => String(x)).join(" / ") || petDataDisplay(pet.speciality_id),
|
|
5077
|
+
relationshipType: petDataDisplay(npcPet.relationship_type),
|
|
5078
|
+
errorText: petDataDisplay(raw.error || raw.message || npcPet.error_message, "")
|
|
5079
|
+
});
|
|
5080
|
+
});
|
|
5081
|
+
return pets;
|
|
5082
|
+
}
|
|
5083
|
+
__name(petDataExtractItems, "petDataExtractItems");
|
|
5084
|
+
function buildPetDataRenderData(res, uid, options = {}) {
|
|
5085
|
+
let payload = res || {};
|
|
5086
|
+
if (payload.result && typeof payload.result === "object") payload = payload.result;
|
|
5087
|
+
const optionMaps = petDataOptionMaps(options.optionsPayload);
|
|
5088
|
+
let playerInfo = payload.player_info && typeof payload.player_info === "object" ? payload.player_info : {};
|
|
5089
|
+
if (!Object.keys(playerInfo).length && payload.npc_pet && typeof payload.npc_pet === "object") {
|
|
5090
|
+
playerInfo = payload.npc_pet.player_info && typeof payload.npc_pet.player_info === "object" ? payload.npc_pet.player_info : {};
|
|
5091
|
+
}
|
|
5092
|
+
const pets = petDataExtractItems(payload, optionMaps, options.skillLookup || {}, options.sizeLookup || {}, {
|
|
5093
|
+
loadSkillIcons: !options.lowBandwidthMode,
|
|
5094
|
+
baseUrl: options.baseUrl || ""
|
|
5095
|
+
});
|
|
5096
|
+
const meta = payload.meta && typeof payload.meta === "object" ? payload.meta : {};
|
|
5097
|
+
const finishedAt = normalizeEpochSeconds(meta.finished_at || meta.created_at);
|
|
5098
|
+
const updatedAt = petDataTimeText(finishedAt || Math.floor(Date.now() / 1e3));
|
|
5099
|
+
const online = playerInfo.online;
|
|
5100
|
+
const onlineText = online === true ? "在线" : online === false ? "离线" : "未知";
|
|
5101
|
+
const okCount = payload.npc_pet_ok_count;
|
|
5102
|
+
const errorCount = payload.npc_pet_error_count;
|
|
5103
|
+
const skippedCount = payload.npc_pet_skipped_count;
|
|
5104
|
+
return {
|
|
5105
|
+
title: "家园详情",
|
|
5106
|
+
subtitle: "Ingame Pet Data",
|
|
5107
|
+
uid: petDataDisplay(payload.uin || playerInfo.uin || uid),
|
|
5108
|
+
playerName: petDataDisplay(playerInfo.name, "未知玩家"),
|
|
5109
|
+
playerLevel: petDataDisplay(playerInfo.level),
|
|
5110
|
+
worldLevel: petDataDisplay(playerInfo.world_level),
|
|
5111
|
+
onlineText,
|
|
5112
|
+
isOnline: online === true,
|
|
5113
|
+
queryMode: options.singleQuery ? "单只精灵" : "家园批量",
|
|
5114
|
+
lowBandwidthMode: Boolean(options.lowBandwidthMode),
|
|
5115
|
+
summaryCards: [
|
|
5116
|
+
{ label: "目标状态", value: onlineText },
|
|
5117
|
+
{ label: "返回精灵", value: String(pets.length) },
|
|
5118
|
+
{ label: "成功/失败", value: `${petDataDisplay(okCount, String(pets.length))} / ${petDataDisplay(errorCount, "0")}` },
|
|
5119
|
+
{ label: "跳过", value: petDataDisplay(skippedCount, "0") }
|
|
5120
|
+
],
|
|
5121
|
+
pets,
|
|
5122
|
+
updatedAt,
|
|
5123
|
+
notice: "该接口依赖目标玩家在线且家园可访问;离线或隐私/上游不可达时可能失败。",
|
|
5124
|
+
emptyText: "未获取到家园精灵完整数据。请确认目标玩家在线、家园可访问,或稍后重试。",
|
|
5125
|
+
commandHint: "💡 洛克.家园详情 <UID> | 洛克.家园详情 <UID> <pet_gid> <npc_id>",
|
|
5126
|
+
copyright: "Koishi & WeGame 洛克王国插件"
|
|
5127
|
+
};
|
|
5128
|
+
}
|
|
5129
|
+
__name(buildPetDataRenderData, "buildPetDataRenderData");
|
|
5130
|
+
|
|
2449
5131
|
// src/commands/query.ts
|
|
2450
|
-
var
|
|
2451
|
-
var
|
|
2452
|
-
var
|
|
5132
|
+
var import_node_fs5 = __toESM(require("node:fs"));
|
|
5133
|
+
var import_node_path5 = __toESM(require("node:path"));
|
|
5134
|
+
var logger10 = new import_koishi10.Logger("rocom-query");
|
|
2453
5135
|
async function sendImage(deps, session, templateName, data, fallback) {
|
|
2454
5136
|
const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
|
|
2455
5137
|
await sendImageWithFallback(session, png, fallback, `query:${templateName}`, deps.config);
|
|
@@ -2659,14 +5341,14 @@ function stringifyInspectValue(value) {
|
|
|
2659
5341
|
return String(value);
|
|
2660
5342
|
}
|
|
2661
5343
|
__name(stringifyInspectValue, "stringifyInspectValue");
|
|
2662
|
-
function
|
|
5344
|
+
function normalizeEpochSeconds2(value) {
|
|
2663
5345
|
const ts = Number(value);
|
|
2664
5346
|
if (!Number.isFinite(ts)) return 0;
|
|
2665
5347
|
if (ts > 1e13) return Math.floor(ts / 1e6);
|
|
2666
5348
|
if (ts > 1e10) return Math.floor(ts / 1e3);
|
|
2667
5349
|
return Math.floor(ts);
|
|
2668
5350
|
}
|
|
2669
|
-
__name(
|
|
5351
|
+
__name(normalizeEpochSeconds2, "normalizeEpochSeconds");
|
|
2670
5352
|
function formatHomeRemaining(targetTs, nowTs = Math.floor(Date.now() / 1e3)) {
|
|
2671
5353
|
if (!targetTs) return "未开始";
|
|
2672
5354
|
if (nowTs >= targetTs) return "已完成";
|
|
@@ -2730,12 +5412,12 @@ var PANEL_PET_STAT_DEFS = [
|
|
|
2730
5412
|
{ key: "special_defense", label: "魔防", addiType: 5, aliases: ["special_defense", "magic_defense", "sp_defense"] },
|
|
2731
5413
|
{ key: "speed", label: "速度", addiType: 6, aliases: ["speed", "spd"] }
|
|
2732
5414
|
];
|
|
2733
|
-
var PANEL_PET_CACHE_PATH =
|
|
5415
|
+
var PANEL_PET_CACHE_PATH = import_node_path5.default.join("data", "rocom", "rocom_pet_panel_cache.json");
|
|
2734
5416
|
var PANEL_EMPTY_FEATURE = { id: "", name: "暂无特性", desc: "当前接口未返回特性描述。" };
|
|
2735
5417
|
var PANEL_EMPTY_SKILL_HINT = "当前接口未返回技能详情。";
|
|
2736
5418
|
var panelPetCacheMem = null;
|
|
2737
5419
|
function panelPetCacheFilePath(deps) {
|
|
2738
|
-
return
|
|
5420
|
+
return import_node_path5.default.join(deps.ctx.baseDir, PANEL_PET_CACHE_PATH);
|
|
2739
5421
|
}
|
|
2740
5422
|
__name(panelPetCacheFilePath, "panelPetCacheFilePath");
|
|
2741
5423
|
function ensureBloodlineLabel(raw, mutationLabel) {
|
|
@@ -2747,15 +5429,15 @@ function loadPanelPetCache(deps) {
|
|
|
2747
5429
|
if (panelPetCacheMem) return panelPetCacheMem;
|
|
2748
5430
|
const filePath = panelPetCacheFilePath(deps);
|
|
2749
5431
|
try {
|
|
2750
|
-
if (!
|
|
5432
|
+
if (!import_node_fs5.default.existsSync(filePath)) {
|
|
2751
5433
|
panelPetCacheMem = {};
|
|
2752
5434
|
return panelPetCacheMem;
|
|
2753
5435
|
}
|
|
2754
|
-
const text =
|
|
5436
|
+
const text = import_node_fs5.default.readFileSync(filePath, "utf-8");
|
|
2755
5437
|
const parsed = JSON.parse(text);
|
|
2756
5438
|
panelPetCacheMem = parsed && typeof parsed === "object" ? parsed : {};
|
|
2757
5439
|
} catch (err) {
|
|
2758
|
-
|
|
5440
|
+
logger10.warn(`加载精灵面板缓存失败: ${err}`);
|
|
2759
5441
|
panelPetCacheMem = {};
|
|
2760
5442
|
}
|
|
2761
5443
|
return panelPetCacheMem;
|
|
@@ -2763,11 +5445,11 @@ function loadPanelPetCache(deps) {
|
|
|
2763
5445
|
__name(loadPanelPetCache, "loadPanelPetCache");
|
|
2764
5446
|
function savePanelPetCache(deps, data) {
|
|
2765
5447
|
const filePath = panelPetCacheFilePath(deps);
|
|
2766
|
-
const dir =
|
|
2767
|
-
|
|
5448
|
+
const dir = import_node_path5.default.dirname(filePath);
|
|
5449
|
+
import_node_fs5.default.mkdirSync(dir, { recursive: true });
|
|
2768
5450
|
const tempPath = `${filePath}.tmp`;
|
|
2769
|
-
|
|
2770
|
-
|
|
5451
|
+
import_node_fs5.default.writeFileSync(tempPath, JSON.stringify(data, null, 2), "utf-8");
|
|
5452
|
+
import_node_fs5.default.renameSync(tempPath, filePath);
|
|
2771
5453
|
panelPetCacheMem = data;
|
|
2772
5454
|
}
|
|
2773
5455
|
__name(savePanelPetCache, "savePanelPetCache");
|
|
@@ -3224,7 +5906,7 @@ function panelPetDetailData(uid, pet, updatedAtText) {
|
|
|
3224
5906
|
}
|
|
3225
5907
|
__name(panelPetDetailData, "panelPetDetailData");
|
|
3226
5908
|
async function refreshPanelPets(deps, uid, userId = "") {
|
|
3227
|
-
const res = await deps.client.ingameHomeInfo(deps.ctx, uid, 2e4);
|
|
5909
|
+
const res = await deps.client.ingameHomeInfo(deps.ctx, uid, { waitMs: 2e4, timeoutMs: 3e4 });
|
|
3228
5910
|
if (res) {
|
|
3229
5911
|
const homeInfo = homeInfoPayload(res);
|
|
3230
5912
|
const pets = buildPanelPetList(homeInfo);
|
|
@@ -3256,7 +5938,7 @@ function formatPanelUpdatedAt(ts) {
|
|
|
3256
5938
|
return new Date(value * 1e3).toLocaleString("zh-CN");
|
|
3257
5939
|
}
|
|
3258
5940
|
__name(formatPanelUpdatedAt, "formatPanelUpdatedAt");
|
|
3259
|
-
function extractHomePet(raw, index, guard = false) {
|
|
5941
|
+
function extractHomePet(deps, raw, index, guard = false) {
|
|
3260
5942
|
if (!raw || typeof raw !== "object") return null;
|
|
3261
5943
|
const homePet = raw.home_pet_info && typeof raw.home_pet_info === "object" ? raw.home_pet_info : raw;
|
|
3262
5944
|
const display = raw.display_info && typeof raw.display_info === "object" ? raw.display_info : {};
|
|
@@ -3264,10 +5946,22 @@ function extractHomePet(raw, index, guard = false) {
|
|
|
3264
5946
|
if (["", "0"].includes(String(petId || "0")) && !guard) return null;
|
|
3265
5947
|
const nowTs = Math.floor(Date.now() / 1e3);
|
|
3266
5948
|
const hasEgg = Boolean(raw.have_egg);
|
|
3267
|
-
const predictedEggTime =
|
|
5949
|
+
const predictedEggTime = normalizeEpochSeconds2(raw.predicted_egg_time);
|
|
3268
5950
|
const eggReady = hasEgg || predictedEggTime > 0 && nowTs >= predictedEggTime;
|
|
3269
5951
|
const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
|
|
3270
5952
|
const gender = Number(display.gender || raw.gender || 0) || 0;
|
|
5953
|
+
let mutationType = Number(display.mutation_type || raw.mutation_type || homePet.mutation_type || 0) || 0;
|
|
5954
|
+
if (!mutationType) {
|
|
5955
|
+
const specialityValues = [];
|
|
5956
|
+
for (const value of [homePet.real_speciality_ids, raw.real_speciality_ids, homePet.speciality_id, raw.speciality_id]) {
|
|
5957
|
+
if (Array.isArray(value)) specialityValues.push(...value);
|
|
5958
|
+
else if (value !== void 0 && value !== null && value !== "") specialityValues.push(value);
|
|
5959
|
+
}
|
|
5960
|
+
const specialityIds = new Set(specialityValues.map((value) => String(value).trim()).filter(Boolean));
|
|
5961
|
+
if (specialityIds.has("103") && specialityIds.has("502")) mutationType = 9;
|
|
5962
|
+
else if (specialityIds.has("103")) mutationType = 1;
|
|
5963
|
+
else if (specialityIds.has("502")) mutationType = 8;
|
|
5964
|
+
}
|
|
3271
5965
|
const isMale = gender === 1;
|
|
3272
5966
|
const status = homePet.status ?? raw.status;
|
|
3273
5967
|
const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ["2", "guard", "守卫"].includes(String(status).toLowerCase());
|
|
@@ -3295,11 +5989,16 @@ function extractHomePet(raw, index, guard = false) {
|
|
|
3295
5989
|
name: String(homePet.name || homePet.pet_name || raw.name || raw.pet_name || `精灵 ${petId || ""}`),
|
|
3296
5990
|
level: display.level || raw.level || homePet.level || "--",
|
|
3297
5991
|
iconUrl: homePetIcon(petId, raw.icon_url || raw.pet_img_url || raw.petIcon || ""),
|
|
5992
|
+
starIconUrl: [1, 8, 9].includes(mutationType) ? deps.renderer.resourceUrl(`render-templates/home/img/rocomuid/star_${mutationType}.png`) : "",
|
|
3298
5993
|
badge: isGuard ? "守" : hasEgg ? "蛋" : "",
|
|
5994
|
+
mutationType,
|
|
3299
5995
|
isGuard,
|
|
3300
5996
|
statusText,
|
|
3301
5997
|
statusClass,
|
|
3302
5998
|
note,
|
|
5999
|
+
hasEgg,
|
|
6000
|
+
eggReady,
|
|
6001
|
+
eggTime: predictedEggTime || 0,
|
|
3303
6002
|
inspireReady: eggReady,
|
|
3304
6003
|
readyAt: predictedEggTime || 0,
|
|
3305
6004
|
gender
|
|
@@ -3339,23 +6038,23 @@ function homePlantIcon(deps, iconId) {
|
|
|
3339
6038
|
}
|
|
3340
6039
|
__name(homePlantIcon, "homePlantIcon");
|
|
3341
6040
|
var homePlantMapCache = null;
|
|
3342
|
-
var HOME_PLANT_MAP_RELATIVE_PATH =
|
|
6041
|
+
var HOME_PLANT_MAP_RELATIVE_PATH = import_node_path5.default.join("render-templates", "home", "data", "home_item_list.json");
|
|
3343
6042
|
function resolveHomePlantMapPath() {
|
|
3344
6043
|
const candidates = [
|
|
3345
|
-
|
|
3346
|
-
|
|
6044
|
+
import_node_path5.default.resolve(__dirname, "..", HOME_PLANT_MAP_RELATIVE_PATH),
|
|
6045
|
+
import_node_path5.default.resolve(__dirname, HOME_PLANT_MAP_RELATIVE_PATH)
|
|
3347
6046
|
];
|
|
3348
|
-
return candidates.find((candidate) =>
|
|
6047
|
+
return candidates.find((candidate) => import_node_fs5.default.existsSync(candidate)) || candidates[0];
|
|
3349
6048
|
}
|
|
3350
6049
|
__name(resolveHomePlantMapPath, "resolveHomePlantMapPath");
|
|
3351
6050
|
function loadHomePlantMap() {
|
|
3352
6051
|
if (homePlantMapCache) return homePlantMapCache;
|
|
3353
6052
|
const filePath = resolveHomePlantMapPath();
|
|
3354
6053
|
try {
|
|
3355
|
-
const data = JSON.parse(
|
|
6054
|
+
const data = JSON.parse(import_node_fs5.default.readFileSync(filePath, "utf-8"));
|
|
3356
6055
|
homePlantMapCache = data && typeof data === "object" ? data : {};
|
|
3357
6056
|
} catch (err) {
|
|
3358
|
-
|
|
6057
|
+
logger10.warn(`加载家园作物映射失败: ${err}`);
|
|
3359
6058
|
homePlantMapCache = {};
|
|
3360
6059
|
}
|
|
3361
6060
|
return homePlantMapCache;
|
|
@@ -3377,7 +6076,7 @@ function extractHomePlants(deps, homeInfo) {
|
|
|
3377
6076
|
if (["", "0"].includes(String(plantId || "0"))) return null;
|
|
3378
6077
|
const mappedPlant = plantMap[String(plantId)] || {};
|
|
3379
6078
|
const iconId = plantData.icon_url || plantData.iconUrl || raw.icon_url || raw.iconUrl || plantData.iconid || raw.iconid || raw.icon_id || mappedPlant.iconid;
|
|
3380
|
-
let readyAt =
|
|
6079
|
+
let readyAt = normalizeEpochSeconds2(raw.plant_rip_time || raw.rip_time || raw.end_time);
|
|
3381
6080
|
const leftTime = Number(raw.left_time || 0);
|
|
3382
6081
|
if (!readyAt && leftTime > 0) readyAt = nowTs + leftTime;
|
|
3383
6082
|
const ready = Boolean(readyAt && nowTs >= readyAt) || ["2", "ready", "mature"].includes(String(raw.status));
|
|
@@ -3410,13 +6109,13 @@ function buildHomeRenderData(deps, res, uid) {
|
|
|
3410
6109
|
const indoorPets = [];
|
|
3411
6110
|
const guardPets = [];
|
|
3412
6111
|
indoorSources.forEach((raw, index) => {
|
|
3413
|
-
const item = extractHomePet(raw, index);
|
|
6112
|
+
const item = extractHomePet(deps, raw, index);
|
|
3414
6113
|
if (!item) return;
|
|
3415
6114
|
if (item.isGuard) guardPets.push(item);
|
|
3416
6115
|
else indoorPets.push(item);
|
|
3417
6116
|
});
|
|
3418
6117
|
guardSources.forEach((raw, index) => {
|
|
3419
|
-
const item = extractHomePet(raw, index, true);
|
|
6118
|
+
const item = extractHomePet(deps, raw, index, true);
|
|
3420
6119
|
if (item) guardPets.push(item);
|
|
3421
6120
|
});
|
|
3422
6121
|
indoorPets.sort((a, b) => {
|
|
@@ -3430,7 +6129,7 @@ function buildHomeRenderData(deps, res, uid) {
|
|
|
3430
6129
|
return 0;
|
|
3431
6130
|
});
|
|
3432
6131
|
const gardenPlots = extractHomePlants(deps, homeInfo);
|
|
3433
|
-
const createdAt =
|
|
6132
|
+
const createdAt = normalizeEpochSeconds2(res?.meta?.created_at);
|
|
3434
6133
|
return {
|
|
3435
6134
|
title: "洛克家园",
|
|
3436
6135
|
subtitle: "Home Information",
|
|
@@ -3591,11 +6290,31 @@ async function resolveHomeUid(deps, session, uid = "") {
|
|
|
3591
6290
|
return String(deps.userMgr.getPrimaryBinding(session?.userId || "")?.role_id || "");
|
|
3592
6291
|
}
|
|
3593
6292
|
__name(resolveHomeUid, "resolveHomeUid");
|
|
6293
|
+
var HOME_SUB_TEXT = {
|
|
6294
|
+
garden: {
|
|
6295
|
+
kindText: "菜园作物",
|
|
6296
|
+
actionText: "成熟",
|
|
6297
|
+
subscribedHint: "的家园菜园提醒:首个成熟和全部成熟时各推送一次。",
|
|
6298
|
+
needUidHint: "请提供玩家 UID,或先完成绑定后再订阅家园菜园。"
|
|
6299
|
+
},
|
|
6300
|
+
inspiration: {
|
|
6301
|
+
kindText: "精灵灵感",
|
|
6302
|
+
actionText: "完成",
|
|
6303
|
+
subscribedHint: "的家园精灵灵感提醒:首个完成和全部完成时各推送一次。",
|
|
6304
|
+
needUidHint: "请提供玩家 UID,或先完成绑定后再订阅家园灵感。"
|
|
6305
|
+
},
|
|
6306
|
+
egg: {
|
|
6307
|
+
kindText: "精灵生蛋",
|
|
6308
|
+
actionText: "可领取",
|
|
6309
|
+
subscribedHint: "的家园精灵生蛋提醒:首个可领取和全部可领取时各推送一次。",
|
|
6310
|
+
needUidHint: "请提供玩家 UID,或先完成绑定后再订阅家园生蛋。"
|
|
6311
|
+
}
|
|
6312
|
+
};
|
|
3594
6313
|
async function subscribeHome(deps, session, uid, kind) {
|
|
3595
6314
|
const target = sessionTarget(session);
|
|
3596
6315
|
if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds)) return "此指令仅限管理员使用。";
|
|
3597
6316
|
const targetUid = await resolveHomeUid(deps, session, uid);
|
|
3598
|
-
if (!targetUid) return kind
|
|
6317
|
+
if (!targetUid) return HOME_SUB_TEXT[kind].needUidHint;
|
|
3599
6318
|
const key = homeSubscriptionKey(session, targetUid, kind);
|
|
3600
6319
|
deps.homeSubMgr.upsert(key, {
|
|
3601
6320
|
key,
|
|
@@ -3609,7 +6328,7 @@ async function subscribeHome(deps, session, uid, kind) {
|
|
|
3609
6328
|
notify_state: {},
|
|
3610
6329
|
updated_at: Math.floor(Date.now() / 1e3)
|
|
3611
6330
|
});
|
|
3612
|
-
return
|
|
6331
|
+
return `已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`;
|
|
3613
6332
|
}
|
|
3614
6333
|
__name(subscribeHome, "subscribeHome");
|
|
3615
6334
|
function homeSubscriptionState(data, kind) {
|
|
@@ -3619,6 +6338,12 @@ function homeSubscriptionState(data, kind) {
|
|
|
3619
6338
|
const names2 = readyItems2.map((item) => `田地${item.landIndex} ${item.plantName}`);
|
|
3620
6339
|
return { items: items2, readyItems: readyItems2, names: names2 };
|
|
3621
6340
|
}
|
|
6341
|
+
if (kind === "egg") {
|
|
6342
|
+
const items2 = [...data.indoorPets || [], ...data.guardPets || []].filter((item) => item.eggTime || item.hasEgg);
|
|
6343
|
+
const readyItems2 = items2.filter((item) => item.eggReady);
|
|
6344
|
+
const names2 = readyItems2.map((item) => item.name || "未知精灵");
|
|
6345
|
+
return { items: items2, readyItems: readyItems2, names: names2 };
|
|
6346
|
+
}
|
|
3622
6347
|
const items = [...data.indoorPets || [], ...data.guardPets || []].filter((item) => item.readyAt);
|
|
3623
6348
|
const readyItems = items.filter((item) => item.inspireReady);
|
|
3624
6349
|
const names = readyItems.map((item) => item.name || "未知精灵");
|
|
@@ -3626,8 +6351,7 @@ function homeSubscriptionState(data, kind) {
|
|
|
3626
6351
|
}
|
|
3627
6352
|
__name(homeSubscriptionState, "homeSubscriptionState");
|
|
3628
6353
|
function homeSubscriptionMessage(uid, kind, level, totalCount, readyItems, names) {
|
|
3629
|
-
const kindText = kind
|
|
3630
|
-
const actionText = kind === "garden" ? "成熟" : "完成";
|
|
6354
|
+
const { kindText, actionText } = HOME_SUB_TEXT[kind];
|
|
3631
6355
|
const levelText = level === "first" ? "首个" : "全部";
|
|
3632
6356
|
return [
|
|
3633
6357
|
`家园${kindText}${levelText}${actionText}提醒:${uid}`,
|
|
@@ -3642,10 +6366,10 @@ async function checkHomeSubscriptions(deps) {
|
|
|
3642
6366
|
let checkedCount = 0;
|
|
3643
6367
|
let pushedCount = 0;
|
|
3644
6368
|
for (const [key, sub] of Object.entries(subs)) {
|
|
3645
|
-
if (!sub.uid || !["garden", "inspiration"].includes(sub.kind)) continue;
|
|
6369
|
+
if (!sub.uid || !["garden", "inspiration", "egg"].includes(sub.kind)) continue;
|
|
3646
6370
|
checkedCount++;
|
|
3647
6371
|
if (!cache.has(sub.uid)) {
|
|
3648
|
-
cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid));
|
|
6372
|
+
cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 3e4 }));
|
|
3649
6373
|
}
|
|
3650
6374
|
const res = cache.get(sub.uid);
|
|
3651
6375
|
if (!res) continue;
|
|
@@ -3677,7 +6401,7 @@ async function checkHomeSubscriptions(deps) {
|
|
|
3677
6401
|
}, messages.join("\n\n"));
|
|
3678
6402
|
if (!sent) continue;
|
|
3679
6403
|
} catch (e) {
|
|
3680
|
-
|
|
6404
|
+
logger10.warn(`家园订阅推送失败: ${e}`);
|
|
3681
6405
|
continue;
|
|
3682
6406
|
}
|
|
3683
6407
|
for (const level of pushLevels) notifyState[level] = true;
|
|
@@ -3733,19 +6457,19 @@ function register2(deps) {
|
|
|
3733
6457
|
const hasExtraProfileData = Boolean(profileSignature || profileHomeItems.length || profileCardItems.length || profileCardImage);
|
|
3734
6458
|
let degraded = false;
|
|
3735
6459
|
if (!sm) {
|
|
3736
|
-
|
|
6460
|
+
logger10.warn("[Rocom] 洛克.档案:pet-summary 接口不可用,已降级为基础档案渲染");
|
|
3737
6461
|
degraded = true;
|
|
3738
6462
|
}
|
|
3739
6463
|
if (!ev) {
|
|
3740
|
-
|
|
6464
|
+
logger10.warn("[Rocom] 洛克.档案:evaluation 接口不可用,已降级为基础档案渲染");
|
|
3741
6465
|
degraded = true;
|
|
3742
6466
|
}
|
|
3743
6467
|
if (!cl) {
|
|
3744
|
-
|
|
6468
|
+
logger10.warn("[Rocom] 洛克.档案:collection 接口不可用,已降级为基础档案渲染");
|
|
3745
6469
|
degraded = true;
|
|
3746
6470
|
}
|
|
3747
6471
|
if (!bo) {
|
|
3748
|
-
|
|
6472
|
+
logger10.warn("[Rocom] 洛克.档案:battle-overview 接口不可用,已降级为基础档案渲染");
|
|
3749
6473
|
degraded = true;
|
|
3750
6474
|
}
|
|
3751
6475
|
if (degraded) {
|
|
@@ -4083,10 +6807,95 @@ function register2(deps) {
|
|
|
4083
6807
|
targetUid = String(binding?.role_id || "");
|
|
4084
6808
|
}
|
|
4085
6809
|
if (!targetUid) return "请提供玩家 UID,或先完成绑定后使用 洛克.家园。";
|
|
4086
|
-
|
|
6810
|
+
let queuedNotified = false;
|
|
6811
|
+
const res = await client.ingameHomeInfo(ctx, targetUid, {
|
|
6812
|
+
waitMs: deps.config.homeQueryWaitMs,
|
|
6813
|
+
intervalMs: deps.config.homeQueryPollIntervalMs,
|
|
6814
|
+
timeoutMs: deps.config.homeQueryTimeoutMs,
|
|
6815
|
+
onQueued: /* @__PURE__ */ __name(async () => {
|
|
6816
|
+
if (queuedNotified) return;
|
|
6817
|
+
queuedNotified = true;
|
|
6818
|
+
await session?.send?.(`UID ${targetUid} 的家园查询已进入队列,正在等待游戏侧返回,请稍候…`);
|
|
6819
|
+
}, "onQueued")
|
|
6820
|
+
});
|
|
4087
6821
|
if (!res) return `家园查询失败:${client.getLastErrorBrief()}`;
|
|
4088
6822
|
await sendImage(deps, session, "home", buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
|
|
4089
6823
|
});
|
|
6824
|
+
ctx.command("洛克").subcommand(".家园详情 [uid:string] [petGid:string] [npcId:string]", "查询家园摆放精灵的完整 ingame 数据").alias("洛克家园详情").alias("家园详情").alias("洛克精灵数据").alias("精灵数据").action(async ({ session }, uid = "", petGid = "", npcId = "") => {
|
|
6825
|
+
const petGidText = String(petGid || "").trim();
|
|
6826
|
+
const npcIdText = String(npcId || "").trim();
|
|
6827
|
+
if (petGidText && !npcIdText || npcIdText && !petGidText) {
|
|
6828
|
+
return [
|
|
6829
|
+
"请同时提供 pet_gid 和 npc_id。用法:洛克.家园详情 <UID> 或 洛克.家园详情 <UID> <pet_gid> <npc_id>",
|
|
6830
|
+
"提示:该接口依赖目标玩家在线,npc_id 也可以使用 furniture_guid。"
|
|
6831
|
+
].join("\n");
|
|
6832
|
+
}
|
|
6833
|
+
let targetUid = String(uid || "").trim();
|
|
6834
|
+
if (!targetUid) {
|
|
6835
|
+
const binding = deps.userMgr.getPrimaryBinding(session.userId);
|
|
6836
|
+
targetUid = String(binding?.role_id || "");
|
|
6837
|
+
}
|
|
6838
|
+
if (!targetUid) return "请提供玩家 UID,或先完成绑定后使用 洛克.家园详情。目标玩家需要在线。";
|
|
6839
|
+
let queuedNotified = false;
|
|
6840
|
+
const res = await client.ingamePetData(ctx, targetUid, { petGid: petGidText, npcId: npcIdText }, {
|
|
6841
|
+
waitMs: 2e4,
|
|
6842
|
+
intervalMs: deps.config.homeQueryPollIntervalMs,
|
|
6843
|
+
timeoutMs: deps.config.homeQueryTimeoutMs,
|
|
6844
|
+
onQueued: /* @__PURE__ */ __name(async () => {
|
|
6845
|
+
if (queuedNotified) return;
|
|
6846
|
+
queuedNotified = true;
|
|
6847
|
+
await session?.send?.(`UID ${targetUid} 的家园详情查询已进入队列,正在等待游戏侧返回,请稍候…`);
|
|
6848
|
+
}, "onQueued")
|
|
6849
|
+
});
|
|
6850
|
+
if (!res) {
|
|
6851
|
+
return [
|
|
6852
|
+
`家园详情查询失败:${client.getLastErrorBrief()}`,
|
|
6853
|
+
"提示:该接口需要目标玩家在线,且目标家园可访问;离线时通常无法获取完整数据。"
|
|
6854
|
+
].join("\n");
|
|
6855
|
+
}
|
|
6856
|
+
const lowBandwidth = Boolean(deps.config.lowBandwidthMode);
|
|
6857
|
+
const [optionsPayload, skillLookup, sizeLookup] = await Promise.all([
|
|
6858
|
+
deps.wikiService.getOptionsPayload().catch(() => ({})),
|
|
6859
|
+
(async () => {
|
|
6860
|
+
const lookup = {};
|
|
6861
|
+
const ids = petDataSkillIdsFromPayload(res);
|
|
6862
|
+
await Promise.all(ids.map(async (id) => {
|
|
6863
|
+
const detail = await deps.wikiService.getSkillDetailCached(id).catch(() => null);
|
|
6864
|
+
if (detail) lookup[id] = detail;
|
|
6865
|
+
}));
|
|
6866
|
+
return lookup;
|
|
6867
|
+
})(),
|
|
6868
|
+
(async () => {
|
|
6869
|
+
const lookup = {};
|
|
6870
|
+
const ids = petDataPetIdsFromPayload(res);
|
|
6871
|
+
await Promise.all(ids.map(async (id) => {
|
|
6872
|
+
const detail = await deps.wikiService.getPetDetailCached(id).catch(() => null);
|
|
6873
|
+
if (detail) lookup[id] = detail;
|
|
6874
|
+
}));
|
|
6875
|
+
return lookup;
|
|
6876
|
+
})()
|
|
6877
|
+
]);
|
|
6878
|
+
const data = buildPetDataRenderData(res, targetUid || "当前绑定", {
|
|
6879
|
+
optionsPayload,
|
|
6880
|
+
skillLookup,
|
|
6881
|
+
sizeLookup,
|
|
6882
|
+
singleQuery: Boolean(petGidText && npcIdText),
|
|
6883
|
+
lowBandwidthMode: lowBandwidth,
|
|
6884
|
+
baseUrl: deps.config.apiBaseUrl.replace(/\/$/, "")
|
|
6885
|
+
});
|
|
6886
|
+
const fallbackLines = [
|
|
6887
|
+
`家园详情 - UID ${data.uid}(${data.onlineText})`,
|
|
6888
|
+
data.notice
|
|
6889
|
+
];
|
|
6890
|
+
for (const pet of (data.pets || []).slice(0, 8)) {
|
|
6891
|
+
fallbackLines.push(`${pet.index}. ${pet.name} Lv.${pet.level} #${pet.baseId} ${pet.variantText} 分贝 ${pet.voiceText || "--"}`);
|
|
6892
|
+
}
|
|
6893
|
+
if (!data.pets?.length) fallbackLines.push(data.emptyText);
|
|
6894
|
+
fallbackLines.push("");
|
|
6895
|
+
fallbackLines.push("若服务器带宽过小导致生成超时,请在配置项打开低带宽模式。");
|
|
6896
|
+
fallbackLines.push("低带宽模式开启后,家园详情将不再加载技能图标。");
|
|
6897
|
+
await sendImage(deps, session, "pet-data", data, fallbackLines.join("\n"));
|
|
6898
|
+
});
|
|
4090
6899
|
ctx.command("洛克").subcommand(".刷新面板 [uid:string]", "刷新指定 UID 的精灵面板缓存").alias("洛克刷新面板").alias("刷新面板").alias("刷新精灵").alias("强制刷新").action(async ({ session }, uid = "") => {
|
|
4091
6900
|
const targetUid = panelUidFromBinding(deps, session, uid);
|
|
4092
6901
|
if (!targetUid) return "请提供 UID,或先绑定账号后再使用 洛克.刷新面板。";
|
|
@@ -4164,10 +6973,11 @@ function register2(deps) {
|
|
|
4164
6973
|
});
|
|
4165
6974
|
ctx.command("订阅家园菜园 [uid:string]", "订阅指定 UID 的家园菜园成熟提醒").action(async ({ session }, uid = "") => subscribeHome(deps, session, uid, "garden"));
|
|
4166
6975
|
ctx.command("订阅家园灵感 [uid:string]", "订阅指定 UID 的家园精灵灵感完成提醒").action(async ({ session }, uid = "") => subscribeHome(deps, session, uid, "inspiration"));
|
|
6976
|
+
ctx.command("订阅家园生蛋 [uid:string]", "订阅指定 UID 的家园精灵生蛋提醒").action(async ({ session }, uid = "") => subscribeHome(deps, session, uid, "egg"));
|
|
4167
6977
|
ctx.command("取消订阅家园 [kind:string] [uid:string]", "取消当前会话的家园订阅").action(async ({ session }, kind = "全部", uid = "") => {
|
|
4168
6978
|
const target = sessionTarget(session);
|
|
4169
6979
|
if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds)) return "此指令仅限管理员使用。";
|
|
4170
|
-
const kindMap = { "菜园": "garden", "灵感": "inspiration", "全部": "", all: "", garden: "garden", inspiration: "inspiration" };
|
|
6980
|
+
const kindMap = { "菜园": "garden", "灵感": "inspiration", "生蛋": "egg", "全部": "", all: "", garden: "garden", inspiration: "inspiration", egg: "egg" };
|
|
4171
6981
|
const deleted = deps.homeSubMgr.deleteMatching(target, kindMap[String(kind || "全部")] ?? "", String(uid || "").trim());
|
|
4172
6982
|
return deleted ? `已取消 ${deleted} 条家园订阅。` : "当前会话没有匹配的家园订阅。";
|
|
4173
6983
|
});
|
|
@@ -4177,16 +6987,17 @@ function register2(deps) {
|
|
|
4177
6987
|
return `家园订阅检查完成:订阅 ${result.subscriptions} 条,检查 ${result.checked} 条,推送 ${result.pushed} 档提醒。`;
|
|
4178
6988
|
});
|
|
4179
6989
|
if (deps.config.homeSubscriptionEnabled) {
|
|
4180
|
-
ctx.setInterval(() => checkHomeSubscriptions(deps).catch((err) =>
|
|
6990
|
+
ctx.setInterval(() => checkHomeSubscriptions(deps).catch((err) => logger10.warn(`家园订阅检查失败: ${err}`)), Math.max(1, deps.config.homeSubscriptionIntervalMinutes || 5) * 6e4);
|
|
4181
6991
|
}
|
|
4182
6992
|
}
|
|
4183
6993
|
__name(register2, "register");
|
|
4184
6994
|
|
|
4185
6995
|
// src/commands/merchant.ts
|
|
4186
|
-
var
|
|
4187
|
-
var
|
|
6996
|
+
var import_koishi11 = require("koishi");
|
|
6997
|
+
var logger11 = new import_koishi11.Logger("rocom-merchant");
|
|
4188
6998
|
var TEXT = {
|
|
4189
6999
|
merchant: "远行商人",
|
|
7000
|
+
todayMerchant: "今日远行商人",
|
|
4190
7001
|
subscribe: "订阅远行商人",
|
|
4191
7002
|
unsubscribe: "取消订阅远行商人",
|
|
4192
7003
|
viewSubscribe: "查看远行商人订阅",
|
|
@@ -4201,6 +7012,12 @@ var CATEGORY_LABELS = {
|
|
|
4201
7012
|
round: "常规商品",
|
|
4202
7013
|
weekend: "周末限定"
|
|
4203
7014
|
};
|
|
7015
|
+
var ROUND_WINDOWS = [
|
|
7016
|
+
{ id: 1, label: "08:00-12:00", startHour: 8, endHour: 12 },
|
|
7017
|
+
{ id: 2, label: "12:00-16:00", startHour: 12, endHour: 16 },
|
|
7018
|
+
{ id: 3, label: "16:00-20:00", startHour: 16, endHour: 20 },
|
|
7019
|
+
{ id: 4, label: "20:00-24:00", startHour: 20, endHour: 24 }
|
|
7020
|
+
];
|
|
4204
7021
|
var CHINA_TIMEZONE = "Asia/Shanghai";
|
|
4205
7022
|
var chinaPartsFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
4206
7023
|
timeZone: CHINA_TIMEZONE,
|
|
@@ -4209,19 +7026,39 @@ var chinaPartsFormatter = new Intl.DateTimeFormat("zh-CN", {
|
|
|
4209
7026
|
day: "2-digit",
|
|
4210
7027
|
hour: "2-digit",
|
|
4211
7028
|
minute: "2-digit",
|
|
7029
|
+
second: "2-digit",
|
|
4212
7030
|
hour12: false
|
|
4213
7031
|
});
|
|
4214
|
-
function getChinaParts(
|
|
7032
|
+
function getChinaParts(input = Date.now()) {
|
|
4215
7033
|
const parts = {};
|
|
4216
|
-
|
|
7034
|
+
const date = input instanceof Date ? input : new Date(input);
|
|
7035
|
+
for (const item of chinaPartsFormatter.formatToParts(date)) {
|
|
4217
7036
|
if (item.type !== "literal") parts[item.type] = item.value;
|
|
4218
7037
|
}
|
|
4219
7038
|
return {
|
|
7039
|
+
year: Number(parts.year || "0"),
|
|
7040
|
+
month: Number(parts.month || "0"),
|
|
7041
|
+
day: Number(parts.day || "0"),
|
|
4220
7042
|
hour: Number(parts.hour || "0"),
|
|
4221
|
-
minute: Number(parts.minute || "0")
|
|
7043
|
+
minute: Number(parts.minute || "0"),
|
|
7044
|
+
second: Number(parts.second || "0")
|
|
4222
7045
|
};
|
|
4223
7046
|
}
|
|
4224
7047
|
__name(getChinaParts, "getChinaParts");
|
|
7048
|
+
function padNumber(value) {
|
|
7049
|
+
return String(value).padStart(2, "0");
|
|
7050
|
+
}
|
|
7051
|
+
__name(padNumber, "padNumber");
|
|
7052
|
+
function getChinaDateText(input = Date.now()) {
|
|
7053
|
+
const parts = getChinaParts(input);
|
|
7054
|
+
return `${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
7055
|
+
}
|
|
7056
|
+
__name(getChinaDateText, "getChinaDateText");
|
|
7057
|
+
function getChinaDayStartMs(input = Date.now()) {
|
|
7058
|
+
const parts = getChinaParts(input);
|
|
7059
|
+
return (/* @__PURE__ */ new Date(`${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}T00:00:00+08:00`)).getTime();
|
|
7060
|
+
}
|
|
7061
|
+
__name(getChinaDayStartMs, "getChinaDayStartMs");
|
|
4225
7062
|
function classifyMerchantItem(item) {
|
|
4226
7063
|
const start = normalizeTimestamp(item?.start_time);
|
|
4227
7064
|
const end = normalizeTimestamp(item?.end_time);
|
|
@@ -4248,16 +7085,12 @@ function formatProductWindow(product) {
|
|
|
4248
7085
|
const end = normalizeTimestamp(product?.end_time);
|
|
4249
7086
|
if (!start && !end) return "";
|
|
4250
7087
|
const formatDate2 = /* @__PURE__ */ __name((timestamp) => {
|
|
4251
|
-
const
|
|
4252
|
-
|
|
4253
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
4254
|
-
return `${month}-${day}`;
|
|
7088
|
+
const parts = getChinaParts(timestamp);
|
|
7089
|
+
return `${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
4255
7090
|
}, "formatDate");
|
|
4256
7091
|
const formatTime = /* @__PURE__ */ __name((timestamp) => {
|
|
4257
|
-
const
|
|
4258
|
-
|
|
4259
|
-
const minute = String(date.getMinutes()).padStart(2, "0");
|
|
4260
|
-
return `${hour}:${minute}`;
|
|
7092
|
+
const parts = getChinaParts(timestamp);
|
|
7093
|
+
return `${padNumber(parts.hour)}:${padNumber(parts.minute)}`;
|
|
4261
7094
|
}, "formatTime");
|
|
4262
7095
|
if (start && end) {
|
|
4263
7096
|
const datePart = formatDate2(start);
|
|
@@ -4272,7 +7105,7 @@ function getMerchantActivity(res) {
|
|
|
4272
7105
|
return activities[0] || {};
|
|
4273
7106
|
}
|
|
4274
7107
|
__name(getMerchantActivity, "getMerchantActivity");
|
|
4275
|
-
function
|
|
7108
|
+
function getMerchantProducts(res) {
|
|
4276
7109
|
const activity = getMerchantActivity(res);
|
|
4277
7110
|
const groups = [];
|
|
4278
7111
|
if (Array.isArray(activity?.products)) groups.push(activity.products);
|
|
@@ -4282,12 +7115,10 @@ function getActiveProducts(res) {
|
|
|
4282
7115
|
if (Array.isArray(activity?.get_pets)) groups.push(activity.get_pets);
|
|
4283
7116
|
const merged = [];
|
|
4284
7117
|
const seen = /* @__PURE__ */ new Set();
|
|
4285
|
-
const now = Date.now();
|
|
4286
7118
|
for (const list of groups) {
|
|
4287
7119
|
for (const item of list) {
|
|
4288
7120
|
const start = normalizeTimestamp(item?.start_time) ?? 0;
|
|
4289
7121
|
const end = normalizeTimestamp(item?.end_time) ?? Infinity;
|
|
4290
|
-
if (now < start || now >= end) continue;
|
|
4291
7122
|
const key = `${item?.id ?? ""}|${item?.name ?? ""}|${start}|${end === Infinity ? "inf" : end}`;
|
|
4292
7123
|
if (seen.has(key)) continue;
|
|
4293
7124
|
seen.add(key);
|
|
@@ -4296,35 +7127,60 @@ function getActiveProducts(res) {
|
|
|
4296
7127
|
}
|
|
4297
7128
|
return merged;
|
|
4298
7129
|
}
|
|
4299
|
-
__name(
|
|
4300
|
-
function
|
|
4301
|
-
const
|
|
4302
|
-
const
|
|
4303
|
-
const
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
7130
|
+
__name(getMerchantProducts, "getMerchantProducts");
|
|
7131
|
+
function isMerchantItemActive(item, now = Date.now()) {
|
|
7132
|
+
const nowMs = now instanceof Date ? now.getTime() : now;
|
|
7133
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
7134
|
+
const end = normalizeTimestamp(item?.end_time);
|
|
7135
|
+
return (start === null || nowMs >= start) && (end === null || nowMs < end);
|
|
7136
|
+
}
|
|
7137
|
+
__name(isMerchantItemActive, "isMerchantItemActive");
|
|
7138
|
+
function isMerchantItemToday(item, now = Date.now()) {
|
|
7139
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
7140
|
+
const end = normalizeTimestamp(item?.end_time);
|
|
7141
|
+
if (start === null || end === null) return true;
|
|
7142
|
+
const startOfDay = getChinaDayStartMs(now);
|
|
7143
|
+
const endOfDay = startOfDay + 24 * 60 * 60 * 1e3;
|
|
7144
|
+
return start < endOfDay && end > startOfDay;
|
|
7145
|
+
}
|
|
7146
|
+
__name(isMerchantItemToday, "isMerchantItemToday");
|
|
7147
|
+
function getRoundForItem(item, now = Date.now()) {
|
|
7148
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
7149
|
+
if (start === null) return null;
|
|
7150
|
+
const startParts = getChinaParts(start);
|
|
7151
|
+
const nowParts = getChinaParts(now);
|
|
7152
|
+
if (startParts.year !== nowParts.year || startParts.month !== nowParts.month || startParts.day !== nowParts.day) {
|
|
7153
|
+
return null;
|
|
7154
|
+
}
|
|
7155
|
+
const startHour = startParts.hour + startParts.minute / 60;
|
|
7156
|
+
const round = ROUND_WINDOWS.find((win) => startHour >= win.startHour && startHour < win.endHour);
|
|
7157
|
+
return round?.id ?? null;
|
|
7158
|
+
}
|
|
7159
|
+
__name(getRoundForItem, "getRoundForItem");
|
|
7160
|
+
function getCurrentMerchantRound(now = /* @__PURE__ */ new Date()) {
|
|
7161
|
+
const parts = getChinaParts(now);
|
|
7162
|
+
const secondsOfDay = parts.hour * 3600 + parts.minute * 60 + parts.second;
|
|
7163
|
+
const marketStartSeconds = 8 * 3600;
|
|
7164
|
+
const marketEndSeconds = 24 * 3600;
|
|
7165
|
+
const datePart = getChinaDateText(now);
|
|
7166
|
+
if (secondsOfDay < marketStartSeconds || secondsOfDay >= marketEndSeconds) {
|
|
7167
|
+
return {
|
|
7168
|
+
current: null,
|
|
7169
|
+
total: ROUND_WINDOWS.length,
|
|
7170
|
+
countdown: TEXT.notOpen,
|
|
7171
|
+
is_open: false,
|
|
7172
|
+
round_id: `${datePart}-closed`
|
|
7173
|
+
};
|
|
4312
7174
|
}
|
|
4313
|
-
|
|
4314
|
-
const
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
const diff = nextTime.getTime() - now.getTime();
|
|
7175
|
+
const currentWindow = ROUND_WINDOWS.find((win) => parts.hour >= win.startHour && parts.hour < win.endHour);
|
|
7176
|
+
const currentRound = currentWindow?.id ?? null;
|
|
7177
|
+
const roundEndSeconds = (currentWindow?.endHour ?? 24) * 3600;
|
|
7178
|
+
const diff = Math.max(0, (roundEndSeconds - secondsOfDay) * 1e3);
|
|
4318
7179
|
const hours = Math.floor(diff / 36e5);
|
|
4319
7180
|
const mins = Math.floor(diff % 36e5 / 6e4);
|
|
4320
|
-
const datePart = [
|
|
4321
|
-
now.getFullYear(),
|
|
4322
|
-
String(now.getMonth() + 1).padStart(2, "0"),
|
|
4323
|
-
String(now.getDate()).padStart(2, "0")
|
|
4324
|
-
].join("-");
|
|
4325
7181
|
return {
|
|
4326
7182
|
current: currentRound,
|
|
4327
|
-
total:
|
|
7183
|
+
total: ROUND_WINDOWS.length,
|
|
4328
7184
|
countdown: `${hours}小时${mins}分钟`,
|
|
4329
7185
|
is_open: currentRound !== null,
|
|
4330
7186
|
round_id: `${datePart}-${currentRound || "closed"}`
|
|
@@ -4365,66 +7221,258 @@ function sameStringArray(left, right) {
|
|
|
4365
7221
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
4366
7222
|
}
|
|
4367
7223
|
__name(sameStringArray, "sameStringArray");
|
|
4368
|
-
function
|
|
4369
|
-
const
|
|
4370
|
-
const
|
|
4371
|
-
const
|
|
4372
|
-
const
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
7224
|
+
function getRandomGoodsMaps(res) {
|
|
7225
|
+
const priceMap = /* @__PURE__ */ new Map();
|
|
7226
|
+
const limitMap = /* @__PURE__ */ new Map();
|
|
7227
|
+
const randomGoods = Array.isArray(res?.random_goods) ? res.random_goods : Array.isArray(res?.randomGoods) ? res.randomGoods : [];
|
|
7228
|
+
for (const item of randomGoods) {
|
|
7229
|
+
const name2 = String(item?.goods_name || item?.name || "").trim();
|
|
7230
|
+
if (!name2) continue;
|
|
7231
|
+
if (item?.price !== void 0 && item?.price !== null && item.price !== "") priceMap.set(name2, item.price);
|
|
7232
|
+
if (item?.buy_limit_num !== void 0 && item?.buy_limit_num !== null && item.buy_limit_num !== "") {
|
|
7233
|
+
limitMap.set(name2, item.buy_limit_num);
|
|
7234
|
+
}
|
|
7235
|
+
}
|
|
7236
|
+
return { priceMap, limitMap };
|
|
7237
|
+
}
|
|
7238
|
+
__name(getRandomGoodsMaps, "getRandomGoodsMaps");
|
|
7239
|
+
function normalizeMerchantProducts(res, now = /* @__PURE__ */ new Date()) {
|
|
7240
|
+
return getMerchantProducts(res).map((item) => {
|
|
7241
|
+
const name2 = String(item?.name || item?.goods_name || TEXT.unknown).trim() || TEXT.unknown;
|
|
7242
|
+
return {
|
|
7243
|
+
name: name2,
|
|
7244
|
+
image: String(item?.icon_url || item?.iconUrl || ""),
|
|
7245
|
+
time_label: formatProductWindow(item),
|
|
7246
|
+
category: classifyMerchantItem(item),
|
|
7247
|
+
round_id: getRoundForItem(item, now),
|
|
7248
|
+
is_active: isMerchantItemActive(item, now),
|
|
7249
|
+
start_time: normalizeTimestamp(item?.start_time),
|
|
7250
|
+
end_time: normalizeTimestamp(item?.end_time)
|
|
7251
|
+
};
|
|
7252
|
+
});
|
|
7253
|
+
}
|
|
7254
|
+
__name(normalizeMerchantProducts, "normalizeMerchantProducts");
|
|
7255
|
+
function buildMerchantCardItems(products, res, options) {
|
|
7256
|
+
const { priceMap, limitMap } = getRandomGoodsMaps(res);
|
|
7257
|
+
const catOrder = { round: 0, normal: 1, weekend: 2 };
|
|
7258
|
+
const startY = 592;
|
|
7259
|
+
const cardHeight = 308;
|
|
7260
|
+
const gap = 43;
|
|
7261
|
+
const goodsAll = products.map((product) => {
|
|
7262
|
+
const limit = limitMap.get(product.name);
|
|
7263
|
+
const limitText = limit === void 0 || limit === null || limit === "" ? "--" : String(limit);
|
|
7264
|
+
const isHot = product.category !== "round";
|
|
7265
|
+
const isEnded = options.includeEnded ? !product.is_active : false;
|
|
7266
|
+
const roundPrefix = product.round_id ? `第${product.round_id}轮·` : "";
|
|
7267
|
+
const remainingStr = product.category === "normal" ? `本日限购${limitText}个` : product.category === "weekend" ? `活动期间限购${limitText}个` : `${isEnded ? roundPrefix : ""}本轮限购${limitText}个`;
|
|
7268
|
+
return {
|
|
7269
|
+
goods_name: product.name,
|
|
7270
|
+
iconUrl: product.image,
|
|
7271
|
+
price: priceMap.get(product.name) ?? 0,
|
|
7272
|
+
num: "",
|
|
7273
|
+
category: product.category,
|
|
7274
|
+
roundId: product.round_id || 0,
|
|
7275
|
+
isHot,
|
|
7276
|
+
isEnded,
|
|
7277
|
+
remainingStr,
|
|
7278
|
+
top: 0
|
|
7279
|
+
};
|
|
7280
|
+
});
|
|
7281
|
+
goodsAll.sort((a, b) => {
|
|
7282
|
+
if (options.includeEnded && a.isEnded !== b.isEnded) return a.isEnded ? 1 : -1;
|
|
7283
|
+
if (a.category !== b.category) return catOrder[a.category] - catOrder[b.category];
|
|
7284
|
+
return Number(b.price || 0) - Number(a.price || 0);
|
|
7285
|
+
});
|
|
7286
|
+
const goods = goodsAll.map((item, index) => ({
|
|
7287
|
+
...item,
|
|
7288
|
+
num: String(index + 1).padStart(2, "0"),
|
|
7289
|
+
top: startY + index * (cardHeight + gap)
|
|
4377
7290
|
}));
|
|
4378
|
-
const
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
7291
|
+
const lastCardTop = goods.length > 0 ? goods[goods.length - 1].top : startY;
|
|
7292
|
+
const bottomFrameTop = lastCardTop + 287;
|
|
7293
|
+
const pageHeight = bottomFrameTop + 160;
|
|
7294
|
+
return { goods, bottomFrameTop, pageHeight };
|
|
7295
|
+
}
|
|
7296
|
+
__name(buildMerchantCardItems, "buildMerchantCardItems");
|
|
7297
|
+
function getCurrentMerchantProducts(products, roundInfo) {
|
|
7298
|
+
return products.filter((product) => {
|
|
7299
|
+
if (!product.is_active) return false;
|
|
7300
|
+
if (product.category === "round") return !!roundInfo.current && product.round_id === roundInfo.current;
|
|
7301
|
+
return true;
|
|
7302
|
+
});
|
|
7303
|
+
}
|
|
7304
|
+
__name(getCurrentMerchantProducts, "getCurrentMerchantProducts");
|
|
7305
|
+
function buildMerchantFallbackText(title, products, roundInfo) {
|
|
7306
|
+
const fallbackLines = [title];
|
|
7307
|
+
if (roundInfo) {
|
|
7308
|
+
fallbackLines.push(`轮次:第 ${roundInfo.current || TEXT.notOpen} / ${roundInfo.total} 轮`);
|
|
7309
|
+
fallbackLines.push(`剩余:${roundInfo.countdown}`);
|
|
7310
|
+
}
|
|
7311
|
+
fallbackLines.push("");
|
|
7312
|
+
if (!products.length) {
|
|
7313
|
+
fallbackLines.push(roundInfo ? "当前轮次暂无商品。" : "今日暂无已公布的远行商人商品。");
|
|
7314
|
+
return fallbackLines.join("\n").trimEnd();
|
|
7315
|
+
}
|
|
7316
|
+
for (const key of CATEGORY_ORDER) {
|
|
7317
|
+
const group = products.filter((product) => product.category === key);
|
|
7318
|
+
if (!group.length) continue;
|
|
7319
|
+
fallbackLines.push(`【${CATEGORY_LABELS[key]}】`);
|
|
7320
|
+
group.forEach((product, index) => {
|
|
7321
|
+
const tail = product.time_label ? ` (${product.time_label})` : "";
|
|
7322
|
+
fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
|
|
7323
|
+
});
|
|
7324
|
+
fallbackLines.push("");
|
|
4385
7325
|
}
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
7326
|
+
return fallbackLines.join("\n").trimEnd();
|
|
7327
|
+
}
|
|
7328
|
+
__name(buildMerchantFallbackText, "buildMerchantFallbackText");
|
|
7329
|
+
function getMerchantActivityTitle(res) {
|
|
7330
|
+
const activity = getMerchantActivity(res);
|
|
7331
|
+
return String(activity?.name || TEXT.merchant).trim() || TEXT.merchant;
|
|
7332
|
+
}
|
|
7333
|
+
__name(getMerchantActivityTitle, "getMerchantActivityTitle");
|
|
7334
|
+
function getMerchantActivitySubtitle(res) {
|
|
7335
|
+
const activity = getMerchantActivity(res);
|
|
7336
|
+
return String(activity?.start_date || "每日 08:00 / 12:00 / 16:00 / 20:00 刷新").trim();
|
|
7337
|
+
}
|
|
7338
|
+
__name(getMerchantActivitySubtitle, "getMerchantActivitySubtitle");
|
|
7339
|
+
function getLegacyRoundGroups(now, roundInfo) {
|
|
7340
|
+
return ROUND_WINDOWS.map((win) => ({
|
|
7341
|
+
round_id: win.id,
|
|
7342
|
+
label: `${padNumber(win.startHour)}:00 - ${padNumber(win.endHour)}:00`,
|
|
7343
|
+
is_current: roundInfo.current === win.id,
|
|
7344
|
+
products: []
|
|
4390
7345
|
}));
|
|
7346
|
+
}
|
|
7347
|
+
__name(getLegacyRoundGroups, "getLegacyRoundGroups");
|
|
7348
|
+
function buildLegacyCategoryGroups(products, roundGroups) {
|
|
7349
|
+
return CATEGORY_ORDER.map((key) => {
|
|
7350
|
+
const groups = roundGroups.map((group) => ({
|
|
7351
|
+
round_id: group.round_id,
|
|
7352
|
+
label: group.label,
|
|
7353
|
+
is_current: group.is_current,
|
|
7354
|
+
products: group.products.filter((product) => product.category === key)
|
|
7355
|
+
})).filter((group) => group.products.length > 0);
|
|
7356
|
+
return {
|
|
7357
|
+
key,
|
|
7358
|
+
label: CATEGORY_LABELS[key],
|
|
7359
|
+
roundGroups: groups,
|
|
7360
|
+
product_count: groups.reduce((sum, group) => sum + group.products.length, 0)
|
|
7361
|
+
};
|
|
7362
|
+
}).filter((category) => category.product_count > 0);
|
|
7363
|
+
}
|
|
7364
|
+
__name(buildLegacyCategoryGroups, "buildLegacyCategoryGroups");
|
|
7365
|
+
function getMerchantDateStr(now = /* @__PURE__ */ new Date()) {
|
|
7366
|
+
const parts = getChinaParts(now);
|
|
7367
|
+
return `${parts.month}.${parts.day}`;
|
|
7368
|
+
}
|
|
7369
|
+
__name(getMerchantDateStr, "getMerchantDateStr");
|
|
7370
|
+
function buildMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
|
|
7371
|
+
const allProducts = normalizeMerchantProducts(res, now);
|
|
7372
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
7373
|
+
const products = getCurrentMerchantProducts(allProducts, roundInfo);
|
|
7374
|
+
const currentWindow = ROUND_WINDOWS.find((win) => win.id === roundInfo.current);
|
|
7375
|
+
const timeRange = currentWindow ? `${padNumber(currentWindow.startHour)}:00-${padNumber(currentWindow.endHour)}:00` : "--:--~--:--";
|
|
7376
|
+
const data = {
|
|
7377
|
+
dateStr: getMerchantDateStr(now),
|
|
7378
|
+
timeRange,
|
|
7379
|
+
...buildMerchantCardItems(products, res, { includeEnded: false })
|
|
7380
|
+
};
|
|
7381
|
+
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
7382
|
+
return { products, roundInfo, data, fallback };
|
|
7383
|
+
}
|
|
7384
|
+
__name(buildMerchantRenderPayload, "buildMerchantRenderPayload");
|
|
7385
|
+
function buildLegacyMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
|
|
7386
|
+
const allProducts = normalizeMerchantProducts(res, now);
|
|
7387
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
7388
|
+
const products = getCurrentMerchantProducts(allProducts, roundInfo);
|
|
7389
|
+
const roundGroups = getLegacyRoundGroups(now, roundInfo);
|
|
7390
|
+
const currentGroup = roundGroups.find((group) => group.round_id === roundInfo.current) || roundGroups[0];
|
|
7391
|
+
currentGroup.products.push(...products);
|
|
7392
|
+
const categories = buildLegacyCategoryGroups(products, roundGroups);
|
|
4391
7393
|
const data = {
|
|
4392
7394
|
background: "",
|
|
4393
|
-
title:
|
|
4394
|
-
subtitle:
|
|
4395
|
-
titleIcon: true,
|
|
4396
|
-
product_count: renderedProducts.length,
|
|
4397
|
-
round_info: roundInfo,
|
|
7395
|
+
title: getMerchantActivityTitle(res),
|
|
7396
|
+
subtitle: getMerchantActivitySubtitle(res),
|
|
4398
7397
|
categories,
|
|
4399
|
-
|
|
7398
|
+
roundGroups,
|
|
7399
|
+
total_products: products.length
|
|
4400
7400
|
};
|
|
4401
|
-
const
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
}
|
|
4417
|
-
const fallback =
|
|
4418
|
-
return { products
|
|
7401
|
+
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
7402
|
+
return { products, roundInfo, data, fallback };
|
|
7403
|
+
}
|
|
7404
|
+
__name(buildLegacyMerchantRenderPayload, "buildLegacyMerchantRenderPayload");
|
|
7405
|
+
function buildTodayMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
|
|
7406
|
+
const products = normalizeMerchantProducts(res, now).filter((product) => {
|
|
7407
|
+
const source = {
|
|
7408
|
+
start_time: product.start_time,
|
|
7409
|
+
end_time: product.end_time
|
|
7410
|
+
};
|
|
7411
|
+
return isMerchantItemToday(source, now);
|
|
7412
|
+
});
|
|
7413
|
+
const data = {
|
|
7414
|
+
dateStr: getMerchantDateStr(now),
|
|
7415
|
+
...buildMerchantCardItems(products, res, { includeEnded: true })
|
|
7416
|
+
};
|
|
7417
|
+
const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, products);
|
|
7418
|
+
return { products, data, fallback };
|
|
7419
|
+
}
|
|
7420
|
+
__name(buildTodayMerchantRenderPayload, "buildTodayMerchantRenderPayload");
|
|
7421
|
+
function buildLegacyTodayMerchantRenderPayload(res, now = /* @__PURE__ */ new Date()) {
|
|
7422
|
+
const allProducts = normalizeMerchantProducts(res, now).filter((product) => {
|
|
7423
|
+
const source = {
|
|
7424
|
+
start_time: product.start_time,
|
|
7425
|
+
end_time: product.end_time
|
|
7426
|
+
};
|
|
7427
|
+
return isMerchantItemToday(source, now);
|
|
7428
|
+
});
|
|
7429
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
7430
|
+
const roundGroups = getLegacyRoundGroups(now, roundInfo);
|
|
7431
|
+
for (const product of allProducts) {
|
|
7432
|
+
const roundId = product.category === "round" ? product.round_id : null;
|
|
7433
|
+
const group = roundId ? roundGroups.find((item) => item.round_id === roundId) : roundGroups[0];
|
|
7434
|
+
if (group) group.products.push(product);
|
|
7435
|
+
}
|
|
7436
|
+
const categories = buildLegacyCategoryGroups(allProducts, roundGroups);
|
|
7437
|
+
const data = {
|
|
7438
|
+
background: "",
|
|
7439
|
+
title: TEXT.todayMerchant,
|
|
7440
|
+
subtitle: `${getChinaDateText(now)} · 每日 08:00 / 12:00 / 16:00 / 20:00 刷新`,
|
|
7441
|
+
categories,
|
|
7442
|
+
roundGroups,
|
|
7443
|
+
total_products: allProducts.length
|
|
7444
|
+
};
|
|
7445
|
+
const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, allProducts);
|
|
7446
|
+
return { products: allProducts, data, fallback };
|
|
4419
7447
|
}
|
|
4420
|
-
__name(
|
|
7448
|
+
__name(buildLegacyTodayMerchantRenderPayload, "buildLegacyTodayMerchantRenderPayload");
|
|
7449
|
+
function useLegacyMerchantUi(deps) {
|
|
7450
|
+
return deps.config.merchantUiStyle === "old";
|
|
7451
|
+
}
|
|
7452
|
+
__name(useLegacyMerchantUi, "useLegacyMerchantUi");
|
|
7453
|
+
function buildConfiguredMerchantRenderPayload(deps, res, now = /* @__PURE__ */ new Date()) {
|
|
7454
|
+
const payload = useLegacyMerchantUi(deps) ? buildLegacyMerchantRenderPayload(res, now) : buildMerchantRenderPayload(res, now);
|
|
7455
|
+
return {
|
|
7456
|
+
...payload,
|
|
7457
|
+
templateName: useLegacyMerchantUi(deps) ? "yuanxing-shangren" : "yuanxing-shangren/merchant"
|
|
7458
|
+
};
|
|
7459
|
+
}
|
|
7460
|
+
__name(buildConfiguredMerchantRenderPayload, "buildConfiguredMerchantRenderPayload");
|
|
7461
|
+
function buildConfiguredTodayMerchantRenderPayload(deps, res, now = /* @__PURE__ */ new Date()) {
|
|
7462
|
+
const payload = useLegacyMerchantUi(deps) ? buildLegacyTodayMerchantRenderPayload(res, now) : buildTodayMerchantRenderPayload(res, now);
|
|
7463
|
+
return {
|
|
7464
|
+
...payload,
|
|
7465
|
+
templateName: useLegacyMerchantUi(deps) ? "yuanxing-shangren" : "yuanxing-shangren/today"
|
|
7466
|
+
};
|
|
7467
|
+
}
|
|
7468
|
+
__name(buildConfiguredTodayMerchantRenderPayload, "buildConfiguredTodayMerchantRenderPayload");
|
|
4421
7469
|
async function checkMerchantSubscriptions(deps) {
|
|
4422
7470
|
const { ctx, client, merchantSubMgr, renderer, config } = deps;
|
|
4423
7471
|
const res = await client.getMerchantInfo(ctx, true);
|
|
4424
7472
|
if (!res) return { subscriptions: 0, matched: 0, pushed: 0 };
|
|
4425
|
-
const { products, roundInfo, data, fallback } =
|
|
7473
|
+
const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
4426
7474
|
const productNames = products.map((p) => p.name || "").filter(Boolean);
|
|
4427
|
-
const rendered = await renderer.renderHtml(ctx,
|
|
7475
|
+
const rendered = await renderer.renderHtml(ctx, templateName, data);
|
|
4428
7476
|
const renderedImage = rendered ? compressPngImage(rendered, config) : null;
|
|
4429
7477
|
const subs = merchantSubMgr.getAll();
|
|
4430
7478
|
let matchedCount = 0;
|
|
@@ -4447,7 +7495,7 @@ async function checkMerchantSubscriptions(deps) {
|
|
|
4447
7495
|
const platform = sub.platform || ctx.bots[0]?.platform;
|
|
4448
7496
|
const channelId = sub.channel_id || sub.group_id || sub.user_id || key;
|
|
4449
7497
|
if (!platform || !channelId) {
|
|
4450
|
-
|
|
7498
|
+
logger11.warn(`推送失败 ${key}: 无法确定平台或频道`);
|
|
4451
7499
|
continue;
|
|
4452
7500
|
}
|
|
4453
7501
|
const fallbackText = `${msg}
|
|
@@ -4471,13 +7519,20 @@ ${fallback}`;
|
|
|
4471
7519
|
__name(checkMerchantSubscriptions, "checkMerchantSubscriptions");
|
|
4472
7520
|
function register3(deps) {
|
|
4473
7521
|
const { ctx, config, client, merchantSubMgr } = deps;
|
|
4474
|
-
ctx.command(TEXT.merchant, "查看远行商人商品").action(async ({ session }) => {
|
|
7522
|
+
ctx.command(TEXT.merchant, "查看远行商人商品").alias("yxsr").action(async ({ session }) => {
|
|
4475
7523
|
const res = await client.getMerchantInfo(ctx, true);
|
|
4476
7524
|
if (!res) return `获取远行商人数据失败:${client.getLastErrorBrief()}`;
|
|
4477
|
-
const { data, fallback } =
|
|
4478
|
-
const png = await deps.renderer.renderHtml(ctx,
|
|
7525
|
+
const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
7526
|
+
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
4479
7527
|
await sendImageWithFallback(session, png, fallback, "merchant:yuanxing-shangren", deps.config);
|
|
4480
7528
|
});
|
|
7529
|
+
ctx.command(TEXT.todayMerchant, "查看今日远行商人全部商品").action(async ({ session }) => {
|
|
7530
|
+
const res = await client.getMerchantInfo(ctx, true);
|
|
7531
|
+
if (!res) return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
|
|
7532
|
+
const { data, fallback, templateName } = buildConfiguredTodayMerchantRenderPayload(deps, res);
|
|
7533
|
+
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
7534
|
+
await sendImageWithFallback(session, png, fallback, "merchant:yuanxing-shangren:today", deps.config);
|
|
7535
|
+
});
|
|
4481
7536
|
ctx.command(`${TEXT.subscribe} [args:text]`, "订阅远行商人商品提醒").action(async ({ session }, args) => {
|
|
4482
7537
|
const target = getSubscriptionTarget(session);
|
|
4483
7538
|
if (!target.privateChat && !isBotAdmin2(session, config.adminUserIds)) return "此指令仅限管理员使用。";
|
|
@@ -4523,6 +7578,13 @@ function register3(deps) {
|
|
|
4523
7578
|
return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
|
|
4524
7579
|
});
|
|
4525
7580
|
if (config.merchantSubscriptionEnabled) {
|
|
7581
|
+
const MERCHANT_JITTER_MS = 3e4;
|
|
7582
|
+
const runCheckWithJitter = /* @__PURE__ */ __name(() => {
|
|
7583
|
+
const jitter = Math.floor(Math.random() * MERCHANT_JITTER_MS);
|
|
7584
|
+
setTimeout(() => {
|
|
7585
|
+
checkMerchantSubscriptions(deps).catch((err) => logger11.warn(`远行商人订阅检查失败: ${err}`));
|
|
7586
|
+
}, jitter);
|
|
7587
|
+
}, "runCheckWithJitter");
|
|
4526
7588
|
if (config.merchantCheckMode === "times" && config.merchantCheckTimes.length > 0) {
|
|
4527
7589
|
let lastMerchantCheckKey = "";
|
|
4528
7590
|
ctx.setInterval(async () => {
|
|
@@ -4532,11 +7594,11 @@ function register3(deps) {
|
|
|
4532
7594
|
const checkKey = `${now.toDateString()}-${timeStr}`;
|
|
4533
7595
|
if (checkKey === lastMerchantCheckKey) return;
|
|
4534
7596
|
lastMerchantCheckKey = checkKey;
|
|
4535
|
-
|
|
7597
|
+
runCheckWithJitter();
|
|
4536
7598
|
}, 6e4);
|
|
4537
7599
|
} else {
|
|
4538
7600
|
ctx.setInterval(async () => {
|
|
4539
|
-
|
|
7601
|
+
runCheckWithJitter();
|
|
4540
7602
|
}, config.merchantCheckInterval);
|
|
4541
7603
|
}
|
|
4542
7604
|
}
|
|
@@ -4544,11 +7606,156 @@ function register3(deps) {
|
|
|
4544
7606
|
__name(register3, "register");
|
|
4545
7607
|
|
|
4546
7608
|
// src/commands/wiki.ts
|
|
4547
|
-
var
|
|
7609
|
+
var import_koishi12 = require("koishi");
|
|
7610
|
+
var import_node_fs6 = __toESM(require("node:fs"));
|
|
7611
|
+
var logger12 = new import_koishi12.Logger("rocom-wiki-cmd");
|
|
7612
|
+
async function sendImage2(deps, session, templateName, data, fallback) {
|
|
7613
|
+
const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
|
|
7614
|
+
await sendImageWithFallback(session, png, fallback, `wiki:${templateName}`, deps.config);
|
|
7615
|
+
}
|
|
7616
|
+
__name(sendImage2, "sendImage");
|
|
7617
|
+
async function handleWikiQuery(deps, session, rawText) {
|
|
7618
|
+
const { ctx, wikiService } = deps;
|
|
7619
|
+
const trimmed = String(rawText || "").trim();
|
|
7620
|
+
const rawKey = trimmed.toLowerCase();
|
|
7621
|
+
const catalogsMeta = await wikiService.getCatalogsPayload();
|
|
7622
|
+
if (!trimmed || ["帮助", "help", "类型", "目录", "专题"].includes(rawKey)) {
|
|
7623
|
+
const optionsMeta = await wikiService.getOptionsPayload();
|
|
7624
|
+
const data2 = wikiService.buildCatalogRenderData(catalogsMeta, optionsMeta);
|
|
7625
|
+
await sendImage2(deps, session, "wiki/menu", data2, wikiService.catalogUsageText());
|
|
7626
|
+
return;
|
|
7627
|
+
}
|
|
7628
|
+
let { catalog, query, pageNo } = wikiService.parseWikiCommand(trimmed);
|
|
7629
|
+
if (catalog) {
|
|
7630
|
+
catalog = wikiService.catalogForKeyFromPayload(String(catalog.key || ""), catalogsMeta) || catalog;
|
|
7631
|
+
}
|
|
7632
|
+
if (!catalog) {
|
|
7633
|
+
const { parts, pageNo: parsedPageNo } = splitWikiCommandParts(trimmed);
|
|
7634
|
+
if (parts.length) {
|
|
7635
|
+
const dynamicCatalog = wikiService.dynamicCatalogByToken(parts[0], catalogsMeta);
|
|
7636
|
+
if (dynamicCatalog) {
|
|
7637
|
+
catalog = dynamicCatalog;
|
|
7638
|
+
query = parts.slice(1).join(" ").trim();
|
|
7639
|
+
pageNo = parsedPageNo;
|
|
7640
|
+
}
|
|
7641
|
+
}
|
|
7642
|
+
}
|
|
7643
|
+
if (!catalog) {
|
|
7644
|
+
const { payload: payload2, mode: mode2, error: error2 } = await wikiService.fetchGlobalSearch(query, pageNo);
|
|
7645
|
+
if (error2) return error2;
|
|
7646
|
+
if (mode2 === "global-detail") {
|
|
7647
|
+
const sourceCatalog = payload2?._catalog || {};
|
|
7648
|
+
const detail = payload2?.item && typeof payload2.item === "object" ? payload2.item : {};
|
|
7649
|
+
const sourceKey = String(sourceCatalog?.key || "");
|
|
7650
|
+
if (sourceKey === "pets") {
|
|
7651
|
+
const sections = await wikiService.fetchWikiPetSections(detail.pet_id);
|
|
7652
|
+
const data4 = buildWikiPetRenderData(detail, sections.profile, sections.skills, sections.family, sections.handbook, query);
|
|
7653
|
+
await sendImage2(deps, session, "wiki/pet", data4, [
|
|
7654
|
+
`${data4.name} #${data4.number}`,
|
|
7655
|
+
`属性:${data4.type_names.join(" / ") || "暂无"}`,
|
|
7656
|
+
`蛋组:${data4.egg_groups.join(" / ") || "暂无"}`,
|
|
7657
|
+
data4.description
|
|
7658
|
+
].join("\n"));
|
|
7659
|
+
return;
|
|
7660
|
+
}
|
|
7661
|
+
if (sourceKey === "skills") {
|
|
7662
|
+
const pets = await deps.client.getWikiSkillPets(ctx, detail.skill_id);
|
|
7663
|
+
const data4 = buildWikiSkillRenderData(detail, pets && typeof pets === "object" ? pets : {}, query);
|
|
7664
|
+
await sendImage2(deps, session, "wiki/skill", data4, `${data4.name} #${data4.skill_id}
|
|
7665
|
+
${data4.description}`);
|
|
7666
|
+
return;
|
|
7667
|
+
}
|
|
7668
|
+
const data3 = wikiService.buildGenericRenderData(sourceCatalog?.key ? sourceCatalog : { title: "全局搜索", key: "global" }, detail, query, "detail", pageNo);
|
|
7669
|
+
await sendImage2(deps, session, "wiki/detail", data3, `${data3.title}
|
|
7670
|
+
${data3.summary || ""}`);
|
|
7671
|
+
return;
|
|
7672
|
+
}
|
|
7673
|
+
const data2 = wikiService.buildGenericRenderData({ title: "全局搜索", key: "global" }, payload2, query, "global-search", pageNo);
|
|
7674
|
+
await sendImage2(deps, session, "wiki/list", data2, `${data2.title}
|
|
7675
|
+
${data2.summary || ""}`);
|
|
7676
|
+
return;
|
|
7677
|
+
}
|
|
7678
|
+
if (["pets", "pet"].includes(catalog.key) && query) {
|
|
7679
|
+
const { detail: overview, candidates, error: error2 } = await wikiService.resolveWikiPet(query);
|
|
7680
|
+
if (error2) return error2;
|
|
7681
|
+
if (candidates.length) return wikiCandidateText(query, candidates, "精灵");
|
|
7682
|
+
if (!overview) return "获取 Wiki 精灵详情失败:接口未返回有效数据。";
|
|
7683
|
+
const sections = await wikiService.fetchWikiPetSections(overview.pet_id);
|
|
7684
|
+
const data2 = buildWikiPetRenderData(overview, sections.profile, sections.skills, sections.family, sections.handbook, query);
|
|
7685
|
+
await sendImage2(deps, session, "wiki/pet", data2, `${data2.name} #${data2.number}
|
|
7686
|
+
${data2.description}`);
|
|
7687
|
+
return;
|
|
7688
|
+
}
|
|
7689
|
+
if (["skills", "skill"].includes(catalog.key) && query) {
|
|
7690
|
+
const { detail, candidates, error: error2 } = await wikiService.resolveWikiSkill(query);
|
|
7691
|
+
if (error2) return error2;
|
|
7692
|
+
if (candidates.length) return wikiCandidateText(query, candidates, "技能");
|
|
7693
|
+
if (!detail) return "获取技能详情失败:接口未返回有效数据。";
|
|
7694
|
+
const pets = await deps.client.getWikiSkillPets(ctx, detail.skill_id);
|
|
7695
|
+
const data2 = buildWikiSkillRenderData(detail, pets && typeof pets === "object" ? pets : {}, query);
|
|
7696
|
+
await sendImage2(deps, session, "wiki/skill", data2, `${data2.name} #${data2.skill_id}
|
|
7697
|
+
${data2.description}`);
|
|
7698
|
+
return;
|
|
7699
|
+
}
|
|
7700
|
+
const { payload, mode, error } = await wikiService.fetchGenericCatalog(catalog, query, pageNo);
|
|
7701
|
+
if (error) return error;
|
|
7702
|
+
const data = wikiService.buildGenericRenderData(catalog, payload, query, mode, pageNo);
|
|
7703
|
+
let templateName = "wiki/detail";
|
|
7704
|
+
if (mode === "list") templateName = "wiki/list";
|
|
7705
|
+
else if (mode === "suggestions") templateName = "wiki/suggestions";
|
|
7706
|
+
await sendImage2(deps, session, templateName, data, `${data.title}
|
|
7707
|
+
${data.summary || ""}`);
|
|
7708
|
+
}
|
|
7709
|
+
__name(handleWikiQuery, "handleWikiQuery");
|
|
4548
7710
|
function register4(deps) {
|
|
4549
|
-
const { ctx } = deps;
|
|
4550
|
-
ctx.command("洛克").subcommand(".wiki
|
|
4551
|
-
ctx.command("洛克").subcommand(".技能 <name:text>", "查询技能
|
|
7711
|
+
const { ctx, atlasService } = deps;
|
|
7712
|
+
ctx.command("洛克").subcommand(".wiki [name:text]", "查询洛克 Wiki(精灵/技能/物品等,支持全局搜索)").alias("洛克wiki").alias("洛克百科").action(async ({ session }, name2 = "") => handleWikiQuery(deps, session, name2));
|
|
7713
|
+
ctx.command("洛克").subcommand(".技能 <name:text>", "查询技能 Wiki").alias("洛克技能").action(async ({ session }, name2 = "") => handleWikiQuery(deps, session, `技能 ${String(name2 || "").trim()}`));
|
|
7714
|
+
ctx.command("图鉴下载", "下载 Rocom-Atlas 精灵图鉴到本地缓存").alias("洛克图鉴下载").action(async ({ session }) => {
|
|
7715
|
+
if (!deps.config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
|
|
7716
|
+
await session?.send?.("开始下载 Rocom-Atlas 图鉴,请稍候…");
|
|
7717
|
+
let lastPercent = -20;
|
|
7718
|
+
try {
|
|
7719
|
+
const { imageCount, totalBytes } = await atlasService.download(ctx, async (percent, stage) => {
|
|
7720
|
+
if (percent - lastPercent >= 20 || percent >= 100) {
|
|
7721
|
+
lastPercent = percent;
|
|
7722
|
+
await session?.send?.(`图鉴下载进度 ${percent}%:${stage}`);
|
|
7723
|
+
}
|
|
7724
|
+
});
|
|
7725
|
+
const sizeMb = (totalBytes / 1024 / 1024).toFixed(1);
|
|
7726
|
+
return `图鉴下载完成:共 ${imageCount} 张精灵图片,占用约 ${sizeMb} MB。
|
|
7727
|
+
使用:精灵图鉴 <精灵名>`;
|
|
7728
|
+
} catch (e) {
|
|
7729
|
+
logger12.warn(`图鉴下载失败: ${e}`);
|
|
7730
|
+
return `图鉴下载失败:${e instanceof Error ? e.message : e}`;
|
|
7731
|
+
}
|
|
7732
|
+
});
|
|
7733
|
+
ctx.command("精灵图鉴 <name:text>", "使用本地 Rocom-Atlas 查询精灵图鉴图片").alias("洛克精灵图鉴").action(async ({ session }, name2 = "") => {
|
|
7734
|
+
const query = String(name2 || "").trim();
|
|
7735
|
+
if (!query) return "请输入精灵名称。用法:精灵图鉴 <精灵名>";
|
|
7736
|
+
if (!atlasService.isReady()) {
|
|
7737
|
+
return "本地图鉴尚未下载。请管理员先执行:图鉴下载";
|
|
7738
|
+
}
|
|
7739
|
+
const { name: matchedName, imagePath, candidates } = atlasService.findMatch(query);
|
|
7740
|
+
if (imagePath) {
|
|
7741
|
+
try {
|
|
7742
|
+
const image = import_node_fs6.default.readFileSync(imagePath);
|
|
7743
|
+
await session?.send?.((0, import_koishi12.h)("message", {}, import_koishi12.h.text(`【精灵图鉴】${matchedName}
|
|
7744
|
+
`), import_koishi12.h.image(image, "image/png")));
|
|
7745
|
+
return;
|
|
7746
|
+
} catch (e) {
|
|
7747
|
+
logger12.warn(`读取图鉴图片失败: ${e}`);
|
|
7748
|
+
return `读取图鉴图片失败:${e instanceof Error ? e.message : e}`;
|
|
7749
|
+
}
|
|
7750
|
+
}
|
|
7751
|
+
if (candidates.length) {
|
|
7752
|
+
const lines = [`未精确匹配「${query}」,可能是:`];
|
|
7753
|
+
candidates.forEach((item, idx) => lines.push(`${idx + 1}. ${item}`));
|
|
7754
|
+
lines.push("请使用更精确名称重新查询。");
|
|
7755
|
+
return lines.join("\n");
|
|
7756
|
+
}
|
|
7757
|
+
return `未在本地图鉴中找到「${query}」。可尝试其他名称,或联系管理员更新图鉴(图鉴下载)。`;
|
|
7758
|
+
});
|
|
4552
7759
|
}
|
|
4553
7760
|
__name(register4, "register");
|
|
4554
7761
|
|
|
@@ -4743,7 +7950,7 @@ function register5(deps) {
|
|
|
4743
7950
|
__name(register5, "register");
|
|
4744
7951
|
|
|
4745
7952
|
// src/commands/tools.ts
|
|
4746
|
-
var
|
|
7953
|
+
var import_koishi13 = require("koishi");
|
|
4747
7954
|
|
|
4748
7955
|
// src/activities-service.ts
|
|
4749
7956
|
var CHINA_TIMEZONE2 = "Asia/Shanghai";
|
|
@@ -5031,7 +8238,7 @@ var ActivitiesService = class {
|
|
|
5031
8238
|
};
|
|
5032
8239
|
|
|
5033
8240
|
// src/commands/tools.ts
|
|
5034
|
-
var
|
|
8241
|
+
var logger13 = new import_koishi13.Logger("rocom-tools");
|
|
5035
8242
|
var activitiesService = new ActivitiesService();
|
|
5036
8243
|
function trimText2(value) {
|
|
5037
8244
|
return String(value ?? "").trim();
|
|
@@ -5070,6 +8277,63 @@ function announcementId(item) {
|
|
|
5070
8277
|
return trimText2(item?.thread_id || item?.id);
|
|
5071
8278
|
}
|
|
5072
8279
|
__name(announcementId, "announcementId");
|
|
8280
|
+
function announcementTimestamp(item) {
|
|
8281
|
+
for (const key of ["published_at_ts", "publish_at_ts", "created_at_ts"]) {
|
|
8282
|
+
const value = Number(item?.[key]);
|
|
8283
|
+
if (Number.isFinite(value) && value > 0) return value;
|
|
8284
|
+
}
|
|
8285
|
+
for (const key of ["publishAt", "published_at", "createdAt"]) {
|
|
8286
|
+
const text = trimText2(item?.[key]);
|
|
8287
|
+
if (!text) continue;
|
|
8288
|
+
const date = new Date(text);
|
|
8289
|
+
if (!Number.isNaN(date.getTime())) return Math.floor(date.getTime() / 1e3);
|
|
8290
|
+
}
|
|
8291
|
+
return 0;
|
|
8292
|
+
}
|
|
8293
|
+
__name(announcementTimestamp, "announcementTimestamp");
|
|
8294
|
+
function announcementSubscriptionKey(session) {
|
|
8295
|
+
const privateChat = !session?.guildId;
|
|
8296
|
+
return [session?.platform || "", session?.channelId || "private", privateChat ? session?.userId || "" : "", "announcement"].join(":");
|
|
8297
|
+
}
|
|
8298
|
+
__name(announcementSubscriptionKey, "announcementSubscriptionKey");
|
|
8299
|
+
async function checkAnnouncementSubscriptions(deps) {
|
|
8300
|
+
const { ctx, client, announcementSubMgr } = deps;
|
|
8301
|
+
const subs = announcementSubMgr.getAll();
|
|
8302
|
+
const subEntries = Object.entries(subs);
|
|
8303
|
+
if (!subEntries.length) return { subscriptions: 0, pushed: 0 };
|
|
8304
|
+
const latest = await client.getLatestAnnouncement(ctx, { category_id: 99 });
|
|
8305
|
+
const item = latest?.detail || latest?.announcement || latest;
|
|
8306
|
+
const latestId = announcementId(item);
|
|
8307
|
+
if (!latestId) return { subscriptions: subEntries.length, pushed: 0 };
|
|
8308
|
+
const latestTs = announcementTimestamp(item);
|
|
8309
|
+
let pushed = 0;
|
|
8310
|
+
for (const [key, sub] of subEntries) {
|
|
8311
|
+
if (latestId === String(sub.last_id || "")) continue;
|
|
8312
|
+
if (latestTs && sub.since_ts && latestTs <= sub.since_ts) continue;
|
|
8313
|
+
const message = [`【洛克王国新公告】`, buildAnnouncementDetailText(latest)].join("\n");
|
|
8314
|
+
try {
|
|
8315
|
+
const sent = await sendScheduledMessage(ctx, {
|
|
8316
|
+
platform: sub.platform,
|
|
8317
|
+
channelId: sub.channel_id || sub.guild_id || sub.user_id || "",
|
|
8318
|
+
guildId: sub.guild_id || "",
|
|
8319
|
+
userId: sub.user_id || ""
|
|
8320
|
+
}, message);
|
|
8321
|
+
if (!sent) continue;
|
|
8322
|
+
} catch (e) {
|
|
8323
|
+
logger13.warn(`公告订阅推送失败: ${e}`);
|
|
8324
|
+
continue;
|
|
8325
|
+
}
|
|
8326
|
+
pushed++;
|
|
8327
|
+
announcementSubMgr.upsert(key, {
|
|
8328
|
+
...sub,
|
|
8329
|
+
last_id: latestId,
|
|
8330
|
+
since_ts: latestTs || Math.floor(Date.now() / 1e3),
|
|
8331
|
+
updated_at: Math.floor(Date.now() / 1e3)
|
|
8332
|
+
});
|
|
8333
|
+
}
|
|
8334
|
+
return { subscriptions: subEntries.length, pushed };
|
|
8335
|
+
}
|
|
8336
|
+
__name(checkAnnouncementSubscriptions, "checkAnnouncementSubscriptions");
|
|
5073
8337
|
function buildAnnouncementListText(data, page) {
|
|
5074
8338
|
const list = firstArray(data, ["list", "items"]);
|
|
5075
8339
|
if (!list.length) return page > 1 ? "该页没有更多公告。" : "当前没有公告数据。";
|
|
@@ -5145,11 +8409,45 @@ function register6(deps) {
|
|
|
5145
8409
|
if (!data) return `公告详情查询失败:${client.getLastErrorBrief()}`;
|
|
5146
8410
|
return buildAnnouncementDetailText(data);
|
|
5147
8411
|
});
|
|
8412
|
+
ctx.command("订阅洛克公告", "订阅洛克王国新公告推送").action(async ({ session }) => {
|
|
8413
|
+
const privateChat = !session?.guildId;
|
|
8414
|
+
if (!privateChat && !config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
|
|
8415
|
+
const key = announcementSubscriptionKey(session);
|
|
8416
|
+
const latest = await client.getLatestAnnouncement(ctx, { category_id: 99 }, session?.userId || "");
|
|
8417
|
+
const item = latest?.detail || latest?.announcement || latest;
|
|
8418
|
+
deps.announcementSubMgr.upsert(key, {
|
|
8419
|
+
key,
|
|
8420
|
+
platform: session?.platform || "",
|
|
8421
|
+
channel_id: session?.channelId || "",
|
|
8422
|
+
guild_id: session?.guildId || "",
|
|
8423
|
+
user_id: privateChat ? session?.userId || "" : "",
|
|
8424
|
+
updated_by: session?.userId || "",
|
|
8425
|
+
last_id: announcementId(item),
|
|
8426
|
+
since_ts: announcementTimestamp(item) || Math.floor(Date.now() / 1e3),
|
|
8427
|
+
updated_at: Math.floor(Date.now() / 1e3)
|
|
8428
|
+
});
|
|
8429
|
+
return "已订阅洛克王国新公告推送,检测到新公告时会在当前会话提醒。";
|
|
8430
|
+
});
|
|
8431
|
+
ctx.command("取消订阅洛克公告", "取消洛克王国新公告推送").action(async ({ session }) => {
|
|
8432
|
+
const privateChat = !session?.guildId;
|
|
8433
|
+
if (!privateChat && !config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
|
|
8434
|
+
const deleted = deps.announcementSubMgr.deleteMatching({
|
|
8435
|
+
platform: session?.platform || "",
|
|
8436
|
+
channelId: session?.channelId || "",
|
|
8437
|
+
userId: privateChat ? session?.userId || "" : ""
|
|
8438
|
+
});
|
|
8439
|
+
return deleted ? "已取消洛克公告订阅。" : "当前会话没有洛克公告订阅。";
|
|
8440
|
+
});
|
|
8441
|
+
if (config.announcementSubscriptionEnabled) {
|
|
8442
|
+
ctx.setInterval(() => {
|
|
8443
|
+
checkAnnouncementSubscriptions(deps).catch((err) => logger13.warn(`公告订阅检查失败: ${err}`));
|
|
8444
|
+
}, Math.max(1, config.announcementPollIntervalMinutes || 10) * 6e4);
|
|
8445
|
+
}
|
|
5148
8446
|
ctx.command("洛克").subcommand(".同步配置", "手动同步 RoCom 远端配置").alias("洛克同步配置").action(async ({ session }) => {
|
|
5149
8447
|
if (!config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
|
|
5150
8448
|
const data = await client.syncConfig(ctx, session?.userId || "");
|
|
5151
8449
|
if (!data) return `同步配置失败:${client.getLastErrorBrief()}`;
|
|
5152
|
-
|
|
8450
|
+
logger13.info(`manual config sync requested by ${session?.userId || "unknown"}`);
|
|
5153
8451
|
const skipped = Array.isArray(data?.skipped_resources) ? data.skipped_resources.length : 0;
|
|
5154
8452
|
return skipped ? `配置同步完成,但有 ${skipped} 个资源跳过。` : "配置同步完成。";
|
|
5155
8453
|
});
|
|
@@ -5157,8 +8455,8 @@ function register6(deps) {
|
|
|
5157
8455
|
__name(register6, "register");
|
|
5158
8456
|
|
|
5159
8457
|
// src/commands/admin.ts
|
|
5160
|
-
var
|
|
5161
|
-
var
|
|
8458
|
+
var import_koishi14 = require("koishi");
|
|
8459
|
+
var logger14 = new import_koishi14.Logger("rocom-admin");
|
|
5162
8460
|
function register7(deps) {
|
|
5163
8461
|
const { ctx, config, client, userMgr } = deps;
|
|
5164
8462
|
function isAdmin(userId) {
|
|
@@ -5204,7 +8502,7 @@ function register7(deps) {
|
|
|
5204
8502
|
for (const [userId, bindings] of Object.entries(allUsers)) {
|
|
5205
8503
|
const token = await getRoleToken(ctx, userId);
|
|
5206
8504
|
if (!token?.fwt) {
|
|
5207
|
-
|
|
8505
|
+
logger14.warn(`用户 ${userId} 没有可用的 fwt,跳过失效检测`);
|
|
5208
8506
|
totalSkipped += bindings.length;
|
|
5209
8507
|
continue;
|
|
5210
8508
|
}
|
|
@@ -5231,7 +8529,7 @@ function register7(deps) {
|
|
|
5231
8529
|
try {
|
|
5232
8530
|
await client.deleteBinding(ctx, binding.binding_id, userId);
|
|
5233
8531
|
} catch (e) {
|
|
5234
|
-
|
|
8532
|
+
logger14.warn(`删除用户 ${userId} 服务端绑定 ${binding.binding_id} 失败:${e}`);
|
|
5235
8533
|
}
|
|
5236
8534
|
}
|
|
5237
8535
|
totalInvalid++;
|
|
@@ -5270,7 +8568,7 @@ function register7(deps) {
|
|
|
5270
8568
|
});
|
|
5271
8569
|
}
|
|
5272
8570
|
} catch (e) {
|
|
5273
|
-
|
|
8571
|
+
logger14.warn(`自动刷新用户 ${userId} 失败: ${e}`);
|
|
5274
8572
|
}
|
|
5275
8573
|
}
|
|
5276
8574
|
}, 3e4);
|
|
@@ -5281,7 +8579,7 @@ __name(register7, "register");
|
|
|
5281
8579
|
// src/index.ts
|
|
5282
8580
|
var name = "rocom";
|
|
5283
8581
|
var inject = { required: ["puppeteer", "database"] };
|
|
5284
|
-
var
|
|
8582
|
+
var logger15 = new import_koishi15.Logger("rocom");
|
|
5285
8583
|
var MENU_PAGE_TITLE = "洛克王国帮助菜单";
|
|
5286
8584
|
var MENU_PAGE_SUBTITLE = "输入对应命令可查看功能,部分数据查询需要先完成账号绑定。";
|
|
5287
8585
|
var MENU_GROUPS = [
|
|
@@ -5309,6 +8607,7 @@ var MENU_GROUPS = [
|
|
|
5309
8607
|
{ cmd: "洛克.交换大厅", desc: "查看交换大厅" },
|
|
5310
8608
|
{ cmd: "洛克.玩家", desc: "查询 ingame 玩家资料" },
|
|
5311
8609
|
{ cmd: "洛克.家园", desc: "查询家园菜园" },
|
|
8610
|
+
{ cmd: "洛克.家园详情", desc: "家园精灵完整数据" },
|
|
5312
8611
|
{ cmd: "洛克.商店", desc: "查询 ingame 商店" },
|
|
5313
8612
|
{ cmd: "洛克.日历", desc: "查看活动日历" },
|
|
5314
8613
|
{ cmd: "洛克.公告", desc: "查看公告列表" },
|
|
@@ -5324,14 +8623,19 @@ var MENU_GROUPS = [
|
|
|
5324
8623
|
{ cmd: "取消订阅远行商人", desc: "取消商人提醒" },
|
|
5325
8624
|
{ cmd: "订阅家园菜园", desc: "菜园成熟提醒" },
|
|
5326
8625
|
{ cmd: "订阅家园灵感", desc: "精灵灵感提醒" },
|
|
5327
|
-
{ cmd: "
|
|
8626
|
+
{ cmd: "订阅家园生蛋", desc: "精灵生蛋提醒" },
|
|
8627
|
+
{ cmd: "取消订阅家园", desc: "取消家园订阅" },
|
|
8628
|
+
{ cmd: "订阅洛克公告", desc: "新公告推送提醒" },
|
|
8629
|
+
{ cmd: "取消订阅洛克公告", desc: "取消公告订阅" }
|
|
5328
8630
|
]
|
|
5329
8631
|
},
|
|
5330
8632
|
{
|
|
5331
8633
|
groupTitle: "百科与查蛋",
|
|
5332
8634
|
menuItems: [
|
|
5333
|
-
{ cmd: "洛克.wiki", desc: "
|
|
8635
|
+
{ cmd: "洛克.wiki", desc: "Wiki 全局/分类查询" },
|
|
5334
8636
|
{ cmd: "洛克.技能", desc: "技能 Wiki 查询" },
|
|
8637
|
+
{ cmd: "精灵图鉴", desc: "本地图鉴查图" },
|
|
8638
|
+
{ cmd: "图鉴下载", desc: "下载图鉴缓存(管理员)" },
|
|
5335
8639
|
{ cmd: "洛克.查蛋", desc: "精灵查蛋 / 尺寸反查" },
|
|
5336
8640
|
{ cmd: "洛克.配种", desc: "配种查询" }
|
|
5337
8641
|
]
|
|
@@ -5357,50 +8661,69 @@ function buildMenuFallbackText() {
|
|
|
5357
8661
|
return lines.join("\n").trimEnd();
|
|
5358
8662
|
}
|
|
5359
8663
|
__name(buildMenuFallbackText, "buildMenuFallbackText");
|
|
5360
|
-
var Config =
|
|
5361
|
-
|
|
5362
|
-
apiBaseUrl:
|
|
5363
|
-
wegameApiKey:
|
|
5364
|
-
qqLoginDebugMode:
|
|
5365
|
-
adminUserIds:
|
|
5366
|
-
autoRefreshEnabled:
|
|
5367
|
-
autoRefreshTime:
|
|
8664
|
+
var Config = import_koishi15.Schema.intersect([
|
|
8665
|
+
import_koishi15.Schema.object({
|
|
8666
|
+
apiBaseUrl: import_koishi15.Schema.string().default("https://wegame.shallow.ink").description("API 基础地址"),
|
|
8667
|
+
wegameApiKey: import_koishi15.Schema.string().default("").description("WeGame API Key(获取 key 查看 Github:https://github.com/Entropy-Increase-Team/koishi-plugin-rocom )"),
|
|
8668
|
+
qqLoginDebugMode: import_koishi15.Schema.boolean().default(false).description("QQ 扫码登录调试模式,仅调试时开启"),
|
|
8669
|
+
adminUserIds: import_koishi15.Schema.array(String).default([]).description("管理员用户 ID 列表"),
|
|
8670
|
+
autoRefreshEnabled: import_koishi15.Schema.boolean().default(false).description("启用自动刷新凭证"),
|
|
8671
|
+
autoRefreshTime: import_koishi15.Schema.array(String).default(["00:00", "12:00"]).description("自动刷新时间")
|
|
5368
8672
|
}).description("基础设置"),
|
|
5369
|
-
|
|
5370
|
-
imageCompressionEnabled:
|
|
5371
|
-
imageCompressionMinBytes:
|
|
5372
|
-
imageCompressionLevel:
|
|
8673
|
+
import_koishi15.Schema.object({
|
|
8674
|
+
imageCompressionEnabled: import_koishi15.Schema.boolean().default(true).description("发送图片前启用 PNG 无损压缩"),
|
|
8675
|
+
imageCompressionMinBytes: import_koishi15.Schema.number().default(262144).description("触发压缩的最小图片大小,单位字节"),
|
|
8676
|
+
imageCompressionLevel: import_koishi15.Schema.number().min(0).max(9).default(9).description("PNG zlib 压缩等级 0-9")
|
|
5373
8677
|
}).description("图片压缩设置"),
|
|
5374
|
-
|
|
5375
|
-
merchantSubscriptionEnabled:
|
|
5376
|
-
merchantSubscriptionItems:
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
8678
|
+
import_koishi15.Schema.object({
|
|
8679
|
+
merchantSubscriptionEnabled: import_koishi15.Schema.boolean().default(true).description("启用远行商人订阅"),
|
|
8680
|
+
merchantSubscriptionItems: import_koishi15.Schema.array(String).default(["国王球", "棱镜球", "炫彩精灵蛋"]).description("默认订阅商品"),
|
|
8681
|
+
merchantUiStyle: import_koishi15.Schema.union(["new", "old"]).default("new").description("远行商人 UI 样式:new 为新版大图卡片,old 为旧版列表卡片"),
|
|
8682
|
+
merchantCheckMode: import_koishi15.Schema.union(["interval", "times"]).default("interval").description("商人检查模式:interval 定期轮询 / times 指定时间点"),
|
|
8683
|
+
merchantCheckInterval: import_koishi15.Schema.number().default(3e5).description("商人检查间隔,单位毫秒(仅 interval 模式生效)"),
|
|
8684
|
+
merchantCheckTimes: import_koishi15.Schema.array(String).default([]).description("商人定时检查时间点(HH:MM 格式,如 08:00,仅 times 模式生效)"),
|
|
8685
|
+
merchantPrivateSubscriptionEnabled: import_koishi15.Schema.boolean().default(true).description("允许个人私聊订阅远行商人推送"),
|
|
8686
|
+
homeSubscriptionEnabled: import_koishi15.Schema.boolean().default(true).description("启用家园菜园和灵感订阅推送"),
|
|
8687
|
+
homeSubscriptionIntervalMinutes: import_koishi15.Schema.number().default(5).description("家园订阅检查间隔,单位分钟"),
|
|
8688
|
+
announcementSubscriptionEnabled: import_koishi15.Schema.boolean().default(true).description("启用洛克公告订阅推送"),
|
|
8689
|
+
announcementPollIntervalMinutes: import_koishi15.Schema.number().default(10).description("公告订阅检查间隔,单位分钟")
|
|
8690
|
+
}).description("订阅推送设置"),
|
|
8691
|
+
import_koishi15.Schema.object({
|
|
8692
|
+
homeQueryWaitMs: import_koishi15.Schema.number().default(5e3).description("家园查询服务端同步等待毫秒(long-poll,超过此时间未出结果则转入排队)"),
|
|
8693
|
+
homeQueryPollIntervalMs: import_koishi15.Schema.number().default(3e3).description("家园查询进入排队后的轮询间隔,单位毫秒"),
|
|
8694
|
+
homeQueryTimeoutMs: import_koishi15.Schema.number().default(18e4).description("家园查询排队等候的总超时,单位毫秒,超时后提示稍后重试"),
|
|
8695
|
+
lowBandwidthMode: import_koishi15.Schema.boolean().default(false).description("低带宽模式:家园详情不再加载技能图标,降低长图生成压力")
|
|
8696
|
+
}).description("家园查询排队设置"),
|
|
8697
|
+
import_koishi15.Schema.object({
|
|
8698
|
+
atlasZipUrls: import_koishi15.Schema.array(String).default([
|
|
8699
|
+
"https://codeload.github.com/Entropy-Increase-Team/Rocom-Atlas/zip/refs/heads/main",
|
|
8700
|
+
"https://github.com/Entropy-Increase-Team/Rocom-Atlas/archive/refs/heads/main.zip"
|
|
8701
|
+
]).description("图鉴压缩包下载地址列表(按顺序尝试,均失败后转 git clone)"),
|
|
8702
|
+
atlasGitUrl: import_koishi15.Schema.string().default("https://github.com/Entropy-Increase-Team/Rocom-Atlas.git").description("图鉴 git clone 地址(压缩包下载失败时回退使用)")
|
|
8703
|
+
}).description("图鉴下载设置")
|
|
5384
8704
|
]);
|
|
5385
8705
|
function apply(ctx, config) {
|
|
5386
8706
|
setupRoleTokenModel(ctx);
|
|
5387
8707
|
const client = new RocomClient(config.apiBaseUrl, config.wegameApiKey);
|
|
5388
|
-
const dataDir =
|
|
8708
|
+
const dataDir = import_node_path6.default.join(ctx.baseDir, "data", "rocom");
|
|
5389
8709
|
const userMgr = new UserManager(dataDir);
|
|
5390
8710
|
const merchantSubMgr = new MerchantSubscriptionManager(dataDir);
|
|
5391
8711
|
const homeSubMgr = new HomeSubscriptionManager(dataDir);
|
|
5392
|
-
const
|
|
8712
|
+
const announcementSubMgr = new AnnouncementSubscriptionManager(dataDir);
|
|
8713
|
+
const resPath = import_node_path6.default.resolve(__dirname, "..");
|
|
5393
8714
|
const renderer = new Renderer(resPath);
|
|
5394
|
-
const renderTemplateRoot =
|
|
5395
|
-
const searcheggsDir =
|
|
8715
|
+
const renderTemplateRoot = import_node_fs7.default.existsSync(import_node_path6.default.join(resPath, "lib", "render-templates")) ? import_node_path6.default.join(resPath, "lib", "render-templates") : import_node_path6.default.join(resPath, "src", "render-templates");
|
|
8716
|
+
const searcheggsDir = import_node_path6.default.join(renderTemplateRoot, "searcheggs");
|
|
5396
8717
|
const eggService = new EggService(searcheggsDir);
|
|
5397
|
-
const
|
|
8718
|
+
const wikiService = new WikiService(ctx, client);
|
|
8719
|
+
const atlasService = new AtlasService(dataDir, { zipUrls: config.atlasZipUrls, gitUrl: config.atlasGitUrl });
|
|
8720
|
+
const deps = { ctx, config, client, userMgr, merchantSubMgr, homeSubMgr, announcementSubMgr, eggService, renderer, wikiService, atlasService };
|
|
5398
8721
|
ctx.on("ready", () => {
|
|
5399
8722
|
migrateRoleTokensToUserId(ctx).catch((err) => {
|
|
5400
|
-
|
|
8723
|
+
logger15.warn(`role token migration failed: ${err}`);
|
|
5401
8724
|
});
|
|
5402
8725
|
migrateLegacyFrameworkTokens(ctx, userMgr).catch((err) => {
|
|
5403
|
-
|
|
8726
|
+
logger15.warn(`legacy framework token migration failed: ${err}`);
|
|
5404
8727
|
});
|
|
5405
8728
|
});
|
|
5406
8729
|
ctx.command("洛克", "洛克王国帮助菜单").action(async ({ session }) => {
|