超级管理后台
- 用户、计费、LLM 与服务号
+ 用户、计费与服务号
diff --git a/ops/src/pages/admin/LlmPage.tsx b/ops/src/pages/admin/LlmPage.tsx
deleted file mode 100644
index 27eac78..0000000
--- a/ops/src/pages/admin/LlmPage.tsx
+++ /dev/null
@@ -1,354 +0,0 @@
-import { useEffect, useState } from 'react';
-import {
- fetchLlmKeys,
- fetchLlmGlobal,
- createLlmKey,
- patchLlmKey,
- deleteLlmKey,
- selectLlmKey,
- testLlmKey,
- putLlmGlobal,
- syncLlmProviders,
- type LlmKey,
-} from '../../api/admin';
-
-export function LlmPage() {
- const [keys, setKeys] = useState
([]);
- const [globalModel, setGlobalModel] = useState('');
- const [globalModelInput, setGlobalModelInput] = useState('');
- const [error, setError] = useState(null);
- const [busy, setBusy] = useState(false);
- const [testResults, setTestResults] = useState>({});
- const [showCreate, setShowCreate] = useState(false);
- const [editKey, setEditKey] = useState(null);
-
- const load = async () => {
- setError(null);
- try {
- const [keysResult, globalResult] = await Promise.all([fetchLlmKeys(), fetchLlmGlobal()]);
- setKeys(keysResult.keys);
- const model = globalResult.settings?.model ?? '';
- setGlobalModel(model);
- setGlobalModelInput(model);
- } catch (err) {
- setError(err instanceof Error ? err.message : '加载失败');
- }
- };
-
- useEffect(() => { void load(); }, []);
-
- const handleSelect = async (keyId: string) => {
- setBusy(true);
- try {
- await selectLlmKey(keyId);
- await load();
- } catch (err) {
- setError(err instanceof Error ? err.message : '操作失败');
- } finally {
- setBusy(false);
- }
- };
-
- const handleDelete = async (key: LlmKey) => {
- if (!window.confirm(`确认删除密钥「${key.name}」?`)) return;
- setBusy(true);
- try {
- await deleteLlmKey(key.id);
- await load();
- } catch (err) {
- setError(err instanceof Error ? err.message : '操作失败');
- } finally {
- setBusy(false);
- }
- };
-
- const handleTest = async (key: LlmKey) => {
- setTestResults((prev) => ({ ...prev, [key.id]: { ok: false, msg: '测试中…' } }));
- try {
- const result = await testLlmKey(key.id);
- setTestResults((prev) => ({
- ...prev,
- [key.id]: { ok: result.ok, msg: result.ok ? `OK (${result.model ?? ''})` : (result.error ?? '失败') },
- }));
- } catch (err) {
- setTestResults((prev) => ({
- ...prev,
- [key.id]: { ok: false, msg: err instanceof Error ? err.message : '连接失败' },
- }));
- }
- };
-
- const handleSaveGlobal = async () => {
- setBusy(true);
- try {
- await putLlmGlobal(globalModelInput.trim());
- setGlobalModel(globalModelInput.trim());
- } catch (err) {
- setError(err instanceof Error ? err.message : '保存失败');
- } finally {
- setBusy(false);
- }
- };
-
- const handleSync = async () => {
- setBusy(true);
- try {
- const result = await syncLlmProviders();
- alert(`同步完成,更新 ${result.synced} 条`);
- await load();
- } catch (err) {
- setError(err instanceof Error ? err.message : '同步失败');
- } finally {
- setBusy(false);
- }
- };
-
- return (
-
- {error ?
{error}
: null}
-
- {/* Global model */}
-
-
全局模型设置
-
- setGlobalModelInput(e.target.value)}
- placeholder="如 deepseek-chat"
- style={{ flex: '1 1 200px', padding: '8px 12px', borderRadius: 999, border: '1px solid #d6d0c3' }}
- />
-
-
- {globalModel ? (
-
当前:{globalModel}
- ) : (
-
未设置(使用选中密钥自带的 model)
- )}
-
-
- {/* Keys list */}
-
-
-
API 密钥({keys.length})
-
-
-
-
-
-
- {keys.length === 0 ? (
-
暂无配置密钥
- ) : (
- keys.map((key) => (
-
-
-
-
{key.name}
- {key.isSelected ? (
-
✓ 当前选中
- ) : null}
-
- {key.provider} {key.model ? `· ${key.model}` : ''}
- {key.models?.length ? ` · ${key.models.join(', ')}` : ''}
-
-
-
- {!key.isSelected ? (
-
- ) : null}
-
-
-
-
-
- {testResults[key.id] ? (
-
- {testResults[key.id].msg}
-
- ) : null}
-
- ))
- )}
-
-
- {showCreate ? (
-
setShowCreate(false)}
- onSuccess={() => { setShowCreate(false); void load(); }}
- />
- ) : null}
-
- {editKey ? (
- setEditKey(null)}
- onSuccess={() => { setEditKey(null); void load(); }}
- />
- ) : null}
-
- );
-}
-
-function CreateKeyModal({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) {
- const [name, setName] = useState('');
- const [provider, setProvider] = useState('openai');
- const [apiKey, setApiKey] = useState('');
- const [model, setModel] = useState('');
- const [baseUrl, setBaseUrl] = useState('');
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState(null);
-
- const handleSubmit = async () => {
- if (!name.trim() || !apiKey.trim()) { setError('名称和 API Key 必填'); return; }
- setBusy(true);
- try {
- await createLlmKey({ name: name.trim(), provider, apiKey, model: model.trim() || undefined, baseUrl: baseUrl.trim() || undefined });
- onSuccess();
- } catch (err) {
- setError(err instanceof Error ? err.message : '创建失败');
- } finally {
- setBusy(false);
- }
- };
-
- return (
-
-
-
- );
-}
-
-function EditKeyModal({ llmKey, onClose, onSuccess }: { llmKey: LlmKey; onClose: () => void; onSuccess: () => void }) {
- const [name, setName] = useState(llmKey.name);
- const [apiKey, setApiKey] = useState('');
- const [model, setModel] = useState(llmKey.model ?? '');
- const [busy, setBusy] = useState(false);
- const [error, setError] = useState(null);
-
- const handleSubmit = async () => {
- setBusy(true);
- try {
- const patch: Record = {};
- if (name !== llmKey.name) patch.name = name.trim();
- if (apiKey) patch.apiKey = apiKey;
- if (model !== (llmKey.model ?? '')) patch.model = model.trim();
- if (Object.keys(patch).length === 0) { onClose(); return; }
- await patchLlmKey(llmKey.id, patch);
- onSuccess();
- } catch (err) {
- setError(err instanceof Error ? err.message : '更新失败');
- } finally {
- setBusy(false);
- }
- };
-
- return (
-
-
-
- );
-}
-
-function Modal({ title, onClose, children }: { title: string; onClose: () => void; children: React.ReactNode }) {
- return (
- e.target === e.currentTarget && onClose()}
- >
-
-
{title}
- {children}
-
-
- );
-}
-
-function LlmField({ label, children }: { label: string; children: React.ReactNode }) {
- return (
-
-
- {children}
-
- );
-}
-
-function ModalActions({ onClose, onSubmit, busy }: { onClose: () => void; onSubmit: () => void; busy: boolean }) {
- return (
-
-
-
-
- );
-}
diff --git a/ops/src/pages/admin/SummaryPage.tsx b/ops/src/pages/admin/SummaryPage.tsx
index c608a7b..6de18a9 100644
--- a/ops/src/pages/admin/SummaryPage.tsx
+++ b/ops/src/pages/admin/SummaryPage.tsx
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
+import { Link } from 'react-router-dom';
import { fetchAdminSummary, type AdminSummary } from '../../api/admin';
function yuan(cents: number) {
@@ -50,6 +51,28 @@ export function SummaryPage() {
LLM 服务未启用
)}
+
+