mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-16 19:49:51 +08:00
♻️ refactor(ai-chat): 抽离运行时资源加载与设置同步
- 新增 useAIChatRuntimeResources 管理供应商、模型、MCP 工具和 Skills 加载 - 收拢 AI 设置事件监听与模型列表刷新逻辑,减少面板内部副作用堆叠 - 保持 AI 面板行为不变,并通过定向测试、构建和真实页面路径复验
This commit is contained in:
@@ -5,6 +5,7 @@ const source = readFileSync(new URL('./AIChatPanel.tsx', import.meta.url), 'utf8
|
||||
const boundarySource = readFileSync(new URL('./ai/AIMessageRenderBoundary.tsx', import.meta.url), 'utf8');
|
||||
const conversationViewSource = readFileSync(new URL('./ai/AIChatPanelConversationView.tsx', import.meta.url), 'utf8');
|
||||
const derivedStateSource = readFileSync(new URL('./ai/aiChatPanelDerivedState.ts', import.meta.url), 'utf8');
|
||||
const runtimeResourcesSource = readFileSync(new URL('./ai/useAIChatRuntimeResources.ts', import.meta.url), 'utf8');
|
||||
const systemContextSource = readFileSync(new URL('./ai/aiSystemContextMessages.ts', import.meta.url), 'utf8');
|
||||
const runtimeSource = readFileSync(new URL('../utils/aiChatRuntime.ts', import.meta.url), 'utf8');
|
||||
|
||||
@@ -21,15 +22,17 @@ describe('AIChatPanel message render isolation', () => {
|
||||
});
|
||||
|
||||
it('loads user prompt settings and appends them as system messages', () => {
|
||||
expect(source).toContain('AIGetUserPromptSettings');
|
||||
expect(source).toContain("window.addEventListener('gonavi:ai:config-changed'");
|
||||
expect(source).toContain("import { useAIChatRuntimeResources } from './ai/useAIChatRuntimeResources';");
|
||||
expect(source).toContain('useAIChatRuntimeResources({ onOpenSettings })');
|
||||
expect(runtimeResourcesSource).toContain('AIGetUserPromptSettings');
|
||||
expect(runtimeResourcesSource).toContain("window.addEventListener('gonavi:ai:config-changed'");
|
||||
expect(systemContextSource).toContain('以下是当前用户的自定义补充提示词');
|
||||
expect(systemContextSource).toContain("appendCustomPromptGroup(systemMessages, ['database']");
|
||||
});
|
||||
|
||||
it('loads MCP tools and skills into the runtime tool chain', () => {
|
||||
expect(source).toContain('AIListMCPTools');
|
||||
expect(source).toContain('AIGetSkills');
|
||||
expect(runtimeResourcesSource).toContain('AIListMCPTools');
|
||||
expect(runtimeResourcesSource).toContain('AIGetSkills');
|
||||
expect(source).toContain('executeLocalAIToolCall');
|
||||
expect(systemContextSource).toContain('以下是当前启用的 Skill');
|
||||
expect(source).toContain('buildAvailableAIChatTools');
|
||||
|
||||
@@ -5,10 +5,6 @@ import { EventsOn, EventsOff } from '../../wailsjs/runtime';
|
||||
import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
import type {
|
||||
AIChatMessage,
|
||||
AIMCPToolDescriptor,
|
||||
AIProviderConfig,
|
||||
AISkillConfig,
|
||||
AIUserPromptSettings,
|
||||
AIToolCall,
|
||||
JVMAIPlanContext,
|
||||
JVMDiagnosticPlanContext,
|
||||
@@ -19,13 +15,11 @@ import { AIChatHeader } from './ai/AIChatHeader';
|
||||
import { AIChatInput } from './ai/AIChatInput';
|
||||
import { AIHistoryDrawer } from './ai/AIHistoryDrawer';
|
||||
import AIChatPanelConversationView from './ai/AIChatPanelConversationView';
|
||||
import type { AIComposerNotice, AIComposerNoticeAction } from '../utils/aiComposerNotice';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import {
|
||||
buildIncompleteProviderNotice,
|
||||
buildMissingModelNotice,
|
||||
buildMissingProviderNotice,
|
||||
buildModelFetchFailedNotice,
|
||||
} from '../utils/aiComposerNotice';
|
||||
import { consumeAIChatSendShortcutOnKeyDown } from '../utils/aiChatSendShortcut';
|
||||
import { toAIRequestMessage } from '../utils/aiMessagePayload';
|
||||
@@ -48,6 +42,7 @@ import {
|
||||
} from './ai/aiChatPanelDerivedState';
|
||||
import { buildAIChatReadinessSnapshot } from './ai/aiChatReadiness';
|
||||
import { buildAISystemContextMessages } from './ai/aiSystemContextMessages';
|
||||
import { useAIChatRuntimeResources } from './ai/useAIChatRuntimeResources';
|
||||
|
||||
interface AIChatPanelProps {
|
||||
width?: number;
|
||||
@@ -61,31 +56,31 @@ interface AIChatPanelProps {
|
||||
|
||||
const genId = () => `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
||||
|
||||
const EMPTY_AI_USER_PROMPT_SETTINGS: AIUserPromptSettings = {
|
||||
global: '',
|
||||
database: '',
|
||||
jvm: '',
|
||||
jvmDiagnostic: '',
|
||||
};
|
||||
|
||||
export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
width = 380, darkMode, bgColor, onClose, onOpenSettings, onWidthChange, overlayTheme
|
||||
}) => {
|
||||
const [input, setInput] = useState('');
|
||||
const [draftImages, setDraftImages] = useState<string[]>([]);
|
||||
const [sending, setSending] = useState(false);
|
||||
const [activeProvider, setActiveProvider] = useState<AIProviderConfig | null>(null);
|
||||
const [userPromptSettings, setUserPromptSettings] = useState<AIUserPromptSettings>(EMPTY_AI_USER_PROMPT_SETTINGS);
|
||||
const [mcpTools, setMcpTools] = useState<AIMCPToolDescriptor[]>([]);
|
||||
const [skills, setSkills] = useState<AISkillConfig[]>([]);
|
||||
const [dynamicModels, setDynamicModels] = useState<string[]>([]);
|
||||
const [showScrollBottom, setShowScrollBottom] = useState(false);
|
||||
const [loadingModels, setLoadingModels] = useState(false);
|
||||
const [composerNotice, setComposerNotice] = useState<AIComposerNotice | null>(null);
|
||||
const [panelWidth, setPanelWidth] = useState(width);
|
||||
const [isResizing, setIsResizing] = useState(false);
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const [activePanelMode, setActivePanelMode] = useState<'chat' | 'insights' | 'history'>('chat');
|
||||
const {
|
||||
activeProvider,
|
||||
composerNotice,
|
||||
dynamicModels,
|
||||
fetchDynamicModels,
|
||||
handleComposerAction,
|
||||
handleModelChange,
|
||||
handleOpenSettingsFromPanel,
|
||||
loadingModels,
|
||||
mcpTools,
|
||||
setComposerNotice,
|
||||
skills,
|
||||
userPromptSettings,
|
||||
} = useAIChatRuntimeResources({ onOpenSettings });
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -260,180 +255,6 @@ export const AIChatPanel: React.FC<AIChatPanelProps> = ({
|
||||
const quickActionBg = darkMode ? 'rgba(255,255,255,0.04)' : 'rgba(255,255,255,0.8)';
|
||||
const quickActionBorder = overlayTheme.sectionBorder;
|
||||
|
||||
const loadActiveProvider = useCallback(async () => {
|
||||
try {
|
||||
const Service = (window as any).go?.aiservice?.Service;
|
||||
if (!Service) return;
|
||||
const [provRes, activeRes] = await Promise.all([
|
||||
Service.AIGetProviders?.(),
|
||||
Service.AIGetActiveProvider?.(),
|
||||
]);
|
||||
if (Array.isArray(provRes) && activeRes) {
|
||||
const current = provRes.find((p: any) => p.id === activeRes);
|
||||
setActiveProvider(current || null);
|
||||
}
|
||||
} catch (e) { console.warn('Failed to load active provider', e); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { loadActiveProvider(); }, [loadActiveProvider]);
|
||||
|
||||
const loadUserPromptSettings = useCallback(async () => {
|
||||
try {
|
||||
const Service = (window as any).go?.aiservice?.Service;
|
||||
if (!Service?.AIGetUserPromptSettings) {
|
||||
setUserPromptSettings(EMPTY_AI_USER_PROMPT_SETTINGS);
|
||||
return;
|
||||
}
|
||||
const nextSettings = await Service.AIGetUserPromptSettings();
|
||||
setUserPromptSettings({
|
||||
...EMPTY_AI_USER_PROMPT_SETTINGS,
|
||||
...nextSettings,
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Failed to load user prompt settings', e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadMCPTools = useCallback(async () => {
|
||||
try {
|
||||
const Service = (window as any).go?.aiservice?.Service;
|
||||
if (!Service?.AIListMCPTools) {
|
||||
setMcpTools([]);
|
||||
return;
|
||||
}
|
||||
const nextTools = await Service.AIListMCPTools();
|
||||
setMcpTools(Array.isArray(nextTools) ? nextTools : []);
|
||||
} catch (e) {
|
||||
console.warn('Failed to load MCP tools', e);
|
||||
setMcpTools([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadSkills = useCallback(async () => {
|
||||
try {
|
||||
const Service = (window as any).go?.aiservice?.Service;
|
||||
if (!Service?.AIGetSkills) {
|
||||
setSkills([]);
|
||||
return;
|
||||
}
|
||||
const nextSkills = await Service.AIGetSkills();
|
||||
setSkills(Array.isArray(nextSkills) ? nextSkills : []);
|
||||
} catch (e) {
|
||||
console.warn('Failed to load skills', e);
|
||||
setSkills([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void loadUserPromptSettings();
|
||||
void loadMCPTools();
|
||||
void loadSkills();
|
||||
const handleAIConfigChanged = () => {
|
||||
void loadUserPromptSettings();
|
||||
void loadMCPTools();
|
||||
void loadSkills();
|
||||
void loadActiveProvider();
|
||||
};
|
||||
window.addEventListener('gonavi:ai:config-changed', handleAIConfigChanged as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener('gonavi:ai:config-changed', handleAIConfigChanged as EventListener);
|
||||
};
|
||||
}, [loadActiveProvider, loadMCPTools, loadSkills, loadUserPromptSettings]);
|
||||
|
||||
// 监听供应商配置变更(来自设置面板的删除/新增/切换操作),重新加载 active provider 并清空已缓存的模型
|
||||
useEffect(() => {
|
||||
const handler = () => {
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(null);
|
||||
activeProviderIdRef.current = null;
|
||||
loadActiveProvider();
|
||||
};
|
||||
window.addEventListener('gonavi:ai:provider-changed', handler);
|
||||
return () => window.removeEventListener('gonavi:ai:provider-changed', handler);
|
||||
}, [loadActiveProvider]);
|
||||
|
||||
const handleModelChange = async (val: string) => {
|
||||
if (!activeProvider) return;
|
||||
try {
|
||||
const Service = (window as any).go?.aiservice?.Service;
|
||||
const payload = {
|
||||
...activeProvider,
|
||||
model: val,
|
||||
apiKey: activeProvider.apiKey || '',
|
||||
hasSecret: activeProvider.hasSecret ?? Boolean(activeProvider.secretRef),
|
||||
};
|
||||
await Service?.AISaveProvider?.(payload);
|
||||
setActiveProvider(payload);
|
||||
setComposerNotice(null);
|
||||
} catch (e) { console.warn('Failed to update provider model', e); }
|
||||
};
|
||||
|
||||
const activeProviderIdRef = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeProvider?.id && activeProvider.id !== activeProviderIdRef.current) {
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(null);
|
||||
activeProviderIdRef.current = activeProvider.id;
|
||||
}
|
||||
// 供应商被删除后 activeProvider 变为 null,此时也必须清空残留模型
|
||||
if (!activeProvider) {
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(null);
|
||||
activeProviderIdRef.current = null;
|
||||
}
|
||||
}, [activeProvider?.id, activeProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeProvider?.model && String(activeProvider.model).trim()) {
|
||||
setComposerNotice(null);
|
||||
}
|
||||
}, [activeProvider?.model]);
|
||||
|
||||
|
||||
// dynamicModels 仅在内存中使用,不再写回供应商配置,避免污染静态 models 列表
|
||||
|
||||
const fetchDynamicModels = useCallback(async () => {
|
||||
try {
|
||||
setLoadingModels(true);
|
||||
setComposerNotice(null);
|
||||
const Service = (window as any).go?.aiservice?.Service;
|
||||
if (!Service) return;
|
||||
const result = await Service.AIListModels?.();
|
||||
if (result?.success && Array.isArray(result.models) && result.models.length > 0) {
|
||||
const sortedModels = [...result.models].sort((a, b) => a.localeCompare(b));
|
||||
setDynamicModels(sortedModels);
|
||||
setComposerNotice(null);
|
||||
} else if (result && !result.success) {
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(buildModelFetchFailedNotice(result.error));
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('Failed to fetch models', e);
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(buildModelFetchFailedNotice('获取模型列表失败:' + (e?.message || '未知错误')));
|
||||
} finally {
|
||||
setLoadingModels(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleOpenSettingsFromPanel = useCallback(() => {
|
||||
onOpenSettings?.();
|
||||
window.setTimeout(() => {
|
||||
void loadActiveProvider();
|
||||
}, 500);
|
||||
}, [loadActiveProvider, onOpenSettings]);
|
||||
|
||||
const handleComposerAction = useCallback((actionKey: AIComposerNoticeAction) => {
|
||||
if (actionKey === 'open-settings') {
|
||||
handleOpenSettingsFromPanel();
|
||||
return;
|
||||
}
|
||||
if (actionKey === 'reload-models') {
|
||||
void fetchDynamicModels();
|
||||
}
|
||||
}, [fetchDynamicModels, handleOpenSettingsFromPanel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length === 0) return;
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: sending ? 'auto' : 'smooth', block: 'end' });
|
||||
|
||||
251
frontend/src/components/ai/useAIChatRuntimeResources.ts
Normal file
251
frontend/src/components/ai/useAIChatRuntimeResources.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
AIMCPToolDescriptor,
|
||||
AIProviderConfig,
|
||||
AISkillConfig,
|
||||
AIUserPromptSettings,
|
||||
} from '../../types';
|
||||
import type { AIComposerNotice, AIComposerNoticeAction } from '../../utils/aiComposerNotice';
|
||||
import { buildModelFetchFailedNotice } from '../../utils/aiComposerNotice';
|
||||
|
||||
interface AIChatRuntimeService {
|
||||
AIGetProviders?: () => Promise<AIProviderConfig[]>;
|
||||
AIGetActiveProvider?: () => Promise<string>;
|
||||
AIGetUserPromptSettings?: () => Promise<Partial<AIUserPromptSettings>>;
|
||||
AIListMCPTools?: () => Promise<AIMCPToolDescriptor[]>;
|
||||
AIGetSkills?: () => Promise<AISkillConfig[]>;
|
||||
AISaveProvider?: (provider: AIProviderConfig & { apiKey?: string; hasSecret?: boolean }) => Promise<unknown>;
|
||||
AIListModels?: () => Promise<{ success?: boolean; models?: string[]; error?: string } | undefined>;
|
||||
}
|
||||
|
||||
interface UseAIChatRuntimeResourcesOptions {
|
||||
onOpenSettings?: () => void;
|
||||
}
|
||||
|
||||
export const EMPTY_AI_USER_PROMPT_SETTINGS: AIUserPromptSettings = {
|
||||
global: '',
|
||||
database: '',
|
||||
jvm: '',
|
||||
jvmDiagnostic: '',
|
||||
};
|
||||
|
||||
export const useAIChatRuntimeResources = ({
|
||||
onOpenSettings,
|
||||
}: UseAIChatRuntimeResourcesOptions) => {
|
||||
const [activeProvider, setActiveProvider] = useState<AIProviderConfig | null>(null);
|
||||
const [userPromptSettings, setUserPromptSettings] = useState<AIUserPromptSettings>(EMPTY_AI_USER_PROMPT_SETTINGS);
|
||||
const [mcpTools, setMcpTools] = useState<AIMCPToolDescriptor[]>([]);
|
||||
const [skills, setSkills] = useState<AISkillConfig[]>([]);
|
||||
const [dynamicModels, setDynamicModels] = useState<string[]>([]);
|
||||
const [loadingModels, setLoadingModels] = useState(false);
|
||||
const [composerNotice, setComposerNotice] = useState<AIComposerNotice | null>(null);
|
||||
|
||||
const activeProviderIdRef = useRef<string | null>(null);
|
||||
|
||||
const getAIService = useCallback(
|
||||
() => (window as any).go?.aiservice?.Service as AIChatRuntimeService | undefined,
|
||||
[],
|
||||
);
|
||||
|
||||
const loadActiveProvider = useCallback(async () => {
|
||||
try {
|
||||
const service = getAIService();
|
||||
if (!service) {
|
||||
setActiveProvider(null);
|
||||
return;
|
||||
}
|
||||
const [providers, activeProviderId] = await Promise.all([
|
||||
service.AIGetProviders?.(),
|
||||
service.AIGetActiveProvider?.(),
|
||||
]);
|
||||
if (!Array.isArray(providers) || !activeProviderId) {
|
||||
setActiveProvider(null);
|
||||
return;
|
||||
}
|
||||
const current = providers.find((item) => item.id === activeProviderId);
|
||||
setActiveProvider(current || null);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load active provider', error);
|
||||
setActiveProvider(null);
|
||||
}
|
||||
}, [getAIService]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadActiveProvider();
|
||||
}, [loadActiveProvider]);
|
||||
|
||||
const loadUserPromptSettings = useCallback(async () => {
|
||||
try {
|
||||
const service = getAIService();
|
||||
if (!service?.AIGetUserPromptSettings) {
|
||||
setUserPromptSettings(EMPTY_AI_USER_PROMPT_SETTINGS);
|
||||
return;
|
||||
}
|
||||
const nextSettings = await service.AIGetUserPromptSettings();
|
||||
setUserPromptSettings({
|
||||
...EMPTY_AI_USER_PROMPT_SETTINGS,
|
||||
...nextSettings,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to load user prompt settings', error);
|
||||
setUserPromptSettings(EMPTY_AI_USER_PROMPT_SETTINGS);
|
||||
}
|
||||
}, [getAIService]);
|
||||
|
||||
const loadMCPTools = useCallback(async () => {
|
||||
try {
|
||||
const service = getAIService();
|
||||
if (!service?.AIListMCPTools) {
|
||||
setMcpTools([]);
|
||||
return;
|
||||
}
|
||||
const nextTools = await service.AIListMCPTools();
|
||||
setMcpTools(Array.isArray(nextTools) ? nextTools : []);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load MCP tools', error);
|
||||
setMcpTools([]);
|
||||
}
|
||||
}, [getAIService]);
|
||||
|
||||
const loadSkills = useCallback(async () => {
|
||||
try {
|
||||
const service = getAIService();
|
||||
if (!service?.AIGetSkills) {
|
||||
setSkills([]);
|
||||
return;
|
||||
}
|
||||
const nextSkills = await service.AIGetSkills();
|
||||
setSkills(Array.isArray(nextSkills) ? nextSkills : []);
|
||||
} catch (error) {
|
||||
console.warn('Failed to load skills', error);
|
||||
setSkills([]);
|
||||
}
|
||||
}, [getAIService]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadUserPromptSettings();
|
||||
void loadMCPTools();
|
||||
void loadSkills();
|
||||
const handleAIConfigChanged = () => {
|
||||
void loadUserPromptSettings();
|
||||
void loadMCPTools();
|
||||
void loadSkills();
|
||||
void loadActiveProvider();
|
||||
};
|
||||
window.addEventListener('gonavi:ai:config-changed', handleAIConfigChanged as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener('gonavi:ai:config-changed', handleAIConfigChanged as EventListener);
|
||||
};
|
||||
}, [loadActiveProvider, loadMCPTools, loadSkills, loadUserPromptSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleProviderChanged = () => {
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(null);
|
||||
activeProviderIdRef.current = null;
|
||||
void loadActiveProvider();
|
||||
};
|
||||
window.addEventListener('gonavi:ai:provider-changed', handleProviderChanged);
|
||||
return () => window.removeEventListener('gonavi:ai:provider-changed', handleProviderChanged);
|
||||
}, [loadActiveProvider]);
|
||||
|
||||
const handleModelChange = useCallback(async (model: string) => {
|
||||
if (!activeProvider) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const service = getAIService();
|
||||
const payload = {
|
||||
...activeProvider,
|
||||
model,
|
||||
apiKey: activeProvider.apiKey || '',
|
||||
hasSecret: activeProvider.hasSecret ?? Boolean(activeProvider.secretRef),
|
||||
};
|
||||
await service?.AISaveProvider?.(payload);
|
||||
setActiveProvider(payload);
|
||||
setComposerNotice(null);
|
||||
} catch (error) {
|
||||
console.warn('Failed to update provider model', error);
|
||||
}
|
||||
}, [activeProvider, getAIService]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeProvider?.id && activeProvider.id !== activeProviderIdRef.current) {
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(null);
|
||||
activeProviderIdRef.current = activeProvider.id;
|
||||
}
|
||||
if (!activeProvider) {
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(null);
|
||||
activeProviderIdRef.current = null;
|
||||
}
|
||||
}, [activeProvider]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeProvider?.model && String(activeProvider.model).trim()) {
|
||||
setComposerNotice(null);
|
||||
}
|
||||
}, [activeProvider?.model]);
|
||||
|
||||
const fetchDynamicModels = useCallback(async () => {
|
||||
try {
|
||||
setLoadingModels(true);
|
||||
setComposerNotice(null);
|
||||
const service = getAIService();
|
||||
if (!service) {
|
||||
return;
|
||||
}
|
||||
const result = await service.AIListModels?.();
|
||||
if (result?.success && Array.isArray(result.models) && result.models.length > 0) {
|
||||
const sortedModels = [...result.models].sort((left, right) => left.localeCompare(right));
|
||||
setDynamicModels(sortedModels);
|
||||
setComposerNotice(null);
|
||||
return;
|
||||
}
|
||||
if (result && !result.success) {
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(buildModelFetchFailedNotice(result.error));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn('Failed to fetch models', error);
|
||||
setDynamicModels([]);
|
||||
setComposerNotice(buildModelFetchFailedNotice(`获取模型列表失败:${error?.message || '未知错误'}`));
|
||||
} finally {
|
||||
setLoadingModels(false);
|
||||
}
|
||||
}, [getAIService]);
|
||||
|
||||
const handleOpenSettingsFromPanel = useCallback(() => {
|
||||
onOpenSettings?.();
|
||||
window.setTimeout(() => {
|
||||
void loadActiveProvider();
|
||||
}, 500);
|
||||
}, [loadActiveProvider, onOpenSettings]);
|
||||
|
||||
const handleComposerAction = useCallback((actionKey: AIComposerNoticeAction) => {
|
||||
if (actionKey === 'open-settings') {
|
||||
handleOpenSettingsFromPanel();
|
||||
return;
|
||||
}
|
||||
if (actionKey === 'reload-models') {
|
||||
void fetchDynamicModels();
|
||||
}
|
||||
}, [fetchDynamicModels, handleOpenSettingsFromPanel]);
|
||||
|
||||
return {
|
||||
activeProvider,
|
||||
composerNotice,
|
||||
dynamicModels,
|
||||
fetchDynamicModels,
|
||||
handleComposerAction,
|
||||
handleModelChange,
|
||||
handleOpenSettingsFromPanel,
|
||||
loadingModels,
|
||||
mcpTools,
|
||||
setComposerNotice,
|
||||
skills,
|
||||
userPromptSettings,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user