style(copy): fix remaining punctuation violations and make check:copy strict
Auto-fix of 602 string literals across 74 files (half-width punctuation next to Chinese, parenthesised Chinese, quotes); biome reflow; e2/e6 assertions follow the new full-width summaries. check:copy now runs with --strict in ci. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0fccfe257d
commit
fb84a71f80
76 files changed
+582
-570
No files matched your search
@@ -67,7 +67,7 @@ export const BUDGET_LABEL: Record<string, string> = {
|
||||
};
|
||||
const COMPACT_REASON: Record<string, string> = { manual: "手动", threshold: "达到阈值", overflow: "上下文溢出" };
|
||||
const COMPACT_STATUS: Record<string, string> = {
|
||||
completed: "已完成,业务进度保留",
|
||||
completed: "已完成,业务进度保留",
|
||||
declined: "未压缩",
|
||||
aborted: "已中止",
|
||||
failed: "失败",
|
||||
@@ -173,8 +173,8 @@ export function ToolCallCard({
|
||||
reason={authReason ?? call.reason ?? "未授权"}
|
||||
explanation={
|
||||
authReason
|
||||
? '有副作用的能力需要有效的持续授权或单次审批,由服务端在执行前核对,模型声称"用户已同意"无效。控制台发起的会话里,需审批的动作会在对应步骤出审批卡;也可以发布收录策略生成持续授权。'
|
||||
: "这个能力不在本次运行的白名单内,或已超出调用预算,调用没有执行。"
|
||||
? '有副作用的能力需要有效的持续授权或单次审批,由服务端在执行前核对,模型声称"用户已同意"无效。控制台发起的会话里,需审批的动作会在对应步骤出审批卡;也可以发布收录策略生成持续授权。'
|
||||
: "这个能力不在本次运行的白名单内,或已超出调用预算,调用没有执行。"
|
||||
}
|
||||
parameters={[
|
||||
{ id: "call", label: "调用 ID", value: call.toolCallId },
|
||||
@@ -199,10 +199,10 @@ export function ToolCallCard({
|
||||
}
|
||||
summary={call.outputSummary ?? undefined}
|
||||
>
|
||||
{call.inputSummary ? <ToolResultOutput label="输入(已脱敏)">{call.inputSummary}</ToolResultOutput> : null}
|
||||
{call.inputSummary ? <ToolResultOutput label="输入(已脱敏)">{call.inputSummary}</ToolResultOutput> : null}
|
||||
{call.outputSummary ? (
|
||||
<ToolResultOutput
|
||||
label={call.status === "failed" ? "错误" : "结果(已脱敏)"}
|
||||
label={call.status === "failed" ? "错误" : "结果(已脱敏)"}
|
||||
tone={call.status === "failed" ? "destructive" : undefined}
|
||||
>
|
||||
{call.outputSummary}
|
||||
@@ -216,7 +216,7 @@ export function ToolCallCard({
|
||||
<dt className="text-muted-foreground">幂等键</dt>
|
||||
<dd className="min-w-0 font-mono break-all text-foreground/80">
|
||||
{audit?.idempotencyKey ??
|
||||
(meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读能力不记操作账本")}
|
||||
(meta && meta.kind !== "read" ? "服务端已生成,审计里没有取到" : "只读能力不记操作账本")}
|
||||
</dd>
|
||||
{audit?.grantId || audit?.approvalId ? (
|
||||
<>
|
||||
@@ -292,7 +292,7 @@ function EndNotice({ item }: { item: Extract<StreamItem, { kind: "end" }> }) {
|
||||
<MessageBubbleContent className={cn(aborted && "text-muted-foreground")}>
|
||||
<p className="font-medium">
|
||||
{budget
|
||||
? `超出预算${item.budget ? `(${BUDGET_LABEL[item.budget] ?? item.budget})` : ""},已停止`
|
||||
? `超出预算${item.budget ? `(${BUDGET_LABEL[item.budget] ?? item.budget})` : ""},已停止`
|
||||
: aborted
|
||||
? "已停止"
|
||||
: "运行失败"}
|
||||
@@ -479,7 +479,7 @@ export function AgentConversation({
|
||||
rows.push({
|
||||
key: `u${m.at}`,
|
||||
at: m.at,
|
||||
node: <UserMessage text={m.text} at={m.at} note={m.pending ? "已送达,等待 Agent 读取" : undefined} animateIn />,
|
||||
node: <UserMessage text={m.text} at={m.at} note={m.pending ? "已送达,等待 Agent 读取" : undefined} animateIn />,
|
||||
});
|
||||
rows.sort((a, b) => a.at.localeCompare(b.at));
|
||||
|
||||
@@ -584,7 +584,7 @@ export function BudgetBar({ run, live }: { run: AgentRunView; live: AgentStreamS
|
||||
exceeded={exceeded === "duration"}
|
||||
/>
|
||||
<Meter
|
||||
label="费用(估算)"
|
||||
label="费用(估算)"
|
||||
used={u.costUsd}
|
||||
max={run.budget.maxCostUsd}
|
||||
format={(n) => `$${n.toFixed(3)}`}
|
||||
@@ -626,7 +626,7 @@ export function ApprovalItem({ approval: a }: { approval: Approval }) {
|
||||
<ApprovalCard
|
||||
status={status}
|
||||
title={`批准执行这份${PLAN_LABEL[a.planType] ?? a.planType}?`}
|
||||
description={a.reason ?? "计划变化后,这份审批自动失效。"}
|
||||
description={a.reason ?? "计划变化后,这份审批自动失效。"}
|
||||
badges={
|
||||
<>
|
||||
{risk ? <RiskBadge level={risk} /> : null}
|
||||
@@ -654,7 +654,7 @@ export function ApprovalItem({ approval: a }: { approval: Approval }) {
|
||||
<dt className="text-muted-foreground">到期</dt>
|
||||
<dd className={cn("text-foreground/85", expired && "text-destructive")}>
|
||||
{a.expiresAt ? formatDateTime(a.expiresAt) : "不过期"}
|
||||
{expired ? "(已过期)" : ""}
|
||||
{expired ? "(已过期)" : ""}
|
||||
</dd>
|
||||
</dl>
|
||||
</ApprovalCard>
|
||||
@@ -721,12 +721,12 @@ export function FinalReport({
|
||||
<div className="min-w-0 rounded-xl border border-dashed border-border p-3">
|
||||
<p className="mb-1.5 flex items-center gap-1.5 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
||||
<Bot aria-hidden className="size-3.5" />
|
||||
模型说的(未经核查)
|
||||
模型说的(未经核查)
|
||||
</p>
|
||||
<StreamingResponse status="complete" copyText={said || undefined} contentClassName="max-h-60 overflow-auto">
|
||||
{said || "(没有文字汇报)"}
|
||||
{said || "(没有文字汇报)"}
|
||||
</StreamingResponse>
|
||||
<p className="mt-2 text-meta text-muted-foreground">仅供参考,不作为完成依据。</p>
|
||||
<p className="mt-2 text-meta text-muted-foreground">仅供参考,不作为完成依据。</p>
|
||||
</div>
|
||||
<div className="min-w-0 rounded-xl border border-border/70 bg-foreground/[0.02] p-3">
|
||||
<p className="mb-1.5 flex items-center gap-1.5 text-label font-semibold tracking-[0.18em] text-muted-foreground uppercase">
|
||||
@@ -788,12 +788,12 @@ export function FinalReport({
|
||||
/>
|
||||
) : null}
|
||||
<p className="mt-2 text-meta text-muted-foreground">
|
||||
取自下载计划、下载任务和整理记录(生成于 {formatDateTime(report.generatedAt)}
|
||||
)。"已提交"指已进入下载器,整理成功才算入库。
|
||||
取自下载计划、下载任务和整理记录(生成于 {formatDateTime(report.generatedAt)}
|
||||
)。"已提交"指已进入下载器,整理成功才算入库。
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-2 text-meta text-muted-foreground">服务端汇报还没有生成,以任务和整理记录的实际状态为准。</p>
|
||||
<p className="mt-2 text-meta text-muted-foreground">服务端汇报还没有生成,以任务和整理记录的实际状态为准。</p>
|
||||
)}
|
||||
{run.error ? (
|
||||
<p className="mt-2 text-caption break-words text-destructive">
|
||||
|
||||
@@ -93,9 +93,9 @@ export function AgentDrawer() {
|
||||
Agent
|
||||
</SheetTitle>
|
||||
<SheetDescription className="break-words">
|
||||
{context?.label ? `当前对象:${context.label}` : "交代目标,Agent 在授权范围内执行并汇报。"}
|
||||
{context?.label ? `当前对象:${context.label}` : "交代目标,Agent 在授权范围内执行并汇报。"}
|
||||
<span className="ml-1 hidden text-muted-foreground/75 sm:inline">
|
||||
({agentShortcutLabel()} 开关,Esc 关闭)
|
||||
({agentShortcutLabel()} 开关,Esc 关闭)
|
||||
</span>
|
||||
</SheetDescription>
|
||||
{context ? <ContextChips context={context} /> : null}
|
||||
@@ -105,8 +105,8 @@ export function AgentDrawer() {
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="absolute top-4 right-14"
|
||||
aria-label="最小化(运行继续)"
|
||||
title="最小化(运行继续)"
|
||||
aria-label="最小化(运行继续)"
|
||||
title="最小化(运行继续)"
|
||||
onClick={minimizeAgentDrawer}
|
||||
>
|
||||
<Minus />
|
||||
@@ -180,7 +180,7 @@ function AgentMiniDock({ runId }: { runId: string }) {
|
||||
? "思考中…"
|
||||
: last?.kind === "end"
|
||||
? last.ok
|
||||
? "已完成,展开查看核实结果"
|
||||
? "已完成,展开查看核实结果"
|
||||
: last.message
|
||||
: "等待输出…";
|
||||
const status = run.data
|
||||
@@ -205,8 +205,8 @@ function AgentMiniDock({ runId }: { runId: string }) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label="关闭小窗(运行在后台继续)"
|
||||
title="关闭小窗(运行在后台继续)"
|
||||
aria-label="关闭小窗(运行在后台继续)"
|
||||
title="关闭小窗(运行在后台继续)"
|
||||
onClick={() => setAgentDrawerOpen(false)}
|
||||
>
|
||||
<X />
|
||||
|
||||
@@ -95,7 +95,7 @@ export function GoalComposer({
|
||||
maxRows={10}
|
||||
maxLength={4000}
|
||||
aria-label="目标"
|
||||
placeholder="例如:把 沙丘:第二部 的 4K 收进来"
|
||||
placeholder="例如:把 沙丘:第二部 的 4K 收进来"
|
||||
submitLabel={pending ? "提交中" : "开始"}
|
||||
header={
|
||||
prefix ? (
|
||||
@@ -242,7 +242,7 @@ export function AgentRunPanel({
|
||||
className="flex items-center gap-2 rounded-xl border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning"
|
||||
>
|
||||
<WifiOff aria-hidden className="size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1">实时连接中断,正在重连。期间每 2.5 秒轮询一次。</span>
|
||||
<span className="min-w-0 flex-1">实时连接中断,正在重连。期间每 2.5 秒轮询一次。</span>
|
||||
</div>
|
||||
) : null}
|
||||
{live.error ? (
|
||||
@@ -250,7 +250,7 @@ export function AgentRunPanel({
|
||||
role="alert"
|
||||
className="flex flex-wrap items-center gap-2 rounded-xl border border-destructive/40 bg-destructive/10 px-3 py-2 text-caption text-destructive"
|
||||
>
|
||||
<span className="min-w-0 flex-1 break-words">事件读取失败:{live.error}</span>
|
||||
<span className="min-w-0 flex-1 break-words">事件读取失败:{live.error}</span>
|
||||
<Button size="xs" variant="outline" onClick={live.retry}>
|
||||
<RefreshCw />
|
||||
重试
|
||||
@@ -295,7 +295,7 @@ export function AgentRunPanel({
|
||||
maxRows={6}
|
||||
maxLength={8000}
|
||||
aria-label="追加消息"
|
||||
placeholder={streaming ? "补充或纠正(运行中也可追加)" : "追加消息"}
|
||||
placeholder={streaming ? "补充或纠正(运行中也可追加)" : "追加消息"}
|
||||
/>
|
||||
<p className="px-1 text-meta text-muted-foreground">
|
||||
{r.status === "waiting" && (r.summary === "等待选择方案" || r.summary === "等待审批")
|
||||
@@ -308,8 +308,8 @@ export function AgentRunPanel({
|
||||
<ConfirmDialog
|
||||
open={confirmStop}
|
||||
onOpenChange={setConfirmStop}
|
||||
title="停止这次运行?"
|
||||
description="后续步骤不再执行,已完成的外部动作(如已提交的下载)不会撤销。"
|
||||
title="停止这次运行?"
|
||||
description="后续步骤不再执行,已完成的外部动作(如已提交的下载)不会撤销。"
|
||||
confirmLabel="停止运行"
|
||||
destructive
|
||||
onConfirm={() =>
|
||||
|
||||
@@ -66,7 +66,7 @@ function Node({ node, byParent, depth }: { node: AgentRunNode; byParent: Map<str
|
||||
</span>
|
||||
</div>
|
||||
{node.spawnReason ? (
|
||||
<p className="mt-0.5 truncate text-meta text-muted-foreground">理由:{node.spawnReason}</p>
|
||||
<p className="mt-0.5 truncate text-meta text-muted-foreground">理由:{node.spawnReason}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,9 +107,9 @@ export function SubagentTree({ snapshot }: { snapshot: RunSnapshot }) {
|
||||
{splitContextGoal(parent.goal).text}
|
||||
</Link>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "}
|
||||
第 {run.depth} 层子会话 · 继承父会话的发起人与授权上限 · 能力 {caps} 项(⊆ 父会话)· 划拨预算{" "}
|
||||
{budgetText(run.budget as Record<string, number | undefined>)}
|
||||
{run.spawnReason ? ` · 理由:${run.spawnReason}` : ""}
|
||||
{run.spawnReason ? ` · 理由:${run.spawnReason}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -76,7 +76,7 @@ function script(goal: string, opts: AgentSessionOptions): Step[] {
|
||||
toolCallId: "t1",
|
||||
capabilityId: "inventory.query",
|
||||
isError: false,
|
||||
outputSummary: "作品 沙丘:第二部 (2024):已有 1080p WEB-DL(已核验);Slot「archival」缺 2160p;无下载中任务",
|
||||
outputSummary: "作品 沙丘:第二部 (2024):已有 1080p WEB-DL(已核验);Slot「archival」缺 2160p;无下载中任务",
|
||||
recovery: false,
|
||||
});
|
||||
plan("s1", "done", "s2");
|
||||
@@ -93,12 +93,12 @@ function script(goal: string, opts: AgentSessionOptions): Step[] {
|
||||
capabilityId: "release.search",
|
||||
isError: false,
|
||||
outputSummary:
|
||||
"3 条候选:① Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX.HDR.DV-FraMeSToR 78.4 GB 免费 · 做种 41;② 2160p WEB-DL DDP5.1 Atmos 24.1 GB;③ 2160p x265 10bit 18.9 GB",
|
||||
"3 条候选:① Dune.Part.Two.2024.2160p.UHD.BluRay.REMUX.HDR.DV-FraMeSToR 78.4 GB 免费 · 做种 41;② 2160p WEB-DL DDP5.1 Atmos 24.1 GB;③ 2160p x265 10bit 18.9 GB",
|
||||
recovery: false,
|
||||
});
|
||||
push(300, { type: "compaction", phase: "start", reason: "threshold" });
|
||||
push(700, { type: "compaction", phase: "end", reason: "threshold", status: "completed" });
|
||||
say("①符合 archival 的硬条件(REMUX、HDR),免费截止时间在 6 小时后。");
|
||||
say("①符合 archival 的硬条件(REMUX、HDR),免费截止时间在 6 小时后。");
|
||||
plan("s2", "done", "s3");
|
||||
if (opts.proposeFirst) {
|
||||
push(300, {
|
||||
@@ -142,7 +142,7 @@ function script(goal: string, opts: AgentSessionOptions): Step[] {
|
||||
toolCallId: "t3",
|
||||
capabilityId: "download.plan",
|
||||
isError: false,
|
||||
outputSummary: "计划已预留:78.4 GB,缓存卷 cache-a 使用率 71% → 预计 86%(硬阈值 90%)",
|
||||
outputSummary: "计划已预留:78.4 GB,缓存卷 cache-a 使用率 71% → 预计 86%(硬阈值 90%)",
|
||||
recovery: false,
|
||||
});
|
||||
plan("s3", "done", "s4");
|
||||
@@ -151,7 +151,7 @@ function script(goal: string, opts: AgentSessionOptions): Step[] {
|
||||
toolCallId: "t4",
|
||||
toolName: "qb_delete_torrent",
|
||||
capabilityId: "download.cleanup",
|
||||
reason: "未授权:删除下载器任务不在本次委托范围内(需要显式授权)",
|
||||
reason: "未授权:删除下载器任务不在本次委托范围内(需要显式授权)",
|
||||
});
|
||||
push(300, {
|
||||
type: "tool_call_start",
|
||||
@@ -165,7 +165,7 @@ function script(goal: string, opts: AgentSessionOptions): Step[] {
|
||||
toolCallId: "t5",
|
||||
capabilityId: "download.submit",
|
||||
isError: false,
|
||||
outputSummary: '{"error":{"code":"unauthorized","message":"已在步骤处发起审批,等待管理员批准"}}',
|
||||
outputSummary: '{"error":{"code":"unauthorized","message":"已在步骤处发起审批,等待管理员批准"}}',
|
||||
recovery: false,
|
||||
});
|
||||
push(200, {
|
||||
@@ -275,7 +275,7 @@ function seed() {
|
||||
type: "run_failed",
|
||||
reason: "budget",
|
||||
budget: "duration",
|
||||
message: "超出单次运行时长预算(600 秒)",
|
||||
message: "超出单次运行时长预算(600 秒)",
|
||||
usage: { ...USAGE, totalTokens: 4_210, inputTokens: 3_900, outputTokens: 310, costUsd: 0.009 },
|
||||
},
|
||||
},
|
||||
@@ -367,7 +367,7 @@ export function mockDetail(id: string, op?: { abort?: boolean; append?: string }
|
||||
consumedAt: null,
|
||||
});
|
||||
const last = e.steps.findIndex((s) => s.body.type === "run_completed" || s.body.type === "run_failed");
|
||||
const reply: Step[] = chunks(`收到补充:「${op.append}」,按此调整。`).map((c, i) => ({
|
||||
const reply: Step[] = chunks(`收到补充:「${op.append}」,按此调整。`).map((c, i) => ({
|
||||
delayMs: at + 200 + i * 60,
|
||||
body: { type: "text_delta", delta: c },
|
||||
}));
|
||||
@@ -390,10 +390,10 @@ function report(e: Entry, run: AgentRunView): GoalReport | null {
|
||||
return {
|
||||
state: submitted ? "waiting" : run.status === "succeeded" ? "no_actions" : "failed",
|
||||
summary: submitted
|
||||
? "1 个下载已进入下载器(qb-main),尚未入库;入库以整理记录 succeeded 为准"
|
||||
? "1 个下载已进入下载器(qb-main),尚未入库;入库以整理记录 succeeded 为准"
|
||||
: run.status === "succeeded"
|
||||
? "本次没有产生下载或整理动作"
|
||||
: "运行未完成,没有产生业务动作",
|
||||
: "运行未完成,没有产生业务动作",
|
||||
submitted: submitted ? 1 : 0,
|
||||
imported: 0,
|
||||
inProgress: submitted ? 1 : 0,
|
||||
|
||||
@@ -18,7 +18,7 @@ const RULES: Rule[] = [
|
||||
},
|
||||
{
|
||||
match: /^\/console\/sites\/?$/,
|
||||
goals: ["在所有站点里找 ", "哪个站点最近抓取失败了,原因是什么", "对比各站点最近一周的新资源数量"],
|
||||
goals: ["在所有站点里找 ", "哪个站点最近抓取失败了,原因是什么", "对比各站点最近一周的新资源数量"],
|
||||
},
|
||||
{
|
||||
match: /^\/console\/tasks\/[^/]+/,
|
||||
@@ -46,11 +46,11 @@ const RULES: Rule[] = [
|
||||
},
|
||||
{
|
||||
match: /^\/console\/subscriptions/,
|
||||
goals: ["帮我新建一个订阅:", "哪些订阅很久没有新收录了", "检查订阅的版本目标是否合理"],
|
||||
goals: ["帮我新建一个订阅:", "哪些订阅很久没有新收录了", "检查订阅的版本目标是否合理"],
|
||||
},
|
||||
{
|
||||
match: /^\/console\/sources/,
|
||||
goals: ["哪些来源不新鲜了,原因是什么", "检查所有 Feed 的抓取状态", "最近一天各来源抓到了什么"],
|
||||
goals: ["哪些来源不新鲜了,原因是什么", "检查所有 Feed 的抓取状态", "最近一天各来源抓到了什么"],
|
||||
},
|
||||
{
|
||||
match: /^\/console\/engine/,
|
||||
@@ -59,7 +59,7 @@ const RULES: Rule[] = [
|
||||
];
|
||||
|
||||
const FALLBACK: [string, string, string] = [
|
||||
"收一部电影:",
|
||||
"收一部电影:",
|
||||
"看看今天有什么需要我处理的",
|
||||
"检查下载器和媒体服务器是否正常",
|
||||
];
|
||||
|
||||
@@ -79,7 +79,7 @@ export function SiteOriginView({ site }: { site: SiteListItem }) {
|
||||
<EmptyState
|
||||
icon={Globe}
|
||||
title="没有启用的浏览器服务"
|
||||
description="登记 camofox 地址与访问凭证后,这里显示站点的真实页面。"
|
||||
description="登记 camofox 地址与访问凭证后,这里显示站点的真实页面。"
|
||||
/>
|
||||
);
|
||||
if (!site.enabled) return <EmptyState icon={Globe} title="站点已停用" description="启用站点后才能打开原站页面。" />;
|
||||
@@ -88,7 +88,7 @@ export function SiteOriginView({ site }: { site: SiteListItem }) {
|
||||
<EmptyState
|
||||
icon={Globe}
|
||||
title="还没有打开这个站点的浏览器会话"
|
||||
description="打开后显示站点首页;验证码等需本人操作在此完成。"
|
||||
description="打开后显示站点首页;验证码等需本人操作在此完成。"
|
||||
action={
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -143,7 +143,7 @@ export function SiteOriginView({ site }: { site: SiteListItem }) {
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge tone={conn.tone}>{conn.text}</StatusBadge>
|
||||
<StatusBadge tone={CONTROLLER_TONE[s.controller] ?? "neutral"}>
|
||||
当前控制:{CONTROLLER_LABEL[s.controller]}
|
||||
当前控制:{CONTROLLER_LABEL[s.controller]}
|
||||
{human && leaseHolder ? `(${leaseHolder})` : ""}
|
||||
</StatusBadge>
|
||||
{waiting ? <StatusBadge tone="warning">{waiting}</StatusBadge> : null}
|
||||
@@ -155,7 +155,7 @@ export function SiteOriginView({ site }: { site: SiteListItem }) {
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
control.release.mutate(s.id, {
|
||||
onSuccess: () => toast.success("已交还,Agent 会重新读取页面"),
|
||||
onSuccess: () => toast.success("已交还,Agent 会重新读取页面"),
|
||||
onError: (e) => toast.error(errorMessage(e)),
|
||||
})
|
||||
}
|
||||
@@ -261,7 +261,7 @@ export function SiteOriginView({ site }: { site: SiteListItem }) {
|
||||
<form onSubmit={onTextSubmit} className="flex min-w-0 items-center gap-1.5">
|
||||
<Input
|
||||
aria-label="输入到页面"
|
||||
placeholder="先点页面上的输入框,再在这里输入"
|
||||
placeholder="先点页面上的输入框,再在这里输入"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
className="h-8 min-w-0 flex-1"
|
||||
@@ -315,7 +315,7 @@ export function SiteOriginView({ site }: { site: SiteListItem }) {
|
||||
variant="outline"
|
||||
size="xs"
|
||||
disabled={busy}
|
||||
title="用站点账号的密码填当前输入框;密码不经过这个页面"
|
||||
title="用站点账号的密码填当前输入框;密码不经过这个页面"
|
||||
onClick={() => send({ kind: "credential", credential: "password" })}
|
||||
>
|
||||
<KeyRound />
|
||||
|
||||
@@ -53,7 +53,7 @@ const KIND_OPTS: { value: string; label: string }[] = SubscriptionMediaKind.opti
|
||||
}));
|
||||
|
||||
export const PUBLISH_NOTICE =
|
||||
"发布后,作用范围内的下载、追更和整理按此策略自动执行,不再逐次审批。升级替换、媒体清理和下载缓存清理仍需单独审批。旧的已发布版本被替代,授权随之失效。";
|
||||
"发布后,作用范围内的下载、追更和整理按此策略自动执行,不再逐次审批。升级替换、媒体清理和下载缓存清理仍需单独审批。旧的已发布版本被替代,授权随之失效。";
|
||||
|
||||
type TemplateDraft = { key: string; name: string; rules: SlotRulesDraft; open: boolean };
|
||||
type PolicyDraft = {
|
||||
@@ -164,7 +164,7 @@ function buildPolicy(d: PolicyDraft):
|
||||
const strict = AcquisitionPolicyRules.strict().safeParse(rules);
|
||||
if (!strict.success && !Object.keys(errors).length) {
|
||||
const i = strict.error.issues[0];
|
||||
errors.rules = `规则结构不合法:${i?.path.join(".")} ${i?.message}`;
|
||||
errors.rules = `规则结构不合法:${i?.path.join(".")} ${i?.message}`;
|
||||
}
|
||||
if (Object.keys(errors).length || !meta.success) return { ok: false, errors };
|
||||
return {
|
||||
@@ -221,7 +221,7 @@ export function PolicyWorkspace({
|
||||
<EmptyState
|
||||
icon={ScrollText}
|
||||
title="还没有收录策略"
|
||||
description="新建草案,预览后发布。"
|
||||
description="新建草案,预览后发布。"
|
||||
action={
|
||||
<Button onClick={() => onCreatingChange(true)}>
|
||||
<FilePlus2 />
|
||||
@@ -297,7 +297,7 @@ function PolicyGroup({
|
||||
description={
|
||||
published
|
||||
? `当前生效 r${published.revision} · ${formatDateTime(published.publishedAt)} 由 ${actorText(published.publishedBy)} 发布`
|
||||
: "未发布,不会自动执行"
|
||||
: "未发布,不会自动执行"
|
||||
}
|
||||
actions={
|
||||
draft ? (
|
||||
@@ -348,13 +348,13 @@ function PolicyGroup({
|
||||
}
|
||||
|
||||
function RulesSummary({ rules, valid }: { rules: AcquisitionPolicyRules | undefined; valid: boolean }) {
|
||||
if (!valid || !rules) return <p className="text-caption text-destructive">规则无法解析,不会执行</p>;
|
||||
if (!valid || !rules) return <p className="text-caption text-destructive">规则无法解析,不会执行</p>;
|
||||
const tplLine = (t: SlotTemplate) => `${t.name}:${summarizeHard(t.hardConditions, t.fallback).join(",") || "不限"}`;
|
||||
return (
|
||||
<div className="space-y-2 text-caption">
|
||||
<div>
|
||||
<span className="text-muted-foreground">默认版本目标 </span>
|
||||
<ChipList items={(rules.defaultSlots ?? []).map(tplLine)} empty="无(订阅没有版本目标时不自动创建)" />
|
||||
<ChipList items={(rules.defaultSlots ?? []).map(tplLine)} empty="无(订阅没有版本目标时不自动创建)" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">新作品收录 </span>
|
||||
@@ -469,12 +469,12 @@ function PolicyDetailSheet({
|
||||
</Button>
|
||||
}
|
||||
title={`回滚到 r${rev.revision}?`}
|
||||
description={`用 r${rev.revision} 的内容发布一个新版本,当前版本标记为已回滚。${PUBLISH_NOTICE}`}
|
||||
description={`用 r${rev.revision} 的内容发布一个新版本,当前版本标记为已回滚。${PUBLISH_NOTICE}`}
|
||||
confirmLabel="回滚"
|
||||
onConfirm={() =>
|
||||
rollback
|
||||
.mutateAsync(rev.id)
|
||||
.then((n) => toast.success(`已回滚,r${n.revision} 生效`))
|
||||
.then((n) => toast.success(`已回滚,r${n.revision} 生效`))
|
||||
.then(onClose)
|
||||
.catch((e) => {
|
||||
toast.error(errorMessage(e));
|
||||
@@ -538,7 +538,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
|
||||
if (!value)
|
||||
return (
|
||||
<p className="text-caption text-muted-foreground">
|
||||
还没有预览。预览用最近已识别的资源评估模板,不会执行任何操作。
|
||||
还没有预览。预览用最近已识别的资源评估模板,不会执行任何操作。
|
||||
</p>
|
||||
);
|
||||
const p = PolicyPreview.safeParse(value);
|
||||
@@ -548,7 +548,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
|
||||
<div className="space-y-3">
|
||||
{!v.valid ? (
|
||||
<p className="rounded-lg border border-destructive/30 bg-destructive/10 px-3 py-2 text-caption text-destructive">
|
||||
规则未通过校验,预览按空模板计算,无法发布。
|
||||
规则未通过校验,预览按空模板计算,无法发布。
|
||||
</p>
|
||||
) : null}
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
@@ -565,7 +565,7 @@ function PreviewBlock({ value }: { value: JsonObject | null }) {
|
||||
))}
|
||||
</div>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
计算于 {formatDateTime(v.evaluatedAt)};接受/排除按"资源 × 模板"计数。
|
||||
计算于 {formatDateTime(v.evaluatedAt)};接受/排除按"资源 × 模板"计数。
|
||||
</p>
|
||||
{v.samples.length ? (
|
||||
<ul className="divide-y divide-border/60 rounded-lg border border-border/70">
|
||||
@@ -660,7 +660,7 @@ function PolicyEditorSheet({
|
||||
onOpenChange={(o) => !o && !pending && onClose()}
|
||||
width="lg"
|
||||
title={editId ? `编辑草案 r${base?.revision}` : base ? `基于 r${base.revision} 新建草案` : "新建收录策略草案"}
|
||||
description="保存为草案,发布后才生效。"
|
||||
description="保存为草案,发布后才生效。"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={onClose} disabled={pending}>
|
||||
@@ -699,7 +699,12 @@ function PolicyEditorSheet({
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<FormField label="本版说明" htmlFor={`${id}-sum`} error={errors.summary} hint="这一版改了什么,显示在版本历史里">
|
||||
<FormField
|
||||
label="本版说明"
|
||||
htmlFor={`${id}-sum`}
|
||||
error={errors.summary}
|
||||
hint="这一版改了什么,显示在版本历史里"
|
||||
>
|
||||
<Textarea id={`${id}-sum`} rows={2} value={d.summary} onChange={(e) => set("summary", e.target.value)} />
|
||||
</FormField>
|
||||
<FormField label="作用范围" hint="不选即全部作品类型">
|
||||
@@ -725,7 +730,7 @@ function PolicyEditorSheet({
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<p className="mb-2 text-meta text-muted-foreground">订阅的作品没有版本目标时,按这些模板创建。</p>
|
||||
<p className="mb-2 text-meta text-muted-foreground">订阅的作品没有版本目标时,按这些模板创建。</p>
|
||||
<TemplateList list={d.defaultSlots} onChange={(v) => set("defaultSlots", v)} errors={errors} />
|
||||
</DetailSection>
|
||||
|
||||
@@ -734,7 +739,7 @@ function PolicyEditorSheet({
|
||||
<div className="flex items-center justify-between gap-3 rounded-lg border border-border px-3 py-2">
|
||||
<div className="min-w-0">
|
||||
<p className="text-caption font-medium text-foreground">开启新作品收录</p>
|
||||
<p className="text-meta text-muted-foreground">按发行/首播日期判断,新上传的旧片不算</p>
|
||||
<p className="text-meta text-muted-foreground">按发行/首播日期判断,新上传的旧片不算</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={d.newTitleEnabled}
|
||||
@@ -743,7 +748,7 @@ function PolicyEditorSheet({
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<FormField label="时间窗口(天)" htmlFor={`${id}-win`} error={errors.windowDays} hint="发行/首播在窗口内">
|
||||
<FormField label="时间窗口(天)" htmlFor={`${id}-win`} error={errors.windowDays} hint="发行/首播在窗口内">
|
||||
<Input
|
||||
id={`${id}-win`}
|
||||
inputMode="numeric"
|
||||
@@ -790,7 +795,7 @@ function PolicyEditorSheet({
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem value="__default">按系统设置</SelectItem>
|
||||
<SelectItem value="copy">复制(保留源文件做种)</SelectItem>
|
||||
<SelectItem value="copy">复制(保留源文件做种)</SelectItem>
|
||||
<SelectItem value="move">移动</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -850,7 +855,7 @@ function TemplateList({
|
||||
aria-label="模板名称"
|
||||
aria-invalid={!!errs.name}
|
||||
value={t.name}
|
||||
placeholder="模板名称,如 archival"
|
||||
placeholder="模板名称,如 archival"
|
||||
onChange={(e) => patch(t.key, { name: e.target.value })}
|
||||
className="h-8 min-w-0 flex-1 font-mono"
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,7 @@ const SlotMeta = z.object({
|
||||
/** 409:别人(或状态机)已改过这个 Slot */
|
||||
export function conflictToast(err: unknown, onRefresh: () => void) {
|
||||
if (err instanceof ApiRequestError && (err.code === "conflict" || err.code === "precondition_failed")) {
|
||||
toast.error("规则已被修改,已刷新。在最新版本上重新编辑。", { duration: 4000 });
|
||||
toast.error("规则已被修改,已刷新。在最新版本上重新编辑。", { duration: 4000 });
|
||||
onRefresh();
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export function SlotsPanel({
|
||||
plain
|
||||
icon={Target}
|
||||
title="还没有版本目标"
|
||||
description="没有时按已发布收录策略的默认模板创建,也可以手动新建。"
|
||||
description="没有时按已发布收录策略的默认模板创建,也可以手动新建。"
|
||||
className="py-8"
|
||||
/>
|
||||
) : (
|
||||
@@ -180,7 +180,7 @@ function SlotEditorSheet({
|
||||
width="lg"
|
||||
title={slot ? `编辑版本目标 ${slot.name}` : "新建版本目标"}
|
||||
description={
|
||||
slot ? `当前 r${slot.revision}。保存时若已被修改,会提示刷新。` : "不满足硬条件的资源不收;软偏好决定先收哪个。"
|
||||
slot ? `当前 r${slot.revision}。保存时若已被修改,会提示刷新。` : "不满足硬条件的资源不收;软偏好决定先收哪个。"
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
|
||||
@@ -97,8 +97,8 @@ function PasskeyRow({ item }: { item: PasskeyInfo }) {
|
||||
<Trash2 />
|
||||
</Button>
|
||||
}
|
||||
title={`删除 Passkey「${item.name}」?`}
|
||||
description="删除后这把钥匙不能再登录控制台;设备上的钥匙要在系统设置里另外删。"
|
||||
title={`删除 Passkey「${item.name}」?`}
|
||||
description="删除后这把钥匙不能再登录控制台;设备上的钥匙要在系统设置里另外删。"
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
onConfirm={async () => {
|
||||
@@ -139,7 +139,7 @@ export function PasskeyBlock({ disabled }: { disabled: boolean }) {
|
||||
};
|
||||
|
||||
let body: React.ReactNode;
|
||||
if (disabled) body = <p className="text-caption text-muted-foreground">当前用 API Key 访问,登录会话才能修改。</p>;
|
||||
if (disabled) body = <p className="text-caption text-muted-foreground">当前用 API Key 访问,登录会话才能修改。</p>;
|
||||
else
|
||||
body = (
|
||||
<div className="grid gap-3">
|
||||
@@ -155,7 +155,7 @@ export function PasskeyBlock({ disabled }: { disabled: boolean }) {
|
||||
<p className="text-caption text-muted-foreground">还没有登记 Passkey。</p>
|
||||
)}
|
||||
{!supported ? (
|
||||
<p className="text-meta text-muted-foreground">这个浏览器用不了 Passkey:需要 HTTPS 或 localhost 访问。</p>
|
||||
<p className="text-meta text-muted-foreground">这个浏览器用不了 Passkey:需要 HTTPS 或 localhost 访问。</p>
|
||||
) : adding ? (
|
||||
<form onSubmit={add} className="flex max-w-md flex-wrap items-center gap-2">
|
||||
<Input
|
||||
@@ -197,7 +197,7 @@ export function PasskeyBlock({ disabled }: { disabled: boolean }) {
|
||||
section="account"
|
||||
anchor="passkeys"
|
||||
title="Passkey"
|
||||
description="用指纹、面容或安全钥匙直接登录,不用输密码和验证码。可以登记多把。"
|
||||
description="用指纹、面容或安全钥匙直接登录,不用输密码和验证码。可以登记多把。"
|
||||
>
|
||||
{body}
|
||||
</SettingsBlock>
|
||||
|
||||
@@ -45,13 +45,13 @@ function QrCode({ value }: { value: string }) {
|
||||
|
||||
/** 恢复码展示:只此一次;可复制、下载 */
|
||||
function RecoveryCodes({ codes, onDone }: { codes: string[]; onDone: () => void }) {
|
||||
const text = `UHD Media Tool 恢复码(每个只能用一次)\n\n${codes.join("\n")}\n`;
|
||||
const text = `UHD Media Tool 恢复码(每个只能用一次)\n\n${codes.join("\n")}\n`;
|
||||
const copy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(codes.join("\n"));
|
||||
toast.success("恢复码已复制");
|
||||
} catch {
|
||||
toast.error("复制失败,手动选中复制");
|
||||
toast.error("复制失败,手动选中复制");
|
||||
}
|
||||
};
|
||||
const download = () => {
|
||||
@@ -65,8 +65,8 @@ function RecoveryCodes({ codes, onDone }: { codes: string[]; onDone: () => void
|
||||
return (
|
||||
<div className="grid max-w-md gap-3">
|
||||
<p className="text-caption">
|
||||
验证器丢了就用恢复码登录,每个只能用一次。
|
||||
<span className="text-muted-foreground">它们只显示这一次,存到密码管理器里。</span>
|
||||
验证器丢了就用恢复码登录,每个只能用一次。
|
||||
<span className="text-muted-foreground">它们只显示这一次,存到密码管理器里。</span>
|
||||
</p>
|
||||
<ol className="grid grid-cols-2 gap-x-6 gap-y-1 rounded-lg bg-muted px-4 py-3 font-mono text-sm select-all">
|
||||
{codes.map((c) => (
|
||||
@@ -173,7 +173,7 @@ export function TotpBlock({ disabled }: { disabled: boolean }) {
|
||||
const grouped = setup.data?.secret.replace(/(.{4})/g, "$1 ").trim();
|
||||
|
||||
let body: React.ReactNode;
|
||||
if (disabled) body = <p className="text-caption text-muted-foreground">当前用 API Key 访问,登录会话才能修改。</p>;
|
||||
if (disabled) body = <p className="text-caption text-muted-foreground">当前用 API Key 访问,登录会话才能修改。</p>;
|
||||
else if (mode === "codes") body = <RecoveryCodes codes={codes} onDone={() => reset()} />;
|
||||
else if (mode === "setup")
|
||||
body = setup.isPending ? (
|
||||
@@ -190,9 +190,11 @@ export function TotpBlock({ disabled }: { disabled: boolean }) {
|
||||
<form onSubmit={enable} noValidate className="grid gap-4 sm:grid-cols-[auto_minmax(0,1fr)] sm:items-start">
|
||||
<QrCode value={setup.data.otpauthUri} />
|
||||
<div className="grid max-w-md gap-2">
|
||||
<p className="text-caption">用验证器(1Password、Google Authenticator 等)扫码,再输入它显示的 6 位验证码。</p>
|
||||
<p className="text-caption">
|
||||
用验证器(1Password、Google Authenticator 等)扫码,再输入它显示的 6 位验证码。
|
||||
</p>
|
||||
<details className="text-meta text-muted-foreground">
|
||||
<summary className="cursor-pointer select-none">扫不了码?手动输入密钥</summary>
|
||||
<summary className="cursor-pointer select-none">扫不了码?手动输入密钥</summary>
|
||||
<code className="mt-1.5 block rounded-lg bg-muted px-2 py-1.5 font-mono text-sm break-all text-foreground select-all">
|
||||
{grouped}
|
||||
</code>
|
||||
@@ -261,7 +263,7 @@ export function TotpBlock({ disabled }: { disabled: boolean }) {
|
||||
body = (
|
||||
<div className="grid justify-items-start gap-3">
|
||||
<p className={left <= 2 ? "text-caption text-warning" : "text-caption text-muted-foreground"}>
|
||||
还剩 {left} 个恢复码(共 {RECOVERY_CODE_COUNT} 个)。{left <= 2 ? "快用完了,重新生成一组。" : ""}
|
||||
还剩 {left} 个恢复码(共 {RECOVERY_CODE_COUNT} 个)。{left <= 2 ? "快用完了,重新生成一组。" : ""}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => reset("regenerate")}>
|
||||
@@ -290,7 +292,7 @@ export function TotpBlock({ disabled }: { disabled: boolean }) {
|
||||
description={
|
||||
enabled
|
||||
? "登录时除了密码还要输入验证器里的 6 位验证码。关闭需要当前密码。"
|
||||
: "开启后登录还要输入验证器里的 6 位验证码,同时生成 8 个一次性恢复码。"
|
||||
: "开启后登录还要输入验证器里的 6 位验证码,同时生成 8 个一次性恢复码。"
|
||||
}
|
||||
actions={
|
||||
disabled || status.isPending ? null : (
|
||||
|
||||
@@ -39,7 +39,7 @@ function PasswordBlock({ disabled }: { disabled: boolean }) {
|
||||
setErrors({});
|
||||
try {
|
||||
const r = await change.mutateAsync({ currentPassword: form.currentPassword, newPassword: form.newPassword });
|
||||
toast.success(r.revokedSessions ? `密码已修改,注销了其它 ${r.revokedSessions} 个会话` : "密码已修改");
|
||||
toast.success(r.revokedSessions ? `密码已修改,注销了其它 ${r.revokedSessions} 个会话` : "密码已修改");
|
||||
setForm(EMPTY_PWD);
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
@@ -56,7 +56,7 @@ function PasswordBlock({ disabled }: { disabled: boolean }) {
|
||||
section="account"
|
||||
anchor="password"
|
||||
title="修改密码"
|
||||
description="修改后其它设备上的会话全部注销,当前会话保留。"
|
||||
description="修改后其它设备上的会话全部注销,当前会话保留。"
|
||||
dirty={dirty}
|
||||
>
|
||||
<form onSubmit={submit} noValidate className="grid gap-x-4 gap-y-1 sm:grid-cols-3">
|
||||
@@ -135,7 +135,7 @@ function SessionBlock() {
|
||||
<div className="mt-4 border-t border-border/60 pt-4">
|
||||
<p className="text-caption font-medium">本机 Token</p>
|
||||
<p className="mb-2 text-meta text-muted-foreground">
|
||||
只存在这个浏览器,有值时请求会带上它。{hasToken ? "已设置。" : "未设置。"}
|
||||
只存在这个浏览器,有值时请求会带上它。{hasToken ? "已设置。" : "未设置。"}
|
||||
</p>
|
||||
<div className="flex max-w-md flex-wrap items-center gap-2">
|
||||
<Input
|
||||
|
||||
@@ -17,7 +17,7 @@ const RUNTIME_CARD: CardSpec = {
|
||||
name: "pi.contextBudget",
|
||||
label: "上下文预算(token)",
|
||||
kind: "number",
|
||||
hint: "4096–2000000,超过模型窗口时按窗口算",
|
||||
hint: "4096–2000000,超过模型窗口时按窗口算",
|
||||
},
|
||||
{ name: "pi.subagentsEnabled", label: "允许子会话", kind: "switch" },
|
||||
{ name: "pi.compaction.enabled", label: "上下文压缩", kind: "switch" },
|
||||
@@ -25,7 +25,7 @@ const RUNTIME_CARD: CardSpec = {
|
||||
name: "pi.compaction.thresholdRatio",
|
||||
label: "压缩触发比例",
|
||||
kind: "number",
|
||||
hint: "0.3–0.95,用到预算的这个比例时压缩",
|
||||
hint: "0.3–0.95,用到预算的这个比例时压缩",
|
||||
visible: (v) => v["pi.compaction.enabled"] === true,
|
||||
},
|
||||
],
|
||||
@@ -50,12 +50,12 @@ const RUNTIME_CARD: CardSpec = {
|
||||
const BUDGET_CARD: CardSpec = {
|
||||
anchor: "budget",
|
||||
title: "预算",
|
||||
description: "超出每日预算的运行停在「超出预算」,等你放行。",
|
||||
description: "超出每日预算的运行停在「超出预算」,等你放行。",
|
||||
fields: [
|
||||
{ name: "budget.agentMaxRunsPerDay", label: "每日运行次数", kind: "number" },
|
||||
{ name: "budget.agentMaxCostUsdPerDay", label: "每日费用(USD)", kind: "number" },
|
||||
{ name: "agent.maxToolCalls", label: "单次工具调用上限", kind: "number", hint: "1–500" },
|
||||
{ name: "agent.maxDurationSec", label: "单次时长上限(秒)", kind: "number" },
|
||||
{ name: "agent.maxDurationSec", label: "单次时长上限(秒)", kind: "number" },
|
||||
{ name: "agent.maxTokensPerRun", label: "单次 token 上限", kind: "number", hint: "留空不限" },
|
||||
{ name: "agent.maxCostUsdPerRun", label: "单次费用上限(USD)", kind: "number", hint: "留空不限" },
|
||||
],
|
||||
@@ -84,7 +84,7 @@ const BUDGET_CARD: CardSpec = {
|
||||
const INVESTIGATE_CARD: CardSpec = {
|
||||
anchor: "investigate",
|
||||
title: "自动调查",
|
||||
description: "待处理事项(识别有歧义、下载失败等)创建时,自动发起一次有界调查。",
|
||||
description: "待处理事项(识别有歧义、下载失败等)创建时,自动发起一次有界调查。",
|
||||
fields: [
|
||||
{ name: "agent.autoInvestigate", label: "自动调查", kind: "switch", wide: true },
|
||||
{
|
||||
@@ -92,10 +92,10 @@ const INVESTIGATE_CARD: CardSpec = {
|
||||
label: "模式",
|
||||
kind: "select",
|
||||
options: [
|
||||
{ value: "investigate", label: "只读调查,结论交还给你" },
|
||||
{ value: "recover", label: "授权内恢复(重试任务、重新校验、确认识别)" },
|
||||
{ value: "investigate", label: "只读调查,结论交还给你" },
|
||||
{ value: "recover", label: "授权内恢复(重试任务、重新校验、确认识别)" },
|
||||
],
|
||||
hint: "恢复只在已发布策略或持续授权覆盖时执行,换源、删除、清理始终排除;没有授权时按只读调查",
|
||||
hint: "恢复只在已发布策略或持续授权覆盖时执行,换源、删除、清理始终排除;没有授权时按只读调查",
|
||||
wide: true,
|
||||
visible: (v) => v["agent.autoInvestigate"] === true,
|
||||
},
|
||||
@@ -110,12 +110,12 @@ const INVESTIGATE_CARD: CardSpec = {
|
||||
name: "agent.investigateMaxAttempts",
|
||||
label: "同一根因最多恢复次数",
|
||||
kind: "number",
|
||||
hint: "1–10,用完后只交还给你",
|
||||
hint: "1–10,用完后只交还给你",
|
||||
visible: (v) => v["agent.autoInvestigate"] === true && v["agent.autoInvestigateMode"] === "recover",
|
||||
},
|
||||
{
|
||||
name: "agent.recoverVerifySec",
|
||||
label: "恢复后核实时长(秒)",
|
||||
label: "恢复后核实时长(秒)",
|
||||
kind: "number",
|
||||
hint: "等下载或任务状态变化的最长时间",
|
||||
visible: (v) => v["agent.autoInvestigate"] === true && v["agent.autoInvestigateMode"] === "recover",
|
||||
@@ -143,16 +143,16 @@ const ADVANCED_CARD: CardSpec = {
|
||||
anchor: "advanced",
|
||||
title: "高级",
|
||||
fields: [
|
||||
{ name: "agent.goalPollSec", label: "目标轮询间隔(秒)", kind: "number", hint: "等下载、入库结束时的检查间隔" },
|
||||
{ name: "agent.goalPollSec", label: "目标轮询间隔(秒)", kind: "number", hint: "等下载、入库结束时的检查间隔" },
|
||||
{
|
||||
name: "agent.goalWaitHours",
|
||||
label: "目标最长等待(小时)",
|
||||
label: "目标最长等待(小时)",
|
||||
kind: "number",
|
||||
hint: "超时后按「仍在等待」汇报结束",
|
||||
},
|
||||
{
|
||||
name: "agent.optionsTimeoutSec",
|
||||
label: "方案选择等待(秒)",
|
||||
label: "方案选择等待(秒)",
|
||||
kind: "number",
|
||||
hint: "Agent 给出方案后等你选择的最长时间",
|
||||
},
|
||||
@@ -167,7 +167,7 @@ const ADVANCED_CARD: CardSpec = {
|
||||
},
|
||||
{
|
||||
name: "agent.approvalTimeoutSec",
|
||||
label: "会话内审批有效期(秒)",
|
||||
label: "会话内审批有效期(秒)",
|
||||
kind: "number",
|
||||
hint: "步骤处的审批卡过期未批准,按拒绝处理",
|
||||
},
|
||||
@@ -177,13 +177,13 @@ const ADVANCED_CARD: CardSpec = {
|
||||
kind: "text",
|
||||
wide: true,
|
||||
placeholder: "留空用 DATA_DIR/agent-sessions",
|
||||
hint: "绝对路径,api 与 worker 都要可读写;有运行中的会话时不要改",
|
||||
hint: "绝对路径,api 与 worker 都要可读写;有运行中的会话时不要改",
|
||||
},
|
||||
{
|
||||
name: "agent.fakeScript",
|
||||
label: "fake 脚本(JSON)",
|
||||
kind: "textarea",
|
||||
hint: "只对 fake 上游生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
|
||||
hint: "只对 fake 上游生效,留空用内置演示脚本;格式见 docs/runbook/agent.md",
|
||||
},
|
||||
],
|
||||
fromSettings: (s) => ({
|
||||
|
||||
@@ -39,7 +39,7 @@ function CopyButton({ text, label }: { text: string; label: string }) {
|
||||
setDone(true);
|
||||
toast.success("已复制");
|
||||
} catch {
|
||||
toast.error("复制失败,手动选中复制");
|
||||
toast.error("复制失败,手动选中复制");
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -149,7 +149,7 @@ export function PiOAuthLogin({
|
||||
waitMs: 25_000,
|
||||
});
|
||||
setProgress(r.progress);
|
||||
if (r.state === "pending") setMessage("还在处理,稍后再提交一次。");
|
||||
if (r.state === "pending") setMessage("还在处理,稍后再提交一次。");
|
||||
else finish(r.state, r.message);
|
||||
} catch (err) {
|
||||
toast.error(errorMessage(err, "提交失败"));
|
||||
@@ -213,7 +213,7 @@ export function PiOAuthLogin({
|
||||
) : session.mode === "device" ? (
|
||||
<div className="grid gap-2 rounded-lg border border-border/60 p-3">
|
||||
<p className="text-caption text-muted-foreground">
|
||||
在任意设备打开验证地址,输入下面的验证码。授权后这里自动完成。
|
||||
在任意设备打开验证地址,输入下面的验证码。授权后这里自动完成。
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="rounded-md bg-muted px-3 py-1.5 font-mono text-lead tracking-widest select-all">
|
||||
@@ -256,7 +256,7 @@ export function PiOAuthLogin({
|
||||
) : (
|
||||
<form onSubmit={submitPasted} noValidate className="grid gap-2 rounded-lg border border-border/60 p-3">
|
||||
<p className="text-caption text-muted-foreground">
|
||||
在浏览器打开授权链接。授权后会跳到 localhost 地址,页面打不开属正常,把地址栏里的完整 URL 粘贴到下面。
|
||||
在浏览器打开授权链接。授权后会跳到 localhost 地址,页面打不开属正常,把地址栏里的完整 URL 粘贴到下面。
|
||||
</p>
|
||||
{session.authorizeUrl ? (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
|
||||
@@ -22,21 +22,21 @@ 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.userDataMaxSeedingPages",
|
||||
label: "做种列表页数上限",
|
||||
kind: "number",
|
||||
hint: "拉取用户数据时最多翻几页做种列表;1–50",
|
||||
hint: "拉取用户数据时最多翻几页做种列表;1–50",
|
||||
},
|
||||
{
|
||||
name: "sites.userAgent",
|
||||
label: "User-Agent",
|
||||
kind: "text",
|
||||
hint: "账号没填 UA 时,登录和拉取用户数据用这个",
|
||||
hint: "账号没填 UA 时,登录和拉取用户数据用这个",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
@@ -44,7 +44,7 @@ const DEFAULTS_CARD: CardSpec = {
|
||||
label: "代理地址",
|
||||
kind: "url",
|
||||
placeholder: "http://127.0.0.1:7890",
|
||||
hint: "只对开了“使用代理”的站点生效;留空不用代理",
|
||||
hint: "只对开了「使用代理」的站点生效;留空不用代理",
|
||||
wide: true,
|
||||
},
|
||||
],
|
||||
@@ -81,7 +81,7 @@ export function SitesSection() {
|
||||
<div className="space-y-6">
|
||||
<div className="pg-slab flex flex-wrap items-center justify-between gap-3 rounded-xl px-4 py-3">
|
||||
<p className="text-caption text-muted-foreground">
|
||||
站点和凭证在「站点」页管理;单个站点的限流、周期、代理在站点的"编辑"里改。
|
||||
站点和凭证在「站点」页管理;单个站点的限流、周期、代理在站点的"编辑"里改。
|
||||
</p>
|
||||
<Button asChild size="sm" variant="outline">
|
||||
<Link to={consolePath("sites")}>
|
||||
|
||||
@@ -136,7 +136,7 @@ export function CredentialFields({
|
||||
<FormField
|
||||
label="TOTP 密钥"
|
||||
htmlFor={id("totp")}
|
||||
hint="站点开了二次验证时填 base32 密钥,登录时自动生成 6 位码"
|
||||
hint="站点开了二次验证时填 base32 密钥,登录时自动生成 6 位码"
|
||||
>
|
||||
<Input
|
||||
id={id("totp")}
|
||||
@@ -157,7 +157,7 @@ export function CredentialFields({
|
||||
<label htmlFor={id("cookie-only")} className="text-caption">
|
||||
<span className="block font-medium text-foreground">只换 Cookie</span>
|
||||
<span className="block text-muted-foreground">
|
||||
密码加密保存,用于 Cookie 失效后自动重新登录。打开后登录成功只留 Cookie,不保存密码
|
||||
密码加密保存,用于 Cookie 失效后自动重新登录。打开后登录成功只留 Cookie,不保存密码
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -168,7 +168,7 @@ export function CredentialFields({
|
||||
htmlFor={id("cookie")}
|
||||
required
|
||||
error={errors.cookie}
|
||||
hint="浏览器登录站点后,从开发者工具复制请求头里的整段 Cookie"
|
||||
hint="浏览器登录站点后,从开发者工具复制请求头里的整段 Cookie"
|
||||
>
|
||||
<Textarea
|
||||
id={id("cookie")}
|
||||
@@ -198,7 +198,7 @@ export function CredentialFields({
|
||||
<FormField
|
||||
label="User-Agent"
|
||||
htmlFor={id("ua")}
|
||||
hint={value.kind === "cookie" ? "Cookie 与浏览器 UA 绑定时填写;留空用站点默认" : "留空用站点默认"}
|
||||
hint={value.kind === "cookie" ? "Cookie 与浏览器 UA 绑定时填写;留空用站点默认" : "留空用站点默认"}
|
||||
>
|
||||
<Input
|
||||
id={id("ua")}
|
||||
|
||||
@@ -36,7 +36,7 @@ export function SiteAccountsTab({
|
||||
<EmptyState
|
||||
icon={UserRound}
|
||||
title="还没有账号"
|
||||
description="填写凭证后,这里显示登录状态和登录历史"
|
||||
description="填写凭证后,这里显示登录状态和登录历史"
|
||||
action={
|
||||
<Button size="sm" onClick={() => onDialog({ kind: "credentials", site })}>
|
||||
<KeyRound />
|
||||
@@ -110,8 +110,8 @@ function AccountBlock({
|
||||
删除账号
|
||||
</Button>
|
||||
}
|
||||
title={`删除账号「${a.label}」?`}
|
||||
description="账号、凭证和用户数据历史一起删除,不可恢复。站点和资源保留。"
|
||||
title={`删除账号「${a.label}」?`}
|
||||
description="账号、凭证和用户数据历史一起删除,不可恢复。站点和资源保留。"
|
||||
confirmLabel="删除账号"
|
||||
destructive
|
||||
onConfirm={async () => {
|
||||
@@ -241,7 +241,7 @@ function PasskeyForm({ site, label, has }: { site: SiteListItem; label: string;
|
||||
>
|
||||
<h2 className="text-lead font-semibold text-foreground">Passkey</h2>
|
||||
<p className="mb-3 text-caption text-muted-foreground">
|
||||
用于 RSS 和下载链接,不参与登录。{has ? "已保存;再次保存会替换。" : "还没有保存。"}
|
||||
用于 RSS 和下载链接,不参与登录。{has ? "已保存;再次保存会替换。" : "还没有保存。"}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-start gap-2">
|
||||
<FormField label="Passkey" htmlFor="pk" error={err} className="min-w-0 flex-1">
|
||||
|
||||
@@ -96,9 +96,9 @@ export function SiteAddDialog({ onClose }: { onClose: () => void }) {
|
||||
const u = new URL(form.baseUrl.trim());
|
||||
if (!/^https?:$/.test(u.protocol)) throw new Error();
|
||||
} catch {
|
||||
fe.baseUrl = "填完整地址,如 https://example.org";
|
||||
fe.baseUrl = "填完整地址,如 https://example.org";
|
||||
}
|
||||
if (custom && !/^[a-z][a-z0-9_-]*$/.test(form.key.trim())) fe.key = "小写字母开头,只含小写字母、数字、- 和 _";
|
||||
if (custom && !/^[a-z][a-z0-9_-]*$/.test(form.key.trim())) fe.key = "小写字母开头,只含小写字母、数字、- 和 _";
|
||||
let body = null;
|
||||
if (kinds.length) {
|
||||
// 账号标签用站点标识:服务端 secret 名按标签生成且全局唯一,多站都叫 default 会冲突
|
||||
@@ -258,14 +258,14 @@ export function SiteAddDialog({ onClose }: { onClose: () => void }) {
|
||||
onClick={() => pick(CUSTOM)}
|
||||
className="w-full rounded-lg border border-dashed border-border px-3 py-2 text-left text-caption text-muted-foreground hover:border-foreground/40 hover:text-foreground"
|
||||
>
|
||||
自定义 RSS 站点:只抓 Feed,不登录
|
||||
自定义 RSS 站点:只抓 Feed,不登录
|
||||
</button>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-3 text-meta text-muted-foreground">
|
||||
"未实测"的定义没有在真实站点上验证过,列表和登录可能失败。
|
||||
"未实测"的定义没有在真实站点上验证过,列表和登录可能失败。
|
||||
</p>
|
||||
</div>
|
||||
) : step === 2 ? (
|
||||
@@ -343,7 +343,7 @@ export function SiteAddDialog({ onClose }: { onClose: () => void }) {
|
||||
<div className="min-w-0 space-y-3" data-state={phase ? "running" : "done"} aria-busy={busy}>
|
||||
<p className="text-caption text-muted-foreground">
|
||||
{created ? `已添加「${created.name}」。` : ""}
|
||||
{phase ?? (login?.ok === false ? "可以返回修改凭证后重试,或先保存,之后在站点页处理。" : "")}
|
||||
{phase ?? (login?.ok === false ? "可以返回修改凭证后重试,或先保存,之后在站点页处理。" : "")}
|
||||
</p>
|
||||
{phase ? <LoadingState rows={2} /> : null}
|
||||
<LoginResultView
|
||||
|
||||
@@ -150,7 +150,7 @@ function CheckButtons({ site, checks, compact }: Omit<Props, "onDialog"> & { com
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={h || !site.enabled}
|
||||
title={site.enabled ? "首页连通 + 登录态,走站点限流" : "站点已停用"}
|
||||
title={site.enabled ? "首页连通 + 登录态,走站点限流" : "站点已停用"}
|
||||
onClick={() => void checks.checkHealth(site)}
|
||||
>
|
||||
{h ? <LoaderCircle className="animate-spin" /> : <Activity />}
|
||||
|
||||
@@ -256,7 +256,7 @@ export function CredentialDialog({
|
||||
onClose();
|
||||
}
|
||||
} else {
|
||||
toast.success("凭证已保存,刷新登录已排队");
|
||||
toast.success("凭证已保存,刷新登录已排队");
|
||||
onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -271,8 +271,8 @@ export function CredentialDialog({
|
||||
<DialogTitle>更新凭证 · {site.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{current
|
||||
? `当前:${AUTH_KIND_LABEL[current.authKind]}。凭证加密保存,不回显;保存后立即刷新登录`
|
||||
: "凭证加密保存,不回显;保存后立即刷新登录"}
|
||||
? `当前:${AUTH_KIND_LABEL[current.authKind]}。凭证加密保存,不回显;保存后立即刷新登录`
|
||||
: "凭证加密保存,不回显;保存后立即刷新登录"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form id="site-cred" onSubmit={submit} noValidate>
|
||||
@@ -334,12 +334,12 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
|
||||
try {
|
||||
new URL(v.baseUrl.trim());
|
||||
} catch {
|
||||
fe.baseUrl = "填完整地址,如 https://example.org";
|
||||
fe.baseUrl = "填完整地址,如 https://example.org";
|
||||
}
|
||||
const posInt = (s: string, k: string, min = 1) => {
|
||||
if (!s.trim()) return undefined;
|
||||
const n = Number(s);
|
||||
if (!Number.isInteger(n) || n < min) fe[k] = `填不小于 ${min} 的整数,留空用站点默认`;
|
||||
if (!Number.isInteger(n) || n < min) fe[k] = `填不小于 ${min} 的整数,留空用站点默认`;
|
||||
return n;
|
||||
};
|
||||
const rpm = posInt(v.rpm, "rpm");
|
||||
@@ -436,7 +436,7 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
|
||||
</label>
|
||||
</div>
|
||||
{v.snapshotEnabled ? (
|
||||
<FormField label="快照周期(分钟)" htmlFor="se-int" error={errors.intervalMin} hint="留空用站点默认">
|
||||
<FormField label="快照周期(分钟)" htmlFor="se-int" error={errors.intervalMin} hint="留空用站点默认">
|
||||
<Input
|
||||
id="se-int"
|
||||
inputMode="numeric"
|
||||
@@ -450,10 +450,10 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
|
||||
<Switch id="se-proxy" checked={v.proxy} onCheckedChange={(x) => set("proxy", x)} disabled={busy} />
|
||||
<label htmlFor="se-proxy" className="text-caption">
|
||||
<span className="block font-medium text-foreground">使用代理</span>
|
||||
<span className="block text-muted-foreground">走「站点默认」里的代理地址;没填代理地址时不生效</span>
|
||||
<span className="block text-muted-foreground">走「站点默认」里的代理地址;没填代理地址时不生效</span>
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-meta text-muted-foreground">User-Agent 随凭证保存,在"更新凭证"里填写。</p>
|
||||
<p className="text-meta text-muted-foreground">User-Agent 随凭证保存,在"更新凭证"里填写。</p>
|
||||
</div>
|
||||
</DetailSheet>
|
||||
);
|
||||
@@ -474,7 +474,7 @@ export function DeleteSiteDialog({
|
||||
<ConfirmDialog
|
||||
open
|
||||
onOpenChange={(o) => !o && onClose()}
|
||||
title={`删除站点「${site.name}」?`}
|
||||
title={`删除站点「${site.name}」?`}
|
||||
description="不可恢复。"
|
||||
confirmLabel="删除站点"
|
||||
destructive
|
||||
@@ -491,8 +491,8 @@ export function DeleteSiteDialog({
|
||||
>
|
||||
<ul className="list-disc space-y-1 pl-5 text-caption text-muted-foreground">
|
||||
<li>站点、{site.accounts?.length ?? 0} 个账号和全部凭证删除</li>
|
||||
<li>已抓到的资源保留,标为"站点已关闭",不再产生新候选</li>
|
||||
<li>该站 Feed 停用;已有下载任务按原策略继续</li>
|
||||
<li>已抓到的资源保留,标为"站点已关闭",不再产生新候选</li>
|
||||
<li>该站 Feed 停用;已有下载任务按原策略继续</li>
|
||||
<li>同一站点以后可以重新添加</li>
|
||||
</ul>
|
||||
</ConfirmDialog>
|
||||
|
||||
@@ -150,15 +150,15 @@ export function loginFailureText(
|
||||
): { text: string; suggestCookie: boolean } {
|
||||
switch (reason) {
|
||||
case "captcha":
|
||||
return { text: "登录失败:站点要求验证码。改用 Cookie,或稍后重试", suggestCookie: true };
|
||||
return { text: "登录失败:站点要求验证码。改用 Cookie,或稍后重试", suggestCookie: true };
|
||||
case "two_factor":
|
||||
return { text: "登录失败:站点要求二次验证。填写 TOTP 密钥后重试,或改用 Cookie", suggestCookie: true };
|
||||
return { text: "登录失败:站点要求二次验证。填写 TOTP 密钥后重试,或改用 Cookie", suggestCookie: true };
|
||||
case "challenge":
|
||||
return { text: "站点返回 Cloudflare 挑战页,无法自动登录。改用 Cookie", suggestCookie: true };
|
||||
return { text: "站点返回 Cloudflare 挑战页,无法自动登录。改用 Cookie", suggestCookie: true };
|
||||
case "bad_credentials":
|
||||
return { text: "登录失败:账号或密码错误", suggestCookie: false };
|
||||
return { text: "登录失败:账号或密码错误", suggestCookie: false };
|
||||
case "account_blocked":
|
||||
return { text: `登录失败:账号被封禁或未确认${message ? `(${message})` : ""}`, suggestCookie: false };
|
||||
return { text: `登录失败:账号被封禁或未确认${message ? `(${message})` : ""}`, suggestCookie: false };
|
||||
case "auth_expired":
|
||||
return {
|
||||
text:
|
||||
@@ -172,7 +172,7 @@ export function loginFailureText(
|
||||
case "unsupported":
|
||||
return { text: message ?? "这个站点不支持该凭证类型的自动登录", suggestCookie: false };
|
||||
default:
|
||||
return { text: message ?? "登录失败,原因未知", suggestCookie: false };
|
||||
return { text: message ?? "登录失败,原因未知", suggestCookie: false };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ export function UserDataChart({ points }: { points: SiteUserDataPoint[] }) {
|
||||
if (rows.length < 2)
|
||||
return (
|
||||
<p className="py-6 text-center text-caption text-muted-foreground">
|
||||
{rows.length ? "只有 1 天的数据,满 2 天后显示趋势" : "还没有历史数据。刷新登录成功后每天记一次"}
|
||||
{rows.length ? "只有 1 天的数据,满 2 天后显示趋势" : "还没有历史数据。刷新登录成功后每天记一次"}
|
||||
</p>
|
||||
);
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export function useSubscriptionOps() {
|
||||
remove: (ids: string[]) =>
|
||||
batch.mutateAsync({ ids, action: "delete" }).then((r) => {
|
||||
const bad = r.results.filter((x) => !x.ok);
|
||||
if (bad.length) toast.error(`${bad.length} 个没有删除:${bad[0]?.error ?? ""}`);
|
||||
if (bad.length) toast.error(`${bad.length} 个没有删除:${bad[0]?.error ?? ""}`);
|
||||
else toast.success(ids.length > 1 ? `已删除 ${ids.length} 个订阅` : "订阅已删除");
|
||||
}),
|
||||
batch: (ids: string[], a: "search" | "pause" | "resume") =>
|
||||
@@ -215,8 +215,8 @@ export function DeleteSubscriptionsDialog({
|
||||
<ConfirmDialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={ids.length > 1 ? `删除 ${ids.length} 个订阅?` : "删除这个订阅?"}
|
||||
description="不再搜索和追更。已入库文件、版本目标和下载记录保留,可在订阅历史里重新订阅。"
|
||||
title={ids.length > 1 ? `删除 ${ids.length} 个订阅?` : "删除这个订阅?"}
|
||||
description="不再搜索和追更。已入库文件、版本目标和下载记录保留,可在订阅历史里重新订阅。"
|
||||
confirmLabel="删除"
|
||||
destructive
|
||||
onConfirm={() =>
|
||||
|
||||
@@ -238,7 +238,7 @@ export function CellSheet({
|
||||
}
|
||||
}}
|
||||
title={sel ? `${sel.season} · ${columnLabel(sel.col)}` : ""}
|
||||
description={sel ? `${sel.row.name}${sel.row.slotId === null ? "(作品全部资产)" : ""}` : null}
|
||||
description={sel ? `${sel.row.name}${sel.row.slotId === null ? "(作品全部资产)" : ""}` : null}
|
||||
footer={
|
||||
sel && c ? (
|
||||
<>
|
||||
@@ -254,7 +254,7 @@ export function CellSheet({
|
||||
<Button
|
||||
onClick={doFill}
|
||||
disabled={!canFill || fill.isPending || done === c.unitKey}
|
||||
title={canFill ? undefined : "订阅不在追踪中,先恢复"}
|
||||
title={canFill ? undefined : "订阅不在追踪中,先恢复"}
|
||||
>
|
||||
<Wand2 />
|
||||
{done === c.unitKey ? "已提交" : c.upgradable ? "补全更好版本" : "手动补全"}
|
||||
@@ -282,7 +282,7 @@ export function CellSheet({
|
||||
c.quality?.label ?? ([c.quality?.resolution, c.quality?.source].filter(Boolean).join(" ") || null),
|
||||
},
|
||||
{ label: "pri", value: c.pri !== null ? String(c.pri) : null },
|
||||
{ label: "可升级", value: c.state === "have" ? (c.upgradable ? "是,低于目标档位" : "否") : null },
|
||||
{ label: "可升级", value: c.state === "have" ? (c.upgradable ? "是,低于目标档位" : "否") : null },
|
||||
{
|
||||
label: "资产",
|
||||
value: c.assetId ? (
|
||||
@@ -306,17 +306,17 @@ export function CellSheet({
|
||||
: null,
|
||||
},
|
||||
{ label: "原因", value: c.reason ? (SKIP_REASON[c.reason] ?? c.reason) : null },
|
||||
{ label: "延后", value: c.deferred ? "有资源在等待重评(预算或自动补全关闭)" : null },
|
||||
{ label: "延后", value: c.deferred ? "有资源在等待重评(预算或自动补全关闭)" : null },
|
||||
]}
|
||||
/>
|
||||
</DetailSection>
|
||||
{fillable ? (
|
||||
<p className="text-meta text-muted-foreground">
|
||||
手动补全不受自动补全开关和补全预算限制,仍受全局每日预算与缓存卷阈值约束。
|
||||
手动补全不受自动补全开关和补全预算限制,仍受全局每日预算与缓存卷阈值约束。
|
||||
</p>
|
||||
) : null}
|
||||
{sel.row.slotId === null && c.state === "missing" ? (
|
||||
<p className="text-meta text-muted-foreground">这一行汇总全部版本。要补全,点具体版本目标那一行的格。</p>
|
||||
<p className="text-meta text-muted-foreground">这一行汇总全部版本。要补全,点具体版本目标那一行的格。</p>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -137,7 +137,7 @@ export const SKIP_REASON: Record<string, string> = {
|
||||
out_of_range: "集范围外",
|
||||
not_aired: "未播出",
|
||||
not_required: "该版本目标不要求这个内容版本",
|
||||
suspected: "总集数未知,按已见最高集推断",
|
||||
suspected: "总集数未知,按已见最高集推断",
|
||||
deferred: "有延后的资源",
|
||||
};
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export function ProgressBlock({ x, className }: { x: Pick<Item, "progress" | "mo
|
||||
"bg-[repeating-linear-gradient(90deg,var(--color-foreground)_0_4px,transparent_4px_8px)] opacity-20",
|
||||
)}
|
||||
role="img"
|
||||
aria-label={`已入库 ${x.progress.have},在途 ${x.progress.inflight},总 ${x.progress.total ?? "未知"}`}
|
||||
aria-label={`已入库 ${x.progress.have},在途 ${x.progress.inflight},总 ${x.progress.total ?? "未知"}`}
|
||||
>
|
||||
{unknown ? null : (
|
||||
<>
|
||||
|
||||
@@ -53,7 +53,7 @@ export function SlotPanel({
|
||||
setOrder.mutate(
|
||||
{ id: sub.id, slotIds: rows.map((r) => r.id) },
|
||||
{
|
||||
onSuccess: () => toast.success(`主版本:${rows[0]?.name ?? ""}`),
|
||||
onSuccess: () => toast.success(`主版本:${rows[0]?.name ?? ""}`),
|
||||
onError: (e) => {
|
||||
toast.error(errorMessage(e));
|
||||
setRows(serverRows);
|
||||
@@ -84,7 +84,7 @@ export function SlotPanel({
|
||||
return (
|
||||
<Panel
|
||||
title="版本目标"
|
||||
description="拖动调整顺序,第一个是主版本。主版本缺的照常追更;其他版本按下面的自动补全设置补齐。"
|
||||
description="拖动调整顺序,第一个是主版本。主版本缺的照常追更;其他版本按下面的自动补全设置补齐。"
|
||||
actions={
|
||||
dirty ? (
|
||||
<>
|
||||
@@ -119,7 +119,7 @@ export function SlotPanel({
|
||||
size="xs"
|
||||
onClick={() => fillSlot(r)}
|
||||
disabled={!canFill || fill.isPending || r.summary.missing === 0}
|
||||
title={canFill ? undefined : "订阅不在追踪中,先恢复"}
|
||||
title={canFill ? undefined : "订阅不在追踪中,先恢复"}
|
||||
>
|
||||
<Wand2 />
|
||||
补全此版本
|
||||
|
||||
@@ -154,7 +154,7 @@ function WorkPicker({ onPick, error }: { onPick: (t: Target) => 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) => (
|
||||
<li key={key}>
|
||||
@@ -178,7 +178,7 @@ function WorkPicker({ onPick, error }: { onPick: (t: Target) => void; error?: st
|
||||
className="pl-9"
|
||||
value={q}
|
||||
aria-invalid={!!error}
|
||||
placeholder="搜索标题 / 原名,库里没有的从 TMDB 选"
|
||||
placeholder="搜索标题 / 原名,库里没有的从 TMDB 选"
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@@ -240,7 +240,7 @@ function WorkPicker({ onPick, error }: { onPick: (t: Target) => void; error?: st
|
||||
</p>
|
||||
) : null}
|
||||
{s.data?.media && !s.data.media.tmdb.configured ? (
|
||||
<p className="px-3 pb-2 text-meta text-muted-foreground">TMDB 未配置,只搜了媒体库。</p>
|
||||
<p className="px-3 pb-2 text-meta text-muted-foreground">TMDB 未配置,只搜了媒体库。</p>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
@@ -405,7 +405,7 @@ export function SubscriptionFormDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editing ? "编辑订阅" : "新建订阅"}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{editing ? titleText : "从媒体库或 TMDB 选作品;库里没有的按标题追踪,建档后自动关联。"}
|
||||
{editing ? titleText : "从媒体库或 TMDB 选作品;库里没有的按标题追踪,建档后自动关联。"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{dup ? (
|
||||
@@ -413,7 +413,7 @@ export function SubscriptionFormDialog({
|
||||
role="alert"
|
||||
className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-warning/40 bg-warning/10 px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<span>这部作品的同一季已有订阅,不能重复创建。</span>
|
||||
<span>这部作品的同一季已有订阅,不能重复创建。</span>
|
||||
<Link
|
||||
to={consolePath("subscriptions", dup)}
|
||||
onClick={onClose}
|
||||
@@ -445,7 +445,7 @@ export function SubscriptionFormDialog({
|
||||
<p className="truncate text-sm font-medium text-foreground">{titleText}</p>
|
||||
)}
|
||||
<p className="mt-0.5 text-meta text-muted-foreground">
|
||||
{target.kind === "entity" ? "媒体库作品" : (target.note ?? "按标题追踪,尚未建档")}
|
||||
{target.kind === "entity" ? "媒体库作品" : (target.note ?? "按标题追踪,尚未建档")}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => setTarget(null)}>
|
||||
@@ -486,7 +486,7 @@ export function SubscriptionFormDialog({
|
||||
<FormField
|
||||
label="季"
|
||||
htmlFor={f("season")}
|
||||
hint={editing ? "季建好后不能改;要换季就新建订阅" : entityId ? undefined : "作品建档后可选季"}
|
||||
hint={editing ? "季建好后不能改;要换季就新建订阅" : entityId ? undefined : "作品建档后可选季"}
|
||||
>
|
||||
<Pick
|
||||
id={f("season")}
|
||||
@@ -534,7 +534,7 @@ export function SubscriptionFormDialog({
|
||||
label="总集数"
|
||||
htmlFor={f("total")}
|
||||
error={err("total")}
|
||||
hint={d.totalLocked ? "锁定后元数据刷新不再改写" : "空 = 按 TMDB;不锁定时只升不降"}
|
||||
hint={d.totalLocked ? "锁定后元数据刷新不再改写" : "空 = 按 TMDB;不锁定时只升不降"}
|
||||
>
|
||||
<Input
|
||||
id={f("total")}
|
||||
@@ -620,7 +620,7 @@ export function SubscriptionFormDialog({
|
||||
) : subSlots.length === 0 ? (
|
||||
<p className="text-caption text-muted-foreground">还没有版本目标。在对比视图里新建。</p>
|
||||
) : (
|
||||
<FormField label="顺序" htmlFor={f("order")} hint="第一个是主版本;其他版本按自动补全设置补齐">
|
||||
<FormField label="顺序" htmlFor={f("order")} hint="第一个是主版本;其他版本按自动补全设置补齐">
|
||||
<SlotOrderList
|
||||
items={order ?? subSlots.map((s) => ({ id: s.id, name: s.name }))}
|
||||
onChange={setOrderState}
|
||||
@@ -634,7 +634,7 @@ export function SubscriptionFormDialog({
|
||||
htmlFor={f("tpl")}
|
||||
hint={
|
||||
target?.kind === "entity"
|
||||
? "作品还没有版本目标时按模板顺序创建,第一个是主版本"
|
||||
? "作品还没有版本目标时按模板顺序创建,第一个是主版本"
|
||||
: "作品建档后按模板创建版本目标"
|
||||
}
|
||||
>
|
||||
@@ -653,7 +653,7 @@ export function SubscriptionFormDialog({
|
||||
<FormField
|
||||
label="升级策略"
|
||||
htmlFor={f("upgrade")}
|
||||
hint="作用于这个订阅的全部版本目标;已入库低于目标档位时继续找更好的"
|
||||
hint="作用于这个订阅的全部版本目标;已入库低于目标档位时继续找更好的"
|
||||
>
|
||||
<Pick
|
||||
id={f("upgrade")}
|
||||
@@ -675,7 +675,7 @@ export function SubscriptionFormDialog({
|
||||
<FormField
|
||||
label="规则组"
|
||||
htmlFor={f("groups")}
|
||||
hint="按选择顺序串联;不选 = 订阅默认规则组。版本目标自己的规则组另算"
|
||||
hint="按选择顺序串联;不选 = 订阅默认规则组。版本目标自己的规则组另算"
|
||||
>
|
||||
{groups.isPending ? (
|
||||
<p className="text-caption text-muted-foreground">读取规则组…</p>
|
||||
@@ -697,7 +697,7 @@ export function SubscriptionFormDialog({
|
||||
)}
|
||||
</FormField>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<FormField label="包含" htmlFor={f("inc")} hint="每行一个正则,匹配标题、副标题和标签">
|
||||
<FormField label="包含" htmlFor={f("inc")} hint="每行一个正则,匹配标题、副标题和标签">
|
||||
<Textarea
|
||||
id={f("inc")}
|
||||
rows={3}
|
||||
@@ -761,7 +761,7 @@ export function SubscriptionFormDialog({
|
||||
label="搜索关键词"
|
||||
htmlFor={f("kw")}
|
||||
error={err("keywords")}
|
||||
hint="每行一个,追加在标题和别名之后;最多 10 个"
|
||||
hint="每行一个,追加在标题和别名之后;最多 10 个"
|
||||
>
|
||||
<Textarea
|
||||
id={f("kw")}
|
||||
@@ -775,7 +775,7 @@ export function SubscriptionFormDialog({
|
||||
<Section title="搜索与优先级">
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<FormField
|
||||
label="主动搜索间隔(小时)"
|
||||
label="主动搜索间隔(小时)"
|
||||
htmlFor={f("interval")}
|
||||
error={err("searchIntervalHours")}
|
||||
hint="空 = 24;0 = 关"
|
||||
@@ -843,7 +843,7 @@ export function AutoFillFields({
|
||||
<div className="space-y-3">
|
||||
{compact ? null : (
|
||||
<p className="text-meta text-muted-foreground">
|
||||
主版本缺的照常追更。开启后,主版本已有的单元也会为其他版本搜索下载;关闭时只记为缺失,可手动补全。
|
||||
主版本缺的照常追更。开启后,主版本已有的单元也会为其他版本搜索下载;关闭时只记为缺失,可手动补全。
|
||||
</p>
|
||||
)}
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
@@ -867,7 +867,7 @@ export function AutoFillFields({
|
||||
))}
|
||||
</fieldset>
|
||||
</FormField>
|
||||
<FormField label="每天最多(个单元)" htmlFor={`${idPrefix}-units`} error={errors.unitsPerDay}>
|
||||
<FormField label="每天最多(个单元)" htmlFor={`${idPrefix}-units`} error={errors.unitsPerDay}>
|
||||
<Input
|
||||
id={`${idPrefix}-units`}
|
||||
inputMode="numeric"
|
||||
@@ -888,7 +888,9 @@ export function AutoFillFields({
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<p className="text-meta text-muted-foreground">预算只算自动补全的计划,超出的延到次日;同时受全局每日预算限制。</p>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
预算只算自动补全的计划,超出的延到次日;同时受全局每日预算限制。
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user