✨ feat: 对decide的功能进行调整
This commit is contained in:
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
47
aide-program/aide/decide/daemon.py
Normal file
47
aide-program/aide/decide/daemon.py
Normal 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())
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user