first commit
69
.dockerignore
Normal file
@@ -0,0 +1,69 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.env
|
||||
.venv
|
||||
|
||||
# Testing
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Project specific
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
*.log
|
||||
.git/
|
||||
README.md
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
nginx.conf
|
||||
187
.gitignore
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Application specific
|
||||
uploads/
|
||||
logs/
|
||||
database/
|
||||
chat_history/
|
||||
45
.trae/documents/增加内网资产应用及美化页面.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 增加内网资产应用跳转及样式美化计划
|
||||
|
||||
我将在 `templates/index.html` 中添加一个新的“应用服务”板块,包含您指定的6个应用跳转项,并对弹出的内网资产页面(二级页面)进行样式美化。
|
||||
|
||||
## 1. 新增应用服务板块
|
||||
|
||||
在“台式机资源”下方新增一个 `<div class="asset-section">`,标题为“应用服务”,包含以下应用:
|
||||
|
||||
* **it-tools**: 端口 40116 (图标: `fa-toolbox`)
|
||||
|
||||
* **langflow**: 端口 7860 (图标: `fa-wind`)
|
||||
|
||||
* **n8n**: 端口 5678 (图标: `fa-project-diagram`)
|
||||
|
||||
* **certimate**: 端口 8090 (图标: `fa-certificate`)
|
||||
|
||||
* **ntfy**: 端口 40265 (图标: `fa-comment-dots`)
|
||||
|
||||
* **screego**: 端口 5050 (图标: `fa-desktop`)
|
||||
|
||||
* QR-code生成
|
||||
|
||||
地址统一为 `6.6.6.66`。
|
||||
|
||||
## 2. 样式美化 (CSS)
|
||||
|
||||
我将更新 JavaScript 中动态注入的 CSS 样式,以美化弹出的二级页面:
|
||||
|
||||
* **整体容器**: 优化 `.internal-assets-container` 的滚动体验和间距。
|
||||
|
||||
* **列表项**: 增强 `.asset-item` 的视觉效果,添加更明显的悬停 (Hover) 效果和阴影。
|
||||
|
||||
* **凭证详情**: 美化 `.asset-credentials` 区域(虽然新应用暂无凭证,但优化现有凭证的展示):
|
||||
|
||||
* 添加展开动画 (`slideDown`)。
|
||||
|
||||
* 优化账号/密码显示框的样式,使其看起来更像代码块或卡片。
|
||||
|
||||
* 调整复制按钮的样式。
|
||||
|
||||
## 3. 实现步骤
|
||||
|
||||
1. 编辑 `templates/index.html`,在 `modalBody.innerHTML` 的模板字符串中插入新的 HTML 结构。
|
||||
2. 在同一个文件的 `<style>` 标签内容中(JS 字符串内)添加和更新 CSS 规则。
|
||||
|
||||
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"idf.pythonInstallPath": "/Applications/miniconda3/bin/python"
|
||||
}
|
||||
118
DEPLOY_NOTES.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 部署脚本优化报告
|
||||
|
||||
## 🚨 主要问题修复
|
||||
|
||||
### 1. Python环境管理问题
|
||||
**问题**: Ubuntu 24.04 使用了 PEP 668 外部管理环境策略,禁止直接使用 pip 安装包到系统环境
|
||||
**解决方案**:
|
||||
- 使用 Python 虚拟环境 (`python3 -m venv`)
|
||||
- 所有依赖安装在隔离的虚拟环境中
|
||||
- 应用启动时自动激活虚拟环境
|
||||
|
||||
### 2. 环境重复安装优化
|
||||
**改进**:
|
||||
- 添加环境检测功能,避免重复安装已存在的组件
|
||||
- 智能判断:基础环境 / 虚拟环境 / 完全安装
|
||||
- 如果服务器已有环境,跳过系统依赖安装
|
||||
|
||||
### 3. 文件上传修复
|
||||
**问题**: `database/*` 被排除,导致 `ip_list.json` 无法上传
|
||||
**解决方案**:
|
||||
- 移除了 `database/*` 的排除规则
|
||||
- `ip_list.json` 现在会正确上传到服务器
|
||||
|
||||
## 📁 文件结构改进
|
||||
|
||||
```
|
||||
服务器部署结构:
|
||||
/home/ubuntu/
|
||||
├── host_message_venv/ # Python虚拟环境
|
||||
│ ├── bin/python # 虚拟环境Python解释器
|
||||
│ ├── bin/pip # 虚拟环境pip
|
||||
│ └── lib/python3.12/ # 依赖包安装位置
|
||||
└── host_message/ # 应用代码
|
||||
├── main.py # 主应用文件
|
||||
├── database/
|
||||
│ └── ip_list.json # ✅ 现在会正确上传
|
||||
├── start_app.sh # 启动脚本(使用虚拟环境)
|
||||
├── debug_app.sh # 调试脚本
|
||||
└── logs/
|
||||
└── supervisor.log # 应用日志
|
||||
```
|
||||
|
||||
## 🔧 技术改进
|
||||
|
||||
### 虚拟环境管理
|
||||
- 创建独立的Python虚拟环境 `/home/ubuntu/host_message_venv`
|
||||
- 所有依赖安装在虚拟环境中,避免系统污染
|
||||
- 启动脚本自动激活虚拟环境
|
||||
|
||||
### 智能部署流程
|
||||
```bash
|
||||
1. 检查环境状态
|
||||
├── 完全已安装 → 跳过所有安装
|
||||
├── 部分安装 → 只创建虚拟环境
|
||||
└── 未安装 → 完整安装流程
|
||||
|
||||
2. 虚拟环境管理
|
||||
├── 检测现有虚拟环境
|
||||
├── 重新创建(确保干净)
|
||||
└── 安装所有Python依赖
|
||||
|
||||
3. 应用部署
|
||||
├── 代码解压
|
||||
├── 启动脚本配置
|
||||
└── Supervisor进程管理
|
||||
```
|
||||
|
||||
### 错误诊断增强
|
||||
- 改进的调试脚本,支持虚拟环境
|
||||
- 详细的环境信息输出
|
||||
- 更好的错误日志收集
|
||||
|
||||
## 🚀 使用指南
|
||||
|
||||
### 1. 预检查(推荐)
|
||||
```bash
|
||||
./pre_deploy_check.sh
|
||||
```
|
||||
|
||||
### 2. 执行部署
|
||||
```bash
|
||||
./deploy.sh
|
||||
```
|
||||
|
||||
### 3. 应用管理
|
||||
```bash
|
||||
# 查看状态
|
||||
ssh ubuntu@6.6.6.86 'sudo supervisorctl status host_message'
|
||||
|
||||
# 重启应用
|
||||
ssh ubuntu@6.6.6.86 'sudo supervisorctl restart host_message'
|
||||
|
||||
# 查看日志
|
||||
ssh ubuntu@6.6.6.86 'tail -f /home/ubuntu/host_message/logs/supervisor.log'
|
||||
|
||||
# 调试应用
|
||||
ssh ubuntu@6.6.6.86 'cd /home/ubuntu/host_message && ./debug_app.sh'
|
||||
```
|
||||
|
||||
## ✅ 问题解决确认
|
||||
|
||||
- ✅ **Python环境管理**: 使用虚拟环境避免 externally-managed-environment 错误
|
||||
- ✅ **依赖安装**: 所有包正确安装在虚拟环境中
|
||||
- ✅ **文件上传**: ip_list.json 正确上传到服务器
|
||||
- ✅ **环境检测**: 智能跳过已安装的组件
|
||||
- ✅ **进程管理**: 正确的启动和监控配置
|
||||
- ✅ **错误诊断**: 详细的调试信息和日志
|
||||
|
||||
## 🎯 预期结果
|
||||
|
||||
部署成功后,应用将:
|
||||
1. 在独立的Python虚拟环境中运行
|
||||
2. 自动启动并保持运行状态
|
||||
3. 监听 8888 端口提供服务
|
||||
4. 包含完整的 ip_list.json 配置
|
||||
5. 具备自动重启和日志管理功能
|
||||
|
||||
访问地址: http://6.6.6.86:8888
|
||||
42
Dockerfile
Normal file
@@ -0,0 +1,42 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置环境变量
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apt-get update && apt-get install -y \
|
||||
--no-install-recommends \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 复制并安装Python依赖
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 创建上传文件目录和设置权限
|
||||
RUN mkdir -p uploads && \
|
||||
chmod 755 uploads
|
||||
|
||||
# 创建非root用户(安全性改进)
|
||||
RUN useradd --create-home --shell /bin/bash app && \
|
||||
chown -R app:app /app
|
||||
USER app
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8888
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8888/health || exit 1
|
||||
|
||||
# 启动命令
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8888", "--proxy-headers", "--forwarded-allow-ips", "*"]
|
||||
177
README.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# FastAPI 文件共享和聊天应用
|
||||
|
||||
这是一个基于FastAPI的文件共享和实时聊天应用,支持Docker部署并解决了Docker环境下IP获取错误的问题。
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
- 📁 文件上传和下载
|
||||
- 💬 实时聊天(WebSocket)
|
||||
- 👥 显示在线用户
|
||||
- 🔍 真实IP地址获取(支持Docker/代理环境)
|
||||
- ❤️ 健康检查
|
||||
- 🗑️ 文件删除功能
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 方式1:Docker Compose(推荐)
|
||||
|
||||
```bash
|
||||
# 使用nginx反向代理(推荐,支持真实IP)
|
||||
docker-compose up -d
|
||||
|
||||
# 访问应用
|
||||
open http://localhost
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### 方式2:仅FastAPI服务
|
||||
|
||||
```bash
|
||||
# 构建并运行
|
||||
docker-compose up -d web
|
||||
|
||||
# 访问应用
|
||||
open http://localhost:1000
|
||||
```
|
||||
|
||||
### 方式3:本地开发
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 开发模式启动
|
||||
./start.sh dev
|
||||
|
||||
# 或直接启动
|
||||
python main.py
|
||||
```
|
||||
|
||||
## 🔧 IP获取优化
|
||||
|
||||
### 问题说明
|
||||
在Docker环境中,传统的 `request.client.host` 会返回Docker内部网络IP(如172.x.x.x),而不是真实的客户端IP。
|
||||
|
||||
### 解决方案
|
||||
本项目实现了智能IP获取机制:
|
||||
|
||||
1. **优先级顺序**:
|
||||
- `X-Forwarded-For` 头部
|
||||
- `X-Real-IP` 头部
|
||||
- `client.host`(排除Docker内部IP)
|
||||
|
||||
2. **支持的代理场景**:
|
||||
- Nginx反向代理
|
||||
- Docker网络
|
||||
- Cloudflare等CDN
|
||||
- 各种负载均衡器
|
||||
|
||||
### 配置示例
|
||||
|
||||
#### Nginx配置
|
||||
```nginx
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
```
|
||||
|
||||
#### Docker启动参数
|
||||
```bash
|
||||
uvicorn main:app --proxy-headers --forwarded-allow-ips "*"
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
├── main.py # 主应用文件
|
||||
├── templates/ # HTML模板
|
||||
│ ├── index.html # 文件上传页面
|
||||
│ └── chat.html # 聊天页面
|
||||
├── uploads/ # 文件存储目录
|
||||
├── Dockerfile # Docker构建文件
|
||||
├── docker-compose.yml # Docker编排文件
|
||||
├── nginx.conf # Nginx配置
|
||||
├── requirements.txt # Python依赖
|
||||
├── start.sh # 启动脚本
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 🔗 API端点
|
||||
|
||||
| 端点 | 方法 | 描述 |
|
||||
|------|------|------|
|
||||
| `/` | GET | 主页(文件上传) |
|
||||
| `/chat` | GET | 聊天页面 |
|
||||
| `/upload` | POST | 上传文件 |
|
||||
| `/files` | GET | 获取文件列表 |
|
||||
| `/files/{filename}` | DELETE | 删除文件 |
|
||||
| `/get_ip` | GET | 获取客户端IP |
|
||||
| `/ws` | WebSocket | 聊天WebSocket |
|
||||
| `/health` | GET | 健康检查 |
|
||||
| `/online_users` | GET | 获取在线用户 |
|
||||
|
||||
## 🐳 Docker部署细节
|
||||
|
||||
### 环境变量
|
||||
```bash
|
||||
FORWARDED_ALLOW_IPS=* # 允许所有IP转发
|
||||
PROXY_HEADERS=1 # 启用代理头部
|
||||
```
|
||||
|
||||
### 端口映射
|
||||
- `1000` - FastAPI应用端口
|
||||
- `80` - Nginx反向代理端口(可选)
|
||||
|
||||
### 数据持久化
|
||||
```yaml
|
||||
volumes:
|
||||
- ./uploads:/app/uploads # 文件存储持久化
|
||||
```
|
||||
|
||||
## 🛠️ 开发说明
|
||||
|
||||
### 本地开发
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 启动开发服务器
|
||||
./start.sh dev
|
||||
|
||||
# 或者
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 调试IP获取
|
||||
```python
|
||||
# 查看请求头
|
||||
print(request.headers)
|
||||
|
||||
# 测试IP获取函数
|
||||
ip = get_real_client_ip(request=request)
|
||||
print(f"Client IP: {ip}")
|
||||
```
|
||||
|
||||
## 🚨 常见问题
|
||||
|
||||
### Q: Docker中IP显示为172.x.x.x?
|
||||
A: 使用nginx反向代理或确保启动时包含 `--proxy-headers` 参数。
|
||||
|
||||
### Q: WebSocket连接失败?
|
||||
A: 检查防火墙设置和代理配置。
|
||||
|
||||
### Q: 文件上传失败?
|
||||
A: 检查uploads目录权限和磁盘空间。
|
||||
|
||||
### Q: 健康检查失败?
|
||||
A: 确保应用正常启动并且端口未被占用。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交Issue和Pull Request!
|
||||
648
deploy.sh
Normal file
@@ -0,0 +1,648 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================================
|
||||
# 自动部署脚本 - host_message 项目
|
||||
# 功能:将本地代码打包上传到远程服务器并自动部署
|
||||
# ==========================================================================
|
||||
|
||||
# 配置信息
|
||||
REMOTE_HOST="6.6.6.86"
|
||||
REMOTE_USER="ubuntu"
|
||||
REMOTE_PASS="qweasdzxc1"
|
||||
REMOTE_DIR="/home/ubuntu/host_message"
|
||||
LOCAL_DIR="/Users/jeremygan/Desktop/TangledupAI/host_message-main"
|
||||
APP_PORT=8888
|
||||
ZIP_NAME="host_message_$(date +%Y%m%d_%H%M%S).zip"
|
||||
TEMP_ZIP="/tmp/$ZIP_NAME"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${BLUE}[STEP]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
# 检查依赖
|
||||
check_dependencies() {
|
||||
log_step "检查系统依赖..."
|
||||
|
||||
# 检查 sshpass
|
||||
if ! command -v sshpass &> /dev/null; then
|
||||
log_error "sshpass 未安装,正在安装..."
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
if command -v brew &> /dev/null; then
|
||||
brew install sshpass
|
||||
else
|
||||
log_error "请先安装 Homebrew,然后运行: brew install sshpass"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
sudo apt-get update && sudo apt-get install -y sshpass
|
||||
else
|
||||
log_error "不支持的操作系统,请手动安装 sshpass"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查 zip
|
||||
if ! command -v zip &> /dev/null; then
|
||||
log_error "zip 命令未找到,请安装 zip 工具"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "依赖检查完成"
|
||||
}
|
||||
|
||||
# 创建代码包
|
||||
create_package() {
|
||||
log_step "创建代码包..."
|
||||
|
||||
# 切换到项目目录
|
||||
cd "$LOCAL_DIR" || {
|
||||
log_error "无法进入项目目录: $LOCAL_DIR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 删除旧的临时文件
|
||||
rm -f "$TEMP_ZIP"
|
||||
|
||||
# 创建排除文件列表
|
||||
EXCLUDE_PATTERNS=(
|
||||
"*.pyc"
|
||||
"__pycache__/*"
|
||||
".git/*"
|
||||
".DS_Store"
|
||||
"logs/*"
|
||||
"*.log"
|
||||
".env"
|
||||
"venv/*"
|
||||
".venv/*"
|
||||
"node_modules/*"
|
||||
"uploads/*"
|
||||
"chat_history/*"
|
||||
)
|
||||
|
||||
# 构建排除参数
|
||||
EXCLUDE_ARGS=""
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
EXCLUDE_ARGS="$EXCLUDE_ARGS -x $pattern"
|
||||
done
|
||||
|
||||
# 创建zip包
|
||||
log_info "正在打包文件..."
|
||||
eval "zip -r \"$TEMP_ZIP\" . $EXCLUDE_ARGS"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "代码包创建成功: $TEMP_ZIP"
|
||||
log_info "包大小: $(du -h "$TEMP_ZIP" | cut -f1)"
|
||||
else
|
||||
log_error "代码包创建失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试SSH连接
|
||||
test_ssh_connection() {
|
||||
log_step "测试SSH连接..."
|
||||
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$REMOTE_USER@$REMOTE_HOST" "echo 'SSH连接测试成功'" 2>/dev/null
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "SSH连接测试成功"
|
||||
else
|
||||
log_error "SSH连接失败,请检查服务器地址、用户名和密码"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 上传代码包
|
||||
upload_package() {
|
||||
log_step "上传代码包到服务器..."
|
||||
|
||||
# 使用scp上传文件
|
||||
sshpass -p "$REMOTE_PASS" scp -o StrictHostKeyChecking=no "$TEMP_ZIP" "$REMOTE_USER@$REMOTE_HOST:/tmp/"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "代码包上传成功"
|
||||
else
|
||||
log_error "代码包上传失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 远程部署
|
||||
remote_deploy() {
|
||||
log_step "在远程服务器上执行部署..."
|
||||
|
||||
# 创建远程部署脚本
|
||||
REMOTE_SCRIPT=$(cat <<EOF
|
||||
#!/bin/bash
|
||||
|
||||
# 设置颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() {
|
||||
echo -e "\${GREEN}[远程INFO]\${NC} \$(date '+%Y-%m-%d %H:%M:%S') - \$1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "\${RED}[远程ERROR]\${NC} \$(date '+%Y-%m-%d %H:%M:%S') - \$1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "\${YELLOW}[远程WARN]\${NC} \$(date '+%Y-%m-%d %H:%M:%S') - \$1"
|
||||
}
|
||||
|
||||
# 检查端口占用并杀死进程
|
||||
check_and_kill_port() {
|
||||
log_info \"检查端口 $APP_PORT 是否被占用...\"
|
||||
|
||||
# 查找占用端口的进程
|
||||
PID=\$(lsof -ti:$APP_PORT 2>/dev/null)
|
||||
|
||||
if [ ! -z \"\$PID\" ]; then
|
||||
log_warn \"发现端口 $APP_PORT 被进程 \$PID 占用,正在终止...\"
|
||||
kill -TERM \$PID
|
||||
sleep 3
|
||||
|
||||
# 检查进程是否还存在
|
||||
if kill -0 \$PID 2>/dev/null; then
|
||||
log_warn \"进程仍然存在,强制终止...\"
|
||||
kill -KILL \$PID
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# 再次检查
|
||||
NEW_PID=\$(lsof -ti:$APP_PORT 2>/dev/null)
|
||||
if [ ! -z \"\$NEW_PID\" ]; then
|
||||
log_error \"无法终止占用端口 $APP_PORT 的进程\"
|
||||
exit 1
|
||||
else
|
||||
log_info \"端口 $APP_PORT 已释放\"
|
||||
fi
|
||||
else
|
||||
log_info \"端口 $APP_PORT 未被占用\"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查环境是否已安装
|
||||
check_environment() {
|
||||
log_info \"检查服务器环境...\"
|
||||
|
||||
# 检查Python3
|
||||
if ! python3 --version &> /dev/null; then
|
||||
log_info \"Python3 未安装,需要安装...\"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查是否存在虚拟环境
|
||||
if [ -d \"/home/ubuntu/host_message_venv\" ]; then
|
||||
log_info \"发现已存在的虚拟环境\"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 检查supervisor
|
||||
if ! command -v supervisorctl &> /dev/null; then
|
||||
log_info \"Supervisor 未安装,需要安装...\"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info \"基础环境检查通过,但需要创建虚拟环境\"
|
||||
return 2
|
||||
}
|
||||
|
||||
# 安装系统依赖
|
||||
install_dependencies() {
|
||||
log_info \"安装系统依赖...\"
|
||||
|
||||
# 更新包列表
|
||||
sudo apt-get update -qq
|
||||
|
||||
# 安装Python3、pip和相关开发工具
|
||||
log_info \"安装 Python3 和相关工具...\"
|
||||
sudo apt-get install -y python3 python3-pip python3-venv python3-dev build-essential python3-full
|
||||
|
||||
# 安装supervisor用于进程保活
|
||||
if ! command -v supervisorctl &> /dev/null; then
|
||||
log_info \"安装 supervisor...\"
|
||||
sudo apt-get install -y supervisor
|
||||
sudo systemctl enable supervisor
|
||||
sudo systemctl start supervisor
|
||||
fi
|
||||
|
||||
# 安装其他必要工具
|
||||
sudo apt-get install -y curl wget unzip lsof net-tools
|
||||
|
||||
log_info \"系统依赖安装完成\"
|
||||
}
|
||||
|
||||
# 创建和管理虚拟环境
|
||||
setup_virtual_environment() {
|
||||
local venv_path=\"/home/ubuntu/host_message_venv\"
|
||||
|
||||
log_info \"设置Python虚拟环境...\"
|
||||
|
||||
# 如果虚拟环境已存在,询问是否重新创建
|
||||
if [ -d \"\$venv_path\" ]; then
|
||||
log_info \"虚拟环境已存在,删除旧环境并重新创建...\"
|
||||
rm -rf \"\$venv_path\"
|
||||
fi
|
||||
|
||||
# 创建虚拟环境
|
||||
log_info \"创建新的虚拟环境...\"
|
||||
python3 -m venv \"\$venv_path\"
|
||||
|
||||
if [ \$? -eq 0 ]; then
|
||||
log_info \"虚拟环境创建成功: \$venv_path\"
|
||||
else
|
||||
log_error \"虚拟环境创建失败\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 激活虚拟环境并升级pip
|
||||
log_info \"激活虚拟环境并升级pip...\"
|
||||
source \"\$venv_path/bin/activate\"
|
||||
pip install --upgrade pip setuptools wheel
|
||||
|
||||
log_info \"虚拟环境设置完成\"
|
||||
}
|
||||
|
||||
# 主要部署逻辑
|
||||
main_deploy() {
|
||||
# 检查并终止端口进程
|
||||
check_and_kill_port
|
||||
|
||||
# 检查环境状态
|
||||
check_environment
|
||||
env_status=\$?
|
||||
|
||||
case \$env_status in
|
||||
0)
|
||||
log_info "环境已完整安装,跳过依赖安装"
|
||||
;;
|
||||
1)
|
||||
log_info "需要安装基础环境"
|
||||
install_dependencies
|
||||
setup_virtual_environment
|
||||
;;
|
||||
2)
|
||||
log_info "基础环境已安装,只需创建虚拟环境"
|
||||
setup_virtual_environment
|
||||
;;
|
||||
esac
|
||||
|
||||
# 备份旧代码(如果存在)
|
||||
if [ -d \"$REMOTE_DIR\" ]; then
|
||||
log_info \"备份现有代码...\"
|
||||
sudo mv \"$REMOTE_DIR\" \"${REMOTE_DIR}_backup_\$(date +%Y%m%d_%H%M%S)\" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 创建部署目录
|
||||
log_info \"创建部署目录...\"
|
||||
sudo mkdir -p \"$REMOTE_DIR\"
|
||||
sudo chown \$USER:\$USER \"$REMOTE_DIR\"
|
||||
|
||||
# 解压代码包
|
||||
log_info \"解压代码包...\"
|
||||
cd \"$REMOTE_DIR\"
|
||||
unzip -q \"/tmp/$ZIP_NAME\"
|
||||
|
||||
if [ \$? -eq 0 ]; then
|
||||
log_info \"代码解压成功\"
|
||||
else
|
||||
log_error \"代码解压失败\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p logs uploads chat_history database
|
||||
|
||||
# 激活虚拟环境并安装Python依赖
|
||||
log_info \"激活虚拟环境并安装Python依赖...\"
|
||||
source \"/home/ubuntu/host_message_venv/bin/activate\"
|
||||
|
||||
if [ -f \"requirements.txt\" ]; then
|
||||
log_info \"发现 requirements.txt,安装依赖包...\"
|
||||
pip install -r requirements.txt
|
||||
if [ \$? -eq 0 ]; then
|
||||
log_info \"Python依赖安装成功\"
|
||||
else
|
||||
log_error \"Python依赖安装失败\"
|
||||
# 尝试单独安装每个包
|
||||
log_info \"尝试单独安装依赖包...\"
|
||||
while IFS= read -r package; do
|
||||
if [[ ! \$package =~ ^[[:space:]]*# ]] && [[ ! -z \$package ]]; then
|
||||
log_info \"安装: \$package\"
|
||||
pip install \$package || log_warn \"安装 \$package 失败\"
|
||||
fi
|
||||
done < requirements.txt
|
||||
fi
|
||||
else
|
||||
log_warn \"未找到 requirements.txt 文件\"
|
||||
fi
|
||||
|
||||
# 验证关键依赖
|
||||
log_info \"验证Python依赖...\"
|
||||
python -c \"import fastapi, uvicorn; print('关键依赖验证成功')\" || {
|
||||
log_error \"关键依赖验证失败,手动安装...\"
|
||||
pip install fastapi uvicorn
|
||||
}
|
||||
|
||||
# 检查main.py文件
|
||||
if [ ! -f \"main.py\" ]; then
|
||||
log_error \"main.py 文件不存在!\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 测试Python应用是否可以启动
|
||||
log_info \"测试Python应用...\"
|
||||
source \"/home/ubuntu/host_message_venv/bin/activate\"
|
||||
timeout 10 python -c \"
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
try:
|
||||
import main
|
||||
print('应用模块导入成功')
|
||||
except Exception as e:
|
||||
print(f'应用模块导入失败: {e}')
|
||||
sys.exit(1)
|
||||
\" || log_warn \"应用模块测试失败,但继续部署...\""
|
||||
|
||||
# 创建启动脚本
|
||||
log_info \"创建应用启动脚本...\"
|
||||
cat > start_app.sh <<SCRIPT_EOF
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 进入应用目录
|
||||
cd \"$REMOTE_DIR\"
|
||||
|
||||
# 激活虚拟环境
|
||||
source \"/home/ubuntu/host_message_venv/bin/activate\"
|
||||
|
||||
# 设置环境变量
|
||||
export PYTHONPATH=\"$REMOTE_DIR:\\\$PYTHONPATH\"
|
||||
|
||||
# 记录启动信息
|
||||
echo \"[\$(date)] 应用启动开始...\"
|
||||
echo \"当前目录: \$(pwd)\"
|
||||
echo \"Python版本: \$(python --version)\"
|
||||
echo \"虚拟环境: \$VIRTUAL_ENV\"
|
||||
|
||||
# 检查必要文件
|
||||
if [ ! -f \"main.py\" ]; then
|
||||
echo \"错误: main.py 文件不存在\"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启动应用
|
||||
echo \"[\$(date)] 启动Python应用...\"
|
||||
python main.py
|
||||
SCRIPT_EOF
|
||||
chmod +x start_app.sh
|
||||
|
||||
# 创建调试脚本
|
||||
log_info \"创建调试脚本...\"
|
||||
cat > debug_app.sh <<DEBUG_EOF
|
||||
#!/bin/bash
|
||||
cd \"$REMOTE_DIR\"
|
||||
echo \"=== 调试信息 ===\"
|
||||
echo \"当前目录: \$(pwd)\"
|
||||
|
||||
# 激活虚拟环境
|
||||
source \"/home/ubuntu/host_message_venv/bin/activate\" 2>/dev/null || echo \"虚拟环境激活失败\"
|
||||
|
||||
echo \"Python版本: \$(python --version 2>/dev/null || python3 --version)\"
|
||||
echo \"pip版本: \$(pip --version 2>/dev/null || echo '无pip')\"
|
||||
echo \"虚拟环境: \$VIRTUAL_ENV\"
|
||||
echo \"文件列表:\"
|
||||
ls -la
|
||||
echo \"=== 尝试导入测试 ===\"
|
||||
python -c \"
|
||||
import sys
|
||||
print('Python路径:', sys.path)
|
||||
try:
|
||||
import fastapi
|
||||
print('fastapi版本:', fastapi.__version__)
|
||||
except ImportError as e:
|
||||
print('fastapi导入失败:', e)
|
||||
try:
|
||||
import uvicorn
|
||||
print('uvicorn版本:', uvicorn.__version__)
|
||||
except ImportError as e:
|
||||
print('uvicorn导入失败:', e)
|
||||
\" 2>/dev/null || python3 -c \"
|
||||
import sys
|
||||
print('Python路径:', sys.path)
|
||||
try:
|
||||
import fastapi
|
||||
print('fastapi版本:', fastapi.__version__)
|
||||
except ImportError as e:
|
||||
print('fastapi导入失败:', e)
|
||||
try:
|
||||
import uvicorn
|
||||
print('uvicorn版本:', uvicorn.__version__)
|
||||
except ImportError as e:
|
||||
print('uvicorn导入失败:', e)
|
||||
\"
|
||||
echo \"=== 检查端口占用 ===\"
|
||||
netstat -tlnp | grep 8888 || echo '端口8888未被占用'
|
||||
echo \"=== 尝试启动应用(5秒后停止) ===\"
|
||||
timeout 5 python main.py 2>/dev/null || timeout 5 python3 main.py || echo '应用启动测试完成'
|
||||
DEBUG_EOF
|
||||
chmod +x debug_app.sh
|
||||
|
||||
# 创建supervisor配置
|
||||
log_info \"配置进程保活监控...\"
|
||||
sudo tee /etc/supervisor/conf.d/host_message.conf > /dev/null <<EOF
|
||||
[program:host_message]
|
||||
command=$REMOTE_DIR/start_app.sh
|
||||
directory=$REMOTE_DIR
|
||||
user=ubuntu
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
stdout_logfile=$REMOTE_DIR/logs/supervisor.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=3
|
||||
environment=PATH=\"/home/ubuntu/host_message_venv/bin:/usr/local/bin:/usr/bin:/bin\",PYTHONPATH=\"$REMOTE_DIR\"
|
||||
startsecs=10
|
||||
startretries=3
|
||||
EOF
|
||||
|
||||
# 停止可能存在的旧进程
|
||||
sudo supervisorctl stop host_message 2>/dev/null || true
|
||||
|
||||
# 重新加载supervisor配置
|
||||
sudo supervisorctl reread
|
||||
sudo supervisorctl update
|
||||
|
||||
# 启动应用
|
||||
log_info \"启动应用...\"
|
||||
sudo supervisorctl start host_message
|
||||
|
||||
# 等待应用启动并检查状态
|
||||
log_info \"等待应用启动...\"
|
||||
for i in {1..30}; do
|
||||
sleep 2
|
||||
STATUS=\$(sudo supervisorctl status host_message 2>/dev/null || echo "ERROR")
|
||||
log_info \"第 \$i 次检查: \$STATUS\"
|
||||
|
||||
if echo \"\$STATUS\" | grep -q \"RUNNING\"; then
|
||||
log_info \"应用启动成功!\"
|
||||
break
|
||||
elif echo \"\$STATUS\" | grep -q \"FATAL\\|BACKOFF\"; then
|
||||
log_error \"应用启动失败: \$STATUS\"
|
||||
log_error \"查看详细日志:\"
|
||||
tail -20 \"$REMOTE_DIR/logs/supervisor.log\" 2>/dev/null || echo \"无法读取日志文件\"
|
||||
|
||||
# 运行调试脚本
|
||||
log_info \"运行调试脚本获取详细信息:\"
|
||||
cd \"$REMOTE_DIR\"
|
||||
./debug_app.sh 2>&1 || true
|
||||
|
||||
# 检查supervisor错误日志
|
||||
log_info \"检查supervisor错误日志:\"
|
||||
sudo tail -20 /var/log/supervisor/supervisord.log 2>/dev/null || echo \"无法读取supervisor日志\"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ \$i -eq 30 ]; then
|
||||
log_error \"应用启动超时\"
|
||||
log_error \"最终状态: \$STATUS\"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# 检查端口监听
|
||||
log_info \"检查端口监听状态...\"
|
||||
for i in {1..10}; do
|
||||
if netstat -tlnp 2>/dev/null | grep -q \":$APP_PORT\" || ss -tlnp 2>/dev/null | grep -q \":$APP_PORT\"; then
|
||||
log_info \"端口 $APP_PORT 监听正常\"
|
||||
log_info \"部署完成!您可以通过 http://$REMOTE_HOST:$APP_PORT 访问应用\"
|
||||
break
|
||||
else
|
||||
log_warn \"等待端口 $APP_PORT 开始监听... (\$i/10)\"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
if [ \$i -eq 10 ]; then
|
||||
log_warn \"端口 $APP_PORT 未在监听,请检查应用日志\"
|
||||
log_info \"当前监听的端口:\"
|
||||
netstat -tlnp 2>/dev/null | grep LISTEN || ss -tlnp 2>/dev/null | grep LISTEN || echo \"无法获取监听端口信息\"
|
||||
fi
|
||||
done
|
||||
|
||||
# 清理临时文件
|
||||
rm -f \"/tmp/$ZIP_NAME\"
|
||||
|
||||
log_info \"部署脚本执行完成\"
|
||||
}
|
||||
|
||||
# 执行主要部署逻辑
|
||||
main_deploy
|
||||
EOF
|
||||
)
|
||||
|
||||
# 执行远程部署脚本
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" "$REMOTE_SCRIPT"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "远程部署完成"
|
||||
else
|
||||
log_error "远程部署失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理本地临时文件
|
||||
cleanup() {
|
||||
log_step "清理临时文件..."
|
||||
rm -f "$TEMP_ZIP"
|
||||
log_info "临时文件清理完成"
|
||||
}
|
||||
|
||||
# 显示部署信息
|
||||
show_deploy_info() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 部署完成信息"
|
||||
echo "=========================================="
|
||||
echo "服务器地址: $REMOTE_HOST"
|
||||
echo "部署目录: $REMOTE_DIR"
|
||||
echo "应用端口: $APP_PORT"
|
||||
echo "访问地址: http://$REMOTE_HOST:$APP_PORT"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "常用管理命令:"
|
||||
echo "查看应用状态: ssh $REMOTE_USER@$REMOTE_HOST 'sudo supervisorctl status host_message'"
|
||||
echo "重启应用: ssh $REMOTE_USER@$REMOTE_HOST 'sudo supervisorctl restart host_message'"
|
||||
echo "停止应用: ssh $REMOTE_USER@$REMOTE_HOST 'sudo supervisorctl stop host_message'"
|
||||
echo "查看日志: ssh $REMOTE_USER@$REMOTE_HOST 'tail -f $REMOTE_DIR/logs/supervisor.log'"
|
||||
echo "调试应用: ssh $REMOTE_USER@$REMOTE_HOST 'cd $REMOTE_DIR && ./debug_app.sh'"
|
||||
echo "=========================================="
|
||||
|
||||
# 最后验证部署
|
||||
log_info "正在验证部署结果..."
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" "
|
||||
echo '=== 最终验证 ==='
|
||||
sudo supervisorctl status host_message
|
||||
echo '=== 端口检查 ==='
|
||||
netstat -tlnp | grep 8888 || ss -tlnp | grep 8888 || echo '端口8888未监听'
|
||||
echo '=== 进程检查 ==='
|
||||
ps aux | grep 'python3 main.py' | grep -v grep || echo '未找到Python进程'
|
||||
" || log_warn "最终验证失败,请手动检查"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
echo "=========================================="
|
||||
echo " host_message 项目自动部署脚本"
|
||||
echo "=========================================="
|
||||
echo "目标服务器: $REMOTE_HOST"
|
||||
echo "部署目录: $REMOTE_DIR"
|
||||
echo "开始时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 执行部署步骤
|
||||
check_dependencies
|
||||
create_package
|
||||
test_ssh_connection
|
||||
upload_package
|
||||
remote_deploy
|
||||
cleanup
|
||||
show_deploy_info
|
||||
|
||||
log_info "🎉 全部部署任务完成!"
|
||||
}
|
||||
|
||||
# 错误处理
|
||||
set -e
|
||||
trap 'log_error "脚本执行过程中发生错误,退出码: $?"' ERR
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
243
deploy_fixed.sh
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ==========================================================================
|
||||
# 自动部署脚本 - host_message 项目
|
||||
# 功能:将本地代码打包上传到远程服务器并自动部署
|
||||
# ==========================================================================
|
||||
|
||||
# 配置信息
|
||||
REMOTE_HOST="6.6.6.86"
|
||||
REMOTE_USER="ubuntu"
|
||||
REMOTE_PASS="qweasdzxc1"
|
||||
REMOTE_DIR="/home/ubuntu/host_message"
|
||||
LOCAL_DIR="/Users/jeremygan/Desktop/TangledupAI/host_message-main"
|
||||
APP_PORT=8888
|
||||
ZIP_NAME="host_message_$(date +%Y%m%d_%H%M%S).zip"
|
||||
TEMP_ZIP="/tmp/$ZIP_NAME"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
log_step() {
|
||||
echo -e "${BLUE}[STEP]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
# 检查依赖
|
||||
check_dependencies() {
|
||||
log_step "检查系统依赖..."
|
||||
|
||||
# 检查 sshpass
|
||||
if ! command -v sshpass &> /dev/null; then
|
||||
log_error "sshpass 未安装,正在安装..."
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
if command -v brew &> /dev/null; then
|
||||
brew install sshpass
|
||||
else
|
||||
log_error "请先安装 Homebrew,然后运行: brew install sshpass"
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# Linux
|
||||
sudo apt-get update && sudo apt-get install -y sshpass
|
||||
else
|
||||
log_error "不支持的操作系统,请手动安装 sshpass"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查 zip
|
||||
if ! command -v zip &> /dev/null; then
|
||||
log_error "zip 命令未找到,请安装 zip 工具"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "依赖检查完成"
|
||||
}
|
||||
|
||||
# 创建代码包
|
||||
create_package() {
|
||||
log_step "创建代码包..."
|
||||
|
||||
# 切换到项目目录
|
||||
cd "$LOCAL_DIR" || {
|
||||
log_error "无法进入项目目录: $LOCAL_DIR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 删除旧的临时文件
|
||||
rm -f "$TEMP_ZIP"
|
||||
|
||||
# 创建排除文件列表
|
||||
EXCLUDE_PATTERNS=(
|
||||
"*.pyc"
|
||||
"__pycache__/*"
|
||||
".git/*"
|
||||
".DS_Store"
|
||||
"logs/*"
|
||||
"*.log"
|
||||
".env"
|
||||
"venv/*"
|
||||
".venv/*"
|
||||
"node_modules/*"
|
||||
"uploads/*"
|
||||
"chat_history/*"
|
||||
)
|
||||
|
||||
# 构建排除参数
|
||||
EXCLUDE_ARGS=""
|
||||
for pattern in "${EXCLUDE_PATTERNS[@]}"; do
|
||||
EXCLUDE_ARGS="$EXCLUDE_ARGS -x $pattern"
|
||||
done
|
||||
|
||||
# 创建zip包
|
||||
log_info "正在打包文件..."
|
||||
eval "zip -r \"$TEMP_ZIP\" . $EXCLUDE_ARGS"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "代码包创建成功: $TEMP_ZIP"
|
||||
log_info "包大小: $(du -h "$TEMP_ZIP" | cut -f1)"
|
||||
else
|
||||
log_error "代码包创建失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试SSH连接
|
||||
test_ssh_connection() {
|
||||
log_step "测试SSH连接..."
|
||||
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 "$REMOTE_USER@$REMOTE_HOST" "echo 'SSH连接测试成功'" 2>/dev/null
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "SSH连接测试成功"
|
||||
else
|
||||
log_error "SSH连接失败,请检查服务器地址、用户名和密码"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 上传代码包
|
||||
upload_package() {
|
||||
log_step "上传代码包到服务器..."
|
||||
|
||||
# 使用scp上传文件
|
||||
sshpass -p "$REMOTE_PASS" scp -o StrictHostKeyChecking=no "$TEMP_ZIP" "$REMOTE_USER@$REMOTE_HOST:/tmp/"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "代码包上传成功"
|
||||
else
|
||||
log_error "代码包上传失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 远程部署
|
||||
remote_deploy() {
|
||||
log_step "在远程服务器上执行部署..."
|
||||
|
||||
# 上传远程部署脚本
|
||||
sshpass -p "$REMOTE_PASS" scp -o StrictHostKeyChecking=no "remote_deploy_script.sh" "$REMOTE_USER@$REMOTE_HOST:/tmp/"
|
||||
|
||||
# 执行远程部署脚本
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" "chmod +x /tmp/remote_deploy_script.sh && /tmp/remote_deploy_script.sh '$ZIP_NAME'"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "远程部署完成"
|
||||
else
|
||||
log_error "远程部署失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理本地临时文件
|
||||
cleanup() {
|
||||
log_step "清理临时文件..."
|
||||
rm -f "$TEMP_ZIP"
|
||||
log_info "临时文件清理完成"
|
||||
}
|
||||
|
||||
# 显示部署信息
|
||||
show_deploy_info() {
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 部署完成信息"
|
||||
echo "=========================================="
|
||||
echo "服务器地址: $REMOTE_HOST"
|
||||
echo "部署目录: $REMOTE_DIR"
|
||||
echo "应用端口: $APP_PORT"
|
||||
echo "访问地址: http://$REMOTE_HOST:$APP_PORT"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "常用管理命令:"
|
||||
echo "查看应用状态: ssh $REMOTE_USER@$REMOTE_HOST 'sudo supervisorctl status host_message'"
|
||||
echo "重启应用: ssh $REMOTE_USER@$REMOTE_HOST 'sudo supervisorctl restart host_message'"
|
||||
echo "停止应用: ssh $REMOTE_USER@$REMOTE_HOST 'sudo supervisorctl stop host_message'"
|
||||
echo "查看日志: ssh $REMOTE_USER@$REMOTE_HOST 'tail -f $REMOTE_DIR/logs/supervisor.log'"
|
||||
echo "调试应用: ssh $REMOTE_USER@$REMOTE_HOST 'cd $REMOTE_DIR && ./debug_app.sh'"
|
||||
echo "=========================================="
|
||||
|
||||
# 最后验证部署
|
||||
log_info "正在验证部署结果..."
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" "
|
||||
echo '=== 最终验证 ==='
|
||||
sudo supervisorctl status host_message
|
||||
echo '=== 端口检查 ==='
|
||||
netstat -tlnp | grep 8888 || ss -tlnp | grep 8888 || echo '端口8888未监听'
|
||||
echo '=== 进程检查 ==='
|
||||
ps aux | grep 'python3 main.py' | grep -v grep || echo '未找到Python进程'
|
||||
" || log_warn "最终验证失败,请手动检查"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
echo "=========================================="
|
||||
echo " host_message 项目自动部署脚本"
|
||||
echo "=========================================="
|
||||
echo "目标服务器: $REMOTE_HOST"
|
||||
echo "部署目录: $REMOTE_DIR"
|
||||
echo "开始时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 执行部署步骤
|
||||
check_dependencies
|
||||
create_package
|
||||
test_ssh_connection
|
||||
upload_package
|
||||
remote_deploy
|
||||
cleanup
|
||||
show_deploy_info
|
||||
|
||||
log_info "🎉 全部部署任务完成!"
|
||||
}
|
||||
|
||||
# 错误处理
|
||||
set -e
|
||||
trap 'log_error "脚本执行过程中发生错误,退出码: $?"' ERR
|
||||
|
||||
# 执行主函数
|
||||
main "$@"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
25
docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8888:8888"
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
environment:
|
||||
- FORWARDED_ALLOW_IPS=*
|
||||
- PROXY_HEADERS=1
|
||||
restart: unless-stopped
|
||||
|
||||
# 可选:添加nginx反向代理来更好地处理真实IP
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "88:88"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- /mnt/server/host_message_files:/mnt/server/host_message_files
|
||||
depends_on:
|
||||
- web
|
||||
restart: unless-stopped
|
||||
BIN
image/avatar/ag.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
image/avatar/azhi.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
image/avatar/changfeng.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
image/avatar/hongzong.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
image/avatar/liwei.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
image/avatar/server.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
image/avatar/xiaoji.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
image/logo2.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
image/logo_w.ico
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
57
nginx.conf
Normal file
@@ -0,0 +1,57 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
upstream fastapi_backend {
|
||||
server web:8000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# 设置客户端最大体上传大小 (用于文件上传)
|
||||
client_max_body_size 100M;
|
||||
|
||||
# 代理配置
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket 支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
location / {
|
||||
proxy_pass http://fastapi_backend;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# WebSocket 特殊处理
|
||||
location /ws {
|
||||
proxy_pass http://fastapi_backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket 超时设置
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# 静态文件处理
|
||||
location /uploads/ {
|
||||
proxy_pass http://fastapi_backend;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
pre_deploy_check.sh
Normal file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 部署前检查脚本
|
||||
echo "=========================================="
|
||||
echo " 部署前环境检查"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查本地环境
|
||||
echo "1. 检查本地环境..."
|
||||
echo " - 当前目录: $(pwd)"
|
||||
echo " - 主文件存在: $(test -f main.py && echo "✅ main.py存在" || echo "❌ main.py不存在")"
|
||||
echo " - 依赖文件存在: $(test -f requirements.txt && echo "✅ requirements.txt存在" || echo "❌ requirements.txt不存在")"
|
||||
echo " - zip命令: $(command -v zip >/dev/null && echo "✅ 已安装" || echo "❌ 未安装")"
|
||||
|
||||
# 检查sshpass
|
||||
if command -v sshpass >/dev/null; then
|
||||
echo " - sshpass: ✅ 已安装"
|
||||
else
|
||||
echo " - sshpass: ❌ 未安装"
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
echo " 安装命令: brew install sshpass"
|
||||
else
|
||||
echo " 安装命令: sudo apt-get install sshpass"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "2. 检查目标服务器连接..."
|
||||
REMOTE_HOST="6.6.6.86"
|
||||
REMOTE_USER="ubuntu"
|
||||
REMOTE_PASS="qweasdzxc1"
|
||||
|
||||
if command -v sshpass >/dev/null; then
|
||||
echo " - 测试SSH连接..."
|
||||
if sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 "$REMOTE_USER@$REMOTE_HOST" "echo 'SSH连接正常'" 2>/dev/null; then
|
||||
echo " - SSH连接: ✅ 正常"
|
||||
|
||||
# 检查服务器系统信息
|
||||
echo " - 服务器信息:"
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" "
|
||||
echo ' 操作系统: '$(lsb_release -d 2>/dev/null | cut -f2 || echo 'Unknown')
|
||||
echo ' Python版本: '$(python3 --version 2>/dev/null || echo '未安装')
|
||||
echo ' 磁盘空间: '$(df -h / | tail -1 | awk '{print \$4}' || echo '未知') 可用
|
||||
" 2>/dev/null
|
||||
else
|
||||
echo " - SSH连接: ❌ 失败"
|
||||
echo " 请检查服务器地址、用户名和密码"
|
||||
fi
|
||||
else
|
||||
echo " - SSH连接: ⚠️ 跳过 (sshpass未安装)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "3. 检查项目文件..."
|
||||
echo " - 项目大小: $(du -sh . 2>/dev/null | cut -f1 || echo "未知")"
|
||||
echo " - 关键文件检查:"
|
||||
echo " * main.py: $(test -f main.py && echo "✅ 存在" || echo "❌ 缺失")"
|
||||
echo " * requirements.txt: $(test -f requirements.txt && echo "✅ 存在" || echo "❌ 缺失")"
|
||||
echo " * database/ip_list.json: $(test -f database/ip_list.json && echo "✅ 存在" || echo "❌ 缺失")"
|
||||
|
||||
if [ -f "requirements.txt" ]; then
|
||||
echo " - Python依赖包:"
|
||||
while IFS= read -r line; do
|
||||
if [[ ! $line =~ ^[[:space:]]*# ]] && [[ ! -z $line ]]; then
|
||||
echo " * $line"
|
||||
fi
|
||||
done < requirements.txt
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "检查完成!如果所有项目都显示 ✅,您可以运行:"
|
||||
echo "./deploy.sh"
|
||||
echo "=========================================="
|
||||
415
remote_deploy_script.sh
Normal file
@@ -0,0 +1,415 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 设置颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[远程INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[远程ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[远程WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||
}
|
||||
|
||||
# 检查端口占用并杀死进程
|
||||
check_and_kill_port() {
|
||||
log_info "检查端口 8888 是否被占用..."
|
||||
|
||||
# 查找占用端口的进程
|
||||
PID=$(lsof -ti:8888 2>/dev/null)
|
||||
|
||||
if [ ! -z "$PID" ]; then
|
||||
log_warn "发现端口 8888 被进程 $PID 占用,正在终止..."
|
||||
kill -TERM $PID
|
||||
sleep 3
|
||||
|
||||
# 检查进程是否还存在
|
||||
if kill -0 $PID 2>/dev/null; then
|
||||
log_warn "进程仍然存在,强制终止..."
|
||||
kill -KILL $PID
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# 再次检查
|
||||
NEW_PID=$(lsof -ti:8888 2>/dev/null)
|
||||
if [ ! -z "$NEW_PID" ]; then
|
||||
log_error "无法终止占用端口 8888 的进程"
|
||||
exit 1
|
||||
else
|
||||
log_info "端口 8888 已释放"
|
||||
fi
|
||||
else
|
||||
log_info "端口 8888 未被占用"
|
||||
fi
|
||||
}
|
||||
|
||||
# 检查环境是否已安装
|
||||
check_environment() {
|
||||
log_info "检查服务器环境..."
|
||||
|
||||
# 检查Python3
|
||||
if ! python3 --version &> /dev/null; then
|
||||
log_info "Python3 未安装,需要安装..."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 检查是否存在虚拟环境
|
||||
if [ -d "/home/ubuntu/host_message_venv" ]; then
|
||||
log_info "发现已存在的虚拟环境"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# 检查supervisor
|
||||
if ! command -v supervisorctl &> /dev/null; then
|
||||
log_info "Supervisor 未安装,需要安装..."
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "基础环境检查通过,但需要创建虚拟环境"
|
||||
return 2
|
||||
}
|
||||
|
||||
# 安装系统依赖
|
||||
install_dependencies() {
|
||||
log_info "安装系统依赖..."
|
||||
|
||||
# 更新包列表
|
||||
sudo apt-get update -qq
|
||||
|
||||
# 安装Python3、pip和相关开发工具
|
||||
log_info "安装 Python3 和相关工具..."
|
||||
sudo apt-get install -y python3 python3-pip python3-venv python3-dev build-essential python3-full
|
||||
|
||||
# 安装supervisor用于进程保活
|
||||
if ! command -v supervisorctl &> /dev/null; then
|
||||
log_info "安装 supervisor..."
|
||||
sudo apt-get install -y supervisor
|
||||
sudo systemctl enable supervisor
|
||||
sudo systemctl start supervisor
|
||||
fi
|
||||
|
||||
# 安装其他必要工具
|
||||
sudo apt-get install -y curl wget unzip lsof net-tools
|
||||
|
||||
log_info "系统依赖安装完成"
|
||||
}
|
||||
|
||||
# 创建和管理虚拟环境
|
||||
setup_virtual_environment() {
|
||||
local venv_path="/home/ubuntu/host_message_venv"
|
||||
|
||||
log_info "设置Python虚拟环境..."
|
||||
|
||||
# 如果虚拟环境已存在,询问是否重新创建
|
||||
if [ -d "$venv_path" ]; then
|
||||
log_info "虚拟环境已存在,删除旧环境并重新创建..."
|
||||
rm -rf "$venv_path"
|
||||
fi
|
||||
|
||||
# 创建虚拟环境
|
||||
log_info "创建新的虚拟环境..."
|
||||
python3 -m venv "$venv_path"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "虚拟环境创建成功: $venv_path"
|
||||
else
|
||||
log_error "虚拟环境创建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 激活虚拟环境并升级pip
|
||||
log_info "激活虚拟环境并升级pip..."
|
||||
source "$venv_path/bin/activate"
|
||||
pip install --upgrade pip setuptools wheel
|
||||
|
||||
log_info "虚拟环境设置完成"
|
||||
}
|
||||
|
||||
# 主要部署逻辑
|
||||
main_deploy() {
|
||||
# 检查并终止端口进程
|
||||
check_and_kill_port
|
||||
|
||||
# 检查环境状态
|
||||
check_environment
|
||||
env_status=$?
|
||||
|
||||
case $env_status in
|
||||
0)
|
||||
log_info "环境已完整安装,跳过依赖安装"
|
||||
;;
|
||||
1)
|
||||
log_info "需要安装基础环境"
|
||||
install_dependencies
|
||||
setup_virtual_environment
|
||||
;;
|
||||
2)
|
||||
log_info "基础环境已安装,只需创建虚拟环境"
|
||||
setup_virtual_environment
|
||||
;;
|
||||
esac
|
||||
|
||||
# 备份旧代码(如果存在)
|
||||
if [ -d "/home/ubuntu/host_message" ]; then
|
||||
log_info "备份现有代码..."
|
||||
sudo mv "/home/ubuntu/host_message" "/home/ubuntu/host_message_backup_$(date +%Y%m%d_%H%M%S)" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 创建部署目录
|
||||
log_info "创建部署目录..."
|
||||
sudo mkdir -p "/home/ubuntu/host_message"
|
||||
sudo chown $USER:$USER "/home/ubuntu/host_message"
|
||||
|
||||
# 解压代码包
|
||||
log_info "解压代码包..."
|
||||
cd "/home/ubuntu/host_message"
|
||||
unzip -q "/tmp/$1"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "代码解压成功"
|
||||
else
|
||||
log_error "代码解压失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建必要的目录
|
||||
mkdir -p logs uploads chat_history database
|
||||
|
||||
# 激活虚拟环境并安装Python依赖
|
||||
log_info "激活虚拟环境并安装Python依赖..."
|
||||
source "/home/ubuntu/host_message_venv/bin/activate"
|
||||
|
||||
if [ -f "requirements.txt" ]; then
|
||||
log_info "发现 requirements.txt,安装依赖包..."
|
||||
pip install -r requirements.txt
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "Python依赖安装成功"
|
||||
else
|
||||
log_error "Python依赖安装失败"
|
||||
# 尝试单独安装每个包
|
||||
log_info "尝试单独安装依赖包..."
|
||||
while IFS= read -r package; do
|
||||
if [[ ! $package =~ ^[[:space:]]*# ]] && [[ ! -z $package ]]; then
|
||||
log_info "安装: $package"
|
||||
pip install $package || log_warn "安装 $package 失败"
|
||||
fi
|
||||
done < requirements.txt
|
||||
fi
|
||||
else
|
||||
log_warn "未找到 requirements.txt 文件"
|
||||
fi
|
||||
|
||||
# 验证关键依赖
|
||||
log_info "验证Python依赖..."
|
||||
python -c "import fastapi, uvicorn; print('关键依赖验证成功')" || {
|
||||
log_error "关键依赖验证失败,手动安装..."
|
||||
pip install fastapi uvicorn
|
||||
}
|
||||
|
||||
# 检查main.py文件
|
||||
if [ ! -f "main.py" ]; then
|
||||
log_error "main.py 文件不存在!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 测试Python应用是否可以启动
|
||||
log_info "测试Python应用..."
|
||||
source "/home/ubuntu/host_message_venv/bin/activate"
|
||||
timeout 10 python -c "
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
try:
|
||||
import main
|
||||
print('应用模块导入成功')
|
||||
except Exception as e:
|
||||
print(f'应用模块导入失败: {e}')
|
||||
sys.exit(1)
|
||||
" || log_warn "应用模块测试失败,但继续部署..."
|
||||
|
||||
# 创建启动脚本
|
||||
log_info "创建应用启动脚本..."
|
||||
cat > start_app.sh <<'SCRIPT_EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# 进入应用目录
|
||||
cd "/home/ubuntu/host_message"
|
||||
|
||||
# 激活虚拟环境
|
||||
source "/home/ubuntu/host_message_venv/bin/activate"
|
||||
|
||||
# 设置环境变量
|
||||
export PYTHONPATH="/home/ubuntu/host_message:$PYTHONPATH"
|
||||
|
||||
# 记录启动信息
|
||||
echo "[$(date)] 应用启动开始..."
|
||||
echo "当前目录: $(pwd)"
|
||||
echo "Python版本: $(python --version)"
|
||||
echo "虚拟环境: $VIRTUAL_ENV"
|
||||
|
||||
# 检查必要文件
|
||||
if [ ! -f "main.py" ]; then
|
||||
echo "错误: main.py 文件不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 启动应用
|
||||
echo "[$(date)] 启动Python应用..."
|
||||
python main.py
|
||||
SCRIPT_EOF
|
||||
chmod +x start_app.sh
|
||||
|
||||
# 创建调试脚本
|
||||
log_info "创建调试脚本..."
|
||||
cat > debug_app.sh <<'DEBUG_EOF'
|
||||
#!/bin/bash
|
||||
cd "/home/ubuntu/host_message"
|
||||
echo "=== 调试信息 ==="
|
||||
echo "当前目录: $(pwd)"
|
||||
|
||||
# 激活虚拟环境
|
||||
source "/home/ubuntu/host_message_venv/bin/activate" 2>/dev/null || echo "虚拟环境激活失败"
|
||||
|
||||
echo "Python版本: $(python --version 2>/dev/null || python3 --version)"
|
||||
echo "pip版本: $(pip --version 2>/dev/null || echo '无pip')"
|
||||
echo "虚拟环境: $VIRTUAL_ENV"
|
||||
echo "文件列表:"
|
||||
ls -la
|
||||
echo "=== 尝试导入测试 ==="
|
||||
python -c "
|
||||
import sys
|
||||
print('Python路径:', sys.path)
|
||||
try:
|
||||
import fastapi
|
||||
print('fastapi版本:', fastapi.__version__)
|
||||
except ImportError as e:
|
||||
print('fastapi导入失败:', e)
|
||||
try:
|
||||
import uvicorn
|
||||
print('uvicorn版本:', uvicorn.__version__)
|
||||
except ImportError as e:
|
||||
print('uvicorn导入失败:', e)
|
||||
" 2>/dev/null || python3 -c "
|
||||
import sys
|
||||
print('Python路径:', sys.path)
|
||||
try:
|
||||
import fastapi
|
||||
print('fastapi版本:', fastapi.__version__)
|
||||
except ImportError as e:
|
||||
print('fastapi导入失败:', e)
|
||||
try:
|
||||
import uvicorn
|
||||
print('uvicorn版本:', uvicorn.__version__)
|
||||
except ImportError as e:
|
||||
print('uvicorn导入失败:', e)
|
||||
"
|
||||
echo "=== 检查端口占用 ==="
|
||||
netstat -tlnp | grep 8888 || echo '端口8888未被占用'
|
||||
echo "=== 尝试启动应用(5秒后停止) ==="
|
||||
timeout 5 python main.py 2>/dev/null || timeout 5 python3 main.py || echo '应用启动测试完成'
|
||||
DEBUG_EOF
|
||||
chmod +x debug_app.sh
|
||||
|
||||
# 创建supervisor配置
|
||||
log_info "配置进程保活监控..."
|
||||
sudo tee /etc/supervisor/conf.d/host_message.conf > /dev/null <<SUPERVISOR_EOF
|
||||
[program:host_message]
|
||||
command=/home/ubuntu/host_message/start_app.sh
|
||||
directory=/home/ubuntu/host_message
|
||||
user=ubuntu
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/home/ubuntu/host_message/logs/supervisor.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=3
|
||||
environment=PATH="/home/ubuntu/host_message_venv/bin:/usr/local/bin:/usr/bin:/bin",PYTHONPATH="/home/ubuntu/host_message"
|
||||
startsecs=10
|
||||
startretries=3
|
||||
SUPERVISOR_EOF
|
||||
|
||||
# 停止可能存在的旧进程
|
||||
sudo supervisorctl stop host_message 2>/dev/null || true
|
||||
|
||||
# 重新加载supervisor配置
|
||||
sudo supervisorctl reread
|
||||
sudo supervisorctl update
|
||||
|
||||
# 启动应用
|
||||
log_info "启动应用..."
|
||||
sudo supervisorctl start host_message
|
||||
|
||||
# 等待应用启动并检查状态
|
||||
log_info "等待应用启动..."
|
||||
for i in {1..30}; do
|
||||
sleep 2
|
||||
STATUS=$(sudo supervisorctl status host_message 2>/dev/null || echo "ERROR")
|
||||
log_info "第 $i 次检查: $STATUS"
|
||||
|
||||
if echo "$STATUS" | grep -q "RUNNING"; then
|
||||
log_info "应用启动成功!"
|
||||
break
|
||||
elif echo "$STATUS" | grep -q "FATAL\|BACKOFF"; then
|
||||
log_error "应用启动失败: $STATUS"
|
||||
log_error "查看详细日志:"
|
||||
tail -20 "/home/ubuntu/host_message/logs/supervisor.log" 2>/dev/null || echo "无法读取日志文件"
|
||||
|
||||
# 运行调试脚本
|
||||
log_info "运行调试脚本获取详细信息:"
|
||||
cd "/home/ubuntu/host_message"
|
||||
./debug_app.sh 2>&1 || true
|
||||
|
||||
# 检查supervisor错误日志
|
||||
log_info "检查supervisor错误日志:"
|
||||
sudo tail -20 /var/log/supervisor/supervisord.log 2>/dev/null || echo "无法读取supervisor日志"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $i -eq 30 ]; then
|
||||
log_error "应用启动超时"
|
||||
log_error "最终状态: $STATUS"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# 检查端口监听
|
||||
log_info "检查端口监听状态..."
|
||||
for i in {1..10}; do
|
||||
if netstat -tlnp 2>/dev/null | grep -q ":8888" || ss -tlnp 2>/dev/null | grep -q ":8888"; then
|
||||
log_info "端口 8888 监听正常"
|
||||
log_info "部署完成!您可以通过 http://6.6.6.86:8888 访问应用"
|
||||
break
|
||||
else
|
||||
log_warn "等待端口 8888 开始监听... ($i/10)"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
if [ $i -eq 10 ]; then
|
||||
log_warn "端口 8888 未在监听,请检查应用日志"
|
||||
log_info "当前监听的端口:"
|
||||
netstat -tlnp 2>/dev/null | grep LISTEN || ss -tlnp 2>/dev/null | grep LISTEN || echo "无法获取监听端口信息"
|
||||
fi
|
||||
done
|
||||
|
||||
# 清理临时文件
|
||||
rm -f "/tmp/$1"
|
||||
|
||||
log_info "部署脚本执行完成"
|
||||
}
|
||||
|
||||
# 执行主要部署逻辑
|
||||
main_deploy "$1"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
7
requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn[standard]==0.27.0
|
||||
python-multipart==0.0.6
|
||||
websockets==12.0
|
||||
jinja2==3.1.2
|
||||
aiofiles==23.2.0
|
||||
python-json-logger==2.0.7
|
||||
172
start.sh
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 定义常量
|
||||
PORT=8888
|
||||
LOG_FILE="logs/$(date +%Y%m%d_%H%M%S)_message.log"
|
||||
PID_FILE="logs/app.pid"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p "$SCRIPT_DIR/logs"
|
||||
|
||||
# 切换到脚本目录
|
||||
cd "$SCRIPT_DIR" || {
|
||||
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] 错误: 无法进入目录 $SCRIPT_DIR" | tee -a "$LOG_FILE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 日志输出函数
|
||||
log_info() {
|
||||
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] [INFO] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] [ERROR] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo "[$(date +%Y-%m-%d\ %H:%M:%S)] [WARN] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# 检查端口是否被占用
|
||||
check_port() {
|
||||
local port=$1
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
lsof -i :$port >/dev/null 2>&1
|
||||
elif command -v netstat >/dev/null 2>&1; then
|
||||
netstat -tuln | grep ":$port " >/dev/null 2>&1
|
||||
elif command -v ss >/dev/null 2>&1; then
|
||||
ss -tuln | grep ":$port " >/dev/null 2>&1
|
||||
else
|
||||
log_error "无法找到检查端口的命令 (lsof, netstat, ss)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 健康检查函数
|
||||
health_check() {
|
||||
local url="http://localhost:$PORT"
|
||||
local timeout=5
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -s --connect-timeout $timeout --max-time $timeout "$url" >/dev/null 2>&1
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -q --timeout=$timeout --tries=1 -O /dev/null "$url" >/dev/null 2>&1
|
||||
else
|
||||
log_warn "无法找到HTTP检查工具 (curl, wget),跳过健康检查"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 停止现有进程
|
||||
stop_existing_process() {
|
||||
log_info "停止现有进程..."
|
||||
|
||||
# 从PID文件停止
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
local pid=$(cat "$PID_FILE")
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
log_info "正在停止进程 PID: $pid"
|
||||
kill "$pid"
|
||||
sleep 2
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
log_warn "进程仍在运行,强制终止"
|
||||
kill -9 "$pid"
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
else
|
||||
log_info "PID文件中的进程已不存在,清理PID文件"
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 通过端口查找并停止进程
|
||||
if check_port $PORT; then
|
||||
log_info "发现端口 $PORT 仍被占用,查找并停止相关进程"
|
||||
if command -v lsof >/dev/null 2>&1; then
|
||||
local pids=$(lsof -ti :$PORT)
|
||||
if [ -n "$pids" ]; then
|
||||
echo "$pids" | xargs kill -15 2>/dev/null || true
|
||||
sleep 2
|
||||
# 如果进程仍在运行,强制杀死
|
||||
local remaining_pids=$(lsof -ti :$PORT 2>/dev/null)
|
||||
if [ -n "$remaining_pids" ]; then
|
||||
echo "$remaining_pids" | xargs kill -9 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动应用
|
||||
start_app() {
|
||||
log_info "开始启动应用..."
|
||||
|
||||
# 后台运行Python程序
|
||||
nohup python main.py >> "$LOG_FILE" 2>&1 &
|
||||
local app_pid=$!
|
||||
|
||||
# 保存PID
|
||||
echo $app_pid > "$PID_FILE"
|
||||
log_info "应用已启动,PID: $app_pid"
|
||||
|
||||
# 等待应用启动
|
||||
local max_wait=30
|
||||
local wait_time=0
|
||||
|
||||
while [ $wait_time -lt $max_wait ]; do
|
||||
if check_port $PORT; then
|
||||
log_info "端口 $PORT 已开始监听"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
wait_time=$((wait_time + 1))
|
||||
done
|
||||
|
||||
if [ $wait_time -ge $max_wait ]; then
|
||||
log_error "应用启动超时,端口 $PORT 未开始监听"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 健康检查
|
||||
sleep 2
|
||||
if health_check; then
|
||||
log_info "应用健康检查通过"
|
||||
return 0
|
||||
else
|
||||
log_warn "应用健康检查失败,但端口已监听"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# 主逻辑
|
||||
main() {
|
||||
log_info "开始检查应用状态..."
|
||||
|
||||
# 检查端口是否被占用
|
||||
if check_port $PORT; then
|
||||
log_info "检测到端口 $PORT 已被占用"
|
||||
|
||||
# 进行健康检查
|
||||
if health_check; then
|
||||
log_info "应用正常运行,端口 $PORT 可正常访问,跳过重启"
|
||||
exit 0
|
||||
else
|
||||
log_warn "端口 $PORT 被占用但健康检查失败,准备重启应用"
|
||||
stop_existing_process
|
||||
fi
|
||||
else
|
||||
log_info "端口 $PORT 未被占用,准备启动应用"
|
||||
fi
|
||||
|
||||
# 启动应用
|
||||
if start_app; then
|
||||
log_info "应用启动成功"
|
||||
else
|
||||
log_error "应用启动失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main
|
||||
6058
templates/chat.html
Normal file
6359
templates/index.html
Normal file
458
templates/login.html
Normal file
@@ -0,0 +1,458 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - 局域网聊天室</title>
|
||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #667eea;
|
||||
--secondary-color: #764ba2;
|
||||
--background-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
--success-color: #4ade80;
|
||||
--error-color: #ef4444;
|
||||
--text-dark: #1f2937;
|
||||
--text-light: #6b7280;
|
||||
--border-color: #e5e7eb;
|
||||
--shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--background-gradient);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
padding: 48px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6px;
|
||||
background: var(--background-gradient);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: var(--text-dark);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: var(--text-light);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
color: var(--text-dark);
|
||||
font-weight: 500;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 16px 20px 16px 48px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: #fafafa;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-light);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: var(--background-gradient);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.login-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: var(--error-color);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
font-size: 0.9rem;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
text-align: center;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.back-link a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.back-link a:hover {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.features {
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.feature-item i {
|
||||
color: var(--success-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 480px) {
|
||||
.login-container {
|
||||
padding: 32px 24px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
.login-container {
|
||||
animation: slideUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 输入框动画 */
|
||||
.input-wrapper {
|
||||
animation: fadeIn 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
animation: fadeIn 0.8s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
animation: fadeIn 0.8s ease-out 0.6s both;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1><i class="bi bi-chat-dots-fill"></i> 局域网聊天室</h1>
|
||||
<p>请输入您的用户名开始聊天</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="error-message" id="errorMessage"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">用户名</label>
|
||||
<div class="input-wrapper">
|
||||
<i class="bi bi-person-fill input-icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="请输入用户名(1-20个字符)"
|
||||
maxlength="20"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-btn" id="loginBtn">
|
||||
<span class="loading-spinner" id="loadingSpinner"></span>
|
||||
<span id="btnText">进入聊天室</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature-list">
|
||||
<div class="feature-item">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
<span>安全可靠</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="bi bi-lightning-fill"></i>
|
||||
<span>实时聊天</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="bi bi-file-earmark-arrow-up"></i>
|
||||
<span>文件传输</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="bi bi-people-fill"></i>
|
||||
<span>群聊私聊</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="back-link">
|
||||
<a href="/">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
返回文件传输
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const usernameInput = document.getElementById('username');
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const loadingSpinner = document.getElementById('loadingSpinner');
|
||||
const btnText = document.getElementById('btnText');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
|
||||
// 自动聚焦用户名输入框
|
||||
usernameInput.focus();
|
||||
|
||||
// 表单提交处理
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = usernameInput.value.trim();
|
||||
if (!username) {
|
||||
showError('请输入用户名');
|
||||
return;
|
||||
}
|
||||
|
||||
if (username.length > 20) {
|
||||
showError('用户名长度不能超过20个字符');
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始登录
|
||||
setLoading(true);
|
||||
hideError();
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('username', username);
|
||||
|
||||
const response = await fetch('/login', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// 登录成功,保存session并跳转
|
||||
localStorage.setItem('session_id', data.session_id);
|
||||
localStorage.setItem('username', data.username);
|
||||
|
||||
// 跳转到聊天页面
|
||||
window.location.href = '/chat';
|
||||
} else {
|
||||
showError(data.detail || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
showError('网络错误,请稍后重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 回车键提交
|
||||
usernameInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
loginForm.dispatchEvent(new Event('submit'));
|
||||
}
|
||||
});
|
||||
|
||||
// 实时验证用户名长度
|
||||
usernameInput.addEventListener('input', (e) => {
|
||||
const value = e.target.value;
|
||||
if (value.length > 20) {
|
||||
showError('用户名长度不能超过20个字符');
|
||||
} else {
|
||||
hideError();
|
||||
}
|
||||
});
|
||||
|
||||
function setLoading(loading) {
|
||||
loginBtn.disabled = loading;
|
||||
loadingSpinner.style.display = loading ? 'inline-block' : 'none';
|
||||
btnText.textContent = loading ? '登录中...' : '进入聊天室';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorMessage.style.display = 'block';
|
||||
usernameInput.focus();
|
||||
}
|
||||
|
||||
function hideError() {
|
||||
errorMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
// 检查是否已经登录
|
||||
window.addEventListener('load', async () => {
|
||||
const sessionId = localStorage.getItem('session_id');
|
||||
const username = localStorage.getItem('username');
|
||||
|
||||
if (sessionId && username) {
|
||||
try {
|
||||
const response = await fetch(`/check_session?session_id=${sessionId}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.valid) {
|
||||
// 会话有效,直接跳转到聊天页面
|
||||
window.location.href = '/chat';
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('会话检查失败:', error);
|
||||
}
|
||||
|
||||
// 清理无效的会话信息
|
||||
localStorage.removeItem('session_id');
|
||||
localStorage.removeItem('username');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
125
test_chat.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>聊天测试</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
#messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 10px; margin-bottom: 10px; }
|
||||
#messageInput { width: 300px; padding: 5px; }
|
||||
button { padding: 5px 10px; margin: 5px; }
|
||||
.status { color: green; }
|
||||
.error { color: red; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>聊天测试</h1>
|
||||
<div id="status">未连接</div>
|
||||
<div id="messages"></div>
|
||||
<input type="text" id="messageInput" placeholder="输入消息...">
|
||||
<button onclick="sendMessage()">发送</button>
|
||||
<button onclick="connectWebSocket()">重新连接</button>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
let myIp = 'unknown';
|
||||
|
||||
async function getMyIp() {
|
||||
try {
|
||||
const response = await fetch('/get_ip');
|
||||
const data = await response.json();
|
||||
myIp = data.ip;
|
||||
console.log('My IP:', myIp);
|
||||
document.getElementById('status').innerHTML = `IP: ${myIp} - 准备连接...`;
|
||||
} catch (error) {
|
||||
console.error('获取IP失败:', error);
|
||||
document.getElementById('status').innerHTML = '<span class="error">获取IP失败</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
if (ws) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
ws = new WebSocket(`ws://${window.location.host}/ws`);
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('WebSocket连接已建立');
|
||||
document.getElementById('status').innerHTML = `<span class="status">已连接 - IP: ${myIp}</span>`;
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('收到消息:', data);
|
||||
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.innerHTML = `
|
||||
<strong>${data.ip}:</strong> ${data.message}
|
||||
<small>(${new Date(data.timestamp).toLocaleTimeString()})</small>
|
||||
`;
|
||||
messagesDiv.appendChild(messageElement);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
console.log('WebSocket连接已关闭');
|
||||
document.getElementById('status').innerHTML = '<span class="error">连接已断开</span>';
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket错误:', error);
|
||||
document.getElementById('status').innerHTML = '<span class="error">连接错误</span>';
|
||||
};
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const message = messageInput.value.trim();
|
||||
|
||||
if (!message || !ws || ws.readyState !== WebSocket.OPEN) {
|
||||
alert('请输入消息并确保连接正常');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageData = {
|
||||
message: message,
|
||||
timestamp: new Date().getTime(),
|
||||
type: 'text'
|
||||
};
|
||||
|
||||
try {
|
||||
ws.send(JSON.stringify(messageData));
|
||||
messageInput.value = '';
|
||||
|
||||
// 显示自己的消息
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const messageElement = document.createElement('div');
|
||||
messageElement.innerHTML = `
|
||||
<strong>我 (${myIp}):</strong> ${message}
|
||||
<small>(${new Date().toLocaleTimeString()})</small>
|
||||
`;
|
||||
messageElement.style.color = 'blue';
|
||||
messagesDiv.appendChild(messageElement);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
alert('发送消息失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 回车键发送消息
|
||||
document.getElementById('messageInput').addEventListener('keypress', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载时初始化
|
||||
window.addEventListener('load', async function() {
|
||||
await getMyIp();
|
||||
connectWebSocket();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||