style(copy): punctuation rules and site-wide fix with a CI check (QA U11)
- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing, full-width parens around Chinese, single ellipsis, 「」 quotes, period rules) - scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX text (not comments, types, keys, match/compare strings); --fix, --strict, --by-file; wired into deno task ci as check:copy in warn mode - --fix applied to web, server and contracts; files other lanes were editing (subscriptions, sites, browser, agent, auth/account, inventory) left for them - tests updated for the changed messages Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9b017906cf
commit
52402864d6
143 files changed
+985
-606
No files matched your search
@@ -26,9 +26,9 @@ import { SettingsBlock } from "./settings-block.tsx";
|
||||
|
||||
const SCOPES: { value: ApiScope; label: string; hint: string }[] = [
|
||||
{ value: "read", label: "read", hint: "只读查询全部数据" },
|
||||
{ value: "organize.read", label: "organize.read", hint: "只读整理记录,供外部增量同步" },
|
||||
{ value: "organize.read", label: "organize.read", hint: "只读整理记录,供外部增量同步" },
|
||||
{ value: "organize.execute", label: "organize.execute", hint: "发起整理" },
|
||||
{ value: "admin", label: "admin", hint: "全部管理权限,含改设置" },
|
||||
{ value: "admin", label: "admin", hint: "全部管理权限,含改设置" },
|
||||
];
|
||||
|
||||
function CreatedKeyDialog({ plaintext, onClose }: { plaintext: string | null; onClose: () => void }) {
|
||||
@@ -38,7 +38,7 @@ function CreatedKeyDialog({ plaintext, onClose }: { plaintext: string | null; on
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>API Key 已创建</DialogTitle>
|
||||
<DialogDescription>明文只显示这一次,关闭后无法再查看。</DialogDescription>
|
||||
<DialogDescription>明文只显示这一次,关闭后无法再查看。</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex min-w-0 items-center gap-2 rounded-lg border border-warning/40 bg-warning/10 p-3">
|
||||
<code className="min-w-0 flex-1 font-mono text-caption break-all text-foreground">{plaintext}</code>
|
||||
@@ -52,7 +52,7 @@ function CreatedKeyDialog({ plaintext, onClose }: { plaintext: string | null; on
|
||||
setCopied(true);
|
||||
toast.success("已复制");
|
||||
} catch {
|
||||
toast.error("复制失败,手动选中复制");
|
||||
toast.error("复制失败,手动选中复制");
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -83,7 +83,7 @@ export function ApiKeysSection() {
|
||||
const parsed = ApiKeyCreate.safeParse({ name: name.trim(), scopes });
|
||||
if (!parsed.success) {
|
||||
const fe = zodFieldErrors(parsed.error.issues);
|
||||
if (fe.name) fe.name = "名称必填,80 字以内";
|
||||
if (fe.name) fe.name = "名称必填,80 字以内";
|
||||
if (fe.scopes) fe.scopes = "至少选一个权限范围";
|
||||
setErrors(fe);
|
||||
return;
|
||||
@@ -105,7 +105,7 @@ export function ApiKeysSection() {
|
||||
section="api-keys"
|
||||
anchor="keys"
|
||||
title="已签发的 Key"
|
||||
description="给媒体服务器插件、脚本等外部程序用,按权限范围授权。本机 Token 在「账户与安全」。"
|
||||
description="给媒体服务器插件、脚本等外部程序用,按权限范围授权。本机 Token 在「账户与安全」。"
|
||||
plain
|
||||
actions={
|
||||
<Button size="sm" onClick={() => setOpen(true)}>
|
||||
@@ -226,7 +226,7 @@ export function ApiKeysSection() {
|
||||
open={!!revoking}
|
||||
onOpenChange={(o) => !o && setRevoking(null)}
|
||||
title={`撤销「${revoking?.name ?? ""}」?`}
|
||||
description="使用这个 Key 的程序立即失去访问权限,无法恢复。"
|
||||
description="使用这个 Key 的程序立即失去访问权限,无法恢复。"
|
||||
confirmLabel="撤销 Key"
|
||||
destructive
|
||||
onConfirm={async () => {
|
||||
|
||||
@@ -16,12 +16,12 @@ const GiB = 1024 ** 3;
|
||||
const CACHE_CARD: CardSpec = {
|
||||
anchor: "cache",
|
||||
title: "缓存水位",
|
||||
description: "缓存卷使用率到硬阈值时停止新增下载并暂停纳管任务;降到恢复水位以下只恢复因阈值暂停的任务。",
|
||||
description: "缓存卷使用率到硬阈值时停止新增下载并暂停纳管任务;降到恢复水位以下只恢复因阈值暂停的任务。",
|
||||
fields: [
|
||||
{ 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" },
|
||||
{ name: "cache.checkIntervalSec", label: "检查间隔(秒)", kind: "number", hint: "10–3600" },
|
||||
],
|
||||
fromSettings: (s) => ({
|
||||
"cache.hardThresholdPercent": n(s.cache.hardThresholdPercent),
|
||||
@@ -42,7 +42,7 @@ const CACHE_CARD: CardSpec = {
|
||||
const BUDGET_CARD: CardSpec = {
|
||||
anchor: "budget",
|
||||
title: "每日预算",
|
||||
description: "任一项到上限后不再建新的下载计划;前两项按自然日计。",
|
||||
description: "任一项到上限后不再建新的下载计划;前两项按自然日计。",
|
||||
fields: [
|
||||
{ name: "budget.maxNewTitlesPerDay", label: "每日新作品数", kind: "number" },
|
||||
{ name: "budget.maxDownloadGiBPerDay", label: "每日下载量(GiB)", kind: "number" },
|
||||
@@ -73,7 +73,7 @@ const RETENTION_CARD: CardSpec = {
|
||||
name: "retention.seedRetentionDays",
|
||||
label: "源文件保留天数",
|
||||
kind: "number",
|
||||
hint: "复制入库后下载源在缓存里至少留这么久;0–365",
|
||||
hint: "复制入库后下载源在缓存里至少留这么久;0–365",
|
||||
},
|
||||
{ name: "retention.logRetentionDays", label: "日志保留天数", kind: "number", hint: "观察与日志类数据" },
|
||||
],
|
||||
@@ -149,7 +149,7 @@ function Switches() {
|
||||
icon={s.automation.enabled ? <Play className="h-4 w-4" /> : <Pause className="h-4 w-4" />}
|
||||
tone={s.automation.enabled ? "success" : "warning"}
|
||||
title={`自动化总开关 · ${s.automation.enabled ? "运行中" : "已暂停"}`}
|
||||
description="关闭后自动收录、下载、整理全部停止;查询与手动操作不受影响。"
|
||||
description="关闭后自动收录、下载、整理全部停止;查询与手动操作不受影响。"
|
||||
checked={s.automation.enabled}
|
||||
disabled={update.isPending}
|
||||
onChange={(v) => set({ enabled: v }, v ? "自动化已恢复" : "自动化已暂停")}
|
||||
@@ -159,7 +159,7 @@ function Switches() {
|
||||
icon={<ShieldAlert className="h-4 w-4" />}
|
||||
tone={s.automation.readOnlyReconcile ? "warning" : "success"}
|
||||
title={`只读对账 · ${s.automation.readOnlyReconcile ? "开启" : "关闭"}`}
|
||||
description="从备份恢复后先开启:只核对库存与路径,不做自动写入。核对完再关闭。"
|
||||
description="从备份恢复后先开启:只核对库存与路径,不做自动写入。核对完再关闭。"
|
||||
checked={s.automation.readOnlyReconcile}
|
||||
disabled={update.isPending}
|
||||
onChange={(v) => set({ readOnlyReconcile: v }, v ? "已开启只读对账" : "已关闭只读对账")}
|
||||
|
||||
@@ -87,7 +87,7 @@ function DownloadersTable() {
|
||||
config={{
|
||||
noun: "下载器实例",
|
||||
description:
|
||||
"只接管带管理标签的任务。多个实例共用一块缓存盘时填相同的缓存卷标识,水位合并计算。点行编辑实例和路径映射。",
|
||||
"只接管带管理标签的任务。多个实例共用一块缓存盘时填相同的缓存卷标识,水位合并计算。点行编辑实例和路径映射。",
|
||||
emptyHint: "添加 qBittorrent 后才能提交下载。",
|
||||
drawerExtra: (r) => <PathMappingsPanel viewKind="downloader" instanceId={r.id} />,
|
||||
columns: [
|
||||
@@ -113,7 +113,7 @@ function DownloadersTable() {
|
||||
],
|
||||
rowActions: (r) => <TestButton pending={test.isPending} run={() => test.mutateAsync(r.id)} />,
|
||||
fields: [
|
||||
{ name: "name", label: "名称", kind: "text", required: true, placeholder: "例如:家里的 qB" },
|
||||
{ name: "name", label: "名称", kind: "text", required: true, placeholder: "例如:家里的 qB" },
|
||||
{
|
||||
name: "baseUrl",
|
||||
label: "WebUI 地址",
|
||||
@@ -181,7 +181,7 @@ function MediaServersTable() {
|
||||
hooks={{ list, create, update, remove } as never}
|
||||
config={{
|
||||
noun: "媒体服务器",
|
||||
description: "只用来查询作品有无、季集和版本;查不到不等于缺失。点行编辑实例和路径映射。",
|
||||
description: "只用来查询作品有无、季集和版本;查不到不等于缺失。点行编辑实例和路径映射。",
|
||||
drawerExtra: (r) => <PathMappingsPanel viewKind="media_server" instanceId={r.id} />,
|
||||
columns: [
|
||||
{
|
||||
@@ -211,7 +211,7 @@ function MediaServersTable() {
|
||||
],
|
||||
},
|
||||
{ name: "baseUrl", label: "地址", kind: "url", required: true, placeholder: "http://192.168.1.10:8096" },
|
||||
{ name: "userId", label: "用户 ID", kind: "text", hint: "可选,按该用户的可见范围查询" },
|
||||
{ name: "userId", label: "用户 ID", kind: "text", hint: "可选,按该用户的可见范围查询" },
|
||||
{ name: "credential", label: "API Key", kind: "password", ...credentialHint },
|
||||
{ name: "enabled", label: "启用", kind: "switch" },
|
||||
],
|
||||
@@ -258,8 +258,9 @@ export function LibraryRootsTable() {
|
||||
hooks={{ list, create, update, remove } as never}
|
||||
config={{
|
||||
noun: "库根",
|
||||
description: "登记或改路径时会在目录里写哨兵文件 .uhd-root,据此判断库根是否在线;目录不存在或不可写时无法保存。",
|
||||
deleteHint: "只删除登记,磁盘上的文件不动。",
|
||||
description:
|
||||
"登记或改路径时会在目录里写哨兵文件 .uhd-root,据此判断库根是否在线;目录不存在或不可写时无法保存。",
|
||||
deleteHint: "只删除登记,磁盘上的文件不动。",
|
||||
columns: [
|
||||
{
|
||||
key: "name",
|
||||
@@ -287,7 +288,7 @@ export function LibraryRootsTable() {
|
||||
],
|
||||
rowActions: (r) => <TestButton label="检查哨兵" pending={test.isPending} run={() => test.mutateAsync(r.id)} />,
|
||||
fields: [
|
||||
{ name: "name", label: "名称", kind: "text", required: true, placeholder: "例如:电影库" },
|
||||
{ name: "name", label: "名称", kind: "text", required: true, placeholder: "例如:电影库" },
|
||||
{ name: "mediaKind", label: "内容类型", kind: "select", options: MEDIA_KIND },
|
||||
{
|
||||
name: "localPath",
|
||||
@@ -304,7 +305,7 @@ export function LibraryRootsTable() {
|
||||
kind: "select",
|
||||
options: [
|
||||
{ value: "register_only", label: "只登记" },
|
||||
{ value: "managed", label: "自动维护(补齐、升级)" },
|
||||
{ value: "managed", label: "自动维护(补齐、升级)" },
|
||||
],
|
||||
},
|
||||
{ name: "enabled", label: "启用", kind: "switch" },
|
||||
|
||||
@@ -9,15 +9,15 @@ import { type CardSpec, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
||||
const ORGANIZE_CARD: CardSpec = {
|
||||
anchor: "organize",
|
||||
title: "整理方式",
|
||||
description: "复制入库时源文件留在缓存继续做种,保留天数在自动化分区设置。",
|
||||
description: "复制入库时源文件留在缓存继续做种,保留天数在自动化分区设置。",
|
||||
fields: [
|
||||
{
|
||||
name: "organize.defaultMode",
|
||||
label: "默认整理方式",
|
||||
kind: "select",
|
||||
options: [
|
||||
{ value: "copy", label: "复制(源文件继续做种)" },
|
||||
{ value: "move", label: "移动(移走源文件)" },
|
||||
{ value: "copy", label: "复制(源文件继续做种)" },
|
||||
{ value: "move", label: "移动(移走源文件)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ const TMDB_CARD: CardSpec = {
|
||||
name: "metadata.tmdbApiKey",
|
||||
label: "API Key",
|
||||
kind: "password",
|
||||
hint: "v3 API Key 或 v4 Read Access Token;留空不修改",
|
||||
hint: "v3 API Key 或 v4 Read Access Token;留空不修改",
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
@@ -25,7 +25,7 @@ const TMDB_CARD: CardSpec = {
|
||||
label: "Base URL",
|
||||
kind: "url",
|
||||
placeholder: "https://api.themoviedb.org",
|
||||
hint: "自建反代时填写;留空用官方地址",
|
||||
hint: "自建反代时填写;留空用官方地址",
|
||||
wide: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -56,7 +56,7 @@ function GlobalNaming() {
|
||||
} catch (err) {
|
||||
const split = splitNamingErrors(errorMessage(err));
|
||||
if (split) setErrors(split);
|
||||
toast.error(split ? "模板有错误,未保存" : errorMessage(err, "保存失败"));
|
||||
toast.error(split ? "模板有错误,未保存" : errorMessage(err, "保存失败"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,7 +100,7 @@ function GlobalNaming() {
|
||||
<span className="min-w-0">
|
||||
<span className="block text-body text-foreground">已有目录的作品固定文件名</span>
|
||||
<span className="block text-meta text-muted-foreground">
|
||||
开启后,这些作品的新文件按建立目录时的模板版本命名
|
||||
开启后,这些作品的新文件按建立目录时的模板版本命名
|
||||
</span>
|
||||
</span>
|
||||
<Switch
|
||||
@@ -127,7 +127,7 @@ function RootNaming() {
|
||||
section="library"
|
||||
anchor="naming-roots"
|
||||
title="库根命名覆盖"
|
||||
description="按类型覆盖全局模板;留空的类型沿用全局。"
|
||||
description="按类型覆盖全局模板;留空的类型沿用全局。"
|
||||
>
|
||||
{roots.isPending ? (
|
||||
<LoadingState rows={2} />
|
||||
@@ -146,7 +146,7 @@ function RootNaming() {
|
||||
{list.map((r) => (
|
||||
<SelectItem key={r.id} value={r.id}>
|
||||
{r.name}
|
||||
{r.naming && Object.keys(r.naming).length ? "(已覆盖)" : ""}
|
||||
{r.naming && Object.keys(r.naming).length ? "(已覆盖)" : ""}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -187,7 +187,7 @@ function RootNamingEditor({ root }: { root: LibraryRoot }) {
|
||||
} catch (err) {
|
||||
const split = splitNamingErrors(errorMessage(err));
|
||||
if (split) setErrors(split);
|
||||
toast.error(split ? "模板有错误,未保存" : errorMessage(err, "保存失败"));
|
||||
toast.error(split ? "模板有错误,未保存" : errorMessage(err, "保存失败"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -88,12 +88,12 @@ export function PathMappingsPanel({
|
||||
>
|
||||
<p className="mb-2 text-meta text-muted-foreground">
|
||||
把{viewKind === "downloader" ? "下载器" : "媒体服务器"}
|
||||
看到的路径换成本服务能访问的路径。前缀越长越优先,同长比优先级。
|
||||
看到的路径换成本服务能访问的路径。前缀越长越优先,同长比优先级。
|
||||
</p>
|
||||
{list.isPending ? (
|
||||
<p className="text-caption text-muted-foreground">加载中…</p>
|
||||
) : rows.length === 0 ? (
|
||||
<p className="text-caption text-muted-foreground">还没有映射,路径两边一致时不用加。</p>
|
||||
<p className="text-caption text-muted-foreground">还没有映射,路径两边一致时不用加。</p>
|
||||
) : (
|
||||
<ul className="divide-y divide-border/60 rounded-lg border border-border/60">
|
||||
{rows.map((m) => {
|
||||
@@ -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>
|
||||
@@ -209,8 +209,8 @@ export function PathMappingsPanel({
|
||||
<ConfirmDialog
|
||||
open={!!deleting}
|
||||
onOpenChange={(o) => !o && setDeleting(null)}
|
||||
title="删除路径映射?"
|
||||
description="依赖这条映射的路径换算会失效,磁盘文件不动。"
|
||||
title="删除路径映射?"
|
||||
description="依赖这条映射的路径换算会失效,磁盘文件不动。"
|
||||
confirmLabel="删除映射"
|
||||
destructive
|
||||
onConfirm={async () => {
|
||||
|
||||
@@ -94,7 +94,7 @@ export function ModelInput({
|
||||
disabled={disabled}
|
||||
aria-invalid={invalid}
|
||||
autoComplete="off"
|
||||
placeholder={placeholder ?? (models.length ? `从 ${models.length} 个模型中选,或手填 id` : "模型 id")}
|
||||
placeholder={placeholder ?? (models.length ? `从 ${models.length} 个模型中选,或手填 id` : "模型 id")}
|
||||
/>
|
||||
<datalist id={listId}>
|
||||
{models.map((m) => (
|
||||
|
||||
@@ -22,9 +22,9 @@ const SOURCE_TEXT: Record<string, string> = { "stored credential": "已存凭证
|
||||
|
||||
/** 上游返回的多是英文原文:先给一句怎么办,原文留作排查 */
|
||||
const ERROR_HINT: Record<string, string> = {
|
||||
network: "连不上上游,检查 Base URL、端口和网络",
|
||||
auth: "凭证被拒,检查 API Key 或重新订阅登录",
|
||||
model: "模型不可用,换一个模型 id",
|
||||
network: "连不上上游,检查 Base URL、端口和网络",
|
||||
auth: "凭证被拒,检查 API Key 或重新订阅登录",
|
||||
model: "模型不可用,换一个模型 id",
|
||||
};
|
||||
|
||||
export function PiTestPanel({ upstream, model }: { upstream: PiUpstream; model?: string | null }) {
|
||||
@@ -60,7 +60,7 @@ export function PiTestPanel({ upstream, model }: { upstream: PiUpstream; model?:
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-meta text-muted-foreground">
|
||||
「测试」只检查凭证和模型,不产生费用。最小补全会调用一次模型(最多 16 个 token)。
|
||||
「测试」只检查凭证和模型,不产生费用。最小补全会调用一次模型(最多 16 个 token)。
|
||||
</p>
|
||||
{result ? (
|
||||
<div className="flex flex-wrap items-center gap-2 text-caption">
|
||||
@@ -73,7 +73,7 @@ export function PiTestPanel({ upstream, model }: { upstream: PiUpstream; model?:
|
||||
</span>
|
||||
{!result.ok && result.message ? (
|
||||
<span className="w-full break-words text-destructive">
|
||||
{ERROR_HINT[result.errorKind ?? ""] ? `${ERROR_HINT[result.errorKind ?? ""]}。原始错误:` : ""}
|
||||
{ERROR_HINT[result.errorKind ?? ""] ? `${ERROR_HINT[result.errorKind ?? ""]}。原始错误:` : ""}
|
||||
{result.message}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -230,7 +230,7 @@ function UpstreamDrawer({
|
||||
{upstream.oauthExpiresAt ? (
|
||||
<>
|
||||
<dt className="text-muted-foreground">令牌到期</dt>
|
||||
<dd>{formatDateTime(upstream.oauthExpiresAt)}(到期前自动刷新)</dd>
|
||||
<dd>{formatDateTime(upstream.oauthExpiresAt)}(到期前自动刷新)</dd>
|
||||
</>
|
||||
) : null}
|
||||
<dt className="text-muted-foreground">最近验证</dt>
|
||||
@@ -268,7 +268,7 @@ function UpstreamDrawer({
|
||||
}
|
||||
>
|
||||
{upstream.hasCredential && upstream.status === "ok" ? (
|
||||
<p className="text-caption text-muted-foreground">已登录。退出只删除本地令牌,不会撤销提供方侧的授权。</p>
|
||||
<p className="text-caption text-muted-foreground">已登录。退出只删除本地令牌,不会撤销提供方侧的授权。</p>
|
||||
) : (
|
||||
<PiOAuthLogin upstream={upstream} provider={provider} />
|
||||
)}
|
||||
@@ -285,7 +285,7 @@ function UpstreamDrawer({
|
||||
disabled={pending}
|
||||
/>
|
||||
<FieldControl
|
||||
spec={{ name: "enabled", label: "启用", kind: "switch", hint: "停用后,使用它的运行会失败" }}
|
||||
spec={{ name: "enabled", label: "启用", kind: "switch", hint: "停用后,使用它的运行会失败" }}
|
||||
value={v.enabled}
|
||||
onChange={(x) => setV((s) => ({ ...s, enabled: x === true }))}
|
||||
disabled={pending}
|
||||
@@ -294,7 +294,7 @@ function UpstreamDrawer({
|
||||
label="默认模型"
|
||||
htmlFor="pu-model"
|
||||
error={errors.defaultModel}
|
||||
hint={models.data?.refreshError ? `模型列表刷新失败:${models.data.refreshError}` : "可从列表选,也可手填"}
|
||||
hint={models.data?.refreshError ? `模型列表刷新失败:${models.data.refreshError}` : "可从列表选,也可手填"}
|
||||
className="sm:col-span-2"
|
||||
>
|
||||
<ModelInput
|
||||
@@ -312,7 +312,7 @@ function UpstreamDrawer({
|
||||
label: "思考档位",
|
||||
kind: "select",
|
||||
options: THINKING_OPTIONS,
|
||||
hint: `当前 ${THINKING_LABEL[upstream.thinkingLevel]};运行时按模型支持的档位取近`,
|
||||
hint: `当前 ${THINKING_LABEL[upstream.thinkingLevel]};运行时按模型支持的档位取近`,
|
||||
}}
|
||||
value={v.thinking}
|
||||
onChange={(x) => setV((s) => ({ ...s, thinking: x as PiThinkingLevel }))}
|
||||
@@ -369,9 +369,9 @@ function UpstreamDrawer({
|
||||
<ConfirmDialog
|
||||
open={deleting}
|
||||
onOpenChange={setDeleting}
|
||||
title={`删除上游「${upstream.name}」?`}
|
||||
title={`删除上游「${upstream.name}」?`}
|
||||
description={
|
||||
upstream.isDefault ? "它是默认上游,删除后没有默认上游;凭证一并删除,无法恢复。" : "凭证一并删除,无法恢复。"
|
||||
upstream.isDefault ? "它是默认上游,删除后没有默认上游;凭证一并删除,无法恢复。" : "凭证一并删除,无法恢复。"
|
||||
}
|
||||
confirmLabel="删除上游"
|
||||
destructive
|
||||
|
||||
@@ -49,12 +49,12 @@ export function PiUpstreamWizard({ open, onClose }: { open: boolean; onClose: ()
|
||||
: (created?.name ?? "");
|
||||
const description =
|
||||
step === "provider"
|
||||
? "第 1 步,共 3 步:选 provider"
|
||||
? "第 1 步,共 3 步:选 provider"
|
||||
: step === "config"
|
||||
? "第 2 步,共 3 步:凭证与模型"
|
||||
? "第 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">
|
||||
@@ -95,7 +95,7 @@ function ProviderPicker({ onPick }: { onPick: (p: PiProviderInfo) => void }) {
|
||||
<Search className="pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
aria-label="筛选 provider"
|
||||
placeholder="筛选,例如 openai"
|
||||
placeholder="筛选,例如 openai"
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="pl-9"
|
||||
@@ -180,7 +180,7 @@ function ConfigStep({
|
||||
const fe: Record<string, string> = parsed.success ? {} : zodFieldErrors(parsed.error.issues);
|
||||
for (const f of provider.envFields) if (f.required && !env[f.name]?.trim()) fe[`env.${f.name}`] = `${f.label}必填`;
|
||||
if (authKind === "api_key" && !apiKey.trim() && !custom) fe.apiKey = "填写 API Key";
|
||||
if (fe.name) fe.name = "名称必填,64 字以内";
|
||||
if (fe.name) fe.name = "名称必填,64 字以内";
|
||||
if (fe["extra.contextWindow"]) fe.contextWindow = "填 1024 以上的整数";
|
||||
if (!parsed.success || Object.keys(fe).length) {
|
||||
setErrors(fe);
|
||||
@@ -236,7 +236,7 @@ function ConfigStep({
|
||||
kind: "password",
|
||||
required: !custom,
|
||||
wide: true,
|
||||
hint: custom ? "端点不要求鉴权时可留空" : "只写入,保存后只显示前后 4 位",
|
||||
hint: custom ? "端点不要求鉴权时可留空" : "只写入,保存后只显示前后 4 位",
|
||||
}}
|
||||
value={apiKey}
|
||||
error={errors.apiKey}
|
||||
@@ -246,7 +246,7 @@ function ConfigStep({
|
||||
) : null}
|
||||
{authKind === "none" && provider.group === "cloud" ? (
|
||||
<p className="mb-3 text-caption text-muted-foreground sm:col-span-2">
|
||||
不存密钥,运行时读服务端进程的环境凭证(AWS 凭证链 / Google ADC)。
|
||||
不存密钥,运行时读服务端进程的环境凭证(AWS 凭证链 / Google ADC)。
|
||||
</p>
|
||||
) : null}
|
||||
{showBaseUrl ? (
|
||||
@@ -375,7 +375,7 @@ function VerifyStep({
|
||||
<FormField
|
||||
label="默认模型"
|
||||
htmlFor="pv-model"
|
||||
hint={models.data?.refreshError ? `模型列表刷新失败:${models.data.refreshError}` : "可从列表选,也可手填"}
|
||||
hint={models.data?.refreshError ? `模型列表刷新失败:${models.data.refreshError}` : "可从列表选,也可手填"}
|
||||
>
|
||||
<ModelInput id="pv-model" value={model} onChange={setModel} models={models.data?.items ?? []} />
|
||||
</FormField>
|
||||
|
||||
@@ -214,7 +214,7 @@ function ResourceFormDrawer<Row extends { id: string }>({
|
||||
open={!!row}
|
||||
onOpenChange={(o) => !o && !pending && onClose()}
|
||||
title={`${mode === "create" ? "添加" : "编辑"}${config.noun}`}
|
||||
description="凭证只写入,保存后不回显。"
|
||||
description="凭证只写入,保存后不回显。"
|
||||
footer={
|
||||
<>
|
||||
<Button variant="outline" onClick={onClose} disabled={pending}>
|
||||
|
||||
Reference in new issue
Block a user