diff --git a/.env.example b/.env.example index 534b4bc..6c5265b 100644 --- a/.env.example +++ b/.env.example @@ -46,8 +46,14 @@ STORAGE_PATH=./storage # ==================== 管理员账号配置 ==================== # 初始管理员账号信息 ADMIN_USERNAME=admin -ADMIN_PASSWORD=admin@123 +ADMIN_PASSWORD=Admin@123456 ADMIN_EMAIL=admin@unisspace.com ADMIN_NICKNAME=系统管理员 -# 开发环境跳过 SSL 证书验证(生产环境请勿开启) -DISABLE_SSL_VERIFY=true +# 仅当 LLM/Embedding 服务使用自签名证书时开启(生产环境保持 false) +DISABLE_SSL_VERIFY=false + +# ==================== ZVec 向量化配置 ==================== +# 向量数据目录(留空时使用 STORAGE_PATH 下的 vector_index,一般无需设置) +ZVEC_DATA_DIR= +# Embedding 向量维度(需与 llm_model_config 中选择的 embedding 模型一致) +ZVEC_EMBEDDING_DIM=1536 diff --git a/backend/.dockerignore b/backend/.dockerignore index 49e3500..ef7d5cd 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -44,3 +44,7 @@ Thumbs.db storage/ temp/ data/ + +# Environment(避免把本地数据库/Redis 配置和明文密码打进镜像) +.env +.env.* diff --git a/backend/Dockerfile b/backend/Dockerfile index 2e5b295..7669e5f 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -34,7 +34,8 @@ RUN set -eux; \ shared-mime-info \ fontconfig \ fonts-dejavu-core \ - fonts-wqy-microhei; then \ + fonts-wqy-microhei \ + curl; then \ cp /tmp/debian.sources.bak /etc/apt/sources.list.d/debian.sources; \ dpkg --configure -a || true; \ apt-get -f install -y || true; \ @@ -48,7 +49,8 @@ RUN set -eux; \ shared-mime-info \ fontconfig \ fonts-dejavu-core \ - fonts-wqy-microhei; \ + fonts-wqy-microhei \ + curl; \ fi; \ rm -rf /var/lib/apt/lists/* diff --git a/backend/app/api/v1/chat.py b/backend/app/api/v1/chat.py index a474ab1..5dd32d3 100644 --- a/backend/app/api/v1/chat.py +++ b/backend/app/api/v1/chat.py @@ -26,7 +26,6 @@ from app.services.project_service import ( ) from app.services.rag_service import rag_service from app.services.zvec_service import zvec_service -from app.services.llm_provider_service import LLMProviderService from app.services.project_vectorization_task_service import project_vectorization_task_service from pydantic import BaseModel @@ -285,43 +284,16 @@ def _compact_cited_refs(answer: str, retrieved_docs): return normalized_answer, refs -async def _generate_session_title(db, llm_config_id: int, question: str, answer: str) -> Optional[str]: - """根据首轮问答,用 LLM 生成简洁的会话标题。失败时返回 None。""" - stmt = select(LLMModelConfig).where(LLMModelConfig.config_id == llm_config_id) - result = await db.execute(stmt) - llm_config = result.scalar_one_or_none() - if not llm_config: - return None +MAX_SESSION_TITLE_LENGTH = 60 - system_prompt = ( - "你是一个会话标题生成器。请根据用户的问题和助手的回答," - "用一句不超过 16 个字的中文短语概括对话主题,作为标题。" - "只输出标题本身,不要引号、标点结尾或任何解释。" - ) - user_content = f"用户问题:{question}\n\n助手回答:{(answer or '')[:500]}" - - try: - title = await LLMProviderService.generate_text( - provider=llm_config.provider, - endpoint_url=llm_config.endpoint_url, - api_key=llm_config.api_key, - llm_model_name=llm_config.llm_model_name, - timeout=min(int(llm_config.llm_timeout or 60), 60), - temperature=0.3, - top_p=float(llm_config.llm_top_p or 0.9), - max_tokens=32, - system_prompt=system_prompt, - messages=[{"role": "user", "content": user_content}], - ) - except Exception as exc: # noqa: BLE001 - 标题生成失败不应影响主流程 - logger.warning("生成会话标题失败: %s", exc) - return None +def _normalize_session_title(title: Optional[str]) -> str: + """会话标题统一处理:取首行、去掉多余引号与空白,超长截断,空值回退。""" title = (title or "").strip().strip('"').strip("「」").strip() title = title.splitlines()[0].strip() if title else "" if not title: - return None - return title[:60] + return "新对话" + return title[:MAX_SESSION_TITLE_LENGTH] @router.post("/sessions", response_model=dict) @@ -347,7 +319,7 @@ async def create_chat_session( user_id=current_user.id, project_id=req.project_id, llm_config_id=req.llm_config_id, - title=req.title, + title=_normalize_session_title(req.title), ) db.add(session) await db.commit() @@ -672,8 +644,6 @@ async def send_chat_message_stream( user_message_id = query_message.id assistant_message_id = assistant_message.id - # 是否首轮对话(生成会话标题用) - is_first_exchange = len(conversation_history) == 0 llm_config_id = session.llm_config_id project_id = session.project_id # 每累计这么多字符就回写一次占位行,平衡「刷新可见性」与「写库频率」 @@ -682,7 +652,7 @@ async def send_chat_message_stream( async def _persist_assistant(parts, *, completed): """把已生成内容回写到占位的助手消息行。 - completed=True 时附带引用解析与标题生成;False 表示流式中途的增量回写。 + completed=True 时附带引用解析;False 表示流式中途的增量回写。 """ assistant_response = "".join(parts) cited_refs = [] @@ -700,22 +670,8 @@ async def send_chat_message_stream( json.dumps(cited_refs, ensure_ascii=False) if cited_refs else None ) - new_title = None - if completed and is_first_exchange: - new_title = await _generate_session_title( - db, llm_config_id, question, assistant_response - ) - # LLM 生成失败时回退为截断后的问题,确保侧边栏拿到有意义的标题 - if not new_title: - fallback = (question or "").strip().splitlines()[0] if question else "" - new_title = (fallback[:24] + "...") if len(fallback) > 24 else fallback - if new_title: - sess = await db.get(ChatSession, req.session_id) - if sess is not None: - sess.title = new_title - await db.commit() - return assistant_response, references, new_title + return assistant_response, references async def event_generator(): assistant_response_parts = [] @@ -746,24 +702,18 @@ async def send_chat_message_stream( chars_since_flush = 0 await _persist_assistant(assistant_response_parts, completed=False) - assistant_response, references, new_title = await _persist_assistant( + assistant_response, references = await _persist_assistant( assistant_response_parts, completed=True ) finished = True yield _stream_event("references", references or []) - if new_title: - yield _stream_event("title", { - "session_id": req.session_id, - "title": new_title, - }) yield _stream_event("done", { "session_id": req.session_id, "user_message_id": user_message_id, "assistant_message_id": assistant_message_id, "content": assistant_response, "references": references or [], - "title": new_title, }) except (asyncio.CancelledError, GeneratorExit): # 客户端中途断开(如刷新页面):尽力把已生成的部分回写到占位行。 diff --git a/docker-compose.yml b/docker-compose.yml index 04a45e6..9122d47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,7 +14,6 @@ services: TZ: Asia/Shanghai volumes: - ${STORAGE_PATH:-./storage}/mysql:/var/lib/mysql - - ./backend/scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro ports: - "${MYSQL_PORT:-3306}:3306" command: @@ -34,12 +33,14 @@ services: command: redis-server --requirepass ${REDIS_PASSWORD:-redis_password_change_me} --appendonly yes environment: TZ: Asia/Shanghai + # 供容器内 redis-cli 健康检查使用,避免 NOAUTH 导致检查失效 + REDISCLI_AUTH: ${REDIS_PASSWORD:-redis_password_change_me} volumes: - ${STORAGE_PATH:-./storage}/redis:/data ports: - "${REDIS_PORT:-6379}:6379" healthcheck: - test: ["CMD", "redis-cli", "--raw", "incr", "ping"] + test: ["CMD", "redis-cli", "--no-auth-warning", "ping"] interval: 10s timeout: 5s retries: 5 @@ -63,6 +64,12 @@ services: - REDIS_DB=${REDIS_DB:-8} - SECRET_KEY=${SECRET_KEY:-your-secret-key-change-me-in-production} - DEBUG=${DEBUG:-false} + - STORAGE_ROOT=/data/nex_docus_store + - CHUNK_SIZE=${CHUNK_SIZE:-800} + - CHUNK_OVERLAP=${CHUNK_OVERLAP:-150} + - DISABLE_SSL_VERIFY=${DISABLE_SSL_VERIFY:-false} + - ZVEC_DATA_DIR=${ZVEC_DATA_DIR:-} + - ZVEC_EMBEDDING_DIM=${ZVEC_EMBEDDING_DIM:-1536} - ADMIN_USERNAME=${ADMIN_USERNAME:-admin} - ADMIN_PASSWORD=${ADMIN_PASSWORD:-Admin@123456} - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 04d5e7a..d5629ea 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -25,8 +25,8 @@ server { # 超时设置 proxy_connect_timeout 60s; - proxy_send_timeout 60s; - proxy_read_timeout 60s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; # 增加上传文件大小限制 client_max_body_size 100M; @@ -41,6 +41,27 @@ server { add_header Cache-Control "no-cache, no-store, must-revalidate"; } + # 聊天流式响应(SSE):LLM 生成耗时较长,需要更长的读写超时 + location /api/v1/chat/send/stream { + proxy_pass http://backend:8000/api/v1/chat/send/stream; + 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_http_version 1.1; + proxy_set_header Connection ""; + + proxy_buffering off; + proxy_request_buffering off; + + proxy_connect_timeout 60s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + # MCP Streamable HTTP 反向代理 # 不对 /mcp 做 301/302 重定向,避免部分 MCP client 在跳转后丢失 POST body 或请求方法 location = /mcp {