diff --git a/package.json b/package.json
index 8742052..1025a71 100644
--- a/package.json
+++ b/package.json
@@ -6,12 +6,12 @@
"cf-typegen": "wrangler types --include-env=false"
},
"dependencies": {
- "chanfana": "^2.8.0",
+ "chanfana": "^2.8.1",
"hono": "^4.8.2",
- "zod": "^3.24.4"
+ "zod": "^3.25.67"
},
"devDependencies": {
- "wrangler": "^4.14.2"
+ "wrangler": "^4.20.5"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 830e4cd..380d651 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,23 +9,23 @@ importers:
.:
dependencies:
chanfana:
- specifier: ^2.8.0
- version: 2.8.0
+ specifier: ^2.8.1
+ version: 2.8.1
hono:
specifier: ^4.8.2
version: 4.8.2
zod:
- specifier: ^3.24.4
- version: 3.24.4
+ specifier: ^3.25.67
+ version: 3.25.67
devDependencies:
wrangler:
- specifier: ^4.14.2
- version: 4.14.2
+ specifier: ^4.20.5
+ version: 4.20.5
packages:
- '@asteasolutions/zod-to-openapi@7.3.0':
- resolution: {integrity: sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==}
+ '@asteasolutions/zod-to-openapi@7.3.4':
+ resolution: {integrity: sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA==}
peerDependencies:
zod: ^3.20.2
@@ -33,41 +33,41 @@ packages:
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
engines: {node: '>=18.0.0'}
- '@cloudflare/unenv-preset@2.3.1':
- resolution: {integrity: sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==}
+ '@cloudflare/unenv-preset@2.3.3':
+ resolution: {integrity: sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==}
peerDependencies:
- unenv: 2.0.0-rc.15
- workerd: ^1.20250320.0
+ unenv: 2.0.0-rc.17
+ workerd: ^1.20250508.0
peerDependenciesMeta:
workerd:
optional: true
- '@cloudflare/workerd-darwin-64@1.20250428.0':
- resolution: {integrity: sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==}
+ '@cloudflare/workerd-darwin-64@1.20250617.0':
+ resolution: {integrity: sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
- '@cloudflare/workerd-darwin-arm64@1.20250428.0':
- resolution: {integrity: sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==}
+ '@cloudflare/workerd-darwin-arm64@1.20250617.0':
+ resolution: {integrity: sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
- '@cloudflare/workerd-linux-64@1.20250428.0':
- resolution: {integrity: sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==}
+ '@cloudflare/workerd-linux-64@1.20250617.0':
+ resolution: {integrity: sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
- '@cloudflare/workerd-linux-arm64@1.20250428.0':
- resolution: {integrity: sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==}
+ '@cloudflare/workerd-linux-arm64@1.20250617.0':
+ resolution: {integrity: sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
- '@cloudflare/workerd-windows-64@1.20250428.0':
- resolution: {integrity: sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==}
+ '@cloudflare/workerd-windows-64@1.20250617.0':
+ resolution: {integrity: sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
@@ -79,152 +79,152 @@ packages:
'@emnapi/runtime@1.4.3':
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
- '@esbuild/aix-ppc64@0.25.2':
- resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==}
+ '@esbuild/aix-ppc64@0.25.4':
+ resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
- '@esbuild/android-arm64@0.25.2':
- resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==}
+ '@esbuild/android-arm64@0.25.4':
+ resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
- '@esbuild/android-arm@0.25.2':
- resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==}
+ '@esbuild/android-arm@0.25.4':
+ resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
- '@esbuild/android-x64@0.25.2':
- resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==}
+ '@esbuild/android-x64@0.25.4':
+ resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
- '@esbuild/darwin-arm64@0.25.2':
- resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==}
+ '@esbuild/darwin-arm64@0.25.4':
+ resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
- '@esbuild/darwin-x64@0.25.2':
- resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==}
+ '@esbuild/darwin-x64@0.25.4':
+ resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
- '@esbuild/freebsd-arm64@0.25.2':
- resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==}
+ '@esbuild/freebsd-arm64@0.25.4':
+ resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
- '@esbuild/freebsd-x64@0.25.2':
- resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==}
+ '@esbuild/freebsd-x64@0.25.4':
+ resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
- '@esbuild/linux-arm64@0.25.2':
- resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==}
+ '@esbuild/linux-arm64@0.25.4':
+ resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
- '@esbuild/linux-arm@0.25.2':
- resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==}
+ '@esbuild/linux-arm@0.25.4':
+ resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
- '@esbuild/linux-ia32@0.25.2':
- resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==}
+ '@esbuild/linux-ia32@0.25.4':
+ resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
- '@esbuild/linux-loong64@0.25.2':
- resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==}
+ '@esbuild/linux-loong64@0.25.4':
+ resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
- '@esbuild/linux-mips64el@0.25.2':
- resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==}
+ '@esbuild/linux-mips64el@0.25.4':
+ resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
- '@esbuild/linux-ppc64@0.25.2':
- resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==}
+ '@esbuild/linux-ppc64@0.25.4':
+ resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
- '@esbuild/linux-riscv64@0.25.2':
- resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==}
+ '@esbuild/linux-riscv64@0.25.4':
+ resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
- '@esbuild/linux-s390x@0.25.2':
- resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==}
+ '@esbuild/linux-s390x@0.25.4':
+ resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
- '@esbuild/linux-x64@0.25.2':
- resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==}
+ '@esbuild/linux-x64@0.25.4':
+ resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
- '@esbuild/netbsd-arm64@0.25.2':
- resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==}
+ '@esbuild/netbsd-arm64@0.25.4':
+ resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
- '@esbuild/netbsd-x64@0.25.2':
- resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==}
+ '@esbuild/netbsd-x64@0.25.4':
+ resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
- '@esbuild/openbsd-arm64@0.25.2':
- resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==}
+ '@esbuild/openbsd-arm64@0.25.4':
+ resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
- '@esbuild/openbsd-x64@0.25.2':
- resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==}
+ '@esbuild/openbsd-x64@0.25.4':
+ resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
- '@esbuild/sunos-x64@0.25.2':
- resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==}
+ '@esbuild/sunos-x64@0.25.4':
+ resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
- '@esbuild/win32-arm64@0.25.2':
- resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==}
+ '@esbuild/win32-arm64@0.25.4':
+ resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
- '@esbuild/win32-ia32@0.25.2':
- resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==}
+ '@esbuild/win32-ia32@0.25.4':
+ resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
- '@esbuild/win32-x64@0.25.2':
- resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==}
+ '@esbuild/win32-x64@0.25.4':
+ resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
@@ -366,8 +366,9 @@ packages:
blake3-wasm@2.1.5:
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
- chanfana@2.8.0:
- resolution: {integrity: sha512-R0lY+c4lBVO8jnop9J5hY6W2Vq5YS3sb9NwI4nY2oJU65E38rC6LCNNhEVEITVmcn+EhuKIQK4FLJLW93gFG4g==}
+ chanfana@2.8.1:
+ resolution: {integrity: sha512-iYNlH+pA2yXAV5YksIe/Wewr0b935+jot+cTDqdkyFAae2QzJ2t1t2/ShEMtUjHc8RdDb7IvPCDjZgtMl3Py8A==}
+ hasBin: true
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
@@ -397,8 +398,8 @@ packages:
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
engines: {node: '>=8'}
- esbuild@0.25.2:
- resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==}
+ esbuild@0.25.4:
+ resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==}
engines: {node: '>=18'}
hasBin: true
@@ -406,8 +407,8 @@ packages:
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
engines: {node: '>=6'}
- exsolve@1.0.5:
- resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==}
+ exsolve@1.0.7:
+ resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
@@ -436,8 +437,8 @@ packages:
engines: {node: '>=10.0.0'}
hasBin: true
- miniflare@4.20250428.1:
- resolution: {integrity: sha512-M3qcJXjeAEimHrEeWXEhrJiC3YHB5M3QSqqK67pOTI+lHn0QyVG/2iFUjVJ/nv+i10uxeAEva8GRGeu+tKRCmQ==}
+ miniflare@4.20250617.3:
+ resolution: {integrity: sha512-j+LZycT11UdlVeNdaqD0XdNnYnqAL+wXmboz+tNPFgTq6zhD489Ujj3BfSDyEHDCA9UFBLbkc5ByGWBh+pYZ5Q==}
engines: {node: '>=18.0.0'}
hasBin: true
@@ -460,8 +461,8 @@ packages:
printable-characters@1.0.42:
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
- semver@7.7.1:
- resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
+ semver@7.7.2:
+ resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
hasBin: true
@@ -493,20 +494,20 @@ packages:
resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
engines: {node: '>=14.0'}
- unenv@2.0.0-rc.15:
- resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==}
+ unenv@2.0.0-rc.17:
+ resolution: {integrity: sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==}
- workerd@1.20250428.0:
- resolution: {integrity: sha512-JJNWkHkwPQKQdvtM9UORijgYdcdJsihA4SfYjwh02IUQsdMyZ9jizV1sX9yWi9B9ptlohTW8UNHJEATuphGgdg==}
+ workerd@1.20250617.0:
+ resolution: {integrity: sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==}
engines: {node: '>=16'}
hasBin: true
- wrangler@4.14.2:
- resolution: {integrity: sha512-kD0ilA7WXwhDt1lfyPwppZ2/lT9uwDLp93tfqnqrUENmP/uj+5tErzfZY51emtgXZoU1HFK6HytBEYLRCvCiPg==}
+ wrangler@4.20.5:
+ resolution: {integrity: sha512-tmiyt2vBHszhdzJEDbCpFLU2RiV7/QzvGMV07Zaz4ptqiU2h/lTojyNJAugPpSFNiOuY+k0g3ENNTDQqoUkMFA==}
engines: {node: '>=18.0.0'}
hasBin: true
peerDependencies:
- '@cloudflare/workers-types': ^4.20250428.0
+ '@cloudflare/workers-types': ^4.20250617.0
peerDependenciesMeta:
'@cloudflare/workers-types':
optional: true
@@ -523,50 +524,54 @@ packages:
utf-8-validate:
optional: true
- yaml@2.7.1:
- resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==}
- engines: {node: '>= 14'}
+ yaml@2.8.0:
+ resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
+ engines: {node: '>= 14.6'}
hasBin: true
+ yargs-parser@22.0.0:
+ resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
youch@3.3.4:
resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==}
zod@3.22.3:
resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==}
- zod@3.24.4:
- resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
+ zod@3.25.67:
+ resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==}
snapshots:
- '@asteasolutions/zod-to-openapi@7.3.0(zod@3.24.4)':
+ '@asteasolutions/zod-to-openapi@7.3.4(zod@3.25.67)':
dependencies:
openapi3-ts: 4.4.0
- zod: 3.24.4
+ zod: 3.25.67
'@cloudflare/kv-asset-handler@0.4.0':
dependencies:
mime: 3.0.0
- '@cloudflare/unenv-preset@2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250428.0)':
+ '@cloudflare/unenv-preset@2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0)':
dependencies:
- unenv: 2.0.0-rc.15
+ unenv: 2.0.0-rc.17
optionalDependencies:
- workerd: 1.20250428.0
+ workerd: 1.20250617.0
- '@cloudflare/workerd-darwin-64@1.20250428.0':
+ '@cloudflare/workerd-darwin-64@1.20250617.0':
optional: true
- '@cloudflare/workerd-darwin-arm64@1.20250428.0':
+ '@cloudflare/workerd-darwin-arm64@1.20250617.0':
optional: true
- '@cloudflare/workerd-linux-64@1.20250428.0':
+ '@cloudflare/workerd-linux-64@1.20250617.0':
optional: true
- '@cloudflare/workerd-linux-arm64@1.20250428.0':
+ '@cloudflare/workerd-linux-arm64@1.20250617.0':
optional: true
- '@cloudflare/workerd-windows-64@1.20250428.0':
+ '@cloudflare/workerd-windows-64@1.20250617.0':
optional: true
'@cspotcode/source-map-support@0.8.1':
@@ -578,79 +583,79 @@ snapshots:
tslib: 2.8.1
optional: true
- '@esbuild/aix-ppc64@0.25.2':
+ '@esbuild/aix-ppc64@0.25.4':
optional: true
- '@esbuild/android-arm64@0.25.2':
+ '@esbuild/android-arm64@0.25.4':
optional: true
- '@esbuild/android-arm@0.25.2':
+ '@esbuild/android-arm@0.25.4':
optional: true
- '@esbuild/android-x64@0.25.2':
+ '@esbuild/android-x64@0.25.4':
optional: true
- '@esbuild/darwin-arm64@0.25.2':
+ '@esbuild/darwin-arm64@0.25.4':
optional: true
- '@esbuild/darwin-x64@0.25.2':
+ '@esbuild/darwin-x64@0.25.4':
optional: true
- '@esbuild/freebsd-arm64@0.25.2':
+ '@esbuild/freebsd-arm64@0.25.4':
optional: true
- '@esbuild/freebsd-x64@0.25.2':
+ '@esbuild/freebsd-x64@0.25.4':
optional: true
- '@esbuild/linux-arm64@0.25.2':
+ '@esbuild/linux-arm64@0.25.4':
optional: true
- '@esbuild/linux-arm@0.25.2':
+ '@esbuild/linux-arm@0.25.4':
optional: true
- '@esbuild/linux-ia32@0.25.2':
+ '@esbuild/linux-ia32@0.25.4':
optional: true
- '@esbuild/linux-loong64@0.25.2':
+ '@esbuild/linux-loong64@0.25.4':
optional: true
- '@esbuild/linux-mips64el@0.25.2':
+ '@esbuild/linux-mips64el@0.25.4':
optional: true
- '@esbuild/linux-ppc64@0.25.2':
+ '@esbuild/linux-ppc64@0.25.4':
optional: true
- '@esbuild/linux-riscv64@0.25.2':
+ '@esbuild/linux-riscv64@0.25.4':
optional: true
- '@esbuild/linux-s390x@0.25.2':
+ '@esbuild/linux-s390x@0.25.4':
optional: true
- '@esbuild/linux-x64@0.25.2':
+ '@esbuild/linux-x64@0.25.4':
optional: true
- '@esbuild/netbsd-arm64@0.25.2':
+ '@esbuild/netbsd-arm64@0.25.4':
optional: true
- '@esbuild/netbsd-x64@0.25.2':
+ '@esbuild/netbsd-x64@0.25.4':
optional: true
- '@esbuild/openbsd-arm64@0.25.2':
+ '@esbuild/openbsd-arm64@0.25.4':
optional: true
- '@esbuild/openbsd-x64@0.25.2':
+ '@esbuild/openbsd-x64@0.25.4':
optional: true
- '@esbuild/sunos-x64@0.25.2':
+ '@esbuild/sunos-x64@0.25.4':
optional: true
- '@esbuild/win32-arm64@0.25.2':
+ '@esbuild/win32-arm64@0.25.4':
optional: true
- '@esbuild/win32-ia32@0.25.2':
+ '@esbuild/win32-ia32@0.25.4':
optional: true
- '@esbuild/win32-x64@0.25.2':
+ '@esbuild/win32-x64@0.25.4':
optional: true
'@fastify/busboy@2.1.1': {}
@@ -751,32 +756,29 @@ snapshots:
blake3-wasm@2.1.5: {}
- chanfana@2.8.0:
+ chanfana@2.8.1:
dependencies:
- '@asteasolutions/zod-to-openapi': 7.3.0(zod@3.24.4)
+ '@asteasolutions/zod-to-openapi': 7.3.4(zod@3.25.67)
js-yaml: 4.1.0
openapi3-ts: 4.4.0
- zod: 3.24.4
+ yargs-parser: 22.0.0
+ zod: 3.25.67
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
- optional: true
- color-name@1.1.4:
- optional: true
+ color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
- optional: true
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
- optional: true
cookie@0.7.2: {}
@@ -784,40 +786,39 @@ snapshots:
defu@6.1.4: {}
- detect-libc@2.0.4:
- optional: true
+ detect-libc@2.0.4: {}
- esbuild@0.25.2:
+ esbuild@0.25.4:
optionalDependencies:
- '@esbuild/aix-ppc64': 0.25.2
- '@esbuild/android-arm': 0.25.2
- '@esbuild/android-arm64': 0.25.2
- '@esbuild/android-x64': 0.25.2
- '@esbuild/darwin-arm64': 0.25.2
- '@esbuild/darwin-x64': 0.25.2
- '@esbuild/freebsd-arm64': 0.25.2
- '@esbuild/freebsd-x64': 0.25.2
- '@esbuild/linux-arm': 0.25.2
- '@esbuild/linux-arm64': 0.25.2
- '@esbuild/linux-ia32': 0.25.2
- '@esbuild/linux-loong64': 0.25.2
- '@esbuild/linux-mips64el': 0.25.2
- '@esbuild/linux-ppc64': 0.25.2
- '@esbuild/linux-riscv64': 0.25.2
- '@esbuild/linux-s390x': 0.25.2
- '@esbuild/linux-x64': 0.25.2
- '@esbuild/netbsd-arm64': 0.25.2
- '@esbuild/netbsd-x64': 0.25.2
- '@esbuild/openbsd-arm64': 0.25.2
- '@esbuild/openbsd-x64': 0.25.2
- '@esbuild/sunos-x64': 0.25.2
- '@esbuild/win32-arm64': 0.25.2
- '@esbuild/win32-ia32': 0.25.2
- '@esbuild/win32-x64': 0.25.2
+ '@esbuild/aix-ppc64': 0.25.4
+ '@esbuild/android-arm': 0.25.4
+ '@esbuild/android-arm64': 0.25.4
+ '@esbuild/android-x64': 0.25.4
+ '@esbuild/darwin-arm64': 0.25.4
+ '@esbuild/darwin-x64': 0.25.4
+ '@esbuild/freebsd-arm64': 0.25.4
+ '@esbuild/freebsd-x64': 0.25.4
+ '@esbuild/linux-arm': 0.25.4
+ '@esbuild/linux-arm64': 0.25.4
+ '@esbuild/linux-ia32': 0.25.4
+ '@esbuild/linux-loong64': 0.25.4
+ '@esbuild/linux-mips64el': 0.25.4
+ '@esbuild/linux-ppc64': 0.25.4
+ '@esbuild/linux-riscv64': 0.25.4
+ '@esbuild/linux-s390x': 0.25.4
+ '@esbuild/linux-x64': 0.25.4
+ '@esbuild/netbsd-arm64': 0.25.4
+ '@esbuild/netbsd-x64': 0.25.4
+ '@esbuild/openbsd-arm64': 0.25.4
+ '@esbuild/openbsd-x64': 0.25.4
+ '@esbuild/sunos-x64': 0.25.4
+ '@esbuild/win32-arm64': 0.25.4
+ '@esbuild/win32-ia32': 0.25.4
+ '@esbuild/win32-x64': 0.25.4
exit-hook@2.2.1: {}
- exsolve@1.0.5: {}
+ exsolve@1.0.7: {}
fsevents@2.3.3:
optional: true
@@ -831,8 +832,7 @@ snapshots:
hono@4.8.2: {}
- is-arrayish@0.3.2:
- optional: true
+ is-arrayish@0.3.2: {}
js-yaml@4.1.0:
dependencies:
@@ -840,16 +840,17 @@ snapshots:
mime@3.0.0: {}
- miniflare@4.20250428.1:
+ miniflare@4.20250617.3:
dependencies:
'@cspotcode/source-map-support': 0.8.1
acorn: 8.14.0
acorn-walk: 8.3.2
exit-hook: 2.2.1
glob-to-regexp: 0.4.1
+ sharp: 0.33.5
stoppable: 1.1.0
undici: 5.29.0
- workerd: 1.20250428.0
+ workerd: 1.20250617.0
ws: 8.18.0
youch: 3.3.4
zod: 3.22.3
@@ -863,7 +864,7 @@ snapshots:
openapi3-ts@4.4.0:
dependencies:
- yaml: 2.7.1
+ yaml: 2.8.0
path-to-regexp@6.3.0: {}
@@ -871,14 +872,13 @@ snapshots:
printable-characters@1.0.42: {}
- semver@7.7.1:
- optional: true
+ semver@7.7.2: {}
sharp@0.33.5:
dependencies:
color: 4.2.3
detect-libc: 2.0.4
- semver: 7.7.1
+ semver: 7.7.2
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
@@ -899,12 +899,10 @@ snapshots:
'@img/sharp-wasm32': 0.33.5
'@img/sharp-win32-ia32': 0.33.5
'@img/sharp-win32-x64': 0.33.5
- optional: true
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
- optional: true
source-map@0.6.1: {}
@@ -924,42 +922,43 @@ snapshots:
dependencies:
'@fastify/busboy': 2.1.1
- unenv@2.0.0-rc.15:
+ unenv@2.0.0-rc.17:
dependencies:
defu: 6.1.4
- exsolve: 1.0.5
+ exsolve: 1.0.7
ohash: 2.0.11
pathe: 2.0.3
ufo: 1.6.1
- workerd@1.20250428.0:
+ workerd@1.20250617.0:
optionalDependencies:
- '@cloudflare/workerd-darwin-64': 1.20250428.0
- '@cloudflare/workerd-darwin-arm64': 1.20250428.0
- '@cloudflare/workerd-linux-64': 1.20250428.0
- '@cloudflare/workerd-linux-arm64': 1.20250428.0
- '@cloudflare/workerd-windows-64': 1.20250428.0
+ '@cloudflare/workerd-darwin-64': 1.20250617.0
+ '@cloudflare/workerd-darwin-arm64': 1.20250617.0
+ '@cloudflare/workerd-linux-64': 1.20250617.0
+ '@cloudflare/workerd-linux-arm64': 1.20250617.0
+ '@cloudflare/workerd-windows-64': 1.20250617.0
- wrangler@4.14.2:
+ wrangler@4.20.5:
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
- '@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250428.0)
+ '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0)
blake3-wasm: 2.1.5
- esbuild: 0.25.2
- miniflare: 4.20250428.1
+ esbuild: 0.25.4
+ miniflare: 4.20250617.3
path-to-regexp: 6.3.0
- unenv: 2.0.0-rc.15
- workerd: 1.20250428.0
+ unenv: 2.0.0-rc.17
+ workerd: 1.20250617.0
optionalDependencies:
fsevents: 2.3.3
- sharp: 0.33.5
transitivePeerDependencies:
- bufferutil
- utf-8-validate
ws@8.18.0: {}
- yaml@2.7.1: {}
+ yaml@2.8.0: {}
+
+ yargs-parser@22.0.0: {}
youch@3.3.4:
dependencies:
@@ -969,4 +968,4 @@ snapshots:
zod@3.22.3: {}
- zod@3.24.4: {}
+ zod@3.25.67: {}
diff --git a/public/index.html b/public/index.html
index 4633772..fcdbccb 100644
--- a/public/index.html
+++ b/public/index.html
@@ -43,12 +43,12 @@
}
.header h1 {
- font-size: 2.5rem;
+ font-size: 1.8rem;
margin-bottom: 10px;
}
.header.compact h1 {
- font-size: 1.8rem;
+ font-size: 1.4rem;
margin-bottom: 5px;
}
@@ -143,12 +143,12 @@
.tab {
flex: 1;
- padding: 15px 20px;
+ padding: 12px 16px;
text-align: center;
cursor: pointer;
border: none;
background: none;
- font-size: 16px;
+ font-size: 14px;
transition: all 0.3s;
}
@@ -175,13 +175,13 @@
.btn {
background: linear-gradient(135deg, #4c51bf 0%, #553c9a 100%);
color: white;
- border: none;
- padding: 12px 24px;
+ border: 2px solid transparent;
+ padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
- font-size: 14px;
+ font-size: 13px;
transition: all 0.3s;
- margin: 5px;
+ margin: 4px;
}
.btn:hover {
@@ -189,14 +189,48 @@
box-shadow: 0 5px 15px rgba(76, 81, 191, 0.4);
}
+ .btn-secondary {
+ background: transparent;
+ color: #4c51bf;
+ border: 2px solid #4c51bf;
+ }
+
+ .btn-secondary:hover {
+ background: rgba(76, 81, 191, 0.1);
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(76, 81, 191, 0.2);
+ }
+
+ .btn-secondary.btn-warning {
+ background: transparent;
+ color: #ed8936;
+ border: 2px solid #ed8936;
+ }
+
+ .btn-secondary.btn-warning:hover {
+ background: rgba(237, 137, 54, 0.1);
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(237, 137, 54, 0.2);
+ }
+
.btn-danger {
background: linear-gradient(135deg, #e53e3e 0%, #c53030 100%);
+ border: 2px solid transparent;
}
.btn-danger:hover {
box-shadow: 0 5px 15px rgba(229, 62, 62, 0.4);
}
+ .btn-warning {
+ background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
+ border: 2px solid transparent;
+ }
+
+ .btn-warning:hover {
+ box-shadow: 0 5px 15px rgba(237, 137, 54, 0.4);
+ }
+
.form-group {
margin-bottom: 20px;
}
@@ -206,6 +240,7 @@
margin-bottom: 8px;
font-weight: 600;
color: #2d3748;
+ font-size: 14px;
}
.form-control {
@@ -222,6 +257,11 @@
border-color: #4c51bf;
}
+ select.form-control {
+ cursor: pointer;
+ background-color: white;
+ }
+
textarea.form-control {
resize: vertical;
min-height: 100px;
@@ -253,7 +293,7 @@
}
.card-title {
- font-size: 1.25rem;
+ font-size: 1.1rem;
font-weight: 600;
color: #2d3748;
}
@@ -339,6 +379,7 @@
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
animation: slideInDown 0.3s ease-out;
+ text-align: center;
}
@keyframes slideInDown {
@@ -371,6 +412,66 @@
color: #718096;
}
+ /* mainContent loading 覆盖层样式 */
+ #loadingOverlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(4px);
+ z-index: 10002;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ color: white;
+ opacity: 0;
+ transition: opacity 0.3s ease-in-out;
+ pointer-events: auto;
+ cursor: wait;
+ }
+
+ /* 当 loading 显示时,禁用 mainContent 交互 */
+ #mainContent.loading-active {
+ position: relative;
+ pointer-events: none;
+ }
+
+ #mainContent.loading-active #loadingOverlay {
+ pointer-events: auto;
+ }
+
+ #loadingOverlay .spinner {
+ border: 4px solid rgba(255, 255, 255, 0.3);
+ border-top: 4px solid #ffffff;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ animation: spin 1s linear infinite;
+ margin: 0 auto 20px;
+ }
+
+ #loadingOverlay p {
+ margin-top: 20px;
+ font-size: 16px;
+ font-weight: 500;
+ animation: pulse 2s ease-in-out infinite;
+ }
+
+ @keyframes pulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.7;
+ }
+ }
+
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #4c51bf;
@@ -397,6 +498,106 @@
gap: 20px;
}
+ .pricing-table {
+ overflow-x: auto;
+ }
+
+ .pricing-table table {
+ min-width: 600px;
+ }
+
+ .pricing-table input[type="text"],
+ .pricing-table input[type="number"] {
+ width: 100%;
+ border: none;
+ background: transparent;
+ padding: 8px;
+ font-size: 14px;
+ }
+
+ .pricing-table input[type="text"]:focus,
+ .pricing-table input[type="number"]:focus {
+ outline: 2px solid #4c51bf;
+ outline-offset: -2px;
+ border-radius: 4px;
+ background: white;
+ }
+
+ .pricing-table tr:hover {
+ background-color: #f8f9fa;
+ }
+
+ /* 模态框样式 */
+ .modal {
+ display: none;
+ position: fixed;
+ z-index: 10000;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(4px);
+ }
+
+ .modal-content {
+ background-color: white;
+ margin: 10% auto;
+ padding: 0;
+ border-radius: 12px;
+ width: 90%;
+ max-width: 500px;
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
+ animation: modalSlideIn 0.3s ease-out;
+ }
+
+ @keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-50px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+
+ .modal-header {
+ background: linear-gradient(135deg, #4c51bf 0%, #553c9a 100%);
+ color: white;
+ padding: 20px;
+ border-radius: 12px 12px 0 0;
+ position: relative;
+ }
+
+ .modal-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin: 0;
+ }
+
+ .modal-close {
+ position: absolute;
+ top: 15px;
+ right: 20px;
+ background: none;
+ border: none;
+ color: white;
+ font-size: 24px;
+ cursor: pointer;
+ opacity: 0.8;
+ transition: opacity 0.3s;
+ }
+
+ .modal-close:hover {
+ opacity: 1;
+ }
+
+ .modal-body {
+ padding: 30px;
+ }
+
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr;
@@ -428,16 +629,34 @@
}
.header h1 {
- font-size: 2rem;
+ font-size: 1.6rem;
}
.header.compact h1 {
- font-size: 1.5rem;
+ font-size: 1.2rem;
}
.auth-section {
padding: 40px 20px;
}
+
+ /* 登录提示在移动端的样式优化 */
+ #loginPrompt>div {
+ flex-direction: column !important;
+ gap: 10px !important;
+ }
+
+ #loginPrompt>div>div:last-child {
+ margin-top: 0 !important;
+ width: 100%;
+ text-align: center;
+ flex-shrink: 1 !important;
+ }
+
+ #loginPrompt>div>div:first-child {
+ min-width: auto !important;
+ text-align: center;
+ }
}
@@ -452,12 +671,14 @@
🚀 Awsl One API
管理面板 - 频道、令牌、定价管理
-
+
-
+
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 💡 要使用管理功能,请先进行身份验证
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AWSL One API 是一个强大的 API 代理和管理平台,为您提供统一的 AI 模型接口管理服务。
+
+
+
+
+
🔗 频道管理
+
配置和管理 AI 服务提供商的 API 端点,支持 Azure
+ OpenAI 等多种平台。
+
+
+
🔑 令牌管理
+
生成和管理 API 访问令牌,控制用户访问权限和使用配额。
+
+
+
💰 定价管理
+
灵活配置不同 AI 模型的计费策略,精确控制使用成本。
+
+
+
🧪 API 测试
+
内置 API 测试工具,快速验证配置和调试接口问题。
+
+
+
+
+
⚡ 快速开始
+
+ - 首先初始化数据库以设置必要的数据表结构
+ - 在频道管理中添加您的 AI 服务提供商配置
+ - 创建访问令牌并设置适当的权限和配额
+ - 配置模型定价策略以控制使用成本
+ - 使用 API 测试工具验证配置是否正常工作
+
+
+
+
-
+
-
点击下面的按钮来初始化数据库表结构。
-
+
点击下面的按钮来初始化数据库表结构。如果这是首次使用,请先进行数据库初始化。
@@ -497,7 +811,7 @@
频道列表
-
+
@@ -510,18 +824,73 @@
-
@@ -536,7 +905,7 @@
令牌列表
-
+
@@ -549,61 +918,82 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
- 例如: azure-openai-1, azure-openai-2
- (留空表示允许访问所有频道)
-
-
@@ -615,24 +1005,59 @@
💡 定价说明
- • 基础配额单位:1百万token = $1.00
- • 定价配置为不同模型相对于基础单位的倍率
- • 例如:gpt-4的input倍率为30,表示1百万token消耗30美元配额
+ 基础配额单位:1百万token = $1.00 | 定价配置为不同模型相对于基础单位的倍率 |
+ 例如:gpt-4的input倍率为30,表示1百万token消耗30美元配额
@@ -645,7 +1070,7 @@
API 测试
-
+
@@ -693,6 +1118,72 @@
// === 认证相关函数 ===
+ // 显示登录模态框
+ function showLoginModal() {
+ document.getElementById('loginModal').style.display = 'block';
+ // 清空输入框
+ document.getElementById('modalAdminToken').value = '';
+ // 聚焦到输入框
+ setTimeout(() => {
+ document.getElementById('modalAdminToken').focus();
+ }, 100);
+ }
+
+ // 关闭登录模态框
+ function closeLoginModal() {
+ document.getElementById('loginModal').style.display = 'none';
+ }
+
+ // 处理模态框登录表单提交
+ function handleModalLogin(event) {
+ event.preventDefault();
+ authenticateFromModal();
+ }
+
+ // 模态框身份验证
+ function authenticateFromModal() {
+ const token = document.getElementById('modalAdminToken').value;
+ if (!token) {
+ showAlert('请输入管理员令牌', 'error');
+ return;
+ }
+
+ adminToken = token;
+ localStorage.setItem('adminToken', token);
+ closeLoginModal();
+ showMainContent();
+ showAlert('登录成功!', 'success');
+ }
+
+ // 显示登录页面 (保留兼容性)
+ function showLoginPage() {
+ showLoginModal();
+ }
+
+ // 显示主页面(未登录状态)
+ function showHomePage() {
+ document.getElementById('authSection').style.display = 'none';
+ document.getElementById('mainContent').style.display = 'block';
+ document.getElementById('logoutSection').style.display = 'none';
+ document.getElementById('mainHeader').classList.add('compact');
+
+ // 显示登录提示
+ document.getElementById('loginPrompt').style.display = 'block';
+
+ // 隐藏管理员专用的tabs
+ const adminTabs = document.querySelectorAll('.admin-only');
+ adminTabs.forEach(tab => tab.style.display = 'none');
+
+ // 确保首页tab是激活状态,先隐藏所有tab内容
+ const allTabs = document.querySelectorAll('.tab-content');
+ allTabs.forEach(tab => tab.classList.remove('active'));
+ const allTabButtons = document.querySelectorAll('.tab');
+ allTabButtons.forEach(btn => btn.classList.remove('active'));
+
+ // 激活首页tab
+ document.getElementById('home').classList.add('active');
+ document.querySelector('.tab[onclick="switchTab(\'home\')"]').classList.add('active');
+ }
// 处理登录表单提交(支持回车键)
function handleLogin(event) {
@@ -713,28 +1204,56 @@
showMainContent();
}
- // 显示主内容区域
+ // 显示主内容区域(已登录状态)
function showMainContent() {
document.getElementById('authSection').style.display = 'none';
document.getElementById('mainContent').style.display = 'block';
document.getElementById('logoutSection').style.display = 'block';
document.getElementById('mainHeader').classList.add('compact');
+
+ // 隐藏登录提示
+ document.getElementById('loginPrompt').style.display = 'none';
+
+ // 显示管理员专用的tabs
+ const adminTabs = document.querySelectorAll('.admin-only');
+ adminTabs.forEach(tab => tab.style.display = 'block');
+
+ // 确保首页tab是激活状态,先隐藏所有tab内容
+ const allTabs = document.querySelectorAll('.tab-content');
+ allTabs.forEach(tab => tab.classList.remove('active'));
+ const allTabButtons = document.querySelectorAll('.tab');
+ allTabButtons.forEach(btn => btn.classList.remove('active'));
+
+ // 激活首页tab
+ document.getElementById('home').classList.add('active');
+ document.querySelector('.tab[onclick="switchTab(\'home\')"]').classList.add('active');
+ }
+
+ // 确认退出登录
+ function confirmLogout() {
+ if (confirm('确定要退出登录吗?\n\n退出后您需要重新输入管理员令牌才能使用管理功能。')) {
+ logout();
+ }
}
// 退出登录
function logout() {
localStorage.removeItem('adminToken');
adminToken = '';
- document.getElementById('authSection').style.display = 'block';
- document.getElementById('mainContent').style.display = 'none';
- document.getElementById('logoutSection').style.display = 'none';
- document.getElementById('mainHeader').classList.remove('compact');
+ showHomePage();
document.getElementById('adminToken').value = '';
showAlert('已退出登录', 'success');
}
// Tab switching
function switchTab(tabName) {
+ // 检查是否为管理员专用tab且用户未登录
+ const adminOnlyTabs = ['database', 'channels', 'tokens', 'pricing', 'api-test'];
+ if (adminOnlyTabs.includes(tabName) && !adminToken) {
+ showAlert('请先登录后再访问管理功能', 'error');
+ return;
+ }
+
// 隐藏所有表单视图,显示列表视图
if (tabName === 'channels') {
showChannelList();
@@ -750,8 +1269,16 @@
tabButtons.forEach(btn => btn.classList.remove('active'));
// Show selected tab
- document.getElementById(tabName).classList.add('active');
- event.target.classList.add('active');
+ const targetTab = document.getElementById(tabName);
+ if (targetTab) {
+ targetTab.classList.add('active');
+ }
+
+ // 找到对应的tab按钮并激活
+ const targetTabButton = document.querySelector(`.tab[onclick="switchTab('${tabName}')"]`);
+ if (targetTabButton) {
+ targetTabButton.classList.add('active');
+ }
// 切换tab时重新获取数据
switch (tabName) {
@@ -770,13 +1297,16 @@
case 'database':
// 数据库页面不需要加载数据
break;
+ case 'home':
+ // 首页不需要加载数据
+ break;
}
}
// === API请求工具函数 ===
// API request helper
- async function apiRequest(url, method = 'GET', body = null) {
+ async function apiRequest(url, method = 'GET', body = null, showGlobalLoading = true, loadingMessage = '正在处理请求...') {
const options = {
method,
headers: {
@@ -789,6 +1319,11 @@
options.body = JSON.stringify(body);
}
+ // 显示全局 loading
+ if (showGlobalLoading) {
+ showLoading(loadingMessage);
+ }
+
try {
const response = await fetch(url, options);
if (!response.ok) {
@@ -797,7 +1332,6 @@
}
const result = await response.json();
- console.log(`API ${method} ${url} 响应:`, result);
// 处理 CommonResponse 格式
if (result.success === false) {
@@ -806,29 +1340,166 @@
// 返回实际数据,如果有 data 字段则返回 data,否则返回整个结果
const finalData = result.data !== undefined ? result.data : result;
- console.log(`API ${method} ${url} 最终数据:`, finalData);
return finalData;
} catch (error) {
console.error('API Error:', error);
throw error;
+ } finally {
+ // 无论成功还是失败都隐藏 loading
+ if (showGlobalLoading) {
+ hideLoading();
+ }
}
}
// Database functions
async function initializeDatabase() {
try {
- showLoading('正在初始化数据库...');
- await apiRequest('/api/admin/db_initialize', 'POST');
+ await apiRequest('/api/admin/db_initialize', 'POST', null, true, '正在初始化数据库...');
showAlert('数据库初始化成功!', 'success');
} catch (error) {
showAlert(`数据库初始化失败: ${error.message}`, 'error');
- } finally {
- hideLoading();
}
}
// === CHANNEL FUNCTIONS ===
+ // 全局变量跟踪编辑模式
+ let channelEditMode = 'form'; // 'form' 或 'json'
+ let tokenEditMode = 'form'; // 'form' 或 'json'
+ let pricingEditMode = 'table'; // 'table' 或 'json'
+
+ // 切换频道编辑模式
+ function toggleChannelEditMode() {
+ const formMode = document.getElementById('channelFormMode');
+ const jsonMode = document.getElementById('channelJsonMode');
+ const toggleBtn = document.getElementById('channelModeToggle');
+
+ if (channelEditMode === 'form') {
+ // 切换到JSON模式
+ // 先从表单收集数据转换为JSON
+ const formData = collectChannelFormData();
+ if (formData) {
+ const key = document.getElementById('channelKey').value;
+ document.getElementById('channelKeyJson').value = key;
+ document.getElementById('channelValueJson').value = JSON.stringify(formData, null, 2);
+ }
+
+ formMode.style.display = 'none';
+ jsonMode.style.display = 'block';
+ toggleBtn.textContent = '📋 切换到表单模式';
+ channelEditMode = 'json';
+ } else {
+ // 切换到表单模式
+ // 先从JSON收集数据填充到表单
+ try {
+ const key = document.getElementById('channelKeyJson').value;
+ const jsonValue = document.getElementById('channelValueJson').value;
+ if (jsonValue) {
+ const parsed = JSON.parse(jsonValue);
+ fillChannelForm(key, parsed);
+ }
+ } catch (error) {
+ console.warn('JSON格式错误,无法转换到表单模式:', error);
+ }
+
+ formMode.style.display = 'block';
+ jsonMode.style.display = 'none';
+ toggleBtn.textContent = '📝 切换到JSON模式';
+ channelEditMode = 'form';
+ }
+ }
+
+ // 收集频道表单数据
+ function collectChannelFormData() {
+ const name = document.getElementById('channelName').value.trim();
+ const type = document.getElementById('channelType').value.trim();
+ const endpoint = document.getElementById('channelEndpoint').value.trim();
+ const apiKey = document.getElementById('channelApiKey').value.trim();
+ const apiVersion = document.getElementById('channelApiVersion').value.trim();
+ const deploymentMapperText = document.getElementById('deploymentMapper').value.trim();
+ const additionalConfigText = document.getElementById('additionalConfig').value.trim();
+
+ if (!name || !endpoint || !apiKey) {
+ return null;
+ }
+
+ const config = {
+ name: name,
+ type: type,
+ endpoint: endpoint,
+ api_key: apiKey
+ };
+
+ if (apiVersion) {
+ config.api_version = apiVersion;
+ }
+
+ if (deploymentMapperText) {
+ try {
+ config.deployment_mapper = JSON.parse(deploymentMapperText);
+ } catch (error) {
+ console.warn('部署映射JSON格式错误:', error);
+ }
+ }
+
+ if (additionalConfigText) {
+ try {
+ const additionalConfig = JSON.parse(additionalConfigText);
+ Object.assign(config, additionalConfig);
+ } catch (error) {
+ console.warn('其他配置JSON格式错误:', error);
+ }
+ }
+
+ return config;
+ }
+
+ // 填充频道表单
+ function fillChannelForm(key, config) {
+ document.getElementById('channelKey').value = key;
+ document.getElementById('channelName').value = config.name || '';
+ document.getElementById('channelType').value = config.type || '';
+ document.getElementById('channelEndpoint').value = config.endpoint || '';
+ document.getElementById('channelApiKey').value = config.api_key || '';
+ document.getElementById('channelApiVersion').value = config.api_version || '';
+
+ if (config.deployment_mapper) {
+ document.getElementById('deploymentMapper').value = JSON.stringify(config.deployment_mapper, null, 2);
+ } else {
+ document.getElementById('deploymentMapper').value = '';
+ }
+
+ // 处理其他配置
+ const additionalConfig = { ...config };
+ delete additionalConfig.name;
+ delete additionalConfig.type;
+ delete additionalConfig.endpoint;
+ delete additionalConfig.api_key;
+ delete additionalConfig.api_version;
+ delete additionalConfig.deployment_mapper;
+
+ if (Object.keys(additionalConfig).length > 0) {
+ document.getElementById('additionalConfig').value = JSON.stringify(additionalConfig, null, 2);
+ } else {
+ document.getElementById('additionalConfig').value = '';
+ }
+ }
+
+ // 切换API版本字段的显示状态
+ function toggleApiVersionField() {
+ const channelType = document.getElementById('channelType').value;
+ const apiVersionGroup = document.getElementById('apiVersionGroup');
+
+ if (channelType === 'azure-openai') {
+ apiVersionGroup.style.display = 'block';
+ } else {
+ apiVersionGroup.style.display = 'none';
+ // 隐藏时清空API版本字段
+ document.getElementById('channelApiVersion').value = '';
+ }
+ }
+
// 显示频道列表
function showChannelList() {
document.getElementById('channelListView').style.display = 'block';
@@ -849,58 +1520,111 @@
// 清空频道表单
function clearChannelForm() {
+ // 清空表单模式字段
document.getElementById('channelKey').value = '';
- document.getElementById('channelConfig').value = '';
+ document.getElementById('channelName').value = '';
+ document.getElementById('channelType').value = 'azure-openai'; // 默认设置为 azure-openai
+ document.getElementById('channelEndpoint').value = '';
+ document.getElementById('channelApiKey').value = '';
+ document.getElementById('channelApiVersion').value = '';
+ document.getElementById('deploymentMapper').value = '';
+ document.getElementById('additionalConfig').value = '';
document.getElementById('channelKey').disabled = false;
+
+ // 清空JSON模式字段
+ document.getElementById('channelKeyJson').value = '';
+ document.getElementById('channelValueJson').value = '';
+
+ // 重置到表单模式
+ document.getElementById('channelFormMode').style.display = 'block';
+ document.getElementById('channelJsonMode').style.display = 'none';
+ document.getElementById('channelModeToggle').textContent = '📝 切换到JSON模式';
+ channelEditMode = 'form';
+
currentEditingChannel = null;
+
+ // 确保API版本字段根据类型正确显示
+ toggleApiVersionField();
}
// 编辑频道
function editChannelFromList(key, originalValue) {
currentEditingChannel = key;
- document.getElementById('channelKey').value = key;
- document.getElementById('channelKey').disabled = true; // 编辑时不允许修改key
- // 直接使用原始的JSON字符串,格式化显示
+
try {
const parsed = JSON.parse(originalValue);
- document.getElementById('channelConfig').value = JSON.stringify(parsed, null, 2);
- } catch {
- document.getElementById('channelConfig').value = originalValue;
+
+ if (channelEditMode === 'form') {
+ // 表单模式
+ fillChannelForm(key, parsed);
+ document.getElementById('channelKey').disabled = true; // 编辑时不允许修改key
+ } else {
+ // JSON模式
+ document.getElementById('channelKeyJson').value = key;
+ document.getElementById('channelValueJson').value = JSON.stringify(parsed, null, 2);
+ }
+
+ // 确保API版本字段根据类型正确显示
+ toggleApiVersionField();
+ showChannelForm(true);
+ } catch (error) {
+ console.error('解析频道配置失败:', error);
+ showAlert('频道配置格式错误', 'error');
}
- showChannelForm(true);
}
// 保存频道
async function saveChannel() {
- const key = document.getElementById('channelKey').value;
- const configText = document.getElementById('channelConfig').value;
+ let key, config;
- if (!key || !configText) {
- showAlert('请填写频道标识和配置', 'error');
- return;
+ if (channelEditMode === 'form') {
+ // 表单模式
+ key = document.getElementById('channelKey').value.trim();
+ config = collectChannelFormData();
+
+ if (!config) {
+ showAlert('请填写所有必填字段(频道名称、端点、API密钥)', 'error');
+ return;
+ }
+
+ if (!key) {
+ showAlert('请填写频道标识', 'error');
+ return;
+ }
+ } else {
+ // JSON模式
+ key = document.getElementById('channelKeyJson').value.trim();
+ const jsonValue = document.getElementById('channelValueJson').value.trim();
+
+ if (!key || !jsonValue) {
+ showAlert('请填写频道标识和配置JSON', 'error');
+ return;
+ }
+
+ try {
+ config = JSON.parse(jsonValue);
+ } catch (error) {
+ showAlert('JSON格式错误,请检查配置格式', 'error');
+ return;
+ }
}
try {
- const config = JSON.parse(configText);
- await apiRequest(`/api/admin/channel/${encodeURIComponent(key)}`, 'POST', config);
+ await apiRequest(`/api/admin/channel/${encodeURIComponent(key)}`, 'POST', config, true, '正在保存频道...');
showAlert('频道保存成功!', 'success');
clearChannelForm();
showChannelList();
loadChannels();
} catch (error) {
- if (error.message.includes('JSON')) {
- showAlert('配置格式错误,请检查JSON格式', 'error');
- } else {
- showAlert(`保存频道失败: ${error.message}`, 'error');
- }
+ showAlert(`保存频道失败: ${error.message}`, 'error');
}
}
// Channel functions
async function loadChannels() {
try {
- const channels = await apiRequest('/api/admin/channel');
+ const channels = await apiRequest('/api/admin/channel', 'GET', null, true);
displayChannels(channels);
} catch (error) {
document.getElementById('channelsList').innerHTML = `
加载频道失败: ${error.message}
`;
@@ -911,7 +1635,7 @@
if (!confirm('确定要删除这个频道吗?')) return;
try {
- await apiRequest(`/api/admin/channel/${encodeURIComponent(key)}`, 'DELETE');
+ await apiRequest(`/api/admin/channel/${encodeURIComponent(key)}`, 'DELETE', null, true, '正在删除频道...');
showAlert('频道删除成功!', 'success');
loadChannels();
} catch (error) {
@@ -934,19 +1658,19 @@
// 解析 value 字段中的 JSON 字符串
parsedValue = JSON.parse(item.value);
} catch {
- parsedValue = { name: 'Invalid JSON', type: 'Unknown', endpoint: '' };
+ parsedValue = { name: 'Invalid JSON', type: 'azure-openai', endpoint: '' };
}
return `
-
${item.key || 'Unknown'}
+
${parsedValue.name || 'No name'}
- ${parsedValue.name || 'No name'} | 类型: ${parsedValue.type || 'Unknown'} | 端点: ${(parsedValue.endpoint || '').substring(0, 50)}${(parsedValue.endpoint || '').length > 50 ? '...' : ''}
+ ${item.key || 'Unknown'} | 类型: ${parsedValue.type || 'Unknown'} | 端点: ${(parsedValue.endpoint || '').substring(0, 50)}${(parsedValue.endpoint || '').length > 50 ? '...' : ''}
-
+
@@ -956,6 +1680,76 @@
// === TOKEN FUNCTIONS ===
+ // 切换令牌编辑模式
+ function toggleTokenEditMode() {
+ const formMode = document.getElementById('tokenFormMode');
+ const jsonMode = document.getElementById('tokenJsonMode');
+ const toggleBtn = document.getElementById('tokenModeToggle');
+
+ if (tokenEditMode === 'form') {
+ // 切换到JSON模式
+ const formData = collectTokenFormData();
+ if (formData.key) {
+ document.getElementById('tokenKeyJson').value = formData.key;
+ delete formData.key; // 从配置中移除key
+ document.getElementById('tokenValueJson').value = JSON.stringify(formData, null, 2);
+ }
+
+ formMode.style.display = 'none';
+ jsonMode.style.display = 'block';
+ toggleBtn.textContent = '📋 切换到表单模式';
+ tokenEditMode = 'json';
+ } else {
+ // 切换到表单模式
+ try {
+ const key = document.getElementById('tokenKeyJson').value;
+ const jsonValue = document.getElementById('tokenValueJson').value;
+ if (jsonValue) {
+ const parsed = JSON.parse(jsonValue);
+ fillTokenForm(key, parsed);
+ }
+ } catch (error) {
+ console.warn('JSON格式错误,无法转换到表单模式:', error);
+ }
+
+ formMode.style.display = 'block';
+ jsonMode.style.display = 'none';
+ toggleBtn.textContent = '📝 切换到JSON模式';
+ tokenEditMode = 'form';
+ }
+ }
+
+ // 收集令牌表单数据
+ function collectTokenFormData() {
+ const name = document.getElementById('tokenName').value.trim();
+ const key = document.getElementById('tokenKey').value.trim();
+ const channelKeysInput = document.getElementById('channelKeysInput').value.trim();
+ const totalQuotaInput = document.getElementById('totalQuotaInput').value.trim();
+
+ const channelKeys = channelKeysInput
+ ? channelKeysInput.split(',').map(key => key.trim()).filter(key => key.length > 0)
+ : [];
+
+ return {
+ key: key,
+ name: name,
+ channel_keys: channelKeys,
+ total_quota: totalQuotaInput ? parseFloat(totalQuotaInput) : 0
+ };
+ }
+
+ // 填充令牌表单
+ function fillTokenForm(key, config) {
+ document.getElementById('tokenName').value = config.name || '';
+ document.getElementById('tokenKey').value = key;
+
+ const channelKeysStr = config.channel_keys && Array.isArray(config.channel_keys)
+ ? config.channel_keys.join(', ')
+ : '';
+ document.getElementById('channelKeysInput').value = channelKeysStr;
+ document.getElementById('totalQuotaInput').value = config.total_quota || '';
+ }
+
// 显示令牌列表
function showTokenList() {
document.getElementById('tokenListView').style.display = 'block';
@@ -997,11 +1791,16 @@
// 清空令牌表单
function clearTokenForm() {
+ // 清空表单模式字段
document.getElementById('tokenName').value = '';
document.getElementById('tokenKey').value = '';
document.getElementById('channelKeysInput').value = '';
document.getElementById('totalQuotaInput').value = '';
+ // 清空JSON模式字段
+ document.getElementById('tokenKeyJson').value = '';
+ document.getElementById('tokenValueJson').value = '';
+
// 重置token key输入框状态
const tokenKeyInput = document.getElementById('tokenKey');
const tokenKeyActions = document.getElementById('tokenKeyActions');
@@ -1015,6 +1814,12 @@
generateBtn.style.display = 'inline-block';
copyBtn.style.display = 'none';
+ // 重置到表单模式
+ document.getElementById('tokenFormMode').style.display = 'block';
+ document.getElementById('tokenJsonMode').style.display = 'none';
+ document.getElementById('tokenModeToggle').textContent = '📝 切换到JSON模式';
+ tokenEditMode = 'form';
+
currentEditingToken = null;
}
@@ -1056,75 +1861,81 @@
function editTokenFromList(key, originalValue) {
currentEditingToken = key;
- // 解析JSON并填充表单字段
try {
const parsed = JSON.parse(originalValue);
- // 填充名称字段
- document.getElementById('tokenName').value = parsed.name || '';
- // 填充token key
- document.getElementById('tokenKey').value = key;
- // 处理 channel_keys 数组,转换为逗号分隔的字符串
- const channelKeysStr = parsed.channel_keys && Array.isArray(parsed.channel_keys)
- ? parsed.channel_keys.join(', ')
- : '';
- document.getElementById('channelKeysInput').value = channelKeysStr;
- document.getElementById('totalQuotaInput').value = parsed.total_quota || '';
- } catch {
- // 如果解析失败,清空表单
- document.getElementById('tokenName').value = '';
- document.getElementById('tokenKey').value = key;
- document.getElementById('channelKeysInput').value = '';
- document.getElementById('totalQuotaInput').value = '';
+
+ if (tokenEditMode === 'form') {
+ // 表单模式
+ fillTokenForm(key, parsed);
+
+ // 设置编辑模式的UI状态
+ const tokenKeyInput = document.getElementById('tokenKey');
+ const tokenKeyActions = document.getElementById('tokenKeyActions');
+ const generateBtn = document.getElementById('generateTokenBtn');
+ const copyBtn = document.getElementById('copyTokenBtn');
+
+ tokenKeyInput.disabled = true; // 编辑时不允许修改key
+ tokenKeyInput.style.cursor = 'not-allowed';
+ tokenKeyInput.placeholder = '编辑时不可修改';
+ tokenKeyActions.style.display = 'flex';
+ generateBtn.style.display = 'none'; // 隐藏生成按钮
+ copyBtn.style.display = 'inline-block'; // 显示复制按钮
+ } else {
+ // JSON模式
+ document.getElementById('tokenKeyJson').value = key;
+ document.getElementById('tokenValueJson').value = JSON.stringify(parsed, null, 2);
+ }
+
+ showTokenForm(true);
+ } catch (error) {
+ console.error('解析令牌配置失败:', error);
+ showAlert('令牌配置格式错误', 'error');
}
-
- // 设置编辑模式的UI状态
- const tokenKeyInput = document.getElementById('tokenKey');
- const tokenKeyActions = document.getElementById('tokenKeyActions');
- const generateBtn = document.getElementById('generateTokenBtn');
- const copyBtn = document.getElementById('copyTokenBtn');
-
- tokenKeyInput.disabled = true; // 编辑时不允许修改key
- tokenKeyInput.style.cursor = 'not-allowed';
- tokenKeyInput.placeholder = '编辑时不可修改';
- tokenKeyActions.style.display = 'flex';
- generateBtn.style.display = 'none'; // 隐藏生成按钮
- copyBtn.style.display = 'inline-block'; // 显示复制按钮
-
- showTokenForm(true);
}
// 保存令牌
async function saveToken() {
- const name = document.getElementById('tokenName').value;
- const key = document.getElementById('tokenKey').value;
- const channelKeysInput = document.getElementById('channelKeysInput').value;
- const totalQuotaInput = document.getElementById('totalQuotaInput').value;
+ let key, config;
- if (!name || !key || !totalQuotaInput) {
- showAlert('请填写令牌名称、令牌标识和总配额', 'error');
- return;
- }
+ if (tokenEditMode === 'form') {
+ // 表单模式
+ const formData = collectTokenFormData();
+ key = formData.key;
+ config = {
+ name: formData.name,
+ channel_keys: formData.channel_keys,
+ total_quota: formData.total_quota
+ };
- // 验证总配额是否为有效数字
- const totalQuota = parseFloat(totalQuotaInput);
- if (isNaN(totalQuota) || totalQuota < 0) {
- showAlert('请输入有效的总配额数值', 'error');
- return;
+ if (!formData.name || !key || !formData.total_quota) {
+ showAlert('请填写令牌名称、令牌标识和总配额', 'error');
+ return;
+ }
+
+ if (isNaN(formData.total_quota) || formData.total_quota < 0) {
+ showAlert('请输入有效的总配额数值', 'error');
+ return;
+ }
+ } else {
+ // JSON模式
+ key = document.getElementById('tokenKeyJson').value.trim();
+ const jsonValue = document.getElementById('tokenValueJson').value.trim();
+
+ if (!key || !jsonValue) {
+ showAlert('请填写令牌标识和配置JSON', 'error');
+ return;
+ }
+
+ try {
+ config = JSON.parse(jsonValue);
+ } catch (error) {
+ showAlert('JSON格式错误,请检查配置格式', 'error');
+ return;
+ }
}
try {
- // 处理 channel_keys:如果输入为空,则设为空数组;否则按逗号分割并去除空白
- const channelKeys = channelKeysInput.trim()
- ? channelKeysInput.split(',').map(key => key.trim()).filter(key => key.length > 0)
- : [];
-
- const config = {
- name: name,
- channel_keys: channelKeys,
- total_quota: totalQuota
- };
-
- await apiRequest(`/api/admin/token/${encodeURIComponent(key)}`, 'POST', config);
+ await apiRequest(`/api/admin/token/${encodeURIComponent(key)}`, 'POST', config, true, '正在保存令牌...');
showAlert('令牌保存成功!', 'success');
clearTokenForm();
@@ -1138,7 +1949,7 @@
// Token functions
async function loadTokens() {
try {
- const tokens = await apiRequest('/api/admin/token');
+ const tokens = await apiRequest('/api/admin/token', 'GET', null, true);
displayTokens(tokens);
} catch (error) {
document.getElementById('tokensList').innerHTML = `
加载令牌失败: ${error.message}
`;
@@ -1149,7 +1960,7 @@
if (!confirm('确定要删除这个令牌吗?')) return;
try {
- await apiRequest(`/api/admin/token/${encodeURIComponent(key)}`, 'DELETE');
+ await apiRequest(`/api/admin/token/${encodeURIComponent(key)}`, 'DELETE', null, true, '正在删除令牌...');
showAlert('令牌删除成功!', 'success');
loadTokens();
} catch (error) {
@@ -1197,8 +2008,8 @@
-
-
+
+
@@ -1208,64 +2019,289 @@
// === PRICING FUNCTIONS ===
- // Pricing functions
- async function loadPricing() {
- try {
- const pricing = await apiRequest('/api/admin/pricing');
- // 成功时显示表单
- document.getElementById('pricingContent').innerHTML = `
-
-
-
-
- 配置格式:模型名称 -> {input: 输入倍率, output: 输出倍率}。倍率基于1百万token=$1.00计算
-
-
- `;
- document.getElementById('pricingConfig').value = JSON.stringify(pricing, null, 2);
- } catch (error) {
- // 失败时显示错误信息,参考频道管理的方式
- document.getElementById('pricingContent').innerHTML = `
加载定价配置失败: ${error.message}
`;
+ // 切换定价编辑模式
+ function togglePricingEditMode() {
+ const tableMode = document.getElementById('pricingTableMode');
+ const jsonMode = document.getElementById('pricingJsonMode');
+ const toggleBtn = document.getElementById('pricingModeToggle');
+ const addBtn = document.getElementById('addPricingModelBtn');
+
+ if (pricingEditMode === 'table') {
+ // 切换到JSON模式
+ const config = collectPricingTableData();
+ document.getElementById('pricingConfigJson').value = JSON.stringify(config, null, 2);
+
+ tableMode.style.display = 'none';
+ jsonMode.style.display = 'block';
+ if (addBtn) addBtn.style.display = 'none'; // 隐藏添加模型按钮
+ toggleBtn.textContent = '📊 切换到表格模式';
+ pricingEditMode = 'json';
+ } else {
+ // 切换到表格模式
+ try {
+ const jsonValue = document.getElementById('pricingConfigJson').value;
+ if (jsonValue) {
+ const parsed = JSON.parse(jsonValue);
+ displayPricingTable(parsed);
+ }
+ } catch (error) {
+ console.warn('JSON格式错误,无法转换到表格模式:', error);
+ }
+
+ tableMode.style.display = 'block';
+ jsonMode.style.display = 'none';
+ if (addBtn) addBtn.style.display = 'inline-block'; // 显示添加模型按钮
+ toggleBtn.textContent = '📝 切换到JSON模式';
+ pricingEditMode = 'table';
}
}
- async function updatePricing() {
- const pricingConfigElement = document.getElementById('pricingConfig');
+ // 收集定价表格数据
+ function collectPricingTableData() {
+ const tbody = document.getElementById('pricingTableBody');
+ const config = {};
- // 如果因为加载失败导致textarea不存在,先重新创建表单
- if (!pricingConfigElement) {
- document.getElementById('pricingContent').innerHTML = `
-
-
-
-
- 配置格式:模型名称 -> {input: 输入倍率, output: 输出倍率}。倍率基于1百万token=$1.00计算
-
-
- `;
+ if (!tbody) return config;
+
+ Array.from(tbody.children).forEach((row) => {
+ const inputs = row.querySelectorAll('input');
+ if (inputs.length < 3) return;
+
+ const modelName = inputs[0].value.trim();
+ const inputPrice = parseFloat(inputs[1].value) || 0;
+ const outputPrice = parseFloat(inputs[2].value) || 0;
+
+ if (modelName) {
+ config[modelName] = {
+ input: inputPrice,
+ output: outputPrice
+ };
+ }
+ });
+
+ return config;
+ }
+
+ // 添加新的定价模型行
+ function addPricingModel() {
+ const tbody = document.getElementById('pricingTableBody');
+ const rowIndex = tbody.children.length;
+
+ const row = document.createElement('tr');
+ row.style.borderBottom = '1px solid #e9ecef';
+ row.innerHTML = `
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ `;
+ tbody.appendChild(row);
+ }
+
+ // 删除定价模型行
+ function removePricingRow(button) {
+ const row = button.closest('tr');
+ row.remove();
+ }
+
+ // 验证定价行数据
+ function validatePricingRow(rowIndex) {
+ // 这里可以添加验证逻辑,比如检查模型名称是否重复等
+ }
+
+ // Pricing functions
+ async function loadPricing() {
+ try {
+ const pricing = await apiRequest('/api/admin/pricing', 'GET', null, true);
+ const addBtn = document.getElementById('addPricingModelBtn');
+
+ if (pricingEditMode === 'table') {
+ displayPricingTable(pricing);
+ if (addBtn) addBtn.style.display = 'inline-block';
+ } else {
+ document.getElementById('pricingConfigJson').value = JSON.stringify(pricing, null, 2);
+ if (addBtn) addBtn.style.display = 'none';
+ }
+ } catch (error) {
+ // 失败时显示错误信息
+ if (pricingEditMode === 'table') {
+ document.getElementById('pricingContent').innerHTML = `
+
加载定价配置失败: ${error.message}
+
+
+
+
+ | 模型名称 |
+ 输入倍率 |
+ 输出倍率 |
+ 操作 |
+
+
+
+
+
+
+
+
+
+
+
+
+ 配置格式:模型名称 -> {input: 输入倍率, output: 输出倍率}。倍率基于1百万token=$1.00计算
+
+
+
+ `;
+ } else {
+ showAlert(`加载定价配置失败: ${error.message}`, 'error');
+ }
}
+ }
- const configText = document.getElementById('pricingConfig').value;
+ // 显示定价配置表格
+ function displayPricingTable(pricing) {
+ const tbody = document.getElementById('pricingTableBody');
+ if (!tbody) return;
- if (!configText) {
- showAlert('请填写定价配置', 'error');
+ tbody.innerHTML = '';
+
+ // 如果没有定价数据,添加一行空白行
+ if (!pricing || Object.keys(pricing).length === 0) {
+ addPricingModel();
return;
}
+ // 为每个模型创建一行
+ Object.entries(pricing).forEach(([modelName, modelPricing]) => {
+ const row = document.createElement('tr');
+ row.style.borderBottom = '1px solid #e9ecef';
+
+ const inputPrice = modelPricing.input || 0;
+ const outputPrice = modelPricing.output || 0;
+
+ row.innerHTML = `
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ `;
+ tbody.appendChild(row);
+ });
+ }
+
+ async function updatePricing() {
+ let config;
+
+ if (pricingEditMode === 'table') {
+ // 表格模式
+ const tbody = document.getElementById('pricingTableBody');
+
+ if (!tbody) {
+ showAlert('定价表格未找到,请刷新页面重试', 'error');
+ return;
+ }
+
+ config = {};
+ let hasError = false;
+ let errorMessage = '';
+
+ // 遍历所有行收集数据
+ Array.from(tbody.children).forEach((row, index) => {
+ const inputs = row.querySelectorAll('input');
+ if (inputs.length < 3) return;
+
+ const modelName = inputs[0].value.trim();
+ const inputPrice = parseFloat(inputs[1].value);
+ const outputPrice = parseFloat(inputs[2].value);
+
+ // 验证数据
+ if (!modelName) {
+ hasError = true;
+ errorMessage = `第 ${index + 1} 行:模型名称不能为空`;
+ return;
+ }
+
+ if (isNaN(inputPrice) || inputPrice < 0) {
+ hasError = true;
+ errorMessage = `第 ${index + 1} 行:输入倍率必须是有效的非负数`;
+ return;
+ }
+
+ if (isNaN(outputPrice) || outputPrice < 0) {
+ hasError = true;
+ errorMessage = `第 ${index + 1} 行:输出倍率必须是有效的非负数`;
+ return;
+ }
+
+ // 检查模型名称是否重复
+ if (config[modelName]) {
+ hasError = true;
+ errorMessage = `模型名称 "${modelName}" 重复`;
+ return;
+ }
+
+ config[modelName] = {
+ input: inputPrice,
+ output: outputPrice
+ };
+ });
+
+ if (hasError) {
+ showAlert(errorMessage, 'error');
+ return;
+ }
+
+ if (Object.keys(config).length === 0) {
+ showAlert('请至少添加一个模型的定价配置', 'error');
+ return;
+ }
+ } else {
+ // JSON模式
+ const configText = document.getElementById('pricingConfigJson').value.trim();
+
+ if (!configText) {
+ showAlert('请填写定价配置', 'error');
+ return;
+ }
+
+ try {
+ config = JSON.parse(configText);
+ } catch (error) {
+ showAlert('配置格式错误,请检查JSON格式', 'error');
+ return;
+ }
+ }
+
try {
- const config = JSON.parse(configText);
- await apiRequest('/api/admin/pricing', 'POST', config);
+ await apiRequest('/api/admin/pricing', 'POST', config, true, '正在更新定价配置...');
showAlert('定价配置更新成功!', 'success');
// 更新成功后重新加载配置
loadPricing();
} catch (error) {
- if (error.message.includes('JSON')) {
- showAlert('配置格式错误,请检查JSON格式', 'error');
- } else {
- showAlert(`更新定价配置失败: ${error.message}`, 'error');
- }
+ showAlert(`更新定价配置失败: ${error.message}`, 'error');
}
}
@@ -1366,6 +2402,7 @@
}
// === UTILITY FUNCTIONS ===
+
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showAlert('已复制到剪贴板', 'success', 2000);
@@ -1399,32 +2436,129 @@
}, duration);
}
- function showLoading(message) {
- const loadingDiv = document.createElement('div');
- loadingDiv.id = 'loadingOverlay';
- loadingDiv.className = 'loading';
- loadingDiv.innerHTML = `
-
-
${message}
- `;
+ // Loading 管理对象
+ const LoadingManager = {
+ delayTimer: null,
+ minShowTimer: null,
+ loadingElement: null,
+ showStartTime: null,
+ showDelay: 200, // 延迟 200ms 显示,避免快速请求的闪烁
+ minShowDuration: 500, // 最小显示时长 500ms,避免一闪而过
- document.body.appendChild(loadingDiv);
+ show(message) {
+ // 如果已存在 loading,先清理
+ this.hide();
+
+ // 设置延迟显示
+ this.delayTimer = setTimeout(() => {
+ this._showLoading(message);
+ }, this.showDelay);
+ },
+
+ _showLoading(message) {
+ const mainContent = document.getElementById('mainContent');
+ if (!mainContent) return;
+
+ // 记录显示开始时间
+ this.showStartTime = Date.now();
+
+ // 添加 loading 状态类
+ mainContent.classList.add('loading-active');
+
+ // 创建 loading 元素
+ const loadingDiv = document.createElement('div');
+ loadingDiv.id = 'loadingOverlay';
+ loadingDiv.innerHTML = `
+
+
${message}
+ `;
+
+ // 初始设置为透明,准备淡入动画
+ loadingDiv.style.opacity = '0';
+ mainContent.appendChild(loadingDiv);
+
+ this.loadingElement = loadingDiv;
+
+ // 触发淡入动画
+ requestAnimationFrame(() => {
+ loadingDiv.style.opacity = '1';
+ });
+ },
+
+ hide() {
+ // 清除延迟显示定时器
+ if (this.delayTimer) {
+ clearTimeout(this.delayTimer);
+ this.delayTimer = null;
+ }
+
+ // 如果 loading 还没有显示,直接返回
+ if (!this.loadingElement) {
+ return;
+ }
+
+ // 计算已显示时长
+ const showDuration = Date.now() - this.showStartTime;
+ const remainingMinShowTime = Math.max(0, this.minShowDuration - showDuration);
+
+ // 如果还没达到最小显示时长,延迟隐藏
+ if (remainingMinShowTime > 0) {
+ this.minShowTimer = setTimeout(() => {
+ this._hideLoading();
+ }, remainingMinShowTime);
+ } else {
+ this._hideLoading();
+ }
+ },
+
+ _hideLoading() {
+ // 清除最小显示定时器
+ if (this.minShowTimer) {
+ clearTimeout(this.minShowTimer);
+ this.minShowTimer = null;
+ }
+
+ if (this.loadingElement) {
+ // 淡出动画
+ this.loadingElement.style.opacity = '0';
+
+ // 等待动画完成后移除元素
+ setTimeout(() => {
+ if (this.loadingElement) {
+ this.loadingElement.remove();
+ this.loadingElement = null;
+ }
+
+ // 移除 loading 状态类
+ const mainContent = document.getElementById('mainContent');
+ if (mainContent) {
+ mainContent.classList.remove('loading-active');
+ }
+ }, 300); // 与 CSS transition 时间匹配
+ }
+
+ this.showStartTime = null;
+ }
+ };
+
+ // 保持原有的函数接口,以兼容现有代码
+ function showLoading(message) {
+ LoadingManager.show(message);
}
function hideLoading() {
- const loadingDiv = document.getElementById('loadingOverlay');
- if (loadingDiv) {
- loadingDiv.remove();
- }
+ LoadingManager.hide();
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function () {
- console.log('Awsl One API 管理面板已加载');
const savedToken = localStorage.getItem('adminToken');
if (savedToken) {
adminToken = savedToken;
showMainContent();
+ } else {
+ // 默认显示首页(未登录状态)
+ showHomePage();
}
// 为登录输入框添加回车键监听
@@ -1437,6 +2571,24 @@
});
}
});
+
+ // 点击模态框外部关闭
+ window.onclick = function (event) {
+ const modal = document.getElementById('loginModal');
+ if (event.target == modal) {
+ closeLoginModal();
+ }
+ }
+
+ // 添加ESC键关闭模态框
+ document.addEventListener('keydown', function (event) {
+ if (event.key === 'Escape') {
+ const modal = document.getElementById('loginModal');
+ if (modal.style.display === 'block') {
+ closeLoginModal();
+ }
+ }
+ });