diff --git a/aide-marketplace/aide-plugin/.claude-plugin/plugin.json b/aide-marketplace/aide-plugin/.claude-plugin/plugin.json index 4b2dfe5..a942d15 100644 --- a/aide-marketplace/aide-plugin/.claude-plugin/plugin.json +++ b/aide-marketplace/aide-plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "aide-plugin", "description": "Aide 工作流体系插件,提供任务准备和执行的标准化流程", - "version": "1.0.0", + "version": "1.0.2", "author": { "name": "Aide Team" }, diff --git a/aide-marketplace/aide-plugin/commands/exec.md b/aide-marketplace/aide-plugin/commands/exec.md index 2bba690..3aca0e4 100644 --- a/aide-marketplace/aide-plugin/commands/exec.md +++ b/aide-marketplace/aide-plugin/commands/exec.md @@ -14,6 +14,14 @@ argument-hint: [任务细则文档路径] --- +## 前置准备 + +**首先触发 `aide` skill 学习 aide 命令的使用方法。** + +这是必要步骤,确保你了解 `aide flow` 等命令的正确用法。 + +--- + ## 开始 ### 确定任务细则 diff --git a/aide-marketplace/aide-plugin/commands/init.md b/aide-marketplace/aide-plugin/commands/init.md index 730c32a..219970d 100644 --- a/aide-marketplace/aide-plugin/commands/init.md +++ b/aide-marketplace/aide-plugin/commands/init.md @@ -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. 所有输出使用简体中文 diff --git a/aide-marketplace/aide-plugin/commands/prep.md b/aide-marketplace/aide-plugin/commands/prep.md index 39685c3..766ebf5 100644 --- a/aide-marketplace/aide-plugin/commands/prep.md +++ b/aide-marketplace/aide-plugin/commands/prep.md @@ -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 '' +aide decide submit .aide/pending-items.json ``` -告知用户访问链接进行确认。 - -用户完成后获取结果: +3. 告知用户访问输出的链接进行确认 +4. 用户完成后获取结果: ```bash aide decide result diff --git a/aide-marketplace/aide-plugin/skills/aide/SKILL.md b/aide-marketplace/aide-plugin/skills/aide/SKILL.md index ae73736..238a117 100644 --- a/aide-marketplace/aide-plugin/skills/aide/SKILL.md +++ b/aide-marketplace/aide-plugin/skills/aide/SKILL.md @@ -208,25 +208,31 @@ aide flow 会自动校验环节跳转是否合理: ## aide decide - 待定项确认 -通过 Web 界面处理待定项确认。 +通过 Web 界面处理待定项确认。服务在后台运行,用户完成决策后自动关闭。 ``` aide decide {submit,result} ... 子命令: - submit 提交待定项数据并启动 Web 服务 + submit 从文件读取待定项数据,启动后台 Web 服务 result 获取用户决策结果 ``` ### aide decide submit -提交待定项数据并启动 Web 服务。 +从 JSON 文件读取待定项数据,启动后台 Web 服务,立即返回。 ```bash -aide decide submit '' +aide decide submit ``` -**JSON 格式**: +**使用流程**: +1. 将待定项数据写入 JSON 文件 +2. 执行 `aide decide submit <文件路径>` 启动服务 +3. 告知用户访问 Web 界面进行决策 +4. 用户完成后执行 `aide decide result` 获取结果 + +**JSON 文件格式**: ```json { "task": "任务简述", @@ -267,7 +273,7 @@ aide decide submit '' | 配置项 | 默认值 | 说明 | |--------|--------|------| -| `port` | 3721 | 起始端口 | +| `port` | 3721 | 起始端口(自动探测可用端口) | | `bind` | `"127.0.0.1"` | 监听地址,设为 `"0.0.0.0"` 可允许外部访问 | | `url` | `""` | 自定义访问地址,为空时自动生成 | | `timeout` | 0 | 超时时间(秒),0 表示不超时 | @@ -276,10 +282,11 @@ aide decide submit '' ``` → Web 服务已启动 → 请访问: http://localhost:3721 -→ 等待用户完成决策... -✓ 决策已完成 +→ 用户完成决策后执行 aide decide result 获取结果 ``` +> 注:服务在后台运行,命令立即返回。用户提交决策后服务自动关闭。 + ### aide decide result 获取用户决策结果。 @@ -298,6 +305,10 @@ aide decide result } ``` +**错误情况**: +- 尚无决策结果(服务运行中):提示等待用户完成操作 +- 尚无决策结果(服务已关闭):提示重新执行 submit + > 注:`note` 字段仅在用户添加备注时出现 > 注:如果数据中有 `recommend` 字段,对应选项会默认选中 diff --git a/aide-program/aide/decide/__init__.py b/aide-program/aide/decide/__init__.py index 3981780..f05e5da 100644 --- a/aide-program/aide/decide/__init__.py +++ b/aide-program/aide/decide/__init__.py @@ -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", ] diff --git a/aide-program/aide/decide/cli.py b/aide-program/aide/decide/cli.py index 9157f5d..67d5889 100644 --- a/aide-program/aide/decide/cli.py +++ b/aide-program/aide/decide/cli.py @@ -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 ''") + _print_error("未找到待定项数据", "请先执行 aide decide submit ") return False session_id = pending.meta.session_id if pending.meta else None if not session_id: - _print_error("决策结果已过期", "pending.json 已被更新,请重新执行 aide decide submit ''") + _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 ''") - 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 diff --git a/aide-program/aide/decide/daemon.py b/aide-program/aide/decide/daemon.py new file mode 100644 index 0000000..229c5d5 --- /dev/null +++ b/aide-program/aide/decide/daemon.py @@ -0,0 +1,47 @@ +"""后台服务入口点。 + +通过 subprocess 启动,独立运行 HTTP 服务。 +用法: python -m aide.decide.daemon +""" + +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 \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()) diff --git a/aide-program/aide/decide/server.py b/aide-program/aide/decide/server.py index 451cef6..a9e634e 100644 --- a/aide-program/aide/decide/server.py +++ b/aide-program/aide/decide/server.py @@ -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): diff --git a/aide-program/aide/decide/storage.py b/aide-program/aide/decide/storage.py index 8452ae6..83f9e89 100644 --- a/aide-program/aide/decide/storage.py +++ b/aide-program/aide/decide/storage.py @@ -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 + diff --git a/aide-program/aide/main.py b/aide-program/aide/main.py index 61e885b..98546a5 100644 --- a/aide-program/aide/main.py +++ b/aide-program/aide/main.py @@ -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 '' + # aide decide submit 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 提交待定项数据并启动 Web 服务") + print(" submit 从文件读取待定项数据,启动后台 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: