- 为 pam_deploy_graph 生产代码补充中文模块、类、函数/方法文档字符串 - 将原有英文说明和主要英文异常提示改为中文 - 新增当前整体逻辑结构流程图文档,覆盖模块结构、执行链路、action 路由、人工确认和 checkpoint 续跑 - 新增 Linux 自带运行环境打包脚本,使用 PyInstaller 生成解压即用目录和 tar.gz - 新增 Linux 打包说明,包含构建命令、运行方式、依赖说明和包大小评估 - 同步 README,补充流程图、打包方式、产物路径和大小预估 - 更新相关测试断言以匹配中文错误提示
310 lines
12 KiB
Python
310 lines
12 KiB
Python
"""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)
|