From 9f5d00609097e2e111045ab6bd79f6c0a99fc887 Mon Sep 17 00:00:00 2001 From: ZJPB Admin Date: Tue, 30 Dec 2025 01:17:08 +0800 Subject: [PATCH] =?UTF-8?q?release:=20v2.1.0=20-=20Prompt=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=B3=BB=E7=BB=9F=E3=80=81=E9=A1=B5=E8=84=9A=E4=BC=98?= =?UTF-8?q?=E5=8C=96=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 --- DEPLOYMENT.md | 463 +++++ DEPLOY_CHECKLIST.md | 132 ++ INCREMENTAL_DEPLOY.md | 208 +++ QUICK_DEPLOY.md | 94 + app.py | 313 +++- deploy.sh | 96 + export_data.py | 58 + git_patch_deploy.sh | 93 + gunicorn_config.py | 44 + manage.sh | 61 + migrate_db.py | 49 + migrate_prompts.py | 141 ++ models.py | 32 + nul | 2 + one_click_deploy.sh | 121 ++ templates/admin/change_password.html | 370 ++++ templates/admin/site/create.html | 455 ++++- templates/admin/site/edit.html | 528 +++++- templates/base_new.html | 40 +- templates/detail_new.html | 165 +- test_deepseek.py | 52 + v2.1.0.patch | 2438 ++++++++++++++++++++++++++ wsgi.py | 15 + 23 files changed, 5871 insertions(+), 99 deletions(-) create mode 100644 DEPLOYMENT.md create mode 100644 DEPLOY_CHECKLIST.md create mode 100644 INCREMENTAL_DEPLOY.md create mode 100644 QUICK_DEPLOY.md create mode 100644 deploy.sh create mode 100644 export_data.py create mode 100644 git_patch_deploy.sh create mode 100644 gunicorn_config.py create mode 100644 manage.sh create mode 100644 migrate_db.py create mode 100644 migrate_prompts.py create mode 100644 nul create mode 100644 one_click_deploy.sh 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/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/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/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/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/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_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/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/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/nul b/nul new file mode 100644 index 0000000..718a5fd --- /dev/null +++ b/nul @@ -0,0 +1,2 @@ +timeout: invalid time interval ‘/t’ +Try 'timeout --help' for more information. 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/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/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 %}
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)