agent_deply/pam_deploy_graph/output_parser.py
dark ab7b839bc6 feat: 新增 PAM 智能部署 Agent 运行时骨架
- 新增 pam_deploy_graph 包,包含 agent、action router、runner、parser 和配置加载能力
- 支持 hybrid_node_mcp 路由策略:PAM_HOME 走脚本 action,PAM_NODE 走 MCP
- 新增 fake runner 和 CLI 预演/全局流程验证入口
- 新增路由、输出解析、配置加载、脚本命令构造、Skill 策略加载测试
- 在 README 中记录当前代码骨架、实现进度、使用方式和下一步建议
2026-05-29 14:49:41 +08:00

134 lines
3.8 KiB
Python

"""Normalize script stdout and MCP tool returns into ActionResult objects."""
from __future__ import annotations
import json
import re
from typing import Any
from .constants import SENSITIVE_KEYS
from .models import ActionResult, BackendName
PENDING_CONFIRMATION_RE = re.compile(r"PENDING_AGENT_CONFIRMATION\((?P<body>[^)]*)\)")
KEY_VALUE_RE = re.compile(r"^(?P<key>[A-Za-z_][A-Za-z0-9_]*)=(?P<value>.*)$")
def redact_text(text: str) -> str:
redacted = text
for key in SENSITIVE_KEYS:
redacted = re.sub(
rf"({re.escape(key)}\s*[:=]\s*)([^\s&]+)",
rf"\1***",
redacted,
flags=re.IGNORECASE,
)
return redacted
def parse_key_values(text: str) -> dict[str, Any]:
values: dict[str, Any] = {}
for raw_line in text.splitlines():
line = raw_line.strip()
match = KEY_VALUE_RE.match(line)
if not match:
continue
key = match.group("key")
value = match.group("value")
if key == "IP":
values.setdefault("IP", []).append(value)
else:
values[key] = value
return values
def normalize_mcp_values(payload: Any) -> dict[str, Any]:
if isinstance(payload, str):
try:
payload = json.loads(payload)
except json.JSONDecodeError:
return parse_key_values(payload)
if not isinstance(payload, dict):
return {"RESULT": payload}
values: dict[str, Any] = dict(payload)
aliases = {
"hashCode": "HASH_CODE",
"nodeUrl": "NODE_URL",
"rateOfProgress": "RATE_OF_PROGRESS",
"logFile": "LOG_FILE",
"action": "ACTION",
"result": "RESULT",
"success": "SUCCESS",
"message": "MESSAGE",
}
for source, target in aliases.items():
if source in values and target not in values:
values[target] = values[source]
return values
def parse_script_result(
action: str,
stdout: str,
stderr: str,
exit_code: int,
backend: BackendName = "script",
tool_name: str = "",
) -> ActionResult:
raw_output = redact_text("\n".join(part for part in (stdout, stderr) if part))
values = parse_key_values(stdout)
pending = PENDING_CONFIRMATION_RE.search(stdout) or PENDING_CONFIRMATION_RE.search(stderr)
if pending:
values["PENDING_AGENT_CONFIRMATION"] = pending.group(0)
ok = exit_code == 0 and not pending
error_summary = "" if ok else _summarize_error(stderr, stdout, pending.group(0) if pending else "")
return ActionResult(
action=action,
backend=backend,
tool_name=tool_name or action,
ok=ok,
values=values,
exit_code=exit_code,
stdout=redact_text(stdout),
stderr=redact_text(stderr),
raw_output=raw_output,
error_summary=error_summary,
)
def parse_mcp_result(
action: str,
payload: Any,
*,
ok: bool = True,
tool_name: str = "",
error: str = "",
) -> ActionResult:
values = normalize_mcp_values(payload)
raw_output = redact_text(json.dumps(payload, ensure_ascii=False, default=str))
return ActionResult(
action=action,
backend="mcp",
tool_name=tool_name or action,
ok=ok,
values=values,
exit_code=0 if ok else 1,
stdout=raw_output if ok else "",
stderr=redact_text(error),
raw_output=raw_output,
error_summary="" if ok else redact_text(error or "MCP tool failed"),
)
def _summarize_error(stderr: str, stdout: str, pending: str) -> str:
if pending:
return pending
for text in (stderr, stdout):
for line in reversed(text.splitlines()):
stripped = line.strip()
if stripped:
return redact_text(stripped)
return "Action failed"