Files
knowledge-governance/docs/DINGTALK_DWS_FETCHER.md
姚章浩 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

7.7 KiB
Raw Permalink Blame History

DingTalk 取件走 dws(DingTalk Workspace CLI)

默认优先使用 dws 取钉钉知识库字节,老路径(本地镜像树 + 浏览器 a-token 实况导出)作为回退保留。

1. 开关

环境变量 取值 含义
ARTIFACT_DINGTALK_FETCHER auto(默认) 先 dws;仅当 dws 返回可重试错误(429/5xx/超时/连接层/dws 二进制缺失)时回落到老路径
dws 只用 dws,失败即失败(灰度验证/压测用)
legacy 完全绕过 dws,走原逻辑(回退开关)
DWS_BIN 默认 /usr/local/bin/dws dws 可执行文件
DWS_CONFIG_DIR 容器内 /data/dws dws 配置目录(profiles/identity)
DWS_HOME 容器内 /data/home dws 子进程专用 HOME:文件型 keychain 必须位于 $HOME/.local/share/dws-cli(XDG_DATA_HOME 不被识别),只给 dws 子进程换 HOME,不动 worker 全局 HOME
DWS_DISABLE_KEYCHAIN 1 无系统 keychain 的环境用 file-DEK 模式

2. 路由

按已解析文件名后缀路由(与老路径判定一致;manifest 无 declared_type,缺失时依次退回 declared_type → dws drive +inspect):

后缀 取件方式 产物
adoc dws doc export --export-format docx docx
axls dws sheet list + sheet range read → scripts/axls_to_xlsx.py 落盘 xlsx
其余可入库类型 dws drive +download 原后缀
dlink / hlink 直接 missing(链接节点 fileSize=0) —
amind / appt unsupported_format(CLI 无内容导出入口,与老路径等价) —

平台侧判定一律保留:pick_suffix、SLIDES(PPT 必须 PDF 双胞胎)、magic 校验、大小上限、sha256、spool 原子写、exported_doc_is_blank() 空壳检查。dws 只负责“取字节”。

3. 错误映射

scripts/dws_error_map.py 把 dws 的错误体(严格版 ok/error 包裹、普通子命令 error 对象、本地错误、TLS/网络文本)统一映射为平台语义 {ok, code, retryable, missing}。要点:

  • 只有明确瞬时信号(429/5xx/超时/连接层失败/服务端导出任务失败/dws 二进制缺失)为 retryable=true;
  • 服务端导出任务失败 export_failed 判为可重试:并发实测同一节点 8 路时偶发失败、1/2/4 路与串行必成(详见 §7),若判终态会把本可成功的文档永久失败;
  • 未分类错误 code=unknown 且不重试——避免被无限重试掩盖真因;
  • workspace node not found → missing=true(走 missing 分支而非 failed);
  • TLS 缺 CA(x509: certificate signed by unknown authority)→ tls_ca,终态(重试无意义,需补 CA)。

覆盖样本与断言见 scripts/test_dws_error_map.py(18 个断言,样本为实测捕获的真实错误体)。

7. 并发实测与上限(真实 worker 容器内)

在已部署的 worker 容器里用平台自己的 artifact_fetch.py 实测(产物写容器 /tmp,不动 spool;每级用不同节点,避免服务端导出去重缓存失真):

场景 并发 结果 墙钟 单件均/p95 取件器
常规体积在线文档(容器 <200MB) 8 8/8 3.6s 3.5s / 3.5s dws
常规体积在线文档 12 12/12 3.5s 3.4s / 3.5s dws
巨型在线文档(容器 307MB~1.33GB) 1 1/1 4.3s 4.3s dws
巨型在线文档 2 2/2 4.6s 4.5s / 4.4s dws
巨型在线文档 4 3/3 4.7s 4.3s / 4.6s dws
混合 8 路(6 常规 + 2 巨型) 8 6/8 3.8s — 2× export_failed(可重试)

结论与要求:

  • 吞吐随并发线性增长(12/3.5s ≈ 3.4 件/秒),全程无 429;墙钟基本恒定 → 瓶颈是服务端导出任务而非本地。
  • 常规体积:≥12 路安全;巨型在线文档:4 路已验证安全;唯一见过的失败是 8 路混合时巨型文档的服务端导出偶发失败 → 已判 export_failed=retryable,并建议 worker 侧对在线文档并发不超过 4。
  • 目录大小只在"普通文件"时等于真实字节数:adoc/axls 等在线文档的目录大小是"容器大小"(实测有 322MB/590MB/1.40GB 的 adoc,导出 docx 仅 15MB/48MB/49.5MB)。因此大小预检只对普通文件生效;曾对在线文档生效导致 3 个可导出文档被误判 over_limit(已修,见 §8 回归)。
  • 定点复测(修复后、同一生产容器):3 个巨型 adoc 全部 ok 导出 docx;无权限的 axls 从"无结果的 no_result"变为明确的 forbidden(终态、不重试)。

8. 压测暴露并已修复的回归

回归 症状 修复 测试
在线文档被大小预检误拒 3 个 adoc(容器 322MB~1.4GB)over_limit 永久失败 预检只对普通文件生效(在线文档以导出件实际大小为准) test_online_document_with_huge_catalog_size_still_exports
转换器错误绕过平台契约 axls 权限失败时进程无结果退出 → 平台只看到 artifact_fetch_no_output axls_to_xlsx 抛 DwsCallError,取件器映射成 forbidden(CLI 侧 exit 2) test_axls_converter_failure_maps_to_platform_error
服务端导出偶发失败被判终态 8 路混合下的 export_failed 永久失败 export_failed 改判 retryable=true dws_error_map 新增断言

4. 镜像与部署

  • 镜像:alpine:3.22 + gcompat libc6-compat ca-certificates(官方 dws 是动态链接 glibc 的二进制,靠 gcompat 在 musl 上运行;已验证端到端字节一致);dws 版本与 sha256 固定在 Dockerfile 的 DWS_VERSION/DWS_SHA256,禁用 dws upgrade。
  • 凭据:与宿主共享同一份 state(refresh token 是轮换制,两个副本会互相顶掉):
    • ~/.dws → /data/dws(DWS_CONFIG_DIR)
    • ~/.local/share/dws-cli → /data/home/.local/share/dws-cli(DWS_HOME)
  • 容器必须 --user <宿主 uid>:<gid>(worker 已是 1000:1000),否则 state 被写成 root 所有。

5. 续期 Runbook(refresh token 30 天)

# 到期前 7 天(阈值告警)在目标机执行,本人完成浏览器授权
dws auth login --device
# 验证必须真实调用,而不是只看 authenticated 字段
dws wiki +space-list --limit 1 -f json     # 期望 ok:true
docker exec <worker> dws auth status --readonly -f json

出现 code=credentials 时立即停批(不要重试风暴),重新授权后 --retry-failed 续跑。健康面板建议展示 refresh_expires_at:<7 天黄、<3 天红。

6. 回退演练(上线前必做)

docker compose exec worker env ARTIFACT_DINGTALK_FETCHER=legacy ...   # 或改 compose 环境变量后重建 worker
# 取同一批节点 → 断言 spool_ref/sha256 契约不变、无新增 code=* → 再切回 auto

保留 dingtalk-login sidecar 与 63GB 镜像树至少 2 周(镜像树可降级为只读热备)。

7. 验证证据(2026-10-09)

项 结果
影子比对(真实 worker 容器 vs 宿主 dws) adoc 24/24 等价(正文相似度 1.0、media 逐项相等、11/24 字节完全相同);普通文件 5/5 sha256 一致;axls 归一化后值覆盖 100%/100%
容器端到端(本改动 + 真容器约束 --read-only --tmpfs /tmp --user 1000:1000) adoc/docx 305,078B(sha 77b393c3…,与平台基准一致)、axls/xlsx 3,330B、pdf 27,140B → 3/3 成功;结果 JSON 字段与改动前完全一致
单元测试 scripts/artifact_fetch_test.py 14 项、scripts/artifact_fetch_blank_doc_test.py 5 项、scripts/test_dws_error_map.py 17 断言 —— 全绿
大表能力 平台老路径失败的 220MB/15 表/3,658 行 axls,dws + 转换器 18s 产出 128KB 有效 xlsx