#!/usr/bin/env python3 """dws 错误 → 平台错误码/retryable 映射(阶段 B-③ 交付物)。 平台契约(internal/workflows/artifactfetch/handler.go)需要: {ok, missing, retryable, code, error} 本模块把 dws 的两种错误体(严格命令的 ok/error 包裹、普通子命令的 error 对象) 以及本地错误、TLS/网络文本,统一映射成平台语义。 用法: from dws_error_map import classify classify(dws_json, stderr_text) -> {"ok":bool,"code":str,"retryable":bool,"missing":bool,"error":str} 原则: - 只有"明确瞬时"的信号才 retryable=True(429/5xx/超时/连接层失败) - 未知错误默认 retryable=False + code=unknown —— 让它显式暴露,而不是被无限重试掩盖 - missing=True 表示源侧对象已不存在/被删(平台据此走 missing 分支而不是 failed) """ import json import re # (匹配正则, code, retryable, missing, 说明) RULES = [ # ---- 凭据 ---- (r"请重新运行 dws auth login|resolve access token|invalidParameter\.authCode\.notFound|未登录|authenticated\"?\s*:\s*false|旧版登录态", "credentials", False, False, "登录态失效/被顶掉,需重新设备流授权"), # ---- TLS 信任库 ---- (r"x509: certificate signed by unknown authority|tls: failed to verify certificate", "tls_ca", False, False, "容器缺少 CA 信任库(装 ca-certificates 或挂载 bundle/SSL_CERT_FILE)"), # ---- 权限 ---- (r"forbidden\.accessDenied|Access Denied|accessdenied|HTTP 403|\"code\":\s*403", "forbidden", False, False, "该账号对目标节点无权限"), # ---- 限流 ---- (r"HTTP 429|\b429\b|rate.?limit|Too Many Requests|请求过于频繁", "http_429", True, False, "被限流,退避后重试"), # ---- 本地文件冲突 / 大小限制 ---- (r"LOCAL_FILE_EXISTS|目标文件已存在", "file_exists", False, False, "本地已有同名产物,换路径或用 drive download --overwrite"), (r"LOCAL_DOWNLOAD_TOO_LARGE|超过上限 \d+ 字节|exceeds ingest size limit|超过入库大小限制", "over_limit", False, False, "超过大小上限(严格版 512MB / 平台 200MB 配置)"), # ---- 类型不支持 ---- (r"不是钉钉在线文档|不是钉钉表格|notSupported\.docType|NODE_TYPE_MISMATCH|不是 a standalone \.adraw|无法使用文档相关工具|file type .* is not supported|在线表格不支持直接下载|不支持直接下载|请使用 getRange", "unsupported_format", False, False, "节点类型与该入口不匹配(走对应产品命令)"), # ---- 源对象不存在 ---- (r"workspace node not found|not found in current inventory|节点不存在|dentry not resolvable|nodeId 指向的节点已", "missing", False, True, "源侧节点已删除/移动"), # ---- 空文档(与平台既有判定同义) ---- (r"content is empty|正文为空|content\s+is\s+empty", "empty_document", False, False, "文档正文为空,无需入库"), # ---- 服务端导出失败/超时 ---- (r"导出任务失败|export job failed|submitExportJob", "export_failed", False, False, "钉钉导出任务失败"), (r"导出任务超时|export job timed out|timed out|timeout|超时", "export_timeout", True, False, "导出/请求超时,可重试"), # ---- 连接层 ---- (r"connection refused|no such host|network is unreachable|i/o timeout|EOF|connection reset|request_failed|502|503|504|HTTP 5\d\d", "source_unavailable", True, False, "源端不可达,可重试"), ] _JSON_ERR_KEYS = ("upstream_code", "server_error_code", "code", "category", "subtype", "message", "cause", "hint", "error") def _flatten(payload): """把 dws 错误体压成一段可正则匹配的文本。""" if payload is None: return "" if isinstance(payload, str): return payload parts = [] def walk(node, depth=0): if depth > 6: return if isinstance(node, dict): for k, v in node.items(): if k in _JSON_ERR_KEYS or isinstance(v, (dict, list)): parts.append(str(k)) walk(v, depth + 1) elif isinstance(node, list): for v in node[:20]: walk(v, depth + 1) else: parts.append(str(node)) walk(payload) return " | ".join(parts) def classify(dws_json=None, stderr=None, exit_code=None): """dws 结果 → 平台语义。dws_json 可为 dict / JSON 字符串 / None。""" payload = dws_json if isinstance(payload, str): try: payload = json.loads(payload) except Exception: payload = {"error": {"message": payload}} text = _flatten(payload) + " || " + _flatten(stderr) ok = bool(payload) and isinstance(payload, dict) and ( payload.get("ok") is True or payload.get("success") is True or payload.get("outcome") == "success" or payload.get("status") == "SUCCESS") if ok: return {"ok": True, "code": "", "retryable": False, "missing": False, "error": ""} # 先判 missing 关键词(missing 优先于其它分类) for pattern, code, retryable, missing, why in RULES: if re.search(pattern, text, re.I): msg = _short_message(payload) or why return {"ok": False, "code": code, "retryable": retryable, "missing": missing, "error": f"{why}: {msg}"[:400]} return {"ok": False, "code": "unknown", "retryable": False, "missing": False, "error": (_short_message(payload) or str(stderr or "") or "dws 未分类错误")[:400]} def _short_message(payload): if isinstance(payload, dict): err = payload.get("error") or payload if isinstance(err, dict): return str(err.get("message") or err.get("cause") or err.get("reason") or "") return str(err)[:200] return "" if __name__ == "__main__": import sys raw = sys.stdin.read() try: print(json.dumps(classify(raw), ensure_ascii=False, indent=1)) except Exception as e: print(json.dumps({"ok": False, "code": "unknown", "error": str(e)}, ensure_ascii=False))