""" 微盛存档任务壳:独立 Stream,与 LibreOffice 槽隔离。 本文件职责:enqueue / process_stub;附件落对象存储后续再接。 群消息轮询不在本 Stream,见 jobs/archive_poll(Worker HTTP 槽拉微盛)。 禁止:与 soffice 共用阻塞线;禁止把存档成功冒充业务成交成功。 """ from __future__ import annotations import logging from dataclasses import dataclass from typing import Any, Optional from agent.redis_coord import get_redis_runtime, try_acquire_idempotency from agent.redis_coord.keys import STREAM_ARCHIVE from agent.redis_coord.runtime import RedisRuntime from agent.redis_coord.stream_queue import StreamQueue logger = logging.getLogger(__name__) @dataclass class ArchiveEnqueueResult: accepted: bool entry_id: str = "" reason: str = "" def archive_stream(redis: Optional[RedisRuntime] = None) -> StreamQueue: """存档 Stream(懒创建消费组由调用方 ensure)。""" rt = redis or get_redis_runtime() return StreamQueue(client=rt.client, stream_suffix=STREAM_ARCHIVE) def enqueue_archive( *, payload: dict[str, Any], idempotency_key: str = "", redis: Optional[RedisRuntime] = None, ) -> ArchiveEnqueueResult: """存档任务入队(快回)。""" rt = redis or get_redis_runtime() q = archive_stream(rt) q.ensure_group() if idempotency_key: ok = try_acquire_idempotency( rt.client, scope="archive", idempotency_key=idempotency_key, ttl_seconds=600, ) if not ok: return ArchiveEnqueueResult(accepted=False, reason="duplicate_idempotency_key") entry_id = q.enqueue(payload=payload, idempotency_key=idempotency_key) logger.info("archive.enqueue id=%s", entry_id) return ArchiveEnqueueResult(accepted=True, entry_id=entry_id) def process_archive_stub(data: dict[str, Any]) -> dict[str, Any]: """存档占位:不访问微盛、不写对象存储。""" logger.info("archive.stub keys=%s", sorted(data.keys())) return {"ok": True, "stub": True, "note": "存档壳:未下载未登记"}