fix: 修复会话鉴权「登录后 token 无效」并补齐 NAS 部署流程

- deps.py: _create_redis_client 增加同步 ping 校验,Redis 不可达时正确降级为内存模式
- main.py: lifespan 复用 deps 的 UserStore 单例,避免 admin 与 API 请求实例不一致
- 前端: 强制改密弹窗(must_change_password 用户)、兼容新旧登录返回格式、markPasswordChanged
- 新增 NAS SSH 部署脚本与批处理测试;gitignore 前端构建产物
This commit is contained in:
2026-08-04 11:38:28 +08:00
parent 884cf78033
commit bdd30f0a88
20 changed files with 2036 additions and 54 deletions
+9 -10
View File
@@ -6,7 +6,6 @@ import structlog
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from redis import asyncio as redis_async
from app.api.response import ApiError, error
from app.api.v1.auth import router as auth_router
@@ -15,7 +14,7 @@ from app.api.v1.knowledge import router as knowledge_router
from app.api.v1.search import router as search_router
from app.api.v1.settings import router as settings_router
from app.config import settings
from app.core.users import UserStore, bootstrap_admin
from app.core.users import bootstrap_admin
from app.services.qdrant import QdrantService
logger = structlog.get_logger()
@@ -33,13 +32,13 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
except Exception:
logger.error("Qdrant 集合初始化失败,跳过初始化继续启动")
try:
# Redis 客户端创建失败(如 URL 非法)时传 None,UserStore 降级为内存模式
redis_client = redis_async.from_url(settings.redis_url, decode_responses=True)
except Exception:
redis_client = None
try:
# 复用 deps 的 UserStore 单例,确保 lifespan 创建的 admin 与 API 请求用的是同一实例
# _get_user_store 内部会检测 Redis 可达性,不可达时降级为内存模式
from app.api.deps import _get_user_store
user_store = _get_user_store()
# 空库引导默认管理员;明文密码由 bootstrap_admin 内部 warning 打印一次
await bootstrap_admin(UserStore(redis_client), logger)
await bootstrap_admin(user_store, logger)
except Exception:
logger.warning("默认管理员初始化失败,跳过", exc_info=True)
try:
@@ -150,8 +149,8 @@ async def admin_spa(rest: str):
_AGENT_SKILL_ZIP = _STATIC_DIR / "agent-skill" / "QMDSearch-Agent-Skill.zip"
@app.get("/agent-skill", include_in_schema=False)
async def agent_skill_download() -> FileResponse | JSONResponse:
@app.get("/agent-skill", include_in_schema=False, response_model=None)
async def agent_skill_download():
"""下载 AI Agent Skill 压缩包(qmdsearch-agent skill,含 SKILL.md 与示例)"""
if not _AGENT_SKILL_ZIP.is_file():
return JSONResponse(