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:
+143
-1
@@ -7,8 +7,8 @@ from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from app.config import Settings
|
||||
from app.core.dedup import DEDUP_KEY_PREFIX
|
||||
from app.core.ingest_tasks import (
|
||||
DEDUP_KEY_PREFIX,
|
||||
REDIS_KEY_PREFIX,
|
||||
IngestTaskManager,
|
||||
IngestTaskStatus,
|
||||
@@ -91,6 +91,19 @@ class FakeRedis:
|
||||
async def get_json(self, key: str) -> dict[str, Any] | None:
|
||||
return self.store.get(key)
|
||||
|
||||
def _get_client(self) -> Any:
|
||||
"""返回支持 scan_iter 的假客户端(供 list_tasks 扫描键)"""
|
||||
store = self.store
|
||||
|
||||
class _FakeClient:
|
||||
async def scan_iter(self, match: str = "*") -> Any:
|
||||
prefix = match[:-1] if match.endswith("*") else match
|
||||
for key in list(store.keys()):
|
||||
if key.startswith(prefix):
|
||||
yield key
|
||||
|
||||
return _FakeClient()
|
||||
|
||||
|
||||
async def test_submit_returns_immediately_and_completes() -> None:
|
||||
"""submit 立即返回;任务后台跑完为 done,结果完整,阶段序列齐全,Redis 镜像同步"""
|
||||
@@ -317,3 +330,132 @@ async def test_dedup_lookup_failure_falls_back_to_normal_pipeline() -> None:
|
||||
final = await manager.wait_done(task, timeout=5)
|
||||
assert final["status"] == IngestTaskStatus.DONE
|
||||
assert len(ingester.calls) == 1
|
||||
|
||||
|
||||
async def test_submit_records_filename_from_metadata_or_title() -> None:
|
||||
"""submit:filename 优先 metadata.original_filename,其次 title"""
|
||||
ingester = FakeIngester()
|
||||
manager = IngestTaskManager(ingester, None, Settings())
|
||||
|
||||
# 有 original_filename
|
||||
t1 = await manager.submit(
|
||||
DocumentInput(text="x", title="t1", metadata={"original_filename": "report.pdf"})
|
||||
)
|
||||
await manager.wait_done(t1, timeout=5)
|
||||
# 无 original_filename,回退 title
|
||||
t2 = await manager.submit(DocumentInput(text="y", title="my-title"))
|
||||
await manager.wait_done(t2, timeout=5)
|
||||
|
||||
items = await manager.list_tasks(limit=20)
|
||||
by_id = {it["task_id"]: it for it in items}
|
||||
assert by_id[t1]["filename"] == "report.pdf"
|
||||
assert by_id[t2]["filename"] == "my-title"
|
||||
|
||||
|
||||
async def test_list_tasks_merges_memory_and_redis_and_sorts() -> None:
|
||||
"""list_tasks:合并内存与 Redis 镜像,去重,按 updated_at 降序"""
|
||||
ingester = FakeIngester()
|
||||
redis = FakeRedis()
|
||||
manager = IngestTaskManager(ingester, redis, Settings())
|
||||
|
||||
# 内存任务 1
|
||||
t1 = await manager.submit(DocumentInput(text="内容A", title="t1"))
|
||||
await manager.wait_done(t1, timeout=5)
|
||||
# 内存任务 2
|
||||
t2 = await manager.submit(DocumentInput(text="内容B", title="t2"))
|
||||
await manager.wait_done(t2, timeout=5)
|
||||
|
||||
# Redis 镜像中独有任务(不在内存里,模拟重启后只存在 Redis 的历史记录)
|
||||
redis.store[f"{REDIS_KEY_PREFIX}redis-only-1"] = {
|
||||
"task_id": "redis-only-1",
|
||||
"status": "done",
|
||||
"filename": "legacy.md",
|
||||
"created_at": "2020-01-01T00:00:00+00:00",
|
||||
"updated_at": "2020-01-01T00:00:00+00:00",
|
||||
"result": {"document_id": "doc-legacy"},
|
||||
"error": None,
|
||||
}
|
||||
|
||||
items = await manager.list_tasks(limit=20)
|
||||
# 共 3 条(内存 2 + Redis 独有 1,内存的 2 也已镜像到 Redis 但按 task_id 去重)
|
||||
assert len(items) == 3
|
||||
# 按 updated_at 降序:内存任务(当前时间)排在 Redis 旧任务前
|
||||
ids = [it["task_id"] for it in items]
|
||||
assert "redis-only-1" in ids
|
||||
assert ids[-1] == "redis-only-1" # 最旧排最后
|
||||
# Redis 独有任务提取 doc_id 与 filename
|
||||
legacy = next(it for it in items if it["task_id"] == "redis-only-1")
|
||||
assert legacy["filename"] == "legacy.md"
|
||||
assert legacy["doc_id"] == "doc-legacy"
|
||||
# 内存 done 任务也提取 doc_id
|
||||
mem_item = next(it for it in items if it["task_id"] == t1)
|
||||
assert mem_item["doc_id"] == "doc-1"
|
||||
|
||||
|
||||
async def test_list_tasks_dedups_memory_and_redis_by_task_id() -> None:
|
||||
"""list_tasks:内存与 Redis 都有的同一 task_id 仅保留内存版本(去重)"""
|
||||
ingester = FakeIngester()
|
||||
redis = FakeRedis()
|
||||
manager = IngestTaskManager(ingester, redis, Settings())
|
||||
|
||||
task_id = await manager.submit(DocumentInput(text="唯一", title="t"))
|
||||
await manager.wait_done(task_id, timeout=5)
|
||||
|
||||
# Redis 镜像中给同一 task_id 篡改一个旧 status,验证内存版本胜出
|
||||
redis.store[f"{REDIS_KEY_PREFIX}{task_id}"]["status"] = "pending"
|
||||
|
||||
items = await manager.list_tasks(limit=20)
|
||||
assert len(items) == 1
|
||||
assert items[0]["task_id"] == task_id
|
||||
assert items[0]["status"] == IngestTaskStatus.DONE # 内存版本(done)
|
||||
|
||||
|
||||
async def test_list_tasks_respects_limit() -> None:
|
||||
"""list_tasks:limit 截断返回条数"""
|
||||
ingester = FakeIngester()
|
||||
manager = IngestTaskManager(ingester, None, Settings())
|
||||
for i in range(5):
|
||||
t = await manager.submit(DocumentInput(text=f"c{i}", title=f"t{i}"))
|
||||
await manager.wait_done(t, timeout=5)
|
||||
|
||||
items = await manager.list_tasks(limit=3)
|
||||
assert len(items) == 3
|
||||
|
||||
|
||||
async def test_list_tasks_without_redis_returns_memory_only() -> None:
|
||||
"""Redis 不可用:list_tasks 仅返回内存任务"""
|
||||
ingester = FakeIngester()
|
||||
manager = IngestTaskManager(ingester, None, Settings())
|
||||
t1 = await manager.submit(DocumentInput(text="x", title="t1"))
|
||||
await manager.wait_done(t1, timeout=5)
|
||||
|
||||
items = await manager.list_tasks(limit=20)
|
||||
assert len(items) == 1
|
||||
assert items[0]["task_id"] == t1
|
||||
assert items[0]["filename"] == "t1"
|
||||
assert items[0]["doc_id"] == "doc-1"
|
||||
|
||||
|
||||
async def test_list_tasks_extract_fields_for_in_progress_task() -> None:
|
||||
"""进行中任务:doc_id 为 None(done 时才从 result.document_id 提取)"""
|
||||
ingester = FakeIngester()
|
||||
ingester.gate = asyncio.Event() # 阻塞任务使其停留在最后一个阶段
|
||||
manager = IngestTaskManager(ingester, None, Settings(ingest_max_concurrency=1))
|
||||
|
||||
t1 = await manager.submit(
|
||||
DocumentInput(text="x", title="t1", metadata={"original_filename": "a.txt"})
|
||||
)
|
||||
await asyncio.wait_for(ingester.started.wait(), timeout=1)
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
items = await manager.list_tasks(limit=20)
|
||||
assert len(items) == 1
|
||||
item = items[0]
|
||||
assert item["task_id"] == t1
|
||||
# 任务已推进到 writing 阶段(gate 阻塞前最后一个 progress_cb)
|
||||
assert item["status"] == IngestTaskStatus.WRITING
|
||||
assert item["doc_id"] is None # 未完成,doc_id 为 None
|
||||
assert item["filename"] == "a.txt"
|
||||
|
||||
ingester.gate.set()
|
||||
await manager.wait_done(t1, timeout=5)
|
||||
|
||||
Reference in New Issue
Block a user