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:
zhaojie
2026-02-11 14:10:18 +08:00
parent f5acb05e61
commit e3a0396567
18 changed files with 1501 additions and 112 deletions

View 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>