koishi-plugin-rocom 1.0.13 → 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 +19 -0
- package/lib/client.js +101 -1
- package/lib/commands/account.js +1 -0
- package/lib/commands/merchant.js +11 -2
- package/lib/commands/query.js +141 -9
- 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 +5 -0
- package/lib/index.js +3153 -188
- package/lib/pet-data-service.d.ts +62 -0
- package/lib/pet-data-service.js +468 -0
- 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.js +37 -8
- package/lib/types.d.ts +9 -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,9 +36,9 @@ __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");
|
|
@@ -174,14 +174,14 @@ var RocomClient = class _RocomClient {
|
|
|
174
174
|
return String(value);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
logRequestFailureDetails(method,
|
|
177
|
+
logRequestFailureDetails(method, path7, headers, params, body, errorOrResponse) {
|
|
178
178
|
const err = errorOrResponse;
|
|
179
179
|
const response = err?.response;
|
|
180
180
|
const details = {
|
|
181
181
|
request: {
|
|
182
182
|
method,
|
|
183
|
-
url: `${this.baseUrl}${
|
|
184
|
-
path:
|
|
183
|
+
url: `${this.baseUrl}${path7}`,
|
|
184
|
+
path: path7,
|
|
185
185
|
params: this.sanitizeForLog(params),
|
|
186
186
|
body: this.sanitizeForLog(body),
|
|
187
187
|
headers: this.sanitizeForLog(headers),
|
|
@@ -201,21 +201,21 @@ var RocomClient = class _RocomClient {
|
|
|
201
201
|
stack: err?.stack
|
|
202
202
|
} : void 0
|
|
203
203
|
};
|
|
204
|
-
logger.warn(`${method} ${
|
|
204
|
+
logger.warn(`${method} ${path7} detailed failure
|
|
205
205
|
${this.stringifyForLog(details)}`);
|
|
206
206
|
}
|
|
207
|
-
async get(ctx,
|
|
207
|
+
async get(ctx, path7, headers, params, options) {
|
|
208
208
|
try {
|
|
209
|
-
const resp = await ctx.http.get(`${this.baseUrl}${
|
|
209
|
+
const resp = await ctx.http.get(`${this.baseUrl}${path7}`, {
|
|
210
210
|
headers,
|
|
211
211
|
params,
|
|
212
212
|
timeout: this.timeout
|
|
213
213
|
});
|
|
214
214
|
if (resp?.code !== 0) {
|
|
215
215
|
this.setLastError(resp?.message || "接口返回异常");
|
|
216
|
-
logger.warn(
|
|
216
|
+
logger.warn(path7 + " error: " + (resp?.message || "unknown"));
|
|
217
217
|
if (!options?.silentFailureDetails) {
|
|
218
|
-
this.logRequestFailureDetails("GET",
|
|
218
|
+
this.logRequestFailureDetails("GET", path7, headers, params, void 0, resp);
|
|
219
219
|
}
|
|
220
220
|
return null;
|
|
221
221
|
}
|
|
@@ -224,40 +224,40 @@ ${this.stringifyForLog(details)}`);
|
|
|
224
224
|
const message = this.formatHttpError(e);
|
|
225
225
|
this.setLastError(message);
|
|
226
226
|
if (!options?.silentFailureDetails) {
|
|
227
|
-
this.logRequestFailureDetails("GET",
|
|
227
|
+
this.logRequestFailureDetails("GET", path7, headers, params, void 0, e);
|
|
228
228
|
}
|
|
229
229
|
const err = e;
|
|
230
230
|
if (err?.response) {
|
|
231
|
-
logger.warn("GET " +
|
|
231
|
+
logger.warn("GET " + path7 + " failed: " + message);
|
|
232
232
|
} else {
|
|
233
|
-
logger.error("GET " +
|
|
233
|
+
logger.error("GET " + path7 + " failed: " + message);
|
|
234
234
|
}
|
|
235
235
|
return null;
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
|
-
async post(ctx,
|
|
238
|
+
async post(ctx, path7, headers, json, params) {
|
|
239
239
|
try {
|
|
240
|
-
const resp = await ctx.http.post(`${this.baseUrl}${
|
|
240
|
+
const resp = await ctx.http.post(`${this.baseUrl}${path7}`, json, {
|
|
241
241
|
headers,
|
|
242
242
|
params,
|
|
243
243
|
timeout: this.timeout
|
|
244
244
|
});
|
|
245
245
|
if (resp?.code !== 0) {
|
|
246
246
|
this.setLastError(resp?.message || "接口返回异常");
|
|
247
|
-
logger.warn(
|
|
248
|
-
this.logRequestFailureDetails("POST",
|
|
247
|
+
logger.warn(path7 + " error: " + (resp?.message || "unknown"));
|
|
248
|
+
this.logRequestFailureDetails("POST", path7, headers, params, json, resp);
|
|
249
249
|
return null;
|
|
250
250
|
}
|
|
251
251
|
return resp?.data ?? {};
|
|
252
252
|
} catch (e) {
|
|
253
253
|
const message = this.formatHttpError(e);
|
|
254
254
|
this.setLastError(message);
|
|
255
|
-
this.logRequestFailureDetails("POST",
|
|
255
|
+
this.logRequestFailureDetails("POST", path7, headers, params, json, e);
|
|
256
256
|
const err = e;
|
|
257
257
|
if (err?.response) {
|
|
258
|
-
logger.warn("POST " +
|
|
258
|
+
logger.warn("POST " + path7 + " failed: " + message);
|
|
259
259
|
} else {
|
|
260
|
-
logger.error("POST " +
|
|
260
|
+
logger.error("POST " + path7 + " failed: " + message);
|
|
261
261
|
}
|
|
262
262
|
return null;
|
|
263
263
|
}
|
|
@@ -267,34 +267,34 @@ ${this.stringifyForLog(details)}`);
|
|
|
267
267
|
if (userIdentifier) result.user_identifier = this.sanitizeUid(userIdentifier);
|
|
268
268
|
return result;
|
|
269
269
|
}
|
|
270
|
-
async delete(ctx,
|
|
270
|
+
async delete(ctx, path7, headers, params) {
|
|
271
271
|
try {
|
|
272
|
-
const resp = await ctx.http("DELETE", `${this.baseUrl}${
|
|
272
|
+
const resp = await ctx.http("DELETE", `${this.baseUrl}${path7}`, { headers, params, timeout: this.timeout });
|
|
273
273
|
if (resp?.code !== 0) {
|
|
274
274
|
this.setLastError(resp?.message || "接口返回异常");
|
|
275
|
-
logger.warn(
|
|
276
|
-
this.logRequestFailureDetails("DELETE",
|
|
275
|
+
logger.warn(path7 + " error: " + (resp?.message || "unknown"));
|
|
276
|
+
this.logRequestFailureDetails("DELETE", path7, headers, params, void 0, resp);
|
|
277
277
|
return null;
|
|
278
278
|
}
|
|
279
279
|
return resp?.data ?? {};
|
|
280
280
|
} catch (e) {
|
|
281
281
|
const message = this.formatHttpError(e);
|
|
282
282
|
this.setLastError(message);
|
|
283
|
-
this.logRequestFailureDetails("DELETE",
|
|
283
|
+
this.logRequestFailureDetails("DELETE", path7, headers, params, void 0, e);
|
|
284
284
|
const err = e;
|
|
285
285
|
if (err?.response) {
|
|
286
|
-
logger.warn("DELETE " +
|
|
286
|
+
logger.warn("DELETE " + path7 + " failed: " + message);
|
|
287
287
|
} else {
|
|
288
|
-
logger.error("DELETE " +
|
|
288
|
+
logger.error("DELETE " + path7 + " failed: " + message);
|
|
289
289
|
}
|
|
290
290
|
return null;
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
// Login and binding APIs.
|
|
294
|
-
async requestWithStatus(ctx, method,
|
|
294
|
+
async requestWithStatus(ctx, method, path7, headers, options = {}) {
|
|
295
295
|
const acceptedStatuses = options.acceptedStatuses || [200];
|
|
296
296
|
try {
|
|
297
|
-
const response = await ctx.http(method, `${this.baseUrl}${
|
|
297
|
+
const response = await ctx.http(method, `${this.baseUrl}${path7}`, {
|
|
298
298
|
headers,
|
|
299
299
|
params: options.params,
|
|
300
300
|
data: options.json,
|
|
@@ -308,7 +308,7 @@ ${this.stringifyForLog(details)}`);
|
|
|
308
308
|
const message = !acceptedStatuses.includes(status) ? `HTTP ${status}: ${rawMessage}` : rawMessage;
|
|
309
309
|
this.setLastError(message);
|
|
310
310
|
if (!options.silentFailureDetails) {
|
|
311
|
-
this.logRequestFailureDetails(method,
|
|
311
|
+
this.logRequestFailureDetails(method, path7, headers, options.params, options.json, body);
|
|
312
312
|
}
|
|
313
313
|
return { status: null, data: null };
|
|
314
314
|
}
|
|
@@ -316,7 +316,7 @@ ${this.stringifyForLog(details)}`);
|
|
|
316
316
|
if (!acceptedStatuses.includes(status)) {
|
|
317
317
|
this.setLastError(`HTTP ${status}`);
|
|
318
318
|
if (!options.silentFailureDetails) {
|
|
319
|
-
this.logRequestFailureDetails(method,
|
|
319
|
+
this.logRequestFailureDetails(method, path7, headers, options.params, options.json, response);
|
|
320
320
|
}
|
|
321
321
|
return { status: null, data: null };
|
|
322
322
|
}
|
|
@@ -325,22 +325,22 @@ ${this.stringifyForLog(details)}`);
|
|
|
325
325
|
const message = this.formatHttpError(e);
|
|
326
326
|
this.setLastError(message);
|
|
327
327
|
if (!options.silentFailureDetails) {
|
|
328
|
-
this.logRequestFailureDetails(method,
|
|
328
|
+
this.logRequestFailureDetails(method, path7, headers, options.params, options.json, e);
|
|
329
329
|
}
|
|
330
330
|
return { status: null, data: null };
|
|
331
331
|
}
|
|
332
332
|
}
|
|
333
|
-
async requestIngameWithFallback(ctx,
|
|
333
|
+
async requestIngameWithFallback(ctx, path7, payload, options = {}) {
|
|
334
334
|
const acceptedStatuses = options.acceptedStatuses || [200, 202];
|
|
335
335
|
const requestOnce = /* @__PURE__ */ __name(async (includeApiKey, silentFailureDetails) => {
|
|
336
336
|
const headers = this.wegameHeaders("", "", "", "", includeApiKey);
|
|
337
|
-
let result2 = await this.requestWithStatus(ctx, "POST",
|
|
337
|
+
let result2 = await this.requestWithStatus(ctx, "POST", path7, headers, {
|
|
338
338
|
json: payload,
|
|
339
339
|
acceptedStatuses,
|
|
340
340
|
silentFailureDetails
|
|
341
341
|
});
|
|
342
342
|
if (result2.status === null) {
|
|
343
|
-
result2 = await this.requestWithStatus(ctx, "GET",
|
|
343
|
+
result2 = await this.requestWithStatus(ctx, "GET", path7, headers, {
|
|
344
344
|
params: payload,
|
|
345
345
|
acceptedStatuses,
|
|
346
346
|
silentFailureDetails
|
|
@@ -372,12 +372,15 @@ ${this.stringifyForLog(details)}`);
|
|
|
372
372
|
return String(payload?.task_id || payload?.taskId || payload?.taskID || "").trim();
|
|
373
373
|
}
|
|
374
374
|
// 判断一份 payload 是否为“已完成的业务结果”(而非排队占位)。
|
|
375
|
-
// 家园接口完成时返回 rows/home_info;玩家、商店完成时返回 source/title/rows
|
|
375
|
+
// 家园接口完成时返回 rows/home_info;玩家、商店完成时返回 source/title/rows;
|
|
376
|
+
// 家园详情(pet/data)完成时返回 npc_pets/npc_pet。
|
|
376
377
|
static isCompletedGatewayPayload(payload) {
|
|
377
378
|
if (!payload || typeof payload !== "object") return false;
|
|
378
379
|
if (Array.isArray(payload.rows)) return true;
|
|
379
380
|
if (payload.home_info !== void 0) return true;
|
|
380
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;
|
|
381
384
|
if (String(payload.title || "").trim()) return true;
|
|
382
385
|
return false;
|
|
383
386
|
}
|
|
@@ -563,9 +566,9 @@ ${this.stringifyForLog(details)}`);
|
|
|
563
566
|
this.setLastError("UID 不能为空");
|
|
564
567
|
return null;
|
|
565
568
|
}
|
|
566
|
-
const
|
|
569
|
+
const path7 = "/api/v1/games/rocom/ingame/player/search";
|
|
567
570
|
const payload = { uid: sanitizedUid, wait_ms: 5e3 };
|
|
568
|
-
const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx,
|
|
571
|
+
const { status, data, usedApiKey } = await this.requestIngameWithFallback(ctx, path7, payload);
|
|
569
572
|
if (status === 200 && data && this.isIngamePlayerPayload(data)) return data;
|
|
570
573
|
if (!data) return null;
|
|
571
574
|
if (this.isIngamePlayerPayload(data)) return data;
|
|
@@ -603,6 +606,22 @@ ${this.stringifyForLog(details)}`);
|
|
|
603
606
|
});
|
|
604
607
|
}
|
|
605
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;
|
|
606
625
|
const params = this.scopedParams({ diameter, weight }, userIdentifier);
|
|
607
626
|
if (sameRideEgg) params.sameRideEgg = 1;
|
|
608
627
|
return this.get(ctx, "/api/v1/games/rocom/pet/size-query", this.wegameHeaders("", userIdentifier, "bot", "koishi"), params);
|
|
@@ -765,9 +784,9 @@ ${this.stringifyForLog(details)}`);
|
|
|
765
784
|
return null;
|
|
766
785
|
}
|
|
767
786
|
const waitMs = Number(options.waitMs) || 5e3;
|
|
768
|
-
const
|
|
787
|
+
const path7 = "/api/v1/games/rocom/ingame/home/info";
|
|
769
788
|
const payload = { uid: sanitizedUid, wait_ms: waitMs };
|
|
770
|
-
const first = await this.requestIngameWithFallback(ctx,
|
|
789
|
+
const first = await this.requestIngameWithFallback(ctx, path7, payload);
|
|
771
790
|
return this.pollIngameTask(ctx, first, options, {
|
|
772
791
|
queuedNoTaskId: "家园查询任务已入队,但未返回 task_id",
|
|
773
792
|
stillQueued: /* @__PURE__ */ __name((taskId) => `家园查询任务仍在排队,请稍后重试(task_id: ${taskId})`, "stillQueued")
|
|
@@ -782,6 +801,25 @@ ${this.stringifyForLog(details)}`);
|
|
|
782
801
|
);
|
|
783
802
|
return status === null ? null : data;
|
|
784
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
|
+
}
|
|
785
823
|
async getFriendship(ctx, fwToken, userIds, userIdentifier = "") {
|
|
786
824
|
return this.get(
|
|
787
825
|
ctx,
|
|
@@ -813,6 +851,66 @@ ${this.stringifyForLog(details)}`);
|
|
|
813
851
|
if (options.name) params.name = options.name;
|
|
814
852
|
return this.get(ctx, "/api/v1/games/rocom/pet/detail", this.wegameHeaders(), params);
|
|
815
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
|
+
}
|
|
816
914
|
};
|
|
817
915
|
|
|
818
916
|
// src/user.ts
|
|
@@ -985,6 +1083,35 @@ var HomeSubscriptionManager = class {
|
|
|
985
1083
|
return JSON.parse(JSON.stringify(this.store.get()));
|
|
986
1084
|
}
|
|
987
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
|
+
};
|
|
988
1115
|
|
|
989
1116
|
// src/egg-service.ts
|
|
990
1117
|
var import_koishi3 = require("koishi");
|
|
@@ -1033,8 +1160,8 @@ __name(petType, "petType");
|
|
|
1033
1160
|
function fmtDur(s) {
|
|
1034
1161
|
if (!s || s <= 0) return "暂无数据";
|
|
1035
1162
|
if (s % 86400 === 0) return `${s / 86400} 天`;
|
|
1036
|
-
const
|
|
1037
|
-
return
|
|
1163
|
+
const h5 = s / 3600;
|
|
1164
|
+
return h5 === Math.floor(h5) ? `${h5} 小时` : `${h5.toFixed(1)} 小时`;
|
|
1038
1165
|
}
|
|
1039
1166
|
__name(fmtDur, "fmtDur");
|
|
1040
1167
|
function wt(v) {
|
|
@@ -1240,20 +1367,29 @@ var EggService = class {
|
|
|
1240
1367
|
};
|
|
1241
1368
|
}
|
|
1242
1369
|
formatSizeApiCard(item) {
|
|
1243
|
-
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);
|
|
1244
1380
|
const matchCount = num(item?.matchCount);
|
|
1245
|
-
const heightMin = num(item?.diameterMin);
|
|
1246
|
-
const heightMax = num(item?.diameterMax);
|
|
1247
|
-
const weightMin = num(item?.weightMin);
|
|
1248
|
-
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);
|
|
1249
1385
|
return {
|
|
1250
|
-
id:
|
|
1251
|
-
name:
|
|
1252
|
-
icon: item?.petIcon || petIconUrl(
|
|
1253
|
-
image: item?.petImage || petImageUrl(
|
|
1254
|
-
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(" / ") : "后端未提供",
|
|
1255
1391
|
egg_group_ids: [],
|
|
1256
|
-
egg_groups_label: "后端未提供",
|
|
1392
|
+
egg_groups_label: eggGroupNames.length ? eggGroupNames.join(" / ") : "后端未提供",
|
|
1257
1393
|
height_min: heightMin,
|
|
1258
1394
|
height_max: heightMax,
|
|
1259
1395
|
height_label: fmtRange(heightMin, heightMax, "m"),
|
|
@@ -1262,7 +1398,30 @@ var EggService = class {
|
|
|
1262
1398
|
weight_label: fmtRange(weightMin, weightMax, "kg"),
|
|
1263
1399
|
probability,
|
|
1264
1400
|
match_count: matchCount,
|
|
1265
|
-
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 : []
|
|
1266
1425
|
};
|
|
1267
1426
|
}
|
|
1268
1427
|
formatEggSearchCard(item) {
|
|
@@ -1421,8 +1580,7 @@ var EggService = class {
|
|
|
1421
1580
|
if (height != null) cond.push(`身高=${heightDisplay || fmtRange(ht(height), ht(height), "m")}`);
|
|
1422
1581
|
if (weight != null) cond.push(`体重=${weight}kg`);
|
|
1423
1582
|
const condStr = cond.join(" + ") || "当前条件";
|
|
1424
|
-
const exact = results
|
|
1425
|
-
const candidates = results?.candidates || [];
|
|
1583
|
+
const { exact, candidates } = this.sizeApiResultGroups(results);
|
|
1426
1584
|
if (!exact.length && !candidates.length) return `❌ 未找到符合 ${condStr} 的精灵。`;
|
|
1427
1585
|
const lines = [];
|
|
1428
1586
|
if (exact.length) {
|
|
@@ -1635,9 +1793,10 @@ var EggService = class {
|
|
|
1635
1793
|
const conditions = [];
|
|
1636
1794
|
if (height != null) conditions.push(`身高 ${heightDisplay || fmtRange(ht(height), ht(height), "m")}`);
|
|
1637
1795
|
if (weight != null) conditions.push(`体重 ${weight} kg`);
|
|
1796
|
+
const groups = this.sizeApiResultGroups(results);
|
|
1638
1797
|
const [perfect, ranged] = this.mergeCardsByName(
|
|
1639
|
-
|
|
1640
|
-
|
|
1798
|
+
groups.exact.map((item) => this.formatSizeApiCard(item)),
|
|
1799
|
+
groups.candidates.map((item) => this.formatSizeApiCard(item))
|
|
1641
1800
|
);
|
|
1642
1801
|
const searchMode = results?.searchMode || "";
|
|
1643
1802
|
const queryLabel = `${conditions.join(" / ") || "尺寸反查"}${searchMode ? ` · 模式 ${searchMode}` : ""}`;
|
|
@@ -1818,13 +1977,16 @@ var Renderer = class {
|
|
|
1818
1977
|
try {
|
|
1819
1978
|
await page.evaluate(async () => {
|
|
1820
1979
|
const images = Array.from(document.images);
|
|
1821
|
-
await Promise.
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
+
]);
|
|
1828
1990
|
const fonts = document.fonts;
|
|
1829
1991
|
if (fonts?.ready) {
|
|
1830
1992
|
await fonts.ready;
|
|
@@ -1841,6 +2003,8 @@ var Renderer = class {
|
|
|
1841
2003
|
".searcheggs-cont",
|
|
1842
2004
|
".bwiki-shell",
|
|
1843
2005
|
".skill-shell",
|
|
2006
|
+
".wiki-page",
|
|
2007
|
+
".pet-data-page",
|
|
1844
2008
|
".lineup-page",
|
|
1845
2009
|
".lineup-detail-page",
|
|
1846
2010
|
".page-section-main",
|
|
@@ -1888,6 +2052,26 @@ var Renderer = class {
|
|
|
1888
2052
|
deviceScaleFactor: initialViewport.deviceScaleFactor
|
|
1889
2053
|
});
|
|
1890
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
|
+
}
|
|
1891
2075
|
const hasOverflow = elementMetrics.width > box.width + 0.5 || elementMetrics.height > box.height + 0.5;
|
|
1892
2076
|
if (capturePadding.left || capturePadding.right || capturePadding.top || capturePadding.bottom || hasOverflow) {
|
|
1893
2077
|
const clipX = Math.max(0, elementMetrics.x - capturePadding.left);
|
|
@@ -1925,6 +2109,1981 @@ var Renderer = class {
|
|
|
1925
2109
|
}
|
|
1926
2110
|
};
|
|
1927
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;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
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);
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
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");
|
|
4086
|
+
|
|
1928
4087
|
// src/role-token.ts
|
|
1929
4088
|
function normalize(value) {
|
|
1930
4089
|
return String(value ?? "").trim();
|
|
@@ -2059,12 +4218,12 @@ async function migrateLegacyFrameworkTokens(ctx, userMgr) {
|
|
|
2059
4218
|
__name(migrateLegacyFrameworkTokens, "migrateLegacyFrameworkTokens");
|
|
2060
4219
|
|
|
2061
4220
|
// src/commands/account.ts
|
|
2062
|
-
var
|
|
4221
|
+
var import_koishi8 = require("koishi");
|
|
2063
4222
|
|
|
2064
4223
|
// src/send-image.ts
|
|
2065
|
-
var
|
|
2066
|
-
var
|
|
2067
|
-
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");
|
|
2068
4227
|
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
2069
4228
|
var JPEG_SIGNATURE = Buffer.from([255, 216, 255]);
|
|
2070
4229
|
function formatSessionContext(session) {
|
|
@@ -2139,9 +4298,9 @@ function compressPngImage(image, config) {
|
|
|
2139
4298
|
if (type === "IEND") break;
|
|
2140
4299
|
}
|
|
2141
4300
|
if (!idatChunks.length) return image;
|
|
2142
|
-
const rawImageData =
|
|
4301
|
+
const rawImageData = import_node_zlib2.default.inflateSync(Buffer.concat(idatChunks));
|
|
2143
4302
|
const level = Math.max(0, Math.min(9, Math.floor(config.imageCompressionLevel ?? 9)));
|
|
2144
|
-
const compressedIdat =
|
|
4303
|
+
const compressedIdat = import_node_zlib2.default.deflateSync(rawImageData, { level });
|
|
2145
4304
|
const rebuiltChunks = [];
|
|
2146
4305
|
let wroteIdat = false;
|
|
2147
4306
|
for (const chunk of chunks) {
|
|
@@ -2157,7 +4316,7 @@ function compressPngImage(image, config) {
|
|
|
2157
4316
|
const compressed = Buffer.concat([PNG_SIGNATURE, ...rebuiltChunks]);
|
|
2158
4317
|
if (compressed.length < image.length) return compressed;
|
|
2159
4318
|
} catch (err) {
|
|
2160
|
-
|
|
4319
|
+
logger7.warn(`PNG compression failed, sending original image: ${err}`);
|
|
2161
4320
|
}
|
|
2162
4321
|
return image;
|
|
2163
4322
|
}
|
|
@@ -2165,36 +4324,36 @@ __name(compressPngImage, "compressPngImage");
|
|
|
2165
4324
|
async function sendImageWithFallback(session, image, fallbackText, scene, compressionConfig) {
|
|
2166
4325
|
const ctxInfo = formatSessionContext(session);
|
|
2167
4326
|
if (!image) {
|
|
2168
|
-
|
|
4327
|
+
logger7.warn(`[${scene}] render returned empty image, fallback to text | ${ctxInfo}`);
|
|
2169
4328
|
try {
|
|
2170
4329
|
await session.send(fallbackText);
|
|
2171
4330
|
} catch (e) {
|
|
2172
|
-
|
|
4331
|
+
logger7.error(`[${scene}] text fallback send failed | ${ctxInfo} | ${e}`);
|
|
2173
4332
|
}
|
|
2174
4333
|
return;
|
|
2175
4334
|
}
|
|
2176
4335
|
const outputImage = compressionConfig ? compressPngImage(image, compressionConfig) : image;
|
|
2177
4336
|
if (outputImage.length < image.length) {
|
|
2178
|
-
|
|
4337
|
+
logger7.info(`[${scene}] compressed image ${image.length}B -> ${outputImage.length}B | ${ctxInfo}`);
|
|
2179
4338
|
}
|
|
2180
4339
|
try {
|
|
2181
|
-
const result = await session.send(
|
|
4340
|
+
const result = await session.send(import_koishi7.h.image(outputImage, detectImageMime(outputImage)));
|
|
2182
4341
|
if (!hasSendResult(result)) {
|
|
2183
|
-
|
|
4342
|
+
logger7.warn(`[${scene}] image send returned empty result | size=${outputImage.length}B | ${ctxInfo}`);
|
|
2184
4343
|
}
|
|
2185
4344
|
} catch (e) {
|
|
2186
|
-
|
|
4345
|
+
logger7.error(`[${scene}] image send failed | size=${outputImage.length}B | ${ctxInfo} | ${e}`);
|
|
2187
4346
|
try {
|
|
2188
4347
|
await session.send(fallbackText);
|
|
2189
4348
|
} catch (fallbackErr) {
|
|
2190
|
-
|
|
4349
|
+
logger7.error(`[${scene}] text fallback send failed after image send error | ${ctxInfo} | ${fallbackErr}`);
|
|
2191
4350
|
}
|
|
2192
4351
|
}
|
|
2193
4352
|
}
|
|
2194
4353
|
__name(sendImageWithFallback, "sendImageWithFallback");
|
|
2195
4354
|
|
|
2196
4355
|
// src/commands/account.ts
|
|
2197
|
-
var
|
|
4356
|
+
var logger8 = new import_koishi8.Logger("rocom-account");
|
|
2198
4357
|
async function recallMessages(session, messageIds) {
|
|
2199
4358
|
if (!session?.bot?.deleteMessage || !session.channelId) return;
|
|
2200
4359
|
for (const id of messageIds) {
|
|
@@ -2202,7 +4361,7 @@ async function recallMessages(session, messageIds) {
|
|
|
2202
4361
|
try {
|
|
2203
4362
|
await session.bot.deleteMessage(session.channelId, id);
|
|
2204
4363
|
} catch (err) {
|
|
2205
|
-
|
|
4364
|
+
logger8.warn(`撤回消息 ${id} 失败:${err}`);
|
|
2206
4365
|
}
|
|
2207
4366
|
}
|
|
2208
4367
|
}
|
|
@@ -2282,19 +4441,19 @@ async function saveBindingWithRoleInfo(deps, session, fwToken, loginType, userId
|
|
|
2282
4441
|
__name(saveBindingWithRoleInfo, "saveBindingWithRoleInfo");
|
|
2283
4442
|
function register(deps) {
|
|
2284
4443
|
const { ctx, client, userMgr } = deps;
|
|
2285
|
-
ctx.command("洛克").subcommand(".QQ登录", "QQ 扫码登录").alias("洛克QQ登录").action(async ({ session }) => {
|
|
4444
|
+
ctx.command("洛克").subcommand(".QQ登录", "QQ 扫码登录").alias("洛克QQ登录").alias("洛克qq登录").action(async ({ session }) => {
|
|
2286
4445
|
const userId = session.userId;
|
|
2287
4446
|
const qrData = await client.qqQrLogin(ctx, userId);
|
|
2288
4447
|
if (!qrData?.qr_image) return "获取 QQ 二维码失败。";
|
|
2289
4448
|
const fwToken = qrData.frameworkToken;
|
|
2290
4449
|
const qrB64 = qrData.qr_image;
|
|
2291
4450
|
const imgData = qrB64.includes(",") ? qrB64.split(",")[1] : qrB64;
|
|
2292
|
-
const qrSendResult = await session.send((0,
|
|
4451
|
+
const qrSendResult = await session.send((0, import_koishi8.h)(
|
|
2293
4452
|
"message",
|
|
2294
4453
|
{},
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
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}`)
|
|
2298
4457
|
));
|
|
2299
4458
|
const qrMessageIds = Array.isArray(qrSendResult) ? qrSendResult.filter(Boolean).map(String) : qrSendResult ? [String(qrSendResult)] : [];
|
|
2300
4459
|
const startTime = Date.now();
|
|
@@ -2481,11 +4640,11 @@ ${qrUrl}`);
|
|
|
2481
4640
|
__name(register, "register");
|
|
2482
4641
|
|
|
2483
4642
|
// src/commands/query.ts
|
|
2484
|
-
var
|
|
4643
|
+
var import_koishi10 = require("koishi");
|
|
2485
4644
|
|
|
2486
4645
|
// src/subscription-send.ts
|
|
2487
|
-
var
|
|
2488
|
-
var
|
|
4646
|
+
var import_koishi9 = require("koishi");
|
|
4647
|
+
var logger9 = new import_koishi9.Logger("rocom-subscription-send");
|
|
2489
4648
|
function findBot(ctx, platform = "") {
|
|
2490
4649
|
if (!ctx.bots?.length) return null;
|
|
2491
4650
|
if (!platform) return ctx.bots[0];
|
|
@@ -2498,7 +4657,7 @@ async function sendScheduledMessage(ctx, target, message) {
|
|
|
2498
4657
|
const userId = target.userId || "";
|
|
2499
4658
|
const bot = findBot(ctx, platform);
|
|
2500
4659
|
if (!bot) {
|
|
2501
|
-
|
|
4660
|
+
logger9.warn("no available bot, skip scheduled push");
|
|
2502
4661
|
return false;
|
|
2503
4662
|
}
|
|
2504
4663
|
try {
|
|
@@ -2521,29 +4680,458 @@ async function sendScheduledMessage(ctx, target, message) {
|
|
|
2521
4680
|
return true;
|
|
2522
4681
|
}
|
|
2523
4682
|
} catch (err) {
|
|
2524
|
-
|
|
4683
|
+
logger9.warn(`scheduled push failed: ${err}`);
|
|
2525
4684
|
return false;
|
|
2526
4685
|
}
|
|
2527
|
-
|
|
4686
|
+
logger9.warn(`scheduled push target is incomplete: ${JSON.stringify(target)}`);
|
|
2528
4687
|
return false;
|
|
2529
4688
|
}
|
|
2530
4689
|
__name(sendScheduledMessage, "sendScheduledMessage");
|
|
2531
4690
|
async function sendScheduledImageWithFallback(ctx, target, image, fallbackText, mentionAll = false) {
|
|
2532
|
-
const buildContent = /* @__PURE__ */ __name((body) => mentionAll ? (0,
|
|
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");
|
|
2533
4692
|
if (!image) {
|
|
2534
|
-
return sendScheduledMessage(ctx, target, buildContent(
|
|
4693
|
+
return sendScheduledMessage(ctx, target, buildContent(import_koishi9.h.text(fallbackText)));
|
|
2535
4694
|
}
|
|
2536
|
-
const imageSegment =
|
|
4695
|
+
const imageSegment = import_koishi9.h.image(image, detectImageMime(image));
|
|
2537
4696
|
const sent = await sendScheduledMessage(ctx, target, buildContent(imageSegment));
|
|
2538
4697
|
if (sent) return true;
|
|
2539
|
-
return sendScheduledMessage(ctx, target, buildContent(
|
|
4698
|
+
return sendScheduledMessage(ctx, target, buildContent(import_koishi9.h.text(fallbackText)));
|
|
2540
4699
|
}
|
|
2541
4700
|
__name(sendScheduledImageWithFallback, "sendScheduledImageWithFallback");
|
|
2542
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
|
+
|
|
2543
5131
|
// src/commands/query.ts
|
|
2544
|
-
var
|
|
2545
|
-
var
|
|
2546
|
-
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");
|
|
2547
5135
|
async function sendImage(deps, session, templateName, data, fallback) {
|
|
2548
5136
|
const png = await deps.renderer.renderHtml(deps.ctx, templateName, data);
|
|
2549
5137
|
await sendImageWithFallback(session, png, fallback, `query:${templateName}`, deps.config);
|
|
@@ -2753,14 +5341,14 @@ function stringifyInspectValue(value) {
|
|
|
2753
5341
|
return String(value);
|
|
2754
5342
|
}
|
|
2755
5343
|
__name(stringifyInspectValue, "stringifyInspectValue");
|
|
2756
|
-
function
|
|
5344
|
+
function normalizeEpochSeconds2(value) {
|
|
2757
5345
|
const ts = Number(value);
|
|
2758
5346
|
if (!Number.isFinite(ts)) return 0;
|
|
2759
5347
|
if (ts > 1e13) return Math.floor(ts / 1e6);
|
|
2760
5348
|
if (ts > 1e10) return Math.floor(ts / 1e3);
|
|
2761
5349
|
return Math.floor(ts);
|
|
2762
5350
|
}
|
|
2763
|
-
__name(
|
|
5351
|
+
__name(normalizeEpochSeconds2, "normalizeEpochSeconds");
|
|
2764
5352
|
function formatHomeRemaining(targetTs, nowTs = Math.floor(Date.now() / 1e3)) {
|
|
2765
5353
|
if (!targetTs) return "未开始";
|
|
2766
5354
|
if (nowTs >= targetTs) return "已完成";
|
|
@@ -2824,12 +5412,12 @@ var PANEL_PET_STAT_DEFS = [
|
|
|
2824
5412
|
{ key: "special_defense", label: "魔防", addiType: 5, aliases: ["special_defense", "magic_defense", "sp_defense"] },
|
|
2825
5413
|
{ key: "speed", label: "速度", addiType: 6, aliases: ["speed", "spd"] }
|
|
2826
5414
|
];
|
|
2827
|
-
var PANEL_PET_CACHE_PATH =
|
|
5415
|
+
var PANEL_PET_CACHE_PATH = import_node_path5.default.join("data", "rocom", "rocom_pet_panel_cache.json");
|
|
2828
5416
|
var PANEL_EMPTY_FEATURE = { id: "", name: "暂无特性", desc: "当前接口未返回特性描述。" };
|
|
2829
5417
|
var PANEL_EMPTY_SKILL_HINT = "当前接口未返回技能详情。";
|
|
2830
5418
|
var panelPetCacheMem = null;
|
|
2831
5419
|
function panelPetCacheFilePath(deps) {
|
|
2832
|
-
return
|
|
5420
|
+
return import_node_path5.default.join(deps.ctx.baseDir, PANEL_PET_CACHE_PATH);
|
|
2833
5421
|
}
|
|
2834
5422
|
__name(panelPetCacheFilePath, "panelPetCacheFilePath");
|
|
2835
5423
|
function ensureBloodlineLabel(raw, mutationLabel) {
|
|
@@ -2841,15 +5429,15 @@ function loadPanelPetCache(deps) {
|
|
|
2841
5429
|
if (panelPetCacheMem) return panelPetCacheMem;
|
|
2842
5430
|
const filePath = panelPetCacheFilePath(deps);
|
|
2843
5431
|
try {
|
|
2844
|
-
if (!
|
|
5432
|
+
if (!import_node_fs5.default.existsSync(filePath)) {
|
|
2845
5433
|
panelPetCacheMem = {};
|
|
2846
5434
|
return panelPetCacheMem;
|
|
2847
5435
|
}
|
|
2848
|
-
const text =
|
|
5436
|
+
const text = import_node_fs5.default.readFileSync(filePath, "utf-8");
|
|
2849
5437
|
const parsed = JSON.parse(text);
|
|
2850
5438
|
panelPetCacheMem = parsed && typeof parsed === "object" ? parsed : {};
|
|
2851
5439
|
} catch (err) {
|
|
2852
|
-
|
|
5440
|
+
logger10.warn(`加载精灵面板缓存失败: ${err}`);
|
|
2853
5441
|
panelPetCacheMem = {};
|
|
2854
5442
|
}
|
|
2855
5443
|
return panelPetCacheMem;
|
|
@@ -2857,11 +5445,11 @@ function loadPanelPetCache(deps) {
|
|
|
2857
5445
|
__name(loadPanelPetCache, "loadPanelPetCache");
|
|
2858
5446
|
function savePanelPetCache(deps, data) {
|
|
2859
5447
|
const filePath = panelPetCacheFilePath(deps);
|
|
2860
|
-
const dir =
|
|
2861
|
-
|
|
5448
|
+
const dir = import_node_path5.default.dirname(filePath);
|
|
5449
|
+
import_node_fs5.default.mkdirSync(dir, { recursive: true });
|
|
2862
5450
|
const tempPath = `${filePath}.tmp`;
|
|
2863
|
-
|
|
2864
|
-
|
|
5451
|
+
import_node_fs5.default.writeFileSync(tempPath, JSON.stringify(data, null, 2), "utf-8");
|
|
5452
|
+
import_node_fs5.default.renameSync(tempPath, filePath);
|
|
2865
5453
|
panelPetCacheMem = data;
|
|
2866
5454
|
}
|
|
2867
5455
|
__name(savePanelPetCache, "savePanelPetCache");
|
|
@@ -3358,11 +5946,22 @@ function extractHomePet(deps, raw, index, guard = false) {
|
|
|
3358
5946
|
if (["", "0"].includes(String(petId || "0")) && !guard) return null;
|
|
3359
5947
|
const nowTs = Math.floor(Date.now() / 1e3);
|
|
3360
5948
|
const hasEgg = Boolean(raw.have_egg);
|
|
3361
|
-
const predictedEggTime =
|
|
5949
|
+
const predictedEggTime = normalizeEpochSeconds2(raw.predicted_egg_time);
|
|
3362
5950
|
const eggReady = hasEgg || predictedEggTime > 0 && nowTs >= predictedEggTime;
|
|
3363
5951
|
const feedRound = Number(homePet.feed_round || raw.feed_round || 0) || 0;
|
|
3364
5952
|
const gender = Number(display.gender || raw.gender || 0) || 0;
|
|
3365
|
-
|
|
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
|
+
}
|
|
3366
5965
|
const isMale = gender === 1;
|
|
3367
5966
|
const status = homePet.status ?? raw.status;
|
|
3368
5967
|
const isGuard = guard || Boolean(raw.is_guard || raw.guard) || ["2", "guard", "守卫"].includes(String(status).toLowerCase());
|
|
@@ -3397,6 +5996,9 @@ function extractHomePet(deps, raw, index, guard = false) {
|
|
|
3397
5996
|
statusText,
|
|
3398
5997
|
statusClass,
|
|
3399
5998
|
note,
|
|
5999
|
+
hasEgg,
|
|
6000
|
+
eggReady,
|
|
6001
|
+
eggTime: predictedEggTime || 0,
|
|
3400
6002
|
inspireReady: eggReady,
|
|
3401
6003
|
readyAt: predictedEggTime || 0,
|
|
3402
6004
|
gender
|
|
@@ -3436,23 +6038,23 @@ function homePlantIcon(deps, iconId) {
|
|
|
3436
6038
|
}
|
|
3437
6039
|
__name(homePlantIcon, "homePlantIcon");
|
|
3438
6040
|
var homePlantMapCache = null;
|
|
3439
|
-
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");
|
|
3440
6042
|
function resolveHomePlantMapPath() {
|
|
3441
6043
|
const candidates = [
|
|
3442
|
-
|
|
3443
|
-
|
|
6044
|
+
import_node_path5.default.resolve(__dirname, "..", HOME_PLANT_MAP_RELATIVE_PATH),
|
|
6045
|
+
import_node_path5.default.resolve(__dirname, HOME_PLANT_MAP_RELATIVE_PATH)
|
|
3444
6046
|
];
|
|
3445
|
-
return candidates.find((candidate) =>
|
|
6047
|
+
return candidates.find((candidate) => import_node_fs5.default.existsSync(candidate)) || candidates[0];
|
|
3446
6048
|
}
|
|
3447
6049
|
__name(resolveHomePlantMapPath, "resolveHomePlantMapPath");
|
|
3448
6050
|
function loadHomePlantMap() {
|
|
3449
6051
|
if (homePlantMapCache) return homePlantMapCache;
|
|
3450
6052
|
const filePath = resolveHomePlantMapPath();
|
|
3451
6053
|
try {
|
|
3452
|
-
const data = JSON.parse(
|
|
6054
|
+
const data = JSON.parse(import_node_fs5.default.readFileSync(filePath, "utf-8"));
|
|
3453
6055
|
homePlantMapCache = data && typeof data === "object" ? data : {};
|
|
3454
6056
|
} catch (err) {
|
|
3455
|
-
|
|
6057
|
+
logger10.warn(`加载家园作物映射失败: ${err}`);
|
|
3456
6058
|
homePlantMapCache = {};
|
|
3457
6059
|
}
|
|
3458
6060
|
return homePlantMapCache;
|
|
@@ -3474,7 +6076,7 @@ function extractHomePlants(deps, homeInfo) {
|
|
|
3474
6076
|
if (["", "0"].includes(String(plantId || "0"))) return null;
|
|
3475
6077
|
const mappedPlant = plantMap[String(plantId)] || {};
|
|
3476
6078
|
const iconId = plantData.icon_url || plantData.iconUrl || raw.icon_url || raw.iconUrl || plantData.iconid || raw.iconid || raw.icon_id || mappedPlant.iconid;
|
|
3477
|
-
let readyAt =
|
|
6079
|
+
let readyAt = normalizeEpochSeconds2(raw.plant_rip_time || raw.rip_time || raw.end_time);
|
|
3478
6080
|
const leftTime = Number(raw.left_time || 0);
|
|
3479
6081
|
if (!readyAt && leftTime > 0) readyAt = nowTs + leftTime;
|
|
3480
6082
|
const ready = Boolean(readyAt && nowTs >= readyAt) || ["2", "ready", "mature"].includes(String(raw.status));
|
|
@@ -3527,7 +6129,7 @@ function buildHomeRenderData(deps, res, uid) {
|
|
|
3527
6129
|
return 0;
|
|
3528
6130
|
});
|
|
3529
6131
|
const gardenPlots = extractHomePlants(deps, homeInfo);
|
|
3530
|
-
const createdAt =
|
|
6132
|
+
const createdAt = normalizeEpochSeconds2(res?.meta?.created_at);
|
|
3531
6133
|
return {
|
|
3532
6134
|
title: "洛克家园",
|
|
3533
6135
|
subtitle: "Home Information",
|
|
@@ -3688,11 +6290,31 @@ async function resolveHomeUid(deps, session, uid = "") {
|
|
|
3688
6290
|
return String(deps.userMgr.getPrimaryBinding(session?.userId || "")?.role_id || "");
|
|
3689
6291
|
}
|
|
3690
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
|
+
};
|
|
3691
6313
|
async function subscribeHome(deps, session, uid, kind) {
|
|
3692
6314
|
const target = sessionTarget(session);
|
|
3693
6315
|
if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds)) return "此指令仅限管理员使用。";
|
|
3694
6316
|
const targetUid = await resolveHomeUid(deps, session, uid);
|
|
3695
|
-
if (!targetUid) return kind
|
|
6317
|
+
if (!targetUid) return HOME_SUB_TEXT[kind].needUidHint;
|
|
3696
6318
|
const key = homeSubscriptionKey(session, targetUid, kind);
|
|
3697
6319
|
deps.homeSubMgr.upsert(key, {
|
|
3698
6320
|
key,
|
|
@@ -3706,7 +6328,7 @@ async function subscribeHome(deps, session, uid, kind) {
|
|
|
3706
6328
|
notify_state: {},
|
|
3707
6329
|
updated_at: Math.floor(Date.now() / 1e3)
|
|
3708
6330
|
});
|
|
3709
|
-
return
|
|
6331
|
+
return `已订阅 UID ${targetUid} ${HOME_SUB_TEXT[kind].subscribedHint}`;
|
|
3710
6332
|
}
|
|
3711
6333
|
__name(subscribeHome, "subscribeHome");
|
|
3712
6334
|
function homeSubscriptionState(data, kind) {
|
|
@@ -3716,6 +6338,12 @@ function homeSubscriptionState(data, kind) {
|
|
|
3716
6338
|
const names2 = readyItems2.map((item) => `田地${item.landIndex} ${item.plantName}`);
|
|
3717
6339
|
return { items: items2, readyItems: readyItems2, names: names2 };
|
|
3718
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
|
+
}
|
|
3719
6347
|
const items = [...data.indoorPets || [], ...data.guardPets || []].filter((item) => item.readyAt);
|
|
3720
6348
|
const readyItems = items.filter((item) => item.inspireReady);
|
|
3721
6349
|
const names = readyItems.map((item) => item.name || "未知精灵");
|
|
@@ -3723,8 +6351,7 @@ function homeSubscriptionState(data, kind) {
|
|
|
3723
6351
|
}
|
|
3724
6352
|
__name(homeSubscriptionState, "homeSubscriptionState");
|
|
3725
6353
|
function homeSubscriptionMessage(uid, kind, level, totalCount, readyItems, names) {
|
|
3726
|
-
const kindText = kind
|
|
3727
|
-
const actionText = kind === "garden" ? "成熟" : "完成";
|
|
6354
|
+
const { kindText, actionText } = HOME_SUB_TEXT[kind];
|
|
3728
6355
|
const levelText = level === "first" ? "首个" : "全部";
|
|
3729
6356
|
return [
|
|
3730
6357
|
`家园${kindText}${levelText}${actionText}提醒:${uid}`,
|
|
@@ -3739,7 +6366,7 @@ async function checkHomeSubscriptions(deps) {
|
|
|
3739
6366
|
let checkedCount = 0;
|
|
3740
6367
|
let pushedCount = 0;
|
|
3741
6368
|
for (const [key, sub] of Object.entries(subs)) {
|
|
3742
|
-
if (!sub.uid || !["garden", "inspiration"].includes(sub.kind)) continue;
|
|
6369
|
+
if (!sub.uid || !["garden", "inspiration", "egg"].includes(sub.kind)) continue;
|
|
3743
6370
|
checkedCount++;
|
|
3744
6371
|
if (!cache.has(sub.uid)) {
|
|
3745
6372
|
cache.set(sub.uid, await deps.client.ingameHomeInfo(deps.ctx, sub.uid, { timeoutMs: 3e4 }));
|
|
@@ -3774,7 +6401,7 @@ async function checkHomeSubscriptions(deps) {
|
|
|
3774
6401
|
}, messages.join("\n\n"));
|
|
3775
6402
|
if (!sent) continue;
|
|
3776
6403
|
} catch (e) {
|
|
3777
|
-
|
|
6404
|
+
logger10.warn(`家园订阅推送失败: ${e}`);
|
|
3778
6405
|
continue;
|
|
3779
6406
|
}
|
|
3780
6407
|
for (const level of pushLevels) notifyState[level] = true;
|
|
@@ -3830,19 +6457,19 @@ function register2(deps) {
|
|
|
3830
6457
|
const hasExtraProfileData = Boolean(profileSignature || profileHomeItems.length || profileCardItems.length || profileCardImage);
|
|
3831
6458
|
let degraded = false;
|
|
3832
6459
|
if (!sm) {
|
|
3833
|
-
|
|
6460
|
+
logger10.warn("[Rocom] 洛克.档案:pet-summary 接口不可用,已降级为基础档案渲染");
|
|
3834
6461
|
degraded = true;
|
|
3835
6462
|
}
|
|
3836
6463
|
if (!ev) {
|
|
3837
|
-
|
|
6464
|
+
logger10.warn("[Rocom] 洛克.档案:evaluation 接口不可用,已降级为基础档案渲染");
|
|
3838
6465
|
degraded = true;
|
|
3839
6466
|
}
|
|
3840
6467
|
if (!cl) {
|
|
3841
|
-
|
|
6468
|
+
logger10.warn("[Rocom] 洛克.档案:collection 接口不可用,已降级为基础档案渲染");
|
|
3842
6469
|
degraded = true;
|
|
3843
6470
|
}
|
|
3844
6471
|
if (!bo) {
|
|
3845
|
-
|
|
6472
|
+
logger10.warn("[Rocom] 洛克.档案:battle-overview 接口不可用,已降级为基础档案渲染");
|
|
3846
6473
|
degraded = true;
|
|
3847
6474
|
}
|
|
3848
6475
|
if (degraded) {
|
|
@@ -4194,6 +6821,81 @@ function register2(deps) {
|
|
|
4194
6821
|
if (!res) return `家园查询失败:${client.getLastErrorBrief()}`;
|
|
4195
6822
|
await sendImage(deps, session, "home", buildHomeRenderData(deps, res, targetUid), `【洛克家园】UID ${targetUid}`);
|
|
4196
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
|
+
});
|
|
4197
6899
|
ctx.command("洛克").subcommand(".刷新面板 [uid:string]", "刷新指定 UID 的精灵面板缓存").alias("洛克刷新面板").alias("刷新面板").alias("刷新精灵").alias("强制刷新").action(async ({ session }, uid = "") => {
|
|
4198
6900
|
const targetUid = panelUidFromBinding(deps, session, uid);
|
|
4199
6901
|
if (!targetUid) return "请提供 UID,或先绑定账号后再使用 洛克.刷新面板。";
|
|
@@ -4271,10 +6973,11 @@ function register2(deps) {
|
|
|
4271
6973
|
});
|
|
4272
6974
|
ctx.command("订阅家园菜园 [uid:string]", "订阅指定 UID 的家园菜园成熟提醒").action(async ({ session }, uid = "") => subscribeHome(deps, session, uid, "garden"));
|
|
4273
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"));
|
|
4274
6977
|
ctx.command("取消订阅家园 [kind:string] [uid:string]", "取消当前会话的家园订阅").action(async ({ session }, kind = "全部", uid = "") => {
|
|
4275
6978
|
const target = sessionTarget(session);
|
|
4276
6979
|
if (!target.userId && !isBotAdmin(session, deps.config.adminUserIds)) return "此指令仅限管理员使用。";
|
|
4277
|
-
const kindMap = { "菜园": "garden", "灵感": "inspiration", "全部": "", all: "", garden: "garden", inspiration: "inspiration" };
|
|
6980
|
+
const kindMap = { "菜园": "garden", "灵感": "inspiration", "生蛋": "egg", "全部": "", all: "", garden: "garden", inspiration: "inspiration", egg: "egg" };
|
|
4278
6981
|
const deleted = deps.homeSubMgr.deleteMatching(target, kindMap[String(kind || "全部")] ?? "", String(uid || "").trim());
|
|
4279
6982
|
return deleted ? `已取消 ${deleted} 条家园订阅。` : "当前会话没有匹配的家园订阅。";
|
|
4280
6983
|
});
|
|
@@ -4284,14 +6987,14 @@ function register2(deps) {
|
|
|
4284
6987
|
return `家园订阅检查完成:订阅 ${result.subscriptions} 条,检查 ${result.checked} 条,推送 ${result.pushed} 档提醒。`;
|
|
4285
6988
|
});
|
|
4286
6989
|
if (deps.config.homeSubscriptionEnabled) {
|
|
4287
|
-
ctx.setInterval(() => checkHomeSubscriptions(deps).catch((err) =>
|
|
6990
|
+
ctx.setInterval(() => checkHomeSubscriptions(deps).catch((err) => logger10.warn(`家园订阅检查失败: ${err}`)), Math.max(1, deps.config.homeSubscriptionIntervalMinutes || 5) * 6e4);
|
|
4288
6991
|
}
|
|
4289
6992
|
}
|
|
4290
6993
|
__name(register2, "register");
|
|
4291
6994
|
|
|
4292
6995
|
// src/commands/merchant.ts
|
|
4293
|
-
var
|
|
4294
|
-
var
|
|
6996
|
+
var import_koishi11 = require("koishi");
|
|
6997
|
+
var logger11 = new import_koishi11.Logger("rocom-merchant");
|
|
4295
6998
|
var TEXT = {
|
|
4296
6999
|
merchant: "远行商人",
|
|
4297
7000
|
todayMerchant: "今日远行商人",
|
|
@@ -4792,7 +7495,7 @@ async function checkMerchantSubscriptions(deps) {
|
|
|
4792
7495
|
const platform = sub.platform || ctx.bots[0]?.platform;
|
|
4793
7496
|
const channelId = sub.channel_id || sub.group_id || sub.user_id || key;
|
|
4794
7497
|
if (!platform || !channelId) {
|
|
4795
|
-
|
|
7498
|
+
logger11.warn(`推送失败 ${key}: 无法确定平台或频道`);
|
|
4796
7499
|
continue;
|
|
4797
7500
|
}
|
|
4798
7501
|
const fallbackText = `${msg}
|
|
@@ -4816,7 +7519,7 @@ ${fallback}`;
|
|
|
4816
7519
|
__name(checkMerchantSubscriptions, "checkMerchantSubscriptions");
|
|
4817
7520
|
function register3(deps) {
|
|
4818
7521
|
const { ctx, config, client, merchantSubMgr } = deps;
|
|
4819
|
-
ctx.command(TEXT.merchant, "查看远行商人商品").action(async ({ session }) => {
|
|
7522
|
+
ctx.command(TEXT.merchant, "查看远行商人商品").alias("yxsr").action(async ({ session }) => {
|
|
4820
7523
|
const res = await client.getMerchantInfo(ctx, true);
|
|
4821
7524
|
if (!res) return `获取远行商人数据失败:${client.getLastErrorBrief()}`;
|
|
4822
7525
|
const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
@@ -4875,6 +7578,13 @@ function register3(deps) {
|
|
|
4875
7578
|
return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
|
|
4876
7579
|
});
|
|
4877
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");
|
|
4878
7588
|
if (config.merchantCheckMode === "times" && config.merchantCheckTimes.length > 0) {
|
|
4879
7589
|
let lastMerchantCheckKey = "";
|
|
4880
7590
|
ctx.setInterval(async () => {
|
|
@@ -4884,11 +7594,11 @@ function register3(deps) {
|
|
|
4884
7594
|
const checkKey = `${now.toDateString()}-${timeStr}`;
|
|
4885
7595
|
if (checkKey === lastMerchantCheckKey) return;
|
|
4886
7596
|
lastMerchantCheckKey = checkKey;
|
|
4887
|
-
|
|
7597
|
+
runCheckWithJitter();
|
|
4888
7598
|
}, 6e4);
|
|
4889
7599
|
} else {
|
|
4890
7600
|
ctx.setInterval(async () => {
|
|
4891
|
-
|
|
7601
|
+
runCheckWithJitter();
|
|
4892
7602
|
}, config.merchantCheckInterval);
|
|
4893
7603
|
}
|
|
4894
7604
|
}
|
|
@@ -4896,11 +7606,156 @@ function register3(deps) {
|
|
|
4896
7606
|
__name(register3, "register");
|
|
4897
7607
|
|
|
4898
7608
|
// src/commands/wiki.ts
|
|
4899
|
-
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");
|
|
4900
7710
|
function register4(deps) {
|
|
4901
|
-
const { ctx } = deps;
|
|
4902
|
-
ctx.command("洛克").subcommand(".wiki
|
|
4903
|
-
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
|
+
});
|
|
4904
7759
|
}
|
|
4905
7760
|
__name(register4, "register");
|
|
4906
7761
|
|
|
@@ -5095,7 +7950,7 @@ function register5(deps) {
|
|
|
5095
7950
|
__name(register5, "register");
|
|
5096
7951
|
|
|
5097
7952
|
// src/commands/tools.ts
|
|
5098
|
-
var
|
|
7953
|
+
var import_koishi13 = require("koishi");
|
|
5099
7954
|
|
|
5100
7955
|
// src/activities-service.ts
|
|
5101
7956
|
var CHINA_TIMEZONE2 = "Asia/Shanghai";
|
|
@@ -5383,7 +8238,7 @@ var ActivitiesService = class {
|
|
|
5383
8238
|
};
|
|
5384
8239
|
|
|
5385
8240
|
// src/commands/tools.ts
|
|
5386
|
-
var
|
|
8241
|
+
var logger13 = new import_koishi13.Logger("rocom-tools");
|
|
5387
8242
|
var activitiesService = new ActivitiesService();
|
|
5388
8243
|
function trimText2(value) {
|
|
5389
8244
|
return String(value ?? "").trim();
|
|
@@ -5422,6 +8277,63 @@ function announcementId(item) {
|
|
|
5422
8277
|
return trimText2(item?.thread_id || item?.id);
|
|
5423
8278
|
}
|
|
5424
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");
|
|
5425
8337
|
function buildAnnouncementListText(data, page) {
|
|
5426
8338
|
const list = firstArray(data, ["list", "items"]);
|
|
5427
8339
|
if (!list.length) return page > 1 ? "该页没有更多公告。" : "当前没有公告数据。";
|
|
@@ -5497,11 +8409,45 @@ function register6(deps) {
|
|
|
5497
8409
|
if (!data) return `公告详情查询失败:${client.getLastErrorBrief()}`;
|
|
5498
8410
|
return buildAnnouncementDetailText(data);
|
|
5499
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
|
+
}
|
|
5500
8446
|
ctx.command("洛克").subcommand(".同步配置", "手动同步 RoCom 远端配置").alias("洛克同步配置").action(async ({ session }) => {
|
|
5501
8447
|
if (!config.adminUserIds.includes(session?.userId || "")) return "此指令仅限管理员使用。";
|
|
5502
8448
|
const data = await client.syncConfig(ctx, session?.userId || "");
|
|
5503
8449
|
if (!data) return `同步配置失败:${client.getLastErrorBrief()}`;
|
|
5504
|
-
|
|
8450
|
+
logger13.info(`manual config sync requested by ${session?.userId || "unknown"}`);
|
|
5505
8451
|
const skipped = Array.isArray(data?.skipped_resources) ? data.skipped_resources.length : 0;
|
|
5506
8452
|
return skipped ? `配置同步完成,但有 ${skipped} 个资源跳过。` : "配置同步完成。";
|
|
5507
8453
|
});
|
|
@@ -5509,8 +8455,8 @@ function register6(deps) {
|
|
|
5509
8455
|
__name(register6, "register");
|
|
5510
8456
|
|
|
5511
8457
|
// src/commands/admin.ts
|
|
5512
|
-
var
|
|
5513
|
-
var
|
|
8458
|
+
var import_koishi14 = require("koishi");
|
|
8459
|
+
var logger14 = new import_koishi14.Logger("rocom-admin");
|
|
5514
8460
|
function register7(deps) {
|
|
5515
8461
|
const { ctx, config, client, userMgr } = deps;
|
|
5516
8462
|
function isAdmin(userId) {
|
|
@@ -5556,7 +8502,7 @@ function register7(deps) {
|
|
|
5556
8502
|
for (const [userId, bindings] of Object.entries(allUsers)) {
|
|
5557
8503
|
const token = await getRoleToken(ctx, userId);
|
|
5558
8504
|
if (!token?.fwt) {
|
|
5559
|
-
|
|
8505
|
+
logger14.warn(`用户 ${userId} 没有可用的 fwt,跳过失效检测`);
|
|
5560
8506
|
totalSkipped += bindings.length;
|
|
5561
8507
|
continue;
|
|
5562
8508
|
}
|
|
@@ -5583,7 +8529,7 @@ function register7(deps) {
|
|
|
5583
8529
|
try {
|
|
5584
8530
|
await client.deleteBinding(ctx, binding.binding_id, userId);
|
|
5585
8531
|
} catch (e) {
|
|
5586
|
-
|
|
8532
|
+
logger14.warn(`删除用户 ${userId} 服务端绑定 ${binding.binding_id} 失败:${e}`);
|
|
5587
8533
|
}
|
|
5588
8534
|
}
|
|
5589
8535
|
totalInvalid++;
|
|
@@ -5622,7 +8568,7 @@ function register7(deps) {
|
|
|
5622
8568
|
});
|
|
5623
8569
|
}
|
|
5624
8570
|
} catch (e) {
|
|
5625
|
-
|
|
8571
|
+
logger14.warn(`自动刷新用户 ${userId} 失败: ${e}`);
|
|
5626
8572
|
}
|
|
5627
8573
|
}
|
|
5628
8574
|
}, 3e4);
|
|
@@ -5633,7 +8579,7 @@ __name(register7, "register");
|
|
|
5633
8579
|
// src/index.ts
|
|
5634
8580
|
var name = "rocom";
|
|
5635
8581
|
var inject = { required: ["puppeteer", "database"] };
|
|
5636
|
-
var
|
|
8582
|
+
var logger15 = new import_koishi15.Logger("rocom");
|
|
5637
8583
|
var MENU_PAGE_TITLE = "洛克王国帮助菜单";
|
|
5638
8584
|
var MENU_PAGE_SUBTITLE = "输入对应命令可查看功能,部分数据查询需要先完成账号绑定。";
|
|
5639
8585
|
var MENU_GROUPS = [
|
|
@@ -5661,6 +8607,7 @@ var MENU_GROUPS = [
|
|
|
5661
8607
|
{ cmd: "洛克.交换大厅", desc: "查看交换大厅" },
|
|
5662
8608
|
{ cmd: "洛克.玩家", desc: "查询 ingame 玩家资料" },
|
|
5663
8609
|
{ cmd: "洛克.家园", desc: "查询家园菜园" },
|
|
8610
|
+
{ cmd: "洛克.家园详情", desc: "家园精灵完整数据" },
|
|
5664
8611
|
{ cmd: "洛克.商店", desc: "查询 ingame 商店" },
|
|
5665
8612
|
{ cmd: "洛克.日历", desc: "查看活动日历" },
|
|
5666
8613
|
{ cmd: "洛克.公告", desc: "查看公告列表" },
|
|
@@ -5676,14 +8623,19 @@ var MENU_GROUPS = [
|
|
|
5676
8623
|
{ cmd: "取消订阅远行商人", desc: "取消商人提醒" },
|
|
5677
8624
|
{ cmd: "订阅家园菜园", desc: "菜园成熟提醒" },
|
|
5678
8625
|
{ cmd: "订阅家园灵感", desc: "精灵灵感提醒" },
|
|
5679
|
-
{ cmd: "
|
|
8626
|
+
{ cmd: "订阅家园生蛋", desc: "精灵生蛋提醒" },
|
|
8627
|
+
{ cmd: "取消订阅家园", desc: "取消家园订阅" },
|
|
8628
|
+
{ cmd: "订阅洛克公告", desc: "新公告推送提醒" },
|
|
8629
|
+
{ cmd: "取消订阅洛克公告", desc: "取消公告订阅" }
|
|
5680
8630
|
]
|
|
5681
8631
|
},
|
|
5682
8632
|
{
|
|
5683
8633
|
groupTitle: "百科与查蛋",
|
|
5684
8634
|
menuItems: [
|
|
5685
|
-
{ cmd: "洛克.wiki", desc: "
|
|
8635
|
+
{ cmd: "洛克.wiki", desc: "Wiki 全局/分类查询" },
|
|
5686
8636
|
{ cmd: "洛克.技能", desc: "技能 Wiki 查询" },
|
|
8637
|
+
{ cmd: "精灵图鉴", desc: "本地图鉴查图" },
|
|
8638
|
+
{ cmd: "图鉴下载", desc: "下载图鉴缓存(管理员)" },
|
|
5687
8639
|
{ cmd: "洛克.查蛋", desc: "精灵查蛋 / 尺寸反查" },
|
|
5688
8640
|
{ cmd: "洛克.配种", desc: "配种查询" }
|
|
5689
8641
|
]
|
|
@@ -5709,56 +8661,69 @@ function buildMenuFallbackText() {
|
|
|
5709
8661
|
return lines.join("\n").trimEnd();
|
|
5710
8662
|
}
|
|
5711
8663
|
__name(buildMenuFallbackText, "buildMenuFallbackText");
|
|
5712
|
-
var Config =
|
|
5713
|
-
|
|
5714
|
-
apiBaseUrl:
|
|
5715
|
-
wegameApiKey:
|
|
5716
|
-
qqLoginDebugMode:
|
|
5717
|
-
adminUserIds:
|
|
5718
|
-
autoRefreshEnabled:
|
|
5719
|
-
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("自动刷新时间")
|
|
5720
8672
|
}).description("基础设置"),
|
|
5721
|
-
|
|
5722
|
-
imageCompressionEnabled:
|
|
5723
|
-
imageCompressionMinBytes:
|
|
5724
|
-
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")
|
|
5725
8677
|
}).description("图片压缩设置"),
|
|
5726
|
-
|
|
5727
|
-
merchantSubscriptionEnabled:
|
|
5728
|
-
merchantSubscriptionItems:
|
|
5729
|
-
merchantUiStyle:
|
|
5730
|
-
merchantCheckMode:
|
|
5731
|
-
merchantCheckInterval:
|
|
5732
|
-
merchantCheckTimes:
|
|
5733
|
-
merchantPrivateSubscriptionEnabled:
|
|
5734
|
-
homeSubscriptionEnabled:
|
|
5735
|
-
homeSubscriptionIntervalMinutes:
|
|
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("公告订阅检查间隔,单位分钟")
|
|
5736
8690
|
}).description("订阅推送设置"),
|
|
5737
|
-
|
|
5738
|
-
homeQueryWaitMs:
|
|
5739
|
-
homeQueryPollIntervalMs:
|
|
5740
|
-
homeQueryTimeoutMs:
|
|
5741
|
-
|
|
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("图鉴下载设置")
|
|
5742
8704
|
]);
|
|
5743
8705
|
function apply(ctx, config) {
|
|
5744
8706
|
setupRoleTokenModel(ctx);
|
|
5745
8707
|
const client = new RocomClient(config.apiBaseUrl, config.wegameApiKey);
|
|
5746
|
-
const dataDir =
|
|
8708
|
+
const dataDir = import_node_path6.default.join(ctx.baseDir, "data", "rocom");
|
|
5747
8709
|
const userMgr = new UserManager(dataDir);
|
|
5748
8710
|
const merchantSubMgr = new MerchantSubscriptionManager(dataDir);
|
|
5749
8711
|
const homeSubMgr = new HomeSubscriptionManager(dataDir);
|
|
5750
|
-
const
|
|
8712
|
+
const announcementSubMgr = new AnnouncementSubscriptionManager(dataDir);
|
|
8713
|
+
const resPath = import_node_path6.default.resolve(__dirname, "..");
|
|
5751
8714
|
const renderer = new Renderer(resPath);
|
|
5752
|
-
const renderTemplateRoot =
|
|
5753
|
-
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");
|
|
5754
8717
|
const eggService = new EggService(searcheggsDir);
|
|
5755
|
-
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 };
|
|
5756
8721
|
ctx.on("ready", () => {
|
|
5757
8722
|
migrateRoleTokensToUserId(ctx).catch((err) => {
|
|
5758
|
-
|
|
8723
|
+
logger15.warn(`role token migration failed: ${err}`);
|
|
5759
8724
|
});
|
|
5760
8725
|
migrateLegacyFrameworkTokens(ctx, userMgr).catch((err) => {
|
|
5761
|
-
|
|
8726
|
+
logger15.warn(`legacy framework token migration failed: ${err}`);
|
|
5762
8727
|
});
|
|
5763
8728
|
});
|
|
5764
8729
|
ctx.command("洛克", "洛克王国帮助菜单").action(async ({ session }) => {
|