dsh-api-dashboard 1.0.3 → 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.
- package/README.md +146 -3
- package/assets/whale/D1.mp3 +0 -0
- package/assets/whale/D2.mp3 +0 -0
- package/assets/whale/DSniang1.png +0 -0
- package/assets/whale/LICENSE-whale-widget.txt +21 -0
- package/assets/whale/Ya1.mp3 +0 -0
- package/assets/whale/Ya2.mp3 +0 -0
- package/assets/whale/rua.gif +0 -0
- package/client/client.js +1367 -117
- package/package.json +1 -1
- package/src/index.js +1425 -179
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,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAABaWlDQ1BEaXNwbGF5IFAzAAB4nHWQvUvDUBTFT6tS0DqIDh0cMolD1NIKdnFoKxRFMFQFq1OafgltfCQpUnETVyn4H1jBWXCwiFRwcXAQRAcR3Zw6KbhoeN6XVNoi3sfl/Ticc7lcwBtQGSv2AijplpFMxKS11Lrke4OHnlOqZrKooiwK/v276/PR9d5PiFlNu3YQ2U9cl84ul3aeAlN//V3Vn8maGv3f1EGNGRbgkYmVbYsJ3iUeMWgp4qrgvMvHgtMunzuelWSc+JZY0gpqhrhJLKc79HwHl4plrbWD2N6f1VeXxRzqUcxhEyYYilBRgQQF4X/8044/ji1yV2BQLo8CLMpESRETssTz0KFhEjJxCEHqkLhz634PrfvJbW3vFZhtcM4v2tpCAzidoZPV29p4BBgaAG7qTDVUR+qh9uZywPsJMJgChu8os2HmwiF3e38M6Hvh/GMM8B0CdpXzryPO7RqFn4Er/QcXKWq8MSlPPgAAAA50RVh0U29mdHdhcmUARmlnbWGesZZjAAAXm0lEQVR4AbVbCXAU55X+untOSegExCHEyIjL4hA24NgOtnxs4qwd23GcxAmbGGdzVLLJmo2rdmtTW2W8m92tZLNlO4nXuznKJpXDydpZO4dzlGOETTAgwBBzmgQGSYBuRtLMaM7u/d7/d0vCgGhxPGimp6en53/vf//33vvej4ErLG+ecmL5IloMYLltIGY4iPFyJY/YO26N80g4Bl9t7LUc7LGC2LNiphHHFRQDV0DaOpwWPvkeB7gXZys6WdkjBw23cdUcoxWXWS6bAd485lQWQnjYcbAOl670+STO47GghdbL5RmXbIBxiq+Hdu1zC91heKAL7W/vR6LnFIYGelAo5BGKlKJm5mzMaliIGbEFMK0AfEjcMPBswMTGSzXEJRlgx0nnYa7XDZhA8dxICod2bEbbKy/iyN42FAsFGsMWe2hxHD0M08LsxkW49f51WLbmDhiGCR8S5/HY6jrjWVykXJQB3iCwWUU8w9OW891jc3YPbP89Xn72W+g9eRy24/2YwT+O0ttRr+pEnasTfnrjnR/CXX/9CLKZEWRSSVTWTEO4tHyiIcW5LG65GG+YtAH8zHpqcAD/9/RX8MetmziTBkwetihaLEKpSmvID8s1R73aPPRQxCBF28aC5SthmSa6O9oxb0kz7vnUelTOiGECSdj0hnfVGU9gEjIpA7R1Oo9zwOsnuqfr2H48/41/QX/3SQQCAc5gCoV8BuLQpmkog8CdeTGEeIYtr1S6wKMoFhEc4BIoLS1DMZdDIBjErNg83HzvWiy+7lZYwdB5f5+P37BqtvEYfIpvA+w44TwDjfDnlVNH9+PX3/06chz04OkBDPWfouIOPcCk8iYsUd41QFEpb2sjUHExQsEu0kls5Ao2zFAYJt8HLUvdL4ZyeF4/fwnu/cwjmLv4momG8ixx4SFcLgPs6HTe5EvzRPec7opj529+ir5T7Th6YB/SQwkEqKxlmZxMvQz07Ct9lPsrxR2tvM3lIR5QKBRpgCLyvKc0FESIXiRfU95B4xR4vaSiEvd8cj1Wv/f+icByD42wAheQC0KtmvkLKJ9NDeLo7lZk08M4vPdNrbyplbc4awFTe4AYQXkCr4WCAYSoYCAQVO9NRoExI/GVCls8teQ5PAJ8lhwWrZdKDOAnT/0bdvzuZxMNq5kT9wwuxQBtJ5xHL+T2drGAzn1bkB7sxx/feB3FTIoD1utdKQQN/6K4rOUwXTscCSMYCimMUO6t1n9ReYO8560KAGF4wAi1VNRnYhS+FkYyeP7pr+LoW9smGt46GuFxXIwBBO35wxtwAelv349MMoHdWzYjPTyoRmuoP9ALTC0Di4qHEAmL4kHl/iOZDIaTKaSpSCaTRT5fQKFoq7UumBFmXAsq91dBU2OA7f2qPLhIgB3GL575BvLZ9ERDXL+t01k/KQNInHdD3YSSSyWQ7I5zze/HQG+XjukiXnIDPZNhonaIMy/XUukRDA4NIZlMIi1xnoCZpxcJBjg0gHxXZjkUsFBTVY6p1ZUoi0aUQUzLUGtem9dUnnP8yEHsfe3lCcdJJR+Vouw8n50tTHI2YaK01pWBjoPIpFM4sn8v7HwOhjOa3yljiNsHqXyAhwDYMEPi0PAwRjjrWc64XIMcBL2AU0Qp1051NIyZFWWoKokyi8zQM3II03NKolEaIUAcMCRC0rCWer4Y4eDOLUy8chMNtTKvE7ez5KzEm2tmHXwUM/mRIYwM9iD+p7eRGhLXF1e1PedXritrXNZ9gbObSqfp8knkmQrLoFndIchvTC+LYvlVc9G8tAmxBQtQPXOmQvkRLosf/fg5bNqyDUUXQwQoAwE3e4RED22I1DDHMpxAadX0iYbcIkvhnYnSGQZQrl/Eo/AhQycOI8M8v+PPR1DM59W69sKcp7wAXZELN5VMI5nSyqsskIBXW1qKlqULce2qazFn4QJU1s5CsHQKDIn7fEyEz/n8334By5ctxcYfPofugQQcF1Pk+RIZVPikh4hBs8O9FzKAXgrHnGdXNBiJcxogYONBx8fs28zshgdOYaC7l1Vdv0JvN79Rg5cBBumu8j5FxYdTaQVyBu+L8pZlc+tw27uuRWPzckyvq0ekogpmOAIVPuRBCvEclQ3eePPNWEQDfec738Mfdr+lvMFxSymLyyHPmqPr+DEa4DR8SGUuoDLZDeOMokVm37lAyPNkZOCEquo6jh5hwZLWyO/NvhiSs2+aAYJchjM/gmwuD5NrfBbX9V3NV+O+992GJTe8myVwI6KVNTAjVD7gKu/BiFskyVEzdRoe+dJ6fPjO98JyxjJH+UzC7eBALw7u2eFn6PITD0sJf5YBCHwt8EVkOMrdhrnu+7u7RpF77AcY/4ngObpmmus+y+VhceZrI1Hc0BhD84plmDV/ISqnTkWwpBQGMUKhmuMqr4O+jvsS9+Tc0Ub92AMfwsfvvYvGlNtsvexMUxl9++ZX1XLwIZ4XnGkAiq+1b+eSjL8JnKTbpZPDBPHiuEJWZ3qiTHqEIS6bg0NPmUFkf1djPRYtno+ZjfNRMbUGoajMegAq5rmG1co7Y8rb7qHOtZHvfv+d+NT99xBHxgoqiwbvOdGOvq52PyqIFzx4hgEUh+eTxsonB2iADHo6O1jl5dzBuhaAo+K0rEtRXtZ9TTiIq2fWYl5DHabPrUcpET4oOYHpwo/jjCnoOMA7lHds/ZnDJaQAlKN/z3tux323rVGYogoqwSA+Kn5wD3xKzNVZG8AxxyxyISmkE+jv62a11884rtNXhc7uMpBZyWazCvENGmLGlDLMqKlE9bRpKCmvYFgMuIDpaAWLnpKuwo6bG9hj1x2VKMm6L6rPxcvuu/suLL0qphIoySfkWufRw37VgEvY6ijAmNzs+PqWrdy/9+RJgl92bPY1twMvr5eZz9EDgjxvP53A8OE83uzuQ3XVPsyeOR2NDQ24uqkJtbPrYKgRMLs33BEoly/qWC8KS5VIHBHQVQSKAkEHQQLnug9/AAe+9iQyqo5wGJl64VvIWvPf9QGF/sWJq71RKYww6RlCor+PVi/o2dL6u5HAVPm8JD5JAmA58/6qslLMqZ2OisoKApaFI0eOYeuuvaj63e/xkXvvxnVrbhqlyjRNpL1ClC/SkMODp7F9Rxs6TpxSn0+rnop5TJxmzKhFLZOmlpUr8JvtuzgZRYwQkyYhMUmPA8E8mm1f/KMAIFPZxCCSLHfFPYW6Gu85sj6lOpQ1u2JWLZbU1WJxYwPqFsxH1QxmeGVTyOYEOdAk9u07wNo/J+Qhn2GNlsGOo13fKRQVxrz0819h2569qpaIhkM48Paf8ctXWxFhVHmQYPgXN92AV3fsVgTLuGrJl0jDJsDEotkvLWTnRpBg4pMlwntxWCfA3vrXpEWI7iy5fDXXf5gRwGLNb7rEhazVipoa3HTrLSpXUOtdnmJqA8jM09/5N6/W/Epmgjdc24yyinJFmcu1HIuoY/FOvL71DSxasBCrmxZj26EjxNUgJiVc+sJVLHd8AQBXQCFDDzit+HwVgjx219a5gEdwlhPoqsoiZG8cvLb3EFJ7D2PqtFo0LWjAquXLUErDGEReM2gr19WTr6kvx2WHFMDSOHUNc5VXOR7WOCEVRZqWVWE+PWuYxdVp5iTbDr6NaFk5JiMcdSwAx38XJ0fgSzP/L6iCRgOVPVr6auZXMKCHYfIX+4fRl86gdm4MI0yF+1/bzoougnriwRcf+CBabljNr0QUXRZQuYP7HFtjiACfTW6wWNDn3b296O4bUElQbM5MlElEoSFKyy1cVV+vuMPZc32rooQ/PZe11YXLXk9kTebp/o472zpv0chsuEyvkKBLuP7f6j2NOddez7UbQusvX1SzPqWqCsOkfTfFu5nH78TNNILFDqgTcDBKF8rzxAPoZQVyBX19fXji+z9F24GDcIIRVFVPQ4ie+NAH7sRN161kLkW+IVpCA9CV330HJimVsjBjfu8u5rOu+9sq3GmKyquCNNFZwpmYz1B344rlaFq6FN2MzZ/49GepaECFxyA9p2b6DOwfGMEIyRHliIYmOLQn6VAqESB+vB3/8M3vYicjR4IVZU/XSfYIlqBh5Q34j43P4clnfsB0m7UGH7FgxfWoqWvEJCXmE/9dA6iZKYzOOFyW23ArQLkUpgEiTE2bYrNRUVKCNENm29Y/KM4gSaq8kgApdPeNt9+BIRZKEhWkBDYUe2yp31GRgL9xNMNrgRA+uHYtjZdXHrd/+1Z84IEHsHTRAqz/5F+hhL+R4HOba0sQ7dgF+MQzTyZlAAEo1dOR1E+oKUeDoOE+RmwQFDTnxTklYdSHTMxiuTvAqrGMwNhApqeEhOiNa27G3Jpy1MVisBjODKkJBMEDYgBtUYkyJazvi4l+ZIaTennxujBG5eQSPnzH7SibMkX9vtBrixvmoCrZjqqu3ZNRCb5asZ6YXjgzMA4H9KtHhylTEAiNXBZLAynMuu9OPM/Uv5vcAVEL71/7IJbPrkFF2OLsV40mU1LVedEoFIogUBVE8xTygswfXvnfHyNCD5ldVYnFS5fBSA5hzapmFS1k2S2cOwfTqqYomj2S7Uc61YNs6XTfBojDJw6Yqs43XbrbXbEKvBzN1LjXJBGSe4I02MIp5fjnz34KgZIyHiWweBgQ/jCiDUe3d7zyF7YCPyl9DX53RqCAL6z9CP7r6f/BtEQC02fV4X3vfR/q7CSjQVTxijIBFWUlKjUOEFAtelo02eXXAPGAIdtS4E8CwbCq9dXAPfLCm0EXB0ylhs172fwQdGb2F2SPLyiurnp6xlgFKId4lDRNpSgSvp+FlDRLJEaZTCqb5zXg6Q3/hDzBTrBCZZ85kqop3S0SL4hIk0WRr0HVZofjGwgSwibE/d5tCc/HwXnsj5cIiRt4f1SPT3p7HIjF9S5Ul8V4LcSHPji7jAiGywMoXBFej0rZI2kawCU1Rt0JCiAtpsEGjW/ymYIZuujKKTAW+i0gn9FANgubEZ/uz58+HuAkHPfbIrUYh4OkqDULM0aAkp3Q56bk8iRDSIFJf0+amST4tdLCEQYsl/R0Q54b80eI4sN9vdr9wyVkhctp7LDiCNV37ZB+tscZqEKR/UP+rIRVwwpJjq0StP5CCLmyGb704Qj28OnwzSLIEpB6XoDQcFvdboaq3/O0LEqai8mJUODZbEbzd4auGUw3WTKcgvZ+14CRqmoEuIYLBE7V+BA3dhkgFRmKIU0YFXQDpeDoBmqBFjDzKWRpzyzvT5bNxEj9Sjqkzxl1aICihVar6O9+cd0AkVxwwFvDugVmKGZLBp+iW5aWlyNXzLNqHFSMryFdXmlyuhhhuf1/t4kIoyhNT3nLJSIeQxeXGkGTIJpwsfn9Io2Xl2KIOcFAOodjfUOIXbOGVq+BU8qIEirBZITD2hO4fqYRZzMkDh+RQAauOrnezOsC3p1JU+/oIF9wbGBQER9JdoIi7BcG1frlLFq817RVcZNjnZ8nsqe7uhjIWcfTW8SwoaoKhGPzmfYS0UNRzTnykLa5sEyifJbp9HMv/w7Tr7kV9XOW4SJF7UHUGYyDl/x8w+AMhQSEVBLkurDjfcbZ5YwW6IpvHu/EwZO9yLCZLwRKkhVbjiCnSRQNnCE2QcJ0/VLOeFBCKK9lWGkOHH4bPbva2HUaUspK9inFUU54Ri6RDL3ihVc2Y9Nrm9FwtT8e53wGkH+UAajIi36+ocFvXOXmRRtDM8LiIQKHKYLgpj8ewI59h4kFbKKQFkvxyArKUxH1FXqSRbwoRCSek0O08zCke0y6vGxRE41TA1UXCNjxc6HYJOf/9ZY2PPfCC2yxR1Ezqx4XK7LxUl5VJig7MH0tA1OHGcN9wjj9x/YASK/OyWOIpfOre95ivp/Ge65fTbOZ6jtqqbgbIiRMVly9FKVXNZJsTSuQM9zlosJcVnOLYrgUQ+u3f/QTbN7yhso5pByWPYYXKXFv1+loKsxJ3cgHPzrh18jgSOnpuJsXHJfLNwy960u6NBKPBeDynLnBTA5vkKlp7+nDB29Zg4X8XrGgSY+yCkOVsOIJ0hOUJolwATaNkKerC+maTg2rbG/XwT/jez96Dr0srCT1la03wg6pTRQXJ495J6MGCBXwRD6IhzEBP2DQALGma3GivYMFyLDa5CADMoUYodsHCVySLpdKkeLy+WKZbhIjP3j1ddw2cBrvXtakQE2ULKvgLLLtLZFFbZyi8rI3MM0QmhwaxuH2U/j5ps3YSU5QOD+HhnXIiNhSUUoyZVm4CJE9ha1nGUA6pts7nCcn9AIOYNai63H/wtUY6u/GQFcnOv90APFDe3GCr4M9pxApq8AXv/Jt1aYS4JM9g2mGQ4cN1eoMmyo97azwWKyQHMlMrUWIM684Qy6JIkNnZiSDP3V24WcEur0HDhD48gp7ZHOE6jrREMWc9B1yOnEKTc4I4unjN1SekTFI05BecAyTYInGnmyrzRKn2Z6aOa/pnLcUjmyHs/V5tYGKEE93z1IphjsWSo4iTIpITJ+H/czzZZ+h6vunJIJkVZKUJykbjpQpT6lgh+njX/5P8oMRTELO2lF6VsrU1umsp+M+jisgxX2vwNwvlHZYrXfZBCmbnfJqKdFAJEatlX+JyMpJU1t+5aF37is+C0VW6R0UrbgC4pzuVmVrkEAanlKBSGU1otVy1CDC7FEapkYujSsk8XNtqj4nIcL0+CGmx7I5cvJL4TwiipmJU8wKxQClXLsCfOIFUQQZ6oRxNugJmaEeXAFJiOuf64NzxhFJj41xoeJySLHzAELII8yGifAEoTBfI1MImpWI0BuijAjKC7IEzNQgLqfIJurz7SSfsGza0eE8wTsexqVKsg/G6z/ElKiFEjJEwXBUb5KUzpAwzEx5c5LtMQRmyf8NRKdjx6BBo1RjZn0DKqtr2fSY4vf/EJwhRP3HrptjbDjf5xesG7d3Os/ypgdxETI40IeBQzvQMHREcYDSHg+REpNtc9Y4YkU1PySsSVudR3owgUOsJdpY7b3w8m9RykSphp2l2bF5PBZg8fLVaFyycmzv8XmEuLrxujpj3UT3+Cqc/WyW9kTS1rfaNuO13/4Mu7e9jq8/8jdomlau9vmFuP618gFNg2uGwG2HFXQLXCo+RodkaTVb6yms+/znNIcgBZOkz8wF5Dn1DY346Ge+hCWrW86nhq/N0n77ohN6gmR28cN7sXPLK2j99Qvo6+1hzM4p/6usrMQXP/Np3PsXtyDMRCfAbrApJbRhuMyQZnhkE0SRn+RIbOaIDQXi8xNPPYWfsvDxqg5vCRiKRrBIi5fj45/7e9xy99ozx+Nj5idtAGWEDmfDOzPFU8eP4MXvfwu7tm1m+jqk6neMa50pppCp6+prrsHaj34Mq1euQpRMhCJTXEJV88GGTnX5Okie4PGnvolf/ea3ox2oUZJw3IiFlxDy5ctf+x7mL13laf/k6jnGer86TcoAIm6iJEao3N/Wiu9/61/RcTzuDtQdg2ZK3X6BNoKqBJkBTq+ZhttbWnANDVLJbK6KKbEsgX7ygidPnsTO3buw5Y2tGDid0M8QwlVV3ob73m3Fu14hnrDy+pvwpa/8d8IKR6/sf5nxRHaV9B8/uunfH3kw1iNb5Vx62xg3Q17T1Bs6xv9raCZpvMHGNlobow0XraYmS5x3PNcjVuRRpWWlrQ984rMPbfjHv4tjknJRBvBk0aJF6whYj7IMjimWyNDuLunF+J3jnjLjxXF5ZWU8x6PYbLf15sDbe+wNcZSCd71A32cnbNt47MSJ9knN+ni5qHrSE7au99RWVLxkm9YgFY9BZY6eIdz//eGdu9eVGF4/0RjtrurWmunyixj1JnfnzJliGAl+/lXLMj/a0XG8FZcgl+QB4yVGsYJWC08fZfEaM8eRFbqbDG/WXJA0PJTwNhrA83XPV7x1ridezXycXrKRhnoiHo8ncBnkshlgvDQ2NrbQAOt4upwz2Dz+p7zWtxv9Ro1hwIsIo11H935F1b1EkH2RSrfiMssVMcB4IU7EbDvfTLRuJqIvp0Ix6lZpyytst7WmlI1Ln5LuHadJjjPG7QlawdZDhw7FcQXl/wGKqPo5VF6eRwAAAABJRU5ErkJggg==" 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: "#
|
|
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",
|
|
71
|
+
stepfun: "mistral", novita: "together", relay: "relay",
|
|
60
72
|
};
|
|
61
|
-
/** 无官方图标、用文字名代替的平台 */
|
|
62
|
-
const TEXT_ONLY = new Set(["stepfun", "novita", "
|
|
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
|
-
.
|
|
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,32 +293,33 @@ 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}
|
|
239
299
|
.dshadb_switch_on .dshadb_switch_knob{transform:translateX(17px)}
|
|
240
300
|
|
|
241
301
|
/* Tab 导航 */
|
|
242
|
-
.dshadb_tabs{display:flex;gap:7px;padding:4px 0 12px;overflow:auto}
|
|
243
|
-
.
|
|
302
|
+
.dshadb_tabs{display:flex;gap:7px;padding:4px 0 12px;overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}
|
|
303
|
+
.dshadb_tabs::-webkit-scrollbar{display:none}
|
|
304
|
+
.dshadb_tab{flex:none;border:0;background:#e9eaf0;border-radius:12px;padding:8px 13px;font-size:11px;color:#686c76;white-space:nowrap;cursor:pointer;font-family:inherit;transition:all .12s ease}
|
|
244
305
|
.dshadb_tab_active{background:#4f7cff;color:#fff}
|
|
245
306
|
|
|
246
307
|
/* 输入框 */
|
|
247
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}
|
|
248
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}
|
|
249
319
|
.dshadb_label{font-size:11px;font-weight:700;color:#777b84;margin-bottom:4px;display:block}
|
|
250
320
|
.dshadb_grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
|
251
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}
|
|
252
322
|
.dshadb_save_btn:active{opacity:0.85}
|
|
253
|
-
.dshadb_danger{color:#e05252}
|
|
254
323
|
|
|
255
324
|
/* ===== 详情页 ===== */
|
|
256
325
|
.dshadb_detail{display:flex;flex-direction:column;gap:12px;padding:4px 2px}
|
|
@@ -266,9 +335,6 @@ window.__ModuleLoader__.load({
|
|
|
266
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}
|
|
267
336
|
.dshadb_detail_metric_val{font-size:16px;font-weight:700;color:#17181c;font-variant-numeric:tabular-nums}
|
|
268
337
|
.dshadb_detail_metric_label{font-size:10px;color:#777b84}
|
|
269
|
-
.dshadb_detail_row{display:flex;align-items:center;justify-content:space-between;padding:7px 0;border-bottom:1px solid #eef0f4;font-size:11px}
|
|
270
|
-
.dshadb_detail_row_label{color:#777b84}
|
|
271
|
-
.dshadb_detail_row_val{color:#17181c;font-weight:600;font-variant-numeric:tabular-nums;text-align:right}
|
|
272
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}
|
|
273
339
|
.dshadb_peakcard.peak{background:rgba(230,167,47,0.08);border-color:rgba(230,167,47,0.25)}
|
|
274
340
|
.dshadb_peakcard.valley{background:rgba(79,124,255,0.06);border-color:rgba(79,124,255,0.20)}
|
|
@@ -291,20 +357,146 @@ window.__ModuleLoader__.load({
|
|
|
291
357
|
|
|
292
358
|
/* ===== 补缺失CSS ===== */
|
|
293
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}
|
|
294
381
|
.dshadb_footer_dot{width:4px;height:4px;border-radius:50%;background:#c9cad0;opacity:0.5}
|
|
295
382
|
.dshadb_group_divider{height:1px;background:#eef0f4;margin:2px 6px 4px}
|
|
296
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}
|
|
297
384
|
.dshadb_icon_btn:active{background:#f0f2f8;opacity:0.85}
|
|
298
385
|
.dshadb_refresh_badge{font-size:9.5px;font-weight:600;color:#9ca0aa;background:#f0f2f8;padding:1px 7px;border-radius:999px;line-height:15px;white-space:nowrap}
|
|
299
|
-
.dshadb_title_ic{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;overflow:hidden;flex:none}
|
|
386
|
+
.dshadb_title_ic{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:7px;overflow:hidden;flex:none}
|
|
387
|
+
/* ===== 设置面板 · 开关行 (v1.1.0) ===== */
|
|
388
|
+
.dshadb_settings_row{display:flex;align-items:center;gap:12px;padding:12px 14px;margin:14px 0 4px;border-radius:16px;background:#ffffff;border:1px solid #e7e8ec;box-shadow:0 6px 20px rgba(0,0,0,0.03)}
|
|
389
|
+
.dshadb_settings_row_main{flex:1;min-width:0}
|
|
390
|
+
.dshadb_settings_row_title{display:block;font-size:13px;font-weight:750;color:#17181c;line-height:1.3}
|
|
391
|
+
.dshadb_settings_row_sub{display:block;font-size:10.5px;font-weight:500;color:#9ca0aa;line-height:1.45;margin-top:3px}
|
|
392
|
+
/* ===== 设置面板 · 大肥鱼页 (v1.1.0) ===== */
|
|
393
|
+
.dshadb_wf_row{display:flex;align-items:center;gap:10px;padding:11px 14px;margin-bottom:8px;border-radius:14px;background:#ffffff;border:1px solid #e7e8ec}
|
|
394
|
+
.dshadb_wf_row_col{display:block;padding:11px 14px;margin-bottom:8px;border-radius:14px;background:#ffffff;border:1px solid #e7e8ec}
|
|
395
|
+
.dshadb_wf_name{font-size:12.5px;font-weight:700;color:#17181c;flex:none}
|
|
396
|
+
.dshadb_wf_sub{display:block;font-size:10px;font-weight:500;color:#9ca0aa;line-height:1.4;margin-top:2px}
|
|
397
|
+
.dshadb_wf_head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
|
|
398
|
+
.dshadb_wf_val{font-size:11px;font-weight:800;color:#4f7cff;font-variant-numeric:tabular-nums;flex:none}
|
|
399
|
+
.dshadb_wf_range{width:100%;height:34px;accent-color:#4f7cff;margin:0}
|
|
400
|
+
.dshadb_wf_seg{display:flex;gap:6px;margin-left:auto;flex:none}
|
|
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}
|
|
402
|
+
.dshadb_wf_segbtn_on{background:#4f7cff;border-color:#4f7cff;color:#ffffff}
|
|
403
|
+
.dshadb_wf_disabled{opacity:.45;pointer-events:none}
|
|
404
|
+
.dshadb_wf_tip{font-size:10.5px;color:#9ca0aa;line-height:1.5;padding:2px 4px 0}
|
|
405
|
+
.dshadb_wf_avatar{flex:none;width:34px;height:34px;object-fit:contain;object-position:center;display:block;-webkit-user-drag:none;filter:drop-shadow(0 2px 4px rgba(0,0,0,.12))}
|
|
406
|
+
@media (prefers-color-scheme:dark){
|
|
407
|
+
.dshadb_wf_row,.dshadb_wf_row_col{background:#262a33;border-color:#363c48}
|
|
408
|
+
.dshadb_wf_name{color:#f2f4f8}
|
|
409
|
+
.dshadb_wf_segbtn{background:#1e222a;border-color:#363c48;color:#8b91a0}
|
|
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
|
+
}
|
|
450
|
+
/* ===== 大肥鱼互动挂件 (v1.1.0) ===== */
|
|
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}
|
|
452
|
+
.dshadb-whale.dshadb-whale-dragging{transition:none}
|
|
453
|
+
.dshadb-whale.dshadb-whale-flip .dshadb-whale-sprite{transform:scaleX(-1)}
|
|
454
|
+
.dshadb-whale-sprite{position:absolute;left:0;top:0;width:100%;height:100%;transition:transform .3s ease}
|
|
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}
|
|
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}
|
|
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))}
|
|
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}
|
|
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}
|
|
479
|
+
.dshadb-whale-bubble-on{opacity:1;transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(0) scale(1)}
|
|
480
|
+
/* 连点时气泡不做淡出淡入的来回, 只跟位置走 —— 避免闪烁 */
|
|
481
|
+
.dshadb-whale-bubble-on.dshadb-whale-bubble-keep{transition:transform .16s ease}
|
|
482
|
+
/* 气泡在上方放不下时改到下方, 尾巴翻到顶边 */
|
|
483
|
+
.dshadb-whale-bubble-below{bottom:auto;top:calc(100% + 6px);transform-origin:var(--dshw-btx) 0}
|
|
484
|
+
.dshadb-whale-bubble-below::after{top:auto;bottom:100%;border-top-color:transparent;border-bottom-color:#fff}
|
|
485
|
+
/* 镜像只作用于鲸鱼本体, 气泡与文字保持正向 */
|
|
486
|
+
.dshadb-whale-flip .dshadb-whale-bubble{transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(10px) scale(.82)}
|
|
487
|
+
.dshadb-whale-flip .dshadb-whale-bubble-on{transform:translateX(calc(-50% + var(--dshw-bdx))) translateY(0) scale(1)}
|
|
488
|
+
.dshadb-whale-btext{display:block}
|
|
489
|
+
.dshadb-whale-gif{display:none;width:100%;max-width:190px;border-radius:10px}
|
|
490
|
+
.dshadb-whale-l-a{font-size:12.5px;font-weight:600;color:#3b4356;letter-spacing:.02em;line-height:1.4;white-space:nowrap}
|
|
491
|
+
.dshadb-whale-l-b{font-size:21px;font-weight:800;color:#17181c;line-height:1.15;white-space:nowrap}
|
|
492
|
+
.dshadb-whale-l-p{font-size:18px;font-weight:800;line-height:1.2;white-space:nowrap}
|
|
493
|
+
.dshadb-whale-l-c{font-size:11px;font-weight:600;color:#9ca0aa;line-height:1.4;white-space:nowrap}
|
|
494
|
+
.dshadb-whale-wrap{white-space:normal;word-break:break-word}
|
|
495
|
+
@media (prefers-color-scheme:dark){
|
|
496
|
+
.dshadb-whale-bubble{background:#262a33}.dshadb-whale-bubble::after{border-top-color:#262a33}
|
|
497
|
+
.dshadb-whale-l-a{color:#d3d7e0}.dshadb-whale-l-b{color:#f2f4f8}.dshadb-whale-l-c{color:#9298a6}}`
|
|
300
498
|
|
|
301
499
|
document.head.appendChild(tag);
|
|
302
|
-
/* 渐进增强: 高端机 (逻辑核 >= 6) 才启用毛玻璃等特效 */
|
|
303
|
-
try {
|
|
304
|
-
if (!navigator.hardwareConcurrency || navigator.hardwareConcurrency >= 6) {
|
|
305
|
-
document.documentElement.classList.add("dshadb-high-end");
|
|
306
|
-
}
|
|
307
|
-
} catch (e) { /* ignore */ }
|
|
308
500
|
}
|
|
309
501
|
//#endregion
|
|
310
502
|
|
|
@@ -320,9 +512,10 @@ window.__ModuleLoader__.load({
|
|
|
320
512
|
"detail.total": "当前余额", "detail.topup": "总充值", "detail.used": "总使用", "detail.note": "类型",
|
|
321
513
|
"detail.sessionCost": "本会话消耗", "detail.noBalance": "该平台未开放余额查询",
|
|
322
514
|
"settings.safe": "安全阈值(绿)", "settings.warn": "预警阈值(黄)", "settings.currency": "计价货币",
|
|
515
|
+
"settings.overseasCurrency": "海外模型计价", "settings.overseasFollow": "跟随主货币", "settings.overseasHint": "海外厂商官方价本就是美元, 选美元可免去 ×7 折算误差",
|
|
323
516
|
"settings.safeHint": "余额高于此值显示绿色(安全)",
|
|
324
517
|
"settings.warnHint": "余额低于此值显示黄色(偏低),再低显示红",
|
|
325
|
-
"settings.refresh": "刷新间隔(秒)", "settings.refreshHint": "
|
|
518
|
+
"settings.refresh": "刷新间隔(秒)", "settings.refreshHint": "1~60 秒,最高一分钟",
|
|
326
519
|
"settings.save": "保存并生效", "settings.saved": "已保存",
|
|
327
520
|
"update.title": "版本与更新", "update.current": "当前版本",
|
|
328
521
|
"update.check": "检查更新", "update.checking": "检查中…",
|
|
@@ -331,6 +524,27 @@ window.__ModuleLoader__.load({
|
|
|
331
524
|
"update.done": "已更新, 重启 Web GUI 生效", "update.fail": "更新失败, 已保持原版本",
|
|
332
525
|
"update.checkfail": "检查失败 (网络异常)",
|
|
333
526
|
"settings.section.basic": "基础设置", "settings.section.relays": "中转站", "settings.section.models": "自定义模型",
|
|
527
|
+
"settings.section.whale": "大肥鱼",
|
|
528
|
+
"whale.enable": "收养大肥鱼", "whale.enableHint": "在屏幕边缘养一只会探头的大肥鱼(纯互动,不显示余额)",
|
|
529
|
+
"whale.scale": "身体大小", "whale.peek": "探出多少",
|
|
530
|
+
"whale.sound": "按压音效", "whale.soundSet": "音效组", "whale.volume": "音量",
|
|
531
|
+
"whale.bubble": "台词气泡", "whale.peakMode": "峰谷文案", "whale.snap": "自动靠边",
|
|
532
|
+
"whale.snapHint": "松手后吸附到最近的屏幕边缘;关掉就停在你放的位置",
|
|
533
|
+
"whale.on": "开", "whale.off": "关",
|
|
534
|
+
"whale.duck": "小黄鸭", "whale.fx1": "音效1",
|
|
535
|
+
"whale.peak.default": "默认", "whale.peak.liangwen": "梁文峰谷", "whale.peak.qiangqiang": "!?强强?!",
|
|
536
|
+
"whale.tip": "拖着大肥鱼可以在屏幕上随意移动;点一下弹全身和台词,再点一下收回。",
|
|
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(不表态)",
|
|
334
548
|
"settings.modelName": "模型名称", "settings.apiUrl": "余额接口 URL", "settings.queryType": "解析方式",
|
|
335
549
|
"settings.totalPath": "总余额字段(可选)", "settings.usedPath": "已用字段(可选)",
|
|
336
550
|
"settings.modelHint": "填你的余额接口(返回 JSON),可选填 API Key 与字段路径,如 data.balance",
|
|
@@ -356,9 +570,10 @@ window.__ModuleLoader__.load({
|
|
|
356
570
|
"detail.total": "Balance", "detail.topup": "Top-up", "detail.used": "Used", "detail.note": "Type",
|
|
357
571
|
"detail.sessionCost": "Session cost", "detail.noBalance": "No balance API",
|
|
358
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",
|
|
359
574
|
"settings.safeHint": "Above this = green (safe)",
|
|
360
575
|
"settings.warnHint": "Below this = yellow, lower = red",
|
|
361
|
-
"settings.refresh": "Refresh (sec)", "settings.refreshHint": "
|
|
576
|
+
"settings.refresh": "Refresh (sec)", "settings.refreshHint": "1-60s, max one minute",
|
|
362
577
|
"settings.save": "Save", "settings.saved": "Saved",
|
|
363
578
|
"update.title": "Version & Updates", "update.current": "Current",
|
|
364
579
|
"update.check": "Check updates", "update.checking": "Checking…",
|
|
@@ -367,6 +582,27 @@ window.__ModuleLoader__.load({
|
|
|
367
582
|
"update.done": "Updated. Restart Web GUI to apply", "update.fail": "Update failed, version unchanged",
|
|
368
583
|
"update.checkfail": "Check failed (network)",
|
|
369
584
|
"settings.section.basic": "Basic", "settings.section.relays": "Relays", "settings.section.models": "Custom Models",
|
|
585
|
+
"settings.section.whale": "Whale",
|
|
586
|
+
"whale.enable": "Adopt Big Whale", "whale.enableHint": "Keep a peeking whale on the screen edge (interactive only)",
|
|
587
|
+
"whale.scale": "Body size", "whale.peek": "Peek amount",
|
|
588
|
+
"whale.sound": "Press sound", "whale.soundSet": "Sound set", "whale.volume": "Volume",
|
|
589
|
+
"whale.bubble": "Speech bubble", "whale.peakMode": "Peak wording", "whale.snap": "Snap to edge",
|
|
590
|
+
"whale.snapHint": "Snaps to the nearest edge on release; turn off to keep it where you drop it",
|
|
591
|
+
"whale.on": "On", "whale.off": "Off",
|
|
592
|
+
"whale.duck": "Duck", "whale.fx1": "FX 1",
|
|
593
|
+
"whale.peak.default": "Default", "whale.peak.liangwen": "Liangwen", "whale.peak.qiangqiang": "!?Qiang?!",
|
|
594
|
+
"whale.tip": "Drag the whale anywhere on screen; tap once for full body and a line, tap again to tuck it back.",
|
|
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)",
|
|
370
606
|
"settings.modelName": "Model name", "settings.apiUrl": "Balance API URL", "settings.queryType": "Parse type",
|
|
371
607
|
"settings.totalPath": "Total field (opt)", "settings.usedPath": "Used field (opt)",
|
|
372
608
|
"settings.modelHint": "A JSON endpoint for your balance; optional API key & field paths like data.balance",
|
|
@@ -390,29 +626,39 @@ window.__ModuleLoader__.load({
|
|
|
390
626
|
const listeners = new Set();
|
|
391
627
|
let timer = null, pollMs = DEFAULT_POLL_MS, inflight = null, started = false;
|
|
392
628
|
function notify() { for (const fn of [...listeners]) fn(); }
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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(); }
|
|
396
638
|
inflight = (async () => {
|
|
397
639
|
try {
|
|
398
|
-
const url =
|
|
399
|
-
|
|
400
|
-
|
|
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" });
|
|
401
645
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
402
646
|
const data = await res.json();
|
|
403
|
-
if (data.config?.clientPollIntervalMs >=
|
|
647
|
+
if (data.config?.clientPollIntervalMs >= 1000) pollMs = Math.min(data.config.clientPollIntervalMs, 3600000);
|
|
404
648
|
// v0.5.0 性能适配: 内容无变化就不重建快照引用, useSyncExternalStore 判定 Object.is 相同直接跳过整棵树重渲
|
|
405
649
|
const newBalances = data.balances || [];
|
|
406
650
|
const oldBalances = Array.isArray(snapshot.balances) ? snapshot.balances : [];
|
|
407
651
|
const changed = JSON.stringify(newBalances) !== JSON.stringify(oldBalances) || snapshot.status !== "ok";
|
|
408
652
|
const nextFetchedAt = data.fetchedAt || Date.now();
|
|
409
653
|
if (!changed) {
|
|
410
|
-
|
|
654
|
+
// v1.1.0: config 必须始终跟随最新响应 —— 旧写法在余额未变时保留旧 config,
|
|
655
|
+
// 导致设置里改完开关(如 whaleEnabled)后前端永远读不到新值, 表现为"开了又变回关"
|
|
656
|
+
snapshot = { ...snapshot, config: data.config || snapshot.config, fetchedAt: nextFetchedAt, isRefreshing: false };
|
|
411
657
|
} else {
|
|
412
658
|
snapshot = { status: "ok", balances: newBalances, config: data.config || snapshot.config, fetchedAt: nextFetchedAt, isRefreshing: false };
|
|
413
659
|
}
|
|
414
660
|
} catch (error) {
|
|
415
|
-
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 };
|
|
416
662
|
}
|
|
417
663
|
inflight = null; notify();
|
|
418
664
|
})();
|
|
@@ -423,7 +669,7 @@ window.__ModuleLoader__.load({
|
|
|
423
669
|
timer = setTimeout(() => { timer = null; if (document.hidden) return; refresh().then(schedule, schedule); }, pollMs);
|
|
424
670
|
}
|
|
425
671
|
const store = {
|
|
426
|
-
subscribe(fn) { listeners.add(fn); if (!started) { started = true; refresh(true).then(schedule, schedule); } return () => { listeners.delete(fn); if (listeners.size === 0) { started = false; if (timer !== null) { clearTimeout(timer); timer = null; } } }; },
|
|
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; } } }; },
|
|
427
673
|
getSnapshot() { return snapshot; },
|
|
428
674
|
forceRefresh() { return refresh(true); },
|
|
429
675
|
};
|
|
@@ -432,17 +678,153 @@ window.__ModuleLoader__.load({
|
|
|
432
678
|
//#region helpers
|
|
433
679
|
const CURRENCY_SYMBOLS = { CNY: "¥", USD: "$", EUR: "€" };
|
|
434
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
|
+
}
|
|
435
814
|
function formatMoney(amount, currency) {
|
|
436
815
|
if (typeof amount !== "number" || isNaN(amount)) return currencySymbol(currency) + "0";
|
|
437
816
|
if (currency === "%") return Math.round(amount) + "%";
|
|
438
817
|
if (currency === "tokens") return (amount >= 1e6 ? (amount/1e6).toFixed(1)+"M" : amount >= 1e3 ? (amount/1e3).toFixed(1)+"K" : Math.round(amount) + " tok");
|
|
439
|
-
const
|
|
818
|
+
const abs = Math.abs(amount);
|
|
819
|
+
const fixed = abs === 0 || abs >= 1 ? 2 : abs >= 0.01 ? 3 : 4;
|
|
440
820
|
return currencySymbol(currency) + amount.toFixed(fixed);
|
|
441
821
|
}
|
|
442
822
|
function formatTime(ms) { if (!ms || ms <= 0) return "—"; return new Date(ms).toLocaleTimeString(); }
|
|
443
|
-
/** 状态条金额文字 (供 BarReadout 的 rAF 节流 state 使用)
|
|
444
|
-
|
|
823
|
+
/** 状态条金额文字 (供 BarReadout 的 rAF 节流 state 使用)
|
|
824
|
+
* viaRelay=true 时走的是中转站, 官方余额与当前用量无关 → 一律显示「—」 */
|
|
825
|
+
function barAmountText(current, t, viaRelay) {
|
|
445
826
|
if (!current) return "";
|
|
827
|
+
if (viaRelay) return "—";
|
|
446
828
|
if (current.status === "ok") {
|
|
447
829
|
const cur = current.currency || "USD";
|
|
448
830
|
const num = typeof current.total === "number" ? current.total : 0;
|
|
@@ -451,15 +833,54 @@ window.__ModuleLoader__.load({
|
|
|
451
833
|
return formatMoney(num, cur);
|
|
452
834
|
}
|
|
453
835
|
if (current.status === "no-key") return t("status.nokey");
|
|
454
|
-
if (current.status === "no-balance-api") return
|
|
836
|
+
if (current.status === "no-balance-api") return "—";
|
|
455
837
|
if (current.status === "local") return t("status.local");
|
|
456
838
|
return t("status.err");
|
|
457
839
|
}
|
|
458
|
-
/**
|
|
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 单独处理。 */
|
|
459
873
|
function modelToPlatform(model) {
|
|
460
874
|
if (!model || typeof model !== "string") return "";
|
|
461
875
|
const m = model.toLowerCase();
|
|
462
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";
|
|
463
884
|
if (/glm|chatglm/.test(m)) return "zhipu";
|
|
464
885
|
if (/kimi|moonshot/.test(m)) return "moonshot";
|
|
465
886
|
if (/^step[-_]/.test(m)) return "stepfun";
|
|
@@ -496,12 +917,17 @@ window.__ModuleLoader__.load({
|
|
|
496
917
|
|
|
497
918
|
|
|
498
919
|
//#region 一层 bar
|
|
499
|
-
function BarReadout({ t, onOpen, onOpenSettings, selectedId, onSelect, config, cost }) {
|
|
920
|
+
function BarReadout({ t, onOpen, onOpenSettings, selectedId, onSelect, config, cost, provider }) {
|
|
500
921
|
const data = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
501
922
|
const balances = data.balances || [];
|
|
502
923
|
let current = balances.find(b => b.platform === selectedId);
|
|
503
924
|
if (!current) current = balances.find(b => b.status === "ok") || balances[0];
|
|
504
|
-
|
|
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
|
+
});
|
|
505
931
|
|
|
506
932
|
// v0.5.0 手机端: 点击状态条开看板, 长按 500ms 弹设置菜单 (替代常驻齿轮)
|
|
507
933
|
const pressTimer = react.useRef(null);
|
|
@@ -522,19 +948,20 @@ window.__ModuleLoader__.load({
|
|
|
522
948
|
const handleBarClick = () => { if (longPressed.current) return; onOpen(); };
|
|
523
949
|
|
|
524
950
|
// v0.5.0: 金额文字 rAF 延迟一帧写入, 合并同一帧内的重复变化
|
|
525
|
-
const [displayAmount, setDisplayAmount] = react.useState(barAmountText(current, t));
|
|
951
|
+
const [displayAmount, setDisplayAmount] = react.useState(barAmountText(current, t, viaRelay));
|
|
526
952
|
react.useEffect(() => {
|
|
527
|
-
const txt = barAmountText(current, t);
|
|
953
|
+
const txt = barAmountText(current, t, viaRelay);
|
|
528
954
|
const raf = requestAnimationFrame(() => setDisplayAmount((prev) => (prev === txt ? prev : txt)));
|
|
529
955
|
return () => cancelAnimationFrame(raf);
|
|
530
956
|
});
|
|
531
957
|
|
|
958
|
+
const hasPeakInfo = typeof config?.isPeak === "boolean";
|
|
532
959
|
const isPeak = config?.isPeak === true;
|
|
533
960
|
const isWknd = config?.isWeekend === true;
|
|
534
961
|
|
|
535
962
|
// 峰谷标记 (趣味版: 梁文峰 ☀️ / 梁文谷 🌙)
|
|
536
963
|
let peakNode = null;
|
|
537
|
-
if (
|
|
964
|
+
if (hasPeakInfo) {
|
|
538
965
|
const peakTitle = isPeak
|
|
539
966
|
? t("peak.now") + " " + t("peak.hint")
|
|
540
967
|
: (isWknd ? t("valley.weekend") : t("valley.now"));
|
|
@@ -560,7 +987,9 @@ window.__ModuleLoader__.load({
|
|
|
560
987
|
]);
|
|
561
988
|
|
|
562
989
|
const meta = metaFor(current.platform), level = getLevel(current, config);
|
|
563
|
-
const
|
|
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;
|
|
564
993
|
const amtClass = "dshadb_bar_amount " + (level === "ok" ? "dshadb_bar_ok" : level === "warn" ? "dshadb_bar_warn" : "dshadb_bar_err");
|
|
565
994
|
return react.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "4px" } }, [
|
|
566
995
|
react.createElement("span", {
|
|
@@ -572,7 +1001,7 @@ window.__ModuleLoader__.load({
|
|
|
572
1001
|
react.createElement("span", { className: "dshadb_bar_logo", key: "logo" }, renderLogo(current)),
|
|
573
1002
|
react.createElement("span", { className: "dshadb_bar_name", key: "name" }, meta.name),
|
|
574
1003
|
react.createElement("span", { className: amtClass, key: "amount" }, displayAmount),
|
|
575
|
-
react.createElement("span", { className: dotClass, key: "dot" }),
|
|
1004
|
+
react.createElement("span", { className: dotClass, style: dotStyle, key: "dot" }),
|
|
576
1005
|
peakNode,
|
|
577
1006
|
react.createElement("span", { className: "dshadb_bar_cost", title: cost?.title, key: "cost" }, cost?.text || "~—"),
|
|
578
1007
|
]),
|
|
@@ -581,7 +1010,6 @@ window.__ModuleLoader__.load({
|
|
|
581
1010
|
//#endregion
|
|
582
1011
|
|
|
583
1012
|
//#region 折叠分组 (带展开过渡动画 + 分类色点)
|
|
584
|
-
const GROUP_DOT = { domestic: "domestic", abroad: "abroad", local: "local", relay: "relay", custom: "custom" };
|
|
585
1013
|
function GroupSection({ label, count, open, onToggle, children }) {
|
|
586
1014
|
return react.createElement("div", { key: label }, [
|
|
587
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(); } } }, [
|
|
@@ -604,7 +1032,7 @@ window.__ModuleLoader__.load({
|
|
|
604
1032
|
function DashboardDrawer({ isOpen, onClose, t, selectedId, onSelect, onOpenDetail, onAddRelay, onAddCustom, onOpenSettings, config, currentModel }) {
|
|
605
1033
|
const data = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
606
1034
|
const [isRefreshing, setRefreshing] = react.useState(false);
|
|
607
|
-
const [openGroups, setOpenGroups] = react.useState({ domestic: false, abroad: false, relay: false,
|
|
1035
|
+
const [openGroups, setOpenGroups] = react.useState({ domestic: false, abroad: false, relay: false, custom: false, nobalance: false });
|
|
608
1036
|
react.useEffect(() => {
|
|
609
1037
|
if (!isOpen) return;
|
|
610
1038
|
const handleKey = (e) => { if (e.key === "Escape") onClose(); };
|
|
@@ -618,10 +1046,17 @@ window.__ModuleLoader__.load({
|
|
|
618
1046
|
const okCount = balances.filter(b => b.status === "ok").length;
|
|
619
1047
|
|
|
620
1048
|
const groups = { domestic: [], abroad: [], relay: [], custom: [] };
|
|
1049
|
+
const noBalanceBrands = [];
|
|
621
1050
|
let selectedBalance = null;
|
|
1051
|
+
const showBrands = !!(config && config.showNoBalanceBrands);
|
|
622
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
|
+
}
|
|
623
1059
|
const cat = b.category || metaFor(b.platform).cat;
|
|
624
|
-
if (b.platform === selectedId) { selectedBalance = b; continue; } // ② 选中模型从分组排除,避免重复
|
|
625
1060
|
if (cat === "海外" || cat === "官方") groups.abroad.push(b);
|
|
626
1061
|
else if (cat === "中转站") groups.relay.push(b);
|
|
627
1062
|
else if (cat === "自定义") groups.custom.push(b);
|
|
@@ -633,17 +1068,21 @@ window.__ModuleLoader__.load({
|
|
|
633
1068
|
const meta = metaFor(b.platform), level = getLevel(b, config), isSelected = b.platform === selectedId;
|
|
634
1069
|
const cardClass = "dshadb_card " + (isSelected ? "dshadb_card_active " : "");
|
|
635
1070
|
// descText 逻辑
|
|
1071
|
+
// v1.4.0 修复: 原分支硬编码 "· 配额正常", 与三色灯(level)完全无关 —— GLM 配额耗尽时
|
|
1072
|
+
// (level=err、note 已写「配额已用完(0), 待重置」) 会渲染成「…待重置 · 配额正常」, 自相矛盾。
|
|
1073
|
+
// 改为统一按 level 取状态标签, 保证文案与灯色一致。
|
|
1074
|
+
// (sessionCost/modelId 服务端目前从不产出, 该分支保留仅为将来兼容, 不再是唯一出口。)
|
|
636
1075
|
let descText;
|
|
637
1076
|
if (b.status === "ok" && b.sessionCost != null) {
|
|
638
1077
|
descText = (b.modelId || "—") + " · 会话消耗 " + (typeof b.sessionCost === "number" ? b.sessionCost.toFixed(2) : b.sessionCost);
|
|
639
1078
|
} else if (b.status === "ok" && b.note) {
|
|
640
|
-
descText = b.note + " ·
|
|
1079
|
+
descText = b.note + " · " + statusLabel(level, t);
|
|
641
1080
|
} else if (b.status === "ok") {
|
|
642
|
-
descText =
|
|
1081
|
+
descText = statusLabel(level, t);
|
|
643
1082
|
} else if (b.status === "local") {
|
|
644
1083
|
descText = "本地模型";
|
|
645
1084
|
} else if (b.status === "noBalance" || b.status === "no-balance-api") {
|
|
646
|
-
descText = "
|
|
1085
|
+
descText = "按价格表估算消耗";
|
|
647
1086
|
} else {
|
|
648
1087
|
descText = statusLabel(level, t);
|
|
649
1088
|
}
|
|
@@ -656,15 +1095,16 @@ window.__ModuleLoader__.load({
|
|
|
656
1095
|
amtClass = level === "ok" ? "dshadb_card_status_ok" : level === "warn" ? "dshadb_card_status_warn" : "dshadb_card_status_err";
|
|
657
1096
|
amount = val;
|
|
658
1097
|
} else if (b.status === "no-key") { amount = t("status.nokey"); amtClass = "dshadb_card_amount_sub"; }
|
|
659
|
-
else if (b.status === "no-balance-api") { amount =
|
|
1098
|
+
else if (b.status === "no-balance-api") { amount = "—"; amtClass = "dshadb_card_amount_sub"; }
|
|
660
1099
|
else if (b.status === "local") { amount = t("status.local"); amtClass = "dshadb_card_amount_sub"; }
|
|
661
1100
|
else { amount = b.error || t("error.unknown"); amtClass = "dshadb_card_status_err"; }
|
|
662
|
-
const
|
|
663
|
-
const
|
|
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);
|
|
664
1104
|
const rightNode = react.createElement("div", { className: "dshadb_card_right", key: "right" }, [
|
|
665
1105
|
react.createElement("span", { className: "dshadb_card_amount" + (amtClass ? " " + amtClass : ""), key: "amt" }, amount),
|
|
666
1106
|
react.createElement("span", { className: "dshadb_card_status " + stClass, key: "st" }, [
|
|
667
|
-
react.createElement("span", { className: "dshadb_status_dot dshadb_status_dot_" + (level === "ok" ? "ok" : level === "warn" ? "warn" : "err"), key: "dot" }),
|
|
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 }),
|
|
668
1108
|
statusText,
|
|
669
1109
|
]),
|
|
670
1110
|
]);
|
|
@@ -701,6 +1141,7 @@ window.__ModuleLoader__.load({
|
|
|
701
1141
|
// 国内平台 (含deepseek)
|
|
702
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,
|
|
703
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,
|
|
704
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,
|
|
705
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,
|
|
706
1147
|
// ③ 添加按钮居中两边对齐
|
|
@@ -715,8 +1156,10 @@ window.__ModuleLoader__.load({
|
|
|
715
1156
|
}
|
|
716
1157
|
|
|
717
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" }),
|
|
718
1161
|
react.createElement("div", { className: "dshadb_drawer", onClick: (e) => e.stopPropagation(), key: "drawer" }, [
|
|
719
|
-
react.createElement(
|
|
1162
|
+
react.createElement(SwipeHandle, { onClose, key: "handle" }),
|
|
720
1163
|
react.createElement("div", { className: "dshadb_header", key: "header" }, [
|
|
721
1164
|
react.createElement("div", { className: "dshadb_header_left", key: "left" }, [
|
|
722
1165
|
// 模型尾椎: 从 currentModel 提取变体名, 如 deepseek-v4-flash → DSH·V4
|
|
@@ -740,7 +1183,7 @@ window.__ModuleLoader__.load({
|
|
|
740
1183
|
t("title"),
|
|
741
1184
|
]),
|
|
742
1185
|
react.createElement("div", { className: "dshadb_header_sub", key: "sub" },
|
|
743
|
-
t("all.ok", { n: okCount }) + " · " + t("refreshing")
|
|
1186
|
+
t("all.ok", { n: okCount }) + (isRefreshing ? " · " + t("refreshing") : "")
|
|
744
1187
|
),
|
|
745
1188
|
]),
|
|
746
1189
|
react.createElement("button", {
|
|
@@ -755,7 +1198,7 @@ window.__ModuleLoader__.load({
|
|
|
755
1198
|
data.fetchedAt ? react.createElement("div", { className: "dshadb_footer", key: "footer" }, [
|
|
756
1199
|
"更新于 " + formatTime(data.fetchedAt),
|
|
757
1200
|
react.createElement("span", { className: "dshadb_footer_dot", key: "d1" }),
|
|
758
|
-
react.createElement("
|
|
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 刷新"),
|
|
759
1202
|
]) : null,
|
|
760
1203
|
]),
|
|
761
1204
|
]);
|
|
@@ -787,21 +1230,9 @@ window.__ModuleLoader__.load({
|
|
|
787
1230
|
const b = (data.balances || []).find(x => x.platform === platformId);
|
|
788
1231
|
if (!b) return null;
|
|
789
1232
|
const meta = metaFor(b.platform), level = getLevel(b, config);
|
|
790
|
-
const
|
|
791
|
-
const costVal =
|
|
1233
|
+
const costFmtDetail = formatSessionCost(cost);
|
|
1234
|
+
const costVal = costFmtDetail !== null && costFmtDetail.hasValue ? costFmtDetail.text : "—";
|
|
792
1235
|
const cur = b.currency || "USD";
|
|
793
|
-
const detailRows = [];
|
|
794
|
-
if (b.status === "ok") {
|
|
795
|
-
detailRows.push(row(t("detail.total"), formatMoney(b.percent != null ? b.percent : b.total, b.percent != null ? "%" : cur) + (b.percent != null ? "%" : "")));
|
|
796
|
-
// v0.5.6: 充值行仅在与余额有差异时显示 (未消耗时 充值==余额 两行重复); 使用行为 0 不显示
|
|
797
|
-
if (b.topup != null && Math.abs(b.topup - (b.total || 0)) > 1e-9) detailRows.push(row(t("detail.topup"), formatMoney(b.topup, cur)));
|
|
798
|
-
if (b.used != null && b.used > 0) detailRows.push(row(t("detail.used"), formatMoney(b.used, cur)));
|
|
799
|
-
if (b.note) detailRows.push(row(t("detail.note"), b.note));
|
|
800
|
-
if (b.staleNote) detailRows.push(row("数据状态", b.staleNote));
|
|
801
|
-
} else {
|
|
802
|
-
detailRows.push(row(t("detail.note"), b.error || "—"));
|
|
803
|
-
}
|
|
804
|
-
detailRows.push(row(t("detail.sessionCost"), costVal));
|
|
805
1236
|
|
|
806
1237
|
// DeepSeek 峰谷趣味卡片
|
|
807
1238
|
let peakCard = null;
|
|
@@ -847,8 +1278,9 @@ window.__ModuleLoader__.load({
|
|
|
847
1278
|
|
|
848
1279
|
// v0.5.3: 回退为底部抽屉样式 (全屏卡片观感不佳, 复用看板同款 scrim+drawer)
|
|
849
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" }),
|
|
850
1282
|
react.createElement("div", { className: "dshadb_drawer", style: { maxHeight: "70vh" }, onClick: (e) => e.stopPropagation(), key: "drawer" }, [
|
|
851
|
-
react.createElement(
|
|
1283
|
+
react.createElement(SwipeHandle, { onClose, key: "handle" }),
|
|
852
1284
|
react.createElement("div", { className: "dshadb_header", key: "header" }, [
|
|
853
1285
|
react.createElement("div", { className: "dshadb_header_left", key: "left" }, [
|
|
854
1286
|
react.createElement("div", { className: "dshadb_header_title", key: "title", style: { fontSize: "18px" } }, meta.name),
|
|
@@ -909,8 +1341,8 @@ window.__ModuleLoader__.load({
|
|
|
909
1341
|
return metrics.length > 0 ? react.createElement("div", { className: "dshadb_detail_metrics", key: "metrics" }, metrics) : null;
|
|
910
1342
|
})(),
|
|
911
1343
|
// staleNote(暂存数据提示)
|
|
912
|
-
b.staleNote ?
|
|
913
|
-
b.status !== "ok" ?
|
|
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,
|
|
914
1346
|
peakCard,
|
|
915
1347
|
priceCard,
|
|
916
1348
|
]),
|
|
@@ -928,7 +1360,31 @@ window.__ModuleLoader__.load({
|
|
|
928
1360
|
const [safe, setSafe] = react.useState(50);
|
|
929
1361
|
const [warn, setWarn] = react.useState(10);
|
|
930
1362
|
const [currency, setCurrency] = react.useState("CNY");
|
|
1363
|
+
// v1.3.2: 海外模型独立计价货币 follow|USD|CNY
|
|
1364
|
+
const [overseasCurrency, setOverseasCurrency] = react.useState("follow");
|
|
931
1365
|
const [refreshSec, setRefreshSec] = react.useState(5);
|
|
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);
|
|
1383
|
+
// v1.1.0: 大肥鱼细项 (独立页签, 走 /api-dashboard/whale/settings, 与看板主配置分开)
|
|
1384
|
+
const [wf, setWf] = react.useState({
|
|
1385
|
+
scale: 1, peekRatio: 0.5, soundOn: true, soundSet: "duck",
|
|
1386
|
+
volume: 0.5, bubbleOn: true, peakMode: "default", snapOn: true,
|
|
1387
|
+
});
|
|
932
1388
|
const [form, setForm] = react.useState({ name: "", baseUrl: "", apiKey: "", queryType: "auto" });
|
|
933
1389
|
const [mform, setMform] = react.useState({ name: "", apiUrl: "", apiKey: "", queryType: "auto", totalPath: "", usedPath: "", currency: "" });
|
|
934
1390
|
const [loading, setLoading] = react.useState(false);
|
|
@@ -954,13 +1410,15 @@ window.__ModuleLoader__.load({
|
|
|
954
1410
|
}, []);
|
|
955
1411
|
react.useEffect(() => { if (isOpen) checkUpdate(false); }, [isOpen]); // 打开面板自动检查
|
|
956
1412
|
const installUpdate = () => {
|
|
957
|
-
if (!window.confirm(t("update.available") + " v" + (upd.info?.remote || "") + " ?")) return;
|
|
958
1413
|
setUpd(s => ({ ...s, phase: "installing" }));
|
|
959
1414
|
fetch("/api-dashboard/update/install", { method: "POST" })
|
|
960
1415
|
.then(r => r.json().then(d => ({ code: r.status, body: d })))
|
|
961
1416
|
.then(({ code, body }) => {
|
|
962
1417
|
if (code === 200 && body.ok) {
|
|
963
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 {}
|
|
964
1422
|
} else {
|
|
965
1423
|
setUpd(s => ({ ...s, phase: "fail", msg: body.error === "already up to date" ? t("update.latest") : t("update.fail") }));
|
|
966
1424
|
checkUpdate(true);
|
|
@@ -981,7 +1439,9 @@ window.__ModuleLoader__.load({
|
|
|
981
1439
|
setSafe(config?.safeThreshold ?? 50);
|
|
982
1440
|
setWarn(config?.warnThreshold ?? 10);
|
|
983
1441
|
setCurrency(config?.currency ?? "CNY");
|
|
1442
|
+
setOverseasCurrency(config?.overseasCurrency ?? "follow");
|
|
984
1443
|
setRefreshSec(Math.round((config?.clientPollIntervalMs || 5000) / 1000) || 5);
|
|
1444
|
+
setWhaleOn(!!(config && config.whaleEnabled));
|
|
985
1445
|
let cancelled = false;
|
|
986
1446
|
fetch("/api-dashboard/config", { cache: "no-store" }).then(r => r.json()).then(d => {
|
|
987
1447
|
if (cancelled) return;
|
|
@@ -989,19 +1449,50 @@ window.__ModuleLoader__.load({
|
|
|
989
1449
|
setRelays(d.customRelays || []);
|
|
990
1450
|
setModels(d.customModels || []);
|
|
991
1451
|
if (d.refreshIntervalSec) setRefreshSec(d.refreshIntervalSec);
|
|
1452
|
+
if (typeof d.overseasCurrency === "string") setOverseasCurrency(d.overseasCurrency);
|
|
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);
|
|
992
1458
|
}
|
|
993
1459
|
}).finally(() => { if (!cancelled) setLoading(false); });
|
|
1460
|
+
// 大肥鱼细项与主配置并行拉取, 失败保持默认值
|
|
1461
|
+
fetch("/api-dashboard/whale/settings", { cache: "no-store" }).then(r => r.json()).then(d => {
|
|
1462
|
+
if (cancelled || !d || !d.ok || !d.settings) return;
|
|
1463
|
+
const s = d.settings;
|
|
1464
|
+
setWf(prev => ({
|
|
1465
|
+
scale: typeof s.scale === "number" ? s.scale : prev.scale,
|
|
1466
|
+
peekRatio: typeof s.peekRatio === "number" ? s.peekRatio : prev.peekRatio,
|
|
1467
|
+
soundOn: typeof s.soundOn === "boolean" ? s.soundOn : prev.soundOn,
|
|
1468
|
+
soundSet: s.soundSet || prev.soundSet,
|
|
1469
|
+
volume: typeof s.volume === "number" ? s.volume : prev.volume,
|
|
1470
|
+
bubbleOn: typeof s.bubbleOn === "boolean" ? s.bubbleOn : prev.bubbleOn,
|
|
1471
|
+
peakMode: s.peakMode || prev.peakMode,
|
|
1472
|
+
snapOn: typeof s.snapOn === "boolean" ? s.snapOn : prev.snapOn,
|
|
1473
|
+
}));
|
|
1474
|
+
}).catch(() => {});
|
|
994
1475
|
return () => { cancelled = true; };
|
|
995
1476
|
}, [isOpen]);
|
|
996
1477
|
if (!isOpen) return null;
|
|
997
1478
|
const save = async () => {
|
|
1479
|
+
const nextRefreshSec = Math.min(Math.max(Number(refreshSec) || 5, 5), 60)
|
|
1480
|
+
setRefreshSec(nextRefreshSec)
|
|
998
1481
|
setSaving(true);
|
|
999
1482
|
try {
|
|
1000
1483
|
await fetch("/api-dashboard/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({
|
|
1001
1484
|
customRelays: relays, customModels: models,
|
|
1002
|
-
safeThreshold: Number(safe), warnThreshold: Number(warn), currency,
|
|
1003
|
-
refreshIntervalSec:
|
|
1485
|
+
safeThreshold: Number(safe), warnThreshold: Number(warn), currency, overseasCurrency,
|
|
1486
|
+
refreshIntervalSec: nextRefreshSec,
|
|
1487
|
+
whaleEnabled: !!whaleOn,
|
|
1488
|
+
officialProviders: officialText,
|
|
1489
|
+
// v1.4.0「真自动」: 关掉的 DSH provider (空数组 = 全部启用)
|
|
1490
|
+
dshProviderOptOut: dshOff,
|
|
1004
1491
|
}) });
|
|
1492
|
+
// v1.1.0: 保存后立刻强拉一次, 让开关类设置(大肥鱼)即时生效, 不必等下一轮轮询
|
|
1493
|
+
try { await store.forceRefresh(); } catch (e) { /* 忽略 */ }
|
|
1494
|
+
// 双保险: 快照万一没及时更新, 也按用户刚保存的值直接挂载/卸载 (两处均幂等)
|
|
1495
|
+
if (whaleOn) ensureWhaleWidget(); else removeWhaleWidget();
|
|
1005
1496
|
onClose();
|
|
1006
1497
|
} finally { setSaving(false); }
|
|
1007
1498
|
};
|
|
@@ -1017,13 +1508,50 @@ window.__ModuleLoader__.load({
|
|
|
1017
1508
|
setMform({ name: "", apiUrl: "", apiKey: "", queryType: "auto", totalPath: "", usedPath: "", currency: "" });
|
|
1018
1509
|
};
|
|
1019
1510
|
const removeModel = (id) => setModels(models.filter(m => m.id !== id));
|
|
1511
|
+
// v1.1.0 修复: 开关类设置即时保存 —— 不能要求用户必须点「保存并生效」,
|
|
1512
|
+
// 否则拨完开关直接返回/关闭就丢了, 表现为"开了又变回关"
|
|
1513
|
+
const toggleWhale = async (next) => {
|
|
1514
|
+
setWhaleOn(next);
|
|
1515
|
+
if (next) ensureWhaleWidget(); else removeWhaleWidget();
|
|
1516
|
+
try {
|
|
1517
|
+
await fetch("/api-dashboard/config", {
|
|
1518
|
+
method: "POST", headers: { "Content-Type": "application/json" },
|
|
1519
|
+
body: JSON.stringify({ whaleEnabled: !!next }),
|
|
1520
|
+
});
|
|
1521
|
+
await store.forceRefresh();
|
|
1522
|
+
} catch (e) { /* 网络失败: 本地仍生效, 下次保存/刷新会对齐 */ }
|
|
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
|
+
};
|
|
1020
1535
|
const qtLabel = (q) => t("qt." + (q || "auto"));
|
|
1021
1536
|
|
|
1537
|
+
// v1.1.0: 大肥鱼细项即时生效 + 即时落盘 (滑块/开关都不依赖「保存并生效」按钮)
|
|
1538
|
+
const patchWf = (patch) => {
|
|
1539
|
+
setWf(prev => ({ ...prev, ...patch }));
|
|
1540
|
+
patchWhaleWidget(patch);
|
|
1541
|
+
try {
|
|
1542
|
+
fetch("/api-dashboard/whale/settings", {
|
|
1543
|
+
method: "PUT", headers: { "Content-Type": "application/json" },
|
|
1544
|
+
body: JSON.stringify(patch),
|
|
1545
|
+
}).catch(() => {});
|
|
1546
|
+
} catch (e) { /* 忽略 */ }
|
|
1547
|
+
};
|
|
1548
|
+
|
|
1022
1549
|
const tab = (id, label) => react.createElement("button", { type: "button", className: "dshadb_tab" + (section === id ? " dshadb_tab_active" : ""), onClick: () => setSection(id), key: id }, label);
|
|
1023
1550
|
const tabsNode = react.createElement("div", { className: "dshadb_tabs", key: "tabs" }, [
|
|
1024
1551
|
tab("basic", t("settings.section.basic")),
|
|
1025
1552
|
tab("relays", t("settings.section.relays")),
|
|
1026
1553
|
tab("models", t("settings.section.models")),
|
|
1554
|
+
tab("whale", t("settings.section.whale")),
|
|
1027
1555
|
]);
|
|
1028
1556
|
|
|
1029
1557
|
// 基础设置: 阈值 / 币种 / 刷新间隔
|
|
@@ -1043,24 +1571,93 @@ window.__ModuleLoader__.load({
|
|
|
1043
1571
|
react.createElement("div", { className: "dshadb_grid2", key: "misc" }, [
|
|
1044
1572
|
react.createElement("div", { key: "cur" }, [
|
|
1045
1573
|
react.createElement("label", { className: "dshadb_label", key: "cur_l" }, t("settings.currency")),
|
|
1046
|
-
react.createElement("select", { className: "dshadb_field", value: currency, onChange: (e) => setCurrency(e.target.value), key: "cur_s" }, [
|
|
1574
|
+
react.createElement("select", { className: "dshadb_field dshadb_field_select", value: currency, onChange: (e) => setCurrency(e.target.value), key: "cur_s" }, [
|
|
1047
1575
|
react.createElement("option", { value: "CNY", key: "c1" }, t("cmn")),
|
|
1048
1576
|
react.createElement("option", { value: "USD", key: "c2" }, t("usd")),
|
|
1049
1577
|
]),
|
|
1050
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
|
+
]),
|
|
1051
1589
|
react.createElement("div", { key: "refresh" }, [
|
|
1052
1590
|
react.createElement("label", { className: "dshadb_label", key: "rf_l" }, t("settings.refresh")),
|
|
1053
|
-
react.createElement("input", { className: "dshadb_field", type: "number", min:
|
|
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" }),
|
|
1054
1592
|
react.createElement("span", { style: { fontSize: "10px", color: "var(--dsw-alias-label-tertiary)" }, key: "rf_h" }, t("settings.refreshHint")),
|
|
1055
1593
|
]),
|
|
1056
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,
|
|
1057
1656
|
]);
|
|
1058
1657
|
|
|
1059
1658
|
// 中转站区块
|
|
1060
1659
|
const relaySection = react.createElement("div", { className: "dshadb_settings_section", key: "s_relays" }, [
|
|
1061
1660
|
react.createElement("div", { style: { marginTop: "2px", marginBottom: "8px" }, key: "relay_form" }, [
|
|
1062
|
-
react.createElement("label", { className: "dshadb_label", key: "r1" }, "中转站名称"),
|
|
1063
|
-
react.createElement("input", { className: "dshadb_field", value: form.name, placeholder: "我的中转站", onChange: (e) => setForm({ ...form, name: e.target.value }), key: "ri1" }),
|
|
1064
1661
|
react.createElement("label", { className: "dshadb_label", key: "r1" }, "中转站名称"),
|
|
1065
1662
|
react.createElement("input", { className: "dshadb_field", value: form.name, placeholder: "我的中转站", onChange: (e) => setForm({ ...form, name: e.target.value }), key: "ri1" }),
|
|
1066
1663
|
react.createElement("label", { className: "dshadb_label", key: "r2" }, "Base URL"),
|
|
@@ -1097,7 +1694,7 @@ window.__ModuleLoader__.load({
|
|
|
1097
1694
|
]),
|
|
1098
1695
|
react.createElement("div", { key: "m4" }, [
|
|
1099
1696
|
react.createElement("label", { className: "dshadb_label", key: "ml4" }, t("settings.queryType")),
|
|
1100
|
-
react.createElement("select", { className: "dshadb_field", value: mform.queryType, onChange: (e) => setMform({ ...mform, queryType: e.target.value }), key: "mi4" }, [
|
|
1697
|
+
react.createElement("select", { className: "dshadb_field dshadb_field_select", value: mform.queryType, onChange: (e) => setMform({ ...mform, queryType: e.target.value }), key: "mi4" }, [
|
|
1101
1698
|
react.createElement("option", { value: "auto", key: "q0" }, t("qt.auto")),
|
|
1102
1699
|
react.createElement("option", { value: "openai", key: "q1" }, t("qt.openai")),
|
|
1103
1700
|
react.createElement("option", { value: "quota", key: "q2" }, t("qt.quota")),
|
|
@@ -1123,7 +1720,7 @@ window.__ModuleLoader__.load({
|
|
|
1123
1720
|
]),
|
|
1124
1721
|
react.createElement("div", { key: "m7" }, [
|
|
1125
1722
|
react.createElement("label", { className: "dshadb_label", key: "ml7" }, t("settings.currency")),
|
|
1126
|
-
react.createElement("select", { className: "dshadb_field", value: mform.currency || "CNY", onChange: (e) => setMform({ ...mform, currency: e.target.value }), key: "mi7" }, [
|
|
1723
|
+
react.createElement("select", { className: "dshadb_field dshadb_field_select", value: mform.currency || "CNY", onChange: (e) => setMform({ ...mform, currency: e.target.value }), key: "mi7" }, [
|
|
1127
1724
|
react.createElement("option", { value: "CNY", key: "cc1" }, t("cmn")),
|
|
1128
1725
|
react.createElement("option", { value: "USD", key: "cc2" }, t("usd")),
|
|
1129
1726
|
]),
|
|
@@ -1144,8 +1741,73 @@ window.__ModuleLoader__.load({
|
|
|
1144
1741
|
]) : null,
|
|
1145
1742
|
]);
|
|
1146
1743
|
|
|
1744
|
+
// v1.1.0: 大肥鱼页 —— 总开关 + 全部细项, 全部即改即存, 不依赖底部保存按钮
|
|
1745
|
+
const wfRow = (key, label, control, sub) => react.createElement("div", { className: "dshadb_wf_row", key }, [
|
|
1746
|
+
react.createElement("div", { key: "n", style: { minWidth: 0 } }, [
|
|
1747
|
+
react.createElement("span", { className: "dshadb_wf_name", key: "t" }, label),
|
|
1748
|
+
sub ? react.createElement("span", { className: "dshadb_wf_sub", key: "s" }, sub) : null,
|
|
1749
|
+
]),
|
|
1750
|
+
control,
|
|
1751
|
+
]);
|
|
1752
|
+
const wfSlider = (key, label, valText, min, max, step, value, onCommit) =>
|
|
1753
|
+
react.createElement("div", { className: "dshadb_wf_row_col", key }, [
|
|
1754
|
+
react.createElement("div", { className: "dshadb_wf_head", key: "h" }, [
|
|
1755
|
+
react.createElement("span", { className: "dshadb_wf_name", key: "n" }, label),
|
|
1756
|
+
react.createElement("span", { className: "dshadb_wf_val", key: "v" }, valText),
|
|
1757
|
+
]),
|
|
1758
|
+
react.createElement("input", {
|
|
1759
|
+
className: "dshadb_wf_range", type: "range", min, max, step, value, key: "r",
|
|
1760
|
+
onChange: (e) => onCommit(Number(e.target.value)),
|
|
1761
|
+
}),
|
|
1762
|
+
]);
|
|
1763
|
+
const wfSeg = (key, label, options, value, onPick) =>
|
|
1764
|
+
wfRow(key, label, react.createElement("div", { className: "dshadb_wf_seg", key: "seg" },
|
|
1765
|
+
options.map(([v, lb]) => react.createElement("button", {
|
|
1766
|
+
type: "button", key: String(v),
|
|
1767
|
+
className: "dshadb_wf_segbtn" + (value === v ? " dshadb_wf_segbtn_on" : ""),
|
|
1768
|
+
onClick: () => onPick(v),
|
|
1769
|
+
}, lb))));
|
|
1770
|
+
|
|
1771
|
+
const whaleSection = react.createElement("div", { className: "dshadb_settings_section", key: "s_whale" }, [
|
|
1772
|
+
// 总开关 (从基础设置迁移过来); 图标用鲸鱼本体缩略图, 不用 emoji
|
|
1773
|
+
react.createElement("div", { className: "dshadb_settings_row", key: "en", style: { margin: "2px 0 10px" } }, [
|
|
1774
|
+
react.createElement("img", {
|
|
1775
|
+
className: "dshadb_wf_avatar", src: "/api-dashboard/whale/image.png",
|
|
1776
|
+
alt: "", draggable: false, key: "av",
|
|
1777
|
+
}),
|
|
1778
|
+
react.createElement("div", { className: "dshadb_settings_row_main", key: "m" }, [
|
|
1779
|
+
react.createElement("span", { className: "dshadb_settings_row_title", key: "t" }, t("whale.enable")),
|
|
1780
|
+
react.createElement("span", { className: "dshadb_settings_row_sub", key: "s" }, t("whale.enableHint")),
|
|
1781
|
+
]),
|
|
1782
|
+
react.createElement(Switch, { checked: whaleOn, onChange: toggleWhale, key: "sw" }),
|
|
1783
|
+
]),
|
|
1784
|
+
// 细项: 未开启时置灰不可点
|
|
1785
|
+
react.createElement("div", { className: whaleOn ? "" : "dshadb_wf_disabled", key: "detail" }, [
|
|
1786
|
+
!whaleOn ? react.createElement("div", { className: "dshadb_wf_tip", key: "offtip", style: { marginBottom: "8px" } }, t("whale.offTip")) : null,
|
|
1787
|
+
wfSlider("scale", t("whale.scale"), Math.round(wf.scale * 100) + "%", 0.6, 2.5, 0.05, wf.scale,
|
|
1788
|
+
(v) => patchWf({ scale: v })),
|
|
1789
|
+
wfSlider("peek", t("whale.peek"), Math.round(wf.peekRatio * 100) + "%", 0.15, 0.9, 0.05, wf.peekRatio,
|
|
1790
|
+
(v) => patchWf({ peekRatio: v })),
|
|
1791
|
+
wfSeg("snap", t("whale.snap"), [[true, t("whale.on")], [false, t("whale.off")]], wf.snapOn,
|
|
1792
|
+
(v) => patchWf({ snapOn: v })),
|
|
1793
|
+
react.createElement("div", { className: "dshadb_wf_tip", key: "snaphint", style: { margin: "-2px 0 8px" } }, t("whale.snapHint")),
|
|
1794
|
+
wfSeg("bubble", t("whale.bubble"), [[true, t("whale.on")], [false, t("whale.off")]], wf.bubbleOn,
|
|
1795
|
+
(v) => patchWf({ bubbleOn: v })),
|
|
1796
|
+
wfSeg("peak", t("whale.peakMode"), [
|
|
1797
|
+
["default", t("whale.peak.default")], ["liangwen", t("whale.peak.liangwen")], ["qiangqiang", t("whale.peak.qiangqiang")],
|
|
1798
|
+
], wf.peakMode, (v) => patchWf({ peakMode: v })),
|
|
1799
|
+
wfSeg("sound", t("whale.sound"), [[true, t("whale.on")], [false, t("whale.off")]], wf.soundOn,
|
|
1800
|
+
(v) => patchWf({ soundOn: v })),
|
|
1801
|
+
wfSeg("soundset", t("whale.soundSet"), [["duck", t("whale.duck")], ["fx1", t("whale.fx1")]], wf.soundSet,
|
|
1802
|
+
(v) => patchWf({ soundSet: v })),
|
|
1803
|
+
wfSlider("vol", t("whale.volume"), Math.round(wf.volume * 100) + "%", 0, 1, 0.05, wf.volume,
|
|
1804
|
+
(v) => patchWf({ volume: v })),
|
|
1805
|
+
react.createElement("div", { className: "dshadb_wf_tip", key: "tip" }, t("whale.tip")),
|
|
1806
|
+
]),
|
|
1807
|
+
]);
|
|
1808
|
+
|
|
1147
1809
|
const output = react.createElement("div", { className: "dshadb_drawer", style: { maxHeight: "86vh" }, onClick: (e) => e.stopPropagation(), key: "drawer" }, [
|
|
1148
|
-
react.createElement(
|
|
1810
|
+
react.createElement(SwipeHandle, { onClose: onBack || onClose, key: "handle" }),
|
|
1149
1811
|
react.createElement("div", { className: "dshadb_header", key: "header" }, [
|
|
1150
1812
|
react.createElement("div", { style: { display: "flex", alignItems: "center", gap: "10px", flex: "1", minWidth: 0 }, key: "leftwrap" }, [
|
|
1151
1813
|
onBack ? react.createElement("button", { type: "button", className: "dshadb_header_back", onClick: onBack, key: "back", title: t("back") }, react.createElement(IconBack, null)) : null,
|
|
@@ -1157,8 +1819,9 @@ window.__ModuleLoader__.load({
|
|
|
1157
1819
|
]),
|
|
1158
1820
|
react.createElement("div", { className: "dshadb_body", key: "body" }, [
|
|
1159
1821
|
tabsNode,
|
|
1160
|
-
section === "basic" ? basicSection : section === "relays" ? relaySection : modelSection,
|
|
1161
|
-
|
|
1822
|
+
section === "basic" ? basicSection : section === "relays" ? relaySection : section === "whale" ? whaleSection : modelSection,
|
|
1823
|
+
// 大肥鱼页全部即改即存, 不需要保存按钮
|
|
1824
|
+
section === "whale" ? null : react.createElement("button", { type: "button", className: "dshadb_add_btn", onClick: save, disabled: saving, key: "save", style: { width: "100%", borderStyle: "solid", background: "#17181c", color: "#fff", borderColor: "#17181c" } }, saving ? t("refreshing") : t("settings.save")),
|
|
1162
1825
|
// v0.6.0: 版本与更新区块
|
|
1163
1826
|
react.createElement("div", { key: "upd", style: { marginTop: "12px", paddingTop: "10px", borderTop: "1px solid #e7e8ec" } }, [
|
|
1164
1827
|
react.createElement("div", { style: { fontSize: "11px", fontWeight: "700", color: "#777b84", marginBottom: "8px" }, key: "upd_t" }, t("update.title")),
|
|
@@ -1173,12 +1836,570 @@ window.__ModuleLoader__.load({
|
|
|
1173
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" } },
|
|
1174
1837
|
upd.phase === "installing" ? t("update.installing") : t("update.install") + " v" + upd.info.remote) : null,
|
|
1175
1838
|
]),
|
|
1176
|
-
|
|
1177
|
-
|
|
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),
|
|
1178
1852
|
]),
|
|
1179
1853
|
]),
|
|
1180
1854
|
]);
|
|
1181
|
-
return react.createElement("div", { className: "dshadb_scrim", onClick: (e) => { if (e.target === e.currentTarget) onClose(); } },
|
|
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
|
+
]);
|
|
1859
|
+
}
|
|
1860
|
+
//#endregion
|
|
1861
|
+
|
|
1862
|
+
//#region 大肥鱼互动挂件 (v1.1.0)
|
|
1863
|
+
// 移植自 MeteorNOX/DeepSeek-Balance-Whale-Widget (MIT License, Copyright (c) 2026 MeteorNOX)
|
|
1864
|
+
// 保留原版全部互动能力: 拖拽 / 四边吸附 / 左吸附镜像 / 按压Q弹 / 音效 / 随机台词 / 汉堡菜单;
|
|
1865
|
+
// 去掉的只有余额与消耗显示 (那部分由输入框下方的看板负责)。
|
|
1866
|
+
function Switch({ checked, onChange }) {
|
|
1867
|
+
return react.createElement("button", {
|
|
1868
|
+
type: "button", role: "switch", "aria-checked": !!checked,
|
|
1869
|
+
className: "dshadb_switch" + (checked ? " dshadb_switch_on" : ""),
|
|
1870
|
+
onClick: () => onChange(!checked), style: { marginLeft: "auto" },
|
|
1871
|
+
}, react.createElement("span", { className: "dshadb_switch_knob" }));
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
var WHALE_DEFAULTS = {
|
|
1875
|
+
scale: 1, soundOn: true, soundSet: "duck", volume: 0.5, bubbleOn: true,
|
|
1876
|
+
peakMode: "default", snapOn: true, peekRatio: 0.5, left: null, top: null, side: "right",
|
|
1877
|
+
};
|
|
1878
|
+
var whaleCtxInfo = { isPeak: false };
|
|
1879
|
+
function updateWhaleContext(info) {
|
|
1880
|
+
whaleCtxInfo = { isPeak: !!(info && info.isPeak) };
|
|
1881
|
+
}
|
|
1882
|
+
function whalePeakWords(peakMode, isPeak) {
|
|
1883
|
+
if (peakMode === "liangwen") return isPeak ? "梁文峰" : "梁文谷";
|
|
1884
|
+
if (peakMode === "qiangqiang") return isPeak ? "!?峰峰?!" : "!?谷谷?!";
|
|
1885
|
+
return isPeak ? "高峰时段" : "空闲时段";
|
|
1886
|
+
}
|
|
1887
|
+
function whalePickOne(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
|
|
1888
|
+
// 台词组: [权重, 生成函数] —— 生成 {gif:true} 或 [{t,s,c}] 三行 (s: A小字 / B大字 / P峰谷 / C灰注)
|
|
1889
|
+
var WHALE_GROUPS = [
|
|
1890
|
+
[40, function (st) {
|
|
1891
|
+
var isPeak = !!whaleCtxInfo.isPeak;
|
|
1892
|
+
return [
|
|
1893
|
+
{ t: "当前时间段为:", s: "A" },
|
|
1894
|
+
{ t: whalePeakWords(st.peakMode, isPeak), s: "P", c: isPeak ? "#e0433f" : "#2fa24c" },
|
|
1895
|
+
{ t: isPeak ? "09:00~12:00 / 14:00~18:00" : "其余时段 / 周末全天", s: "C" },
|
|
1896
|
+
];
|
|
1897
|
+
}],
|
|
1898
|
+
[10, function () { return { gif: true }; }],
|
|
1899
|
+
[8, function () { return [null, { t: whalePickOne(["好模型... ↓", "好女孩...↓"]), s: "B" }, null]; }],
|
|
1900
|
+
[8, function () {
|
|
1901
|
+
return [null, { t: whalePickOne([
|
|
1902
|
+
"不知道用户有什么用,先赶走吧~", "我...我...我也要挣钱吗?", "我去吃饭啦,测完叫我",
|
|
1903
|
+
"压力一只蓝色大肥鱼?!", "DeepSleep...", "坏了...用户彻底怒了!",
|
|
1904
|
+
"摸头摸头~ 再摸摸嘛", "尾巴被你拖到墙角啦", "偷看你打代码半天啦",
|
|
1905
|
+
]), s: "A", w: true }, null]; }],
|
|
1906
|
+
[4, function () {
|
|
1907
|
+
return [null, { t: whalePickOne([
|
|
1908
|
+
"你目录里的dsh是什么...大烧货吗...?", "恭喜你实现token自由!token全跑了!", "真当我是便宜货啊...",
|
|
1909
|
+
]), s: "A", w: true }, null]; }],
|
|
1910
|
+
[2, function () { return [{ t: "这个", s: "A" }, { t: "凶", s: "B" }, { t: "是什么意思呀...", s: "A" }]; }],
|
|
1911
|
+
[1, function () { return [null, { t: "哦鲸鲸... ", s: "B" }, null]; }],
|
|
1912
|
+
];
|
|
1913
|
+
function whalePickLines(st) {
|
|
1914
|
+
var total = 0, i;
|
|
1915
|
+
for (i = 0; i < WHALE_GROUPS.length; i++) total += WHALE_GROUPS[i][0];
|
|
1916
|
+
var r = Math.random() * total;
|
|
1917
|
+
for (i = 0; i < WHALE_GROUPS.length; i++) { r -= WHALE_GROUPS[i][0]; if (r < 0) return WHALE_GROUPS[i][1](st); }
|
|
1918
|
+
return WHALE_GROUPS[0][1](st);
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
var whaleWidget = null;
|
|
1922
|
+
var whaleRefs = 0;
|
|
1923
|
+
// 最近一次已知的大肥鱼设置缓存 — 会话切换重建时直接用它初始化, 避免先从默认(右侧)闪一下再跳回用户位置
|
|
1924
|
+
var lastWhaleSettings = null;
|
|
1925
|
+
// 停止互动后多久缩回 / 气泡存活时长 (用户要求: 3 秒)
|
|
1926
|
+
var WHALE_IDLE_MS = 3000;
|
|
1927
|
+
// 左右「边缘带」占屏宽比例: 松手时中心落在带内才吸附缩回, 中间区域原地不动
|
|
1928
|
+
var WHALE_EDGE_ZONE = 0.22;
|
|
1929
|
+
// 贴边时横向要拉开多少 (占鲸鱼宽比例) 才脱离边轨、转为自由拖拽并展开
|
|
1930
|
+
var WHALE_RAIL_BREAK = 0.42;
|
|
1931
|
+
// 连点节流: 间隔小于此值只续命不换台词 (换词会重建气泡, 连点时会闪没)
|
|
1932
|
+
var WHALE_LINE_MIN_MS = 420;
|
|
1933
|
+
function ensureWhaleWidget() {
|
|
1934
|
+
if (whaleWidget) return;
|
|
1935
|
+
if (typeof document === "undefined") return;
|
|
1936
|
+
var st = {};
|
|
1937
|
+
var disposed = false;
|
|
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
|
+
}
|
|
1947
|
+
|
|
1948
|
+
// ---------- DOM ----------
|
|
1949
|
+
var root = document.createElement("div");
|
|
1950
|
+
root.className = "dshadb-whale";
|
|
1951
|
+
var sprite = document.createElement("div");
|
|
1952
|
+
sprite.className = "dshadb-whale-sprite";
|
|
1953
|
+
var body = document.createElement("div");
|
|
1954
|
+
body.className = "dshadb-whale-body";
|
|
1955
|
+
var img = document.createElement("img");
|
|
1956
|
+
img.className = "dshadb-whale-img";
|
|
1957
|
+
img.src = "/api-dashboard/whale/image.png";
|
|
1958
|
+
img.alt = "大肥鱼"; img.draggable = false;
|
|
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);
|
|
1969
|
+
sprite.appendChild(body);
|
|
1970
|
+
var bubble = document.createElement("div");
|
|
1971
|
+
bubble.className = "dshadb-whale-bubble";
|
|
1972
|
+
var bText = document.createElement("div");
|
|
1973
|
+
bText.className = "dshadb-whale-btext";
|
|
1974
|
+
var gif = document.createElement("img");
|
|
1975
|
+
gif.className = "dshadb-whale-gif";
|
|
1976
|
+
gif.src = "/api-dashboard/whale/rua.gif";
|
|
1977
|
+
gif.alt = ""; gif.draggable = false;
|
|
1978
|
+
var gifBroken = false;
|
|
1979
|
+
gif.addEventListener("error", function () { gifBroken = true; });
|
|
1980
|
+
bubble.appendChild(gif);
|
|
1981
|
+
bubble.appendChild(bText);
|
|
1982
|
+
root.appendChild(bubble);
|
|
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";
|
|
1989
|
+
document.body.appendChild(root);
|
|
1990
|
+
// ---------- 音效 ----------
|
|
1991
|
+
var audio = { press: null, release: null };
|
|
1992
|
+
function reloadAudio() {
|
|
1993
|
+
audio.press = new Audio("/api-dashboard/whale/sound/press.mp3?set=" + st.soundSet);
|
|
1994
|
+
audio.release = new Audio("/api-dashboard/whale/sound/release.mp3?set=" + st.soundSet);
|
|
1995
|
+
audio.press.volume = st.volume; audio.release.volume = st.volume;
|
|
1996
|
+
}
|
|
1997
|
+
reloadAudio();
|
|
1998
|
+
function playSound(which) {
|
|
1999
|
+
if (!st.soundOn || st.volume <= 0) return;
|
|
2000
|
+
var a = audio[which]; if (!a) return;
|
|
2001
|
+
try { a.volume = st.volume; a.currentTime = 0; var p = a.play(); if (p && p.catch) p.catch(function () {}); } catch (e) {}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// ---------- 状态 ----------
|
|
2005
|
+
var full = false; // 是否整只露出
|
|
2006
|
+
var bubbleOpen = false;
|
|
2007
|
+
var hideTimer = null, bubbleTimer = null, saveTimer = null;
|
|
2008
|
+
var drag = null;
|
|
2009
|
+
var lastLineAt = 0; // 上次换台词的时刻 (连点节流用)
|
|
2010
|
+
|
|
2011
|
+
function sizePx() { return root.getBoundingClientRect().width || 120; }
|
|
2012
|
+
function vw() { return window.innerWidth || document.documentElement.clientWidth; }
|
|
2013
|
+
function vh() { return window.innerHeight || document.documentElement.clientHeight; }
|
|
2014
|
+
|
|
2015
|
+
function applyScale() { root.style.setProperty("--dshw-scale", String(st.scale)); }
|
|
2016
|
+
|
|
2017
|
+
/** 把 state 的位置写进 left/top; 探头位移交给 transform, 所以这里一律夹在屏幕内 */
|
|
2018
|
+
function applyPos() {
|
|
2019
|
+
var s = sizePx();
|
|
2020
|
+
if (st.left === null || st.top === null) {
|
|
2021
|
+
st.left = Math.max(0, vw() - s);
|
|
2022
|
+
st.top = Math.round(vh() * 0.62);
|
|
2023
|
+
}
|
|
2024
|
+
st.left = Math.min(Math.max(st.left, 0), Math.max(0, vw() - s));
|
|
2025
|
+
st.top = Math.min(Math.max(st.top, 0), Math.max(0, vh() - s));
|
|
2026
|
+
root.style.left = st.left + "px";
|
|
2027
|
+
root.style.top = st.top + "px";
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
/** 探头位移: 只有吸在左右两侧时才把身子推出屏幕; 中间自由摆放时整只显示 */
|
|
2031
|
+
function applyPeek() {
|
|
2032
|
+
if (full || !st.side) { root.style.transform = "translate(0px, 0px)"; return; }
|
|
2033
|
+
var s = sizePx();
|
|
2034
|
+
var out = Math.round(s * (1 - st.peekRatio));
|
|
2035
|
+
var dx = st.side === "left" ? -out : st.side === "right" ? out : 0;
|
|
2036
|
+
root.style.transform = "translate(" + dx + "px, 0px)";
|
|
2037
|
+
}
|
|
2038
|
+
function applyFlip() {
|
|
2039
|
+
if (st.side === "left") root.classList.add("dshadb-whale-flip");
|
|
2040
|
+
else root.classList.remove("dshadb-whale-flip");
|
|
2041
|
+
}
|
|
2042
|
+
function applyAll() { applyScale(); applyPos(); applyFlip(); applyPeek(); }
|
|
2043
|
+
|
|
2044
|
+
// ---------- 持久化 ----------
|
|
2045
|
+
function saveSoon() {
|
|
2046
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
2047
|
+
saveTimer = setTimeout(function () {
|
|
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
|
+
};
|
|
2054
|
+
try {
|
|
2055
|
+
fetch("/api-dashboard/whale/settings", {
|
|
2056
|
+
method: "PUT", headers: { "Content-Type": "application/json" },
|
|
2057
|
+
body: JSON.stringify(lastWhaleSettings),
|
|
2058
|
+
}).catch(function () {});
|
|
2059
|
+
} catch (e) {}
|
|
2060
|
+
}, 400);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
// ---------- 气泡 ----------
|
|
2064
|
+
var STYLE_CLASS = { A: "dshadb-whale-l-a", B: "dshadb-whale-l-b", P: "dshadb-whale-l-p", C: "dshadb-whale-l-c" };
|
|
2065
|
+
function renderLines(lines) {
|
|
2066
|
+
bText.textContent = "";
|
|
2067
|
+
if (lines && lines.gif && !gifBroken) { gif.style.display = "block"; bText.style.display = "none"; return; }
|
|
2068
|
+
gif.style.display = "none"; bText.style.display = "block";
|
|
2069
|
+
var arr = (lines && lines.gif) ? [null, { t: "动图跑掉了...", s: "A", w: true }, null] : lines;
|
|
2070
|
+
for (var i = 0; i < arr.length; i++) {
|
|
2071
|
+
var ln = arr[i]; if (!ln) continue;
|
|
2072
|
+
var el = document.createElement("div");
|
|
2073
|
+
el.className = STYLE_CLASS[ln.s] || STYLE_CLASS.A;
|
|
2074
|
+
if (ln.w) el.className += " dshadb-whale-wrap";
|
|
2075
|
+
if (ln.c) el.style.color = ln.c;
|
|
2076
|
+
el.textContent = ln.t;
|
|
2077
|
+
bText.appendChild(el);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
/**
|
|
2081
|
+
* 鲸鱼的「目标」位置 —— 不用 getBoundingClientRect。
|
|
2082
|
+
* 探头/展开是 0.45s transform 过渡, 点击瞬间读实时 rect 拿到的是过渡中的中间态,
|
|
2083
|
+
* 气泡会按旧位置算避让, 连点时越算越偏, 最后整个飘到屏幕外 = 看着"不显示"。
|
|
2084
|
+
*/
|
|
2085
|
+
function targetRect() {
|
|
2086
|
+
var s = sizePx();
|
|
2087
|
+
var dx = 0;
|
|
2088
|
+
if (!full && st.side) {
|
|
2089
|
+
var out = Math.round(s * (1 - st.peekRatio));
|
|
2090
|
+
dx = st.side === "left" ? -out : st.side === "right" ? out : 0;
|
|
2091
|
+
}
|
|
2092
|
+
return { left: st.left + dx, top: st.top, size: s };
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* 把气泡摆到不出屏的位置:
|
|
2096
|
+
* 1) 水平方向整体平移, 保证左右都留 8px 边距 (贴边时不会有一半在屏幕外)
|
|
2097
|
+
* 2) 尾巴单独定位, 仍然指向鲸鱼中心
|
|
2098
|
+
* 3) 上方空间不够时翻到鲸鱼下方
|
|
2099
|
+
*/
|
|
2100
|
+
function placeBubble() {
|
|
2101
|
+
var margin = 8;
|
|
2102
|
+
bubble.classList.remove("dshadb-whale-bubble-below");
|
|
2103
|
+
bubble.style.setProperty("--dshw-bdx", "0px");
|
|
2104
|
+
bubble.style.setProperty("--dshw-btx", "50%");
|
|
2105
|
+
var r = targetRect();
|
|
2106
|
+
var bw = bubble.offsetWidth, bh = bubble.offsetHeight;
|
|
2107
|
+
if (!bw) return;
|
|
2108
|
+
var cx = r.left + r.size / 2;
|
|
2109
|
+
var want = cx - bw / 2;
|
|
2110
|
+
var shift = 0;
|
|
2111
|
+
if (want < margin) shift = margin - want;
|
|
2112
|
+
else if (want + bw > vw() - margin) shift = (vw() - margin) - (want + bw);
|
|
2113
|
+
bubble.style.setProperty("--dshw-bdx", Math.round(shift) + "px");
|
|
2114
|
+
var tail = bw / 2 - shift;
|
|
2115
|
+
tail = Math.min(Math.max(tail, 18), bw - 18);
|
|
2116
|
+
bubble.style.setProperty("--dshw-btx", Math.round(tail) + "px");
|
|
2117
|
+
if (r.top - bh - 6 < margin && (vh() - r.top - r.size - bh - 6 >= margin)) bubble.classList.add("dshadb-whale-bubble-below");
|
|
2118
|
+
}
|
|
2119
|
+
function openBubble() {
|
|
2120
|
+
if (!st.bubbleOn) return;
|
|
2121
|
+
renderLines(whalePickLines(st));
|
|
2122
|
+
bubble.classList.remove("dshadb-whale-bubble-keep");
|
|
2123
|
+
bubble.classList.add("dshadb-whale-bubble-on");
|
|
2124
|
+
bubbleOpen = true;
|
|
2125
|
+
placeBubble();
|
|
2126
|
+
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
2127
|
+
bubbleTimer = setTimeout(closeBubble, WHALE_IDLE_MS);
|
|
2128
|
+
}
|
|
2129
|
+
/**
|
|
2130
|
+
* 连点续命: 保持当前台词不变, 只把气泡留住并重新计时。
|
|
2131
|
+
* 连点时不重建内容 —— 重建会让气泡宽度反复跳变、重新测量, 视觉上就是闪一下没了。
|
|
2132
|
+
*/
|
|
2133
|
+
function keepBubble() {
|
|
2134
|
+
if (!st.bubbleOn) return;
|
|
2135
|
+
if (!bubbleOpen) { openBubble(); return; }
|
|
2136
|
+
bubble.classList.add("dshadb-whale-bubble-on");
|
|
2137
|
+
bubble.classList.add("dshadb-whale-bubble-keep");
|
|
2138
|
+
placeBubble();
|
|
2139
|
+
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
2140
|
+
bubbleTimer = setTimeout(closeBubble, WHALE_IDLE_MS);
|
|
2141
|
+
}
|
|
2142
|
+
function closeBubble() {
|
|
2143
|
+
bubble.classList.remove("dshadb-whale-bubble-on");
|
|
2144
|
+
bubble.classList.remove("dshadb-whale-bubble-keep");
|
|
2145
|
+
bubbleOpen = false;
|
|
2146
|
+
if (bubbleTimer) { clearTimeout(bubbleTimer); bubbleTimer = null; }
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
// ---------- 探头 / 全身 ----------
|
|
2150
|
+
function toFull() {
|
|
2151
|
+
full = true; applyPeek();
|
|
2152
|
+
lastLineAt = Date.now();
|
|
2153
|
+
openBubble();
|
|
2154
|
+
armHide();
|
|
2155
|
+
}
|
|
2156
|
+
function toPeek() {
|
|
2157
|
+
full = false; applyPeek();
|
|
2158
|
+
closeBubble();
|
|
2159
|
+
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
|
|
2160
|
+
}
|
|
2161
|
+
/** 停手 WHALE_IDLE_MS 后自动缩回; 每次互动都重新计时 */
|
|
2162
|
+
function armHide() {
|
|
2163
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
2164
|
+
hideTimer = setTimeout(function () { hideTimer = null; if (full) toPeek(); }, WHALE_IDLE_MS);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// ---------- 吸附 ----------
|
|
2168
|
+
/**
|
|
2169
|
+
* 松手后的落位: 只有拖到左右两侧的「边缘带」里才吸附 + 探头,
|
|
2170
|
+
* 停在屏幕中间就整只留在原地 (side="" → applyPeek 不做位移)。
|
|
2171
|
+
*/
|
|
2172
|
+
function settle() {
|
|
2173
|
+
var s = sizePx();
|
|
2174
|
+
var W = vw(), H = vh();
|
|
2175
|
+
if (!st.snapOn) {
|
|
2176
|
+
st.side = "";
|
|
2177
|
+
applyPos(); applyFlip(); applyPeek(); saveSoon();
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
var cx = st.left + s / 2;
|
|
2181
|
+
var zone = Math.min(Math.max(s * 0.75, W * WHALE_EDGE_ZONE), Math.max(1, W / 2 - 1)); // 边缘带宽度, 不超过半屏
|
|
2182
|
+
if (cx <= zone) { st.side = "left"; st.left = 0; }
|
|
2183
|
+
else if (cx >= W - zone) { st.side = "right"; st.left = W - s; }
|
|
2184
|
+
else { st.side = ""; } // 屏幕中间: 不缩
|
|
2185
|
+
// 纵向永远不缩, 只是别跑出屏幕
|
|
2186
|
+
st.top = Math.min(Math.max(st.top, 0), Math.max(0, H - s));
|
|
2187
|
+
applyPos(); applyFlip(); applyPeek(); saveSoon();
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
// ---------- 拖拽 ----------
|
|
2191
|
+
function onDown(e) {
|
|
2192
|
+
// v1.4.0: 面板开着时挂件已被 setLocked 关掉 pointer-events, 这里再兜一道
|
|
2193
|
+
if (root.classList.contains("dshadb-whale-locked")) return;
|
|
2194
|
+
if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; }
|
|
2195
|
+
drag = {
|
|
2196
|
+
id: e.pointerId, sx: e.clientX, sy: e.clientY,
|
|
2197
|
+
ox: st.left, oy: st.top, moved: false,
|
|
2198
|
+
// 起手时贴着哪一侧: 贴边状态下的纵向滑动算「沿边轨道滑」, 不展开
|
|
2199
|
+
rail: full ? "" : st.side, freed: false,
|
|
2200
|
+
};
|
|
2201
|
+
root.classList.add("dshadb-whale-dragging");
|
|
2202
|
+
root.classList.add("dshadb-whale-press");
|
|
2203
|
+
try { body.setPointerCapture(e.pointerId); } catch (err) {}
|
|
2204
|
+
playSound("press");
|
|
2205
|
+
}
|
|
2206
|
+
function onMove(e) {
|
|
2207
|
+
if (!drag || e.pointerId !== drag.id) return;
|
|
2208
|
+
var dx = e.clientX - drag.sx, dy = e.clientY - drag.sy;
|
|
2209
|
+
if (!drag.moved && dx * dx + dy * dy < 9) return;
|
|
2210
|
+
drag.moved = true;
|
|
2211
|
+
// 沿边纵向滑动: 横向没拉开就保持探头姿态, 只上下移动
|
|
2212
|
+
if (drag.rail && !drag.freed) {
|
|
2213
|
+
var breakOut = Math.abs(dx) > Math.max(28, sizePx() * WHALE_RAIL_BREAK);
|
|
2214
|
+
if (!breakOut) {
|
|
2215
|
+
st.top = drag.oy + dy;
|
|
2216
|
+
applyPos(); // 夹在屏幕内并写回 top
|
|
2217
|
+
applyPeek(); // 维持探头位移, 不展开
|
|
2218
|
+
e.preventDefault();
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
drag.freed = true; // 横向拉开了 → 转为自由拖拽
|
|
2222
|
+
}
|
|
2223
|
+
if (!full) {
|
|
2224
|
+
full = true; // 拖动时整只跟手, 不再半隐
|
|
2225
|
+
root.style.transform = "translate(0px, 0px)";
|
|
2226
|
+
closeBubble();
|
|
2227
|
+
}
|
|
2228
|
+
st.left = drag.ox + dx; st.top = drag.oy + dy;
|
|
2229
|
+
root.style.left = st.left + "px";
|
|
2230
|
+
root.style.top = st.top + "px";
|
|
2231
|
+
e.preventDefault();
|
|
2232
|
+
}
|
|
2233
|
+
function onUp(e) {
|
|
2234
|
+
if (!drag || e.pointerId !== drag.id) return;
|
|
2235
|
+
var moved = drag.moved;
|
|
2236
|
+
var railed = !!drag.rail && !drag.freed;
|
|
2237
|
+
drag = null;
|
|
2238
|
+
root.classList.remove("dshadb-whale-dragging");
|
|
2239
|
+
root.classList.remove("dshadb-whale-press");
|
|
2240
|
+
try { body.releasePointerCapture(e.pointerId); } catch (err) {}
|
|
2241
|
+
if (moved) {
|
|
2242
|
+
// 沿边滑完仍是探头状态: 只记住新高度, 不吸附动画也不展开
|
|
2243
|
+
if (railed) { saveSoon(); return; }
|
|
2244
|
+
settle(); armHide(); return;
|
|
2245
|
+
}
|
|
2246
|
+
// 没移动 = 点击: 弹全身 / 换台词, 但绝不立刻收回 —— 一律等停手 3 秒
|
|
2247
|
+
playSound("release");
|
|
2248
|
+
if (!full) { toFull(); return; }
|
|
2249
|
+
// 连点节流: 间隔太短只续命不换词, 避免气泡反复重建导致"看不到"
|
|
2250
|
+
var now = Date.now();
|
|
2251
|
+
if (now - lastLineAt < WHALE_LINE_MIN_MS) { keepBubble(); armHide(); return; }
|
|
2252
|
+
lastLineAt = now;
|
|
2253
|
+
openBubble();
|
|
2254
|
+
armHide();
|
|
2255
|
+
}
|
|
2256
|
+
body.addEventListener("pointerdown", onDown);
|
|
2257
|
+
body.addEventListener("pointermove", onMove);
|
|
2258
|
+
body.addEventListener("pointerup", onUp);
|
|
2259
|
+
body.addEventListener("pointercancel", onUp);
|
|
2260
|
+
|
|
2261
|
+
// ---------- 外部设置接口 ----------
|
|
2262
|
+
// 设置面板「大肥鱼」页改动后调用: 立刻应用到当前挂件并落盘
|
|
2263
|
+
function applyPatch(patch) {
|
|
2264
|
+
var soundTouched = false;
|
|
2265
|
+
for (var key in patch) {
|
|
2266
|
+
if (!Object.prototype.hasOwnProperty.call(patch, key)) continue;
|
|
2267
|
+
if (patch[key] === undefined) continue;
|
|
2268
|
+
st[key] = patch[key];
|
|
2269
|
+
if (key === "soundSet") soundTouched = true;
|
|
2270
|
+
if (key === "volume" || key === "soundOn") soundTouched = true;
|
|
2271
|
+
}
|
|
2272
|
+
if (soundTouched) {
|
|
2273
|
+
if (patch.soundSet !== undefined) reloadAudio();
|
|
2274
|
+
if (audio.press) audio.press.volume = st.volume;
|
|
2275
|
+
if (audio.release) audio.release.volume = st.volume;
|
|
2276
|
+
}
|
|
2277
|
+
if (patch.bubbleOn === false) closeBubble();
|
|
2278
|
+
if (patch.snapOn !== undefined) {
|
|
2279
|
+
if (st.snapOn) { settle(); return; }
|
|
2280
|
+
st.side = "";
|
|
2281
|
+
}
|
|
2282
|
+
applyAll();
|
|
2283
|
+
if (bubbleOpen) placeBubble();
|
|
2284
|
+
saveSoon();
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// ---------- 全局监听 ----------
|
|
2288
|
+
var onDocDown = function (e) {
|
|
2289
|
+
if (root.contains(e.target)) return;
|
|
2290
|
+
if (full && !drag) toPeek();
|
|
2291
|
+
};
|
|
2292
|
+
document.addEventListener("pointerdown", onDocDown);
|
|
2293
|
+
var lastVw = vw();
|
|
2294
|
+
var onResize = function () {
|
|
2295
|
+
var nowVw = vw();
|
|
2296
|
+
applyPos();
|
|
2297
|
+
// 键盘弹出/收起只改视口高度, 不应触发水平吸附(否则鲸鱼会被吸到边上去);
|
|
2298
|
+
// 只有视口宽度真正变化(横竖屏/分屏)才重新 settle
|
|
2299
|
+
if (st.snapOn && st.side && Math.abs(nowVw - lastVw) > 4) settle(); else applyPeek();
|
|
2300
|
+
lastVw = nowVw;
|
|
2301
|
+
if (bubbleOpen) placeBubble();
|
|
2302
|
+
};
|
|
2303
|
+
window.addEventListener("resize", onResize);
|
|
2304
|
+
var onVis = function () {
|
|
2305
|
+
if (document.hidden) { if (hideTimer) { clearTimeout(hideTimer); hideTimer = null; } }
|
|
2306
|
+
else if (full) armHide();
|
|
2307
|
+
};
|
|
2308
|
+
document.addEventListener("visibilitychange", onVis);
|
|
2309
|
+
|
|
2310
|
+
// v1.4.0: 挂件是在面板可能已经开着的时候才挂上来的(用户在设置里刚打开大肥鱼) → 立刻套用锁定态
|
|
2311
|
+
if (whaleLocked) root.classList.add("dshadb-whale-locked");
|
|
2312
|
+
whaleWidget = {
|
|
2313
|
+
applyPatch: applyPatch,
|
|
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");
|
|
2325
|
+
},
|
|
2326
|
+
dispose: function () {
|
|
2327
|
+
disposed = true;
|
|
2328
|
+
body.removeEventListener("pointerdown", onDown);
|
|
2329
|
+
body.removeEventListener("pointermove", onMove);
|
|
2330
|
+
body.removeEventListener("pointerup", onUp);
|
|
2331
|
+
body.removeEventListener("pointercancel", onUp);
|
|
2332
|
+
document.removeEventListener("pointerdown", onDocDown);
|
|
2333
|
+
document.removeEventListener("visibilitychange", onVis);
|
|
2334
|
+
window.removeEventListener("resize", onResize);
|
|
2335
|
+
if (hideTimer) clearTimeout(hideTimer);
|
|
2336
|
+
if (bubbleTimer) clearTimeout(bubbleTimer);
|
|
2337
|
+
if (saveTimer) clearTimeout(saveTimer);
|
|
2338
|
+
if (revealTimer) clearTimeout(revealTimer); // v1.4.1: 卸载后别再回调
|
|
2339
|
+
if (root.parentNode) root.parentNode.removeChild(root);
|
|
2340
|
+
whaleWidget = null;
|
|
2341
|
+
},
|
|
2342
|
+
};
|
|
2343
|
+
|
|
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); // 兜底: 请求挂住也不能让鱼永远不出现
|
|
2364
|
+
applyAll();
|
|
2365
|
+
fetch("/api-dashboard/whale/settings", { cache: "no-store" })
|
|
2366
|
+
.then(function (r) { return r.json(); })
|
|
2367
|
+
.then(function (d) {
|
|
2368
|
+
if (disposed) return;
|
|
2369
|
+
if (!d || !d.ok || !d.settings) return;
|
|
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;
|
|
2378
|
+
}
|
|
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
|
+
};
|
|
2384
|
+
applyAll();
|
|
2385
|
+
})
|
|
2386
|
+
.catch(function () { /* 网络异常静默, 由 2s 兜底露出 */ })
|
|
2387
|
+
.then(reveal); // 成功 / 失败 / 上面提前 return 都会走到这里
|
|
2388
|
+
}
|
|
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(); }
|
|
2393
|
+
/** 设置面板改动后推给活着的挂件 (未挂载时静默忽略, 值已由端点落盘) */
|
|
2394
|
+
function patchWhaleWidget(patch) {
|
|
2395
|
+
if (whaleWidget && whaleWidget.applyPatch) whaleWidget.applyPatch(patch);
|
|
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);
|
|
1182
2403
|
}
|
|
1183
2404
|
//#endregion
|
|
1184
2405
|
|
|
@@ -1220,6 +2441,26 @@ window.__ModuleLoader__.load({
|
|
|
1220
2441
|
const dashData = react.useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1221
2442
|
const config = dashData.config;
|
|
1222
2443
|
|
|
2444
|
+
// v1.1.0: 大肥鱼互动挂件 — 设置里开启「收养大肥鱼」后挂载/卸载 (纯互动, 不含余额)
|
|
2445
|
+
const whaleOn = !!(config && config.whaleEnabled);
|
|
2446
|
+
react.useEffect(() => {
|
|
2447
|
+
if (!whaleOn) { releaseWhaleWidget(); return; }
|
|
2448
|
+
acquireWhaleWidget();
|
|
2449
|
+
return releaseWhaleWidget;
|
|
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]);
|
|
2459
|
+
// 峰谷时段变化同步给挂件 (台词里的「当前时间段」用它, 不涉及任何金额)
|
|
2460
|
+
react.useEffect(() => {
|
|
2461
|
+
updateWhaleContext({ isPeak: !!(config && config.isPeak) });
|
|
2462
|
+
}, [config && config.isPeak]);
|
|
2463
|
+
|
|
1223
2464
|
const handleSelect = (id) => { setSelected(id); setSelectedId(id); };
|
|
1224
2465
|
|
|
1225
2466
|
// v0.5.15: 按模型名切到对应平台的核心逻辑 (两条信号共用)
|
|
@@ -1240,7 +2481,11 @@ window.__ModuleLoader__.load({
|
|
|
1240
2481
|
// v0.5.15: 即时信号 — 订阅模型目录 store, 选中模型瞬间就切平台 (不再等投影/请求)
|
|
1241
2482
|
const modelDirSub = modelStore && typeof modelStore.subscribe === "function" ? modelStore.subscribe : () => () => {};
|
|
1242
2483
|
const modelDirSnap = modelStore && typeof modelStore.getSnapshot === "function" ? modelStore.getSnapshot : () => void 0;
|
|
1243
|
-
const
|
|
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) || "";
|
|
1244
2489
|
const [didModelDir, setDidModelDir] = react.useState(false);
|
|
1245
2490
|
react.useEffect(() => {
|
|
1246
2491
|
const m = modelDirModel;
|
|
@@ -1278,32 +2523,37 @@ window.__ModuleLoader__.load({
|
|
|
1278
2523
|
// 本会话消耗节点 (无条件渲染, waiting=true 显示 ~— 等待数据, 否则精确到6位)
|
|
1279
2524
|
let costText = "~—"
|
|
1280
2525
|
let costTitle = t("detail.sessionCost") + " — 等待数据"
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const fixed = amount >= 1 ? 2 : amount >= 0.01 ? 3 : amount >= 0.001 ? 4 : amount >= 0.0001 ? 5 : 6
|
|
1286
|
-
costText = "~" + sym + amount.toFixed(fixed)
|
|
1287
|
-
costTitle = "本会话消耗: " + sym + amount.toFixed(6) + (cost.models && cost.models.length ? " | 模型: " + cost.models.join(", ") : "")
|
|
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(", ") : "")
|
|
1288
2530
|
} else if (cost !== undefined && cost.waiting === true) {
|
|
1289
2531
|
costText = "~—"
|
|
1290
2532
|
costTitle = "本会话消耗 — 等待数据, 发消息后自动更新"
|
|
1291
2533
|
}
|
|
1292
2534
|
const costDisp = { text: costText, title: costTitle };
|
|
1293
2535
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
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,
|
|
1300
2550
|
]);
|
|
1301
2551
|
});
|
|
1302
2552
|
return () => { dispose(); };
|
|
1303
2553
|
});
|
|
1304
2554
|
|
|
1305
2555
|
ctx.effect(() => {
|
|
1306
|
-
const onVisibility = () => { if (!document.hidden) refresh(true).then(schedule, schedule); };
|
|
2556
|
+
const onVisibility = () => { if (!document.hidden) refresh(false, true).then(schedule, schedule); };
|
|
1307
2557
|
document.addEventListener("visibilitychange", onVisibility);
|
|
1308
2558
|
return () => document.removeEventListener("visibilitychange", onVisibility);
|
|
1309
2559
|
}, "dsh-api-dashboard: visibility resume");
|