# Changelog — 编辑器重构 P0

**日期**: 2026-06-02  
**模块**: 文件编辑器（FileEditorWorkbench + 后端 read/write-file）

---

## 变更摘要

修复编辑器三个 P0 正确性问题：

1. **撤销历史串台**（M1）：改为每个标签页独立持有 Monaco `ITextModel`，切换时通过 `editor.setModel()` + `saveViewState`/`restoreViewState` 保留光标、折叠、选区，撤销历史互不影响。

2. **EOL 强制 LF**（M2）：`read-file` 响应新增 `eol` 字段（LF/CRLF）；编辑器内部用 LF-only 做对比，保存时按原始 EOL 还原。CRLF 文件不再被破坏。

3. **并发写覆盖**（M3）：`read-file` 新增 `mtime`、`etag`（sha256hex）；`write-file` 接受可选 `etag`，写前校验，不一致返回 HTTP 409；前端弹出冲突弹窗（覆盖/放弃）。

---

## 动机

- M1：多标签编辑时，A 文件的撤销会回到 B 文件内容（共享同一 Monaco model 的 `setValue` 导致）
- M2：CRLF 脚本/配置保存后被改成 LF，破坏 Windows 批处理等文件
- M3：并发场景（两个管理员、外部工具）下静默覆盖，数据丢失无提示

---

## 涉及文件

| 文件 | 变更 |
|------|------|
| `server/api/schemas.py` | `FileWrite` 加 `etag: str | None` 字段 |
| `server/api/sync_v2.py` | `read_file` 返回 mtime/etag/eol；`write_file` 加 etag 校验（409）和写后更新 etag |
| `frontend/src/utils/filePreload.ts` | 新增 `RemoteFileReadResult` 接口，缓存改存完整结果 |
| `frontend/src/composables/files/useFilesEditor.ts` | 新增 `OpenFilePayload` 接口，`editFile`/`previewFile` 适配新返回类型 |
| `frontend/src/components/FileEditorWorkbench.vue` | per-tab model + viewState；EOL 保留；409 冲突弹窗 |
| `tests/test_editor_write_conflict.py` | 11 个 pytest 用例，覆盖 etag 校验逻辑 |
| `docs/design/specs/2026-06-02-editor-refactor-design.md` | 设计文档 |
| `docs/design/plans/2026-06-02-editor-refactor.md` | 技术文档 |

---

## 是否需要迁移/重启

- **后端**：需 `supervisorctl restart nexus`（API 响应字段变更）
- **前端**：需重新构建并上传（bundle 变更）
- **数据库**：无 migration

---

## 验证方式

1. `python -m ruff check server/api/schemas.py server/api/sync_v2.py` → 0 errors
2. `python -m pytest tests/test_editor_write_conflict.py -v` → 11 passed
3. `npx vite build` → build succeeded, 0 TS errors
4. 浏览器：打开两个文件 A、B，编辑 B 后切回 A，Ctrl+Z 只撤销 A 的历史
5. 浏览器：上传 CRLF 文件，编辑后保存，下载验证 CRLF 保留
6. 浏览器：同一文件两标签保存，后者收到冲突弹窗
