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