"""PAM 部署 Agent 的常驻式交互 CLI 会话。""" from __future__ import annotations import time from dataclasses import asdict from pathlib import Path from typing import Any, Callable from .agent import PamDeployAgent from .checkpoint_store import load_agent_state, redact_mapping from .models import AgentState, ExecutionStrategy InputFunc = Callable[[str], str] OutputFunc = Callable[[str], None] COMMAND_HELP = """可用命令: help 显示帮助 preview 查看当前参数和执行策略 analyze <需求> 只做理解和计划,不执行 set KEY=VALUE 修改当前会话参数 run 创建部署任务并执行 status 查看当前运行状态 approve 确认待处理回滚 reject [原因] 拒绝待处理回滚 resume 从当前 checkpoint 续跑 checkpoint 显示 checkpoint 路径 exit 退出 也可以直接输入自然语言需求,Agent 会先分析并更新会话参数;执行仍需输入 run。 """ class InteractiveCliSession: """维护一次交互式 CLI 会话的参数、状态和命令处理逻辑。""" def __init__( self, *, agent: PamDeployAgent, params: dict[str, Any], strategy: ExecutionStrategy = "hybrid_node_mcp", checkpoint_path: str | None = None, target_ips: list[str] | None = None, input_func: InputFunc = input, output_func: OutputFunc = print, ) -> None: """初始化会话上下文和输入输出函数。""" self.agent = agent self.params = dict(params) self.strategy = strategy self.checkpoint_path = checkpoint_path or _default_checkpoint_path() self.target_ips = list(target_ips or []) self.input = input_func self.output = output_func self.state: AgentState | None = None self.last_analysis: dict[str, Any] | None = None def run(self) -> None: """启动 REPL 循环,直到用户 exit 或输入流结束。""" self.output("PAM 部署 Agent 交互式会话") self.output("输入 help 查看命令,输入 exit 退出。") self._load_existing_checkpoint_if_any() while True: try: line = self.input("PAM> ") except EOFError: self.output("bye") return if not self.handle_line(line): return def handle_line(self, line: str) -> bool: """处理用户输入的一行命令;返回 False 表示退出会话。""" text = line.strip() if not text: return True command, _, rest = text.partition(" ") normalized = command.lower() if normalized in ("exit", "quit", "q"): self.output("bye") return False if normalized in ("help", "?"): self.output(COMMAND_HELP.rstrip()) return True if normalized == "preview": self.output(self.agent.preview(self.params, self.strategy)) return True if normalized == "analyze": self._analyze(rest.strip()) return True if normalized == "set": self._set_param(rest.strip()) return True if normalized in ("run", "deploy", "execute"): self._run_deploy() return True if normalized == "resume": self._resume() return True if normalized == "status": self._status() return True if normalized == "approve": self._confirm(approved=True, note=rest.strip()) return True if normalized == "reject": self._confirm(approved=False, note=rest.strip()) return True if normalized == "checkpoint": self.output(f"checkpoint: {self.checkpoint_path}") return True self._analyze(text) return True def _analyze(self, text: str) -> None: """分析自然语言需求,并更新会话中的参数、策略和目标 IP。""" if not text: self.output("请输入要分析的自然语言需求,例如:analyze 请用 MCP 预演部署 HET。") return result = self.agent.analyze_request(text, self.params) self.last_analysis = result param_result = result["params"] intent_result = result["intent"] plan = result["plan"] self.params = dict(param_result.extracted_params) self.strategy = _choose_strategy(intent_result.strategy_preference, self.strategy) user_ips = param_result.extracted_control.get("user_specified_ips") if isinstance(user_ips, list): self.target_ips = [str(item) for item in user_ips] safe_payload = redact_mapping({key: asdict(value) for key, value in result.items()}) self.output("已生成结构化理解:") self.output(f"- intent: {intent_result.intent}") self.output(f"- strategy: {self.strategy}") self.output(f"- summary: {plan.summary}") if param_result.missing_required_params: self.output("- missing: " + ", ".join(param_result.missing_required_params)) if self.target_ips: self.output("- target_ips: " + ", ".join(self.target_ips)) self.output("执行请输 run;查看完整 JSON 可用一次性 analyze 命令。") self.output(_format_redacted_params(safe_payload["params"]["extracted_params"])) def _set_param(self, assignment: str) -> None: """处理 `set KEY=VALUE` 命令,更新当前会话参数。""" if "=" not in assignment: self.output("格式:set KEY=VALUE") return key, value = assignment.split("=", 1) key = key.strip() if not key: self.output("参数名不能为空。") return self.params[key] = value.strip() self.output(f"已设置 {key}") def _run_deploy(self) -> None: """在用户确认后创建状态并执行完整部署流程。""" if self.state and self.state.pending_confirmation: self._print_confirmation() return if not self._ask_yes_no("即将执行真实 action;确认执行请输入 yes: "): self.output("已取消执行。") return self.state = self.agent.create_state( params=self.params, execution_strategy=self.strategy, checkpoint_path=self.checkpoint_path, target_ips=self.target_ips, ) self._execute_current_state() def _resume(self) -> None: """从内存状态或 checkpoint 文件继续执行部署流程。""" if self.state is None: checkpoint = Path(self.checkpoint_path) if not checkpoint.exists(): self.output("当前没有可续跑的 checkpoint。") return self.state = load_agent_state(checkpoint) self.state.checkpoint_path = self.state.checkpoint_path or str(checkpoint) self._execute_current_state() def _execute_current_state(self) -> None: """执行当前 state,并输出报告、确认提示和 checkpoint 路径。""" if self.state is None: self.output("当前没有运行状态。") return self.state = self.agent.run_deploy_flow(self.state) self.output(self.agent.render_report(self.state)) if self.state.pending_confirmation: self._print_confirmation() self.output(f"checkpoint: {self.state.checkpoint_path or self.checkpoint_path}") def _status(self) -> None: """输出当前运行状态;没有 state 时输出 checkpoint 路径。""" if self.state is None: self.output("当前还没有运行状态。") self.output(f"checkpoint: {self.checkpoint_path}") return self.output(self.agent.render_report(self.state)) if self.state.pending_confirmation: self._print_confirmation() def _confirm(self, *, approved: bool, note: str = "") -> None: """处理 approve/reject 命令。""" if self.state is None: checkpoint = Path(self.checkpoint_path) if checkpoint.exists(): self.state = load_agent_state(checkpoint) self.state.checkpoint_path = self.state.checkpoint_path or str(checkpoint) else: self.output("当前没有待确认任务。") return if not self.state.pending_confirmation: self.output("当前没有待确认任务。") return self.state = self.agent.confirm_pending(self.state, approved=approved, operator_note=note) self.output(self.agent.render_report(self.state)) if self.state.pending_confirmation: self._print_confirmation() def _print_confirmation(self) -> None: """输出当前待人工确认事项。""" if self.state is None: return request = self.agent.build_confirmation_request(self.state) if not request: return self.output("需要人工确认:") self.output(f"- type: {request.get('type')}") if request.get("ip"): self.output(f"- ip: {request['ip']}") if request.get("failed_stage"): self.output(f"- failed_stage: {request['failed_stage']}") if request.get("failure_reason"): self.output(f"- reason: {request['failure_reason']}") self.output("输入 approve 执行回滚,或 reject [原因] 拒绝回滚。") def _ask_yes_no(self, prompt: str) -> bool: """读取一次 yes/no 确认,只有 yes/y 视为确认。""" try: answer = self.input(prompt).strip().lower() except EOFError: return False return answer in ("yes", "y") def _load_existing_checkpoint_if_any(self) -> None: """会话启动时自动加载已存在的 checkpoint。""" checkpoint = Path(self.checkpoint_path) if not checkpoint.exists(): return self.state = load_agent_state(checkpoint) self.state.checkpoint_path = self.state.checkpoint_path or str(checkpoint) self.output(f"已加载 checkpoint: {checkpoint}") if self.state.pending_confirmation: self._print_confirmation() def run_interactive_chat( *, agent: PamDeployAgent, params: dict[str, Any], strategy: ExecutionStrategy, checkpoint_path: str | None = None, target_ips: list[str] | None = None, input_func: InputFunc = input, output_func: OutputFunc = print, ) -> InteractiveCliSession: """创建并运行交互式 CLI 会话,返回会话对象便于测试。""" session = InteractiveCliSession( agent=agent, params=params, strategy=strategy, checkpoint_path=checkpoint_path, target_ips=target_ips, input_func=input_func, output_func=output_func, ) session.run() return session def _default_checkpoint_path() -> str: """生成默认 chat checkpoint 路径。""" return str(Path("runtime") / "checkpoints" / f"chat_{time.strftime('%Y%m%d_%H%M%S')}.json") def _choose_strategy(preference: str, default: ExecutionStrategy) -> ExecutionStrategy: """根据 LLM 偏好更新执行策略,非法值保留默认策略。""" if preference in ("hybrid_node_mcp", "script_only", "fake"): return preference # type: ignore[return-value] return default def _format_redacted_params(params: dict[str, Any]) -> str: """把脱敏后的参数字典格式化为多行文本。""" lines = ["当前参数:"] for key in sorted(params): lines.append(f"- {key}: {params[key]}") return "\n".join(lines)