Add Agent capabilities and configurations across the application
- Extend the AssistantConfig model to include agent_interface_type, agent_values, and agent_secrets for enhanced agent management. - Update ModelType to include "Agent" for broader capability support. - Seed new agent models and configurations in the database for Dify, FastGPT, and OpenCode applications. - Modify the Assistant routes to validate and handle agent capabilities. - Implement agent resource resolution in the config resolver for runtime configuration. - Enhance the interface catalog with agent-specific fields and definitions. - Update frontend components to manage agent configurations, including form handling and state management for agent-related inputs.
This commit is contained in:
@@ -39,10 +39,13 @@ VALUES
|
||||
('asst_002', 'TTS', 'model_004', '{}'),
|
||||
('asst_003', 'ASR', 'model_002', '{}'),
|
||||
('asst_003', 'TTS', 'model_004', '{}'),
|
||||
('asst_003', 'Agent', 'model_014', '{}'),
|
||||
('asst_004', 'ASR', 'model_002', '{}'),
|
||||
('asst_004', 'TTS', 'model_004', '{}'),
|
||||
('asst_004', 'Agent', 'model_015', '{}'),
|
||||
('asst_005', 'ASR', 'model_002', '{}'),
|
||||
('asst_005', 'TTS', 'model_004', '{}')
|
||||
('asst_005', 'TTS', 'model_004', '{}'),
|
||||
('asst_005', 'Agent', 'model_016', '{}')
|
||||
ON CONFLICT (assistant_id, capability) DO UPDATE SET
|
||||
model_resource_id = EXCLUDED.model_resource_id,
|
||||
updated_at = now();
|
||||
|
||||
@@ -40,6 +40,22 @@ VALUES
|
||||
{"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true},
|
||||
{"key":"voice","label":"Voice","group":"values","type":"text","required":false}
|
||||
]} $$::jsonb, TRUE, 1),
|
||||
('dify', 'Dify Agent', 'Agent',
|
||||
$$ {"fields":[
|
||||
{"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true},
|
||||
{"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}
|
||||
]} $$::jsonb, TRUE, 1),
|
||||
('fastgpt', 'FastGPT Agent', 'Agent',
|
||||
$$ {"fields":[
|
||||
{"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true},
|
||||
{"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true},
|
||||
{"key":"appId","label":"App ID","group":"values","type":"text","required":true}
|
||||
]} $$::jsonb, TRUE, 1),
|
||||
('opencode', 'OpenCode Agent', 'Agent',
|
||||
$$ {"fields":[
|
||||
{"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true},
|
||||
{"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}
|
||||
]} $$::jsonb, TRUE, 1),
|
||||
('stepfun-realtime', 'StepFun StepAudio Realtime', 'Realtime',
|
||||
$$ {"fields":[
|
||||
{"key":"modelId","label":"Model ID","group":"values","type":"text","required":true},
|
||||
|
||||
@@ -41,6 +41,15 @@ VALUES
|
||||
'{"apiKey":"replace-me"}', FALSE, TRUE, FALSE),
|
||||
('model_013', 'doubao-seed-2-0-mini-260428', 'LLM', 'openai-llm',
|
||||
'{"modelId":"doubao-seed-2-0-mini-260428","apiUrl":"https://ark.cn-beijing.volces.com/api/v3","temperature":0.7,"extraBody":{"thinking":{"type":"disabled"}}}',
|
||||
'{"apiKey":"replace-me"}', TRUE, TRUE, FALSE)
|
||||
'{"apiKey":"replace-me"}', TRUE, TRUE, FALSE),
|
||||
('model_014', 'Dify 客服应用', 'Agent', 'dify',
|
||||
'{"apiUrl":"https://api.dify.ai/v1/chat-messages"}',
|
||||
'{"apiKey":"replace-me"}', FALSE, TRUE, FALSE),
|
||||
('model_015', 'FastGPT 售后应用', 'Agent', 'fastgpt',
|
||||
'{"apiUrl":"https://api.fastgpt.in/api/v1/chat/completions","appId":"app-fastgpt-001"}',
|
||||
'{"apiKey":"replace-me"}', FALSE, TRUE, FALSE),
|
||||
('model_016', 'OpenCode 服务', 'Agent', 'opencode',
|
||||
'{"apiUrl":"http://localhost:4096"}',
|
||||
'{"apiKey":"replace-me"}', FALSE, TRUE, FALSE)
|
||||
-- Seed defaults must never overwrite resources configured through the UI.
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
@@ -36,6 +36,9 @@ class AssistantConfig(BaseModel):
|
||||
realtime_interface_type: str = ""
|
||||
realtime_values: dict = {}
|
||||
realtime_secrets: dict = {}
|
||||
agent_interface_type: str = ""
|
||||
agent_values: dict = {}
|
||||
agent_secrets: dict = {}
|
||||
llm_interface_type: str = "openai-llm"
|
||||
stt_interface_type: str = "openai-asr"
|
||||
tts_interface_type: str = "openai-tts"
|
||||
|
||||
@@ -12,7 +12,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
router = APIRouter(prefix="/api/assistants", tags=["assistants"])
|
||||
CAPABILITIES = ("LLM", "ASR", "TTS", "Realtime", "Embedding")
|
||||
CAPABILITIES = ("LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent")
|
||||
|
||||
|
||||
def _validate_workflow(body: AssistantUpsert) -> None:
|
||||
|
||||
@@ -13,7 +13,7 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from pydantic.alias_generators import to_camel
|
||||
|
||||
RuntimeMode = Literal["pipeline", "realtime"]
|
||||
ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding"]
|
||||
ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent"]
|
||||
AssistantType = Literal["prompt", "workflow", "dify", "fastgpt", "opencode"]
|
||||
|
||||
# 外部应用类型:其 config.apiKey 是该助手私有密钥,读时打码 / 写时哨兵
|
||||
|
||||
@@ -33,6 +33,36 @@ async def _resource_for(
|
||||
return resource
|
||||
|
||||
|
||||
async def _agent_resource_for(
|
||||
session: AsyncSession,
|
||||
assistant_id: str,
|
||||
interface_type: str,
|
||||
) -> ModelResource | None:
|
||||
if interface_type not in {"dify", "fastgpt", "opencode"}:
|
||||
return None
|
||||
binding = await session.get(AssistantModelBinding, (assistant_id, "Agent"))
|
||||
resource_id = binding.model_resource_id if binding else None
|
||||
resource = await session.get(ModelResource, resource_id) if resource_id else None
|
||||
if (
|
||||
resource
|
||||
and resource.capability == "Agent"
|
||||
and resource.interface_type == interface_type
|
||||
):
|
||||
return resource
|
||||
return (
|
||||
await session.execute(
|
||||
select(ModelResource)
|
||||
.where(
|
||||
ModelResource.capability == "Agent",
|
||||
ModelResource.interface_type == interface_type,
|
||||
ModelResource.enabled.is_(True),
|
||||
)
|
||||
.order_by(ModelResource.is_default.desc(), ModelResource.id.asc())
|
||||
.limit(1)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
|
||||
def _value(resource: ModelResource | None, key: str, default):
|
||||
if not resource:
|
||||
return default
|
||||
@@ -58,6 +88,7 @@ async def resolve_runtime_config(
|
||||
stt_resource = await _resource_for(session, assistant.id, "ASR")
|
||||
tts_resource = await _resource_for(session, assistant.id, "TTS")
|
||||
realtime_resource = await _resource_for(session, assistant.id, "Realtime")
|
||||
agent_resource = await _agent_resource_for(session, assistant.id, assistant.type)
|
||||
vision_resource = (
|
||||
await session.get(ModelResource, assistant.vision_model_resource_id)
|
||||
if assistant.vision_model_resource_id
|
||||
@@ -75,9 +106,9 @@ async def resolve_runtime_config(
|
||||
# workflow 图:仅 workflow 类型非空,引擎据此启用图驱动对话
|
||||
graph=(assistant.graph or {}) if assistant.type == "workflow" else {},
|
||||
# 外部托管类型连接信息(DB 存真 key,直接注入)
|
||||
fastgpt_api_url=assistant.api_url,
|
||||
fastgpt_api_key=assistant.api_key,
|
||||
fastgpt_app_id=assistant.app_id,
|
||||
fastgpt_api_url=str(_value(agent_resource, "apiUrl", assistant.api_url)),
|
||||
fastgpt_api_key=_secret(agent_resource, "apiKey", assistant.api_key),
|
||||
fastgpt_app_id=str(_value(agent_resource, "appId", assistant.app_id)),
|
||||
# 模型/音色:模型资源中的配置优先
|
||||
model=str(_value(llm_resource, "modelId", "")),
|
||||
asr=str(_value(stt_resource, "modelId", "")),
|
||||
@@ -114,6 +145,9 @@ async def resolve_runtime_config(
|
||||
),
|
||||
realtime_values=(realtime_resource.values or {}) if realtime_resource else {},
|
||||
realtime_secrets=(realtime_resource.secrets or {}) if realtime_resource else {},
|
||||
agent_interface_type=(agent_resource.interface_type if agent_resource else ""),
|
||||
agent_values=(agent_resource.values or {}) if agent_resource else {},
|
||||
agent_secrets=(agent_resource.secrets or {}) if agent_resource else {},
|
||||
# 运行时连接信息(真 key + url):模型资源优先,否则 .env 兜底
|
||||
llm_api_key=_secret(llm_resource, "apiKey", config.LLM_API_KEY),
|
||||
llm_base_url=str(_value(llm_resource, "apiUrl", config.LLM_BASE_URL)),
|
||||
|
||||
@@ -34,6 +34,10 @@ OPENAI_COMMON = [
|
||||
field("apiUrl", "API URL", type_="url", required=True),
|
||||
field("apiKey", "API Key", group="secrets", type_="password", required=True),
|
||||
]
|
||||
AGENT_COMMON = [
|
||||
field("apiUrl", "API URL", type_="url", required=True),
|
||||
field("apiKey", "API Key", group="secrets", type_="password", required=True),
|
||||
]
|
||||
XFYUN_AUTH = [
|
||||
field("apiUrl", "WebSocket URL", type_="url", required=True),
|
||||
field("appId", "App ID", group="secrets", type_="password", required=True),
|
||||
@@ -78,6 +82,24 @@ INTERFACE_DEFINITIONS: list[dict] = [
|
||||
"capability": "Realtime",
|
||||
"fields": OPENAI_COMMON + [field("voice", "Voice")],
|
||||
},
|
||||
{
|
||||
"interface_type": "dify",
|
||||
"name": "Dify Agent",
|
||||
"capability": "Agent",
|
||||
"fields": AGENT_COMMON,
|
||||
},
|
||||
{
|
||||
"interface_type": "fastgpt",
|
||||
"name": "FastGPT Agent",
|
||||
"capability": "Agent",
|
||||
"fields": AGENT_COMMON + [field("appId", "App ID", required=True)],
|
||||
},
|
||||
{
|
||||
"interface_type": "opencode",
|
||||
"name": "OpenCode Agent",
|
||||
"capability": "Agent",
|
||||
"fields": AGENT_COMMON,
|
||||
},
|
||||
{
|
||||
"interface_type": "stepfun-realtime",
|
||||
"name": "StepFun StepAudio Realtime",
|
||||
|
||||
@@ -131,9 +131,7 @@ type AssistantForm = {
|
||||
|
||||
type FastGptForm = {
|
||||
name: string;
|
||||
appId: string;
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
agent: string;
|
||||
asr: string;
|
||||
voice: string;
|
||||
enableInterrupt: boolean;
|
||||
@@ -141,8 +139,7 @@ type FastGptForm = {
|
||||
|
||||
type DifyForm = {
|
||||
name: string;
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
agent: string;
|
||||
asr: string;
|
||||
voice: string;
|
||||
enableInterrupt: boolean;
|
||||
@@ -151,8 +148,7 @@ type DifyForm = {
|
||||
type OpenCodeForm = {
|
||||
name: string;
|
||||
prompt: string;
|
||||
apiUrl: string;
|
||||
apiKey: string;
|
||||
agent: string;
|
||||
model: string;
|
||||
asr: string;
|
||||
voice: string;
|
||||
@@ -226,9 +222,7 @@ function blankPromptForm(name: string): AssistantForm {
|
||||
function blankFastGptForm(name: string): FastGptForm {
|
||||
return {
|
||||
name,
|
||||
appId: "",
|
||||
apiUrl: "",
|
||||
apiKey: "",
|
||||
agent: "",
|
||||
asr: "",
|
||||
voice: "",
|
||||
enableInterrupt: true,
|
||||
@@ -238,8 +232,7 @@ function blankFastGptForm(name: string): FastGptForm {
|
||||
function blankDifyForm(name: string): DifyForm {
|
||||
return {
|
||||
name,
|
||||
apiUrl: "",
|
||||
apiKey: "",
|
||||
agent: "",
|
||||
asr: "",
|
||||
voice: "",
|
||||
enableInterrupt: true,
|
||||
@@ -250,8 +243,7 @@ function blankOpenCodeForm(name: string): OpenCodeForm {
|
||||
return {
|
||||
name,
|
||||
prompt: "",
|
||||
apiUrl: "",
|
||||
apiKey: "",
|
||||
agent: "",
|
||||
model: "",
|
||||
asr: "",
|
||||
voice: "",
|
||||
@@ -434,6 +426,14 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
modelResources
|
||||
.filter((c) => c.capability === type)
|
||||
.map((c) => ({ value: c.id, label: c.name }));
|
||||
const agentOptions = (interfaceType: "dify" | "fastgpt" | "opencode") =>
|
||||
modelResources
|
||||
.filter(
|
||||
(resource) =>
|
||||
resource.capability === "Agent" &&
|
||||
resource.interfaceType === interfaceType,
|
||||
)
|
||||
.map((resource) => ({ value: resource.id, label: resource.name }));
|
||||
const visionModelOptionsFor = (currentModelId: string) => modelResources
|
||||
.filter(
|
||||
(c) =>
|
||||
@@ -573,15 +573,15 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
if (view === "create") {
|
||||
setSavedSnapshot(JSON.stringify(form));
|
||||
} else if (view === "create-dify") {
|
||||
const next = { ...difyForm, apiKey: "" };
|
||||
const next = { ...difyForm };
|
||||
setDifyForm(next);
|
||||
setSavedSnapshot(JSON.stringify(next));
|
||||
} else if (view === "create-fastgpt") {
|
||||
const next = { ...fastGptForm, apiKey: "" };
|
||||
const next = { ...fastGptForm };
|
||||
setFastGptForm(next);
|
||||
setSavedSnapshot(JSON.stringify(next));
|
||||
} else if (view === "create-opencode") {
|
||||
const next = { ...openCodeForm, apiKey: "" };
|
||||
const next = { ...openCodeForm };
|
||||
setOpenCodeForm(next);
|
||||
setSavedSnapshot(JSON.stringify(next));
|
||||
} else if (view === "workflow") {
|
||||
@@ -626,9 +626,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
function fillDifyForm(a: Assistant): DifyForm {
|
||||
const next: DifyForm = {
|
||||
name: a.name,
|
||||
apiUrl: a.apiUrl,
|
||||
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||
apiKey: "",
|
||||
agent: a.modelResourceIds.Agent ?? "",
|
||||
asr: a.modelResourceIds.ASR ?? "",
|
||||
voice: a.modelResourceIds.TTS ?? "",
|
||||
enableInterrupt: a.enableInterrupt,
|
||||
@@ -644,11 +642,10 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
type: "dify",
|
||||
enableInterrupt: difyForm.enableInterrupt,
|
||||
modelResourceIds: {
|
||||
...(difyForm.agent ? { Agent: difyForm.agent } : {}),
|
||||
...(difyForm.asr ? { ASR: difyForm.asr } : {}),
|
||||
...(difyForm.voice ? { TTS: difyForm.voice } : {}),
|
||||
},
|
||||
apiUrl: difyForm.apiUrl,
|
||||
apiKey: difyForm.apiKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -657,10 +654,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
function fillFastGptForm(a: Assistant): FastGptForm {
|
||||
const next: FastGptForm = {
|
||||
name: a.name,
|
||||
appId: a.appId,
|
||||
apiUrl: a.apiUrl,
|
||||
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||
apiKey: "",
|
||||
agent: a.modelResourceIds.Agent ?? "",
|
||||
asr: a.modelResourceIds.ASR ?? "",
|
||||
voice: a.modelResourceIds.TTS ?? "",
|
||||
enableInterrupt: a.enableInterrupt,
|
||||
@@ -676,12 +670,10 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
type: "fastgpt",
|
||||
enableInterrupt: fastGptForm.enableInterrupt,
|
||||
modelResourceIds: {
|
||||
...(fastGptForm.agent ? { Agent: fastGptForm.agent } : {}),
|
||||
...(fastGptForm.asr ? { ASR: fastGptForm.asr } : {}),
|
||||
...(fastGptForm.voice ? { TTS: fastGptForm.voice } : {}),
|
||||
},
|
||||
appId: fastGptForm.appId,
|
||||
apiUrl: fastGptForm.apiUrl,
|
||||
apiKey: fastGptForm.apiKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -691,9 +683,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
const next: OpenCodeForm = {
|
||||
name: a.name,
|
||||
prompt: a.prompt,
|
||||
apiUrl: a.apiUrl,
|
||||
// 编辑时不把打码占位符放入输入框;空值写回后端表示保留旧 key
|
||||
apiKey: "",
|
||||
agent: a.modelResourceIds.Agent ?? "",
|
||||
model: a.modelResourceIds.LLM ?? "",
|
||||
asr: a.modelResourceIds.ASR ?? "",
|
||||
voice: a.modelResourceIds.TTS ?? "",
|
||||
@@ -772,13 +762,12 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
visionEnabled: openCodeForm.visionEnabled,
|
||||
visionModelResourceId: openCodeForm.visionModelResourceId || null,
|
||||
modelResourceIds: {
|
||||
...(openCodeForm.agent ? { Agent: openCodeForm.agent } : {}),
|
||||
...(openCodeForm.model ? { LLM: openCodeForm.model } : {}),
|
||||
...(openCodeForm.asr ? { ASR: openCodeForm.asr } : {}),
|
||||
...(openCodeForm.voice ? { TTS: openCodeForm.voice } : {}),
|
||||
},
|
||||
prompt: openCodeForm.prompt,
|
||||
apiUrl: openCodeForm.apiUrl,
|
||||
apiKey: openCodeForm.apiKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1355,7 +1344,9 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
)}
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={saving || !dirty || !difyForm.name.trim()}
|
||||
disabled={
|
||||
saving || !dirty || !difyForm.name.trim() || !difyForm.agent
|
||||
}
|
||||
onClick={() => void handleSaveDify()}
|
||||
>
|
||||
{saving ? (
|
||||
@@ -1373,20 +1364,14 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
<SectionCard
|
||||
icon={<Boxes size={18} />}
|
||||
title="Dify 应用配置"
|
||||
description="填写 API URL 与 API Key 以对接 Dify 应用。开场白、知识库、提示词等对话编排请在 Dify 平台配置,本页不重复设置。"
|
||||
description="从「模型资源」中选择 Dify 应用。开场白、知识库、提示词等对话编排请在 Dify 平台配置,本页不重复设置。"
|
||||
>
|
||||
<InputField
|
||||
label="API URL"
|
||||
value={difyForm.apiUrl}
|
||||
onChange={(value) => updateDifyForm("apiUrl", value)}
|
||||
placeholder="https://api.dify.ai/v1/chat-messages"
|
||||
/>
|
||||
<SecretInputField
|
||||
label="API Key"
|
||||
value={difyForm.apiKey}
|
||||
onChange={(value) => updateDifyForm("apiKey", value)}
|
||||
placeholder="请输入 Dify API Key"
|
||||
storedValueMask={storedApiKeyMask}
|
||||
<ResourceSelectField
|
||||
label="Dify 应用"
|
||||
value={difyForm.agent}
|
||||
onChange={(value) => updateDifyForm("agent", value)}
|
||||
options={agentOptions("dify")}
|
||||
noneLabel="请选择"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
@@ -1454,7 +1439,12 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
)}
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={saving || !dirty || !fastGptForm.name.trim()}
|
||||
disabled={
|
||||
saving ||
|
||||
!dirty ||
|
||||
!fastGptForm.name.trim() ||
|
||||
!fastGptForm.agent
|
||||
}
|
||||
onClick={() => void handleSaveFastGpt()}
|
||||
>
|
||||
{saving ? (
|
||||
@@ -1472,35 +1462,14 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
<SectionCard
|
||||
icon={<Database size={18} />}
|
||||
title="FastGPT 应用配置"
|
||||
description="填写 App ID、API URL 与 API Key 以对接 FastGPT 应用。开场白、知识库、提示词等对话编排请在 FastGPT 平台配置,本页不重复设置。"
|
||||
description="从「模型资源」中选择 FastGPT 应用。开场白、知识库、提示词等对话编排请在 FastGPT 平台配置,本页不重复设置。"
|
||||
>
|
||||
<InputField
|
||||
label="App ID"
|
||||
hint={
|
||||
"FastGPT 应用的 appId,用于拉取应用预设开场白等信息。\n在 FastGPT「应用详情 → 发布渠道 / API 访问」中获取。\n留空则不展示应用开场白,回退使用本助手自身的开场白。"
|
||||
}
|
||||
value={fastGptForm.appId}
|
||||
onChange={(value) => updateFastGptForm("appId", value)}
|
||||
placeholder="请输入 FastGPT 应用 ID"
|
||||
/>
|
||||
<InputField
|
||||
label="API URL"
|
||||
hint={
|
||||
"填 FastGPT 服务的主机根地址即可,例如 http://localhost:3000。\n系统会自动拼接 /api/v1/chat/completions,无需手动带上该路径。\n(即使粘贴了完整接口地址也会自动归一,但建议只填根地址。)"
|
||||
}
|
||||
value={fastGptForm.apiUrl}
|
||||
onChange={(value) => updateFastGptForm("apiUrl", value)}
|
||||
placeholder="http://localhost:3000"
|
||||
/>
|
||||
<SecretInputField
|
||||
label="API Key"
|
||||
hint={
|
||||
"FastGPT 为该应用生成的 API Key(形如 fastgpt-xxxxx)。\n在 FastGPT「应用详情 → API 访问」中创建。每个应用的 Key 独立。"
|
||||
}
|
||||
value={fastGptForm.apiKey}
|
||||
onChange={(value) => updateFastGptForm("apiKey", value)}
|
||||
placeholder="请输入 FastGPT API Key"
|
||||
storedValueMask={storedApiKeyMask}
|
||||
<ResourceSelectField
|
||||
label="FastGPT 应用"
|
||||
value={fastGptForm.agent}
|
||||
onChange={(value) => updateFastGptForm("agent", value)}
|
||||
options={agentOptions("fastgpt")}
|
||||
noneLabel="请选择"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
@@ -1572,7 +1541,12 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
)}
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={saving || !dirty || !openCodeForm.name.trim()}
|
||||
disabled={
|
||||
saving ||
|
||||
!dirty ||
|
||||
!openCodeForm.name.trim() ||
|
||||
!openCodeForm.agent
|
||||
}
|
||||
onClick={() => void handleSaveOpenCode()}
|
||||
>
|
||||
{saving ? (
|
||||
@@ -1590,20 +1564,14 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
<SectionCard
|
||||
icon={<Terminal size={18} />}
|
||||
title="OpenCode 服务配置"
|
||||
description="填写 OpenCode 服务地址与 API Key 以对接代码助手。"
|
||||
description="从「模型资源」中选择 OpenCode 服务资源。"
|
||||
>
|
||||
<InputField
|
||||
label="OpenCode URL"
|
||||
value={openCodeForm.apiUrl}
|
||||
onChange={(value) => updateOpenCodeForm("apiUrl", value)}
|
||||
placeholder="http://localhost:4096"
|
||||
/>
|
||||
<SecretInputField
|
||||
label="API Key"
|
||||
value={openCodeForm.apiKey}
|
||||
onChange={(value) => updateOpenCodeForm("apiKey", value)}
|
||||
placeholder="请输入 OpenCode API Key"
|
||||
storedValueMask={storedApiKeyMask}
|
||||
<ResourceSelectField
|
||||
label="OpenCode 服务"
|
||||
value={openCodeForm.agent}
|
||||
onChange={(value) => updateOpenCodeForm("agent", value)}
|
||||
options={agentOptions("opencode")}
|
||||
noneLabel="请选择"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ChevronUp,
|
||||
Copy,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Loader2,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
@@ -62,6 +63,7 @@ const capabilities: ModelType[] = [
|
||||
"TTS",
|
||||
"Realtime",
|
||||
"Embedding",
|
||||
"Agent",
|
||||
];
|
||||
|
||||
const capabilityFilters = ["全部", ...capabilities] as const;
|
||||
@@ -120,6 +122,15 @@ function fieldValue(draft: ResourceDraft, field: InterfaceField): unknown {
|
||||
: draft.values[field.key];
|
||||
}
|
||||
|
||||
function secretMask(
|
||||
masks: Record<string, unknown>,
|
||||
field: InterfaceField,
|
||||
): string {
|
||||
if (field.group !== "secrets") return "";
|
||||
const value = masks[field.key];
|
||||
return typeof value === "string" && value.includes("****") ? value : "";
|
||||
}
|
||||
|
||||
function extraBodyText(values: Record<string, unknown>): string {
|
||||
const value = values.extraBody;
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
||||
@@ -171,6 +182,7 @@ export function ComponentsModelsPage() {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [draft, setDraft] = useState<ResourceDraft>(emptyDraft);
|
||||
const [storedSecretMasks, setStoredSecretMasks] = useState<Record<string, unknown>>({});
|
||||
const [extraBodyDraft, setExtraBodyDraft] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saveError, setSaveError] = useState("");
|
||||
@@ -284,6 +296,7 @@ export function ComponentsModelsPage() {
|
||||
values: definition ? defaults(definition) : {},
|
||||
secrets: {},
|
||||
}));
|
||||
setStoredSecretMasks({});
|
||||
setExtraBodyDraft("");
|
||||
setTestResult(null);
|
||||
}
|
||||
@@ -301,6 +314,7 @@ export function ComponentsModelsPage() {
|
||||
supportImageInput:
|
||||
capability === "LLM" ? previous.supportImageInput : false,
|
||||
}));
|
||||
setStoredSecretMasks({});
|
||||
setExtraBodyDraft("");
|
||||
setTestResult(null);
|
||||
}
|
||||
@@ -314,8 +328,10 @@ export function ComponentsModelsPage() {
|
||||
...emptyDraft,
|
||||
interfaceType: first?.interfaceType ?? "",
|
||||
values: first ? defaults(first) : {},
|
||||
secrets: {},
|
||||
supportImageInput: false,
|
||||
});
|
||||
setStoredSecretMasks({});
|
||||
setExtraBodyDraft("");
|
||||
setSaveError("");
|
||||
setTestResult(null);
|
||||
@@ -329,9 +345,10 @@ export function ComponentsModelsPage() {
|
||||
capability: resource.capability,
|
||||
interfaceType: resource.interfaceType,
|
||||
values: resource.values,
|
||||
secrets: resource.secrets,
|
||||
secrets: {},
|
||||
supportImageInput: resource.supportImageInput,
|
||||
});
|
||||
setStoredSecretMasks(resource.secrets);
|
||||
setExtraBodyDraft(extraBodyText(resource.values));
|
||||
setSaveError("");
|
||||
setTestResult(null);
|
||||
@@ -355,7 +372,10 @@ export function ComponentsModelsPage() {
|
||||
!extraBodyError &&
|
||||
selectedDefinition.fieldSchema.fields.every((field) => {
|
||||
if (!field.required) return true;
|
||||
const value = fieldValue(draft, field);
|
||||
const value =
|
||||
field.group === "secrets" && secretMask(storedSecretMasks, field)
|
||||
? secretMask(storedSecretMasks, field)
|
||||
: fieldValue(draft, field);
|
||||
return value !== undefined && value !== null && value !== "";
|
||||
}),
|
||||
);
|
||||
@@ -369,7 +389,9 @@ export function ComponentsModelsPage() {
|
||||
draft.capability === "LLM"
|
||||
? valuesWithExtraBody(draft.values, extraBody)
|
||||
: draft.values,
|
||||
secrets: draft.secrets,
|
||||
secrets: editingId
|
||||
? { ...storedSecretMasks, ...draft.secrets }
|
||||
: draft.secrets,
|
||||
supportImageInput:
|
||||
draft.capability === "LLM" ? draft.supportImageInput : false,
|
||||
enabled: editingId
|
||||
@@ -747,6 +769,7 @@ export function ComponentsModelsPage() {
|
||||
key={`${field.group}-${field.key}`}
|
||||
field={field}
|
||||
value={fieldValue(draft, field)}
|
||||
storedValueMask={secretMask(storedSecretMasks, field)}
|
||||
onChange={(value) => updateField(field, value)}
|
||||
/>
|
||||
))}
|
||||
@@ -779,6 +802,7 @@ export function ComponentsModelsPage() {
|
||||
key={`${field.group}-${field.key}`}
|
||||
field={field}
|
||||
value={fieldValue(draft, field)}
|
||||
storedValueMask={secretMask(storedSecretMasks, field)}
|
||||
onChange={(value) => updateField(field, value)}
|
||||
/>
|
||||
))
|
||||
@@ -901,12 +925,16 @@ function FieldSection({
|
||||
function DynamicField({
|
||||
field,
|
||||
value,
|
||||
storedValueMask,
|
||||
onChange,
|
||||
}: {
|
||||
field: InterfaceField;
|
||||
value: unknown;
|
||||
storedValueMask?: string;
|
||||
onChange: (value: unknown) => void;
|
||||
}) {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
if (field.type === "boolean") {
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-xl border border-hairline p-4">
|
||||
@@ -942,17 +970,63 @@ function DynamicField({
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type === "password") {
|
||||
const inputValue = String(value ?? "");
|
||||
const hasStoredValue = Boolean(storedValueMask);
|
||||
|
||||
return (
|
||||
<Field label={field.label} required={field.required}>
|
||||
{hasStoredValue && (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span>当前密钥</span>
|
||||
<code className="rounded-md bg-surface-strong px-2 py-1 font-mono text-foreground">
|
||||
{storedValueMask}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={inputValue}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
if (!nextValue) setShowPassword(false);
|
||||
onChange(nextValue);
|
||||
}}
|
||||
placeholder={
|
||||
hasStoredValue ? "已配置,留空则保持不变" : undefined
|
||||
}
|
||||
autoComplete="new-password"
|
||||
className="pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!inputValue}
|
||||
onClick={() => setShowPassword((current) => !current)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-soft transition-colors hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
|
||||
aria-label={showPassword ? "隐藏密钥" : "显示密钥"}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
{hasStoredValue && (
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
仅显示当前密钥首尾用于识别。留空可保持原密钥,输入新值将覆盖原密钥。
|
||||
</p>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Field label={field.label} required={field.required}>
|
||||
<Input
|
||||
type={
|
||||
field.type === "password"
|
||||
? "password"
|
||||
: field.type === "number"
|
||||
? "number"
|
||||
: field.type === "url"
|
||||
? "url"
|
||||
: "text"
|
||||
field.type === "number"
|
||||
? "number"
|
||||
: field.type === "url"
|
||||
? "url"
|
||||
: "text"
|
||||
}
|
||||
value={String(value ?? "")}
|
||||
onChange={(event) =>
|
||||
@@ -962,11 +1036,6 @@ function DynamicField({
|
||||
: event.target.value,
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
field.type === "password" && String(value ?? "").includes("****")
|
||||
? "已配置,留空或保留掩码表示不修改"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
export const API_BASE =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000";
|
||||
|
||||
export type ModelType = "LLM" | "ASR" | "TTS" | "Realtime" | "Embedding";
|
||||
export type ModelType = "LLM" | "ASR" | "TTS" | "Realtime" | "Embedding" | "Agent";
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
Reference in New Issue
Block a user