《AI大模型趣味实战 》第8集:多端适配 个人新闻头条 基于大模型和RSS聚合打造个人新闻电台(Flask WEB版) 2

发布于:2025-03-24 ⋅ 阅读:(35) ⋅ 点赞:(0)

《AI大模型趣味实战 》第8集:多端适配 个人新闻头条 基于大模型和RSS聚合打造个人新闻电台(Flask WEB版) 2

摘要
本文末尾介绍了如何实现新闻智能体的方法。在信息爆炸的时代,如何高效获取和筛选感兴趣的新闻内容成为一个现实问题。本文将带领读者通过Python和Flask框架,结合大模型的强大能力,构建一个个性化的新闻聚合平台,不仅能够自动收集整理各类RSS源的新闻,还能以语音播报的形式提供"新闻电台"功能。我们将重点探讨如何利用AI大模型优化新闻内容提取、自动生成标签分类,以及如何通过语音合成技术实现新闻播报功能,打造一个真正实用的个人新闻助手。

项目代码仓库:https://github.com/wyg5208/rss_news_flask

在这里插入图片描述
以下内容接上一个博客:《AI大模型趣味实战 》第7集:多端适配 个人新闻头条 基于大模型和RSS聚合打造个人新闻电台(Flask WEB版) 1

9. 系统日志集成

为了更好地监控系统运行状态和调试问题,我们需要实现一个完善的日志系统,实现日志文件自动轮转和网页查看功能:

# 日志系统配置
LOG_DIR = 'logs'
if not os.path.exists(LOG_DIR):
    os.makedirs(LOG_DIR)

# 内存缓冲区,用于在UI中显示最新日志
log_buffer = deque(maxlen=1000)

# 创建自定义的日志记录器
class MemoryHandler(logging.Handler):
    """将日志记录到内存缓冲区,用于Web界面显示"""
    def emit(self, record):
        log_entry = self.format(record)
        log_buffer.append({
            'time': datetime.datetime.fromtimestamp(record.created).strftime('%Y-%m-%d %H:%M:%S'),
            'level': record.levelname,
            'message': record.getMessage(),
            'formatted': log_entry
        })

# 配置日志记录器
logger = logging.getLogger('rss_app')
logger.setLevel(logging.INFO)

# 小时文件处理器,每小时自动创建一个新文件
hourly_handler = TimedRotatingFileHandler(
    filename=os.path.join(LOG_DIR, 'rss_app.log'),
    when='H',
    interval=1,
    backupCount=72,  # 保留3天的日志
    encoding='utf-8'
)
# 设置日志文件后缀格式为 年-月-日_小时
hourly_handler.suffix = "%Y-%m-%d_%H"
hourly_handler.setLevel(logging.INFO)
hourly_handler.setFormatter(console_format)
logger.addHandler(hourly_handler)

为了让用户能够在Web界面上查看系统日志,我们添加了相应的路由和API端点:

@app.route('/system_logs')
@login_required
def system_logs():
    """显示系统日志页面"""
    logger.info('访问系统日志页面')
    
    # 获取日志文件列表
    log_files = []
    try:
        # 获取所有日志文件并按修改时间排序
        log_pattern = os.path.join(LOG_DIR, 'rss_app.log*')
        all_log_files = glob.glob(log_pattern)
        all_log_files.sort(key=os.path.getmtime, reverse=True)
        
        for file_path in all_log_files:
            # 获取文件信息并添加到列表
            file_name = os.path.basename(file_path)
            file_stats = os.stat(file_path)
            file_size = file_stats.st_size / 1024  # KB
            file_time = datetime.datetime.fromtimestamp(file_stats.st_mtime).strftime('%Y-%m-%d %H:%M:%S')
            
            # 格式化显示名称
            if file_name == 'rss_app.log':
                display_name = f"当前日志 ({file_size:.1f} KB) - {file_time}"
            else:
                # 解析时间戳
                timestamp = file_name.replace('rss_app.log.', '')
                try:
                    parsed_time = datetime.datetime.strptime(timestamp, '%Y-%m-%d_%H')
                    display_name = f"{parsed_time.strftime('%Y-%m-%d %H:00')} ({file_size:.1f} KB)"
                except:
                    display_name = f"{file_name} ({file_size:.1f} KB) - {file_time}"
            
            log_files.append({
                'name': display_name,
                'path': file_path
            })
    except Exception as e:
        logger.error(f"获取日志文件列表出错: {str(e)}")
    
    # 统计信息
    stats = {
        'total': len(log_buffer),
        'error': sum(1 for log in log_buffer if log['level'] == 'ERROR'),
        'warning': sum(1 for log in log_buffer if log['level'] == 'WARNING'),
        'files': len(log_files)
    }
    
    return render_template('system_logs.html', log_files=log_files, stats=stats)

创建系统日志页面的模板,实现实时日志显示、日志过滤和历史日志查看功能:

<!-- templates/system_logs.html -->
{% extends "base.html" %}

{% block title %}系统日志{% endblock %}

{% block content %}
<div class="container-fluid">
    <h1 class="mb-4">系统日志</h1>
    
    <div class="row mb-4">
        <div class="col-md-12">
            <div class="card">
                <div class="card-header">
                    <div class="d-flex justify-content-between align-items-center">
                        <h5 class="mb-0">实时日志监控</h5>
                        <div>
                            <button id="refreshBtn" class="btn btn-sm btn-outline-primary">刷新</button>
                            <button id="clearBtn" class="btn btn-sm btn-outline-secondary">清除显示</button>
                            <div class="btn-group ms-2">
                                <button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown">
                                    过滤级别
                                </button>
                                <div class="dropdown-menu">
                                    <a class="dropdown-item log-filter active" href="#" data-level="all">全部</a>
                                    <a class="dropdown-item log-filter" href="#" data-level="ERROR">错误</a>
                                    <a class="dropdown-item log-filter" href="#" data-level="WARNING">警告</a>
                                    <a class="dropdown-item log-filter" href="#" data-level="INFO">信息</a>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="card-body">
                    <div class="log-stats mb-3">
                        <span class="badge bg-primary">总计: <span id="total-count">{{ stats.total }}</span></span>
                        <span class="badge bg-danger">错误: <span id="error-count">{{ stats.error }}</span></span>
                        <span class="badge bg-warning text-dark">警告: <span id="warning-count">{{ stats.warning }}</span></span>
                    </div>
                    <div id="log-container" class="log-display">
                        <div class="text-center my-5">
                            <div class="spinner-border text-primary" role="status">
                                <span class="visually-hidden">加载中...</span>
                            </div>
                            <p class="mt-2">加载日志数据...</p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <div class="row">
        <div class="col-md-12">
            <div class="card">
                <div class="card-header">
                    <h5 class="mb-0">日志文件 ({{ stats.files }})</h5>
                </div>
                <div class="card-body">
                    <div class="list-group">
                        {% for log_file in log_files %}
                        <a href="#" class="list-group-item list-group-item-action log-file-item" data-path="{{ log_file.path }}">
                            {{ log_file.name }}
                        </a>
                        {% else %}
                        <div class="text-center py-3">
                            <p class="text-muted mb-0">没有找到日志文件</p>
                        </div>
                        {% endfor %}
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <!-- 日志文件查看模态框 -->
    <div class="modal fade" id="logFileModal" tabindex="-1" aria-hidden="true">
        <div class="modal-dialog modal-xl modal-dialog-scrollable">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="logFileTitle">日志文件</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <pre id="logFileContent" class="log-file-content">加载中...</pre>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    // 日志系统交互JavaScript代码
    $(document).ready(function() {
        // 当前过滤级别
        let currentLevel = 'all';
        
        // 加载日志数据
        function loadLogs() {
            $.getJSON('/api/logs', { level: currentLevel }, function(data) {
                const logs = data.logs;
                const stats = data.stats;
                
                // 更新统计信息
                $('#total-count').text(stats.total);
                $('#error-count').text(stats.error);
                $('#warning-count').text(stats.warning);
                
                // 清空并填充日志容器
                const container = $('#log-container');
                container.empty();
                
                if (logs.length === 0) {
                    container.html('<p class="text-center text-muted my-5">没有日志记录</p>');
                    return;
                }
                
                // 创建日志表格
                const table = $('<table class="table table-sm table-hover log-table"></table>');
                const tbody = $('<tbody></tbody>');
                
                // 添加日志行
                logs.forEach(log => {
                    const row = $('<tr></tr>');
                    
                    // 根据日志级别设置行样式
                    if (log.level === 'ERROR') {
                        row.addClass('table-danger');
                    } else if (log.level === 'WARNING') {
                        row.addClass('table-warning');
                    }
                    
                    row.append(`<td class="log-time">${log.time}</td>`);
                    row.append(`<td class="log-level">${log.level}</td>`);
                    row.append(`<td class="log-message">${log.message}</td>`);
                    
                    tbody.append(row);
                });
                
                table.append(tbody);
                container.append(table);
                
                // 滚动到底部
                container.scrollTop(container[0].scrollHeight);
            });
        }
        
        // 初始加载日志
        loadLogs();
        
        // 刷新按钮点击事件
        $('#refreshBtn').click(function() {
            loadLogs();
        });
        
        // 清除按钮点击事件
        $('#clearBtn').click(function() {
            $('#log-container').empty();
        });
        
        // 日志级别过滤器点击事件
        $('.log-filter').click(function(e) {
            e.preventDefault();
            
            // 更新选中状态
            $('.log-filter').removeClass('active');
            $(this).addClass('active');
            
            // 设置当前级别并重新加载
            currentLevel = $(this).data('level');
            loadLogs();
        });
        
        // 日志文件项点击事件
        $('.log-file-item').click(function(e) {
            e.preventDefault();
            
            const path = $(this).data('path');
            const name = $(this).text();
            
            // 设置模态框标题
            $('#logFileTitle').text(name);
            $('#logFileContent').text('加载中...');
            
            // 显示模态框
            const modal = new bootstrap.Modal(document.getElementById('logFileModal'));
            modal.show();
            
            // 加载日志文件内容
            $.getJSON('/api/logs/file', { file_path: path }, function(data) {
                if (data.status === 'success') {
                    $('#logFileContent').text(data.content);
                } else {
                    $('#logFileContent').text('加载失败: ' + data.message);
                }
            });
        });
        
        // 自动刷新(每10秒)
        setInterval(loadLogs, 10000);
    });
</script>
{% endblock %}

通过实现这些功能,我们的系统可以自动记录所有关键操作和错误信息,用户可以实时查看系统状态和历史日志,便于问题诊断和监控。

10. 移动设备优化与兼容性处理

针对移动设备访问,我们需要优化用户界面和交互体验,特别是在移动浏览器上的按钮功能:

// static/modal_fix.js
document.addEventListener('DOMContentLoaded', function() {
    console.log('modal_fix.js 已加载');
    
    // 检查jQuery和Bootstrap是否加载
    if (typeof jQuery === 'undefined') {
        console.error('jQuery 未加载!');
        return;
    }
    
    if (typeof bootstrap === 'undefined') {
        console.error('Bootstrap 未加载!');
        return;
    }
    
    console.log('jQuery和Bootstrap已正确加载');
    
    // 在新闻详情页初始化
    initNewsDetailPage();

    function initNewsDetailPage() {
        // 检查是否是新闻详情页
        if (!document.getElementById('news-content')) {
            return;
        }
        
        console.log('初始化新闻详情页面');
        
        // 初始化模态框
        const shareModal = new bootstrap.Modal(document.getElementById('shareModal'), {
            keyboard: true
        });
        
        const helpModal = new bootstrap.Modal(document.getElementById('helpModal'), {
            keyboard: true
        });
        
        // 重新绑定按钮事件
        $('#btnShare').off('click').on('click', function() {
            console.log('分享按钮被点击');
            shareModal.show();
        });
        
        $('#btnHelp').off('click').on('click', function() {
            console.log('帮助按钮被点击');
            helpModal.show();
        });
        
        // 语音朗读功能
        $('#btnSpeak').off('click').on('click', function() {
            try {
                console.log('朗读按钮被点击');
                const title = document.getElementById('news-title').innerText;
                const content = document.getElementById('news-content').innerText;
                
                console.log(`准备朗读,标题长度: ${title.length}, 内容长度: ${content.length}`);
                
                // 组合完整的朗读文本
                const fullText = title + '。' + content;
                
                // 尝试使用Web Speech API
                if ('speechSynthesis' in window) {
                    console.log('使用Web Speech API朗读');
                    
                    // 创建语音对象
                    const speech = new SpeechSynthesisUtterance();
                    speech.text = fullText;
                    speech.lang = 'zh-CN';
                    speech.rate = 1.0;  // 语速
                    speech.pitch = 1.0; // 音调
                    speech.volume = 1.0;  // 音量
                    
                    // 开始朗读
                    window.speechSynthesis.speak(speech);
                } else {
                    console.log('Web Speech API不可用,使用后端API');
                    
                    // 使用后端API生成语音
                    $.ajax({
                        url: '/api/text_to_speech',
                        type: 'POST',
                        contentType: 'application/json',
                        data: JSON.stringify({ text: fullText }),
                        success: function(response) {
                            if (response.status === 'success') {
                                console.log('语音生成成功,URL:', response.audio_url);
                                
                                // 创建音频元素播放
                                const audio = new Audio(response.audio_url);
                                audio.play();
                            } else {
                                console.error('语音生成失败:', response.message);
                                alert('语音生成失败: ' + response.message);
                            }
                        },
                        error: function(xhr, status, error) {
                            console.error('API请求失败:', error);
                            alert('无法连接到语音服务: ' + error);
                        }
                    });
                }
            } catch (e) {
                console.error('朗读过程出错:', e);
                alert('朗读功能出错: ' + e.message);
            }
        });
        
        // 复制内容功能
        $('#btnCopy').off('click').on('click', function() {
            try {
                console.log('复制按钮被点击');
                const title = document.getElementById('news-title').innerText;
                const description = document.getElementById('news-description').innerText;
                const content = document.getElementById('news-content').innerText;
                
                // 组合要复制的文本
                const textToCopy = `${title}\n\n${description}\n\n${content}`;
                
                // 使用剪贴板API
                navigator.clipboard.writeText(textToCopy).then(function() {
                    console.log('内容已复制到剪贴板');
                    alert('内容已复制到剪贴板');
                }).catch(function(err) {
                    console.error('剪贴板操作失败:', err);
                    alert('复制失败: ' + err.message);
                });
            } catch (e) {
                console.error('复制过程出错:', e);
                alert('复制功能出错: ' + e.message);
            }
        });
        
        // 复制链接按钮
        $('#copyLinkBtn').off('click').on('click', function() {
            try {
                const currentUrl = window.location.href;
                
                navigator.clipboard.writeText(currentUrl).then(function() {
                    alert('链接已复制到剪贴板');
                }).catch(function(err) {
                    console.error('复制链接失败:', err);
                    alert('复制链接失败: ' + err.message);
                });
            } catch (e) {
                console.error('复制链接过程出错:', e);
                alert('复制链接功能出错: ' + e.message);
            }
        });
        
        console.log('新闻详情页面按钮事件已重新绑定');
    }
});

在HTML模板中引入这个修复脚本:

<!-- templates/news_detail.html -->
{% extends "base.html" %}

{% block title %}{{ news.title }}{% endblock %}

{% block content %}
<div class="container">
    <nav aria-label="breadcrumb">
        <ol class="breadcrumb">
            <li class="breadcrumb-item"><a href="{{ url_for('dashboard') }}">首页</a></li>
            <li class="breadcrumb-item"><a href="{{ url_for('news_list') }}">新闻列表</a></li>
            <li class="breadcrumb-item active" aria-current="page">新闻详情</li>
        </ol>
    </nav>
    
    <div class="card">
        <div class="card-header">
            <h1 id="news-title" class="h3">{{ news.title }}</h1>
            <div class="mt-2 text-muted">
                <small>来源: {{ news.source }}</small>
                {% if news.pub_date %}
                <small class="ms-3">发布时间: {{ news.pub_date.strftime('%Y-%m-%d %H:%M') }}</small>
                {% endif %}
                <small class="ms-3">添加时间: {{ news.add_date.strftime('%Y-%m-%d %H:%M') }}</small>
            </div>
        </div>
        <div class="card-body">
            {% if news.description %}
            <div id="news-description" class="lead mb-4">{{ news.description }}</div>
            {% endif %}
            
            <div class="mb-3">
                <div class="btn-group" role="group">
                    <button id="btnSpeak" class="btn btn-outline-primary" type="button">
                        <i class="fas fa-volume-up"></i> 朗读
                    </button>
                    <button id="btnCopy" class="btn btn-outline-secondary" type="button">
                        <i class="fas fa-copy"></i> 复制内容
                    </button>
                    <button id="btnShare" class="btn btn-outline-success" type="button">
                        <i class="fas fa-share-alt"></i> 分享
                    </button>
                    <button id="btnHelp" class="btn btn-outline-info" type="button">
                        <i class="fas fa-question-circle"></i> 帮助
                    </button>
                </div>
            </div>
            
            <div id="news-content" class="news-content">
                {{ news.content|safe }}
            </div>
            
            {% if news.tags %}
            <div class="mt-4">
                <h5>标签</h5>
                <div class="news-tags">
                    {% for tag in news.tags %}
                    <a href="{{ url_for('news_list', tag='{{ tag.name }}') }}" class="badge bg-primary text-decoration-none">{{ tag.name }}</a>
                    {% endfor %}
                </div>
            </div>
            {% endif %}
            
            {% if news.link %}
            <div class="mt-4">
                <a href="{{ news.link }}" target="_blank" class="btn btn-sm btn-outline-dark">
                    <i class="fas fa-external-link-alt"></i> 查看原文
                </a>
            </div>
            {% endif %}
        </div>
    </div>
</div>

<!-- 分享模态框 -->
<div class="modal fade" id="shareModal" tabindex="-1" aria-labelledby="shareModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="shareModalLabel">分享此新闻</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <p>复制以下链接分享:</p>
                <div class="input-group">
                    <input type="text" class="form-control" id="shareLink" value="{{ request.url }}" readonly>
                    <button class="btn btn-outline-secondary" type="button" id="copyLinkBtn">复制</button>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
            </div>
        </div>
    </div>
</div>

<!-- 帮助模态框 -->
<div class="modal fade" id="helpModal" tabindex="-1" aria-labelledby="helpModalLabel" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="helpModalLabel">功能帮助</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div class="mb-3">
                    <h6><i class="fas fa-volume-up"></i> 朗读</h6>
                    <p>使用语音朗读当前新闻内容,支持中文朗读。</p>
                </div>
                <div class="mb-3">
                    <h6><i class="fas fa-copy"></i> 复制内容</h6>
                    <p>将新闻标题、描述和正文内容复制到剪贴板。</p>
                </div>
                <div class="mb-3">
                    <h6><i class="fas fa-share-alt"></i> 分享</h6>
                    <p>获取当前新闻的链接,以便分享给他人。</p>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="{{ url_for('static', filename='speech.js') }}"></script>
<script src="{{ url_for('static', filename='modal_fix.js') }}"></script>
{% endblock %}

通过这些优化,我们解决了移动设备上按钮不响应的问题,并提高了用户交互体验。

总结与扩展思考

项目梳理

在这个项目中,我们成功构建了一个功能完整的个人新闻聚合平台,它具有以下核心功能:

  1. RSS源管理与内容抓取:支持添加、删除和管理多个RSS源,自动获取最新新闻内容。

  2. 大模型内容优化:使用大语言模型(GLM4)对抓取的内容进行智能处理,提取关键内容并去除无关元素。

  3. 自动标签生成:基于大模型分析新闻内容,自动生成相关标签,便于内容分类和检索。

  4. 语音合成与播报:支持将新闻内容转换为语音,实现类似广播的新闻播报功能。

  5. 定时任务系统:实现新闻自动抓取和定时播报功能,减少手动操作。

  6. 系统日志与监控:完善的日志系统,支持Web界面查看系统运行状态和历史日志。

  7. 移动设备适配:优化移动端用户体验,确保在各种设备上都能正常使用。

通过这个项目,我们展示了如何将大语言模型应用于实际应用场景,利用其强大的文本理解与生成能力提升应用的智能化水平。同时,项目也演示了从数据获取、处理、存储到展示的完整流程,涵盖了Web开发的各个方面。

扩展思考

  1. 个性化推荐系统

基于用户的阅读历史和标签偏好,我们可以实现个性化的新闻推荐功能。这可以通过分析用户与标签的交互行为,构建用户画像,然后使用协同过滤或内容匹配算法实现。

def get_recommended_news(user_id, limit=10):
    """获取给用户推荐的新闻"""
    # 获取用户标签偏好
    user_preferences = UserTagPreference.query.filter_by(user_id=user_id).all()
    preferred_tags = [pref.tag.name for pref in user_preferences]
    
    # 获取含有这些标签的近期新闻
    recommended_news = []
    if preferred_tags:
        # 基于标签匹配的推荐
        tagged_news = db.session.query(News)\
            .join(Tag, News.id == Tag.news_id)\
            .filter(Tag.name.in_(preferred_tags))\
            .order_by(News.add_date.desc())\
            .limit(limit*2)\
            .all()
        
        recommended_news.extend(tagged_news)
    
    # 如果推荐数量不足,添加最新新闻
    if len(recommended_news) < limit:
        recent_news = News.query.order_by(News.add_date.desc())\
            .limit(limit - len(recommended_news))\
            .all()
        recommended_news.extend(recent_news)
    
    # 去重并限制数量
    unique_news = []
    news_ids = set()
    for news in recommended_news:
        if news.id not in news_ids and len(unique_news) < limit:
            news_ids.add(news.id)
            unique_news.append(news)
    
    return unique_news
  1. 情感分析与分类

使用大模型进行新闻的情感分析与主题分类,为用户提供更多维度的内容筛选。

def analyze_news_sentiment(news):
    """分析新闻情感倾向"""
    try:
        prompt = f"""
分析以下新闻文章的情感倾向,返回一个值:
正面 - 积极、乐观的内容
中性 - 客观、中立的报道
负面 - 消极、悲观的内容

只返回一个词:正面、

## 扩展思考

### 1. 多模态内容处理

当前项目主要处理文本内容,但现代新闻往往包含图片、视频等多模态内容。我们可以扩展系统以支持这些内容:

```python
def extract_images_from_content(content):
    """从HTML内容中提取图片链接"""
    try:
        soup = BeautifulSoup(content, 'html.parser')
        images = []
        
        # 查找所有图片标签
        for img in soup.find_all('img'):
            src = img.get('src')
            if src and not src.startswith('data:'):  # 排除base64编码的图片
                images.append({
                    'url': src,
                    'alt': img.get('alt', ''),
                    'width': img.get('width', ''),
                    'height': img.get('height', '')
                })
        
        return images
    except Exception as e:
        logger.error(f"提取图片时出错: {e}")
        return []

def analyze_images_with_llm(images):
    """使用大模型分析图片内容"""
    if not images:
        return []
    
    try:
        # 构建prompt
        image_urls = [img['url'] for img in images]
        prompt = f"""
分析以下新闻图片链接,提供每张图片可能的内容描述。
不需要访问链接,仅根据URL和alt文本推测内容。
图片链接:
{json.dumps(image_urls, indent=2)}
"""
        # 调用Ollama API
        response = ollama.chat(model='glm4', messages=[
            {
                'role': 'user',
                'content': prompt
            }
        ])
        
        return response['message']['content']
    except Exception as e:
        logger.error(f"分析图片时出错: {e}")
        return "无法分析图片内容"

2. 个性化推荐系统

基于用户阅读历史和兴趣标签,实现智能推荐功能:

class UserReadHistory(db.Model):
    """用户阅读历史模型"""
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    news_id = db.Column(db.Integer, db.ForeignKey('news.id'))
    read_time = db.Column(db.DateTime, default=datetime.datetime.now)
    read_duration = db.Column(db.Integer, default=0)  # 阅读时长(秒)
    user = db.relationship('User', backref=db.backref('read_history', lazy=True))
    news = db.relationship('News', backref=db.backref('read_by', lazy=True))

def get_personalized_recommendations(user_id, limit=10):
    """为用户生成个性化推荐"""
    # 1. 获取用户感兴趣的标签
    user_tags = db.session.query(TagLibrary.name)\
                .join(UserTagPreference, UserTagPreference.tag_id == TagLibrary.id)\
                .filter(UserTagPreference.user_id == user_id)\
                .all()
    user_tags = [t[0] for t in user_tags]
    
    # 2. 获取用户最近阅读的新闻中的标签
    recent_reads = db.session.query(News)\
                  .join(UserReadHistory, UserReadHistory.news_id == News.id)\
                  .filter(UserReadHistory.user_id == user_id)\
                  .order_by(UserReadHistory.read_time.desc())\
                  .limit(20)\
                  .all()
    
    recent_news_ids = [n.id for n in recent_reads]
    
    recent_tags = db.session.query(Tag.name)\
                 .filter(Tag.news_id.in_(recent_news_ids))\
                 .group_by(Tag.name)\
                 .all()
    recent_tags = [t[0] for t in recent_tags]
    
    # 3. 合并兴趣标签和最近阅读标签
    all_tags = set(user_tags + recent_tags)
    
    # 4. 查找包含这些标签的新闻,但用户尚未阅读
    if all_tags:
        recommended_news = db.session.query(News)\
                          .join(Tag, Tag.news_id == News.id)\
                          .filter(Tag.name.in_(all_tags))\
                          .filter(~News.id.in_(recent_news_ids))\
                          .order_by(News.add_date.desc())\
                          .limit(limit)\
                          .all()
        
        return recommended_news
    else:
        # 如果没有标签信息,返回最新新闻
        return News.query.order_by(News.add_date.desc()).limit(limit).all()

3. 社交分享与交互功能

增加社交功能,让用户能分享和讨论新闻内容:

class Comment(db.Model):
    """新闻评论模型"""
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.Text, nullable=False)
    news_id = db.Column(db.Integer, db.ForeignKey('news.id'))
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    created_at = db.Column(db.DateTime, default=datetime.datetime.now)
    news = db.relationship('News', backref=db.backref('comments', lazy=True))
    user = db.relationship('User', backref=db.backref('comments', lazy=True))

@app.route('/news/<int:news_id>/comment', methods=['POST'])
@login_required
def add_comment(news_id):
    """添加评论"""
    content = request.form.get('content', '').strip()
    if not content:
        flash('评论内容不能为空', 'warning')
        return redirect(url_for('news_detail', news_id=news_id))
    
    # 使用大模型检测不良内容
    is_appropriate = check_content_appropriate(content)
    if not is_appropriate:
        flash('评论内容不适当,请修改后重试', 'danger')
        return redirect(url_for('news_detail', news_id=news_id))
    
    # 创建评论
    comment = Comment(
        content=content,
        news_id=news_id,
        user_id=current_user.id
    )
    
    try:
        db.session.add(comment)
        db.session.commit()
        flash('评论发布成功', 'success')
    except Exception as e:
        db.session.rollback()
        logger.error(f"添加评论出错: {e}")
        flash('评论发布失败,请稍后重试', 'danger')
    
    return redirect(url_for('news_detail', news_id=news_id))

def check_content_appropriate(content):
    """使用大模型检查内容是否适当"""
    try:
        prompt = f"""
判断以下评论内容是否适当,不包含侮辱、歧视、暴力或政治敏感内容。
只回答"适当"或"不适当"。

评论内容: {content}
"""
        # 调用Ollama API
        response = ollama.chat(model='glm4', messages=[
            {
                'role': 'user',
                'content': prompt
            }
        ])
        
        result = response['message']['content'].strip().lower()
        return '适当' in result
    except Exception as e:
        logger.error(f"检查内容适当性时出错: {e}")
        return True  # 出错时默认允许

4. 语音交互与语音助手功能

将系统拓展为完整的语音助手,支持语音命令控制:

@app.route('/api/speech_command', methods=['POST'])
@login_required
def process_speech_command():
    """处理语音命令"""
    try:
        data = request.get_json()
        if not data or 'command' not in data:
            return jsonify({'status': 'error', 'message': '缺少命令参数'})
        
        command = data['command']
        logger.info(f"接收到语音命令: {command}")
        
        # 使用大模型解析命令
        parsed_command = parse_command_with_llm(command)
        logger.info(f"解析后的命令: {parsed_command}")
        
        # 执行相应操作
        if parsed_command['type'] == 'read_news':
            # 获取特定标签或最新的新闻
            if parsed_command.get('tag'):
                news_list = get_news_by_tag(parsed_command['tag'], parsed_command.get('count', 3))
            else:
                news_list = News.query.order_by(News.add_date.desc()).limit(parsed_command.get('count', 3)).all()
            
            # 生成语音
            news_text = "为您播报以下新闻:\n\n"
            for i, news in enumerate(news_list):
                news_text += f"第{i+1}条:{news.title}\n{news.description or ''}\n\n"
            
            # 调用语音合成API
            result = generate_speech_response(news_text)
            return jsonify(result)
            
        elif parsed_command['type'] == 'search_news':
            # 搜索新闻
            keyword = parsed_command.get('keyword', '')
            news_list = News.query.filter(News.title.contains(keyword) | News.description.contains(keyword))\
                           .order_by(News.add_date.desc())\
                           .limit(5)\
                           .all()
            
            if news_list:
                news_text = f"找到{len(news_list)}条关于"{keyword}"的新闻:\n\n"
                for i, news in enumerate(news_list):
                    news_text += f"第{i+1}条:{news.title}\n"
                
                result = generate_speech_response(news_text)
                return jsonify(result)
            else:
                return jsonify({
                    'status': 'success',
                    'message': f'没有找到关于"{keyword}"的新闻',
                    'audio_url': None
                })
                
        else:
            return jsonify({
                'status': 'error',
                'message': '无法识别的命令类型'
            })
            
    except Exception as e:
        logger.error(f"处理语音命令时出错: {e}")
        return jsonify({
            'status': 'error',
            'message': str(e)
        })

def parse_command_with_llm(command):
    """使用大模型解析语音命令"""
    try:
        prompt = f"""
解析以下语音命令,提取出命令类型和参数。返回JSON格式。
支持的命令类型:
1. read_news: 朗读新闻(可能包含标签和数量)
2. search_news: 搜索新闻(包含关键词)
3. system_status: 查询系统状态

例如:
- "给我读3条最新的科技新闻" -> {{"type": "read_news", "tag": "科技", "count": 3}}
- "搜索关于人工智能的新闻" -> {{"type": "search_news", "keyword": "人工智能"}}
- "查询系统状态" -> {{"type": "system_status"}}

命令: {command}
"""
        # 调用Ollama API
        response = ollama.chat(model='glm4', messages=[
            {
                'role': 'user',
                'content': prompt
            }
        ])
        
        # 尝试解析JSON响应
        content = response['message']['content']
        # 提取JSON部分
        json_match = re.search(r'\{.*\}', content, re.DOTALL)
        if json_match:
            json_str = json_match.group(0)
            return json.loads(json_str)
        else:
            # 默认返回
            return {'type': 'unknown'}
            
    except Exception as e:
        logger.error(f"解析语音命令时出错: {e}")
        return {'type': 'error', 'message': str(e)}

5. 多语言支持与翻译功能

添加多语言支持,使系统能够抓取、翻译和展示不同语言的新闻:

class News(db.Model):
    # ... 现有字段 ...
    language = db.Column(db.String(10), default='zh-cn')  # 新增字段
    translated_title = db.Column(db.String(500))
    translated_description = db.Column(db.Text)
    translated_content = db.Column(db.Text)

def detect_language(text):
    """检测文本语言"""
    try:
        prompt = f"""
请识别以下文本的语言,并返回相应的语言代码,如:
- 中文:zh-cn
- 英文:en
- 日文:ja
- 俄文:ru
等等。只返回语言代码,不需要其他解释。

文本:
{text[:200]}
"""
        # 调用Ollama API
        response = ollama.chat(model='glm4', messages=[
            {
                'role': 'user',
                'content': prompt
            }
        ])
        
        language_code = response['message']['content'].strip().lower()
        return language_code
    except Exception as e:
        logger.error(f"检测语言时出错: {e}")
        return 'zh-cn'  # 默认中文

def translate_with_llm(text, from_lang, to_lang='zh-cn'):
    """使用大模型翻译文本"""
    if not text or from_lang == to_lang:
        return text
    
    try:
        prompt = f"""
请将以下{from_lang}文本翻译成{to_lang},保持原意,注意专业术语的准确性:

原文:
{text[:5000]}

只返回翻译结果,不需要添加解释。
"""
        # 调用Ollama API
        response = ollama.chat(model='glm4', messages=[
            {
                'role': 'user',
                'content': prompt
            }
        ])
        
        translated_text = response['message']['content']
        return translated_text
    except Exception as e:
        logger.error(f"翻译文本时出错: {e}")
        return text

6. 智能摘要与内容浓缩

为长篇新闻生成简明摘要,方便用户快速了解内容:

def generate_summary_for_news(news, max_length=200):
    """为新闻生成摘要"""
    try:
        # 获取新闻正文
        content_text = ""
        if news.content:
            soup = BeautifulSoup(news.content, 'html.parser')
            content_text = soup.get_text(separator=' ', strip=True)
        
        # 如果没有内容或内容太短,使用描述
        if not content_text or len(content_text) < 100:
            if news.description:
                content_text = news.description
        
        # 如果还是没有内容,返回标题
        if not content_text:
            return news.title
        
        # 使用大模型生成摘要
        prompt = f"""
为以下新闻生成一个简洁的摘要,不超过{max_length}个字符:

标题:{news.title}
内容:{content_text[:3000]}

只返回摘要,不要添加任何解释。
"""
        # 调用Ollama API
        response = ollama.chat(model='glm4', messages=[
            {
                'role': 'user',
                'content': prompt
            }
        ])
        
        summary = response['message']['content'].strip()
        return summary
    except Exception as e:
        logger.error(f"生成摘要时出错: {e}")
        # 如果出错,返回原始描述或截断的内容
        if news.description:
            return news.description[:max_length] + ('...' if len(news.description) > max_length else '')
        return content_text[:max_length] + ('...' if len(content_text) > max_length else '')

7. 数据分析与可视化

添加数据分析功能,生成新闻趋势报告和可视化图表:

@app.route('/analytics')
@login_required
def analytics_dashboard():
    """数据分析仪表板"""
    # 获取时间范围
    days = request.args.get('days', 30, type=int)
    start_date = datetime.datetime.now() - datetime.timedelta(days=days)
    
    # 获取每日新闻数量
    daily_news_counts = db.session.query(
        func.date(News.add_date).label('date'),
        func.count().label('count')
    ).filter(News.add_date >= start_date).group_by(func.date(News.add_date)).all()
    
    # 转换为图表数据格式
    dates = [item.date for item in daily_news_counts]
    counts = [item.count for item in daily_news_counts]
    
    # 获取热门标签
    popular_tags = db.session.query(
        Tag.name,
        func.count().label('count')
    ).filter(
        Tag.news_id == News.id,
        News.add_date >= start_date
    ).group_by(Tag.name).order_by(func.count().desc()).limit(20).all()
    
    tag_names = [item.name for item in popular_tags]
    tag_counts = [item.count for item in popular_tags]
    
    # 生成热门话题分析
    topics_analysis = generate_topics_analysis(start_date)
    
    return render_template('analytics.html',
                          days=days,
                          dates=dates,
                          counts=counts,
                          tag_names=tag_names,
                          tag_counts=tag_counts,
                          topics_analysis=topics_analysis)

def generate_topics_analysis(start_date):
    """生成热门话题分析"""
    # 获取期间的所有新闻
    recent_news = News.query.filter(News.add_date >= start_date).all()
    
    # 提取所有标题
    titles = [news.title for news in recent_news]
    
    # 使用大模型分析热门话题
    if titles:
        try:
            prompt = f"""
分析以下{len(titles)}条新闻标题,识别出5个主要热门话题,并对每个话题进行简要分析。
返回JSON格式,每个话题包含名称、相关新闻数量和简短描述。

新闻标题:
{json.dumps(titles[:500], ensure_ascii=False, indent=2)}

返回格式示例:
[
  {{"topic": "人工智能", "count": 15, "description": "主要集中在AI在医疗领域的应用,以及GPT-4的发布"}},
  ...
]
"""
            # 调用Ollama API
            response = ollama.chat(model='glm4', messages=[
                {
                    'role': 'user',
                    'content': prompt
                }
            ])
            
            # 提取JSON部分
            content = response['message']['content']
            json_match = re.search(r'\[.*\]', content, re.DOTALL)
            if json_match:
                json_str = json_match.group(0)
                return json.loads(json_str)
        except Exception as e:
            logger.error(f"生成热门话题分析时出错: {e}")
    
    return []

在这里插入图片描述

总结

通过本项目,我们探索了如何将大语言模型与传统Web开发技术结合,打造一个智能化的新闻聚合平台。这种结合不仅增强了用户体验,也展示了AI在实际应用场景中的巨大潜力。

项目实现了以下核心功能:

  1. 基于RSS的多源新闻自动抓取与管理
  2. 使用大模型优化内容提取与标签生成
  3. 语音合成与新闻播报功能
  4. 完善的日志系统与系统监控
  5. 移动设备适配与跨平台兼容

扩展思考中,我们进一步探讨了多模态内容处理、个性化推荐、社交互动、语音助手、多语言支持、智能摘要以及数据分析等更高级功能。这些拓展方向展示了如何将这个基础项目发展成一个更全面、智能的信息服务平台。

从技术角度看,本项目不仅是对Flask、SQLAlchemy等传统Web开发框架的应用,更重要的是展示了如何将新兴的AI技术(如大语言模型)无缝集成到现有系统中,实现传统技术难以达成的智能功能。

对于开发者而言,这个项目提供了一个完整的参考案例,展示了从需求分析、系统设计、功能实现到优化升级的全流程,特别适合那些希望将AI能力融入自己Web应用的开发者学习和借鉴。

人工智能的快速发展正在重塑软件开发的方式和可能性,本项目只是展示了其中的一小部分潜力。未来,随着模型能力的提升和应用场景的拓展,AI驱动的软件将变得更加智能、自然和个性化,为用户创造更大的价值。