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:
@@ -20,7 +20,7 @@ from typing import Any
|
||||
import structlog
|
||||
|
||||
from app.config import Settings
|
||||
from app.core.dedup import DEDUP_KEY_PREFIX, get_dedup_strategy
|
||||
from app.core.dedup import get_dedup_strategy
|
||||
from app.core.ingestion import Ingester, IngestionError
|
||||
from app.models.document import DocumentInput
|
||||
from app.services.redis import RedisCache
|
||||
@@ -79,6 +79,7 @@ class IngestTaskManager:
|
||||
"""
|
||||
task_id = uuid.uuid4().hex
|
||||
now = _utc_now_iso()
|
||||
filename = self._extract_filename(doc)
|
||||
dedup = get_dedup_strategy(self._redis)
|
||||
|
||||
# 1. 去重命中:直接置 done,复用旧结果,不调 _run
|
||||
@@ -93,6 +94,7 @@ class IngestTaskManager:
|
||||
"updated_at": now,
|
||||
"result": result_dict,
|
||||
"error": None,
|
||||
"filename": filename,
|
||||
}
|
||||
self._schedule_mirror(task_id)
|
||||
logger.info(
|
||||
@@ -110,6 +112,7 @@ class IngestTaskManager:
|
||||
"updated_at": now,
|
||||
"result": None,
|
||||
"error": None,
|
||||
"filename": filename,
|
||||
}
|
||||
self._schedule_mirror(task_id)
|
||||
background = asyncio.create_task(self._run(task_id, doc, dedup))
|
||||
@@ -135,6 +138,72 @@ class IngestTaskManager:
|
||||
)
|
||||
return None
|
||||
|
||||
async def list_tasks(self, limit: int = 20) -> list[dict[str, Any]]:
|
||||
"""合并内存注册表与 Redis 镜像,去重,按 updated_at 降序,截断 limit
|
||||
|
||||
每项提取 {task_id, status, filename, created_at, updated_at, doc_id}:
|
||||
doc_id 从 done 任务的 result.document_id 提取,其余状态为 None。
|
||||
"""
|
||||
records: dict[str, dict[str, Any]] = {}
|
||||
# 1. 内存注册表(主)
|
||||
for task_id, record in self._tasks.items():
|
||||
records[task_id] = record
|
||||
# 2. Redis 镜像补充内存中没有的(内存未命中的任务,如重启后只存在 Redis 的历史记录)
|
||||
for task_id, record in await self._scan_redis_tasks():
|
||||
records.setdefault(task_id, record)
|
||||
# 3. 按 updated_at 降序,截断 limit
|
||||
sorted_records = sorted(
|
||||
records.values(),
|
||||
key=lambda r: r.get("updated_at") or "",
|
||||
reverse=True,
|
||||
)[:limit]
|
||||
# 4. 提取展示字段
|
||||
return [self._to_list_item(r) for r in sorted_records]
|
||||
|
||||
async def _scan_redis_tasks(self) -> list[tuple[str, dict[str, Any]]]:
|
||||
"""扫描 Redis 中 ingest_task:* 键,返回 (task_id, record) 列表
|
||||
|
||||
RedisCache 未暴露公开扫描接口,借道底层 _get_client().scan_iter;
|
||||
任何异常降级为空列表,不影响 list_tasks 主流程。
|
||||
"""
|
||||
if self._redis is None:
|
||||
return []
|
||||
get_client = getattr(self._redis, "_get_client", None)
|
||||
if not callable(get_client):
|
||||
return []
|
||||
try:
|
||||
client = get_client()
|
||||
tasks: list[tuple[str, dict[str, Any]]] = []
|
||||
async for key in client.scan_iter(match=f"{REDIS_KEY_PREFIX}*"):
|
||||
key_str = key if isinstance(key, str) else key.decode("utf-8", "replace")
|
||||
task_id = key_str[len(REDIS_KEY_PREFIX) :]
|
||||
record = await self._redis.get_json(key_str)
|
||||
if isinstance(record, dict):
|
||||
tasks.append((task_id, record))
|
||||
return tasks
|
||||
except Exception:
|
||||
logger.warning("扫描 Redis 任务键失败", exc_info=True)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _extract_filename(doc: DocumentInput) -> str | None:
|
||||
"""从文档输入提取展示用文件名:优先 metadata.original_filename,其次 title"""
|
||||
return doc.metadata.get("original_filename") or doc.title or None
|
||||
|
||||
@staticmethod
|
||||
def _to_list_item(record: dict[str, Any]) -> dict[str, Any]:
|
||||
"""从完整任务记录提取列表展示字段"""
|
||||
result = record.get("result")
|
||||
doc_id = result.get("document_id") if isinstance(result, dict) else None
|
||||
return {
|
||||
"task_id": record.get("task_id"),
|
||||
"status": record.get("status"),
|
||||
"filename": record.get("filename"),
|
||||
"created_at": record.get("created_at"),
|
||||
"updated_at": record.get("updated_at"),
|
||||
"doc_id": doc_id,
|
||||
}
|
||||
|
||||
async def wait_done(self, task_id: str, timeout: float = 30.0) -> dict[str, Any]:
|
||||
"""轮询内存注册表直到任务进入终态(done/failed)或超时
|
||||
|
||||
|
||||
Reference in New Issue
Block a user