135 lines
5.1 KiB
Markdown
135 lines
5.1 KiB
Markdown
|
|
# 2026-05-29 推送页面迭代 Round 2 — 技术文档
|
|||
|
|
|
|||
|
|
## 涉及文件清单
|
|||
|
|
|
|||
|
|
| 文件 | 改动类型 | 涉及功能 |
|
|||
|
|
|------|---------|---------|
|
|||
|
|
| `server/infrastructure/telegram/__init__.py` | 新增函数 | F3 Telegram |
|
|||
|
|
| `server/application/services/sync_engine_v2.py` | 修改 | F1 重试 + F3 Telegram |
|
|||
|
|
| `server/api/websocket.py` | 修改参数 | F1 重试 |
|
|||
|
|
| `server/api/sync_v2.py` | 新增端点 | F4 预览 |
|
|||
|
|
| `server/api/schemas.py` | 新增模型 | F4 预览 |
|
|||
|
|
| `web/app/push.html` | 修改 | F1+F2+F4+F5 前端 |
|
|||
|
|
| `docs/changelog/2026-05-29-push-round2.md` | 新增 | changelog |
|
|||
|
|
|
|||
|
|
## 实现步骤
|
|||
|
|
|
|||
|
|
### Step 1: F5 同步模式详细说明(纯前端)
|
|||
|
|
|
|||
|
|
**文件**: `web/app/push.html`
|
|||
|
|
|
|||
|
|
1. 同步模式 `</div>` 后增加 `<div id="syncModeDesc">` 说明文字
|
|||
|
|
2. `onSyncModeChange()` 函数末尾增加说明文字更新逻辑
|
|||
|
|
3. 默认显示增量同步说明
|
|||
|
|
|
|||
|
|
说明内容:
|
|||
|
|
- 增量同步:仅传输源目录中新增或修改的文件,目标已有且未变的文件跳过。安全快速,适合日常更新。
|
|||
|
|
- 全量同步:使目标目录与源目录完全一致,会删除目标中不存在于源的文件。⚠️ 可能造成数据丢失,建议先预览。
|
|||
|
|
- 校验和:通过文件内容校验和(而非修改时间+大小)判断是否需要传输。更精确但更慢,适合时钟不同步的环境。
|
|||
|
|
|
|||
|
|
### Step 2: F2 服务器在线状态标识(纯前端)
|
|||
|
|
|
|||
|
|
**文件**: `web/app/push.html`
|
|||
|
|
|
|||
|
|
1. `filterServers()` 中渲染 `<option>` 时,根据 `s.is_online` 加🟢/🔴前缀
|
|||
|
|
2. 离线服务器选项加 `style="color: #666"` 提示
|
|||
|
|
3. 数据来源:`/api/servers/` 已返回 `is_online`(从 Redis heartbeat 实时读取),无需改后端
|
|||
|
|
|
|||
|
|
### Step 3: F1 失败自动重试(后端+前端)
|
|||
|
|
|
|||
|
|
**后端**:
|
|||
|
|
|
|||
|
|
1. `server/application/services/sync_engine_v2.py` — `_sync_one()`:
|
|||
|
|
- 当 `sync_log.status == "failed"` 时,创建 `PushRetryJob`:
|
|||
|
|
```python
|
|||
|
|
retry_job = PushRetryJob(
|
|||
|
|
server_id=server.id, server_name=server.name,
|
|||
|
|
source_path=source_path, target_path=dest,
|
|||
|
|
status="pending", retry_count=0, max_retries=3,
|
|||
|
|
next_retry_at=datetime.now(timezone.utc),
|
|||
|
|
last_error=sync_log.error_message[:500] if sync_log.error_message else None,
|
|||
|
|
)
|
|||
|
|
retry_job = await self.retry_repo.create(retry_job)
|
|||
|
|
```
|
|||
|
|
- 将 `retry_job.id` 存入变量,传给 WS 广播
|
|||
|
|
|
|||
|
|
2. `server/api/websocket.py` — `broadcast_sync_progress()` 增加参数:
|
|||
|
|
- `retry_job_id: int = None`
|
|||
|
|
- WS 消息中增加 `retry_job_id` 字段
|
|||
|
|
|
|||
|
|
**前端**:
|
|||
|
|
|
|||
|
|
1. `updateProgressFromWS()` — 失败行渲染"🔄 重试"按钮
|
|||
|
|
2. `updateProgressFromResult()` — 同上
|
|||
|
|
3. 新增 `retryServer(retryJobId)` — 调用 `POST /api/retries/{id}/retry`
|
|||
|
|
4. 存储映射 `_retryJobMap = {}` — `server_id → retry_job_id`
|
|||
|
|
|
|||
|
|
### Step 4: F3 推送完成 Telegram 通知(后端)
|
|||
|
|
|
|||
|
|
1. `server/infrastructure/telegram/__init__.py` — 新增函数:
|
|||
|
|
```python
|
|||
|
|
async def send_telegram_sync_complete(
|
|||
|
|
completed: int, failed: int, total: int,
|
|||
|
|
source_path: str, operator: str,
|
|||
|
|
failed_servers: list[str] = None,
|
|||
|
|
duration_seconds: int = None,
|
|||
|
|
) -> bool:
|
|||
|
|
```
|
|||
|
|
- 消息格式:HTML,含 emoji 标识状态
|
|||
|
|
- 全部成功: 🟢 推送完成
|
|||
|
|
- 部分失败: 🟡 推送完成(部分失败)+ 失败服务器名列表
|
|||
|
|
- 全部失败: 🔴 推送失败
|
|||
|
|
- `source_path` 经 `sanitize_external_message()` 脱敏
|
|||
|
|
|
|||
|
|
2. `server/application/services/sync_engine_v2.py` — `sync_files()`:
|
|||
|
|
- `asyncio.gather()` 完成后,计算总耗时
|
|||
|
|
- 收集失败服务器名列表
|
|||
|
|
- 调用 `send_telegram_sync_complete()`
|
|||
|
|
- try/except 包裹,Telegram 失败不影响推送结果
|
|||
|
|
|
|||
|
|
### Step 5: F4 文件内容预览(后端+前端)
|
|||
|
|
|
|||
|
|
**后端**:
|
|||
|
|
|
|||
|
|
1. `server/api/schemas.py` — 新增:
|
|||
|
|
```python
|
|||
|
|
class LocalFilePreview(BaseModel):
|
|||
|
|
path: str = Field(..., min_length=1)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. `server/api/sync_v2.py` — 新增 `POST /api/sync/local-file-preview`:
|
|||
|
|
- 安全:`os.path.realpath()` + `startswith("/tmp/nexus_upload_")`
|
|||
|
|
- 限制:仅文件(非目录)+ 最多读 4KB
|
|||
|
|
- 返回:`{name, size, content_base64, truncated, encoding_hint}`
|
|||
|
|
- `encoding_hint`: 尝试 UTF-8 解码,成功 `"text"` 否则 `"binary"`
|
|||
|
|
- 审计日志
|
|||
|
|
|
|||
|
|
**前端**:
|
|||
|
|
|
|||
|
|
1. `renderFileManager()` — 文件名加 `cursor-pointer` + `onclick="previewLocalFile(path, name)"`
|
|||
|
|
2. 新增 `previewLocalFile(path, name)`:
|
|||
|
|
- 调用 `/api/sync/local-file-preview`
|
|||
|
|
- 弹出模态框显示文件名、大小、内容
|
|||
|
|
- 二进制文件提示"无法预览"
|
|||
|
|
3. 新增模态框 HTML:`#previewModal`
|
|||
|
|
|
|||
|
|
## 依赖与配置变更
|
|||
|
|
|
|||
|
|
无新依赖。无数据库 schema 迁移。
|
|||
|
|
|
|||
|
|
## 测试要点
|
|||
|
|
|
|||
|
|
1. F5: 切换同步模式 → 说明文字正确切换
|
|||
|
|
2. F2: 在线服务器🟢、离线服务器🔴显示正确
|
|||
|
|
3. F1: 推送失败 → 行内重试按钮 → 点击重试成功
|
|||
|
|
4. F3: 推送完成 → Telegram 收到通知
|
|||
|
|
5. F4: 点击文件 → 预览模态框显示内容
|
|||
|
|
6. F4: 点击二进制文件 → 提示无法预览
|
|||
|
|
7. F4 安全: 尝试预览 `/etc/passwd` → 403 拒绝
|
|||
|
|
|
|||
|
|
## 回滚方式
|
|||
|
|
|
|||
|
|
所有改动均为增量(新增函数/端点/前端元素),回滚只需:
|
|||
|
|
1. `git revert` 提交
|
|||
|
|
2. `supervisorctl restart nexus`
|