feat: 完善 Web UI 细节
新增页面:
- session_detail.html - 会话详情独立页面
- channel_edit.html - 频道配置编辑独立页面
- 错误页面 (401/403/404/500)
功能优化:
- 添加错误处理器支持 HTML 响应
- 更新编辑按钮跳转独立页面
- 完善暗黑主题支持
作者: 小黑 🐶
This commit is contained in:
@@ -123,24 +123,34 @@ def _configure_logging(app):
|
||||
|
||||
def _register_error_handlers(app):
|
||||
"""注册错误处理器"""
|
||||
from flask import jsonify
|
||||
from flask import jsonify, render_template, request, render_template
|
||||
|
||||
@app.errorhandler(400)
|
||||
def bad_request(error):
|
||||
if request.accepts('text/html'):
|
||||
return render_template('errors/400.html', error=str(error)), 400
|
||||
return jsonify({'error': 'Bad Request', 'message': str(error)}), 400
|
||||
|
||||
@app.errorhandler(401)
|
||||
def unauthorized(error):
|
||||
if request.accepts('text/html'):
|
||||
return render_template('errors/401.html', error=str(error)), 401
|
||||
return jsonify({'error': 'Unauthorized', 'message': str(error)}), 401
|
||||
|
||||
@app.errorhandler(403)
|
||||
def forbidden(error):
|
||||
if request.accepts('text/html'):
|
||||
return render_template('errors/403.html', error=str(error)), 403
|
||||
return jsonify({'error': 'Forbidden', 'message': str(error)}), 403
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(error):
|
||||
if request.accepts('text/html'):
|
||||
return render_template('errors/404.html', error=str(error)), 404
|
||||
return jsonify({'error': 'Not Found', 'message': str(error)}), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(error):
|
||||
if request.accepts('text/html'):
|
||||
return render_template('errors/500.html', error=str(error)), 500
|
||||
return jsonify({'error': 'Internal Server Error', 'message': str(error)}), 500
|
||||
|
||||
232
app/templates/config/channel_edit.html
Normal file
232
app/templates/config/channel_edit.html
Normal file
@@ -0,0 +1,232 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}编辑频道 - 智队中枢{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{# 页面头部 #}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ url_for('web.channels') }}" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold dark:text-white">编辑频道配置</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ config.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 表单 #}
|
||||
<form id="channel-form" class="space-y-6">
|
||||
<input type="hidden" id="channel-id" value="{{ config.id }}">
|
||||
|
||||
{# 基本信息 #}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-lg font-semibold dark:text-white">基本信息</h2>
|
||||
</div>
|
||||
<div class="card-body space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">
|
||||
名称 <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="channel-name" name="name" class="input" required
|
||||
value="{{ config.name }}" placeholder="如:默认频道">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">频道配置的唯一名称</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">
|
||||
Gateway URL <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="url" id="channel-url" name="gateway_url" class="input" required
|
||||
value="{{ config.gateway_url }}" placeholder="ws://localhost:18888/ws">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">WebSocket 连接地址</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">认证 Token</label>
|
||||
<div class="relative">
|
||||
<input type="password" id="channel-token" name="auth_token" class="input pr-10"
|
||||
value="{{ config.auth_token or '' }}" placeholder="可选,用于认证">
|
||||
<button type="button" onclick="toggleTokenVisibility()"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600">
|
||||
<svg id="eye-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">用于连接认证的 Token</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 连接配置 #}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-lg font-semibold dark:text-white">连接配置</h2>
|
||||
</div>
|
||||
<div class="card-body space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">重连间隔 (ms)</label>
|
||||
<input type="number" id="channel-reconnect" name="reconnect_interval" class="input"
|
||||
value="{{ config.reconnect_interval or 5000 }}" min="1000" step="1000">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">连接断开后的重连间隔</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1 dark:text-gray-300">心跳间隔 (ms)</label>
|
||||
<input type="number" id="channel-heartbeat" name="heartbeat_interval" class="input"
|
||||
value="{{ config.heartbeat_interval or 30000 }}" min="1000" step="1000">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">心跳包发送间隔</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<input type="checkbox" id="channel-enabled" name="enabled"
|
||||
class="w-5 h-5 text-primary-600 rounded focus:ring-primary-500"
|
||||
{{ 'checked' if config.enabled else '' }}>
|
||||
<div>
|
||||
<label for="channel-enabled" class="font-medium dark:text-gray-200 cursor-pointer">启用此频道</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">启用后将自动连接到 Gateway</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 状态信息 #}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-lg font-semibold dark:text-white">状态信息</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">连接状态</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{% if config.status == 'online' %}
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
<span class="font-medium text-green-600 dark:text-green-400">在线</span>
|
||||
{% else %}
|
||||
<span class="w-2 h-2 bg-red-500 rounded-full"></span>
|
||||
<span class="font-medium text-red-600 dark:text-red-400">离线</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">最后连接</div>
|
||||
<div class="font-medium dark:text-white">
|
||||
{{ config.last_connected.strftime('%Y-%m-%d %H:%M') if config.last_connected else '从未连接' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">更新时间</div>
|
||||
<div class="font-medium dark:text-white">
|
||||
{{ config.updated_at.strftime('%Y-%m-%d %H:%M') if config.updated_at else '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if config.last_error %}
|
||||
<div class="mt-4 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div class="flex items-start gap-2">
|
||||
<svg class="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="font-medium text-red-700 dark:text-red-400">最后错误</div>
|
||||
<div class="text-sm text-red-600 dark:text-red-300 mt-1">{{ config.last_error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 操作按钮 #}
|
||||
<div class="flex items-center justify-between">
|
||||
<button type="button" onclick="testConnection()" class="btn btn-secondary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||
</svg>
|
||||
测试连接
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ url_for('web.channels') }}" class="btn btn-secondary">取消</a>
|
||||
<button type="submit" class="btn btn-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
保存更改
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleTokenVisibility() {
|
||||
const input = document.getElementById('channel-token');
|
||||
const icon = document.getElementById('eye-icon');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
icon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>';
|
||||
}
|
||||
}
|
||||
|
||||
function testConnection() {
|
||||
showToast('正在测试连接...', 'info');
|
||||
|
||||
fetch(`/api/web/channels/{{ config.id }}/test`, { method: 'POST' })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast('连接测试通过!', 'success');
|
||||
} else {
|
||||
showToast(`连接失败: ${data.error}`, 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showToast(`测试出错: ${err.message}`, 'error');
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('channel-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = {
|
||||
name: document.getElementById('channel-name').value,
|
||||
gateway_url: document.getElementById('channel-url').value,
|
||||
auth_token: document.getElementById('channel-token').value,
|
||||
reconnect_interval: parseInt(document.getElementById('channel-reconnect').value),
|
||||
heartbeat_interval: parseInt(document.getElementById('channel-heartbeat').value),
|
||||
enabled: document.getElementById('channel-enabled').checked
|
||||
};
|
||||
|
||||
fetch(`/api/web/channels/{{ config.id }}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
showToast('保存成功', 'success');
|
||||
setTimeout(() => location.href = '{{ url_for("web.channels") }}', 1000);
|
||||
} else {
|
||||
return res.json().then(d => { throw new Error(d.error || '保存失败'); });
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showToast(err.message, 'error');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -148,20 +148,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑频道
|
||||
// 编辑频道 - 跳转到独立编辑页面
|
||||
function editChannel(id) {
|
||||
fetch(`/api/web/channels/${id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
document.getElementById('modal-title').textContent = '编辑配置';
|
||||
document.getElementById('channel-id').value = data.id;
|
||||
document.getElementById('channel-name').value = data.name;
|
||||
document.getElementById('channel-url').value = data.gateway_url;
|
||||
document.getElementById('channel-reconnect').value = data.reconnect_interval;
|
||||
document.getElementById('channel-heartbeat').value = data.heartbeat_interval;
|
||||
document.getElementById('channel-enabled').checked = data.enabled;
|
||||
showModal('channel-modal');
|
||||
});
|
||||
window.location.href = `/web/config/channels/${id}/edit`;
|
||||
}
|
||||
|
||||
// 删除频道
|
||||
|
||||
30
app/templates/errors/401.html
Normal file
30
app/templates/errors/401.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>401 - 未授权 | 智队中枢</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = { darkMode: 'class', theme: { extend: { colors: { primary: { 500: '#0ea5e9', 600: '#0284c7' } } } } }
|
||||
</script>
|
||||
<style>body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex items-center justify-center">
|
||||
<div class="text-center px-4">
|
||||
<div class="text-[150px] font-bold text-yellow-200 dark:text-yellow-900/30 leading-none select-none">401</div>
|
||||
<div class="mt-[-20px] mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">未授权</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400">请先登录后再访问此页面</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<a href="/api/auth/login" class="btn btn-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path></svg>
|
||||
登录
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-8 text-sm text-gray-400 dark:text-gray-500">智队中枢 v0.7.0</div>
|
||||
</div>
|
||||
<script>const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme);</script>
|
||||
</body>
|
||||
</html>
|
||||
30
app/templates/errors/403.html
Normal file
30
app/templates/errors/403.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>403 - 禁止访问 | 智队中枢</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = { darkMode: 'class', theme: { extend: { colors: { primary: { 500: '#0ea5e9', 600: '#0284c7' } } } } }
|
||||
</script>
|
||||
<style>body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex items-center justify-center">
|
||||
<div class="text-center px-4">
|
||||
<div class="text-[150px] font-bold text-red-200 dark:text-red-900/30 leading-none select-none">403</div>
|
||||
<div class="mt-[-20px] mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">禁止访问</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400">您没有权限访问此页面</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<a href="/" class="btn btn-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
|
||||
返回首页
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-8 text-sm text-gray-400 dark:text-gray-500">智队中枢 v0.7.0</div>
|
||||
</div>
|
||||
<script>const savedTheme = localStorage.getItem('theme') || 'light'; document.documentElement.setAttribute('data-theme', savedTheme);</script>
|
||||
</body>
|
||||
</html>
|
||||
104
app/templates/errors/404.html
Normal file
104
app/templates/errors/404.html
Normal file
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - 页面未找到 | 智队中枢</title>
|
||||
|
||||
{# Tailwind CSS #}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex items-center justify-center">
|
||||
<div class="text-center px-4">
|
||||
{# 404 数字 #}
|
||||
<div class="text-[150px] font-bold text-gray-200 dark:text-gray-700 leading-none select-none">
|
||||
404
|
||||
</div>
|
||||
|
||||
{# 错误信息 #}
|
||||
<div class="mt-[-20px] mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">页面未找到</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400">抱歉,您访问的页面不存在或已被移除</p>
|
||||
</div>
|
||||
|
||||
{# 操作按钮 #}
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<a href="javascript:history.back()" class="btn btn-secondary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回上一页
|
||||
</a>
|
||||
<a href="/" class="btn btn-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
返回首页
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# 建议 #}
|
||||
<div class="mt-12 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm max-w-md mx-auto">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white mb-3">您可以尝试</h3>
|
||||
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-2 text-left">
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-primary-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>检查 URL 是否正确</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-primary-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>刷新页面</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-primary-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>联系管理员</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# 页脚 #}
|
||||
<div class="mt-8 text-sm text-gray-400 dark:text-gray-500">
|
||||
智队中枢 v0.7.0 | <a href="https://github.com/yunxiafei/pit-router" class="hover:text-primary-500">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 初始化主题
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
112
app/templates/errors/500.html
Normal file
112
app/templates/errors/500.html
Normal file
@@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="light">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>500 - 服务器错误 | 智队中枢</title>
|
||||
|
||||
{# Tailwind CSS #}
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex items-center justify-center">
|
||||
<div class="text-center px-4">
|
||||
{# 500 数字 #}
|
||||
<div class="text-[150px] font-bold text-red-200 dark:text-red-900/30 leading-none select-none">
|
||||
500
|
||||
</div>
|
||||
|
||||
{# 错误信息 #}
|
||||
<div class="mt-[-20px] mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">服务器错误</h1>
|
||||
<p class="text-gray-500 dark:text-gray-400">抱歉,服务器遇到了一个意外情况</p>
|
||||
</div>
|
||||
|
||||
{# 操作按钮 #}
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<button onclick="location.reload()" class="btn btn-secondary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
刷新页面
|
||||
</button>
|
||||
<a href="/" class="btn btn-primary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
|
||||
</svg>
|
||||
返回首页
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# 错误详情 #}
|
||||
{% if error %}
|
||||
<div class="mt-8 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg max-w-2xl mx-auto text-left">
|
||||
<h3 class="font-semibold text-red-700 dark:text-red-400 mb-2">错误信息</h3>
|
||||
<pre class="text-sm text-red-600 dark:text-red-300 overflow-auto">{{ error }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# 建议 #}
|
||||
<div class="mt-8 p-6 bg-white dark:bg-gray-800 rounded-lg shadow-sm max-w-md mx-auto">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white mb-3">如果问题持续存在</h3>
|
||||
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-2 text-left">
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-primary-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>稍后再试</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-primary-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>清除浏览器缓存</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<svg class="w-4 h-4 text-primary-500 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>联系管理员报告问题</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{# 页脚 #}
|
||||
<div class="mt-8 text-sm text-gray-400 dark:text-gray-500">
|
||||
智队中枢 v0.7.0 | <a href="https://github.com/yunxiafei/pit-router" class="hover:text-primary-500">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 初始化主题
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
187
app/templates/monitor/session_detail.html
Normal file
187
app/templates/monitor/session_detail.html
Normal file
@@ -0,0 +1,187 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}会话详情 - 智队中枢{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
{# 页面头部 #}
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ url_for('web.sessions') }}" class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold dark:text-white">会话详情</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">ID: {{ session.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{% if session.status == 'active' %}
|
||||
<button onclick="closeSession()" class="btn btn-danger flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
关闭会话
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="exportSession()" class="btn btn-secondary flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
导出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 会话信息卡片 #}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="card p-4">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">用户</div>
|
||||
<div class="font-medium dark:text-white flex items-center gap-2">
|
||||
<div class="w-8 h-8 bg-primary-100 dark:bg-primary-900/30 rounded-full flex items-center justify-center">
|
||||
<span class="text-primary-600 dark:text-primary-400 text-sm font-medium">
|
||||
{{ session.user.username[0] if session.user else '?' }}
|
||||
</span>
|
||||
</div>
|
||||
{{ session.user.username if session.user else '匿名用户' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">主 Agent</div>
|
||||
<div class="font-medium dark:text-white">
|
||||
{{ session.primary_agent.name if session.primary_agent else '未分配' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">状态</div>
|
||||
<div>
|
||||
{% if session.status == 'active' %}
|
||||
<span class="badge badge-success flex items-center gap-1 w-fit">
|
||||
<span class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||
活跃
|
||||
</span>
|
||||
{% elif session.status == 'paused' %}
|
||||
<span class="badge badge-warning w-fit">暂停</span>
|
||||
{% else %}
|
||||
<span class="badge badge-danger w-fit">已关闭</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card p-4">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">消息数</div>
|
||||
<div class="text-2xl font-bold dark:text-white">{{ session.message_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 时间线 #}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="text-lg font-semibold dark:text-white">时间线</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-8 text-sm">
|
||||
<div>
|
||||
<div class="text-gray-500 dark:text-gray-400">创建时间</div>
|
||||
<div class="font-medium dark:text-white">{{ session.created_at.strftime('%Y-%m-%d %H:%M:%S') if session.created_at else '-' }}</div>
|
||||
</div>
|
||||
<div class="w-px h-8 bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div>
|
||||
<div class="text-gray-500 dark:text-gray-400">最后活跃</div>
|
||||
<div class="font-medium dark:text-white">{{ session.last_active_at.strftime('%Y-%m-%d %H:%M:%S') if session.last_active_at else '-' }}</div>
|
||||
</div>
|
||||
<div class="w-px h-8 bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div>
|
||||
<div class="text-gray-500 dark:text-gray-400">更新时间</div>
|
||||
<div class="font-medium dark:text-white">{{ session.updated_at.strftime('%Y-%m-%d %H:%M:%S') if session.updated_at else '-' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 消息记录 #}
|
||||
<div class="card">
|
||||
<div class="card-header flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold dark:text-white">消息记录</h2>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">共 {{ messages|length }} 条</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if messages %}
|
||||
<div class="space-y-4 max-h-[600px] overflow-auto pr-2" id="messages-container">
|
||||
{% for msg in messages %}
|
||||
<div class="flex {{ 'justify-end' if msg.sender_type == 'agent' else 'justify-start' }}">
|
||||
<div class="max-w-[80%] {{ 'bg-primary-100 dark:bg-primary-900/30 text-gray-900 dark:text-gray-100' if msg.sender_type == 'agent' else 'bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100' }} rounded-2xl px-4 py-3">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs font-medium {{ 'text-primary-700 dark:text-primary-400' if msg.sender_type == 'agent' else 'text-gray-600 dark:text-gray-400' }}">
|
||||
{{ 'Agent' if msg.sender_type == 'agent' else ('用户' if msg.sender_type == 'user' else '系统') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">{{ msg.created_at.strftime('%H:%M:%S') if msg.created_at else '' }}</span>
|
||||
{% if msg.status == 'sent' %}
|
||||
<svg class="w-3 h-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
{% elif msg.status == 'delivered' %}
|
||||
<svg class="w-3 h-3 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7m0 0l-4-4m4 4l-4-4"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="text-sm whitespace-pre-wrap">{{ msg.content }}</div>
|
||||
{% if msg.message_type == 'media' %}
|
||||
<div class="mt-2">
|
||||
<span class="inline-flex items-center gap-1 text-xs text-gray-500 bg-gray-200 dark:bg-gray-600 px-2 py-1 rounded">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
媒体文件
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-12">
|
||||
<div class="w-16 h-16 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-gray-500 dark:text-gray-400">暂无消息</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function closeSession() {
|
||||
if (!confirm('确定要关闭此会话吗?此操作不可撤销。')) return;
|
||||
|
||||
fetch(`/api/web/sessions/{{ session.id }}/close`, { method: 'PUT' })
|
||||
.then(res => {
|
||||
if (res.ok) {
|
||||
showToast('会话已关闭', 'success');
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
showToast('关闭失败', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function exportSession() {
|
||||
window.open(`/api/sessions/{{ session.id }}/messages?format=json`, '_blank');
|
||||
showToast('开始导出...', 'info');
|
||||
}
|
||||
|
||||
// 自动滚动到底部
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const container = document.getElementById('messages-container');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -57,3 +57,23 @@ def sessions():
|
||||
agents = Agent.query.all()
|
||||
sessions = Session.query.order_by(Session.last_active_at.desc()).limit(50).all()
|
||||
return render_template('monitor/sessions.html', sessions=sessions, agents=agents)
|
||||
|
||||
|
||||
@web_bp.route('/monitor/sessions/<session_id>')
|
||||
@jwt_required()
|
||||
def session_detail(session_id):
|
||||
"""会话详情页面"""
|
||||
session = Session.query.get_or_404(session_id)
|
||||
messages = Message.query.filter_by(session_id=session_id).order_by(
|
||||
Message.created_at.asc()
|
||||
).limit(200).all()
|
||||
return render_template('monitor/session_detail.html', session=session, messages=messages)
|
||||
|
||||
|
||||
@web_bp.route('/config/channels/<channel_id>/edit')
|
||||
@jwt_required()
|
||||
def channel_edit(channel_id):
|
||||
"""编辑频道配置页面"""
|
||||
from app.models.channel_config import ChannelConfig
|
||||
config = ChannelConfig.query.get_or_404(channel_id)
|
||||
return render_template('config/channel_edit.html', config=config)
|
||||
|
||||
Reference in New Issue
Block a user