docs: plan v2 — fix all P0/P1 (deps, error status, redo keys, health-bound, i18n)

This commit is contained in:
LLLin000 2026-07-05 00:36:21 +08:00
parent 069c344af7
commit 44bbad9250

View file

@ -1,104 +1,167 @@
# OCR 维护标签页 — Implementation Plan
# OCR 维护标签页 — Implementation Plan (v2)
> Based on: `docs/superpowers/specs/2026-07-05-ocr-maintenance-tab-design.md`
> Status: Draft plan
> Review: 7.8/10 → P0/P1 fixes applied. Status: Ready for execution.
## 概述
## 执行顺序
3 个独立 PR可并行执行
```
PR A → PR B → PR C → PR D (可选)
| PR | 范围 | 依赖 |
|----|------|------|
| **A** | 后端 row model + display contract | 无 |
| **B** | CLI `--manifest` / `--keys` 增量接口 | 无(依赖 A 的 to_dict 扩展,但接口可先做) |
| **C** | Plugin 维护标签 UI + 缓存 | A + B |
PR A: 后端 display contract + manifest
PR B: CLI keyed actions + manifest/keys
PR C: Plugin 维护标签 UI + 缓存
PR D: Base view action labels可选见下方讨论
```
**⚠️ 不是独立 PR。A 必须先合并B 基于 AC 依赖 A+B。**
如果想 A/B 并行PR B 需要自己 stub manifest 函数,不建议。
---
# PR A: Row Model Display Contract
## Non-goals本轮不做
- **Layer 2 质量指标展示** — quality_indicators、user_readiness 不在维护标签展示
- **Dashboard per-paper quality detail** — 留给后续 Layer 3
- **Base view action label migration** — 本轮 **不** 改 Base views
> 关于 Baseplan 只实现维护标签页。PR A 的 `display_label` 可以被 Base 未来消费,但本 plan 不包含 Base 改造。Base 仍然使用 `status/health` 原始值。
如果要求在 Base 也避免显示 `failed/degraded`,需要加 PR D。
---
# PR A: 后端 display contract + manifest
**文件:**
- `paperforge/worker/ocr_maintenance.py``OCRMaintenanceRow` + display 映射 + `compute_maintenance_manifest()`
- `tests/test_ocr_maintenance.py` — 新增(不存在)
- `paperforge/worker/ocr_maintenance.py``OCRMaintenanceRow` + `_compute_display_fields()` + `compute_maintenance_manifest()`
- `tests/test_ocr_maintenance.py` — 新增(现存无
## A1. OCRMaintenanceRow 新增 display 字段
## A1. Row model 新增 display 字段
在现有 `@dataclass` 底部追加:
`OCRMaintenanceRow` dataclass 底部追加:
```python
display_action: str = "none"
display_label: str = "已完成"
display_reason: str = ""
display_group: str = "hidden"
display_severity: str = "normal"
display_action: str = "none" # retry_ocr / rebuild_result / upgrade_legacy / add_pdf / configure_ocr / none
display_label: str = "已完成" # 用户可见的中文标签fallback前端可用 i18n 覆盖)
display_label_key: str = "" # i18n key前端优先用这个
display_reason: str = "" # 用户可见的中文原因(一行)
display_reason_key: str = "" # i18n key前端优先用这个
display_group: str = "hidden" # retry / rebuild / legacy_optional / external_action / hidden
display_severity: str = "normal" # actionable / optional / external / normal
visible_in_maintenance: bool = False
```
`to_dict()` 加入这 6 个字段。
`to_dict()` 加入全部新增字段。
## A2. `_compute_display_fields()` 函数
## A2. `_compute_display_fields()` — 映射规则
新增函数,接收 `(meta, health, has_raw, has_source_meta, status, health_overall, version)`,返回 display 字段 dict。
新增函数,签名:
实现 spec §Row Model 新增字段 → 映射规则的完整 if/elif 链:
```
failed / done_incomplete / retryable_error + can_redo
→ display_action=retry_ocr, label=重试 OCR, group=retry, visible=True
version==v1 + can_redo
→ display_action=upgrade_legacy, label=升级旧结果, group=legacy_optional, visible=True
done_degraded + can_rebuild
→ display_action=rebuild_result, label=重建结果, group=rebuild, visible=True
done + health≠green + can_rebuild
→ display_action=rebuild_result, label=重建结果, group=rebuild, visible=True
done_degraded + !can_rebuild + can_redo
→ display_action=retry_ocr, label=重试 OCR, group=retry, visible=True
nopdf
→ display_action=add_pdf, label=补充 PDF, group=external_action, visible=False
blocked
→ display_action=configure_ocr, label=配置 OCR, group=external_action, visible=False
else
→ display_action=none, label=已完成, group=hidden, visible=False
最后:如果 visible 但 !can_redo 且 !can_rebuild → 改为 hidden
```python
def _compute_display_fields(
status: str,
health_overall: str,
version: str,
can_redo: bool,
can_rebuild: bool,
error_stage: str = "",
error_summary: str = "",
degraded_reasons: list[str] | None = None,
) -> dict:
```
`display_reason` 随分支填入中文简述。
### 规则(顺序敏感,第一条匹配即返回)
## A3. 在 `collect_maintenance_rows()` 中调用
```python
# ── ready 状态:不显示在维护页,但 label 对 Base 正确 ──
if status in ("pending",):
return {display_action="none", label="等待处理", group="hidden", severity="normal", visible=False}
`OCRMaintenanceRow(...)` 构造完成后,调用 `_compute_display_fields()`,将返回的 display 字段赋值到 row。
if status in ("running", "queued", "processing"):
return {display_action="none", label="处理中", group="hidden", severity="normal", visible=False}
也可以在构造时直接传入——取决于方便程度。
# ── retry: 需要重跑的 ──
# 覆盖真实管线中所有表示失败的状态值
if status in ("failed", "error", "fatal_error", "done_incomplete", "retryable_error") and can_redo:
return {display_action="retry_ocr", label="重试 OCR", group="retry", severity="actionable", visible=True,
reason="上次处理未完成,可以重新尝试"}
## A4. `compute_maintenance_manifest()` 新增函数
# ── legacy: 可选升级 ──
if version == "v1" and can_redo:
return {display_action="upgrade_legacy", label="升级旧结果", group="legacy_optional", severity="optional", visible=True,
reason="旧版本结果仍然可用,升级后可获得更好的章节、图表和问答效果"}
# ── rebuild: 可重建 ──
# 注意health ≠ green 必须限于已知的 yellow/red
# health="-" / "" / "unknown" / missing 不能当异常
_health_is_bad = health_overall in {"yellow", "red"}
if status == "done_degraded" and can_rebuild:
return {display_action="rebuild_result", label="重建结果", group="rebuild", severity="actionable", visible=True,
reason="已有OCR数据可重建获得更稳定的结果"}
if status == "done" and _health_is_bad and can_rebuild:
return {display_action="rebuild_result", label="重建结果", group="rebuild", severity="actionable", visible=True,
reason="已有OCR数据可重建新版结果"}
if status == "done_degraded" and not can_rebuild and can_redo:
return {display_action="retry_ocr", label="重试 OCR", group="retry", severity="actionable", visible=True,
reason="降级结果无法重建可重新OCR"}
# ── 不进维护页的外部行动 ──
if status == "nopdf":
return {display_action="add_pdf", label="补充 PDF", group="external_action", severity="external", visible=False,
reason="请去 Zotero 添加 PDF 文件"}
if status == "blocked":
return {display_action="configure_ocr", label="配置 OCR", group="external_action", severity="external", visible=False,
reason="请配置 PaddleOCR API Token"}
# ── 正常 / 其他 → 已完成 ──
if status == "done" and not _health_is_bad:
return {display_action="none", label="已完成", group="hidden", severity="normal", visible=False}
# ── 排除规则:修不了的不显示 ──
# done + health_bad 但 !can_rebuild 且 !can_redo → hidden
# done_degraded 但 !can_rebuild 且 !can_redo → hidden
if not can_redo and not can_rebuild:
return {display_action="none", label="已完成", group="hidden", severity="normal", visible=False}
# ── fallback ──
return {display_action="none", label="已完成", group="hidden", severity="normal", visible=False}
```
### 关键边界规则
1. `health≠green` 必须显式只认 `health_overall in {"yellow", "red"}`。`-` / `""` / `"unknown"` / missing 不算"可重建"
2. `pending/running/queued/processing``display_label``"等待处理"` / `"处理中"`,不是 `"已完成"`
3. `status` 覆盖 `error`(现有 pipeline 会写 `meta["ocr_status"] = "error"`
## A3. 在 `collect_maintenance_rows()` 中集成
在 row 构造后调用 `_compute_display_fields()` 赋值。
## A4. `compute_maintenance_manifest()` — 轻量 manifest
```python
def compute_maintenance_manifest(vault: Path) -> dict[str, str]:
"""Return {key: sha256(concat(...))} for all OCR papers.
Lighter than collect_maintenance_rows — no big file reads.
Only reads meta.json + health/ocr_health.json + title_by_key.
"""Return {key: sha256} for all OCR papers.
Only reads meta.json + health/ocr_health.json.
Does NOT read source_metadata, raw blocks, or formal-library index.
"""
```
hash 输入字段(按顺序拼接,`|` 分隔):
hash 输入字段(`|` 分隔,顺序固定
```
key | status | health | version | recommended_action |
display_action | display_label | display_reason | display_group |
can_redo | can_rebuild |
error_stage | error_summary | degraded_reasons
key | status | health | version | recommended_action
| display_action | display_group | display_severity | display_reason_key
| can_redo | can_rebuild
| error_stage | error_summary | degraded_reasons
```
可复用 `collect_maintenance_rows` 的 paper_dir 遍历逻辑,但跳过 raw blocks / source_metadata 等大文件读取。
**`display_label` 和 `display_reason` 不进 hash**,因为中文文案变化不应导致全量缓存过期。
`display_reason_key``display_action` 代替。
## A5. 测试
@ -106,34 +169,37 @@ error_stage | error_summary | degraded_reasons
| # | 场景 | 预期 |
|---|------|------|
| 1 | failed + can_redo | `retry_ocr` / `重试 OCR` / group=retry / visible=True |
| 2 | done_degraded + can_rebuild | `rebuild_result` / `重建结果` |
| 3 | done + health=red + can_rebuild | `rebuild_result` / 同上 |
| 4 | version=v1 + can_redo | `upgrade_legacy` / `升级旧结果` / group=legacy_optional |
| 5 | done_degraded + !can_rebuild + can_redo | `retry_ocr` |
| 6 | done_degraded + !can_rebuild + !can_redo | hidden |
| 7 | done + health≠green + !can_rebuild + !can_redo | hidden |
| 8 | nopdf | `add_pdf` / visible=False |
| 9 | blocked | `configure_ocr` / visible=False |
| 10 | done + green | `none` / hidden |
| 11 | pending / running / queued | `none` / hidden |
| 12 | display_reason 不为空 | 至少 3 个场景有中文原因 |
| 13 | manifest hash 一致性 | 同一输入产出相同 hash |
| 14 | manifest hash 对 display 字段敏感 | display_label 变化 → hash 变化 |
| 1 | `failed` + `can_redo` | `retry_ocr` / `重试 OCR` / group=retry / visible=True |
| 2 | `error` + `can_redo` | `retry_ocr`(覆盖真实管线状态) |
| 3 | `done_degraded` + `can_rebuild` | `rebuild_result` / `重建结果` |
| 4 | `done` + health=red + `can_rebuild` | `rebuild_result` |
| 5 | `done` + health=yellow + `can_rebuild` | `rebuild_result` |
| 6 | `done` + health=`"-"` + `can_rebuild` | `none` / hidden不属于已知异常 |
| 7 | `done` + health=`"unknown"` + `can_rebuild` | `none` / hidden |
| 8 | version=v1 + `can_redo` | `upgrade_legacy` / `升级旧结果` / group=legacy_optional |
| 9 | `done_degraded` + `!can_rebuild` + `can_redo` | `retry_ocr` |
| 10 | `done_degraded` + `!can_rebuild` + `!can_redo` | `none` / hidden |
| 11 | `done` + health=red + `!can_rebuild` + `!can_redo` | `none` / hidden |
| 12 | `nopdf` | `add_pdf` / visible=False |
| 13 | `blocked` | `configure_ocr` / visible=False |
| 14 | `pending` | `等待处理` / hiddenlabel 对 Base 正确) |
| 15 | `running` / `queued` / `processing` | `处理中` / hidden |
| 16 | `done` + green | `已完成` / hidden |
| 17 | display_reason 不含内部诊断词 | assert no "failed"/"degraded"/"health"/"质量降级"/"fatal" |
| 18 | manifest hash 一致性 | 同一输入 → 相同 hash |
| 19 | manifest hash 对 display_action 敏感 | 改 display_action → hash 变 |
| 20 | manifest 不依赖 title_by_key | 不读 source_metadata / formal-library |
---
# PR B: CLI `--manifest` / `--keys`
# PR B: CLI — manifest/keys + keyed redo
**文件:**
- `paperforge/cli.py` — 参数定义
- `paperforge/commands/ocr.py``_run_ocr_list()` 扩展
- `paperforge/worker/ocr_maintenance.py`PR A 已加 `compute_maintenance_manifest`
- `paperforge/cli.py``list_parser``redo_parser`
- `paperforge/commands/ocr.py``_run_ocr_list()` + `_run_ocr_redo()`
- `tests/test_ocr_maintenance.py` — 追加 CLI 测试
## B1. CLI 参数
`list_parser` 追加:
## B1. `list` 子命令新增参数
```python
list_parser.add_argument("--manifest", action="store_true",
@ -142,236 +208,175 @@ list_parser.add_argument("--keys", nargs="*", metavar="KEY",
help="Only output rows for these specific keys")
```
## B2. `_run_ocr_list()` 扩展
## B2. `redo` 子命令添加 keys 参数
**P0现有 `redo` 不支持 positional keys必须加。**
```python
redo_parser.add_argument("keys", nargs="*", metavar="KEY",
help="Paper keys to redo (all actionable if empty)")
```
扩展 `_run_ocr_redo()`
```python
def _run_ocr_redo(vault, keys=None, dry_run=False, verbose=False, no_progress=False):
papers = [...] # 如 keys 则仅处理 keys否则处理全部
```
## B3. `_run_ocr_list()` 逻辑
```python
def _run_ocr_list(vault, json_output=False, output_file=None,
manifest=False, keys=None):
```
### --manifest 模式
- 如果 `manifest=True`
- 调用 `compute_maintenance_manifest(vault)` 获取 `{key: hash}` dict
- JSON 输出到 stdout 或 output_file
- 不执行 `collect_maintenance_rows()`(快很多)
- `manifest=True`:调用 `compute_maintenance_manifest()` → JSON 输出。`collect_maintenance_rows()` 不调用。
- `keys` 非空:调用 `collect_maintenance_rows()` → 只输出 key 在 `keys` 中的行。
- 优先级:`manifest` > `keys`
### --keys 过滤模式
- 如果提供了 `keys`list[str]
- 调用 `collect_maintenance_rows(vault)` 获取全部 rows
- 只输出 key 在 `keys` 中的 rows
- 保持 JSON 完整格式
### 优先级:--manifest 优先于 --keys
逻辑:
```
if manifest:
output manifest dict
else:
full = collect_maintenance_rows(vault)
if keys:
full = [r for r in full if r.key in keys]
if json_output:
output [r.to_dict() for r in full]
```
## B3. 测试
## B4. 测试
| # | 场景 | 预期 |
|---|------|------|
| 1 | `paperforge ocr list --manifest` | 输出 JSON dictkey=hash |
| 2 | manifest 包含 display 字段变化 | hash 不一致时触发 |
| 1 | `paperforge ocr list --manifest` | JSON `{"key": "hash", ...}` |
| 2 | manifest 不调用 `collect_maintenance_rows()` | mock 验证(非时间比) |
| 3 | `paperforge ocr list --json --keys KEY1 KEY2` | 只输出 2 行 |
| 4 | `--keys` 有不存在的 key | 忽略,不报错 |
| 5 | `--manifest` 速度测试 | 应比全量快 10x+ |
| 6 | `_run_ocr_list` 签名兼容 | 不破坏已部署的 json_output / output_file |
| 4 | `--keys` 含不存在的 key | 忽略,不报错 |
| 5 | 签名兼容 | 不破坏已有 `json_output` / `output_file` |
| 6 | `paperforge ocr redo KEY1 KEY2` | 只重跑这 2 篇 |
| 7 | `paperforge ocr redo`(无 keys | 重跑全部(兼容旧行为) |
---
# PR C: Plugin 维护标签 UI + 缓存
**文件:**
- `paperforge/plugin/src/services/ocr-maintenance-ui.ts` — 扩展
- `paperforge/plugin/src/services/ocr-maintenance-ui.ts` — 扩展(缓存的函数 + 类型)
- `paperforge/plugin/src/settings.ts``_renderMaintenanceTab()` 重写
- `paperforge/plugin/i18n.ts`中文文案
- `paperforge/plugin/i18n.ts` — 新增文案
- `tests/ocr-maintenance-ui.test.ts` — 追加
## C1. 服务层扩展
## C1. 服务层新增
`ocr-maintenance-ui.ts`
### 新增类型
```typescript
export type DisplayAction = "retry_ocr" | "rebuild_result" | "upgrade_legacy" | "add_pdf" | "configure_ocr" | "none";
export type DisplayGroup = "retry" | "rebuild" | "legacy_optional" | "external_action" | "hidden";
export type DisplaySeverity = "actionable" | "optional" | "external" | "normal";
export interface MaintenanceDisplayRow {
key: string;
title: string;
display_action: DisplayAction;
display_label: string;
display_reason: string;
display_group: DisplayGroup;
visible_in_maintenance: boolean;
// 后端已有字段
can_redo: boolean;
can_rebuild: boolean;
}
```
### `categorizeMaintenanceRow()` 保持兼容
现有函数已返回 `{category, label, primaryAction, reason}`。保持它运行,新增一个对等函数给新 UI 用。
可以考虑加一个适配层,或者直接让新 UI 解析 `display_action/display_group`
决策:**新 UI 直接读 `display_action` / `display_group`**,不通过 `categorizeMaintenanceRow()`
## C2. 缓存管理
### `readMaintenanceCache()` / `writeMaintenanceCache()`
```typescript
interface MaintenanceCache {
export interface MaintenanceCache {
manifest: Record<string, string>;
papers: Record<string, MaintenanceDisplayRow>;
cached_at: string;
}
export function readMaintenanceCache(vaultPath: string): MaintenanceCache | null;
export function writeMaintenanceCache(vaultPath: string, cache: MaintenanceCache): void;
```
路径:`{vault}/System/PaperForge/cache/ocr_maintenance.json`
**重要:`categorizeMaintenanceRow()` 不再被新 UI 调用。**
PR C 会删除 settings.ts 中对旧函数的引用,或者将它适配成 display fields 的薄包装。
### 刷新逻辑
## C2. 三阶段渲染
```
Phase 1: 读缓存 → 立即显示(<2ms
Phase 2: 后台 paperforge ocr list --manifest
├─ 匹配 → 不动,停止
└─ 不匹配 → 刷新图标旋转
paperforge ocr list --json --keys K1 K2
合并缓存 → 局部更新表格 → 停转
Phase 3: 无缓存 → 全量拉取并写入缓存
```
**`--keys` 调用格式:**
```typescript
async function refreshMaintenanceData(
vaultPath: string,
pythonExe: string,
extraArgs: string[],
currentCache: MaintenanceCache | null
): Promise<{ data: MaintenanceDisplayRow[]; manifest: Record<string, string>; changed: boolean }>
// ✅ 正确
const args = ["-m", "paperforge", "ocr", "list", "--json", "--keys", ...changedKeys];
// ❌ 错误
// "--keys=K1,K2" 不可用nargs="*" 不会拆分逗号)
```
1. execFile `paperforge ocr list --manifest`
2. 比对 manifest
- 一致 → 返回缓存数据changed=false
- 不一致 → 找出变化 keyexecFile `paperforge ocr list --json --keys=K1,K2`
3. 合并到缓存并写入文件
4. 返回完整数据 + changed
## C3. 分组渲染
## C3. `_renderMaintenanceTab()` 重写
| 组 | group | 标题 | 默认 |
|----|-------|------|------|
| 1 | `retry` | 需要重试 | 展开 |
| 2 | `rebuild` | 可重建结果 | 展开 |
| 3 | `legacy_optional` | 可升级旧结果(可选) | 折叠 |
### 三阶段渲染
```typescript
// Phase 1: 读缓存 → 立即显示
const cache = readMaintenanceCache(vaultPath);
if (cache) {
renderTable(filterVisible(cache.papers));
}
// Phase 2: 后台刷新
const result = await refreshMaintenanceData(...);
if (result.changed) {
updateTable(result.data); // 局部更新
stopSpin(); // 停转
}
// Phase 3: 首次无缓存
if (!cache) {
const result = await fullFetch(...);
renderTable(result.data);
writeCache(result);
}
```
### 分组渲染
表格列:
```
组 1: retry → 标题 "需要重试"
组 2: rebuild → 标题 "可重建结果"
组 3: legacy → 标题 "可升级旧结果(可选)"(默认折叠 <details>
☑ | Key | Title | 建议操作 | 原因 | 操作按钮
```
每组的表格
操作按钮映射:
| ☑ | Key | Title | 建议操作 | 原因 | 操作 |
|---|---|---|---|---|---|
| ☑ | 2BB... | 标题 | 重试 OCR | API 失败 | [重试] |
| ☐ | 53B... | 标题 | 重建结果 | 质量降级 | [重建] |
| display_action | 按钮文字 | CLI 命令 |
|---------------|---------|---------|
| `retry_ocr` | 重试 | `paperforge ocr redo KEY` |
| `rebuild_result` | 重建 | `paperforge ocr rebuild KEY` |
| `upgrade_legacy` | 升级 | `paperforge ocr redo KEY` |
工具栏:全选 → 取消全选 → ▶ 执行已选
### 操作执行
## C4. `visible_in_maintenance=false` 的处理
- "重试" → `execFile(pythonExe, ["-m", "paperforge", "ocr", "redo", ...keys])`
- "重建" → `execFile(pythonExe, ["-m", "paperforge", "ocr", "rebuild", ...keys])`
- "升级" → `execFile(pythonExe, ["-m", "paperforge", "ocr", "redo", ...keys])`(与重试相同命令)
- `nopdf` / `blocked` / `done+green` / `pending` / `running` — 不进表格
- `external_action` 组不渲染
### 所有正常 / 无操作的论文不进表格
`visible_in_maintenance === false` 的一律不渲染(含 `nopdf`, `blocked`, `done+green`, `pending` 等)。
## C4. 刷新图标
在设置标签页的 tab bar 区域,维护标签按钮旁加一个刷新图标。
## C5. 刷新图标
```css
.paperforge-maintenance-refresh {
width: 16px; height: 16px;
display: inline-block;
vertical-align: middle;
transition: transform 0.3s;
}
.paperforge-maintenance-refresh--spinning {
animation: spin 1s linear infinite;
}
.pf-maint-spinner { animation: spin 1s linear infinite; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
```
- `spin()` / `stopSpin()` 控制
- 无缓存首次加载 → 不出现(整个页面在加载,不需要额外指示)
- 缓存存在 + manifest 有变化 → 旋转 → 更新完成 → 停止
- 无缓存首次加载 → 不显示 spinner整体加载中
- 缓存 + manifest 变化 → 旋转 → 更新完成 → 停止
## C5. i18n 新增文案
## C6. i18n 新增
```typescript
"maintenance_group_retry": "需要重试",
"maintenance_group_rebuild": "可重建结果",
"maintenance_group_rebuild": "可重建结果",
"maintenance_group_legacy": "可升级旧结果(可选)",
"maintenance_retry_btn": "重试",
"maintenance_rebuild_btn": "重建",
"maintenance_upgrade_btn": "升级",
"maintenance_all_good": "✅ 全部正常",
"maintenance_refreshing": "正在更新...",
"maintenance_btn_retry": "重试",
"maintenance_btn_rebuild": "重建",
"maintenance_btn_upgrade": "升级",
"maintenance_refresh_spinning": "正在更新…",
```
## C6. 测试
`tests/ocr-maintenance-ui.test.ts` 追加:
## C7. 测试
| # | 场景 | 预期 |
|---|------|------|
| 1 | `visible_in_maintenance=false` 不进表格 | 过滤正确 |
| 2 | 3 组分组渲染 | retry / rebuild / legacy 分开 |
| 3 | 批量操作只执行 visible 的论文 | 不操作 hidden 的 |
| 4 | 缓存读写 | manifest 一致时不触发刷新 |
| 5 | hash 不匹配触发增量拉取 | 只拉变化的 key |
| 6 | 刷新图标状态切换 | 旋转 / 静止 |
| 2 | 3 组分开展示 | retry / rebuild / legacy 分离 |
| 3 | 批量操作只执行 `visible=true` 的论文 | 不操作 hidden |
| 4 | 缓存读 — manifest 匹配 → 无刷新 | 不触发 execFile |
| 5 | manifest 不匹配 → 增量拉取 | 只请求变化 key |
| 6 | `--keys` 调用格式为 `["--keys", ...keys]` | 不拼接 `--keys=` |
| 7 | 刷新图标状态切换 | 静止 ↔ 旋转 |
| 8 | 旧 `categorizeMaintenanceRow` 不再渲染 visible 路径 | 无 "OCR Failed" / "Rebuild Recommended" |
---
## 执行顺序
# PR D: Base view action labels可选 — 按需决定)
```
PR A ──→ PR C
│ ↑
↓ │
PR B ───────┘
```
**如果要求在 Base 也避免显示 `failed/degraded/health`,这轮做。**
- PR A 和 PR B 可并行(无代码冲突,修改不同函数)
- PR C 需要 Adisplay 字段存在)和 Bmanifest/keys CLI 存在)
- PR C 开始前,先合并 A 和 B 到 master
**文件:**
- base generator — 依赖具体实现,需要先确认哪个文件
**改什么:**
- Base 读 `display_label` 替代 `status` + `health` 的原始拼接
- `nopdf``"补充 PDF"`
- `blocked``"配置 OCR"`(或从 `meta.error` 读具体原因)
- version=v1 → `"可升级"`
- `done+green``"已完成"`
- `pending``"等待处理"`
- `running/queued/processing``"处理中"`
**时机:** PR D 应等 PR A 合并后开发,可在 PR C 之后或并行。