feat(providers): add provider categorization system
- Add ProviderCategory type with official, cn_official, aggregator, third_party, and custom categories - Update Provider interface and Rust struct to include optional category field - Enhance ProviderForm to automatically sync category when selecting presets - Improve PresetSelector to show category-based styling and hints - Add category classification to all provider presets - Support differentiated interactions (e.g., hide API key input for official providers) - Maintain backward compatibility with existing configurations
This commit is contained in:
@@ -14,6 +14,8 @@ pub struct Provider {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(rename = "websiteUrl")]
|
#[serde(rename = "websiteUrl")]
|
||||||
pub website_url: Option<String>,
|
pub website_url: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub category: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Provider {
|
impl Provider {
|
||||||
@@ -29,6 +31,7 @@ impl Provider {
|
|||||||
name,
|
name,
|
||||||
settings_config,
|
settings_config,
|
||||||
website_url,
|
website_url,
|
||||||
|
category: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Provider } from "../types";
|
import { Provider, ProviderCategory } from "../types";
|
||||||
import { AppType } from "../lib/tauri-api";
|
import { AppType } from "../lib/tauri-api";
|
||||||
import {
|
import {
|
||||||
updateCoAuthoredSetting,
|
updateCoAuthoredSetting,
|
||||||
@@ -16,6 +16,7 @@ import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor";
|
|||||||
import CodexConfigEditor from "./ProviderForm/CodexConfigEditor";
|
import CodexConfigEditor from "./ProviderForm/CodexConfigEditor";
|
||||||
import KimiModelSelector from "./ProviderForm/KimiModelSelector";
|
import KimiModelSelector from "./ProviderForm/KimiModelSelector";
|
||||||
import { X, AlertCircle, Save } from "lucide-react";
|
import { X, AlertCircle, Save } from "lucide-react";
|
||||||
|
// 分类仅用于控制少量交互(如官方禁用 API Key),不显示介绍组件
|
||||||
|
|
||||||
interface ProviderFormProps {
|
interface ProviderFormProps {
|
||||||
appType?: AppType;
|
appType?: AppType;
|
||||||
@@ -46,6 +47,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
? JSON.stringify(initialData.settingsConfig, null, 2)
|
? JSON.stringify(initialData.settingsConfig, null, 2)
|
||||||
: "",
|
: "",
|
||||||
});
|
});
|
||||||
|
const [category, setCategory] = useState<ProviderCategory | undefined>(
|
||||||
|
initialData?.category,
|
||||||
|
);
|
||||||
|
|
||||||
// Codex 特有的状态
|
// Codex 特有的状态
|
||||||
const [codexAuth, setCodexAuth] = useState("");
|
const [codexAuth, setCodexAuth] = useState("");
|
||||||
@@ -113,6 +117,26 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}
|
}
|
||||||
}, [initialData]);
|
}, [initialData]);
|
||||||
|
|
||||||
|
// 当选择预设变化时,同步类别
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showPresets) return;
|
||||||
|
if (!isCodex) {
|
||||||
|
if (selectedPreset !== null && selectedPreset >= 0) {
|
||||||
|
const preset = providerPresets[selectedPreset];
|
||||||
|
setCategory(preset?.category || (preset?.isOfficial ? "official" : undefined));
|
||||||
|
} else if (selectedPreset === -1) {
|
||||||
|
setCategory("custom");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedCodexPreset !== null && selectedCodexPreset >= 0) {
|
||||||
|
const preset = codexProviderPresets[selectedCodexPreset];
|
||||||
|
setCategory(preset?.category || (preset?.isOfficial ? "official" : undefined));
|
||||||
|
} else if (selectedCodexPreset === -1) {
|
||||||
|
setCategory("custom");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [showPresets, isCodex, selectedPreset, selectedCodexPreset]);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError("");
|
setError("");
|
||||||
@@ -177,6 +201,8 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
name: formData.name,
|
name: formData.name,
|
||||||
websiteUrl: formData.websiteUrl,
|
websiteUrl: formData.websiteUrl,
|
||||||
settingsConfig,
|
settingsConfig,
|
||||||
|
// 仅在用户选择了预设或手动选择“自定义”时持久化分类
|
||||||
|
...(category ? { category } : {}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -230,6 +256,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
websiteUrl: preset.websiteUrl,
|
websiteUrl: preset.websiteUrl,
|
||||||
settingsConfig: configString,
|
settingsConfig: configString,
|
||||||
});
|
});
|
||||||
|
setCategory(preset.category || (preset.isOfficial ? "official" : undefined));
|
||||||
|
|
||||||
// 设置选中的预设
|
// 设置选中的预设
|
||||||
setSelectedPreset(index);
|
setSelectedPreset(index);
|
||||||
@@ -272,6 +299,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setDisableCoAuthored(false);
|
setDisableCoAuthored(false);
|
||||||
setKimiAnthropicModel("");
|
setKimiAnthropicModel("");
|
||||||
setKimiAnthropicSmallFastModel("");
|
setKimiAnthropicSmallFastModel("");
|
||||||
|
setCategory("custom");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Codex: 应用预设
|
// Codex: 应用预设
|
||||||
@@ -290,6 +318,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
setSelectedCodexPreset(index);
|
setSelectedCodexPreset(index);
|
||||||
|
setCategory(preset.category || (preset.isOfficial ? "official" : undefined));
|
||||||
|
|
||||||
// 清空 API Key,让用户重新输入
|
// 清空 API Key,让用户重新输入
|
||||||
setCodexApiKey("");
|
setCodexApiKey("");
|
||||||
@@ -306,6 +335,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
setCodexAuth("");
|
setCodexAuth("");
|
||||||
setCodexConfig("");
|
setCodexConfig("");
|
||||||
setCodexApiKey("");
|
setCodexApiKey("");
|
||||||
|
setCategory("custom");
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理 API Key 输入并自动更新配置
|
// 处理 API Key 输入并自动更新配置
|
||||||
@@ -349,9 +379,11 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
|
|
||||||
// 判断当前选中的预设是否是官方
|
// 判断当前选中的预设是否是官方
|
||||||
const isOfficialPreset =
|
const isOfficialPreset =
|
||||||
selectedPreset !== null &&
|
(selectedPreset !== null &&
|
||||||
selectedPreset >= 0 &&
|
selectedPreset >= 0 &&
|
||||||
providerPresets[selectedPreset]?.isOfficial === true;
|
(providerPresets[selectedPreset]?.isOfficial === true ||
|
||||||
|
providerPresets[selectedPreset]?.category === "official")) ||
|
||||||
|
category === "official";
|
||||||
|
|
||||||
// 判断当前选中的预设是否是 Kimi
|
// 判断当前选中的预设是否是 Kimi
|
||||||
const isKimiPreset =
|
const isKimiPreset =
|
||||||
@@ -385,10 +417,14 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
|||||||
(selectedCodexPreset !== null && selectedCodexPreset !== -1) ||
|
(selectedCodexPreset !== null && selectedCodexPreset !== -1) ||
|
||||||
(!showPresets && getCodexAuthApiKey(codexAuth) !== "");
|
(!showPresets && getCodexAuthApiKey(codexAuth) !== "");
|
||||||
|
|
||||||
|
// 不再渲染分类介绍组件,避免造成干扰
|
||||||
|
|
||||||
const isCodexOfficialPreset =
|
const isCodexOfficialPreset =
|
||||||
selectedCodexPreset !== null &&
|
(selectedCodexPreset !== null &&
|
||||||
selectedCodexPreset >= 0 &&
|
selectedCodexPreset >= 0 &&
|
||||||
codexProviderPresets[selectedCodexPreset]?.isOfficial === true;
|
(codexProviderPresets[selectedCodexPreset]?.isOfficial === true ||
|
||||||
|
codexProviderPresets[selectedCodexPreset]?.category === "official")) ||
|
||||||
|
category === "official";
|
||||||
|
|
||||||
// Kimi 模型选择处理函数
|
// Kimi 模型选择处理函数
|
||||||
const handleKimiModelChange = (
|
const handleKimiModelChange = (
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Zap } from "lucide-react";
|
import { Zap } from "lucide-react";
|
||||||
|
import { ProviderCategory } from "../../types";
|
||||||
|
|
||||||
interface Preset {
|
interface Preset {
|
||||||
name: string;
|
name: string;
|
||||||
isOfficial?: boolean;
|
isOfficial?: boolean;
|
||||||
|
category?: ProviderCategory;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PresetSelectorProps {
|
interface PresetSelectorProps {
|
||||||
@@ -23,13 +25,13 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
onCustomClick,
|
onCustomClick,
|
||||||
customLabel = "自定义",
|
customLabel = "自定义",
|
||||||
}) => {
|
}) => {
|
||||||
const getButtonClass = (index: number, isOfficial?: boolean) => {
|
const getButtonClass = (index: number, isOfficial?: boolean, category?: ProviderCategory) => {
|
||||||
const isSelected = selectedIndex === index;
|
const isSelected = selectedIndex === index;
|
||||||
const baseClass =
|
const baseClass =
|
||||||
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
"inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors";
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
return isOfficial
|
return isOfficial || category === "official"
|
||||||
? `${baseClass} bg-amber-500 text-white`
|
? `${baseClass} bg-amber-500 text-white`
|
||||||
: `${baseClass} bg-blue-500 text-white`;
|
: `${baseClass} bg-blue-500 text-white`;
|
||||||
}
|
}
|
||||||
@@ -44,8 +46,8 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
|
|
||||||
if (selectedIndex !== null && selectedIndex >= 0) {
|
if (selectedIndex !== null && selectedIndex >= 0) {
|
||||||
const preset = presets[selectedIndex];
|
const preset = presets[selectedIndex];
|
||||||
return preset?.isOfficial
|
return preset?.isOfficial || preset?.category === "official"
|
||||||
? "Claude 官方登录,不需要填写 API Key"
|
? "官方登录,不需要填写 API Key"
|
||||||
: "使用预设配置,只需填写 API Key";
|
: "使用预设配置,只需填写 API Key";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +72,10 @@ const PresetSelector: React.FC<PresetSelectorProps> = ({
|
|||||||
<button
|
<button
|
||||||
key={index}
|
key={index}
|
||||||
type="button"
|
type="button"
|
||||||
className={getButtonClass(index, preset.isOfficial)}
|
className={getButtonClass(index, preset.isOfficial, preset.category)}
|
||||||
onClick={() => onSelectPreset(index)}
|
onClick={() => onSelectPreset(index)}
|
||||||
>
|
>
|
||||||
{preset.isOfficial && <Zap size={14} />}
|
{(preset.isOfficial || preset.category === "official") && <Zap size={14} />}
|
||||||
{preset.name}
|
{preset.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import { Provider } from "../types";
|
import { Provider } from "../types";
|
||||||
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
import { Play, Edit3, Trash2, CheckCircle2, Users } from "lucide-react";
|
||||||
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
import { buttonStyles, cardStyles, badgeStyles, cn } from "../lib/styles";
|
||||||
|
// 不再在列表中显示分类徽章,避免造成困惑
|
||||||
|
|
||||||
interface ProviderListProps {
|
interface ProviderListProps {
|
||||||
providers: Record<string, Provider>;
|
providers: Record<string, Provider>;
|
||||||
@@ -99,6 +100,7 @@ const ProviderList: React.FC<ProviderListProps> = ({
|
|||||||
<h3 className="font-medium text-gray-900 dark:text-gray-100">
|
<h3 className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{provider.name}
|
{provider.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
{/* 分类徽章已移除 */}
|
||||||
{isCurrent && (
|
{isCurrent && (
|
||||||
<div className={badgeStyles.success}>
|
<div className={badgeStyles.success}>
|
||||||
<CheckCircle2 size={12} />
|
<CheckCircle2 size={12} />
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Codex 预设供应商配置模板
|
* Codex 预设供应商配置模板
|
||||||
*/
|
*/
|
||||||
|
import { ProviderCategory } from "../types";
|
||||||
|
|
||||||
export interface CodexProviderPreset {
|
export interface CodexProviderPreset {
|
||||||
name: string;
|
name: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
auth: Record<string, any>; // 将写入 ~/.codex/auth.json
|
auth: Record<string, any>; // 将写入 ~/.codex/auth.json
|
||||||
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
config: string; // 将写入 ~/.codex/config.toml(TOML 字符串)
|
||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
|
category?: ProviderCategory; // 新增:分类
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codexProviderPresets: CodexProviderPreset[] = [
|
export const codexProviderPresets: CodexProviderPreset[] = [
|
||||||
@@ -14,6 +17,7 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
|||||||
name: "Codex官方",
|
name: "Codex官方",
|
||||||
websiteUrl: "https://chatgpt.com/codex",
|
websiteUrl: "https://chatgpt.com/codex",
|
||||||
isOfficial: true,
|
isOfficial: true,
|
||||||
|
category: "official",
|
||||||
// 官方的 key 为null
|
// 官方的 key 为null
|
||||||
auth: {
|
auth: {
|
||||||
OPENAI_API_KEY: null,
|
OPENAI_API_KEY: null,
|
||||||
@@ -23,6 +27,7 @@ export const codexProviderPresets: CodexProviderPreset[] = [
|
|||||||
{
|
{
|
||||||
name: "PackyCode",
|
name: "PackyCode",
|
||||||
websiteUrl: "https://codex.packycode.com/",
|
websiteUrl: "https://codex.packycode.com/",
|
||||||
|
category: "third_party",
|
||||||
// PackyCode 一般通过 API Key;请将占位符替换为你的实际 key
|
// PackyCode 一般通过 API Key;请将占位符替换为你的实际 key
|
||||||
auth: {
|
auth: {
|
||||||
OPENAI_API_KEY: "sk-your-api-key-here",
|
OPENAI_API_KEY: "sk-your-api-key-here",
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* 预设供应商配置模板
|
* 预设供应商配置模板
|
||||||
*/
|
*/
|
||||||
|
import { ProviderCategory } from "../types";
|
||||||
|
|
||||||
export interface ProviderPreset {
|
export interface ProviderPreset {
|
||||||
name: string;
|
name: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
settingsConfig: object;
|
settingsConfig: object;
|
||||||
isOfficial?: boolean; // 标识是否为官方预设
|
isOfficial?: boolean; // 标识是否为官方预设
|
||||||
|
category?: ProviderCategory; // 新增:分类
|
||||||
}
|
}
|
||||||
|
|
||||||
export const providerPresets: ProviderPreset[] = [
|
export const providerPresets: ProviderPreset[] = [
|
||||||
@@ -16,6 +19,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
env: {},
|
env: {},
|
||||||
},
|
},
|
||||||
isOfficial: true, // 明确标识为官方预设
|
isOfficial: true, // 明确标识为官方预设
|
||||||
|
category: "official",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "DeepSeek v3.1",
|
name: "DeepSeek v3.1",
|
||||||
@@ -28,6 +32,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_SMALL_FAST_MODEL: "deepseek-chat",
|
ANTHROPIC_SMALL_FAST_MODEL: "deepseek-chat",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: "cn_official",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "智谱GLM",
|
name: "智谱GLM",
|
||||||
@@ -38,6 +43,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: "cn_official",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "千问Qwen-Coder",
|
name: "千问Qwen-Coder",
|
||||||
@@ -49,6 +55,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: "cn_official",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Kimi k2",
|
name: "Kimi k2",
|
||||||
@@ -61,6 +68,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_SMALL_FAST_MODEL: "kimi-k2-turbo-preview",
|
ANTHROPIC_SMALL_FAST_MODEL: "kimi-k2-turbo-preview",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: "cn_official",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "魔搭",
|
name: "魔搭",
|
||||||
@@ -73,6 +81,7 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.5",
|
ANTHROPIC_SMALL_FAST_MODEL: "ZhipuAI/GLM-4.5",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: "aggregator",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "PackyCode",
|
name: "PackyCode",
|
||||||
@@ -83,5 +92,6 @@ export const providerPresets: ProviderPreset[] = [
|
|||||||
ANTHROPIC_AUTH_TOKEN: "",
|
ANTHROPIC_AUTH_TOKEN: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category: "third_party",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
|
export type ProviderCategory =
|
||||||
|
| "official" // 官方
|
||||||
|
| "cn_official" // 国产官方
|
||||||
|
| "aggregator" // 聚合网站
|
||||||
|
| "third_party" // 第三方供应商
|
||||||
|
| "custom"; // 自定义
|
||||||
|
|
||||||
export interface Provider {
|
export interface Provider {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
settingsConfig: Record<string, any>; // 应用配置对象:Claude 为 settings.json;Codex 为 { auth, config }
|
settingsConfig: Record<string, any>; // 应用配置对象:Claude 为 settings.json;Codex 为 { auth, config }
|
||||||
websiteUrl?: string;
|
websiteUrl?: string;
|
||||||
|
// 新增:供应商分类(用于差异化提示/能力开关)
|
||||||
|
category?: ProviderCategory;
|
||||||
createdAt?: number; // 添加时间戳(毫秒)
|
createdAt?: number; // 添加时间戳(毫秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user