dsh-api-dashboard 1.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.
@@ -0,0 +1,1317 @@
1
+ /**
2
+ * dsh-api-dashboard — browser half (v4 精致UI版).
3
+ *
4
+ * 三层 + 分组抽屉结构:
5
+ * 一层「状态条 bar」: 输入框下方, 显示当前选中平台 [图标]平台名 余额 三色灯。
6
+ * 二层「看板 drawer」: 底部抽屉, 分【国内】【海外】两个折叠区(默认收起), 点开展示平台。
7
+ * 三层「详情 detail」: 点平台卡片 → 总充值/总使用/备注。
8
+ *
9
+ * 借鉴 dsh-balance:
10
+ * - 三色阈值灯: 余额>safe 绿 / >warn 黄 / 否则红 (可设置)
11
+ * - 单例轮询器: 页面隐藏暂停, 前台恢复刷新, 手动强刷。
12
+ * - 完整设置面板(安全值/货币/中转站管理)。
13
+ * - 精致 SVG 图标(无 emoji)。
14
+ */
15
+ window.__ModuleLoader__.load({
16
+ id: "dsh-api-dashboard",
17
+ factory: (require) => {
18
+ var module = { exports: {} };
19
+ var exports = module.exports;
20
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
21
+ let react = require("react");
22
+ let _ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
23
+
24
+ //#region 品牌官方 SVG 图标
25
+ const ICONS = {
26
+ 'alibaba': '<svg fill="#FF6A00" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M3.996 4.517h5.291L8.01 6.324L4.153 7.506a1.67 1.67 0 0 0-1.165 1.601v5.786a1.67 1.67 0 0 0 1.165 1.6l3.857 1.183l1.277 1.807H3.996A3.996 3.996 0 0 1 0 15.487V8.513a3.996 3.996 0 0 1 3.996-3.996m16.008 0h-5.291l1.277 1.807l3.857 1.182c.715.227 1.17.889 1.165 1.601v5.786a1.67 1.67 0 0 1-1.165 1.6l-3.857 1.183l-1.277 1.807h5.291A3.996 3.996 0 0 0 24 15.487V8.513a3.996 3.996 0 0 0-3.996-3.996m-4.007 8.345H8.002v-1.804h7.995Z"/></svg>',
27
+ 'claude': '<svg fill="#D97757" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="m4.714 15.956l4.718-2.648l.079-.23l-.08-.128h-.23l-.79-.048l-2.695-.073l-2.337-.097l-2.265-.122l-.57-.121l-.535-.704l.055-.353l.48-.321l.685.06l1.518.104l2.277.157l1.651.098l2.447.255h.389l.054-.158l-.133-.097l-.103-.098l-2.356-1.596l-2.55-1.688l-1.336-.972l-.722-.491L2 6.223l-.158-1.008l.656-.722l.88.06l.224.061l.893.686l1.906 1.476l2.49 1.833l.364.304l.146-.104l.018-.072l-.164-.274l-1.354-2.446l-1.445-2.49l-.644-1.032l-.17-.619a3 3 0 0 1-.103-.729L6.287.133L6.7 0l.995.134l.42.364l.619 1.415L9.735 4.14l1.555 3.03l.455.898l.243.832l.09.255h.159V9.01l.127-1.706l.237-2.095l.23-2.695l.08-.76l.376-.91l.747-.492l.583.28l.48.685l-.067.444l-.286 1.851l-.558 2.903l-.365 1.942h.213l.243-.242l.983-1.306l1.652-2.064l.728-.82l.85-.904l.547-.431h1.032l.759 1.129l-.34 1.166l-1.063 1.347l-.88 1.142l-1.263 1.7l-.79 1.36l.074.11l.188-.02l2.853-.606l1.542-.28l1.84-.315l.832.388l.09.395l-.327.807l-1.967.486l-2.307.462l-3.436.813l-.043.03l.049.061l1.548.146l.662.036h1.62l3.018.225l.79.522l.473.638l-.08.485l-1.213.62l-1.64-.389l-3.825-.91l-1.31-.329h-.183v.11l1.093 1.068l2.003 1.81l2.508 2.33l.127.578l-.321.455l-.34-.049l-2.204-1.657l-.85-.747l-1.925-1.62h-.127v.17l.443.649l2.343 3.521l.122 1.08l-.17.353l-.607.213l-.668-.122l-1.372-1.924l-1.415-2.168l-1.141-1.943l-.14.08l-.674 7.254l-.316.37l-.728.28l-.607-.461l-.322-.747l.322-1.476l.388-1.924l.316-1.53l.285-1.9l.17-.632l-.012-.042l-.14.018l-1.432 1.967l-2.18 2.945l-1.724 1.845l-.413.164l-.716-.37l.066-.662l.401-.589l2.386-3.036l1.439-1.882l.929-1.086l-.006-.158h-.055L4.138 18.56l-1.13.146l-.485-.456l.06-.746l.231-.243l1.907-1.312Z"/></svg>',
28
+ 'deepseek': '<svg fill="#4D6BFE" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M23.748 4.651c-.254-.124-.364.113-.512.233c-.051.04-.094.09-.137.137c-.372.397-.806.657-1.373.626c-.829-.046-1.537.214-2.163.848c-.133-.782-.575-1.248-1.247-1.548c-.352-.155-.708-.311-.955-.65c-.172-.24-.219-.509-.305-.774c-.055-.16-.11-.323-.293-.35c-.2-.031-.278.136-.356.276c-.313.572-.434 1.202-.422 1.84c.027 1.436.633 2.58 1.838 3.393c.137.094.172.187.129.323c-.082.28-.18.553-.266.833c-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836c.27-.098.094-.433-.778-.428c-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136a9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653c1.857 1.533 3.997 2.284 6.438 2.14c1.482-.085 3.132-.284 4.994-1.86c.47.234.962.328 1.78.398c.629.058 1.235-.031 1.705-.129c.735-.155.684-.836.418-.961c-2.155-1.004-1.682-.595-2.112-.926c1.095-1.295 2.768-3.598 3.284-6.733c.05-.346.115-.834.108-1.114c-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517c.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16c-.39.024-.32.472-.234.763c.09.288.207.487.371.74c.114.167.192.416-.113.603c-.673.416-1.842-.14-1.897-.168c-1.361-.801-2.5-1.86-3.301-3.306c-.775-1.393-1.225-2.888-1.299-4.482c-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774c.868.86 1.525 1.887 2.202 2.89c.72 1.066 1.494 2.082 2.48 2.915c.348.291.626.513.892.677c-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287a.3.3 0 0 1 .113.074a.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727c-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078a.253.253 0 0 1-.114-.358a1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016c.352.144.618.408 1.001.782c.392.451.462.576.685.915c.176.264.336.536.446.848c.066.194-.02.353-.25.45"/></svg>',
29
+ 'gemini': '<svg fill="#4285F4" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68q.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58a12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68q-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96q2.19.93 3.81 2.55t2.55 3.81"/></svg>',
30
+ 'groq': '<svg fill="#F55036" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M3 3v18h18V3zm11.72 13.37c-.41.38-.82.66-1.33.87l-.21.09c-.83.3-1.82.21-2.63-.1c-.45-.21-.82-.46-1.19-.8c.33-.41.66-.75 1.07-1.07l.27.21c.5.35 1 .47 1.61.41c.62-.12 1.12-.4 1.52-.9c.37-.61.41-1.09.41-1.8V10.4c0-.72-.15-1.18-.6-1.74c-.61-.49-1.17-.74-1.96-.7c-.66.11-1.19.42-1.59.95c-.33.53-.48 1.07-.37 1.69c.2.68.45 1.25 1.07 1.61c.52.27.98.32 1.56.33h.25c.2.02.4.02.61.03V14c-1.49.06-2.65.06-3.84-.97a4.22 4.22 0 0 1-1.23-2.8c.04-.88.35-1.6.86-2.32l.15-.23c1.43-1.51 3.7-1.61 5.31-.31l.17.14c.58.52.96 1.25 1.08 2.01c0 .16.01.33.01.49v3.6c0 1.05-.3 1.95-1.02 2.74Z"/></svg>',
31
+ 'mistral': '<svg fill="#FA520F" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M17.143 3.429v3.428h-3.429v3.429h-3.428V6.857H6.857V3.43H3.43v13.714H0v3.428h10.286v-3.428H6.857v-3.429h3.429v3.429h3.429v-3.429h3.428v3.429h-3.428v3.428H24v-3.428h-3.43V3.429z"/></svg>',
32
+ 'moonshot': '<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><path d="m11.222 180.43l101.75 27.218a224 224 0 0 0 .64 21.683l63.53 16.99a128 128 0 0 1-59.584 9.226l-1.92-.17l-.47-.044l-.896-.095l-1.002-.107q-.838-.101-1.675-.213l-1.141-.15l-1.174-.17q-1.71-.25-3.413-.544l-.448-.086l-.8-.138l-1.141-.214l-.747-.16l-.992-.202l-.8-.17l-1.013-.214l-1.035-.246l-1.003-.234l-.725-.182l-.939-.234l-.96-.256l-1.013-.267l-.874-.245l-1.163-.32l-.661-.213l-.896-.267l-.992-.299l-1.12-.362l-.62-.203l-.852-.277l-.96-.33l-.704-.257l-.47-.16l-.725-.266l-1.077-.395l-.608-.235l-.854-.32l-.928-.373l-.938-.373l-.843-.342l-1.013-.426l-.672-.299l-.672-.288l-.438-.192l-.704-.32l-1.098-.501l-.555-.256l-1.024-.49l-.661-.32l-.896-.427l-.918-.47l-.992-.5l-.554-.289l-1.1-.586l-.607-.32l-.619-.342l-.49-.277l-1.003-.565l-.64-.363l-.544-.32l-.768-.437l-.875-.533l-.992-.598l-.554-.34l-.896-.566l-.651-.416l-.843-.533l-.746-.502l-.566-.373l-.576-.384l-.469-.32l-.47-.32l-.426-.299l-.608-.426l-.81-.576l-.737-.533l-.789-.576l-.597-.448l-.811-.608l-.81-.63l-.918-.714l-.48-.373l-.683-.555l-.789-.64l-.95-.778l-.49-.416l-.49-.416l-.46-.395l-.48-.427l-.65-.565l-.747-.661l-.725-.64l-.661-.619l-.715-.661l-.565-.533l-.939-.896q-.531-.514-1.056-1.035l-.31-.298l-.437-.448l-.736-.747l-.533-.544l-.533-.565a69 69 0 0 1-1.792-1.91l-.854-.938l-.661-.746l-.757-.854l-.448-.522l-.566-.662l-.618-.725l-.491-.597l-.288-.352l-.48-.587l-.704-.874l-.437-.555l-.534-.682l-.213-.267a128 128 0 0 1-15.36-25.619M.342 118.632l121.098 32.391a218 218 0 0 0-5.002 21.449l115.38 30.866a128.8 128.8 0 0 1-19.68 21.384L7.009 169.838l-.17-.49l-.373-1.11q-.273-.813-.534-1.631l-.074-.246a127 127 0 0 1-2.208-7.903l-.32-1.344l-.192-.853l-.224-1.034l-.192-.864l-.192-.96l-.182-.896l-.192-1.003a126 126 0 0 1-.757-4.543l-.181-1.259l-.118-.885l-.138-1.088a128 128 0 0 1-.203-1.717l-.053-.501c-.797-7.6-.919-15.256-.363-22.878m16.992-54.928L144.78 97.793a218 218 0 0 0-10.773 19.997l120.48 32.232a126.4 126.4 0 0 1-7.126 25.224l-123.2-32.957L1.323 109.44l.16-1.066l.086-.523l.106-.714l.16-.928l.192-1.046c.278-1.578.598-3.146.94-4.714l.298-1.322l.213-.907l.256-1.034c.235-.96.48-1.92.747-2.859l.298-1.088l.246-.885l.32-1.066l.266-.875l.32-1.024l.278-.875l.33-1.013a127 127 0 0 1 10.774-23.805zm47.381-46.928l120.373 32.189a222 222 0 0 0-18.005 18.355l83.445 22.323A127.8 127.8 0 0 1 256 118.068L22.465 55.61l.48-.693l.288-.427l.426-.586l.491-.694l.587-.81l.576-.768l.682-.917l.534-.694l.608-.778l.586-.747l.64-.789l.587-.736l.693-.821l.576-.704l.704-.821l.566-.64l.768-.875l.565-.64l.715-.789l.576-.619l.778-.832l.619-.64l.672-.714l1.792-1.813l1.067-1.045l.629-.598l.81-.757a129 129 0 0 1 24.235-17.886M128.182.01h1.034l.875.01l.736.011l.576.021l.725.022l.491.01l.81.032l.502.022l.64.032l.576.021l.928.053l1.12.075l1.536.117l.939.075l.469.043l.821.085l.875.085l.501.054l1.088.128l.534.064l1.152.149l.864.107l.448.064l.693.106l2.208.341l.747.128l.693.118l1.493.277l.982.192l1.173.235l.49.106l.8.171l.438.107l.661.138l.448.107l.694.16l.522.128l.758.181l1.024.256l1.194.32l1.206.32l1.205.341l.533.16l.747.214l.832.256l.779.245l.533.17l.533.171l.81.267l1.057.352l1.088.384l.512.181l.683.246l.992.362l1.173.437l1.237.48l1.067.427l.501.213l.64.256l.438.192l.672.278l.426.192l.608.266l1.174.512l1.066.49l.79.374l.8.384l.64.299l.98.49l.972.48l1.088.555l.565.298l.523.278l.49.256l.64.352l.438.234l.554.31l.939.533l1.13.64l.929.544l.608.362l.565.342l1.024.629l.939.587l1.045.66l.384.257l.683.437l.896.597l.426.288l.662.448l.66.459l.246.181c.576.395 1.152.8 1.717 1.216l.886.64l.693.512l.597.459l.918.693l.874.683l.427.32l.533.437l.918.736l.842.693l.907.757a129 129 0 0 1 20.363 21.662L77.035 10.612l.662-.288l.693-.298l.864-.363l.917-.373c1.206-.48 2.422-.96 3.638-1.397l1.024-.374l.992-.352l.896-.32l1.024-.33c.928-.32 1.877-.619 2.816-.907l.97-.288l.918-.267l1.088-.32l.906-.245l1.067-.277l.917-.256l.96-.245l.97-.235l1.014-.235l.96-.213l1.045-.224l.971-.213l1.013-.192l.982-.192l1.066-.192l.971-.17l1.045-.182l.982-.15l1.034-.16l.982-.138l1.088-.139l.97-.128l1.12-.128l.96-.106l1.12-.107a69 69 0 0 1 2.987-.256l1.13-.085l.96-.054l1.174-.064l.992-.042l1.067-.043l1.034-.021L126.08.02l2.102-.02z"/></svg>',
33
+ 'ollama': '<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M16.361 10.26a.9.9 0 0 0-.558.47l-.072.148l.001.207c0 .193.004.217.059.353c.076.193.152.312.291.448c.24.238.51.3.872.205a.86.86 0 0 0 .517-.436a.75.75 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.1 1.1 0 0 0-.466 0m-9.203.005c-.305.096-.533.32-.65.639a1.2 1.2 0 0 0-.06.52c.057.309.31.59.598.667c.362.095.632.033.872-.205c.14-.136.215-.255.291-.448c.055-.136.059-.16.059-.353l.001-.207l-.072-.148a.9.9 0 0 0-.565-.472a1 1 0 0 0-.474.007m4.184 2c-.131.071-.223.25-.195.383c.031.143.157.288.353.407c.105.063.112.072.117.136c.004.038-.01.146-.029.243c-.02.094-.036.194-.036.222c.002.074.07.195.143.253c.064.052.076.054.255.059c.164.005.198.001.264-.03c.169-.082.212-.234.15-.525c-.052-.243-.042-.28.087-.355c.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48a.4.4 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053l-.053-.032c-.219-.13-.259-.145-.391-.143a.4.4 0 0 0-.193.032m.39-2.195c-.373.036-.475.05-.654.086a4.5 4.5 0 0 0-.951.328c-.94.46-1.589 1.226-1.787 2.114c-.04.176-.045.234-.045.53c0 .294.005.357.043.524c.264 1.16 1.332 2.017 2.714 2.173c.3.033 1.596.033 1.896 0c1.11-.125 2.064-.727 2.493-1.571c.114-.226.169-.372.22-.602c.039-.167.044-.23.044-.523c0-.297-.005-.355-.045-.531c-.288-1.29-1.539-2.304-3.072-2.497a7 7 0 0 0-.855-.031zm.645.937a3.3 3.3 0 0 1 1.44.514c.223.148.537.458.671.662c.166.251.26.508.303.82c.02.143.01.251-.043.482c-.08.345-.332.705-.672.957a3 3 0 0 1-.689.348c-.382.122-.632.144-1.525.138c-.582-.006-.686-.01-.853-.042q-.856-.16-1.35-.68c-.264-.28-.385-.535-.45-.946c-.03-.192.025-.509.137-.776c.136-.326.488-.73.836-.963c.403-.269.934-.46 1.422-.512c.187-.02.586-.02.773-.002m-5.503-11a1.65 1.65 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503c0 .544.064 1.24.155 1.721c.02.107.031.202.023.208l-.187.152a5.3 5.3 0 0 0-.949 1.02a5.5 5.5 0 0 0-.94 2.339a6.6 6.6 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195l-.037.064c-.269.452-.498 1.105-.605 1.732c-.084.496-.095.629-.095 1.294c0 .67.009.803.088 1.266c.095.555.288 1.143.503 1.534c.071.128.243.393.264.407c.007.003-.014.067-.046.141a7.4 7.4 0 0 0-.548 1.873a5 5 0 0 0-.071.991c0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597c.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.9.9 0 0 0-.194-.25a1.7 1.7 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451c.124-.486.329-.918.544-1.154a.8.8 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.14 3.14 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834c.563-.814 1.353-1.336 2.237-1.475c.199-.033.57-.028.776.01c.226.04.367.028.512-.041c.179-.085.268-.19.374-.431c.093-.215.165-.333.36-.576c.234-.29.46-.489.822-.729c.413-.27.884-.467 1.352-.561c.17-.035.25-.04.569-.04s.398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602c.034.057.095.177.132.267c.105.241.195.346.374.43c.14.068.286.082.503.045c.343-.058.607-.053.943.016c1.144.23 2.14 1.173 2.581 2.437c.385 1.108.276 2.267-.296 3.153c-.097.15-.193.27-.333.419c-.301.322-.301.722-.001 1.053c.493.539.801 1.866.708 3.036c-.062.772-.26 1.463-.533 1.854a2 2 0 0 1-.224.258a.9.9 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295c.253 1.008.231 2.01-.059 2.581a1 1 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074l.036-.134c.019-.076.057-.3.088-.516a9 9 0 0 0 0-1.258c-.11-.875-.295-1.57-.597-2.226c-.032-.074-.053-.138-.046-.141a1.4 1.4 0 0 0 .108-.152c.376-.569.607-1.284.724-2.228c.031-.26.031-1.378 0-1.628c-.083-.645-.182-1.082-.348-1.525a6 6 0 0 0-.329-.7l-.038-.064l.131-.194c.402-.604.636-1.262.727-2.04a6.6 6.6 0 0 0-.024-1.358a5.5 5.5 0 0 0-.939-2.339a5.3 5.3 0 0 0-.95-1.02l-.186-.152a.7.7 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503c-.19-.924-.535-1.658-.98-2.082c-.354-.338-.716-.482-1.15-.455c-.996.059-1.8 1.205-2.116 3.01a7 7 0 0 0-.097.726c0 .036-.007.066-.015.066a1 1 0 0 1-.149-.078A4.86 4.86 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a1 1 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a7 7 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2 2 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388c.03.113.06.244.069.292c.007.047.026.152.041.233c.067.365.098.76.102 1.24l.002.475l-.12.175l-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.4 8.4 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711c.067-.05.079-.049.157.013m9.825-.012c.17.126.358.46.498.888c.28.854.36 2.028.212 3.145c-.019.14-.024.151-.057.144l-.238-.06a3.7 3.7 0 0 0-.954-.124h-.278l-.119-.178l-.119-.175l.002-.474c.004-.669.066-1.19.214-1.772c.157-.623.434-1.185.68-1.382c.078-.062.09-.063.159-.012"/></svg>',
34
+ 'openai': '<svg fill="#10A37F" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M22.282 9.821a6 6 0 0 0-.516-4.91a6.05 6.05 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a6 6 0 0 0-3.998 2.9a6.05 6.05 0 0 0 .743 7.097a5.98 5.98 0 0 0 .51 4.911a6.05 6.05 0 0 0 6.515 2.9A6 6 0 0 0 13.26 24a6.06 6.06 0 0 0 5.772-4.206a6 6 0 0 0 3.997-2.9a6.06 6.06 0 0 0-.747-7.073M13.26 22.43a4.48 4.48 0 0 1-2.876-1.04l.141-.081l4.779-2.758a.8.8 0 0 0 .392-.681v-6.737l2.02 1.168a.07.07 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494M3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085l4.783 2.759a.77.77 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646M2.34 7.896a4.5 4.5 0 0 1 2.366-1.973V11.6a.77.77 0 0 0 .388.677l5.815 3.354l-2.02 1.168a.08.08 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.08.08 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667m2.01-3.023l-.141-.085l-4.774-2.782a.78.78 0 0 0-.785 0L9.409 9.23V6.897a.07.07 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.8.8 0 0 0-.393.681zm1.097-2.365l2.602-1.5l2.607 1.5v2.999l-2.597 1.5l-2.607-1.5Z"/></svg>',
35
+ 'openrouter': '<svg fill="#6469FF" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032q-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237c-2.911 2.066-2.731 1.95-4.14 2.75c-.396.223-1.342.574-2.185.798c-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799c1.41.798 1.228.683 4.14 2.75c2.126 1.509 4.22 2.11 6.148 2.236c.88.058 1.716.041 2.555.005v1.918l7.222-4.168l-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021c-1.364-.09-2.417-.357-3.979-1.465c-2.244-1.593-2.866-2.027-3.68-2.508c.889-.518 1.449-.906 3.822-2.59c1.56-1.109 2.614-1.377 3.978-1.466c.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>',
36
+ 'qwen': '<svg fill="#623AE7" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M23.919 14.545L20.817 9.17l1.47-2.544a.56.56 0 0 0 0-.566l-1.633-2.83a.57.57 0 0 0-.49-.283h-6.207L12.487.402a.57.57 0 0 0-.49-.284H8.732a.56.56 0 0 0-.49.284L5.139 5.775h-2.94a.56.56 0 0 0-.49.284L.077 8.887a.56.56 0 0 0 0 .567L3.18 14.83l-1.47 2.545a.56.56 0 0 0 0 .566l1.634 2.83a.57.57 0 0 0 .49.283h6.205l1.47 2.545a.57.57 0 0 0 .49.284h3.266a.57.57 0 0 0 .49-.284l3.104-5.375h2.94a.57.57 0 0 0 .49-.283l1.634-2.828a.55.55 0 0 0-.004-.568M8.733.686l1.634 2.828l-1.634 2.828H21.8L20.164 9.17H7.425L5.63 6.06Zm1.306 19.801l-6.205-.002l1.634-2.83h3.265L2.201 6.344h3.267q3.182 5.517 6.367 11.032zm10.124-5.66L18.53 12l-6.532 11.315l-1.634-2.83c2.129-3.673 4.25-7.351 6.373-11.028h3.592l3.102 5.374z"/></svg>',
37
+ 'relay': '<svg fill="#64748B" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M13 19h1a1 1 0 0 1 1 1h7v2h-7a1 1 0 0 1-1 1h-4a1 1 0 0 1-1-1H2v-2h7a1 1 0 0 1 1-1h1v-2H4a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-7zM4 3h16a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m5 4h1V5H9zm0 8h1v-2H9zM5 5v2h2V5zm0 8v2h2v-2z"/></svg>',
38
+ 'siliconflow': '<svg fill="#6E29F6" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044" clip-rule="evenodd"/></svg>',
39
+ 'together': '<svg fill="#0F6FFF" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><g fill-rule="evenodd"><path d="M17.385 11.23a4.615 4.615 0 1 0 0-9.23a4.615 4.615 0 0 0 0 9.23m0 10.77a4.615 4.615 0 1 0 0-9.23a4.615 4.615 0 0 0 0 9.23m-10.77 0a4.615 4.615 0 1 0 0-9.23a4.615 4.615 0 0 0 0 9.23" opacity=".2"/><circle cx="6.615" cy="6.615" r="4.615"/></g></svg>',
40
+ 'zhipu': '<svg fill="#3859FF" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M11.991 23.503a.24.24 0 0 0-.244.248a.24.24 0 0 0 .244.249a.24.24 0 0 0 .245-.249a.24.24 0 0 0-.22-.247zM9.671 5.365a1.697 1.697 0 0 1 1.099 2.132l-.071.172l-.016.04l-.018.054c-.07.16-.104.32-.104.498c-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523c-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368c0 .124.018.23.053.337c.209.373.54.658.96.8c.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711c.854.284 1.378 1.226 1.099 2.167a1.66 1.66 0 0 1-2.077 1.102a1.7 1.7 0 0 1-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 0 0-1.954.746a1.66 1.66 0 0 1-1.065.764a1.677 1.677 0 0 1-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.5 1.5 0 0 1 .296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.3 1.3 0 0 0-.227-.693a2.5 2.5 0 0 1-.366-1.403a2.4 2.4 0 0 1 .366-1.208c.14-.195.21-.444.227-.693c.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.4 1.4 0 0 1-.299-.07l-.05-.019a1.7 1.7 0 0 1-1.047-2.114a1.68 1.68 0 0 1 2.094-1.101m-5.575 10.11c.26-.264.639-.367.994-.27s.633.379.728.74c.095.362-.007.748-.267 1.013c-.402.41-1.053.41-1.455 0a1.06 1.06 0 0 1 0-1.482zm14.845-.294c.359-.09.738.024.992.297c.254.274.344.665.237 1.025s-.396.634-.756.718c-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 0 1 .757-1.26zm-.064-4.39c.314.32.49.753.49 1.206s-.176.886-.49 1.206c-.315.32-.74.5-1.185.5c-.444 0-.87-.18-1.184-.5a1.727 1.727 0 0 1 0-2.412a1.654 1.654 0 0 1 2.369 0m-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 0 1-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 0 1 1.33-1.038a1.66 1.66 0 0 1 1.547.652m11.545-4.221c.368 0 .708.2.892.524s.184.724 0 1.048a1.03 1.03 0 0 1-.892.524a1.04 1.04 0 0 1-1.03-1.048a1.04 1.04 0 0 1 1.03-1.048m-14.358 0c.368 0 .707.2.891.524s.184.724 0 1.048a1.03 1.03 0 0 1-.891.524a1.04 1.04 0 0 1-1.03-1.048c0-.579.461-1.048 1.03-1.048m10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705s-1.674-.763-1.674-1.705s.75-1.706 1.674-1.706m-2.626-.684c.362-.082.653-.356.761-.718a1.06 1.06 0 0 0-.238-1.028a1.02 1.02 0 0 0-.996-.294c-.547.14-.881.7-.752 1.257c.13.558.675.907 1.225.783m0 16.876c.359-.087.644-.36.75-.72a1.06 1.06 0 0 0-.237-1.019a1.02 1.02 0 0 0-.985-.301a1.04 1.04 0 0 0-.762.717c-.108.361-.017.754.239 1.028c.245.263.606.377.953.305zM17.19 3.5a.63.63 0 0 0 .628-.64a.63.63 0 0 0-.628-.64a.63.63 0 0 0-.628.64c0 .355.28.64.628.64m-10.38 0a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64a.63.63 0 0 0-.628.64c0 .355.279.64.628.64m-5.182 7.852a.63.63 0 0 0-.628.64c0 .354.28.639.628.639a.63.63 0 0 0 .627-.606l.001-.034a.62.62 0 0 0-.628-.64zm5.182 9.13a.63.63 0 0 0-.628.64c0 .355.279.64.628.64a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64m10.38.018a.63.63 0 0 0-.628.64c0 .355.28.64.628.64a.63.63 0 0 0 .628-.64a.63.63 0 0 0-.628-.64m5.182-9.148a.63.63 0 0 0-.628.64c0 .354.279.639.628.639a.63.63 0 0 0 .628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 0 0 .244-.249a.24.24 0 0 0-.244-.249a.24.24 0 0 0-.244.249c0 .142.122.249.244.249M11.991.497a.24.24 0 0 0 .245-.248A.24.24 0 0 0 11.99 0a.24.24 0 0 0-.244.249c0 .133.108.236.223.247zM2.011 6.36a.24.24 0 0 0 .245-.249a.24.24 0 0 0-.244-.249a.24.24 0 0 0-.244.249a.24.24 0 0 0 .244.249zm0 11.263a.24.24 0 0 0-.243.248a.24.24 0 0 0 .244.249a.24.24 0 0 0 .244-.249a.25.25 0 0 0-.244-.248zm19.995-.018a.24.24 0 0 0-.245.248a.24.24 0 0 0 .245.25a.24.24 0 0 0 .244-.25a.25.25 0 0 0-.244-.248"/></svg>',
41
+ };
42
+ //#endregion
43
+
44
+ //#region 品牌元数据
45
+ const BRAND_META = {
46
+ deepseek: { name: "DeepSeek", color: "#4D6BFE", cat: "国内" },
47
+ zhipu: { name: "智谱 GLM", color: "#3859FF", cat: "国内" },
48
+ moonshot: { name: "Kimi", color: "#000000", cat: "国内" },
49
+ stepfun: { name: "阶跃星辰", color: "#FA520F", cat: "国内" },
50
+ siliconflow: { name: "硅基流动", color: "#6E29F6", cat: "国内" },
51
+ minimax: { name: "MiniMax", color: "#1E40AF", cat: "国内" },
52
+ openrouter: { name: "OpenRouter", color: "#6469FF", cat: "海外" },
53
+ novita: { name: "Novita", color: "#FA520F", cat: "海外" },
54
+ xai: { name: "xAI Grok", color: "#000000", cat: "海外" },
55
+ relay: { name: "中转站", color: "#64748B", cat: "中转站" },
56
+ };
57
+ /** 平台 id → 图标文件名 映射 (服务端 icon 字段即文件名) */
58
+ const ICON_ALIAS = {
59
+ stepfun: "mistral", novita: "together", minimax: "together", xai: "mistral", relay: "relay",
60
+ };
61
+ /** 无官方图标、用文字名代替的平台 */
62
+ const TEXT_ONLY = new Set(["stepfun", "novita", "minimax", "xai", "relay"]);
63
+ /** 文字图标: 取平台名的首字符做一个彩色圆圈字 */
64
+ function textIcon(id, meta) {
65
+ const color = meta.color || "#64748B";
66
+ const name = meta.name || id;
67
+ const c = name.trim()[0] ? name.trim()[0].toUpperCase() : "?";
68
+ return react.createElement("span", { className: "dshadb_texticon", style: { background: color } }, c);
69
+ }
70
+ function metaFor(id) { return BRAND_META[id] || { name: id, color: "#64748B", cat: "中转站" }; }
71
+ function iconFor(id) { const key = ICON_ALIAS[id] || id; return ICONS[key] || ICONS.relay; }
72
+ function iconOf(b) {
73
+ if (b && b.icon && ICONS[b.icon]) return ICONS[b.icon];
74
+ if (b) return iconFor(b.platform);
75
+ return ICONS.relay;
76
+ }
77
+ function renderLogo(b) {
78
+ const meta = metaFor(b ? b.platform : "");
79
+ if (TEXT_ONLY.has(b ? b.platform : "")) return textIcon(b ? b.platform : "?", meta);
80
+ return react.createElement("span", { className: "dshadb_imglogo", dangerouslySetInnerHTML: { __html: iconOf(b) } });
81
+ }
82
+ //#endregion
83
+
84
+ //#region 精致 SVG 图标组件
85
+ function IconTitle() {
86
+ return react.createElement("img", { src: "/api-dashboard/icon", alt: "哦鲸鲸", style: { width: "100%", height: "100%", borderRadius: "6px", objectFit: "cover" } });
87
+ }
88
+ function IconGear() {
89
+ return react.createElement("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
90
+ react.createElement("circle", { cx: 12, cy: 12, r: 3 }),
91
+ react.createElement("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" }));
92
+ }
93
+ function IconClose() {
94
+ return react.createElement("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round" },
95
+ react.createElement("path", { d: "M18 6 6 18M6 6l12 12" }));
96
+ }
97
+ function IconBack() {
98
+ return react.createElement("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round" },
99
+ react.createElement("path", { d: "M19 12H5M12 19l-7-7 7-7" }));
100
+ }
101
+ function IconTrash() {
102
+ return react.createElement("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
103
+ react.createElement("path", { d: "M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }));
104
+ }
105
+ function IconChevron({ open }) {
106
+ return react.createElement("svg", { width: 12, height: 12, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", strokeLinejoin: "round", style: { transition: "transform .2s ease", transform: open ? "rotate(180deg)" : "none" } },
107
+ react.createElement("path", { d: "m6 9 6 6 6-6" }));
108
+ }
109
+ function IconPlus() {
110
+ return react.createElement("svg", { width: 13, height: 13, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round" },
111
+ react.createElement("path", { d: "M12 5v14M5 12h14" }));
112
+ }
113
+ //#endregion
114
+
115
+ //#region styles (A 风格)
116
+ const CSS_ID = "dsh-api-dashboard/styles.css";
117
+ if (typeof document !== "undefined" && document.querySelector('style[data-plugin-css="' + CSS_ID + '"]') === null) {
118
+ const tag = document.createElement("style");
119
+ tag.dataset.plugin = "dsh-api-dashboard";
120
+ tag.dataset.pluginCss = CSS_ID;
121
+ tag.textContent = `/* ===== 动画 ===== */
122
+ @keyframes dshadb-fadein{from{opacity:0}to{opacity:1}}
123
+ @keyframes dshadb-slideup{from{transform:translateY(100%)}to{transform:translateY(0)}}
124
+ @keyframes dshadb-pulse{0%,100%{opacity:1}50%{opacity:0.35}}
125
+
126
+ /* ===== 状态条(顶部小条) ===== */
127
+ .dshadb_bar{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:4px 10px 4px 6px;border-radius:10px;background:#f5f6f8;border:1px solid #e7e8ec;color:#777b84;cursor:pointer;white-space:nowrap;user-select:none;-webkit-tap-highlight-color:transparent}
128
+ .dshadb_bar:active{opacity:0.8}
129
+ .dshadb_bar_logo{width:16px;height:16px;border-radius:5px;background:#eef2ff;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:800;color:#4f7cff}
130
+ .dshadb_bar_name{font-weight:700;color:#17181c;font-size:11px}
131
+ .dshadb_bar_amount{font-weight:800;font-variant-numeric:tabular-nums;color:#17181c}
132
+ .dshadb_bar_dot{width:6px;height:6px;border-radius:50%;flex:none}
133
+ .dshadb_bar_dot_ok{background:#35b56b}
134
+ .dshadb_bar_dot_warn{background:#e6a72f}
135
+ .dshadb_bar_dot_err{background:#e05252}
136
+ .dshadb_bar_dot_loading{animation:dshadb-pulse 1.2s ease-in-out infinite;background:#c7c7cc}
137
+ .dshadb_bar_peak{font-size:10px;padding:2px 7px;border-radius:999px;font-weight:700}
138
+ .dshadb_bar_peak.peak{background:rgba(230,167,47,0.12);color:#b07d1a;border:1px solid rgba(230,167,47,0.2)}
139
+ .dshadb_bar_peak.valley{background:rgba(79,124,255,0.10);color:#3d5fcc;border:1px solid rgba(79,124,255,0.18)}
140
+
141
+ /* ===== 遮罩 ===== */
142
+ .dshadb_scrim{position:fixed;inset:0;z-index:99998;background:rgba(0,0,0,0.35);animation:dshadb-fadein .15s ease-out}
143
+
144
+ /* ===== 抽屉 ===== */
145
+ .dshadb_drawer{position:fixed;left:0;right:0;bottom:0;z-index:99999;max-height:85vh;background:#f5f6f8;border-radius:20px 20px 0 0;box-shadow:0 -8px 32px rgba(0,0,0,0.12);display:flex;flex-direction:column;animation:dshadb-slideup .22s cubic-bezier(.16,1,.3,1);overflow:hidden}
146
+ .dshadb_handle{display:flex;align-items:center;justify-content:center;padding:10px 0 6px;flex:none}
147
+ .dshadb_handle_bar{width:38px;height:4px;border-radius:9px;background:#c9cad0}
148
+
149
+ /* ===== 头部 ===== */
150
+ .dshadb_header{padding:12px 18px 10px;flex:none;display:flex;align-items:center;justify-content:space-between}
151
+ .dshadb_header_left{flex:1}
152
+ .dshadb_header_meta{font-size:11px;font-weight:600;color:#777b84}
153
+ .dshadb_header_title{font-size:22px;font-weight:800;color:#17181c;letter-spacing:-0.5px;line-height:1.15}
154
+ .dshadb_header_sub{font-size:11px;color:#777b84;margin-top:3px}
155
+ .dshadb_header_settings{width:38px;height:38px;border-radius:13px;background:#ffffff;border:0;box-shadow:0 10px 30px rgba(0,0,0,0.07);display:flex;align-items:center;justify-content:center;color:#777b84;cursor:pointer;-webkit-tap-highlight-color:transparent}
156
+ .dshadb_header_back{width:38px;height:38px;border-radius:13px;background:#ffffff;border:0;box-shadow:0 10px 30px rgba(0,0,0,0.07);display:flex;align-items:center;justify-content:center;color:#777b84;cursor:pointer;-webkit-tap-highlight-color:transparent}.dshadb_header_back:active{opacity:0.8;transform:scale(0.95)}
157
+ .dshadb_header_settings:active{opacity:0.8;transform:scale(0.95)}
158
+ .dshadb_refresh_btn{display:inline-flex;align-items:center;gap:4px;background:transparent;border:none;padding:0;font-size:11px;font-weight:600;color:#777b84;cursor:pointer;font-family:inherit}
159
+ .dshadb_refresh_btn:active{opacity:0.7}
160
+
161
+ /* ===== 内容区 ===== */
162
+ .dshadb_body{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:0 14px max(16px,env(safe-area-inset-bottom))}
163
+
164
+ /* ===== 分组头 ===== */
165
+ .dshadb_group{margin-top:4px}
166
+ .dshadb_group_head{display:flex;align-items:center;justify-content:space-between;padding:14px 4px 8px;cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;transition:opacity .15s ease}
167
+ .dshadb_group_head:active{opacity:0.7}
168
+ .dshadb_group_label{font-size:13px;font-weight:750;color:#17181c}
169
+ .dshadb_group_meta{font-size:10px;font-weight:600;color:#777b84}
170
+
171
+ /* ===== 卡片(核心) ===== */
172
+ .dshadb_card{display:flex;align-items:center;gap:10px;padding:13px 14px;margin-bottom:9px;border-radius:18px;background:#ffffff;border:1px solid #e7e8ec;box-shadow:0 6px 20px rgba(0,0,0,0.03);cursor:pointer;position:relative;transition:transform .12s ease,box-shadow .2s ease,border-color .2s ease;-webkit-tap-highlight-color:transparent;animation:dshadb-fadein .2s ease-out}
173
+ .dshadb_card:active{transform:scale(0.985);box-shadow:0 6px 20px rgba(0,0,0,0.05)}
174
+ @media(hover:hover){.dshadb_card:hover{box-shadow:0 8px 25px rgba(0,0,0,0.06);border-color:#d0d2d8}}
175
+ .dshadb_card_active{background:#f0f4ff;border-color:rgba(79,124,255,0.25);box-shadow:0 6px 20px rgba(79,124,255,0.06);transition:all .2s ease}
176
+ .dshadb_card_logo{width:35px;height:35px;border-radius:12px;background:#eef2ff;display:flex;align-items:center;justify-content:center;flex:none;font-size:16px;font-weight:800;color:#17181c;overflow:hidden}
177
+ .dshadb_card_logo svg{width:22px;height:22px;display:block}
178
+ .dshadb_imglogo{display:flex;align-items:center;justify-content:center;width:100%;height:100%}
179
+ .dshadb_texticon{display:flex;align-items:center;justify-content:center;width:100%;height:100%;border-radius:8px;color:#fff;font-weight:800;font-size:13px;line-height:1}
180
+ .dshadb_card_info{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
181
+ .dshadb_card_name{font-size:14px;font-weight:750;color:#17181c;line-height:1.25}
182
+ .dshadb_card_desc{font-size:10px;color:#777b84;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
183
+ .dshadb_card_right{display:flex;flex-direction:column;align-items:flex-end;gap:2px;flex:none}
184
+ .dshadb_card_amount{font-size:16px;font-weight:850;font-variant-numeric:tabular-nums;color:#17181c;letter-spacing:-0.3px;line-height:1.1}
185
+ .dshadb_card_amount_sub{font-size:13px;font-weight:700;color:#777b84}
186
+ .dshadb_card_status{display:flex;align-items:center;gap:4px;font-size:9px;font-weight:600}
187
+ .dshadb_card_status_ok{color:#35b56b}
188
+ .dshadb_card_status_warn{color:#e6a72f}
189
+ .dshadb_card_status_err{color:#e05252}
190
+ .dshadb_card_status_loading{color:#9ca0aa}
191
+ .dshadb_status_dot{width:7px;height:7px;border-radius:50%;flex:none}
192
+ .dshadb_status_dot_ok{background:#35b56b}
193
+ .dshadb_status_dot_warn{background:#e6a72f}
194
+ .dshadb_status_dot_err{background:#e05252}
195
+ .dshadb_status_dot_loading{background:#c7c7cc;animation:dshadb-pulse 1.2s ease-in-out infinite}
196
+ .dshadb_card_chevron{color:#9ca0aa;font-size:18px;margin-left:2px}
197
+ .dshadb_card_error{font-size:11px;color:#e05252;line-height:1.3;text-align:right}
198
+
199
+ /* ===== 底部按钮 ===== */
200
+ .dshadb_add_grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:6px 0 0}
201
+ .dshadb_add_btn{display:flex;align-items:center;justify-content:center;gap:5px;padding:10px;border-radius:12px;background:#ffffff;color:#17181c;font-size:12px;font-weight:700;border:1px solid #e7e8ec;cursor:pointer;font-family:inherit;box-shadow:0 6px 20px rgba(0,0,0,0.03);transition:transform .12s ease,box-shadow .2s ease}
202
+ .dshadb_add_btn:active{transform:scale(0.97);opacity:0.9}
203
+
204
+ /* ===== 空状态 ===== */
205
+ .dshadb_empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 20px;color:#777b84;font-size:13px;gap:10px;text-align:center}
206
+
207
+ /* ===== 骨架屏 ===== */
208
+ .dshadb_skeleton{display:flex;align-items:center;gap:10px;padding:13px 14px;margin-bottom:9px;border-radius:18px;background:#ffffff;border:1px solid #e7e8ec}
209
+ .dshadb_skeleton_logo{width:35px;height:35px;border-radius:12px;background:#eef2ff;flex:none}
210
+ .dshadb_skeleton_lines{flex:1;display:flex;flex-direction:column;gap:6px}
211
+ .dshadb_skeleton_line{height:10px;border-radius:999px;background:#e7e8ec}
212
+ .dshadb_skeleton_line_short{width:45%}
213
+ .dshadb_skeleton_line_long{width:70%}
214
+
215
+ /* ===== Footer ===== */
216
+ .dshadb_footer{text-align:center;font-size:10px;color:#9ca0aa;padding:12px 14px 8px}
217
+
218
+ /* ===== 分组折叠动画 ===== */
219
+ .dshadb_group_body{max-height:0;opacity:0;overflow:hidden;transition:max-height .3s cubic-bezier(.4,0,.2,1),opacity .2s ease-out;will-change:max-height,opacity}
220
+ .dshadb_group_body_open{max-height:2000px;opacity:1}
221
+ .dshadb_group_body_inner>div{padding-top:2px}
222
+ .dshadb_group_body_inner>div:first-child{padding-top:0}
223
+
224
+ /* ===== 设置面板 ===== */
225
+ .dshadb_settings_body{padding:4px 14px}
226
+ .dshadb_settings_section{margin-bottom:14px}
227
+ .dshadb_settings_group_title{font-size:11px;color:#777b84;font-weight:750;margin:13px 5px 7px;text-transform:uppercase;letter-spacing:0.4px}
228
+ .dshadb_srow{background:#ffffff;border:1px solid #e7e8ec;min-height:52px;padding:11px 13px;display:flex;align-items:center;gap:10px}
229
+ .dshadb_srow:first-of-type{border-radius:15px 15px 4px 4px}
230
+ .dshadb_srow:last-of-type{border-radius:4px 4px 15px 15px}
231
+ .dshadb_srow:only-of-type{border-radius:15px}
232
+ .dshadb_sicon{width:30px;height:30px;border-radius:10px;background:#f0f2f8;display:flex;align-items:center;justify-content:center;font-size:14px;color:#17181c;flex:none}
233
+ .dshadb_srow_label{font-size:13px;font-weight:700;color:#17181c}
234
+ .dshadb_srow_meta{font-size:10px;color:#777b84;margin-top:2px}
235
+ .dshadb_srow_value{margin-left:auto;color:#777b84;font-size:12px;font-weight:600}
236
+ .dshadb_switch{margin-left:auto;width:42px;height:25px;border-radius:20px;background:#c8cad0;padding:3px;cursor:pointer;transition:background .2s ease;border:none;flex:none}
237
+ .dshadb_switch_on{background:#4f7cff}
238
+ .dshadb_switch_knob{display:block;width:19px;height:19px;background:white;border-radius:50%;transition:transform .18s ease}
239
+ .dshadb_switch_on .dshadb_switch_knob{transform:translateX(17px)}
240
+
241
+ /* Tab 导航 */
242
+ .dshadb_tabs{display:flex;gap:7px;padding:4px 0 12px;overflow:auto}
243
+ .dshadb_tab{border:0;background:#e9eaf0;border-radius:12px;padding:8px 13px;font-size:11px;color:#686c76;white-space:nowrap;cursor:pointer;font-family:inherit;transition:all .12s ease}
244
+ .dshadb_tab_active{background:#4f7cff;color:#fff}
245
+
246
+ /* 输入框 */
247
+ .dshadb_field{width:100%;padding:11px 14px;border-radius:12px;border:1px solid #e7e8ec;background:#ffffff;color:#17181c;font-size:13px;font-family:inherit;box-sizing:border-box;margin-bottom:10px;transition:border-color .15s ease}
248
+ .dshadb_field:focus{outline:none;border-color:#4f7cff}
249
+ .dshadb_label{font-size:11px;font-weight:700;color:#777b84;margin-bottom:4px;display:block}
250
+ .dshadb_grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
251
+ .dshadb_save_btn{width:100%;padding:14px;border-radius:15px;background:#17181c;color:#fff;font-size:15px;font-weight:800;border:none;cursor:pointer;font-family:inherit;transition:opacity .1s ease}
252
+ .dshadb_save_btn:active{opacity:0.85}
253
+ .dshadb_danger{color:#e05252}
254
+
255
+ /* ===== 详情页 ===== */
256
+ .dshadb_detail{display:flex;flex-direction:column;gap:12px;padding:4px 2px}
257
+ .dshadb_detail_head{display:flex;align-items:center;gap:12px;padding:2px 2px 0;border-bottom:none}
258
+ .dshadb_detail_logo{display:flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:14px;flex:none;background:#f0f2f8;padding:10px}
259
+ .dshadb_detail_logo svg{width:100%;height:100%;display:block}
260
+ .dshadb_detail_name{font-size:18px;font-weight:800;color:#17181c}
261
+ .dshadb_detail_name_sub{font-size:11px;color:#777b84}
262
+ .dshadb_detail_bigwrap{display:flex;flex-direction:column;gap:2px;padding:8px 0 4px}
263
+ .dshadb_detail_big{font-size:36px;font-weight:800;font-variant-numeric:tabular-nums;color:#17181c;letter-spacing:-1px;line-height:1.1}
264
+ .dshadb_detail_label{font-size:11px;color:#777b84}
265
+ .dshadb_detail_metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:4px 0}
266
+ .dshadb_detail_metric{display:flex;flex-direction:column;align-items:center;gap:4px;padding:12px 8px;border-radius:12px;background:#ffffff;border:1px solid #e7e8ec;animation:dshadb-fadein .25s ease-out}
267
+ .dshadb_detail_metric_val{font-size:16px;font-weight:700;color:#17181c;font-variant-numeric:tabular-nums}
268
+ .dshadb_detail_metric_label{font-size:10px;color:#777b84}
269
+ .dshadb_detail_row{display:flex;align-items:center;justify-content:space-between;padding:7px 0;border-bottom:1px solid #eef0f4;font-size:11px}
270
+ .dshadb_detail_row_label{color:#777b84}
271
+ .dshadb_detail_row_val{color:#17181c;font-weight:600;font-variant-numeric:tabular-nums;text-align:right}
272
+ .dshadb_peakcard{display:flex;flex-direction:column;gap:5px;padding:12px 14px;border-radius:12px;margin-top:7px;border:1px solid;transition:all .2s ease}
273
+ .dshadb_peakcard.peak{background:rgba(230,167,47,0.08);border-color:rgba(230,167,47,0.25)}
274
+ .dshadb_peakcard.valley{background:rgba(79,124,255,0.06);border-color:rgba(79,124,255,0.20)}
275
+ .dshadb_peakcard_title{font-size:13px;font-weight:700;display:flex;align-items:center;gap:6px}
276
+ .dshadb_peakcard.peak .dshadb_peakcard_title{color:#b07d1a}
277
+ .dshadb_peakcard.valley .dshadb_peakcard_title{color:#3d5fcc}
278
+ .dshadb_peakcard_sub{font-size:10px;color:#777b84}
279
+ .dshadb_peakcard_slogan{font-size:11px;font-weight:600;color:#777b84;margin-top:3px;font-style:italic}
280
+ .dshadb_pricelist{display:flex;flex-direction:column;gap:6px;padding:10px 12px;border-radius:12px;background:#ffffff;border:1px solid #e7e8ec}
281
+ .dshadb_pricelist_title{display:flex;align-items:center;justify-content:space-between;font-size:11px;font-weight:700;color:#777b84}
282
+ .dshadb_price_nowtag{font-size:9px;padding:1px 7px;border-radius:999px;font-weight:600}
283
+ .dshadb_price_nowtag.peak{background:rgba(230,167,47,0.12);color:#b07d1a}
284
+ .dshadb_price_nowtag.valley{background:rgba(79,124,255,0.10);color:#3d5fcc}
285
+ .dshadb_price_row{display:flex;align-items:center;justify-content:space-between;gap:8px}
286
+ .dshadb_price_model{font-size:11px;font-weight:600;color:#17181c;flex:none}
287
+ .dshadb_price_seg{display:inline-flex;align-items:center;gap:4px;padding:2px 7px;border-radius:8px;background:#f5f6f8;border:1px solid #e7e8ec;opacity:.75}
288
+ .dshadb_price_seg_now{opacity:1;border-color:#4f7cff}
289
+ .dshadb_price_seg_label{font-size:10px}
290
+ .dshadb_price_seg_val{font-size:10.5px;font-weight:700;font-variant-numeric:tabular-nums;color:#17181c}
291
+
292
+ /* ===== 补缺失CSS ===== */
293
+ .dshadb_bar_cost{display:inline-flex;align-items:center;margin-left:2px;padding-left:8px;border-left:1px solid #e7e8ec;color:#9ca0aa;font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
294
+ .dshadb_footer_dot{width:4px;height:4px;border-radius:50%;background:#c9cad0;opacity:0.5}
295
+ .dshadb_group_divider{height:1px;background:#eef0f4;margin:2px 6px 4px}
296
+ .dshadb_icon_btn{display:inline-flex;align-items:center;justify-content:center;min-width:38px;min-height:38px;padding:8px;border-radius:10px;background:transparent;border:none;color:#777b84;cursor:pointer;transition:background .1s ease}
297
+ .dshadb_icon_btn:active{background:#f0f2f8;opacity:0.85}
298
+ .dshadb_refresh_badge{font-size:9.5px;font-weight:600;color:#9ca0aa;background:#f0f2f8;padding:1px 7px;border-radius:999px;line-height:15px;white-space:nowrap}
299
+ .dshadb_title_ic{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;overflow:hidden;flex:none}`
300
+
301
+ document.head.appendChild(tag);
302
+ /* 渐进增强: 高端机 (逻辑核 >= 6) 才启用毛玻璃等特效 */
303
+ try {
304
+ if (!navigator.hardwareConcurrency || navigator.hardwareConcurrency >= 6) {
305
+ document.documentElement.classList.add("dshadb-high-end");
306
+ }
307
+ } catch (e) { /* ignore */ }
308
+ }
309
+ //#endregion
310
+
311
+ //#region locale
312
+ const NS = "apiDashboard";
313
+ const zh = {
314
+ "bar.hint": "点击切换", "title": "哦鲸鲸", "refresh": "刷新", "refreshing": "刷新中",
315
+ "close": "关闭", "back": "返回", "status.ok": "正常", "status.warn": "偏低", "status.err": "异常",
316
+ "status.nokey": "未配置", "status.loading": "查询中", "status.noBalance": "未开放",
317
+ "status.auth": "认证失败", "status.local": "本地", "amount.free": "免费",
318
+ "all.ok": "{n} 正常", "domestic": "国内平台", "abroad": "海外平台", "local": "本地", "relay": "中转站", "custom": "自定义模型",
319
+ "selectedTag": "当前", "add.relay": "添加中转站", "add.custom": "添加自定义模型", "settings.title": "哦鲸鲸设置",
320
+ "detail.total": "当前余额", "detail.topup": "总充值", "detail.used": "总使用", "detail.note": "类型",
321
+ "detail.sessionCost": "本会话消耗", "detail.noBalance": "该平台未开放余额查询",
322
+ "settings.safe": "安全阈值(绿)", "settings.warn": "预警阈值(黄)", "settings.currency": "计价货币",
323
+ "settings.safeHint": "余额高于此值显示绿色(安全)",
324
+ "settings.warnHint": "余额低于此值显示黄色(偏低),再低显示红",
325
+ "settings.refresh": "刷新间隔(秒)", "settings.refreshHint": "5~60 秒,最高一分钟",
326
+ "settings.save": "保存并生效", "settings.saved": "已保存",
327
+ "update.title": "版本与更新", "update.current": "当前版本",
328
+ "update.check": "检查更新", "update.checking": "检查中…",
329
+ "update.latest": "已是最新版本 ✓", "update.available": "发现新版本",
330
+ "update.install": "一键更新", "update.installing": "下载安装中…",
331
+ "update.done": "已更新, 重启 Web GUI 生效", "update.fail": "更新失败, 已保持原版本",
332
+ "update.checkfail": "检查失败 (网络异常)",
333
+ "settings.section.basic": "基础设置", "settings.section.relays": "中转站", "settings.section.models": "自定义模型",
334
+ "settings.modelName": "模型名称", "settings.apiUrl": "余额接口 URL", "settings.queryType": "解析方式",
335
+ "settings.totalPath": "总余额字段(可选)", "settings.usedPath": "已用字段(可选)",
336
+ "settings.modelHint": "填你的余额接口(返回 JSON),可选填 API Key 与字段路径,如 data.balance",
337
+ "qt.auto": "自动探测", "qt.openai": "OpenAI 格式", "qt.quota": "OneAPI/NewAPI quota", "qt.deepseek": "DeepSeek 格式", "qt.openai-billing": "OpenAI 订阅", "qt.custom": "手动映射",
338
+ "cmn": "人民币 ¥", "usd": "美元 $",
339
+ "peak.now": "梁文峰 ☀️ 峰时计费",
340
+ "valley.now": "梁文谷 🌙 谷时 5 折",
341
+ "valley.weekend": "梁文谷 🌙 周末全天特惠",
342
+ "peak.hint": "09:00~12:00 / 14:00~18:00",
343
+ "valley.hint": "其余时段 / 周末全天",
344
+ "peak.slogan": "打工人的黄金档,趁午休速跑大任务!",
345
+ "valley.slogan": "夜猫子福利,错峰用 AI 物美价廉 ~",
346
+ "weekend.slogan": "周末静静嗨,5 折用 DeepSeek 真香!",
347
+ "error.timeout": "超时", "error.network": "网络错误", "error.unknown": "未知",
348
+ };
349
+ const en = {
350
+ "bar.hint": "Tap to switch", "title": "哦鲸鲸", "refresh": "Refresh", "refreshing": "Refreshing",
351
+ "close": "Close", "back": "Back", "status.ok": "OK", "status.warn": "Low", "status.err": "Error",
352
+ "status.nokey": "No Key", "status.loading": "Loading", "status.noBalance": "No API",
353
+ "status.auth": "Auth", "status.local": "Local", "amount.free": "Free",
354
+ "all.ok": "{n} OK", "domestic": "Domestic", "abroad": "Global", "local": "Local", "relay": "Relay", "custom": "Custom",
355
+ "selectedTag": "Now", "add.relay": "Add Relay", "add.custom": "Add Custom", "settings.title": "哦鲸鲸 Settings",
356
+ "detail.total": "Balance", "detail.topup": "Top-up", "detail.used": "Used", "detail.note": "Type",
357
+ "detail.sessionCost": "Session cost", "detail.noBalance": "No balance API",
358
+ "settings.safe": "Safe (green)", "settings.warn": "Warn (yellow)", "settings.currency": "Currency",
359
+ "settings.safeHint": "Above this = green (safe)",
360
+ "settings.warnHint": "Below this = yellow, lower = red",
361
+ "settings.refresh": "Refresh (sec)", "settings.refreshHint": "5-60s, max one minute",
362
+ "settings.save": "Save", "settings.saved": "Saved",
363
+ "update.title": "Version & Updates", "update.current": "Current",
364
+ "update.check": "Check updates", "update.checking": "Checking…",
365
+ "update.latest": "Up to date ✓", "update.available": "New version available",
366
+ "update.install": "Update now", "update.installing": "Installing…",
367
+ "update.done": "Updated. Restart Web GUI to apply", "update.fail": "Update failed, version unchanged",
368
+ "update.checkfail": "Check failed (network)",
369
+ "settings.section.basic": "Basic", "settings.section.relays": "Relays", "settings.section.models": "Custom Models",
370
+ "settings.modelName": "Model name", "settings.apiUrl": "Balance API URL", "settings.queryType": "Parse type",
371
+ "settings.totalPath": "Total field (opt)", "settings.usedPath": "Used field (opt)",
372
+ "settings.modelHint": "A JSON endpoint for your balance; optional API key & field paths like data.balance",
373
+ "qt.auto": "Auto", "qt.openai": "OpenAI", "qt.quota": "OneAPI quota", "qt.deepseek": "DeepSeek", "qt.openai-billing": "OpenAI sub", "qt.custom": "Manual map",
374
+ "cmn": "CNY ¥", "usd": "USD $",
375
+ "peak.now": "Peak ☀️ full rate",
376
+ "valley.now": "Off-peak 🌙 50% off",
377
+ "valley.weekend": "Weekend 🌙 all-day off",
378
+ "peak.hint": "09:00-12:00 / 14:00-18:00",
379
+ "valley.hint": "other time / weekends",
380
+ "peak.slogan": "Lunch break is peak, run big jobs now!",
381
+ "valley.slogan": "Night-owl deal, use AI cheap at off-peak~",
382
+ "weekend.slogan": "Weekend vibes, 50% off DeepSeek!",
383
+ "error.timeout": "Timeout", "error.network": "Network error", "error.unknown": "Unknown",
384
+ };
385
+ //#endregion
386
+
387
+ //#region store (单例轮询器)
388
+ const DEFAULT_POLL_MS = 30000;
389
+ let snapshot = { status: "loading", balances: [], config: { safeThreshold: 50, warnThreshold: 10 } };
390
+ const listeners = new Set();
391
+ let timer = null, pollMs = DEFAULT_POLL_MS, inflight = null, started = false;
392
+ function notify() { for (const fn of [...listeners]) fn(); }
393
+ async function refresh(force = false) {
394
+ if (inflight !== null) return inflight;
395
+ if (force && snapshot.isRefreshing !== true) { snapshot = { ...snapshot, isRefreshing: true }; notify(); }
396
+ inflight = (async () => {
397
+ try {
398
+ const url = force ? "/api-dashboard/balances?force=1" : "/api-dashboard/balances";
399
+ // v0.5.0: 走浏览器默认缓存 (配合服务端 ETag), 数据没变时 304 空响应; force 用独立 URL 不受影响
400
+ const res = await fetch(url, { headers: { accept: "application/json" } });
401
+ if (!res.ok) throw new Error("HTTP " + res.status);
402
+ const data = await res.json();
403
+ if (data.config?.clientPollIntervalMs >= 5000) pollMs = Math.min(data.config.clientPollIntervalMs, 3600000);
404
+ // v0.5.0 性能适配: 内容无变化就不重建快照引用, useSyncExternalStore 判定 Object.is 相同直接跳过整棵树重渲
405
+ const newBalances = data.balances || [];
406
+ const oldBalances = Array.isArray(snapshot.balances) ? snapshot.balances : [];
407
+ const changed = JSON.stringify(newBalances) !== JSON.stringify(oldBalances) || snapshot.status !== "ok";
408
+ const nextFetchedAt = data.fetchedAt || Date.now();
409
+ if (!changed) {
410
+ snapshot = { ...snapshot, fetchedAt: nextFetchedAt, isRefreshing: false };
411
+ } else {
412
+ snapshot = { status: "ok", balances: newBalances, config: data.config || snapshot.config, fetchedAt: nextFetchedAt, isRefreshing: false };
413
+ }
414
+ } catch (error) {
415
+ snapshot = { status: "error", balances: snapshot.balances || [], message: error instanceof Error ? error.message : String(error), isRefreshing: false };
416
+ }
417
+ inflight = null; notify();
418
+ })();
419
+ return inflight;
420
+ }
421
+ function schedule() {
422
+ if (timer !== null) return;
423
+ timer = setTimeout(() => { timer = null; if (document.hidden) return; refresh().then(schedule, schedule); }, pollMs);
424
+ }
425
+ const store = {
426
+ subscribe(fn) { listeners.add(fn); if (!started) { started = true; refresh(true).then(schedule, schedule); } return () => { listeners.delete(fn); if (listeners.size === 0) { started = false; if (timer !== null) { clearTimeout(timer); timer = null; } } }; },
427
+ getSnapshot() { return snapshot; },
428
+ forceRefresh() { return refresh(true); },
429
+ };
430
+ //#endregion
431
+
432
+ //#region helpers
433
+ const CURRENCY_SYMBOLS = { CNY: "¥", USD: "$", EUR: "€" };
434
+ const currencySymbol = (c) => { if (c === "%" || c === "tokens") return ""; return CURRENCY_SYMBOLS[c] || c + " "; };
435
+ function formatMoney(amount, currency) {
436
+ if (typeof amount !== "number" || isNaN(amount)) return currencySymbol(currency) + "0";
437
+ if (currency === "%") return Math.round(amount) + "%";
438
+ if (currency === "tokens") return (amount >= 1e6 ? (amount/1e6).toFixed(1)+"M" : amount >= 1e3 ? (amount/1e3).toFixed(1)+"K" : Math.round(amount) + " tok");
439
+ const fixed = amount >= 1 ? 2 : amount >= 0.01 ? 3 : 4;
440
+ return currencySymbol(currency) + amount.toFixed(fixed);
441
+ }
442
+ function formatTime(ms) { if (!ms || ms <= 0) return "—"; return new Date(ms).toLocaleTimeString(); }
443
+ /** 状态条金额文字 (供 BarReadout 的 rAF 节流 state 使用) */
444
+ function barAmountText(current, t) {
445
+ if (!current) return "";
446
+ if (current.status === "ok") {
447
+ const cur = current.currency || "USD";
448
+ const num = typeof current.total === "number" ? current.total : 0;
449
+ if (String(cur).trim() === "%") return formatMoney(num, "%");
450
+ if (String(cur).trim() === "tokens") return formatMoney(num, "tokens");
451
+ return formatMoney(num, cur);
452
+ }
453
+ if (current.status === "no-key") return t("status.nokey");
454
+ if (current.status === "no-balance-api") return t("status.noBalance");
455
+ if (current.status === "local") return t("status.local");
456
+ return t("status.err");
457
+ }
458
+ /** v0.5.3: 模型名 → 平台 id 映射 (仅强特征匹配, 避免误切; 自定义/中转由调用方按名称二次匹配) */
459
+ function modelToPlatform(model) {
460
+ if (!model || typeof model !== "string") return "";
461
+ const m = model.toLowerCase();
462
+ if (/^deepseek/.test(m)) return "deepseek";
463
+ if (/glm|chatglm/.test(m)) return "zhipu";
464
+ if (/kimi|moonshot/.test(m)) return "moonshot";
465
+ if (/^step[-_]/.test(m)) return "stepfun";
466
+ if (/minimax|abab/.test(m)) return "minimax";
467
+ if (/grok/.test(m)) return "xai";
468
+ return "";
469
+ }
470
+ /** 三色等级: 百分比型(套餐剩余%)用安全/预警阈值; 真实货币金额无百分比语义, 一律 ok
471
+ * v0.5.6 修复: 旧逻辑把 ¥2.55 这类绝对金额跟 50/10 的百分比阈值比较, 导致显示「异常」 */
472
+ function getLevel(b, cfg) {
473
+ if (!b) return "err";
474
+ if (["no-key", "no-balance-api"].includes(b.status)) return "nokey";
475
+ if (b.status === "local") return "local";
476
+ if (["error", "auth-error", "parse-error"].includes(b.status)) return "err";
477
+ if (b.status !== "ok") return "err";
478
+ const safe = cfg?.safeThreshold ?? 50;
479
+ const warn = cfg?.warnThreshold ?? 10;
480
+ // v0.7.1: 修复「负数余额显示正常」。percent 与 token 数量不能用金额阈值比。
481
+ // - 百分比: 走阈值
482
+ // - 货币: 负数→err, 正数走阈值
483
+ // - tokens: 数量>0 安全, <=0 异常 (token 数是规模非金额)
484
+ const cur = String(b.currency || "").trim();
485
+ if (cur === "tokens") {
486
+ const v = typeof b.total === "number" ? b.total : (b.available ?? 0);
487
+ if (!Number.isFinite(v)) return "err";
488
+ return v > 0 ? "ok" : "err";
489
+ }
490
+ const v = typeof b.percent === "number" ? b.percent : (typeof b.total === "number" ? b.total : null);
491
+ if (!(typeof v === "number") || isNaN(v)) return "err";
492
+ return v > safe ? "ok" : v > warn ? "warn" : "err";
493
+ }
494
+ function statusLabel(level, t) { return t({ ok:"status.ok", warn:"status.warn", err:"status.err", nokey:"status.nokey", loading:"status.loading", local:"status.local", auth:"status.auth" }[level] || "status.err"); }
495
+ //#endregion
496
+
497
+
498
+ //#region 一层 bar
499
+ function BarReadout({ t, onOpen, onOpenSettings, selectedId, onSelect, config, cost }) {
500
+ const data = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
501
+ const balances = data.balances || [];
502
+ let current = balances.find(b => b.platform === selectedId);
503
+ if (!current) current = balances.find(b => b.status === "ok") || balances[0];
504
+ if (current && selectedId !== current.platform && current.status === "ok") onSelect(current.platform);
505
+
506
+ // v0.5.0 手机端: 点击状态条开看板, 长按 500ms 弹设置菜单 (替代常驻齿轮)
507
+ const pressTimer = react.useRef(null);
508
+ const longPressed = react.useRef(false);
509
+ const startLongPress = () => {
510
+ if (pressTimer.current) clearTimeout(pressTimer.current);
511
+ pressTimer.current = setTimeout(() => {
512
+ longPressed.current = true;
513
+ try { if (navigator.vibrate) navigator.vibrate(20); } catch (e) { /* ignore */ }
514
+ onOpenSettings();
515
+ }, 500);
516
+ };
517
+ const cancelLongPress = () => {
518
+ if (pressTimer.current) { clearTimeout(pressTimer.current); pressTimer.current = null; }
519
+ // 短暂保活标记, 吞掉长按松手后的 click, 避免误开看板
520
+ setTimeout(() => { longPressed.current = false; }, 80);
521
+ };
522
+ const handleBarClick = () => { if (longPressed.current) return; onOpen(); };
523
+
524
+ // v0.5.0: 金额文字 rAF 延迟一帧写入, 合并同一帧内的重复变化
525
+ const [displayAmount, setDisplayAmount] = react.useState(barAmountText(current, t));
526
+ react.useEffect(() => {
527
+ const txt = barAmountText(current, t);
528
+ const raf = requestAnimationFrame(() => setDisplayAmount((prev) => (prev === txt ? prev : txt)));
529
+ return () => cancelAnimationFrame(raf);
530
+ });
531
+
532
+ const isPeak = config?.isPeak === true;
533
+ const isWknd = config?.isWeekend === true;
534
+
535
+ // 峰谷标记 (趣味版: 梁文峰 ☀️ / 梁文谷 🌙)
536
+ let peakNode = null;
537
+ if (isPeak !== undefined) {
538
+ const peakTitle = isPeak
539
+ ? t("peak.now") + " " + t("peak.hint")
540
+ : (isWknd ? t("valley.weekend") : t("valley.now"));
541
+ peakNode = react.createElement("span", {
542
+ className: "dshadb_bar_peak " + (isPeak ? "peak" : "valley"),
543
+ key: "peak",
544
+ title: peakTitle,
545
+ }, isPeak ? "☀️" : "🌙");
546
+ }
547
+
548
+ if (!current) return react.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "4px" } }, [
549
+ react.createElement("span", {
550
+ className: "dshadb_bar", onClick: handleBarClick, role: "button", tabIndex: 0,
551
+ onKeyDown: (e) => { if (e.key === "Enter") onOpen(); },
552
+ onTouchStart: startLongPress, onTouchEnd: cancelLongPress, onTouchCancel: cancelLongPress,
553
+ onContextMenu: (e) => e.preventDefault(), key: "main"
554
+ }, [
555
+ react.createElement("span", { className: "dshadb_bar_logo", key: "logo" }, renderLogo({platform:"deepseek"})),
556
+ react.createElement("span", { className: "dshadb_bar_name", key: "name" }, t("title")),
557
+ peakNode,
558
+ react.createElement("span", { className: "dshadb_bar_cost", title: cost?.title, key: "cost" }, cost?.text || "~—"),
559
+ ]),
560
+ ]);
561
+
562
+ const meta = metaFor(current.platform), level = getLevel(current, config);
563
+ const dotClass = "dshadb_bar_dot " + (level === "ok" ? "dshadb_bar_dot_ok" : level === "warn" ? "dshadb_bar_dot_warn" : "dshadb_bar_dot_err");
564
+ const amtClass = "dshadb_bar_amount " + (level === "ok" ? "dshadb_bar_ok" : level === "warn" ? "dshadb_bar_warn" : "dshadb_bar_err");
565
+ return react.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "4px" } }, [
566
+ react.createElement("span", {
567
+ className: "dshadb_bar", onClick: handleBarClick, role: "button", tabIndex: 0,
568
+ onKeyDown: (e) => { if (e.key === "Enter") onOpen(); },
569
+ onTouchStart: startLongPress, onTouchEnd: cancelLongPress, onTouchCancel: cancelLongPress,
570
+ onContextMenu: (e) => e.preventDefault(), title: t("bar.hint") + " · " + meta.name, key: "main"
571
+ }, [
572
+ react.createElement("span", { className: "dshadb_bar_logo", key: "logo" }, renderLogo(current)),
573
+ react.createElement("span", { className: "dshadb_bar_name", key: "name" }, meta.name),
574
+ react.createElement("span", { className: amtClass, key: "amount" }, displayAmount),
575
+ react.createElement("span", { className: dotClass, key: "dot" }),
576
+ peakNode,
577
+ react.createElement("span", { className: "dshadb_bar_cost", title: cost?.title, key: "cost" }, cost?.text || "~—"),
578
+ ]),
579
+ ]);
580
+ }
581
+ //#endregion
582
+
583
+ //#region 折叠分组 (带展开过渡动画 + 分类色点)
584
+ const GROUP_DOT = { domestic: "domestic", abroad: "abroad", local: "local", relay: "relay", custom: "custom" };
585
+ function GroupSection({ label, count, open, onToggle, children }) {
586
+ return react.createElement("div", { key: label }, [
587
+ react.createElement("div", { className: "dshadb_group_head", onClick: onToggle, role: "button", tabIndex: 0, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggle(); } } }, [
588
+ react.createElement("span", { className: "dshadb_group_label", key: "label" }, label),
589
+ react.createElement("span", { style: { display: "flex", alignItems: "center", gap: "6px" }, key: "right" }, [
590
+ react.createElement("span", { className: "dshadb_group_meta", key: "meta" }, count),
591
+ react.createElement(IconChevron, { open }),
592
+ ]),
593
+ ]),
594
+ react.createElement("div", { className: "dshadb_group_body" + (open ? " dshadb_group_body_open" : ""), key: "body" }, [
595
+ react.createElement("div", { className: "dshadb_group_body_inner", key: "inner" }, [
596
+ react.createElement("div", { key: "cards" }, children),
597
+ ]),
598
+ ]),
599
+ ]);
600
+ }
601
+ //#endregion
602
+
603
+ //#region 二层 看板面板
604
+ function DashboardDrawer({ isOpen, onClose, t, selectedId, onSelect, onOpenDetail, onAddRelay, onAddCustom, onOpenSettings, config, currentModel }) {
605
+ const data = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
606
+ const [isRefreshing, setRefreshing] = react.useState(false);
607
+ const [openGroups, setOpenGroups] = react.useState({ domestic: false, abroad: false, relay: false, local: false, custom: false });
608
+ react.useEffect(() => {
609
+ if (!isOpen) return;
610
+ const handleKey = (e) => { if (e.key === "Escape") onClose(); };
611
+ window.addEventListener("keydown", handleKey);
612
+ return () => window.removeEventListener("keydown", handleKey);
613
+ }, [isOpen, onClose]);
614
+ if (!isOpen) return null;
615
+ const handleRefresh = () => { setRefreshing(true); store.forceRefresh().then(() => setTimeout(() => setRefreshing(false), 500)); };
616
+ const balances = data.balances || [];
617
+ const isLoading = data.status === "loading" && balances.length === 0;
618
+ const okCount = balances.filter(b => b.status === "ok").length;
619
+
620
+ const groups = { domestic: [], abroad: [], relay: [], custom: [] };
621
+ let selectedBalance = null;
622
+ for (const b of balances) {
623
+ const cat = b.category || metaFor(b.platform).cat;
624
+ if (b.platform === selectedId) { selectedBalance = b; continue; } // ② 选中模型从分组排除,避免重复
625
+ if (cat === "海外" || cat === "官方") groups.abroad.push(b);
626
+ else if (cat === "中转站") groups.relay.push(b);
627
+ else if (cat === "自定义") groups.custom.push(b);
628
+ else groups.domestic.push(b);
629
+ }
630
+ const toggle = (k) => setOpenGroups(prev => ({ ...prev, [k]: !prev[k] }));
631
+
632
+ const card = (b) => {
633
+ const meta = metaFor(b.platform), level = getLevel(b, config), isSelected = b.platform === selectedId;
634
+ const cardClass = "dshadb_card " + (isSelected ? "dshadb_card_active " : "");
635
+ // descText 逻辑
636
+ let descText;
637
+ if (b.status === "ok" && b.sessionCost != null) {
638
+ descText = (b.modelId || "—") + " · 会话消耗 " + (typeof b.sessionCost === "number" ? b.sessionCost.toFixed(2) : b.sessionCost);
639
+ } else if (b.status === "ok" && b.note) {
640
+ descText = b.note + " · 配额正常";
641
+ } else if (b.status === "ok") {
642
+ descText = "已启用";
643
+ } else if (b.status === "local") {
644
+ descText = "本地模型";
645
+ } else if (b.status === "noBalance" || b.status === "no-balance-api") {
646
+ descText = "自动探测余额接口";
647
+ } else {
648
+ descText = statusLabel(level, t);
649
+ }
650
+ let amount;
651
+ let amtClass = "";
652
+ if (b.status === "ok") {
653
+ const cur = b.currency || "USD";
654
+ const num = typeof b.total === "number" ? b.total : 0;
655
+ const val = String(cur).trim() === "%" ? formatMoney(num, "%") : String(cur).trim() === "tokens" ? formatMoney(num, "tokens") : formatMoney(num, cur);
656
+ amtClass = level === "ok" ? "dshadb_card_status_ok" : level === "warn" ? "dshadb_card_status_warn" : "dshadb_card_status_err";
657
+ amount = val;
658
+ } else if (b.status === "no-key") { amount = t("status.nokey"); amtClass = "dshadb_card_amount_sub"; }
659
+ else if (b.status === "no-balance-api") { amount = t("status.noBalance"); amtClass = "dshadb_card_amount_sub"; }
660
+ else if (b.status === "local") { amount = t("status.local"); amtClass = "dshadb_card_amount_sub"; }
661
+ else { amount = b.error || t("error.unknown"); amtClass = "dshadb_card_status_err"; }
662
+ const stClass = level === "ok" ? "dshadb_card_status_ok" : level === "warn" ? "dshadb_card_status_warn" : "dshadb_card_status_err";
663
+ const statusText = statusLabel(level, t);
664
+ const rightNode = react.createElement("div", { className: "dshadb_card_right", key: "right" }, [
665
+ react.createElement("span", { className: "dshadb_card_amount" + (amtClass ? " " + amtClass : ""), key: "amt" }, amount),
666
+ react.createElement("span", { className: "dshadb_card_status " + stClass, key: "st" }, [
667
+ react.createElement("span", { className: "dshadb_status_dot dshadb_status_dot_" + (level === "ok" ? "ok" : level === "warn" ? "warn" : "err"), key: "dot" }),
668
+ statusText,
669
+ ]),
670
+ ]);
671
+ return react.createElement("div", { className: cardClass, key: b.platform, onClick: () => { onSelect(b.platform); } }, [
672
+ react.createElement("span", { className: "dshadb_card_logo", key: "logo", style: { color: meta.color || undefined } }, renderLogo(b)),
673
+ react.createElement("div", { className: "dshadb_card_info", key: "info" }, [
674
+ react.createElement("div", { className: "dshadb_card_name", key: "name" }, meta.name),
675
+ react.createElement("div", { className: "dshadb_card_desc", key: "desc" }, descText),
676
+ ]),
677
+ rightNode,
678
+ isSelected ? react.createElement("span", { className: "dshadb_card_chevron", key: "chev", onClick: (e) => { e.stopPropagation(); onOpenDetail(b.platform); }, style: { cursor: "pointer", padding: "4px" } }, "›") : null,
679
+ ]);
680
+ };
681
+
682
+ let bodyContent;
683
+ if (isLoading) {
684
+ bodyContent = react.createElement("div", null, [0,1,2,3].map(i => react.createElement("div", { className: "dshadb_skeleton", key: i }, [
685
+ react.createElement("div", { className: "dshadb_skeleton_logo", key: "logo" }),
686
+ react.createElement("div", { className: "dshadb_skeleton_lines", key: "lines" }, [
687
+ react.createElement("div", { className: "dshadb_skeleton_line dshadb_skeleton_line_long", key: "l1" }),
688
+ react.createElement("div", { className: "dshadb_skeleton_line dshadb_skeleton_line_short", key: "l2" }),
689
+ ]),
690
+ ])));
691
+ } else if (balances.length === 0) {
692
+ bodyContent = react.createElement("div", { className: "dshadb_empty" }, [
693
+ react.createElement("span", { key: "icon", style: { fontSize: 30, opacity: 0.5 } }, "📊"),
694
+ react.createElement("span", { key: "title" }, t("title")),
695
+ ]);
696
+ } else {
697
+ bodyContent = [
698
+ // ⑤ 选中模型置顶 (原deepseek位置)
699
+ selectedBalance ? card(selectedBalance) : null,
700
+ selectedBalance ? react.createElement("div", { className: "dshadb_group_divider", key: "div" }) : null,
701
+ // 国内平台 (含deepseek)
702
+ groups.domestic.length ? react.createElement(GroupSection, { label: t("domestic"), count: groups.domestic.length, open: openGroups.domestic, onToggle: () => toggle("domestic"), key: "g_domestic" }, groups.domestic.map(card)) : null,
703
+ groups.abroad.length ? react.createElement(GroupSection, { label: t("abroad"), count: groups.abroad.length, open: openGroups.abroad, onToggle: () => toggle("abroad"), key: "g_abroad" }, groups.abroad.map(card)) : null,
704
+ groups.relay.length ? react.createElement(GroupSection, { label: t("relay"), count: groups.relay.length, open: openGroups.relay, onToggle: () => toggle("relay"), key: "g_relay" }, groups.relay.map(card)) : null,
705
+ groups.custom.length ? react.createElement(GroupSection, { label: t("custom"), count: groups.custom.length, open: openGroups.custom, onToggle: () => toggle("custom"), key: "g_custom" }, groups.custom.map(card)) : null,
706
+ // ③ 添加按钮居中两边对齐
707
+ react.createElement("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: "10px", marginTop: "6px" }, key: "add_btns" }, [
708
+ react.createElement("button", { type: "button", className: "dshadb_add_btn", key: "add_relay", onClick: onAddRelay }, react.createElement(IconPlus, null), t("add.relay")),
709
+ react.createElement("button", { type: "button", className: "dshadb_add_btn", key: "add_custom", onClick: onAddCustom }, react.createElement(IconPlus, null), t("add.custom")),
710
+ ]),
711
+ react.createElement("div", { style: { fontSize: "10px", color: "#9ca0aa", padding: "8px 2px 2px", lineHeight: "1.5", textAlign: "center" }, key: "hint" }, [
712
+ "💡 OpenAI、Claude、Gemini、Groq、Mistral 等平台未开放余额查询,请通过「添加自定义模型」配置。",
713
+ ]),
714
+ ];
715
+ }
716
+
717
+ return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, [
718
+ react.createElement("div", { className: "dshadb_drawer", onClick: (e) => e.stopPropagation(), key: "drawer" }, [
719
+ react.createElement("div", { className: "dshadb_handle", key: "handle" }, react.createElement("div", { className: "dshadb_handle_bar" })),
720
+ react.createElement("div", { className: "dshadb_header", key: "header" }, [
721
+ react.createElement("div", { className: "dshadb_header_left", key: "left" }, [
722
+ // 模型尾椎: 从 currentModel 提取变体名, 如 deepseek-v4-flash → DSH·V4
723
+ react.createElement("div", { className: "dshadb_header_meta", key: "meta" },
724
+ (currentModel && typeof currentModel === "string")
725
+ ? (() => {
726
+ const parts = currentModel.split(/[-_\/]/);
727
+ // 找 v4/v3/v2 等版本号, 或取最长片段
728
+ let ver = parts.find(p => /^v\d/i.test(p) || /^[23456]\.?\d*$/.test(p));
729
+ if (!ver) {
730
+ // 取最后一个有意义的片段
731
+ const meaningful = parts.filter(p => p.length > 1 && !/^(deepseek|chat|reasoner)$/i.test(p));
732
+ ver = meaningful.length > 0 ? meaningful[meaningful.length - 1] : parts[parts.length - 1];
733
+ }
734
+ return "DSH·" + (ver ? ver.toUpperCase() : "");
735
+ })()
736
+ : ""
737
+ ),
738
+ react.createElement("div", { className: "dshadb_header_title", key: "title" }, [
739
+ react.createElement("span", { className: "dshadb_title_ic", key: "ic", style: { display: "inline-flex", alignItems: "center", justifyContent: "center", width: "26px", height: "26px", borderRadius: "7px", overflow: "hidden", marginRight: "8px", verticalAlign: "middle" } }, react.createElement(IconTitle, null)),
740
+ t("title"),
741
+ ]),
742
+ react.createElement("div", { className: "dshadb_header_sub", key: "sub" },
743
+ t("all.ok", { n: okCount }) + " · " + t("refreshing")
744
+ ),
745
+ ]),
746
+ react.createElement("button", {
747
+ className: "dshadb_header_settings",
748
+ key: "settings",
749
+ onClick: onOpenSettings,
750
+ title: t("settings.title"),
751
+ "aria-label": t("settings.title"),
752
+ }, react.createElement(IconGear, null)),
753
+ ]),
754
+ react.createElement("div", { className: "dshadb_body", key: "body" }, bodyContent),
755
+ data.fetchedAt ? react.createElement("div", { className: "dshadb_footer", key: "footer" }, [
756
+ "更新于 " + formatTime(data.fetchedAt),
757
+ react.createElement("span", { className: "dshadb_footer_dot", key: "d1" }),
758
+ react.createElement("span", { className: "dshadb_refresh_badge", key: "ri" }, "每 " + ((config && config.clientPollIntervalMs) ? Math.round(config.clientPollIntervalMs / 1000) : 5) + "s 刷新"),
759
+ ]) : null,
760
+ ]),
761
+ ]);
762
+ }
763
+ //#endregion
764
+
765
+ //#region 三层 平台详情
766
+ function PlatformDetail({ isOpen, onClose, platformId, t, useProjection, config }) {
767
+ const data = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
768
+ const cost = useProjection ? useProjection("queryBalanceCost") : undefined;
769
+ // v0.5.5: 价格表 (仅 DeepSeek 有峰谷两段价), 打开详情时拉取一次
770
+ const [priceData, setPriceData] = react.useState(null);
771
+ react.useEffect(() => {
772
+ if (!isOpen || platformId !== "deepseek") return;
773
+ let live = true;
774
+ fetch("/api-dashboard/prices", { headers: { accept: "application/json" } })
775
+ .then((r) => (r.ok ? r.json() : null))
776
+ .then((d) => { if (live && d && d.ok) setPriceData(d); })
777
+ .catch(() => {});
778
+ return () => { live = false; };
779
+ }, [isOpen, platformId]);
780
+ react.useEffect(() => {
781
+ if (!isOpen) return;
782
+ const handleKey = (e) => { if (e.key === "Escape") onClose(); };
783
+ window.addEventListener("keydown", handleKey);
784
+ return () => window.removeEventListener("keydown", handleKey);
785
+ }, [isOpen, onClose]);
786
+ if (!isOpen) return null;
787
+ const b = (data.balances || []).find(x => x.platform === platformId);
788
+ if (!b) return null;
789
+ const meta = metaFor(b.platform), level = getLevel(b, config);
790
+ const row = (label, val) => react.createElement("div", { className: "dshadb_detail_row", key: label }, [ react.createElement("span", { className: "dshadb_detail_row_label" }, label), react.createElement("span", { className: "dshadb_detail_row_val" }, val) ]);
791
+ const costVal = cost && cost.cost > 0 ? formatMoney(cost.cost, cost.currency || "CNY") : "—";
792
+ const cur = b.currency || "USD";
793
+ const detailRows = [];
794
+ if (b.status === "ok") {
795
+ detailRows.push(row(t("detail.total"), formatMoney(b.percent != null ? b.percent : b.total, b.percent != null ? "%" : cur) + (b.percent != null ? "%" : "")));
796
+ // v0.5.6: 充值行仅在与余额有差异时显示 (未消耗时 充值==余额 两行重复); 使用行为 0 不显示
797
+ if (b.topup != null && Math.abs(b.topup - (b.total || 0)) > 1e-9) detailRows.push(row(t("detail.topup"), formatMoney(b.topup, cur)));
798
+ if (b.used != null && b.used > 0) detailRows.push(row(t("detail.used"), formatMoney(b.used, cur)));
799
+ if (b.note) detailRows.push(row(t("detail.note"), b.note));
800
+ if (b.staleNote) detailRows.push(row("数据状态", b.staleNote));
801
+ } else {
802
+ detailRows.push(row(t("detail.note"), b.error || "—"));
803
+ }
804
+ detailRows.push(row(t("detail.sessionCost"), costVal));
805
+
806
+ // DeepSeek 峰谷趣味卡片
807
+ let peakCard = null;
808
+ if (b.platform === 'deepseek' && config) {
809
+ const isPeak = config.isPeak === true;
810
+ const isWknd = config.isWeekend === true;
811
+ const icon = isPeak ? "☀️" : "🌙";
812
+ const mainTxt = isPeak ? t("peak.now") : (isWknd ? t("valley.weekend") : t("valley.now"));
813
+ const slogan = isPeak ? t("peak.slogan") : (isWknd ? t("weekend.slogan") : t("valley.slogan"));
814
+ peakCard = react.createElement("div", { className: "dshadb_peakcard " + (isPeak ? "peak" : "valley"), key: "peakcard" }, [
815
+ react.createElement("div", { className: "dshadb_peakcard_title", key: "title" }, [react.createElement("span", { key: "ic" }, icon), react.createElement("span", { key: "txt" }, mainTxt)]),
816
+ react.createElement("div", { className: "dshadb_peakcard_sub", key: "sub" }, isPeak ? t("peak.hint") : (isWknd ? t("valley.hint") : t("valley.hint"))),
817
+ react.createElement("div", { className: "dshadb_peakcard_slogan", key: "slogan" }, slogan),
818
+ ]);
819
+ }
820
+
821
+ // v0.5.5: DeepSeek 峰谷价格卡 (峰时 ☀️ / 谷时 🌙, 单位: 货币/百万 tokens)
822
+ let priceCard = null;
823
+ if (platformId === "deepseek" && priceData && Array.isArray(priceData.models)) {
824
+ const cur = priceData.currency || "CNY";
825
+ const sym = cur === "CNY" ? "¥" : cur === "USD" ? "$" : cur + " ";
826
+ const fmt = (v) => (typeof v !== "number" ? "—" : String(v));
827
+ const seg = (label, p, now) => !p ? null : react.createElement("div", { className: "dshadb_price_seg" + (now ? " dshadb_price_seg_now" : ""), key: label }, [
828
+ react.createElement("span", { className: "dshadb_price_seg_label", key: "l" }, label),
829
+ react.createElement("span", { className: "dshadb_price_seg_val", key: "v", title: "未命中输入 / 缓存命中 / 输出" },
830
+ fmt(p.cacheMiss) + " / " + fmt(p.cacheHit) + " / " + fmt(p.output)),
831
+ ]);
832
+ priceCard = react.createElement("div", { className: "dshadb_pricelist", key: "prices" }, [
833
+ react.createElement("div", { className: "dshadb_pricelist_title", key: "pt" }, [
834
+ "单价 (" + sym + "/M tokens)",
835
+ react.createElement("span", { className: "dshadb_price_nowtag" + (priceData.peakNow ? " peak" : " valley"), key: "tag" }, priceData.peakNow ? "当前 ☀️ 峰时" : "当前 🌙 谷时"),
836
+ ]),
837
+ ...priceData.models.map((m) => react.createElement("div", { className: "dshadb_price_row", key: m.model }, [
838
+ react.createElement("span", { className: "dshadb_price_model", key: "m" }, m.model),
839
+ react.createElement("div", { style: { display: "flex", gap: "6px" }, key: "segs" }, [
840
+ seg("☀️", m.peak, m.peakValley && priceData.peakNow),
841
+ seg("🌙", m.offPeak, m.peakValley && !priceData.peakNow),
842
+ !m.peakValley && m.flat ? react.createElement("span", { className: "dshadb_price_seg_val", key: "flat", title: "未命中输入 / 缓存命中 / 输出" }, fmt(m.flat.cacheMiss) + " / " + fmt(m.flat.cacheHit) + " / " + fmt(m.flat.output)) : null,
843
+ ]),
844
+ ])),
845
+ ]);
846
+ }
847
+
848
+ // v0.5.3: 回退为底部抽屉样式 (全屏卡片观感不佳, 复用看板同款 scrim+drawer)
849
+ return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, [
850
+ react.createElement("div", { className: "dshadb_drawer", style: { maxHeight: "70vh" }, onClick: (e) => e.stopPropagation(), key: "drawer" }, [
851
+ react.createElement("div", { className: "dshadb_handle", key: "handle" }, react.createElement("div", { className: "dshadb_handle_bar" })),
852
+ react.createElement("div", { className: "dshadb_header", key: "header" }, [
853
+ react.createElement("div", { className: "dshadb_header_left", key: "left" }, [
854
+ react.createElement("div", { className: "dshadb_header_title", key: "title", style: { fontSize: "18px" } }, meta.name),
855
+ ]),
856
+ react.createElement("button", { type: "button", className: "dshadb_header_settings", onClick: onClose, key: "close" }, react.createElement(IconClose, null)),
857
+ ]),
858
+ react.createElement("div", { className: "dshadb_body", key: "body" }, [
859
+ react.createElement("div", { className: "dshadb_detail", key: "detail" }, [
860
+ // 头部:logo + 平台名 + 状态
861
+ react.createElement("div", { className: "dshadb_detail_head", key: "head" }, [
862
+ react.createElement("span", { className: "dshadb_detail_logo", key: "logo" }, renderLogo(b)),
863
+ react.createElement("div", { key: "who" }, [
864
+ react.createElement("div", { className: "dshadb_detail_name", key: "name" }, meta.name),
865
+ react.createElement("div", { className: "dshadb_detail_name_sub", key: "sub" }, statusLabel(level, t)),
866
+ ]),
867
+ ]),
868
+ // v0.5.9: 大号余额数字 + 当前余额标签(支持百分比、token数、真实货币)
869
+ react.createElement("div", { className: "dshadb_detail_bigwrap", key: "bigwrap" }, [
870
+ react.createElement("span", { className: "dshadb_detail_big", key: "bigv" },
871
+ b.status === "ok" ? (
872
+ String(b.currency || "").trim() === "%" ? formatMoney(typeof b.total === "number" ? b.total : 0, "%") :
873
+ String(b.currency || "").trim() === "tokens" ? formatMoney(typeof b.total === "number" ? b.total : 0, "tokens") :
874
+ formatMoney(b.total, cur)
875
+ ) : "—"),
876
+ react.createElement("span", { className: "dshadb_detail_label", key: "lbl" }, t("detail.total")),
877
+ ]),
878
+ // v0.5.9: 三列指标卡网格 — 只显示有真实数据的指标(DeepSeek 没有充值/使用数据则跳过)
879
+ (() => {
880
+ if (b.status !== "ok") return null;
881
+ const metrics = [];
882
+ // 总充值:仅在 API 返回且与余额有差异时显示(DeepSeek 返回 topup==total → 不显示)
883
+ if (b.topup != null && Math.abs(b.topup - (b.total || 0)) > 1e-9) {
884
+ metrics.push(react.createElement("div", { className: "dshadb_detail_metric", key: "topup" }, [
885
+ react.createElement("div", { className: "dshadb_detail_metric_val", key: "v" }, formatMoney(b.topup, cur)),
886
+ react.createElement("div", { className: "dshadb_detail_metric_label", key: "l" }, t("detail.topup")),
887
+ ]));
888
+ }
889
+ // 总使用:仅在有实际消耗时显示
890
+ if (b.used != null && b.used > 0) {
891
+ metrics.push(react.createElement("div", { className: "dshadb_detail_metric", key: "used" }, [
892
+ react.createElement("div", { className: "dshadb_detail_metric_val", key: "v" }, formatMoney(b.used, cur)),
893
+ react.createElement("div", { className: "dshadb_detail_metric_label", key: "l" }, t("detail.used")),
894
+ ]));
895
+ }
896
+ // 类型:余额(真实货币)或 套餐(百分比),字体与三列指标卡对齐
897
+ if (b.status === "ok") {
898
+ const typeLabel = (b.percent != null || String(b.currency || "").trim() === "%") ? "套餐" : "余额";
899
+ metrics.push(react.createElement("div", { className: "dshadb_detail_metric", key: "type" }, [
900
+ react.createElement("div", { className: "dshadb_detail_metric_val", key: "v" }, typeLabel),
901
+ react.createElement("div", { className: "dshadb_detail_metric_label", key: "l" }, t("detail.note")),
902
+ ]));
903
+ }
904
+ // 本会话消耗:始终显示
905
+ metrics.push(react.createElement("div", { className: "dshadb_detail_metric", key: "session" }, [
906
+ react.createElement("div", { className: "dshadb_detail_metric_val", key: "v" }, costVal),
907
+ react.createElement("div", { className: "dshadb_detail_metric_label", key: "l" }, t("detail.sessionCost")),
908
+ ]));
909
+ return metrics.length > 0 ? react.createElement("div", { className: "dshadb_detail_metrics", key: "metrics" }, metrics) : null;
910
+ })(),
911
+ // staleNote(暂存数据提示)
912
+ b.staleNote ? row("数据状态", b.staleNote) : null,
913
+ b.status !== "ok" ? row(t("detail.note"), b.error || "—") : null,
914
+ peakCard,
915
+ priceCard,
916
+ ]),
917
+ ]),
918
+ ]),
919
+ ]);
920
+ }
921
+ //#endregion
922
+
923
+ //#region 设置面板 (安全阈值 + 中转站)
924
+ function SettingsModal({ isOpen, onClose, onBack, t, config, initialSection }) {
925
+ const [section, setSection] = react.useState(initialSection || "basic");
926
+ const [relays, setRelays] = react.useState([]);
927
+ const [models, setModels] = react.useState([]);
928
+ const [safe, setSafe] = react.useState(50);
929
+ const [warn, setWarn] = react.useState(10);
930
+ const [currency, setCurrency] = react.useState("CNY");
931
+ const [refreshSec, setRefreshSec] = react.useState(5);
932
+ const [form, setForm] = react.useState({ name: "", baseUrl: "", apiKey: "", queryType: "auto" });
933
+ const [mform, setMform] = react.useState({ name: "", apiUrl: "", apiKey: "", queryType: "auto", totalPath: "", usedPath: "", currency: "" });
934
+ const [loading, setLoading] = react.useState(false);
935
+ const [saving, setSaving] = react.useState(false);
936
+ // v0.6.2: 初值也用预热的检查结果,并直接推导出提示文案(已是最新也可见"✓"),
937
+ // 避免打开瞬间空渲染再闪烁成提示; SSR/无 window 环境安全兜底.
938
+ const initInfo = typeof window !== "undefined" ? (window.__dshadbUpdateInfo || null) : null;
939
+ const [upd, setUpd] = react.useState({
940
+ phase: "idle", info: initInfo,
941
+ msg: initInfo && initInfo.ok && !initInfo.hasUpdate ? t("update.latest") : "",
942
+ });
943
+ const checkUpdate = react.useCallback((force) => {
944
+ setUpd(s => ({ ...s, phase: "checking" }));
945
+ fetch("/api-dashboard/update" + (force ? "?force=1" : ""), { headers: { accept: "application/json" } })
946
+ .then(r => r.json())
947
+ .then(d => {
948
+ // v0.6.2: 检查完必有反馈 —— 已是最新也提示, 有新版提示版本号, 失败给固定文案
949
+ const msg = d.ok ? (d.hasUpdate ? "" : t("update.latest")) : t("update.checkfail");
950
+ setUpd({ phase: "idle", info: d, msg });
951
+ if (typeof window !== "undefined") { window.__dshadbUpdateInfo = d; window.__dshadbUpdateCheckedAt = Date.now(); }
952
+ })
953
+ .catch(() => setUpd(s => ({ ...s, phase: "idle", msg: t("update.checkfail") })));
954
+ }, []);
955
+ react.useEffect(() => { if (isOpen) checkUpdate(false); }, [isOpen]); // 打开面板自动检查
956
+ const installUpdate = () => {
957
+ if (!window.confirm(t("update.available") + " v" + (upd.info?.remote || "") + " ?")) return;
958
+ setUpd(s => ({ ...s, phase: "installing" }));
959
+ fetch("/api-dashboard/update/install", { method: "POST" })
960
+ .then(r => r.json().then(d => ({ code: r.status, body: d })))
961
+ .then(({ code, body }) => {
962
+ if (code === 200 && body.ok) {
963
+ setUpd({ phase: "done", info: { ok: true, current: body.installed, remote: body.installed, hasUpdate: false }, msg: t("update.done") });
964
+ } else {
965
+ setUpd(s => ({ ...s, phase: "fail", msg: body.error === "already up to date" ? t("update.latest") : t("update.fail") }));
966
+ checkUpdate(true);
967
+ }
968
+ })
969
+ .catch(() => setUpd(s => ({ ...s, phase: "fail", msg: t("update.fail") })));
970
+ };
971
+ // v0.6.1: 初始化仅在弹窗"从关到开"的瞬间执行一次。
972
+ // 旧写法把 config 放进依赖数组 —— 看板轮询每 ~5s 换一次快照引用,
973
+ // 导致打开期间 effect 反复重跑: 页签被拽回 basic、编辑中的表单被打回。
974
+ const onceRef = react.useRef(false);
975
+ react.useEffect(() => {
976
+ if (!isOpen) { onceRef.current = false; return; }
977
+ if (onceRef.current) return; // 打开期间的重渲/轮询不再重置任何状态
978
+ onceRef.current = true;
979
+ if (initialSection) setSection(initialSection);
980
+ setLoading(true);
981
+ setSafe(config?.safeThreshold ?? 50);
982
+ setWarn(config?.warnThreshold ?? 10);
983
+ setCurrency(config?.currency ?? "CNY");
984
+ setRefreshSec(Math.round((config?.clientPollIntervalMs || 5000) / 1000) || 5);
985
+ let cancelled = false;
986
+ fetch("/api-dashboard/config", { cache: "no-store" }).then(r => r.json()).then(d => {
987
+ if (cancelled) return;
988
+ if (d.ok) {
989
+ setRelays(d.customRelays || []);
990
+ setModels(d.customModels || []);
991
+ if (d.refreshIntervalSec) setRefreshSec(d.refreshIntervalSec);
992
+ }
993
+ }).finally(() => { if (!cancelled) setLoading(false); });
994
+ return () => { cancelled = true; };
995
+ }, [isOpen]);
996
+ if (!isOpen) return null;
997
+ const save = async () => {
998
+ setSaving(true);
999
+ try {
1000
+ await fetch("/api-dashboard/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({
1001
+ customRelays: relays, customModels: models,
1002
+ safeThreshold: Number(safe), warnThreshold: Number(warn), currency,
1003
+ refreshIntervalSec: Math.min(Math.max(Number(refreshSec) || 5, 5), 60),
1004
+ }) });
1005
+ onClose();
1006
+ } finally { setSaving(false); }
1007
+ };
1008
+ const addRelay = () => {
1009
+ if (!form.name || !form.baseUrl) return;
1010
+ setRelays([...relays, { id: Math.random().toString(36).slice(2), name: form.name, baseUrl: form.baseUrl.replace(/\/+$/, ''), apiKey: form.apiKey, queryType: form.queryType }]);
1011
+ setForm({ name: "", baseUrl: "", apiKey: "", queryType: "auto" });
1012
+ };
1013
+ const removeRelay = (id) => setRelays(relays.filter(r => r.id !== id));
1014
+ const addModel = () => {
1015
+ if (!mform.name || !mform.apiUrl) return;
1016
+ setModels([...models, { id: Math.random().toString(36).slice(2), name: mform.name, apiUrl: mform.apiUrl.trim(), apiKey: mform.apiKey, queryType: mform.queryType, totalPath: mform.totalPath.trim(), usedPath: mform.usedPath.trim(), currency: mform.currency.trim() || "CNY" }]);
1017
+ setMform({ name: "", apiUrl: "", apiKey: "", queryType: "auto", totalPath: "", usedPath: "", currency: "" });
1018
+ };
1019
+ const removeModel = (id) => setModels(models.filter(m => m.id !== id));
1020
+ const qtLabel = (q) => t("qt." + (q || "auto"));
1021
+
1022
+ const tab = (id, label) => react.createElement("button", { type: "button", className: "dshadb_tab" + (section === id ? " dshadb_tab_active" : ""), onClick: () => setSection(id), key: id }, label);
1023
+ const tabsNode = react.createElement("div", { className: "dshadb_tabs", key: "tabs" }, [
1024
+ tab("basic", t("settings.section.basic")),
1025
+ tab("relays", t("settings.section.relays")),
1026
+ tab("models", t("settings.section.models")),
1027
+ ]);
1028
+
1029
+ // 基础设置: 阈值 / 币种 / 刷新间隔
1030
+ const basicSection = react.createElement("div", { className: "dshadb_settings_section", key: "s_basic" }, [
1031
+ react.createElement("div", { className: "dshadb_grid2", key: "thresh" }, [
1032
+ react.createElement("div", { key: "safe" }, [
1033
+ react.createElement("label", { className: "dshadb_label", key: "l1" }, t("settings.safe")),
1034
+ react.createElement("input", { className: "dshadb_field", type: "number", value: safe, onChange: (e) => setSafe(e.target.value), key: "i1" }),
1035
+ react.createElement("span", { style: { fontSize: "10px", color: "var(--dsw-alias-label-tertiary)" }, key: "h1" }, t("settings.safeHint")),
1036
+ ]),
1037
+ react.createElement("div", { key: "warn" }, [
1038
+ react.createElement("label", { className: "dshadb_label", key: "l2" }, t("settings.warn")),
1039
+ react.createElement("input", { className: "dshadb_field", type: "number", value: warn, onChange: (e) => setWarn(e.target.value), key: "i2" }),
1040
+ react.createElement("span", { style: { fontSize: "10px", color: "var(--dsw-alias-label-tertiary)" }, key: "h2" }, t("settings.warnHint")),
1041
+ ]),
1042
+ ]),
1043
+ react.createElement("div", { className: "dshadb_grid2", key: "misc" }, [
1044
+ react.createElement("div", { key: "cur" }, [
1045
+ react.createElement("label", { className: "dshadb_label", key: "cur_l" }, t("settings.currency")),
1046
+ react.createElement("select", { className: "dshadb_field", value: currency, onChange: (e) => setCurrency(e.target.value), key: "cur_s" }, [
1047
+ react.createElement("option", { value: "CNY", key: "c1" }, t("cmn")),
1048
+ react.createElement("option", { value: "USD", key: "c2" }, t("usd")),
1049
+ ]),
1050
+ ]),
1051
+ react.createElement("div", { key: "refresh" }, [
1052
+ react.createElement("label", { className: "dshadb_label", key: "rf_l" }, t("settings.refresh")),
1053
+ react.createElement("input", { className: "dshadb_field", type: "number", min: 5, max: 60, step: 5, value: refreshSec, onChange: (e) => setRefreshSec(e.target.value), key: "rf_i" }),
1054
+ react.createElement("span", { style: { fontSize: "10px", color: "var(--dsw-alias-label-tertiary)" }, key: "rf_h" }, t("settings.refreshHint")),
1055
+ ]),
1056
+ ]),
1057
+ ]);
1058
+
1059
+ // 中转站区块
1060
+ const relaySection = react.createElement("div", { className: "dshadb_settings_section", key: "s_relays" }, [
1061
+ react.createElement("div", { style: { marginTop: "2px", marginBottom: "8px" }, key: "relay_form" }, [
1062
+ react.createElement("label", { className: "dshadb_label", key: "r1" }, "中转站名称"),
1063
+ react.createElement("input", { className: "dshadb_field", value: form.name, placeholder: "我的中转站", onChange: (e) => setForm({ ...form, name: e.target.value }), key: "ri1" }),
1064
+ react.createElement("label", { className: "dshadb_label", key: "r1" }, "中转站名称"),
1065
+ react.createElement("input", { className: "dshadb_field", value: form.name, placeholder: "我的中转站", onChange: (e) => setForm({ ...form, name: e.target.value }), key: "ri1" }),
1066
+ react.createElement("label", { className: "dshadb_label", key: "r2" }, "Base URL"),
1067
+ react.createElement("input", { className: "dshadb_field", value: form.baseUrl, placeholder: "https://api.xxx.com", onChange: (e) => setForm({ ...form, baseUrl: e.target.value }), key: "ri2" }),
1068
+ react.createElement("label", { className: "dshadb_label", key: "r3" }, "API Key"),
1069
+ react.createElement("input", { className: "dshadb_field", value: form.apiKey, placeholder: "sk-xxx", onChange: (e) => setForm({ ...form, apiKey: e.target.value }), key: "ri3" }),
1070
+ react.createElement("button", { type: "button", className: "dshadb_add_btn", onClick: addRelay, key: "addbtn" }, react.createElement(IconPlus, null), t("add.relay")),
1071
+ ]),
1072
+ relays.length > 0 ? react.createElement("div", { key: "list" }, [
1073
+ react.createElement("div", { style: { fontSize: "11px", fontWeight: "700", color: "#777b84", padding: "4px 2px", margin: "4px 0 6px", display: "flex", alignItems: "center", gap: "6px" }, key: "gsec" }, t("relay")),
1074
+ relays.map(r => react.createElement("div", { className: "dshadb_card", key: r.id }, [
1075
+ react.createElement("span", { className: "dshadb_card_logo", key: "logo" }, renderLogo({platform:"relay"})),
1076
+ react.createElement("div", { className: "dshadb_card_info", key: "info" }, [
1077
+ react.createElement("div", { className: "dshadb_card_name", key: "name" }, r.name),
1078
+ react.createElement("div", { className: "dshadb_card_status", key: "status" }, r.baseUrl),
1079
+ ]),
1080
+ react.createElement("button", { type: "button", className: "dshadb_icon_btn", onClick: () => removeRelay(r.id), key: "del", title: "删除" }, react.createElement(IconTrash, null)),
1081
+ ])),
1082
+ ]) : react.createElement("div", { className: "dshadb_empty", style: { padding: "14px" }, key: "empty" }, t("relay") + " —"),
1083
+ ]);
1084
+
1085
+ // 自定义模型区块
1086
+ const modelSection = react.createElement("div", { className: "dshadb_settings_section", key: "s_models" }, [
1087
+ react.createElement("div", { style: { fontSize: "11px", fontWeight: "700", color: "#777b84", margin: "4px 0 6px", display: "flex", alignItems: "center", gap: "6px" }, key: "hint" }, t("settings.modelHint")),
1088
+ react.createElement("div", { style: { marginBottom: "8px" }, key: "model_form" }, [
1089
+ react.createElement("label", { className: "dshadb_label", key: "m1" }, t("settings.modelName")),
1090
+ react.createElement("input", { className: "dshadb_field", value: mform.name, placeholder: "我的模型余额", onChange: (e) => setMform({ ...mform, name: e.target.value }), key: "mi1" }),
1091
+ react.createElement("label", { className: "dshadb_label", key: "m2" }, t("settings.apiUrl")),
1092
+ react.createElement("input", { className: "dshadb_field", value: mform.apiUrl, placeholder: "https://api.example.com/balance", onChange: (e) => setMform({ ...mform, apiUrl: e.target.value }), key: "mi2" }),
1093
+ react.createElement("div", { className: "dshadb_grid2", key: "mrow1" }, [
1094
+ react.createElement("div", { key: "m3" }, [
1095
+ react.createElement("label", { className: "dshadb_label", key: "ml3" }, "API Key"),
1096
+ react.createElement("input", { className: "dshadb_field", value: mform.apiKey, placeholder: "可选", onChange: (e) => setMform({ ...mform, apiKey: e.target.value }), key: "mi3" }),
1097
+ ]),
1098
+ react.createElement("div", { key: "m4" }, [
1099
+ react.createElement("label", { className: "dshadb_label", key: "ml4" }, t("settings.queryType")),
1100
+ react.createElement("select", { className: "dshadb_field", value: mform.queryType, onChange: (e) => setMform({ ...mform, queryType: e.target.value }), key: "mi4" }, [
1101
+ react.createElement("option", { value: "auto", key: "q0" }, t("qt.auto")),
1102
+ react.createElement("option", { value: "openai", key: "q1" }, t("qt.openai")),
1103
+ react.createElement("option", { value: "quota", key: "q2" }, t("qt.quota")),
1104
+ react.createElement("option", { value: "deepseek", key: "q3" }, t("qt.deepseek")),
1105
+ react.createElement("option", { value: "openai-billing", key: "q4" }, t("qt.openai-billing")),
1106
+ react.createElement("option", { value: "custom", key: "q5" }, t("qt.custom")),
1107
+ ]),
1108
+ ]),
1109
+ ]),
1110
+ mform.queryType === "custom" ? react.createElement("div", { className: "dshadb_grid2", key: "mrow2" }, [
1111
+ react.createElement("div", { key: "m5" }, [
1112
+ react.createElement("label", { className: "dshadb_label", key: "ml5" }, t("settings.totalPath")),
1113
+ react.createElement("input", { className: "dshadb_field", value: mform.totalPath, placeholder: "data.balance", onChange: (e) => setMform({ ...mform, totalPath: e.target.value }), key: "mi5" }),
1114
+ ]),
1115
+ react.createElement("div", { key: "m6" }, [
1116
+ react.createElement("label", { className: "dshadb_label", key: "ml6" }, t("settings.usedPath")),
1117
+ react.createElement("input", { className: "dshadb_field", value: mform.usedPath, placeholder: "可选", onChange: (e) => setMform({ ...mform, usedPath: e.target.value }), key: "mi6" }),
1118
+ ]),
1119
+ ]) : react.createElement("div", { className: "dshadb_grid2", key: "mrow2b" }, [
1120
+ react.createElement("div", { key: "m5" }, [
1121
+ react.createElement("label", { className: "dshadb_label", key: "ml5" }, t("settings.totalPath")),
1122
+ react.createElement("input", { className: "dshadb_field", value: mform.totalPath, placeholder: "可选, 如 data.balance", onChange: (e) => setMform({ ...mform, totalPath: e.target.value }), key: "mi5" }),
1123
+ ]),
1124
+ react.createElement("div", { key: "m7" }, [
1125
+ react.createElement("label", { className: "dshadb_label", key: "ml7" }, t("settings.currency")),
1126
+ react.createElement("select", { className: "dshadb_field", value: mform.currency || "CNY", onChange: (e) => setMform({ ...mform, currency: e.target.value }), key: "mi7" }, [
1127
+ react.createElement("option", { value: "CNY", key: "cc1" }, t("cmn")),
1128
+ react.createElement("option", { value: "USD", key: "cc2" }, t("usd")),
1129
+ ]),
1130
+ ]),
1131
+ ]),
1132
+ react.createElement("button", { type: "button", className: "dshadb_add_btn", onClick: addModel, key: "addbtn2" }, react.createElement(IconPlus, null), t("add.custom")),
1133
+ ]),
1134
+ models.length > 0 ? react.createElement("div", { key: "mlist" }, [
1135
+ react.createElement("div", { style: { fontSize: "11px", fontWeight: "700", color: "#777b84", padding: "4px 2px", margin: "4px 0 6px", display: "flex", alignItems: "center", gap: "6px" }, key: "gsec2" }, t("custom")),
1136
+ models.map(m => react.createElement("div", { className: "dshadb_card", key: m.id }, [
1137
+ react.createElement("span", { className: "dshadb_card_logo", key: "logo" }, renderLogo({platform:"relay"})),
1138
+ react.createElement("div", { className: "dshadb_card_info", key: "info" }, [
1139
+ react.createElement("div", { className: "dshadb_card_name", key: "name" }, m.name),
1140
+ react.createElement("div", { className: "dshadb_card_status", key: "status" }, (m.apiUrl || "") + (m.queryType ? " · " + qtLabel(m.queryType) : "")),
1141
+ ]),
1142
+ react.createElement("button", { type: "button", className: "dshadb_icon_btn", onClick: () => removeModel(m.id), key: "del", title: "删除" }, react.createElement(IconTrash, null)),
1143
+ ])),
1144
+ ]) : null,
1145
+ ]);
1146
+
1147
+ const output = react.createElement("div", { className: "dshadb_drawer", style: { maxHeight: "86vh" }, onClick: (e) => e.stopPropagation(), key: "drawer" }, [
1148
+ react.createElement("div", { className: "dshadb_handle", key: "handle" }, react.createElement("div", { className: "dshadb_handle_bar" })),
1149
+ react.createElement("div", { className: "dshadb_header", key: "header" }, [
1150
+ react.createElement("div", { style: { display: "flex", alignItems: "center", gap: "10px", flex: "1", minWidth: 0 }, key: "leftwrap" }, [
1151
+ onBack ? react.createElement("button", { type: "button", className: "dshadb_header_back", onClick: onBack, key: "back", title: t("back") }, react.createElement(IconBack, null)) : null,
1152
+ react.createElement("div", { className: "dshadb_header_left", key: "left", style: { flex: "1" } }, [
1153
+ react.createElement("div", { className: "dshadb_header_title", key: "title", style: { fontSize: "18px" } }, t("settings.title")),
1154
+ ]),
1155
+ ]),
1156
+ react.createElement("button", { type: "button", className: "dshadb_header_settings", onClick: onClose, key: "close" }, react.createElement(IconClose, null)),
1157
+ ]),
1158
+ react.createElement("div", { className: "dshadb_body", key: "body" }, [
1159
+ tabsNode,
1160
+ section === "basic" ? basicSection : section === "relays" ? relaySection : modelSection,
1161
+ react.createElement("button", { type: "button", className: "dshadb_add_btn", onClick: save, disabled: saving, key: "save", style: { width: "100%", borderStyle: "solid", background: "#17181c", color: "#fff", borderColor: "#17181c" } }, saving ? t("refreshing") : t("settings.save")),
1162
+ // v0.6.0: 版本与更新区块
1163
+ react.createElement("div", { key: "upd", style: { marginTop: "12px", paddingTop: "10px", borderTop: "1px solid #e7e8ec" } }, [
1164
+ react.createElement("div", { style: { fontSize: "11px", fontWeight: "700", color: "#777b84", marginBottom: "8px" }, key: "upd_t" }, t("update.title")),
1165
+ react.createElement("div", { key: "upd_row", style: { display: "flex", alignItems: "center", gap: "8px", fontSize: "13px", color: "#17181c", fontWeight: "600" } }, [
1166
+ react.createElement("span", { key: "upd_v" },
1167
+ t("update.current") + ": v" + (upd.info?.current || "?")),
1168
+ upd.info?.hasUpdate ? react.createElement("span", { key: "upd_badge", style: { color: "#35b56b", fontSize: "12px", fontWeight: "700" } }, "→ v" + upd.info.remote) : null,
1169
+ ]),
1170
+ react.createElement("div", { key: "upd_btns", style: { display: "flex", gap: "8px", marginTop: "8px" } }, [
1171
+ react.createElement("button", { type: "button", className: "dshadb_add_btn", onClick: () => checkUpdate(true), disabled: upd.phase === "checking" || upd.phase === "installing", key: "upd_chk", style: { flex: 1, padding: "8px", fontSize: "12px" } },
1172
+ upd.phase === "checking" ? t("update.checking") : t("update.check")),
1173
+ upd.info?.hasUpdate ? react.createElement("button", { type: "button", className: "dshadb_save_btn", onClick: installUpdate, disabled: upd.phase === "installing", key: "upd_go", style: { flex: 1, padding: "8px", fontSize: "12px" } },
1174
+ upd.phase === "installing" ? t("update.installing") : t("update.install") + " v" + upd.info.remote) : null,
1175
+ ]),
1176
+ upd.msg || upd.info?.hasUpdate ? react.createElement("div", { key: "upd_msg", style: { color: upd.phase === "fail" ? "#e05252" : "#777b84", fontSize: "11px", marginTop: "6px" } },
1177
+ upd.msg || (upd.info?.hasUpdate ? t("update.available") + " v" + upd.info.remote : null)) : null,
1178
+ ]),
1179
+ ]),
1180
+ ]);
1181
+ return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, output);
1182
+ }
1183
+ //#endregion
1184
+
1185
+ //#region plugin
1186
+ const inject = ["slots", "locale", "modelDirectories"];
1187
+ function apply(ctx) {
1188
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-api-dashboard: dictionaries");
1189
+ const getSelected = () => { try { return localStorage.getItem("dsh-api-dashboard:selected") || ""; } catch { return ""; } };
1190
+ const setSelected = (id) => { try { localStorage.setItem("dsh-api-dashboard:selected", id); } catch {} };
1191
+
1192
+ ctx.slots.inject("conversation.composer.dock", () => {
1193
+ const dispose = ctx.slots.register({
1194
+ name: "conversation.composer.dock",
1195
+ id: "dsh-api-dashboard",
1196
+ order: 5,
1197
+ locale: NS,
1198
+ // v0.5.15: 注入模型目录 store — 选中模型瞬间即同步平台, 消除等投影的窗口延迟
1199
+ inject: (sessionId) => {
1200
+ const models = ctx.modelDirectories;
1201
+ if (models && sessionId !== void 0) {
1202
+ try {
1203
+ const dir = models.directoryFor(sessionId);
1204
+ if (dir && dir.store) return { modelStore: dir.store };
1205
+ } catch (e) { /* 忽略 */ }
1206
+ }
1207
+ return { modelStore: void 0 };
1208
+ }
1209
+ }, function DashboardSlot(props) {
1210
+ const t = props.t;
1211
+ const useProjection = props.useProjection;
1212
+ const modelStore = props.modelStore;
1213
+ // 本会话消耗 (参考 dsh-balance: 无条件读取投影, 放在组件顶部保证 hooks 顺序)
1214
+ const cost = useProjection ? useProjection("queryBalanceCost") : undefined;
1215
+ const [view, setView] = react.useState("bar");
1216
+ const [isSettingsOpen, setSettingsOpen] = react.useState(false);
1217
+ const [settingsSection, setSettingsSection] = react.useState("basic");
1218
+ const [detailId, setDetailId] = react.useState("");
1219
+ const [selectedId, setSelectedId] = react.useState(getSelected());
1220
+ const dashData = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1221
+ const config = dashData.config;
1222
+
1223
+ const handleSelect = (id) => { setSelected(id); setSelectedId(id); };
1224
+
1225
+ // v0.5.15: 按模型名切到对应平台的核心逻辑 (两条信号共用)
1226
+ const applyModelToPlatform = react.useCallback((m) => {
1227
+ if (!m || typeof m !== "string") return;
1228
+ let pid = modelToPlatform(m);
1229
+ if (!pid) {
1230
+ try {
1231
+ const low = m.toLowerCase();
1232
+ const hit = (dashData.balances || []).find((b) =>
1233
+ (b.category === "中转站" || b.category === "自定义") && b.platform && low.includes(String(b.platform).toLowerCase()));
1234
+ pid = hit ? hit.platform : "";
1235
+ } catch (e) { pid = ""; }
1236
+ }
1237
+ if (pid && pid !== selectedId) handleSelect(pid);
1238
+ }, [selectedId, dashData.balances]);
1239
+
1240
+ // v0.5.15: 即时信号 — 订阅模型目录 store, 选中模型瞬间就切平台 (不再等投影/请求)
1241
+ const modelDirSub = modelStore && typeof modelStore.subscribe === "function" ? modelStore.subscribe : () => () => {};
1242
+ const modelDirSnap = modelStore && typeof modelStore.getSnapshot === "function" ? modelStore.getSnapshot : () => void 0;
1243
+ const modelDirModel = react.useSyncExternalStore(modelDirSub, modelDirSnap, modelDirSnap)?.current?.model;
1244
+ const [didModelDir, setDidModelDir] = react.useState(false);
1245
+ react.useEffect(() => {
1246
+ const m = modelDirModel;
1247
+ if (!m) return;
1248
+ if (!didModelDir) { setDidModelDir(true); return; }
1249
+ applyModelToPlatform(m);
1250
+ }, [modelDirModel, didModelDir]);
1251
+
1252
+ // v0.5.3: 自动联动 — 会话当前模型变化时, 输入框下方平台跟着切换
1253
+ // 仅在模型实际变化时触发一次, 用户手动选择的平台不会被后续渲染覆盖
1254
+ const [lastLinkedModel, setLastLinkedModel] = react.useState(null);
1255
+ react.useEffect(() => {
1256
+ const m = cost && cost.currentModel;
1257
+ if (!m || typeof m !== "string" || m === lastLinkedModel) return;
1258
+ setLastLinkedModel(m);
1259
+ applyModelToPlatform(m);
1260
+ }, [cost && cost.currentModel]);
1261
+ const currentModel = cost && cost.currentModel;
1262
+ const openList = () => setView("list");
1263
+ const openDetail = (id) => { setDetailId(id); setView("detail"); };
1264
+ const closeDetail = () => setView("list");
1265
+ const closeList = () => setView("bar");
1266
+ const openSettings = (section) => { setSettingsSection(section || "basic"); setView("bar"); setSettingsOpen(true); };
1267
+
1268
+ // v0.6.0: 启动自动检查更新 (一次静默请求, 服务端 5 分钟缓存; 结果供设置弹窗直接显示)
1269
+ react.useEffect(() => {
1270
+ const cachedAt = window.__dshadbUpdateCheckedAt || 0;
1271
+ if (Date.now() - cachedAt < 5 * 60 * 1000) return; // 会话内查过就不重复
1272
+ fetch("/api-dashboard/update", { headers: { accept: "application/json" } })
1273
+ .then((r) => r.json())
1274
+ .then((d) => { if (d && d.ok) { window.__dshadbUpdateInfo = d; window.__dshadbUpdateCheckedAt = Date.now(); } })
1275
+ .catch(() => { /* 网络异常静默, 打开设置时可见固定错误文案 */ });
1276
+ }, []);
1277
+
1278
+ // 本会话消耗节点 (无条件渲染, waiting=true 显示 ~— 等待数据, 否则精确到6位)
1279
+ let costText = "~—"
1280
+ let costTitle = t("detail.sessionCost") + " — 等待数据"
1281
+ if (cost !== undefined && typeof cost.cost === 'number' && cost.waiting !== true) {
1282
+ const amount = cost.cost
1283
+ const cur = cost.currency || "CNY"
1284
+ const sym = cur === "CNY" ? "¥" : cur === "USD" ? "$" : cur + " "
1285
+ const fixed = amount >= 1 ? 2 : amount >= 0.01 ? 3 : amount >= 0.001 ? 4 : amount >= 0.0001 ? 5 : 6
1286
+ costText = "~" + sym + amount.toFixed(fixed)
1287
+ costTitle = "本会话消耗: " + sym + amount.toFixed(6) + (cost.models && cost.models.length ? " | 模型: " + cost.models.join(", ") : "")
1288
+ } else if (cost !== undefined && cost.waiting === true) {
1289
+ costText = "~—"
1290
+ costTitle = "本会话消耗 — 等待数据, 发消息后自动更新"
1291
+ }
1292
+ const costDisp = { text: costText, title: costTitle };
1293
+
1294
+ return react.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "2px", maxWidth: "100%" } }, [
1295
+ // v0.5.7: 本会话消耗并入同一个胶囊, 不再裸露在外
1296
+ react.createElement(BarReadout, { t, onOpen: openList, onOpenSettings: () => openSettings("basic"), selectedId, onSelect: handleSelect, config, cost: costDisp, key: "bar" }),
1297
+ react.createElement(DashboardDrawer, { isOpen: view === "list", onClose: closeList, t, selectedId, onSelect: handleSelect, onOpenDetail: (id) => { setDetailId(id); setView("detail"); }, onAddRelay: () => openSettings("relays"), onAddCustom: () => openSettings("models"), onOpenSettings: () => openSettings("basic"), config, currentModel, key: "drawer" }),
1298
+ react.createElement(PlatformDetail, { isOpen: view === "detail", onClose: closeDetail, platformId: detailId, t, useProjection, config, key: "detail" }),
1299
+ react.createElement(SettingsModal, { isOpen: isSettingsOpen, onClose: () => setSettingsOpen(false), onBack: () => { setSettingsOpen(false); setView("list"); }, t, config, initialSection: settingsSection, key: "settings" }),
1300
+ ]);
1301
+ });
1302
+ return () => { dispose(); };
1303
+ });
1304
+
1305
+ ctx.effect(() => {
1306
+ const onVisibility = () => { if (!document.hidden) refresh(true).then(schedule, schedule); };
1307
+ document.addEventListener("visibilitychange", onVisibility);
1308
+ return () => document.removeEventListener("visibilitychange", onVisibility);
1309
+ }, "dsh-api-dashboard: visibility resume");
1310
+ }
1311
+ //#endregion
1312
+
1313
+ exports.apply = apply;
1314
+ exports.inject = inject;
1315
+ return module.exports;
1316
+ }
1317
+ });