koishi-plugin-wcl-lili 0.9.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.
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{openBlock as s,createElementBlock as o,createCommentVNode as d,createStaticVNode as c}from"vue";const l=(t,a)=>{const f=t.__vccOpts||t;for(const[v,e]of a)f[v]=e;return f},n={},r={class:"wcl-page"};function i(t,a){return s(),o("div",r,[d(" Hero "),a[0]||(a[0]=c('<div class="hero-section" data-v-f4ac6001><div class="hero-bg" data-v-f4ac6001></div><div class="hero-content" data-v-f4ac6001><h1 class="hero-title" data-v-f4ac6001><span class="hero-icon" data-v-f4ac6001>🎮</span> Warcraft Logs 战绩查询 </h1><p class="hero-subtitle" data-v-f4ac6001>WCL v2 GraphQL + Raider.io · 团本战绩 · 大秘境 · 分数线 · 角色信息</p></div></div>',1)),d(" 命令速查 "),a[1]||(a[1]=c('<section class="info-section" data-v-f4ac6001><h2 class="section-title" data-v-f4ac6001>⌨️ 命令速查</h2><table class="cmd-table" data-v-f4ac6001><thead data-v-f4ac6001><tr data-v-f4ac6001><th data-v-f4ac6001>命令</th><th data-v-f4ac6001>说明</th><th data-v-f4ac6001>示例</th></tr></thead><tbody data-v-f4ac6001><tr data-v-f4ac6001><td data-v-f4ac6001><code data-v-f4ac6001>wcl &lt;角色-服务器&gt; [ID] [难度]</code></td><td data-v-f4ac6001>查询团本战绩</td><td data-v-f4ac6001><code data-v-f4ac6001>wcl 蓝耀-shadowmoon 38 m</code></td></tr><tr data-v-f4ac6001><td data-v-f4ac6001><code data-v-f4ac6001>wcl tw &lt;角色-服务器&gt;</code></td><td data-v-f4ac6001>指定台服查询</td><td data-v-f4ac6001><code data-v-f4ac6001>wcl tw Asuna-azuremyst</code></td></tr><tr data-v-f4ac6001><td data-v-f4ac6001><code data-v-f4ac6001>wcl.角色 &lt;角色-服务器&gt;</code></td><td data-v-f4ac6001>查询角色信息</td><td data-v-f4ac6001><code data-v-f4ac6001>wcl.角色 蓝耀-shadowmoon</code></td></tr><tr data-v-f4ac6001><td data-v-f4ac6001><code data-v-f4ac6001>wcl.副本 &lt;角色-服务器&gt; [副本名/ID] [难度]</code></td><td data-v-f4ac6001>查询指定副本战绩(副本名支持中文如「孢陨幽境」或英文如「Sporefall」,也可用数字 ID)</td><td data-v-f4ac6001><code data-v-f4ac6001>wcl.副本 蓝耀-shadowmoon 孢陨幽境 h</code></td></tr><tr data-v-f4ac6001><td data-v-f4ac6001><code data-v-f4ac6001>wcl.大秘境 &lt;角色-服务器&gt;</code></td><td data-v-f4ac6001>大秘境评分 + 真实DPS</td><td data-v-f4ac6001><code data-v-f4ac6001>wcl.大秘境 蓝耀-shadowmoon</code></td></tr><tr data-v-f4ac6001><td data-v-f4ac6001><code data-v-f4ac6001>wcl.分数线 [区域]</code></td><td data-v-f4ac6001>大秘境前0.1%/1%分数线</td><td data-v-f4ac6001><code data-v-f4ac6001>wcl.分数线</code> / <code data-v-f4ac6001>wcl.分数线 us</code></td></tr><tr data-v-f4ac6001><td data-v-f4ac6001><code data-v-f4ac6001>wcl.额度</code></td><td data-v-f4ac6001>查询 API 配额使用</td><td data-v-f4ac6001><code data-v-f4ac6001>wcl.额度</code></td></tr><tr data-v-f4ac6001><td data-v-f4ac6001><code data-v-f4ac6001>wcl.副本列表</code></td><td data-v-f4ac6001>列出可用副本</td><td data-v-f4ac6001><code data-v-f4ac6001>wcl.副本列表</code></td></tr></tbody></table><p class="tip-text" data-v-f4ac6001>格式说明:角色名和服务器名用 <code data-v-f4ac6001>-</code> 连接,如 <code data-v-f4ac6001>蓝耀-shadowmoon</code>。默认区域为配置中的 region(通常 cn),查询台服在前面加 <code data-v-f4ac6001>tw</code>。难度可跟在命令后:<code data-v-f4ac6001>m</code>=史诗 <code data-v-f4ac6001>h</code>=英雄 <code data-v-f4ac6001>n</code>=普通 <code data-v-f4ac6001>lfr</code>=随机,不填默认史诗。副本也可用中文/英文名代替数字 ID,如 <code data-v-f4ac6001>wcl.副本 蓝耀-shadowmoon 孢陨幽境 h</code>。</p></section>',1)),d(" 副本列表 "),a[2]||(a[2]=c('<section class="info-section" data-v-f4ac6001><h2 class="section-title" data-v-f4ac6001>🏰 副本 Zone ID</h2><table class="cmd-table" data-v-f4ac6001><thead data-v-f4ac6001><tr data-v-f4ac6001><th data-v-f4ac6001>Zone ID</th><th data-v-f4ac6001>副本名称</th><th data-v-f4ac6001>资料片</th><th data-v-f4ac6001>类型</th></tr></thead><tbody data-v-f4ac6001><tr data-v-f4ac6001><td data-v-f4ac6001>50</td><td data-v-f4ac6001>孢陨幽境 (Sporefall)</td><td data-v-f4ac6001>Midnight</td><td data-v-f4ac6001>团本</td></tr><tr data-v-f4ac6001><td data-v-f4ac6001>46</td><td data-v-f4ac6001>虚影尖塔 / 梦境裂隙 / 进军奎尔丹纳斯</td><td data-v-f4ac6001>Midnight</td><td data-v-f4ac6001>团本</td></tr><tr data-v-f4ac6001><td data-v-f4ac6001>44</td><td data-v-f4ac6001>法力锻造欧米茄 (Manaforge Omega)</td><td data-v-f4ac6001>Midnight</td><td data-v-f4ac6001>团本</td></tr><tr data-v-f4ac6001><td data-v-f4ac6001>47</td><td data-v-f4ac6001>大秘境 S1</td><td data-v-f4ac6001>Midnight</td><td data-v-f4ac6001>大秘境</td></tr><tr data-v-f4ac6001><td data-v-f4ac6001>42</td><td data-v-f4ac6001>解放安德麦 (Liberation of Undermine)</td><td data-v-f4ac6001>地心之战</td><td data-v-f4ac6001>团本</td></tr><tr data-v-f4ac6001><td data-v-f4ac6001>38</td><td data-v-f4ac6001>尼鲁巴尔宫 (Nerub-ar Palace)</td><td data-v-f4ac6001>地心之战</td><td data-v-f4ac6001>团本</td></tr><tr data-v-f4ac6001><td data-v-f4ac6001>35</td><td data-v-f4ac6001>阿梅达希尔 (Amirdrassil)</td><td data-v-f4ac6001>巨龙时代</td><td data-v-f4ac6001>团本</td></tr></tbody></table></section>',1)),d(" 难度说明 "),a[3]||(a[3]=c('<section class="info-section" data-v-f4ac6001><h2 class="section-title" data-v-f4ac6001>⚔️ 难度与指标</h2><div class="dual-col" data-v-f4ac6001><div data-v-f4ac6001><h3 data-v-f4ac6001>难度 (difficulty)</h3><ul class="kv-list" data-v-f4ac6001><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>1</span> 随机团本 (LFR)</li><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>2</span> 普通 (Normal)</li><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>3</span> 英雄 (Heroic)</li><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>5</span> 史诗 (Mythic)</li><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>10</span> 大秘境 (M+)</li></ul></div><div data-v-f4ac6001><h3 data-v-f4ac6001>指标 (metric)</h3><ul class="kv-list" data-v-f4ac6001><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>dps</span> 伤害输出</li><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>hps</span> 治疗输出</li><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>playerscore</span> 大秘境评分</li><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>rdps</span> 团队伤害</li><li data-v-f4ac6001><span class="kv-key" data-v-f4ac6001>bossdps</span> Boss 伤害</li></ul></div></div><p class="tip-text" data-v-f4ac6001>大秘境用 <code data-v-f4ac6001>zoneRankings(playerscore)</code> 取评分,再逐个地下城调 <code data-v-f4ac6001>encounterRankings(encounterID, metric: dps)</code> 取真实 DPS(万/亿格式)、DPS 百分位、最高钥石等级、估算排名与最快用时,末尾附专精 AllStars 积分与本服排名。团本使用配置中的指标。</p></section>',1)),d(" 百分位说明 "),a[4]||(a[4]=c('<section class="info-section" data-v-f4ac6001><h2 class="section-title" data-v-f4ac6001>📊 百分位颜色</h2><div class="perf-row" data-v-f4ac6001><span class="perf-badge perf-purple" data-v-f4ac6001>🟣 99%+</span><span class="perf-badge perf-orange" data-v-f4ac6001>🟠 95-99%</span><span class="perf-badge perf-blue" data-v-f4ac6001>🔵 75-95%</span><span class="perf-badge perf-green" data-v-f4ac6001>🟢 50-75%</span><span class="perf-badge perf-white" data-v-f4ac6001>⚪ &lt;50%</span></div></section>',1))])}const p=l(n,[["render",i],["__scopeId","data-v-f4ac6001"]]),w=t=>{t.page({name:"WCL 战绩查询",path:"/wcl-lili",component:p})};export{w as default};
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .wcl-page[data-v-f4ac6001]{max-width:800px;margin:0 auto;padding:0 16px 40px}.hero-section[data-v-f4ac6001]{position:relative;border-radius:16px;overflow:hidden;margin-bottom:24px}.hero-bg[data-v-f4ac6001]{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(135deg,#1a0a2e,#16213e 40%,#0f3460,#533483)}.hero-bg[data-v-f4ac6001]:before{content:"";position:absolute;top:0;right:0;bottom:0;left:0;background:radial-gradient(ellipse at 20% 50%,rgba(168,85,247,.3) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(34,197,94,.2) 0%,transparent 50%)}.hero-content[data-v-f4ac6001]{position:relative;padding:48px 32px;text-align:center}.hero-title[data-v-f4ac6001]{font-size:28px;font-weight:800;color:#fff;margin:0 0 8px}.hero-icon[data-v-f4ac6001]{font-size:32px}.hero-subtitle[data-v-f4ac6001]{color:#fffc;font-size:14px;margin:0}.info-section[data-v-f4ac6001]{margin-bottom:24px}.section-title[data-v-f4ac6001]{font-size:16px;font-weight:700;margin:0 0 12px;padding-bottom:8px;border-bottom:2px solid var(--border-color, #e8e8e8)}.cmd-table[data-v-f4ac6001]{width:100%;border-collapse:collapse;font-size:13px}.cmd-table th[data-v-f4ac6001]{background:var(--th-bg, #f0f0f0);padding:8px 12px;text-align:left;font-weight:700;border-bottom:2px solid var(--border-color, #ddd)}.cmd-table td[data-v-f4ac6001]{padding:8px 12px;border-bottom:1px solid var(--border-color, #eee)}.cmd-table code[data-v-f4ac6001]{background:var(--code-bg, #f5f5f5);padding:2px 6px;border-radius:4px;font-size:12px}.tip-text[data-v-f4ac6001]{margin-top:12px;font-size:12px;color:var(--text-muted, #888);line-height:1.6}.tip-text code[data-v-f4ac6001]{background:var(--code-bg, #f5f5f5);padding:1px 4px;border-radius:3px;font-size:11px}.dual-col[data-v-f4ac6001]{display:flex;gap:32px}.dual-col>div[data-v-f4ac6001]{flex:1}.dual-col h3[data-v-f4ac6001]{font-size:14px;font-weight:700;margin:0 0 8px}.kv-list[data-v-f4ac6001]{list-style:none;padding:0;margin:0;font-size:13px;line-height:1.8}.kv-key[data-v-f4ac6001]{display:inline-block;min-width:60px;font-weight:700;color:#533483}.perf-row[data-v-f4ac6001]{display:flex;gap:8px;flex-wrap:wrap}.perf-badge[data-v-f4ac6001]{padding:4px 12px;border-radius:8px;font-size:13px;font-weight:600}.perf-purple[data-v-f4ac6001]{background:#f3e8ff;color:#7c3aed}.perf-orange[data-v-f4ac6001]{background:#ffedd5;color:#ea580c}.perf-blue[data-v-f4ac6001]{background:#dbeafe;color:#2563eb}.perf-green[data-v-f4ac6001]{background:#dcfce7;color:#16a34a}.perf-white[data-v-f4ac6001]{background:#f5f5f5;color:#999}@media (max-width: 640px){.dual-col[data-v-f4ac6001]{flex-direction:column;gap:16px}}.absolute{position:absolute}.relative{position:relative}.inline-block{display:inline-block}.hidden{display:none}.h3{height:.75rem}.flex{display:flex}.flex-wrap{flex-wrap:wrap}
package/lib/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "wcl-lili";
3
+ export interface Config {
4
+ clientId: string;
5
+ clientSecret: string;
6
+ region: string;
7
+ difficulty: number;
8
+ metric: string;
9
+ zoneId: number;
10
+ mplusZoneId: number;
11
+ seasonSlug: string;
12
+ }
13
+ export declare const Config: Schema<Config>;
14
+ export declare const usage = "\n# \uD83C\uDFAE Warcraft Logs \u6218\u7EE9\u67E5\u8BE2\n\n\u57FA\u4E8E WCL v2 GraphQL API + Raider.io \u00B7 \u56E2\u672C\u6218\u7EE9 / \u5927\u79D8\u5883 / \u5206\u6570\u7EBF / \u89D2\u8272\u4FE1\u606F\n\n---\n\n## \u547D\u4EE4\n\n| \u547D\u4EE4 | \u8BF4\u660E | \u793A\u4F8B |\n| --- | --- | --- |\n| `wcl <\u89D2\u8272-\u670D\u52A1\u5668> [ID] [\u96BE\u5EA6]` | \u67E5\u8BE2\u56E2\u672C\u6218\u7EE9 | `wcl \u84DD\u8000-shadowmoon 38 m` |\n| `wcl tw <\u89D2\u8272-\u670D\u52A1\u5668>` | \u6307\u5B9A\u53F0\u670D\u67E5\u8BE2 | `wcl tw Asuna-azuremyst` |\n| `wcl.\u89D2\u8272 <\u89D2\u8272-\u670D\u52A1\u5668>` | \u67E5\u8BE2\u89D2\u8272\u4FE1\u606F | `wcl.\u89D2\u8272 \u84DD\u8000-shadowmoon` |\n| `wcl.\u526F\u672C <\u89D2\u8272-\u670D\u52A1\u5668> [ID] [\u96BE\u5EA6]` | \u6307\u5B9A\u526F\u672C\u6218\u7EE9 | `wcl.\u526F\u672C \u84DD\u8000-shadowmoon 46 m` |\n| `wcl.\u5927\u79D8\u5883 <\u89D2\u8272-\u670D\u52A1\u5668>` | \u5927\u79D8\u5883\u8BC4\u5206+\u771F\u5B9EDPS | `wcl.\u5927\u79D8\u5883 \u84DD\u8000-shadowmoon` |\n| `wcl.\u5206\u6570\u7EBF [\u533A\u57DF]` | \u5927\u79D8\u5883\u5206\u6570\u7EBF | `wcl.\u5206\u6570\u7EBF` / `wcl.\u5206\u6570\u7EBF cn` |\n| `wcl.\u989D\u5EA6` | \u67E5\u8BE2 API \u914D\u989D | `wcl.\u989D\u5EA6` |\n| `wcl.\u526F\u672C\u5217\u8868` | \u5217\u51FA\u53EF\u7528\u526F\u672C | `wcl.\u526F\u672C\u5217\u8868` |\n\n## \u96BE\u5EA6\u53C2\u6570\n\n`m`=\u53F2\u8BD7 `h`=\u82F1\u96C4 `n`=\u666E\u901A `lfr`=\u968F\u673A\uFF08\u4E5F\u53EF\u76F4\u63A5\u7528\u6570\u5B57 5/3/2/1\uFF09\u3002\u4E0D\u586B\u9ED8\u8BA4\u53F2\u8BD7\u3002\n\n## \u670D\u52A1\u5668\u540D\u79F0\n\n**\u56FD\u670D\u53EF\u76F4\u63A5\u7528\u4E2D\u6587\u670D\u52A1\u5668\u540D**\uFF0C\u5982 `wcl.\u5927\u79D8\u5883 \u7231\u65E0\u804A\u7684\u5C0F\u4E03-\u52A0\u57FA\u68EE`\uFF1B\u4E5F\u53EF\u7528\u82F1\u6587 slug\u3002\n\u67E5\u8BE2\u53F0\u670D\u5728\u89D2\u8272\u524D\u52A0 `tw `\uFF0C\u5982 `wcl tw Asuna-azuremyst`\u3002\n\n## CN \u670D\u52A1\u5668 slug \u53C2\u8003\n\n| \u4E2D\u6587\u540D | slug | \u4E2D\u6587\u540D | slug |\n| --- | --- | --- | --- |\n| \u5F71\u4E4B\u54C0\u4F24 | shadowmoon | \u5BD2\u51B0\u7687\u51A0 | icecrown |\n| \u5B89\u82CF | ansu | \u9ED1\u9F99\u516C\u4E3B | onyxia |\n| \u52A0\u57FA\u68EE | gadgetzan | \u767D\u94F6\u4E4B\u624B | silver-hand |\n\n## \u526F\u672C Zone ID \u53C2\u8003\n\n| Zone ID | \u526F\u672C |\n| --- | --- |\n| 50 | \u5B62\u9668\u5E7D\u5883 (Midnight) |\n| 46 | \u865A\u5F71\u5C16\u5854 / \u68A6\u5883\u88C2\u9699 / \u8FDB\u519B\u594E\u5C14\u4E39\u7EB3\u65AF (Midnight) |\n| 44 | \u6CD5\u529B\u953B\u9020\u6B27\u7C73\u8304 (Midnight) |\n| 42 | \u89E3\u653E\u5B89\u5FB7\u9EA6 (\u5730\u5FC3\u4E4B\u6218) |\n| 38 | \u5C3C\u9C81\u5DF4\u5C14\u5BAB (\u5730\u5FC3\u4E4B\u6218) |\n| 35 | \u963F\u6885\u8FBE\u5E0C\u5C14 (\u5DE8\u9F99\u65F6\u4EE3) |\n| 47 | \u5927\u79D8\u5883 Midnight S1 |\n";
15
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,986 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ var __export = (target, all) => {
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Config: () => Config,
24
+ apply: () => apply,
25
+ name: () => name,
26
+ usage: () => usage
27
+ });
28
+ module.exports = __toCommonJS(src_exports);
29
+ var import_koishi = require("koishi");
30
+ var import_path = require("path");
31
+ var name = "wcl-lili";
32
+ var Config = import_koishi.Schema.object({
33
+ clientId: import_koishi.Schema.string().description("WCL API Client ID,在 warcraftlogs.com/api/clients 创建").required(),
34
+ clientSecret: import_koishi.Schema.string().description("WCL API Client Secret").role("secret").required(),
35
+ region: import_koishi.Schema.string().description("默认服务器区域(cn / us / eu / kr / tw)").default("cn"),
36
+ difficulty: import_koishi.Schema.number().description("默认团本难度(1=随机 2=弹性 3=普通 4=英雄 5=史诗)").default(5),
37
+ metric: import_koishi.Schema.string().description("默认团本指标(dps / hps / bossdps / rdps 等)").default("dps"),
38
+ zoneId: import_koishi.Schema.number().description("默认团本 Zone ID(50=孢陨幽境 46=虚影尖塔/梦境裂隙/进军奎尔丹纳斯 44=法力锻造欧米茄 42=解放安德麦 38=尼鲁巴尔宫)").default(50),
39
+ mplusZoneId: import_koishi.Schema.number().description("默认大秘境 Zone ID(47=Midnight S1 39=地心之战 S1 43=地心之战 S2)").default(47),
40
+ seasonSlug: import_koishi.Schema.string().description("Raider.io 大秘境赛季 slug(season-mn-1=Midnight S1)").default("season-mn-1")
41
+ });
42
+ var usage = `
43
+ # 🎮 Warcraft Logs 战绩查询
44
+
45
+ 基于 WCL v2 GraphQL API + Raider.io · 团本战绩 / 大秘境 / 分数线 / 角色信息
46
+
47
+ ---
48
+
49
+ ## 命令
50
+
51
+ | 命令 | 说明 | 示例 |
52
+ | --- | --- | --- |
53
+ | \`wcl <角色-服务器> [ID] [难度]\` | 查询团本战绩 | \`wcl 蓝耀-shadowmoon 38 m\` |
54
+ | \`wcl tw <角色-服务器>\` | 指定台服查询 | \`wcl tw Asuna-azuremyst\` |
55
+ | \`wcl.角色 <角色-服务器>\` | 查询角色信息 | \`wcl.角色 蓝耀-shadowmoon\` |
56
+ | \`wcl.副本 <角色-服务器> [ID] [难度]\` | 指定副本战绩 | \`wcl.副本 蓝耀-shadowmoon 46 m\` |
57
+ | \`wcl.大秘境 <角色-服务器>\` | 大秘境评分+真实DPS | \`wcl.大秘境 蓝耀-shadowmoon\` |
58
+ | \`wcl.分数线 [区域]\` | 大秘境分数线 | \`wcl.分数线\` / \`wcl.分数线 cn\` |
59
+ | \`wcl.额度\` | 查询 API 配额 | \`wcl.额度\` |
60
+ | \`wcl.副本列表\` | 列出可用副本 | \`wcl.副本列表\` |
61
+
62
+ ## 难度参数
63
+
64
+ \`m\`=史诗 \`h\`=英雄 \`n\`=普通 \`lfr\`=随机(也可直接用数字 5/3/2/1)。不填默认史诗。
65
+
66
+ ## 服务器名称
67
+
68
+ **国服可直接用中文服务器名**,如 \`wcl.大秘境 爱无聊的小七-加基森\`;也可用英文 slug。
69
+ 查询台服在角色前加 \`tw \`,如 \`wcl tw Asuna-azuremyst\`。
70
+
71
+ ## CN 服务器 slug 参考
72
+
73
+ | 中文名 | slug | 中文名 | slug |
74
+ | --- | --- | --- | --- |
75
+ | 影之哀伤 | shadowmoon | 寒冰皇冠 | icecrown |
76
+ | 安苏 | ansu | 黑龙公主 | onyxia |
77
+ | 加基森 | gadgetzan | 白银之手 | silver-hand |
78
+
79
+ ## 副本 Zone ID 参考
80
+
81
+ | Zone ID | 副本 |
82
+ | --- | --- |
83
+ | 50 | 孢陨幽境 (Midnight) |
84
+ | 46 | 虚影尖塔 / 梦境裂隙 / 进军奎尔丹纳斯 (Midnight) |
85
+ | 44 | 法力锻造欧米茄 (Midnight) |
86
+ | 42 | 解放安德麦 (地心之战) |
87
+ | 38 | 尼鲁巴尔宫 (地心之战) |
88
+ | 35 | 阿梅达希尔 (巨龙时代) |
89
+ | 47 | 大秘境 Midnight S1 |
90
+ `;
91
+ var zoneNameMap = {
92
+ // Midnight 资料片(至暗之夜)
93
+ "Sporefall": "孢陨幽境",
94
+ "Mythic+ Season 1": "大秘境 S1",
95
+ "Mythic+ Season 2": "大秘境 S2",
96
+ "Mythic+ Season 3": "大秘境 S3",
97
+ "VS / DR / MQD": "虚影尖塔 / 梦境裂隙 / 进军奎尔丹纳斯",
98
+ "Manaforge Omega": "法力锻造欧米茄",
99
+ // 地心之战
100
+ "Liberation of Undermine": "解放安德麦",
101
+ "Nerub-ar Palace": "尼鲁巴尔宫",
102
+ "Mythic+ Dungeons": "大秘境地下城",
103
+ // 巨龙时代
104
+ "Amirdrassil, the Dream's Hope": "阿梅达希尔",
105
+ "Aberrus, the Shadowed Crucible": "亚贝鲁斯",
106
+ "Vault of the Incarnates": "化身密室",
107
+ // 暗影国度
108
+ "Sepulcher of the First Ones": "初诞者圣墓",
109
+ "Sanctum of Domination": "统御圣所",
110
+ "Castle Nathria": "纳斯利亚堡",
111
+ // 经典副本
112
+ "Ny'alotha": "尼奥罗萨",
113
+ "The Eternal Palace": "永恒王宫",
114
+ "Crucible of Storms": "风暴熔炉",
115
+ "Battle of Dazar'alor": "达萨罗之战",
116
+ "Uldir": "奥迪尔",
117
+ "Trial of Valor": "勇气的试炼",
118
+ "Emerald Nightmare": "翡翠梦魇",
119
+ "Antorus, The Burning Throne": "安托鲁斯",
120
+ "Tomb of Sargeras": "萨格拉斯之墓",
121
+ "The Nighthold": "暗夜要塞",
122
+ "Hellfire Citadel": "地狱火堡垒",
123
+ "Blackrock Foundry": "黑石铸造厂",
124
+ "Highmaul": "悬垂堡"
125
+ };
126
+ var encounterNameMap = {
127
+ // ── 孢陨幽境 (Zone 50) ──
128
+ "Rotmire": "腐沼",
129
+ // ── 虚影尖塔 / 梦境裂隙 / 进军奎尔丹纳斯 (Zone 46) ──
130
+ "Imperator Averzian": "皇帝阿维齐安",
131
+ "Vorasius": "沃拉修斯",
132
+ "Fallen-King Salhadaar": "陨落之王萨尔哈达尔",
133
+ "Vaelgor & Ezzorak": "维尔戈与埃佐拉克",
134
+ "Lightblinded Vanguard": "盲光先锋",
135
+ "Crown of the Cosmos": "宇宙之冠",
136
+ "Chimaerus, the Undreamt God": "奇美鲁斯,未梦之神",
137
+ "Belo'ren, Child of Al'ar": "贝洛伦,阿拉尔之子",
138
+ "Midnight Falls": "至暗之夜",
139
+ // ── 法力锻造欧米茄 (Zone 44) ──
140
+ "Plexus Sentinel": "丛集哨兵",
141
+ "Loom'ithar": "织者伊萨尔",
142
+ "Soulbinder Naazindhri": "缚魂者纳辛德里",
143
+ "Forgeweaver Araz": "锻织者阿拉兹",
144
+ "The Soul Hunters": "灵魂猎手",
145
+ "Fractillus": "弗拉克提卢斯",
146
+ "Nexus-King Salhadaar": "节点之王萨尔哈达尔",
147
+ "Dimensius, the All-Devouring": "迪门修斯,万物吞噬者",
148
+ // ── 解放安德麦 (Zone 42) ──
149
+ "Vexie and the Geargrinders": "维克西与齿轮研磨者",
150
+ "Cauldron of Carnage": "屠戮大锅",
151
+ "Stix Bunkjunker": "斯蒂克斯废料机",
152
+ "Rik Reverb": "混响里科",
153
+ "Sprocketmonger Lockenstock": "齿轮贩洛克恩斯托克",
154
+ "One-Armed Bandit": "独臂强盗",
155
+ "Mug'Zee, Heads of Security": "穆格齐,安保主管",
156
+ "Chrome King Gallywix": "铬金王加里维克斯",
157
+ // ── 尼鲁巴尔宫 (Zone 38) ──
158
+ "Ulgrax the Devourer": "吞噬者乌尔格拉克斯",
159
+ "The Bloodbound Horror": "血缚恐怖",
160
+ "Sikran, Captain of the Sureki": "苏雷基队长司克兰",
161
+ "Rasha'nan": "拉夏南",
162
+ "Broodtwister Ovi'nax": "育蛛者奥维纳克斯",
163
+ "Nexus-Princess Ky'veza": "节点公主凯威扎",
164
+ "The Silken Court": "丝之庭",
165
+ "Queen Ansurek": "安苏雷卡女王",
166
+ // ── 阿梅达希尔 (Zone 35) ──
167
+ "Gnarlroot": "盘根者",
168
+ "Igira the Cruel": "残忍的伊吉拉",
169
+ "Volcoross": "沃尔科洛斯",
170
+ "Council of Dreams": "梦境议会",
171
+ "Larodar, Keeper of the Flame": "拉罗达尔,烈焰守护者",
172
+ "Nymue, Weaver of the Cycle": "妮缪,轮回织者",
173
+ "Smolderon": "斯莫尔德隆",
174
+ "Tindral Sageswift, Seer of the Flame": "智者迅风,烈焰先知",
175
+ "Fyrakk the Blazing": "炽燃菲拉克",
176
+ // ── 大秘境地下城 (Mythic+) ──
177
+ "Algeth'ar Academy": "艾杰斯亚学院",
178
+ "Magisters' Terrace": "魔导师平台",
179
+ "Maisara Caverns": "迈萨拉洞窟",
180
+ "Nexus-Point Xenas": "节点希纳斯",
181
+ "Pit of Saron": "萨隆矿坑",
182
+ "Seat of the Triumvirate": "执政团之座",
183
+ "Skyreach": "通天峰",
184
+ "Windrunner Spire": "风行者之塔"
185
+ // 如果有其他地下城名称需要翻译,请在此添加
186
+ };
187
+ var specNameMap = {
188
+ // 死亡骑士
189
+ Blood: "鲜血",
190
+ Frost: "冰霜",
191
+ Unholy: "邪恶",
192
+ // 恶魔猎手
193
+ Havoc: "浩劫",
194
+ Vengeance: "复仇",
195
+ // 德鲁伊
196
+ Balance: "平衡",
197
+ Feral: "野性",
198
+ Guardian: "守护",
199
+ Restoration: "恢复",
200
+ // 唤魔师
201
+ Devastation: "湮灭",
202
+ Preservation: "恒护",
203
+ Augmentation: "增辉",
204
+ // 猎人
205
+ BeastMastery: "野兽控制",
206
+ Marksmanship: "射击",
207
+ Survival: "生存",
208
+ // 法师
209
+ Arcane: "奥术",
210
+ Fire: "火焰",
211
+ // 武僧
212
+ Brewmaster: "酒仙",
213
+ Mistweaver: "织雾",
214
+ Windwalker: "踏风",
215
+ // 圣骑士
216
+ Holy: "神圣",
217
+ Protection: "防护",
218
+ Retribution: "惩戒",
219
+ // 牧师
220
+ Discipline: "戒律",
221
+ Shadow: "暗影",
222
+ // 潜行者
223
+ Assassination: "奇袭",
224
+ Outlaw: "狂徒",
225
+ Subtlety: "敏锐",
226
+ // 萨满
227
+ Elemental: "元素",
228
+ Enhancement: "增强",
229
+ // 术士
230
+ Affliction: "痛苦",
231
+ Demonology: "恶魔学识",
232
+ Destruction: "毁灭",
233
+ // 战士
234
+ Arms: "武器",
235
+ Fury: "狂怒"
236
+ };
237
+ function tZone(name2) {
238
+ return zoneNameMap[name2] || name2;
239
+ }
240
+ __name(tZone, "tZone");
241
+ function tEncounter(name2) {
242
+ return encounterNameMap[name2] || name2;
243
+ }
244
+ __name(tEncounter, "tEncounter");
245
+ function tSpec(name2) {
246
+ if (!name2) return "未知";
247
+ return specNameMap[name2] || name2;
248
+ }
249
+ __name(tSpec, "tSpec");
250
+ var classNameMap = {
251
+ 1: "死亡骑士",
252
+ 2: "德鲁伊",
253
+ 3: "猎人",
254
+ 4: "法师",
255
+ 5: "武僧",
256
+ 6: "圣骑士",
257
+ 7: "牧师",
258
+ 8: "盗贼",
259
+ 9: "萨满",
260
+ 10: "术士",
261
+ 11: "战士",
262
+ 12: "恶魔猎手",
263
+ 13: "唤魔师"
264
+ };
265
+ var classEmojiMap = {
266
+ 1: "💀",
267
+ 2: "🌿",
268
+ 3: "🏹",
269
+ 4: "🔮",
270
+ 5: "🐼",
271
+ 6: "✨",
272
+ 7: "📿",
273
+ 8: "🗡️",
274
+ 9: "⚡",
275
+ 10: "🔥",
276
+ 11: "⚔️",
277
+ 12: "👁️",
278
+ 13: "🐉"
279
+ };
280
+ var difficultyMap = {
281
+ 1: "随机团队",
282
+ 2: "弹性",
283
+ 3: "普通",
284
+ 4: "英雄",
285
+ 5: "史诗",
286
+ 10: "大秘境"
287
+ };
288
+ var defaultZones = [
289
+ { id: 50, name: "Sporefall", exp: "Midnight", type: "团本" },
290
+ { id: 46, name: "VS / DR / MQD", exp: "Midnight", type: "团本" },
291
+ { id: 44, name: "Manaforge Omega", exp: "Midnight", type: "团本" },
292
+ { id: 47, name: "Mythic+ Season 1", exp: "Midnight", type: "大秘境" },
293
+ { id: 42, name: "Liberation of Undermine", exp: "地心之战", type: "团本" },
294
+ { id: 38, name: "Nerub-ar Palace", exp: "地心之战", type: "团本" },
295
+ { id: 35, name: "Amirdrassil, the Dream's Hope", exp: "巨龙时代", type: "团本" }
296
+ ];
297
+ var knownRegions = ["cn", "tw", "us", "eu", "kr"];
298
+ var regionNameMap = {
299
+ cn: "国服",
300
+ us: "美服",
301
+ eu: "欧服",
302
+ tw: "台服",
303
+ kr: "韩服"
304
+ };
305
+ function parseCharInput(input, defaultRegion) {
306
+ let region = defaultRegion;
307
+ let rest = input.trim();
308
+ const parts = rest.split(/\s+/);
309
+ if (parts.length >= 2 && knownRegions.includes(parts[0].toLowerCase())) {
310
+ region = parts[0].toLowerCase();
311
+ rest = parts.slice(1).join(" ");
312
+ }
313
+ const dashIdx = rest.indexOf("-");
314
+ if (dashIdx === -1) return null;
315
+ const charName = rest.slice(0, dashIdx).trim();
316
+ const serverSlug = rest.slice(dashIdx + 1).trim();
317
+ if (!charName || !serverSlug) return null;
318
+ return { charName, serverSlug, region };
319
+ }
320
+ __name(parseCharInput, "parseCharInput");
321
+ function parseZoneAndDiff(input, defaultZoneId, defaultDifficulty) {
322
+ const tokens = input.trim().split(/\s+/);
323
+ let zoneId = null;
324
+ let difficulty = null;
325
+ let zoneName = null;
326
+ while (tokens.length > 1) {
327
+ const last = tokens[tokens.length - 1];
328
+ const d = parseDifficulty(last);
329
+ if (d !== null) {
330
+ difficulty = d;
331
+ tokens.pop();
332
+ continue;
333
+ }
334
+ if (/^\d+$/.test(last)) {
335
+ const n = parseInt(last, 10);
336
+ if (n > 0 && n < 1e3) {
337
+ zoneId = n;
338
+ tokens.pop();
339
+ continue;
340
+ }
341
+ }
342
+ zoneName = last;
343
+ tokens.pop();
344
+ break;
345
+ }
346
+ return {
347
+ charInput: tokens.join(" "),
348
+ zoneId: zoneId ?? defaultZoneId,
349
+ difficulty: difficulty ?? defaultDifficulty,
350
+ zoneName
351
+ };
352
+ }
353
+ __name(parseZoneAndDiff, "parseZoneAndDiff");
354
+ var tokenCache = null;
355
+ async function getToken(ctx, config) {
356
+ const now = Date.now();
357
+ if (tokenCache && tokenCache.expiresAt > now + 6e4) {
358
+ return tokenCache.token;
359
+ }
360
+ const auth = Buffer.from(`${config.clientId}:${config.clientSecret}`).toString("base64");
361
+ const res = await ctx.http.post("https://www.warcraftlogs.com/oauth/token", "grant_type=client_credentials", {
362
+ headers: {
363
+ "Authorization": `Basic ${auth}`,
364
+ "Content-Type": "application/x-www-form-urlencoded"
365
+ }
366
+ });
367
+ tokenCache = {
368
+ token: res.access_token,
369
+ expiresAt: now + (res.expires_in || 3600) * 1e3
370
+ };
371
+ return tokenCache.token;
372
+ }
373
+ __name(getToken, "getToken");
374
+ async function gql(ctx, config, query, variables) {
375
+ const token = await getToken(ctx, config);
376
+ const res = await ctx.http.post("https://www.warcraftlogs.com/api/v2/client", JSON.stringify({ query, variables }), {
377
+ headers: {
378
+ "Authorization": `Bearer ${token}`,
379
+ "Content-Type": "application/json"
380
+ }
381
+ });
382
+ if (res.errors) {
383
+ const msg = res.errors.map((e) => e.message).join("; ");
384
+ throw new Error(msg);
385
+ }
386
+ if (res.error) {
387
+ throw new Error(typeof res.error === "string" ? res.error : JSON.stringify(res.error));
388
+ }
389
+ return res.data;
390
+ }
391
+ __name(gql, "gql");
392
+ async function lookupCharacter(ctx, config, charName, serverSlug, region) {
393
+ const data = await gql(
394
+ ctx,
395
+ config,
396
+ "query($name: String!, $server: String!, $region: String!) { characterData { character(name: $name, serverSlug: $server, serverRegion: $region) { id name classID level faction { id name } server { name slug region { id name } } } } }",
397
+ { name: charName, server: serverSlug, region }
398
+ );
399
+ return data?.characterData?.character || null;
400
+ }
401
+ __name(lookupCharacter, "lookupCharacter");
402
+ async function getZoneRankings(ctx, config, charId, zoneId, difficulty, metric) {
403
+ const query = difficulty !== null ? "query($id: Int!, $zoneID: Int!, $difficulty: Int, $metric: CharacterPageRankingMetricType) { characterData { character(id: $id) { zoneRankings(zoneID: $zoneID, difficulty: $difficulty, metric: $metric) } } }" : "query($id: Int!, $zoneID: Int!, $metric: CharacterPageRankingMetricType) { characterData { character(id: $id) { zoneRankings(zoneID: $zoneID, metric: $metric) } } }";
404
+ const vars = { id: charId, zoneID: zoneId, metric };
405
+ if (difficulty !== null) vars.difficulty = difficulty;
406
+ const data = await gql(ctx, config, query, vars);
407
+ const raw = data?.characterData?.character?.zoneRankings;
408
+ if (!raw) return null;
409
+ try {
410
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
411
+ } catch {
412
+ return null;
413
+ }
414
+ }
415
+ __name(getZoneRankings, "getZoneRankings");
416
+ function estimateRank(e) {
417
+ if (e.rankPercent === null || e.rankPercent === void 0 || !e.rankTotalParses) return null;
418
+ return Math.max(1, Math.round(e.rankTotalParses * (1 - e.rankPercent / 100)));
419
+ }
420
+ __name(estimateRank, "estimateRank");
421
+ async function getEncounterRankings(ctx, config, charId, encounterId, metric) {
422
+ const query = "query($id: Int!, $encounterID: Int!, $metric: CharacterRankingMetricType) { characterData { character(id: $id) { encounterRankings(encounterID: $encounterID, metric: $metric) } } }";
423
+ const data = await gql(ctx, config, query, { id: charId, encounterID: encounterId, metric });
424
+ const raw = data?.characterData?.character?.encounterRankings;
425
+ if (!raw) return null;
426
+ try {
427
+ return typeof raw === "string" ? JSON.parse(raw) : raw;
428
+ } catch {
429
+ return null;
430
+ }
431
+ }
432
+ __name(getEncounterRankings, "getEncounterRankings");
433
+ async function getZone(ctx, config, zoneId) {
434
+ const data = await gql(
435
+ ctx,
436
+ config,
437
+ `{ worldData { zone(id: ${zoneId}) { id name encounters { id name } } } }`
438
+ );
439
+ return data?.worldData?.zone || null;
440
+ }
441
+ __name(getZone, "getZone");
442
+ var zoneListCache = null;
443
+ async function getZoneList(ctx, config) {
444
+ if (zoneListCache) return zoneListCache;
445
+ const data = await gql(ctx, config, "{ worldData { zones { id name } } }");
446
+ const zones = data?.worldData?.zones;
447
+ if (Array.isArray(zones)) zoneListCache = zones;
448
+ return zoneListCache || [];
449
+ }
450
+ __name(getZoneList, "getZoneList");
451
+ var zoneNameReverse = {};
452
+ for (const [en, zh] of Object.entries(zoneNameMap)) zoneNameReverse[zh] = en;
453
+ async function resolveZoneByName(ctx, config, name2) {
454
+ const cleaned = (name2 || "").trim();
455
+ if (!cleaned) return null;
456
+ const enName = zoneNameReverse[cleaned] || cleaned;
457
+ const lower = enName.toLowerCase();
458
+ const zones = await getZoneList(ctx, config);
459
+ let hit = zones.find((z) => z.name.toLowerCase() === lower);
460
+ if (hit) return hit.id;
461
+ hit = zones.find((z) => z.name.toLowerCase().includes(lower) || lower.includes(z.name.toLowerCase()));
462
+ if (hit) return hit.id;
463
+ return null;
464
+ }
465
+ __name(resolveZoneByName, "resolveZoneByName");
466
+ function knownZoneHints() {
467
+ const zones = zoneListCache || [];
468
+ const parts = [];
469
+ for (const [en, zh] of Object.entries(zoneNameMap)) {
470
+ if (/Mythic\+/.test(en)) continue;
471
+ const z = zones.find((z2) => z2.name === en);
472
+ if (z) parts.push(`${zh}(${z.id})`);
473
+ }
474
+ return parts.join(" ");
475
+ }
476
+ __name(knownZoneHints, "knownZoneHints");
477
+ async function getCutoffs(ctx, seasonSlug, region) {
478
+ const url = `https://raider.io/api/v1/mythic-plus/season-cutoffs?season=${seasonSlug}&region=${region}`;
479
+ const res = await ctx.http.get(url, {
480
+ headers: { "User-Agent": "Mozilla/5.0" }
481
+ });
482
+ return res?.cutoffs || null;
483
+ }
484
+ __name(getCutoffs, "getCutoffs");
485
+ async function getWorldCutoffs(ctx, seasonSlug) {
486
+ const regions = ["us", "eu", "cn", "tw", "kr"];
487
+ let totalPop = 0;
488
+ let p999Sum = 0;
489
+ let p990Sum = 0;
490
+ for (const region of regions) {
491
+ try {
492
+ const c = await getCutoffs(ctx, seasonSlug, region);
493
+ if (c?.p999?.all && c?.p990?.all) {
494
+ const pop = c.p999.all.totalPopulationCount;
495
+ totalPop += pop;
496
+ p999Sum += c.p999.all.quantileMinValue * pop;
497
+ p990Sum += c.p990.all.quantileMinValue * pop;
498
+ }
499
+ } catch {
500
+ }
501
+ }
502
+ if (totalPop === 0) return null;
503
+ return {
504
+ p999: p999Sum / totalPop,
505
+ p990: p990Sum / totalPop,
506
+ totalPop
507
+ };
508
+ }
509
+ __name(getWorldCutoffs, "getWorldCutoffs");
510
+ function formatCharacter(char) {
511
+ const className = classNameMap[char.classID] || "未知";
512
+ const classEmoji = classEmojiMap[char.classID] || "🎮";
513
+ const factionName = char.faction?.name || "未知";
514
+ const serverName = char.server?.name || "未知";
515
+ const regionName = char.server?.region?.name || "未知";
516
+ return [
517
+ `${classEmoji} ${char.name} — 角色信息`,
518
+ "",
519
+ `📋 名称:${char.name}`,
520
+ `⚔️ 职业:${className}`,
521
+ `📊 等级:${char.level}`,
522
+ `🛡️ 阵营:${factionName}`,
523
+ `🌐 服务器:${serverName} (${char.server?.slug})`,
524
+ `📍 区域:${regionName}`,
525
+ `🆔 WCL ID:${char.id}`
526
+ ].join("\n");
527
+ }
528
+ __name(formatCharacter, "formatCharacter");
529
+ function formatAmount(best, metric) {
530
+ if (best <= 0) return "";
531
+ if (metric === "playerscore") {
532
+ return `最佳评分 ${best.toFixed(1)}`;
533
+ }
534
+ const unit = metric.toUpperCase();
535
+ if (best >= 1e8) {
536
+ return `最佳 ${(best / 1e8).toFixed(2)}亿 ${unit}`;
537
+ }
538
+ if (best >= 1e4) {
539
+ return `最佳 ${(best / 1e4).toFixed(2)}万 ${unit}`;
540
+ }
541
+ if (best >= 1e3) {
542
+ return `最佳 ${(best / 1e3).toFixed(1)}K ${unit}`;
543
+ }
544
+ return `最佳 ${best.toFixed(1)} ${unit}`;
545
+ }
546
+ __name(formatAmount, "formatAmount");
547
+ function formatDps(dps) {
548
+ if (dps >= 1e8) return `${(dps / 1e8).toFixed(2)}亿`;
549
+ if (dps >= 1e4) return `${(dps / 1e4).toFixed(2)}万`;
550
+ if (dps >= 1e3) return `${(dps / 1e3).toFixed(1)}K`;
551
+ return dps.toFixed(0);
552
+ }
553
+ __name(formatDps, "formatDps");
554
+ function formatDuration(ms) {
555
+ if (!ms || ms <= 0) return "";
556
+ const totalSec = Math.round(ms / 1e3);
557
+ const min = Math.floor(totalSec / 60);
558
+ const sec = totalSec % 60;
559
+ return `${min}:${sec.toString().padStart(2, "0")}`;
560
+ }
561
+ __name(formatDuration, "formatDuration");
562
+ function formatZoneRankings(char, zone, rankings, metric) {
563
+ const className = classNameMap[char.classID] || "未知";
564
+ const classEmoji = classEmojiMap[char.classID] || "🎮";
565
+ const diffName = difficultyMap[rankings.difficulty] || `难度${rankings.difficulty}`;
566
+ const zoneName = zone ? tZone(zone.name) : "未知副本";
567
+ const lines = [
568
+ `${classEmoji} ${char.name} — ${zoneName} ${diffName} ${metric.toUpperCase()} 战绩`,
569
+ `职业:${className} 服务器:${char.server?.name}`,
570
+ ""
571
+ ];
572
+ const rankingList = rankings?.rankings;
573
+ if (!rankingList || rankingList.length === 0) {
574
+ lines.push("❌ 该副本下暂无战绩数据。");
575
+ return lines.join("\n");
576
+ }
577
+ const hasData = rankingList.filter((r) => r.totalKills > 0 || r.rankPercent !== null);
578
+ if (hasData.length === 0) {
579
+ lines.push(`❌ ${zoneName} 暂无战绩数据(共 ${rankingList.length} 个 Boss,均无击杀记录)。`);
580
+ return lines.join("\n");
581
+ }
582
+ for (const r of hasData) {
583
+ const percent = r.rankPercent;
584
+ const median = r.medianPercent;
585
+ const kills = r.totalKills;
586
+ const best = r.bestAmount;
587
+ let perfEmoji = "⚪";
588
+ if (percent !== null) {
589
+ if (percent >= 99) perfEmoji = "🩷";
590
+ else if (percent >= 90) perfEmoji = "🟠";
591
+ else if (percent >= 75) perfEmoji = "🟣";
592
+ else if (percent >= 50) perfEmoji = "🔵";
593
+ else if (percent >= 25) perfEmoji = "🟢";
594
+ }
595
+ lines.push(`${perfEmoji} ${tEncounter(r.encounter?.name || "未知")}`);
596
+ const detailParts = [];
597
+ if (percent !== null) detailParts.push(`百分位 ${percent.toFixed(1)}%`);
598
+ if (median !== null) detailParts.push(`中位 ${median.toFixed(1)}%`);
599
+ detailParts.push(`击杀 ${kills}`);
600
+ const bestStr = formatAmount(best, metric);
601
+ if (bestStr) detailParts.push(bestStr);
602
+ if (r.fastestKill > 0) {
603
+ const killSec = Math.round(r.fastestKill / 1e3);
604
+ const min = Math.floor(killSec / 60);
605
+ const sec = killSec % 60;
606
+ detailParts.push(`最快 ${min}m${sec}s`);
607
+ }
608
+ lines.push(` ${detailParts.join(" ")}`);
609
+ lines.push("");
610
+ }
611
+ const avgBest = rankings.bestPerformanceAverage;
612
+ const avgMedian = rankings.medianPerformanceAverage;
613
+ lines.push("━━━━━━━━━━━━━━━━━━━━");
614
+ const summaryParts = [`📊 总计:${hasData.length} 个 Boss`];
615
+ if (avgBest !== null && avgBest !== void 0) summaryParts.push(`平均最佳 ${avgBest.toFixed(1)}%`);
616
+ if (avgMedian !== null && avgMedian !== void 0) summaryParts.push(`平均中位 ${avgMedian.toFixed(1)}%`);
617
+ lines.push(summaryParts.join(" "));
618
+ return lines.join("\n");
619
+ }
620
+ __name(formatZoneRankings, "formatZoneRankings");
621
+ function formatMplusRankings(char, scoreRankings, dpsMap) {
622
+ const className = classNameMap[char.classID] || "未知";
623
+ const classEmoji = classEmojiMap[char.classID] || "🎮";
624
+ const lines = [
625
+ `${classEmoji} ${char.name} — 大秘境战绩`,
626
+ `职业:${className} 服务器:${char.server?.name}`,
627
+ ""
628
+ ];
629
+ const rankingList = scoreRankings?.rankings;
630
+ if (!rankingList || rankingList.length === 0) {
631
+ lines.push("❌ 暂无大秘境战绩数据。");
632
+ return lines.join("\n");
633
+ }
634
+ const hasData = rankingList.filter((r) => r.totalKills > 0 || r.rankPercent !== null);
635
+ if (hasData.length === 0) {
636
+ lines.push(`❌ 暂无大秘境战绩(共 ${rankingList.length} 个副本,均无完成记录)。`);
637
+ return lines.join("\n");
638
+ }
639
+ const sorted = [...hasData].sort((a, b) => {
640
+ const ap = a.bestAmount ?? 0;
641
+ const bp = b.bestAmount ?? 0;
642
+ return bp - ap;
643
+ });
644
+ for (const r of sorted) {
645
+ const score = r.bestAmount;
646
+ const kills = r.totalKills;
647
+ const encName = r.encounter?.name || "未知";
648
+ const dps = dpsMap.get(encName);
649
+ const displayPercent = dps && dps.dpsPercent !== null ? dps.dpsPercent : r.rankPercent;
650
+ let perfEmoji = "⚪";
651
+ if (displayPercent !== null) {
652
+ if (displayPercent >= 99) perfEmoji = "🩷";
653
+ else if (displayPercent >= 90) perfEmoji = "🟠";
654
+ else if (displayPercent >= 75) perfEmoji = "🟣";
655
+ else if (displayPercent >= 50) perfEmoji = "🔵";
656
+ else if (displayPercent >= 25) perfEmoji = "🟢";
657
+ }
658
+ lines.push(`${perfEmoji} ${tEncounter(encName)}`);
659
+ const detailParts = [];
660
+ if (score > 0) detailParts.push(`评分 ${score.toFixed(1)}`);
661
+ if (dps) {
662
+ if (dps.keystone > 0) detailParts.push(`最高 +${dps.keystone}`);
663
+ detailParts.push(`最高DPS ${formatDps(dps.dps)}`);
664
+ if (dps.dpsPercent !== null) detailParts.push(`百分位 ${dps.dpsPercent.toFixed(1)}%`);
665
+ if (dps.rank !== null && dps.rank > 0) detailParts.push(`排名 #${dps.rank}`);
666
+ }
667
+ if (kills > 0) detailParts.push(`完成 ${kills} 次`);
668
+ const dur = dps ? formatDuration(dps.duration) : "";
669
+ if (dur) detailParts.push(`最快 ${dur}`);
670
+ lines.push(` ${detailParts.join(" ")}`);
671
+ lines.push("");
672
+ }
673
+ const avgBest = scoreRankings.bestPerformanceAverage;
674
+ const avgMedian = scoreRankings.medianPerformanceAverage;
675
+ lines.push("━━━━━━━━━━━━━━━━━━━━");
676
+ const summaryParts = [`📊 总计:${hasData.length} 个地下城`];
677
+ if (avgBest !== null && avgBest !== void 0) summaryParts.push(`平均最佳 ${avgBest.toFixed(1)}%`);
678
+ if (avgMedian !== null && avgMedian !== void 0) summaryParts.push(`平均中位 ${avgMedian.toFixed(1)}%`);
679
+ lines.push(summaryParts.join(" "));
680
+ const allStars = scoreRankings.allStars;
681
+ if (allStars && allStars.length > 0) {
682
+ for (const a of allStars) {
683
+ const bits = [`⭐ ${tSpec(a.spec)}`];
684
+ if (a.points !== void 0) bits.push(`${a.points.toFixed(0)} 分`);
685
+ if (a.rank) bits.push(`全球 #${a.rank}`);
686
+ if (a.serverRank) bits.push(`本服 #${a.serverRank}`);
687
+ if (a.rankPercent !== void 0 && a.rankPercent !== null) bits.push(`${a.rankPercent.toFixed(1)}%`);
688
+ lines.push(bits.join(" "));
689
+ }
690
+ }
691
+ return lines.join("\n");
692
+ }
693
+ __name(formatMplusRankings, "formatMplusRankings");
694
+ function formatCutoffs(cnCutoffs, worldCutoffs) {
695
+ const lines = ["🏆 大秘境分数线 (Midnight S1)", ""];
696
+ if (cnCutoffs) {
697
+ const cnAll = cnCutoffs.p999?.all;
698
+ const cnAll1 = cnCutoffs.p990?.all;
699
+ const cnHorde = cnCutoffs.p999?.horde;
700
+ const cnHorde1 = cnCutoffs.p990?.horde;
701
+ const cnAlly = cnCutoffs.p999?.alliance;
702
+ const cnAlly1 = cnCutoffs.p990?.alliance;
703
+ lines.push("🇨🇳 国服 (CN)");
704
+ if (cnAll && cnAll1) {
705
+ lines.push(` 全部 前0.1%: ${cnAll.quantileMinValue.toFixed(1)} 前1%: ${cnAll1.quantileMinValue.toFixed(1)} (${(cnAll.totalPopulationCount / 1e4).toFixed(1)}万人)`);
706
+ }
707
+ if (cnHorde && cnHorde1) {
708
+ lines.push(` 部落 前0.1%: ${cnHorde.quantileMinValue.toFixed(1)} 前1%: ${cnHorde1.quantileMinValue.toFixed(1)} (${(cnHorde.totalPopulationCount / 1e4).toFixed(1)}万人)`);
709
+ }
710
+ if (cnAlly && cnAlly1) {
711
+ lines.push(` 联盟 前0.1%: ${cnAlly.quantileMinValue.toFixed(1)} 前1%: ${cnAlly1.quantileMinValue.toFixed(1)} (${(cnAlly.totalPopulationCount / 1e4).toFixed(1)}万人)`);
712
+ }
713
+ lines.push("");
714
+ } else {
715
+ lines.push("🇨🇳 国服 (CN): 获取失败");
716
+ lines.push("");
717
+ }
718
+ if (worldCutoffs) {
719
+ lines.push("🌍 世界 (全部区域加权)");
720
+ lines.push(` 前0.1%: ${worldCutoffs.p999.toFixed(1)} 前1%: ${worldCutoffs.p990.toFixed(1)} (${(worldCutoffs.totalPop / 1e4).toFixed(1)}万人)`);
721
+ lines.push("");
722
+ } else {
723
+ lines.push("🌍 世界: 获取失败");
724
+ lines.push("");
725
+ }
726
+ lines.push("━━━━━━━━━━━━━━━━━━━━");
727
+ lines.push("数据来源: Raider.io · 实时更新");
728
+ lines.push("可指定区域: wcl.分数线 us / eu / tw / kr");
729
+ return lines.join("\n");
730
+ }
731
+ __name(formatCutoffs, "formatCutoffs");
732
+ function formatZoneList(zones) {
733
+ const lines = ["📋 可用副本列表", ""];
734
+ for (const z of zones) {
735
+ const encounterCount = z.encounters?.length || 0;
736
+ const zoneMatch = defaultZones.find((dz) => dz.id === z.id);
737
+ const displayName = zoneMatch ? `${tZone(z.name)} (${z.name})` : tZone(z.name);
738
+ const typeTag = zoneMatch?.type ? `[${zoneMatch.type}] ` : "";
739
+ lines.push(`${typeTag}Zone ${z.id}: ${displayName} (${encounterCount} Boss)`);
740
+ if (z.encounters && z.encounters.length > 0) {
741
+ const bossList = z.encounters.slice(0, 10).map((e) => tEncounter(e.name)).join("、");
742
+ lines.push(` Boss: ${bossList}`);
743
+ }
744
+ lines.push("");
745
+ }
746
+ return lines.join("\n");
747
+ }
748
+ __name(formatZoneList, "formatZoneList");
749
+ function parseDifficulty(input) {
750
+ const lower = input.toLowerCase();
751
+ if (lower === "lfr" || input === "随机") return 1;
752
+ if (lower === "n" || lower === "normal" || input === "普通") return 3;
753
+ if (lower === "h" || lower === "heroic" || input === "英雄") return 4;
754
+ if (lower === "m" || lower === "mythic" || input === "史诗") return 5;
755
+ const num = parseInt(input);
756
+ if ([1, 2, 3, 4, 5, 10].includes(num)) return num;
757
+ return null;
758
+ }
759
+ __name(parseDifficulty, "parseDifficulty");
760
+ function apply(ctx, config) {
761
+ const logger = ctx.logger("wcl-lili");
762
+ ctx.command("wcl <input:text>", "查询角色团本战绩(WCL)").alias("战绩").example("wcl 蓝耀-shadowmoon — 查询国服角色团本战绩").example("wcl tw Asuna-azuremyst — 查询台服角色").example("wcl 蓝耀-shadowmoon 38 m — 尼鲁巴尔宫史诗").example("wcl 蓝耀-shadowmoon 孢陨幽境 m — 用副本名查询").usage("格式:角色名-服务器名 [ZoneID或副本名] [难度]。副本名支持中文(如 孢陨幽境)或英文(如 Sporefall)。默认区域为配置中的 region(通常 cn),难度 m=史诗 h=英雄 n=普通 lfr=随机。查询台服在前面加 tw。").action(async ({}, input) => {
763
+ if (!input) return "❌ 请输入角色信息,例如:wcl 蓝耀-shadowmoon\n台服查询:wcl tw Asuna-azuremyst";
764
+ if (!config.clientId || !config.clientSecret) return "❌ 未配置 WCL API Client ID / Secret,请在插件配置中填写。";
765
+ const zd = parseZoneAndDiff(input, config.zoneId, config.difficulty);
766
+ const parsed = parseCharInput(zd.charInput, config.region);
767
+ if (!parsed) return "❌ 格式错误。用法:wcl 角色名-服务器名 [副本名/ID] [难度]\n示例:wcl 蓝耀-shadowmoon\n台服:wcl tw Asuna-azuremyst";
768
+ try {
769
+ let zoneId = zd.zoneId;
770
+ if (zd.zoneName) {
771
+ const resolved = await resolveZoneByName(ctx, config, zd.zoneName);
772
+ if (resolved === null) {
773
+ return `❌ 未找到副本「${zd.zoneName}」。可用数字 ID 或英文名,如:孢陨幽境(50)、尼鲁巴尔宫(38)。
774
+ 已知团本:${knownZoneHints()}`;
775
+ }
776
+ zoneId = resolved;
777
+ }
778
+ const char = await lookupCharacter(ctx, config, parsed.charName, parsed.serverSlug, parsed.region);
779
+ if (!char) {
780
+ return `❌ 未找到角色「${parsed.charName}-${parsed.serverSlug}」(区域: ${parsed.region})
781
+ 请检查角色名和服务器 slug。`;
782
+ }
783
+ const [zone, rankings] = await Promise.all([
784
+ getZone(ctx, config, zoneId),
785
+ getZoneRankings(ctx, config, char.id, zoneId, zd.difficulty, config.metric)
786
+ ]);
787
+ if (!rankings) return "❌ 获取团本战绩失败,请稍后再试。";
788
+ return formatZoneRankings(char, zone, rankings, config.metric);
789
+ } catch (e) {
790
+ logger.error(e);
791
+ if (e.message?.includes("Client") || e.response?.status === 401) {
792
+ return "❌ API 凭证无效,请检查 Client ID 和 Secret。";
793
+ }
794
+ return `❌ 查询失败:${e.message || "未知错误"}`;
795
+ }
796
+ });
797
+ ctx.command("wcl.角色 <input:text>", "查询角色基本信息").example("wcl.角色 蓝耀-shadowmoon").usage("返回角色的职业、等级、阵营、服务器等信息。").action(async ({}, input) => {
798
+ if (!input) return "❌ 请输入角色信息,例如:wcl.角色 蓝耀-shadowmoon";
799
+ if (!config.clientId || !config.clientSecret) return "❌ 未配置 WCL API 凭证。";
800
+ const parsed = parseCharInput(input, config.region);
801
+ if (!parsed) return "❌ 格式错误。用法:wcl.角色 角色名-服务器名";
802
+ try {
803
+ const char = await lookupCharacter(ctx, config, parsed.charName, parsed.serverSlug, parsed.region);
804
+ if (!char) {
805
+ return `❌ 未找到角色「${parsed.charName}-${parsed.serverSlug}」(区域: ${parsed.region})`;
806
+ }
807
+ return formatCharacter(char);
808
+ } catch (e) {
809
+ logger.error(e);
810
+ return `❌ 查询失败:${e.message || "未知错误"}`;
811
+ }
812
+ });
813
+ ctx.command("wcl.副本 <input:text>", "查询指定副本战绩").example("wcl.副本 蓝耀-shadowmoon 38 — 查询尼鲁巴尔宫战绩").example("wcl.副本 蓝耀-shadowmoon 38 m — 史诗难度").example("wcl.副本 蓝耀-shadowmoon 孢陨幽境 h — 用副本名查询英雄难度").usage("格式:角色名-服务器名 [副本名/ID] [难度]。副本名支持中文(如 孢陨幽境)或英文(如 Sporefall),也可用数字 ID(50=孢陨幽境 46=虚影尖塔 44=法力锻造 38=尼鲁巴尔宫)。难度 m=史诗 h=英雄 n=普通 lfr=随机,不填默认史诗。").action(async ({}, input) => {
814
+ if (!input) return "❌ 请输入角色信息,例如:wcl.副本 蓝耀-shadowmoon 38";
815
+ if (!config.clientId || !config.clientSecret) return "❌ 未配置 WCL API 凭证。";
816
+ const zd = parseZoneAndDiff(input, config.zoneId, config.difficulty);
817
+ const charParsed = parseCharInput(zd.charInput, config.region);
818
+ if (!charParsed) return "❌ 格式错误。用法:wcl.副本 角色名-服务器名 [副本名/ID] [难度]";
819
+ try {
820
+ let zoneId = zd.zoneId;
821
+ if (zd.zoneName) {
822
+ const resolved = await resolveZoneByName(ctx, config, zd.zoneName);
823
+ if (resolved === null) {
824
+ return `❌ 未找到副本「${zd.zoneName}」。可用数字 ID 或英文名,如:孢陨幽境(50)、尼鲁巴尔宫(38)。
825
+ 已知团本:${knownZoneHints()}`;
826
+ }
827
+ zoneId = resolved;
828
+ }
829
+ const char = await lookupCharacter(ctx, config, charParsed.charName, charParsed.serverSlug, charParsed.region);
830
+ if (!char) {
831
+ return `❌ 未找到角色「${charParsed.charName}-${charParsed.serverSlug}」(区域: ${charParsed.region})`;
832
+ }
833
+ const [zone, rankings] = await Promise.all([
834
+ getZone(ctx, config, zoneId),
835
+ getZoneRankings(ctx, config, char.id, zoneId, zd.difficulty, config.metric)
836
+ ]);
837
+ if (!rankings) return "❌ 获取副本战绩失败,请稍后再试。";
838
+ return formatZoneRankings(char, zone, rankings, config.metric);
839
+ } catch (e) {
840
+ logger.error(e);
841
+ return `❌ 查询失败:${e.message || "未知错误"}`;
842
+ }
843
+ });
844
+ ctx.command("wcl.大秘境 <input:text>", "查询角色大秘境评分").example("wcl.大秘境 蓝耀-shadowmoon").usage("查询角色在当前赛季大秘境中的各副本评分。使用 playerscore 指标。").action(async ({}, input) => {
845
+ if (!input) return "❌ 请输入角色信息,例如:wcl.大秘境 蓝耀-shadowmoon";
846
+ if (!config.clientId || !config.clientSecret) return "❌ 未配置 WCL API 凭证。";
847
+ const parsed = parseCharInput(input, config.region);
848
+ if (!parsed) return "❌ 格式错误。用法:wcl.大秘境 角色名-服务器名";
849
+ try {
850
+ const char = await lookupCharacter(ctx, config, parsed.charName, parsed.serverSlug, parsed.region);
851
+ if (!char) {
852
+ return `❌ 未找到角色「${parsed.charName}-${parsed.serverSlug}」(区域: ${parsed.region})`;
853
+ }
854
+ const scoreRankings = await getZoneRankings(ctx, config, char.id, config.mplusZoneId, null, "playerscore");
855
+ if (!scoreRankings) return "❌ 获取大秘境战绩失败,请稍后再试。";
856
+ const dpsMap = /* @__PURE__ */ new Map();
857
+ const scored = (scoreRankings.rankings || []).filter((r) => r.totalKills > 0 && r.encounter?.id);
858
+ await Promise.all(scored.map(async (r) => {
859
+ try {
860
+ const er = await getEncounterRankings(ctx, config, char.id, r.encounter.id, "dps");
861
+ const entries = er?.ranks || [];
862
+ if (entries.length === 0) return;
863
+ let best = entries[0];
864
+ let maxKeystone = 0;
865
+ for (const e of entries) {
866
+ if (e.amount > best.amount) best = e;
867
+ if ((e.bracketData || 0) > maxKeystone) maxKeystone = e.bracketData || 0;
868
+ }
869
+ dpsMap.set(r.encounter.name, {
870
+ dps: er.bestAmount ?? best.amount,
871
+ dpsPercent: best.rankPercent,
872
+ // fastestKill 是该副本最快通关用时(毫秒)
873
+ duration: er.fastestKill ?? best.duration,
874
+ keystone: maxKeystone,
875
+ rank: estimateRank(best),
876
+ medal: best.medal || ""
877
+ });
878
+ } catch {
879
+ }
880
+ }));
881
+ return formatMplusRankings(char, scoreRankings, dpsMap);
882
+ } catch (e) {
883
+ logger.error(e);
884
+ return `❌ 查询失败:${e.message || "未知错误"}`;
885
+ }
886
+ });
887
+ ctx.command("wcl.分数线 [region:text]", "查询大秘境赛季分数线(前0.1%和前1%)").example("wcl.分数线 — 查询国服和世界分数线").example("wcl.分数线 us — 查询美服分数线").usage("数据来源 Raider.io,展示各区域前0.1%和前1%分数线。不指定区域则显示国服+世界。").action(async ({}, region) => {
888
+ const targetRegion = region && knownRegions.includes(region.toLowerCase()) ? region.toLowerCase() : config.region;
889
+ try {
890
+ if (region && knownRegions.includes(region.toLowerCase())) {
891
+ const cutoffs = await getCutoffs(ctx, config.seasonSlug, targetRegion);
892
+ const world = await getWorldCutoffs(ctx, config.seasonSlug);
893
+ if (!cutoffs && !world) return "❌ 获取分数线失败,请稍后再试。";
894
+ const lines = ["🏆 大秘境分数线 (Midnight S1)", ""];
895
+ if (cutoffs) {
896
+ const regName = regionNameMap[targetRegion] || targetRegion.toUpperCase();
897
+ const flag = { cn: "🇨🇳", us: "🇺🇸", eu: "🇪🇺", tw: "🇹🇼", kr: "🇰🇷" }[targetRegion] || "🌐";
898
+ lines.push(`${flag} ${regName} (${targetRegion.toUpperCase()})`);
899
+ const all = cutoffs.p999?.all;
900
+ const all1 = cutoffs.p990?.all;
901
+ const horde = cutoffs.p999?.horde;
902
+ const horde1 = cutoffs.p990?.horde;
903
+ const ally = cutoffs.p999?.alliance;
904
+ const ally1 = cutoffs.p990?.alliance;
905
+ if (all && all1) {
906
+ lines.push(` 全部 前0.1%: ${all.quantileMinValue.toFixed(1)} 前1%: ${all1.quantileMinValue.toFixed(1)} (${(all.totalPopulationCount / 1e4).toFixed(1)}万人)`);
907
+ }
908
+ if (horde && horde1) {
909
+ lines.push(` 部落 前0.1%: ${horde.quantileMinValue.toFixed(1)} 前1%: ${horde1.quantileMinValue.toFixed(1)} (${(horde.totalPopulationCount / 1e4).toFixed(1)}万人)`);
910
+ }
911
+ if (ally && ally1) {
912
+ lines.push(` 联盟 前0.1%: ${ally.quantileMinValue.toFixed(1)} 前1%: ${ally1.quantileMinValue.toFixed(1)} (${(ally.totalPopulationCount / 1e4).toFixed(1)}万人)`);
913
+ }
914
+ lines.push("");
915
+ }
916
+ if (world) {
917
+ lines.push("🌍 世界 (全部区域加权)");
918
+ lines.push(` 前0.1%: ${world.p999.toFixed(1)} 前1%: ${world.p990.toFixed(1)} (${(world.totalPop / 1e4).toFixed(1)}万人)`);
919
+ lines.push("");
920
+ }
921
+ lines.push("━━━━━━━━━━━━━━━━━━━━");
922
+ lines.push("数据来源: Raider.io · 实时更新");
923
+ return lines.join("\n");
924
+ } else {
925
+ const cnCutoffs = await getCutoffs(ctx, config.seasonSlug, "cn");
926
+ const worldCutoffs = await getWorldCutoffs(ctx, config.seasonSlug);
927
+ if (!cnCutoffs && !worldCutoffs) return "❌ 获取分数线失败,请稍后再试。";
928
+ return formatCutoffs(cnCutoffs, worldCutoffs);
929
+ }
930
+ } catch (e) {
931
+ logger.error(e);
932
+ return `❌ 获取分数线失败:${e.message || "未知错误"}`;
933
+ }
934
+ });
935
+ ctx.command("wcl.副本列表", "列出当前可用副本").usage("列出默认副本及其 Boss 列表。").action(async ({}) => {
936
+ if (!config.clientId || !config.clientSecret) return "❌ 未配置 WCL API 凭证。";
937
+ try {
938
+ const zones = await Promise.all(
939
+ defaultZones.map((z) => getZone(ctx, config, z.id))
940
+ );
941
+ const validZones = zones.filter((z) => z !== null);
942
+ if (validZones.length === 0) return "❌ 获取副本列表失败。";
943
+ return formatZoneList(validZones);
944
+ } catch (e) {
945
+ logger.error(e);
946
+ return `❌ 查询失败:${e.message || "未知错误"}`;
947
+ }
948
+ });
949
+ ctx.command("wcl.额度", "查询 WCL API 配额使用情况").example("wcl.额度").usage("显示当前 OAuth 凭证的每小时请求配额、已用点数与重置时间。").action(async ({}) => {
950
+ if (!config.clientId || !config.clientSecret) return "❌ 未配置 WCL API 凭证。";
951
+ try {
952
+ const data = await gql(ctx, config, "{ rateLimitData { limitPerHour pointsSpentThisHour pointsResetIn } }");
953
+ const rl = data?.rateLimitData;
954
+ if (!rl) return "❌ 获取配额失败,请稍后再试。";
955
+ const limit = rl.limitPerHour;
956
+ const spent = rl.pointsSpentThisHour;
957
+ const remaining = Math.max(0, limit - spent);
958
+ const usedPct = (spent / limit * 100).toFixed(1);
959
+ const resetMin = Math.ceil((rl.pointsResetIn || 0) / 60);
960
+ return [
961
+ "📊 WCL API 配额",
962
+ `每小时上限:${limit} 点`,
963
+ `已用:${spent.toFixed(2)} 点 (${usedPct}%)`,
964
+ `剩余:${remaining.toFixed(2)} 点`,
965
+ `重置:约 ${resetMin} 分钟后`
966
+ ].join("\n");
967
+ } catch (e) {
968
+ logger.error(e);
969
+ return `❌ 获取配额失败:${e.message || "未知错误"}`;
970
+ }
971
+ });
972
+ ctx.inject(["console"], (ctx2) => {
973
+ ctx2.console.addEntry({
974
+ dev: (0, import_path.resolve)(__dirname, "../client/index.ts"),
975
+ prod: (0, import_path.resolve)(__dirname, "../dist")
976
+ });
977
+ });
978
+ }
979
+ __name(apply, "apply");
980
+ // Annotate the CommonJS export names for ESM import in node:
981
+ 0 && (module.exports = {
982
+ Config,
983
+ apply,
984
+ name,
985
+ usage
986
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "koishi-plugin-wcl-lili",
3
+ "description": "Warcraft Logs 角色战绩查询 · 团本/大秘境/分数线/角色信息",
4
+ "version": "0.9.0",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "chatbot",
14
+ "koishi",
15
+ "plugin"
16
+ ],
17
+ "devDependencies": {
18
+ "@koishijs/client": "^5.30.4"
19
+ },
20
+ "peerDependencies": {
21
+ "@koishijs/plugin-console": "^5.30.4",
22
+ "koishi": "^4.18.7"
23
+ }
24
+ }
package/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # koishi-plugin-wcl-lili
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-wcl-lili?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-wcl-lili)
4
+
5
+