feat: 新增飞书长连接模式,无需公网域名
## 🚀 重大更新 ### 飞书集成升级 - ✅ 迁移到飞书官方 SDK 的事件订阅 2.0(长连接模式) - ✅ 无需公网域名和 webhook 配置 - ✅ 支持内网部署 - ✅ 自动重连机制 ### 核心功能优化 - ✅ 优化群聊隔离机制(每个用户在每个群独立会话) - ✅ 增强日志输出(emoji 标记便于快速识别) - ✅ 完善错误处理和异常恢复 - ✅ 添加 SSL 证书问题解决方案 ### 新增文件 - `src/integrations/feishu_longconn_service.py` - 飞书长连接服务 - `start_feishu_bot.py` - 启动脚本 - `test_feishu_connection.py` - 连接诊断工具 - `docs/FEISHU_LONGCONN.md` - 详细使用文档 - `README.md` - 项目说明文档 ### 技术改进 - 添加 lark-oapi==1.3.5 官方 SDK - 升级 certifi 包以支持 SSL 验证 - 优化配置加载逻辑 - 改进会话管理机制 ### 文档更新 - 新增飞书长连接模式完整文档 - 更新快速开始指南 - 添加常见问题解答(SSL、权限、部署等) - 完善架构说明和技术栈介绍 ## 📝 使用方式 启动飞书长连接服务(无需公网域名): ```bash python3 start_feishu_bot.py ``` 详见:docs/FEISHU_LONGCONN.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
287
src/web/templates/login.html
Normal file
287
src/web/templates/login.html
Normal file
@@ -0,0 +1,287 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TSP 智能助手 - 登录</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
overflow: hidden;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
.login-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 40px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
.login-header h1 {
|
||||
font-size: 28px;
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.login-header p {
|
||||
margin: 10px 0 0;
|
||||
opacity: 0.9;
|
||||
font-size: 14px;
|
||||
}
|
||||
.login-body {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
.form-floating {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-floating label {
|
||||
color: #6c757d;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.25rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
color: white;
|
||||
}
|
||||
.btn-login:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.remember-me input {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.logo-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.input-group-text {
|
||||
background: transparent;
|
||||
border-right: none;
|
||||
}
|
||||
.form-control {
|
||||
border-left: none;
|
||||
}
|
||||
.input-icon-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.input-icon-wrapper i {
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #6c757d;
|
||||
z-index: 10;
|
||||
}
|
||||
.input-icon-wrapper input {
|
||||
padding-left: 45px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<!-- 登录头部 -->
|
||||
<div class="login-header">
|
||||
<div class="logo-icon">
|
||||
<i class="bi bi-car-front-fill"></i>
|
||||
</div>
|
||||
<h1>TSP 智能助手</h1>
|
||||
<p>Telematics Service Platform</p>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<div class="login-body">
|
||||
<!-- 错误提示 -->
|
||||
<div id="alert-container"></div>
|
||||
|
||||
<form id="loginForm">
|
||||
<!-- 用户名输入 -->
|
||||
<div class="input-icon-wrapper">
|
||||
<i class="bi bi-person-fill"></i>
|
||||
<input type="text" class="form-control" id="username" name="username"
|
||||
placeholder="用户名" required autofocus>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入 -->
|
||||
<div class="input-icon-wrapper">
|
||||
<i class="bi bi-lock-fill"></i>
|
||||
<input type="password" class="form-control" id="password" name="password"
|
||||
placeholder="密码" required>
|
||||
</div>
|
||||
|
||||
<!-- 记住我 -->
|
||||
<div class="remember-me">
|
||||
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||
<label class="form-check-label" for="remember">
|
||||
记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button type="submit" class="btn btn-login" id="loginBtn">
|
||||
<span id="loginBtnText">登录</span>
|
||||
<span id="loginBtnSpinner" class="spinner-border spinner-border-sm d-none" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="text-center mt-4" style="color: #6c757d; font-size: 13px;">
|
||||
<p class="mb-0">默认账号: <strong>admin</strong></p>
|
||||
<p>默认密码: <strong>admin123</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const loginBtnText = document.getElementById('loginBtnText');
|
||||
const loginBtnSpinner = document.getElementById('loginBtnSpinner');
|
||||
const alertContainer = document.getElementById('alert-container');
|
||||
|
||||
// 检查是否已登录
|
||||
checkLoginStatus();
|
||||
|
||||
loginForm.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// 获取表单数据
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const remember = document.getElementById('remember').checked;
|
||||
|
||||
if (!username || !password) {
|
||||
showAlert('请输入用户名和密码', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
setLoading(true);
|
||||
clearAlert();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
remember: remember
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
showAlert('登录成功,正在跳转...', 'success');
|
||||
|
||||
// 保存 token 到 localStorage
|
||||
if (data.token) {
|
||||
localStorage.setItem('auth_token', data.token);
|
||||
}
|
||||
|
||||
// 延迟跳转
|
||||
setTimeout(() => {
|
||||
window.location.href = '/dashboard';
|
||||
}, 500);
|
||||
} else {
|
||||
showAlert(data.message || '登录失败,请检查用户名和密码', 'danger');
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
showAlert('网络错误,请稍后重试', 'danger');
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
function setLoading(loading) {
|
||||
loginBtn.disabled = loading;
|
||||
if (loading) {
|
||||
loginBtnText.classList.add('d-none');
|
||||
loginBtnSpinner.classList.remove('d-none');
|
||||
} else {
|
||||
loginBtnText.classList.remove('d-none');
|
||||
loginBtnSpinner.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alertHtml = `
|
||||
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-${type === 'success' ? 'check-circle-fill' : 'exclamation-triangle-fill'}"></i>
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
`;
|
||||
alertContainer.innerHTML = alertHtml;
|
||||
}
|
||||
|
||||
function clearAlert() {
|
||||
alertContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
async function checkLoginStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/status');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.authenticated) {
|
||||
// 已登录,直接跳转
|
||||
window.location.href = '/dashboard';
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('未登录');
|
||||
}
|
||||
}
|
||||
|
||||
// 回车键快捷登录
|
||||
document.getElementById('password').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
loginForm.dispatchEvent(new Event('submit'));
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user