From 52dba37f22837ae063e56f43e309e9dd2f95d05e Mon Sep 17 00:00:00 2001 From: TradeMate Dev Date: Fri, 22 May 2026 18:35:30 +0800 Subject: [PATCH] Add admin-frontend and user-frontend standalone projects, certification/invoice/discovery features, fix auth header and theme consistency --- .gitignore | 14 +- AGENTS.md | 24 +- admin-frontend/index.html | 12 + admin-frontend/package-lock.json | 1961 +++++++++++++++++ admin-frontend/package.json | 24 + admin-frontend/src/App.vue | 22 + admin-frontend/src/api/index.js | 58 + admin-frontend/src/layouts/AdminLayout.vue | 115 + admin-frontend/src/main.js | 15 + admin-frontend/src/router/index.js | 85 + admin-frontend/src/stores/auth.js | 28 + admin-frontend/src/views/Certifications.vue | 117 + admin-frontend/src/views/Config.vue | 70 + admin-frontend/src/views/Dashboard.vue | 77 + admin-frontend/src/views/Invoices.vue | 112 + admin-frontend/src/views/Landing.vue | 146 ++ admin-frontend/src/views/Login.vue | 60 + admin-frontend/src/views/Logs.vue | 83 + admin-frontend/src/views/Quota.vue | 86 + admin-frontend/src/views/Stats.vue | 83 + admin-frontend/src/views/Users.vue | 120 + admin-frontend/vite.config.js | 21 + ...b04cc0e1d_add_certification_and_invoice.py | 76 + backend/app/ai/providers/nvidia.py | 2 +- backend/app/api/v1/admin.py | 62 + backend/app/api/v1/certification.py | 41 + backend/app/api/v1/discovery.py | 61 + backend/app/api/v1/invoice.py | 39 + backend/app/config.py | 14 +- backend/app/main.py | 5 +- backend/app/models/__init__.py | 4 + backend/app/models/certification.py | 41 + backend/app/models/invoice.py | 41 + backend/app/services/certification.py | 112 + backend/app/services/discovery.py | 272 +++ backend/app/services/invoice.py | 126 ++ backend/app/services/mcp_search_client.py | 101 + backend/app/services/mcp_search_server.py | 105 + backend/app/services/search_web.py | 73 + backend/requirements.txt | 13 +- package-lock.json | 757 +++++++ package.json | 5 + uni-app/src/App.vue | 204 +- uni-app/src/config.js | 3 + uni-app/src/pages.json | 20 +- uni-app/src/pages/admin/admin.vue | 260 ++- .../src/pages/certification/certification.vue | 172 ++ uni-app/src/pages/discovery/discovery.vue | 357 +++ uni-app/src/pages/index/index.vue | 54 +- uni-app/src/pages/invoice/invoice.vue | 165 ++ uni-app/src/pages/profile/profile.vue | 16 + uni-app/src/utils/api.js | 24 + user-frontend/index.html | 12 + user-frontend/package-lock.json | 1961 +++++++++++++++++ user-frontend/package.json | 24 + user-frontend/src/App.vue | 22 + user-frontend/src/api/index.js | 113 + user-frontend/src/layouts/UserLayout.vue | 109 + user-frontend/src/main.js | 15 + user-frontend/src/router/index.js | 43 + user-frontend/src/stores/auth.js | 40 + user-frontend/src/views/Analytics.vue | 77 + user-frontend/src/views/Certification.vue | 57 + user-frontend/src/views/Customers.vue | 165 ++ user-frontend/src/views/Discovery.vue | 93 + user-frontend/src/views/Feedback.vue | 61 + user-frontend/src/views/Followup.vue | 145 ++ user-frontend/src/views/Invoice.vue | 86 + user-frontend/src/views/Login.vue | 60 + user-frontend/src/views/Marketing.vue | 84 + user-frontend/src/views/Notifications.vue | 61 + user-frontend/src/views/Products.vue | 112 + user-frontend/src/views/Profile.vue | 119 + user-frontend/src/views/Quotations.vue | 153 ++ user-frontend/src/views/Team.vue | 106 + user-frontend/src/views/Translate.vue | 103 + user-frontend/src/views/Upgrade.vue | 64 + user-frontend/src/views/Workspace.vue | 117 + user-frontend/vite.config.js | 21 + 79 files changed, 10333 insertions(+), 248 deletions(-) create mode 100644 admin-frontend/index.html create mode 100644 admin-frontend/package-lock.json create mode 100644 admin-frontend/package.json create mode 100644 admin-frontend/src/App.vue create mode 100644 admin-frontend/src/api/index.js create mode 100644 admin-frontend/src/layouts/AdminLayout.vue create mode 100644 admin-frontend/src/main.js create mode 100644 admin-frontend/src/router/index.js create mode 100644 admin-frontend/src/stores/auth.js create mode 100644 admin-frontend/src/views/Certifications.vue create mode 100644 admin-frontend/src/views/Config.vue create mode 100644 admin-frontend/src/views/Dashboard.vue create mode 100644 admin-frontend/src/views/Invoices.vue create mode 100644 admin-frontend/src/views/Landing.vue create mode 100644 admin-frontend/src/views/Login.vue create mode 100644 admin-frontend/src/views/Logs.vue create mode 100644 admin-frontend/src/views/Quota.vue create mode 100644 admin-frontend/src/views/Stats.vue create mode 100644 admin-frontend/src/views/Users.vue create mode 100644 admin-frontend/vite.config.js create mode 100644 backend/alembic/versions/ecab04cc0e1d_add_certification_and_invoice.py create mode 100644 backend/app/api/v1/certification.py create mode 100644 backend/app/api/v1/discovery.py create mode 100644 backend/app/api/v1/invoice.py create mode 100644 backend/app/models/certification.py create mode 100644 backend/app/models/invoice.py create mode 100644 backend/app/services/certification.py create mode 100644 backend/app/services/discovery.py create mode 100644 backend/app/services/invoice.py create mode 100644 backend/app/services/mcp_search_client.py create mode 100644 backend/app/services/mcp_search_server.py create mode 100644 backend/app/services/search_web.py create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 uni-app/src/pages/certification/certification.vue create mode 100644 uni-app/src/pages/discovery/discovery.vue create mode 100644 uni-app/src/pages/invoice/invoice.vue create mode 100644 user-frontend/index.html create mode 100644 user-frontend/package-lock.json create mode 100644 user-frontend/package.json create mode 100644 user-frontend/src/App.vue create mode 100644 user-frontend/src/api/index.js create mode 100644 user-frontend/src/layouts/UserLayout.vue create mode 100644 user-frontend/src/main.js create mode 100644 user-frontend/src/router/index.js create mode 100644 user-frontend/src/stores/auth.js create mode 100644 user-frontend/src/views/Analytics.vue create mode 100644 user-frontend/src/views/Certification.vue create mode 100644 user-frontend/src/views/Customers.vue create mode 100644 user-frontend/src/views/Discovery.vue create mode 100644 user-frontend/src/views/Feedback.vue create mode 100644 user-frontend/src/views/Followup.vue create mode 100644 user-frontend/src/views/Invoice.vue create mode 100644 user-frontend/src/views/Login.vue create mode 100644 user-frontend/src/views/Marketing.vue create mode 100644 user-frontend/src/views/Notifications.vue create mode 100644 user-frontend/src/views/Products.vue create mode 100644 user-frontend/src/views/Profile.vue create mode 100644 user-frontend/src/views/Quotations.vue create mode 100644 user-frontend/src/views/Team.vue create mode 100644 user-frontend/src/views/Translate.vue create mode 100644 user-frontend/src/views/Upgrade.vue create mode 100644 user-frontend/src/views/Workspace.vue create mode 100644 user-frontend/vite.config.js diff --git a/.gitignore b/.gitignore index 0dd45e2..e02f6b6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,13 +37,21 @@ logs/ .DS_Store Thumbs.db -# Uni-app +# Node +node_modules/ uni-app/dist/ -uni-app/node_modules/ +admin-frontend/dist/ +user-frontend/dist/ + +# Python test cache +.pytest_cache/ # Docker docker-compose.override.yml # Misc *.bak -*.tmp \ No newline at end of file +*.tmp + +# Generated by MCP search server +backend/app/services/_bing_search.js \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index e1062d4..603b428 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,16 +15,24 @@ # Backend (from project root — .env is there) cd backend && source venv/bin/activate && uvicorn app.main:app --reload --port 8000 -# Frontend +# Frontend — uni-app (mobile) cd uni-app && npm run dev:h5 +# Admin frontend (PC management) +cd admin-frontend && npm run dev # port 5173, base: /admin/ + +# User workspace (PC workbench) +cd user-frontend && npm run dev # port 5174, base: /workspace/ + # Tests (backend — needs PostgreSQL running with foreign_trade_test DB) cd backend && venv/bin/pytest # all venv/bin/pytest tests/test_auth_api.py # single file venv/bin/pytest tests/ -k "test_login" # keyword filter -# Frontend build (produces uni-app/dist/build/h5/) -cd uni-app && npm run build:h5 +# Builds +cd uni-app && npm run build:h5 # uni-app (mobile H5) +cd admin-frontend && npm run build # admin => /www/wwwroot/trade.yuzhiran.com/admin/ +cd user-frontend && npm run build # workspace => /www/wwwroot/trade.yuzhiran.com/workspace/ # Alembic migrations cd backend && alembic upgrade head @@ -34,10 +42,12 @@ alembic revision --autogenerate -m "desc" ## Deployment - **Landing page** at `trade.yuzhiran.com/` — static marketing HTML -- **SPA** at `trade.yuzhiran.com/app/` — uni-app build -- **Nginx**: `location /app/ { try_files $uri $uri/ /app/index.html; }` for SPA fallback -- **vite config**: `base: '/app/'` so all asset paths are `/app/assets/...` -- **API**: proxied via nginx `location /api/` to `127.0.0.1:8000` +- **SPA** at `trade.yuzhiran.com/app/` — uni-app build (mobile) +- **Admin** at `trade.yuzhiran.com/admin/` — Vue 3 + Element Plus (standalone) +- **Workspace** at `trade.yuzhiran.com/workspace/` — Vue 3 + Element Plus (standalone) +- **Nginx**: SPA fallbacks for `/app/`, `/admin/`, `/workspace/` +- **vite config**: each project has its own `base` path and dev port +- **API**: proxied via nginx `location /api/` to `127.0.0.1:8002` ## Critical Quirks diff --git a/admin-frontend/index.html b/admin-frontend/index.html new file mode 100644 index 0000000..1339ae8 --- /dev/null +++ b/admin-frontend/index.html @@ -0,0 +1,12 @@ + + + + + + TradeMate 管理后台 + + +
+ + + diff --git a/admin-frontend/package-lock.json b/admin-frontend/package-lock.json new file mode 100644 index 0000000..15e47bf --- /dev/null +++ b/admin-frontend/package-lock.json @@ -0,0 +1,1961 @@ +{ + "name": "trademate-admin", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trademate-admin", + "version": "1.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.7.9", + "dayjs": "^1.11.13", + "element-plus": "^2.9.1", + "pinia": "^2.3.0", + "vue": "^3.5.13", + "vue-router": "^4.5.0" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.7" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.3", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.34.tgz", + "integrity": "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/shared": "3.5.34", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.34.tgz", + "integrity": "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.34.tgz", + "integrity": "sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@vue/compiler-core": "3.5.34", + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.14", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.34.tgz", + "integrity": "sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.34.tgz", + "integrity": "sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.34.tgz", + "integrity": "sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/shared": "3.5.34" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.34.tgz", + "integrity": "sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.34", + "@vue/runtime-core": "3.5.34", + "@vue/shared": "3.5.34", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.34.tgz", + "integrity": "sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "vue": "3.5.34" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.34.tgz", + "integrity": "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-14.3.0.tgz", + "integrity": "sha512-aHfz47g0ZhMtTVHmIzMVpJy8ePhhOy68GY5bv110+5DVtZ+W7BsOx+m61UNQqfrWyPztIHIanWa3E2tib3NFIw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "14.3.0", + "@vueuse/shared": "14.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@vueuse/metadata": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.3.0.tgz", + "integrity": "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "14.3.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.3.0.tgz", + "integrity": "sha512-bZpge9eSXwa4ToSiqJ7j6KRwhAsneMFoSz3LMWKQDkqimm3D/tbFlrklrs/IOqC8tEcYmXQZJ6N0UrjhBirVCg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.14.0", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.0.tgz", + "integrity": "sha512-POgH+TtoreaEKWqYYAVQyE6i8rQMEFqAEublyF29dBA5yASWPLKY6EzfeqBTr2Uv26mPss4vSrMrNPyaK7LX5w==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.8", + "@types/lodash": "^4.17.24", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "14.3.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.20", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.2.8" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.34", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.34.tgz", + "integrity": "sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.34", + "@vue/compiler-sfc": "3.5.34", + "@vue/runtime-dom": "3.5.34", + "@vue/server-renderer": "3.5.34", + "@vue/shared": "3.5.34" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.3.1.tgz", + "integrity": "sha512-pu58kqxmVyEH6VfNYW1UyEfR3XAnJ27ZXT3yzXxxpjLxVzAbyC35Zk/nm/RMs7ijWnJNSd9fWkeex2OhUsx3MA==", + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + } + } +} diff --git a/admin-frontend/package.json b/admin-frontend/package.json new file mode 100644 index 0000000..842e279 --- /dev/null +++ b/admin-frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "trademate-admin", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.13", + "vue-router": "^4.5.0", + "element-plus": "^2.9.1", + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.7.9", + "pinia": "^2.3.0", + "dayjs": "^1.11.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.1", + "vite": "^6.0.7" + } +} diff --git a/admin-frontend/src/App.vue b/admin-frontend/src/App.vue new file mode 100644 index 0000000..d5494ec --- /dev/null +++ b/admin-frontend/src/App.vue @@ -0,0 +1,22 @@ + + + diff --git a/admin-frontend/src/api/index.js b/admin-frontend/src/api/index.js new file mode 100644 index 0000000..34343ae --- /dev/null +++ b/admin-frontend/src/api/index.js @@ -0,0 +1,58 @@ +import axios from 'axios' + +const http = axios.create({ baseURL: '/api/v1', timeout: 30000 }) + +http.interceptors.request.use(config => { + const token = localStorage.getItem('admin_token') + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +http.interceptors.response.use( + res => res.data, + err => { + if (err.response?.status === 401) { + localStorage.removeItem('admin_token') + localStorage.removeItem('admin_user') + const currentPath = window.location.pathname.replace('/admin', '') || '/' + window.location.href = '/admin/login?redirect=' + encodeURIComponent(currentPath) + } + return Promise.reject(err.response?.data || err) + } +) + +export function login(data) { return http.post('/auth/login', data) } + +export function getDashboard() { return http.get('/admin/dashboard') } + +export function searchUsers(query) { return http.post('/admin/users/search', { query }) } +export function listUsers(page = 1, size = 20) { return http.get('/admin/users', { params: { page, size } }) } +export function getUserDetail(id) { return http.get(`/admin/users/${id}`) } +export function updateUser(id, data) { return http.put(`/admin/users/${id}`, data) } + +export function getUsageStats() { return http.get('/admin/stats/usage') } + +export function listLogs(params) { return http.get('/admin/logs', { params }) } + +export function listConfig() { return http.get('/admin/config') } +export function updateConfig(key, value) { return http.put(`/admin/config/${key}`, { value }) } + +export function listQuotas() { return http.get('/admin/quotas') } +export function updateQuota(version, data) { return http.put(`/admin/quotas/${version}`, data) } +export function resetQuota(version) { return http.post(`/admin/quotas/${version}/reset`) } + +export function listCertifications(page = 1, size = 50, status = '') { + return http.get('/admin/certifications', { params: { page, size, status: status || undefined } }) +} +export function reviewCertification(id, action, reason = '') { + return http.post(`/admin/certifications/${id}/review`, { action, reason }) +} + +export function listInvoices(page = 1, size = 50, status = '') { + return http.get('/admin/invoices', { params: { page, size, status: status || undefined } }) +} +export function processInvoice(id, action) { + return http.post(`/admin/invoices/${id}/process`, { action }) +} + +export default http diff --git a/admin-frontend/src/layouts/AdminLayout.vue b/admin-frontend/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..fdae772 --- /dev/null +++ b/admin-frontend/src/layouts/AdminLayout.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/admin-frontend/src/main.js b/admin-frontend/src/main.js new file mode 100644 index 0000000..2806f87 --- /dev/null +++ b/admin-frontend/src/main.js @@ -0,0 +1,15 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import App from './App.vue' +import router from './router' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.use(ElementPlus, { locale: zhCn }) +for (const [k, v] of Object.entries(ElementPlusIconsVue)) app.component(k, v) +app.mount('#app') diff --git a/admin-frontend/src/router/index.js b/admin-frontend/src/router/index.js new file mode 100644 index 0000000..6f3b660 --- /dev/null +++ b/admin-frontend/src/router/index.js @@ -0,0 +1,85 @@ +import { createRouter, createWebHistory } from 'vue-router' +import AdminLayout from '@/layouts/AdminLayout.vue' + +const routes = [ + { path: '/login', redirect: '/' }, + { path: '/', name: 'Landing', component: () => import('@/views/Landing.vue') }, + { + path: '/dashboard', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '概览' } }, + ] + }, + { + path: '/users', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Users', component: () => import('@/views/Users.vue'), meta: { title: '用户' } }, + ] + }, + { + path: '/stats', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Stats', component: () => import('@/views/Stats.vue'), meta: { title: '统计' } }, + ] + }, + { + path: '/logs', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Logs', component: () => import('@/views/Logs.vue'), meta: { title: '日志' } }, + ] + }, + { + path: '/config', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Config', component: () => import('@/views/Config.vue'), meta: { title: '配置' } }, + ] + }, + { + path: '/quota', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Quota', component: () => import('@/views/Quota.vue'), meta: { title: '翻译配额' } }, + ] + }, + { + path: '/certifications', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Certifications', component: () => import('@/views/Certifications.vue'), meta: { title: '认证审核' } }, + ] + }, + { + path: '/invoices', + component: AdminLayout, + meta: { requiresAuth: true }, + children: [ + { path: '', name: 'Invoices', component: () => import('@/views/Invoices.vue'), meta: { title: '发票管理' } }, + ] + }, +] + +const router = createRouter({ history: createWebHistory('/admin/'), routes }) + +router.beforeEach((to, from, next) => { + if (to.meta?.requiresAuth) { + const token = localStorage.getItem('admin_token') + if (!token) next({ name: 'Login', query: { redirect: to.fullPath } }) + else next() + } else { + next() + } +}) + +export default router diff --git a/admin-frontend/src/stores/auth.js b/admin-frontend/src/stores/auth.js new file mode 100644 index 0000000..f4a3f0d --- /dev/null +++ b/admin-frontend/src/stores/auth.js @@ -0,0 +1,28 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { login as loginApi } from '@/api' + +export const useAuthStore = defineStore('auth', () => { + const token = ref(localStorage.getItem('admin_token') || '') + const user = ref(JSON.parse(localStorage.getItem('admin_user') || 'null')) + + const isLoggedIn = computed(() => !!token.value) + + async function login(credentials) { + const res = await loginApi(credentials) + token.value = res.access_token + user.value = res.user || {} + localStorage.setItem('admin_token', res.access_token) + localStorage.setItem('admin_user', JSON.stringify(res.user || {})) + return res + } + + function logout() { + token.value = '' + user.value = null + localStorage.removeItem('admin_token') + localStorage.removeItem('admin_user') + } + + return { token, user, isLoggedIn, login, logout } +}) diff --git a/admin-frontend/src/views/Certifications.vue b/admin-frontend/src/views/Certifications.vue new file mode 100644 index 0000000..3a4a3ea --- /dev/null +++ b/admin-frontend/src/views/Certifications.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/admin-frontend/src/views/Config.vue b/admin-frontend/src/views/Config.vue new file mode 100644 index 0000000..a466405 --- /dev/null +++ b/admin-frontend/src/views/Config.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/admin-frontend/src/views/Dashboard.vue b/admin-frontend/src/views/Dashboard.vue new file mode 100644 index 0000000..0d57c80 --- /dev/null +++ b/admin-frontend/src/views/Dashboard.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/admin-frontend/src/views/Invoices.vue b/admin-frontend/src/views/Invoices.vue new file mode 100644 index 0000000..35ca220 --- /dev/null +++ b/admin-frontend/src/views/Invoices.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/admin-frontend/src/views/Landing.vue b/admin-frontend/src/views/Landing.vue new file mode 100644 index 0000000..846deb0 --- /dev/null +++ b/admin-frontend/src/views/Landing.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/admin-frontend/src/views/Login.vue b/admin-frontend/src/views/Login.vue new file mode 100644 index 0000000..0df6066 --- /dev/null +++ b/admin-frontend/src/views/Login.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/admin-frontend/src/views/Logs.vue b/admin-frontend/src/views/Logs.vue new file mode 100644 index 0000000..96a14c7 --- /dev/null +++ b/admin-frontend/src/views/Logs.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/admin-frontend/src/views/Quota.vue b/admin-frontend/src/views/Quota.vue new file mode 100644 index 0000000..b7fefa2 --- /dev/null +++ b/admin-frontend/src/views/Quota.vue @@ -0,0 +1,86 @@ + + + + + diff --git a/admin-frontend/src/views/Stats.vue b/admin-frontend/src/views/Stats.vue new file mode 100644 index 0000000..87f541b --- /dev/null +++ b/admin-frontend/src/views/Stats.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/admin-frontend/src/views/Users.vue b/admin-frontend/src/views/Users.vue new file mode 100644 index 0000000..0207b2c --- /dev/null +++ b/admin-frontend/src/views/Users.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/admin-frontend/vite.config.js b/admin-frontend/vite.config.js new file mode 100644 index 0000000..d156d8a --- /dev/null +++ b/admin-frontend/vite.config.js @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [vue()], + base: '/admin/', + resolve: { + alias: { '@': resolve(__dirname, 'src') } + }, + build: { + outDir: 'dist', + assetsDir: 'assets' + }, + server: { + port: 5173, + proxy: { + '/api': { target: 'http://localhost:8002', changeOrigin: true } + } + } +}) diff --git a/backend/alembic/versions/ecab04cc0e1d_add_certification_and_invoice.py b/backend/alembic/versions/ecab04cc0e1d_add_certification_and_invoice.py new file mode 100644 index 0000000..236eb0e --- /dev/null +++ b/backend/alembic/versions/ecab04cc0e1d_add_certification_and_invoice.py @@ -0,0 +1,76 @@ +"""add certification and invoice + +Revision ID: ecab04cc0e1d +Revises: 93a81b22bd80 +Create Date: 2026-05-22 09:20:37.807327 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision: str = 'ecab04cc0e1d' +down_revision: Union[str, None] = '93a81b22bd80' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('certifications', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('cert_type', sa.Enum('individual', 'enterprise', name='certtype'), nullable=False), + sa.Column('personal_name', sa.String(length=100), nullable=True), + sa.Column('personal_id', sa.String(length=30), nullable=True), + sa.Column('company_name', sa.String(length=255), nullable=True), + sa.Column('tax_id', sa.String(length=30), nullable=True), + sa.Column('business_license_url', sa.String(length=500), nullable=True), + sa.Column('status', sa.Enum('pending', 'approved', 'rejected', name='certstatus'), nullable=True), + sa.Column('reject_reason', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_certifications_user_id'), 'certifications', ['user_id'], unique=False) + op.create_table('invoices', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('certification_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('invoice_type', sa.Enum('individual', 'enterprise', name='invoicetype'), nullable=False), + sa.Column('title', sa.String(length=255), nullable=False), + sa.Column('tax_id', sa.String(length=30), nullable=True), + sa.Column('amount', sa.Float(), nullable=False), + sa.Column('status', sa.Enum('pending', 'issued', 'rejected', name='invoicestatus'), nullable=True), + sa.Column('reject_reason', sa.Text(), nullable=True), + sa.Column('issued_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['certification_id'], ['certifications.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_invoices_user_id'), 'invoices', ['user_id'], unique=False) + op.drop_index('ix_preference_analyses_user_id', table_name='preference_analyses') + op.create_index(op.f('ix_preference_analyses_user_id'), 'preference_analyses', ['user_id'], unique=True) + op.drop_constraint('system_configs_key_key', 'system_configs', type_='unique') + op.drop_index('ix_system_configs_key', table_name='system_configs') + op.create_index(op.f('ix_system_configs_key'), 'system_configs', ['key'], unique=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_system_configs_key'), table_name='system_configs') + op.create_index('ix_system_configs_key', 'system_configs', ['key'], unique=False) + op.create_unique_constraint('system_configs_key_key', 'system_configs', ['key']) + op.drop_index(op.f('ix_preference_analyses_user_id'), table_name='preference_analyses') + op.create_index('ix_preference_analyses_user_id', 'preference_analyses', ['user_id'], unique=False) + op.drop_index(op.f('ix_invoices_user_id'), table_name='invoices') + op.drop_table('invoices') + op.drop_index(op.f('ix_certifications_user_id'), table_name='certifications') + op.drop_table('certifications') + # ### end Alembic commands ### \ No newline at end of file diff --git a/backend/app/ai/providers/nvidia.py b/backend/app/ai/providers/nvidia.py index bb53732..8131e13 100644 --- a/backend/app/ai/providers/nvidia.py +++ b/backend/app/ai/providers/nvidia.py @@ -13,7 +13,7 @@ class NvidiaProvider(OpenAIProvider): api_key=api_key, model=model, base_url=base_url, - http_client=httpx.AsyncClient(timeout=httpx.Timeout(60.0)), + http_client=httpx.AsyncClient(timeout=httpx.Timeout(20.0)), ) self._name = f"nvidia-{model}" diff --git a/backend/app/api/v1/admin.py b/backend/app/api/v1/admin.py index e1f38e6..94cecf1 100644 --- a/backend/app/api/v1/admin.py +++ b/backend/app/api/v1/admin.py @@ -6,6 +6,8 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_db from app.services.admin import AdminService from app.services.translation_quota import TranslationQuotaService +from app.services.certification import CertificationService +from app.services.invoice import InvoiceService from app.api.v1.deps import get_current_user router = APIRouter() @@ -212,3 +214,63 @@ async def reset_translation_quota( if not result: raise HTTPException(status_code=404, detail="Quota not found") return result + + +@router.get("/certifications") +async def admin_list_certifications( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + status: Optional[str] = Query(None), + _: dict = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + service = CertificationService(db) + return await service.list_all(page, size, status) + + +@router.post("/certifications/{cert_id}/review") +async def admin_review_certification( + cert_id: str, + data: dict, + _: dict = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + _validate_uuid(cert_id) + service = CertificationService(db) + action = data.get("action") + if action not in ("approve", "reject"): + raise HTTPException(status_code=400, detail="Action must be 'approve' or 'reject'") + result = await service.review(cert_id, action, data.get("reason")) + if not result: + raise HTTPException(status_code=404, detail="Certification not found") + return result + + +@router.get("/invoices") +async def admin_list_invoices( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + status: Optional[str] = Query(None), + _: dict = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + service = InvoiceService(db) + return await service.list_all(page, size, status) + + +@router.post("/invoices/{invoice_id}/process") +async def admin_process_invoice( + invoice_id: str, + data: dict, + _: dict = Depends(require_admin), + db: AsyncSession = Depends(get_db), +): + _validate_uuid(invoice_id) + service = InvoiceService(db) + action = data.get("action") + if action not in ("issue", "reject"): + raise HTTPException(status_code=400, detail="Action must be 'issue' or 'reject'") + result = await service.process(invoice_id, action, data.get("reason")) + if not result: + raise HTTPException(status_code=404, detail="Invoice not found") + return result diff --git a/backend/app/api/v1/certification.py b/backend/app/api/v1/certification.py new file mode 100644 index 0000000..ff63f90 --- /dev/null +++ b/backend/app/api/v1/certification.py @@ -0,0 +1,41 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from pydantic import BaseModel +from typing import Optional +from app.database import get_db +from app.api.v1.deps import get_current_user_id +from app.services.certification import CertificationService + +router = APIRouter() + + +class CertSubmitRequest(BaseModel): + cert_type: str + personal_name: Optional[str] = None + personal_id: Optional[str] = None + company_name: Optional[str] = None + tax_id: Optional[str] = None + business_license_url: Optional[str] = None + + +@router.post("/submit") +async def submit_certification( + data: CertSubmitRequest, + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + service = CertificationService(db) + result = await service.submit(user_id, data.model_dump()) + if "error" in result: + raise HTTPException(status_code=400, detail=result["error"]) + return {"success": True, "data": result} + + +@router.get("/status") +async def get_certification_status( + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + service = CertificationService(db) + cert = await service.get_user_cert(user_id) + return {"success": True, "data": cert} diff --git a/backend/app/api/v1/discovery.py b/backend/app/api/v1/discovery.py new file mode 100644 index 0000000..cfb3224 --- /dev/null +++ b/backend/app/api/v1/discovery.py @@ -0,0 +1,61 @@ +from fastapi import APIRouter, HTTPException +from typing import Optional, Dict, Any +from pydantic import BaseModel +from app.services.discovery import DiscoveryService + +router = APIRouter() + + +class SearchRequest(BaseModel): + product_description: str + target_market: str = "US" + + +class AnalyzeRequest(BaseModel): + company_url: str + product_description: str + + +class OutreachRequest(BaseModel): + company: Dict[str, Any] + product: Dict[str, Any] + + +@router.post("/search") +async def search_leads(req: SearchRequest): + if not req.product_description.strip(): + raise HTTPException(status_code=400, detail="请填写产品描述") + svc = DiscoveryService() + try: + result = await svc.search(req.product_description, req.target_market) + return {"success": True, "data": result} + except Exception as e: + raise HTTPException(status_code=500, detail=f"搜索失败: {str(e)}") + + +@router.post("/analyze") +async def analyze_company(req: AnalyzeRequest): + if not req.company_url.strip(): + raise HTTPException(status_code=400, detail="请填写公司网址") + if not req.product_description.strip(): + raise HTTPException(status_code=400, detail="请填写产品描述") + svc = DiscoveryService() + try: + result = await svc.analyze(req.company_url, req.product_description) + return {"success": True, "data": result} + except Exception as e: + raise HTTPException(status_code=500, detail=f"分析失败: {str(e)}") + + +@router.post("/outreach") +async def generate_outreach(req: OutreachRequest): + if not req.company.get("name"): + raise HTTPException(status_code=400, detail="请填写公司名称") + if not req.product.get("name"): + raise HTTPException(status_code=400, detail="请填写产品名称") + svc = DiscoveryService() + try: + result = await svc.outreach(req.company, req.product) + return {"success": True, "data": result} + except Exception as e: + raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}") diff --git a/backend/app/api/v1/invoice.py b/backend/app/api/v1/invoice.py new file mode 100644 index 0000000..6a0dab8 --- /dev/null +++ b/backend/app/api/v1/invoice.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from pydantic import BaseModel +from typing import Optional +from app.database import get_db +from app.api.v1.deps import get_current_user_id +from app.services.invoice import InvoiceService + +router = APIRouter() + + +class InvoiceApplyRequest(BaseModel): + invoice_type: str + title: str + tax_id: Optional[str] = None + amount: float + + +@router.post("/apply") +async def apply_invoice( + data: InvoiceApplyRequest, + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + service = InvoiceService(db) + result = await service.apply(user_id, data.model_dump()) + if "error" in result: + raise HTTPException(status_code=400, detail=result["error"]) + return {"success": True, "data": result} + + +@router.get("/list") +async def list_invoices( + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + service = InvoiceService(db) + items = await service.list_user(user_id) + return {"success": True, "data": items} diff --git a/backend/app/config.py b/backend/app/config.py index 0c7f5b7..aa9cc58 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,4 +1,4 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings from typing import Optional from pathlib import Path @@ -8,10 +8,11 @@ ENV_FILE = PROJECT_ROOT / ".env" class Settings(BaseSettings): - class Config: - env_file = str(ENV_FILE) - env_file_encoding = "utf-8" - extra = "ignore" + model_config = { + "env_file": str(ENV_FILE), + "env_file_encoding": "utf-8", + "extra": "ignore", + } APP_NAME: str = "TradeMate" @@ -71,6 +72,9 @@ class Settings(BaseSettings): EXCHANGE_RATE_API_KEY: Optional[str] = None + GOOGLE_API_KEY: Optional[str] = None + GOOGLE_CSE_ID: Optional[str] = None + UPLOAD_DIR: str = "./uploads" MAX_UPLOAD_SIZE: int = 10 * 1024 * 1024 diff --git a/backend/app/main.py b/backend/app/main.py index 5c4c7df..c814765 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -54,7 +54,7 @@ async def health(): return {"status": "ok", "app": settings.APP_NAME, "version": "1.0.0"} -from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant +from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, certification, invoice app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(marketing.router, prefix="/api/v1/marketing", tags=["marketing"]) @@ -78,6 +78,9 @@ app.include_router(silent_pattern.router, prefix="/api/v1/silent-pattern", tags= app.include_router(training.router, prefix="/api/v1/training", tags=["training"]) app.include_router(followup.router, prefix="/api/v1/followup", tags=["followup"]) app.include_router(ai_assistant.router, prefix="/api/v1/ai", tags=["ai-assistant"]) +app.include_router(discovery.router, prefix="/api/v1/discovery", tags=["discovery"]) +app.include_router(certification.router, prefix="/api/v1/certification", tags=["certification"]) +app.include_router(invoice.router, prefix="/api/v1/invoices", tags=["invoices"]) if __name__ == "__main__": diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index fbcc1e8..0272c60 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -12,6 +12,8 @@ from .device import Device from .followup import FollowupStrategy, FollowupLog from .system_config import SystemConfig from .translation_quota import TranslationQuota +from .certification import Certification, CertType, CertStatus +from .invoice import Invoice, InvoiceType, InvoiceStatus __all__ = [ "User", "Product", @@ -28,4 +30,6 @@ __all__ = [ "FollowupStrategy", "FollowupLog", "SystemConfig", "TranslationQuota", + "Certification", "CertType", "CertStatus", + "Invoice", "InvoiceType", "InvoiceStatus", ] diff --git a/backend/app/models/certification.py b/backend/app/models/certification.py new file mode 100644 index 0000000..674e71c --- /dev/null +++ b/backend/app/models/certification.py @@ -0,0 +1,41 @@ +from sqlalchemy import Column, String, Boolean, Integer, DateTime, Text, ForeignKey, Enum as SAEnum +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from datetime import datetime +from app.database import Base +import uuid +import enum + + +class CertType(str, enum.Enum): + individual = "individual" + enterprise = "enterprise" + + +class CertStatus(str, enum.Enum): + pending = "pending" + approved = "approved" + rejected = "rejected" + + +class Certification(Base): + __tablename__ = "certifications" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) + cert_type = Column(SAEnum(CertType), nullable=False) + + personal_name = Column(String(100)) + personal_id = Column(String(30)) + + company_name = Column(String(255)) + tax_id = Column(String(30)) + business_license_url = Column(String(500)) + + status = Column(SAEnum(CertStatus), default=CertStatus.pending) + reject_reason = Column(Text) + + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User") diff --git a/backend/app/models/invoice.py b/backend/app/models/invoice.py new file mode 100644 index 0000000..e4da597 --- /dev/null +++ b/backend/app/models/invoice.py @@ -0,0 +1,41 @@ +from sqlalchemy import Column, String, Boolean, Integer, DateTime, Text, Float, ForeignKey, Enum as SAEnum +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import relationship +from datetime import datetime +from app.database import Base +import uuid +import enum + + +class InvoiceType(str, enum.Enum): + individual = "individual" + enterprise = "enterprise" + + +class InvoiceStatus(str, enum.Enum): + pending = "pending" + issued = "issued" + rejected = "rejected" + + +class Invoice(Base): + __tablename__ = "invoices" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, index=True) + certification_id = Column(UUID(as_uuid=True), ForeignKey("certifications.id"), nullable=True) + + invoice_type = Column(SAEnum(InvoiceType), nullable=False) + title = Column(String(255), nullable=False) + tax_id = Column(String(30)) + amount = Column(Float, nullable=False) + + status = Column(SAEnum(InvoiceStatus), default=InvoiceStatus.pending) + reject_reason = Column(Text) + issued_at = Column(DateTime) + + created_at = Column(DateTime, default=datetime.utcnow) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + user = relationship("User") + certification = relationship("Certification") diff --git a/backend/app/services/certification.py b/backend/app/services/certification.py new file mode 100644 index 0000000..fff1d00 --- /dev/null +++ b/backend/app/services/certification.py @@ -0,0 +1,112 @@ +from typing import Optional, Dict, Any +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, desc +from app.models.certification import Certification, CertType, CertStatus +from datetime import datetime +import uuid + + +class CertificationService: + def __init__(self, db: AsyncSession): + self.db = db + + async def submit(self, user_id: str, data: Dict[str, Any]) -> Dict[str, Any]: + existing = await self._get_pending(user_id) + if existing: + return {"error": "已有审核中的认证申请,请勿重复提交"} + + cert = Certification( + user_id=uuid.UUID(user_id), + cert_type=CertType(data["cert_type"]), + personal_name=data.get("personal_name"), + personal_id=data.get("personal_id"), + company_name=data.get("company_name"), + tax_id=data.get("tax_id"), + business_license_url=data.get("business_license_url"), + status=CertStatus.pending, + ) + self.db.add(cert) + await self.db.flush() + return {"id": str(cert.id), "status": cert.status.value} + + async def get_user_cert(self, user_id: str) -> Optional[Dict[str, Any]]: + result = await self.db.execute( + select(Certification) + .where(Certification.user_id == uuid.UUID(user_id)) + .order_by(desc(Certification.created_at)) + .limit(1) + ) + cert = result.scalar_one_or_none() + if not cert: + return None + return { + "id": str(cert.id), + "cert_type": cert.cert_type.value, + "personal_name": cert.personal_name, + "personal_id": cert.personal_id, + "company_name": cert.company_name, + "tax_id": cert.tax_id, + "business_license_url": cert.business_license_url, + "status": cert.status.value, + "reject_reason": cert.reject_reason, + "created_at": cert.created_at.isoformat() if cert.created_at else None, + "updated_at": cert.updated_at.isoformat() if cert.updated_at else None, + } + + async def list_all(self, page: int, size: int, status: Optional[str] = None) -> Dict[str, Any]: + query = select(Certification).order_by(desc(Certification.created_at)) + if status: + query = query.where(Certification.status == CertStatus(status)) + offset = (page - 1) * size + result = await self.db.execute(query.offset(offset).limit(size)) + certs = result.scalars().all() + total_result = await self.db.execute( + select(Certification).where(Certification.status == CertStatus(status)) if status else select(Certification) + ) + total = len(total_result.scalars().all()) + return { + "items": [ + { + "id": str(c.id), + "user_id": str(c.user_id), + "cert_type": c.cert_type.value, + "personal_name": c.personal_name, + "personal_id": c.personal_id, + "company_name": c.company_name, + "tax_id": c.tax_id, + "status": c.status.value, + "reject_reason": c.reject_reason, + "created_at": c.created_at.isoformat() if c.created_at else None, + } + for c in certs + ], + "total": total, + "page": page, + "size": size, + } + + async def review(self, cert_id: str, action: str, reason: Optional[str] = None) -> Optional[Dict[str, Any]]: + result = await self.db.execute( + select(Certification).where(Certification.id == uuid.UUID(cert_id)) + ) + cert = result.scalar_one_or_none() + if not cert: + return None + if action == "approve": + cert.status = CertStatus.approved + else: + cert.status = CertStatus.rejected + cert.reject_reason = reason + await self.db.flush() + return {"id": str(cert.id), "status": cert.status.value} + + async def _get_pending(self, user_id: str) -> Optional[Certification]: + result = await self.db.execute( + select(Certification) + .where( + Certification.user_id == uuid.UUID(user_id), + Certification.status == CertStatus.pending, + ) + .limit(1) + ) + return result.scalar_one_or_none() diff --git a/backend/app/services/discovery.py b/backend/app/services/discovery.py new file mode 100644 index 0000000..f6f1ba6 --- /dev/null +++ b/backend/app/services/discovery.py @@ -0,0 +1,272 @@ +import asyncio +import json +import logging +from typing import Dict, Any, Optional + +from app.ai.router import get_ai_router +from app.services.search_web import search_companies, fetch_page_text +from app.services.mcp_search_client import mcp_search + +logger = logging.getLogger(__name__) + +ANALYZE_MATCH_PROMPT = """你是外贸客户分析专家。分析目标公司的业务描述,判断其与用户产品的匹配度。 + +请以 JSON 格式返回(不要用 markdown 代码块标记): +{ + "match_score": 0-100, + "match_reason": "为什么匹配/不匹配", + "company_summary": "这家公司的主要业务", + "product_fit": "产品匹配度说明", + "contact_info": { + "emails": ["找到的邮箱"], + "phones": ["找到的电话"], + "social": ["LinkedIn等社媒链接"] + } +} + +只返回 JSON,不要其他内容。""" + + +class DiscoveryService: + def __init__(self): + ai_router = get_ai_router() + self.ai = ai_router + self._ai_available = len(ai_router.providers) > 0 + + async def search(self, product_description: str, target_market: str) -> Dict[str, Any]: + queries = self._build_queries(product_description, target_market) + all_results = await self._mcp_search_all(queries) + if all_results: + return { + "companies": all_results[:15], + "query": product_description, + "market": target_market, + "provider": "mcp_search", + } + + all_results = await self._google_search_all(queries) + if all_results: + return { + "companies": all_results[:15], + "query": product_description, + "market": target_market, + "provider": "web_search", + } + + logger.info("No real search results, using AI strategy") + return await self._ai_strategy(product_description, target_market) + + async def analyze(self, company_url: str, product_description: str) -> Dict[str, Any]: + page_text = await fetch_page_text(company_url) + company_info = {"url": company_url} + if page_text: + company_info["page_text"] = page_text[:2500] + + if not self._ai_available: + return self._template_analysis(company_url) + + prompt = f"""用户的产品:{product_description} + +目标公司信息: +URL: {company_url} +网页内容:{page_text[:2500] if page_text else "无法获取网页内容"} + +请分析该公司的业务与用户产品的匹配度。""" + try: + result = await self.ai.chat(prompt, system_prompt=ANALYZE_MATCH_PROMPT) + content = result.get("reply", "") + parsed = self._extract_json(content) + if parsed: + parsed["url"] = company_url + parsed["provider"] = result.get("provider_used", "unknown") + return parsed + except (json.JSONDecodeError, Exception) as e: + logger.warning(f"Analysis AI parse failed: {e}") + return self._template_analysis(company_url) + + async def outreach(self, company_info: Dict[str, Any], product_info: Dict[str, Any]) -> Dict[str, Any]: + if not self._ai_available: + return self._template_outreach(company_info, product_info) + + prompt = f"""目标公司信息: +{json.dumps(company_info, ensure_ascii=False)} + +我的产品信息: +{json.dumps(product_info, ensure_ascii=False)} + +请生成个性化触达文案。""" + system = """你是外贸开发信专家。根据目标公司信息和你的产品,生成个性化触达文案。 + +请以 JSON 格式返回(不要用 markdown 代码块标记): +{ + "subject": "邮件标题(如适用)", + "linkedin_message": "LinkedIn 私信文案(150字以内)", + "whatsapp_message": "WhatsApp 消息文案(100字以内)", + "email_body": "邮件正文(含开头问候、自我介绍、价值主张、行动号召、签名)", + "key_points": ["客户关注的3个要点"], + "tips": ["发送时的建议"] +}""" + try: + result = await self.ai.chat(prompt, system_prompt=system) + content = result.get("reply", "") + parsed = self._extract_json(content) + if parsed: + parsed["provider"] = result.get("provider_used", "unknown") + return parsed + except (json.JSONDecodeError, Exception) as e: + logger.warning(f"Outreach AI parse failed: {e}") + return self._template_outreach(company_info, product_info) + + async def _mcp_search_all(self, queries: list) -> list: + seen_urls = set() + tasks = [asyncio.create_task(mcp_search(q, max_results=6)) for q in queries[:2]] + all_results = [] + try: + for coro in asyncio.as_completed(tasks, timeout=8): + try: + results = await coro + for r in results: + url = r.get("url", "").rstrip("/") + if url and url not in seen_urls: + seen_urls.add(url) + all_results.append(r) + except (asyncio.TimeoutError, Exception) as e: + logger.debug(f"MCP search query failed: {e}") + except asyncio.TimeoutError: + logger.warning("MCP search overall timeout") + finally: + for t in tasks: + if not t.done(): + t.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + if all_results: + return self._dedup_and_filter(all_results)[:15] + return [] + + def _dedup_and_filter(self, results: list) -> list: + seen = set() + filtered = [] + for r in results: + url = r.get("url", "").rstrip("/") + title = r.get("title", "") + if not url or url in seen: + continue + seen.add(url) + s = url.split("/")[2] if "://" in url else url + hostname = s.split(":")[0].lower() if ":" in s else s.lower() + if any(tld in hostname for tld in [".cn", ".com.cn", ".edu", ".ac.", ".gov"]): + continue + if any(domain in hostname for domain in + ["sciencedirect", "mdpi", "springer", "wiley", "acm.org", + "ieee.org", "researchgate", "nature.com", "oup.com", + "sagepub", "tandfonline", "ncbi", "semanticscholar", + "britannica", "dictionary", "cambridge", "iciba", "wikipedia"]): + continue + filtered.append(r) + return filtered + + async def _google_search_all(self, queries: list) -> list: + all_results = [] + seen_urls = set() + for q in queries[:3]: + results = await search_companies(q, max_results=8) + for r in results: + url = r["url"].rstrip("/") + if url not in seen_urls: + seen_urls.add(url) + all_results.append(r) + if len(all_results) >= 15: + break + return self._dedup_and_filter(all_results)[:15] + + def _build_queries(self, product: str, market: str) -> list: + return [ + f"{product} importer {market}", + f"{product} distributor {market}", + f"{product} wholesale buyer {market}", + f"{product} procurement {market}", + f"{product} company {market}", + f"buy {product} from {market}", + f"{product} supply chain {market}", + f"top {product} manufacturers {market}", + f"{product} import export {market}", + f"{product} trading company {market}", + ] + + def _extract_json(self, text: str) -> Optional[dict]: + text = text.strip() + for prefix in ["```json", "```", "```JSON"]: + if text.startswith(prefix): + text = text[len(prefix):] + for suffix in ["```"]: + if text.endswith(suffix): + text = text[:-len(suffix)] + text = text.strip() + try: + return json.loads(text) + except json.JSONDecodeError: + import re + brace = text.find("{") + end = text.rfind("}") + if brace >= 0 and end > brace: + try: + return json.loads(text[brace:end+1]) + except json.JSONDecodeError: + pass + return None + + async def _ai_strategy(self, product: str, market: str) -> Dict[str, Any]: + if not self._ai_available: + return self._template_strategy(product, market) + system = """你是外贸客户发现专家。根据用户的产品和目标市场,分析出潜在买家画像和获取策略。 + +请以 JSON 格式返回(不要用 markdown 代码块标记): +{ + "buyer_personas": [{"type": "", "description": "", "channels": [], "search_queries": []}], + "strategy": "", + "tips": [] +}""" + prompt = f"产品:{product}\n目标市场:{market}\n请分析潜在买家画像和获取策略。" + try: + result = await self.ai.chat(prompt, system_prompt=system) + content = result.get("reply", "") + parsed = self._extract_json(content) + if parsed: + parsed["provider"] = result.get("provider_used", "unknown") + return parsed + return self._template_strategy(product, market) + except Exception as e: + logger.warning(f"AI strategy failed: {e}") + return self._template_strategy(product, market) + + def _template_strategy(self, product: str, market: str) -> Dict[str, Any]: + return { + "buyer_personas": [ + {"type": "进口商/批发商", "description": f"从中国进口{product}并在{market}批发的贸易商", "channels": ["LinkedIn", "Google"], "search_queries": [f"{product} importer {market}"]}, + {"type": "品牌商/OEM买家", "description": f"在{market}销售自有品牌{product}的公司", "channels": ["LinkedIn", "行业展会"], "search_queries": [f"{product} manufacturer {market}"]}, + ], + "strategy": f"建议在 LinkedIn 和 Google 搜索 {market} 的 {product} 相关公司", + "tips": ["使用多个搜索词", "找到公司后在 LinkedIn 找决策人"], + "provider": "template", + } + + def _template_analysis(self, url: str) -> Dict[str, Any]: + return { + "match_score": 50, + "match_reason": "无法获取网页内容进行分析,建议手动查看", + "url": url, + "provider": "template", + } + + def _template_outreach(self, company: Dict[str, Any], product: Dict[str, Any]) -> Dict[str, Any]: + company_name = company.get("name", "") + product_name = product.get("name", "") + return { + "subject": f"关于{product_name}的合作机会", + "linkedin_message": f"您好!了解到贵司{company_name}在经营相关业务,我们专业生产{product_name},品质稳定,价格有竞争力。如有兴趣,我可以发详细资料供参考。", + "whatsapp_message": f"Hello! We are a professional {product_name} manufacturer. Interested in exploring cooperation? Happy to share details.", + "email_body": f"Dear {company_name} team,\n\nWe are a professional {product_name} manufacturer with competitive pricing and consistent quality. Would you be open to a quick chat to explore potential cooperation?\n\nBest regards,\n[Your Name]", + "key_points": ["产品质量有保障", "价格有竞争力", "可定制"], + "tips": ["发送前先了解对方背景", "LinkedIn 消息要简短"], + "provider": "template", + } diff --git a/backend/app/services/invoice.py b/backend/app/services/invoice.py new file mode 100644 index 0000000..0b46524 --- /dev/null +++ b/backend/app/services/invoice.py @@ -0,0 +1,126 @@ +from typing import Optional, Dict, Any, List +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, desc +from app.models.invoice import Invoice, InvoiceType, InvoiceStatus +from app.models.certification import Certification, CertStatus +from datetime import datetime +import uuid + + +class InvoiceService: + def __init__(self, db: AsyncSession): + self.db = db + + async def apply(self, user_id: str, data: Dict[str, Any]) -> Dict[str, Any]: + invoice_type = InvoiceType(data["invoice_type"]) + certification_id = None + cert = None + + if invoice_type == InvoiceType.individual: + cert_result = await self.db.execute( + select(Certification) + .where( + Certification.user_id == uuid.UUID(user_id), + Certification.cert_type == "individual", + Certification.status == CertStatus.approved, + ) + .limit(1) + ) + cert = cert_result.scalar_one_or_none() + if not cert: + return {"error": "请先完成个人实名认证"} + certification_id = cert.id + + else: + cert_result = await self.db.execute( + select(Certification) + .where( + Certification.user_id == uuid.UUID(user_id), + Certification.cert_type == "enterprise", + Certification.status == CertStatus.approved, + ) + .limit(1) + ) + cert = cert_result.scalar_one_or_none() + if not cert: + return {"error": "请先完成企业认证"} + certification_id = cert.id + + invoice = Invoice( + user_id=uuid.UUID(user_id), + certification_id=certification_id, + invoice_type=invoice_type, + title=data["title"], + tax_id=data.get("tax_id"), + amount=data["amount"], + status=InvoiceStatus.pending, + ) + self.db.add(invoice) + await self.db.flush() + return {"id": str(invoice.id), "status": invoice.status.value} + + async def list_user(self, user_id: str) -> List[Dict[str, Any]]: + result = await self.db.execute( + select(Invoice) + .where(Invoice.user_id == uuid.UUID(user_id)) + .order_by(desc(Invoice.created_at)) + ) + invoices = result.scalars().all() + return [ + { + "id": str(inv.id), + "invoice_type": inv.invoice_type.value, + "title": inv.title, + "tax_id": inv.tax_id, + "amount": inv.amount, + "status": inv.status.value, + "reject_reason": inv.reject_reason, + "issued_at": inv.issued_at.isoformat() if inv.issued_at else None, + "created_at": inv.created_at.isoformat() if inv.created_at else None, + } + for inv in invoices + ] + + async def list_all(self, page: int, size: int, status: Optional[str] = None) -> Dict[str, Any]: + query = select(Invoice).order_by(desc(Invoice.created_at)) + if status: + query = query.where(Invoice.status == InvoiceStatus(status)) + offset = (page - 1) * size + result = await self.db.execute(query.offset(offset).limit(size)) + invoices = result.scalars().all() + return { + "items": [ + { + "id": str(inv.id), + "user_id": str(inv.user_id), + "invoice_type": inv.invoice_type.value, + "title": inv.title, + "tax_id": inv.tax_id, + "amount": inv.amount, + "status": inv.status.value, + "reject_reason": inv.reject_reason, + "issued_at": inv.issued_at.isoformat() if inv.issued_at else None, + "created_at": inv.created_at.isoformat() if inv.created_at else None, + } + for inv in invoices + ], + "total": len(invoices), + "page": page, + "size": size, + } + + async def process(self, invoice_id: str, action: str, reason: Optional[str] = None) -> Optional[Dict[str, Any]]: + result = await self.db.execute( + select(Invoice).where(Invoice.id == uuid.UUID(invoice_id)) + ) + inv = result.scalar_one_or_none() + if not inv: + return None + if action == "issue": + inv.status = InvoiceStatus.issued + inv.issued_at = datetime.utcnow() + else: + inv.status = InvoiceStatus.rejected + inv.reject_reason = reason + await self.db.flush() + return {"id": str(inv.id), "status": inv.status.value} diff --git a/backend/app/services/mcp_search_client.py b/backend/app/services/mcp_search_client.py new file mode 100644 index 0000000..4530fe7 --- /dev/null +++ b/backend/app/services/mcp_search_client.py @@ -0,0 +1,101 @@ +import asyncio +import json +import logging +import os +import sys +import warnings +from typing import Dict, Any, List, Optional + +from mcp.client.stdio import stdio_client, StdioServerParameters +from mcp.client.session import ClientSession + +logger = logging.getLogger(__name__) + +SERVER_SCRIPT = os.path.join(os.path.dirname(__file__), "mcp_search_server.py") +VENV_PYTHON = sys.executable + + +class MCPClientManager: + _instance: Optional["MCPClientManager"] = None + _lock = asyncio.Lock() + + def __init__(self): + self._session: Optional[ClientSession] = None + self._read = None + self._write = None + self._ctx = None + self._initialized = False + + @classmethod + async def get_instance(cls) -> "MCPClientManager": + if cls._instance is None or not cls._instance._initialized: + async with cls._lock: + if cls._instance is None or not cls._instance._initialized: + cls._instance = cls() + try: + await asyncio.wait_for(cls._instance._start(), timeout=10) + except Exception as e: + logger.warning(f"MCP init failed: {e}") + cls._instance = None + raise + return cls._instance + + async def _start(self): + params = StdioServerParameters( + command=VENV_PYTHON, + args=[SERVER_SCRIPT], + ) + self._ctx = stdio_client(params) + self._read, self._write = await asyncio.wait_for( + self._ctx.__aenter__(), timeout=5 + ) + self._session = await asyncio.wait_for( + ClientSession(self._read, self._write).__aenter__(), timeout=5 + ) + await asyncio.wait_for(self._session.initialize(), timeout=5) + self._initialized = True + logger.info("MCP search client initialized") + + async def search(self, query: str, max_results: int = 10) -> List[Dict[str, str]]: + if not self._initialized or self._session is None: + logger.warning("MCP client not initialized") + return [] + try: + result = await asyncio.wait_for( + self._session.call_tool( + "web_search", + {"query": query, "max_results": max_results}, + ), + timeout=10, + ) + if result.content and len(result.content) > 0: + text = result.content[0].text + data = json.loads(text) + return data.get("results", []) + return [] + except (asyncio.TimeoutError, Exception) as e: + logger.warning(f"MCP search call failed: {e}") + return [] + + async def close(self): + self._initialized = False + MCPClientManager._instance = None + if self._session: + try: + await self._session.__aexit__(None, None, None) + except (BaseExceptionGroup, RuntimeError, Exception): + pass + if self._ctx: + try: + await self._ctx.__aexit__(None, None, None) + except (BaseExceptionGroup, RuntimeError, Exception): + pass + + +async def mcp_search(query: str, max_results: int = 10) -> List[Dict[str, str]]: + try: + mgr = await MCPClientManager.get_instance() + return await mgr.search(query, max_results) + except Exception as e: + logger.warning(f"MCP search failed: {e}") + return [] diff --git a/backend/app/services/mcp_search_server.py b/backend/app/services/mcp_search_server.py new file mode 100644 index 0000000..c096816 --- /dev/null +++ b/backend/app/services/mcp_search_server.py @@ -0,0 +1,105 @@ +import asyncio +import json +import logging +import os +import subprocess +from typing import List, Dict + +from mcp.server.fastmcp import FastMCP + +logger = logging.getLogger(__name__) +PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) +NODE_BIN = "/usr/bin/node" + +BING_SCRIPT = r""" +const p = require('puppeteer'); +(async () => { + const b = await p.launch({headless:true,args:['--no-sandbox','--disable-setuid-sandbox','--disable-blink-features=AutomationControlled']}); + const page = await b.newPage(); + await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); + await page.setExtraHTTPHeaders({'Accept-Language':'en-US,en;q=0.9'}); + await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', {get:()=>undefined}); }); + const q = process.argv[process.argv.length - 2]; + const max = parseInt(process.argv[process.argv.length - 1] || '10', 10); + const sk = ['bing.com','google.com','facebook.com','twitter.com','instagram.com','youtube.com','reddit.com','amazon.com','wikipedia.org','baidu.com','linkedin.com','pinterest.com','ebay.com','walmart.com','w3.org','whatsapp.com','wechat.com','qq.com','taobao.com','tmall.com','alibaba.com','alipay.com','dict','dictionary','translate','zhihu.com','baike.baidu.com','sogou.com','163.com','sohu.com','sina.com','iciba.com','cambridge','britannica','sciencedirect','mdpi.com','springer','wiley.com','acm.org','ieee.org','researchgate','semanticscholar','ncbi.nlm.nih','nature.com','oup.com','sagepub','tandfonline']; + try { + await page.goto('https://cn.bing.com/search?q=' + encodeURIComponent(q) + '&setlang=en-US&cc=US', {waitUntil:'domcontentloaded',timeout:10000}); + await page.waitForSelector('.b_algo', {timeout:5000}).catch(()=>{}); + const results = await page.evaluate((m, sk) => { + const reCJK = /[\u4e00-\u9fff\u3400-\u4dbf]/; + const found = []; const seen = new Set(); + document.querySelectorAll('li.b_algo').forEach(li => { + const a = li.querySelector('h2 a'); if (!a) return; + let url = (a.href || '').replace(/\/$/,''); + if (!url.startsWith('http') || seen.has(url)) return; + seen.add(url); + if (sk.some(d => url.includes(d))) return; + const hostname = url.replace(/^https?:\/\//,'').split('/')[0]; + if (hostname.endsWith('.cn') || hostname.endsWith('.com.cn') || hostname.endsWith('.edu') || hostname.endsWith('.ac')) return; + const title = (a.textContent||'').trim().substring(0,100); + if (reCJK.test(title)) return; + const s = li.querySelector('.b_caption p, .b_lineclamp2'); + found.push({title, url, snippet:s?s.textContent.trim().substring(0,200):''}); + }); + return found.slice(0,m); + }, max, sk); + console.log(JSON.stringify(results)); + } catch(e) { console.log('[]'); } + await b.close(); +})(); +""" + + +BING_SCRIPT_FILE = os.path.join(os.path.dirname(__file__), "_bing_search.js") +NODE_MODULES = os.path.join(PROJECT_ROOT, "node_modules") + + +async def search_bing(query: str, max_results: int = 10) -> List[Dict[str, str]]: + try: + with open(BING_SCRIPT_FILE, "w") as f: + f.write(BING_SCRIPT) + env = os.environ.copy() + env["NODE_PATH"] = NODE_MODULES + result = subprocess.run( + [NODE_BIN, BING_SCRIPT_FILE, query, str(max_results)], + capture_output=True, + text=True, + timeout=15, + cwd=PROJECT_ROOT, + env=env, + ) + if result.returncode != 0: + logger.warning(f"Bing search failed: {result.stderr[:300]}") + return [] + for line in result.stdout.strip().split("\n"): + line = line.strip() + if line.startswith("["): + return json.loads(line) + return [] + except subprocess.TimeoutExpired: + logger.warning("Bing search timed out") + return [] + except (json.JSONDecodeError, Exception) as e: + logger.warning(f"Bing search error: {e}") + return [] + + +mcp = FastMCP("trade-search", log_level="WARNING") + + +@mcp.tool( + name="web_search", + description="Search the web for companies, buyers, or business information. Returns title, URL, and snippet for each result. Useful for finding potential customers, researching companies, or gathering market intelligence.", +) +async def web_search(query: str, max_results: int = 10) -> str: + results = await search_bing(query, max_results) + if not results: + return json.dumps({"results": [], "error": None}) + return json.dumps({"results": results, "error": None}) + + +def main(): + asyncio.run(mcp.run_stdio_async()) + +if __name__ == "__main__": + main() diff --git a/backend/app/services/search_web.py b/backend/app/services/search_web.py new file mode 100644 index 0000000..717736a --- /dev/null +++ b/backend/app/services/search_web.py @@ -0,0 +1,73 @@ +from typing import List, Dict, Optional +import httpx +import json +import logging +from app.config import settings + +logger = logging.getLogger(__name__) + +GOOGLE_CSE_URL = "https://www.googleapis.com/customsearch/v1" + +IGNORE_DOMAINS = [ + "google.com", "facebook.com", "twitter.com", "instagram.com", + "youtube.com", "reddit.com", "amazon.com", "ebay.com", + "wikipedia.org", "linkedin.com", "pinterest.com", "baidu.com", + "bing.com", "duckduckgo.com", +] + + +async def search_companies(query: str, max_results: int = 10) -> List[Dict[str, str]]: + api_key = settings.GOOGLE_API_KEY or "" + cse_id = settings.GOOGLE_CSE_ID or "" + if api_key and cse_id: + return await _google_cse(query, max_results, api_key, cse_id) + logger.info("Google CSE not configured, using template results") + return [] + + +async def _google_cse(query: str, max_results: int, api_key: str, cse_id: str) -> List[Dict[str, str]]: + try: + async with httpx.AsyncClient(timeout=15.0) as client: + resp = await client.get(GOOGLE_CSE_URL, params={ + "key": api_key, + "cx": cse_id, + "q": query, + "num": min(max_results, 10), + "lr": "lang_en", + }) + if resp.status_code != 200: + logger.warning(f"Google CSE returned {resp.status_code}") + return [] + data = resp.json() + results = [] + for item in data.get("items", []): + url = item.get("link", "") + if not url or any(d in url for d in IGNORE_DOMAINS): + continue + results.append({ + "title": item.get("title", url)[:100], + "url": url.rstrip("/"), + "snippet": item.get("snippet", "")[:200], + }) + return results[:max_results] + except Exception as e: + logger.warning(f"Google CSE failed: {e}") + return [] + + +async def fetch_page_text(url: str) -> Optional[str]: + try: + async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: + resp = await client.get(url, headers={"User-Agent": "Mozilla/5.0"}) + if resp.status_code == 200: + from bs4 import BeautifulSoup + soup = BeautifulSoup(resp.text, "html.parser") + for tag in soup(["script", "style", "nav", "footer", "header"]): + tag.decompose() + text = soup.get_text(separator=" ", strip=True) + import re + text = re.sub(r"\s+", " ", text)[:3000] + return text if len(text) > 100 else None + except Exception as e: + logger.debug(f"fetch {url} failed: {e}") + return None diff --git a/backend/requirements.txt b/backend/requirements.txt index b04745b..ee756dd 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,14 +1,15 @@ -fastapi==0.100.0 -uvicorn==0.23.2 +fastapi==0.136.1 +uvicorn==0.47.0 sqlalchemy==1.4.48 asyncpg==0.27.0 -pydantic==1.10.12 +pydantic==2.13.4 +pydantic-settings==2.14.1 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 python-multipart==0.0.6 redis==4.5.5 celery==5.2.7 -httpx==0.23.3 +httpx>=0.23.3,<0.28 openai==1.12.0 anthropic==0.8.1 jinja2==3.1.2 @@ -19,4 +20,6 @@ pytest-asyncio==0.21.1 pytest-cov==4.1.0 weasyprint==60.2 openpyxl==3.1.2 -edge-tts>=6.0.0 \ No newline at end of file +edge-tts>=6.0.0 +mcp==1.27.1 +starlette==1.0.0 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d13b3b5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,757 @@ +{ + "name": "trade-assistant", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "puppeteer": "^25.0.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@puppeteer/browsers/-/browsers-3.0.3.tgz", + "integrity": "sha512-v3YaiGpzUTgOZkHBFR0iZg58Vto25SqBQxfLUXDiofJccwVl6Mlr7BdLCS1NZgxikdeIHf936cxYWL9IZp3tow==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "progress": "^2.0.3", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.3", + "resolved": "https://registry.npmmirror.com/bare-events/-/bare-events-2.8.3.tgz", + "integrity": "sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmmirror.com/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmmirror.com/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmmirror.com/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/bare-url/-/bare-url-2.4.3.tgz", + "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "16.0.1", + "resolved": "https://registry.npmmirror.com/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmmirror.com/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "25.0.4", + "resolved": "https://registry.npmmirror.com/puppeteer/-/puppeteer-25.0.4.tgz", + "integrity": "sha512-QFdBAuNOqL0I+AdARTlRR1KcgPk0fo0dU127e1ZQFVxb9QPcpBDIiQp/dMgdbyLXHpF2GRjC/OezDmjKcLCKYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.3", + "chromium-bidi": "16.0.1", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1608973", + "puppeteer-core": "25.0.4", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.0.4", + "resolved": "https://registry.npmmirror.com/puppeteer-core/-/puppeteer-core-25.0.4.tgz", + "integrity": "sha512-K1LQKDP6w1rIr1jUyN9obH16TO/DCy86k3q+FBd2prGY+TStxhFySxmaZZuRF+0D3BJXjwCYFke7tMHCH4olTA==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.3", + "chromium-bidi": "16.0.1", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmmirror.com/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmmirror.com/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..73ac75b --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "puppeteer": "^25.0.4" + } +} diff --git a/uni-app/src/App.vue b/uni-app/src/App.vue index eb2918e..05db8a4 100644 --- a/uni-app/src/App.vue +++ b/uni-app/src/App.vue @@ -1,207 +1,13 @@ diff --git a/uni-app/src/config.js b/uni-app/src/config.js index 90200e0..7dd275c 100644 --- a/uni-app/src/config.js +++ b/uni-app/src/config.js @@ -22,8 +22,11 @@ export const PAGES = { FOLLOWUP: '/pages/followup/followup', NOTIFICATION: '/pages/notification/notification', ANALYTICS: '/pages/analytics/analytics', + DISCOVERY: '/pages/discovery/discovery', TEAM: '/pages/team/team', ADMIN: '/pages/admin/admin', + CERTIFICATION: '/pages/certification/certification', + INVOICE: '/pages/invoice/invoice', AGREEMENT_PRIVACY: '/pages/agreement/privacy', AGREEMENT_TERMS: '/pages/agreement/terms', } diff --git a/uni-app/src/pages.json b/uni-app/src/pages.json index eea5f9a..4451b5e 100644 --- a/uni-app/src/pages.json +++ b/uni-app/src/pages.json @@ -46,7 +46,7 @@ { "path": "pages/admin/admin", "style": { - "navigationBarTitleText": "管理后台" + "navigationStyle": "custom" } }, { @@ -102,6 +102,24 @@ "style": { "navigationBarTitleText": "个人中心" } + }, + { + "path": "pages/discovery/discovery", + "style": { + "navigationBarTitleText": "挖掘新客" + } + }, + { + "path": "pages/certification/certification", + "style": { + "navigationBarTitleText": "实名认证" + } + }, + { + "path": "pages/invoice/invoice", + "style": { + "navigationBarTitleText": "发票管理" + } } ], "globalStyle": { diff --git a/uni-app/src/pages/admin/admin.vue b/uni-app/src/pages/admin/admin.vue index 10ba9f9..15b4809 100644 --- a/uni-app/src/pages/admin/admin.vue +++ b/uni-app/src/pages/admin/admin.vue @@ -1,13 +1,45 @@ @@ -572,19 +691,110 @@ const resetQuota = async (version) => { } } +const certs = ref([]) +const certFilter = ref('pending') +const showRejectCert = ref(null) +const showRejectInv = ref(null) +const rejectReason = ref('') +const adminInvoices = ref([]) +const invFilter = ref('pending') + +const loadCerts = async () => { + try { + const res = await adminApi.listCertifications(1, 50, certFilter.value || undefined) + certs.value = res.items || [] + } catch (e) { certs.value = [] } +} + +const reviewCert = async (id, action) => { + try { + await adminApi.reviewCertification(id, action) + uni.showToast({ title: action === 'approve' ? '已通过' : '已驳回', icon: 'success' }) + loadCerts() + } catch (e) { + uni.showToast({ title: e.message || '操作失败', icon: 'none' }) + } +} + +const confirmReject = async () => { + const id = showRejectCert.value || showRejectInv.value + const type = showRejectCert.value ? 'cert' : 'inv' + if (!rejectReason.value) { uni.showToast({ title: '请输入驳回原因', icon: 'none' }); return } + showRejectCert.value = null + showRejectInv.value = null + try { + if (type === 'cert') { + await adminApi.reviewCertification(id, 'reject', rejectReason.value) + } else { + await adminApi.processInvoice(id, 'reject', rejectReason.value) + } + uni.showToast({ title: '已驳回', icon: 'success' }) + rejectReason.value = '' + loadCerts() + loadAdminInvoices() + } catch (e) { + uni.showToast({ title: e.message || '操作失败', icon: 'none' }) + } +} + +const loadAdminInvoices = async () => { + try { + const res = await adminApi.listInvoices(1, 50, invFilter.value || undefined) + adminInvoices.value = res.items || [] + } catch (e) { adminInvoices.value = [] } +} + +const processInv = async (id, action) => { + try { + await adminApi.processInvoice(id, action) + uni.showToast({ title: action === 'issue' ? '已开票' : '已驳回', icon: 'success' }) + loadAdminInvoices() + } catch (e) { + uni.showToast({ title: e.message || '操作失败', icon: 'none' }) + } +} + watch(tab, (val) => { if (val === 'stats') loadUsageStats() else if (val === 'logs') { logPage.value = 1; loadLogs() } else if (val === 'config') loadConfig() else if (val === 'quota') loadQuotas() + else if (val === 'cert') loadCerts() + else if (val === 'invoice') loadAdminInvoices() }) diff --git a/uni-app/src/pages/certification/certification.vue b/uni-app/src/pages/certification/certification.vue new file mode 100644 index 0000000..c05e681 --- /dev/null +++ b/uni-app/src/pages/certification/certification.vue @@ -0,0 +1,172 @@ + + + + + diff --git a/uni-app/src/pages/discovery/discovery.vue b/uni-app/src/pages/discovery/discovery.vue new file mode 100644 index 0000000..dd4b91b --- /dev/null +++ b/uni-app/src/pages/discovery/discovery.vue @@ -0,0 +1,357 @@ +