✨ feat: 对decide的功能进行调整
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "aide-plugin",
|
||||
"description": "Aide 工作流体系插件,提供任务准备和执行的标准化流程",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.2",
|
||||
"author": {
|
||||
"name": "Aide Team"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,14 @@ argument-hint: [任务细则文档路径]
|
||||
|
||||
---
|
||||
|
||||
## 前置准备
|
||||
|
||||
**首先触发 `aide` skill 学习 aide 命令的使用方法。**
|
||||
|
||||
这是必要步骤,确保你了解 `aide flow` 等命令的正确用法。
|
||||
|
||||
---
|
||||
|
||||
## 开始
|
||||
|
||||
### 确定任务细则
|
||||
|
||||
@@ -7,6 +7,22 @@ argument-hint: [无参数]
|
||||
|
||||
你正在使用 Aide 工作流体系。本命令帮助你快速认知项目并确保环境就绪。
|
||||
|
||||
---
|
||||
|
||||
## 核心约束(必须遵守)
|
||||
|
||||
**禁止直接读取或编辑 `.aide/` 目录下的任何文件!**
|
||||
|
||||
- ❌ 禁止使用 Read 工具读取 `.aide/config.toml`
|
||||
- ❌ 禁止使用 Edit/Write 工具修改 `.aide/` 下的文件
|
||||
- ✅ 必须通过 `aide config get/set` 读写配置
|
||||
- ✅ 必须通过 `aide env set` 修改环境配置
|
||||
- ✅ 配置有误时,触发 `env-config` skill 学习正确的设置方法
|
||||
|
||||
**原因**:`.aide/` 是 aide 程序的内部数据目录,直接操作可能导致格式错误或状态不一致。
|
||||
|
||||
---
|
||||
|
||||
## 执行步骤
|
||||
|
||||
### 1. Aide 运行时环境检测
|
||||
@@ -31,7 +47,7 @@ aide init
|
||||
|
||||
此命令会:
|
||||
- 创建 `.aide/` 目录(如不存在)
|
||||
- 生成默认配置文件 `.aide/config.toml`
|
||||
- 生成默认配置文件
|
||||
- 检查并更新 `.gitignore`
|
||||
|
||||
### 3. 项目认知
|
||||
@@ -51,40 +67,40 @@ aide init
|
||||
aide env ensure
|
||||
```
|
||||
|
||||
此命令会:
|
||||
- 读取 `.aide/config.toml` 中的环境配置
|
||||
- 检测并修复项目开发环境
|
||||
- 输出环境状态和配置信息
|
||||
|
||||
根据输出处理:
|
||||
- `✓`:环境就绪,继续到步骤 5
|
||||
- `⚠`:有警告但可继续,记录并继续
|
||||
- `✗`:**触发 `env-config` skill**,按指导完成配置后重试
|
||||
- `✓`:环境就绪,**直接进入步骤 5**(不要再查看配置文件)
|
||||
- `⚠`:有警告但可继续,记录警告信息并继续
|
||||
- `✗`:**触发 `env-config` skill**,学习如何使用 `aide env set` 命令修复配置,然后重试
|
||||
|
||||
**注意**:
|
||||
- 此时不带 `--runtime` 参数,会读取项目配置文件检查项目环境
|
||||
- aide env 会输出项目配置信息,包括默认的任务文档路径,记住这些信息供后续使用
|
||||
**环境检测失败时的正确做法**:
|
||||
1. 触发 `env-config` skill 获取配置指导
|
||||
2. 使用 `aide env set <模块>.<配置项> <值>` 修改配置
|
||||
3. 重新执行 `aide env ensure` 验证
|
||||
|
||||
**错误做法**(禁止):
|
||||
- 直接编辑 `.aide/config.toml` 文件
|
||||
|
||||
### 5. 汇报就绪状态
|
||||
|
||||
向用户汇报:
|
||||
环境检测通过后,直接向用户汇报(不要再读取配置文件):
|
||||
|
||||
```
|
||||
项目概况:[来自步骤3的概要]
|
||||
|
||||
环境状态:✓ 环境就绪 (python:3.12, ...)
|
||||
|
||||
项目配置:
|
||||
- 任务原文档:task-now.md
|
||||
- 任务细则:task-spec.md
|
||||
环境状态:[aide env ensure 的输出结果]
|
||||
|
||||
Aide 已就绪,可用命令:
|
||||
- /aide:prep [文档路径] - 任务准备
|
||||
- /aide:exec [文档路径] - 任务执行
|
||||
```
|
||||
|
||||
> 注:任务文档路径等配置信息已在 aide env ensure 输出中显示,无需额外查看配置文件。
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 如果在初始化过程中发现严重环境问题无法解决,建议用户修复后重开对话
|
||||
2. 本命令只做认知和环境准备,不修改任何业务代码
|
||||
3. 所有输出使用简体中文
|
||||
1. **不要读取 `.aide/` 目录下的文件**——所有配置操作通过 aide 命令完成
|
||||
2. 如果在初始化过程中发现严重环境问题无法解决,建议用户修复后重开对话
|
||||
3. 本命令只做认知和环境准备,不修改任何业务代码
|
||||
4. 所有输出使用简体中文
|
||||
|
||||
@@ -14,6 +14,14 @@ argument-hint: [任务原文档路径]
|
||||
|
||||
---
|
||||
|
||||
## 前置准备
|
||||
|
||||
**首先触发 `aide` skill 学习 aide 命令的使用方法。**
|
||||
|
||||
这是必要步骤,确保你了解 `aide flow` 等命令的正确用法。
|
||||
|
||||
---
|
||||
|
||||
## 开始
|
||||
|
||||
### 启动流程追踪
|
||||
@@ -95,15 +103,15 @@ aide flow next-step "任务优化完成,生成待定项"
|
||||
|
||||
### 有待定项时
|
||||
|
||||
提交待定项数据:
|
||||
1. 将待定项数据写入 JSON 文件(如 `.aide/pending-items.json`)
|
||||
2. 提交待定项数据:
|
||||
|
||||
```bash
|
||||
aide decide submit '<json数据>'
|
||||
aide decide submit .aide/pending-items.json
|
||||
```
|
||||
|
||||
告知用户访问链接进行确认。
|
||||
|
||||
用户完成后获取结果:
|
||||
3. 告知用户访问输出的链接进行确认
|
||||
4. 用户完成后获取结果:
|
||||
|
||||
```bash
|
||||
aide decide result
|
||||
|
||||
@@ -208,25 +208,31 @@ aide flow 会自动校验环节跳转是否合理:
|
||||
|
||||
## aide decide - 待定项确认
|
||||
|
||||
通过 Web 界面处理待定项确认。
|
||||
通过 Web 界面处理待定项确认。服务在后台运行,用户完成决策后自动关闭。
|
||||
|
||||
```
|
||||
aide decide {submit,result} ...
|
||||
|
||||
子命令:
|
||||
submit <json> 提交待定项数据并启动 Web 服务
|
||||
submit <file> 从文件读取待定项数据,启动后台 Web 服务
|
||||
result 获取用户决策结果
|
||||
```
|
||||
|
||||
### aide decide submit
|
||||
|
||||
提交待定项数据并启动 Web 服务。
|
||||
从 JSON 文件读取待定项数据,启动后台 Web 服务,立即返回。
|
||||
|
||||
```bash
|
||||
aide decide submit '<json数据>'
|
||||
aide decide submit <json文件路径>
|
||||
```
|
||||
|
||||
**JSON 格式**:
|
||||
**使用流程**:
|
||||
1. 将待定项数据写入 JSON 文件
|
||||
2. 执行 `aide decide submit <文件路径>` 启动服务
|
||||
3. 告知用户访问 Web 界面进行决策
|
||||
4. 用户完成后执行 `aide decide result` 获取结果
|
||||
|
||||
**JSON 文件格式**:
|
||||
```json
|
||||
{
|
||||
"task": "任务简述",
|
||||
@@ -267,7 +273,7 @@ aide decide submit '<json数据>'
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `port` | 3721 | 起始端口 |
|
||||
| `port` | 3721 | 起始端口(自动探测可用端口) |
|
||||
| `bind` | `"127.0.0.1"` | 监听地址,设为 `"0.0.0.0"` 可允许外部访问 |
|
||||
| `url` | `""` | 自定义访问地址,为空时自动生成 |
|
||||
| `timeout` | 0 | 超时时间(秒),0 表示不超时 |
|
||||
@@ -276,10 +282,11 @@ aide decide submit '<json数据>'
|
||||
```
|
||||
→ Web 服务已启动
|
||||
→ 请访问: http://localhost:3721
|
||||
→ 等待用户完成决策...
|
||||
✓ 决策已完成
|
||||
→ 用户完成决策后执行 aide decide result 获取结果
|
||||
```
|
||||
|
||||
> 注:服务在后台运行,命令立即返回。用户提交决策后服务自动关闭。
|
||||
|
||||
### aide decide result
|
||||
|
||||
获取用户决策结果。
|
||||
@@ -298,6 +305,10 @@ aide decide result
|
||||
}
|
||||
```
|
||||
|
||||
**错误情况**:
|
||||
- 尚无决策结果(服务运行中):提示等待用户完成操作
|
||||
- 尚无决策结果(服务已关闭):提示重新执行 submit
|
||||
|
||||
> 注:`note` 字段仅在用户添加备注时出现
|
||||
> 注:如果数据中有 `recommend` 字段,对应选项会默认选中
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import Any
|
||||
|
||||
from aide.core import output
|
||||
from aide.core.config import ConfigManager
|
||||
from aide.decide import cmd_decide
|
||||
from aide.env.manager import EnvManager
|
||||
from aide.flow.tracker import FlowTracker
|
||||
|
||||
@@ -134,9 +133,9 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
decide_parser = subparsers.add_parser("decide", help="待定项确认与决策记录")
|
||||
decide_subparsers = decide_parser.add_subparsers(dest="decide_cmd")
|
||||
|
||||
# aide decide submit '<json>'
|
||||
# aide decide submit <file>
|
||||
decide_submit_parser = decide_subparsers.add_parser("submit", help="提交待定项数据并启动 Web 服务")
|
||||
decide_submit_parser.add_argument("data", help="待定项 JSON 数据")
|
||||
decide_submit_parser.add_argument("file", help="待定项 JSON 数据文件路径")
|
||||
decide_submit_parser.set_defaults(func=handle_decide_submit)
|
||||
|
||||
# aide decide result
|
||||
@@ -302,18 +301,18 @@ def handle_decide_help(args: argparse.Namespace) -> bool:
|
||||
print("usage: aide decide {submit,result} ...")
|
||||
print("")
|
||||
print("子命令:")
|
||||
print(" submit <json> 提交待定项数据并启动 Web 服务")
|
||||
print(" submit <file> 从文件读取待定项数据,启动后台 Web 服务")
|
||||
print(" result 获取用户决策结果")
|
||||
print("")
|
||||
print("示例:")
|
||||
print(" aide decide submit '{\"task\":\"...\",\"source\":\"...\",\"items\":[...]}'")
|
||||
print(" aide decide submit ./pending-items.json")
|
||||
print(" aide decide result")
|
||||
return True
|
||||
|
||||
|
||||
def handle_decide_submit(args: argparse.Namespace) -> bool:
|
||||
from aide.decide import cmd_decide_submit
|
||||
return cmd_decide_submit(args.data)
|
||||
return cmd_decide_submit(args.file)
|
||||
|
||||
|
||||
def handle_decide_result(args: argparse.Namespace) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user