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.
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/assets/icon.png +0 -0
- package/assets/icons/alibaba.svg +1 -0
- package/assets/icons/claude.svg +1 -0
- package/assets/icons/deepseek.svg +1 -0
- package/assets/icons/gemini.svg +1 -0
- package/assets/icons/groq.svg +1 -0
- package/assets/icons/icons.js +17 -0
- package/assets/icons/mistral.svg +1 -0
- package/assets/icons/moonshot.svg +1 -0
- package/assets/icons/ollama.svg +1 -0
- package/assets/icons/openai.svg +1 -0
- package/assets/icons/openrouter.svg +1 -0
- package/assets/icons/qwen.svg +1 -0
- package/assets/icons/relay.svg +1 -0
- package/assets/icons/siliconflow.svg +1 -0
- package/assets/icons/together.svg +1 -0
- package/assets/icons/zhipu.svg +1 -0
- package/client/client.js +1317 -0
- package/cordis.patch.yml +8 -0
- package/package.json +66 -0
- package/src/index.js +1202 -0
package/client/client.js
ADDED
|
@@ -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
|
+
});
|