Compare commits

...

13 Commits

Author SHA1 Message Date
3411de1612 feat: 后台管理系统 (Python Flask)
后台管理功能:
- 大模型接口配置管理(增删改、设为默认)
- 智能体管理(增删改、类别、标签、prompt、LLM配置、热度等)
- 搜索配置管理(Tavily等搜索接口)
- 系统设置(应用名称、版本、游客限制、管理员密码)
- 统计信息(LLM调用、搜索调用、智能体使用排行)

技术方案:
- Python Flask 后端服务 (端口 19020)
- SQLite 数据库存储配置
- 前端后台管理界面 (/admin)
- RESTful API 接口

后台入口:
- URL: http://localhost:19020/admin
- 默认账号: admin / admin123

前端配置获取:
- /api/config 获取LLM、搜索、智能体、系统配置
2026-04-27 12:34:01 +08:00
f24cefb2ab fix: 设置页面返回按钮修复
- switchPage函数增加mainContent存在检查
- 如果当前页面不是主页结构,先调用showMainPage重建页面
- 确保从设置子页面返回能正确回到'我的'页面
2026-04-27 12:22:37 +08:00
cacdb09201 feat: 我的界面功能完善
用户编辑:
- 用户头像选择(15种预设头像)
- 用户名修改(唯一性检查)
- 个性签名
- 个人资料(手机号只读、邮箱、性别、年龄、地区)

消息通知:
- 通知开关、声音、震动设置
- 免打扰时段设置

隐私安全:
- 修改密码
- 账号信息查看
- 清除聊天记录
- 清除缓存
- 数据安全状态显示

帮助与反馈:
- 使用帮助指南
- 问题反馈表单
- 联系方式

关于:
- 版本信息
- 检查更新
- 隐私政策/用户协议入口
2026-04-27 11:50:16 +08:00
55902aa8b6 fix: 移除验证码功能(暂时隐藏)
- 移除验证码输入框和发送验证码按钮
- 移除验证码验证逻辑
- 保留手机号格式验证(11位中国手机号)
- 保留手机号唯一性检查
- 注册字段:用户名、手机号(必填)、邮箱(可选)、密码
2026-04-27 11:31:44 +08:00
295218b4e4 feat: 注册增加手机号验证和邮箱
- 手机号输入(必填,11位中国手机号格式验证)
- 验证码机制(模拟,6位随机码,5分钟有效期)
- 发送验证码按钮(60秒倒计时)
- 验证码输入框
- 邮箱输入(可选,格式验证)
- 手机号唯一性检查(防止重复注册)
2026-04-27 11:26:51 +08:00
0812a6192f fix: 普通对话返回后不再显示智能体对话
- showConversationList 简化为直接返回主页
- 主页对话列表正确过滤(只显示没有 agentId 的普通对话)
- 添加操作菜单支持(重命名、分享、置顶、删除)
- 统一使用全局变量 currentActionConvId
2026-04-27 11:18:15 +08:00
c8cea4c1a1 feat: 登录注册系统 + 游客使用限制
功能:
- 登录/注册界面(本地存储用户数据)
- 我的页面显示用户状态、登录/退出按钮
- 游客配额显示(对话会话/消息/智能体消息)

游客限制:
- 每天最多1个对话会话
- 每天最多20条对话消息
- 只能使用通用助手智能体
- 每天最多20条智能体消息
- 登录用户无限制

限制提示:
- 达到限制弹出提示对话框
- 提示登录解锁全部功能
2026-04-27 11:06:20 +08:00
439743c051 fix: 智能体对话不显示在历史对话列表中
- 对话页面只显示没有智能体的普通对话
- 智能体对话只在智能体页面的最近使用列表显示
- 搜索功能也只搜索普通对话
2026-04-27 10:50:43 +08:00
dbd1853e6e feat: 智能体发现和收藏功能
- 智能体界面顶部右侧添加搜索按钮(发现智能体)
- 点击进入发现页面,显示所有系统智能体
- 每个智能体显示简介、热度、类别
- 支持添加和收藏按钮
- 添加的智能体自动归类到对应类别
- 智能体界面顶部右侧添加收藏按钮(☆)
- 点击进入收藏夹页面,点击智能体进入对话
- 智能体长按弹出操作菜单(置顶、收藏、删除)
- 置顶、收藏、用户智能体配置持久化存储
2026-04-27 10:45:30 +08:00
213b11c707 feat: 智能体历史改为显示每次对话记录而非合并统计 2026-04-27 00:26:51 +08:00
0303ccabc0 fix: 智能体使用记录改为发送第一条消息时才记录 2026-04-26 23:52:14 +08:00
e6b3465aa7 fix: 最近使用改为标题右侧显示更多>>链接 2026-04-26 23:48:42 +08:00
b6455e4720 feat: 智能体历史使用列表 - 显示最近使用的智能体(最多5个)+ 使用次数和时间 + 查看全部历史页面 2026-04-26 23:24:58 +08:00
9 changed files with 5910 additions and 306 deletions

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg-info/
dist/
build/
*.db
data.db
node_modules/

611
backend/app.py Normal file
View File

@@ -0,0 +1,611 @@
#!/usr/bin/env python3
"""
AI Chat App - 后台管理服务
端口: 19020 (与前端同一端口)
"""
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
app = Flask(__name__, static_folder='../www')
CORS(app)
# 数据库路径
DB_PATH = os.path.join(os.path.dirname(__file__), 'data.db')
# 管理员账户(默认)
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_search INTEGER DEFAULT 0,
tags TEXT,
heat INTEGER DEFAULT 0,
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 search_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
provider TEXT NOT NULL,
api_url TEXT NOT NULL,
api_key TEXT NOT NULL,
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 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('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 search_configs')
if cursor.fetchone()[0] == 0:
cursor.execute('''
INSERT INTO search_configs (name, provider, api_url, api_key, is_default)
VALUES ('Tavily', 'tavily', 'https://api.tavily.com/search',
'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', 1)
''')
# 初始化默认智能体
cursor.execute('SELECT COUNT(*) FROM agents')
if cursor.fetchone()[0] == 0:
default_agents = [
('assistant', '通用助手', '🤖', 'hot', '能回答各类问题,帮助写作、分析、解答疑惑', '你是一个智能助手,能够回答各类问题,帮助用户解决问题。', 9500),
('writer', '写作助手', '✍️', 'hot', '专注于文章写作、文案创作、内容润色', '你是一个专业的写作助手,擅长各类文章写作、文案创作和内容润色。', 8800),
('coder', '编程助手', '👨‍💻', 'hot', '精通编程语言,解答技术问题,生成代码', '你是一个专业的编程助手,精通各类编程语言,能够解答技术问题并生成高质量代码。', 8500),
('translator', '翻译助手', '🌐', 'hot', '多语言翻译,精准表达,文化适配', '你是一个专业的翻译助手,精通多语言翻译,能够精准表达并适配文化差异。', 7200),
('work', '工作助手', '💼', 'work', '职场问题解答,工作效率提升', '你是一个工作助手,帮助解决职场问题,提升工作效率。', 5000),
('ppt', 'PPT助手', '📊', 'work', 'PPT内容生成结构优化设计建议', '你是一个PPT助手擅长PPT内容生成、结构优化和设计建议。', 4500),
('excel', 'Excel助手', '📈', 'work', 'Excel公式、数据分析、表格优化', '你是一个Excel助手精通Excel公式、数据分析和表格优化。', 4000),
('teacher', '学习助手', '📚', 'study', '知识讲解,学习方法,考试辅导', '你是一个学习助手,擅长知识讲解、学习方法指导和考试辅导。', 5500),
('english', '英语助手', '🔤', 'study', '英语学习,语法纠正,口语练习', '你是一个英语助手,帮助英语学习、语法纠正和口语练习。', 5000),
('math', '数学助手', '🔢', 'study', '数学解题,公式推导,概念讲解', '你是一个数学助手,擅长数学解题、公式推导和概念讲解。', 4500),
('health', '健康助手', '🏥', 'life', '健康咨询,养生建议,运动指导', '你是一个健康助手,提供健康咨询、养生建议和运动指导。', 3500),
('travel', '旅行助手', '✈️', 'life', '旅行规划,景点推荐,美食指南', '你是一个旅行助手,擅长旅行规划、景点推荐和美食指南。', 3200),
('food', '美食助手', '🍳', 'life', '菜谱推荐,烹饪技巧,营养搭配', '你是一个美食助手,提供菜谱推荐、烹饪技巧和营养搭配建议。', 3000),
]
for agent in default_agents:
cursor.execute('''
INSERT INTO agents (agent_id, name, avatar, category, description, system_prompt, heat)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', agent)
# 初始化系统配置
cursor.execute('SELECT COUNT(*) FROM system_configs')
if cursor.fetchone()[0] == 0:
default_configs = [
('app_name', 'AI助手', '应用名称'),
('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', '管理员密码'),
]
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/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_search, tags, heat)
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_search', 0), data.get('tags', ''), data.get('heat', 0)))
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_search=?, tags=?, heat=?,
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_search', 0), data.get('tags', ''), data.get('heat', 0), 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/search', methods=['GET'])
def get_search_configs():
"""获取搜索配置"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('SELECT * FROM search_configs ORDER BY is_default DESC')
configs = [dict(row) for row in cursor.fetchall()]
conn.close()
return jsonify(configs)
@app.route('/api/admin/search', methods=['POST'])
def add_search_config():
"""添加搜索配置"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
INSERT INTO search_configs (name, provider, api_url, api_key, max_results)
VALUES (?, ?, ?, ?, ?)
''', (data['name'], data['provider'], data['api_url'], data['api_key'],
data.get('max_results', 10)))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/search/<int:id>', methods=['PUT'])
def update_search_config(id):
"""更新搜索配置"""
data = request.json
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
UPDATE search_configs SET name=?, provider=?, api_url=?, api_key=?, max_results=?,
updated_at=CURRENT_TIMESTAMP WHERE id=?
''', (data['name'], data['provider'], data['api_url'], data['api_key'],
data.get('max_results', 10), id))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/search/<int:id>', methods=['DELETE'])
def delete_search_config(id):
"""删除搜索配置"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('DELETE FROM search_configs WHERE id=?', (id,))
conn.commit()
conn.close()
return jsonify({'success': True})
@app.route('/api/admin/search/<int:id>/default', methods=['POST'])
def set_default_search(id):
"""设置默认搜索"""
conn = get_db()
cursor = conn.cursor()
cursor.execute('UPDATE search_configs SET is_default=0')
cursor.execute('UPDATE search_configs SET is_default=1 WHERE id=?', (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 search_configs WHERE is_default=1 AND is_active=1 LIMIT 1')
search = cursor.fetchone()
# 获取所有智能体
cursor.execute('SELECT agent_id, name, avatar, category, description, system_prompt, heat FROM agents WHERE is_active=1')
agents = [dict(row) for row in cursor.fetchall()]
# 获取系统配置
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,
'search': dict(search) if search else None,
'agents': agents,
'system': {
'appName': system.get('app_name', 'AI助手'),
'version': system.get('app_version', '3.5.1'),
'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')),
}
}
}
return jsonify(config)
# ==================== 启动 ====================
if __name__ == '__main__':
# 初始化数据库
init_db()
# 启动服务
print('🚀 AI Chat App Backend started on http://localhost:19020')
print('📊 Admin Panel: http://localhost:19020/admin')
print('👤 Default admin: admin / admin123')
app.run(host='0.0.0.0', port=19020, debug=True)

2
backend/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
flask>=2.0.0
flask-cors>=3.0.0

4
backend/start.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")"
pip install -r requirements.txt -q
python app.py

471
www/admin.html Normal file
View File

@@ -0,0 +1,471 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI助手 - 后台管理</title>
<style>
:root {
--primary: #667eea;
--primary-dark: #5a67d8;
--bg-color: #f5f7fa;
--card-bg: #ffffff;
--text-color: #2d3748;
--text-light: #718096;
--border-color: #e2e8f0;
--danger: #e53e3e;
--success: #22c55e;
--warning: #f59e0b;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
}
/* 登录页面 */
.login-page {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
}
.login-card {
background: white;
padding: 40px;
border-radius: 16px;
width: 350px;
text-align: center;
}
.login-logo {
font-size: 48px;
margin-bottom: 16px;
}
.login-title {
font-size: 24px;
font-weight: 600;
margin-bottom: 24px;
}
.login-input {
width: 100%;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: 10px;
font-size: 16px;
margin-bottom: 16px;
outline: none;
}
.login-input:focus {
border-color: var(--primary);
}
.login-btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
margin-top: 8px;
}
.login-btn:hover {
opacity: 0.9;
}
/* 主页面 */
.admin-page {
display: none;
}
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: white;
border-bottom: 1px solid var(--border-color);
}
.admin-logo {
display: flex;
align-items: center;
gap: 8px;
font-size: 20px;
font-weight: 600;
}
.admin-logo span {
font-size: 28px;
}
.admin-header-right {
display: flex;
align-items: center;
gap: 16px;
}
.admin-logout {
padding: 8px 16px;
background: var(--danger);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
}
/* 侧边栏 */
.admin-container {
display: flex;
height: calc(100vh - 60px);
}
.admin-sidebar {
width: 220px;
background: white;
border-right: 1px solid var(--border-color);
padding: 16px;
}
.sidebar-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
margin-bottom: 4px;
}
.sidebar-item:hover {
background: rgba(102, 126, 234, 0.1);
}
.sidebar-item.active {
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
color: white;
}
.sidebar-icon {
font-size: 20px;
}
/* 内容区 */
.admin-content {
flex: 1;
padding: 24px;
overflow-y: auto;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.content-title {
font-size: 24px;
font-weight: 600;
}
.add-btn {
padding: 10px 20px;
background: var(--primary);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
/* 数据卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 12px;
text-align: center;
}
.stat-icon {
font-size: 32px;
margin-bottom: 8px;
}
.stat-value {
font-size: 28px;
font-weight: 600;
color: var(--primary);
}
.stat-label {
font-size: 14px;
color: var(--text-light);
}
/* 数据表格 */
.data-table {
background: white;
border-radius: 12px;
overflow: hidden;
}
.data-table table {
width: 100%;
border-collapse: collapse;
}
.data-table th, .data-table td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.data-table th {
background: var(--bg-color);
font-weight: 600;
color: var(--text-light);
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table tr:hover td {
background: rgba(102, 126, 234, 0.05);
}
/* 操作按钮 */
.action-btns {
display: flex;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
}
.action-btn.edit {
background: var(--primary);
color: white;
}
.action-btn.delete {
background: var(--danger);
color: white;
}
.action-btn.default {
background: var(--success);
color: white;
}
/* 默认标记 */
.default-badge {
padding: 4px 8px;
background: var(--success);
color: white;
border-radius: 4px;
font-size: 12px;
}
/* 模态框 */
.modal {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.show {
display: flex;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 16px;
max-width: 500px;
width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.modal-title {
font-size: 20px;
font-weight: 600;
}
.modal-close {
font-size: 24px;
cursor: pointer;
color: var(--text-light);
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 14px;
outline: none;
}
.form-input:focus, .form-select:focus, .form-textarea:focus {
border-color: var(--primary);
}
.form-textarea {
resize: vertical;
min-height: 100px;
}
.form-submit {
width: 100%;
padding: 14px;
background: var(--primary);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
margin-top: 16px;
}
/* Toast */
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: var(--text-color);
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
opacity: 0;
transition: opacity 0.3s;
z-index: 2000;
}
.toast.show {
opacity: 1;
}
</style>
</head>
<body>
<!-- 登录页面 -->
<div class="login-page" id="loginPage">
<div class="login-card">
<div class="login-logo">🤖</div>
<h1 class="login-title">后台管理系统</h1>
<input type="text" class="login-input" id="loginUsername" placeholder="用户名">
<input type="password" class="login-input" id="loginPassword" placeholder="密码">
<button class="login-btn" id="loginBtn">登录</button>
<p style="margin-top: 16px; color: #999; font-size: 12px;">默认: admin / admin123</p>
</div>
</div>
<!-- 管理页面 -->
<div class="admin-page" id="adminPage">
<header class="admin-header">
<div class="admin-logo">
<span>🤖</span>
AI助手 - 后台管理
</div>
<div class="admin-header-right">
<span id="adminName">管理员</span>
<button class="admin-logout" id="logoutBtn">退出</button>
</div>
</header>
<div class="admin-container">
<aside class="admin-sidebar">
<div class="sidebar-item active" data-page="stats">
<span class="sidebar-icon">📊</span>
<span>统计信息</span>
</div>
<div class="sidebar-item" data-page="llm">
<span class="sidebar-icon">🧠</span>
<span>大模型配置</span>
</div>
<div class="sidebar-item" data-page="agents">
<span class="sidebar-icon">🤖</span>
<span>智能体管理</span>
</div>
<div class="sidebar-item" data-page="search">
<span class="sidebar-icon">🔍</span>
<span>搜索配置</span>
</div>
<div class="sidebar-item" data-page="system">
<span class="sidebar-icon">⚙️</span>
<span>系统设置</span>
</div>
</aside>
<main class="admin-content" id="mainContent">
<!-- 内容区动态加载 -->
</main>
</div>
</div>
<!-- Toast -->
<div class="toast" id="toast"></div>
<!-- 模态框 -->
<div class="modal" id="modal">
<div class="modal-content" id="modalContent">
<!-- 动态内容 -->
</div>
</div>
<script src="admin.js?v=1.0.0"></script>
</body>
</html>

865
www/admin.js Normal file
View File

@@ -0,0 +1,865 @@
// AI助手 - 后台管理前端
const API_BASE = '';
let currentPage = 'stats';
let llmConfigs = [];
let agents = [];
let searchConfigs = [];
let systemConfigs = {};
// ==================== 登录 ====================
document.getElementById('loginBtn').addEventListener('click', async () => {
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value;
if (!username || !password) {
showToast('请输入用户名和密码');
return;
}
try {
const res = await fetch(`${API_BASE}/api/admin/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.success) {
localStorage.setItem('adminLoggedIn', 'true');
localStorage.setItem('adminUsername', username);
document.getElementById('loginPage').style.display = 'none';
document.getElementById('adminPage').style.display = 'block';
document.getElementById('adminName').textContent = username;
loadPage('stats');
} else {
showToast(data.error || '登录失败');
}
} catch (e) {
showToast('登录失败,请检查网络');
}
});
// 检查登录状态
if (localStorage.getItem('adminLoggedIn') === 'true') {
document.getElementById('loginPage').style.display = 'none';
document.getElementById('adminPage').style.display = 'block';
document.getElementById('adminName').textContent = localStorage.getItem('adminUsername');
loadPage('stats');
}
// 退出登录
document.getElementById('logoutBtn').addEventListener('click', () => {
localStorage.removeItem('adminLoggedIn');
localStorage.removeItem('adminUsername');
document.getElementById('adminPage').style.display = 'none';
document.getElementById('loginPage').style.display = 'flex';
});
// ==================== 侧边栏导航 ====================
document.querySelectorAll('.sidebar-item').forEach(item => {
item.addEventListener('click', () => {
const page = item.getAttribute('data-page');
document.querySelectorAll('.sidebar-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
loadPage(page);
});
});
// ==================== 页面加载 ====================
async function loadPage(page) {
currentPage = page;
const content = document.getElementById('mainContent');
switch (page) {
case 'stats':
await loadStatsPage(content);
break;
case 'llm':
await loadLLMPage(content);
break;
case 'agents':
await loadAgentsPage(content);
break;
case 'search':
await loadSearchPage(content);
break;
case 'system':
await loadSystemPage(content);
break;
}
}
// ==================== 统计页面 ====================
async function loadStatsPage(content) {
const stats = await fetchAPI('/api/admin/stats');
content.innerHTML = `
<div class="content-header">
<h1 class="content-title">统计信息</h1>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">🧠</div>
<div class="stat-value">${stats.llm_total_calls || 0}</div>
<div class="stat-label">LLM 总调用</div>
</div>
<div class="stat-card">
<div class="stat-icon">📊</div>
<div class="stat-value">${stats.llm_today_calls || 0}</div>
<div class="stat-label">今日 LLM 调用</div>
</div>
<div class="stat-card">
<div class="stat-icon">🔍</div>
<div class="stat-value">${stats.search_total_calls || 0}</div>
<div class="stat-label">搜索总调用</div>
</div>
<div class="stat-card">
<div class="stat-icon">📈</div>
<div class="stat-value">${stats.search_today_calls || 0}</div>
<div class="stat-label">今日搜索</div>
</div>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">🤖</div>
<div class="stat-value">${stats.agent_count || 0}</div>
<div class="stat-label">智能体数量</div>
</div>
<div class="stat-card">
<div class="stat-icon">🧠</div>
<div class="stat-value">${stats.llm_count || 0}</div>
<div class="stat-label">LLM 配置数量</div>
</div>
<div class="stat-card">
<div class="stat-icon">👥</div>
<div class="stat-value">${stats.total_users || 0}</div>
<div class="stat-label">注册用户数</div>
</div>
<div class="stat-card">
<div class="stat-icon">📅</div>
<div class="stat-value">${new Date().toLocaleDateString()}</div>
<div class="stat-label">统计日期</div>
</div>
</div>
<div class="data-table" style="margin-top: 24px;">
<h3 style="padding: 16px; background: var(--bg-color);">热门智能体使用排行</h3>
<table>
<thead>
<tr>
<th>智能体</th>
<th>使用次数</th>
</tr>
</thead>
<tbody>
${stats.agent_usage?.map(a => `
<tr>
<td>${a.log_key}</td>
<td>${a.count}</td>
</tr>
`).join('') || '<tr><td colspan="2">暂无数据</td></tr>'}
</tbody>
</table>
</div>
`;
}
// ==================== 大模型配置页面 ====================
async function loadLLMPage(content) {
llmConfigs = await fetchAPI('/api/admin/llm');
content.innerHTML = `
<div class="content-header">
<h1 class="content-title">大模型配置</h1>
<button class="add-btn" onclick="showAddLLMModal()">+ 添加配置</button>
</div>
<div class="data-table">
<table>
<thead>
<tr>
<th>名称</th>
<th>提供商</th>
<th>模型</th>
<th>API URL</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${llmConfigs.map(c => `
<tr>
<td>${c.name} ${c.is_default ? '<span class="default-badge">默认</span>' : ''}</td>
<td>${c.provider}</td>
<td>${c.model}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${c.api_url}</td>
<td>${c.is_active ? '✅ 启用' : '❌ 禁用'}</td>
<td>
<div class="action-btns">
<button class="action-btn edit" onclick="showEditLLMModal(${c.id})">编辑</button>
${!c.is_default ? `<button class="action-btn default" onclick="setDefaultLLM(${c.id})">设为默认</button>` : ''}
<button class="action-btn delete" onclick="deleteLLM(${c.id})">删除</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
function showAddLLMModal() {
showModal('添加大模型配置', `
<div class="form-group">
<label class="form-label">名称</label>
<input type="text" class="form-input" id="llmName" placeholder="如智谱GLM">
</div>
<div class="form-group">
<label class="form-label">提供商</label>
<select class="form-select" id="llmProvider">
<option value="zhipu">智谱</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="custom">自定义</option>
</select>
</div>
<div class="form-group">
<label class="form-label">API URL</label>
<input type="text" class="form-input" id="llmApiUrl" placeholder="API endpoint URL">
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="text" class="form-input" id="llmApiKey" placeholder="API Key">
</div>
<div class="form-group">
<label class="form-label">模型</label>
<input type="text" class="form-input" id="llmModel" placeholder="如glm-4.5-air">
</div>
<div class="form-group">
<label class="form-label">Max Tokens</label>
<input type="number" class="form-input" id="llmMaxTokens" value="2048">
</div>
<div class="form-group">
<label class="form-label">Temperature</label>
<input type="number" class="form-input" id="llmTemperature" value="0.7" step="0.1">
</div>
<button class="form-submit" onclick="saveLLM()">保存</button>
`);
}
function showEditLLMModal(id) {
const config = llmConfigs.find(c => c.id === id);
if (!config) return;
showModal('编辑大模型配置', `
<div class="form-group">
<label class="form-label">名称</label>
<input type="text" class="form-input" id="llmName" value="${config.name}">
</div>
<div class="form-group">
<label class="form-label">提供商</label>
<select class="form-select" id="llmProvider">
<option value="zhipu" ${config.provider === 'zhipu' ? 'selected' : ''}>智谱</option>
<option value="openai" ${config.provider === 'openai' ? 'selected' : ''}>OpenAI</option>
<option value="anthropic" ${config.provider === 'anthropic' ? 'selected' : ''}>Anthropic</option>
<option value="custom" ${config.provider === 'custom' ? 'selected' : ''}>自定义</option>
</select>
</div>
<div class="form-group">
<label class="form-label">API URL</label>
<input type="text" class="form-input" id="llmApiUrl" value="${config.api_url}">
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="text" class="form-input" id="llmApiKey" value="${config.api_key}">
</div>
<div class="form-group">
<label class="form-label">模型</label>
<input type="text" class="form-input" id="llmModel" value="${config.model}">
</div>
<div class="form-group">
<label class="form-label">Max Tokens</label>
<input type="number" class="form-input" id="llmMaxTokens" value="${config.max_tokens}">
</div>
<div class="form-group">
<label class="form-label">Temperature</label>
<input type="number" class="form-input" id="llmTemperature" value="${config.temperature}" step="0.1">
</div>
<button class="form-submit" onclick="updateLLM(${id})">保存</button>
`);
}
async function saveLLM() {
const data = {
name: document.getElementById('llmName').value,
provider: document.getElementById('llmProvider').value,
api_url: document.getElementById('llmApiUrl').value,
api_key: document.getElementById('llmApiKey').value,
model: document.getElementById('llmModel').value,
max_tokens: parseInt(document.getElementById('llmMaxTokens').value),
temperature: parseFloat(document.getElementById('llmTemperature').value)
};
if (!data.name || !data.api_url || !data.api_key || !data.model) {
showToast('请填写完整信息');
return;
}
await fetchAPI('/api/admin/llm', 'POST', data);
closeModal();
showToast('添加成功');
loadPage('llm');
}
async function updateLLM(id) {
const data = {
name: document.getElementById('llmName').value,
provider: document.getElementById('llmProvider').value,
api_url: document.getElementById('llmApiUrl').value,
api_key: document.getElementById('llmApiKey').value,
model: document.getElementById('llmModel').value,
max_tokens: parseInt(document.getElementById('llmMaxTokens').value),
temperature: parseFloat(document.getElementById('llmTemperature').value)
};
await fetchAPI(`/api/admin/llm/${id}`, 'PUT', data);
closeModal();
showToast('更新成功');
loadPage('llm');
}
async function setDefaultLLM(id) {
await fetchAPI(`/api/admin/llm/${id}/default`, 'POST');
showToast('已设为默认');
loadPage('llm');
}
async function deleteLLM(id) {
if (!confirm('确定删除此配置?')) return;
await fetchAPI(`/api/admin/llm/${id}`, 'DELETE');
showToast('删除成功');
loadPage('llm');
}
// ==================== 智能体管理页面 ====================
async function loadAgentsPage(content) {
agents = await fetchAPI('/api/admin/agents');
llmConfigs = await fetchAPI('/api/admin/llm');
const categories = {
hot: '热门',
work: '工作',
study: '学习',
life: '生活'
};
content.innerHTML = `
<div class="content-header">
<h1 class="content-title">智能体管理</h1>
<button class="add-btn" onclick="showAddAgentModal()">+ 添加智能体</button>
</div>
<div class="data-table">
<table>
<thead>
<tr>
<th>头像</th>
<th>名称</th>
<th>类别</th>
<th>描述</th>
<th>热度</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${agents.map(a => `
<tr>
<td style="font-size: 24px;">${a.avatar}</td>
<td>${a.name}</td>
<td>${categories[a.category] || a.category}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${a.description || '-'}</td>
<td>${a.heat || 0}</td>
<td>${a.is_active ? '✅ 启用' : '❌ 禁用'}</td>
<td>
<div class="action-btns">
<button class="action-btn edit" onclick="showEditAgentModal('${a.agent_id}')">编辑</button>
<button class="action-btn delete" onclick="deleteAgent('${a.agent_id}')">删除</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
function showAddAgentModal() {
showModal('添加智能体', `
<div class="form-group">
<label class="form-label">ID</label>
<input type="text" class="form-input" id="agentId" placeholder="如assistant">
</div>
<div class="form-group">
<label class="form-label">名称</label>
<input type="text" class="form-input" id="agentName" placeholder="如:通用助手">
</div>
<div class="form-group">
<label class="form-label">头像</label>
<select class="form-select" id="agentAvatar">
${['🤖', '✍️', '👨‍💻', '🌐', '💼', '📊', '📈', '📚', '🔤', '🔢', '🏥', '✈️', '🍳', '🧑', '😊', '😎', '🤓', '🦸', '🧙', '🥷'].map(a =>
`<option value="${a}">${a}</option>`
).join('')}
</select>
</div>
<div class="form-group">
<label class="form-label">类别</label>
<select class="form-select" id="agentCategory">
<option value="hot">热门</option>
<option value="work">工作</option>
<option value="study">学习</option>
<option value="life">生活</option>
</select>
</div>
<div class="form-group">
<label class="form-label">描述</label>
<input type="text" class="form-input" id="agentDescription" placeholder="智能体描述">
</div>
<div class="form-group">
<label class="form-label">System Prompt</label>
<textarea class="form-textarea" id="agentPrompt" placeholder="系统提示词"></textarea>
</div>
<div class="form-group">
<label class="form-label">LLM配置</label>
<select class="form-select" id="agentLLM">
<option value="">使用默认</option>
${llmConfigs.map(c => `<option value="${c.id}">${c.name}</option>`).join('')}
</select>
</div>
<div class="form-group">
<label class="form-label">热度</label>
<input type="number" class="form-input" id="agentHeat" value="0">
</div>
<div class="form-group">
<label class="form-label">标签</label>
<input type="text" class="form-input" id="agentTags" placeholder="多个标签用逗号分隔">
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="agentEnableSearch"> 启用联网搜索
</label>
</div>
<button class="form-submit" onclick="saveAgent()">保存</button>
`);
}
function showEditAgentModal(agentId) {
const agent = agents.find(a => a.agent_id === agentId);
if (!agent) return;
showModal('编辑智能体', `
<div class="form-group">
<label class="form-label">ID不可修改</label>
<input type="text" class="form-input" id="agentId" value="${agent.agent_id}" readonly style="background: #f5f7fa;">
</div>
<div class="form-group">
<label class="form-label">名称</label>
<input type="text" class="form-input" id="agentName" value="${agent.name}">
</div>
<div class="form-group">
<label class="form-label">头像</label>
<select class="form-select" id="agentAvatar">
${['🤖', '✍️', '👨‍💻', '🌐', '💼', '📊', '📈', '📚', '🔤', '🔢', '🏥', '✈️', '🍳', '🧑', '😊', '😎', '🤓', '🦸', '🧙', '🥷'].map(a =>
`<option value="${a}" ${a === agent.avatar ? 'selected' : ''}>${a}</option>`
).join('')}
</select>
</div>
<div class="form-group">
<label class="form-label">类别</label>
<select class="form-select" id="agentCategory">
<option value="hot" ${agent.category === 'hot' ? 'selected' : ''}>热门</option>
<option value="work" ${agent.category === 'work' ? 'selected' : ''}>工作</option>
<option value="study" ${agent.category === 'study' ? 'selected' : ''}>学习</option>
<option value="life" ${agent.category === 'life' ? 'selected' : ''}>生活</option>
</select>
</div>
<div class="form-group">
<label class="form-label">描述</label>
<input type="text" class="form-input" id="agentDescription" value="${agent.description || ''}">
</div>
<div class="form-group">
<label class="form-label">System Prompt</label>
<textarea class="form-textarea" id="agentPrompt">${agent.system_prompt || ''}</textarea>
</div>
<div class="form-group">
<label class="form-label">LLM配置</label>
<select class="form-select" id="agentLLM">
<option value="">使用默认</option>
${llmConfigs.map(c => `<option value="${c.id}" ${c.id === agent.llm_config_id ? 'selected' : ''}>${c.name}</option>`).join('')}
</select>
</div>
<div class="form-group">
<label class="form-label">热度</label>
<input type="number" class="form-input" id="agentHeat" value="${agent.heat || 0}">
</div>
<div class="form-group">
<label class="form-label">标签</label>
<input type="text" class="form-input" id="agentTags" value="${agent.tags || ''}">
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="agentEnableSearch" ${agent.enable_search ? 'checked' : ''}> 启用联网搜索
</label>
</div>
<button class="form-submit" onclick="updateAgent('${agentId}')">保存</button>
`);
}
async function saveAgent() {
const data = {
agent_id: document.getElementById('agentId').value,
name: document.getElementById('agentName').value,
avatar: document.getElementById('agentAvatar').value,
category: document.getElementById('agentCategory').value,
description: document.getElementById('agentDescription').value,
system_prompt: document.getElementById('agentPrompt').value,
llm_config_id: document.getElementById('agentLLM').value || null,
heat: parseInt(document.getElementById('agentHeat').value),
tags: document.getElementById('agentTags').value,
enable_search: document.getElementById('agentEnableSearch').checked ? 1 : 0
};
if (!data.agent_id || !data.name || !data.system_prompt) {
showToast('请填写完整信息');
return;
}
await fetchAPI('/api/admin/agents', 'POST', data);
closeModal();
showToast('添加成功');
loadPage('agents');
}
async function updateAgent(agentId) {
const data = {
name: document.getElementById('agentName').value,
avatar: document.getElementById('agentAvatar').value,
category: document.getElementById('agentCategory').value,
description: document.getElementById('agentDescription').value,
system_prompt: document.getElementById('agentPrompt').value,
llm_config_id: document.getElementById('agentLLM').value || null,
heat: parseInt(document.getElementById('agentHeat').value),
tags: document.getElementById('agentTags').value,
enable_search: document.getElementById('agentEnableSearch').checked ? 1 : 0
};
await fetchAPI(`/api/admin/agents/${agentId}`, 'PUT', data);
closeModal();
showToast('更新成功');
loadPage('agents');
}
async function deleteAgent(agentId) {
if (!confirm('确定删除此智能体?')) return;
await fetchAPI(`/api/admin/agents/${agentId}`, 'DELETE');
showToast('删除成功');
loadPage('agents');
}
// ==================== 搜索配置页面 ====================
async function loadSearchPage(content) {
searchConfigs = await fetchAPI('/api/admin/search');
content.innerHTML = `
<div class="content-header">
<h1 class="content-title">搜索配置</h1>
<button class="add-btn" onclick="showAddSearchModal()">+ 添加配置</button>
</div>
<div class="data-table">
<table>
<thead>
<tr>
<th>名称</th>
<th>提供商</th>
<th>API URL</th>
<th>最大结果数</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${searchConfigs.map(c => `
<tr>
<td>${c.name} ${c.is_default ? '<span class="default-badge">默认</span>' : ''}</td>
<td>${c.provider}</td>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">${c.api_url}</td>
<td>${c.max_results}</td>
<td>${c.is_active ? '✅ 启用' : '❌ 禁用'}</td>
<td>
<div class="action-btns">
<button class="action-btn edit" onclick="showEditSearchModal(${c.id})">编辑</button>
${!c.is_default ? `<button class="action-btn default" onclick="setDefaultSearch(${c.id})">设为默认</button>` : ''}
<button class="action-btn delete" onclick="deleteSearch(${c.id})">删除</button>
</div>
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
function showAddSearchModal() {
showModal('添加搜索配置', `
<div class="form-group">
<label class="form-label">名称</label>
<input type="text" class="form-input" id="searchName" placeholder="如Tavily">
</div>
<div class="form-group">
<label class="form-label">提供商</label>
<select class="form-select" id="searchProvider">
<option value="tavily">Tavily</option>
<option value="google">Google</option>
<option value="bing">Bing</option>
<option value="custom">自定义</option>
</select>
</div>
<div class="form-group">
<label class="form-label">API URL</label>
<input type="text" class="form-input" id="searchApiUrl" placeholder="API endpoint URL">
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="text" class="form-input" id="searchApiKey" placeholder="API Key">
</div>
<div class="form-group">
<label class="form-label">最大结果数</label>
<input type="number" class="form-input" id="searchMaxResults" value="10">
</div>
<button class="form-submit" onclick="saveSearch()">保存</button>
`);
}
function showEditSearchModal(id) {
const config = searchConfigs.find(c => c.id === id);
if (!config) return;
showModal('编辑搜索配置', `
<div class="form-group">
<label class="form-label">名称</label>
<input type="text" class="form-input" id="searchName" value="${config.name}">
</div>
<div class="form-group">
<label class="form-label">提供商</label>
<select class="form-select" id="searchProvider">
<option value="tavily" ${config.provider === 'tavily' ? 'selected' : ''}>Tavily</option>
<option value="google" ${config.provider === 'google' ? 'selected' : ''}>Google</option>
<option value="bing" ${config.provider === 'bing' ? 'selected' : ''}>Bing</option>
<option value="custom" ${config.provider === 'custom' ? 'selected' : ''}>自定义</option>
</select>
</div>
<div class="form-group">
<label class="form-label">API URL</label>
<input type="text" class="form-input" id="searchApiUrl" value="${config.api_url}">
</div>
<div class="form-group">
<label class="form-label">API Key</label>
<input type="text" class="form-input" id="searchApiKey" value="${config.api_key}">
</div>
<div class="form-group">
<label class="form-label">最大结果数</label>
<input type="number" class="form-input" id="searchMaxResults" value="${config.max_results}">
</div>
<button class="form-submit" onclick="updateSearch(${id})">保存</button>
`);
}
async function saveSearch() {
const data = {
name: document.getElementById('searchName').value,
provider: document.getElementById('searchProvider').value,
api_url: document.getElementById('searchApiUrl').value,
api_key: document.getElementById('searchApiKey').value,
max_results: parseInt(document.getElementById('searchMaxResults').value)
};
if (!data.name || !data.api_url || !data.api_key) {
showToast('请填写完整信息');
return;
}
await fetchAPI('/api/admin/search', 'POST', data);
closeModal();
showToast('添加成功');
loadPage('search');
}
async function updateSearch(id) {
const data = {
name: document.getElementById('searchName').value,
provider: document.getElementById('searchProvider').value,
api_url: document.getElementById('searchApiUrl').value,
api_key: document.getElementById('searchApiKey').value,
max_results: parseInt(document.getElementById('searchMaxResults').value)
};
await fetchAPI(`/api/admin/search/${id}`, 'PUT', data);
closeModal();
showToast('更新成功');
loadPage('search');
}
async function setDefaultSearch(id) {
await fetchAPI(`/api/admin/search/${id}/default`, 'POST');
showToast('已设为默认');
loadPage('search');
}
async function deleteSearch(id) {
if (!confirm('确定删除此配置?')) return;
await fetchAPI(`/api/admin/search/${id}`, 'DELETE');
showToast('删除成功');
loadPage('search');
}
// ==================== 系统设置页面 ====================
async function loadSystemPage(content) {
systemConfigs = await fetchAPI('/api/admin/system');
content.innerHTML = `
<div class="content-header">
<h1 class="content-title">系统设置</h1>
</div>
<div class="data-table" style="padding: 24px;">
<div class="form-group">
<label class="form-label">应用名称</label>
<input type="text" class="form-input" id="appName" value="${systemConfigs.app_name?.value || ''}">
<span style="color: #999; font-size: 12px;">${systemConfigs.app_name?.description || ''}</span>
</div>
<div class="form-group">
<label class="form-label">应用版本</label>
<input type="text" class="form-input" id="appVersion" value="${systemConfigs.app_version?.value || ''}">
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 8px;">
<input type="checkbox" id="enableSearch" ${systemConfigs.enable_search?.value === 'true' ? 'checked' : ''}>
启用联网搜索
</label>
</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="number" class="form-input" id="guestChatSessions" value="${systemConfigs.guest_chat_sessions?.value || 1}">
</div>
<div class="form-group">
<label class="form-label">每日对话消息数</label>
<input type="number" class="form-input" id="guestChatMessages" value="${systemConfigs.guest_chat_messages?.value || 20}">
</div>
<div class="form-group">
<label class="form-label">每日智能体消息数</label>
<input type="number" class="form-input" id="guestAgentMessages" value="${systemConfigs.guest_agent_messages?.value || 20}">
</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="adminPassword" value="${systemConfigs.admin_password?.value || ''}" placeholder="修改管理员密码">
</div>
<button class="form-submit" onclick="saveSystemConfig()">保存设置</button>
</div>
`;
}
async function saveSystemConfig() {
const data = {
app_name: document.getElementById('appName').value,
app_version: document.getElementById('appVersion').value,
enable_search: document.getElementById('enableSearch').checked ? 'true' : 'false',
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
};
await fetchAPI('/api/admin/system', 'POST', data);
showToast('保存成功');
loadPage('system');
}
// ==================== 工具函数 ====================
async function fetchAPI(url, method = 'GET', data = null) {
const options = {
method,
headers: { 'Content-Type': 'application/json' }
};
if (data) {
options.body = JSON.stringify(data);
}
const res = await fetch(`${API_BASE}${url}`, options);
return res.json();
}
function showModal(title, content) {
const modal = document.getElementById('modal');
const modalContent = document.getElementById('modalContent');
modalContent.innerHTML = `
<div class="modal-header">
<h2 class="modal-title">${title}</h2>
<span class="modal-close" onclick="closeModal()">×</span>
</div>
${content}
`;
modal.classList.add('show');
}
function closeModal() {
document.getElementById('modal').classList.remove('show');
}
function showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
// 点击模态框背景关闭
document.getElementById('modal').addEventListener('click', (e) => {
if (e.target.id === 'modal') {
closeModal();
}
});

2682
www/app.js

File diff suppressed because it is too large Load Diff

View File

@@ -8,12 +8,12 @@
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>AI助手</title>
<link rel="stylesheet" href="style.css?v=3.0.2">
<link rel="stylesheet" href="style.css?v=3.5.1">
<link rel="manifest" href="manifest.json">
</head>
<body>
<div id="app"></div>
<script src="marked.min.js?v=3.0.2"></script>
<script src="app.js?v=3.0.2"></script>
<script src="marked.min.js?v=3.5.1"></script>
<script src="app.js?v=3.5.1"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff