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.
Files changed (36) hide show
  1. package/lib/atlas-service.d.ts +33 -0
  2. package/lib/atlas-service.js +439 -0
  3. package/lib/client.d.ts +19 -0
  4. package/lib/client.js +101 -1
  5. package/lib/commands/account.js +1 -0
  6. package/lib/commands/merchant.js +11 -2
  7. package/lib/commands/query.js +141 -9
  8. package/lib/commands/tools.js +104 -0
  9. package/lib/commands/wiki.js +167 -6
  10. package/lib/egg-service.d.ts +2 -0
  11. package/lib/egg-service.js +50 -15
  12. package/lib/index.d.ts +5 -0
  13. package/lib/index.js +3153 -188
  14. package/lib/pet-data-service.d.ts +62 -0
  15. package/lib/pet-data-service.js +468 -0
  16. package/lib/render-templates/pet-data/index.html +172 -0
  17. package/lib/render-templates/pet-data/style.css +553 -0
  18. package/lib/render-templates/wiki/detail/index.html +84 -0
  19. package/lib/render-templates/wiki/list/index.html +84 -0
  20. package/lib/render-templates/wiki/menu/index.html +65 -0
  21. package/lib/render-templates/wiki/pet/index.html +158 -0
  22. package/lib/render-templates/wiki/pet/style.css +393 -0
  23. package/lib/render-templates/wiki/shared/style.css +431 -0
  24. package/lib/render-templates/wiki/skill/index.html +79 -0
  25. package/lib/render-templates/wiki/skill/style.css +233 -0
  26. package/lib/render-templates/wiki/suggestions/index.html +84 -0
  27. package/lib/render.js +37 -8
  28. package/lib/types.d.ts +9 -1
  29. package/lib/user.d.ts +23 -1
  30. package/lib/user.js +32 -1
  31. package/lib/wiki-catalog.d.ts +19 -0
  32. package/lib/wiki-catalog.js +124 -0
  33. package/lib/wiki-service.d.ts +220 -0
  34. package/lib/wiki-service.js +1463 -0
  35. package/package.json +1 -1
  36. package/readme.md +4 -5
@@ -0,0 +1,62 @@
1
+ export interface PetDataOptionMaps {
2
+ natures: Record<string, string>;
3
+ bloodlines: Record<string, string>;
4
+ types: Record<string, string>;
5
+ talent_ratings: Record<string, string>;
6
+ }
7
+ export declare function petDataDisplay(value: any, defaultText?: string): string;
8
+ export declare function petDataImageUrl(petId: any, imageType?: 'image' | 'icon'): string;
9
+ export declare function petDataWikiPetId(value: any): string;
10
+ export declare function normalizeEpochSeconds(value: any): number;
11
+ export declare function petDataTimeText(value: any): string;
12
+ export declare function petDataSizeText(value: any, unit: 'g' | 'cm' | string): string;
13
+ export declare function petDataVoiceText(value: any): string;
14
+ export declare function petDataVoiceInfo(value: any): {
15
+ value: string;
16
+ hint: string;
17
+ className: string;
18
+ };
19
+ export declare function petDataKgCompact(grams: any): string;
20
+ export declare function petDataWeightSizeInfo(pet: any, wikiPet: any): {
21
+ label?: string;
22
+ className?: string;
23
+ hint?: string;
24
+ };
25
+ export declare function petDataOptionMaps(options: any): PetDataOptionMaps;
26
+ export declare function petDataLookup(mapping: Record<string, string>, value: any, defaultText?: string): string;
27
+ export declare function petDataVariant(pet: any, fallback?: any): {
28
+ variantText: string;
29
+ variantIcon: string;
30
+ };
31
+ export declare function petDataAttributes(pet: any): Array<{
32
+ label: string;
33
+ value: string;
34
+ race: string;
35
+ talent: string;
36
+ percent: number;
37
+ }>;
38
+ export declare function petDataSkillItems(pet: any): any[];
39
+ export declare function petDataSkillIdsFromPayload(payload: any): string[];
40
+ export declare function petDataPetIdsFromPayload(payload: any): string[];
41
+ export declare function petDataSkillIconUrl(baseUrl: string, skillId: any, detail?: any): string;
42
+ export declare function petDataSkills(pet: any, skillLookup?: Record<string, any>, options?: {
43
+ loadSkillIcons?: boolean;
44
+ baseUrl?: string;
45
+ }): any[];
46
+ export declare function petDataCardItems(pet: any, optionMaps: PetDataOptionMaps, sizeInfo?: {
47
+ label?: string;
48
+ className?: string;
49
+ hint?: string;
50
+ }): any[];
51
+ export declare function petDataExtractItems(payload: any, optionMaps: PetDataOptionMaps, skillLookup?: Record<string, any>, sizeLookup?: Record<string, any>, options?: {
52
+ loadSkillIcons?: boolean;
53
+ baseUrl?: string;
54
+ }): any[];
55
+ export declare function buildPetDataRenderData(res: any, uid: string, options?: {
56
+ optionsPayload?: any;
57
+ skillLookup?: Record<string, any>;
58
+ sizeLookup?: Record<string, any>;
59
+ singleQuery?: boolean;
60
+ lowBandwidthMode?: boolean;
61
+ baseUrl?: string;
62
+ }): any;
@@ -0,0 +1,468 @@
1
+ "use strict";
2
+ // 家园详情(ingame pet/data)渲染数据构造(移植自上游 astrbot v3.6.1/v3.7.0)。
3
+ // 纯函数模块,便于单元测试。
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.petDataDisplay = petDataDisplay;
6
+ exports.petDataImageUrl = petDataImageUrl;
7
+ exports.petDataWikiPetId = petDataWikiPetId;
8
+ exports.normalizeEpochSeconds = normalizeEpochSeconds;
9
+ exports.petDataTimeText = petDataTimeText;
10
+ exports.petDataSizeText = petDataSizeText;
11
+ exports.petDataVoiceText = petDataVoiceText;
12
+ exports.petDataVoiceInfo = petDataVoiceInfo;
13
+ exports.petDataKgCompact = petDataKgCompact;
14
+ exports.petDataWeightSizeInfo = petDataWeightSizeInfo;
15
+ exports.petDataOptionMaps = petDataOptionMaps;
16
+ exports.petDataLookup = petDataLookup;
17
+ exports.petDataVariant = petDataVariant;
18
+ exports.petDataAttributes = petDataAttributes;
19
+ exports.petDataSkillItems = petDataSkillItems;
20
+ exports.petDataSkillIdsFromPayload = petDataSkillIdsFromPayload;
21
+ exports.petDataPetIdsFromPayload = petDataPetIdsFromPayload;
22
+ exports.petDataSkillIconUrl = petDataSkillIconUrl;
23
+ exports.petDataSkills = petDataSkills;
24
+ exports.petDataCardItems = petDataCardItems;
25
+ exports.petDataExtractItems = petDataExtractItems;
26
+ exports.buildPetDataRenderData = buildPetDataRenderData;
27
+ const CN_TIME_ZONE = 'Asia/Shanghai';
28
+ function petDataDisplay(value, defaultText = '--') {
29
+ if (value === undefined || value === null || value === '')
30
+ return defaultText;
31
+ if (typeof value === 'boolean')
32
+ return value ? '是' : '否';
33
+ return String(value);
34
+ }
35
+ function petDataImageUrl(petId, imageType = 'image') {
36
+ const assetIdRaw = parseInt(String(petId), 10);
37
+ if (!Number.isFinite(assetIdRaw) || assetIdRaw <= 0)
38
+ return '';
39
+ const assetId = assetIdRaw < 3000 ? assetIdRaw + 3000 : assetIdRaw;
40
+ const type = imageType === 'icon' ? 'icon' : 'image';
41
+ return `https://game.gtimg.cn/images/rocom/rocodata/jingling/${assetId}/${type}.png`;
42
+ }
43
+ function petDataWikiPetId(value) {
44
+ const petId = parseInt(String(value), 10);
45
+ if (!Number.isFinite(petId) || petId <= 0)
46
+ return '';
47
+ return String(petId < 3000 ? petId + 3000 : petId);
48
+ }
49
+ function normalizeEpochSeconds(value) {
50
+ const num = Number(value);
51
+ if (!Number.isFinite(num) || num <= 0)
52
+ return 0;
53
+ // 毫秒时间戳转秒
54
+ return num > 10000000000 ? Math.floor(num / 1000) : Math.floor(num);
55
+ }
56
+ function petDataTimeText(value) {
57
+ const ts = normalizeEpochSeconds(value);
58
+ if (!ts)
59
+ return '--';
60
+ const date = new Date(ts * 1000);
61
+ const formatter = new Intl.DateTimeFormat('zh-CN', {
62
+ timeZone: CN_TIME_ZONE,
63
+ year: 'numeric', month: '2-digit', day: '2-digit',
64
+ hour: '2-digit', minute: '2-digit', hour12: false,
65
+ });
66
+ const parts = Object.fromEntries(formatter.formatToParts(date).map(p => [p.type, p.value]));
67
+ return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`;
68
+ }
69
+ function petDataSizeText(value, unit) {
70
+ const number = Number(value);
71
+ if (!Number.isFinite(number))
72
+ return '--';
73
+ if (unit === 'g')
74
+ return `${(number / 1000).toFixed(2)} kg`;
75
+ if (unit === 'cm')
76
+ return `${number} cm`;
77
+ return `${number} ${unit}`.trim();
78
+ }
79
+ function petDataVoiceText(value) {
80
+ if (value === undefined || value === null || value === '')
81
+ return '--';
82
+ const number = Number(value);
83
+ if (!Number.isFinite(number))
84
+ return petDataDisplay(value);
85
+ return Number.isInteger(number) ? `${number} dB` : `${number} dB`;
86
+ }
87
+ function petDataVoiceInfo(value) {
88
+ const text = petDataVoiceText(value);
89
+ const number = Number(value);
90
+ if (!Number.isFinite(number))
91
+ return { value: text, hint: '', className: '' };
92
+ const intNumber = Math.trunc(number);
93
+ if (intNumber >= 96 && intNumber <= 100) {
94
+ return { value: `${text} · 婉转声`, hint: '婉转声 96~100', className: 'voice-soft' };
95
+ }
96
+ if (intNumber >= -100 && intNumber <= -96) {
97
+ return { value: `${text} · 粗嗓门`, hint: '粗嗓门 -96~-100', className: 'voice-rough' };
98
+ }
99
+ return { value: text, hint: '婉转声 96~100 / 粗嗓门 -96~-100', className: '' };
100
+ }
101
+ function petDataKgCompact(grams) {
102
+ const number = Number(grams);
103
+ if (!Number.isFinite(number))
104
+ return '--';
105
+ const text = (number / 1000).toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
106
+ return `${text}kg`;
107
+ }
108
+ // 上游 v3.7.0:接入 Wiki 精灵体重范围,前 5% 标小块头、后 5% 标大块头。
109
+ function petDataWeightSizeInfo(pet, wikiPet) {
110
+ const bodySize = wikiPet && typeof wikiPet === 'object' ? wikiPet.body_size : null;
111
+ const weightRange = bodySize && typeof bodySize === 'object' ? bodySize.weight : null;
112
+ if (!weightRange || typeof weightRange !== 'object')
113
+ return {};
114
+ const current = Number(pet?.weight);
115
+ const low = Number(weightRange.min_g);
116
+ const high = Number(weightRange.max_g);
117
+ if (!Number.isFinite(current) || !Number.isFinite(low) || !Number.isFinite(high))
118
+ return {};
119
+ if (high <= low)
120
+ return {};
121
+ const span = high - low;
122
+ const smallCut = low + span * 0.05;
123
+ const largeCut = high - span * 0.05;
124
+ const rangeText = `${petDataKgCompact(low)}-${petDataKgCompact(high)}`;
125
+ if (current <= smallCut) {
126
+ return { label: '小块头', className: 'size-small', hint: `小块头 · ≤${petDataKgCompact(smallCut)} · 范围 ${rangeText}` };
127
+ }
128
+ if (current >= largeCut) {
129
+ return { label: '大块头', className: 'size-large', hint: `大块头 · ≥${petDataKgCompact(largeCut)} · 范围 ${rangeText}` };
130
+ }
131
+ return { label: '', className: '', hint: `范围 ${rangeText}` };
132
+ }
133
+ function petDataOptionMaps(options) {
134
+ const petOptions = options && typeof options === 'object' && options.pet && typeof options.pet === 'object' ? options.pet : {};
135
+ const buildMap = (key, preferShort = false) => {
136
+ const result = {};
137
+ const values = petOptions[key];
138
+ if (!Array.isArray(values))
139
+ return result;
140
+ for (const item of values) {
141
+ if (!item || typeof item !== 'object')
142
+ continue;
143
+ const itemId = item.id;
144
+ if (itemId === undefined || itemId === null || itemId === '')
145
+ continue;
146
+ const name = preferShort && item.short_name ? item.short_name : item.name;
147
+ if (name)
148
+ result[String(itemId)] = String(name);
149
+ }
150
+ return result;
151
+ };
152
+ const natures = {};
153
+ for (const item of petOptions.natures || []) {
154
+ if (!item || typeof item !== 'object' || item.id === undefined || item.id === null || item.id === '')
155
+ continue;
156
+ const name = String(item.name || item.id);
157
+ const summary = String(item.summary || '').trim();
158
+ natures[String(item.id)] = summary ? `${name}(${summary})` : name;
159
+ }
160
+ return {
161
+ natures,
162
+ bloodlines: buildMap('bloodlines', true),
163
+ types: buildMap('types'),
164
+ talent_ratings: buildMap('talent_ratings'),
165
+ };
166
+ }
167
+ function petDataLookup(mapping, value, defaultText = '--') {
168
+ const key = String(value ?? '').trim();
169
+ if (!key)
170
+ return defaultText;
171
+ return mapping[key] || key;
172
+ }
173
+ // 变体识别:mutation_type 9/1/8 或 mutation_name 或 real_speciality_ids(103=异色、502=炫彩)。
174
+ function petDataVariant(pet, fallback = {}) {
175
+ fallback = fallback || {};
176
+ const mutationName = String(pet?.mutation_name || fallback?.mutation_name || fallback?.pet_mutation_name || '').trim();
177
+ const mutationType = String(pet?.mutation_type ?? fallback?.mutation_type ?? '').trim();
178
+ const specialityValues = [];
179
+ for (const value of [pet?.real_speciality_ids, fallback?.real_speciality_ids, pet?.speciality_id, fallback?.speciality_id]) {
180
+ if (Array.isArray(value))
181
+ specialityValues.push(...value);
182
+ else if (value !== undefined && value !== null && value !== '')
183
+ specialityValues.push(value);
184
+ }
185
+ const specialityIds = new Set(specialityValues.map(value => String(value).trim()).filter(Boolean));
186
+ if (mutationType === '9' || (mutationName.includes('异色') && mutationName.includes('炫彩')) || (specialityIds.has('103') && specialityIds.has('502'))) {
187
+ return { variantText: '异色炫彩', variantIcon: '异色炫彩.png' };
188
+ }
189
+ if (mutationType === '1' || mutationName.includes('异色') || specialityIds.has('103')) {
190
+ return { variantText: '异色', variantIcon: '异色.png' };
191
+ }
192
+ if (mutationType === '8' || mutationName.includes('炫彩') || specialityIds.has('502')) {
193
+ return { variantText: '炫彩', variantIcon: '炫彩.png' };
194
+ }
195
+ return { variantText: mutationName || '普通', variantIcon: '' };
196
+ }
197
+ function petDataAttributes(pet) {
198
+ const attributeInfo = pet?.attribute_info && typeof pet.attribute_info === 'object' ? pet.attribute_info : {};
199
+ const fields = [
200
+ ['hp', '生命'],
201
+ ['attack', '物攻'],
202
+ ['special_attack', '魔攻'],
203
+ ['defense', '物防'],
204
+ ['special_defense', '魔防'],
205
+ ['speed', '速度'],
206
+ ];
207
+ return fields.map(([key, label]) => {
208
+ const raw = attributeInfo[key] && typeof attributeInfo[key] === 'object' ? attributeInfo[key] : {};
209
+ let percent = 6;
210
+ const base = Number(raw.base_value);
211
+ if (Number.isFinite(base))
212
+ percent = Math.trunc(Math.max(6, Math.min(100, base / 200 * 100)));
213
+ return {
214
+ label,
215
+ value: petDataDisplay(raw.base_value),
216
+ race: petDataDisplay(raw.total_race),
217
+ talent: petDataDisplay(raw.talent),
218
+ percent,
219
+ };
220
+ });
221
+ }
222
+ function petDataSkillItems(pet) {
223
+ const skillRoot = pet?.skill && typeof pet.skill === 'object' ? pet.skill : {};
224
+ const skills = Array.isArray(skillRoot.skill_data) ? skillRoot.skill_data : [];
225
+ const sortKey = (item) => {
226
+ const equippedRank = item?.is_equipped ? 0 : 1;
227
+ const learnedRank = item?.is_learned ? 0 : 1;
228
+ let pos = parseInt(String(item?.pos ?? 99), 10);
229
+ if (!Number.isFinite(pos))
230
+ pos = 99;
231
+ return [equippedRank, learnedRank, pos];
232
+ };
233
+ return skills
234
+ .filter((s) => s && typeof s === 'object')
235
+ .sort((a, b) => {
236
+ const ka = sortKey(a);
237
+ const kb = sortKey(b);
238
+ return ka[0] - kb[0] || ka[1] - kb[1] || ka[2] - kb[2];
239
+ });
240
+ }
241
+ function extractRawItems(payload) {
242
+ const rawItems = [];
243
+ if (Array.isArray(payload?.npc_pets)) {
244
+ rawItems.push(...payload.npc_pets.filter((item) => item && typeof item === 'object'));
245
+ }
246
+ else if (payload?.npc_pet && typeof payload.npc_pet === 'object') {
247
+ const query = payload?.query && typeof payload.query === 'object' ? payload.query : {};
248
+ rawItems.push({
249
+ status: 'ok',
250
+ npc_pet: payload.npc_pet,
251
+ pet_gid: query.pet_gid,
252
+ furniture_guid: query.npc_id || query.furniture_guid,
253
+ npc_id_source: 'query',
254
+ });
255
+ }
256
+ return rawItems;
257
+ }
258
+ function petDataSkillIdsFromPayload(payload) {
259
+ let root = payload || {};
260
+ if (root.result && typeof root.result === 'object')
261
+ root = root.result;
262
+ const ids = [];
263
+ for (const raw of extractRawItems(root)) {
264
+ const npcPet = raw?.npc_pet && typeof raw.npc_pet === 'object' ? raw.npc_pet : {};
265
+ const pet = npcPet?.pet && typeof npcPet.pet === 'object' ? npcPet.pet : {};
266
+ for (const item of petDataSkillItems(pet).slice(0, 8)) {
267
+ const skillId = String(item?.id ?? '').trim();
268
+ if (skillId && !ids.includes(skillId))
269
+ ids.push(skillId);
270
+ }
271
+ }
272
+ return ids;
273
+ }
274
+ function petDataPetIdsFromPayload(payload) {
275
+ let root = payload || {};
276
+ if (root.result && typeof root.result === 'object')
277
+ root = root.result;
278
+ const ids = [];
279
+ for (const raw of extractRawItems(root)) {
280
+ const npcPet = raw?.npc_pet && typeof raw.npc_pet === 'object' ? raw.npc_pet : {};
281
+ const pet = npcPet?.pet && typeof npcPet.pet === 'object' ? npcPet.pet : {};
282
+ const petId = petDataWikiPetId(pet.base_conf_id || raw.pet_cfg_id || pet.catch_base_id || pet.conf_id);
283
+ if (petId && !ids.includes(petId))
284
+ ids.push(petId);
285
+ }
286
+ return ids;
287
+ }
288
+ function petDataSkillIconUrl(baseUrl, skillId, detail = null) {
289
+ const icon = String(detail?.icon || '').trim();
290
+ if (icon)
291
+ return icon;
292
+ const skillText = String(skillId ?? '').trim();
293
+ if (!skillText)
294
+ return '';
295
+ return `${baseUrl}/api/v1/resources/wiki/assets/skills/${skillText}.png`;
296
+ }
297
+ function wikiNamedValue(value) {
298
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
299
+ return String(value.name || value.label || value.value || '');
300
+ }
301
+ return String(value ?? '');
302
+ }
303
+ function petDataSkills(pet, skillLookup = {}, options = {}) {
304
+ const loadSkillIcons = options.loadSkillIcons !== false;
305
+ const baseUrl = options.baseUrl || '';
306
+ const result = [];
307
+ for (const item of petDataSkillItems(pet).slice(0, 8)) {
308
+ const skillId = petDataDisplay(item?.id);
309
+ const detail = skillId !== '--' ? skillLookup[String(skillId)] : null;
310
+ let status;
311
+ let statusClass;
312
+ if (item?.is_equipped) {
313
+ status = '已装备';
314
+ statusClass = 'equipped';
315
+ }
316
+ else if (item?.is_learned) {
317
+ status = '已学会';
318
+ statusClass = 'learned';
319
+ }
320
+ else {
321
+ status = '未学会';
322
+ statusClass = 'locked';
323
+ }
324
+ const element = detail ? wikiNamedValue(detail.element_type) : '';
325
+ const skillType = detail ? wikiNamedValue(detail.skill_type || detail.damage_type) : '';
326
+ result.push({
327
+ id: skillId,
328
+ name: String(detail?.name || skillId),
329
+ icon: loadSkillIcons ? petDataSkillIconUrl(baseUrl, skillId, detail) : '',
330
+ element: element || '未知',
331
+ type: skillType || '技能',
332
+ power: petDataDisplay(detail?.power),
333
+ cost: petDataDisplay(detail?.cost),
334
+ pos: petDataDisplay(item?.pos),
335
+ status,
336
+ statusClass,
337
+ unlock: item?.unlock_need_lv !== undefined && item?.unlock_need_lv !== null && item?.unlock_need_lv !== '' && item?.unlock_need_lv !== 0
338
+ ? `Lv.${item.unlock_need_lv}`
339
+ : '--',
340
+ description: String(detail?.description || detail?.desc || ''),
341
+ });
342
+ }
343
+ return result;
344
+ }
345
+ function petDataCardItems(pet, optionMaps, sizeInfo = {}) {
346
+ const genderMap = { '0': '未知', '1': '雄性', '2': '雌性' };
347
+ const gender = genderMap[String(pet?.gender ?? '')] || petDataDisplay(pet?.gender);
348
+ const nature = petDataLookup(optionMaps.natures, pet?.nature);
349
+ const blood = petDataLookup(optionMaps.bloodlines, pet?.blood_id);
350
+ const talentRank = petDataLookup(optionMaps.talent_ratings, pet?.talent_rank, petDataDisplay(pet?.talent_rank));
351
+ const voiceInfo = petDataVoiceInfo(pet?.voice);
352
+ let weightValue = petDataSizeText(pet?.weight, 'g');
353
+ if (sizeInfo.label)
354
+ weightValue = `${weightValue} · ${sizeInfo.label}`;
355
+ return [
356
+ { label: '等级', value: petDataDisplay(pet?.level) },
357
+ { label: '性别', value: gender },
358
+ { label: '分贝', ...voiceInfo },
359
+ { label: '性格', value: nature },
360
+ { label: '血脉', value: blood },
361
+ { label: '天赋评级', value: talentRank },
362
+ { label: '身高', value: petDataSizeText(pet?.height, 'cm') },
363
+ {
364
+ label: '体重',
365
+ value: weightValue,
366
+ hint: sizeInfo.hint || '',
367
+ className: sizeInfo.className || '',
368
+ },
369
+ ];
370
+ }
371
+ function petDataExtractItems(payload, optionMaps, skillLookup = {}, sizeLookup = {}, options = {}) {
372
+ const rawItems = extractRawItems(payload);
373
+ const pets = [];
374
+ rawItems.forEach((raw, index) => {
375
+ const npcPet = raw?.npc_pet && typeof raw.npc_pet === 'object' ? raw.npc_pet : {};
376
+ const pet = npcPet?.pet && typeof npcPet.pet === 'object' ? npcPet.pet : {};
377
+ const status = String(raw?.status || (Object.keys(pet).length ? 'ok' : 'unknown'));
378
+ const { variantText, variantIcon } = petDataVariant(pet, raw);
379
+ const baseId = pet.base_conf_id || raw.pet_cfg_id || pet.catch_base_id || pet.conf_id;
380
+ const wikiPetId = petDataWikiPetId(baseId);
381
+ const sizeInfo = petDataWeightSizeInfo(pet, sizeLookup[wikiPetId]);
382
+ const name = pet.name || pet.pet_default_name || raw.pet_default_name || raw.name || `精灵 ${baseId || index + 1}`;
383
+ const defaultName = pet.pet_default_name || raw.pet_default_name || '';
384
+ const displayDefault = defaultName && defaultName !== name ? defaultName : '';
385
+ const petGid = pet.gid || raw.pet_gid;
386
+ const sceneInfo = pet.scene_info && typeof pet.scene_info === 'object' ? pet.scene_info : null;
387
+ const furnitureGuid = raw.furniture_guid ?? (sceneInfo ? sceneInfo.npc_id : undefined);
388
+ pets.push({
389
+ index: index + 1,
390
+ status,
391
+ statusText: status === 'ok' ? '成功' : status,
392
+ retCode: petDataDisplay(npcPet.ret_code),
393
+ name: String(name),
394
+ defaultName: String(displayDefault),
395
+ level: petDataDisplay(pet.level),
396
+ baseId: petDataDisplay(baseId),
397
+ confId: petDataDisplay(pet.conf_id),
398
+ petGid: petDataDisplay(petGid),
399
+ npcId: petDataDisplay(furnitureGuid),
400
+ npcIdSource: petDataDisplay(raw.npc_id_source),
401
+ imageUrl: petDataImageUrl(baseId, 'image'),
402
+ iconUrl: petDataImageUrl(baseId, 'icon'),
403
+ variantText,
404
+ variantIcon,
405
+ voiceText: petDataVoiceText(pet.voice),
406
+ cards: petDataCardItems(pet, optionMaps, sizeInfo),
407
+ attributes: petDataAttributes(pet),
408
+ skills: petDataSkills(pet, skillLookup, options),
409
+ catchItems: [
410
+ { label: '捕捉等级', value: petDataDisplay(pet.catch_lv) },
411
+ { label: '捕捉方式', value: petDataDisplay(pet.catch_way) },
412
+ { label: '捕捉营地', value: petDataDisplay(pet.caught_camp) },
413
+ { label: '获得时间', value: petDataTimeText(pet.add_time) },
414
+ ],
415
+ specialityIds: (Array.isArray(pet.real_speciality_ids) ? pet.real_speciality_ids : [])
416
+ .map((x) => String(x)).join(' / ') || petDataDisplay(pet.speciality_id),
417
+ relationshipType: petDataDisplay(npcPet.relationship_type),
418
+ errorText: petDataDisplay(raw.error || raw.message || npcPet.error_message, ''),
419
+ });
420
+ });
421
+ return pets;
422
+ }
423
+ function buildPetDataRenderData(res, uid, options = {}) {
424
+ let payload = res || {};
425
+ if (payload.result && typeof payload.result === 'object')
426
+ payload = payload.result;
427
+ const optionMaps = petDataOptionMaps(options.optionsPayload);
428
+ let playerInfo = payload.player_info && typeof payload.player_info === 'object' ? payload.player_info : {};
429
+ if (!Object.keys(playerInfo).length && payload.npc_pet && typeof payload.npc_pet === 'object') {
430
+ playerInfo = payload.npc_pet.player_info && typeof payload.npc_pet.player_info === 'object' ? payload.npc_pet.player_info : {};
431
+ }
432
+ const pets = petDataExtractItems(payload, optionMaps, options.skillLookup || {}, options.sizeLookup || {}, {
433
+ loadSkillIcons: !options.lowBandwidthMode,
434
+ baseUrl: options.baseUrl || '',
435
+ });
436
+ const meta = payload.meta && typeof payload.meta === 'object' ? payload.meta : {};
437
+ const finishedAt = normalizeEpochSeconds(meta.finished_at || meta.created_at);
438
+ const updatedAt = petDataTimeText(finishedAt || Math.floor(Date.now() / 1000));
439
+ const online = playerInfo.online;
440
+ const onlineText = online === true ? '在线' : online === false ? '离线' : '未知';
441
+ const okCount = payload.npc_pet_ok_count;
442
+ const errorCount = payload.npc_pet_error_count;
443
+ const skippedCount = payload.npc_pet_skipped_count;
444
+ return {
445
+ title: '家园详情',
446
+ subtitle: 'Ingame Pet Data',
447
+ uid: petDataDisplay(payload.uin || playerInfo.uin || uid),
448
+ playerName: petDataDisplay(playerInfo.name, '未知玩家'),
449
+ playerLevel: petDataDisplay(playerInfo.level),
450
+ worldLevel: petDataDisplay(playerInfo.world_level),
451
+ onlineText,
452
+ isOnline: online === true,
453
+ queryMode: options.singleQuery ? '单只精灵' : '家园批量',
454
+ lowBandwidthMode: Boolean(options.lowBandwidthMode),
455
+ summaryCards: [
456
+ { label: '目标状态', value: onlineText },
457
+ { label: '返回精灵', value: String(pets.length) },
458
+ { label: '成功/失败', value: `${petDataDisplay(okCount, String(pets.length))} / ${petDataDisplay(errorCount, '0')}` },
459
+ { label: '跳过', value: petDataDisplay(skippedCount, '0') },
460
+ ],
461
+ pets,
462
+ updatedAt,
463
+ notice: '该接口依赖目标玩家在线且家园可访问;离线或隐私/上游不可达时可能失败。',
464
+ emptyText: '未获取到家园精灵完整数据。请确认目标玩家在线、家园可访问,或稍后重试。',
465
+ commandHint: '💡 洛克.家园详情 <UID> | 洛克.家园详情 <UID> <pet_gid> <npc_id>',
466
+ copyright: 'Koishi & WeGame 洛克王国插件',
467
+ };
468
+ }