# 编辑器重构实施计划（P0）

**日期**: 2026-06-02  
**设计文档**: `docs/design/specs/2026-06-02-editor-refactor-design.md`

---

## 涉及文件清单

| 文件 | 变更类型 | 说明 |
|------|---------|------|
| `server/api/schemas.py` | 修改 | `FileRead` 响应无变化；`FileWrite` 加 `etag: str | None` |
| `server/api/sync_v2.py` | 修改 | `read_file` 加 mtime/etag/eol；`write_file` 加 etag 校验 |
| `frontend/src/utils/filePreload.ts` | 修改 | `readRemoteFileContent` 返回 `RemoteFileReadResult` |
| `frontend/src/composables/files/useFilesEditor.ts` | 修改 | `editFile`/`previewFile` 接收新返回类型；`saveActive` 传 etag |
| `frontend/src/components/FileEditorWorkbench.vue` | 修改 | per-tab model；EOL 保留；409 冲突弹窗 |

---

## 实施步骤

### Step 1：后端 schemas.py — FileWrite 加 etag

```python
class FileWrite(BaseModel):
    ...
    etag: str | None = Field(None, max_length=64)  # sha256hex
```

### Step 2：后端 sync_v2.py — read_file 加 mtime/etag/eol

- `stat -c '%s %Y'` → size + mtime（已有，扩展解析 mtime）
- 新增 `sha256sum path_q`（timeout=5）
- 返回 `{"content":..., "size":..., "path":..., "mtime": int, "etag": str, "eol": "LF"|"CRLF"}`

### Step 3：后端 sync_v2.py — write_file 加 etag 校验

- 如 `payload.etag` 非空，先 `sha256sum` 校验
- 不一致 → 409 `{"detail": "文件已被外部修改", "code": "ETAG_MISMATCH"}`
- sha256sum 命令失败（权限等）→ 跳过校验（降级，不报错）

### Step 4：前端 filePreload.ts — 返回类型扩展

- 新增 `interface RemoteFileReadResult { content, mtime, etag, eol }`
- `readRemoteFileContent` 签名改为返回 `Promise<RemoteFileReadResult>`
- 缓存存 `RemoteFileReadResult`（非纯 string）
- 更新 `invalidateCachedFile`、`isPreloadEligible` 调用方

### Step 5：前端 useFilesEditor.ts — 适配新返回类型

- `editFile`：接收 `RemoteFileReadResult`，传给 `editorWorkbench.openFile`
- `previewFile`：只取 `.content`，其余不变
- 新增 `onConflict` 回调签名

### Step 6：前端 FileEditorWorkbench.vue — per-tab model + EOL + 409

- `EditorTabState` 加 `eol / etag / mtime / model / viewState`
- `openFile`：`monaco.editor.createModel` with EOL 匹配，不 `normalizeEol`
- `applyTabToEditor`：改为 `editor.setModel(tab.model)` + `restoreViewState`
- `persistEditorToActiveTab`：加 `tab.viewState = editor.saveViewState()`
- `removeTab`：加 `tab.model?.dispose()`
- `destroyEditor`：不 dispose tab models（改由 removeTab/closePanel 负责）
- `saveActive`：请求体加 `etag: tab.etag`，捕获 409 → `showConflictDialog`
- 新增 `ConflictDialog`（覆盖/放弃两个按钮）

### Step 7：测试

- `tests/test_editor_write_conflict.py`（新增）
  - 写入后 etag 变更 → 409
  - etag=null → 无校验直接写入
  - sha256sum 失败 → 降级通过

---

## 依赖与配置变更

- 无新依赖
- 后端：仅 schema + API handler 变更，无 DB migration

## 回滚方式

- 前端：`git revert` 对应 commit，重新 build+deploy
- 后端：`git revert` + `supervisorctl restart nexus`

## 测试要点

1. 双标签撤销互不影响
2. CRLF 文件保存后不变为 LF
3. etag 不一致时收到 409
4. etag=null（首次打开未保存过）写入正常
