From 52402864d6abffbc7846d6d2476dba47b95a4c20 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 28 Sep 2026 20:09:29 +0800 Subject: [PATCH] style(copy): punctuation rules and site-wide fix with a CI check (QA U11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/server/src/config.ts | 2 +- apps/server/src/context.ts | 2 +- apps/server/src/http/routes/health.ts | 2 +- apps/server/src/http/sse-pg.ts | 2 +- apps/server/src/http/sse.ts | 2 +- apps/server/src/main.ts | 4 +- .../__tests__/organize-fs.test.ts | 2 +- .../acquisition-organize/capabilities.ts | 15 +- .../src/modules/acquisition-organize/index.ts | 12 +- .../src/modules/acquisition-organize/ports.ts | 2 +- .../acquisition-organize/routes/dashboard.ts | 6 +- .../acquisition-organize/routes/downloads.ts | 6 +- .../routes/organization.ts | 2 +- .../acquisition-organize/routes/rules.ts | 10 +- .../services/organize-guard.ts | 20 +- .../services/organize-transfer.ts | 6 +- .../acquisition-organize/services/organize.ts | 42 +-- .../server/src/modules/file-manager/common.ts | 18 +- .../src/modules/file-manager/download.ts | 8 +- .../server/src/modules/file-manager/mutate.ts | 18 +- .../src/modules/file-manager/organize.ts | 28 +- apps/server/src/modules/file-manager/probe.ts | 4 +- .../src/modules/file-manager/transfer.ts | 36 +- apps/server/src/modules/file-manager/trash.ts | 26 +- apps/server/src/modules/file-manager/tree.ts | 2 +- apps/server/src/modules/pi/index.ts | 2 +- apps/server/src/modules/pi/refresh.ts | 2 +- apps/server/src/modules/pi/routes.ts | 4 +- apps/server/src/modules/pi/service.ts | 8 +- .../src/modules/schedules/agent-checks.ts | 24 +- apps/server/src/modules/schedules/cron.ts | 8 +- apps/server/src/modules/schedules/index.ts | 2 +- apps/server/src/modules/schedules/registry.ts | 2 +- .../server/src/modules/schedules/scheduler.ts | 10 +- apps/server/src/modules/schedules/store.ts | 10 +- apps/server/src/modules/search/dispatch.ts | 2 +- apps/server/src/modules/search/routes.ts | 2 +- .../src/modules/sources-identity/defs.ts | 24 +- .../src/modules/sources-identity/index.ts | 6 +- .../sources-identity/routes/decisions.ts | 2 +- .../sources-identity/routes/identity.ts | 12 +- .../sources-identity/routes/sources.ts | 22 +- .../sources-identity/routes/workbench.ts | 4 +- .../src/modules/sources-identity/runtime.ts | 2 +- .../service/entity-from-candidate.ts | 4 +- .../modules/sources-identity/service/feeds.ts | 6 +- .../service/identity-admin.ts | 12 +- .../sources-identity/service/resolve.ts | 10 +- .../sources-identity/service/snapshot.ts | 2 +- .../sources-identity/service/workbench.ts | 4 +- apps/server/src/preflight.ts | 10 +- apps/server/test/file-manager/w5a2.test.ts | 2 +- apps/web/app/api/feeds.ts | 4 +- apps/web/app/api/parse.ts | 2 +- apps/web/app/api/tasks.ts | 14 +- apps/web/app/components/agentui/file-diff.tsx | 4 +- .../components/agentui/message-scroller.tsx | 2 +- .../app/components/agentui/tool-approval.tsx | 4 +- .../web/app/components/console/inbox-bell.tsx | 2 +- apps/web/app/components/console/nav.ts | 8 +- .../components/downloads/business-tasks.tsx | 2 +- .../downloads/business-timeline.tsx | 2 +- .../components/downloads/cache-volumes.tsx | 8 +- .../components/downloads/download-detail.tsx | 8 +- apps/web/app/components/files/dialogs.tsx | 64 ++-- apps/web/app/components/files/file-detail.tsx | 8 +- apps/web/app/components/files/file-list.tsx | 2 +- .../app/components/files/shortcuts-dialog.tsx | 6 +- .../app/components/library/entity-dialogs.tsx | 12 +- .../components/library/identity-aliases.tsx | 6 +- .../library/identity-entity-picker.tsx | 2 +- .../library/identity-pool-sheet.tsx | 4 +- .../app/components/library/identity-views.tsx | 4 +- apps/web/app/components/library/labels.ts | 8 +- .../components/library/preferences-form.tsx | 10 +- apps/web/app/components/library/slot-card.tsx | 2 +- .../web/app/components/library/slot-rules.tsx | 32 +- .../naming/entity-naming-dialog.tsx | 4 +- apps/web/app/components/naming/model.ts | 2 +- .../app/components/naming/template-editor.tsx | 20 +- .../organize-wizard/entity-picker.tsx | 2 +- .../app/components/organize-wizard/model.ts | 2 +- .../components/organize-wizard/step-files.tsx | 6 +- .../organize-wizard/step-options.tsx | 26 +- .../organize-wizard/step-preview.tsx | 14 +- .../web/app/components/rules/group-editor.tsx | 18 +- apps/web/app/components/rules/group-list.tsx | 16 +- apps/web/app/components/rules/labels.ts | 4 +- .../app/components/rules/ladder-editor.tsx | 12 +- .../app/components/rules/preview-panel.tsx | 4 +- apps/web/app/components/rules/tabs.tsx | 31 +- .../schedules/agent-check-dialog.tsx | 12 +- .../components/schedules/interval-editor.tsx | 2 +- .../app/components/schedules/runs-sheet.tsx | 2 +- .../components/schedules/schedule-board.tsx | 2 +- .../components/schedules/schedule-card.tsx | 10 +- .../app/components/search/command-palette.tsx | 8 +- .../app/components/search/release-results.tsx | 8 +- .../search/search-download-dialog.tsx | 18 +- .../app/components/search/search-sections.tsx | 4 +- apps/web/app/components/settings/api-keys.tsx | 14 +- .../app/components/settings/automation.tsx | 12 +- .../web/app/components/settings/instances.tsx | 17 +- apps/web/app/components/settings/library.tsx | 6 +- apps/web/app/components/settings/metadata.tsx | 4 +- apps/web/app/components/settings/naming.tsx | 10 +- .../app/components/settings/path-mappings.tsx | 10 +- .../web/app/components/settings/pi-shared.tsx | 2 +- apps/web/app/components/settings/pi-test.tsx | 10 +- .../app/components/settings/pi-upstreams.tsx | 14 +- .../web/app/components/settings/pi-wizard.tsx | 18 +- .../components/settings/resource-section.tsx | 2 +- apps/web/app/components/tasks/task-detail.tsx | 17 +- apps/web/app/components/tasks/task-stream.tsx | 3 +- .../components/workbench/download-dialog.tsx | 16 +- .../app/components/workbench/feed-form.tsx | 10 +- .../app/components/workbench/feed-table.tsx | 4 +- .../workbench/release-detail-sheet.tsx | 18 +- .../components/workbench/release-fields.ts | 2 +- .../app/components/workbench/release-list.tsx | 4 +- .../components/workbench/snapshot-panel.tsx | 4 +- apps/web/app/lib/form-errors.ts | 2 +- apps/web/app/lib/labels.ts | 4 +- apps/web/app/routes/console/audit.tsx | 4 +- .../app/routes/console/downloads.plans.tsx | 2 +- apps/web/app/routes/console/downloads.tsx | 2 +- apps/web/app/routes/console/files.tsx | 24 +- apps/web/app/routes/console/home.tsx | 8 +- apps/web/app/routes/console/inbox.tsx | 16 +- apps/web/app/routes/console/library.$id.tsx | 16 +- apps/web/app/routes/console/library.tsx | 4 +- apps/web/app/routes/console/organize.$id.tsx | 10 +- apps/web/app/routes/console/organize.tsx | 4 +- apps/web/app/routes/console/status.tsx | 8 +- apps/web/app/routes/console/tasks.tsx | 2 +- apps/web/app/routes/not-found.tsx | 2 +- deno.json | 6 +- deno.lock | 1 + docs/design/glossary.md | 15 + docs/plan/09-visual-qa.md | 7 + packages/contracts/src/schedules.ts | 2 +- packages/contracts/src/settings.ts | 2 +- scripts/check-copy-punctuation.mjs | 331 ++++++++++++++++++ 143 files changed, 985 insertions(+), 606 deletions(-) create mode 100644 scripts/check-copy-punctuation.mjs diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 814f64c9..e92b140b 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -63,7 +63,7 @@ export function loadConfig(env: Record = Deno.env.to const parsed = ConfigSchema.safeParse(env); if (!parsed.success) { const lines = parsed.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`); - throw new Error(`配置无效:\n${lines.join("\n")}`); + throw new Error(`配置无效:\n${lines.join("\n")}`); } return parsed.data; } diff --git a/apps/server/src/context.ts b/apps/server/src/context.ts index c91c4f62..ef49ce1a 100644 --- a/apps/server/src/context.ts +++ b/apps/server/src/context.ts @@ -31,7 +31,7 @@ export class DeferredTaskEngine implements TaskEngine { } private get e(): TaskEngine { - if (!this.inner) throw new Error("任务引擎尚未就绪(模块不要在 register* 阶段调用引擎)"); + if (!this.inner) throw new Error("任务引擎尚未就绪(模块不要在 register* 阶段调用引擎)"); return this.inner; } diff --git a/apps/server/src/http/routes/health.ts b/apps/server/src/http/routes/health.ts index 55150353..f7762508 100644 --- a/apps/server/src/http/routes/health.ts +++ b/apps/server/src/http/routes/health.ts @@ -13,7 +13,7 @@ const route = createRoute({ method: "get", path: "/health", tags: ["system"], - summary: "服务健康状态(免鉴权)", + summary: "服务健康状态(免鉴权)", responses: { 200: { description: "服务状态", diff --git a/apps/server/src/http/sse-pg.ts b/apps/server/src/http/sse-pg.ts index 410d43f8..1a0a9357 100644 --- a/apps/server/src/http/sse-pg.ts +++ b/apps/server/src/http/sse-pg.ts @@ -89,7 +89,7 @@ export class PgEventBus implements EventBusPort { client.removeAllListeners("notification"); client.end().catch(() => {}); if (!this.stopped) { - this.log.warn({ err }, "SSE 总线 LISTEN 连接断开,准备重连"); + this.log.warn({ err }, "SSE 总线 LISTEN 连接断开,准备重连"); this.reconnect(500); } }; diff --git a/apps/server/src/http/sse.ts b/apps/server/src/http/sse.ts index db02d32c..92e84f78 100644 --- a/apps/server/src/http/sse.ts +++ b/apps/server/src/http/sse.ts @@ -110,7 +110,7 @@ export function sseRoutes({ bus, keepaliveMs = 15_000 }: SseOptions) { summary: def.summary, responses: { 200: { - description: "JSON 补读页(带 limit 或 Accept: application/json);否则为 text/event-stream", + description: "JSON 补读页(带 limit 或 Accept: application/json);否则为 text/event-stream", content: { "application/json": { schema: apiResponse(def.response) }, "text/event-stream": { schema: { type: "string" } }, diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 0cab982e..7b0520f6 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -124,14 +124,14 @@ async function runOneShot(r: OneShotRole): Promise { const storage = await checkStorage({ ...paths, subdirs: DATA_SUBDIRS, mounts }); problems.push(...storage.problems); for (const w of storage.warnings) { - logger.warn({ kind: w.kind, id: w.id, path: w.path }, `挂载路径不可用: ${w.name} ${w.reason}`); + logger.warn({ kind: w.kind, id: w.id, path: w.path }, `挂载路径不可用:${w.name} ${w.reason}`); } await auditStorageWarnings(db, storage.warnings).catch((err) => logger.warn({ err }, "存储告警写审计失败")); // 验收开关误设提示(验收 07 S-10):只有 UMT_FS_TEST_HOOKS=1 时才真正生效 if (Deno.env.get("UMT_FS_TEST_HANG_AFTER_BYTES")) logger.warn( { active: Deno.env.get("UMT_FS_TEST_HOOKS") === "1" }, - "设置了 UMT_FS_TEST_HANG_AFTER_BYTES(fsproxy 挂死验收开关);生产环境不要设置,生效时所有数据类文件操作都会卡死到被 SIGKILL", + "设置了 UMT_FS_TEST_HANG_AFTER_BYTES(fsproxy 挂死验收开关);生产环境不要设置,生效时所有数据类文件操作都会卡死到被 SIGKILL", ); for (const p of problems) logger.error({ check: p.check }, p.message); if (problems.length) return 1; diff --git a/apps/server/src/modules/acquisition-organize/__tests__/organize-fs.test.ts b/apps/server/src/modules/acquisition-organize/__tests__/organize-fs.test.ts index f9a17c78..54d05575 100644 --- a/apps/server/src/modules/acquisition-organize/__tests__/organize-fs.test.ts +++ b/apps/server/src/modules/acquisition-organize/__tests__/organize-fs.test.ts @@ -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"]) diff --git a/apps/server/src/modules/acquisition-organize/capabilities.ts b/apps/server/src/modules/acquisition-organize/capabilities.ts index b6ed70ea..ce03f5e5 100644 --- a/apps/server/src/modules/acquisition-organize/capabilities.ts +++ b/apps/server/src/modules/acquisition-organize/capabilities.ts @@ -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 }; diff --git a/apps/server/src/modules/acquisition-organize/index.ts b/apps/server/src/modules/acquisition-organize/index.ts index 65f45a4b..7ec51daf 100644 --- a/apps/server/src/modules/acquisition-organize/index.ts +++ b/apps/server/src/modules/acquisition-organize/index.ts @@ -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), }, diff --git a/apps/server/src/modules/acquisition-organize/ports.ts b/apps/server/src/modules/acquisition-organize/ports.ts index 784fd701..f74f39d9 100644 --- a/apps/server/src/modules/acquisition-organize/ports.ts +++ b/apps/server/src/modules/acquisition-organize/ports.ts @@ -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; const siteMeta = (raw._site ?? {}) as Record; const defId = [str(siteMeta.adapterSiteId), site.key].find((k) => k && getSiteDefinition(k)); diff --git a/apps/server/src/modules/acquisition-organize/routes/dashboard.ts b/apps/server/src/modules/acquisition-organize/routes/dashboard.ts index 0180111c..0c8eb4ad 100644 --- a/apps/server/src/modules/acquisition-organize/routes/dashboard.ts +++ b/apps/server/src/modules/acquisition-organize/routes/dashboard.ts @@ -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, diff --git a/apps/server/src/modules/acquisition-organize/routes/downloads.ts b/apps/server/src/modules/acquisition-organize/routes/downloads.ts index cae96db1..321fbcc1 100644 --- a/apps/server/src/modules/acquisition-organize/routes/downloads.ts +++ b/apps/server/src/modules/acquisition-organize/routes/downloads.ts @@ -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(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, }, diff --git a/apps/server/src/modules/acquisition-organize/routes/organization.ts b/apps/server/src/modules/acquisition-organize/routes/organization.ts index 546fc753..e1269b01 100644 --- a/apps/server/src/modules/acquisition-organize/routes/organization.ts +++ b/apps/server/src/modules/acquisition-organize/routes/organization.ts @@ -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}`, ); } diff --git a/apps/server/src/modules/acquisition-organize/routes/rules.ts b/apps/server/src/modules/acquisition-organize/routes/rules.ts index a14384c2..dab2b3b5 100644 --- a/apps/server/src/modules/acquisition-organize/routes/rules.ts +++ b/apps/server/src/modules/acquisition-organize/routes/rules.ts @@ -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(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"); }); diff --git a/apps/server/src/modules/acquisition-organize/services/organize-guard.ts b/apps/server/src/modules/acquisition-organize/services/organize-guard.ts index d143b5da..0b24b109 100644 --- a/apps/server/src/modules/acquisition-organize/services/organize-guard.ts +++ b/apps/server/src/modules/acquisition-organize/services/organize-guard.ts @@ -58,16 +58,16 @@ export async function sourceGuard(exec: Exec): Promise { 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 }; diff --git a/apps/server/src/modules/acquisition-organize/services/organize-transfer.ts b/apps/server/src/modules/acquisition-organize/services/organize-transfer.ts index a0c40bd3..cd5c9318 100644 --- a/apps/server/src/modules/acquisition-organize/services/organize-transfer.ts +++ b/apps/server/src/modules/acquisition-organize/services/organize-transfer.ts @@ -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(); } diff --git a/apps/server/src/modules/acquisition-organize/services/organize.ts b/apps/server/src/modules/acquisition-organize/services/organize.ts index 22575c67..b00c2db3 100644 --- a/apps/server/src/modules/acquisition-organize/services/organize.ts +++ b/apps/server/src/modules/acquisition-organize/services/organize.ts @@ -178,7 +178,7 @@ async function walk( guard?: ((p: string) => Promise) | undefined, ): Promise { 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 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 { export async function rootDef(exec: Exec, id: string): Promise { const r = (await loadRootDefs(exec)).find((x) => x.id === id); - if (!r) throw new FilesError("not_found", `根不存在或未启用: ${id}`); + if (!r) throw new FilesError("not_found", `根不存在或未启用:${id}`); return r; } @@ -261,7 +261,7 @@ export interface Resolved { } function checkRel(rel: string) { - if (rel.startsWith("/") || rel.includes("\0")) throw new FilesError("path_escape", `非法相对路径: ${rel}`); + if (rel.startsWith("/") || rel.includes("\0")) throw new FilesError("path_escape", `非法相对路径:${rel}`); const segs = rel.split("/").filter((s) => s.length > 0); if (segs.some((s) => s === ".." || s === ".")) throw new FilesError("path_escape", `路径不允许包含 . 或 ..: ${rel}`); return segs.join("/"); @@ -290,9 +290,9 @@ export async function resolveIn( parentReal = await realpath(dirname(joined)); } catch (e) { if (isOfflineErr(e)) throw new FilesError("root_unavailable", `根 ${def.name} 读取失败(${errnoOf(e)})`); - throw new FilesError("not_found", `父目录不存在: ${dirname(rel) || "/"}`); + throw new FilesError("not_found", `父目录不存在:${dirname(rel) || "/"}`); } - if (!within(parentReal, rootReal)) throw new FilesError("path_escape", `路径经符号链接指向根外: ${rel}`); + if (!within(parentReal, rootReal)) throw new FilesError("path_escape", `路径经符号链接指向根外:${rel}`); const abs = join(parentReal, basename(joined)); let lst: Awaited> | null = null; try { @@ -301,12 +301,12 @@ export async function resolveIn( if (isOfflineErr(e)) throw new FilesError("root_unavailable", `根 ${def.name} 读取失败(${errnoOf(e)})`); if (errnoOf(e) !== "ENOENT") throw new FilesError("bad_request", `无法访问 ${rel}: ${errnoOf(e)}`); } - if (!lst && opts.mustExist !== false) throw new FilesError("not_found", `不存在: ${rel}`); + if (!lst && opts.mustExist !== false) throw new FilesError("not_found", `不存在:${rel}`); const isSymlink = !!lst?.isSymbolicLink(); if (isSymlink) { const target = await realpath(abs).catch(() => null); if (!target || !within(target, rootReal)) - throw new FilesError("path_escape", `符号链接指向根外或无法解析,不能操作: ${rel}`); + throw new FilesError("path_escape", `符号链接指向根外或无法解析,不能操作:${rel}`); } return { def, rootReal, rel: relOf(rootReal, abs), abs, lst, isSymlink }; } @@ -318,9 +318,9 @@ export async function recheck(abs: string, rootReal: string): Promise { r = await realpath(abs); } catch (e) { if (isOfflineErr(e)) throw new FilesError("root_unavailable", `读取失败(${errnoOf(e)})`); - throw new FilesError("plan_stale", `源已不存在: ${abs}`); + throw new FilesError("plan_stale", `源已不存在:${abs}`); } - if (!within(r, rootReal)) throw new FilesError("path_escape", `执行时路径指向根外: ${abs}`); + if (!within(r, rootReal)) throw new FilesError("path_escape", `执行时路径指向根外:${abs}`); return r; } diff --git a/apps/server/src/modules/file-manager/download.ts b/apps/server/src/modules/file-manager/download.ts index 940141b2..8834f8de 100644 --- a/apps/server/src/modules/file-manager/download.ts +++ b/apps/server/src/modules/file-manager/download.ts @@ -43,12 +43,12 @@ export async function downloadFile( ): Promise { // 路由中间件已按 auth:"session" 拦截;这里同口径复核(纵深防御) if (!isAdminSession(principal)) - throw new FilesError("forbidden", "下载仅限控制台管理员会话,API Key / DEV key 不可用"); + throw new FilesError("forbidden", "下载仅限控制台管理员会话,API Key / DEV key 不可用"); const r = await resolveIn(d, q.root, q.path); - if (r.rel === "" || r.lst?.isDirectory()) throw new FilesError("bad_request", "只能下载文件,不能下载目录"); + if (r.rel === "" || r.lst?.isDirectory()) throw new FilesError("bad_request", "只能下载文件,不能下载目录"); const name = basename(r.abs); const kind = kindOfName(name); - if (!ALLOWED.has(kind)) throw new FilesError("forbidden", `只能下载 NFO、字幕和图片,此文件类型为 ${kind}`); + if (!ALLOWED.has(kind)) throw new FilesError("forbidden", `只能下载 NFO、字幕和图片,此文件类型为 ${kind}`); const real = await recheck(r.abs, r.rootReal); const file = await Deno.open(real, { read: true }); let size: number; @@ -59,7 +59,7 @@ export async function downloadFile( if (size > d.options.downloadMaxBytes) throw new FilesError( "too_large", - `文件 ${size} 字节,超过下载上限 ${d.options.downloadMaxBytes}(FILES_DOWNLOAD_MAX_BYTES)`, + `文件 ${size} 字节,超过下载上限 ${d.options.downloadMaxBytes}(FILES_DOWNLOAD_MAX_BYTES)`, ); } catch (e) { file.close(); diff --git a/apps/server/src/modules/file-manager/mutate.ts b/apps/server/src/modules/file-manager/mutate.ts index a2c6c621..6b1733ba 100644 --- a/apps/server/src/modules/file-manager/mutate.ts +++ b/apps/server/src/modules/file-manager/mutate.ts @@ -16,7 +16,7 @@ const parentRel = (rel: string) => (rel.includes("/") ? dirname(rel) : ""); function assertCleanName(name: string) { if (sanitizeSegment(name) !== name) - throw new FilesError("bad_request", `文件名含非法字符或首尾有空白/点,可改为:${sanitizeSegment(name)}`); + throw new FilesError("bad_request", `文件名含非法字符或首尾有空白/点,可改为:${sanitizeSegment(name)}`); } export async function mkdirIn( @@ -26,15 +26,15 @@ export async function mkdirIn( ): Promise { const r = await resolveIn(d, body.root, body.path); if (!(await isWritableTarget(r.def, r.rootReal))) throw new FilesError("not_writable", `根 ${r.def.name} 不可写`); - if (!r.lst?.isDirectory() || r.isSymlink) throw new FilesError("bad_request", `不是目录: ${r.rel || "/"}`); + if (!r.lst?.isDirectory() || r.isSymlink) throw new FilesError("bad_request", `不是目录:${r.rel || "/"}`); assertCleanName(body.name); await recheck(r.abs, r.rootReal); const abs = join(r.abs, body.name); try { await mkdir(abs); } catch (e) { - if (errnoOf(e) === "EEXIST") throw new FilesError("exists", `已存在: ${body.name}`); - throw new FilesError("bad_request", `创建目录失败: ${errMessage(e)}`); + if (errnoOf(e) === "EEXIST") throw new FilesError("exists", `已存在:${body.name}`); + throw new FilesError("bad_request", `创建目录失败:${errMessage(e)}`); } const rel = relOf(r.rootReal, abs); await audit(d, principal, "mkdir", { root: body.root, path: rel }); @@ -57,7 +57,7 @@ export async function renameEntry( ): Promise { const r = await resolveIn(d, body.root, body.path); if (r.rel === "") throw new FilesError("bad_request", "根目录不能重命名"); - if (!r.def.modifiable) throw new FilesError("not_writable", `根 ${r.def.name} 只读,不能重命名`); + if (!r.def.modifiable) throw new FilesError("not_writable", `根 ${r.def.name} 只读,不能重命名`); assertCleanName(body.newName); const oldName = basename(r.abs); if (body.newName === oldName) throw new FilesError("bad_request", "新名称与原名称相同"); @@ -68,7 +68,7 @@ export async function renameEntry( if (hits.length > 0) throw new FilesError( "download_active", - `该路径属于下载任务 ${hits.map((h) => h.taskId).join(", ")},重命名会让下载器找不到文件。先整理入库再改`, + `该路径属于下载任务 ${hits.map((h) => h.taskId).join(", ")},重命名会让下载器找不到文件。先整理入库再改`, ); const dirAbs = dirname(r.abs); @@ -91,7 +91,7 @@ export async function renameEntry( const st = await lstat(p.toAbs).catch((e) => (errnoOf(e) === "ENOENT" ? null : e)); if (st) taken.push(p.to); } - if (taken.length > 0) throw new FilesError("exists", `目标已存在,不覆盖: ${taken.join(", ")}`); + if (taken.length > 0) throw new FilesError("exists", `目标已存在,不覆盖:${taken.join(", ")}`); const lib = r.def.libraryRootId; let locCount = 0; @@ -123,11 +123,11 @@ export async function renameEntry( await recheck(p.fromAbs, r.rootReal); const before = await lstat(p.fromAbs); const res = await moveSafe(p.fromAbs, p.toAbs); - if (res.status === "exists") throw new FilesError("exists", `目标已存在,不覆盖: ${p.to}`); + if (res.status === "exists") throw new FilesError("exists", `目标已存在,不覆盖:${p.to}`); if (res.status !== "moved") throw new FilesError("bad_request", `重命名失败(${res.status}): ${p.from}`); const after = await lstat(p.toAbs); if (after.ino !== before.ino || after.dev !== before.dev) - throw new FilesError("bad_request", `重命名后 inode 不一致,已中止: ${p.to}`); + throw new FilesError("bad_request", `重命名后 inode 不一致,已中止:${p.to}`); done.push(p); } catch (e) { await rollback(); diff --git a/apps/server/src/modules/file-manager/organize.ts b/apps/server/src/modules/file-manager/organize.ts index 0afb6724..867ad2aa 100644 --- a/apps/server/src/modules/file-manager/organize.ts +++ b/apps/server/src/modules/file-manager/organize.ts @@ -213,10 +213,10 @@ export async function organizeFiles( for (const it of body.items) { const r = await resolveIn(d, it.root, it.path); if (r.rel === "") throw new FilesError("bad_request", "不能整理整个根"); - if (r.isSymlink) throw new FilesError("bad_request", `不整理符号链接: ${r.rel}`); + if (r.isSymlink) throw new FilesError("bad_request", `不整理符号链接:${r.rel}`); const action = it.operation ?? body.action; if (action === "move" && !r.def.modifiable) - throw new FilesError("not_writable", `源根 ${r.def.name} 只读,不能移动`); + throw new FilesError("not_writable", `源根 ${r.def.name} 只读,不能移动`); roots.add(it.root); const isDir = !!r.lst?.isDirectory(); const entityId = entityOf(it); @@ -231,7 +231,7 @@ export async function organizeFiles( if (it.targetDir) { const t = await resolveIn(d, it.targetDir.root, it.targetDir.path); if (!t.lst?.isDirectory() || t.isSymlink || !t.def.libraryRootId) - throw new FilesError("bad_request", `目标目录必须是库根下已存在的目录: ${it.targetDir.path || "/"}`); + throw new FilesError("bad_request", `目标目录必须是库根下已存在的目录:${it.targetDir.path || "/"}`); if (!(await isWritableTarget(t.def, t.rootReal))) throw new FilesError("not_writable", `目标根 ${t.def.name} 不可写`); targetDir = join(t.def.hostPath, t.rel); @@ -239,13 +239,13 @@ export async function organizeFiles( } // 季/集 if (it.season !== undefined || it.episodes !== undefined) { - if (!isTv) warnings.push(`${r.rel}: 电影不适用季/集覆写,已忽略`); + if (!isTv) warnings.push(`${r.rel}:电影不适用季/集覆写,已忽略`); else { if (it.season !== undefined) ov.season = it.season; if (it.episodes?.length) { const range = episodeRange(it.episodes); - if (!range) warnings.push(`${r.rel}: 集号 ${it.episodes.join(",")} 不连续,未应用(只支持连续区间)`); - else if (isDir) warnings.push(`${r.rel}: 目录项不能指定集号(会套到其下所有视频),未应用`); + if (!range) warnings.push(`${r.rel}:集号 ${it.episodes.join(",")} 不连续,未应用(只支持连续区间)`); + else if (isDir) warnings.push(`${r.rel}:目录项不能指定集号(会套到其下所有视频),未应用`); else ov.episodes = range; } } @@ -255,7 +255,7 @@ export async function organizeFiles( if (unit) { const u = units.get(unit); if (!u || u.media_entity_id !== entityId) { - warnings.push(`${r.rel}: 内容单元 ${unit} 不属于该作品,未应用`); + warnings.push(`${r.rel}:内容单元 ${unit} 不属于该作品,未应用`); unit = null; } else if (u.kind === "episode" || u.kind === "part") ov.contentUnitIds = [u.id]; } @@ -264,10 +264,10 @@ export async function organizeFiles( if (slotId) { const s = slots.get(slotId); if (!s || s.media_entity_id !== entityId) { - warnings.push(`${r.rel}: 版本目标 ${slotId} 不属于该作品,未应用`); + warnings.push(`${r.rel}:版本目标 ${slotId} 不属于该作品,未应用`); slotId = null; } else if (BUSY_SLOT.has(s.status)) { - warnings.push(`${r.rel}: 版本目标正在获取中(${s.status}),不改其状态`); + warnings.push(`${r.rel}:版本目标正在获取中(${s.status}),不改其状态`); slotId = null; } } @@ -416,12 +416,12 @@ export async function organizeFiles( const frozen = (await loadPlan(d, planId, "organize", reqHash)).content; const now = await factsOf(frozen.facts.map((f) => f.path)); const changed = now.find((f, i) => f.size !== frozen.facts[i]!.size || f.mtimeMs !== frozen.facts[i]!.mtimeMs); - if (changed) throw new FilesError("plan_stale", `源文件在预览后发生变化: ${changed.path}`); + if (changed) throw new FilesError("plan_stale", `源文件在预览后发生变化:${changed.path}`); const again = await planImport(d.ao, importReq(true)); const nowPairs = pairsOf(again).map(({ src, dst }) => ({ src, dst })); if (hashJson(nowPairs) !== hashJson(frozen.pairs)) - throw new FilesError("plan_stale", "目标目录或绑定已变化,与预览不一致。重新预览"); - if (again.status === "failed") throw new FilesError("bad_request", `计划不可执行:${again.reason ?? "blocked"}`); + throw new FilesError("plan_stale", "目标目录或绑定已变化,与预览不一致。重新预览"); + if (again.status === "failed") throw new FilesError("bad_request", `计划不可执行:${again.reason ?? "blocked"}`); if (g.action === "move") { const managed = await managedTasksUnder(db, req.sourcePaths); @@ -436,7 +436,7 @@ export async function organizeFiles( if (block) throw new FilesError( "download_active", - `下载任务 ${block.downloadTaskId} 的做种义务${block.hnr === "unknown" ? "未知" : "未满足"},不能移动,也不会自动改为复制。已建待处理事项 ${block.decisionId}`, + `下载任务 ${block.downloadTaskId} 的做种义务${block.hnr === "unknown" ? "未知" : "未满足"},不能移动,也不会自动改为复制。已建待处理事项 ${block.decisionId}`, ); } await retirePlan(d, planId); @@ -551,6 +551,6 @@ async function pickPlan(d: FmDeps, planIds: string[], reqHash: string): Promise< .where("title", "=", "files_plan") .execute(); const hit = rows.find((r) => (r.content as { op?: string; requestHash?: string }).requestHash === reqHash); - if (!hit) throw new FilesError("plan_stale", "没有与该组请求一致的 planId,重新预览"); + if (!hit) throw new FilesError("plan_stale", "没有与该组请求一致的 planId,重新预览"); return hit.id; } diff --git a/apps/server/src/modules/file-manager/probe.ts b/apps/server/src/modules/file-manager/probe.ts index c29c051c..f33e9a82 100644 --- a/apps/server/src/modules/file-manager/probe.ts +++ b/apps/server/src/modules/file-manager/probe.ts @@ -120,7 +120,7 @@ export async function scrape( .where("media_entity_id", "=", e.id) .where("status", "=", "active") .executeTakeFirst(); - if (!registered) throw new FilesError("bad_request", "作品没有已登记资产。先识别并整理入库,再刮削"); + if (!registered) throw new FilesError("bad_request", "作品没有已登记资产。先识别并整理入库,再刮削"); const { runId } = await d.ctx.engine.enqueue( "refreshMetadata", { mediaEntityId: e.id }, @@ -166,7 +166,7 @@ export async function scrapeBatch( for (const id of found) ids.add(id); } if (ids.size === 0 && skipped.length === 0) throw new FilesError("bad_request", "需要 items 或 mediaEntityIds"); - if (ids.size > 200) throw new FilesError("bad_request", `一次最多刷新 200 个作品(本次 ${ids.size})`); + if (ids.size > 200) throw new FilesError("bad_request", `一次最多刷新 200 个作品(本次 ${ids.size})`); const list = [...ids]; const ents = list.length ? await db.selectFrom("media_entity").select(["id", "title", "field_locks"]).where("id", "in", list).execute() diff --git a/apps/server/src/modules/file-manager/transfer.ts b/apps/server/src/modules/file-manager/transfer.ts index 3fbc3722..9c026cf0 100644 --- a/apps/server/src/modules/file-manager/transfer.ts +++ b/apps/server/src/modules/file-manager/transfer.ts @@ -95,20 +95,20 @@ export async function transfer( const t = await resolveIn(d, body.target.root, body.target.path); if (!t.lst?.isDirectory() || t.isSymlink) throw new FilesError("bad_request", "目标必须是已存在的目录"); if (!t.def.libraryRootId || !(await isWritableTarget(t.def, t.rootReal))) - throw new FilesError("not_writable", `目标根 ${t.def.name} 不可写,只能选 managed 库根`); + throw new FilesError("not_writable", `目标根 ${t.def.name} 不可写,只能选 managed 库根`); const sources: Resolved[] = []; for (const it of body.items) { const s = await resolveIn(d, it.root, it.path); if (s.rel === "") throw new FilesError("bad_request", "根目录不能移动或复制"); - if (mode === "move" && !s.def.modifiable) throw new FilesError("not_writable", `源根 ${s.def.name} 只读,只能复制`); - if (within(t.abs, s.abs)) throw new FilesError("bad_request", `目标在源目录之内: ${s.rel}`); - if (dirname(s.abs) === t.abs) throw new FilesError("bad_request", `源已在目标目录中: ${s.rel}`); - if (s.isSymlink) throw new FilesError("bad_request", `不能移动符号链接: ${s.rel}`); + if (mode === "move" && !s.def.modifiable) throw new FilesError("not_writable", `源根 ${s.def.name} 只读,只能复制`); + if (within(t.abs, s.abs)) throw new FilesError("bad_request", `目标在源目录之内:${s.rel}`); + if (dirname(s.abs) === t.abs) throw new FilesError("bad_request", `源已在目标目录中:${s.rel}`); + if (s.isSymlink) throw new FilesError("bad_request", `不能移动符号链接:${s.rel}`); sources.push(s); } const names = sources.map((s) => basename(s.abs)); - if (new Set(names).size !== names.length) throw new FilesError("bad_request", "多个源同名,会在目标目录冲突"); + if (new Set(names).size !== names.length) throw new FilesError("bad_request", "多个源同名,会在目标目录冲突"); const snaps = await Promise.all(sources.map((s, i) => snapshot(d, s, body.items[i]!.root))); const downloads = await managedDownloads(d); @@ -148,7 +148,7 @@ export async function transfer( const reason = planned.reason ?? "plan_failed"; throw new FilesError( reason.startsWith("library_root_offline") ? "root_unavailable" : "bad_request", - `无法生成计划:${reason}`, + `无法生成计划:${reason}`, ); } const planFiles = planned.plan.organize.files; @@ -189,9 +189,9 @@ export async function transfer( const allowCross = (await d.ctx.settings.get("organize")).allowCrossVolumeMove; const crossBlocked = mode === "move" && !allowCross && pairs.some((p) => p.crossDevice); const warnings: string[] = []; - if (crossBlocked) warnings.push("未开启跨卷移动,执行会失败。改用复制,或在 设置 → 整理 中开启"); - if (conflictSrc.size > 0) warnings.push(`${conflictSrc.size} 个文件在目标已存在,不覆盖,执行时记为失败`); - if (truncated) warnings.push("源文件数超过上限,分批操作"); + if (crossBlocked) warnings.push("未开启跨卷移动,执行会失败。改用复制,或在 设置 → 整理 中开启"); + if (conflictSrc.size > 0) warnings.push(`${conflictSrc.size} 个文件在目标已存在,不覆盖,执行时记为失败`); + if (truncated) warnings.push("源文件数超过上限,分批操作"); const base = { mode, pairs, totalBytes, freeBytes, spaceOk, hnrBlocks, truncated, warnings }; if (body.dryRun) { @@ -215,22 +215,22 @@ export async function transfer( for (const [i, s] of snaps.entries()) { const frozen = plan.content.sources[i]; if (!frozen || frozen.abs !== s.abs || frozen.fingerprint !== s.fingerprint) - throw new FilesError("plan_stale", `源在预览后发生变化(大小/修改时间/文件清单): ${s.rel}`); + throw new FilesError("plan_stale", `源在预览后发生变化(大小/修改时间/文件清单):${s.rel}`); } if (plan.content.files && hashJson(plan.content.files) !== hashJson(filePairs)) - throw new FilesError("plan_stale", "目标已变化,与预览不一致。重新预览"); - if (truncated) throw new FilesError("bad_request", "源目录文件数超过上限,分批操作"); - if (!spaceOk) throw new FilesError("bad_request", `目标卷空间不足:需要 ${needBytes},可用 ${freeBytes}`); + throw new FilesError("plan_stale", "目标已变化,与预览不一致。重新预览"); + if (truncated) throw new FilesError("bad_request", "源目录文件数超过上限,分批操作"); + if (!spaceOk) throw new FilesError("bad_request", `目标卷空间不足:需要 ${needBytes},可用 ${freeBytes}`); if (crossBlocked) throw new FilesError( "bad_request", - "未开启跨卷移动(settings.organize.allowCrossVolumeMove)。改用复制,或开启后重试", + "未开启跨卷移动(settings.organize.allowCrossVolumeMove)。改用复制,或开启后重试", ); if (mode === "move") { if (hnrBlocks.length > 0) throw new FilesError( "download_active", - `源属于下载任务 ${[...new Set(hnrBlocks.map((h) => h.taskId))].join(", ")},直接移动会让下载器丢失文件。改用整理入库,它会按做种义务决定能否移动`, + `源属于下载任务 ${[...new Set(hnrBlocks.map((h) => h.taskId))].join(", ")},直接移动会让下载器丢失文件。改用整理入库,它会按做种义务决定能否移动`, ); if (!isAdmin(principal)) { // 每个源按其(目录则为其下)已登记资产求作品集合,逐一核对授权;任一不覆盖 → 整体拒绝并列出未授权项 @@ -272,7 +272,7 @@ export async function transfer( } if (!rec.recordId || rec.status === "failed") { await markTask(db, taskId, "failed", { error: rec.reason ?? rec.status }); - throw new FilesError("bad_request", `计划不可执行:${rec.reason ?? rec.status}`); + throw new FilesError("bad_request", `计划不可执行:${rec.reason ?? rec.status}`); } const recordId = rec.recordId; await db.updateTable("organization_record").set({ task_id: taskId }).where("id", "=", recordId).execute(); @@ -316,7 +316,7 @@ export async function transfer( failed.push({ path: s.rel, code: err.code === "EEXIST" ? "exists" : (err.code ?? bad.status), - message: `${err.message ?? bad.status}${mine.length > 1 ? `(${mine.filter((f) => f.status === "succeeded").length}/${mine.length} 个文件已完成)` : ""}`, + message: `${err.message ?? bad.status}${mine.length > 1 ? `(${mine.filter((f) => f.status === "succeeded").length}/${mine.length} 个文件已完成)` : ""}`, }); } } diff --git a/apps/server/src/modules/file-manager/trash.ts b/apps/server/src/modules/file-manager/trash.ts index a74e309b..2b00e995 100644 --- a/apps/server/src/modules/file-manager/trash.ts +++ b/apps/server/src/modules/file-manager/trash.ts @@ -157,8 +157,8 @@ const BLOCK_ERR: Record< > = { download_active: ["download_active", "属于未完成或做种义务未满足的下载任务"], is_root: ["bad_request", "不能删除根目录或只读根下的内容"], - needs_retire: ["needs_retire", "含已登记资产,需勾选“同时退役资产”"], - needs_unbind: ["needs_unbind", "是作品/季的绑定目录,需勾选“解除绑定”"], + needs_retire: ["needs_retire", "含已登记资产,需勾选「同时退役资产」"], + needs_unbind: ["needs_unbind", "是作品/季的绑定目录,需勾选「解除绑定」"], root_unavailable: ["root_unavailable", "根不可用"], not_found: ["not_found", "不存在"], escape: ["path_escape", "路径指向根外"], @@ -276,8 +276,8 @@ export async function deleteFiles( for (const s of slots) await upsertDecision(d.ao, trx, { kind: "other", - title: `版本目标的文件已删除:${s.name}`, - summary: "满足该版本目标的资产已在文件管理中删除,版本目标改为需关注。确认是否重新获取", + title: `版本目标的文件已删除:${s.name}`, + summary: "满足该版本目标的资产已在文件管理中删除,版本目标改为需关注。确认是否重新获取", subjectType: "acquisition_slot", subjectId: s.id, dedupeKey: `files_delete_slot:${s.id}`, @@ -384,7 +384,7 @@ async function moveToTrash( try { await mkdir(dirname(trashPath), { recursive: true }); const r = await moveSafe(abs, trashPath); - if (r.status === "exists") throw new FilesError("exists", `回收站路径已存在: ${trashPath}`); + if (r.status === "exists") throw new FilesError("exists", `回收站路径已存在:${trashPath}`); moved = r.status === "moved"; } catch (e) { if (e instanceof FilesError) throw e; @@ -396,7 +396,7 @@ async function moveToTrash( if (check.size > d.options.trashCrossMaxBytes) throw new FilesError( "bad_request", - `无法同卷移入回收站,且大小 ${check.size} 超过跨卷上限 ${d.options.trashCrossMaxBytes}。改用永久删除`, + `无法同卷移入回收站,且大小 ${check.size} 超过跨卷上限 ${d.options.trashCrossMaxBytes}。改用永久删除`, ); await rm(join(ctx.rootReal!, TRASH_DIR, stamp, id), { recursive: true, force: true }).catch(() => undefined); trashPath = join(resolve(d.options.dataDir), "trash", stamp, id, check.path); @@ -487,10 +487,10 @@ export async function mkdirWithin(rootReal: string, rel: string): Promise throw e; }); if (st?.isSymbolicLink()) - throw new FilesError("path_escape", `还原路径中的 ${relOfRoot(rootReal, cur)} 是符号链接,不能还原`); + throw new FilesError("path_escape", `还原路径中的 ${relOfRoot(rootReal, cur)} 是符号链接,不能还原`); if (st && !st.isDirectory()) throw new FilesError("exists", `还原路径中的 ${relOfRoot(rootReal, cur)} 不是目录`); if (!st) await mkdir(cur); - if (!within(await realpath(cur), rootReal)) throw new FilesError("path_escape", `还原路径指向根外: ${rel}`); + if (!within(await realpath(cur), rootReal)) throw new FilesError("path_escape", `还原路径指向根外:${rel}`); } } const relOfRoot = (root: string, p: string) => (p === root ? "" : p.slice(root.length + 1)); @@ -509,12 +509,12 @@ export async function restoreTrash( throw new FilesError("bad_request", "还原目标必须是已存在的目录"); // "还原到…"只能进可写的 managed 库根(与 move/copy 的目标规则一致;register_only / 下载根拒绝,验收 07 S-5) if (target && !(await isWritableTarget(target.def, target.rootReal))) - throw new FilesError("not_writable", `还原目标根 ${target.def.name} 不可写,只能选 managed 库根`); + throw new FilesError("not_writable", `还原目标根 ${target.def.name} 不可写,只能选 managed 库根`); for (const id of body.ids) { try { const row = await d.ctx.db.selectFrom("trash_item").selectAll().where("id", "=", id).executeTakeFirst(); if (!row) throw new FilesError("not_found", "回收站条目不存在"); - if (!trashPathSafe(d, row)) throw new FilesError("path_escape", "回收站路径异常,拒绝操作"); + if (!trashPathSafe(d, row)) throw new FilesError("path_escape", "回收站路径异常,拒绝操作"); let rootId = row.root_id; let dst: string; let rootReal: string; @@ -537,7 +537,7 @@ export async function restoreTrash( ); dst = parent.abs; } - if (await lstat(dst).catch(() => null)) throw new FilesError("exists", "原位置已被占用,可选择“还原到…”"); + if (await lstat(dst).catch(() => null)) throw new FilesError("exists", "原位置已被占用,可选择「还原到…」"); const r = await moveSafe(row.trash_path, dst); if (r.status === "exists") throw new FilesError("exists", "原位置已被占用"); if (r.status === "exdev") { @@ -547,7 +547,7 @@ export async function restoreTrash( if (bytes > d.options.trashCrossMaxBytes) throw new FilesError( "too_large", - `跨卷还原大小 ${bytes} 超过上限 ${d.options.trashCrossMaxBytes}。在同卷还原,或手工处理 ${row.trash_path}`, + `跨卷还原大小 ${bytes} 超过上限 ${d.options.trashCrossMaxBytes}。在同卷还原,或手工处理 ${row.trash_path}`, ); await copyTree(d, row.trash_path, dst, files); await rm(row.trash_path, { recursive: true }); @@ -597,7 +597,7 @@ export async function purgeTrash( const failed: FilesTrashPurgeResult["failed"] = []; for (const row of rows) { try { - if (!trashPathSafe(d, row)) throw new FilesError("path_escape", "回收站路径异常,拒绝清除"); + if (!trashPathSafe(d, row)) throw new FilesError("path_escape", "回收站路径异常,拒绝清除"); const gone = await d.ctx.db.deleteFrom("trash_item").where("id", "=", row.id).returning("id").executeTakeFirst(); if (!gone) continue; await rm(row.trash_path, { recursive: true, force: true }); diff --git a/apps/server/src/modules/file-manager/tree.ts b/apps/server/src/modules/file-manager/tree.ts index d11dd7fa..8d4846b6 100644 --- a/apps/server/src/modules/file-manager/tree.ts +++ b/apps/server/src/modules/file-manager/tree.ts @@ -19,7 +19,7 @@ const hiddenName = (n: string) => n.startsWith(".") || isTempName(n); export async function treeOf(d: FmDeps, q: FilesTreeQuery): Promise { const r = await resolveIn(d, q.root, q.path); - if (!r.lst?.isDirectory() || r.isSymlink) throw new FilesError("bad_request", `不是目录: ${r.rel || "/"}`); + if (!r.lst?.isDirectory() || r.isSymlink) throw new FilesError("bad_request", `不是目录:${r.rel || "/"}`); const names = await readdir(r.abs, { withFileTypes: true }); const subdirs = names .filter((n) => n.isDirectory() && !hiddenName(n.name)) diff --git a/apps/server/src/modules/pi/index.ts b/apps/server/src/modules/pi/index.ts index b5672bee..cce60b12 100644 --- a/apps/server/src/modules/pi/index.ts +++ b/apps/server/src/modules/pi/index.ts @@ -74,7 +74,7 @@ export function createPiModule( key: "pi.token_refresh", defaults: { name: "上游登录刷新", - description: "订阅登录即将过期的 Agent 上游,提前刷新 token", + description: "订阅登录即将过期的 Agent 上游,提前刷新 token", group: "maintain", schedule: { type: "interval", everyMs: interval }, }, diff --git a/apps/server/src/modules/pi/refresh.ts b/apps/server/src/modules/pi/refresh.ts index 7ea7b5c2..db35dd04 100644 --- a/apps/server/src/modules/pi/refresh.ts +++ b/apps/server/src/modules/pi/refresh.ts @@ -24,7 +24,7 @@ const DECISION_KEY = (id: string) => `pi_upstream_expired:${id}`; /** 与其它模块的 upsertDecision 同一写法(跨模块不 import):同键未关闭时只累加 attempts */ async function upsertExpiredDecision(ctx: ServerContext, row: PiUpstreamRow, message: string) { - const title = `Agent 上游「${row.name}」登录已失效,需要重新登录`; + const title = `Agent 上游「${row.name}」登录已失效,需要重新登录`; await ctx.db.transaction().execute(async (trx) => { const r = await sql<{ id: string; inserted: boolean }>` insert into decision (kind, severity, title, summary, subject_type, subject_id, dedupe_key, evidence, options) diff --git a/apps/server/src/modules/pi/routes.ts b/apps/server/src/modules/pi/routes.ts index c53f4c4d..b3177106 100644 --- a/apps/server/src/modules/pi/routes.ts +++ b/apps/server/src/modules/pi/routes.ts @@ -107,8 +107,8 @@ export function registerPiRoutes( // 自定义 Base URL 的凭证检查不连端点:不据此改写可用状态,提示用最小补全确认可达(QA U2) const unprobed = mode === "auth" && !!row.base_url && row.auth_kind !== "oauth"; const notes = [ - t.inCatalog ? null : `模型 ${t.model} 不在内置模型目录中,按手填 id 调用`, - unprobed ? "只核对了凭证与模型,没有连接 Base URL;用「最小补全」确认端点可达" : null, + t.inCatalog ? null : `模型 ${t.model} 不在内置模型目录中,按手填 id 调用`, + unprobed ? "只核对了凭证与模型,没有连接 Base URL;用「最小补全」确认端点可达" : null, ].filter(Boolean); out = { ok: true, source: t.source, model: t.model, errorKind: null, message: notes.join(";") || null }; if (!unprobed) await setStatus(ctx, row.id, { status: "ok", verified: true }); diff --git a/apps/server/src/modules/pi/service.ts b/apps/server/src/modules/pi/service.ts index 1d79b571..edaf09ec 100644 --- a/apps/server/src/modules/pi/service.ts +++ b/apps/server/src/modules/pi/service.ts @@ -63,12 +63,12 @@ function validateShape(input: { if (!info.authKinds.includes(input.authKind as never)) throw new ApiException( "validation_failed", - `${info.name} 不支持认证方式 ${input.authKind},可用:${info.authKinds.join(" / ")}`, + `${info.name} 不支持认证方式 ${input.authKind},可用:${info.authKinds.join(" / ")}`, ); if (input.baseUrl && (!info.baseUrlOverridable || input.authKind === "oauth")) throw new ApiException( "validation_failed", - `${info.name}${input.authKind === "oauth" ? "(订阅登录)" : ""} 不能覆盖 baseUrl`, + `${info.name}${input.authKind === "oauth" ? "(订阅登录)" : ""} 不能覆盖 baseUrl`, ); if (input.provider === "openai-compatible" && (!input.baseUrl || !input.defaultModel)) throw new ApiException("validation_failed", "自定义兼容端点必须填 baseUrl 与模型 id"); @@ -213,9 +213,9 @@ export async function resolveRunUpstream(ctx: ServerContext, upstreamId?: string const id = upstreamId ?? (await defaultUpstreamId(ctx)); if (!id) return null; const row = await getRow(ctx, id); - if (!row) throw new Error(upstreamId ? `指定的上游 ${upstreamId} 不存在` : "默认上游已不存在,在设置里重新选择"); + if (!row) throw new Error(upstreamId ? `指定的上游 ${upstreamId} 不存在` : "默认上游已不存在,在设置里重新选择"); if (!row.enabled) throw new Error(`上游「${row.name}」已停用`); - if (row.status === "expired") throw new Error(`上游「${row.name}」登录已失效,在设置里重新登录`); + if (row.status === "expired") throw new Error(`上游「${row.name}」登录已失效,在设置里重新登录`); if (row.status === "auth_required") throw new Error(`上游「${row.name}」${row.auth_kind === "oauth" ? "未登录" : "未配置 API Key"}`); return { row, spec: { ...specOf(row), extra: parseExtra(row.extra) }, credentials: dbCredentialStore(ctx, row.id) }; diff --git a/apps/server/src/modules/schedules/agent-checks.ts b/apps/server/src/modules/schedules/agent-checks.ts index d82eb014..3f8f76c2 100644 --- a/apps/server/src/modules/schedules/agent-checks.ts +++ b/apps/server/src/modules/schedules/agent-checks.ts @@ -29,12 +29,12 @@ export const BUILTIN_AGENT_CHECKS: { { key: "check.pending", name: "待处理巡检", - description: "检查待处理事项并给出建议(只读)", + description: "检查待处理事项并给出建议(只读)", everyMs: 6 * HOUR, config: { goalTemplate: - "巡检待处理({{now}}):有 {{openDecisions}} 条未关闭的待处理事项。逐条查看证据,按紧急程度排序," + - "给出处理建议和理由。只读,不做任何变更。", + "巡检待处理({{now}}):有 {{openDecisions}} 条未关闭的待处理事项。逐条查看证据,按紧急程度排序," + + "给出处理建议和理由。只读,不做任何变更。", capabilityAllowlist: [...INVESTIGATE_CAPABILITIES], budget: { maxToolCalls: 20, maxDurationSec: 300 }, triggerCondition: "open_decisions", @@ -43,12 +43,12 @@ export const BUILTIN_AGENT_CHECKS: { { key: "check.downloads", name: "下载诊断", - description: "检查无进展或失败的下载并诊断;开启恢复后在授权内重试或重新校验", + description: "检查无进展或失败的下载并诊断;开启恢复后在授权内重试或重新校验", everyMs: 2 * HOUR, config: { goalTemplate: - "巡检下载({{now}}):有 {{stalledDownloads}} 个下载任务无进展或失败。用只读能力查明原因(站点、做种、磁盘、任务错误)," + - "给出诊断与建议;若本次允许恢复,只在授权内做重试或强制重新校验,每个任务最多一次,不换源、不删除。", + "巡检下载({{now}}):有 {{stalledDownloads}} 个下载任务无进展或失败。用只读能力查明原因(站点、做种、磁盘、任务错误)," + + "给出诊断与建议;若本次允许恢复,只在授权内做重试或强制重新校验,每个任务最多一次,不换源、不删除。", capabilityAllowlist: [...INVESTIGATE_CAPABILITIES], budget: { maxToolCalls: 24, maxDurationSec: 420 }, triggerCondition: "stalled_downloads", @@ -58,12 +58,12 @@ export const BUILTIN_AGENT_CHECKS: { { key: "check.weekly", name: "订阅与站点周报", - description: "检查订阅缺集与站点健康,汇总写入待处理(只读)", + description: "检查订阅缺集与站点健康,汇总写入待处理(只读)", everyMs: 7 * 24 * HOUR, config: { goalTemplate: - "生成本周巡检周报({{date}}):追踪中的订阅 {{activeSubscriptions}} 个。列出缺集的订阅及可能原因、" + - "健康异常的站点与 Feed,给出本周建议。只读。最后用不超过 300 字的中文汇总。", + "生成本周巡检周报({{date}}):追踪中的订阅 {{activeSubscriptions}} 个。列出缺集的订阅及可能原因、" + + "健康异常的站点与 Feed,给出本周建议。只读。最后用不超过 300 字的中文汇总。", capabilityAllowlist: [...INVESTIGATE_CAPABILITIES], budget: { maxToolCalls: 30, maxDurationSec: 600 }, triggerCondition: "subscriptions_active", @@ -106,7 +106,7 @@ const isPlanOnly = (id: string) => { /** agent_check 的执行体(内置与自定义共用) */ export async function runAgentCheck(ctx: ServerContext, c: JobRunContext): Promise { const parsed = AgentCheckConfig.safeParse(c.config); - if (!parsed.success) throw new Error(`巡检参数无效:${parsed.error.issues[0]?.message ?? "未知"}`); + if (!parsed.success) throw new Error(`巡检参数无效:${parsed.error.issues[0]?.message ?? "未知"}`); const cfg = parsed.data; const f = await facts(ctx); const unmet = @@ -114,7 +114,7 @@ export async function runAgentCheck(ctx: ServerContext, c: JobRunContext): Promi (cfg.triggerCondition === "stalled_downloads" && f.stalledDownloads === 0) || (cfg.triggerCondition === "subscriptions_active" && f.activeSubscriptions === 0); // 手动执行不看触发条件 - if (unmet && c.trigger === "schedule") return { status: "skipped", summary: "触发条件不满足,未唤醒 Agent" }; + if (unmet && c.trigger === "schedule") return { status: "skipped", summary: "触发条件不满足,未唤醒 Agent" }; let mode: "execute" | "plan_only" = "plan_only"; let allowlist = cfg.capabilityAllowlist.filter(isPlanOnly); @@ -124,7 +124,7 @@ export async function runAgentCheck(ctx: ServerContext, c: JobRunContext): Promi if (grants.length > 0) { mode = "execute"; allowlist = [...new Set([...(allowlist.length ? allowlist : INVESTIGATE_CAPABILITIES), ...RECOVER_CAPABILITIES])]; - } else note = "(没有覆盖恢复动作的有效授权,改为只读)"; + } else note = "(没有覆盖恢复动作的有效授权,改为只读)"; } const goal = renderGoal(cfg.goalTemplate, c.now, f); const dedupeKey = c.trigger === "manual" ? `schedule:${c.jobId}:manual:${c.runId}` : `schedule:${c.jobId}:${c.slot}`; diff --git a/apps/server/src/modules/schedules/cron.ts b/apps/server/src/modules/schedules/cron.ts index 04a1d80c..b51fab55 100644 --- a/apps/server/src/modules/schedules/cron.ts +++ b/apps/server/src/modules/schedules/cron.ts @@ -23,7 +23,7 @@ function parseField(src: string, [lo, hi]: [number, number], label: string): Set const out = new Set(); for (const part of src.split(",")) { const m = /^(\*|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/.exec(part); - if (!m) throw new Error(`cron ${label} 段无效:${part}`); + if (!m) throw new Error(`cron ${label} 段无效:${part}`); let a = lo; let b = hi; if (m[1] !== "*") { @@ -31,7 +31,7 @@ function parseField(src: string, [lo, hi]: [number, number], label: string): Set b = m[3] !== undefined ? Number(m[3]) : m[4] !== undefined ? hi : a; } const step = m[4] !== undefined ? Number(m[4]) : 1; - if (a < lo || b > hi || a > b || step < 1) throw new Error(`cron ${label} 段越界:${part}`); + if (a < lo || b > hi || a > b || step < 1) throw new Error(`cron ${label} 段越界:${part}`); for (let v = a; v <= b; v += step) out.add(v); } return out; @@ -39,7 +39,7 @@ function parseField(src: string, [lo, hi]: [number, number], label: string): Set export function parseCron(expr: string): CronFields { const parts = expr.trim().split(/\s+/); - if (parts.length !== 5) throw new Error("cron 需要 5 段:分 时 日 月 周"); + if (parts.length !== 5) throw new Error("cron 需要 5 段:分 时 日 月 周"); const [mi, h, d, mo, w] = parts as [string, string, string, string, string]; const weekday = parseField(w, RANGES[4]!, "周"); if (weekday.has(7)) weekday.add(0); @@ -85,5 +85,5 @@ export function nextCronAfter(expr: string | CronFields, after: Date): Date { } return t; } - throw new Error(`cron 在 4 年内没有触发时间:${typeof expr === "string" ? expr : "(parsed)"}`); + throw new Error(`cron 在 4 年内没有触发时间:${typeof expr === "string" ? expr : "(parsed)"}`); } diff --git a/apps/server/src/modules/schedules/index.ts b/apps/server/src/modules/schedules/index.ts index bd294f3f..bd4d8a54 100644 --- a/apps/server/src/modules/schedules/index.ts +++ b/apps/server/src/modules/schedules/index.ts @@ -37,7 +37,7 @@ export function createSchedulesModule(opts: SchedulesModuleOptions = {}): Server key: "maintain.backup", defaults: { name: "备份", - description: "数据库与数据目录备份;目前用 umt backup(宿主机 cron 调用),未接入调度", + description: "数据库与数据目录备份;目前用 umt backup(宿主机 cron 调用),未接入调度", group: "maintain", schedule: { type: "cron", cron: "30 3 * * *" }, enabled: false, diff --git a/apps/server/src/modules/schedules/registry.ts b/apps/server/src/modules/schedules/registry.ts index ec265981..f65e43d8 100644 --- a/apps/server/src/modules/schedules/registry.ts +++ b/apps/server/src/modules/schedules/registry.ts @@ -71,7 +71,7 @@ class MemoryRegistry implements ScheduleRegistry { return [...this.defs.values()]; } register(def: ScheduledJobDef) { - if (!/^[a-z][a-z0-9_.:-]{1,120}$/.test(def.key)) throw new Error(`定时任务 key 不合法:${def.key}`); + if (!/^[a-z][a-z0-9_.:-]{1,120}$/.test(def.key)) throw new Error(`定时任务 key 不合法:${def.key}`); this.defs.set(def.key, def); this.version += 1; } diff --git a/apps/server/src/modules/schedules/scheduler.ts b/apps/server/src/modules/schedules/scheduler.ts index f96a3496..6a147ead 100644 --- a/apps/server/src/modules/schedules/scheduler.ts +++ b/apps/server/src/modules/schedules/scheduler.ts @@ -113,7 +113,7 @@ export class Scheduler { }); } catch (err) { this.leader = false; - this.log.warn({ err: { message: errText(err) } }, "调度器循环出错,稍后重试"); + this.log.warn({ err: { message: errText(err) } }, "调度器循环出错,稍后重试"); if (!this.stopped) await this.sleep(retry); } } @@ -121,7 +121,7 @@ export class Scheduler { /** 上一个执行者中断时留下的 running 状态:标为失败 */ private async recoverInterrupted(): Promise { - const msg = "调度器重启,上次运行中断"; + const msg = "调度器重启,上次运行中断"; await this.db .updateTable("scheduled_job_run") .set({ status: "failed", error: msg, finished_at: this.now() }) @@ -296,7 +296,7 @@ export class Scheduler { where r.agent_status is null and a.status = any(${TERMINAL_AGENT}) limit 50`.execute(this.db); for (const r of rows.rows) { - const summary = (r.summary ?? "").trim() || `Agent 运行结束:${r.status}`; + const summary = (r.summary ?? "").trim() || `Agent 运行结束:${r.status}`; await this.db .updateTable("scheduled_job_run") .set({ agent_status: r.status, summary: summary.slice(0, 4000) }) @@ -305,7 +305,7 @@ export class Scheduler { if (r.config?.reportAsDecision === true && r.status === "succeeded") { const ins = await sql<{ id: string }>` insert into decision (kind, severity, title, summary, subject_type, subject_id, dedupe_key, evidence, agent_run_id) - values ('other', 'info', ${`巡检报告:${r.job_name}`}, ${summary.slice(0, 4000)}, 'agent_run', ${r.agent_run_id}, + values ('other', 'info', ${`巡检报告:${r.job_name}`}, ${summary.slice(0, 4000)}, 'agent_run', ${r.agent_run_id}, ${`schedule-report:${r.id}`}, ${JSON.stringify({ scheduledJobId: r.job_id, scheduledRunId: r.id })}::jsonb, ${r.agent_run_id}) on conflict do nothing @@ -315,7 +315,7 @@ export class Scheduler { await this.ctx.outbox.emit( this.db, "decision.created", - { decisionId: d.id, kind: "other", severity: "info", title: `巡检报告:${r.job_name}` }, + { decisionId: d.id, kind: "other", severity: "info", title: `巡检报告:${r.job_name}` }, { idempotencyKey: `decision.created:${d.id}` }, ); } diff --git a/apps/server/src/modules/schedules/store.ts b/apps/server/src/modules/schedules/store.ts index 7ae4b729..bf5e9356 100644 --- a/apps/server/src/modules/schedules/store.ts +++ b/apps/server/src/modules/schedules/store.ts @@ -139,7 +139,7 @@ export async function syncDefinitions(db: Kysely, defs: readonly ScheduledJo export async function getJob(db: Kysely, id: string): Promise { const row = await db.selectFrom("scheduled_job").selectAll().where("id", "=", id).executeTakeFirst(); - if (!row) throw new ApiException("not_found", `定时任务不存在:${id}`); + if (!row) throw new ApiException("not_found", `定时任务不存在:${id}`); return row; } @@ -201,7 +201,7 @@ export async function updateJob( }); } if (patch.enabled !== undefined) { - if (patch.enabled && !row.available) throw new ApiException("conflict", "任务未配置,不能启用"); + if (patch.enabled && !row.available) throw new ApiException("conflict", "任务未配置,不能启用"); set.enabled = patch.enabled; set.customized = true; if (patch.enabled && !row.enabled && !set.next_run_at) { @@ -228,7 +228,7 @@ export async function updateJob( /** 立即执行:置 run_requested_at(已在排队或运行中则合并,不重复) */ export async function requestRun(db: Kysely, id: string): Promise<{ row: ScheduledJobRow; queued: boolean }> { const row = await getJob(db, id); - if (!row.available) throw new ApiException("conflict", "任务未配置,不能执行"); + if (!row.available) throw new ApiException("conflict", "任务未配置,不能执行"); const upd = await db .updateTable("scheduled_job") .set({ run_requested_at: sql`now()` }) @@ -262,7 +262,7 @@ export async function skipNext(db: Kysely, id: string, now: Date): Promise, body: AgentCheckCreate, n export async function deleteJob(db: Kysely, id: string): Promise { const row = await getJob(db, id); - if (row.builtin || row.kind !== "agent_check") throw new ApiException("conflict", "内置任务不能删除,可以停用"); + if (row.builtin || row.kind !== "agent_check") throw new ApiException("conflict", "内置任务不能删除,可以停用"); await db.deleteFrom("scheduled_job").where("id", "=", id).execute(); } diff --git a/apps/server/src/modules/search/dispatch.ts b/apps/server/src/modules/search/dispatch.ts index d8cb1939..d4601cb6 100644 --- a/apps/server/src/modules/search/dispatch.ts +++ b/apps/server/src/modules/search/dispatch.ts @@ -41,7 +41,7 @@ export class InternalDispatcher { return c.body(null, 500); }); inner.notFound((c) => { - this.errors.set(c.req.raw, new ApiException("internal", `内部端点不存在:${c.req.method} ${c.req.path}`)); + this.errors.set(c.req.raw, new ApiException("internal", `内部端点不存在:${c.req.method} ${c.req.path}`)); return c.body(null, 404); }); this.inner = inner; diff --git a/apps/server/src/modules/search/routes.ts b/apps/server/src/modules/search/routes.ts index 654f633a..7f0770fc 100644 --- a/apps/server/src/modules/search/routes.ts +++ b/apps/server/src/modules/search/routes.ts @@ -208,7 +208,7 @@ export function registerSearchRoutes(app: ModuleApp, ctx: ServerContext, d: Sear row = await find(); } } - if (!row) throw new ApiException("not_found", "资源不在搜索结果里,重新搜索后再下载"); + if (!row) throw new ApiException("not_found", "资源不在搜索结果里,重新搜索后再下载"); return dispatch.call(c, "POST", "/acquisition-plans", { ...(body as SearchReleasePlanRequest), sourceReleaseId: row.id, diff --git a/apps/server/src/modules/sources-identity/defs.ts b/apps/server/src/modules/sources-identity/defs.ts index b8b2816c..c6f81468 100644 --- a/apps/server/src/modules/sources-identity/defs.ts +++ b/apps/server/src/modules/sources-identity/defs.ts @@ -102,7 +102,7 @@ export const D = { method: "get" as const, path: "/sites", scope: "read" as const, - summary: "站点列表(include=accounts 附带账号与整体账号状态)", + summary: "站点列表(include=accounts 附带账号与整体账号状态)", tags: TS, query: SiteListQuery, response: cursorPage(SiteListItem), @@ -117,7 +117,7 @@ export const D = { body: SiteCreate, response: Site, }, - update: write("patch", "/sites/{id}", "admin", "修改站点(启停、快照周期、限流、代理)", SiteUpdate, Site, TS), + update: write("patch", "/sites/{id}", "admin", "修改站点(启停、快照周期、限流、代理)", SiteUpdate, Site, TS), }, siteAccounts: { list: list("/site-accounts", "站点账号列表", SiteAccount, { siteId: Id.optional() }, TS), @@ -126,7 +126,7 @@ export const D = { method: "post" as const, path: "/site-accounts", scope: "admin" as const, - summary: "新建站点账号(凭证只写,存为 secret)", + summary: "新建站点账号(凭证只写,存为 secret)", tags: TS, body: SiteAccountCreate, response: SiteAccount, @@ -148,7 +148,7 @@ export const D = { method: "post" as const, path: "/feed-sources", scope: "admin" as const, - summary: "新建 Feed(url 只写,存为 secret;对外只给脱敏 URL)", + summary: "新建 Feed(url 只写,存为 secret;对外只给脱敏 URL)", tags: TF, body: FeedSourceCreate, response: FeedSource, @@ -158,7 +158,7 @@ export const D = { "post", "/feed-sources/{id}/fetch", "admin", - "立即抓取(仍受站点限流)", + "立即抓取(仍受站点限流)", undefined, z.object({ taskId: Id }), TF, @@ -202,7 +202,7 @@ export const D = { method: "post" as const, path: "/media-entities", scope: "organize.execute" as const, - summary: "手动建本地作品(TMDB 无记录时可先建档)", + summary: "手动建本地作品(TMDB 无记录时可先建档)", tags: TE, body: MediaEntityCreate, response: MediaEntity, @@ -211,7 +211,7 @@ export const D = { "patch", "/media-entities/{id}", "organize.execute", - "修改作品(标题/原名/年份按字段覆盖并锁定)", + "修改作品(标题/原名/年份按字段覆盖并锁定)", MediaEntityUpdate, MediaEntity, TE, @@ -220,7 +220,7 @@ export const D = { "post", "/media-entities/{id}/merge", "organize.execute", - "合并重复作品(保留旧 ID 跳转)", + "合并重复作品(保留旧 ID 跳转)", MediaEntityMerge, MediaEntity, TE, @@ -239,7 +239,7 @@ export const D = { "patch", "/content-units/{id}", "organize.execute", - "修改内容单元(改编号即确认并锁定)", + "修改内容单元(改编号即确认并锁定)", ContentUnitUpdate, ContentUnit, TE, @@ -270,7 +270,7 @@ export const D = { "post", "/external-identities", "organize.execute", - "新增外部身份(accepted 等同人工纠错)", + "新增外部身份(accepted 等同人工纠错)", ExternalIdentityCreate, ExternalIdentity, TI, @@ -280,7 +280,7 @@ export const D = { "patch", "/external-identities/{id}", "organize.execute", - "修订外部身份(乐观并发)", + "修订外部身份(乐观并发)", ExternalIdentityUpdate, ExternalIdentity, TI, @@ -295,7 +295,7 @@ export const D = { decisions: { list: list( "/decisions", - "待处理事项(缺省只列未关闭)", + "待处理事项(缺省只列未关闭)", Decision, { status: DecisionStatus.optional(), kind: DecisionKind.optional() }, TD, diff --git a/apps/server/src/modules/sources-identity/index.ts b/apps/server/src/modules/sources-identity/index.ts index 312f1b39..2e1a5983 100644 --- a/apps/server/src/modules/sources-identity/index.ts +++ b/apps/server/src/modules/sources-identity/index.ts @@ -58,7 +58,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S return { summary: `${await fn(rt)} 项` }; }, }); - job("feeds.poll", "Feed 轮询", "扫描到期的 Feed 并入队抓取(每个 Feed 仍按自己的轮询间隔)", tickFeeds); + job("feeds.poll", "Feed 轮询", "扫描到期的 Feed 并入队抓取(每个 Feed 仍按自己的轮询间隔)", tickFeeds); job("sites.snapshot", "站点快照", "扫描到周期的站点并入队快照", tickSnapshots); job("identity.review", "待识别复查", "到期的待识别条目重新识别", tickReviews); // W9-A 站点中心:用户数据每 6 小时、健康检测每小时(可在定时任务页关闭或改周期) @@ -66,7 +66,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S key: "sites.user_data", defaults: { name: "站点数据刷新", - description: "登录状态正常的站点账号刷新上传 / 下载 / 分享率等用户数据;会话失效的账密账号自动重登一次", + description: "登录状态正常的站点账号刷新上传 / 下载 / 分享率等用户数据;会话失效的账密账号自动重登一次", group: "collect", schedule: { type: "interval", everyMs: 6 * 3_600_000 }, firstRun: "after_interval", @@ -80,7 +80,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S key: "sites.health_check", defaults: { name: "站点健康检测", - description: "已启用站点逐个检测首页连通与登录态,结果显示在站点页", + description: "已启用站点逐个检测首页连通与登录态,结果显示在站点页", group: "collect", schedule: { type: "interval", everyMs: 3_600_000 }, firstRun: "after_interval", diff --git a/apps/server/src/modules/sources-identity/routes/decisions.ts b/apps/server/src/modules/sources-identity/routes/decisions.ts index ec4e3476..3465e513 100644 --- a/apps/server/src/modules/sources-identity/routes/decisions.ts +++ b/apps/server/src/modules/sources-identity/routes/decisions.ts @@ -154,7 +154,7 @@ export function decisionRoutes(app: ModuleApp, rt: () => SourcesRuntime) { .where("source_release_id", "=", d.subject_id) .executeTakeFirst(); if (!(pool?.candidates as PoolCandidate[] | undefined)?.[opt.input.candidateIndex]) { - throw new ApiException("conflict", "候选已变化,刷新后重试"); + throw new ApiException("conflict", "候选已变化,刷新后重试"); } await assignReleaseIdentity( rt(), diff --git a/apps/server/src/modules/sources-identity/routes/identity.ts b/apps/server/src/modules/sources-identity/routes/identity.ts index 15b37257..dfc1f7f7 100644 --- a/apps/server/src/modules/sources-identity/routes/identity.ts +++ b/apps/server/src/modules/sources-identity/routes/identity.ts @@ -112,7 +112,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "get", path: "/media-entities/{id}/detail", scope: "read", - summary: "作品详情:外部身份、别名、内容单元、字段来源与覆盖(旧 ID 自动跳转)", + summary: "作品详情:外部身份、别名、内容单元、字段来源与覆盖(旧 ID 自动跳转)", tags: T_ENT, params: IdParam, response: MediaEntityDetail, @@ -202,7 +202,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/media-entities/{id}/identities", scope: "organize.execute", - summary: "人工纠错外部映射:旧映射退役(留修订),新映射 accepted + 人工锁", + summary: "人工纠错外部映射:旧映射退役(留修订),新映射 accepted + 人工锁", tags: T_ID, params: IdParam, body: EntityIdentitySet, @@ -221,7 +221,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/media-entities/{id}/refresh", scope: "organize.execute", - summary: "刷新元数据(字段级;用户锁优先,不动路径)", + summary: "刷新元数据(字段级;用户锁优先,不动路径)", tags: T_ENT, params: IdParam, response: Accepted, @@ -248,7 +248,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "put", path: "/media-entities/{id}/fields/{field}", scope: "organize.execute", - summary: "覆盖作品字段并锁定(自动刷新不再改写)", + summary: "覆盖作品字段并锁定(自动刷新不再改写)", tags: T_ENT, params: FieldParam, body: FieldOverrideInput, @@ -501,7 +501,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "get", path: "/identity-pool", scope: "read", - summary: "待识别池(可按原始标题检索;候选不建正式作品)", + summary: "待识别池(可按原始标题检索;候选不建正式作品)", tags: T_ID, query: IdentityPoolQuery, response: cursorPage(IdentityPoolItem), @@ -600,7 +600,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/identity-pool/{id}/dismiss", scope: "organize.execute", - summary: "忽略待识别资源(不再自动复查)", + summary: "忽略待识别资源(不再自动复查)", tags: T_ID, params: IdParam, body: z.object({ note: z.string().max(2000).optional() }), diff --git a/apps/server/src/modules/sources-identity/routes/sources.ts b/apps/server/src/modules/sources-identity/routes/sources.ts index c60dee67..d691ab46 100644 --- a/apps/server/src/modules/sources-identity/routes/sources.ts +++ b/apps/server/src/modules/sources-identity/routes/sources.ts @@ -75,7 +75,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "get", path: "/site-definitions", scope: "read", - summary: "随产品发布的站点适配器定义(含是否已登记)", + summary: "随产品发布的站点适配器定义(含是否已登记)", tags: T_SITES, response: z.object({ items: z.array(SiteDefinitionView) }), }, @@ -138,7 +138,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { throw new ApiException("bad_request", `站点 ${body.key} 的适配器类型应为 ${adapterOf(def)}`); } if (!def && body.adapter !== "generic_rss") { - throw new ApiException("bad_request", `注册表中没有站点 ${body.key};未适配站点只能以 generic_rss 登记`); + throw new ApiException("bad_request", `注册表中没有站点 ${body.key};未适配站点只能以 generic_rss 登记`); } try { const row = await db() @@ -199,7 +199,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: `/sites/{id}/${enabled ? "enable" : "disable"}`, scope: "admin", - summary: enabled ? "开启站点" : "关闭站点(停止网络访问与新候选,历史资源仍可查)", + summary: enabled ? "开启站点" : "关闭站点(停止网络访问与新候选,历史资源仍可查)", tags: T_SITES, params: IdParam, response: Site, @@ -213,7 +213,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/sites/{id}/snapshot", scope: "admin", - summary: "立即同步站点快照(受站点限流)", + summary: "立即同步站点快照(受站点限流)", tags: T_SITES, params: IdParam, body: z.object({ maxPages: z.number().int().min(1).max(20).optional() }), @@ -236,7 +236,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "get", path: "/sites/{id}/snapshots", scope: "read", - summary: "站点快照/补查记录(覆盖范围与新鲜度)", + summary: "站点快照/补查记录(覆盖范围与新鲜度)", tags: T_SITES, params: IdParam, query: CursorQuery, @@ -264,7 +264,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "delete", path: "/sites/{id}", scope: "admin", - summary: "删除站点:停用后删账号与凭证;已抓到的资源和下载任务保留(标站点已关闭)", + summary: "删除站点:停用后删账号与凭证;已抓到的资源和下载任务保留(标站点已关闭)", tags: T_SITES, params: IdParam, response: OkResult, @@ -281,7 +281,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/sites/{id}/health-check", scope: "admin", - summary: "检测健康:首页连通 + 登录态,同步返回并写站点健康列", + summary: "检测健康:首页连通 + 登录态,同步返回并写站点健康列", tags: T_SITES, params: IdParam, response: SiteHealthResult, @@ -294,7 +294,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/sites/{id}/refresh-login", scope: "admin", - summary: "刷新登录:账密重新登录换 Cookie;Cookie / API Key 校验;成功后拉用户数据", + summary: "刷新登录:账密重新登录换 Cookie;Cookie / API Key 校验;成功后拉用户数据", tags: T_SITES, params: IdParam, body: SiteRefreshLoginInput, @@ -356,7 +356,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "put", path: "/sites/{id}/credentials", scope: "admin", - summary: "设置站点账号凭证(按 kind:password / cookie / api_key / passkey;存为 secret,不回显)", + summary: "设置站点账号凭证(按 kind:password / cookie / api_key / passkey;存为 secret,不回显)", tags: T_SITES, params: IdParam, body: SiteCredentialsPut, @@ -625,7 +625,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/feed-sources/{id}/reset", scope: "admin", - summary: "手动恢复:关闭熔断、清空退避(凭证已更新后使用)", + summary: "手动恢复:关闭熔断、清空退避(凭证已更新后使用)", tags: T_FEEDS, params: IdParam, response: FeedSource, @@ -713,7 +713,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/source-releases/{id}/identity", scope: "organize.execute", - summary: "人工指定资源的作品/内容单元(加人工锁,自动识别不再改写)", + summary: "人工指定资源的作品/内容单元(加人工锁,自动识别不再改写)", tags: T_RELEASES, params: IdParam, body: ReleaseIdentityAssign, diff --git a/apps/server/src/modules/sources-identity/routes/workbench.ts b/apps/server/src/modules/sources-identity/routes/workbench.ts index 23533241..ac471058 100644 --- a/apps/server/src/modules/sources-identity/routes/workbench.ts +++ b/apps/server/src/modules/sources-identity/routes/workbench.ts @@ -17,7 +17,7 @@ export function workbenchRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "get", path: "/sites/{id}/releases", scope: "read", - summary: "资源列表/搜索(快照优先,必要时实时补查;标注来源与更新时间)", + summary: "资源列表/搜索(快照优先,必要时实时补查;标注来源与更新时间)", tags: T, params: z.object({ id: Id }), query: WorkbenchReleaseQuery, @@ -31,7 +31,7 @@ export function workbenchRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "get", path: "/sites/{id}/releases/{rid}", scope: "read", - summary: "资源详情(refresh=true 时实时复核详情;失败保留既有数据)", + summary: "资源详情(refresh=true 时实时复核详情;失败保留既有数据)", tags: T, params: z.object({ id: Id, rid: Id }), query: z.object({ refresh: z.enum(["true", "false"]).optional() }), diff --git a/apps/server/src/modules/sources-identity/runtime.ts b/apps/server/src/modules/sources-identity/runtime.ts index 18e2946f..b4062a73 100644 --- a/apps/server/src/modules/sources-identity/runtime.ts +++ b/apps/server/src/modules/sources-identity/runtime.ts @@ -86,7 +86,7 @@ export class SourcesRuntime { if (/^[0-9a-f]{32}$/i.test(secret)) apiKey = secret; else accessToken = secret; } catch (err) { - this.ctx.logger.warn({ err: { message: (err as Error).message } }, "TMDB 密钥解密失败,按未配置处理"); + this.ctx.logger.warn({ err: { message: (err as Error).message } }, "TMDB 密钥解密失败,按未配置处理"); } } else { apiKey = env("TMDB_API_KEY"); diff --git a/apps/server/src/modules/sources-identity/service/entity-from-candidate.ts b/apps/server/src/modules/sources-identity/service/entity-from-candidate.ts index 13b1b8f1..27c9c5b0 100644 --- a/apps/server/src/modules/sources-identity/service/entity-from-candidate.ts +++ b/apps/server/src/modules/sources-identity/service/entity-from-candidate.ts @@ -41,14 +41,14 @@ export async function createEntityFromTmdbCandidate( if (existing) return describe(db, existing, false, 0); const client = await tmdb(); - if (!client) throw new CandidateError("not_configured", "未配置 TMDB,无法按候选建作品"); + if (!client) throw new CandidateError("not_configured", "未配置 TMDB,无法按候选建作品"); let detail: Awaited>; try { detail = await client.details(ref.kind, ref.externalId); } catch (e) { if (e instanceof TmdbError && e.kind === "notfound") throw new CandidateError("not_found", `TMDB 无此条目 ${ref.kind}/${ref.externalId}`); - if (e instanceof TmdbError) throw new CandidateError("upstream", `TMDB 查询失败:${e.kind}`); + if (e instanceof TmdbError) throw new CandidateError("upstream", `TMDB 查询失败:${e.kind}`); throw e; } diff --git a/apps/server/src/modules/sources-identity/service/feeds.ts b/apps/server/src/modules/sources-identity/service/feeds.ts index 121b6551..bbaf1af3 100644 --- a/apps/server/src/modules/sources-identity/service/feeds.ts +++ b/apps/server/src/modules/sources-identity/service/feeds.ts @@ -144,7 +144,7 @@ export async function ingestFeed( } const lastError = auth - ? { ...(state.lastError ?? {}), kind: "auth", message: "Feed 凭证失效或过期(401/403/过期提示),已停止自动重试" } + ? { ...(state.lastError ?? {}), kind: "auth", message: "Feed 凭证失效或过期(401/403/过期提示),已停止自动重试" } : (state.lastError ?? null); // 疑似缺口:本次 ok 且零重叠 → 记下;本次 ok 且衔接上 → 清空;抓取失败 → 保留上次状态 const gap: StoredCursor["gap"] = @@ -209,13 +209,13 @@ export async function ingestFeed( kind: "source_unreachable", severity: "warning", title: `Feed「${feed.name}」凭证失效`, - summary: "RSS 返回 401/403 或过期提示,已停止自动重试。更新 Feed 链接或站点账号凭证后恢复抓取。", + summary: "RSS 返回 401/403 或过期提示,已停止自动重试。更新 Feed 链接或站点账号凭证后恢复抓取。", subjectType: "feed_source", subjectId: feed.id, dedupeKey: `feed-auth:${feed.id}`, evidence: { feedId: feed.id, siteId: feed.site_id, siteAccountId: feed.site_account_id, error: lastError }, options: [ - { key: "reset", label: "已更新凭证,恢复抓取" }, + { key: "reset", label: "已更新凭证,恢复抓取" }, { key: "disable", label: "停用 Feed" }, ], }); diff --git a/apps/server/src/modules/sources-identity/service/identity-admin.ts b/apps/server/src/modules/sources-identity/service/identity-admin.ts index fb9ecd25..1409915b 100644 --- a/apps/server/src/modules/sources-identity/service/identity-admin.ts +++ b/apps/server/src/modules/sources-identity/service/identity-admin.ts @@ -34,16 +34,16 @@ async function entityForExternal(rt: SourcesRuntime, exec: Exec, ref: ExternalRe const hit = await entityByExternal(exec, ref); if (hit) return hit; if (ref.provider !== "tmdb") - throw new ApiException("bad_request", `未登记的外部身份 ${ref.provider}:${ref.externalId},只支持按 TMDB 建档`); + throw new ApiException("bad_request", `未登记的外部身份 ${ref.provider}:${ref.externalId},只支持按 TMDB 建档`); const tmdb = await rt.tmdb(); - if (!tmdb) throw new ApiException("precondition_failed", "未配置 TMDB,无法按外部身份建档"); + if (!tmdb) throw new ApiException("precondition_failed", "未配置 TMDB,无法按外部身份建档"); let detail: Awaited>; try { detail = await tmdb.details(ref.entityType, ref.externalId); } catch (e) { if (e instanceof TmdbError && e.kind === "notfound") throw new ApiException("not_found", `TMDB 无此条目 ${ref.entityType}/${ref.externalId}`); - if (e instanceof TmdbError) throw new ApiException("upstream_unavailable", `TMDB 查询失败:${e.kind}`); + if (e instanceof TmdbError) throw new ApiException("upstream_unavailable", `TMDB 查询失败:${e.kind}`); throw e; } const r = await createEntityFromTmdb(exec, detail, { @@ -241,7 +241,7 @@ export async function setEntityIdentity(rt: SourcesRuntime, entityId: string, in if (!id) throw new ApiException("not_found", "作品不存在"); const other = await entityByExternal(trx, input); if (other && other !== id) { - throw new ApiException("conflict", `该外部身份已属于作品 ${other}。如果是重复作品,改用合并`); + throw new ApiException("conflict", `该外部身份已属于作品 ${other}。如果是重复作品,改用合并`); } if (other === id) return { mediaEntityId: id, changed: false }; const olds = await trx @@ -290,7 +290,7 @@ export async function updateExternalIdentity( .executeTakeFirst(); if (!row) throw new ApiException("not_found", "外部身份不存在"); if (row.revision !== input.expectedRevision) { - throw new ApiException("precondition_failed", `修订号已变化(当前 ${row.revision})`); + throw new ApiException("precondition_failed", `修订号已变化(当前 ${row.revision})`); } const rev = Math.max(row.revision + 1, await nextRevision(trx, id)); const patch: Record = { revision: rev }; @@ -365,7 +365,7 @@ export async function mergeEntities( if (!from) throw new ApiException("not_found", "源作品不存在"); if (!intoResolved) throw new ApiException("not_found", "目标作品不存在"); if (from.status === "merged") throw new ApiException("conflict", "源作品已被合并"); - if (intoResolved === fromId) throw new ApiException("conflict", "目标作品已合并到源作品,会形成环"); + if (intoResolved === fromId) throw new ApiException("conflict", "目标作品已合并到源作品,会形成环"); // 外部身份:目标已有同 provider+类型 accepted 的,源的退役;否则迁到目标 const idents = await trx diff --git a/apps/server/src/modules/sources-identity/service/resolve.ts b/apps/server/src/modules/sources-identity/service/resolve.ts index 1cb3f986..98735a8d 100644 --- a/apps/server/src/modules/sources-identity/service/resolve.ts +++ b/apps/server/src/modules/sources-identity/service/resolve.ts @@ -511,13 +511,13 @@ function manualIdentityOptions(sourceReleaseId: string) { { key: "manual_tmdb", label: "手动指定 TMDB ID", - description: "按 TMDB 电影或剧集 ID 建档并关联,加人工锁", + description: "按 TMDB 电影或剧集 ID 建档并关联,加人工锁", input: { sourceReleaseId, requires: "tmdb" }, }, { key: "local_entity", label: "选本地作品", - description: "关联到已有的本地作品,加人工锁", + description: "关联到已有的本地作品,加人工锁", input: { sourceReleaseId, requires: "mediaEntityId" }, }, ]; @@ -592,7 +592,7 @@ async function toPool( const d = await openDecision(ctx, trx, { kind: "identity_ambiguous", severity: "info", - title: `待确认身份:${release.title}`, + title: `待确认身份:${release.title}`, summary: `${candidates.length} 个候选证据等级相近(${detailText(detail)})。选择一个或忽略`, subjectType: "source_release", subjectId: release.id, @@ -615,10 +615,10 @@ async function toPool( const d = await openDecision(ctx, trx, { kind: "identity_unresolved", severity: "info", - title: `待识别:${release.title}`, + title: `待识别:${release.title}`, summary: reason === "lookup_failed" - ? `元数据查询多次失败(${detail}),已停止自动复查。手动指定作品或忽略` + ? `元数据查询多次失败(${detail}),已停止自动复查。手动指定作品或忽略` : `没有匹配的作品(${detailText(detail)})。手动指定 TMDB ID、选本地作品或忽略`, subjectType: "source_release", subjectId: release.id, diff --git a/apps/server/src/modules/sources-identity/service/snapshot.ts b/apps/server/src/modules/sources-identity/service/snapshot.ts index 5217798b..42854bce 100644 --- a/apps/server/src/modules/sources-identity/service/snapshot.ts +++ b/apps/server/src/modules/sources-identity/service/snapshot.ts @@ -91,7 +91,7 @@ async function onAuthError(rt: SourcesRuntime, trx: Exec, site: SiteRow, e: Site subjectId: site.id, dedupeKey: `site-auth:${site.id}`, evidence: { siteId: site.id, kind: e.kind, httpStatus: e.httpStatus ?? null }, - options: [{ key: "reset", label: "已更新凭证,恢复访问" }], + options: [{ key: "reset", label: "已更新凭证,恢复访问" }], }); // 有密码的账号:自动重登一次(同账号 10 分钟内最多一次),成功后关闭本待处理并复位站点健康 await enqueueRelogin(rt, site.id); diff --git a/apps/server/src/modules/sources-identity/service/workbench.ts b/apps/server/src/modules/sources-identity/service/workbench.ts index 9adffafc..b89e3e8e 100644 --- a/apps/server/src/modules/sources-identity/service/workbench.ts +++ b/apps/server/src/modules/sources-identity/service/workbench.ts @@ -41,7 +41,7 @@ export async function liveSearch( const st = liveState(rt); const key = `${site.id}|${normalizeTitle(q) || q.toLowerCase()}`; const failed = st.failedUntil.get(key); - if (failed && failed.until > rt.now().getTime()) return { error: `${failed.error}(退避中)`, coalesced: false }; + if (failed && failed.until > rt.now().getTime()) return { error: `${failed.error}(退避中)`, coalesced: false }; const running = st.inflight.get(key); if (running) return { ...(await running), coalesced: true }; const p = (async () => { @@ -149,7 +149,7 @@ export async function listSiteReleases( let coalesced = false; const wantLive = q.live === "true" || (q.live !== "false" && offset === 0 && !!q.q && (rows.length === 0 || stale)); if (wantLive && q.q) { - if (!site.enabled) liveError = "站点已关闭,只展示历史数据"; + if (!site.enabled) liveError = "站点已关闭,只展示历史数据"; else { const r = await liveSearch(rt, site, q.q); live = r.error === null; diff --git a/apps/server/src/preflight.ts b/apps/server/src/preflight.ts index b2abc473..a554bd06 100644 --- a/apps/server/src/preflight.ts +++ b/apps/server/src/preflight.ts @@ -24,7 +24,7 @@ export async function runPreflight({ config, db, uid = Deno.uid() }: PreflightDe const prod = config.NODE_ENV === "production"; if (prod && PLACEHOLDER.test(config.SECRET_MASTER_KEY)) { - problems.push({ check: "secret", message: "SECRET_MASTER_KEY 仍是占位符,生产环境必须换成随机串" }); + problems.push({ check: "secret", message: "SECRET_MASTER_KEY 仍是占位符,生产环境必须换成随机串" }); } if (prod && uid === 0) { problems.push({ check: "user", message: "生产环境不允许以 root 运行" }); @@ -32,7 +32,7 @@ export async function runPreflight({ config, db, uid = Deno.uid() }: PreflightDe try { await sql`select 1`.execute(db); } catch (err) { - problems.push({ check: "db", message: `数据库不可达: ${(err as Error).message}` }); + problems.push({ check: "db", message: `数据库不可达:${(err as Error).message}` }); } if (config.WEB_ROOT) { try { @@ -109,7 +109,7 @@ export async function checkStorage(opts: { await Deno.mkdir(dir, { recursive: true }); await probeWrite(dir); } catch (e) { - problems.push({ check: "data_dir", message: `数据目录不可写: ${dir}(${errText(e)});检查卷挂载与 PUID/PGID` }); + problems.push({ check: "data_dir", message: `数据目录不可写:${dir}(${errText(e)});检查卷挂载与 PUID/PGID` }); } } for (const m of opts.mounts) { @@ -118,13 +118,13 @@ export async function checkStorage(opts: { if (!st.isDirectory) throw Object.assign(new Error("不是目录"), { code: "ENOTDIR" }); for await (const _ of Deno.readDir(m.path)) break; } catch (e) { - warnings.push({ ...m, reason: `不可读: ${errText(e)}` }); + warnings.push({ ...m, reason: `不可读:${errText(e)}` }); continue; } try { await probeWrite(m.path); } catch (e) { - warnings.push({ ...m, reason: `不可写: ${errText(e)}` }); + warnings.push({ ...m, reason: `不可写:${errText(e)}` }); } } return { problems, warnings }; diff --git a/apps/server/test/file-manager/w5a2.test.ts b/apps/server/test/file-manager/w5a2.test.ts index 7393392e..63b88544 100644 --- a/apps/server/test/file-manager/w5a2.test.ts +++ b/apps/server/test/file-manager/w5a2.test.ts @@ -244,7 +244,7 @@ describe.skipIf(skip)("files W5-A2", () => { const tvTargets = tv.files.map((f: { target: string | null }) => f.target); expect(tvTargets.some((t: string | null) => t?.endsWith("Season 2/剧 - S02E03-E04 - 第 3-4 集.mkv"))).toBe(true); // 非连续集号不应用(只按季覆写,文件名无集号 → 计划排除),其余覆写不进 warnings - expect(tv.warnings).toEqual([expect.stringMatching(/weird-b\.mkv: 集号 5,7 不连续/)]); + expect(tv.warnings).toEqual([expect.stringMatching(/weird-b\.mkv:集号 5,7 不连续/)]); expect(mv.warnings).toEqual([expect.stringMatching(/电影不适用季\/集覆写/)]); expect(mv).toMatchObject({ action: "copy", slotId: slot.id, targetDir: join(h.lib, "picked") }); expect(mv.files[0].target).toBe(join(h.lib, "picked", "Film (2019) - 1080p.mkv")); diff --git a/apps/web/app/api/feeds.ts b/apps/web/app/api/feeds.ts index cb3e5767..04ee3d4c 100644 --- a/apps/web/app/api/feeds.ts +++ b/apps/web/app/api/feeds.ts @@ -78,10 +78,10 @@ export function useFetchFeedNow() { export function feedGapHint(f: FeedSource, now = Date.now()): string | null { if (f.possibleGap) { const at = new Date(f.possibleGap.detectedAt).toLocaleString("zh-CN", { hour12: false }); - return `疑似缺口(${at} 检测,${f.possibleGap.backfillQueued ? "已排补漏" : "Feed 未关联站点,无法自动补漏"})`; + return `疑似缺口(${at} 检测,${f.possibleGap.backfillQueued ? "已排补漏" : "Feed 未关联站点,无法自动补漏"})`; } if (!f.enabled || !f.lastSuccessAt) return null; const gapMs = now - new Date(f.lastSuccessAt).getTime(); - if (gapMs > f.pollIntervalSec * 1000 * 3) return "距上次成功已超过 3 个周期,可能漏项"; + if (gapMs > f.pollIntervalSec * 1000 * 3) return "距上次成功已超过 3 个周期,可能漏项"; return null; } diff --git a/apps/web/app/api/parse.ts b/apps/web/app/api/parse.ts index 2ddb4815..84210f4e 100644 --- a/apps/web/app/api/parse.ts +++ b/apps/web/app/api/parse.ts @@ -13,7 +13,7 @@ export async function apiParsed(schema: S, path: string, ar const issue = parsed.error.issues[0]; throw new ApiRequestError( "internal" satisfies ApiErrorCode, - `响应格式不符合接口定义:${issue ? `${issue.path.join(".")} ${issue.message}` : "未知字段"}`, + `响应格式不符合接口定义:${issue ? `${issue.path.join(".")} ${issue.message}` : "未知字段"}`, ); } return parsed.data; diff --git a/apps/web/app/api/tasks.ts b/apps/web/app/api/tasks.ts index 7fad02e8..ab9b9900 100644 --- a/apps/web/app/api/tasks.ts +++ b/apps/web/app/api/tasks.ts @@ -25,7 +25,7 @@ const DecisionPage = cursorPage(Decision); const RESOLUTION_LABEL = { unresolved: "待识别", resolved: "已识别", - ambiguous: "有歧义,待确认", + ambiguous: "有歧义,待确认", unidentifiable: "无法识别", rejected: "已排除", } as const; @@ -162,8 +162,8 @@ export function buildTimeline(input: { at: release.firstSeenAt, title: release.resolutionStatus === "resolved" - ? "识别结果:已关联本地作品" - : `识别结果:${RESOLUTION_LABEL[release.resolutionStatus]}`, + ? "识别结果:已关联本地作品" + : `识别结果:${RESOLUTION_LABEL[release.resolutionStatus]}`, tone: release.resolutionStatus === "resolved" ? "success" : "warning", }); } @@ -181,7 +181,7 @@ export function buildTimeline(input: { key: "plan-status", stage: "plan", at: plan.updatedAt, - title: `计划状态:${PLAN_STATUS_LABEL[plan.status]}`, + title: `计划状态:${PLAN_STATUS_LABEL[plan.status]}`, detail: plan.statusReason, tone: plan.status === "failed" ? "destructive" : plan.status === "expired" ? "warning" : "neutral", }); @@ -199,7 +199,7 @@ export function buildTimeline(input: { key: "dl-pause", stage: "download", at: task.updatedAt, - title: task.pauseReason === "threshold" ? "缓存达到硬阈值,已暂停" : `已暂停(${task.pauseReason})`, + title: task.pauseReason === "threshold" ? "缓存达到硬阈值,已暂停" : `已暂停(${task.pauseReason})`, tone: "warning", ongoing: task.status === "paused", }); @@ -208,7 +208,7 @@ export function buildTimeline(input: { key: "dl-stall", stage: "download", at: task.noProgressSince, - title: "无进展,不自动换源", + title: "无进展,不自动换源", tone: "warning", ongoing: task.status === "stalled", }); @@ -250,7 +250,7 @@ export function buildTimeline(input: { key: `dec-${d.id}`, stage: "agent", at: d.createdAt, - title: `待处理:${d.title}`, + title: `待处理:${d.title}`, detail: d.summary, tone: d.severity === "critical" ? "destructive" : "warning", href: "/console/inbox", diff --git a/apps/web/app/components/agentui/file-diff.tsx b/apps/web/app/components/agentui/file-diff.tsx index 2f1bd1dc..8a678be1 100644 --- a/apps/web/app/components/agentui/file-diff.tsx +++ b/apps/web/app/components/agentui/file-diff.tsx @@ -85,7 +85,9 @@ export function FileDiff({ {l.type === "added" ? "+" : l.type === "removed" ? "−" : " "} - {l.type === "added" ? "新增:" : l.type === "removed" ? "移除:" : ""} + + {l.type === "added" ? "新增:" : l.type === "removed" ? "移除:" : ""} + {l.content} {l.note ? {l.note} : null} diff --git a/apps/web/app/components/agentui/message-scroller.tsx b/apps/web/app/components/agentui/message-scroller.tsx index 537fb6b1..46c53bf6 100644 --- a/apps/web/app/components/agentui/message-scroller.tsx +++ b/apps/web/app/components/agentui/message-scroller.tsx @@ -171,7 +171,7 @@ export function MessageScroller({ > {jumpLabel} - {busy ? (仍在输出) : null} + {busy ? (仍在输出) : null} ) : null} diff --git a/apps/web/app/components/agentui/tool-approval.tsx b/apps/web/app/components/agentui/tool-approval.tsx index 1be0ac9e..0290f843 100644 --- a/apps/web/app/components/agentui/tool-approval.tsx +++ b/apps/web/app/components/agentui/tool-approval.tsx @@ -68,7 +68,7 @@ export interface ToolApprovalProps { export function ToolApproval({ tool, - title = "允许执行这个工具?", + title = "允许执行这个工具?", description, parameters = [], status = "pending", @@ -134,7 +134,7 @@ export function ToolApproval({ {badges ?
{badges}
: null} {rejected && reason ? ( -

原因:{reason}

+

原因:{reason}

) : null} {description ?

{description}

: null} {rejected && explanation ? ( diff --git a/apps/web/app/components/console/inbox-bell.tsx b/apps/web/app/components/console/inbox-bell.tsx index d0bd0ff8..57a3cce5 100644 --- a/apps/web/app/components/console/inbox-bell.tsx +++ b/apps/web/app/components/console/inbox-bell.tsx @@ -89,7 +89,7 @@ export function InboxBell() { {list.isPending ? (

加载中…

) : list.isError ? ( -

加载失败,稍后再试

+

加载失败,稍后再试

) : groups.length === 0 ? (
diff --git a/apps/web/app/components/console/nav.ts b/apps/web/app/components/console/nav.ts index 5be82b7d..9bd9e489 100644 --- a/apps/web/app/components/console/nav.ts +++ b/apps/web/app/components/console/nav.ts @@ -51,13 +51,13 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/tasks", label: "任务", icon: ListTodo, - description: "每次运行都是一条任务:定时、事件、手动、Agent 触发都在这里,含定时任务计划", + description: "每次运行都是一条任务:定时、事件、手动、Agent 触发都在这里,含定时任务计划", }, { to: "/console/agent", label: "Agent 会话", icon: Bot, - description: "交代目标,Agent 在授权内执行并汇报", + description: "交代目标,Agent 在授权内执行并汇报", }, ], }, @@ -68,7 +68,7 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/sites", label: "站点", icon: Globe, - description: "站点、凭证、账号数据与健康;资源、快照与 Feed", + description: "站点、凭证、账号数据与健康;资源、快照与 Feed", }, { to: "/console/sources", label: "RSS 订阅", icon: Rss, description: "全部 Feed 与抓取状态" }, ], @@ -94,7 +94,7 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/files", label: "文件管理", icon: FolderTree, - description: "浏览登记目录,改名、移动、删除与整理", + description: "浏览登记目录,改名、移动、删除与整理", }, { to: "/console/library", diff --git a/apps/web/app/components/downloads/business-tasks.tsx b/apps/web/app/components/downloads/business-tasks.tsx index 28791665..7d9d8afd 100644 --- a/apps/web/app/components/downloads/business-tasks.tsx +++ b/apps/web/app/components/downloads/business-tasks.tsx @@ -103,7 +103,7 @@ export function useTaskAction() { body: op === "cancel" ? { reason: "控制台取消" } : {}, }).then((d) => Task.parse(d)), onSuccess: (_t, v) => { - toast.success(v.op === "cancel" ? "已请求取消,后续步骤不再执行" : "已重新排队"); + toast.success(v.op === "cancel" ? "已请求取消,后续步骤不再执行" : "已重新排队"); void qc.invalidateQueries({ queryKey: ["tasks"] }); }, onError: (e) => toast.error(errorMessage(e)), diff --git a/apps/web/app/components/downloads/business-timeline.tsx b/apps/web/app/components/downloads/business-timeline.tsx index d9c5003d..74a259cd 100644 --- a/apps/web/app/components/downloads/business-timeline.tsx +++ b/apps/web/app/components/downloads/business-timeline.tsx @@ -37,7 +37,7 @@ export function BusinessTimeline({ events, loading }: { events: TimelineEvent[]; })}
{ongoing.length ? ( -

当前:{ongoing.map((e) => e.title).join(";")}

+

当前:{ongoing.map((e) => e.title).join(";")}

) : null} {loading && !events.length ? ( diff --git a/apps/web/app/components/downloads/cache-volumes.tsx b/apps/web/app/components/downloads/cache-volumes.tsx index 83327f25..56afaffb 100644 --- a/apps/web/app/components/downloads/cache-volumes.tsx +++ b/apps/web/app/components/downloads/cache-volumes.tsx @@ -76,8 +76,8 @@ function VolumeBar({ v }: { v: Volume }) { 保留锁定 {formatBytes(v.retentionLockedBytes, 1)}
- {pct === null ? 读不到磁盘状态,已暂停新提交 : null} - {tripped ? 已达硬阈值,停止新提交 : null} + {pct === null ? 读不到磁盘状态,已暂停新提交 : null} + {tripped ? 已达硬阈值,停止新提交 : null} {v.pausedByThreshold ? 阈值暂停 {v.pausedByThreshold} 个任务 : null}
{v.notes.map((n) => ( @@ -112,8 +112,8 @@ export function CacheVolumes() { 立即清理 } - title="按规则清理下载缓存?" - description="只清理已入库、且满足保留期与 H&R 的纳管任务,其余不动。" + title="按规则清理下载缓存?" + description="只清理已入库、且满足保留期与 H&R 的纳管任务,其余不动。" confirmLabel="清理" onConfirm={() => cleanup.mutateAsync(undefined).then( diff --git a/apps/web/app/components/downloads/download-detail.tsx b/apps/web/app/components/downloads/download-detail.tsx index 5dd65d30..9e5ac01d 100644 --- a/apps/web/app/components/downloads/download-detail.tsx +++ b/apps/web/app/components/downloads/download-detail.tsx @@ -69,7 +69,7 @@ export function PauseResume({ t, size = "sm" }: { t: DownloadTask; size?: "sm" | variant="outline" size={size} disabled={act.isPending} - title={threshold ? "阈值暂停,缓存回落到恢复水位后自动恢复" : undefined} + title={threshold ? "阈值暂停,缓存回落到恢复水位后自动恢复" : undefined} onKeyDown={(e) => e.stopPropagation()} onClick={(e) => { e.stopPropagation(); @@ -117,7 +117,7 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { size="sm" onClick={() => openAgentDrawer({ - label: `下载:${task.name}`, + label: `下载:${task.name}`, downloadTaskId: task.id, goal: task.status === "stalled" || task.status === "failed" ? "排查这个下载为什么没有进展" : "", }) @@ -131,12 +131,12 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { {task.pauseReason === "threshold" ? (

- 缓存卷达到硬阈值,任务已暂停,水位回落到恢复线后自动恢复。手动恢复和 Agent 都不能绕过阈值。 + 缓存卷达到硬阈值,任务已暂停,水位回落到恢复线后自动恢复。手动恢复和 Agent 都不能绕过阈值。

) : null} {task.noProgressSince ? (

- 自 {formatDateTime(task.noProgressSince)} 起无进展,已记入待处理,不会自动换源。 + 自 {formatDateTime(task.noProgressSince)} 起无进展,已记入待处理,不会自动换源。

) : null} diff --git a/apps/web/app/components/files/dialogs.tsx b/apps/web/app/components/files/dialogs.tsx index a573a117..91bc99a6 100644 --- a/apps/web/app/components/files/dialogs.tsx +++ b/apps/web/app/components/files/dialogs.tsx @@ -184,14 +184,14 @@ export function MkdirDialog({ // ─── 重命名 ───────────────────────────────────────────────────── const SUGGEST_REASON: Record = { - identity_uncertain: "识别不可信,没有推荐名", + identity_uncertain: "识别不可信,没有推荐名", }; const TEMPLATE_SOURCE: Record = { entity: "作品覆盖", root: "库根", global: "全局", override: "覆写" }; function suggestReasonText(r: string | null): string { if (!r) return "没有推荐名"; if (SUGGEST_REASON[r]) return SUGGEST_REASON[r] as string; - if (r.startsWith("naming_failed")) return `命名模板渲染失败:${r.replace(/^naming_failed:\s*/, "")}`; + if (r.startsWith("naming_failed")) return `命名模板渲染失败:${r.replace(/^naming_failed:\s*/, "")}`; return r; } @@ -291,13 +291,13 @@ export function RenameDialog({ ev.preventDefault(); if (problem) return setErr(problem); if (!changed) return onClose(); - if (taken) return setErr("同名已存在,不会覆盖"); + if (taken) return setErr("同名已存在,不会覆盖"); exec.mutate( { idempotencyKey: key, root: root.id, path: entry.path, newName: name.trim(), withSidecars, dryRun: false }, { onSuccess: (r) => { toast.success( - `已重命名${r.renames.length > 1 ? `(含 ${r.renames.length - 1} 个附属文件)` : ""}`, + `已重命名${r.renames.length > 1 ? `(含 ${r.renames.length - 1} 个附属文件)` : ""}`, r.recordId ? { action: { @@ -313,9 +313,9 @@ export function RenameDialog({ const reason = filesErrorReason(e); setErr( reason === "exists" - ? "同名已存在,不会覆盖" + ? "同名已存在,不会覆盖" : reason === "download_active" - ? "做种中,改名会让下载器找不到文件" + ? "做种中,改名会让下载器找不到文件" : filesErrorText(e), ); }, @@ -338,7 +338,7 @@ export function RenameDialog({
{pv.assetLocations > 0 ? ( - 同步 {pv.assetLocations} 条资产位置,写一条整理记录。 + 同步 {pv.assetLocations} 条资产位置,写一条整理记录。 ) : null} {pv.bindings.length > 0 ? ( - 这是作品或季的绑定目录。改名后旧绑定标为已替代,之后入库用新目录。 + 这是作品或季的绑定目录。改名后旧绑定标为已替代,之后入库用新目录。 ) : null} ) : null} @@ -532,7 +532,7 @@ export function TransferDialog({ onError: (e) => { const reason = filesErrorReason(e); if (reason === "plan_stale") { - setErr("源文件在预览后有变化,已重新预览"); + setErr("源文件在预览后有变化,已重新预览"); runPreview(target, mode); } else setErr(filesErrorText(e)); }, @@ -613,13 +613,15 @@ export function TransferDialog({ {plan.freeBytes != null ? ` · 剩余 ${formatBytes(plan.freeBytes, 1)}` : ""} {conflicts.length ? ( - 冲突 {conflicts.length}(跳过,不覆盖) + 冲突 {conflicts.length}(跳过,不覆盖) + ) : null} + {plan.pairs.some((p) => p.registered) ? ( + 含已登记资产,执行后同步位置 ) : null} - {plan.pairs.some((p) => p.registered) ? 含已登记资产,执行后同步位置 : null}
{hnrBlocked ? ( - {plan.hnrBlocks.length} 项做种中且 H&R 未满足,不能移动。改用复制,或等 H&R 满足后再移动。 + {plan.hnrBlocks.length} 项做种中且 H&R 未满足,不能移动。改用复制,或等 H&R 满足后再移动。 } - title={`删除别名「${a.alias}」?`} - description="删除后不再用这个别名匹配作品,已确认的外部身份不受影响。" + title={`删除别名「${a.alias}」?`} + description="删除后不再用这个别名匹配作品,已确认的外部身份不受影响。" confirmLabel="删除" destructive onConfirm={async () => { diff --git a/apps/web/app/components/library/identity-entity-picker.tsx b/apps/web/app/components/library/identity-entity-picker.tsx index 00e113a9..19cba61b 100644 --- a/apps/web/app/components/library/identity-entity-picker.tsx +++ b/apps/web/app/components/library/identity-entity-picker.tsx @@ -14,7 +14,7 @@ export type PickedEntity = { id: string; title: string; year: number | null; kin export function EntityPicker({ value, onChange, - placeholder = "搜索本地作品(标题/原名)", + placeholder = "搜索本地作品(标题/原名)", id, }: { value: PickedEntity | null; diff --git a/apps/web/app/components/library/identity-pool-sheet.tsx b/apps/web/app/components/library/identity-pool-sheet.tsx index 6431cb95..26e6ef0a 100644 --- a/apps/web/app/components/library/identity-pool-sheet.tsx +++ b/apps/web/app/components/library/identity-pool-sheet.tsx @@ -127,7 +127,7 @@ export function PoolSheet({ item, onClose }: { item: IdentityPoolItem | null; on > {queued ? (

- 已进待处理,在那里选择候选或忽略。 + 已进待处理,在那里选择候选或忽略。

) : null} - + ); } diff --git a/apps/web/app/components/library/identity-views.tsx b/apps/web/app/components/library/identity-views.tsx index bf9ccb6f..70b67f81 100644 --- a/apps/web/app/components/library/identity-views.tsx +++ b/apps/web/app/components/library/identity-views.tsx @@ -58,7 +58,7 @@ export function PoolTab() {

{r.release.title}

- {r.release.subtitle ? `${r.release.subtitle} · ` : ""}解析:{querySummary(r.query)} + {r.release.subtitle ? `${r.release.subtitle} · ` : ""}解析:{querySummary(r.query)}

), @@ -149,7 +149,7 @@ export function PoolTab() { empty={{ icon: SearchX, title: filtered ? "没有符合条件的待识别资源" : "没有待识别资源", - description: "证据不足或冲突的资源会出现在这里,有新证据时自动重试。", + description: "证据不足或冲突的资源会出现在这里,有新证据时自动重试。", }} hasMore={pool.hasNextPage} onLoadMore={() => pool.fetchNextPage()} diff --git a/apps/web/app/components/library/labels.ts b/apps/web/app/components/library/labels.ts index 8ccc3d7a..9a4c1340 100644 --- a/apps/web/app/components/library/labels.ts +++ b/apps/web/app/components/library/labels.ts @@ -128,7 +128,7 @@ export const CANDIDATE_DECISION: Record = { reject: { label: "拒绝", tone: "destructive" }, unknown_field: { label: "字段未知", tone: "warning" }, skipped: { label: "跳过", tone: "neutral" }, - deferred: { label: "延后:已有计划在途", tone: "warning" }, + deferred: { label: "延后:已有计划在途", tone: "warning" }, }; export const DECISION_KIND: Record = { @@ -174,7 +174,7 @@ export const POOL_STATUS: Record = { }; export const PREF_KIND: Record = { - soft: { label: "软偏好", hint: "影响排序,不排除" }, + soft: { label: "软偏好", hint: "影响排序,不排除" }, hard: { label: "硬规则", hint: "命中即排除" }, version: { label: "版本偏好", hint: "同一作品内选哪种版本" }, target: { label: "版本目标", hint: "为作品新建一个版本目标" }, @@ -214,7 +214,7 @@ export const PRIORITY: Record = { export const SUB_STATUS: Record = { active: { label: "进行中", tone: "success" }, paused: { label: "已暂停", tone: "warning" }, - pending: { label: "待定(元数据未就绪)", tone: "warning" }, + pending: { label: "待定(元数据未就绪)", tone: "warning" }, completed: { label: "已完成", tone: "neutral" }, cancelled: { label: "已取消", tone: "neutral" }, }; @@ -268,7 +268,7 @@ export const opts = (rec: Record`)→ 中文;不认识的原样显示 */ const SLOT_REASON: Record = { "blocked:no_main_video": "没有可入库的主视频", - "deferred:slot_busy": "已有计划在途,等它结束再评估", + "deferred:slot_busy": "已有计划在途,等它结束再评估", verified: "已核验入库", verified_partial: "部分核验入库", hard_conditions_met: "硬条件全部满足", diff --git a/apps/web/app/components/library/preferences-form.tsx b/apps/web/app/components/library/preferences-form.tsx index f31434d6..838041ce 100644 --- a/apps/web/app/components/library/preferences-form.tsx +++ b/apps/web/app/components/library/preferences-form.tsx @@ -31,7 +31,7 @@ const jsonObject = (name: string) => } catch { // 落到下面的错误 } - ctx.addIssue({ code: "custom", message: `${name}必须是 JSON 对象,如 {"mediaKinds":["movie"]}` }); + ctx.addIssue({ code: "custom", message: `${name}必须是 JSON 对象,如 {"mediaKinds":["movie"]}` }); return z.NEVER; }); @@ -108,8 +108,8 @@ export function PreferenceFormDialog({ {tag ? "编辑偏好" : "添加偏好"} {tag - ? "修改作用范围或规则会留下旧语义的抑制记录,Agent 不会按旧证据加回。" - : "手动添加等同对话确认。之前取消过的偏好,重新添加会解除抑制。"} + ? "修改作用范围或规则会留下旧语义的抑制记录,Agent 不会按旧证据加回。" + : "手动添加等同对话确认。之前取消过的偏好,重新添加会解除抑制。"} @@ -120,7 +120,7 @@ export function PreferenceFormDialog({ value={draft.label} aria-invalid={!!errors.label} onChange={(e) => set("label", e.target.value)} - placeholder="如:偏爱科幻" + placeholder="如:偏爱科幻" />
@@ -157,7 +157,7 @@ export function PreferenceFormDialog({ onChange={(e) => set("scope", e.target.value)} /> - +