feat: add domestic model provider presets
This commit is contained in:
@@ -34,13 +34,14 @@ export type ConversationDetail = components["schemas"]["ConversationDetail"];
|
||||
export type ConversationUsage = components["schemas"]["ConversationUsage"];
|
||||
export type ConversationTurn = components["schemas"]["ConversationTurn"];
|
||||
export type ConversationContentSummary = components["schemas"]["ConversationContentSummary"];
|
||||
export type ContentModelProvider = "ark" | "dashscope" | "deepseek" | "moonshot" | "zhipu" | "qianfan" | "hunyuan" | "minimax" | "baichuan" | "siliconflow" | "ollama" | "openai_compatible";
|
||||
export type ContentModel = {
|
||||
id: string; tenant_id: string; display_name: string; provider: "ark" | "openai_compatible";
|
||||
id: string; tenant_id: string; display_name: string; provider: ContentModelProvider;
|
||||
base_url: string; model_id: string; credential_hint: string; temperature: number; max_tokens: number;
|
||||
status: "inactive" | "active" | "revoked"; test_status: "untested" | "passed" | "failed";
|
||||
last_tested_at?: string; last_error_code?: string; activated_at?: string; version: number; created_at: string; updated_at: string;
|
||||
};
|
||||
export type CreateContentModelRequest = { display_name: string; provider: "ark" | "openai_compatible"; base_url: string; model_id: string; api_key: string; temperature: number; max_tokens: number };
|
||||
export type CreateContentModelRequest = { display_name: string; provider: ContentModelProvider; base_url: string; model_id: string; api_key: string; temperature: number; max_tokens: number };
|
||||
export type ConversationMedia = components["schemas"]["ConversationMedia"];
|
||||
export type MediaSegment = components["schemas"]["MediaSegment"];
|
||||
export type RecordingAccessGrant = components["schemas"]["RecordingAccessGrant"];
|
||||
|
||||
Vendored
+7
-4
@@ -5347,8 +5347,7 @@ export interface components {
|
||||
/** Format: uuid */
|
||||
tenant_id: string;
|
||||
display_name: string;
|
||||
/** @enum {string} */
|
||||
provider: "ark" | "openai_compatible";
|
||||
provider: components["schemas"]["ContentModelProvider"];
|
||||
/** Format: uri */
|
||||
base_url: string;
|
||||
model_id: string;
|
||||
@@ -5374,8 +5373,7 @@ export interface components {
|
||||
};
|
||||
CreateContentModelRequest: {
|
||||
display_name: string;
|
||||
/** @enum {string} */
|
||||
provider: "ark" | "openai_compatible";
|
||||
provider: components["schemas"]["ContentModelProvider"];
|
||||
/** Format: uri */
|
||||
base_url: string;
|
||||
model_id: string;
|
||||
@@ -5383,6 +5381,11 @@ export interface components {
|
||||
temperature: number;
|
||||
max_tokens: number;
|
||||
};
|
||||
/**
|
||||
* @description 模型厂商分类;所有类型均通过 OpenAI Chat Completions 兼容协议调用。
|
||||
* @enum {string}
|
||||
*/
|
||||
ContentModelProvider: "ark" | "dashscope" | "deepseek" | "moonshot" | "zhipu" | "qianfan" | "hunyuan" | "minimax" | "baichuan" | "siliconflow" | "ollama" | "openai_compatible";
|
||||
/** @enum {string} */
|
||||
StoryStatus: "CANDIDATE" | "IN_REVIEW" | "CONFIRMED" | "REJECTED" | "SUPERSEDED";
|
||||
StorySource: {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ContentModelProvider } from "./api/client";
|
||||
|
||||
export type ContentModelProviderPreset = {
|
||||
id: ContentModelProvider;
|
||||
name: string;
|
||||
shortName: string;
|
||||
category: "domestic" | "local" | "custom";
|
||||
baseURL: string;
|
||||
modelID: string;
|
||||
modelPlaceholder: string;
|
||||
keyLabel: string;
|
||||
description: string;
|
||||
docsURL?: string;
|
||||
};
|
||||
|
||||
export const contentModelProviderPresets: ContentModelProviderPreset[] = [
|
||||
{ id: "ark", name: "火山方舟 / 豆包", shortName: "豆包", category: "domestic", baseURL: "https://ark.cn-beijing.volces.com/api/v3", modelID: "", modelPlaceholder: "模型 ID 或推理 Endpoint ID", keyLabel: "方舟 API Key", description: "豆包及方舟模型,支持模型 ID 或专属推理接入点。", docsURL: "https://www.volcengine.com/docs/82379/1795150" },
|
||||
{ id: "dashscope", name: "阿里云百炼 / 千问", shortName: "千问", category: "domestic", baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", modelID: "qwen-plus", modelPlaceholder: "例如 qwen-plus", keyLabel: "百炼 API Key", description: "使用北京地域 OpenAI 兼容入口,Key 与地域需一致。", docsURL: "https://help.aliyun.com/zh/model-studio/compatibility-of-openai-with-dashscope" },
|
||||
{ id: "deepseek", name: "DeepSeek", shortName: "DeepSeek", category: "domestic", baseURL: "https://api.deepseek.com", modelID: "deepseek-v4-flash", modelPlaceholder: "例如 deepseek-v4-flash", keyLabel: "DeepSeek API Key", description: "DeepSeek 官方 OpenAI 兼容接口。", docsURL: "https://api-docs.deepseek.com/zh-cn/" },
|
||||
{ id: "moonshot", name: "月之暗面 / Kimi", shortName: "Kimi", category: "domestic", baseURL: "https://api.moonshot.cn/v1", modelID: "kimi-k2.5", modelPlaceholder: "例如 kimi-k2.5", keyLabel: "Moonshot API Key", description: "Kimi 官方开放平台兼容接口。", docsURL: "https://platform.moonshot.cn/docs" },
|
||||
{ id: "zhipu", name: "智谱 AI / GLM", shortName: "智谱 GLM", category: "domestic", baseURL: "https://open.bigmodel.cn/api/paas/v4", modelID: "glm-5", modelPlaceholder: "例如 glm-5", keyLabel: "智谱 API Key", description: "智谱开放平台 OpenAI Chat Completions 接口。", docsURL: "https://docs.bigmodel.cn/cn/guide/develop/openai/introduction" },
|
||||
{ id: "qianfan", name: "百度千帆 / 文心", shortName: "百度千帆", category: "domestic", baseURL: "https://qianfan.baidubce.com/v2", modelID: "", modelPlaceholder: "填写千帆控制台中的 model 参数", keyLabel: "千帆 Bearer Token", description: "千帆 V2 OpenAI 兼容入口,使用 Bearer Token。", docsURL: "https://cloud.baidu.com/doc/qianfan/s/rmh4stn9m" },
|
||||
{ id: "hunyuan", name: "腾讯混元", shortName: "腾讯混元", category: "domestic", baseURL: "https://api.hunyuan.cloud.tencent.com/v1", modelID: "hunyuan-turbos-latest", modelPlaceholder: "例如 hunyuan-turbos-latest", keyLabel: "混元 API Key", description: "腾讯混元 OpenAI 兼容入口。", docsURL: "https://cloud.tencent.com/document/product/1729/111007" },
|
||||
{ id: "minimax", name: "MiniMax", shortName: "MiniMax", category: "domestic", baseURL: "https://api.minimaxi.com/v1", modelID: "MiniMax-M2.7", modelPlaceholder: "例如 MiniMax-M2.7", keyLabel: "MiniMax API Key", description: "MiniMax 文本对话兼容接口,已适配思考内容包装。", docsURL: "https://platform.minimaxi.com/docs/api-reference/text-chat-openai" },
|
||||
{ id: "baichuan", name: "百川智能", shortName: "百川", category: "domestic", baseURL: "https://api.baichuan-ai.com/v1", modelID: "Baichuan2-Turbo", modelPlaceholder: "例如 Baichuan2-Turbo", keyLabel: "百川 API Key", description: "百川官方 Chat Completions 接口。", docsURL: "https://platform.baichuan-ai.com/docs/api" },
|
||||
{ id: "siliconflow", name: "硅基流动", shortName: "硅基流动", category: "domestic", baseURL: "https://api.siliconflow.cn/v1", modelID: "", modelPlaceholder: "填写硅基流动模型 ID", keyLabel: "SiliconFlow API Key", description: "一个 Key 接入多种国内开源模型。", docsURL: "https://docs.siliconflow.cn/cn/api-reference/chat-completions/chat-completions" },
|
||||
{ id: "ollama", name: "本地 Ollama", shortName: "本地 Ollama", category: "local", baseURL: "http://ollama:11434/v1", modelID: "qwen2.5:7b", modelPlaceholder: "例如 qwen2.5:7b", keyLabel: "本地占位密钥", description: "当前 Docker 网络内的本地模型,不向外部发送聊天内容。" },
|
||||
{ id: "openai_compatible", name: "自定义兼容服务", shortName: "自定义服务", category: "custom", baseURL: "https://", modelID: "", modelPlaceholder: "填写服务提供方给出的模型 ID", keyLabel: "API Key", description: "用于其他支持 OpenAI Chat Completions 的 HTTPS 服务。" },
|
||||
];
|
||||
|
||||
export function contentModelProviderPreset(provider: ContentModelProvider) {
|
||||
return contentModelProviderPresets.find((item) => item.id === provider) ?? contentModelProviderPresets[contentModelProviderPresets.length - 1];
|
||||
}
|
||||
|
||||
export function contentModelProviderLabel(provider: ContentModelProvider) {
|
||||
return contentModelProviderPreset(provider).name;
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
type ConversationTurn,
|
||||
type ConversationMedia,
|
||||
type ContentModel,
|
||||
type ContentModelProvider,
|
||||
type CreateContentModelRequest,
|
||||
type CreateSubjectFactRequest,
|
||||
type CreateSubjectRequest,
|
||||
@@ -73,6 +74,7 @@ import {
|
||||
type UpdateSubjectRequest,
|
||||
type Story
|
||||
} from "../api/client";
|
||||
import { contentModelProviderLabel, contentModelProviderPreset, contentModelProviderPresets } from "../contentModelProviders";
|
||||
import { FamilyUsageOverview } from "../components/UsageOverview";
|
||||
|
||||
export type CustomerSection = "home" | "members" | "subjects" | "devices" | "conversations" | "stories" | "sharing" | "privacy" | "billing" | "messages" | "models";
|
||||
@@ -2103,7 +2105,7 @@ function ConversationPanel({ subjects, selectedID, onSelect, conversations, insi
|
||||
<footer><button type="button" className="button-secondary" onClick={() => setDetailOpen(true)}>查看完整聊天</button>{storyCandidate ? <button type="button" onClick={onOpenStories}>审核并保存为故事</button> : null}<button type="button" onClick={() => void onRegenerate()} disabled={actionBusy || !activeModel}>{actionBusy ? "正在提交…" : "重新生成总结"}</button></footer>
|
||||
</> : <div className="memory-summary-empty"><BookOutlined /><h3>选择一段聊天</h3><p>这里会展示大模型生成的摘要、关键词、故事线索和后续话题。</p></div>}
|
||||
</section>
|
||||
<aside className="memory-model-card"><span className="memory-model-card__icon"><RobotOutlined /></span><p>当前总结模型</p><h3>{activeModel?.display_name || "系统默认模型"}</h3><small>{activeModel ? `${activeModel.provider === "ark" ? "火山方舟" : "OpenAI 兼容"} · ${activeModel.model_id}` : "尚未激活自定义模型,将使用部署环境默认配置"}</small><div><span className={activeModel ? "is-active" : ""} />{activeModel ? "已激活" : "默认回退"}</div>{currentSummary ? <p className="memory-model-card__history"><CheckCircleOutlined /> 最近结果由<br /><strong>{currentSummary.model_ref}</strong><br />生成版本 v{currentSummary.generation_version}</p> : null}</aside>
|
||||
<aside className="memory-model-card"><span className="memory-model-card__icon"><RobotOutlined /></span><p>当前总结模型</p><h3>{activeModel?.display_name || "系统默认模型"}</h3><small>{activeModel ? `${contentModelProviderLabel(activeModel.provider)} · ${activeModel.model_id}` : "尚未激活自定义模型,将使用部署环境默认配置"}</small><div><span className={activeModel ? "is-active" : ""} />{activeModel ? "已激活" : "默认回退"}</div>{currentSummary ? <p className="memory-model-card__history"><CheckCircleOutlined /> 最近结果由<br /><strong>{currentSummary.model_ref}</strong><br />生成版本 v{currentSummary.generation_version}</p> : null}</aside>
|
||||
</div>
|
||||
{detailOpen ? <div className="workspace-dialog-backdrop" role="presentation" onMouseDown={(event) => { if (event.currentTarget === event.target) setDetailOpen(false); }}><section className="conversation-detail workspace-dialog workspace-dialog--wide" role="dialog" aria-modal="true" aria-labelledby="conversation-detail-title"><header><div><h3 id="conversation-detail-title">通话详情</h3><p>查看文字内容、整理结果、语音授权与状态历史</p></div><button type="button" aria-label="关闭" onClick={() => setDetailOpen(false)}><CloseOutlined /></button></header><div className="workspace-dialog__body">{detailLoading ? <StatePanel title="正在读取通话详情" detail="正在核验状态、最终话轮、整理结果、用量和录音访问权限…" /> : detailError && selectedConversationID ? <StatePanel title="通话详情读取失败" detail={detailError} action={() => void onOpen(selectedConversationID)} /> : detail ? <>
|
||||
<div className="resource-detail__head"><div><p className="eyebrow">CONVERSATION DETAIL</p><h3>{conversationStatusLabel(detail.status)}</h3><p>{formatDate(detail.created_at)} · {detail.duration_ms ? `${Math.round(detail.duration_ms / 1000)}秒` : "时长待确认"}</p></div></div>
|
||||
@@ -2128,13 +2130,44 @@ function ContentModelPanel({ models, loading, error, canManage, actionBusy, onCr
|
||||
const [open, setOpen] = useState(false);
|
||||
const [draft, setDraft] = useState<CreateContentModelRequest>({ display_name: "豆包聊天总结", provider: "ark", base_url: "https://ark.cn-beijing.volces.com/api/v3", model_id: "", api_key: "", temperature: 0.2, max_tokens: 1280 });
|
||||
const active = models.find((item) => item.status === "active");
|
||||
async function submit(event: FormEvent) { event.preventDefault(); if (await onCreate(draft)) { setOpen(false); setDraft((item) => ({ ...item, display_name: "", model_id: "", api_key: "" })); } }
|
||||
const selectedProvider = contentModelProviderPreset(draft.provider);
|
||||
const providerGroups = [
|
||||
{ category: "domestic" as const, title: "国内主流云模型", detail: "已预填各厂商官方兼容接口" },
|
||||
{ category: "local" as const, title: "本地模型", detail: "聊天内容不离开本机" },
|
||||
{ category: "custom" as const, title: "其他服务", detail: "接入任意 OpenAI 兼容 HTTPS 接口" },
|
||||
];
|
||||
function selectProvider(provider: ContentModelProvider) {
|
||||
const preset = contentModelProviderPreset(provider);
|
||||
setDraft((item) => ({
|
||||
...item,
|
||||
provider,
|
||||
display_name: `${preset.shortName}聊天总结`,
|
||||
base_url: preset.baseURL,
|
||||
model_id: preset.modelID,
|
||||
api_key: provider === "ollama" ? "ollama-local-key" : "",
|
||||
}));
|
||||
}
|
||||
async function submit(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
if (await onCreate(draft)) {
|
||||
setOpen(false);
|
||||
setDraft((item) => ({ ...item, api_key: "" }));
|
||||
}
|
||||
}
|
||||
return <div className="model-management-page">
|
||||
<section className="model-active-hero"><div><span className="model-active-hero__icon"><RobotOutlined /></span><div><p>当前总结引擎</p><h3>{active?.display_name || "系统默认模型"}</h3><span>{active ? `${active.model_id} · 下一条总结任务立即生效` : "配置并激活模型后,聊天摘要、关键词和故事候选将由它生成"}</span></div></div>{canManage ? <button type="button" onClick={() => setOpen(true)}><PlusOutlined /> 添加大模型</button> : null}</section>
|
||||
{error ? <div className="workspace__inline-error" role="alert">{error}</div> : null}
|
||||
{loading ? <StatePanel title="正在读取模型配置" detail="密钥不会回显,仅展示末四位提示。" /> : <section className="model-card-grid">{models.length ? models.map((item) => <article className={item.status === "active" ? "model-config-card is-active" : "model-config-card"} key={item.id}><header><span><RobotOutlined /></span><div><h3>{item.display_name}</h3><p>{item.provider === "ark" ? "火山方舟 / 豆包" : "OpenAI 兼容服务"}</p></div><b>{item.status === "active" ? "正在使用" : "备用"}</b></header><dl><div><dt>模型 ID</dt><dd>{item.model_id}</dd></div><div><dt>接口地址</dt><dd>{item.base_url}</dd></div><div><dt>访问密钥</dt><dd>{item.credential_hint}</dd></div><div><dt>生成参数</dt><dd>温度 {item.temperature} · 最多 {item.max_tokens} tokens</dd></div></dl><footer><span className={`model-test-status model-test-status--${item.test_status}`}>{item.test_status === "passed" ? "连通性正常" : item.test_status === "failed" ? `测试失败 · ${item.last_error_code || "请检查配置"}` : "尚未测试"}</span>{canManage ? <div><button type="button" className="button-secondary" disabled={actionBusy} onClick={() => void onTest(item)}>测试连接</button>{item.status !== "active" ? <button type="button" disabled={actionBusy || item.test_status !== "passed"} onClick={() => void onActivate(item)}>设为当前模型</button> : null}{item.status !== "active" ? <button type="button" className="button-danger" disabled={actionBusy} onClick={() => void onRevoke(item)}>移除</button> : null}</div> : null}</footer></article>) : <div className="model-empty"><RobotOutlined /><h3>还没有自定义总结模型</h3><p>添加火山方舟或其他 OpenAI 兼容模型,测试通过后即可激活。</p>{canManage ? <button type="button" onClick={() => setOpen(true)}>添加第一个模型</button> : null}</div>}</section>}
|
||||
{loading ? <StatePanel title="正在读取模型配置" detail="密钥不会回显,仅展示末四位提示。" /> : <section className="model-card-grid">{models.length ? models.map((item) => <article className={item.status === "active" ? "model-config-card is-active" : "model-config-card"} key={item.id}><header><span><RobotOutlined /></span><div><h3>{item.display_name}</h3><p>{contentModelProviderLabel(item.provider)}</p></div><b>{item.status === "active" ? "正在使用" : "备用"}</b></header><dl><div><dt>模型 ID</dt><dd>{item.model_id}</dd></div><div><dt>接口地址</dt><dd>{item.base_url}</dd></div><div><dt>访问密钥</dt><dd>{item.credential_hint}</dd></div><div><dt>生成参数</dt><dd>温度 {item.temperature} · 最多 {item.max_tokens} tokens</dd></div></dl><footer><span className={`model-test-status model-test-status--${item.test_status}`}>{item.test_status === "passed" ? "连通性正常" : item.test_status === "failed" ? `测试失败 · ${item.last_error_code || "请检查配置"}` : "尚未测试"}</span>{canManage ? <div><button type="button" className="button-secondary" disabled={actionBusy} onClick={() => void onTest(item)}>测试连接</button>{item.status !== "active" ? <button type="button" disabled={actionBusy || item.test_status !== "passed"} onClick={() => void onActivate(item)}>设为当前模型</button> : null}{item.status !== "active" ? <button type="button" className="button-danger" disabled={actionBusy} onClick={() => void onRevoke(item)}>移除</button> : null}</div> : null}</footer></article>) : <div className="model-empty"><RobotOutlined /><h3>还没有自定义总结模型</h3><p>选择一个厂商,只需填写密钥并确认模型 ID,测试通过后即可激活。</p>{canManage ? <button type="button" onClick={() => setOpen(true)}>添加第一个模型</button> : null}</div>}</section>}
|
||||
<section className="model-security-note"><SafetyCertificateOutlined /><div><strong>密钥如何保存</strong><p>API Key 使用独立主密钥经 AES-256-GCM 加密后存入数据库,页面和接口都不会再次回显明文;每次激活、测试和移除都会写入审计记录。</p></div></section>
|
||||
{open ? <div className="workspace-dialog-backdrop" role="presentation" onMouseDown={(event) => { if (event.currentTarget === event.target) setOpen(false); }}><form className="workspace-dialog model-create-dialog" role="dialog" aria-modal="true" onSubmit={(event) => void submit(event)}><header><div><h3>添加总结大模型</h3><p>该模型用于生成聊天摘要、关键词、故事和下次话题。</p></div><button type="button" aria-label="关闭" onClick={() => setOpen(false)}><CloseOutlined /></button></header><div className="workspace-dialog__body"><label><span>配置名称</span><input autoFocus required maxLength={120} value={draft.display_name} onChange={(event) => setDraft({ ...draft, display_name:event.target.value })} placeholder="例如:豆包聊天总结" /></label><label><span>服务类型</span><select value={draft.provider} onChange={(event) => setDraft({ ...draft, provider:event.target.value as ContentModel["provider"] })}><option value="ark">火山方舟 / 豆包</option><option value="openai_compatible">OpenAI 兼容接口</option></select></label><label><span>接口基础地址</span><input required type="url" value={draft.base_url} onChange={(event) => setDraft({ ...draft, base_url:event.target.value })} /></label><label><span>模型或 Endpoint ID</span><input required value={draft.model_id} onChange={(event) => setDraft({ ...draft, model_id:event.target.value })} placeholder="ep-xxxxxxxx" /></label><label><span>API Key</span><input required type="password" minLength={8} autoComplete="new-password" value={draft.api_key} onChange={(event) => setDraft({ ...draft, api_key:event.target.value })} placeholder="只在本次保存时使用" /></label><div className="model-parameter-row"><label><span>温度</span><input type="number" min="0" max="2" step="0.1" value={draft.temperature} onChange={(event) => setDraft({ ...draft, temperature:Number(event.target.value) })} /></label><label><span>最大输出</span><input type="number" min="256" max="8192" step="128" value={draft.max_tokens} onChange={(event) => setDraft({ ...draft, max_tokens:Number(event.target.value) })} /></label></div></div><footer><button type="button" className="button-secondary" onClick={() => setOpen(false)}>取消</button><button type="submit" disabled={actionBusy || !draft.display_name.trim() || !draft.model_id.trim() || draft.api_key.length < 8}>{actionBusy ? "正在加密保存…" : "保存配置"}</button></footer></form></div> : null}
|
||||
{open ? <div className="workspace-dialog-backdrop" role="presentation" onMouseDown={(event) => { if (event.currentTarget === event.target) setOpen(false); }}><form className="workspace-dialog model-create-dialog" role="dialog" aria-modal="true" aria-labelledby="model-create-title" onSubmit={(event) => void submit(event)}><header><div><h3 id="model-create-title">添加总结大模型</h3><p>先选厂商,再填写该账号对应的密钥和模型信息。</p></div><button type="button" aria-label="关闭" onClick={() => setOpen(false)}><CloseOutlined /></button></header><div className="workspace-dialog__body">
|
||||
<section className="model-provider-catalog" aria-label="选择模型厂商">{providerGroups.map((group) => <div className="model-provider-group" key={group.category}><div><strong>{group.title}</strong><small>{group.detail}</small></div><div>{contentModelProviderPresets.filter((item) => item.category === group.category).map((item) => <button type="button" aria-pressed={draft.provider === item.id} className={draft.provider === item.id ? "is-selected" : ""} disabled={actionBusy} key={item.id} onClick={() => selectProvider(item.id)}><span>{item.shortName}</span><small>{item.description}</small></button>)}</div></div>)}</section>
|
||||
<section className="model-provider-selection"><div><strong>{selectedProvider.name}</strong><span>{selectedProvider.description} · OpenAI Chat Completions 兼容协议</span></div>{selectedProvider.docsURL ? <a href={selectedProvider.docsURL} target="_blank" rel="noreferrer">查看官方接入文档</a> : null}</section>
|
||||
<label><span>配置名称</span><input autoFocus required maxLength={120} value={draft.display_name} onChange={(event) => setDraft({ ...draft, display_name:event.target.value })} placeholder={`例如:${selectedProvider.shortName}聊天总结`} /></label>
|
||||
<label><span>接口基础地址</span><input required type="url" value={draft.base_url} onChange={(event) => setDraft({ ...draft, base_url:event.target.value })} /><small>已预填官方地址;只有使用专属地域、业务空间或代理网关时才需要修改。</small></label>
|
||||
<label><span>模型 ID</span><input required value={draft.model_id} onChange={(event) => setDraft({ ...draft, model_id:event.target.value })} placeholder={selectedProvider.modelPlaceholder} /><small>必须与该厂商控制台中显示的模型名称或 Endpoint ID 完全一致。</small></label>
|
||||
<label><span>{selectedProvider.keyLabel}</span><input required type="password" minLength={8} autoComplete="new-password" value={draft.api_key} onChange={(event) => setDraft({ ...draft, api_key:event.target.value })} placeholder={draft.provider === "ollama" ? "本地服务使用占位值即可" : "只在本次保存时使用,保存后不再回显"} /></label>
|
||||
<details className="model-advanced-settings"><summary>高级生成参数</summary><div className="model-parameter-row"><label><span>温度</span><input type="number" min="0" max="2" step="0.1" value={draft.temperature} onChange={(event) => setDraft({ ...draft, temperature:Number(event.target.value) })} /></label><label><span>最大输出</span><input type="number" min="256" max="8192" step="128" value={draft.max_tokens} onChange={(event) => setDraft({ ...draft, max_tokens:Number(event.target.value) })} /></label></div></details>
|
||||
</div><footer><button type="button" className="button-secondary" onClick={() => setOpen(false)}>取消</button><button type="submit" disabled={actionBusy || !draft.display_name.trim() || !draft.base_url.trim() || !draft.model_id.trim() || draft.api_key.length < 8}>{actionBusy ? "正在加密保存…" : "保存并去测试"}</button></footer></form></div> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
+26
-1
@@ -111,7 +111,29 @@ main {
|
||||
.model-security-note > .anticon { margin-top: 3px; color: #5e8a6b; font-size: 20px; }
|
||||
.model-security-note p { margin: 5px 0 0; line-height: 1.65; }
|
||||
.model-parameter-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
.model-create-dialog .workspace-dialog__body { display: grid; gap: 14px; }
|
||||
.model-create-dialog { width: min(900px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); }
|
||||
.model-create-dialog .workspace-dialog__body { display: grid; gap: 16px; overflow-y: auto; }
|
||||
.model-create-dialog label > small { display: block; margin-top: 5px; color: #917f78; font-size: 11px; line-height: 1.5; }
|
||||
.model-provider-catalog { display: grid; gap: 13px; padding: 15px; border: 1px solid #eadfda; border-radius: 14px; background: #fbf8f5; }
|
||||
.model-provider-group { display: grid; grid-template-columns: 142px minmax(0, 1fr); gap: 12px; align-items: start; }
|
||||
.model-provider-group > div:first-child { padding-top: 8px; }
|
||||
.model-provider-group > div:first-child strong, .model-provider-group > div:first-child small { display: block; }
|
||||
.model-provider-group > div:first-child strong { color: #4f413c; font-size: 13px; }
|
||||
.model-provider-group > div:first-child small { margin-top: 3px; color: #998680; font-size: 10px; line-height: 1.45; }
|
||||
.model-provider-group > div:last-child { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
|
||||
.model-provider-group button { min-height: 64px; padding: 9px 11px; border: 1px solid #e6d9d4; border-radius: 11px; color: #594a45; background: #fff; text-align: left; box-shadow: none; }
|
||||
.model-provider-group button:hover { border-color: #9baeb7; background: #f7fafb; }
|
||||
.model-provider-group button.is-selected { border-color: #617e8b; color: #344e5a; background: #edf4f6; box-shadow: inset 0 0 0 1px #617e8b; }
|
||||
.model-provider-group button span, .model-provider-group button small { display: block; }
|
||||
.model-provider-group button span { font-size: 13px; font-weight: 700; }
|
||||
.model-provider-group button small { margin-top: 3px; overflow: hidden; color: #93817a; font-size: 10px; font-weight: 400; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.model-provider-selection { display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 12px 14px; border-radius: 12px; color: #53636a; background: #edf3f4; }
|
||||
.model-provider-selection strong, .model-provider-selection span { display: block; }
|
||||
.model-provider-selection span { margin-top: 2px; color: #78888e; font-size: 11px; }
|
||||
.model-provider-selection a { color: #506f7c; font-size: 12px; white-space: nowrap; }
|
||||
.model-advanced-settings { padding: 12px 14px; border: 1px solid #e9dfdb; border-radius: 12px; background: #fffdfb; }
|
||||
.model-advanced-settings summary { color: #695752; cursor: pointer; font-size: 13px; font-weight: 650; }
|
||||
.model-advanced-settings[open] summary { margin-bottom: 13px; }
|
||||
|
||||
/* 非首页业务页统一为轻量、长者优先的内容画布。 */
|
||||
.customer-page:not(.customer-page--home) > .workspace { border-color: #e9deda; border-radius: 18px; background: rgba(255, 253, 251, .96); box-shadow: 0 14px 38px rgba(78, 59, 52, .055); }
|
||||
@@ -131,6 +153,9 @@ main {
|
||||
.memory-summary-panel > header, .model-active-hero, .model-config-card footer { align-items: stretch; flex-direction: column; }
|
||||
.memory-model-card { grid-column: auto; }
|
||||
.model-parameter-row { grid-template-columns: 1fr; }
|
||||
.model-provider-group { grid-template-columns: 1fr; }
|
||||
.model-provider-group > div:last-child { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.model-provider-selection { align-items: flex-start; flex-direction: column; }
|
||||
}
|
||||
|
||||
.hero {
|
||||
|
||||
Reference in New Issue
Block a user