feat: 大模型配置添加思考模式和视觉能力属性

- 数据库 llm_configs 表新增 enable_thinking 和 enable_vision 字段
- 后台管理界面大模型列表显示思考模式/视觉能力状态
- 添加/编辑大模型配置支持设置这两个能力开关
- 前端配置API返回LLM能力信息
- 对话界面根据能力显示或隐藏深度思考按钮
- 对话界面根据能力显示或隐藏上传图片选项
- 不支持视觉能力时上传图片提示用户等待升级

enable_thinking: 控制深度思考功能可用性
enable_vision: 控制图片分析功能可用性
This commit is contained in:
2026-04-29 11:37:57 +08:00
parent d4b1f3dfff
commit c439842bb2
3 changed files with 258 additions and 1764 deletions

View File

@@ -1,19 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
AI Chat App - 后台管理服务 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 from flask_cors import CORS
import os import os
import json import json
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
import hashlib import hashlib
import base64
import asyncio
import edge_tts
app = Flask(__name__, static_folder='../www') app = Flask(__name__, static_folder='../www')
CORS(app) CORS(app)
@@ -21,11 +18,6 @@ CORS(app)
# 数据库路径 # 数据库路径
DB_PATH = os.path.join(os.path.dirname(__file__), 'data.db') 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_USERNAME = 'admin'
ADMIN_PASSWORD_HASH = hashlib.sha256('admin123'.encode()).hexdigest() ADMIN_PASSWORD_HASH = hashlib.sha256('admin123'.encode()).hexdigest()
@@ -54,6 +46,8 @@ def init_db():
model TEXT NOT NULL, model TEXT NOT NULL,
max_tokens INTEGER DEFAULT 2048, max_tokens INTEGER DEFAULT 2048,
temperature REAL DEFAULT 0.7, temperature REAL DEFAULT 0.7,
enable_thinking INTEGER DEFAULT 0,
enable_vision INTEGER DEFAULT 0,
is_default INTEGER DEFAULT 0, is_default INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1, is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 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(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS agents ( 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') cursor.execute('SELECT COUNT(*) FROM llm_configs')
if cursor.fetchone()[0] == 0: if cursor.fetchone()[0] == 0:
@@ -262,40 +237,16 @@ def init_db():
if cursor.fetchone()[0] == 0: if cursor.fetchone()[0] == 0:
default_configs = [ default_configs = [
('app_name', 'AI助手', '应用名称'), ('app_name', 'AI助手', '应用名称'),
('app_version', '3.10.0', '应用版本'), ('app_version', '3.5.1', '应用版本'),
('llm_provider', 'zhipu', '默认大模型提供商'), ('llm_provider', 'zhipu', '默认大模型提供商'),
('enable_search', 'true', '是否启用联网搜索'), ('enable_search', 'true', '是否启用联网搜索'),
('guest_chat_sessions', '1', '游客每日对话会话限制'), ('guest_chat_sessions', '1', '游客每日对话会话限制'),
('guest_chat_messages', '20', '游客每日对话消息限制'), ('guest_chat_messages', '20', '游客每日对话消息限制'),
('guest_agent_messages', '20', '游客每日智能体消息限制'), ('guest_agent_messages', '20', '游客每日智能体消息限制'),
('admin_password', 'admin123', '管理员密码'), ('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: for key, value, desc in default_configs:
cursor.execute('INSERT INTO system_configs (key, value, description) VALUES (?, ?, ?)', (key, value, desc)) 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') cursor.execute('SELECT COUNT(*) FROM admin_users')
@@ -507,9 +458,8 @@ def get_users():
@app.route('/api/admin/users/<int:id>', methods=['GET']) @app.route('/api/admin/users/<int:id>', methods=['GET'])
@app.route('/api/user/<int:id>', methods=['GET'])
def get_user(id): def get_user(id):
"""获取单个用户信息""" """获取单个用户"""
conn = get_db() conn = get_db()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE id = ?', (id,)) cursor.execute('SELECT * FROM users WHERE id = ?', (id,))
@@ -643,348 +593,6 @@ def change_user_password(id):
return jsonify({'success': True}) return jsonify({'success': True})
# ==================== 用户头像上传 ====================
@app.route('/api/user/<int:user_id>/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/<filename>', methods=['GET'])
def get_avatar(filename):
"""获取头像图片"""
return send_from_directory(AVATAR_DIR, filename)
@app.route('/api/admin/users/<int:user_id>/avatar', methods=['POST'])
def admin_upload_user_avatar(user_id):
"""管理员上传用户头像"""
return upload_user_avatar(user_id)
# ==================== 用户对话数据同步 ====================
@app.route('/api/user/<int:user_id>/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/<int:user_id>/conversations/<int:conv_id>', 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/<int:user_id>/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/<int:user_id>/conversations/<int:conv_id>', 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/<int:user_id>/conversations/<int:conv_id>', 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/<int:user_id>/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/<int:user_id>/agents/<agent_id>', 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/<int:user_id>/agents/<agent_id>', 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/<int:user_id>/agents/<agent_id>/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/<int:user_id>/agents/<agent_id>/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']) @app.route('/api/admin/llm', methods=['GET'])
@@ -1005,10 +613,11 @@ def add_llm_config():
conn = get_db() conn = get_db()
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature) INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature, enable_thinking, enable_vision)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (data['name'], data['provider'], data['api_url'], data['api_key'], ''', (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() conn.commit()
config_id = cursor.lastrowid config_id = cursor.lastrowid
conn.close() conn.close()
@@ -1023,9 +632,10 @@ def update_llm_config(id):
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
UPDATE llm_configs SET name=?, provider=?, api_url=?, api_key=?, model=?, 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['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.commit()
conn.close() conn.close()
return jsonify({'success': True}) return jsonify({'success': True})
@@ -1345,32 +955,22 @@ def get_frontend_config():
conn = get_db() conn = get_db()
cursor = conn.cursor() cursor = conn.cursor()
# 获取默认对话配置 # 获取默认LLM配置
cursor.execute('SELECT * FROM chat_configs WHERE is_default=1 LIMIT 1') cursor.execute('SELECT * FROM llm_configs WHERE is_default=1 AND is_active=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.fetchone() 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') cursor.execute('SELECT * FROM tool_configs WHERE is_default=1 AND is_active=1')
tools = [dict(row) for row in cursor.fetchall()] 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') 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()] 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') cursor.execute('SELECT key, value FROM system_configs')
system = {row['key']: row['value'] for row in cursor.fetchall()} system = {row['key']: row['value'] for row in cursor.fetchall()}
@@ -1380,80 +980,23 @@ def get_frontend_config():
config = { config = {
'llm': dict(llm) if llm else None, 'llm': dict(llm) if llm else None,
'tools': tools, 'tools': tools,
'allTools': allTools, # 所有活跃的工具(供前端选择)
'agents': agents, 'agents': agents,
'chat_config': dict(chat_config) if chat_config else None, 'chat_config': dict(chat_config) if chat_config else None,
'system': { 'system': {
'appName': system.get('app_name', 'AI助手'), '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', 'enableSearch': system.get('enable_search', 'true') == 'true',
'guestLimits': { 'guestLimits': {
'chatSessions': int(system.get('guest_chat_sessions', '1')), 'chatSessions': int(system.get('guest_chat_sessions', '1')),
'chatMessages': int(system.get('guest_chat_messages', '20')), 'chatMessages': int(system.get('guest_chat_messages', '20')),
'agentMessages': int(system.get('guest_agent_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) 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__': if __name__ == '__main__':

View File

@@ -250,7 +250,6 @@ async function loadUsersPage(content) {
<td> <td>
<div class="action-btns"> <div class="action-btns">
<button class="action-btn edit" onclick="showEditUserModal(${u.id})">编辑</button> <button class="action-btn edit" onclick="showEditUserModal(${u.id})">编辑</button>
<button class="action-btn" style="background: #8b5cf6; color: white;" onclick="showUserConversations(${u.id}, '${u.username}')">查看对话</button>
<button class="action-btn" style="background: #f59e0b; color: white;" onclick="showResetPasswordModal(${u.id})">重置密码</button> <button class="action-btn" style="background: #f59e0b; color: white;" onclick="showResetPasswordModal(${u.id})">重置密码</button>
<button class="action-btn delete" onclick="deleteUser(${u.id})">删除</button> <button class="action-btn delete" onclick="deleteUser(${u.id})">删除</button>
</div> </div>
@@ -397,150 +396,6 @@ async function deleteUser(id) {
loadPage('users'); loadPage('users');
} }
// ==================== 查看用户对话记录 ====================
async function showUserConversations(userId, username) {
// 加载用户对话列表
const conversations = await fetchAPI(`/api/user/${userId}/conversations`);
const content = document.getElementById('mainContent');
content.innerHTML = `
<div class="content-header">
<h1 class="content-title">用户对话记录 - ${username}</h1>
<button class="add-btn" style="background: #718096;" onclick="loadPage('users')">返回用户列表</button>
</div>
<div class="stats-grid" style="grid-template-columns: repeat(3, 1fr);">
<div class="stat-card">
<div class="stat-icon">💬</div>
<div class="stat-value">${conversations.length}</div>
<div class="stat-label">对话总数</div>
</div>
<div class="stat-card">
<div class="stat-icon">🤖</div>
<div class="stat-value">${conversations.filter(c => c.agentId).length}</div>
<div class="stat-label">智能体对话</div>
</div>
<div class="stat-card">
<div class="stat-icon">📝</div>
<div class="stat-value">${conversations.reduce((sum, c) => sum + (c.messages?.length || 0), 0)}</div>
<div class="stat-label">消息总数</div>
</div>
</div>
<div class="data-table">
<table>
<thead>
<tr>
<th>ID</th>
<th>标题</th>
<th>智能体</th>
<th>消息数</th>
<th>创建时间</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${conversations.length === 0 ? '<tr><td colspan="7" style="text-align: center; color: #999;">暂无对话记录</td></tr>' :
conversations.map(conv => `
<tr>
<td>${conv.id}</td>
<td>${conv.title || '新对话'}</td>
<td>${conv.agentId ? getAgentName(conv.agentId) : '普通对话'}</td>
<td>${conv.messages?.length || 0}</td>
<td>${formatDate(conv.createdAt || conv.created_at)}</td>
<td>${formatDate(conv.updatedAt || conv.updated_at)}</td>
<td>
<div class="action-btns">
<button class="action-btn edit" onclick="showConversationMessages(${userId}, ${conv.id}, '${conv.title || '新对话'}')">查看详情</button>
<button class="action-btn delete" onclick="deleteUserConversation(${userId}, ${conv.id})">删除</button>
</div>
</td>
</tr>
`).join('')
}
</tbody>
</table>
</div>
`;
}
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 = `
<div class="content-header">
<h1 class="content-title">对话详情 - ${title}</h1>
<button class="add-btn" style="background: #718096;" onclick="showUserConversations(${userId}, '用户')">返回对话列表</button>
</div>
<div style="background: white; padding: 16px; border-radius: 12px; margin-bottom: 16px;">
<div style="display: flex; gap: 16px; color: #718096;">
<span>💬 消息数: ${messages.length}</span>
<span>🤖 智能体: ${conv.agentId ? getAgentName(conv.agentId) : '普通对话'}</span>
<span>📅 创建: ${formatDate(conv.createdAt || conv.created_at)}</span>
</div>
</div>
<div class="data-table">
<table>
<thead>
<tr>
<th style="width: 60px;">序号</th>
<th style="width: 80px;">角色</th>
<th>内容</th>
<th style="width: 150px;">时间</th>
</tr>
</thead>
<tbody>
${messages.length === 0 ? '<tr><td colspan="4" style="text-align: center; color: #999;">暂无消息</td></tr>' :
messages.map((msg, idx) => `
<tr>
<td>${idx + 1}</td>
<td style="color: ${msg.role === 'user' ? '#3b82f6' : '#10b981'};">
${msg.role === 'user' ? '👤 用户' : '🤖 AI'}
</td>
<td style="max-width: 500px; white-space: pre-wrap; word-break: break-all;">
${escapeHtml(msg.content?.slice(0, 500) || '')}${msg.content?.length > 500 ? '...' : ''}
${msg.thinking ? `<div style="color: #f59e0b; margin-top: 8px; font-size: 12px;">💭 思考: ${escapeHtml(msg.thinking?.slice(0, 200) || '')}...</div>` : ''}
</td>
<td>${formatDate(msg.timestamp || msg.createdAt)}</td>
</tr>
`).join('')
}
</tbody>
</table>
</div>
`;
}
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) { async function loadLLMPage(content) {
@@ -559,8 +414,9 @@ async function loadLLMPage(content) {
<th>名称</th> <th>名称</th>
<th>提供商</th> <th>提供商</th>
<th>模型</th> <th>模型</th>
<th>思考模式</th>
<th>视觉能力</th>
<th>API URL</th> <th>API URL</th>
<th>状态</th>
<th>操作</th> <th>操作</th>
</tr> </tr>
</thead> </thead>
@@ -570,8 +426,9 @@ async function loadLLMPage(content) {
<td>${c.name} ${c.is_default ? '<span class="default-badge">默认</span>' : ''}</td> <td>${c.name} ${c.is_default ? '<span class="default-badge">默认</span>' : ''}</td>
<td>${c.provider}</td> <td>${c.provider}</td>
<td>${c.model}</td> <td>${c.model}</td>
<td>${c.enable_thinking ? '✅ 支持' : '❌ 不支持'}</td>
<td>${c.enable_vision ? '✅ 支持' : '❌ 不支持'}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${c.api_url}</td> <td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${c.api_url}</td>
<td>${c.is_active ? '✅ 启用' : '❌ 禁用'}</td>
<td> <td>
<div class="action-btns"> <div class="action-btns">
<button class="action-btn edit" onclick="showEditLLMModal(${c.id})">编辑</button> <button class="action-btn edit" onclick="showEditLLMModal(${c.id})">编辑</button>
@@ -622,6 +479,18 @@ function showAddLLMModal() {
<label class="form-label">Temperature</label> <label class="form-label">Temperature</label>
<input type="number" class="form-input" id="llmTemperature" value="0.7" step="0.1"> <input type="number" class="form-input" id="llmTemperature" value="0.7" step="0.1">
</div> </div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="llmEnableThinking"> 支持思考模式(深度思考)
</label>
<span style="color: #999; font-size: 12px;">模型是否支持原生思考功能如DeepSeek的think标签</span>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="llmEnableVision"> 支持视觉能力(图片分析)
</label>
<span style="color: #999; font-size: 12px;">模型是否支持图片输入和分析</span>
</div>
<button class="form-submit" onclick="saveLLM()">保存</button> <button class="form-submit" onclick="saveLLM()">保存</button>
`); `);
} }
@@ -664,6 +533,18 @@ function showEditLLMModal(id) {
<label class="form-label">Temperature</label> <label class="form-label">Temperature</label>
<input type="number" class="form-input" id="llmTemperature" value="${config.temperature}" step="0.1"> <input type="number" class="form-input" id="llmTemperature" value="${config.temperature}" step="0.1">
</div> </div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="llmEnableThinking" ${config.enable_thinking ? 'checked' : ''}> 支持思考模式(深度思考)
</label>
<span style="color: #999; font-size: 12px;">模型是否支持原生思考功能如DeepSeek的think标签</span>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="llmEnableVision" ${config.enable_vision ? 'checked' : ''}> 支持视觉能力(图片分析)
</label>
<span style="color: #999; font-size: 12px;">模型是否支持图片输入和分析</span>
</div>
<button class="form-submit" onclick="updateLLM(${id})">保存</button> <button class="form-submit" onclick="updateLLM(${id})">保存</button>
`); `);
} }
@@ -676,7 +557,9 @@ async function saveLLM() {
api_key: document.getElementById('llmApiKey').value, api_key: document.getElementById('llmApiKey').value,
model: document.getElementById('llmModel').value, model: document.getElementById('llmModel').value,
max_tokens: parseInt(document.getElementById('llmMaxTokens').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) { 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, api_key: document.getElementById('llmApiKey').value,
model: document.getElementById('llmModel').value, model: document.getElementById('llmModel').value,
max_tokens: parseInt(document.getElementById('llmMaxTokens').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); await fetchAPI(`/api/admin/llm/${id}`, 'PUT', data);
@@ -1355,26 +1240,6 @@ async function loadSystemPage(content) {
<input type="text" class="form-input" id="appVersion" value="${systemConfigs.app_version?.value || ''}"> <input type="text" class="form-input" id="appVersion" value="${systemConfigs.app_version?.value || ''}">
</div> </div>
<div class="form-group">
<label class="form-label">应用简介</label>
<input type="text" class="form-input" id="appDescription" value="${systemConfigs.app_description?.value || ''}" placeholder="如:提供智能对话、多种智能体服务">
</div>
<div class="form-group">
<label class="form-label">技术基础</label>
<input type="text" class="form-input" id="appTechnology" value="${systemConfigs.app_technology?.value || ''}" placeholder="如:智谱 GLM-4.5-Air 大模型">
</div>
<div class="form-group">
<label class="form-label">开发者</label>
<input type="text" class="form-input" id="appDeveloper" value="${systemConfigs.app_developer?.value || ''}" placeholder="如OpenClaw Team">
</div>
<div class="form-group">
<label class="form-label">更新日期</label>
<input type="text" class="form-input" id="appUpdateDate" value="${systemConfigs.app_update_date?.value || ''}" placeholder="如2026-04-27">
</div>
<h3 style="margin: 24px 0 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">游客使用限制</h3> <h3 style="margin: 24px 0 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">游客使用限制</h3>
<div class="form-group"> <div class="form-group">
@@ -1399,51 +1264,6 @@ async function loadSystemPage(content) {
<input type="text" class="form-input" id="adminPassword" value="${systemConfigs.admin_password?.value || ''}" placeholder="修改管理员密码"> <input type="text" class="form-input" id="adminPassword" value="${systemConfigs.admin_password?.value || ''}" placeholder="修改管理员密码">
</div> </div>
<h3 style="margin: 24px 0 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">TTS语音配置</h3>
<div class="form-group">
<label class="form-label">TTS方案</label>
<select class="form-input" id="ttsProvider">
<option value="edge" ${systemConfigs.tts_provider?.value === 'edge' ? 'selected' : ''}>Edge TTS免费</option>
</select>
<span style="color: #999; font-size: 12px;">目前仅支持 Edge TTS后续将添加更多方案</span>
</div>
<div class="form-group">
<label class="form-label">TTS语音</label>
<select class="form-input" id="ttsVoice">
<option value="zh-CN-XiaoxiaoNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoxiaoNeural' ? 'selected' : ''}>晓晓(女声)</option>
<option value="zh-CN-YunxiNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunxiNeural' ? 'selected' : ''}>云希(男声)</option>
<option value="zh-CN-YunjianNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunjianNeural' ? 'selected' : ''}>云健(男声)</option>
<option value="zh-CN-XiaoyiNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoyiNeural' ? 'selected' : ''}>晓伊(女声)</option>
<option value="zh-CN-YunfengNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunfengNeural' ? 'selected' : ''}>云枫(男声)</option>
<option value="zh-CN-XiaochenNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaochenNeural' ? 'selected' : ''}>晓辰(女声)</option>
<option value="zh-CN-XiaohanNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaohanNeural' ? 'selected' : ''}>晓涵(女声)</option>
<option value="zh-CN-XiaomengNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaomengNeural' ? 'selected' : ''}>晓梦(女声)</option>
<option value="zh-CN-XiaomoNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaomoNeural' ? 'selected' : ''}>晓墨(女声)</option>
<option value="zh-CN-XiaoruiNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoruiNeural' ? 'selected' : ''}>晓睿(女声)</option>
<option value="zh-CN-XiaoshuangNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoshuangNeural' ? 'selected' : ''}>晓双(女声)</option>
<option value="zh-CN-XiaoxuanNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoxuanNeural' ? 'selected' : ''}>晓萱(女声)</option>
<option value="zh-CN-XiaoyanNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoyanNeural' ? 'selected' : ''}>晓颜(女声)</option>
<option value="zh-CN-XiaoyouNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-XiaoyouNeural' ? 'selected' : ''}>晓悠(女声)</option>
<option value="zh-CN-YunyaNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunyaNeural' ? 'selected' : ''}>云雅(女声)</option>
<option value="zh-CN-YunyangNeural" ${systemConfigs.tts_voice?.value === 'zh-CN-YunyangNeural' ? 'selected' : ''}>云扬(男声)</option>
</select>
<span style="color: #999; font-size: 12px;">选择AI回复的朗读语音</span>
</div>
<h3 style="margin: 24px 0 16px; padding-top: 16px; border-top: 1px solid #e2e8f0;">链接配置</h3>
<div class="form-group">
<label class="form-label">隐私政策链接</label>
<input type="text" class="form-input" id="privacyPolicyUrl" value="${systemConfigs.privacy_policy_url?.value || ''}" placeholder="隐私政策页面URL">
</div>
<div class="form-group">
<label class="form-label">用户协议链接</label>
<input type="text" class="form-input" id="userAgreementUrl" value="${systemConfigs.user_agreement_url?.value || ''}" placeholder="用户协议页面URL">
</div>
<button class="form-submit" onclick="saveSystemConfig()">保存设置</button> <button class="form-submit" onclick="saveSystemConfig()">保存设置</button>
</div> </div>
@@ -1459,18 +1279,10 @@ async function saveSystemConfig() {
const data = { const data = {
app_name: document.getElementById('appName').value, app_name: document.getElementById('appName').value,
app_version: document.getElementById('appVersion').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_sessions: document.getElementById('guestChatSessions').value,
guest_chat_messages: document.getElementById('guestChatMessages').value, guest_chat_messages: document.getElementById('guestChatMessages').value,
guest_agent_messages: document.getElementById('guestAgentMessages').value, guest_agent_messages: document.getElementById('guestAgentMessages').value,
admin_password: document.getElementById('adminPassword').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,
}; };
await fetchAPI('/api/admin/system', 'POST', data); await fetchAPI('/api/admin/system', 'POST', data);

1245
www/app.js

File diff suppressed because it is too large Load Diff