diff --git a/apps/server/src/modules/acquisition-organize/__tests__/organize-guard.test.ts b/apps/server/src/modules/acquisition-organize/__tests__/organize-guard.test.ts index 93a5a4c4..55e57b28 100644 --- a/apps/server/src/modules/acquisition-organize/__tests__/organize-guard.test.ts +++ b/apps/server/src/modules/acquisition-organize/__tests__/organize-guard.test.ts @@ -72,7 +72,7 @@ describe.skipIf(!hasPg)("手动整理守卫:路径 / Move 授权 / 做种义务" for (const dryRun of [true, false]) { const r = await post({ sourcePaths: [outFile], mediaEntityId: movie, action: "copy", dryRun }); expect(r.status).toBe(400); - expect(r.body.error.message).toContain("不在已登记的库根或下载器路径映射之下"); + expect(r.body.error.message).toContain("不在已登记的媒体库目录或下载器路径映射之下"); } // `..` 即使规范化后仍在根内也拒绝 const inside = await file("Dots.2020.1080p.BluRay-X.mkv"); diff --git a/apps/server/src/modules/acquisition-organize/capabilities.ts b/apps/server/src/modules/acquisition-organize/capabilities.ts index 0684a4ed..f5e1da62 100644 --- a/apps/server/src/modules/acquisition-organize/capabilities.ts +++ b/apps/server/src/modules/acquisition-organize/capabilities.ts @@ -77,7 +77,7 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () => return { items } as never; }); - // AG-3:订阅摘要(分组计数 + 条目),版本目标只给总数 / 已满足数,细节用 inventory.query + // AG-3:订阅摘要(分组计数 + 条目),收录版本只给总数 / 已满足数,细节用 inventory.query registry.register(capabilitySpec("subscription.list"), async (input) => { const db = deps().ctx.db; let q = db @@ -253,7 +253,7 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () => const d = deps(); const db = d.ctx.db; const slot = await db.selectFrom("acquisition_slot").selectAll().where("id", "=", input.slotId).executeTakeFirst(); - if (!slot) throw new CapabilityError("not_found", "版本目标不存在"); + if (!slot) throw new CapabilityError("not_found", "收录版本不存在"); const rel = await db .selectFrom("source_release") .select(["id", "media_entity_id"]) @@ -285,7 +285,7 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () => 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: "未生成计划:版本目标正忙或条件不满足" }); + rejected.push({ code: "not_planned", message: "未生成计划:收录版本正忙或条件不满足" }); } const fresh = await db .selectFrom("acquisition_slot") @@ -324,8 +324,8 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () => .where("id", "=", plan.slot_id) .forUpdate() .executeTakeFirstOrThrow(); - if (slot.revision + 1 !== plan.expected_revision) return "版本目标已变化,重新生成计划"; - if (BUSY.includes(slot.status)) return `版本目标正忙(${slot.status})`; + 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" }); st = transition(st, { type: "reserve" }); @@ -348,7 +348,7 @@ export function registerAoCapabilities(registry: CapabilityRegistry, deps: () => return null; }) .catch((err) => { - if ((err as { code?: string }).code === "23505") return "版本目标已被其他计划预留"; + if ((err as { code?: string }).code === "23505") return "收录版本已被其他计划预留"; throw err; }); if (ok) throw new CapabilityError("conflict", ok); diff --git a/apps/server/src/modules/acquisition-organize/index.ts b/apps/server/src/modules/acquisition-organize/index.ts index 8313eb33..0b9e23bd 100644 --- a/apps/server/src/modules/acquisition-organize/index.ts +++ b/apps/server/src/modules/acquisition-organize/index.ts @@ -110,7 +110,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se key: "downloads.sync", defaults: { name: "qB 同步", - description: "同步下载器任务状态,按缓存阈值暂停或恢复", + description: "同步下载器任务状态;下载盘到暂停线时暂停,降到恢复线后恢复", group: "download", schedule: every(syncMs), }, @@ -129,8 +129,8 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se registerScheduledJob(ctx, { key: "cache.threshold", defaults: { - name: "缓存阈值检查", - description: "下载缓存超过硬阈值时入队清理", + name: "下载盘检查", + description: "下载盘超过暂停线时清理已入库的下载", group: "maintain", schedule: every(planMs), }, @@ -145,7 +145,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se key: "downloads.orchestrate", defaults: { name: "补提交与补入库", - description: "补投已预留未提交的下载、已完成未入库的任务", + description: "补交没提交成功的下载,补整理下载完但没入库的任务", group: "download", schedule: every(planMs), }, @@ -159,7 +159,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se key: "slots.reevaluate", defaults: { name: "到期重评", - description: "到了重评时间的收录位重新评估(忙时延后的发布、定时)", + description: "重新检查等待中的资源", group: "download", schedule: every(planMs), }, @@ -169,7 +169,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se key: "subscriptions.search", defaults: { name: "订阅周期搜索", - description: "到期的订阅主动搜索站点", + description: "到期的订阅去站点搜索", group: "collect", schedule: every(planMs), }, @@ -190,8 +190,8 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se registerScheduledJob(ctx, { key: "library.reconcile", defaults: { - name: "资产对账", - description: "核对媒体库文件与资产记录", + name: "文件核对", + description: "检查媒体库里的文件是否还在", group: "organize", schedule: every(reconcileMs), }, diff --git a/apps/server/src/modules/acquisition-organize/routes/assets.ts b/apps/server/src/modules/acquisition-organize/routes/assets.ts index 006ca3f3..d437c9c7 100644 --- a/apps/server/src/modules/acquisition-organize/routes/assets.ts +++ b/apps/server/src/modules/acquisition-organize/routes/assets.ts @@ -30,19 +30,19 @@ const READ_ONLY: ReadOnly[] = [ { ...Api.mediaAssets, table: "media_asset", - what: "资产", + what: "文件", filters: { mediaEntityId: "media_entity_id", status: "status" }, }, { ...Api.assetUnitCoverage, table: "asset_unit_coverage", - what: "资产覆盖", + what: "文件覆盖", filters: { mediaAssetId: "media_asset_id", contentUnitId: "content_unit_id" }, }, { ...Api.assetLocations, table: "asset_location", - what: "资产位置", + what: "文件位置", filters: { mediaAssetId: "media_asset_id", status: "status" }, }, { diff --git a/apps/server/src/modules/acquisition-organize/routes/dashboard.ts b/apps/server/src/modules/acquisition-organize/routes/dashboard.ts index 0c8eb4ad..6311b391 100644 --- a/apps/server/src/modules/acquisition-organize/routes/dashboard.ts +++ b/apps/server/src/modules/acquisition-organize/routes/dashboard.ts @@ -143,7 +143,7 @@ export function registerDashboardRoutes(app: ModuleApp, d: AoDeps) { const notes: string[] = []; 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 321fbcc1..e523c1da 100644 --- a/apps/server/src/modules/acquisition-organize/routes/downloads.ts +++ b/apps/server/src/modules/acquisition-organize/routes/downloads.ts @@ -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/naming.ts b/apps/server/src/modules/acquisition-organize/routes/naming.ts index 41cf8777..73594878 100644 --- a/apps/server/src/modules/acquisition-organize/routes/naming.ts +++ b/apps/server/src/modules/acquisition-organize/routes/naming.ts @@ -49,7 +49,7 @@ export function registerNamingRoutes(app: ModuleApp, d: AoDeps) { }; const rootRow = async (id: string) => { const r = await db().selectFrom("library_root").selectAll().where("id", "=", id).executeTakeFirst(); - if (!r) throw notFound("库根"); + if (!r) throw notFound("媒体库目录"); return r; }; const audit = ( @@ -209,7 +209,7 @@ async function loadSample(d: AoDeps, input: NamingPreviewInput): Promise { const r = await sql<{ n: number }>` select (select count(*) from acquisition_slot where rule_group_ids @> ${JSON.stringify([id])}::jsonb) @@ -117,7 +117,7 @@ async function checkIdsExist(exec: Trx | AoDeps["ctx"]["db"], ids: readonly stri if (missing.length) throw new ApiException("bad_request", `规则组不存在:${missing.join(", ")}`); } -/** 版本目标 / 订阅引用的规则组 id 校验(供 slots 路由复用) */ +/** 收录版本 / 订阅引用的规则组 id 校验(供 slots 路由复用) */ export const assertRuleGroupIds = checkIdsExist; const TRACKED_FIELDS = [ @@ -528,7 +528,7 @@ export function registerRuleGroupRoutes(app: ModuleApp, d: AoDeps) { .executeTakeFirst(); if (!cur) notFound("规则组"); const refs = await referenceCount(trx, cur!.id); - if (refs > 0) conflict(`规则组被 ${refs} 个版本目标或订阅引用,先解除引用`); + if (refs > 0) conflict(`规则组被 ${refs} 个收录版本或订阅引用,先解除引用`); const row = await trx .updateTable("rule_group") .set({ @@ -597,7 +597,7 @@ export function registerRuleGroupRoutes(app: ModuleApp, d: AoDeps) { ); } -/** 规则组改动后重评引用它的版本目标(显式引用;默认链改动由周期重评覆盖) */ +/** 规则组改动后重评引用它的收录版本(显式引用;默认链改动由周期重评覆盖) */ async function kickReevaluate(d: AoDeps, groupId: string) { try { const slots = await d.ctx.db diff --git a/apps/server/src/modules/acquisition-organize/routes/rules.ts b/apps/server/src/modules/acquisition-organize/routes/rules.ts index dab2b3b5..3712dbb2 100644 --- a/apps/server/src/modules/acquisition-organize/routes/rules.ts +++ b/apps/server/src/modules/acquisition-organize/routes/rules.ts @@ -136,7 +136,7 @@ export function registerRuleRoutes(app: ModuleApp, d: AoDeps) { Api.acquisitionSlots.get, async (_c, { params }) => { const r = await db().selectFrom("acquisition_slot").selectAll().where("id", "=", params.id).executeTakeFirst(); - return toApi(r ?? notFound("版本目标")); + return toApi(r ?? notFound("收录版本")); }, { tag: TAG }, ); @@ -171,7 +171,7 @@ export function registerRuleRoutes(app: ModuleApp, d: AoDeps) { .onConflict((oc) => oc.doNothing()) .returningAll() .executeTakeFirst(); - if (!r) conflict("该作品或内容单元下已有同名版本目标"); + if (!r) conflict("该作品或内容单元下已有同名收录版本"); await kickEvaluate(d, { acquisitionSlotId: r!.id }); return toApi(r!); }, @@ -208,7 +208,7 @@ export function registerRuleRoutes(app: ModuleApp, d: AoDeps) { .select("revision") .where("id", "=", params.id) .executeTakeFirst(); - if (!exists) notFound("版本目标"); + if (!exists) notFound("收录版本"); conflict(`revision 不匹配(当前 ${exists!.revision})`); } await kickEvaluate(d, { acquisitionSlotId: params.id }); diff --git a/apps/server/src/modules/acquisition-organize/routes/subscriptions.ts b/apps/server/src/modules/acquisition-organize/routes/subscriptions.ts index fa396220..02e86142 100644 --- a/apps/server/src/modules/acquisition-organize/routes/subscriptions.ts +++ b/apps/server/src/modules/acquisition-organize/routes/subscriptions.ts @@ -49,7 +49,7 @@ import { actorOf, conflict, errMessage, jsonb, notFound, toApi, toApiList } from import { mount, paginate } from "./helpers.ts"; // W9-E 订阅 API(docs/design/subscriptions.md §1–§3、§5):总览(分组/排序/筛选)、覆盖矩阵、表单字段规范化、 -// 暂停/恢复/重新订阅、批量、手动补全某版本、版本目标排序;主动搜索、进度、总集数、完成历史沿用 W5-C。 +// 暂停/恢复/重新订阅、批量、手动补全某版本、收录版本排序;主动搜索、进度、总集数、完成历史沿用 W5-C。 const TAG = "subscriptions"; const OVERVIEW_MAX = 2000; @@ -106,12 +106,12 @@ async function assertNoDuplicate( if (hit) conflict(`该作品同一季已有订阅:${hit.id}`); } -/** 没有收录策略模板、表单给了质量预设时,新订阅按这个模板建一个版本目标(预设随后写入) */ +/** 没有收录策略模板、表单给了质量预设时,新订阅按这个模板建一个收录版本(预设随后写入) */ const DEFAULT_SLOT_TEMPLATE: SlotTemplate = { name: "默认版本" }; /** * 保存后应用表单的派生写入:包含 / 排除 → 订阅私有规则组;质量预设(MP resolution / quality / effect)→ - * 该订阅自己的版本目标;MP total_episode → 季总集数;best_version → 版本目标升级策略。 + * 该订阅自己的收录版本;MP total_episode → 季总集数;best_version → 收录版本升级策略。 */ async function applyDerived( exec: Exec, @@ -616,7 +616,7 @@ export function registerSubscriptionRoutes(app: ModuleApp, d: AoDeps) { { tag: TAG }, ); - // ── 版本目标:手动补全 / 排序 ── + // ── 收录版本:手动补全 / 排序 ── mount( app, Api.subscriptions.fillSlot, @@ -632,7 +632,7 @@ export function registerSubscriptionRoutes(app: ModuleApp, d: AoDeps) { .execute() : []; const slot = slotsOfSub(slots, sub).find((s) => s.id === params.slotId); - if (!slot) notFound("版本目标"); + if (!slot) notFound("收录版本"); invalidateCoverage(sub.id); try { return await d.ctx.engine.enqueue( @@ -668,7 +668,7 @@ export function registerSubscriptionRoutes(app: ModuleApp, d: AoDeps) { new Set(want).size !== want.length || !want.every((id) => mine.some((s) => s.id === id)) ) - throw new ApiException("bad_request", "slotIds 必须恰好是该订阅的全部版本目标"); + throw new ApiException("bad_request", "slotIds 必须恰好是该订阅的全部收录版本"); for (const [i, id] of want.entries()) await trx.updateTable("acquisition_slot").set({ position: i }).where("id", "=", id).execute(); return (await trx.selectFrom("acquisition_slot").selectAll().where("id", "in", want).execute()).sort( diff --git a/apps/server/src/modules/acquisition-organize/services/acquisition.ts b/apps/server/src/modules/acquisition-organize/services/acquisition.ts index fb627f7a..d9fa24fa 100644 --- a/apps/server/src/modules/acquisition-organize/services/acquisition.ts +++ b/apps/server/src/modules/acquisition-organize/services/acquisition.ts @@ -200,7 +200,7 @@ export async function createSlotsFromTemplates( await exec .insertInto("acquisition_slot") .values({ - // 同一事务建的版本目标 created_at 相同:按模板顺序给 position,第一个为主版本 + // 同一事务建的收录版本 created_at 相同:按模板顺序给 position,第一个为主版本 position: i, media_entity_id: input.entityId, subscription_id: input.subscriptionId, @@ -556,7 +556,7 @@ async function evaluateForSlot(c: Ctx, p: SlotInput): Promise<{ planId: string | } const spec = slotSpecFromRow(toApi(slot)); const ctx = contextOf(rel, now); - // 规则组:版本目标 ruleGroupIds → 订阅 ruleGroupIds → 订阅默认链;开升级叠加洗版默认链 + // 规则组:收录版本 ruleGroupIds → 订阅 ruleGroupIds → 订阅默认链;开升级叠加洗版默认链 c.rules ??= loadRuleCatalog(db); const chain = resolveRuleChain(await c.rules, { kind: "acquisition", @@ -568,7 +568,7 @@ async function evaluateForSlot(c: Ctx, p: SlotInput): Promise<{ planId: string | siteId: rel.site_id, subscriptionId: sub?.id, }); - // 规则组的结构化硬条件(订阅私有组的包含 / 排除)并入版本目标硬条件 + // 规则组的结构化硬条件(订阅私有组的包含 / 排除)并入收录版本硬条件 spec.hard = mergeHard(spec.hard, chain.hard); if (chain.groups.length) { spec.rules = { groups: chain.groups, atoms: chain.atoms }; @@ -811,7 +811,7 @@ async function unitQuality( exec: Exec, slot: AcquisitionSlotRow, keys: readonly string[], - /** 版本目标引用了带层的规则组时,按规则组重算该单元现有资产的 pri(覆盖入库时按 soft.tiers 记下的值) */ + /** 收录版本引用了带层的规则组时,按规则组重算该单元现有资产的 pri(覆盖入库时按 soft.tiers 记下的值) */ priOf?: (assetId: string) => Promise, ): Promise> { const rows = await exec diff --git a/apps/server/src/modules/acquisition-organize/services/coverage.ts b/apps/server/src/modules/acquisition-organize/services/coverage.ts index 20368fb0..2deb3b8d 100644 --- a/apps/server/src/modules/acquisition-organize/services/coverage.ts +++ b/apps/server/src/modules/acquisition-organize/services/coverage.ts @@ -25,9 +25,9 @@ import { defaultChainIds, loadRuleCatalog, type RuleCatalog, resolveRuleChain } import { INFLIGHT_PLAN_STATUSES, rangeOf, subParams } from "./subscriptions.ts"; // W9-E 覆盖矩阵与订阅总览(docs/design/subscriptions.md §1、§2.1、§5): -// 一批订阅一次取齐 版本目标 / 资产 / 覆盖 / 分集账本 / 在途计划 / 内容单元 / 延后(固定 8 条查询,与订阅数无关), -// 在内存里按版本目标算每格状态(domain coverageRow)。覆盖口径与 slotCoveredUnits 相同: -// 版本目标覆盖 = 该目标计划产出的资产 + 当前资产 + 分集账本资产(作品只有一个版本目标时,作品下全部资产都算)。 +// 一批订阅一次取齐 收录版本 / 资产 / 覆盖 / 分集账本 / 在途计划 / 内容单元 / 延后(固定 8 条查询,与订阅数无关), +// 在内存里按收录版本算每格状态(domain coverageRow)。覆盖口径与 slotCoveredUnits 相同: +// 收录版本覆盖 = 该目标计划产出的资产 + 当前资产 + 分集账本资产(作品只有一个收录版本时,作品下全部资产都算)。 const MAX_EPISODES = 2000; const COVERAGE_TTL_MS = 30_000; @@ -111,11 +111,11 @@ export function subscriptionAutoFill( }; } -/** 版本目标排序:position → created_at → id;第一个为主版本 */ +/** 收录版本排序:position → created_at → id;第一个为主版本 */ export const slotOrder = (a: AcquisitionSlotRow, b: AcquisitionSlotRow) => a.position - b.position || a.created_at.getTime() - b.created_at.getTime() || a.id.localeCompare(b.id); -/** 订阅关联的版本目标:同作品、属于该订阅或不属于任何订阅 */ +/** 订阅关联的收录版本:同作品、属于该订阅或不属于任何订阅 */ export const slotsOfSub = (all: readonly AcquisitionSlotRow[], sub: Pick) => all .filter( @@ -287,7 +287,7 @@ export async function buildSubscriptionModels( if (p?.episode !== undefined || /^d\d{8}$/.test(k)) put(k, { assetId: id, quality: q, pri }); } } - // 分集账本优先(质量与 pri 按版本目标记) + // 分集账本优先(质量与 pri 按收录版本记) for (const r of ledger) { const f = { assetId: r.media_asset_id, quality: qualityOf(r.quality), pri: r.pri }; if (r.kind === "episode" && r.episode_number !== null) @@ -542,7 +542,7 @@ export async function buildSubscriptionModels( upgrade: m.upgrade, }); if (isMovie) { - // 电影:该版本目标不要求的内容版本不算缺 + // 电影:该收录版本不要求的内容版本不算缺 for (const c of r.cells) if (c.state === "missing" && !m.requiredEditions.includes(c.unitKey.slice("edition:".length))) { c.state = "skipped"; diff --git a/apps/server/src/modules/acquisition-organize/services/naming.ts b/apps/server/src/modules/acquisition-organize/services/naming.ts index 42f3fd84..502ec0b0 100644 --- a/apps/server/src/modules/acquisition-organize/services/naming.ts +++ b/apps/server/src/modules/acquisition-organize/services/naming.ts @@ -106,7 +106,7 @@ export async function namingByVersion(exec: Exec, version: number): Promise { throw new PathGuardError(`源路径不存在或不可访问:${p}`, p); } if (!roots.some((root) => within(r, root))) - throw new PathGuardError(`源路径不在已登记的库根或下载器路径映射之下:${p}`, p); + throw new PathGuardError(`源路径不在已登记的媒体库目录或下载器路径映射之下:${p}`, p); return r; }, }; diff --git a/apps/server/src/modules/acquisition-organize/services/organize.ts b/apps/server/src/modules/acquisition-organize/services/organize.ts index 7d16154d..ac5c86d1 100644 --- a/apps/server/src/modules/acquisition-organize/services/organize.ts +++ b/apps/server/src/modules/acquisition-organize/services/organize.ts @@ -15,6 +15,7 @@ import { statSafe, } from "@umt/adapters"; import type { ActorType, OrganizationRecord, OrganizationTrigger, PathBinding } from "@umt/contracts"; +import { reasonSentence, reasonText } from "@umt/contracts"; import type { DownloadTaskRow, LibraryRootRow, MediaEntityRow, OrganizationRecordRow } from "@umt/db"; import { sql } from "@umt/db"; import { @@ -415,7 +416,7 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise c.kind === "target_exists"); await upsertDecision(d, trx, { kind: hnrConflict ? "approval_required" : "path_conflict", - title: hnrConflict ? `做种义务未满足,已阻止移动:${entity.title}` : `整理冲突:${entity.title}`, + title: hnrConflict + ? `做种义务未满足,已阻止移动:${entity.title}` + : `${reasonText(blocking[0]?.kind)?.text ?? "整理冲突"}:${entity.title}`, summary: blocking - .map((c) => c.detail ?? c.kind) + .map((c) => c.detail ?? reasonText(c.kind)?.text ?? c.kind) .join(";") .slice(0, 500), subjectType: "organization_record", @@ -911,7 +914,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) @@ -1730,7 +1733,7 @@ export async function finalizeRecord(d: AoDeps, recordId: string): Promise code - ? { code: code.split(":")[0]!, message: conflictText(err) || code } + ? { code: code.split(":")[0]!, message: conflictText(err) || reasonSentence(code, code) } : err ? { code: (err as { code?: string }).code ?? "error", diff --git a/apps/server/src/modules/acquisition-organize/services/rules.ts b/apps/server/src/modules/acquisition-organize/services/rules.ts index 3f750587..b635004a 100644 --- a/apps/server/src/modules/acquisition-organize/services/rules.ts +++ b/apps/server/src/modules/acquisition-organize/services/rules.ts @@ -35,7 +35,7 @@ import { ApiException } from "../../../http/errors.ts"; import { hashJson, toApi } from "../util.ts"; import type { AoDeps, Exec } from "./common.ts"; -// 规则组(W9-D):目录加载、引用链解析(版本目标 → 订阅 → 默认链,开升级叠加洗版默认)、指纹、输入规范化、试算。 +// 规则组(W9-D):目录加载、引用链解析(收录版本 → 订阅 → 默认链,开升级叠加洗版默认)、指纹、输入规范化、试算。 export interface RuleCatalog { /** 未删除的全部规则组(含停用,解析链时再过滤) */ @@ -132,17 +132,17 @@ export function defaultChainIds(cat: RuleCatalog, kind: RuleGroupKind): string[] } export interface ResolveInput extends RuleTarget { - /** 版本目标 rule_group_ids(非空时优先) */ + /** 收录版本 rule_group_ids(非空时优先) */ slotRuleGroupIds?: readonly string[] | undefined; /** 订阅 params.ruleGroupIds */ subRuleGroupIds?: readonly string[] | undefined; kind: "acquisition" | "search"; - /** 版本目标开了升级:叠加洗版默认链 */ + /** 收录版本开了升级:叠加洗版默认链 */ upgrade?: boolean | undefined; } /** - * 引用链:订阅私有组 → (版本目标 ruleGroupIds || 订阅 ruleGroupIds || 该类默认链)→ 开升级时叠加洗版默认链。 + * 引用链:订阅私有组 → (收录版本 ruleGroupIds || 订阅 ruleGroupIds || 该类默认链)→ 开升级时叠加洗版默认链。 * 按 scope 过滤、跳过停用/已删;pri 取链上最后一个带层的组(对齐 MP 多组串联)。 */ export function resolveRuleChain( @@ -190,7 +190,7 @@ export function chainHard(rows: readonly Pick[]): SlotHard return { ...(inc.length ? { include: inc } : {}), ...(exclude.length ? { exclude } : {}) }; } -/** 版本目标硬条件 + 规则组硬条件:两边的 include 都要满足,exclude 取并集 */ +/** 收录版本硬条件 + 规则组硬条件:两边的 include 都要满足,exclude 取并集 */ export function mergeHard(base: SlotHardConditions, extra: SlotHardConditions): SlotHardConditions { if (!extra.include?.length && !extra.exclude?.length) return base; const out: SlotHardConditions = { ...base }; diff --git a/apps/server/src/modules/acquisition-organize/services/subscription-calendar.ts b/apps/server/src/modules/acquisition-organize/services/subscription-calendar.ts index d2c372f7..ef37a01c 100644 --- a/apps/server/src/modules/acquisition-organize/services/subscription-calendar.ts +++ b/apps/server/src/modules/acquisition-organize/services/subscription-calendar.ts @@ -11,7 +11,7 @@ import { TRACKING_SUB_STATUSES } from "./subscriptions.ts"; // 订阅日历(docs/design/subscriptions.md §4):事件 = 追踪中订阅范围内集的播出日期(集内容单元 air_date, // 由 refreshMetadata 按 TMDB 季集数据写入),同日同季聚合;电影取上映日期(元数据 releaseDate)。 -// 状态取主版本那一行的格(没有版本目标时取"库内任一版本"):全已入库 have / 部分 partial / 都没有 missing / +// 状态取主版本那一行的格(没有收录版本时取"库内任一版本"):全已入库 have / 部分 partial / 都没有 missing / // 播出日期在今天之后 upcoming。范围内没有播出日期的集列进 unknown(界面显示"播出日期未知")。 // 与覆盖矩阵同一套模型(buildSubscriptionModels),口径一致。 diff --git a/apps/server/src/modules/acquisition-organize/services/subscription-reset.ts b/apps/server/src/modules/acquisition-organize/services/subscription-reset.ts index f16b3f9c..ac2d0a4e 100644 --- a/apps/server/src/modules/acquisition-organize/services/subscription-reset.ts +++ b/apps/server/src/modules/acquisition-organize/services/subscription-reset.ts @@ -8,7 +8,7 @@ import { INFLIGHT_PLAN_STATUSES } from "./subscriptions.ts"; // 订阅重置(差距 #24,对照 MP GET /subscribe/reset:清 note / episode_priority,lack = total,state = R)。 // 我们的"note"是在途计划:已预留未提交的、以及下载任务已不在下载器里的已提交计划都释放(计划 cancelled, -// Slot 走 release 迁移);下载器里仍在跑的计划保留(否则会重复下载)。清掉版本目标的延后记录,标记立即重评。 +// Slot 走 release 迁移);下载器里仍在跑的计划保留(否则会重复下载)。清掉收录版本的延后记录,标记立即重评。 // 已入库资产、分集账本、总集数(含锁定)不动。 /** 仍算"活着"的下载任务状态:任务在下载器里,重置不能释放它的计划 */ diff --git a/apps/server/src/modules/acquisition-organize/services/subscriptions-params.ts b/apps/server/src/modules/acquisition-organize/services/subscriptions-params.ts index 499b6b9f..a24ba6d2 100644 --- a/apps/server/src/modules/acquisition-organize/services/subscriptions-params.ts +++ b/apps/server/src/modules/acquisition-organize/services/subscriptions-params.ts @@ -19,10 +19,10 @@ import { allOfAny } from "./rules.ts"; // total_episode (+ manual_total_episode) → 保存后写季总集数(manual 锁定) // include / exclude(字符串或数组)→ include[] / exclude[](正则白名单校验) // keyword / keywords(字符串)→ keywords[] filter_groups(组名或 id)→ ruleGroupIds -// best_version / best_version_full → 订阅版本目标的升级策略(按集 / 整季;电影 = 单片) +// best_version / best_version_full → 订阅收录版本的升级策略(按集 / 整季;电影 = 单片) // autoFillVersions{enabled, budget{perDay, bytes}} / autoFill:boolean → autoFill + fillBudget{unitsPerDay, bytesPerDay} // resolution / quality(resource_type) / effect(MP 标签或正则)→ resolution / source / effect 质量预设 -// 保存后:include / exclude(+ 预设里只能用正则表达的 UHD / Atmos)同步为订阅私有规则组;质量预设写到该订阅自己的版本目标。 +// 保存后:include / exclude(+ 预设里只能用正则表达的 UHD / Atmos)同步为订阅私有规则组;质量预设写到该订阅自己的收录版本。 const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; @@ -32,7 +32,7 @@ export interface NormalizedParams { params: SubscriptionParams; /** MP total_episode:保存后写入订阅季的总集数 */ episodeTotal?: { total: number; locked: boolean }; - /** MP best_version:写到订阅版本目标的升级策略 */ + /** MP best_version:写到订阅收录版本的升级策略 */ upgrade?: UpgradeStrategy; /** 被改写的兼容字段(调试/审计用) */ mapped: string[]; @@ -141,7 +141,7 @@ export function parseMpEffect(v: string): SubscriptionEffect | null { return null; } -/** 质量预设 → 版本目标硬条件 / 软偏好;只能用正则表达的(UHD、Atmos)进私有规则组的包含 */ +/** 质量预设 → 收录版本硬条件 / 软偏好;只能用正则表达的(UHD、Atmos)进私有规则组的包含 */ export interface PresetConditions { hard: Record; soft: Record; @@ -214,8 +214,8 @@ export function presetConditions(p: Pick JSON.stringify(a) === JSON.stringify(b); /** - * 质量预设写到该订阅自己的版本目标:新预设给出的字段覆盖;旧预设给过、新预设不再给的字段, - * 只有当版本目标上的值仍等于旧预设生成的值时才删掉(手改过的保留)。有变化 revision+1。 + * 质量预设写到该订阅自己的收录版本:新预设给出的字段覆盖;旧预设给过、新预设不再给的字段, + * 只有当收录版本上的值仍等于旧预设生成的值时才删掉(手改过的保留)。有变化 revision+1。 */ export async function applySlotPreset( exec: Exec, @@ -461,7 +461,7 @@ export async function normalizeSubscriptionParams( // ── 库根存在性与正则白名单 ── if (p.libraryRootId) { const r = await exec.selectFrom("library_root").select("id").where("id", "=", p.libraryRootId).executeTakeFirst(); - if (!r) bad(`库根不存在:${p.libraryRootId}`); + if (!r) bad(`媒体库目录不存在:${p.libraryRootId}`); } for (const k of ["include", "exclude"] as const) for (const pat of p[k] ?? []) { @@ -481,7 +481,7 @@ export async function normalizeSubscriptionParams( return out; } -/** best_version 等 → 订阅版本目标的升级策略(保留其余升级参数;revision+1) */ +/** best_version 等 → 订阅收录版本的升级策略(保留其余升级参数;revision+1) */ export async function applySlotUpgrade( exec: Exec, input: { subscriptionId: string; entityId: string; strategy: UpgradeStrategy }, diff --git a/apps/server/src/modules/acquisition-organize/services/subscriptions.ts b/apps/server/src/modules/acquisition-organize/services/subscriptions.ts index 323a797b..74ee39a2 100644 --- a/apps/server/src/modules/acquisition-organize/services/subscriptions.ts +++ b/apps/server/src/modules/acquisition-organize/services/subscriptions.ts @@ -709,7 +709,7 @@ export async function sweepMetadataRefresh(d: AoDeps): Promise { } /** - * 订阅指定的库根(params.libraryRootId):下载计划所属版本目标的订阅优先,否则取该作品最近的未取消订阅。 + * 订阅指定的库根(params.libraryRootId):下载计划所属收录版本的订阅优先,否则取该作品最近的未取消订阅。 * 整理时只在作品没有路径绑定时使用(首次建绑定)。 */ export async function subscriptionLibraryRoot( diff --git a/apps/server/src/modules/agent/authorize.ts b/apps/server/src/modules/agent/authorize.ts index ea48f859..905cfb52 100644 --- a/apps/server/src/modules/agent/authorize.ts +++ b/apps/server/src/modules/agent/authorize.ts @@ -204,13 +204,13 @@ export function createAuthorizer(deps: { if (d.id === "agent.spawn") { return runId && (ctx.source === "agent" || ctx.source === "task") ? { allowed: true } - : { allowed: false, reason: "只有 Agent 会话可以派生子会话" }; + : { allowed: false, reason: "只有智能体会话可以派生子会话" }; } // agent.propose_options 只登记一个待用户选择的事项(无外部副作用);能否问用户由处理器校验 if (d.id === "agent.propose_options") { return runId && (ctx.source === "agent" || ctx.source === "task") ? { allowed: true } - : { allowed: false, reason: "只有 Agent 会话可以让用户选方案" }; + : { allowed: false, reason: "只有智能体会话可以让用户选方案" }; } // metadata.enrich 只存补全建议(不写作品表);次数由每日预算约束,应用建议另走控制台会话 if (d.id === "metadata.enrich") return { allowed: true }; diff --git a/apps/server/src/modules/agent/capabilities-download.ts b/apps/server/src/modules/agent/capabilities-download.ts index ed278815..edcc0ba5 100644 --- a/apps/server/src/modules/agent/capabilities-download.ts +++ b/apps/server/src/modules/agent/capabilities-download.ts @@ -62,7 +62,7 @@ export function registerDownloadRecoveryCapabilities( .executeTakeFirst(); if (!dt) throw new CapabilityError("not_found", "下载任务不存在"); if (!dt.managed || dt.removed_at || dt.status === "removed") - throw new CapabilityError("precondition", "未纳管或已移除的下载任务不做自动恢复"); + throw new CapabilityError("precondition", "外部或已移除的下载任务不做自动恢复"); const status = dt.status as DownloadStatus; if (input.action === "recheck" && !RECHECK_FROM.includes(status)) throw new CapabilityError("precondition", `当前状态 ${status} 不需要重新校验`); diff --git a/apps/server/src/modules/agent/capabilities-metadata.ts b/apps/server/src/modules/agent/capabilities-metadata.ts index e047d235..9e104e77 100644 --- a/apps/server/src/modules/agent/capabilities-metadata.ts +++ b/apps/server/src/modules/agent/capabilities-metadata.ts @@ -59,7 +59,8 @@ export async function upstreamComplete( sessionId: `umt-enrich-${randomUUID()}`, }, ); - if (res.stopReason === "error" || res.stopReason === "aborted") throw new Error(res.errorMessage ?? "上游返回错误"); + if (res.stopReason === "error" || res.stopReason === "aborted") + throw new Error(res.errorMessage ?? "模型服务返回错误"); const text = res.content .filter((b): b is { type: "text"; text: string } => b.type === "text") .map((b) => b.text) @@ -87,7 +88,7 @@ export function registerMetadataCapabilities( const agent = await ctx.settings.get("agent").catch(() => null); const ws = { ...DEFAULT_WEB_SEARCH, ...(agent?.webSearch ?? {}) }; if (ws.provider === "none" || !ws.apiKeySecretRef || !registry.has("web.search")) - throw new CapabilityError("precondition", "网页搜索未开启,在设置 → Agent → 网页搜索里开启后再补全"); + throw new CapabilityError("precondition", "网页搜索未开启,在设置 → 智能体 → 网页搜索里开启后再补全"); // 内部调用 web.* 用本次补全自己的调用方身份:白名单与 Agent 会话的互不覆盖 const sub = (scope: string) => ({ actorId: scope, diff --git a/apps/server/src/modules/agent/challenge.ts b/apps/server/src/modules/agent/challenge.ts index 2ec1b1d7..595850a8 100644 --- a/apps/server/src/modules/agent/challenge.ts +++ b/apps/server/src/modules/agent/challenge.ts @@ -149,7 +149,7 @@ async function solveChallenge(env: AgentEnv, registry: CapabilityRegistry, input reason = "页面要求二次验证 / 短信 / 本人设备确认,不自动处理"; } else if (r.kind === "no_vision") { status = "needs_human"; - reason = "当前 Agent 上游模型不支持图片输入,不能做视觉自动化;换支持视觉的模型或人工处理"; + reason = "当前模型不支持图片输入,不能做视觉自动化;换支持视觉的模型或人工处理"; } else if (r.kind === "no_upstream") { status = "needs_human"; reason = r.message; @@ -230,7 +230,7 @@ async function runChild( childRunId: null, steps: 0, outcome: null, - message: "未配置 Agent 上游,不能做视觉自动化", + message: "未配置模型服务,不能做视觉自动化", }; const fake = upstream.row.provider === "fake"; const s = await ctx.settings.all(); diff --git a/apps/server/src/modules/agent/fake-scripts.ts b/apps/server/src/modules/agent/fake-scripts.ts index ac6f335f..3a065ed8 100644 --- a/apps/server/src/modules/agent/fake-scripts.ts +++ b/apps/server/src/modules/agent/fake-scripts.ts @@ -79,7 +79,7 @@ export const DEMO_SCRIPTS: FakeScript[] = [ finalText: (results) => { const r = results.find((x) => x.capabilityId === "inventory.query"); const n = (r?.json as { items?: unknown[] } | undefined)?.items?.length ?? 0; - return `[fake] 演示模式:库存查询返回 ${n} 个作品。演示模型不提交下载;要真实执行,在设置里配置 Agent 上游。`; + return `[fake] 演示模式:库存查询返回 ${n} 个作品。演示模型不提交下载;要真实执行,在设置里配置模型服务。`; }, }, ]; diff --git a/apps/server/src/modules/agent/interaction.ts b/apps/server/src/modules/agent/interaction.ts index 0dc0defe..27724a38 100644 --- a/apps/server/src/modules/agent/interaction.ts +++ b/apps/server/src/modules/agent/interaction.ts @@ -76,9 +76,9 @@ const isUuid = (s: string) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}- async function runOf(ctx: ServerContext, c: HandlerContext | InvokeContext): Promise { const id = c.agentRunId && isUuid(c.agentRunId) ? c.agentRunId : null; - if (!id) throw new CapabilityError("precondition", "只有 Agent 会话可以使用这个能力"); + if (!id) throw new CapabilityError("precondition", "只有智能体会话可以使用这个能力"); const run = await ctx.db.selectFrom("agent_run").selectAll().where("id", "=", id).executeTakeFirst(); - if (!run) throw new CapabilityError("not_found", "Agent 会话不存在"); + if (!run) throw new CapabilityError("not_found", "智能体会话不存在"); return run; } @@ -405,7 +405,7 @@ export async function inlineApproval( const summary = await planSummary(db, plan.planType, plan.planId); const decision = await insertDecision(sctx, { kind: "approval_required", - title: `Agent 请求审批:${CAPABILITY_CATALOG[d.id as KnownCapabilityId]?.title ?? d.id}`, + title: `智能体请求审批:${CAPABILITY_CATALOG[d.id as KnownCapabilityId]?.title ?? d.id}`, summary, runId, dedupeKey: `${APPROVAL_KEY}:${runId}:${inv}`, @@ -625,7 +625,7 @@ export function registerInteractionCapabilities(registry: CapabilityRegistry, ct const stepId = await currentStepId(ctx, live, run.id); d = await insertDecision(ctx, { kind: "other", - title: `Agent 等你选方案:${clip(input.question, 80)}`, + title: `智能体等你选方案:${clip(input.question, 80)}`, summary: input.options.map((o) => `${o.id}. ${o.title}`).join(";"), runId: run.id, dedupeKey, diff --git a/apps/server/src/modules/agent/routes.ts b/apps/server/src/modules/agent/routes.ts index 6ae8ed0f..1e54f450 100644 --- a/apps/server/src/modules/agent/routes.ts +++ b/apps/server/src/modules/agent/routes.ts @@ -139,7 +139,7 @@ export function registerAgentRoutes( route(app, Api.agentRuns.get, async ({ params, query }) => { const run = await db().selectFrom("agent_run").selectAll().where("id", "=", params.id).executeTakeFirst(); - if (!run) throw new ApiException("not_found", "Agent 运行不存在"); + if (!run) throw new ApiException("not_found", "智能体运行不存在"); const events = await db() .selectFrom("agent_run_event") .selectAll() @@ -222,9 +222,9 @@ export function registerAgentRoutes( route(app, Api.agentRuns.choose, async ({ params, body, actorId }) => { const run = await db().selectFrom("agent_run").selectAll().where("id", "=", params.id).executeTakeFirst(); - if (!run) throw new ApiException("not_found", "Agent 运行不存在"); + if (!run) throw new ApiException("not_found", "智能体运行不存在"); if (body.optionId === null && !body.reason?.trim()) - throw new ApiException("validation_failed", "都不要时写一句原因,Agent 按它调整方案"); + throw new ApiException("validation_failed", "都不要时写一句原因,智能体按它调整方案"); const r = await answerOptions(ctx, run.id, body, actorId); if (r === "not_found") throw new ApiException("not_found", "这个会话没有这项方案选择"); if (r === "bad_option") throw new ApiException("validation_failed", "没有这个方案"); @@ -427,7 +427,7 @@ export function registerAgentRoutes( let slotIds: string[]; if (body.slotId) { const s = await db().selectFrom("acquisition_slot").select("id").where("id", "=", body.slotId).executeTakeFirst(); - if (!s) throw new ApiException("not_found", "版本目标不存在"); + if (!s) throw new ApiException("not_found", "收录版本不存在"); slotIds = [s.id]; } else { const entityId = body.entityId ?? rel.media_entity_id; @@ -444,7 +444,7 @@ export function registerAgentRoutes( } const rejected: { slotId: string | null; code: string; message: string }[] = []; if (slotIds.length === 0) - rejected.push({ slotId: null, code: "no_slot", message: "这个作品没有版本目标,先在订阅里新建" }); + rejected.push({ slotId: null, code: "no_slot", message: "这个作品没有收录版本,先在订阅里新建" }); const invokeCtx = { actorId, taskId: `http:${c.get("requestId") ?? "req"}`, diff --git a/apps/server/src/modules/agent/runner.ts b/apps/server/src/modules/agent/runner.ts index 2b7be3ec..19ee3d1a 100644 --- a/apps/server/src/modules/agent/runner.ts +++ b/apps/server/src/modules/agent/runner.ts @@ -288,7 +288,7 @@ function systemPromptFor( constraints.push("本任务由后台事件触发,只做只读调查:收集证据,给出结论与建议,不要反复重试同一操作。"); const opts = sessionOptions(run); return buildSystemPrompt({ - role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化 Agent", + role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化智能体", goal: run.goal, constraints, // 自动化优先:用户会话默认执行,先出步骤清单;有取舍时给方案;"先看方案再执行"是会话选项 @@ -326,8 +326,8 @@ async function dailyBudgetBlock(env: AgentEnv, s: GlobalSettings, run: AgentRunR from agent_run where started_at >= ${day} and id <> ${run.id}`.execute(env.ctx.db); const n = r.rows[0]?.n ?? 0; const cost = r.rows[0]?.cost ?? 0; - if (n >= s.budget.agentMaxRunsPerDay) return `今日 Agent 运行次数已达上限 ${s.budget.agentMaxRunsPerDay}`; - if (cost >= s.budget.agentMaxCostUsdPerDay) return `今日 Agent 费用已达上限 $${s.budget.agentMaxCostUsdPerDay}`; + if (n >= s.budget.agentMaxRunsPerDay) return `今日智能体运行次数已达上限 ${s.budget.agentMaxRunsPerDay}`; + if (cost >= s.budget.agentMaxCostUsdPerDay) return `今日智能体费用已达上限 $${s.budget.agentMaxCostUsdPerDay}`; return null; } @@ -357,7 +357,7 @@ export async function drive( let settings: AgentSettings; try { const resolved = await resolveRunUpstream(ctx, run.upstream_id); - if (!resolved) throw new Error("未配置 Agent 上游(设置 → Agent → 上游)"); + if (!resolved) throw new Error("未配置模型服务(设置 → 智能体 → 模型服务)"); upstream = resolved; fake = upstream.row.provider === "fake"; settings = agentSettings(s, (run.budget ?? {}) as AgentBudget, fake, upstream); diff --git a/apps/server/src/modules/agent/service.ts b/apps/server/src/modules/agent/service.ts index f2352186..43a5bf76 100644 --- a/apps/server/src/modules/agent/service.ts +++ b/apps/server/src/modules/agent/service.ts @@ -152,7 +152,7 @@ async function enqueueRun(ctx: ServerContext, run: AgentRunRow, key: string) { /** 追加用户消息:运行中由当前会话在本轮结束后消费;已结束/等待中则唤醒续跑(新的 runAgent run) */ export async function appendMessage(ctx: ServerContext, runId: string, text: string, by: string): Promise { const run = await ctx.db.selectFrom("agent_run").selectAll().where("id", "=", runId).executeTakeFirst(); - if (!run) throw new AgentRunError("not_found", "Agent 运行不存在"); + if (!run) throw new AgentRunError("not_found", "智能体运行不存在"); const msg = await ctx.db .insertInto("agent_run_message") .values({ run_id: runId, text, created_by: by }) @@ -173,7 +173,7 @@ export async function appendMessage(ctx: ServerContext, runId: string, text: str /** 中止:只写取消标志,由会话监视器中止模型并走 finalize(不取消引擎 run,保证终态落库) */ export async function abortRun(ctx: ServerContext, runId: string, reason: string | undefined, by: string) { const run = await ctx.db.selectFrom("agent_run").selectAll().where("id", "=", runId).executeTakeFirst(); - if (!run) throw new AgentRunError("not_found", "Agent 运行不存在"); + if (!run) throw new AgentRunError("not_found", "智能体运行不存在"); if (TERMINAL.includes(run.status)) return run; const now = new Date(); const row = await ctx.db @@ -353,7 +353,7 @@ export async function finalize( // 同根因(同一待处理事项 / 同一定时巡检 / 同一目标模板)超预算合并到已有事项,不重复新建 await writeDecision(ctx, { kind: "other", - title: "Agent 运行超出预算", + title: "智能体运行超出预算", summary: res.message ?? "超出预算", subjectId: runId, dedupeKey: await budgetDedupeKey(ctx, run), diff --git a/apps/server/src/modules/agent/subagents.ts b/apps/server/src/modules/agent/subagents.ts index 027f28c1..9ae30e59 100644 --- a/apps/server/src/modules/agent/subagents.ts +++ b/apps/server/src/modules/agent/subagents.ts @@ -707,9 +707,9 @@ export interface SpawnDeps { async function parentOf(ctx: ServerContext, c: HandlerContext): Promise { const id = c.agentRunId; - if (!id) throw new CapabilityError("precondition", "只有 Agent 会话可以派生 / 等待子会话"); + if (!id) throw new CapabilityError("precondition", "只有智能体会话可以派生 / 等待子会话"); const run = await ctx.db.selectFrom("agent_run").selectAll().where("id", "=", id).executeTakeFirst(); - if (!run) throw new CapabilityError("not_found", "Agent 会话不存在"); + if (!run) throw new CapabilityError("not_found", "智能体会话不存在"); return run; } @@ -719,7 +719,7 @@ export function registerSubagentCapabilities(registry: CapabilityRegistry, ctx: const parent = await parentOf(ctx, c); const s = await ctx.settings.all(); const cfg = subagentSettings(s); - if (!cfg.enabled) throw new CapabilityError("precondition", "子会话未开启(设置 → Agent → 允许子会话)"); + if (!cfg.enabled) throw new CapabilityError("precondition", "子会话未开启(设置 → 智能体 → 允许子会话)"); if (parent.cancel_requested_at || TERMINAL.includes(parent.status)) throw new CapabilityError("precondition", "本会话已取消或已结束,不能派生子会话"); const dedupeKey = `subagent:${c.idempotencyKey ?? `${parent.id}:${Date.now()}`}`; diff --git a/apps/server/src/modules/auth-settings/index.ts b/apps/server/src/modules/auth-settings/index.ts index 6240c0a7..4995f91f 100644 --- a/apps/server/src/modules/auth-settings/index.ts +++ b/apps/server/src/modules/auth-settings/index.ts @@ -52,7 +52,7 @@ export function createAuthSettingsModule(opts: AuthSettingsModuleOptions = {}): key: "auth.cleanup", defaults: { name: "登录会话清理", - description: "删除 7 天前的登录尝试和 30 天前过期或吊销的会话", + description: "删除 7 天前的登录尝试和 30 天前失效的会话", group: "maintain", schedule: { type: "interval", everyMs }, }, diff --git a/apps/server/src/modules/auth-settings/instances-routes.ts b/apps/server/src/modules/auth-settings/instances-routes.ts index 9edd7a8e..b41fb73e 100644 --- a/apps/server/src/modules/auth-settings/instances-routes.ts +++ b/apps/server/src/modules/auth-settings/instances-routes.ts @@ -63,7 +63,7 @@ const test = (path: string, summary: string, tag: string) => ({ type Table = "library_root" | "downloader_instance" | "media_server_instance" | "path_mapping"; const NOUN: Record = { - library_root: "库根", + library_root: "媒体库目录", downloader_instance: "下载器实例", media_server_instance: "媒体服务器实例", path_mapping: "路径映射", @@ -72,7 +72,7 @@ const JSON_COLS = ["settings", "version_info", "health_detail", "verify_detail"] const errMessage = (e: unknown) => redactString(String((e as Error)?.message ?? e)).slice(0, 500); -const LR = crudDefs("/library-roots", "库根", { +const LR = crudDefs("/library-roots", "媒体库目录", { row: LibraryRoot, create: LibraryRootCreate, update: LibraryRootUpdate, @@ -182,7 +182,7 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) { } catch (e) { throw new ApiException( "precondition_failed", - `库根不可写(需要目录存在且可写入哨兵文件 ${name}):${errMessage(e)}`, + `媒体库目录不可写(需要目录存在且可写入标记文件 ${name}):${errMessage(e)}`, ); } return name; @@ -216,7 +216,7 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) { remove("library_root", "library-roots", "/library-roots/{id}", "library_root.delete"); endpoint( app, - test("/library-roots/{id}/test", "检查库根在线(目录 + 哨兵文件)", "library-roots"), + test("/library-roots/{id}/test", "检查媒体库目录在线(目录 + 哨兵文件)", "library-roots"), async ({ params, actor }) => { const row = await getRow("library_root", params.id); const h = await checkRoot(String(row.local_path), (row.sentinel_file as string | null) ?? DEFAULT_ROOT_SENTINEL); diff --git a/apps/server/src/modules/browser/__tests__/browser-api.test.ts b/apps/server/src/modules/browser/__tests__/browser-api.test.ts index dc3aefb0..813cc6bc 100644 --- a/apps/server/src/modules/browser/__tests__/browser-api.test.ts +++ b/apps/server/src/modules/browser/__tests__/browser-api.test.ts @@ -149,7 +149,7 @@ describe.skipIf(!up)("浏览器 HTTP 端点", () => { expect(files).toHaveLength(1); expect(new Uint8Array(await readFile(join(h.stagingDir, files[0]!)))).toEqual(h.torrent.bytes); - // 识别到作品后再下载:走同一流程(无版本目标 → no_slot);同一文件第二次捕获标 duplicate、不提交 + // 识别到作品后再下载:走同一流程(无收录版本 → no_slot);同一文件第二次捕获标 duplicate、不提交 const entity = await h.db .insertInto("media_entity") .values({ kind: "movie", title: "奥本海默", year: 2023, status: "active" }) diff --git a/apps/server/src/modules/browser/service.ts b/apps/server/src/modules/browser/service.ts index 4d411839..e56242d5 100644 --- a/apps/server/src/modules/browser/service.ts +++ b/apps/server/src/modules/browser/service.ts @@ -1579,7 +1579,7 @@ export class BrowserService { const give = opts.wantImage && !blocked && (!sensitive || opts.sensitiveAllowed); let note: string | null = null; if (blocked) note = "页面要求二次验证 / 短信 / 本人设备确认:不自动处理,会话已转等待处理,由人在原站视图接管"; - else if (!opts.wantImage) note = "当前上游模型不支持图片输入,截图没有交给模型;下面是页面的文字快照"; + else if (!opts.wantImage) note = "当前模型不支持图片输入,截图没有交给模型;下面是页面的文字快照"; else if (sensitive && !opts.sensitiveAllowed) note = "登录页 / 含密码框的截图属于敏感截图,默认不交给模型(需要 browser.sensitive_screenshot 持续授权并传 allowSensitive);下面是已脱敏的文字快照"; @@ -1801,7 +1801,7 @@ const REASON_TEXT: Record = { challenge_unsolved: "自动处理页面挑战未成功", login_required: "登录已失效:接管后在画面里登录一次,交还后全站生效", lease_expired: "人工接管超时未交还", - agent_request: "Agent 请求人工处理", + agent_request: "智能体请求人工处理", human_released_waiting: "人工交还时选择继续等待", }; diff --git a/apps/server/src/modules/cache/index.ts b/apps/server/src/modules/cache/index.ts index dd0b53d8..7e2ae53f 100644 --- a/apps/server/src/modules/cache/index.ts +++ b/apps/server/src/modules/cache/index.ts @@ -183,7 +183,7 @@ export function createCacheModule(opts: CacheModuleOptions = {}): ServerModule & key: "cache.sweep", defaults: { name: "缓存清理", - description: "删除 PG 缓存里已过期的条目(TMDB 请求与识别缓存)", + description: "删除过期的 TMDB 与识别缓存", group: "maintain", schedule: { type: "interval", everyMs: 10 * 60_000 }, firstRun: "after_interval", @@ -194,7 +194,7 @@ export function createCacheModule(opts: CacheModuleOptions = {}): ServerModule & key: "cache.images_clean", defaults: { name: "图片缓存清理", - description: "删除超过保留天数的图片代理缓存文件(对照 MP 启动时的 clear_temp)", + description: "删除过期的海报缓存", group: "maintain", schedule: { type: "interval", everyMs: 24 * 3_600_000 }, }, @@ -208,7 +208,7 @@ export function createCacheModule(opts: CacheModuleOptions = {}): ServerModule & key: "cache.images_lru", defaults: { name: "图片缓存容量控制", - description: "图片代理缓存超过容量预算(存储设置,缺省 20 GB)时,按最久未访问删除到预算的 90%", + description: "海报缓存超过容量上限时,删掉最久没用的", group: "maintain", schedule: { type: "interval", everyMs: 30 * 60_000 }, firstRun: "after_interval", diff --git a/apps/server/src/modules/file-manager/organize.ts b/apps/server/src/modules/file-manager/organize.ts index 867ad2aa..0975f712 100644 --- a/apps/server/src/modules/file-manager/organize.ts +++ b/apps/server/src/modules/file-manager/organize.ts @@ -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); @@ -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; } } diff --git a/apps/server/src/modules/file-manager/probe.ts b/apps/server/src/modules/file-manager/probe.ts index f33e9a82..590e2c29 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 }, diff --git a/apps/server/src/modules/file-manager/transfer.ts b/apps/server/src/modules/file-manager/transfer.ts index 9c026cf0..742ee55f 100644 --- a/apps/server/src/modules/file-manager/transfer.ts +++ b/apps/server/src/modules/file-manager/transfer.ts @@ -95,7 +95,7 @@ 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} 不可写,只能选 可写的媒体库目录`); const sources: Resolved[] = []; for (const it of body.items) { diff --git a/apps/server/src/modules/file-manager/trash.ts b/apps/server/src/modules/file-manager/trash.ts index 2b00e995..2806eb17 100644 --- a/apps/server/src/modules/file-manager/trash.ts +++ b/apps/server/src/modules/file-manager/trash.ts @@ -157,7 +157,7 @@ const BLOCK_ERR: Record< > = { download_active: ["download_active", "属于未完成或做种义务未满足的下载任务"], is_root: ["bad_request", "不能删除根目录或只读根下的内容"], - needs_retire: ["needs_retire", "含已登记资产,需勾选「同时退役资产」"], + needs_retire: ["needs_retire", "含已入库文件,需勾选「同时从媒体库移除」"], needs_unbind: ["needs_unbind", "是作品/季的绑定目录,需勾选「解除绑定」"], root_unavailable: ["root_unavailable", "根不可用"], not_found: ["not_found", "不存在"], @@ -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}`, @@ -509,7 +509,7 @@ 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} 不可写,只能选 可写的媒体库目录`); for (const id of body.ids) { try { const row = await d.ctx.db.selectFrom("trash_item").selectAll().where("id", "=", id).executeTakeFirst(); diff --git a/apps/server/src/modules/mcp/catalog.ts b/apps/server/src/modules/mcp/catalog.ts index 3c514cae..ccc13c50 100644 --- a/apps/server/src/modules/mcp/catalog.ts +++ b/apps/server/src/modules/mcp/catalog.ts @@ -21,7 +21,7 @@ export const MCP_RESOURCES: readonly McpResourceDef[] = [ uri: "umt://settings/summary", name: "settings-summary", title: "设置概要", - description: "自动化总开关、只读对账、预算与 Agent 运行上限。不含凭证。", + description: "自动化总开关、只读对账、预算与智能体运行上限。不含凭证。", mimeType: "application/json", async load(ctx) { const s = await ctx.settings.all(); diff --git a/apps/server/src/modules/mcp/config-store.ts b/apps/server/src/modules/mcp/config-store.ts index eb731e8b..d495ea18 100644 --- a/apps/server/src/modules/mcp/config-store.ts +++ b/apps/server/src/modules/mcp/config-store.ts @@ -48,7 +48,7 @@ export function createMcpConfigStore(db: () => Kysely): McpConfigStore { /** 这个 Key 能不能连 MCP:总开关开着,且在允许列表里(列表为 null 表示不限) */ export function keyAllowed(cfg: McpServerConfig, principal: { kind: string; id: string }): string | null { - if (!cfg.enabled) return "MCP 服务端已停用。在设置 → Agent → 工具与 MCP 里启用"; + if (!cfg.enabled) return "MCP 服务端已停用。在设置 → 智能体 → 工具与 MCP 里启用"; if (principal.kind === "api_key" && cfg.allowedApiKeyIds && !cfg.allowedApiKeyIds.includes(principal.id)) { return "这个 API Key 不在 MCP 允许列表里"; } diff --git a/apps/server/src/modules/mcp/server.ts b/apps/server/src/modules/mcp/server.ts index c607b415..3caf4033 100644 --- a/apps/server/src/modules/mcp/server.ts +++ b/apps/server/src/modules/mcp/server.ts @@ -102,7 +102,7 @@ export function createMcpServer( prompts: { listChanged: false }, }, instructions: - "工具与控制台、内置 Agent 共用同一套能力、授权与审计。只读工具可以直接调用;有副作用的工具需要持续授权或单次审批,缺授权时返回 unauthorized,不要重复尝试。资源里有设置概要、待处理事项与订阅总览。", + "工具与控制台、内置智能体共用同一套能力、授权与审计。只读工具可以直接调用;有副作用的工具需要持续授权或单次审批,缺授权时返回 unauthorized,不要重复尝试。资源里有设置概要、待处理事项与订阅总览。", }, ); const limiter = sharedInvokeLimiter(); diff --git a/apps/server/src/modules/pi/index.ts b/apps/server/src/modules/pi/index.ts index cce60b12..401dadd9 100644 --- a/apps/server/src/modules/pi/index.ts +++ b/apps/server/src/modules/pi/index.ts @@ -73,8 +73,8 @@ export function createPiModule( registerScheduledJob(ctx, { key: "pi.token_refresh", defaults: { - name: "上游登录刷新", - description: "订阅登录即将过期的 Agent 上游,提前刷新 token", + name: "模型服务登录刷新", + description: "订阅登录快过期时提前续期", group: "maintain", schedule: { type: "interval", everyMs: interval }, }, diff --git a/apps/server/src/modules/pi/models.ts b/apps/server/src/modules/pi/models.ts index f9524774..448643fb 100644 --- a/apps/server/src/modules/pi/models.ts +++ b/apps/server/src/modules/pi/models.ts @@ -92,12 +92,12 @@ function keyOf(cred: UpstreamCredential | undefined): string | null { /** 按 provider / 认证方式挑模型列表接口 */ function planFor(spec: UpstreamSpec, cred: UpstreamCredential | undefined): Plan { const extra = spec.extra ?? {}; - if (spec.provider === "fake") return { kind: "unsupported", reason: "Fake 上游没有远程模型列表" }; + if (spec.provider === "fake") return { kind: "unsupported", reason: "Fake 模型服务没有远程模型列表" }; if (spec.provider === "radius") return { kind: "pi" }; const custom = spec.provider === "openai-compatible"; // 订阅登录只有 OpenRouter 换到的是普通 API Key,其它订阅 token 不能调公开的模型列表接口 if (spec.authKind === "oauth" && spec.provider !== "openrouter") - return { kind: "unsupported", reason: "订阅登录的上游没有公开的模型列表接口,用内置模型表" }; + return { kind: "unsupported", reason: "订阅登录的模型服务没有公开的模型列表接口,用内置模型表" }; const key = keyOf(cred); if (spec.authKind !== "none" && !key && !custom) throw new FetchError("auth", "还没有配置 API Key"); @@ -232,7 +232,7 @@ function normalizeGoogle(item: Record): Remote | null { function parseList(shape: Shape, body: unknown): Remote[] { const b = obj(body); const raw = Array.isArray(body) ? body : Array.isArray(b.data) ? b.data : Array.isArray(b.models) ? b.models : null; - if (!raw) throw new FetchError("other", "上游返回的不是模型列表"); + if (!raw) throw new FetchError("other", "模型服务返回的不是模型列表"); const norm = shape === "anthropic" ? normalizeAnthropic : shape === "google" ? normalizeGoogle : normalizeOpenAI; const out: Remote[] = []; const seen = new Set(); @@ -376,7 +376,7 @@ async function fetchRemote( if (plan.kind === "unsupported") throw new FetchError("unsupported", plan.reason); if (plan.kind === "http") return await fetchList(plan.endpoint); // Radius:pi-ai 自带的动态刷新 - if (!client) throw new FetchError("config", "上游配置不完整"); + if (!client) throw new FetchError("config", "模型服务配置不完整"); const r = await client.listModels({ refresh: true }); if (!r.refreshed) throw toFetchError(new Error(r.refreshError ?? "刷新失败")); return r.items.map(fromInfo); diff --git a/apps/server/src/modules/pi/oauth.ts b/apps/server/src/modules/pi/oauth.ts index 26afd5f5..04aab222 100644 --- a/apps/server/src/modules/pi/oauth.ts +++ b/apps/server/src/modules/pi/oauth.ts @@ -105,8 +105,8 @@ export class OAuthSessions { async start(row: PiUpstreamRow, req: { mode?: PiOAuthMode; enterpriseDomain?: string }): Promise { const info = providerInfo(row.provider); if (row.auth_kind !== "oauth" || !info?.oauth) - throw new ApiException("precondition_failed", "这个上游不是订阅登录类型"); - if (!row.enabled) throw new ApiException("precondition_failed", "上游已停用"); + throw new ApiException("precondition_failed", "这个模型服务不是订阅登录类型"); + if (!row.enabled) throw new ApiException("precondition_failed", "模型服务已停用"); const mode = req.mode ?? info.oauth.modes[0]!; if (!info.oauth.modes.includes(mode)) throw new ApiException( @@ -270,6 +270,6 @@ export class OAuthSessions { export async function requireRow(ctx: ServerContext, id: string): Promise { const row = await getRow(ctx, id); - if (!row) throw new ApiException("not_found", "上游不存在"); + if (!row) throw new ApiException("not_found", "模型服务不存在"); return row; } diff --git a/apps/server/src/modules/pi/refresh.ts b/apps/server/src/modules/pi/refresh.ts index db35dd04..15f0b6cd 100644 --- a/apps/server/src/modules/pi/refresh.ts +++ b/apps/server/src/modules/pi/refresh.ts @@ -24,13 +24,13 @@ 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 = `模型服务「${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) values ('other', 'warning', ${title}, ${message}, 'pi_upstream', ${row.id}, ${DECISION_KEY(row.id)}, ${JSON.stringify({ provider: row.provider, upstreamId: row.id, error: message })}::jsonb, - ${JSON.stringify([{ key: "relogin", label: "重新登录", description: "设置 → Agent → 上游" }])}::jsonb) + ${JSON.stringify([{ key: "relogin", label: "重新登录", description: "设置 → 智能体 → 模型服务" }])}::jsonb) on conflict (dedupe_key) where status in ('open', 'investigating', 'awaiting_user') do update set attempts = decision.attempts + 1, evidence = excluded.evidence, summary = excluded.summary, updated_at = now() @@ -102,7 +102,7 @@ export async function refreshPiTokens(ctx: ServerContext, opts: RefreshOptions = if (r.ok) report.refreshed += 1; else { report.failed.push({ upstreamId: row.id, message: r.message }); - ctx.logger.warn({ upstreamId: row.id, provider: row.provider, message: r.message }, "Agent 上游 token 刷新失败"); + ctx.logger.warn({ upstreamId: row.id, provider: row.provider, message: r.message }, "模型服务 token 刷新失败"); } } return report; diff --git a/apps/server/src/modules/pi/service.ts b/apps/server/src/modules/pi/service.ts index c1ec85ce..3ffd541b 100644 --- a/apps/server/src/modules/pi/service.ts +++ b/apps/server/src/modules/pi/service.ts @@ -116,7 +116,7 @@ export async function createUpstream( .execute(); } catch (err) { if (secretRef) await ctx.secrets.remove(secretRef).catch(() => {}); - if (isUnique(err)) throw new ApiException("conflict", `上游名称「${body.name}」已存在`); + if (isUnique(err)) throw new ApiException("conflict", `模型服务名称「${body.name}」已存在`); throw err; } if (body.makeDefault || !(await defaultUpstreamId(ctx))) await setDefaultUpstream(ctx, id); @@ -140,7 +140,7 @@ export async function updateUpstream( const defaultModel = body.defaultModel !== undefined ? body.defaultModel : row.default_model; validateShape({ provider: row.provider, authKind, baseUrl, defaultModel }); if (body.apiKey && authKind !== "api_key") - throw new ApiException("validation_failed", "只有 api_key 型上游可以填 apiKey"); + throw new ApiException("validation_failed", "只有 api_key 型模型服务可以填 apiKey"); const kindChanged = authKind !== row.auth_kind; const store = dbCredentialStore(ctx, row.id); @@ -167,7 +167,7 @@ export async function updateUpstream( .where("id", "=", row.id) .execute() .catch((err) => { - if (isUnique(err)) throw new ApiException("conflict", `上游名称「${body.name}」已存在`); + if (isUnique(err)) throw new ApiException("conflict", `模型服务名称「${body.name}」已存在`); throw err; }); if (typeof body.apiKey === "string") { @@ -217,10 +217,11 @@ 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.enabled) throw new Error(`上游「${row.name}」已停用`); - if (row.status === "expired") throw new Error(`上游「${row.name}」登录已失效,在设置里重新登录`); + 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 === "auth_required") - throw new Error(`上游「${row.name}」${row.auth_kind === "oauth" ? "未登录" : "未配置 API Key"}`); + 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/pi/store.ts b/apps/server/src/modules/pi/store.ts index 746de61c..3b68274b 100644 --- a/apps/server/src/modules/pi/store.ts +++ b/apps/server/src/modules/pi/store.ts @@ -59,7 +59,7 @@ async function readSecret(ctx: Ctx, row: Pick> { const out = new Map(); if (ids.length === 0) return out; diff --git a/apps/server/src/modules/sources-identity/index.ts b/apps/server/src/modules/sources-identity/index.ts index 8fc18caf..6fd1bd19 100644 --- a/apps/server/src/modules/sources-identity/index.ts +++ b/apps/server/src/modules/sources-identity/index.ts @@ -61,15 +61,15 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S return { summary: `${await fn(rt)} 项` }; }, }); - job("feeds.poll", "Feed 轮询", "扫描到期的 Feed 并入队抓取(每个 Feed 仍按自己的轮询间隔)", tickFeeds); + job("feeds.poll", "RSS 轮询", "按各 RSS 自己的间隔抓取新资源", tickFeeds); job("sites.snapshot", "站点快照", "扫描到周期的站点并入队快照", tickSnapshots); - job("identity.review", "待识别复查", "到期的待识别条目重新识别", tickReviews); + job("identity.review", "未识别复查", "到期的未识别条目重新识别", tickReviews); // W9-A 站点中心:用户数据每 6 小时、健康检测每小时(可在定时任务页关闭或改周期) registerScheduledJob(ctx, { key: "sites.user_data", defaults: { name: "站点数据刷新", - description: "登录状态正常的站点账号刷新上传 / 下载 / 分享率等用户数据;会话失效的账密账号自动重登一次", + description: "刷新站点账号的上传、下载和分享率;登录失效的账密账号自动重登一次", group: "collect", schedule: { type: "interval", everyMs: 6 * 3_600_000 }, firstRun: "after_interval", @@ -83,7 +83,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", @@ -95,8 +95,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S key: BACKFILL_LANGUAGES_JOB, defaults: { name: "元数据语言回刷", - description: - "对已确认 TMDB 身份的作品按语言回退链完整刷新一次(标题、简介、每集信息),TMDB 每秒最多 2 次请求;跑完自动停用,可手动再执行", + description: "按语言设置重新拉一遍作品的标题、简介和每集信息;跑完自动停用,可手动再执行", group: "maintain", schedule: { type: "interval", everyMs: 24 * 3_600_000 }, firstRun: "now", diff --git a/apps/server/src/modules/sources-identity/routes/enrich.ts b/apps/server/src/modules/sources-identity/routes/enrich.ts index cefdb7cf..4659183f 100644 --- a/apps/server/src/modules/sources-identity/routes/enrich.ts +++ b/apps/server/src/modules/sources-identity/routes/enrich.ts @@ -79,7 +79,7 @@ export function enrichRoutes(app: ModuleApp, rt: () => SourcesRuntime): void { path: "/identity-pool/{id}/enrich/apply", scope: "admin", auth: "session", - summary: "应用元数据补全建议:建本地临时作品(或并入同名作品)并关联待识别资源(只接受控制台管理员会话)", + summary: "应用元数据补全建议:建本地临时作品(或并入同名作品)并关联未识别资源(只接受控制台管理员会话)", tags: T_ID, params: IdParam, body: EnrichApplyInput, diff --git a/apps/server/src/modules/sources-identity/routes/identity.ts b/apps/server/src/modules/sources-identity/routes/identity.ts index 6d6c4292..9e8de47c 100644 --- a/apps/server/src/modules/sources-identity/routes/identity.ts +++ b/apps/server/src/modules/sources-identity/routes/identity.ts @@ -546,7 +546,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), @@ -623,7 +623,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { method: "post", path: "/identity-pool/{id}/assign", scope: "organize.execute", - summary: "为待识别资源手动指定候选 / 本地作品 / 外部身份", + summary: "为未识别资源手动指定候选 / 本地作品 / 外部身份", tags: T_ID, params: IdParam, body: ReleaseIdentityAssign, @@ -635,7 +635,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { .select(["source_release_id"]) .where("id", "=", params.id) .executeTakeFirst(); - if (!pool) throw new ApiException("not_found", "待识别条目不存在"); + if (!pool) throw new ApiException("not_found", "未识别条目不存在"); return assignReleaseIdentity(rt(), pool.source_release_id, body, actor); }, ); @@ -645,7 +645,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() }), @@ -670,7 +670,7 @@ export function identityRoutes(app: ModuleApp, rt: () => SourcesRuntime) { .select(["source_release_id", "attempts"]) .where("id", "=", params.id) .executeTakeFirst(); - if (!pool) throw new ApiException("not_found", "待识别条目不存在"); + if (!pool) throw new ApiException("not_found", "未识别条目不存在"); const r = await rt().ctx.engine.enqueue( "resolveObservation", // 用户点"重新识别" = 手动触发:即使不相关也查 TMDB(惰性识别的 c 路径) diff --git a/apps/server/src/modules/sources-identity/service/enrich.ts b/apps/server/src/modules/sources-identity/service/enrich.ts index 0953d40a..513a576f 100644 --- a/apps/server/src/modules/sources-identity/service/enrich.ts +++ b/apps/server/src/modules/sources-identity/service/enrich.ts @@ -43,7 +43,7 @@ const MAX_SEARCHES = 2; const SITE_FILTER_DOMAINS = 6; /** 交给模型的每页正文上限 */ const PAGE_CHARS_FOR_MODEL = 6000; -export const ENRICH_NOTICE = "字段来自网页与 Agent 抽取,不是 TMDB 数据;应用前核对出处"; +export const ENRICH_NOTICE = "字段来自网页与智能体抽取,不是 TMDB 数据;应用前核对出处"; export class EnrichError extends Error { constructor( @@ -411,7 +411,7 @@ async function resolveQuery(ctx: Pick, input: MetadataEnric .select(["p.id", "p.query", "r.title as release_title"]) .where("p.id", "=", input.poolEntryId) .executeTakeFirst(); - if (!p) throw new EnrichError("not_found", "待识别条目不存在"); + if (!p) throw new EnrichError("not_found", "未识别条目不存在"); const q = (p.query ?? {}) as { title?: string | null; titleCn?: string | null; year?: number | null }; const title = (q.titleCn || q.title || "").trim(); if (!title) throw new EnrichError("precondition", "这条资源没有解析出标题,无法补全"); @@ -737,7 +737,7 @@ export async function applyEnrichment( .select(["id", "source_release_id", "status"]) .where("id", "=", poolEntryId) .executeTakeFirst(); - if (!pool) throw new ApiException("not_found", "待识别条目不存在"); + if (!pool) throw new ApiException("not_found", "未识别条目不存在"); const row = await ctx.db .selectFrom("metadata_enrichment") .selectAll() 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 b357424c..97ffc700 100644 --- a/apps/server/src/modules/sources-identity/service/identity-admin.ts +++ b/apps/server/src/modules/sources-identity/service/identity-admin.ts @@ -44,9 +44,12 @@ async function entityForExternal( const hit = await entityByExternal(exec, ref); if (hit) return { entityId: hit, created: false }; 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); @@ -480,7 +483,7 @@ export async function dismissPoolEntry(rt: SourcesRuntime, poolId: string, actor .where("id", "=", poolId) .returningAll() .executeTakeFirst(); - if (!row) throw new ApiException("not_found", "待识别条目不存在"); + if (!row) throw new ApiException("not_found", "未识别条目不存在"); await trx .updateTable("source_release") .set({ resolution_status: "rejected" }) diff --git a/apps/server/src/modules/sources-identity/service/identity-decisions.ts b/apps/server/src/modules/sources-identity/service/identity-decisions.ts index 0dd5473f..3547fec5 100644 --- a/apps/server/src/modules/sources-identity/service/identity-decisions.ts +++ b/apps/server/src/modules/sources-identity/service/identity-decisions.ts @@ -25,7 +25,7 @@ function manualIdentityOptions(sourceReleaseId: string): DecisionOption[] { { key: "manual_tmdb", label: "手动指定 TMDB ID", - description: "按 TMDB 电影或剧集 ID 建档并关联,加人工锁", + description: "按 TMDB 电影或剧集 ID 加入媒体库并关联,加人工锁", input: { sourceReleaseId, requires: "tmdb" }, }, { @@ -79,7 +79,7 @@ function titleFor( one: string, work: string | null, ) { - const what = kind === "identity_ambiguous" ? "待确认身份" : "待识别"; + const what = kind === "identity_ambiguous" ? "没识别出作品" : "未识别"; if (n <= 1) return `${what}:${one}`; return `来源「${siteName}」的 ${n} 条资源${what}${work ? `:${work}` : ""}`; } diff --git a/apps/server/src/modules/system/index.ts b/apps/server/src/modules/system/index.ts index ea77bb97..8af99ac2 100644 --- a/apps/server/src/modules/system/index.ts +++ b/apps/server/src/modules/system/index.ts @@ -210,7 +210,7 @@ export function createSystemModule(opts: SystemModuleOptions = {}): ServerModule key: "system.heartbeat_cleanup", defaults: { name: "进程心跳清理", - description: "删除 24 小时没有心跳的进程行和超过保留期(存储设置,缺省 24 小时)的心跳采样", + description: "删除过期的进程状态记录", group: "maintain", schedule: { type: "interval", everyMs }, }, diff --git a/apps/server/src/modules/system/retention.ts b/apps/server/src/modules/system/retention.ts index 93185f4d..9fce60a3 100644 --- a/apps/server/src/modules/system/retention.ts +++ b/apps/server/src/modules/system/retention.ts @@ -105,8 +105,8 @@ export const RETENTION_JOBS: RetentionJob[] = [ { target: "observation", jobId: "storage.retention.observation", - name: "观察记录清理", - description: "删除超过保留期的 Feed 观察记录(observation,按 created_at)", + name: "RSS 抓取记录清理", + description: "删除过期的 RSS 抓取记录", everyMs: 6 * HOUR, retention: (s) => `${s.retentionDays.observation} 天`, async run(c) { @@ -118,8 +118,8 @@ export const RETENTION_JOBS: RetentionJob[] = [ { target: "sse_event", jobId: "storage.retention.sse_event", - name: "SSE 事件清理", - description: "删除超过保留期的 SSE 补读事件(sse_event,按 at;发布方另按条数裁剪)", + name: "实时事件清理", + description: "删除过期的页面实时推送记录", everyMs: HOUR, retention: (s) => `${s.retentionDays.sseEvent} 天`, async run(c) { @@ -132,7 +132,7 @@ export const RETENTION_JOBS: RetentionJob[] = [ target: "task_run", jobId: "storage.retention.task_run", name: "任务运行清理", - description: "删除超过保留期、已结束的业务任务(task)与引擎 run(含步骤、运行元数据)", + description: "删除过期的已结束任务", everyMs: 6 * HOUR, retention: (s) => `${s.retentionDays.taskRun} 天`, async run(c) { @@ -167,8 +167,8 @@ export const RETENTION_JOBS: RetentionJob[] = [ { target: "agent_event", jobId: "storage.retention.agent_event", - name: "Agent 事件清理", - description: "删除超过保留期、会话已结束的 Agent 事件流与消息(agent_run_event / agent_run_message;会话本身保留)", + name: "智能体事件清理", + description: "删除已结束会话的过期消息,会话本身保留", everyMs: 6 * HOUR, retention: (s) => `${s.retentionDays.agentEvent} 天`, async run(c) { @@ -182,14 +182,14 @@ export const RETENTION_JOBS: RetentionJob[] = [ sql`created_at < ${cutoff} and status = 'consumed' and ${ended}`, c.batch, ); - return `Agent 事件 ${ev.deleted}、消息 ${msg.deleted} 行已删除${ev.truncated || msg.truncated ? ",未删完,下次继续" : ""}`; + return `智能体事件 ${ev.deleted}、消息 ${msg.deleted} 行已删除${ev.truncated || msg.truncated ? ",未删完,下次继续" : ""}`; }, }, { target: "audit", jobId: "storage.retention.audit", name: "审计清理", - description: "删除超过保留期的审计事件(audit_event,能力调用审计按能力统计的保留期另算)", + description: "删除过期的审计日志", everyMs: 24 * HOUR, retention: (s) => `${s.retentionDays.audit} 天`, async run(c) { @@ -207,7 +207,7 @@ export const RETENTION_JOBS: RetentionJob[] = [ target: "capability_stats", jobId: "storage.retention.capability_stats", name: "能力统计清理", - description: "删除超过保留期的能力调用审计(audit_event target_type=capability)与已结束的能力调用记录", + description: "删除过期的能力调用记录", everyMs: 24 * HOUR, retention: (s) => `${s.retentionDays.capabilityStats} 天`, async run(c) { @@ -230,8 +230,8 @@ export const RETENTION_JOBS: RetentionJob[] = [ { target: "identity_pool", jobId: "storage.retention.identity_pool", - name: "待识别池归档", - description: "待识别池里 unmatched 条目超过保留期没有新证据就归档(不删除,仍可按状态筛出)", + name: "未识别资源归档", + description: "长期没有新线索的未识别资源归档,仍可手动指定", everyMs: 6 * HOUR, retention: (s) => `${s.retentionDays.identityPoolArchive} 天`, async run(c) { @@ -246,14 +246,14 @@ export const RETENTION_JOBS: RetentionJob[] = [ archived += n; if (n < size) break; } - return `归档 ${archived} 条待识别条目`; + return `归档 ${archived} 条未识别条目`; }, }, { target: "analyze", jobId: "storage.analyze", - name: "统计信息更新", - description: "对全库执行 ANALYZE,刷新查询规划用的统计信息(每周)", + name: "数据库维护", + description: "每周刷新数据库统计信息", everyMs: 7 * 24 * HOUR, retention: () => null, async run(c) { diff --git a/apps/server/src/modules/websearch/service.ts b/apps/server/src/modules/websearch/service.ts index 5187a61f..d8b46333 100644 --- a/apps/server/src/modules/websearch/service.ts +++ b/apps/server/src/modules/websearch/service.ts @@ -88,7 +88,7 @@ export class WebSearchService { private async client(s: WebSearchSettings) { if (s.provider === "none") - throw new WebSearchServiceError("disabled", "网页搜索未开启,在设置 → Agent → 网页搜索里开启"); + throw new WebSearchServiceError("disabled", "网页搜索未开启,在设置 → 智能体 → 网页搜索里开启"); if (!s.apiKeySecretRef) throw new WebSearchServiceError("auth", "未保存搜索服务 API Key"); const apiKey = await this.ctx.secrets.resolve(s.apiKeySecretRef); return createBraveClient({ @@ -175,7 +175,7 @@ export class WebSearchService { async fetch(url: string, o: { scopeKey: string; signal?: AbortSignal }): Promise { const s = await this.settings(); if (s.provider === "none") - throw new WebSearchServiceError("disabled", "网页搜索未开启,在设置 → Agent → 网页搜索里开启"); + throw new WebSearchServiceError("disabled", "网页搜索未开启,在设置 → 智能体 → 网页搜索里开启"); const want = canonicalUrl(url.trim()); const allow = await pgCacheFor(this.ctx.db).get<{ urls: string[] }>(WEBSEARCH_ALLOW_REGION, o.scopeKey); if (!want || !allow?.urls.includes(want)) diff --git a/apps/server/test/bootstrap.test.ts b/apps/server/test/bootstrap.test.ts index 736db048..64357cac 100644 --- a/apps/server/test/bootstrap.test.ts +++ b/apps/server/test/bootstrap.test.ts @@ -158,7 +158,7 @@ describe.skipIf(!hasPg)("bootstrap + worker(真实 PG)", () => { decisionId, kind: "identity_unresolved", severity: "info", - title: "待识别", + title: "未识别", }); }); const deadline = Date.now() + 20_000; diff --git a/apps/server/test/e2e/e12-interaction.test.ts b/apps/server/test/e2e/e12-interaction.test.ts index f6f22e45..31845c93 100644 --- a/apps/server/test/e2e/e12-interaction.test.ts +++ b/apps/server/test/e2e/e12-interaction.test.ts @@ -93,7 +93,7 @@ const optionsScript = (goal: string): FakeScript => ({ }, }); -// 审批:两个目标各用一个版本目标(拒绝与批准互不影响);ID 在 beforeAll 里填 +// 审批:两个目标各用一个收录版本(拒绝与批准互不影响);ID 在 beforeAll 里填 const T: Record<"approve" | "reject", { slotId: string; releaseId: string }> = { approve: { slotId: ZERO, releaseId: ZERO }, reject: { slotId: ZERO, releaseId: ZERO }, diff --git a/apps/server/test/e2e/e15-agent-quality.test.ts b/apps/server/test/e2e/e15-agent-quality.test.ts index 0dfa6e40..449fbf87 100644 --- a/apps/server/test/e2e/e15-agent-quality.test.ts +++ b/apps/server/test/e2e/e15-agent-quality.test.ts @@ -15,7 +15,7 @@ type Any = any; const hasPg = await pgAvailable(); const HASH = "ab".repeat(20); const SURVEY = "盘点订阅、待处理与下载"; -const LOOP = (at: string) => `巡检库存(${at}):逐个作品查看版本目标`; +const LOOP = (at: string) => `巡检库存(${at}):逐个作品查看收录版本`; describe.skipIf(!hasPg)("E15 Agent 质量(fake 模型 + 真实能力)", () => { const scripts: FakeScript[] = []; diff --git a/apps/server/test/pi/upstreams.test.ts b/apps/server/test/pi/upstreams.test.ts index ad82b41a..40dbf37d 100644 --- a/apps/server/test/pi/upstreams.test.ts +++ b/apps/server/test/pi/upstreams.test.ts @@ -394,7 +394,7 @@ describe.skipIf(!hasPg)("Pi 上游(真实 PG)", () => { const r4 = await newRun(null); const d4 = await drive(env, h.ctx.capabilities, r4, new AbortController().signal); expect(d4).toMatchObject({ status: "failed" }); - expect(d4.message).toMatch(/未配置 Agent 上游/); + expect(d4.message).toMatch(/未配置模型服务/); await setDefaultUpstream(h.ctx, fake.id); }); }); diff --git a/apps/server/test/rules/rule-groups.test.ts b/apps/server/test/rules/rule-groups.test.ts index bbd8aeed..e39a3aad 100644 --- a/apps/server/test/rules/rule-groups.test.ts +++ b/apps/server/test/rules/rule-groups.test.ts @@ -154,7 +154,7 @@ describe.skipIf(!hasPg)("规则组 API(真实 PG)", () => { expect(draft.body.data.items[0]).toMatchObject({ passed: true, pri: 100 }); }); - it("评估接入:版本目标引用规则组 → 硬排除拒绝、pri 进候选评估 trace;被引用的组不能删", async () => { + it("评估接入:收录版本引用规则组 → 硬排除拒绝、pri 进候选评估 trace;被引用的组不能删", async () => { const e = await seedEntity(h.db, { kind: "movie", title: "规则片", year: 2024 }); const slot = await seedSlot(h.db, { entityId: e }); const g = await h.api("POST", "/rules", { name: "片源", ruleString: "4K > 1080P", hardExcludes: ["REMUX"] }); @@ -285,7 +285,7 @@ describe.skipIf(!hasPg)("规则组 API(真实 PG)", () => { expect(plan.evaluation).toMatchObject({ pri: 100 }); }); - it("默认链:订阅默认组有层时对未引用规则组的版本目标生效;PUT /rules/defaults 校验类型", async () => { + it("默认链:订阅默认组有层时对未引用规则组的收录版本生效;PUT /rules/defaults 校验类型", async () => { const defs = (await h.api("GET", "/rules/defaults")).body.data; const dflt = defs.acquisition[0]; expect((await h.api("PUT", "/rules/defaults", { ...defs, acquisition: defs.upgrade })).status).toBe(400); diff --git a/apps/server/test/search/search.test.ts b/apps/server/test/search/search.test.ts index b1b3f22c..c8f611da 100644 --- a/apps/server/test/search/search.test.ts +++ b/apps/server/test/search/search.test.ts @@ -293,7 +293,7 @@ describe.skipIf(!up)("全局搜索", () => { expect(a.body.data.items[0]).toMatchObject({ siteId: sites.mteam, siteReleaseId: "912234" }); }); - it("库存叠加:已识别资源 → resolved + 订阅/版本目标缺口;未识别但标题命中唯一作品别名 → alias", async () => { + it("库存叠加:已识别资源 → resolved + 订阅/收录版本缺口;未识别但标题命中唯一作品别名 → alias", async () => { const entity = ( await db .insertInto("media_entity") @@ -346,7 +346,7 @@ describe.skipIf(!up)("全局搜索", () => { const mt = items.find((i) => i.siteReleaseId === "990001"); expect(mt).toMatchObject({ mediaEntityId: entity, entityMatch: "alias", inventory: { state: "gap" } }); - // 本地作品组:别名精确命中 + 版本目标状态摘要 + // 本地作品组:别名精确命中 + 收录版本状态摘要 const all = await req("/search?q=沙丘2&scope=media"); const local = all.body.data.media.local.items; expect(local[0]).toMatchObject({ @@ -512,7 +512,7 @@ describe.skipIf(!up)("全局搜索", () => { expect(unresolved.status).toBe(412); expect(unresolved.body.error.code).toBe("precondition_failed"); - // 已识别但作品没有版本目标 → submitted=false + no_slot + // 已识别但作品没有收录版本 → submitted=false + no_slot const other = ( await db .insertInto("media_entity") diff --git a/apps/server/test/search/search2.test.ts b/apps/server/test/search/search2.test.ts index 4540bd54..3ff572d3 100644 --- a/apps/server/test/search/search2.test.ts +++ b/apps/server/test/search/search2.test.ts @@ -167,7 +167,7 @@ describe.skipIf(!up)("两阶段搜索(SEARCH-2)", () => { sites.c = await addSite("hdhome", "家园", null, true); await db.updateTable("site").set({ enabled: false }).where("id", "=", sites.c).execute(); - // 本地已建档的沙丘2(接受了 TMDB 693134),有一个未满足的版本目标 + // 本地已建档的沙丘2(接受了 TMDB 693134),有一个未满足的收录版本 dune = ( await db .insertInto("media_entity") diff --git a/apps/server/test/sources-identity/lazy-identity.test.ts b/apps/server/test/sources-identity/lazy-identity.test.ts index 3d279281..dee354d0 100644 --- a/apps/server/test/sources-identity/lazy-identity.test.ts +++ b/apps/server/test/sources-identity/lazy-identity.test.ts @@ -179,7 +179,7 @@ describe.skipIf(!pg)("DEC-1 惰性识别 + 待识别池", () => { expect(ds).toHaveLength(1); const d = ds[0]!; expect(d.severity).toBe("warning"); - expect(d.title).toMatch(/^来源「.+」的 3 条资源待确认身份:Twins$/); + expect(d.title).toMatch(/^来源「.+」的 3 条资源没识别出作品:Twins$/); const ev = d.evidence as { count: number; releases: { sourceReleaseId: string }[] }; expect(ev.count).toBe(3); expect(ev.releases.map((x) => x.sourceReleaseId).sort()).toEqual([...ids].sort()); @@ -228,7 +228,7 @@ describe.skipIf(!pg)("DEC-1 惰性识别 + 待识别池", () => { expect(after.status).toBe("open"); expect((after.evidence as { count: number }).count).toBe(1); expect(after.subject_id).toBe(pool[1]!.source_release_id); - expect(after.title).toMatch(/^待确认身份:Nightfall\.1995\./); + expect(after.title).toMatch(/^没识别出作品:Nightfall\.1995\./); // 最后一条忽略 → 事项关闭 const last = await h.db .selectFrom("identity_candidate") @@ -336,7 +336,7 @@ describe.skipIf(!pg)("迁移 0047:存量 identity_unresolved 清理", () => { .values({ kind: "identity_unresolved", severity: "info", - title: `待识别:${title}`, + title: `未识别:${title}`, subject_type: "source_release", subject_id: r.id, dedupe_key: `identity:${r.id}`, diff --git a/apps/server/test/subscriptions/subscriptions-c1a.test.ts b/apps/server/test/subscriptions/subscriptions-c1a.test.ts index e2ee728f..5f3b94a4 100644 --- a/apps/server/test/subscriptions/subscriptions-c1a.test.ts +++ b/apps/server/test/subscriptions/subscriptions-c1a.test.ts @@ -26,7 +26,7 @@ import { presetConditions, } from "../../src/modules/acquisition-organize/services/subscriptions-params.ts"; -// C1-A 订阅遗留收尾:包含 / 排除 → 订阅私有规则组(评估链纳入、随订阅删除)、MP 分辨率 / 来源 / 特效 → 版本目标、 +// C1-A 订阅遗留收尾:包含 / 排除 → 订阅私有规则组(评估链纳入、随订阅删除)、MP 分辨率 / 来源 / 特效 → 收录版本、 // 保存路径经路径映射传给 qB、库根用于首次整理、下载器实例、重置进度、日历。 const hasPg = await pgAvailable(); @@ -182,7 +182,7 @@ describe.skipIf(!hasPg)("C1-A 订阅遗留收尾(真实 PG + mock qB)", () => { expect(again[0]!.hard).toEqual({ exclude: ["CAM"] }); }); - it("评估链纳入私有组:包含没命中 / 排除命中都拒绝;命中包含建计划;与版本目标自己的包含取 AND", async () => { + it("评估链纳入私有组:包含没命中 / 排除命中都拒绝;命中包含建计划;与收录版本自己的包含取 AND", async () => { const e = await seedEntity(db(), { kind: "movie", title: "Filter Film", year: 2024 }); const c = await h.api("POST", "/subscriptions", { mediaEntityId: e, @@ -207,7 +207,7 @@ describe.skipIf(!hasPg)("C1-A 订阅遗留收尾(真实 PG + mock qB)", () => { const ok = await release(e, "Filter.Film.2024.1080p.WEB-DL.CHS.x264-GRP"); const r3 = await evaluateAcquisition(h.d, { sourceReleaseId: ok }); expect(r3.plans, JSON.stringify(r3)).toHaveLength(1); - // 版本目标自己也要求 "GRP2":与私有组的 CHS 取 AND + // 收录版本自己也要求 "GRP2":与私有组的 CHS 取 AND await db().updateTable("acquisition_plan").set({ status: "cancelled" }).where("id", "=", r3.plans[0]!).execute(); await db() .updateTable("acquisition_slot") @@ -223,7 +223,7 @@ describe.skipIf(!hasPg)("C1-A 订阅遗留收尾(真实 PG + mock qB)", () => { expect((await evaluateAcquisition(h.d, { sourceReleaseId: both })).plans).toHaveLength(1); }); - it("MP 分辨率 / 来源 / 特效:新建时建默认版本目标承载预设;改预设覆盖同一字段,手改过的字段保留;无法识别 400", async () => { + it("MP 分辨率 / 来源 / 特效:新建时建默认收录版本承载预设;改预设覆盖同一字段,手改过的字段保留;无法识别 400", async () => { const e = await seedEntity(db(), { kind: "series", title: "预设剧", year: 2025 }); const bad = await h.api("POST", "/subscriptions", { mediaEntityId: e, @@ -461,7 +461,7 @@ describe.skipIf(!hasPg)("C1-A 订阅遗留收尾(真实 PG + mock qB)", () => { }); expect(c.status, JSON.stringify(c.body)).toBe(201); const subId = c.body.data.id as string; - // E01 已入库(单版本目标:作品下资产都算) + // E01 已入库(单收录版本:作品下资产都算) const a = await db() .insertInto("media_asset") .values({ diff --git a/apps/server/test/subscriptions/subscriptions-v2.test.ts b/apps/server/test/subscriptions/subscriptions-v2.test.ts index aa28b637..6f372ec8 100644 --- a/apps/server/test/subscriptions/subscriptions-v2.test.ts +++ b/apps/server/test/subscriptions/subscriptions-v2.test.ts @@ -264,7 +264,7 @@ describe.skipIf(!hasPg)("订阅重做(真实 PG)", () => { expect((await h.api("GET", "/subscriptions/00000000-0000-0000-0000-000000000000/coverage")).status).toBe(404); }); - it("覆盖矩阵:集范围裁剪;总集数未知列到已见最高集并标 suspected;电影按内容版本 × 版本目标", async () => { + it("覆盖矩阵:集范围裁剪;总集数未知列到已见最高集并标 suspected;电影按内容版本 × 收录版本", async () => { const e = await seedEntity(db(), { kind: "series", title: "范围剧", year: 2025 }); await season(e, 1, null); const e3 = await episode(e, 1, 3); @@ -325,7 +325,7 @@ describe.skipIf(!hasPg)("订阅重做(真实 PG)", () => { // 暂停 const e4 = await seedEntity(db(), { kind: "series", title: `${tag}丁`, year: 2022 }); const s4 = await sub(e4, { status: "paused" }); - // 已完成(电影:唯一版本目标已入库) + // 已完成(电影:唯一收录版本已入库) const e5 = await seedEntity(db(), { kind: "movie", title: `${tag}戊`, year: 2021 }); const a5 = await asset(e5); const s5 = await sub(e5, { kind: "movie", mode: "once" }); @@ -584,7 +584,7 @@ describe.skipIf(!hasPg)("订阅重做(真实 PG)", () => { expect(r.status, JSON.stringify(bad)).toBe(400); } - // PATCH:autoFill 布尔 → on/off;best_version → 版本目标升级策略 + // PATCH:autoFill 布尔 → on/off;best_version → 收录版本升级策略 const sid = c.body.data.id; const sl = await slot(e, sid, "main"); const u = await h.api("PATCH", `/subscriptions/${sid}`, { @@ -611,7 +611,7 @@ describe.skipIf(!hasPg)("订阅重做(真实 PG)", () => { ).toEqual({ enabled: false, mode: "full_pack" }); }); - it("暂停 / 恢复 / 重新订阅 / 批量 / 版本目标排序", async () => { + it("暂停 / 恢复 / 重新订阅 / 批量 / 收录版本排序", async () => { const e = await seedEntity(db(), { kind: "movie", title: "动作片", year: 2024 }); const s = await sub(e, { kind: "movie", mode: "once" }); const a = await slot(e, s, "A", { position: 0 }); diff --git a/apps/server/test/system/storage.test.ts b/apps/server/test/system/storage.test.ts index 7788d378..0ade5655 100644 --- a/apps/server/test/system/storage.test.ts +++ b/apps/server/test/system/storage.test.ts @@ -211,7 +211,7 @@ describe.skipIf(!hasPg)("CACHE-2 存储(真实 PG)", () => { (${done}, 1, 'run_started', ${old}), (${done}, 2, 'run_started', ${now}), (${live}, 1, 'run_started', ${old})`.execute( h.db, ); - expect(await job("agent_event")(now)).toMatch(/Agent 事件 1、/); + expect(await job("agent_event")(now)).toMatch(/智能体事件 1、/); expect(await count(sql<{ n: number }>`select count(*)::int as n from agent_run_event`)).toBe(2); await sql`delete from audit_event`.execute(h.db); @@ -250,7 +250,7 @@ describe.skipIf(!hasPg)("CACHE-2 存储(真实 PG)", () => { await cand("r1", "unmatched", 31); await cand("r2", "unmatched", 29); await cand("r3", "ambiguous", 31); - expect(await job("identity_pool")(now)).toBe("归档 1 条待识别条目"); + expect(await job("identity_pool")(now)).toBe("归档 1 条未识别条目"); const st = await sql<{ t: string; s: string }>`select sr.title as t, c.status as s from identity_candidate c join source_release sr on sr.id = c.source_release_id order by sr.title`.execute(h.db); expect(st.rows).toEqual([ diff --git a/apps/web/app/api/agent-mock.test.ts b/apps/web/app/api/agent-mock.test.ts index c86f01f9..11f4de5f 100644 --- a/apps/web/app/api/agent-mock.test.ts +++ b/apps/web/app/api/agent-mock.test.ts @@ -15,7 +15,7 @@ class MemStore { } // 验收 S7:生产构建里 ?mock=1 不得把演示数据当真实运行展示 -describe("Agent 演示开关", () => { +describe("智能体演示开关", () => { it("只在开发构建或后端 provider=fake 时允许", () => { expect(agentMockAllowed({ dev: true })).toBe(true); expect(agentMockAllowed({ dev: false, provider: "fake" })).toBe(true); diff --git a/apps/web/app/api/downloads.ts b/apps/web/app/api/downloads.ts index 86da4215..95729455 100644 --- a/apps/web/app/api/downloads.ts +++ b/apps/web/app/api/downloads.ts @@ -155,9 +155,9 @@ export const DOWNLOAD_STATUS_TONE: Record = { diff --git a/apps/web/app/api/feeds.ts b/apps/web/app/api/feeds.ts index 731b84df..30bc440a 100644 --- a/apps/web/app/api/feeds.ts +++ b/apps/web/app/api/feeds.ts @@ -106,7 +106,7 @@ 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 ? "已排补漏" : "RSS 未关联站点,无法自动补漏"})`; } if (!f.enabled || !f.lastSuccessAt) return null; const gapMs = now - new Date(f.lastSuccessAt).getTime(); diff --git a/apps/web/app/api/inventory.ts b/apps/web/app/api/inventory.ts index 3b1230fb..fd1d0dfe 100644 --- a/apps/web/app/api/inventory.ts +++ b/apps/web/app/api/inventory.ts @@ -64,7 +64,7 @@ export function inventoryFlags( }); } if (releaseResolution && releaseResolution === "resolved" && flags.length === 0) { - flags.push({ key: "tracked", label: "已识别,无版本目标", tone: "neutral" }); + flags.push({ key: "tracked", label: "已识别,无收录版本", tone: "neutral" }); } return flags; } diff --git a/apps/web/app/api/records.ts b/apps/web/app/api/records.ts index 1bb58dd2..7df956d0 100644 --- a/apps/web/app/api/records.ts +++ b/apps/web/app/api/records.ts @@ -107,7 +107,7 @@ export const RECORD_STATUS_TONE: Record = { auto: "自动", manual: "手动", agent: "Agent" }; +export const TRIGGER_LABEL: Record = { auto: "自动", manual: "手动", agent: "智能体" }; /** dryRun 生成的整理计划冻结为 artifact(kind=plan),GET /artifacts/{id} 读取逐文件预览 */ export function useArtifact(id: string | null | undefined) { diff --git a/apps/web/app/api/rules.ts b/apps/web/app/api/rules.ts index 4d822be7..f38d8e6c 100644 --- a/apps/web/app/api/rules.ts +++ b/apps/web/app/api/rules.ts @@ -99,14 +99,14 @@ export function useRuleSuppressions() { export type RuleRefs = { /** 规则组 id → 引用它的订阅(params.ruleGroupIds 或私有组) */ subscriptions: Map; - /** 规则组 id → 引用它的版本目标 */ + /** 规则组 id → 引用它的收录版本 */ slots: Map; }; const SubPage = cursorPage(Subscription); const SlotPage = cursorPage(AcquisitionSlot); -/** 引用关系:列表接口不带引用计数,前端从订阅与版本目标反查(各取前 200 条) */ +/** 引用关系:列表接口不带引用计数,前端从订阅与收录版本反查(各取前 200 条) */ export function useRuleRefs() { return useQuery({ queryKey: ruleKeys.refs, @@ -150,7 +150,7 @@ function useInvalidate() { const qc = useQueryClient(); return () => { void qc.invalidateQueries({ queryKey: ruleKeys.all }); - // 旧偏好标签页与订阅/版本目标的规则组显示同源 + // 旧偏好标签页与订阅/收录版本的规则组显示同源 void qc.invalidateQueries({ queryKey: ["preferences"] }); }; } @@ -175,7 +175,7 @@ export function useUpdateRuleGroup() { }); } -/** 删除:标记删除并写抑制记录;被版本目标/订阅引用时 409 */ +/** 删除:标记删除并写抑制记录;被收录版本/订阅引用时 409 */ export function useDeleteRuleGroup() { const done = useInvalidate(); return useMutation({ diff --git a/apps/web/app/api/sites.ts b/apps/web/app/api/sites.ts index e448799c..69a3b7f6 100644 --- a/apps/web/app/api/sites.ts +++ b/apps/web/app/api/sites.ts @@ -105,7 +105,7 @@ export function useSiteRequestStats(siteId: string | undefined) { export function accessSummary(s: SiteRequestStats): string { const dur = (sec: number) => (sec % 3600 === 0 ? `${sec / 3600} 小时` : `${Math.round(sec / 60)} 分钟`); const parts = [ - `快照每 ${dur(s.snapshotIntervalSec)} ${s.access.snapshotMaxPages} 页`, + `抓取每 ${dur(s.snapshotIntervalSec)} ${s.access.snapshotMaxPages} 页`, `健康检测每 ${dur(s.access.healthCheckIntervalSec)}`, ]; if (s.access.detailCacheSec > 0) parts.push(`详情缓存 ${dur(s.access.detailCacheSec)}`); diff --git a/apps/web/app/api/subscriptions.ts b/apps/web/app/api/subscriptions.ts index f4382228..06a479fe 100644 --- a/apps/web/app/api/subscriptions.ts +++ b/apps/web/app/api/subscriptions.ts @@ -77,7 +77,7 @@ export function useUpdateSubscription() { api.patch(`/subscriptions/${id}`, body).then((d) => parseOrThrow(Subscription, d, "/subscriptions")), onSuccess: () => { void qc.invalidateQueries({ queryKey: subscriptionKeys.all }); - // best_version 写到版本目标的升级策略 + // best_version 写到收录版本的升级策略 void qc.invalidateQueries({ queryKey: ["slots"] }); }, }); @@ -246,7 +246,7 @@ export function useFillSlot() { }); } -/** 版本目标排序:第一个成为主版本 */ +/** 收录版本排序:第一个成为主版本 */ export function useSetSlotOrder() { const done = useInvalidateSubs(); return useMutation({ diff --git a/apps/web/app/api/tasks.ts b/apps/web/app/api/tasks.ts index ab9b9900..040a1661 100644 --- a/apps/web/app/api/tasks.ts +++ b/apps/web/app/api/tasks.ts @@ -11,7 +11,8 @@ import { type TaskStatus, } from "@umt/contracts"; import { organizationActionLabel, organizationLabel } from "~/lib/labels.ts"; -import { PLAN_STATUS_LABEL } from "./downloads.ts"; +import { humanizeReason, reasonLine } from "~/lib/reasons.ts"; +import { PAUSE_REASON_LABEL, PLAN_STATUS_LABEL } from "./downloads.ts"; import { apiParsed } from "./parse.ts"; // 业务任务视图:按 trace 汇总的业务时间线(参考 MovieClaw task-activity) @@ -23,7 +24,7 @@ const RecordPage = cursorPage(PublicOrganizationRecord); const DecisionPage = cursorPage(Decision); const RESOLUTION_LABEL = { - unresolved: "待识别", + unresolved: "未识别", resolved: "已识别", ambiguous: "有歧义,待确认", unidentifiable: "无法识别", @@ -173,7 +174,7 @@ export function buildTimeline(input: { stage: "plan", at: plan.createdAt, title: `创建下载计划(${plan.purpose === "fill" ? "补缺" : plan.purpose === "upgrade" ? "升级" : "换源"})`, - detail: plan.statusReason, + detail: humanizeReason(plan.statusReason), tone: "neutral", }); if (plan.updatedAt !== plan.createdAt) @@ -182,7 +183,7 @@ export function buildTimeline(input: { stage: "plan", at: plan.updatedAt, title: `计划状态:${PLAN_STATUS_LABEL[plan.status]}`, - detail: plan.statusReason, + detail: humanizeReason(plan.statusReason), tone: plan.status === "failed" ? "destructive" : plan.status === "expired" ? "warning" : "neutral", }); } @@ -199,7 +200,7 @@ export function buildTimeline(input: { key: "dl-pause", stage: "download", at: task.updatedAt, - title: task.pauseReason === "threshold" ? "缓存达到硬阈值,已暂停" : `已暂停(${task.pauseReason})`, + title: task.pauseReason === "threshold" ? "下载盘超过暂停线,已暂停" : PAUSE_REASON_LABEL[task.pauseReason], tone: "warning", ongoing: task.status === "paused", }); @@ -239,7 +240,15 @@ export function buildTimeline(input: { stage: "organization", at: r.startedAt ?? r.createdAt, title: `整理 ${organizationActionLabel(r.action)}:${organizationLabel(r.status).label}`, - detail: `${c.succeeded}/${c.total} 文件成功${c.failed ? `,${c.failed} 失败` : ""}${c.skipped ? `,${c.skipped} 跳过` : ""}${r.reason ? ` · ${r.reason.message}` : ""}`, + detail: + [ + c.total ? `${c.succeeded}/${c.total} 个文件成功` : null, + c.failed ? `${c.failed} 个失败` : null, + c.skipped ? `${c.skipped} 个跳过` : null, + r.reason ? reasonLine(r.reason) : null, + ] + .filter(Boolean) + .join(" · ") || null, tone: RECORD_TONE[r.status], href: `/console/organize/${r.id}`, ongoing: r.status === "running" || r.status === "queued", @@ -260,7 +269,7 @@ export function buildTimeline(input: { key: `agent-${d.id}`, stage: "agent", at: d.updatedAt, - title: "Agent 已介入调查", + title: "智能体已介入调查", tone: "neutral", href: `/console/agent/${d.agentRunId}`, }); @@ -275,7 +284,9 @@ export function buildTimeline(input: { at: t.startedAt ?? t.createdAt, title: `任务 ${TASK_KIND_LABEL[t.kind] ?? t.kind}:${TASK_STATUS_TEXT[t.status] ?? t.status}`, detail: - [t.attempts > 1 ? `第 ${t.attempts} 次尝试` : null, t.waitReason, err].filter(Boolean).join(" · ") || null, + [t.attempts > 1 ? `第 ${t.attempts} 次尝试` : null, humanizeReason(t.waitReason), humanizeReason(err)] + .filter(Boolean) + .join(" · ") || null, tone: TASK_STATUS_TONE[t.status], href: `/console/tasks/${encodeURIComponent(t.id)}`, ongoing: ACTIVE_TASK.includes(t.status), diff --git a/apps/web/app/components/agent/agent-conversation.tsx b/apps/web/app/components/agent/agent-conversation.tsx index a9863c31..552cf1e3 100644 --- a/apps/web/app/components/agent/agent-conversation.tsx +++ b/apps/web/app/components/agent/agent-conversation.tsx @@ -32,6 +32,8 @@ import { useDecideApproval, useIsAdminSession, } from "~/api/agent.ts"; +import { DOWNLOAD_STATUS_LABEL, PLAN_STATUS_LABEL } from "~/api/downloads.ts"; +import { RECORD_STATUS_LABEL } from "~/api/records.ts"; import { ApprovalCard, type ApprovalCardStatus } from "~/components/agentui/approval-card.tsx"; import { type CitationItem, CitationList } from "~/components/agentui/citations.tsx"; import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx"; @@ -51,8 +53,11 @@ import { StreamingCursor, StreamingResponse } from "~/components/agentui/streami import { ToolApproval } from "~/components/agentui/tool-approval.tsx"; import { ToolResult, ToolResultOutput, type ToolResultStatus } from "~/components/agentui/tool-result.tsx"; import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx"; +import { TechInfo } from "~/components/console/tech-info.tsx"; +import { formatUsd } from "~/components/schedules/labels.ts"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; +import { humanizeReason } from "~/lib/reasons.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; import { InlineApproval, OptionsCard } from "./agent-interaction.tsx"; import { ToolCallIo } from "./tool-call-card-io.tsx"; @@ -103,16 +108,13 @@ function CapabilityBadges({ id, extra }: { id: string; extra?: ReactNode }) { const m = capabilityMeta(id); return ( <> + {/* 只标需要注意的:会改动数据的动作和中高风险;只读、scope、幂等进技术信息 */} {m ? ( <> - {KIND_LABEL[m.kind] ?? m.kind} - scope · {m.scope} - - {m.idempotent ? 幂等 : null} + {m.kind !== "read" ? {KIND_LABEL[m.kind] ?? m.kind} : null} + {m.riskLevel !== "low" ? : null} - ) : ( - 未登记能力 - )} + ) : null} {extra} ); @@ -197,20 +199,21 @@ export function ToolCallCard({ {authReason ? "授权校验未通过" : "白名单拦截"}} + extra={{authReason ? "需要你批准" : "本次没有开放"}} /> } reason={authReason ?? call.reason ?? "未授权"} explanation={ authReason - ? '有副作用的能力需要有效的持续授权或单次审批,由服务端在执行前核对,模型声称"用户已同意"无效。控制台发起的会话里,需审批的动作会在对应步骤出审批卡;也可以发布收录策略生成持续授权。' - : "这个能力不在本次运行的白名单内,或已超出调用预算,调用没有执行。" + ? "会改动数据的操作要你批准:在对应步骤的审批卡上批准,或在设置 → 智能体里给它持续授权。" + : "这次会话没有开放这个操作,或已用完调用次数。" } parameters={[ + { id: "cap", label: "能力", value: call.capabilityId }, { id: "call", label: "调用 ID", value: call.toolCallId }, ...(audit?.idempotencyKey ? [{ id: "idem", label: "幂等键", value: audit.idempotencyKey }] : []), { id: "at", label: "时间", value: formatDateTime(call.startedAt) }, @@ -222,9 +225,8 @@ export function ToolCallCard({ return ( 等待结果…

) : null} -
-
调用 ID
-
{call.toolCallId}
-
幂等键
-
- {audit?.idempotencyKey ?? - (meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读能力不记操作账本")} -
- {audit?.grantId || audit?.approvalId ? ( - <> -
授权依据
-
- {audit.grantId ? `持续授权 ${audit.grantId.slice(0, 8)}` : `单次审批 ${audit.approvalId?.slice(0, 8)}`} -
- - ) : null} -
- + +
+
能力
+
+ {call.capabilityId} + {meta + ? ` · ${KIND_LABEL[meta.kind] ?? meta.kind} · scope ${meta.scope}${meta.idempotent ? " · 幂等" : ""}` + : ""} +
+ {durationText(call.startedAt, call.endedAt) ? ( + <> +
耗时
+
{durationText(call.startedAt, call.endedAt)}
+ + ) : null} +
调用 ID
+
{call.toolCallId}
+
幂等键
+
+ {audit?.idempotencyKey ?? + (meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读操作不记录")} +
+ {audit?.grantId || audit?.approvalId ? ( + <> +
授权依据
+
+ {audit.grantId ? `持续授权 ${audit.grantId.slice(0, 8)}` : `单次审批 ${audit.approvalId?.slice(0, 8)}`} +
+ + ) : null} +
+ +
); } @@ -652,7 +669,7 @@ export function AgentConversation({ at: m.at, role: "user", foldable: false, - node: , + node: , }); entries.sort((a, b) => a.at.localeCompare(b.at)); const blocks = groupConversation(entries); @@ -678,13 +695,13 @@ export function AgentConversation({ b.kind === "single" ? (
{b.entry.node}
) : ( - + - Agent + 智能体 {b.parts.map((p) => @@ -773,35 +790,60 @@ export function BudgetBar({ ? (run.finishedAt ? new Date(run.finishedAt).getTime() : Date.now()) - new Date(run.startedAt).getTime() : undefined); return ( -
- String(n)} - exceeded={exceeded === "tool_calls"} - /> - (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))} - exceeded={exceeded === "tokens"} - /> - `${n}s`} - exceeded={exceeded === "duration"} - /> - `$${n.toFixed(3)}`} - exceeded={exceeded === "cost"} - /> +
+
+ formatUsd(n)} + exceeded={exceeded === "cost"} + /> + `${n} 秒`} + exceeded={exceeded === "duration"} + /> +
+ {/* 调用次数与 token 超限时直接显示,否则收进技术信息 */} + {exceeded === "tool_calls" || exceeded === "tokens" ? ( +
+ String(n)} + exceeded={exceeded === "tool_calls"} + /> + (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n))} + exceeded={exceeded === "tokens"} + /> +
+ ) : ( + + )}
); } @@ -947,17 +989,25 @@ export function FinalReport({ ...report.downloads.map((d) => ({ id: `d${d.downloadTaskId}`, icon: , - title: d.name ?? `下载任务 ${d.downloadTaskId.slice(0, 8)}`, + title: d.name ?? "下载任务", domain: "下载任务", - meta: {d.status}, + meta: ( + + {DOWNLOAD_STATUS_LABEL[d.status as keyof typeof DOWNLOAD_STATUS_LABEL] ?? d.status} + + ), to: `/console/downloads/${d.downloadTaskId}`, })), ...report.organization.map((o) => ({ id: `o${o.recordId}`, icon: , - title: o.targetPath ?? `整理记录 ${o.recordId.slice(0, 8)}`, + title: o.targetPath ?? "整理记录", domain: "整理记录", - meta: {o.status}, + meta: ( + + {RECORD_STATUS_LABEL[o.status as keyof typeof RECORD_STATUS_LABEL] ?? o.status} + + ), to: `/console/organize/${o.recordId}`, })), ] @@ -976,7 +1026,6 @@ export function FinalReport({ {said ? : "(没有文字汇报)"} -

仅供参考,不作为完成依据。

@@ -985,19 +1034,19 @@ export function FinalReport({ ) : ( )} - 系统核实的结果 + 实际结果

-
- {state ? {state.label} : null} - 运行{RUN_STATUS_LABEL[run.status]} - 工具成功 {count("succeeded")} - {count("failed") ? 失败 {count("failed")} : null} - {denied ? 被拒绝 {denied} : null} -
+ {state || count("failed") || denied ? ( +
+ {state ? {state.label} : null} + {count("failed") ? {count("failed")} 步失败 : null} + {denied ? {denied} 步未执行 : null} +
+ ) : null} {report ? ( <>

{report.summary}

-
+
{( [ ["已提交", report.submitted], @@ -1005,20 +1054,26 @@ export function FinalReport({ ["进行中", report.inProgress], ["失败", report.failed], ] as const - ).map(([k, v]) => ( -
-
{k}
-
{v}
-
- ))} + ) + .filter(([, v]) => v > 0) + .map(([k, v]) => ( +
+
{k}
+
{v}
+
+ ))}
{report.plans.length ? (
    {report.plans.map((p) => (
  • 下载计划 - {p.planId.slice(0, 8)} - {p.status} + + {PLAN_STATUS_LABEL[p.status as keyof typeof PLAN_STATUS_LABEL] ?? p.status} +
  • ))}
@@ -1037,18 +1092,17 @@ export function FinalReport({ }))} /> ) : null} -

- 取自下载计划、下载任务和整理记录(生成于 {formatDateTime(report.generatedAt)} - )。「已提交」指已进入下载器,整理成功才算入库。 -

) : (

服务端汇报还没有生成,以任务和整理记录的实际状态为准。

)} {run.error ? ( -

- {String((run.error as { message?: unknown }).message ?? JSON.stringify(run.error))} -

+ <> +

+ {humanizeReason(String((run.error as { message?: unknown }).message ?? "")) || "会话出错"} +

+ + ) : null}
diff --git a/apps/web/app/components/agent/agent-drawer.tsx b/apps/web/app/components/agent/agent-drawer.tsx index a846a22b..4a4d80a2 100644 --- a/apps/web/app/components/agent/agent-drawer.tsx +++ b/apps/web/app/components/agent/agent-drawer.tsx @@ -25,8 +25,8 @@ import { setAgentDrawerOpen, setAgentDrawerRun, setAgentDrawerTab, + shortGoalTitle, showAgentRunInDrawer, - splitContextGoal, useAgentDrawer, } from "./agent-store.ts"; import { AgentRunOpenContext } from "./agent-subagent-tree.tsx"; @@ -72,7 +72,7 @@ function DrawerTabs({ tab }: { tab: AgentDrawerTab }) { return (
{TABS.map((t) => ( @@ -108,7 +108,7 @@ function HistoryRunView({ runId }: { runId: string }) { 返回列表

- {run.data ? splitContextGoal(run.data.run.goal).text : ""} + {run.data ? shortGoalTitle(run.data.run.goal) : ""}

{formatDateTime(view.expiresAt)} 前没选, - {view.timeoutAction === "recommended" ? "自动选推荐项" : "Agent 停止"}。 + {view.timeoutAction === "recommended" ? "自动选推荐项" : "智能体停止"}。
)} @@ -285,7 +285,7 @@ export function InlineApproval({ view, stepTitle }: { view: ApprovalView; stepTi ...(s === "rejected" && reason.trim() ? { reason: reason.trim() } : {}), }, { - onSuccess: () => toast.success(s === "approved" ? "已批准,Agent 继续执行" : "已拒绝,Agent 按理由调整"), + onSuccess: () => toast.success(s === "approved" ? "已批准,智能体继续执行" : "已拒绝,智能体按理由调整"), onError: (e) => toast.error(errorMessage(e)), }, ); @@ -332,7 +332,7 @@ export function InlineApproval({ view, stepTitle }: { view: ApprovalView; stepTi value={reason} maxLength={2000} aria-label="拒绝理由" - placeholder="拒绝时写理由或建议(可选),Agent 按它调整" + placeholder="拒绝时写理由或建议(可选),智能体按它调整" onChange={(e) => setReason(e.target.value)} /> ) : null} diff --git a/apps/web/app/components/agent/agent-run-panel.tsx b/apps/web/app/components/agent/agent-run-panel.tsx index fe820833..728e6177 100644 --- a/apps/web/app/components/agent/agent-run-panel.tsx +++ b/apps/web/app/components/agent/agent-run-panel.tsx @@ -25,6 +25,7 @@ import { PromptInput } from "~/components/agentui/prompt-input.tsx"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; +import { formatUsd } from "~/components/schedules/labels.ts"; import { Button } from "~/components/ui/button.tsx"; import { Label } from "~/components/ui/label.tsx"; import { Switch } from "~/components/ui/switch.tsx"; @@ -114,9 +115,8 @@ export function GoalComposer({ />

{proposeFirst - ? "Agent 先调查并给出方案,你选定后才执行。" - : "Agent 列出步骤后直接执行,需要审批的动作会停在对应步骤等你批准。"}{" "} - Enter 发送,Shift+Enter 换行。 + ? "智能体先调查并给出方案,你选定后才执行。" + : "智能体列出步骤后直接执行,需要审批的动作会停在对应步骤等你批准。"}

{suggestions?.length ? (
@@ -143,10 +143,6 @@ export function GoalComposer({ ); } -function shortTokens(n: number | undefined) { - return n === undefined ? null : n >= 1000 ? `${(n / 1000).toFixed(1)}k token` : `${n} token`; -} - /** 状态 + 来源 + 时间 + 模型 */ function RunMeta({ run, status }: { run: RunSnapshot["run"]; status: AgentRunStatus }) { return ( @@ -163,7 +159,6 @@ function RunMeta({ run, status }: { run: RunSnapshot["run"]; status: AgentRunSta

{TRIGGER_LABEL[run.trigger]} · {formatDateTime(run.createdAt)} - {run.model ? ` · ${run.provider ?? ""}/${run.model}` : ""}

); @@ -194,8 +189,7 @@ function RunOverviewBar({ const current = steps.find((s) => s.status === "running") ?? steps.find((s) => s.status === "pending"); const failed = steps.some((s) => s.status === "failed"); const u = { ...r.usage, ...(live.usage ?? {}) }; - const calls = Math.max(r.toolCallCount, live.items.filter((i) => i.kind === "tool").length); - const usage = [`${calls} 次调用`, shortTokens(u.totalTokens)].filter(Boolean).join(" · "); + const usage = u.costUsd ? `费用约 ${formatUsd(u.costUsd)}` : ""; return (
, @@ -196,7 +197,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b onNavigate?.(); toggleAgentDrawer(pathname); }; - const title = `Agent(${shortcut})${summary ? `\n${summary}` : ""}`; + const title = `智能体(${shortcut})${summary ? `\n${summary}` : ""}`; if (collapsed) { return ( @@ -205,7 +206,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b {...{ [AGENT_LAUNCHER_ATTR]: "" }} onClick={activate} title={title} - aria-label={count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent"} + aria-label={count ? `打开智能体(${count} 个会话进行中)` : "打开智能体"} aria-keyshortcuts="Control+J Meta+J" className="prism-ring relative mb-2 grid h-10 w-10 shrink-0 cursor-pointer place-items-center rounded-xl text-foreground outline-none transition-transform duration-micro active:scale-[0.95] focus-visible:ring-2 focus-visible:ring-ring" > @@ -228,12 +229,10 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b - Agent + 智能体 {count ? : null} - - {count ? `${count} 个会话进行中` : "交代目标,边做边汇报"} - + {count ? {count} 个会话进行中 : null} {count ? ( @@ -250,8 +249,8 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b export function AgentShortcutsHelp() { const shortcut = agentShortcutLabel(); const rows: [string, string][] = [ - [shortcut, "打开 / 收起 Agent,附带当前页面"], - ["Esc", "关闭 Agent"], + [shortcut, "打开 / 收起智能体,附带当前页面"], + ["Esc", "关闭智能体"], ["Enter", "发送目标;Shift+Enter 换行"], ]; return ( diff --git a/apps/web/app/components/console/data-table.test.ts b/apps/web/app/components/console/data-table.test.ts index a21a88a3..29011569 100644 --- a/apps/web/app/components/console/data-table.test.ts +++ b/apps/web/app/components/console/data-table.test.ts @@ -6,15 +6,15 @@ import { groupRows, readCollapsed, toggleCollapsed, writeCollapsed } from "./dat type Row = { id: string; status: string; name: string }; const rows: Row[] = [ - { id: "1", status: "待识别", name: "a.mkv" }, + { id: "1", status: "未识别", name: "a.mkv" }, { id: "2", status: "已登记", name: "b.mkv" }, - { id: "3", status: "待识别", name: "c.mkv" }, + { id: "3", status: "未识别", name: "c.mkv" }, ]; describe("DataTable groupBy:分组逻辑", () => { it("按首次出现顺序分组,组内保持原顺序", () => { expect(groupRows(rows, (r) => r.status).map((g) => [g.key, g.rows.map((r) => r.id)])).toEqual([ - ["待识别", ["1", "3"]], + ["未识别", ["1", "3"]], ["已登记", ["2"]], ]); }); @@ -24,7 +24,7 @@ describe("DataTable groupBy:分组逻辑", () => { (r) => r.status, (a, b) => a.localeCompare(b, "zh-CN"), ); - expect(g.map((x) => x.key)).toEqual(["待识别", "已登记"].sort((a, b) => a.localeCompare(b, "zh-CN"))); + expect(g.map((x) => x.key)).toEqual(["未识别", "已登记"].sort((a, b) => a.localeCompare(b, "zh-CN"))); }); it("折叠切换返回新集合", () => { const a = new Set(); @@ -72,8 +72,8 @@ describe("DataTable groupBy:渲染", () => { }), ); expect(html.match(/aria-expanded="true"/g)).toHaveLength(2); - expect(html).toContain("待识别"); - expect(html).toMatch(/待识别<\/span>]*>2<\/span>/); + expect(html).toContain("未识别"); + expect(html).toMatch(/未识别<\/span>]*>2<\/span>/); expect(html).toMatch(/已登记<\/span>]*>1<\/span>/); for (const n of ["a.mkv", "b.mkv", "c.mkv"]) expect(html).toContain(n); }); diff --git a/apps/web/app/components/console/nav.test.ts b/apps/web/app/components/console/nav.test.ts index e2b78838..08e2d01a 100644 --- a/apps/web/app/components/console/nav.test.ts +++ b/apps/web/app/components/console/nav.test.ts @@ -19,10 +19,10 @@ describe("控制台导航", () => { it("IA-3:5 组,按定稿清单逐项列出,路径唯一且都在 /console 下", () => { expect(NAV_GROUPS.map((g) => g.title)).toEqual(["概览", "资源", "订阅", "整理", "系统"]); expect(NAV_GROUPS.map((g) => g.items.map((i) => i.label))).toEqual([ - ["首页", "任务", "计划", "Agent 会话"], - ["站点", "RSS 订阅"], + ["首页", "任务", "计划", "智能体会话"], + ["站点", "RSS"], ["订阅", "规则"], - ["下载面板", "整理记录", "文件管理", "媒体概览"], + ["下载面板", "整理记录", "文件管理", "媒体库"], ["设置", "审计", "运行状态"], ]); // IA-3 逐项清单 14 项 + FE-6 计划页 @@ -44,7 +44,7 @@ describe("控制台导航", () => { it("按最长前缀匹配当前项", () => { expect(findNavItem("/console")?.label).toBe("首页"); expect(findNavItem("/console/")?.label).toBe("首页"); - expect(findNavItem("/console/library/abc")?.label).toBe("媒体概览"); + expect(findNavItem("/console/library/abc")?.label).toBe("媒体库"); expect(findNavItem("/console/tasks/r1")?.label).toBe("任务"); expect(findNavItem("/console/schedules")?.label).toBe("计划"); expect(findNavItem("/console/downloads")?.label).toBe("下载面板"); diff --git a/apps/web/app/components/console/nav.ts b/apps/web/app/components/console/nav.ts index b735ca90..5405f755 100644 --- a/apps/web/app/components/console/nav.ts +++ b/apps/web/app/components/console/nav.ts @@ -46,25 +46,25 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console", label: "首页", icon: House, - description: "正在进行、需要处理、计划、最近结果与运行状态", + description: "在忙什么、要我处理什么、最近入库了什么", }, { to: "/console/tasks", label: "任务", icon: ListTodo, - description: "每次运行都是一条任务:定时、事件、手动、Agent 触发都在这里", + description: "最近处理了什么、有没有失败", }, { to: "/console/schedules", label: "计划", icon: CalendarCog, - description: "定时任务的开关、间隔和下次运行:自动任务与 Agent 巡检", + description: "智能体巡检与自动任务的开关和间隔", }, { to: "/console/agent", - label: "Agent 会话", + label: "智能体会话", icon: Bot, - description: "交代目标,Agent 在授权内执行并汇报", + description: "给智能体一个目标,看它做了什么", }, ], }, @@ -75,9 +75,9 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/sites", label: "站点", icon: Globe, - description: "站点、凭证、账号数据与健康;资源、快照与 Feed", + description: "站点账号、资源与 RSS", }, - { to: "/console/sources", label: "RSS 订阅", icon: Rss, description: "全部 Feed 与抓取状态" }, + { to: "/console/sources", label: "RSS", icon: Rss, description: "全部 RSS 与抓取状态" }, ], }, { @@ -90,7 +90,7 @@ export const NAV_GROUPS: NavGroup[] = [ { title: "整理", items: [ - { to: "/console/downloads", label: "下载面板", icon: Download, description: "下载任务、下载计划与缓存水位" }, + { to: "/console/downloads", label: "下载面板", icon: Download, description: "下载任务、下载计划与下载盘" }, { to: "/console/organize", label: "整理记录", @@ -101,13 +101,13 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/files", label: "文件管理", icon: FolderTree, - description: "浏览登记目录,改名、移动、删除与整理", + description: "浏览下载目录和媒体库,改名、移动、删除与整理", }, { to: "/console/library", - label: "媒体概览", + label: "媒体库", icon: Library, - description: "作品、季 / 集、版本目标、外部身份与待识别资源", + description: "作品、季与集、未识别资源", }, ], }, @@ -118,7 +118,7 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/settings", label: "设置", icon: Settings, - description: "下载器、媒体服务器、库根、站点账号、Agent 上游与自动化", + description: "下载器、媒体服务器、媒体库目录、站点账号、模型服务与自动化", }, { to: "/console/audit", @@ -130,7 +130,7 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/status", label: "运行状态", icon: Gauge, - description: "进程心跳、事件循环、数据库、磁盘与挂载根", + description: "服务是否正常、磁盘与数据库", }, ], }, diff --git a/apps/web/app/components/console/tech-info.tsx b/apps/web/app/components/console/tech-info.tsx new file mode 100644 index 00000000..9e380c6d --- /dev/null +++ b/apps/web/app/components/console/tech-info.tsx @@ -0,0 +1,53 @@ +import { ChevronDown } from "lucide-react"; +import { type ReactNode, useId, useState } from "react"; +import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx"; +import { type KvItem, KvList } from "~/components/console/kv-list.tsx"; +import { useUiPrefs } from "~/lib/ui-prefs.ts"; +import { cn } from "~/lib/utils.ts"; + +/** + * 技术信息折叠区:ID、hash、trace、token 数、耗时、模型 ID、原始字段、内部状态码都放这里,默认收起 + * (设置 → 界面可改为默认展开)。值为空的项不显示;全部为空时整块不渲染。 + */ +export function TechInfo({ + items, + children, + title = "技术信息", + className, + columns = 2, +}: { + items?: (KvItem | null | false | undefined)[]; + children?: ReactNode; + title?: string; + className?: string; + columns?: 1 | 2; +}) { + const prefs = useUiPrefs(); + const [override, setOverride] = useState(null); + const open = override ?? prefs.techInfoExpanded; + const id = useId(); + const rows = (items ?? []).filter( + (it): it is KvItem => !!it && it.value !== null && it.value !== undefined && it.value !== "", + ); + if (!rows.length && !children) return null; + return ( +
+ + +
+ {rows.length ? ({ mono: true, ...r }))} columns={columns} /> : null} + {children} +
+
+
+ ); +} diff --git a/apps/web/app/components/downloads/business-tasks.tsx b/apps/web/app/components/downloads/business-tasks.tsx index 7d9d8afd..cc2d2e99 100644 --- a/apps/web/app/components/downloads/business-tasks.tsx +++ b/apps/web/app/components/downloads/business-tasks.tsx @@ -30,10 +30,10 @@ export const TASK_KIND_LABEL: Record = { submitDownload: "提交下载", verifyAndImport: "核验入库", refreshMetadata: "刷新元数据", - reconcileLibrary: "资产对账", + reconcileLibrary: "文件核对", cleanupDownloadCache: "清理缓存", resolveDecision: "处理待处理事项", - agentRun: "Agent 运行", + agentRun: "智能体运行", }; export const TASK_STATUS: Record< @@ -54,15 +54,15 @@ export const ACTIVE_TASK: TaskStatus[] = ["pending", "running", "waiting"]; export const SUBJECT_LABEL: Record = { download_task: "下载任务", acquisition_plan: "下载计划", - acquisition_slot: "版本目标", + acquisition_slot: "收录版本", organization_record: "整理记录", organize_request: "整理请求", media_entity: "作品", - media_asset: "资产", + media_asset: "文件", source_release: "资源", - feed_source: "Feed", + feed_source: "RSS", site: "站点", - library_root: "库根", + library_root: "媒体库目录", cache_volume: "缓存卷", }; diff --git a/apps/web/app/components/downloads/business-timeline.tsx b/apps/web/app/components/downloads/business-timeline.tsx index 74a259cd..eacca1ca 100644 --- a/apps/web/app/components/downloads/business-timeline.tsx +++ b/apps/web/app/components/downloads/business-timeline.tsx @@ -9,11 +9,11 @@ import { Timeline } from "~/components/console/timeline.tsx"; // 阶段计数与首页/侧栏同口径;每条事件都有文字说明,不只靠颜色。 export const STAGE_LABEL: Record = { - observation: "观察", + observation: "发现", plan: "计划", download: "下载", organization: "整理", - agent: "Agent", + agent: "智能体", engine: "任务", }; @@ -26,12 +26,11 @@ export function BusinessTimeline({ events, loading }: { events: TimelineEvent[]; return (
- {ORDER.map((s) => { - const n = counts.get(s) ?? 0; + {ORDER.filter((s) => counts.get(s)).map((s) => { const bad = events.some((e) => e.stage === s && e.tone === "destructive"); return ( - - {STAGE_LABEL[s]} {n ? `· ${n}` : "· 无"} + + {STAGE_LABEL[s]} · {counts.get(s)} ); })} diff --git a/apps/web/app/components/downloads/cache-volumes.tsx b/apps/web/app/components/downloads/cache-volumes.tsx index 56afaffb..6295a7d3 100644 --- a/apps/web/app/components/downloads/cache-volumes.tsx +++ b/apps/web/app/components/downloads/cache-volumes.tsx @@ -8,6 +8,7 @@ import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { consolePath, settingsPath } from "~/components/console/nav.ts"; import { StatusBadge } from "~/components/console/status-badge.tsx"; +import { TechInfo } from "~/components/console/tech-info.tsx"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn, formatBytes } from "~/lib/utils.ts"; @@ -37,10 +38,11 @@ function VolumeBar({ v }: { v: Volume }) { {volumeLabel(v)} - {pct === null ? "使用率未知" : `${pct.toFixed(1)}%`} {v.usedBytes !== null && v.totalBytes !== null - ? ` · ${formatBytes(v.usedBytes, 1)} / ${formatBytes(v.totalBytes, 1)}` - : ""} + ? `已用 ${formatBytes(v.usedBytes, 0)} / ${formatBytes(v.totalBytes, 1)}` + : pct === null + ? "用量未知" + : `已用 ${Math.round(pct)}%`}
@@ -57,28 +59,32 @@ function VolumeBar({ v }: { v: Volume }) { aria-hidden className="absolute -top-1 h-3.5 w-px bg-destructive" style={{ left: `${v.hardThresholdPercent}%` }} - title={`硬阈值 ${v.hardThresholdPercent}%`} + title={`暂停线 ${v.hardThresholdPercent}%`} />
-
- - 硬阈值 {v.hardThresholdPercent}% / 恢复 {v.resumePercent}% - - 在途 {formatBytes(v.pendingBytes, 1)} - 预留 {formatBytes(v.reservedBytes, 1)} - 可清理 {formatBytes(v.cleanableBytes, 1)} - 保留锁定 {formatBytes(v.retentionLockedBytes, 1)} -
+
{pct === null ? 读不到磁盘状态,已暂停新提交 : null} - {tripped ? 已达硬阈值,停止新提交 : null} - {v.pausedByThreshold ? 阈值暂停 {v.pausedByThreshold} 个任务 : null} + {tripped ? 超过暂停线,停止新下载 : null} + {v.pausedByThreshold ? ( + 因下载盘暂停 {v.pausedByThreshold} 个任务 + ) : null}
{v.notes.map((n) => (

@@ -97,7 +103,7 @@ export function CacheVolumes() { return (

-

缓存水位

+

下载盘

} title="按规则清理下载缓存?" - description="只清理已入库、且满足保留期与 H&R 的纳管任务,其余不动。" + 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 9e5ac01d..7bee97e3 100644 --- a/apps/web/app/components/downloads/download-detail.tsx +++ b/apps/web/app/components/downloads/download-detail.tsx @@ -1,6 +1,7 @@ import type { DownloadTask } from "@umt/contracts"; import { Bot, FolderInput, Pause, Play, ShieldAlert } from "lucide-react"; import { useState } from "react"; +import { Link } from "react-router"; import { toast } from "sonner"; import { DOWNLOAD_STATUS_LABEL, @@ -17,11 +18,13 @@ import { DetailSection } from "~/components/console/detail-sheet.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; +import { TechInfo } from "~/components/console/tech-info.tsx"; import { OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx"; import { Button } from "~/components/ui/button.tsx"; import { relTime } from "~/components/workbench/release-fields.ts"; import { errorMessage } from "~/lib/api.ts"; import { planReasonText } from "~/lib/labels.ts"; +import { humanizeReason, reasonLine } from "~/lib/reasons.ts"; import { formatBytes, formatDateTime } from "~/lib/utils.ts"; import { BusinessTimeline } from "./business-timeline.tsx"; @@ -47,12 +50,11 @@ export function DownloadStatusBadges({ t }: { t: DownloadTask }) { {DOWNLOAD_STATUS_LABEL[t.status]} {t.pauseReason === "threshold" ? ( - 阈值暂停 + 下载盘满,已暂停 ) : t.pauseReason ? ( {PAUSE_REASON_LABEL[t.pauseReason]} ) : null} - {!t.managed ? 未纳管 : null} - {t.origin === "adopted" ? 接管 : null} + {!t.managed ? 外部任务 : null} ); } @@ -69,14 +71,14 @@ 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(); act.mutate( { id: t.id, action: canResume ? "resume" : "pause" }, { - onSuccess: () => toast.success(canResume ? "已恢复" : "已暂停。阈值回落时不会自动恢复"), + onSuccess: () => toast.success(canResume ? "已恢复" : "已暂停。下载盘回落时不会自动恢复"), onError: (err) => toast.error(errorMessage(err)), }, ); @@ -95,6 +97,11 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { const trace = useDownloadTrace(task, plan.data); const [organizing, setOrganizing] = useState(false); const canOrganize = ["completed", "importing", "imported"].includes(task.status); + // 下载完成但最近一次整理失败:状态行只写"下载完成"会误导,单独说明入库失败 + const lastRecord = [...(trace.records.data?.items ?? [])].sort((a, b) => + (b.startedAt ?? b.createdAt).localeCompare(a.startedAt ?? a.createdAt), + )[0]; + const importFailed = task.status === "completed" && lastRecord?.status === "failed" ? lastRecord : null; return (
@@ -123,7 +130,8 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { }) } > - 问 Agent + + 问智能体
@@ -131,7 +139,15 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { {task.pauseReason === "threshold" ? (

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

+ ) : null} + {importFailed ? ( +

+ 入库失败:{reasonLine(importFailed.reason)}。 + + 去待处理 +

) : null} {task.noProgressSince ? ( @@ -150,17 +166,26 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { { label: "剩余", value: task.remainingBytes === null ? "未知" : formatBytes(task.remainingBytes, 2) }, { label: "分享率", value: task.ratio === null ? "—" : task.ratio.toFixed(2) }, { label: "最近进展", value: relTime(task.lastProgressAt) }, - { label: "最近同步", value: relTime(task.lastSyncedAt) }, - { label: "标签", value: task.tags.join(", ") }, - { label: "分类", value: task.category }, { label: "保存路径", value: task.savePath, mono: true, wide: true }, - { label: "内容路径", value: task.contentPath, mono: true, wide: true }, - { label: "torrent hash", value: task.torrentHash, mono: true, wide: true }, ]} /> + + {task.lastError ? : null} + - + @@ -174,7 +199,9 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { { label: "说明", value: planReasonText(plan.data.statusReason), wide: true }, ]} /> - + + + ) : null} @@ -194,7 +221,12 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { ) : null} - {task.lastError ? : null} + {task.lastError ? ( +

+ 下载器报错: + {humanizeReason(String((task.lastError as { message?: unknown }).message ?? "")) || "原文见技术信息"} +

+ ) : null} {organizing ? ( = { identity_uncertain: "识别不可信,没有推荐名", }; -const TEMPLATE_SOURCE: Record = { entity: "作品覆盖", root: "库根", global: "全局", override: "覆写" }; +const TEMPLATE_SOURCE: Record = { + entity: "作品覆盖", + root: "媒体库目录", + global: "全局", + override: "覆写", +}; function suggestReasonText(r: string | null): string { if (!r) return "没有推荐名"; @@ -434,7 +439,7 @@ export function RenameDialog({ ))} {pv.assetLocations > 0 ? ( - 同步 {pv.assetLocations} 条资产位置,写一条整理记录。 + 同步 {pv.assetLocations} 条文件位置,写一条整理记录。 ) : null} {pv.bindings.length > 0 ? ( 这是作品或季的绑定目录。改名后旧绑定标为已替代,之后入库用新目录。 @@ -582,7 +587,7 @@ export function TransferDialog({ }} className="max-h-72 md:max-h-96" /> -

只能选可写库根下的目录。

+

只能选可写媒体库目录下的目录。

{target && targetRoot ? ( @@ -616,7 +621,7 @@ export function TransferDialog({ 冲突 {conflicts.length}(跳过,不覆盖) ) : null} {plan.pairs.some((p) => p.registered) ? ( - 含已登记资产,执行后同步位置 + 含已入库文件,执行后同步位置 ) : null}
{hnrBlocked ? ( @@ -777,7 +782,7 @@ export function DeleteDialog({ {single ? `删除 ${single}?` : `删除 ${items.length} 项?`} - 已检查做种任务、已登记资产和路径绑定。下载器里的任务不受影响。 + 已检查做种任务、已入库文件和路径绑定。下载器里的任务不受影响。
{check.isPending ? ( @@ -824,7 +829,7 @@ export function DeleteDialog({

))} {c.assets.length ? ( -

已登记资产 {c.assets.length} 个(删除后退役,已满足的版本目标退回待处理)

+

已入库文件 {c.assets.length} 个(删除后从媒体库移除,已满足的收录版本退回待处理)

) : null} {c.bindings.map((bd) => (

@@ -843,9 +848,9 @@ export function DeleteDialog({ @@ -1042,9 +1047,9 @@ export function RefreshMetadataDialog({ items, onClose }: { items: FileEntry[];

{s.ref}: {s.reason === "not_registered" - ? "没有已登记资产" + ? "没有已入库文件" : s.reason === "no_entity" - ? "不属于任何已登记作品" + ? "不属于媒体库里的作品" : s.reason === "not_found" ? "作品不存在" : s.reason} diff --git a/apps/web/app/components/files/dir-picker.tsx b/apps/web/app/components/files/dir-picker.tsx index 117ac992..2d18c0a3 100644 --- a/apps/web/app/components/files/dir-picker.tsx +++ b/apps/web/app/components/files/dir-picker.tsx @@ -29,7 +29,7 @@ export function DirPicker({ onSelect={onChange} /> ) : ( -

没有可写的库根。在设置里把库根设为纳管后再选。

+

没有可写入的媒体库目录。先在设置里添加。

)}
); diff --git a/apps/web/app/components/files/dir-tree.tsx b/apps/web/app/components/files/dir-tree.tsx index 747ce437..886afdc4 100644 --- a/apps/web/app/components/files/dir-tree.tsx +++ b/apps/web/app/components/files/dir-tree.tsx @@ -2,7 +2,7 @@ import { ChevronRight, Download, Folder, FolderOpen, HardDrive, LoaderCircle, Tr import { useEffect, useState } from "react"; import { type FileRoot, useDirChildren } from "~/api/files.ts"; import { healthLabel } from "~/lib/labels.ts"; -import { cn, formatBytes } from "~/lib/utils.ts"; +import { cn } from "~/lib/utils.ts"; import { groupRoots, joinRel, segments } from "./model.ts"; type Loc = { root: string; path: string }; @@ -140,19 +140,13 @@ function RootNode({ {h.tone !== "neutral" ? : null} {h.label} - ) : ( - - - 正常 - - )} + ) : null} {root.pendingCount ? ( - 待识别 {root.pendingCount} + 未识别 {root.pendingCount} {root.pendingPartial ? "+" : ""} ) : null} - {root.usage ? 剩余 {formatBytes(root.usage.freeBytes, 1)} : null} {!root.writable && root.kind === "library" ? 只读 : null} diff --git a/apps/web/app/components/files/file-detail.tsx b/apps/web/app/components/files/file-detail.tsx index 0dab7257..18e6a8de 100644 --- a/apps/web/app/components/files/file-detail.tsx +++ b/apps/web/app/components/files/file-detail.tsx @@ -78,7 +78,7 @@ function RecordsSection({ entityId }: { entityId: string }) { ); } -/** 选中单项的详情抽屉:文件信息、作品/版本目标/下载任务、识别、整理记录、附属文件、媒体信息、路径绑定与小文件预览 */ +/** 选中单项的详情抽屉:文件信息、作品/收录版本/下载任务、识别、整理记录、附属文件、媒体信息、路径绑定与小文件预览 */ export function FileDetail({ root, entry, @@ -123,7 +123,7 @@ export function FileDetail({ value: e.type === "dir" ? e.dirSummary - ? `${e.dirSummary.videoCount}${e.dirSummary.partial ? "+" : ""} 个视频 · ${e.dirSummary.registeredCount} 个已登记` + ? `${e.dirSummary.videoCount}${e.dirSummary.partial ? "+" : ""} 个视频 · ${e.dirSummary.registeredCount} 个已入库` : "—" : formatBytes(e.size, 2), }, @@ -148,7 +148,7 @@ export function FileDetail({ {e.asset ? ( - + {e.asset.slotStatus ? (SLOT_STATUS[e.asset.slotStatus]?.label ?? e.asset.slotStatus) : "查看"} ) : ( - "未满足任何版本目标" + "未满足任何收录版本" ), }, - { label: "主资产", value: e.asset.isPrimary ? "是" : "否" }, + { label: "主文件", value: e.asset.isPrimary ? "是" : "否" }, ]} /> diff --git a/apps/web/app/components/files/mock.ts b/apps/web/app/components/files/mock.ts index 99d0fbc2..f941cdf4 100644 --- a/apps/web/app/components/files/mock.ts +++ b/apps/web/app/components/files/mock.ts @@ -707,7 +707,7 @@ function doMove(body: { const tRoot = rootOf(body.target.root); const target = find(body.target.root, body.target.path).node; if (!target.dir || !tRoot.writable) - throw new ApiRequestError("bad_request", "not_writable:目标必须是可写库根下的目录"); + throw new ApiRequestError("bad_request", "not_writable:目标必须是可写媒体库目录下的目录"); const pairs = body.items.map((it) => { const { node } = find(it.root, it.path); const name = node.name; diff --git a/apps/web/app/components/files/model.test.ts b/apps/web/app/components/files/model.test.ts index 751f710d..9db096db 100644 --- a/apps/web/app/components/files/model.test.ts +++ b/apps/web/app/components/files/model.test.ts @@ -89,7 +89,7 @@ const base = { describe("状态与禁用原因", () => { it("状态徽标带文字,H&R 未满足为 warning,逃逸为 destructive", () => { - expect(entryStatusView({ ...base, status: "registered" })?.label).toBe("已登记"); + expect(entryStatusView({ ...base, status: "registered" })?.label).toBe("已入库"); expect( entryStatusView({ ...base, @@ -228,7 +228,7 @@ describe("v2 菜单与副行", () => { type: "dir" as const, dirSummary: { videoCount: 13, registeredCount: 0, partial: false }, })?.text, - ).toBe("13 视频 · 0 已登记"); + ).toBe("13 个视频 · 未入库"); // 没有 parsedHint(服务端解析不出标题)时不显示副行,前端不自己解析 expect(entrySubline({ ...video, parsedHint: null })).toBeNull(); }); diff --git a/apps/web/app/components/files/model.ts b/apps/web/app/components/files/model.ts index 401e1ba0..ea0457c2 100644 --- a/apps/web/app/components/files/model.ts +++ b/apps/web/app/components/files/model.ts @@ -19,20 +19,20 @@ export function entryStatusView(e: Pick 合并作品 - 「{entity.title}」会标为已合并,旧 ID 跳转到目标作品,订阅、资产和历史记录一并转过去。合并错了可以再修订。 + 「{entity.title}」会标为已合并,旧 ID 跳转到目标作品,订阅、文件和历史记录一并转过去。合并错了可以再修订。 @@ -392,7 +392,7 @@ export function MergeDialog({ id="merge-reason" value={reason} onChange={(e) => setReason(e.target.value)} - placeholder="重复建档" + placeholder="重复添加" /> diff --git a/apps/web/app/components/library/identity-enrich.tsx b/apps/web/app/components/library/identity-enrich.tsx index 23b7b4f7..c79f0129 100644 --- a/apps/web/app/components/library/identity-enrich.tsx +++ b/apps/web/app/components/library/identity-enrich.tsx @@ -68,7 +68,7 @@ function FieldRow({ k, s, enrichmentId }: { k: EnrichFieldName; s: Suggestion; e {k !== "posterUrl" ?

{String(v.value)}

: null}
{EVIDENCE_LEVEL[v.evidenceLevel] ?? v.evidenceLevel} - {v.verbatim ? null : Agent 归纳} + {v.verbatim ? null : 智能体归纳}
@@ -142,7 +142,7 @@ export function EnrichSection({ item }: { item: IdentityPoolItem }) { {out.notice} {" · "} {out.cached ? "7 天内的缓存结果" : `搜索 ${out.usage.searchCalls} 次,读取网页 ${out.usage.fetchCalls} 次`} - {s ? ` · ${s.extractor === "model" ? `Agent 抽取${s.model ? `(${s.model})` : ""}` : "规则抽取"}` : ""} + {s ? ` · ${s.extractor === "model" ? `智能体抽取${s.model ? `(${s.model})` : ""}` : "规则抽取"}` : ""} {` · 今日 ${out.budget.usedToday}/${out.budget.dailyLimit}`}

{out.error ?

{out.error}

: null} diff --git a/apps/web/app/components/library/identity-external-sheet.tsx b/apps/web/app/components/library/identity-external-sheet.tsx index 3e91e408..b6d6fc8e 100644 --- a/apps/web/app/components/library/identity-external-sheet.tsx +++ b/apps/web/app/components/library/identity-external-sheet.tsx @@ -23,7 +23,7 @@ const ACTION: Record = { locked: "加人工锁", unlocked: "解除人工锁", }; -const ACTOR: Record = { user: "用户", agent: "Agent", system: "系统", api_key: "API Key" }; +const ACTOR: Record = { user: "用户", agent: "智能体", system: "系统", api_key: "API Key" }; export const identityRef = (i: Pick) => `${i.provider}:${i.entityType}:${i.externalId}`; diff --git a/apps/web/app/components/library/identity-pool-sheet.tsx b/apps/web/app/components/library/identity-pool-sheet.tsx index b5a823fd..ec96b646 100644 --- a/apps/web/app/components/library/identity-pool-sheet.tsx +++ b/apps/web/app/components/library/identity-pool-sheet.tsx @@ -76,7 +76,7 @@ function CandidateRow({ c, index }: { c: PoolCandidate; index: number }) { ) : ( - 本地未建档 + 不在媒体库 )} {c.evidence.length ? ( diff --git a/apps/web/app/components/library/identity-views.tsx b/apps/web/app/components/library/identity-views.tsx index c0afcc9d..b80e042b 100644 --- a/apps/web/app/components/library/identity-views.tsx +++ b/apps/web/app/components/library/identity-views.tsx @@ -148,7 +148,7 @@ export function PoolTab() { selectedKey={openId} empty={{ icon: SearchX, - title: filtered ? "没有符合条件的待识别资源" : "没有待识别资源", + title: filtered ? "没有符合条件的未识别资源" : "没有未识别资源", description: "未命中订阅、证据不足或冲突的资源会出现在这里;新增相近别名时自动重试,也可手动指定。", }} hasMore={pool.hasNextPage} @@ -249,7 +249,7 @@ export function ExternalTab() { empty={{ icon: Fingerprint, title: "没有外部身份", - description: "没有外部身份的作品也能建档入库。", + description: "没有外部编号的作品也能入库。", }} hasMore={list.hasNextPage} onLoadMore={() => list.fetchNextPage()} diff --git a/apps/web/app/components/library/labels.ts b/apps/web/app/components/library/labels.ts index 1332f271..737cb0ee 100644 --- a/apps/web/app/components/library/labels.ts +++ b/apps/web/app/components/library/labels.ts @@ -34,7 +34,7 @@ export const MEDIA_KIND: Record = { }; export const ENTITY_STATUS: Record = { - provisional: { label: "临时建档", tone: "warning" }, + provisional: { label: "临时作品", tone: "warning" }, active: { label: "正常", tone: "success" }, merged: { label: "已合并", tone: "neutral" }, archived: { label: "已归档", tone: "neutral" }, @@ -132,11 +132,11 @@ export const CANDIDATE_DECISION: Record = { }; export const DECISION_KIND: Record = { - identity_ambiguous: "身份歧义", - identity_unresolved: "待识别", + identity_ambiguous: "没识别出作品", + identity_unresolved: "未识别", download_stalled: "下载无进展", download_failed: "下载失败", - path_conflict: "路径冲突", + path_conflict: "整理冲突", space_insufficient: "空间不足", approval_required: "需要审批", metadata_conflict: "元数据冲突", @@ -146,7 +146,7 @@ export const DECISION_KIND: Record = { export const DECISION_STATUS: Record = { open: { label: "待处理", tone: "warning" }, - investigating: { label: "Agent 调查中", tone: "neutral" }, + investigating: { label: "智能体调查中", tone: "neutral" }, awaiting_user: { label: "等你决定", tone: "warning" }, resolved: { label: "已处理", tone: "success" }, dismissed: { label: "已忽略", tone: "neutral" }, @@ -168,7 +168,7 @@ export const POOL_REASON: Record = { }; export const POOL_STATUS: Record = { - pending: { label: "待识别", tone: "warning" }, + pending: { label: "未识别", tone: "warning" }, resolved: { label: "已识别", tone: "success" }, dismissed: { label: "已忽略", tone: "neutral" }, archived: { label: "已归档", tone: "neutral" }, @@ -178,13 +178,13 @@ export const PREF_KIND: Record soft: { label: "软偏好", hint: "影响排序,不排除" }, hard: { label: "硬规则", hint: "命中即排除" }, version: { label: "版本偏好", hint: "同一作品内选哪种版本" }, - target: { label: "版本目标", hint: "为作品新建一个版本目标" }, + target: { label: "收录版本", hint: "为作品新建一个收录版本" }, }; export const PREF_ORIGIN: Record = { manual: "手动", conversation: "对话确认", - agent_inferred: "Agent 推断", + agent_inferred: "智能体推断", }; export const PREF_STATUS: Record = { @@ -219,12 +219,12 @@ export const SUB_STATUS: Record = { completed: { label: "已完成", tone: "neutral" }, cancelled: { label: "已取消", tone: "neutral" }, }; -export const SUB_ORIGIN: Record = { user: "用户", agent: "Agent", policy: "策略", import: "导入" }; +export const SUB_ORIGIN: Record = { user: "用户", agent: "智能体", policy: "策略", import: "导入" }; export const METADATA_SOURCE: Record = { tmdb: "TMDB", user: "人工", - agent: "Agent", + agent: "智能体", nfo: "NFO", site: "站点", import: "导入", @@ -316,7 +316,7 @@ export const opts = (rec: Record`)→ 中文;不认识的原样显示 */ +/** 收录版本 / 候选的原因串(如 `blocked:no_main_video`、`deferred:slot_busy;inflight:`)→ 中文;不认识的原样显示 */ const SLOT_REASON: Record = { "blocked:no_main_video": "没有可入库的主视频", "deferred:slot_busy": "已有计划在途,等它结束再评估", diff --git a/apps/web/app/components/library/preferences-form.tsx b/apps/web/app/components/library/preferences-form.tsx index 838041ce..4e64c881 100644 --- a/apps/web/app/components/library/preferences-form.tsx +++ b/apps/web/app/components/library/preferences-form.tsx @@ -108,7 +108,7 @@ export function PreferenceFormDialog({ {tag ? "编辑偏好" : "添加偏好"} {tag - ? "修改作用范围或规则会留下旧语义的抑制记录,Agent 不会按旧证据加回。" + ? "修改作用范围或规则会留下旧语义的抑制记录,智能体不会按旧证据加回。" : "手动添加等同对话确认。之前取消过的偏好,重新添加会解除抑制。"} diff --git a/apps/web/app/components/library/slot-card.tsx b/apps/web/app/components/library/slot-card.tsx index 64551b59..68e2495a 100644 --- a/apps/web/app/components/library/slot-card.tsx +++ b/apps/web/app/components/library/slot-card.tsx @@ -39,7 +39,7 @@ export function SlotCard({ {slot.requireDistinctAsset ? ( - 需不同物理资产{slot.distinctGroup ? ` · ${slot.distinctGroup}` : ""} + 需独立文件{slot.distinctGroup ? ` · ${slot.distinctGroup}` : ""} ) : null} r{slot.revision} @@ -72,7 +72,7 @@ export function SlotCard({
{summarizeCompletion(slot.completion)}
-
当前资产
+
当前文件
{currentAsset ?? "无"}
diff --git a/apps/web/app/components/library/subscriptions-policy.tsx b/apps/web/app/components/library/subscriptions-policy.tsx index 3bd75f2d..e108c9d2 100644 --- a/apps/web/app/components/library/subscriptions-policy.tsx +++ b/apps/web/app/components/library/subscriptions-policy.tsx @@ -353,8 +353,8 @@ function RulesSummary({ rules, valid }: { rules: AcquisitionPolicyRules | undefi return (
- 默认版本目标 - + 默认收录版本 +
新作品收录 @@ -718,7 +718,7 @@ function PolicyEditorSheet({ } > -

订阅的作品没有版本目标时,按这些模板创建。

+

订阅的作品没有收录版本时,按这些模板创建。

set("defaultSlots", v)} errors={errors} />
@@ -768,7 +768,7 @@ function PolicyEditorSheet({
-

新作品的版本目标模板

+

新作品的收录版本模板

{q.isPending ? ( @@ -65,7 +65,7 @@ export function SlotsPanel({ @@ -140,7 +140,7 @@ function SlotEditorSheet({ const errs: Record = {}; if (!meta.success) for (const i of meta.error.issues) errs[String(i.path[0])] ??= i.message; if (!rules.ok) Object.assign(errs, rules.errors); - if (requireDistinct && !distinctGroup.trim()) errs.distinctGroup = "要求不同物理资产时须填写分组"; + if (requireDistinct && !distinctGroup.trim()) errs.distinctGroup = "要求独立文件时须填写分组"; setErrors(errs); if (Object.keys(errs).length || !meta.success || !rules.ok) return; const common = { @@ -178,7 +178,7 @@ function SlotEditorSheet({ open onOpenChange={(o) => !o && !pending && onClose()} width="lg" - title={slot ? `编辑版本目标 ${slot.name}` : "新建版本目标"} + title={slot ? `编辑收录版本 ${slot.name}` : "新建收录版本"} description={ slot ? `当前 r${slot.revision}。保存时若已被修改,会提示刷新。` : "不满足硬条件的资源不收;软偏好决定先收哪个。" } @@ -225,10 +225,10 @@ function SlotEditorSheet({
-

要求不同物理资产

+

要求独立文件

同一个文件不能同时满足两个目标

- +
diff --git a/apps/web/app/components/library/work-assign-dialog.tsx b/apps/web/app/components/library/work-assign-dialog.tsx new file mode 100644 index 00000000..9a408229 --- /dev/null +++ b/apps/web/app/components/library/work-assign-dialog.tsx @@ -0,0 +1,157 @@ +import type { SearchWork } from "@umt/contracts"; +import { Check, Search } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useSearch } from "~/api/search.ts"; +import { LoadingState } from "~/components/console/empty-state.tsx"; +import { Button } from "~/components/ui/button.tsx"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog.tsx"; +import { Input } from "~/components/ui/input.tsx"; +import { cn } from "~/lib/utils.ts"; +import { MEDIA_KIND } from "./labels.ts"; + +/** 选中的作品:本地作品给 mediaEntityId,只在 TMDB 上的给 tmdb */ +export type AssignedWork = + | { mediaEntityId: string; title: string } + | { tmdb: { type: "movie" | "tv"; id: string }; title: string }; + +const workTitle = (w: SearchWork) => `${w.title}${w.year ? ` (${w.year})` : ""}`; + +/** + * 「指定作品」:搜本地媒体库与 TMDB(复用全局搜索 /search?scope=media),选一个交给 onAssign。 + * onAssign 返回 Promise:执行中按钮禁用,resolve 后关闭,reject 时保持打开(错误由调用方提示)。 + */ +export function WorkAssignDialog({ + open, + onOpenChange, + initialQuery = "", + subject, + onAssign, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + initialQuery?: string; + /** 在说明里点名要指定的是什么(文件名 / 资源标题) */ + subject?: string | null; + onAssign: (w: AssignedWork) => Promise; +}) { + const [q, setQ] = useState(initialQuery); + const [debounced, setDebounced] = useState(initialQuery); + const [picked, setPicked] = useState(null); + const [pending, setPending] = useState(false); + useEffect(() => { + const t = setTimeout(() => setDebounced(q), 300); + return () => clearTimeout(t); + }, [q]); + useEffect(() => { + if (open) { + setQ(initialQuery); + setDebounced(initialQuery); + setPicked(null); + } + }, [open, initialQuery]); + const search = useSearch(debounced, "media", 12, open); + const works = search.data?.media?.works ?? []; + const tmdbOff = search.data?.media && !search.data.media.tmdb.configured; + + const confirm = async () => { + if (!picked) return; + const target: AssignedWork | null = picked.localEntityId + ? { mediaEntityId: picked.localEntityId, title: workTitle(picked) } + : picked.tmdb + ? { tmdb: { type: picked.tmdb.entityType, id: picked.tmdb.externalId }, title: workTitle(picked) } + : null; + if (!target) return; + setPending(true); + try { + await onAssign(target); + onOpenChange(false); + } catch { + // 调用方已提示,保持打开 + } finally { + setPending(false); + } + }; + + return ( + (pending ? undefined : onOpenChange(o))}> + + + 指定作品 + + {subject ? `「${subject}」属于哪部作品?` : "选这个资源属于哪部作品。"} + 选中后加人工锁,自动识别不再改它。 + + +
+
+ + { + setQ(e.target.value); + setPicked(null); + }} + placeholder="作品名,中英文都行" + className="pl-9" + aria-label="搜索作品" + /> +
+ {debounced.trim() ? ( +
+ {search.isPending ? ( + + ) : search.isError ? ( +

{(search.error as Error).message}

+ ) : works.length === 0 ? ( +

+ 没有找到{tmdbOff ? "本地作品;TMDB 未配置,只搜了媒体库" : "匹配的作品"} +

+ ) : ( +
    + {works.map((w) => { + const active = picked?.key === w.key; + return ( +
  • + +
  • + ); + })} +
+ )} +
+ ) : null} +
+ + + + +
+
+ ); +} diff --git a/apps/web/app/components/naming/model.ts b/apps/web/app/components/naming/model.ts index 52b8b3fc..e61df131 100644 --- a/apps/web/app/components/naming/model.ts +++ b/apps/web/app/components/naming/model.ts @@ -3,7 +3,7 @@ import type { NamingKind, NamingOverride } from "@umt/contracts"; // 命名模板表单的纯函数:保存错误拆分、覆盖请求体、来源文案。 export const NAMING_KINDS: NamingKind[] = ["movie", "tv", "variety"]; -export const SOURCE_LABEL = { global: "全局", root: "库根", entity: "作品" } as const; +export const SOURCE_LABEL = { global: "全局", root: "媒体库目录", entity: "作品" } as const; /** 422 消息 "movie: 1:3 …; tv: …" → 按类型拆开,行列号改写成中文 */ export function splitNamingErrors(message: string): Partial> | null { diff --git a/apps/web/app/components/naming/template-editor.tsx b/apps/web/app/components/naming/template-editor.tsx index 2e007f85..60eae686 100644 --- a/apps/web/app/components/naming/template-editor.tsx +++ b/apps/web/app/components/naming/template-editor.tsx @@ -444,7 +444,6 @@ function VariablePalette({ > {v.name} - {v.ours ? MP 无 : null} {v.example ? ( {v.example} diff --git a/apps/web/app/components/organize-wizard/model.ts b/apps/web/app/components/organize-wizard/model.ts index 38f60405..f086f563 100644 --- a/apps/web/app/components/organize-wizard/model.ts +++ b/apps/web/app/components/organize-wizard/model.ts @@ -358,7 +358,7 @@ export const FILE_KIND: Record = { export const TEMPLATE_SOURCE: Record = { entity: "作品覆盖", - root: "库根", + root: "媒体库目录", global: "全局", override: "保留原文件名", }; diff --git a/apps/web/app/components/organize-wizard/step-files.tsx b/apps/web/app/components/organize-wizard/step-files.tsx index 68d0f32b..1fe4e7ba 100644 --- a/apps/web/app/components/organize-wizard/step-files.tsx +++ b/apps/web/app/components/organize-wizard/step-files.tsx @@ -194,11 +194,11 @@ function SlotSelect({ const items = slots.data?.items ?? []; return ( - + @@ -177,7 +177,7 @@ function RootNamingEditor({ root }: { root: LibraryRoot }) { try { await save.mutateAsync({ id: root.id, naming: body }); setDraft({ ...(body ?? {}) }); - toast.success(body ? "库根命名覆盖已保存" : "已清除库根命名覆盖"); + toast.success(body ? "媒体库目录命名覆盖已保存" : "已清除媒体库目录命名覆盖"); } catch (err) { const split = splitNamingErrors(errorMessage(err)); if (split) setErrors(split); @@ -190,7 +190,7 @@ function RootNamingEditor({ root }: { root: LibraryRoot }) {
{KINDS.map((k) => ( - {NAMING_KIND_LABEL[k]}:{saved[k] ? "库根覆盖" : "沿用全局"} + {NAMING_KIND_LABEL[k]}:{saved[k] ? "媒体库目录覆盖" : "沿用全局"} ))}
diff --git a/apps/web/app/components/settings/path-mappings.tsx b/apps/web/app/components/settings/path-mappings.tsx index 9462a089..8a116ddc 100644 --- a/apps/web/app/components/settings/path-mappings.tsx +++ b/apps/web/app/components/settings/path-mappings.tsx @@ -104,7 +104,7 @@ export function PathMappingsPanel({

{m.remotePrefix}

→ {m.localPrefix}

{rootName(m.libraryRootId) ? ( -

库根:{rootName(m.libraryRootId)}

+

媒体库目录:{rootName(m.libraryRootId)}

) : null} {h.label} @@ -175,7 +175,7 @@ export function PathMappingsPanel({ disabled={create.isPending} /> setV((s) => ({ ...s, libraryRootId: x }))} disabled={create.isPending} diff --git a/apps/web/app/components/settings/pi-test.tsx b/apps/web/app/components/settings/pi-test.tsx index 594e454f..fcdf8a21 100644 --- a/apps/web/app/components/settings/pi-test.tsx +++ b/apps/web/app/components/settings/pi-test.tsx @@ -22,7 +22,7 @@ const SOURCE_TEXT: Record = { "stored credential": "已存凭证 /** 上游返回的多是英文原文:先给一句怎么办,原文留作排查 */ const ERROR_HINT: Record = { - network: "连不上上游,检查 Base URL、端口和网络", + network: "连不上模型服务,检查 Base URL、端口和网络", auth: "凭证被拒,检查 API Key 或重新订阅登录", model: "模型不可用,换一个模型 id", }; diff --git a/apps/web/app/components/settings/pi-upstreams.tsx b/apps/web/app/components/settings/pi-upstreams.tsx index 038c16cc..37f49c42 100644 --- a/apps/web/app/components/settings/pi-upstreams.tsx +++ b/apps/web/app/components/settings/pi-upstreams.tsx @@ -49,13 +49,13 @@ export function PiUpstreamsBlock() { setWizard(true)}> - 新建上游 + 新建模型服务 } > @@ -66,7 +66,7 @@ export function PiUpstreamsBlock() { error={list.error} onRetry={() => list.refetch()} onRowClick={(r) => setOpenId(r.id)} - empty={{ title: "还没有上游" }} + empty={{ title: "还没有模型服务" }} columns={[ { key: "name", @@ -181,7 +181,7 @@ function UpstreamDrawer({ const makeDefault = async () => { const model = v.model.trim(); if (!model) { - setErrors((e) => ({ ...e, defaultModel: "设为默认上游前先选默认模型" })); + setErrors((e) => ({ ...e, defaultModel: "设为默认模型服务前先选默认模型" })); return; } try { @@ -189,7 +189,7 @@ function UpstreamDrawer({ id: upstream.id, body: { makeDefault: true, ...(model !== (upstream.defaultModel ?? "") ? { defaultModel: model } : {}) }, }); - toast.success(`${upstream.name}已设为默认上游`); + toast.success(`${upstream.name}已设为默认模型服务`); } catch (err) { toast.error(errorMessage(err, "设置失败")); } @@ -204,7 +204,7 @@ function UpstreamDrawer({ description={ - {upstream.isDefault ? 默认上游 : null} + {upstream.isDefault ? 默认模型服务 : null} {provider?.name ?? upstream.provider} · {AUTH_LABEL[upstream.authKind]} @@ -402,11 +402,13 @@ function UpstreamDrawer({ { try { diff --git a/apps/web/app/components/settings/pi-wizard.tsx b/apps/web/app/components/settings/pi-wizard.tsx index 66a73e1a..8bf67c67 100644 --- a/apps/web/app/components/settings/pi-wizard.tsx +++ b/apps/web/app/components/settings/pi-wizard.tsx @@ -60,9 +60,9 @@ export function PiUpstreamWizard({ open, onClose }: { open: boolean; onClose: () const title = step === "provider" - ? "新建 Agent 上游" + ? "新建模型服务" : step === "config" - ? `新建上游 · ${provider?.name ?? ""}` + ? `新建模型服务 · ${provider?.name ?? ""}` : (created?.name ?? ""); const description = step === "provider" @@ -70,8 +70,8 @@ export function PiUpstreamWizard({ open, onClose }: { open: boolean; onClose: () : step === "config" ? "第 2 步,共 3 步:凭证与模型" : created?.authKind === "oauth" - ? "第 3 步,共 3 步:登录并测试。上游已创建。" - : "第 3 步,共 3 步:测试。上游已创建。"; + ? "第 3 步,共 3 步:登录并测试。模型服务已创建。" + : "第 3 步,共 3 步:测试。模型服务已创建。"; return ( !o && close()} title={title} description={description} width="lg"> @@ -413,7 +413,7 @@ function VerifyStep({ const finish = async () => { if (makeDefault && !model.trim()) { - setModelError("设为默认上游前先选默认模型"); + setModelError("设为默认模型服务前先选默认模型"); return; } const body = { @@ -422,7 +422,7 @@ function VerifyStep({ }; try { if (Object.keys(body).length) await update.mutateAsync({ id: upstream.id, body }); - if (makeDefault && !upstream.isDefault) toast.success(`${upstream.name}已设为默认上游`); + if (makeDefault && !upstream.isDefault) toast.success(`${upstream.name}已设为默认模型服务`); onDone(); } catch (err) { toast.error(errorMessage(err, "保存失败")); @@ -476,7 +476,7 @@ function VerifyStep({ onCheckedChange={setMakeDefault} disabled={upstream.isDefault} /> - {upstream.isDefault ? "已是默认上游" : "设为默认上游"} + {upstream.isDefault ? "已是默认模型服务" : "设为默认模型服务"}
@@ -335,7 +335,7 @@ export function SiteAddDialog({ onClose }: { onClose: () => void }) { ) : (

- 不需要登录凭证。添加后在站点详情的"快照与 Feed"里新建 Feed。 + 不需要登录凭证。添加后在站点详情的"抓取与 RSS"里新建 RSS。

)} diff --git a/apps/web/app/components/sites/site-card.tsx b/apps/web/app/components/sites/site-card.tsx index 5e04be17..2db342b7 100644 --- a/apps/web/app/components/sites/site-card.tsx +++ b/apps/web/app/components/sites/site-card.tsx @@ -27,7 +27,6 @@ import { cn } from "~/lib/utils.ts"; import type { SiteDialog, useSiteChecks } from "./site-dialogs.tsx"; import { ACCOUNT_OVERALL, - AUTH_KIND_LABEL, bytes, dataAccount, healthLine, @@ -36,7 +35,6 @@ import { loginFailureText, overallStatus, primaryAccount, - rateLimitText, ratioText, } from "./site-fields.ts"; @@ -141,11 +139,11 @@ export function SiteMoreMenu({ site, onDialog }: { site: SiteListItem; onDialog: {item(, "编辑站点", () => onDialog({ kind: "edit", site }))} {item( , - "抓取快照", + "抓取最新资源", () => snap.mutate( { id: site.id }, - { onSuccess: () => toast.success("快照已排队"), onError: (e) => toast.error(errorMessage(e)) }, + { onSuccess: () => toast.success("已开始抓取"), onError: (e) => toast.error(errorMessage(e)) }, ), { disabled: !site.enabled }, )} @@ -197,21 +195,8 @@ function CheckButtons({ site, checks, compact }: Omit & { com function AccountBadges({ site }: { site: SiteListItem }) { const st = ACCOUNT_OVERALL[overallStatus(site)]; - const acc = primaryAccount(site.accounts); - const hasPasskey = site.accounts?.some((a) => a.hasPasskey || a.authKind === "passkey"); - return ( - <> - 账号{st.label} - {acc && acc.authKind !== "passkey" ? ( - - {AUTH_KIND_LABEL[acc.authKind]} - - ) : null} - {hasPasskey ? ( - Passkey - ) : null} - - ); + // 正常不标;登录方式不做徽章(DEAI-2) + return st.tone === "success" ? null : 账号{st.label}; } function Stat({ label, children, className }: { label: string; children: ReactNode; className?: string }) { @@ -307,26 +292,22 @@ export function SiteCard({ site, checks, onDialog }: Props) {
{relTime(acc?.lastLoginAt)}
-
健康检测
+
状态
{health.text}
-
快照
+
资源更新
{!site.snapshotEnabled ? "未开启" : site.lastSnapshotAt ? `${relTime(site.lastSnapshotAt)}${stale ? " · 已过期" : ""}` - : "还没有快照"} + : "还没抓取过"}
-
-
限流
-
{rateLimitText(site.rateLimit)}
-