From 4d3163575c63b9632e2348569bde611f5cf24005 Mon Sep 17 00:00:00 2001 From: Jowe <123822645+Selei1983@users.noreply.github.com> Date: Wed, 31 Dec 2025 01:33:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0v2.2.0=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC=E5=92=8C=E5=B7=A5=E5=85=B7=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/settings.local.json | 6 +- 1PANEL_DEPLOY.md | 358 ++++ 1PANEL_QUICK.md | 180 ++ CHECK_GIT.md | 92 + DEPLOYMENT.md | 463 +++++ DEPLOY_CHECKLIST.md | 132 ++ GIT_PATCH_DEPLOY.md | 232 +++ MANUAL_DEPLOY.md | 195 ++ QUICK_DEPLOY.md | 94 + UPLOAD_FILES_v2.2.0.txt | 221 +++ deploy.sh | 96 + export_sites.py | 67 + export_urls.py | 33 + git_patch_deploy.sh | 93 + gunicorn_config.py | 44 + manage.sh | 61 + one_click_deploy.sh | 121 ++ quick_deploy_server.sh | 66 + static/logos/logo_5fda15d305a84866.jpg | Bin 0 -> 30205 bytes static/logos/logo_aipmclub_com.png | Bin 0 -> 14009 bytes templates/admin/change_password.html | 370 ++++ test_deepseek.py | 52 + v2.1.0.patch | 2438 ++++++++++++++++++++++++ wsgi.py | 15 + 24 files changed, 5428 insertions(+), 1 deletion(-) create mode 100644 1PANEL_DEPLOY.md create mode 100644 1PANEL_QUICK.md create mode 100644 CHECK_GIT.md create mode 100644 DEPLOYMENT.md create mode 100644 DEPLOY_CHECKLIST.md create mode 100644 GIT_PATCH_DEPLOY.md create mode 100644 MANUAL_DEPLOY.md create mode 100644 QUICK_DEPLOY.md create mode 100644 UPLOAD_FILES_v2.2.0.txt create mode 100644 deploy.sh create mode 100644 export_sites.py create mode 100644 export_urls.py create mode 100644 git_patch_deploy.sh create mode 100644 gunicorn_config.py create mode 100644 manage.sh create mode 100644 one_click_deploy.sh create mode 100644 quick_deploy_server.sh create mode 100644 static/logos/logo_5fda15d305a84866.jpg create mode 100644 static/logos/logo_aipmclub_com.png create mode 100644 templates/admin/change_password.html create mode 100644 test_deepseek.py create mode 100644 v2.1.0.patch create mode 100644 wsgi.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2a4bf4e..c7768c5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -28,7 +28,11 @@ "Bash(timeout /t 3 /nobreak)", "Bash(ping:*)", "Bash(git diff-tree:*)", - "Bash(git format-patch:*)" + "Bash(git format-patch:*)", + "WebFetch(domain:bocha-ai.feishu.cn)", + "Bash(ls:*)", + "Bash(git pull:*)", + "Bash(del nul)" ] } } diff --git a/1PANEL_DEPLOY.md b/1PANEL_DEPLOY.md new file mode 100644 index 0000000..f9e05ac --- /dev/null +++ b/1PANEL_DEPLOY.md @@ -0,0 +1,358 @@ +# 1Panel Web界面部署指南 + +## 使用1Panel Web界面部署ZJPB项目 + +### 前提条件 +- 1Panel已安装并可访问 +- 1Panel版本支持Python项目管理 + +--- + +## 方式一:使用1Panel的运行时环境(推荐) + +### 步骤1:准备项目文件 + +1. **压缩项目** + - 在本地压缩整个项目文件夹 + - 排除:`venv/`, `__pycache__/`, `.env`, `test_*.py`, `logs/` + +### 步骤2:在1Panel中创建数据库 + +1. 登录1Panel管理面板 +2. 进入 **数据库** 菜单 +3. 点击 **创建数据库** +4. 填写信息: + ``` + 数据库名:ai_nav + 用户名:ai_nav_user + 密码:(自动生成或自定义) + 权限:本地访问 + ``` +5. 点击确定,**记录数据库密码** + +### 步骤3:上传项目文件 + +1. 进入 **文件** 菜单 +2. 导航到网站目录(如 `/www/wwwroot/`) +3. 创建项目目录 `zjpb` +4. 上传并解压 `zjpb.zip` + +### 步骤4:配置环境变量 + +1. 在项目目录中,找到 `.env.example` 文件 +2. 复制为 `.env` +3. 点击编辑,填写配置: + +```env +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=ai_nav_user +DB_PASSWORD=你的数据库密码 +DB_NAME=ai_nav + +# 安全配置 +SECRET_KEY=你的密钥(使用下方命令生成) +FLASK_ENV=production + +# DeepSeek API(可选) +DEEPSEEK_API_KEY=sk-xxxxx +DEEPSEEK_BASE_URL=https://api.deepseek.com +``` + +生成SECRET_KEY: +```bash +# 在1Panel终端执行 +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +### 步骤5:使用1Panel创建Python项目 + +#### 5.1 进入网站管理 + +1. 点击 **网站** 菜单 +2. 点击 **创建网站** +3. 选择 **运行时** 类型 + +#### 5.2 配置Python项目 + +填写以下信息: + +**基本设置:** +``` +网站类型:运行时 +运行时:Python +域名:your-domain.com(或IP地址) +代码目录:/www/wwwroot/zjpb +``` + +**Python设置:** +``` +Python版本:选择 3.8+ 的版本 +应用类型:选择 "Flask" 或 "其他" +启动文件:app.py +启动命令:gunicorn -c gunicorn_config.py app:app +端口:5000(默认) +``` + +**环境变量:**(如果1Panel支持在界面配置) +``` +FLASK_ENV=production +``` + +**其他选项:** +``` +☑ 自动启动 +☑ 守护进程 +进程数:4 +``` + +#### 5.3 安装依赖 + +1. 创建网站后,1Panel会自动创建虚拟环境 +2. 进入网站设置 +3. 找到 **依赖管理** 或 **包管理** +4. 上传 `requirements.txt` 或手动安装 +5. 点击 **安装依赖** + +或者使用1Panel的终端: +```bash +# 进入项目目录 +cd /www/wwwroot/zjpb + +# 激活虚拟环境(1Panel自动创建的) +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt +``` + +### 步骤6:初始化数据库 + +1. 在1Panel中打开 **终端** 或 SSH连接 +2. 执行: +```bash +cd /www/wwwroot/zjpb +source venv/bin/activate +python init_db.py +``` + +### 步骤7:启动和管理 + +1. 在1Panel网站列表中找到你的项目 +2. 点击 **启动** 按钮 +3. 查看状态:运行中 ✓ + +### 步骤8:配置反向代理(如果需要) + +如果1Panel没有自动配置Nginx: + +1. 进入网站设置 +2. 找到 **反向代理** 或 **代理配置** +3. 配置: + ``` + 目标地址:http://127.0.0.1:5000 + ``` + +### 步骤9:配置SSL证书(推荐) + +1. 在网站设置中找到 **SSL** +2. 选择 **Let's Encrypt** +3. 点击申请证书 +4. 启用 **强制HTTPS** + +### 步骤10:访问验证 + +1. 前台:`https://your-domain.com` +2. 后台:`https://your-domain.com/admin/login` + - 默认用户名:`admin` + - 默认密码:`admin123` +3. **立即修改密码**:访问 `/admin/change-password` + +--- + +## 方式二:使用1Panel的OpenResty/Nginx + Supervisor + +如果1Panel的Python运行时不支持或不稳定,可以使用传统方式: + +### 步骤1-4:同上(数据库、文件上传、环境配置) + +### 步骤5:手动创建虚拟环境 + +```bash +cd /www/wwwroot/zjpb +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt +``` + +### 步骤6:创建Supervisor配置 + +1. 在1Panel中进入 **容器** 或 **进程管理** +2. 如果有Supervisor功能,创建新任务 + +或手动创建配置文件: +```bash +nano /etc/supervisor/conf.d/zjpb.conf +``` + +内容: +```ini +[program:zjpb] +command=/www/wwwroot/zjpb/venv/bin/gunicorn -c gunicorn_config.py app:app +directory=/www/wwwroot/zjpb +user=www +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=/www/wwwroot/zjpb/logs/supervisor.log +environment=FLASK_ENV="production" +``` + +启动: +```bash +supervisorctl reread +supervisorctl update +supervisorctl start zjpb +``` + +### 步骤7:创建反向代理网站 + +1. 在1Panel **网站** 菜单中点击 **创建网站** +2. 选择 **反向代理** +3. 填写: + ``` + 域名:your-domain.com + 代理地址:http://127.0.0.1:5000 + ``` + +### 步骤8:配置SSL + +同方式一 + +--- + +## 常见问题 + +### Q1: 1Panel在哪里创建Python项目? + +**A:** +- 进入 **网站** 菜单 +- 点击 **创建网站** +- 选择 **运行时** 或 **Runtime** +- 选择 **Python** + +### Q2: 找不到虚拟环境? + +**A:** +- 1Panel创建的虚拟环境通常在项目目录下的 `venv/` 文件夹 +- 路径:`/www/wwwroot/zjpb/venv/` +- 可以在文件管理器中查看 + +### Q3: 如何查看应用日志? + +**A:** +- 方式1:在网站管理中点击 **日志** 按钮 +- 方式2:查看文件 `logs/error.log` +- 方式3:使用1Panel的终端:`tail -f logs/error.log` + +### Q4: 如何重启应用? + +**A:** +- 方式1:在1Panel网站列表中点击 **重启** 按钮 +- 方式2:使用管理脚本:`./manage.sh restart` +- 方式3:Supervisor:`supervisorctl restart zjpb` + +### Q5: 端口被占用怎么办? + +**A:** +修改 `gunicorn_config.py` 中的端口: +```python +bind = "0.0.0.0:5001" # 改为其他端口 +``` +然后在反向代理中也修改为对应端口。 + +--- + +## 管理和维护 + +### 更新代码 + +1. 在1Panel文件管理中上传新文件 +2. 在网站管理中点击 **重启** + +### 查看状态 + +1. 进入网站列表 +2. 查看状态指示灯 +3. 点击网站名称查看详细信息 + +### 备份数据库 + +1. 进入 **数据库** 菜单 +2. 找到 `ai_nav` 数据库 +3. 点击 **备份** 按钮 + +### 监控日志 + +1. 在网站设置中找到 **日志** 选项 +2. 查看访问日志和错误日志 +3. 可以设置日志轮转 + +--- + +## 推荐配置 + +### 生产环境推荐配置 + +``` +服务器配置: +- CPU: 2核+ +- 内存: 2GB+ +- 硬盘: 20GB+ + +Python版本: +- Python 3.8+ + +数据库: +- MySQL 5.7+ +- MariaDB 10.3+ + +Web服务器: +- Nginx (1Panel自带) + +进程管理: +- Supervisor 或 1Panel内置 + +工作进程数: +- gunicorn workers: 4 +- gunicorn threads: 2 +``` + +--- + +## 安全建议 + +1. ✅ 修改默认管理员密码 +2. ✅ 使用强密码的SECRET_KEY +3. ✅ 启用HTTPS (Let's Encrypt) +4. ✅ 定期备份数据库 +5. ✅ 设置文件权限: + ```bash + chmod 600 .env + chmod 755 static/uploads + ``` +6. ✅ 配置防火墙(1Panel通常自动配置) + +--- + +## 获取帮助 + +如果遇到问题: +1. 查看 `logs/error.log` 日志文件 +2. 检查1Panel的系统日志 +3. 验证数据库连接 +4. 检查端口是否被占用 +5. 确认虚拟环境依赖已安装 + +祝部署顺利!🎉 diff --git a/1PANEL_QUICK.md b/1PANEL_QUICK.md new file mode 100644 index 0000000..7ff9f8b --- /dev/null +++ b/1PANEL_QUICK.md @@ -0,0 +1,180 @@ +# 1Panel快速部署向导 + +## 🎯 使用1Panel Web界面 - 5步完成部署 + +### 📋 准备工作 +- [x] 1Panel已安装 +- [x] 项目文件已压缩(zjpb.zip) + +--- + +## 第1步:创建数据库(2分钟) + +1. 登录1Panel → **数据库** +2. 点击 **创建数据库** +3. 填写: + - 数据库名:`ai_nav` + - 用户名:`ai_nav_user` + - 密码:自动生成(**记录下来!**) +4. 确定 + +--- + +## 第2步:上传项目(3分钟) + +1. **文件** → 导航到 `/www/wwwroot/` +2. 创建文件夹 `zjpb` +3. 上传 `zjpb.zip` +4. 解压缩 +5. 编辑 `.env.example` → 另存为 `.env` +6. 填写数据库密码和密钥 + +--- + +## 第3步:创建Python网站(5分钟) + +### 方式A:使用1Panel的Python运行时(推荐) + +1. **网站** → **创建网站** +2. 类型:**运行时 (Runtime)** +3. 配置: + ``` + 运行时:Python 3.8+ + 应用类型:Flask + 域名:your-domain.com + 代码目录:/www/wwwroot/zjpb + 启动文件:app.py + 启动命令:gunicorn -c gunicorn_config.py app:app + 端口:5000 + ☑ 自动启动 + ``` +4. 创建 + +### 方式B:使用反向代理(备选) + +如果没有Python运行时选项: + +1. **网站** → **创建网站** +2. 类型:**反向代理** +3. 配置: + ``` + 域名:your-domain.com + 代理地址:http://127.0.0.1:5000 + ``` + +然后SSH到服务器手动启动(参考完整文档) + +--- + +## 第4步:安装依赖和初始化(5分钟) + +### 4.1 打开1Panel终端 + +点击1Panel右上角 **终端** 图标,或SSH连接 + +### 4.2 安装依赖 + +```bash +cd /www/wwwroot/zjpb + +# 激活1Panel创建的虚拟环境 +source venv/bin/activate + +# 安装依赖 +pip install -r requirements.txt +``` + +### 4.3 初始化数据库 + +```bash +python init_db.py +``` + +看到成功提示后,记住默认账号: +- 用户名:`admin` +- 密码:`admin123` + +--- + +## 第5步:启动和访问(2分钟) + +### 5.1 启动应用 + +**在1Panel界面:** +1. 进入 **网站** 列表 +2. 找到你的项目 +3. 点击 **启动** 按钮 +4. 状态变为 **运行中 ✓** + +### 5.2 配置SSL(推荐) + +1. 点击网站名称进入设置 +2. 找到 **SSL** 选项 +3. 选择 **Let's Encrypt** +4. 申请证书 +5. 启用 **强制HTTPS** + +### 5.3 访问网站 + +- 前台:`https://your-domain.com` +- 后台:`https://your-domain.com/admin/login` + +### 5.4 修改密码(重要!) + +1. 登录后台(admin / admin123) +2. 点击左侧菜单 **修改密码** +3. 设置新密码 + +--- + +## ✅ 完成! + +现在你的网站已经部署完成并运行了! + +--- + +## 🔧 常用操作 + +### 重启应用 +- 1Panel界面:网站列表 → 点击 **重启** + +### 查看日志 +- 1Panel界面:网站设置 → **日志** +- 或查看文件:`/www/wwwroot/zjpb/logs/error.log` + +### 备份数据库 +- 1Panel界面:数据库 → 选择 `ai_nav` → **备份** + +### 更新代码 +1. 上传新文件到项目目录 +2. 重启应用 + +--- + +## ❓ 常见问题 + +**Q: 找不到Python运行时选项?** +- 使用反向代理方式,然后手动启动应用 +- 参考:`1PANEL_DEPLOY.md` 方式二 + +**Q: 虚拟环境在哪?** +- 1Panel自动创建在:`/www/wwwroot/zjpb/venv/` + +**Q: 启动失败?** +1. 查看日志:`logs/error.log` +2. 检查 `.env` 配置 +3. 确认数据库连接 +4. 验证依赖已安装 + +**Q: 需要修改端口?** +- 编辑 `gunicorn_config.py`,修改 `bind` 参数 + +--- + +## 📚 更多帮助 + +- 完整文档:`1PANEL_DEPLOY.md` +- 传统部署:`DEPLOYMENT.md` +- 检查清单:`DEPLOY_CHECKLIST.md` + +祝部署顺利!🚀 diff --git a/CHECK_GIT.md b/CHECK_GIT.md new file mode 100644 index 0000000..a434438 --- /dev/null +++ b/CHECK_GIT.md @@ -0,0 +1,92 @@ +# 检查生产服务器Git配置 + +## 方法1:通过SSH命令检查 + +```bash +# SSH登录到生产服务器 +ssh root@your-server-ip + +# 检查git是否安装 +git --version + +# 如果显示版本号,说明已安装,例如: +# git version 2.30.2 + +# 如果显示 "command not found",说明未安装 +``` + +## 方法2:通过1Panel面板检查 + +```bash +# 1. 登录1Panel管理面板 +# 2. 进入 "主机" -> "终端" +# 3. 输入命令: +git --version +``` + +## 方法3:检查项目目录是否有.git + +```bash +# SSH登录后 +cd /www/wwwroot/zjpb +ls -la | grep .git + +# 如果显示 .git 目录,说明项目已经是git仓库 +# 可以直接使用 git pull +``` + +--- + +## 如果Git未安装 + +### CentOS/RHEL系统: +```bash +yum install -y git +``` + +### Ubuntu/Debian系统: +```bash +apt update +apt install -y git +``` + +### 验证安装: +```bash +git --version +``` + +--- + +## 如果项目目录没有.git(需要初始化) + +```bash +cd /www/wwwroot/zjpb + +# 初始化git仓库 +git init + +# 添加远程仓库(如果你有GitHub/Gitee等) +git remote add origin https://github.com/yourusername/zjpb.git + +# 或者使用SSH方式 +git remote add origin git@github.com:yourusername/zjpb.git + +# 拉取代码 +git pull origin master +``` + +--- + +## 推荐:使用Git部署的优势 + +✅ **版本控制** - 可以随时回滚到之前的版本 +✅ **增量更新** - 只传输修改的文件,速度快 +✅ **操作简单** - 一条命令完成更新 +✅ **团队协作** - 多人开发更方便 + +## 如果不用Git,手动部署也可以 + +可以使用FTP/SFTP工具上传文件: +- FileZilla(免费) +- WinSCP(免费) +- 1Panel自带的文件管理器 diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..291cbbd --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,463 @@ +# ZJPB - 焦提示词 部署文档 + +## 1Panel部署指南 + +### 前置要求 +- 1Panel管理面板已安装 +- MySQL 5.7+ 或 MariaDB +- Python 3.8+ +- Nginx(1Panel自带) + +--- + +## 一、数据库准备 + +### 1.1 在1Panel中创建MySQL数据库 + +1. 登录1Panel管理面板 +2. 进入「数据库」菜单 +3. 点击「创建数据库」 +4. 填写信息: + - 数据库名:`ai_nav` + - 用户名:`ai_nav_user` + - 密码:自动生成或自定义(记录下来) + - 权限:本地访问 +5. 点击创建 + +### 1.2 导入数据库结构 + +使用1Panel的phpMyAdmin或命令行导入: + +```sql +-- 如果你有数据库备份文件,可以直接导入 +-- 否则在部署后通过init_db.py初始化 +``` + +--- + +## 二、上传项目文件 + +### 2.1 压缩项目 + +在本地Windows环境压缩项目文件夹为 `zjpb.zip`,**排除以下文件/文件夹**: +- `__pycache__/` +- `*.pyc` +- `.git/` +- `.env`(生产环境重新配置) +- `venv/` 或 `env/` +- `test_*.py`(测试文件) + +### 2.2 上传到服务器 + +1. 在1Panel中进入「文件」菜单 +2. 导航到 `/opt/1panel/apps/` 或你的网站目录(如 `/www/wwwroot/`) +3. 创建项目目录:`zjpb` +4. 上传 `zjpb.zip` 并解压 +5. 最终路径示例:`/www/wwwroot/zjpb/` + +--- + +## 三、环境配置 + +### 3.1 SSH连接到服务器 + +使用1Panel的终端或SSH工具连接服务器 + +### 3.2 创建Python虚拟环境 + +```bash +cd /www/wwwroot/zjpb + +# 创建虚拟环境 +python3 -m venv venv + +# 激活虚拟环境 +source venv/bin/activate + +# 升级pip +pip install --upgrade pip +``` + +### 3.3 安装Python依赖 + +```bash +pip install -r requirements.txt +``` + +### 3.4 配置环境变量 + +创建生产环境的 `.env` 文件: + +```bash +nano .env +``` + +填写以下内容(**修改为实际值**): + +```env +# 数据库配置 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=ai_nav_user +DB_PASSWORD=你的数据库密码 +DB_NAME=ai_nav + +# 安全配置(生成随机密钥) +SECRET_KEY=your-production-secret-key-change-this + +# 运行环境 +FLASK_ENV=production + +# DeepSeek API配置(可选) +DEEPSEEK_API_KEY=你的DeepSeek_API密钥 +DEEPSEEK_BASE_URL=https://api.deepseek.com +``` + +**生成安全的SECRET_KEY**: +```bash +python3 -c "import secrets; print(secrets.token_hex(32))" +``` + +### 3.5 创建必要的目录 + +```bash +# 创建日志目录 +mkdir -p logs + +# 创建静态文件上传目录 +mkdir -p static/uploads + +# 设置权限 +chmod 755 logs static/uploads +``` + +--- + +## 四、初始化数据库 + +### 4.1 运行初始化脚本 + +```bash +source venv/bin/activate +python init_db.py +``` + +这会创建所有表并创建默认管理员账号: +- 用户名:`admin` +- 密码:`admin123` + +**⚠️ 重要:登录后立即修改默认密码!** + +--- + +## 五、配置Nginx反向代理 + +### 5.1 在1Panel中创建网站 + +1. 进入1Panel「网站」菜单 +2. 点击「创建网站」 +3. 填写信息: + - 网站类型:反向代理 + - 域名:`your-domain.com`(或IP) + - 代理地址:`http://127.0.0.1:5000` + - 启用SSL:推荐(自动申请Let's Encrypt证书) + +### 5.2 自定义Nginx配置(可选) + +如果需要自定义,编辑Nginx配置: + +```nginx +server { + listen 80; + server_name your-domain.com; + + # 如果启用SSL,这里会自动重定向到443 + + client_max_body_size 10M; # 允许上传文件大小 + + location / { + proxy_pass http://127.0.0.1:5000; + 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"; + } + + # 静态文件直接由Nginx处理 + location /static/ { + alias /www/wwwroot/zjpb/static/; + expires 30d; + add_header Cache-Control "public, immutable"; + } +} +``` + +--- + +## 六、配置Supervisor守护进程 + +### 6.1 在1Panel中使用Supervisor + +1. 进入1Panel「容器」→「应用编排」或「进程管理」 +2. 创建新的守护进程配置 + +### 6.2 创建Supervisor配置文件 + +如果1Panel没有内置,手动创建: + +```bash +nano /etc/supervisor/conf.d/zjpb.conf +``` + +配置内容: + +```ini +[program:zjpb] +command=/www/wwwroot/zjpb/venv/bin/gunicorn -c gunicorn_config.py app:app +directory=/www/wwwroot/zjpb +user=www +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=/www/wwwroot/zjpb/logs/supervisor.log +environment=FLASK_ENV="production" +``` + +### 6.3 启动服务 + +```bash +# 重新加载Supervisor配置 +supervisorctl reread +supervisorctl update + +# 启动应用 +supervisorctl start zjpb + +# 查看状态 +supervisorctl status zjpb +``` + +--- + +## 七、使用systemd守护进程(替代方案) + +如果不使用Supervisor,可以用systemd: + +### 7.1 创建systemd服务文件 + +```bash +sudo nano /etc/systemd/system/zjpb.service +``` + +内容: + +```ini +[Unit] +Description=ZJPB AI Navigation Flask Application +After=network.target mysql.service + +[Service] +Type=notify +User=www +Group=www +WorkingDirectory=/www/wwwroot/zjpb +Environment="PATH=/www/wwwroot/zjpb/venv/bin" +Environment="FLASK_ENV=production" +ExecStart=/www/wwwroot/zjpb/venv/bin/gunicorn -c gunicorn_config.py app:app +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +### 7.2 启动服务 + +```bash +# 重新加载systemd +sudo systemctl daemon-reload + +# 启动服务 +sudo systemctl start zjpb + +# 设置开机自启 +sudo systemctl enable zjpb + +# 查看状态 +sudo systemctl status zjpb +``` + +--- + +## 八、验证部署 + +### 8.1 检查服务状态 + +```bash +# 查看gunicorn进程 +ps aux | grep gunicorn + +# 查看日志 +tail -f logs/error.log +tail -f logs/access.log +``` + +### 8.2 访问网站 + +1. 前台访问:`http://your-domain.com` +2. 后台访问:`http://your-domain.com/admin/login` + - 用户名:`admin` + - 密码:`admin123`(首次登录后立即修改) + +--- + +## 九、常用管理命令 + +### 9.1 重启应用 + +**使用Supervisor:** +```bash +supervisorctl restart zjpb +``` + +**使用systemd:** +```bash +sudo systemctl restart zjpb +``` + +### 9.2 查看日志 + +```bash +# 应用日志 +tail -f logs/error.log + +# Nginx访问日志 +tail -f /www/server/nginx/logs/your-domain.com.log + +# Supervisor日志 +tail -f logs/supervisor.log +``` + +### 9.3 更新代码 + +```bash +cd /www/wwwroot/zjpb + +# 备份数据库 +mysqldump -u ai_nav_user -p ai_nav > backup_$(date +%Y%m%d).sql + +# 拉取新代码或上传新文件 +# ... + +# 激活虚拟环境 +source venv/bin/activate + +# 安装新依赖 +pip install -r requirements.txt + +# 重启应用 +supervisorctl restart zjpb +# 或 +sudo systemctl restart zjpb +``` + +--- + +## 十、安全加固 + +### 10.1 修改默认管理员密码 + +登录后台后立即修改密码: +1. 访问:`/admin/change-password` +2. 输入旧密码:`admin123` +3. 设置新的强密码 + +### 10.2 配置防火墙 + +```bash +# 只允许80和443端口(1Panel通常已配置) +sudo ufw allow 80/tcp +sudo ufw allow 443/tcp +sudo ufw enable +``` + +### 10.3 配置SSL证书 + +在1Panel中为网站启用SSL: +1. 进入网站设置 +2. 启用SSL +3. 选择Let's Encrypt免费证书 +4. 自动申请并配置 + +--- + +## 十一、故障排查 + +### 11.1 应用无法启动 + +检查日志: +```bash +tail -f logs/error.log +``` + +常见问题: +- 数据库连接失败:检查`.env`配置 +- 端口被占用:修改`gunicorn_config.py`中的端口 +- 权限问题:确保文件所有者为`www`用户 + +### 11.2 静态文件404 + +检查Nginx配置和目录权限: +```bash +ls -la static/ +chmod -R 755 static/ +``` + +### 11.3 数据库连接失败 + +```bash +# 测试数据库连接 +mysql -u ai_nav_user -p ai_nav + +# 检查MySQL服务 +sudo systemctl status mysql +``` + +--- + +## 十二、备份策略 + +### 12.1 数据库备份 + +创建定时任务: +```bash +crontab -e +``` + +添加每日备份: +```cron +0 2 * * * mysqldump -u ai_nav_user -p密码 ai_nav > /www/backup/zjpb_$(date +\%Y\%m\%d).sql +``` + +### 12.2 文件备份 + +```bash +# 备份上传的文件 +tar -czf uploads_backup_$(date +%Y%m%d).tar.gz static/uploads/ +``` + +--- + +## 支持与帮助 + +如有问题,请检查: +1. 应用日志:`logs/error.log` +2. Nginx日志:`/www/server/nginx/logs/` +3. 系统日志:`journalctl -u zjpb -f` + +祝部署顺利!🚀 diff --git a/DEPLOY_CHECKLIST.md b/DEPLOY_CHECKLIST.md new file mode 100644 index 0000000..c25fd0c --- /dev/null +++ b/DEPLOY_CHECKLIST.md @@ -0,0 +1,132 @@ +# 📦 1Panel部署打包清单 + +部署前请确保以下文件都已准备好: + +## ✅ 必需文件 + +### 应用文件 +- [x] app.py - 主应用文件 +- [x] wsgi.py - WSGI入口(生产环境) +- [x] config.py - 配置文件 +- [x] models.py - 数据模型 +- [x] init_db.py - 数据库初始化脚本 +- [x] requirements.txt - Python依赖列表 + +### 配置文件 +- [x] gunicorn_config.py - Gunicorn配置 +- [x] .env.example - 环境变量模板 +- [ ] .env - 生产环境变量(需在服务器上创建) + +### 部署脚本 +- [x] deploy.sh - 一键部署脚本 +- [x] manage.sh - 应用管理脚本 + +### 文档 +- [x] DEPLOYMENT.md - 完整部署文档 +- [x] QUICK_DEPLOY.md - 快速部署指南 +- [x] README.md - 项目说明 + +### 目录结构 +``` +zjpb/ +├── static/ # 静态文件 +│ ├── css/ +│ ├── js/ +│ └── uploads/ # 上传文件目录 +├── templates/ # 模板文件 +│ ├── admin/ +│ └── *.html +├── utils/ # 工具类 +│ ├── website_fetcher.py +│ ├── tag_generator.py +│ └── bookmark_parser.py +├── migrations/ # 数据库迁移脚本 +├── logs/ # 日志目录(自动创建) +└── venv/ # 虚拟环境(服务器上创建) +``` + +## 🚫 排除文件(不要上传到服务器) + +- `__pycache__/` - Python缓存 +- `*.pyc` - 编译的Python文件 +- `.git/` - Git仓库 +- `.env` - 本地环境变量 +- `venv/` `env/` - 虚拟环境 +- `test_*.py` - 测试文件 +- `*.log` - 日志文件 +- `logs/` - 日志目录 +- `static/uploads/*` - 上传的文件 + +## 📋 部署前检查清单 + +### 本地准备 +- [ ] 更新 requirements.txt +- [ ] 测试应用运行正常 +- [ ] 准备 .env.example 模板 +- [ ] 压缩项目文件 + +### 服务器准备 +- [ ] 1Panel已安装 +- [ ] MySQL数据库已创建 +- [ ] 域名已解析(可选) +- [ ] SSH访问权限 + +### 部署步骤 +- [ ] 上传项目文件到服务器 +- [ ] 解压并设置目录权限 +- [ ] 执行 deploy.sh 脚本 +- [ ] 配置 .env 文件 +- [ ] 初始化数据库 +- [ ] 在1Panel中创建网站(反向代理) +- [ ] 启动应用 +- [ ] 配置SSL证书(可选) + +### 部署后验证 +- [ ] 前台页面访问正常 +- [ ] 后台登录成功 +- [ ] 修改默认管理员密码 +- [ ] 测试网站添加功能 +- [ ] 测试标签创建功能 +- [ ] 测试图片上传功能 + +## 🔐 安全检查 + +- [ ] 修改默认管理员密码 +- [ ] 设置强密码的 SECRET_KEY +- [ ] 配置 .env 权限(chmod 600) +- [ ] 启用 SSL/HTTPS +- [ ] 配置防火墙规则 +- [ ] 定期备份数据库 + +## 📝 压缩命令 + +Windows PowerShell: +```powershell +Compress-Archive -Path * -DestinationPath zjpb.zip -Force +``` + +Linux/Mac: +```bash +zip -r zjpb.zip . -x "*.pyc" "*__pycache__*" "*.git*" ".env" "venv/*" "test_*.py" "logs/*" +``` + +## 🎯 快速部署(服务器上) + +```bash +# 1. 上传并解压 +cd /www/wwwroot/zjpb +unzip zjpb.zip + +# 2. 一键部署 +chmod +x deploy.sh +./deploy.sh + +# 3. 配置环境变量 +nano .env + +# 4. 启动应用 +chmod +x manage.sh +./manage.sh start +``` + +详细步骤请参考:`DEPLOYMENT.md` diff --git a/GIT_PATCH_DEPLOY.md b/GIT_PATCH_DEPLOY.md new file mode 100644 index 0000000..4c45046 --- /dev/null +++ b/GIT_PATCH_DEPLOY.md @@ -0,0 +1,232 @@ +# ZJPB v2.1 Git Patch 部署指南(最简单) + +## ✨ 优势 + +- ✅ **最干净** - 使用Git patch,保持版本历史完整 +- ✅ **最安全** - 自动备份未提交的修改 +- ✅ **最快速** - 只需上传2个文件(89KB) +- ✅ **可追溯** - 所有更改都有Git记录 + +--- + +## 📦 需要上传的文件(仅2个) + +1. `v2.1.0.patch` (89KB) - 代码补丁文件 +2. `git_patch_deploy.sh` - 自动化部署脚本 + +--- + +## 🚀 部署步骤 + +### 第一步:备份生产数据库(在1Panel中操作) + +1. 登录1Panel管理面板 +2. 进入 **数据库** → 找到 `ai_nav` 数据库 +3. 点击 **备份** 按钮 +4. 下载备份文件保存 + +### 第二步:上传文件到服务器 + +**方法1:使用1Panel文件管理器** + +1. 登录1Panel +2. 进入 **文件** → 导航到 `/opt/1panel/apps/zjpb/` +3. 上传文件: + - `v2.1.0.patch` + - `git_patch_deploy.sh` + +**方法2:使用命令行(如果你用SSH)** + +```bash +# 在本地(Windows)使用SCP上传 +scp v2.1.0.patch root@your-server-ip:/opt/1panel/apps/zjpb/ +scp git_patch_deploy.sh root@your-server-ip:/opt/1panel/apps/zjpb/ +``` + +### 第三步:执行部署脚本 + +在1Panel终端或SSH中执行: + +```bash +# 进入项目目录 +cd /opt/1panel/apps/zjpb + +# 赋予执行权限 +chmod +x git_patch_deploy.sh + +# 执行部署 +./git_patch_deploy.sh +``` + +**脚本会自动完成:** +1. ✅ 停止应用 +2. ✅ 检查Git状态 +3. ✅ 备份未提交的修改(如有) +4. ✅ 应用v2.1.0补丁 +5. ✅ 提交到Git +6. ✅ 安装依赖 +7. ✅ 运行数据库迁移(创建prompt_templates表) +8. ✅ 重启应用 +9. ✅ 检查状态 + +**预期输出:** +``` +================================ +ZJPB v2.1 Git Patch 部署 +================================ + +当前目录: /opt/1panel/apps/zjpb + +1. 停止应用... +2. 检查Git状态... +3. 备份当前修改(如有)... +4. 应用v2.1.0补丁... + ✅ 补丁应用成功 +5. 提交更改到Git... +6. 激活虚拟环境... +7. 检查依赖... +8. 运行数据库迁移... +正在创建 prompt_templates 表... +[OK] 表创建成功 +[OK] 默认prompt模板初始化成功 +9. 重启应用... +10. 检查应用状态... +zjpb 正在运行 (PID: xxxx) + +================================ +✅ 部署完成! +================================ +``` + +### 第四步:验证部署 + +**前台验证:** +1. 访问首页,检查页脚: + - `© 2025 ZJPB - 焦提示词 | AI工具导航` + - `浙ICP备2025154782号-1` + - 打开F12检查Network,确认Clarity统计已加载 + +2. 访问任意网站详情页,检查图标: + - 返回首页:`←` 而不是 `arrow_back` + - 访问网站:`↗` 而不是 `north_east` + - 浏览次数:`👁` 而不是 `visibility` + +**后台验证:** +1. 登录 `/admin/login` +2. 左侧菜单应该有 **Prompt管理** +3. 点击进入,查看3条默认模板: + - 标签生成 + - 主要功能生成 + - 详细介绍生成 + +4. 进入 **网站管理** → 编辑任意网站 +5. 标签区域应该正常显示标签名称(蓝色标签,有文字) + +**AI功能验证:** +1. 编辑或创建网站 +2. 测试 **AI生成标签** 按钮 +3. 测试 **AI生成详细介绍** 按钮 +4. 测试 **AI生成主要功能** 按钮 + +--- + +## 🔍 部署后检查 + +查看Git提交历史: +```bash +cd /opt/1panel/apps/zjpb +git log --oneline -5 +``` + +应该看到最新的提交: +``` +xxxxxxx release: v2.1.0 - Prompt管理系统、页脚优化、图标修复 +``` + +查看应用状态: +```bash +./manage.sh status +``` + +查看应用日志(如有问题): +```bash +./manage.sh logs +``` + +--- + +## 🔄 回滚方案(如出现问题) + +```bash +cd /opt/1panel/apps/zjpb + +# 停止应用 +./manage.sh stop + +# 回滚到上一个提交 +git reset --hard HEAD~1 + +# 如果有stash的备份,恢复它 +git stash list +git stash pop + +# 重启应用 +./manage.sh start +``` + +--- + +## 📝 注意事项 + +1. ✅ 服务器路径是 `/opt/1panel/apps/zjpb` 不是 `/www/wwwroot/zjpb` +2. ✅ 已确认服务器有Git仓库 +3. ✅ patch文件会自动保存现有未提交的修改 +4. ✅ 部署过程中会自动运行数据库迁移 +5. ✅ 所有更改都会提交到Git,可随时回滚 + +--- + +## 🎯 完整部署命令(复制粘贴) + +```bash +# 1. 进入项目目录 +cd /opt/1panel/apps/zjpb + +# 2. 检查文件是否上传成功 +ls -lh v2.1.0.patch git_patch_deploy.sh + +# 3. 赋予执行权限 +chmod +x git_patch_deploy.sh + +# 4. 执行部署 +./git_patch_deploy.sh + +# 5. 部署完成后验证 +./manage.sh status +git log --oneline -3 +``` + +--- + +## ✅ 部署检查清单 + +- [ ] 生产数据库已备份 +- [ ] v2.1.0.patch 已上传到服务器 +- [ ] git_patch_deploy.sh 已上传到服务器 +- [ ] 部署脚本执行成功 +- [ ] 前台页脚显示正确(ICP+统计) +- [ ] 详情页图标显示正确(Emoji) +- [ ] 后台Prompt管理菜单存在 +- [ ] 标签显示正常(有文字) +- [ ] AI生成功能测试通过 + +--- + +## 💡 优势说明 + +相比手动上传11个文件,Git patch方式: +- 只需上传2个文件(89KB) +- 自动处理文件合并 +- 保留完整Git历史 +- 可以一键回滚 +- 更安全可靠 diff --git a/MANUAL_DEPLOY.md b/MANUAL_DEPLOY.md new file mode 100644 index 0000000..4bfecf1 --- /dev/null +++ b/MANUAL_DEPLOY.md @@ -0,0 +1,195 @@ +# ZJPB v2.1 手动上传部署指南 + +## 📋 需要上传的文件(共10个) + +### 核心文件(必须上传) +1. ✅ `app.py` - 新增Prompt管理视图 +2. ✅ `models.py` - 新增PromptTemplate模型 +3. ✅ `migrate_prompts.py` - 数据库迁移脚本(新文件) + +### 模板文件(必须上传) +4. ✅ `templates/base_new.html` - 页脚和统计代码 +5. ✅ `templates/detail_new.html` - 图标修复 +6. ✅ `templates/admin/site/create.html` - 标签显示修复 +7. ✅ `templates/admin/site/edit.html` - 标签显示修复 + +### 辅助文件(可选) +8. ⭕ `INCREMENTAL_DEPLOY.md` - 部署文档 +9. ⭕ `export_data.py` - 数据导出工具 +10. ⭕ `migrate_db.py` - 数据库工具 + +### 部署脚本(新增) +11. ✅ `quick_deploy_server.sh` - 服务器端快速部署脚本 + +--- + +## 🚀 部署步骤(使用1Panel文件管理器) + +### 第一步:备份生产数据库 + +1. 登录1Panel管理面板 +2. 进入 **数据库** → 找到 `ai_nav` 数据库 +3. 点击 **备份** 按钮 +4. 下载备份文件到本地保存 + +### 第二步:备份现有代码 + +1. 在1Panel中进入 **文件管理** +2. 进入 `/www/wwwroot/` +3. 右键点击 `zjpb` 文件夹 +4. 选择 **压缩** → 命名为 `zjpb_backup_20250130.tar.gz` +5. 下载到本地保存 + +### 第三步:上传新文件 + +#### 方法1:使用1Panel文件管理器(推荐) + +1. 在1Panel文件管理器中进入 `/www/wwwroot/zjpb/` + +2. **上传核心文件**(覆盖): + - 上传 `app.py` 到 `/www/wwwroot/zjpb/` + - 上传 `models.py` 到 `/www/wwwroot/zjpb/` + - 上传 `migrate_prompts.py` 到 `/www/wwwroot/zjpb/`(新文件) + - 上传 `quick_deploy_server.sh` 到 `/www/wwwroot/zjpb/`(新文件) + +3. **上传模板文件**(覆盖): + - 进入 `/www/wwwroot/zjpb/templates/` + - 上传 `base_new.html` 到 `templates/` + - 上传 `detail_new.html` 到 `templates/` + - 进入 `templates/admin/site/` + - 上传 `create.html` 到 `templates/admin/site/` + - 上传 `edit.html` 到 `templates/admin/site/` + +#### 方法2:使用FTP工具(FileZilla/WinSCP) + +``` +服务器地址: your-server-ip +用户名: root +密码: your-password +端口: 22 (SFTP) +``` + +连接后进入 `/www/wwwroot/zjpb/`,直接拖拽文件上传覆盖。 + +### 第四步:设置脚本权限并执行部署 + +在1Panel终端或SSH中执行: + +```bash +# 进入项目目录 +cd /www/wwwroot/zjpb + +# 设置脚本执行权限 +chmod +x quick_deploy_server.sh + +# 执行部署脚本 +./quick_deploy_server.sh +``` + +**脚本会自动完成以下操作:** +1. ✅ 停止应用 +2. ✅ 备份当前代码 +3. ✅ 安装依赖(如有更新) +4. ✅ 运行数据库迁移 +5. ✅ 重启应用 +6. ✅ 检查状态 + +### 第五步:验证部署 + +**前台验证:** +1. 访问首页,检查页脚是否显示: + - `© 2025 ZJPB - 焦提示词 | AI工具导航` + - `浙ICP备2025154782号-1` + +2. 访问任意网站详情页,检查: + - 图标是否正常显示(不是Material Icons文本) + - 返回首页按钮显示 `←` 而不是 `arrow_back` + +**后台验证:** +1. 登录后台 `/admin/login` +2. 检查左侧菜单是否有 **Prompt管理** +3. 点击进入,应该看到3条默认数据: + - 标签生成 + - 主要功能生成 + - 详细介绍生成 + +4. 进入 **网站管理** → 编辑任意网站 +5. 检查标签区域是否正常显示标签名称(不是空白蓝框) + +**AI功能验证:** +1. 创建新网站或编辑现有网站 +2. 测试 "AI生成标签" 按钮 +3. 测试 "AI生成详细介绍" 按钮 +4. 测试 "AI生成主要功能" 按钮 + +--- + +## 🐛 常见问题 + +### Q1: 上传后文件权限错误 +```bash +# 设置正确的权限 +cd /www/wwwroot/zjpb +chmod 644 *.py +chmod 755 *.sh +``` + +### Q2: 脚本执行失败 +```bash +# 手动执行步骤 +cd /www/wwwroot/zjpb +./manage.sh stop +source venv/bin/activate +python migrate_prompts.py +./manage.sh start +``` + +### Q3: 数据库迁移报错 "表已存在" +这是正常的,说明之前已经运行过,可以忽略。 + +### Q4: 应用无法启动 +```bash +# 查看错误日志 +./manage.sh logs + +# 或 +tail -f logs/error.log +``` + +--- + +## 🔄 快速回滚(如出现问题) + +```bash +cd /www/wwwroot/zjpb +./manage.sh stop + +# 恢复备份 +cd /www/wwwroot +rm -rf zjpb +tar -xzf zjpb_backup_20250130.tar.gz + +# 重启 +cd zjpb +./manage.sh start +``` + +--- + +## 📊 部署检查清单 + +- [ ] 生产数据库已备份 +- [ ] 现有代码已备份 +- [ ] 所有文件上传完成 +- [ ] 部署脚本执行成功 +- [ ] 前台页脚显示正常 +- [ ] 详情页图标显示正常 +- [ ] 后台Prompt管理菜单存在 +- [ ] 标签显示正常 +- [ ] AI功能测试通过 + +--- + +## 💡 提示 + +如果你经常需要部署更新,建议配置Git远程仓库(GitHub/Gitee),下次就可以直接 `git pull` 更新,更加方便快捷。 diff --git a/QUICK_DEPLOY.md b/QUICK_DEPLOY.md new file mode 100644 index 0000000..8883708 --- /dev/null +++ b/QUICK_DEPLOY.md @@ -0,0 +1,94 @@ +# 1Panel部署快速指南 + +## 简化部署步骤 + +### 1. 准备工作(本地) + +1. 压缩项目: +```bash +# 排除不需要的文件 +zip -r zjpb.zip . -x "*.pyc" "*__pycache__*" "*.git*" ".env" "venv/*" "test_*.py" +``` + +### 2. 服务器操作 + +#### 2.1 上传并解压 +```bash +cd /www/wwwroot +mkdir zjpb +cd zjpb +# 上传zjpb.zip到此目录 +unzip zjpb.zip +``` + +#### 2.2 一键部署脚本 +```bash +# 赋予执行权限 +chmod +x deploy.sh + +# 执行部署 +./deploy.sh +``` + +### 3. 配置数据库 + +在1Panel中创建数据库: +- 名称:ai_nav +- 用户:ai_nav_user +- 密码:(记录下来) + +### 4. 配置环境变量 + +编辑 `.env` 文件: +```bash +nano .env +``` + +填写数据库信息和密钥。 + +### 5. 初始化 + +```bash +source venv/bin/activate +python init_db.py +``` + +### 6. 启动服务 + +**方法1: 使用管理脚本** +```bash +chmod +x manage.sh +./manage.sh start +``` + +**方法2: 使用1Panel** +在1Panel中创建反向代理网站,指向 `http://127.0.0.1:5000` + +### 7. 访问 + +- 前台:http://your-domain.com +- 后台:http://your-domain.com/admin/login + - 默认账号:admin / admin123 + +--- + +## 快速命令参考 + +```bash +# 启动应用 +./manage.sh start + +# 停止应用 +./manage.sh stop + +# 重启应用 +./manage.sh restart + +# 查看状态 +./manage.sh status + +# 查看日志 +./manage.sh logs +``` + +详细部署文档请查看:`DEPLOYMENT.md` diff --git a/UPLOAD_FILES_v2.2.0.txt b/UPLOAD_FILES_v2.2.0.txt new file mode 100644 index 0000000..2a781ec --- /dev/null +++ b/UPLOAD_FILES_v2.2.0.txt @@ -0,0 +1,221 @@ +================================================================================ +ZJPB v2.2.0 手动上传文件清单 +================================================================================ + +本地路径: D:\315mac\Code\zjpb\ +目标路径: /opt/1panel/apps/zjpb/ + +方案一:Git Patch方式(推荐) +-------------------------------- +只需上传2个文件: + +1. v2.2.0.patch (56KB) + 目标: /opt/1panel/apps/zjpb/v2.2.0.patch + +2. git_patch_deploy_v2.2.sh (2.6KB) + 目标: /opt/1panel/apps/zjpb/git_patch_deploy_v2.2.sh + +上传后执行: +cd /opt/1panel/apps/zjpb +chmod +x git_patch_deploy_v2.2.sh +./git_patch_deploy_v2.2.sh + +================================================================================ + +方案二:手动上传所有文件(如果不想用patch) +---------------------------------------- +需要上传9个文件: + +新增文件(4个): +-------------- +1. utils/news_searcher.py + 本地: D:\315mac\Code\zjpb\utils\news_searcher.py + 目标: /opt/1panel/apps/zjpb/utils/news_searcher.py + 说明: NewsSearcher工具类,封装博查API调用 + +2. migrate_news_fields.py + 本地: D:\315mac\Code\zjpb\migrate_news_fields.py + 目标: /opt/1panel/apps/zjpb/migrate_news_fields.py + 说明: 数据库迁移脚本,添加source_name和source_icon字段 + +3. test_news_feature.py + 本地: D:\315mac\Code\zjpb\test_news_feature.py + 目标: /opt/1panel/apps/zjpb/test_news_feature.py + 说明: 新闻功能测试脚本 + +4. fetch_news_cron.py + 本地: D:\315mac\Code\zjpb\fetch_news_cron.py + 目标: /opt/1panel/apps/zjpb/fetch_news_cron.py + 说明: 定期任务脚本,批量更新新闻 + +修改文件(4个): +-------------- +5. config.py + 本地: D:\315mac\Code\zjpb\config.py + 目标: /opt/1panel/apps/zjpb/config.py + 说明: 添加博查API配置 + +6. models.py + 本地: D:\315mac\Code\zjpb\models.py + 目标: /opt/1panel/apps/zjpb/models.py + 说明: News模型添加source_name和source_icon字段 + +7. app.py + 本地: D:\315mac\Code\zjpb\app.py + 目标: /opt/1panel/apps/zjpb/app.py + 说明: 添加新闻路由、智能更新逻辑、NewsAdmin优化 + +8. templates/detail_new.html + 本地: D:\315mac\Code\zjpb\templates\detail_new.html + 目标: /opt/1panel/apps/zjpb/templates/detail_new.html + 说明: 新闻展示UI,布局优化 + +文档文件(1个,可选): +-------------------- +9. NEWS_FEATURE_v2.2.md + 本地: D:\315mac\Code\zjpb\NEWS_FEATURE_v2.2.md + 目标: /opt/1panel/apps/zjpb/NEWS_FEATURE_v2.2.md + 说明: 新闻功能完整文档 + +================================================================================ + +手动上传后需要执行的操作: +------------------------ + +1. 运行数据库迁移: + cd /opt/1panel/apps/zjpb + source venv/bin/activate + python migrate_news_fields.py + +2. 配置.env文件,添加: + BOCHA_API_KEY=sk-76d0236a50d445ae92e75b634ed5313c + BOCHA_BASE_URL=https://api.bocha.cn + +3. 重启应用: + ./manage.sh restart + +4. 测试功能: + python test_news_feature.py + +================================================================================ + +文件详细信息: +------------ + +utils/news_searcher.py (271行) +- NewsSearcher类 +- 博查API封装 +- 新闻搜索和解析 +- 错误处理 + +migrate_news_fields.py (99行) +- 添加source_name字段 +- 添加source_icon字段 +- 检查字段是否已存在 +- 显示表结构 + +test_news_feature.py (142行) +- API配置检查 +- 数据库连接测试 +- 新闻搜索测试 +- 保存数据测试 + +fetch_news_cron.py (167行) +- 批量新闻更新 +- 命令行参数支持 +- 进度显示 +- 错误处理 + +config.py (修改部分) +- BOCHA_API_KEY配置 +- BOCHA_BASE_URL配置 +- BOCHA_SEARCH_ENDPOINT配置 +- NEWS_SEARCH_*配置 + +models.py (修改部分) +- News.source_name字段 +- News.source_icon字段 + +app.py (修改部分) +- site_detail路由:智能新闻更新逻辑(64行新增) +- /api/fetch-site-news路由(91行) +- /api/fetch-all-news路由(105行) +- NewsAdmin: 新增source_name和source_icon字段 + +templates/detail_new.html (修改部分) +- 新闻模块HTML(47行) +- 新闻来源展示 +- 布局调整(新闻左侧,推荐右侧) + +================================================================================ + +推荐使用方案一(Git Patch)的原因: +--------------------------------- +✅ 只需上传2个文件(58.6KB总共) +✅ 自动处理文件合并,避免手动覆盖错误 +✅ Git历史完整,可追溯 +✅ 自动备份现有修改 +✅ 一键回滚 +✅ 自动运行数据库迁移 +✅ 自动重启应用 + +手动上传的缺点: +-------------- +❌ 需要上传9个文件 +❌ 需要确保文件路径正确 +❌ 需要手动运行迁移脚本 +❌ 需要手动重启应用 +❌ 无法自动回滚 +❌ 可能覆盖生产环境的修改 + +================================================================================ + +1Panel文件管理器上传步骤: +------------------------- + +1. 登录1Panel管理面板 +2. 进入"文件"菜单 +3. 导航到 /opt/1panel/apps/zjpb/ +4. 点击"上传"按钮 +5. 选择需要上传的文件 +6. 等待上传完成 + +如果上传utils/news_searcher.py: +- 先导航到 /opt/1panel/apps/zjpb/utils/ +- 然后上传 news_searcher.py + +如果上传templates/detail_new.html: +- 先导航到 /opt/1panel/apps/zjpb/templates/ +- 然后上传 detail_new.html + +================================================================================ + +SCP命令上传(如果使用SSH): +-------------------------- + +# 方案一:上传patch文件 +scp v2.2.0.patch root@your-server:/opt/1panel/apps/zjpb/ +scp git_patch_deploy_v2.2.sh root@your-server:/opt/1panel/apps/zjpb/ + +# 方案二:上传所有文件 +scp utils/news_searcher.py root@your-server:/opt/1panel/apps/zjpb/utils/ +scp migrate_news_fields.py root@your-server:/opt/1panel/apps/zjpb/ +scp test_news_feature.py root@your-server:/opt/1panel/apps/zjpb/ +scp fetch_news_cron.py root@your-server:/opt/1panel/apps/zjpb/ +scp config.py root@your-server:/opt/1panel/apps/zjpb/ +scp models.py root@your-server:/opt/1panel/apps/zjpb/ +scp app.py root@your-server:/opt/1panel/apps/zjpb/ +scp templates/detail_new.html root@your-server:/opt/1panel/apps/zjpb/templates/ +scp NEWS_FEATURE_v2.2.md root@your-server:/opt/1panel/apps/zjpb/ + +================================================================================ + +建议: +----- +强烈推荐使用方案一(Git Patch)! +- 更安全 +- 更快速 +- 更可靠 +- 更易回滚 + +================================================================================ diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..34b223e --- /dev/null +++ b/deploy.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# ZJPB 一键部署脚本 +# 用于1Panel环境快速部署 + +echo "================================" +echo "ZJPB - 焦提示词 部署脚本" +echo "================================" +echo "" + +# 检查Python版本 +python_version=$(python3 --version 2>&1 | awk '{print $2}') +echo "✓ Python版本: $python_version" + +# 创建必要目录 +echo "创建必要目录..." +mkdir -p logs +mkdir -p static/uploads +echo "✓ 目录创建完成" + +# 创建虚拟环境 +if [ ! -d "venv" ]; then + echo "创建Python虚拟环境..." + python3 -m venv venv + echo "✓ 虚拟环境创建完成" +else + echo "✓ 虚拟环境已存在" +fi + +# 激活虚拟环境 +source venv/bin/activate + +# 升级pip +echo "升级pip..." +pip install --upgrade pip -q + +# 安装依赖 +echo "安装Python依赖包..." +pip install -r requirements.txt -q +echo "✓ 依赖包安装完成" + +# 检查.env文件 +if [ ! -f ".env" ]; then + echo "" + echo "⚠️ 警告: .env文件不存在" + echo "请从.env.example复制并配置:" + echo " cp .env.example .env" + echo " nano .env" + echo "" + read -p "是否现在创建.env文件? (y/n) " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + cp .env.example .env + echo "✓ .env文件已创建,请编辑配置" + nano .env + fi +else + echo "✓ .env配置文件存在" +fi + +# 询问是否初始化数据库 +echo "" +read -p "是否需要初始化数据库? (首次部署选择y) (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "初始化数据库..." + python init_db.py + echo "✓ 数据库初始化完成" + echo "" + echo "默认管理员账号:" + echo " 用户名: admin" + echo " 密码: admin123" + echo " ⚠️ 请登录后立即修改密码!" +fi + +# 设置权限 +echo "" +echo "设置目录权限..." +chmod 755 logs static/uploads +chmod +x manage.sh +echo "✓ 权限设置完成" + +echo "" +echo "================================" +echo "部署完成!" +echo "================================" +echo "" +echo "后续步骤:" +echo "1. 确保已在1Panel中创建MySQL数据库" +echo "2. 确保.env文件配置正确" +echo "3. 在1Panel中创建反向代理网站,指向: http://127.0.0.1:5000" +echo "4. 启动应用: ./manage.sh start" +echo "5. 访问网站并修改默认密码" +echo "" +echo "详细文档: 查看 DEPLOYMENT.md" +echo "" diff --git a/export_sites.py b/export_sites.py new file mode 100644 index 0000000..366da09 --- /dev/null +++ b/export_sites.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +""" +导出网站数据 +生成CSV文件,方便批量导入 +""" +import csv +import sys +from app import create_app +from models import Site, Tag + +def export_sites_to_csv(): + """导出网站数据到CSV""" + app = create_app('development') + + with app.app_context(): + sites = Site.query.order_by(Site.sort_order.desc()).all() + + if not sites: + print("没有找到任何网站数据") + return + + # 导出为CSV + filename = 'sites_export.csv' + with open(filename, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.writer(f) + + # 写入表头 + writer.writerow([ + '网站名称', + 'URL', + '简短描述', + '详细描述', + '特色功能', + '标签(逗号分隔)', + '排序值', + '是否可见' + ]) + + # 写入数据 + for site in sites: + tags_str = ', '.join([tag.name for tag in site.tags]) + writer.writerow([ + site.name, + site.url, + site.short_desc or '', + site.description or '', + site.features or '', + tags_str, + site.sort_order, + '是' if site.is_visible else '否' + ]) + + print(f"✓ 成功导出 {len(sites)} 个网站") + print(f" 文件: {filename}") + print("\n网站列表:") + print("-" * 80) + + for i, site in enumerate(sites, 1): + tags = ', '.join([tag.name for tag in site.tags]) + print(f"{i}. {site.name}") + print(f" URL: {site.url}") + print(f" 描述: {site.short_desc}") + print(f" 标签: {tags}") + print() + +if __name__ == '__main__': + export_sites_to_csv() diff --git a/export_urls.py b/export_urls.py new file mode 100644 index 0000000..997ded7 --- /dev/null +++ b/export_urls.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +导出网站URL列表 +""" +from app import create_app +from models import Site + +def export_urls(): + """导出所有网站URL""" + app = create_app('development') + + with app.app_context(): + sites = Site.query.order_by(Site.sort_order.desc()).all() + + if not sites: + print("没有找到任何网站数据") + return + + # 导出到文件 + filename = 'urls.txt' + with open(filename, 'w', encoding='utf-8') as f: + for site in sites: + f.write(site.url + '\n') + + print(f"✓ 成功导出 {len(sites)} 个URL") + print(f" 文件: {filename}") + print("\nURL列表:") + print("-" * 80) + for url in [site.url for site in sites]: + print(url) + +if __name__ == '__main__': + export_urls() diff --git a/git_patch_deploy.sh b/git_patch_deploy.sh new file mode 100644 index 0000000..60abcf9 --- /dev/null +++ b/git_patch_deploy.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# ZJPB v2.1 Git Patch 部署脚本 +# 在生产服务器上执行 + +echo "================================" +echo "ZJPB v2.1 Git Patch 部署" +echo "================================" +echo "" + +# 项目路径 +PROJECT_DIR="/opt/1panel/apps/zjpb" + +# 检查是否在正确目录 +cd $PROJECT_DIR || { echo "❌ 项目目录不存在"; exit 1; } + +echo "当前目录: $(pwd)" +echo "" + +# 停止应用 +echo "1. 停止应用..." +./manage.sh stop +sleep 2 + +# 检查Git状态 +echo "2. 检查Git状态..." +git status + +# 备份未提交的修改(如果有) +echo "3. 备份当前修改(如有)..." +if ! git diff-index --quiet HEAD --; then + echo " 发现未提交的修改,正在保存..." + git stash save "backup_before_v2.1_$(date +%Y%m%d_%H%M%S)" +fi + +# 应用patch +echo "4. 应用v2.1.0补丁..." +if [ -f "v2.1.0.patch" ]; then + git apply --check v2.1.0.patch + if [ $? -eq 0 ]; then + git apply v2.1.0.patch + echo " ✅ 补丁应用成功" + else + echo " ❌ 补丁应用失败,请检查" + exit 1 + fi +else + echo " ❌ v2.1.0.patch 文件不存在" + exit 1 +fi + +# 提交更改 +echo "5. 提交更改到Git..." +git add . +git commit -m "release: v2.1.0 - Prompt管理系统、页脚优化、图标修复 + +通过patch部署,包含以下更新: +- Prompt管理系统 +- 页脚ICP备案和统计代码 +- 详情页图标修复 +- 标签显示修复 +" + +# 激活虚拟环境 +echo "6. 激活虚拟环境..." +source venv/bin/activate + +# 安装依赖 +echo "7. 检查依赖..." +pip install -r requirements.txt -q + +# 运行数据库迁移 +echo "8. 运行数据库迁移..." +python migrate_prompts.py + +# 重启应用 +echo "9. 重启应用..." +./manage.sh start +sleep 3 + +# 检查状态 +echo "10. 检查应用状态..." +./manage.sh status + +echo "" +echo "================================" +echo "✅ 部署完成!" +echo "================================" +echo "" +echo "Git提交历史:" +git log --oneline -3 +echo "" +echo "请访问网站验证更新是否成功" +echo "" diff --git a/gunicorn_config.py b/gunicorn_config.py new file mode 100644 index 0000000..87e0129 --- /dev/null +++ b/gunicorn_config.py @@ -0,0 +1,44 @@ +# Gunicorn配置文件 +import os + +# 绑定地址和端口 +bind = "0.0.0.0:5000" + +# 工作进程数 (推荐: CPU核心数 * 2 + 1) +workers = 4 + +# 工作线程数 +threads = 2 + +# 工作模式 +worker_class = "sync" + +# 超时时间(秒) +timeout = 120 + +# 访问日志 +accesslog = "logs/access.log" + +# 错误日志 +errorlog = "logs/error.log" + +# 日志级别 +loglevel = "info" + +# 进程名称 +proc_name = "zjpb_app" + +# 守护进程模式(设置为False,由systemd或supervisor管理) +daemon = False + +# PID文件 +pidfile = "logs/gunicorn.pid" + +# 预加载应用(提高性能) +preload_app = True + +# 优雅重启超时 +graceful_timeout = 30 + +# 保持连接 +keepalive = 5 diff --git a/manage.sh b/manage.sh new file mode 100644 index 0000000..51cf1e9 --- /dev/null +++ b/manage.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# ZJPB 应用管理脚本 +# 用法: ./manage.sh [start|stop|restart|status|logs] + +APP_NAME="zjpb" +APP_DIR="/www/wwwroot/zjpb" +VENV_DIR="$APP_DIR/venv" +PID_FILE="$APP_DIR/logs/gunicorn.pid" + +cd $APP_DIR + +case "$1" in + start) + echo "启动 $APP_NAME..." + source $VENV_DIR/bin/activate + gunicorn -c gunicorn_config.py app:app + echo "$APP_NAME 已启动" + ;; + + stop) + echo "停止 $APP_NAME..." + if [ -f $PID_FILE ]; then + kill $(cat $PID_FILE) + echo "$APP_NAME 已停止" + else + echo "PID文件不存在,可能未运行" + fi + ;; + + restart) + $0 stop + sleep 2 + $0 start + ;; + + status) + if [ -f $PID_FILE ]; then + PID=$(cat $PID_FILE) + if ps -p $PID > /dev/null; then + echo "$APP_NAME 正在运行 (PID: $PID)" + else + echo "$APP_NAME 未运行(但PID文件存在)" + fi + else + echo "$APP_NAME 未运行" + fi + ;; + + logs) + echo "实时查看日志(Ctrl+C退出):" + tail -f logs/error.log + ;; + + *) + echo "用法: $0 {start|stop|restart|status|logs}" + exit 1 + ;; +esac + +exit 0 diff --git a/one_click_deploy.sh b/one_click_deploy.sh new file mode 100644 index 0000000..8da3098 --- /dev/null +++ b/one_click_deploy.sh @@ -0,0 +1,121 @@ +#!/bin/bash +# ZJPB v2.1 一键部署命令集合 +# 复制整个脚本到服务器终端执行即可 + +set -e # 遇到错误立即停止 + +echo "================================" +echo "ZJPB v2.1 一键部署" +echo "================================" +echo "" + +# 进入项目目录 +cd /opt/1panel/apps/zjpb + +# 显示当前位置 +echo "📂 当前目录: $(pwd)" +echo "" + +# 停止应用 +echo "⏸️ 停止应用..." +./manage.sh stop 2>/dev/null || echo "应用未运行" +sleep 2 + +# 检查Git状态 +echo "" +echo "🔍 检查Git状态..." +git status --short + +# 备份未提交的修改 +echo "" +echo "💾 备份当前修改..." +if ! git diff-index --quiet HEAD -- 2>/dev/null; then + git stash save "backup_before_v2.1_$(date +%Y%m%d_%H%M%S)" + echo " ✅ 已保存未提交的修改" +else + echo " ℹ️ 无需备份" +fi + +# 检查patch文件 +echo "" +echo "📦 检查补丁文件..." +if [ ! -f "v2.1.0.patch" ]; then + echo " ❌ v2.1.0.patch 不存在,请先上传" + exit 1 +fi +echo " ✅ v2.1.0.patch 存在" + +# 应用patch +echo "" +echo "🔧 应用v2.1.0补丁..." +git apply --check v2.1.0.patch 2>&1 +if [ $? -eq 0 ]; then + git apply v2.1.0.patch + echo " ✅ 补丁应用成功" +else + echo " ❌ 补丁应用失败" + exit 1 +fi + +# 提交更改 +echo "" +echo "📝 提交更改到Git..." +git add . +git commit -m "release: v2.1.0 - Prompt管理系统、页脚优化、图标修复 + +部署时间: $(date '+%Y-%m-%d %H:%M:%S') +部署方式: Git Patch + +更新内容: +- 新增Prompt管理系统 +- 页脚添加ICP备案号和统计代码 +- 详情页图标优化(Material Icons → Emoji) +- 修复标签显示问题 +" 2>&1 + +# 激活虚拟环境 +echo "" +echo "🐍 激活虚拟环境..." +source venv/bin/activate + +# 安装依赖 +echo "" +echo "📚 检查依赖..." +pip install -r requirements.txt -q + +# 运行数据库迁移 +echo "" +echo "🗄️ 运行数据库迁移..." +python migrate_prompts.py + +# 重启应用 +echo "" +echo "🚀 重启应用..." +./manage.sh start +sleep 3 + +# 检查状态 +echo "" +echo "✅ 检查应用状态..." +./manage.sh status + +# 显示Git历史 +echo "" +echo "📜 Git提交历史(最近3条):" +git log --oneline -3 + +echo "" +echo "================================" +echo "🎉 部署完成!" +echo "================================" +echo "" +echo "📋 验证清单:" +echo " 1. 访问首页,检查页脚ICP备案号" +echo " 2. 访问详情页,检查图标是否为emoji" +echo " 3. 登录后台,检查Prompt管理菜单" +echo " 4. 编辑网站,检查标签显示是否正常" +echo " 5. 测试AI生成功能" +echo "" +echo "🔧 查看日志: ./manage.sh logs" +echo "📊 查看状态: ./manage.sh status" +echo "" diff --git a/quick_deploy_server.sh b/quick_deploy_server.sh new file mode 100644 index 0000000..7651792 --- /dev/null +++ b/quick_deploy_server.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# ZJPB v2.1 快速部署脚本 +# 在生产服务器上执行此脚本 + +echo "================================" +echo "ZJPB v2.1 增量部署" +echo "================================" +echo "" + +# 检查是否在正确目录 +if [ ! -f "app.py" ]; then + echo "❌ 错误:请在项目根目录执行此脚本" + exit 1 +fi + +# 停止应用 +echo "1. 停止应用..." +./manage.sh stop +sleep 2 + +# 备份当前代码 +echo "2. 备份当前代码..." +BACKUP_DIR="../zjpb_backup_$(date +%Y%m%d_%H%M%S)" +cp -r ../zjpb "$BACKUP_DIR" +echo " 备份已保存到: $BACKUP_DIR" + +# 激活虚拟环境 +echo "3. 激活虚拟环境..." +source venv/bin/activate + +# 安装依赖(如有更新) +echo "4. 检查依赖..." +pip install -r requirements.txt -q + +# 运行数据库迁移 +echo "5. 运行数据库迁移..." +python migrate_prompts.py + +# 重启应用 +echo "6. 重启应用..." +./manage.sh start +sleep 2 + +# 检查状态 +echo "7. 检查应用状态..." +./manage.sh status + +echo "" +echo "================================" +echo "✅ 部署完成!" +echo "================================" +echo "" +echo "验证项目:" +echo "1. 访问前台首页,检查页脚ICP备案号" +echo "2. 访问详情页,检查图标是否正常" +echo "3. 登录后台,检查Prompt管理菜单" +echo "4. 编辑网站,检查标签是否正常显示" +echo "" +echo "如遇问题,可快速回滚:" +echo " ./manage.sh stop" +echo " cd .." +echo " rm -rf zjpb" +echo " mv $BACKUP_DIR zjpb" +echo " cd zjpb" +echo " ./manage.sh start" +echo "" diff --git a/static/logos/logo_5fda15d305a84866.jpg b/static/logos/logo_5fda15d305a84866.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff9037098b760e6e2c6d4c94a921b355809e3982 GIT binary patch literal 30205 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 L-VJU1>-+x!>8?7M literal 0 HcmV?d00001 diff --git a/static/logos/logo_aipmclub_com.png b/static/logos/logo_aipmclub_com.png new file mode 100644 index 0000000000000000000000000000000000000000..0c34f2b8ecd6e861e37a108b54c79f0cc9ece43c GIT binary patch literal 14009 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 literal 0 HcmV?d00001 diff --git a/templates/admin/change_password.html b/templates/admin/change_password.html new file mode 100644 index 0000000..e8e8f89 --- /dev/null +++ b/templates/admin/change_password.html @@ -0,0 +1,370 @@ + + + + + + 修改密码 - ZJPB 焦提示词 + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ 控制台 + / + 修改密码 +
+
+ + + +
+
+ + +
+ + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + +
+
+
+
+
+ +
+ +
+
+ + lock + +
+ +
+
+ + +
+ +
+
+ + lock_reset + +
+ +
+ 密码长度至少6位 +
+ + +
+ +
+
+ + check_circle + +
+ +
+
+ + +
+ + + cancel + 取消 + +
+
+
+
+ + +
+ info + 安全提示: +
    +
  • 密码修改成功后,您将被自动登出,需要使用新密码重新登录
  • +
  • 请妥善保管您的密码,不要与他人分享
  • +
  • 建议定期修改密码以保证账号安全
  • +
+
+
+
+
+
+ + + + + + + + + + diff --git a/test_deepseek.py b/test_deepseek.py new file mode 100644 index 0000000..127b642 --- /dev/null +++ b/test_deepseek.py @@ -0,0 +1,52 @@ +"""测试DeepSeek API配置""" +import os +from dotenv import load_dotenv +from openai import OpenAI + +# 加载环境变量 +load_dotenv() + +def test_deepseek_api(): + """测试DeepSeek API连接""" + api_key = os.getenv('DEEPSEEK_API_KEY') + base_url = os.getenv('DEEPSEEK_BASE_URL', 'https://api.deepseek.com') + + print(f"API Key: {api_key[:20]}..." if api_key else "未找到API Key") + print(f"Base URL: {base_url}") + + if not api_key: + print("[ERROR] DEEPSEEK_API_KEY not configured") + return False + + try: + # 创建客户端 + client = OpenAI( + api_key=api_key, + base_url=base_url + ) + + # 发送测试请求 + print("\nTesting API connection...") + response = client.chat.completions.create( + model="deepseek-chat", + messages=[ + {"role": "system", "content": "你是一个AI助手"}, + {"role": "user", "content": "你好,请用一句话介绍你自己"} + ], + max_tokens=100 + ) + + result = response.choices[0].message.content + # 移除emoji和特殊字符 + result_clean = result.encode('ascii', 'ignore').decode('ascii') + print(f"\n[SUCCESS] API connection successful!") + print(f"Response: {result_clean if result_clean else result[:50]}") + print(f"Usage: {response.usage}") + return True + + except Exception as e: + print(f"\n[ERROR] API connection failed: {str(e)}") + return False + +if __name__ == '__main__': + test_deepseek_api() diff --git a/v2.1.0.patch b/v2.1.0.patch new file mode 100644 index 0000000..26bf5ca --- /dev/null +++ b/v2.1.0.patch @@ -0,0 +1,2438 @@ +From 30b1ef75d655f962da385ad8630e06562a8b5bdf Mon Sep 17 00:00:00 2001 +From: Jowe <123822645+Selei1983@users.noreply.github.com> +Date: Tue, 30 Dec 2025 00:45:39 +0800 +Subject: [PATCH] =?UTF-8?q?release:=20v2.1=20-=20Prompt=E7=AE=A1=E7=90=86?= + =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E3=80=81=E9=A1=B5=E8=84=9A=E4=BC=98=E5=8C=96?= + =?UTF-8?q?=E3=80=81=E5=9B=BE=E6=A0=87=E4=BF=AE=E5=A4=8D?= +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +新增功能: +- Prompt管理:后台新增Prompt模板管理功能 +- 数据库迁移:新增prompt_templates表及默认数据 +- 页脚优化:添加ICP备案号(浙ICP备2025154782号-1)和Microsoft Clarity统计 +- 图标修复:详情页Material Icons替换为Emoji +- 标签显示:修复编辑页标签名称无法显示的问题 + +技术改进: +- 添加正则表达式提取标签名称 +- 优化页脚样式和链接 +- 完善增量部署文档 + +🚀 Generated with Claude Code +Co-Authored-By: Claude Sonnet 4.5 +--- + INCREMENTAL_DEPLOY.md | 208 ++++++++++++ + app.py | 313 +++++++++++++++++- + export_data.py | 58 ++++ + migrate_db.py | 49 +++ + migrate_prompts.py | 141 +++++++++ + models.py | 32 ++ + templates/admin/site/create.html | 455 ++++++++++++++++++++++++++- + templates/admin/site/edit.html | 522 +++++++++++++++++++++++++++++-- + templates/base_new.html | 40 ++- + templates/detail_new.html | 165 ++++++++-- + 10 files changed, 1887 insertions(+), 96 deletions(-) + create mode 100644 INCREMENTAL_DEPLOY.md + create mode 100644 export_data.py + create mode 100644 migrate_db.py + create mode 100644 migrate_prompts.py + +diff --git a/INCREMENTAL_DEPLOY.md b/INCREMENTAL_DEPLOY.md +new file mode 100644 +index 0000000..dc18d04 +--- /dev/null ++++ b/INCREMENTAL_DEPLOY.md +@@ -0,0 +1,208 @@ ++# ZJPB v2.1 增量部署指南 ++ ++## 📋 本次更新内容 ++ ++### 新增功能 ++1. **Prompt管理系统** - 后台可管理AI提示词模板 ++2. **详情页图标优化** - Material Icons替换为Emoji ++3. **标签显示修复** - 修复编辑页标签名称无法显示问题 ++4. **页脚优化** - 添加ICP备案号和Microsoft Clarity统计 ++ ++### 数据库变更 ++- 新增表:`prompt_templates` (Prompt模板表) ++- 无现有表结构变更,完全兼容旧数据 ++ ++--- ++ ++## 🔒 增量部署步骤 ++ ++### 第一步:备份生产数据库(必须!) ++ ++在1Panel中备份数据库: ++```bash ++# 方法1:使用1Panel面板 ++1. 进入1Panel -> 数据库 ++2. 找到 ai_nav 数据库 ++3. 点击"备份"按钮 ++4. 下载备份文件到本地保存 ++ ++# 方法2:使用命令行 ++mysqldump -h 112.124.42.38 -u ai_nav_user -p ai_nav > backup_$(date +%Y%m%d_%H%M%S).sql ++``` ++ ++### 第二步:停止生产应用 ++ ++```bash ++# SSH登录到生产服务器 ++ssh root@your-server-ip ++ ++# 进入应用目录 ++cd /www/wwwroot/zjpb ++ ++# 停止应用 ++./manage.sh stop ++``` ++ ++### 第三步:备份现有代码 ++ ++```bash ++# 创建备份目录 ++cd /www/wwwroot ++cp -r zjpb zjpb_backup_$(date +%Y%m%d_%H%M%S) ++``` ++ ++### 第四步:上传新代码 ++ ++**方法1:使用Git(推荐)** ++ ++```bash ++# 在本地提交所有修改 ++git add . ++git commit -m "release: v2.1 - Prompt管理系统、页脚优化、图标修复" ++git push origin master ++ ++# 在服务器上拉取 ++cd /www/wwwroot/zjpb ++git pull origin master ++``` ++ ++**方法2:手动上传** ++ ++```bash ++# 在本地压缩(排除不需要的文件) ++zip -r zjpb_v2.1.zip . -x "*.pyc" "*__pycache__*" "*.git*" ".env" "venv/*" "test_*.py" "*.db" "nul" ++ ++# 上传到服务器 /www/wwwroot/zjpb_new.zip ++# 然后解压覆盖 ++cd /www/wwwroot/zjpb ++unzip -o ../zjpb_new.zip ++``` ++ ++### 第五步:安装新依赖(如有) ++ ++```bash ++cd /www/wwwroot/zjpb ++source venv/bin/activate ++pip install -r requirements.txt ++``` ++ ++### 第六步:运行数据库迁移 ++ ++```bash ++# 激活虚拟环境 ++source venv/bin/activate ++ ++# 运行迁移脚本(创建 prompt_templates 表) ++python migrate_prompts.py ++``` ++ ++**预期输出:** ++``` ++正在创建 prompt_templates 表... ++[OK] 表创建成功 ++正在初始化默认prompt模板... ++[OK] 默认prompt模板初始化成功 ++ - 标签生成: 1 ++ - 主要功能生成: 2 ++ - 详细介绍生成: 3 ++``` ++ ++### 第七步:重启应用 ++ ++```bash ++./manage.sh start ++``` ++ ++### 第八步:验证部署 ++ ++**检查项:** ++ ++1. **访问前台首页** ++ - 检查页脚是否显示ICP备案号 ++ - 检查Clarity统计是否加载(F12查看Network) ++ ++2. **访问详情页** ++ - 检查图标是否正常显示(不是Material Icons文本) ++ ++3. **登录后台** ++ - 检查是否有"Prompt管理"菜单 ++ - 进入Prompt管理,查看是否有3条默认数据 ++ ++4. **测试网站编辑** ++ - 编辑任意网站,检查标签是否正常显示(不是空白蓝框) ++ ++5. **测试AI功能** ++ - 创建新网站,测试"AI生成标签"功能 ++ - 测试"AI生成详细介绍"功能 ++ - 测试"AI生成主要功能"功能 ++ ++--- ++ ++## 🔄 回滚方案(如出现问题) ++ ++### 快速回滚代码 ++ ++```bash ++cd /www/wwwroot ++./zjpb/manage.sh stop ++ ++# 删除新版本 ++rm -rf zjpb ++ ++# 恢复备份 ++mv zjpb_backup_YYYYMMDD_HHMMSS zjpb ++ ++# 重启 ++cd zjpb ++./manage.sh start ++``` ++ ++### 恢复数据库 ++ ++```bash ++# 如果新表导致问题,可以删除新表 ++mysql -h 112.124.42.38 -u ai_nav_user -p ai_nav ++ ++# 在MySQL中执行 ++DROP TABLE IF EXISTS prompt_templates; ++``` ++ ++--- ++ ++## 📝 注意事项 ++ ++1. ✅ **本次更新不会影响现有数据** - 只是新增表,不修改现有表 ++2. ✅ **完全向后兼容** - 即使不运行迁移脚本,前台也能正常访问 ++3. ✅ **可以随时回滚** - 保留了完整备份 ++4. ⚠️ **必须备份数据库** - 虽然风险很低,但备份是必须的 ++5. ⚠️ **检查.env配置** - 确保生产环境的.env配置正确 ++ ++--- ++ ++## 🐛 常见问题 ++ ++### Q1: 迁移脚本报错 "表已存在" ++**A:** 说明之前已经运行过迁移,可以跳过此步骤 ++ ++### Q2: Prompt管理菜单看不到 ++**A:** 清除浏览器缓存,重新登录后台 ++ ++### Q3: 标签还是显示不出来 ++**A:** 清除浏览器缓存,强制刷新(Ctrl+F5) ++ ++### Q4: Clarity统计没加载 ++**A:** 检查网络是否能访问 clarity.ms,可能被墙 ++ ++--- ++ ++## 📞 技术支持 ++ ++如遇问题,请检查日志: ++ ++```bash ++# 查看应用日志 ++./manage.sh logs ++ ++# 查看错误日志 ++tail -f logs/error.log ++``` +diff --git a/app.py b/app.py +index 14288f1..7a2e392 100644 +--- a/app.py ++++ b/app.py +@@ -1,11 +1,12 @@ + import os ++import markdown + from flask import Flask, render_template, redirect, url_for, request, flash, jsonify + from flask_login import LoginManager, login_user, logout_user, login_required, current_user + from flask_admin import Admin, AdminIndexView, expose + from flask_admin.contrib.sqla import ModelView + from datetime import datetime + from config import config +-from models import db, Site, Tag, Admin as AdminModel, News ++from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate + from utils.website_fetcher import WebsiteFetcher + from utils.tag_generator import TagGenerator + +@@ -19,6 +20,14 @@ def create_app(config_name='default'): + # 初始化数据库 + db.init_app(app) + ++ # 添加Markdown过滤器 ++ @app.template_filter('markdown') ++ def markdown_filter(text): ++ """将Markdown文本转换为HTML""" ++ if not text: ++ return '' ++ return markdown.markdown(text, extensions=['nl2br', 'fenced_code']) ++ + # 初始化登录管理 + login_manager = LoginManager() + login_manager.init_app(app) +@@ -36,6 +45,22 @@ def create_app(config_name='default'): + # 获取所有启用的标签 + tags = Tag.query.order_by(Tag.sort_order.desc(), Tag.id).all() + ++ # 优化:使用一次SQL查询统计所有标签的网站数量 ++ tag_counts = {} ++ if tags: ++ # 使用JOIN查询一次性获取所有标签的网站数量 ++ from sqlalchemy import func ++ counts_query = db.session.query( ++ site_tags.c.tag_id, ++ func.count(site_tags.c.site_id).label('count') ++ ).join( ++ Site, site_tags.c.site_id == Site.id ++ ).filter( ++ Site.is_active == True ++ ).group_by(site_tags.c.tag_id).all() ++ ++ tag_counts = {tag_id: count for tag_id, count in counts_query} ++ + # 获取筛选参数 + tag_slug = request.args.get('tag') + search_query = request.args.get('q', '').strip() +@@ -57,7 +82,7 @@ def create_app(config_name='default'): + pagination = None + return render_template('index_new.html', sites=sites, tags=tags, + selected_tag=selected_tag, search_query=search_query, +- pagination=pagination) ++ pagination=pagination, tag_counts=tag_counts) + + # 搜索功能 + if search_query: +@@ -79,7 +104,7 @@ def create_app(config_name='default'): + + return render_template('index_new.html', sites=sites, tags=tags, + selected_tag=selected_tag, search_query=search_query, +- pagination=pagination) ++ pagination=pagination, tag_counts=tag_counts) + + @app.route('/site/') + def site_detail(code): +@@ -138,6 +163,51 @@ def create_app(config_name='default'): + logout_user() + return redirect(url_for('index')) + ++ @app.route('/admin/change-password', methods=['GET', 'POST']) ++ @login_required ++ def change_password(): ++ """修改密码""" ++ if request.method == 'POST': ++ old_password = request.form.get('old_password', '').strip() ++ new_password = request.form.get('new_password', '').strip() ++ confirm_password = request.form.get('confirm_password', '').strip() ++ ++ # 验证旧密码 ++ if not current_user.check_password(old_password): ++ flash('旧密码错误', 'error') ++ return render_template('admin/change_password.html') ++ ++ # 验证新密码 ++ if not new_password: ++ flash('新密码不能为空', 'error') ++ return render_template('admin/change_password.html') ++ ++ if len(new_password) < 6: ++ flash('新密码长度至少6位', 'error') ++ return render_template('admin/change_password.html') ++ ++ if new_password != confirm_password: ++ flash('两次输入的新密码不一致', 'error') ++ return render_template('admin/change_password.html') ++ ++ if old_password == new_password: ++ flash('新密码不能与旧密码相同', 'error') ++ return render_template('admin/change_password.html') ++ ++ # 修改密码 ++ try: ++ current_user.set_password(new_password) ++ db.session.commit() ++ flash('密码修改成功,请重新登录', 'success') ++ logout_user() ++ return redirect(url_for('admin_login')) ++ except Exception as e: ++ db.session.rollback() ++ flash(f'密码修改失败:{str(e)}', 'error') ++ return render_template('admin/change_password.html') ++ ++ return render_template('admin/change_password.html') ++ + # ========== API路由 ========== + @app.route('/api/fetch-website-info', methods=['POST']) + @login_required +@@ -165,17 +235,18 @@ def create_app(config_name='default'): + 'message': '无法获取网站信息,请检查URL是否正确或手动填写' + }) + +- # 下载Logo(如果有) ++ # 下载Logo到本地(如果有) + logo_path = None + if info.get('logo_url'): +- logo_path = fetcher.download_logo(info['logo_url']) ++ logo_path = fetcher.download_logo(info['logo_url'], save_dir='static/logos') + ++ # 如果下载失败,不返回远程URL,让用户手动上传 + return jsonify({ + 'success': True, + 'data': { + 'name': info.get('name', ''), + 'description': info.get('description', ''), +- 'logo': logo_path or info.get('logo_url', '') ++ 'logo': logo_path if logo_path else '' + } + }) + +@@ -185,6 +256,148 @@ def create_app(config_name='default'): + 'message': f'抓取失败: {str(e)}' + }), 500 + ++ @app.route('/api/upload-logo', methods=['POST']) ++ @login_required ++ def upload_logo(): ++ """上传Logo图片API""" ++ try: ++ # 检查文件是否存在 ++ if 'logo' not in request.files: ++ return jsonify({ ++ 'success': False, ++ 'message': '请选择要上传的图片' ++ }), 400 ++ ++ file = request.files['logo'] ++ ++ # 检查文件名 ++ if file.filename == '': ++ return jsonify({ ++ 'success': False, ++ 'message': '未选择文件' ++ }), 400 ++ ++ # 检查文件类型 ++ allowed_extensions = {'png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp'} ++ filename = file.filename.lower() ++ if not any(filename.endswith('.' + ext) for ext in allowed_extensions): ++ return jsonify({ ++ 'success': False, ++ 'message': '不支持的文件格式,请上传图片文件(png, jpg, gif, svg, ico, webp)' ++ }), 400 ++ ++ # 创建保存目录 ++ save_dir = 'static/logos' ++ os.makedirs(save_dir, exist_ok=True) ++ ++ # 生成安全的文件名 ++ import time ++ import hashlib ++ ext = os.path.splitext(filename)[1] ++ timestamp = str(int(time.time() * 1000)) ++ hash_name = hashlib.md5(f"{filename}{timestamp}".encode()).hexdigest()[:16] ++ safe_filename = f"logo_{hash_name}{ext}" ++ filepath = os.path.join(save_dir, safe_filename) ++ ++ # 保存文件 ++ file.save(filepath) ++ ++ # 返回相对路径 ++ return jsonify({ ++ 'success': True, ++ 'path': f'/{filepath.replace(os.sep, "/")}' ++ }) ++ ++ except Exception as e: ++ return jsonify({ ++ 'success': False, ++ 'message': f'上传失败: {str(e)}' ++ }), 500 ++ ++ @app.route('/api/generate-features', methods=['POST']) ++ @login_required ++ def generate_features(): ++ """使用DeepSeek自动生成网站主要功能""" ++ try: ++ data = request.get_json() ++ name = data.get('name', '').strip() ++ description = data.get('description', '').strip() ++ url = data.get('url', '').strip() ++ ++ if not name or not description: ++ return jsonify({ ++ 'success': False, ++ 'message': '请提供网站名称和描述' ++ }), 400 ++ ++ # 生成功能列表 ++ generator = TagGenerator() ++ features = generator.generate_features(name, description, url) ++ ++ if not features: ++ return jsonify({ ++ 'success': False, ++ 'message': 'DeepSeek功能生成失败,请检查API配置' ++ }), 500 ++ ++ return jsonify({ ++ 'success': True, ++ 'features': features ++ }) ++ ++ except ValueError as e: ++ return jsonify({ ++ 'success': False, ++ 'message': str(e) ++ }), 400 ++ except Exception as e: ++ return jsonify({ ++ 'success': False, ++ 'message': f'生成失败: {str(e)}' ++ }), 500 ++ ++ @app.route('/api/generate-description', methods=['POST']) ++ @login_required ++ def generate_description(): ++ """使用DeepSeek自动生成网站详细介绍""" ++ try: ++ data = request.get_json() ++ name = data.get('name', '').strip() ++ short_desc = data.get('short_desc', '').strip() ++ url = data.get('url', '').strip() ++ ++ if not name: ++ return jsonify({ ++ 'success': False, ++ 'message': '请提供网站名称' ++ }), 400 ++ ++ # 生成详细介绍 ++ generator = TagGenerator() ++ description = generator.generate_description(name, short_desc, url) ++ ++ if not description: ++ return jsonify({ ++ 'success': False, ++ 'message': 'DeepSeek详细介绍生成失败,请检查API配置' ++ }), 500 ++ ++ return jsonify({ ++ 'success': True, ++ 'description': description ++ }) ++ ++ except ValueError as e: ++ return jsonify({ ++ 'success': False, ++ 'message': str(e) ++ }), 400 ++ except Exception as e: ++ return jsonify({ ++ 'success': False, ++ 'message': f'生成失败: {str(e)}' ++ }), 500 ++ + @app.route('/api/generate-tags', methods=['POST']) + @login_required + def generate_tags(): +@@ -345,11 +558,11 @@ def create_app(config_name='default'): + }) + continue + +- # 4. 下载Logo(失败不影响导入) ++ # 4. 下载Logo到本地(失败不影响导入) + logo_path = None + if info.get('logo_url'): + try: +- logo_path = fetcher.download_logo(info['logo_url']) ++ logo_path = fetcher.download_logo(info['logo_url'], save_dir='static/logos') + except Exception as e: + print(f"下载Logo失败 ({url}): {str(e)}") + # Logo下载失败不影响网站导入 +@@ -544,9 +757,47 @@ def create_app(config_name='default'): + import re + import random + from pypinyin import lazy_pinyin ++ from flask import request + + # 使用no_autoflush防止在查询时触发提前flush + with db.session.no_autoflush: ++ # 处理手动输入的新标签 ++ new_tags_str = request.form.get('new_tags', '') ++ if new_tags_str: ++ new_tag_names = [name.strip() for name in new_tags_str.split(',') if name.strip()] ++ for tag_name in new_tag_names: ++ # 检查标签是否已存在 ++ existing_tag = Tag.query.filter_by(name=tag_name).first() ++ if not existing_tag: ++ # 创建新标签 ++ tag_slug = ''.join(lazy_pinyin(tag_name)) ++ tag_slug = tag_slug.lower() ++ tag_slug = re.sub(r'[^\w\s-]', '', tag_slug) ++ tag_slug = re.sub(r'[-\s]+', '-', tag_slug).strip('-') ++ ++ # 确保slug唯一 ++ base_tag_slug = tag_slug[:50] ++ counter = 1 ++ final_tag_slug = tag_slug ++ while Tag.query.filter_by(slug=final_tag_slug).first(): ++ final_tag_slug = f"{base_tag_slug}-{counter}" ++ counter += 1 ++ if counter > 100: ++ final_tag_slug = f"{base_tag_slug}-{random.randint(1000, 9999)}" ++ break ++ ++ new_tag = Tag(name=tag_name, slug=final_tag_slug) ++ db.session.add(new_tag) ++ db.session.flush() # 确保新标签有ID ++ ++ # 添加到模型的标签列表 ++ if new_tag not in model.tags: ++ model.tags.append(new_tag) ++ else: ++ # 添加已存在的标签 ++ if existing_tag not in model.tags: ++ model.tags.append(existing_tag) ++ + # 如果code为空,自动生成唯一的8位数字编码 + if not model.code or model.code.strip() == '': + max_attempts = 10 +@@ -684,6 +935,51 @@ def create_app(config_name='default'): + ] + } + ++ # Prompt模板管理视图 ++ class PromptAdmin(SecureModelView): ++ can_edit = True ++ can_delete = False # 不允许删除,避免系统必需的prompt被删除 ++ can_create = True ++ can_view_details = False ++ ++ # 显示操作列 ++ column_display_actions = True ++ ++ column_list = ['id', 'key', 'name', 'description', 'is_active', 'updated_at'] ++ column_searchable_list = ['key', 'name', 'description'] ++ column_filters = ['is_active', 'key'] ++ column_labels = { ++ 'id': 'ID', ++ 'key': '唯一标识', ++ 'name': '模板名称', ++ 'system_prompt': '系统提示词', ++ 'user_prompt_template': '用户提示词模板', ++ 'description': '模板说明', ++ 'is_active': '是否启用', ++ 'created_at': '创建时间', ++ 'updated_at': '更新时间' ++ } ++ form_columns = ['key', 'name', 'description', 'system_prompt', 'user_prompt_template', 'is_active'] ++ ++ # 字段说明 ++ column_descriptions = { ++ 'key': '唯一标识,如: tags, features, description', ++ 'system_prompt': 'AI的系统角色设定', ++ 'user_prompt_template': '用户提示词模板,支持变量如 {name}, {description}, {url}', ++ } ++ ++ # 表单字段配置 ++ form_widget_args = { ++ 'system_prompt': { ++ 'rows': 3, ++ 'style': 'font-family: monospace;' ++ }, ++ 'user_prompt_template': { ++ 'rows': 20, ++ 'style': 'font-family: monospace;' ++ } ++ } ++ + # 初始化 Flask-Admin + admin = Admin( + app, +@@ -696,6 +992,7 @@ def create_app(config_name='default'): + admin.add_view(SiteAdmin(Site, db.session, name='网站管理')) + admin.add_view(TagAdmin(Tag, db.session, name='标签管理')) + admin.add_view(NewsAdmin(News, db.session, name='新闻管理')) ++ admin.add_view(PromptAdmin(PromptTemplate, db.session, name='Prompt管理')) + admin.add_view(AdminAdmin(AdminModel, db.session, name='管理员', endpoint='admin_users')) + + return app +diff --git a/export_data.py b/export_data.py +new file mode 100644 +index 0000000..df212a9 +--- /dev/null ++++ b/export_data.py +@@ -0,0 +1,58 @@ ++# -*- coding: utf-8 -*- ++""" ++数据导出脚本 ++导出现有数据到SQL文件 ++""" ++import subprocess ++import os ++from datetime import datetime ++ ++def export_database(): ++ """导出数据库到SQL文件""" ++ # 从.env读取数据库配置 ++ from dotenv import load_dotenv ++ load_dotenv() ++ ++ db_host = os.getenv('DB_HOST', 'localhost') ++ db_port = os.getenv('DB_PORT', '3306') ++ db_user = os.getenv('DB_USER') ++ db_password = os.getenv('DB_PASSWORD') ++ db_name = os.getenv('DB_NAME') ++ ++ # 生成备份文件名 ++ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') ++ backup_file = f'backup_{db_name}_{timestamp}.sql' ++ ++ # mysqldump命令 ++ cmd = [ ++ 'mysqldump', ++ '-h', db_host, ++ '-P', db_port, ++ '-u', db_user, ++ f'-p{db_password}', ++ '--single-transaction', ++ '--routines', ++ '--triggers', ++ db_name ++ ] ++ ++ print(f"正在导出数据库 {db_name} ...") ++ ++ try: ++ with open(backup_file, 'w', encoding='utf-8') as f: ++ subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, check=True) ++ ++ file_size = os.path.getsize(backup_file) / 1024 # KB ++ print(f"✓ 数据库导出成功!") ++ print(f" 文件: {backup_file}") ++ print(f" 大小: {file_size:.2f} KB") ++ print(f"\n请将此文件上传到服务器进行恢复") ++ ++ except subprocess.CalledProcessError as e: ++ print(f"✗ 导出失败: {e.stderr.decode()}") ++ except FileNotFoundError: ++ print("✗ 错误: 找不到 mysqldump 命令") ++ print(" 请确保MySQL客户端工具已安装并在PATH中") ++ ++if __name__ == '__main__': ++ export_database() +diff --git a/migrate_db.py b/migrate_db.py +new file mode 100644 +index 0000000..4d4bdef +--- /dev/null ++++ b/migrate_db.py +@@ -0,0 +1,49 @@ ++# -*- coding: utf-8 -*- ++""" ++数据库安全迁移脚本 ++只创建缺失的表,不删除现有数据 ++用于生产环境部署 ++""" ++ ++import sys ++from app import create_app ++from models import db, Admin ++ ++def migrate_database(): ++ """安全迁移数据库(不删除数据)""" ++ app = create_app('production') ++ ++ with app.app_context(): ++ print("正在检查数据库表...") ++ ++ # 只创建不存在的表(不会删除数据) ++ db.create_all() ++ print("✓ 数据库表检查完成") ++ ++ # 检查是否存在管理员 ++ admin_count = Admin.query.count() ++ ++ if admin_count == 0: ++ print("\n未找到管理员账号,正在创建默认管理员...") ++ admin = Admin( ++ username='admin', ++ email='admin@example.com', ++ is_active=True ++ ) ++ admin.set_password('admin123') ++ db.session.add(admin) ++ db.session.commit() ++ ++ print("✓ 默认管理员创建成功") ++ print(" 用户名: admin") ++ print(" 密码: admin123") ++ print("\n⚠️ 请立即登录后台修改密码!") ++ else: ++ print(f"\n✓ 已存在 {admin_count} 个管理员账号") ++ ++ print("\n" + "="*50) ++ print("数据库迁移完成!") ++ print("="*50) ++ ++if __name__ == '__main__': ++ migrate_database() +diff --git a/migrate_prompts.py b/migrate_prompts.py +new file mode 100644 +index 0000000..a6bd693 +--- /dev/null ++++ b/migrate_prompts.py +@@ -0,0 +1,141 @@ ++"""创建 prompt_templates 表并初始化默认数据""" ++import os ++import sys ++from app import create_app ++from models import db, PromptTemplate ++ ++# 设置输出编码为UTF-8 ++if sys.platform.startswith('win'): ++ sys.stdout.reconfigure(encoding='utf-8') ++ ++def migrate_prompts(): ++ """创建表并初始化默认prompt模板""" ++ app = create_app(os.getenv('FLASK_ENV', 'development')) ++ ++ with app.app_context(): ++ # 创建表 ++ print("正在创建 prompt_templates 表...") ++ db.create_all() ++ print("[OK] 表创建成功") ++ ++ # 检查是否已有数据 ++ existing_count = PromptTemplate.query.count() ++ if existing_count > 0: ++ print(f"[WARN] 表中已有 {existing_count} 条数据,跳过初始化") ++ return ++ ++ # 初始化默认prompt模板 ++ print("正在初始化默认prompt模板...") ++ ++ # 1. 标签生成模板 ++ tags_prompt = PromptTemplate( ++ key='tags', ++ name='标签生成', ++ description='根据网站名称和描述生成3-5个分类标签', ++ system_prompt='你是一个专业的AI工具分类专家,擅长为各类AI产品生成准确的标签。', ++ user_prompt_template='''你是一个AI工具导航网站的标签生成助手。根据以下产品信息,生成3-5个最合适的标签。 ++ ++产品名称: {name} ++ ++产品描述: {description} ++{existing_tags} ++ ++要求: ++1. 标签应该准确描述产品的功能、类型或应用场景 ++2. 每个标签2-4个汉字 ++3. 标签要具体且有区分度 ++4. 如果是AI工具,可以标注具体的AI类型(如"GPT"、"图像生成"等) ++5. 只返回标签,用逗号分隔,不要其他说明 ++ ++示例输出格式:写作助手,营销,GPT,内容生成 ++ ++请生成标签:''', ++ is_active=True ++ ) ++ ++ # 2. 主要功能生成模板 ++ features_prompt = PromptTemplate( ++ key='features', ++ name='主要功能生成', ++ description='根据网站名称和描述生成5-8个主要功能点', ++ system_prompt='你是一个专业的AI产品文案专家,擅长提炼产品核心功能和价值点。', ++ user_prompt_template='''你是一个AI工具导航网站的内容编辑助手。根据以下产品信息,生成详细的主要功能列表。 ++ ++产品名称: {name} ++ ++产品描述: {description} ++{url_info} ++ ++要求: ++1. 生成5-8个主要功能点 ++2. 每个功能点要具体、清晰、有吸引力 ++3. 使用Markdown无序列表格式(以"- "开头) ++4. 每个功能点一行,简洁有力(10-30字) ++5. 突出产品的核心价值和特色功能 ++6. 使用专业但易懂的语言 ++7. 不要添加任何标题或额外说明,直接输出功能列表 ++ ++示例输出格式: ++- 智能文本生成,支持多种写作场景 ++- 实时语法检查和优化建议 ++- 多语言翻译,准确率高达95% ++- 一键生成营销文案和广告语 ++- 团队协作,支持多人同时编辑 ++ ++请生成功能列表:''', ++ is_active=True ++ ) ++ ++ # 3. 详细介绍生成模板 ++ description_prompt = PromptTemplate( ++ key='description', ++ name='详细介绍生成', ++ description='根据网站名称和简短描述生成200-400字的详细介绍', ++ system_prompt='你是一个专业的AI产品文案专家,擅长撰写准确、客观、有吸引力的产品介绍。', ++ user_prompt_template='''你是一个AI工具导航网站的内容编辑助手。根据以下产品信息,生成详细、专业且吸引人的产品介绍。 ++ ++产品名称: {name} ++{short_desc_info} ++{url_info} ++ ++要求: ++1. 生成200-400字的详细介绍 ++2. 包含以下内容: ++ - 产品定位和核心价值(这是什么产品,解决什么问题) ++ - 主要特点和优势(为什么选择这个产品) ++ - 适用场景和目标用户(谁会用,用在哪里) ++3. 使用Markdown格式,可以包含: ++ - 段落分隔(空行) ++ - 加粗重点内容(**文字**) ++ - 列表(- 列表项) ++4. 语言专业但易懂,突出产品价值 ++5. 不要添加标题,直接输出正文内容 ++6. 语气客观、事实性强,避免过度营销 ++ ++示例输出格式: ++ChatGPT是由OpenAI开发的**先进对话式AI助手**,基于GPT-4大语言模型构建。它能够理解和生成自然语言,为用户提供智能对话、内容创作、代码编写等多种服务。 ++ ++**核心优势:** ++- 强大的语言理解和生成能力 ++- 支持多轮对话,上下文连贯 ++- 覆盖编程、写作、翻译等多个领域 ++ ++适用于内容创作者、程序员、学生等各类用户,可用于日常问答、文案撰写、学习辅导、编程助手等多种场景。 ++ ++请生成详细介绍:''', ++ is_active=True ++ ) ++ ++ # 添加到数据库 ++ db.session.add(tags_prompt) ++ db.session.add(features_prompt) ++ db.session.add(description_prompt) ++ db.session.commit() ++ ++ print("[OK] 默认prompt模板初始化成功") ++ print(f" - 标签生成: {tags_prompt.id}") ++ print(f" - 主要功能生成: {features_prompt.id}") ++ print(f" - 详细介绍生成: {description_prompt.id}") ++ ++if __name__ == '__main__': ++ migrate_prompts() +diff --git a/models.py b/models.py +index 57e988a..fae1c97 100644 +--- a/models.py ++++ b/models.py +@@ -136,3 +136,35 @@ class Admin(UserMixin, db.Model): + + def __repr__(self): + return f'' ++ ++class PromptTemplate(db.Model): ++ """AI提示词模板模型""" ++ __tablename__ = 'prompt_templates' ++ ++ id = db.Column(db.Integer, primary_key=True) ++ key = db.Column(db.String(50), unique=True, nullable=False, comment='唯一标识(tags/features/description)') ++ name = db.Column(db.String(100), nullable=False, comment='模板名称') ++ system_prompt = db.Column(db.Text, nullable=False, comment='系统提示词') ++ user_prompt_template = db.Column(db.Text, nullable=False, comment='用户提示词模板(支持变量)') ++ description = db.Column(db.String(200), comment='模板说明') ++ is_active = db.Column(db.Boolean, default=True, comment='是否启用') ++ created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间') ++ updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间') ++ ++ def __repr__(self): ++ return f'' ++ ++ def to_dict(self): ++ """转换为字典""" ++ return { ++ 'id': self.id, ++ 'key': self.key, ++ 'name': self.name, ++ 'system_prompt': self.system_prompt, ++ 'user_prompt_template': self.user_prompt_template, ++ 'description': self.description, ++ 'is_active': self.is_active, ++ 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None, ++ 'updated_at': self.updated_at.strftime('%Y-%m-%d %H:%M:%S') if self.updated_at else None ++ } ++ +diff --git a/templates/admin/site/create.html b/templates/admin/site/create.html +index f32bed6..ed0597f 100644 +--- a/templates/admin/site/create.html ++++ b/templates/admin/site/create.html +@@ -11,6 +11,28 @@ + margin-top: 10px; + margin-bottom: 15px; + } ++ .generate-features-btn { ++ margin-top: 10px; ++ margin-bottom: 15px; ++ } ++ .upload-logo-btn { ++ margin-top: 10px; ++ margin-bottom: 15px; ++ } ++ .logo-preview { ++ margin-top: 10px; ++ max-width: 200px; ++ max-height: 200px; ++ display: none; ++ border: 1px solid #ddd; ++ border-radius: 8px; ++ padding: 10px; ++ } ++ .logo-preview img { ++ max-width: 100%; ++ max-height: 150px; ++ object-fit: contain; ++ } + .fetch-status { + margin-top: 10px; + padding: 10px; +@@ -47,6 +69,26 @@ + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } ++ /* 标签输入框样式 */ ++ .tag-input-wrapper { ++ margin-top: 10px; ++ } ++ .tag-input-field { ++ width: 100%; ++ padding: 8px 12px; ++ border: 1px solid #DCDFE6; ++ border-radius: 4px; ++ font-size: 14px; ++ } ++ .tag-input-field:focus { ++ outline: none; ++ border-color: #0052D9; ++ } ++ .tag-input-help { ++ margin-top: 5px; ++ font-size: 12px; ++ color: #606266; ++ } + + + + {% endblock %} +diff --git a/templates/admin/site/edit.html b/templates/admin/site/edit.html +index aef6d86..6594b5f 100644 +--- a/templates/admin/site/edit.html ++++ b/templates/admin/site/edit.html +@@ -11,6 +11,28 @@ + margin-top: 10px; + margin-bottom: 15px; + } ++ .generate-features-btn { ++ margin-top: 10px; ++ margin-bottom: 15px; ++ } ++ .upload-logo-btn { ++ margin-top: 10px; ++ margin-bottom: 15px; ++ } ++ .logo-preview { ++ margin-top: 10px; ++ max-width: 200px; ++ max-height: 200px; ++ display: none; ++ border: 1px solid #ddd; ++ border-radius: 8px; ++ padding: 10px; ++ } ++ .logo-preview img { ++ max-width: 100%; ++ max-height: 150px; ++ object-fit: contain; ++ } + .fetch-status { + margin-top: 10px; + padding: 10px; +@@ -47,6 +69,26 @@ + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } ++ /* 标签输入框样式 */ ++ .tag-input-wrapper { ++ margin-top: 10px; ++ } ++ .tag-input-field { ++ width: 100%; ++ padding: 8px 12px; ++ border: 1px solid #DCDFE6; ++ border-radius: 4px; ++ font-size: 14px; ++ } ++ .tag-input-field:focus { ++ outline: none; ++ border-color: #0052D9; ++ } ++ .tag-input-help { ++ margin-top: 5px; ++ font-size: 12px; ++ color: #606266; ++ } + + + ++ + {% block extra_js %}{% endblock %} + + +diff --git a/templates/detail_new.html b/templates/detail_new.html +index 1033f7c..c84192f 100644 +--- a/templates/detail_new.html ++++ b/templates/detail_new.html +@@ -20,12 +20,6 @@ + color: var(--text-primary); + } + +- .back-link .material-symbols-outlined { +- font-size: 24px; +- line-height: 1; +- vertical-align: middle; +- margin-top: -2px; +- } + + /* 产品头部区域 */ + .product-header-wrapper { +@@ -84,9 +78,6 @@ + text-decoration: underline; + } + +- .product-link .material-symbols-outlined { +- font-size: 16px; +- } + + .product-meta { + display: flex; +@@ -103,9 +94,6 @@ + font-size: 14px; + } + +- .meta-item .material-symbols-outlined { +- font-size: 18px; +- } + + .product-tags-list { + display: flex; +@@ -195,9 +183,6 @@ + box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3); + } + +- .visit-btn .material-symbols-outlined { +- font-size: 18px; +- } + + .visit-hint { + text-align: center; +@@ -242,10 +227,6 @@ + margin-bottom: 20px; + } + +- .content-block h2 .material-symbols-outlined { +- font-size: 24px; +- color: var(--primary-blue); +- } + + .content-block p { + color: var(--text-secondary); +@@ -399,6 +380,126 @@ + font-weight: 500; + } + ++ /* Markdown内容样式 */ ++ .markdown-content { ++ color: var(--text-secondary); ++ line-height: 1.8; ++ } ++ ++ .markdown-content h1, ++ .markdown-content h2, ++ .markdown-content h3 { ++ color: var(--text-primary); ++ font-weight: 600; ++ margin-top: 24px; ++ margin-bottom: 16px; ++ line-height: 1.4; ++ } ++ ++ .markdown-content h1 { ++ font-size: 24px; ++ } ++ ++ .markdown-content h2 { ++ font-size: 20px; ++ } ++ ++ .markdown-content h3 { ++ font-size: 18px; ++ } ++ ++ .markdown-content p { ++ margin-bottom: 16px; ++ line-height: 1.8; ++ } ++ ++ .markdown-content ul, ++ .markdown-content ol { ++ margin: 16px 0; ++ padding-left: 24px; ++ } ++ ++ .markdown-content ul li { ++ list-style: none; ++ position: relative; ++ padding-left: 20px; ++ margin-bottom: 12px; ++ line-height: 1.8; ++ } ++ ++ .markdown-content ul li:before { ++ content: "▸"; ++ position: absolute; ++ left: 0; ++ color: var(--primary-blue); ++ font-weight: bold; ++ } ++ ++ .markdown-content ol li { ++ margin-bottom: 12px; ++ line-height: 1.8; ++ padding-left: 8px; ++ } ++ ++ .markdown-content code { ++ background: #f1f5f9; ++ color: #e11d48; ++ padding: 2px 6px; ++ border-radius: 4px; ++ font-family: 'Consolas', 'Monaco', 'Courier New', monospace; ++ font-size: 0.9em; ++ } ++ ++ .markdown-content pre { ++ background: #1e293b; ++ color: #e2e8f0; ++ padding: 16px; ++ border-radius: 8px; ++ overflow-x: auto; ++ margin: 16px 0; ++ } ++ ++ .markdown-content pre code { ++ background: transparent; ++ color: inherit; ++ padding: 0; ++ border-radius: 0; ++ } ++ ++ .markdown-content strong { ++ font-weight: 600; ++ color: var(--text-primary); ++ } ++ ++ .markdown-content em { ++ font-style: italic; ++ } ++ ++ .markdown-content a { ++ color: var(--primary-blue); ++ text-decoration: none; ++ border-bottom: 1px solid transparent; ++ transition: border-color 0.2s; ++ } ++ ++ .markdown-content a:hover { ++ border-bottom-color: var(--primary-blue); ++ } ++ ++ .markdown-content blockquote { ++ border-left: 4px solid var(--primary-blue); ++ padding-left: 16px; ++ margin: 16px 0; ++ color: var(--text-secondary); ++ font-style: italic; ++ } ++ ++ .markdown-content hr { ++ border: none; ++ border-top: 1px solid var(--border-color); ++ margin: 24px 0; ++ } ++ + /* 响应式 */ + @media (max-width: 968px) { + .product-header-wrapper { +@@ -432,7 +533,7 @@ + + + +- arrow_back ++ + 返回首页 + + +@@ -456,16 +557,16 @@ +

{{ site.name }}

+ + {{ site.url }} +- open_in_new ++ + + +
+
+- visibility ++ 👁 + {{ site.view_count | default(0) }} 次浏览 +
+
+- calendar_today ++ 📅 + 添加于 {{ site.created_at.strftime('%Y年%m月%d日') }} +
+
+@@ -488,7 +589,7 @@ + + + 访问网站 +- north_east ++ + +

在新标签页打开 • {{ site.url.split('/')[2] if site.url else '' }}

+ +@@ -501,20 +602,20 @@ + +
+

+- info ++ ℹ️ + 产品概述 +

+-

{{ site.description }}

++
{{ site.description | markdown | safe }}
+
+ + + {% if site.features %} +
+

+- description +- 详细描述 ++ 📋 ++ 主要功能 +

+-
{{ site.features | safe }}
++
{{ site.features | markdown | safe }}
+
+ {% endif %} + +@@ -522,7 +623,7 @@ + {% if news_list %} +
+

+- newspaper ++ 📰 + 相关新闻 +

+ {% for news in news_list %} +@@ -540,7 +641,7 @@ + {% if recommended_sites %} +
+

+- auto_awesome ++ + 相似推荐 +

+
+@@ -560,7 +661,7 @@ + {% endfor %} +
+
+- north_east ++ + + {% endfor %} +
+-- +2.50.1.windows.1 + diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..93fa2fd --- /dev/null +++ b/wsgi.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +""" +生产环境应用入口 +用于gunicorn启动 +""" +import os +from app import create_app + +# 创建应用实例 +app = create_app(os.getenv('FLASK_ENV', 'production')) + +if __name__ == '__main__': + # 这个分支仅用于直接运行脚本测试 + # 生产环境应该使用 gunicorn + app.run(host='0.0.0.0', port=5000, debug=False)