feat: 对decide的功能进行调整

This commit is contained in:
2025-12-15 04:01:12 +08:00
parent ed7c45b48e
commit 3b07a8160a
11 changed files with 284 additions and 71 deletions

View File

@@ -1,9 +1,8 @@
"""aide decide 模块入口。"""
from .cli import cmd_decide, cmd_decide_result, cmd_decide_submit
from .cli import cmd_decide_result, cmd_decide_submit
__all__ = [
"cmd_decide",
"cmd_decide_submit",
"cmd_decide_result",
]

View File

@@ -3,57 +3,90 @@
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
from aide.core import output
from aide.decide.errors import DecideError
from aide.decide.server import DecideServer
from aide.decide.storage import DecideStorage
from aide.decide.types import DecideInput
def cmd_decide(args) -> bool:
"""aide decide 统一入口"""
if getattr(args, "data", None) == "result":
return cmd_decide_result()
if getattr(args, "data", None) is None:
_print_error("缺少参数: 需要传入 JSON 数据或 result")
return False
return cmd_decide_submit(args.data)
def cmd_decide_submit(json_data: str) -> bool:
"""提交待定项并启动 Web 服务。"""
def cmd_decide_submit(file_path: str) -> bool:
"""从文件读取数据,启动后台 Web 服务"""
root = Path.cwd()
storage = DecideStorage(root)
# 1. 读取 JSON 文件
json_file = Path(file_path)
if not json_file.is_absolute():
json_file = root / json_file
if not json_file.exists():
_print_error(f"文件不存在: {file_path}")
return False
try:
raw = json.loads(json_data)
raw = json.loads(json_file.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
_print_error(f"JSON 解析失败: {exc}", "检查 JSON 格式是否正确")
return False
# 2. 验证数据格式
try:
decide_input = DecideInput.from_dict(raw)
except DecideError as exc:
_print_error(f"数据验证失败: {exc}", "检查必填字段是否完整")
return False
# 3. 保存到 pending.json
try:
storage.save_pending(decide_input)
except DecideError as exc:
_print_error(str(exc))
return False
server = DecideServer(root, storage)
return server.start()
# 4. 启动后台服务
return _start_daemon(root, storage)
def _start_daemon(root: Path, storage: DecideStorage) -> bool:
"""启动后台服务进程。"""
# 启动 daemon 进程
daemon_module = "aide.decide.daemon"
try:
subprocess.Popen(
[sys.executable, "-m", daemon_module, str(root)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True, # 脱离父进程
)
except Exception as exc:
_print_error(f"启动后台服务失败: {exc}")
return False
# 等待服务启动(检查状态文件)
for _ in range(50): # 最多等待 5 秒
time.sleep(0.1)
info = storage.load_server_info()
if info and "url" in info:
output.info("Web 服务已启动")
output.info(f"请访问: {info['url']}")
output.info("用户完成决策后执行 aide decide result 获取结果")
return True
_print_error("服务启动超时", "请检查端口是否被占用")
return False
def cmd_decide_result() -> bool:
"""读取最新决策结果并输出 JSON"""
"""获取决策结果(服务在用户提交后自动关闭)"""
root = Path.cwd()
storage = DecideStorage(root)
# 检查 pending
try:
pending = storage.load_pending()
except DecideError as exc:
@@ -61,14 +94,15 @@ def cmd_decide_result() -> bool:
return False
if pending is None:
_print_error("未找到待定项数据", "请先执行 aide decide submit '<json>'")
_print_error("未找到待定项数据", "请先执行 aide decide submit <file>")
return False
session_id = pending.meta.session_id if pending.meta else None
if not session_id:
_print_error("决策结果已过期", "pending.json 已被更新,请重新执行 aide decide submit '<json>'")
_print_error("数据异常", "pending.json 缺少 session_id,请重新执行 aide decide submit")
return False
# 检查结果
try:
result = storage.load_result()
except DecideError as exc:
@@ -76,20 +110,20 @@ def cmd_decide_result() -> bool:
return False
if result is None:
has_history = any(
path.is_file()
and path.name.endswith(".json")
and path.name != "pending.json"
for path in storage.decisions_dir.glob("*.json")
)
if has_history:
_print_error("决策结果已过期", "pending.json 已被更新,请重新执行 aide decide submit '<json>'")
else:
# 检查服务是否还在运行
if storage.is_server_running():
_print_error("尚无决策结果", "请等待用户在 Web 界面完成操作")
else:
# 服务已关闭但没有结果,可能是超时或异常
_print_error("尚无决策结果", "服务可能已超时关闭,请重新执行 aide decide submit")
return False
# 输出结果
payload = json.dumps(result.to_dict(), ensure_ascii=False, separators=(",", ":"))
print(payload)
# 清理服务状态文件(如存在)
storage.clear_server_info()
return True

View File

@@ -0,0 +1,47 @@
"""后台服务入口点。
通过 subprocess 启动,独立运行 HTTP 服务。
用法: python -m aide.decide.daemon <project_root>
"""
from __future__ import annotations
import os
import signal
import sys
from pathlib import Path
def main() -> int:
if len(sys.argv) < 2:
sys.stderr.write("用法: python -m aide.decide.daemon <project_root>\n")
return 1
root = Path(sys.argv[1])
if not root.exists():
sys.stderr.write(f"目录不存在: {root}\n")
return 1
# 延迟导入,避免循环依赖
from aide.decide.server import DecideServer
from aide.decide.storage import DecideStorage
storage = DecideStorage(root)
server = DecideServer(root, storage)
# 注册信号处理
def handle_sigterm(signum: int, frame: object) -> None:
server.stop("terminated")
signal.signal(signal.SIGTERM, handle_sigterm)
# 获取当前进程 PID
pid = os.getpid()
# 启动服务(会保存 server.json阻塞等待
success = server.start_daemon(pid)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -95,6 +95,51 @@ class DecideServer:
self.should_close = True
self.close_reason = reason
def start_daemon(self, pid: int) -> bool:
"""作为后台进程启动服务(由 daemon.py 调用)。
与 start() 的区别:
- 不输出到 stdout后台运行
- 保存服务信息到 server.json
- 退出时清理 server.json
"""
try:
config = ConfigManager(self.root).load_config()
start_port = _get_int(config, "decide", "port", default=3721)
self.timeout = _get_int(config, "decide", "timeout", default=0)
self.bind = _get_str(config, "decide", "bind", default="127.0.0.1")
self.url = _get_str(config, "decide", "url", default="")
available = self._find_available_port(start_port)
if available is None:
return False
self.port = available
handlers = DecideHandlers(
storage=self.storage,
web_dir=self.web_dir,
stop_callback=self.stop,
)
RequestHandler = self._build_request_handler(handlers)
self.httpd = DecideHTTPServer((self.bind, self.port), RequestHandler, handlers)
self.httpd.timeout = 1.0
# 生成访问地址
access_url = self.url if self.url else f"http://localhost:{self.port}"
# 保存服务信息(供 CLI 读取)
self.storage.save_server_info(pid, self.port, access_url)
# 阻塞等待用户操作
self._serve_forever()
# 清理服务信息
self.storage.clear_server_info()
return True
except Exception:
self.storage.clear_server_info()
return False
def _find_available_port(self, start: int) -> int | None:
attempts = 10
for offset in range(attempts):

View File

@@ -115,3 +115,49 @@ class DecideStorage:
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