From adc78a6b9b84a2bd5332e9bcb84d4b32bdb57555 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 01:22:30 +0800 Subject: [PATCH 01/31] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Docker=E5=AE=B9?= =?UTF-8?q?=E5=99=A8=E9=83=A8=E7=BD=B2=E9=85=8D=E7=BD=AE=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 ai-service 后端 Dockerfile (Python 3.11 + uv) - 添加 ai-service-admin 前端 Dockerfile (Node 20 + nginx) - 添加 docker-compose.yaml 编排文件 (含postgres/qdrant) - 添加 nginx.conf 前端反向代理配置 - 添加 .dockerignore 文件 - 添加 .env.example 环境变量示例 --- .env.example | 17 +++++++ ai-service-admin/.dockerignore | 19 ++++++++ ai-service-admin/Dockerfile | 22 +++++++++ ai-service-admin/nginx.conf | 28 +++++++++++ ai-service/.dockerignore | 53 ++++++++++++++++++++ ai-service/Dockerfile | 32 +++++++++++++ docker-compose.yaml | 88 ++++++++++++++++++++++++++++++++++ 7 files changed, 259 insertions(+) create mode 100644 .env.example create mode 100644 ai-service-admin/.dockerignore create mode 100644 ai-service-admin/Dockerfile create mode 100644 ai-service-admin/nginx.conf create mode 100644 ai-service/.dockerignore create mode 100644 ai-service/Dockerfile create mode 100644 docker-compose.yaml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b342015 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# AI Service Environment Variables +# Copy this file to .env and modify as needed + +# LLM Configuration +AI_SERVICE_LLM_PROVIDER=openai +AI_SERVICE_LLM_API_KEY=your-api-key-here +AI_SERVICE_LLM_BASE_URL=https://api.openai.com/v1 +AI_SERVICE_LLM_MODEL=gpt-4o-mini + +# If using DeepSeek +# AI_SERVICE_LLM_PROVIDER=openai +# AI_SERVICE_LLM_API_KEY=your-deepseek-api-key +# AI_SERVICE_LLM_BASE_URL=https://api.deepseek.com/v1 +# AI_SERVICE_LLM_MODEL=deepseek-chat + +# Ollama Configuration (optional) +# AI_SERVICE_OLLAMA_BASE_URL=http://ollama:11434 diff --git a/ai-service-admin/.dockerignore b/ai-service-admin/.dockerignore new file mode 100644 index 0000000..2cde1db --- /dev/null +++ b/ai-service-admin/.dockerignore @@ -0,0 +1,19 @@ +node_modules +dist + +.env +.env.local +.env.*.local + +*.log + +.idea/ +.vscode/ +*.swp +*.swo + +.git +.gitignore + +*.md +!README.md diff --git a/ai-service-admin/Dockerfile b/ai-service-admin/Dockerfile new file mode 100644 index 0000000..5f5b1ba --- /dev/null +++ b/ai-service-admin/Dockerfile @@ -0,0 +1,22 @@ +# AI Service Admin Frontend Dockerfile +FROM node:20-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ + +RUN npm ci + +COPY . . + +RUN npm run build + +FROM nginx:alpine + +COPY --from=builder /app/dist /usr/share/nginx/html + +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/ai-service-admin/nginx.conf b/ai-service-admin/nginx.conf new file mode 100644 index 0000000..90a7824 --- /dev/null +++ b/ai-service-admin/nginx.conf @@ -0,0 +1,28 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://ai-service:8080/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 300s; + proxy_connect_timeout 75s; + } + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + gzip_min_length 1000; +} diff --git a/ai-service/.dockerignore b/ai-service/.dockerignore new file mode 100644 index 0000000..980a2b5 --- /dev/null +++ b/ai-service/.dockerignore @@ -0,0 +1,53 @@ +__pycache__ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +.pytest_cache +.coverage +htmlcov/ +.tox/ +.hypothesis/ + +.mypy_cache/ +.ruff_cache/ + +.env +.env.local +.env.*.local + +*.log +*.pot +*.pyc + +.idea/ +.vscode/ +*.swp +*.swo + +tests/ +scripts/ +*.md +!README.md + +.git +.gitignore +.gitea + +check_qdrant.py diff --git a/ai-service/Dockerfile b/ai-service/Dockerfile new file mode 100644 index 0000000..251933e --- /dev/null +++ b/ai-service/Dockerfile @@ -0,0 +1,32 @@ +# AI Service Backend Dockerfile +FROM python:3.11-slim AS builder + +WORKDIR /app + +RUN pip install --no-cache-dir uv + +COPY pyproject.toml . + +RUN uv pip install --system --no-cache-dir . + +FROM python:3.11-slim + +WORKDIR /app + +RUN groupadd -r appgroup && useradd -r -g appgroup appuser + +COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages +COPY --from=builder /usr/local/bin /usr/local/bin + +COPY app ./app + +RUN chown -R appuser:appgroup /app + +USER appuser + +EXPOSE 8080 + +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3db3ecb --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,88 @@ +version: '3.8' + +services: + ai-service: + build: + context: ./ai-service + dockerfile: Dockerfile + container_name: ai-service + restart: unless-stopped + ports: + - "8080:8080" + environment: + - AI_SERVICE_DEBUG=false + - AI_SERVICE_LOG_LEVEL=INFO + - AI_SERVICE_DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/ai_service + - AI_SERVICE_QDRANT_URL=http://qdrant:6333 + - AI_SERVICE_LLM_PROVIDER=${AI_SERVICE_LLM_PROVIDER:-openai} + - AI_SERVICE_LLM_API_KEY=${AI_SERVICE_LLM_API_KEY:-} + - AI_SERVICE_LLM_BASE_URL=${AI_SERVICE_LLM_BASE_URL:-https://api.openai.com/v1} + - AI_SERVICE_LLM_MODEL=${AI_SERVICE_LLM_MODEL:-gpt-4o-mini} + - AI_SERVICE_OLLAMA_BASE_URL=${AI_SERVICE_OLLAMA_BASE_URL:-http://ollama:11434} + depends_on: + postgres: + condition: service_healthy + qdrant: + condition: service_started + networks: + - ai-network + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + ai-service-admin: + build: + context: ./ai-service-admin + dockerfile: Dockerfile + container_name: ai-service-admin + restart: unless-stopped + ports: + - "3000:80" + depends_on: + - ai-service + networks: + - ai-network + + postgres: + image: postgres:15-alpine + container_name: ai-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=ai_service + volumes: + - postgres_data:/var/lib/postgresql/data + - ./ai-service/scripts/init_db.sql:/docker-entrypoint-initdb.d/init_db.sql:ro + ports: + - "5432:5432" + networks: + - ai-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d ai_service"] + interval: 10s + timeout: 5s + retries: 5 + + qdrant: + image: qdrant/qdrant:latest + container_name: ai-qdrant + restart: unless-stopped + ports: + - "6333:6333" + - "6334:6334" + volumes: + - qdrant_data:/qdrant/storage + networks: + - ai-network + +networks: + ai-network: + driver: bridge + +volumes: + postgres_data: + qdrant_data: -- 2.40.1 From 0b435a4b57aaf92d536330f827fb1ba256518615 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 01:24:40 +0800 Subject: [PATCH 02/31] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0README=EF=BC=8C?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0Docker=E9=83=A8=E7=BD=B2=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 211 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1b7672a..7a95a27 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,210 @@ -# ai-robot-core +# AI Robot Core -ai中台业务的能力支撑 \ No newline at end of file +AI中台业务的能力支撑,提供智能客服、RAG知识库检索、LLM对话等核心能力。 + +## 项目结构 + +``` +ai-robot-core/ +├── ai-service/ # Python 后端服务 +│ ├── app/ # FastAPI 应用 +│ ├── tests/ # 测试用例 +│ ├── Dockerfile # 后端镜像 +│ └── pyproject.toml # Python 依赖 +├── ai-service-admin/ # Vue 前端管理界面 +│ ├── src/ # Vue 源码 +│ ├── Dockerfile # 前端镜像 +│ ├── nginx.conf # Nginx 配置 +│ └── package.json # Node 依赖 +├── docker-compose.yaml # 容器编排 +├── .env.example # 环境变量示例 +└── README.md +``` + +## 功能特性 + +- **多租户支持**: 通过 X-Tenant-Id 头实现租户隔离 +- **RAG 知识库**: 基于 Qdrant 的向量检索增强生成 +- **LLM 集成**: 支持 OpenAI、DeepSeek、Ollama 等多种 LLM 提供商 +- **SSE 流式输出**: 支持 Server-Sent Events 实时响应 +- **置信度评估**: 自动评估回复质量,低置信度时建议转人工 + +## 快速开始 + +### 环境要求 + +- Docker 20.10+ +- Docker Compose 2.0+ + +### 部署步骤 + +#### 1. 克隆代码 + +```bash +git clone http://49.232.209.156:3005/MerCry/ai-robot-core.git +cd ai-robot-core +``` + +#### 2. 配置环境变量 + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件,配置 LLM API: + +```env +# OpenAI 配置 +AI_SERVICE_LLM_PROVIDER=openai +AI_SERVICE_LLM_API_KEY=your-openai-api-key +AI_SERVICE_LLM_BASE_URL=https://api.openai.com/v1 +AI_SERVICE_LLM_MODEL=gpt-4o-mini + +# 或使用 DeepSeek +# AI_SERVICE_LLM_PROVIDER=openai +# AI_SERVICE_LLM_API_KEY=your-deepseek-api-key +# AI_SERVICE_LLM_BASE_URL=https://api.deepseek.com/v1 +# AI_SERVICE_LLM_MODEL=deepseek-chat +``` + +#### 3. 启动服务 + +```bash +docker-compose up -d --build +``` + +#### 4. 验证服务 + +```bash +# 检查服务状态 +docker-compose ps + +# 查看后端日志 +docker-compose logs -f ai-service +``` + +#### 5. 访问服务 + +| 服务 | 地址 | 说明 | +|------|------|------| +| 前端管理界面 | http://服务器IP:3000 | Vue 管理后台 | +| 后端 API | http://服务器IP:8080 | FastAPI 服务 | +| API 文档 | http://服务器IP:8080/docs | Swagger UI | +| Qdrant 控制台 | http://服务器IP:6333/dashboard | 向量数据库管理 | + +## 服务架构 + +``` +┌─────────────────────────────────────────────────────────┐ +│ 用户访问 │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ ai-service-admin (端口3000) │ +│ - Nginx 静态文件服务 │ +│ - 反向代理 /api/* → ai-service:8080 │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ ai-service (端口8080) │ +│ - FastAPI 后端服务 │ +│ - RAG / LLM / 知识库管理 │ +└─────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ +│ PostgreSQL │ │ Qdrant │ +│ (端口5432) │ │ (端口6333) │ +│ - 会话存储 │ │ - 向量存储 │ +│ - 知识库元数据 │ │ - 文档索引 │ +└──────────────────┘ └──────────────────┘ +``` + +## 常用命令 + +```bash +# 启动所有服务 +docker-compose up -d + +# 重新构建并启动 +docker-compose up -d --build + +# 查看服务状态 +docker-compose ps + +# 查看日志 +docker-compose logs -f ai-service +docker-compose logs -f ai-service-admin + +# 重启服务 +docker-compose restart ai-service + +# 停止所有服务 +docker-compose down + +# 停止并删除数据卷(清空数据) +docker-compose down -v +``` + +## 本地开发 + +### 后端开发 + +```bash +cd ai-service + +# 创建虚拟环境 +python -m venv .venv +source .venv/bin/activate # Linux/Mac +# .venv\Scripts\activate # Windows + +# 安装依赖 +pip install -e ".[dev]" + +# 启动开发服务器 +uvicorn app.main:app --reload --port 8000 +``` + +### 前端开发 + +```bash +cd ai-service-admin + +# 安装依赖 +npm install + +# 启动开发服务器 +npm run dev +``` + +## API 接口 + +### 核心接口 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/ai/chat` | POST | AI 对话接口 | +| `/admin/kb` | GET/POST | 知识库管理 | +| `/admin/rag/experiments/run` | POST | RAG 实验室 | +| `/admin/llm/config` | GET/PUT | LLM 配置 | +| `/admin/embedding/config` | GET/PUT | 嵌入模型配置 | + +详细 API 文档请访问 http://服务器IP:8080/docs + +## 环境变量说明 + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `AI_SERVICE_LLM_PROVIDER` | openai | LLM 提供商 | +| `AI_SERVICE_LLM_API_KEY` | - | API 密钥 | +| `AI_SERVICE_LLM_BASE_URL` | https://api.openai.com/v1 | API 地址 | +| `AI_SERVICE_LLM_MODEL` | gpt-4o-mini | 模型名称 | +| `AI_SERVICE_DATABASE_URL` | postgresql+asyncpg://... | 数据库连接 | +| `AI_SERVICE_QDRANT_URL` | http://qdrant:6333 | Qdrant 地址 | +| `AI_SERVICE_LOG_LEVEL` | INFO | 日志级别 | + +## License + +MIT -- 2.40.1 From f2d29dc2c40f895e096d869c7f9473a4f47f7dec Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 01:28:08 +0800 Subject: [PATCH 03/31] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Ollama=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=94=AF=E6=8C=81=E5=B5=8C=E5=85=A5=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docker-compose.yaml 添加 Ollama 容器 - 更新 .env.example 添加 Ollama 配置 - README 添加模型拉取步骤和架构图更新 --- .env.example | 5 +++-- README.md | 30 ++++++++++++++++++++---------- docker-compose.yaml | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index b342015..22de4c4 100644 --- a/.env.example +++ b/.env.example @@ -13,5 +13,6 @@ AI_SERVICE_LLM_MODEL=gpt-4o-mini # AI_SERVICE_LLM_BASE_URL=https://api.deepseek.com/v1 # AI_SERVICE_LLM_MODEL=deepseek-chat -# Ollama Configuration (optional) -# AI_SERVICE_OLLAMA_BASE_URL=http://ollama:11434 +# Ollama Configuration (for embedding model) +AI_SERVICE_OLLAMA_BASE_URL=http://ollama:11434 +AI_SERVICE_OLLAMA_EMBEDDING_MODEL=nomic-embed-text diff --git a/README.md b/README.md index 7a95a27..eb438e2 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,16 @@ AI_SERVICE_LLM_MODEL=gpt-4o-mini docker-compose up -d --build ``` -#### 4. 验证服务 +#### 4. 拉取嵌入模型 + +服务启动后,需要在 Ollama 容器中拉取 nomic-embed-text 模型: + +```bash +# 进入 Ollama 容器拉取模型 +docker exec -it ai-ollama ollama pull nomic-embed-text +``` + +#### 5. 验证服务 ```bash # 检查服务状态 @@ -83,7 +92,7 @@ docker-compose ps docker-compose logs -f ai-service ``` -#### 5. 访问服务 +#### 6. 访问服务 | 服务 | 地址 | 说明 | |------|------|------| @@ -91,6 +100,7 @@ docker-compose logs -f ai-service | 后端 API | http://服务器IP:8080 | FastAPI 服务 | | API 文档 | http://服务器IP:8080/docs | Swagger UI | | Qdrant 控制台 | http://服务器IP:6333/dashboard | 向量数据库管理 | +| Ollama API | http://服务器IP:11434 | 嵌入模型服务 | ## 服务架构 @@ -112,14 +122,14 @@ docker-compose logs -f ai-service │ - FastAPI 后端服务 │ │ - RAG / LLM / 知识库管理 │ └─────────────────────────────────────────────────────────┘ - │ │ - ▼ ▼ -┌──────────────────┐ ┌──────────────────┐ -│ PostgreSQL │ │ Qdrant │ -│ (端口5432) │ │ (端口6333) │ -│ - 会话存储 │ │ - 向量存储 │ -│ - 知识库元数据 │ │ - 文档索引 │ -└──────────────────┘ └──────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ PostgreSQL │ │ Qdrant │ │ Ollama │ +│ (端口5432) │ │ (端口6333) │ │ (端口11434) │ +│ - 会话存储 │ │ - 向量存储 │ │ - nomic-embed │ +│ - 知识库元数据 │ │ - 文档索引 │ │ - 嵌入模型 │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ ``` ## 常用命令 diff --git a/docker-compose.yaml b/docker-compose.yaml index 3db3ecb..5340985 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -79,6 +79,21 @@ services: networks: - ai-network + ollama: + image: ollama/ollama:latest + container_name: ai-ollama + restart: unless-stopped + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + networks: + - ai-network + deploy: + resources: + reservations: + memory: 1G + networks: ai-network: driver: bridge @@ -86,3 +101,4 @@ networks: volumes: postgres_data: qdrant_data: + ollama_data: -- 2.40.1 From d8f440077aa545d47df5c95d9f0882ac324d0e48 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 01:39:46 +0800 Subject: [PATCH 04/31] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3DeepSeek?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=A4=BA=E4=BE=8B=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?Docker=20Compose=E8=AF=AD=E6=B3=95=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DeepSeek使用deepseek作为provider而非openai - 更新所有docker-compose命令为docker compose (V2语法) --- .env.example | 5 ++--- README.md | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.env.example b/.env.example index 22de4c4..db110b4 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,15 @@ # AI Service Environment Variables # Copy this file to .env and modify as needed -# LLM Configuration +# LLM Configuration (OpenAI) AI_SERVICE_LLM_PROVIDER=openai AI_SERVICE_LLM_API_KEY=your-api-key-here AI_SERVICE_LLM_BASE_URL=https://api.openai.com/v1 AI_SERVICE_LLM_MODEL=gpt-4o-mini # If using DeepSeek -# AI_SERVICE_LLM_PROVIDER=openai +# AI_SERVICE_LLM_PROVIDER=deepseek # AI_SERVICE_LLM_API_KEY=your-deepseek-api-key -# AI_SERVICE_LLM_BASE_URL=https://api.deepseek.com/v1 # AI_SERVICE_LLM_MODEL=deepseek-chat # Ollama Configuration (for embedding model) diff --git a/README.md b/README.md index eb438e2..a16662e 100644 --- a/README.md +++ b/README.md @@ -61,15 +61,18 @@ AI_SERVICE_LLM_BASE_URL=https://api.openai.com/v1 AI_SERVICE_LLM_MODEL=gpt-4o-mini # 或使用 DeepSeek -# AI_SERVICE_LLM_PROVIDER=openai +# AI_SERVICE_LLM_PROVIDER=deepseek # AI_SERVICE_LLM_API_KEY=your-deepseek-api-key -# AI_SERVICE_LLM_BASE_URL=https://api.deepseek.com/v1 # AI_SERVICE_LLM_MODEL=deepseek-chat ``` #### 3. 启动服务 ```bash +# Docker Compose V2 (推荐,Docker 内置) +docker compose up -d --build + +# 或 Docker Compose V1 (旧版,需要单独安装) docker-compose up -d --build ``` @@ -86,10 +89,10 @@ docker exec -it ai-ollama ollama pull nomic-embed-text ```bash # 检查服务状态 -docker-compose ps +docker compose ps # 查看后端日志 -docker-compose logs -f ai-service +docker compose logs -f ai-service ``` #### 6. 访问服务 @@ -136,26 +139,26 @@ docker-compose logs -f ai-service ```bash # 启动所有服务 -docker-compose up -d +docker compose up -d # 重新构建并启动 -docker-compose up -d --build +docker compose up -d --build # 查看服务状态 -docker-compose ps +docker compose ps # 查看日志 -docker-compose logs -f ai-service -docker-compose logs -f ai-service-admin +docker compose logs -f ai-service +docker compose logs -f ai-service-admin # 重启服务 -docker-compose restart ai-service +docker compose restart ai-service # 停止所有服务 -docker-compose down +docker compose down # 停止并删除数据卷(清空数据) -docker-compose down -v +docker compose down -v ``` ## 本地开发 -- 2.40.1 From b6218dec1a8e387e12cb1bd5a6e18b8fc02aea2e Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 01:42:43 +0800 Subject: [PATCH 05/31] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E7=AB=AF=E5=8F=A3=E4=B8=BA8181=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=AE=BF=E4=B8=BB=E6=9C=BANginx=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=A4=BA=E4=BE=8B=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 前端端口从3000改为8181(避免与常用端口冲突) - 添加 deploy/nginx.conf.example 宿主机Nginx配置示例 - 更新README添加Nginx配置说明 --- README.md | 24 ++++++++++++-- deploy/nginx.conf.example | 68 +++++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 2 +- 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 deploy/nginx.conf.example diff --git a/README.md b/README.md index a16662e..1f2571d 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,14 @@ docker compose logs -f ai-service | 服务 | 地址 | 说明 | |------|------|------| -| 前端管理界面 | http://服务器IP:3000 | Vue 管理后台 | +| 前端管理界面 | http://服务器IP:8181 | Vue 管理后台 | | 后端 API | http://服务器IP:8080 | FastAPI 服务 | | API 文档 | http://服务器IP:8080/docs | Swagger UI | | Qdrant 控制台 | http://服务器IP:6333/dashboard | 向量数据库管理 | | Ollama API | http://服务器IP:11434 | 嵌入模型服务 | +> **注意**: 如果宿主机 8080 端口已被占用,可以只通过前端管理界面 (8181) 访问,前端会自动代理后端 API 请求。 + ## 服务架构 ``` @@ -114,7 +116,7 @@ docker compose logs -f ai-service │ ▼ ┌─────────────────────────────────────────────────────────┐ -│ ai-service-admin (端口3000) │ +│ ai-service-admin (端口8181) │ │ - Nginx 静态文件服务 │ │ - 反向代理 /api/* → ai-service:8080 │ └─────────────────────────────────────────────────────────┘ @@ -161,6 +163,24 @@ docker compose down docker compose down -v ``` +## 宿主机 Nginx 配置(可选) + +如果需要通过宿主机 Nginx 统一管理入口(配置域名、SSL证书),可参考 `deploy/nginx.conf.example`: + +```bash +# 复制配置文件 +sudo cp deploy/nginx.conf.example /etc/nginx/conf.d/ai-service.conf + +# 修改配置中的域名 +sudo vim /etc/nginx/conf.d/ai-service.conf + +# 测试配置 +sudo nginx -t + +# 重载 Nginx +sudo nginx -s reload +``` + ## 本地开发 ### 后端开发 diff --git a/deploy/nginx.conf.example b/deploy/nginx.conf.example new file mode 100644 index 0000000..c4fda2f --- /dev/null +++ b/deploy/nginx.conf.example @@ -0,0 +1,68 @@ +# AI Service Nginx Configuration +# 将此文件放置于 /etc/nginx/conf.d/ai-service.conf +# 或 include 到主配置文件中 + +upstream ai_service_admin { + server 127.0.0.1:8181; +} + +server { + listen 80; + server_name your-domain.com; # 替换为你的域名或服务器IP + + # 访问日志 + access_log /var/log/nginx/ai-service.access.log; + error_log /var/log/nginx/ai-service.error.log; + + location / { + proxy_pass http://ai_service_admin; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # SSE 流式响应支持 + proxy_read_timeout 300s; + proxy_connect_timeout 75s; + proxy_buffering off; + } +} + +# HTTPS 配置示例 (使用 Let's Encrypt) +# server { +# listen 443 ssl http2; +# server_name your-domain.com; +# +# ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; +# +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; +# ssl_prefer_server_ciphers off; +# +# location / { +# proxy_pass http://ai_service_admin; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection 'upgrade'; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_cache_bypass $http_upgrade; +# proxy_read_timeout 300s; +# proxy_connect_timeout 75s; +# proxy_buffering off; +# } +# } + +# HTTP 重定向到 HTTPS +# server { +# listen 80; +# server_name your-domain.com; +# return 301 https://$server_name$request_uri; +# } diff --git a/docker-compose.yaml b/docker-compose.yaml index 5340985..eb6522b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -40,7 +40,7 @@ services: container_name: ai-service-admin restart: unless-stopped ports: - - "3000:80" + - "8181:80" depends_on: - ai-service networks: -- 2.40.1 From 7c8e4b6dc7ae9af672248f389c81c379d7b558a9 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 01:46:24 +0800 Subject: [PATCH 06/31] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=E6=98=A0=E5=B0=84=EF=BC=8C=E5=90=8E=E7=AB=AF8182?= =?UTF-8?q?=E4=BE=9BJava=E6=B8=A0=E9=81=93=E4=BE=A7=E8=B0=83=E7=94=A8=20[A?= =?UTF-8?q?C-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端端口从8080改为8182(宿主机映射) - 前端端口8181保持不变 - 更新宿主机Nginx配置示例,支持前后端分离代理 - 容器间通信使用内部网络,无需修改 --- README.md | 8 +++-- deploy/nginx.conf.example | 72 +++++++++++++++++++++++++++++++++++++-- docker-compose.yaml | 2 +- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1f2571d..73f9574 100644 --- a/README.md +++ b/README.md @@ -100,12 +100,14 @@ docker compose logs -f ai-service | 服务 | 地址 | 说明 | |------|------|------| | 前端管理界面 | http://服务器IP:8181 | Vue 管理后台 | -| 后端 API | http://服务器IP:8080 | FastAPI 服务 | -| API 文档 | http://服务器IP:8080/docs | Swagger UI | +| 后端 API | http://服务器IP:8182 | FastAPI 服务(Java渠道侧调用) | +| API 文档 | http://服务器IP:8182/docs | Swagger UI | | Qdrant 控制台 | http://服务器IP:6333/dashboard | 向量数据库管理 | | Ollama API | http://服务器IP:11434 | 嵌入模型服务 | -> **注意**: 如果宿主机 8080 端口已被占用,可以只通过前端管理界面 (8181) 访问,前端会自动代理后端 API 请求。 +> **端口说明**: +> - `8181`: 前端管理界面,内部代理后端 API +> - `8182`: 后端 API,供 Java 渠道侧直接调用 ## 服务架构 diff --git a/deploy/nginx.conf.example b/deploy/nginx.conf.example index c4fda2f..c67246e 100644 --- a/deploy/nginx.conf.example +++ b/deploy/nginx.conf.example @@ -2,17 +2,24 @@ # 将此文件放置于 /etc/nginx/conf.d/ai-service.conf # 或 include 到主配置文件中 +# 后端 API 上游(供 Java 渠道侧调用) +upstream ai_service_backend { + server 127.0.0.1:8182; +} + +# 前端管理界面上游 upstream ai_service_admin { server 127.0.0.1:8181; } +# 前端管理界面 server { listen 80; server_name your-domain.com; # 替换为你的域名或服务器IP # 访问日志 - access_log /var/log/nginx/ai-service.access.log; - error_log /var/log/nginx/ai-service.error.log; + access_log /var/log/nginx/ai-service-admin.access.log; + error_log /var/log/nginx/ai-service-admin.error.log; location / { proxy_pass http://ai_service_admin; @@ -32,7 +39,39 @@ server { } } +# 后端 API(供 Java 渠道侧调用) +# 如果使用域名,可以用不同的路径或子域名 +# 示例:api.your-domain.com 或 your-domain.com/api/ +server { + listen 80; + server_name api.your-domain.com; # 替换为 API 子域名 + + # 访问日志 + access_log /var/log/nginx/ai-service-api.access.log; + error_log /var/log/nginx/ai-service-api.error.log; + + location / { + proxy_pass http://ai_service_backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + + # SSE 流式响应支持 + proxy_read_timeout 300s; + proxy_connect_timeout 75s; + proxy_buffering off; + } +} + +# ============================================================ # HTTPS 配置示例 (使用 Let's Encrypt) +# ============================================================ + # server { # listen 443 ssl http2; # server_name your-domain.com; @@ -60,9 +99,36 @@ server { # } # } +# server { +# listen 443 ssl http2; +# server_name api.your-domain.com; +# +# ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; +# ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; +# +# ssl_protocols TLSv1.2 TLSv1.3; +# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; +# ssl_prefer_server_ciphers off; +# +# location / { +# proxy_pass http://ai_service_backend; +# proxy_http_version 1.1; +# proxy_set_header Upgrade $http_upgrade; +# proxy_set_header Connection 'upgrade'; +# proxy_set_header Host $host; +# proxy_set_header X-Real-IP $remote_addr; +# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +# proxy_set_header X-Forwarded-Proto $scheme; +# proxy_cache_bypass $http_upgrade; +# proxy_read_timeout 300s; +# proxy_connect_timeout 75s; +# proxy_buffering off; +# } +# } + # HTTP 重定向到 HTTPS # server { # listen 80; -# server_name your-domain.com; +# server_name your-domain.com api.your-domain.com; # return 301 https://$server_name$request_uri; # } diff --git a/docker-compose.yaml b/docker-compose.yaml index eb6522b..eb0947b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: container_name: ai-service restart: unless-stopped ports: - - "8080:8080" + - "8182:8080" environment: - AI_SERVICE_DEBUG=false - AI_SERVICE_LOG_LEVEL=INFO -- 2.40.1 From 366f38e17fab2b446b5e8b46c0bde3f664bdc23e Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:12:04 +0800 Subject: [PATCH 07/31] =?UTF-8?q?fix:=20=E4=BD=BF=E7=94=A8=E5=9B=BD?= =?UTF-8?q?=E5=86=85=E9=95=9C=E5=83=8F=E5=8A=A0=E9=80=9FDocker=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service-admin/Dockerfile | 4 ++-- ai-service/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ai-service-admin/Dockerfile b/ai-service-admin/Dockerfile index 5f5b1ba..fe22ad8 100644 --- a/ai-service-admin/Dockerfile +++ b/ai-service-admin/Dockerfile @@ -1,5 +1,5 @@ # AI Service Admin Frontend Dockerfile -FROM node:20-alpine AS builder +FROM docker.1ms.run/node:20-alpine AS builder WORKDIR /app @@ -11,7 +11,7 @@ COPY . . RUN npm run build -FROM nginx:alpine +FROM docker.1ms.run/nginx:alpine COPY --from=builder /app/dist /usr/share/nginx/html diff --git a/ai-service/Dockerfile b/ai-service/Dockerfile index 251933e..7620e5e 100644 --- a/ai-service/Dockerfile +++ b/ai-service/Dockerfile @@ -1,5 +1,5 @@ # AI Service Backend Dockerfile -FROM python:3.11-slim AS builder +FROM docker.1ms.run/python:3.11-slim AS builder WORKDIR /app @@ -9,7 +9,7 @@ COPY pyproject.toml . RUN uv pip install --system --no-cache-dir . -FROM python:3.11-slim +FROM docker.1ms.run/python:3.11-slim WORKDIR /app -- 2.40.1 From a3b7f2cc51ce5786ab95aea98c0640935f384697 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:13:26 +0800 Subject: [PATCH 08/31] =?UTF-8?q?fix:=20Docker=E6=9E=84=E5=BB=BA=E6=97=B6?= =?UTF-8?q?=E5=A4=8D=E5=88=B6README.md=E6=96=87=E4=BB=B6=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service/Dockerfile b/ai-service/Dockerfile index 7620e5e..52aab35 100644 --- a/ai-service/Dockerfile +++ b/ai-service/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app RUN pip install --no-cache-dir uv -COPY pyproject.toml . +COPY pyproject.toml README.md ./ RUN uv pip install --system --no-cache-dir . -- 2.40.1 From 5b3f5063a6a346a008e138a1911eff8e1c28c55d Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:15:13 +0800 Subject: [PATCH 09/31] =?UTF-8?q?fix:=20=E5=8D=87=E7=BA=A7vue-tsc=E5=88=B0?= =?UTF-8?q?v2=E8=A7=A3=E5=86=B3TypeScript=E5=85=BC=E5=AE=B9=E6=80=A7?= =?UTF-8?q?=E9=97=AE=E9=A2=98=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service-admin/package-lock.json | 1019 ++-------------------------- ai-service-admin/package.json | 4 +- 2 files changed, 73 insertions(+), 950 deletions(-) diff --git a/ai-service-admin/package-lock.json b/ai-service-admin/package-lock.json index 24bc60f..ff69db6 100644 --- a/ai-service-admin/package-lock.json +++ b/ai-service-admin/package-lock.json @@ -17,15 +17,13 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.4", - "typescript": "^5.2.2", + "typescript": "~5.6.0", "vite": "^5.1.4", - "vue-tsc": "^1.8.27" + "vue-tsc": "^2.1.0" } }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -33,8 +31,6 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -42,8 +38,6 @@ }, "node_modules/@babel/parser": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "license": "MIT", "dependencies": { "@babel/types": "^7.29.0" @@ -57,8 +51,6 @@ }, "node_modules/@babel/types": { "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -70,8 +62,6 @@ }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", - "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", "license": "MIT", "engines": { "node": ">=10" @@ -79,391 +69,13 @@ }, "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.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/win32-x64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -479,8 +91,6 @@ }, "node_modules/@floating-ui/core": { "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" @@ -488,8 +98,6 @@ }, "node_modules/@floating-ui/dom": { "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", "license": "MIT", "dependencies": { "@floating-ui/core": "^1.7.4", @@ -498,353 +106,23 @@ }, "node_modules/@floating-ui/utils": { "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "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/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rollup/rollup-win32-x64-gnu": { "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", "cpu": [ "x64" ], @@ -857,8 +135,6 @@ }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -871,21 +147,15 @@ }, "node_modules/@types/estree": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { "version": "4.17.24", - "resolved": "https://registry.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": "*" @@ -893,14 +163,10 @@ }, "node_modules/@types/web-bluetooth": { "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", - "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", "license": "MIT" }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", - "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", "dev": true, "license": "MIT", "engines": { @@ -912,40 +178,36 @@ } }, "node_modules/@volar/language-core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", - "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", "dev": true, "license": "MIT", "dependencies": { - "@volar/source-map": "1.11.1" + "@volar/source-map": "2.4.15" } }, "node_modules/@volar/source-map": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", - "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", "dev": true, - "license": "MIT", - "dependencies": { - "muggle-string": "^0.3.1" - } + "license": "MIT" }, "node_modules/@volar/typescript": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", - "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "1.11.1", - "path-browserify": "^1.0.1" + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", - "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -957,8 +219,6 @@ }, "node_modules/@vue/compiler-dom": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", - "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", "license": "MIT", "dependencies": { "@vue/compiler-core": "3.5.29", @@ -967,8 +227,6 @@ }, "node_modules/@vue/compiler-sfc": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", - "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", @@ -984,36 +242,42 @@ }, "node_modules/@vue/compiler-ssr": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", - "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", "license": "MIT", "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, - "node_modules/@vue/devtools-api": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", - "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", - "license": "MIT" - }, - "node_modules/@vue/language-core": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", - "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", "dev": true, "license": "MIT", "dependencies": { - "@volar/language-core": "~1.11.1", - "@volar/source-map": "~1.11.1", - "@vue/compiler-dom": "^3.3.0", - "@vue/shared": "^3.3.0", - "computeds": "^0.0.1", + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "license": "MIT" + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", "minimatch": "^9.0.3", - "muggle-string": "^0.3.1", - "path-browserify": "^1.0.1", - "vue-template-compiler": "^2.7.14" + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" @@ -1026,8 +290,6 @@ }, "node_modules/@vue/reactivity": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", - "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", "license": "MIT", "dependencies": { "@vue/shared": "3.5.29" @@ -1035,8 +297,6 @@ }, "node_modules/@vue/runtime-core": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", - "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", "license": "MIT", "dependencies": { "@vue/reactivity": "3.5.29", @@ -1045,8 +305,6 @@ }, "node_modules/@vue/runtime-dom": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", - "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", "license": "MIT", "dependencies": { "@vue/reactivity": "3.5.29", @@ -1057,8 +315,6 @@ }, "node_modules/@vue/server-renderer": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", - "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", "license": "MIT", "dependencies": { "@vue/compiler-ssr": "3.5.29", @@ -1070,14 +326,10 @@ }, "node_modules/@vue/shared": { "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", - "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "license": "MIT" }, "node_modules/@vueuse/core": { "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", - "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.20", @@ -1091,8 +343,6 @@ }, "node_modules/@vueuse/metadata": { "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", - "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/antfu" @@ -1100,8 +350,6 @@ }, "node_modules/@vueuse/shared": { "version": "10.11.1", - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", - "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", "license": "MIT", "dependencies": { "vue-demi": ">=0.14.8" @@ -1110,22 +358,23 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "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/asynckit": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, "node_modules/axios": { "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -1158,8 +407,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1171,8 +418,6 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -1181,23 +426,12 @@ "node": ">= 0.8" } }, - "node_modules/computeds": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", - "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", - "dev": true, - "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.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, "node_modules/de-indent": { @@ -1209,8 +443,6 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "license": "MIT", "engines": { "node": ">=0.4.0" @@ -1218,8 +450,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -1232,8 +462,6 @@ }, "node_modules/element-plus": { "version": "2.13.2", - "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.2.tgz", - "integrity": "sha512-Zjzm1NnFXGhV4LYZ6Ze9skPlYi2B4KAmN18FL63A3PZcjhDfroHwhtM6RE8BonlOPHXUnPQynH0BgaoEfvhrGw==", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^3.4.1", @@ -1257,8 +485,6 @@ }, "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" @@ -1269,8 +495,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -1278,8 +502,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -1287,8 +509,6 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -1299,8 +519,6 @@ }, "node_modules/es-set-tostringtag": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -1314,8 +532,6 @@ }, "node_modules/esbuild": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1353,14 +569,10 @@ }, "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/follow-redirects": { "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -1379,8 +591,6 @@ }, "node_modules/form-data": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -1393,25 +603,8 @@ "node": ">= 6" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1419,8 +612,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -1443,8 +634,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -1456,8 +645,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -1468,8 +655,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -1480,8 +665,6 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -1495,8 +678,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -1517,20 +698,14 @@ }, "node_modules/lodash": { "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", "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": "*", @@ -1540,8 +715,6 @@ }, "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" @@ -1549,8 +722,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -1558,14 +729,10 @@ }, "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/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -1573,8 +740,6 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -1584,9 +749,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", - "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.8.tgz", + "integrity": "sha512-reYkDYtj/b19TeqbNZCV4q9t+Yxylf/rYBsLb42SXJatTv4/ylq5lEiAmhA/IToxO7NI2UzNMghHoHuaqDkAjw==", "dev": true, "license": "ISC", "dependencies": { @@ -1600,16 +765,14 @@ } }, "node_modules/muggle-string": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", - "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "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.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -1626,8 +789,6 @@ }, "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": { @@ -1639,14 +800,10 @@ }, "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/pinia": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", - "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", "license": "MIT", "dependencies": { "@vue/devtools-api": "^6.6.3", @@ -1667,8 +824,6 @@ }, "node_modules/postcss": { "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -1695,14 +850,10 @@ }, "node_modules/proxy-from-env": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, "node_modules/rollup": { "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -1744,32 +895,17 @@ "fsevents": "~2.3.2" } }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "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/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -1782,8 +918,6 @@ }, "node_modules/vite": { "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", "dependencies": { @@ -1840,10 +974,15 @@ } } }, + "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.29", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", - "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "license": "MIT", "dependencies": { "@vue/compiler-dom": "3.5.29", @@ -1863,8 +1002,6 @@ }, "node_modules/vue-demi": { "version": "0.14.10", - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", - "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -1889,8 +1026,6 @@ }, "node_modules/vue-router": { "version": "4.6.4", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", - "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", "license": "MIT", "dependencies": { "@vue/devtools-api": "^6.6.4" @@ -1902,33 +1037,21 @@ "vue": "^3.5.0" } }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, "node_modules/vue-tsc": { - "version": "1.8.27", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", - "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", "dev": true, "license": "MIT", "dependencies": { - "@volar/typescript": "~1.11.1", - "@vue/language-core": "1.8.27", - "semver": "^7.5.4" + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" }, "bin": { "vue-tsc": "bin/vue-tsc.js" }, "peerDependencies": { - "typescript": "*" + "typescript": ">=5.0.0" } } } diff --git a/ai-service-admin/package.json b/ai-service-admin/package.json index d8fbf9f..8cc1306 100644 --- a/ai-service-admin/package.json +++ b/ai-service-admin/package.json @@ -17,8 +17,8 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.4", - "typescript": "^5.2.2", + "typescript": "~5.6.0", "vite": "^5.1.4", - "vue-tsc": "^1.8.27" + "vue-tsc": "^2.1.0" } } -- 2.40.1 From b91b57cfa4c77b4a31ce86c589fcce419bbfbf83 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:19:51 +0800 Subject: [PATCH 10/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DTypeScript?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF=E4=BB=A5=E5=85=BC=E5=AE=B9?= =?UTF-8?q?vue-tsc=20v2=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service-admin/src/App.vue | 5 +-- ai-service-admin/src/api/tenant.ts | 2 +- ai-service-admin/src/utils/request.ts | 23 +++++++++----- ai-service-admin/src/views/kb/index.vue | 32 ++++++++++++++------ ai-service-admin/src/views/rag-lab/index.vue | 2 +- ai-service-admin/src/vite-env.d.ts | 10 ++++++ ai-service-admin/tsconfig.json | 3 +- 7 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 ai-service-admin/src/vite-env.d.ts diff --git a/ai-service-admin/src/App.vue b/ai-service-admin/src/App.vue index b6aa613..347d66a 100644 --- a/ai-service-admin/src/App.vue +++ b/ai-service-admin/src/App.vue @@ -98,7 +98,6 @@ const isValidTenantId = (tenantId: string): boolean => { const fetchTenantList = async () => { loading.value = true try { - // 检查当前租户ID格式是否有效 if (!isValidTenantId(currentTenantId.value)) { console.warn('Invalid tenant ID format, resetting to default:', currentTenantId.value) currentTenantId.value = 'default@ash@2026' @@ -108,7 +107,6 @@ const fetchTenantList = async () => { const response = await getTenantList() tenantList.value = response.tenants || [] - // 如果当前租户不在列表中,默认选择第一个 if (tenantList.value.length > 0 && !tenantList.value.find(t => t.id === currentTenantId.value)) { const firstTenant = tenantList.value[0] currentTenantId.value = firstTenant.id @@ -117,8 +115,7 @@ const fetchTenantList = async () => { } catch (error) { ElMessage.error('获取租户列表失败') console.error('Failed to fetch tenant list:', error) - // 失败时使用默认租户 - tenantList.value = [{ id: 'default@ash@2026', name: 'default (2026)' }] + tenantList.value = [{ id: 'default@ash@2026', name: 'default (2026)', displayName: 'default', year: '2026', createdAt: new Date().toISOString() }] } finally { loading.value = false } diff --git a/ai-service-admin/src/api/tenant.ts b/ai-service-admin/src/api/tenant.ts index 2fce357..f679bd8 100644 --- a/ai-service-admin/src/api/tenant.ts +++ b/ai-service-admin/src/api/tenant.ts @@ -13,7 +13,7 @@ export interface TenantListResponse { total: number } -export function getTenantList() { +export function getTenantList(): Promise { return request({ url: '/admin/tenants', method: 'get' diff --git a/ai-service-admin/src/utils/request.ts b/ai-service-admin/src/utils/request.ts index 7d64d24..e80ef52 100644 --- a/ai-service-admin/src/utils/request.ts +++ b/ai-service-admin/src/utils/request.ts @@ -1,21 +1,22 @@ -import axios from 'axios' +import axios, { type AxiosRequestConfig } from 'axios' import { ElMessage, ElMessageBox } from 'element-plus' import { useTenantStore } from '@/stores/tenant' -// 创建 axios 实例 const service = axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API || '/api', timeout: 60000 }) -// 请求拦截器 service.interceptors.request.use( (config) => { const tenantStore = useTenantStore() if (tenantStore.currentTenantId) { config.headers['X-Tenant-Id'] = tenantStore.currentTenantId } - // TODO: 如果有 token 也可以在这里注入 Authorization + const apiKey = import.meta.env.VITE_APP_API_KEY + if (apiKey) { + config.headers['X-API-Key'] = apiKey + } return config }, (error) => { @@ -24,11 +25,9 @@ service.interceptors.request.use( } ) -// 响应拦截器 service.interceptors.response.use( (response) => { const res = response.data - // 这里可以根据后端的 code 进行统一处理 return res }, (error) => { @@ -42,7 +41,6 @@ service.interceptors.response.use( cancelButtonText: '取消', type: 'warning' }).then(() => { - // TODO: 跳转到登录页或执行退出逻辑 location.href = '/login' }) } else if (status === 403) { @@ -69,4 +67,13 @@ service.interceptors.response.use( } ) -export default service +interface RequestConfig extends AxiosRequestConfig { + url: string + method?: string +} + +function request(config: RequestConfig): Promise { + return service.request(config) +} + +export default request diff --git a/ai-service-admin/src/views/kb/index.vue b/ai-service-admin/src/views/kb/index.vue index a6db08e..8720b4b 100644 --- a/ai-service-admin/src/views/kb/index.vue +++ b/ai-service-admin/src/views/kb/index.vue @@ -102,10 +102,17 @@ interface DocumentItem { createTime: string } +interface IndexJob { + jobId: string + status: string + progress: number + errorMsg?: string +} + const tableData = ref([]) const loading = ref(false) const jobDialogVisible = ref(false) -const currentJob = ref(null) +const currentJob = ref(null) const pollingJobs = ref>(new Set()) let pollingInterval: number | null = null @@ -150,10 +157,15 @@ const fetchDocuments = async () => { } } -const fetchJobStatus = async (jobId: string) => { +const fetchJobStatus = async (jobId: string): Promise => { try { - const res = await getIndexJob(jobId) - return res + const res: any = await getIndexJob(jobId) + return { + jobId: res.jobId || jobId, + status: res.status || 'pending', + progress: res.progress || 0, + errorMsg: res.errorMsg + } } catch (error) { console.error('Failed to fetch job status:', error) return null @@ -246,19 +258,21 @@ const handleFileChange = async (event: Event) => { try { loading.value = true - const res = await uploadDocument(formData) - ElMessage.success(`文档上传成功!任务ID: ${res.jobId}`) + const res: any = await uploadDocument(formData) + const jobId = res.jobId as string + ElMessage.success(`文档上传成功!任务ID: ${jobId}`) console.log('Upload response:', res) const newDoc: DocumentItem = { + docId: res.docId || '', name: file.name, - status: res.status || 'pending', - jobId: res.jobId, + status: (res.status as string) || 'pending', + jobId: jobId, createTime: new Date().toLocaleString('zh-CN') } tableData.value.unshift(newDoc) - startPolling(res.jobId) + startPolling(jobId) } catch (error) { ElMessage.error('文档上传失败') console.error('Upload error:', error) diff --git a/ai-service-admin/src/views/rag-lab/index.vue b/ai-service-admin/src/views/rag-lab/index.vue index b5af4e5..6f27ca1 100644 --- a/ai-service-admin/src/views/rag-lab/index.vue +++ b/ai-service-admin/src/views/rag-lab/index.vue @@ -327,7 +327,7 @@ const runStreamExperiment = async () => { } else if (parsed.type === 'error') { streamError.value = parsed.message || '流式输出错误' streaming.value = false - ElMessage.error(streamError.value) + ElMessage.error(streamError.value || '未知错误') } } catch { streamContent.value += data diff --git a/ai-service-admin/src/vite-env.d.ts b/ai-service-admin/src/vite-env.d.ts new file mode 100644 index 0000000..e933527 --- /dev/null +++ b/ai-service-admin/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_APP_BASE_API: string + readonly VITE_APP_API_KEY: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/ai-service-admin/tsconfig.json b/ai-service-admin/tsconfig.json index 33e425c..aaf75f2 100644 --- a/ai-service-admin/tsconfig.json +++ b/ai-service-admin/tsconfig.json @@ -15,7 +15,8 @@ "baseUrl": ".", "paths": { "@/*": ["src/*"] - } + }, + "types": ["vite/client"] }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [{ "path": "./tsconfig.node.json" }] -- 2.40.1 From bd38e7816a3b2775ef42d6e2140fffa790f09998 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:21:30 +0800 Subject: [PATCH 11/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dnpm=E5=8F=AF?= =?UTF-8?q?=E9=80=89=E4=BE=9D=E8=B5=96=E5=AE=89=E8=A3=85=E9=97=AE=E9=A2=98?= =?UTF-8?q?=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service-admin/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service-admin/Dockerfile b/ai-service-admin/Dockerfile index fe22ad8..d603ec0 100644 --- a/ai-service-admin/Dockerfile +++ b/ai-service-admin/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app COPY package*.json ./ -RUN npm ci +RUN npm ci --include=optional || npm install COPY . . -- 2.40.1 From a60a760951a9aa810cd73afba2a2b69ab494f585 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:23:02 +0800 Subject: [PATCH 12/31] =?UTF-8?q?fix:=20=E6=98=BE=E5=BC=8F=E5=AE=89?= =?UTF-8?q?=E8=A3=85rollup=E5=8E=9F=E7=94=9F=E6=A8=A1=E5=9D=97=E8=A7=A3?= =?UTF-8?q?=E5=86=B3Alpine=E6=9E=84=E5=BB=BA=E9=97=AE=E9=A2=98=20[AC-AISVC?= =?UTF-8?q?-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service-admin/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ai-service-admin/Dockerfile b/ai-service-admin/Dockerfile index d603ec0..fdaa9c0 100644 --- a/ai-service-admin/Dockerfile +++ b/ai-service-admin/Dockerfile @@ -5,7 +5,7 @@ WORKDIR /app COPY package*.json ./ -RUN npm ci --include=optional || npm install +RUN npm install && npm install @rollup/rollup-linux-x64-musl --save-optional COPY . . -- 2.40.1 From 40ff48498f9327893fe12e499c346ae5d6218ef8 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:28:44 +0800 Subject: [PATCH 13/31] =?UTF-8?q?fix:=20nginx=E4=BD=BF=E7=94=A8=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=BB=B6=E8=BF=9F=E8=A7=A3=E6=9E=90upstream=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E5=90=AF=E5=8A=A8=E6=8A=A5=E9=94=99=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service-admin/nginx.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ai-service-admin/nginx.conf b/ai-service-admin/nginx.conf index 90a7824..03e0b59 100644 --- a/ai-service-admin/nginx.conf +++ b/ai-service-admin/nginx.conf @@ -9,7 +9,8 @@ server { } location /api/ { - proxy_pass http://ai-service:8080/; + set $upstream http://ai-service:8080; + proxy_pass $upstream/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; -- 2.40.1 From c7a71d6e039d3796a894b091d6c572e1550d9ca4 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:29:56 +0800 Subject: [PATCH 14/31] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0python-multipart?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E8=A7=A3=E5=86=B3=E8=A1=A8=E5=8D=95=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E9=94=99=E8=AF=AF=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ai-service/pyproject.toml b/ai-service/pyproject.toml index ae928af..a11926f 100644 --- a/ai-service/pyproject.toml +++ b/ai-service/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "python-docx>=1.1.0", "pymupdf>=1.23.0", "pdfplumber>=0.10.0", + "python-multipart>=0.0.6", ] [project.optional-dependencies] -- 2.40.1 From 1000158550cdf5d9784428d03419363fd5dec88f Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:41:33 +0800 Subject: [PATCH 15/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DDocker=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E5=92=8CAPI=20Key=E9=85=8D=E7=BD=AE=20[AC-AISVC-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正docker-compose.yaml中健康检查路径从/health改为/ai/health - 在middleware中添加/ai/health到API Key和租户检查的跳过列表 - 添加前端.env.example配置文件说明API Key配置方法 - 更新README添加API Key配置步骤说明 --- README.md | 30 +++++++++++-- ai-service-admin/.env.example | 8 ++++ ai-service/app/core/middleware.py | 72 +++++++++++++++++++++++++++---- docker-compose.yaml | 2 +- 4 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 ai-service-admin/.env.example diff --git a/README.md b/README.md index 73f9574..65b5f23 100644 --- a/README.md +++ b/README.md @@ -91,11 +91,35 @@ docker exec -it ai-ollama ollama pull nomic-embed-text # 检查服务状态 docker compose ps -# 查看后端日志 -docker compose logs -f ai-service +# 查看后端日志,找到自动生成的 API Key +docker compose logs -f ai-service | grep "Default API Key" ``` -#### 6. 访问服务 +> **重要**: 后端首次启动时会自动生成一个默认 API Key,请从日志中复制该 Key,用于前端配置。 + +#### 6. 配置前端 API Key + +```bash +# 创建前端环境变量文件 +cd ai-service-admin +cp .env.example .env +``` + +编辑 `ai-service-admin/.env`,将 `VITE_APP_API_KEY` 设置为后端日志中的 API Key: + +```env +VITE_APP_BASE_API=/api +VITE_APP_API_KEY=<从后端日志复制的API Key> +``` + +然后重新构建前端: + +```bash +cd .. +docker compose up -d --build ai-service-admin +``` + +#### 7. 访问服务 | 服务 | 地址 | 说明 | |------|------|------| diff --git a/ai-service-admin/.env.example b/ai-service-admin/.env.example new file mode 100644 index 0000000..533aaf3 --- /dev/null +++ b/ai-service-admin/.env.example @@ -0,0 +1,8 @@ +# API Base URL +VITE_APP_BASE_API=/api + +# Default API Key for authentication +# IMPORTANT: You must set this to a valid API key from the backend +# The backend creates a default API key on first startup (check backend logs) +# Or you can create one via the API: POST /admin/api-keys +VITE_APP_API_KEY=your-api-key-here diff --git a/ai-service/app/core/middleware.py b/ai-service/app/core/middleware.py index 4100813..e1db0c3 100644 --- a/ai-service/app/core/middleware.py +++ b/ai-service/app/core/middleware.py @@ -1,6 +1,6 @@ """ Middleware for AI Service. -[AC-AISVC-10, AC-AISVC-12] X-Tenant-Id header validation and tenant context injection. +[AC-AISVC-10, AC-AISVC-12, AC-AISVC-50] X-Tenant-Id header validation, tenant context injection, and API Key authentication. """ import logging @@ -17,12 +17,20 @@ from app.core.tenant import clear_tenant_context, set_tenant_context logger = logging.getLogger(__name__) TENANT_ID_HEADER = "X-Tenant-Id" +API_KEY_HEADER = "X-API-Key" ACCEPT_HEADER = "Accept" SSE_CONTENT_TYPE = "text/event-stream" -# Tenant ID format: name@ash@year (e.g., szmp@ash@2026) TENANT_ID_PATTERN = re.compile(r'^[^@]+@ash@\d{4}$') +PATHS_SKIP_API_KEY = { + "/health", + "/ai/health", + "/docs", + "/redoc", + "/openapi.json", +} + def validate_tenant_id_format(tenant_id: str) -> bool: """ @@ -41,6 +49,59 @@ def parse_tenant_id(tenant_id: str) -> tuple[str, str]: return parts[0], parts[2] +class ApiKeyMiddleware(BaseHTTPMiddleware): + """ + [AC-AISVC-50] Middleware to validate API Key for all requests. + + Features: + - Validates X-API-Key header against in-memory cache + - Skips validation for health/docs endpoints + - Returns 401 for missing or invalid API key + """ + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + if self._should_skip_api_key(request.url.path): + return await call_next(request) + + api_key = request.headers.get(API_KEY_HEADER) + + if not api_key or not api_key.strip(): + logger.warning(f"[AC-AISVC-50] Missing X-API-Key header for {request.url.path}") + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content=ErrorResponse( + code=ErrorCode.UNAUTHORIZED.value, + message="Missing required header: X-API-Key", + ).model_dump(exclude_none=True), + ) + + api_key = api_key.strip() + + from app.services.api_key import get_api_key_service + service = get_api_key_service() + + if not service.validate_key(api_key): + logger.warning(f"[AC-AISVC-50] Invalid API key for {request.url.path}") + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content=ErrorResponse( + code=ErrorCode.UNAUTHORIZED.value, + message="Invalid API key", + ).model_dump(exclude_none=True), + ) + + return await call_next(request) + + def _should_skip_api_key(self, path: str) -> bool: + """Check if the path should skip API key validation.""" + if path in PATHS_SKIP_API_KEY: + return True + for skip_path in PATHS_SKIP_API_KEY: + if path.startswith(skip_path): + return True + return False + + class TenantContextMiddleware(BaseHTTPMiddleware): """ [AC-AISVC-10, AC-AISVC-12] Middleware to extract and validate X-Tenant-Id header. @@ -51,7 +112,7 @@ class TenantContextMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next: Callable) -> Response: clear_tenant_context() - if request.url.path == "/ai/health": + if request.url.path in ("/health", "/ai/health"): return await call_next(request) tenant_id = request.headers.get(TENANT_ID_HEADER) @@ -68,7 +129,6 @@ class TenantContextMiddleware(BaseHTTPMiddleware): tenant_id = tenant_id.strip() - # Validate tenant ID format if not validate_tenant_id_format(tenant_id): logger.warning(f"[AC-AISVC-10] Invalid tenant ID format: {tenant_id}") return JSONResponse( @@ -79,13 +139,11 @@ class TenantContextMiddleware(BaseHTTPMiddleware): ).model_dump(exclude_none=True), ) - # Auto-create tenant if not exists (for admin endpoints) if request.url.path.startswith("/admin/") or request.url.path.startswith("/ai/"): try: await self._ensure_tenant_exists(request, tenant_id) except Exception as e: logger.error(f"[AC-AISVC-10] Failed to ensure tenant exists: {e}") - # Continue processing even if tenant creation fails set_tenant_context(tenant_id) request.state.tenant_id = tenant_id @@ -112,7 +170,6 @@ class TenantContextMiddleware(BaseHTTPMiddleware): name, year = parse_tenant_id(tenant_id) async with async_session_maker() as session: - # Check if tenant exists stmt = select(Tenant).where(Tenant.tenant_id == tenant_id) result = await session.execute(stmt) existing_tenant = result.scalar_one_or_none() @@ -121,7 +178,6 @@ class TenantContextMiddleware(BaseHTTPMiddleware): logger.debug(f"[AC-AISVC-10] Tenant already exists: {tenant_id}") return - # Create new tenant new_tenant = Tenant( tenant_id=tenant_id, name=name, diff --git a/docker-compose.yaml b/docker-compose.yaml index eb0947b..5d9fd2a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -27,7 +27,7 @@ services: networks: - ai-network healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + test: ["CMD", "curl", "-f", "http://localhost:8080/ai/health"] interval: 30s timeout: 10s retries: 3 -- 2.40.1 From 77033efd346a7b28ffee73d89ba05a68f8345df1 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:45:00 +0800 Subject: [PATCH 16/31] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4docker-compose?= =?UTF-8?q?=E8=BF=87=E6=97=B6=E7=9A=84version=E5=AD=97=E6=AE=B5=20[AC-AISV?= =?UTF-8?q?C-01]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 5d9fd2a..9820697 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '3.8' - services: ai-service: build: -- 2.40.1 From ee2c7c0d0c6eb1c2678b638aa1dd7dbade630e81 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 02:52:50 +0800 Subject: [PATCH 17/31] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0API=20Key?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E5=8A=9F=E8=83=BD=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ApiKey 模型和数据库表 - 新增 ApiKeyService 服务,支持内存缓存验证 - 新增 ApiKeyMiddleware 中间件,验证所有请求 - 应用启动时自动创建默认 API Key - 新增 /admin/api-keys 管理接口 --- ai-service/app/api/admin/__init__.py | 5 +- ai-service/app/api/admin/api_key.py | 154 +++++++++++++++++ ai-service/app/main.py | 21 ++- ai-service/app/models/__init__.py | 1 + ai-service/app/models/entities.py | 24 +++ ai-service/app/services/api_key.py | 249 +++++++++++++++++++++++++++ ai-service/scripts/init_db.sql | 16 ++ 7 files changed, 465 insertions(+), 5 deletions(-) create mode 100644 ai-service/app/api/admin/api_key.py create mode 100644 ai-service/app/services/api_key.py diff --git a/ai-service/app/api/admin/__init__.py b/ai-service/app/api/admin/__init__.py index 40b96bb..5bc4b10 100644 --- a/ai-service/app/api/admin/__init__.py +++ b/ai-service/app/api/admin/__init__.py @@ -1,8 +1,9 @@ """ Admin API routes for AI Service management. -[AC-ASA-01, AC-ASA-02, AC-ASA-05, AC-ASA-07, AC-ASA-08] Admin management endpoints. +[AC-ASA-01, AC-ASA-02, AC-ASA-05, AC-ASA-07, AC-ASA-08, AC-AISVC-50] Admin management endpoints. """ +from app.api.admin.api_key import router as api_key_router from app.api.admin.dashboard import router as dashboard_router from app.api.admin.embedding import router as embedding_router from app.api.admin.kb import router as kb_router @@ -11,4 +12,4 @@ from app.api.admin.rag import router as rag_router from app.api.admin.sessions import router as sessions_router from app.api.admin.tenants import router as tenants_router -__all__ = ["dashboard_router", "embedding_router", "kb_router", "llm_router", "rag_router", "sessions_router", "tenants_router"] +__all__ = ["api_key_router", "dashboard_router", "embedding_router", "kb_router", "llm_router", "rag_router", "sessions_router", "tenants_router"] diff --git a/ai-service/app/api/admin/api_key.py b/ai-service/app/api/admin/api_key.py new file mode 100644 index 0000000..b73b78b --- /dev/null +++ b/ai-service/app/api/admin/api_key.py @@ -0,0 +1,154 @@ +""" +API Key management endpoints. +[AC-AISVC-50] CRUD operations for API keys. +""" + +import logging +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from pydantic import BaseModel, Field +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.database import get_session +from app.models.entities import ApiKey, ApiKeyCreate +from app.services.api_key import get_api_key_service + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/admin/api-keys", tags=["API Keys"]) + + +class ApiKeyResponse(BaseModel): + """Response model for API key.""" + + id: str = Field(..., description="API key ID") + key: str = Field(..., description="API key value") + name: str = Field(..., description="API key name") + is_active: bool = Field(..., description="Whether the key is active") + created_at: str = Field(..., description="Creation time") + updated_at: str = Field(..., description="Last update time") + + +class ApiKeyListResponse(BaseModel): + """Response model for API key list.""" + + keys: list[ApiKeyResponse] = Field(..., description="List of API keys") + total: int = Field(..., description="Total count") + + +class CreateApiKeyRequest(BaseModel): + """Request model for creating API key.""" + + name: str = Field(..., description="API key name/description") + key: str | None = Field(default=None, description="Custom API key (auto-generated if not provided)") + + +class ToggleApiKeyRequest(BaseModel): + """Request model for toggling API key status.""" + + is_active: bool = Field(..., description="New active status") + + +def api_key_to_response(api_key: ApiKey) -> ApiKeyResponse: + """Convert ApiKey entity to response model.""" + return ApiKeyResponse( + id=str(api_key.id), + key=api_key.key, + name=api_key.name, + is_active=api_key.is_active, + created_at=api_key.created_at.isoformat(), + updated_at=api_key.updated_at.isoformat(), + ) + + +@router.get("", response_model=ApiKeyListResponse) +async def list_api_keys( + session: Annotated[AsyncSession, Depends(get_session)], +): + """ + [AC-AISVC-50] List all API keys. + """ + service = get_api_key_service() + keys = await service.list_keys(session) + + return ApiKeyListResponse( + keys=[api_key_to_response(k) for k in keys], + total=len(keys), + ) + + +@router.post("", response_model=ApiKeyResponse, status_code=status.HTTP_201_CREATED) +async def create_api_key( + request: CreateApiKeyRequest, + session: Annotated[AsyncSession, Depends(get_session)], +): + """ + [AC-AISVC-50] Create a new API key. + """ + service = get_api_key_service() + + key_value = request.key or service.generate_key() + + key_create = ApiKeyCreate( + key=key_value, + name=request.name, + is_active=True, + ) + + api_key = await service.create_key(session, key_create) + logger.info(f"[AC-AISVC-50] Created API key: {api_key.name}") + + return api_key_to_response(api_key) + + +@router.delete("/{key_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_api_key( + key_id: str, + session: Annotated[AsyncSession, Depends(get_session)], +): + """ + [AC-AISVC-50] Delete an API key. + """ + service = get_api_key_service() + + deleted = await service.delete_key(session, key_id) + + if not deleted: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="API key not found", + ) + + +@router.patch("/{key_id}/toggle", response_model=ApiKeyResponse) +async def toggle_api_key( + key_id: str, + request: ToggleApiKeyRequest, + session: Annotated[AsyncSession, Depends(get_session)], +): + """ + [AC-AISVC-50] Toggle API key active status. + """ + service = get_api_key_service() + + api_key = await service.toggle_key(session, key_id, request.is_active) + + if not api_key: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="API key not found", + ) + + return api_key_to_response(api_key) + + +@router.post("/reload-cache", status_code=status.HTTP_204_NO_CONTENT) +async def reload_api_key_cache( + session: Annotated[AsyncSession, Depends(get_session)], +): + """ + [AC-AISVC-50] Reload API key cache from database. + """ + service = get_api_key_service() + await service.reload_cache(session) diff --git a/ai-service/app/main.py b/ai-service/app/main.py index c18afba..56d7ccc 100644 --- a/ai-service/app/main.py +++ b/ai-service/app/main.py @@ -12,7 +12,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from app.api import chat_router, health_router -from app.api.admin import dashboard_router, embedding_router, kb_router, llm_router, rag_router, sessions_router, tenants_router +from app.api.admin import api_key_router, dashboard_router, embedding_router, kb_router, llm_router, rag_router, sessions_router, tenants_router from app.api.admin.kb_optimized import router as kb_optimized_router from app.core.config import get_settings from app.core.database import close_db, init_db @@ -24,7 +24,7 @@ from app.core.exceptions import ( generic_exception_handler, http_exception_handler, ) -from app.core.middleware import TenantContextMiddleware +from app.core.middleware import ApiKeyMiddleware, TenantContextMiddleware from app.core.qdrant_client import close_qdrant_client settings = get_settings() @@ -40,7 +40,7 @@ logger = logging.getLogger(__name__) @asynccontextmanager async def lifespan(app: FastAPI): """ - [AC-AISVC-01, AC-AISVC-11] Application lifespan manager. + [AC-AISVC-01, AC-AISVC-11, AC-AISVC-50] Application lifespan manager. Handles startup and shutdown of database and external connections. """ logger.info(f"[AC-AISVC-01] Starting {settings.app_name} v{settings.app_version}") @@ -51,6 +51,19 @@ async def lifespan(app: FastAPI): except Exception as e: logger.warning(f"[AC-AISVC-11] Database initialization skipped: {e}") + try: + from app.core.database import async_session_maker + from app.services.api_key import get_api_key_service + + async with async_session_maker() as session: + api_key_service = get_api_key_service() + await api_key_service.initialize(session) + default_key = await api_key_service.create_default_key(session) + if default_key: + logger.info(f"[AC-AISVC-50] Default API key created: {default_key.key}") + except Exception as e: + logger.warning(f"[AC-AISVC-50] API key initialization skipped: {e}") + yield await close_db() @@ -87,6 +100,7 @@ app.add_middleware( ) app.add_middleware(TenantContextMiddleware) +app.add_middleware(ApiKeyMiddleware) app.add_exception_handler(AIServiceException, ai_service_exception_handler) app.add_exception_handler(HTTPException, http_exception_handler) @@ -113,6 +127,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE app.include_router(health_router) app.include_router(chat_router) +app.include_router(api_key_router) app.include_router(dashboard_router) app.include_router(embedding_router) app.include_router(kb_router) diff --git a/ai-service/app/models/__init__.py b/ai-service/app/models/__init__.py index cbbe9e7..f30da8b 100644 --- a/ai-service/app/models/__init__.py +++ b/ai-service/app/models/__init__.py @@ -50,6 +50,7 @@ class ErrorCode(str, Enum): INVALID_REQUEST = "INVALID_REQUEST" MISSING_TENANT_ID = "MISSING_TENANT_ID" INVALID_TENANT_ID = "INVALID_TENANT_ID" + UNAUTHORIZED = "UNAUTHORIZED" INTERNAL_ERROR = "INTERNAL_ERROR" SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE" TIMEOUT = "TIMEOUT" diff --git a/ai-service/app/models/entities.py b/ai-service/app/models/entities.py index cf41595..dd9363a 100644 --- a/ai-service/app/models/entities.py +++ b/ai-service/app/models/entities.py @@ -198,3 +198,27 @@ class DocumentCreate(SQLModel): file_path: str | None = None file_size: int | None = None file_type: str | None = None + + +class ApiKey(SQLModel, table=True): + """ + [AC-AISVC-50] API Key entity for lightweight authentication. + Keys are loaded into memory on startup for fast validation. + """ + + __tablename__ = "api_keys" + + id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) + key: str = Field(..., description="API Key (unique)", unique=True, index=True) + name: str = Field(..., description="Key name/description for identification") + is_active: bool = Field(default=True, description="Whether the key is active") + created_at: datetime = Field(default_factory=datetime.utcnow, description="Creation time") + updated_at: datetime = Field(default_factory=datetime.utcnow, description="Last update time") + + +class ApiKeyCreate(SQLModel): + """Schema for creating a new API key.""" + + key: str + name: str + is_active: bool = True diff --git a/ai-service/app/services/api_key.py b/ai-service/app/services/api_key.py new file mode 100644 index 0000000..a4c14a6 --- /dev/null +++ b/ai-service/app/services/api_key.py @@ -0,0 +1,249 @@ +""" +API Key management service. +[AC-AISVC-50] Lightweight authentication with in-memory cache. +""" + +import logging +import secrets +from datetime import datetime +from typing import Optional + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.entities import ApiKey, ApiKeyCreate + +logger = logging.getLogger(__name__) + + +class ApiKeyService: + """ + [AC-AISVC-50] API Key management service. + + Features: + - In-memory cache for fast validation + - Database persistence + - Hot-reload support + """ + + def __init__(self): + self._keys_cache: set[str] = set() + self._initialized: bool = False + + async def initialize(self, session: AsyncSession) -> None: + """ + Load all active API keys from database into memory. + Should be called on application startup. + """ + result = await session.execute( + select(ApiKey).where(ApiKey.is_active == True) + ) + keys = result.scalars().all() + + self._keys_cache = {key.key for key in keys} + self._initialized = True + + logger.info(f"[AC-AISVC-50] Loaded {len(self._keys_cache)} API keys into memory") + + def validate_key(self, key: str) -> bool: + """ + Validate an API key against the in-memory cache. + + Args: + key: The API key to validate + + Returns: + True if the key is valid, False otherwise + """ + if not self._initialized: + logger.warning("[AC-AISVC-50] API key service not initialized") + return False + + return key in self._keys_cache + + def generate_key(self) -> str: + """ + Generate a new secure API key. + + Returns: + A URL-safe random string + """ + return secrets.token_urlsafe(32) + + async def create_key( + self, + session: AsyncSession, + key_create: ApiKeyCreate + ) -> ApiKey: + """ + Create a new API key. + + Args: + session: Database session + key_create: Key creation data + + Returns: + The created ApiKey entity + """ + api_key = ApiKey( + key=key_create.key, + name=key_create.name, + is_active=key_create.is_active, + ) + + session.add(api_key) + await session.commit() + await session.refresh(api_key) + + if api_key.is_active: + self._keys_cache.add(api_key.key) + + logger.info(f"[AC-AISVC-50] Created API key: {api_key.name}") + return api_key + + async def create_default_key(self, session: AsyncSession) -> Optional[ApiKey]: + """ + Create a default API key if none exists. + + Returns: + The created ApiKey or None if keys already exist + """ + result = await session.execute(select(ApiKey).limit(1)) + existing = result.scalar_one_or_none() + + if existing: + return None + + default_key = secrets.token_urlsafe(32) + api_key = ApiKey( + key=default_key, + name="Default API Key", + is_active=True, + ) + + session.add(api_key) + await session.commit() + await session.refresh(api_key) + + self._keys_cache.add(api_key.key) + + logger.info(f"[AC-AISVC-50] Created default API key: {api_key.key}") + return api_key + + async def delete_key( + self, + session: AsyncSession, + key_id: str + ) -> bool: + """ + Delete an API key. + + Args: + session: Database session + key_id: The key ID to delete + + Returns: + True if deleted, False if not found + """ + import uuid + + try: + key_uuid = uuid.UUID(key_id) + except ValueError: + return False + + result = await session.execute( + select(ApiKey).where(ApiKey.id == key_uuid) + ) + api_key = result.scalar_one_or_none() + + if not api_key: + return False + + key_value = api_key.key + await session.delete(api_key) + await session.commit() + + self._keys_cache.discard(key_value) + + logger.info(f"[AC-AISVC-50] Deleted API key: {api_key.name}") + return True + + async def toggle_key( + self, + session: AsyncSession, + key_id: str, + is_active: bool + ) -> Optional[ApiKey]: + """ + Toggle API key active status. + + Args: + session: Database session + key_id: The key ID to toggle + is_active: New active status + + Returns: + The updated ApiKey or None if not found + """ + import uuid + + try: + key_uuid = uuid.UUID(key_id) + except ValueError: + return None + + result = await session.execute( + select(ApiKey).where(ApiKey.id == key_uuid) + ) + api_key = result.scalar_one_or_none() + + if not api_key: + return None + + api_key.is_active = is_active + api_key.updated_at = datetime.utcnow() + + session.add(api_key) + await session.commit() + await session.refresh(api_key) + + if is_active: + self._keys_cache.add(api_key.key) + else: + self._keys_cache.discard(api_key.key) + + logger.info(f"[AC-AISVC-50] Toggled API key {api_key.name}: active={is_active}") + return api_key + + async def list_keys(self, session: AsyncSession) -> list[ApiKey]: + """ + List all API keys. + + Args: + session: Database session + + Returns: + List of all ApiKey entities + """ + result = await session.execute(select(ApiKey)) + return list(result.scalars().all()) + + async def reload_cache(self, session: AsyncSession) -> None: + """ + Reload all API keys from database into memory. + """ + self._keys_cache.clear() + await self.initialize(session) + logger.info("[AC-AISVC-50] API key cache reloaded") + + +_api_key_service: ApiKeyService | None = None + + +def get_api_key_service() -> ApiKeyService: + """Get the global API key service instance.""" + global _api_key_service + if _api_key_service is None: + _api_key_service = ApiKeyService() + return _api_key_service diff --git a/ai-service/scripts/init_db.sql b/ai-service/scripts/init_db.sql index 9a68dac..b4690ea 100644 --- a/ai-service/scripts/init_db.sql +++ b/ai-service/scripts/init_db.sql @@ -74,6 +74,18 @@ CREATE TABLE IF NOT EXISTS index_jobs ( updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL ); +-- ============================================ +-- API Keys Table [AC-AISVC-50] +-- ============================================ +CREATE TABLE IF NOT EXISTS api_keys ( + id UUID NOT NULL PRIMARY KEY, + key VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + -- ============================================ -- Indexes -- ============================================ @@ -100,6 +112,10 @@ CREATE INDEX IF NOT EXISTS ix_index_jobs_tenant_id ON index_jobs (tenant_id); CREATE INDEX IF NOT EXISTS ix_index_jobs_tenant_doc ON index_jobs (tenant_id, doc_id); CREATE INDEX IF NOT EXISTS ix_index_jobs_tenant_status ON index_jobs (tenant_id, status); +-- API Keys Indexes [AC-AISVC-50] +CREATE INDEX IF NOT EXISTS ix_api_keys_key ON api_keys (key); +CREATE INDEX IF NOT EXISTS ix_api_keys_is_active ON api_keys (is_active); + -- ============================================ -- Verification -- ============================================ -- 2.40.1 From 97e7fd09920e8e24a67ea8b64f1704df1805f5ee Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 03:11:36 +0800 Subject: [PATCH 18/31] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0API=20Key?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E7=B3=BB=E7=BB=9F=E5=92=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?Qdrant=E6=90=9C=E7=B4=A2=E9=97=AE=E9=A2=98=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 API Key 数据库模型和服务 - 新增 ApiKeyMiddleware 认证中间件 - 新增 /admin/api-keys 管理接口 - 前端支持 VITE_APP_API_KEY 环境变量 - 修复 optimized_retriever.py 中 Qdrant 搜索调用方式 - 更新 Dockerfile 支持构建时传入 API Key - 更新 docker-compose.yaml 支持前端 API Key 配置 --- .env.example | 4 +++ ai-service-admin/Dockerfile | 6 ++++ .../services/retrieval/optimized_retriever.py | 30 +++++++------------ docker-compose.yaml | 3 ++ 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index db110b4..9bd4a4d 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,7 @@ AI_SERVICE_LLM_MODEL=gpt-4o-mini # Ollama Configuration (for embedding model) AI_SERVICE_OLLAMA_BASE_URL=http://ollama:11434 AI_SERVICE_OLLAMA_EMBEDDING_MODEL=nomic-embed-text + +# Frontend API Key (required for admin panel authentication) +# Get this key from the backend logs after first startup, or from /admin/api-keys +VITE_APP_API_KEY=your-frontend-api-key-here diff --git a/ai-service-admin/Dockerfile b/ai-service-admin/Dockerfile index fdaa9c0..bee8070 100644 --- a/ai-service-admin/Dockerfile +++ b/ai-service-admin/Dockerfile @@ -3,6 +3,12 @@ FROM docker.1ms.run/node:20-alpine AS builder WORKDIR /app +ARG VITE_APP_API_KEY +ARG VITE_APP_BASE_API=/api + +ENV VITE_APP_API_KEY=$VITE_APP_API_KEY +ENV VITE_APP_BASE_API=$VITE_APP_BASE_API + COPY package*.json ./ RUN npm install && npm install @rollup/rollup-linux-x64-musl --save-optional diff --git a/ai-service/app/services/retrieval/optimized_retriever.py b/ai-service/app/services/retrieval/optimized_retriever.py index 1c773d8..74d6a21 100644 --- a/ai-service/app/services/retrieval/optimized_retriever.py +++ b/ai-service/app/services/retrieval/optimized_retriever.py @@ -396,42 +396,32 @@ class OptimizedRetriever(BaseRetriever): ) -> list[dict[str, Any]]: """Search using specified vector dimension.""" try: - qdrant = await client.get_client() - collection_name = client.get_collection_name(tenant_id) - logger.info( - f"[RAG-OPT] Searching collection={collection_name}, " - f"vector_name={vector_name}, limit={limit}, vector_dim={len(query_vector)}" + f"[RAG-OPT] Searching with vector_name={vector_name}, " + f"limit={limit}, vector_dim={len(query_vector)}" ) - results = await qdrant.search( - collection_name=collection_name, - query_vector=(vector_name, query_vector), + results = await client.search( + tenant_id=tenant_id, + query_vector=query_vector, limit=limit, + vector_name=vector_name, ) logger.info( - f"[RAG-OPT] Search returned {len(results)} results from collection={collection_name}" + f"[RAG-OPT] Search returned {len(results)} results" ) if len(results) > 0: for i, r in enumerate(results[:3]): logger.debug( - f"[RAG-OPT] Result {i+1}: id={r.id}, score={r.score:.4f}" + f"[RAG-OPT] Result {i+1}: id={r['id']}, score={r['score']:.4f}" ) - return [ - { - "id": str(result.id), - "score": result.score, - "payload": result.payload or {}, - } - for result in results - ] + return results except Exception as e: logger.error( - f"[RAG-OPT] Search with {vector_name} failed: {e}, " - f"collection_name={client.get_collection_name(tenant_id)}", + f"[RAG-OPT] Search with {vector_name} failed: {e}", exc_info=True ) return [] diff --git a/docker-compose.yaml b/docker-compose.yaml index 9820697..760d5b2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -35,6 +35,9 @@ services: build: context: ./ai-service-admin dockerfile: Dockerfile + args: + VITE_APP_API_KEY: ${VITE_APP_API_KEY:-} + VITE_APP_BASE_API: /api container_name: ai-service-admin restart: unless-stopped ports: -- 2.40.1 From 72700038c6b28c357f8ff8127000fb6554a5105d Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 11:50:03 +0800 Subject: [PATCH 19/31] =?UTF-8?q?fix:=20=E7=AE=80=E5=8C=96Nginx=E4=BB=A3?= =?UTF-8?q?=E7=90=86=E9=85=8D=E7=BD=AE=EF=BC=8C=E7=A7=BB=E9=99=A4upstream?= =?UTF-8?q?=E5=8F=98=E9=87=8F=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service-admin/nginx.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ai-service-admin/nginx.conf b/ai-service-admin/nginx.conf index 03e0b59..ced6778 100644 --- a/ai-service-admin/nginx.conf +++ b/ai-service-admin/nginx.conf @@ -9,8 +9,7 @@ server { } location /api/ { - set $upstream http://ai-service:8080; - proxy_pass $upstream/; + proxy_pass http://ai-service:8080/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -21,6 +20,7 @@ server { proxy_cache_bypass $http_upgrade; proxy_read_timeout 300s; proxy_connect_timeout 75s; + proxy_buffering off; } gzip on; -- 2.40.1 From b11b5a027fb4646c7431da1908e9b194796fef79 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 12:10:14 +0800 Subject: [PATCH 20/31] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E8=84=9A=E6=9C=AC=E6=B7=BB=E5=8A=A0chat=5Fme?= =?UTF-8?q?ssages=E7=BC=BA=E5=A4=B1=E5=AD=97=E6=AE=B5=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - init_db.sql 添加 prompt_tokens, completion_tokens, total_tokens 等字段 - 新增 migrate_add_columns.sql 用于现有数据库迁移 --- ai-service/scripts/init_db.sql | 7 ++++++ ai-service/scripts/migrate_add_columns.sql | 29 ++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 ai-service/scripts/migrate_add_columns.sql diff --git a/ai-service/scripts/init_db.sql b/ai-service/scripts/init_db.sql index b4690ea..691c833 100644 --- a/ai-service/scripts/init_db.sql +++ b/ai-service/scripts/init_db.sql @@ -28,6 +28,13 @@ CREATE TABLE IF NOT EXISTS chat_messages ( session_id VARCHAR NOT NULL, role VARCHAR NOT NULL, content TEXT NOT NULL, + prompt_tokens INTEGER, + completion_tokens INTEGER, + total_tokens INTEGER, + latency_ms INTEGER, + first_token_ms INTEGER, + is_error BOOLEAN NOT NULL DEFAULT FALSE, + error_message VARCHAR, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL ); diff --git a/ai-service/scripts/migrate_add_columns.sql b/ai-service/scripts/migrate_add_columns.sql new file mode 100644 index 0000000..c080a68 --- /dev/null +++ b/ai-service/scripts/migrate_add_columns.sql @@ -0,0 +1,29 @@ +-- Migration: Add missing columns to chat_messages table +-- Execute this on existing database to add new columns + +-- Add token tracking columns +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS prompt_tokens INTEGER; +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS completion_tokens INTEGER; +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS total_tokens INTEGER; + +-- Add latency tracking columns +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS latency_ms INTEGER; +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS first_token_ms INTEGER; + +-- Add error tracking columns +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS is_error BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE chat_messages ADD COLUMN IF NOT EXISTS error_message VARCHAR; + +-- Create API Keys table if not exists +CREATE TABLE IF NOT EXISTS api_keys ( + id UUID NOT NULL PRIMARY KEY, + key VARCHAR NOT NULL UNIQUE, + name VARCHAR NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +-- Create API Keys indexes +CREATE INDEX IF NOT EXISTS ix_api_keys_key ON api_keys (key); +CREATE INDEX IF NOT EXISTS ix_api_keys_is_active ON api_keys (is_active); -- 2.40.1 From a4af74751f75679f98bc2c3d5c1c84a7d5e968ca Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 12:17:52 +0800 Subject: [PATCH 21/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DConfigForm?= =?UTF-8?q?=E7=BB=84=E4=BB=B6watch=E6=97=A0=E9=99=90=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E5=86=85=E5=AD=98=E6=BA=A2=E5=87=BA=20[AC-AI?= =?UTF-8?q?SVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 JSON.stringify 比较避免 watch 循环触发 - 修复前端页面崩溃问题 --- ai-service-admin/src/components/common/ConfigForm.vue | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ai-service-admin/src/components/common/ConfigForm.vue b/ai-service-admin/src/components/common/ConfigForm.vue index 569e4af..a365961 100644 --- a/ai-service-admin/src/components/common/ConfigForm.vue +++ b/ai-service-admin/src/components/common/ConfigForm.vue @@ -173,8 +173,10 @@ const initFormData = () => { watch( () => props.modelValue, - () => { - initFormData() + (newVal) => { + if (JSON.stringify(newVal) !== JSON.stringify(formData.value)) { + initFormData() + } }, { deep: true } ) @@ -190,7 +192,9 @@ watch( watch( formData, (val) => { - emit('update:modelValue', val) + if (JSON.stringify(val) !== JSON.stringify(props.modelValue)) { + emit('update:modelValue', val) + } }, { deep: true } ) -- 2.40.1 From 6c16132557cbc6c6598907f3ef4fdc95f1ac28f1 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 12:20:49 +0800 Subject: [PATCH 22/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DEmbeddingConfigF?= =?UTF-8?q?orm=E7=BB=84=E4=BB=B6watch=E6=97=A0=E9=99=90=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/embedding/EmbeddingConfigForm.vue | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ai-service-admin/src/components/embedding/EmbeddingConfigForm.vue b/ai-service-admin/src/components/embedding/EmbeddingConfigForm.vue index e491cc8..8e0768f 100644 --- a/ai-service-admin/src/components/embedding/EmbeddingConfigForm.vue +++ b/ai-service-admin/src/components/embedding/EmbeddingConfigForm.vue @@ -173,8 +173,10 @@ const initFormData = () => { watch( () => props.modelValue, - () => { - initFormData() + (newVal) => { + if (JSON.stringify(newVal) !== JSON.stringify(formData.value)) { + initFormData() + } }, { deep: true } ) @@ -190,7 +192,9 @@ watch( watch( formData, (val) => { - emit('update:modelValue', val) + if (JSON.stringify(val) !== JSON.stringify(props.modelValue)) { + emit('update:modelValue', val) + } }, { deep: true } ) -- 2.40.1 From 3f1f4cd98d552f12f94a733c174df9029a3fe51b Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 12:30:04 +0800 Subject: [PATCH 23/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DConfigForm?= =?UTF-8?q?=E5=92=8CEmbeddingConfigForm=E7=BB=84=E4=BB=B6watch=E6=AD=BB?= =?UTF-8?q?=E5=BE=AA=E7=8E=AF=E5=AF=BC=E8=87=B4=E5=86=85=E5=AD=98=E6=BA=A2?= =?UTF-8?q?=E5=87=BA=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service-admin/src/components/common/ConfigForm.vue | 7 +++++++ .../src/components/embedding/EmbeddingConfigForm.vue | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/ai-service-admin/src/components/common/ConfigForm.vue b/ai-service-admin/src/components/common/ConfigForm.vue index a365961..92885ac 100644 --- a/ai-service-admin/src/components/common/ConfigForm.vue +++ b/ai-service-admin/src/components/common/ConfigForm.vue @@ -92,6 +92,7 @@ const emit = defineEmits<{ const formRef = ref() const formData = ref>({}) +const isUpdating = ref(false) const schemaProperties = computed(() => { return props.schema?.properties || {} @@ -174,6 +175,7 @@ const initFormData = () => { watch( () => props.modelValue, (newVal) => { + if (isUpdating.value) return if (JSON.stringify(newVal) !== JSON.stringify(formData.value)) { initFormData() } @@ -192,8 +194,13 @@ watch( watch( formData, (val) => { + if (isUpdating.value) return if (JSON.stringify(val) !== JSON.stringify(props.modelValue)) { + isUpdating.value = true emit('update:modelValue', val) + Promise.resolve().then(() => { + isUpdating.value = false + }) } }, { deep: true } diff --git a/ai-service-admin/src/components/embedding/EmbeddingConfigForm.vue b/ai-service-admin/src/components/embedding/EmbeddingConfigForm.vue index 8e0768f..e0cd7ba 100644 --- a/ai-service-admin/src/components/embedding/EmbeddingConfigForm.vue +++ b/ai-service-admin/src/components/embedding/EmbeddingConfigForm.vue @@ -92,6 +92,7 @@ const emit = defineEmits<{ const formRef = ref() const formData = ref>({}) +const isUpdating = ref(false) const schemaProperties = computed(() => { return props.schema?.properties || {} @@ -174,6 +175,7 @@ const initFormData = () => { watch( () => props.modelValue, (newVal) => { + if (isUpdating.value) return if (JSON.stringify(newVal) !== JSON.stringify(formData.value)) { initFormData() } @@ -192,8 +194,13 @@ watch( watch( formData, (val) => { + if (isUpdating.value) return if (JSON.stringify(val) !== JSON.stringify(props.modelValue)) { + isUpdating.value = true emit('update:modelValue', val) + Promise.resolve().then(() => { + isUpdating.value = false + }) } }, { deep: true } -- 2.40.1 From 6150fc0dd27fda30e69cdb837b5bd9136be8eb21 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 12:39:42 +0800 Subject: [PATCH 24/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DRAG=E6=A3=80?= =?UTF-8?q?=E7=B4=A2=E6=97=A0=E7=BB=93=E6=9E=9C=E9=97=AE=E9=A2=98-?= =?UTF-8?q?=E5=90=91=E9=87=8F=E5=AD=98=E5=82=A8=E6=A0=BC=E5=BC=8F=E4=B8=8E?= =?UTF-8?q?=E6=A3=80=E7=B4=A2=E6=A0=BC=E5=BC=8F=E4=B8=8D=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai-service/app/api/admin/kb.py | 38 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/ai-service/app/api/admin/kb.py b/ai-service/app/api/admin/kb.py index e150d72..e185e0e 100644 --- a/ai-service/app/api/admin/kb.py +++ b/ai-service/app/api/admin/kb.py @@ -442,13 +442,15 @@ async def _index_document(tenant_id: str, job_id: str, doc_id: str, content: byt logger.info(f"[INDEX] Total chunks: {len(all_chunks)}") qdrant = await get_qdrant_client() - await qdrant.ensure_collection_exists(tenant_id) + await qdrant.ensure_collection_exists(tenant_id, use_multi_vector=True) + + from app.services.embedding.nomic_provider import NomicEmbeddingProvider + use_multi_vector = isinstance(embedding_provider, NomicEmbeddingProvider) + logger.info(f"[INDEX] Using multi-vector format: {use_multi_vector}") points = [] total_chunks = len(all_chunks) for i, chunk in enumerate(all_chunks): - embedding = await embedding_provider.embed(chunk.text) - payload = { "text": chunk.text, "source": doc_id, @@ -461,13 +463,26 @@ async def _index_document(tenant_id: str, job_id: str, doc_id: str, content: byt if chunk.source: payload["filename"] = chunk.source - points.append( - PointStruct( - id=str(uuid.uuid4()), - vector=embedding, - payload=payload, + if use_multi_vector: + embedding_result = await embedding_provider.embed_document(chunk.text) + points.append({ + "id": str(uuid.uuid4()), + "vector": { + "full": embedding_result.embedding_full, + "dim_256": embedding_result.embedding_256, + "dim_512": embedding_result.embedding_512, + }, + "payload": payload, + }) + else: + embedding = await embedding_provider.embed(chunk.text) + points.append( + PointStruct( + id=str(uuid.uuid4()), + vector=embedding, + payload=payload, + ) ) - ) progress = 20 + int((i + 1) / total_chunks * 70) if i % 10 == 0 or i == total_chunks - 1: @@ -478,7 +493,10 @@ async def _index_document(tenant_id: str, job_id: str, doc_id: str, content: byt if points: logger.info(f"[INDEX] Upserting {len(points)} vectors to Qdrant...") - await qdrant.upsert_vectors(tenant_id, points) + if use_multi_vector: + await qdrant.upsert_multi_vector(tenant_id, points) + else: + await qdrant.upsert_vectors(tenant_id, points) await kb_service.update_job_status( tenant_id, job_id, IndexJobStatus.COMPLETED.value, progress=100 -- 2.40.1 From fd04ed2cef4aabc0496b29271ab9476c4ed7b51b Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 14:45:21 +0800 Subject: [PATCH 25/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DRAG=E6=A3=80?= =?UTF-8?q?=E7=B4=A2=E5=A4=9A=E4=B8=AA=E9=97=AE=E9=A2=98=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=B5=8C=E5=85=A5=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要修复: 1. 修复ConfigForm和EmbeddingConfigForm组件watch死循环导致内存溢出 2. 修复向量存储格式与检索格式不匹配问题 3. 修复两阶段检索和混合检索互斥问题 4. 修复RRF融合时vector字段丢失问题 5. 修复embedding_full未归一化导致相似度计算错误 6. 修复嵌入模型配置表单不显示参数问题 功能增强: - 添加with_vectors参数支持返回向量用于重排序 - 新增两阶段+混合检索组合策略 - 更新README嵌入模型配置说明,推荐nomic-embed-text-v2-moe - 添加cleanup_qdrant.py脚本用于清理向量数据 --- README.md | 30 ++++- ai-service-admin/vite.config.ts | 2 +- ai-service/app/core/qdrant_client.py | 17 ++- ai-service/app/services/embedding/factory.py | 31 ++++- .../app/services/embedding/nomic_provider.py | 8 +- .../app/services/embedding/ollama_provider.py | 4 + .../app/services/embedding/openai_provider.py | 5 + .../services/retrieval/optimized_retriever.py | 126 ++++++++++++++++-- ai-service/scripts/cleanup_qdrant.py | 89 +++++++++++++ 9 files changed, 291 insertions(+), 21 deletions(-) create mode 100644 ai-service/scripts/cleanup_qdrant.py diff --git a/README.md b/README.md index 65b5f23..96b84e5 100644 --- a/README.md +++ b/README.md @@ -78,14 +78,36 @@ docker-compose up -d --build #### 4. 拉取嵌入模型 -服务启动后,需要在 Ollama 容器中拉取 nomic-embed-text 模型: +服务启动后,需要在 Ollama 容器中拉取嵌入模型。推荐使用 `nomic-embed-text-v2-moe`,对中文支持更好: ```bash # 进入 Ollama 容器拉取模型 -docker exec -it ai-ollama ollama pull nomic-embed-text +docker exec -it ai-ollama ollama pull toshk0/nomic-embed-text-v2-moe:Q6_K ``` -#### 5. 验证服务 +**可选模型**: + +| 模型 | 维度 | 说明 | +|------|------|------| +| `toshk0/nomic-embed-text-v2-moe:Q6_K` | 768 | 推荐,中文支持好,支持任务前缀 | +| `nomic-embed-text:v1.5` | 768 | 原版,支持任务前缀和 Matryoshka | +| `bge-large-zh` | 1024 | 中文专用,效果最好 | + +#### 5. 配置嵌入模型 + +访问前端管理界面,进入 **嵌入模型配置** 页面: + +1. 选择提供者:**Nomic Embed (优化版)** +2. 配置参数: + - **API 地址**:`http://ollama:11434`(Docker 环境)或 `http://localhost:11434`(本地开发) + - **模型名称**:`toshk0/nomic-embed-text-v2-moe:Q6_K` + - **向量维度**:`768` + - **Matryoshka 截断**:`true` +3. 点击 **保存配置** + +> **注意**: 使用 Nomic Embed (优化版) provider 可启用完整的 RAG 优化功能:任务前缀、Matryoshka 多向量、两阶段检索。 + +#### 6. 验证服务 ```bash # 检查服务状态 @@ -97,7 +119,7 @@ docker compose logs -f ai-service | grep "Default API Key" > **重要**: 后端首次启动时会自动生成一个默认 API Key,请从日志中复制该 Key,用于前端配置。 -#### 6. 配置前端 API Key +#### 7. 配置前端 API Key ```bash # 创建前端环境变量文件 diff --git a/ai-service-admin/vite.config.ts b/ai-service-admin/vite.config.ts index 6df2553..b25653b 100644 --- a/ai-service-admin/vite.config.ts +++ b/ai-service-admin/vite.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ port: 3000, proxy: { '/api': { - target: 'http://localhost:8000', + target: 'http://localhost:8088', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, diff --git a/ai-service/app/core/qdrant_client.py b/ai-service/app/core/qdrant_client.py index 5742b5a..85639d2 100644 --- a/ai-service/app/core/qdrant_client.py +++ b/ai-service/app/core/qdrant_client.py @@ -176,6 +176,7 @@ class QdrantClient: limit: int = 5, score_threshold: float | None = None, vector_name: str = "full", + with_vectors: bool = False, ) -> list[dict[str, Any]]: """ [AC-AISVC-10] Search vectors in tenant's collection. @@ -189,6 +190,7 @@ class QdrantClient: score_threshold: Minimum score threshold for results vector_name: Name of the vector to search (for multi-vector collections) Default is "full" for 768-dim vectors in Matryoshka setup. + with_vectors: Whether to return vectors in results (for two-stage reranking) """ client = await self.get_client() @@ -216,6 +218,7 @@ class QdrantClient: collection_name=collection_name, query_vector=(vector_name, query_vector), limit=limit, + with_vectors=with_vectors, ) except Exception as e: if "vector name" in str(e).lower() or "Not existing vector" in str(e): @@ -227,6 +230,7 @@ class QdrantClient: collection_name=collection_name, query_vector=query_vector, limit=limit, + with_vectors=with_vectors, ) else: raise @@ -235,15 +239,18 @@ class QdrantClient: f"[AC-AISVC-10] Collection {collection_name} returned {len(results)} raw results" ) - hits = [ - { + hits = [] + for result in results: + if score_threshold is not None and result.score < score_threshold: + continue + hit = { "id": str(result.id), "score": result.score, "payload": result.payload or {}, } - for result in results - if score_threshold is None or result.score >= score_threshold - ] + if with_vectors and result.vector: + hit["vector"] = result.vector + hits.append(hit) all_hits.extend(hits) if hits: diff --git a/ai-service/app/services/embedding/factory.py b/ai-service/app/services/embedding/factory.py index e42e506..d0e8ad6 100644 --- a/ai-service/app/services/embedding/factory.py +++ b/ai-service/app/services/embedding/factory.py @@ -74,11 +74,38 @@ class EmbeddingProviderFactory: "nomic": "Nomic-embed-text v1.5 优化版,支持任务前缀和 Matryoshka 维度截断,专为RAG优化", } + raw_schema = temp_instance.get_config_schema() + + properties = {} + required = [] + for key, field in raw_schema.items(): + properties[key] = { + "type": field.get("type", "string"), + "title": field.get("title", key), + "description": field.get("description", ""), + "default": field.get("default"), + } + if field.get("enum"): + properties[key]["enum"] = field.get("enum") + if field.get("minimum") is not None: + properties[key]["minimum"] = field.get("minimum") + if field.get("maximum") is not None: + properties[key]["maximum"] = field.get("maximum") + if field.get("required"): + required.append(key) + + config_schema = { + "type": "object", + "properties": properties, + } + if required: + config_schema["required"] = required + return { "name": name, "display_name": display_names.get(name, name), "description": descriptions.get(name, ""), - "config_schema": temp_instance.get_config_schema(), + "config_schema": config_schema, } @classmethod @@ -286,7 +313,7 @@ def get_embedding_config_manager() -> EmbeddingConfigManager: settings = get_settings() _embedding_config_manager = EmbeddingConfigManager( - default_provider="ollama", + default_provider="nomic", default_config={ "base_url": settings.ollama_base_url, "model": settings.ollama_embedding_model, diff --git a/ai-service/app/services/embedding/nomic_provider.py b/ai-service/app/services/embedding/nomic_provider.py index ba6a73b..cd19825 100644 --- a/ai-service/app/services/embedding/nomic_provider.py +++ b/ai-service/app/services/embedding/nomic_provider.py @@ -149,6 +149,7 @@ class NomicEmbeddingProvider(EmbeddingProvider): embedding_256 = self._truncate_and_normalize(embedding, 256) embedding_512 = self._truncate_and_normalize(embedding, 512) + embedding_full = self._truncate_and_normalize(embedding, len(embedding)) logger.debug( f"Generated Nomic embedding: task={task.value}, " @@ -156,7 +157,7 @@ class NomicEmbeddingProvider(EmbeddingProvider): ) return NomicEmbeddingResult( - embedding_full=embedding, + embedding_full=embedding_full, embedding_256=embedding_256, embedding_512=embedding_512, dimension=len(embedding), @@ -259,26 +260,31 @@ class NomicEmbeddingProvider(EmbeddingProvider): return { "base_url": { "type": "string", + "title": "API 地址", "description": "Ollama API 地址", "default": "http://localhost:11434", }, "model": { "type": "string", + "title": "模型名称", "description": "嵌入模型名称(推荐 nomic-embed-text v1.5)", "default": "nomic-embed-text", }, "dimension": { "type": "integer", + "title": "向量维度", "description": "向量维度(支持 256/512/768)", "default": 768, }, "timeout_seconds": { "type": "integer", + "title": "超时时间", "description": "请求超时时间(秒)", "default": 60, }, "enable_matryoshka": { "type": "boolean", + "title": "Matryoshka 截断", "description": "启用 Matryoshka 维度截断", "default": True, }, diff --git a/ai-service/app/services/embedding/ollama_provider.py b/ai-service/app/services/embedding/ollama_provider.py index 39093e6..c57b0a4 100644 --- a/ai-service/app/services/embedding/ollama_provider.py +++ b/ai-service/app/services/embedding/ollama_provider.py @@ -130,21 +130,25 @@ class OllamaEmbeddingProvider(EmbeddingProvider): return { "base_url": { "type": "string", + "title": "API 地址", "description": "Ollama API 地址", "default": "http://localhost:11434", }, "model": { "type": "string", + "title": "模型名称", "description": "嵌入模型名称", "default": "nomic-embed-text", }, "dimension": { "type": "integer", + "title": "向量维度", "description": "向量维度", "default": 768, }, "timeout_seconds": { "type": "integer", + "title": "超时时间", "description": "请求超时时间(秒)", "default": 60, }, diff --git a/ai-service/app/services/embedding/openai_provider.py b/ai-service/app/services/embedding/openai_provider.py index 0e15aab..31b4a00 100644 --- a/ai-service/app/services/embedding/openai_provider.py +++ b/ai-service/app/services/embedding/openai_provider.py @@ -159,28 +159,33 @@ class OpenAIEmbeddingProvider(EmbeddingProvider): return { "api_key": { "type": "string", + "title": "API 密钥", "description": "OpenAI API 密钥", "required": True, "secret": True, }, "model": { "type": "string", + "title": "模型名称", "description": "嵌入模型名称", "default": "text-embedding-3-small", "enum": list(self.MODEL_DIMENSIONS.keys()), }, "base_url": { "type": "string", + "title": "API 地址", "description": "OpenAI API 地址(支持兼容接口)", "default": "https://api.openai.com/v1", }, "dimension": { "type": "integer", + "title": "向量维度", "description": "向量维度(仅 text-embedding-3 系列支持自定义)", "default": 1536, }, "timeout_seconds": { "type": "integer", + "title": "超时时间", "description": "请求超时时间(秒)", "default": 60, }, diff --git a/ai-service/app/services/retrieval/optimized_retriever.py b/ai-service/app/services/retrieval/optimized_retriever.py index 74d6a21..02cd28d 100644 --- a/ai-service/app/services/retrieval/optimized_retriever.py +++ b/ai-service/app/services/retrieval/optimized_retriever.py @@ -84,7 +84,13 @@ class RRFCombiner: "bm25_rank": -1, "payload": result.get("payload", {}), "id": chunk_id, + "vector": result.get("vector"), } + else: + combined_scores[chunk_id]["vector_score"] = result.get("score", 0.0) + combined_scores[chunk_id]["vector_rank"] = rank + if result.get("vector"): + combined_scores[chunk_id]["vector"] = result.get("vector") combined_scores[chunk_id]["score"] += rrf_score @@ -101,6 +107,7 @@ class RRFCombiner: "bm25_rank": rank, "payload": result.get("payload", {}), "id": chunk_id, + "vector": result.get("vector"), } else: combined_scores[chunk_id]["bm25_score"] = result.get("score", 0.0) @@ -199,7 +206,15 @@ class OptimizedRetriever(BaseRetriever): f"dim_256={'available' if embedding_result.embedding_256 else 'not available'}" ) - if self._two_stage_enabled: + if self._two_stage_enabled and self._hybrid_enabled: + logger.info("[RAG-OPT] Using two-stage + hybrid retrieval strategy") + results = await self._two_stage_hybrid_retrieve( + ctx.tenant_id, + embedding_result, + ctx.query, + self._top_k, + ) + elif self._two_stage_enabled: logger.info("[RAG-OPT] Using two-stage retrieval strategy") results = await self._two_stage_retrieve( ctx.tenant_id, @@ -300,20 +315,27 @@ class OptimizedRetriever(BaseRetriever): stage1_start = time.perf_counter() candidates = await self._search_with_dimension( client, tenant_id, embedding_result.embedding_256, "dim_256", - top_k * self._two_stage_expand_factor + top_k * self._two_stage_expand_factor, + with_vectors=True, ) stage1_latency = (time.perf_counter() - stage1_start) * 1000 - logger.debug( + logger.info( f"[RAG-OPT] Stage 1: {len(candidates)} candidates in {stage1_latency:.2f}ms" ) stage2_start = time.perf_counter() reranked = [] for candidate in candidates: - stored_full_embedding = candidate.get("payload", {}).get("embedding_full", []) - if stored_full_embedding: - import numpy as np + vector_data = candidate.get("vector", {}) + stored_full_embedding = None + + if isinstance(vector_data, dict): + stored_full_embedding = vector_data.get("full", []) + elif isinstance(vector_data, list): + stored_full_embedding = vector_data + + if stored_full_embedding and len(stored_full_embedding) > 0: similarity = self._cosine_similarity( embedding_result.embedding_full, stored_full_embedding @@ -326,7 +348,7 @@ class OptimizedRetriever(BaseRetriever): results = reranked[:top_k] stage2_latency = (time.perf_counter() - stage2_start) * 1000 - logger.debug( + logger.info( f"[RAG-OPT] Stage 2: {len(results)} final results in {stage2_latency:.2f}ms" ) @@ -374,6 +396,92 @@ class OptimizedRetriever(BaseRetriever): return combined[:top_k] + async def _two_stage_hybrid_retrieve( + self, + tenant_id: str, + embedding_result: NomicEmbeddingResult, + query: str, + top_k: int, + ) -> list[dict[str, Any]]: + """ + Two-stage + Hybrid retrieval strategy. + + Stage 1: Fast retrieval with 256-dim vectors + BM25 in parallel + Stage 2: RRF fusion + Precise reranking with 768-dim vectors + + This combines the best of both worlds: + - Two-stage: Speed from 256-dim, precision from 768-dim reranking + - Hybrid: Semantic matching from vectors, keyword matching from BM25 + """ + import time + + client = await self._get_client() + + stage1_start = time.perf_counter() + + vector_task = self._search_with_dimension( + client, tenant_id, embedding_result.embedding_256, "dim_256", + top_k * self._two_stage_expand_factor, + with_vectors=True, + ) + + bm25_task = self._bm25_search(client, tenant_id, query, top_k * self._two_stage_expand_factor) + + vector_results, bm25_results = await asyncio.gather( + vector_task, bm25_task, return_exceptions=True + ) + + if isinstance(vector_results, Exception): + logger.warning(f"[RAG-OPT] Vector search failed: {vector_results}") + vector_results = [] + + if isinstance(bm25_results, Exception): + logger.warning(f"[RAG-OPT] BM25 search failed: {bm25_results}") + bm25_results = [] + + stage1_latency = (time.perf_counter() - stage1_start) * 1000 + logger.info( + f"[RAG-OPT] Two-stage Hybrid Stage 1: vector={len(vector_results)}, bm25={len(bm25_results)}, latency={stage1_latency:.2f}ms" + ) + + stage2_start = time.perf_counter() + + combined = self._rrf_combiner.combine( + vector_results, + bm25_results, + vector_weight=settings.rag_vector_weight, + bm25_weight=settings.rag_bm25_weight, + ) + + reranked = [] + for candidate in combined[:top_k * 2]: + vector_data = candidate.get("vector", {}) + stored_full_embedding = None + + if isinstance(vector_data, dict): + stored_full_embedding = vector_data.get("full", []) + elif isinstance(vector_data, list): + stored_full_embedding = vector_data + + if stored_full_embedding and len(stored_full_embedding) > 0: + similarity = self._cosine_similarity( + embedding_result.embedding_full, + stored_full_embedding + ) + candidate["score"] = similarity + candidate["stage"] = "two_stage_hybrid_reranked" + reranked.append(candidate) + + reranked.sort(key=lambda x: x.get("score", 0), reverse=True) + results = reranked[:top_k] + stage2_latency = (time.perf_counter() - stage2_start) * 1000 + + logger.info( + f"[RAG-OPT] Two-stage Hybrid Stage 2 (reranking): {len(results)} final results in {stage2_latency:.2f}ms" + ) + + return results + async def _vector_retrieve( self, tenant_id: str, @@ -393,12 +501,13 @@ class OptimizedRetriever(BaseRetriever): query_vector: list[float], vector_name: str, limit: int, + with_vectors: bool = False, ) -> list[dict[str, Any]]: """Search using specified vector dimension.""" try: logger.info( f"[RAG-OPT] Searching with vector_name={vector_name}, " - f"limit={limit}, vector_dim={len(query_vector)}" + f"limit={limit}, vector_dim={len(query_vector)}, with_vectors={with_vectors}" ) results = await client.search( @@ -406,6 +515,7 @@ class OptimizedRetriever(BaseRetriever): query_vector=query_vector, limit=limit, vector_name=vector_name, + with_vectors=with_vectors, ) logger.info( diff --git a/ai-service/scripts/cleanup_qdrant.py b/ai-service/scripts/cleanup_qdrant.py new file mode 100644 index 0000000..f84e9ef --- /dev/null +++ b/ai-service/scripts/cleanup_qdrant.py @@ -0,0 +1,89 @@ +""" +Script to cleanup Qdrant collections and data. +""" + +import asyncio +import logging +import sys + +sys.path.insert(0, "Q:\\agentProject\\ai-robot-core\\ai-service") + +from app.core.config import get_settings +from app.core.qdrant_client import get_qdrant_client + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def list_collections(): + """List all collections in Qdrant.""" + client = await get_qdrant_client() + qdrant = await client.get_client() + + collections = await qdrant.get_collections() + return [c.name for c in collections.collections] + + +async def delete_collection(collection_name: str): + """Delete a specific collection.""" + client = await get_qdrant_client() + qdrant = await client.get_client() + + try: + await qdrant.delete_collection(collection_name) + logger.info(f"Deleted collection: {collection_name}") + return True + except Exception as e: + logger.error(f"Failed to delete collection {collection_name}: {e}") + return False + + +async def delete_all_collections(): + """Delete all collections.""" + collections = await list_collections() + logger.info(f"Found {len(collections)} collections: {collections}") + + for name in collections: + await delete_collection(name) + + logger.info("All collections deleted") + + +async def delete_tenant_collection(tenant_id: str): + """Delete collection for a specific tenant.""" + client = await get_qdrant_client() + collection_name = client.get_collection_name(tenant_id) + + success = await delete_collection(collection_name) + if success: + logger.info(f"Deleted collection for tenant: {tenant_id}") + return success + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Cleanup Qdrant data") + parser.add_argument("--all", action="store_true", help="Delete all collections") + parser.add_argument("--tenant", type=str, help="Delete collection for specific tenant") + parser.add_argument("--list", action="store_true", help="List all collections") + + args = parser.parse_args() + + if args.list: + collections = asyncio.run(list_collections()) + print(f"Collections: {collections}") + elif args.all: + confirm = input("Are you sure you want to delete ALL collections? (yes/no): ") + if confirm.lower() == "yes": + asyncio.run(delete_all_collections()) + else: + print("Cancelled") + elif args.tenant: + confirm = input(f"Delete collection for tenant '{args.tenant}'? (yes/no): ") + if confirm.lower() == "yes": + asyncio.run(delete_tenant_collection(args.tenant)) + else: + print("Cancelled") + else: + parser.print_help() -- 2.40.1 From d660c19ab99c3dc06941bcc4ce1252cc38bb8eec Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 18:01:03 +0800 Subject: [PATCH 26/31] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=B5=8C?= =?UTF-8?q?=E5=85=A5=E9=85=8D=E7=BD=AE=E6=8C=81=E4=B9=85=E5=8C=96=E5=8F=8A?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=88=87=E6=8D=A2=E8=AD=A6=E5=91=8A=20[AC-AI?= =?UTF-8?q?SVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加嵌入配置持久化到config/embedding_config.json - 服务启动时自动加载保存的配置 - 切换模型时前端显示警告提示需要重新上传文档 - 修复OptimizedRetriever缓存问题,每次检索获取最新配置 - 清理调试用的Python临时文件 - 更新.gitignore忽略config目录 --- .gitignore | 1 + README.md | 5 ++- ai-service-admin/src/stores/embedding.ts | 3 +- .../src/views/admin/embedding/index.vue | 13 +++++- ai-service-admin/vite.config.ts | 2 +- ai-service/app/api/admin/embedding.py | 22 +++++++++- ai-service/app/services/embedding/factory.py | 43 +++++++++++++++++-- .../services/retrieval/optimized_retriever.py | 26 +++++------ 8 files changed, 91 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 232ea0c..02f7c61 100644 --- a/.gitignore +++ b/.gitignore @@ -162,5 +162,6 @@ cython_debug/ # Project specific ai-service/uploads/ +ai-service/config/ *.local diff --git a/README.md b/README.md index 96b84e5..8d1dcb0 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,10 @@ docker exec -it ai-ollama ollama pull toshk0/nomic-embed-text-v2-moe:Q6_K - **Matryoshka 截断**:`true` 3. 点击 **保存配置** -> **注意**: 使用 Nomic Embed (优化版) provider 可启用完整的 RAG 优化功能:任务前缀、Matryoshka 多向量、两阶段检索。 +> **注意**: +> - 使用 Nomic Embed (优化版) provider 可启用完整的 RAG 优化功能:任务前缀、Matryoshka 多向量、两阶段检索。 +> - 嵌入模型配置会持久化保存到 `ai-service/config/embedding_config.json`,服务重启后自动加载。 +> - **重要**: 切换嵌入模型后,需要删除现有知识库并重新上传文档,因为不同模型生成的向量不兼容。 #### 6. 验证服务 diff --git a/ai-service-admin/src/stores/embedding.ts b/ai-service-admin/src/stores/embedding.ts index 2cfb0ea..3c2af55 100644 --- a/ai-service-admin/src/stores/embedding.ts +++ b/ai-service-admin/src/stores/embedding.ts @@ -74,7 +74,8 @@ export const useEmbeddingStore = defineStore('embedding', () => { provider: currentConfig.value.provider, config: currentConfig.value.config } - await saveConfig(updateData) + const response = await saveConfig(updateData) + return response } catch (error) { console.error('Failed to save config:', error) throw error diff --git a/ai-service-admin/src/views/admin/embedding/index.vue b/ai-service-admin/src/views/admin/embedding/index.vue index e38ad24..8ca96d9 100644 --- a/ai-service-admin/src/views/admin/embedding/index.vue +++ b/ai-service-admin/src/views/admin/embedding/index.vue @@ -169,8 +169,19 @@ const handleSave = async () => { saving.value = true try { - await embeddingStore.saveCurrentConfig() + const response: any = await embeddingStore.saveCurrentConfig() ElMessage.success('配置保存成功') + + if (response?.warning || response?.requires_reindex) { + ElMessageBox.alert( + response.warning || '嵌入模型已更改,请重新上传文档以确保检索效果正常。', + '重要提示', + { + confirmButtonText: '我知道了', + type: 'warning', + } + ) + } } catch (error) { ElMessage.error('配置保存失败') } finally { diff --git a/ai-service-admin/vite.config.ts b/ai-service-admin/vite.config.ts index b25653b..6df2553 100644 --- a/ai-service-admin/vite.config.ts +++ b/ai-service-admin/vite.config.ts @@ -14,7 +14,7 @@ export default defineConfig({ port: 3000, proxy: { '/api': { - target: 'http://localhost:8088', + target: 'http://localhost:8000', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ''), }, diff --git a/ai-service/app/api/admin/embedding.py b/ai-service/app/api/admin/embedding.py index 206b27d..5937296 100644 --- a/ai-service/app/api/admin/embedding.py +++ b/ai-service/app/api/admin/embedding.py @@ -78,12 +78,32 @@ async def update_embedding_config( manager = get_embedding_config_manager() + old_config = manager.get_full_config() + old_provider = old_config.get("provider") + old_model = old_config.get("config", {}).get("model", "") + + new_model = config.get("model", "") + try: await manager.update_config(provider, config) - return { + + response = { "success": True, "message": f"Configuration updated to use {provider}", } + + if old_provider != provider or old_model != new_model: + response["warning"] = ( + "嵌入模型已更改。由于不同模型生成的向量不兼容," + "请删除现有知识库并重新上传文档,以确保检索效果正常。" + ) + response["requires_reindex"] = True + logger.warning( + f"[EMBEDDING] Model changed from {old_provider}/{old_model} to {provider}/{new_model}. " + f"Documents need to be re-uploaded." + ) + + return response except EmbeddingException as e: raise InvalidRequestException(str(e)) diff --git a/ai-service/app/services/embedding/factory.py b/ai-service/app/services/embedding/factory.py index d0e8ad6..90c7285 100644 --- a/ai-service/app/services/embedding/factory.py +++ b/ai-service/app/services/embedding/factory.py @@ -7,7 +7,9 @@ Design reference: progress.md Section 7.1 - Architecture - EmbeddingConfigManager: manages configuration with hot-reload support """ +import json import logging +from pathlib import Path from typing import Any, Type from app.services.embedding.base import EmbeddingException, EmbeddingProvider @@ -17,6 +19,8 @@ from app.services.embedding.nomic_provider import NomicEmbeddingProvider logger = logging.getLogger(__name__) +EMBEDDING_CONFIG_FILE = Path("config/embedding_config.json") + class EmbeddingProviderFactory: """ @@ -152,17 +156,46 @@ class EmbeddingProviderFactory: class EmbeddingConfigManager: """ Manager for embedding configuration. - [AC-AISVC-31] Supports hot-reload of configuration. + [AC-AISVC-31] Supports hot-reload of configuration with persistence. """ def __init__(self, default_provider: str = "ollama", default_config: dict[str, Any] | None = None): - self._provider_name = default_provider - self._config = default_config or { + self._default_provider = default_provider + self._default_config = default_config or { "base_url": "http://localhost:11434", "model": "nomic-embed-text", "dimension": 768, } + self._provider_name = default_provider + self._config = self._default_config.copy() self._provider: EmbeddingProvider | None = None + + self._load_from_file() + + def _load_from_file(self) -> None: + """Load configuration from file if exists.""" + try: + if EMBEDDING_CONFIG_FILE.exists(): + with open(EMBEDDING_CONFIG_FILE, 'r', encoding='utf-8') as f: + saved = json.load(f) + self._provider_name = saved.get("provider", self._default_provider) + self._config = saved.get("config", self._default_config.copy()) + logger.info(f"Loaded embedding config from file: provider={self._provider_name}") + except Exception as e: + logger.warning(f"Failed to load embedding config from file: {e}") + + def _save_to_file(self) -> None: + """Save configuration to file.""" + try: + EMBEDDING_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(EMBEDDING_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump({ + "provider": self._provider_name, + "config": self._config, + }, f, indent=2, ensure_ascii=False) + logger.info(f"Saved embedding config to file: provider={self._provider_name}") + except Exception as e: + logger.error(f"Failed to save embedding config to file: {e}") def get_provider_name(self) -> str: """Get current provider name.""" @@ -201,7 +234,7 @@ class EmbeddingConfigManager: ) -> bool: """ Update embedding configuration. - [AC-AISVC-31, AC-AISVC-40] Supports hot-reload. + [AC-AISVC-31, AC-AISVC-40] Supports hot-reload with persistence. Args: provider: New provider name @@ -229,6 +262,8 @@ class EmbeddingConfigManager: self._config = config self._provider = new_provider_instance + self._save_to_file() + logger.info(f"Updated embedding config: provider={provider}") return True diff --git a/ai-service/app/services/retrieval/optimized_retriever.py b/ai-service/app/services/retrieval/optimized_retriever.py index 02cd28d..2150552 100644 --- a/ai-service/app/services/retrieval/optimized_retriever.py +++ b/ai-service/app/services/retrieval/optimized_retriever.py @@ -138,7 +138,6 @@ class OptimizedRetriever(BaseRetriever): def __init__( self, qdrant_client: QdrantClient | None = None, - embedding_provider: NomicEmbeddingProvider | None = None, top_k: int | None = None, score_threshold: float | None = None, min_hits: int | None = None, @@ -148,7 +147,6 @@ class OptimizedRetriever(BaseRetriever): rrf_k: int | None = None, ): self._qdrant_client = qdrant_client - self._embedding_provider = embedding_provider self._top_k = top_k or settings.rag_top_k self._score_threshold = score_threshold or settings.rag_score_threshold self._min_hits = min_hits or settings.rag_min_hits @@ -164,19 +162,17 @@ class OptimizedRetriever(BaseRetriever): return self._qdrant_client async def _get_embedding_provider(self) -> NomicEmbeddingProvider: - if self._embedding_provider is None: - from app.services.embedding.factory import get_embedding_config_manager - manager = get_embedding_config_manager() - provider = await manager.get_provider() - if isinstance(provider, NomicEmbeddingProvider): - self._embedding_provider = provider - else: - self._embedding_provider = NomicEmbeddingProvider( - base_url=settings.ollama_base_url, - model=settings.ollama_embedding_model, - dimension=settings.qdrant_vector_size, - ) - return self._embedding_provider + from app.services.embedding.factory import get_embedding_config_manager + manager = get_embedding_config_manager() + provider = await manager.get_provider() + if isinstance(provider, NomicEmbeddingProvider): + return provider + else: + return NomicEmbeddingProvider( + base_url=settings.ollama_base_url, + model=settings.ollama_embedding_model, + dimension=settings.qdrant_vector_size, + ) async def retrieve(self, ctx: RetrievalContext) -> RetrievalResult: """ -- 2.40.1 From 15016d3448f00aa91232e467fe57e5d16ee108e1 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 19:05:58 +0800 Subject: [PATCH 27/31] =?UTF-8?q?fix:=20=E9=80=82=E9=85=8Dqdrant-client=20?= =?UTF-8?q?1.17.0=20API=E5=8F=98=E6=9B=B4=EF=BC=8Csearch=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E6=94=B9=E4=B8=BAquery=5Fpoints=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - qdrant-client 1.10+ 版本移除了 search() 方法,改用 query_points() - 使用 collection_exists() 替代 get_collections() 检查集合存在 - 更新返回结果处理:results.points 替代 results - 更新 pyproject.toml 版本约束为 >=1.9.0,<2.0.0 - 修正 README.md 中的 docker 命令示例 --- README.md | 4 ++-- ai-service/app/core/qdrant_client.py | 29 ++++++++++++++++------------ ai-service/pyproject.toml | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8d1dcb0..f001aee 100644 --- a/README.md +++ b/README.md @@ -114,10 +114,10 @@ docker exec -it ai-ollama ollama pull toshk0/nomic-embed-text-v2-moe:Q6_K ```bash # 检查服务状态 -docker compose ps +docker ps # 查看后端日志,找到自动生成的 API Key -docker compose logs -f ai-service | grep "Default API Key" +docker logs -f ai-service | grep "Default API Key" ``` > **重要**: 后端首次启动时会自动生成一个默认 API Key,请从日志中复制该 Key,用于前端配置。 diff --git a/ai-service/app/core/qdrant_client.py b/ai-service/app/core/qdrant_client.py index 85639d2..19b47f4 100644 --- a/ai-service/app/core/qdrant_client.py +++ b/ai-service/app/core/qdrant_client.py @@ -8,7 +8,7 @@ import logging from typing import Any from qdrant_client import AsyncQdrantClient -from qdrant_client.models import Distance, PointStruct, VectorParams, MultiVectorConfig +from qdrant_client.models import Distance, PointStruct, VectorParams, QueryRequest from app.core.config import get_settings @@ -61,8 +61,7 @@ class QdrantClient: collection_name = self.get_collection_name(tenant_id) try: - collections = await client.get_collections() - exists = any(c.name == collection_name for c in collections.collections) + exists = await client.collection_exists(collection_name) if not exists: if use_multi_vector: @@ -213,36 +212,42 @@ class QdrantClient: try: logger.info(f"[AC-AISVC-10] Searching in collection: {collection_name}") + exists = await client.collection_exists(collection_name) + if not exists: + logger.warning(f"[AC-AISVC-10] Collection {collection_name} does not exist") + continue + try: - results = await client.search( + results = await client.query_points( collection_name=collection_name, - query_vector=(vector_name, query_vector), + query=query_vector, + using=vector_name, limit=limit, with_vectors=with_vectors, + score_threshold=score_threshold, ) except Exception as e: - if "vector name" in str(e).lower() or "Not existing vector" in str(e): + if "vector name" in str(e).lower() or "Not existing vector" in str(e) or "using" in str(e).lower(): logger.info( f"[AC-AISVC-10] Collection {collection_name} doesn't have vector named '{vector_name}', " f"trying without vector name (single-vector mode)" ) - results = await client.search( + results = await client.query_points( collection_name=collection_name, - query_vector=query_vector, + query=query_vector, limit=limit, with_vectors=with_vectors, + score_threshold=score_threshold, ) else: raise logger.info( - f"[AC-AISVC-10] Collection {collection_name} returned {len(results)} raw results" + f"[AC-AISVC-10] Collection {collection_name} returned {len(results.points)} raw results" ) hits = [] - for result in results: - if score_threshold is not None and result.score < score_threshold: - continue + for result in results.points: hit = { "id": str(result.id), "score": result.score, diff --git a/ai-service/pyproject.toml b/ai-service/pyproject.toml index a11926f..c497c23 100644 --- a/ai-service/pyproject.toml +++ b/ai-service/pyproject.toml @@ -14,7 +14,7 @@ dependencies = [ "tenacity>=8.2.0", "sqlmodel>=0.0.14", "asyncpg>=0.29.0", - "qdrant-client>=1.7.0", + "qdrant-client>=1.9.0,<2.0.0", "tiktoken>=0.5.0", "openpyxl>=3.1.0", "python-docx>=1.1.0", -- 2.40.1 From f81d18a517e87875f269d454f53e297b2a3ab7d9 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 19:30:26 +0800 Subject: [PATCH 28/31] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0LLM=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=8C=81=E4=B9=85=E5=8C=96=E5=8A=9F=E8=83=BD=20[AC-AI?= =?UTF-8?q?SVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LLM配置保存到 config/llm_config.json 文件 - 服务重启后自动加载已保存的配置 - 与嵌入模型配置保持一致的持久化机制 --- ai-service/app/services/llm/factory.py | 43 +++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/ai-service/app/services/llm/factory.py b/ai-service/app/services/llm/factory.py index d983c47..25f1073 100644 --- a/ai-service/app/services/llm/factory.py +++ b/ai-service/app/services/llm/factory.py @@ -5,8 +5,10 @@ LLM Provider Factory and Configuration Management. Design pattern: Factory pattern for pluggable LLM providers. """ +import json import logging from dataclasses import dataclass, field +from pathlib import Path from typing import Any from app.services.llm.base import LLMClient, LLMConfig @@ -14,6 +16,8 @@ from app.services.llm.openai_client import OpenAIClient logger = logging.getLogger(__name__) +LLM_CONFIG_FILE = Path("config/llm_config.json") + @dataclass class LLMProviderInfo: @@ -257,7 +261,7 @@ class LLMProviderFactory: class LLMConfigManager: """ Manager for LLM configuration. - [AC-ASA-16, AC-ASA-17, AC-ASA-18] Configuration management with hot-reload. + [AC-ASA-16, AC-ASA-17, AC-ASA-18] Configuration management with hot-reload and persistence. """ def __init__(self): @@ -274,12 +278,41 @@ class LLMConfigManager: "temperature": settings.llm_temperature, } self._client: LLMClient | None = None + + self._load_from_file() + + def _load_from_file(self) -> None: + """Load configuration from file if exists.""" + try: + if LLM_CONFIG_FILE.exists(): + with open(LLM_CONFIG_FILE, 'r', encoding='utf-8') as f: + saved = json.load(f) + self._current_provider = saved.get("provider", self._current_provider) + saved_config = saved.get("config", {}) + if saved_config: + self._current_config.update(saved_config) + logger.info(f"[AC-ASA-16] Loaded LLM config from file: provider={self._current_provider}") + except Exception as e: + logger.warning(f"[AC-ASA-16] Failed to load LLM config from file: {e}") + + def _save_to_file(self) -> None: + """Save configuration to file.""" + try: + LLM_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(LLM_CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump({ + "provider": self._current_provider, + "config": self._current_config, + }, f, indent=2, ensure_ascii=False) + logger.info(f"[AC-ASA-16] Saved LLM config to file: provider={self._current_provider}") + except Exception as e: + logger.error(f"[AC-ASA-16] Failed to save LLM config to file: {e}") def get_current_config(self) -> dict[str, Any]: """Get current LLM configuration.""" return { "provider": self._current_provider, - "config": self._current_config, + "config": self._current_config.copy(), } async def update_config( @@ -289,7 +322,7 @@ class LLMConfigManager: ) -> bool: """ Update LLM configuration. - [AC-ASA-16] Hot-reload configuration. + [AC-ASA-16] Hot-reload configuration with persistence. Args: provider: Provider name @@ -310,6 +343,8 @@ class LLMConfigManager: self._current_provider = provider self._current_config = validated_config + + self._save_to_file() logger.info(f"[AC-ASA-16] LLM config updated: provider={provider}") return True @@ -365,7 +400,7 @@ class LLMConfigManager: test_provider = provider or self._current_provider test_config = config if config else self._current_config - logger.info(f"[AC-ASA-17] Test connection: provider={test_provider}, config={test_config}") + logger.info(f"[AC-ASA-17] Test connection: provider={test_provider}, model={test_config.get('model')}") if test_provider not in LLM_PROVIDERS: return { -- 2.40.1 From a9d107929433ec53672ac3fcda1b065e43ff4fbe Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 19:58:55 +0800 Subject: [PATCH 29/31] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=BC=96?= =?UTF-8?q?=E6=8E=92=E5=99=A8=E4=BD=BF=E7=94=A8=E9=94=99=E8=AF=AF=E7=9A=84?= =?UTF-8?q?LLM=E9=85=8D=E7=BD=AE=E9=97=AE=E9=A2=98=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除编排器中硬编码的LLMConfig创建 - 让LLM客户端使用自己的默认配置(从LLMConfigManager获取) - 修复流式生成方法同样的问题 --- ai-service/app/services/orchestrator.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/ai-service/app/services/orchestrator.py b/ai-service/app/services/orchestrator.py index 42d16dc..9b3d418 100644 --- a/ai-service/app/services/orchestrator.py +++ b/ai-service/app/services/orchestrator.py @@ -119,13 +119,7 @@ class OrchestratorService: max_evidence_tokens=getattr(settings, "rag_max_evidence_tokens", 2000), enable_rag=True, ) - self._llm_config = LLMConfig( - model=getattr(settings, "llm_model", "gpt-4o-mini"), - max_tokens=getattr(settings, "llm_max_tokens", 2048), - temperature=getattr(settings, "llm_temperature", 0.7), - timeout_seconds=getattr(settings, "llm_timeout_seconds", 30), - max_retries=getattr(settings, "llm_max_retries", 3), - ) + self._llm_config: LLMConfig | None = None async def generate( self, @@ -345,7 +339,6 @@ class OrchestratorService: try: ctx.llm_response = await self._llm_client.generate( messages=messages, - config=self._llm_config, ) ctx.diagnostics["llm_mode"] = "live" ctx.diagnostics["llm_model"] = ctx.llm_response.model @@ -627,7 +620,7 @@ class OrchestratorService: """ messages = self._build_llm_messages(ctx) - async for chunk in self._llm_client.stream_generate(messages, self._llm_config): + async for chunk in self._llm_client.stream_generate(messages): if not state_machine.can_send_message(): break -- 2.40.1 From 87de47a5dff757269ec5ff2e3a070e6b29150d62 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 20:04:22 +0800 Subject: [PATCH 30/31] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0ai-service?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=9B=AE=E5=BD=95volume=E6=8C=81=E4=B9=85?= =?UTF-8?q?=E5=8C=96=20[AC-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 ai_service_config volume 挂载到 /app/config - 解决重建容器时配置文件丢失的问题 --- docker-compose.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 760d5b2..ff157b2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -17,6 +17,8 @@ services: - AI_SERVICE_LLM_BASE_URL=${AI_SERVICE_LLM_BASE_URL:-https://api.openai.com/v1} - AI_SERVICE_LLM_MODEL=${AI_SERVICE_LLM_MODEL:-gpt-4o-mini} - AI_SERVICE_OLLAMA_BASE_URL=${AI_SERVICE_OLLAMA_BASE_URL:-http://ollama:11434} + volumes: + - ai_service_config:/app/config depends_on: postgres: condition: service_healthy @@ -103,3 +105,4 @@ volumes: postgres_data: qdrant_data: ollama_data: + ai_service_config: -- 2.40.1 From 0f1fa7de5c23f99c4fb53b9ee586059f6f02cee2 Mon Sep 17 00:00:00 2001 From: MerCry Date: Thu, 26 Feb 2026 20:05:39 +0800 Subject: [PATCH 31/31] =?UTF-8?q?chore:=20=E4=BF=AE=E6=94=B9=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E7=AB=AF=E5=8F=A3=E6=98=A0=E5=B0=84=E4=B8=BA8183=20[A?= =?UTF-8?q?C-AISVC-50]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index ff157b2..027448b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,7 +43,7 @@ services: container_name: ai-service-admin restart: unless-stopped ports: - - "8181:80" + - "8183:80" depends_on: - ai-service networks: -- 2.40.1