---
name: paiban-maintain
description: Maintain and rebuild the two scheduling-tool versions (v1 area-claim, v2 nearest-branch) at D:\workbbbbbb — build, headless-Edge e2e, regression, and every known pitfall.
---

# 排班工具维护（v1 + v2）

改 D:\workbbbbbb\排班工具.html（第一版）与 D:\workbbbbbb\v2\排班工具_就近版.html（第二版）时，按本流程走。两版是**单文件 HTML 网页**，离线，数据存 localStorage，可导出 xlsx/CSV。

## 目录与职责

| 路径 | 说明 |
|---|---|
| `D:\workbbbbbb\build\` | v1 源码：`make.py`（拼装）、`app.css`、`body.html`、`app.js`（全部逻辑）、`shell.html`、`geodb.json`（地理库）、`dump.py`/`extract.py`（e2e 工具）、`gen_*_e2e.py`（各功能测试） |
| `D:\workbbbbbb\排班工具.html` | v1 产物（make.py 生成） |
| `D:\workbbbbbb\v2\build\` | v2 源码，含 `mk_v2_app.py`（从 v1 app.js 重放 v2 差异） |
| `D:\workbbbbbb\v2\排班工具_就近版.html` | v2 产物 |
| `D:\workbbbbbb\workb\` | 源表：`杭州机构表.xlsx`（182 机构 c/n/r1/r2/note）、`人员信息表.xlsx`（9 列，F~I 是「负责辖区」） |
| `D:\workbbbbbb\versions\` | 冻结存档 |
| `D:\workbbbbbb\paiban-skill\` | 本 skill 所在 bundle |

**v1 与 v2 只差排班排序方式，其余一样**：v1「同辖区优先→工作量→距离」，v2「机构就近优先（2km 档）→工作量→偏好→姓名」。v2 的 localStorage 键是 `paiban2_near_state_v1`，v1 是 `paiban_state_v1`，完全隔离。

## 工具绝对路径（PATH 里没有 node/python）

```bash
PY=/c/Users/黑书/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/python/python.exe
NODE=/c/Users/黑书/.dsh/dsh-runtimes/dsh-primary-runtime/dependencies/node/bin/node.exe
EDGE="/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
```

## 改动后必做（v1）

```bash
cd /d/workbbbbbb/build
"$NODE" --check app.js                       # 语法
"$PY" make.py                                # 构建，看 out= 行与 orgs/areas/people/geo 计数
```

v2 同理，目录换成 `/d/workbbbbbb/v2/build`；v2 的 app.js 由 `mk_v2_app.py` 从 v1 生成，**改 v2 前先想清楚该改 v1 还是 v2 差异**。

## 无头 Edge 自测/回归

```bash
taskkill //F //IM msedge.exe 2>/dev/null; sleep 2
"$PY" dump.py <html绝对路径> <dom绝对路径> 12000     # 必须绝对路径，否则 dom=0
"$PY" extract.py <dom> <out.txt>                     # 取 id="E2E" 节点，utf-8 落盘
```

- e2e 脚本 `gen_*_e2e.py` 生成 `_x_e2e.html`：把 ERRTRAP 插在 `<script>window.__PAIBAN_DATA__` 之前，TEST 插在 `rfind('</body>')` 之前。
- 回归基线：`build/gen_e2e.py`（v1）/ `v2/build/gen_v2_e2e.py`（v2）。断言必须以 `ERRS=none` 结尾才算绿。

## 踩坑清单（每条都真实踩过，别再犯）

1. **脚本注入用 `rfind('</body>')`**，绝不用 `str.replace("</body>", ...)`——会把 app.js 字符串里的 `'</tbody></table></body></html>'` 也替换掉，破坏 JS。
2. **注入脚本必须在 `window.__PAIBAN_DATA__` 之前**，否则 app.js 的 init 先读走旧 localStorage。
3. **Edge 无头**：用 `--headless=new` + 每次换新 `--user-data-dir`，且串行（两个实例同时启动会输出 0 字节）；先 `taskkill //F //IM msedge.exe`。
4. **终端中文乱码**：断言输出必须由脚本自己写 utf-8 文件，再用 read 工具看，别靠 stdout。
5. **`window.__PB.state` 是 getter 函数**，要 `__PB.state()` 调用，写成 `.state` 是 undefined。
6. **make.py 人员循环里局部变量禁止叫 `areas`**，会覆盖机构表的 areas（构建日志变成 `areas=3`）。
7. **OSRM table 接口**：`distances[0][k]` 用 `k` 不是 `k+1`，写错会整体错位一格、每批末尾缺 1。
8. **小区坐标不得抄网点坐标**（会算成 0 km）；小区坐标用 OSM photon `https://photon.komoot.io/api/?q=<小区名> 杭州&limit=3`，curl 要 `--noproxy '*'`，不能加 `lang=zh`（会 400），需带 `-H "Origin: http://localhost"` 拿 CORS。
9. **桌面被重定向**到 `D:\blackdark\桌面备份`（不是 C 盘），写快捷方式用注册表 `HKCU\...\User Shell Folders` 的 Desktop 值。
10. **python 写中文日志**要 `io.open(..., encoding='utf-8')`，重定向 stdout 会按 GBK 报 `UnicodeEncodeError`。
11. **heredoc 里 python 的 `'\\n'`** 会变成真换行写进源码，造成 `SyntaxError`；用 `chr(10)`。
12. **edit 替换函数头会残留旧函数体**成僵尸代码（如 renderGeoInfo），要整段删干净再 `node --check`。
13. **距离优先级**：`distP2O` 依次是 `GEO.drive[home][code]`（驾车）→ `GEO.dist[home][code]`（球面）→ `p.geo` → `GEO_HOME` → 兜底；`homeKey()` 按「互相包含」匹配带后缀住址（「圣苑小区」→「圣苑」）。
14. **curl 默认走 `proxy 127.0.0.1` 不通**，联网必须加 `--noproxy '*'`。

## 数据流与关键函数（app.js）

- `fresh()`/`loadState()`/`save()`（save 会 `scheduleXlsx` + `scheduleDisk` + 写 localStorage）。
- `generate()`：固定任务 `planFixed(cnt,busy)` 先占人 → 逐条安排 `evalDay()` 试算（候选排序见版本差异）→ 施加 load(分)/cnt(次)。
- 工作量三档：`WORK_NAME`（high/mid/low）、`workW()`（默认 3/2/1，设置页可改）、`workPts(pl)`。
- 辖区认领：`autoAssign()`/`reassignByHome()`（v1 用）；`quotas`+`solveAssign` 保证每人辖区数差≤1。
- 距离：`distP2O`（人→网点）、`distP2A`（人→辖区）、`driveMinP2O`（分钟）、`km`/`havKm`。
- 地理库 `geodb.json`：`orgs`/`homes`/`dist`/`drive`/`driveMin`/`areaCenters`；`geo_build.py`（球面 dist）、`route_build.py`（驾车 drive，需联网）、`reassign_geo.py`（按 drive 重算认领写回 xlsx）。

## 验收标准

- `node --check` 过；`make.py` 打印 `orgs=182 areas=26 people=8 geo=182 orgs / 10 homes / 1820 distPairs / drive=1820`。
- e2e `ERRS=none` 且关键断言（resultRows、hasLeaveGuy=true、peopleRows=8、histWeeks2=2 等）全过。
- 产品文件 `grep -c "https\?://"` 只允许 OOXML 命名空间 + photon 接口，其余 0。
- 收尾清理 `_*.html/_*.txt/_*.json` 临时文件，CG 归档（cg op=write），`present` 交付。
