# nantong-lease-audit Workflow 架构与批量运行指南

## 概述

`nantong-lease-audit` 是南通新东方租赁合同梳理的 uwf 状态机工作流（2026-06-27 建成并通过金飞达12份+人民中路2份测试）。

**架构**：
```
OCR(手动/脚本) → uwf thread start(每份合同) → Excel builder(汇总) → delivery-gate → 交付
```

**与 contract-portfolio-analysis skill Step 0→7 的关系**：
- workflow 替代 Step 2 的 delegate_task 动作B（模版比对）+ rule-analyzer（法律风险分析）+ data-extractor（结构化数据提取）
- Step 0（盘点）、Step 1（OCR）、Step 3（Excel生成）、Step 6（交付闸门）仍由脚本完成
- Step 4-5（校对+终审）在 workflow 完成后由人工做

## 4 角色定义

| 角色 | 耗时(典型) | 产出 |
|------|-----------|------|
| classifier | 60-90s | 校区、主体、合同类型、模版类型、面积、期限 |
| template-diff | 180-200s | 与07模版逐条比对差异清单（供L列） |
| rule-analyzer | 280-300s | 独立法律风险分析（供K列）+ 提前退租分析 |
| data-extractor | 120-150s | 12列结构化数据 + H列四检 + 数学交叉验证 |

**单份合同总耗时：~10分钟**（4角色串行）。

## YAML Frontmatter 输出大小限制

uwf 的 YAML frontmatter 解析有大小限制。**当角色输出内容超过 ~2000 字符时，frontmatter 解析会失败**，导致 thread suspended。

**data-extractor 的解决方案**：
- 将12列数据写入 `/tmp/nantong-lease-audit/row_data.json` 文件
- YAML frontmatter 中只传文件路径：`output_file: /tmp/nantong-lease-audit/row_data.json`
- 下游 Excel builder 从文件读取数据

**规则**：任何 uwf 角色的输出如果包含大量文本（>2000字符），应该：
1. 将详细内容写入文件
2. frontmatter 中只放文件路径和摘要字段
3. 在 procedure 中明确指示 agent 必须写文件

## 批量启动模式

### batch_runner.sh 模板

```bash
#!/bin/bash
UWF=/home/maggie/.hermes/node/bin/uwf
WF=nantong-lease-audit
CAMPUS=校区名
OCR_DIR=/tmp/校区/ocr
LOG=/tmp/校区/threads.log

FILES=("合同1" "合同2" ...)  # 文件名（不含.md后缀）
THREAD_IDS=()

for fname in "${FILES[@]}"; do
  md_path="${OCR_DIR}/${fname}.md"
  [ ! -f "$md_path" ] && continue
  
  OUT=$($UWF thread start $WF -p "校区：${CAMPUS} | 合同文件：${fname} | OCR文本路径：${md_path} | 原始文件名：${fname}.pdf" 2>&1)
  TID=$(echo "$OUT" | python3 -c "import re,sys; t=sys.stdin.read(); m=(re.search(r'\"thread\"\s*:\s*\"([^\"]+)\"', t) or re.search(r'Thread\s+(\S+)', t)); print(m.group(1) if m else '')")
  
  [ -n "$TID" ] && { THREAD_IDS+=("$TID"); sleep 8; }  # 8s CAS settle
done

# exec all threads
for tid in "${THREAD_IDS[@]}"; do
  cd /home/maggie && $UWF thread exec "$tid" -c 20 --background 2>&1
  sleep 2
done

# Wait loop with auto-resume of suspended threads
while true; do
  ALL_DONE=true
  for tid in "${THREAD_IDS[@]}"; do
    STATUS=$(cd /home/maggie && $UWF thread list --all 2>/dev/null | grep "$tid" | awk '{print $3}')
    if [ "$STATUS" = "suspended" ] || [ "$STATUS" = "idle" ]; then
      cd /home/maggie && $UWF thread exec "$tid" -c 10 --background 2>&1
      sleep 5
      ALL_DONE=false
    elif [ "$STATUS" != "end" ] && [ "$STATUS" != "cancelled" ]; then
      ALL_DONE=false
    fi
  done
  $ALL_DONE && break
  sleep 60
done
```

**关键参数**：
- `sleep 8` between thread starts：CAS settle 防止 phantom thread
- `-c 20 --background`：一次跑20步（足够覆盖4角色）
- 自动 resume suspended/idle threads（API 429 限流会导致 suspended）

### API 限流处理

14个并发 thread 会触发 API 429 (rate limiting)。表现：
- rule-analyzer 或 data-extractor 阶段 suspended
- 日志显示 "HTTP 429: Request rate increased too quickly"

**处理**：batch runner 的 wait loop 自动检测 suspended 状态并 resume。不需要手动干预。

### 生成 Excel

```bash
# 单份合同
python3 scripts/nantong-excel-builder.py <thread-id> <校区名> <xlsx路径> [文件名]

# 批量（所有已完成的thread）
python3 scripts/batch-excel-builder.py <xlsx路径>
```

## Thread Read Quota 陷阱

`uwf thread read` 默认 quota 只有 4000 字符，**远远不够读取完整输出**（template-diff 和 rule-analyzer 输出通常 10000-30000 字符）。

**必须用**：`uwf thread read <id> --quota 200000 --start`
- `--quota 200000`：200K 字符足够
- `--start`：包含 init step

Excel builder 脚本已内置此参数，不需要手动指定。

## 测试结果（2026-06-27）

| 校区 | 合同数 | 总耗时 | 完成率 |
|------|--------|--------|--------|
| 金飞达 | 12份 | ~60分钟 | 12/12 end |
| 人民中路 | 2份 | ~60分钟 | 2/2 end |

14个并发 thread，API 限流导致部分 thread 需要 auto-resume，但全部完成。

## 已知问题

1. **Excel builder 解析精度**：各 thread 输出格式不完全一致，部分行的 C/F 列（合同类型/面积）可能为空
2. **K/L 列内容**：data-extractor 的 K/L 列有时写占位符而非实际分析内容（已修复 workflow prompt，但需验证）
3. **按租赁物分类排序**：batch builder 按 thread 完成顺序排列，不自动按"租赁物→合同性质→时间"排序——需要后续手动调整或增加排序逻辑
