dark 8d390aa416 完善 chat/runtime 的 LLM 审核、断点续跑与热更新,并同步打包文档
调整 workflow 执行逻辑:每个 action 完成后统一进入 LLM/规则审核,审核开始/结果可播报,审核阻断时自动暂停并给出建议
增强 chat 交互:支持执行中 Ctrl+C 中断并保存 checkpoint,后续可 resume 继续
增加运行时热更新能力:支持 set KEY=VALUE 和 load params <路径> 同步更新当前 state、config.txt 和 checkpoint
支持自定义 action 审核提示词:新增 --llm-action-analysis-prompt-file / PAM_LLM_ACTION_ANALYSIS_PROMPT_FILE
新增 prompts/action_review.txt,落地保存当前默认审核提示词,便于后续按基线调整
更新 Linux 打包脚本,将 prompts/action_review.txt 一并带入发布包
同步更新 README、流程图、todo 和打包文档,修正 --analyze-actions 语义说明与 chat 最新行为说明
2026-06-03 17:02:17 +08:00

212 lines
8.0 KiB
Python

"""PAM 部署 Agent 的命令行入口。"""
from __future__ import annotations
import argparse
import json
from dataclasses import asdict
from .agent import PamDeployAgent
from .checkpoint_store import load_agent_state, redact_mapping
from .interactive import run_interactive_chat
from .langgraph_runtime import LangGraphDeploymentRuntime, LangGraphRunResult
from .llm import build_llm_client
from .mcp_factory import build_mcp_runner_from_config
from .params_loader import load_params_file
def add_llm_args(parser: argparse.ArgumentParser) -> None:
"""为子命令追加真实 LLM 配置参数。"""
parser.add_argument("--llm-base-url")
parser.add_argument("--llm-api-key")
parser.add_argument("--llm-model")
parser.add_argument("--llm-action-analysis-prompt-file")
def add_mcp_args(parser: argparse.ArgumentParser) -> None:
"""为需要执行 MCP action 的子命令追加 MCP 配置参数。"""
parser.add_argument("--mcp-config", help="MCP client JSON 配置文件路径")
def add_action_analysis_arg(parser: argparse.ArgumentParser) -> None:
"""为执行类子命令追加 action 后诊断开关。"""
parser.add_argument("--analyze-actions", action="store_true", help="每个 action 后追加 LLM/规则诊断建议")
def require_confirm(args: argparse.Namespace) -> None:
"""真实执行前强制要求命令行显式传入 --confirm。"""
if not getattr(args, "confirm", False):
raise SystemExit("Refusing to execute actions without --confirm.")
def print_pause_payload(agent: PamDeployAgent, state) -> None:
"""输出 checkpoint 和待确认信息,便于用户续跑或确认。"""
if state.pending_confirmation:
print(json.dumps({"confirmation": agent.build_confirmation_request(state)}, ensure_ascii=False, indent=2))
if state.checkpoint_path:
print(json.dumps({"checkpoint": state.checkpoint_path}, ensure_ascii=False, indent=2))
def run_graph_once(agent: PamDeployAgent, state, *, flow: str = "deploy") -> LangGraphRunResult:
"""用 LangGraph runtime 执行一次状态,返回图执行结果。"""
runtime = LangGraphDeploymentRuntime(agent=agent, flow=flow) # type: ignore[arg-type]
return runtime.start(state)
def print_graph_result(agent: PamDeployAgent, result: LangGraphRunResult) -> None:
"""输出 LangGraph 执行结果、报告和暂停信息。"""
state = result.state
if result.report:
print(result.report)
elif state is not None:
print(agent.render_report(state))
if result.interrupted and result.confirmation:
print(json.dumps({"confirmation": result.confirmation}, ensure_ascii=False, indent=2))
if state is not None:
print_pause_payload(agent, state)
def main() -> None:
"""解析 CLI 参数并分发到对应命令。"""
parser = argparse.ArgumentParser(prog="pam-deploy-agent")
sub = parser.add_subparsers(dest="command", required=True)
preview = sub.add_parser("preview")
preview.add_argument("--config", required=True)
preview.add_argument("--strategy", default="hybrid_node_mcp", choices=["hybrid_node_mcp", "script_only", "fake"])
analyze = sub.add_parser("analyze")
analyze.add_argument("--text", required=True)
analyze.add_argument("--config")
add_llm_args(analyze)
chat = sub.add_parser("chat")
chat.add_argument("--config", required=True)
chat.add_argument("--strategy", default="fake", choices=["hybrid_node_mcp", "script_only", "fake"])
chat.add_argument("--target-ip", action="append", default=[])
chat.add_argument("--checkpoint")
add_llm_args(chat)
add_mcp_args(chat)
add_action_analysis_arg(chat)
run = sub.add_parser("run-global")
run.add_argument("--config", required=True)
run.add_argument("--strategy", default="fake", choices=["hybrid_node_mcp", "script_only", "fake"])
run.add_argument("--checkpoint")
run.add_argument("--confirm", action="store_true")
add_llm_args(run)
add_mcp_args(run)
add_action_analysis_arg(run)
deploy = sub.add_parser("run-deploy")
deploy.add_argument("--config", required=True)
deploy.add_argument("--strategy", default="fake", choices=["hybrid_node_mcp", "script_only", "fake"])
deploy.add_argument("--target-ip", action="append", default=[])
deploy.add_argument("--checkpoint")
deploy.add_argument("--confirm", action="store_true")
add_llm_args(deploy)
add_mcp_args(deploy)
add_action_analysis_arg(deploy)
resume = sub.add_parser("resume")
resume.add_argument("--checkpoint", required=True)
resume.add_argument("--confirm", action="store_true")
add_llm_args(resume)
add_mcp_args(resume)
add_action_analysis_arg(resume)
confirm = sub.add_parser("confirm")
confirm.add_argument("--checkpoint", required=True)
confirm.add_argument("--decision", required=True, choices=["approve", "reject"])
confirm.add_argument("--note", default="")
confirm.add_argument("--confirm", action="store_true")
add_llm_args(confirm)
add_mcp_args(confirm)
add_action_analysis_arg(confirm)
args = parser.parse_args()
params = load_params_file(args.config) if getattr(args, "config", None) else {}
llm_client = None
if args.command != "preview":
llm_client = build_llm_client(
base_url=getattr(args, "llm_base_url", None),
api_key=getattr(args, "llm_api_key", None),
model=getattr(args, "llm_model", None),
action_analysis_prompt_path=getattr(args, "llm_action_analysis_prompt_file", None),
)
mcp_runner = None
if getattr(args, "mcp_config", None):
mcp_runner = build_mcp_runner_from_config(args.mcp_config)
agent = PamDeployAgent(
llm_client=llm_client,
mcp_runner=mcp_runner,
action_analysis_enabled=bool(getattr(args, "analyze_actions", False)),
)
if args.command == "analyze":
result = agent.analyze_request(args.text, params)
payload = redact_mapping({key: asdict(value) for key, value in result.items()})
print(json.dumps(payload, ensure_ascii=False, indent=2))
return
if args.command == "chat":
run_interactive_chat(
agent=agent,
params=params,
strategy=args.strategy,
checkpoint_path=args.checkpoint,
target_ips=args.target_ip,
)
return
if args.command == "preview":
print(agent.preview(params, args.strategy))
return
require_confirm(args)
if args.command == "run-global":
state = agent.create_state(
params=params,
execution_strategy=args.strategy,
checkpoint_path=args.checkpoint,
)
result = run_graph_once(agent, state, flow="global")
if result.state is not None:
print(json.dumps({"events": result.state.events}, ensure_ascii=False, indent=2))
print_pause_payload(agent, result.state)
return
if args.command == "resume":
state = load_agent_state(args.checkpoint)
state.checkpoint_path = state.checkpoint_path or args.checkpoint
if state.paused:
state = agent.resume_state(state)
result = run_graph_once(agent, state, flow="deploy")
print_graph_result(agent, result)
return
if args.command == "confirm":
state = load_agent_state(args.checkpoint)
state.checkpoint_path = state.checkpoint_path or args.checkpoint
runtime = LangGraphDeploymentRuntime(agent=agent, flow="deploy")
first = runtime.start(state)
if first.interrupted:
result = runtime.resume(approved=args.decision == "approve", note=args.note)
print_graph_result(agent, result)
return
print_graph_result(agent, first)
return
state = agent.create_state(
params=params,
execution_strategy=args.strategy,
checkpoint_path=args.checkpoint,
target_ips=args.target_ip,
)
result = run_graph_once(agent, state, flow="deploy")
print_graph_result(agent, result)
if __name__ == "__main__":
main()