diff --git a/src/updater/index.ts b/src/updater/index.ts index b1cb1827..9feba6fe 100644 --- a/src/updater/index.ts +++ b/src/updater/index.ts @@ -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, @@ -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'; diff --git a/src/web/client/src/components/SettingsPanel.tsx b/src/web/client/src/components/SettingsPanel.tsx index f9376444..ffbc1ad6 100644 --- a/src/web/client/src/components/SettingsPanel.tsx +++ b/src/web/client/src/components/SettingsPanel.tsx @@ -148,9 +148,7 @@ export function SettingsPanel({ case 'api': return ( { - console.log('API config saved'); - }} + onSave={() => { onClose(); }} onClose={onClose} /> ); @@ -158,9 +156,7 @@ export function SettingsPanel({ case 'permissions': return ( { - console.log('Permissions config saved'); - }} + onSave={() => { onClose(); }} onClose={onClose} /> ); @@ -168,9 +164,7 @@ export function SettingsPanel({ case 'hooks': return ( { - console.log('Hooks config saved'); - }} + onSave={() => { onClose(); }} onClose={onClose} /> ); @@ -178,9 +172,7 @@ export function SettingsPanel({ case 'system': return ( { - console.log('System config saved'); - }} + onSave={() => { onClose(); }} onClose={onClose} /> ); diff --git a/src/web/client/src/components/auth/OAuthLogin.css b/src/web/client/src/components/auth/OAuthLogin.css index 08847c25..21dd0375 100644 --- a/src/web/client/src/components/auth/OAuthLogin.css +++ b/src/web/client/src/components/auth/OAuthLogin.css @@ -62,6 +62,10 @@ border-color: #5c8acc; } +.oauth-button.apikey { + border-color: #7c6acc; +} + .button-content { display: flex; align-items: center; diff --git a/src/web/client/src/components/auth/OAuthLogin.tsx b/src/web/client/src/components/auth/OAuthLogin.tsx index b8e1bbe5..fce70e46 100644 --- a/src/web/client/src/components/auth/OAuthLogin.tsx +++ b/src/web/client/src/components/auth/OAuthLogin.tsx @@ -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(); @@ -34,6 +34,7 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) { const [authCode, setAuthCode] = useState(''); const [selectedAccountType, setSelectedAccountType] = useState(null); const [authUrl, setAuthUrl] = useState(''); + const [apiKeyInput, setApiKeyInput] = useState(''); /** * 启动 OAuth 登录流程 @@ -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); + } + }; + /** * 返回选择阶段 */ @@ -149,6 +189,7 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) { setStatus(''); setStatusIsError(false); setSelectedAccountType(null); + setApiKeyInput(''); }; /** @@ -199,6 +240,20 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) { + + {status && ( @@ -226,6 +281,56 @@ export function OAuthLogin({ onSuccess, onError }: OAuthLoginProps) { ); } + // 渲染 API Key 输入阶段 + if (phase === 'input-apikey') { + return ( +
+
+

{t('auth.apiKey.inputTitle')}

+

{t('auth.apiKey.inputDesc')}

+
+ +
+
+ setApiKeyInput(e.target.value)} + disabled={loading} + onKeyDown={(e) => { + if (e.key === 'Enter' && apiKeyInput.trim()) { + handleSubmitApiKey(); + } + }} + /> + +
+ + {status && ( +
+ {loading &&
} + {status} +
+ )} +
+ +
+ +
+
+ ); + } + // 渲染手动打开链接阶段(弹窗被阻止时) if (phase === 'authorize') { return ( diff --git a/src/web/client/src/components/config/ApiConfigPanel.tsx b/src/web/client/src/components/config/ApiConfigPanel.tsx index b975f3fe..e7caa295 100644 --- a/src/web/client/src/components/config/ApiConfigPanel.tsx +++ b/src/web/client/src/components/config/ApiConfigPanel.tsx @@ -108,14 +108,16 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) { // 加载状态 const [loading, setLoading] = useState(false); - // 错误信息 + // 加载/保存错误(顶部显示) const [error, setError] = useState(null); - // 验证错误 + // 验证错误(按钮附近显示) const [validationError, setValidationError] = useState(null); // 测试状态 const [testing, setTesting] = useState(false); - // 测试成功消息 + // 测试成功消息(按钮附近显示) const [testSuccess, setTestSuccess] = useState(null); + // 测试失败消息(按钮附近显示) + const [testError, setTestError] = useState(null); // 保存成功消息 const [saveSuccess, setSaveSuccess] = useState(null); @@ -201,6 +203,7 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) { setConfig({ ...config, [field]: value }); setValidationError(null); setTestSuccess(null); + setTestError(null); setSaveSuccess(null); }; @@ -217,7 +220,7 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) { } setTesting(true); - setError(null); + setTestError(null); setTestSuccess(null); setValidationError(null); @@ -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); @@ -257,24 +260,10 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) { {t('apiConfig.description')}

- {/* 错误消息 */} - {(error || validationError) && ( + {/* 加载/保存错误消息(顶部显示) */} + {error && (
- {validationError || error} -
- )} - - {/* 成功消息 */} - {(testSuccess || saveSuccess) && ( -
- ✓ {testSuccess || saveSuccess} + {error}
)} @@ -462,6 +451,28 @@ export function ApiConfigPanel({ onSave, onClose }: ApiConfigPanelProps) { + {/* 验证/测试错误(按钮上方显示,保证可见) */} + {(validationError || testError) && ( +
+ {validationError || testError} +
+ )} + + {/* 测试成功消息(按钮上方显示,保证可见) */} + {testSuccess && ( +
+ ✓ {testSuccess} +
+ )} + {/* 操作按钮 */}
{onClose && ( diff --git a/src/web/client/src/i18n/locales.ts b/src/web/client/src/i18n/locales.ts index 27db2c32..267e495d 100644 --- a/src/web/client/src/i18n/locales.ts +++ b/src/web/client/src/i18n/locales.ts @@ -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-...', @@ -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-...', diff --git a/src/web/server/channels/index.ts b/src/web/server/channels/index.ts index 86b04dee..59943fdc 100644 --- a/src/web/server/channels/index.ts +++ b/src/web/server/channels/index.ts @@ -5,6 +5,7 @@ * 是 channels/ 模块的唯一对外入口。 */ +import * as fs from 'fs'; import type { ConversationManager } from '../conversation.js'; import type { ChannelAdapter, @@ -224,8 +225,18 @@ export class ChannelManager { // ========================================================================== private getChannelsConfig(): ChannelsConfig | undefined { - const config = configManager.getAll(); - return (config as any).channels as ChannelsConfig | undefined; + // UserConfigSchema.parse() strips unknown fields (like 'channels') from mergedConfig, + // so we read directly from settings.json to avoid the Zod stripping issue. + try { + const settingsPath = configManager.getConfigPaths().userSettings; + if (fs.existsSync(settingsPath)) { + const raw = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + return raw.channels as ChannelsConfig | undefined; + } + } catch { + // fall through + } + return undefined; } private broadcastStatusUpdate(channelId: string): void { diff --git a/src/web/server/routes/auth.ts b/src/web/server/routes/auth.ts index 7a016b18..fc42404e 100644 --- a/src/web/server/routes/auth.ts +++ b/src/web/server/routes/auth.ts @@ -289,6 +289,39 @@ router.get('/status', async (req: Request, res: Response) => { res.json({ authenticated: false }); }); +/** + * POST /api/auth/api-key + * 使用 API Key 直接登录 + */ +router.post('/api-key', async (req: Request, res: Response) => { + try { + const { apiKey } = req.body as { apiKey: string }; + + if (!apiKey || typeof apiKey !== 'string' || !apiKey.trim()) { + return res.status(400).json({ error: 'API Key is required' }); + } + + const trimmedKey = apiKey.trim(); + + // 验证 API Key 有效性 + const isValid = await webAuth.validateApiKey(trimmedKey); + if (!isValid) { + return res.status(401).json({ error: 'Invalid API Key' }); + } + + // 保存 API Key 并将认证优先级设为 apiKey + const saved = webAuth.saveApiKeyLogin(trimmedKey); + if (!saved) { + return res.status(500).json({ error: 'Failed to save API Key' }); + } + + res.json({ success: true }); + } catch (error) { + console.error('[Auth] API Key login error:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + /** * POST /api/auth/logout * 登出(清除 WebUI 管理的所有认证) diff --git a/src/web/server/web-auth.ts b/src/web/server/web-auth.ts index c6bb99d8..1d258505 100644 --- a/src/web/server/web-auth.ts +++ b/src/web/server/web-auth.ts @@ -261,6 +261,21 @@ class WebAuthProvider { } } + /** + * 保存 API Key 并将认证优先级设为 apiKey + */ + saveApiKeyLogin(key: string): boolean { + if (!key || typeof key !== 'string') return false; + try { + configManager.set('apiKey', key); + configManager.set('authPriority', 'apiKey'); + return true; + } catch (error) { + console.error('[WebAuth] Failed to save API Key login:', error); + return false; + } + } + /** * 清除 API Key */