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 @@
+
+
+
+
+
+
+
+
+ {{ row.cert_type === 'individual' ? '个人认证' : '企业认证' }}
+
+
+ {{ row.personal_name || '-' }}
+
+
+ {{ row.company_name || '-' }}
+
+
+ {{ row.tax_id || '-' }}
+
+
+ {{ row.user_id?.substring(0,8) }}...
+
+
+
+
+ {{ { pending: '待审核', approved: '已通过', rejected: '已驳回' }[row.status] }}
+
+
+
+
+
+
+
+ 通过
+ 驳回
+
+ 原因:{{ row.reject_reason }}
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认驳回
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ {{ fieldLabel(cfg.key, k) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ {{ item.value }}
+ {{ item.label }}
+
+
+
+
+
+ 最近注册用户
+
+
+
+
+
+ {{ row.tier }}
+
+
+
+
+ {{ row.role }}
+
+
+
+
+
+ 详情
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ {{ row.invoice_type === 'individual' ? '个人' : '企业' }}
+
+
+
+
+ ¥{{ (row.amount || 0).toFixed(2) }}
+
+
+ {{ row.user_id?.substring(0,8) }}...
+
+
+
+
+ {{ { pending: '待开票', issued: '已开票', rejected: '已驳回' }[row.status] }}
+
+
+
+
+
+
+
+ 已开票
+ 驳回
+
+ 原因:{{ row.reject_reason }}
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认驳回
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
TradeMate 管理后台
+
一站式管理你的外贸业务:用户、数据、配置、认证与发票
+
+
+
+
管理员登录
+
+
+
+
+
+
+
+ {{ error }}
+ 登录
+
+
+
+
+
已登录
+ 进入后台
+
+
+
+
+
+
+
+
+
+
+
+
{{ f.title }}
+
{{ f.desc }}
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
TradeMate 管理后台
+
+
+
+
+
+
+
+
+ 登录
+
+
+
{{ error }}
+
返回首页
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 筛选
+ 重置
+
+
+
+
+
+ 操作日志 (共 {{ total }} 条)
+
+
+
+ {{ row.user_id?.substring(0,8) }}...
+
+
+
+
+ {{ JSON.stringify(row.detail) }}
+ -
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ {{ q.current_month || '当前月' }}
+
+ {{ q.used_chars }} / {{ q.monthly_limit }}
+
+
+
+
+
+
+
+
+
+
+ 保存
+ 重置用量
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ {{ item.value }}
+ {{ item.label }}
+
+
+
+
+
+
+
+ 今日各功能调用
+
+
+ {{ action }}
+
+ {{ count }}
+
+
+
+
+
+
+
+ 近7日趋势
+
+
+ {{ dayjs(d.date).format('MM-DD') }}
+
+ {{ d.count }}
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ 搜索
+ 清空
+
+
+
+
+
+
+ {{ row.tier }}
+
+
+ {{ row.role }}
+
+
+
+ 免费
+ Pro
+ 企业
+ {{ row.is_active?'禁用':'启用' }}
+
+
+
+
+
+
+ 所有用户 (共 {{ total }} 人)
+
+
+
+
+
+ {{ row.tier }}
+
+
+ {{ row.role }}
+
+
+ {{ row.is_active?'正常':'禁用' }}
+
+
+
+ 免费
+ Pro
+ 企业
+ {{ row.is_active?'禁用':'启用' }}
+ {{ row.role==='admin'?'撤销管理':'设为管理' }}
+
+
+
+
+
+
+
+
+
+
+
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 @@
-
-
-
TradeMate
-
- {{ item.icon }}
- {{ item.text }}
-
-
-
-
+
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 @@
-
- 概览
- 用户
- 统计
- 日志
- 配置
- 翻译配额
-
+
+
+
+ 概览
+ 用户
+ 统计
+ 日志
+ 配置
+ 翻译配额
+ 认证审核
+ 发票管理
+
@@ -266,6 +298,92 @@
+
+
+
+
+
+ 全部
+ 待审核
+ 已通过
+ 已驳回
+
+
+
+
+
+ 姓名:{{ c.personal_name }}
+ 企业:{{ c.company_name }}
+ 税号:{{ c.tax_id }}
+ 用户ID:{{ c.user_id?.substring(0, 8) }}...
+ {{ c.created_at?.substring(0, 10) }}
+
+
+ 通过
+ 驳回
+
+ 驳回原因:{{ c.reject_reason }}
+
+
+ 暂无认证申请
+
+
+
+
+
+
+
+
+ 全部
+ 待开票
+ 已开票
+ 已驳回
+
+
+
+
+
+ {{ inv.invoice_type === 'individual' ? '个人' : '企业' }}发票
+ {{ { pending: '待开票', issued: '已开票', rejected: '已驳回' }[inv.status] }}
+
+
+ 确认已开
+ 驳回
+
+ 驳回原因:{{ inv.reject_reason }}
+
+
+ 暂无开票申请
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确认驳回
+
+
+
+
+
@@ -294,6 +412,7 @@
+
@@ -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 @@
+
+
+
+
+
+ 加载中...
+
+
+
+
+ {{ statusIcon }}
+ {{ statusText }}
+ 原因:{{ cert.reject_reason }}
+
+
+
+ 认证类型
+ {{ cert.cert_type === 'individual' ? '个人认证' : '企业认证' }}
+
+
+ 姓名
+ {{ cert.personal_name }}
+
+
+ 企业名称
+ {{ cert.company_name }}
+
+
+
+
+
+
+
+
+
+ 认证类型
+
+ 个人认证
+ 企业认证
+
+
+
+
+ 姓名
+
+
+
+ 身份证号
+
+
+
+
+
+ 企业名称
+
+
+
+ 统一社会信用代码
+
+
+
+
+
+ 提交后预计 1-3 个工作日审核完成
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ 客户挖掘
+
+
+ 开发信生成
+
+
+
+
+
+ 你的产品/服务
+
+
+
+ 目标市场
+
+
+
+
+
+
+
+
+ {{ company.title }}
+ {{ company.url }}
+ {{ company.snippet }}
+
+ 🔍 分析
+ ➕ 加入客户
+
+
+ 分析中...
+
+
+
+
+ 匹配度
+
+
+
+ {{ company.analysis.match_score || '?' }}/100
+
+ {{ company.analysis.match_reason }}
+
+
+ {{ key }}: {{ Array.isArray(emails) ? emails.join(', ') : emails }}
+
+
+
+
+
+
+
+
+ 由于搜索服务未配置,以下是 AI 推荐的发现策略:
+
+
+ {{ persona.type }}
+ {{ persona.description }}
+
+ {{ ch }}
+
+
+ 搜索关键词:
+ {{ q }}
+
+
+
+
+ 策略建议
+ {{ searchResult.strategy }}
+
+
+
+ 💡 实用建议
+ {{ ti + 1 }}. {{ tip }}
+
+
+
+
+
+
+
+
+ 目标公司名称
+
+
+
+ 公司简介(可选)
+
+
+
+ 你的产品名称
+
+
+
+ 产品优势(可选)
+
+
+
+
+
+
+ 📧 邮件正文
+ {{ outreachResult.email_body }}
+
+
+
+ 💼 LinkedIn 私信
+ {{ outreachResult.linkedin_message }}
+
+
+
+ 📱 WhatsApp 消息
+ {{ outreachResult.whatsapp_message }}
+
+
+
+ 💡 发送建议
+ {{ ti + 1 }}. {{ tip }}
+
+
+
+
+
+
+
+
+
diff --git a/uni-app/src/pages/index/index.vue b/uni-app/src/pages/index/index.vue
index d4b7a79..50a38cb 100644
--- a/uni-app/src/pages/index/index.vue
+++ b/uni-app/src/pages/index/index.vue
@@ -136,6 +136,10 @@
🔤
翻译
+
+ 🔍
+ 挖掘新客
+
📦
产品库
@@ -166,10 +170,6 @@
👨👩👧👦
团队
-
- ⚙️
- 管理
-
💁
联系客服
@@ -277,7 +277,7 @@
+
+
diff --git a/uni-app/src/pages/profile/profile.vue b/uni-app/src/pages/profile/profile.vue
index f941f69..5f328be 100644
--- a/uni-app/src/pages/profile/profile.vue
+++ b/uni-app/src/pages/profile/profile.vue
@@ -28,6 +28,20 @@
+
+ 认证与发票
+
+
+
+
其他