mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-18 08:27:28 +08:00
config/webui: improve providers.proxies editing and support proxies as map or array in config loader
This commit is contained in:
@@ -198,6 +198,49 @@ type ProvidersConfig struct {
|
|||||||
Proxies map[string]ProviderConfig `json:"proxies"`
|
Proxies map[string]ProviderConfig `json:"proxies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type providerProxyItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ProviderConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProvidersConfig) UnmarshalJSON(data []byte) error {
|
||||||
|
var tmp struct {
|
||||||
|
Proxy ProviderConfig `json:"proxy"`
|
||||||
|
Proxies json.RawMessage `json:"proxies"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &tmp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Proxy = tmp.Proxy
|
||||||
|
p.Proxies = map[string]ProviderConfig{}
|
||||||
|
if len(bytes.TrimSpace(tmp.Proxies)) == 0 || string(bytes.TrimSpace(tmp.Proxies)) == "null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Preferred format: object map
|
||||||
|
var asMap map[string]ProviderConfig
|
||||||
|
if err := json.Unmarshal(tmp.Proxies, &asMap); err == nil {
|
||||||
|
for k, v := range asMap {
|
||||||
|
if k == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.Proxies[k] = v
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Compatibility format: array [{name, ...provider fields...}]
|
||||||
|
var asArr []providerProxyItem
|
||||||
|
if err := json.Unmarshal(tmp.Proxies, &asArr); err == nil {
|
||||||
|
for _, it := range asArr {
|
||||||
|
if it.Name == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p.Proxies[it.Name] = it.ProviderConfig
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("providers.proxies must be object map or array of {name,...}")
|
||||||
|
}
|
||||||
|
|
||||||
type ProviderConfig struct {
|
type ProviderConfig struct {
|
||||||
APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"`
|
APIKey string `json:"api_key" env:"CLAWGO_PROVIDERS_{{.Name}}_API_KEY"`
|
||||||
APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"`
|
APIBase string `json:"api_base" env:"CLAWGO_PROVIDERS_{{.Name}}_API_BASE"`
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const Config: React.FC = () => {
|
|||||||
const [basicMode, setBasicMode] = useState(true);
|
const [basicMode, setBasicMode] = useState(true);
|
||||||
const [hotOnly, setHotOnly] = useState(false);
|
const [hotOnly, setHotOnly] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
|
const [newProxyName, setNewProxyName] = useState('');
|
||||||
|
|
||||||
const hotPrefixes = useMemo(() => hotReloadFieldDetails.map((x) => String(x.path || '').replace(/\.\*$/, '')).filter(Boolean), [hotReloadFieldDetails]);
|
const hotPrefixes = useMemo(() => hotReloadFieldDetails.map((x) => String(x.path || '').replace(/\.\*$/, '')).filter(Boolean), [hotReloadFieldDetails]);
|
||||||
|
|
||||||
@@ -84,6 +85,45 @@ const Config: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, [cfg, baseline]);
|
}, [cfg, baseline]);
|
||||||
|
|
||||||
|
function updateProxyField(name: string, field: string, value: any) {
|
||||||
|
setCfg((v) => setPath(v, `providers.proxies.${name}.${field}`, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeProxy(name: string) {
|
||||||
|
setCfg((v) => {
|
||||||
|
const next = JSON.parse(JSON.stringify(v || {}));
|
||||||
|
if (next?.providers?.proxies && typeof next.providers.proxies === 'object') {
|
||||||
|
delete next.providers.proxies[name];
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addProxy() {
|
||||||
|
const name = newProxyName.trim();
|
||||||
|
if (!name) return;
|
||||||
|
setCfg((v) => {
|
||||||
|
const next = JSON.parse(JSON.stringify(v || {}));
|
||||||
|
if (!next.providers || typeof next.providers !== 'object') next.providers = {};
|
||||||
|
if (!next.providers.proxies || typeof next.providers.proxies !== 'object' || Array.isArray(next.providers.proxies)) {
|
||||||
|
next.providers.proxies = {};
|
||||||
|
}
|
||||||
|
if (!next.providers.proxies[name]) {
|
||||||
|
next.providers.proxies[name] = {
|
||||||
|
api_key: '',
|
||||||
|
api_base: '',
|
||||||
|
protocol: 'responses',
|
||||||
|
models: [],
|
||||||
|
supports_responses_compact: false,
|
||||||
|
auth: 'bearer',
|
||||||
|
timeout_sec: 120,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setNewProxyName('');
|
||||||
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveConfig() {
|
||||||
try {
|
try {
|
||||||
const payload = showRaw ? JSON.parse(cfgRaw) : cfg;
|
const payload = showRaw ? JSON.parse(cfgRaw) : cfg;
|
||||||
@@ -158,7 +198,33 @@ const Config: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="flex-1 p-4 md:p-6 overflow-y-auto">
|
<div className="flex-1 p-4 md:p-6 overflow-y-auto space-y-4">
|
||||||
|
{activeTop === 'providers' && !showRaw && (
|
||||||
|
<div className="rounded-xl border border-zinc-800 bg-zinc-950/40 p-3 space-y-3">
|
||||||
|
<div className="flex items-center justify-between gap-2 flex-wrap">
|
||||||
|
<div className="text-sm font-semibold text-zinc-200">Proxies</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input value={newProxyName} onChange={(e)=>setNewProxyName(e.target.value)} placeholder="new provider name" className="px-2 py-1 rounded bg-zinc-900 border border-zinc-700 text-xs" />
|
||||||
|
<button onClick={addProxy} className="px-2 py-1 rounded bg-indigo-600 hover:bg-indigo-500 text-xs">Add</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.entries(((cfg as any)?.providers?.proxies || {}) as Record<string, any>).map(([name, p]) => (
|
||||||
|
<div key={name} className="grid grid-cols-1 md:grid-cols-8 gap-2 rounded-lg border border-zinc-800 bg-zinc-900/40 p-2 text-xs">
|
||||||
|
<div className="md:col-span-1 font-mono text-zinc-300 flex items-center">{name}</div>
|
||||||
|
<input value={String(p?.api_base || '')} onChange={(e)=>updateProxyField(name, 'api_base', e.target.value)} placeholder="api_base" className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
|
||||||
|
<input value={String(p?.api_key || '')} onChange={(e)=>updateProxyField(name, 'api_key', e.target.value)} placeholder="api_key" className="md:col-span-2 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
|
||||||
|
<input value={String(p?.protocol || '')} onChange={(e)=>updateProxyField(name, 'protocol', e.target.value)} placeholder="protocol" className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
|
||||||
|
<input value={Array.isArray(p?.models) ? p.models.join(',') : ''} onChange={(e)=>updateProxyField(name, 'models', e.target.value.split(',').map(s=>s.trim()).filter(Boolean))} placeholder="models,a,b" className="md:col-span-1 px-2 py-1 rounded bg-zinc-950 border border-zinc-800" />
|
||||||
|
<button onClick={()=>removeProxy(name)} className="md:col-span-1 px-2 py-1 rounded bg-red-900/60 hover:bg-red-800 text-red-100">Delete</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{Object.keys(((cfg as any)?.providers?.proxies || {}) as Record<string, any>).length === 0 && (
|
||||||
|
<div className="text-xs text-zinc-500">No custom providers yet.</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{activeTop ? (
|
{activeTop ? (
|
||||||
<RecursiveConfig
|
<RecursiveConfig
|
||||||
data={(cfg as any)?.[activeTop] || {}}
|
data={(cfg as any)?.[activeTop] || {}}
|
||||||
|
|||||||
Reference in New Issue
Block a user