feat(web): settings with 9 sidebar sections and Agent upstream management
- /console/settings#<section>[/<anchor>]; legacy ?tab= redirected in clientLoader - SETTINGS_SECTIONS in contracts with field-ownership test - Agent section: upstream list, create wizard (provider -> credentials/model -> login/test/default), edit drawer with OAuth browser/device login, test, set default, delete - sites section with site registration, snapshot/rate-limit drawer and account credentials - path mappings moved into downloader / media-server drawers; metadata and account sections Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
459991421e
commit
c67f6a9682
25 files changed
+3842
-942
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import type { PiAuthKind, PiProviderGroup, PiProviderInfo, PiThinkingLevel, PiUpstream } from "@umt/contracts";
|
||||
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
|
||||
// Agent 上游的界面标签(术语见 docs/design/glossary.md:上游配置叫"Agent 上游")
|
||||
|
||||
export const GROUP_LABEL: Record<PiProviderGroup, string> = {
|
||||
api_key: "API Key",
|
||||
subscription: "订阅登录",
|
||||
cloud: "云凭据",
|
||||
custom: "兼容端点",
|
||||
test: "测试",
|
||||
};
|
||||
export const GROUP_ORDER: PiProviderGroup[] = ["api_key", "subscription", "cloud", "custom", "test"];
|
||||
|
||||
export const AUTH_LABEL: Record<PiAuthKind, string> = {
|
||||
api_key: "API Key",
|
||||
oauth: "订阅登录",
|
||||
none: "不需要凭据",
|
||||
};
|
||||
|
||||
export const THINKING_LABEL: Record<PiThinkingLevel, string> = {
|
||||
off: "关闭",
|
||||
minimal: "最低",
|
||||
low: "低",
|
||||
medium: "中",
|
||||
high: "高",
|
||||
xhigh: "很高",
|
||||
max: "最高",
|
||||
};
|
||||
export const THINKING_OPTIONS = (Object.keys(THINKING_LABEL) as PiThinkingLevel[]).map((v) => ({
|
||||
value: v,
|
||||
label: `${THINKING_LABEL[v]}(${v})`,
|
||||
}));
|
||||
|
||||
export const API_OPTIONS = [
|
||||
{ value: "openai-completions", label: "OpenAI Chat Completions" },
|
||||
{ value: "openai-responses", label: "OpenAI Responses" },
|
||||
{ value: "anthropic-messages", label: "Anthropic Messages" },
|
||||
{ value: "google-generative-ai", label: "Google Generative AI" },
|
||||
];
|
||||
|
||||
export function upstreamStatus(u: Pick<PiUpstream, "status" | "authKind" | "enabled">): {
|
||||
label: string;
|
||||
tone: StatusTone;
|
||||
} {
|
||||
if (!u.enabled) return { label: "已停用", tone: "neutral" };
|
||||
switch (u.status) {
|
||||
case "ok":
|
||||
return { label: "可用", tone: "success" };
|
||||
case "auth_required":
|
||||
return { label: u.authKind === "oauth" ? "待登录" : "缺 API Key", tone: "warning" };
|
||||
case "expired":
|
||||
return { label: "登录已失效", tone: "destructive" };
|
||||
case "error":
|
||||
return { label: "出错", tone: "destructive" };
|
||||
}
|
||||
}
|
||||
|
||||
export function UpstreamStatusBadge({ u }: { u: Pick<PiUpstream, "status" | "authKind" | "enabled"> }) {
|
||||
const s = upstreamStatus(u);
|
||||
return <StatusBadge tone={s.tone}>{s.label}</StatusBadge>;
|
||||
}
|
||||
|
||||
export const providerName = (providers: PiProviderInfo[] | undefined, id: string) =>
|
||||
providers?.find((p) => p.id === id)?.name ?? id;
|
||||
|
||||
/** 模型 id:可从列表挑,也可手填(目录滞后于上游时) */
|
||||
export function ModelInput({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
models,
|
||||
disabled,
|
||||
invalid,
|
||||
placeholder,
|
||||
}: {
|
||||
id: string;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
models: { id: string; name: string }[];
|
||||
disabled?: boolean;
|
||||
invalid?: boolean;
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const listId = `${id}-models`;
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id={id}
|
||||
list={listId}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
aria-invalid={invalid}
|
||||
autoComplete="off"
|
||||
placeholder={placeholder ?? (models.length ? `从 ${models.length} 个模型中选,或手填 id` : "模型 id")}
|
||||
/>
|
||||
<datalist id={listId}>
|
||||
{models.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.name}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user