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