"""把脚本 stdout 和 MCP tool 返回值归一化为 ActionResult。"""
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
[^)]*)\)")
KEY_VALUE_RE = re.compile(r"^(?P[A-Za-z_][A-Za-z0-9_]*)=(?P.*)$")
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]:
"""解析脚本输出中的 KEY=VALUE 行,重复 IP 会聚合为列表。"""
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]:
"""把 MCP 返回值归一化为脚本兼容的字段名。"""
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:
"""解析 MCP tool 返回值,并包装为统一 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 执行失败"),
)
def _summarize_error(stderr: str, stdout: str, pending: str) -> str:
"""从 stderr/stdout/确认标记中提取简短错误摘要。"""
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 执行失败"