2025-08-21 22:20:57 +08:00
|
|
|
|
import React, { useState, useEffect } from "react";
|
2025-08-23 23:11:39 +08:00
|
|
|
|
import { Provider } from "../types";
|
2025-08-21 22:20:57 +08:00
|
|
|
|
import {
|
|
|
|
|
|
updateCoAuthoredSetting,
|
|
|
|
|
|
checkCoAuthoredSetting,
|
|
|
|
|
|
extractWebsiteUrl,
|
2025-08-26 10:37:44 +08:00
|
|
|
|
getApiKeyFromConfig,
|
|
|
|
|
|
hasApiKeyField,
|
|
|
|
|
|
setApiKeyInConfig,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
} from "../utils/providerConfigUtils";
|
|
|
|
|
|
import { providerPresets } from "../config/providerPresets";
|
|
|
|
|
|
import "./AddProviderModal.css";
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
interface ProviderFormProps {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
title: string;
|
|
|
|
|
|
submitText: string;
|
|
|
|
|
|
initialData?: Provider;
|
|
|
|
|
|
showPresets?: boolean;
|
|
|
|
|
|
onSubmit: (data: Omit<Provider, "id">) => void;
|
|
|
|
|
|
onClose: () => void;
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ProviderForm: React.FC<ProviderFormProps> = ({
|
|
|
|
|
|
title,
|
|
|
|
|
|
submitText,
|
|
|
|
|
|
initialData,
|
|
|
|
|
|
showPresets = false,
|
|
|
|
|
|
onSubmit,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
onClose,
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}) => {
|
|
|
|
|
|
const [formData, setFormData] = useState({
|
2025-08-21 22:20:57 +08:00
|
|
|
|
name: initialData?.name || "",
|
|
|
|
|
|
websiteUrl: initialData?.websiteUrl || "",
|
|
|
|
|
|
settingsConfig: initialData
|
|
|
|
|
|
? JSON.stringify(initialData.settingsConfig, null, 2)
|
|
|
|
|
|
: "",
|
|
|
|
|
|
});
|
|
|
|
|
|
const [error, setError] = useState("");
|
|
|
|
|
|
const [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
|
|
|
|
|
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
|
|
|
|
|
|
const [apiKey, setApiKey] = useState("");
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 初始化时检查禁用签名状态
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (initialData) {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
|
|
|
|
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
|
|
|
|
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
2025-08-21 22:20:57 +08:00
|
|
|
|
}, [initialData]);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
setError("");
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
if (!formData.name) {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
setError("请填写供应商名称");
|
|
|
|
|
|
return;
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!formData.settingsConfig.trim()) {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
setError("请填写配置内容");
|
|
|
|
|
|
return;
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-21 22:20:57 +08:00
|
|
|
|
let settingsConfig: Record<string, any>;
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
try {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
settingsConfig = JSON.parse(formData.settingsConfig);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
} catch (err) {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
setError("配置JSON格式错误,请检查语法");
|
|
|
|
|
|
return;
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onSubmit({
|
|
|
|
|
|
name: formData.name,
|
|
|
|
|
|
websiteUrl: formData.websiteUrl,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
settingsConfig,
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
const handleChange = (
|
2025-08-27 11:00:53 +08:00
|
|
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
2025-08-08 15:03:38 +08:00
|
|
|
|
) => {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const { name, value } = e.target;
|
|
|
|
|
|
|
|
|
|
|
|
if (name === "settingsConfig") {
|
2025-08-08 15:03:38 +08:00
|
|
|
|
// 当用户修改配置时,尝试自动提取官网地址
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const extractedWebsiteUrl = extractWebsiteUrl(value);
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
// 同时检查并同步选择框状态
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(value);
|
|
|
|
|
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
|
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 同步 API Key 输入框显示与值
|
|
|
|
|
|
const parsedKey = getApiKeyFromConfig(value);
|
|
|
|
|
|
setApiKey(parsedKey);
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
[name]: value,
|
|
|
|
|
|
// 只有在官网地址为空时才自动填入
|
|
|
|
|
|
websiteUrl: formData.websiteUrl || extractedWebsiteUrl,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
});
|
2025-08-08 15:03:38 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
[name]: value,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
});
|
2025-08-08 15:03:38 +08:00
|
|
|
|
}
|
2025-08-21 22:20:57 +08:00
|
|
|
|
};
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 处理选择框变化
|
|
|
|
|
|
const handleCoAuthoredToggle = (checked: boolean) => {
|
2025-08-21 22:20:57 +08:00
|
|
|
|
setDisableCoAuthored(checked);
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
// 更新JSON配置
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const updatedConfig = updateCoAuthoredSetting(
|
|
|
|
|
|
formData.settingsConfig,
|
2025-08-27 11:00:53 +08:00
|
|
|
|
checked,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
setFormData({
|
|
|
|
|
|
...formData,
|
|
|
|
|
|
settingsConfig: updatedConfig,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const applyPreset = (preset: (typeof providerPresets)[0], index: number) => {
|
|
|
|
|
|
const configString = JSON.stringify(preset.settingsConfig, null, 2);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
|
|
|
|
|
setFormData({
|
|
|
|
|
|
name: preset.name,
|
|
|
|
|
|
websiteUrl: preset.websiteUrl,
|
2025-08-21 22:20:57 +08:00
|
|
|
|
settingsConfig: configString,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-21 21:46:48 +08:00
|
|
|
|
// 设置选中的预设
|
2025-08-21 22:20:57 +08:00
|
|
|
|
setSelectedPreset(index);
|
|
|
|
|
|
|
|
|
|
|
|
// 清空 API Key 输入框,让用户重新输入
|
|
|
|
|
|
setApiKey("");
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
// 同步选择框状态
|
2025-08-21 22:20:57 +08:00
|
|
|
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
|
|
|
|
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理 API Key 输入并自动更新配置
|
|
|
|
|
|
const handleApiKeyChange = (key: string) => {
|
|
|
|
|
|
setApiKey(key);
|
|
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
const configString = setApiKeyInConfig(
|
|
|
|
|
|
formData.settingsConfig,
|
|
|
|
|
|
key.trim(),
|
2025-08-27 11:00:53 +08:00
|
|
|
|
{ createIfMissing: selectedPreset !== null },
|
2025-08-26 10:37:44 +08:00
|
|
|
|
);
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 更新表单配置
|
|
|
|
|
|
setFormData((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
settingsConfig: configString,
|
|
|
|
|
|
}));
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 同步选择框状态
|
|
|
|
|
|
const hasCoAuthoredDisabled = checkCoAuthoredSetting(configString);
|
|
|
|
|
|
setDisableCoAuthored(hasCoAuthoredDisabled);
|
|
|
|
|
|
};
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 根据当前配置决定是否展示 API Key 输入框
|
|
|
|
|
|
const showApiKey =
|
|
|
|
|
|
selectedPreset !== null || hasApiKeyField(formData.settingsConfig);
|
2025-08-21 22:20:57 +08:00
|
|
|
|
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// 初始时从配置中同步 API Key(编辑模式)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (initialData) {
|
|
|
|
|
|
const parsedKey = getApiKeyFromConfig(
|
2025-08-27 11:00:53 +08:00
|
|
|
|
JSON.stringify(initialData.settingsConfig),
|
2025-08-26 10:37:44 +08:00
|
|
|
|
);
|
|
|
|
|
|
if (parsedKey) setApiKey(parsedKey);
|
2025-08-21 22:20:57 +08:00
|
|
|
|
}
|
2025-08-26 10:37:44 +08:00
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, []);
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-08-26 12:34:47 +08:00
|
|
|
|
// 支持按下 ESC 关闭弹窗
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const onKeyDown = (e: KeyboardEvent) => {
|
2025-08-26 15:12:27 +08:00
|
|
|
|
if (e.key === "Escape") {
|
2025-08-26 12:34:47 +08:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
onClose();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-08-26 15:12:27 +08:00
|
|
|
|
window.addEventListener("keydown", onKeyDown);
|
|
|
|
|
|
return () => window.removeEventListener("keydown", onKeyDown);
|
2025-08-26 12:34:47 +08:00
|
|
|
|
}, [onClose]);
|
|
|
|
|
|
|
2025-08-08 15:03:38 +08:00
|
|
|
|
return (
|
2025-08-26 15:12:27 +08:00
|
|
|
|
<div
|
|
|
|
|
|
className="modal-overlay"
|
|
|
|
|
|
onMouseDown={(e) => {
|
|
|
|
|
|
if (e.target === e.currentTarget) onClose();
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2025-08-08 15:03:38 +08:00
|
|
|
|
<div className="modal-content">
|
2025-08-26 12:34:47 +08:00
|
|
|
|
<div className="modal-titlebar">
|
|
|
|
|
|
<div className="modal-spacer" />
|
2025-08-26 15:12:27 +08:00
|
|
|
|
<div className="modal-title" title={title}>
|
|
|
|
|
|
{title}
|
|
|
|
|
|
</div>
|
2025-08-26 12:34:47 +08:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="modal-close-btn"
|
|
|
|
|
|
aria-label="关闭"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
title="关闭"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="modal-form">
|
|
|
|
|
|
<div className="modal-body">
|
2025-08-26 15:12:27 +08:00
|
|
|
|
{error && <div className="error-message">{error}</div>}
|
|
|
|
|
|
|
|
|
|
|
|
{showPresets && (
|
|
|
|
|
|
<div className="presets">
|
|
|
|
|
|
<label>一键导入(只需要填写 key)</label>
|
|
|
|
|
|
<div className="preset-buttons">
|
|
|
|
|
|
{providerPresets.map((preset, index) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className={`preset-btn ${
|
|
|
|
|
|
selectedPreset === index ? "selected" : ""
|
|
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => applyPreset(preset, index)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{preset.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2025-08-26 12:34:47 +08:00
|
|
|
|
</div>
|
2025-08-26 15:12:27 +08:00
|
|
|
|
)}
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-08-21 22:20:57 +08:00
|
|
|
|
<div className="form-group">
|
2025-08-26 15:12:27 +08:00
|
|
|
|
<label htmlFor="name">供应商名称 *</label>
|
2025-08-21 22:20:57 +08:00
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
2025-08-26 15:12:27 +08:00
|
|
|
|
id="name"
|
|
|
|
|
|
name="name"
|
|
|
|
|
|
value={formData.name}
|
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
|
placeholder="例如:Anthropic 官方"
|
|
|
|
|
|
required
|
2025-08-21 22:20:57 +08:00
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-08-27 11:00:53 +08:00
|
|
|
|
<div
|
|
|
|
|
|
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
|
|
|
|
|
|
>
|
2025-08-27 10:39:39 +08:00
|
|
|
|
<label htmlFor="apiKey">API Key *</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
id="apiKey"
|
|
|
|
|
|
value={apiKey}
|
|
|
|
|
|
onChange={(e) => handleApiKeyChange(e.target.value)}
|
|
|
|
|
|
placeholder="只需要填这里,下方配置会自动填充"
|
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
2025-08-26 15:12:27 +08:00
|
|
|
|
|
|
|
|
|
|
<div className="form-group">
|
|
|
|
|
|
<label htmlFor="websiteUrl">官网地址</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="url"
|
|
|
|
|
|
id="websiteUrl"
|
|
|
|
|
|
name="websiteUrl"
|
|
|
|
|
|
value={formData.websiteUrl}
|
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
|
placeholder="https://example.com(可选)"
|
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
|
/>
|
2025-08-08 15:03:38 +08:00
|
|
|
|
</div>
|
2025-08-26 15:12:27 +08:00
|
|
|
|
|
|
|
|
|
|
<div className="form-group">
|
|
|
|
|
|
<div className="label-with-checkbox">
|
|
|
|
|
|
<label htmlFor="settingsConfig">
|
|
|
|
|
|
Claude Code 配置 (JSON) *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<label className="checkbox-label">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={disableCoAuthored}
|
|
|
|
|
|
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
禁止 Claude Code 签名
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
id="settingsConfig"
|
|
|
|
|
|
name="settingsConfig"
|
|
|
|
|
|
value={formData.settingsConfig}
|
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
|
placeholder={`{
|
2025-08-08 15:03:38 +08:00
|
|
|
|
"env": {
|
|
|
|
|
|
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
|
|
|
|
|
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
|
|
|
|
|
}
|
|
|
|
|
|
}`}
|
2025-08-26 15:12:27 +08:00
|
|
|
|
rows={12}
|
|
|
|
|
|
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<small className="field-hint">
|
|
|
|
|
|
完整的 Claude Code settings.json 配置内容
|
|
|
|
|
|
</small>
|
|
|
|
|
|
</div>
|
2025-08-08 15:03:38 +08:00
|
|
|
|
</div>
|
2025-08-26 12:34:47 +08:00
|
|
|
|
|
|
|
|
|
|
<div className="modal-footer">
|
2025-08-26 15:12:27 +08:00
|
|
|
|
<button type="button" className="cancel-btn" onClick={onClose}>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="submit" className="submit-btn">
|
|
|
|
|
|
{submitText}
|
|
|
|
|
|
</button>
|
2025-08-26 12:34:47 +08:00
|
|
|
|
</div>
|
2025-08-08 15:03:38 +08:00
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-21 22:20:57 +08:00
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-08-08 15:03:38 +08:00
|
|
|
|
|
2025-08-21 22:20:57 +08:00
|
|
|
|
export default ProviderForm;
|