dsh-api-dashboard 1.1.2 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +138 -2
  2. package/client/client.js +716 -142
  3. package/package.json +1 -1
  4. package/src/index.js +1298 -182
package/client/client.js CHANGED
@@ -25,11 +25,15 @@ window.__ModuleLoader__.load({
25
25
  const ICONS = {
26
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
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
+ 'doubao': '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 64 64"><image href="data:image/png;base64,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" width="64" height="64"/></svg>',
28
29
  '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>',
30
+ 'hunyuan': '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 31 30" fill="none"><path d="M15.2287 30C23.4603 30 30.1333 23.2843 30.1333 15C30.1333 6.71573 23.4603 0 15.2287 0C6.9972 0 0.324219 6.71573 0.324219 15C0.324219 23.2843 6.9972 30 15.2287 30Z" fill="#B3DDF2"/><path d="M15.2318 29.9966C19.3467 29.9966 22.6824 26.6395 22.6824 22.4983C22.6824 18.3571 19.3467 15 15.2318 15C11.117 15 7.78125 18.3571 7.78125 22.4983C7.78125 26.6395 11.117 29.9966 15.2318 29.9966Z" fill="#0053E0"/><mask id="mask0_621_3115" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="31" height="30"><path d="M15.2326 30C23.4642 30 30.1372 23.2843 30.1372 15C30.1372 6.71573 23.4642 0 15.2326 0C7.00111 0 0.328125 6.71573 0.328125 15C0.328125 23.2843 7.00111 30 15.2326 30Z" fill="white"/></mask><g mask="url(#mask0_621_3115)"><path d="M15.2324 30L30.137 30L30.137 1.14441e-05L15.2324 1.14441e-05L15.2324 30Z" fill="#0053E0"/></g><path d="M15.2318 14.9966C19.3467 14.9966 22.6824 11.6395 22.6824 7.49831C22.6824 3.35711 19.3467 0 15.2318 0C11.117 0 7.78125 3.35711 7.78125 7.49831C7.78125 11.6395 11.117 14.9966 15.2318 14.9966Z" fill="#B3DDF2"/><path d="M15.3712 0.00674309C18.5069 0.0842886 21.0228 2.66015 21.0228 5.83277C21.0228 8.50977 19.2271 10.8328 16.6007 11.5037C16.6007 11.5037 16.6241 11.5003 16.6576 11.4936C14.4097 11.9757 12.7246 13.9852 12.7246 16.3924C12.7246 19.1605 14.9524 21.4026 17.7028 21.4026C18.4164 21.4026 19.3879 21.234 20.001 20.9609C23.9675 19.3493 26.47 15.4552 26.47 10.8867C26.47 4.87188 21.6258 0 15.6559 0C15.5588 0 15.465 0.00337154 15.3712 0.00674309ZM16.9491 11.443C16.9491 11.443 16.9591 11.443 16.9625 11.443C16.9591 11.443 16.9524 11.443 16.9491 11.443Z" fill="#2AB9FF"/><path d="M4.20222 18.9028C4.20222 15.8718 5.4116 13.124 7.36805 11.1213V11.1145C7.87726 10.5549 8.19217 9.81313 8.19217 8.99384C8.19217 7.2575 6.79518 5.85156 5.06989 5.85156C3.87056 5.85156 2.82868 6.53599 2.30607 7.53733C1.04979 9.73558 0.326172 12.2811 0.326172 15.0019C0.326172 23.0936 6.69468 29.685 14.6645 29.9884C8.83873 29.6917 4.20222 24.8435 4.20222 18.9028ZM2.16537 7.84752C2.15532 7.87112 2.14862 7.89135 2.14192 7.91495C2.14862 7.89135 2.15867 7.87112 2.16537 7.84752Z" fill="#0053E0"/><path d="M14.668 29.9844C14.8556 29.9945 15.0465 29.9979 15.2341 29.9979C15.0432 29.9979 14.8556 29.9911 14.668 29.9844Z" fill="#0053E0"/><path d="M15.3756 0.0078125C18.5113 0.085358 21.0272 2.66122 21.0272 5.83384C21.0272 8.51084 19.2014 10.8372 16.6051 11.5048C14.6285 11.9667 13.1679 13.5344 12.8262 15.4023C12.8262 15.4023 13.8948 15.0044 15.2148 15.0044C15.2148 15.0044 15.2081 15.0044 15.2047 15.0044C15.2148 15.0044 15.2215 15.0044 15.2315 15.0044C19.3488 15.0044 22.6821 11.6464 22.6821 7.50612C22.6821 3.36587 19.4225 0.085358 15.3756 0.0078125Z" fill="#ECECEE"/></svg>',
29
31
  '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
32
  '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
33
  '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
34
  '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>',
35
+ 'minimax': '<svg fill="#E73562" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M11.43 3.92a.86.86 0 1 0-1.718 0v14.236a1.999 1.999 0 0 1-3.997 0V9.022a.86.86 0 1 0-1.718 0v3.87a1.999 1.999 0 0 1-3.997 0V11.49a.57.57 0 0 1 1.139 0v1.404a.86.86 0 0 0 1.719 0V9.022a1.999 1.999 0 0 1 3.997 0v9.134a.86.86 0 0 0 1.719 0V3.92a1.998 1.998 0 1 1 3.996 0v11.788a.57.57 0 1 1-1.139 0zm10.572 3.105a2 2 0 0 0-1.999 1.997v7.63a.86.86 0 0 1-1.718 0V3.923a1.999 1.999 0 0 0-3.997 0v16.16a.86.86 0 0 1-1.719 0V18.08a.57.57 0 1 0-1.138 0v2a1.998 1.998 0 0 0 3.996 0V3.92a.86.86 0 0 1 1.719 0v12.73a1.999 1.999 0 0 0 3.996 0V9.023a.86.86 0 1 1 1.72 0v6.686a.57.57 0 0 0 1.138 0V9.022a2 2 0 0 0-1.998-1.997"/></svg>',
36
+ 'mimo': '<svg fill="#FF6900" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M12 0C8.016 0 4.756.255 2.493 2.516.23 4.776 0 8.033 0 12.012c0 3.98.23 7.235 2.494 9.497C4.757 23.77 8.017 24 12 24c3.983 0 7.243-.23 9.506-2.491C23.77 19.247 24 15.99 24 12.012c0-3.984-.233-7.243-2.502-9.504C19.234.252 15.978 0 12 0zM4.906 7.405h5.624c1.47 0 3.007.068 3.764.827.746.746.827 2.233.83 3.676v4.54a.15.15 0 0 1-.152.147h-1.947a.15.15 0 0 1-.152-.148V11.83c-.002-.806-.048-1.634-.464-2.051-.358-.36-1.026-.441-1.72-.458H7.158a.15.15 0 0 0-.151.147v6.98a.15.15 0 0 1-.152.148H4.906a.15.15 0 0 1-.15-.148V7.554a.15.15 0 0 1 .15-.149zm12.131 0h1.949a.15.15 0 0 1 .15.15v8.892a.15.15 0 0 1-.15.148h-1.949a.15.15 0 0 1-.151-.148V7.554a.15.15 0 0 1 .151-.149zM8.92 10.948h2.046c.083 0 .15.066.15.147v5.352a.15.15 0 0 1-.15.148H8.92a.15.15 0 0 1-.152-.148v-5.352a.15.15 0 0 1 .152-.147Z"/></svg>',
33
37
  '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
38
  '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
39
  '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>',
@@ -37,6 +41,7 @@ window.__ModuleLoader__.load({
37
41
  '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
42
  '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
43
  '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>',
44
+ 'xai': '<svg fill="#000000" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"/></svg>',
40
45
  '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
46
  };
42
47
  //#endregion
@@ -48,18 +53,25 @@ window.__ModuleLoader__.load({
48
53
  moonshot: { name: "Kimi", color: "#000000", cat: "国内" },
49
54
  stepfun: { name: "阶跃星辰", color: "#FA520F", cat: "国内" },
50
55
  siliconflow: { name: "硅基流动", color: "#6E29F6", cat: "国内" },
51
- minimax: { name: "MiniMax", color: "#1E40AF", cat: "国内" },
56
+ minimax: { name: "MiniMax", color: "#E73562", cat: "国内" },
57
+ doubao: { name: "豆包 Seed", color: "#3C8CFF", cat: "国内" },
58
+ hunyuan: { name: "腾讯混元", color: "#0052D9", cat: "国内" },
52
59
  openrouter: { name: "OpenRouter", color: "#6469FF", cat: "海外" },
53
60
  novita: { name: "Novita", color: "#FA520F", cat: "海外" },
54
61
  xai: { name: "xAI Grok", color: "#000000", cat: "海外" },
62
+ openai: { name: "OpenAI", color: "#10A37F", cat: "海外" },
63
+ claude: { name: "Anthropic Claude", color: "#D97757", cat: "海外" },
64
+ gemini: { name: "Google Gemini", color: "#4285F4", cat: "海外" },
65
+ qwen: { name: "通义千问", color: "#623AE7", cat: "国内" },
66
+ mimo: { name: "MiMo", color: "#FF6900", cat: "国内" },
55
67
  relay: { name: "中转站", color: "#64748B", cat: "中转站" },
56
68
  };
57
69
  /** 平台 id → 图标文件名 映射 (服务端 icon 字段即文件名) */
58
70
  const ICON_ALIAS = {
59
- stepfun: "mistral", novita: "together", minimax: "together", xai: "mistral", relay: "relay",
71
+ stepfun: "mistral", novita: "together", relay: "relay",
60
72
  };
61
- /** 无官方图标、用文字名代替的平台 */
62
- const TEXT_ONLY = new Set(["stepfun", "novita", "minimax", "xai", "relay"]);
73
+ /** 无官方图标、用文字名代替的平台 (v1.2.1: minimax/xai/mimo 已换官方 SVG; hunyuan 暂无官方 SVG 用文字) */
74
+ const TEXT_ONLY = new Set(["stepfun", "novita", "relay"]);
63
75
  /** 文字图标: 取平台名的首字符做一个彩色圆圈字 */
64
76
  function textIcon(id, meta) {
65
77
  const color = meta.color || "#64748B";
@@ -81,6 +93,53 @@ window.__ModuleLoader__.load({
81
93
  }
82
94
  //#endregion
83
95
 
96
+ //#region 把手下滑关闭 (v1.2.1)
97
+ /** 把手 = 下滑关闭手势区: pointer 跟手拖拽整个抽屉, 超过阈值或快速下滑 → 播滑出动画后关闭, 否则回弹。
98
+ * 三处复用: 看板抽屉(onClose) / 平台详情(onClose) / 设置面板(onBack || onClose)。 */
99
+ function SwipeHandle({ onClose }) {
100
+ const st = react.useRef({ y0: 0, dy: 0, t0: 0, drawer: null, dragging: false, done: false })
101
+ const setT = (el, dy, anim) => {
102
+ if (!el) return
103
+ el.style.transition = anim ? "transform .24s ease" : "none"
104
+ el.style.transform = dy > 0 ? "translateY(" + dy + "px)" : ""
105
+ }
106
+ const down = (e) => {
107
+ const s = st.current
108
+ if (s.done) return
109
+ s.y0 = e.clientY; s.dy = 0; s.t0 = Date.now(); s.dragging = true
110
+ s.drawer = e.currentTarget.closest(".dshadb_drawer")
111
+ if (s.drawer) s.drawer.style.transition = "none"
112
+ try { e.currentTarget.setPointerCapture(e.pointerId) } catch (err) { /* 无 PointerCapture 时靠事件冒泡兜底 */ }
113
+ }
114
+ const move = (e) => {
115
+ const s = st.current
116
+ if (!s.dragging) return
117
+ s.dy = Math.max(0, e.clientY - s.y0) // 只跟手下滑, 不允许上拉越顶
118
+ setT(s.drawer, s.dy, false)
119
+ }
120
+ const end = () => {
121
+ const s = st.current
122
+ if (!s.dragging || s.done) return
123
+ s.dragging = false
124
+ const dt = Math.max(1, Date.now() - s.t0)
125
+ const fast = s.dy > 24 && s.dy / dt > 0.55 // 快速轻扫也算关闭意图
126
+ if (s.dy > 72 || fast) {
127
+ s.done = true
128
+ if (s.drawer) { // 先播滑出动画再卸载, 避免拖到一半突然消失
129
+ s.drawer.style.transition = "transform .2s ease-in"
130
+ s.drawer.style.transform = "translateY(100%)"
131
+ }
132
+ setTimeout(onClose, 180)
133
+ } else setT(s.drawer, 0, true)
134
+ }
135
+ return react.createElement("div", {
136
+ className: "dshadb_handle", key: "handle", title: "下滑关闭",
137
+ style: { touchAction: "none", cursor: "grab" },
138
+ onPointerDown: down, onPointerMove: move, onPointerUp: end, onPointerCancel: end,
139
+ }, react.createElement("div", { className: "dshadb_handle_bar" }))
140
+ }
141
+ //#endregion
142
+
84
143
  //#region 精致 SVG 图标组件
85
144
  function IconTitle() {
86
145
  return react.createElement("img", { src: "/api-dashboard/icon", alt: "哦鲸鲸", style: { width: "100%", height: "100%", borderRadius: "6px", objectFit: "cover" } });
@@ -121,10 +180,13 @@ window.__ModuleLoader__.load({
121
180
  tag.textContent = `/* ===== 动画 ===== */
122
181
  @keyframes dshadb-fadein{from{opacity:0}to{opacity:1}}
123
182
  @keyframes dshadb-slideup{from{transform:translateY(100%)}to{transform:translateY(0)}}
124
- @keyframes dshadb-pulse{0%,100%{opacity:1}50%{opacity:0.35}}
125
183
 
126
184
  /* ===== 状态条(顶部小条) ===== */
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}
185
+ /* v1.4.1: 定住高度下限 —— 空态(18px 内容)与有数据态(20px 内容)差 2px, 钱数/平台名一进来
186
+ 状态条就从 26px 变 28px, 把上面的会话区顶 2px(真机实测 CLS 0.00094+0.00053)。
187
+ 用 min-height 而不是 height: 系统字体放大时仍可自然增高, 不会裁字。
188
+ 28px = 有数据态的实测总高(20px 内容 + 4px 上下 padding), 所以有数据时外观完全不变。 */
189
+ .dshadb_bar{display:inline-flex;align-items:center;gap:5px;font-size:11px;min-height:28px;box-sizing:border-box;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
190
  .dshadb_bar:active{opacity:0.8}
129
191
  .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
192
  .dshadb_bar_name{font-weight:700;color:#17181c;font-size:11px}
@@ -133,13 +195,23 @@ window.__ModuleLoader__.load({
133
195
  .dshadb_bar_dot_ok{background:#35b56b}
134
196
  .dshadb_bar_dot_warn{background:#e6a72f}
135
197
  .dshadb_bar_dot_err{background:#e05252}
136
- .dshadb_bar_dot_loading{animation:dshadb-pulse 1.2s ease-in-out infinite;background:#c7c7cc}
137
198
  .dshadb_bar_peak{font-size:10px;padding:2px 7px;border-radius:999px;font-weight:700}
138
199
  .dshadb_bar_peak.peak{background:rgba(230,167,47,0.12);color:#b07d1a;border:1px solid rgba(230,167,47,0.2)}
139
200
  .dshadb_bar_peak.valley{background:rgba(79,124,255,0.10);color:#3d5fcc;border:1px solid rgba(79,124,255,0.18)}
140
201
 
141
202
  /* ===== 遮罩 ===== */
142
- .dshadb_scrim{position:fixed;inset:0;z-index:99998;background:rgba(0,0,0,0.35);animation:dshadb-fadein .15s ease-out}
203
+ .dshadb_scrim{position:fixed;inset:0;z-index:99998;background:rgba(0,0,0,0.35);animation:dshadb-fadein .15s ease-out;overflow-x:auto;overflow-y:hidden;scrollbar-width:none}
204
+ .dshadb_scrim::-webkit-scrollbar{display:none}
205
+ /* v1.4.0: 手机壳 (dsh-web-mobile) 的「左边缘滑入打开侧边栏」手势层 —— 详见 AGENTS.md ①。
206
+ 它在 document **捕获**阶段吃 pointerdown, 起手点落在屏幕左侧 45% 内、横向位移占优时,
207
+ 整条手势就被判成「开侧边栏」。我们面板里的横向拖拽 (大肥鱼滑块 / 文本选择 / 横向列表)
208
+ 因此会把侧边栏拉出来, 而且它先于我们的监听器执行, 拦不住。
209
+ 它留了一条明路: 起手元素若属于「真·横向滚动容器」(overflow-x 为 auto/scroll 且
210
+ scrollWidth > clientWidth + 1), beginStroke 直接放弃识别。
211
+ 于是给遮罩补 2px **不可见**的横向溢出, 让整块面板都落在它的让路条件里。
212
+ 溢出由 0 高度的 .dshadb_swipeguard 提供; 抽屉自身是 position:fixed,
213
+ 所以遮罩滚动不会移动面板 —— 视觉与布局零影响。 */
214
+ .dshadb_swipeguard{display:block;width:calc(100% + 2px);height:0;pointer-events:none}
143
215
 
144
216
  /* ===== 抽屉 ===== */
145
217
  .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}
@@ -187,17 +259,13 @@ window.__ModuleLoader__.load({
187
259
  .dshadb_card_status_ok{color:#35b56b}
188
260
  .dshadb_card_status_warn{color:#e6a72f}
189
261
  .dshadb_card_status_err{color:#e05252}
190
- .dshadb_card_status_loading{color:#9ca0aa}
191
262
  .dshadb_status_dot{width:7px;height:7px;border-radius:50%;flex:none}
192
263
  .dshadb_status_dot_ok{background:#35b56b}
193
264
  .dshadb_status_dot_warn{background:#e6a72f}
194
265
  .dshadb_status_dot_err{background:#e05252}
195
- .dshadb_status_dot_loading{background:#c7c7cc;animation:dshadb-pulse 1.2s ease-in-out infinite}
196
266
  .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
267
 
199
268
  /* ===== 底部按钮 ===== */
200
- .dshadb_add_grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:6px 0 0}
201
269
  .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
270
  .dshadb_add_btn:active{transform:scale(0.97);opacity:0.9}
203
271
 
@@ -225,14 +293,6 @@ window.__ModuleLoader__.load({
225
293
  .dshadb_settings_body{padding:4px 14px}
226
294
  .dshadb_settings_section{margin-bottom:14px}
227
295
  .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
296
  .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
297
  .dshadb_switch_on{background:#4f7cff}
238
298
  .dshadb_switch_knob{display:block;width:19px;height:19px;background:white;border-radius:50%;transition:transform .18s ease}
@@ -247,11 +307,19 @@ window.__ModuleLoader__.load({
247
307
  /* 输入框 */
248
308
  .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}
249
309
  .dshadb_field:focus{outline:none;border-color:#4f7cff}
310
+ .dshadb_textarea{resize:vertical;min-height:56px;line-height:1.5;margin-bottom:6px}
311
+ /* provider 自动判定结果标签 */
312
+ .dshadb_kinds{margin:8px 0 4px}
313
+ .dshadb_kinds_title{display:block;font-size:10px;font-weight:700;color:#9ca0aa;margin-bottom:5px}
314
+ .dshadb_kinds_empty{display:block;font-size:10px;color:#9ca0aa;line-height:1.5}
315
+ .dshadb_kinds_list{display:flex;flex-wrap:wrap;gap:5px}
316
+ .dshadb_kind{font-size:10px;font-weight:600;padding:3px 8px;border-radius:999px;border:1px solid;white-space:nowrap}
317
+ .dshadb_kind_official{background:rgba(52,199,89,0.10);border-color:rgba(52,199,89,0.28);color:#2a8c4a}
318
+ .dshadb_kind_relay{background:rgba(142,142,147,0.10);border-color:rgba(142,142,147,0.26);color:#6f737c}
250
319
  .dshadb_label{font-size:11px;font-weight:700;color:#777b84;margin-bottom:4px;display:block}
251
320
  .dshadb_grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
252
321
  .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}
253
322
  .dshadb_save_btn:active{opacity:0.85}
254
- .dshadb_danger{color:#e05252}
255
323
 
256
324
  /* ===== 详情页 ===== */
257
325
  .dshadb_detail{display:flex;flex-direction:column;gap:12px;padding:4px 2px}
@@ -267,9 +335,6 @@ window.__ModuleLoader__.load({
267
335
  .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}
268
336
  .dshadb_detail_metric_val{font-size:16px;font-weight:700;color:#17181c;font-variant-numeric:tabular-nums}
269
337
  .dshadb_detail_metric_label{font-size:10px;color:#777b84}
270
- .dshadb_detail_row{display:flex;align-items:center;justify-content:space-between;padding:7px 0;border-bottom:1px solid #eef0f4;font-size:11px}
271
- .dshadb_detail_row_label{color:#777b84}
272
- .dshadb_detail_row_val{color:#17181c;font-weight:600;font-variant-numeric:tabular-nums;text-align:right}
273
338
  .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}
274
339
  .dshadb_peakcard.peak{background:rgba(230,167,47,0.08);border-color:rgba(230,167,47,0.25)}
275
340
  .dshadb_peakcard.valley{background:rgba(79,124,255,0.06);border-color:rgba(79,124,255,0.20)}
@@ -292,6 +357,27 @@ window.__ModuleLoader__.load({
292
357
 
293
358
  /* ===== 补缺失CSS ===== */
294
359
  .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}
360
+ /* ===== v1.4.0 子代理消耗行 =====
361
+ 设计约束: 无论几个子代理都**只占一行高度**(约 20px), 绝不换行挤压输入框。
362
+ 做法 = 固定 flex-wrap:nowrap + 横向滚动(隐藏滚动条) + 名字截断到 72px。
363
+ 完整名字/模型/精确金额 → 长按弹 .dshadb_subtip 浮层。
364
+ ⚠️ **别指望 title**: 手机上 title 根本不显示, 长按弹的是系统「选择/复制」菜单
365
+ (实测踩到, 用户反馈「长按只能复制」)。已加 user-select:none +
366
+ -webkit-touch-callout:none 抑制系统菜单, 详情改由 pointerdown 计时器弹浮层,
367
+ 见 bindSubagentTip()。浮层必须挂在 document.body 上 —— 挂在胶囊里会被
368
+ .dshadb_subs 的 overflow-x:auto 裁掉。
369
+ ⚠️ 本段在 JS 模板字符串里, 注释里**不能出现反引号**(会提前结束模板串)。 */
370
+ .dshadb_barwrap{display:inline-flex;flex-direction:column;align-items:flex-start;gap:0;max-width:100%;min-width:0}
371
+ .dshadb_barrow{display:inline-flex;align-items:center;gap:2px;max-width:100%}
372
+ .dshadb_subs{display:flex;flex-wrap:nowrap;align-items:center;gap:4px;margin:3px 0 0 2px;max-width:100%;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;-ms-overflow-style:none;-webkit-overflow-scrolling:touch}
373
+ .dshadb_subs::-webkit-scrollbar{display:none}
374
+ .dshadb_sub{flex:none;display:inline-flex;align-items:center;gap:4px;font-size:10px;line-height:1.15;padding:2px 7px;border-radius:8px;background:#f5f6f8;border:1px solid #e7e8ec;color:#777b84;white-space:nowrap;-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}
375
+ .dshadb_sub_dot{width:5px;height:5px;border-radius:50%;background:#8b93a7;flex:none}
376
+ .dshadb_sub_name{font-weight:700;color:#17181c;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:72px}
377
+ .dshadb_sub_amt{font-weight:800;font-variant-numeric:tabular-nums;color:#4f7cff;flex:none}
378
+ /* 子代理长按详情浮层 (v1.4.0): 挂在 document.body 上, 不能被 .dshadb_subs 的
379
+ overflow 裁掉; z-index 要高过抽屉(99999)/遮罩。纯展示, 不接收触摸事件。 */
380
+ .dshadb_subtip{position:fixed;z-index:100000;max-width:min(280px,calc(100vw - 24px));padding:9px 11px;border-radius:12px;background:#17181c;color:#ffffff;font-size:11px;font-weight:600;line-height:1.6;white-space:pre-line;word-break:break-word;box-shadow:0 10px 30px rgba(0,0,0,0.3);pointer-events:none;animation:dshadb-fadein .12s ease-out}
295
381
  .dshadb_footer_dot{width:4px;height:4px;border-radius:50%;background:#c9cad0;opacity:0.5}
296
382
  .dshadb_group_divider{height:1px;background:#eef0f4;margin:2px 6px 4px}
297
383
  .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}
@@ -310,7 +396,7 @@ window.__ModuleLoader__.load({
310
396
  .dshadb_wf_sub{display:block;font-size:10px;font-weight:500;color:#9ca0aa;line-height:1.4;margin-top:2px}
311
397
  .dshadb_wf_head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
312
398
  .dshadb_wf_val{font-size:11px;font-weight:800;color:#4f7cff;font-variant-numeric:tabular-nums;flex:none}
313
- .dshadb_wf_range{width:100%;height:24px;accent-color:#4f7cff;margin:0}
399
+ .dshadb_wf_range{width:100%;height:34px;accent-color:#4f7cff;margin:0}
314
400
  .dshadb_wf_seg{display:flex;gap:6px;margin-left:auto;flex:none}
315
401
  .dshadb_wf_segbtn{font-size:11px;font-weight:700;font-family:inherit;padding:6px 11px;border-radius:10px;border:1px solid #e7e8ec;background:#f5f6f8;color:#9ca0aa;cursor:pointer;-webkit-tap-highlight-color:transparent;transition:background .15s ease,color .15s ease}
316
402
  .dshadb_wf_segbtn_on{background:#4f7cff;border-color:#4f7cff;color:#ffffff}
@@ -322,21 +408,79 @@ window.__ModuleLoader__.load({
322
408
  .dshadb_wf_name{color:#f2f4f8}
323
409
  .dshadb_wf_segbtn{background:#1e222a;border-color:#363c48;color:#8b91a0}
324
410
  .dshadb_wf_segbtn_on{background:#4f7cff;border-color:#4f7cff;color:#fff}}
411
+ /* ===== v1.4.0 设置界面控件统一 (此前是浏览器默认外观 + 无深色样式) =====
412
+ 注意: 曾把「币种」那几个 <select> 换成插件自己的分段按钮, 但在 2 列网格里
413
+ 「人民币」会被截成「人民…」, 观感反而更差 —— 已改回 <select>, 只做外观清理:
414
+ 去掉系统箭头/系统上下箭头, 让它长得跟旁边的 .dshadb_field 一致。 */
415
+ .dshadb_field_select{-webkit-appearance:none;appearance:none;background-image:linear-gradient(45deg,transparent 50%,#9ca0aa 50%),linear-gradient(135deg,#9ca0aa 50%,transparent 50%);background-position:calc(100% - 18px) calc(50% - 2px),calc(100% - 13px) calc(50% - 2px);background-size:5px 5px,5px 5px;background-repeat:no-repeat;padding-right:36px}
416
+ /* 数字框去掉系统上下箭头 (手机上会挤掉输入区) */
417
+ .dshadb_field[type=number]{-moz-appearance:textfield;appearance:textfield}
418
+ .dshadb_field[type=number]::-webkit-outer-spin-button,.dshadb_field[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
419
+ /* 滑块: accent-color 只管颜色, 这里补齐轨道/滑块的圆润观感 */
420
+ .dshadb_wf_range{-webkit-appearance:none;appearance:none;background:transparent}
421
+ .dshadb_wf_range::-webkit-slider-runnable-track{height:6px;border-radius:999px;background:#e7e8ec}
422
+ .dshadb_wf_range::-webkit-slider-thumb{-webkit-appearance:none;width:24px;height:24px;margin-top:-9px;border-radius:50%;background:#4f7cff;border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.18)}
423
+ .dshadb_wf_range::-moz-range-track{height:6px;border-radius:999px;background:#e7e8ec}
424
+ .dshadb_wf_range::-moz-range-thumb{width:16px;height:16px;border-radius:50%;background:#4f7cff;border:2px solid #fff}
425
+ /* 深色模式: 设置面板 + 状态条 + 子代理行 (此前这些一律硬编码白底, 深色下是白块) */
426
+ @media (prefers-color-scheme:dark){
427
+ .dshadb_field{background:#1e222a;border-color:#363c48;color:#f2f4f8}
428
+ .dshadb_field::placeholder{color:#6b7280}
429
+ .dshadb_field:focus{border-color:#4f7cff}
430
+ .dshadb_field_select{background-image:linear-gradient(45deg,transparent 50%,#8b91a0 50%),linear-gradient(135deg,#8b91a0 50%,transparent 50%)}
431
+ .dshadb_switch{background:#3d434f}
432
+ .dshadb_switch_on{background:#4f7cff}
433
+ .dshadb_switch_knob{background:#f2f4f8}
434
+ .dshadb_tab{background:#262a33;color:#9aa1b0}
435
+ .dshadb_tab_active{background:#4f7cff;color:#fff}
436
+ .dshadb_settings_group_title{color:#8b91a0}
437
+ .dshadb_kinds_title,.dshadb_kinds_empty{color:#7e8494}
438
+ .dshadb_kind{background:#262a33;border-color:#363c48;color:#9aa1b0}
439
+ .dshadb_bar{background:#262a33;border-color:#363c48;color:#9aa1b0}
440
+ .dshadb_bar_name,.dshadb_bar_amount{color:#f2f4f8}
441
+ .dshadb_bar_cost{border-left-color:#363c48;color:#8b91a0}
442
+ .dshadb_subs .dshadb_sub,.dshadb_sub{background:#262a33;border-color:#363c48;color:#9aa1b0}
443
+ .dshadb_sub_name{color:#f2f4f8}
444
+ .dshadb_sub_amt{color:#7d9bff}
445
+ .dshadb_subtip{background:#f2f4f8;color:#17181c;box-shadow:0 10px 30px rgba(0,0,0,0.5)}
446
+ .dshadb_sub_dot{background:#8b93a7}
447
+ .dshadb_wf_range::-webkit-slider-runnable-track{background:#363c48}
448
+ .dshadb_wf_range::-moz-range-track{background:#363c48}
449
+ }
325
450
  /* ===== 大肥鱼互动挂件 (v1.1.0) ===== */
326
- .dshadb-whale{position:fixed;left:0;top:60vh;z-index:9600;--dshw-scale:1;--dshw-size:clamp(96px,calc(min(150px,min(100vw,100vh) * 0.30) * var(--dshw-scale)),420px);width:var(--dshw-size);height:var(--dshw-size);pointer-events:none;-webkit-user-select:none;user-select:none;touch-action:none;transition:transform .45s cubic-bezier(.34,1.4,.64,1),left .3s ease,top .3s ease}
451
+ .dshadb-whale{position:fixed;left:0;top:60vh;z-index:9600;--dshw-scale:1;--dshw-size:clamp(calc(96px * var(--dshw-scale)),calc(min(150px,min(100vw,100vh) * 0.30) * var(--dshw-scale)),420px);width:var(--dshw-size);height:var(--dshw-size);pointer-events:none;-webkit-user-select:none;user-select:none;touch-action:none;transition:transform .45s cubic-bezier(.34,1.4,.64,1),left .3s ease,top .3s ease}
327
452
  .dshadb-whale.dshadb-whale-dragging{transition:none}
328
453
  .dshadb-whale.dshadb-whale-flip .dshadb-whale-sprite{transform:scaleX(-1)}
329
454
  .dshadb-whale-sprite{position:absolute;left:0;top:0;width:100%;height:100%;transition:transform .3s ease}
330
455
  .dshadb-whale-body{position:absolute;left:0;top:0;width:100%;height:100%;transform-origin:50% 92%;transition:transform .2s cubic-bezier(.34,1.56,.64,1);pointer-events:auto;cursor:grab;-webkit-tap-highlight-color:transparent}
456
+ /* 手机壳侧滑手势层会给「真·横向滚动容器」让路 (dsh-web-mobile 的 findHorizontalScroller:
457
+ overflow-x 为 auto/scroll 且 scrollWidth > clientWidth + 1 → beginStroke 直接放弃识别)。
458
+ 挂件本体此前没有这层守卫: 鱼停在屏幕左侧 45% 以内时(典型 = 左边缘吸附位, 可见区只有 ~32px 宽),
459
+ 在它身上往右拖会被判成「左边缘滑入开抽屉」(挂件自己的拖拽同时照常执行, 所以是"拖着鱼把侧边栏拉出来")。
460
+ 这层透明抓取层就是那个滚动容器 —— 起手点落在它身上, 手机壳就不再识别。
461
+ ⚠️ 必须挂在 .dshadb-whale-body **内部**(不能挂 root 下): 这样 pointerdown 照常冒泡到 body 上
462
+ 已有的拖拽处理器, 拖拽逻辑一行不用改。
463
+ ⚠️ touch-action:none 必须写在这一层: 滚动容器是浏览器判定可触摸行为的终点, 漏了它横向 pan
464
+ 会被这个滚动容器自己抢走(pointercancel) → 鱼直接拖不动。
465
+ ⚠️ 别改成给 .dshadb-whale-body 加 overflow —— 那会把它里面 img 的 drop-shadow 裁掉, 鱼会显平。 */
466
+ .dshadb-whale-grab{position:absolute;left:0;top:0;width:100%;height:100%;z-index:1;overflow-x:auto;overflow-y:hidden;touch-action:none;overscroll-behavior:contain;scrollbar-width:none;pointer-events:auto;cursor:grab;background:transparent;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}
467
+ .dshadb-whale-grab::-webkit-scrollbar{display:none;width:0;height:0}
468
+ .dshadb-whale-grab-guard{display:block;width:calc(100% + 2px);height:0;pointer-events:none}
469
+ /* v1.4.0: 我们自己的抽屉/面板开着时锁住挂件触摸 —— 否则它会盖在面板上抢走滑块的拖拽。
470
+ 注意只关交互、不隐藏: 调「身体大小/露出比例」时还要看实时预览。 */
471
+ .dshadb-whale-locked .dshadb-whale-body{pointer-events:none;cursor:default}
331
472
  .dshadb-whale-press .dshadb-whale-body{transform:scale(1.1,.88);cursor:grabbing}
473
+ /* pointer-events 不是"继承即锁": 抓取层自己写了 auto, body 被锁成 none 时它照样吃事件, 所以必须单独上锁 */
474
+ .dshadb-whale-locked .dshadb-whale-grab{pointer-events:none;cursor:default}
475
+ .dshadb-whale-press .dshadb-whale-grab{cursor:grabbing}
332
476
  .dshadb-whale-img{width:100%;height:100%;object-fit:contain;display:block;pointer-events:none;-webkit-user-drag:none;filter:drop-shadow(0 6px 14px rgba(0,0,0,.18))}
333
- .dshadb-whale-bubble{position:absolute;left:50%;bottom:calc(100% - 6px);--dshw-bdx:0px;--dshw-btx:50%;transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(10px) scale(.82);transform-origin:var(--dshw-btx) 100%;opacity:0;pointer-events:none;background:#fff;border-radius:16px;padding:10px 14px;min-width:112px;max-width:min(74vw,248px);box-shadow:0 8px 28px rgba(0,0,0,.16);transition:opacity .2s ease,transform .26s cubic-bezier(.34,1.5,.64,1);z-index:2;text-align:center;box-sizing:border-box}
477
+ .dshadb-whale-bubble{position:absolute;left:50%;bottom:calc(100% + 6px);--dshw-bdx:0px;--dshw-btx:50%;transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(10px) scale(.82);transform-origin:var(--dshw-btx) 100%;opacity:0;pointer-events:none;background:#fff;border-radius:16px;padding:10px 14px;min-width:112px;max-width:min(74vw,248px);box-shadow:0 8px 28px rgba(0,0,0,.16);transition:opacity .2s ease,transform .26s cubic-bezier(.34,1.5,.64,1);z-index:2;text-align:center;box-sizing:border-box}
334
478
  .dshadb-whale-bubble::after{content:"";position:absolute;left:var(--dshw-btx);top:100%;transform:translateX(-50%);border:8px solid transparent;border-top-color:#fff}
335
479
  .dshadb-whale-bubble-on{opacity:1;transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(0) scale(1)}
336
480
  /* 连点时气泡不做淡出淡入的来回, 只跟位置走 —— 避免闪烁 */
337
481
  .dshadb-whale-bubble-on.dshadb-whale-bubble-keep{transition:transform .16s ease}
338
482
  /* 气泡在上方放不下时改到下方, 尾巴翻到顶边 */
339
- .dshadb-whale-bubble-below{bottom:auto;top:calc(100% - 6px);transform-origin:var(--dshw-btx) 0}
483
+ .dshadb-whale-bubble-below{bottom:auto;top:calc(100% + 6px);transform-origin:var(--dshw-btx) 0}
340
484
  .dshadb-whale-bubble-below::after{top:auto;bottom:100%;border-top-color:transparent;border-bottom-color:#fff}
341
485
  /* 镜像只作用于鲸鱼本体, 气泡与文字保持正向 */
342
486
  .dshadb-whale-flip .dshadb-whale-bubble{transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(10px) scale(.82)}
@@ -353,12 +497,6 @@ window.__ModuleLoader__.load({
353
497
  .dshadb-whale-l-a{color:#d3d7e0}.dshadb-whale-l-b{color:#f2f4f8}.dshadb-whale-l-c{color:#9298a6}}`
354
498
 
355
499
  document.head.appendChild(tag);
356
- /* 渐进增强: 高端机 (逻辑核 >= 6) 才启用毛玻璃等特效 */
357
- try {
358
- if (!navigator.hardwareConcurrency || navigator.hardwareConcurrency >= 6) {
359
- document.documentElement.classList.add("dshadb-high-end");
360
- }
361
- } catch (e) { /* ignore */ }
362
500
  }
363
501
  //#endregion
364
502
 
@@ -374,9 +512,10 @@ window.__ModuleLoader__.load({
374
512
  "detail.total": "当前余额", "detail.topup": "总充值", "detail.used": "总使用", "detail.note": "类型",
375
513
  "detail.sessionCost": "本会话消耗", "detail.noBalance": "该平台未开放余额查询",
376
514
  "settings.safe": "安全阈值(绿)", "settings.warn": "预警阈值(黄)", "settings.currency": "计价货币",
515
+ "settings.overseasCurrency": "海外模型计价", "settings.overseasFollow": "跟随主货币", "settings.overseasHint": "海外厂商官方价本就是美元, 选美元可免去 ×7 折算误差",
377
516
  "settings.safeHint": "余额高于此值显示绿色(安全)",
378
517
  "settings.warnHint": "余额低于此值显示黄色(偏低),再低显示红",
379
- "settings.refresh": "刷新间隔(秒)", "settings.refreshHint": "5~60 秒,最高一分钟",
518
+ "settings.refresh": "刷新间隔(秒)", "settings.refreshHint": "1~60 秒,最高一分钟",
380
519
  "settings.save": "保存并生效", "settings.saved": "已保存",
381
520
  "update.title": "版本与更新", "update.current": "当前版本",
382
521
  "update.check": "检查更新", "update.checking": "检查中…",
@@ -396,6 +535,16 @@ window.__ModuleLoader__.load({
396
535
  "whale.peak.default": "默认", "whale.peak.liangwen": "梁文峰谷", "whale.peak.qiangqiang": "!?强强?!",
397
536
  "whale.tip": "拖着大肥鱼可以在屏幕上随意移动;点一下弹全身和台词,再点一下收回。",
398
537
  "whale.offTip": "先打开上面的「收养大肥鱼」,才能调下面这些。",
538
+ "brands": "模型品牌",
539
+ "settings.showBrands": "显示模型品牌", "settings.showBrandsHint": "在看板中显示 OpenAI / Claude / Gemini / Qwen / 豆包 / 混元 / MiMo 等无余额接口的品牌",
540
+ "settings.official": "官方直连 provider",
541
+ "settings.officialHint": "逗号或换行分隔。写在这里的 provider 名一律按官方直连处理,状态条显示官方余额;留空则自动判定(按 settings.yaml 里 baseURL 的域名,识别不出按中转站显示「—」)",
542
+ "settings.officialAuto": "自动判定结果",
543
+ "settings.officialKindOfficial": "官方", "settings.officialKindRelay": "中转",
544
+ "settings.officialAutoEmpty": "settings.yaml 里没有写 baseURL 的 provider,无法自动判定",
545
+ "settings.dshProviders": "来自 DSH 的中转站(自动)",
546
+ "settings.dshProvidersHint": "直接读 settings.yaml 的 provider 列表,默认全部自动查询余额。用右侧开关单独关掉某个;关过的不再自动打开。官方直连与没写 baseURL 的 provider 不在此列。",
547
+ "settings.dshNoBaseUrl": "没写 baseURL(不表态)",
399
548
  "settings.modelName": "模型名称", "settings.apiUrl": "余额接口 URL", "settings.queryType": "解析方式",
400
549
  "settings.totalPath": "总余额字段(可选)", "settings.usedPath": "已用字段(可选)",
401
550
  "settings.modelHint": "填你的余额接口(返回 JSON),可选填 API Key 与字段路径,如 data.balance",
@@ -421,9 +570,10 @@ window.__ModuleLoader__.load({
421
570
  "detail.total": "Balance", "detail.topup": "Top-up", "detail.used": "Used", "detail.note": "Type",
422
571
  "detail.sessionCost": "Session cost", "detail.noBalance": "No balance API",
423
572
  "settings.safe": "Safe (green)", "settings.warn": "Warn (yellow)", "settings.currency": "Currency",
573
+ "settings.overseasCurrency": "Overseas models", "settings.overseasFollow": "Follow main", "settings.overseasHint": "Overseas vendors price in USD; picking USD avoids the x7 conversion error",
424
574
  "settings.safeHint": "Above this = green (safe)",
425
575
  "settings.warnHint": "Below this = yellow, lower = red",
426
- "settings.refresh": "Refresh (sec)", "settings.refreshHint": "5-60s, max one minute",
576
+ "settings.refresh": "Refresh (sec)", "settings.refreshHint": "1-60s, max one minute",
427
577
  "settings.save": "Save", "settings.saved": "Saved",
428
578
  "update.title": "Version & Updates", "update.current": "Current",
429
579
  "update.check": "Check updates", "update.checking": "Checking…",
@@ -443,6 +593,16 @@ window.__ModuleLoader__.load({
443
593
  "whale.peak.default": "Default", "whale.peak.liangwen": "Liangwen", "whale.peak.qiangqiang": "!?Qiang?!",
444
594
  "whale.tip": "Drag the whale anywhere on screen; tap once for full body and a line, tap again to tuck it back.",
445
595
  "whale.offTip": "Turn on \"Adopt Big Whale\" above to edit these.",
596
+ "brands": "Model Brands",
597
+ "settings.showBrands": "Show model brands", "settings.showBrandsHint": "Show OpenAI / Claude / Gemini / Qwen / Doubao / Hunyuan / MiMo etc. (no balance API) in dashboard",
598
+ "settings.official": "Official-direct providers",
599
+ "settings.officialHint": "Comma or newline separated. Providers listed here always count as official direct connections and show their official balance; leave empty to auto-detect from the baseURL host in settings.yaml (undetected ones are treated as relays and show \"—\")",
600
+ "settings.officialAuto": "Auto-detected",
601
+ "settings.officialKindOfficial": "official", "settings.officialKindRelay": "relay",
602
+ "settings.officialAutoEmpty": "No provider in settings.yaml declares a baseURL, so nothing can be auto-detected",
603
+ "settings.dshProviders": "Relays from DSH (automatic)",
604
+ "settings.dshProvidersHint": "Reads the provider list straight from settings.yaml and queries balances automatically. Use the switch to turn one off; switched-off ones stay off. Official-direct providers and ones without a baseURL are not listed here.",
605
+ "settings.dshNoBaseUrl": "no baseURL (stays neutral)",
446
606
  "settings.modelName": "Model name", "settings.apiUrl": "Balance API URL", "settings.queryType": "Parse type",
447
607
  "settings.totalPath": "Total field (opt)", "settings.usedPath": "Used field (opt)",
448
608
  "settings.modelHint": "A JSON endpoint for your balance; optional API key & field paths like data.balance",
@@ -466,17 +626,25 @@ window.__ModuleLoader__.load({
466
626
  const listeners = new Set();
467
627
  let timer = null, pollMs = DEFAULT_POLL_MS, inflight = null, started = false;
468
628
  function notify() { for (const fn of [...listeners]) fn(); }
469
- async function refresh(force = false) {
470
- if (inflight !== null) return inflight;
471
- if (force && snapshot.isRefreshing !== true) { snapshot = { ...snapshot, isRefreshing: true }; notify(); }
629
+ /**
630
+ * @param force 显式强刷 (手动刷新按钮 / 保存设置后) —— **阻塞**, 等服务端拉完最新数据
631
+ * @param peek v1.4.0 stale-while-revalidate: 首屏 / 切回前台用 —— 服务端有缓存就**立刻**回,
632
+ * 刷新丢后台。force 那条路要等服务端跑完一次全量轮询(最慢端点可到 8s),
633
+ * 应用切回前台时用它就会「等一段时间」。
634
+ */
635
+ async function refresh(force = false, peek = false) {
636
+ if (inflight !== null && !force && !peek) return inflight;
637
+ if ((force || peek) && snapshot.isRefreshing !== true) { snapshot = { ...snapshot, isRefreshing: true }; notify(); }
472
638
  inflight = (async () => {
473
639
  try {
474
- const url = force ? "/api-dashboard/balances?force=1" : "/api-dashboard/balances";
475
- // v0.5.0: 走浏览器默认缓存 (配合服务端 ETag), 数据没变时 304 空响应; force 用独立 URL 不受影响
476
- const res = await fetch(url, { headers: { accept: "application/json" } });
640
+ const url = peek ? "/api-dashboard/balances?stale=1"
641
+ : force ? "/api-dashboard/balances?force=1&_t=" + Date.now()
642
+ : "/api-dashboard/balances";
643
+ // v0.5.0: 走浏览器默认缓存 (配合服务端 ETag), 数据没变时 304 空响应; force 用独立 URL + no-store 强刷
644
+ const res = await fetch(url, { headers: { accept: "application/json" }, cache: force ? "no-store" : "default" });
477
645
  if (!res.ok) throw new Error("HTTP " + res.status);
478
646
  const data = await res.json();
479
- if (data.config?.clientPollIntervalMs >= 5000) pollMs = Math.min(data.config.clientPollIntervalMs, 3600000);
647
+ if (data.config?.clientPollIntervalMs >= 1000) pollMs = Math.min(data.config.clientPollIntervalMs, 3600000);
480
648
  // v0.5.0 性能适配: 内容无变化就不重建快照引用, useSyncExternalStore 判定 Object.is 相同直接跳过整棵树重渲
481
649
  const newBalances = data.balances || [];
482
650
  const oldBalances = Array.isArray(snapshot.balances) ? snapshot.balances : [];
@@ -490,7 +658,7 @@ window.__ModuleLoader__.load({
490
658
  snapshot = { status: "ok", balances: newBalances, config: data.config || snapshot.config, fetchedAt: nextFetchedAt, isRefreshing: false };
491
659
  }
492
660
  } catch (error) {
493
- snapshot = { status: "error", balances: snapshot.balances || [], message: error instanceof Error ? error.message : String(error), isRefreshing: false };
661
+ snapshot = { ...snapshot, status: "error", balances: snapshot.balances || [], message: error instanceof Error ? error.message : String(error), isRefreshing: false };
494
662
  }
495
663
  inflight = null; notify();
496
664
  })();
@@ -501,7 +669,7 @@ window.__ModuleLoader__.load({
501
669
  timer = setTimeout(() => { timer = null; if (document.hidden) return; refresh().then(schedule, schedule); }, pollMs);
502
670
  }
503
671
  const store = {
504
- 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; } } }; },
672
+ subscribe(fn) { listeners.add(fn); if (!started) { started = true; refresh(false, true).then(schedule, schedule); } return () => { listeners.delete(fn); if (listeners.size === 0) { started = false; if (timer !== null) { clearTimeout(timer); timer = null; } } }; },
505
673
  getSnapshot() { return snapshot; },
506
674
  forceRefresh() { return refresh(true); },
507
675
  };
@@ -510,17 +678,153 @@ window.__ModuleLoader__.load({
510
678
  //#region helpers
511
679
  const CURRENCY_SYMBOLS = { CNY: "¥", USD: "$", EUR: "€" };
512
680
  const currencySymbol = (c) => { if (c === "%" || c === "tokens") return ""; return CURRENCY_SYMBOLS[c] || c + " "; };
681
+ /**
682
+ * v1.3.2: 会话消耗格式化 —— 海外模型可独立走 USD, 一个会话可能同时有两种货币。
683
+ * 不做汇率折算合并 (折算=再引入 ×7 误差, 正是用户「¥1285 被看成 $1285」的成因),
684
+ * 改为两段拼接: ~¥3.40+$12.50。主货币排在前。
685
+ * 返回 { text, title, mixed }; 无数据返回 null。
686
+ */
687
+ function formatSessionCost(cost) {
688
+ if (!cost || typeof cost.cost !== "number" || cost.waiting === true) return null;
689
+ const mainCur = cost.currency || "CNY";
690
+ const symOf = (c) => (c === "CNY" ? "¥" : c === "USD" ? "$" : c + " ");
691
+ const fixOf = (a) => (a >= 1 ? 2 : a >= 0.01 ? 3 : a >= 0.001 ? 4 : a >= 0.0001 ? 5 : 6);
692
+ const byCur = cost.costByCurrency && typeof cost.costByCurrency === "object" ? cost.costByCurrency : null;
693
+ let segs = [];
694
+ if (byCur) {
695
+ segs = Object.keys(byCur)
696
+ .filter((k) => typeof byCur[k] === "number" && byCur[k] > 0)
697
+ .sort((a, b) => (a === mainCur ? -1 : b === mainCur ? 1 : byCur[b] - byCur[a]))
698
+ .map((k) => ({ cur: k, amount: byCur[k] }));
699
+ }
700
+ // 旧 bundle / 未开启独立币种 / 全零 → 退回单段, 行为同 v1.2.6
701
+ if (segs.length === 0) segs = [{ cur: mainCur, amount: cost.cost }];
702
+ const mixed = segs.length > 1;
703
+ // hasValue: 是否真有非零金额。不能用 cost.cost > 0 判断 —— 开启海外独立币种后,
704
+ // 纯 claude 会话的主货币段就是 0, 金额全在 USD 段里 (详情页会误显示成「—」)。
705
+ const hasValue = segs.some((x) => x.amount > 0);
706
+ return {
707
+ text: segs.map((x) => symOf(x.cur) + x.amount.toFixed(fixOf(x.amount))).join("+"),
708
+ title: segs.map((x) => symOf(x.cur) + x.amount.toFixed(6)).join(" + ") + (mixed ? " (两种货币, 未按汇率合并)" : ""),
709
+ mixed: mixed,
710
+ hasValue: hasValue,
711
+ };
712
+ }
713
+ /* ===== v1.4.0 子代理胶囊 · 长按详情浮层 =====
714
+ * 为什么不用 title: **手机上 title 根本不显示**, 长按弹的是系统「选择/复制」菜单
715
+ * (用户实测反馈「长按只能复制」)。所以自己用 pointerdown 计时器弹浮层, 并靠 CSS 的
716
+ * user-select:none + -webkit-touch-callout:none 把系统菜单压掉。
717
+ * 浮层必须挂 document.body —— 挂在胶囊里会被 .dshadb_subs 的 overflow-x:auto 裁掉。
718
+ */
719
+ const SUBTIP_LONG_PRESS_MS = 420;
720
+ let subTipNode = null;
721
+ let subTipTimer = 0;
722
+ let subTipDismissBound = false;
723
+ function hideSubagentTip() {
724
+ if (subTipTimer) { clearTimeout(subTipTimer); subTipTimer = 0; }
725
+ if (subTipNode && subTipNode.parentNode) subTipNode.parentNode.removeChild(subTipNode);
726
+ subTipNode = null;
727
+ }
728
+ /** 浮层摆在胶囊上方; 上方放不下翻到下方; 左右夹在视口内。 */
729
+ function showSubagentTip(anchor, text) {
730
+ hideSubagentTip();
731
+ if (!anchor || !text) return;
732
+ const el = document.createElement("div");
733
+ el.className = "dshadb_subtip";
734
+ el.textContent = text; // 纯文本, 不走 innerHTML (label 可能来自用户/模型)
735
+ document.body.appendChild(el);
736
+ subTipNode = el;
737
+ const rect = anchor.getBoundingClientRect ? anchor.getBoundingClientRect() : null;
738
+ const vw = window.innerWidth || 360;
739
+ const vh = window.innerHeight || 640;
740
+ const w = el.offsetWidth || Math.min(240, vw - 24);
741
+ const h = el.offsetHeight || 60;
742
+ let top = rect ? rect.top - h - 8 : 16;
743
+ if (top < 8) top = rect ? Math.min(vh - h - 8, rect.bottom + 8) : 8;
744
+ const left = rect ? rect.left : 12;
745
+ el.style.left = Math.max(8, Math.min(vw - w - 8, left)) + "px";
746
+ el.style.top = Math.max(8, top) + "px";
747
+ subTipTimer = setTimeout(hideSubagentTip, 5000);
748
+ }
749
+ function bindSubTipDismiss() {
750
+ if (subTipDismissBound) return;
751
+ subTipDismissBound = true;
752
+ // 点别处 / 滚动 / 转屏都收起。capture 阶段先于胶囊自己的 pointerdown 跑,
753
+ // 所以「已开着一个浮层时再长按另一个胶囊」不会被立刻收掉。
754
+ document.addEventListener("pointerdown", hideSubagentTip, true);
755
+ window.addEventListener("scroll", hideSubagentTip, true);
756
+ window.addEventListener("resize", hideSubagentTip);
757
+ }
758
+ /** 给一个胶囊挂长按: 位移 > 8px(其实是在滑动这一行) 或抬手都取消; 长按抬手后的 click 吞掉。 */
759
+ function bindSubagentTip(node, text) {
760
+ let timer = 0, sx = 0, sy = 0, fired = false;
761
+ const cancel = () => { if (timer) { clearTimeout(timer); timer = 0; } };
762
+ node.addEventListener("pointerdown", (e) => {
763
+ bindSubTipDismiss();
764
+ fired = false;
765
+ sx = e.clientX; sy = e.clientY;
766
+ cancel();
767
+ timer = setTimeout(() => { timer = 0; fired = true; showSubagentTip(node, text); }, SUBTIP_LONG_PRESS_MS);
768
+ });
769
+ node.addEventListener("pointermove", (e) => {
770
+ if (timer && (Math.abs(e.clientX - sx) > 8 || Math.abs(e.clientY - sy) > 8)) cancel();
771
+ });
772
+ node.addEventListener("pointerup", cancel);
773
+ node.addEventListener("pointercancel", cancel);
774
+ node.addEventListener("pointerleave", cancel);
775
+ // Android WebView 有时仍会弹系统「复制」长按菜单, 再压一道
776
+ node.addEventListener("contextmenu", (e) => e.preventDefault());
777
+ node.addEventListener("click", (e) => {
778
+ if (fired) { fired = false; e.preventDefault(); e.stopPropagation(); }
779
+ });
780
+ }
781
+ /**
782
+ * v1.4.0: 子代理消耗行节点 (换行显示在状态条下方, 左→右 = 服务端给的创建顺序)。
783
+ * 抽成独立函数便于单测 —— 挂载点里的内联 JSX 测不到。
784
+ * 完整详情同时写进 `data-tip`(长按浮层读它, 也是单测抓手) 与 `title`(桌面 hover)。
785
+ * @param cost 会话消耗投影视图 (取其 subagents 与主币种)
786
+ * @returns 节点, 没有子代理时返回 null
787
+ */
788
+ function buildSubagentRow(cost) {
789
+ const list = Array.isArray(cost && cost.subagents) ? cost.subagents : [];
790
+ if (list.length === 0) return null;
791
+ const mainCur = (cost && cost.currency) || "CNY";
792
+ return react.createElement("span", { className: "dshadb_subs", key: "subs" },
793
+ list.map((s, i) => {
794
+ const f = formatSessionCost({ cost: s.cost, currency: mainCur, costByCurrency: s.costByCurrency, waiting: false });
795
+ const amt = f !== null && f.hasValue ? "~" + f.text : "~—";
796
+ const kind = s.mode === "continuable" ? "可续聊子代理" : "一次性子代理";
797
+ const models = (s.models || []).join(", ");
798
+ const tip = [
799
+ kind + " " + (s.label || "子代理"),
800
+ "消耗 " + (f !== null ? f.title : "—"),
801
+ models ? "模型 " + models : "",
802
+ s.id ? "ID " + String(s.id).slice(0, 8) : "",
803
+ ].filter(Boolean).join("\n");
804
+ return react.createElement("span", {
805
+ className: "dshadb_sub", key: s.id || ("s" + i), title: tip, "data-tip": tip,
806
+ ref: (n) => { if (n) bindSubagentTip(n, tip); },
807
+ }, [
808
+ react.createElement("span", { className: "dshadb_sub_dot", key: "d" }),
809
+ react.createElement("span", { className: "dshadb_sub_name", key: "n" }, s.label || "子代理"),
810
+ react.createElement("span", { className: "dshadb_sub_amt", key: "a" }, amt),
811
+ ]);
812
+ }));
813
+ }
513
814
  function formatMoney(amount, currency) {
514
815
  if (typeof amount !== "number" || isNaN(amount)) return currencySymbol(currency) + "0";
515
816
  if (currency === "%") return Math.round(amount) + "%";
516
817
  if (currency === "tokens") return (amount >= 1e6 ? (amount/1e6).toFixed(1)+"M" : amount >= 1e3 ? (amount/1e3).toFixed(1)+"K" : Math.round(amount) + " tok");
517
- const fixed = amount >= 1 ? 2 : amount >= 0.01 ? 3 : 4;
818
+ const abs = Math.abs(amount);
819
+ const fixed = abs === 0 || abs >= 1 ? 2 : abs >= 0.01 ? 3 : 4;
518
820
  return currencySymbol(currency) + amount.toFixed(fixed);
519
821
  }
520
822
  function formatTime(ms) { if (!ms || ms <= 0) return "—"; return new Date(ms).toLocaleTimeString(); }
521
- /** 状态条金额文字 (供 BarReadout 的 rAF 节流 state 使用) */
522
- function barAmountText(current, t) {
823
+ /** 状态条金额文字 (供 BarReadout 的 rAF 节流 state 使用)
824
+ * viaRelay=true 时走的是中转站, 官方余额与当前用量无关 → 一律显示「—」 */
825
+ function barAmountText(current, t, viaRelay) {
523
826
  if (!current) return "";
827
+ if (viaRelay) return "—";
524
828
  if (current.status === "ok") {
525
829
  const cur = current.currency || "USD";
526
830
  const num = typeof current.total === "number" ? current.total : 0;
@@ -529,15 +833,54 @@ window.__ModuleLoader__.load({
529
833
  return formatMoney(num, cur);
530
834
  }
531
835
  if (current.status === "no-key") return t("status.nokey");
532
- if (current.status === "no-balance-api") return t("status.noBalance");
836
+ if (current.status === "no-balance-api") return "";
533
837
  if (current.status === "local") return t("status.local");
534
838
  return t("status.err");
535
839
  }
536
- /** v0.5.3: 模型名 平台 id 映射 (仅强特征匹配, 避免误切; 自定义/中转由调用方按名称二次匹配) */
840
+ /** 当前 provider 是否为中转站 (非官方直连) —— 中转站无余额接口, 金额显示「—」
841
+ *
842
+ * 三层判定, 优先级由高到低 (与服务端注释一致):
843
+ * 1) 用户在设置里显式声明的官方直连名单 config.officialProviders (兜住一切误判)
844
+ * 2) 服务端读 settings.yaml 按 baseURL **域名** 判定的 config.providerKinds
845
+ * (官方域名白名单命中→official, 不命中→relay; 没写 baseURL 的 provider 不在表里)
846
+ * 3) DSH 官方插件命名约定: `-official` / `_official` 后缀 (如 deepseek-official)
847
+ * 都不命中 → 按中转站处理。保守取向: 宁可不显示余额, 也不显示错的余额。
848
+ * (provider 为空表示还不知道走哪条路 → 不判中转站, 照常显示所选平台余额) */
849
+ function isRelayProvider(provider, config) {
850
+ if (typeof provider !== "string" || provider === "") return false;
851
+ const p = provider.toLowerCase();
852
+ // 第 1 层: 用户显式名单
853
+ const official = config && config.officialProviders;
854
+ if (Array.isArray(official)) {
855
+ for (const name of official) {
856
+ if (typeof name === "string" && name.trim().toLowerCase() === p) return false;
857
+ }
858
+ }
859
+ // 第 2 层: 服务端按 baseURL 域名判定
860
+ const kinds = config && config.providerKinds;
861
+ if (kinds && typeof kinds === "object") {
862
+ const kind = kinds[provider] !== undefined ? kinds[provider] : kinds[p];
863
+ if (kind === "official") return false;
864
+ if (kind === "relay") return true;
865
+ }
866
+ // 第 3 层: 官方插件后缀约定
867
+ if (p.endsWith("-official") || p.endsWith("_official")) return false;
868
+ return true;
869
+ }
870
+ /** v0.5.3: 模型名 → 平台 id 映射 (仅强特征匹配, 避免误切; 自定义/中转由调用方按名称二次匹配)
871
+ * 只按模型名映射, 不因中转站返回空 —— 否则状态条不会跟着模型切换。
872
+ * 中转站不显示余额由 BarReadout 的 viaRelay 单独处理。 */
537
873
  function modelToPlatform(model) {
538
874
  if (!model || typeof model !== "string") return "";
539
875
  const m = model.toLowerCase();
540
876
  if (/^deepseek/.test(m)) return "deepseek";
877
+ if (/^gpt/.test(m) || /^o[1-3]([-.]|$)/.test(m) || m.includes("openai")) return "openai";
878
+ if (/^claude/.test(m)) return "claude";
879
+ if (/^gemini/.test(m)) return "gemini";
880
+ if (/^qwen/.test(m)) return "qwen";
881
+ if (/^doubao/.test(m) || /^seed[-_]/.test(m)) return "doubao";
882
+ if (/hunyuan/.test(m)) return "hunyuan";
883
+ if (/^mimo/.test(m)) return "mimo";
541
884
  if (/glm|chatglm/.test(m)) return "zhipu";
542
885
  if (/kimi|moonshot/.test(m)) return "moonshot";
543
886
  if (/^step[-_]/.test(m)) return "stepfun";
@@ -574,12 +917,17 @@ window.__ModuleLoader__.load({
574
917
 
575
918
 
576
919
  //#region 一层 bar
577
- function BarReadout({ t, onOpen, onOpenSettings, selectedId, onSelect, config, cost }) {
920
+ function BarReadout({ t, onOpen, onOpenSettings, selectedId, onSelect, config, cost, provider }) {
578
921
  const data = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
579
922
  const balances = data.balances || [];
580
923
  let current = balances.find(b => b.platform === selectedId);
581
924
  if (!current) current = balances.find(b => b.status === "ok") || balances[0];
582
- if (current && selectedId !== current.platform && current.status === "ok") onSelect(current.platform);
925
+ // 当前走中转站时不显示官方余额 (中转站没有余额接口)
926
+ const viaRelay = isRelayProvider(provider, config);
927
+ // 选中项与当前平台不同步时, commit 后再同步 (render 阶段调用会触发 React 18 警告)
928
+ react.useEffect(() => {
929
+ if (current && selectedId !== current.platform && current.status === "ok") onSelect(current.platform);
930
+ });
583
931
 
584
932
  // v0.5.0 手机端: 点击状态条开看板, 长按 500ms 弹设置菜单 (替代常驻齿轮)
585
933
  const pressTimer = react.useRef(null);
@@ -600,19 +948,20 @@ window.__ModuleLoader__.load({
600
948
  const handleBarClick = () => { if (longPressed.current) return; onOpen(); };
601
949
 
602
950
  // v0.5.0: 金额文字 rAF 延迟一帧写入, 合并同一帧内的重复变化
603
- const [displayAmount, setDisplayAmount] = react.useState(barAmountText(current, t));
951
+ const [displayAmount, setDisplayAmount] = react.useState(barAmountText(current, t, viaRelay));
604
952
  react.useEffect(() => {
605
- const txt = barAmountText(current, t);
953
+ const txt = barAmountText(current, t, viaRelay);
606
954
  const raf = requestAnimationFrame(() => setDisplayAmount((prev) => (prev === txt ? prev : txt)));
607
955
  return () => cancelAnimationFrame(raf);
608
956
  });
609
957
 
958
+ const hasPeakInfo = typeof config?.isPeak === "boolean";
610
959
  const isPeak = config?.isPeak === true;
611
960
  const isWknd = config?.isWeekend === true;
612
961
 
613
962
  // 峰谷标记 (趣味版: 梁文峰 ☀️ / 梁文谷 🌙)
614
963
  let peakNode = null;
615
- if (isPeak !== undefined) {
964
+ if (hasPeakInfo) {
616
965
  const peakTitle = isPeak
617
966
  ? t("peak.now") + " " + t("peak.hint")
618
967
  : (isWknd ? t("valley.weekend") : t("valley.now"));
@@ -638,7 +987,9 @@ window.__ModuleLoader__.load({
638
987
  ]);
639
988
 
640
989
  const meta = metaFor(current.platform), level = getLevel(current, config);
641
- const dotClass = "dshadb_bar_dot " + (level === "ok" ? "dshadb_bar_dot_ok" : level === "warn" ? "dshadb_bar_dot_warn" : "dshadb_bar_dot_err");
990
+ const isNoBalance = viaRelay || (current && current.status === "no-balance-api");
991
+ const dotClass = "dshadb_bar_dot" + (isNoBalance ? "" : " " + (level === "ok" ? "dshadb_bar_dot_ok" : level === "warn" ? "dshadb_bar_dot_warn" : "dshadb_bar_dot_err"));
992
+ const dotStyle = isNoBalance ? { background: "#c7c7cc" } : undefined;
642
993
  const amtClass = "dshadb_bar_amount " + (level === "ok" ? "dshadb_bar_ok" : level === "warn" ? "dshadb_bar_warn" : "dshadb_bar_err");
643
994
  return react.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "4px" } }, [
644
995
  react.createElement("span", {
@@ -650,7 +1001,7 @@ window.__ModuleLoader__.load({
650
1001
  react.createElement("span", { className: "dshadb_bar_logo", key: "logo" }, renderLogo(current)),
651
1002
  react.createElement("span", { className: "dshadb_bar_name", key: "name" }, meta.name),
652
1003
  react.createElement("span", { className: amtClass, key: "amount" }, displayAmount),
653
- react.createElement("span", { className: dotClass, key: "dot" }),
1004
+ react.createElement("span", { className: dotClass, style: dotStyle, key: "dot" }),
654
1005
  peakNode,
655
1006
  react.createElement("span", { className: "dshadb_bar_cost", title: cost?.title, key: "cost" }, cost?.text || "~—"),
656
1007
  ]),
@@ -659,7 +1010,6 @@ window.__ModuleLoader__.load({
659
1010
  //#endregion
660
1011
 
661
1012
  //#region 折叠分组 (带展开过渡动画 + 分类色点)
662
- const GROUP_DOT = { domestic: "domestic", abroad: "abroad", local: "local", relay: "relay", custom: "custom" };
663
1013
  function GroupSection({ label, count, open, onToggle, children }) {
664
1014
  return react.createElement("div", { key: label }, [
665
1015
  react.createElement("div", { className: "dshadb_group_head", onClick: onToggle, role: "button", tabIndex: 0, onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggle(); } } }, [
@@ -682,7 +1032,7 @@ window.__ModuleLoader__.load({
682
1032
  function DashboardDrawer({ isOpen, onClose, t, selectedId, onSelect, onOpenDetail, onAddRelay, onAddCustom, onOpenSettings, config, currentModel }) {
683
1033
  const data = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
684
1034
  const [isRefreshing, setRefreshing] = react.useState(false);
685
- const [openGroups, setOpenGroups] = react.useState({ domestic: false, abroad: false, relay: false, local: false, custom: false });
1035
+ const [openGroups, setOpenGroups] = react.useState({ domestic: false, abroad: false, relay: false, custom: false, nobalance: false });
686
1036
  react.useEffect(() => {
687
1037
  if (!isOpen) return;
688
1038
  const handleKey = (e) => { if (e.key === "Escape") onClose(); };
@@ -696,10 +1046,17 @@ window.__ModuleLoader__.load({
696
1046
  const okCount = balances.filter(b => b.status === "ok").length;
697
1047
 
698
1048
  const groups = { domestic: [], abroad: [], relay: [], custom: [] };
1049
+ const noBalanceBrands = [];
699
1050
  let selectedBalance = null;
1051
+ const showBrands = !!(config && config.showNoBalanceBrands);
700
1052
  for (const b of balances) {
1053
+ if (b.platform === selectedId) { selectedBalance = b; continue; }
1054
+ // 无余额模型品牌: 设置开关开启才显示, 放入单独分组
1055
+ if (b.status === "no-balance-api" || b.status === "noBalance") {
1056
+ if (showBrands) noBalanceBrands.push(b);
1057
+ continue;
1058
+ }
701
1059
  const cat = b.category || metaFor(b.platform).cat;
702
- if (b.platform === selectedId) { selectedBalance = b; continue; } // ② 选中模型从分组排除,避免重复
703
1060
  if (cat === "海外" || cat === "官方") groups.abroad.push(b);
704
1061
  else if (cat === "中转站") groups.relay.push(b);
705
1062
  else if (cat === "自定义") groups.custom.push(b);
@@ -711,17 +1068,21 @@ window.__ModuleLoader__.load({
711
1068
  const meta = metaFor(b.platform), level = getLevel(b, config), isSelected = b.platform === selectedId;
712
1069
  const cardClass = "dshadb_card " + (isSelected ? "dshadb_card_active " : "");
713
1070
  // descText 逻辑
1071
+ // v1.4.0 修复: 原分支硬编码 "· 配额正常", 与三色灯(level)完全无关 —— GLM 配额耗尽时
1072
+ // (level=err、note 已写「配额已用完(0), 待重置」) 会渲染成「…待重置 · 配额正常」, 自相矛盾。
1073
+ // 改为统一按 level 取状态标签, 保证文案与灯色一致。
1074
+ // (sessionCost/modelId 服务端目前从不产出, 该分支保留仅为将来兼容, 不再是唯一出口。)
714
1075
  let descText;
715
1076
  if (b.status === "ok" && b.sessionCost != null) {
716
1077
  descText = (b.modelId || "—") + " · 会话消耗 " + (typeof b.sessionCost === "number" ? b.sessionCost.toFixed(2) : b.sessionCost);
717
1078
  } else if (b.status === "ok" && b.note) {
718
- descText = b.note + " · 配额正常";
1079
+ descText = b.note + " · " + statusLabel(level, t);
719
1080
  } else if (b.status === "ok") {
720
- descText = "已启用";
1081
+ descText = statusLabel(level, t);
721
1082
  } else if (b.status === "local") {
722
1083
  descText = "本地模型";
723
1084
  } else if (b.status === "noBalance" || b.status === "no-balance-api") {
724
- descText = "自动探测余额接口";
1085
+ descText = "按价格表估算消耗";
725
1086
  } else {
726
1087
  descText = statusLabel(level, t);
727
1088
  }
@@ -734,15 +1095,16 @@ window.__ModuleLoader__.load({
734
1095
  amtClass = level === "ok" ? "dshadb_card_status_ok" : level === "warn" ? "dshadb_card_status_warn" : "dshadb_card_status_err";
735
1096
  amount = val;
736
1097
  } else if (b.status === "no-key") { amount = t("status.nokey"); amtClass = "dshadb_card_amount_sub"; }
737
- else if (b.status === "no-balance-api") { amount = t("status.noBalance"); amtClass = "dshadb_card_amount_sub"; }
1098
+ else if (b.status === "no-balance-api") { amount = ""; amtClass = "dshadb_card_amount_sub"; }
738
1099
  else if (b.status === "local") { amount = t("status.local"); amtClass = "dshadb_card_amount_sub"; }
739
1100
  else { amount = b.error || t("error.unknown"); amtClass = "dshadb_card_status_err"; }
740
- const stClass = level === "ok" ? "dshadb_card_status_ok" : level === "warn" ? "dshadb_card_status_warn" : "dshadb_card_status_err";
741
- const statusText = statusLabel(level, t);
1101
+ const isNoBalance = b.status === "no-balance-api";
1102
+ const stClass = isNoBalance ? "dshadb_card_amount_sub" : (level === "ok" ? "dshadb_card_status_ok" : level === "warn" ? "dshadb_card_status_warn" : "dshadb_card_status_err");
1103
+ const statusText = isNoBalance ? "—" : statusLabel(level, t);
742
1104
  const rightNode = react.createElement("div", { className: "dshadb_card_right", key: "right" }, [
743
1105
  react.createElement("span", { className: "dshadb_card_amount" + (amtClass ? " " + amtClass : ""), key: "amt" }, amount),
744
1106
  react.createElement("span", { className: "dshadb_card_status " + stClass, key: "st" }, [
745
- react.createElement("span", { className: "dshadb_status_dot dshadb_status_dot_" + (level === "ok" ? "ok" : level === "warn" ? "warn" : "err"), key: "dot" }),
1107
+ react.createElement("span", { className: "dshadb_status_dot" + (isNoBalance ? "" : " dshadb_status_dot_" + (level === "ok" ? "ok" : level === "warn" ? "warn" : "err")), key: "dot", style: isNoBalance ? { background: "#c7c7cc" } : undefined }),
746
1108
  statusText,
747
1109
  ]),
748
1110
  ]);
@@ -779,6 +1141,7 @@ window.__ModuleLoader__.load({
779
1141
  // 国内平台 (含deepseek)
780
1142
  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,
781
1143
  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,
1144
+ noBalanceBrands.length ? react.createElement(GroupSection, { label: t("brands"), count: noBalanceBrands.length, open: openGroups.nobalance, onToggle: () => toggle("nobalance"), key: "g_brands" }, noBalanceBrands.map(card)) : null,
782
1145
  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,
783
1146
  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,
784
1147
  // ③ 添加按钮居中两边对齐
@@ -793,8 +1156,10 @@ window.__ModuleLoader__.load({
793
1156
  }
794
1157
 
795
1158
  return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, [
1159
+ // v1.4.0: 侧滑守卫 —— 让手机壳的「左边缘开侧边栏」手势层放弃识别 (CSS 与 AGENTS.md ① 有详解)
1160
+ react.createElement("div", { className: "dshadb_swipeguard", "aria-hidden": "true", key: "swipeguard" }),
796
1161
  react.createElement("div", { className: "dshadb_drawer", onClick: (e) => e.stopPropagation(), key: "drawer" }, [
797
- react.createElement("div", { className: "dshadb_handle", key: "handle" }, react.createElement("div", { className: "dshadb_handle_bar" })),
1162
+ react.createElement(SwipeHandle, { onClose, key: "handle" }),
798
1163
  react.createElement("div", { className: "dshadb_header", key: "header" }, [
799
1164
  react.createElement("div", { className: "dshadb_header_left", key: "left" }, [
800
1165
  // 模型尾椎: 从 currentModel 提取变体名, 如 deepseek-v4-flash → DSH·V4
@@ -818,7 +1183,7 @@ window.__ModuleLoader__.load({
818
1183
  t("title"),
819
1184
  ]),
820
1185
  react.createElement("div", { className: "dshadb_header_sub", key: "sub" },
821
- t("all.ok", { n: okCount }) + " · " + t("refreshing")
1186
+ t("all.ok", { n: okCount }) + (isRefreshing ? " · " + t("refreshing") : "")
822
1187
  ),
823
1188
  ]),
824
1189
  react.createElement("button", {
@@ -833,7 +1198,7 @@ window.__ModuleLoader__.load({
833
1198
  data.fetchedAt ? react.createElement("div", { className: "dshadb_footer", key: "footer" }, [
834
1199
  "更新于 " + formatTime(data.fetchedAt),
835
1200
  react.createElement("span", { className: "dshadb_footer_dot", key: "d1" }),
836
- react.createElement("span", { className: "dshadb_refresh_badge", key: "ri" }, "每 " + ((config && config.clientPollIntervalMs) ? Math.round(config.clientPollIntervalMs / 1000) : 5) + "s 刷新"),
1201
+ react.createElement("button", { type: "button", className: "dshadb_refresh_badge", key: "ri", onClick: handleRefresh, title: t("refresh") }, "每 " + ((config && config.clientPollIntervalMs) ? Math.round(config.clientPollIntervalMs / 1000) : 5) + "s 刷新"),
837
1202
  ]) : null,
838
1203
  ]),
839
1204
  ]);
@@ -865,21 +1230,9 @@ window.__ModuleLoader__.load({
865
1230
  const b = (data.balances || []).find(x => x.platform === platformId);
866
1231
  if (!b) return null;
867
1232
  const meta = metaFor(b.platform), level = getLevel(b, config);
868
- 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) ]);
869
- const costVal = cost && cost.cost > 0 ? formatMoney(cost.cost, cost.currency || "CNY") : "—";
1233
+ const costFmtDetail = formatSessionCost(cost);
1234
+ const costVal = costFmtDetail !== null && costFmtDetail.hasValue ? costFmtDetail.text : "—";
870
1235
  const cur = b.currency || "USD";
871
- const detailRows = [];
872
- if (b.status === "ok") {
873
- detailRows.push(row(t("detail.total"), formatMoney(b.percent != null ? b.percent : b.total, b.percent != null ? "%" : cur) + (b.percent != null ? "%" : "")));
874
- // v0.5.6: 充值行仅在与余额有差异时显示 (未消耗时 充值==余额 两行重复); 使用行为 0 不显示
875
- if (b.topup != null && Math.abs(b.topup - (b.total || 0)) > 1e-9) detailRows.push(row(t("detail.topup"), formatMoney(b.topup, cur)));
876
- if (b.used != null && b.used > 0) detailRows.push(row(t("detail.used"), formatMoney(b.used, cur)));
877
- if (b.note) detailRows.push(row(t("detail.note"), b.note));
878
- if (b.staleNote) detailRows.push(row("数据状态", b.staleNote));
879
- } else {
880
- detailRows.push(row(t("detail.note"), b.error || "—"));
881
- }
882
- detailRows.push(row(t("detail.sessionCost"), costVal));
883
1236
 
884
1237
  // DeepSeek 峰谷趣味卡片
885
1238
  let peakCard = null;
@@ -925,8 +1278,9 @@ window.__ModuleLoader__.load({
925
1278
 
926
1279
  // v0.5.3: 回退为底部抽屉样式 (全屏卡片观感不佳, 复用看板同款 scrim+drawer)
927
1280
  return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, [
1281
+ react.createElement("div", { className: "dshadb_swipeguard", "aria-hidden": "true", key: "swipeguard" }),
928
1282
  react.createElement("div", { className: "dshadb_drawer", style: { maxHeight: "70vh" }, onClick: (e) => e.stopPropagation(), key: "drawer" }, [
929
- react.createElement("div", { className: "dshadb_handle", key: "handle" }, react.createElement("div", { className: "dshadb_handle_bar" })),
1283
+ react.createElement(SwipeHandle, { onClose, key: "handle" }),
930
1284
  react.createElement("div", { className: "dshadb_header", key: "header" }, [
931
1285
  react.createElement("div", { className: "dshadb_header_left", key: "left" }, [
932
1286
  react.createElement("div", { className: "dshadb_header_title", key: "title", style: { fontSize: "18px" } }, meta.name),
@@ -987,8 +1341,8 @@ window.__ModuleLoader__.load({
987
1341
  return metrics.length > 0 ? react.createElement("div", { className: "dshadb_detail_metrics", key: "metrics" }, metrics) : null;
988
1342
  })(),
989
1343
  // staleNote(暂存数据提示)
990
- b.staleNote ? row("数据状态", b.staleNote) : null,
991
- b.status !== "ok" ? row(t("detail.note"), b.error || "—") : null,
1344
+ b.staleNote ? react.createElement("div", { className: "dshadb_detail_info", key: "stale" }, b.staleNote) : null,
1345
+ b.status !== "ok" ? react.createElement("div", { className: "dshadb_detail_info", key: "err" }, b.error || "—") : null,
992
1346
  peakCard,
993
1347
  priceCard,
994
1348
  ]),
@@ -1006,8 +1360,26 @@ window.__ModuleLoader__.load({
1006
1360
  const [safe, setSafe] = react.useState(50);
1007
1361
  const [warn, setWarn] = react.useState(10);
1008
1362
  const [currency, setCurrency] = react.useState("CNY");
1363
+ // v1.3.2: 海外模型独立计价货币 follow|USD|CNY
1364
+ const [overseasCurrency, setOverseasCurrency] = react.useState("follow");
1009
1365
  const [refreshSec, setRefreshSec] = react.useState(5);
1010
1366
  const [whaleOn, setWhaleOn] = react.useState(!!(config && config.whaleEnabled));
1367
+ const [showBrands, setShowBrands] = react.useState(!!(config && config.showNoBalanceBrands));
1368
+ // 官方直连 provider 名单 (第 1 层判定): 多行文本, 逗号/换行分隔
1369
+ const [officialText, setOfficialText] = react.useState(
1370
+ Array.isArray(config && config.officialProviders) ? config.officialProviders.join(", ") : "");
1371
+ // 服务端按 baseURL 域名自动判定的结果 (只读, 供用户判断还需不需要手填)
1372
+ const [providerKinds, setProviderKinds] = react.useState(
1373
+ (config && config.providerKinds) || {});
1374
+ // v1.4.0「真自动」: 从 settings.yaml 自动发现的 DSH provider + 用户关掉的名单。
1375
+ // 服务端只下发「名字/baseURL/是否官方」, **不下发 key**。
1376
+ const [dshProviders, setDshProviders] = react.useState(
1377
+ Array.isArray(config && config.dshProviders) ? config.dshProviders : []);
1378
+ const [dshOff, setDshOff] = react.useState(
1379
+ Array.isArray(config && config.dshProviderOptOut) ? config.dshProviderOptOut : []);
1380
+ // v1.4.0: 来自 DSH 的中转站列表默认收起 (跟首页分组同一套折叠手法)。
1381
+ // 维护者原话「应该折叠的是来自 DSH 中转站下面的那一罗列」—— 「自动判定结果」保持平铺不折。
1382
+ const [showDsh, setShowDsh] = react.useState(false);
1011
1383
  // v1.1.0: 大肥鱼细项 (独立页签, 走 /api-dashboard/whale/settings, 与看板主配置分开)
1012
1384
  const [wf, setWf] = react.useState({
1013
1385
  scale: 1, peekRatio: 0.5, soundOn: true, soundSet: "duck",
@@ -1038,13 +1410,15 @@ window.__ModuleLoader__.load({
1038
1410
  }, []);
1039
1411
  react.useEffect(() => { if (isOpen) checkUpdate(false); }, [isOpen]); // 打开面板自动检查
1040
1412
  const installUpdate = () => {
1041
- if (!window.confirm(t("update.available") + " v" + (upd.info?.remote || "") + " ?")) return;
1042
1413
  setUpd(s => ({ ...s, phase: "installing" }));
1043
1414
  fetch("/api-dashboard/update/install", { method: "POST" })
1044
1415
  .then(r => r.json().then(d => ({ code: r.status, body: d })))
1045
1416
  .then(({ code, body }) => {
1046
1417
  if (code === 200 && body.ok) {
1047
1418
  setUpd({ phase: "done", info: { ok: true, current: body.installed, remote: body.installed, hasUpdate: false }, msg: t("update.done") });
1419
+ // v1.3.3: 更新完成后发 Toast 通知 —— 设置面板底部的 11px 灰色提示太容易被忽略,
1420
+ // 移动端尤其如此。Toast 在屏幕中央弹出, 不会被错过。
1421
+ try { fetch("/app/toast?text=" + encodeURIComponent("插件已更新到 v" + body.installed + ",重启 Web GUI 生效"), { mode: "no-cors" }).catch(() => {}); } catch {}
1048
1422
  } else {
1049
1423
  setUpd(s => ({ ...s, phase: "fail", msg: body.error === "already up to date" ? t("update.latest") : t("update.fail") }));
1050
1424
  checkUpdate(true);
@@ -1065,6 +1439,7 @@ window.__ModuleLoader__.load({
1065
1439
  setSafe(config?.safeThreshold ?? 50);
1066
1440
  setWarn(config?.warnThreshold ?? 10);
1067
1441
  setCurrency(config?.currency ?? "CNY");
1442
+ setOverseasCurrency(config?.overseasCurrency ?? "follow");
1068
1443
  setRefreshSec(Math.round((config?.clientPollIntervalMs || 5000) / 1000) || 5);
1069
1444
  setWhaleOn(!!(config && config.whaleEnabled));
1070
1445
  let cancelled = false;
@@ -1074,7 +1449,12 @@ window.__ModuleLoader__.load({
1074
1449
  setRelays(d.customRelays || []);
1075
1450
  setModels(d.customModels || []);
1076
1451
  if (d.refreshIntervalSec) setRefreshSec(d.refreshIntervalSec);
1452
+ if (typeof d.overseasCurrency === "string") setOverseasCurrency(d.overseasCurrency);
1077
1453
  if (typeof d.whaleEnabled === "boolean") setWhaleOn(d.whaleEnabled);
1454
+ if (Array.isArray(d.officialProviders)) setOfficialText(d.officialProviders.join(", "));
1455
+ if (d.providerKinds && typeof d.providerKinds === "object") setProviderKinds(d.providerKinds);
1456
+ if (Array.isArray(d.dshProviders)) setDshProviders(d.dshProviders);
1457
+ if (Array.isArray(d.dshProviderOptOut)) setDshOff(d.dshProviderOptOut);
1078
1458
  }
1079
1459
  }).finally(() => { if (!cancelled) setLoading(false); });
1080
1460
  // 大肥鱼细项与主配置并行拉取, 失败保持默认值
@@ -1096,13 +1476,18 @@ window.__ModuleLoader__.load({
1096
1476
  }, [isOpen]);
1097
1477
  if (!isOpen) return null;
1098
1478
  const save = async () => {
1479
+ const nextRefreshSec = Math.min(Math.max(Number(refreshSec) || 5, 5), 60)
1480
+ setRefreshSec(nextRefreshSec)
1099
1481
  setSaving(true);
1100
1482
  try {
1101
1483
  await fetch("/api-dashboard/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({
1102
1484
  customRelays: relays, customModels: models,
1103
- safeThreshold: Number(safe), warnThreshold: Number(warn), currency,
1104
- refreshIntervalSec: Math.min(Math.max(Number(refreshSec) || 5, 5), 60),
1485
+ safeThreshold: Number(safe), warnThreshold: Number(warn), currency, overseasCurrency,
1486
+ refreshIntervalSec: nextRefreshSec,
1105
1487
  whaleEnabled: !!whaleOn,
1488
+ officialProviders: officialText,
1489
+ // v1.4.0「真自动」: 关掉的 DSH provider (空数组 = 全部启用)
1490
+ dshProviderOptOut: dshOff,
1106
1491
  }) });
1107
1492
  // v1.1.0: 保存后立刻强拉一次, 让开关类设置(大肥鱼)即时生效, 不必等下一轮轮询
1108
1493
  try { await store.forceRefresh(); } catch (e) { /* 忽略 */ }
@@ -1136,6 +1521,17 @@ window.__ModuleLoader__.load({
1136
1521
  await store.forceRefresh();
1137
1522
  } catch (e) { /* 网络失败: 本地仍生效, 下次保存/刷新会对齐 */ }
1138
1523
  };
1524
+ // v1.1.3: 显示模型品牌开关即时生效 (同 toggleWhale 模式, 不依赖「保存并生效」按钮)
1525
+ const toggleBrands = async (next) => {
1526
+ setShowBrands(next);
1527
+ try {
1528
+ await fetch("/api-dashboard/config", {
1529
+ method: "POST", headers: { "Content-Type": "application/json" },
1530
+ body: JSON.stringify({ showNoBalanceBrands: !!next }),
1531
+ });
1532
+ await store.forceRefresh();
1533
+ } catch (e) { /* 网络失败: 本地仍生效, 下次保存/刷新会对齐 */ }
1534
+ };
1139
1535
  const qtLabel = (q) => t("qt." + (q || "auto"));
1140
1536
 
1141
1537
  // v1.1.0: 大肥鱼细项即时生效 + 即时落盘 (滑块/开关都不依赖「保存并生效」按钮)
@@ -1175,24 +1571,93 @@ window.__ModuleLoader__.load({
1175
1571
  react.createElement("div", { className: "dshadb_grid2", key: "misc" }, [
1176
1572
  react.createElement("div", { key: "cur" }, [
1177
1573
  react.createElement("label", { className: "dshadb_label", key: "cur_l" }, t("settings.currency")),
1178
- react.createElement("select", { className: "dshadb_field", value: currency, onChange: (e) => setCurrency(e.target.value), key: "cur_s" }, [
1574
+ react.createElement("select", { className: "dshadb_field dshadb_field_select", value: currency, onChange: (e) => setCurrency(e.target.value), key: "cur_s" }, [
1179
1575
  react.createElement("option", { value: "CNY", key: "c1" }, t("cmn")),
1180
1576
  react.createElement("option", { value: "USD", key: "c2" }, t("usd")),
1181
1577
  ]),
1182
1578
  ]),
1579
+ // v1.3.2: 海外模型 (gpt/claude/gemini/grok...) 可独立选币种, follow=跟随上面的主货币
1580
+ react.createElement("div", { key: "ocur" }, [
1581
+ react.createElement("label", { className: "dshadb_label", key: "ocur_l" }, t("settings.overseasCurrency")),
1582
+ react.createElement("select", { className: "dshadb_field dshadb_field_select", value: overseasCurrency, onChange: (e) => setOverseasCurrency(e.target.value), key: "ocur_s" }, [
1583
+ react.createElement("option", { value: "follow", key: "o0" }, t("settings.overseasFollow")),
1584
+ react.createElement("option", { value: "USD", key: "o1" }, t("usd")),
1585
+ react.createElement("option", { value: "CNY", key: "o2" }, t("cmn")),
1586
+ ]),
1587
+ react.createElement("span", { style: { fontSize: "10px", color: "var(--dsw-alias-label-tertiary)" }, key: "ocur_h" }, t("settings.overseasHint")),
1588
+ ]),
1183
1589
  react.createElement("div", { key: "refresh" }, [
1184
1590
  react.createElement("label", { className: "dshadb_label", key: "rf_l" }, t("settings.refresh")),
1185
- 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" }),
1591
+ react.createElement("input", { className: "dshadb_field", type: "number", min: 1, max: 60, step: 1, value: refreshSec, onChange: (e) => setRefreshSec(Math.min(Math.max(Number(e.target.value) || 1, 1), 60)), key: "rf_i" }),
1186
1592
  react.createElement("span", { style: { fontSize: "10px", color: "var(--dsw-alias-label-tertiary)" }, key: "rf_h" }, t("settings.refreshHint")),
1187
1593
  ]),
1188
1594
  ]),
1595
+ // 无余额模型品牌开关
1596
+ react.createElement("div", { className: "dshadb_settings_row", key: "brands", style: { margin: "2px 0 10px" } }, [
1597
+ react.createElement("div", { className: "dshadb_settings_row_main", key: "bm" }, [
1598
+ react.createElement("span", { className: "dshadb_settings_row_title", key: "bt" }, t("settings.showBrands")),
1599
+ react.createElement("span", { className: "dshadb_settings_row_sub", key: "bs" }, t("settings.showBrandsHint")),
1600
+ ]),
1601
+ react.createElement(Switch, { checked: showBrands, onChange: toggleBrands, key: "bsw" }),
1602
+ ]),
1603
+ // 官方直连 provider 名单 (第 1 层判定, 覆盖自动判定)
1604
+ react.createElement("div", { key: "official" }, [
1605
+ react.createElement("label", { className: "dshadb_label", key: "of_l" }, t("settings.official")),
1606
+ react.createElement("textarea", {
1607
+ className: "dshadb_field dshadb_textarea", rows: 2, value: officialText,
1608
+ placeholder: "deepseek, zhipu-official",
1609
+ onChange: (e) => setOfficialText(e.target.value), key: "of_i",
1610
+ }),
1611
+ react.createElement("span", { style: { fontSize: "10px", color: "var(--dsw-alias-label-tertiary)", display: "block", lineHeight: 1.5 }, key: "of_h" }, t("settings.officialHint")),
1612
+ react.createElement("div", { className: "dshadb_kinds", key: "of_k" }, [
1613
+ react.createElement("span", { className: "dshadb_kinds_title", key: "kt" }, t("settings.officialAuto")),
1614
+ Object.keys(providerKinds || {}).length === 0
1615
+ ? react.createElement("span", { className: "dshadb_kinds_empty", key: "ke" }, t("settings.officialAutoEmpty"))
1616
+ : react.createElement("div", { className: "dshadb_kinds_list", key: "kl" },
1617
+ Object.keys(providerKinds).sort().map((name) => react.createElement("span", {
1618
+ className: "dshadb_kind" + (providerKinds[name] === "official" ? " dshadb_kind_official" : " dshadb_kind_relay"),
1619
+ key: name,
1620
+ }, name + " · " + t(providerKinds[name] === "official" ? "settings.officialKindOfficial" : "settings.officialKindRelay")))),
1621
+ ]),
1622
+ ]),
1623
+ // v1.4.0「真自动」: 从 settings.yaml 自动发现的中转站 (默认全开, 可单独关;
1624
+ // 官方直连与没写 baseURL 的只展示、不可开 —— 前者归预设平台管, 后者按铁律 9 不表态)
1625
+ // v1.4.0: 列表默认收起, 直接复用首页的分组折叠组件 GroupSection
1626
+ // (表头 + 条数 + 箭头 + max-height 过渡), 维护者要求「跟首页一样的折叠方法」。
1627
+ dshProviders.length > 0
1628
+ ? react.createElement(GroupSection, {
1629
+ label: t("settings.dshProviders"), count: dshProviders.length,
1630
+ open: showDsh, onToggle: () => setShowDsh(!showDsh), key: "dshprov",
1631
+ }, [
1632
+ react.createElement("span", { className: "dshadb_kinds_empty", key: "dp_h", style: { marginBottom: "6px" } }, t("settings.dshProvidersHint")),
1633
+ react.createElement("div", { key: "dp_list" }, dshProviders.map((p) => {
1634
+ const locked = p.kind === "official" || p.kind === "no-base-url";
1635
+ const name = String(p.name || "");
1636
+ const off = dshOff.some((n) => String(n).toLowerCase() === name.toLowerCase());
1637
+ return react.createElement("div", { className: "dshadb_settings_row", key: name, style: { margin: "6px 0" } }, [
1638
+ react.createElement("div", { className: "dshadb_settings_row_main", key: "m" }, [
1639
+ react.createElement("span", { className: "dshadb_settings_row_title", key: "n" }, name),
1640
+ react.createElement("span", { className: "dshadb_settings_row_sub", key: "s" }, p.baseURL || t("settings.dshNoBaseUrl")),
1641
+ ]),
1642
+ locked
1643
+ ? react.createElement("span", { className: "dshadb_refresh_badge", key: "lock" },
1644
+ t(p.kind === "official" ? "settings.officialKindOfficial" : "settings.dshNoBaseUrl"))
1645
+ : react.createElement(Switch, {
1646
+ checked: !off, key: "sw",
1647
+ // 关掉 = 加进 optOut; 打开 = 从 optOut 移除 (大小写不敏感, 与后端一致)
1648
+ onChange: (next) => setDshOff(next
1649
+ ? dshOff.filter((n) => String(n).toLowerCase() !== name.toLowerCase())
1650
+ : dshOff.concat([name])),
1651
+ }),
1652
+ ]);
1653
+ })),
1654
+ ])
1655
+ : null,
1189
1656
  ]);
1190
1657
 
1191
1658
  // 中转站区块
1192
1659
  const relaySection = react.createElement("div", { className: "dshadb_settings_section", key: "s_relays" }, [
1193
1660
  react.createElement("div", { style: { marginTop: "2px", marginBottom: "8px" }, key: "relay_form" }, [
1194
- react.createElement("label", { className: "dshadb_label", key: "r1" }, "中转站名称"),
1195
- react.createElement("input", { className: "dshadb_field", value: form.name, placeholder: "我的中转站", onChange: (e) => setForm({ ...form, name: e.target.value }), key: "ri1" }),
1196
1661
  react.createElement("label", { className: "dshadb_label", key: "r1" }, "中转站名称"),
1197
1662
  react.createElement("input", { className: "dshadb_field", value: form.name, placeholder: "我的中转站", onChange: (e) => setForm({ ...form, name: e.target.value }), key: "ri1" }),
1198
1663
  react.createElement("label", { className: "dshadb_label", key: "r2" }, "Base URL"),
@@ -1229,7 +1694,7 @@ window.__ModuleLoader__.load({
1229
1694
  ]),
1230
1695
  react.createElement("div", { key: "m4" }, [
1231
1696
  react.createElement("label", { className: "dshadb_label", key: "ml4" }, t("settings.queryType")),
1232
- react.createElement("select", { className: "dshadb_field", value: mform.queryType, onChange: (e) => setMform({ ...mform, queryType: e.target.value }), key: "mi4" }, [
1697
+ react.createElement("select", { className: "dshadb_field dshadb_field_select", value: mform.queryType, onChange: (e) => setMform({ ...mform, queryType: e.target.value }), key: "mi4" }, [
1233
1698
  react.createElement("option", { value: "auto", key: "q0" }, t("qt.auto")),
1234
1699
  react.createElement("option", { value: "openai", key: "q1" }, t("qt.openai")),
1235
1700
  react.createElement("option", { value: "quota", key: "q2" }, t("qt.quota")),
@@ -1255,7 +1720,7 @@ window.__ModuleLoader__.load({
1255
1720
  ]),
1256
1721
  react.createElement("div", { key: "m7" }, [
1257
1722
  react.createElement("label", { className: "dshadb_label", key: "ml7" }, t("settings.currency")),
1258
- react.createElement("select", { className: "dshadb_field", value: mform.currency || "CNY", onChange: (e) => setMform({ ...mform, currency: e.target.value }), key: "mi7" }, [
1723
+ react.createElement("select", { className: "dshadb_field dshadb_field_select", value: mform.currency || "CNY", onChange: (e) => setMform({ ...mform, currency: e.target.value }), key: "mi7" }, [
1259
1724
  react.createElement("option", { value: "CNY", key: "cc1" }, t("cmn")),
1260
1725
  react.createElement("option", { value: "USD", key: "cc2" }, t("usd")),
1261
1726
  ]),
@@ -1342,7 +1807,7 @@ window.__ModuleLoader__.load({
1342
1807
  ]);
1343
1808
 
1344
1809
  const output = react.createElement("div", { className: "dshadb_drawer", style: { maxHeight: "86vh" }, onClick: (e) => e.stopPropagation(), key: "drawer" }, [
1345
- react.createElement("div", { className: "dshadb_handle", key: "handle" }, react.createElement("div", { className: "dshadb_handle_bar" })),
1810
+ react.createElement(SwipeHandle, { onClose: onBack || onClose, key: "handle" }),
1346
1811
  react.createElement("div", { className: "dshadb_header", key: "header" }, [
1347
1812
  react.createElement("div", { style: { display: "flex", alignItems: "center", gap: "10px", flex: "1", minWidth: 0 }, key: "leftwrap" }, [
1348
1813
  onBack ? react.createElement("button", { type: "button", className: "dshadb_header_back", onClick: onBack, key: "back", title: t("back") }, react.createElement(IconBack, null)) : null,
@@ -1371,12 +1836,26 @@ window.__ModuleLoader__.load({
1371
1836
  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" } },
1372
1837
  upd.phase === "installing" ? t("update.installing") : t("update.install") + " v" + upd.info.remote) : null,
1373
1838
  ]),
1374
- upd.msg || upd.info?.hasUpdate ? react.createElement("div", { key: "upd_msg", style: { color: upd.phase === "fail" ? "#e05252" : "#777b84", fontSize: "11px", marginTop: "6px" } },
1375
- upd.msg || (upd.info?.hasUpdate ? t("update.available") + " v" + upd.info.remote : null)) : null,
1839
+ // v1.3.3: 更新结果改为醒目横幅 —— 原来的 11px 灰色字在移动端几乎看不到,
1840
+ // 用户点完「一键更新」后以为没反应。成功=绿色底, 失败=红色底, 检查有新版=原来的灰色。
1841
+ upd.msg ? react.createElement("div", { key: "upd_msg", style: {
1842
+ color: upd.phase === "fail" ? "#b33" : upd.phase === "done" ? "#1a7a3a" : "#777b84",
1843
+ background: upd.phase === "fail" ? "#fde8e8" : upd.phase === "done" ? "#e8fde8" : "transparent",
1844
+ fontSize: upd.phase === "done" || upd.phase === "fail" ? "13px" : "11px",
1845
+ fontWeight: upd.phase === "done" || upd.phase === "fail" ? "600" : "400",
1846
+ padding: upd.phase === "done" || upd.phase === "fail" ? "8px 10px" : "0",
1847
+ borderRadius: upd.phase === "done" || upd.phase === "fail" ? "6px" : "0",
1848
+ marginTop: "8px",
1849
+ lineHeight: "1.4"
1850
+ } }, upd.msg) : (upd.info?.hasUpdate ? react.createElement("div", { key: "upd_msg", style: { color: "#777b84", fontSize: "11px", marginTop: "6px" } },
1851
+ t("update.available") + " v" + upd.info.remote) : null),
1376
1852
  ]),
1377
1853
  ]),
1378
1854
  ]);
1379
- return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, output);
1855
+ return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } }, [
1856
+ react.createElement("div", { className: "dshadb_swipeguard", "aria-hidden": "true", key: "swipeguard" }),
1857
+ output,
1858
+ ]);
1380
1859
  }
1381
1860
  //#endregion
1382
1861
 
@@ -1399,7 +1878,6 @@ window.__ModuleLoader__.load({
1399
1878
  var whaleCtxInfo = { isPeak: false };
1400
1879
  function updateWhaleContext(info) {
1401
1880
  whaleCtxInfo = { isPeak: !!(info && info.isPeak) };
1402
- if (whaleWidget && whaleWidget.onContext) whaleWidget.onContext();
1403
1881
  }
1404
1882
  function whalePeakWords(peakMode, isPeak) {
1405
1883
  if (peakMode === "liangwen") return isPeak ? "梁文峰" : "梁文谷";
@@ -1441,6 +1919,9 @@ window.__ModuleLoader__.load({
1441
1919
  }
1442
1920
 
1443
1921
  var whaleWidget = null;
1922
+ var whaleRefs = 0;
1923
+ // 最近一次已知的大肥鱼设置缓存 — 会话切换重建时直接用它初始化, 避免先从默认(右侧)闪一下再跳回用户位置
1924
+ var lastWhaleSettings = null;
1444
1925
  // 停止互动后多久缩回 / 气泡存活时长 (用户要求: 3 秒)
1445
1926
  var WHALE_IDLE_MS = 3000;
1446
1927
  // 左右「边缘带」占屏宽比例: 松手时中心落在带内才吸附缩回, 中间区域原地不动
@@ -1453,7 +1934,16 @@ window.__ModuleLoader__.load({
1453
1934
  if (whaleWidget) return;
1454
1935
  if (typeof document === "undefined") return;
1455
1936
  var st = {};
1937
+ var disposed = false;
1456
1938
  for (var k in WHALE_DEFAULTS) st[k] = WHALE_DEFAULTS[k];
1939
+ // 用最近一次已知设置初始化, 避免重建时先从默认(右侧)闪一下再跳回用户位置
1940
+ if (lastWhaleSettings) {
1941
+ for (var k2 in WHALE_DEFAULTS) {
1942
+ if (lastWhaleSettings[k2] !== undefined && lastWhaleSettings[k2] !== null) st[k2] = lastWhaleSettings[k2];
1943
+ }
1944
+ if (lastWhaleSettings.left !== undefined) st.left = lastWhaleSettings.left;
1945
+ if (lastWhaleSettings.top !== undefined) st.top = lastWhaleSettings.top;
1946
+ }
1457
1947
 
1458
1948
  // ---------- DOM ----------
1459
1949
  var root = document.createElement("div");
@@ -1467,6 +1957,15 @@ window.__ModuleLoader__.load({
1467
1957
  img.src = "/api-dashboard/whale/image.png";
1468
1958
  img.alt = "大肥鱼"; img.draggable = false;
1469
1959
  body.appendChild(img);
1960
+ // 透明抓取层: 让手机壳的侧滑手势层对挂件让路(它自己是「真·横向滚动容器」,
1961
+ // 由 0 高度的守卫子元素提供 2px 不可见横向溢出)。放在 body 内部 →
1962
+ // pointerdown 照常冒泡到下面绑在 body 上的拖拽处理器, 拖拽代码一行都不用改。
1963
+ var grab = document.createElement("div");
1964
+ grab.className = "dshadb-whale-grab";
1965
+ var grabGuard = document.createElement("div");
1966
+ grabGuard.className = "dshadb-whale-grab-guard";
1967
+ grab.appendChild(grabGuard);
1968
+ body.appendChild(grab);
1470
1969
  sprite.appendChild(body);
1471
1970
  var bubble = document.createElement("div");
1472
1971
  bubble.className = "dshadb-whale-bubble";
@@ -1482,8 +1981,12 @@ window.__ModuleLoader__.load({
1482
1981
  bubble.appendChild(bText);
1483
1982
  root.appendChild(bubble);
1484
1983
  root.appendChild(sprite);
1984
+ // 初始化时禁用 CSS transition, 避免 left:0 → 实际位置 的 300ms 滑动动画
1985
+ // (CSS 里写死了 left:0, JS 第一次设 left 时会触发 transition)
1986
+ root.style.transition = "none";
1987
+ // v1.4.1: 拿到保存的设置之前**先不显示** —— 见下面 reveal() 的注释。
1988
+ root.style.visibility = "hidden";
1485
1989
  document.body.appendChild(root);
1486
-
1487
1990
  // ---------- 音效 ----------
1488
1991
  var audio = { press: null, release: null };
1489
1992
  function reloadAudio() {
@@ -1543,14 +2046,15 @@ window.__ModuleLoader__.load({
1543
2046
  if (saveTimer) clearTimeout(saveTimer);
1544
2047
  saveTimer = setTimeout(function () {
1545
2048
  saveTimer = null;
2049
+ lastWhaleSettings = {
2050
+ scale: st.scale, soundOn: st.soundOn, soundSet: st.soundSet, volume: st.volume,
2051
+ bubbleOn: st.bubbleOn, peakMode: st.peakMode, snapOn: st.snapOn,
2052
+ peekRatio: st.peekRatio, left: st.left, top: st.top, side: st.side,
2053
+ };
1546
2054
  try {
1547
2055
  fetch("/api-dashboard/whale/settings", {
1548
2056
  method: "PUT", headers: { "Content-Type": "application/json" },
1549
- body: JSON.stringify({
1550
- scale: st.scale, soundOn: st.soundOn, soundSet: st.soundSet, volume: st.volume,
1551
- bubbleOn: st.bubbleOn, peakMode: st.peakMode, snapOn: st.snapOn,
1552
- peekRatio: st.peekRatio, left: st.left, top: st.top, side: st.side,
1553
- }),
2057
+ body: JSON.stringify(lastWhaleSettings),
1554
2058
  }).catch(function () {});
1555
2059
  } catch (e) {}
1556
2060
  }, 400);
@@ -1610,7 +2114,7 @@ window.__ModuleLoader__.load({
1610
2114
  var tail = bw / 2 - shift;
1611
2115
  tail = Math.min(Math.max(tail, 18), bw - 18);
1612
2116
  bubble.style.setProperty("--dshw-btx", Math.round(tail) + "px");
1613
- if (r.top - bh - 6 < margin) bubble.classList.add("dshadb-whale-bubble-below");
2117
+ if (r.top - bh - 6 < margin && (vh() - r.top - r.size - bh - 6 >= margin)) bubble.classList.add("dshadb-whale-bubble-below");
1614
2118
  }
1615
2119
  function openBubble() {
1616
2120
  if (!st.bubbleOn) return;
@@ -1647,14 +2151,11 @@ window.__ModuleLoader__.load({
1647
2151
  full = true; applyPeek();
1648
2152
  lastLineAt = Date.now();
1649
2153
  openBubble();
1650
- playSound("press");
1651
2154
  armHide();
1652
2155
  }
1653
2156
  function toPeek() {
1654
2157
  full = false; applyPeek();
1655
2158
  closeBubble();
1656
- // 只在真的会缩回边缘时才播收回音 (停在屏幕中间时不缩, 也就不该有动静)
1657
- if (st.side) playSound("release");
1658
2159
  if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
1659
2160
  }
1660
2161
  /** 停手 WHALE_IDLE_MS 后自动缩回; 每次互动都重新计时 */
@@ -1677,7 +2178,7 @@ window.__ModuleLoader__.load({
1677
2178
  return;
1678
2179
  }
1679
2180
  var cx = st.left + s / 2;
1680
- var zone = Math.max(s * 0.75, W * WHALE_EDGE_ZONE); // 边缘带宽度
2181
+ var zone = Math.min(Math.max(s * 0.75, W * WHALE_EDGE_ZONE), Math.max(1, W / 2 - 1)); // 边缘带宽度, 不超过半屏
1681
2182
  if (cx <= zone) { st.side = "left"; st.left = 0; }
1682
2183
  else if (cx >= W - zone) { st.side = "right"; st.left = W - s; }
1683
2184
  else { st.side = ""; } // 屏幕中间: 不缩
@@ -1688,6 +2189,8 @@ window.__ModuleLoader__.load({
1688
2189
 
1689
2190
  // ---------- 拖拽 ----------
1690
2191
  function onDown(e) {
2192
+ // v1.4.0: 面板开着时挂件已被 setLocked 关掉 pointer-events, 这里再兜一道
2193
+ if (root.classList.contains("dshadb-whale-locked")) return;
1691
2194
  if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
1692
2195
  drag = {
1693
2196
  id: e.pointerId, sx: e.clientX, sy: e.clientY,
@@ -1787,9 +2290,14 @@ window.__ModuleLoader__.load({
1787
2290
  if (full && !drag) toPeek();
1788
2291
  };
1789
2292
  document.addEventListener("pointerdown", onDocDown);
2293
+ var lastVw = vw();
1790
2294
  var onResize = function () {
2295
+ var nowVw = vw();
1791
2296
  applyPos();
1792
- if (st.snapOn && st.side) settle(); else applyPeek();
2297
+ // 键盘弹出/收起只改视口高度, 不应触发水平吸附(否则鲸鱼会被吸到边上去);
2298
+ // 只有视口宽度真正变化(横竖屏/分屏)才重新 settle
2299
+ if (st.snapOn && st.side && Math.abs(nowVw - lastVw) > 4) settle(); else applyPeek();
2300
+ lastVw = nowVw;
1793
2301
  if (bubbleOpen) placeBubble();
1794
2302
  };
1795
2303
  window.addEventListener("resize", onResize);
@@ -1799,16 +2307,24 @@ window.__ModuleLoader__.load({
1799
2307
  };
1800
2308
  document.addEventListener("visibilitychange", onVis);
1801
2309
 
2310
+ // v1.4.0: 挂件是在面板可能已经开着的时候才挂上来的(用户在设置里刚打开大肥鱼) → 立刻套用锁定态
2311
+ if (whaleLocked) root.classList.add("dshadb-whale-locked");
1802
2312
  whaleWidget = {
1803
- root: root,
1804
2313
  applyPatch: applyPatch,
1805
- getState: function () {
1806
- var out = {};
1807
- for (var k in st) out[k] = st[k];
1808
- return out;
2314
+ /**
2315
+ * v1.4.0: 我们自己的抽屉/面板开着时, 挂件**不能**再接收触摸。
2316
+ * 挂件是 document.body 的直接子元素 (z-index 9600), 而设置面板嵌在 DSH 的 composer dock 里 ——
2317
+ * 只要面板的某个祖先带 transform/filter/contain, 面板就会被关进一个更低的层叠上下文,
2318
+ * 于是挂件反而盖在面板上面, 拖「身体大小 / 露出比例」滑块时变成**拖挂件**(用户反馈
2319
+ * 「滑动的时候容易把侧边栏拉过来」)。这里只关交互、**不隐藏** ——
2320
+ * 调大小/露出比例时还要看挂件的实时预览。
2321
+ */
2322
+ setLocked: function (locked) {
2323
+ if (locked) root.classList.add("dshadb-whale-locked");
2324
+ else root.classList.remove("dshadb-whale-locked");
1809
2325
  },
1810
- onContext: function () { /* 峰谷变化下次开泡时自动取用 */ },
1811
2326
  dispose: function () {
2327
+ disposed = true;
1812
2328
  body.removeEventListener("pointerdown", onDown);
1813
2329
  body.removeEventListener("pointermove", onMove);
1814
2330
  body.removeEventListener("pointerup", onUp);
@@ -1819,31 +2335,72 @@ window.__ModuleLoader__.load({
1819
2335
  if (hideTimer) clearTimeout(hideTimer);
1820
2336
  if (bubbleTimer) clearTimeout(bubbleTimer);
1821
2337
  if (saveTimer) clearTimeout(saveTimer);
2338
+ if (revealTimer) clearTimeout(revealTimer); // v1.4.1: 卸载后别再回调
1822
2339
  if (root.parentNode) root.parentNode.removeChild(root);
1823
2340
  whaleWidget = null;
1824
2341
  },
1825
2342
  };
1826
2343
 
1827
2344
  // ---------- 拉取已存设置 ----------
2345
+ /**
2346
+ * v1.4.1: 拿到保存的设置之前**不显示**挂件。
2347
+ * 旧行为: 先用默认值(scale=1 → 108px、贴右边、top=62% 屏高)画出来, 等 /whale/settings
2348
+ * 回来再改成保存值 —— 真机实测那一下是「往上跳 307px + 从 108px 缩到 65px」,
2349
+ * LayoutShift 记 0.0119(我们这边最大的一笔), 而且冷启动时它发生在首屏之后十几秒
2350
+ * (配置要等一次全量轮询), 用户正好在看设置面板时就以为「打开设置界面才抖」。
2351
+ * 现在: 首帧隐藏 → 设置到位(或失败/2s 兜底)才露出, 鱼直接出现在正确的位置和大小。
2352
+ * ⚠️ 露出必须覆盖**所有**分支(成功/失败/早退), 否则请求异常会让鱼永远不出现。
2353
+ */
2354
+ var revealed = false;
2355
+ var revealTimer = null;
2356
+ function reveal() {
2357
+ if (revealed) return;
2358
+ revealed = true;
2359
+ if (revealTimer) { clearTimeout(revealTimer); revealTimer = null; }
2360
+ root.style.visibility = "";
2361
+ root.style.transition = ""; // 初始化完成, 恢复过渡动画(拖拽/吸附时平滑)
2362
+ }
2363
+ revealTimer = setTimeout(reveal, 2000); // 兜底: 请求挂住也不能让鱼永远不出现
1828
2364
  applyAll();
1829
2365
  fetch("/api-dashboard/whale/settings", { cache: "no-store" })
1830
2366
  .then(function (r) { return r.json(); })
1831
2367
  .then(function (d) {
2368
+ if (disposed) return;
1832
2369
  if (!d || !d.ok || !d.settings) return;
1833
- for (var key in WHALE_DEFAULTS) {
1834
- if (d.settings[key] !== undefined && d.settings[key] !== null) st[key] = d.settings[key];
2370
+ // 本会话内已拖过/存过位置(缓存非空)时, 不再用服务端旧值覆盖当前落点,
2371
+ // 否则重建(新对话)会把用户刚放好的中间/左边又弹回服务端的旧右侧
2372
+ if (!lastWhaleSettings) {
2373
+ for (var key in WHALE_DEFAULTS) {
2374
+ if (d.settings[key] !== undefined && d.settings[key] !== null) st[key] = d.settings[key];
2375
+ }
2376
+ if (d.settings.left !== undefined) st.left = d.settings.left;
2377
+ if (d.settings.top !== undefined) st.top = d.settings.top;
1835
2378
  }
1836
- if (d.settings.left !== undefined) st.left = d.settings.left;
1837
- if (d.settings.top !== undefined) st.top = d.settings.top;
2379
+ lastWhaleSettings = {
2380
+ scale: st.scale, soundOn: st.soundOn, soundSet: st.soundSet, volume: st.volume,
2381
+ bubbleOn: st.bubbleOn, peakMode: st.peakMode, snapOn: st.snapOn,
2382
+ peekRatio: st.peekRatio, left: st.left, top: st.top, side: st.side,
2383
+ };
1838
2384
  applyAll();
1839
2385
  })
1840
- .catch(function () {});
2386
+ .catch(function () { /* 网络异常静默, 由 2s 兜底露出 */ })
2387
+ .then(reveal); // 成功 / 失败 / 上面提前 return 都会走到这里
1841
2388
  }
1842
2389
  function removeWhaleWidget() { if (whaleWidget) whaleWidget.dispose(); }
2390
+ /** 引用计数: 多个 dock 实例并存时, 只有最后一个释放才真正卸载挂件 */
2391
+ function acquireWhaleWidget() { ensureWhaleWidget(); whaleRefs++; }
2392
+ function releaseWhaleWidget() { whaleRefs = Math.max(0, whaleRefs - 1); if (whaleRefs === 0) removeWhaleWidget(); }
1843
2393
  /** 设置面板改动后推给活着的挂件 (未挂载时静默忽略, 值已由端点落盘) */
1844
2394
  function patchWhaleWidget(patch) {
1845
2395
  if (whaleWidget && whaleWidget.applyPatch) whaleWidget.applyPatch(patch);
1846
2396
  }
2397
+ /** v1.4.0: 我们自己的抽屉/面板是否开着 —— 开着就锁住挂件的触摸 (见 setLocked 注释)。
2398
+ * 记在模块变量里: 面板开着时挂件才被打开(比如刚在设置里开启大肥鱼)也能立刻套用。 */
2399
+ var whaleLocked = false;
2400
+ function setWhaleLocked(locked) {
2401
+ whaleLocked = !!locked;
2402
+ if (whaleWidget && whaleWidget.setLocked) whaleWidget.setLocked(whaleLocked);
2403
+ }
1847
2404
  //#endregion
1848
2405
 
1849
2406
  //#region plugin
@@ -1887,10 +2444,18 @@ window.__ModuleLoader__.load({
1887
2444
  // v1.1.0: 大肥鱼互动挂件 — 设置里开启「收养大肥鱼」后挂载/卸载 (纯互动, 不含余额)
1888
2445
  const whaleOn = !!(config && config.whaleEnabled);
1889
2446
  react.useEffect(() => {
1890
- if (!whaleOn) { removeWhaleWidget(); return; }
1891
- ensureWhaleWidget();
1892
- return removeWhaleWidget;
2447
+ if (!whaleOn) { releaseWhaleWidget(); return; }
2448
+ acquireWhaleWidget();
2449
+ return releaseWhaleWidget;
1893
2450
  }, [whaleOn]);
2451
+ // v1.4.0: 看板/详情/设置任一开着 → 锁住大肥鱼挂件的触摸。
2452
+ // 不这么做的话, 挂件(在 body 上, z-index 9600)会盖在设置面板上面,
2453
+ // 拖「身体大小 / 露出比例」滑块时实际拖的是挂件。
2454
+ const overlayOpen = isSettingsOpen || view !== "bar";
2455
+ react.useEffect(() => {
2456
+ setWhaleLocked(overlayOpen);
2457
+ return () => setWhaleLocked(false);
2458
+ }, [overlayOpen]);
1894
2459
  // 峰谷时段变化同步给挂件 (台词里的「当前时间段」用它, 不涉及任何金额)
1895
2460
  react.useEffect(() => {
1896
2461
  updateWhaleContext({ isPeak: !!(config && config.isPeak) });
@@ -1916,7 +2481,11 @@ window.__ModuleLoader__.load({
1916
2481
  // v0.5.15: 即时信号 — 订阅模型目录 store, 选中模型瞬间就切平台 (不再等投影/请求)
1917
2482
  const modelDirSub = modelStore && typeof modelStore.subscribe === "function" ? modelStore.subscribe : () => () => {};
1918
2483
  const modelDirSnap = modelStore && typeof modelStore.getSnapshot === "function" ? modelStore.getSnapshot : () => void 0;
1919
- const modelDirModel = react.useSyncExternalStore(modelDirSub, modelDirSnap, modelDirSnap)?.current?.model;
2484
+ const modelDirState = react.useSyncExternalStore(modelDirSub, modelDirSnap, modelDirSnap);
2485
+ const modelDirModel = modelDirState?.current?.model;
2486
+ const modelDirProvider = modelDirState?.current?.provider;
2487
+ // 当前 provider: 模型目录优先(切换瞬间就更新), 投影兜底
2488
+ const curProvider = modelDirProvider || (cost && cost.currentProvider) || "";
1920
2489
  const [didModelDir, setDidModelDir] = react.useState(false);
1921
2490
  react.useEffect(() => {
1922
2491
  const m = modelDirModel;
@@ -1954,32 +2523,37 @@ window.__ModuleLoader__.load({
1954
2523
  // 本会话消耗节点 (无条件渲染, waiting=true 显示 ~— 等待数据, 否则精确到6位)
1955
2524
  let costText = "~—"
1956
2525
  let costTitle = t("detail.sessionCost") + " — 等待数据"
1957
- if (cost !== undefined && typeof cost.cost === 'number' && cost.waiting !== true) {
1958
- const amount = cost.cost
1959
- const cur = cost.currency || "CNY"
1960
- const sym = cur === "CNY" ? "¥" : cur === "USD" ? "$" : cur + " "
1961
- const fixed = amount >= 1 ? 2 : amount >= 0.01 ? 3 : amount >= 0.001 ? 4 : amount >= 0.0001 ? 5 : 6
1962
- costText = "~" + sym + amount.toFixed(fixed)
1963
- costTitle = "本会话消耗: " + sym + amount.toFixed(6) + (cost.models && cost.models.length ? " | 模型: " + cost.models.join(", ") : "")
2526
+ const costFmt = formatSessionCost(cost)
2527
+ if (costFmt !== null) {
2528
+ costText = "~" + costFmt.text
2529
+ costTitle = "本会话消耗: " + costFmt.title + (cost.models && cost.models.length ? " | 模型: " + cost.models.join(", ") : "")
1964
2530
  } else if (cost !== undefined && cost.waiting === true) {
1965
2531
  costText = "~—"
1966
2532
  costTitle = "本会话消耗 — 等待数据, 发消息后自动更新"
1967
2533
  }
1968
2534
  const costDisp = { text: costText, title: costTitle };
1969
2535
 
1970
- return react.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "2px", maxWidth: "100%" } }, [
1971
- // v0.5.7: 本会话消耗并入同一个胶囊, 不再裸露在外
1972
- react.createElement(BarReadout, { t, onOpen: openList, onOpenSettings: () => openSettings("basic"), selectedId, onSelect: handleSelect, config, cost: costDisp, key: "bar" }),
1973
- 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" }),
1974
- react.createElement(PlatformDetail, { isOpen: view === "detail", onClose: closeDetail, platformId: detailId, t, useProjection, config, key: "detail" }),
1975
- react.createElement(SettingsModal, { isOpen: isSettingsOpen, onClose: () => setSettingsOpen(false), onBack: () => { setSettingsOpen(false); setView("list"); }, t, config, initialSection: settingsSection, key: "settings" }),
2536
+ // v1.4.0: 子代理消耗行 —— 有子代理时在状态条**下面换行**追加一行,
2537
+ // 按父会话里的创建顺序从左到右排列 (顺序由服务端 subagentCatalog 决定, 这里不排序)。
2538
+ // 金额是「该子代理及其后代」的汇总, 与主板数字同一套计价口径。
2539
+ const subRow = buildSubagentRow(cost);
2540
+
2541
+ return react.createElement("span", { className: "dshadb_barwrap" }, [
2542
+ react.createElement("span", { className: "dshadb_barrow", key: "row" }, [
2543
+ // v0.5.7: 本会话消耗并入同一个胶囊, 不再裸露在外
2544
+ react.createElement(BarReadout, { t, onOpen: openList, onOpenSettings: () => openSettings("basic"), selectedId, onSelect: handleSelect, config, cost: costDisp, provider: curProvider, key: "bar" }),
2545
+ view === "list" ? react.createElement(DashboardDrawer, { isOpen: true, 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" }) : null,
2546
+ view === "detail" ? react.createElement(PlatformDetail, { isOpen: true, onClose: closeDetail, platformId: detailId, t, useProjection, config, key: "detail" }) : null,
2547
+ isSettingsOpen ? react.createElement(SettingsModal, { isOpen: true, onClose: () => setSettingsOpen(false), onBack: () => { setSettingsOpen(false); setView("list"); }, t, config, initialSection: settingsSection, key: "settings" }) : null,
2548
+ ]),
2549
+ subRow,
1976
2550
  ]);
1977
2551
  });
1978
2552
  return () => { dispose(); };
1979
2553
  });
1980
2554
 
1981
2555
  ctx.effect(() => {
1982
- const onVisibility = () => { if (!document.hidden) refresh(true).then(schedule, schedule); };
2556
+ const onVisibility = () => { if (!document.hidden) refresh(false, true).then(schedule, schedule); };
1983
2557
  document.addEventListener("visibilitychange", onVisibility);
1984
2558
  return () => document.removeEventListener("visibilitychange", onVisibility);
1985
2559
  }, "dsh-api-dashboard: visibility resume");