文案(站点 / 字幕):重写站点卡、凭证、RSS、Cloudflare 与补字幕文案

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-07 01:48:14 +08:00
1 parent 0bf26b78c6
commit 03c5f668ac
9 files changed
+30 -30

No files matched your search

@@ -118,7 +118,7 @@ function AccountBlock({
onConfirm={async () => {
try {
await del.mutateAsync(a.id);
toast.success(`已删除账号 ${a.label}`);
toast.success(`已删除账号「${a.label}」`);
} catch (err) {
toast.error(errorMessage(err, "删除失败"));
throw err;
@@ -260,7 +260,7 @@ function PasskeyForm({ site, label, has }: { site: SiteListItem; label: string;
}
}}
>
<h2 className="text-lead font-semibold text-foreground">站点 Passkey(RSS 与下载链接用)</h2>
<h2 className="text-lead font-semibold text-foreground">站点 Passkey</h2>
<p className="mb-3 text-caption text-muted-foreground">
用于 RSS 和下载链接,不参与登录。{has ? "填写新值保存会替换。" : "还没有保存。"}
</p>
@@ -375,7 +375,7 @@ export function SiteAddDialog({ onClose, initialKind }: { onClose: () => void; i
)}
</div>
<p className="mt-3 text-meta text-muted-foreground">
"未实测"的定义没有在真实站点上验证过,列表和登录可能失败。
标「未实测」的站点定义没有在真实站点上验证过,列表和登录可能失败。
</p>
</div>
) : step === 2 ? (
+3 -3
View File
@@ -183,7 +183,7 @@ function CheckButtons({ site, checks, compact }: Omit<Props, "onDialog"> & { com
size={compact ? "icon-sm" : "sm"}
disabled={h || !site.enabled}
aria-label="检测健康"
title={site.enabled ? "检测健康:首页连通 + 登录态,走站点限流" : "站点已停用"}
title={site.enabled ? "检测健康:首页能否打开、登录态是否有效,计入站点限流" : "站点已停用"}
onClick={() => void checks.checkHealth(site)}
>
{h ? <LoaderCircle className="animate-spin" /> : <Activity />}
@@ -194,7 +194,7 @@ function CheckButtons({ site, checks, compact }: Omit<Props, "onDialog"> & { com
size={compact ? "icon-sm" : "sm"}
disabled={l || !hasAccount}
aria-label="刷新登录"
title={hasAccount ? "刷新登录:用凭证刷新登录状态并拉取用户数据" : "先填写凭证"}
title={hasAccount ? "刷新登录:用凭证重新登录并拉取用户数据" : "先填写凭证"}
onClick={() => void checks.refreshLogin(site)}
>
{l ? <LoaderCircle className="animate-spin" /> : <RefreshCw />}
@@ -289,7 +289,7 @@ function FeedSiteCard({ site, onDialog }: Omit<Props, "checks">) {
<div aria-hidden className="min-h-4 flex-1" />
<footer className="mt-auto flex flex-wrap items-center justify-end gap-2 border-t border-border/60 pt-3">
<Button asChild variant="outline" size="sm">
<Link to={`${consolePath("sites", site.id)}?tab=rss`}>{f.loaded && !f.count ? "添加 RSS" : "管理 RSS"}</Link>
<Link to={`${consolePath("sites", site.id)}?tab=rss`}>{f.loaded && !f.count ? "新建 RSS" : "管理 RSS"}</Link>
</Button>
<SiteMoreMenu site={site} onDialog={onDialog} />
</footer>
@@ -507,7 +507,7 @@ export function SiteEditSheet({ site, onClose }: { site: SiteListItem; onClose:
</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>
);
@@ -544,9 +544,9 @@ 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>该站 RSS 停用;已有下载任务按原策略继续</li>
<li>站点、{site.accounts?.length ?? 0} 个账号和全部凭证都会删除</li>
<li>已抓到的资源保留,标为「站点已关闭」,不再进入收录评估</li>
<li>这个站点的 RSS 停用;已有下载任务继续</li>
<li>同一站点以后可以重新添加</li>
</ul>
</ConfirmDialog>
@@ -70,7 +70,7 @@ export function SiteFailureSamples({ siteId, items }: { siteId: string; items: S
if (!items.length) return null;
return (
<section className="grid gap-2">
<h3 className="text-meta text-muted-foreground">最近的失败页面(已脱敏,留最近 5 份)</h3>
<h3 className="text-meta text-muted-foreground">最近的失败页面(已脱敏,最多保留 5 份)</h3>
<ul className="grid gap-2">
{items.map((s) => (
<FailureSampleRow key={s.id} siteId={siteId} sample={s} />
@@ -23,7 +23,7 @@ import { cn } from "~/lib/utils.ts";
const PURPOSES: { value: FeedPurpose; label: string; hint: string }[] = [
{ value: "generic", label: "发现", hint: "全站新种,进入识别和收录评估" },
{ value: "search", label: "追踪", hint: "按关键词、分类或收藏筛过的 RSS" },
{ value: "promotion", label: "促销", hint: "只含免费 / 促销种子的 RSS" },
{ value: "promotion", label: "促销", hint: "只含免费 / 促销资源的 RSS" },
];
/** 首抓最多等 90 秒(站点限流排队);超时后提示去列表看 */
@@ -106,7 +106,7 @@ export function SiteFeedDialog({
<Dialog open onOpenChange={onOpenChange}>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>添加 {site.name} RSS</DialogTitle>
<DialogTitle>新建 RSS · {site.name}</DialogTitle>
<DialogDescription>地址加密保存,页面上只显示去掉 passkey 的地址。</DialogDescription>
</DialogHeader>
@@ -219,7 +219,7 @@ export function SiteFeedDialog({
</FormField>
<p className="text-caption text-muted-foreground">
{account ? `账号 ${account.label}` : "该站没有账号,按地址里的 passkey 抓取"} · 解析器按站点定义 · 每 30
{account ? `账号 ${account.label}` : "这个站点没有账号,按地址里的 passkey 抓取"} · 解析器按站点定义 · 每 30
分钟抓一次,可在 RSS 详情里改
</p>
</form>
@@ -43,7 +43,7 @@ describe("cloudflareLine / cloudflareTechItems(CF-2)", () => {
const until = new Date("2026-09-30T17:30:00+08:00");
const line = cloudflareLine(cfg({ lastChallengeAt: at, browserUntil: until.toISOString() }), now);
const hm = `${String(until.getHours()).padStart(2, "0")}:${String(until.getMinutes()).padStart(2, "0")}`;
expect(line?.text).toBe(`Cloudflare:浏览器代理中,至 ${hm}`);
expect(line?.text).toBe(`Cloudflare:浏览器代抓中,到 ${hm}`);
expect(cloudflareLine(cfg({ lastChallengeAt: at, lastSolvedBy: "vision" }), now)?.text).toBe(
"Cloudflare:自动通过",
);
@@ -63,8 +63,8 @@ describe("cloudflareLine / cloudflareTechItems(CF-2)", () => {
}),
);
expect(items.map((i) => i.label)).toEqual([
"最近遇到挑战",
"挑战规则",
"最近遇到验证",
"验证规则",
"处理结果",
"最近通过方式",
"cf_clearance 复用",
+12 -12
View File
@@ -158,7 +158,7 @@ export function healthMessage(h: SiteLastHealth | null): string | null {
const HEALTH_REASON_TEXT: Record<string, string> = {
logged_in: "首页有登录标志",
user_data: "首页没找到登录标志,按用户数据解析成功判为已登录",
user_data: "首页没有登录标志,但用户数据解析成功,按已登录处理",
login_page: "返回登录页",
permission_page: "返回权限提示页",
challenge: "疑似人机验证",
@@ -198,7 +198,7 @@ export function cloudflareLine(
if (st.browserUntil && Date.parse(st.browserUntil) > now.getTime()) {
const t = new Date(st.browserUntil);
const hm = `${String(t.getHours()).padStart(2, "0")}:${String(t.getMinutes()).padStart(2, "0")}`;
return { text: `Cloudflare:浏览器代理中,至 ${hm}`, tone: "neutral", takeover: false };
return { text: `Cloudflare:浏览器代抓中,到 ${hm}`, tone: "neutral", takeover: false };
}
if (st.lastSolvedBy) return { text: "Cloudflare:自动通过", tone: "success", takeover: false };
if (st.lastError) return { text: "Cloudflare:没能通过", tone: "warning", takeover: false };
@@ -206,8 +206,8 @@ export function cloudflareLine(
}
const CF_SOLVED_BY: Record<string, string> = {
auto: "等它自动放行",
vision: "视觉子流程点了复选框",
auto: "自动放行",
vision: "自动点了验证复选框",
human: "你在原站视图里通过",
};
const CF_RESULT: Record<string, string> = {
@@ -227,8 +227,8 @@ export function cloudflareTechItems(config: unknown): { label: string; value: st
if (!st?.lastChallengeAt) return [];
const t = (v: string | null) => (v ? new Date(v).toLocaleString("zh-CN", { hour12: false }) : "");
return [
{ label: "最近遇到挑战", value: t(st.lastChallengeAt) },
{ label: "挑战规则", value: st.lastRule ?? "", mono: true },
{ label: "最近遇到验证", value: t(st.lastChallengeAt) },
{ label: "验证规则", value: st.lastRule ?? "", mono: true },
{ label: "处理结果", value: st.lastResult ? `${CF_RESULT[st.lastResult] ?? st.lastResult}(${st.lastResult})` : "" },
{ label: "最近通过方式", value: st.lastSolvedBy ? (CF_SOLVED_BY[st.lastSolvedBy] ?? st.lastSolvedBy) : "" },
{ label: "最近通过时间", value: t(st.lastSolvedAt) },
@@ -236,7 +236,7 @@ export function cloudflareTechItems(config: unknown): { label: string; value: st
label: "cf_clearance 复用",
value: st.cfReuse === "ok" ? "有效(ok)" : st.cfReuse === "fails" ? "无效(fails)" : "",
},
{ label: "浏览器代理至", value: t(st.browserUntil) },
{ label: "浏览器代抓到", value: t(st.browserUntil) },
{ label: "最近尝试", value: t(st.lastAttemptAt) },
{ label: "最近错误", value: st.lastError ?? "" },
].filter((it) => it.value);
@@ -317,7 +317,7 @@ export function loginFailureText(
return { text: "登录失败:站点要求二次验证。填写 TOTP 密钥后重试,或改用 Cookie", suggestCookie: true };
case "challenge":
return {
text: "站点要求人机验证(Cloudflare)。会用浏览器自动过,过不去时在铃铛里提醒你",
text: "站点要求人机验证(Cloudflare)。会先用浏览器自动通过,失败时在铃铛里提醒",
suggestCookie: false,
};
case "bad_credentials":
@@ -333,11 +333,11 @@ export function loginFailureText(
suggestCookie: false,
};
case "network":
return { text: `连不上站点${message ? `:${message}` : ""}`, suggestCookie: false };
return { text: `连不上站点${message ? `:${message}` : ""}`, suggestCookie: false };
case "unsupported":
return { text: message ?? "这个站点不支持该凭证类型的自动登录", suggestCookie: false };
return { text: message ?? "这个站点不支持用这种凭证自动登录", suggestCookie: false };
default:
return { text: message ?? "登录失败,原因未知", suggestCookie: false };
return { text: message ?? "登录失败", suggestCookie: false };
}
}
@@ -365,7 +365,7 @@ export const REASON_SHORT: Record<SiteLoginFailureReason, string> = {
two_factor: "需二次验证",
bad_credentials: "密码错误",
account_blocked: "账号封禁",
challenge: "Cloudflare 挑战",
challenge: "Cloudflare 验证",
auth_expired: "凭证失效",
network: "网络失败",
unsupported: "不支持",
@@ -470,7 +470,7 @@ function OnlinePanel({
) : null}
{data && !data.noSites && !data.results.length ? (
<p className="text-caption text-muted-foreground">
没搜到(用了:{data.keywords.join("、") || "无"})。可以换个关键词,或去掉季集限制再试。
没有找到字幕(搜索词:{data.keywords.join("、") || "无"})。换个关键词,或去掉季集限制再试。
</p>
) : null}
{data?.results.length ? (