Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/updater/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as https from 'https';
import * as child_process from 'child_process';
import { fileURLToPath } from 'url';
import { EventEmitter } from 'events';
import {
getPackageManagerInfo,
Expand Down Expand Up @@ -110,6 +111,7 @@ export class UpdateManager extends EventEmitter {
// 读取当前版本
private readCurrentVersion(): string {
try {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packagePath = path.join(__dirname, '../../package.json');
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf-8'));
return packageJson.version || '0.0.0';
Expand Down
16 changes: 4 additions & 12 deletions src/web/client/src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,39 +148,31 @@ export function SettingsPanel({
case 'api':
return (
<ApiConfigPanel
onSave={() => {
console.log('API config saved');
}}
onSave={() => { onClose(); }}
onClose={onClose}
/>
);

case 'permissions':
return (
<PermissionsConfigPanel
onSave={() => {
console.log('Permissions config saved');
}}
onSave={() => { onClose(); }}
onClose={onClose}
/>
);

case 'hooks':
return (
<HooksConfigPanel
onSave={() => {
console.log('Hooks config saved');
}}
onSave={() => { onClose(); }}
onClose={onClose}
/>
);

case 'system':
return (
<SystemConfigPanel
onSave={() => {
console.log('System config saved');
}}
onSave={() => { onClose(); }}
onClose={onClose}
/>
);
Expand Down
4 changes: 4 additions & 0 deletions src/web/client/src/components/auth/OAuthLogin.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@
border-color: #5c8acc;
}

.oauth-button.apikey {
border-color: #7c6acc;
}

.button-content {
display: flex;
align-items: center;
Expand Down
107 changes: 106 additions & 1 deletion src/web/client/src/components/auth/OAuthLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface OAuthLoginProps {
onError?: (error: string) => void;
}

type LoginPhase = 'select' | 'authorize' | 'input-code';
type LoginPhase = 'select' | 'authorize' | 'input-code' | 'input-apikey';

export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) {
const { t } = useLanguage();
Expand All @@ -34,6 +34,7 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) {
const [authCode, setAuthCode] = useState<string>('');
const [selectedAccountType, setSelectedAccountType] = useState<AccountType | null>(null);
const [authUrl, setAuthUrl] = useState<string>('');
const [apiKeyInput, setApiKeyInput] = useState<string>('');

/**
* 启动 OAuth 登录流程
Expand Down Expand Up @@ -138,6 +139,45 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) {
}
};

/**
* 提交 API Key 登录
*/
const handleSubmitApiKey = async () => {
if (!apiKeyInput.trim()) {
setStatusIsError(true);
setStatus(t('auth.apiKey.required'));
return;
}

setLoading(true);
setStatusIsError(false);
setStatus(t('auth.apiKey.submitting'));

try {
const response = await fetch('/api/auth/api-key', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKey: apiKeyInput.trim() }),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || t('auth.apiKey.error', { error: response.statusText }));
}

setStatus(t('auth.apiKey.success'));
setLoading(false);
onSuccess?.();
} catch (error) {
setLoading(false);
setStatusIsError(true);
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
setStatus(t('auth.apiKey.error', { error: errorMsg }));
onError?.(errorMsg);
}
};

/**
* 返回选择阶段
*/
Expand All @@ -149,6 +189,7 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) {
setStatus('');
setStatusIsError(false);
setSelectedAccountType(null);
setApiKeyInput('');
};

/**
Expand Down Expand Up @@ -199,6 +240,20 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) {
</div>
</div>
</button>

<button
className="oauth-button apikey"
onClick={() => setPhase('input-apikey')}
disabled={loading}
>
<div className="button-content">
<div className="icon">🗝️</div>
<div className="text">
<div className="title">{t('auth.apiKey.title')}</div>
<div className="subtitle">{t('auth.apiKey.desc')}</div>
</div>
</div>
</button>
</div>

{status && (
Expand Down Expand Up @@ -226,6 +281,56 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) {
);
}

// 渲染 API Key 输入阶段
if (phase === 'input-apikey') {
return (
<div className="oauth-login">
<div className="oauth-header">
<h2>{t('auth.apiKey.inputTitle')}</h2>
<p>{t('auth.apiKey.inputDesc')}</p>
</div>

<div className="oauth-code-section">
<div className="code-input-group">
<input
type="password"
className="code-input"
placeholder={t('auth.apiKey.placeholder')}
value={apiKeyInput}
onChange={(e) => setApiKeyInput(e.target.value)}
disabled={loading}
onKeyDown={(e) => {
if (e.key === 'Enter' && apiKeyInput.trim()) {
handleSubmitApiKey();
}
}}
/>
<button
className="submit-button"
onClick={handleSubmitApiKey}
disabled={loading || !apiKeyInput.trim()}
>
{loading ? t('auth.apiKey.submitting') : t('auth.apiKey.submit')}
</button>
</div>

{status && (
<div className={`oauth-status ${loading ? 'loading' : statusIsError ? 'error' : ''}`}>
{loading && <div className="spinner"></div>}
<span>{status}</span>
</div>
)}
</div>

<div className="oauth-back">
<button className="back-button" onClick={handleBack} disabled={loading}>
{t('auth.oauth.backToLogin')}
</button>
</div>
</div>
);
}

// 渲染手动打开链接阶段(弹窗被阻止时)
if (phase === 'authorize') {
return (
Expand Down
61 changes: 36 additions & 25 deletions src/web/client/src/components/config/ApiConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,16 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) {

// 加载状态
const [loading, setLoading] = useState(false);
// 错误信息
// 加载/保存错误(顶部显示)
const [error, setError] = useState<string | null>(null);
// 验证错误
// 验证错误(按钮附近显示)
const [validationError, setValidationError] = useState<string | null>(null);
// 测试状态
const [testing, setTesting] = useState(false);
// 测试成功消息
// 测试成功消息(按钮附近显示)
const [testSuccess, setTestSuccess] = useState<string | null>(null);
// 测试失败消息(按钮附近显示)
const [testError, setTestError] = useState<string | null>(null);
// 保存成功消息
const [saveSuccess, setSaveSuccess] = useState<string | null>(null);

Expand Down Expand Up @@ -201,6 +203,7 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) {
setConfig({ ...config, [field]: value });
setValidationError(null);
setTestSuccess(null);
setTestError(null);
setSaveSuccess(null);
};

Expand All @@ -217,7 +220,7 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) {
}

setTesting(true);
setError(null);
setTestError(null);
setTestSuccess(null);
setValidationError(null);

Expand All @@ -231,18 +234,18 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) {
customModelName: config.customModelName || '',
}),
});

const data = await response.json();

if (data.success) {
setTestSuccess(t('apiConfig.testSuccess', { model: data.data.model, baseUrl: data.data.baseUrl }));
setError(null);
setTestError(null);
} else {
setError(data.error || t('apiConfig.testFailed', { error: '' }));
setTestError(data.error || t('apiConfig.testFailed', { error: '' }));
setTestSuccess(null);
}
} catch (err) {
setError(t('apiConfig.testFailed', { error: err instanceof Error ? err.message : String(err) }));
setTestError(t('apiConfig.testFailed', { error: err instanceof Error ? err.message : String(err) }));
setTestSuccess(null);
} finally {
setTesting(false);
Expand All @@ -257,24 +260,10 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) {
{t('apiConfig.description')}
</p>

{/* 错误消息 */}
{(error || validationError) && (
{/* 加载/保存错误消息(顶部显示) */}
{error && (
<div className="mcp-form-error">
{validationError || error}
</div>
)}

{/* 成功消息 */}
{(testSuccess || saveSuccess) && (
<div className="mcp-form-success" style={{
padding: '12px',
marginBottom: '16px',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: '4px',
color: '#22c55e'
}}>
✓ {testSuccess || saveSuccess}
{error}
</div>
)}

Expand Down Expand Up @@ -462,6 +451,28 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) {
</div>
</div>

{/* 验证/测试错误(按钮上方显示,保证可见) */}
{(validationError || testError) && (
<div className="mcp-form-error" style={{ marginBottom: '12px' }}>
{validationError || testError}
</div>
)}

{/* 测试成功消息(按钮上方显示,保证可见) */}
{testSuccess && (
<div style={{
padding: '10px 12px',
marginBottom: '12px',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: '4px',
color: '#22c55e',
fontSize: '13px',
}}>
✓ {testSuccess}
</div>
)}

{/* 操作按钮 */}
<div className="mcp-form-actions">
{onClose && (
Expand Down
25 changes: 25 additions & 0 deletions src/web/client/src/i18n/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,18 @@ const en: Translations = {
'auth.oauth.success': 'Login successful!',
'auth.oauth.exchangeFailed': 'Failed to exchange code',

// API Key login
'auth.apiKey.title': 'Use API Key',
'auth.apiKey.desc': 'Directly enter your Anthropic API Key',
'auth.apiKey.inputTitle': 'Enter API Key',
'auth.apiKey.inputDesc': 'Paste your API Key from console.anthropic.com',
'auth.apiKey.placeholder': 'sk-ant-...',
'auth.apiKey.submit': 'Confirm',
'auth.apiKey.submitting': 'Verifying...',
'auth.apiKey.required': 'Please enter an API Key',
'auth.apiKey.success': 'API Key verified and saved',
'auth.apiKey.error': 'Verification failed: {{error}}',

// Placeholders
'placeholder.apiBaseUrl': 'https://api.anthropic.com',
'placeholder.apiKey': 'sk-ant-...',
Expand Down Expand Up @@ -1788,6 +1800,19 @@ const zh: Translations = {
'auth.oauth.exchanging': '正在交换访问令牌...',
'auth.oauth.success': '授权成功!',
'auth.oauth.exchangeFailed': '交换授权码失败',

// API Key 登录
'auth.apiKey.title': '使用 API Key',
'auth.apiKey.desc': '直接输入您的 Anthropic API Key',
'auth.apiKey.inputTitle': '输入 API Key',
'auth.apiKey.inputDesc': '从 console.anthropic.com 获取您的 API Key 并粘贴到此处',
'auth.apiKey.placeholder': 'sk-ant-...',
'auth.apiKey.submit': '确认',
'auth.apiKey.submitting': '验证中...',
'auth.apiKey.required': '请输入 API Key',
'auth.apiKey.success': 'API Key 验证成功并已保存',
'auth.apiKey.error': '验证失败:{{error}}',

// Placeholders
'placeholder.apiBaseUrl': 'https://api.anthropic.com',
'placeholder.apiKey': 'sk-ant-...',
Expand Down
Loading
Loading