style(copy): punctuation rules and site-wide fix with a CI check (QA U11)
- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing, full-width parens around Chinese, single ellipsis, 「」 quotes, period rules) - scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX text (not comments, types, keys, match/compare strings); --fix, --strict, --by-file; wired into deno task ci as check:copy in warn mode - --fix applied to web, server and contracts; files other lanes were editing (subscriptions, sites, browser, agent, auth/account, inventory) left for them - tests updated for the changed messages Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9b017906cf
commit
52402864d6
143 files changed
+985
-606
No files matched your search
@@ -147,7 +147,7 @@ describe.skipIf(!hasPg)("整理执行:onExisting / partial 清理 / TIMEOUT 重
|
||||
for (const attempt of [1, 2]) {
|
||||
const err = await executeRecord(h.d, plan.recordId!).catch((x) => x);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
expect(String(err.message)).toContain(`TIMEOUT@${f.src}(第 ${attempt} 次)`);
|
||||
expect(String(err.message)).toContain(`TIMEOUT@${f.src}(第 ${attempt} 次)`);
|
||||
const fr = await h.db
|
||||
.selectFrom("organization_file_result")
|
||||
.select(["status"])
|
||||
|
||||
@@ -111,7 +111,8 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
||||
const c = (ev?.content ?? {}) as { rejected?: { code: string; message: string }[]; reasons?: string[] };
|
||||
for (const r of c.rejected ?? []) rejected.push({ code: r.code, message: r.message });
|
||||
for (const r of c.reasons ?? []) rejected.push({ code: r.split(":")[0] ?? r, message: r });
|
||||
if (rejected.length === 0) rejected.push({ code: "not_planned", message: "未生成计划:版本目标正忙或条件不满足" });
|
||||
if (rejected.length === 0)
|
||||
rejected.push({ code: "not_planned", message: "未生成计划:版本目标正忙或条件不满足" });
|
||||
}
|
||||
const fresh = await db
|
||||
.selectFrom("acquisition_slot")
|
||||
@@ -148,7 +149,7 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
||||
.where("id", "=", plan.slot_id)
|
||||
.forUpdate()
|
||||
.executeTakeFirstOrThrow();
|
||||
if (slot.revision + 1 !== plan.expected_revision) return "版本目标已变化,重新生成计划";
|
||||
if (slot.revision + 1 !== plan.expected_revision) return "版本目标已变化,重新生成计划";
|
||||
if (BUSY.includes(slot.status)) return `版本目标正忙(${slot.status})`;
|
||||
let st = { status: slot.status, hasAsset: slot.current_asset_id !== null };
|
||||
if (st.status === "satisfied") st = transition(st, { type: "upgrade_found" });
|
||||
@@ -177,7 +178,7 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
||||
});
|
||||
if (ok) throw new CapabilityError("conflict", ok);
|
||||
} else if (!["reserved", "submitted", "completed"].includes(plan.status)) {
|
||||
throw new CapabilityError("precondition", `计划状态为 ${plan.status},不能提交`);
|
||||
throw new CapabilityError("precondition", `计划状态为 ${plan.status},不能提交`);
|
||||
}
|
||||
// 与自动路径同一幂等键:同一计划只会有一个 submitDownload 任务
|
||||
const { taskId } = await enqueueTracked(d, {
|
||||
@@ -231,7 +232,7 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
||||
if (!frozen) {
|
||||
throw new CapabilityError(
|
||||
"precondition",
|
||||
res.recordId ? `下载任务已有进行中的整理记录 ${res.recordId}` : `无法生成整理计划:${res.reason ?? res.status}`,
|
||||
res.recordId ? `下载任务已有进行中的整理记录 ${res.recordId}` : `无法生成整理计划:${res.reason ?? res.status}`,
|
||||
);
|
||||
}
|
||||
const entity = await d.ctx.db
|
||||
@@ -313,7 +314,7 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
||||
dryRun: true,
|
||||
});
|
||||
if (!again.plan || planHash(req, again.plan) !== art.content_hash)
|
||||
throw new CapabilityError("conflict", "源文件或目标已变化,计划失效。重新生成整理计划");
|
||||
throw new CapabilityError("conflict", "源文件或目标已变化,计划失效。重新生成整理计划");
|
||||
const taskId = await ensureTaskRow(db, {
|
||||
kind: "verifyAndImport",
|
||||
idempotencyKey: taskKey,
|
||||
@@ -389,8 +390,8 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () =>
|
||||
if (!cur) throw new CapabilityError("not_found", "策略版本不存在");
|
||||
if (cur.status === "published") return cur; // 幂等:已发布
|
||||
if (cur.revision !== input.expectedRevision)
|
||||
throw new CapabilityError("conflict", `revision 不匹配(当前 ${cur.revision})`);
|
||||
if (cur.status !== "draft") throw new CapabilityError("precondition", `只能发布草案(当前 ${cur.status})`);
|
||||
throw new CapabilityError("conflict", `revision 不匹配(当前 ${cur.revision})`);
|
||||
if (cur.status !== "draft") throw new CapabilityError("precondition", `只能发布草案(当前 ${cur.status})`);
|
||||
return publishRevision(trx, cur.id, cur.policy_key, ctx.actorId, "superseded");
|
||||
});
|
||||
return { policy: toApi(row) as never };
|
||||
|
||||
@@ -106,19 +106,19 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
|
||||
key: "downloads.sync",
|
||||
defaults: {
|
||||
name: "qB 同步",
|
||||
description: "同步下载器任务状态,按缓存阈值暂停或恢复",
|
||||
description: "同步下载器任务状态,按缓存阈值暂停或恢复",
|
||||
group: "download",
|
||||
schedule: every(syncMs),
|
||||
},
|
||||
run: async () => {
|
||||
const st = await syncDownloads(d);
|
||||
if (st.instances > 0 && st.unreachable.length === st.instances) {
|
||||
throw new Error(`下载器都连不上:${st.unreachable.join("、")}`);
|
||||
throw new Error(`下载器都连不上:${st.unreachable.join("、")}`);
|
||||
}
|
||||
return {
|
||||
summary:
|
||||
`${st.instances} 个下载器,更新 ${st.updated},完成 ${st.completed.length}` +
|
||||
(st.unreachable.length ? `,连不上 ${st.unreachable.length}` : ""),
|
||||
`${st.instances} 个下载器,更新 ${st.updated},完成 ${st.completed.length}` +
|
||||
(st.unreachable.length ? `,连不上 ${st.unreachable.length}` : ""),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -148,14 +148,14 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se
|
||||
run: guarded(d, async (d) => {
|
||||
const submitted = await sweepReservedPlans(d);
|
||||
const imports = await sweepPendingImports(d);
|
||||
return `补提交 ${submitted},补入库 ${imports}`;
|
||||
return `补提交 ${submitted},补入库 ${imports}`;
|
||||
}),
|
||||
});
|
||||
registerScheduledJob(ctx, {
|
||||
key: "slots.reevaluate",
|
||||
defaults: {
|
||||
name: "到期重评",
|
||||
description: "到了重评时间的收录位重新评估(忙时延后的发布、定时)",
|
||||
description: "到了重评时间的收录位重新评估(忙时延后的发布、定时)",
|
||||
group: "download",
|
||||
schedule: every(planMs),
|
||||
},
|
||||
|
||||
@@ -107,7 +107,7 @@ export function createPorts(
|
||||
return client;
|
||||
},
|
||||
async torrentSource({ release, site }) {
|
||||
if (!site) throw new Error("站点不存在,无法获取种子");
|
||||
if (!site) throw new Error("站点不存在,无法获取种子");
|
||||
const raw = (release.raw ?? {}) as Record<string, unknown>;
|
||||
const siteMeta = (raw._site ?? {}) as Record<string, unknown>;
|
||||
const defId = [str(siteMeta.adapterSiteId), site.key].find((k) => k && getSiteDefinition(k));
|
||||
|
||||
@@ -18,7 +18,7 @@ export function registerDashboardRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "get",
|
||||
path: "/dashboard",
|
||||
scope: "read",
|
||||
summary: "首页聚合:自动化概览/当前工作/需要我处理/缓存与保留/来源健康/最近产出",
|
||||
summary: "首页聚合:自动化概览/当前工作/需要我处理/缓存与保留/来源健康/最近产出",
|
||||
response: Dashboard,
|
||||
},
|
||||
async () => {
|
||||
@@ -141,9 +141,9 @@ export function registerDashboardRoutes(app: ModuleApp, d: AoDeps) {
|
||||
const dv = decideVolume(v, cache);
|
||||
const cands = await cleanupCandidates(d, v);
|
||||
const notes: string[] = [];
|
||||
if (!v.snapshot) notes.push(v.path ? "磁盘用量读取失败,已暂停新增下载" : "实例未配置保存路径,无法统计");
|
||||
if (!v.snapshot) notes.push(v.path ? "磁盘用量读取失败,已暂停新增下载" : "实例未配置保存路径,无法统计");
|
||||
if (dv.unknownSizeTaskIds.length) notes.push(`${dv.unknownSizeTaskIds.length} 个在途任务体积未知`);
|
||||
if (dv.unmanagedGrowthRisk) notes.push("有未纳管任务仍在增长,暂停纳管任务后磁盘仍会增长");
|
||||
if (dv.unmanagedGrowthRisk) notes.push("有未纳管任务仍在增长,暂停纳管任务后磁盘仍会增长");
|
||||
const pct = usedPercent(v);
|
||||
volumes.push({
|
||||
volumeKey: v.volumeKey,
|
||||
|
||||
@@ -75,7 +75,7 @@ export function registerDownloadRoutes(app: ModuleApp, d: AoDeps) {
|
||||
const exists = await db().selectFrom("download_task").select("id").where("id", "=", params.id).executeTakeFirst();
|
||||
if (!exists) notFound("下载任务");
|
||||
const r = await userResume(d, params.id);
|
||||
if (r === "threshold_active") throw new ApiException("precondition_failed", "缓存卷仍高于恢复水位,不能手动恢复");
|
||||
if (r === "threshold_active") throw new ApiException("precondition_failed", "缓存卷仍高于恢复水位,不能手动恢复");
|
||||
return toApi<DownloadTask>(r);
|
||||
},
|
||||
{ tag: TAG },
|
||||
@@ -91,7 +91,7 @@ export function registerDownloadRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "post",
|
||||
path: "/download-cache/cleanup",
|
||||
scope: "admin",
|
||||
summary: "入队一轮缓存清理(只清已验证入库且满足保留/H&R 的纳管任务);返回任务 id,进度见任务页",
|
||||
summary: "入队一轮缓存清理(只清已验证入库且满足保留/H&R 的纳管任务);返回任务 id,进度见任务页",
|
||||
body: z.object({ downloaderInstanceId: z.uuid().optional() }),
|
||||
response: accepted,
|
||||
},
|
||||
@@ -110,7 +110,7 @@ export function registerDownloadRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "post",
|
||||
path: "/library/reconcile",
|
||||
scope: "admin",
|
||||
summary: "入队一轮资产对账(库根哨兵 + 增量 stat;不删记录、不补下载);返回任务 id,进度见任务页",
|
||||
summary: "入队一轮资产对账(库根哨兵 + 增量 stat;不删记录、不补下载);返回任务 id,进度见任务页",
|
||||
body: z.object({ libraryRootId: z.uuid().optional() }),
|
||||
response: accepted,
|
||||
},
|
||||
|
||||
@@ -137,7 +137,7 @@ export function registerOrganizationRoutes(app: ModuleApp, d: AoDeps) {
|
||||
if (block) {
|
||||
throw new ApiException(
|
||||
"conflict",
|
||||
`下载任务 ${block.downloadTaskId} 的做种义务${block.hnr === "unknown" ? "未知" : "未满足"},已拒绝移动(未改为复制)。` +
|
||||
`下载任务 ${block.downloadTaskId} 的做种义务${block.hnr === "unknown" ? "未知" : "未满足"},已拒绝移动(未改为复制)。` +
|
||||
`待处理事项 ${block.decisionId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ export function registerRuleRoutes(app: ModuleApp, d: AoDeps) {
|
||||
.where("id", "=", params.id)
|
||||
.executeTakeFirst();
|
||||
if (!exists) notFound("版本目标");
|
||||
conflict(`revision 不匹配(当前 ${exists!.revision})`);
|
||||
conflict(`revision 不匹配(当前 ${exists!.revision})`);
|
||||
}
|
||||
await kickEvaluate(d, { acquisitionSlotId: params.id });
|
||||
return toApi<AcquisitionSlot>(r!);
|
||||
@@ -246,7 +246,7 @@ export function registerRuleRoutes(app: ModuleApp, d: AoDeps) {
|
||||
if (!r.success)
|
||||
throw new ApiException(
|
||||
"validation_failed",
|
||||
`rules 不符合收录策略结构: ${r.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; ")}`,
|
||||
`rules 不符合收录策略结构:${r.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; ")}`,
|
||||
);
|
||||
};
|
||||
mount(
|
||||
@@ -293,7 +293,7 @@ export function registerRuleRoutes(app: ModuleApp, d: AoDeps) {
|
||||
async (_c, { params, body }) => {
|
||||
const cur = await db().selectFrom("policy_revision").selectAll().where("id", "=", params.id).executeTakeFirst();
|
||||
if (!cur) notFound("策略版本");
|
||||
if (cur!.status !== "draft") conflict("只能修改草案。要改已发布版本,新建草案");
|
||||
if (cur!.status !== "draft") conflict("只能修改草案。要改已发布版本,新建草案");
|
||||
if (body.rules !== undefined) checkRules(cur!.kind, body.rules);
|
||||
const r = await db()
|
||||
.updateTable("policy_revision")
|
||||
@@ -344,8 +344,8 @@ export function registerRuleRoutes(app: ModuleApp, d: AoDeps) {
|
||||
.forUpdate()
|
||||
.executeTakeFirst();
|
||||
if (!cur) notFound("策略版本");
|
||||
if (cur!.revision !== body.expectedRevision) conflict(`revision 不匹配(当前 ${cur!.revision})`);
|
||||
if (cur!.status !== "draft") conflict(`只能发布草案(当前 ${cur!.status})`);
|
||||
if (cur!.revision !== body.expectedRevision) conflict(`revision 不匹配(当前 ${cur!.revision})`);
|
||||
if (cur!.status !== "draft") conflict(`只能发布草案(当前 ${cur!.status})`);
|
||||
checkRules(cur!.kind, cur!.rules);
|
||||
return publishRevision(trx, cur!.id, cur!.policy_key, actorOf(c), "superseded");
|
||||
});
|
||||
|
||||
@@ -58,16 +58,16 @@ export async function sourceGuard(exec: Exec): Promise<SourceGuard> {
|
||||
return {
|
||||
roots,
|
||||
async check(p) {
|
||||
if (!isAbsolute(p)) throw new PathGuardError(`源路径必须是绝对路径: ${p}`, p);
|
||||
if (!isAbsolute(p)) throw new PathGuardError(`源路径必须是绝对路径:${p}`, p);
|
||||
if (p.split("/").some((seg) => seg === "..")) throw new PathGuardError(`源路径不允许包含 "..": ${p}`, p);
|
||||
let r: string;
|
||||
try {
|
||||
r = await realpath(p);
|
||||
} catch {
|
||||
throw new PathGuardError(`源路径不存在或不可访问: ${p}`, p);
|
||||
throw new PathGuardError(`源路径不存在或不可访问:${p}`, p);
|
||||
}
|
||||
if (!roots.some((root) => within(r, root)))
|
||||
throw new PathGuardError(`源路径不在已登记的库根或下载器路径映射之下: ${p}`, p);
|
||||
throw new PathGuardError(`源路径不在已登记的库根或下载器路径映射之下:${p}`, p);
|
||||
return r;
|
||||
},
|
||||
};
|
||||
@@ -280,7 +280,7 @@ export async function authorizeMoveEntities(
|
||||
unauthorized.push({
|
||||
label: s.label,
|
||||
entityId: null,
|
||||
reason: "无法确定所属作品,只能用一次性审批",
|
||||
reason: "无法确定所属作品,只能用一次性审批",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -306,8 +306,8 @@ export async function authorizeMoveEntities(
|
||||
unauthorized,
|
||||
reason:
|
||||
detail.length > 0 && (reasons.length > 0 || grants.length > 0 || !single || unauthorized[0]?.entityId === null)
|
||||
? `移动需要授权:${detail.join(";")}`
|
||||
: "移动会删除源文件和下载器任务,需要管理员会话,或覆盖该作品的持续授权 / 一次性审批",
|
||||
? `移动需要授权:${detail.join(";")}`
|
||||
: "移动会删除源文件和下载器任务,需要管理员会话,或覆盖该作品的持续授权 / 一次性审批",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -339,11 +339,11 @@ export async function hnrBlockForMove(
|
||||
if (hnr === true) continue;
|
||||
const decisionId = await upsertDecision(d, db, {
|
||||
kind: "approval_required",
|
||||
title: `做种义务未满足,已阻止移动:${dt.name ?? dt.torrent_hash}`,
|
||||
title: `做种义务未满足,已阻止移动:${dt.name ?? dt.torrent_hash}`,
|
||||
summary:
|
||||
hnr === false
|
||||
? "H&R / 做种要求未满足,移动会删除源文件和下载任务,已拒绝(未改为复制)"
|
||||
: "H&R 要求未知,按未满足处理,已拒绝移动(未改为复制)",
|
||||
? "H&R / 做种要求未满足,移动会删除源文件和下载任务,已拒绝(未改为复制)"
|
||||
: "H&R 要求未知,按未满足处理,已拒绝移动(未改为复制)",
|
||||
subjectType: "download_task",
|
||||
subjectId: dt.id,
|
||||
dedupeKey: `manual_move_hnr:${dt.id}`,
|
||||
@@ -357,7 +357,7 @@ export async function hnrBlockForMove(
|
||||
},
|
||||
options: [
|
||||
{ key: "wait", label: "做种满足后再移动" },
|
||||
{ key: "copy", label: "改用复制(需重新发起)" },
|
||||
{ key: "copy", label: "改用复制(需重新发起)" },
|
||||
],
|
||||
});
|
||||
return { downloadTaskId: dt.id, hnr, decisionId };
|
||||
|
||||
@@ -55,7 +55,7 @@ async function walkTree(
|
||||
if (out.files.length >= limit) return true;
|
||||
const p = join(dir, n);
|
||||
const st = await statSafe(p);
|
||||
if (st.kind !== "ok") throw new Error(`源不可访问: ${p} (${st.kind})`);
|
||||
if (st.kind !== "ok") throw new Error(`源不可访问:${p} (${st.kind})`);
|
||||
if (st.facts.isSymbolicLink) out.links.push(p);
|
||||
else if (st.facts.isDirectory) {
|
||||
out.dirs.push(p);
|
||||
@@ -93,7 +93,7 @@ export async function planTransfer(d: AoDeps, req: ImportRequest, t: TransferSpe
|
||||
const add = async (src: string, dst: string) => {
|
||||
if (t.guard) await t.guard(src);
|
||||
const s = await statSafe(src);
|
||||
if (s.kind !== "ok") throw new Error(`源不可访问: ${src} (${s.kind})`);
|
||||
if (s.kind !== "ok") throw new Error(`源不可访问:${src} (${s.kind})`);
|
||||
files.push({
|
||||
src,
|
||||
dst,
|
||||
@@ -205,7 +205,7 @@ export async function planTransfer(d: AoDeps, req: ImportRequest, t: TransferSpe
|
||||
target_path: rel(f.targetPath),
|
||||
status: held ? "failed" : f.status,
|
||||
skip_reason: held ? null : f.skipReason,
|
||||
...(held ? { error: jsonb({ code: "EEXIST", message: "目标已存在,不覆盖" }), finished_at: now } : {}),
|
||||
...(held ? { error: jsonb({ code: "EEXIST", message: "目标已存在,不覆盖" }), finished_at: now } : {}),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ async function walk(
|
||||
guard?: ((p: string) => Promise<string>) | undefined,
|
||||
): Promise<void> {
|
||||
const st = await statSafe(p);
|
||||
if (st.kind !== "ok") throw new FsError("ENOENT", `源路径不可访问: ${p} (${st.kind})`, { path: p });
|
||||
if (st.kind !== "ok") throw new FsError("ENOENT", `源路径不可访问:${p} (${st.kind})`, { path: p });
|
||||
if (st.facts.isFile) {
|
||||
// 目录里的符号链接可能指向根外:逐个文件复核 realpath
|
||||
if (guard) await guard(p);
|
||||
@@ -389,7 +389,7 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
|
||||
const rid = await fail("identity_unknown");
|
||||
await decide_({
|
||||
kind: "identity_ambiguous",
|
||||
title: `无法确定作品:${dt?.name ?? req.sourcePaths?.[0] ?? "未知来源"}`,
|
||||
title: `无法确定作品:${dt?.name ?? req.sourcePaths?.[0] ?? "未知来源"}`,
|
||||
summary: "下载内容未关联作品。识别或指定作品后再整理",
|
||||
subjectType: "organization_record",
|
||||
subjectId: rid.recordId,
|
||||
@@ -450,8 +450,8 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
|
||||
await decide_({
|
||||
kind: "other",
|
||||
severity: "critical",
|
||||
title: `库根离线:${root.name}`,
|
||||
summary: `哨兵检查失败(${health.reason}),整理已暂停。文件不会因此被判为缺失`,
|
||||
title: `库根离线:${root.name}`,
|
||||
summary: `哨兵检查失败(${health.reason}),整理已暂停。文件不会因此被判为缺失`,
|
||||
subjectType: "library_root",
|
||||
subjectId: root.id,
|
||||
dedupeKey: `library_root_offline:${root.id}`,
|
||||
@@ -508,7 +508,7 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
|
||||
if (dec.kind === "conflict") {
|
||||
await decide_({
|
||||
kind: "path_conflict",
|
||||
title: `作品存在多个历史目录:${entity.title}`,
|
||||
title: `作品存在多个历史目录:${entity.title}`,
|
||||
summary: "不会按最新标题自动合并。指定要保留的目录",
|
||||
subjectType: "media_entity",
|
||||
subjectId: entity.id,
|
||||
@@ -700,7 +700,7 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise<PlanRes
|
||||
const hnrConflict = blocking.find((c) => c.kind === "hnr_unsatisfied" || c.kind === "hnr_unknown");
|
||||
await upsertDecision(d, trx, {
|
||||
kind: hnrConflict ? "approval_required" : "path_conflict",
|
||||
title: hnrConflict ? `做种义务未满足,已阻止移动:${entity.title}` : `整理冲突:${entity.title}`,
|
||||
title: hnrConflict ? `做种义务未满足,已阻止移动:${entity.title}` : `整理冲突:${entity.title}`,
|
||||
summary: blocking
|
||||
.map((c) => c.detail ?? c.kind)
|
||||
.join(";")
|
||||
@@ -850,7 +850,7 @@ export async function executeRecord(
|
||||
const fsr = fsRuntime(d);
|
||||
// 根检查也走执行子进程:NFS/FUSE 挂死 → offline(timeout),按根离线重试,绝不当成文件缺失
|
||||
const health = await fsr.proxy.checkRoot(frozen.rootPath, frozen.sentinel);
|
||||
if (health.kind === "offline") throw new Error(`库根离线(${health.reason}),稍后重试`);
|
||||
if (health.kind === "offline") throw new Error(`库根离线(${health.reason}),稍后重试`);
|
||||
for (const dir of frozen.targetDirs) await mkdir(dir, { recursive: true });
|
||||
// 写前顺带清本目录残留 partial(对照 MP:只清本目录、>24h;失败吞掉)。copy 路径在 copyWithVerify 内还会再清一次
|
||||
if (fsr.partialMaxAgeMs > 0)
|
||||
@@ -987,7 +987,7 @@ export async function executeRecord(
|
||||
error: r.error
|
||||
? jsonb(r.error)
|
||||
: r.status === "attention"
|
||||
? jsonb({ code: "source_not_removed", message: "目标已就绪,源文件未能删除" })
|
||||
? jsonb({ code: "source_not_removed", message: "目标已就绪,源文件未能删除" })
|
||||
: null,
|
||||
finished_at: now,
|
||||
})
|
||||
@@ -1010,8 +1010,8 @@ export async function executeRecord(
|
||||
if (outcome.status === "failed" && outcome.error && fsErrorClass(outcome.error.code) === "retry") {
|
||||
await upsertDecision(d, db, {
|
||||
kind: "other",
|
||||
title: outcome.error.code === "TIMEOUT" ? "文件操作挂死,已达重试上限" : "文件读写失败,已达重试上限",
|
||||
summary: `${pf.src} → ${pf.dst}:${outcome.error.message}(已尝试 ${outcome.attempt ?? fsr.maxAttempts} 次;记录与下载已保留,不会重新下载)`,
|
||||
title: outcome.error.code === "TIMEOUT" ? "文件操作挂死,已达重试上限" : "文件读写失败,已达重试上限",
|
||||
summary: `${pf.src} → ${pf.dst}:${outcome.error.message}(已尝试 ${outcome.attempt ?? fsr.maxAttempts} 次;记录与下载已保留,不会重新下载)`,
|
||||
subjectType: "organization_record",
|
||||
subjectId: recordId,
|
||||
dedupeKey: `fs_retry_exhausted:${opId}`,
|
||||
@@ -1027,8 +1027,8 @@ export async function executeRecord(
|
||||
if (outcome.replaced?.disposition === "left") {
|
||||
await upsertDecision(d, db, {
|
||||
kind: "other",
|
||||
title: "新版本已入库,旧文件未能处置",
|
||||
summary: `${pf.dst} 已替换为新版本;旧文件留在 ${outcome.replaced.path}`,
|
||||
title: "新版本已入库,旧文件未能处置",
|
||||
summary: `${pf.dst} 已替换为新版本;旧文件留在 ${outcome.replaced.path}`,
|
||||
subjectType: "organization_record",
|
||||
subjectId: recordId,
|
||||
dedupeKey: `replace_old_left:${opId}`,
|
||||
@@ -1039,7 +1039,7 @@ export async function executeRecord(
|
||||
await upsertDecision(d, db, {
|
||||
kind: "other",
|
||||
title: "跨卷移动后删除源文件失败",
|
||||
summary: `${pf.src} 已复制到目标并校验,源文件未删除`,
|
||||
summary: `${pf.src} 已复制到目标并校验,源文件未删除`,
|
||||
subjectType: "organization_record",
|
||||
subjectId: recordId,
|
||||
dedupeKey: `move_source_left:${opId}`,
|
||||
@@ -1050,7 +1050,7 @@ export async function executeRecord(
|
||||
if (deferred.length > 0) {
|
||||
// 交给任务引擎退避重试(executeRecord 幂等:已完成文件直接跳过,账本无结果的文件重跑并先对账)
|
||||
throw new Error(
|
||||
`文件操作可重试失败,等待重试:${deferred.map((x) => `${x.code}@${x.src}(第 ${x.attempt} 次)`).join(";")}`,
|
||||
`文件操作可重试失败,等待重试:${deferred.map((x) => `${x.code}@${x.src}(第 ${x.attempt} 次)`).join(";")}`,
|
||||
);
|
||||
}
|
||||
return finalizeRecord(d, recordId);
|
||||
@@ -1149,9 +1149,9 @@ function statFailure(st: StatResult, path: string, role: "源" | "目标"): { co
|
||||
case "ok":
|
||||
return null;
|
||||
case "missing":
|
||||
return role === "源" ? { code: "SOURCE_MISSING", message: `源文件不存在: ${path}` } : null;
|
||||
return role === "源" ? { code: "SOURCE_MISSING", message: `源文件不存在:${path}` } : null;
|
||||
case "permission_denied":
|
||||
return { code: "EACCES", message: `${role}无权限访问: ${path}` };
|
||||
return { code: "EACCES", message: `${role}无权限访问:${path}` };
|
||||
case "root_offline":
|
||||
return { code: "ROOT_OFFLINE", message: `${role}所在根离线(${st.reason}): ${path}` };
|
||||
case "error":
|
||||
@@ -1217,7 +1217,7 @@ async function transferFile(
|
||||
const err = fsErr(e);
|
||||
return {
|
||||
status: "failed",
|
||||
error: { ...err, message: `对账比对失败:${err.message}` },
|
||||
error: { ...err, message: `对账比对失败:${err.message}` },
|
||||
evidence: { phase: "reconcile", src, dst },
|
||||
};
|
||||
}
|
||||
@@ -1234,7 +1234,7 @@ async function transferFile(
|
||||
};
|
||||
}
|
||||
} else if (x.onExisting !== "replace")
|
||||
return { status: "failed", error: { code: "EEXIST", message: `目标已存在且内容不同: ${dst}` } };
|
||||
return { status: "failed", error: { code: "EEXIST", message: `目标已存在且内容不同:${dst}` } };
|
||||
}
|
||||
}
|
||||
const sFail = statFailure(s, src, "源");
|
||||
@@ -1262,7 +1262,7 @@ async function transferFile(
|
||||
if (crossDevice !== true) {
|
||||
const m = await px.moveSafe(src, dst, call);
|
||||
if (m.status === "moved") return { status: "succeeded", bytes: size };
|
||||
if (m.status === "exists") return { status: "failed", error: { code: "EEXIST", message: `目标已存在: ${dst}` } };
|
||||
if (m.status === "exists") return { status: "failed", error: { code: "EEXIST", message: `目标已存在:${dst}` } };
|
||||
// EXDEV:不静默降级;按设置显式走可恢复三步
|
||||
}
|
||||
if (!x.allowCrossVolume)
|
||||
@@ -1575,7 +1575,7 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise<Execu
|
||||
kind: "other",
|
||||
severity: "info",
|
||||
title: "旧版本待处置",
|
||||
summary: "新版本已核验入库,旧文件保留,按保留策略处理",
|
||||
summary: "新版本已核验入库,旧文件保留,按保留策略处理",
|
||||
subjectType: "media_asset",
|
||||
subjectId: old,
|
||||
dedupeKey: `old_version:${old}`,
|
||||
@@ -1636,7 +1636,7 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise<Execu
|
||||
} catch (err) {
|
||||
d.ctx.logger.warn(
|
||||
{ downloadTaskId: result.dt.id, err: { message: errMessage(err) } },
|
||||
"Move 后移除 qB 任务失败,下次同步重试",
|
||||
"Move 后移除 qB 任务失败,下次同步重试",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user