feat: initial release - Hunzi agent framework v0.1.0
This commit is contained in:
+446
@@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user