Files
knowledge-governance/scripts/dws_error_map.py
T
姚章浩 6496ee3571
build / build (push) Successful in 25s
fix(artifact): 在线文档大小预检只对普通文件生效,压测暴露的取件器错误映射补齐
- 大小预检此前对 adoc/axls 也用目录大小,而在线文档目录大小是容器大小
  (实测有 1.40GB 的 adoc,导出 docx 仅 49.5MB)→ 3 个可导出文档被误判 over_limit
- axls 转换器的 dws 失败改为抛 DwsCallError,取件器映射成平台错误码
  (此前进程无结果退出,平台只看到 artifact_fetch_no_output)
- export_failed 改判 retryable:8 路混合时巨型文档服务端导出偶发失败,1/2/4 路与串行必成
- 新增 2 个回归测试 + dws_error_map 1 个断言;文档补并发实测表与回归清单
2026-10-09 11:51:26 +08:00

126 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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", True, 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))