- 新增 pam_deploy_graph 包,包含 agent、action router、runner、parser 和配置加载能力 - 支持 hybrid_node_mcp 路由策略:PAM_HOME 走脚本 action,PAM_NODE 走 MCP - 新增 fake runner 和 CLI 预演/全局流程验证入口 - 新增路由、输出解析、配置加载、脚本命令构造、Skill 策略加载测试 - 在 README 中记录当前代码骨架、实现进度、使用方式和下一步建议
134 lines
3.8 KiB
Python
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"
|
|
|