fix: 添加 handleAuthenticate 到依赖数组

- 修复 React Hooks exhaustive-deps 警告
This commit is contained in:
LaZzyMan 2026-03-09 11:05:11 +08:00
parent 41bb300542
commit 23c3518dff
4 changed files with 224 additions and 3 deletions

View file

@ -20,6 +20,7 @@ import { ServerDetailStep } from './steps/ServerDetailStep.js';
import { ToolListStep } from './steps/ToolListStep.js';
import { ToolDetailStep } from './steps/ToolDetailStep.js';
import { DisableScopeSelectStep } from './steps/DisableScopeSelectStep.js';
import { AuthenticateStep } from './steps/AuthenticateStep.js';
import { useConfig } from '../../contexts/ConfigContext.js';
import {
getMCPServerStatus,
@ -225,6 +226,11 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
handleNavigateToStep(MCP_MANAGEMENT_STEPS.TOOL_LIST);
}, [handleNavigateToStep]);
// Authenticate
const handleAuthenticate = useCallback(() => {
handleNavigateToStep(MCP_MANAGEMENT_STEPS.AUTHENTICATE);
}, [handleNavigateToStep]);
// Select tool
const handleSelectTool = useCallback(
(tool: MCPToolDisplayInfo) => {
@ -401,6 +407,9 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
case MCP_MANAGEMENT_STEPS.TOOL_DETAIL:
headerText = selectedTool?.name || t('Tool Detail');
break;
case MCP_MANAGEMENT_STEPS.AUTHENTICATE:
headerText = t('OAuth Authentication');
break;
default:
headerText = t('MCP Management');
}
@ -435,6 +444,7 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
onViewTools={handleViewTools}
onReconnect={handleReconnect}
onDisable={handleDisable}
onAuthenticate={handleAuthenticate}
onBack={handleNavigateBack}
/>
);
@ -463,6 +473,18 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
<ToolDetailStep tool={selectedTool} onBack={handleNavigateBack} />
);
case MCP_MANAGEMENT_STEPS.AUTHENTICATE:
return (
<AuthenticateStep
server={selectedServer}
onSuccess={() => {
// TODO: 认证成功后重新加载服务器列表
handleNavigateBack();
}}
onBack={handleNavigateBack}
/>
);
default:
return (
<Box>
@ -480,6 +502,7 @@ export const MCPManagementDialog: React.FC<MCPManagementDialogProps> = ({
handleViewTools,
handleReconnect,
handleDisable,
handleAuthenticate,
handleNavigateBack,
handleSelectTool,
handleSelectDisableScope,

View file

@ -0,0 +1,162 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { useState } from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../../semantic-colors.js';
import { useKeypress } from '../../../hooks/useKeypress.js';
import { RadioButtonSelect } from '../../shared/RadioButtonSelect.js';
import { t } from '../../../../i18n/index.js';
import type { AuthenticateStepProps } from '../types.js';
import type { MCPServerConfig } from '@qwen-code/qwen-code-core';
// TODO: 稍后从 utils.ts 导入此函数
const getOAuthConfigFromServerConfig = (_config: MCPServerConfig): unknown =>
null;
type AuthAction = 'authenticate' | 'back';
export const AuthenticateStep: React.FC<AuthenticateStepProps> = ({
server,
onBack,
}) => {
const [selectedAction, setSelectedAction] = useState<AuthAction>('back');
const [isAuthenticating, setIsAuthenticating] = useState(false);
const [authError, setAuthError] = useState<string | null>(null);
const actions = [
{
key: 'authenticate',
label: t('Authenticate'),
value: 'authenticate' as const,
},
{
key: 'back',
label: t('Back'),
value: 'back' as const,
},
];
useKeypress(
(key) => {
if (key.name === 'escape') {
onBack();
} else if (key.name === 'return' && !isAuthenticating) {
switch (selectedAction) {
case 'authenticate':
handleAuthenticate();
break;
case 'back':
onBack();
break;
default:
break;
}
}
},
{ isActive: true },
);
const handleAuthenticate = async () => {
if (!server) return;
setIsAuthenticating(true);
setAuthError(null);
try {
// TODO: 实现 OAuth 认证逻辑
// 这里需要调用 MCPOAuthProvider 进行认证
// 认证成功后调用 onSuccess()
// 认证失败时设置 authError
// 临时实现:显示提示信息
setAuthError(t('OAuth authentication is not yet implemented'));
} catch (error) {
setAuthError(
error instanceof Error ? error.message : t('Authentication failed'),
);
} finally {
setIsAuthenticating(false);
}
};
if (!server) {
return (
<Box>
<Text color={theme.status.error}>{t('No server selected')}</Text>
</Box>
);
}
const oauthConfig = getOAuthConfigFromServerConfig(server.config);
const hasOAuth = !!oauthConfig;
return (
<Box flexDirection="column" gap={1}>
{/* 认证说明 */}
<Box flexDirection="column">
<Text color={theme.text.primary} bold>
{t('OAuth Authentication')}
</Text>
<Box marginTop={1}>
<Text color={theme.text.secondary}>
{t('Server:')} {server.name}
</Text>
</Box>
{!hasOAuth && (
<Box marginTop={1}>
<Text color={theme.status.warning}>
{t('This server does not have OAuth configuration.')}
</Text>
</Box>
)}
{authError && (
<Box marginTop={1}>
<Text color={theme.status.error}>{authError}</Text>
</Box>
)}
</Box>
{/* 操作列表 */}
{!hasOAuth ? (
<Box>
<RadioButtonSelect<AuthAction>
items={actions.filter((a) => a.key === 'back')}
onHighlight={(value: AuthAction) => setSelectedAction(value)}
onSelect={(value: AuthAction) => {
if (value === 'back') {
onBack();
}
}}
/>
</Box>
) : (
<Box>
<RadioButtonSelect<AuthAction>
items={actions}
onHighlight={(value: AuthAction) => setSelectedAction(value)}
onSelect={(value: AuthAction) => {
if (value === 'back') {
onBack();
}
}}
/>
</Box>
)}
{isAuthenticating && (
<Box>
<Text color={theme.text.secondary}>
{t('Authenticating... Please wait.')}
</Text>
</Box>
)}
</Box>
);
};

View file

@ -20,13 +20,18 @@ import {
// 标签列宽度
const LABEL_WIDTH = 15;
type ServerAction = 'view-tools' | 'reconnect' | 'toggle-disable';
type ServerAction =
| 'view-tools'
| 'reconnect'
| 'toggle-disable'
| 'authenticate';
export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
server,
onViewTools,
onReconnect,
onDisable,
onAuthenticate,
onBack,
}) => {
const [selectedAction, setSelectedAction] =
@ -71,6 +76,16 @@ export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
value: 'toggle-disable',
});
// 如果服务器配置了 OAuth显示认证选项
if (server && !server.isDisabled) {
// TODO: 检查服务器是否有 OAuth 配置
result.push({
key: 'authenticate',
label: t('Authenticate'),
value: 'authenticate',
});
}
return result;
}, [server]);
@ -89,6 +104,9 @@ export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
case 'toggle-disable':
onDisable?.();
break;
case 'authenticate':
onAuthenticate?.();
break;
default:
break;
}
@ -228,6 +246,9 @@ export const ServerDetailStep: React.FC<ServerDetailStepProps> = ({
case 'toggle-disable':
onDisable?.();
break;
case 'authenticate':
onAuthenticate?.();
break;
default:
break;
}

View file

@ -18,6 +18,7 @@ export const MCP_MANAGEMENT_STEPS = {
DISABLE_SCOPE_SELECT: 'disable-scope-select',
TOOL_LIST: 'tool-list',
TOOL_DETAIL: 'tool-detail',
AUTHENTICATE: 'authenticate', // OAuth 认证步骤
} as const;
export type MCPManagementStep =
@ -120,7 +121,7 @@ export interface ServerListStepProps {
}
/**
* ServerDetailStep组件属
* ServerDetailStep
*/
export interface ServerDetailStepProps {
/** 选中的服务器 */
@ -131,6 +132,8 @@ export interface ServerDetailStepProps {
onReconnect?: () => void;
/** 禁用服务器回调 */
onDisable?: () => void;
/** OAuth 认证回调 */
onAuthenticate?: () => void;
/** 返回回调 */
onBack: () => void;
}
@ -162,7 +165,7 @@ export interface ToolListStepProps {
}
/**
* ToolDetailStep组件属
* ToolDetailStep
*/
export interface ToolDetailStepProps {
/** 工具信息 */
@ -171,6 +174,18 @@ export interface ToolDetailStepProps {
onBack: () => void;
}
/**
* AuthenticateStep
*/
export interface AuthenticateStepProps {
/** 服务器信息 */
server: MCPServerDisplayInfo | null;
/** 认证成功回调 */
onSuccess?: () => void;
/** 返回回调 */
onBack: () => void;
}
/**
* MCP管理对话框属性
*/