docs(guide): rewrite CLAUDE.md with ZCLAW-first perspective
Major changes: - Shift from "OpenFang desktop client" to "independent AI Agent desktop app" - Add decision principle: "Is this useful for ZCLAW? Does it affect ZCLAW?" - Simplify project structure and tech stack sections - Replace OpenClaw vs OpenFang comparison with unified backend approach - Consolidate troubleshooting from scattered sections into organized FAQ - Update Hands system documentation with 8 capabilities and status - Stream
This commit is contained in:
@@ -1,36 +1,86 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getStoredGatewayToken, getStoredGatewayUrl } from '../../lib/gateway-client';
|
||||
import { useGatewayStore } from '../../store/gatewayStore';
|
||||
import { useChatStore } from '../../store/chatStore';
|
||||
import { silentErrorHandler } from '../../lib/error-utils';
|
||||
import { Plus, Pencil, Trash2, Star, Eye, EyeOff, AlertCircle, X } from 'lucide-react';
|
||||
|
||||
// Helper function to format context window size
|
||||
function formatContextWindow(tokens?: number): string {
|
||||
if (!tokens) return '';
|
||||
if (tokens >= 1000000) {
|
||||
return `${(tokens / 1000000).toFixed(1)}M`;
|
||||
// 自定义模型数据结构
|
||||
interface CustomModel {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
apiKey?: string;
|
||||
apiProtocol: 'openai' | 'anthropic' | 'custom';
|
||||
baseUrl?: string;
|
||||
isDefault?: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// 可用的 Provider 列表
|
||||
const AVAILABLE_PROVIDERS = [
|
||||
{ id: 'zhipu', name: '智谱 (ZhipuAI)', baseUrl: 'https://open.bigmodel.cn/api/paas/v4' },
|
||||
{ id: 'qwen', name: '百炼/通义千问 (Qwen)', baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1' },
|
||||
{ id: 'kimi', name: 'Kimi (Moonshot)', baseUrl: 'https://api.moonshot.cn/v1' },
|
||||
{ id: 'minimax', name: 'MiniMax', baseUrl: 'https://api.minimax.chat/v1' },
|
||||
{ id: 'deepseek', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1' },
|
||||
{ id: 'openai', name: 'OpenAI', baseUrl: 'https://api.openai.com/v1' },
|
||||
{ id: 'custom', name: '自定义', baseUrl: '' },
|
||||
];
|
||||
|
||||
const STORAGE_KEY = 'zclaw-custom-models';
|
||||
|
||||
// 从 localStorage 加载自定义模型
|
||||
function loadCustomModels(): CustomModel[] {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
return JSON.parse(stored);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (tokens >= 1000) {
|
||||
return `${(tokens / 1000).toFixed(0)}K`;
|
||||
return [];
|
||||
}
|
||||
|
||||
// 保存自定义模型到 localStorage
|
||||
function saveCustomModels(models: CustomModel[]): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(models));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return `${tokens}`;
|
||||
}
|
||||
|
||||
export function ModelsAPI() {
|
||||
const { connectionState, connect, disconnect, quickConfig, saveQuickConfig, models, modelsLoading, modelsError, loadModels } = useGatewayStore();
|
||||
const { connectionState, connect, disconnect, quickConfig, loadModels } = useGatewayStore();
|
||||
const { currentModel, setCurrentModel } = useChatStore();
|
||||
const [gatewayUrl, setGatewayUrl] = useState(getStoredGatewayUrl());
|
||||
const [gatewayToken, setGatewayToken] = useState(quickConfig.gatewayToken || getStoredGatewayToken());
|
||||
|
||||
// 自定义模型状态
|
||||
const [customModels, setCustomModels] = useState<CustomModel[]>([]);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [editingModel, setEditingModel] = useState<CustomModel | null>(null);
|
||||
const [showApiKey, setShowApiKey] = useState(false);
|
||||
|
||||
// 表单状态
|
||||
const [formData, setFormData] = useState({
|
||||
provider: 'zhipu',
|
||||
modelId: '',
|
||||
displayName: '',
|
||||
apiKey: '',
|
||||
apiProtocol: 'openai' as 'openai' | 'anthropic' | 'custom',
|
||||
baseUrl: '',
|
||||
});
|
||||
|
||||
const connected = connectionState === 'connected';
|
||||
const connecting = connectionState === 'connecting' || connectionState === 'reconnecting';
|
||||
|
||||
// Load models when connected
|
||||
// 加载自定义模型
|
||||
useEffect(() => {
|
||||
if (connected && models.length === 0 && !modelsLoading) {
|
||||
loadModels();
|
||||
}
|
||||
}, [connected, models.length, modelsLoading, loadModels]);
|
||||
setCustomModels(loadCustomModels());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setGatewayUrl(quickConfig.gatewayUrl || getStoredGatewayUrl());
|
||||
@@ -45,196 +95,335 @@ export function ModelsAPI() {
|
||||
).catch(silentErrorHandler('ModelsAPI')), 500);
|
||||
};
|
||||
|
||||
const handleSaveGatewaySettings = () => {
|
||||
saveQuickConfig({
|
||||
gatewayUrl,
|
||||
gatewayToken,
|
||||
}).catch(silentErrorHandler('ModelsAPI'));
|
||||
// 打开添加模型弹窗
|
||||
const handleOpenAddModal = () => {
|
||||
setFormData({
|
||||
provider: 'zhipu',
|
||||
modelId: '',
|
||||
displayName: '',
|
||||
apiKey: '',
|
||||
apiProtocol: 'openai',
|
||||
baseUrl: AVAILABLE_PROVIDERS[0].baseUrl,
|
||||
});
|
||||
setEditingModel(null);
|
||||
setShowAddModal(true);
|
||||
};
|
||||
|
||||
const handleRefreshModels = () => {
|
||||
// 打开编辑模型弹窗
|
||||
const handleOpenEditModal = (model: CustomModel) => {
|
||||
setFormData({
|
||||
provider: model.provider,
|
||||
modelId: model.id,
|
||||
displayName: model.name,
|
||||
apiKey: model.apiKey || '',
|
||||
apiProtocol: model.apiProtocol,
|
||||
baseUrl: model.baseUrl || '',
|
||||
});
|
||||
setEditingModel(model);
|
||||
setShowAddModal(true);
|
||||
};
|
||||
|
||||
// 保存模型
|
||||
const handleSaveModel = () => {
|
||||
if (!formData.modelId.trim()) return;
|
||||
|
||||
const newModel: CustomModel = {
|
||||
id: formData.modelId.trim(),
|
||||
name: formData.displayName.trim() || formData.modelId.trim(),
|
||||
provider: formData.provider,
|
||||
apiKey: formData.apiKey.trim(),
|
||||
apiProtocol: formData.apiProtocol,
|
||||
baseUrl: formData.baseUrl.trim() || AVAILABLE_PROVIDERS.find(p => p.id === formData.provider)?.baseUrl,
|
||||
createdAt: editingModel?.createdAt || new Date().toISOString(),
|
||||
};
|
||||
|
||||
let updatedModels: CustomModel[];
|
||||
if (editingModel) {
|
||||
// 编辑模式
|
||||
updatedModels = customModels.map(m => m.id === editingModel.id ? newModel : m);
|
||||
} else {
|
||||
// 添加模式
|
||||
updatedModels = [...customModels, newModel];
|
||||
}
|
||||
|
||||
setCustomModels(updatedModels);
|
||||
saveCustomModels(updatedModels);
|
||||
setShowAddModal(false);
|
||||
setEditingModel(null);
|
||||
|
||||
// 刷新模型列表
|
||||
loadModels();
|
||||
};
|
||||
|
||||
// 删除模型
|
||||
const handleDeleteModel = (modelId: string) => {
|
||||
const updatedModels = customModels.filter(m => m.id !== modelId);
|
||||
setCustomModels(updatedModels);
|
||||
saveCustomModels(updatedModels);
|
||||
};
|
||||
|
||||
// 设为默认模型
|
||||
const handleSetDefault = (modelId: string) => {
|
||||
setCurrentModel(modelId);
|
||||
// 更新自定义模型的默认状态
|
||||
const updatedModels = customModels.map(m => ({
|
||||
...m,
|
||||
isDefault: m.id === modelId,
|
||||
}));
|
||||
setCustomModels(updatedModels);
|
||||
saveCustomModels(updatedModels);
|
||||
};
|
||||
|
||||
// Provider 变更时更新 baseUrl
|
||||
const handleProviderChange = (providerId: string) => {
|
||||
const provider = AVAILABLE_PROVIDERS.find(p => p.id === providerId);
|
||||
setFormData({
|
||||
...formData,
|
||||
provider: providerId,
|
||||
baseUrl: provider?.baseUrl || '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-xl font-bold text-gray-900">模型与 API</h1>
|
||||
<h1 className="text-xl font-bold text-gray-900 dark:text-white">模型与 API</h1>
|
||||
<button
|
||||
onClick={handleReconnect}
|
||||
disabled={connecting}
|
||||
className="text-xs text-gray-500 hover:text-gray-700 px-3 py-1.5 border border-gray-200 rounded-lg transition-colors disabled:opacity-50"
|
||||
className="text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 px-3 py-1.5 border border-gray-200 dark:border-gray-700 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{connecting ? '连接中...' : '重新连接'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Gateway 连接状态 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xs font-semibold text-gray-500 mb-3 uppercase tracking-wider">当前会话模型</h3>
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-4 shadow-sm space-y-2">
|
||||
<h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 mb-3 uppercase tracking-wider">Gateway 连接</h3>
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 shadow-sm space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500">当前选择</span>
|
||||
<span className="text-sm font-medium text-orange-600">{currentModel}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500">Gateway 状态</span>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">连接状态</span>
|
||||
<span className={`text-sm ${connected ? 'text-green-600' : connecting ? 'text-yellow-600' : 'text-gray-400'}`}>
|
||||
{connected ? '已连接' : connecting ? '连接中...' : '未连接'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">当前模型</span>
|
||||
<span className="text-sm font-medium text-orange-600">{currentModel || '未选择'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内置模型 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 mb-3 uppercase tracking-wider">内置模型</h3>
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">ZCLAW 默认模型</span>
|
||||
<span className="text-xs text-gray-400">由 Gateway 配置决定</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 自定义模型 */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider">可选模型</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-400">切换后用于新的桌面对话请求</span>
|
||||
{connected && (
|
||||
<button
|
||||
onClick={handleRefreshModels}
|
||||
disabled={modelsLoading}
|
||||
className="text-xs text-orange-600 hover:text-orange-700 disabled:opacity-50"
|
||||
>
|
||||
{modelsLoading ? '加载中...' : '刷新'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">自定义模型</h3>
|
||||
<button
|
||||
onClick={handleOpenAddModal}
|
||||
className="text-xs text-orange-600 hover:text-orange-700 flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
添加自定义模型
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Loading state */}
|
||||
{modelsLoading && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-8 shadow-sm">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-orange-500"></div>
|
||||
<span className="ml-3 text-sm text-gray-500">正在加载模型列表...</span>
|
||||
</div>
|
||||
{customModels.length === 0 ? (
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 shadow-sm text-center">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">暂无自定义模型</p>
|
||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">点击上方按钮添加你的第一个自定义模型</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{modelsError && !modelsLoading && (
|
||||
<div className="bg-white rounded-xl border border-red-200 p-4 shadow-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-800">加载模型列表失败</p>
|
||||
<p className="text-xs text-red-600 mt-1">{modelsError}</p>
|
||||
<button
|
||||
onClick={handleRefreshModels}
|
||||
className="mt-2 text-xs text-red-600 hover:text-red-700 underline"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Not connected state */}
|
||||
{!connected && !modelsLoading && !modelsError && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
|
||||
<div className="text-center">
|
||||
<svg className="w-8 h-8 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<p className="text-sm text-gray-500">请先连接 Gateway 以获取可用模型列表</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model list */}
|
||||
{connected && !modelsLoading && !modelsError && models.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 divide-y divide-gray-100 shadow-sm">
|
||||
{models.map((model) => {
|
||||
const isActive = model.id === currentModel;
|
||||
return (
|
||||
<div key={model.id} className={`flex justify-between items-center p-4 ${isActive ? 'bg-orange-50/50' : ''}`}>
|
||||
<div>
|
||||
<div className="text-sm text-gray-900">{model.name}</div>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{model.provider && (
|
||||
<span className="text-xs text-gray-400">{model.provider}</span>
|
||||
)}
|
||||
{model.contextWindow && (
|
||||
<span className="text-xs text-gray-400">
|
||||
{model.provider && '|'}
|
||||
上下文 {formatContextWindow(model.contextWindow)}
|
||||
</span>
|
||||
)}
|
||||
{model.maxOutput && (
|
||||
<span className="text-xs text-gray-400">
|
||||
最大输出 {formatContextWindow(model.maxOutput)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 text-xs items-center">
|
||||
{isActive ? (
|
||||
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">当前选择</span>
|
||||
) : (
|
||||
<button onClick={() => setCurrentModel(model.id)} className="text-orange-600 hover:underline">切换到此模型</button>
|
||||
) : (
|
||||
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl divide-y divide-gray-100 dark:divide-gray-700 shadow-sm">
|
||||
{customModels.map((model) => (
|
||||
<div
|
||||
key={model.id}
|
||||
className={`flex justify-between items-center p-4 ${currentModel === model.id ? 'bg-orange-50/50 dark:bg-orange-900/10' : ''}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{model.name}</span>
|
||||
{currentModel === model.id && (
|
||||
<span className="px-1.5 py-0.5 text-xs bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400 rounded">当前</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">
|
||||
{AVAILABLE_PROVIDERS.find(p => p.id === model.provider)?.name || model.provider}
|
||||
{model.apiKey ? ' · 已配置 API Key' : ' · 未配置 API Key'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
{currentModel !== model.id && (
|
||||
<button
|
||||
onClick={() => handleSetDefault(model.id)}
|
||||
className="text-orange-600 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Star className="w-3 h-3" />
|
||||
设为默认
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleOpenEditModal(model)}
|
||||
className="text-gray-500 dark:text-gray-400 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Pencil className="w-3 h-3" />
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteModel(model.id)}
|
||||
className="text-red-500 hover:underline flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{connected && !modelsLoading && !modelsError && models.length === 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6 shadow-sm">
|
||||
<div className="text-center">
|
||||
<svg className="w-8 h-8 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 20a8 8 0 100-16 8 8 0 000 16z" />
|
||||
</svg>
|
||||
<p className="text-sm text-gray-500">暂无可用模型</p>
|
||||
<p className="text-xs text-gray-400 mt-1">请检查 Gateway 配置或 Provider 设置</p>
|
||||
{/* 添加/编辑模型弹窗 */}
|
||||
{showAddModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div className="absolute inset-0 bg-black/50" onClick={() => setShowAddModal(false)} />
|
||||
<div className="relative bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-y-auto">
|
||||
{/* 弹窗头部 */}
|
||||
<div className="sticky top-0 bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700 p-6 flex justify-between items-center z-10">
|
||||
<h3 className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{editingModel ? '编辑模型' : '添加模型'}
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 弹窗内容 */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* 警告提示 */}
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-100 dark:border-yellow-800 rounded-lg p-3 text-xs text-yellow-800 dark:text-yellow-200 flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
||||
<span>添加外部模型即表示你理解并同意自行承担使用风险。</span>
|
||||
</div>
|
||||
|
||||
{/* 服务商 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">* 服务商</label>
|
||||
<select
|
||||
value={formData.provider}
|
||||
onChange={(e) => handleProviderChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>
|
||||
{AVAILABLE_PROVIDERS.map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 模型 ID */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">* 模型 ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.modelId}
|
||||
onChange={(e) => setFormData({ ...formData, modelId: e.target.value })}
|
||||
placeholder="如:glm-4-plus"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 显示名称 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">显示名称</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.displayName}
|
||||
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
||||
placeholder="如:GLM-4-Plus"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API Key */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">API Key</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
value={formData.apiKey}
|
||||
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
||||
placeholder="请填写 API Key"
|
||||
className="w-full px-3 py-2 pr-10 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowApiKey(!showApiKey)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showApiKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API 协议 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">API 协议</label>
|
||||
<select
|
||||
value={formData.apiProtocol}
|
||||
onChange={(e) => setFormData({ ...formData, apiProtocol: e.target.value as 'openai' | 'anthropic' | 'custom' })}
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="custom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Base URL */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.baseUrl}
|
||||
onChange={(e) => setFormData({ ...formData, baseUrl: e.target.value })}
|
||||
placeholder="https://api.example.com/v1"
|
||||
className="w-full px-3 py-2 border border-gray-200 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 弹窗底部 */}
|
||||
<div className="sticky bottom-0 bg-white dark:bg-gray-800 border-t border-gray-100 dark:border-gray-700 p-6 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setShowAddModal(false)}
|
||||
className="px-4 py-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg text-sm"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSaveModel}
|
||||
disabled={!formData.modelId.trim()}
|
||||
className="px-4 py-2 bg-orange-500 text-white rounded-lg text-sm hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{editingModel ? '保存' : '添加'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-xs text-amber-700 bg-amber-50 rounded-lg p-3">
|
||||
当前页面只支持切换桌面端可选模型与维护 Gateway 连接信息,Provider Key、自定义模型增删改尚未在此页面接入。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-900">Gateway URL</span>
|
||||
<span className={`px-2 py-0.5 rounded text-xs border ${connected ? 'bg-green-50 text-green-600 border-green-100' : 'bg-red-50 text-red-600 border-red-100'}`}>
|
||||
{connected ? '已连接' : connecting ? '连接中...' : '未连接'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={handleReconnect} className="text-xs text-gray-500 hover:text-gray-700 px-3 py-1.5 border border-gray-200 rounded-lg transition-colors">
|
||||
重新连接
|
||||
</button>
|
||||
<button onClick={handleSaveGatewaySettings} className="text-xs text-white bg-orange-500 hover:bg-orange-600 px-3 py-1.5 rounded-lg transition-colors">
|
||||
保存连接设置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 bg-gray-50 border border-gray-200 rounded-xl p-3 text-xs text-gray-600 font-mono shadow-sm">
|
||||
<input
|
||||
type="text"
|
||||
value={gatewayUrl}
|
||||
onChange={(e) => setGatewayUrl(e.target.value)}
|
||||
onBlur={() => { saveQuickConfig({ gatewayUrl }).catch(silentErrorHandler('ModelsAPI')); }}
|
||||
className="w-full bg-transparent border-none outline-none"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
value={gatewayToken}
|
||||
onChange={(e) => setGatewayToken(e.target.value)}
|
||||
onBlur={() => { saveQuickConfig({ gatewayToken }).catch(silentErrorHandler('ModelsAPI')); }}
|
||||
placeholder="Gateway auth token"
|
||||
className="w-full bg-transparent border-none outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user