Merge branch 'deai2'

Co-Authored-By: Claude Fable 5.1 <[email protected]>

# Conflicts:
#	apps/server/src/modules/acquisition-organize/services/organize.ts
#	apps/web/app/routes/console/sites.$siteId.tsx
This commit is contained in:
root committed 2026-09-29 21:26:16 +08:00
commit 12bc4664b6
247 files changed
+2658 -2148

No files matched your search

@@ -196,7 +196,7 @@ export function PasskeyBlock({ disabled }: { disabled: boolean }) {
<SettingsBlock
section="account"
anchor="passkeys"
title="Passkey"
title="通行密钥(Passkey)"
description="用指纹、面容或安全钥匙直接登录,不用输密码和验证码。可以登记多把。"
>
{body}
@@ -50,17 +50,17 @@ const RISK: Record<RiskLevel, { label: string; tone: StatusTone }> = {
const AUTH: Record<CapabilityAuthState, { label: string; tone: StatusTone; hint: string }> = {
read_only: { label: "无需授权", tone: "neutral", hint: "只读或预览,不落变更,Key 有对应 scope 即可调用" },
granted: { label: "有持续授权", tone: "success", hint: "Agent、MCP、API Key 调用按持续授权放行" },
granted: { label: "有持续授权", tone: "success", hint: "智能体、MCP、API Key 调用按持续授权放行" },
needs_approval: {
label: "需审批",
tone: "warning",
hint: "没有覆盖它的持续授权,Agent、MCP、API Key 调用需要单次审批",
hint: "没有覆盖它的持续授权,智能体、MCP、API Key 调用需要单次审批",
},
agent_only: { label: "仅 Agent 会话", tone: "neutral", hint: "只在 Agent 会话内可用,不经 MCP 暴露" },
agent_only: { label: "仅智能体会话", tone: "neutral", hint: "只在智能体会话内可用,不经 MCP 暴露" },
};
export const CALLER_LABEL: Record<CapabilityCaller, string> = {
agent: "内置 Agent",
agent: "内置智能体",
mcp: "MCP",
rest: "REST",
other: "其他",
@@ -86,7 +86,7 @@ function McpServerBlock() {
section="agent"
anchor="mcp"
title="MCP 服务端"
description="外部 Agent 用 API Key 经 MCP 调用同一套能力。授权、审计、幂等与控制台一致,有副作用的工具没有持续授权或单次审批时被拒绝。"
description="外部程序用 API Key 经 MCP 调用和智能体同一套能力,授权与审计规则相同。"
>
{status.isPending ? (
<LoadingState rows={3} />
@@ -318,13 +318,13 @@ function CapabilitiesBlock() {
section="agent"
anchor="capabilities"
title="能力清单"
description="Agent、MCP、REST 共用这些能力。调用数与失败率为最近 7 天,含被拒绝的调用。"
description="智能体、MCP、REST 共用这些能力。调用数与失败率为最近 7 天,含被拒绝的调用。"
plain
>
{automationOff ? (
<p className="mb-3 rounded-lg border border-warning/30 bg-warning/10 px-3 py-2 text-caption text-warning">
{tools.data?.readOnlyReconcile ? "只读对账模式" : "自动化总开关已关闭"}
:执行类能力对 Agent、MCP、API Key 一律拒绝,持续授权暂不生效。
:执行类能力对智能体、MCP、API Key 一律拒绝,持续授权暂不生效。
</p>
) : null}
<DataTable
@@ -14,7 +14,7 @@ export const WEBSEARCH_CARD: CardSpec = {
anchor: "websearch",
title: "网页搜索",
description:
"开启后 Agent 可以用 web.search 搜索站外公开信息,用 web.fetch 读取搜索结果里的网页。结果和正文按不可信数据交给模型,web.fetch 不能读内网地址和结果以外的链接。",
"开启后智能体可以用 web.search 搜索站外公开信息,用 web.fetch 读取搜索结果里的网页。结果和正文按不可信数据交给模型,web.fetch 不能读内网地址和结果以外的链接。",
fields: [
{
name: "agent.webSearch.provider",
@@ -63,7 +63,7 @@ export const WEBSEARCH_CARD: CardSpec = {
name: "agent.webSearch.maxResults",
label: "每次条数",
kind: "number",
hint: "1–10,Agent 可以要更少",
hint: "1–10,智能体可以要更少",
visible: (v) => v["agent.webSearch.provider"] !== "none",
},
{
+51 -46
View File
@@ -1,7 +1,7 @@
import { AgentToolsBlock } from "./agent-tools.tsx";
import { WEBSEARCH_CARD } from "./agent-websearch.tsx";
import { PiUpstreamsBlock } from "./pi-upstreams.tsx";
import type { FieldValue } from "./resource-section.tsx";
import type { FieldSpec, FieldValue } from "./resource-section.tsx";
import { num, str } from "./resource-section.tsx";
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
@@ -85,7 +85,7 @@ const BUDGET_CARD: CardSpec = {
const INVESTIGATE_CARD: CardSpec = {
anchor: "investigate",
title: "自动调查",
description: "待处理事项(识别有歧义、下载失败等)创建时,自动发起一次有界调查。",
description: "待处理事项(没识别出作品、下载失败等)创建时,自动发起一次有界调查。",
fields: [
{ name: "agent.autoInvestigate", label: "自动调查", kind: "switch", wide: true },
{
@@ -140,53 +140,58 @@ const INVESTIGATE_CARD: CardSpec = {
}),
};
const DEV_ONLY_FIELDS = new Set(["agent.sessionDir", "agent.fakeScript"]);
const ADVANCED_CARD: CardSpec = {
anchor: "advanced",
title: "高级",
fields: [
{ name: "agent.goalPollSec", label: "目标轮询间隔(秒)", kind: "number", hint: "等下载、入库结束时的检查间隔" },
{
name: "agent.goalWaitHours",
label: "目标最长等待(小时)",
kind: "number",
hint: "超时后按「仍在等待」汇报结束",
},
{
name: "agent.optionsTimeoutSec",
label: "方案选择等待(秒)",
kind: "number",
hint: "Agent 给出方案后等你选择的最长时间",
},
{
name: "agent.optionsTimeoutAction",
label: "超时未选",
kind: "select",
options: [
{ value: "recommended", label: "自动选推荐项" },
{ value: "stop", label: "不选,Agent 收尾" },
],
},
{
name: "agent.approvalTimeoutSec",
label: "会话内审批有效期(秒)",
kind: "number",
hint: "步骤处的审批卡过期未批准,按拒绝处理",
},
{
name: "agent.sessionDir",
label: "会话目录",
kind: "text",
wide: true,
placeholder: "留空用 DATA_DIR/agent-sessions",
hint: "绝对路径,api 与 worker 都要可读写;有运行中的会话时不要改",
},
{
name: "agent.fakeScript",
label: "fake 脚本(JSON)",
kind: "textarea",
hint: "只对 fake 上游生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
},
],
// 会话目录与 fake 脚本是开发调试项,生产构建不显示(DEAI-2)
fields: (
[
{ name: "agent.goalPollSec", label: "目标轮询间隔(秒)", kind: "number", hint: "等下载、入库结束时的检查间隔" },
{
name: "agent.goalWaitHours",
label: "目标最长等待(小时)",
kind: "number",
hint: "超时后按「仍在等待」汇报结束",
},
{
name: "agent.optionsTimeoutSec",
label: "方案选择等待(秒)",
kind: "number",
hint: "智能体给出方案后等你选择的最长时间",
},
{
name: "agent.optionsTimeoutAction",
label: "超时未选",
kind: "select",
options: [
{ value: "recommended", label: "自动选推荐项" },
{ value: "stop", label: "不选,智能体收尾" },
],
},
{
name: "agent.approvalTimeoutSec",
label: "会话内审批有效期(秒)",
kind: "number",
hint: "步骤处的审批卡过期未批准,按拒绝处理",
},
{
name: "agent.sessionDir",
label: "会话目录",
kind: "text",
wide: true,
placeholder: "留空用 DATA_DIR/agent-sessions",
hint: "绝对路径,api 与 worker 都要可读写;有运行中的会话时不要改",
},
{
name: "agent.fakeScript",
label: "fake 脚本(JSON)",
kind: "textarea",
hint: "只对 fake 模型服务生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
},
] as FieldSpec[]
).filter((f) => import.meta.env.DEV || !DEV_ONLY_FIELDS.has(f.name)),
fromSettings: (s) => ({
"agent.goalPollSec": n(s.agent.goalPollSec),
"agent.goalWaitHours": n(s.agent.goalWaitHours),
@@ -25,10 +25,10 @@ import { SettingsBlock } from "./settings-block.tsx";
// ─── API Key ───
const SCOPES: { value: ApiScope; label: string; hint: string }[] = [
{ value: "read", label: "read", hint: "只读查询全部数据" },
{ value: "organize.read", label: "organize.read", hint: "只读整理记录,供外部增量同步" },
{ value: "organize.execute", label: "organize.execute", hint: "发起整理" },
{ value: "admin", label: "admin", hint: "全部管理权限,含改设置" },
{ value: "read", label: "只读 · read", hint: "只读查询全部数据" },
{ value: "organize.read", label: "读整理记录 · organize.read", hint: "只读整理记录,供外部增量同步" },
{ value: "organize.execute", label: "执行整理 · organize.execute", hint: "发起整理" },
{ value: "admin", label: "管理员 · admin", hint: "全部管理权限,含改设置" },
];
function CreatedKeyDialog({ plaintext, onClose }: { plaintext: string | null; onClose: () => void }) {
@@ -200,7 +200,7 @@ export function ApiKeysSection() {
checked={on}
onChange={() => setScopes((xs) => (on ? xs.filter((x) => x !== s.value) : [...xs, s.value]))}
/>
<code className="text-caption">{s.label}</code>
<span className="text-caption">{s.label}</span>
<span className="text-meta text-muted-foreground">{s.hint}</span>
</label>
);
@@ -250,7 +250,7 @@ function ScopeLegend() {
{SCOPES.map((s) => (
<div key={s.value} className="contents">
<dt>
<code className="rounded-tight bg-foreground/[0.06] px-1.5 py-0.5">{s.label}</code>
<span className="rounded-tight bg-foreground/[0.06] px-1.5 py-0.5">{s.label}</span>
</dt>
<dd className="text-muted-foreground">{s.hint}</dd>
</div>
+73 -26
View File
@@ -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),
@@ -115,7 +116,7 @@ const STORAGE_BUDGET_CARD: CardSpec = {
gbField("pgDataWarnGb", "数据库告警线", "不高于数据库预算"),
gbField("imagesGb", "图片缓存容量"),
gbField("backupsGb", "备份目录预算"),
gbField("sessionsEvidenceGb", "Agent 会话与截图证据预算"),
gbField("sessionsEvidenceGb", "智能体会话与截图证据预算"),
],
fromSettings: (s) => Object.fromEntries(BUDGET_KEYS.map((k) => [`storage.budgets.${k}`, n(s.storage.budgets[k])])),
toPatch: (v) => ({ storage: { budgets: pick(v, "storage.budgets", BUDGET_KEYS) } }),
@@ -136,17 +137,17 @@ const STORAGE_RETENTION_CARD: CardSpec = {
title: "流水数据保留",
description: "到期由定时清理任务分批删除。单位天,心跳为小时。",
fields: [
dayField("retentionDays", "observation", "Feed 观察"),
dayField("retentionDays", "observation", "RSS 抓取记录"),
dayField("retentionDays", "sseEvent", "实时事件"),
dayField("retentionDays", "heartbeatHours", "进程心跳(小时)"),
dayField("retentionDays", "taskRun", "任务运行记录"),
dayField("retentionDays", "agentEvent", "Agent 会话事件", "只删已结束的会话"),
dayField("retentionDays", "agentEvent", "智能体会话事件", "只删已结束的会话"),
dayField("retentionDays", "audit", "审计日志", "能力调用审计除外"),
dayField("retentionDays", "capabilityStats", "能力调用统计"),
dayField(
"retentionDays",
"identityPoolArchive",
"待识别池归档",
"未识别资源归档",
"未命中的条目这么久没有新证据就归档,仍可手动指定",
),
],
@@ -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 (
<section id={anchorId("automation", "advanced")} className="scroll-mt-20">
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
className="inline-flex items-center gap-1.5 text-lead font-semibold text-foreground"
>
高级
<ChevronDown
className={cn("size-4 text-muted-foreground transition-transform duration-base", open && "rotate-180")}
/>
</button>
<p className="mt-1 text-caption text-muted-foreground">只读对账、存储预算、数据保留和缓存有效期。一般不用改。</p>
{open ? <div className="mt-6 grid grid-cols-1 gap-8">{children}</div> : null}
</section>
);
}
function ReadOnlySwitch() {
const update = useUpdateSettings();
return (
<SettingsGate>
{(s) => (
<SettingsBlock section="automation" anchor="readonly" title="只读对账">
<ToggleRow
id="tg-readonly"
icon={<ShieldAlert className="h-4 w-4" />}
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, "切换失败")),
)
}
/>
</SettingsBlock>
)}
</SettingsGate>
);
}
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 ? "自动化已恢复" : "自动化已暂停")}
/>
<ToggleRow
id="tg-readonly"
icon={<ShieldAlert className="h-4 w-4" />}
tone={s.automation.readOnlyReconcile ? "warning" : "success"}
title={`只读对账 · ${s.automation.readOnlyReconcile ? "开启" : "关闭"}`}
description="从备份恢复后先开启:只核对库存与路径,不做自动写入。核对完再关闭。"
checked={s.automation.readOnlyReconcile}
disabled={update.isPending}
onChange={(v) => set({ readOnlyReconcile: v }, v ? "已开启只读对账" : "已关闭只读对账")}
/>
</div>
</SettingsBlock>
)}
@@ -263,9 +307,12 @@ export function AutomationSection() {
<SettingsCard section="automation" spec={CACHE_CARD} settings={s} />
<SettingsCard section="automation" spec={BUDGET_CARD} settings={s} />
<SettingsCard section="automation" spec={RETENTION_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_BUDGET_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_RETENTION_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_TTL_CARD} settings={s} />
<Advanced>
<ReadOnlySwitch />
<SettingsCard section="automation" spec={STORAGE_BUDGET_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_RETENTION_CARD} settings={s} />
<SettingsCard section="automation" spec={STORAGE_TTL_CARD} settings={s} />
</Advanced>
</>
)}
</SettingsGate>
@@ -97,7 +97,7 @@ export function BrowserProvidersBlock() {
section="sites"
anchor="browser"
title="浏览器服务"
description="原站视图和 Agent 打开站点网页用的 camofox-browser。部署自带的一台由环境变量登记(camofox-env)。"
description="打开站点网页用(原站视图、智能体)。随部署提供的一台显示为内置。"
plain
>
<ResourceSection<BrowserProviderView>
@@ -297,9 +297,8 @@ export function LibraryRootsTable() {
<ResourceSection<LibraryRoot>
hooks={{ list, create, update, remove } as never}
config={{
noun: "库根",
description:
"登记或改路径时会在目录里写哨兵文件 .uhd-root,据此判断库根是否在线;目录不存在或不可写时无法保存。",
noun: "媒体库目录",
description: "保存时会在目录里放一个标记文件,用来判断目录是否挂载;目录不存在或不可写时无法保存。",
deleteHint: "只删除登记,磁盘上的文件不动。",
columns: [
{
@@ -326,7 +325,7 @@ export function LibraryRootsTable() {
},
{ key: "health", header: "在线", cell: (r) => <Health status={r.healthStatus} at={r.lastHealthAt} /> },
],
rowActions: (r) => <TestButton label="检查哨兵" pending={test.isPending} run={() => test.mutateAsync(r.id)} />,
rowActions: (r) => <TestButton label="检查目录" pending={test.isPending} run={() => test.mutateAsync(r.id)} />,
fields: [
{ name: "name", label: "名称", kind: "text", required: true, placeholder: "例如:电影库" },
{ name: "mediaKind", label: "内容类型", kind: "select", options: MEDIA_KIND },
@@ -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 (
<SettingsBlock section="interface" anchor="display" title="显示">
<div className="flex items-start justify-between gap-4">
<div className="min-w-0">
<p className="text-sm font-medium text-foreground" id="ui-techinfo-label">
默认展开技术信息
</p>
<p className="mt-1 text-caption text-muted-foreground">
ID、hash、token 数、耗时和原始错误码平时收在「技术信息」里。开启后各页默认展开。
</p>
</div>
<Switch
aria-labelledby="ui-techinfo-label"
checked={prefs.techInfoExpanded}
onCheckedChange={(v) => setUiPref("techInfoExpanded", v)}
/>
</div>
</SettingsBlock>
);
}
+1 -1
View File
@@ -42,7 +42,7 @@ const ORGANIZE_CARD: CardSpec = {
export function LibrarySection() {
return (
<div className="grid grid-cols-1 gap-8">
<SettingsBlock section="library" anchor="roots" title="库根" plain>
<SettingsBlock section="library" anchor="roots" title="媒体库目录" plain>
<LibraryRootsTable />
</SettingsBlock>
<SettingsGate>{(s) => <SettingsCard section="library" spec={ORGANIZE_CARD} settings={s} />}</SettingsGate>
@@ -211,21 +211,20 @@ function ImageCacheStatus() {
const IDENTITY_CARD: CardSpec = {
anchor: "identity",
title: "资源识别",
description:
"缺省只在资源命中订阅或本地作品别名、新作品收录规则前置条件通过、你手动识别或搜索时查 TMDB;其余进待识别池,不进待处理。",
description: "默认只在资源命中订阅、作品别名或新作品规则,或你手动识别和搜索时查 TMDB;其余放进未识别,不进待处理。",
fields: [
{
name: "identity.eagerTmdbLookup",
label: "每条新资源都查 TMDB",
kind: "switch",
hint: "旧行为。每条资源约 1–2 次搜索 + 命中时 1 次详情(识别缓存命中为 0);每小时 100 条的 RSS 约每天 2400–7200 次请求,站点独有的短剧 / 综艺多半查不到",
hint: "每条新资源都查一次 TMDB。RSS 量大时每天可能上千次请求,站点独有的短剧和综艺多半查不到",
wide: true,
},
{
name: "identity.autoEnrich",
label: "TMDB 查不到时自动补全元数据",
kind: "switch",
hint: "资源命中订阅或新作品收录规则、TMDB 却没有结果时,自动搜索网页出一份补全建议(每条资源一次,不建作品)。每次约 1–2 次网页搜索、最多读 3 个网页、1 次 Agent 上游调用",
hint: "资源命中订阅或新作品规则、TMDB 却查不到时,搜索网页出一份补全建议(每条资源一次,不建作品)。每次最多读 3 个网页,调用一次模型服务",
wide: true,
},
{
+5 -5
View File
@@ -120,7 +120,7 @@ function RootNaming() {
<SettingsBlock
section="library"
anchor="naming-roots"
title="库根命名覆盖"
title="媒体库目录命名覆盖"
description="按类型覆盖全局模板;留空的类型沿用全局。"
>
{roots.isPending ? (
@@ -128,12 +128,12 @@ function RootNaming() {
) : roots.isError ? (
<ErrorState plain error={roots.error} onRetry={() => roots.refetch()} />
) : !root ? (
<p className="text-caption text-muted-foreground">还没有库根。先在上方「库根」添加。</p>
<p className="text-caption text-muted-foreground">还没有媒体库目录。先在上方「媒体库目录」添加。</p>
) : (
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
<Select value={root.id} onValueChange={setRootId}>
<SelectTrigger size="sm" aria-label="库根" className="max-w-full min-w-48">
<SelectTrigger size="sm" aria-label="媒体库目录" className="max-w-full min-w-48">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
@@ -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 }) {
<div className="flex flex-wrap items-center gap-2">
{KINDS.map((k) => (
<StatusBadge key={k} tone={saved[k] ? "success" : "neutral"}>
{NAMING_KIND_LABEL[k]}:{saved[k] ? "库根覆盖" : "沿用全局"}
{NAMING_KIND_LABEL[k]}:{saved[k] ? "媒体库目录覆盖" : "沿用全局"}
</StatusBadge>
))}
</div>
@@ -104,7 +104,7 @@ export function PathMappingsPanel({
<p className="truncate">{m.remotePrefix}</p>
<p className="truncate text-muted-foreground">→ {m.localPrefix}</p>
{rootName(m.libraryRootId) ? (
<p className="font-sans text-muted-foreground">库根:{rootName(m.libraryRootId)}</p>
<p className="font-sans text-muted-foreground">媒体库目录:{rootName(m.libraryRootId)}</p>
) : null}
</div>
<StatusBadge tone={h.tone}>{h.label}</StatusBadge>
@@ -175,7 +175,7 @@ export function PathMappingsPanel({
disabled={create.isPending}
/>
<FieldControl
spec={{ name: "libraryRootId", label: "关联库根", kind: "select", options: rootOpts }}
spec={{ name: "libraryRootId", label: "关联媒体库目录", kind: "select", options: rootOpts }}
value={v.libraryRootId}
onChange={(x) => setV((s) => ({ ...s, libraryRootId: x }))}
disabled={create.isPending}
+1 -1
View File
@@ -22,7 +22,7 @@ const SOURCE_TEXT: Record<string, string> = { "stored credential": "已存凭证
/** 上游返回的多是英文原文:先给一句怎么办,原文留作排查 */
const ERROR_HINT: Record<string, string> = {
network: "连不上上游,检查 Base URL、端口和网络",
network: "连不上模型服务,检查 Base URL、端口和网络",
auth: "凭证被拒,检查 API Key 或重新订阅登录",
model: "模型不可用,换一个模型 id",
};
@@ -49,13 +49,13 @@ export function PiUpstreamsBlock() {
<SettingsBlock
section="agent"
anchor="upstream"
title="上游"
description="没指定上游的运行用默认上游。密钥和登录凭证保存后不回显。"
title="模型服务"
description="没指定模型服务的运行用默认模型服务。密钥和登录凭证保存后不回显。"
plain
actions={
<Button size="sm" onClick={() => setWizard(true)}>
<Plus />
新建上游
新建模型服务
</Button>
}
>
@@ -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={
<span className="flex flex-wrap items-center gap-2">
<UpstreamStatusBadge u={upstream} />
{upstream.isDefault ? <StatusBadge tone="success">默认上游</StatusBadge> : null}
{upstream.isDefault ? <StatusBadge tone="success">默认模型服务</StatusBadge> : null}
<span>
{provider?.name ?? upstream.provider} · {AUTH_LABEL[upstream.authKind]}
</span>
@@ -402,11 +402,13 @@ function UpstreamDrawer({
<ConfirmDialog
open={deleting}
onOpenChange={setDeleting}
title={`删除上游「${upstream.name}」?`}
title={`删除模型服务「${upstream.name}」?`}
description={
upstream.isDefault ? "它是默认上游,删除后没有默认上游;凭证一并删除,无法恢复。" : "凭证一并删除,无法恢复。"
upstream.isDefault
? "它是默认模型服务,删除后没有默认模型服务;凭证一并删除,无法恢复。"
: "凭证一并删除,无法恢复。"
}
confirmLabel="删除上游"
confirmLabel="删除模型服务"
destructive
onConfirm={async () => {
try {
@@ -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 (
<DetailSheet open={open} onOpenChange={(o) => !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 ? "已是默认模型服务" : "设为默认模型服务"}
</label>
<Button onClick={finish} disabled={update.isPending}>
{update.isPending ? <LoaderCircle className="animate-spin" /> : null}
+15 -1
View File
@@ -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<SettingsSectionId, { icon: LucideIcon; render: () => ReactNode
metadata: { icon: Tags, render: () => <MetadataSection /> },
agent: { icon: Bot, render: () => <AgentSection /> },
"api-keys": { icon: KeyRound, render: () => <ApiKeysSection /> },
interface: { icon: Monitor, render: () => <InterfaceSection /> },
account: { icon: Shield, render: () => <AccountSection /> },
};
+4 -4
View File
@@ -23,15 +23,15 @@ const DEFAULTS_CARD: CardSpec = {
{ name: "sites.defaultRateLimit.concurrency", label: "并发", kind: "number", hint: "同一站点同时进行的请求" },
{
name: "sites.defaultSnapshotIntervalSec",
label: "快照周期(分钟)",
label: "抓取周期(分钟)",
kind: "number",
hint: "站点开了定时快照但没设周期时使用;5–10080",
hint: "站点开了定时抓取但没设周期时使用;5–10080",
},
{
name: "sites.defaultSnapshotMaxPages",
label: "快照页数",
label: "抓取页数",
kind: "number",
hint: "定时和手动快照最多抓几页列表;站点定义更少时按站点;1–20",
hint: "定时和手动抓取最多抓几页列表;站点定义更少时按站点;1–20",
},
{
name: "sites.defaultHealthCheckIntervalSec",