Files
Nexus/docs/design/plans/2026-05-16-simplified-push.md
2026-07-08 22:31:31 +08:00

28 KiB
Raw Permalink Blame History

简化推送模型 实现计划

⚠️ 已归档 — 2026-05-22 本文档引用的文件结构已过时:server/database.py 已拆分为 server/domain/models/server/api/tasks.py 已删除;PHP 前端 (servers.php/tasks.php/push.php) 已迁移到 Tailwind CSS v4 + Alpine.jssync_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 配置)

  • 步骤 1Server 模型加 target_path + category,删 SyncTask,调整 SyncLog

修改 server/database.py,在 Server 模型的 description 字段后添加:

    target_path = Column(String(500), nullable=True, comment="推送目标路径")
    category = Column(String(100), nullable=True, comment="服务器分类(如 Web服务器/API服务器)")

删除整个 SyncTask 类(第 112-141 行)。

修改 SyncLog 类:删除 task_id 列,添加 source_pathtarget_path 列:

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):

    sync_logs = relationship("SyncLog", back_populates="server", cascade="all, delete-orphan")
  • 步骤 2:删 config.py 中 SYNC_INTERVAL_SECONDS
    # 删除以下行:
    # SYNC_INTERVAL_SECONDS: int = 300  # 5 minutes
  • 步骤 3:更新 .env.example

NEXUS_SYNC_INTERVAL_SECONDSNEXUS_TRIGGER_FILENAME 行。

  • 步骤 4:验证数据库初始化
python -m py_compile server/database.py && python -m py_compile server/config.py && echo "OK"
  • 步骤 5Commit
git add server/database.py server/config.py .env.example
git commit -m "refactor(db): 简化数据模型 — Server 加 target_path/category,删 SyncTask,调整 SyncLog"

任务 2Python 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 路由

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.routerapp.include_router 调用(含 dependencies 行)。

  • 步骤 2:更新 servers.py — Pydantic 模型加 target_path/category

修改 server/api/servers.pyServerCreateServerUpdateServerResponse

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
  • 步骤 3servers.py 添加 POST /push 批量推送端点

server/api/servers.py 末尾添加:

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:验证编译
python -m py_compile server/api/servers.py server/api/agent.py server/main.py && echo "OK"
  • 步骤 6Commit
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

"""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_diffexecute_synctrigger_sync_task 函数。

  • 步骤 2:清理 ssh_direct.py — 删除触发文件轮询

删除 ssh_trigger_poll_loop 函数、check_trigger_fileremove_trigger_filecheck_triggers_on_serverlist_remote_dir 函数,以及 _trigger_locks_trigger_lock_key 变量和 import Dict

保留:ssh_health_check_loopcheck_server_health_sshSSHConfigget_ssh_configcreate_ssh_clientssh_connectionexec_command

  • 步骤 3:删除 trigger_poller.py
rm server/services/trigger_poller.py
  • 步骤 4:更新 main.py 启动逻辑

修改 server/main.py 第 79-103 行,三种模式都删除 trigger_poll_loop 相关:

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:验证编译
python -m py_compile server/services/sync_engine.py server/services/ssh_direct.py server/main.py && echo "OK"
  • 步骤 6Commit
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"

任务 4PHP API 客户端更新

文件:

  • 修改:web/api_client.php

  • 修改:web/api_proxy.php

  • 步骤 1api_client.php 删 task 方法,加 pushServers

删除:getTasksgetTaskcreateTaskupdateTaskdeleteTasktriggerSyncgetTaskDiffgetTaskLogs 方法。

删除 getDashboardStats 中的 tasks 部分,简化为只统计 servers。

新增方法:

public function pushServers($sourcePath, $serverIds) {
    return $this->request('POST', '/api/servers/push', [
        'source_path' => $sourcePath,
        'server_ids' => $serverIds,
    ]);
}
  • 步骤 2api_proxy.php 删 task action,加 push

删除 caseget_tasksget_tasktrigger_syncsync_allget_diffget_task_logsget_logslogs 数据通过 sync_logs API 读取)。

新增 case

case 'push_servers':
    $input = json_decode(file_get_contents('php://input'), true);
    $result = api()->pushServers($input['source_path'] ?? '', $input['server_ids'] ?? []);
    break;
  • 步骤 3Commit
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 表格表头 <th>Agent端口</th> 后添加:

<th>分类</th>
<th>目标路径</th>

在循环中对应位置添加:

<td><?= htmlspecialchars($s['category'] ?? '-') ?></td>
<td class="path-cell"><?= htmlspecialchars($s['target_path'] ?? '-') ?></td>
  • 步骤 2:编辑表单加分类下拉和目标路径输入框

在 servers.php 的 <form> 中,description 字段前添加:

<div class="form-row">
    <div class="form-group">
        <label>服务器分类</label>
        <input type="text" name="category" class="form-control"
               value="<?= htmlspecialchars($server['category'] ?? '') ?>"
               placeholder="如: Web服务器, API服务器"
               list="categoryList">
        <datalist id="categoryList">
            <option value="Web服务器">
            <option value="API服务器">
            <option value="数据库服务器">
            <option value="缓存服务器">
        </datalist>
    </div>
    <div class="form-group">
        <label>推送目标路径</label>
        <input type="text" name="target_path" class="form-control"
               value="<?= htmlspecialchars($server['target_path'] ?? '') ?>"
               placeholder="/var/www/html">
    </div>
</div>
  • 步骤 3:添加 "推送" 按钮 + 推送对话框 CSS/HTML/JS

在 servers.php 的顶部操作区添加:

<button class="btn btn-success" onclick="openPushDialog()">
    <i class="fas fa-paper-plane"></i> 推送文件
</button>

添加推送对话框样式(参考 tasks.php 的 .fm-overlay 风格,更紧凑):

.push-overlay { display:none; position:fixed; top:0;left:0;right:0;bottom:0; background:rgba(0,0,0,0.4); z-index:9999; align-items:center;justify-content:center; }
.push-overlay.active { display:flex; }
.push-dialog { background:#fff; border-radius:8px; width:90%; max-width:700px; max-height:85vh; overflow-y:auto; box-shadow:0 20px 50px rgba(0,0,0,0.3); }
.push-dialog h3 { padding:16px 20px; margin:0; border-bottom:1px solid #e5e7eb; font-size:16px; }
.push-body { padding:20px; }
.push-body .src-row { display:flex; gap:8px; align-items:center; margin-bottom:16px; }
.push-body .src-row input { flex:1; }
.push-category { margin-bottom:12px; }
.push-cat-title { font-weight:600; font-size:14px; padding:8px 0; cursor:pointer; display:flex; align-items:center; gap:6px; }
.push-cat-title .count { font-weight:400; color:#9ca3af; font-size:12px; }
.push-cat-title .cat-check { margin-left:auto; font-weight:400; font-size:12px; color:#3b82f6; cursor:pointer; }
.push-server { display:flex; align-items:center; gap:8px; padding:6px 12px; margin-left:20px; font-size:13px; }
.push-server label { flex:1; cursor:pointer; }
.push-server .path { color:#6b7280; font-size:12px; }
.push-footer { padding:12px 20px; border-top:1px solid #e5e7eb; display:flex; align-items:center; gap:12px; }
.push-footer .btn-push { background:#22c55e; color:#fff; border:none; padding:10px 24px; border-radius:6px; font-weight:600; cursor:pointer; }
.push-footer .btn-push:hover { background:#16a34a; }
.push-footer .btn-push:disabled { background:#9ca3af; cursor:not-allowed; }
.push-footer .summary { flex:1; font-size:13px; color:#6b7280; }

推送对话框 HTML

<div class="push-overlay" id="pushOverlay" onclick="if(event.target===this)closePushDialog()">
    <div class="push-dialog">
        <h3>推送文件</h3>
        <div class="push-body">
            <div class="src-row">
                <input type="text" id="pushSourcePath" class="form-control"
                       placeholder="/www/wwwroot/你的项目" value="/www/wwwroot/">
                <button type="button" class="btn-browse" onclick="FM.open('source','pushSourcePath')"><i class="fas fa-folder-open"></i></button>
            </div>
            <div class="push-category" id="allCheckRow">
                <div class="push-cat-title">
                    <input type="checkbox" id="pushSelectAll" onchange="toggleAllServers()">
                    <label for="pushSelectAll">全选 (<span id="pushTotalCount">0</span> 台)</label>
                </div>
            </div>
            <div id="pushCategoryList"></div>
        </div>
        <div class="push-footer">
            <span class="summary" id="pushSummary">已选 0 台</span>
            <button class="btn btn-secondary" onclick="closePushDialog()">取消</button>
            <button class="btn-push" id="btnDoPush" onclick="doPush()">推送到 <span id="pushBtnCount">0</span> 台服务器</button>
        </div>
    </div>
</div>

推送 JS

let pushServersData = [];

function loadPushServers() {
    const API_BASE = <?= json_encode(defined('API_BASE_URL') ? API_BASE_URL : 'http://localhost:8600') ?>;
    const API_KEY = <?= json_encode(defined('API_KEY') ? API_KEY : '') ?>;
    fetch(API_BASE + '/api/servers/', { headers: API_KEY ? {'X-API-Key': API_KEY} : {} })
        .then(r => r.json()).then(data => {
            pushServersData = data.data || data;
            renderPushDialog();
        });
}

function renderPushDialog() {
    const grouped = {};
    pushServersData.forEach(s => {
        const cat = s.category || '未分类';
        if (!grouped[cat]) grouped[cat] = [];
        grouped[cat].push(s);
    });
    document.getElementById('pushTotalCount').textContent = pushServersData.length;

    let html = '';
    for (const [cat, servers] of Object.entries(grouped)) {
        html += '<div class="push-category"><div class="push-cat-title">' +
            '<input type="checkbox" class="cat-check-all" onchange="toggleCategory(this)" data-cat="' + esc(cat) + '">' +
            esc(cat) + ' <span class="count">(' + servers.length + ' 台)</span>' +
            '<span class="cat-check" onclick="checkCategory(\'' + esc(cat) + '\')">全选此分类</span>' +
            '</div>';
        servers.forEach(s => {
            html += '<div class="push-server">' +
                '<input type="checkbox" class="server-check" data-id="' + s.id + '" data-cat="' + esc(cat) + '" onchange="updatePushCount()">' +
                '<label>' + esc(s.name) + ' (' + esc(s.domain) + ')</label>' +
                '<span class="path">→ ' + esc(s.target_path || '/') + '</span>' +
                '</div>';
        });
        html += '</div>';
    }
    document.getElementById('pushCategoryList').innerHTML = html;
}

function openPushDialog() {
    document.getElementById('pushOverlay').classList.add('active');
    loadPushServers();
}
function closePushDialog() { document.getElementById('pushOverlay').classList.remove('active'); }

function toggleAllServers() {
    const all = document.getElementById('pushSelectAll').checked;
    document.querySelectorAll('.server-check').forEach(cb => cb.checked = all);
    document.querySelectorAll('.cat-check-all').forEach(cb => cb.checked = all);
    updatePushCount();
}

function toggleCategory(el) {
    const cat = el.dataset.cat;
    const checked = el.checked;
    document.querySelectorAll('.server-check[data-cat="' + esc(cat) + '"]').forEach(cb => cb.checked = checked);
    updatePushCount();
}

function checkCategory(cat) {
    document.querySelectorAll('.server-check[data-cat="' + esc(cat) + '"]').forEach(cb => cb.checked = true);
    document.querySelector('.cat-check-all[data-cat="' + esc(cat) + '"]').checked = true;
    updatePushCount();
}

function updatePushCount() {
    const checked = document.querySelectorAll('.server-check:checked').length;
    document.getElementById('pushSummary').textContent = '已选 ' + checked + ' 台';
    document.getElementById('pushBtnCount').textContent = checked;
    document.getElementById('pushSelectAll').checked = checked === pushServersData.length;
    document.getElementById('btnDoPush').disabled = checked === 0;
}

function doPush() {
    const ids = Array.from(document.querySelectorAll('.server-check:checked')).map(cb => parseInt(cb.dataset.id));
    if (!ids.length) return;
    const sp = document.getElementById('pushSourcePath').value.trim();
    if (!sp) { alert('请输入源目录'); return; }

    document.getElementById('btnDoPush').disabled = true;
    document.getElementById('btnDoPush').textContent = '推送中...';

    fetch('api_proxy.php?action=push_servers', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ source_path: sp, server_ids: ids })
    }).then(r => r.json()).then(data => {
        const d = data.data || data;
        alert('推送完成: 成功 ' + (d.success || 0) + ' / 失败 ' + (d.failed || 0));
        closePushDialog();
        location.reload();
    }).catch(e => {
        alert('推送失败: ' + e.message);
        document.getElementById('btnDoPush').disabled = false;
        document.getElementById('btnDoPush').innerHTML = '推送到 <span id="pushBtnCount">' + ids.length + '</span> 台服务器';
    });
}

function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
  • 步骤 4sidebar 导航删除 "同步任务" 链接,添加 "推送日志"
<li><a href="logs.php"><i class="fas fa-history"></i> <span>推送日志</span></a></li>

删掉 <li><a href="tasks.php">... 那一行。

  • 步骤 5Commit
git add web/servers.php
git commit -m "feat(web): servers.php 加入分类/目标路径编辑 + 分类批量推送对话框"

任务 6tasks.php → push.php + index.php + logs.php 更新

文件:

  • 重命名:web/tasks.phpweb/push.php

  • 修改:web/index.php

  • 修改:web/logs.php

  • 修改:web/servers.phpsidebar 链接更新)

  • 步骤 1:tasks.php 改为推送历史页

mv web/tasks.php web/push.php

修改 push.php:删掉所有任务表单/编辑/添加/删除相关代码。保留表格结构,改为显示 SyncLog(推送历史):

$logs = [];
$resp = api()->getAllLogs(100);
$logs = $resp['data'] ?? [];

表格列改为:ID、服务器、源目录、目标目录、状态、文件数、传输大小、耗时、时间。

  • 步骤 2index.php 仪表盘更新

删除 "活跃同步任务" 卡片。新增 "推送目标服务器" 卡片(统计有 target_path 的服务器数)。

dashboard stats 简化为:服务器总数、在线服务器、离线服务器、已配置推送目标。

  • 步骤 3logs.php 简化

删除 task_id 筛选,保留 server_id 和 status 筛选。

  • 步骤 4:所有页面 sidebar 链接更新

<a href="tasks.php"> 改为 <a href="push.php">推送日志</a>。 在 servers.php、index.php、push.php、logs.php、settings.php、deploy.php 的 sidebar 中统一修改。

  • 步骤 5Commit
git add web/
git commit -m "refactor(web): tasks.php→push.php 推送历史页,仪表盘/日志简化,sidebar 统一"

任务 7:脚本和配置文件清理

文件:

  • 修改:install.sh

  • 修改:deploy/install_central.sh

  • 修改:deploy/install_agent.sh

  • 修改:agent/install.sh

  • 修改:agent/agent.sh

  • 修改:agent/config.example.yaml

  • 修改:web/agent/install.sh

  • 修改:web/agent/agent.sh

  • 修改:web/deploy.php

  • 步骤 1agent 脚本删除 trigger 相关

agent/agent.sh 删除 notify_central 函数和 watch_directory 中对 TRIGGER_FILE 的检测逻辑。保留 heartbeat。

agent/install.shweb/agent/install.sh 删除 NEXUS_TRIGGER_FILE 环境变量行。

agent/config.example.yaml 删除 trigger_file 字段。

  • 步骤 2install.sh 清理

删除 --db-path 参数和 DB_PATH 变量。更新 help 文本删除 trigger/cron 相关说明。

  • 步骤 3deploy/install_central.sh 清理

删除 trigger/sync 相关 echo 提示。

  • 步骤 4deploy/install_agent.sh 清理

删除 trigger_file 配置项。

  • 步骤 5web/deploy.php 清理

删除 .sync-trigger 命令示例文本。

  • 步骤 6Commit
git add agent/ deploy/ install.sh web/agent/ web/deploy.php
git commit -m "chore: 清理 scripts/config 中的 trigger/task 引用"

任务 8:最终验证和文档更新

文件:

  • 修改:docs/CHANGELOG.md

  • 修改:README.md(部分)

  • 修改:宝塔部署指南.md(部分)

  • 步骤 1:全量 Python 编译验证

find server -name "*.py" | xargs python -m py_compile && echo "All Python OK"
  • 步骤 2:更新 CHANGELOG.md

追加迭代 2 记录。

  • 步骤 3:更新 README.md 架构图和特性列表

删除 sync_tasks / trigger_file / cron 相关描述。更新系统架构图。

  • 步骤 4Commit
git add docs/ README.md 宝塔部署指南.md
git commit -m "docs: 更新 CHANGELOG/README/宝塔指南 适配简化推送模型"

自检

  • 规格全覆盖:数据模型 ✓ API ✓ 服务层 ✓ 前端 ✓ 脚本 ✓ 文档 ✓
  • 无占位符/TODO
  • 类型一致性:push_to_server(server, source_path, target_path) 签名在 sync_engine 和 servers.py 一致
  • 每个任务都有精确文件路径和完整代码