"""决策数据的读写与原子化保存。""" from __future__ import annotations import json import os from datetime import datetime from pathlib import Path from typing import Any from aide.decide.errors import DecideError from aide.decide.types import DecideInput, DecideOutput, DecisionRecord, MetaInfo from aide.flow.utils import now_task_id class DecideStorage: """管理 pending.json 与历史记录文件。""" def __init__(self, root: Path): self.root = root self.aide_dir = self.root / ".aide" self.decisions_dir = self.aide_dir / "decisions" self.pending_path = self.decisions_dir / "pending.json" def ensure_ready(self) -> None: if not self.aide_dir.exists(): raise DecideError(".aide 目录不存在,请先执行 aide init") self.decisions_dir.mkdir(parents=True, exist_ok=True) def save_pending(self, data: DecideInput) -> str: """保存待定项数据并生成 session_id。""" self.ensure_ready() session_id = now_task_id() created_at = datetime.now().astimezone().isoformat(timespec="seconds") meta = MetaInfo(created_at=created_at, session_id=session_id) payload = data.with_meta(meta) self._save_atomic(self.pending_path, payload.to_dict()) return session_id def load_pending(self) -> DecideInput | None: """读取 pending.json,若不存在返回 None。""" self.ensure_ready() if not self.pending_path.exists(): return None data = self._load_json(self.pending_path) return DecideInput.from_dict(data) def get_session_id(self) -> str | None: pending = self.load_pending() if pending is None: return None if pending.meta is None: raise DecideError("pending.json 缺少 _meta.session_id") return pending.meta.session_id def save_result(self, output: DecideOutput) -> None: """保存用户决策为历史记录。""" pending = self.load_pending() if pending is None: raise DecideError("未找到待定项数据,请先执行 aide decide submit ''") if pending.meta is None: raise DecideError("pending.json 缺少 _meta.session_id") record = DecisionRecord( input=pending.without_meta(), output=output, completed_at=datetime.now().astimezone().isoformat(timespec="seconds"), ) target = self.decisions_dir / f"{pending.meta.session_id}.json" self._save_atomic(target, record.to_dict()) def load_result(self) -> DecideOutput | None: """读取当前会话的决策结果。""" session_id = self.get_session_id() if session_id is None: return None record_path = self.decisions_dir / f"{session_id}.json" if not record_path.exists(): return None data = self._load_json(record_path) record = DecisionRecord.from_dict(data) return record.output def has_pending(self) -> bool: return self.pending_path.exists() def has_result(self) -> bool: session_id = None try: session_id = self.get_session_id() except DecideError: return False if not session_id: return False record_path = self.decisions_dir / f"{session_id}.json" return record_path.exists() def _save_atomic(self, path: Path, data: dict[str, Any]) -> None: payload = json.dumps(data, ensure_ascii=False, indent=2) + "\n" tmp_path = path.with_suffix(path.suffix + ".tmp") try: tmp_path.write_text(payload, encoding="utf-8") os.replace(tmp_path, path) except Exception as exc: raise DecideError(f"写入 {path.name} 失败: {exc}") def _load_json(self, path: Path) -> dict[str, Any]: try: raw = path.read_text(encoding="utf-8") data = json.loads(raw) if not isinstance(data, dict): raise DecideError(f"{path.name} 顶层必须为对象") return data except DecideError: raise except Exception as exc: raise DecideError(f"无法解析 {path.name}: {exc}") # ========== 服务状态管理 ========== @property def server_info_path(self) -> Path: """服务状态文件路径。""" return self.decisions_dir / "server.json" def save_server_info(self, pid: int, port: int, url: str) -> None: """保存后台服务信息。""" self.ensure_ready() info = { "pid": pid, "port": port, "url": url, "started_at": datetime.now().astimezone().isoformat(timespec="seconds"), } self._save_atomic(self.server_info_path, info) def load_server_info(self) -> dict[str, Any] | None: """读取服务信息,不存在返回 None。""" if not self.server_info_path.exists(): return None try: return self._load_json(self.server_info_path) except DecideError: return None def clear_server_info(self) -> None: """清理服务状态文件。""" if self.server_info_path.exists(): try: self.server_info_path.unlink() except OSError: pass def is_server_running(self) -> bool: """检查后台服务是否运行中。""" info = self.load_server_info() if not info or "pid" not in info: return False try: os.kill(info["pid"], 0) # 检查进程是否存在 return True except (ProcessLookupError, PermissionError, OSError): return False