118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
|
|
"""决策数据的读写与原子化保存。"""
|
|||
|
|
|
|||
|
|
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 '<json>'")
|
|||
|
|
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}")
|
|||
|
|
|