6496ee3571
build / build (push) Successful in 25s
- 大小预检此前对 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 个断言;文档补并发实测表与回归清单
7.7 KiB
7.7 KiB
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 |