v0.9.9
parent
ae09cf358e
commit
c3fccd2723
12
.env.example
12
.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
|
||||
|
|
|
|||
|
|
@ -44,3 +44,7 @@ Thumbs.db
|
|||
storage/
|
||||
temp/
|
||||
data/
|
||||
|
||||
# Environment(避免把本地数据库/Redis 配置和明文密码打进镜像)
|
||||
.env
|
||||
.env.*
|
||||
|
|
|
|||
|
|
@ -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/*
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
# 客户端中途断开(如刷新页面):尽力把已生成的部分回写到占位行。
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue