Files
ai-chat-app/backend/app.py

1391 lines
49 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
AI Chat App - 后台管理服务
端口: 19021 (与前端同一端口)
"""
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
app = Flask(__name__, static_folder='../www')
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()
def get_db():
"""获取数据库连接"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def init_db():
"""初始化数据库"""
conn = get_db()
cursor = conn.cursor()
# 大模型接口配置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS llm_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
provider TEXT NOT NULL,
api_url TEXT NOT NULL,
api_key TEXT NOT NULL,
model TEXT NOT NULL,
max_tokens INTEGER DEFAULT 2048,
temperature REAL DEFAULT 0.7,
is_default INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 智能体配置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS agents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar TEXT DEFAULT '🤖',
category TEXT NOT NULL,
description TEXT,
system_prompt TEXT NOT NULL,
llm_config_id INTEGER,
temperature REAL DEFAULT 0.7,
max_tokens INTEGER DEFAULT 2048,
enable_tools TEXT,
tags TEXT,
heat INTEGER DEFAULT 0,
is_online INTEGER DEFAULT 1,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (llm_config_id) REFERENCES llm_configs(id)
)
''')
# 搜索配置表(改为工具配置)
cursor.execute('''
CREATE TABLE IF NOT EXISTS tool_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tool_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
type TEXT NOT NULL,
provider TEXT NOT NULL,
api_url TEXT NOT NULL,
api_key TEXT,
config_json TEXT,
max_results INTEGER DEFAULT 10,
is_default INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 对话配置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS chat_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
llm_config_id INTEGER,
enable_search INTEGER DEFAULT 1,
enable_tools TEXT,
max_history INTEGER DEFAULT 20,
temperature REAL DEFAULT 0.7,
system_prompt TEXT,
is_default INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (llm_config_id) REFERENCES llm_configs(id)
)
''')
# 系统配置表
cursor.execute('''
CREATE TABLE IF NOT EXISTS system_configs (
key TEXT PRIMARY KEY,
value TEXT,
description TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 统计日志表
cursor.execute('''
CREATE TABLE IF NOT EXISTS stats_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
log_type TEXT NOT NULL,
log_key TEXT NOT NULL,
log_value INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 用户表(后台管理)
cursor.execute('''
CREATE TABLE IF NOT EXISTS admin_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT DEFAULT 'admin',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 用户表(前端注册用户)
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
phone TEXT NOT NULL UNIQUE,
email TEXT,
avatar TEXT DEFAULT '👤',
signature TEXT,
gender TEXT,
age INTEGER,
region TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login_at TIMESTAMP
)
''')
# 用户智能体配置表(我的智能体)
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:
cursor.execute('''
INSERT INTO llm_configs (name, provider, api_url, api_key, model, is_default)
VALUES ('智谱GLM', 'zhipu', 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
'2259e33a1357460abe17919aaf81e73d.K44a8LPQTmFM5PKm', 'glm-4.5-air', 1)
''')
# 初始化默认工具配置(搜索工具)
cursor.execute('SELECT COUNT(*) FROM tool_configs')
if cursor.fetchone()[0] == 0:
cursor.execute('''
INSERT INTO tool_configs (tool_id, name, type, provider, api_url, api_key, max_results, is_default)
VALUES ('search', '联网搜索', 'search', 'tavily', 'https://api.tavily.com/search',
'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', 10, 1)
''')
# 初始化默认对话配置
cursor.execute('SELECT COUNT(*) FROM chat_configs')
if cursor.fetchone()[0] == 0:
cursor.execute('''
INSERT INTO chat_configs (config_id, name, llm_config_id, enable_tools, is_default)
VALUES ('default', '默认对话配置', 1, 'search', 1)
''')
# 初始化默认智能体
cursor.execute('SELECT COUNT(*) FROM agents')
if cursor.fetchone()[0] == 0:
default_agents = [
# 基础类别
('assistant', '通用助手', '🤖', 'basic', '能回答各类问题,帮助写作、分析、解答疑惑', '你是一个智能助手,能够回答各类问题,帮助用户解决问题。', 'hot', 9500),
('writer', '写作助手', '✍️', 'basic', '专注于文章写作、文案创作、内容润色', '你是一个专业的写作助手,擅长各类文章写作、文案创作和内容润色。', 'hot', 8800),
('coder', '编程助手', '👨‍💻', 'basic', '精通编程语言,解答技术问题,生成代码', '你是一个专业的编程助手,精通各类编程语言,能够解答技术问题并生成高质量代码。', 'hot', 8500),
('translator', '翻译助手', '🌐', 'basic', '多语言翻译,精准表达,文化适配', '你是一个专业的翻译助手,精通多语言翻译,能够精准表达并适配文化差异。', 'hot', 7200),
# 工作类别
('work', '工作助手', '💼', 'work', '职场问题解答,工作效率提升', '你是一个工作助手,帮助解决职场问题,提升工作效率。', 'popular', 5000),
('ppt', 'PPT助手', '📊', 'work', 'PPT内容生成结构优化设计建议', '你是一个PPT助手擅长PPT内容生成、结构优化和设计建议。', 'popular', 4500),
('excel', 'Excel助手', '📈', 'work', 'Excel公式、数据分析、表格优化', '你是一个Excel助手精通Excel公式、数据分析和表格优化。', '', 4000),
# 学习类别
('teacher', '学习助手', '📚', 'study', '知识讲解,学习方法,考试辅导', '你是一个学习助手,擅长知识讲解、学习方法指导和考试辅导。', 'popular', 5500),
('english', '英语助手', '🔤', 'study', '英语学习,语法纠正,口语练习', '你是一个英语助手,帮助英语学习、语法纠正和口语练习。', '', 5000),
('math', '数学助手', '🔢', 'study', '数学解题,公式推导,概念讲解', '你是一个数学助手,擅长数学解题、公式推导和概念讲解。', '', 4500),
# 生活类别
('health', '健康助手', '🏥', 'life', '健康咨询,养生建议,运动指导', '你是一个健康助手,提供健康咨询、养生建议和运动指导。', '', 3500),
('travel', '旅行助手', '✈️', 'life', '旅行规划,景点推荐,美食指南', '你是一个旅行助手,擅长旅行规划、景点推荐和美食指南。', 'popular', 3200),
('food', '美食助手', '🍳', 'life', '菜谱推荐,烹饪技巧,营养搭配', '你是一个美食助手,提供菜谱推荐、烹饪技巧和营养搭配建议。', '', 3000),
]
for agent in default_agents:
cursor.execute('''
INSERT INTO agents (agent_id, name, avatar, category, description, system_prompt, tags, heat, is_online)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1)
''', agent)
# 初始化系统配置
cursor.execute('SELECT COUNT(*) FROM system_configs')
if cursor.fetchone()[0] == 0:
default_configs = [
('app_name', 'AI助手', '应用名称'),
('app_version', '3.10.0', '应用版本'),
('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', '', '用户协议链接'),
]
for key, value, desc in default_configs:
cursor.execute('INSERT INTO system_configs (key, value, description) VALUES (?, ?, ?)', (key, value, desc))
# 初始化管理员账户
cursor.execute('SELECT COUNT(*) FROM admin_users')
if cursor.fetchone()[0] == 0:
cursor.execute('INSERT INTO admin_users (username, password_hash) VALUES (?, ?)',
(ADMIN_USERNAME, ADMIN_PASSWORD_HASH))
conn.commit()
conn.close()
# ==================== 前端静态文件服务 ====================
@app.route('/')
def index():
"""前端首页"""
return send_from_directory(app.static_folder, 'index.html')
@app.route('/admin')
def admin():
"""后台管理页面"""
return send_from_directory(app.static_folder, 'admin.html')
@app.route('/admin/<path:path>')
def admin_static(path):
"""后台管理静态文件"""
return send_from_directory(app.static_folder, path)
@app.route('/<path:path>')
def static_files(path):
"""前端静态文件"""
return send_from_directory(app.static_folder, path)
# ==================== 后台管理 API ====================
# 管理员登录验证
def check_admin_auth():
"""检查管理员权限"""
auth = request.authorization
if not auth:
return False
password_hash = hashlib.sha256(auth.password.encode()).hexdigest()
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM admin_users WHERE username = ? AND password_hash = ?',
(auth.username, password_hash))
user = cursor.fetchone()
conn.close()
return user is not None
@app.route('/api/admin/login', methods=['POST'])
def admin_login():
"""管理员登录"""
data = request.json
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'error': '请输入用户名和密码'}), 400
password_hash = hashlib.sha256(password.encode()).hexdigest()
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM admin_users WHERE username = ? AND password_hash = ?',
(username, password_hash))
user = cursor.fetchone()
conn.close()
if user:
return jsonify({'success': True, 'message': '登录成功'})
else:
return jsonify({'error': '用户名或密码错误'}), 401
# ==================== 用户管理 ====================
@app.route('/api/register', methods=['POST'])
def user_register():
"""用户注册"""
data = request.json
username = data.get('username')
password = data.get('password')
phone = data.get('phone')
email = data.get('email')
code = data.get('code') # 验证码(暂时只校验格式)
# 参数验证
if not username or not password or not phone:
return jsonify({'error': '请填写完整信息'}), 400
# 用户名长度检查
if len(username) < 2 or len(username) > 20:
return jsonify({'error': '用户名长度应为2-20字符'}), 400
# 手机号格式检查
import re
if not re.match(r'^1[3-9]\d{9}$', phone):
return jsonify({'error': '手机号格式不正确'}), 400
# 验证码检查模拟6位数字
if not code or not re.match(r'^\d{6}$', code):
return jsonify({'error': '验证码格式不正确'}), 400
# 邮箱格式检查(可选)
if email and not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', email):
return jsonify({'error': '邮箱格式不正确'}), 400
conn = get_db()
cursor = conn.cursor()
# 检查用户名是否已存在
cursor.execute('SELECT id FROM users WHERE username = ?', (username,))
if cursor.fetchone():
conn.close()
return jsonify({'error': '用户名已存在'}), 400
# 检查手机号是否已存在
cursor.execute('SELECT id FROM users WHERE phone = ?', (phone,))
if cursor.fetchone():
conn.close()
return jsonify({'error': '手机号已注册'}), 400
# 创建用户
password_hash = hashlib.sha256(password.encode()).hexdigest()
cursor.execute('''
INSERT INTO users (username, password_hash, phone, email)
VALUES (?, ?, ?, ?)
''', (username, password_hash, phone, email))
# 记录注册统计
cursor.execute('INSERT INTO stats_logs (log_type, log_key) VALUES (?, ?)', ('user_register', 'new'))
conn.commit()
user_id = cursor.lastrowid
conn.close()
return jsonify({'success': True, 'user_id': user_id})
@app.route('/api/login', methods=['POST'])
def user_login():
"""用户登录"""
data = request.json
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'error': '请输入用户名和密码'}), 400
password_hash = hashlib.sha256(password.encode()).hexdigest()
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE username = ? AND password_hash = ?', (username, password_hash))
user = cursor.fetchone()
if user:
# 更新最后登录时间
cursor.execute('UPDATE users SET last_login_at = CURRENT_TIMESTAMP WHERE id = ?', (user['id'],))
conn.commit()
conn.close()
return jsonify({
'success': True,
'user': {
'id': user['id'],
'username': user['username'],
'phone': user['phone'],
'email': user['email'],
'avatar': user['avatar'],
'signature': user['signature'],
'gender': user['gender'],
'age': user['age'],
'region': user['region']
}
})
else:
conn.close()
return jsonify({'error': '用户名或密码错误'}), 401
@app.route('/api/admin/users', methods=['GET'])
def get_users():
"""获取用户列表"""
conn = get_db()
cursor = conn.cursor()
# 支持搜索
search = request.args.get('search', '')
if search:
cursor.execute('''
SELECT id, username, phone, email, avatar, signature, gender, age, region,
created_at, last_login_at FROM users
WHERE username LIKE ? OR phone LIKE ? OR email LIKE ?
ORDER BY created_at DESC
''', (f'%{search}%', f'%{search}%', f'%{search}%'))
else:
cursor.execute('''
SELECT id, username, phone, email, avatar, signature, gender, age, region,
created_at, last_login_at FROM users ORDER BY created_at DESC
''')
users = [dict(row) for row in cursor.fetchall()]
conn.close()
return jsonify(users)
@app.route('/api/admin/users/<int:id>', methods=['GET'])
def get_user(id):
"""获取单个用户"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE id = ?', (id,))
user = cursor.fetchone()
conn.close()
if user:
return jsonify(dict(user))
else:
return jsonify({'error': '用户不存在'}), 404
@app.route('/api/admin/users/<int:id>', methods=['PUT'])
def update_user(id):
"""更新用户信息"""
data = request.json
conn = get_db()
cursor = conn.cursor()
# 检查用户是否存在
cursor.execute('SELECT id FROM users WHERE id = ?', (id,))
if not cursor.fetchone():
conn.close()
return jsonify({'error': '用户不存在'}), 404
# 检查用户名是否重复(如果要修改用户名)
if data.get('username'):
cursor.execute('SELECT id FROM users WHERE username = ? AND id != ?', (data['username'], id))
if cursor.fetchone():
conn.close()
return jsonify({'error': '用户名已存在'}), 400
# 更新用户信息
cursor.execute('''
UPDATE users SET username=?, email=?, avatar=?, signature=?, gender=?, age=?, region=?,
updated_at=CURRENT_TIMESTAMP WHERE id=?
''', (data.get('username'), data.get('email'), data.get('avatar', '👤'),
data.get('signature'), data.get('gender'), data.get('age'),
data.get('region'), id))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/users/<int:id>', methods=['DELETE'])
def delete_user(id):
"""删除用户"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('DELETE FROM users WHERE id = ?', (id,))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/users/<int:id>/password', methods=['PUT'])
def reset_user_password(id):
"""重置用户密码"""
data = request.json
new_password = data.get('password')
if not new_password or len(new_password) < 6:
return jsonify({'error': '密码长度至少6位'}), 400
password_hash = hashlib.sha256(new_password.encode()).hexdigest()
conn = get_db()
cursor = conn.cursor()
cursor.execute('UPDATE users SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE id=?',
(password_hash, id))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/user/<int:id>', methods=['PUT'])
def update_user_profile(id):
"""用户更新自己的资料"""
data = request.json
conn = get_db()
cursor = conn.cursor()
# 检查用户是否存在
cursor.execute('SELECT id FROM users WHERE id = ?', (id,))
if not cursor.fetchone():
conn.close()
return jsonify({'error': '用户不存在'}), 404
# 更新用户资料
cursor.execute('''
UPDATE users SET avatar=?, signature=?, gender=?, age=?, region=?, email=?,
updated_at=CURRENT_TIMESTAMP WHERE id=?
''', (data.get('avatar', '👤'), data.get('signature'), data.get('gender'),
data.get('age'), data.get('region'), data.get('email'), id))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/user/<int:id>/password', methods=['PUT'])
def change_user_password(id):
"""用户修改自己的密码"""
data = request.json
old_password = data.get('old_password')
new_password = data.get('new_password')
if not old_password or not new_password:
return jsonify({'error': '请输入旧密码和新密码'}), 400
if len(new_password) < 6:
return jsonify({'error': '新密码长度至少6位'}), 400
old_hash = hashlib.sha256(old_password.encode()).hexdigest()
new_hash = hashlib.sha256(new_password.encode()).hexdigest()
conn = get_db()
cursor = conn.cursor()
# 验证旧密码
cursor.execute('SELECT id FROM users WHERE id = ? AND password_hash = ?', (id, old_hash))
if not cursor.fetchone():
conn.close()
return jsonify({'error': '旧密码不正确'}), 400
# 更新密码
cursor.execute('UPDATE users SET password_hash=?, updated_at=CURRENT_TIMESTAMP WHERE id=?',
(new_hash, id))
conn.commit()
conn.close()
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'])
def get_llm_configs():
"""获取所有大模型配置"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM llm_configs ORDER BY is_default DESC, created_at DESC')
configs = [dict(row) for row in cursor.fetchall()]
conn.close()
return jsonify(configs)
@app.route('/api/admin/llm', methods=['POST'])
def add_llm_config():
"""添加大模型配置"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO llm_configs (name, provider, api_url, api_key, model, max_tokens, temperature)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (data['name'], data['provider'], data['api_url'], data['api_key'],
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7)))
conn.commit()
config_id = cursor.lastrowid
conn.close()
return jsonify({'success': True, 'id': config_id})
@app.route('/api/admin/llm/<int:id>', methods=['PUT'])
def update_llm_config(id):
"""更新大模型配置"""
data = request.json
conn = get_db()
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=?
''', (data['name'], data['provider'], data['api_url'], data['api_key'],
data['model'], data.get('max_tokens', 2048), data.get('temperature', 0.7), id))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/llm/<int:id>', methods=['DELETE'])
def delete_llm_config(id):
"""删除大模型配置"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('DELETE FROM llm_configs WHERE id=?', (id,))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/llm/<int:id>/default', methods=['POST'])
def set_default_llm(id):
"""设置默认大模型"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('UPDATE llm_configs SET is_default=0')
cursor.execute('UPDATE llm_configs SET is_default=1 WHERE id=?', (id,))
conn.commit()
conn.close()
return jsonify({'success': True})
# ==================== 智能体管理 ====================
@app.route('/api/admin/agents', methods=['GET'])
def get_agents():
"""获取所有智能体"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM agents ORDER BY category, heat DESC')
agents = [dict(row) for row in cursor.fetchall()]
conn.close()
return jsonify(agents)
@app.route('/api/admin/agents', methods=['POST'])
def add_agent():
"""添加智能体"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO agents (agent_id, name, avatar, category, description, system_prompt,
llm_config_id, temperature, max_tokens, enable_tools, tags, heat, is_online)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (data['agent_id'], data['name'], data.get('avatar', '🤖'), data['category'],
data.get('description', ''), data['system_prompt'], data.get('llm_config_id'),
data.get('temperature', 0.7), data.get('max_tokens', 2048),
data.get('enable_tools', ''), data.get('tags', ''), data.get('heat', 0),
data.get('is_online', 1)))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/agents/<agent_id>', methods=['PUT'])
def update_agent(agent_id):
"""更新智能体"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
UPDATE agents SET name=?, avatar=?, category=?, description=?, system_prompt=?,
llm_config_id=?, temperature=?, max_tokens=?, enable_tools=?, tags=?, heat=?, is_online=?,
updated_at=CURRENT_TIMESTAMP WHERE agent_id=?
''', (data['name'], data.get('avatar', '🤖'), data['category'],
data.get('description', ''), data['system_prompt'], data.get('llm_config_id'),
data.get('temperature', 0.7), data.get('max_tokens', 2048),
data.get('enable_tools', ''), data.get('tags', ''), data.get('heat', 0),
data.get('is_online', 1), agent_id))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/agents/<agent_id>', methods=['DELETE'])
def delete_agent(agent_id):
"""删除智能体"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('DELETE FROM agents WHERE agent_id=?', (agent_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/agents/<agent_id>/online', methods=['POST'])
def toggle_agent_online(agent_id):
"""切换智能体上线状态"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('UPDATE agents SET is_online=?, updated_at=CURRENT_TIMESTAMP WHERE agent_id=?',
(data.get('is_online', 1), agent_id))
conn.commit()
conn.close()
return jsonify({'success': True})
# ==================== 对话配置管理 ====================
@app.route('/api/admin/chat', methods=['GET'])
def get_chat_config():
"""获取对话配置(只有一个默认配置)"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM chat_configs WHERE is_default=1 LIMIT 1')
config = cursor.fetchone()
if not config:
# 如果没有配置,创建默认配置
cursor.execute('''
INSERT INTO chat_configs (config_id, name, llm_config_id, enable_search, enable_tools, max_history, temperature, is_default)
VALUES ('default', '默认配置', 1, 1, 'search', 20, 0.7, 1)
''')
conn.commit()
cursor.execute('SELECT * FROM chat_configs WHERE is_default=1 LIMIT 1')
config = cursor.fetchone()
conn.close()
return jsonify(dict(config) if config else {})
@app.route('/api/admin/chat', methods=['PUT'])
def update_chat_config():
"""更新对话配置"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
UPDATE chat_configs SET
llm_config_id=?, enable_search=?, enable_tools=?, max_history=?,
temperature=?, system_prompt=?, updated_at=CURRENT_TIMESTAMP
WHERE is_default=1
''', (data.get('llm_config_id', 1), data.get('enable_search', 1),
data.get('enable_tools', 'search'), data.get('max_history', 20),
data.get('temperature', 0.7), data.get('system_prompt', '')))
conn.commit()
conn.close()
return jsonify({'success': True})
# ==================== 工具配置管理 ====================
@app.route('/api/admin/tools', methods=['GET'])
def get_tool_configs():
"""获取工具配置"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM tool_configs ORDER BY is_default DESC, type')
tools = [dict(row) for row in cursor.fetchall()]
conn.close()
return jsonify(tools)
@app.route('/api/admin/tools', methods=['POST'])
def add_tool_config():
"""添加工具配置"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO tool_configs (tool_id, name, type, provider, api_url, api_key, config_json, max_results)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (data['tool_id'], data['name'], data['type'], data['provider'],
data['api_url'], data.get('api_key', ''), data.get('config_json', ''),
data.get('max_results', 10)))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/tools/<tool_id>', methods=['PUT'])
def update_tool_config(tool_id):
"""更新工具配置"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
UPDATE tool_configs SET name=?, type=?, provider=?, api_url=?, api_key=?,
config_json=?, max_results=?, updated_at=CURRENT_TIMESTAMP WHERE tool_id=?
''', (data['name'], data['type'], data['provider'], data['api_url'],
data.get('api_key', ''), data.get('config_json', ''),
data.get('max_results', 10), tool_id))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/tools/<tool_id>', methods=['DELETE'])
def delete_tool_config(tool_id):
"""删除工具配置"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('DELETE FROM tool_configs WHERE tool_id=?', (tool_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/tools/<tool_id>/default', methods=['POST'])
def set_default_tool(tool_id):
"""设置默认工具"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('UPDATE tool_configs SET is_default=0 WHERE type=(SELECT type FROM tool_configs WHERE tool_id=?)', (tool_id,))
cursor.execute('UPDATE tool_configs SET is_default=1 WHERE tool_id=?', (tool_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
# ==================== 系统配置管理 ====================
@app.route('/api/admin/system', methods=['GET'])
def get_system_configs():
"""获取系统配置"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM system_configs')
configs = {row['key']: {'value': row['value'], 'description': row['description']}
for row in cursor.fetchall()}
conn.close()
return jsonify(configs)
@app.route('/api/admin/system', methods=['POST'])
def update_system_config():
"""更新系统配置"""
data = request.json
conn = get_db()
cursor = conn.cursor()
for key, value in data.items():
cursor.execute('''
UPDATE system_configs SET value=?, updated_at=CURRENT_TIMESTAMP WHERE key=?
''', (value, key))
conn.commit()
conn.close()
return jsonify({'success': True})
# ==================== 统计信息 ====================
@app.route('/api/admin/stats', methods=['GET'])
def get_stats():
"""获取统计信息"""
conn = get_db()
cursor = conn.cursor()
# 今日统计
today = datetime.now().strftime('%Y-%m-%d')
# 各类型统计
stats = {}
# LLM调用统计
cursor.execute('SELECT SUM(log_value) FROM stats_logs WHERE log_type="llm_call"')
stats['llm_total_calls'] = cursor.fetchone()[0] or 0
cursor.execute('SELECT SUM(log_value) FROM stats_logs WHERE log_type="llm_call" AND created_at >= ?', (today,))
stats['llm_today_calls'] = cursor.fetchone()[0] or 0
# 搜索统计
cursor.execute('SELECT SUM(log_value) FROM stats_logs WHERE log_type="search_call"')
stats['search_total_calls'] = cursor.fetchone()[0] or 0
cursor.execute('SELECT SUM(log_value) FROM stats_logs WHERE log_type="search_call" AND created_at >= ?', (today,))
stats['search_today_calls'] = cursor.fetchone()[0] or 0
# 智能体使用统计
cursor.execute('''
SELECT log_key, SUM(log_value) as count FROM stats_logs
WHERE log_type="agent_use" GROUP BY log_key ORDER BY count DESC LIMIT 10
''')
stats['agent_usage'] = [dict(row) for row in cursor.fetchall()]
# 用户统计
cursor.execute('SELECT COUNT(*) FROM stats_logs WHERE log_type="user_register"')
stats['total_users'] = cursor.fetchone()[0] or 0
# 智能体数量
cursor.execute('SELECT COUNT(*) FROM agents WHERE is_active=1')
stats['agent_count'] = cursor.fetchone()[0]
# LLM配置数量
cursor.execute('SELECT COUNT(*) FROM llm_configs WHERE is_active=1')
stats['llm_count'] = cursor.fetchone()[0]
conn.close()
return jsonify(stats)
@app.route('/api/admin/stats/log', methods=['POST'])
def log_stats():
"""记录统计日志"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('INSERT INTO stats_logs (log_type, log_key, log_value) VALUES (?, ?, ?)',
(data['type'], data.get('key', ''), data.get('value', 1)))
conn.commit()
conn.close()
return jsonify({'success': True})
# ==================== 前端配置获取 ====================
@app.route('/api/config', methods=['GET'])
def get_frontend_config():
"""获取前端使用的配置"""
conn = get_db()
cursor = conn.cursor()
# 获取默认LLM配置
cursor.execute('SELECT * FROM llm_configs WHERE is_default=1 AND is_active=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()}
conn.close()
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'),
'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', ''),
}
}
return jsonify(config)
# ==================== 启动 ====================
if __name__ == '__main__':
# 初始化数据库
init_db()
# 启动服务
print('🚀 AI Chat App Backend started on http://localhost:19021')
print('📊 Admin Panel: http://localhost:19021/admin')
print('👤 Default admin: admin / admin123')
app.run(host='0.0.0.0', port=19021, debug=True)