feat: add ai account onboarding entry points
This commit is contained in:
@@ -13,7 +13,7 @@ export default async function AiAccountsPage() {
|
||||
<AppShell bottomNav={false}>
|
||||
<StatusBar />
|
||||
<PageNav title="AI 账号" backHref="/me" />
|
||||
<HeaderTitle title="主控与账号状态" />
|
||||
<HeaderTitle title="主控身份与接入入口" />
|
||||
<AiAccountsClient
|
||||
accounts={data.accounts}
|
||||
activeIdentity={data.activeIdentity}
|
||||
|
||||
@@ -26,6 +26,25 @@ type AccountDraft = {
|
||||
loginStatusNote: string;
|
||||
};
|
||||
|
||||
type OnboardingMode = "openai_api" | "master_codex_node" | null;
|
||||
|
||||
type OpenAiOnboardDraft = {
|
||||
label: string;
|
||||
displayName: string;
|
||||
accountIdentifier: string;
|
||||
model: string;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
type MasterNodeOnboardDraft = {
|
||||
label: string;
|
||||
displayName: string;
|
||||
accountIdentifier: string;
|
||||
nodeId: string;
|
||||
nodeLabel: string;
|
||||
model: string;
|
||||
};
|
||||
|
||||
function roleOptions() {
|
||||
return [
|
||||
{ value: "primary", label: "主 GPT" },
|
||||
@@ -41,19 +60,24 @@ function providerOptions() {
|
||||
] as const;
|
||||
}
|
||||
|
||||
function emptyDraft(): AccountDraft {
|
||||
function defaultOpenAiOnboardDraft(): OpenAiOnboardDraft {
|
||||
return {
|
||||
label: "API 容灾",
|
||||
role: "api_fallback",
|
||||
provider: "openai_api",
|
||||
displayName: "",
|
||||
label: "主 GPT",
|
||||
displayName: "OpenAI 平台账号",
|
||||
accountIdentifier: "",
|
||||
model: "gpt-5.4",
|
||||
apiKey: "",
|
||||
};
|
||||
}
|
||||
|
||||
function defaultMasterNodeOnboardDraft(): MasterNodeOnboardDraft {
|
||||
return {
|
||||
label: "主 GPT",
|
||||
displayName: "Master Codex Node",
|
||||
accountIdentifier: "",
|
||||
nodeId: "",
|
||||
nodeLabel: "",
|
||||
model: "gpt-5.4",
|
||||
apiKey: "",
|
||||
enabled: true,
|
||||
loginStatusNote: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,10 +179,14 @@ export function AiAccountsClient({
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [drafts, setDrafts] = useState<Record<string, AccountDraft>>({});
|
||||
const [newDraft, setNewDraft] = useState<AccountDraft>(emptyDraft());
|
||||
const [busyKey, setBusyKey] = useState<string | null>(null);
|
||||
const [message, setMessage] = useState("");
|
||||
const [guideAccountId, setGuideAccountId] = useState<string | null>(null);
|
||||
const [onboardingMode, setOnboardingMode] = useState<OnboardingMode>(null);
|
||||
const [openAiOnboardDraft, setOpenAiOnboardDraft] = useState<OpenAiOnboardDraft>(defaultOpenAiOnboardDraft());
|
||||
const [masterNodeOnboardDraft, setMasterNodeOnboardDraft] = useState<MasterNodeOnboardDraft>(
|
||||
defaultMasterNodeOnboardDraft(),
|
||||
);
|
||||
|
||||
const accountDrafts = useMemo(
|
||||
() =>
|
||||
@@ -175,35 +203,23 @@ export function AiAccountsClient({
|
||||
}));
|
||||
}
|
||||
|
||||
async function saveAccount(accountId?: string) {
|
||||
const isNew = !accountId;
|
||||
const draft = isNew ? newDraft : accountDrafts[accountId];
|
||||
async function saveAccount(accountId: string) {
|
||||
const draft = accountDrafts[accountId];
|
||||
if (!draft.displayName.trim()) {
|
||||
setMessage("AI 账号名称不能为空。");
|
||||
return;
|
||||
}
|
||||
|
||||
setBusyKey(`${isNew ? "create" : "save"}:${accountId ?? "new"}`);
|
||||
const response = await fetch(isNew ? "/api/v1/accounts" : `/api/v1/accounts/${accountId}`, {
|
||||
method: isNew ? "POST" : "PATCH",
|
||||
setBusyKey(`save:${accountId}`);
|
||||
const response = await fetch(`/api/v1/accounts/${accountId}`, {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(draft),
|
||||
});
|
||||
const result = (await response.json()) as { ok: boolean; message?: string };
|
||||
setBusyKey(null);
|
||||
setMessage(
|
||||
result.ok
|
||||
? isNew
|
||||
? "AI 账号已创建。"
|
||||
: "AI 账号已更新。"
|
||||
: result.message || "AI 账号保存失败。",
|
||||
);
|
||||
if (result.ok) {
|
||||
if (isNew) {
|
||||
setNewDraft(emptyDraft());
|
||||
}
|
||||
router.refresh();
|
||||
}
|
||||
setMessage(result.ok ? "AI 账号已更新。" : result.message || "AI 账号保存失败。");
|
||||
if (result.ok) router.refresh();
|
||||
}
|
||||
|
||||
async function activateAccount(accountId: string) {
|
||||
@@ -245,6 +261,77 @@ export function AiAccountsClient({
|
||||
}
|
||||
}
|
||||
|
||||
function closeOnboarding() {
|
||||
setOnboardingMode(null);
|
||||
}
|
||||
|
||||
async function submitOpenAiOnboarding() {
|
||||
const draft = openAiOnboardDraft;
|
||||
if (!draft.displayName.trim() || !draft.apiKey.trim()) {
|
||||
setMessage("请先填写 OpenAI 平台账号名称和 API Key。");
|
||||
return;
|
||||
}
|
||||
|
||||
setBusyKey("onboard:openai_api");
|
||||
const response = await fetch("/api/v1/accounts/onboard/openai-api", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
label: draft.label.trim() || "主 GPT",
|
||||
displayName: draft.displayName.trim(),
|
||||
accountIdentifier: draft.accountIdentifier.trim(),
|
||||
model: draft.model.trim() || "gpt-5.4",
|
||||
apiKey: draft.apiKey.trim(),
|
||||
}),
|
||||
});
|
||||
const result = (await response.json()) as { ok: boolean; message?: string };
|
||||
setBusyKey(null);
|
||||
if (result.ok) {
|
||||
setMessage(result.message || "OpenAI 平台账号已登录,并设为当前主控。");
|
||||
setOpenAiOnboardDraft(defaultOpenAiOnboardDraft());
|
||||
closeOnboarding();
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
setMessage(result.message || "OpenAI 平台账号登录失败。");
|
||||
}
|
||||
|
||||
async function submitMasterNodeOnboarding() {
|
||||
const draft = masterNodeOnboardDraft;
|
||||
if (!draft.displayName.trim() || !draft.nodeId.trim()) {
|
||||
setMessage("请先填写 Master Codex Node 的名称和节点 ID。");
|
||||
return;
|
||||
}
|
||||
|
||||
setBusyKey("onboard:master_codex_node");
|
||||
const response = await fetch("/api/v1/accounts/onboard/master-node", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
label: draft.label.trim() || "主 GPT",
|
||||
displayName: draft.displayName.trim(),
|
||||
accountIdentifier: draft.accountIdentifier.trim(),
|
||||
nodeId: draft.nodeId.trim(),
|
||||
nodeLabel: draft.nodeLabel.trim(),
|
||||
model: draft.model.trim() || "gpt-5.4",
|
||||
}),
|
||||
});
|
||||
const result = (await response.json()) as { ok: boolean; message?: string };
|
||||
setBusyKey(null);
|
||||
if (result.ok) {
|
||||
setMessage(result.message || "Master Codex Node 已绑定。");
|
||||
setMasterNodeOnboardDraft(defaultMasterNodeOnboardDraft());
|
||||
closeOnboarding();
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
setMessage(result.message || "Master Codex Node 绑定失败。");
|
||||
}
|
||||
|
||||
function onboardingTitle() {
|
||||
return onboardingMode === "openai_api" ? "登录 OpenAI 平台账号" : "绑定电脑上的 Codex 节点";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-[18px] pb-6">
|
||||
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
|
||||
@@ -279,20 +366,62 @@ export function AiAccountsClient({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
|
||||
<div className="text-[16px] font-semibold text-[#111111]">设计约束</div>
|
||||
<div className="mt-2 text-[12px] leading-6 text-[#57606A]">
|
||||
原始设计里的“主 GPT / 备用 GPT”不是纯 API 概念,主链路优先走单独在线的 Master
|
||||
Codex Node,也就是已经在对应电脑上登录了 ChatGPT Plus / Codex 的执行节点。当前生产链路已经是
|
||||
<span className="font-medium text-[#111111]">
|
||||
Boss Web -> task queue -> local-agent -> codex exec -> 回写项目账本
|
||||
</span>
|
||||
,OpenAI API 只作为用户可配置的容灾补位。
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div>
|
||||
<div className="text-[16px] font-semibold text-[#111111]">接入入口</div>
|
||||
<div className="mt-1 text-[12px] text-[#8C8C8C]">先选接入方式,再进入账号列表管理。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 rounded-2xl bg-[#F7F8FA] px-3 py-3 text-[12px] leading-6 text-[#57606A]">
|
||||
如果你要新增一个“主 GPT / 备用 GPT”,这里直接新增多个
|
||||
<span className="font-medium text-[#111111]"> Master Codex Node </span>
|
||||
账号即可;真正的登录动作发生在那台绑定电脑上的 Codex / ChatGPT 会话里,APP 负责展示、切换和回退。
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setOnboardingMode("openai_api");
|
||||
setMessage("");
|
||||
}}
|
||||
disabled={!canManage}
|
||||
className="rounded-3xl border border-[#DDE6FF] bg-gradient-to-br from-[#F7FAFF] to-white p-4 text-left shadow-sm transition hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[16px] font-semibold text-[#111111]">登录 OpenAI 平台账号</div>
|
||||
<div className="mt-2 text-[12px] leading-6 text-[#57606A]">
|
||||
填写 API Key,校验通过后立即设为当前主控。
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-full bg-[#EEF5FF] px-3 py-1 text-[11px] font-semibold text-[#2457C5]">
|
||||
API 登录
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 rounded-2xl bg-white/80 px-3 py-3 text-[12px] leading-6 text-[#57606A]">
|
||||
入口字段:显示名称、账号标识、模型、API Key。
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setOnboardingMode("master_codex_node");
|
||||
setMessage("");
|
||||
}}
|
||||
disabled={!canManage}
|
||||
className="rounded-3xl border border-[#E5E5EA] bg-gradient-to-br from-[#FFF8EE] to-white p-4 text-left shadow-sm transition hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[16px] font-semibold text-[#111111]">绑定电脑上的 Codex 节点</div>
|
||||
<div className="mt-2 text-[12px] leading-6 text-[#57606A]">
|
||||
登录动作发生在绑定设备上,手机只负责展示、校验和切换。
|
||||
</div>
|
||||
</div>
|
||||
<span className="rounded-full bg-[#FFF5E8] px-3 py-1 text-[11px] font-semibold text-[#B54708]">
|
||||
节点绑定
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 rounded-2xl bg-white/80 px-3 py-3 text-[12px] leading-6 text-[#57606A]">
|
||||
入口字段:显示名称、节点 ID、节点名称、模型。
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -508,128 +637,9 @@ export function AiAccountsClient({
|
||||
{buildMasterNodeLoginGuide(account)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
|
||||
<div className="text-[16px] font-semibold text-[#111111]">新增 AI 账号</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-3">
|
||||
<AccountField
|
||||
label="标签"
|
||||
value={newDraft.label}
|
||||
onChange={(value) => setNewDraft((current) => ({ ...current, label: value }))}
|
||||
placeholder="例如:API 容灾"
|
||||
/>
|
||||
<AccountField
|
||||
label="AI 名称"
|
||||
value={newDraft.displayName}
|
||||
onChange={(value) => setNewDraft((current) => ({ ...current, displayName: value }))}
|
||||
placeholder="例如:OpenAI 生产主控"
|
||||
/>
|
||||
<label className="space-y-1">
|
||||
<div className="text-[12px] text-[#8C8C8C]">角色</div>
|
||||
<select
|
||||
value={newDraft.role}
|
||||
onChange={(event) =>
|
||||
setNewDraft((current) => ({ ...current, role: event.target.value as AiAccountRole }))
|
||||
}
|
||||
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
|
||||
>
|
||||
{roleOptions().map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<div className="text-[12px] text-[#8C8C8C]">类型</div>
|
||||
<select
|
||||
value={newDraft.provider}
|
||||
onChange={(event) =>
|
||||
setNewDraft((current) => ({ ...current, provider: event.target.value as AiProvider }))
|
||||
}
|
||||
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
|
||||
>
|
||||
{providerOptions().map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<AccountField
|
||||
label="账号标识"
|
||||
value={newDraft.accountIdentifier}
|
||||
onChange={(value) => setNewDraft((current) => ({ ...current, accountIdentifier: value }))}
|
||||
placeholder="手机号 / 邮箱 / 账号别名"
|
||||
/>
|
||||
<AccountField
|
||||
label="节点标签"
|
||||
value={newDraft.nodeLabel}
|
||||
onChange={(value) => setNewDraft((current) => ({ ...current, nodeLabel: value }))}
|
||||
placeholder="例如:备用 Mac mini"
|
||||
/>
|
||||
<AccountField
|
||||
label="节点 ID"
|
||||
value={newDraft.nodeId}
|
||||
onChange={(value) => setNewDraft((current) => ({ ...current, nodeId: value }))}
|
||||
placeholder="例如:mac-mini-02"
|
||||
/>
|
||||
<AccountField
|
||||
label="模型"
|
||||
value={newDraft.model}
|
||||
onChange={(value) => setNewDraft((current) => ({ ...current, model: value }))}
|
||||
placeholder="例如:gpt-5.4"
|
||||
/>
|
||||
{newDraft.provider === "openai_api" ? (
|
||||
<div className="col-span-2">
|
||||
<AccountField
|
||||
label="API Key"
|
||||
value={newDraft.apiKey}
|
||||
onChange={(value) => setNewDraft((current) => ({ ...current, apiKey: value }))}
|
||||
placeholder="输入 OpenAI API Key"
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<label className="mt-3 block space-y-1">
|
||||
<div className="text-[12px] text-[#8C8C8C]">状态备注</div>
|
||||
<textarea
|
||||
value={newDraft.loginStatusNote}
|
||||
onChange={(event) =>
|
||||
setNewDraft((current) => ({ ...current, loginStatusNote: event.target.value }))
|
||||
}
|
||||
rows={3}
|
||||
className="w-full rounded-2xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-3 text-[13px] leading-6 text-[#111111] outline-none"
|
||||
placeholder="例如:该节点已单独登录 ChatGPT,等待后续接入远程执行器。"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<label className="flex items-center gap-2 text-[13px] text-[#57606A]">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newDraft.enabled}
|
||||
onChange={(event) =>
|
||||
setNewDraft((current) => ({ ...current, enabled: event.target.checked }))
|
||||
}
|
||||
/>
|
||||
启用
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveAccount()}
|
||||
disabled={!canManage || busyKey === "create:new"}
|
||||
className="rounded-full bg-[#111111] px-4 py-2 text-[12px] font-semibold text-white"
|
||||
>
|
||||
{busyKey === "create:new" ? "创建中" : "新增账号"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
|
||||
<div className="text-[16px] font-semibold text-[#111111]">最近切换记录</div>
|
||||
@@ -658,6 +668,155 @@ export function AiAccountsClient({
|
||||
{message}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{onboardingMode ? (
|
||||
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/45 px-4 pb-4 pt-16">
|
||||
<div className="w-full max-w-[520px] rounded-[28px] bg-white p-4 shadow-2xl">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[18px] font-semibold text-[#111111]">{onboardingTitle()}</div>
|
||||
<div className="mt-1 text-[12px] leading-6 text-[#8C8C8C]">
|
||||
{onboardingMode === "openai_api"
|
||||
? "填写 API Key 后会立即校验,并将这个账号设为当前主控。"
|
||||
: "这个入口只负责绑定电脑节点,真正登录仍发生在那台电脑上。"}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeOnboarding}
|
||||
className="rounded-full bg-[#F3F4F6] px-3 py-1 text-[12px] font-semibold text-[#57606A]"
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{onboardingMode === "openai_api" ? (
|
||||
<div className="mt-4 space-y-3">
|
||||
<AccountField
|
||||
label="标签"
|
||||
value={openAiOnboardDraft.label}
|
||||
onChange={(value) => setOpenAiOnboardDraft((current) => ({ ...current, label: value }))}
|
||||
placeholder="例如:主 GPT"
|
||||
/>
|
||||
<AccountField
|
||||
label="显示名称"
|
||||
value={openAiOnboardDraft.displayName}
|
||||
onChange={(value) =>
|
||||
setOpenAiOnboardDraft((current) => ({ ...current, displayName: value }))
|
||||
}
|
||||
placeholder="例如:OpenAI 平台账号"
|
||||
/>
|
||||
<AccountField
|
||||
label="账号标识"
|
||||
value={openAiOnboardDraft.accountIdentifier}
|
||||
onChange={(value) =>
|
||||
setOpenAiOnboardDraft((current) => ({ ...current, accountIdentifier: value }))
|
||||
}
|
||||
placeholder="例如:sk-proj-***"
|
||||
/>
|
||||
<AccountField
|
||||
label="模型"
|
||||
value={openAiOnboardDraft.model}
|
||||
onChange={(value) => setOpenAiOnboardDraft((current) => ({ ...current, model: value }))}
|
||||
placeholder="例如:gpt-5.4"
|
||||
/>
|
||||
<AccountField
|
||||
label="API Key"
|
||||
value={openAiOnboardDraft.apiKey}
|
||||
onChange={(value) => setOpenAiOnboardDraft((current) => ({ ...current, apiKey: value }))}
|
||||
placeholder="输入 OpenAI API Key"
|
||||
type="password"
|
||||
/>
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitOpenAiOnboarding}
|
||||
disabled={!canManage || busyKey === "onboard:openai_api"}
|
||||
className="flex-1 rounded-full bg-[#111111] px-4 py-3 text-[13px] font-semibold text-white disabled:opacity-50"
|
||||
>
|
||||
{busyKey === "onboard:openai_api" ? "登录中" : "登录并设为当前主控"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeOnboarding}
|
||||
className="rounded-full border border-[#E5E5EA] px-4 py-3 text-[13px] font-semibold text-[#57606A]"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 space-y-3">
|
||||
<AccountField
|
||||
label="标签"
|
||||
value={masterNodeOnboardDraft.label}
|
||||
onChange={(value) =>
|
||||
setMasterNodeOnboardDraft((current) => ({ ...current, label: value }))
|
||||
}
|
||||
placeholder="例如:主 GPT"
|
||||
/>
|
||||
<AccountField
|
||||
label="显示名称"
|
||||
value={masterNodeOnboardDraft.displayName}
|
||||
onChange={(value) =>
|
||||
setMasterNodeOnboardDraft((current) => ({ ...current, displayName: value }))
|
||||
}
|
||||
placeholder="例如:Mac Studio 主控节点"
|
||||
/>
|
||||
<AccountField
|
||||
label="账号标识"
|
||||
value={masterNodeOnboardDraft.accountIdentifier}
|
||||
onChange={(value) =>
|
||||
setMasterNodeOnboardDraft((current) => ({ ...current, accountIdentifier: value }))
|
||||
}
|
||||
placeholder="例如:mac-studio"
|
||||
/>
|
||||
<AccountField
|
||||
label="节点 ID"
|
||||
value={masterNodeOnboardDraft.nodeId}
|
||||
onChange={(value) =>
|
||||
setMasterNodeOnboardDraft((current) => ({ ...current, nodeId: value }))
|
||||
}
|
||||
placeholder="例如:mac-studio"
|
||||
/>
|
||||
<AccountField
|
||||
label="节点名称"
|
||||
value={masterNodeOnboardDraft.nodeLabel}
|
||||
onChange={(value) =>
|
||||
setMasterNodeOnboardDraft((current) => ({ ...current, nodeLabel: value }))
|
||||
}
|
||||
placeholder="例如:Mac Studio"
|
||||
/>
|
||||
<AccountField
|
||||
label="模型"
|
||||
value={masterNodeOnboardDraft.model}
|
||||
onChange={(value) =>
|
||||
setMasterNodeOnboardDraft((current) => ({ ...current, model: value }))
|
||||
}
|
||||
placeholder="例如:gpt-5.4"
|
||||
/>
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitMasterNodeOnboarding}
|
||||
disabled={!canManage || busyKey === "onboard:master_codex_node"}
|
||||
className="flex-1 rounded-full bg-[#111111] px-4 py-3 text-[13px] font-semibold text-white disabled:opacity-50"
|
||||
>
|
||||
{busyKey === "onboard:master_codex_node" ? "绑定中" : "绑定并保存"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeOnboarding}
|
||||
className="rounded-full border border-[#E5E5EA] px-4 py-3 text-[13px] font-semibold text-[#57606A]"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user