mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-02-27 08:00:36 +08:00
feat: web console module display
This commit is contained in:
@@ -54,7 +54,7 @@
|
||||
transform -translate-x-full lg:relative lg:translate-x-0
|
||||
transition-transform duration-300 ease-in-out">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center gap-3 px-5 h-16 border-b border-white/10 flex-shrink-0">
|
||||
<div class="flex items-center gap-3 px-5 h-14 border-b border-white/10 flex-shrink-0">
|
||||
<img src="assets/logo.jpg" alt="CowAgent" class="w-8 h-8 rounded-lg flex-shrink-0">
|
||||
<div class="flex flex-col min-w-0">
|
||||
<span class="text-white font-semibold text-sm truncate">CowAgent</span>
|
||||
@@ -134,7 +134,10 @@
|
||||
<div class="px-4 py-3 border-t border-white/10 flex-shrink-0">
|
||||
<div class="flex items-center gap-2 text-xs text-neutral-600">
|
||||
<i class="fas fa-circle text-[6px] text-primary-400"></i>
|
||||
<span>CowAgent v1.0</span>
|
||||
<a id="sidebar-version"
|
||||
href="https://github.com/zhayujie/chatgpt-on-wechat/releases"
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
class="hover:text-primary-400 transition-colors duration-150 cursor-pointer"></a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -202,7 +205,7 @@
|
||||
<img src="assets/logo.jpg" alt="CowAgent" class="w-16 h-16 rounded-2xl mb-6 shadow-lg shadow-primary-500/20">
|
||||
<h1 id="welcome-title" class="text-2xl font-bold text-slate-800 dark:text-slate-100 mb-3">CowAgent</h1>
|
||||
<p id="welcome-subtitle" class="text-slate-500 dark:text-slate-400 text-center max-w-lg mb-10 leading-relaxed"
|
||||
data-i18n="welcome_subtitle">I can help you answer questions, manage your computer, create and execute skills, and keep growing through long-term memory.</p>
|
||||
data-i18n-html="welcome_subtitle">I can help you answer questions, manage your computer, create and execute skills,<br>and keep growing through long-term memory.</p>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 w-full max-w-2xl">
|
||||
<div class="example-card group bg-white dark:bg-[#1A1A1A] border border-slate-200 dark:border-white/10 rounded-xl p-4
|
||||
@@ -295,10 +298,6 @@
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 w-32 flex-shrink-0">Model</span>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 flex-1 font-mono" id="cfg-model">--</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 p-3 rounded-lg bg-slate-50 dark:bg-white/5">
|
||||
<span class="text-sm font-medium text-slate-500 dark:text-slate-400 w-32 flex-shrink-0">API Base</span>
|
||||
<span class="text-sm text-slate-700 dark:text-slate-200 flex-1 font-mono truncate" id="cfg-api-base">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Agent Config Card -->
|
||||
@@ -383,34 +382,60 @@
|
||||
<div id="view-memory" class="view">
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="memory_title">Memory</h2>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="memory_desc">View agent memory files and contents</p>
|
||||
|
||||
<!-- Panel: list -->
|
||||
<div id="memory-panel-list">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-slate-800 dark:text-slate-100" data-i18n="memory_title">Memory</h2>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="memory_desc">View agent memory files and contents</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="memory-empty" class="flex flex-col items-center justify-center py-20">
|
||||
<div class="w-16 h-16 rounded-2xl bg-purple-50 dark:bg-purple-900/20 flex items-center justify-center mb-4">
|
||||
<i class="fas fa-brain text-purple-400 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="memory_loading">Loading memory files...</p>
|
||||
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="memory_loading_desc">Memory files will be displayed here</p>
|
||||
</div>
|
||||
<div id="memory-list" class="hidden">
|
||||
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-200 dark:border-white/10">
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_name">Filename</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_type">Type</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_size">Size</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_updated">Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="memory-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="memory-pagination" class="flex items-center justify-between mt-4 text-sm text-slate-500 dark:text-slate-400"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="memory-empty" class="flex flex-col items-center justify-center py-20">
|
||||
<div class="w-16 h-16 rounded-2xl bg-purple-50 dark:bg-purple-900/20 flex items-center justify-center mb-4">
|
||||
<i class="fas fa-brain text-purple-400 text-xl"></i>
|
||||
|
||||
<!-- Panel: file viewer (replaces list) -->
|
||||
<div id="memory-panel-viewer" class="hidden">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<button onclick="closeMemoryViewer()"
|
||||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm
|
||||
text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-white/10
|
||||
border border-slate-200 dark:border-white/10 transition-colors cursor-pointer">
|
||||
<i class="fas fa-arrow-left text-xs"></i>
|
||||
<span data-i18n="memory_back">Back</span>
|
||||
</button>
|
||||
<h2 id="memory-viewer-title"
|
||||
class="text-base font-semibold text-slate-800 dark:text-slate-100 font-mono truncate"></h2>
|
||||
</div>
|
||||
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="memory_loading">Loading memory files...</p>
|
||||
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="memory_loading_desc">Memory files will be displayed here</p>
|
||||
</div>
|
||||
<div id="memory-list" class="hidden">
|
||||
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 overflow-hidden">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-slate-200 dark:border-white/10">
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_name">Filename</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_type">Type</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_size">Size</th>
|
||||
<th class="text-left px-4 py-3 text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400" data-i18n="memory_col_updated">Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="memory-table-body"></tbody>
|
||||
</table>
|
||||
<div id="memory-viewer-content"
|
||||
class="p-5 overflow-y-auto text-sm msg-content text-slate-700 dark:text-slate-200"
|
||||
style="max-height: calc(100vh - 220px)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,13 +452,7 @@
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="channels_desc">View and manage messaging channels</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center py-20">
|
||||
<div class="w-16 h-16 rounded-2xl bg-sky-50 dark:bg-sky-900/20 flex items-center justify-center mb-4">
|
||||
<i class="fas fa-tower-broadcast text-sky-400 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="channels_coming">Coming Soon</p>
|
||||
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="channels_coming_desc">Channel management will be available here</p>
|
||||
</div>
|
||||
<div id="channels-content" class="grid gap-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -450,13 +469,13 @@
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1" data-i18n="tasks_desc">View and manage scheduled tasks</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center py-20">
|
||||
<div id="tasks-empty" class="flex flex-col items-center justify-center py-20">
|
||||
<div class="w-16 h-16 rounded-2xl bg-rose-50 dark:bg-rose-900/20 flex items-center justify-center mb-4">
|
||||
<i class="fas fa-clock text-rose-400 text-xl"></i>
|
||||
</div>
|
||||
<p class="text-slate-500 dark:text-slate-400 font-medium" data-i18n="tasks_coming">Coming Soon</p>
|
||||
<p class="text-sm text-slate-400 dark:text-slate-500 mt-1" data-i18n="tasks_coming_desc">Scheduled task management will be available here</p>
|
||||
<p class="text-slate-500 dark:text-slate-400 font-medium">Loading...</p>
|
||||
</div>
|
||||
<div id="tasks-list" class="grid gap-4 hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,7 +507,7 @@
|
||||
<span class="text-xs text-slate-500" data-i18n="logs_live">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="log-output" class="p-4 h-[500px] overflow-y-auto font-mono text-xs leading-relaxed text-slate-300">
|
||||
<div id="log-output" class="p-4 overflow-y-auto font-mono text-xs leading-relaxed text-slate-300 whitespace-pre-wrap break-all" style="height: calc(100vh - 272px)">
|
||||
<p class="text-slate-500" data-i18n="logs_coming_msg">Log streaming will be available here. Connects to run.log for real-time output similar to tail -f.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
CowAgent Console - Main Application Script
|
||||
===================================================================== */
|
||||
|
||||
// =====================================================================
|
||||
// Version — update this before each release
|
||||
// =====================================================================
|
||||
const APP_VERSION = 'v2.0.1';
|
||||
|
||||
// =====================================================================
|
||||
// i18n
|
||||
// =====================================================================
|
||||
@@ -10,9 +15,9 @@ const I18N = {
|
||||
console: '控制台',
|
||||
nav_chat: '对话', nav_manage: '管理', nav_monitor: '监控',
|
||||
menu_chat: '对话', menu_config: '配置', menu_skills: '技能',
|
||||
menu_memory: '记忆', menu_channels: '通道', menu_tasks: '定时任务',
|
||||
menu_memory: '记忆', menu_channels: '通道', menu_tasks: '定时',
|
||||
menu_logs: '日志',
|
||||
welcome_subtitle: '我可以帮你解答问题、管理计算机、创造和执行技能,并通过长期记忆不断成长',
|
||||
welcome_subtitle: '我可以帮你解答问题、管理计算机、创造和执行技能,并通过长期记忆<br>不断成长',
|
||||
example_sys_title: '系统管理', example_sys_text: '帮我查看工作空间里有哪些文件',
|
||||
example_task_title: '智能任务', example_task_text: '提醒我5分钟后查看服务器情况',
|
||||
example_code_title: '编程助手', example_code_text: '帮我编写一个Python爬虫脚本',
|
||||
@@ -28,6 +33,7 @@ const I18N = {
|
||||
skills_loading: '加载技能中...', skills_loading_desc: '技能加载后将显示在此处',
|
||||
memory_title: '记忆管理', memory_desc: '查看 Agent 记忆文件和内容',
|
||||
memory_loading: '加载记忆文件中...', memory_loading_desc: '记忆文件将显示在此处',
|
||||
memory_back: '返回列表',
|
||||
memory_col_name: '文件名', memory_col_type: '类型', memory_col_size: '大小', memory_col_updated: '更新时间',
|
||||
channels_title: '通道管理', channels_desc: '查看和管理消息通道',
|
||||
channels_coming: '即将推出', channels_coming_desc: '通道管理功能即将在此提供',
|
||||
@@ -43,7 +49,7 @@ const I18N = {
|
||||
menu_chat: 'Chat', menu_config: 'Config', menu_skills: 'Skills',
|
||||
menu_memory: 'Memory', menu_channels: 'Channels', menu_tasks: 'Tasks',
|
||||
menu_logs: 'Logs',
|
||||
welcome_subtitle: 'I can help you answer questions, manage your computer, create and execute skills, and keep growing through long-term memory.',
|
||||
welcome_subtitle: 'I can help you answer questions, manage your computer, create and execute skills, and keep growing through <br> long-term memory.',
|
||||
example_sys_title: 'System', example_sys_text: 'Show me the files in the workspace',
|
||||
example_task_title: 'Smart Task', example_task_text: 'Remind me to check the server in 5 minutes',
|
||||
example_code_title: 'Coding', example_code_text: 'Write a Python web scraper script',
|
||||
@@ -59,6 +65,7 @@ const I18N = {
|
||||
skills_loading: 'Loading skills...', skills_loading_desc: 'Skills will be displayed here after loading',
|
||||
memory_title: 'Memory', memory_desc: 'View agent memory files and contents',
|
||||
memory_loading: 'Loading memory files...', memory_loading_desc: 'Memory files will be displayed here',
|
||||
memory_back: 'Back to list',
|
||||
memory_col_name: 'Filename', memory_col_type: 'Type', memory_col_size: 'Size', memory_col_updated: 'Updated',
|
||||
channels_title: 'Channels', channels_desc: 'View and manage messaging channels',
|
||||
channels_coming: 'Coming Soon', channels_coming_desc: 'Channel management will be available here',
|
||||
@@ -80,6 +87,9 @@ function applyI18n() {
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
el.textContent = t(el.dataset.i18n);
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-html]').forEach(el => {
|
||||
el.innerHTML = t(el.dataset.i18nHtml);
|
||||
});
|
||||
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
||||
el.placeholder = t(el.dataset['i18nPlaceholder']);
|
||||
});
|
||||
@@ -241,7 +251,6 @@ fetch('/config').then(r => r.json()).then(data => {
|
||||
const title = data.title || 'CowAgent';
|
||||
document.getElementById('welcome-title').textContent = title;
|
||||
document.getElementById('cfg-model').textContent = data.model || '--';
|
||||
document.getElementById('cfg-api-base').textContent = data.open_ai_api_base || '--';
|
||||
document.getElementById('cfg-agent').textContent = data.use_agent ? 'Enabled' : 'Disabled';
|
||||
document.getElementById('cfg-max-tokens').textContent = data.agent_max_context_tokens || '--';
|
||||
document.getElementById('cfg-max-turns').textContent = data.agent_max_context_turns || '--';
|
||||
@@ -676,7 +685,6 @@ function loadConfigView() {
|
||||
fetch('/config').then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
document.getElementById('cfg-model').textContent = data.model || '--';
|
||||
document.getElementById('cfg-api-base').textContent = data.open_ai_api_base || '--';
|
||||
document.getElementById('cfg-agent').textContent = data.use_agent ? 'Enabled' : 'Disabled';
|
||||
document.getElementById('cfg-max-tokens').textContent = data.agent_max_context_tokens || '--';
|
||||
document.getElementById('cfg-max-turns').textContent = data.agent_max_context_turns || '--';
|
||||
@@ -685,9 +693,279 @@ function loadConfigView() {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Skills View
|
||||
// =====================================================================
|
||||
let skillsLoaded = false;
|
||||
function loadSkillsView() {
|
||||
if (skillsLoaded) return;
|
||||
fetch('/api/skills').then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
const emptyEl = document.getElementById('skills-empty');
|
||||
const listEl = document.getElementById('skills-list');
|
||||
const skills = data.skills || [];
|
||||
if (skills.length === 0) {
|
||||
emptyEl.querySelector('p').textContent = currentLang === 'zh' ? '暂无技能' : 'No skills found';
|
||||
return;
|
||||
}
|
||||
emptyEl.classList.add('hidden');
|
||||
listEl.innerHTML = '';
|
||||
|
||||
const builtins = skills.filter(s => s.source === 'builtin');
|
||||
const customs = skills.filter(s => s.source !== 'builtin');
|
||||
|
||||
function renderGroup(title, items) {
|
||||
if (items.length === 0) return;
|
||||
const header = document.createElement('div');
|
||||
header.className = 'sm:col-span-2 text-xs font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500 mt-2';
|
||||
header.textContent = title;
|
||||
listEl.appendChild(header);
|
||||
items.forEach(sk => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-4 flex items-start gap-3';
|
||||
const iconColor = sk.enabled ? 'text-primary-400' : 'text-slate-300 dark:text-slate-600';
|
||||
const statusDot = sk.enabled
|
||||
? '<span class="w-2 h-2 rounded-full bg-primary-400 flex-shrink-0 mt-1"></span>'
|
||||
: '<span class="w-2 h-2 rounded-full bg-slate-300 dark:bg-slate-600 flex-shrink-0 mt-1"></span>';
|
||||
card.innerHTML = `
|
||||
<div class="w-9 h-9 rounded-lg bg-amber-50 dark:bg-amber-900/20 flex items-center justify-center flex-shrink-0">
|
||||
<i class="fas fa-bolt ${iconColor} text-sm"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-sm text-slate-700 dark:text-slate-200 truncate">${escapeHtml(sk.name)}</span>
|
||||
${statusDot}
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 dark:text-slate-500 mt-1 line-clamp-2">${escapeHtml(sk.description || '--')}</p>
|
||||
</div>`;
|
||||
listEl.appendChild(card);
|
||||
});
|
||||
}
|
||||
renderGroup(currentLang === 'zh' ? '内置技能' : 'Built-in Skills', builtins);
|
||||
renderGroup(currentLang === 'zh' ? '自定义技能' : 'Custom Skills', customs);
|
||||
skillsLoaded = true;
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Memory View
|
||||
// =====================================================================
|
||||
let memoryPage = 1;
|
||||
const memoryPageSize = 10;
|
||||
|
||||
function loadMemoryView(page) {
|
||||
page = page || 1;
|
||||
memoryPage = page;
|
||||
fetch(`/api/memory?page=${page}&page_size=${memoryPageSize}`).then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
const emptyEl = document.getElementById('memory-empty');
|
||||
const listEl = document.getElementById('memory-list');
|
||||
const files = data.list || [];
|
||||
const total = data.total || 0;
|
||||
|
||||
if (total === 0) {
|
||||
emptyEl.querySelector('p').textContent = currentLang === 'zh' ? '暂无记忆文件' : 'No memory files';
|
||||
emptyEl.classList.remove('hidden');
|
||||
listEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
emptyEl.classList.add('hidden');
|
||||
listEl.classList.remove('hidden');
|
||||
|
||||
const tbody = document.getElementById('memory-table-body');
|
||||
tbody.innerHTML = '';
|
||||
files.forEach(f => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b border-slate-100 dark:border-white/5 hover:bg-slate-50 dark:hover:bg-white/5 cursor-pointer transition-colors';
|
||||
tr.onclick = () => openMemoryFile(f.filename);
|
||||
const typeLabel = f.type === 'global'
|
||||
? '<span class="px-2 py-0.5 rounded-full text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400">Global</span>'
|
||||
: '<span class="px-2 py-0.5 rounded-full text-xs bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400">Daily</span>';
|
||||
const sizeStr = f.size < 1024 ? f.size + ' B' : (f.size / 1024).toFixed(1) + ' KB';
|
||||
tr.innerHTML = `
|
||||
<td class="px-4 py-3 text-sm font-mono text-slate-700 dark:text-slate-200">${escapeHtml(f.filename)}</td>
|
||||
<td class="px-4 py-3 text-sm">${typeLabel}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-500 dark:text-slate-400">${sizeStr}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-500 dark:text-slate-400">${escapeHtml(f.updated_at)}</td>`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// Pagination
|
||||
const totalPages = Math.ceil(total / memoryPageSize);
|
||||
const pagEl = document.getElementById('memory-pagination');
|
||||
if (totalPages <= 1) { pagEl.innerHTML = ''; return; }
|
||||
let pagHtml = `<span>${page} / ${totalPages}</span><div class="flex gap-2">`;
|
||||
if (page > 1) pagHtml += `<button onclick="loadMemoryView(${page - 1})" class="px-3 py-1 rounded-lg border border-slate-200 dark:border-white/10 hover:bg-slate-100 dark:hover:bg-white/10 text-xs">Prev</button>`;
|
||||
if (page < totalPages) pagHtml += `<button onclick="loadMemoryView(${page + 1})" class="px-3 py-1 rounded-lg border border-slate-200 dark:border-white/10 hover:bg-slate-100 dark:hover:bg-white/10 text-xs">Next</button>`;
|
||||
pagHtml += '</div>';
|
||||
pagEl.innerHTML = pagHtml;
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function openMemoryFile(filename) {
|
||||
fetch(`/api/memory/content?filename=${encodeURIComponent(filename)}`).then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
document.getElementById('memory-panel-list').classList.add('hidden');
|
||||
const panel = document.getElementById('memory-panel-viewer');
|
||||
document.getElementById('memory-viewer-title').textContent = filename;
|
||||
document.getElementById('memory-viewer-content').innerHTML = renderMarkdown(data.content || '');
|
||||
panel.classList.remove('hidden');
|
||||
applyHighlighting(panel);
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function closeMemoryViewer() {
|
||||
document.getElementById('memory-panel-viewer').classList.add('hidden');
|
||||
document.getElementById('memory-panel-list').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Channels View
|
||||
// =====================================================================
|
||||
function loadChannelsView() {
|
||||
const container = document.getElementById('channels-content');
|
||||
const channelType = appConfig.channel_type || 'web';
|
||||
const channelMap = {
|
||||
web: { name: 'Web', icon: 'fa-globe', color: 'primary' },
|
||||
terminal: { name: 'Terminal', icon: 'fa-terminal', color: 'slate' },
|
||||
feishu: { name: 'Feishu', icon: 'fa-paper-plane', color: 'blue' },
|
||||
dingtalk: { name: 'DingTalk', icon: 'fa-comments', color: 'blue' },
|
||||
wechatcom_app: { name: 'WeCom', icon: 'fa-building', color: 'emerald' },
|
||||
wechatmp: { name: 'WeChat MP', icon: 'fa-comment-dots', color: 'emerald' },
|
||||
wechatmp_service: { name: 'WeChat Service', icon: 'fa-comment-dots', color: 'emerald' },
|
||||
};
|
||||
const info = channelMap[channelType] || { name: channelType, icon: 'fa-tower-broadcast', color: 'sky' };
|
||||
container.innerHTML = `
|
||||
<div class="bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-6 flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-xl bg-${info.color}-50 dark:bg-${info.color}-900/20 flex items-center justify-center">
|
||||
<i class="fas ${info.icon} text-${info.color}-500 text-lg"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold text-slate-800 dark:text-slate-100">${info.name}</span>
|
||||
<span class="w-2 h-2 rounded-full bg-primary-400"></span>
|
||||
<span class="text-xs text-primary-500">Active</span>
|
||||
</div>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5 font-mono">${escapeHtml(channelType)}</p>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Scheduler View
|
||||
// =====================================================================
|
||||
let tasksLoaded = false;
|
||||
function loadTasksView() {
|
||||
if (tasksLoaded) return;
|
||||
fetch('/api/scheduler').then(r => r.json()).then(data => {
|
||||
if (data.status !== 'success') return;
|
||||
const emptyEl = document.getElementById('tasks-empty');
|
||||
const listEl = document.getElementById('tasks-list');
|
||||
const allTasks = data.tasks || [];
|
||||
// Only show active (enabled) tasks
|
||||
const tasks = allTasks.filter(t => t.enabled !== false);
|
||||
if (tasks.length === 0) {
|
||||
emptyEl.querySelector('p').textContent = currentLang === 'zh' ? '暂无定时任务' : 'No scheduled tasks';
|
||||
return;
|
||||
}
|
||||
emptyEl.classList.add('hidden');
|
||||
listEl.classList.remove('hidden');
|
||||
listEl.innerHTML = '';
|
||||
|
||||
tasks.forEach(task => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'bg-white dark:bg-[#1A1A1A] rounded-xl border border-slate-200 dark:border-white/10 p-4';
|
||||
const typeLabel = task.type === 'cron'
|
||||
? `<span class="text-xs font-mono text-slate-400">${escapeHtml(task.cron || '')}</span>`
|
||||
: `<span class="text-xs text-slate-400">${escapeHtml(task.type || 'once')}</span>`;
|
||||
let nextRun = '--';
|
||||
if (task.next_run_at) {
|
||||
// next_run_at is an ISO string, not a Unix timestamp
|
||||
const d = new Date(task.next_run_at);
|
||||
if (!isNaN(d.getTime())) nextRun = d.toLocaleString();
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="w-2 h-2 rounded-full bg-primary-400"></span>
|
||||
<span class="font-medium text-sm text-slate-700 dark:text-slate-200">${escapeHtml(task.name || task.id || '--')}</span>
|
||||
<div class="flex-1"></div>
|
||||
${typeLabel}
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mb-2 line-clamp-2">${escapeHtml(task.prompt || task.description || '')}</p>
|
||||
<div class="flex items-center gap-4 text-xs text-slate-400 dark:text-slate-500">
|
||||
<span><i class="fas fa-clock mr-1"></i>${currentLang === 'zh' ? '下次执行' : 'Next run'}: ${nextRun}</span>
|
||||
</div>`;
|
||||
listEl.appendChild(card);
|
||||
});
|
||||
tasksLoaded = true;
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Logs View
|
||||
// =====================================================================
|
||||
let logEventSource = null;
|
||||
|
||||
function startLogStream() {
|
||||
if (logEventSource) return;
|
||||
const output = document.getElementById('log-output');
|
||||
output.innerHTML = '';
|
||||
|
||||
logEventSource = new EventSource('/api/logs');
|
||||
logEventSource.onmessage = function(e) {
|
||||
let item;
|
||||
try { item = JSON.parse(e.data); } catch (_) { return; }
|
||||
|
||||
if (item.type === 'init') {
|
||||
output.textContent = item.content || '';
|
||||
output.scrollTop = output.scrollHeight;
|
||||
} else if (item.type === 'line') {
|
||||
output.textContent += item.content;
|
||||
output.scrollTop = output.scrollHeight;
|
||||
} else if (item.type === 'error') {
|
||||
output.textContent = item.message || 'Error loading logs';
|
||||
}
|
||||
};
|
||||
logEventSource.onerror = function() {
|
||||
logEventSource.close();
|
||||
logEventSource = null;
|
||||
};
|
||||
}
|
||||
|
||||
function stopLogStream() {
|
||||
if (logEventSource) {
|
||||
logEventSource.close();
|
||||
logEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// View Navigation Hook
|
||||
// =====================================================================
|
||||
const _origNavigateTo = navigateTo;
|
||||
navigateTo = function(viewId) {
|
||||
// Stop log stream when leaving logs view
|
||||
if (currentView === 'logs' && viewId !== 'logs') stopLogStream();
|
||||
|
||||
_origNavigateTo(viewId);
|
||||
|
||||
// Lazy-load view data
|
||||
if (viewId === 'skills') loadSkillsView();
|
||||
else if (viewId === 'memory') {
|
||||
// Always start from the list panel when navigating to memory
|
||||
document.getElementById('memory-panel-viewer').classList.add('hidden');
|
||||
document.getElementById('memory-panel-list').classList.remove('hidden');
|
||||
loadMemoryView(1);
|
||||
}
|
||||
else if (viewId === 'channels') loadChannelsView();
|
||||
else if (viewId === 'tasks') loadTasksView();
|
||||
else if (viewId === 'logs') startLogStream();
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// Initialization
|
||||
// =====================================================================
|
||||
applyTheme();
|
||||
applyI18n();
|
||||
document.getElementById('sidebar-version').textContent = `CowAgent ${APP_VERSION}`;
|
||||
chatInput.focus();
|
||||
|
||||
@@ -281,8 +281,8 @@ class WebChannel(ChatChannel):
|
||||
logger.info("[WebChannel] 5. wechatcom_app - 企微自建应用")
|
||||
logger.info("[WebChannel] 6. wechatmp - 个人公众号")
|
||||
logger.info("[WebChannel] 7. wechatmp_service - 企业公众号")
|
||||
logger.info(f"[WebChannel] 🌐 本地访问: http://localhost:{port}/chat")
|
||||
logger.info(f"[WebChannel] 🌍 服务器访问: http://YOUR_IP:{port}/chat (请将YOUR_IP替换为服务器IP)")
|
||||
logger.info(f"[WebChannel] 🌐 本地访问: http://localhost:{port}")
|
||||
logger.info(f"[WebChannel] 🌍 服务器访问: http://YOUR_IP:{port} (请将YOUR_IP替换为服务器IP)")
|
||||
logger.info("[WebChannel] ✅ Web对话网页已运行")
|
||||
|
||||
# 确保静态文件目录存在
|
||||
@@ -298,6 +298,11 @@ class WebChannel(ChatChannel):
|
||||
'/stream', 'StreamHandler',
|
||||
'/chat', 'ChatHandler',
|
||||
'/config', 'ConfigHandler',
|
||||
'/api/skills', 'SkillsHandler',
|
||||
'/api/memory', 'MemoryHandler',
|
||||
'/api/memory/content', 'MemoryContentHandler',
|
||||
'/api/scheduler', 'SchedulerHandler',
|
||||
'/api/logs', 'LogsHandler',
|
||||
'/assets/(.*)', 'AssetsHandler',
|
||||
)
|
||||
app = web.application(urls, globals(), autoreload=False)
|
||||
@@ -385,7 +390,6 @@ class ConfigHandler:
|
||||
"use_agent": use_agent,
|
||||
"title": title,
|
||||
"model": local_config.get("model", ""),
|
||||
"open_ai_api_base": local_config.get("open_ai_api_base", ""),
|
||||
"channel_type": local_config.get("channel_type", ""),
|
||||
"agent_max_context_tokens": local_config.get("agent_max_context_tokens", ""),
|
||||
"agent_max_context_turns": local_config.get("agent_max_context_turns", ""),
|
||||
@@ -396,6 +400,125 @@ class ConfigHandler:
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
def _get_workspace_root():
|
||||
"""Resolve the agent workspace directory."""
|
||||
from common.utils import expand_path
|
||||
return expand_path(conf().get("agent_workspace", "~/cow"))
|
||||
|
||||
|
||||
class SkillsHandler:
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.skills.service import SkillService
|
||||
from agent.skills.manager import SkillManager
|
||||
workspace_root = _get_workspace_root()
|
||||
manager = SkillManager(custom_dir=os.path.join(workspace_root, "skills"))
|
||||
service = SkillService(manager)
|
||||
skills = service.query()
|
||||
return json.dumps({"status": "success", "skills": skills}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Skills API error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
class MemoryHandler:
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.memory.service import MemoryService
|
||||
params = web.input(page='1', page_size='20')
|
||||
workspace_root = _get_workspace_root()
|
||||
service = MemoryService(workspace_root)
|
||||
result = service.list_files(page=int(params.page), page_size=int(params.page_size))
|
||||
return json.dumps({"status": "success", **result}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Memory API error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
class MemoryContentHandler:
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.memory.service import MemoryService
|
||||
params = web.input(filename='')
|
||||
if not params.filename:
|
||||
return json.dumps({"status": "error", "message": "filename required"})
|
||||
workspace_root = _get_workspace_root()
|
||||
service = MemoryService(workspace_root)
|
||||
result = service.get_content(params.filename)
|
||||
return json.dumps({"status": "success", **result}, ensure_ascii=False)
|
||||
except FileNotFoundError:
|
||||
return json.dumps({"status": "error", "message": "file not found"})
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Memory content API error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
class SchedulerHandler:
|
||||
def GET(self):
|
||||
web.header('Content-Type', 'application/json; charset=utf-8')
|
||||
try:
|
||||
from agent.tools.scheduler.task_store import TaskStore
|
||||
workspace_root = _get_workspace_root()
|
||||
store_path = os.path.join(workspace_root, "scheduler", "tasks.json")
|
||||
store = TaskStore(store_path)
|
||||
tasks = store.list_tasks()
|
||||
return json.dumps({"status": "success", "tasks": tasks}, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
logger.error(f"[WebChannel] Scheduler API error: {e}")
|
||||
return json.dumps({"status": "error", "message": str(e)})
|
||||
|
||||
|
||||
class LogsHandler:
|
||||
def GET(self):
|
||||
"""Stream the last N lines of run.log as SSE, then tail new lines."""
|
||||
web.header('Content-Type', 'text/event-stream; charset=utf-8')
|
||||
web.header('Cache-Control', 'no-cache')
|
||||
web.header('X-Accel-Buffering', 'no')
|
||||
|
||||
from config import get_root
|
||||
log_path = os.path.join(get_root(), "run.log")
|
||||
|
||||
def generate():
|
||||
if not os.path.isfile(log_path):
|
||||
yield b"data: {\"type\": \"error\", \"message\": \"run.log not found\"}\n\n"
|
||||
return
|
||||
|
||||
# Read last 200 lines for initial display
|
||||
try:
|
||||
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
lines = f.readlines()
|
||||
tail_lines = lines[-200:]
|
||||
chunk = ''.join(tail_lines)
|
||||
payload = json.dumps({"type": "init", "content": chunk}, ensure_ascii=False)
|
||||
yield f"data: {payload}\n\n".encode('utf-8')
|
||||
except Exception as e:
|
||||
yield f"data: {{\"type\": \"error\", \"message\": \"{e}\"}}\n\n".encode('utf-8')
|
||||
return
|
||||
|
||||
# Tail new lines
|
||||
try:
|
||||
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
|
||||
f.seek(0, 2) # seek to end
|
||||
deadline = time.time() + 600 # 10 min max
|
||||
while time.time() < deadline:
|
||||
line = f.readline()
|
||||
if line:
|
||||
payload = json.dumps({"type": "line", "content": line}, ensure_ascii=False)
|
||||
yield f"data: {payload}\n\n".encode('utf-8')
|
||||
else:
|
||||
yield b": keepalive\n\n"
|
||||
time.sleep(1)
|
||||
except GeneratorExit:
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
|
||||
return generate()
|
||||
|
||||
|
||||
class AssetsHandler:
|
||||
def GET(self, file_path): # 修改默认参数
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user