# 简化推送模型 实现计划 > ⚠️ **已归档** — 2026-05-22 > 本文档引用的文件结构已过时:`server/database.py` 已拆分为 `server/domain/models/`;`server/api/tasks.py` 已删除;PHP 前端 (`servers.php`/`tasks.php`/`push.php`) 已迁移到 Tailwind CSS v4 + Alpine.js;sync_engine 已改为 asyncssh。功能已实现,保留本文档供历史参考。 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 将复杂 SyncTask 模型简化为 Server.target_path + 批量 push API + 分类选择推送界面 **架构:** Server 模型新增 target_path/category 字段;删除 SyncTask 表;新增 POST /api/servers/push 批量推送端点;sync_engine 简化为纯单向 push;前端 servers.php 加入分类推送对话框 **技术栈:** Python FastAPI + SQLAlchemy + MySQL + PHP 7.4 + rsync --- ### 任务 1:数据库模型变更 **文件:** - 修改:`server/database.py`(全文件) - 修改:`server/config.py:27`(删 SYNC_INTERVAL_SECONDS) - 修改:`.env.example:26-30`(删 trigger/sync 配置) - [ ] **步骤 1:Server 模型加 target_path + category,删 SyncTask,调整 SyncLog** 修改 `server/database.py`,在 Server 模型的 `description` 字段后添加: ```python target_path = Column(String(500), nullable=True, comment="推送目标路径") category = Column(String(100), nullable=True, comment="服务器分类(如 Web服务器/API服务器)") ``` 删除整个 SyncTask 类(第 112-141 行)。 修改 SyncLog 类:删除 `task_id` 列,添加 `source_path`、`target_path` 列: ```python class SyncLog(Base): """Push operation log""" __tablename__ = "sync_logs" id = Column(Integer, primary_key=True, autoincrement=True) server_id = Column(Integer, ForeignKey("servers.id"), nullable=False) source_path = Column(String(500), nullable=False, comment="源目录") target_path = Column(String(500), nullable=False, comment="目标目录") trigger_type = Column(String(20), comment="触发类型: manual") status = Column(String(20), comment="pending/running/success/failed") files_total = Column(Integer, default=0) files_transferred = Column(Integer, default=0) files_skipped = Column(Integer, default=0) bytes_transferred = Column(Integer, default=0) duration_seconds = Column(Integer, default=0) diff_summary = Column(Text, nullable=True, comment="rsync输出摘要") error_message = Column(Text, nullable=True) started_at = Column(DateTime, default=datetime.datetime.utcnow) finished_at = Column(DateTime, nullable=True) server = relationship("Server", back_populates="sync_logs") ``` 更新 Server 模型的 relationship(删 sync_tasks): ```python sync_logs = relationship("SyncLog", back_populates="server", cascade="all, delete-orphan") ``` - [ ] **步骤 2:删 config.py 中 SYNC_INTERVAL_SECONDS** ```python # 删除以下行: # SYNC_INTERVAL_SECONDS: int = 300 # 5 minutes ``` - [ ] **步骤 3:更新 .env.example** 删 `NEXUS_SYNC_INTERVAL_SECONDS` 和 `NEXUS_TRIGGER_FILENAME` 行。 - [ ] **步骤 4:验证数据库初始化** ```bash python -m py_compile server/database.py && python -m py_compile server/config.py && echo "OK" ``` - [ ] **步骤 5:Commit** ```bash git add server/database.py server/config.py .env.example git commit -m "refactor(db): 简化数据模型 — Server 加 target_path/category,删 SyncTask,调整 SyncLog" ``` --- ### 任务 2:Python API 变更 — 删除 tasks 路由 + 新增 push 端点 + 清理 agent **文件:** - 删除:`server/api/tasks.py` - 修改:`server/api/servers.py`(添加 push 端点 + ServerCreate/Update/Response 更新) - 修改:`server/api/agent.py`(删除 /trigger 端点) - 修改:`server/main.py`(删除 tasks 路由注册,更新 imports) - [ ] **步骤 1:删除 tasks.py + 清理 main.py 中 tasks 路由** ```bash rm server/api/tasks.py ``` 修改 `server/main.py`,删除 `from server.api import servers, tasks, agent, auth` 中的 `tasks` → 改为 `from server.api import servers, agent, auth`。删除 `tasks.router` 的 `app.include_router` 调用(含 dependencies 行)。 - [ ] **步骤 2:更新 servers.py — Pydantic 模型加 target_path/category** 修改 `server/api/servers.py` 的 `ServerCreate`、`ServerUpdate`、`ServerResponse`: ```python class ServerCreate(BaseModel): name: str domain: str port: int = 22 username: str = "root" auth_method: str = "key" password: Optional[str] = None ssh_key_path: Optional[str] = None agent_port: int = 8601 agent_api_key: Optional[str] = None description: Optional[str] = None target_path: Optional[str] = None # 新增 category: Optional[str] = None # 新增 class ServerUpdate(BaseModel): # ... 所有已有字段 ... target_path: Optional[str] = None # 新增 category: Optional[str] = None # 新增 class ServerResponse(BaseModel): id: int name: str domain: str port: int username: str auth_method: str agent_port: int description: Optional[str] target_path: Optional[str] # 新增 category: Optional[str] # 新增 is_online: bool last_heartbeat: Optional[datetime] system_info: Optional[str] created_at: datetime updated_at: datetime class Config: from_attributes = True ``` - [ ] **步骤 3:servers.py 添加 POST /push 批量推送端点** 在 `server/api/servers.py` 末尾添加: ```python from pydantic import BaseModel as PydanticBase class PushRequest(PydanticBase): source_path: str server_ids: List[int] @router.post("/push") async def push_to_servers(req: PushRequest, db: Session = Depends(get_db)): """批量推送到多台服务器""" if not req.server_ids: raise HTTPException(status_code=400, detail="No servers selected") servers_list = db.query(Server).filter(Server.id.in_(req.server_ids)).all() if not servers_list: raise HTTPException(status_code=404, detail="No servers found") from server.services.sync_engine import push_to_server import asyncio tasks = [] for server in servers_list: target = server.target_path or "/" tasks.append(push_to_server(server, req.source_path, target)) results = await asyncio.gather(*tasks, return_exceptions=True) successes = sum(1 for r in results if isinstance(r, dict) and r.get("status") == "success") return { "total": len(servers_list), "success": successes, "failed": len(servers_list) - successes, "results": [ {"server_id": s.id, "server_name": s.name, **r} if isinstance(r, dict) else {"server_id": s.id, "server_name": s.name, "status": "error", "message": str(r)} for s, r in zip(servers_list, results) ], } ``` - [ ] **步骤 4:删除 agent.py 中 /trigger 端点** 修改 `server/api/agent.py`,删除 `TriggerPayload` 类和 `@router.post("/trigger")` 整个函数。 - [ ] **步骤 5:验证编译** ```bash python -m py_compile server/api/servers.py server/api/agent.py server/main.py && echo "OK" ``` - [ ] **步骤 6:Commit** ```bash git add server/api/ server/main.py git rm server/api/tasks.py 2>/dev/null git commit -m "refactor(api): 删除 tasks 路由,新增 POST /api/servers/push,清理 agent trigger" ``` --- ### 任务 3:服务层简化 — sync_engine 纯 push + 清理 ssh_direct + 删除 trigger_poller **文件:** - 修改:`server/services/sync_engine.py` - 修改:`server/services/ssh_direct.py` - 删除:`server/services/trigger_poller.py` - 修改:`server/main.py`(启动逻辑中删除 trigger_poller 引用) - [ ] **步骤 1:重写 sync_engine.py 为纯 push** ```python """Sync engine — push source directory to target server via rsync""" import asyncio from datetime import datetime from typing import Dict, Any from server.database import SessionLocal, SyncLog, Server from server.config import settings async def push_to_server(server: Server, source_path: str, target_path: str) -> Dict[str, Any]: """Push source_path to server:target_path via rsync over SSH""" db = SessionLocal() try: log = SyncLog( server_id=server.id, source_path=source_path, target_path=target_path, trigger_type="manual", status="running", started_at=datetime.utcnow(), ) db.add(log) db.commit() db.refresh(log) ssh_opts = f"-p {server.port} -o ConnectTimeout={settings.HEALTH_CHECK_TIMEOUT}" if not settings.SSH_STRICT_HOST_CHECKING: ssh_opts += " -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" if server.auth_method == "key" and server.ssh_key_path: ssh_opts += f" -i {server.ssh_key_path}" cmd = [ "rsync", "-avz", "--stats", "-e", f"ssh {ssh_opts}", f"{source_path.rstrip('/')}/", f"{server.username}@{server.domain}:{target_path.rstrip('/')}/", ] start = datetime.utcnow() proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await proc.communicate() end = datetime.utcnow() output = stdout.decode() err = stderr.decode() duration = int((end - start).total_seconds()) # 解析统计 import re files_total = files_transferred = bytes_transferred = 0 for line in output.split("\n"): if m := re.search(r'Number of files(?:\s+transferred)?:\s*([\d,]+)', line): if "transferred" in line: files_transferred = int(m.group(1).replace(",", "")) else: files_total = int(m.group(1).replace(",", "")) elif m := re.search(r'Total (?:transferred )?file size:\s*([\d,.]+)\s*bytes', line): bytes_transferred = int(m.group(1).replace(",", "").replace(".", "")) success = proc.returncode == 0 status = "success" if success else "failed" log.status = status log.files_total = files_total log.files_transferred = files_transferred log.files_skipped = files_total - files_transferred log.bytes_transferred = bytes_transferred log.duration_seconds = duration log.diff_summary = output[-2000:] if len(output) > 2000 else output log.error_message = err if err else None log.finished_at = end db.commit() summary = f"Transferred {files_transferred} files ({bytes_transferred} bytes) in {duration}s" return { "status": status, "message": summary if success else f"Failed: {err[:200]}", "log_id": log.id, "duration": duration, "files_transferred": files_transferred, "bytes_transferred": bytes_transferred, } except Exception as e: if 'log' in locals(): try: log.status = "failed" log.error_message = str(e) log.finished_at = datetime.utcnow() db.commit() except Exception: pass return {"status": "failed", "message": str(e)} finally: db.close() ``` 这完全替换原有的 `compute_diff`、`execute_sync`、`trigger_sync_task` 函数。 - [ ] **步骤 2:清理 ssh_direct.py — 删除触发文件轮询** 删除 `ssh_trigger_poll_loop` 函数、`check_trigger_file`、`remove_trigger_file`、`check_triggers_on_server`、`list_remote_dir` 函数,以及 `_trigger_locks`、`_trigger_lock_key` 变量和 import `Dict`。 保留:`ssh_health_check_loop`、`check_server_health_ssh`、`SSHConfig`、`get_ssh_config`、`create_ssh_client`、`ssh_connection`、`exec_command`。 - [ ] **步骤 3:删除 trigger_poller.py** ```bash rm server/services/trigger_poller.py ``` - [ ] **步骤 4:更新 main.py 启动逻辑** 修改 `server/main.py` 第 79-103 行,三种模式都删除 `trigger_poll_loop` 相关: ```python if SYNC_MODE == "ssh_direct": from server.services.ssh_direct import ssh_health_check_loop background_tasks.append(asyncio.create_task(ssh_health_check_loop())) elif SYNC_MODE == "agent": from server.services.health_checker import health_check_loop background_tasks.append(asyncio.create_task(health_check_loop())) else: from server.services.ssh_direct import ssh_health_check_loop from server.services.health_checker import health_check_loop background_tasks.append(asyncio.create_task(ssh_health_check_loop())) background_tasks.append(asyncio.create_task(health_check_loop())) ``` - [ ] **步骤 5:验证编译** ```bash python -m py_compile server/services/sync_engine.py server/services/ssh_direct.py server/main.py && echo "OK" ``` - [ ] **步骤 6:Commit** ```bash git add server/services/sync_engine.py server/services/ssh_direct.py server/main.py git rm server/services/trigger_poller.py git commit -m "refactor(services): sync_engine 简化为纯 push,删除 trigger_poller,清理 ssh_direct" ``` --- ### 任务 4:PHP API 客户端更新 **文件:** - 修改:`web/api_client.php` - 修改:`web/api_proxy.php` - [ ] **步骤 1:api_client.php 删 task 方法,加 pushServers** 删除:`getTasks`、`getTask`、`createTask`、`updateTask`、`deleteTask`、`triggerSync`、`getTaskDiff`、`getTaskLogs` 方法。 删除 `getDashboardStats` 中的 tasks 部分,简化为只统计 servers。 新增方法: ```php public function pushServers($sourcePath, $serverIds) { return $this->request('POST', '/api/servers/push', [ 'source_path' => $sourcePath, 'server_ids' => $serverIds, ]); } ``` - [ ] **步骤 2:api_proxy.php 删 task action,加 push** 删除 case:`get_tasks`、`get_task`、`trigger_sync`、`sync_all`、`get_diff`、`get_task_logs`、`get_logs`(logs 数据通过 sync_logs API 读取)。 新增 case: ```php case 'push_servers': $input = json_decode(file_get_contents('php://input'), true); $result = api()->pushServers($input['source_path'] ?? '', $input['server_ids'] ?? []); break; ``` - [ ] **步骤 3:Commit** ```bash git add web/api_client.php web/api_proxy.php git commit -m "refactor(php): API 客户端删除 task 方法,新增 pushServers" ``` --- ### 任务 5:前端 — servers.php 加入分类 + 目标路径 + 推送对话框 **文件:** - 修改:`web/servers.php` - [ ] **步骤 1:列表表头加分类和目标路径列** 在 servers.php 表格表头 `