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