fix: 跳过自动调度空目录

This commit is contained in:
2026-05-25 17:54:56 +08:00
parent 8e891964d5
commit 272ff38070
6 changed files with 39 additions and 7 deletions
+2
View File
@@ -238,6 +238,8 @@ Server Portable 和桌面版需要在目标系统原生构建:Windows 包在 W
自动调度开启后,系统会强制开启 `RemoteData.enabled` 和 `auto_delete_source`。调度器每轮先检查 `cache/auto_scheduler/ready.flag`;如果标识存在则直接触发远程下载并处理。没有标识时,会按 ZIP 文件名中的第一个时间戳判断每个目录是否覆盖目标自然周 7 天;全部就绪后写入标识,下一个检查周期再启动处理。处理成功并完成远程源文件清理后会删除标识;处理失败或源文件清理失败会保留标识,后续自动重试。 自动调度开启后,系统会强制开启 `RemoteData.enabled` 和 `auto_delete_source`。调度器每轮先检查 `cache/auto_scheduler/ready.flag`;如果标识存在则直接触发远程下载并处理。没有标识时,会按 ZIP 文件名中的第一个时间戳判断每个目录是否覆盖目标自然周 7 天;全部就绪后写入标识,下一个检查周期再启动处理。处理成功并完成远程源文件清理后会删除标识;处理失败或源文件清理失败会保留标识,后续自动重试。
如果配置了 `expected_directories`,其中某个目录可访问但完全没有 ZIP 文件,会视为该目录已停推并跳过,不再阻塞其他目录;但所有目录都为空时不会触发自动处理。
无论手动上传还是远程下载,处理流程都会按文件名日期对每个目录只保留最近 7 天文件。文件名支持 `XXX_YYYYMMDDHHMM_YYYYMMDDHHMM` 和 `XXX_YYYYMMDDHHMM` 两类格式,数据日期始终取第一个时间戳。 无论手动上传还是远程下载,处理流程都会按文件名日期对每个目录只保留最近 7 天文件。文件名支持 `XXX_YYYYMMDDHHMM_YYYYMMDDHHMM` 和 `XXX_YYYYMMDDHHMM` 两类格式,数据日期始终取第一个时间戳。
登录密码保存在本地 `auth.ini`,该文件不应提交到版本库。 登录密码保存在本地 `auth.ini`,该文件不应提交到版本库。
+30 -4
View File
@@ -30,6 +30,8 @@ class DirectoryReadyStatus:
missing_days: list[date] missing_days: list[date]
file_count: int file_count: int
error: str | None = None error: str | None = None
skipped: bool = False
skip_reason: str | None = None
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
@@ -40,6 +42,8 @@ class DirectoryReadyStatus:
"required_count": len(self.found_days) + len(self.missing_days), "required_count": len(self.found_days) + len(self.missing_days),
"file_count": self.file_count, "file_count": self.file_count,
"error": self.error, "error": self.error,
"skipped": self.skipped,
"skip_reason": self.skip_reason,
} }
@@ -145,7 +149,6 @@ class AutoScheduler:
target_days = required_week_days(scheduler.week_offset) target_days = required_week_days(scheduler.week_offset)
directory_status = self._check_remote_ready(remote_config, scheduler, target_days) directory_status = self._check_remote_ready(remote_config, scheduler, target_days)
ready = bool(directory_status) and all(item.ready for item in directory_status.values())
self._set_directory_status(directory_status) self._set_directory_status(directory_status)
error_count = sum(1 for item in directory_status.values() if item.error) error_count = sum(1 for item in directory_status.values() if item.error)
@@ -156,18 +159,30 @@ class AutoScheduler:
manual, manual,
) )
active_status = [item for item in directory_status.values() if not item.skipped]
skipped_count = len(directory_status) - len(active_status)
ready = bool(active_status) and all(item.ready for item in active_status)
if ready: if ready:
self._mark_ready(target_days, directory_status) self._mark_ready(target_days, directory_status)
skipped_text = f",已跳过 {skipped_count} 个停推目录" if skipped_count else ""
return self._finish_check( return self._finish_check(
"marked_ready", "marked_ready",
"远程数据已满足目标周 7 天,已写入就绪标识,下次检查将自动处理", f"远程数据已满足目标周 7 天{skipped_text},已写入就绪标识,下次检查将自动处理",
manual, manual,
) )
ready_count = sum(1 for item in directory_status.values() if item.ready) if not active_status and directory_status:
return self._finish_check(
"waiting",
f"远程数据未就绪,{len(directory_status)} 个目录均为空,已视为停推但不会触发处理",
manual,
)
ready_count = sum(1 for item in active_status if item.ready)
skipped_text = f",跳过 {skipped_count} 个停推目录" if skipped_count else ""
return self._finish_check( return self._finish_check(
"waiting", "waiting",
f"远程数据未就绪,{ready_count}/{len(directory_status)} 个目录满足目标周 7 天", f"远程数据未就绪,{ready_count}/{len(active_status)} 个有效目录满足目标周 7 天{skipped_text}",
manual, manual,
) )
@@ -249,6 +264,17 @@ class AutoScheduler:
error="远程目录不存在或无法访问", error="远程目录不存在或无法访问",
) )
if not files:
return DirectoryReadyStatus(
directory=directory,
ready=True,
found_days=[],
missing_days=[],
file_count=0,
skipped=True,
skip_reason="目录为空,视为已停推并跳过",
)
required = set(target_days) required = set(target_days)
found = { found = {
file_date file_date
+2 -1
View File
@@ -202,6 +202,7 @@ def get_target_week_range(week_offset: int = 0) -> tuple[date, date]:
1. **自动调度开启时,`auto_delete_source` 强制为 `true`**(前端灰显、后端校验) 1. **自动调度开启时,`auto_delete_source` 强制为 `true`**(前端灰显、后端校验)
2. `expected_directories` 为空时按远程 ZIP 实际所在目录逐个检测 2. `expected_directories` 为空时按远程 ZIP 实际所在目录逐个检测
3. `check_interval_hours` 最小值为 1 3. `check_interval_hours` 最小值为 1
4. 配置了 `expected_directories` 时,某个预期目录可访问但完全没有 ZIP 文件,视为该目录已停推并跳过;所有目录都为空时不触发自动处理
--- ---
@@ -308,7 +309,7 @@ GET /api/remote/scheduler/status
| 连续失败 ≥3 次 | 在前端调度状态面板显示红色失败状态 | | 连续失败 ≥3 次 | 在前端调度状态面板显示红色失败状态 |
| 处理失败 | **保留就绪标识**,下次检查时直接触发处理(不重新检测) | | 处理失败 | **保留就绪标识**,下次检查时直接触发处理(不重新检测) |
| 远程目录不存在 | 跳过该目录,记录警告,其他目录正常检测 | | 远程目录不存在 | 跳过该目录,记录警告,其他目录正常检测 |
| 目录内无 ZIP 文件 | 该目录判定为未就绪 | | 预期目录内无 ZIP 文件 | 视为该目录已停推并跳过,不阻塞其他目录;如果所有目录都为空,则继续等待,不触发处理 |
| 手动触发远程处理 | 自动调度运行中禁止手动触发,反之亦然(互斥锁复用 `global_task_lock`) | | 手动触发远程处理 | 自动调度运行中禁止手动触发,反之亦然(互斥锁复用 `global_task_lock`) |
--- ---
+1
View File
@@ -10,6 +10,7 @@
- `app/api/routers/remote.py` 新增 `/api/remote/scheduler/status` 和 `/api/remote/scheduler/trigger`,并将远程处理启动逻辑抽成 `start_remote_processing_job()` 供手动按钮和调度器复用。 - `app/api/routers/remote.py` 新增 `/api/remote/scheduler/status` 和 `/api/remote/scheduler/trigger`,并将远程处理启动逻辑抽成 `start_remote_processing_job()` 供手动按钮和调度器复用。
- `frontend/src/components/SettingsPanel.vue` 在远程数据源配置中加入自动调度区域:启用开关、检查间隔、目标周期、预期目录维护、调度状态、刷新状态和立即检查。配置下载/上传会随 `RemoteData` 一起携带自动调度配置。 - `frontend/src/components/SettingsPanel.vue` 在远程数据源配置中加入自动调度区域:启用开关、检查间隔、目标周期、预期目录维护、调度状态、刷新状态和立即检查。配置下载/上传会随 `RemoteData` 一起携带自动调度配置。
- 已验证:文件名日期解析、目标周计算、每目录 7 天筛选、调度开启强制删除源文件配置、调度日期精确筛选逻辑均通过临时 Python 片段;`.venv\Scripts\python.exe -m compileall app` 与 `npm run build` 通过,前端构建仅保留既有 Vite 大 chunk 警告。 - 已验证:文件名日期解析、目标周计算、每目录 7 天筛选、调度开启强制删除源文件配置、调度日期精确筛选逻辑均通过临时 Python 片段;`.venv\Scripts\python.exe -m compileall app` 与 `npm run build` 通过,前端构建仅保留既有 Vite 大 chunk 警告。
- 后续调整:预期目录可访问但完全没有 ZIP 文件时,自动调度会把该目录标记为 `skipped/已停推`,不再阻塞其他目录就绪;但所有目录都为空时不会写入 ready flag,也不会触发处理。
## 2026-05-25:修复 API Token 指定日期输入不可见 ## 2026-05-25:修复 API Token 指定日期输入不可见
+2 -2
View File
@@ -230,8 +230,8 @@
<div v-if="schedulerDirectoryRows.length > 0" class="scheduler-directory-list"> <div v-if="schedulerDirectoryRows.length > 0" class="scheduler-directory-list">
<div v-for="row in schedulerDirectoryRows" :key="row.name" class="scheduler-directory-row"> <div v-for="row in schedulerDirectoryRows" :key="row.name" class="scheduler-directory-row">
<span class="scheduler-directory-name">{{ row.name }}</span> <span class="scheduler-directory-name">{{ row.name }}</span>
<n-tag size="small" :type="row.ready ? 'success' : 'warning'"> <n-tag size="small" :type="row.skipped ? 'info' : row.ready ? 'success' : 'warning'">
{{ row.ready ? '就绪' : `缺 ${row.missing_days.length} 天` }} {{ row.skipped ? '已停推' : row.ready ? '就绪' : `缺 ${row.missing_days.length} 天` }}
</n-tag> </n-tag>
</div> </div>
</div> </div>
+2
View File
@@ -140,6 +140,8 @@ export interface RemoteSchedulerStatus {
required_count: number; required_count: number;
file_count: number; file_count: number;
error?: string | null; error?: string | null;
skipped?: boolean;
skip_reason?: string | null;
} }
>; >;
message?: string; message?: string;