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:
rootandClaude Opus 5.5 committed 2026-09-28 20:09:29 +08:00
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 任务失败,下次同步重试",
);
}
}