diff --git a/src/api/agents.ts b/src/api/agents.ts index 663c980..fe6e1aa 100644 --- a/src/api/agents.ts +++ b/src/api/agents.ts @@ -73,13 +73,24 @@ export interface ExternalToolPlugin extends ExternalToolPluginPayload { isTemp?: boolean; } +export interface AgentModelConfig { + model: { + id: string; + name: string; + }; + role: string; + priority: number; + enabled: boolean; +} + export interface Agent { id: string; name: string; description: string; avatar: string; prompt: string; - model: string; + model?: string; + models?: AgentModelConfig[]; temperature: number; owner_id?: string | null; team_id?: string | null; diff --git a/src/components/ChatPreview.tsx b/src/components/ChatPreview.tsx index bace61c..702609a 100644 --- a/src/components/ChatPreview.tsx +++ b/src/components/ChatPreview.tsx @@ -9,6 +9,7 @@ import { ToolCallTrace } from '../api'; import Markdown from './Markdown'; +import { parseAgentModels } from '../pages/chat/utils/agentModels'; interface Props { agent: Agent; @@ -74,8 +75,9 @@ export default function ChatPreview({ agent, agentId }: Props) { sid = created.id; setSessionId(sid); } - const model = String(agent?.model || '').split(',')[0]?.trim() || undefined; - const modelId = model ? undefined : undefined; + const agentModels = parseAgentModels(agent?.models ?? agent?.model); + const model = agentModels[0]?.name || undefined; + const modelId = agentModels[0]?.id || undefined; const targetAgentId = agentId; await streamChat( sid, diff --git a/src/pages/AgentEditor/constants.ts b/src/pages/AgentEditor/constants.ts index d82b0ba..938f7fe 100644 --- a/src/pages/AgentEditor/constants.ts +++ b/src/pages/AgentEditor/constants.ts @@ -51,20 +51,33 @@ export const TYPE_TAG: Record url?.startsWith('http') || url?.startsWith('/'); -export const parseModelSelections = (value?: string | string[]) => { +// parseModelSelectionItem 将多种模型结构统一提取为模型 ID。 +const parseModelSelectionItem = (item: any) => { + if (!item) { + return ''; + } + if (typeof item === 'string') { + return item; + } + if (typeof item === 'object' && item.model?.id) { + return String(item.model.id); + } + if (typeof item === 'object' && item.id) { + return String(item.id); + } + return String(item); +}; + +// parseModelSelections 负责把接口返回的模型配置统一转换为模型 ID 列表。 +export const parseModelSelections = (value?: unknown) => { if (Array.isArray(value)) { - return value; + return value.map(parseModelSelectionItem).filter(Boolean); } // 尝试解析 JSON 格式 try { const parsed = JSON.parse(String(value || '[]')); if (Array.isArray(parsed)) { - return parsed.map((item: any) => { - if (typeof item === 'object' && item.id) { - return item.id; - } - return String(item); - }).filter(Boolean); + return parsed.map(parseModelSelectionItem).filter(Boolean); } } catch { // 兼容旧格式:逗号分隔的字符串 diff --git a/src/pages/AgentEditor/hooks/useAgentEditor.ts b/src/pages/AgentEditor/hooks/useAgentEditor.ts index 3d0c4a1..0f4743f 100644 --- a/src/pages/AgentEditor/hooks/useAgentEditor.ts +++ b/src/pages/AgentEditor/hooks/useAgentEditor.ts @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react'; import { FormInstance } from 'antd'; -import { Agent, AgentAPI, Team, TeamAPI, AiModel, ModelAPI, ImageAPI } from '../../../api'; -import { DEFAULT_AVATAR } from '../constants'; +import { Agent, AgentAPI, AgentModelConfig, Team, TeamAPI, AiModel, ModelAPI, ImageAPI } from '../../../api'; +import { DEFAULT_AVATAR, parseModelSelections } from '../constants'; interface UseAgentEditorOptions { id?: string; @@ -11,6 +11,31 @@ interface UseAgentEditorOptions { navigate: any; } +// buildAgentModelsPayload 将表单里的单选模型值转换为后端需要的 models 数组结构。 +const buildAgentModelsPayload = (modelId: string | undefined, models: AiModel[]): AgentModelConfig[] => { + if (!modelId) { + return []; + } + const selectedModel = models.find((item) => item.id === modelId); + return [ + { + model: { + id: modelId, + name: selectedModel?.model_name || '', + }, + role: 'primary', + priority: 1, + enabled: true, + }, + ]; +}; + +// normalizeAgentFormValues 将接口返回的 models 字段映射为表单使用的 model 单值。 +const normalizeAgentFormValues = (data: Agent) => ({ + ...data, + model: parseModelSelections(data.models)[0] || '', +}); + export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentEditorOptions) { const [agent, setAgent] = useState(null); const [saving, setSaving] = useState(false); @@ -85,7 +110,7 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE setAgent(data); if (force || autoSaveStatus !== 'dirty') { hydratingRef.current = true; - form.setFieldsValue(data); + form.setFieldsValue(normalizeAgentFormValues(data)); window.setTimeout(() => { hydratingRef.current = false; }, 0); @@ -163,14 +188,15 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE Object.keys(values).forEach((key) => { const formValue = (values as any)[key]; const originalValue = (agent as any)?.[key]; - // 特殊处理 model 字段:将 id 和 name 组合成 JSON 数组字符串 - if (key === 'model' && Array.isArray(formValue)) { - const modelObjects = formValue.map((modelId: string) => { - const model = models.find((m) => m.id === modelId); - return { id: modelId, name: model?.model_name || '' }; - }); - changedFields[key] = JSON.stringify(modelObjects); - } else if (formValue !== originalValue) { + // model 字段在表单内是单值,提交给后端时需要转换为新的 models 数组结构。 + if (key === 'model') { + const nextModels = buildAgentModelsPayload(formValue, models); + if (JSON.stringify(nextModels) !== JSON.stringify(agent?.models || [])) { + changedFields.models = nextModels; + } + return; + } + if (formValue !== originalValue) { changedFields[key] = formValue; } }); @@ -181,7 +207,7 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE } const updatedAgent = await AgentAPI.update(id!, changedFields); setAgent(updatedAgent); - form.setFieldsValue(updatedAgent); + form.setFieldsValue(normalizeAgentFormValues(updatedAgent)); if (!silent) message.success('已保存'); setAutoSaveStatus('saved'); } catch (e) { diff --git a/src/pages/LoginPage/components/LoginFormCard.tsx b/src/pages/LoginPage/components/LoginFormCard.tsx index 7de45d5..2c7fbd9 100644 --- a/src/pages/LoginPage/components/LoginFormCard.tsx +++ b/src/pages/LoginPage/components/LoginFormCard.tsx @@ -42,7 +42,6 @@ export default function LoginFormCard({ logic }: { logic: LoginPageLogic }) { label="手机号" rules={[ { required: true, message: '请填写手机号' }, - { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式' } ]} > diff --git a/src/pages/chat/components/ChatHeader.tsx b/src/pages/chat/components/ChatHeader.tsx index 0d314e9..6479c5b 100644 --- a/src/pages/chat/components/ChatHeader.tsx +++ b/src/pages/chat/components/ChatHeader.tsx @@ -2,37 +2,26 @@ import { useState, useEffect } from 'react'; import { ApiOutlined, DeleteOutlined, EditOutlined, SettingOutlined, EllipsisOutlined, HistoryOutlined, LogoutOutlined } from '@ant-design/icons'; import { Button, Dropdown, Modal, Switch } from 'antd'; import { Agent, ModelAPI, AiModel } from '../../../api'; +import { parseAgentModels } from '../utils/agentModels'; import './ChatHeader.css'; -function formatAgentModel(raw: string | null | undefined, models: AiModel[]) { +function formatAgentModel(raw: unknown, models: AiModel[]) { const findModelName = (id: string) => { const model = models.find(m => m.id === id); return model ? model.model_name : id; }; - const s = String(raw ?? '').trim(); - if (!s) return '默认模型'; - if (s.startsWith('[') || s.startsWith('{')) { - try { - const parsed = JSON.parse(s); - if (Array.isArray(parsed)) { - const names = parsed - .map((x: any) => { - if (typeof x === 'string') return findModelName(x); - return String(x?.name || x?.model || findModelName(x?.id) || '').trim(); - }) - .filter(Boolean); - if (names.length > 2) { - return names.slice(0, 2).join(', ') + '...'; - } - if (names.length) return names.join(', '); - } else if (parsed && typeof parsed === 'object') { - const name = String((parsed as any).name || (parsed as any).model || findModelName((parsed as any).id) || '').trim(); - if (name) return name; - } - } catch {} + const parsedModels = parseAgentModels(raw) + .map((item) => findModelName(item.id) || item.name) + .filter(Boolean); + if (parsedModels.length > 2) { + return parsedModels.slice(0, 2).join(', ') + '...'; } - return findModelName(s); + if (parsedModels.length > 0) { + return parsedModels.join(', '); + } + const s = String(raw ?? '').trim(); + return s ? findModelName(s) : '默认模型'; } const isImageUrl = (url: string) => url?.startsWith('http') || url?.startsWith('/'); @@ -55,7 +44,7 @@ export default function ChatHeader(props: { ModelAPI.list().then(setModels).catch(console.error); }, []); - const modelText = formatAgentModel(agent.model, models); + const modelText = formatAgentModel(agent.models ?? agent.model, models); return (
diff --git a/src/pages/chat/hooks/useChatData.ts b/src/pages/chat/hooks/useChatData.ts index 399976c..b218193 100644 --- a/src/pages/chat/hooks/useChatData.ts +++ b/src/pages/chat/hooks/useChatData.ts @@ -36,7 +36,7 @@ export function useChatData(args: { } const a = await AgentAPI.detail(agentId); setAgent(a); - const models = parseAgentModels(a.model); + const models = parseAgentModels(a.models ?? a.model); const firstModel = models[0]; if (firstModel) { setOverrides((o) => ({ diff --git a/src/pages/chat/hooks/useChatSender.ts b/src/pages/chat/hooks/useChatSender.ts index a1c0d4d..47e88f0 100644 --- a/src/pages/chat/hooks/useChatSender.ts +++ b/src/pages/chat/hooks/useChatSender.ts @@ -114,7 +114,7 @@ export function useChatSender(args: { }); const [sessionRefresh, setSessionRefresh] = useState(0); - const agentModels = useMemo(() => parseAgentModels(agent?.model), [agent?.model]); + const agentModels = useMemo(() => parseAgentModels(agent?.models ?? agent?.model), [agent?.model, agent?.models]); const modelOptions = useMemo(() => agentModels.map((model) => ({ value: model.id, label: model.name })), [agentModels]); const activeModelValue = overrides.model_id || ''; @@ -141,7 +141,7 @@ export function useChatSender(args: { let targetModel: string; let targetModelId: string; if (targetAgent && targetAgent.id !== agentId) { - const models = parseAgentModels(targetAgent.model); + const models = parseAgentModels(targetAgent.models ?? targetAgent.model); targetModel = models[0]?.name || ''; targetModelId = models[0]?.id || ''; } else { @@ -166,7 +166,10 @@ export function useChatSender(args: { onRetry: (data) => { setStreaming((s) => ({ ...s, retryInfo: data })); if (data?.stage === 'fallback_model' && data?.toModel) { - setOverrides((o) => ({ ...o, model: String(data.toModel) })); + const nextModel = parseAgentModels(data.toModel)[0]; + if (nextModel) { + setOverrides((o) => ({ ...o, model: nextModel.name, model_id: nextModel.id })); + } } }, onReasoningDelta: (chunk) => @@ -272,7 +275,7 @@ export function useChatSender(args: { let targetModel: string; let targetModelId: string; if (targetAgent && targetAgent.id !== agentId) { - const models = parseAgentModels(targetAgent.model); + const models = parseAgentModels(targetAgent.models ?? targetAgent.model); targetModel = models[0]?.name || ''; targetModelId = models[0]?.id || ''; } else { diff --git a/src/pages/chat/utils/agentModels.ts b/src/pages/chat/utils/agentModels.ts index 483a9ea..168baf5 100644 --- a/src/pages/chat/utils/agentModels.ts +++ b/src/pages/chat/utils/agentModels.ts @@ -3,15 +3,46 @@ export interface AgentModelOption { name: string; } -export const parseAgentModels = (value?: string): AgentModelOption[] => { +// parseAgentModelItem 将后端多种模型结构统一转换为聊天侧使用的模型选项。 +const parseAgentModelItem = (item: any): AgentModelOption | null => { + if (!item) { + return null; + } + if (typeof item === 'string') { + return { id: item, name: item }; + } + if (typeof item === 'object' && item.model?.id) { + return { + id: String(item.model.id), + name: String(item.model.name || item.model.id) + }; + } + if (typeof item === 'object' && item.id) { + return { + id: String(item.id), + name: String(item.name || item.model || item.id) + }; + } + return { + id: String(item), + name: String(item) + }; +}; + +// parseAgentModels 负责兼容旧 model 字符串和新 models 数组结构。 +export const parseAgentModels = (value?: unknown): AgentModelOption[] => { if (!value) return []; + if (Array.isArray(value)) { + return value.map(parseAgentModelItem).filter(Boolean) as AgentModelOption[]; + } try { - const parsed = JSON.parse(value); + const parsed = JSON.parse(String(value)); if (Array.isArray(parsed)) { - return parsed.map((item: any) => ({ - id: typeof item === 'object' ? item.id : String(item), - name: typeof item === 'object' ? item.name : String(item) - })); + return parsed.map(parseAgentModelItem).filter(Boolean) as AgentModelOption[]; + } + if (parsed && typeof parsed === 'object') { + const single = parseAgentModelItem(parsed); + return single ? [single] : []; } } catch { // ignore @@ -22,4 +53,3 @@ export const parseAgentModels = (value?: string): AgentModelOption[] => { .filter(Boolean) .map((item) => ({ id: item, name: item })); }; -