style(copy): fix remaining punctuation violations and make check:copy strict
Auto-fix of 602 string literals across 74 files (half-width punctuation next to Chinese, parenthesised Chinese, quotes); biome reflow; e2/e6 assertions follow the new full-width summaries. check:copy now runs with --strict in ci. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0fccfe257d
commit
fb84a71f80
76 files changed
+582
-570
No files matched your search
@@ -60,7 +60,7 @@ export async function queryMediaServer(
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeout = new Promise<never>((_, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error(`timeout: 媒体服务器 ${limits.timeoutMs / 1000}s 内未响应`)),
|
||||
() => reject(new Error(`timeout:媒体服务器 ${limits.timeoutMs / 1000}s 内未响应`)),
|
||||
limits.timeoutMs,
|
||||
);
|
||||
});
|
||||
@@ -131,7 +131,7 @@ export function registerLibraryRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "get",
|
||||
path: "/library",
|
||||
scope: "read",
|
||||
summary: "媒体库列表(已登记资产或正在追踪的作品)",
|
||||
summary: "媒体库列表(已登记资产或正在追踪的作品)",
|
||||
query: LibraryListQuery,
|
||||
response: cursorPage(LibraryItemSchema),
|
||||
},
|
||||
@@ -232,7 +232,7 @@ export function registerLibraryRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "get",
|
||||
path: "/library/{id}",
|
||||
scope: "read",
|
||||
summary: "作品详情:实际路径、身份映射、元数据来源、各 Slot、候选拒绝理由、下载历史与校验结果",
|
||||
summary: "作品详情:实际路径、身份映射、元数据来源、各 Slot、候选拒绝理由、下载历史与校验结果",
|
||||
params: IdParam,
|
||||
response: LibraryDetail,
|
||||
},
|
||||
@@ -349,7 +349,7 @@ export function registerLibraryRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "get",
|
||||
path: "/inventory",
|
||||
scope: "read",
|
||||
summary: "库存叠加(站点工作台):按作品返回已有版本 / Slot 缺口 / 下载中",
|
||||
summary: "库存叠加(站点工作台):按作品返回已有版本 / Slot 缺口 / 下载中",
|
||||
query: InventoryQuery,
|
||||
response: InventoryResponse,
|
||||
},
|
||||
@@ -469,7 +469,7 @@ export function registerLibraryRoutes(app: ModuleApp, d: AoDeps) {
|
||||
method: "get",
|
||||
path: "/inventory/media-servers",
|
||||
scope: "read",
|
||||
summary: "Emby 兼容库存查询(多实例;接口失败/截断 = unknown,不等于缺失)",
|
||||
summary: "Emby 兼容库存查询(多实例;接口失败/截断 = unknown,不等于缺失)",
|
||||
query: MediaServerInventoryQuery,
|
||||
response: MediaServerInventory,
|
||||
},
|
||||
|
||||
@@ -91,7 +91,7 @@ export async function checkSuppression(trx: Trx, fingerprint: string, source: st
|
||||
.where("active", "=", true)
|
||||
.executeTakeFirst();
|
||||
if (!sup) return;
|
||||
if (source === "agent") conflict("用户已停用或删除过同一语义的规则,Agent 不能自动加回");
|
||||
if (source === "agent") conflict("用户已停用或删除过同一语义的规则,Agent 不能自动加回");
|
||||
await liftSuppressions(trx, fingerprint, by);
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ async function checkIdsExist(exec: Trx | AoDeps["ctx"]["db"], ids: readonly stri
|
||||
.execute();
|
||||
const have = new Set(rows.map((r) => r.id));
|
||||
const missing = ids.filter((x) => !have.has(x));
|
||||
if (missing.length) throw new ApiException("bad_request", `规则组不存在:${missing.join(", ")}`);
|
||||
if (missing.length) throw new ApiException("bad_request", `规则组不存在:${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
/** 版本目标 / 订阅引用的规则组 id 校验(供 slots 路由复用) */
|
||||
@@ -253,7 +253,7 @@ export function registerRuleGroupRoutes(app: ModuleApp, d: AoDeps) {
|
||||
Api.ruleAtoms.create,
|
||||
async (_c, { body }) => {
|
||||
const key = String(body.key).toUpperCase();
|
||||
if (BUILTIN_ATOM_MAP.has(key)) conflict(`${key} 是内置关键字,不能覆盖`);
|
||||
if (BUILTIN_ATOM_MAP.has(key)) conflict(`${key} 是内置关键字,不能覆盖`);
|
||||
checkAtomPatterns(body.include);
|
||||
checkAtomPatterns(body.exclude);
|
||||
const r = await db()
|
||||
@@ -318,7 +318,7 @@ export function registerRuleGroupRoutes(app: ModuleApp, d: AoDeps) {
|
||||
(e) => atomsOf(e).has(a!.key),
|
||||
),
|
||||
);
|
||||
if (users.length) conflict(`自定义条件 ${a!.key} 被规则组引用:${users.map((u) => u.name).join("、")}`);
|
||||
if (users.length) conflict(`自定义条件 ${a!.key} 被规则组引用:${users.map((u) => u.name).join("、")}`);
|
||||
await db().deleteFrom("rule_atom").where("id", "=", params.id).execute();
|
||||
return { ok: true as const };
|
||||
},
|
||||
@@ -434,7 +434,7 @@ export function registerRuleGroupRoutes(app: ModuleApp, d: AoDeps) {
|
||||
.executeTakeFirst();
|
||||
if (!cur) notFound("规则组");
|
||||
if (b.expectedRevision !== undefined && b.expectedRevision !== cur!.revision)
|
||||
conflict(`revision 不匹配(当前 ${cur!.revision})`);
|
||||
conflict(`revision 不匹配(当前 ${cur!.revision})`);
|
||||
if (b.name !== undefined && b.name !== cur!.name) {
|
||||
const dupName = await trx
|
||||
.selectFrom("rule_group")
|
||||
@@ -458,7 +458,7 @@ export function registerRuleGroupRoutes(app: ModuleApp, d: AoDeps) {
|
||||
ladderRequired: b.ladderRequired ?? cur!.ladder_required,
|
||||
};
|
||||
if (b.kind !== undefined && b.kind !== cur!.kind && cur!.default_order !== null)
|
||||
conflict("该规则组在默认链里,先从默认值移出再改类型");
|
||||
conflict("该规则组在默认链里,先从默认值移出再改类型");
|
||||
// 偏好标签迁来的组沿用旧指纹口径,规则没变就不重算
|
||||
const semanticChanged =
|
||||
b.kind !== undefined ||
|
||||
@@ -528,7 +528,7 @@ export function registerRuleGroupRoutes(app: ModuleApp, d: AoDeps) {
|
||||
.executeTakeFirst();
|
||||
if (!cur) notFound("规则组");
|
||||
const refs = await referenceCount(trx, cur!.id);
|
||||
if (refs > 0) conflict(`规则组被 ${refs} 个版本目标或订阅引用,先解除引用`);
|
||||
if (refs > 0) conflict(`规则组被 ${refs} 个版本目标或订阅引用,先解除引用`);
|
||||
const row = await trx
|
||||
.updateTable("rule_group")
|
||||
.set({
|
||||
|
||||
@@ -103,7 +103,7 @@ async function assertNoDuplicate(
|
||||
q = input.seasonUnitId ? q.where("season_unit_id", "=", input.seasonUnitId) : q.where("season_unit_id", "is", null);
|
||||
if (input.excludeId) q = q.where("id", "!=", input.excludeId);
|
||||
const hit = await q.executeTakeFirst();
|
||||
if (hit) conflict(`该作品同一季已有订阅:${hit.id}`);
|
||||
if (hit) conflict(`该作品同一季已有订阅:${hit.id}`);
|
||||
}
|
||||
|
||||
/** 没有收录策略模板、表单给了质量预设时,新订阅按这个模板建一个版本目标(预设随后写入) */
|
||||
@@ -497,12 +497,12 @@ export function registerSubscriptionRoutes(app: ModuleApp, d: AoDeps) {
|
||||
const pause = async (sub: SubscriptionRow) => {
|
||||
if (sub.status === "paused") return sub;
|
||||
if (!(TRACKING_SUB_STATUSES as readonly string[]).includes(sub.status))
|
||||
conflict(`订阅状态为 ${sub.status},不能暂停`);
|
||||
conflict(`订阅状态为 ${sub.status},不能暂停`);
|
||||
return setStatus(sub, "paused", "user_paused");
|
||||
};
|
||||
const resume = async (sub: SubscriptionRow) => {
|
||||
if ((TRACKING_SUB_STATUSES as readonly string[]).includes(sub.status)) return sub;
|
||||
if (sub.status !== "paused") conflict(`订阅状态为 ${sub.status},已完成/已取消的订阅用"重新订阅"`);
|
||||
if (sub.status !== "paused") conflict(`订阅状态为 ${sub.status},已完成/已取消的订阅用"重新订阅"`);
|
||||
const ready = await tmdbReady(db(), sub.media_entity_id, sub.media_kind);
|
||||
const r = await setStatus(sub, ready ? "active" : "pending", ready ? "resumed" : "metadata_pending", {
|
||||
nextSearchNow: true,
|
||||
@@ -511,11 +511,11 @@ export function registerSubscriptionRoutes(app: ModuleApp, d: AoDeps) {
|
||||
return r;
|
||||
};
|
||||
const search = async (sub: SubscriptionRow) => {
|
||||
if (!sub.media_entity_id) throw new ApiException("bad_request", "订阅未关联作品,无法搜索");
|
||||
if (!sub.media_entity_id) throw new ApiException("bad_request", "订阅未关联作品,无法搜索");
|
||||
try {
|
||||
return await enqueueSubscriptionSearch(d, sub.id, "manual");
|
||||
} catch (err) {
|
||||
throw new ApiException("upstream_unavailable", `搜索任务入队失败:${errMessage(err)}`);
|
||||
throw new ApiException("upstream_unavailable", `搜索任务入队失败:${errMessage(err)}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -623,7 +623,7 @@ export function registerSubscriptionRoutes(app: ModuleApp, d: AoDeps) {
|
||||
async (_c, { params, body }) => {
|
||||
const sub = await load(params.id);
|
||||
if (!(TRACKING_SUB_STATUSES as readonly string[]).includes(sub.status))
|
||||
conflict("订阅不在追踪中(暂停/已完成/已取消),先恢复或重新订阅");
|
||||
conflict("订阅不在追踪中(暂停/已完成/已取消),先恢复或重新订阅");
|
||||
const slots = sub.media_entity_id
|
||||
? await db()
|
||||
.selectFrom("acquisition_slot")
|
||||
@@ -641,7 +641,7 @@ export function registerSubscriptionRoutes(app: ModuleApp, d: AoDeps) {
|
||||
{ idempotencyKey: `fill:${params.slotId}:${d.ports.now().getTime()}`, traceId: params.slotId },
|
||||
);
|
||||
} catch (err) {
|
||||
throw new ApiException("upstream_unavailable", `补全任务入队失败:${errMessage(err)}`);
|
||||
throw new ApiException("upstream_unavailable", `补全任务入队失败:${errMessage(err)}`);
|
||||
}
|
||||
},
|
||||
{ tag: TAG, status: 202 },
|
||||
|
||||
@@ -801,7 +801,7 @@ async function evaluateForSlot(c: Ctx, p: SlotInput): Promise<{ planId: string |
|
||||
}).then(
|
||||
({ taskId }) => db.updateTable("acquisition_plan").set({ task_id: taskId }).where("id", "=", planId).execute(),
|
||||
(err) =>
|
||||
d.ctx.logger.warn({ err: { message: (err as Error).message }, planId }, "submitDownload 入队失败,留给巡检补投"),
|
||||
d.ctx.logger.warn({ err: { message: (err as Error).message }, planId }, "submitDownload 入队失败,留给巡检补投"),
|
||||
);
|
||||
return { planId, deferred: false };
|
||||
}
|
||||
@@ -932,7 +932,7 @@ async function gates(
|
||||
reasons: [b.decision === "unknown" ? "budget_unknown_size" : "budget_exceeded"],
|
||||
rejected: b.violations.map((v) => ({
|
||||
code: `budget.${v.kind}`,
|
||||
message: `${v.kind}: 上限 ${v.limit},当前 ${v.current},请求 ${v.requested}`,
|
||||
message: `${v.kind}:上限 ${v.limit},当前 ${v.current},请求 ${v.requested}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -949,7 +949,7 @@ async function gates(
|
||||
rejected: [
|
||||
{
|
||||
code: `cache.${dv.decision}`,
|
||||
message: `缓存卷 ${vol.volumeKey} 使用率 ${dv.usedPct?.toFixed(1) ?? "未知"}%,预测 ${dv.projectedPct?.toFixed(1) ?? "未知"}%`,
|
||||
message: `缓存卷 ${vol.volumeKey} 使用率 ${dv.usedPct?.toFixed(1) ?? "未知"}%,预测 ${dv.projectedPct?.toFixed(1) ?? "未知"}%`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -151,7 +151,7 @@ export async function applySlotEvent(
|
||||
return false;
|
||||
}
|
||||
if (!next) {
|
||||
d.ctx.logger.warn({ slotId, event: event.type, from: slot.status }, "Slot 状态迁移被拒绝(已被人工处理?)");
|
||||
d.ctx.logger.warn({ slotId, event: event.type, from: slot.status }, "Slot 状态迁移被拒绝(已被人工处理?)");
|
||||
return false;
|
||||
}
|
||||
const ended = PLAN_EVENTS.has(event.type) && stageAfter(event.type) === null;
|
||||
@@ -288,7 +288,7 @@ export async function submitDownload(d: AoDeps, planId: string, opts: { runId?:
|
||||
return { kind: "already_exists", ...view(out.existing), tags: [...new Set([...out.existing.tags, tag])] };
|
||||
}
|
||||
case "accepted_unconfirmed":
|
||||
if (!out.hash) throw new Error("qB 已收单但未能确认任务(无 hash),稍后按探针标签对账");
|
||||
if (!out.hash) throw new Error("qB 已收单但未能确认任务(无 hash),稍后按探针标签对账");
|
||||
return {
|
||||
kind: "accepted_unconfirmed",
|
||||
hash: out.hash,
|
||||
@@ -351,7 +351,7 @@ export async function submitDownload(d: AoDeps, planId: string, opts: { runId?:
|
||||
// 季包/多集包:按缺集选文件(接管的已有任务不改文件选择);失败不影响提交结果
|
||||
if (result.kind === "added" && plan.unit_keys.length > 0 && plan.file_selection === null) {
|
||||
await selectPackFiles(d, client, hash, plan, rel).catch((err) =>
|
||||
d.ctx.logger.warn({ planId: plan.id, err: { message: errMessage(err) } }, "季包选文件失败,按整包下载"),
|
||||
d.ctx.logger.warn({ planId: plan.id, err: { message: errMessage(err) } }, "季包选文件失败,按整包下载"),
|
||||
);
|
||||
}
|
||||
if (!dtId) {
|
||||
@@ -754,7 +754,7 @@ async function observeLocked(
|
||||
// 无进展:只记证据 + 待处理,保留原任务,不换源(REQ §8.4.2)
|
||||
await upsertDecision(d, trx, {
|
||||
kind: "download_stalled",
|
||||
title: `下载无进展:${t.name}`,
|
||||
title: `下载无进展:${t.name}`,
|
||||
summary: `已 ${Math.round((now.getTime() - (noProgressSince?.getTime() ?? now.getTime())) / 60000)} 分钟没有新增字节`,
|
||||
subjectType: "download_task",
|
||||
subjectId: row.id,
|
||||
@@ -771,7 +771,7 @@ async function observeLocked(
|
||||
if ((merged.status === "failed" || merged.status === "missing") && changed) {
|
||||
await upsertDecision(d, trx, {
|
||||
kind: "download_failed",
|
||||
title: `下载异常:${t.name}`,
|
||||
title: `下载异常:${t.name}`,
|
||||
summary: `qB 状态 ${t.rawState}`,
|
||||
subjectType: "download_task",
|
||||
subjectId: row.id,
|
||||
@@ -1133,7 +1133,7 @@ export async function cleanupDownloadCache(
|
||||
kind: "space_insufficient",
|
||||
severity: "critical",
|
||||
title: `下载缓存卷 ${v.volumeKey} 空间不足`,
|
||||
summary: `使用率 ${pct.toFixed(1)}%,可清理任务已用尽;新下载已暂停`,
|
||||
summary: `使用率 ${pct.toFixed(1)}%,可清理任务已用尽;新下载已暂停`,
|
||||
subjectType: "cache_volume",
|
||||
dedupeKey: `space_insufficient:${v.volumeKey}`,
|
||||
evidence: { usedPercent: pct, locked: entry.skipped },
|
||||
@@ -1146,7 +1146,7 @@ export async function cleanupDownloadCache(
|
||||
/** qB 删种 + 删数据:必经 assertSafeToDeleteData(本机视图与下载器视图各查一次) */
|
||||
async function removeWithData(d: AoDeps, cand: CleanupCandidate, libraryRootsLocal: string[]): Promise<boolean> {
|
||||
const { row, topo } = cand;
|
||||
if (!row.content_path) throw new Error("content_path 未知,不能删数据");
|
||||
if (!row.content_path) throw new Error("content_path 未知,不能删数据");
|
||||
const localContent = hostPathOf(topo, row.content_path);
|
||||
if (!localContent) throw new Error("content_path 无法映射到本机路径");
|
||||
assertSafeToDeleteData(
|
||||
|
||||
@@ -217,14 +217,14 @@ export function ruleFingerprint(g: {
|
||||
}
|
||||
|
||||
const fmtErrors = (errs: readonly RuleParseError[]) =>
|
||||
errs.map((e) => `${e.message}(第 ${e.layer + 1} 层,位置 ${e.position})`).join(";");
|
||||
errs.map((e) => `${e.message}(第 ${e.layer + 1} 层,位置 ${e.position})`).join(";");
|
||||
|
||||
/** 表达式里的原子都要存在(内置且支持,或自定义) */
|
||||
function checkAtoms(e: RuleExpr, custom: ReadonlySet<string>): string[] {
|
||||
const bad: string[] = [];
|
||||
for (const id of atomsOf(e)) {
|
||||
const b = BUILTIN_ATOM_MAP.get(id);
|
||||
if (b ? !b.supported : !custom.has(id)) bad.push(b ? `${id}(不支持)` : `${id}(不存在)`);
|
||||
if (b ? !b.supported : !custom.has(id)) bad.push(b ? `${id}(不支持)` : `${id}(不存在)`);
|
||||
}
|
||||
return bad;
|
||||
}
|
||||
@@ -240,7 +240,7 @@ export function normalizeGroupInput(
|
||||
if (body.ruleString.trim() === "") out.layers = [];
|
||||
else {
|
||||
const p = parseRuleString(body.ruleString, { customAtoms: custom });
|
||||
if (p.errors.length) throw new ApiException("validation_failed", `规则串解析失败:${fmtErrors(p.errors)}`);
|
||||
if (p.errors.length) throw new ApiException("validation_failed", `规则串解析失败:${fmtErrors(p.errors)}`);
|
||||
out.layers = p.layers;
|
||||
}
|
||||
} else if (body.layers !== undefined) {
|
||||
@@ -252,16 +252,16 @@ export function normalizeGroupInput(
|
||||
if (body.hardExcludes !== undefined) {
|
||||
out.hardExcludes = body.hardExcludes.map((x: string | ApiRuleExpr, i: number) => {
|
||||
if (typeof x !== "string") return normalizeExpr(x as RuleExpr);
|
||||
if (x.includes(">")) throw new ApiException("validation_failed", `硬排除第 ${i + 1} 条不能分层(去掉 >)`);
|
||||
if (x.includes(">")) throw new ApiException("validation_failed", `硬排除第 ${i + 1} 条不能分层(去掉 >)`);
|
||||
const p = parseExpr(x, { customAtoms: custom });
|
||||
if (!p.expr) throw new ApiException("validation_failed", `硬排除第 ${i + 1} 条解析失败:${fmtErrors(p.errors)}`);
|
||||
if (!p.expr) throw new ApiException("validation_failed", `硬排除第 ${i + 1} 条解析失败:${fmtErrors(p.errors)}`);
|
||||
return p.expr;
|
||||
});
|
||||
}
|
||||
const bad = [...(out.layers ?? []).map((l) => l.expr), ...(out.hardExcludes ?? [])].flatMap((e) =>
|
||||
checkAtoms(e, custom),
|
||||
);
|
||||
if (bad.length) throw new ApiException("validation_failed", `条件无效:${[...new Set(bad)].join("、")}`);
|
||||
if (bad.length) throw new ApiException("validation_failed", `条件无效:${[...new Set(bad)].join("、")}`);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ async function idsByKey(
|
||||
.execute();
|
||||
return values.map((v) => {
|
||||
const hit = rows.find((r) => r.id === v || r.k === v);
|
||||
if (!hit) bad(`${label}不存在:${v}`);
|
||||
if (!hit) bad(`${label}不存在:${v}`);
|
||||
return hit!.id;
|
||||
});
|
||||
}
|
||||
@@ -454,14 +454,14 @@ export async function normalizeSubscriptionParams(
|
||||
const parsed = SubscriptionParams.safeParse(src);
|
||||
if (!parsed.success) {
|
||||
const i = parsed.error.issues[0];
|
||||
bad(`params.${i?.path.join(".") ?? ""} 无效:${i?.message ?? ""}`);
|
||||
bad(`params.${i?.path.join(".") ?? ""} 无效:${i?.message ?? ""}`);
|
||||
}
|
||||
const p = parsed.data as SubscriptionParams;
|
||||
|
||||
// ── 库根存在性与正则白名单 ──
|
||||
if (p.libraryRootId) {
|
||||
const r = await exec.selectFrom("library_root").select("id").where("id", "=", p.libraryRootId).executeTakeFirst();
|
||||
if (!r) bad(`库根不存在:${p.libraryRootId}`);
|
||||
if (!r) bad(`库根不存在:${p.libraryRootId}`);
|
||||
}
|
||||
for (const k of ["include", "exclude"] as const)
|
||||
for (const pat of p[k] ?? []) {
|
||||
@@ -470,7 +470,7 @@ export async function normalizeSubscriptionParams(
|
||||
try {
|
||||
new RegExp(pat, "i");
|
||||
} catch {
|
||||
bad(`${k} 正则语法错误:${pat}`);
|
||||
bad(`${k} 正则语法错误:${pat}`);
|
||||
}
|
||||
}
|
||||
if (p.stopAfter && Number.isNaN(Date.parse(p.stopAfter))) bad("stopAfter 不是有效时间");
|
||||
|
||||
@@ -474,7 +474,7 @@ export async function afterSlotVerified(
|
||||
kind: "other",
|
||||
severity: "info",
|
||||
title: "疑似缺集",
|
||||
summary: `已入库最高集之前缺:${desc}。总集数未知,订阅继续追踪`.slice(0, 500),
|
||||
summary: `已入库最高集之前缺:${desc}。总集数未知,订阅继续追踪`.slice(0, 500),
|
||||
subjectType: "acquisition_slot",
|
||||
subjectId: slot.id,
|
||||
dedupeKey: `suspected_missing:${slot.id}`,
|
||||
@@ -549,7 +549,7 @@ export async function completeSubscription(
|
||||
completed_at: d.ports.now(),
|
||||
})
|
||||
.execute();
|
||||
d.ctx.logger.info({ subscriptionId: sub.id, reason }, "订阅完成,已写历史");
|
||||
d.ctx.logger.info({ subscriptionId: sub.id, reason }, "订阅完成,已写历史");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user