koishi-plugin-dwrgbwiki 1.3.2 → 2.0.0

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 (9) hide show
  1. package/basicdata.js +407 -0
  2. package/cangtou.js +166 -0
  3. package/codex.js +524 -0
  4. package/gacha.js +1498 -1501
  5. package/index.js +923 -406
  6. package/lib.js +1453 -1453
  7. package/package.json +32 -28
  8. package/readme.md +223 -125
  9. package/wiki.js +96 -0
package/basicdata.js ADDED
@@ -0,0 +1,407 @@
1
+ 'use strict'
2
+
3
+ const CAMP_META = {
4
+ hunter: { label: '监管者', file: 'hunter_data.csv', enc: 'utf8' },
5
+ }
6
+
7
+ const FALLBACK_COLUMNS = {
8
+ hunter: ['模型体积', '移动速度', '转向速度', '跨过窗户', '摧毁木板', '被砸眩晕', '普攻前摇', '普攻距离最小值', '普攻距离最大值', '普攻空刀后摇', '普攻擦刀', '蓄力前摇', '蓄力距离最小值', '蓄力距离最大值', '蓄力空刀后摇', '蓄力擦刀', '牵气球时长', '挣脱眩晕时长', '牵气球攻击前摇', '气球刀最小距离', '气球刀最大距离', '恐惧半径', '弹刀时长'],
9
+ }
10
+
11
+ const CACHE_TTL = 15 * 60 * 1000
12
+ const cache = new Map()
13
+
14
+ function esc(s) {
15
+ return String(s == null ? '' : s)
16
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;')
18
+ }
19
+
20
+ function campLabel(camp) {
21
+ return CAMP_META[camp] ? CAMP_META[camp].label : camp
22
+ }
23
+
24
+ function baseName(name) {
25
+ return String(name)
26
+ .replace(/["“”]/g, '')
27
+ .replace(/[((].*?[))]$/, '')
28
+ .trim()
29
+ }
30
+
31
+ async function fetchTextRaw(fullUrl, timeout) {
32
+ const hit = cache.get(fullUrl)
33
+ if (hit && Date.now() - hit.ts < hit.ttl) return hit.buf
34
+ const ctrl = new AbortController()
35
+ const timer = setTimeout(() => ctrl.abort(), timeout)
36
+ try {
37
+ const res = await fetch(fullUrl, { signal: ctrl.signal })
38
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' ' + fullUrl)
39
+ const buf = Buffer.from(await res.arrayBuffer())
40
+ cache.set(fullUrl, { buf, ts: Date.now(), ttl: CACHE_TTL })
41
+ return buf
42
+ } finally {
43
+ clearTimeout(timer)
44
+ }
45
+ }
46
+
47
+ function parseCSV(text) {
48
+ const lines = text.trim().split('\n').filter(l => l.trim())
49
+ if (lines.length < 2) throw new Error('CSV 数据为空')
50
+ const rawHeaders = lines[0].split(',').map(h => h.trim()).filter(Boolean)
51
+ const columns = []
52
+ rawHeaders.forEach((h, i) => {
53
+ if (i === 0) return
54
+ const m = h.match(/^(.*?)\(([01])\)$/)
55
+ if (m) {
56
+ columns.push({ name: m[1], raw: h, idx: i, sortType: m[2] === '1' ? 'desc' : 'asc' })
57
+ } else {
58
+ columns.push({ name: h, raw: h, idx: i, sortType: 'asc' })
59
+ }
60
+ })
61
+ const rows = []
62
+ lines.slice(1).forEach(line => {
63
+ const vals = line.split(',').map(v => {
64
+ const s = v.trim().replace(/^"|"$/g, '')
65
+ if (s !== '' && !isNaN(parseFloat(s)) && isFinite(s)) return parseFloat(s)
66
+ return s
67
+ })
68
+ rows.push({ name: vals[0], vals })
69
+ })
70
+ return { columns, rows }
71
+ }
72
+
73
+ async function loadCamp(camp, config) {
74
+ const meta = CAMP_META[camp] || CAMP_META.hunter
75
+ const base = (config.dataSource || 'https://website.homeworkkun.top').replace(/\/+$/, '')
76
+ const url = base + '/' + encodeURI('WIKI/基础数据排名/' + meta.file)
77
+ const buf = await fetchTextRaw(url, config.timeout || 15000)
78
+ return parseCSV(buf.toString('utf8'))
79
+ }
80
+
81
+ function resolveColumn(data, q) {
82
+ const s = String(q || '').trim()
83
+ if (!s) return null
84
+ const exact = data.columns.find(c => c.name === s)
85
+ if (exact) return exact
86
+ const matches = data.columns.filter(c => c.name.includes(s) || s.includes(c.name))
87
+ return matches.length === 1 ? matches[0] : null
88
+ }
89
+
90
+ function findColumnCandidates(data, q) {
91
+ const s = String(q || '').trim()
92
+ const names = data.columns.map(c => c.name)
93
+ if (!s) return names
94
+ const hit = names.filter(n => n.includes(s))
95
+ return hit.length ? hit : names.filter(n => s.includes(n))
96
+ }
97
+
98
+ function resolveChar(data, q) {
99
+ const s = String(q || '').trim()
100
+ if (!s) return null
101
+ const raw = data.rows.find(r => r.name === s)
102
+ if (raw) return raw
103
+ const exact = data.rows.filter(r => baseName(r.name) === s)
104
+ if (exact.length) {
105
+ const plain = exact.find(r => baseName(r.name) === r.name.replace(/["“”]/g, ''))
106
+ return plain || exact[0]
107
+ }
108
+ const subs = data.rows.filter(r => r.name.includes(s) || s.includes(r.name))
109
+ return subs.length === 1 ? subs[0] : null
110
+ }
111
+
112
+ function findCharCandidates(data, q) {
113
+ const s = String(q || '').trim()
114
+ const names = []
115
+ const seen = new Set()
116
+ data.rows.forEach(r => {
117
+ const b = baseName(r.name)
118
+ if (!seen.has(b)) { seen.add(b); names.push(b) }
119
+ })
120
+ if (!s) return names
121
+ const hit = names.filter(n => n.includes(s))
122
+ return (hit.length ? hit : names.filter(n => s.includes(n))).slice(0, 10)
123
+ }
124
+
125
+ function computeRanking(data, col, label) {
126
+ const numeric = [], nonNumeric = []
127
+ data.rows.forEach(row => {
128
+ const v = row.vals[col.idx]
129
+ const item = { name: row.name, value: v }
130
+ if (typeof v === 'number' && !isNaN(v)) numeric.push(item)
131
+ else nonNumeric.push(item)
132
+ })
133
+ let avg = 0
134
+ if (numeric.length) avg = numeric.reduce((sum, x) => sum + x.value, 0) / numeric.length
135
+ const dir = col.sortType === 'desc' ? -1 : 1
136
+ numeric.sort((a, b) => (a.value - b.value) * dir)
137
+ let rank = 1
138
+ for (let i = 0; i < numeric.length;) {
139
+ let j = i
140
+ while (j < numeric.length && numeric[j].value === numeric[i].value) j++
141
+ for (let k = i; k < j; k++) numeric[k].rank = rank
142
+ rank += j - i
143
+ i = j
144
+ }
145
+ const rows = numeric.concat(nonNumeric.map(x => ({ ...x, rank: null })))
146
+ rows.forEach(x => {
147
+ if (typeof x.value === 'number' && !isNaN(x.value)) {
148
+ x.deviation = avg === 0
149
+ ? '-'
150
+ : (() => {
151
+ const d = ((x.value - avg) / Math.abs(avg)) * 100
152
+ return (d >= 0 ? '+' : '') + d.toFixed(1) + '%'
153
+ })()
154
+ } else {
155
+ x.deviation = null
156
+ }
157
+ })
158
+ return { col, avg, total: data.rows.length, rows, campLabel: label || '' }
159
+ }
160
+
161
+ function computeCharacter(data, row) {
162
+ const items = data.columns.map(col => {
163
+ const value = row.vals[col.idx]
164
+ const num = typeof value === 'number' && !isNaN(value)
165
+ const item = { col, value, num }
166
+ if (num) {
167
+ const r = computeRanking(data, col, '')
168
+ const entry = r.rows.find(x => x.name === row.name)
169
+ if (entry) {
170
+ item.rank = entry.rank
171
+ item.total = r.rows.filter(x => x.rank != null).length
172
+ } else {
173
+ item.rank = null
174
+ item.total = null
175
+ }
176
+ }
177
+ return item
178
+ })
179
+ return { row, items, numCount: items.filter(i => i.num).length }
180
+ }
181
+
182
+ function renderName(name) {
183
+ const m = String(name).match(/^(.*?)[((](.+?)[))]$/)
184
+ if (m) {
185
+ return esc(m[1]) + '<span class="morph">(' + esc(m[2]) + ')</span>'
186
+ }
187
+ return esc(name)
188
+ }
189
+
190
+ function buildRankingHtml(r, config, highlight) {
191
+ const width = config.width || 1300
192
+ const half = Math.ceil(r.rows.length / 2)
193
+ const left = r.rows.slice(0, half)
194
+ const right = r.rows.slice(half)
195
+
196
+ const hlName = highlight ? String(highlight).trim() : ''
197
+ const hlBase = hlName ? baseName(hlName) : ''
198
+ const hlRow = hlName ? r.rows.find(x => baseName(x.name) === hlBase || x.name === hlName) : null
199
+
200
+ const rowHtml = (rows) => rows.map(row => {
201
+ const num = typeof row.value === 'number' && !isNaN(row.value)
202
+ const isHl = hlBase && baseName(row.name) === hlBase
203
+ return '<tr' + (isHl ? ' class="hl"' : '') + '>' +
204
+ '<td class="c-rank">' + (row.rank != null ? row.rank : '-') + '</td>' +
205
+ '<td class="c-name">' + renderName(row.name) + (isHl ? '<span class="hl-tag">你查询的角色</span>' : '') + '</td>' +
206
+ '<td class="c-val">' + (num ? row.value.toFixed(2) : '<span class="na">' + esc(row.value || '-') + '</span>') + '</td>' +
207
+ '<td class="c-dev">' + (row.deviation != null ? row.deviation : '-') + '</td>' +
208
+ '</tr>'
209
+ }).join('')
210
+
211
+ const noteBar = hlRow
212
+ ? '<div class="hl-note">已高亮:' + renderName(hlRow.name) + ' · 该要素第 ' + (hlRow.rank != null ? hlRow.rank : '-') + ' 名' + (typeof hlRow.value === 'number' ? '(' + hlRow.value.toFixed(2) + ')' : '') + '</div>'
213
+ : ''
214
+
215
+ const tableHtml = (rows, title) =>
216
+ '<div class="table-col">' +
217
+ (title ? '<div class="table-title">' + title + '</div>' : '') +
218
+ '<table><thead><tr><th class="c-rank">排名</th><th class="c-name">角色名</th><th class="c-val">' + esc(r.col.name) + '</th><th class="c-dev">距平</th></tr></thead><tbody>' +
219
+ rowHtml(rows) +
220
+ '</tbody></table></div>'
221
+
222
+ return `<!DOCTYPE html>
223
+ <html lang="zh-CN">
224
+ <head>
225
+ <meta charset="UTF-8">
226
+ <style>
227
+ * { margin:0; padding:0; box-sizing:border-box; }
228
+ body { background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
229
+ .page { width:100%; margin:0 auto; }
230
+ .title-bar { display:flex; justify-content:center; align-items:center; gap:8px; flex-wrap:wrap; padding:6px 18px; background:rgba(22,27,34,.9); border:1px solid #30363d; border-radius:20px; font-size:20px; margin-bottom:14px; }
231
+ .title-bar .sep { color:#30363d; }
232
+ .title-bar .metric-tag { color:#58a6ff; font-weight:600; }
233
+ .card { background:#161b22; border:1px solid #30363d; border-radius:18px; padding:20px; }
234
+ .hl-note { text-align:center; font-size:14px; color:#ffd76d; font-weight:600; margin:0 0 14px; }
235
+ .tables { display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap; }
236
+ .table-col { flex:1; min-width:320px; }
237
+ .table-title { text-align:center; font-size:14px; color:#d2991d; font-weight:700; margin-bottom:8px; letter-spacing:1px; }
238
+ table { width:100%; border-collapse:collapse; }
239
+ thead th { background:#21262d; color:#8b949e; font-size:12px; font-weight:600; padding:8px 10px; text-align:center; border:1px solid #30363d; letter-spacing:.05em; }
240
+ td { padding:7px 10px; font-size:13px; border:1px solid #21262d; text-align:center; }
241
+ tr:nth-child(even) td { background:rgba(255,255,255,.02); }
242
+ tr.hl td { background:rgba(255,211,109,.10) !important; }
243
+ tr.hl td.c-rank { color:#ffd76d; }
244
+ tr.hl td.c-name { color:#ffd76d; font-weight:700; }
245
+ tr.hl td.c-val { color:#ffd76d; }
246
+ .c-rank { color:#f85149; font-weight:700; font-family:'SF Mono',Consolas,monospace; width:48px; }
247
+ .c-name { text-align:left; font-weight:600; color:#e6edf3; white-space:nowrap; }
248
+ .c-name .morph { color:#8ab4f8; font-size:.85em; font-weight:400; margin-left:.2em; }
249
+ .hl-tag { display:inline-block; margin-left:6px; padding:1px 6px; font-size:10px; color:#0d1117; background:#ffd76d; border-radius:4px; font-weight:700; vertical-align:middle; }
250
+ .c-val { font-family:'SF Mono',Consolas,monospace; color:#79c0ff; }
251
+ .c-dev { font-family:'SF Mono',Consolas,monospace; color:#8b949e; }
252
+ .c-dev:not(:empty) { color:#3fb950; }
253
+ .na { color:#484f58; font-style:italic; }
254
+ .foot { text-align:center; color:#8b949e; font-size:12px; margin-top:14px; opacity:.8; }
255
+ </style>
256
+ </head>
257
+ <body>
258
+ <div class="page">
259
+ <div class="title-bar">
260
+ <span>${esc(r.campLabel)}</span><span class="sep">·</span>
261
+ <span>基础数据</span><span class="sep">·</span>
262
+ <span>${esc(r.col.name)}</span><span class="sep">·</span>
263
+ <span class="metric-tag">${r.col.sortType === 'desc' ? '越大越优' : '越小越优'} · 平均 ${r.avg.toFixed(2)} · ${r.total} 位</span>
264
+ </div>
265
+ <div class="card">
266
+ ${noteBar}
267
+ <div class="tables">
268
+ ${tableHtml(left, left.length ? '排名 1-' + left.length : '')}
269
+ ${tableHtml(right, right.length ? '排名 ' + (half + 1) + '-' + r.rows.length : '')}
270
+ </div>
271
+ </div>
272
+ <div class="foot">基础数据排名 · 数据根据 BWIKI 基础数据计算 · 无数据不计入排名</div>
273
+ </div>
274
+ </body>
275
+ </html>`
276
+ }
277
+
278
+ function buildCharacterHtml(d, config) {
279
+ const width = config.width || 1300
280
+ const { row, items } = d
281
+ const charName = row.name.replace(/["“”]/g, '')
282
+ const nameHtml = renderName(charName)
283
+
284
+ const cellHtml = items.map(it => {
285
+ const up = it.col.sortType === 'desc'
286
+ const rankText = it.num && it.rank != null ? '#' + it.rank + ' / ' + it.total : '--'
287
+ return '<div class="cell">' +
288
+ '<div class="cell-top"><span class="cell-label">' + esc(it.col.name) + '</span><span class="cell-dir ' + (up ? 'up' : 'down') + '">' + (up ? '↑ 越大越优' : '↓ 越小越优') + '</span></div>' +
289
+ '<div class="cell-val">' + (it.num ? it.value.toFixed(2) : '<span class="na">' + esc(it.value || '无数据') + '</span>') + '</div>' +
290
+ '<div class="cell-rank">' + rankText + '</div>' +
291
+ '</div>'
292
+ }).join('')
293
+
294
+ return `<!DOCTYPE html>
295
+ <html lang="zh-CN">
296
+ <head>
297
+ <meta charset="UTF-8">
298
+ <style>
299
+ * { margin:0; padding:0; box-sizing:border-box; }
300
+ body { background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
301
+ .page { width:100%; margin:0 auto; }
302
+ .title-bar { display:flex; justify-content:center; align-items:center; gap:8px; flex-wrap:wrap; padding:6px 18px; background:rgba(22,27,34,.9); border:1px solid #30363d; border-radius:20px; font-size:20px; margin-bottom:14px; }
303
+ .title-bar .sep { color:#30363d; }
304
+ .title-bar .metric-tag { color:#58a6ff; font-weight:600; }
305
+ .card { background:#161b22; border:1px solid #30363d; border-radius:18px; padding:22px 24px; }
306
+ .hero { display:flex; flex-direction:column; align-items:center; gap:8px; text-align:center; margin-bottom:16px; }
307
+ .char-avatar { width:84px; height:84px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:38px; font-weight:700; color:#e6edf3; background:linear-gradient(135deg,#1c2333,#22304a); border:2px solid rgba(88,166,255,.35); box-shadow:0 0 30px rgba(88,166,255,.15); }
308
+ .char-name { font-size:30px; font-weight:800; letter-spacing:1px; color:#e6edf3; }
309
+ .char-name .morph { color:#8ab4f8; font-size:.75em; font-weight:500; margin-left:.25em; }
310
+ .char-sub { font-size:13px; color:#8b949e; }
311
+ .note { display:flex; align-items:center; gap:8px; font-size:13px; line-height:1.6; color:#d2991d; background:rgba(210,153,29,.08); border:1px solid rgba(210,153,29,.25); border-radius:10px; padding:10px 14px; margin-bottom:16px; }
312
+ .note b { color:#ffd76d; }
313
+ .grid { display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:10px; }
314
+ .cell { background:rgba(255,255,255,.03); border:1px solid #21262d; border-radius:12px; padding:10px 14px; }
315
+ .cell-top { display:flex; justify-content:space-between; align-items:center; gap:8px; margin-bottom:6px; }
316
+ .cell-label { font-size:13px; color:#8b949e; font-weight:600; }
317
+ .cell-dir { font-size:10px; font-weight:700; padding:1px 6px; border-radius:4px; flex-shrink:0; }
318
+ .cell-dir.up { color:#79c0ff; background:rgba(88,166,255,.12); }
319
+ .cell-dir.down { color:#ffa657; background:rgba(255,166,87,.12); }
320
+ .cell-val { font-size:22px; font-weight:800; font-family:'SF Mono',Consolas,monospace; color:#e6edf3; }
321
+ .cell-val .na { font-size:15px; color:#484f58; font-weight:400; font-style:italic; }
322
+ .cell-rank { font-size:13px; color:#58a6ff; font-family:'SF Mono',Consolas,monospace; margin-top:4px; }
323
+ .foot { text-align:center; color:#8b949e; font-size:12px; margin-top:14px; opacity:.8; }
324
+ </style>
325
+ </head>
326
+ <body>
327
+ <div class="page">
328
+ <div class="title-bar">
329
+ <span>基础数据</span><span class="sep">·</span>
330
+ <span>${esc(campLabel('hunter'))}</span><span class="sep">·</span>
331
+ <span class="metric-tag">全部项目 · 阵营内排名</span>
332
+ </div>
333
+ <div class="card">
334
+ <div class="hero">
335
+ <div class="char-avatar">${esc(charName.charAt(0))}</div>
336
+ <div class="char-name">${nameHtml}</div>
337
+ <div class="char-sub">${d.items.length} 项数据 · ${d.numCount} 项有数值 · 其余无数据/未公布</div>
338
+ </div>
339
+ <div class="note">
340
+ <b>排名规则:</b>距离类数据(普攻距离、蓄力距离、气球刀距离)数值<b>越大</b>排名越高;其他数据数值<b>越小</b>排名越高。数值相同排名并列。
341
+ </div>
342
+ <div class="grid">${cellHtml}</div>
343
+ </div>
344
+ <div class="foot">数据根据 BWIKI 基础数据计算</div>
345
+ </div>
346
+ </body>
347
+ </html>`
348
+ }
349
+
350
+ function buildTabsHtml(camps, config) {
351
+ const width = config.width || 1300
352
+ const sections = camps.map(camp => {
353
+ const names = camp.columns && camp.columns.length ? camp.columns : FALLBACK_COLUMNS[camp.key]
354
+ const chips = names.map(n => '<span class="chip">' + esc(n) + '</span>').join('')
355
+ return '<div class="camp-block"><div class="camp-label">' + esc(camp.label) + '(' + names.length + ')</div><div class="chips">' + chips + '</div></div>'
356
+ }).join('')
357
+ return `<!DOCTYPE html>
358
+ <html lang="zh-CN">
359
+ <head>
360
+ <meta charset="UTF-8">
361
+ <style>
362
+ * { margin:0; padding:0; box-sizing:border-box; }
363
+ body { background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
364
+ .page { width:100%; margin:0 auto; }
365
+ .hero { display:flex; flex-direction:column; align-items:center; gap:8px; text-align:center; margin-bottom:14px; }
366
+ .hero-title { font-size:28px; font-weight:800; letter-spacing:1px; background:linear-gradient(90deg,#79c0ff,#ffd76d); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
367
+ .hero-sub { font-size:13px; color:#8b949e; }
368
+ .card { background:#161b22; border:1px solid #30363d; border-radius:18px; padding:20px 22px; }
369
+ .camp-block { margin-bottom:16px; }
370
+ .camp-block:last-child { margin-bottom:0; }
371
+ .camp-label { font-size:15px; font-weight:700; color:#ffd76d; margin-bottom:8px; display:flex; align-items:center; gap:8px; }
372
+ .camp-label::before { content:''; width:4px; height:15px; border-radius:2px; background:linear-gradient(#ffd76d,#ff8c00); }
373
+ .chips { display:flex; flex-wrap:wrap; gap:7px; }
374
+ .chip { padding:5px 12px; font-size:13px; color:#c9d1d9; background:rgba(88,166,255,.08); border:1px solid rgba(88,166,255,.22); border-radius:999px; }
375
+ .chip:nth-child(4n+1) { background:rgba(210,153,29,.08); border-color:rgba(210,153,29,.3); }
376
+ .foot { text-align:center; color:#8b949e; font-size:12px; margin-top:14px; opacity:.8; }
377
+ </style>
378
+ </head>
379
+ <body>
380
+ <div class="page">
381
+ <div class="hero">
382
+ <div class="hero-title">基础数据排名</div>
383
+ <div class="hero-sub">「基础数据 &lt;项目&gt;」查排名 · 「基础数据 &lt;项目&gt; &lt;角色&gt;」高亮该角色 · 「基础数据 &lt;角色&gt;」查看全部项目</div>
384
+ </div>
385
+ <div class="card">${sections}</div>
386
+ <div class="foot">项目为上方标签名 · 数据根据 BWIKI 基础数据计算</div>
387
+ </div>
388
+ </body>
389
+ </html>`
390
+ }
391
+
392
+ module.exports = {
393
+ CAMP_META,
394
+ campLabel,
395
+ baseName,
396
+ loadCamp,
397
+ parseCSV,
398
+ resolveColumn,
399
+ resolveChar,
400
+ findColumnCandidates,
401
+ findCharCandidates,
402
+ computeRanking,
403
+ computeCharacter,
404
+ buildRankingHtml,
405
+ buildCharacterHtml,
406
+ buildTabsHtml,
407
+ }
package/cangtou.js ADDED
@@ -0,0 +1,166 @@
1
+ 'use strict'
2
+
3
+ const CACHE_TTL = 60 * 60 * 1000
4
+ const FILES = [
5
+ 'char_mapping.json',
6
+ 'first_letter_char_mapping.json',
7
+ 'full_pinyin_char_mapping.json',
8
+ 'char_first_letter_mapping.json',
9
+ 'char_full_pinyin_mapping.json',
10
+ ]
11
+
12
+ let dataCache = null
13
+ let cacheTs = 0
14
+
15
+ async function fetchText(url, timeout) {
16
+ const ctrl = new AbortController()
17
+ const timer = setTimeout(() => ctrl.abort(), timeout)
18
+ try {
19
+ const res = await fetch(url, { signal: ctrl.signal })
20
+ if (!res.ok) throw new Error('HTTP ' + res.status)
21
+ return await res.text()
22
+ } finally {
23
+ clearTimeout(timer)
24
+ }
25
+ }
26
+
27
+ async function loadData(config) {
28
+ if (dataCache && Date.now() - cacheTs < CACHE_TTL) return dataCache
29
+ const base = (config.dataSource || 'https://website.homeworkkun.top').replace(/\/+$/, '')
30
+ const timeout = config.timeout || 15000
31
+ const results = await Promise.all(FILES.map(async f => {
32
+ const url = base + '/' + encodeURI('WIKI/图片获取/cangtou/' + f)
33
+ return JSON.parse(await fetchText(url, timeout))
34
+ }))
35
+ dataCache = {
36
+ charMapping: results[0],
37
+ firstLetterCharMapping: results[1],
38
+ fullPinyinCharMapping: results[2],
39
+ charFirstLetterMapping: results[3],
40
+ charFullPinyinMapping: results[4],
41
+ }
42
+ cacheTs = Date.now()
43
+ return dataCache
44
+ }
45
+
46
+ function pickItems(d, chars, typeFilter) {
47
+ const seen = new Set()
48
+ const all = []
49
+ for (const c of chars) {
50
+ for (const it of (d.charMapping[c] || [])) {
51
+ if (typeFilter !== '所有' && it.type !== typeFilter) continue
52
+ if (seen.has(it.item_full_title)) continue
53
+ seen.add(it.item_full_title)
54
+ all.push(it)
55
+ }
56
+ }
57
+ return all
58
+ }
59
+
60
+ function buildSegments(d, text, mode) {
61
+ const segs = []
62
+ for (const ch of String(text || '')) {
63
+ if (/[\u4e00-\u9fff]/.test(ch)) {
64
+ if (mode === '谐音') {
65
+ const py = d.charFullPinyinMapping[ch] || ''
66
+ segs.push({ input: ch, tag: py, chars: py ? (d.fullPinyinCharMapping[py] || []) : [] })
67
+ } else {
68
+ const initial = d.charFirstLetterMapping[ch] || ''
69
+ segs.push({ input: ch, tag: initial, chars: initial ? (d.firstLetterCharMapping[initial] || []) : [] })
70
+ }
71
+ } else if (/[a-z]/i.test(ch) && mode !== '谐音') {
72
+ const l = ch.toLowerCase()
73
+ segs.push({ input: ch, tag: l, chars: d.firstLetterCharMapping[l] || [] })
74
+ }
75
+ }
76
+ return segs
77
+ }
78
+
79
+ function resolve(d, text, mode, typeFilter) {
80
+ return buildSegments(d, text, mode).slice(0, 50).map(seg => {
81
+ const items = pickItems(d, seg.chars, typeFilter)
82
+ return Object.assign({}, seg, { item: items.length ? items[Math.floor(Math.random() * items.length)] : null })
83
+ })
84
+ }
85
+
86
+ function resolveAll(d, text, mode, typeFilter) {
87
+ return buildSegments(d, text, mode).slice(0, 10).map(seg => {
88
+ return Object.assign({}, seg, { items: pickItems(d, seg.chars, typeFilter) })
89
+ })
90
+ }
91
+
92
+ function itemText(item) {
93
+ return item.item_display_title + '(' + item.role + '·' + item.quality + '·' + item.type + ')'
94
+ }
95
+
96
+ function buildText(result, text, mode) {
97
+ const lines = []
98
+ if (!result.length) return '未识别到可用的汉字或字母'
99
+ lines.push('【藏头·' + mode + '】' + String(text || '').trim())
100
+ result.forEach(seg => {
101
+ const line = mode === '谐音'
102
+ ? seg.input + '(' + seg.tag + ')'
103
+ : seg.tag.toUpperCase() + ' ' + seg.input
104
+ lines.push(line + ' → ' + (seg.item ? itemText(seg.item) : '无匹配'))
105
+ })
106
+ return lines.join('\n')
107
+ }
108
+
109
+ function esc(s) {
110
+ return String(s == null ? '' : s)
111
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
112
+ .replace(/"/g, '&quot;').replace(/'/g, '&#39;')
113
+ }
114
+
115
+ function buildFullHtml(result, text, mode, config) {
116
+ const width = config.width || 1300
117
+ const blocks = result.map(seg => {
118
+ const head = mode === '谐音'
119
+ ? esc(seg.input) + '<span class="pinyin">(' + esc(seg.tag) + ')</span>'
120
+ : '<span class="initial">' + esc(seg.tag.toUpperCase()) + '</span> ' + esc(seg.input)
121
+ const body = seg.items && seg.items.length
122
+ ? seg.items.map(it => '<span class="chip">' + esc(it.item_display_title) + '<span class="meta">' + esc(it.role + ' · ' + it.quality + ' · ' + it.type) + '</span></span>').join('')
123
+ : '<span class="none">无匹配</span>'
124
+ return '<div class="block"><div class="b-head">' + head + '<span class="cnt">' + (seg.items ? seg.items.length : 0) + ' 个</span></div><div class="chips">' + body + '</div></div>'
125
+ }).join('')
126
+
127
+ return `<!DOCTYPE html>
128
+ <html lang="zh-CN">
129
+ <head>
130
+ <meta charset="UTF-8">
131
+ <style>
132
+ * { margin:0; padding:0; box-sizing:border-box; }
133
+ body { background:#0d1117; color:#c9d1d9; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',sans-serif; padding:16px; }
134
+ .page { width:100%; margin:0 auto; }
135
+ .title-bar { display:flex; justify-content:center; align-items:center; gap:10px; flex-wrap:wrap; padding:8px 20px; background:rgba(22,27,34,.9); border:1px solid #30363d; border-radius:20px; font-size:30px; margin-bottom:14px; }
136
+ .title-bar .sep { color:#30363d; }
137
+ .card { background:#161b22; border:1px solid #30363d; border-radius:18px; padding:20px 24px; }
138
+ .block { margin-bottom:18px; }
139
+ .block:last-child { margin-bottom:0; }
140
+ .b-head { font-size:26px; font-weight:800; color:#ffd76d; margin-bottom:10px; display:flex; align-items:center; gap:12px; }
141
+ .b-head .initial { color:#79c0ff; font-family:'SF Mono',Consolas,monospace; }
142
+ .b-head .pinyin { font-size:18px; color:#8b949e; font-weight:400; font-family:'SF Mono',Consolas,monospace; margin-left:4px; }
143
+ .b-head .cnt { font-size:16px; font-weight:400; color:#8b949e; margin-left:auto; }
144
+ .chips { display:flex; flex-wrap:wrap; gap:8px; }
145
+ .chip { display:inline-flex; align-items:baseline; gap:6px; padding:5px 14px; font-size:20px; font-weight:600; color:#e6edf3; background:rgba(88,166,255,.07); border:1px solid rgba(88,166,255,.22); border-radius:999px; }
146
+ .chip .meta { font-size:14px; font-weight:400; color:#8b949e; }
147
+ .chip:nth-child(3n+1) { background:rgba(210,153,29,.07); border-color:rgba(210,153,29,.28); }
148
+ .none { font-size:20px; color:#8b949e; }
149
+ .foot { text-align:center; color:#8b949e; font-size:12px; margin-top:14px; opacity:.8; }
150
+ </style>
151
+ </head>
152
+ <body>
153
+ <div class="page">
154
+ <div class="title-bar">
155
+ <span>藏头完整</span><span class="sep">·</span>
156
+ <span>${esc(mode)}</span><span class="sep">·</span>
157
+ <span>${esc(String(text || '').trim())}</span>
158
+ </div>
159
+ <div class="card">${blocks || '<div class="none">未识别到可用的汉字或字母</div>'}</div>
160
+ <div class="foot">数据根据 BWIKI 时装/随身物品池计算</div>
161
+ </div>
162
+ </body>
163
+ </html>`
164
+ }
165
+
166
+ module.exports = { loadData, resolve, resolveAll, buildText, buildFullHtml }