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 个断言;文档补并发实测表与回归清单
107 lines
7.7 KiB
Markdown
107 lines
7.7 KiB
Markdown
# 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 | |