const { useState, useEffect, useMemo, useRef } = React; // 简单 Markdown 渲染组件,优先使用 window.marked function Markdown({ content }) { const html = useMemo(() => { if (!content) return ""; if (window.marked && window.marked.parse) { return window.marked.parse(content, { breaks: true }); } // 兜底:仅做换行处理,避免完全不可读 return String(content).replace(/\n/g, "
"); }, [content]); return
; } // 将字符串尽量按 JSON 美化后用于展示(不会修改原数据) function formatJsonForDisplay(value) { if (value == null) return ""; if (typeof value !== "string") { try { return JSON.stringify(value, null, 2); } catch { return String(value); } } const trimmed = value.trim(); if (!trimmed) return ""; try { // 尝试把字符串当 JSON 解析,再格式化 const parsed = JSON.parse(trimmed); return JSON.stringify(parsed, null, 2); } catch { // 不是合法 JSON,则原样返回(保持和原始内容一致) return value; } } // 专门用于 Response JSON 的展示: // - 如果是 { streaming: [...] },只展开 streaming 数组,每项一行 // - 如果是纯数组,也按「每项一行」展示 // - 其它情况回退到通用 JSON 美化 function formatResponseJsonForDisplay(value) { if (value == null) return ""; const raw = typeof value === "string" ? value : (() => { try { return JSON.stringify(value); } catch { return String(value); } })(); try { const parsed = JSON.parse(raw); // 兼容 { streaming: [...] } 结构 if (parsed && Array.isArray(parsed.streaming)) { return parsed.streaming.map((item) => JSON.stringify(item)).join("\n"); } // 兼容直接存数组(当前后端实现) if (Array.isArray(parsed)) { return parsed.map((item) => JSON.stringify(item)).join("\n"); } // 其它情况:正常美化整个 JSON return formatJsonForDisplay(raw); } catch { // 不是合法 JSON,则原样返回 return raw; } } const TOKEN_KEY = "agents_console_token"; const VARIABLES_STORAGE_KEY = "agents_console_variables"; // ===== Variables helpers ===== function createEmptyVariable() { return { id: crypto.randomUUID(), name: "", type: "short", // 'short' | 'long' | 'rule' ruleType: "current_time", // for type === 'rule' value: "", }; } // 计算单个变量当前值(静态或规则) function computeVariableValue(v) { if (!v) return ""; if (v.type === "rule") { const now = new Date(); if (v.ruleType === "current_time") { return now.toLocaleString(); } if (v.ruleType === "hour_to_season") { const h = now.getHours(); // 0-23 // 0-6: 春, 7-12: 夏, 13-16: 秋, 17-23: 冬 if (h >= 0 && h <= 6) return "春"; if (h >= 7 && h <= 12) return "夏"; if (h >= 13 && h <= 16) return "秋"; return "冬"; } return ""; } return v.value || ""; } // 把文本中的 @变量名 替换为变量值(仅用于预览/发送) function resolveVariablesInText(text, variables) { if (!text || !variables || variables.length === 0) return text; const map = {}; for (const v of variables) { if (!v.name) continue; map[v.name] = computeVariableValue(v); } // 支持中文等 Unicode 字符的变量名 const re = /@([\p{L}\p{N}_]+)/gu; return String(text).replace(re, (match, name) => { if (Object.prototype.hasOwnProperty.call(map, name)) { return map[name]; } return match; }); } // ===== Types (JS 版本,仅做注释参考) ===== // OpenAIConfig: { baseUrl, apiKey, organization?, project? } // Agent: { id, name, description, model, temperature, topP, maxTokens, presencePenalty, frequencyPenalty, seed?, stop? } // Conversation: { id, agentId, title?, messages: Array } // Message: { id, role, content, createdAt? } // 将后端 AgentOut + config 转成前端 Agent 对象 function normalizeAgent(agent) { const cfg = agent.config || {}; return { id: String(agent.id), name: agent.name || "", description: agent.description || "", model: agent.model || "", temperature: cfg.temperature ?? 1, topP: cfg.top_p ?? 1, maxTokens: cfg.max_tokens ?? undefined, presencePenalty: cfg.presence_penalty ?? 0, frequencyPenalty: cfg.frequency_penalty ?? 0, seed: cfg.extra?.seed ?? undefined, stop: cfg.extra?.stop ?? "", note: cfg.extra?.note ?? "", }; } // 将后端 ConversationOut 转成前端使用的结构 function normalizeConversation(conv) { return { id: conv.id, agentId: String(conv.agent_id), title: conv.title || "", messages: (conv.messages || []).map((m) => ({ id: m.id, role: m.role, content: m.content, createdAt: null, requestJson: m.request_json || null, responseJson: m.response_json || null, isError: m.is_error || false, })), }; } // 将前端 Agent 转成后端需要的 config 结构 function buildAgentConfig(agent) { return { temperature: agent.temperature, top_p: agent.topP, max_tokens: agent.maxTokens ?? null, presence_penalty: agent.presencePenalty, frequency_penalty: agent.frequencyPenalty, extra: { seed: agent.seed ?? null, stop: agent.stop || "", note: agent.note || "", }, }; } // ===== Login ===== function Login({ onLogin }) { const [secret, setSecret] = useState(""); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); const trimmed = secret.trim(); if (!trimmed) { setError("请输入密钥"); return; } setLoading(true); setError(null); try { const res = await fetch("/api/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ secret: trimmed }), }); const data = await res.json().catch(() => ({})); if (!res.ok || !data.token) { setError(data.detail || data.error || "登录失败,请检查密钥"); return; } const token = data.token; localStorage.setItem(TOKEN_KEY, token); onLogin(token); } catch (err) { setError(err?.message || "登录请求失败"); } finally { setLoading(false); } }; return (

登录 🤖 Agents 控制台

请输入本地配置文件中的密钥,以访问本地 API。

{error && {error}}
); } // ===== AgentsPanel ===== function createEmptyAgent(modelOptions = []) { const defaultModel = Array.isArray(modelOptions) && modelOptions.length > 0 ? modelOptions[0] : ""; return { id: crypto.randomUUID(), name: "", description: "", model: defaultModel, temperature: 0.7, topP: 1, maxTokens: 2048, presencePenalty: 0, frequencyPenalty: 0, seed: undefined, stop: "", note: "", }; } function AgentsPanel({ agents, onChange, onSelectAgent, modelOptions = [], authToken, variables = [], }) { const [editingId, setEditingId] = useState(null); const editingAgent = useMemo( () => agents.find((a) => a.id === editingId) || null, [agents, editingId], ); const handleCreate = () => { const next = createEmptyAgent(modelOptions); if (!next.model?.trim()) { alert( "请选择或输入模型。若下拉列表为空,请先在设置中配置 OpenAI 并获取模型列表。", ); return; } const optimisticList = [...agents, next]; onChange(optimisticList); setEditingId(next.id); fetch("/api/agents", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ name: next.name, model: next.model, description: next.description, config: buildAgentConfig(next), }), }) .then(async (res) => { if (!res.ok) return null; const saved = await res.json().catch(() => null); if (!saved || saved.id == null) return null; return saved; }) .then((saved) => { if (!saved) return; const normalized = normalizeAgent(saved); const list = optimisticList.map((a) => a.id === next.id ? normalized : a, ); onChange(list); setEditingId(normalized.id); }) .catch(() => {}); }; const handleUpdate = (patch) => { if (!editingAgent) return; const updated = { ...editingAgent, ...patch }; const list = agents.map((a) => (a.id === updated.id ? updated : a)); onChange(list); const numericId = Number(updated.id); fetch(`/api/agents/${numericId}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ name: updated.name, model: updated.model, description: updated.description, config: buildAgentConfig(updated), }), }).catch(() => {}); }; const handleDelete = (id) => { const list = agents.filter((a) => a.id !== id); onChange(list); if (editingId === id) { setEditingId(null); } const numericId = Number(id); if (!Number.isFinite(numericId)) return; fetch(`/api/agents/${numericId}`, { method: "DELETE", headers: { Authorization: `Bearer ${authToken}`, }, }).catch(() => {}); }; const renderVariableHints = (onInsert) => { if (!variables || variables.length === 0) { return (
还没有变量,可以在左侧「变量」标签页中创建,然后通过 @变量名 引用。
); } return (
可用变量: {variables.map((v) => ( ))}
); }; const renderPreview = (value) => { if (!value) return null; const resolved = resolveVariablesInText(value, variables); if (resolved === value) return null; return (
预览(变量已展开):
{resolved}
); }; return (

Agents 管理

为不同用途创建多个带有独立参数的 Agent。

{agents.length === 0 && (

还没有 Agent,点击「新建 Agent」开始。

)} {agents.map((agent) => ( ))}
{!editingAgent ? (

选择或创建一个 Agent

在左侧列表中选择已有 Agent,或点击「新建 Agent」创建新的配置。

) : ( <>