feat: initial release - Hunzi agent framework v0.1.0

This commit is contained in:
2026-07-15 16:37:07 +08:00
parent d510ff2d3f
commit 30aa483090
+446
View File
@@ -0,0 +1,446 @@
/**
* Hunzi 前端应用
*/
const API = '/api';
let currentAgentId = null;
let agentsCache = [];
// ==================== Navigation ====================
function navigateTo(page) {
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
const pageEl = document.getElementById(`page-${page}`);
if (pageEl) pageEl.classList.add('active');
const navEl = document.querySelector(`.nav-item[data-page="${page}"]`);
if (navEl) navEl.classList.add('active');
const titles = {
dashboard: '控制台',
agents: '智能体管理',
tools: '工具管理',
workflows: '工作流',
chat: '对话',
monitor: '系统监控',
};
document.getElementById('pageTitle').textContent = titles[page] || page;
// Load page data
switch (page) {
case 'dashboard': loadDashboard(); break;
case 'agents': loadAgents(); break;
case 'tools': loadTools(); break;
case 'workflows': loadWorkflows(); break;
case 'chat': loadChatAgents(); break;
case 'monitor': loadHealth(); break;
}
}
document.querySelectorAll('.nav-item[data-page]').forEach(btn => {
btn.addEventListener('click', () => navigateTo(btn.dataset.page));
});
// ==================== Dashboard ====================
async function loadDashboard() {
try {
const [agents, tools, workflows, convos] = await Promise.all([
fetchJson(`${API}/agents`),
fetchJson(`${API}/tools`),
fetchJson(`${API}/workflows`),
fetchJson(`${API}/conversations`),
]);
document.getElementById('stat-agents').textContent = agents.length;
document.getElementById('stat-tools').textContent = tools.length;
document.getElementById('stat-workflows').textContent = workflows.length;
document.getElementById('stat-conversations').textContent = convos.length;
} catch (e) {
console.error('Dashboard load error:', e);
}
loadHealth();
}
async function loadHealth() {
try {
const data = await fetchJson(`${API}/health`);
const el = document.getElementById('health-status');
if (el) {
el.innerHTML = `<span class="badge badge-active">● 服务正常</span> 版本: ${data.version} | 名称: ${data.name}`;
}
const monEl = document.getElementById('mon-uptime');
if (monEl) monEl.textContent = '●';
const logEl = document.getElementById('monitor-log');
if (logEl) {
logEl.textContent = JSON.stringify(data, null, 2);
}
} catch (e) {
const el = document.getElementById('health-status');
if (el) el.innerHTML = `<span class="badge badge-failed">● 服务异常</span> ${e.message}`;
}
}
// ==================== Agents ====================
async function loadAgents() {
try {
const agents = await fetchJson(`${API}/agents`);
agentsCache = agents;
const tbody = document.getElementById('agents-table-body');
if (!agents.length) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--text-muted);padding:40px;">
暂无智能体,点击"创建智能体"开始
</td></tr>`;
return;
}
tbody.innerHTML = agents.map(a => `
<tr>
<td><strong>${esc(a.name)}</strong></td>
<td style="color:var(--text-secondary);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(a.description)}</td>
<td><code>${esc(a.llm_provider)}</code></td>
<td>${a.temperature}</td>
<td>${a.tools.length}</td>
<td style="color:var(--text-muted);font-size:12px;">${formatTime(a.created_at)}</td>
<td>
<button class="btn btn-secondary btn-sm" onclick="testAgent('${a.id}','${esc(a.name)}')">💬 对话</button>
<button class="btn btn-danger btn-sm" onclick="deleteAgent('${a.id}')">🗑️</button>
</td>
</tr>
`).join('');
} catch (e) {
console.error('Agents load error:', e);
}
}
function showAgentModal(agent = null) {
const modal = document.getElementById('agent-modal');
modal.style.display = 'flex';
document.getElementById('agent-modal-title').textContent = agent ? '编辑智能体' : '创建智能体';
if (agent) {
document.getElementById('agent-name').value = agent.name;
document.getElementById('agent-description').value = agent.description || '';
document.getElementById('agent-system-prompt').value = agent.system_prompt || '';
document.getElementById('agent-llm-provider').value = agent.llm_provider || 'default';
document.getElementById('agent-temperature').value = agent.temperature;
document.getElementById('agent-max-tokens').value = agent.max_tokens;
document.getElementById('agent-tools').value = (agent.tools || []).join(',');
currentAgentId = agent.id;
} else {
document.getElementById('agent-name').value = '';
document.getElementById('agent-description').value = '';
document.getElementById('agent-system-prompt').value = '你是一个有用的助手。';
document.getElementById('agent-llm-provider').value = 'default';
document.getElementById('agent-temperature').value = '0.7';
document.getElementById('agent-max-tokens').value = '4096';
document.getElementById('agent-tools').value = '';
currentAgentId = null;
}
}
async function saveAgent() {
const name = document.getElementById('agent-name').value.trim();
if (!name) { alert('请输入名称'); return; }
const data = {
name,
description: document.getElementById('agent-description').value.trim(),
system_prompt: document.getElementById('agent-system-prompt').value.trim(),
llm_provider: document.getElementById('agent-llm-provider').value.trim() || 'default',
temperature: parseFloat(document.getElementById('agent-temperature').value) || 0.7,
max_tokens: parseInt(document.getElementById('agent-max-tokens').value) || 4096,
tool_ids: document.getElementById('agent-tools').value.split(',').map(s => s.trim()).filter(Boolean),
};
try {
if (currentAgentId) {
await apiPost(`${API}/agents/${currentAgentId}`, data, 'PUT');
} else {
await apiPost(`${API}/agents`, data);
}
closeModal('agent-modal');
loadAgents();
} catch (e) {
alert('保存失败: ' + e.message);
}
}
async function deleteAgent(id) {
if (!confirm('确定删除这个智能体?')) return;
try {
await fetch(`${API}/agents/${id}`, { method: 'DELETE' });
loadAgents();
} catch (e) {
alert('删除失败: ' + e.message);
}
}
// ==================== Tools ====================
async function loadTools() {
try {
const tools = await fetchJson(`${API}/tools`);
const container = document.getElementById('tools-list');
if (!tools.length) {
container.innerHTML = `<div class="empty-state"><div class="empty-icon">🔧</div><div class="empty-text">暂无工具</div></div>`;
return;
}
container.innerHTML = tools.map(t => `
<div style="padding:12px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:start;">
<div>
<strong>${esc(t.name)}</strong>
<div style="color:var(--text-secondary);font-size:13px;margin-top:4px;">${esc(t.description)}</div>
<div style="font-size:11px;color:var(--text-muted);margin-top:4px;">
参数: ${JSON.stringify(t.parameters || {})}
</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="testTool('${esc(t.name)}')">测试</button>
</div>
`).join('');
} catch (e) {
console.error('Tools load error:', e);
}
}
async function testTool(name) {
const args = prompt(`输入测试参数 (JSON格式, 留空使用默认):`, '{}');
if (args === null) return;
try {
const result = await apiPost(`${API}/tools/${name}/test`, { args: JSON.parse(args) });
alert(`结果:\n${result.success ? result.result : 'Error: ' + result.error}`);
} catch (e) {
alert('测试失败: ' + e.message);
}
}
// ==================== Workflows ====================
async function loadWorkflows() {
try {
const workflows = await fetchJson(`${API}/workflows`);
const tbody = document.getElementById('workflows-table-body');
if (!workflows.length) {
tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:var(--text-muted);padding:40px;">暂无工作流</td></tr>`;
return;
}
tbody.innerHTML = workflows.map(w => `
<tr>
<td><strong>${esc(w.name)}</strong></td>
<td style="color:var(--text-secondary);max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${esc(w.description)}</td>
<td><span class="badge badge-${w.status}">${esc(w.status)}</span></td>
<td>${w.step_count}</td>
<td style="color:var(--text-muted);font-size:12px;">${formatTime(w.created_at)}</td>
<td>
<button class="btn btn-secondary btn-sm" onclick="runWorkflow('${w.id}')">▶ 运行</button>
<button class="btn btn-danger btn-sm" onclick="deleteWorkflow('${w.id}')">🗑️</button>
</td>
</tr>
`).join('');
} catch (e) {
console.error('Workflows load error:', e);
}
}
function showWorkflowModal() {
document.getElementById('wf-name').value = '';
document.getElementById('wf-description').value = '';
document.getElementById('wf-steps').value = '';
document.getElementById('workflow-modal').style.display = 'flex';
}
async function saveWorkflow() {
const name = document.getElementById('wf-name').value.trim();
if (!name) { alert('请输入名称'); return; }
let steps = [];
try {
steps = JSON.parse(document.getElementById('wf-steps').value || '[]');
} catch (e) {
alert('步骤 JSON 格式错误: ' + e.message);
return;
}
try {
await apiPost(`${API}/workflows`, {
name,
description: document.getElementById('wf-description').value.trim(),
steps,
});
closeModal('workflow-modal');
loadWorkflows();
} catch (e) {
alert('创建失败: ' + e.message);
}
}
async function runWorkflow(id) {
if (!confirm('确定运行此工作流?')) return;
try {
const result = await apiPost(`${API}/workflows/${id}/run`, { context: {} });
alert(`工作流已开始运行\nRun ID: ${result.run_id}`);
// Poll for status
pollWorkflowRun(result.run_id);
} catch (e) {
alert('运行失败: ' + e.message);
}
}
async function pollWorkflowRun(runId) {
let status = 'running';
const interval = setInterval(async () => {
try {
const data = await fetchJson(`${API}/workflows/runs/${runId}`);
status = data.status;
if (status !== 'running' && status !== 'pending') {
clearInterval(interval);
const msg = data.steps?.map(s => ` [${s.status}] ${s.step_name}`).join('\n') || '';
alert(`工作流完成 (${status})\n${msg}\n\n${data.error || ''}`);
}
} catch (e) {
clearInterval(interval);
}
}, 2000);
}
async function deleteWorkflow(id) {
if (!confirm('确定删除此工作流?')) return;
try {
await fetch(`${API}/workflows/${id}`, { method: 'DELETE' });
loadWorkflows();
} catch (e) {
alert('删除失败: ' + e.message);
}
}
// ==================== Chat ====================
async function loadChatAgents() {
try {
const agents = await fetchJson(`${API}/agents`);
agentsCache = agents;
const select = document.getElementById('chat-agent-select');
select.innerHTML = agents.map(a => `<option value="${a.id}">${esc(a.name)}</option>`).join('');
if (agents.length) select.value = agents[0].id;
} catch (e) {
console.error('Chat agents load error:', e);
}
}
async function sendMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) return;
const agentId = document.getElementById('chat-agent-select').value;
if (!agentId) {
alert('请先创建并选择智能体');
return;
}
const messagesEl = document.getElementById('chat-messages');
// Remove empty state
const emptyEl = messagesEl.querySelector('.empty-state');
if (emptyEl) emptyEl.remove();
// Add user message
appendMessage('user', message);
input.value = '';
// Add loading indicator
const loadingId = 'loading-' + Date.now();
messagesEl.innerHTML += `
<div class="chat-message assistant" id="${loadingId}">
<div class="chat-avatar">🐟</div>
<div class="chat-bubble"><span class="spinner"></span></div>
</div>
`;
messagesEl.scrollTop = messagesEl.scrollHeight;
try {
const result = await apiPost(`${API}/agents/${agentId}/chat`, { message, images: [] });
// Remove loading
const loadingEl = document.getElementById(loadingId);
if (loadingEl) loadingEl.remove();
appendMessage('assistant', result.response || '(无响应)');
} catch (e) {
const loadingEl = document.getElementById(loadingId);
if (loadingEl) loadingEl.remove();
appendMessage('assistant', `⚠️ 错误: ${e.message}`);
}
}
function testAgent(agentId, agentName) {
navigateTo('chat');
document.getElementById('chat-agent-select').value = agentId;
}
function appendMessage(role, content) {
const messagesEl = document.getElementById('chat-messages');
const avatar = role === 'user' ? '👤' : '🐟';
const div = document.createElement('div');
div.className = `chat-message ${role}`;
div.innerHTML = `
<div class="chat-avatar">${avatar}</div>
<div class="chat-bubble">${esc(content)}</div>
`;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
// ==================== Utils ====================
async function fetchJson(url) {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
async function apiPost(url, data, method = 'POST') {
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${res.status}`);
}
return res.json();
}
function closeModal(id) {
document.getElementById(id).style.display = 'none';
}
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function formatTime(iso) {
if (!iso) return '-';
return new Date(iso.replace(' ', 'T')).toLocaleString('zh-CN');
}
function refreshCurrent() {
const activePage = document.querySelector('.page.active');
if (activePage) {
const pageId = activePage.id.replace('page-', '');
navigateTo(pageId);
}
}
// ==================== Init ====================
document.addEventListener('DOMContentLoaded', () => {
loadDashboard();
});