v0.9.9
parent
ae09cf358e
commit
c3fccd2723
12
.env.example
12
.env.example
|
|
@ -46,8 +46,14 @@ STORAGE_PATH=./storage
|
||||||
# ==================== 管理员账号配置 ====================
|
# ==================== 管理员账号配置 ====================
|
||||||
# 初始管理员账号信息
|
# 初始管理员账号信息
|
||||||
ADMIN_USERNAME=admin
|
ADMIN_USERNAME=admin
|
||||||
ADMIN_PASSWORD=admin@123
|
ADMIN_PASSWORD=Admin@123456
|
||||||
ADMIN_EMAIL=admin@unisspace.com
|
ADMIN_EMAIL=admin@unisspace.com
|
||||||
ADMIN_NICKNAME=系统管理员
|
ADMIN_NICKNAME=系统管理员
|
||||||
# 开发环境跳过 SSL 证书验证(生产环境请勿开启)
|
# 仅当 LLM/Embedding 服务使用自签名证书时开启(生产环境保持 false)
|
||||||
DISABLE_SSL_VERIFY=true
|
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/
|
storage/
|
||||||
temp/
|
temp/
|
||||||
data/
|
data/
|
||||||
|
|
||||||
|
# Environment(避免把本地数据库/Redis 配置和明文密码打进镜像)
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ RUN set -eux; \
|
||||||
shared-mime-info \
|
shared-mime-info \
|
||||||
fontconfig \
|
fontconfig \
|
||||||
fonts-dejavu-core \
|
fonts-dejavu-core \
|
||||||
fonts-wqy-microhei; then \
|
fonts-wqy-microhei \
|
||||||
|
curl; then \
|
||||||
cp /tmp/debian.sources.bak /etc/apt/sources.list.d/debian.sources; \
|
cp /tmp/debian.sources.bak /etc/apt/sources.list.d/debian.sources; \
|
||||||
dpkg --configure -a || true; \
|
dpkg --configure -a || true; \
|
||||||
apt-get -f install -y || true; \
|
apt-get -f install -y || true; \
|
||||||
|
|
@ -48,7 +49,8 @@ RUN set -eux; \
|
||||||
shared-mime-info \
|
shared-mime-info \
|
||||||
fontconfig \
|
fontconfig \
|
||||||
fonts-dejavu-core \
|
fonts-dejavu-core \
|
||||||
fonts-wqy-microhei; \
|
fonts-wqy-microhei \
|
||||||
|
curl; \
|
||||||
fi; \
|
fi; \
|
||||||
rm -rf /var/lib/apt/lists/*
|
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.rag_service import rag_service
|
||||||
from app.services.zvec_service import zvec_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 app.services.project_vectorization_task_service import project_vectorization_task_service
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
@ -285,43 +284,16 @@ def _compact_cited_refs(answer: str, retrieved_docs):
|
||||||
return normalized_answer, refs
|
return normalized_answer, refs
|
||||||
|
|
||||||
|
|
||||||
async def _generate_session_title(db, llm_config_id: int, question: str, answer: str) -> Optional[str]:
|
MAX_SESSION_TITLE_LENGTH = 60
|
||||||
"""根据首轮问答,用 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
|
|
||||||
|
|
||||||
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 or "").strip().strip('"').strip("「」").strip()
|
||||||
title = title.splitlines()[0].strip() if title else ""
|
title = title.splitlines()[0].strip() if title else ""
|
||||||
if not title:
|
if not title:
|
||||||
return None
|
return "新对话"
|
||||||
return title[:60]
|
return title[:MAX_SESSION_TITLE_LENGTH]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/sessions", response_model=dict)
|
@router.post("/sessions", response_model=dict)
|
||||||
|
|
@ -347,7 +319,7 @@ async def create_chat_session(
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
project_id=req.project_id,
|
project_id=req.project_id,
|
||||||
llm_config_id=req.llm_config_id,
|
llm_config_id=req.llm_config_id,
|
||||||
title=req.title,
|
title=_normalize_session_title(req.title),
|
||||||
)
|
)
|
||||||
db.add(session)
|
db.add(session)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
@ -672,8 +644,6 @@ async def send_chat_message_stream(
|
||||||
user_message_id = query_message.id
|
user_message_id = query_message.id
|
||||||
assistant_message_id = assistant_message.id
|
assistant_message_id = assistant_message.id
|
||||||
|
|
||||||
# 是否首轮对话(生成会话标题用)
|
|
||||||
is_first_exchange = len(conversation_history) == 0
|
|
||||||
llm_config_id = session.llm_config_id
|
llm_config_id = session.llm_config_id
|
||||||
project_id = session.project_id
|
project_id = session.project_id
|
||||||
# 每累计这么多字符就回写一次占位行,平衡「刷新可见性」与「写库频率」
|
# 每累计这么多字符就回写一次占位行,平衡「刷新可见性」与「写库频率」
|
||||||
|
|
@ -682,7 +652,7 @@ async def send_chat_message_stream(
|
||||||
async def _persist_assistant(parts, *, completed):
|
async def _persist_assistant(parts, *, completed):
|
||||||
"""把已生成内容回写到占位的助手消息行。
|
"""把已生成内容回写到占位的助手消息行。
|
||||||
|
|
||||||
completed=True 时附带引用解析与标题生成;False 表示流式中途的增量回写。
|
completed=True 时附带引用解析;False 表示流式中途的增量回写。
|
||||||
"""
|
"""
|
||||||
assistant_response = "".join(parts)
|
assistant_response = "".join(parts)
|
||||||
cited_refs = []
|
cited_refs = []
|
||||||
|
|
@ -700,22 +670,8 @@ async def send_chat_message_stream(
|
||||||
json.dumps(cited_refs, ensure_ascii=False) if cited_refs else None
|
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()
|
await db.commit()
|
||||||
return assistant_response, references, new_title
|
return assistant_response, references
|
||||||
|
|
||||||
async def event_generator():
|
async def event_generator():
|
||||||
assistant_response_parts = []
|
assistant_response_parts = []
|
||||||
|
|
@ -746,24 +702,18 @@ async def send_chat_message_stream(
|
||||||
chars_since_flush = 0
|
chars_since_flush = 0
|
||||||
await _persist_assistant(assistant_response_parts, completed=False)
|
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
|
assistant_response_parts, completed=True
|
||||||
)
|
)
|
||||||
finished = True
|
finished = True
|
||||||
|
|
||||||
yield _stream_event("references", references or [])
|
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", {
|
yield _stream_event("done", {
|
||||||
"session_id": req.session_id,
|
"session_id": req.session_id,
|
||||||
"user_message_id": user_message_id,
|
"user_message_id": user_message_id,
|
||||||
"assistant_message_id": assistant_message_id,
|
"assistant_message_id": assistant_message_id,
|
||||||
"content": assistant_response,
|
"content": assistant_response,
|
||||||
"references": references or [],
|
"references": references or [],
|
||||||
"title": new_title,
|
|
||||||
})
|
})
|
||||||
except (asyncio.CancelledError, GeneratorExit):
|
except (asyncio.CancelledError, GeneratorExit):
|
||||||
# 客户端中途断开(如刷新页面):尽力把已生成的部分回写到占位行。
|
# 客户端中途断开(如刷新页面):尽力把已生成的部分回写到占位行。
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ services:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
volumes:
|
volumes:
|
||||||
- ${STORAGE_PATH:-./storage}/mysql:/var/lib/mysql
|
- ${STORAGE_PATH:-./storage}/mysql:/var/lib/mysql
|
||||||
- ./backend/scripts/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
||||||
ports:
|
ports:
|
||||||
- "${MYSQL_PORT:-3306}:3306"
|
- "${MYSQL_PORT:-3306}:3306"
|
||||||
command:
|
command:
|
||||||
|
|
@ -34,12 +33,14 @@ services:
|
||||||
command: redis-server --requirepass ${REDIS_PASSWORD:-redis_password_change_me} --appendonly yes
|
command: redis-server --requirepass ${REDIS_PASSWORD:-redis_password_change_me} --appendonly yes
|
||||||
environment:
|
environment:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
|
# 供容器内 redis-cli 健康检查使用,避免 NOAUTH 导致检查失效
|
||||||
|
REDISCLI_AUTH: ${REDIS_PASSWORD:-redis_password_change_me}
|
||||||
volumes:
|
volumes:
|
||||||
- ${STORAGE_PATH:-./storage}/redis:/data
|
- ${STORAGE_PATH:-./storage}/redis:/data
|
||||||
ports:
|
ports:
|
||||||
- "${REDIS_PORT:-6379}:6379"
|
- "${REDIS_PORT:-6379}:6379"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
|
test: ["CMD", "redis-cli", "--no-auth-warning", "ping"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
@ -63,6 +64,12 @@ services:
|
||||||
- REDIS_DB=${REDIS_DB:-8}
|
- REDIS_DB=${REDIS_DB:-8}
|
||||||
- SECRET_KEY=${SECRET_KEY:-your-secret-key-change-me-in-production}
|
- SECRET_KEY=${SECRET_KEY:-your-secret-key-change-me-in-production}
|
||||||
- DEBUG=${DEBUG:-false}
|
- 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_USERNAME=${ADMIN_USERNAME:-admin}
|
||||||
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-Admin@123456}
|
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-Admin@123456}
|
||||||
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
|
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com}
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ server {
|
||||||
|
|
||||||
# 超时设置
|
# 超时设置
|
||||||
proxy_connect_timeout 60s;
|
proxy_connect_timeout 60s;
|
||||||
proxy_send_timeout 60s;
|
proxy_send_timeout 300s;
|
||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 300s;
|
||||||
|
|
||||||
# 增加上传文件大小限制
|
# 增加上传文件大小限制
|
||||||
client_max_body_size 100M;
|
client_max_body_size 100M;
|
||||||
|
|
@ -41,6 +41,27 @@ server {
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
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 Streamable HTTP 反向代理
|
||||||
# 不对 /mcp 做 301/302 重定向,避免部分 MCP client 在跳转后丢失 POST body 或请求方法
|
# 不对 /mcp 做 301/302 重定向,避免部分 MCP client 在跳转后丢失 POST body 或请求方法
|
||||||
location = /mcp {
|
location = /mcp {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue