# 运维手册（日常工作）

**最后更新**：2026-09-21

## 一、每天要做的事（只有一步是人工）

### 第 1 步：人工在通达信里下载数据（约 1 分钟）

> **必须人工，无法自动化。** 原因与完整证据见 `自动化边界.md`——
> 通达信顶部菜单栏对**所有**合成输入免疫（鼠标点击 / SendInput / Alt /
> UI Automation 全部实测无效），而下载入口只在那个菜单里。

操作：

1. 打开通达信 → 登录
2. 顶部 **选项** → **盘后数据下载** → 勾选「**日线和实时行情数据**」→ 点「**开始下载**」
3. 顶部 **选项** → **专业财务数据** → **三个**「开始下载」按钮依次点击
4. 等全部下载完成 → **关闭通达信**

> 官方提示：交易日在 **15:45** 之后才能下载到当天数据。

### 第 2 步：跑更新（一条命令）

```cmd
C:\FundamentalWorkbench\deploy\daily_all.cmd
```

它内部依次执行：

1. `run_daily_update.cmd` → **M1 数据刷新 → M2 因子研究 → 生成看板**
2. `build_full_market_catalog.py` → 重建全市场目录（让目录与看板同源）

耗时约 **15–27 分钟**。

### 第 3 步：确认（一条命令）

```powershell
powershell -ExecutionPolicy Bypass -File C:\FundamentalWorkbench\deploy\health_check.ps1
```

输出 `VERDICT: OK` 即正常。它会检查：服务存活、数据新鲜度、看板状态、
`runId` 一致性、磁盘空间、公网可达性。

## 二、要看网页时

```powershell
Get-Content C:\FundamentalWorkbench\deploy\public_url.txt
```

本地地址固定：`http://127.0.0.1:8765/reports/dashboard/index.html`

> ⚠️ 公网地址是 **Cloudflare 快速隧道**，**每次隧道重启都会变**。
> 小主机不重启的话它会一直有效。

## 三、关键路径

| 项目 | 路径 |
| --- | --- |
| 项目根目录 | `C:\FundamentalWorkbench` |
| 通达信 | `C:\new_tdx64` |
| 数据产物 | `C:\FundamentalWorkbench\data\processed` |
| 看板 | `C:\FundamentalWorkbench\reports\dashboard\index.html` |
| 日更日志 | `C:\FundamentalWorkbench\reports\dashboard\logs` |
| 部署脚本 | `C:\FundamentalWorkbench\deploy` |

## 四、deploy 目录里的脚本

| 文件 | 用途 | 何时用 |
| --- | --- | --- |
| `daily_all.cmd` | 日更 + 目录重建 | **每天必跑** |
| `health_check.ps1` | 只读健康检查 | 跑完更新后确认 |
| `build_full_market_catalog.py` | 单独重建全市场目录 | 一般不用（已被 daily_all 包含） |
| `watchdog.vbs` | 看护看板服务与隧道 | 已放进启动文件夹，自动运行 |
| `start_dashboard_server.ps1` | 起本地看板服务 | 由看门狗调用 |
| `start_tunnel.ps1` | 起公网隧道 | 由看门狗调用 |
| `install_tasks.ps1` | 注册开机计划任务 | **需管理员**，目前未用 |
| `tdx_e2e_download.ps1` | 启动+登录通达信 | **部分可用**，菜单步骤无效，勿无人值守跑 |

## 五、服务是怎么保活的

```
启动文件夹
  └─ FundamentalWorkbenchWatchdog.vbs
        └─ watchdog.vbs（每 60 秒轮询一次）
              ├─ 看板服务不在 → 拉起 start_dashboard_server.ps1
              └─ 隧道不在     → 拉起 start_tunnel.ps1
```

**限制**：启动文件夹只在**用户登录后**执行。主机重启若停在登录界面，
看板服务与隧道都不会起来。`AutoAdminLogon` 目前为 `0`（未配置自动登录）。

## 六、已知的重要限制

1. **下载必须人工**（通达信菜单栏拒绝合成输入）
2. **公网地址不固定**（快速隧道；重启即变）
3. **看板无访问验证**，任何拿到 URL 的人都能看到全部投研数据
4. **日线历史仅约 5 年**（2021-08-02 起）。项目原有数据可回溯到 1991/1999，
   所以 M2 因子回测的历史深度比原机器浅（61 期 / 训练 24 期）。
   如需更长历史，可用官方 `hsjday.zip`（523.8MB，含 1991 年起全量）补种。
5. **主机重启后需人工登录**，否则服务不会自启（见第五节）

## 七、跑挂了怎么办

### 更新失败

先看 `reports\dashboard\update_status.json` 的 `state` / `stage` / `message`，
再读 `logPath` 指向的日志末尾。**旧看板会保留可用**（原子替换设计）。

按 `自动化边界.md` 与提示词的失败分类判断：

| 现象 | 分类 | 处理 |
| --- | --- | --- |
| 网络超时/DNS 失败 | `TRANSIENT_NETWORK` | 可重跑一次 |
| 通达信数据没下到 | `SOURCE_NOT_READY` | 检查下载是否完成 |
| 缺 Python 模块 | `DEPENDENCY_MISSING` | 装进 `vendor\` |
| `ready=false` | `ENV_CHECK_FAILED` | 跑 `run_daily_update.cmd --check` 看缺什么 |
| 退出码 2 | `LOCKED` | 已有日更在跑，**别杀进程** |

### 网页打不开

```powershell
# 1. 看服务在不在
Get-NetTCPConnection -State Listen -LocalPort 8765

# 2. 看 Tailscale 与 Funnel 是否正常
$ts='C:\Program Files\Tailscale\tailscale.exe'
& $ts status          # 应显示 100.120.247.24 那一行
& $ts funnel status   # 应显示 "Funnel on"

# 3. 看门狗在不在（wscript 和 cscript 都要查）
$filter="Name='wscript.exe'"
@(Get-CimInstance Win32_Process -Filter $filter | Where-Object { $_.CommandLine -like '*watchdog*' })
@(Get-CimInstance Win32_Process -Filter "Name='cscript.exe'" | Where-Object { $_.CommandLine -like '*watchdog*' })

# 4. 看门狗不在就手动拉起来
Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = 'wscript.exe "C:\FundamentalWorkbench\deploy\watchdog.vbs"' }

# 5. Funnel 被关掉了就重开
& $ts funnel --bg 8765
```

## 八、曾踩过的坑（避免重犯）

| 坑 | 教训 |
| --- | --- |
| `.ps1` / `.cmd` 里写中文注释 | **必须纯 ASCII**。PowerShell 5.1 按 ANSI 读，cmd.exe 按 GBK 读，UTF-8 中文会把文件解析坏 |
| 用 `Set-Content -Encoding UTF8` 写文本文件 | PowerShell 5.1 **会加 UTF-8 BOM**。`tomllib` 不接受 BOM；`public_url.txt` 带 BOM 会让看门狗的健康探测全部失败。一律用 `[System.IO.File]::WriteAllText($p,$s,[System.Text.UTF8Encoding]::new($false))` |
| 改 `T0002\user.ini` | 它是 **GBK**，必须用 `Encoding.GetEncoding(936)` 读写，否则中文字体名乱码 |
| 用手动方式启动隧道 | 看门狗已负责拉起，**手动启动会产生重复隧道** |
| **PowerShell 里用 `\"` 转义引号做进程检查** | **会产生假结果**（`-Filter` 被破坏，永远返回空）。我因此在 9-17 和 10-08 **两次**误判看门狗没运行，重复启动实例。**必须把过滤器写成变量**：`$filter="Name='wscript.exe'"; Get-CimInstance -Filter $filter` |
| **检查脚本宿主只过滤 `wscript.exe`** | 会漏掉 `cscript.exe`。我因此没发现一个 10-08 启动的旧看门狗**跑了 5 天**，并持续改写 `public_url.txt`。**必须同时查 `wscript.exe` 和 `cscript.exe`** |
| **中途关闭更新窗口** | 会留下**被截断的 parquet**（`daily_price.parquet` 末 4 字节不是 `PAR1`）**和残留锁**。而且损坏文件比源文件新时，缓存逻辑会**跳过重建**，导致重跑一直失败。需要检查 `data\processed\*.parquet` 的首尾字节，必要时用 `build_daily_price(settings, force=True)` 强制重建 |
| **用 `Set-Content -Encoding UTF8` 写配置文件** | PowerShell 5.1 **会加 UTF-8 BOM**。`tomllib` 不接受；看门狗读 `public_url.txt` 时带 BOM 会导致地址解析为空、进而**误杀健康隧道**。一律用 `[System.IO.File]::WriteAllText($p,$s,[System.Text.UTF8Encoding]::new($false))`；读取端也要容错 |
| **用 `ADODB.Stream` 读被重定向独占的日志** | 读不到且被 `On Error Resume Next` 静默吞掉，检查形同虚设。改为直接探测公网地址 |
| **用 `taskkill` 杀进程（在 VBScript 里）** | 实测静默失败。改用 WMI 的 `Win32_Process.Terminate()`，并记录返回值 |
| **改了启动脚本就以为生效了** | 启动脚本含 `while($true)` 重启循环，**旧实例会一直活着**并继续用旧参数拉起子进程，让改动看起来"没生效"。我遇到过两次：一个 9-14 启动的 `start_dashboard_server.ps1`、一个**跑了 29.7 天**的 `start_tunnel.ps1`（后者持续把已停用的隧道拉回来）。**改完必须杀掉旧启动器进程，或重启整机**。审计命令见下 |
| **过滤进程时匹配到了自己** | 命令文本里含目标关键字（如 `start_dashboard_server`）时，`Where-Object { $_.CommandLine -like '*关键字*' }` **会匹配执行命令的自己**，可能把自己杀掉。必须排除 `$PID`，并把模式写得更具体（如 `-like '*-File*脚本名.ps1*'`） |
| `run_daily_update.cmd --check` 用了 `data/input` 空目录 | 曾触发 `audit_legacy_csv` 的 `KeyError`，已修复（见 `缺陷报告-audit_legacy_csv.md`） |

### 审计"僵尸启动器"（改动不生效时先查这个）

列出所有运行超过 1 小时的脚本宿主，看有没有旧的循环实例：

```powershell
$now = Get-Date
Get-CimInstance Win32_Process |
  Where-Object { $_.Name -in 'powershell.exe','wscript.exe','cscript.exe' } |
  ForEach-Object {
    $age = ($now - $_.CreationDate).TotalHours
    if ($age -gt 1) { [pscustomobject]@{ Pid=$_.ProcessId; 小时=[math]::Round($age,1); Cmd=$_.CommandLine } }
  } | Sort-Object 小时 -Descending | Format-Table -AutoSize -Wrap
```

正常的常驻进程只有：`start_dashboard_server.ps1`（看板启动器）和看门狗
（`wscript.exe watchdog.vbs`）。**任何 `start_tunnel.ps1` 都是残留，应当杀掉。**

## 九、当前状态快照（2026-10-14）

```
看板 asOf      : 2026-10-13（10-14 数据待处理）
因子截面       : 2026-09-30
runId          : dd143654dc5249b6bc090bcf9b4042b6
公网地址       : https://desktop-ue6cc4h.tail87341f.ts.net/reports/dashboard/index.html
公网方式       : Tailscale Funnel（固定、免费、自动 HTTPS）
健康检查       : VERDICT WARN（仅因当日数据未处理）
```

### 中断更新后如何自检

若更新中途被关闭，可能留下**截断的 parquet** 与**残留锁**：

```powershell
# 1. 检查 parquet 完整性：首尾 4 字节都应是 50 41 52 31 (PAR1)
$p="C:\FundamentalWorkbench\data\processed\daily_price.parquet"
$b=[System.IO.File]::ReadAllBytes($p)
"首:$(($b[0..3]|%{$_.ToString('X2')}) -join ' ') 末:$(($b[-4..-1]|%{$_.ToString('X2')}) -join ' ')"

# 2. 检查残留锁（锁里的 pid 若不存在即为残留，新逻辑会自动接管）
Get-Content C:\FundamentalWorkbench\reports\dashboard\.daily_update.lock
```

> 锁现在会**自动识别持有进程是否存活**，残留锁不再阻塞运行。
> 但截断的 parquet 会因为"比源文件新"被缓存逻辑跳过，必须强制重建。

### 检查看门狗要同时覆盖两种宿主

```powershell
$filter="Name='wscript.exe'"
@(Get-CimInstance Win32_Process -Filter $filter | Where-Object { $_.CommandLine -like '*watchdog*' })
Get-CimInstance Win32_Process -Filter "Name='cscript.exe'" | Where-Object { $_.CommandLine -like '*watchdog*' }
```

**必须两种都查**——我曾漏掉一个跑了 5 天的 `cscript.exe` 旧看门狗。

看门狗自己的动作记录在 `deploy\logs\watchdog.log`。

### 公网访问：Tailscale Funnel（2026-10-13 起）

**固定公网地址（永久不变，任何设备任何网络可访问，无需装客户端）：**

```
https://desktop-ue6cc4h.tail87341f.ts.net/reports/dashboard/index.html
```

架构：

```
公网 → Tailscale Funnel → tailscaled → http://127.0.0.1:8765
```

**为什么换掉了 Cloudflare 快速隧道**：它的地址每次重启都变，而且会被 Cloudflare
在服务端注销（实测寿命 4.7 天、5 天、24 天，不可预测）。每次注销都要人工介入，
还引出过看门狗误杀、地址文件被覆盖等一连串问题。

Tailscale Funnel 的优势：

- **地址永久固定**
- **自动 HTTPS 证书**
- **不需要开端口、不需要防火墙规则**（走 tailscaled 已建立的出站隧道）
- **免费**（个人版）

常用命令：

```powershell
$ts='C:\Program Files\Tailscale\tailscale.exe'
& $ts status           # 连接状态与 IP
& $ts funnel status    # 公网发布状态，应显示 "Funnel on"
& $ts funnel --bg 8765 # 若被关掉，用这条重新开启
```

> `tailscaled` 服务是 **Automatic**，开机自启，Funnel 配置持久化，无需人工维护。
> 看门狗**不再管理隧道**（相关逻辑已于 2026-10-13 移除）。

历史脚本 `deploy\start_tunnel.ps1` 已重命名为 **`start_tunnel.ps1.retired`**
（改名是为了防止被误执行；内容保留以便将来需要时参考）。

连续跑通记录：**9-14 / 9-15 / 9-16 / 9-17 / 9-18 / 9-21 / 9-23 / 9-24 / 9-28 / 9-29 / 10-08 / 10-12 / 10-13**
