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

107 lines
7.7 KiB
Markdown
Raw Permalink 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.
# 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 天)
```bash
# 到期前 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. 回退演练(上线前必做)
```bash
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 |