link0518 commited on
Commit
b88ce1b
·
0 Parent(s):
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +1 -0
  2. LICENSE +23 -0
  3. README.md +151 -0
  4. client/.gitignore +24 -0
  5. client/README.md +16 -0
  6. client/eslint.config.js +29 -0
  7. client/index.html +13 -0
  8. client/package-lock.json +0 -0
  9. client/package.json +40 -0
  10. client/postcss.config.js +6 -0
  11. client/public/vite.svg +1 -0
  12. client/src/App.css +42 -0
  13. client/src/App.jsx +55 -0
  14. client/src/assets/react.svg +1 -0
  15. client/src/components/AuthOverlay.jsx +137 -0
  16. client/src/components/CodeBlock.jsx +29 -0
  17. client/src/components/ErrorBoundary.jsx +50 -0
  18. client/src/components/Layout.jsx +120 -0
  19. client/src/context/AuthContext.jsx +83 -0
  20. client/src/index.css +103 -0
  21. client/src/lib/utils.js +6 -0
  22. client/src/main.jsx +16 -0
  23. client/src/pages/Dashboard.jsx +201 -0
  24. client/src/pages/Docs.jsx +132 -0
  25. client/src/pages/Keys.jsx +384 -0
  26. client/src/pages/Logs.jsx +113 -0
  27. client/src/pages/Monitor.jsx +183 -0
  28. client/src/pages/Settings.jsx +254 -0
  29. client/src/pages/Test.jsx +257 -0
  30. client/src/pages/Tokens.jsx +396 -0
  31. client/tailwind.config.js +55 -0
  32. client/vite.config.js +22 -0
  33. config.json +24 -0
  34. node_modules/.bin/crc32 +16 -0
  35. node_modules/.bin/crc32.cmd +17 -0
  36. node_modules/.bin/crc32.ps1 +28 -0
  37. node_modules/.bin/glob +16 -0
  38. node_modules/.bin/glob.cmd +17 -0
  39. node_modules/.bin/glob.ps1 +28 -0
  40. node_modules/.bin/mkdirp +16 -0
  41. node_modules/.bin/mkdirp.cmd +17 -0
  42. node_modules/.bin/mkdirp.ps1 +28 -0
  43. node_modules/.bin/node-which +16 -0
  44. node_modules/.bin/node-which.cmd +17 -0
  45. node_modules/.bin/node-which.ps1 +28 -0
  46. node_modules/.package-lock.json +1910 -0
  47. node_modules/@isaacs/cliui/LICENSE.txt +14 -0
  48. node_modules/@isaacs/cliui/README.md +143 -0
  49. node_modules/@isaacs/cliui/build/index.cjs +317 -0
  50. node_modules/@isaacs/cliui/build/index.d.cts +43 -0
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ data/*.json
LICENSE ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## 📜 许可证
2
+ 本项目采用 CC BY-NC-SA 4.0 协议,仅供学习使用,禁止商用
3
+
4
+ 知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议
5
+ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
6
+
7
+ 版权所有 © 2025 [liuw1535]
8
+
9
+ 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
10
+
11
+ 您可以自由地:
12
+ ✓ 共享 — 在任何媒介以任何形式复制、发行本作品
13
+ ✓ 演绎 — 修改、转换或以本作品为基础进行创作
14
+
15
+ 惟须遵守下列条件:
16
+ ✓ 署名 — 您必须给出适当的署名,提供指向本许可协议的链接
17
+ ✓ 非商业性使用 — 您不得将本作品用于商业目的
18
+ ✓ 相同方式共享 — 如果您再混合、转换或者基于本作品进行创作,
19
+ 您必须基于与原先许可协议相同的许可协议分发您贡献的作品
20
+
21
+ 完整协议文本请访问:
22
+ https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.zh-Hans
23
+
README.md ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Antigravity Gateway
2
+
3
+ <div align="center">
4
+ <img src="client/public/rocket.svg" width="120" alt="Antigravity Logo" />
5
+ <h3>Google Antigravity API to OpenAI Proxy</h3>
6
+ <p>
7
+ 将 Google Antigravity API 转换为 OpenAI 兼容格式的高性能网关服务。
8
+ <br />
9
+ 内置现代化管理后台,支持多账号轮询、Token 自动刷新、密钥管理与实时监控。
10
+ </p>
11
+ </div>
12
+
13
+ ---
14
+
15
+ ## ✨ 功能特性
16
+
17
+ ### 核心功能
18
+ - **OpenAI 兼容**: 完全兼容 OpenAI Chat Completions API 格式,无缝对接现有生态。
19
+ - **流式响应**: 支持 SSE (Server-Sent Events) 流式输出,体验流畅。
20
+ - **多模态支持**: 支持文本及 Base64 编码的图片输入 (GPT-4 Vision 兼容)。
21
+ - **工具调用**: 支持 Function Calling,扩展模型能力。
22
+
23
+ ### 增强特性
24
+ - **多账号池**: 支持配置多个 Google 账号,自动负载均衡与轮询。
25
+ - **Token 自动保活**: 内置 Token 刷新机制,自动处理过期与 403 错误。
26
+ - **高并发支持**: 优化的请求处理队列,支持高并发场景。
27
+
28
+ ### 管理后台 (Dashboard)
29
+ - **现代化 UI**: 基于 React + Tailwind CSS 构建的极简主义设计风格。
30
+ - **密钥管理**: 创建、删除、禁用 API Key,支持设置额度与过期时间。
31
+ - **Token 管理**: 可视化管理 Google 账号,实时查看 Token 状态。
32
+ - **系统监控**: 实时监控 CPU、内存、请求数与响应时间。
33
+ - **在线测试**: 内置 Chat 调试界面,方便测试模型效果。
34
+ - **日志审计**: 完整的请求日志记录与查询。
35
+
36
+ ## 🛠️ 技术栈
37
+
38
+ - **后端**: Node.js (Express), Native Fetch
39
+ - **前端**: React, Vite, Tailwind CSS, Framer Motion, Lucide React
40
+ - **数据存储**: 本地 JSON 文件存储 (轻量级,无外部数据库依赖)
41
+
42
+ ## 🚀 快速开始
43
+
44
+ ### 环境要求
45
+ - Node.js >= 18.0.0
46
+
47
+ ### 1. 安装与构建
48
+
49
+ ```bash
50
+ # 安装项目依赖
51
+ npm install
52
+
53
+ # 构建前端资源
54
+ npm run build
55
+ ```
56
+
57
+ ### 2. 配置服务
58
+
59
+ 编辑根目录下的 `config.json` 文件:
60
+
61
+ ```json
62
+ {
63
+ "server": {
64
+ "port": 8045, // 服务端口
65
+ "host": "0.0.0.0" // 监听地址
66
+ },
67
+ "security": {
68
+ "apiKey": "sk-admin", // 管理员/默认 API Key
69
+ "maxRequestSize": "50mb" // 最大请求体大小
70
+ },
71
+ "defaults": {
72
+ "model": "gemini-2.0-flash-exp" // 默认模型
73
+ }
74
+ }
75
+ ```
76
+
77
+ ### 3. 添加 Google 账号
78
+
79
+ 运行 OAuth 登录脚本获取 Access Token:
80
+
81
+ ```bash
82
+ npm run login
83
+ ```
84
+ 按提示在浏览器中授权,获取的 Token 将自动保存到 `data/accounts.json`。
85
+
86
+ ### 4. 启动服务
87
+
88
+ ```bash
89
+ # 生产模式
90
+ npm start
91
+
92
+ # 开发模式 (支持热重载)
93
+ npm run dev
94
+ ```
95
+
96
+ 服务启动后,访问 `http://localhost:8045` 进入管理后台。
97
+
98
+ ## 🔌 API 使用指南
99
+
100
+ ### 基础 URL
101
+ `http://localhost:8045`
102
+
103
+ ### 认证
104
+ 所有请求需在 Header 中携带 API Key:
105
+ `Authorization: Bearer <YOUR_API_KEY>`
106
+
107
+ ### 1. 获取模型列表
108
+ `GET /v1/models`
109
+
110
+ ### 2. 聊天补全
111
+ `POST /v1/chat/completions`
112
+
113
+ **请求示例:**
114
+ ```bash
115
+ curl http://localhost:8045/v1/chat/completions \
116
+ -H "Content-Type: application/json" \
117
+ -H "Authorization: Bearer sk-admin" \
118
+ -d '{
119
+ "model": "gemini-2.0-flash-exp",
120
+ "messages": [{"role": "user", "content": "Hello!"}],
121
+ "stream": true
122
+ }'
123
+ ```
124
+
125
+ ## 📂 项目结构
126
+
127
+ ```
128
+ .
129
+ ├── client/ # 前端 React 项目
130
+ │ ├── src/
131
+ │ │ ├── components/ # UI 组件
132
+ │ │ ├── pages/ # 页面组件
133
+ │ │ └── ...
134
+ │ └── ...
135
+ ├── data/ # 数据存储目录
136
+ │ ├── accounts.json # Google 账号数据
137
+ │ ├── keys.json # API Key 数据
138
+ │ └── ...
139
+ ├── src/ # 后端源码
140
+ │ ├── server/ # 服务器入口
141
+ │ ├── api/ # API 路由处理
142
+ │ ├── auth/ # 认证与 Token 管理
143
+ │ └── ...
144
+ ├── scripts/ # 工具脚本
145
+ ├── config.json # 配置文件
146
+ └── package.json
147
+ ```
148
+
149
+ ## 📝 License
150
+
151
+ MIT License
client/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
client/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
client/eslint.config.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ ecmaVersion: 2020,
18
+ globals: globals.browser,
19
+ parserOptions: {
20
+ ecmaVersion: 'latest',
21
+ ecmaFeatures: { jsx: true },
22
+ sourceType: 'module',
23
+ },
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27
+ },
28
+ },
29
+ ])
client/index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>client</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.jsx"></script>
12
+ </body>
13
+ </html>
client/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
client/package.json ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "client",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "antigravity-to-openai": "file:..",
14
+ "clsx": "^2.1.1",
15
+ "framer-motion": "^12.23.24",
16
+ "lucide-react": "^0.554.0",
17
+ "react": "^19.2.0",
18
+ "react-dom": "^19.2.0",
19
+ "react-router-dom": "^7.9.6",
20
+ "tailwind-merge": "^3.4.0"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^9.39.1",
24
+ "@tailwindcss/postcss": "^4.1.17",
25
+ "@types/react": "^19.2.5",
26
+ "@types/react-dom": "^19.2.3",
27
+ "@vitejs/plugin-react": "^5.1.1",
28
+ "autoprefixer": "^10.4.22",
29
+ "eslint": "^9.39.1",
30
+ "eslint-plugin-react-hooks": "^7.0.1",
31
+ "eslint-plugin-react-refresh": "^0.4.24",
32
+ "globals": "^16.5.0",
33
+ "postcss": "^8.5.6",
34
+ "tailwindcss": "^4.1.17",
35
+ "vite": "npm:rolldown-vite@7.2.5"
36
+ },
37
+ "overrides": {
38
+ "vite": "npm:rolldown-vite@7.2.5"
39
+ }
40
+ }
client/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ autoprefixer: {},
5
+ },
6
+ }
client/public/vite.svg ADDED
client/src/App.css ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #root {
2
+ max-width: 1280px;
3
+ margin: 0 auto;
4
+ padding: 2rem;
5
+ text-align: center;
6
+ }
7
+
8
+ .logo {
9
+ height: 6em;
10
+ padding: 1.5em;
11
+ will-change: filter;
12
+ transition: filter 300ms;
13
+ }
14
+ .logo:hover {
15
+ filter: drop-shadow(0 0 2em #646cffaa);
16
+ }
17
+ .logo.react:hover {
18
+ filter: drop-shadow(0 0 2em #61dafbaa);
19
+ }
20
+
21
+ @keyframes logo-spin {
22
+ from {
23
+ transform: rotate(0deg);
24
+ }
25
+ to {
26
+ transform: rotate(360deg);
27
+ }
28
+ }
29
+
30
+ @media (prefers-reduced-motion: no-preference) {
31
+ a:nth-of-type(2) .logo {
32
+ animation: logo-spin infinite 20s linear;
33
+ }
34
+ }
35
+
36
+ .card {
37
+ padding: 2em;
38
+ }
39
+
40
+ .read-the-docs {
41
+ color: #888;
42
+ }
client/src/App.jsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Routes, Route, Navigate } from 'react-router-dom';
2
+ import { AuthProvider, useAuth } from './context/AuthContext';
3
+ import AuthOverlay from './components/AuthOverlay';
4
+ import Layout from './components/Layout';
5
+ import Dashboard from './pages/Dashboard';
6
+ import Tokens from './pages/Tokens';
7
+ import Keys from './pages/Keys';
8
+ import Test from './pages/Test';
9
+ import Docs from './pages/Docs';
10
+ import Logs from './pages/Logs';
11
+ import Monitor from './pages/Monitor';
12
+ import Settings from './pages/Settings';
13
+
14
+ function ProtectedRoute({ children }) {
15
+ const { isAuthenticated, isLoading } = useAuth();
16
+
17
+ if (isLoading) {
18
+ return (
19
+ <div className="min-h-screen flex items-center justify-center bg-slate-50">
20
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
21
+ </div>
22
+ );
23
+ }
24
+
25
+ if (!isAuthenticated) {
26
+ return <AuthOverlay />;
27
+ }
28
+
29
+ return children;
30
+ }
31
+
32
+ function App() {
33
+ return (
34
+ <AuthProvider>
35
+ <Routes>
36
+ <Route path="/" element={
37
+ <ProtectedRoute>
38
+ <Layout />
39
+ </ProtectedRoute>
40
+ }>
41
+ <Route index element={<Dashboard />} />
42
+ <Route path="tokens" element={<Tokens />} />
43
+ <Route path="keys" element={<Keys />} />
44
+ <Route path="test" element={<Test />} />
45
+ <Route path="docs" element={<Docs />} />
46
+ <Route path="logs" element={<Logs />} />
47
+ <Route path="monitor" element={<Monitor />} />
48
+ <Route path="settings" element={<Settings />} />
49
+ </Route>
50
+ </Routes>
51
+ </AuthProvider>
52
+ );
53
+ }
54
+
55
+ export default App;
client/src/assets/react.svg ADDED
client/src/components/AuthOverlay.jsx ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Loader2, ArrowRight, Command, ShieldCheck } from 'lucide-react';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { cn } from '../lib/utils';
6
+
7
+ export default function AuthOverlay() {
8
+ const { isAuthenticated, login } = useAuth();
9
+ const [password, setPassword] = useState('');
10
+ const [error, setError] = useState('');
11
+ const [isSubmitting, setIsSubmitting] = useState(false);
12
+
13
+ const handleLogin = async (e) => {
14
+ e.preventDefault();
15
+ if (!password) return;
16
+
17
+ setIsSubmitting(true);
18
+ setError('');
19
+
20
+ await new Promise(resolve => setTimeout(resolve, 600));
21
+
22
+ const result = await login(password);
23
+ if (!result.success) {
24
+ setError(result.error);
25
+ setPassword('');
26
+ }
27
+ setIsSubmitting(false);
28
+ };
29
+
30
+ if (isAuthenticated) return null;
31
+
32
+ return (
33
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-white">
34
+ {/* 极简网格背景 */}
35
+ <div className="absolute inset-0 subtle-grid opacity-40" />
36
+
37
+ {/* 顶部装饰 */}
38
+ <div className="absolute top-0 inset-x-0 h-px bg-gradient-to-r from-transparent via-zinc-200 to-transparent opacity-50" />
39
+
40
+ <motion.div
41
+ initial={{ opacity: 0, y: 10 }}
42
+ animate={{ opacity: 1, y: 0 }}
43
+ transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
44
+ className="relative w-full max-w-[360px] mx-auto z-10"
45
+ >
46
+ <div className="bg-white rounded-2xl border border-zinc-200 shadow-xl shadow-zinc-200/50 p-8">
47
+ <div className="text-center mb-8">
48
+ <div className="inline-flex items-center justify-center w-10 h-10 mb-4 rounded-lg bg-zinc-100 text-zinc-900 border border-zinc-200">
49
+ <Command className="w-5 h-5" />
50
+ </div>
51
+ <h1 className="text-xl font-semibold text-zinc-900 tracking-tight mb-1">
52
+ Antigravity
53
+ </h1>
54
+ <p className="text-zinc-500 text-sm">
55
+ 请输入访问密码
56
+ </p>
57
+ </div>
58
+
59
+ <form onSubmit={handleLogin} className="space-y-4">
60
+ <div className="space-y-1.5">
61
+ <div className="relative">
62
+ <input
63
+ type="password"
64
+ value={password}
65
+ onChange={(e) => {
66
+ setPassword(e.target.value);
67
+ if (error) setError('');
68
+ }}
69
+ placeholder="Password"
70
+ className={cn(
71
+ "w-full px-4 py-2.5 bg-zinc-50 border border-zinc-200 rounded-lg text-zinc-900 placeholder:text-zinc-400",
72
+ "focus:outline-none focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 transition-all duration-200",
73
+ "text-sm font-medium tracking-widest placeholder:tracking-normal",
74
+ error && "border-red-500 focus:border-red-500 focus:ring-red-500/5 bg-red-50/50 text-red-900 placeholder:text-red-300"
75
+ )}
76
+ autoFocus
77
+ />
78
+ <AnimatePresence>
79
+ {error && (
80
+ <motion.div
81
+ initial={{ opacity: 0, scale: 0.8 }}
82
+ animate={{ opacity: 1, scale: 1 }}
83
+ exit={{ opacity: 0, scale: 0.8 }}
84
+ className="absolute right-3 top-1/2 -translate-y-1/2"
85
+ >
86
+ <div className="w-1.5 h-1.5 rounded-full bg-red-500" />
87
+ </motion.div>
88
+ )}
89
+ </AnimatePresence>
90
+ </div>
91
+ <AnimatePresence>
92
+ {error && (
93
+ <motion.p
94
+ initial={{ opacity: 0, height: 0 }}
95
+ animate={{ opacity: 1, height: 'auto' }}
96
+ exit={{ opacity: 0, height: 0 }}
97
+ className="text-[11px] font-medium text-red-600 pl-1"
98
+ >
99
+ {error}
100
+ </motion.p>
101
+ )}
102
+ </AnimatePresence>
103
+ </div>
104
+
105
+ <button
106
+ type="submit"
107
+ disabled={isSubmitting || !password}
108
+ className={cn(
109
+ "w-full flex items-center justify-center gap-2 py-2.5 px-4 rounded-lg",
110
+ "bg-zinc-900 text-white font-medium text-sm",
111
+ "hover:bg-zinc-800 active:scale-[0.98] transition-all duration-200",
112
+ "disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100",
113
+ "shadow-sm hover:shadow-md"
114
+ )}
115
+ >
116
+ {isSubmitting ? (
117
+ <Loader2 className="w-4 h-4 animate-spin" />
118
+ ) : (
119
+ <>
120
+ <span>进入系统</span>
121
+ <ArrowRight className="w-4 h-4 opacity-50" />
122
+ </>
123
+ )}
124
+ </button>
125
+ </form>
126
+ </div>
127
+
128
+ <div className="mt-8 text-center">
129
+ <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-zinc-100/50 border border-zinc-200/50 text-[10px] font-medium text-zinc-400">
130
+ <ShieldCheck className="w-3 h-3" />
131
+ <span>SECURE GATEWAY</span>
132
+ </div>
133
+ </div>
134
+ </motion.div>
135
+ </div>
136
+ );
137
+ }
client/src/components/CodeBlock.jsx ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { Check, Copy } from 'lucide-react';
3
+ import { cn } from '../lib/utils';
4
+
5
+ export default function CodeBlock({ code, language = 'bash' }) {
6
+ const [copied, setCopied] = useState(false);
7
+
8
+ const copy = () => {
9
+ navigator.clipboard.writeText(code);
10
+ setCopied(true);
11
+ setTimeout(() => setCopied(false), 2000);
12
+ };
13
+
14
+ return (
15
+ <div className="relative group rounded-xl overflow-hidden bg-slate-900 border border-slate-800">
16
+ <div className="absolute right-3 top-3 opacity-0 group-hover:opacity-100 transition-opacity">
17
+ <button
18
+ onClick={copy}
19
+ className="p-1.5 text-slate-400 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
20
+ >
21
+ {copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
22
+ </button>
23
+ </div>
24
+ <pre className="p-4 overflow-x-auto text-sm font-mono text-slate-300 leading-relaxed">
25
+ <code>{code}</code>
26
+ </pre>
27
+ </div>
28
+ );
29
+ }
client/src/components/ErrorBoundary.jsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ class ErrorBoundary extends React.Component {
4
+ constructor(props) {
5
+ super(props);
6
+ this.state = { hasError: false, error: null, errorInfo: null };
7
+ }
8
+
9
+ static getDerivedStateFromError(error) {
10
+ return { hasError: true };
11
+ }
12
+
13
+ componentDidCatch(error, errorInfo) {
14
+ this.setState({ error, errorInfo });
15
+ console.error("Uncaught error:", error, errorInfo);
16
+ }
17
+
18
+ render() {
19
+ if (this.state.hasError) {
20
+ return (
21
+ <div className="min-h-screen flex items-center justify-center bg-red-50 p-4">
22
+ <div className="bg-white p-8 rounded-xl shadow-xl max-w-2xl w-full border border-red-100">
23
+ <h1 className="text-2xl font-bold text-red-600 mb-4">Something went wrong</h1>
24
+ <div className="bg-red-50 p-4 rounded-lg mb-4 overflow-auto max-h-60">
25
+ <code className="text-red-800 text-sm font-mono whitespace-pre-wrap">
26
+ {this.state.error && this.state.error.toString()}
27
+ </code>
28
+ </div>
29
+ <details className="text-sm text-slate-500">
30
+ <summary className="cursor-pointer mb-2 font-medium">Stack Trace</summary>
31
+ <pre className="bg-slate-50 p-4 rounded-lg overflow-auto max-h-60 text-xs font-mono">
32
+ {this.state.errorInfo && this.state.errorInfo.componentStack}
33
+ </pre>
34
+ </details>
35
+ <button
36
+ onClick={() => window.location.reload()}
37
+ className="mt-6 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
38
+ >
39
+ Reload Page
40
+ </button>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
45
+
46
+ return this.props.children;
47
+ }
48
+ }
49
+
50
+ export default ErrorBoundary;
client/src/components/Layout.jsx ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react';
2
+ import { NavLink, Outlet, useLocation } from 'react-router-dom';
3
+ import { motion } from 'framer-motion';
4
+ import {
5
+ LayoutDashboard, Key, Ticket, TestTube2, FileText,
6
+ ScrollText, Activity, Settings, LogOut, Menu, Rocket
7
+ } from 'lucide-react';
8
+ import { useAuth } from '../context/AuthContext';
9
+ import { cn } from '../lib/utils';
10
+
11
+ const navItems = [
12
+ { icon: LayoutDashboard, label: '首页', path: '/' },
13
+ { icon: Ticket, label: 'Token 管理', path: '/tokens' },
14
+ { icon: Key, label: '密钥管理', path: '/keys' },
15
+ { icon: TestTube2, label: 'API 测试', path: '/test' },
16
+ { icon: FileText, label: 'API 文档', path: '/docs' },
17
+ { icon: ScrollText, label: '日志查看', path: '/logs' },
18
+ { icon: Activity, label: '系统监控', path: '/monitor' },
19
+ { icon: Settings, label: '系统设置', path: '/settings' },
20
+ ];
21
+
22
+ export default function Layout() {
23
+ const { logout } = useAuth();
24
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
25
+ const location = useLocation();
26
+
27
+ return (
28
+ <div className="min-h-screen bg-white flex">
29
+ {/* Mobile Menu Overlay */}
30
+ {isMobileMenuOpen && (
31
+ <div
32
+ className="fixed inset-0 bg-black/5 backdrop-blur-sm z-40 lg:hidden"
33
+ onClick={() => setIsMobileMenuOpen(false)}
34
+ />
35
+ )}
36
+
37
+ {/* Sidebar */}
38
+ <aside className={cn(
39
+ "fixed lg:sticky top-0 left-0 z-50 h-screen w-64 bg-zinc-50/50 border-r border-zinc-200 transition-transform duration-300 ease-in-out lg:translate-x-0 flex flex-col",
40
+ isMobileMenuOpen ? "translate-x-0" : "-translate-x-full"
41
+ )}>
42
+ <div className="p-6 flex items-center gap-3">
43
+ <div className="w-8 h-8 rounded-lg bg-zinc-900 flex items-center justify-center text-white shadow-sm">
44
+ <Rocket className="w-4 h-4" />
45
+ </div>
46
+ <div>
47
+ <h1 className="font-semibold text-zinc-900 text-sm leading-tight tracking-tight">Antigravity</h1>
48
+ <p className="text-[10px] text-zinc-500 font-medium tracking-wide uppercase">Gateway</p>
49
+ </div>
50
+ </div>
51
+
52
+ <nav className="flex-1 overflow-y-auto px-4 py-4 space-y-1">
53
+ {navItems.map((item) => (
54
+ <NavLink
55
+ key={item.path}
56
+ to={item.path}
57
+ onClick={() => setIsMobileMenuOpen(false)}
58
+ className={({ isActive }) => cn(
59
+ "flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200",
60
+ isActive
61
+ ? "bg-white text-zinc-900 shadow-sm border border-zinc-200/50"
62
+ : "text-zinc-500 hover:bg-zinc-100 hover:text-zinc-900"
63
+ )}
64
+ >
65
+ {({ isActive }) => (
66
+ <>
67
+ <item.icon className={cn("w-5 h-5", isActive ? "text-zinc-900" : "text-zinc-400")} />
68
+ {item.label}
69
+ </>
70
+ )}
71
+ </NavLink>
72
+ ))}
73
+ </nav>
74
+
75
+ <div className="p-4 border-t border-zinc-200">
76
+ <button
77
+ onClick={logout}
78
+ className="flex items-center gap-3 px-4 py-3 w-full rounded-xl text-sm font-medium text-zinc-500 hover:bg-red-50 hover:text-red-600 transition-colors"
79
+ >
80
+ <LogOut className="w-5 h-5" />
81
+ 退出登录
82
+ </button>
83
+ </div>
84
+ </aside>
85
+
86
+ {/* Main Content */}
87
+ <main className="flex-1 min-w-0 flex flex-col h-screen overflow-hidden bg-white">
88
+ {/* Mobile Header */}
89
+ <header className="lg:hidden h-14 bg-white border-b border-zinc-200 flex items-center justify-between px-4 sticky top-0 z-30">
90
+ <div className="flex items-center gap-3">
91
+ <div className="w-8 h-8 rounded-lg bg-zinc-900 flex items-center justify-center text-white">
92
+ <Rocket className="w-4 h-4" />
93
+ </div>
94
+ <span className="font-semibold text-zinc-900">Antigravity</span>
95
+ </div>
96
+ <button
97
+ onClick={() => setIsMobileMenuOpen(true)}
98
+ className="p-2 rounded-lg hover:bg-zinc-100 text-zinc-500"
99
+ >
100
+ <Menu className="w-5 h-5" />
101
+ </button>
102
+ </header>
103
+
104
+ {/* Page Content */}
105
+ <div className="flex-1 overflow-y-auto p-4 lg:p-10 scroll-smooth">
106
+ <div className="max-w-5xl mx-auto">
107
+ <motion.div
108
+ key={location.pathname}
109
+ initial={{ opacity: 0, y: 10 }}
110
+ animate={{ opacity: 1, y: 0 }}
111
+ transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
112
+ >
113
+ <Outlet />
114
+ </motion.div>
115
+ </div>
116
+ </div>
117
+ </main>
118
+ </div>
119
+ );
120
+ }
client/src/context/AuthContext.jsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createContext, useContext, useState, useEffect } from 'react';
2
+
3
+ const AuthContext = createContext(null);
4
+
5
+ export function AuthProvider({ children }) {
6
+ const [token, setToken] = useState(localStorage.getItem('adminToken') || '');
7
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
8
+ const [isLoading, setIsLoading] = useState(true);
9
+
10
+ useEffect(() => {
11
+ verifySession();
12
+ }, [token]);
13
+
14
+ const verifySession = async () => {
15
+ if (!token) {
16
+ setIsAuthenticated(false);
17
+ setIsLoading(false);
18
+ return;
19
+ }
20
+
21
+ try {
22
+ const response = await fetch('/admin/verify', {
23
+ headers: { 'X-Admin-Token': token }
24
+ });
25
+
26
+ if (response.ok) {
27
+ setIsAuthenticated(true);
28
+ } else {
29
+ logout();
30
+ }
31
+ } catch (error) {
32
+ console.error('Session verification failed:', error);
33
+ setIsAuthenticated(false); // Don't logout immediately on network error, but for now safe
34
+ } finally {
35
+ setIsLoading(false);
36
+ }
37
+ };
38
+
39
+ const login = async (password) => {
40
+ try {
41
+ const response = await fetch('/admin/login', {
42
+ method: 'POST',
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ password })
45
+ });
46
+
47
+ const data = await response.json();
48
+
49
+ if (response.ok && data.success) {
50
+ setToken(data.token);
51
+ localStorage.setItem('adminToken', data.token);
52
+ setIsAuthenticated(true);
53
+ return { success: true };
54
+ } else {
55
+ return { success: false, error: data.error || 'Login failed' };
56
+ }
57
+ } catch (error) {
58
+ return { success: false, error: error.message };
59
+ }
60
+ };
61
+
62
+ const logout = async () => {
63
+ try {
64
+ await fetch('/admin/logout', {
65
+ method: 'POST',
66
+ headers: { 'X-Admin-Token': token }
67
+ });
68
+ } catch (e) {
69
+ // Ignore logout errors
70
+ }
71
+ setToken('');
72
+ localStorage.removeItem('adminToken');
73
+ setIsAuthenticated(false);
74
+ };
75
+
76
+ return (
77
+ <AuthContext.Provider value={{ token, isAuthenticated, isLoading, login, logout }}>
78
+ {children}
79
+ </AuthContext.Provider>
80
+ );
81
+ }
82
+
83
+ export const useAuth = () => useContext(AuthContext);
client/src/index.css ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ @layer base {
4
+ :root {
5
+ /* Zinc-based Light Theme */
6
+ --background: 0 0% 100%;
7
+ --foreground: 240 10% 3.9%;
8
+
9
+ --card: 0 0% 100%;
10
+ --card-foreground: 240 10% 3.9%;
11
+
12
+ --popover: 0 0% 100%;
13
+ --popover-foreground: 240 10% 3.9%;
14
+
15
+ --primary: 240 5.9% 10%;
16
+ --primary-foreground: 0 0% 98%;
17
+
18
+ --secondary: 240 4.8% 95.9%;
19
+ --secondary-foreground: 240 5.9% 10%;
20
+
21
+ --muted: 240 4.8% 95.9%;
22
+ --muted-foreground: 240 3.8% 46.1%;
23
+
24
+ --accent: 240 4.8% 95.9%;
25
+ --accent-foreground: 240 5.9% 10%;
26
+
27
+ --destructive: 0 84.2% 60.2%;
28
+ --destructive-foreground: 0 0% 98%;
29
+
30
+ --border: 240 5.9% 90%;
31
+ --input: 240 5.9% 90%;
32
+ --ring: 240 10% 3.9%;
33
+
34
+ --radius: 0.75rem;
35
+ }
36
+
37
+ .dark {
38
+ /* Dark mode is currently disabled/hidden in this redesign,
39
+ but keeping variables just in case */
40
+ --background: 240 10% 3.9%;
41
+ --foreground: 0 0% 98%;
42
+ --card: 240 10% 3.9%;
43
+ --card-foreground: 0 0% 98%;
44
+ --popover: 240 10% 3.9%;
45
+ --popover-foreground: 0 0% 98%;
46
+ --primary: 0 0% 98%;
47
+ --primary-foreground: 240 5.9% 10%;
48
+ --secondary: 240 3.7% 15.9%;
49
+ --secondary-foreground: 0 0% 98%;
50
+ --muted: 240 3.7% 15.9%;
51
+ --muted-foreground: 240 5% 64.9%;
52
+ --accent: 240 3.7% 15.9%;
53
+ --accent-foreground: 0 0% 98%;
54
+ --destructive: 0 62.8% 30.6%;
55
+ --destructive-foreground: 0 0% 98%;
56
+ --border: 240 3.7% 15.9%;
57
+ --input: 240 3.7% 15.9%;
58
+ --ring: 240 4.9% 83.9%;
59
+ }
60
+ }
61
+
62
+ @layer base {
63
+ * {
64
+ border-color: hsl(var(--border));
65
+ }
66
+
67
+ body {
68
+ background-color: hsl(var(--background));
69
+ color: hsl(var(--foreground));
70
+ @apply antialiased;
71
+ font-feature-settings: "rlig" 1, "calt" 1;
72
+ }
73
+ }
74
+
75
+ /* Modern Utilities */
76
+ .glass {
77
+ @apply bg-white/80 backdrop-blur-xl border border-zinc-200/50 shadow-sm;
78
+ }
79
+
80
+ .glass-card {
81
+ @apply bg-white border border-zinc-200 shadow-sm hover:shadow-md transition-all duration-300;
82
+ }
83
+
84
+ .subtle-grid {
85
+ background-size: 20px 20px;
86
+ background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
87
+ linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
88
+ mask-image: linear-gradient(to bottom, black 40%, transparent 100%);
89
+ }
90
+
91
+ /* Custom Scrollbar */
92
+ ::-webkit-scrollbar {
93
+ width: 6px;
94
+ height: 6px;
95
+ }
96
+
97
+ ::-webkit-scrollbar-track {
98
+ @apply bg-transparent;
99
+ }
100
+
101
+ ::-webkit-scrollbar-thumb {
102
+ @apply bg-zinc-200 rounded-full hover:bg-zinc-300 transition-colors;
103
+ }
client/src/lib/utils.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs) {
5
+ return twMerge(clsx(inputs))
6
+ }
client/src/main.jsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import { BrowserRouter } from 'react-router-dom'
4
+ import App from './App.jsx'
5
+ import ErrorBoundary from './components/ErrorBoundary.jsx'
6
+ import './index.css'
7
+
8
+ ReactDOM.createRoot(document.getElementById('root')).render(
9
+ <React.StrictMode>
10
+ <ErrorBoundary>
11
+ <BrowserRouter>
12
+ <App />
13
+ </BrowserRouter>
14
+ </ErrorBoundary>
15
+ </React.StrictMode>,
16
+ )
client/src/pages/Dashboard.jsx ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { motion } from 'framer-motion';
4
+ import { Activity, Users, Key, BarChart3, ArrowRight, CheckCircle2, XCircle } from 'lucide-react';
5
+ import { useAuth } from '../context/AuthContext';
6
+
7
+ export default function Dashboard() {
8
+ const { token } = useAuth();
9
+ const [stats, setStats] = useState({
10
+ keys: 0,
11
+ tokens: 0,
12
+ keyRequests: 0,
13
+ tokenEnabled: 0,
14
+ tokenDisabled: 0,
15
+ todayRequests: 0
16
+ });
17
+ const [isLoading, setIsLoading] = useState(true);
18
+
19
+ useEffect(() => {
20
+ const fetchData = async () => {
21
+ try {
22
+ const headers = { 'X-Admin-Token': token };
23
+ const [keysRes, tokensRes, keyStatsRes, tokenStatsRes] = await Promise.all([
24
+ fetch('/admin/keys', { headers }),
25
+ fetch('/admin/tokens', { headers }),
26
+ fetch('/admin/keys/stats', { headers }),
27
+ fetch('/admin/tokens/stats', { headers })
28
+ ]);
29
+
30
+ const keys = await keysRes.json();
31
+ const tokens = await tokensRes.json();
32
+ const keyStats = await keyStatsRes.json();
33
+ const tokenStats = await tokenStatsRes.json();
34
+
35
+ setStats({
36
+ keys: keys.length,
37
+ tokens: tokens.length,
38
+ keyRequests: keyStats.totalRequests || 0,
39
+ tokenEnabled: tokenStats.enabled || 0,
40
+ tokenDisabled: tokenStats.disabled || 0,
41
+ todayRequests: 0
42
+ });
43
+ } catch (error) {
44
+ console.error('Failed to fetch dashboard data', error);
45
+ } finally {
46
+ setIsLoading(false);
47
+ }
48
+ };
49
+
50
+ fetchData();
51
+ }, [token]);
52
+
53
+ const container = {
54
+ hidden: { opacity: 0 },
55
+ show: {
56
+ opacity: 1,
57
+ transition: {
58
+ staggerChildren: 0.1
59
+ }
60
+ }
61
+ };
62
+
63
+ const item = {
64
+ hidden: { opacity: 0, y: 20 },
65
+ show: { opacity: 1, y: 0 }
66
+ };
67
+
68
+ return (
69
+ <motion.div
70
+ variants={container}
71
+ initial="hidden"
72
+ animate="show"
73
+ className="space-y-8"
74
+ >
75
+ {/* Welcome Section */}
76
+ <motion.div variants={item} className="relative overflow-hidden rounded-2xl bg-zinc-900 p-8 text-white shadow-lg">
77
+ <div className="absolute top-0 right-0 -mt-4 -mr-4 h-32 w-32 rounded-full bg-zinc-800 blur-2xl opacity-50" />
78
+ <div className="absolute bottom-0 left-0 -mb-4 -ml-4 h-32 w-32 rounded-full bg-zinc-800 blur-2xl opacity-50" />
79
+
80
+ <div className="relative z-10">
81
+ <h2 className="text-2xl font-semibold mb-2 tracking-tight">欢迎回来, 管理员</h2>
82
+ <p className="text-zinc-400 text-base max-w-2xl">
83
+ Antigravity API 网关正在平稳运行。这里是您的控制中心,您可以管理 Token、密钥并监控系统状态。
84
+ </p>
85
+ </div>
86
+ </motion.div>
87
+
88
+ {/* Stats Grid */}
89
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
90
+ <StatsCard
91
+ title="服务状态"
92
+ value="运行中"
93
+ icon={Activity}
94
+ color="text-emerald-600"
95
+ bg="bg-emerald-50"
96
+ subtext="系统正常"
97
+ />
98
+ <StatsCard
99
+ title="Token 账号"
100
+ value={stats.tokens}
101
+ icon={Users}
102
+ color="text-blue-600"
103
+ bg="bg-blue-50"
104
+ subtext={
105
+ <div className="flex gap-2 text-xs">
106
+ <span className="text-emerald-600 flex items-center gap-1"><CheckCircle2 className="w-3 h-3" /> {stats.tokenEnabled}</span>
107
+ <span className="text-zinc-400 flex items-center gap-1"><XCircle className="w-3 h-3" /> {stats.tokenDisabled}</span>
108
+ </div>
109
+ }
110
+ />
111
+ <StatsCard
112
+ title="API 密钥"
113
+ value={stats.keys}
114
+ icon={Key}
115
+ color="text-violet-600"
116
+ bg="bg-violet-50"
117
+ subtext={`总请求: ${stats.keyRequests}`}
118
+ />
119
+ <StatsCard
120
+ title="今日请求"
121
+ value={stats.todayRequests}
122
+ icon={BarChart3}
123
+ color="text-orange-600"
124
+ bg="bg-orange-50"
125
+ subtext="实时统计"
126
+ />
127
+ </div>
128
+
129
+ {/* Quick Start */}
130
+ <motion.div variants={item} className="bg-white rounded-xl border border-zinc-200 p-8 shadow-sm">
131
+ <h3 className="text-lg font-semibold text-zinc-900 mb-6">快速开始指南</h3>
132
+ <div className="grid gap-6 md:grid-cols-2">
133
+ <QuickStartStep
134
+ number="01"
135
+ title="获取 Google Token"
136
+ desc="在 Token 管理页面登录 Google 账号获取 Access Token,这是服务的核心凭证。"
137
+ link="/tokens"
138
+ />
139
+ <QuickStartStep
140
+ number="02"
141
+ title="生成 API 密钥"
142
+ desc="在密钥管理页面生成对外服务的 API Key,支持设置频率限制。"
143
+ link="/keys"
144
+ />
145
+ <QuickStartStep
146
+ number="03"
147
+ title="测试接口"
148
+ desc="使用 API 测试工具验证接口连通性,确保模型回复正常。"
149
+ link="/test"
150
+ />
151
+ <QuickStartStep
152
+ number="04"
153
+ title="监控日志"
154
+ desc="在日志页面查看实时请求记录,排查潜在问题。"
155
+ link="/logs"
156
+ />
157
+ </div>
158
+ </motion.div>
159
+ </motion.div>
160
+ );
161
+ }
162
+
163
+ function StatsCard({ title, value, icon: Icon, color, bg, subtext }) {
164
+ return (
165
+ <motion.div
166
+ variants={{
167
+ hidden: { opacity: 0, y: 20 },
168
+ show: { opacity: 1, y: 0 }
169
+ }}
170
+ className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm hover:shadow-md transition-all duration-300 group"
171
+ >
172
+ <div className="flex justify-between items-start mb-4">
173
+ <div className={`p-3 rounded-lg ${bg} ${color} group-hover:scale-105 transition-transform duration-300`}>
174
+ <Icon className="w-5 h-5" />
175
+ </div>
176
+ </div>
177
+ <h3 className="text-zinc-500 text-xs font-medium mb-1 uppercase tracking-wider">{title}</h3>
178
+ <div className="text-2xl font-bold text-zinc-900 mb-2">{value}</div>
179
+ <div className="text-xs text-zinc-400">{subtext}</div>
180
+ </motion.div>
181
+ );
182
+ }
183
+
184
+ function QuickStartStep({ number, title, desc, link }) {
185
+ return (
186
+ <Link to={link} className="flex gap-4 p-4 rounded-xl hover:bg-zinc-50 transition-colors group cursor-pointer border border-transparent hover:border-zinc-100">
187
+ <div className="text-3xl font-bold text-zinc-100 group-hover:text-zinc-200 transition-colors">
188
+ {number}
189
+ </div>
190
+ <div>
191
+ <h4 className="text-base font-semibold text-zinc-900 mb-1 group-hover:text-zinc-900 transition-colors flex items-center gap-2">
192
+ {title}
193
+ <ArrowRight className="w-4 h-4 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all text-zinc-400" />
194
+ </h4>
195
+ <p className="text-zinc-500 text-sm leading-relaxed">
196
+ {desc}
197
+ </p>
198
+ </div>
199
+ </Link>
200
+ );
201
+ }
client/src/pages/Docs.jsx ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Book, Globe, MessageSquare, Shield, Zap, Lightbulb, Code2 } from 'lucide-react';
2
+ import CodeBlock from '../components/CodeBlock';
3
+ import { cn } from '../lib/utils';
4
+
5
+ export default function Docs() {
6
+ return (
7
+ <div className="space-y-8 max-w-4xl mx-auto pb-12">
8
+ <div>
9
+ <h2 className="text-2xl font-semibold text-zinc-900 mb-2 tracking-tight">API 文档</h2>
10
+ <p className="text-zinc-500">Antigravity API 提供与 OpenAI 兼容的接口,可无缝对接现有应用。</p>
11
+ </div>
12
+
13
+ {/* Base URL */}
14
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
15
+ <h3 className="font-semibold text-zinc-900 mb-4 flex items-center gap-2 text-base">
16
+ <Globe className="w-5 h-5 text-zinc-900" />
17
+ 基础 URL
18
+ </h3>
19
+ <CodeBlock code="http://localhost:8045" />
20
+ </div>
21
+
22
+ {/* Endpoints */}
23
+ <div className="space-y-6">
24
+ <EndpointCard
25
+ method="GET"
26
+ path="/v1/models"
27
+ title="获取模型列表"
28
+ desc="获取所有可用的 AI 模型列表。"
29
+ req={`curl http://localhost:8045/v1/models \\
30
+ -H "Authorization: Bearer YOUR_API_KEY"`}
31
+ res={`{
32
+ "object": "list",
33
+ "data": [
34
+ {
35
+ "id": "gemini-2.0-flash-exp",
36
+ "object": "model",
37
+ "created": 1234567890,
38
+ "owned_by": "google"
39
+ }
40
+ ]
41
+ }`}
42
+ />
43
+
44
+ <EndpointCard
45
+ method="POST"
46
+ path="/v1/chat/completions"
47
+ title="聊天补全"
48
+ desc="创建聊天对话补全,支持流式和非流式响应。"
49
+ req={`curl http://localhost:8045/v1/chat/completions \\
50
+ -H "Content-Type: application/json" \\
51
+ -H "Authorization: Bearer YOUR_API_KEY" \\
52
+ -d '{
53
+ "model": "gemini-2.0-flash-exp",
54
+ "messages": [
55
+ {"role": "user", "content": "你好"}
56
+ ],
57
+ "stream": true
58
+ }'`}
59
+ res={`data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk",...}
60
+
61
+ data: [DONE]`}
62
+ />
63
+ </div>
64
+
65
+ {/* Auth Info */}
66
+ <div className="grid md:grid-cols-2 gap-6">
67
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
68
+ <h3 className="font-semibold text-zinc-900 mb-4 flex items-center gap-2 text-base">
69
+ <Shield className="w-5 h-5 text-zinc-900" />
70
+ 认证方式
71
+ </h3>
72
+ <p className="text-zinc-600 text-sm mb-4 leading-relaxed">
73
+ 所有 API 请求都需要在请求头中包含有效的 API 密钥:
74
+ </p>
75
+ <CodeBlock code="Authorization: Bearer YOUR_API_KEY" />
76
+ </div>
77
+
78
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
79
+ <h3 className="font-semibold text-zinc-900 mb-4 flex items-center gap-2 text-base">
80
+ <Zap className="w-5 h-5 text-zinc-900" />
81
+ 频率限制
82
+ </h3>
83
+ <p className="text-zinc-600 text-sm mb-4 leading-relaxed">
84
+ 当请求超过频率限制时,API 会返回 429 状态码。响应头包含限制详情:
85
+ </p>
86
+ <ul className="text-sm text-zinc-500 space-y-2 list-disc list-inside marker:text-zinc-300">
87
+ <li><code className="bg-zinc-100 px-1.5 py-0.5 rounded text-zinc-700 border border-zinc-200">X-RateLimit-Limit</code>: 最大请求数</li>
88
+ <li><code className="bg-zinc-100 px-1.5 py-0.5 rounded text-zinc-700 border border-zinc-200">X-RateLimit-Remaining</code>: 剩余请求数</li>
89
+ <li><code className="bg-zinc-100 px-1.5 py-0.5 rounded text-zinc-700 border border-zinc-200">X-RateLimit-Reset</code>: 重置等待秒数</li>
90
+ </ul>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ function EndpointCard({ method, path, title, desc, req, res }) {
98
+ return (
99
+ <div className="bg-white rounded-xl border border-zinc-200 overflow-hidden shadow-sm">
100
+ <div className="p-6 border-b border-zinc-100">
101
+ <div className="flex items-center gap-3 mb-3">
102
+ <span className={cn(
103
+ "px-2.5 py-1 rounded-md text-xs font-bold tracking-wide",
104
+ method === 'GET' ? "bg-blue-50 text-blue-700 border border-blue-100" : "bg-emerald-50 text-emerald-700 border border-emerald-100"
105
+ )}>
106
+ {method}
107
+ </span>
108
+ <code className="text-zinc-700 font-mono font-semibold text-sm bg-zinc-50 px-2 py-1 rounded border border-zinc-200/50">{path}</code>
109
+ </div>
110
+ <h3 className="text-lg font-semibold text-zinc-900 mb-2">{title}</h3>
111
+ <p className="text-zinc-500 text-sm leading-relaxed">{desc}</p>
112
+ </div>
113
+
114
+ <div className="bg-zinc-50/50 p-6 space-y-6">
115
+ <div>
116
+ <div className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-3 flex items-center gap-2">
117
+ <div className="w-1.5 h-1.5 rounded-full bg-zinc-300"></div>
118
+ Request
119
+ </div>
120
+ <CodeBlock code={req} />
121
+ </div>
122
+ <div>
123
+ <div className="text-xs font-bold text-zinc-400 uppercase tracking-wider mb-3 flex items-center gap-2">
124
+ <div className="w-1.5 h-1.5 rounded-full bg-zinc-300"></div>
125
+ Response
126
+ </div>
127
+ <CodeBlock code={res} />
128
+ </div>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
client/src/pages/Keys.jsx ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ Key, Plus, Trash2, Copy, Check, Shield, Clock, Zap, AlertCircle, RefreshCw, AlertTriangle, Eye, EyeOff
5
+ } from 'lucide-react';
6
+ import { useAuth } from '../context/AuthContext';
7
+ import { cn } from '../lib/utils';
8
+
9
+ export default function Keys() {
10
+ const { token: adminToken } = useAuth();
11
+ const [keys, setKeys] = useState([]);
12
+ const [isLoading, setIsLoading] = useState(true);
13
+ const [isGenerating, setIsGenerating] = useState(false);
14
+
15
+ // Form State
16
+ const [keyName, setKeyName] = useState('');
17
+ const [customKey, setCustomKey] = useState('');
18
+ const [enableRateLimit, setEnableRateLimit] = useState(false);
19
+ const [maxRequests, setMaxRequests] = useState(100);
20
+ const [windowSeconds, setWindowSeconds] = useState(60);
21
+
22
+ const [copiedKey, setCopiedKey] = useState('');
23
+ const [message, setMessage] = useState({ type: '', content: '' });
24
+
25
+ // UI State
26
+ const [deleteConfirm, setDeleteConfirm] = useState({ isOpen: false, key: null });
27
+ const [visibleKeys, setVisibleKeys] = useState(new Set());
28
+
29
+ const fetchKeys = async () => {
30
+ setIsLoading(true);
31
+ try {
32
+ const res = await fetch('/admin/keys', {
33
+ headers: { 'X-Admin-Token': adminToken }
34
+ });
35
+ const data = await res.json();
36
+ setKeys(data);
37
+ } catch (error) {
38
+ console.error('Failed to fetch keys', error);
39
+ setMessage({ type: 'error', content: '加载密钥失败' });
40
+ } finally {
41
+ setIsLoading(false);
42
+ }
43
+ };
44
+
45
+ useEffect(() => {
46
+ fetchKeys();
47
+ }, [adminToken]);
48
+
49
+ const generateKey = async () => {
50
+ setIsGenerating(true);
51
+ try {
52
+ const body = {
53
+ name: keyName,
54
+ key: customKey || undefined,
55
+ rate_limit: enableRateLimit ? {
56
+ requests: parseInt(maxRequests),
57
+ window: parseInt(windowSeconds)
58
+ } : undefined
59
+ };
60
+
61
+ const res = await fetch('/admin/keys/generate', {
62
+ method: 'POST',
63
+ headers: {
64
+ 'Content-Type': 'application/json',
65
+ 'X-Admin-Token': adminToken
66
+ },
67
+ body: JSON.stringify(body)
68
+ });
69
+
70
+ const data = await res.json();
71
+ if (data.key) {
72
+ setMessage({ type: 'success', content: '密钥生成成功' });
73
+ setKeyName('');
74
+ setCustomKey('');
75
+ setEnableRateLimit(false);
76
+ fetchKeys();
77
+ } else {
78
+ setMessage({ type: 'error', content: data.error || '生成失败' });
79
+ }
80
+ } catch (error) {
81
+ setMessage({ type: 'error', content: '请求失败: ' + error.message });
82
+ } finally {
83
+ setIsGenerating(false);
84
+ }
85
+ };
86
+
87
+ const handleDeleteClick = (key) => {
88
+ setDeleteConfirm({ isOpen: true, key });
89
+ };
90
+
91
+ const confirmDelete = async () => {
92
+ if (!deleteConfirm.key) return;
93
+
94
+ try {
95
+ const res = await fetch(`/admin/keys/${deleteConfirm.key}`, {
96
+ method: 'DELETE',
97
+ headers: { 'X-Admin-Token': adminToken }
98
+ });
99
+ if (res.ok) {
100
+ fetchKeys();
101
+ setDeleteConfirm({ isOpen: false, key: null });
102
+ }
103
+ } catch (error) {
104
+ console.error('Delete failed', error);
105
+ }
106
+ };
107
+
108
+ const copyToClipboard = (text) => {
109
+ navigator.clipboard.writeText(text);
110
+ setCopiedKey(text);
111
+ setTimeout(() => setCopiedKey(''), 2000);
112
+ };
113
+
114
+ const toggleKeyVisibility = (key) => {
115
+ setVisibleKeys(prev => {
116
+ const newSet = new Set(prev);
117
+ if (newSet.has(key)) {
118
+ newSet.delete(key);
119
+ } else {
120
+ newSet.add(key);
121
+ }
122
+ return newSet;
123
+ });
124
+ };
125
+
126
+ const maskKey = (key) => {
127
+ if (visibleKeys.has(key)) return key;
128
+ return key.substring(0, 3) + '•'.repeat(20) + key.substring(key.length - 4);
129
+ };
130
+
131
+ return (
132
+ <div className="space-y-8 relative">
133
+ {/* Delete Confirmation Modal */}
134
+ <AnimatePresence>
135
+ {deleteConfirm.isOpen && (
136
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/5 backdrop-blur-sm">
137
+ <motion.div
138
+ initial={{ opacity: 0, scale: 0.95 }}
139
+ animate={{ opacity: 1, scale: 1 }}
140
+ exit={{ opacity: 0, scale: 0.95 }}
141
+ className="bg-white rounded-xl shadow-xl max-w-md w-full overflow-hidden border border-zinc-200"
142
+ >
143
+ <div className="p-8 text-center">
144
+ <div className="w-12 h-12 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-4 border border-red-100">
145
+ <AlertTriangle className="w-6 h-6 text-red-600" />
146
+ </div>
147
+ <h3 className="text-lg font-semibold text-zinc-900 mb-2">确认删除密钥?</h3>
148
+ <p className="text-zinc-500 text-sm mb-8 leading-relaxed">
149
+ 您确定要删除密钥 <code className="bg-zinc-100 px-2 py-1 rounded text-zinc-700 border border-zinc-200 font-mono">{deleteConfirm.key?.substring(0, 8)}...</code> 吗?<br />
150
+ 此操作无法撤销,相关应用将立即失去访问权限。
151
+ </p>
152
+ <div className="flex gap-4 justify-center">
153
+ <button
154
+ onClick={() => setDeleteConfirm({ isOpen: false, key: null })}
155
+ className="px-6 py-2.5 bg-white border border-zinc-200 hover:bg-zinc-50 text-zinc-700 font-medium rounded-lg transition-colors text-sm"
156
+ >
157
+ 取消
158
+ </button>
159
+ <button
160
+ onClick={confirmDelete}
161
+ className="px-6 py-2.5 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors shadow-sm text-sm"
162
+ >
163
+ 确认删除
164
+ </button>
165
+ </div>
166
+ </div>
167
+ </motion.div>
168
+ </div>
169
+ )}
170
+ </AnimatePresence>
171
+
172
+ <div className="flex justify-between items-end pb-2">
173
+ <div>
174
+ <h2 className="text-2xl font-semibold text-zinc-900 tracking-tight">密钥管理</h2>
175
+ <p className="text-base text-zinc-500 mt-1">生成和管理 API 访问密钥</p>
176
+ </div>
177
+ <button
178
+ onClick={fetchKeys}
179
+ className="p-2.5 text-zinc-400 hover:text-zinc-900 hover:bg-zinc-100 rounded-lg transition-colors"
180
+ >
181
+ <RefreshCw className={cn("w-5 h-5", isLoading && "animate-spin")} />
182
+ </button>
183
+ </div>
184
+
185
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
186
+ {/* Generator Card */}
187
+ <div className="lg:col-span-1">
188
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm sticky top-24">
189
+ <h3 className="font-medium text-zinc-900 mb-6 flex items-center gap-2 text-base">
190
+ <Plus className="w-5 h-5" />
191
+ 生成新密钥
192
+ </h3>
193
+
194
+ <div className="space-y-5">
195
+ <div>
196
+ <label className="block text-sm font-medium text-zinc-700 mb-2">密钥名称</label>
197
+ <input
198
+ type="text"
199
+ value={keyName}
200
+ onChange={(e) => setKeyName(e.target.value)}
201
+ placeholder="例如: 我的应用密钥"
202
+ className="w-full px-4 py-2.5 bg-zinc-50 border border-zinc-200 rounded-lg focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 outline-none transition-all text-sm placeholder:text-zinc-400"
203
+ />
204
+ </div>
205
+
206
+ <div>
207
+ <label className="block text-sm font-medium text-zinc-700 mb-2">自定义密钥 (可选)</label>
208
+ <div className="relative">
209
+ <Key className="absolute left-3.5 top-3 w-4 h-4 text-zinc-400" />
210
+ <input
211
+ type="text"
212
+ value={customKey}
213
+ onChange={(e) => setCustomKey(e.target.value)}
214
+ placeholder="留空自动生成"
215
+ className="w-full pl-10 pr-4 py-2.5 bg-zinc-50 border border-zinc-200 rounded-lg focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 outline-none transition-all font-mono text-sm placeholder:text-zinc-400"
216
+ />
217
+ </div>
218
+ </div>
219
+
220
+ <div className="p-4 bg-zinc-50/50 rounded-lg border border-zinc-100">
221
+ <label className="flex items-center gap-3 cursor-pointer mb-3">
222
+ <input
223
+ type="checkbox"
224
+ checked={enableRateLimit}
225
+ onChange={(e) => setEnableRateLimit(e.target.checked)}
226
+ className="w-4 h-4 rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
227
+ />
228
+ <span className="text-sm font-medium text-zinc-700">启用频率限制</span>
229
+ </label>
230
+
231
+ <AnimatePresence>
232
+ {enableRateLimit && (
233
+ <motion.div
234
+ initial={{ opacity: 0, height: 0 }}
235
+ animate={{ opacity: 1, height: 'auto' }}
236
+ exit={{ opacity: 0, height: 0 }}
237
+ className="space-y-4 overflow-hidden pt-1"
238
+ >
239
+ <div className="grid grid-cols-2 gap-4">
240
+ <div>
241
+ <label className="block text-xs font-medium text-zinc-500 mb-1.5">最大请求</label>
242
+ <input
243
+ type="number"
244
+ value={maxRequests}
245
+ onChange={(e) => setMaxRequests(e.target.value)}
246
+ min="1"
247
+ className="w-full px-3 py-2 bg-white border border-zinc-200 rounded-md text-sm focus:border-zinc-900 outline-none"
248
+ />
249
+ </div>
250
+ <div>
251
+ <label className="block text-xs font-medium text-zinc-500 mb-1.5">窗口(秒)</label>
252
+ <input
253
+ type="number"
254
+ value={windowSeconds}
255
+ onChange={(e) => setWindowSeconds(e.target.value)}
256
+ min="1"
257
+ className="w-full px-3 py-2 bg-white border border-zinc-200 rounded-md text-sm focus:border-zinc-900 outline-none"
258
+ />
259
+ </div>
260
+ </div>
261
+ </motion.div>
262
+ )}
263
+ </AnimatePresence>
264
+ </div>
265
+
266
+ <button
267
+ onClick={generateKey}
268
+ disabled={isGenerating}
269
+ className="w-full py-2.5 bg-zinc-900 hover:bg-zinc-800 text-white font-medium rounded-lg transition-all disabled:opacity-50 shadow-sm hover:shadow-md text-sm mt-2"
270
+ >
271
+ {isGenerating ? '生成中...' : '生成密钥'}
272
+ </button>
273
+
274
+ <AnimatePresence>
275
+ {message.content && (
276
+ <motion.div
277
+ initial={{ opacity: 0, height: 0 }}
278
+ animate={{ opacity: 1, height: 'auto' }}
279
+ exit={{ opacity: 0, height: 0 }}
280
+ className={cn(
281
+ "flex items-center gap-2 p-3 rounded-lg text-sm font-medium",
282
+ message.type === 'error' ? "bg-red-50 text-red-600 border border-red-100" : "bg-emerald-50 text-emerald-600 border border-emerald-100"
283
+ )}
284
+ >
285
+ <AlertCircle className="w-4 h-4" />
286
+ {message.content}
287
+ </motion.div>
288
+ )}
289
+ </AnimatePresence>
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ {/* Keys List */}
295
+ <div className="lg:col-span-2">
296
+ <div className="bg-white rounded-xl border border-zinc-200 overflow-hidden shadow-sm flex flex-col min-h-[600px]">
297
+ <div className="px-6 py-4 border-b border-zinc-100 bg-zinc-50/30 flex justify-between items-center">
298
+ <span className="text-sm font-medium text-zinc-600">密钥列表</span>
299
+ <span className="text-xs text-zinc-500 bg-zinc-100 px-2.5 py-1 rounded-full font-medium">{keys.length} ACTIVE</span>
300
+ </div>
301
+
302
+ <div className="flex-1 overflow-y-auto">
303
+ {keys.length === 0 ? (
304
+ <div className="h-full flex flex-col items-center justify-center p-12 text-center">
305
+ <div className="w-16 h-16 bg-zinc-50 rounded-full flex items-center justify-center mb-4">
306
+ <Key className="w-8 h-8 text-zinc-300" />
307
+ </div>
308
+ <p className="text-base text-zinc-500 font-medium">暂无 API 密钥</p>
309
+ <p className="text-sm text-zinc-400 mt-1">请在左侧创建您的第一个密钥</p>
310
+ </div>
311
+ ) : (
312
+ <div className="divide-y divide-zinc-100">
313
+ {keys.map((k) => (
314
+ <motion.div
315
+ key={k.key}
316
+ layout
317
+ initial={{ opacity: 0 }}
318
+ animate={{ opacity: 1 }}
319
+ className="p-6 hover:bg-zinc-50/50 transition-colors group"
320
+ >
321
+ <div className="flex items-start justify-between gap-6">
322
+ <div className="flex-1 min-w-0 space-y-4">
323
+ <div className="flex items-center gap-3">
324
+ <h3 className="font-semibold text-zinc-900 text-base">{k.name || '未命名密钥'}</h3>
325
+ {k.rate_limit ? (
326
+ <span className="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium bg-amber-50 text-amber-700 border border-amber-100">
327
+ <Shield className="w-3.5 h-3.5" />
328
+ {k.rate_limit.requests}/{k.rate_limit.window}s
329
+ </span>
330
+ ) : (
331
+ <span className="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-100">
332
+ <Zap className="w-3.5 h-3.5" />
333
+ 无限制
334
+ </span>
335
+ )}
336
+ </div>
337
+
338
+ <div className="flex items-center gap-3">
339
+ <code className="bg-zinc-100 px-3 py-1.5 rounded-md text-sm font-mono text-zinc-600 border border-zinc-200/50 tracking-wide">
340
+ {maskKey(k.key)}
341
+ </code>
342
+ <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
343
+ <button
344
+ onClick={() => toggleKeyVisibility(k.key)}
345
+ className="p-2 text-zinc-400 hover:text-zinc-900 hover:bg-zinc-100 rounded-lg transition-colors"
346
+ title={visibleKeys.has(k.key) ? "隐藏密钥" : "显示密钥"}
347
+ >
348
+ {visibleKeys.has(k.key) ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
349
+ </button>
350
+ <button
351
+ onClick={() => copyToClipboard(k.key)}
352
+ className="p-2 text-zinc-400 hover:text-zinc-900 hover:bg-zinc-100 rounded-lg transition-colors"
353
+ title="复制密钥"
354
+ >
355
+ {copiedKey === k.key ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
356
+ </button>
357
+ </div>
358
+ </div>
359
+
360
+ <div className="flex items-center gap-2 text-xs text-zinc-400">
361
+ <Clock className="w-3.5 h-3.5" />
362
+ <span>创建于: {new Date(k.created).toLocaleString()}</span>
363
+ </div>
364
+ </div>
365
+
366
+ <button
367
+ onClick={() => handleDeleteClick(k.key)}
368
+ className="p-2.5 text-zinc-400 hover:text-red-600 hover:bg-red-50 rounded-xl opacity-0 group-hover:opacity-100 transition-all"
369
+ title="删除密钥"
370
+ >
371
+ <Trash2 className="w-5 h-5" />
372
+ </button>
373
+ </div>
374
+ </motion.div>
375
+ ))}
376
+ </div>
377
+ )}
378
+ </div>
379
+ </div>
380
+ </div>
381
+ </div>
382
+ </div>
383
+ );
384
+ }
client/src/pages/Logs.jsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { RefreshCw, Trash2, ScrollText, Pause, Play } from 'lucide-react';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { cn } from '../lib/utils';
6
+
7
+ export default function Logs() {
8
+ const { token: adminToken } = useAuth();
9
+ const [logs, setLogs] = useState([]);
10
+ const [isLoading, setIsLoading] = useState(true);
11
+ const [autoRefresh, setAutoRefresh] = useState(false);
12
+ const logsEndRef = useRef(null);
13
+
14
+ const fetchLogs = async () => {
15
+ setIsLoading(true);
16
+ try {
17
+ const res = await fetch('/admin/logs', {
18
+ headers: { 'X-Admin-Token': adminToken }
19
+ });
20
+ const text = await res.text();
21
+ // Parse logs: Assuming they are line separated JSON or text
22
+ let logLines = [];
23
+ try {
24
+ const json = JSON.parse(text);
25
+ if (Array.isArray(json)) logLines = json;
26
+ } catch {
27
+ logLines = text.split('\n').filter(Boolean);
28
+ }
29
+ setLogs(logLines);
30
+ } catch (error) {
31
+ console.error('Failed to fetch logs', error);
32
+ } finally {
33
+ setIsLoading(false);
34
+ }
35
+ };
36
+
37
+ useEffect(() => {
38
+ fetchLogs();
39
+ }, [adminToken]);
40
+
41
+ useEffect(() => {
42
+ let interval;
43
+ if (autoRefresh) {
44
+ interval = setInterval(fetchLogs, 5000);
45
+ }
46
+ return () => clearInterval(interval);
47
+ }, [autoRefresh, adminToken]);
48
+
49
+ const clearLogs = async () => {
50
+ if (!confirm('确定要清空日志吗?')) return;
51
+ try {
52
+ await fetch('/admin/logs', {
53
+ method: 'DELETE',
54
+ headers: { 'X-Admin-Token': adminToken }
55
+ });
56
+ setLogs([]);
57
+ } catch (error) {
58
+ console.error('Failed to clear logs', error);
59
+ }
60
+ };
61
+
62
+ return (
63
+ <div className="space-y-6 h-[calc(100vh-8rem)] flex flex-col">
64
+ <div className="flex justify-between items-center shrink-0">
65
+ <div>
66
+ <h2 className="text-2xl font-semibold text-zinc-900 tracking-tight">系统日志</h2>
67
+ <p className="text-zinc-500">查看实时系统运行日志</p>
68
+ </div>
69
+ <div className="flex gap-2">
70
+ <button
71
+ onClick={() => setAutoRefresh(!autoRefresh)}
72
+ className={cn(
73
+ "flex items-center gap-2 px-4 py-2 rounded-lg transition-colors font-medium text-sm border",
74
+ autoRefresh
75
+ ? "bg-emerald-50 text-emerald-600 border-emerald-100"
76
+ : "bg-white border-zinc-200 text-zinc-600 hover:bg-zinc-50"
77
+ )}
78
+ >
79
+ {autoRefresh ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
80
+ {autoRefresh ? '自动刷新中' : '自动刷新'}
81
+ </button>
82
+ <button
83
+ onClick={fetchLogs}
84
+ className="p-2 text-zinc-600 hover:bg-zinc-100 rounded-lg transition-colors border border-transparent hover:border-zinc-200"
85
+ >
86
+ <RefreshCw className={cn("w-5 h-5", isLoading && "animate-spin")} />
87
+ </button>
88
+ <button
89
+ onClick={clearLogs}
90
+ className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors border border-transparent hover:border-red-100"
91
+ >
92
+ <Trash2 className="w-5 h-5" />
93
+ </button>
94
+ </div>
95
+ </div>
96
+
97
+ <div className="flex-1 bg-zinc-950 rounded-2xl border border-zinc-800 overflow-hidden flex flex-col shadow-inner">
98
+ <div className="flex-1 overflow-y-auto p-4 space-y-1 font-mono text-sm">
99
+ {logs.length === 0 ? (
100
+ <div className="text-zinc-500 text-center py-12">暂无日志</div>
101
+ ) : (
102
+ logs.map((log, idx) => (
103
+ <div key={idx} className="text-zinc-300 break-all hover:bg-white/5 px-2 py-0.5 rounded transition-colors">
104
+ {typeof log === 'string' ? log : JSON.stringify(log)}
105
+ </div>
106
+ ))
107
+ )}
108
+ <div ref={logsEndRef} />
109
+ </div>
110
+ </div>
111
+ </div>
112
+ );
113
+ }
client/src/pages/Monitor.jsx ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Activity, Cpu, HardDrive, Clock, Zap, RefreshCw, Pause, Play } from 'lucide-react';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { cn } from '../lib/utils';
6
+
7
+ export default function Monitor() {
8
+ const { token: adminToken } = useAuth();
9
+ const [stats, setStats] = useState({
10
+ cpu: 0,
11
+ memory: { used: 0, total: 0, percentage: 0 },
12
+ uptime: 0,
13
+ requests: 0,
14
+ status: 'idle',
15
+ idleTime: 0
16
+ });
17
+ const [isLoading, setIsLoading] = useState(true);
18
+ const [autoRefresh, setAutoRefresh] = useState(false);
19
+
20
+ const fetchStats = async () => {
21
+ setIsLoading(true);
22
+ try {
23
+ const [monitorRes, keyStatsRes] = await Promise.all([
24
+ fetch('/admin/status', { headers: { 'X-Admin-Token': adminToken } }),
25
+ fetch('/admin/keys/stats', { headers: { 'X-Admin-Token': adminToken } })
26
+ ]);
27
+
28
+ const monitor = await monitorRes.json();
29
+ const keyStats = await keyStatsRes.json();
30
+
31
+ // Parse memory string "123.45 MB / 16384.00 MB"
32
+ let memUsed = 0, memTotal = 0, memPercent = 0;
33
+ if (monitor.systemMemory) {
34
+ const parts = monitor.systemMemory.split(' / ');
35
+ if (parts.length === 2) {
36
+ memUsed = parseFloat(parts[0]);
37
+ memTotal = parseFloat(parts[1]);
38
+ if (memTotal > 0) {
39
+ memPercent = ((memUsed / memTotal) * 100).toFixed(1);
40
+ }
41
+ }
42
+ }
43
+
44
+ setStats({
45
+ cpu: monitor.cpu || 0,
46
+ memory: {
47
+ used: memUsed * 1024 * 1024, // Convert back to bytes for display consistency if needed, or just use raw
48
+ total: memTotal * 1024 * 1024,
49
+ percentage: memPercent,
50
+ display: monitor.systemMemory
51
+ },
52
+ uptime: monitor.uptime || '0s', // Backend returns formatted string
53
+ requests: keyStats.totalRequests || 0,
54
+ status: monitor.idle === '活跃' ? 'busy' : 'idle',
55
+ idleTime: monitor.idleTime || 0
56
+ });
57
+ } catch (error) {
58
+ console.error('Failed to fetch monitor stats', error);
59
+ } finally {
60
+ setIsLoading(false);
61
+ }
62
+ };
63
+
64
+ useEffect(() => {
65
+ fetchStats();
66
+ }, [adminToken]);
67
+
68
+ useEffect(() => {
69
+ let interval;
70
+ if (autoRefresh) {
71
+ interval = setInterval(fetchStats, 5000);
72
+ }
73
+ return () => clearInterval(interval);
74
+ }, [autoRefresh, adminToken]);
75
+
76
+ const formatUptime = (seconds) => {
77
+ const d = Math.floor(seconds / (3600 * 24));
78
+ const h = Math.floor((seconds % (3600 * 24)) / 3600);
79
+ const m = Math.floor((seconds % 3600) / 60);
80
+ const s = Math.floor(seconds % 60);
81
+ return `${d}d ${h}h ${m}m ${s}s`;
82
+ };
83
+
84
+ return (
85
+ <div className="space-y-6">
86
+ <div className="flex justify-between items-center">
87
+ <div>
88
+ <h2 className="text-2xl font-semibold text-zinc-900 tracking-tight">系统监控</h2>
89
+ <p className="text-zinc-500">实时监控服务器资源和状态</p>
90
+ </div>
91
+ <div className="flex gap-2">
92
+ <button
93
+ onClick={() => setAutoRefresh(!autoRefresh)}
94
+ className={cn(
95
+ "flex items-center gap-2 px-4 py-2 rounded-lg transition-colors font-medium text-sm border",
96
+ autoRefresh
97
+ ? "bg-emerald-50 text-emerald-600 border-emerald-100"
98
+ : "bg-white border-zinc-200 text-zinc-600 hover:bg-zinc-50"
99
+ )}
100
+ >
101
+ {autoRefresh ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}
102
+ {autoRefresh ? '自动刷新中' : '自动刷新'}
103
+ </button>
104
+ <button
105
+ onClick={fetchStats}
106
+ className="p-2 text-zinc-600 hover:bg-zinc-100 rounded-lg transition-colors border border-transparent hover:border-zinc-200"
107
+ >
108
+ <RefreshCw className={cn("w-5 h-5", isLoading && "animate-spin")} />
109
+ </button>
110
+ </div>
111
+ </div>
112
+
113
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
114
+ <MonitorCard
115
+ title="CPU 使用率"
116
+ value={`${stats.cpu}%`}
117
+ icon={Cpu}
118
+ color="text-blue-600"
119
+ bg="bg-blue-50 border-blue-100"
120
+ />
121
+ <MonitorCard
122
+ title="内存使用"
123
+ value={`${stats.memory.percentage}%`}
124
+ subtext={stats.memory.display}
125
+ icon={HardDrive}
126
+ color="text-purple-600"
127
+ bg="bg-purple-50 border-purple-100"
128
+ />
129
+ <MonitorCard
130
+ title="运行时间"
131
+ value={stats.uptime}
132
+ icon={Clock}
133
+ color="text-emerald-600"
134
+ bg="bg-emerald-50 border-emerald-100"
135
+ className="text-lg"
136
+ />
137
+ <MonitorCard
138
+ title="总请求数"
139
+ value={stats.requests}
140
+ icon={Activity}
141
+ color="text-orange-600"
142
+ bg="bg-orange-50 border-orange-100"
143
+ />
144
+ </div>
145
+
146
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
147
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
148
+ <h3 className="font-semibold text-zinc-900 mb-4 flex items-center gap-2 text-base">
149
+ <Zap className="w-5 h-5 text-zinc-900" />
150
+ 服务器状态
151
+ </h3>
152
+ <div className="flex items-center gap-4">
153
+ <div className={cn(
154
+ "w-4 h-4 rounded-full animate-pulse",
155
+ stats.status === 'busy' ? "bg-amber-500" : "bg-emerald-500"
156
+ )} />
157
+ <span className="text-lg font-medium capitalize text-zinc-900">{stats.status === 'busy' ? '繁忙' : '空闲'}</span>
158
+ <span className="text-zinc-400 text-sm ml-auto font-mono">空闲时间: {stats.idleTime}s</span>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ function MonitorCard({ title, value, subtext, icon: Icon, color, bg, className }) {
167
+ return (
168
+ <motion.div
169
+ initial={{ opacity: 0, y: 20 }}
170
+ animate={{ opacity: 1, y: 0 }}
171
+ className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm hover:shadow-md transition-shadow"
172
+ >
173
+ <div className="flex justify-between items-start mb-4">
174
+ <div className={`p-3 rounded-xl border ${bg} ${color}`}>
175
+ <Icon className="w-6 h-6" />
176
+ </div>
177
+ </div>
178
+ <h3 className="text-zinc-500 text-sm font-medium mb-1">{title}</h3>
179
+ <div className={cn("text-2xl font-bold text-zinc-900 mb-1 tracking-tight", className)}>{value}</div>
180
+ {subtext && <div className="text-xs text-zinc-400 font-mono">{subtext}</div>}
181
+ </motion.div>
182
+ );
183
+ }
client/src/pages/Settings.jsx ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Save, Server, Shield, Sliders, MessageSquare, AlertCircle, Loader2 } from 'lucide-react';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { cn } from '../lib/utils';
6
+
7
+ export default function Settings() {
8
+ const { token: adminToken } = useAuth();
9
+ const [settings, setSettings] = useState({});
10
+ const [isLoading, setIsLoading] = useState(true);
11
+ const [isSaving, setIsSaving] = useState(false);
12
+ const [message, setMessage] = useState({ type: '', content: '' });
13
+
14
+ useEffect(() => {
15
+ const fetchSettings = async () => {
16
+ try {
17
+ const res = await fetch('/admin/settings', {
18
+ headers: { 'X-Admin-Token': adminToken }
19
+ });
20
+ const data = await res.json();
21
+ // Map nested backend data to flat UI state
22
+ setSettings({
23
+ port: data.server?.port,
24
+ host: data.server?.host,
25
+ apiKey: data.security?.apiKey,
26
+ adminPassword: data.security?.adminPassword,
27
+ maxRequestSize: data.security?.maxRequestSize,
28
+ temperature: data.defaults?.temperature,
29
+ topP: data.defaults?.top_p,
30
+ topK: data.defaults?.top_k,
31
+ maxTokens: data.defaults?.max_tokens,
32
+ systemInstruction: data.systemInstruction
33
+ });
34
+ } catch (error) {
35
+ console.error('Failed to fetch settings', error);
36
+ setMessage({ type: 'error', content: '加载设置失败' });
37
+ } finally {
38
+ setIsLoading(false);
39
+ }
40
+ };
41
+ fetchSettings();
42
+ }, [adminToken]);
43
+
44
+ const handleChange = (key, value) => {
45
+ setSettings(prev => ({ ...prev, [key]: value }));
46
+ };
47
+
48
+ const handleSave = async () => {
49
+ setIsSaving(true);
50
+ setMessage({ type: '', content: '' });
51
+ try {
52
+ // Map flat UI state back to nested backend structure
53
+ const payload = {
54
+ server: {
55
+ port: settings.port,
56
+ host: settings.host
57
+ },
58
+ security: {
59
+ apiKey: settings.apiKey,
60
+ adminPassword: settings.adminPassword,
61
+ maxRequestSize: settings.maxRequestSize
62
+ },
63
+ defaults: {
64
+ temperature: settings.temperature,
65
+ top_p: settings.topP,
66
+ top_k: settings.topK,
67
+ max_tokens: settings.maxTokens
68
+ },
69
+ systemInstruction: settings.systemInstruction
70
+ };
71
+
72
+ const res = await fetch('/admin/settings', {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ 'X-Admin-Token': adminToken
77
+ },
78
+ body: JSON.stringify(payload)
79
+ });
80
+ const data = await res.json();
81
+ if (data.success) {
82
+ setMessage({ type: 'success', content: '设置保存成功' });
83
+ } else {
84
+ setMessage({ type: 'error', content: '保存失败' });
85
+ }
86
+ } catch (error) {
87
+ setMessage({ type: 'error', content: '保存失败: ' + error.message });
88
+ } finally {
89
+ setIsSaving(false);
90
+ }
91
+ };
92
+
93
+ if (isLoading) return <div className="p-12 text-center text-zinc-400">加载中...</div>;
94
+
95
+ return (
96
+ <div className="space-y-6 max-w-4xl mx-auto pb-12">
97
+ <div className="flex justify-between items-center sticky top-0 bg-zinc-50/90 backdrop-blur-sm py-4 z-10">
98
+ <div>
99
+ <h2 className="text-2xl font-semibold text-zinc-900 tracking-tight">系统设置</h2>
100
+ <p className="text-zinc-500">配置服务器参数和模型默认值</p>
101
+ </div>
102
+ <button
103
+ onClick={handleSave}
104
+ disabled={isSaving}
105
+ className="flex items-center gap-2 px-6 py-2.5 bg-zinc-900 hover:bg-zinc-800 text-white font-medium rounded-xl transition-colors disabled:opacity-50 shadow-sm"
106
+ >
107
+ {isSaving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
108
+ {isSaving ? '保存中...' : '保存设置'}
109
+ </button>
110
+ </div>
111
+
112
+ <AnimatePresence>
113
+ {message.content && (
114
+ <motion.div
115
+ initial={{ opacity: 0, height: 0 }}
116
+ animate={{ opacity: 1, height: 'auto' }}
117
+ exit={{ opacity: 0, height: 0 }}
118
+ className={cn(
119
+ "flex items-center gap-2 p-4 rounded-xl text-sm font-medium border",
120
+ message.type === 'error'
121
+ ? "bg-red-50 text-red-600 border-red-100"
122
+ : "bg-emerald-50 text-emerald-600 border-emerald-100"
123
+ )}
124
+ >
125
+ <AlertCircle className="w-4 h-4" />
126
+ {message.content}
127
+ </motion.div>
128
+ )}
129
+ </AnimatePresence>
130
+
131
+ {/* Server Config */}
132
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
133
+ <h3 className="font-semibold text-zinc-900 mb-6 flex items-center gap-2 text-base">
134
+ <Server className="w-5 h-5 text-zinc-900" />
135
+ 服务器配置
136
+ </h3>
137
+ <div className="grid md:grid-cols-2 gap-6">
138
+ <FormInput
139
+ label="服务端口"
140
+ value={settings.port || ''}
141
+ onChange={v => handleChange('port', v)}
142
+ placeholder="8045"
143
+ type="number"
144
+ />
145
+ <FormInput
146
+ label="监听地址"
147
+ value={settings.host || ''}
148
+ onChange={v => handleChange('host', v)}
149
+ placeholder="0.0.0.0"
150
+ />
151
+ </div>
152
+ </div>
153
+
154
+ {/* Security Config */}
155
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
156
+ <h3 className="font-semibold text-zinc-900 mb-6 flex items-center gap-2 text-base">
157
+ <Shield className="w-5 h-5 text-zinc-900" />
158
+ 安全配置
159
+ </h3>
160
+ <div className="space-y-6">
161
+ <FormInput
162
+ label="默认 API 密钥"
163
+ value={settings.apiKey || ''}
164
+ onChange={v => handleChange('apiKey', v)}
165
+ placeholder="sk-test"
166
+ helper="此密钥不受频率限制约束,用于测试或内部使用"
167
+ />
168
+ <FormInput
169
+ label="管理员密码"
170
+ value={settings.adminPassword || ''}
171
+ onChange={v => handleChange('adminPassword', v)}
172
+ placeholder="admin123"
173
+ type="password"
174
+ />
175
+ <FormInput
176
+ label="最大请求体大小"
177
+ value={settings.maxRequestSize || ''}
178
+ onChange={v => handleChange('maxRequestSize', v)}
179
+ placeholder="50mb"
180
+ />
181
+ </div>
182
+ </div>
183
+
184
+ {/* Model Defaults */}
185
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
186
+ <h3 className="font-semibold text-zinc-900 mb-6 flex items-center gap-2 text-base">
187
+ <Sliders className="w-5 h-5 text-zinc-900" />
188
+ 模型默认参数
189
+ </h3>
190
+ <div className="grid md:grid-cols-2 gap-6">
191
+ <FormInput
192
+ label="Temperature"
193
+ value={settings.temperature || ''}
194
+ onChange={v => handleChange('temperature', parseFloat(v))}
195
+ type="number" step="0.1" min="0" max="2"
196
+ />
197
+ <FormInput
198
+ label="Top P"
199
+ value={settings.topP || ''}
200
+ onChange={v => handleChange('topP', parseFloat(v))}
201
+ type="number" step="0.01" min="0" max="1"
202
+ />
203
+ <FormInput
204
+ label="Top K"
205
+ value={settings.topK || ''}
206
+ onChange={v => handleChange('topK', parseInt(v))}
207
+ type="number" min="1"
208
+ />
209
+ <FormInput
210
+ label="最大 Token 数"
211
+ value={settings.maxTokens || ''}
212
+ onChange={v => handleChange('maxTokens', parseInt(v))}
213
+ type="number" min="1"
214
+ />
215
+ </div>
216
+ </div>
217
+
218
+ {/* System Instruction */}
219
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm">
220
+ <h3 className="font-semibold text-zinc-900 mb-6 flex items-center gap-2 text-base">
221
+ <MessageSquare className="w-5 h-5 text-zinc-900" />
222
+ 系统指令
223
+ </h3>
224
+ <div>
225
+ <label className="block text-sm font-medium text-zinc-700 mb-2">System Instruction</label>
226
+ <textarea
227
+ value={settings.systemInstruction || ''}
228
+ onChange={e => handleChange('systemInstruction', e.target.value)}
229
+ rows={5}
230
+ placeholder="输入系统提示词..."
231
+ className="w-full px-4 py-3 bg-zinc-50 border border-zinc-200 rounded-xl focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 outline-none transition-all resize-y text-sm placeholder:text-zinc-400"
232
+ />
233
+ </div>
234
+ </div>
235
+ </div>
236
+ );
237
+ }
238
+
239
+ function FormInput({ label, value, onChange, type = "text", placeholder, helper, ...props }) {
240
+ return (
241
+ <div>
242
+ <label className="block text-sm font-medium text-zinc-700 mb-2">{label}</label>
243
+ <input
244
+ type={type}
245
+ value={value}
246
+ onChange={e => onChange(e.target.value)}
247
+ placeholder={placeholder}
248
+ className="w-full px-4 py-2.5 bg-zinc-50 border border-zinc-200 rounded-xl focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 outline-none transition-all text-sm placeholder:text-zinc-400"
249
+ {...props}
250
+ />
251
+ {helper && <p className="mt-1.5 text-xs text-zinc-400">{helper}</p>}
252
+ </div>
253
+ );
254
+ }
client/src/pages/Test.jsx ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Send, Trash2, RefreshCw, Bot, User, Key, Settings2 } from 'lucide-react';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { cn } from '../lib/utils';
6
+
7
+ export default function Test() {
8
+ const { token: adminToken } = useAuth();
9
+ // Initialize state from localStorage
10
+ const [messages, setMessages] = useState(() => {
11
+ const saved = localStorage.getItem('test_messages');
12
+ return saved ? JSON.parse(saved) : [];
13
+ });
14
+ const [input, setInput] = useState('');
15
+ const [models, setModels] = useState([]);
16
+ const [selectedModel, setSelectedModel] = useState(() => localStorage.getItem('test_selected_model') || '');
17
+ const [apiKey, setApiKey] = useState(() => localStorage.getItem('test_api_key') || '');
18
+
19
+ // Persist API key
20
+ useEffect(() => {
21
+ localStorage.setItem('test_api_key', apiKey);
22
+ }, [apiKey]);
23
+
24
+ // Persist messages
25
+ useEffect(() => {
26
+ localStorage.setItem('test_messages', JSON.stringify(messages));
27
+ }, [messages]);
28
+
29
+ // Persist selected model
30
+ useEffect(() => {
31
+ if (selectedModel) {
32
+ localStorage.setItem('test_selected_model', selectedModel);
33
+ }
34
+ }, [selectedModel]);
35
+ const [isLoading, setIsLoading] = useState(false);
36
+ const [isStreaming, setIsStreaming] = useState(false);
37
+ const messagesEndRef = useRef(null);
38
+
39
+ const fetchModels = async () => {
40
+ try {
41
+ const headers = {};
42
+ if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
43
+ else if (adminToken) headers['Authorization'] = `Bearer ${adminToken}`;
44
+
45
+ const res = await fetch('/v1/models', {
46
+ headers: {
47
+ 'Authorization': `Bearer ${apiKey || 'sk-test'}`
48
+ }
49
+ });
50
+
51
+ if (res.ok) {
52
+ const data = await res.json();
53
+ setModels(data.data || []);
54
+ if (data.data?.length > 0 && !selectedModel) setSelectedModel(data.data[0].id);
55
+ }
56
+ } catch (error) {
57
+ console.error('Failed to fetch models', error);
58
+ }
59
+ };
60
+
61
+ useEffect(() => {
62
+ fetchModels();
63
+ }, []);
64
+
65
+ useEffect(() => {
66
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
67
+ }, [messages]);
68
+
69
+ const handleSend = async () => {
70
+ if (!input.trim() || !selectedModel) return;
71
+
72
+ const userMsg = { role: 'user', content: input };
73
+ setMessages(prev => [...prev, userMsg]);
74
+ setInput('');
75
+ setIsLoading(true);
76
+ setIsStreaming(true);
77
+
78
+ const assistantMsgId = Date.now();
79
+ setMessages(prev => [...prev, { role: 'assistant', content: '', id: assistantMsgId }]);
80
+
81
+ try {
82
+ const response = await fetch('/v1/chat/completions', {
83
+ method: 'POST',
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ 'Authorization': `Bearer ${apiKey || 'sk-test'}`
87
+ },
88
+ body: JSON.stringify({
89
+ model: selectedModel,
90
+ messages: [...messages, userMsg].map(m => ({ role: m.role, content: m.content })),
91
+ stream: true
92
+ })
93
+ });
94
+
95
+ if (!response.ok) {
96
+ throw new Error(`HTTP error! status: ${response.status}`);
97
+ }
98
+
99
+ const reader = response.body.getReader();
100
+ const decoder = new TextDecoder();
101
+ let assistantContent = '';
102
+
103
+ while (true) {
104
+ const { done, value } = await reader.read();
105
+ if (done) break;
106
+
107
+ const chunk = decoder.decode(value);
108
+ const lines = chunk.split('\n');
109
+
110
+ for (const line of lines) {
111
+ if (line.startsWith('data: ') && line !== 'data: [DONE]') {
112
+ try {
113
+ const data = JSON.parse(line.slice(6));
114
+ const content = data.choices[0]?.delta?.content || '';
115
+ assistantContent += content;
116
+
117
+ setMessages(prev => prev.map(msg =>
118
+ msg.id === assistantMsgId ? { ...msg, content: assistantContent } : msg
119
+ ));
120
+ } catch (e) {
121
+ console.error('Error parsing chunk', e);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ } catch (error) {
127
+ setMessages(prev => prev.map(msg =>
128
+ msg.id === assistantMsgId ? { ...msg, content: `Error: ${error.message}` } : msg
129
+ ));
130
+ } finally {
131
+ setIsLoading(false);
132
+ setIsStreaming(false);
133
+ }
134
+ };
135
+
136
+ return (
137
+ <div className="h-[calc(100vh-8rem)] flex flex-col lg:flex-row gap-6">
138
+ {/* Chat Area */}
139
+ <div className="flex-1 flex flex-col bg-white rounded-xl border border-zinc-200 shadow-sm overflow-hidden">
140
+ <div className="flex-1 overflow-y-auto p-6 space-y-6 bg-zinc-50/30">
141
+ {messages.length === 0 && (
142
+ <div className="h-full flex flex-col items-center justify-center text-zinc-400">
143
+ <Bot className="w-12 h-12 mb-4 opacity-20" />
144
+ <p className="text-sm font-medium">开始一段新的对话...</p>
145
+ </div>
146
+ )}
147
+ {messages.map((msg, idx) => (
148
+ <motion.div
149
+ key={idx}
150
+ initial={{ opacity: 0, y: 10 }}
151
+ animate={{ opacity: 1, y: 0 }}
152
+ className={cn(
153
+ "flex gap-4 max-w-3xl",
154
+ msg.role === 'user' ? "ml-auto flex-row-reverse" : ""
155
+ )}
156
+ >
157
+ <div className={cn(
158
+ "w-8 h-8 rounded-full flex items-center justify-center shrink-0 border",
159
+ msg.role === 'user'
160
+ ? "bg-zinc-900 text-white border-zinc-900"
161
+ : "bg-white text-zinc-600 border-zinc-200"
162
+ )}>
163
+ {msg.role === 'user' ? <User className="w-4 h-4" /> : <Bot className="w-4 h-4" />}
164
+ </div>
165
+ <div className={cn(
166
+ "p-4 rounded-2xl text-sm leading-relaxed shadow-sm border",
167
+ msg.role === 'user'
168
+ ? "bg-zinc-900 text-white border-zinc-900 rounded-tr-none"
169
+ : "bg-white border-zinc-200 text-zinc-800 rounded-tl-none"
170
+ )}>
171
+ <div className="whitespace-pre-wrap">{msg.content}</div>
172
+ </div>
173
+ </motion.div>
174
+ ))}
175
+ <div ref={messagesEndRef} />
176
+ </div>
177
+
178
+ <div className="p-4 bg-white border-t border-zinc-200">
179
+ <div className="flex gap-3">
180
+ <input
181
+ type="text"
182
+ value={input}
183
+ onChange={(e) => setInput(e.target.value)}
184
+ onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
185
+ placeholder="输入消息..."
186
+ className="flex-1 px-4 py-3 bg-zinc-50 border border-zinc-200 rounded-xl focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 outline-none transition-all text-sm placeholder:text-zinc-400"
187
+ />
188
+ <button
189
+ onClick={handleSend}
190
+ disabled={isLoading || !input.trim()}
191
+ className="px-6 py-3 bg-zinc-900 hover:bg-zinc-800 text-white font-medium rounded-xl transition-colors disabled:opacity-50 shadow-sm flex items-center gap-2 text-sm"
192
+ >
193
+ <Send className="w-4 h-4" />
194
+ 发送
195
+ </button>
196
+ </div>
197
+ </div>
198
+ </div>
199
+
200
+ {/* Settings Sidebar */}
201
+ <div className="w-full lg:w-80 flex flex-col gap-6">
202
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm space-y-6">
203
+ <h3 className="font-semibold text-zinc-900 flex items-center gap-2 text-base">
204
+ <Settings2 className="w-5 h-5" />
205
+ 配置
206
+ </h3>
207
+
208
+ <div className="space-y-5">
209
+ <div>
210
+ <label className="block text-xs font-medium text-zinc-500 mb-1.5">模型</label>
211
+ <div className="flex gap-2">
212
+ <select
213
+ value={selectedModel}
214
+ onChange={(e) => setSelectedModel(e.target.value)}
215
+ className="flex-1 px-3 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-sm focus:border-zinc-900 outline-none transition-all"
216
+ >
217
+ {models.length === 0 && <option>加载中...</option>}
218
+ {models.map(m => (
219
+ <option key={m.id} value={m.id}>{m.id}</option>
220
+ ))}
221
+ </select>
222
+ <button
223
+ onClick={fetchModels}
224
+ className="p-2 text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 rounded-lg transition-colors"
225
+ >
226
+ <RefreshCw className="w-4 h-4" />
227
+ </button>
228
+ </div>
229
+ </div>
230
+
231
+ <div>
232
+ <label className="block text-xs font-medium text-zinc-500 mb-1.5">API Key</label>
233
+ <div className="relative">
234
+ <Key className="absolute left-3 top-2.5 w-4 h-4 text-zinc-400" />
235
+ <input
236
+ type="password"
237
+ value={apiKey}
238
+ onChange={(e) => setApiKey(e.target.value)}
239
+ placeholder="sk-..."
240
+ className="w-full pl-9 pr-3 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-sm focus:border-zinc-900 outline-none transition-all placeholder:text-zinc-400"
241
+ />
242
+ </div>
243
+ </div>
244
+
245
+ <button
246
+ onClick={() => setMessages([])}
247
+ className="w-full py-2.5 flex items-center justify-center gap-2 text-red-600 hover:bg-red-50 border border-red-100 hover:border-red-200 rounded-lg transition-colors text-sm font-medium"
248
+ >
249
+ <Trash2 className="w-4 h-4" />
250
+ 清空对话
251
+ </button>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
256
+ );
257
+ }
client/src/pages/Tokens.jsx ADDED
@@ -0,0 +1,396 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import {
4
+ Plus, RefreshCw, Trash2, Power, LogIn, Upload, Download,
5
+ CheckCircle2, XCircle, Search, MoreVertical, AlertCircle
6
+ } from 'lucide-react';
7
+ import { useAuth } from '../context/AuthContext';
8
+ import { cn } from '../lib/utils';
9
+
10
+ export default function Tokens() {
11
+ const { token: adminToken } = useAuth();
12
+ const [tokens, setTokens] = useState([]);
13
+ const [isLoading, setIsLoading] = useState(true);
14
+ const [selectedTokens, setSelectedTokens] = useState(new Set());
15
+ const [manualUrl, setManualUrl] = useState('');
16
+ const [isAdding, setIsAdding] = useState(false);
17
+ const [message, setMessage] = useState({ type: '', content: '' });
18
+
19
+ const fetchTokens = async () => {
20
+ setIsLoading(true);
21
+ try {
22
+ const res = await fetch('/admin/tokens', {
23
+ headers: { 'X-Admin-Token': adminToken }
24
+ });
25
+ const data = await res.json();
26
+
27
+ // Fetch details for names
28
+ if (data.length > 0) {
29
+ const indices = data.map(t => t.index);
30
+ const detailsRes = await fetch('/admin/tokens/details', {
31
+ method: 'POST',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'X-Admin-Token': adminToken
35
+ },
36
+ body: JSON.stringify({ indices })
37
+ });
38
+ const details = await detailsRes.json();
39
+ const detailsMap = {};
40
+ details.forEach(d => detailsMap[d.index] = d);
41
+
42
+ const enrichedTokens = data.map(t => ({
43
+ ...t,
44
+ ...detailsMap[t.index]
45
+ }));
46
+ setTokens(enrichedTokens);
47
+ } else {
48
+ setTokens([]);
49
+ }
50
+ } catch (error) {
51
+ console.error('Failed to fetch tokens', error);
52
+ setMessage({ type: 'error', content: '加载 Token 失败' });
53
+ } finally {
54
+ setIsLoading(false);
55
+ }
56
+ };
57
+
58
+ useEffect(() => {
59
+ fetchTokens();
60
+ }, [adminToken]);
61
+
62
+ const handleGoogleLogin = async () => {
63
+ try {
64
+ const res = await fetch('/admin/tokens/login', {
65
+ method: 'POST',
66
+ headers: { 'X-Admin-Token': adminToken }
67
+ });
68
+ const data = await res.json();
69
+ if (data.success && data.authUrl) {
70
+ window.open(data.authUrl, '_blank');
71
+ setMessage({ type: 'info', content: '已打开登录页面,完成后请刷新列表' });
72
+ // Auto refresh after 10s
73
+ setTimeout(fetchTokens, 10000);
74
+ } else {
75
+ setMessage({ type: 'error', content: data.message || '启动登录失败' });
76
+ }
77
+ } catch (error) {
78
+ setMessage({ type: 'error', content: '请求失败: ' + error.message });
79
+ }
80
+ };
81
+
82
+ const handleManualAdd = async () => {
83
+ if (!manualUrl) return;
84
+ setIsAdding(true);
85
+ try {
86
+ // Extract code from URL if full URL is pasted
87
+ let code = manualUrl;
88
+ if (manualUrl.includes('code=')) {
89
+ code = new URL(manualUrl).searchParams.get('code');
90
+ }
91
+
92
+ const res = await fetch('/admin/tokens', {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ 'X-Admin-Token': adminToken
97
+ },
98
+ body: JSON.stringify({ url: manualUrl })
99
+ });
100
+ const data = await res.json();
101
+ if (data.success) {
102
+ setMessage({ type: 'success', content: 'Token 添加成功' });
103
+ setManualUrl('');
104
+ fetchTokens();
105
+ } else {
106
+ setMessage({ type: 'error', content: data.error || '添加失败' });
107
+ }
108
+ } catch (error) {
109
+ setMessage({ type: 'error', content: '请求失败: ' + error.message });
110
+ } finally {
111
+ setIsAdding(false);
112
+ }
113
+ };
114
+
115
+ const toggleToken = async (index, enable) => {
116
+ try {
117
+ const res = await fetch('/admin/tokens/toggle', {
118
+ method: 'POST',
119
+ headers: {
120
+ 'Content-Type': 'application/json',
121
+ 'X-Admin-Token': adminToken
122
+ },
123
+ body: JSON.stringify({ index, enable })
124
+ });
125
+ if (res.ok) {
126
+ fetchTokens();
127
+ }
128
+ } catch (error) {
129
+ console.error('Toggle failed', error);
130
+ }
131
+ };
132
+
133
+ const deleteToken = async (index) => {
134
+ if (!confirm('确定要删除这个 Token 吗?')) return;
135
+ try {
136
+ const res = await fetch(`/admin/tokens/${index}`, {
137
+ method: 'DELETE',
138
+ headers: { 'X-Admin-Token': adminToken }
139
+ });
140
+ if (res.ok) {
141
+ fetchTokens();
142
+ const newSelected = new Set(selectedTokens);
143
+ newSelected.delete(index);
144
+ setSelectedTokens(newSelected);
145
+ }
146
+ } catch (error) {
147
+ console.error('Delete failed', error);
148
+ }
149
+ };
150
+
151
+ const toggleSelection = (index) => {
152
+ const newSelected = new Set(selectedTokens);
153
+ if (newSelected.has(index)) {
154
+ newSelected.delete(index);
155
+ } else {
156
+ newSelected.add(index);
157
+ }
158
+ setSelectedTokens(newSelected);
159
+ };
160
+
161
+ const selectAll = () => {
162
+ if (selectedTokens.size === tokens.length) {
163
+ setSelectedTokens(new Set());
164
+ } else {
165
+ setSelectedTokens(new Set(tokens.map(t => t.index)));
166
+ }
167
+ };
168
+
169
+ const exportTokens = async () => {
170
+ if (selectedTokens.size === 0) return alert('请先选择要导出的账号');
171
+ try {
172
+ const res = await fetch('/admin/tokens/export', {
173
+ method: 'POST',
174
+ headers: {
175
+ 'Content-Type': 'application/json',
176
+ 'X-Admin-Token': adminToken
177
+ },
178
+ body: JSON.stringify({ indices: Array.from(selectedTokens) })
179
+ });
180
+ if (res.ok) {
181
+ const blob = await res.blob();
182
+ const url = window.URL.createObjectURL(blob);
183
+ const a = document.createElement('a');
184
+ a.href = url;
185
+ a.download = `tokens_export_${new Date().toISOString().slice(0, 10)}.zip`;
186
+ a.click();
187
+ }
188
+ } catch (error) {
189
+ console.error('Export failed', error);
190
+ }
191
+ };
192
+
193
+ const importTokens = () => {
194
+ const input = document.createElement('input');
195
+ input.type = 'file';
196
+ input.accept = '.zip';
197
+ input.onchange = async (e) => {
198
+ const file = e.target.files[0];
199
+ if (!file) return;
200
+
201
+ const formData = new FormData();
202
+ formData.append('file', file);
203
+
204
+ try {
205
+ const res = await fetch('/admin/tokens/import', {
206
+ method: 'POST',
207
+ headers: { 'X-Admin-Token': adminToken },
208
+ body: formData
209
+ });
210
+ const data = await res.json();
211
+ if (data.success) {
212
+ setMessage({ type: 'success', content: `成功导入 ${data.count} 个 Token` });
213
+ fetchTokens();
214
+ } else {
215
+ setMessage({ type: 'error', content: data.error || '导入失败' });
216
+ }
217
+ } catch (error) {
218
+ setMessage({ type: 'error', content: '导入失败: ' + error.message });
219
+ }
220
+ };
221
+ input.click();
222
+ };
223
+
224
+ return (
225
+ <div className="space-y-6">
226
+ <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 pb-2">
227
+ <div>
228
+ <h2 className="text-2xl font-semibold text-zinc-900 tracking-tight">Token 管理</h2>
229
+ <p className="text-base text-zinc-500 mt-1">管理 Google OAuth 账号和 Access Token</p>
230
+ </div>
231
+ <div className="flex gap-2">
232
+ <button
233
+ onClick={fetchTokens}
234
+ className="p-2.5 text-zinc-600 hover:bg-zinc-100 rounded-lg transition-colors"
235
+ title="刷新列表"
236
+ >
237
+ <RefreshCw className={cn("w-5 h-5", isLoading && "animate-spin")} />
238
+ </button>
239
+ </div>
240
+ </div>
241
+
242
+ {/* Actions Card */}
243
+ <div className="bg-white rounded-xl border border-zinc-200 p-6 shadow-sm space-y-6">
244
+ <div className="flex flex-wrap gap-4">
245
+ <button
246
+ onClick={handleGoogleLogin}
247
+ className="flex items-center gap-2 px-5 py-2.5 bg-white border border-zinc-200 hover:bg-zinc-50 text-zinc-700 font-medium rounded-xl transition-all shadow-sm hover:shadow-md text-sm"
248
+ >
249
+ <LogIn className="w-4 h-4 text-blue-600" />
250
+ Google 登录
251
+ </button>
252
+ <div className="w-px h-10 bg-zinc-200 hidden md:block" />
253
+ <button
254
+ onClick={exportTokens}
255
+ className="flex items-center gap-2 px-5 py-2.5 bg-amber-50 hover:bg-amber-100 text-amber-700 font-medium rounded-xl transition-colors text-sm"
256
+ >
257
+ <Download className="w-4 h-4" />
258
+ 导出选中
259
+ </button>
260
+ <button
261
+ onClick={importTokens}
262
+ className="flex items-center gap-2 px-5 py-2.5 bg-emerald-50 hover:bg-emerald-100 text-emerald-700 font-medium rounded-xl transition-colors text-sm"
263
+ >
264
+ <Upload className="w-4 h-4" />
265
+ 导入
266
+ </button>
267
+ </div>
268
+
269
+ <div className="flex gap-3 items-center">
270
+ <input
271
+ type="text"
272
+ value={manualUrl}
273
+ onChange={(e) => setManualUrl(e.target.value)}
274
+ placeholder="粘贴回调链接以手动添加..."
275
+ className="flex-1 px-4 py-2.5 border border-zinc-200 rounded-xl focus:ring-2 focus:ring-zinc-900/5 focus:border-zinc-900 outline-none transition-all text-sm placeholder:text-zinc-400"
276
+ />
277
+ <button
278
+ onClick={handleManualAdd}
279
+ disabled={!manualUrl || isAdding}
280
+ className="px-6 py-2.5 bg-zinc-900 hover:bg-zinc-800 text-white font-medium rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-sm"
281
+ >
282
+ {isAdding ? '添加中...' : '添加'}
283
+ </button>
284
+ </div>
285
+
286
+ <AnimatePresence>
287
+ {message.content && (
288
+ <motion.div
289
+ initial={{ opacity: 0, height: 0 }}
290
+ animate={{ opacity: 1, height: 'auto' }}
291
+ exit={{ opacity: 0, height: 0 }}
292
+ className={cn(
293
+ "flex items-center gap-2 p-3 rounded-lg text-sm font-medium",
294
+ message.type === 'error' ? "bg-red-50 text-red-600 border border-red-100" :
295
+ message.type === 'success' ? "bg-emerald-50 text-emerald-600 border border-emerald-100" :
296
+ "bg-blue-50 text-blue-600 border border-blue-100"
297
+ )}
298
+ >
299
+ <AlertCircle className="w-4 h-4" />
300
+ {message.content}
301
+ </motion.div>
302
+ )}
303
+ </AnimatePresence>
304
+ </div>
305
+
306
+ {/* Token List */}
307
+ <div className="bg-white rounded-xl border border-zinc-200 overflow-hidden shadow-sm">
308
+ <div className="px-6 py-4 border-b border-zinc-100 flex items-center justify-between bg-zinc-50/30">
309
+ <div className="flex items-center gap-3">
310
+ <input
311
+ type="checkbox"
312
+ checked={tokens.length > 0 && selectedTokens.size === tokens.length}
313
+ onChange={selectAll}
314
+ className="w-4 h-4 rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
315
+ />
316
+ <span className="text-sm font-medium text-zinc-600">全选 ({selectedTokens.size})</span>
317
+ </div>
318
+ <div className="text-sm text-zinc-400">共 {tokens.length} 个账号</div>
319
+ </div>
320
+
321
+ {tokens.length === 0 ? (
322
+ <div className="p-12 text-center text-zinc-400">
323
+ {isLoading ? '加载中...' : '暂无 Token 账号'}
324
+ </div>
325
+ ) : (
326
+ <div className="divide-y divide-zinc-100">
327
+ {tokens.map((t) => (
328
+ <motion.div
329
+ key={t.index}
330
+ layout
331
+ initial={{ opacity: 0 }}
332
+ animate={{ opacity: 1 }}
333
+ className={cn(
334
+ "p-5 hover:bg-zinc-50/50 transition-colors group",
335
+ selectedTokens.has(t.index) && "bg-zinc-50"
336
+ )}
337
+ >
338
+ <div className="flex items-start gap-5">
339
+ <div className="pt-1">
340
+ <input
341
+ type="checkbox"
342
+ checked={selectedTokens.has(t.index)}
343
+ onChange={() => toggleSelection(t.index)}
344
+ className="w-4 h-4 rounded border-zinc-300 text-zinc-900 focus:ring-zinc-900"
345
+ />
346
+ </div>
347
+ <div className="flex-1 min-w-0">
348
+ <div className="flex items-center gap-3 mb-1.5">
349
+ <h3 className="font-semibold text-zinc-900 truncate text-base">{t.name || 'Unknown'}</h3>
350
+ {t.enable ? (
351
+ <span className="px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 text-xs font-medium flex items-center gap-1">
352
+ <CheckCircle2 className="w-3 h-3" /> 已启用
353
+ </span>
354
+ ) : (
355
+ <span className="px-2 py-0.5 rounded-full bg-zinc-100 text-zinc-600 text-xs font-medium flex items-center gap-1">
356
+ <XCircle className="w-3 h-3" /> 已禁用
357
+ </span>
358
+ )}
359
+ </div>
360
+ <p className="text-sm text-zinc-500 mb-3">{t.email || 'No Email'}</p>
361
+ <div className="bg-zinc-50 rounded-lg px-3 py-2 text-xs font-mono text-zinc-600 truncate max-w-2xl border border-zinc-200/50">
362
+ {t.access_token}
363
+ </div>
364
+ <div className="flex items-center gap-4 mt-3 text-xs text-zinc-400">
365
+ <span>创建: {t.created}</span>
366
+ <span>过期: {t.expires_in}s</span>
367
+ </div>
368
+ </div>
369
+ <div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
370
+ <button
371
+ onClick={() => toggleToken(t.index, !t.enable)}
372
+ className={cn(
373
+ "p-2.5 rounded-lg transition-colors",
374
+ t.enable ? "text-amber-600 hover:bg-amber-50" : "text-emerald-600 hover:bg-emerald-50"
375
+ )}
376
+ title={t.enable ? "禁用" : "启用"}
377
+ >
378
+ <Power className="w-4 h-4" />
379
+ </button>
380
+ <button
381
+ onClick={() => deleteToken(t.index)}
382
+ className="p-2.5 text-zinc-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
383
+ title="删除"
384
+ >
385
+ <Trash2 className="w-4 h-4" />
386
+ </button>
387
+ </div>
388
+ </div>
389
+ </motion.div>
390
+ ))}
391
+ </div>
392
+ )}
393
+ </div>
394
+ </div>
395
+ );
396
+ }
client/tailwind.config.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ border: "hsl(var(--border))",
11
+ input: "hsl(var(--input))",
12
+ ring: "hsl(var(--ring))",
13
+ background: "hsl(var(--background))",
14
+ foreground: "hsl(var(--foreground))",
15
+ primary: {
16
+ DEFAULT: "hsl(var(--primary))",
17
+ foreground: "hsl(var(--primary-foreground))",
18
+ },
19
+ secondary: {
20
+ DEFAULT: "hsl(var(--secondary))",
21
+ foreground: "hsl(var(--secondary-foreground))",
22
+ },
23
+ destructive: {
24
+ DEFAULT: "hsl(var(--destructive))",
25
+ foreground: "hsl(var(--destructive-foreground))",
26
+ },
27
+ muted: {
28
+ DEFAULT: "hsl(var(--muted))",
29
+ foreground: "hsl(var(--muted-foreground))",
30
+ },
31
+ accent: {
32
+ DEFAULT: "hsl(var(--accent))",
33
+ foreground: "hsl(var(--accent-foreground))",
34
+ },
35
+ popover: {
36
+ DEFAULT: "hsl(var(--popover))",
37
+ foreground: "hsl(var(--popover-foreground))",
38
+ },
39
+ card: {
40
+ DEFAULT: "hsl(var(--card))",
41
+ foreground: "hsl(var(--card-foreground))",
42
+ },
43
+ },
44
+ borderRadius: {
45
+ lg: "var(--radius)",
46
+ md: "calc(var(--radius) - 2px)",
47
+ sm: "calc(var(--radius) - 4px)",
48
+ },
49
+ fontFamily: {
50
+ sans: ['Inter', 'sans-serif'],
51
+ },
52
+ },
53
+ },
54
+ plugins: [],
55
+ }
client/vite.config.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import path from 'path'
4
+
5
+ // https://vitejs.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react()],
8
+ resolve: {
9
+ alias: {
10
+ '@': path.resolve(__dirname, './src'),
11
+ },
12
+ },
13
+ build: {
14
+ emptyOutDir: true,
15
+ },
16
+ server: {
17
+ proxy: {
18
+ '/admin': 'http://localhost:8045',
19
+ '/v1': 'http://localhost:8045',
20
+ }
21
+ }
22
+ })
config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "server": {
3
+ "port": 8045,
4
+ "host": "0.0.0.0"
5
+ },
6
+ "api": {
7
+ "url": "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent?alt=sse",
8
+ "modelsUrl": "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels",
9
+ "host": "daily-cloudcode-pa.sandbox.googleapis.com",
10
+ "userAgent": "antigravity/1.11.3 windows/amd64"
11
+ },
12
+ "defaults": {
13
+ "temperature": 1,
14
+ "top_p": 0.85,
15
+ "top_k": 50,
16
+ "max_tokens": 8096
17
+ },
18
+ "security": {
19
+ "maxRequestSize": "50mb",
20
+ "apiKey": "sk-admin",
21
+ "adminPassword": "admin123"
22
+ },
23
+ "systemInstruction": "你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演"
24
+ }
node_modules/.bin/crc32 ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -x "$basedir/node" ]; then
13
+ exec "$basedir/node" "$basedir/../crc-32/bin/crc32.njs" "$@"
14
+ else
15
+ exec node "$basedir/../crc-32/bin/crc32.njs" "$@"
16
+ fi
node_modules/.bin/crc32.cmd ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @ECHO off
2
+ GOTO start
3
+ :find_dp0
4
+ SET dp0=%~dp0
5
+ EXIT /b
6
+ :start
7
+ SETLOCAL
8
+ CALL :find_dp0
9
+
10
+ IF EXIST "%dp0%\node.exe" (
11
+ SET "_prog=%dp0%\node.exe"
12
+ ) ELSE (
13
+ SET "_prog=node"
14
+ SET PATHEXT=%PATHEXT:;.JS;=;%
15
+ )
16
+
17
+ endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\crc-32\bin\crc32.njs" %*
node_modules/.bin/crc32.ps1 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env pwsh
2
+ $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
3
+
4
+ $exe=""
5
+ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
6
+ # Fix case when both the Windows and Linux builds of Node
7
+ # are installed in the same directory
8
+ $exe=".exe"
9
+ }
10
+ $ret=0
11
+ if (Test-Path "$basedir/node$exe") {
12
+ # Support pipeline input
13
+ if ($MyInvocation.ExpectingInput) {
14
+ $input | & "$basedir/node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
15
+ } else {
16
+ & "$basedir/node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
17
+ }
18
+ $ret=$LASTEXITCODE
19
+ } else {
20
+ # Support pipeline input
21
+ if ($MyInvocation.ExpectingInput) {
22
+ $input | & "node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
23
+ } else {
24
+ & "node$exe" "$basedir/../crc-32/bin/crc32.njs" $args
25
+ }
26
+ $ret=$LASTEXITCODE
27
+ }
28
+ exit $ret
node_modules/.bin/glob ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -x "$basedir/node" ]; then
13
+ exec "$basedir/node" "$basedir/../glob/dist/esm/bin.mjs" "$@"
14
+ else
15
+ exec node "$basedir/../glob/dist/esm/bin.mjs" "$@"
16
+ fi
node_modules/.bin/glob.cmd ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @ECHO off
2
+ GOTO start
3
+ :find_dp0
4
+ SET dp0=%~dp0
5
+ EXIT /b
6
+ :start
7
+ SETLOCAL
8
+ CALL :find_dp0
9
+
10
+ IF EXIST "%dp0%\node.exe" (
11
+ SET "_prog=%dp0%\node.exe"
12
+ ) ELSE (
13
+ SET "_prog=node"
14
+ SET PATHEXT=%PATHEXT:;.JS;=;%
15
+ )
16
+
17
+ endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\glob\dist\esm\bin.mjs" %*
node_modules/.bin/glob.ps1 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env pwsh
2
+ $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
3
+
4
+ $exe=""
5
+ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
6
+ # Fix case when both the Windows and Linux builds of Node
7
+ # are installed in the same directory
8
+ $exe=".exe"
9
+ }
10
+ $ret=0
11
+ if (Test-Path "$basedir/node$exe") {
12
+ # Support pipeline input
13
+ if ($MyInvocation.ExpectingInput) {
14
+ $input | & "$basedir/node$exe" "$basedir/../glob/dist/esm/bin.mjs" $args
15
+ } else {
16
+ & "$basedir/node$exe" "$basedir/../glob/dist/esm/bin.mjs" $args
17
+ }
18
+ $ret=$LASTEXITCODE
19
+ } else {
20
+ # Support pipeline input
21
+ if ($MyInvocation.ExpectingInput) {
22
+ $input | & "node$exe" "$basedir/../glob/dist/esm/bin.mjs" $args
23
+ } else {
24
+ & "node$exe" "$basedir/../glob/dist/esm/bin.mjs" $args
25
+ }
26
+ $ret=$LASTEXITCODE
27
+ }
28
+ exit $ret
node_modules/.bin/mkdirp ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -x "$basedir/node" ]; then
13
+ exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@"
14
+ else
15
+ exec node "$basedir/../mkdirp/bin/cmd.js" "$@"
16
+ fi
node_modules/.bin/mkdirp.cmd ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @ECHO off
2
+ GOTO start
3
+ :find_dp0
4
+ SET dp0=%~dp0
5
+ EXIT /b
6
+ :start
7
+ SETLOCAL
8
+ CALL :find_dp0
9
+
10
+ IF EXIST "%dp0%\node.exe" (
11
+ SET "_prog=%dp0%\node.exe"
12
+ ) ELSE (
13
+ SET "_prog=node"
14
+ SET PATHEXT=%PATHEXT:;.JS;=;%
15
+ )
16
+
17
+ endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mkdirp\bin\cmd.js" %*
node_modules/.bin/mkdirp.ps1 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env pwsh
2
+ $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
3
+
4
+ $exe=""
5
+ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
6
+ # Fix case when both the Windows and Linux builds of Node
7
+ # are installed in the same directory
8
+ $exe=".exe"
9
+ }
10
+ $ret=0
11
+ if (Test-Path "$basedir/node$exe") {
12
+ # Support pipeline input
13
+ if ($MyInvocation.ExpectingInput) {
14
+ $input | & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
15
+ } else {
16
+ & "$basedir/node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
17
+ }
18
+ $ret=$LASTEXITCODE
19
+ } else {
20
+ # Support pipeline input
21
+ if ($MyInvocation.ExpectingInput) {
22
+ $input | & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
23
+ } else {
24
+ & "node$exe" "$basedir/../mkdirp/bin/cmd.js" $args
25
+ }
26
+ $ret=$LASTEXITCODE
27
+ }
28
+ exit $ret
node_modules/.bin/node-which ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -x "$basedir/node" ]; then
13
+ exec "$basedir/node" "$basedir/../which/bin/node-which" "$@"
14
+ else
15
+ exec node "$basedir/../which/bin/node-which" "$@"
16
+ fi
node_modules/.bin/node-which.cmd ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @ECHO off
2
+ GOTO start
3
+ :find_dp0
4
+ SET dp0=%~dp0
5
+ EXIT /b
6
+ :start
7
+ SETLOCAL
8
+ CALL :find_dp0
9
+
10
+ IF EXIST "%dp0%\node.exe" (
11
+ SET "_prog=%dp0%\node.exe"
12
+ ) ELSE (
13
+ SET "_prog=node"
14
+ SET PATHEXT=%PATHEXT:;.JS;=;%
15
+ )
16
+
17
+ endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\which\bin\node-which" %*
node_modules/.bin/node-which.ps1 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env pwsh
2
+ $basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
3
+
4
+ $exe=""
5
+ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
6
+ # Fix case when both the Windows and Linux builds of Node
7
+ # are installed in the same directory
8
+ $exe=".exe"
9
+ }
10
+ $ret=0
11
+ if (Test-Path "$basedir/node$exe") {
12
+ # Support pipeline input
13
+ if ($MyInvocation.ExpectingInput) {
14
+ $input | & "$basedir/node$exe" "$basedir/../which/bin/node-which" $args
15
+ } else {
16
+ & "$basedir/node$exe" "$basedir/../which/bin/node-which" $args
17
+ }
18
+ $ret=$LASTEXITCODE
19
+ } else {
20
+ # Support pipeline input
21
+ if ($MyInvocation.ExpectingInput) {
22
+ $input | & "node$exe" "$basedir/../which/bin/node-which" $args
23
+ } else {
24
+ & "node$exe" "$basedir/../which/bin/node-which" $args
25
+ }
26
+ $ret=$LASTEXITCODE
27
+ }
28
+ exit $ret
node_modules/.package-lock.json ADDED
@@ -0,0 +1,1910 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "antigravity-to-openai",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "node_modules/@isaacs/cliui": {
8
+ "version": "8.0.2",
9
+ "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
10
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
11
+ "license": "ISC",
12
+ "dependencies": {
13
+ "string-width": "^5.1.2",
14
+ "string-width-cjs": "npm:string-width@^4.2.0",
15
+ "strip-ansi": "^7.0.1",
16
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
17
+ "wrap-ansi": "^8.1.0",
18
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
19
+ },
20
+ "engines": {
21
+ "node": ">=12"
22
+ }
23
+ },
24
+ "node_modules/@pkgjs/parseargs": {
25
+ "version": "0.11.0",
26
+ "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
27
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
28
+ "license": "MIT",
29
+ "optional": true,
30
+ "engines": {
31
+ "node": ">=14"
32
+ }
33
+ },
34
+ "node_modules/abort-controller": {
35
+ "version": "3.0.0",
36
+ "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz",
37
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
38
+ "license": "MIT",
39
+ "dependencies": {
40
+ "event-target-shim": "^5.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=6.5"
44
+ }
45
+ },
46
+ "node_modules/accepts": {
47
+ "version": "2.0.0",
48
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
49
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
50
+ "license": "MIT",
51
+ "dependencies": {
52
+ "mime-types": "^3.0.0",
53
+ "negotiator": "^1.0.0"
54
+ },
55
+ "engines": {
56
+ "node": ">= 0.6"
57
+ }
58
+ },
59
+ "node_modules/adm-zip": {
60
+ "version": "0.5.16",
61
+ "resolved": "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz",
62
+ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
63
+ "license": "MIT",
64
+ "engines": {
65
+ "node": ">=12.0"
66
+ }
67
+ },
68
+ "node_modules/ansi-regex": {
69
+ "version": "6.2.2",
70
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
71
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
72
+ "license": "MIT",
73
+ "engines": {
74
+ "node": ">=12"
75
+ },
76
+ "funding": {
77
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
78
+ }
79
+ },
80
+ "node_modules/ansi-styles": {
81
+ "version": "6.2.3",
82
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz",
83
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
84
+ "license": "MIT",
85
+ "engines": {
86
+ "node": ">=12"
87
+ },
88
+ "funding": {
89
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
90
+ }
91
+ },
92
+ "node_modules/append-field": {
93
+ "version": "1.0.0",
94
+ "resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz",
95
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
96
+ "license": "MIT"
97
+ },
98
+ "node_modules/archiver": {
99
+ "version": "7.0.1",
100
+ "resolved": "https://registry.npmmirror.com/archiver/-/archiver-7.0.1.tgz",
101
+ "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==",
102
+ "license": "MIT",
103
+ "dependencies": {
104
+ "archiver-utils": "^5.0.2",
105
+ "async": "^3.2.4",
106
+ "buffer-crc32": "^1.0.0",
107
+ "readable-stream": "^4.0.0",
108
+ "readdir-glob": "^1.1.2",
109
+ "tar-stream": "^3.0.0",
110
+ "zip-stream": "^6.0.1"
111
+ },
112
+ "engines": {
113
+ "node": ">= 14"
114
+ }
115
+ },
116
+ "node_modules/archiver-utils": {
117
+ "version": "5.0.2",
118
+ "resolved": "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-5.0.2.tgz",
119
+ "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==",
120
+ "license": "MIT",
121
+ "dependencies": {
122
+ "glob": "^10.0.0",
123
+ "graceful-fs": "^4.2.0",
124
+ "is-stream": "^2.0.1",
125
+ "lazystream": "^1.0.0",
126
+ "lodash": "^4.17.15",
127
+ "normalize-path": "^3.0.0",
128
+ "readable-stream": "^4.0.0"
129
+ },
130
+ "engines": {
131
+ "node": ">= 14"
132
+ }
133
+ },
134
+ "node_modules/async": {
135
+ "version": "3.2.6",
136
+ "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz",
137
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
138
+ "license": "MIT"
139
+ },
140
+ "node_modules/b4a": {
141
+ "version": "1.7.3",
142
+ "resolved": "https://registry.npmmirror.com/b4a/-/b4a-1.7.3.tgz",
143
+ "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==",
144
+ "license": "Apache-2.0",
145
+ "peerDependencies": {
146
+ "react-native-b4a": "*"
147
+ },
148
+ "peerDependenciesMeta": {
149
+ "react-native-b4a": {
150
+ "optional": true
151
+ }
152
+ }
153
+ },
154
+ "node_modules/balanced-match": {
155
+ "version": "1.0.2",
156
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
157
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
158
+ "license": "MIT"
159
+ },
160
+ "node_modules/bare-events": {
161
+ "version": "2.8.2",
162
+ "resolved": "https://registry.npmmirror.com/bare-events/-/bare-events-2.8.2.tgz",
163
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
164
+ "license": "Apache-2.0",
165
+ "peerDependencies": {
166
+ "bare-abort-controller": "*"
167
+ },
168
+ "peerDependenciesMeta": {
169
+ "bare-abort-controller": {
170
+ "optional": true
171
+ }
172
+ }
173
+ },
174
+ "node_modules/base64-js": {
175
+ "version": "1.5.1",
176
+ "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz",
177
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
178
+ "funding": [
179
+ {
180
+ "type": "github",
181
+ "url": "https://github.com/sponsors/feross"
182
+ },
183
+ {
184
+ "type": "patreon",
185
+ "url": "https://www.patreon.com/feross"
186
+ },
187
+ {
188
+ "type": "consulting",
189
+ "url": "https://feross.org/support"
190
+ }
191
+ ],
192
+ "license": "MIT"
193
+ },
194
+ "node_modules/body-parser": {
195
+ "version": "2.2.0",
196
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
197
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
198
+ "license": "MIT",
199
+ "dependencies": {
200
+ "bytes": "^3.1.2",
201
+ "content-type": "^1.0.5",
202
+ "debug": "^4.4.0",
203
+ "http-errors": "^2.0.0",
204
+ "iconv-lite": "^0.6.3",
205
+ "on-finished": "^2.4.1",
206
+ "qs": "^6.14.0",
207
+ "raw-body": "^3.0.0",
208
+ "type-is": "^2.0.0"
209
+ },
210
+ "engines": {
211
+ "node": ">=18"
212
+ }
213
+ },
214
+ "node_modules/brace-expansion": {
215
+ "version": "2.0.2",
216
+ "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
217
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
218
+ "license": "MIT",
219
+ "dependencies": {
220
+ "balanced-match": "^1.0.0"
221
+ }
222
+ },
223
+ "node_modules/buffer": {
224
+ "version": "6.0.3",
225
+ "resolved": "https://registry.npmmirror.com/buffer/-/buffer-6.0.3.tgz",
226
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
227
+ "funding": [
228
+ {
229
+ "type": "github",
230
+ "url": "https://github.com/sponsors/feross"
231
+ },
232
+ {
233
+ "type": "patreon",
234
+ "url": "https://www.patreon.com/feross"
235
+ },
236
+ {
237
+ "type": "consulting",
238
+ "url": "https://feross.org/support"
239
+ }
240
+ ],
241
+ "license": "MIT",
242
+ "dependencies": {
243
+ "base64-js": "^1.3.1",
244
+ "ieee754": "^1.2.1"
245
+ }
246
+ },
247
+ "node_modules/buffer-crc32": {
248
+ "version": "1.0.0",
249
+ "resolved": "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz",
250
+ "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==",
251
+ "license": "MIT",
252
+ "engines": {
253
+ "node": ">=8.0.0"
254
+ }
255
+ },
256
+ "node_modules/buffer-from": {
257
+ "version": "1.1.2",
258
+ "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
259
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
260
+ "license": "MIT"
261
+ },
262
+ "node_modules/busboy": {
263
+ "version": "1.6.0",
264
+ "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz",
265
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
266
+ "dependencies": {
267
+ "streamsearch": "^1.1.0"
268
+ },
269
+ "engines": {
270
+ "node": ">=10.16.0"
271
+ }
272
+ },
273
+ "node_modules/bytes": {
274
+ "version": "3.1.2",
275
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
276
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
277
+ "license": "MIT",
278
+ "engines": {
279
+ "node": ">= 0.8"
280
+ }
281
+ },
282
+ "node_modules/call-bind-apply-helpers": {
283
+ "version": "1.0.2",
284
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
285
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
286
+ "license": "MIT",
287
+ "dependencies": {
288
+ "es-errors": "^1.3.0",
289
+ "function-bind": "^1.1.2"
290
+ },
291
+ "engines": {
292
+ "node": ">= 0.4"
293
+ }
294
+ },
295
+ "node_modules/call-bound": {
296
+ "version": "1.0.4",
297
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
298
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
299
+ "license": "MIT",
300
+ "dependencies": {
301
+ "call-bind-apply-helpers": "^1.0.2",
302
+ "get-intrinsic": "^1.3.0"
303
+ },
304
+ "engines": {
305
+ "node": ">= 0.4"
306
+ },
307
+ "funding": {
308
+ "url": "https://github.com/sponsors/ljharb"
309
+ }
310
+ },
311
+ "node_modules/color-convert": {
312
+ "version": "2.0.1",
313
+ "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
314
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
315
+ "license": "MIT",
316
+ "dependencies": {
317
+ "color-name": "~1.1.4"
318
+ },
319
+ "engines": {
320
+ "node": ">=7.0.0"
321
+ }
322
+ },
323
+ "node_modules/color-name": {
324
+ "version": "1.1.4",
325
+ "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
326
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
327
+ "license": "MIT"
328
+ },
329
+ "node_modules/compress-commons": {
330
+ "version": "6.0.2",
331
+ "resolved": "https://registry.npmmirror.com/compress-commons/-/compress-commons-6.0.2.tgz",
332
+ "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==",
333
+ "license": "MIT",
334
+ "dependencies": {
335
+ "crc-32": "^1.2.0",
336
+ "crc32-stream": "^6.0.0",
337
+ "is-stream": "^2.0.1",
338
+ "normalize-path": "^3.0.0",
339
+ "readable-stream": "^4.0.0"
340
+ },
341
+ "engines": {
342
+ "node": ">= 14"
343
+ }
344
+ },
345
+ "node_modules/concat-stream": {
346
+ "version": "2.0.0",
347
+ "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-2.0.0.tgz",
348
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
349
+ "engines": [
350
+ "node >= 6.0"
351
+ ],
352
+ "license": "MIT",
353
+ "dependencies": {
354
+ "buffer-from": "^1.0.0",
355
+ "inherits": "^2.0.3",
356
+ "readable-stream": "^3.0.2",
357
+ "typedarray": "^0.0.6"
358
+ }
359
+ },
360
+ "node_modules/concat-stream/node_modules/readable-stream": {
361
+ "version": "3.6.2",
362
+ "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz",
363
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
364
+ "license": "MIT",
365
+ "dependencies": {
366
+ "inherits": "^2.0.3",
367
+ "string_decoder": "^1.1.1",
368
+ "util-deprecate": "^1.0.1"
369
+ },
370
+ "engines": {
371
+ "node": ">= 6"
372
+ }
373
+ },
374
+ "node_modules/content-disposition": {
375
+ "version": "1.0.1",
376
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
377
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
378
+ "license": "MIT",
379
+ "engines": {
380
+ "node": ">=18"
381
+ },
382
+ "funding": {
383
+ "type": "opencollective",
384
+ "url": "https://opencollective.com/express"
385
+ }
386
+ },
387
+ "node_modules/content-type": {
388
+ "version": "1.0.5",
389
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
390
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
391
+ "license": "MIT",
392
+ "engines": {
393
+ "node": ">= 0.6"
394
+ }
395
+ },
396
+ "node_modules/cookie": {
397
+ "version": "0.7.2",
398
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
399
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
400
+ "license": "MIT",
401
+ "engines": {
402
+ "node": ">= 0.6"
403
+ }
404
+ },
405
+ "node_modules/cookie-signature": {
406
+ "version": "1.2.2",
407
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
408
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
409
+ "license": "MIT",
410
+ "engines": {
411
+ "node": ">=6.6.0"
412
+ }
413
+ },
414
+ "node_modules/core-util-is": {
415
+ "version": "1.0.3",
416
+ "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
417
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
418
+ "license": "MIT"
419
+ },
420
+ "node_modules/crc-32": {
421
+ "version": "1.2.2",
422
+ "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz",
423
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
424
+ "license": "Apache-2.0",
425
+ "bin": {
426
+ "crc32": "bin/crc32.njs"
427
+ },
428
+ "engines": {
429
+ "node": ">=0.8"
430
+ }
431
+ },
432
+ "node_modules/crc32-stream": {
433
+ "version": "6.0.0",
434
+ "resolved": "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-6.0.0.tgz",
435
+ "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==",
436
+ "license": "MIT",
437
+ "dependencies": {
438
+ "crc-32": "^1.2.0",
439
+ "readable-stream": "^4.0.0"
440
+ },
441
+ "engines": {
442
+ "node": ">= 14"
443
+ }
444
+ },
445
+ "node_modules/cross-spawn": {
446
+ "version": "7.0.6",
447
+ "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
448
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
449
+ "license": "MIT",
450
+ "dependencies": {
451
+ "path-key": "^3.1.0",
452
+ "shebang-command": "^2.0.0",
453
+ "which": "^2.0.1"
454
+ },
455
+ "engines": {
456
+ "node": ">= 8"
457
+ }
458
+ },
459
+ "node_modules/debug": {
460
+ "version": "4.4.3",
461
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
462
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
463
+ "license": "MIT",
464
+ "dependencies": {
465
+ "ms": "^2.1.3"
466
+ },
467
+ "engines": {
468
+ "node": ">=6.0"
469
+ },
470
+ "peerDependenciesMeta": {
471
+ "supports-color": {
472
+ "optional": true
473
+ }
474
+ }
475
+ },
476
+ "node_modules/depd": {
477
+ "version": "2.0.0",
478
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
479
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
480
+ "license": "MIT",
481
+ "engines": {
482
+ "node": ">= 0.8"
483
+ }
484
+ },
485
+ "node_modules/dunder-proto": {
486
+ "version": "1.0.1",
487
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
488
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
489
+ "license": "MIT",
490
+ "dependencies": {
491
+ "call-bind-apply-helpers": "^1.0.1",
492
+ "es-errors": "^1.3.0",
493
+ "gopd": "^1.2.0"
494
+ },
495
+ "engines": {
496
+ "node": ">= 0.4"
497
+ }
498
+ },
499
+ "node_modules/eastasianwidth": {
500
+ "version": "0.2.0",
501
+ "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
502
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
503
+ "license": "MIT"
504
+ },
505
+ "node_modules/ee-first": {
506
+ "version": "1.1.1",
507
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
508
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
509
+ "license": "MIT"
510
+ },
511
+ "node_modules/emoji-regex": {
512
+ "version": "9.2.2",
513
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
514
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
515
+ "license": "MIT"
516
+ },
517
+ "node_modules/encodeurl": {
518
+ "version": "2.0.0",
519
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
520
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
521
+ "license": "MIT",
522
+ "engines": {
523
+ "node": ">= 0.8"
524
+ }
525
+ },
526
+ "node_modules/es-define-property": {
527
+ "version": "1.0.1",
528
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
529
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
530
+ "license": "MIT",
531
+ "engines": {
532
+ "node": ">= 0.4"
533
+ }
534
+ },
535
+ "node_modules/es-errors": {
536
+ "version": "1.3.0",
537
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
538
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
539
+ "license": "MIT",
540
+ "engines": {
541
+ "node": ">= 0.4"
542
+ }
543
+ },
544
+ "node_modules/es-object-atoms": {
545
+ "version": "1.1.1",
546
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
547
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
548
+ "license": "MIT",
549
+ "dependencies": {
550
+ "es-errors": "^1.3.0"
551
+ },
552
+ "engines": {
553
+ "node": ">= 0.4"
554
+ }
555
+ },
556
+ "node_modules/escape-html": {
557
+ "version": "1.0.3",
558
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
559
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
560
+ "license": "MIT"
561
+ },
562
+ "node_modules/etag": {
563
+ "version": "1.8.1",
564
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
565
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
566
+ "license": "MIT",
567
+ "engines": {
568
+ "node": ">= 0.6"
569
+ }
570
+ },
571
+ "node_modules/event-target-shim": {
572
+ "version": "5.0.1",
573
+ "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz",
574
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
575
+ "license": "MIT",
576
+ "engines": {
577
+ "node": ">=6"
578
+ }
579
+ },
580
+ "node_modules/events": {
581
+ "version": "3.3.0",
582
+ "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz",
583
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
584
+ "license": "MIT",
585
+ "engines": {
586
+ "node": ">=0.8.x"
587
+ }
588
+ },
589
+ "node_modules/events-universal": {
590
+ "version": "1.0.1",
591
+ "resolved": "https://registry.npmmirror.com/events-universal/-/events-universal-1.0.1.tgz",
592
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
593
+ "license": "Apache-2.0",
594
+ "dependencies": {
595
+ "bare-events": "^2.7.0"
596
+ }
597
+ },
598
+ "node_modules/express": {
599
+ "version": "5.1.0",
600
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
601
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
602
+ "license": "MIT",
603
+ "dependencies": {
604
+ "accepts": "^2.0.0",
605
+ "body-parser": "^2.2.0",
606
+ "content-disposition": "^1.0.0",
607
+ "content-type": "^1.0.5",
608
+ "cookie": "^0.7.1",
609
+ "cookie-signature": "^1.2.1",
610
+ "debug": "^4.4.0",
611
+ "encodeurl": "^2.0.0",
612
+ "escape-html": "^1.0.3",
613
+ "etag": "^1.8.1",
614
+ "finalhandler": "^2.1.0",
615
+ "fresh": "^2.0.0",
616
+ "http-errors": "^2.0.0",
617
+ "merge-descriptors": "^2.0.0",
618
+ "mime-types": "^3.0.0",
619
+ "on-finished": "^2.4.1",
620
+ "once": "^1.4.0",
621
+ "parseurl": "^1.3.3",
622
+ "proxy-addr": "^2.0.7",
623
+ "qs": "^6.14.0",
624
+ "range-parser": "^1.2.1",
625
+ "router": "^2.2.0",
626
+ "send": "^1.1.0",
627
+ "serve-static": "^2.2.0",
628
+ "statuses": "^2.0.1",
629
+ "type-is": "^2.0.1",
630
+ "vary": "^1.1.2"
631
+ },
632
+ "engines": {
633
+ "node": ">= 18"
634
+ },
635
+ "funding": {
636
+ "type": "opencollective",
637
+ "url": "https://opencollective.com/express"
638
+ }
639
+ },
640
+ "node_modules/fast-fifo": {
641
+ "version": "1.3.2",
642
+ "resolved": "https://registry.npmmirror.com/fast-fifo/-/fast-fifo-1.3.2.tgz",
643
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
644
+ "license": "MIT"
645
+ },
646
+ "node_modules/finalhandler": {
647
+ "version": "2.1.0",
648
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
649
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
650
+ "license": "MIT",
651
+ "dependencies": {
652
+ "debug": "^4.4.0",
653
+ "encodeurl": "^2.0.0",
654
+ "escape-html": "^1.0.3",
655
+ "on-finished": "^2.4.1",
656
+ "parseurl": "^1.3.3",
657
+ "statuses": "^2.0.1"
658
+ },
659
+ "engines": {
660
+ "node": ">= 0.8"
661
+ }
662
+ },
663
+ "node_modules/foreground-child": {
664
+ "version": "3.3.1",
665
+ "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
666
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
667
+ "license": "ISC",
668
+ "dependencies": {
669
+ "cross-spawn": "^7.0.6",
670
+ "signal-exit": "^4.0.1"
671
+ },
672
+ "engines": {
673
+ "node": ">=14"
674
+ },
675
+ "funding": {
676
+ "url": "https://github.com/sponsors/isaacs"
677
+ }
678
+ },
679
+ "node_modules/forwarded": {
680
+ "version": "0.2.0",
681
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
682
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
683
+ "license": "MIT",
684
+ "engines": {
685
+ "node": ">= 0.6"
686
+ }
687
+ },
688
+ "node_modules/fresh": {
689
+ "version": "2.0.0",
690
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
691
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
692
+ "license": "MIT",
693
+ "engines": {
694
+ "node": ">= 0.8"
695
+ }
696
+ },
697
+ "node_modules/function-bind": {
698
+ "version": "1.1.2",
699
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
700
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
701
+ "license": "MIT",
702
+ "funding": {
703
+ "url": "https://github.com/sponsors/ljharb"
704
+ }
705
+ },
706
+ "node_modules/get-intrinsic": {
707
+ "version": "1.3.0",
708
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
709
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
710
+ "license": "MIT",
711
+ "dependencies": {
712
+ "call-bind-apply-helpers": "^1.0.2",
713
+ "es-define-property": "^1.0.1",
714
+ "es-errors": "^1.3.0",
715
+ "es-object-atoms": "^1.1.1",
716
+ "function-bind": "^1.1.2",
717
+ "get-proto": "^1.0.1",
718
+ "gopd": "^1.2.0",
719
+ "has-symbols": "^1.1.0",
720
+ "hasown": "^2.0.2",
721
+ "math-intrinsics": "^1.1.0"
722
+ },
723
+ "engines": {
724
+ "node": ">= 0.4"
725
+ },
726
+ "funding": {
727
+ "url": "https://github.com/sponsors/ljharb"
728
+ }
729
+ },
730
+ "node_modules/get-proto": {
731
+ "version": "1.0.1",
732
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
733
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
734
+ "license": "MIT",
735
+ "dependencies": {
736
+ "dunder-proto": "^1.0.1",
737
+ "es-object-atoms": "^1.0.0"
738
+ },
739
+ "engines": {
740
+ "node": ">= 0.4"
741
+ }
742
+ },
743
+ "node_modules/glob": {
744
+ "version": "10.5.0",
745
+ "resolved": "https://registry.npmmirror.com/glob/-/glob-10.5.0.tgz",
746
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
747
+ "license": "ISC",
748
+ "dependencies": {
749
+ "foreground-child": "^3.1.0",
750
+ "jackspeak": "^3.1.2",
751
+ "minimatch": "^9.0.4",
752
+ "minipass": "^7.1.2",
753
+ "package-json-from-dist": "^1.0.0",
754
+ "path-scurry": "^1.11.1"
755
+ },
756
+ "bin": {
757
+ "glob": "dist/esm/bin.mjs"
758
+ },
759
+ "funding": {
760
+ "url": "https://github.com/sponsors/isaacs"
761
+ }
762
+ },
763
+ "node_modules/gopd": {
764
+ "version": "1.2.0",
765
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
766
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
767
+ "license": "MIT",
768
+ "engines": {
769
+ "node": ">= 0.4"
770
+ },
771
+ "funding": {
772
+ "url": "https://github.com/sponsors/ljharb"
773
+ }
774
+ },
775
+ "node_modules/graceful-fs": {
776
+ "version": "4.2.11",
777
+ "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz",
778
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
779
+ "license": "ISC"
780
+ },
781
+ "node_modules/has-symbols": {
782
+ "version": "1.1.0",
783
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
784
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
785
+ "license": "MIT",
786
+ "engines": {
787
+ "node": ">= 0.4"
788
+ },
789
+ "funding": {
790
+ "url": "https://github.com/sponsors/ljharb"
791
+ }
792
+ },
793
+ "node_modules/hasown": {
794
+ "version": "2.0.2",
795
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
796
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
797
+ "license": "MIT",
798
+ "dependencies": {
799
+ "function-bind": "^1.1.2"
800
+ },
801
+ "engines": {
802
+ "node": ">= 0.4"
803
+ }
804
+ },
805
+ "node_modules/http-errors": {
806
+ "version": "2.0.0",
807
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
808
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
809
+ "license": "MIT",
810
+ "dependencies": {
811
+ "depd": "2.0.0",
812
+ "inherits": "2.0.4",
813
+ "setprototypeof": "1.2.0",
814
+ "statuses": "2.0.1",
815
+ "toidentifier": "1.0.1"
816
+ },
817
+ "engines": {
818
+ "node": ">= 0.8"
819
+ }
820
+ },
821
+ "node_modules/http-errors/node_modules/statuses": {
822
+ "version": "2.0.1",
823
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
824
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
825
+ "license": "MIT",
826
+ "engines": {
827
+ "node": ">= 0.8"
828
+ }
829
+ },
830
+ "node_modules/iconv-lite": {
831
+ "version": "0.6.3",
832
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
833
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
834
+ "license": "MIT",
835
+ "dependencies": {
836
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
837
+ },
838
+ "engines": {
839
+ "node": ">=0.10.0"
840
+ }
841
+ },
842
+ "node_modules/ieee754": {
843
+ "version": "1.2.1",
844
+ "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz",
845
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
846
+ "funding": [
847
+ {
848
+ "type": "github",
849
+ "url": "https://github.com/sponsors/feross"
850
+ },
851
+ {
852
+ "type": "patreon",
853
+ "url": "https://www.patreon.com/feross"
854
+ },
855
+ {
856
+ "type": "consulting",
857
+ "url": "https://feross.org/support"
858
+ }
859
+ ],
860
+ "license": "BSD-3-Clause"
861
+ },
862
+ "node_modules/inherits": {
863
+ "version": "2.0.4",
864
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
865
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
866
+ "license": "ISC"
867
+ },
868
+ "node_modules/ipaddr.js": {
869
+ "version": "1.9.1",
870
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
871
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
872
+ "license": "MIT",
873
+ "engines": {
874
+ "node": ">= 0.10"
875
+ }
876
+ },
877
+ "node_modules/is-fullwidth-code-point": {
878
+ "version": "3.0.0",
879
+ "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
880
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
881
+ "license": "MIT",
882
+ "engines": {
883
+ "node": ">=8"
884
+ }
885
+ },
886
+ "node_modules/is-promise": {
887
+ "version": "4.0.0",
888
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
889
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
890
+ "license": "MIT"
891
+ },
892
+ "node_modules/is-stream": {
893
+ "version": "2.0.1",
894
+ "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz",
895
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
896
+ "license": "MIT",
897
+ "engines": {
898
+ "node": ">=8"
899
+ },
900
+ "funding": {
901
+ "url": "https://github.com/sponsors/sindresorhus"
902
+ }
903
+ },
904
+ "node_modules/isarray": {
905
+ "version": "1.0.0",
906
+ "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
907
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
908
+ "license": "MIT"
909
+ },
910
+ "node_modules/isexe": {
911
+ "version": "2.0.0",
912
+ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
913
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
914
+ "license": "ISC"
915
+ },
916
+ "node_modules/jackspeak": {
917
+ "version": "3.4.3",
918
+ "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
919
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
920
+ "license": "BlueOak-1.0.0",
921
+ "dependencies": {
922
+ "@isaacs/cliui": "^8.0.2"
923
+ },
924
+ "funding": {
925
+ "url": "https://github.com/sponsors/isaacs"
926
+ },
927
+ "optionalDependencies": {
928
+ "@pkgjs/parseargs": "^0.11.0"
929
+ }
930
+ },
931
+ "node_modules/lazystream": {
932
+ "version": "1.0.1",
933
+ "resolved": "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz",
934
+ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
935
+ "license": "MIT",
936
+ "dependencies": {
937
+ "readable-stream": "^2.0.5"
938
+ },
939
+ "engines": {
940
+ "node": ">= 0.6.3"
941
+ }
942
+ },
943
+ "node_modules/lazystream/node_modules/readable-stream": {
944
+ "version": "2.3.8",
945
+ "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
946
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
947
+ "license": "MIT",
948
+ "dependencies": {
949
+ "core-util-is": "~1.0.0",
950
+ "inherits": "~2.0.3",
951
+ "isarray": "~1.0.0",
952
+ "process-nextick-args": "~2.0.0",
953
+ "safe-buffer": "~5.1.1",
954
+ "string_decoder": "~1.1.1",
955
+ "util-deprecate": "~1.0.1"
956
+ }
957
+ },
958
+ "node_modules/lazystream/node_modules/safe-buffer": {
959
+ "version": "5.1.2",
960
+ "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
961
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
962
+ "license": "MIT"
963
+ },
964
+ "node_modules/lazystream/node_modules/string_decoder": {
965
+ "version": "1.1.1",
966
+ "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
967
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
968
+ "license": "MIT",
969
+ "dependencies": {
970
+ "safe-buffer": "~5.1.0"
971
+ }
972
+ },
973
+ "node_modules/lodash": {
974
+ "version": "4.17.21",
975
+ "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
976
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
977
+ "license": "MIT"
978
+ },
979
+ "node_modules/lru-cache": {
980
+ "version": "10.4.3",
981
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
982
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
983
+ "license": "ISC"
984
+ },
985
+ "node_modules/math-intrinsics": {
986
+ "version": "1.1.0",
987
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
988
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
989
+ "license": "MIT",
990
+ "engines": {
991
+ "node": ">= 0.4"
992
+ }
993
+ },
994
+ "node_modules/media-typer": {
995
+ "version": "1.1.0",
996
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
997
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
998
+ "license": "MIT",
999
+ "engines": {
1000
+ "node": ">= 0.8"
1001
+ }
1002
+ },
1003
+ "node_modules/merge-descriptors": {
1004
+ "version": "2.0.0",
1005
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
1006
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
1007
+ "license": "MIT",
1008
+ "engines": {
1009
+ "node": ">=18"
1010
+ },
1011
+ "funding": {
1012
+ "url": "https://github.com/sponsors/sindresorhus"
1013
+ }
1014
+ },
1015
+ "node_modules/mime-db": {
1016
+ "version": "1.54.0",
1017
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
1018
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
1019
+ "license": "MIT",
1020
+ "engines": {
1021
+ "node": ">= 0.6"
1022
+ }
1023
+ },
1024
+ "node_modules/mime-types": {
1025
+ "version": "3.0.1",
1026
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
1027
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
1028
+ "license": "MIT",
1029
+ "dependencies": {
1030
+ "mime-db": "^1.54.0"
1031
+ },
1032
+ "engines": {
1033
+ "node": ">= 0.6"
1034
+ }
1035
+ },
1036
+ "node_modules/minimatch": {
1037
+ "version": "9.0.5",
1038
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
1039
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
1040
+ "license": "ISC",
1041
+ "dependencies": {
1042
+ "brace-expansion": "^2.0.1"
1043
+ },
1044
+ "engines": {
1045
+ "node": ">=16 || 14 >=14.17"
1046
+ },
1047
+ "funding": {
1048
+ "url": "https://github.com/sponsors/isaacs"
1049
+ }
1050
+ },
1051
+ "node_modules/minimist": {
1052
+ "version": "1.2.8",
1053
+ "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz",
1054
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
1055
+ "license": "MIT",
1056
+ "funding": {
1057
+ "url": "https://github.com/sponsors/ljharb"
1058
+ }
1059
+ },
1060
+ "node_modules/minipass": {
1061
+ "version": "7.1.2",
1062
+ "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
1063
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
1064
+ "license": "ISC",
1065
+ "engines": {
1066
+ "node": ">=16 || 14 >=14.17"
1067
+ }
1068
+ },
1069
+ "node_modules/mkdirp": {
1070
+ "version": "0.5.6",
1071
+ "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz",
1072
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
1073
+ "license": "MIT",
1074
+ "dependencies": {
1075
+ "minimist": "^1.2.6"
1076
+ },
1077
+ "bin": {
1078
+ "mkdirp": "bin/cmd.js"
1079
+ }
1080
+ },
1081
+ "node_modules/ms": {
1082
+ "version": "2.1.3",
1083
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1084
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1085
+ "license": "MIT"
1086
+ },
1087
+ "node_modules/multer": {
1088
+ "version": "2.0.2",
1089
+ "resolved": "https://registry.npmmirror.com/multer/-/multer-2.0.2.tgz",
1090
+ "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
1091
+ "license": "MIT",
1092
+ "dependencies": {
1093
+ "append-field": "^1.0.0",
1094
+ "busboy": "^1.6.0",
1095
+ "concat-stream": "^2.0.0",
1096
+ "mkdirp": "^0.5.6",
1097
+ "object-assign": "^4.1.1",
1098
+ "type-is": "^1.6.18",
1099
+ "xtend": "^4.0.2"
1100
+ },
1101
+ "engines": {
1102
+ "node": ">= 10.16.0"
1103
+ }
1104
+ },
1105
+ "node_modules/multer/node_modules/media-typer": {
1106
+ "version": "0.3.0",
1107
+ "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
1108
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1109
+ "license": "MIT",
1110
+ "engines": {
1111
+ "node": ">= 0.6"
1112
+ }
1113
+ },
1114
+ "node_modules/multer/node_modules/mime-db": {
1115
+ "version": "1.52.0",
1116
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
1117
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1118
+ "license": "MIT",
1119
+ "engines": {
1120
+ "node": ">= 0.6"
1121
+ }
1122
+ },
1123
+ "node_modules/multer/node_modules/mime-types": {
1124
+ "version": "2.1.35",
1125
+ "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
1126
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1127
+ "license": "MIT",
1128
+ "dependencies": {
1129
+ "mime-db": "1.52.0"
1130
+ },
1131
+ "engines": {
1132
+ "node": ">= 0.6"
1133
+ }
1134
+ },
1135
+ "node_modules/multer/node_modules/type-is": {
1136
+ "version": "1.6.18",
1137
+ "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
1138
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1139
+ "license": "MIT",
1140
+ "dependencies": {
1141
+ "media-typer": "0.3.0",
1142
+ "mime-types": "~2.1.24"
1143
+ },
1144
+ "engines": {
1145
+ "node": ">= 0.6"
1146
+ }
1147
+ },
1148
+ "node_modules/negotiator": {
1149
+ "version": "1.0.0",
1150
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
1151
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
1152
+ "license": "MIT",
1153
+ "engines": {
1154
+ "node": ">= 0.6"
1155
+ }
1156
+ },
1157
+ "node_modules/normalize-path": {
1158
+ "version": "3.0.0",
1159
+ "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
1160
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
1161
+ "license": "MIT",
1162
+ "engines": {
1163
+ "node": ">=0.10.0"
1164
+ }
1165
+ },
1166
+ "node_modules/object-assign": {
1167
+ "version": "4.1.1",
1168
+ "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
1169
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1170
+ "license": "MIT",
1171
+ "engines": {
1172
+ "node": ">=0.10.0"
1173
+ }
1174
+ },
1175
+ "node_modules/object-inspect": {
1176
+ "version": "1.13.4",
1177
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1178
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1179
+ "license": "MIT",
1180
+ "engines": {
1181
+ "node": ">= 0.4"
1182
+ },
1183
+ "funding": {
1184
+ "url": "https://github.com/sponsors/ljharb"
1185
+ }
1186
+ },
1187
+ "node_modules/on-finished": {
1188
+ "version": "2.4.1",
1189
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1190
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1191
+ "license": "MIT",
1192
+ "dependencies": {
1193
+ "ee-first": "1.1.1"
1194
+ },
1195
+ "engines": {
1196
+ "node": ">= 0.8"
1197
+ }
1198
+ },
1199
+ "node_modules/once": {
1200
+ "version": "1.4.0",
1201
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1202
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1203
+ "license": "ISC",
1204
+ "dependencies": {
1205
+ "wrappy": "1"
1206
+ }
1207
+ },
1208
+ "node_modules/package-json-from-dist": {
1209
+ "version": "1.0.1",
1210
+ "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
1211
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
1212
+ "license": "BlueOak-1.0.0"
1213
+ },
1214
+ "node_modules/parseurl": {
1215
+ "version": "1.3.3",
1216
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1217
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1218
+ "license": "MIT",
1219
+ "engines": {
1220
+ "node": ">= 0.8"
1221
+ }
1222
+ },
1223
+ "node_modules/path-key": {
1224
+ "version": "3.1.1",
1225
+ "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
1226
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
1227
+ "license": "MIT",
1228
+ "engines": {
1229
+ "node": ">=8"
1230
+ }
1231
+ },
1232
+ "node_modules/path-scurry": {
1233
+ "version": "1.11.1",
1234
+ "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz",
1235
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
1236
+ "license": "BlueOak-1.0.0",
1237
+ "dependencies": {
1238
+ "lru-cache": "^10.2.0",
1239
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
1240
+ },
1241
+ "engines": {
1242
+ "node": ">=16 || 14 >=14.18"
1243
+ },
1244
+ "funding": {
1245
+ "url": "https://github.com/sponsors/isaacs"
1246
+ }
1247
+ },
1248
+ "node_modules/path-to-regexp": {
1249
+ "version": "8.3.0",
1250
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
1251
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
1252
+ "license": "MIT",
1253
+ "funding": {
1254
+ "type": "opencollective",
1255
+ "url": "https://opencollective.com/express"
1256
+ }
1257
+ },
1258
+ "node_modules/process": {
1259
+ "version": "0.11.10",
1260
+ "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz",
1261
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
1262
+ "license": "MIT",
1263
+ "engines": {
1264
+ "node": ">= 0.6.0"
1265
+ }
1266
+ },
1267
+ "node_modules/process-nextick-args": {
1268
+ "version": "2.0.1",
1269
+ "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
1270
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
1271
+ "license": "MIT"
1272
+ },
1273
+ "node_modules/proxy-addr": {
1274
+ "version": "2.0.7",
1275
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1276
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1277
+ "license": "MIT",
1278
+ "dependencies": {
1279
+ "forwarded": "0.2.0",
1280
+ "ipaddr.js": "1.9.1"
1281
+ },
1282
+ "engines": {
1283
+ "node": ">= 0.10"
1284
+ }
1285
+ },
1286
+ "node_modules/qs": {
1287
+ "version": "6.14.0",
1288
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
1289
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
1290
+ "license": "BSD-3-Clause",
1291
+ "dependencies": {
1292
+ "side-channel": "^1.1.0"
1293
+ },
1294
+ "engines": {
1295
+ "node": ">=0.6"
1296
+ },
1297
+ "funding": {
1298
+ "url": "https://github.com/sponsors/ljharb"
1299
+ }
1300
+ },
1301
+ "node_modules/range-parser": {
1302
+ "version": "1.2.1",
1303
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1304
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1305
+ "license": "MIT",
1306
+ "engines": {
1307
+ "node": ">= 0.6"
1308
+ }
1309
+ },
1310
+ "node_modules/raw-body": {
1311
+ "version": "3.0.1",
1312
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
1313
+ "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
1314
+ "license": "MIT",
1315
+ "dependencies": {
1316
+ "bytes": "3.1.2",
1317
+ "http-errors": "2.0.0",
1318
+ "iconv-lite": "0.7.0",
1319
+ "unpipe": "1.0.0"
1320
+ },
1321
+ "engines": {
1322
+ "node": ">= 0.10"
1323
+ }
1324
+ },
1325
+ "node_modules/raw-body/node_modules/iconv-lite": {
1326
+ "version": "0.7.0",
1327
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
1328
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
1329
+ "license": "MIT",
1330
+ "dependencies": {
1331
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
1332
+ },
1333
+ "engines": {
1334
+ "node": ">=0.10.0"
1335
+ },
1336
+ "funding": {
1337
+ "type": "opencollective",
1338
+ "url": "https://opencollective.com/express"
1339
+ }
1340
+ },
1341
+ "node_modules/readable-stream": {
1342
+ "version": "4.7.0",
1343
+ "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz",
1344
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
1345
+ "license": "MIT",
1346
+ "dependencies": {
1347
+ "abort-controller": "^3.0.0",
1348
+ "buffer": "^6.0.3",
1349
+ "events": "^3.3.0",
1350
+ "process": "^0.11.10",
1351
+ "string_decoder": "^1.3.0"
1352
+ },
1353
+ "engines": {
1354
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1355
+ }
1356
+ },
1357
+ "node_modules/readdir-glob": {
1358
+ "version": "1.1.3",
1359
+ "resolved": "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz",
1360
+ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
1361
+ "license": "Apache-2.0",
1362
+ "dependencies": {
1363
+ "minimatch": "^5.1.0"
1364
+ }
1365
+ },
1366
+ "node_modules/readdir-glob/node_modules/minimatch": {
1367
+ "version": "5.1.6",
1368
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.6.tgz",
1369
+ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
1370
+ "license": "ISC",
1371
+ "dependencies": {
1372
+ "brace-expansion": "^2.0.1"
1373
+ },
1374
+ "engines": {
1375
+ "node": ">=10"
1376
+ }
1377
+ },
1378
+ "node_modules/router": {
1379
+ "version": "2.2.0",
1380
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
1381
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
1382
+ "license": "MIT",
1383
+ "dependencies": {
1384
+ "debug": "^4.4.0",
1385
+ "depd": "^2.0.0",
1386
+ "is-promise": "^4.0.0",
1387
+ "parseurl": "^1.3.3",
1388
+ "path-to-regexp": "^8.0.0"
1389
+ },
1390
+ "engines": {
1391
+ "node": ">= 18"
1392
+ }
1393
+ },
1394
+ "node_modules/safe-buffer": {
1395
+ "version": "5.2.1",
1396
+ "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
1397
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1398
+ "funding": [
1399
+ {
1400
+ "type": "github",
1401
+ "url": "https://github.com/sponsors/feross"
1402
+ },
1403
+ {
1404
+ "type": "patreon",
1405
+ "url": "https://www.patreon.com/feross"
1406
+ },
1407
+ {
1408
+ "type": "consulting",
1409
+ "url": "https://feross.org/support"
1410
+ }
1411
+ ],
1412
+ "license": "MIT"
1413
+ },
1414
+ "node_modules/safer-buffer": {
1415
+ "version": "2.1.2",
1416
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1417
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1418
+ "license": "MIT"
1419
+ },
1420
+ "node_modules/send": {
1421
+ "version": "1.2.0",
1422
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
1423
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
1424
+ "license": "MIT",
1425
+ "dependencies": {
1426
+ "debug": "^4.3.5",
1427
+ "encodeurl": "^2.0.0",
1428
+ "escape-html": "^1.0.3",
1429
+ "etag": "^1.8.1",
1430
+ "fresh": "^2.0.0",
1431
+ "http-errors": "^2.0.0",
1432
+ "mime-types": "^3.0.1",
1433
+ "ms": "^2.1.3",
1434
+ "on-finished": "^2.4.1",
1435
+ "range-parser": "^1.2.1",
1436
+ "statuses": "^2.0.1"
1437
+ },
1438
+ "engines": {
1439
+ "node": ">= 18"
1440
+ }
1441
+ },
1442
+ "node_modules/serve-static": {
1443
+ "version": "2.2.0",
1444
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
1445
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
1446
+ "license": "MIT",
1447
+ "dependencies": {
1448
+ "encodeurl": "^2.0.0",
1449
+ "escape-html": "^1.0.3",
1450
+ "parseurl": "^1.3.3",
1451
+ "send": "^1.2.0"
1452
+ },
1453
+ "engines": {
1454
+ "node": ">= 18"
1455
+ }
1456
+ },
1457
+ "node_modules/setprototypeof": {
1458
+ "version": "1.2.0",
1459
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1460
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1461
+ "license": "ISC"
1462
+ },
1463
+ "node_modules/shebang-command": {
1464
+ "version": "2.0.0",
1465
+ "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
1466
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
1467
+ "license": "MIT",
1468
+ "dependencies": {
1469
+ "shebang-regex": "^3.0.0"
1470
+ },
1471
+ "engines": {
1472
+ "node": ">=8"
1473
+ }
1474
+ },
1475
+ "node_modules/shebang-regex": {
1476
+ "version": "3.0.0",
1477
+ "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
1478
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
1479
+ "license": "MIT",
1480
+ "engines": {
1481
+ "node": ">=8"
1482
+ }
1483
+ },
1484
+ "node_modules/side-channel": {
1485
+ "version": "1.1.0",
1486
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1487
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1488
+ "license": "MIT",
1489
+ "dependencies": {
1490
+ "es-errors": "^1.3.0",
1491
+ "object-inspect": "^1.13.3",
1492
+ "side-channel-list": "^1.0.0",
1493
+ "side-channel-map": "^1.0.1",
1494
+ "side-channel-weakmap": "^1.0.2"
1495
+ },
1496
+ "engines": {
1497
+ "node": ">= 0.4"
1498
+ },
1499
+ "funding": {
1500
+ "url": "https://github.com/sponsors/ljharb"
1501
+ }
1502
+ },
1503
+ "node_modules/side-channel-list": {
1504
+ "version": "1.0.0",
1505
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1506
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1507
+ "license": "MIT",
1508
+ "dependencies": {
1509
+ "es-errors": "^1.3.0",
1510
+ "object-inspect": "^1.13.3"
1511
+ },
1512
+ "engines": {
1513
+ "node": ">= 0.4"
1514
+ },
1515
+ "funding": {
1516
+ "url": "https://github.com/sponsors/ljharb"
1517
+ }
1518
+ },
1519
+ "node_modules/side-channel-map": {
1520
+ "version": "1.0.1",
1521
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1522
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1523
+ "license": "MIT",
1524
+ "dependencies": {
1525
+ "call-bound": "^1.0.2",
1526
+ "es-errors": "^1.3.0",
1527
+ "get-intrinsic": "^1.2.5",
1528
+ "object-inspect": "^1.13.3"
1529
+ },
1530
+ "engines": {
1531
+ "node": ">= 0.4"
1532
+ },
1533
+ "funding": {
1534
+ "url": "https://github.com/sponsors/ljharb"
1535
+ }
1536
+ },
1537
+ "node_modules/side-channel-weakmap": {
1538
+ "version": "1.0.2",
1539
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1540
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1541
+ "license": "MIT",
1542
+ "dependencies": {
1543
+ "call-bound": "^1.0.2",
1544
+ "es-errors": "^1.3.0",
1545
+ "get-intrinsic": "^1.2.5",
1546
+ "object-inspect": "^1.13.3",
1547
+ "side-channel-map": "^1.0.1"
1548
+ },
1549
+ "engines": {
1550
+ "node": ">= 0.4"
1551
+ },
1552
+ "funding": {
1553
+ "url": "https://github.com/sponsors/ljharb"
1554
+ }
1555
+ },
1556
+ "node_modules/signal-exit": {
1557
+ "version": "4.1.0",
1558
+ "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
1559
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
1560
+ "license": "ISC",
1561
+ "engines": {
1562
+ "node": ">=14"
1563
+ },
1564
+ "funding": {
1565
+ "url": "https://github.com/sponsors/isaacs"
1566
+ }
1567
+ },
1568
+ "node_modules/statuses": {
1569
+ "version": "2.0.2",
1570
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1571
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1572
+ "license": "MIT",
1573
+ "engines": {
1574
+ "node": ">= 0.8"
1575
+ }
1576
+ },
1577
+ "node_modules/streamsearch": {
1578
+ "version": "1.1.0",
1579
+ "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz",
1580
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
1581
+ "engines": {
1582
+ "node": ">=10.0.0"
1583
+ }
1584
+ },
1585
+ "node_modules/streamx": {
1586
+ "version": "2.23.0",
1587
+ "resolved": "https://registry.npmmirror.com/streamx/-/streamx-2.23.0.tgz",
1588
+ "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
1589
+ "license": "MIT",
1590
+ "dependencies": {
1591
+ "events-universal": "^1.0.0",
1592
+ "fast-fifo": "^1.3.2",
1593
+ "text-decoder": "^1.1.0"
1594
+ }
1595
+ },
1596
+ "node_modules/string_decoder": {
1597
+ "version": "1.3.0",
1598
+ "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz",
1599
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
1600
+ "license": "MIT",
1601
+ "dependencies": {
1602
+ "safe-buffer": "~5.2.0"
1603
+ }
1604
+ },
1605
+ "node_modules/string-width": {
1606
+ "version": "5.1.2",
1607
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
1608
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
1609
+ "license": "MIT",
1610
+ "dependencies": {
1611
+ "eastasianwidth": "^0.2.0",
1612
+ "emoji-regex": "^9.2.2",
1613
+ "strip-ansi": "^7.0.1"
1614
+ },
1615
+ "engines": {
1616
+ "node": ">=12"
1617
+ },
1618
+ "funding": {
1619
+ "url": "https://github.com/sponsors/sindresorhus"
1620
+ }
1621
+ },
1622
+ "node_modules/string-width-cjs": {
1623
+ "name": "string-width",
1624
+ "version": "4.2.3",
1625
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
1626
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
1627
+ "license": "MIT",
1628
+ "dependencies": {
1629
+ "emoji-regex": "^8.0.0",
1630
+ "is-fullwidth-code-point": "^3.0.0",
1631
+ "strip-ansi": "^6.0.1"
1632
+ },
1633
+ "engines": {
1634
+ "node": ">=8"
1635
+ }
1636
+ },
1637
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
1638
+ "version": "5.0.1",
1639
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
1640
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1641
+ "license": "MIT",
1642
+ "engines": {
1643
+ "node": ">=8"
1644
+ }
1645
+ },
1646
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
1647
+ "version": "8.0.0",
1648
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
1649
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1650
+ "license": "MIT"
1651
+ },
1652
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
1653
+ "version": "6.0.1",
1654
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
1655
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1656
+ "license": "MIT",
1657
+ "dependencies": {
1658
+ "ansi-regex": "^5.0.1"
1659
+ },
1660
+ "engines": {
1661
+ "node": ">=8"
1662
+ }
1663
+ },
1664
+ "node_modules/strip-ansi": {
1665
+ "version": "7.1.2",
1666
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz",
1667
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
1668
+ "license": "MIT",
1669
+ "dependencies": {
1670
+ "ansi-regex": "^6.0.1"
1671
+ },
1672
+ "engines": {
1673
+ "node": ">=12"
1674
+ },
1675
+ "funding": {
1676
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
1677
+ }
1678
+ },
1679
+ "node_modules/strip-ansi-cjs": {
1680
+ "name": "strip-ansi",
1681
+ "version": "6.0.1",
1682
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
1683
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1684
+ "license": "MIT",
1685
+ "dependencies": {
1686
+ "ansi-regex": "^5.0.1"
1687
+ },
1688
+ "engines": {
1689
+ "node": ">=8"
1690
+ }
1691
+ },
1692
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
1693
+ "version": "5.0.1",
1694
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
1695
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1696
+ "license": "MIT",
1697
+ "engines": {
1698
+ "node": ">=8"
1699
+ }
1700
+ },
1701
+ "node_modules/tar-stream": {
1702
+ "version": "3.1.7",
1703
+ "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.1.7.tgz",
1704
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
1705
+ "license": "MIT",
1706
+ "dependencies": {
1707
+ "b4a": "^1.6.4",
1708
+ "fast-fifo": "^1.2.0",
1709
+ "streamx": "^2.15.0"
1710
+ }
1711
+ },
1712
+ "node_modules/text-decoder": {
1713
+ "version": "1.2.3",
1714
+ "resolved": "https://registry.npmmirror.com/text-decoder/-/text-decoder-1.2.3.tgz",
1715
+ "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
1716
+ "license": "Apache-2.0",
1717
+ "dependencies": {
1718
+ "b4a": "^1.6.4"
1719
+ }
1720
+ },
1721
+ "node_modules/toidentifier": {
1722
+ "version": "1.0.1",
1723
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1724
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1725
+ "license": "MIT",
1726
+ "engines": {
1727
+ "node": ">=0.6"
1728
+ }
1729
+ },
1730
+ "node_modules/type-is": {
1731
+ "version": "2.0.1",
1732
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
1733
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
1734
+ "license": "MIT",
1735
+ "dependencies": {
1736
+ "content-type": "^1.0.5",
1737
+ "media-typer": "^1.1.0",
1738
+ "mime-types": "^3.0.0"
1739
+ },
1740
+ "engines": {
1741
+ "node": ">= 0.6"
1742
+ }
1743
+ },
1744
+ "node_modules/typedarray": {
1745
+ "version": "0.0.6",
1746
+ "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz",
1747
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
1748
+ "license": "MIT"
1749
+ },
1750
+ "node_modules/unpipe": {
1751
+ "version": "1.0.0",
1752
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1753
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1754
+ "license": "MIT",
1755
+ "engines": {
1756
+ "node": ">= 0.8"
1757
+ }
1758
+ },
1759
+ "node_modules/util-deprecate": {
1760
+ "version": "1.0.2",
1761
+ "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
1762
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1763
+ "license": "MIT"
1764
+ },
1765
+ "node_modules/vary": {
1766
+ "version": "1.1.2",
1767
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1768
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1769
+ "license": "MIT",
1770
+ "engines": {
1771
+ "node": ">= 0.8"
1772
+ }
1773
+ },
1774
+ "node_modules/which": {
1775
+ "version": "2.0.2",
1776
+ "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
1777
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
1778
+ "license": "ISC",
1779
+ "dependencies": {
1780
+ "isexe": "^2.0.0"
1781
+ },
1782
+ "bin": {
1783
+ "node-which": "bin/node-which"
1784
+ },
1785
+ "engines": {
1786
+ "node": ">= 8"
1787
+ }
1788
+ },
1789
+ "node_modules/wrap-ansi": {
1790
+ "version": "8.1.0",
1791
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
1792
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
1793
+ "license": "MIT",
1794
+ "dependencies": {
1795
+ "ansi-styles": "^6.1.0",
1796
+ "string-width": "^5.0.1",
1797
+ "strip-ansi": "^7.0.1"
1798
+ },
1799
+ "engines": {
1800
+ "node": ">=12"
1801
+ },
1802
+ "funding": {
1803
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1804
+ }
1805
+ },
1806
+ "node_modules/wrap-ansi-cjs": {
1807
+ "name": "wrap-ansi",
1808
+ "version": "7.0.0",
1809
+ "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
1810
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
1811
+ "license": "MIT",
1812
+ "dependencies": {
1813
+ "ansi-styles": "^4.0.0",
1814
+ "string-width": "^4.1.0",
1815
+ "strip-ansi": "^6.0.0"
1816
+ },
1817
+ "engines": {
1818
+ "node": ">=10"
1819
+ },
1820
+ "funding": {
1821
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1822
+ }
1823
+ },
1824
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
1825
+ "version": "5.0.1",
1826
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
1827
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
1828
+ "license": "MIT",
1829
+ "engines": {
1830
+ "node": ">=8"
1831
+ }
1832
+ },
1833
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
1834
+ "version": "4.3.0",
1835
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
1836
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
1837
+ "license": "MIT",
1838
+ "dependencies": {
1839
+ "color-convert": "^2.0.1"
1840
+ },
1841
+ "engines": {
1842
+ "node": ">=8"
1843
+ },
1844
+ "funding": {
1845
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1846
+ }
1847
+ },
1848
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
1849
+ "version": "8.0.0",
1850
+ "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
1851
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
1852
+ "license": "MIT"
1853
+ },
1854
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
1855
+ "version": "4.2.3",
1856
+ "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
1857
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
1858
+ "license": "MIT",
1859
+ "dependencies": {
1860
+ "emoji-regex": "^8.0.0",
1861
+ "is-fullwidth-code-point": "^3.0.0",
1862
+ "strip-ansi": "^6.0.1"
1863
+ },
1864
+ "engines": {
1865
+ "node": ">=8"
1866
+ }
1867
+ },
1868
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
1869
+ "version": "6.0.1",
1870
+ "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
1871
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1872
+ "license": "MIT",
1873
+ "dependencies": {
1874
+ "ansi-regex": "^5.0.1"
1875
+ },
1876
+ "engines": {
1877
+ "node": ">=8"
1878
+ }
1879
+ },
1880
+ "node_modules/wrappy": {
1881
+ "version": "1.0.2",
1882
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1883
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1884
+ "license": "ISC"
1885
+ },
1886
+ "node_modules/xtend": {
1887
+ "version": "4.0.2",
1888
+ "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
1889
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
1890
+ "license": "MIT",
1891
+ "engines": {
1892
+ "node": ">=0.4"
1893
+ }
1894
+ },
1895
+ "node_modules/zip-stream": {
1896
+ "version": "6.0.1",
1897
+ "resolved": "https://registry.npmmirror.com/zip-stream/-/zip-stream-6.0.1.tgz",
1898
+ "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==",
1899
+ "license": "MIT",
1900
+ "dependencies": {
1901
+ "archiver-utils": "^5.0.0",
1902
+ "compress-commons": "^6.0.2",
1903
+ "readable-stream": "^4.0.0"
1904
+ },
1905
+ "engines": {
1906
+ "node": ">= 14"
1907
+ }
1908
+ }
1909
+ }
1910
+ }
node_modules/@isaacs/cliui/LICENSE.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright (c) 2015, Contributors
2
+
3
+ Permission to use, copy, modify, and/or distribute this software
4
+ for any purpose with or without fee is hereby granted, provided
5
+ that the above copyright notice and this permission notice
6
+ appear in all copies.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
10
+ OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
11
+ LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
12
+ OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
13
+ WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
14
+ ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
node_modules/@isaacs/cliui/README.md ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @isaacs/cliui
2
+
3
+ Temporary fork of [cliui](http://npm.im/cliui).
4
+
5
+ ![ci](https://github.com/yargs/cliui/workflows/ci/badge.svg)
6
+ [![NPM version](https://img.shields.io/npm/v/cliui.svg)](https://www.npmjs.com/package/cliui)
7
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
8
+ ![nycrc config on GitHub](https://img.shields.io/nycrc/yargs/cliui)
9
+
10
+ easily create complex multi-column command-line-interfaces.
11
+
12
+ ## Example
13
+
14
+ ```js
15
+ const ui = require('cliui')()
16
+
17
+ ui.div('Usage: $0 [command] [options]')
18
+
19
+ ui.div({
20
+ text: 'Options:',
21
+ padding: [2, 0, 1, 0]
22
+ })
23
+
24
+ ui.div(
25
+ {
26
+ text: "-f, --file",
27
+ width: 20,
28
+ padding: [0, 4, 0, 4]
29
+ },
30
+ {
31
+ text: "the file to load." +
32
+ chalk.green("(if this description is long it wraps).")
33
+ ,
34
+ width: 20
35
+ },
36
+ {
37
+ text: chalk.red("[required]"),
38
+ align: 'right'
39
+ }
40
+ )
41
+
42
+ console.log(ui.toString())
43
+ ```
44
+
45
+ ## Deno/ESM Support
46
+
47
+ As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and
48
+ [ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules):
49
+
50
+ ```typescript
51
+ import cliui from "https://deno.land/x/cliui/deno.ts";
52
+
53
+ const ui = cliui({})
54
+
55
+ ui.div('Usage: $0 [command] [options]')
56
+
57
+ ui.div({
58
+ text: 'Options:',
59
+ padding: [2, 0, 1, 0]
60
+ })
61
+
62
+ ui.div({
63
+ text: "-f, --file",
64
+ width: 20,
65
+ padding: [0, 4, 0, 4]
66
+ })
67
+
68
+ console.log(ui.toString())
69
+ ```
70
+
71
+ <img width="500" src="screenshot.png">
72
+
73
+ ## Layout DSL
74
+
75
+ cliui exposes a simple layout DSL:
76
+
77
+ If you create a single `ui.div`, passing a string rather than an
78
+ object:
79
+
80
+ * `\n`: characters will be interpreted as new rows.
81
+ * `\t`: characters will be interpreted as new columns.
82
+ * `\s`: characters will be interpreted as padding.
83
+
84
+ **as an example...**
85
+
86
+ ```js
87
+ var ui = require('./')({
88
+ width: 60
89
+ })
90
+
91
+ ui.div(
92
+ 'Usage: node ./bin/foo.js\n' +
93
+ ' <regex>\t provide a regex\n' +
94
+ ' <glob>\t provide a glob\t [required]'
95
+ )
96
+
97
+ console.log(ui.toString())
98
+ ```
99
+
100
+ **will output:**
101
+
102
+ ```shell
103
+ Usage: node ./bin/foo.js
104
+ <regex> provide a regex
105
+ <glob> provide a glob [required]
106
+ ```
107
+
108
+ ## Methods
109
+
110
+ ```js
111
+ cliui = require('cliui')
112
+ ```
113
+
114
+ ### cliui({width: integer})
115
+
116
+ Specify the maximum width of the UI being generated.
117
+ If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`.
118
+
119
+ ### cliui({wrap: boolean})
120
+
121
+ Enable or disable the wrapping of text in a column.
122
+
123
+ ### cliui.div(column, column, column)
124
+
125
+ Create a row with any number of columns, a column
126
+ can either be a string, or an object with the following
127
+ options:
128
+
129
+ * **text:** some text to place in the column.
130
+ * **width:** the width of a column.
131
+ * **align:** alignment, `right` or `center`.
132
+ * **padding:** `[top, right, bottom, left]`.
133
+ * **border:** should a border be placed around the div?
134
+
135
+ ### cliui.span(column, column, column)
136
+
137
+ Similar to `div`, except the next row will be appended without
138
+ a new line being created.
139
+
140
+ ### cliui.resetOutput()
141
+
142
+ Resets the UI elements of the current cliui instance, maintaining the values
143
+ set for `width` and `wrap`.
node_modules/@isaacs/cliui/build/index.cjs ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ const align = {
4
+ right: alignRight,
5
+ center: alignCenter
6
+ };
7
+ const top = 0;
8
+ const right = 1;
9
+ const bottom = 2;
10
+ const left = 3;
11
+ class UI {
12
+ constructor(opts) {
13
+ var _a;
14
+ this.width = opts.width;
15
+ /* c8 ignore start */
16
+ this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
17
+ /* c8 ignore stop */
18
+ this.rows = [];
19
+ }
20
+ span(...args) {
21
+ const cols = this.div(...args);
22
+ cols.span = true;
23
+ }
24
+ resetOutput() {
25
+ this.rows = [];
26
+ }
27
+ div(...args) {
28
+ if (args.length === 0) {
29
+ this.div('');
30
+ }
31
+ if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
32
+ return this.applyLayoutDSL(args[0]);
33
+ }
34
+ const cols = args.map(arg => {
35
+ if (typeof arg === 'string') {
36
+ return this.colFromString(arg);
37
+ }
38
+ return arg;
39
+ });
40
+ this.rows.push(cols);
41
+ return cols;
42
+ }
43
+ shouldApplyLayoutDSL(...args) {
44
+ return args.length === 1 && typeof args[0] === 'string' &&
45
+ /[\t\n]/.test(args[0]);
46
+ }
47
+ applyLayoutDSL(str) {
48
+ const rows = str.split('\n').map(row => row.split('\t'));
49
+ let leftColumnWidth = 0;
50
+ // simple heuristic for layout, make sure the
51
+ // second column lines up along the left-hand.
52
+ // don't allow the first column to take up more
53
+ // than 50% of the screen.
54
+ rows.forEach(columns => {
55
+ if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
56
+ leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
57
+ }
58
+ });
59
+ // generate a table:
60
+ // replacing ' ' with padding calculations.
61
+ // using the algorithmically generated width.
62
+ rows.forEach(columns => {
63
+ this.div(...columns.map((r, i) => {
64
+ return {
65
+ text: r.trim(),
66
+ padding: this.measurePadding(r),
67
+ width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
68
+ };
69
+ }));
70
+ });
71
+ return this.rows[this.rows.length - 1];
72
+ }
73
+ colFromString(text) {
74
+ return {
75
+ text,
76
+ padding: this.measurePadding(text)
77
+ };
78
+ }
79
+ measurePadding(str) {
80
+ // measure padding without ansi escape codes
81
+ const noAnsi = mixin.stripAnsi(str);
82
+ return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
83
+ }
84
+ toString() {
85
+ const lines = [];
86
+ this.rows.forEach(row => {
87
+ this.rowToString(row, lines);
88
+ });
89
+ // don't display any lines with the
90
+ // hidden flag set.
91
+ return lines
92
+ .filter(line => !line.hidden)
93
+ .map(line => line.text)
94
+ .join('\n');
95
+ }
96
+ rowToString(row, lines) {
97
+ this.rasterize(row).forEach((rrow, r) => {
98
+ let str = '';
99
+ rrow.forEach((col, c) => {
100
+ const { width } = row[c]; // the width with padding.
101
+ const wrapWidth = this.negatePadding(row[c]); // the width without padding.
102
+ let ts = col; // temporary string used during alignment/padding.
103
+ if (wrapWidth > mixin.stringWidth(col)) {
104
+ ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
105
+ }
106
+ // align the string within its column.
107
+ if (row[c].align && row[c].align !== 'left' && this.wrap) {
108
+ const fn = align[row[c].align];
109
+ ts = fn(ts, wrapWidth);
110
+ if (mixin.stringWidth(ts) < wrapWidth) {
111
+ /* c8 ignore start */
112
+ const w = width || 0;
113
+ /* c8 ignore stop */
114
+ ts += ' '.repeat(w - mixin.stringWidth(ts) - 1);
115
+ }
116
+ }
117
+ // apply border and padding to string.
118
+ const padding = row[c].padding || [0, 0, 0, 0];
119
+ if (padding[left]) {
120
+ str += ' '.repeat(padding[left]);
121
+ }
122
+ str += addBorder(row[c], ts, '| ');
123
+ str += ts;
124
+ str += addBorder(row[c], ts, ' |');
125
+ if (padding[right]) {
126
+ str += ' '.repeat(padding[right]);
127
+ }
128
+ // if prior row is span, try to render the
129
+ // current row on the prior line.
130
+ if (r === 0 && lines.length > 0) {
131
+ str = this.renderInline(str, lines[lines.length - 1]);
132
+ }
133
+ });
134
+ // remove trailing whitespace.
135
+ lines.push({
136
+ text: str.replace(/ +$/, ''),
137
+ span: row.span
138
+ });
139
+ });
140
+ return lines;
141
+ }
142
+ // if the full 'source' can render in
143
+ // the target line, do so.
144
+ renderInline(source, previousLine) {
145
+ const match = source.match(/^ */);
146
+ /* c8 ignore start */
147
+ const leadingWhitespace = match ? match[0].length : 0;
148
+ /* c8 ignore stop */
149
+ const target = previousLine.text;
150
+ const targetTextWidth = mixin.stringWidth(target.trimEnd());
151
+ if (!previousLine.span) {
152
+ return source;
153
+ }
154
+ // if we're not applying wrapping logic,
155
+ // just always append to the span.
156
+ if (!this.wrap) {
157
+ previousLine.hidden = true;
158
+ return target + source;
159
+ }
160
+ if (leadingWhitespace < targetTextWidth) {
161
+ return source;
162
+ }
163
+ previousLine.hidden = true;
164
+ return target.trimEnd() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimStart();
165
+ }
166
+ rasterize(row) {
167
+ const rrows = [];
168
+ const widths = this.columnWidths(row);
169
+ let wrapped;
170
+ // word wrap all columns, and create
171
+ // a data-structure that is easy to rasterize.
172
+ row.forEach((col, c) => {
173
+ // leave room for left and right padding.
174
+ col.width = widths[c];
175
+ if (this.wrap) {
176
+ wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
177
+ }
178
+ else {
179
+ wrapped = col.text.split('\n');
180
+ }
181
+ if (col.border) {
182
+ wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
183
+ wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
184
+ }
185
+ // add top and bottom padding.
186
+ if (col.padding) {
187
+ wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
188
+ wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
189
+ }
190
+ wrapped.forEach((str, r) => {
191
+ if (!rrows[r]) {
192
+ rrows.push([]);
193
+ }
194
+ const rrow = rrows[r];
195
+ for (let i = 0; i < c; i++) {
196
+ if (rrow[i] === undefined) {
197
+ rrow.push('');
198
+ }
199
+ }
200
+ rrow.push(str);
201
+ });
202
+ });
203
+ return rrows;
204
+ }
205
+ negatePadding(col) {
206
+ /* c8 ignore start */
207
+ let wrapWidth = col.width || 0;
208
+ /* c8 ignore stop */
209
+ if (col.padding) {
210
+ wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
211
+ }
212
+ if (col.border) {
213
+ wrapWidth -= 4;
214
+ }
215
+ return wrapWidth;
216
+ }
217
+ columnWidths(row) {
218
+ if (!this.wrap) {
219
+ return row.map(col => {
220
+ return col.width || mixin.stringWidth(col.text);
221
+ });
222
+ }
223
+ let unset = row.length;
224
+ let remainingWidth = this.width;
225
+ // column widths can be set in config.
226
+ const widths = row.map(col => {
227
+ if (col.width) {
228
+ unset--;
229
+ remainingWidth -= col.width;
230
+ return col.width;
231
+ }
232
+ return undefined;
233
+ });
234
+ // any unset widths should be calculated.
235
+ /* c8 ignore start */
236
+ const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
237
+ /* c8 ignore stop */
238
+ return widths.map((w, i) => {
239
+ if (w === undefined) {
240
+ return Math.max(unsetWidth, _minWidth(row[i]));
241
+ }
242
+ return w;
243
+ });
244
+ }
245
+ }
246
+ function addBorder(col, ts, style) {
247
+ if (col.border) {
248
+ if (/[.']-+[.']/.test(ts)) {
249
+ return '';
250
+ }
251
+ if (ts.trim().length !== 0) {
252
+ return style;
253
+ }
254
+ return ' ';
255
+ }
256
+ return '';
257
+ }
258
+ // calculates the minimum width of
259
+ // a column, based on padding preferences.
260
+ function _minWidth(col) {
261
+ const padding = col.padding || [];
262
+ const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
263
+ if (col.border) {
264
+ return minWidth + 4;
265
+ }
266
+ return minWidth;
267
+ }
268
+ function getWindowWidth() {
269
+ /* c8 ignore start */
270
+ if (typeof process === 'object' && process.stdout && process.stdout.columns) {
271
+ return process.stdout.columns;
272
+ }
273
+ return 80;
274
+ }
275
+ /* c8 ignore stop */
276
+ function alignRight(str, width) {
277
+ str = str.trim();
278
+ const strWidth = mixin.stringWidth(str);
279
+ if (strWidth < width) {
280
+ return ' '.repeat(width - strWidth) + str;
281
+ }
282
+ return str;
283
+ }
284
+ function alignCenter(str, width) {
285
+ str = str.trim();
286
+ const strWidth = mixin.stringWidth(str);
287
+ /* c8 ignore start */
288
+ if (strWidth >= width) {
289
+ return str;
290
+ }
291
+ /* c8 ignore stop */
292
+ return ' '.repeat((width - strWidth) >> 1) + str;
293
+ }
294
+ let mixin;
295
+ function cliui(opts, _mixin) {
296
+ mixin = _mixin;
297
+ return new UI({
298
+ /* c8 ignore start */
299
+ width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
300
+ wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
301
+ /* c8 ignore stop */
302
+ });
303
+ }
304
+
305
+ // Bootstrap cliui with CommonJS dependencies:
306
+ const stringWidth = require('string-width-cjs');
307
+ const stripAnsi = require('strip-ansi-cjs');
308
+ const wrap = require('wrap-ansi-cjs');
309
+ function ui(opts) {
310
+ return cliui(opts, {
311
+ stringWidth,
312
+ stripAnsi,
313
+ wrap
314
+ });
315
+ }
316
+
317
+ module.exports = ui;
node_modules/@isaacs/cliui/build/index.d.cts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ interface UIOptions {
2
+ width: number;
3
+ wrap?: boolean;
4
+ rows?: string[];
5
+ }
6
+ interface Column {
7
+ text: string;
8
+ width?: number;
9
+ align?: "right" | "left" | "center";
10
+ padding: number[];
11
+ border?: boolean;
12
+ }
13
+ interface ColumnArray extends Array<Column> {
14
+ span: boolean;
15
+ }
16
+ interface Line {
17
+ hidden?: boolean;
18
+ text: string;
19
+ span?: boolean;
20
+ }
21
+ declare class UI {
22
+ width: number;
23
+ wrap: boolean;
24
+ rows: ColumnArray[];
25
+ constructor(opts: UIOptions);
26
+ span(...args: ColumnArray): void;
27
+ resetOutput(): void;
28
+ div(...args: (Column | string)[]): ColumnArray;
29
+ private shouldApplyLayoutDSL;
30
+ private applyLayoutDSL;
31
+ private colFromString;
32
+ private measurePadding;
33
+ toString(): string;
34
+ rowToString(row: ColumnArray, lines: Line[]): Line[];
35
+ // if the full 'source' can render in
36
+ // the target line, do so.
37
+ private renderInline;
38
+ private rasterize;
39
+ private negatePadding;
40
+ private columnWidths;
41
+ }
42
+ declare function ui(opts: UIOptions): UI;
43
+ export { ui as default };