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
*/