chore: checkpoint admin restore state

This commit is contained in:
john
2026-06-30 20:26:33 +08:00
parent f2172322fd
commit f9a7584ad4
63 changed files with 7555 additions and 1030 deletions
-535
View File
@@ -1,535 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
createLlmProviderKey,
deleteLlmProviderKey,
getLlmGlobalSettings,
listLlmProviderCatalog,
listLlmProviderKeys,
selectLlmProviderKey,
setLlmGlobalModel,
syncLlmProviderToGoosed,
testLlmProviderDraft,
testLlmProviderKey,
updateLlmProviderKey,
} from '../api/client';
import type {
LlmConnectionTestResult,
LlmGlobalSettings,
LlmProviderDefinition,
LlmProviderKeyRow,
} from '../types';
const CUSTOM_PROVIDER_ID = '__custom__';
const emptyForm = {
providerId: CUSTOM_PROVIDER_ID,
name: 'Relay Buyer Ollama',
apiKey: 'UqyHPKSSEZq0-oPnl8sru-7hZcJ2anPUL1yAVk866Vo',
apiUrl: 'http://127.0.0.1:18300/relay/buyer/v1/chat/completions',
basePath: '',
modelsText: 'qwen2.5:3b',
defaultModel: 'qwen2.5:3b',
relayProvider: 'ollama',
engine: 'openai',
};
function parseModelsText(text: string) {
return [...new Set(text.split(/[\n,]/).map((item) => item.trim()).filter(Boolean))];
}
function TestResultBanner({ result }: { result: LlmConnectionTestResult | null }) {
if (!result) return null;
if (!result.ok) {
return <p className="banner banner-error">{result.message ?? '未知错误'}</p>;
}
return (
<p className="banner banner-info">
{result.latencyMs}ms {result.model}{result.reply}
</p>
);
}
export function ProviderKeySettings() {
const [catalog, setCatalog] = useState<LlmProviderDefinition[]>([]);
const [keys, setKeys] = useState<LlmProviderKeyRow[]>([]);
const [globalSettings, setGlobalSettings] = useState<LlmGlobalSettings | null>(null);
const [globalModelDraft, setGlobalModelDraft] = useState('');
const [form, setForm] = useState(emptyForm);
const [loading, setLoading] = useState(true);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [testingForm, setTestingForm] = useState(false);
const [testingGlobal, setTestingGlobal] = useState(false);
const [testingKeyId, setTestingKeyId] = useState<string | null>(null);
const [formTestResult, setFormTestResult] = useState<LlmConnectionTestResult | null>(null);
const [globalTestResult, setGlobalTestResult] = useState<LlmConnectionTestResult | null>(null);
const isCustom = form.providerId === CUSTOM_PROVIDER_ID;
const selectedCatalog = useMemo(
() => catalog.find((item) => item.id === form.providerId) ?? null,
[catalog, form.providerId],
);
const customModels = useMemo(() => parseModelsText(form.modelsText), [form.modelsText]);
const load = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [catalogItems, keyRows, globalState] = await Promise.all([
listLlmProviderCatalog(),
listLlmProviderKeys(),
getLlmGlobalSettings(),
]);
setCatalog(catalogItems);
setKeys(keyRows);
setGlobalSettings(globalState);
setGlobalModelDraft(globalState.globalModel ?? '');
} catch (err) {
setError(err instanceof Error ? err.message : '加载 LLM 配置失败');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
useEffect(() => {
if (isCustom) {
setForm((current) => ({
...current,
defaultModel: current.defaultModel || customModels[0] || '',
}));
return;
}
if (!selectedCatalog) return;
setForm((current) => ({
...current,
defaultModel: current.defaultModel || selectedCatalog.defaultModel,
}));
}, [customModels, isCustom, selectedCatalog]);
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
setError(null);
setFormTestResult(null);
try {
if (isCustom) {
await createLlmProviderKey({
providerId: CUSTOM_PROVIDER_ID,
name: form.name,
apiKey: form.apiKey,
apiUrl: form.apiUrl,
basePath: form.basePath || undefined,
models: customModels,
defaultModel: form.defaultModel || customModels[0],
engine: form.engine,
relayProvider: form.relayProvider || undefined,
});
} else {
await createLlmProviderKey({
providerId: form.providerId,
name: form.name,
apiKey: form.apiKey,
defaultModel: form.defaultModel || undefined,
});
}
setForm({ ...emptyForm, providerId: form.providerId });
setMessage('LLM 配置已保存到数据库');
await load();
} catch (err) {
setError(err instanceof Error ? err.message : '保存失败');
}
};
const handleTestForm = async () => {
setTestingForm(true);
setFormTestResult(null);
setError(null);
try {
const result = await testLlmProviderDraft({
providerId: CUSTOM_PROVIDER_ID,
name: form.name || 'test',
apiKey: form.apiKey,
apiUrl: form.apiUrl,
basePath: form.basePath || undefined,
models: customModels,
defaultModel: form.defaultModel || customModels[0],
testModel: form.defaultModel || customModels[0],
relayProvider: form.relayProvider || undefined,
});
setFormTestResult(result);
} catch (err) {
setFormTestResult({
ok: false,
message: err instanceof Error ? err.message : '联通测试失败',
});
} finally {
setTestingForm(false);
}
};
const handleApplyGlobalModel = async () => {
setMessage(null);
setError(null);
setGlobalTestResult(null);
try {
const result = await setLlmGlobalModel(globalModelDraft);
setGlobalSettings(result.global);
setMessage(`全局模型已设为 ${result.global.globalModel},并已同步到 TKMind Agent`);
await load();
} catch (err) {
setError(err instanceof Error ? err.message : '保存全局模型失败');
}
};
const handleTestGlobal = async () => {
if (!globalSettings?.keyId) {
setError('请先启用一个 LLM 配置');
return;
}
setTestingGlobal(true);
setGlobalTestResult(null);
setError(null);
try {
const result = await testLlmProviderKey(
globalSettings.keyId,
globalModelDraft || globalSettings.globalModel || undefined,
);
setGlobalTestResult(result);
} catch (err) {
setGlobalTestResult({
ok: false,
message: err instanceof Error ? err.message : '联通测试失败',
});
} finally {
setTestingGlobal(false);
}
};
const handleSelect = async (keyId: string) => {
setMessage(null);
setError(null);
try {
await selectLlmProviderKey(keyId);
setMessage('已切换当前 LLM 并同步到 TKMind Agent');
await load();
} catch (err) {
setError(err instanceof Error ? err.message : '切换失败');
}
};
const handleTestKey = async (row: LlmProviderKeyRow) => {
setTestingKeyId(row.id);
setError(null);
try {
const result = await testLlmProviderKey(row.id, row.defaultModel);
if (result.ok) {
setMessage(`${row.name}」联通成功(${result.latencyMs}ms):${result.reply}`);
} else {
setError(`${row.name}」联通失败:${result.message}`);
}
} catch (err) {
setError(err instanceof Error ? err.message : '联通测试失败');
} finally {
setTestingKeyId(null);
}
};
const handleToggleStatus = async (row: LlmProviderKeyRow) => {
setMessage(null);
setError(null);
try {
await updateLlmProviderKey(row.id, {
status: row.status === 'active' ? 'disabled' : 'active',
});
setMessage('状态已更新');
await load();
} catch (err) {
setError(err instanceof Error ? err.message : '更新失败');
}
};
const handleDelete = async (row: LlmProviderKeyRow) => {
if (!window.confirm(`确定删除配置「${row.name}」?`)) return;
setMessage(null);
setError(null);
try {
await deleteLlmProviderKey(row.id);
setMessage('配置已删除');
await load();
} catch (err) {
setError(err instanceof Error ? err.message : '删除失败');
}
};
const handleSync = async () => {
setMessage(null);
setError(null);
try {
const result = await syncLlmProviderToGoosed();
setMessage(result.synced ? '已重新同步到 TKMind Agent' : '当前没有启用的 LLM 配置');
} catch (err) {
setError(err instanceof Error ? err.message : '同步失败');
}
};
return (
<section className="admin-card">
<div className="admin-card-head">
<h2>LLM Key </h2>
<button type="button" className="ghost-btn" onClick={() => void load()}>
</button>
</div>
<p className="muted">
Relay Buyer Ollama
GOOSE_MODEL / TKMIND_MODEL使
</p>
<div className="admin-card global-model-card">
<div className="admin-card-head">
<h3></h3>
</div>
{loading ? (
<p className="muted"></p>
) : !globalSettings?.keyId ? (
<p className="muted"> LLM Relay </p>
) : (
<>
<p className="muted">
Provider<strong>{globalSettings.providerLabel}</strong>
{globalSettings.keyName}
</p>
<div className="admin-form global-model-form">
<select
value={globalModelDraft}
onChange={(e) => setGlobalModelDraft(e.target.value)}
>
{globalSettings.availableModels.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
<button type="button" className="send-btn" onClick={() => void handleApplyGlobalModel()}>
</button>
<button
type="button"
className="ghost-btn"
disabled={testingGlobal}
onClick={() => void handleTestGlobal()}
>
{testingGlobal ? '测试中…' : '联通测试'}
</button>
</div>
<TestResultBanner result={globalTestResult} />
</>
)}
</div>
<form className="admin-form" onSubmit={handleCreate}>
<select
value={form.providerId}
onChange={(e) =>
setForm((state) => ({
...state,
providerId: e.target.value,
defaultModel:
e.target.value === CUSTOM_PROVIDER_ID
? parseModelsText(state.modelsText)[0] ?? ''
: (catalog.find((item) => item.id === e.target.value)?.defaultModel ?? ''),
}))
}
>
{catalog.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
<input
placeholder="配置名称"
value={form.name}
onChange={(e) => setForm((state) => ({ ...state, name: e.target.value }))}
/>
{isCustom ? (
<>
<input
placeholder="API 地址"
value={form.apiUrl}
onChange={(e) => setForm((state) => ({ ...state, apiUrl: e.target.value }))}
/>
<textarea
placeholder="模型列表,每行一个"
value={form.modelsText}
rows={4}
onChange={(e) =>
setForm((state) => ({
...state,
modelsText: e.target.value,
defaultModel: parseModelsText(e.target.value)[0] ?? state.defaultModel,
}))
}
/>
<input
placeholder="Relay Provider(请求 body 中的 provider,如 ollama"
value={form.relayProvider}
onChange={(e) => setForm((state) => ({ ...state, relayProvider: e.target.value }))}
/>
</>
) : null}
<input
placeholder="API Key / Bearer Token"
type="password"
value={form.apiKey}
onChange={(e) => setForm((state) => ({ ...state, apiKey: e.target.value }))}
/>
{isCustom ? (
<select
value={form.defaultModel || customModels[0] || ''}
onChange={(e) => setForm((state) => ({ ...state, defaultModel: e.target.value }))}
>
{customModels.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
) : (
<select
value={form.defaultModel || selectedCatalog?.defaultModel || ''}
onChange={(e) => setForm((state) => ({ ...state, defaultModel: e.target.value }))}
>
{(selectedCatalog?.models ?? []).map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
)}
<button type="submit" className="send-btn">
</button>
{isCustom && (
<button
type="button"
className="ghost-btn"
disabled={testingForm}
onClick={() => void handleTestForm()}
>
{testingForm ? '测试中…' : '联通测试'}
</button>
)}
</form>
<TestResultBanner result={formTestResult} />
{message && <p className="banner banner-info">{message}</p>}
{error && <p className="banner banner-error">{error}</p>}
<div className="admin-card-head">
<h3></h3>
<button type="button" className="ghost-btn" onClick={() => void handleSync()}>
Agent
</button>
</div>
{loading ? (
<p className="muted"></p>
) : keys.length === 0 ? (
<p className="muted"> Relay Buyer Ollama</p>
) : (
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr>
<th></th>
<th>Endpoint</th>
<th></th>
<th>Key</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{keys.map((row) => (
<tr key={row.id}>
<td>
{row.name}
{row.isSelected && <span className="risk-pill risk-low"></span>}
</td>
<td className="mono">
{row.providerKind === 'custom' ? (
<>
<div>{row.apiUrl}</div>
{row.relayProvider && (
<div className="muted">provider: {row.relayProvider}</div>
)}
</>
) : (
row.providerLabel
)}
</td>
<td className="mono">
<div>{row.defaultModel}</div>
{row.models.length > 1 && (
<div className="muted">+{row.models.length - 1} </div>
)}
</td>
<td className="mono">{row.apiKeyMasked}</td>
<td>{row.status === 'active' ? '可用' : '禁用'}</td>
<td className="admin-actions">
{row.providerKind === 'custom' && (
<button
type="button"
className="ghost-btn"
disabled={testingKeyId === row.id}
onClick={() => void handleTestKey(row)}
>
{testingKeyId === row.id ? '测试中…' : '测试'}
</button>
)}
{!row.isSelected && row.status === 'active' && (
<button
type="button"
className="ghost-btn"
onClick={() => void handleSelect(row.id)}
>
</button>
)}
<button
type="button"
className="ghost-btn"
disabled={row.isSelected}
onClick={() => void handleToggleStatus(row)}
>
{row.status === 'active' ? '禁用' : '恢复'}
</button>
<button
type="button"
className="ghost-btn"
disabled={row.isSelected}
onClick={() => void handleDelete(row)}
>
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
);
}
+1 -1
View File
@@ -173,7 +173,7 @@ export function SkillSettings({
{loading ? (
<p className="muted"></p>
) : catalog.length === 0 ? (
<p className="muted"> ui/h5/skills/ SKILL.md</p>
<p className="muted"> Memind/skills/ SKILL.md</p>
) : (
<>
{!userOnly && (