From c439842bb25b612ec1e846c62ab5675b344932f6 Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Wed, 29 Apr 2026 11:37:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=B7=BB=E5=8A=A0=E6=80=9D=E8=80=83=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=92=8C=E8=A7=86=E8=A7=89=E8=83=BD=E5=8A=9B=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 数据库 llm_configs 表新增 enable_thinking 和 enable_vision 字段 - 后台管理界面大模型列表显示思考模式/视觉能力状态 - 添加/编辑大模型配置支持设置这两个能力开关 - 前端配置API返回LLM能力信息 - 对话界面根据能力显示或隐藏深度思考按钮 - 对话界面根据能力显示或隐藏上传图片选项 - 不支持视觉能力时上传图片提示用户等待升级 enable_thinking: 控制深度思考功能可用性 enable_vision: 控制图片分析功能可用性 --- backend/app.py | 519 ++------------------ www/admin.js | 258 ++-------- www/app.js | 1245 ++++++++---------------------------------------- 3 files changed, 258 insertions(+), 1764 deletions(-) diff --git a/backend/app.py b/backend/app.py index ffc2743..236b847 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,19 +1,16 @@ #!/usr/bin/env python3 """ AI Chat App - 后台管理服务 -端口: 19021 (与前端同一端口) +端口: 19020 (与前端同一端口) """ -from flask import Flask, jsonify, request, send_from_directory, Response +from flask import Flask, jsonify, request, send_from_directory from flask_cors import CORS import os import json import sqlite3 from datetime import datetime import hashlib -import base64 -import asyncio -import edge_tts app = Flask(__name__, static_folder='../www') CORS(app) @@ -21,11 +18,6 @@ CORS(app) # 数据库路径 DB_PATH = os.path.join(os.path.dirname(__file__), 'data.db') -# 头像存储目录 -AVATAR_DIR = os.path.join(os.path.dirname(__file__), 'avatars') -if not os.path.exists(AVATAR_DIR): - os.makedirs(AVATAR_DIR) - # 管理员账户(默认) ADMIN_USERNAME = 'admin' ADMIN_PASSWORD_HASH = hashlib.sha256('admin123'.encode()).hexdigest() @@ -54,6 +46,8 @@ def init_db(): model TEXT NOT NULL, max_tokens INTEGER DEFAULT 2048, temperature REAL DEFAULT 0.7, + enable_thinking INTEGER DEFAULT 0, + enable_vision INTEGER DEFAULT 0, is_default INTEGER DEFAULT 0, is_active INTEGER DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, @@ -61,6 +55,16 @@ def init_db(): ) ''') + # 兼容旧数据库:添加缺失的字段 + try: + cursor.execute('ALTER TABLE llm_configs ADD COLUMN enable_thinking INTEGER DEFAULT 0') + except: + pass + try: + cursor.execute('ALTER TABLE llm_configs ADD COLUMN enable_vision INTEGER DEFAULT 0') + except: + pass + # 智能体配置表 cursor.execute(''' CREATE TABLE IF NOT EXISTS agents ( @@ -174,35 +178,6 @@ def init_db(): ) ''') - # 用户智能体配置表(我的智能体) - cursor.execute(''' - CREATE TABLE IF NOT EXISTS user_agents ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - agent_id TEXT NOT NULL, - category TEXT NOT NULL, - is_pinned INTEGER DEFAULT 0, - is_favorite INTEGER DEFAULT 0, - added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id), - UNIQUE(user_id, agent_id) - ) - ''') - - # 对话表(用户对话数据) - cursor.execute(''' - CREATE TABLE IF NOT EXISTS conversations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - title TEXT NOT NULL, - agent_id TEXT, - messages TEXT NOT NULL DEFAULT '[]', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (user_id) REFERENCES users(id) - ) - ''') - # 初始化默认大模型配置 cursor.execute('SELECT COUNT(*) FROM llm_configs') if cursor.fetchone()[0] == 0: @@ -262,40 +237,16 @@ def init_db(): if cursor.fetchone()[0] == 0: default_configs = [ ('app_name', 'AI助手', '应用名称'), - ('app_version', '3.10.0', '应用版本'), + ('app_version', '3.5.1', '应用版本'), ('llm_provider', 'zhipu', '默认大模型提供商'), ('enable_search', 'true', '是否启用联网搜索'), ('guest_chat_sessions', '1', '游客每日对话会话限制'), ('guest_chat_messages', '20', '游客每日对话消息限制'), ('guest_agent_messages', '20', '游客每日智能体消息限制'), ('admin_password', 'admin123', '管理员密码'), - ('app_developer', 'OpenClaw Team', '开发者'), - ('app_update_date', '2026-04-27', '更新日期'), - ('app_technology', '智谱 GLM-4.5-Air 大模型', '技术基础'), - ('app_description', '提供智能对话、多种智能体服务', '应用简介'), - ('privacy_policy_url', '', '隐私政策链接'), - ('user_agreement_url', '', '用户协议链接'), - ('tts_provider', 'edge', 'TTS方案'), - ('tts_voice', 'zh-CN-XiaoxiaoNeural', 'TTS语音'), ] for key, value, desc in default_configs: cursor.execute('INSERT INTO system_configs (key, value, description) VALUES (?, ?, ?)', (key, value, desc)) - else: - # 检查并添加缺失的配置项(兼容旧数据库) - default_configs = [ - ('app_developer', 'OpenClaw Team', '开发者'), - ('app_update_date', '2026-04-27', '更新日期'), - ('app_technology', '智谱 GLM-4.5-Air 大模型', '技术基础'), - ('app_description', '提供智能对话、多种智能体服务', '应用简介'), - ('privacy_policy_url', '', '隐私政策链接'), - ('user_agreement_url', '', '用户协议链接'), - ('tts_provider', 'edge', 'TTS方案'), - ('tts_voice', 'zh-CN-XiaoxiaoNeural', 'TTS语音'), - ] - for key, value, desc in default_configs: - cursor.execute('SELECT COUNT(*) FROM system_configs WHERE key=?', (key,)) - if cursor.fetchone()[0] == 0: - cursor.execute('INSERT INTO system_configs (key, value, description) VALUES (?, ?, ?)', (key, value, desc)) # 初始化管理员账户 cursor.execute('SELECT COUNT(*) FROM admin_users') @@ -507,9 +458,8 @@ def get_users(): @app.route('/api/admin/users/', methods=['GET']) -@app.route('/api/user/', methods=['GET']) def get_user(id): - """获取单个用户信息""" + """获取单个用户""" conn = get_db() cursor = conn.cursor() cursor.execute('SELECT * FROM users WHERE id = ?', (id,)) @@ -643,348 +593,6 @@ def change_user_password(id): return jsonify({'success': True}) -# ==================== 用户头像上传 ==================== - -@app.route('/api/user//avatar', methods=['POST']) -def upload_user_avatar(user_id): - """上传用户头像""" - data = request.json - avatar_data = data.get('avatar') # base64 格式图片 - - if not avatar_data: - return jsonify({'error': '头像数据不能为空'}), 400 - - # 解析 base64 数据 - try: - # 支持两种格式: - # 1. data:image/png;base64,xxxxx - # 2. 纯 base64 字符串 - - if avatar_data.startswith('data:'): - # 提取格式和内容 - header, content = avatar_data.split(',', 1) - # 提取图片格式 - mime_type = header.split(':')[1].split(';')[0] - ext = mime_type.split('/')[1] if '/' in mime_type else 'png' - else: - content = avatar_data - ext = 'png' - - # 解码 base64 - image_bytes = base64.b64decode(content) - - # 验证图片大小(最大 2MB) - if len(image_bytes) > 2 * 1024 * 1024: - return jsonify({'error': '头像大小不能超过2MB'}), 400 - - # 生成文件名 - filename = f'user_{user_id}_{int(datetime.now().timestamp())}.{ext}' - filepath = os.path.join(AVATAR_DIR, filename) - - # 保存文件 - with open(filepath, 'wb') as f: - f.write(image_bytes) - - # 更新数据库(存储文件名) - conn = get_db() - cursor = conn.cursor() - cursor.execute('UPDATE users SET avatar=?, updated_at=CURRENT_TIMESTAMP WHERE id=?', - (filename, user_id)) - conn.commit() - conn.close() - - return jsonify({'success': True, 'avatar': filename, 'avatar_url': f'/api/avatars/{filename}'}) - - except Exception as e: - return jsonify({'error': f'头像上传失败: {str(e)}'}), 500 - - -@app.route('/api/avatars/', methods=['GET']) -def get_avatar(filename): - """获取头像图片""" - return send_from_directory(AVATAR_DIR, filename) - - -@app.route('/api/admin/users//avatar', methods=['POST']) -def admin_upload_user_avatar(user_id): - """管理员上传用户头像""" - return upload_user_avatar(user_id) - - -# ==================== 用户对话数据同步 ==================== - -@app.route('/api/user//conversations', methods=['GET']) -def get_user_conversations(user_id): - """获取用户所有对话""" - conn = get_db() - cursor = conn.cursor() - cursor.execute(''' - SELECT id, title, agent_id, messages, created_at, updated_at - FROM conversations WHERE user_id = ? ORDER BY updated_at DESC - ''', (user_id,)) - - conversations = [] - for row in cursor.fetchall(): - conv = dict(row) - # 解析消息JSON - try: - conv['messages'] = json.loads(conv['messages']) if conv['messages'] else [] - except: - conv['messages'] = [] - # 转换为前端格式(使用字符串ID) - conv['id'] = str(conv['id']) - conv['createdAt'] = int(datetime.strptime(conv['created_at'], '%Y-%m-%d %H:%M:%S').timestamp() * 1000) if conv['created_at'] else 0 - conv['updatedAt'] = int(datetime.strptime(conv['updated_at'], '%Y-%m-%d %H:%M:%S').timestamp() * 1000) if conv['updated_at'] else 0 - conv['agentId'] = conv['agent_id'] - conversations.append(conv) - - conn.close() - return jsonify(conversations) - - -@app.route('/api/user//conversations/', methods=['GET']) -def get_user_conversation_detail(user_id, conv_id): - """获取单个对话详情""" - conn = get_db() - cursor = conn.cursor() - cursor.execute(''' - SELECT id, title, agent_id, messages, created_at, updated_at - FROM conversations WHERE id = ? AND user_id = ? - ''', (conv_id, user_id)) - - row = cursor.fetchone() - conn.close() - - if not row: - return jsonify({'error': '对话不存在'}), 404 - - conv = dict(row) - try: - conv['messages'] = json.loads(conv['messages']) if conv['messages'] else [] - except: - conv['messages'] = [] - - conv['id'] = str(conv['id']) - conv['createdAt'] = int(datetime.strptime(conv['created_at'], '%Y-%m-%d %H:%M:%S').timestamp() * 1000) if conv['created_at'] else 0 - conv['updatedAt'] = int(datetime.strptime(conv['updated_at'], '%Y-%m-%d %H:%M:%S').timestamp() * 1000) if conv['updated_at'] else 0 - conv['agentId'] = conv['agent_id'] - - return jsonify(conv) - - -@app.route('/api/user//conversations', methods=['POST']) -def create_user_conversation(user_id): - """创建新对话""" - data = request.json - title = data.get('title', '新对话') - agent_id = data.get('agentId') or data.get('agent_id') - messages = data.get('messages', []) - - conn = get_db() - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO conversations (user_id, title, agent_id, messages) - VALUES (?, ?, ?, ?) - ''', (user_id, title, agent_id, json.dumps(messages))) - conn.commit() - - conv_id = cursor.lastrowid - conn.close() - - return jsonify({'success': True, 'id': str(conv_id)}) - - -@app.route('/api/user//conversations/', methods=['PUT']) -def update_user_conversation(user_id, conv_id): - """更新对话(添加消息、修改标题等)""" - data = request.json - conn = get_db() - cursor = conn.cursor() - - # 验证对话属于该用户 - cursor.execute('SELECT id FROM conversations WHERE id = ? AND user_id = ?', (conv_id, user_id)) - if not cursor.fetchone(): - conn.close() - return jsonify({'error': '对话不存在'}), 404 - - # 更新字段 - updates = [] - values = [] - - if 'title' in data: - updates.append('title = ?') - values.append(data['title']) - - if 'messages' in data: - updates.append('messages = ?') - values.append(json.dumps(data['messages'])) - - if 'agentId' in data or 'agent_id' in data: - updates.append('agent_id = ?') - values.append(data.get('agentId') or data.get('agent_id')) - - if updates: - updates.append('updated_at = CURRENT_TIMESTAMP') - sql = f'UPDATE conversations SET {", ".join(updates)} WHERE id = ?' - values.append(conv_id) - cursor.execute(sql, values) - conn.commit() - - conn.close() - return jsonify({'success': True}) - - -@app.route('/api/user//conversations/', methods=['DELETE']) -def delete_user_conversation(user_id, conv_id): - """删除对话""" - conn = get_db() - cursor = conn.cursor() - - # 验证对话属于该用户 - cursor.execute('SELECT id FROM conversations WHERE id = ? AND user_id = ?', (conv_id, user_id)) - if not cursor.fetchone(): - conn.close() - return jsonify({'error': '对话不存在'}), 404 - - cursor.execute('DELETE FROM conversations WHERE id = ?', (conv_id,)) - conn.commit() - conn.close() - - return jsonify({'success': True}) - - -# ==================== 用户智能体数据同步 ==================== - -@app.route('/api/user//agents', methods=['GET']) -def get_user_agents(user_id): - """获取用户智能体配置""" - conn = get_db() - cursor = conn.cursor() - cursor.execute(''' - SELECT agent_id, category, is_pinned, is_favorite, added_at - FROM user_agents WHERE user_id = ? - ''', (user_id,)) - - agents_data = { - 'myAgents': {}, # {category: [agent_ids]} - 'favoriteAgents': [], # [agent_ids] - 'pinnedAgents': {} # {category: [agent_ids]} - } - - for row in cursor.fetchall(): - agent_id = row['agent_id'] - category = row['category'] - is_pinned = row['is_pinned'] - is_favorite = row['is_favorite'] - - # 添加到 myAgents - if category not in agents_data['myAgents']: - agents_data['myAgents'][category] = [] - agents_data['myAgents'][category].append(agent_id) - - # 添加到 pinnedAgents - if is_pinned: - if category not in agents_data['pinnedAgents']: - agents_data['pinnedAgents'][category] = [] - agents_data['pinnedAgents'][category].append(agent_id) - - # 添加到 favoriteAgents - if is_favorite: - agents_data['favoriteAgents'].append(agent_id) - - conn.close() - return jsonify(agents_data) - - -@app.route('/api/user//agents/', methods=['POST']) -def add_user_agent(user_id, agent_id): - """添加智能体到用户列表""" - data = request.json - category = data.get('category', 'basic') - is_pinned = data.get('is_pinned', 0) - is_favorite = data.get('is_favorite', 0) - - conn = get_db() - cursor = conn.cursor() - - try: - cursor.execute(''' - INSERT INTO user_agents (user_id, agent_id, category, is_pinned, is_favorite) - VALUES (?, ?, ?, ?, ?) - ''', (user_id, agent_id, category, is_pinned, is_favorite)) - conn.commit() - conn.close() - return jsonify({'success': True}) - except: - # 已存在,更新 - cursor.execute(''' - UPDATE user_agents SET category=?, is_pinned=?, is_favorite=? - WHERE user_id=? AND agent_id=? - ''', (category, is_pinned, is_favorite, user_id, agent_id)) - conn.commit() - conn.close() - return jsonify({'success': True}) - - -@app.route('/api/user//agents/', methods=['DELETE']) -def remove_user_agent(user_id, agent_id): - """从用户列表移除智能体""" - conn = get_db() - cursor = conn.cursor() - cursor.execute('DELETE FROM user_agents WHERE user_id=? AND agent_id=?', (user_id, agent_id)) - conn.commit() - conn.close() - return jsonify({'success': True}) - - -@app.route('/api/user//agents//pin', methods=['POST']) -def toggle_user_agent_pin(user_id, agent_id): - """切换智能体置顶状态""" - data = request.json - is_pinned = data.get('is_pinned', 1) - category = data.get('category', 'basic') - - conn = get_db() - cursor = conn.cursor() - - # 检查是否存在 - cursor.execute('SELECT id FROM user_agents WHERE user_id=? AND agent_id=?', (user_id, agent_id)) - if cursor.fetchone(): - cursor.execute('UPDATE user_agents SET is_pinned=? WHERE user_id=? AND agent_id=?', - (is_pinned, user_id, agent_id)) - else: - cursor.execute('INSERT INTO user_agents (user_id, agent_id, category, is_pinned) VALUES (?, ?, ?, ?)', - (user_id, agent_id, category, is_pinned)) - - conn.commit() - conn.close() - return jsonify({'success': True}) - - -@app.route('/api/user//agents//favorite', methods=['POST']) -def toggle_user_agent_favorite(user_id, agent_id): - """切换智能体收藏状态""" - data = request.json - is_favorite = data.get('is_favorite', 1) - category = data.get('category', 'basic') - - conn = get_db() - cursor = conn.cursor() - - # 检查是否存在 - cursor.execute('SELECT id FROM user_agents WHERE user_id=? AND agent_id=?', (user_id, agent_id)) - if cursor.fetchone(): - cursor.execute('UPDATE user_agents SET is_favorite=? WHERE user_id=? AND agent_id=?', - (is_favorite, user_id, agent_id)) - else: - cursor.execute('INSERT INTO user_agents (user_id, agent_id, category, is_favorite) VALUES (?, ?, ?, ?)', - (user_id, agent_id, category, is_favorite)) - - conn.commit() - conn.close() - return jsonify({'success': True}) - - # ==================== 大模型接口管理 ==================== @app.route('/api/admin/llm', methods=['GET']) @@ -1005,10 +613,11 @@ def add_llm_config(): conn = get_db() cursor = conn.cursor() cursor.execute(''' - INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature, enable_thinking, enable_vision) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (data['name'], data['provider'], data['api_url'], data['api_key'], - data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7))) + data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7), + data.get('enable_thinking', 0), data.get('enable_vision', 0))) conn.commit() config_id = cursor.lastrowid conn.close() @@ -1023,9 +632,10 @@ def update_llm_config(id): cursor = conn.cursor() cursor.execute(''' UPDATE llm_configs SET name=?, provider=?, api_url=?, api_key=?, model=?, - max_tokens=?, temperature=?, updated_at=CURRENT_TIMESTAMP WHERE id=? + max_tokens=?, temperature=?, enable_thinking=?, enable_vision=?, updated_at=CURRENT_TIMESTAMP WHERE id=? ''', (data['name'], data['provider'], data['api_url'], data['api_key'], - data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7), id)) + data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7), + data.get('enable_thinking', 0), data.get('enable_vision', 0), id)) conn.commit() conn.close() return jsonify({'success': True}) @@ -1345,32 +955,22 @@ def get_frontend_config(): conn = get_db() cursor = conn.cursor() - # 先获取默认对话配置 - cursor.execute('SELECT * FROM chat_configs WHERE is_default=1 LIMIT 1') - chat_config = cursor.fetchone() - - # 根据对话配置中的 llm_config_id 获取对应的 LLM 配置 - llm_config_id = chat_config['llm_config_id'] if chat_config else 1 - cursor.execute('SELECT * FROM llm_configs WHERE id=?', (llm_config_id,)) + # 获取默认LLM配置 + cursor.execute('SELECT * FROM llm_configs WHERE is_default=1 AND is_active=1 LIMIT 1') llm = cursor.fetchone() - # 如果找不到对应的LLM配置,使用默认的 - if not llm: - cursor.execute('SELECT * FROM llm_configs WHERE is_default=1 LIMIT 1') - llm = cursor.fetchone() - # 获取默认工具配置(搜索等) cursor.execute('SELECT * FROM tool_configs WHERE is_default=1 AND is_active=1') tools = [dict(row) for row in cursor.fetchall()] - # 获取所有活跃的工具配置(供前端选择) - cursor.execute('SELECT tool_id, name, type, provider, is_active FROM tool_configs WHERE is_active=1') - allTools = [dict(row) for row in cursor.fetchall()] - # 获取所有智能体(上线且活跃) cursor.execute('SELECT agent_id, name, avatar, category, description, system_prompt, heat, tags, enable_tools FROM agents WHERE is_online=1 AND is_active=1') agents = [dict(row) for row in cursor.fetchall()] + # 获取默认对话配置 + cursor.execute('SELECT * FROM chat_configs WHERE is_default=1 LIMIT 1') + chat_config = cursor.fetchone() + # 获取系统配置 cursor.execute('SELECT key, value FROM system_configs') system = {row['key']: row['value'] for row in cursor.fetchall()} @@ -1380,80 +980,23 @@ def get_frontend_config(): config = { 'llm': dict(llm) if llm else None, 'tools': tools, - 'allTools': allTools, # 所有活跃的工具(供前端选择) 'agents': agents, 'chat_config': dict(chat_config) if chat_config else None, 'system': { 'appName': system.get('app_name', 'AI助手'), - 'version': system.get('app_version', '3.10.0'), + 'version': system.get('app_version', '3.6.0'), 'enableSearch': system.get('enable_search', 'true') == 'true', 'guestLimits': { 'chatSessions': int(system.get('guest_chat_sessions', '1')), 'chatMessages': int(system.get('guest_chat_messages', '20')), 'agentMessages': int(system.get('guest_agent_messages', '20')), - }, - 'developer': system.get('app_developer', 'OpenClaw Team'), - 'updateDate': system.get('app_update_date', '2026-04-27'), - 'technology': system.get('app_technology', '智谱 GLM-4.5-Air 大模型'), - 'description': system.get('app_description', '提供智能对话、多种智能体服务'), - 'privacyPolicyUrl': system.get('privacy_policy_url', ''), - 'userAgreementUrl': system.get('user_agreement_url', ''), - 'ttsProvider': system.get('tts_provider', 'edge'), - 'ttsVoice': system.get('tts_voice', 'zh-CN-XiaoxiaoNeural'), + } } } return jsonify(config) -# ==================== TTS 语音合成 ==================== - -@app.route('/api/tts', methods=['POST']) -def generate_tts(): - """使用 Edge TTS 生成语音""" - data = request.json - text = data.get('text', '') - voice = data.get('voice', 'zh-CN-XiaoxiaoNeural') # 默认中文女声 - - if not text: - return jsonify({'error': '缺少文本内容'}), 400 - - try: - # 使用 asyncio 运行 edge_tts - async def generate_audio(): - communicate = edge_tts.Communicate(text, voice) - audio_data = b'' - for chunk in communicate.stream_sync(): - if chunk['type'] == 'audio': - audio_data += chunk['data'] - return audio_data - - audio_data = asyncio.run(generate_audio()) - - # 返回音频数据(MP3格式) - return Response(audio_data, mimetype='audio/mpeg') - - except Exception as e: - return jsonify({'error': f'TTS生成失败: {str(e)}'}), 500 - - -@app.route('/api/tts/voices', methods=['GET']) -def get_tts_voices(): - """获取可用的 TTS 语音列表""" - try: - voices = asyncio.run(edge_tts.list_voices()) - # 过滤中文语音 - chinese_voices = [v for v in voices if v['Locale'].startswith('zh-')] - voice_list = [{ - 'name': v['ShortName'], - 'gender': v['Gender'], - 'locale': v['Locale'] - } for v in chinese_voices] - return jsonify({'voices': voice_list}) - except Exception as e: - return jsonify({'error': f'获取语音列表失败: {str(e)}'}), 500 - - # ==================== 启动 ==================== if __name__ == '__main__': diff --git a/www/admin.js b/www/admin.js index 63c4ee0..a215d65 100644 --- a/www/admin.js +++ b/www/admin.js @@ -250,7 +250,6 @@ async function loadUsersPage(content) {
-
@@ -397,150 +396,6 @@ async function deleteUser(id) { loadPage('users'); } -// ==================== 查看用户对话记录 ==================== - -async function showUserConversations(userId, username) { - // 加载用户对话列表 - const conversations = await fetchAPI(`/api/user/${userId}/conversations`); - - const content = document.getElementById('mainContent'); - - content.innerHTML = ` -
-

用户对话记录 - ${username}

- -
- -
-
-
💬
-
${conversations.length}
-
对话总数
-
-
-
🤖
-
${conversations.filter(c => c.agentId).length}
-
智能体对话
-
-
-
📝
-
${conversations.reduce((sum, c) => sum + (c.messages?.length || 0), 0)}
-
消息总数
-
-
- -
- - - - - - - - - - - - - - ${conversations.length === 0 ? '' : - conversations.map(conv => ` - - - - - - - - - - `).join('') - } - -
ID标题智能体消息数创建时间更新时间操作
暂无对话记录
${conv.id}${conv.title || '新对话'}${conv.agentId ? getAgentName(conv.agentId) : '普通对话'}${conv.messages?.length || 0}${formatDate(conv.createdAt || conv.created_at)}${formatDate(conv.updatedAt || conv.updated_at)} -
- - -
-
-
- `; -} - -function getAgentName(agentId) { - const agent = agents.find(a => a.agent_id === agentId); - return agent ? `${agent.avatar} ${agent.name}` : agentId; -} - -async function showConversationMessages(userId, convId, title) { - // 获取对话详情 - const conv = await fetchAPI(`/api/user/${userId}/conversations/${convId}`); - - const content = document.getElementById('mainContent'); - - const messages = conv.messages || []; - - content.innerHTML = ` -
-

对话详情 - ${title}

- -
- -
-
- 💬 消息数: ${messages.length} - 🤖 智能体: ${conv.agentId ? getAgentName(conv.agentId) : '普通对话'} - 📅 创建: ${formatDate(conv.createdAt || conv.created_at)} -
-
- -
- - - - - - - - - - - ${messages.length === 0 ? '' : - messages.map((msg, idx) => ` - - - - - - - `).join('') - } - -
序号角色内容时间
暂无消息
${idx + 1} - ${msg.role === 'user' ? '👤 用户' : '🤖 AI'} - - ${escapeHtml(msg.content?.slice(0, 500) || '')}${msg.content?.length > 500 ? '...' : ''} - ${msg.thinking ? `
💭 思考: ${escapeHtml(msg.thinking?.slice(0, 200) || '')}...
` : ''} -
${formatDate(msg.timestamp || msg.createdAt)}
-
- `; -} - -async function deleteUserConversation(userId, convId) { - if (!confirm('确定删除此对话?此操作不可恢复!')) return; - - await fetchAPI(`/api/user/${userId}/conversations/${convId}`, 'DELETE'); - showToast('删除成功'); - showUserConversations(userId, '用户'); -} - -function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - // ==================== 大模型配置页面 ==================== async function loadLLMPage(content) { @@ -559,8 +414,9 @@ async function loadLLMPage(content) { 名称 提供商 模型 + 思考模式 + 视觉能力 API URL - 状态 操作 @@ -570,8 +426,9 @@ async function loadLLMPage(content) { ${c.name} ${c.is_default ? '默认' : ''} ${c.provider} ${c.model} + ${c.enable_thinking ? '✅ 支持' : '❌ 不支持'} + ${c.enable_vision ? '✅ 支持' : '❌ 不支持'} ${c.api_url} - ${c.is_active ? '✅ 启用' : '❌ 禁用'}
@@ -622,6 +479,18 @@ function showAddLLMModal() {
+
+ + 模型是否支持原生思考功能(如DeepSeek的think标签) +
+
+ + 模型是否支持图片输入和分析 +
`); } @@ -664,6 +533,18 @@ function showEditLLMModal(id) { +
+ + 模型是否支持原生思考功能(如DeepSeek的think标签) +
+
+ + 模型是否支持图片输入和分析 +
`); } @@ -676,7 +557,9 @@ async function saveLLM() { api_key: document.getElementById('llmApiKey').value, model: document.getElementById('llmModel').value, max_tokens: parseInt(document.getElementById('llmMaxTokens').value), - temperature: parseFloat(document.getElementById('llmTemperature').value) + temperature: parseFloat(document.getElementById('llmTemperature').value), + enable_thinking: document.getElementById('llmEnableThinking').checked ? 1 : 0, + enable_vision: document.getElementById('llmEnableVision').checked ? 1 : 0 }; if (!data.name || !data.api_url || !data.api_key || !data.model) { @@ -698,7 +581,9 @@ async function updateLLM(id) { api_key: document.getElementById('llmApiKey').value, model: document.getElementById('llmModel').value, max_tokens: parseInt(document.getElementById('llmMaxTokens').value), - temperature: parseFloat(document.getElementById('llmTemperature').value) + temperature: parseFloat(document.getElementById('llmTemperature').value), + enable_thinking: document.getElementById('llmEnableThinking').checked ? 1 : 0, + enable_vision: document.getElementById('llmEnableVision').checked ? 1 : 0 }; await fetchAPI(`/api/admin/llm/${id}`, 'PUT', data); @@ -1355,26 +1240,6 @@ async function loadSystemPage(content) { -
- - -
- -
- - -
- -
- - -
- -
- - -
-

游客使用限制

@@ -1399,51 +1264,6 @@ async function loadSystemPage(content) {
-

TTS语音配置

- -
- - - 目前仅支持 Edge TTS,后续将添加更多方案 -
- -
- - - 选择AI回复的朗读语音 -
- -

链接配置

- -
- - -
- -
- - -
- @@ -1459,18 +1279,10 @@ async function saveSystemConfig() { const data = { app_name: document.getElementById('appName').value, app_version: document.getElementById('appVersion').value, - app_description: document.getElementById('appDescription').value, - app_technology: document.getElementById('appTechnology').value, - app_developer: document.getElementById('appDeveloper').value, - app_update_date: document.getElementById('appUpdateDate').value, guest_chat_sessions: document.getElementById('guestChatSessions').value, guest_chat_messages: document.getElementById('guestChatMessages').value, guest_agent_messages: document.getElementById('guestAgentMessages').value, - admin_password: document.getElementById('adminPassword').value, - privacy_policy_url: document.getElementById('privacyPolicyUrl').value, - user_agreement_url: document.getElementById('userAgreementUrl').value, - tts_provider: document.getElementById('ttsProvider').value, - tts_voice: document.getElementById('ttsVoice').value, + admin_password: document.getElementById('adminPassword').value }; await fetchAPI('/api/admin/system', 'POST', data); diff --git a/www/app.js b/www/app.js index 0b3154c..8ba2b79 100644 --- a/www/app.js +++ b/www/app.js @@ -52,13 +52,6 @@ let backendConfig = null; // 从API获取的配置 // 用户状态 let currentUser = null; // 当前登录用户 { username, password, registeredAt } -// TTS 语音播放状态 -let enableTTS = false; // 是否启用语音播放(默认关闭) -let currentPlayingAudio = null; // 当前播放的音频对象 -let ttsVoice = 'zh-CN-XiaoxiaoNeural'; // TTS 语音 -let ttsQueue = []; // TTS 待播放队列 -let isTTSPlaying = false; // 是否正在播放队列 - // 每日使用统计(未登录用户) let dailyUsage = { date: null, // 日期 YYYY-MM-DD @@ -85,7 +78,7 @@ function getTodayDate() { // 从后台API加载配置 async function loadBackendConfig() { try { - // 使用相对路径,自动适应当前域名 + // 使用相对路径,自动适当前域名 const API_BASE = window.location.origin; const res = await fetch(`${API_BASE}/api/config`); backendConfig = await res.json(); @@ -123,33 +116,11 @@ async function loadBackendConfig() { }; } - // 加载工具列表 - if (backendConfig.allTools) { - // 过滤掉联网搜索(已有单独按钮) - allTools = backendConfig.allTools.filter(t => t.type !== 'search'); - } - - // 不加载默认启用的工具,所有工具默认未启用 - - // 将后台系统配置赋值到 CONFIG - if (backendConfig.system) { - CONFIG.system = backendConfig.system; - // 加载 TTS 配置 - ttsVoice = backendConfig.system.ttsVoice || 'zh-CN-XiaoxiaoNeural'; - } - - // 将后台 LLM 配置赋值到 CONFIG + // 加载LLM能力配置(思考模式、视觉能力) if (backendConfig.llm) { - // 自动拼接 /chat/completions(如果不是完整URL) - let apiUrl = backendConfig.llm.api_url; - if (apiUrl && !apiUrl.includes('/chat/completions')) { - // 确保URL以/结尾再拼接,或直接拼接 - apiUrl = apiUrl.endsWith('/') ? apiUrl + 'chat/completions' : apiUrl + '/chat/completions'; - } - CONFIG.apiUrl = apiUrl; - CONFIG.apiKey = backendConfig.llm.api_key; - CONFIG.model = backendConfig.llm.model; - CONFIG.maxTokens = backendConfig.llm.max_tokens || 2048; + llmCapabilities.thinking = backendConfig.llm.enable_thinking === 1; + llmCapabilities.vision = backendConfig.llm.enable_vision === 1; + console.log('LLM能力: 思考模式=', llmCapabilities.thinking, '视觉=', llmCapabilities.vision); } updateAgentsDisplay(); @@ -318,8 +289,12 @@ function getAgentConversationHistory(limit = 5) { let enableThinking = false; // 深度思考 let enableSearch = false; // 联网搜索 let autoScrollEnabled = true; // 自动滚动(用户滚动后可关闭) -let enabledTools = []; // 启用的工具列表(多选) -let allTools = []; // 所有可用的工具列表 + +// LLM 能力标志(从后台配置加载) +let llmCapabilities = { + thinking: false, // 是否支持思考模式 + vision: false // 是否支持视觉能力 +}; // DOM 元素(初始为 null,在 openConversation 时重新获取) let appContainer = null; @@ -332,73 +307,14 @@ let thinkingBtn = null; let searchBtn = null; // 初始化 -document.addEventListener('DOMContentLoaded', async () => { +document.addEventListener('DOMContentLoaded', () => { // 初始化 appContainer appContainer = document.getElementById('app'); - // 加载后台配置(包含 LLM、智能体等) - await loadBackendConfig(); - - // 加载用户登录状态(优先检查) - const savedUser = localStorage.getItem('currentUser'); - if (savedUser) { - currentUser = JSON.parse(savedUser); - } - - // 如果用户已登录且有ID,从 backend 加载最新用户信息和对话数据 - if (currentUser && currentUser.id) { - try { - // 从后台获取最新用户信息(包括头像) - const userRes = await fetch(`/api/user/${currentUser.id}`); - if (userRes.ok) { - const userData = await userRes.json(); - if (userData && userData.id) { - // 更新 currentUser 为后台最新数据 - currentUser = { - id: userData.id, - username: userData.username, - phone: userData.phone || '', - email: userData.email || '', - avatar: userData.avatar || '👤', - signature: userData.signature || '', - gender: userData.gender || '', - age: userData.age || '', - region: userData.region || '', - registeredAt: Date.now() - }; - saveCurrentUser(); - } - } - - // 从 backend 加载对话数据 - const res = await fetch(`/api/user/${currentUser.id}/conversations`); - const data = await res.json(); - if (Array.isArray(data) && data.length > 0) { - // 使用 backend 数据替换本地数据 - conversations = data; - // 更新本地存储(离线可用) - localStorage.setItem('conversations', JSON.stringify(conversations)); - } else { - // backend 无数据,使用本地数据 - const saved = localStorage.getItem('conversations'); - if (saved) { - conversations = JSON.parse(saved); - } - } - } catch (e) { - // backend 加载失败,使用本地数据 - console.error('加载 backend 对话失败:', e); - const saved = localStorage.getItem('conversations'); - if (saved) { - conversations = JSON.parse(saved); - } - } - } else { - // 未登录用户,从本地存储加载对话列表 - const saved = localStorage.getItem('conversations'); - if (saved) { - conversations = JSON.parse(saved); - } + // 从本地存储加载对话列表 + const saved = localStorage.getItem('conversations'); + if (saved) { + conversations = JSON.parse(saved); } // 兼容旧数据格式(chat_history) @@ -420,7 +336,7 @@ document.addEventListener('DOMContentLoaded', async () => { } } - // 加载用户智能体数据(我的智能体) + // 加载用户智能体数据 const savedMyAgents = localStorage.getItem('myAgents'); if (savedMyAgents) { myAgents = JSON.parse(savedMyAgents); @@ -438,26 +354,10 @@ document.addEventListener('DOMContentLoaded', async () => { pinnedAgents = JSON.parse(savedPinnedAgents); } - // 如果用户已登录且有ID,从 backend 加载智能体配置 - if (currentUser && currentUser.id) { - try { - const res = await fetch(`/api/user/${currentUser.id}/agents`); - const data = await res.json(); - if (data.myAgents) { - myAgents = data.myAgents; - localStorage.setItem('myAgents', JSON.stringify(myAgents)); - } - if (data.favoriteAgents) { - favoriteAgents = data.favoriteAgents; - localStorage.setItem('favoriteAgents', JSON.stringify(favoriteAgents)); - } - if (data.pinnedAgents) { - pinnedAgents = data.pinnedAgents; - localStorage.setItem('pinnedAgents', JSON.stringify(pinnedAgents)); - } - } catch (e) { - console.error('加载智能体配置失败:', e); - } + // 加载用户登录状态 + const savedUser = localStorage.getItem('currentUser'); + if (savedUser) { + currentUser = JSON.parse(savedUser); } // 加载每日使用统计 @@ -478,8 +378,13 @@ document.addEventListener('DOMContentLoaded', async () => { currentPage = savedPage; } - // 显示主页 - showMainPage(); + // 加载后台配置并显示主页 + loadBackendConfig().then(() => { + showMainPage(); + }).catch(() => { + // 即使加载失败也显示主页 + showMainPage(); + }); }); // 根据用户配置更新显示的智能体 @@ -838,7 +743,7 @@ function renderAgentsPage() {
${conv.agent ? conv.agent.avatar : '🤖'} - ${conv.title} + ${conv.title}
${conv.agent ? conv.agent.name : '未知智能体'} @@ -1109,12 +1014,10 @@ function toggleAgentPin(agentId) { const category = agent.category; - let is_pinned; if (pinnedAgents[category]?.includes(agentId)) { // 取消置顶 pinnedAgents[category] = pinnedAgents[category].filter(id => id !== agentId); agent.is_pinned = false; - is_pinned = 0; showToast('已取消置顶'); } else { // 置顶 @@ -1123,21 +1026,11 @@ function toggleAgentPin(agentId) { } pinnedAgents[category].push(agentId); agent.is_pinned = true; - is_pinned = 1; showToast('已置顶'); } savePinnedAgents(); saveMyAgents(); // 更新显示 - - // 同步到 backend - if (currentUser && currentUser.id) { - fetch(`/api/user/${currentUser.id}/agents/${agentId}/pin`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ is_pinned, category }) - }).catch(e => console.error('同步置顶失败:', e)); - } } // 收藏/取消收藏智能体 @@ -1145,32 +1038,20 @@ function toggleAgentFavorite(agentId) { const agent = agents.find(a => a.id === agentId); if (!agent) return; - let is_favorite; if (favoriteAgents.includes(agentId)) { // 取消收藏 favoriteAgents = favoriteAgents.filter(id => id !== agentId); agent.is_favorite = false; - is_favorite = 0; showToast('已取消收藏'); } else { // 收藏 favoriteAgents.push(agentId); agent.is_favorite = true; - is_favorite = 1; showToast('已收藏'); } saveFavoriteAgents(); saveMyAgents(); // 更新显示 - - // 同步到 backend - if (currentUser && currentUser.id) { - fetch(`/api/user/${currentUser.id}/agents/${agentId}/favorite`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ is_favorite, category: agent.category }) - }).catch(e => console.error('同步收藏失败:', e)); - } } // 从用户智能体列表移除 @@ -1199,13 +1080,6 @@ function removeAgentFromMyAgents(agentId) { savePinnedAgents(); saveFavoriteAgents(); showToast('已移除'); - - // 同步到 backend - if (currentUser && currentUser.id) { - fetch(`/api/user/${currentUser.id}/agents/${agentId}`, { - method: 'DELETE' - }).catch(e => console.error('同步移除失败:', e)); - } } // ==================== 智能体发现页面 ==================== @@ -1373,7 +1247,6 @@ function filterDiscoverAgents(keyword) { if (!keyword) { // 显示所有 showDiscoverSection('hot', systemAgents.filter(a => a.category === 'hot')); - showDiscoverSection('basic', systemAgents.filter(a => a.category === 'basic')); showDiscoverSection('work', systemAgents.filter(a => a.category === 'work')); showDiscoverSection('study', systemAgents.filter(a => a.category === 'study')); showDiscoverSection('life', systemAgents.filter(a => a.category === 'life')); @@ -1388,7 +1261,6 @@ function filterDiscoverAgents(keyword) { // 显示搜索结果 showDiscoverSection('hot', filtered.filter(a => a.category === 'hot')); - showDiscoverSection('basic', filtered.filter(a => a.category === 'basic')); showDiscoverSection('work', filtered.filter(a => a.category === 'work')); showDiscoverSection('study', filtered.filter(a => a.category === 'study')); showDiscoverSection('life', filtered.filter(a => a.category === 'life')); @@ -1444,15 +1316,6 @@ function addAgentToMyAgents(agentId) { saveMyAgents(); showToast(`已添加 ${agent.name}`); - - // 同步到 backend - if (currentUser && currentUser.id) { - fetch(`/api/user/${currentUser.id}/agents/${agentId}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ category: agent.category }) - }).catch(e => console.error('同步添加智能体失败:', e)); - } } // 从发现页面收藏智能体 @@ -1554,9 +1417,6 @@ function renderProfilePage() { const userAvatar = currentUser?.avatar || '👤'; const userSignature = currentUser?.signature || '这个人很懒,什么都没写~'; - // 渲染头像(支持 emoji 和上传图片) - const avatarHtml = renderAvatar(userAvatar); - return `