main
mula.liu 2026-08-04 16:40:13 +08:00
parent ae09cf358e
commit c3fccd2723
6 changed files with 58 additions and 68 deletions

View File

@ -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

View File

@ -44,3 +44,7 @@ Thumbs.db
storage/
temp/
data/
# Environment避免把本地数据库/Redis 配置和明文密码打进镜像)
.env
.env.*

View File

@ -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/*

View File

@ -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):
# 客户端中途断开(如刷新页面):尽力把已生成的部分回写到占位行。

View File

@ -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}

View File

@ -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";
}
# 聊天流式响应SSELLM 生成耗时较长,需要更长的读写超时
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 {