♻️ refactor(settings/ai): 聊天入口打开 AI 设置改走设置中心

- 从 AI 聊天面板打开设置时进入设置中心服务-AI 页
- 移除独立 AISettingsModal 挂载,复用 AISettingsContent
- 安全更新修复跳转同步走设置中心,关闭后可回到修复流程
- 更新设置中心与工具中心相关源码级测试断言
This commit is contained in:
Syngnat
2026-07-09 12:19:22 +08:00
parent c77d338ab1
commit dec9b8317c
3 changed files with 75 additions and 37 deletions

View File

@@ -105,6 +105,14 @@ describe('settings center layout', () => {
expect(appSource).toContain('renderSettingsCenterAboutPane()');
});
it('opens AI settings from the chat panel via settings center instead of a standalone modal', () => {
expect(appSource).toContain('const handleOpenAISettings = useCallback((providerId?: string) => {');
expect(appSource).toContain("setActiveSettingsCenterPane({ key: 'ai', group: 'services' })");
expect(appSource).toContain('setIsSettingsModalOpen(true)');
expect(appSource).not.toContain('setIsAISettingsOpen(true)');
expect(appSource).not.toContain('<AISettingsModal');
});
it('opens the about group directly instead of showing a one-item list', () => {
expect(appSource).toContain('const resolveSettingsCenterGroupInitialPane = (group: SettingsCenterGroupKey): SettingsCenterPaneState | null => (');
expect(appSource).toContain("group === 'about' ? { key: 'about-go-navi', group: 'about' } : null");

View File

@@ -304,7 +304,7 @@ describe('tool center menu entries', () => {
expect(appSource).toContain('{isSettingsModalOpen && (');
expect(appSource).toContain('{isThemeModalOpen && (');
expect(appSource).toContain('{isShortcutModalOpen && (');
expect(appSource).toContain('{isAISettingsOpen && (');
expect(appSource).not.toContain('{isAISettingsOpen && (');
expect(appSource).toContain('{isDriverModalOpen && (');
expect(appSource).toContain('{isSyncModalOpen && (');
});
@@ -320,8 +320,11 @@ describe('tool center menu entries', () => {
expect(appSource).toContain('setIsModalOpen(true);');
});
it('loads editable AI provider details before opening the edit modal so stored api keys can be shown', () => {
expect(appSource).toContain('<AISettingsModal');
it('loads editable AI provider details inside settings-center AI pane content', () => {
// 聊天/入口打开 AI 配置走设置中心 AISettingsContent不再挂独立 AISettingsModal
expect(appSource).toContain('<AISettingsContent');
expect(appSource).toContain("activeSettingsCenterPane.key === 'ai'");
expect(appSource).not.toContain('<AISettingsModal');
const modalSource = readFileSync(new URL('./components/AISettingsModal.tsx', import.meta.url), 'utf8');
expect(modalSource).toContain("typeof Service?.AIGetEditableProvider === 'function'");
expect(modalSource).toContain('await Service.AIGetEditableProvider(p.id)');

View File

@@ -9,6 +9,7 @@ import { BrowserOpenURL, Environment, EventsOn, WindowFullscreen, WindowGetPosit
import Sidebar from './components/Sidebar';
import TabManager from './components/TabManager';
import FloatingWorkbenchWindows from './components/FloatingWorkbenchWindows';
import FloatingAIChatWindow from './components/FloatingAIChatWindow';
import FloatingQueryResultWindows from './components/FloatingQueryResultWindows';
import ConnectionModal from './components/ConnectionModal';
import SnippetSettingsModal from './components/SnippetSettingsModal';
@@ -18,7 +19,7 @@ import { type DataSyncEntryMode } from './components/dataSyncEntryMode';
import DriverManagerModal from './components/DriverManagerModal';
import LinuxCJKFontBanner from './components/LinuxCJKFontBanner';
import LogPanel from './components/LogPanel';
import AISettingsModal, { AISettingsContent } from './components/AISettingsModal';
import { AISettingsContent } from './components/AISettingsModal';
import AIChatPanel from './components/AIChatPanel';
import AIPanelErrorBoundary from './components/ai/AIPanelErrorBoundary';
import SecurityUpdateBanner from './components/SecurityUpdateBanner';
@@ -836,6 +837,9 @@ function App() {
const sidebarWidth = useStore(state => state.sidebarWidth);
const setSidebarWidth = useStore(state => state.setSidebarWidth);
const aiPanelVisible = useStore(state => state.aiPanelVisible);
const detachedAIChatWindow = useStore(state => state.detachedAIChatWindow);
const detachAIChatPanel = useStore(state => state.detachAIChatPanel);
const aiChatDetached = Boolean(detachedAIChatWindow);
const toggleAIPanel = useStore(state => state.toggleAIPanel);
const setAIPanelVisible = useStore(state => state.setAIPanelVisible);
const windowDiagSequenceRef = React.useRef(0);
@@ -1905,7 +1909,9 @@ function App() {
setIsSecurityUpdateSettingsOpen(false);
setSecurityUpdateRepairSource(repairEntry.repairSource);
setFocusedAIProviderId(repairEntry.providerId);
setIsAISettingsOpen(true);
setActiveSettingsCenterGroupKey('services');
setActiveSettingsCenterPane({ key: 'ai', group: 'services' });
setIsSettingsModalOpen(true);
return;
}
if (repairEntry.type === 'retry') {
@@ -2636,7 +2642,7 @@ function App() {
const [selectedDataRootPath, setSelectedDataRootPath] = useState('');
const [dataRootLoading, setDataRootLoading] = useState(false);
const [dataRootApplying, setDataRootApplying] = useState(false);
const [isAISettingsOpen, setIsAISettingsOpen] = useState(false);
const aiEntryPlacement = resolveAIEntryPlacement();
const legacyAiEdgeHandleAttachment = resolveLegacyAIEdgeHandleAttachment(aiPanelVisible);
const aiPanelOverlayActive = aiPanelVisible && shouldOverlayAIPanel({
@@ -2892,15 +2898,36 @@ function App() {
setActiveSettingsCenterPane({ key, group });
setIsSettingsModalOpen(true);
}, []);
const finalizeSecurityRepairReturnFromAISettings = useCallback(() => {
const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource);
setFocusedAIProviderId(undefined);
setSecurityUpdateRepairSource(null);
if (reopenSecurityUpdateDetails) {
setIsSecurityUpdateSettingsOpen(true);
}
}, [securityUpdateRepairSource]);
const handleBackFromSettingsCenterPane = useCallback(() => {
const leavingAI = activeSettingsCenterPane?.key === 'ai';
const returnGroup = activeSettingsCenterPane?.group ?? activeSettingsCenterGroupKey;
setActiveSettingsCenterGroupKey(returnGroup);
setActiveSettingsCenterPane(null);
}, [activeSettingsCenterGroupKey, activeSettingsCenterPane?.group]);
if (leavingAI) {
finalizeSecurityRepairReturnFromAISettings();
}
}, [
activeSettingsCenterGroupKey,
activeSettingsCenterPane?.group,
activeSettingsCenterPane?.key,
finalizeSecurityRepairReturnFromAISettings,
]);
const handleCancelSettingsCenterPane = useCallback(() => {
const leavingAI = activeSettingsCenterPane?.key === 'ai';
setActiveSettingsCenterPane(null);
setIsSettingsModalOpen(false);
}, []);
if (leavingAI) {
finalizeSecurityRepairReturnFromAISettings();
}
}, [activeSettingsCenterPane?.key, finalizeSecurityRepairReturnFromAISettings]);
const isSettingsAboutPaneOpen = isSettingsModalOpen && activeSettingsCenterPane?.key === 'about-go-navi';
const isSettingsAboutPaneOpenRef = useRef(false);
useEffect(() => {
@@ -3245,10 +3272,13 @@ function App() {
}
}, [securityUpdateRepairSource]);
/** 从聊天面板等入口打开 AI 配置:走设置中心,不再弹独立 AISettingsModal */
const handleOpenAISettings = useCallback((providerId?: string) => {
setSecurityUpdateRepairSource(null);
setFocusedAIProviderId(providerId);
setIsAISettingsOpen(true);
setActiveSettingsCenterGroupKey('services');
setActiveSettingsCenterPane({ key: 'ai', group: 'services' });
setIsSettingsModalOpen(true);
}, []);
const handleAIPanelRenderError = useCallback((error: Error, errorInfo: React.ErrorInfo) => {
@@ -3268,16 +3298,6 @@ function App() {
setAiPanelRenderNonce((current) => current + 1);
}, []);
const handleCloseAISettings = useCallback(() => {
const reopenSecurityUpdateDetails = shouldReopenSecurityUpdateDetails(securityUpdateRepairSource);
setIsAISettingsOpen(false);
setFocusedAIProviderId(undefined);
setSecurityUpdateRepairSource(null);
if (reopenSecurityUpdateDetails) {
setIsSecurityUpdateSettingsOpen(true);
}
}, [securityUpdateRepairSource]);
const handleWebLogout = useCallback(async () => {
try {
await fetch('/__gonavi/auth/logout', {
@@ -6498,7 +6518,7 @@ function App() {
)}
</>
)}
{aiPanelVisible && (
{aiPanelVisible && !aiChatDetached && (
<div
className={aiPanelOverlayActive ? 'gn-v2-ai-panel-overlay' : undefined}
style={aiPanelOverlayActive
@@ -6601,13 +6621,33 @@ function App() {
</div>
)}
>
<AIChatPanel width={aiPanelRenderWidth} darkMode={darkMode} bgColor={bgContent} onClose={() => setAIPanelVisible(false)} onOpenSettings={() => {
handleOpenAISettings();
}} overlayTheme={overlayTheme} />
<AIChatPanel
width={aiPanelRenderWidth}
darkMode={darkMode}
bgColor={bgContent}
presentation="dock"
onClose={() => setAIPanelVisible(false)}
onDetach={() => detachAIChatPanel()}
onOpenSettings={() => {
handleOpenAISettings();
}}
overlayTheme={overlayTheme}
/>
</AIPanelErrorBoundary>
</div>
</div>
)}
{aiPanelVisible && aiChatDetached && (
<FloatingAIChatWindow
darkMode={darkMode}
bgColor={bgContent}
overlayTheme={overlayTheme}
renderNonce={aiPanelRenderNonce}
onOpenSettings={() => handleOpenAISettings()}
onRenderError={handleAIPanelRenderError}
onRetryRender={handleRetryAIPanelRender}
/>
)}
</div>
{!isV2Ui && isLogPanelOpen && (
<LogPanel
@@ -7282,10 +7322,7 @@ function App() {
<Modal
title={renderUtilityModalTitle(<SettingOutlined />, t('app.settings.title'), t('app.settings.description'))}
open={isSettingsModalOpen}
onCancel={() => {
setActiveSettingsCenterPane(null);
setIsSettingsModalOpen(false);
}}
onCancel={handleCancelSettingsCenterPane}
footer={null}
centered
width={1080}
@@ -7632,16 +7669,6 @@ function App() {
overlayTheme={overlayTheme}
surfaceOpacity={effectiveOpacity}
/>
{isAISettingsOpen && (
<AISettingsModal
open={isAISettingsOpen}
onClose={handleCloseAISettings}
darkMode={darkMode}
overlayTheme={overlayTheme}
focusProviderId={focusedAIProviderId}
onBeforeExternalMCPUse={handlePrepareExternalMCPUse}
/>
)}
<ConnectionPackagePasswordModal
open={connectionPackageDialog.open && !(isToolsModalOpen && activeToolCenterPane?.key === 'connection-package')}
title={connectionPackageDialog.mode === 'export'