Files
agent-aide/aide-program/aide/decide/storage.py

164 lines
5.7 KiB
Python
Raw Normal View History

2025-12-15 02:08:06 +08:00
"""决策数据的读写与原子化保存。"""
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:
2025-12-15 02:59:03 +08:00
raise DecideError("未找到待定项数据,请先执行 aide decide submit '<json>'")
2025-12-15 02:08:06 +08:00
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