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 @@
+
+
+
+
+
+ AI
+ AI 知识库后台
+ 用于维护用户、知识库、Prompt、模型和审计记录。
+
+
+
+
+
+
+
+ 登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dashboard
+
核心业务数据概览。
+
+
+
用户数{{ stats?.userCount ?? 0 }}
+
会话数{{ stats?.sessionCount ?? 0 }}
+
消息数{{ stats?.messageCount ?? 0 }}
+
AI 请求{{ stats?.aiRequestCount ?? 0 }}
+
知识库{{ stats?.knowledgeCount ?? 0 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
+
+ 知识库管理
维护飞书知识库 SpaceID 和 NodeID。
+
+
+
+
+ 新增
+
+
+
+
+
+
+
+
+
+
+
+
Prompt 管理
保存后会影响后续问答 Prompt 组装。
+
恢复默认
+
+
+ 保存 Prompt
+
+
+
+
+
+
+
+
+
+ 新增模型
+
+
+
+
+
+
+
+ 启用
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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: