From 62eeb3186c32010b9dc2bd4744aeaf38acffc613 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Mon, 6 Jul 2026 18:13:08 +0800 Subject: [PATCH] Add V2 admin console --- ai_knowledge_base_v2/README.md | 1 + .../apps/admin-web/.dockerignore | 5 + .../apps/admin-web/.env.example | 1 + .../apps/admin-web/.gitignore | 5 + .../apps/admin-web/Dockerfile | 12 + ai_knowledge_base_v2/apps/admin-web/README.md | 33 + .../apps/admin-web/index.html | 12 + .../apps/admin-web/package-lock.json | 1572 +++++++++++++++++ .../apps/admin-web/package.json | 21 + .../apps/admin-web/src/App.vue | 300 ++++ .../apps/admin-web/src/main.ts | 8 + .../apps/admin-web/src/services/api.ts | 62 + .../apps/admin-web/src/styles.css | 190 ++ .../apps/admin-web/src/types/api.ts | 52 + .../apps/admin-web/src/vite-env.d.ts | 1 + .../apps/admin-web/tsconfig.json | 17 + .../apps/admin-web/vite.config.ts | 10 + .../apps/backend/.env.example | 4 + .../apps/backend/app/api/admin_auth.py | 24 + .../apps/backend/app/api/admin_dashboard.py | 22 + .../apps/backend/app/api/admin_knowledge.py | 95 + .../apps/backend/app/api/admin_records.py | 118 ++ .../apps/backend/app/api/admin_settings.py | 159 ++ .../apps/backend/app/api/admin_users.py | 98 + .../apps/backend/app/api/router.py | 19 +- .../apps/backend/app/core/config.py | 3 + .../apps/backend/app/core/dependencies.py | 17 + .../apps/backend/app/core/security.py | 20 + .../apps/backend/app/schemas/admin.py | 71 + .../backend/app/services/admin_service.py | 83 + .../2026-07-06-decision-register.md | 2 + .../2026-07-06-phase5-admin-console.md | 84 + .../development_records/README.md | 1 + ai_knowledge_base_v2/docker-compose.dev.yml | 11 + 34 files changed, 3132 insertions(+), 1 deletion(-) create mode 100644 ai_knowledge_base_v2/apps/admin-web/.dockerignore create mode 100644 ai_knowledge_base_v2/apps/admin-web/.env.example create mode 100644 ai_knowledge_base_v2/apps/admin-web/.gitignore create mode 100644 ai_knowledge_base_v2/apps/admin-web/Dockerfile create mode 100644 ai_knowledge_base_v2/apps/admin-web/README.md create mode 100644 ai_knowledge_base_v2/apps/admin-web/index.html create mode 100644 ai_knowledge_base_v2/apps/admin-web/package-lock.json create mode 100644 ai_knowledge_base_v2/apps/admin-web/package.json create mode 100644 ai_knowledge_base_v2/apps/admin-web/src/App.vue create mode 100644 ai_knowledge_base_v2/apps/admin-web/src/main.ts create mode 100644 ai_knowledge_base_v2/apps/admin-web/src/services/api.ts create mode 100644 ai_knowledge_base_v2/apps/admin-web/src/styles.css create mode 100644 ai_knowledge_base_v2/apps/admin-web/src/types/api.ts create mode 100644 ai_knowledge_base_v2/apps/admin-web/src/vite-env.d.ts create mode 100644 ai_knowledge_base_v2/apps/admin-web/tsconfig.json create mode 100644 ai_knowledge_base_v2/apps/admin-web/vite.config.ts create mode 100644 ai_knowledge_base_v2/apps/backend/app/api/admin_auth.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/api/admin_dashboard.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/api/admin_records.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/api/admin_users.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/schemas/admin.py create mode 100644 ai_knowledge_base_v2/apps/backend/app/services/admin_service.py create mode 100644 ai_knowledge_base_v2/development_records/2026-07-06-phase5-admin-console.md diff --git a/ai_knowledge_base_v2/README.md b/ai_knowledge_base_v2/README.md index b312d7c..9adca5e 100644 --- a/ai_knowledge_base_v2/README.md +++ b/ai_knowledge_base_v2/README.md @@ -40,6 +40,7 @@ docker compose -f docker-compose.dev.yml up --build 默认访问地址: - 用户端 H5:`http://127.0.0.1:5173/` +- 管理后台:`http://127.0.0.1:5174/` - 后端 API:`http://127.0.0.1:8100/api/health` - MySQL:`127.0.0.1:3307` diff --git a/ai_knowledge_base_v2/apps/admin-web/.dockerignore b/ai_knowledge_base_v2/apps/admin-web/.dockerignore new file mode 100644 index 0000000..3aab5df --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/.dockerignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.env +.env.local +*.tsbuildinfo diff --git a/ai_knowledge_base_v2/apps/admin-web/.env.example b/ai_knowledge_base_v2/apps/admin-web/.env.example new file mode 100644 index 0000000..a87eaed --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=http://127.0.0.1:8100/api diff --git a/ai_knowledge_base_v2/apps/admin-web/.gitignore b/ai_knowledge_base_v2/apps/admin-web/.gitignore new file mode 100644 index 0000000..bd22d1f --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.tsbuildinfo +.env +.env.local diff --git a/ai_knowledge_base_v2/apps/admin-web/Dockerfile b/ai_knowledge_base_v2/apps/admin-web/Dockerfile new file mode 100644 index 0000000..0db5cc2 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/Dockerfile @@ -0,0 +1,12 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package*.json ./ +RUN npm ci + +COPY . . + +EXPOSE 5174 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5174"] diff --git a/ai_knowledge_base_v2/apps/admin-web/README.md b/ai_knowledge_base_v2/apps/admin-web/README.md new file mode 100644 index 0000000..e1eb828 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/README.md @@ -0,0 +1,33 @@ +# AI 知识库系统 V2 管理后台 + +这是 V2 的管理后台,使用 Vue 3、Vite、TypeScript 和 Element Plus。 + +## 本地启动 + +```bash +npm install +npm run dev -- --port 5174 +``` + +默认访问地址: + +```text +http://127.0.0.1:5174/ +``` + +默认后端地址: + +```text +http://127.0.0.1:8100/api +``` + +## 默认管理员 + +开发阶段后端会在首次管理员登录时自动创建默认管理员: + +```text +账号:admin +密码:admin123456 +``` + +生产环境必须通过环境变量修改默认密码。 diff --git a/ai_knowledge_base_v2/apps/admin-web/index.html b/ai_knowledge_base_v2/apps/admin-web/index.html new file mode 100644 index 0000000..b12a7d4 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/index.html @@ -0,0 +1,12 @@ + + + + + + AI 知识库后台 + + +
+ + + diff --git a/ai_knowledge_base_v2/apps/admin-web/package-lock.json b/ai_knowledge_base_v2/apps/admin-web/package-lock.json new file mode 100644 index 0000000..19b0348 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/package-lock.json @@ -0,0 +1,1572 @@ +{ + "name": "ai-kb-admin-web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-kb-admin-web", + "version": "0.1.0", + "dependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "element-plus": "^2.11.0", + "vite": "^7.1.0", + "vue": "^3.5.0" + }, + "devDependencies": { + "typescript": "^5.9.0", + "vue-tsc": "^3.1.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.6.tgz", + "integrity": "sha512-LgBMZAy2sR3cQWknpyaxnI6yBkqDfLBPkbdhwRhQCvzfNJRQXPilgQIrdI/v4ytJ0sAq9bWhaPsjqBqneomJ3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, + "node_modules/element-plus": { + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.14.2.tgz", + "integrity": "sha512-eNH9uP3wQoNqieEIHXiNvIVv+zO5sZDU0CAZq5b0zqSN06DD0/V9xIq1R/qm3rw5k3nBTM1JvpxhCfRbaFLzDQ==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.7.6", + "@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.3.3" + }, + "peerDependencies": { + "vue": "^3.3.7" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/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/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/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/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "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/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/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.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "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/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.3.6.tgz", + "integrity": "sha512-FkljacAwJ9BUoSUdpFe3VDy0sGigNlTH9+2zcXUWmZOjN8swiCkl3t48wOJun0OsUd2cEIda1l04tsxMiKIIrQ==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.6.tgz", + "integrity": "sha512-ERXGgbKSBGFUkavrJ1Iwj0ZVxKqB/5UOx65IXy7fPf2UsoI21n3ssQLwdvx8xyUGgJe9PvSZTM/FYzIdwUDPFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.6" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + } + } +} diff --git a/ai_knowledge_base_v2/apps/admin-web/package.json b/ai_knowledge_base_v2/apps/admin-web/package.json new file mode 100644 index 0000000..99c6a59 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/package.json @@ -0,0 +1,21 @@ +{ + "name": "ai-kb-admin-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vue-tsc -b && vite build", + "preview": "vite preview --host 127.0.0.1" + }, + "dependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "element-plus": "^2.11.0", + "vite": "^7.1.0", + "vue": "^3.5.0" + }, + "devDependencies": { + "typescript": "^5.9.0", + "vue-tsc": "^3.1.0" + } +} diff --git a/ai_knowledge_base_v2/apps/admin-web/src/App.vue b/ai_knowledge_base_v2/apps/admin-web/src/App.vue new file mode 100644 index 0000000..80dc8ef --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/src/App.vue @@ -0,0 +1,300 @@ + + + diff --git a/ai_knowledge_base_v2/apps/admin-web/src/main.ts b/ai_knowledge_base_v2/apps/admin-web/src/main.ts new file mode 100644 index 0000000..7dae774 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/src/main.ts @@ -0,0 +1,8 @@ +import ElementPlus from "element-plus"; +import "element-plus/dist/index.css"; +import { createApp } from "vue"; + +import App from "./App.vue"; +import "./styles.css"; + +createApp(App).use(ElementPlus).mount("#app"); diff --git a/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts new file mode 100644 index 0000000..cfba85d --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/src/services/api.ts @@ -0,0 +1,62 @@ +import type { AdminProfile, ApiResponse, DashboardStats, KnowledgeItem, ModelItem, AdminUser } from "../types/api"; + +const API_BASE = import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8100/api"; +const TOKEN_KEY = "ai-kb-admin-token"; + +export function getToken() { + return window.localStorage.getItem(TOKEN_KEY); +} + +export function saveToken(token: string) { + window.localStorage.setItem(TOKEN_KEY, token); +} + +export function clearToken() { + window.localStorage.removeItem(TOKEN_KEY); +} + +async function request(path: string, init: RequestInit = {}): Promise { + const headers = new Headers(init.headers); + headers.set("Content-Type", "application/json"); + const token = getToken(); + if (token) headers.set("Authorization", `Bearer ${token}`); + const response = await fetch(`${API_BASE}${path}`, { ...init, headers }); + const body = (await response.json()) as ApiResponse; + if (!response.ok || body.code !== 0) { + throw new Error(body.message || `${response.status} ${response.statusText}`); + } + return body.data; +} + +export const api = { + login: (username: string, password: string) => + request<{ token: string; admin: AdminProfile }>("/admin/login", { + method: "POST", + body: JSON.stringify({ username, password }), + }), + profile: () => request("/admin/profile"), + dashboard: () => request("/admin/dashboard"), + users: (keyword = "") => request(`/admin/user/list?keyword=${encodeURIComponent(keyword)}`), + updateUser: (id: number, payload: Record) => + request(`/admin/user/${id}`, { method: "PUT", body: JSON.stringify(payload) }), + knowledge: () => request("/admin/knowledge/list"), + createKnowledge: (payload: Record) => + request("/admin/knowledge", { method: "POST", body: JSON.stringify(payload) }), + updateKnowledge: (id: number, payload: Record) => + request(`/admin/knowledge/${id}`, { method: "PUT", body: JSON.stringify(payload) }), + prompt: () => request<{ promptContent: string }>("/admin/prompt"), + savePrompt: (promptContent: string) => + request<{ promptContent: string }>("/admin/prompt", { method: "PUT", body: JSON.stringify({ promptContent }) }), + resetPrompt: () => request<{ promptContent: string }>("/admin/prompt/reset", { method: "POST", body: "{}" }), + models: () => request("/admin/model/list"), + createModel: (payload: Record) => + request("/admin/model", { method: "POST", body: JSON.stringify(payload) }), + enableModel: (modelId: number) => + request("/admin/model/enable", { method: "POST", body: JSON.stringify({ modelId }) }), + configs: () => request[]>("/admin/config"), + saveConfig: (payload: Record) => + request>("/admin/config", { method: "PUT", body: JSON.stringify(payload) }), + chats: () => request[]>("/admin/chat/list"), + aiLogs: () => request[]>("/admin/ai-log/list"), + operationLogs: () => request[]>("/admin/log/list"), +}; diff --git a/ai_knowledge_base_v2/apps/admin-web/src/styles.css b/ai_knowledge_base_v2/apps/admin-web/src/styles.css new file mode 100644 index 0000000..82b5551 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/src/styles.css @@ -0,0 +1,190 @@ +:root { + font-family: Inter, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; + color: #1f2d2a; + background: #f2f5f4; + font-synthesis: none; + text-rendering: optimizeLegibility; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 1180px; +} + +button, +input, +textarea { + font: inherit; +} + +.login-page { + min-height: 100vh; + display: grid; + place-items: center; + background: #eef4f2; +} + +.login-box { + width: 380px; + padding: 32px; + border: 1px solid #dce6e2; + border-radius: 8px; + background: #ffffff; + box-shadow: 0 18px 44px rgba(35, 54, 49, 0.08); +} + +.brand { + width: 44px; + height: 44px; + display: grid; + place-items: center; + margin-bottom: 18px; + border-radius: 8px; + background: #0f8b6f; + color: #ffffff; + font-weight: 800; +} + +.login-box h1, +.page-head h2 { + margin: 0; + letter-spacing: 0; +} + +.login-box p, +.page-head p { + margin: 8px 0 0; + color: #667a73; +} + +.full-btn { + width: 100%; +} + +.admin-shell { + min-height: 100vh; + display: grid; + grid-template-columns: 220px minmax(0, 1fr); +} + +.sidebar { + padding: 20px 14px; + border-right: 1px solid #dfe8e5; + background: #ffffff; +} + +.sidebar-title { + padding: 0 10px 18px; + color: #0f735d; + font-weight: 800; +} + +.sidebar button { + width: 100%; + height: 40px; + margin-bottom: 4px; + padding: 0 12px; + border: 0; + border-radius: 6px; + background: transparent; + color: #40524b; + text-align: left; + cursor: pointer; +} + +.sidebar button.active { + background: #e9f5f1; + color: #0f735d; + font-weight: 700; +} + +.workspace { + min-width: 0; + display: grid; + grid-template-rows: 58px minmax(0, 1fr); +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 24px; + border-bottom: 1px solid #dfe8e5; + background: #ffffff; +} + +.topbar div { + display: grid; + gap: 2px; +} + +.topbar span { + color: #70837c; + font-size: 12px; +} + +.content { + min-width: 0; + padding: 24px; + overflow: auto; +} + +.page-head { + margin-bottom: 18px; +} + +.page-head.inline { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.page-head.inline .el-input { + width: 260px; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 14px; +} + +.stat { + padding: 18px; + border: 1px solid #dfe8e5; + border-radius: 8px; + background: #ffffff; +} + +.stat span { + display: block; + color: #667a73; + font-size: 13px; +} + +.stat strong { + display: block; + margin-top: 10px; + font-size: 28px; +} + +.inline-form, +.model-form { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 16px; +} + +.model-form { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.actions { + margin-top: 12px; +} diff --git a/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts new file mode 100644 index 0000000..55c3c94 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/src/types/api.ts @@ -0,0 +1,52 @@ +export interface ApiResponse { + code: number; + message: string; + data: T; +} + +export interface AdminProfile { + id: number; + username: string; + name: string; + status: number; +} + +export interface DashboardStats { + userCount: number; + sessionCount: number; + messageCount: number; + aiRequestCount: number; + knowledgeCount: number; +} + +export interface AdminUser { + id: number; + phone: string; + name: string; + status: number; + dailyChatLimit: number; + dailyChatUsed: number; + expiredAt?: string | null; + lastLoginAt?: string | null; +} + +export interface KnowledgeItem { + id: number; + name: string; + feishuSpaceId: string; + feishuNodeId: string; + status: number; + remark?: string | null; +} + +export interface ModelItem { + id: number; + provider: string; + modelName: string; + apiUrl: string; + apiKeyMasked: string; + temperature?: number | null; + maxToken?: number | null; + timeoutSecond: number; + enabled: number; +} diff --git a/ai_knowledge_base_v2/apps/admin-web/src/vite-env.d.ts b/ai_knowledge_base_v2/apps/admin-web/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/ai_knowledge_base_v2/apps/admin-web/tsconfig.json b/ai_knowledge_base_v2/apps/admin-web/tsconfig.json new file mode 100644 index 0000000..ca98fa9 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "preserve", + "strict": true + }, + "include": ["src/**/*.ts", "src/**/*.vue"] +} diff --git a/ai_knowledge_base_v2/apps/admin-web/vite.config.ts b/ai_knowledge_base_v2/apps/admin-web/vite.config.ts new file mode 100644 index 0000000..7f14175 --- /dev/null +++ b/ai_knowledge_base_v2/apps/admin-web/vite.config.ts @@ -0,0 +1,10 @@ +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [vue()], + server: { + host: "127.0.0.1", + port: 5174, + }, +}); diff --git a/ai_knowledge_base_v2/apps/backend/.env.example b/ai_knowledge_base_v2/apps/backend/.env.example index 7e15518..1e2b96e 100644 --- a/ai_knowledge_base_v2/apps/backend/.env.example +++ b/ai_knowledge_base_v2/apps/backend/.env.example @@ -22,3 +22,7 @@ FEISHU_RETRY_COUNT=2 DEFAULT_DAILY_CHAT_LIMIT=100 DEFAULT_USER_NAME_PREFIX=用户 + +BOOTSTRAP_ADMIN_USERNAME=admin +BOOTSTRAP_ADMIN_PASSWORD=admin123456 +BOOTSTRAP_ADMIN_NAME=系统管理员 diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_auth.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_auth.py new file mode 100644 index 0000000..5c0e38c --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_auth.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.dependencies import get_current_admin +from app.core.responses import api_success +from app.models.admin import Admin +from app.schemas.admin import AdminLoginRequest, AdminLoginResponse, AdminRead +from app.services.admin_service import AdminAuthService + +router = APIRouter() + + +@router.post("/login") +def login(payload: AdminLoginRequest, db: Session = Depends(get_db)) -> dict: + result = AdminAuthService.login(db, payload.username, payload.password) + return api_success(AdminLoginResponse.model_validate(result).model_dump(mode="json")) + + +@router.get("/profile") +def profile(current_admin: Admin = Depends(get_current_admin)) -> dict: + return api_success(AdminRead.model_validate(current_admin).model_dump()) diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_dashboard.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_dashboard.py new file mode 100644 index 0000000..cdba2b1 --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_dashboard.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.dependencies import get_current_admin +from app.core.responses import api_success +from app.models.admin import Admin +from app.schemas.admin import DashboardStats +from app.services.admin_service import AdminDashboardService + +router = APIRouter() + + +@router.get("/dashboard") +def dashboard( + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + stats = AdminDashboardService.stats(db) + return api_success(DashboardStats.model_validate(stats).model_dump()) diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py new file mode 100644 index 0000000..b4187ed --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_knowledge.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.dependencies import get_current_admin +from app.core.responses import api_success +from app.models.admin import Admin +from app.models.knowledge import Knowledge +from app.schemas.admin import KnowledgeSaveRequest +from app.services.admin_service import OperationLogService + +router = APIRouter() + + +@router.get("/knowledge/list") +def list_knowledge(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict: + items = db.scalars(select(Knowledge).order_by(Knowledge.id.desc())).all() + return api_success([_knowledge_dict(item) for item in items]) + + +@router.post("/knowledge") +def create_knowledge( + payload: KnowledgeSaveRequest, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + knowledge = Knowledge( + name=payload.name, + feishu_space_id=payload.feishuSpaceId, + feishu_node_id=payload.feishuNodeId, + status=payload.status, + remark=payload.remark, + ) + db.add(knowledge) + db.flush() + OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="create", target_id=knowledge.id) + db.commit() + db.refresh(knowledge) + return api_success(_knowledge_dict(knowledge)) + + +@router.put("/knowledge/{knowledge_id}") +def update_knowledge( + knowledge_id: int, + payload: KnowledgeSaveRequest, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + knowledge = _get_knowledge(db, knowledge_id) + knowledge.name = payload.name + knowledge.feishu_space_id = payload.feishuSpaceId + knowledge.feishu_node_id = payload.feishuNodeId + knowledge.status = payload.status + knowledge.remark = payload.remark + db.add(knowledge) + OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="update", target_id=knowledge.id) + db.commit() + db.refresh(knowledge) + return api_success(_knowledge_dict(knowledge)) + + +@router.delete("/knowledge/{knowledge_id}") +def delete_knowledge( + knowledge_id: int, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + knowledge = _get_knowledge(db, knowledge_id) + db.delete(knowledge) + OperationLogService.write(db, admin_id=current_admin.id, module="knowledge", action="delete", target_id=knowledge.id) + db.commit() + return api_success() + + +def _get_knowledge(db: Session, knowledge_id: int) -> Knowledge: + knowledge = db.get(Knowledge, knowledge_id) + if knowledge is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="知识库不存在") + return knowledge + + +def _knowledge_dict(knowledge: Knowledge) -> dict: + return { + "id": knowledge.id, + "name": knowledge.name, + "feishuSpaceId": knowledge.feishu_space_id, + "feishuNodeId": knowledge.feishu_node_id, + "status": knowledge.status, + "remark": knowledge.remark, + "createdAt": knowledge.created_at, + "updatedAt": knowledge.updated_at, + } diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_records.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_records.py new file mode 100644 index 0000000..88551df --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_records.py @@ -0,0 +1,118 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, Query +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.dependencies import get_current_admin +from app.core.responses import api_success +from app.models.admin import Admin +from app.models.chat import ChatMessage, ChatSession +from app.models.logs import AiRequestLog, OperationLog + +router = APIRouter() + + +@router.get("/chat/list") +def chat_list( + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + sessions = db.scalars( + select(ChatSession) + .where(ChatSession.is_deleted == 0) + .order_by(ChatSession.updated_at.desc()) + .limit(200) + ).all() + return api_success( + [ + { + "id": item.id, + "userId": item.user_id, + "title": item.title, + "messageCount": item.message_count, + "lastMessageAt": item.last_message_at, + "updatedAt": item.updated_at, + } + for item in sessions + ] + ) + + +@router.get("/chat/{session_id}") +def chat_detail( + session_id: int, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + messages = db.scalars( + select(ChatMessage) + .where(ChatMessage.session_id == session_id) + .order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc()) + ).all() + return api_success( + [ + { + "id": item.id, + "role": item.role, + "content": item.content, + "messageStatus": item.message_status, + "tokenInput": item.token_input, + "tokenOutput": item.token_output, + "responseTimeMs": item.response_time_ms, + "createdAt": item.created_at, + } + for item in messages + ] + ) + + +@router.get("/log/list") +def operation_logs( + module: str = Query(default=""), + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + query = select(OperationLog).order_by(OperationLog.created_at.desc()).limit(200) + if module: + query = query.where(OperationLog.module == module) + logs = db.scalars(query).all() + return api_success( + [ + { + "id": item.id, + "adminId": item.admin_id, + "module": item.module, + "action": item.action, + "targetId": item.target_id, + "result": item.result, + "createdAt": item.created_at, + } + for item in logs + ] + ) + + +@router.get("/ai-log/list") +def ai_logs(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict: + logs = db.scalars(select(AiRequestLog).order_by(AiRequestLog.created_at.desc()).limit(200)).all() + return api_success( + [ + { + "id": item.id, + "sessionId": item.session_id, + "messageId": item.message_id, + "userId": item.user_id, + "modelName": item.model_name, + "knowledgeIds": item.knowledge_ids, + "retrieveCount": item.retrieve_count, + "totalToken": item.total_token, + "costMs": item.cost_ms, + "status": item.status, + "errorMessage": item.error_message, + "createdAt": item.created_at, + } + for item in logs + ] + ) diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py new file mode 100644 index 0000000..c35687b --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_settings.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.dependencies import get_current_admin +from app.core.responses import api_success +from app.models.admin import Admin +from app.models.ai_config import ModelConfig, Prompt, SystemConfig +from app.schemas.admin import EnableModelRequest, ModelSaveRequest, PromptSaveRequest, SystemConfigSaveRequest +from app.services.admin_service import OperationLogService + +router = APIRouter() + +DEFAULT_PROMPT = "你是企业飞书知识库 AI 助手。你只能基于提供的知识片段回答,不能编造。" + + +@router.get("/prompt") +def get_prompt(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict: + prompt = db.scalar(select(Prompt).order_by(Prompt.updated_at.desc(), Prompt.id.desc()).limit(1)) + return api_success({"promptContent": prompt.prompt_content if prompt else DEFAULT_PROMPT}) + + +@router.put("/prompt") +def save_prompt( + payload: PromptSaveRequest, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + prompt = Prompt(prompt_content=payload.promptContent, updated_by=current_admin.id) + db.add(prompt) + db.flush() + OperationLogService.write(db, admin_id=current_admin.id, module="prompt", action="save", target_id=prompt.id) + db.commit() + return api_success({"promptContent": prompt.prompt_content}) + + +@router.post("/prompt/reset") +def reset_prompt(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict: + prompt = Prompt(prompt_content=DEFAULT_PROMPT, updated_by=current_admin.id) + db.add(prompt) + db.flush() + OperationLogService.write(db, admin_id=current_admin.id, module="prompt", action="reset", target_id=prompt.id) + db.commit() + return api_success({"promptContent": prompt.prompt_content}) + + +@router.get("/model/list") +def list_models(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict: + models = db.scalars(select(ModelConfig).order_by(ModelConfig.id.desc())).all() + return api_success([_model_dict(model) for model in models]) + + +@router.post("/model") +def create_model( + payload: ModelSaveRequest, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + model = ModelConfig( + provider=payload.provider, + model_name=payload.modelName, + api_url=payload.apiUrl, + api_key=payload.apiKey, + temperature=payload.temperature, + max_token=payload.maxToken, + timeout_second=payload.timeoutSecond, + enabled=0, + ) + db.add(model) + db.flush() + OperationLogService.write(db, admin_id=current_admin.id, module="model", action="create", target_id=model.id) + db.commit() + db.refresh(model) + return api_success(_model_dict(model)) + + +@router.post("/model/enable") +def enable_model( + payload: EnableModelRequest, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + target = db.get(ModelConfig, payload.modelId) + if target is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在") + for model in db.scalars(select(ModelConfig)).all(): + model.enabled = 1 if model.id == target.id else 0 + db.add(model) + OperationLogService.write(db, admin_id=current_admin.id, module="model", action="enable", target_id=target.id) + db.commit() + return api_success() + + +@router.delete("/model/{model_id}") +def delete_model( + model_id: int, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + model = db.get(ModelConfig, model_id) + if model is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="模型不存在") + db.delete(model) + OperationLogService.write(db, admin_id=current_admin.id, module="model", action="delete", target_id=model.id) + db.commit() + return api_success() + + +@router.get("/config") +def list_config(db: Session = Depends(get_db), current_admin: Admin = Depends(get_current_admin)) -> dict: + configs = db.scalars(select(SystemConfig).order_by(SystemConfig.config_key.asc())).all() + return api_success([_config_dict(config) for config in configs]) + + +@router.put("/config") +def save_config( + payload: SystemConfigSaveRequest, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + config = db.scalar(select(SystemConfig).where(SystemConfig.config_key == payload.configKey)) + if config is None: + config = SystemConfig(config_key=payload.configKey, config_value=payload.configValue) + config.config_value = payload.configValue + config.description = payload.description + config.updated_by = current_admin.id + db.add(config) + db.flush() + OperationLogService.write(db, admin_id=current_admin.id, module="config", action="save", target_id=config.id) + db.commit() + db.refresh(config) + return api_success(_config_dict(config)) + + +def _model_dict(model: ModelConfig) -> dict: + return { + "id": model.id, + "provider": model.provider, + "modelName": model.model_name, + "apiUrl": model.api_url, + "apiKeyMasked": "******" if model.api_key else "", + "temperature": float(model.temperature) if model.temperature is not None else None, + "maxToken": model.max_token, + "timeoutSecond": model.timeout_second, + "enabled": model.enabled, + } + + +def _config_dict(config: SystemConfig) -> dict: + return { + "id": config.id, + "configKey": config.config_key, + "configValue": config.config_value, + "description": config.description, + "updatedAt": config.updated_at, + } diff --git a/ai_knowledge_base_v2/apps/backend/app/api/admin_users.py b/ai_knowledge_base_v2/apps/backend/app/api/admin_users.py new file mode 100644 index 0000000..acc766f --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/api/admin_users.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +from fastapi import APIRouter, Depends, Query +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.dependencies import get_current_admin +from app.core.responses import api_success +from app.models.admin import Admin +from app.models.user import User +from app.schemas.admin import AdminUserUpdateRequest +from app.services.admin_service import OperationLogService + +router = APIRouter() + + +@router.get("/user/list") +def list_users( + keyword: str = Query(default=""), + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + query = select(User).where(User.is_deleted == 0).order_by(User.id.desc()) + if keyword: + like = f"%{keyword}%" + query = query.where((User.phone.like(like)) | (User.name.like(like))) + users = db.scalars(query.limit(200)).all() + return api_success([_user_dict(user) for user in users]) + + +@router.get("/user/{user_id}") +def user_detail( + user_id: int, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + user = _get_user(db, user_id) + return api_success(_user_dict(user)) + + +@router.put("/user/{user_id}") +def update_user( + user_id: int, + payload: AdminUserUpdateRequest, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + user = _get_user(db, user_id) + if payload.status is not None: + user.status = payload.status + if payload.dailyChatLimit is not None: + user.daily_chat_limit = payload.dailyChatLimit + if payload.expiredAt is not None: + user.expired_at = payload.expiredAt.replace(tzinfo=None) + db.add(user) + OperationLogService.write(db, admin_id=current_admin.id, module="user", action="update", target_id=user.id) + db.commit() + db.refresh(user) + return api_success(_user_dict(user)) + + +@router.delete("/user/{user_id}") +def delete_user( + user_id: int, + db: Session = Depends(get_db), + current_admin: Admin = Depends(get_current_admin), +) -> dict: + user = _get_user(db, user_id) + user.is_deleted = 1 + db.add(user) + OperationLogService.write(db, admin_id=current_admin.id, module="user", action="delete", target_id=user.id) + db.commit() + return api_success() + + +def _get_user(db: Session, user_id: int) -> User: + user = db.get(User, user_id) + if user is None or user.is_deleted: + from fastapi import HTTPException, status + + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") + return user + + +def _user_dict(user: User) -> dict: + return { + "id": user.id, + "phone": user.phone, + "name": user.name, + "nickname": user.nickname, + "status": user.status, + "dailyChatLimit": user.daily_chat_limit, + "dailyChatUsed": user.daily_chat_used, + "expiredAt": user.expired_at, + "lastLoginAt": user.last_login_at, + "createdAt": user.created_at, + } diff --git a/ai_knowledge_base_v2/apps/backend/app/api/router.py b/ai_knowledge_base_v2/apps/backend/app/api/router.py index 33d21d6..a325bce 100644 --- a/ai_knowledge_base_v2/apps/backend/app/api/router.py +++ b/ai_knowledge_base_v2/apps/backend/app/api/router.py @@ -2,10 +2,27 @@ from __future__ import annotations from fastapi import APIRouter -from app.api import auth, chat, health, user +from app.api import ( + admin_auth, + admin_dashboard, + admin_knowledge, + admin_records, + admin_settings, + admin_users, + auth, + chat, + health, + user, +) api_router = APIRouter() api_router.include_router(health.router, tags=["health"]) api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) api_router.include_router(user.router, prefix="/user", tags=["user"]) api_router.include_router(chat.router, prefix="/chat", tags=["chat"]) +api_router.include_router(admin_auth.router, prefix="/admin", tags=["admin"]) +api_router.include_router(admin_dashboard.router, prefix="/admin", tags=["admin"]) +api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"]) +api_router.include_router(admin_knowledge.router, prefix="/admin", tags=["admin"]) +api_router.include_router(admin_settings.router, prefix="/admin", tags=["admin"]) +api_router.include_router(admin_records.router, prefix="/admin", tags=["admin"]) diff --git a/ai_knowledge_base_v2/apps/backend/app/core/config.py b/ai_knowledge_base_v2/apps/backend/app/core/config.py index df44857..d33300f 100644 --- a/ai_knowledge_base_v2/apps/backend/app/core/config.py +++ b/ai_knowledge_base_v2/apps/backend/app/core/config.py @@ -41,6 +41,9 @@ class Settings(BaseSettings): default_daily_chat_limit: int = 100 default_user_name_prefix: str = "用户" + bootstrap_admin_username: str = "admin" + bootstrap_admin_password: str = "admin123456" + bootstrap_admin_name: str = "系统管理员" @lru_cache diff --git a/ai_knowledge_base_v2/apps/backend/app/core/dependencies.py b/ai_knowledge_base_v2/apps/backend/app/core/dependencies.py index e9fc539..03fb364 100644 --- a/ai_knowledge_base_v2/apps/backend/app/core/dependencies.py +++ b/ai_knowledge_base_v2/apps/backend/app/core/dependencies.py @@ -8,6 +8,7 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.core.security import decode_access_token +from app.models.admin import Admin from app.models.user import User from app.services.auth_service import AuthService @@ -40,3 +41,19 @@ def get_current_user( if user.expired_at and user.expired_at < now: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已过期") return user + + +def get_current_admin( + payload: dict = Depends(get_current_token_payload), + db: Session = Depends(get_db), +) -> Admin: + if payload.get("type") != "admin": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="当前凭证不是管理员登录态") + + admin_id = payload.get("sub") + admin = db.get(Admin, int(admin_id)) if admin_id else None + if admin is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="管理员不存在") + if admin.status != 1: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="管理员已禁用") + return admin diff --git a/ai_knowledge_base_v2/apps/backend/app/core/security.py b/ai_knowledge_base_v2/apps/backend/app/core/security.py index eb278af..d53d72b 100644 --- a/ai_knowledge_base_v2/apps/backend/app/core/security.py +++ b/ai_knowledge_base_v2/apps/backend/app/core/security.py @@ -1,6 +1,9 @@ from __future__ import annotations from datetime import UTC, datetime, timedelta +import hashlib +import hmac +import os from uuid import uuid4 import jwt @@ -31,3 +34,20 @@ def decode_access_token(token: str) -> dict: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="登录已过期") from exc except jwt.InvalidTokenError as exc: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效登录凭证") from exc + + +def hash_password(password: str) -> str: + salt = os.urandom(16).hex() + digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), 120_000) + return f"pbkdf2_sha256${salt}${digest.hex()}" + + +def verify_password(password: str, password_hash: str) -> bool: + try: + algorithm, salt, expected = password_hash.split("$", 2) + except ValueError: + return False + if algorithm != "pbkdf2_sha256": + return False + digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt.encode("utf-8"), 120_000) + return hmac.compare_digest(digest.hex(), expected) diff --git a/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py b/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py new file mode 100644 index 0000000..2c006d0 --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/schemas/admin.py @@ -0,0 +1,71 @@ +from __future__ import annotations + +from datetime import datetime + +from pydantic import BaseModel, Field + +from app.schemas.common import ORMModel + + +class AdminLoginRequest(BaseModel): + username: str = Field(min_length=1, max_length=50) + password: str = Field(min_length=1, max_length=100) + + +class AdminLoginResponse(BaseModel): + token: str + expiredAt: datetime + admin: dict + + +class AdminRead(ORMModel): + id: int + username: str + name: str + status: int + + +class DashboardStats(BaseModel): + userCount: int + sessionCount: int + messageCount: int + aiRequestCount: int + knowledgeCount: int + + +class AdminUserUpdateRequest(BaseModel): + status: int | None = Field(default=None, ge=0, le=1) + dailyChatLimit: int | None = Field(default=None, ge=0, le=100000) + expiredAt: datetime | None = None + + +class KnowledgeSaveRequest(BaseModel): + name: str = Field(min_length=1, max_length=100) + feishuSpaceId: str = Field(min_length=1, max_length=100) + feishuNodeId: str = Field(min_length=1, max_length=100) + status: int = Field(default=1, ge=0, le=1) + remark: str | None = Field(default=None, max_length=255) + + +class PromptSaveRequest(BaseModel): + promptContent: str = Field(min_length=1) + + +class ModelSaveRequest(BaseModel): + provider: str = Field(min_length=1, max_length=50) + modelName: str = Field(min_length=1, max_length=100) + apiUrl: str = Field(min_length=1, max_length=255) + apiKey: str = Field(min_length=1) + temperature: float | None = Field(default=0.2, ge=0, le=2) + maxToken: int | None = Field(default=1024, ge=1, le=100000) + timeoutSecond: int = Field(default=30, ge=1, le=300) + + +class EnableModelRequest(BaseModel): + modelId: int + + +class SystemConfigSaveRequest(BaseModel): + configKey: str = Field(min_length=1, max_length=100) + configValue: str + description: str | None = Field(default=None, max_length=255) diff --git a/ai_knowledge_base_v2/apps/backend/app/services/admin_service.py b/ai_knowledge_base_v2/apps/backend/app/services/admin_service.py new file mode 100644 index 0000000..b9c4385 --- /dev/null +++ b/ai_knowledge_base_v2/apps/backend/app/services/admin_service.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +from datetime import UTC, datetime + +from fastapi import HTTPException, status +from sqlalchemy import func, select +from sqlalchemy.orm import Session + +from app.core.config import get_settings +from app.core.security import create_access_token, hash_password, verify_password +from app.models.admin import Admin +from app.models.chat import ChatMessage, ChatSession +from app.models.knowledge import Knowledge +from app.models.logs import AiRequestLog, OperationLog +from app.models.user import User + + +class AdminAuthService: + @classmethod + def login(cls, db: Session, username: str, password: str) -> dict: + cls.ensure_bootstrap_admin(db) + admin = db.scalar(select(Admin).where(Admin.username == username)) + if admin is None or not verify_password(password, admin.password): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="管理员账号或密码错误") + if admin.status != 1: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="管理员已禁用") + + admin.last_login_at = datetime.now(UTC).replace(tzinfo=None) + db.add(admin) + db.commit() + db.refresh(admin) + + token, expired_at = create_access_token(str(admin.id), "admin") + return {"token": token, "expiredAt": expired_at, "admin": admin} + + @staticmethod + def ensure_bootstrap_admin(db: Session) -> None: + has_admin = db.scalar(select(func.count(Admin.id))) or 0 + if has_admin: + return + settings = get_settings() + admin = Admin( + username=settings.bootstrap_admin_username, + password=hash_password(settings.bootstrap_admin_password), + name=settings.bootstrap_admin_name, + status=1, + ) + db.add(admin) + db.commit() + + +class AdminDashboardService: + @staticmethod + def stats(db: Session) -> dict: + return { + "userCount": db.scalar(select(func.count(User.id)).where(User.is_deleted == 0)) or 0, + "sessionCount": db.scalar(select(func.count(ChatSession.id)).where(ChatSession.is_deleted == 0)) or 0, + "messageCount": db.scalar(select(func.count(ChatMessage.id))) or 0, + "aiRequestCount": db.scalar(select(func.count(AiRequestLog.id))) or 0, + "knowledgeCount": db.scalar(select(func.count(Knowledge.id))) or 0, + } + + +class OperationLogService: + @staticmethod + def write( + db: Session, + *, + admin_id: int | None, + module: str, + action: str, + target_id: int | None = None, + result: str = "SUCCESS", + ) -> None: + db.add( + OperationLog( + admin_id=admin_id, + module=module, + action=action, + target_id=target_id, + result=result, + ) + ) diff --git a/ai_knowledge_base_v2/development_records/2026-07-06-decision-register.md b/ai_knowledge_base_v2/development_records/2026-07-06-decision-register.md index 819964d..e9e6676 100644 --- a/ai_knowledge_base_v2/development_records/2026-07-06-decision-register.md +++ b/ai_knowledge_base_v2/development_records/2026-07-06-decision-register.md @@ -33,6 +33,8 @@ | D-010 | 用户端停止生成先使用浏览器 `AbortController` 中断 SSE 读取,后端真实中断等接入真实模型时实现。 | 默认采用 | 当前后端仍是 mock 模型,前端先保证用户操作反馈真实有效。 | | D-011 | 阶段四外部服务默认保持 mock 开启,通过配置关闭 mock 后再走真实 provider。 | 默认采用 | 当前缺少真实飞书检索地址和模型凭证,默认 mock 可以保证开发、演示和测试连续。 | | D-012 | 大模型真实调用先按 OpenAI 兼容 `chat/completions` 非流式响应接入。 | 默认采用 | 先打通配置化真实模型调用和失败日志,后续再升级为模型原生流式透传。 | +| D-013 | 管理后台开发阶段支持首次登录自动创建默认管理员。 | 默认采用 | 降低本地初始化成本;生产环境必须修改默认账号密码。 | +| D-014 | 管理后台 Element Plus 先整体引入,后续再做按需导入优化。 | 默认采用 | 阶段五优先保证后台功能闭环和交付速度,体积优化后续处理。 | ## 待确认决策 diff --git a/ai_knowledge_base_v2/development_records/2026-07-06-phase5-admin-console.md b/ai_knowledge_base_v2/development_records/2026-07-06-phase5-admin-console.md new file mode 100644 index 0000000..231cc6e --- /dev/null +++ b/ai_knowledge_base_v2/development_records/2026-07-06-phase5-admin-console.md @@ -0,0 +1,84 @@ +# 阶段五记录:管理后台 + +日期:2026-07-06 + +## 本次目标 + +补齐一期后台管理能力的第一版,让系统具备基础运营、配置和审计入口。 + +## 已完成后端能力 + +- 管理员登录: + - 新增 `/api/admin/login` + - 新增 `/api/admin/profile` + - 开发阶段支持首次登录自动创建默认管理员。 +- Dashboard: + - 用户数、会话数、消息数、AI 请求数、知识库数。 +- 用户管理: + - 用户列表 + - 用户详情 + - 修改状态、每日额度、过期时间 + - 逻辑删除 +- 知识库管理: + - 列表 + - 新增 + - 编辑 + - 删除 +- Prompt 管理: + - 查看当前 Prompt + - 保存 Prompt + - 恢复默认 +- 模型管理: + - 列表 + - 新增模型 + - 启用模型 + - 删除模型 +- 系统配置: + - 列表 + - 新增或更新配置项 +- 记录审计: + - 聊天会话列表 + - 聊天详情 + - AI 请求日志 + - 操作日志 + +## 已完成前端能力 + +- 新增 `apps/admin-web`。 +- 技术栈:Vue 3 + Vite + TypeScript + Element Plus。 +- 页面包括: + - 登录页 + - Dashboard + - 用户管理 + - 知识库管理 + - Prompt 管理 + - 模型管理 + - 系统配置 + - 记录审计 +- 已接入阶段五后端管理接口。 + +## 默认管理员 + +```text +账号:admin +密码:admin123456 +``` + +该账号只用于开发阶段,生产环境必须通过环境变量修改。 + +## 当前边界 + +- 管理后台是第一版可用骨架,表单校验、批量操作、导出文件和更细权限还需要后续增强。 +- 当前管理员权限尚未按角色矩阵细分,默认登录管理员拥有全部后台能力。 +- 模型 API Key 当前新增时写入数据库,页面列表只展示脱敏值;生产环境还需要进一步做加密或密钥托管。 +- Element Plus 当前整体引入,构建有体积提示;阶段五先保证功能闭环,后续可做按需导入优化。 + +## 阶段五判断 + +阶段五已经具备管理后台主链路: + +1. 管理员可以登录后台。 +2. 后台可以查看核心统计。 +3. 后台可以维护用户、知识库、Prompt、模型和系统配置。 +4. 后台可以查看聊天、AI 请求和操作日志。 +5. 关键管理动作会写操作日志。 diff --git a/ai_knowledge_base_v2/development_records/README.md b/ai_knowledge_base_v2/development_records/README.md index 9e57981..c64f027 100644 --- a/ai_knowledge_base_v2/development_records/README.md +++ b/ai_knowledge_base_v2/development_records/README.md @@ -31,3 +31,4 @@ | `2026-07-06-phase2-rag-skeleton.md` | 阶段二 RAG 问答链路骨架记录。 | | `2026-07-06-phase3-user-client-completion.md` | 阶段三用户端 H5 主链路补齐记录。 | | `2026-07-06-phase4-rag-external-services.md` | 阶段四 RAG 和外部服务接入骨架记录。 | +| `2026-07-06-phase5-admin-console.md` | 阶段五管理后台记录。 | diff --git a/ai_knowledge_base_v2/docker-compose.dev.yml b/ai_knowledge_base_v2/docker-compose.dev.yml index 92d31cf..df4c0ce 100644 --- a/ai_knowledge_base_v2/docker-compose.dev.yml +++ b/ai_knowledge_base_v2/docker-compose.dev.yml @@ -53,5 +53,16 @@ services: depends_on: - backend + admin-web: + build: + context: ./apps/admin-web + container_name: ai_kb_v2_admin_web + environment: + VITE_API_BASE_URL: http://127.0.0.1:8100/api + ports: + - "5174:5174" + depends_on: + - backend + volumes: ai_kb_v2_mysql_data: