feat: expand scheduling, notifications, and queue runtime

- add scheduled task plumbing across backend core, CLI, and frontend surfaces

- add frontend notification UI and keep service pages using the repaired shared stylesheet

- refactor code queue runtime and update baidu netdisk/service integration docs
This commit is contained in:
Codex
2026-05-13 08:43:43 +00:00
parent 6a04144d3f
commit a242e3e3ec
45 changed files with 10421 additions and 6494 deletions
@@ -99,9 +99,9 @@ const serviceStartedAt = new Date().toISOString();
const recentLogs: JsonRecord[] = [];
function normalizedAppRoot(value: string): string {
const root = pathPosix.normalize(`/${String(value || "").replace(/^\/+/, "")}`);
if (!root.startsWith("/apps/")) return "/apps/UniDeskBaiduNetdisk";
return root.replace(/\/+$/u, "") || "/apps/UniDeskBaiduNetdisk";
const raw = String(value || "/").trim() || "/";
const normalized = pathPosix.normalize(raw.startsWith("/") ? raw : `/${raw}`);
return normalized === "/" ? "/" : normalized.replace(/\/+$/u, "") || "/";
}
function configFromEnv(): RuntimeConfig {
@@ -116,7 +116,7 @@ function configFromEnv(): RuntimeConfig {
clientId: process.env.BAIDU_NETDISK_CLIENT_ID || process.env.BAIDU_NETDISK_APP_KEY || "",
clientSecret: process.env.BAIDU_NETDISK_CLIENT_SECRET || process.env.BAIDU_NETDISK_SECRET_KEY || "",
tokenKey: process.env.BAIDU_NETDISK_TOKEN_KEY || "",
appRoot: normalizedAppRoot(process.env.BAIDU_NETDISK_APP_ROOT || "/apps/UniDeskBaiduNetdisk"),
appRoot: normalizedAppRoot(process.env.BAIDU_NETDISK_APP_ROOT || "/"),
stagingDir: resolve(process.env.BAIDU_NETDISK_STAGING_DIR || "/data/staging"),
partSizeBytes: Number.isFinite(partSizeBytes) && partSizeBytes > 0 ? partSizeBytes : 4 * 1024 * 1024,
userAgent: process.env.BAIDU_NETDISK_USER_AGENT || "pan.baidu.com",
@@ -448,6 +448,11 @@ async function createRemoteFolder(accessToken: string, path: string): Promise<Js
}
async function ensureAppRoot(accessToken: string, force = false): Promise<void> {
if (config.appRoot === "/") {
appRootEnsuredAt = Date.now();
if (force) log("remote_root_ready", { path: config.appRoot });
return;
}
if (!force && appRootEnsuredAt > 0 && Date.now() - appRootEnsuredAt < 10 * 60 * 1000) return;
if (appRootEnsurePromise) return appRootEnsurePromise;
appRootEnsurePromise = (async () => {
@@ -465,10 +470,11 @@ async function ensureAppRoot(accessToken: string, force = false): Promise<void>
function remotePathInsideRoot(input: string | undefined, fallback = config.appRoot): string {
const raw = String(input || fallback).trim() || fallback;
if (raw.includes("\0")) throw new HttpError(400, "remote path must not contain null bytes");
const normalized = pathPosix.normalize(raw.startsWith("/") ? raw : pathPosix.join(config.appRoot, raw));
const clean = normalized.replace(/\/+$/u, "") || config.appRoot;
if (clean !== config.appRoot && !clean.startsWith(`${config.appRoot}/`)) {
throw new HttpError(400, "remote path must stay inside app root", { appRoot: config.appRoot, path: clean });
const clean = normalized === "/" ? "/" : normalized.replace(/\/+$/u, "") || config.appRoot;
if (config.appRoot !== "/" && clean !== config.appRoot && !clean.startsWith(`${config.appRoot}/`)) {
throw new HttpError(400, "remote path must stay inside configured root", { rootPath: config.appRoot, path: clean });
}
return clean;
}