fix: 优化电脑端弹窗尺寸并彻底修复系统更新排队调度卡死问题
TallyNote release / linux-x64 (push) Successful in 6m46s

This commit is contained in:
Qiufeng
2026-09-04 14:24:54 +08:00
parent 1ecb783d0c
commit b46a7ddc87
12 changed files with 184 additions and 18 deletions
+12
View File
@@ -62,6 +62,7 @@ import {
reconcileOrphanedUpdateJobs,
readCachedRelease,
writeUpdateRequest,
cancelUpdateJob,
type UpdateRequest,
} from "./update-service.js";
@@ -1182,6 +1183,17 @@ export async function buildApp(database: DatabaseContext, config: AppConfig) {
return reply.code(202).send({ job: { id, status: "queued", operation: "download", version } });
});
app.post("/api/update/cancel", { preHandler: guard(database, config) }, async (request, reply) => {
reconcileOrphanedUpdateJobs(database.sqlite, config);
const body = (request.body && typeof request.body === "object" ? request.body : {}) as { jobId?: string };
const result = cancelUpdateJob(database.sqlite, config, request.auth!.admin.id, request.id, body.jobId);
if (!result.cancelled) {
throw new AppError(409, "CANNOT_CANCEL", result.message || "无法取消当前更新任务");
}
reply.header("Cache-Control", "no-store");
return reply.send({ success: true, message: "已取消更新任务" });
});
app.get("/api/update/jobs/:id", { preHandler: guard(database, config) }, async (request, reply) => {
const id = z.string().uuid().parse((request.params as { id: string }).id);
reconcileOrphanedUpdateJobs(database.sqlite, config);
+50 -3
View File
@@ -38,6 +38,7 @@ export const ACTIVE_UPDATE_STATUSES: readonly UpdateJobStatus[] = [
// a lease while doing long downloads/backups; only an expired lease permits
// the server to reclaim an active row.
export const ORPHANED_UPDATE_TIMEOUT_MS = 5 * 60 * 1000;
export const QUEUED_UPDATE_TIMEOUT_MS = 25 * 1000;
export type CachedRelease = {
checkedAt: number;
@@ -384,6 +385,14 @@ function markerMtime(filePath: string): number | null {
}
}
function forceRemoveRequest(filePath: string): void {
try {
const info = lstatSync(filePath);
if (!info.isFile() && !info.isSymbolicLink()) return;
unlinkSync(filePath);
} catch {}
}
function removeExpiredRequest(filePath: string, now: number): void {
try {
const info = lstatSync(filePath);
@@ -521,10 +530,48 @@ export function reconcileOrphanedUpdateJobs(database: Database.Database, config:
&& !reconciledIds.has(queuedRequest.id)
&& !(queuedRequest.status === "staged" && queuedRequest.operation === "download"),
);
if (!stateFresh
&& (!requestPresent || (requestMtime !== null && now - requestMtime >= ORPHANED_UPDATE_TIMEOUT_MS))
&& !requestStillNeeded) {
if (!stateFresh && !requestStillNeeded) {
forceRemoveRequest(config.updateRequestPath);
} else if (!stateFresh && (!requestPresent || (requestMtime !== null && now - requestMtime >= ORPHANED_UPDATE_TIMEOUT_MS))) {
removeExpiredRequest(config.updateRequestPath, now);
}
return reconciled;
}
export function cancelUpdateJob(
database: Database.Database,
config: AppConfig,
adminId: string,
requestId: string,
jobId?: string,
): { cancelled: boolean; message?: string } {
const job = jobId
? database.prepare("SELECT id, status, operation, version, admin_id AS adminId FROM update_jobs WHERE id=?").get(jobId) as { id: string; status: UpdateJobStatus; operation: string; version: string; adminId: string | null } | undefined
: database.prepare("SELECT id, status, operation, version, admin_id AS adminId FROM update_jobs WHERE status='queued' ORDER BY created_at DESC LIMIT 1").get() as { id: string; status: UpdateJobStatus; operation: string; version: string; adminId: string | null } | undefined;
if (!job) return { cancelled: false, message: "当前没有处于等待调度的更新任务" };
if (job.status !== "queued") return { cancelled: false, message: "任务已开始处理,无法取消" };
const now = Date.now();
const changed = database.transaction(() => {
const result = database.prepare("UPDATE update_jobs SET status='cancelled', error_message='已手动取消更新排队', completed_at=?, updated_at=? WHERE id=? AND status='queued'").run(now, now, job.id);
if (result.changes !== 1) return false;
writeAudit(database, {
requestId,
actorAdminId: adminId,
action: "update.cancelled",
targetType: "update",
targetId: job.id,
outcome: "success",
before: { status: job.status, operation: job.operation, version: job.version },
after: { status: "cancelled", version: job.version },
});
return true;
})();
if (changed) {
forceRemoveRequest(config.updateRequestPath);
return { cancelled: true };
}
return { cancelled: false, message: "取消失败,任务状态可能已改变" };
}