koishi-plugin-rocom 1.0.12 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/atlas-service.d.ts +33 -0
- package/lib/atlas-service.js +439 -0
- package/lib/client.d.ts +36 -1
- package/lib/client.js +221 -21
- package/lib/commands/account.js +1 -0
- package/lib/commands/merchant.js +351 -91
- package/lib/commands/query.js +162 -14
- package/lib/commands/tools.js +104 -0
- package/lib/commands/wiki.js +167 -6
- package/lib/egg-service.d.ts +2 -0
- package/lib/egg-service.js +50 -15
- package/lib/index.d.ts +9 -0
- package/lib/index.js +3626 -303
- package/lib/pet-data-service.d.ts +62 -0
- package/lib/pet-data-service.js +468 -0
- package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
- package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
- package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
- package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
- package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
- package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
- package/lib/render-templates/home/index.html +81 -97
- package/lib/render-templates/home/style.css +316 -375
- package/lib/render-templates/pet-data/index.html +172 -0
- package/lib/render-templates/pet-data/style.css +553 -0
- package/lib/render-templates/wiki/detail/index.html +84 -0
- package/lib/render-templates/wiki/list/index.html +84 -0
- package/lib/render-templates/wiki/menu/index.html +65 -0
- package/lib/render-templates/wiki/pet/index.html +158 -0
- package/lib/render-templates/wiki/pet/style.css +393 -0
- package/lib/render-templates/wiki/shared/style.css +431 -0
- package/lib/render-templates/wiki/skill/index.html +79 -0
- package/lib/render-templates/wiki/skill/style.css +233 -0
- package/lib/render-templates/wiki/suggestions/index.html +84 -0
- package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
- package/lib/render-templates/yuanxing-shangren/README.md +86 -0
- package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//345/272/225/351/203/250/344/277/241/346/201/257/346/241/206.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/274/226/345/217/267/350/203/214/346/231/257/346/241/206.png +0 -0
- package/lib/render-templates/yuanxing-shangren/index.html +198 -170
- package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
- package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
- package/lib/render-templates/yuanxing-shangren/style.css +201 -256
- package/lib/render-templates/yuanxing-shangren/today.html +461 -0
- package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
- package/lib/render.js +48 -12
- package/lib/subscription-send.js +4 -5
- package/lib/ttf/rocom_skill_origin.ttf +0 -0
- package/lib/ttf//351/200/240/345/255/227/345/267/245/346/210/277/345/220/257/351/273/221/344/275/223.ttf +0 -0
- package/lib/types.d.ts +13 -1
- package/lib/user.d.ts +23 -1
- package/lib/user.js +32 -1
- package/lib/wiki-catalog.d.ts +19 -0
- package/lib/wiki-catalog.js +124 -0
- package/lib/wiki-service.d.ts +220 -0
- package/lib/wiki-service.js +1463 -0
- package/package.json +1 -1
- package/readme.md +4 -5
|
@@ -0,0 +1,1463 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WikiService = void 0;
|
|
4
|
+
exports.normalizeQueryText = normalizeQueryText;
|
|
5
|
+
exports.wikiNamedValue = wikiNamedValue;
|
|
6
|
+
exports.wikiNames = wikiNames;
|
|
7
|
+
exports.wikiRangeLabel = wikiRangeLabel;
|
|
8
|
+
exports.similarityRatio = similarityRatio;
|
|
9
|
+
exports.wikiSimilarityScore = wikiSimilarityScore;
|
|
10
|
+
exports.wikiGenericValue = wikiGenericValue;
|
|
11
|
+
exports.wikiPickImage = wikiPickImage;
|
|
12
|
+
exports.wikiTitleForItem = wikiTitleForItem;
|
|
13
|
+
exports.wikiMetaForItem = wikiMetaForItem;
|
|
14
|
+
exports.wikiCardForItem = wikiCardForItem;
|
|
15
|
+
exports.wikiSectionsForPayload = wikiSectionsForPayload;
|
|
16
|
+
exports.findExactWikiMatch = findExactWikiMatch;
|
|
17
|
+
exports.wikiCandidateText = wikiCandidateText;
|
|
18
|
+
exports.buildWikiPetRenderData = buildWikiPetRenderData;
|
|
19
|
+
exports.buildWikiSkillRenderData = buildWikiSkillRenderData;
|
|
20
|
+
exports.splitWikiCommandParts = splitWikiCommandParts;
|
|
21
|
+
exports.wikiCatalogByToken = wikiCatalogByToken;
|
|
22
|
+
const koishi_1 = require("koishi");
|
|
23
|
+
const wiki_catalog_1 = require("./wiki-catalog");
|
|
24
|
+
const logger = new koishi_1.Logger('rocom-wiki');
|
|
25
|
+
const COPYRIGHT = 'Koishi & WeGame 洛克王国插件';
|
|
26
|
+
// ===== 纯文本工具(可独立测试)=====
|
|
27
|
+
function normalizeQueryText(text) {
|
|
28
|
+
return String(text ?? '').replace(/\s+/g, '').trim().toLowerCase();
|
|
29
|
+
}
|
|
30
|
+
function wikiNamedValue(value) {
|
|
31
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
32
|
+
return String(value.name || value.label || value.value || '');
|
|
33
|
+
}
|
|
34
|
+
return String(value ?? '');
|
|
35
|
+
}
|
|
36
|
+
function wikiNames(values) {
|
|
37
|
+
if (!values)
|
|
38
|
+
return [];
|
|
39
|
+
const list = Array.isArray(values) ? values : [values];
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const value of list) {
|
|
42
|
+
const text = wikiNamedValue(value).trim();
|
|
43
|
+
if (text)
|
|
44
|
+
result.push(text);
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function wikiRangeLabel(data, keyMin, keyMax, unit) {
|
|
49
|
+
if (!data || typeof data !== 'object')
|
|
50
|
+
return '暂无';
|
|
51
|
+
const low = data[keyMin];
|
|
52
|
+
const high = data[keyMax];
|
|
53
|
+
const empty = (v) => v === undefined || v === null || v === '';
|
|
54
|
+
if (empty(low) && empty(high))
|
|
55
|
+
return '暂无';
|
|
56
|
+
if (empty(low))
|
|
57
|
+
return `${high}${unit}`;
|
|
58
|
+
if (empty(high) || low === high)
|
|
59
|
+
return `${low}${unit}`;
|
|
60
|
+
return `${low}-${high}${unit}`;
|
|
61
|
+
}
|
|
62
|
+
// 与上游 SequenceMatcher.ratio 近似:基于编辑距离的相似度。
|
|
63
|
+
function similarityRatio(a, b) {
|
|
64
|
+
if (!a || !b)
|
|
65
|
+
return 0;
|
|
66
|
+
if (a === b)
|
|
67
|
+
return 1;
|
|
68
|
+
const lenA = a.length;
|
|
69
|
+
const lenB = b.length;
|
|
70
|
+
const dp = Array.from({ length: lenB + 1 }, (_, j) => j);
|
|
71
|
+
for (let i = 1; i <= lenA; i++) {
|
|
72
|
+
let prev = dp[0];
|
|
73
|
+
dp[0] = i;
|
|
74
|
+
for (let j = 1; j <= lenB; j++) {
|
|
75
|
+
const temp = dp[j];
|
|
76
|
+
dp[j] = a[i - 1] === b[j - 1] ? prev : Math.min(prev, dp[j - 1], dp[j]) + 1;
|
|
77
|
+
prev = temp;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return 1 - dp[lenB] / Math.max(lenA, lenB);
|
|
81
|
+
}
|
|
82
|
+
function wikiSimilarityScore(query, item) {
|
|
83
|
+
const normalizedQuery = normalizeQueryText(query);
|
|
84
|
+
if (!normalizedQuery)
|
|
85
|
+
return 0;
|
|
86
|
+
const candidates = [
|
|
87
|
+
wikiTitleForItem(item, ''),
|
|
88
|
+
item?.name || '',
|
|
89
|
+
item?.summary || '',
|
|
90
|
+
item?.description || item?.desc || '',
|
|
91
|
+
];
|
|
92
|
+
let best = 0;
|
|
93
|
+
for (const text of candidates) {
|
|
94
|
+
const normalizedText = normalizeQueryText(text);
|
|
95
|
+
if (!normalizedText)
|
|
96
|
+
continue;
|
|
97
|
+
if (normalizedQuery === normalizedText)
|
|
98
|
+
best = Math.max(best, 1);
|
|
99
|
+
else if (normalizedText.includes(normalizedQuery) || normalizedQuery.includes(normalizedText))
|
|
100
|
+
best = Math.max(best, 0.82);
|
|
101
|
+
else
|
|
102
|
+
best = Math.max(best, similarityRatio(normalizedQuery, normalizedText));
|
|
103
|
+
}
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
106
|
+
function wikiGenericValue(value, depth = 0) {
|
|
107
|
+
if (value === undefined || value === null || value === '')
|
|
108
|
+
return '';
|
|
109
|
+
if (typeof value === 'boolean')
|
|
110
|
+
return value ? '是' : '否';
|
|
111
|
+
if (typeof value === 'number')
|
|
112
|
+
return String(value);
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
const texts = value.slice(0, 6).map(item => wikiGenericValue(item, depth + 1)).filter(Boolean);
|
|
115
|
+
const suffix = value.length > 6 ? ' ...' : '';
|
|
116
|
+
return texts.join('、') + suffix;
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === 'object') {
|
|
119
|
+
for (const key of ['name', 'label', 'title', 'summary', 'description', 'text']) {
|
|
120
|
+
const text = String(value[key] || '').trim();
|
|
121
|
+
if (text)
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
if ('min_m' in value || 'max_m' in value)
|
|
125
|
+
return wikiRangeLabel(value, 'min_m', 'max_m', 'm');
|
|
126
|
+
if ('min_kg' in value || 'max_kg' in value)
|
|
127
|
+
return wikiRangeLabel(value, 'min_kg', 'max_kg', 'kg');
|
|
128
|
+
if (depth >= 1) {
|
|
129
|
+
const pairs = [];
|
|
130
|
+
for (const [key, item] of Object.entries(value).slice(0, 4)) {
|
|
131
|
+
const text = wikiGenericValue(item, depth + 1);
|
|
132
|
+
if (text)
|
|
133
|
+
pairs.push(`${(0, wiki_catalog_1.wikiLabelForKey)(key)}:${text}`);
|
|
134
|
+
}
|
|
135
|
+
return pairs.join(';');
|
|
136
|
+
}
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
return String(value);
|
|
140
|
+
}
|
|
141
|
+
function wikiSizeLabel(value) {
|
|
142
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
143
|
+
return wikiGenericValue(value);
|
|
144
|
+
const parts = [];
|
|
145
|
+
if (value.height && typeof value.height === 'object') {
|
|
146
|
+
const text = wikiRangeLabel(value.height, 'min_m', 'max_m', 'm');
|
|
147
|
+
if (text && text !== '暂无')
|
|
148
|
+
parts.push(`高 ${text}`);
|
|
149
|
+
}
|
|
150
|
+
if (value.weight && typeof value.weight === 'object') {
|
|
151
|
+
const text = wikiRangeLabel(value.weight, 'min_kg', 'max_kg', 'kg');
|
|
152
|
+
if (text && text !== '暂无')
|
|
153
|
+
parts.push(`重 ${text}`);
|
|
154
|
+
}
|
|
155
|
+
return parts.join(' / ');
|
|
156
|
+
}
|
|
157
|
+
function wikiFootprintLabel(value) {
|
|
158
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
159
|
+
return wikiGenericValue(value);
|
|
160
|
+
const width = value.width ?? value.x ?? value.cols;
|
|
161
|
+
const height = value.height ?? value.y ?? value.rows;
|
|
162
|
+
if (width !== undefined && width !== null && width !== '' && height !== undefined && height !== null && height !== '') {
|
|
163
|
+
return `${width} x ${height}`;
|
|
164
|
+
}
|
|
165
|
+
return wikiGenericValue(value, 1);
|
|
166
|
+
}
|
|
167
|
+
function wikiCountLabel(value, unit = '个') {
|
|
168
|
+
if (Array.isArray(value))
|
|
169
|
+
return `${value.length}${unit}`;
|
|
170
|
+
if (value === undefined || value === null || value === '')
|
|
171
|
+
return '';
|
|
172
|
+
return typeof value === 'number' ? `${value}${unit}` : String(value);
|
|
173
|
+
}
|
|
174
|
+
function wikiPickImage(item) {
|
|
175
|
+
const imageKeys = [
|
|
176
|
+
'big_icon', 'image', 'cover_image', 'preview_image', 'checkout_icon',
|
|
177
|
+
'icon', 'small_icon', 'background_image', 'display_image', 'package_bg', 'package_cover',
|
|
178
|
+
];
|
|
179
|
+
for (const key of imageKeys) {
|
|
180
|
+
const value = item?.[key];
|
|
181
|
+
if (typeof value === 'string' && /^(https?:\/\/|\{\{)/.test(value))
|
|
182
|
+
return value;
|
|
183
|
+
if (value && typeof value === 'object') {
|
|
184
|
+
const url = value.url || value.image || value.icon;
|
|
185
|
+
if (typeof url === 'string' && /^(https?:\/\/|\{\{)/.test(url))
|
|
186
|
+
return url;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return '';
|
|
190
|
+
}
|
|
191
|
+
function wikiTitleForItem(item, fallback = 'Wiki 条目') {
|
|
192
|
+
for (const key of ['name', 'title', 'display_name', 'summary', 'description']) {
|
|
193
|
+
const value = String(item?.[key] || '').trim();
|
|
194
|
+
if (value)
|
|
195
|
+
return value.slice(0, 48);
|
|
196
|
+
}
|
|
197
|
+
for (const key of [
|
|
198
|
+
'pet_id', 'skill_id', 'item_id', 'egg_conf_id', 'random_egg_id', 'tree_id', 'medal_id',
|
|
199
|
+
'ball_id', 'plant_id', 'furniture_id', 'suit_id', 'region_id', 'dungeon_id', 'mail_id',
|
|
200
|
+
'music_id', 'task_id', 'shop_id', 'exchange_id', 'id',
|
|
201
|
+
]) {
|
|
202
|
+
const value = item?.[key];
|
|
203
|
+
if (value !== undefined && value !== null && value !== '')
|
|
204
|
+
return `${fallback} #${value}`;
|
|
205
|
+
}
|
|
206
|
+
return fallback;
|
|
207
|
+
}
|
|
208
|
+
function wikiSummaryForItem(item) {
|
|
209
|
+
for (const key of ['summary', 'description', 'desc', 'effect', 'flavor_text', 'text', 'content']) {
|
|
210
|
+
const value = wikiGenericValue(item?.[key]);
|
|
211
|
+
if (value)
|
|
212
|
+
return value.slice(0, 180);
|
|
213
|
+
}
|
|
214
|
+
return '';
|
|
215
|
+
}
|
|
216
|
+
function wikiBadgesForItem(item) {
|
|
217
|
+
const badges = [];
|
|
218
|
+
for (const key of ['type', 'skill_type', 'damage_type', 'element_type', 'category', 'quality', 'rarity', 'label_type', 'egg_type', 'grade', 'gender']) {
|
|
219
|
+
const value = wikiGenericValue(item?.[key]);
|
|
220
|
+
if (value)
|
|
221
|
+
badges.push(value);
|
|
222
|
+
}
|
|
223
|
+
for (const badge of item?.badges || []) {
|
|
224
|
+
const value = wikiGenericValue(badge);
|
|
225
|
+
if (value)
|
|
226
|
+
badges.push(value);
|
|
227
|
+
}
|
|
228
|
+
for (const key of ['type_names', 'egg_group_names', 'tags', 'source_counts']) {
|
|
229
|
+
badges.push(...wikiNames(item?.[key]).slice(0, 4));
|
|
230
|
+
}
|
|
231
|
+
const output = [];
|
|
232
|
+
const seen = new Set();
|
|
233
|
+
for (const badge of badges) {
|
|
234
|
+
const key = String(badge);
|
|
235
|
+
if (key && !seen.has(key)) {
|
|
236
|
+
output.push(key);
|
|
237
|
+
seen.add(key);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return output.slice(0, 8);
|
|
241
|
+
}
|
|
242
|
+
function wikiAddFact(rows, label, value) {
|
|
243
|
+
const text = wikiGenericValue(value);
|
|
244
|
+
if (text)
|
|
245
|
+
rows.push({ label, value: text.slice(0, 96) });
|
|
246
|
+
}
|
|
247
|
+
function wikiMetaForItem(item, limit = 12, catalogKey = '') {
|
|
248
|
+
const rows = [];
|
|
249
|
+
if (catalogKey === 'pets') {
|
|
250
|
+
wikiAddFact(rows, '图鉴编号', item?.handbook_no);
|
|
251
|
+
wikiAddFact(rows, '精灵ID', item?.pet_id);
|
|
252
|
+
wikiAddFact(rows, '属性', item?.type_names || item?.types);
|
|
253
|
+
wikiAddFact(rows, '蛋组', item?.egg_group_names || item?.egg_groups);
|
|
254
|
+
}
|
|
255
|
+
else if (catalogKey === 'skills') {
|
|
256
|
+
wikiAddFact(rows, '技能ID', item?.skill_id);
|
|
257
|
+
wikiAddFact(rows, '属性', item?.element_type || item?.type);
|
|
258
|
+
wikiAddFact(rows, '类型', item?.skill_type || item?.damage_type);
|
|
259
|
+
wikiAddFact(rows, '威力 / 能量', `${item?.power ?? '-'} / ${item?.cost ?? '-'}`);
|
|
260
|
+
wikiAddFact(rows, '可用精灵', item?.pet_count);
|
|
261
|
+
}
|
|
262
|
+
else if (['pet-eggs', 'egg-items', 'random-eggs', 'pet-egg'].includes(catalogKey)) {
|
|
263
|
+
wikiAddFact(rows, '蛋ID', item?.egg_conf_id || item?.random_egg_id || item?.item_id);
|
|
264
|
+
wikiAddFact(rows, '蛋类型', item?.egg_type);
|
|
265
|
+
wikiAddFact(rows, '孵化时间', item?.hatch_label);
|
|
266
|
+
wikiAddFact(rows, '尺寸', wikiSizeLabel(item?.egg_size));
|
|
267
|
+
wikiAddFact(rows, '关联精灵', item?.pet);
|
|
268
|
+
}
|
|
269
|
+
else if (['pet-foods', 'pet-gifts'].includes(catalogKey)) {
|
|
270
|
+
wikiAddFact(rows, '物品ID', item?.item_id);
|
|
271
|
+
wikiAddFact(rows, '亲密经验', item?.food_close_exp || item?.close_exp);
|
|
272
|
+
wikiAddFact(rows, '家园经验', item?.home_exp_num);
|
|
273
|
+
wikiAddFact(rows, '制作时间', item?.need_time_label);
|
|
274
|
+
wikiAddFact(rows, '交互次数', item?.interaction_count);
|
|
275
|
+
}
|
|
276
|
+
else if (catalogKey === 'home-egg-lay-rates') {
|
|
277
|
+
wikiAddFact(rows, '巢穴数', item?.nest_num);
|
|
278
|
+
wikiAddFact(rows, '产蛋巢', item?.egg_laying_nest_num);
|
|
279
|
+
const rate = item?.pet_lay_egg_rate_percent;
|
|
280
|
+
wikiAddFact(rows, '产蛋概率', rate !== undefined && rate !== null && rate !== '' ? `${rate}%` : '');
|
|
281
|
+
}
|
|
282
|
+
else if (catalogKey === 'pet-levels') {
|
|
283
|
+
wikiAddFact(rows, '等级', item?.level);
|
|
284
|
+
wikiAddFact(rows, '累计经验', item?.total_exp);
|
|
285
|
+
wikiAddFact(rows, '下级所需', item?.next_level_exp);
|
|
286
|
+
}
|
|
287
|
+
else if (catalogKey === 'furniture') {
|
|
288
|
+
wikiAddFact(rows, '家具ID', item?.furniture_id);
|
|
289
|
+
wikiAddFact(rows, '分类', item?.category);
|
|
290
|
+
wikiAddFact(rows, '舒适度', item?.comfort);
|
|
291
|
+
wikiAddFact(rows, '占地', wikiFootprintLabel(item?.footprint));
|
|
292
|
+
}
|
|
293
|
+
else if (catalogKey === 'fashion') {
|
|
294
|
+
wikiAddFact(rows, '套装ID', item?.suit_id);
|
|
295
|
+
wikiAddFact(rows, '性别', item?.gender);
|
|
296
|
+
wikiAddFact(rows, '品级', item?.grade);
|
|
297
|
+
wikiAddFact(rows, '部件', wikiCountLabel(item?.parts));
|
|
298
|
+
}
|
|
299
|
+
else if (catalogKey === 'exchanges') {
|
|
300
|
+
wikiAddFact(rows, '兑换ID', item?.exchange_id);
|
|
301
|
+
wikiAddFact(rows, '获得', item?.get_items);
|
|
302
|
+
wikiAddFact(rows, '消耗', item?.cost_items);
|
|
303
|
+
}
|
|
304
|
+
else if (catalogKey === 'shops') {
|
|
305
|
+
wikiAddFact(rows, '商店ID', item?.shop_id);
|
|
306
|
+
wikiAddFact(rows, '页签', item?.tab_name);
|
|
307
|
+
wikiAddFact(rows, '商品数', wikiCountLabel(item?.goods));
|
|
308
|
+
}
|
|
309
|
+
else if (['skill-stones', 'skill-stone-recipes', 'items', 'pet-fruits', 'recipes', 'balls', 'plants', 'pet-carryons', 'medals'].includes(catalogKey)) {
|
|
310
|
+
wikiAddFact(rows, '物品ID', item?.item_id || item?.medal_id || item?.ball_id || item?.plant_id || item?.carryon_id);
|
|
311
|
+
wikiAddFact(rows, '类型', item?.type || item?.label_type);
|
|
312
|
+
wikiAddFact(rows, '品质', item?.quality);
|
|
313
|
+
wikiAddFact(rows, '效果', item?.summary || item?.catch_effect || item?.carryon);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
for (const key of [
|
|
317
|
+
'pet_id', 'skill_id', 'item_id', 'tree_id', 'level', 'close_level', 'action_label',
|
|
318
|
+
'region_id', 'dungeon_id', 'mail_id', 'music_id', 'emoji_id', 'task_id', 'summary_id',
|
|
319
|
+
'asset_type', 'asset_id', 'action_type', 'action_id', 'quality', 'type', 'category',
|
|
320
|
+
]) {
|
|
321
|
+
if (item && key in item)
|
|
322
|
+
wikiAddFact(rows, (0, wiki_catalog_1.wikiLabelForKey)(key), item[key]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (rows.length)
|
|
326
|
+
return rows.slice(0, limit);
|
|
327
|
+
const skip = new Set([
|
|
328
|
+
'name', 'title', 'description', 'desc', 'summary', 'icon', 'small_icon', 'big_icon',
|
|
329
|
+
'image', 'cover_image', 'preview_image', 'background_image', 'items', 'rewards',
|
|
330
|
+
'reward_items', 'tags', 'badges', 'catalog',
|
|
331
|
+
]);
|
|
332
|
+
for (const [key, value] of Object.entries(item || {})) {
|
|
333
|
+
if (skip.has(key))
|
|
334
|
+
continue;
|
|
335
|
+
const text = wikiGenericValue(value);
|
|
336
|
+
if (!text)
|
|
337
|
+
continue;
|
|
338
|
+
rows.push({ label: (0, wiki_catalog_1.wikiLabelForKey)(key), value: text.slice(0, 80) });
|
|
339
|
+
if (rows.length >= limit)
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
return rows;
|
|
343
|
+
}
|
|
344
|
+
function wikiCardForItem(item, fallback, catalog) {
|
|
345
|
+
const catalogKey = String(catalog?.key || '');
|
|
346
|
+
return {
|
|
347
|
+
title: wikiTitleForItem(item, fallback),
|
|
348
|
+
image: wikiPickImage(item),
|
|
349
|
+
summary: wikiSummaryForItem(item),
|
|
350
|
+
badges: wikiBadgesForItem(item),
|
|
351
|
+
meta: wikiMetaForItem(item, 5, catalogKey),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function wikiRowsFromDict(value, limit = 16) {
|
|
355
|
+
const rows = [];
|
|
356
|
+
for (const [key, item] of Object.entries(value)) {
|
|
357
|
+
if (['icon', 'small_icon', 'big_icon', 'image', 'display_image'].includes(key))
|
|
358
|
+
continue;
|
|
359
|
+
const text = wikiGenericValue(item);
|
|
360
|
+
if (text)
|
|
361
|
+
rows.push({ label: (0, wiki_catalog_1.wikiLabelForKey)(key), value: text.slice(0, 120) });
|
|
362
|
+
if (rows.length >= limit)
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
return rows;
|
|
366
|
+
}
|
|
367
|
+
function wikiSectionsForPayload(payload, catalog) {
|
|
368
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
369
|
+
return [];
|
|
370
|
+
const sections = [];
|
|
371
|
+
const addList = (title, value) => {
|
|
372
|
+
if (!Array.isArray(value) || !value.length)
|
|
373
|
+
return;
|
|
374
|
+
const dictItems = value.filter(item => item && typeof item === 'object' && !Array.isArray(item));
|
|
375
|
+
if (dictItems.length) {
|
|
376
|
+
sections.push({
|
|
377
|
+
title,
|
|
378
|
+
cards: dictItems.slice(0, 12).map(item => wikiCardForItem(item, title, catalog)),
|
|
379
|
+
rows: [],
|
|
380
|
+
total: dictItems.length,
|
|
381
|
+
});
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const texts = value.slice(0, 16).map(item => wikiGenericValue(item)).filter(Boolean);
|
|
385
|
+
if (!texts.length)
|
|
386
|
+
return;
|
|
387
|
+
sections.push({
|
|
388
|
+
title,
|
|
389
|
+
cards: [],
|
|
390
|
+
rows: texts.map((text, idx) => ({ label: String(idx + 1), value: text })),
|
|
391
|
+
total: texts.length,
|
|
392
|
+
});
|
|
393
|
+
};
|
|
394
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
395
|
+
const title = (0, wiki_catalog_1.wikiSectionTitle)(key);
|
|
396
|
+
if (value && typeof value === 'object' && !Array.isArray(value)
|
|
397
|
+
&& ['probabilities', 'npc_reactions', 'catch_effect', 'carryon', 'planting', 'limit', 'bond', 'mall_package'].includes(key)) {
|
|
398
|
+
const rows = wikiRowsFromDict(value);
|
|
399
|
+
if (rows.length)
|
|
400
|
+
sections.push({ title, cards: [], rows, total: rows.length });
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
addList(title, value);
|
|
404
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
405
|
+
for (const [subkey, subvalue] of Object.entries(value)) {
|
|
406
|
+
addList(`${title} / ${(0, wiki_catalog_1.wikiSectionTitle)(subkey)}`, subvalue);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return sections.slice(0, 8);
|
|
411
|
+
}
|
|
412
|
+
function findExactWikiMatch(results, query) {
|
|
413
|
+
const normalizedQuery = normalizeQueryText(query);
|
|
414
|
+
if (!normalizedQuery)
|
|
415
|
+
return null;
|
|
416
|
+
for (const item of results) {
|
|
417
|
+
const name = String(item?.name || '');
|
|
418
|
+
const form = String(item?.form || '');
|
|
419
|
+
const candidates = [
|
|
420
|
+
normalizeQueryText(name),
|
|
421
|
+
normalizeQueryText(`${name}${form}`),
|
|
422
|
+
normalizeQueryText(`${name} ${form}`),
|
|
423
|
+
normalizeQueryText(`${form}${name}`),
|
|
424
|
+
];
|
|
425
|
+
if (candidates.includes(normalizedQuery))
|
|
426
|
+
return item;
|
|
427
|
+
}
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
function wikiCandidateText(query, items, kind) {
|
|
431
|
+
const lines = [`找到多个${kind}候选,请使用更精确名称:`];
|
|
432
|
+
items.slice(0, 10).forEach((item, idx) => {
|
|
433
|
+
const name = item?.name || '未知';
|
|
434
|
+
const form = item?.form || '';
|
|
435
|
+
const itemId = item?.pet_id || item?.skill_id || item?.id || '-';
|
|
436
|
+
const suffix = form && form !== '普通' ? `(${form})` : '';
|
|
437
|
+
lines.push(`${idx + 1}. ${name}${suffix} #${itemId}`);
|
|
438
|
+
});
|
|
439
|
+
lines.push(`\n你查询的是:${query}`);
|
|
440
|
+
return lines.join('\n');
|
|
441
|
+
}
|
|
442
|
+
function wikiBodySize(...sources) {
|
|
443
|
+
let bodySize = {};
|
|
444
|
+
for (const source of sources) {
|
|
445
|
+
if (source && typeof source === 'object' && source.body_size && typeof source.body_size === 'object') {
|
|
446
|
+
bodySize = source.body_size;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
height: wikiRangeLabel(bodySize?.height, 'min_m', 'max_m', 'm'),
|
|
452
|
+
weight: wikiRangeLabel(bodySize?.weight, 'min_kg', 'max_kg', 'kg'),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function wikiGenderLabel(value) {
|
|
456
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
457
|
+
const parts = [];
|
|
458
|
+
if (value.male_percent !== undefined && value.male_percent !== null && value.male_percent !== '')
|
|
459
|
+
parts.push(`雄 ${value.male_percent}%`);
|
|
460
|
+
if (value.female_percent !== undefined && value.female_percent !== null && value.female_percent !== '')
|
|
461
|
+
parts.push(`雌 ${value.female_percent}%`);
|
|
462
|
+
return parts.length ? parts.join(' / ') : '暂无';
|
|
463
|
+
}
|
|
464
|
+
return String(value || '暂无');
|
|
465
|
+
}
|
|
466
|
+
function wikiEcologyLabel(value, fallback = '') {
|
|
467
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
468
|
+
const parts = [];
|
|
469
|
+
for (const key of ['pet_text', 'pet_style']) {
|
|
470
|
+
const text = String(value[key] || '').trim();
|
|
471
|
+
if (text)
|
|
472
|
+
parts.push(text);
|
|
473
|
+
}
|
|
474
|
+
const habitats = wikiNames(value.habitats);
|
|
475
|
+
if (habitats.length)
|
|
476
|
+
parts.push('栖息:' + habitats.join(' / '));
|
|
477
|
+
return parts.join(';');
|
|
478
|
+
}
|
|
479
|
+
return String(value || fallback || '').trim();
|
|
480
|
+
}
|
|
481
|
+
function wikiSourceLabel(source, fallback = '') {
|
|
482
|
+
const mapping = { level: '升级', machine: '技能石', blood: '血脉', field: '特殊来源' };
|
|
483
|
+
const text = String(source || '').trim();
|
|
484
|
+
return String(fallback || mapping[text] || text || '');
|
|
485
|
+
}
|
|
486
|
+
function wikiTypeEffectiveness(profile) {
|
|
487
|
+
const data = profile?.type_effectiveness;
|
|
488
|
+
if (!data || typeof data !== 'object')
|
|
489
|
+
return [];
|
|
490
|
+
const rows = [];
|
|
491
|
+
const definitions = [
|
|
492
|
+
['攻击克制', ['attack', 'strong']],
|
|
493
|
+
['攻击弱效', ['attack', 'weak']],
|
|
494
|
+
['防御弱点', ['defense', 'weak']],
|
|
495
|
+
['防御抵抗', ['defense', 'resist']],
|
|
496
|
+
];
|
|
497
|
+
for (const [label, [groupKey, itemKey]] of definitions) {
|
|
498
|
+
const group = data[groupKey] && typeof data[groupKey] === 'object' ? data[groupKey] : {};
|
|
499
|
+
const names = wikiNames(group?.[itemKey]);
|
|
500
|
+
if (names.length)
|
|
501
|
+
rows.push({ label, value: names.join(' / ') });
|
|
502
|
+
}
|
|
503
|
+
return rows;
|
|
504
|
+
}
|
|
505
|
+
function buildPetStats(profile) {
|
|
506
|
+
const attrs = profile?.attributes && typeof profile.attributes === 'object' ? profile.attributes : {};
|
|
507
|
+
const defs = [
|
|
508
|
+
['精力', 'hp', '#42b883'],
|
|
509
|
+
['物攻', 'physical_attack', '#e86452'],
|
|
510
|
+
['魔攻', 'magic_attack', '#5987f5'],
|
|
511
|
+
['物防', 'physical_defense', '#d69a32'],
|
|
512
|
+
['魔防', 'magic_defense', '#25a6a6'],
|
|
513
|
+
['速度', 'speed', '#8d62d9'],
|
|
514
|
+
];
|
|
515
|
+
return defs.map(([label, key, color]) => {
|
|
516
|
+
const value = Number(attrs[key]) || 0;
|
|
517
|
+
return { label, value, color, percent: Math.min(value / 160 * 100, 100) };
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
function buildPetSkillGroups(skillsData) {
|
|
521
|
+
const groups = [];
|
|
522
|
+
const sourceDefs = [
|
|
523
|
+
['level', '升级习得'],
|
|
524
|
+
['machine', '技能机'],
|
|
525
|
+
['blood', '血脉技能'],
|
|
526
|
+
['field', '场景习得'],
|
|
527
|
+
];
|
|
528
|
+
for (const [key, label] of sourceDefs) {
|
|
529
|
+
const rawItems = Array.isArray(skillsData?.[key]) ? skillsData[key] : [];
|
|
530
|
+
const items = rawItems.slice(0, 10).map((skill) => ({
|
|
531
|
+
name: skill?.name || '未知技能',
|
|
532
|
+
icon: skill?.icon || '',
|
|
533
|
+
level: skill?.level || skill?.source_label || '',
|
|
534
|
+
type: wikiNamedValue(skill?.element_type || skill?.type) || '未知',
|
|
535
|
+
category: wikiNamedValue(skill?.skill_type) || wikiNamedValue(skill?.damage_type) || '未知',
|
|
536
|
+
cost: skill?.cost ?? '?',
|
|
537
|
+
power: skill?.power ?? '?',
|
|
538
|
+
desc: skill?.desc || skill?.description || skill?.flavor_text || '暂无说明',
|
|
539
|
+
}));
|
|
540
|
+
if (items.length)
|
|
541
|
+
groups.push({ label, items });
|
|
542
|
+
}
|
|
543
|
+
return groups;
|
|
544
|
+
}
|
|
545
|
+
function buildPetFamily(familyData, currentId) {
|
|
546
|
+
let items = [];
|
|
547
|
+
if (familyData && typeof familyData === 'object' && !Array.isArray(familyData)) {
|
|
548
|
+
if (Array.isArray(familyData.members))
|
|
549
|
+
items.push(...familyData.members);
|
|
550
|
+
for (const formGroup of familyData.forms || []) {
|
|
551
|
+
for (const member of formGroup?.members || []) {
|
|
552
|
+
const merged = { ...member };
|
|
553
|
+
if (formGroup?.name && !merged.form_group)
|
|
554
|
+
merged.form_group = formGroup.name;
|
|
555
|
+
items.push(merged);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (!items.length) {
|
|
559
|
+
for (const key of ['items', 'family', 'evolutions']) {
|
|
560
|
+
if (Array.isArray(familyData[key])) {
|
|
561
|
+
items.push(...familyData[key]);
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
else if (Array.isArray(familyData)) {
|
|
568
|
+
items = familyData;
|
|
569
|
+
}
|
|
570
|
+
const result = [];
|
|
571
|
+
const seen = new Set();
|
|
572
|
+
for (const item of items.slice(0, 12)) {
|
|
573
|
+
const petId = item?.pet_id ?? item?.id;
|
|
574
|
+
const key = String(petId ?? item?.name ?? '');
|
|
575
|
+
if (key && seen.has(key))
|
|
576
|
+
continue;
|
|
577
|
+
seen.add(key);
|
|
578
|
+
const conditionTexts = Array.isArray(item?.condition_texts) ? item.condition_texts : [];
|
|
579
|
+
result.push({
|
|
580
|
+
pet_id: petId ?? '-',
|
|
581
|
+
name: item?.name || '未知精灵',
|
|
582
|
+
form: item?.form || item?.form_group || '',
|
|
583
|
+
icon: item?.icon || item?.small_icon || '',
|
|
584
|
+
condition: item?.condition_summary
|
|
585
|
+
|| conditionTexts.filter(Boolean).join(' / ')
|
|
586
|
+
|| item?.evolution_description
|
|
587
|
+
|| item?.condition
|
|
588
|
+
|| item?.evolve_condition
|
|
589
|
+
|| item?.relation
|
|
590
|
+
|| '',
|
|
591
|
+
is_current: String(petId ?? '') === String(currentId ?? ''),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
596
|
+
function buildHandbookTopics(handbookData) {
|
|
597
|
+
const topics = Array.isArray(handbookData?.topics) ? handbookData.topics : [];
|
|
598
|
+
const result = [];
|
|
599
|
+
for (const item of topics.slice(0, 8)) {
|
|
600
|
+
const rewards = [];
|
|
601
|
+
let rewardItems = item?.rewards || item?.reward_items;
|
|
602
|
+
if (!rewardItems && item?.reward && typeof item.reward === 'object')
|
|
603
|
+
rewardItems = item.reward.items;
|
|
604
|
+
for (const reward of rewardItems || []) {
|
|
605
|
+
rewards.push({
|
|
606
|
+
name: reward?.name || '奖励',
|
|
607
|
+
count: reward?.count || '',
|
|
608
|
+
icon: reward?.icon || '',
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
result.push({
|
|
612
|
+
name: item?.name || item?.title || `课题 ${item?.topic_id || ''}`.trim(),
|
|
613
|
+
desc: item?.description || item?.desc || '',
|
|
614
|
+
rewards: rewards.slice(0, 4),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
return result;
|
|
618
|
+
}
|
|
619
|
+
function buildWikiPetRenderData(overview, profile, skills, family, handbook, query) {
|
|
620
|
+
profile = profile || {};
|
|
621
|
+
skills = skills || {};
|
|
622
|
+
family = family || {};
|
|
623
|
+
handbook = handbook || {};
|
|
624
|
+
const petId = overview?.pet_id ?? profile?.pet_id;
|
|
625
|
+
const typeNames = wikiNames(overview?.type_names || overview?.types);
|
|
626
|
+
const eggGroups = wikiNames(overview?.egg_group_names || overview?.egg_groups);
|
|
627
|
+
const feature = overview?.feature || {};
|
|
628
|
+
const bodySize = wikiBodySize(profile, overview);
|
|
629
|
+
const stats = buildPetStats(profile);
|
|
630
|
+
const totalStats = profile?.attributes?.sum || stats.reduce((sum, item) => sum + item.value, 0);
|
|
631
|
+
const form = overview?.form || profile?.form || '';
|
|
632
|
+
const classis = wikiNamedValue(overview?.classis) || '暂无';
|
|
633
|
+
const habitats = wikiNames(profile?.habitats);
|
|
634
|
+
const areas = wikiNames(handbook?.areas);
|
|
635
|
+
return {
|
|
636
|
+
name: overview?.name || profile?.name || query,
|
|
637
|
+
query,
|
|
638
|
+
pet_id: petId ?? '-',
|
|
639
|
+
number: overview?.handbook_no || profile?.handbook_no || '---',
|
|
640
|
+
form,
|
|
641
|
+
formLine: `${form || '普通'}${classis && classis !== '暂无' ? ` · ${classis}` : ''}`,
|
|
642
|
+
quality: overview?.quality || profile?.quality || '',
|
|
643
|
+
stage: overview?.stage || profile?.stage || '',
|
|
644
|
+
pet_icon: overview?.icon || overview?.small_icon || '',
|
|
645
|
+
main_image: profile?.small_icon || overview?.small_icon || profile?.icon || overview?.icon || '',
|
|
646
|
+
type_names: typeNames,
|
|
647
|
+
egg_groups: eggGroups,
|
|
648
|
+
description: profile?.description || overview?.description || '暂无图鉴描述',
|
|
649
|
+
classis,
|
|
650
|
+
feature_name: feature?.name || '暂无',
|
|
651
|
+
feature_desc: feature?.desc || '暂无特性说明',
|
|
652
|
+
ride_talent: overview?.has_ride_talent ? '支持' : '不支持',
|
|
653
|
+
height_label: bodySize.height,
|
|
654
|
+
weight_label: bodySize.weight,
|
|
655
|
+
gender_ratio: wikiGenderLabel(profile?.gender_ratio),
|
|
656
|
+
move_type: profile?.move_type || '暂无',
|
|
657
|
+
habitats,
|
|
658
|
+
habitatsLabel: habitats.length ? habitats.join(' / ') : '暂无',
|
|
659
|
+
ecology: wikiEcologyLabel(profile?.ecology, profile?.pet_style),
|
|
660
|
+
type_effectiveness: wikiTypeEffectiveness(profile),
|
|
661
|
+
total_stats: totalStats,
|
|
662
|
+
pet_stats: stats,
|
|
663
|
+
skill_groups: buildPetSkillGroups(skills),
|
|
664
|
+
family_members: buildPetFamily(family, petId),
|
|
665
|
+
handbook_topics: buildHandbookTopics(handbook),
|
|
666
|
+
areas,
|
|
667
|
+
areasLabel: areas.join(' / '),
|
|
668
|
+
commandHint: '💡 洛克wiki <类型> <关键词或ID> | 示例:洛克wiki 技能 圣光斩',
|
|
669
|
+
copyright: COPYRIGHT,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
function buildWikiSkillRenderData(item, petsData, query) {
|
|
673
|
+
const pets = ((petsData || {}).items || []).slice(0, 30).map((pet) => {
|
|
674
|
+
const source = wikiSourceLabel(pet?.source, pet?.source_label);
|
|
675
|
+
const level = pet?.level || '';
|
|
676
|
+
return {
|
|
677
|
+
name: pet?.name || '未知精灵',
|
|
678
|
+
pet_id: pet?.pet_id ?? '-',
|
|
679
|
+
icon: pet?.icon || pet?.small_icon || '',
|
|
680
|
+
source,
|
|
681
|
+
level,
|
|
682
|
+
sourceLine: source || level ? `${source}${level ? ` · Lv.${level}` : ''}` : '',
|
|
683
|
+
};
|
|
684
|
+
});
|
|
685
|
+
const tags = (item?.tags || []).map((tag) => ({ name: tag?.name || '', icon: tag?.icon || '' }));
|
|
686
|
+
return {
|
|
687
|
+
name: item?.name || query,
|
|
688
|
+
skill_id: item?.skill_id ?? '-',
|
|
689
|
+
query,
|
|
690
|
+
icon: item?.icon || '',
|
|
691
|
+
type: wikiNamedValue(item?.type) || '未知',
|
|
692
|
+
skill_type: wikiNamedValue(item?.skill_type) || '未知',
|
|
693
|
+
damage_type: wikiNamedValue(item?.damage_type) || '未知',
|
|
694
|
+
element_type: wikiNamedValue(item?.element_type) || '未知',
|
|
695
|
+
cost: item?.cost ?? '?',
|
|
696
|
+
power: item?.power ?? '?',
|
|
697
|
+
families: item?.families || '暂无',
|
|
698
|
+
description: item?.description || '暂无说明',
|
|
699
|
+
flavor_text: item?.flavor_text || '',
|
|
700
|
+
tags,
|
|
701
|
+
pets,
|
|
702
|
+
pet_total: (petsData || {}).total || pets.length,
|
|
703
|
+
commandHint: '💡 洛克wiki 技能 <技能名或ID>',
|
|
704
|
+
copyright: COPYRIGHT,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
function splitWikiCommandParts(text) {
|
|
708
|
+
const parts = String(text || '').trim().split(/\s+/).filter(Boolean);
|
|
709
|
+
let pageNo = 1;
|
|
710
|
+
if (parts.length) {
|
|
711
|
+
const tail = parts[parts.length - 1];
|
|
712
|
+
const pageMatch = /^(?:(?:p|P|页|第)(\d+)(?:页)?|(\d+)页)$/.exec(tail);
|
|
713
|
+
if (pageMatch && parts.length > 1) {
|
|
714
|
+
const pageValue = pageMatch[1] || pageMatch[2];
|
|
715
|
+
pageNo = Math.max(parseInt(pageValue, 10), 1);
|
|
716
|
+
parts.pop();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return { parts, pageNo };
|
|
720
|
+
}
|
|
721
|
+
function wikiCatalogByToken(token) {
|
|
722
|
+
const text = String(token || '').trim().toLowerCase();
|
|
723
|
+
return wiki_catalog_1.WIKI_CATALOG_ROUTES_BY_ALIAS.get(text) || null;
|
|
724
|
+
}
|
|
725
|
+
// ===== 需要访问后端的服务 =====
|
|
726
|
+
class WikiService {
|
|
727
|
+
ctx;
|
|
728
|
+
client;
|
|
729
|
+
catalogsCache = null;
|
|
730
|
+
catalogsCacheTs = 0;
|
|
731
|
+
optionsCache = null;
|
|
732
|
+
optionsCacheTs = 0;
|
|
733
|
+
skillDetailCache = new Map();
|
|
734
|
+
petDetailCache = new Map();
|
|
735
|
+
constructor(ctx, client) {
|
|
736
|
+
this.ctx = ctx;
|
|
737
|
+
this.client = client;
|
|
738
|
+
}
|
|
739
|
+
async getCatalogsPayload(force = false) {
|
|
740
|
+
const now = Date.now();
|
|
741
|
+
if (!force && this.catalogsCache !== null && now - this.catalogsCacheTs < 300000) {
|
|
742
|
+
return this.catalogsCache;
|
|
743
|
+
}
|
|
744
|
+
const payload = await this.client.getWikiCatalogs(this.ctx);
|
|
745
|
+
if (payload && typeof payload === 'object') {
|
|
746
|
+
this.catalogsCache = payload;
|
|
747
|
+
this.catalogsCacheTs = now;
|
|
748
|
+
return payload;
|
|
749
|
+
}
|
|
750
|
+
return this.catalogsCache || {};
|
|
751
|
+
}
|
|
752
|
+
async getOptionsPayload(force = false) {
|
|
753
|
+
const now = Date.now();
|
|
754
|
+
if (!force && this.optionsCache !== null && now - this.optionsCacheTs < 300000) {
|
|
755
|
+
return this.optionsCache;
|
|
756
|
+
}
|
|
757
|
+
const payload = await this.client.getWikiOptions(this.ctx);
|
|
758
|
+
if (payload && typeof payload === 'object') {
|
|
759
|
+
this.optionsCache = payload;
|
|
760
|
+
this.optionsCacheTs = now;
|
|
761
|
+
return payload;
|
|
762
|
+
}
|
|
763
|
+
return this.optionsCache || {};
|
|
764
|
+
}
|
|
765
|
+
backendCatalogItems(payload) {
|
|
766
|
+
if (!payload || typeof payload !== 'object')
|
|
767
|
+
return [];
|
|
768
|
+
const items = payload.items;
|
|
769
|
+
if (!Array.isArray(items))
|
|
770
|
+
return [];
|
|
771
|
+
return items.filter(item => item && typeof item === 'object');
|
|
772
|
+
}
|
|
773
|
+
catalogFromBackendItem(item) {
|
|
774
|
+
const key = String(item?.key || '').trim();
|
|
775
|
+
const base = wiki_catalog_1.WIKI_CATALOG_ROUTES_BY_KEY.get(key);
|
|
776
|
+
const catalog = base
|
|
777
|
+
? { ...base, aliases: [...base.aliases] }
|
|
778
|
+
: { key, title: String(item?.name || key || 'Wiki'), aliases: [], list_path: '', detail_path: '', id_fields: [], search: null };
|
|
779
|
+
const backendName = String(item?.name || '').trim();
|
|
780
|
+
if (backendName && !base)
|
|
781
|
+
catalog.title = backendName;
|
|
782
|
+
for (const alias of [key, backendName, backendName.replace(/图鉴/g, '').trim()]) {
|
|
783
|
+
if (alias && !catalog.aliases.includes(alias))
|
|
784
|
+
catalog.aliases.push(alias);
|
|
785
|
+
}
|
|
786
|
+
const filters = Array.isArray(item?.filters) ? item.filters : [];
|
|
787
|
+
const hasQFilter = filters.some((f) => f && typeof f === 'object' && f.key === 'q');
|
|
788
|
+
const routeSearch = catalog.search;
|
|
789
|
+
catalog.search = routeSearch === null || routeSearch === undefined ? hasQFilter : Boolean(routeSearch);
|
|
790
|
+
if (item?.path)
|
|
791
|
+
catalog.list_path = String(item.path);
|
|
792
|
+
catalog._backend = item;
|
|
793
|
+
return catalog;
|
|
794
|
+
}
|
|
795
|
+
catalogForKeyFromPayload(key, catalogsPayload) {
|
|
796
|
+
const item = this.backendCatalogItems(catalogsPayload).find(it => String(it?.key || '') === String(key || ''));
|
|
797
|
+
if (item)
|
|
798
|
+
return this.catalogFromBackendItem(item);
|
|
799
|
+
const route = wiki_catalog_1.WIKI_CATALOG_ROUTES_BY_KEY.get(String(key || ''));
|
|
800
|
+
return route ? { ...route, aliases: [...route.aliases] } : null;
|
|
801
|
+
}
|
|
802
|
+
catalogsFromPayload(catalogsPayload) {
|
|
803
|
+
return this.backendCatalogItems(catalogsPayload).map(item => this.catalogFromBackendItem(item));
|
|
804
|
+
}
|
|
805
|
+
async getCatalogByKey(key) {
|
|
806
|
+
return this.catalogForKeyFromPayload(key, await this.getCatalogsPayload());
|
|
807
|
+
}
|
|
808
|
+
dynamicCatalogByToken(token, catalogsPayload) {
|
|
809
|
+
const text = normalizeQueryText(token);
|
|
810
|
+
if (!text)
|
|
811
|
+
return null;
|
|
812
|
+
for (const item of this.backendCatalogItems(catalogsPayload)) {
|
|
813
|
+
const candidates = [
|
|
814
|
+
item?.key,
|
|
815
|
+
item?.name,
|
|
816
|
+
String(item?.name || '').replace(/图鉴/g, '').trim(),
|
|
817
|
+
];
|
|
818
|
+
if (candidates.some(candidate => candidate && normalizeQueryText(candidate) === text)) {
|
|
819
|
+
return this.catalogFromBackendItem(item);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
parseWikiCommand(text) {
|
|
825
|
+
const trimmed = String(text || '').trim();
|
|
826
|
+
if (!trimmed)
|
|
827
|
+
return { catalog: null, query: '', pageNo: 1 };
|
|
828
|
+
const { parts, pageNo } = splitWikiCommandParts(trimmed);
|
|
829
|
+
const catalog = parts.length ? wikiCatalogByToken(parts[0]) : null;
|
|
830
|
+
if (catalog)
|
|
831
|
+
return { catalog: { ...catalog, aliases: [...catalog.aliases] }, query: parts.slice(1).join(' ').trim(), pageNo };
|
|
832
|
+
return { catalog: null, query: trimmed, pageNo };
|
|
833
|
+
}
|
|
834
|
+
catalogUsageText() {
|
|
835
|
+
const catalogs = this.catalogsFromPayload(this.catalogsCache || {});
|
|
836
|
+
const names = catalogs.slice(0, 24).map(item => item.title || item.key).join('、');
|
|
837
|
+
return [
|
|
838
|
+
'Wiki 用法:',
|
|
839
|
+
' 洛克wiki <类型> [关键词或ID]',
|
|
840
|
+
' 洛克wiki <关键词或ID>',
|
|
841
|
+
'示例:洛克wiki 水灵、洛克wiki 技能 圣光斩、洛克wiki 物品 xx球、洛克wiki 种植 食谱名',
|
|
842
|
+
`后端目录:${names || '请稍后重试'}`,
|
|
843
|
+
].join('\n');
|
|
844
|
+
}
|
|
845
|
+
buildCatalogRenderData(catalogsPayload, optionsPayload) {
|
|
846
|
+
const catalogs = this.catalogsFromPayload(catalogsPayload);
|
|
847
|
+
const topicCard = (catalog) => {
|
|
848
|
+
const backend = catalog._backend && typeof catalog._backend === 'object' ? catalog._backend : {};
|
|
849
|
+
const filters = Array.isArray(backend.filters) ? backend.filters : [];
|
|
850
|
+
const filterText = filters.slice(0, 6)
|
|
851
|
+
.filter((item) => item && typeof item === 'object')
|
|
852
|
+
.map((item) => String(item.label || item.key || ''))
|
|
853
|
+
.join('、');
|
|
854
|
+
const title = String(catalog.title || backend.name || catalog.key || 'Wiki');
|
|
855
|
+
const key = String(catalog.key || '');
|
|
856
|
+
const count = backend.count;
|
|
857
|
+
let hint = `洛克wiki ${title} <关键词或ID>`;
|
|
858
|
+
if (key)
|
|
859
|
+
hint = `${hint} / 洛克wiki ${key} <关键词或ID>`;
|
|
860
|
+
let coverage = String(backend.description || '').trim();
|
|
861
|
+
if (filterText)
|
|
862
|
+
coverage = `${coverage} 筛选:${filterText}`.trim();
|
|
863
|
+
return {
|
|
864
|
+
title,
|
|
865
|
+
key,
|
|
866
|
+
summary: count !== undefined && count !== null && count !== '' ? `${hint} · 条目 ${count}` : hint,
|
|
867
|
+
desc: key,
|
|
868
|
+
children: coverage,
|
|
869
|
+
};
|
|
870
|
+
};
|
|
871
|
+
const groups = [{
|
|
872
|
+
title: '后端 Wiki 图鉴入口',
|
|
873
|
+
desc: '',
|
|
874
|
+
items: catalogs.map(topicCard),
|
|
875
|
+
total: catalogs.length,
|
|
876
|
+
}];
|
|
877
|
+
return {
|
|
878
|
+
title: '洛克 Wiki',
|
|
879
|
+
subtitle: '统一资料查询入口',
|
|
880
|
+
summary: '',
|
|
881
|
+
badges: [],
|
|
882
|
+
facts: [],
|
|
883
|
+
primary: [],
|
|
884
|
+
groups,
|
|
885
|
+
commandHint: '💡 示例:洛克wiki 水灵 | 洛克wiki 技能 圣光斩 | 洛克wiki 物品 国王球',
|
|
886
|
+
copyright: COPYRIGHT,
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
globalSearchCatalogs(catalogsPayload) {
|
|
890
|
+
const catalogs = [];
|
|
891
|
+
const seen = new Set();
|
|
892
|
+
for (const catalog of this.catalogsFromPayload(catalogsPayload)) {
|
|
893
|
+
const key = String(catalog.key || '');
|
|
894
|
+
if (seen.has(key) || !catalog.list_path || catalog.search === false)
|
|
895
|
+
continue;
|
|
896
|
+
catalogs.push(catalog);
|
|
897
|
+
seen.add(key);
|
|
898
|
+
}
|
|
899
|
+
return catalogs;
|
|
900
|
+
}
|
|
901
|
+
resultCommandExamples(items, catalog, limit = 3) {
|
|
902
|
+
const examples = [];
|
|
903
|
+
const seen = new Set();
|
|
904
|
+
for (const item of items) {
|
|
905
|
+
if (!item || typeof item !== 'object')
|
|
906
|
+
continue;
|
|
907
|
+
const child = this.catalogForKeyFromPayload(String(item._catalog_key || ''), this.catalogsCache || {}) || catalog || null;
|
|
908
|
+
const category = String(item._catalog_title || child?.title || '').trim();
|
|
909
|
+
const title = wikiTitleForItem(item, '').trim();
|
|
910
|
+
if (!category || !title)
|
|
911
|
+
continue;
|
|
912
|
+
const command = `洛克wiki ${category} ${title}`;
|
|
913
|
+
if (seen.has(command))
|
|
914
|
+
continue;
|
|
915
|
+
examples.push(command);
|
|
916
|
+
seen.add(command);
|
|
917
|
+
if (examples.length >= limit)
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
return examples;
|
|
921
|
+
}
|
|
922
|
+
wikiPathParamsFromItem(catalog, item, rawText = '') {
|
|
923
|
+
const tokens = String(rawText || '').split(/\s+/).filter(Boolean);
|
|
924
|
+
const params = {};
|
|
925
|
+
const idFields = catalog.id_fields || [];
|
|
926
|
+
idFields.forEach((field, idx) => {
|
|
927
|
+
let value = item?.[field];
|
|
928
|
+
const empty = (v) => v === undefined || v === null || v === '';
|
|
929
|
+
if (empty(value) && ['item_id', 'asset_id', 'action_id'].includes(field))
|
|
930
|
+
value = item?.id;
|
|
931
|
+
if (empty(value) && ['ball_id', 'plant_id', 'carryon_id'].includes(field))
|
|
932
|
+
value = item?.item_id ?? item?.id;
|
|
933
|
+
if (empty(value) && idx < tokens.length)
|
|
934
|
+
value = tokens[idx];
|
|
935
|
+
if (!empty(value))
|
|
936
|
+
params[field] = String(value);
|
|
937
|
+
});
|
|
938
|
+
return params;
|
|
939
|
+
}
|
|
940
|
+
fillDetailPath(catalog, params) {
|
|
941
|
+
let path = String(catalog.detail_path || '');
|
|
942
|
+
for (const field of catalog.id_fields || []) {
|
|
943
|
+
const value = params[field];
|
|
944
|
+
if (value === undefined || value === null || value === '')
|
|
945
|
+
return '';
|
|
946
|
+
path = path.replace(`{${field}}`, String(value));
|
|
947
|
+
}
|
|
948
|
+
return path;
|
|
949
|
+
}
|
|
950
|
+
itemMatchesQueryExact(catalog, item, query) {
|
|
951
|
+
const trimmed = String(query || '').trim();
|
|
952
|
+
if (!trimmed)
|
|
953
|
+
return false;
|
|
954
|
+
const normalizedQuery = normalizeQueryText(trimmed);
|
|
955
|
+
for (const field of catalog.id_fields || []) {
|
|
956
|
+
let value = item?.[field];
|
|
957
|
+
if ((value === undefined || value === null || value === '') && ['ball_id', 'plant_id', 'carryon_id'].includes(field)) {
|
|
958
|
+
value = item?.item_id ?? item?.id;
|
|
959
|
+
}
|
|
960
|
+
if (String(value ?? '') === trimmed)
|
|
961
|
+
return true;
|
|
962
|
+
}
|
|
963
|
+
return normalizeQueryText(wikiTitleForItem(item, '')) === normalizedQuery;
|
|
964
|
+
}
|
|
965
|
+
findCatalogMatch(catalog, items, query, allowSingle = true) {
|
|
966
|
+
const trimmed = String(query || '').trim();
|
|
967
|
+
if (!trimmed)
|
|
968
|
+
return null;
|
|
969
|
+
for (const item of items) {
|
|
970
|
+
if (this.itemMatchesQueryExact(catalog, item, trimmed))
|
|
971
|
+
return item;
|
|
972
|
+
}
|
|
973
|
+
if (allowSingle && items.length === 1)
|
|
974
|
+
return items[0];
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
async suggestCatalogItems(catalog, query, limit = 10) {
|
|
978
|
+
if (!catalog || !catalog.list_path)
|
|
979
|
+
return [];
|
|
980
|
+
const trimmed = String(query || '').trim();
|
|
981
|
+
if (!trimmed)
|
|
982
|
+
return [];
|
|
983
|
+
const terms = [];
|
|
984
|
+
for (const term of [trimmed, trimmed.slice(0, 2), trimmed.slice(0, 1), trimmed.slice(0, -1)]) {
|
|
985
|
+
const cleaned = String(term || '').trim();
|
|
986
|
+
if (cleaned && !terms.includes(cleaned))
|
|
987
|
+
terms.push(cleaned);
|
|
988
|
+
}
|
|
989
|
+
const collected = new Map();
|
|
990
|
+
for (const term of terms.slice(0, 4)) {
|
|
991
|
+
const res = await this.client.listWikiCatalogItems(this.ctx, catalog.list_path, term, 1, 30, Boolean(term && catalog.search !== false));
|
|
992
|
+
const items = res && typeof res === 'object' ? res.items : [];
|
|
993
|
+
for (const item of items || []) {
|
|
994
|
+
if (!item || typeof item !== 'object')
|
|
995
|
+
continue;
|
|
996
|
+
const title = wikiTitleForItem(item, '');
|
|
997
|
+
const keyParts = [catalog.key || '', title];
|
|
998
|
+
for (const field of catalog.id_fields || []) {
|
|
999
|
+
if (item[field] !== undefined && item[field] !== null && item[field] !== '')
|
|
1000
|
+
keyParts.push(String(item[field]));
|
|
1001
|
+
}
|
|
1002
|
+
const dedupeKey = keyParts.join('|');
|
|
1003
|
+
if (!collected.has(dedupeKey))
|
|
1004
|
+
collected.set(dedupeKey, item);
|
|
1005
|
+
}
|
|
1006
|
+
if (collected.size >= limit * 3)
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
const scored = [];
|
|
1010
|
+
for (const item of collected.values()) {
|
|
1011
|
+
const score = wikiSimilarityScore(trimmed, item);
|
|
1012
|
+
if (score >= 0.18)
|
|
1013
|
+
scored.push([score, item]);
|
|
1014
|
+
}
|
|
1015
|
+
scored.sort((a, b) => b[0] - a[0]);
|
|
1016
|
+
if (!scored.length)
|
|
1017
|
+
return [...collected.values()].slice(0, limit);
|
|
1018
|
+
return scored.slice(0, limit).map(([, item]) => item);
|
|
1019
|
+
}
|
|
1020
|
+
buildGenericRenderData(catalog, payload, query, mode, pageNo = 1) {
|
|
1021
|
+
payload = payload || {};
|
|
1022
|
+
const fullCatalog = catalog;
|
|
1023
|
+
if (mode === 'global-search') {
|
|
1024
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
1025
|
+
const cards = [];
|
|
1026
|
+
for (const item of items) {
|
|
1027
|
+
if (!item || typeof item !== 'object')
|
|
1028
|
+
continue;
|
|
1029
|
+
const child = this.catalogForKeyFromPayload(String(item._catalog_key || ''), this.catalogsCache || {}) || fullCatalog;
|
|
1030
|
+
const card = wikiCardForItem(item, child?.title || 'Wiki', child);
|
|
1031
|
+
if (item._catalog_title)
|
|
1032
|
+
card.badges = [item._catalog_title, ...card.badges];
|
|
1033
|
+
cards.push(card);
|
|
1034
|
+
}
|
|
1035
|
+
return {
|
|
1036
|
+
title: `全局搜索:${query}`,
|
|
1037
|
+
subtitle: 'Wiki / 全局搜索',
|
|
1038
|
+
image: '',
|
|
1039
|
+
summary: '未指定分类时,会在所有可搜索的大分类接口中使用后端 q 参数查询。',
|
|
1040
|
+
badges: ['全局搜索', '可继续指定分类'],
|
|
1041
|
+
facts: [
|
|
1042
|
+
{ label: '查询词', value: query || '-' },
|
|
1043
|
+
{ label: '结果数', value: String(payload?.total ?? cards.length) },
|
|
1044
|
+
{ label: '搜索接口', value: String(this.globalSearchCatalogs(this.catalogsCache || {}).length) },
|
|
1045
|
+
],
|
|
1046
|
+
actionHint: '要查看具体条目,请按卡片绿色分类 tag 加名称继续查询。',
|
|
1047
|
+
actionExamples: this.resultCommandExamples(items, fullCatalog),
|
|
1048
|
+
cards,
|
|
1049
|
+
sections: [],
|
|
1050
|
+
commandHint: '💡 结果过多时可指定分类,例如 洛克wiki 技能 水花 或 洛克wiki 物品 国王球',
|
|
1051
|
+
copyright: COPYRIGHT,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
if (mode === 'suggestions') {
|
|
1055
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
1056
|
+
const cards = [];
|
|
1057
|
+
for (const item of items) {
|
|
1058
|
+
if (!item || typeof item !== 'object')
|
|
1059
|
+
continue;
|
|
1060
|
+
const child = this.catalogForKeyFromPayload(String(item._catalog_key || ''), this.catalogsCache || {}) || fullCatalog;
|
|
1061
|
+
const card = wikiCardForItem(item, child?.title || fullCatalog.title, child);
|
|
1062
|
+
if (item._catalog_title)
|
|
1063
|
+
card.badges = [item._catalog_title, ...card.badges];
|
|
1064
|
+
cards.push(card);
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
title: `没有找到「${query}」`,
|
|
1068
|
+
subtitle: `Wiki / ${fullCatalog.title} / 联想结果`,
|
|
1069
|
+
summary: '接口没有返回精确结果,下面是按名称、摘要和相似度整理的候选。',
|
|
1070
|
+
image: '',
|
|
1071
|
+
badges: [fullCatalog.title, '联想'],
|
|
1072
|
+
facts: [
|
|
1073
|
+
{ label: '查询词', value: query || '-' },
|
|
1074
|
+
{ label: '候选数', value: String(cards.length) },
|
|
1075
|
+
],
|
|
1076
|
+
actionHint: '接口没有精确命中时,可按候选卡片的绿色分类 tag 加名称继续查询。',
|
|
1077
|
+
actionExamples: this.resultCommandExamples(items, fullCatalog),
|
|
1078
|
+
cards,
|
|
1079
|
+
sections: [],
|
|
1080
|
+
commandHint: `💡 可使用 洛克wiki ${fullCatalog.title} <候选名称或ID> 继续查询`,
|
|
1081
|
+
copyright: COPYRIGHT,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
if (mode === 'list') {
|
|
1085
|
+
const items = Array.isArray(payload?.items) ? payload.items : [];
|
|
1086
|
+
const cards = items.filter((item) => item && typeof item === 'object')
|
|
1087
|
+
.map((item) => wikiCardForItem(item, fullCatalog.title, fullCatalog));
|
|
1088
|
+
const catalogInfo = payload?.catalog && typeof payload.catalog === 'object' ? payload.catalog : {};
|
|
1089
|
+
return {
|
|
1090
|
+
title: `${fullCatalog.title}列表`,
|
|
1091
|
+
subtitle: `Wiki / ${fullCatalog.title} / 第 ${pageNo} 页`,
|
|
1092
|
+
image: '',
|
|
1093
|
+
summary: catalogInfo.description || `共 ${payload?.total ?? cards.length} 条`,
|
|
1094
|
+
badges: [fullCatalog.title, '列表'],
|
|
1095
|
+
facts: [
|
|
1096
|
+
{ label: '当前页', value: String(payload?.page_no ?? pageNo) },
|
|
1097
|
+
{ label: '总页数', value: String(payload?.total_pages ?? '-') },
|
|
1098
|
+
{ label: '总数', value: String(payload?.total ?? cards.length) },
|
|
1099
|
+
],
|
|
1100
|
+
actionHint: query ? '搜索结果较多时,可按分类加名称继续查询具体条目。' : '',
|
|
1101
|
+
actionExamples: query ? this.resultCommandExamples(items, fullCatalog) : [],
|
|
1102
|
+
cards,
|
|
1103
|
+
sections: [],
|
|
1104
|
+
commandHint: '💡 洛克wiki <类型> <关键词或ID> | 洛克wiki 查看支持类型',
|
|
1105
|
+
copyright: COPYRIGHT,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const item = payload && typeof payload === 'object' ? payload : {};
|
|
1109
|
+
return {
|
|
1110
|
+
title: wikiTitleForItem(item, fullCatalog.title),
|
|
1111
|
+
subtitle: `Wiki / ${fullCatalog.title}`,
|
|
1112
|
+
image: wikiPickImage(item),
|
|
1113
|
+
summary: wikiSummaryForItem(item),
|
|
1114
|
+
badges: [fullCatalog.title, ...wikiBadgesForItem(item)],
|
|
1115
|
+
facts: wikiMetaForItem(item, 16, String(fullCatalog.key || '')),
|
|
1116
|
+
cards: [],
|
|
1117
|
+
sections: wikiSectionsForPayload(item, fullCatalog),
|
|
1118
|
+
commandHint: '💡 洛克wiki <类型> <关键词或ID> | 洛克wiki 查看支持类型',
|
|
1119
|
+
copyright: COPYRIGHT,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
mergeDetailPayloads(base, extra) {
|
|
1123
|
+
const merged = { ...(base || {}) };
|
|
1124
|
+
for (const [key, value] of Object.entries(extra || {})) {
|
|
1125
|
+
if (value === undefined || value === null || value === ''
|
|
1126
|
+
|| (Array.isArray(value) && !value.length)
|
|
1127
|
+
|| (typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length))
|
|
1128
|
+
continue;
|
|
1129
|
+
const oldValue = merged[key];
|
|
1130
|
+
if (oldValue && typeof oldValue === 'object' && !Array.isArray(oldValue)
|
|
1131
|
+
&& value && typeof value === 'object' && !Array.isArray(value)) {
|
|
1132
|
+
merged[key] = this.mergeDetailPayloads(oldValue, value);
|
|
1133
|
+
}
|
|
1134
|
+
else if (Array.isArray(oldValue) && Array.isArray(value)) {
|
|
1135
|
+
const seen = new Set();
|
|
1136
|
+
const combined = [];
|
|
1137
|
+
for (const item of [...oldValue, ...value]) {
|
|
1138
|
+
const marker = typeof item === 'object' ? JSON.stringify(item) : String(item);
|
|
1139
|
+
if (seen.has(marker))
|
|
1140
|
+
continue;
|
|
1141
|
+
seen.add(marker);
|
|
1142
|
+
combined.push(item);
|
|
1143
|
+
}
|
|
1144
|
+
merged[key] = combined;
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
merged[key] = value;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
return merged;
|
|
1151
|
+
}
|
|
1152
|
+
itemDetailCompanionKeys(catalog, detail) {
|
|
1153
|
+
const catalogKey = String(catalog?.key || '');
|
|
1154
|
+
const itemId = detail?.item_id;
|
|
1155
|
+
if (itemId === undefined || itemId === null || itemId === '')
|
|
1156
|
+
return [];
|
|
1157
|
+
if (['skill-stones', 'skill-stone-recipes', 'balls', 'medals', 'pet-carryons'].includes(catalogKey)) {
|
|
1158
|
+
return detail?.item_kind ? ['items'] : [];
|
|
1159
|
+
}
|
|
1160
|
+
if (catalogKey !== 'items')
|
|
1161
|
+
return [];
|
|
1162
|
+
const flags = detail?.flags && typeof detail.flags === 'object' ? detail.flags : {};
|
|
1163
|
+
const typeLabel = wikiGenericValue(detail?.type || detail?.label_type);
|
|
1164
|
+
if (flags.is_skill_stone || typeLabel === '技能石')
|
|
1165
|
+
return ['skill-stones'];
|
|
1166
|
+
if (flags.is_skill_stone_recipe || String(detail?.name || '').startsWith('配方-'))
|
|
1167
|
+
return ['skill-stone-recipes'];
|
|
1168
|
+
if (typeLabel === '咕噜球')
|
|
1169
|
+
return ['balls'];
|
|
1170
|
+
if (['奖章', '勋章'].includes(typeLabel))
|
|
1171
|
+
return ['medals'];
|
|
1172
|
+
if (['携带物', '精灵携带物'].includes(typeLabel))
|
|
1173
|
+
return ['pet-carryons'];
|
|
1174
|
+
return [];
|
|
1175
|
+
}
|
|
1176
|
+
async enrichItemDetail(catalog, detail) {
|
|
1177
|
+
if (!detail || typeof detail !== 'object')
|
|
1178
|
+
return detail;
|
|
1179
|
+
const itemId = detail.item_id;
|
|
1180
|
+
if (itemId === undefined || itemId === null || itemId === '')
|
|
1181
|
+
return detail;
|
|
1182
|
+
let merged = { ...detail };
|
|
1183
|
+
for (const key of this.itemDetailCompanionKeys(catalog, detail)) {
|
|
1184
|
+
const companion = this.catalogForKeyFromPayload(key, this.catalogsCache || {});
|
|
1185
|
+
if (!companion)
|
|
1186
|
+
continue;
|
|
1187
|
+
if (key === 'items') {
|
|
1188
|
+
const itemKind = detail.item_kind;
|
|
1189
|
+
if (itemKind === undefined || itemKind === null || itemKind === '')
|
|
1190
|
+
continue;
|
|
1191
|
+
const companionDetail = await this.client.getWikiPath(this.ctx, `/api/v1/games/rocom/wiki/items/${itemKind}/${itemId}`);
|
|
1192
|
+
if (companionDetail)
|
|
1193
|
+
merged = this.mergeDetailPayloads(companionDetail, merged);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const companionDetail = await this.client.getWikiPath(this.ctx, `/api/v1/games/rocom/wiki/${key}/${itemId}`);
|
|
1197
|
+
if (companionDetail)
|
|
1198
|
+
merged = this.mergeDetailPayloads(merged, companionDetail);
|
|
1199
|
+
}
|
|
1200
|
+
return merged;
|
|
1201
|
+
}
|
|
1202
|
+
async resolveWikiPet(query) {
|
|
1203
|
+
const trimmed = String(query || '').trim();
|
|
1204
|
+
if (!trimmed)
|
|
1205
|
+
return { detail: null, candidates: [], error: '请输入精灵名称或 ID。用法:洛克wiki 精灵 <精灵名或ID>' };
|
|
1206
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1207
|
+
const detail = await this.client.getWikiPet(this.ctx, trimmed);
|
|
1208
|
+
if (detail)
|
|
1209
|
+
return { detail, candidates: [], error: '' };
|
|
1210
|
+
return { detail: null, candidates: [], error: `获取 Wiki 精灵详情失败:${this.client.getLastError()}` };
|
|
1211
|
+
}
|
|
1212
|
+
const searchRes = await this.client.listWikiPets(this.ctx, trimmed, 1, 10);
|
|
1213
|
+
const items = (searchRes || {}).items || [];
|
|
1214
|
+
if (!items.length) {
|
|
1215
|
+
const suggestions = await this.suggestCatalogItems(await this.getCatalogByKey('pets'), trimmed, 10);
|
|
1216
|
+
if (suggestions.length)
|
|
1217
|
+
return { detail: null, candidates: suggestions, error: '' };
|
|
1218
|
+
return { detail: null, candidates: [], error: `未找到「${trimmed}」的 Wiki 精灵资料:${this.client.getLastError('无匹配结果')}` };
|
|
1219
|
+
}
|
|
1220
|
+
let selected = findExactWikiMatch(items, trimmed);
|
|
1221
|
+
if (!selected && items.length === 1)
|
|
1222
|
+
selected = items[0];
|
|
1223
|
+
if (!selected)
|
|
1224
|
+
return { detail: null, candidates: items, error: '' };
|
|
1225
|
+
const detail = await this.client.getWikiPet(this.ctx, selected.pet_id);
|
|
1226
|
+
if (detail)
|
|
1227
|
+
return { detail, candidates: [], error: '' };
|
|
1228
|
+
return { detail: null, candidates: [], error: `获取 Wiki 精灵详情失败:${this.client.getLastError()}` };
|
|
1229
|
+
}
|
|
1230
|
+
async resolveWikiSkill(query) {
|
|
1231
|
+
const trimmed = String(query || '').trim();
|
|
1232
|
+
if (!trimmed)
|
|
1233
|
+
return { detail: null, candidates: [], error: '请输入技能名称或 ID。用法:洛克wiki 技能 <技能名或ID>' };
|
|
1234
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1235
|
+
const detail = await this.client.getWikiSkill(this.ctx, trimmed);
|
|
1236
|
+
if (detail)
|
|
1237
|
+
return { detail, candidates: [], error: '' };
|
|
1238
|
+
return { detail: null, candidates: [], error: `获取技能详情失败:${this.client.getLastError()}` };
|
|
1239
|
+
}
|
|
1240
|
+
const searchRes = await this.client.listWikiSkills(this.ctx, trimmed, 1, 10);
|
|
1241
|
+
const items = (searchRes || {}).items || [];
|
|
1242
|
+
if (!items.length) {
|
|
1243
|
+
const suggestions = await this.suggestCatalogItems(await this.getCatalogByKey('skills'), trimmed, 10);
|
|
1244
|
+
if (suggestions.length)
|
|
1245
|
+
return { detail: null, candidates: suggestions, error: '' };
|
|
1246
|
+
return { detail: null, candidates: [], error: `未找到「${trimmed}」的技能 Wiki 资料:${this.client.getLastError('无匹配结果')}。用法:洛克wiki 技能 <技能名或ID>` };
|
|
1247
|
+
}
|
|
1248
|
+
let selected = findExactWikiMatch(items, trimmed);
|
|
1249
|
+
if (!selected && items.length === 1)
|
|
1250
|
+
selected = items[0];
|
|
1251
|
+
if (!selected)
|
|
1252
|
+
return { detail: null, candidates: items, error: '' };
|
|
1253
|
+
const detail = await this.client.getWikiSkill(this.ctx, selected.skill_id);
|
|
1254
|
+
if (detail)
|
|
1255
|
+
return { detail, candidates: [], error: '' };
|
|
1256
|
+
return { detail: null, candidates: [], error: `获取技能详情失败:${this.client.getLastError()}` };
|
|
1257
|
+
}
|
|
1258
|
+
async fetchWikiPetSections(petId) {
|
|
1259
|
+
const [profile, skills, family, handbook] = await Promise.all([
|
|
1260
|
+
this.client.getWikiPetProfile(this.ctx, petId).catch(() => null),
|
|
1261
|
+
this.client.getWikiPetSkills(this.ctx, petId).catch(() => null),
|
|
1262
|
+
this.client.getWikiPetFamily(this.ctx, petId).catch(() => null),
|
|
1263
|
+
this.client.getWikiPetHandbook(this.ctx, petId).catch(() => null),
|
|
1264
|
+
]);
|
|
1265
|
+
return {
|
|
1266
|
+
profile: profile && typeof profile === 'object' ? profile : {},
|
|
1267
|
+
skills: skills && typeof skills === 'object' ? skills : {},
|
|
1268
|
+
family: family && typeof family === 'object' ? family : {},
|
|
1269
|
+
handbook: handbook && typeof handbook === 'object' ? handbook : {},
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
// 通过缓存查询技能详情(供家园详情技能补全使用)
|
|
1273
|
+
async getSkillDetailCached(skillId) {
|
|
1274
|
+
const key = String(skillId || '').trim();
|
|
1275
|
+
if (!key)
|
|
1276
|
+
return null;
|
|
1277
|
+
if (this.skillDetailCache.has(key))
|
|
1278
|
+
return this.skillDetailCache.get(key);
|
|
1279
|
+
const detail = await this.client.getWikiSkill(this.ctx, key);
|
|
1280
|
+
if (detail && typeof detail === 'object') {
|
|
1281
|
+
this.skillDetailCache.set(key, detail);
|
|
1282
|
+
return detail;
|
|
1283
|
+
}
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
// 通过缓存查询精灵详情(供家园详情体重范围使用)
|
|
1287
|
+
async getPetDetailCached(petId) {
|
|
1288
|
+
const key = String(petId || '').trim();
|
|
1289
|
+
if (!key)
|
|
1290
|
+
return null;
|
|
1291
|
+
if (this.petDetailCache.has(key))
|
|
1292
|
+
return this.petDetailCache.get(key);
|
|
1293
|
+
const detail = await this.client.getWikiPet(this.ctx, key);
|
|
1294
|
+
if (detail && typeof detail === 'object') {
|
|
1295
|
+
this.petDetailCache.set(key, detail);
|
|
1296
|
+
return detail;
|
|
1297
|
+
}
|
|
1298
|
+
return null;
|
|
1299
|
+
}
|
|
1300
|
+
async fetchGenericCatalog(catalog, query, pageNo) {
|
|
1301
|
+
const trimmed = String(query || '').trim();
|
|
1302
|
+
const detailPath = String(catalog.detail_path || '');
|
|
1303
|
+
const listPath = String(catalog.list_path || '');
|
|
1304
|
+
if (detailPath && trimmed) {
|
|
1305
|
+
const directParams = this.wikiPathParamsFromItem(catalog, {}, trimmed);
|
|
1306
|
+
const idFields = catalog.id_fields || [];
|
|
1307
|
+
if (idFields.length === 1 && idFields[0] === 'pet_id' && !/^\d+$/.test(trimmed)) {
|
|
1308
|
+
const { detail: pet } = await this.resolveWikiPet(trimmed);
|
|
1309
|
+
if (pet?.pet_id !== undefined && pet?.pet_id !== null && pet?.pet_id !== '')
|
|
1310
|
+
directParams.pet_id = String(pet.pet_id);
|
|
1311
|
+
}
|
|
1312
|
+
if (idFields.length === 1 && idFields[0] === 'skill_id' && !/^\d+$/.test(trimmed)) {
|
|
1313
|
+
const { detail: skill } = await this.resolveWikiSkill(trimmed);
|
|
1314
|
+
if (skill?.skill_id !== undefined && skill?.skill_id !== null && skill?.skill_id !== '')
|
|
1315
|
+
directParams.skill_id = String(skill.skill_id);
|
|
1316
|
+
}
|
|
1317
|
+
const directPath = this.fillDetailPath(catalog, directParams);
|
|
1318
|
+
const typeFields = new Set(['item_kind', 'asset_type', 'action_type']);
|
|
1319
|
+
let directAllowed = Boolean(directPath) && Object.keys(directParams).length >= idFields.length;
|
|
1320
|
+
for (const field of idFields) {
|
|
1321
|
+
if (typeFields.has(field))
|
|
1322
|
+
continue;
|
|
1323
|
+
if (!/^\d+$/.test(String(directParams[field] || ''))) {
|
|
1324
|
+
directAllowed = false;
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
if (directAllowed) {
|
|
1329
|
+
const detail = await this.client.getWikiPath(this.ctx, directPath);
|
|
1330
|
+
if (detail)
|
|
1331
|
+
return { payload: await this.enrichItemDetail(catalog, detail), mode: 'detail', error: '' };
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
if (!listPath) {
|
|
1335
|
+
return { payload: null, mode: 'detail', error: `${catalog.title} 需要提供 ID。用法:洛克wiki ${catalog.title} <ID>` };
|
|
1336
|
+
}
|
|
1337
|
+
const listRes = await this.client.listWikiCatalogItems(this.ctx, listPath, trimmed, pageNo, 12, catalog.search !== false);
|
|
1338
|
+
if (listRes === null) {
|
|
1339
|
+
return { payload: null, mode: 'list', error: `获取 ${catalog.title} 失败:${this.client.getLastError()}` };
|
|
1340
|
+
}
|
|
1341
|
+
if (!listRes || typeof listRes !== 'object' || !('items' in listRes)) {
|
|
1342
|
+
return { payload: listRes, mode: 'detail', error: '' };
|
|
1343
|
+
}
|
|
1344
|
+
const items = listRes.items || [];
|
|
1345
|
+
if (trimmed && !items.length) {
|
|
1346
|
+
const suggestions = await this.suggestCatalogItems(catalog, trimmed, 10);
|
|
1347
|
+
if (suggestions.length)
|
|
1348
|
+
return { payload: { items: suggestions, query: trimmed }, mode: 'suggestions', error: '' };
|
|
1349
|
+
}
|
|
1350
|
+
if (detailPath && trimmed && items.length) {
|
|
1351
|
+
const selected = this.findCatalogMatch(catalog, items, trimmed);
|
|
1352
|
+
if (selected) {
|
|
1353
|
+
const params = this.wikiPathParamsFromItem(catalog, selected, trimmed);
|
|
1354
|
+
const selectedPath = this.fillDetailPath(catalog, params);
|
|
1355
|
+
if (selectedPath) {
|
|
1356
|
+
const detail = await this.client.getWikiPath(this.ctx, selectedPath);
|
|
1357
|
+
if (detail)
|
|
1358
|
+
return { payload: await this.enrichItemDetail(catalog, detail), mode: 'detail', error: '' };
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
return { payload: listRes, mode: 'list', error: '' };
|
|
1363
|
+
}
|
|
1364
|
+
async fetchDetailForCatalogItem(catalog, item, query) {
|
|
1365
|
+
if (!catalog.detail_path)
|
|
1366
|
+
return null;
|
|
1367
|
+
const params = this.wikiPathParamsFromItem(catalog, item, query);
|
|
1368
|
+
const detailPath = this.fillDetailPath(catalog, params);
|
|
1369
|
+
if (!detailPath)
|
|
1370
|
+
return null;
|
|
1371
|
+
const detail = await this.client.getWikiPath(this.ctx, detailPath);
|
|
1372
|
+
return this.enrichItemDetail(catalog, detail);
|
|
1373
|
+
}
|
|
1374
|
+
async fetchGlobalSearch(query, pageNo) {
|
|
1375
|
+
const trimmed = String(query || '').trim();
|
|
1376
|
+
if (!trimmed)
|
|
1377
|
+
return { payload: null, mode: 'global-search', error: '请输入 Wiki 关键词。' };
|
|
1378
|
+
const catalogsPayload = await this.getCatalogsPayload();
|
|
1379
|
+
const catalogs = this.globalSearchCatalogs(catalogsPayload);
|
|
1380
|
+
if (!catalogs.length)
|
|
1381
|
+
return { payload: null, mode: 'global-search', error: '暂无可全局搜索的 Wiki 接口。' };
|
|
1382
|
+
// 有限并发抓取各目录
|
|
1383
|
+
const results = [];
|
|
1384
|
+
const concurrency = 8;
|
|
1385
|
+
let index = 0;
|
|
1386
|
+
const worker = async () => {
|
|
1387
|
+
while (index < catalogs.length) {
|
|
1388
|
+
const catalog = catalogs[index++];
|
|
1389
|
+
try {
|
|
1390
|
+
const res = await this.client.listWikiCatalogItems(this.ctx, catalog.list_path, trimmed, pageNo, 6, true);
|
|
1391
|
+
if (res === null) {
|
|
1392
|
+
results.push({ catalog, items: [], error: this.client.getLastError('') });
|
|
1393
|
+
}
|
|
1394
|
+
else {
|
|
1395
|
+
const items = res && typeof res === 'object' ? res.items : [];
|
|
1396
|
+
results.push({ catalog, items: (items || []).filter((item) => item && typeof item === 'object'), error: '' });
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
catch (e) {
|
|
1400
|
+
results.push({ catalog, items: [], error: String(e) });
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, catalogs.length) }, worker));
|
|
1405
|
+
const collected = new Map();
|
|
1406
|
+
const exactKeys = new Set();
|
|
1407
|
+
const errors = [];
|
|
1408
|
+
for (const { catalog, items, error } of results) {
|
|
1409
|
+
if (error) {
|
|
1410
|
+
errors.push(`${catalog.title || catalog.key}:${error}`);
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
for (const item of items) {
|
|
1414
|
+
const merged = { ...item, _catalog_key: catalog.key, _catalog_title: catalog.title };
|
|
1415
|
+
const title = wikiTitleForItem(item, '');
|
|
1416
|
+
const keyParts = [String(catalog.key || ''), title];
|
|
1417
|
+
for (const field of catalog.id_fields || []) {
|
|
1418
|
+
let value = item[field];
|
|
1419
|
+
if ((value === undefined || value === null || value === '') && ['ball_id', 'plant_id', 'carryon_id'].includes(field)) {
|
|
1420
|
+
value = item.item_id ?? item.id;
|
|
1421
|
+
}
|
|
1422
|
+
if (value !== undefined && value !== null && value !== '')
|
|
1423
|
+
keyParts.push(String(value));
|
|
1424
|
+
}
|
|
1425
|
+
const dedupeKey = keyParts.join('|');
|
|
1426
|
+
if (collected.has(dedupeKey))
|
|
1427
|
+
continue;
|
|
1428
|
+
let score = wikiSimilarityScore(trimmed, merged);
|
|
1429
|
+
if (this.itemMatchesQueryExact(catalog, item, trimmed)) {
|
|
1430
|
+
score += 10 - (0, wiki_catalog_1.wikiGlobalCatalogPriority)(catalog) * 0.01;
|
|
1431
|
+
exactKeys.add(dedupeKey);
|
|
1432
|
+
}
|
|
1433
|
+
merged._match_score = score;
|
|
1434
|
+
collected.set(dedupeKey, merged);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
const items = [...collected.values()];
|
|
1438
|
+
items.sort((a, b) => (b._match_score || 0) - (a._match_score || 0));
|
|
1439
|
+
const exactItems = [...exactKeys].filter(key => collected.has(key)).map(key => collected.get(key));
|
|
1440
|
+
if (exactItems.length) {
|
|
1441
|
+
const priorityOf = (item) => (0, wiki_catalog_1.wikiGlobalCatalogPriority)(this.catalogForKeyFromPayload(String(item._catalog_key || ''), catalogsPayload));
|
|
1442
|
+
exactItems.sort((a, b) => priorityOf(a) - priorityOf(b) || (b._match_score || 0) - (a._match_score || 0));
|
|
1443
|
+
const bestPriority = priorityOf(exactItems[0]);
|
|
1444
|
+
const bestItems = exactItems.filter(item => priorityOf(item) === bestPriority);
|
|
1445
|
+
if (bestItems.length === 1) {
|
|
1446
|
+
const exactItem = bestItems[0];
|
|
1447
|
+
const catalog = this.catalogForKeyFromPayload(String(exactItem._catalog_key || ''), catalogsPayload);
|
|
1448
|
+
if (catalog) {
|
|
1449
|
+
const detail = await this.fetchDetailForCatalogItem(catalog, exactItem, trimmed);
|
|
1450
|
+
if (detail)
|
|
1451
|
+
return { payload: { item: detail, _catalog: catalog }, mode: 'global-detail', error: '' };
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (items.length) {
|
|
1456
|
+
return { payload: { items: items.slice(0, 12), query: trimmed, total: items.length, _global: true }, mode: 'global-search', error: '' };
|
|
1457
|
+
}
|
|
1458
|
+
if (errors.length)
|
|
1459
|
+
return { payload: null, mode: 'global-search', error: `全局搜索失败:${errors[0]}` };
|
|
1460
|
+
return { payload: { items: [], query: trimmed, total: 0, _global: true }, mode: 'global-search', error: '' };
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
exports.WikiService = WikiService;
|