From 4480fc346d5924aa84e4346e982345439a05f4d4 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Sep 2026 20:35:43 +0800 Subject: [PATCH 01/10] =?UTF-8?q?refactor(copy):=20=E6=9C=AF=E8=AF=AD?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E2=80=94=E2=80=94=E6=94=B6=E5=BD=95=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E3=80=81=E6=A8=A1=E5=9E=8B=E6=9C=8D=E5=8A=A1=E3=80=81?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E4=BD=93=E7=AD=89(DEAI-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 版本目标 → 收录版本(前后端用户可见字符串、能力描述、给模型的提示) - Agent 上游 / 上游 → 模型服务;界面上的 Agent / Agent 会话 → 智能体 / 智能体会话 - 身份歧义 / 识别有歧义 / 无法确定作品 → 没识别出作品;待识别(池) → 未识别 - 建档 → 加入媒体库;资产 → 文件;库根 → 媒体库目录;纳管 → 本工具 / 外部任务; 哨兵 → 标记文件 / 检查目录;站点快照 → 抓取;Feed / RSS 订阅 / 来源健康 → RSS - 代码标识符、API 字段名、迁移文件不改;glossary.md 同步;smoke 选择器跟随 Co-Authored-By: Claude Opus 5.5 --- .../__tests__/organize-guard.test.ts | 2 +- .../acquisition-organize/capabilities.ts | 12 ++--- .../src/modules/acquisition-organize/index.ts | 4 +- .../acquisition-organize/routes/assets.ts | 6 +-- .../acquisition-organize/routes/dashboard.ts | 2 +- .../acquisition-organize/routes/downloads.ts | 2 +- .../acquisition-organize/routes/naming.ts | 6 +-- .../routes/rule-groups.ts | 10 ++-- .../acquisition-organize/routes/rules.ts | 6 +-- .../routes/subscriptions.ts | 12 ++--- .../services/acquisition.ts | 8 +-- .../acquisition-organize/services/coverage.ts | 14 ++--- .../acquisition-organize/services/naming.ts | 2 +- .../services/organize-guard.ts | 2 +- .../acquisition-organize/services/organize.ts | 8 +-- .../acquisition-organize/services/rules.ts | 10 ++-- .../services/subscription-calendar.ts | 2 +- .../services/subscription-reset.ts | 2 +- .../services/subscriptions-params.ts | 16 +++--- .../services/subscriptions.ts | 2 +- apps/server/src/modules/agent/authorize.ts | 4 +- .../modules/agent/capabilities-download.ts | 2 +- .../modules/agent/capabilities-metadata.ts | 5 +- apps/server/src/modules/agent/challenge.ts | 4 +- apps/server/src/modules/agent/fake-scripts.ts | 2 +- apps/server/src/modules/agent/interaction.ts | 8 +-- apps/server/src/modules/agent/routes.ts | 10 ++-- apps/server/src/modules/agent/runner.ts | 8 +-- apps/server/src/modules/agent/service.ts | 6 +-- apps/server/src/modules/agent/subagents.ts | 6 +-- .../modules/auth-settings/instances-routes.ts | 8 +-- .../browser/__tests__/browser-api.test.ts | 2 +- apps/server/src/modules/browser/service.ts | 4 +- .../src/modules/file-manager/organize.ts | 6 +-- apps/server/src/modules/file-manager/probe.ts | 2 +- .../src/modules/file-manager/transfer.ts | 2 +- apps/server/src/modules/file-manager/trash.ts | 8 +-- apps/server/src/modules/mcp/catalog.ts | 2 +- apps/server/src/modules/mcp/config-store.ts | 2 +- apps/server/src/modules/mcp/server.ts | 2 +- apps/server/src/modules/pi/index.ts | 4 +- apps/server/src/modules/pi/models.ts | 8 +-- apps/server/src/modules/pi/oauth.ts | 6 +-- apps/server/src/modules/pi/refresh.ts | 6 +-- apps/server/src/modules/pi/service.ts | 15 +++--- apps/server/src/modules/pi/store.ts | 4 +- .../src/modules/schedules/agent-checks.ts | 4 +- .../server/src/modules/schedules/scheduler.ts | 2 +- apps/server/src/modules/search/actions.ts | 4 +- apps/server/src/modules/search/works.ts | 2 +- .../src/modules/sources-identity/index.ts | 2 +- .../modules/sources-identity/routes/enrich.ts | 2 +- .../sources-identity/routes/identity.ts | 10 ++-- .../sources-identity/service/enrich.ts | 6 +-- .../service/identity-admin.ts | 9 ++-- .../service/identity-decisions.ts | 4 +- apps/server/src/modules/system/retention.ts | 12 ++--- apps/server/src/modules/websearch/service.ts | 4 +- apps/server/test/bootstrap.test.ts | 2 +- apps/server/test/e2e/e12-interaction.test.ts | 2 +- .../server/test/e2e/e15-agent-quality.test.ts | 2 +- apps/server/test/pi/upstreams.test.ts | 2 +- apps/server/test/rules/rule-groups.test.ts | 4 +- apps/server/test/search/search.test.ts | 6 +-- apps/server/test/search/search2.test.ts | 2 +- .../sources-identity/lazy-identity.test.ts | 6 +-- .../subscriptions/subscriptions-c1a.test.ts | 10 ++-- .../subscriptions/subscriptions-v2.test.ts | 8 +-- apps/server/test/system/storage.test.ts | 4 +- apps/web/app/api/agent-mock.test.ts | 2 +- apps/web/app/api/downloads.ts | 2 +- apps/web/app/api/feeds.ts | 2 +- apps/web/app/api/inventory.ts | 2 +- apps/web/app/api/records.ts | 2 +- apps/web/app/api/rules.ts | 8 +-- apps/web/app/api/sites.ts | 2 +- apps/web/app/api/subscriptions.ts | 4 +- apps/web/app/api/tasks.ts | 4 +- .../components/agent/agent-conversation.tsx | 9 ++-- .../web/app/components/agent/agent-drawer.tsx | 12 ++--- .../components/agent/agent-interaction.tsx | 14 ++--- .../app/components/agent/agent-run-panel.tsx | 6 +-- .../components/agent/agent-session-list.tsx | 2 +- apps/web/app/components/agent/mock.ts | 4 +- apps/web/app/components/agent/quick-goals.ts | 6 +-- apps/web/app/components/agentui/message.tsx | 2 +- .../web/app/components/browser/coords.test.ts | 4 +- apps/web/app/components/browser/coords.ts | 4 +- .../app/components/browser/desktop-view.tsx | 6 +-- .../components/browser/site-origin-view.tsx | 2 +- .../app/components/console/agent-launcher.tsx | 12 ++--- .../app/components/console/data-table.test.ts | 12 ++--- apps/web/app/components/console/nav.test.ts | 4 +- apps/web/app/components/console/nav.ts | 16 +++--- .../components/downloads/business-tasks.tsx | 12 ++--- .../downloads/business-timeline.tsx | 2 +- .../components/downloads/cache-volumes.tsx | 2 +- .../components/downloads/download-detail.tsx | 7 +-- apps/web/app/components/files/dialogs.tsx | 23 ++++---- apps/web/app/components/files/dir-picker.tsx | 2 +- apps/web/app/components/files/dir-tree.tsx | 2 +- apps/web/app/components/files/file-detail.tsx | 10 ++-- apps/web/app/components/files/mock.ts | 2 +- apps/web/app/components/files/model.ts | 6 +-- .../app/components/library/entity-dialogs.tsx | 4 +- .../components/library/identity-enrich.tsx | 4 +- .../library/identity-external-sheet.tsx | 2 +- .../library/identity-pool-sheet.tsx | 2 +- .../app/components/library/identity-views.tsx | 4 +- apps/web/app/components/library/labels.ts | 20 +++---- .../components/library/preferences-form.tsx | 2 +- apps/web/app/components/library/slot-card.tsx | 4 +- .../library/subscriptions-policy.tsx | 10 ++-- .../library/subscriptions-slots.tsx | 14 ++--- apps/web/app/components/naming/model.ts | 2 +- .../app/components/organize-wizard/model.ts | 2 +- .../components/organize-wizard/step-files.tsx | 6 +-- .../organize-wizard/step-options.tsx | 4 +- .../organize-wizard/step-preview.tsx | 2 +- .../web/app/components/rules/group-editor.tsx | 4 +- apps/web/app/components/rules/group-list.tsx | 16 +++--- apps/web/app/components/rules/labels.ts | 10 ++-- apps/web/app/components/rules/tabs.tsx | 6 +-- .../components/schedules/agent-check-card.tsx | 12 ++--- .../schedules/agent-check-dialog.tsx | 2 +- apps/web/app/components/schedules/labels.ts | 2 +- .../app/components/schedules/runs-sheet.tsx | 2 +- .../components/schedules/schedule-board.tsx | 8 +-- .../app/components/search/release-results.tsx | 2 +- .../search/search-download-dialog.tsx | 10 ++-- .../app/components/search/search-labels.ts | 2 +- .../web/app/components/search/search2.test.ts | 10 ++-- apps/web/app/components/search/work-card.tsx | 10 ++-- .../app/components/settings/agent-tools.tsx | 14 ++--- .../components/settings/agent-websearch.tsx | 4 +- apps/web/app/components/settings/agent.tsx | 8 +-- .../app/components/settings/automation.tsx | 10 ++-- .../components/settings/browser-providers.tsx | 2 +- .../web/app/components/settings/instances.tsx | 7 ++- apps/web/app/components/settings/library.tsx | 2 +- apps/web/app/components/settings/metadata.tsx | 4 +- apps/web/app/components/settings/naming.tsx | 10 ++-- .../app/components/settings/path-mappings.tsx | 4 +- apps/web/app/components/settings/pi-test.tsx | 2 +- .../app/components/settings/pi-upstreams.tsx | 22 ++++---- .../web/app/components/settings/pi-wizard.tsx | 14 ++--- apps/web/app/components/settings/sites.tsx | 8 +-- .../app/components/sites/site-add-dialog.tsx | 4 +- apps/web/app/components/sites/site-card.tsx | 10 ++-- .../web/app/components/sites/site-dialogs.tsx | 8 +-- .../app/components/sites/site-feed-dialog.tsx | 4 +- .../app/components/sites/site-overview.tsx | 4 +- .../app/components/status/storage-section.tsx | 6 +-- .../app/components/subscriptions/actions.tsx | 2 +- .../subscriptions/coverage-matrix.tsx | 10 ++-- .../components/subscriptions/model.test.ts | 2 +- .../web/app/components/subscriptions/model.ts | 12 ++--- .../subscriptions/overview-item.tsx | 6 ++- .../components/subscriptions/slot-order.tsx | 2 +- .../components/subscriptions/slot-panel.tsx | 6 +-- .../subscriptions/subscription-form.tsx | 32 +++++------ apps/web/app/components/tasks/labels.test.ts | 2 +- apps/web/app/components/tasks/labels.ts | 18 +++---- .../components/workbench/download-dialog.tsx | 8 +-- .../app/components/workbench/feed-form.tsx | 6 +-- .../app/components/workbench/feed-table.tsx | 4 +- .../workbench/release-detail-sheet.tsx | 9 ++-- .../components/workbench/release-fields.ts | 6 +-- .../app/components/workbench/release-list.tsx | 2 +- .../components/workbench/snapshot-panel.tsx | 16 +++--- apps/web/app/lib/labels.ts | 8 +-- apps/web/app/routes/console/agent.$runId.tsx | 2 +- apps/web/app/routes/console/agent.tsx | 4 +- apps/web/app/routes/console/audit.tsx | 8 +-- .../app/routes/console/downloads.plans.tsx | 4 +- apps/web/app/routes/console/downloads.tsx | 6 +-- apps/web/app/routes/console/files.trash.tsx | 8 +-- apps/web/app/routes/console/files.tsx | 12 +++-- apps/web/app/routes/console/home.tsx | 10 ++-- apps/web/app/routes/console/inbox.tsx | 10 ++-- apps/web/app/routes/console/library.$id.tsx | 24 +++++---- apps/web/app/routes/console/library.tsx | 10 ++-- .../routes/console/media.tmdb.$type.$id.tsx | 14 ++--- apps/web/app/routes/console/organize.$id.tsx | 5 +- apps/web/app/routes/console/sites.$siteId.tsx | 38 +++++++------ apps/web/app/routes/console/sites.tsx | 2 +- apps/web/app/routes/console/sources.tsx | 12 ++--- apps/web/app/routes/console/status.tsx | 14 ++--- .../app/routes/console/subscriptions.$id.tsx | 10 ++-- apps/web/app/routes/console/subscriptions.tsx | 2 +- apps/web/app/routes/console/tasks.$id.tsx | 2 +- apps/web/app/routes/console/tasks.tsx | 2 +- apps/web/scripts/smoke.mjs | 12 ++--- docs/design/glossary.md | 35 +++++++----- packages/agent/src/pi/model.ts | 6 +-- packages/agent/src/pi/runtime.ts | 4 +- packages/agent/src/pi/upstream.ts | 4 +- packages/contracts/src/api.ts | 54 +++++++++---------- packages/contracts/src/capabilities-io.ts | 2 +- packages/contracts/src/capability.ts | 10 ++-- packages/contracts/src/organization.ts | 2 +- packages/contracts/src/pi.ts | 4 +- packages/contracts/src/policy.ts | 24 ++++----- packages/contracts/src/search.ts | 8 +-- packages/contracts/src/settings.ts | 10 ++-- packages/domain/src/acquisition/fill.ts | 2 +- packages/domain/src/naming/variables.ts | 4 +- packages/domain/src/slot/evaluate.ts | 4 +- packages/domain/src/slot/overview.ts | 6 +-- packages/domain/src/slot/types.ts | 2 +- packages/domain/test/naming.test.ts | 2 +- packages/domain/test/rules-slot.test.ts | 2 +- 212 files changed, 741 insertions(+), 715 deletions(-) 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 7ec51daf..3812616b 100644 --- a/apps/server/src/modules/acquisition-organize/index.ts +++ b/apps/server/src/modules/acquisition-organize/index.ts @@ -186,8 +186,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 0f8483df..c8a9f1c4 100644 --- a/apps/server/src/modules/acquisition-organize/services/organize.ts +++ b/apps/server/src/modules/acquisition-organize/services/organize.ts @@ -393,7 +393,7 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise24h;失败吞掉)。copy 路径在 copyWithVerify 内还会再清一次 if (fsr.partialMaxAgeMs > 0) 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 b63dfcde..2e881660 100644 --- a/apps/server/src/modules/agent/capabilities-download.ts +++ b/apps/server/src/modules/agent/capabilities-download.ts @@ -63,7 +63,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/instances-routes.ts b/apps/server/src/modules/auth-settings/instances-routes.ts index 9b6619e3..1ccea786 100644 --- a/apps/server/src/modules/auth-settings/instances-routes.ts +++ b/apps/server/src/modules/auth-settings/instances-routes.ts @@ -62,7 +62,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: "路径映射", @@ -71,7 +71,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, @@ -181,7 +181,7 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) { } catch (e) { throw new ApiException( "precondition_failed", - `库根不可写(需要目录存在且可写入哨兵文件 ${name}):${errMessage(e)}`, + `媒体库目录不可写(需要目录存在且可写入标记文件 ${name}):${errMessage(e)}`, ); } return name; @@ -215,7 +215,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/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..3225108f 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: "订阅登录即将过期的模型服务,提前刷新 token", 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..770f0562 100644 --- a/apps/server/src/modules/sources-identity/index.ts +++ b/apps/server/src/modules/sources-identity/index.ts @@ -63,7 +63,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S }); job("feeds.poll", "Feed 轮询", "扫描到期的 Feed 并入队抓取(每个 Feed 仍按自己的轮询间隔)", tickFeeds); job("sites.snapshot", "站点快照", "扫描到周期的站点并入队快照", tickSnapshots); - job("identity.review", "待识别复查", "到期的待识别条目重新识别", tickReviews); + job("identity.review", "未识别复查", "到期的未识别条目重新识别", tickReviews); // W9-A 站点中心:用户数据每 6 小时、健康检测每小时(可在定时任务页关闭或改周期) registerScheduledJob(ctx, { key: "sites.user_data", 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 7fca1f93..77b7fccf 100644 --- a/apps/server/src/modules/sources-identity/routes/identity.ts +++ b/apps/server/src/modules/sources-identity/routes/identity.ts @@ -542,7 +542,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), @@ -619,7 +619,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, @@ -631,7 +631,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); }, ); @@ -641,7 +641,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() }), @@ -666,7 +666,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 76ffec59..cdd27b6c 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); @@ -475,7 +478,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/retention.ts b/apps/server/src/modules/system/retention.ts index 93185f4d..1c0bf0a6 100644 --- a/apps/server/src/modules/system/retention.ts +++ b/apps/server/src/modules/system/retention.ts @@ -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: "删除超过保留期、会话已结束的智能体事件流与消息(agent_run_event / agent_run_message;会话本身保留)", everyMs: 6 * HOUR, retention: (s) => `${s.retentionDays.agentEvent} 天`, async run(c) { @@ -182,7 +182,7 @@ 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 ? ",未删完,下次继续" : ""}`; }, }, { @@ -230,8 +230,8 @@ export const RETENTION_JOBS: RetentionJob[] = [ { target: "identity_pool", jobId: "storage.retention.identity_pool", - name: "待识别池归档", - description: "待识别池里 unmatched 条目超过保留期没有新证据就归档(不删除,仍可按状态筛出)", + name: "未识别资源归档", + description: "未识别资源里 unmatched 条目超过保留期没有新证据就归档(不删除,仍可按状态筛出)", everyMs: 6 * HOUR, retention: (s) => `${s.retentionDays.identityPoolArchive} 天`, async run(c) { @@ -246,7 +246,7 @@ export const RETENTION_JOBS: RetentionJob[] = [ archived += n; if (n < size) break; } - return `归档 ${archived} 条待识别条目`; + return `归档 ${archived} 条未识别条目`; }, }, { 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 872a139a..07704ae4 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({ @@ -428,7 +428,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 3941c8c4..913181c5 100644 --- a/apps/server/test/sources-identity/lazy-identity.test.ts +++ b/apps/server/test/sources-identity/lazy-identity.test.ts @@ -177,7 +177,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()); @@ -226,7 +226,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") @@ -296,7 +296,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..a23386bc 100644 --- a/apps/web/app/api/downloads.ts +++ b/apps/web/app/api/downloads.ts @@ -157,7 +157,7 @@ export const PAUSE_REASON_LABEL = { user: "用户暂停", threshold: "阈值暂停", policy: "策略暂停", - agent: "Agent 暂停", + agent: "智能体暂停", } as const; export const PLAN_STATUS_LABEL: 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..c9fe7970 100644 --- a/apps/web/app/api/tasks.ts +++ b/apps/web/app/api/tasks.ts @@ -23,7 +23,7 @@ const RecordPage = cursorPage(PublicOrganizationRecord); const DecisionPage = cursorPage(Decision); const RESOLUTION_LABEL = { - unresolved: "待识别", + unresolved: "未识别", resolved: "已识别", ambiguous: "有歧义,待确认", unidentifiable: "无法识别", @@ -260,7 +260,7 @@ export function buildTimeline(input: { key: `agent-${d.id}`, stage: "agent", at: d.updatedAt, - title: "Agent 已介入调查", + title: "智能体已介入调查", tone: "neutral", href: `/console/agent/${d.agentRunId}`, }); diff --git a/apps/web/app/components/agent/agent-conversation.tsx b/apps/web/app/components/agent/agent-conversation.tsx index a9863c31..4495debf 100644 --- a/apps/web/app/components/agent/agent-conversation.tsx +++ b/apps/web/app/components/agent/agent-conversation.tsx @@ -249,8 +249,7 @@ export function ToolCallCard({
{call.toolCallId}
幂等键
- {audit?.idempotencyKey ?? - (meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读能力不记操作账本")} + {audit?.idempotencyKey ?? (meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读操作不记录")}
{audit?.grantId || audit?.approvalId ? ( <> @@ -652,7 +651,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 +677,13 @@ export function AgentConversation({ b.kind === "single" ? (
{b.entry.node}
) : ( - + - Agent + 智能体 {b.parts.map((p) => diff --git a/apps/web/app/components/agent/agent-drawer.tsx b/apps/web/app/components/agent/agent-drawer.tsx index a846a22b..2a5d498d 100644 --- a/apps/web/app/components/agent/agent-drawer.tsx +++ b/apps/web/app/components/agent/agent-drawer.tsx @@ -72,7 +72,7 @@ function DrawerTabs({ tab }: { tab: AgentDrawerTab }) { return (
{TABS.map((t) => ( @@ -161,10 +161,10 @@ export function AgentDrawer() { - Agent + 智能体 - {context?.label ? `当前对象:${context.label}` : "交代目标,Agent 在授权范围内执行并汇报。"} + {context?.label ? `当前对象:${context.label}` : "交代目标,智能体在授权范围内执行并汇报。"} ({agentShortcutLabel()} 开关,Esc 关闭) @@ -281,7 +281,7 @@ function AgentMiniDock({ runId }: { runId: string }) { : null; return (
@@ -290,7 +290,7 @@ function AgentMiniDock({ runId }: { runId: string }) { {run.data ? splitContextGoal(run.data.run.goal).text : ""} - {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..f469c0c7 100644 --- a/apps/web/app/components/agent/agent-run-panel.tsx +++ b/apps/web/app/components/agent/agent-run-panel.tsx @@ -114,8 +114,8 @@ export function GoalComposer({ />

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

{suggestions?.length ? ( @@ -400,7 +400,7 @@ export function AgentRunPanel({ viewportClassName="pr-1" contentClassName={column} busy={streaming} - label="Agent 会话记录" + label="智能体会话记录" > +
{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,7 +1047,7 @@ export function RefreshMetadataDialog({ items, onClose }: { items: FileEntry[];

{s.ref}: {s.reason === "not_registered" - ? "没有已登记资产" + ? "没有已入库文件" : s.reason === "no_entity" ? "不属于任何已登记作品" : s.reason === "not_found" 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..6e60dfb0 100644 --- a/apps/web/app/components/files/dir-tree.tsx +++ b/apps/web/app/components/files/dir-tree.tsx @@ -148,7 +148,7 @@ function RootNode({ )} {root.pendingCount ? ( - 待识别 {root.pendingCount} + 未识别 {root.pendingCount} {root.pendingPartial ? "+" : ""} ) : null} diff --git a/apps/web/app/components/files/file-detail.tsx b/apps/web/app/components/files/file-detail.tsx index 0dab7257..c492f0cf 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, @@ -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.ts b/apps/web/app/components/files/model.ts index 401e1ba0..0850ea1c 100644 --- a/apps/web/app/components/files/model.ts +++ b/apps/web/app/components/files/model.ts @@ -32,7 +32,7 @@ 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..b7ec7eaa 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,8 +132,8 @@ export const CANDIDATE_DECISION: Record = { }; export const DECISION_KIND: Record = { - identity_ambiguous: "身份歧义", - identity_unresolved: "待识别", + identity_ambiguous: "没识别出作品", + identity_unresolved: "未识别", download_stalled: "下载无进展", download_failed: "下载失败", path_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/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/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..d93c4ef7 100644 --- a/apps/web/app/components/sites/site-card.tsx +++ b/apps/web/app/components/sites/site-card.tsx @@ -141,11 +141,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 }, )} @@ -314,13 +314,13 @@ export function SiteCard({ site, checks, onDialog }: Props) {
-
快照
+
抓取
{!site.snapshotEnabled ? "未开启" : site.lastSnapshotAt ? `${relTime(site.lastSnapshotAt)}${stale ? " · 已过期" : ""}` - : "还没有快照"} + : "还没抓取过"}
@@ -455,7 +455,7 @@ export function SiteTable({ }, { key: "snapshot", - header: "快照", + header: "抓取", hideBelow: "lg", cell: (s) => ( diff --git a/apps/web/app/components/sites/site-dialogs.tsx b/apps/web/app/components/sites/site-dialogs.tsx index 68de8523..9bfe2333 100644 --- a/apps/web/app/components/sites/site-dialogs.tsx +++ b/apps/web/app/components/sites/site-dialogs.tsx @@ -380,7 +380,7 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose: open onOpenChange={(o) => !o && !busy && onClose()} title={`编辑 · ${site.name}`} - description="限流和快照周期留空时用「设置 → 站点默认」" + description="限流和抓取周期留空时用「设置 → 站点默认」" footer={ <>
{v.snapshotEnabled ? ( - +
  • 站点、{site.accounts?.length ?? 0} 个账号和全部凭证删除
  • 已抓到的资源保留,标为"站点已关闭",不再产生新候选
  • -
  • 该站 Feed 停用;已有下载任务按原策略继续
  • +
  • 该站 RSS 停用;已有下载任务按原策略继续
  • 同一站点以后可以重新添加
  • diff --git a/apps/web/app/components/sites/site-feed-dialog.tsx b/apps/web/app/components/sites/site-feed-dialog.tsx index 1566e4f6..3b2df6bd 100644 --- a/apps/web/app/components/sites/site-feed-dialog.tsx +++ b/apps/web/app/components/sites/site-feed-dialog.tsx @@ -216,7 +216,7 @@ export function SiteFeedDialog({

    {account ? `账号 ${account.label}` : "该站没有账号,按地址里的 passkey 抓取"} · 解析器按站点定义 · 每 30 - 分钟抓一次,可在 Feed 详情里改 + 分钟抓一次,可在 RSS 详情里改

    )} @@ -279,7 +279,7 @@ function FirstFetch({

    ) : timedOut ? (

    - 已保存。抓取还在排队(站点限流),结果稍后显示在 Feed 列表里。 + 已保存。抓取还在排队(站点限流),结果稍后显示在 RSS 列表里。

    ) : (

    diff --git a/apps/web/app/components/sites/site-overview.tsx b/apps/web/app/components/sites/site-overview.tsx index 94e8f9c7..015b3481 100644 --- a/apps/web/app/components/sites/site-overview.tsx +++ b/apps/web/app/components/sites/site-overview.tsx @@ -99,9 +99,9 @@ export function SiteOverview({ site }: { site: SiteListItem }) { ), }, { - label: "快照", + label: "抓取", value: site.snapshotEnabled - ? `${site.lastSnapshotAt ? relTime(site.lastSnapshotAt) : "还没有快照"} · ${site.snapshotIntervalSec ? `每 ${Math.round(site.snapshotIntervalSec / 60)} 分钟` : "默认周期"}` + ? `${site.lastSnapshotAt ? relTime(site.lastSnapshotAt) : "还没抓取过"} · ${site.snapshotIntervalSec ? `每 ${Math.round(site.snapshotIntervalSec / 60)} 分钟` : "默认周期"}` : "未开启", }, { label: "限流", value: rateLimitText(site.rateLimit) }, diff --git a/apps/web/app/components/status/storage-section.tsx b/apps/web/app/components/status/storage-section.tsx index b208bcb0..cb8e79f4 100644 --- a/apps/web/app/components/status/storage-section.tsx +++ b/apps/web/app/components/status/storage-section.tsx @@ -30,14 +30,14 @@ const LEVEL: Record = { /** 清理目标的界面名称 */ export const CLEANUP_LABEL: Record = { - observation: "Feed 观察记录", + observation: "RSS 抓取记录", sse_event: "SSE 补读事件", heartbeat: "进程心跳", task_run: "任务运行", - agent_event: "Agent 事件", + agent_event: "智能体事件", audit: "审计事件", capability_stats: "能力调用统计", - identity_pool: "待识别池归档", + identity_pool: "未识别资源归档", analyze: "统计信息更新", images: "图片缓存容量", kv_cache: "过期缓存", diff --git a/apps/web/app/components/subscriptions/actions.tsx b/apps/web/app/components/subscriptions/actions.tsx index 8381f5a1..5d86801b 100644 --- a/apps/web/app/components/subscriptions/actions.tsx +++ b/apps/web/app/components/subscriptions/actions.tsx @@ -216,7 +216,7 @@ export function DeleteSubscriptionsDialog({ open={open} onOpenChange={onOpenChange} title={ids.length > 1 ? `删除 ${ids.length} 个订阅?` : "删除这个订阅?"} - description="不再搜索和追更。已入库文件、版本目标和下载记录保留,可在订阅历史里重新订阅。" + description="不再搜索和追更。已入库文件、收录版本和下载记录保留,可在订阅历史里重新订阅。" confirmLabel="删除" destructive onConfirm={() => diff --git a/apps/web/app/components/subscriptions/coverage-matrix.tsx b/apps/web/app/components/subscriptions/coverage-matrix.tsx index 1bdd4c5b..13fa3e9a 100644 --- a/apps/web/app/components/subscriptions/coverage-matrix.tsx +++ b/apps/web/app/components/subscriptions/coverage-matrix.tsx @@ -22,7 +22,7 @@ import { seasonTitle, } from "./model.ts"; -// 覆盖矩阵(设计 §2.1):列 = 集(电影 = 内容版本),行 = 版本目标,首行"库内任一版本"。 +// 覆盖矩阵(设计 §2.1):列 = 集(电影 = 内容版本),行 = 收录版本,首行"库内任一版本"。 // 底色无彩,只有状态用状态色:已入库 success、缺失 destructive、可升级/延后 warning;在途用斜纹。 type Row = SubscriptionCoverage["rows"][number]; @@ -120,7 +120,7 @@ export function CoverageMatrix({ scope="col" className="sticky left-0 z-10 bg-surface-raised pr-2 text-left font-normal text-muted-foreground" > - 版本目标 + 收录版本 {cols.map((c) => ( @@ -284,7 +284,7 @@ export function CellSheet({ { label: "pri", value: c.pri !== null ? String(c.pri) : null }, { label: "可升级", value: c.state === "have" ? (c.upgradable ? "是,低于目标档位" : "否") : null }, { - label: "资产", + label: "文件", value: c.assetId ? ( mediaEntityId ? ( ) : null} {sel.row.slotId === null && c.state === "missing" ? ( -

    这一行汇总全部版本。要补全,点具体版本目标那一行的格。

    +

    这一行汇总全部版本。要补全,点具体收录版本那一行的格。

    ) : null} ) : null} diff --git a/apps/web/app/components/subscriptions/model.test.ts b/apps/web/app/components/subscriptions/model.test.ts index a0e3aef6..29c76d8b 100644 --- a/apps/web/app/components/subscriptions/model.test.ts +++ b/apps/web/app/components/subscriptions/model.test.ts @@ -86,7 +86,7 @@ describe("覆盖矩阵格", () => { expect(moveItem(["a", "b", "c"], 2, 0)).toEqual(["c", "a", "b"]); expect(moveItem(["a", "b"], 0, 5)).toEqual(["a", "b"]); }); - it("升级策略取自版本目标", () => { + it("升级策略取自收录版本", () => { expect(upgradeOf([{ upgradeThreshold: {} }], "series")).toBe("off"); expect(upgradeOf([{ upgradeThreshold: { enabled: true, mode: "full_pack" } }], "series")).toBe("full_pack"); expect(upgradeOf([{ upgradeThreshold: { enabled: true } }], "movie")).toBe("movie"); diff --git a/apps/web/app/components/subscriptions/model.ts b/apps/web/app/components/subscriptions/model.ts index 8936aaca..005062b8 100644 --- a/apps/web/app/components/subscriptions/model.ts +++ b/apps/web/app/components/subscriptions/model.ts @@ -55,7 +55,7 @@ export function seasonLabel( return x.seasonScope === "following_seasons" && x.seasonNumber > 0 ? `${base} 起` : base; } -/** 版本目标徽标:洗版中 → 在途 → 缺 N → 已齐;都没有 → 已入库 N */ +/** 收录版本徽标:洗版中 → 在途 → 缺 N → 已齐;都没有 → 已入库 N */ export function slotBadge(s: SubscriptionSlotSummary): ToneLabel { if (s.upgrading) return { @@ -136,7 +136,7 @@ export const PLAN_STAGE: Record = { reserved: "已预留", downl export const SKIP_REASON: Record = { out_of_range: "集范围外", not_aired: "未播出", - not_required: "该版本目标不要求这个内容版本", + not_required: "该收录版本不要求这个内容版本", suspected: "总集数未知,按已见最高集推断", deferred: "有延后的资源", }; @@ -155,7 +155,7 @@ export function seasonTitle(s: SubscriptionCoverage["seasons"][number], kind: Su return s.seasonNumber === 0 ? "特别篇" : `第 ${s.seasonNumber} 季`; } -// ── 版本目标 ── +// ── 收录版本 ── export type UpgradeStrategy = "off" | "per_unit" | "full_pack" | "movie"; export const UPGRADE_LABEL: Record = { @@ -165,7 +165,7 @@ export const UPGRADE_LABEL: Record = { movie: "洗版", }; -/** 订阅版本目标当前的升级策略:任一开启取其 mode */ +/** 订阅收录版本当前的升级策略:任一开启取其 mode */ export function upgradeOf( slots: Pick[], kind: SubscriptionMediaKind, @@ -231,7 +231,7 @@ export type SubscriptionDraft = { include: string; exclude: string; keywords: string; - /** MP 分辨率 / 资源质量 / 特效预设(写到该订阅自己的版本目标) */ + /** MP 分辨率 / 资源质量 / 特效预设(写到该订阅自己的收录版本) */ resolution: SubscriptionResolution | ""; source: SubscriptionSourcePreset | ""; effect: SubscriptionEffect | ""; @@ -334,7 +334,7 @@ const isInt = (s: string) => /^\d+$/.test(s.trim()); /** * 草稿 → params(PATCH 整体替换,所以以现有 params 为底保留未知键)。 - * upgradeChanged:只有升级策略改了才带 best_version(否则每次保存都会 bump 版本目标 revision)。 + * upgradeChanged:只有升级策略改了才带 best_version(否则每次保存都会 bump 收录版本 revision)。 */ export function draftToParams( d: SubscriptionDraft, diff --git a/apps/web/app/components/subscriptions/overview-item.tsx b/apps/web/app/components/subscriptions/overview-item.tsx index f2999920..4cb5c568 100644 --- a/apps/web/app/components/subscriptions/overview-item.tsx +++ b/apps/web/app/components/subscriptions/overview-item.tsx @@ -26,7 +26,9 @@ function Flags({ x }: { x: Item }) { return ( <> {x.group === "paused" ? 暂停 : null} - {x.group === "pending" ? {x.mediaEntityId ? "待定" : "尚未建档"} : null} + {x.group === "pending" ? ( + {x.mediaEntityId ? "待定" : "不在媒体库"} + ) : null} {x.upgrading ? 洗版中 : null} {x.awaitingNewSeason ? 等待新季 : null} @@ -72,7 +74,7 @@ export function ProgressBlock({ x, className }: { x: Pick没有版本目标
    ; + if (slots.length === 0) return 没有收录版本; return (
    {slots.slice(0, max).map((s) => { diff --git a/apps/web/app/components/subscriptions/slot-order.tsx b/apps/web/app/components/subscriptions/slot-order.tsx index 7ff0139b..d3d08047 100644 --- a/apps/web/app/components/subscriptions/slot-order.tsx +++ b/apps/web/app/components/subscriptions/slot-order.tsx @@ -4,7 +4,7 @@ import { Button } from "~/components/ui/button.tsx"; import { cn } from "~/lib/utils.ts"; import { moveItem } from "./model.ts"; -// 版本目标排序:拖动(桌面)或上移/下移(键盘、手机)。第一个是主版本。 +// 收录版本排序:拖动(桌面)或上移/下移(键盘、手机)。第一个是主版本。 export function SlotOrderList({ items, diff --git a/apps/web/app/components/subscriptions/slot-panel.tsx b/apps/web/app/components/subscriptions/slot-panel.tsx index 73720dab..9d2da3f9 100644 --- a/apps/web/app/components/subscriptions/slot-panel.tsx +++ b/apps/web/app/components/subscriptions/slot-panel.tsx @@ -16,7 +16,7 @@ import { import { SlotOrderList } from "./slot-order.tsx"; import { AutoFillFields } from "./subscription-form.tsx"; -// 对比视图的"版本目标"区(设计 §2.2):拖动排序(PUT slot-order)、行尾计数、补全此版本、自动补全开关与预算。 +// 对比视图的"收录版本"区(设计 §2.2):拖动排序(PUT slot-order)、行尾计数、补全此版本、自动补全开关与预算。 type SlotRow = SubscriptionCoverage["rows"][number] & { id: string }; @@ -83,7 +83,7 @@ export function SlotPanel({ return ( {rows.length === 0 ? (

    - {sub.mediaEntityId ? "还没有版本目标。在下方规则区新建。" : "作品建档后才有版本目标。"} + {sub.mediaEntityId ? "还没有收录版本。在下方规则区新建。" : "作品加入媒体库后才有收录版本。"}

    ) : ( void; error?: st kind: "title", titleQuery: c.year ? `${c.title} ${c.year}` : c.title, mediaKind: c.entityType === "movie" ? "movie" : "series", - note: `TMDB ${c.externalId},建档失败,按标题追踪`, + note: `TMDB ${c.externalId},加入媒体库失败,按标题追踪`, }); }; const row = (key: string, main: ReactNode, sub: string, onClick: () => void) => ( @@ -321,7 +321,7 @@ export function SubscriptionFormDialog({ const [totalTouched, setTotalTouched] = useState(false); const [errors, setErrors] = useState({}); const [dup, setDup] = useState(null); - // 编辑:版本目标到了再回填升级策略 + // 编辑:收录版本到了再回填升级策略 useEffect(() => { if (sub && slotsQ.isSuccess && !upgradeTouched) setD((x) => ({ ...x, upgrade: initialUpgrade })); }, [sub, slotsQ.isSuccess, initialUpgrade, upgradeTouched]); @@ -454,7 +454,7 @@ export function SubscriptionFormDialog({ {editing ? "编辑订阅" : "新建订阅"} - {editing ? titleText : initialTarget ? titleText : "从媒体库或 TMDB 选作品;选 TMDB 条目会先建档。"} + {editing ? titleText : initialTarget ? titleText : "从媒体库或 TMDB 选作品;选 TMDB 条目会先加入媒体库。"} {dup ? ( @@ -497,8 +497,8 @@ export function SubscriptionFormDialog({ {target.kind === "entity" ? "媒体库作品" : target.kind === "tmdb" - ? "TMDB 条目,保存时建档" - : (target.note ?? "按标题追踪,尚未建档")} + ? "TMDB 条目,保存时加入媒体库" + : (target.note ?? "按标题追踪,不在媒体库")}

    {initialTarget ? null : ( @@ -541,7 +541,9 @@ export function SubscriptionFormDialog({ ({ value: x.id, label: x.name }))} /> - + -
    +
    {editing ? ( slotsQ.isPending ? ( -

    读取版本目标…

    +

    读取收录版本…

    ) : subSlots.length === 0 ? ( -

    还没有版本目标。在对比视图里新建。

    +

    还没有收录版本。在对比视图里新建。

    ) : ( {groups.isPending ? (

    读取规则组…

    @@ -810,7 +812,7 @@ export function SubscriptionFormDialog({

    - 分辨率、资源质量、特效写进这个订阅自己的版本目标;UHD 和杜比全景声按标题匹配,与「包含」同时生效。 + 分辨率、资源质量、特效写进这个订阅自己的收录版本;UHD 和杜比全景声按标题匹配,与「包含」同时生效。

    { triggeredBy: null, outcome, }); - it("引擎跑完但 Agent 会话超出预算 / 失败:显示业务结果,不显示「完成」", () => { + it("引擎跑完但智能体会话超出预算 / 失败:显示业务结果,不显示「完成」", () => { expect(engineRunStatus({ status: "completed", business: agent("budget_exceeded") })).toEqual({ label: "超出预算", tone: "warning", diff --git a/apps/web/app/components/tasks/labels.ts b/apps/web/app/components/tasks/labels.ts index 4389c2bd..b271149e 100644 --- a/apps/web/app/components/tasks/labels.ts +++ b/apps/web/app/components/tasks/labels.ts @@ -8,9 +8,9 @@ export const BUSINESS_KIND_LABEL: Record = { subscription: "订阅", download: "下载", organization: "整理", - agent: "Agent", + agent: "智能体", schedule: "定时任务", - feed: "Feed", + feed: "RSS", site: "站点", decision: "待处理事项", media: "作品", @@ -23,7 +23,7 @@ export const SOURCE_LABEL: Record = { schedule: "定时", event: "事件", manual: "手动", - agent: "Agent", + agent: "智能体", }; /** 来源 → 服务端 triggeredBy 筛选值(逗号分隔多值) */ export const SOURCE_TRIGGERED_BY: Record = { @@ -46,15 +46,15 @@ export const WORKFLOW_LABEL: Record = { submitDownload: "提交下载", verifyAndImport: "核验入库", refreshMetadata: "刷新元数据", - reconcileLibrary: "资产对账", + reconcileLibrary: "文件核对", cleanupDownloadCache: "清理缓存", resolveDecision: "处理待处理事项", - runAgent: "Agent 会话", - snapshotSite: "站点快照", + runAgent: "智能体会话", + snapshotSite: "站点抓取", refreshSiteAccount: "刷新站点账号", searchSubscription: "订阅搜索", purgeTrash: "清理回收站", - refreshPiTokens: "刷新上游登录", + refreshPiTokens: "刷新模型服务登录", }; export const workflowLabel = (name: string) => WORKFLOW_LABEL[name] ?? name; @@ -115,11 +115,11 @@ export function businessLink(b: EngineRun["business"], input?: unknown): { to: s case "organization": return { to: `/console/organize/${id}`, label: "整理记录" }; case "agent": - return { to: `/console/agent/${id}`, label: "Agent 会话" }; + return { to: `/console/agent/${id}`, label: "智能体会话" }; case "schedule": return { to: `/console/schedules?job=${id}`, label: "定时任务" }; case "feed": - return { to: consolePath("sources"), label: "Feed" }; + return { to: consolePath("sources"), label: "RSS" }; case "site": return { to: `/console/sites/${id}`, label: "站点" }; case "decision": diff --git a/apps/web/app/components/workbench/download-dialog.tsx b/apps/web/app/components/workbench/download-dialog.tsx index e1c7d32d..b22ca448 100644 --- a/apps/web/app/components/workbench/download-dialog.tsx +++ b/apps/web/app/components/workbench/download-dialog.tsx @@ -19,7 +19,7 @@ import { errorMessage } from "~/lib/api.ts"; import { formatBytes } from "~/lib/utils.ts"; const STEPS = [ - "生成下载计划,预留版本目标和预算;每个版本目标同时只有一个在途计划", + "生成下载计划,预留收录版本和预算;每个收录版本同时只有一个在途计划", "已有相同资源或相同 hash 的任务时直接接管,不重复提交", "缓存卷超过硬阈值或读不到磁盘状态时无法提交", "提交前复核免费、做种等易变字段", @@ -93,7 +93,7 @@ export function DownloadDialog({ {slots.length ? (
    {release.entity - ? "这个作品没有版本目标。先在订阅里新建。" + ? "这个作品没有收录版本。先在订阅里新建。" : "资源未关联本地作品。先在「身份与元数据」指定。"}

    )} @@ -143,7 +143,7 @@ export function DownloadDialog({ }} > - 委托 Agent + 委托智能体
    @@ -80,7 +80,7 @@ export function SnapshotPanel({ site }: { site: Site }) { ) : snaps.error ? ( void snaps.refetch()} /> ) : !snaps.data.items.length ? ( -

    还没有快照。

    +

    还没抓取过。

    ) : (
      {snaps.data.items.map((s) => ( diff --git a/apps/web/app/lib/labels.ts b/apps/web/app/lib/labels.ts index a59a8b4e..c308c9c1 100644 --- a/apps/web/app/lib/labels.ts +++ b/apps/web/app/lib/labels.ts @@ -37,8 +37,8 @@ const DOWNLOAD: Record = { export const downloadLabel = (v: string | null | undefined) => pick(DOWNLOAD, v); const DECISION_KIND: Record = { - identity_ambiguous: "识别有歧义", - identity_unresolved: "待识别", + identity_ambiguous: "没识别出作品", + identity_unresolved: "未识别", download_stalled: "下载无进展", download_failed: "下载失败", path_conflict: "路径冲突", @@ -132,7 +132,7 @@ export function actorText(v: string | null | undefined): string { const kind = i < 0 ? v : v.slice(0, i); const rest = i < 0 ? "" : v.slice(i + 1); if (kind === "admin_session" || kind === "dev_admin" || kind === "user") return "管理员"; - if (kind === "agent" || kind === "agent_run") return "Agent"; + if (kind === "agent" || kind === "agent_run") return "智能体"; if (kind === "schedule" || kind === "scheduler") return "定时任务"; if (kind === "api_key") return "API Key"; if (kind === "policy") return `收录策略 ${rest}`; @@ -143,7 +143,7 @@ export function actorText(v: string | null | undefined): string { export const ACTOR_TYPES = [ { value: "user", label: "管理员" }, { value: "api_key", label: "API Key" }, - { value: "agent", label: "Agent" }, + { value: "agent", label: "智能体" }, { value: "system", label: "系统" }, ]; export const actorTypeLabel = (t: string) => ACTOR_TYPES.find((a) => a.value === t)?.label ?? t; diff --git a/apps/web/app/routes/console/agent.$runId.tsx b/apps/web/app/routes/console/agent.$runId.tsx index e918f9f3..82026097 100644 --- a/apps/web/app/routes/console/agent.$runId.tsx +++ b/apps/web/app/routes/console/agent.$runId.tsx @@ -17,7 +17,7 @@ export default function AgentRunPage() { return ( <> diff --git a/apps/web/app/routes/console/agent.tsx b/apps/web/app/routes/console/agent.tsx index a6bb57e2..4ca2f346 100644 --- a/apps/web/app/routes/console/agent.tsx +++ b/apps/web/app/routes/console/agent.tsx @@ -16,8 +16,8 @@ export default function AgentPage() { return ( <> {mock ? (
      diff --git a/apps/web/app/routes/console/audit.tsx b/apps/web/app/routes/console/audit.tsx index 9e7ecb98..a7b585ce 100644 --- a/apps/web/app/routes/console/audit.tsx +++ b/apps/web/app/routes/console/audit.tsx @@ -40,10 +40,10 @@ const ACTION_LABEL: Record = { "schedule.skip": "跳过定时任务", "acquisition_plan.manual_submit": "手动提交下载计划", "engine.run_retry": "重跑任务", - "capability.invoke": "Agent 调用能力", + "capability.invoke": "智能体调用能力", "site.created": "登记站点", "site.updated": "修改站点", - "feed_source.created": "新建 Feed", + "feed_source.created": "新建 RSS", "identity.release_assigned": "指定资源身份", "schedule.update": "修改定时任务", "files.mkdir": "新建目录", @@ -56,11 +56,11 @@ const ACTION_LABEL: Record = { }; const VERB: Record = { create: "新建", update: "修改", delete: "删除", test: "测试", verify: "校验" }; const NOUN: Record = { - library_root: "库根", + library_root: "媒体库目录", downloader_instance: "下载器", media_server_instance: "媒体服务器", path_mapping: "路径映射", - pi_upstream: "Agent 上游", + pi_upstream: "模型服务", schedule: "定时任务", automation_grant: "持续授权", }; diff --git a/apps/web/app/routes/console/downloads.plans.tsx b/apps/web/app/routes/console/downloads.plans.tsx index df560b76..a11e7691 100644 --- a/apps/web/app/routes/console/downloads.plans.tsx +++ b/apps/web/app/routes/console/downloads.plans.tsx @@ -78,7 +78,7 @@ export default function DownloadPlansPage() { return ( <> - + diff --git a/apps/web/app/routes/console/downloads.tsx b/apps/web/app/routes/console/downloads.tsx index bfde0232..a1962032 100644 --- a/apps/web/app/routes/console/downloads.tsx +++ b/apps/web/app/routes/console/downloads.tsx @@ -109,12 +109,12 @@ export default function DownloadsPage() { options={Object.entries(DOWNLOAD_STATUS_LABEL).map(([value, label]) => ({ value, label }))} /> f.set("managed", v)} options={[ - { value: "true", label: "纳管任务" }, - { value: "false", label: "未纳管" }, + { value: "true", label: "本工具" }, + { value: "false", label: "其他" }, ]} /> diff --git a/apps/web/app/routes/console/files.trash.tsx b/apps/web/app/routes/console/files.trash.tsx index 74bac6bc..ab4a3249 100644 --- a/apps/web/app/routes/console/files.trash.tsx +++ b/apps/web/app/routes/console/files.trash.tsx @@ -212,14 +212,14 @@ export default function FilesTrashPage() { }, { key: "asset", - header: "关联资产", + header: "媒体库记录", hideBelow: "lg", cell: (r) => { const snap = r.assetSnapshot as { title?: string; contentUnit?: string | null; mediaEntityId?: string } | null; if (!snap?.title) return —; const label = `${snap.title}${snap.contentUnit ? ` ${snap.contentUnit}` : ""}`; return ( - + {snap.mediaEntityId ? ( {label} @@ -274,7 +274,7 @@ export default function FilesTrashPage() { <> {filesMockEnabled() ? 演示数据 : null} @@ -419,7 +419,7 @@ function RestoreToDialog({ 还原到… - {ids.length} 项 · 选一个可写库根下的目录。同名项已存在时不覆盖。 + {ids.length} 项 · 选一个可写媒体库目录下的目录。同名项已存在时不覆盖。 diff --git a/apps/web/app/routes/console/files.tsx b/apps/web/app/routes/console/files.tsx index 05081a9a..3277c71b 100644 --- a/apps/web/app/routes/console/files.tsx +++ b/apps/web/app/routes/console/files.tsx @@ -97,7 +97,7 @@ import { cn, formatBytes } from "~/lib/utils.ts"; const HEALTH_REASON: Record = { root_missing: "挂载点不存在", - sentinel_missing: "目录里没有哨兵文件,挂载可能没生效", + sentinel_missing: "目录里没有标记文件,挂载可能没生效", timeout: "探测超时", permission_denied: "没有读取权限", }; @@ -398,7 +398,9 @@ export default function FilesPage() { onSuccess: (r) => { const ok = r.items.filter((i) => i.status === "ok").length; toast.success(`探测完成:${ok}/${r.items.length} 个读到媒体信息`, { - description: r.items.some((i) => i.savedToAsset) ? "媒体信息已写入资产" : "未登记文件的结果缓存 24 小时", + description: r.items.some((i) => i.savedToAsset) + ? "媒体信息已写入媒体库记录" + : "未登记文件的结果缓存 24 小时", }); }, onError: (err) => toast.error(filesErrorText(err, "探测失败")), @@ -540,7 +542,7 @@ export default function FilesPage() { if (items.every((i) => i.root === target.root && (i.path === target.path || parentRel(i.path) === target.path))) return; const tRoot = rootList.find((r) => r.id === target.root); - if (!tRoot?.writable) return toast.error("只能拖到可写的库根"); + if (!tRoot?.writable) return toast.error("只能拖到可写的媒体库目录"); setDialog({ kind: "transfer", items, mode: ev.altKey ? "copy" : "move", target }); }; @@ -567,7 +569,7 @@ export default function FilesPage() { plain icon={FolderTree} title="还没有登记目录" - description="在设置里添加库根或下载器路径映射。" + description="在设置里添加媒体库目录或下载器路径映射。" className="px-2 py-8" /> ) : null} @@ -596,7 +598,7 @@ export default function FilesPage() {

      {root.name} 离线

      - {offline ? filesErrorText(list.error) : healthReasonText(root.healthReason)}。离线期间资产状态不变。 + {offline ? filesErrorText(list.error) : healthReasonText(root.healthReason)}。离线期间文件状态不变。

      上次检查:{relTime(root.checkedAt)}

      - 版本目标满足率 + 收录版本满足率 {rate === null ? "—" : `${rate}%`}
      @@ -376,7 +376,7 @@ function PlanStrip() { 失败 {s.auto.failed} - + 启用 {s.checks.enabled} / {s.checks.total} @@ -464,7 +464,7 @@ function SourcesSection({ s }: { s: Dashboard["sources"] }) { ...siteRows, ...s.feeds.map((x) => ({ id: x.id, - kind: "Feed", + kind: "RSS", name: x.name, enabled: x.enabled, health: x.health, @@ -491,7 +491,7 @@ function SourcesSection({ s }: { s: Dashboard["sources"] }) { return (
      {rows.length === 0 ? ( - + ) : ( <> {accountIssues ?

      {accountIssues} 个站点的账号需要处理

      : null} diff --git a/apps/web/app/routes/console/inbox.tsx b/apps/web/app/routes/console/inbox.tsx index a3195355..b155fbe6 100644 --- a/apps/web/app/routes/console/inbox.tsx +++ b/apps/web/app/routes/console/inbox.tsx @@ -78,7 +78,7 @@ function GroupReleases({ d }: { d: Decision }) { ))} {count > list.length ? (
    • - 另有 {count - list.length} 条,可在媒体概览 → 待识别按标题检索 + 另有 {count - list.length} 条,可在媒体概览 → 未识别按标题检索
    • ) : null}
    @@ -94,7 +94,7 @@ function entityOf(d: Decision): string | null { } const IMPACT_KEY: Record = { - slots: "版本目标", + slots: "收录版本", units: "内容单元", files: "文件", releases: "资源", @@ -140,7 +140,7 @@ function Evidence({ d }: { d: Decision }) {
    - Agent 已检查 + 智能体已检查 {checked.map((c) => ( {c} @@ -303,7 +303,7 @@ function DecisionCard({ d }: { d: Decision }) { to={consolePath("agent", d.agentRunId)} className="ml-auto inline-flex items-center text-meta text-muted-foreground hover:text-foreground hover:underline" > - Agent 调查记录 + 智能体调查记录 ) : null} @@ -404,7 +404,7 @@ export default function InboxPage() { <> diff --git a/apps/web/app/routes/console/library.$id.tsx b/apps/web/app/routes/console/library.$id.tsx index 42b00361..baf39825 100644 --- a/apps/web/app/routes/console/library.$id.tsx +++ b/apps/web/app/routes/console/library.$id.tsx @@ -83,7 +83,7 @@ import { formatBytes, formatDateTime } from "~/lib/utils.ts"; // 作品详情(REQ §13.1 / §13.4.3):实际路径、外部身份映射及状态、元数据来源、各 Slot、候选拒绝理由、 // 内容单元、下载历史与校验结果,同一页同时展示。 -// MD-1(对照 MP 媒体详情):头部背景图 + 海报 + 评分 / 类型 / 状态 + 简介、主创、季折叠与每集行(库内状态按版本目标), +// MD-1(对照 MP 媒体详情):头部背景图 + 海报 + 评分 / 类型 / 状态 + 简介、主创、季折叠与每集行(库内状态按收录版本), // 头部 / 主创 / 季集组件与仅 TMDB 的作品详情页共用(components/media/media-detail)。 const pad = (n: number) => String(n).padStart(2, "0"); @@ -120,13 +120,13 @@ function PathsPanel({ detail }: { detail: LibraryDetail }) { const rootOf = (id: string | null) => roots.data?.items.find((r) => r.id === id); const bindings = detail.pathBindings.filter((b) => b.status !== "superseded"); return ( - + {bindings.length === 0 && detail.assets.length === 0 ? ( ) : ( @@ -139,7 +139,9 @@ function PathsPanel({ detail }: { detail: LibraryDetail }) {
  • {b.bindingKind === "season" ? `第 ${b.seasonNumber} 季目录` : "作品目录"} - 库根 {root?.name ?? "未知库根"} + + 媒体库目录 {root?.name ?? "未知媒体库目录"} + {b.status === "conflict" ? 绑定冲突 : null}

    @@ -171,7 +173,7 @@ function PathsPanel({ detail }: { detail: LibraryDetail }) { {l.libraryRootId ? ( - {rootOf(l.libraryRootId)?.name ?? "库根"} + {rootOf(l.libraryRootId)?.name ?? "媒体库目录"} ) : null}

  • @@ -474,12 +476,12 @@ function MetadataPanel({ detail, entity }: { detail: LibraryDetail; entity: Medi function SlotsPanel({ detail, units }: { detail: LibraryDetail; units: ContentUnit[] }) { if (detail.slots.length === 0) { return ( - + 去订阅 @@ -492,7 +494,7 @@ function SlotsPanel({ detail, units }: { detail: LibraryDetail; units: ContentUn } const assetOf = (id: string | null) => detail.assets.find((a) => a.asset.id === id); return ( - +
    {detail.slots.map((s) => { const cur = assetOf(s.currentAssetId); @@ -593,7 +595,7 @@ function UnitsPanel({ entity, detail }: { entity: ReturnType = { have: "success", @@ -701,7 +703,7 @@ function EpisodesPanel({ detail, units }: { detail: LibraryDetail; units: Conten } > {rows.length > 1 ? ( -
    +
    {rows.map((r, i) => (
    ), }, - { key: "slots", header: "版本目标", cell: (r) => }, + { key: "slots", header: "收录版本", cell: (r) => }, { key: "assets", - header: "资产", + header: "文件", align: "right", hideBelow: "sm", cell: (r) => {r.assetCount}, @@ -214,7 +214,7 @@ function WorksView() { ? { title: "没有符合条件的作品", description: "换个关键字或清除筛选。", icon: LibraryIcon } : { title: "还没有作品", - description: "扫描库根或新建订阅后,作品会出现在这里。", + description: "扫描媒体库目录或新建订阅后,作品会出现在这里。", icon: LibraryIcon, action: ( )}
    @@ -197,13 +197,13 @@ function InventoryPanel({ d }: { d: TmdbMediaDetail }) { const inv = d.inventory; const line = slotLine(inv); return ( - +
    -
    本地账本
    +
    媒体库
    {inv.state === "not_collected" ? ( -

    未收录。订阅或建档后开始追踪。

    +

    不在媒体库。

    ) : ( <>

    @@ -283,7 +283,7 @@ function SeasonsPanel({ d }: { d: TmdbMediaDetail }) { }); const seasons = [...d.seasons].sort((a, b) => (a.seasonNumber || 999) - (b.seasonNumber || 999)); return ( - +

    {seasons.map((s) => ( = { queued: { label: "排队", tone: "neutral" }, running: { label: "执行中", tone: "neutral" }, @@ -160,7 +160,8 @@ export default function OrganizeRecordPage() { }) } > - 让 Agent 排查 + + 让智能体排查 ) : null} diff --git a/apps/web/app/routes/console/sites.$siteId.tsx b/apps/web/app/routes/console/sites.$siteId.tsx index 31dd52e9..4b97ec66 100644 --- a/apps/web/app/routes/console/sites.$siteId.tsx +++ b/apps/web/app/routes/console/sites.$siteId.tsx @@ -49,7 +49,7 @@ const PROMOS = [ ]; const RESOLUTION: { value: ResolutionStatus; label: string }[] = [ { value: "resolved", label: "已识别" }, - { value: "unresolved", label: "待识别" }, + { value: "unresolved", label: "未识别" }, { value: "ambiguous", label: "有歧义" }, { value: "unidentifiable", label: "无法识别" }, ]; @@ -210,7 +210,7 @@ export default function SiteWorkbenchPage() { {health ? 健康 {health.text} : null} {!s.enabled ? 站点已停用,只显示历史数据 : null} {circuit ? {circuit} : null} - {s.snapshotEnabled && snapshotStale(s) ? 快照过期 : null} + {s.snapshotEnabled && snapshotStale(s) ? 抓取已过期 : null} ) : ( "加载中…" @@ -254,7 +254,7 @@ export default function SiteWorkbenchPage() { onClick={() => openAgentDrawer({ label: `站点:${s?.name ?? siteId}`, siteId })} > - 问 Agent + 问智能体 } @@ -277,7 +277,7 @@ export default function SiteWorkbenchPage() { 概览 资源 原站视图 - 快照与 Feed + 抓取与 RSS 账号 @@ -302,26 +302,26 @@ export default function SiteWorkbenchPage() { snap.mutate( { id: siteId }, { - onSuccess: () => toast.success("快照已排队"), + onSuccess: () => toast.success("已开始抓取"), onError: (e) => toast.error(errorMessage(e)), }, ) } > - 快照 + 刷新 } @@ -374,17 +374,15 @@ export default function SiteWorkbenchPage() { ) : ( @@ -437,11 +435,11 @@ export default function SiteWorkbenchPage() {
    {s ? : } setCreatingFeed(true)}> - 新建 Feed + 新建 RSS } > @@ -486,12 +484,12 @@ function CoverageBanner({ notes.push({ tone: "warning", text: coverage.lastSnapshotAt - ? `快照已过期(上次 ${relTime(coverage.lastSnapshotAt)})` - : "没有快照,列表只来自 Feed 和实时补查", + ? `抓取已过期(上次 ${relTime(coverage.lastSnapshotAt)})` + : "还没抓取过,列表只来自 RSS 和站点搜索", }); - if (coverage.liveError) notes.push({ tone: "warning", text: `实时补查失败:${coverage.liveError}` }); + if (coverage.liveError) notes.push({ tone: "warning", text: `去站点搜失败:${coverage.liveError}` }); else if (coverage.live) - notes.push({ tone: "success", text: coverage.coalesced ? "已合并到进行中的同一搜索" : "已实时补查" }); + notes.push({ tone: "success", text: coverage.coalesced ? "已合并到进行中的同一搜索" : "站点搜索完成" }); } if (!notes.length) return null; return ( diff --git a/apps/web/app/routes/console/sites.tsx b/apps/web/app/routes/console/sites.tsx index 3091977d..4ecf92a0 100644 --- a/apps/web/app/routes/console/sites.tsx +++ b/apps/web/app/routes/console/sites.tsx @@ -100,7 +100,7 @@ export default function SitesPage() { <> } /> @@ -108,7 +108,7 @@ export default function SourcesPage() { }} /> - + = { const KIND_LABEL: Record = { api: "API", worker: "Worker(io)", - "worker:agent": "Worker(Agent)", + "worker:agent": "Worker(智能体)", all: "单进程(all)", }; @@ -51,13 +51,13 @@ const KIND_LABEL: Record = { const KIND_NAME: Record = { api: "API", worker: "io worker", - "worker:agent": "Agent worker", + "worker:agent": "智能体 worker", all: "单进程", }; const MOUNT_REASON: Record = { root_missing: "目录不存在", - sentinel_missing: "哨兵文件缺失", + sentinel_missing: "目录不可访问或未挂载", permission_denied: "没有权限", error: "检查出错", timeout: "检查超时", @@ -227,7 +227,7 @@ function Processes({ d }: { d: SystemHealth }) { — ) : ( - io {p.concurrency.io ?? 0} · Agent {p.concurrency.agent ?? 0} + io {p.concurrency.io ?? 0} · 智能体 {p.concurrency.agent ?? 0} ), }, @@ -371,7 +371,7 @@ function History() { function Disks({ d }: { d: SystemHealth }) { const t = d.diskThresholds; - const scope = { data_dir: "数据目录", library_root: "库根", download_dir: "下载目录" } as const; + const scope = { data_dir: "数据目录", library_root: "媒体库目录", download_dir: "下载目录" } as const; return ( x.level))}>

    @@ -428,7 +428,7 @@ function Mounts({ d }: { d: SystemHealth }) { return ( {d.mounts.length === 0 ? ( -

    没有启用的库根或下载目录映射。

    +

    没有启用的媒体库目录或下载目录映射。

    ) : (
      {d.mounts.map((m) => ( @@ -436,7 +436,7 @@ function Mounts({ d }: { d: SystemHealth }) {

      - {m.kind === "library_root" ? "库根" : "下载目录"} + {m.kind === "library_root" ? "媒体库目录" : "下载目录"} {m.name}

      diff --git a/apps/web/app/routes/console/subscriptions.$id.tsx b/apps/web/app/routes/console/subscriptions.$id.tsx index f263ebc9..d941cf23 100644 --- a/apps/web/app/routes/console/subscriptions.$id.tsx +++ b/apps/web/app/routes/console/subscriptions.$id.tsx @@ -39,7 +39,7 @@ import { errorMessage } from "~/lib/api.ts"; import { actorText } from "~/lib/labels.ts"; import { cn, formatDateTime } from "~/lib/utils.ts"; -// 订阅对比视图(FE-5,docs/design/subscriptions.md §2):季页签 → 集 × 版本目标矩阵;版本目标排序与自动补全;订阅信息。 +// 订阅对比视图(FE-5,docs/design/subscriptions.md §2):季页签 → 集 × 收录版本矩阵;收录版本排序与自动补全;订阅信息。 export const meta = () => [{ title: "订阅详情 · UHD 媒体自动化控制台" }]; @@ -118,7 +118,7 @@ export default function SubscriptionDetailPage() { {item?.group ? {GROUP_LABEL[item.group]} : null} {item?.upgrading ? 洗版中 : null} {item?.awaitingNewSeason ? 等待新季 : null} - {!sub.mediaEntityId ? 尚未建档 : null} + {!sub.mediaEntityId ? 不在媒体库 : null}
      {item ? (
      @@ -163,7 +163,7 @@ export default function SubscriptionDetailPage() {
      @@ -172,7 +172,7 @@ export default function SubscriptionDetailPage() { } > {!sub.mediaEntityId ? ( -

      作品建档后才能对比。建档后自动关联到这个订阅。

      +

      作品加入媒体库后才能对比,加入后自动关联到这个订阅。

      ) : covQ.isPending ? ( ) : covQ.isError ? ( @@ -271,7 +271,7 @@ export default function SubscriptionDetailPage() { />
      {sub.mediaEntityId ? ( - + ) : null} diff --git a/apps/web/app/routes/console/subscriptions.tsx b/apps/web/app/routes/console/subscriptions.tsx index 13f7e6ea..62a84810 100644 --- a/apps/web/app/routes/console/subscriptions.tsx +++ b/apps/web/app/routes/console/subscriptions.tsx @@ -393,7 +393,7 @@ export default function SubscriptionsPage() { icon={CalendarClock} title={filtered || v.group || v.kind ? "没有符合条件的订阅" : "还没有订阅"} description={ - filtered || v.group || v.kind ? undefined : "从媒体库、搜索或 Agent 添加,也可以点「新建订阅」。" + filtered || v.group || v.kind ? undefined : "从媒体库、搜索或智能体添加,也可以点「新建订阅」。" } /> ) : grouped ? ( diff --git a/apps/web/app/routes/console/tasks.$id.tsx b/apps/web/app/routes/console/tasks.$id.tsx index ebdb47ca..e2714a4f 100644 --- a/apps/web/app/routes/console/tasks.$id.tsx +++ b/apps/web/app/routes/console/tasks.$id.tsx @@ -59,7 +59,7 @@ const OP_KIND: Record = { delete_source: "删除源文件", delete_target: "删除目标文件", verify: "校验", - register: "登记资产", + register: "登记入库", qb_pause: "暂停下载", qb_resume: "恢复下载", qb_remove: "移除下载", diff --git a/apps/web/app/routes/console/tasks.tsx b/apps/web/app/routes/console/tasks.tsx index f45f9388..12efd5a9 100644 --- a/apps/web/app/routes/console/tasks.tsx +++ b/apps/web/app/routes/console/tasks.tsx @@ -99,7 +99,7 @@ export default function TasksPage() { <> qc.invalidateQueries({ queryKey: qk.all("engine") })}> diff --git a/apps/web/scripts/smoke.mjs b/apps/web/scripts/smoke.mjs index adc3d11f..5cd0e717 100644 --- a/apps/web/scripts/smoke.mjs +++ b/apps/web/scripts/smoke.mjs @@ -219,12 +219,12 @@ async function main() { if (SHOTS) await page.screenshot({ path: join(OUT, `${name}.png`), fullPage: true }).catch(() => {}); results.push({ path, ms: Date.now() - t0, problems: [...new Set(problems)] }); } - // ── 交互:Agent 上游抽屉的模型选择(PI-2;只看来源行与可搜索下拉,不点「获取」,避免拿真实凭证出站)── + // ── 交互:模型服务抽屉的模型选择(PI-2;只看来源行与可搜索下拉,不点「获取」,避免拿真实凭证出站)── { - const label = "/console/settings#agent → 上游抽屉 · 模型选择"; + const label = "/console/settings#agent → 模型服务抽屉 · 模型选择"; const up = await page.request.get(`${BASE}/api/v1/pi/upstreams`); const first = up.ok() ? (await up.json().catch(() => null))?.data?.items?.[0] : null; - if (!first) skipped.push(`${label}(没有上游)`); + if (!first) skipped.push(`${label}(没有模型服务)`); else { problems = []; const t0 = Date.now(); @@ -254,9 +254,9 @@ async function main() { } } - // ── 交互:Agent 抽屉「历史」Tab(AGUI-2;演示数据,点一条在抽屉内打开、返回列表、关了再开仍停在历史)── + // ── 交互:智能体抽屉「历史」Tab(AGUI-2;演示数据,点一条在抽屉内打开、返回列表、关了再开仍停在历史)── { - const label = "Agent 抽屉 → 历史 → 抽屉内打开会话"; + const label = "智能体抽屉 → 历史 → 抽屉内打开会话"; problems = []; const t0 = Date.now(); try { @@ -265,7 +265,7 @@ async function main() { const launcher = page.locator("[data-agent-launcher]:visible").last(); await launcher.click(); await page.getByRole("tab", { name: "历史" }).click(); - const list = page.getByRole("navigation", { name: "Agent 会话列表" }); + const list = page.getByRole("navigation", { name: "智能体会话列表" }); await list.getByRole("button").first().click(); await page.getByRole("button", { name: "返回列表" }).waitFor({ timeout: 10_000 }); if (new URL(page.url()).pathname !== "/console/audit") problems.push("点历史会话跳走了页面"); diff --git a/docs/design/glossary.md b/docs/design/glossary.md index 0b4d3de1..feed20a6 100644 --- a/docs/design/glossary.md +++ b/docs/design/glossary.md @@ -1,6 +1,6 @@ # 术语表与文案规则 -界面、错误信息、Agent 提示词里同一概念只用一个名字。代码标识符(`slot`、`decision`、`feed`)不受限制。依据 REQ v0.7 §3.1。 +界面、错误信息、给模型的提示词里同一概念只用一个名字。代码标识符(`slot`、`decision`、`feed`)不受限制。依据 REQ v0.7 §3.1。 ## 术语 @@ -8,33 +8,38 @@ | --- | --- | --- | | MediaEntity | 作品 | 媒体实体、条目、影片 | | ContentUnit | 季 / 集;统称"内容单元" | 单元、分集单元 | -| Slot | 版本目标 | Slot、收录目标、目标槽。给模型的说明可写一次"版本目标(slot)"对应参数名 | +| Slot | 收录版本(DEAI-2 起;原"版本目标") | Slot、版本目标、收录目标、收录位、目标槽。给模型的说明可写一次"收录版本(slot)"对应参数名 | | AcquisitionPolicy | 收录策略(版本号称"策略版本") | 收录规则、规则集 | | RuleGroup | 规则组;条件叫"关键字"(内置)/"自定义条件";只有一条规则的组在规则页以"偏好"标签显示 | 偏好标签、过滤规则、规则集 | | Subscription | 订阅 | 追剧、关注 | | Site / SiteAccount | 站点 / 站点账号 | PT 源 | -| FeedSource | Feed | RSS 源、订阅源 | -| 站点列表/搜索结果的一次抓取 | 快照 | | +| FeedSource | RSS;新建叫"新建 RSS",地址叫"RSS 地址" | Feed、RSS 源、订阅源、来源健康、RSS 订阅 | +| 站点列表/搜索结果的一次抓取 | 抓取;时间写"上次抓取",按钮"刷新";站点实时搜索叫"去站点搜" | 快照、实时补查 | | SourceRelease | 资源 | 来源资源、站点资源、种子(种子只指 .torrent 文件) | -| 资源按版本目标评估后 | 候选 | | +| 资源按收录版本评估后 | 不单独命名;说"可下载的资源" | 候选(只在代码与给模型的说明里) | | Decision | 待处理事项;入口叫"待处理" | 决策、工单、待决 | | Approval | 审批(名词);按钮用"批准 / 拒绝" | 授权记录 | | DownloadTask | 下载任务 | 下载单 | | Organization record | 整理记录;动作叫"整理",结果状态叫"已入库" | 转移、入库记录 | | 整理方式 | 复制 / 移动 / 硬链接 | Copy / Move | -| Library root | 库根 | 媒体库根、根目录 | +| Library root | 媒体库目录 | 库根、媒体库根、根目录 | | 文件管理可浏览的目录(库根 + 下载器路径映射) | 登记目录 | 登记根、根 | -| MediaAsset / AssetLocation | 资产 / 资产位置(登记进媒体库的文件) | 媒体文件记录 | +| MediaAsset / AssetLocation | 文件 / 已入库文件;位置叫"文件位置" | 资产、资产位置、媒体文件记录 | | ExternalIdentity | 外部身份(TMDB/TVDB/IMDb ID) | 外部 ID 映射、外部标识 | -| 未确认身份的文件/资源 | 待识别 | 未识别(IA 重构时若改名,全站一起改) | +| 未确认身份的文件/资源 | 未识别 | 待识别、待识别池 | +| identity_ambiguous / 整理时认不出作品的待处理事项 | 没识别出作品;主按钮"指定作品" | 身份歧义、识别有歧义、无法确定作品、待确认身份 | +| 作品登记进本地库(不订阅) | 加入媒体库;状态"在媒体库 · 未下载"/"不在媒体库" | 建档、已建档、未收录 | +| 本工具提交并跟踪的下载任务(managed) | 本工具管理;反面叫"外部任务";筛选"来源:本工具 / 其他" | 纳管、未纳管、接管 | +| 媒体库目录里的 `.uhd-root` | 标记文件;按钮"检查目录";失败说"目录不可访问或未挂载" | 哨兵、哨兵文件 | +| 作品 / 版本 / 文件 / 外部身份的本地记录总称 | 不单独命名 | 账本、本地账本 | | Trash | 回收站 | 垃圾桶 | -| Agent | Agent;上游模型配置叫"Agent 上游" | 助手、智能体、AI、Pi 上游、模型上游 | -| PiUpstream | Agent 上游;在 Agent 设置分区和上游页内简称"上游" | Pi 上游、模型上游、Provider 配置 | +| Agent | 智能体(DEAI-2 起;"智能体会话""问智能体""智能体巡检");代码标识符、API 与 MCP 文档里的 agent 不改 | Agent(界面)、助手、AI、Agent 会话、Agent 对话 | +| PiUpstream | 模型服务;默认的一个叫"默认模型服务" | Agent 上游、上游、Pi 上游、模型上游、Provider 配置 | | OAuth 型上游的登录 | 订阅登录 | OAuth 登录、账号授权 | | 密钥、Cookie、密码、令牌的统称 | 凭证 | 凭据、认证信息 | | ScheduledJob | 定时任务 | 计划任务、周期作业、cron 任务 | -| 定时任务的配置页 `/console/schedules` | 计划;两节叫"自动任务"(kind=task)与"Agent 巡检" | 定时任务看板、计划任务 | -| kind=agent_check 的定时任务 | 巡检 | 巡查、Agent 定时任务 | +| 定时任务的配置页 `/console/schedules` | 计划;两节叫"智能体巡检"(置顶)与"自动任务"(kind=task,按分类分组);维护类任务合成一组"数据清理" | 定时任务看板、计划任务 | +| kind=agent_check 的定时任务 | 巡检;全称"智能体巡检" | 巡查、Agent 定时任务 | | 定时任务每次执行的记录 | 运行记录 | 历史、执行历史 | | AgentRun(parent_run_id 非空) | 子会话;派生它的叫"父会话" | 子代理、子 Agent、subagent | | AgentRun 被中止 | 停止;状态"已停止" | 中止、取消 | @@ -44,7 +49,7 @@ | agent.propose_options | 方案;动作叫"选定",都不要叫"退回" | 选项、提案 | | scope.proposeFirst | 先看方案再执行 | 只要方案、只给方案、plan_only 模式 | | 工作流引擎的一次执行 | 运行 | run | -| 正在写心跳的进程(process_heartbeat 当前行) | 当前实例;横幅按角色称呼:API / io worker / Agent worker / 单进程 | 活进程、在线实例 | +| 正在写心跳的进程(process_heartbeat 当前行) | 当前实例;横幅按角色称呼:API / io worker / 智能体 worker / 单进程 | 活进程、在线实例 | | 已退出、容器已重建或被同容器新进程替换的心跳行 | 已下线实例;正常退出的标"已退出" | 历史实例、僵尸进程、死进程 | ## 文案规则 @@ -61,6 +66,8 @@ 10. 标题、按钮、标签、Toast 末尾不加句号。 11. 订阅登录的条款提示只陈述事实:借用哪个官方客户端身份、可能违反条款、账号可能被限流或封禁;不劝阻,不美化。 12. 给模型看的文字(巡检目标、调查提示)同样适用:具体、短,用界面同一套术语。 +13. ID、hash、trace、token 数、耗时、模型 ID、原始字段、内部状态码和错误码不进默认视图,放进「技术信息」折叠区(`TechInfo` 组件,设置 → 界面可改为默认展开)。错误码先映射成一句人话 + 下一步动作(`lib/reasons.ts`),原码留在技术信息里。 +14. 只列非零项:0 计数、全部正常的状态不铺开,正常态不加徽章。 ## 标点 @@ -69,7 +76,7 @@ 1. 中文句子用全角标点:`,。;:?!、`。半角 `, ; : ? !` 只出现在代码、路径、URL、键值、时间(`12:30`)和整句英文里。 2. 全角标点前后不加空格;不叠用标点(不写 `,。`、`!!`、`?!`)。 3. 括号里有中文用全角 `()`,紧贴前后文字;括号里只有英文、数字、代码时用半角 `()`,前面是英文时空一格:`Oppenheimer (2023)`、`读取失败(ENOTCONN)`。 -4. 中文与英文、数字之间空一个半角空格:`3 个作品`、`打开 Agent`、`TMDB 不可用`;行首行尾和全角标点旁不加。 +4. 中文与英文、数字之间空一个半角空格:`3 个作品`、`打开 TMDB`、`TMDB 不可用`;行首行尾和全角标点旁不加。 5. 数字与单位:`%`、`°` 紧贴数字(`80%`);英文单位空一格,与 `formatBytes` 输出一致(`1.1 TB`、`15 s`);数字和字母组成的记号连写(`1080p`、`S01E02`、`H.265`、`4K`)。 6. 省略号只用一个 `…`(U+2026),不用 `...` 或 `……`;进行中状态写 `加载中…`,菜单项"需要再选一步"写 `移动到…`。 7. 标题、按钮、标签、表头、列表项、Toast 末尾不加句号(同文案规则 10)。 diff --git a/packages/agent/src/pi/model.ts b/packages/agent/src/pi/model.ts index 76c0aae0..817f5b45 100644 --- a/packages/agent/src/pi/model.ts +++ b/packages/agent/src/pi/model.ts @@ -23,8 +23,8 @@ const PROVIDER_ID = { anthropic: "umt-anthropic", "openai-compatible": "umt-open */ export function resolveModelFromSettings(settings: ResolvedAgentSettings): ResolvedPiModel { if (settings.provider !== "anthropic" && settings.provider !== "openai-compatible") - throw new Error(`provider ${settings.provider} 需要按上游装配(PiAgentRuntimeOptions.upstream)`); - if (!settings.apiKey) throw new Error("未配置上游密钥"); + throw new Error(`provider ${settings.provider} 需要按模型服务装配(PiAgentRuntimeOptions.upstream)`); + if (!settings.apiKey) throw new Error("未配置模型服务密钥"); const providerId = PROVIDER_ID[settings.provider]; const common = { id: settings.model, @@ -49,7 +49,7 @@ export function resolveModelFromSettings(settings: ResolvedAgentSettings): Resol models.setProvider(createProvider({ id: providerId, auth, models: [m], api: anthropicMessagesApi() })); model = m; } else { - if (!settings.baseUrl) throw new Error("openai-compatible 上游必须配置 baseUrl(带 /v1)"); + if (!settings.baseUrl) throw new Error("openai-compatible 模型服务必须配置 baseUrl(带 /v1)"); const m: Model<"openai-completions"> = { ...common, api: "openai-completions", diff --git a/packages/agent/src/pi/runtime.ts b/packages/agent/src/pi/runtime.ts index 06c2eb25..2343264d 100644 --- a/packages/agent/src/pi/runtime.ts +++ b/packages/agent/src/pi/runtime.ts @@ -400,7 +400,7 @@ class PiAgentRun implements AgentRunHandle { const err = record && "error" in record ? record.error : undefined; outcome = this.failed("error", redactString(err ? `${err.code}: ${err.message}` : "运行失败")); } else if (status === "suspended") { - outcome = this.failed("error", "上游进入延迟模式(deferred),v0.1 不支持"); + outcome = this.failed("error", "模型服务进入延迟模式(deferred),v0.1 不支持"); } else { outcome = { status: "completed", finalText: this.finalText, usage: this.usage }; this.emit({ type: "run_completed", finalText: outcome.finalText, usage: this.usage }); @@ -540,7 +540,7 @@ export function toolContent( if (!vision) return [ { type: "text", text }, - { type: "text", text: "(截图未发送:当前上游模型不支持图片输入;用 browser.snapshot 读取页面文字)" }, + { type: "text", text: "(截图未发送:当前模型不支持图片输入;用 browser.snapshot 读取页面文字)" }, ]; return [ { type: "text", text }, diff --git a/packages/agent/src/pi/upstream.ts b/packages/agent/src/pi/upstream.ts index b60771b9..0bc014da 100644 --- a/packages/agent/src/pi/upstream.ts +++ b/packages/agent/src/pi/upstream.ts @@ -329,7 +329,7 @@ export class PiUpstreamClient { private pickModel(modelId: string | null | undefined): { model: Model; inCatalog: boolean } { const all = this.models.getModels(this.providerId); const id = modelId ?? this.spec.model ?? all[0]?.id; - if (!id) throw new UpstreamError("model", "该上游没有可用模型,请填写模型 id"); + if (!id) throw new UpstreamError("model", "该模型服务没有可用模型,请填写模型 id"); const found = this.models.getModel(this.providerId, id); if (found) return { model: found, inCatalog: true }; const template = all[0]; @@ -369,7 +369,7 @@ export class PiUpstreamClient { throw classifyUpstreamError(err); } if (res.stopReason === "error" || res.stopReason === "aborted") - throw classifyUpstreamError(new Error(res.errorMessage ?? "上游返回错误")); + throw classifyUpstreamError(new Error(res.errorMessage ?? "模型服务返回错误")); } return { source, model: model.id, inCatalog }; } diff --git a/packages/contracts/src/api.ts b/packages/contracts/src/api.ts index a4db168d..478f87b8 100644 --- a/packages/contracts/src/api.ts +++ b/packages/contracts/src/api.ts @@ -718,7 +718,7 @@ export const Api = { path: "/subscriptions/{id}/coverage", scope: "read", summary: - "覆盖矩阵:季 → 集 × 版本目标(电影:内容版本 × 版本目标),每格 have/inflight/missing/skipped;缓存 30s,refresh=true 强制重算", + "覆盖矩阵:季 → 集 × 收录版本(电影:内容版本 × 收录版本),每格 have/inflight/missing/skipped;缓存 30s,refresh=true 强制重算", params: IdParam, query: z.object({ refresh: z.enum(["true", "false"]).optional() }), response: policy.SubscriptionCoverage, @@ -759,7 +759,7 @@ export const Api = { method: "post", path: "/subscriptions/{id}/slots/{slotId}/fill", scope: "admin", - summary: "手动补全某版本目标(不受自动补全开关与补全预算限制)", + summary: "手动补全某收录版本(不受自动补全开关与补全预算限制)", params: z.object({ id: Id, slotId: Id }), body: policy.SlotFillRequest, response: z.object({ runId: z.string(), deduplicated: z.boolean() }), @@ -768,7 +768,7 @@ export const Api = { method: "put", path: "/subscriptions/{id}/slot-order", scope: "admin", - summary: "版本目标排序(第一个为主版本)", + summary: "收录版本排序(第一个为主版本)", params: IdParam, body: policy.SlotOrderSet, response: z.array(policy.AcquisitionSlot), @@ -785,7 +785,7 @@ export const Api = { method: "get", path: "/subscriptions/{id}/progress", scope: "read", - summary: "订阅进度:季总集数账本、每集状态(已覆盖/在途/缺失/疑似缺集)、版本目标在途与延后", + summary: "订阅进度:季总集数账本、每集状态(已覆盖/在途/缺失/疑似缺集)、收录版本在途与延后", params: IdParam, response: policy.SubscriptionProgress, }), @@ -848,7 +848,7 @@ export const Api = { }, acquisitionSlots: crud( "/acquisition-slots", - "版本目标", + "收录版本", { row: policy.AcquisitionSlot, create: policy.AcquisitionSlotCreate, @@ -1226,7 +1226,7 @@ export const Api = { method: "get", path: "/agent/runs", scope: "read", - summary: "Agent 运行列表(新→旧)", + summary: "智能体运行列表(新→旧)", query: agent.AgentRunListQuery, response: cursorPage(agent.AgentRun), }), @@ -1234,7 +1234,7 @@ export const Api = { method: "get", path: "/agent/runs/{id}", scope: "read", - summary: "Agent 运行详情:事件按 seq 分页、追加消息、服务端核查的目标汇报", + summary: "智能体运行详情:事件按 seq 分页、追加消息、服务端核查的目标汇报", params: IdParam, query: agent.AgentRunDetailQuery, response: agent.AgentRunDetail, @@ -1616,7 +1616,7 @@ export const Api = { method: "get", path: "/capabilities/stats", scope: "read", - summary: "能力调用统计:每能力调用数、失败数、平均耗时、最近调用、调用来源(内置 Agent / MCP / REST)", + summary: "能力调用统计:每能力调用数、失败数、平均耗时、最近调用、调用来源(内置智能体 / MCP / REST)", query: mcp.CapabilityStatsQuery, response: mcp.CapabilityStatsResponse, }), @@ -1774,14 +1774,14 @@ export const Api = { method: "get", path: "/pi/upstreams", scope: "admin", - summary: "Agent 上游列表(不含密钥)", + summary: "模型服务列表(不含密钥)", response: pi.PiUpstreamList, }), get: ep({ method: "get", path: "/pi/upstreams/{id}", scope: "admin", - summary: "Agent 上游详情", + summary: "模型服务详情", params: IdParam, response: pi.PiUpstream, }), @@ -1790,7 +1790,7 @@ export const Api = { path: "/pi/upstreams", scope: "admin", auth: "session", - summary: "新建 Agent 上游(apiKey 只写,存 secret 表)", + summary: "新建模型服务(apiKey 只写,存 secret 表)", body: pi.PiUpstreamCreate, response: pi.PiUpstream, }), @@ -1799,7 +1799,7 @@ export const Api = { path: "/pi/upstreams/{id}", scope: "admin", auth: "session", - summary: "修改 Agent 上游(provider 不可改;makeDefault 设默认上游)", + summary: "修改模型服务(provider 不可改;makeDefault 设默认模型服务)", params: IdParam, body: pi.PiUpstreamUpdate, response: pi.PiUpstream, @@ -1809,7 +1809,7 @@ export const Api = { path: "/pi/upstreams/{id}", scope: "admin", auth: "session", - summary: "删除 Agent 上游(连同凭据;是默认上游时清空默认)", + summary: "删除模型服务(连同凭据;是默认模型服务时清空默认)", params: IdParam, response: OkResult, }), @@ -1818,7 +1818,7 @@ export const Api = { path: "/pi/upstreams/{id}/test", scope: "admin", auth: "session", - summary: "测试上游:默认只解析凭据与校验模型(不产生费用);mode=completion 发一次最小补全", + summary: "测试模型服务:默认只解析凭据与校验模型(不产生费用);mode=completion 发一次最小补全", params: IdParam, body: pi.PiUpstreamTestRequest, response: pi.PiUpstreamTestResult, @@ -1827,7 +1827,7 @@ export const Api = { method: "get", path: "/pi/upstreams/{id}/models", scope: "admin", - summary: "上游模型列表:已获取过远程列表时返回远程与静态目录的合并(远程优先),否则静态目录", + summary: "模型列表:已获取过远程列表时返回远程与静态目录的合并(远程优先),否则静态目录", params: IdParam, response: pi.PiUpstreamModels, }), @@ -1836,7 +1836,7 @@ export const Api = { path: "/pi/upstreams/{id}/models/refresh", scope: "admin", auth: "session", - summary: "获取模型列表:用该上游的凭证调 provider 的模型列表接口并存库;失败或不支持时回静态目录并给出原因", + summary: "获取模型列表:用该模型服务的凭证调 provider 的模型列表接口并存库;失败或不支持时回静态目录并给出原因", params: IdParam, response: pi.PiUpstreamModels, }), @@ -1845,7 +1845,7 @@ export const Api = { path: "/pi/models/probe", scope: "admin", auth: "session", - summary: "新建上游前试拉模型列表(凭证只用于这一次请求,不落库)", + summary: "新建模型服务前试拉模型列表(凭证只用于这一次请求,不落库)", body: pi.PiModelsProbeRequest, response: pi.PiUpstreamModels, }), @@ -2111,7 +2111,7 @@ export const Api = { method: "get", path: "/identity-pool", scope: "read", - summary: "待识别池(可按原始标题检索;候选不建正式作品)", + summary: "未识别资源(可按原始标题检索;候选不建正式作品)", query: identity.IdentityPoolQuery, response: cursorPage(identity.IdentityPoolItem), }), @@ -2119,7 +2119,7 @@ export const Api = { method: "post", path: "/identity-pool/{id}/assign", scope: "organize.execute", - summary: "为待识别资源手动指定候选 / 本地作品 / 外部身份", + summary: "为未识别资源手动指定候选 / 本地作品 / 外部身份", params: IdParam, body: identity.ReleaseIdentityAssign, response: ReleaseIdentityResult, @@ -2128,7 +2128,7 @@ export const Api = { method: "post", path: "/identity-pool/{id}/dismiss", scope: "organize.execute", - summary: "忽略待识别资源(不再自动复查)", + summary: "忽略未识别资源(不再自动复查)", params: IdParam, body: z.object({ note: z.string().max(2000).optional() }), response: identity.IdentityCandidate, @@ -2156,7 +2156,7 @@ export const Api = { path: "/identity-pool/{id}/enrich/apply", scope: "admin", auth: "session", - summary: "应用元数据补全建议:建本地临时作品(或并入同名作品)并关联待识别资源(只接受控制台管理员会话)", + summary: "应用元数据补全建议:建本地临时作品(或并入同名作品)并关联未识别资源(只接受控制台管理员会话)", params: IdParam, body: identity.EnrichApplyInput, response: identity.EnrichApplyResult, @@ -2177,7 +2177,7 @@ export const Api = { method: "get", path: "/library/{id}", scope: "read", - summary: "作品详情:实际路径、身份映射、元数据来源、各版本目标、候选拒绝理由、下载历史与校验结果", + summary: "作品详情:实际路径、身份映射、元数据来源、各收录版本、候选拒绝理由、下载历史与校验结果", params: IdParam, response: organization.LibraryDetail, }), @@ -2195,7 +2195,7 @@ export const Api = { method: "get", path: "/inventory", scope: "read", - summary: "库存叠加(站点工作台):按作品返回已有版本 / 版本目标缺口 / 下载中", + summary: "库存叠加(站点工作台):按作品返回已有版本 / 收录版本缺口 / 下载中", query: organization.InventoryQuery, response: organization.InventoryResponse, }), @@ -2472,7 +2472,7 @@ export const Api = { method: "post", path: "/browser/sessions/{id}/control/release", scope: "admin", - summary: "交还控制权:Agent 恢复前必须重新读取页面,旧元素引用失效", + summary: "交还控制权:智能体恢复前必须重新读取页面,旧元素引用失效", params: browser.BrowserSessionParams, body: browser.BrowserControlRelease, response: browser.BrowserControlResult, @@ -2486,7 +2486,7 @@ export const Api = { path: "/schedules", scope: "admin", auth: "session", - summary: "定时任务看板:全部登记的周期任务与 Agent 巡检、统计、调度器是否在线", + summary: "定时任务看板:全部登记的周期任务与智能体巡检、统计、调度器是否在线", response: schedules.ScheduleBoard, }), create: ep({ @@ -2494,7 +2494,7 @@ export const Api = { path: "/schedules", scope: "admin", auth: "session", - summary: "新建自定义 Agent 巡检(目标模板 + 能力白名单 + 预算 + 间隔)", + summary: "新建自定义智能体巡检(目标模板 + 能力白名单 + 预算 + 间隔)", body: schedules.AgentCheckCreate, response: schedules.ScheduledJob, }), @@ -2513,7 +2513,7 @@ export const Api = { path: "/schedules/{id}", scope: "admin", auth: "session", - summary: "删除自定义 Agent 巡检(内置任务不可删)", + summary: "删除自定义智能体巡检(内置任务不可删)", params: schedules.ScheduleIdParam, response: OkResult, }), diff --git a/packages/contracts/src/capabilities-io.ts b/packages/contracts/src/capabilities-io.ts index 263a3c03..28aad0e8 100644 --- a/packages/contracts/src/capabilities-io.ts +++ b/packages/contracts/src/capabilities-io.ts @@ -256,7 +256,7 @@ export const SubscriptionBrief = z.object({ status: SubscriptionStatus, priority: AcquisitionPriority, statusReason: z.string().nullable(), - /** 版本目标:总数 / 已满足数 */ + /** 收录版本:总数 / 已满足数 */ slots: z.object({ total: NonNegInt, satisfied: NonNegInt }), lastSearchedAt: z.string().nullable(), nextSearchAt: z.string().nullable(), diff --git a/packages/contracts/src/capability.ts b/packages/contracts/src/capability.ts index 304123be..744e46b7 100644 --- a/packages/contracts/src/capability.ts +++ b/packages/contracts/src/capability.ts @@ -133,7 +133,7 @@ export const CAPABILITY_CATALOG: Record "catalog.lookup": d( "catalog.lookup", "查作品", - "按标题/别名/外部 ID 查本地作品与元数据候选。确认作品身份、拿作品 ID 时用;查库里已有文件与版本目标用 inventory.query", + "按标题/别名/外部 ID 查本地作品与元数据候选。确认作品身份、拿作品 ID 时用;查库里已有文件与收录版本用 inventory.query", "read", "CatalogLookup", "read", @@ -177,7 +177,7 @@ export const CAPABILITY_CATALOG: Record "download.plan": d( "download.plan", "生成下载计划", - "评估候选,生成占用版本目标(slot)与预算预留的下载计划;不提交。选定资源后、提交前用;不会提交下载,也不预留版本目标", + "评估候选,生成占用收录版本(slot)与预算预留的下载计划;不提交。选定资源后、提交前用;不会提交下载,也不预留收录版本", "preview", "DownloadPlan", "organize.execute", @@ -199,7 +199,7 @@ export const CAPABILITY_CATALOG: Record "download.recheck": d( "download.recheck", "重新校验/恢复下载", - "对已纳管的下载任务强制重新校验(action=recheck),或恢复非阈值、非人工的暂停(action=resume);不换源、不删除、不改保存路径。下载失败或文件缺失时用;换源、删除不在此能力内", + "对本工具管理的下载任务强制重新校验(action=recheck),或恢复非阈值、非人工的暂停(action=resume);不换源、不删除、不改保存路径。下载失败或文件缺失时用;换源、删除不在此能力内", "execute", "DownloadRecheck", "organize.execute", @@ -287,7 +287,7 @@ export const CAPABILITY_CATALOG: Record "inventory.query": d( "inventory.query", "查库存", - "查询本地资产、版本目标满足情况与外部库存证据。查某个作品有哪些文件与版本目标时用;想盘点全部就不给 query,不要用单字母探查", + "查询本地资产、收录版本满足情况与外部库存证据。查某个作品有哪些文件与收录版本时用;想盘点全部就不给 query,不要用单字母探查", "read", "InventoryQuery", "read", @@ -299,7 +299,7 @@ export const CAPABILITY_CATALOG: Record "subscription.list": d( "subscription.list", "列出订阅", - "按状态或媒体类型分组列出订阅摘要(作品、模式、状态、版本目标满足数、上次/下次搜索)。盘点订阅、找缺版本时用;查单个作品的文件与版本目标用 inventory.query", + "按状态或媒体类型分组列出订阅摘要(作品、模式、状态、收录版本满足数、上次/下次搜索)。盘点订阅、找缺版本时用;查单个作品的文件与收录版本用 inventory.query", "read", "SubscriptionList", "read", diff --git a/packages/contracts/src/organization.ts b/packages/contracts/src/organization.ts index 4b1e043b..7731806f 100644 --- a/packages/contracts/src/organization.ts +++ b/packages/contracts/src/organization.ts @@ -489,7 +489,7 @@ export const Dashboard = z.object({ }), activeSubscriptions: NonNegInt, slotCounts: SlotStatusCounts, - /** 版本目标满足率 = satisfied / 全部版本目标(0–1);没有版本目标时为 null */ + /** 收录版本满足率 = satisfied / 全部收录版本(0–1);没有收录版本时为 null */ slotSatisfiedRate: z.number().min(0).max(1).nullable(), /** 本地账本计数:作品不含已合并;季 = content_unit.kind=season(不含 retired);资产 = active 的视频 / 原盘目录 */ library: z.object({ diff --git a/packages/contracts/src/pi.ts b/packages/contracts/src/pi.ts index 67d417f1..90af2088 100644 --- a/packages/contracts/src/pi.ts +++ b/packages/contracts/src/pi.ts @@ -245,9 +245,9 @@ export const PiUpstreamCreate = z }) .superRefine((v, ctx) => { if (v.authKind !== "api_key" && v.apiKey !== undefined) - ctx.addIssue({ code: "custom", path: ["apiKey"], message: "只有 api_key 型上游可以填 apiKey" }); + ctx.addIssue({ code: "custom", path: ["apiKey"], message: "只有 api_key 型模型服务可以填 apiKey" }); if (v.authKind === "oauth" && v.baseUrl) - ctx.addIssue({ code: "custom", path: ["baseUrl"], message: "订阅登录型上游不能覆盖 baseUrl" }); + ctx.addIssue({ code: "custom", path: ["baseUrl"], message: "订阅登录型模型服务不能覆盖 baseUrl" }); if (v.provider === "openai-compatible") { if (!v.baseUrl) ctx.addIssue({ code: "custom", path: ["baseUrl"], message: "自定义兼容端点必须填 baseUrl" }); if (!v.defaultModel) diff --git a/packages/contracts/src/policy.ts b/packages/contracts/src/policy.ts index 4738cdd0..2d213a61 100644 --- a/packages/contracts/src/policy.ts +++ b/packages/contracts/src/policy.ts @@ -79,7 +79,7 @@ export const Subscription = EntityBase.extend({ /** 上次主动搜索时间 / 下次到期(按 params.searchIntervalHours,缺省 24h) */ lastSearchedAt: Timestamp.nullable(), nextSearchAt: Timestamp.nullable(), - /** 上次进展:任一版本目标的单元转为已入库、或总集数变化时写入;总览"上次更新"另合并计划预留时间 */ + /** 上次进展:任一收录版本的单元转为已入库、或总集数变化时写入;总览"上次更新"另合并计划预留时间 */ lastProgressAt: Timestamp.nullable(), /** 派生字段(列表返回):作品标题,未建档时为 null */ mediaEntityTitle: z.string().nullable().optional(), @@ -97,10 +97,10 @@ export const FillBudget = z .strict(); export type FillBudget = z.infer; -/** MP 分辨率(resolution)→ 版本目标硬条件 resolutions */ +/** MP 分辨率(resolution)→ 收录版本硬条件 resolutions */ export const SubscriptionResolution = z.enum(["4k", "1080p", "720p"]); export type SubscriptionResolution = z.infer; -/** MP 资源质量(quality / resource_type)→ 版本目标硬条件 sources / codecs;uhd 写进订阅私有规则组的包含 */ +/** MP 资源质量(quality / resource_type)→ 收录版本硬条件 sources / codecs;uhd 写进订阅私有规则组的包含 */ export const SubscriptionSourcePreset = z.enum([ "bluray_disc", "remux", @@ -112,7 +112,7 @@ export const SubscriptionSourcePreset = z.enum([ "h264", ]); export type SubscriptionSourcePreset = z.infer; -/** MP 特效(effect)→ 版本目标硬条件 hdr + 软偏好 hdr 顺序;atmos 写进订阅私有规则组的包含 */ +/** MP 特效(effect)→ 收录版本硬条件 hdr + 软偏好 hdr 顺序;atmos 写进订阅私有规则组的包含 */ export const SubscriptionEffect = z.enum(["dv", "hdr", "sdr", "atmos"]); export type SubscriptionEffect = z.infer; @@ -120,7 +120,7 @@ export type SubscriptionEffect = z.infer; * subscription.params 的已知键(JSON 列,其余键原样保留): * siteIds 站点范围;episodeRange 集范围(对照 MP start/total_episode);searchIntervalHours 主动搜索周期(0 = 关); * stopAfter 截止时间;downloaderInstanceId 指定下载器;ruleGroupIds 规则组;keywords 追加检索词; - * include/exclude 订阅级关键词正则(作用于全部版本目标);libraryRootId 库根;downloadSavePath 下载保存路径; + * include/exclude 订阅级关键词正则(作用于全部收录版本);libraryRootId 库根;downloadSavePath 下载保存路径; * autoFill/fillBudget 多版本自动补全;resolution/source/effect 质量预设(MP resolution/quality/effect)。 * include/exclude 保存时同步为订阅私有规则组(scope.subscriptionId,随订阅删除),评估只读规则组。 * 写入时兼容 MP 字段与旧键(sites/downloader/save_path/start_episode/filter_groups/keyword/autoFillVersions…), @@ -150,7 +150,7 @@ export const SubscriptionParams = z downloadSavePath: z.string().min(1).max(1000).optional(), autoFill: AutoFillMode.optional(), fillBudget: FillBudget.optional(), - /** MP resolution / quality / effect:保存时写到该订阅自己的版本目标(硬条件 + 软偏好) */ + /** MP resolution / quality / effect:保存时写到该订阅自己的收录版本(硬条件 + 软偏好) */ resolution: SubscriptionResolution.optional(), source: SubscriptionSourcePreset.optional(), effect: SubscriptionEffect.optional(), @@ -296,11 +296,11 @@ export type SubscriptionGroup = z.infer; export const SubscriptionSort = z.enum(["lastUpdated", "nextSearch", "missing", "created", "name"]); export type SubscriptionSort = z.infer; -/** 版本目标在订阅范围内的计数摘要(卡片徽标 / 矩阵行尾) */ +/** 收录版本在订阅范围内的计数摘要(卡片徽标 / 矩阵行尾) */ export const SubscriptionSlotSummary = z.object({ slotId: Id, name: z.string(), - /** 同订阅内顺序最小的版本目标 */ + /** 同订阅内顺序最小的收录版本 */ primary: z.boolean(), status: z.string(), have: z.number().int(), @@ -347,7 +347,7 @@ export const SubscriptionOverviewItem = Subscription.extend({ source: z.enum(["subscription", "rule_group", "default"]), budget: FillBudget, }), - /** 任一版本目标开了升级且未达顶档 */ + /** 任一收录版本开了升级且未达顶档 */ upgrading: z.boolean(), /** 已完成分组里 continuous 订阅在等下一季 */ awaitingNewSeason: z.boolean(), @@ -418,7 +418,7 @@ export const SubscriptionCoverage = z.object({ columns: z.array(CoverageColumn), }), ), - /** 首行 slotId = null 为"库内任一版本";其后每个版本目标一行,按 position */ + /** 首行 slotId = null 为"库内任一版本";其后每个收录版本一行,按 position */ rows: z.array( z.object({ slotId: Id.nullable(), @@ -714,7 +714,7 @@ export type PreferenceSuppressionUpdate = z.infer; /** * 库存叠加状态(只在关联到作品时给出): - * downloading 有下载中任务 > gap 有未满足的版本目标 > owned 已有版本 > subscribed 只有订阅 > none 作品已登记但无库存/目标 + * downloading 有下载中任务 > gap 有未满足的收录版本 > owned 已有版本 > subscribed 只有订阅 > none 作品已登记但无库存/目标 */ export const SearchInventoryState = z.enum(["downloading", "gap", "owned", "subscribed", "none"]); export type SearchInventoryState = z.infer; @@ -188,10 +188,10 @@ export const WorkInventory = z.object({ /** 已入库版本数(主视频资产) */ versionCount: NonNegInt, downloadingCount: NonNegInt, - /** 版本目标:总数 / 已满足 */ + /** 收录版本:总数 / 已满足 */ slotTotal: NonNegInt, slotSatisfied: NonNegInt, - /** 未满足的版本目标(empty / attention / upgrade_pending) */ + /** 未满足的收录版本(empty / attention / upgrade_pending) */ missing: z.array(z.object({ slotId: Id, name: z.string() })), }); export type WorkInventory = z.infer; diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 173e034f..2cea6f33 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -864,10 +864,10 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [ { id: "library", label: "媒体库与目录", - description: "库根、哨兵状态、默认整理方式、命名模板", + description: "媒体库目录、默认整理方式、命名模板", legacyTabs: [{ tab: "library-roots", anchor: "roots" }], anchors: [ - { id: "roots", label: "库根" }, + { id: "roots", label: "媒体库目录" }, { id: "organize", label: "整理方式" }, { id: "naming", label: "命名模板" }, ], @@ -918,11 +918,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [ }, { id: "agent", - label: "Agent", - description: "Agent 上游、运行预算、自动调查、网页搜索", + label: "智能体", + description: "模型服务、运行预算、自动调查、网页搜索", legacyTabs: [{ tab: "pi" }], anchors: [ - { id: "upstream", label: "上游" }, + { id: "upstream", label: "模型服务" }, { id: "runtime", label: "上下文与子会话" }, { id: "budget", label: "预算" }, { id: "investigate", label: "自动调查" }, diff --git a/packages/domain/src/acquisition/fill.ts b/packages/domain/src/acquisition/fill.ts index f7522185..cc2d0330 100644 --- a/packages/domain/src/acquisition/fill.ts +++ b/packages/domain/src/acquisition/fill.ts @@ -1,4 +1,4 @@ -// 多版本自动补全(docs/design/subscriptions.md §2.2):非主版本目标在主版本已满足的单元上补下其他版本。 +// 多版本自动补全(docs/design/subscriptions.md §2.2):非主收录版本在主版本已满足的单元上补下其他版本。 // 开关按 订阅 → 规则组(链上后者优先)→ 系统缺省(开)取值;补全预算按订阅、按自然日计,超出记延后。 export type AutoFillModeValue = "inherit" | "on" | "off"; diff --git a/packages/domain/src/naming/variables.ts b/packages/domain/src/naming/variables.ts index ddba12e9..4e2348c5 100644 --- a/packages/domain/src/naming/variables.ts +++ b/packages/domain/src/naming/variables.ts @@ -76,7 +76,7 @@ export const NAMING_VARIABLES: readonly NamingVariableDef[] = [ ours: true, }, { name: "issue", group: "ours", label: "综艺期号", example: "12", ours: true }, - { name: "slotName", group: "ours", label: "版本目标名", example: "4K HDR", ours: true }, + { name: "slotName", group: "ours", label: "收录版本名", example: "4K HDR", ours: true }, { name: "libraryRoot", group: "ours", label: "库根名", example: "电视剧-A", ours: true }, ]; @@ -124,7 +124,7 @@ function sourceOf(p: Partial): string | undefined { } /** - * 渲染上下文(MP 变量名):作品信息 + 发布解析 + 季集元数据 + 版本目标/库根。 + * 渲染上下文(MP 变量名):作品信息 + 发布解析 + 季集元数据 + 收录版本/库根。 * 标题用本地作品名,不用发布标题。空值不放进上下文(同 MP 剔除 None)。 */ export function buildNamingVars( diff --git a/packages/domain/src/slot/evaluate.ts b/packages/domain/src/slot/evaluate.ts index 1a7fe432..3183dfd3 100644 --- a/packages/domain/src/slot/evaluate.ts +++ b/packages/domain/src/slot/evaluate.ts @@ -376,14 +376,14 @@ export function currentPri(spec: SlotSpec, current: QualityFacts): number { return effectiveLadder(spec, pseudo, {}, ruleChainOf(spec, pseudo, {})).pri; } -/** 版本目标引用的规则组链求值;没有规则组时为 null */ +/** 收录版本引用的规则组链求值;没有规则组时为 null */ export function ruleChainOf(spec: SlotSpec, r: ParsedRelease, ctx: EvaluationContext = {}): RuleChainResult | null { if (!spec.rules?.groups.length) return null; return evaluateRuleChain(spec.rules.groups, r, ctx, catalogOf(spec.rules.atoms)); } /** - * 实际生效的阶梯:版本目标自身 soft.tiers 视为排在最前的匿名组,规则组链里有带层的组时 pri 取最后一个带层的组。 + * 实际生效的阶梯:收录版本自身 soft.tiers 视为排在最前的匿名组,规则组链里有带层的组时 pri 取最后一个带层的组。 */ export function effectiveLadder( spec: SlotSpec, diff --git a/packages/domain/src/slot/overview.ts b/packages/domain/src/slot/overview.ts index 0699acde..28936971 100644 --- a/packages/domain/src/slot/overview.ts +++ b/packages/domain/src/slot/overview.ts @@ -11,9 +11,9 @@ export interface SubscriptionGroupInput { mode: "once" | "continuous"; /** 范围内各季总集数是否都已知(电影恒为 true) */ totalsKnown: boolean; - /** 任一版本目标有已入库或在途单元 */ + /** 任一收录版本有已入库或在途单元 */ anyProgress: boolean; - /** 所有版本目标在范围内全覆盖(且总集数已知) */ + /** 所有收录版本在范围内全覆盖(且总集数已知) */ allSlotsComplete: boolean; } @@ -86,7 +86,7 @@ export interface CoverageRowResult { } /** - * 一行(一个版本目标或"库内任一版本")的格状态:已入库 > 在途(含整季通配预留)> 范围外/未播出 > 缺失。 + * 一行(一个收录版本或"库内任一版本")的格状态:已入库 > 在途(含整季通配预留)> 范围外/未播出 > 缺失。 * 在途按计划单元键与列键求交(unitsOverlap:s{季}* 通配与同季各集相交)。 */ export function coverageRow(i: CoverageRowInput): CoverageRowResult { diff --git a/packages/domain/src/slot/types.ts b/packages/domain/src/slot/types.ts index c37c2e05..515beaa4 100644 --- a/packages/domain/src/slot/types.ts +++ b/packages/domain/src/slot/types.ts @@ -116,7 +116,7 @@ export interface SlotUpgradePolicy { export type SlotCompletionSpec = { kind: "movie"; cuts?: (Cut | "default")[] } | { kind: "units" }; -/** 版本目标引用的规则组链(已按 scope 过滤、按引用顺序排列)与自定义条件 */ +/** 收录版本引用的规则组链(已按 scope 过滤、按引用顺序排列)与自定义条件 */ export interface SlotRuleChain { groups: RuleGroupSpec[]; atoms?: CustomAtom[]; diff --git a/packages/domain/test/naming.test.ts b/packages/domain/test/naming.test.ts index cf333f11..0ca4b8ef 100644 --- a/packages/domain/test/naming.test.ts +++ b/packages/domain/test/naming.test.ts @@ -125,7 +125,7 @@ describe("变量表与上下文", () => { "libraryRoot", ]); }); - it("季集元数据、版本目标、库根、外部 ID", () => { + it("季集元数据、收录版本、库根、外部 ID", () => { const v = buildNamingVars( { title: "绝命毒师", diff --git a/packages/domain/test/rules-slot.test.ts b/packages/domain/test/rules-slot.test.ts index 6af23bf0..3e9d1ff3 100644 --- a/packages/domain/test/rules-slot.test.ts +++ b/packages/domain/test/rules-slot.test.ts @@ -22,7 +22,7 @@ const base = (groups: RuleGroupSpec[], extra: Partial = {}): SlotSpec }); const rel = (t: string, subtitle?: string) => parseRelease(t, subtitle ? { subtitle } : {}); -describe("版本目标接入规则组", () => { +describe("收录版本接入规则组", () => { it("硬排除命中 → reject(rule.hard_exclude),trace 带组名与修订号", () => { const g = group("不要原盘", "", { id: "g1", From f0d4935fa7cecefaa6a86137a7c7ff949777870f Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Sep 2026 20:42:27 +0800 Subject: [PATCH 02/10] =?UTF-8?q?feat(web):=20=E6=8A=80=E6=9C=AF=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=8A=98=E5=8F=A0=E3=80=81=E5=8E=9F=E5=9B=A0=E7=A0=81?= =?UTF-8?q?=E4=BA=BA=E8=AF=9D=E5=8C=96=E3=80=81=E5=BE=85=E5=A4=84=E7=90=86?= =?UTF-8?q?=E3=80=8C=E6=8C=87=E5=AE=9A=E4=BD=9C=E5=93=81=E3=80=8D(DEAI-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 TechInfo 折叠组件(默认收起,标题「技术信息」);设置新增「界面」分区, 「默认展开技术信息」开关存本机浏览器 - 原因码表 @umt/contracts reasons.ts(identity_unknown、blocked:target_exists、below_min_size… → 一句人话 + 下一步),服务端整理记录 message 与冲突事项标题/摘要改用它 - 整理记录列表/详情、下载详情与时间线:原码、ID、hash、用时、变更序号进技术信息; 下载完成但入库失败时单独提示;时间线只列有记录的阶段,标题改「经过」 - 待处理:没识别出作品类事项主按钮「指定作品」(搜媒体库 + TMDB,走已有 resolve; 整理类事项打开重新整理),忽略为次要;去掉正常态徽章与「实时」指示;原始证据改「详细信息」进技术信息 - 整理成功后同时关闭该下载的「没识别出作品」事项 Co-Authored-By: Claude Opus 5.5 --- .../acquisition-organize/services/organize.ts | 13 +- apps/web/app/api/tasks.ts | 23 ++- apps/web/app/components/console/tech-info.tsx | 53 ++++++ .../downloads/business-timeline.tsx | 9 +- .../components/downloads/download-detail.tsx | 51 ++++-- apps/web/app/components/library/labels.ts | 2 +- .../components/library/work-assign-dialog.tsx | 157 ++++++++++++++++++ .../web/app/components/settings/interface.tsx | 28 ++++ apps/web/app/components/settings/sections.tsx | 16 +- apps/web/app/lib/labels.ts | 2 +- apps/web/app/lib/reasons.ts | 35 ++++ apps/web/app/lib/ui-prefs.ts | 72 ++++++++ apps/web/app/routes/console/inbox.tsx | 118 +++++++++++-- apps/web/app/routes/console/organize.$id.tsx | 69 ++++---- apps/web/app/routes/console/organize.tsx | 4 +- apps/web/app/routes/console/settings.tsx | 2 +- packages/contracts/src/index.ts | 1 + packages/contracts/src/reasons.ts | 73 ++++++++ packages/contracts/src/settings.ts | 10 ++ packages/contracts/test/reasons.test.ts | 26 +++ .../contracts/test/settings-sections.test.ts | 2 +- 21 files changed, 688 insertions(+), 78 deletions(-) create mode 100644 apps/web/app/components/console/tech-info.tsx create mode 100644 apps/web/app/components/library/work-assign-dialog.tsx create mode 100644 apps/web/app/components/settings/interface.tsx create mode 100644 apps/web/app/lib/reasons.ts create mode 100644 apps/web/app/lib/ui-prefs.ts create mode 100644 packages/contracts/src/reasons.ts create mode 100644 packages/contracts/test/reasons.test.ts diff --git a/apps/server/src/modules/acquisition-organize/services/organize.ts b/apps/server/src/modules/acquisition-organize/services/organize.ts index c8a9f1c4..903395e3 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 { @@ -710,9 +711,11 @@ export async function planImport(d: AoDeps, req: ImportRequest): Promise c.kind === "hnr_unsatisfied" || c.kind === "hnr_unknown"); await upsertDecision(d, trx, { kind: hnrConflict ? "approval_required" : "path_conflict", - title: hnrConflict ? `做种义务未满足,已阻止移动:${entity.title}` : `整理冲突:${entity.title}`, + title: hnrConflict + ? `做种义务未满足,已阻止移动:${entity.title}` + : `${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", @@ -1654,7 +1657,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/web/app/api/tasks.ts b/apps/web/app/api/tasks.ts index c9fe7970..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) @@ -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", @@ -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/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-timeline.tsx b/apps/web/app/components/downloads/business-timeline.tsx index d132455a..eacca1ca 100644 --- a/apps/web/app/components/downloads/business-timeline.tsx +++ b/apps/web/app/components/downloads/business-timeline.tsx @@ -9,7 +9,7 @@ import { Timeline } from "~/components/console/timeline.tsx"; // 阶段计数与首页/侧栏同口径;每条事件都有文字说明,不只靠颜色。 export const STAGE_LABEL: Record = { - observation: "观察", + observation: "发现", plan: "计划", download: "下载", organization: "整理", @@ -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/download-detail.tsx b/apps/web/app/components/downloads/download-detail.tsx index d7c4066c..035ce17c 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"; @@ -52,7 +55,6 @@ export function DownloadStatusBadges({ t }: { t: DownloadTask }) { {PAUSE_REASON_LABEL[t.pauseReason]} ) : null} {!t.managed ? 外部任务 : null} - {t.origin === "adopted" ? 接管 : null} ); } @@ -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 (
      @@ -132,7 +139,15 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { {task.pauseReason === "threshold" ? (

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

      + ) : null} + {importFailed ? ( +

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

      ) : null} {task.noProgressSince ? ( @@ -151,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} + - + @@ -175,7 +199,9 @@ export function DownloadDetail({ task }: { task: DownloadTask }) { { label: "说明", value: planReasonText(plan.data.statusReason), wide: true }, ]} /> - + + + ) : null} @@ -195,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_unresolved: "未识别", download_stalled: "下载无进展", download_failed: "下载失败", - path_conflict: "路径冲突", + path_conflict: "整理冲突", space_insufficient: "空间不足", approval_required: "需要审批", metadata_conflict: "元数据冲突", 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/settings/interface.tsx b/apps/web/app/components/settings/interface.tsx new file mode 100644 index 00000000..d78cdb61 --- /dev/null +++ b/apps/web/app/components/settings/interface.tsx @@ -0,0 +1,28 @@ +import { Switch } from "~/components/ui/switch.tsx"; +import { setUiPref, useUiPrefs } from "~/lib/ui-prefs.ts"; +import { SettingsBlock } from "./settings-block.tsx"; + +// 界面:本机显示偏好(存浏览器,不进 settings 表) + +export function InterfaceSection() { + const prefs = useUiPrefs(); + return ( + +
      +
      +

      + 默认展开技术信息 +

      +

      + ID、hash、token 数、耗时和原始错误码平时收在「技术信息」里。开启后各页默认展开。 +

      +
      + setUiPref("techInfoExpanded", v)} + /> +
      +
      + ); +} diff --git a/apps/web/app/components/settings/sections.tsx b/apps/web/app/components/settings/sections.tsx index bf67821a..6cb5d110 100644 --- a/apps/web/app/components/settings/sections.tsx +++ b/apps/web/app/components/settings/sections.tsx @@ -1,11 +1,24 @@ import { SETTINGS_SECTIONS, type SettingsSectionDef, type SettingsSectionId } from "@umt/contracts"; -import { Bot, FolderTree, KeyRound, type LucideIcon, Server, Shield, Tags, Tv, UserCog, Zap } from "lucide-react"; +import { + Bot, + FolderTree, + KeyRound, + type LucideIcon, + Monitor, + Server, + Shield, + Tags, + Tv, + UserCog, + Zap, +} from "lucide-react"; import type { ReactNode } from "react"; import { AccountSection } from "./account.tsx"; import { AgentSection } from "./agent.tsx"; import { ApiKeysSection } from "./api-keys.tsx"; import { AutomationSection } from "./automation.tsx"; import { DownloadersSection, MediaServersSection } from "./instances.tsx"; +import { InterfaceSection } from "./interface.tsx"; import { LibrarySection } from "./library.tsx"; import { MetadataSection } from "./metadata.tsx"; import { SitesSection } from "./sites.tsx"; @@ -21,6 +34,7 @@ const UI: Record ReactNode metadata: { icon: Tags, render: () => }, agent: { icon: Bot, render: () => }, "api-keys": { icon: KeyRound, render: () => }, + interface: { icon: Monitor, render: () => }, account: { icon: Shield, render: () => }, }; diff --git a/apps/web/app/lib/labels.ts b/apps/web/app/lib/labels.ts index c308c9c1..b9631c32 100644 --- a/apps/web/app/lib/labels.ts +++ b/apps/web/app/lib/labels.ts @@ -41,7 +41,7 @@ const DECISION_KIND: Record = { identity_unresolved: "未识别", download_stalled: "下载无进展", download_failed: "下载失败", - path_conflict: "路径冲突", + path_conflict: "整理冲突", space_insufficient: "空间不足", approval_required: "待审批", metadata_conflict: "元数据冲突", diff --git a/apps/web/app/lib/reasons.ts b/apps/web/app/lib/reasons.ts new file mode 100644 index 00000000..1d95ed71 --- /dev/null +++ b/apps/web/app/lib/reasons.ts @@ -0,0 +1,35 @@ +import { looksLikeCode, reasonText } from "@umt/contracts"; + +// 原因串 → 一句人话(DEAI-2)。映射表在 @umt/contracts 的 reasons.ts,服务端与前端共用; +// 原码交给 显示,不直接给人看。 + +/** `{ code, message }`(整理记录 / 文件结果)或裸原因串 → 一句话;不认识的码返回 fallback */ +export function reasonLine( + r: { code?: string | null; message?: string | null } | string | null | undefined, + fallback = "处理失败", +): string | null { + if (!r) return null; + const code = typeof r === "string" ? r : (r.code ?? null); + const message = typeof r === "string" ? null : (r.message ?? null); + // 服务端给的中文说明(冲突明细等)优先 + if (message && !looksLikeCode(message) && message !== code) return message; + const t = reasonText(code) ?? reasonText(message); + if (t) return t.action ? `${t.text}。${t.action}` : t.text; + if (code && !looksLikeCode(code)) return code; + return fallback; +} + +/** 原码(给技术信息用):只有是内部码时才返回 */ +export function reasonCode(r: { code?: string | null; message?: string | null } | string | null | undefined) { + if (!r) return null; + if (typeof r === "string") return looksLikeCode(r) ? r : null; + const c = r.code ?? null; + const m = r.message ?? null; + return [c, m && looksLikeCode(m) && m !== c ? m : null].filter(Boolean).join(" · ") || null; +} + +/** 原因串里的内部码替换成人话(如下载计划 statusReason、任务 waitReason) */ +export function humanizeReason(v: string | null | undefined): string | null { + if (!v) return null; + return looksLikeCode(v) ? reasonLine(v, v) : v; +} diff --git a/apps/web/app/lib/ui-prefs.ts b/apps/web/app/lib/ui-prefs.ts new file mode 100644 index 00000000..8d34f972 --- /dev/null +++ b/apps/web/app/lib/ui-prefs.ts @@ -0,0 +1,72 @@ +import { useSyncExternalStore } from "react"; + +// 本机界面偏好(设置 → 界面):只影响当前浏览器,存 localStorage;读写失败(隐私模式、禁用存储)时用默认值。 + +export type UiPrefs = { + /** 技术信息(ID、hash、token、耗时、原始字段、内部状态码)默认展开 */ + techInfoExpanded: boolean; +}; + +const KEY = "umt.ui-prefs"; +const DEFAULTS: UiPrefs = { techInfoExpanded: false }; +const listeners = new Set<() => void>(); +let cache: UiPrefs | null = null; + +function read(): UiPrefs { + if (cache) return cache; + let v: UiPrefs = DEFAULTS; + try { + const raw = typeof localStorage === "undefined" ? null : localStorage.getItem(KEY); + if (raw) v = { ...DEFAULTS, ...(JSON.parse(raw) as Partial) }; + } catch { + v = DEFAULTS; + } + cache = v; + return v; +} + +export function setUiPref(key: K, value: UiPrefs[K]) { + cache = { ...read(), [key]: value }; + try { + localStorage.setItem(KEY, JSON.stringify(cache)); + } catch { + // 存不下就只在本次会话生效 + } + for (const l of listeners) l(); +} + +function subscribe(l: () => void) { + listeners.add(l); + const onStorage = (e: StorageEvent) => { + if (e.key !== KEY) return; + cache = null; + l(); + }; + if (typeof window !== "undefined") window.addEventListener("storage", onStorage); + return () => { + listeners.delete(l); + if (typeof window !== "undefined") window.removeEventListener("storage", onStorage); + }; +} + +export function useUiPrefs(): UiPrefs { + return useSyncExternalStore(subscribe, read, () => DEFAULTS); +} + +/** 本地记住的折叠状态(计划页分组等):key → 是否展开;读写失败时返回 null */ +export function readCollapseState(key: string): Record | null { + try { + const raw = localStorage.getItem(key); + return raw ? (JSON.parse(raw) as Record) : null; + } catch { + return null; + } +} + +export function writeCollapseState(key: string, v: Record) { + try { + localStorage.setItem(key, JSON.stringify(v)); + } catch { + // 忽略 + } +} diff --git a/apps/web/app/routes/console/inbox.tsx b/apps/web/app/routes/console/inbox.tsx index b155fbe6..299d478e 100644 --- a/apps/web/app/routes/console/inbox.tsx +++ b/apps/web/app/routes/console/inbox.tsx @@ -1,6 +1,6 @@ import { useQueryClient } from "@tanstack/react-query"; import type { Decision, DecisionKind, DecisionOption, DecisionStatus } from "@umt/contracts"; -import { BellOff, Bot, CheckCheck, ChevronDown, ChevronRight, Inbox, Radio, RotateCw } from "lucide-react"; +import { BellOff, Bot, CheckCheck, ChevronDown, ChevronRight, Inbox, Radio, RotateCw, Search } from "lucide-react"; import { type ReactNode, useId, useMemo, useRef, useState } from "react"; import { Link, useSearchParams } from "react-router"; import { toast } from "sonner"; @@ -14,9 +14,12 @@ import { consolePath } from "~/components/console/nav.ts"; import { PageHeader } from "~/components/console/page-header.tsx"; import { StatTile } from "~/components/console/stat-tile.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; +import { TechInfo } from "~/components/console/tech-info.tsx"; import { EntityPicker, type PickedEntity } from "~/components/library/identity-entity-picker.tsx"; import { DECISION_KIND, DECISION_STATUS, opts, SEVERITY } from "~/components/library/labels.ts"; import { Chip, relTime, Tone } from "~/components/library/parts.tsx"; +import { type AssignedWork, WorkAssignDialog } from "~/components/library/work-assign-dialog.tsx"; +import { OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Input } from "~/components/ui/input.tsx"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx"; @@ -78,7 +81,7 @@ function GroupReleases({ d }: { d: Decision }) { ))} {count > list.length ? (
    • - 另有 {count - list.length} 条,可在媒体概览 → 未识别按标题检索 + 另有 {count - list.length} 条,可在媒体库 → 未识别按标题检索
    • ) : null}
    @@ -147,7 +150,14 @@ function Evidence({ d }: { d: Decision }) { ))}
    ) : null} - + + +
    ); } @@ -246,6 +256,82 @@ function OptionButton({ d, option }: { d: Decision; option: DecisionOption }) { ); } +/** 事项里能拿来搜作品的名字:代表资源标题 / 下载名 / 标题冒号后半段 */ +function subjectName(d: Decision): string | null { + const e = d.evidence; + const first = Array.isArray(e.releases) ? (e.releases[0] as { title?: unknown } | undefined) : undefined; + return ( + str(first?.title) ?? + str(e.title) ?? + str(e.name) ?? + (d.title.includes(":") ? d.title.split(":").slice(1).join(":") : null) + ); +} + +/** 搜作品用的关键词:去掉扩展名、分隔符、年份之后的规格串 */ +function queryOf(name: string | null): string { + if (!name) return ""; + const base = name.replace(/\.(mkv|mp4|ts|m2ts|iso|avi)$/i, "").replace(/[._]+/g, " "); + const m = base.match(/^(.*?)(?:\b(19|20)\d{2}\b|\bS\d{1,2}E?\d{0,3}\b|\b(2160|1080|720)p\b)/i); + return (m?.[1]?.trim() || base) + .replace(/[[\]()]/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, 60); +} + +const IDENTITY_KINDS: DecisionKind[] = ["identity_ambiguous", "identity_unresolved"]; + +/** 「没识别出作品」的主操作:搜作品并指定。资源类事项走 resolve(本地作品 / TMDB),整理类事项打开重新整理 */ +function AssignWorkButton({ d }: { d: Decision }) { + const resolve = useResolveDecision(); + const [open, setOpen] = useState(false); + const [wizard, setWizard] = useState(false); + const local = d.options.find((o) => o.input?.requires === "mediaEntityId"); + const tmdb = d.options.find((o) => o.input?.requires === "tmdb"); + const recordId = d.subjectType === "organization_record" ? d.subjectId : null; + if (!local && !tmdb && !recordId) return null; + const name = subjectName(d); + const assign = (w: AssignedWork) => { + const option = "mediaEntityId" in w ? local : tmdb; + if (!option) { + toast.error("mediaEntityId" in w ? "这条事项只能按 TMDB 指定" : "这条事项只能选媒体库里的作品"); + return Promise.reject(new Error("unsupported")); + } + return resolve + .mutateAsync({ + id: d.id, + optionKey: option.key, + identity: "mediaEntityId" in w ? { mediaEntityId: w.mediaEntityId } : { tmdb: w.tmdb }, + }) + .then( + () => toast.success(`已指定为「${w.title}」`), + (err) => { + toast.error(errorMessage(err, "指定失败")); + throw err; + }, + ); + }; + return ( + <> + + + {wizard && recordId ? ( + setWizard(false)} /> + ) : null} + + ); +} + function DismissButton({ d }: { d: Decision }) { const resolve = useResolveDecision(); return ( @@ -274,13 +360,18 @@ function DismissButton({ d }: { d: Decision }) { function DecisionCard({ d }: { d: Decision }) { const open = OPEN.includes(d.status); - const options = d.options.filter((o) => o.key !== "dismiss"); + const identity = IDENTITY_KINDS.includes(d.kind); + // 身份类:手动 TMDB / 选本地作品两个选项并进「指定作品」 + const options = d.options.filter( + (o) => + o.key !== "dismiss" && !(identity && (o.input?.requires === "tmdb" || o.input?.requires === "mediaEntityId")), + ); const resolution = d.resolution as { optionKey?: string | null; note?: string | null } | null; return (
    - - + {d.severity === "critical" ? : null} + {d.status === "investigating" ? : null} {relTime(d.createdAt)} {d.attempts ? ` · 调查 ${d.attempts} 次` : ""} @@ -294,6 +385,7 @@ function DecisionCard({ d }: { d: Decision }) {
    {open ? (
    + {identity ? : null} {options.map((o) => ( ))} @@ -404,13 +496,15 @@ export default function InboxPage() { <> - - - {live === "open" ? "实时" : live === "reconnecting" ? "重连中" : "连接中"} - + {live === "reconnecting" ? ( + + + 重连中 + + ) : null}
    {reason ? (

    - {f.failure ? "失败" : "跳过"}原因:{reasonText(reason)} + {f.failure ? "失败" : "跳过"}原因:{reasonLine(reason)}

    ) : null}

    {ACTION_LABEL[f.mode]} · {formatDateTime(f.startedAt)} - {duration(f.startedAt, f.finishedAt) ? ` · 用时 ${duration(f.startedAt, f.finishedAt)}` : ""}

    +
    ) : null} @@ -153,7 +152,7 @@ export default function OrganizeRecordPage() { size="sm" onClick={() => openAgentDrawer({ - label: `整理记录:${r.media?.title ?? r.id.slice(0, 8)}`, + label: `整理记录:${r.media?.title ?? "未关联作品"}`, recordId: r.id, entityId: r.media?.entityId, goal: "排查这次整理为什么没有全部成功,给出恢复办法", @@ -176,15 +175,17 @@ export default function OrganizeRecordPage() { ) : (
    -
    +
    {RECORD_STATUS_LABEL[r.status]} - {ACTION_LABEL[r.action]} - - {TRIGGER_LABEL[r.trigger]} - {r.trigger === "agent" ? "(计为自动)" : ""} - - {r.counts.succeeded}/{r.counts.total} 成功 · 失败 {r.counts.failed} · 跳过 {r.counts.skipped} + {[ + `${TRIGGER_LABEL[r.trigger]}${ACTION_LABEL[r.action]}`, + r.counts.total ? `${r.counts.succeeded}/${r.counts.total} 个文件成功` : null, + r.counts.failed ? `${r.counts.failed} 个失败` : null, + r.counts.skipped ? `${r.counts.skipped} 个跳过` : null, + ] + .filter(Boolean) + .join(" · ")}
    {r.reason ? ( @@ -196,7 +197,7 @@ export default function OrganizeRecordPage() { : "border-warning/40 bg-warning/10 text-warning", )} > - {reasonText(r.reason)} + {reasonLine(r.reason)}

    ) : null}
    @@ -208,19 +209,21 @@ export default function OrganizeRecordPage() { items={[ { label: "开始", value: formatDateTime(r.startedAt) }, { label: "完成", value: formatDateTime(r.finishedAt) }, - { label: "用时", value: duration(r.startedAt, r.finishedAt) }, + { label: "操作者", value: actorTypeLabel(r.actor.type) }, + ]} + /> + `${e.provider}:${e.entityType}:${e.externalId}`).join(", "), - mono: true, }, - { label: "变更游标", value: r.changeSeq, mono: true }, - { label: "记录 ID", value: r.id, mono: true, wide: true }, - { label: "任务 ID", value: r.taskId, mono: true, wide: true }, + { label: "操作者 ID", value: r.actor.type !== "system" ? r.actor.id : null }, + { label: "变更序号", value: r.changeSeq }, + { label: "记录 ID", value: r.id, wide: true }, + { label: "任务 ID", value: r.taskId, wide: true }, ]} /> {r.media ? ( diff --git a/apps/web/app/routes/console/organize.tsx b/apps/web/app/routes/console/organize.tsx index e37620c7..5417395b 100644 --- a/apps/web/app/routes/console/organize.tsx +++ b/apps/web/app/routes/console/organize.tsx @@ -12,6 +12,7 @@ import { PageHeader } from "~/components/console/page-header.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { relTime } from "~/components/workbench/release-fields.ts"; +import { reasonLine } from "~/lib/reasons.ts"; import { useSse } from "~/lib/sse.ts"; const ACTION_LABEL = { @@ -63,7 +64,7 @@ const COLUMNS: DataColumn[] = [
    {RECORD_STATUS_LABEL[r.status]} {r.reason ? ( - {r.reason.message} + {reasonLine(r.reason)} ) : null}
    ), @@ -120,7 +121,6 @@ export default function OrganizePage() { <> navigate("/console/files")}> diff --git a/apps/web/app/routes/console/settings.tsx b/apps/web/app/routes/console/settings.tsx index 6cd2fb05..508dee79 100644 --- a/apps/web/app/routes/console/settings.tsx +++ b/apps/web/app/routes/console/settings.tsx @@ -59,7 +59,7 @@ export default function SettingsPage() { return ( <> - +
    {/* 窄屏:顶部下拉 */}
    diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 0fccabdb..8f6a39d9 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -17,6 +17,7 @@ export * from "./ops.ts"; export * from "./organization.ts"; export * from "./pi.ts"; export * from "./policy.ts"; +export * from "./reasons.ts"; export * from "./redact.ts"; export * from "./settings.ts"; export * from "./sources.ts"; diff --git a/packages/contracts/src/reasons.ts b/packages/contracts/src/reasons.ts new file mode 100644 index 00000000..79fe46c7 --- /dev/null +++ b/packages/contracts/src/reasons.ts @@ -0,0 +1,73 @@ +// 内部原因码 → 一句人话 + 下一步(DEAI-2)。整理记录、待处理、下载时间线、任务都用这张表; +// 原码只放在界面的「技术信息」里,不直接给人看。不认识的码返回 null,由调用方决定兜底文字。 + +export type ReasonText = { + /** 发生了什么(一句,不带句号) */ + text: string; + /** 下一步能做什么(一句,不带句号);没有可做的就不给 */ + action?: string; +}; + +const REASONS: Record = { + identity_unknown: { text: "没识别出作品", action: "指定作品后重新整理" }, + identity_ambiguous: { text: "没识别出作品", action: "指定作品" }, + identity_unresolved: { text: "没识别出作品", action: "指定作品" }, + target_exists: { text: "媒体库里已有同名文件", action: "在待处理里选择替换或跳过" }, + duplicate_target: { text: "改名后与其他文件同名", action: "改命名模板或排除其中一个" }, + replace_requires_grant: { text: "替换已有文件需要你批准", action: "在待处理里批准" }, + hnr_unsatisfied: { text: "做种时间还不够,不能移动", action: "改用复制,或等做种完成" }, + hnr_unknown: { text: "不确定做种要求是否满足", action: "改用复制,或确认站点规则" }, + orphan_sidecar: { text: "字幕等附属文件找不到对应视频" }, + no_main_video: { text: "没有可入库的主视频" }, + below_min_size: { text: "文件小于最小体积,没有识别", action: "需要时在设置里调低最小体积" }, + too_small: { text: "文件小于最小体积" }, + source_path_unknown: { text: "下载器没有报告保存路径", action: "在下载器里确认任务还在" }, + path_mapping_missing: { text: "下载目录没有路径映射", action: "在设置 → 下载器里添加路径映射" }, + file_list_unavailable: { text: "读不到下载器里的文件列表", action: "稍后重试" }, + source_missing: { text: "找不到源文件", action: "检查下载目录是否挂载" }, + source_size_mismatch: { text: "源文件大小与下载器记录不一致", action: "等下载完成后重试" }, + too_many_files: { text: "文件太多,超出单次整理上限", action: "分批整理" }, + no_source: { text: "没有要整理的文件" }, + entity_not_found: { text: "作品已不存在" }, + no_library_root: { text: "没有可用的媒体库目录", action: "在设置 → 媒体库与目录里添加" }, + library_root_offline: { text: "媒体库目录不可访问或未挂载", action: "检查挂载后重试" }, + target_dir_outside_root: { text: "目标目录不在媒体库目录里" }, + slot_entity_mismatch: { text: "收录版本和作品对不上" }, + binding_on_other_root: { text: "作品已放在另一个媒体库目录", action: "在作品详情里调整目录" }, + move_downgraded_to_copy: { text: "做种未完成,已改为复制" }, + already_in_library: { text: "已在媒体库里" }, + main_not_transferred: { text: "主文件没有整理,附属文件一起跳过" }, + timeout: { text: "操作超时", action: "稍后重试" }, +}; + +/** + * 解析原因串:`identity_unknown`、`blocked:target_exists,hnr_unknown`、`move_downgraded_to_copy:hnr_unknown`。 + * 多个码时合并成一句(用";"分隔)。 + */ +export function reasonText(raw: string | null | undefined): ReasonText | null { + if (!raw) return null; + const s = raw.trim(); + const [head, rest] = s.includes(":") ? [s.slice(0, s.indexOf(":")), s.slice(s.indexOf(":") + 1)] : [s, ""]; + if (head === "blocked" && rest) { + const parts = rest + .split(",") + .map((c) => REASONS[c.trim()]) + .filter((x): x is ReasonText => !!x); + if (!parts.length) return null; + return { + text: [...new Set(parts.map((p) => p.text))].join(";"), + ...(parts[0]?.action ? { action: parts[0].action } : {}), + }; + } + return REASONS[s] ?? REASONS[head] ?? null; +} + +/** 只要一句话:"没识别出作品。指定作品后重新整理";不认识的码返回 fallback */ +export function reasonSentence(raw: string | null | undefined, fallback = "处理失败"): string { + const r = reasonText(raw); + if (!r) return raw && !/^[a-z_:.,]+$/.test(raw) ? raw : fallback; + return r.action ? `${r.text}。${r.action}` : r.text; +} + +/** 看起来像内部码(小写下划线 / 冒号)而不是给人看的句子 */ +export const looksLikeCode = (v: string | null | undefined) => !!v && /^[a-z0-9_]+([:.,][a-z0-9_:.,-]*)?$/.test(v); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 2cea6f33..9afeda4d 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -780,6 +780,7 @@ export const SettingsSectionId = z.enum([ "metadata", "agent", "api-keys", + "interface", "account", ]); export type SettingsSectionId = z.infer; @@ -968,6 +969,15 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [ fields: [], resources: ["api-keys"], }, + { + id: "interface", + label: "界面", + description: "只影响当前浏览器", + legacyTabs: [], + anchors: [{ id: "display", label: "显示" }], + fields: [], + resources: ["ui-preferences"], + }, { id: "account", label: "账户与安全", diff --git a/packages/contracts/test/reasons.test.ts b/packages/contracts/test/reasons.test.ts new file mode 100644 index 00000000..aaedff5c --- /dev/null +++ b/packages/contracts/test/reasons.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { looksLikeCode, reasonSentence, reasonText } from "../src/reasons.ts"; + +describe("reasonText(DEAI-2 原因码人话化)", () => { + it("裸码、blocked:多码、带后缀的码都能翻译", () => { + expect(reasonText("identity_unknown")).toEqual({ text: "没识别出作品", action: "指定作品后重新整理" }); + expect(reasonText("blocked:target_exists")?.text).toBe("媒体库里已有同名文件"); + expect(reasonText("blocked:target_exists,hnr_unknown")?.text).toBe("媒体库里已有同名文件;不确定做种要求是否满足"); + expect(reasonText("move_downgraded_to_copy:hnr_unknown")?.text).toBe("做种未完成,已改为复制"); + expect(reasonText("below_min_size")?.text).toContain("最小体积"); + }); + + it("不认识的码返回 null;句子兜底不把码原样给人看", () => { + expect(reasonText("some_new_code")).toBeNull(); + expect(reasonSentence("some_new_code")).toBe("处理失败"); + expect(reasonSentence("磁盘已满")).toBe("磁盘已满"); + expect(reasonSentence("identity_unknown")).toBe("没识别出作品。指定作品后重新整理"); + }); + + it("looksLikeCode 只认内部码", () => { + expect(looksLikeCode("blocked:target_exists")).toBe(true); + expect(looksLikeCode("identity_unknown")).toBe(true); + expect(looksLikeCode("目标已存在")).toBe(false); + expect(looksLikeCode("Connection refused")).toBe(false); + }); +}); diff --git a/packages/contracts/test/settings-sections.test.ts b/packages/contracts/test/settings-sections.test.ts index 21b54a1e..e8ccc109 100644 --- a/packages/contracts/test/settings-sections.test.ts +++ b/packages/contracts/test/settings-sections.test.ts @@ -8,7 +8,7 @@ import { } from "../src/settings.ts"; describe("SETTINGS_SECTIONS", () => { - it("9 个分区,顺序与 SettingsSectionId 一致", () => { + it("10 个分区,顺序与 SettingsSectionId 一致", () => { expect(SETTINGS_SECTIONS.map((s) => s.id)).toEqual(SettingsSectionId.options); }); From 260f3ab27f5fe7d553f83b9548dce9389ea44d2c Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Sep 2026 20:48:35 +0800 Subject: [PATCH 03/10] =?UTF-8?q?feat(web):=20=E9=A6=96=E9=A1=B5=E5=8F=AA?= =?UTF-8?q?=E5=9B=9E=E7=AD=94=E4=B8=89=E4=BB=B6=E4=BA=8B=E3=80=81=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E9=A1=B5=E5=B7=A1=E6=A3=80=E7=BD=AE=E9=A1=B6=E4=B8=8E?= =?UTF-8?q?=E5=88=86=E7=BB=84=E6=8A=98=E5=8F=A0=E3=80=81=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E5=8E=BB=E5=99=AA(DEAI-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首页:顶部一行状态(自动化开关 + 只在异常时出现的服务 / 下载盘 / 站点账号 / 下载器问题), 下面只有「在忙什么」「要我处理」「最近入库」三块;去掉运行状态、计划、缓存、来源健康、 满足率与 0 值铺陈;0 订阅时写「还没有订阅。去搜索添加」。只读对账移出首页,设置里放进 自动化 →「高级」(与存储预算、流水保留、缓存有效期一起默认收起)。 计划页:「智能体巡检」移到最上面——巡检数量少、由用户自己建、每次会花钱,是这页最需要 看的;自动任务任务多、多数不用管,放在下面。自动任务按分类分组、每组可折叠:有失败或 运行中的组默认展开,其余收起为「N 个任务 · 下次 xx」一行,折叠状态记在 localStorage; 维护类整组叫「数据清理」,未接入调度的备份不列。巡检卡只留名称、开关、间隔、上次结果与 费用,目标 / 能力 / 预算 / 模型 / 最近会话收进「详情」。去掉顶部 6 个计数卡。定时任务说明 去掉表名、字段名与实现备注。 任务页:默认隐藏系统任务(无业务对象的引擎步骤、定时任务的成功运行),可一键显示; 标题不再用 ID;耗时移入技术信息;< 1 秒写「< 1 秒」;费用两位小数,不足一分写「< $0.01」。 下载盘相关文案改用「暂停线 / 恢复线」。 Co-Authored-By: Claude Opus 5.5 --- .../src/modules/acquisition-organize/index.ts | 12 +- .../server/src/modules/auth-settings/index.ts | 2 +- apps/server/src/modules/cache/index.ts | 6 +- apps/server/src/modules/pi/index.ts | 2 +- apps/server/src/modules/schedules/index.ts | 2 +- .../src/modules/sources-identity/index.ts | 9 +- apps/server/src/modules/system/index.ts | 2 +- apps/server/src/modules/system/retention.ts | 22 +- .../components/schedules/agent-check-card.tsx | 237 +++--- .../components/schedules/auto-task-list.tsx | 121 +++- .../app/components/schedules/labels.test.ts | 5 +- apps/web/app/components/schedules/labels.ts | 9 +- .../components/schedules/schedule-board.tsx | 115 ++- .../app/components/settings/automation.tsx | 91 ++- .../app/components/status/storage-section.tsx | 2 +- apps/web/app/components/tasks/labels.ts | 2 +- apps/web/app/components/tasks/task-detail.tsx | 11 +- apps/web/app/components/tasks/task-stream.tsx | 32 +- apps/web/app/lib/labels.ts | 2 +- apps/web/app/routes/console/home.tsx | 681 +++++------------- apps/web/app/routes/console/schedules.tsx | 1 - apps/web/app/routes/console/tasks.tsx | 3 +- packages/contracts/src/settings.ts | 6 +- 23 files changed, 559 insertions(+), 816 deletions(-) diff --git a/apps/server/src/modules/acquisition-organize/index.ts b/apps/server/src/modules/acquisition-organize/index.ts index 3812616b..4488decd 100644 --- a/apps/server/src/modules/acquisition-organize/index.ts +++ b/apps/server/src/modules/acquisition-organize/index.ts @@ -106,7 +106,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se key: "downloads.sync", defaults: { name: "qB 同步", - description: "同步下载器任务状态,按缓存阈值暂停或恢复", + description: "同步下载器任务状态;下载盘到暂停线时暂停,降到恢复线后恢复", group: "download", schedule: every(syncMs), }, @@ -125,8 +125,8 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se registerScheduledJob(ctx, { key: "cache.threshold", defaults: { - name: "缓存阈值检查", - description: "下载缓存超过硬阈值时入队清理", + name: "下载盘检查", + description: "下载盘超过暂停线时清理已入库的下载", group: "maintain", schedule: every(planMs), }, @@ -141,7 +141,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se key: "downloads.orchestrate", defaults: { name: "补提交与补入库", - description: "补投已预留未提交的下载、已完成未入库的任务", + description: "补交没提交成功的下载,补整理下载完但没入库的任务", group: "download", schedule: every(planMs), }, @@ -155,7 +155,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se key: "slots.reevaluate", defaults: { name: "到期重评", - description: "到了重评时间的收录位重新评估(忙时延后的发布、定时)", + description: "重新检查等待中的资源", group: "download", schedule: every(planMs), }, @@ -165,7 +165,7 @@ export function createAcquisitionOrganizeModule(overrides: AoOverrides = {}): Se key: "subscriptions.search", defaults: { name: "订阅周期搜索", - description: "到期的订阅主动搜索站点", + description: "到期的订阅去站点搜索", group: "collect", schedule: every(planMs), }, 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/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/pi/index.ts b/apps/server/src/modules/pi/index.ts index 3225108f..401dadd9 100644 --- a/apps/server/src/modules/pi/index.ts +++ b/apps/server/src/modules/pi/index.ts @@ -74,7 +74,7 @@ export function createPiModule( key: "pi.token_refresh", defaults: { name: "模型服务登录刷新", - description: "订阅登录即将过期的模型服务,提前刷新 token", + description: "订阅登录快过期时提前续期", group: "maintain", schedule: { type: "interval", everyMs: interval }, }, diff --git a/apps/server/src/modules/schedules/index.ts b/apps/server/src/modules/schedules/index.ts index bd4d8a54..3e0521d5 100644 --- a/apps/server/src/modules/schedules/index.ts +++ b/apps/server/src/modules/schedules/index.ts @@ -37,7 +37,7 @@ export function createSchedulesModule(opts: SchedulesModuleOptions = {}): Server key: "maintain.backup", defaults: { name: "备份", - description: "数据库与数据目录备份;目前用 umt backup(宿主机 cron 调用),未接入调度", + description: "数据库与数据目录备份,目前在服务器上用命令执行", group: "maintain", schedule: { type: "cron", cron: "30 3 * * *" }, enabled: false, diff --git a/apps/server/src/modules/sources-identity/index.ts b/apps/server/src/modules/sources-identity/index.ts index 770f0562..6fd1bd19 100644 --- a/apps/server/src/modules/sources-identity/index.ts +++ b/apps/server/src/modules/sources-identity/index.ts @@ -61,7 +61,7 @@ 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); // W9-A 站点中心:用户数据每 6 小时、健康检测每小时(可在定时任务页关闭或改周期) @@ -69,7 +69,7 @@ export function createSourcesIdentityModule(overrides: RuntimeOverrides = {}): S key: "sites.user_data", defaults: { name: "站点数据刷新", - description: "登录状态正常的站点账号刷新上传 / 下载 / 分享率等用户数据;会话失效的账密账号自动重登一次", + description: "刷新站点账号的上传、下载和分享率;登录失效的账密账号自动重登一次", group: "collect", schedule: { type: "interval", everyMs: 6 * 3_600_000 }, firstRun: "after_interval", @@ -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/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 1c0bf0a6..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) { @@ -168,7 +168,7 @@ export const RETENTION_JOBS: RetentionJob[] = [ target: "agent_event", jobId: "storage.retention.agent_event", name: "智能体事件清理", - description: "删除超过保留期、会话已结束的智能体事件流与消息(agent_run_event / agent_run_message;会话本身保留)", + description: "删除已结束会话的过期消息,会话本身保留", everyMs: 6 * HOUR, retention: (s) => `${s.retentionDays.agentEvent} 天`, async run(c) { @@ -189,7 +189,7 @@ export const RETENTION_JOBS: RetentionJob[] = [ 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) { @@ -231,7 +231,7 @@ export const RETENTION_JOBS: RetentionJob[] = [ target: "identity_pool", jobId: "storage.retention.identity_pool", name: "未识别资源归档", - description: "未识别资源里 unmatched 条目超过保留期没有新证据就归档(不删除,仍可按状态筛出)", + description: "长期没有新线索的未识别资源归档,仍可手动指定", everyMs: 6 * HOUR, retention: (s) => `${s.retentionDays.identityPoolArchive} 天`, async run(c) { @@ -252,8 +252,8 @@ export const RETENTION_JOBS: RetentionJob[] = [ { target: "analyze", jobId: "storage.analyze", - name: "统计信息更新", - description: "对全库执行 ANALYZE,刷新查询规划用的统计信息(每周)", + name: "数据库维护", + description: "每周刷新数据库统计信息", everyMs: 7 * 24 * HOUR, retention: () => null, async run(c) { diff --git a/apps/web/app/components/schedules/agent-check-card.tsx b/apps/web/app/components/schedules/agent-check-card.tsx index 4259991b..f605743c 100644 --- a/apps/web/app/components/schedules/agent-check-card.tsx +++ b/apps/web/app/components/schedules/agent-check-card.tsx @@ -7,6 +7,7 @@ import { useDeleteSchedule } from "~/api/schedules.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { consolePath } 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 { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; @@ -16,7 +17,8 @@ import { NextRun, useJobActions } from "./auto-task-list.tsx"; import { IntervalSelect } from "./interval-select.tsx"; import { CHECK_TRIGGER_LABEL, formatUsd, RUN_STATUS } from "./labels.ts"; -// 计划页「Agent 巡检」:到点按目标模板发起一次 Agent 会话(kind=agent_check),一个巡检一张卡片。 +// 计划页「智能体巡检」:到点按目标模板发起一次智能体会话(kind=agent_check),一个巡检一张卡片。 +// 卡片默认只显示名称、开关、间隔、上次结果与费用;目标、能力、预算、模型、最近会话收进「详情」。 /** 卡片上能力白名单最多列几个,其余折成"+N" */ const MAX_CAPS = 6; @@ -113,103 +115,31 @@ export function AgentCheckCard({ /> -
    -

    - {c.goalTemplate ?? "—"} -

    +
    + + + 下次 +
    - -
    - - {allow.length ? ( - - {allow.slice(0, MAX_CAPS).map((id) => ( - - {capTitle(id)} - - ))} - {allow.length > MAX_CAPS ? ( - - +{allow.length - MAX_CAPS} - - ) : null} - +

    + {last ? ( + job.lastStatus === "ok" ? ( + 上次成功 ) : ( - "全部只读能力" - )} - {c.allowRecover ? · 允许授权内恢复 : null} - - {budgetParts.length ? budgetParts.join(" · ") : "默认"} - - {stats?.lastModel ? ( - - {stats.lastProvider ? `${stats.lastProvider} · ` : ""} - {stats.lastModel} - {stats.models.length > 1 ? ( - 等 {stats.models.length} 个模型 - ) : null} - - ) : upstream ? ( - - 默认模型服务 {upstream.name} · {upstream.model ?? "模型未指定"} - {upstream.modelFromCatalog ? (目录首个) : null} - - ) : ( - 未配置模型服务 - )} - - - {CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]} - {c.reportAsDecision ? · 结果写入待处理 : null} - - - - - - - - - - - {stats?.count ? ( - - 上次 {formatUsd(stats.lastCostUsd)} - - {" "} - · 近 {stats.count} 次平均 {formatUsd(stats.avgCostUsd)} - - - ) : ( - 还没有会话 - )} - - - - {last ? {last.label} : 未运行} - {job.lastRunAt ? {formatRelative(job.lastRunAt)} : null} - - -

    + {last.label} + ) + ) : ( + 还没运行过 + )} + {job.lastRunAt ? {formatRelative(job.lastRunAt)} : null} + {stats?.count ? · 每次约 {formatUsd(stats.avgCostUsd)} : null} +

    {job.lastStatus === "failed" && job.lastError ? (
    @@ -218,33 +148,96 @@ export function AgentCheckCard({
    ) : null} - {runs.length ? ( -
    -

    最近会话

    -
      - {runs.slice(0, 3).map((r) => { - const s = agentRunLabel(r.status); - return ( -
    • - +
      + + + {c.goalTemplate ?? "—"} + + + + {allow.length ? ( + + {allow.slice(0, MAX_CAPS).map((id) => ( + - {s.label} - - {plainSummary(r.summary ?? r.goal)} - - {formatUsd(r.costUsd)} - - {formatRelative(r.finishedAt ?? r.createdAt)} - - -
    • - ); - })} -
    -
    - ) : null} + {capTitle(id)} + + ))} + {allow.length > MAX_CAPS ? ( + + +{allow.length - MAX_CAPS} + + ) : null} + + ) : ( + "全部只读能力" + )} + {c.allowRecover ? · 允许授权内恢复 : null} + + + {CHECK_TRIGGER_LABEL[c.triggerCondition ?? "always"]} + {c.reportAsDecision ? · 结果写入待处理 : null} + + {budgetParts.length ? budgetParts.join(" · ") : "默认"} + + {stats?.lastModel ? ( + + {stats.lastProvider ? `${stats.lastProvider} · ` : ""} + {stats.lastModel} + {stats.models.length > 1 ? ( + 等 {stats.models.length} 个模型 + ) : null} + + ) : upstream ? ( + + {upstream.name} · {upstream.model ?? "模型未指定"} + + ) : ( + 未配置模型服务 + )} + + {stats?.count ? ( + + + 上次 {formatUsd(stats.lastCostUsd)} · 近 {stats.count} 次平均 {formatUsd(stats.avgCostUsd)} + + + ) : null} +
    + {runs.length ? ( +
    +

    最近会话

    +
      + {runs.slice(0, 3).map((r) => { + const st = agentRunLabel(r.status); + return ( +
    • + + {r.status === "succeeded" ? null : {st.label}} + + {plainSummary(r.summary ?? r.goal)} + + + {formatRelative(r.finishedAt ?? r.createdAt)} + + +
    • + ); + })} +
    +
    + ) : null} +
    -
      - {list.map((j) => ( - - ))} -
    + {open ? ( +
      + {list.map((j) => ( + + ))} +
    + ) : null}
    ); })} diff --git a/apps/web/app/components/schedules/labels.test.ts b/apps/web/app/components/schedules/labels.test.ts index ca147590..e4f24173 100644 --- a/apps/web/app/components/schedules/labels.test.ts +++ b/apps/web/app/components/schedules/labels.test.ts @@ -12,10 +12,11 @@ describe("计划页标签", () => { expect(scheduleStreamPath("trash.purge")).toBe("/console/tasks?tab=all&kind=schedule&ref=trash.purge"); }); - it("formatUsd:小额保留 4 位", () => { + it("formatUsd:两位小数,不足一分写 < $0.01", () => { expect(formatUsd(undefined)).toBe("—"); expect(formatUsd(0)).toBe("$0"); - expect(formatUsd(0.0031258)).toBe("$0.0031"); + expect(formatUsd(0.0031258)).toBe("< $0.01"); + expect(formatUsd(0.0191)).toBe("$0.02"); expect(formatUsd(1.234)).toBe("$1.23"); }); diff --git a/apps/web/app/components/schedules/labels.ts b/apps/web/app/components/schedules/labels.ts index b24e119c..1b675515 100644 --- a/apps/web/app/components/schedules/labels.ts +++ b/apps/web/app/components/schedules/labels.ts @@ -10,7 +10,7 @@ import type { StatusTone } from "~/components/console/status-badge.tsx"; // 定时任务看板的标签映射(只在本页用,不放 lib/labels.ts) export const GROUP_ORDER: ScheduleGroup[] = ["collect", "download", "organize", "maintain", "agent"]; -/** 计划页「自动任务」一节的分组(Agent 巡检单独一节) */ +/** 计划页「自动任务」一节的分组(智能体巡检单独一节,放在最上面);维护类合成一组「数据清理」 */ export const TASK_GROUPS: ScheduleGroup[] = ["collect", "download", "organize", "maintain"]; /** 计划页两节的锚点:/console/schedules#auto、#checks */ @@ -28,7 +28,7 @@ export const GROUP_LABEL: Record = { collect: "采集", download: "下载", organize: "整理", - maintain: "维护", + maintain: "数据清理", agent: "智能体巡检", }; @@ -70,11 +70,12 @@ export const INTERVAL_PRESETS: number[] = [ 7 * 24 * 3600_000, ]; -/** 美元费用:"$0.0031" / "$0.12";空值 "—" */ +/** 美元费用:两位小数,不足一分写 "< $0.01";空值 "—" */ export function formatUsd(v: number | null | undefined): string { if (v === null || v === undefined) return "—"; if (v === 0) return "$0"; - return `$${v < 0.1 ? v.toFixed(4) : v.toFixed(2)}`; + if (v < 0.01) return "< $0.01"; + return `$${v.toFixed(2)}`; } /** 毫秒 → 最大整除单位 */ diff --git a/apps/web/app/components/schedules/schedule-board.tsx b/apps/web/app/components/schedules/schedule-board.tsx index d494d174..1330c7bd 100644 --- a/apps/web/app/components/schedules/schedule-board.tsx +++ b/apps/web/app/components/schedules/schedule-board.tsx @@ -1,12 +1,11 @@ import { useQueryClient } from "@tanstack/react-query"; import { SCHEDULE_CHANGED_EVENT, type ScheduledJob } from "@umt/contracts"; -import { AlertTriangle, Bot, CalendarClock, CircleCheck, Coins, LoaderCircle, Plus, XCircle } from "lucide-react"; +import { AlertTriangle, Bot, CalendarClock, Plus } from "lucide-react"; import { type ReactNode, useEffect, useRef, useState } from "react"; import { useLocation } from "react-router"; import { usePiUpstreamModels, usePiUpstreams } from "~/api/pi.ts"; import { useScheduleBoard } from "~/api/schedules.ts"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; -import { StatTile } from "~/components/console/stat-tile.tsx"; import { Button } from "~/components/ui/button.tsx"; import { qk } from "~/lib/query-keys.ts"; import { useSse } from "~/lib/sse.ts"; @@ -16,8 +15,9 @@ import { AutoTaskList } from "./auto-task-list.tsx"; import { formatUsd } from "./labels.ts"; import { RunsSheet } from "./runs-sheet.tsx"; -// 计划页(FE-6,/console/schedules):「自动任务」= 程序按固定周期执行的定时任务(紧凑列表); -// 「Agent 巡检」= 到点发起 Agent 会话的定时任务(卡片)。自动任务的运行记录跳任务页按该定时任务筛选。 +// 计划页(FE-6,/console/schedules):「智能体巡检」= 到点发起智能体会话的定时任务(卡片,放最上面: +// 数量少、由用户自己建、会花钱,是这页最需要看的);「自动任务」= 程序按固定周期执行的定时任务 +// (按分类折叠的紧凑列表,维护类合成「数据清理」)。自动任务的运行记录跳任务页按该定时任务筛选。 /** 每秒一跳的当前时间(倒计时用) */ export function useNow(ms = 1000) { @@ -69,7 +69,17 @@ export function planStats(items: ScheduledJob[]) { }; } -function SectionHead({ id, title, hint, actions }: { id: string; title: string; hint: string; actions?: ReactNode }) { +function SectionHead({ + id, + title, + hint, + actions, +}: { + id: string; + title: string; + hint: ReactNode; + actions?: ReactNode; +}) { return (
    @@ -107,7 +117,6 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) { } : null; const s = planStats(items); - const val = (n: number) => (board.isPending ? "…" : board.isError ? "—" : n); // 首页计数链到 #auto / #checks:数据到了再滚过去 const loaded = !!data; @@ -117,59 +126,8 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) { if (id === "auto" || id === "checks") document.getElementById(id)?.scrollIntoView({ block: "start" }); }, [loaded, hash, highlightJob]); - const jump = (id: string) => document.getElementById(id)?.scrollIntoView({ block: "start", behavior: "smooth" }); - return ( <> -
    -
    -

    自动任务

    -
    - jump("auto")} - /> - 0 ? "destructive" : "neutral"} - value={val(s.auto.failed)} - onClick={() => jump("auto")} - /> - jump("auto")} /> -
    -
    -
    -

    智能体巡检

    -
    - jump("checks")} - /> - 0 ? "destructive" : "neutral"} - value={val(s.checks.failed)} - onClick={() => jump("checks")} - /> - jump("checks")} - /> -
    -
    -
    - {data && !data.schedulerOnline ? (

    board.refetch()} /> ) : (

    -
    - - {autoJobs.length === 0 ? ( - - ) : ( - - )} -
    -
    setEditing({ job: null, key: Date.now() })}> @@ -229,6 +184,24 @@ export function PlanBoard({ highlightJob }: { highlightJob?: string }) {
    )}
    +
    + + {autoJobs.length === 0 ? ( + + ) : ( + + )} +
    )} diff --git a/apps/web/app/components/settings/automation.tsx b/apps/web/app/components/settings/automation.tsx index 7e7bb75c..bb26eb02 100644 --- a/apps/web/app/components/settings/automation.tsx +++ b/apps/web/app/components/settings/automation.tsx @@ -1,12 +1,13 @@ -import { Pause, Play, ShieldAlert } from "lucide-react"; -import type { ReactNode } from "react"; +import { ChevronDown, Pause, Play, ShieldAlert } from "lucide-react"; +import { type ReactNode, useEffect, useState } from "react"; +import { useLocation } from "react-router"; import { toast } from "sonner"; import { useUpdateSettings } from "~/api/settings.ts"; import { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn } from "~/lib/utils.ts"; import { type FormValues, num } from "./resource-section.tsx"; -import { SettingsBlock } from "./settings-block.tsx"; +import { anchorId, SettingsBlock } from "./settings-block.tsx"; import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx"; // 自动化分区:总开关、只读对账、缓存水位、每日下载预算、保留天数、存储预算与保留(CACHE-2) @@ -15,12 +16,12 @@ const GiB = 1024 ** 3; const CACHE_CARD: CardSpec = { anchor: "cache", - title: "缓存水位", - description: "缓存卷使用率到硬阈值时停止新增下载并暂停本工具的下载任务;降到恢复水位以下只恢复因阈值暂停的任务。", + title: "下载盘", + description: "下载盘用量到暂停线时不再新增下载,并暂停本工具的下载任务;降到恢复线以下只恢复因此暂停的任务。", fields: [ - { name: "cache.hardThresholdPercent", label: "硬阈值(%)", kind: "number" }, - { name: "cache.resumePercent", label: "恢复水位(%)", kind: "number", hint: "低于硬阈值" }, - { name: "cache.cleanupTargetPercent", label: "清理目标(%)", kind: "number", hint: "不高于恢复水位" }, + { name: "cache.hardThresholdPercent", label: "暂停线(%)", kind: "number" }, + { name: "cache.resumePercent", label: "恢复线(%)", kind: "number", hint: "低于暂停线" }, + { name: "cache.cleanupTargetPercent", label: "清理目标(%)", kind: "number", hint: "不高于恢复线" }, { name: "cache.checkIntervalSec", label: "检查间隔(秒)", kind: "number", hint: "10–3600" }, ], fromSettings: (s) => ({ @@ -75,7 +76,7 @@ const RETENTION_CARD: CardSpec = { kind: "number", hint: "复制入库后下载源在缓存里至少留这么久;0–365", }, - { name: "retention.logRetentionDays", label: "日志保留天数", kind: "number", hint: "观察与日志类数据" }, + { name: "retention.logRetentionDays", label: "日志保留天数", kind: "number" }, ], fromSettings: (s) => ({ "retention.seedRetentionDays": n(s.retention.seedRetentionDays), @@ -211,6 +212,59 @@ function ToggleRow({ ); } +/** 高级:只读对账、存储预算、流水保留、缓存有效期;默认收起,从 #automation/advanced 进来时展开 */ +function Advanced({ children }: { children: ReactNode }) { + const location = useLocation(); + const [open, setOpen] = useState(() => location.hash.endsWith("/advanced")); + useEffect(() => { + if (location.hash.endsWith("/advanced")) setOpen(true); + }, [location.hash]); + return ( +
    + +

    只读对账、存储预算、数据保留和缓存有效期。一般不用改。

    + {open ?
    {children}
    : null} +
    + ); +} + +function ReadOnlySwitch() { + const update = useUpdateSettings(); + return ( + + {(s) => ( + + } + tone={s.automation.readOnlyReconcile ? "warning" : "success"} + title={`只读对账 · ${s.automation.readOnlyReconcile ? "开启" : "关闭"}`} + description="从备份恢复后先开启:只核对库存与路径,不做自动写入。核对完再关闭。" + checked={s.automation.readOnlyReconcile} + disabled={update.isPending} + onChange={(v) => + update.mutateAsync({ automation: { readOnlyReconcile: v } }).then( + () => toast.success(v ? "已开启只读对账" : "已关闭只读对账"), + (err) => toast.error(errorMessage(err, "切换失败")), + ) + } + /> + + )} + + ); +} + function Switches() { const update = useUpdateSettings(); const set = async (patch: { enabled?: boolean; readOnlyReconcile?: boolean }, msg: string) => { @@ -236,16 +290,6 @@ function Switches() { disabled={update.isPending} onChange={(v) => set({ enabled: v }, v ? "自动化已恢复" : "自动化已暂停")} /> - } - tone={s.automation.readOnlyReconcile ? "warning" : "success"} - title={`只读对账 · ${s.automation.readOnlyReconcile ? "开启" : "关闭"}`} - description="从备份恢复后先开启:只核对库存与路径,不做自动写入。核对完再关闭。" - checked={s.automation.readOnlyReconcile} - disabled={update.isPending} - onChange={(v) => set({ readOnlyReconcile: v }, v ? "已开启只读对账" : "已关闭只读对账")} - />
    )} @@ -263,9 +307,12 @@ export function AutomationSection() { - - - + + + + + + )} diff --git a/apps/web/app/components/status/storage-section.tsx b/apps/web/app/components/status/storage-section.tsx index cb8e79f4..431de792 100644 --- a/apps/web/app/components/status/storage-section.tsx +++ b/apps/web/app/components/status/storage-section.tsx @@ -38,7 +38,7 @@ export const CLEANUP_LABEL: Record = { audit: "审计事件", capability_stats: "能力调用统计", identity_pool: "未识别资源归档", - analyze: "统计信息更新", + analyze: "数据库维护", images: "图片缓存容量", kv_cache: "过期缓存", }; diff --git a/apps/web/app/components/tasks/labels.ts b/apps/web/app/components/tasks/labels.ts index b271149e..2e2cbcb9 100644 --- a/apps/web/app/components/tasks/labels.ts +++ b/apps/web/app/components/tasks/labels.ts @@ -94,7 +94,7 @@ export const RUN_STATUS_OPTIONS = ["pending", "running", "completed", "failed", export function runTitle(r: Pick): string { const b = r.business; if (b?.title) return b.title; - if (b) return `${BUSINESS_KIND_LABEL[b.kind]} ${b.refId.length > 12 ? b.refId.slice(0, 8) : b.refId}`; + if (b) return BUSINESS_KIND_LABEL[b.kind]; return workflowLabel(r.workflowName); } diff --git a/apps/web/app/components/tasks/task-detail.tsx b/apps/web/app/components/tasks/task-detail.tsx index 355e0758..986fb0fc 100644 --- a/apps/web/app/components/tasks/task-detail.tsx +++ b/apps/web/app/components/tasks/task-detail.tsx @@ -254,7 +254,7 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun return ( <> - + - {BUSINESS_KIND_LABEL[b.kind]} · {b.title ?? b.refId} + {b.title ? `${BUSINESS_KIND_LABEL[b.kind]} · ${b.title}` : BUSINESS_KIND_LABEL[b.kind]} ) : ( - `${BUSINESS_KIND_LABEL[b.kind]} · ${b.title ?? b.refId}` + `${BUSINESS_KIND_LABEL[b.kind]}${b.title ? ` · ${b.title}` : ""}` ) ) : ( "无" @@ -279,7 +279,6 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun label: "开始", value: r.startedAt ? `${formatDateTime(r.startedAt)}(${formatRelative(r.startedAt)})` : "未开始", }, - { label: "耗时", value: formatDuration(spanSec(r)) }, ]} /> @@ -304,9 +303,11 @@ export function TaskDetailBody({ r, onOpenRun }: { r: EngineRunDetail; onOpenRun - + p.items); - const filtered = Object.values(filters).some(Boolean); + const all = list.data?.pages.flatMap((p) => p.items); + // 系统任务(没有业务对象的引擎步骤、定时任务的成功运行)默认不列;按对象 / 定时任务筛选或勾选时显示 + const showSystem = filters.system === "1" || !!filters.ref || filters.kind === "schedule" || !!filters.workflow; + const isSystem = (r: EngineRun) => !r.business || (r.business.kind === "schedule" && r.status === "completed"); + const rows = showSystem ? all : all?.filter((r) => !isSystem(r)); + const hidden = (all?.length ?? 0) - (rows?.length ?? 0); + const filtered = Object.entries(filters).some(([k, v]) => k !== "system" && !!v); return ( <> @@ -196,6 +203,14 @@ export function TaskStream({ options={SINCE} allLabel="不限" /> + {filters.workflow ? ( + "自动化已暂停,手动操作仍可用" + ) : a.readOnlyReconcile ? ( + <> + 只读模式:不会自动下载和整理。 + + 去设置关闭 + + + ) : a.activeSubscriptions > 0 ? ( + `自动化已开启 · ${a.activeSubscriptions} 个订阅` ) : ( - - - 暂停自动化 - - } - title="暂停全部自动化?" - description="自动收录、下载、整理会停止,已提交的下载任务不受影响。可随时恢复。" - confirmLabel="暂停自动化" - onConfirm={() => setAutomation({ enabled: false }, "自动化已暂停")} - /> + <> + 自动化已开启 · 还没有订阅。 + + 去搜索添加 + + )} - + {problems.map((p) => ( + + {" · "} + + {p.text} + + + ))} +

    + {paused ? ( + + ) : ( + + + 暂停 + + } + title="暂停全部自动化?" + description="自动收录、下载、整理会停止,已提交的下载任务不受影响。可随时恢复。" + confirmLabel="暂停自动化" + onConfirm={() => setEnabled(false)} + /> + )} ); } -// ─── 正在进行:任务流 top N ─── -function InProgress({ w }: { w: Dashboard["currentWork"] }) { - const running = useEngineRunsPage({ status: "running" }, 6, 10_000); - const stats = useEngineStats(); - const rows = running.data?.items ?? []; +// ─── 在忙什么:下载中的任务 + 进行中的智能体会话 + 有业务对象的运行 ─── +function Busy({ w }: { w: Dashboard["currentWork"] }) { + const running = useEngineRunsPage({ status: "running" }, 10, 10_000); + const runs = (running.data?.items ?? []).filter((r) => r.business); + const downloads = w.items.slice(0, 4); + const agents = w.agentRuns.slice(0, 2); + const summary = nonZero([ + [w.downloading, "个下载中"], + [w.importing, "个入库中"], + [w.stalled, "个没有进展"], + [w.paused, "个已暂停"], + ]); + const empty = !downloads.length && !agents.length && !runs.length; return ( -
    - {running.isPending ? ( +
    + {running.isPending && !downloads.length ? ( - ) : running.isError ? ( - running.refetch()} /> - ) : rows.length === 0 ? ( - + ) : empty ? ( + ) : (
      - {rows.slice(0, 5).map((r) => ( + {downloads.map((t) => ( + + + {t.name} + {t.status === "stalled" ? 没有进展 : null} + + + + {Math.round(t.progress * 100)}% + + + ))} + {agents.map((r) => ( + + + {r.goal} + + + + ))} + {runs.slice(0, Math.max(0, 5 - downloads.length - agents.length)).map((r) => ( {runTitle(r)} - {workflowLabel(r.workflowName)} - - {formatDuration(spanSec(r))} - ))}
    )} -

    - 排队 {stats.data?.byStatus.pending ?? "—"} · 下载中 {w.downloading} · 无进展 {w.stalled} · 入库中 {w.importing} -

    + {summary ?

    {summary}

    : null}
    ); } -// ─── 需要我处理:计数 + 前 3 条 ─── +// ─── 要我处理什么:计数 + 前 3 条 ─── function NeedsMe({ n }: { n: Dashboard["needsAttention"] }) { const list = useDecisions({}); const top = (list.data?.pages[0]?.items ?? []).slice(0, 3); return ( -
    +
    {n.total === 0 ? ( - + ) : ( <>

    @@ -268,9 +278,7 @@ function NeedsMe({ n }: { n: Dashboard["needsAttention"] }) { {d.title} - - {decisionKindLabel(d.kind)} · {formatRelative(d.createdAt)} - + {formatRelative(d.createdAt)} ))} @@ -281,387 +289,39 @@ function NeedsMe({ n }: { n: Dashboard["needsAttention"] }) { ); } -// ─── 媒体概览:作品/季/资产计数 + 收录版本满足率 + 今日产出 ─── -function MediaOverview({ a }: { a: Dashboard["automation"] }) { - const c = a.slotCounts; - const total = Object.values(c).reduce((x, y) => x + y, 0); - const rate = a.slotSatisfiedRate === null ? null : Math.round(a.slotSatisfiedRate * 100); - const lib = a.library; +// ─── 最近入库了什么:成功 / 部分成功的整理 ─── +function RecentImports({ d }: { d: Dashboard }) { + const rows = d.recentOutput.filter((r) => r.status === "succeeded" || r.status === "partial").slice(0, 6); + const today = d.automation.today.filesImported; return ( -

    -
    - - - -
    -
    -
    - 收录版本满足率 - {rate === null ? "—" : `${rate}%`} -
    -
    -
    -
    -

    - 满足 {c.satisfied} / {total} · 需关注 {c.attention} · 下载中 {c.downloading} -

    -
    -

    - 今日 新计划 {a.today.plansCreated} · 提交下载 {a.today.downloadsSubmitted} · 入库文件 {a.today.filesImported} · - 升级 {a.today.upgrades} -

    -
    - ); -} - -// ─── 计划带:自动任务与巡检分别计数,链到计划页对应节 ─── -function PlanCount({ - to, - label, - icon: Icon, - children, -}: { - to: string; - label: string; - icon: LucideIcon; - children: ReactNode; -}) { - return ( - - - - {label} - - - {children} - - - ); -} - -function Num({ children, tone }: { children: ReactNode; tone?: "destructive" }) { - return ( - - {children} - - ); -} - -function PlanStrip() { - const board = useLiveSchedules(); - const s = planStats(board.data?.items ?? []); - return ( -
    调度器未在线 : null - } - > - {board.isPending ? ( - - ) : board.isError ? ( - board.refetch()} /> - ) : ( -
    - - - 启用 {s.auto.enabled} / {s.auto.total} - - - 失败 {s.auto.failed} - - - - - 启用 {s.checks.enabled} / {s.checks.total} - - - 近 7 天 {formatUsd(s.checks.weekCost)} - - -
    - )} -
    - ); -} - -// ─── 最近完成 / 失败 ─── -function RecentRuns() { - const done = useEngineRunsPage({ status: "completed" }, 6); - const failed = useEngineRunsPage({ status: "failed" }, 6); - const rows = [...(done.data?.items ?? []), ...(failed.data?.items ?? [])] - .sort((x, y) => (y.finishedAt ?? y.createdAt).localeCompare(x.finishedAt ?? x.createdAt)) - .slice(0, 6); - const loading = done.isPending || failed.isPending; - const err = done.error ?? failed.error; - return ( -
    - {loading ? ( - - ) : err ? ( - { - void done.refetch(); - void failed.refetch(); - }} - /> - ) : rows.length === 0 ? ( - +
    + {rows.length === 0 ? ( + ) : (
      - {rows.map((r) => { - const s = engineRunStatus(r); - return ( - - - {runTitle(r)} - - {workflowLabel(r.workflowName)} · {formatRelative(r.finishedAt)} + {rows.map((r) => ( + + + {r.title ?? r.targetPath?.split("/").pop() ?? "未关联作品"} + {r.status === "partial" ? ( + + {r.counts.succeeded}/{r.counts.total} 个文件入库 - - {s.label} - - ); - })} + ) : null} + + {formatRelative(r.finishedAt)} + + ))}
    )} + {today ?

    今天入库 {today} 个文件

    : null}
    ); } -// ─── 来源健康:站点(含账号状态)+ Feed / 下载器 / 媒体服务器,有问题的排前 ─── -const ACCOUNT: Record = { - unconfigured: { label: "未配置账号", tone: "neutral" }, - unknown: { label: "账号未验证", tone: "neutral" }, - active: { label: "账号正常", tone: "success" }, - invalid: { label: "账号失效", tone: "destructive" }, - expired: { label: "登录过期", tone: "warning" }, - challenge: { label: "需人工验证", tone: "warning" }, -}; - -function SourcesSection({ s }: { s: Dashboard["sources"] }) { - const sites = useSitesWithAccounts(); - const siteRows = (sites.data?.items ?? s.sites).map((x) => { - const acc = "accountStatus" in x && x.accountStatus ? ACCOUNT[x.accountStatus] : undefined; - return { - id: x.id, - kind: "站点", - name: x.name, - enabled: x.enabled, - health: "healthStatus" in x ? x.healthStatus : x.health, - account: acc, - to: `/console/sites/${x.id}`, - }; - }); - const rows = [ - ...siteRows, - ...s.feeds.map((x) => ({ - id: x.id, - kind: "RSS", - name: x.name, - enabled: x.enabled, - health: x.health, - account: undefined, - to: consolePath("sources"), - })), - ...s.downloaders.map((x) => ({ - id: x.id, - kind: "下载器", - name: x.name, - enabled: x.enabled, - health: x.health, - account: undefined, - to: settingsPath("downloaders"), - })), - ]; - const bad = (r: (typeof rows)[number]) => - (r.enabled && healthLabel(r.health).tone !== "success" ? 2 : 0) + - (r.account && r.account.tone !== "success" ? 1 : 0); - rows.sort((x, y) => bad(y) - bad(x)); - const accountIssues = siteRows.filter( - (r) => r.account && (r.account.tone === "destructive" || r.account.tone === "warning"), - ).length; - return ( -
    - {rows.length === 0 ? ( - - ) : ( - <> - {accountIssues ?

    {accountIssues} 个站点的账号需要处理

    : null} -
      - {rows.slice(0, 7).map((r) => { - const h = r.enabled ? healthLabel(r.health) : healthLabel("disabled"); - return ( - - - - {r.kind} - {r.name} - - {r.account ? ( - - {r.account.label} - - ) : null} - - {h.label} - - ); - })} -
    - - )} -
    - ); -} - -// ─── 运行状态摘要(/system/health) ─── -const LEVEL: Record = { - ok: { label: "正常", tone: "success" }, - warn: { label: "需要关注", tone: "warning" }, - critical: { label: "需要处理", tone: "destructive" }, - unknown: { label: "未知", tone: "neutral" }, -}; - -function StatusSummary() { - const h = useSystemHealth(30_000); - const d = h.data; - const stale = d?.processes.filter((p) => p.stale) ?? []; - const alive = (d?.processes.length ?? 0) - stale.length; - const mountsBad = d?.mounts.filter((m) => m.status !== "online").length ?? 0; - const lv = LEVEL[d?.level ?? "unknown"] ?? LEVEL.unknown; - return ( -
    {lv.label} : null} - > - {h.isPending ? ( - - ) : h.isError ? ( - h.refetch()} /> - ) : d ? ( -
    - - 进程 {alive} 在线 - {stale.length ? ( - - {" "} - · {stale.length} 个无心跳({stale.map((p) => `${p.processKind} ${p.pid}`).join("、")}) - - ) : null} - - - 待执行 {d.engine.pending} - {d.engine.oldestPendingAgeSec ? ` · 最久 ${formatDuration(d.engine.oldestPendingAgeSec)}` : ""} - - - 24 小时失败率{" "} - - {d.engine.failureRate24h === null ? "—" : `${(d.engine.failureRate24h * 100).toFixed(1)}%`} - - - - 数据库{" "} - {d.db.latencyMs === null ? "—" : `${d.db.latencyMs} 毫秒`} - - {mountsBad ? {mountsBad} 个挂载根不可用 : null} -
    - ) : null} -
    - ); -} - -function CacheSection({ c }: { c: Dashboard["cache"] }) { - const volumeLabel = useVolumeLabel(); - return ( -
    - {c.volumes.length === 0 ? ( - - ) : ( -
      - {c.volumes.map((v) => { - const pct = v.usedPercent; - const over = pct !== null && pct >= v.hardThresholdPercent; - const near = pct !== null && pct >= v.resumePercent; - return ( -
    • -
      - {volumeLabel(v)} - - {pct === null ? "用量未知" : `${pct.toFixed(0)}%${over ? " 已达阈值" : ""}`} - -
      -
      -
      - -
      -

      - 阈值 {v.hardThresholdPercent}% / 恢复 {v.resumePercent}% · 在途 {formatBytes(v.pendingBytes)} · 可清理{" "} - {formatBytes(v.cleanableBytes)} · 保留期锁定 {formatBytes(v.retentionLockedBytes)} - {v.pausedByThreshold ? ` · 因阈值暂停 ${v.pausedByThreshold} 个` : ""} -

      - {v.notes.map((n) => ( -

      - {n} -

      - ))} -
    • - ); - })} -
    - )} - - 调整阈值与保留天数 - -
    - ); -} - -function LiveIndicator() { - const s = useSseStatus(); - const tone = s === "open" ? "success" : s === "reconnecting" ? "warning" : "neutral"; - const label = s === "open" ? "实时" : s === "reconnecting" ? "重连中" : "连接中"; - return {label}; -} - export default function Home() { const q = useLiveDashboard(); useLiveTasks(); @@ -669,21 +329,15 @@ export default function Home() { <> - - {q.data ? ( - - 更新于 {formatRelative(q.data.generatedAt)} - - ) : null} - + q.data ? ( + 更新于 {formatRelative(q.data.generatedAt)} + ) : null } /> {q.isPending ? (
    - {["a", "b", "c", "d", "e", "f"].map((k) => ( + {["a", "b", "c"].map((k) => (
    @@ -693,16 +347,11 @@ export default function Home() { q.refetch()} /> ) : ( <> - +
    - + - - - - - - +
    )} diff --git a/apps/web/app/routes/console/schedules.tsx b/apps/web/app/routes/console/schedules.tsx index ce6e0f0d..455260a8 100644 --- a/apps/web/app/routes/console/schedules.tsx +++ b/apps/web/app/routes/console/schedules.tsx @@ -18,7 +18,6 @@ export default function SchedulesPage() { <> qc.invalidateQueries({ queryKey: qk.all("engine") })}> diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 9afeda4d..5ff2b00d 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -803,14 +803,14 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [ { id: "automation", label: "自动化", - description: "总开关、缓存水位、每日预算、保留天数", + description: "总开关、下载盘、每日预算、保留天数", legacyTabs: [{ tab: "automation" }, { tab: "storage", anchor: "cache" }], anchors: [ { id: "switches", label: "开关" }, - { id: "cache", label: "缓存水位" }, + { id: "cache", label: "下载盘" }, { id: "budget", label: "每日预算" }, { id: "retention", label: "保留" }, - { id: "storage", label: "存储与保留" }, + { id: "advanced", label: "高级" }, ], fields: [ "automation.enabled", From 5a717e414cf1d87c647f53facf58c7466366d62b Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Sep 2026 20:52:48 +0800 Subject: [PATCH 04/10] =?UTF-8?q?feat(web):=20=E6=99=BA=E8=83=BD=E4=BD=93?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=8E=BB=E5=99=AA=E2=80=94=E2=80=94=E5=8D=95?= =?UTF-8?q?=E4=B8=80=E5=85=8D=E8=B4=A3=E3=80=81=E7=9F=AD=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E3=80=81=E6=8A=80=E6=9C=AF=E4=BF=A1=E6=81=AF=E6=8A=98=E5=8F=A0?= =?UTF-8?q?(DEAI-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 「未经核查」只在最终汇报左栏标题保留一处,删「仅供参考」与数据来源说明; 右栏改叫「实际结果」,去掉「运行已完成」「工具成功 N」徽章,计数只列非零项, 状态值用中文标签,不再显示计划 / 记录短 ID - 工具卡片只标会改数据的动作和中高风险;能力 ID、scope、幂等、耗时、调用 ID、 schema 入口收进技术信息;拒绝说明改成一句「要你批准」 - 用量只显示费用与时长,工具调用数、token、模型、会话 ID 进技术信息(超限时照常显示) - 会话标题 shortGoalTitle:去上下文前缀、UUID、JSON 参数,能力 ID 换中文名,取第一句,40 字截断 - 入口去口号:删「交代目标,边做边汇报」与抽屉里的重复快捷键说明 - 对话组件结构不动 Co-Authored-By: Claude Opus 5.5 --- .../components/agent/agent-conversation.tsx | 231 +++++++++++------- .../web/app/components/agent/agent-drawer.tsx | 15 +- .../components/agent/agent-helpers.test.ts | 19 +- .../app/components/agent/agent-run-panel.tsx | 18 +- .../components/agent/agent-session-list.tsx | 25 +- apps/web/app/components/agent/agent-store.ts | 27 ++ .../components/agent/agent-subagent-tree.tsx | 6 +- .../app/components/console/agent-launcher.tsx | 14 +- apps/web/app/components/console/nav.ts | 2 +- apps/web/app/routes/console/agent.$runId.tsx | 4 +- apps/web/app/routes/console/agent.tsx | 5 +- 11 files changed, 223 insertions(+), 143 deletions(-) diff --git a/apps/web/app/components/agent/agent-conversation.tsx b/apps/web/app/components/agent/agent-conversation.tsx index 4495debf..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} +
    + +
    ); } @@ -772,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"} + /> +
    + ) : ( + + )}
    ); } @@ -946,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}`, })), ] @@ -975,7 +1026,6 @@ export function FinalReport({ {said ? : "(没有文字汇报)"} -

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

    @@ -984,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], @@ -1004,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} +
    • ))}
    @@ -1036,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 2a5d498d..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"; @@ -108,7 +108,7 @@ function HistoryRunView({ runId }: { runId: string }) { 返回列表

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

    , @@ -196,7 +196,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 ( @@ -231,9 +231,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b 智能体 {count ? : null} - - {count ? `${count} 个会话进行中` : "交代目标,边做边汇报"} - + {count ? {count} 个会话进行中 : null} {count ? ( diff --git a/apps/web/app/components/console/nav.ts b/apps/web/app/components/console/nav.ts index 9fd2307f..3419cd53 100644 --- a/apps/web/app/components/console/nav.ts +++ b/apps/web/app/components/console/nav.ts @@ -64,7 +64,7 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/agent", label: "智能体会话", icon: Bot, - description: "交代目标,智能体在授权内执行并汇报", + description: "给智能体一个目标,看它做了什么", }, ], }, diff --git a/apps/web/app/routes/console/agent.$runId.tsx b/apps/web/app/routes/console/agent.$runId.tsx index 82026097..b4a18a7f 100644 --- a/apps/web/app/routes/console/agent.$runId.tsx +++ b/apps/web/app/routes/console/agent.$runId.tsx @@ -3,7 +3,7 @@ import { Link, useParams } from "react-router"; import { agentMockEnabled, useAgentRun } from "~/api/agent.ts"; import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx"; import { AgentSessionList } from "~/components/agent/agent-session-list.tsx"; -import { splitContextGoal } from "~/components/agent/agent-store.ts"; +import { shortGoalTitle } from "~/components/agent/agent-store.ts"; import { PageHeader } from "~/components/console/page-header.tsx"; import { Button } from "~/components/ui/button.tsx"; @@ -21,7 +21,7 @@ export default function AgentRunPage() { description={ run.data ? ( - {splitContextGoal(run.data.run.goal).text} + {shortGoalTitle(run.data.run.goal)} ) : ( "加载中…" diff --git a/apps/web/app/routes/console/agent.tsx b/apps/web/app/routes/console/agent.tsx index 4ca2f346..7d75249b 100644 --- a/apps/web/app/routes/console/agent.tsx +++ b/apps/web/app/routes/console/agent.tsx @@ -15,10 +15,7 @@ export default function AgentPage() { return ( <> - + {mock ? (
    From 57ce40168d4f5e6a70d9d636746c96812a84dc94 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Sep 2026 20:58:38 +0800 Subject: [PATCH 05/10] =?UTF-8?q?refactor(web):=20=E6=8A=80=E6=9C=AF?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E6=8A=98=E5=8F=A0=E9=93=BA=E5=88=B0=E5=AA=92?= =?UTF-8?q?=E4=BD=93=E5=BA=93=20/=20=E5=AE=A1=E8=AE=A1=20/=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E7=8A=B6=E6=80=81,=E6=B8=85=E7=90=86=E6=AD=A3?= =?UTF-8?q?=E5=B8=B8=E6=80=81=E5=BE=BD=E7=AB=A0=E4=B8=8E=E8=A7=A3=E9=87=8A?= =?UTF-8?q?=E5=9E=8B=E9=A1=B5=E6=8F=8F=E8=BF=B0(DEAI-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 媒体库详情:作品 UUID 进技术信息;「实际路径与库根」改「文件位置」,正常态(已核验、 可访问、主位置)不加徽章;外部身份显示「TMDB 90228 · 你确认的 / 自动匹配」,修订号进悬停; 元数据字段来源默认收起(「显示来源」,跟随界面设置);别名默认只列中英文与手动加的; 「内容单元」→「季与集」,「临时单元」→「编号待确认」;侧栏与页标题统一「媒体库」 - 审计:动作列不再重复英文码,对象列显示中文类型 / 能力名,ID 只在详情;补 capability.denied - 运行状态:进程、已下线实例、趋势图与最大表收进折叠区;图例同名序列加序号,不再拼接 - 站点卡:账号正常与登录方式不加徽章,健康不显示毫秒,限流移出卡片 - 文件管理:目录「正常」徽章与重复的剩余空间去掉;已登记 → 已入库,临时文件 → 未完成 - 规则:「修订」列改「最后修改」;不支持的关键字合成一行;页签改「默认规则 / 自动收新作品 / 已忽略」; 默认规则说明去掉「带层」等算法描述 - 站点 Passkey 与登录用通行密钥区分命名;命令面板提示去重 - 删解释型页描述(审计、搜索、下载、下载计划、RSS、规则、媒体库、运行状态、站点、任务、计划、整理) Co-Authored-By: Claude Opus 5.5 --- apps/server/src/modules/search/actions.ts | 2 +- apps/web/app/components/console/nav.test.ts | 4 +- apps/web/app/components/console/nav.ts | 18 +-- apps/web/app/components/files/dialogs.tsx | 2 +- apps/web/app/components/files/dir-tree.tsx | 10 +- apps/web/app/components/files/file-detail.tsx | 2 +- apps/web/app/components/files/model.test.ts | 4 +- apps/web/app/components/files/model.ts | 17 +-- apps/web/app/components/rules/group-list.tsx | 8 +- apps/web/app/components/rules/tabs.tsx | 23 ++-- .../app/components/search/command-palette.tsx | 7 +- .../components/settings/account-passkeys.tsx | 2 +- .../components/sites/site-accounts-tab.tsx | 2 +- apps/web/app/components/sites/site-card.tsx | 31 +---- apps/web/app/components/sites/site-fields.ts | 2 +- .../app/components/status/history-chart.tsx | 20 ++- .../app/components/status/storage-section.tsx | 97 +++++++------- .../subscriptions/coverage-matrix.tsx | 4 +- apps/web/app/routes/console/audit.tsx | 38 ++++-- .../app/routes/console/downloads.plans.tsx | 2 +- apps/web/app/routes/console/downloads.tsx | 1 - apps/web/app/routes/console/files.tsx | 2 +- apps/web/app/routes/console/inbox.tsx | 2 +- apps/web/app/routes/console/library.$id.tsx | 118 +++++++++++------- apps/web/app/routes/console/library.tsx | 5 +- apps/web/app/routes/console/rules.tsx | 9 +- apps/web/app/routes/console/search.tsx | 2 +- apps/web/app/routes/console/sites.tsx | 1 - apps/web/app/routes/console/sources.tsx | 1 - apps/web/app/routes/console/status.tsx | 24 ++-- packages/contracts/src/settings.ts | 2 +- 31 files changed, 253 insertions(+), 209 deletions(-) diff --git a/apps/server/src/modules/search/actions.ts b/apps/server/src/modules/search/actions.ts index 1eea6973..00c0d13e 100644 --- a/apps/server/src/modules/search/actions.ts +++ b/apps/server/src/modules/search/actions.ts @@ -34,7 +34,7 @@ export const SEARCH_ACTIONS: readonly SearchAction[] = [ page("organize", "整理", "整理记录", "/console/organize", ["organize", "入库", "历史"]), page("files", "整理", "文件管理", "/console/files", ["files", "文件"]), page("trash", "整理", "回收站", "/console/files/trash", ["trash", "删除"]), - page("library", "整理", "媒体概览", "/console/library", ["library", "媒体库", "作品"]), + page("library", "整理", "媒体库", "/console/library", ["library", "媒体库", "作品"]), page("settings", "系统", "设置", "/console/settings", ["settings", "配置", "tmdb", "下载器"]), page("schedules", "概览", "定时任务", "/console/tasks?tab=plan", ["schedule", "cron", "巡检", "计划"]), page("audit", "系统", "审计", "/console/audit", ["audit", "日志"]), diff --git a/apps/web/app/components/console/nav.test.ts b/apps/web/app/components/console/nav.test.ts index ed954d18..08e2d01a 100644 --- a/apps/web/app/components/console/nav.test.ts +++ b/apps/web/app/components/console/nav.test.ts @@ -22,7 +22,7 @@ describe("控制台导航", () => { ["首页", "任务", "计划", "智能体会话"], ["站点", "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 3419cd53..5405f755 100644 --- a/apps/web/app/components/console/nav.ts +++ b/apps/web/app/components/console/nav.ts @@ -46,19 +46,19 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console", label: "首页", icon: House, - description: "正在进行、需要处理、计划、最近结果与运行状态", + description: "在忙什么、要我处理什么、最近入库了什么", }, { to: "/console/tasks", label: "任务", icon: ListTodo, - description: "每次运行都是一条任务:定时、事件、手动、智能体触发都在这里", + description: "最近处理了什么、有没有失败", }, { to: "/console/schedules", label: "计划", icon: CalendarCog, - description: "定时任务的开关、间隔和下次运行:自动任务与智能体巡检", + description: "智能体巡检与自动任务的开关和间隔", }, { to: "/console/agent", @@ -75,7 +75,7 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/sites", label: "站点", icon: Globe, - description: "站点、凭证、账号数据与健康;资源、抓取与 RSS", + description: "站点账号、资源与 RSS", }, { 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: "作品、季与集、未识别资源", }, ], }, @@ -130,7 +130,7 @@ export const NAV_GROUPS: NavGroup[] = [ to: "/console/status", label: "运行状态", icon: Gauge, - description: "进程心跳、事件循环、数据库、磁盘与挂载根", + description: "服务是否正常、磁盘与数据库", }, ], }, diff --git a/apps/web/app/components/files/dialogs.tsx b/apps/web/app/components/files/dialogs.tsx index 4bedbd67..36a4ab1b 100644 --- a/apps/web/app/components/files/dialogs.tsx +++ b/apps/web/app/components/files/dialogs.tsx @@ -1049,7 +1049,7 @@ export function RefreshMetadataDialog({ items, onClose }: { items: FileEntry[]; {s.reason === "not_registered" ? "没有已入库文件" : s.reason === "no_entity" - ? "不属于任何已登记作品" + ? "不属于媒体库里的作品" : s.reason === "not_found" ? "作品不存在" : s.reason} diff --git a/apps/web/app/components/files/dir-tree.tsx b/apps/web/app/components/files/dir-tree.tsx index 6e60dfb0..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.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 c492f0cf..18e6a8de 100644 --- a/apps/web/app/components/files/file-detail.tsx +++ b/apps/web/app/components/files/file-detail.tsx @@ -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), }, 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 0850ea1c..ea0457c2 100644 --- a/apps/web/app/components/files/model.ts +++ b/apps/web/app/components/files/model.ts @@ -19,14 +19,14 @@ export function entryStatusView(e: Pick , hideBelow: "lg" }, { - key: "rev", - header: "修订", + key: "updated", + header: "最后修改", cell: (g) => ( - - r{g.revision} + + {relTime(g.updatedAt)} ), hideBelow: "md", diff --git a/apps/web/app/components/rules/tabs.tsx b/apps/web/app/components/rules/tabs.tsx index b0ced928..224ca371 100644 --- a/apps/web/app/components/rules/tabs.tsx +++ b/apps/web/app/components/rules/tabs.tsx @@ -339,21 +339,26 @@ export function AtomsTab() { onRowClick={(a) => setDialog({ open: true, atom: a })} empty={{ icon: FlaskConical, title: "还没有自定义条件", description: "内置关键字覆盖不到时再新建。" }} /> - + {keywords.isPending ? ( ) : keywords.isError ? ( keywords.refetch()} /> ) : (
      - {builtins.map((k) => ( -
    • - {k.id} - - {k.supported ? k.description || k.name : "不支持,规则串里出现会报错"} - + {builtins + .filter((k) => k.supported) + .map((k) => ( +
    • + {k.id} + {k.description || k.name} +
    • + ))} + {builtins.some((k) => !k.supported) ? ( +
    • + 另有 {builtins.filter((k) => !k.supported).length} 个关键字暂不支持,写进规则串会报错
    • - ))} + ) : null}
    )}
    @@ -385,7 +390,7 @@ export function DefaultsTab({ groups }: { groups: RuleGroup[] | undefined }) {

    - 订阅和收录版本没指定规则组时用这里的链。链上的组按顺序串联,任一组拒绝即拒绝,优先级取最后一个带层的组。 + 订阅和收录版本没指定规则组时用这里的链。按顺序检查,任一组不通过就不下载。

    {dirty ? 未保存 : null} diff --git a/apps/web/app/components/search/command-palette.tsx b/apps/web/app/components/search/command-palette.tsx index 799284e9..6549f18c 100644 --- a/apps/web/app/components/search/command-palette.tsx +++ b/apps/web/app/components/search/command-palette.tsx @@ -1,5 +1,5 @@ import { type SearchAction, type SearchWork, tmdbImageUrl } from "@umt/contracts"; -import { ArrowRight, CornerDownLeft, Film, Globe, LoaderCircle, type LucideIcon, Search } from "lucide-react"; +import { CornerDownLeft, Film, Globe, LoaderCircle, type LucideIcon, Search } from "lucide-react"; import { type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react"; import { Link, useNavigate } from "react-router"; import { useSearch } from "~/api/search.ts"; @@ -111,7 +111,6 @@ function PaletteBody({ initial }: { initial: string }) { 查看「{term}」的全部结果 ), - meta: "作品卡上可以选站点搜资源", hint: "打开", run: () => go(searchPagePath(term)), }); @@ -254,10 +253,6 @@ function PaletteBody({ initial }: { initial: string }) {
    ↑↓ 选择 ↵ 打开 - - - 搜资源在作品详情页 -
    ); diff --git a/apps/web/app/components/settings/account-passkeys.tsx b/apps/web/app/components/settings/account-passkeys.tsx index cbfa6734..cb8aae6b 100644 --- a/apps/web/app/components/settings/account-passkeys.tsx +++ b/apps/web/app/components/settings/account-passkeys.tsx @@ -196,7 +196,7 @@ export function PasskeyBlock({ disabled }: { disabled: boolean }) { {body} diff --git a/apps/web/app/components/sites/site-accounts-tab.tsx b/apps/web/app/components/sites/site-accounts-tab.tsx index 5f4e747f..2eddd0d6 100644 --- a/apps/web/app/components/sites/site-accounts-tab.tsx +++ b/apps/web/app/components/sites/site-accounts-tab.tsx @@ -239,7 +239,7 @@ function PasskeyForm({ site, label, has }: { site: SiteListItem; label: string; } }} > -

    Passkey

    +

    站点 Passkey(RSS 与下载链接用)

    用于 RSS 和下载链接,不参与登录。{has ? "已保存;再次保存会替换。" : "还没有保存。"}

    diff --git a/apps/web/app/components/sites/site-card.tsx b/apps/web/app/components/sites/site-card.tsx index d93c4ef7..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"; @@ -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,14 +292,14 @@ export function SiteCard({ site, checks, onDialog }: Props) {
    {relTime(acc?.lastLoginAt)}
    -
    健康检测
    +
    状态
    {health.text}
    -
    抓取
    +
    资源更新
    {!site.snapshotEnabled ? "未开启" @@ -323,10 +308,6 @@ export function SiteCard({ site, checks, onDialog }: Props) { : "还没抓取过"}
    -
    -
    限流
    -
    {rateLimitText(site.rateLimit)}
    -