add tts
This commit is contained in:
parent
6d3e07ce35
commit
31581e24a9
173
app.py
173
app.py
@ -4,13 +4,15 @@
|
||||
import os
|
||||
import json
|
||||
import base64
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, session
|
||||
import requests
|
||||
from flask import Flask, render_template, request, jsonify, redirect, url_for, session, Response
|
||||
from flask_cors import CORS
|
||||
from werkzeug.utils import secure_filename
|
||||
from app.api.baidu_image_search import BaiduImageSearch
|
||||
from app.api.image_utils import ImageUtils
|
||||
from app.api.azure_openai import AzureOpenAI
|
||||
from app.api.type_manager_mongo import TypeManagerMongo
|
||||
from app.api.robot_manager import RobotManager
|
||||
|
||||
app = Flask(__name__, template_folder='app/templates', static_folder='app/static')
|
||||
CORS(app) # 启用CORS支持跨域请求
|
||||
@ -40,6 +42,9 @@ except ValueError as e:
|
||||
# 初始化类型管理器
|
||||
type_manager = TypeManagerMongo()
|
||||
|
||||
# 初始化机器人角色管理器
|
||||
robot_manager = RobotManager()
|
||||
|
||||
def allowed_file(filename):
|
||||
"""检查文件扩展名是否允许"""
|
||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
@ -231,6 +236,9 @@ def upload_chat_image():
|
||||
if not allowed_file(file.filename):
|
||||
return jsonify({'error': '不支持的文件类型'}), 400
|
||||
|
||||
# 获取选择的机器人角色ID
|
||||
robot_id = request.form.get('robot_id', '')
|
||||
|
||||
# 保存文件
|
||||
filename = secure_filename(file.filename)
|
||||
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
||||
@ -239,6 +247,9 @@ def upload_chat_image():
|
||||
# 存储图片路径到会话
|
||||
session['chat_image_path'] = file_path
|
||||
|
||||
# 存储机器人角色ID到会话
|
||||
session['robot_id'] = robot_id
|
||||
|
||||
# 清空对话历史
|
||||
session['conversation_history'] = []
|
||||
|
||||
@ -269,11 +280,22 @@ def upload_chat_image():
|
||||
except:
|
||||
continue
|
||||
|
||||
# 获取机器人角色信息
|
||||
robot_info = None
|
||||
if robot_id:
|
||||
robot = robot_manager.get_robot(robot_id)
|
||||
if robot:
|
||||
robot_info = {
|
||||
'name': robot.get('name', ''),
|
||||
'background': robot.get('background', '')
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'image_path': file_path,
|
||||
'image_type': image_type,
|
||||
'description': description
|
||||
'description': description,
|
||||
'robot': robot_info
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@ -290,6 +312,7 @@ def chat():
|
||||
|
||||
message = data['message']
|
||||
image_path = session.get('chat_image_path')
|
||||
robot_id = session.get('robot_id', '')
|
||||
|
||||
if not image_path or not os.path.exists(image_path):
|
||||
return jsonify({'error': '没有上传图片或图片已失效'}), 400
|
||||
@ -297,12 +320,23 @@ def chat():
|
||||
# 获取对话历史
|
||||
conversation_history = session.get('conversation_history', [])
|
||||
|
||||
# 获取机器人角色信息
|
||||
robot_info = None
|
||||
if robot_id:
|
||||
robot = robot_manager.get_robot(robot_id)
|
||||
if robot:
|
||||
robot_info = {
|
||||
'name': robot.get('name', ''),
|
||||
'background': robot.get('background', '')
|
||||
}
|
||||
|
||||
try:
|
||||
# 调用Azure OpenAI API进行对话
|
||||
response = azure_openai_api.chat_with_image(
|
||||
image_path=image_path,
|
||||
message=message,
|
||||
conversation_history=conversation_history
|
||||
conversation_history=conversation_history,
|
||||
robot_info=robot_info
|
||||
)
|
||||
|
||||
# 提取回复内容
|
||||
@ -320,5 +354,138 @@ def chat():
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/imgsearcherApi/robots', methods=['GET'])
|
||||
def robot_page():
|
||||
"""机器人角色管理页面"""
|
||||
return render_template('robots.html')
|
||||
|
||||
@app.route('/imgsearcherApi/api/robots', methods=['GET'])
|
||||
def get_robots():
|
||||
"""获取所有机器人角色"""
|
||||
try:
|
||||
robots = robot_manager.get_all_robots()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'robots': robots
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/imgsearcherApi/api/robots', methods=['POST'])
|
||||
def add_robot():
|
||||
"""添加新的机器人角色"""
|
||||
try:
|
||||
name = request.form.get('name')
|
||||
background = request.form.get('background')
|
||||
|
||||
if not name or not background:
|
||||
return jsonify({'error': '机器人名称和背景故事不能为空'}), 400
|
||||
|
||||
avatar_file = None
|
||||
if 'avatar' in request.files:
|
||||
avatar_file = request.files['avatar']
|
||||
if avatar_file.filename == '':
|
||||
avatar_file = None
|
||||
elif not allowed_file(avatar_file.filename):
|
||||
return jsonify({'error': '不支持的文件类型'}), 400
|
||||
|
||||
result = robot_manager.add_robot(name, background, avatar_file)
|
||||
|
||||
if 'error' in result:
|
||||
return jsonify(result), 400
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'robot': result
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['GET'])
|
||||
def get_robot(robot_id):
|
||||
"""获取指定机器人角色"""
|
||||
try:
|
||||
robot = robot_manager.get_robot(robot_id)
|
||||
if not robot:
|
||||
return jsonify({'error': '找不到该机器人'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'robot': robot
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['PUT'])
|
||||
def update_robot(robot_id):
|
||||
"""更新机器人角色"""
|
||||
try:
|
||||
name = request.form.get('name')
|
||||
background = request.form.get('background')
|
||||
|
||||
avatar_file = None
|
||||
if 'avatar' in request.files:
|
||||
avatar_file = request.files['avatar']
|
||||
if avatar_file.filename == '':
|
||||
avatar_file = None
|
||||
elif not allowed_file(avatar_file.filename):
|
||||
return jsonify({'error': '不支持的文件类型'}), 400
|
||||
|
||||
result = robot_manager.update_robot(robot_id, name, background, avatar_file)
|
||||
|
||||
if 'error' in result:
|
||||
return jsonify(result), 400
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'robot': result
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/imgsearcherApi/api/robots/<robot_id>', methods=['DELETE'])
|
||||
def delete_robot(robot_id):
|
||||
"""删除机器人角色"""
|
||||
try:
|
||||
success = robot_manager.delete_robot(robot_id)
|
||||
if not success:
|
||||
return jsonify({'error': '找不到该机器人或删除失败'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# ==================== TTS ====================
|
||||
@app.route('/imgsearcherApi/api/tts', methods=['POST'])
|
||||
def tts():
|
||||
"""文本转语音接口,返回音频数据流"""
|
||||
data = request.get_json()
|
||||
if not data or 'text' not in data:
|
||||
return jsonify({'error': '缺少text参数'}), 400
|
||||
|
||||
text = data['text']
|
||||
prompt_text = data.get('prompt_text') or "我是威震天,我只代表月亮消灭你"
|
||||
prompt_wav = data.get('prompt_wav') or "data_workspace/data/workspace_170/我是威震天.wav"
|
||||
|
||||
tts_base_url = "http://180.76.186.85:20099/CosyVoice/v1/zero_shot"
|
||||
params = {
|
||||
'tts_text': text,
|
||||
'prompt_text': prompt_text,
|
||||
'prompt_wav': prompt_wav,
|
||||
'text_split_method': 'cut5'
|
||||
}
|
||||
|
||||
try:
|
||||
r = requests.get(tts_base_url, params=params, timeout=20)
|
||||
if r.status_code == 200:
|
||||
return Response(r.content, content_type=r.headers.get('Content-Type', 'audio/wav'))
|
||||
else:
|
||||
print(f"TTS remote error {r.status_code}: {r.text}")
|
||||
return jsonify({'error': f'TTS服务错误: {r.text}'}), 502
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'TTS请求失败: {str(e)}'}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5001)
|
||||
|
||||
@ -85,7 +85,7 @@ class AzureOpenAI:
|
||||
print(f"获取图片类型和描述失败: {str(e)}")
|
||||
return "", ""
|
||||
|
||||
def chat_with_image(self, image_path, message, conversation_history=None):
|
||||
def chat_with_image(self, image_path, message, conversation_history=None, robot_info=None):
|
||||
"""
|
||||
使用图片和消息与GPT-4o多模态模型进行对话
|
||||
|
||||
@ -93,6 +93,7 @@ class AzureOpenAI:
|
||||
image_path: 图片路径
|
||||
message: 用户消息
|
||||
conversation_history: 对话历史记录
|
||||
robot_info: 机器人角色信息,包含name和background
|
||||
|
||||
Returns:
|
||||
dict: 模型响应
|
||||
@ -107,7 +108,14 @@ class AzureOpenAI:
|
||||
base64_image = self._encode_image(image_path)
|
||||
|
||||
# 构建系统提示
|
||||
system_message = "你是一个智能助手,能够分析图片并回答问题。"
|
||||
if robot_info and robot_info.get('name') and robot_info.get('background'):
|
||||
# 如果有机器人角色信息,使用机器人角色
|
||||
system_message = f"你是{robot_info['name']},一个能够分析图片并回答问题的角色。\n\n你的背景故事:{robot_info['background']}\n\n在对话中,你应该始终保持这个角色的身份和特点,用第一人称回答问题。"
|
||||
else:
|
||||
# 默认智能助手
|
||||
system_message = "你是一个智能助手,能够分析图片并回答问题。"
|
||||
|
||||
# 添加图片类型和描述信息
|
||||
if image_type and description:
|
||||
system_message += f"\n\n这是一张{image_type}的图片。\n描述信息:{description}\n\n请基于这些信息和图片内容回答用户的问题。"
|
||||
|
||||
|
||||
217
app/api/robot_manager.py
Normal file
217
app/api/robot_manager.py
Normal file
@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import datetime
|
||||
from pymongo import MongoClient
|
||||
from dotenv import load_dotenv
|
||||
import base64
|
||||
import uuid
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
class RobotManager:
|
||||
"""管理机器人角色的类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化MongoDB连接"""
|
||||
# 从环境变量获取MongoDB连接信息
|
||||
mongo_uri = os.getenv('MONGO_URI', 'mongodb://localhost:27017/')
|
||||
db_name = os.getenv('MONGO_DB_NAME', 'imgsearcher')
|
||||
|
||||
# 连接MongoDB
|
||||
self.client = MongoClient(mongo_uri)
|
||||
self.db = self.client[db_name]
|
||||
self.collection = self.db['robot_characters']
|
||||
|
||||
# 确保有索引以提高查询性能
|
||||
self.collection.create_index('robot_id')
|
||||
self.collection.create_index('name')
|
||||
|
||||
# 上传文件夹路径
|
||||
self.upload_folder = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'uploads')
|
||||
if not os.path.exists(self.upload_folder):
|
||||
os.makedirs(self.upload_folder)
|
||||
|
||||
print(f"MongoDB RobotManager 初始化完成,连接到 {db_name}")
|
||||
|
||||
def add_robot(self, name, background, avatar_file=None):
|
||||
"""
|
||||
添加新的机器人角色
|
||||
|
||||
Args:
|
||||
name: 机器人名称
|
||||
background: 机器人背景故事
|
||||
avatar_file: 机器人头像文件(可选)
|
||||
|
||||
Returns:
|
||||
dict: 包含新添加的机器人信息
|
||||
"""
|
||||
if not name or not background:
|
||||
return {"error": "机器人名称和背景故事不能为空"}
|
||||
|
||||
# 检查是否已存在同名机器人
|
||||
if self.collection.find_one({"name": name}):
|
||||
return {"error": f"已存在名为 '{name}' 的机器人"}
|
||||
|
||||
# 生成唯一ID
|
||||
robot_id = str(uuid.uuid4())
|
||||
|
||||
# 处理头像文件
|
||||
avatar_path = None
|
||||
if avatar_file:
|
||||
# 保存头像文件
|
||||
filename = f"robot_avatar_{robot_id}.{avatar_file.filename.split('.')[-1]}"
|
||||
file_path = os.path.join(self.upload_folder, filename)
|
||||
avatar_file.save(file_path)
|
||||
avatar_path = file_path
|
||||
|
||||
# 创建机器人记录
|
||||
robot = {
|
||||
"robot_id": robot_id,
|
||||
"name": name,
|
||||
"background": background,
|
||||
"avatar_path": avatar_path,
|
||||
"created_at": datetime.datetime.now()
|
||||
}
|
||||
|
||||
# 保存到数据库
|
||||
self.collection.insert_one(robot)
|
||||
|
||||
# 返回新添加的机器人信息(不包含MongoDB的_id字段)
|
||||
robot.pop("_id", None)
|
||||
return robot
|
||||
|
||||
def get_robot(self, robot_id):
|
||||
"""
|
||||
获取指定ID的机器人信息
|
||||
|
||||
Args:
|
||||
robot_id: 机器人ID
|
||||
|
||||
Returns:
|
||||
dict: 机器人信息,如果不存在则返回None
|
||||
"""
|
||||
robot = self.collection.find_one({"robot_id": robot_id})
|
||||
if robot:
|
||||
# 转换MongoDB的_id为字符串
|
||||
robot["_id"] = str(robot["_id"])
|
||||
|
||||
# 如果有头像,添加base64编码的头像数据
|
||||
if robot.get("avatar_path") and os.path.exists(robot["avatar_path"]):
|
||||
with open(robot["avatar_path"], "rb") as f:
|
||||
avatar_data = base64.b64encode(f.read()).decode('utf-8')
|
||||
robot["avatar_data"] = avatar_data
|
||||
|
||||
return robot
|
||||
return None
|
||||
|
||||
def update_robot(self, robot_id, name=None, background=None, avatar_file=None):
|
||||
"""
|
||||
更新机器人信息
|
||||
|
||||
Args:
|
||||
robot_id: 机器人ID
|
||||
name: 新的机器人名称(可选)
|
||||
background: 新的背景故事(可选)
|
||||
avatar_file: 新的头像文件(可选)
|
||||
|
||||
Returns:
|
||||
dict: 更新后的机器人信息
|
||||
"""
|
||||
# 获取现有机器人
|
||||
robot = self.collection.find_one({"robot_id": robot_id})
|
||||
if not robot:
|
||||
return {"error": f"找不到ID为 '{robot_id}' 的机器人"}
|
||||
|
||||
# 准备更新数据
|
||||
update_data = {}
|
||||
|
||||
if name:
|
||||
# 检查新名称是否与其他机器人冲突
|
||||
existing = self.collection.find_one({"name": name, "robot_id": {"$ne": robot_id}})
|
||||
if existing:
|
||||
return {"error": f"已存在名为 '{name}' 的机器人"}
|
||||
update_data["name"] = name
|
||||
|
||||
if background:
|
||||
update_data["background"] = background
|
||||
|
||||
# 处理新头像
|
||||
if avatar_file:
|
||||
# 删除旧头像
|
||||
old_avatar_path = robot.get("avatar_path")
|
||||
if old_avatar_path and os.path.exists(old_avatar_path):
|
||||
try:
|
||||
os.remove(old_avatar_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 保存新头像
|
||||
filename = f"robot_avatar_{robot_id}.{avatar_file.filename.split('.')[-1]}"
|
||||
file_path = os.path.join(self.upload_folder, filename)
|
||||
avatar_file.save(file_path)
|
||||
update_data["avatar_path"] = file_path
|
||||
|
||||
# 更新数据库
|
||||
if update_data:
|
||||
update_data["updated_at"] = datetime.datetime.now()
|
||||
self.collection.update_one({"robot_id": robot_id}, {"$set": update_data})
|
||||
|
||||
# 返回更新后的机器人信息
|
||||
return self.get_robot(robot_id)
|
||||
|
||||
def delete_robot(self, robot_id):
|
||||
"""
|
||||
删除机器人
|
||||
|
||||
Args:
|
||||
robot_id: 机器人ID
|
||||
|
||||
Returns:
|
||||
bool: 是否成功删除
|
||||
"""
|
||||
# 获取机器人信息
|
||||
robot = self.collection.find_one({"robot_id": robot_id})
|
||||
if not robot:
|
||||
return False
|
||||
|
||||
# 删除头像文件
|
||||
avatar_path = robot.get("avatar_path")
|
||||
if avatar_path and os.path.exists(avatar_path):
|
||||
try:
|
||||
os.remove(avatar_path)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 从数据库中删除
|
||||
result = self.collection.delete_one({"robot_id": robot_id})
|
||||
return result.deleted_count > 0
|
||||
|
||||
def get_all_robots(self):
|
||||
"""
|
||||
获取所有机器人列表
|
||||
|
||||
Returns:
|
||||
list: 机器人信息列表
|
||||
"""
|
||||
robots = []
|
||||
for robot in self.collection.find():
|
||||
# 转换MongoDB的_id为字符串
|
||||
robot["_id"] = str(robot["_id"])
|
||||
|
||||
# 如果有头像,添加base64编码的头像数据
|
||||
if robot.get("avatar_path") and os.path.exists(robot["avatar_path"]):
|
||||
with open(robot["avatar_path"], "rb") as f:
|
||||
avatar_data = base64.b64encode(f.read()).decode('utf-8')
|
||||
robot["avatar_data"] = avatar_data
|
||||
|
||||
robots.append(robot)
|
||||
|
||||
return robots
|
||||
|
||||
def close(self):
|
||||
"""关闭MongoDB连接"""
|
||||
if self.client:
|
||||
self.client.close()
|
||||
@ -1,5 +1,8 @@
|
||||
// 页面加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 加载机器人角色列表
|
||||
loadRobotCharacters();
|
||||
|
||||
// 初始化图片上传表单
|
||||
initUploadImageForm();
|
||||
|
||||
@ -7,6 +10,28 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
initChatForm();
|
||||
});
|
||||
|
||||
// 加载机器人角色列表
|
||||
async function loadRobotCharacters() {
|
||||
const robotSelect = document.getElementById('robotSelect');
|
||||
|
||||
try {
|
||||
const response = await fetch('/imgsearcherApi/api/robots');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.robots && data.robots.length > 0) {
|
||||
// 添加机器人选项
|
||||
data.robots.forEach(robot => {
|
||||
const option = document.createElement('option');
|
||||
option.value = robot.robot_id;
|
||||
option.textContent = robot.name;
|
||||
robotSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载机器人角色失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图片上传表单
|
||||
function initUploadImageForm() {
|
||||
const form = document.getElementById('uploadImageForm');
|
||||
@ -74,14 +99,31 @@ function initUploadImageForm() {
|
||||
// 显示聊天界面
|
||||
chatContainer.classList.remove('d-none');
|
||||
|
||||
// 添加欢迎消息
|
||||
// 显示欢迎消息
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
chatMessages.innerHTML = '';
|
||||
|
||||
const welcomeMessage = document.createElement('div');
|
||||
welcomeMessage.className = 'message message-assistant';
|
||||
|
||||
let welcomeText = '你好!我是AI助手,可以帮你分析这张图片并回答问题。';
|
||||
let welcomeText = '';
|
||||
|
||||
// 如果选择了机器人角色,使用机器人的自我介绍
|
||||
if (data.robot) {
|
||||
welcomeText = `你好!我是${data.robot.name},`;
|
||||
// 存储机器人信息到页面数据中
|
||||
document.getElementById('chatContainer').dataset.robotName = data.robot.name;
|
||||
|
||||
// 显示机器人信息在界面上
|
||||
const robotInfoElement = document.createElement('div');
|
||||
robotInfoElement.className = 'alert alert-info mt-2 mb-3';
|
||||
robotInfoElement.innerHTML = `<strong>当前角色:${data.robot.name}</strong><br><small>${data.robot.background}</small>`;
|
||||
document.getElementById('chatContainer').prepend(robotInfoElement);
|
||||
} else {
|
||||
welcomeText = '你好!我是AI助手,';
|
||||
}
|
||||
|
||||
welcomeText += '可以帮你分析这张图片并回答问题。';
|
||||
if (data.image_type && data.description) {
|
||||
welcomeText += `我看到这是一张${data.image_type}的图片,${data.description}。你有什么想问的吗?`;
|
||||
} else {
|
||||
@ -171,7 +213,7 @@ function initChatForm() {
|
||||
}
|
||||
|
||||
// 添加消息到聊天界面
|
||||
function addMessage(role, content) {
|
||||
async function addMessage(role, content) {
|
||||
const chatMessages = document.getElementById('chatMessages');
|
||||
|
||||
const messageElement = document.createElement('div');
|
||||
@ -183,6 +225,44 @@ function addMessage(role, content) {
|
||||
`;
|
||||
|
||||
chatMessages.appendChild(messageElement);
|
||||
|
||||
// 如果是AI回复,调用TTS并播放
|
||||
if (role === 'assistant') {
|
||||
try {
|
||||
const ttsResp = await fetch('/imgsearcherApi/api/tts', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: content
|
||||
})
|
||||
});
|
||||
if (ttsResp.ok) {
|
||||
const blob = await ttsResp.blob();
|
||||
const audioUrl = URL.createObjectURL(blob);
|
||||
const audio = document.createElement('audio');
|
||||
audio.src = audioUrl;
|
||||
audio.autoplay = true;
|
||||
// 可选:提供播放控制按钮
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'audio-controls mt-1';
|
||||
const playBtn = document.createElement('button');
|
||||
playBtn.type = 'button';
|
||||
playBtn.className = 'btn btn-sm btn-outline-secondary';
|
||||
playBtn.textContent = '🔊 播放';
|
||||
playBtn.addEventListener('click', () => {
|
||||
audio.play();
|
||||
});
|
||||
controls.appendChild(playBtn);
|
||||
messageElement.appendChild(controls);
|
||||
} else {
|
||||
console.error('TTS 接口错误', await ttsResp.text());
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('TTS 播放失败', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
|
||||
297
app/static/js/robots.js
Normal file
297
app/static/js/robots.js
Normal file
@ -0,0 +1,297 @@
|
||||
// 页面加载完成后执行
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化机器人表单
|
||||
initRobotForm();
|
||||
|
||||
// 加载机器人列表
|
||||
loadRobots();
|
||||
|
||||
// 初始化编辑和删除功能
|
||||
initEditRobotModal();
|
||||
initDeleteRobotModal();
|
||||
});
|
||||
|
||||
// 初始化机器人表单
|
||||
function initRobotForm() {
|
||||
const form = document.getElementById('robotForm');
|
||||
const avatarInput = document.getElementById('robotAvatar');
|
||||
const avatarPreview = document.getElementById('avatarPreview');
|
||||
const avatarPreviewContainer = document.querySelector('#robotForm .avatar-preview');
|
||||
|
||||
// 头像预览
|
||||
avatarInput.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
avatarPreview.src = e.target.result;
|
||||
avatarPreviewContainer.classList.remove('d-none');
|
||||
};
|
||||
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
} else {
|
||||
avatarPreviewContainer.classList.add('d-none');
|
||||
}
|
||||
});
|
||||
|
||||
// 表单提交
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
// 显示提交中状态
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 添加中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/imgsearcherApi/api/robots', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
alert(`添加失败: ${data.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
form.reset();
|
||||
avatarPreviewContainer.classList.add('d-none');
|
||||
|
||||
// 刷新机器人列表
|
||||
loadRobots();
|
||||
|
||||
// 显示成功消息
|
||||
alert('机器人角色添加成功!');
|
||||
|
||||
} catch (error) {
|
||||
alert(`添加失败: ${error.message}`);
|
||||
} finally {
|
||||
// 恢复按钮状态
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 加载机器人列表
|
||||
async function loadRobots() {
|
||||
const robotsList = document.getElementById('robotsList');
|
||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||
const noRobotsMessage = document.getElementById('noRobotsMessage');
|
||||
|
||||
// 显示加载指示器
|
||||
loadingIndicator.classList.remove('d-none');
|
||||
noRobotsMessage.classList.add('d-none');
|
||||
|
||||
// 清除现有的机器人卡片(保留标题和加载指示器)
|
||||
const existingCards = robotsList.querySelectorAll('.robot-card-container');
|
||||
existingCards.forEach(card => card.remove());
|
||||
|
||||
try {
|
||||
const response = await fetch('/imgsearcherApi/api/robots');
|
||||
const data = await response.json();
|
||||
|
||||
// 隐藏加载指示器
|
||||
loadingIndicator.classList.add('d-none');
|
||||
|
||||
if (!data.robots || data.robots.length === 0) {
|
||||
noRobotsMessage.classList.remove('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// 渲染机器人卡片
|
||||
data.robots.forEach(robot => {
|
||||
const robotCard = createRobotCard(robot);
|
||||
robotsList.appendChild(robotCard);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载机器人列表失败:', error);
|
||||
loadingIndicator.classList.add('d-none');
|
||||
alert(`加载机器人列表失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建机器人卡片
|
||||
function createRobotCard(robot) {
|
||||
const cardContainer = document.createElement('div');
|
||||
cardContainer.className = 'col-md-4 mb-4 robot-card-container';
|
||||
|
||||
const avatarHtml = robot.avatar_data
|
||||
? `<img src="data:image/jpeg;base64,${robot.avatar_data}" alt="${robot.name}" class="robot-avatar">`
|
||||
: `<div class="robot-avatar-placeholder"><i class="bi bi-person"></i></div>`;
|
||||
|
||||
cardContainer.innerHTML = `
|
||||
<div class="card robot-card">
|
||||
<div class="card-body text-center">
|
||||
${avatarHtml}
|
||||
<h5 class="card-title">${robot.name}</h5>
|
||||
<div class="robot-background text-start mb-3">
|
||||
<small class="text-muted">${robot.background}</small>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary edit-robot" data-robot-id="${robot.robot_id}">
|
||||
<i class="bi bi-pencil"></i> 编辑
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger delete-robot" data-robot-id="${robot.robot_id}">
|
||||
<i class="bi bi-trash"></i> 删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 添加编辑按钮事件
|
||||
cardContainer.querySelector('.edit-robot').addEventListener('click', function() {
|
||||
openEditRobotModal(robot);
|
||||
});
|
||||
|
||||
// 添加删除按钮事件
|
||||
cardContainer.querySelector('.delete-robot').addEventListener('click', function() {
|
||||
openDeleteRobotModal(robot.robot_id, robot.name);
|
||||
});
|
||||
|
||||
return cardContainer;
|
||||
}
|
||||
|
||||
// 初始化编辑机器人模态框
|
||||
function initEditRobotModal() {
|
||||
const editRobotForm = document.getElementById('editRobotForm');
|
||||
const saveChangesButton = document.getElementById('saveRobotChanges');
|
||||
const editRobotAvatar = document.getElementById('editRobotAvatar');
|
||||
const editAvatarPreview = document.getElementById('editAvatarPreview');
|
||||
|
||||
// 头像预览
|
||||
editRobotAvatar.addEventListener('change', function() {
|
||||
if (this.files && this.files[0]) {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function(e) {
|
||||
editAvatarPreview.src = e.target.result;
|
||||
};
|
||||
|
||||
reader.readAsDataURL(this.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// 保存更改按钮点击事件
|
||||
saveChangesButton.addEventListener('click', async function() {
|
||||
const robotId = document.getElementById('editRobotId').value;
|
||||
const formData = new FormData(editRobotForm);
|
||||
|
||||
// 显示保存中状态
|
||||
const originalButtonText = saveChangesButton.innerHTML;
|
||||
saveChangesButton.disabled = true;
|
||||
saveChangesButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 保存中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/imgsearcherApi/api/robots/${robotId}`, {
|
||||
method: 'PUT',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
alert(`更新失败: ${data.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const editModal = bootstrap.Modal.getInstance(document.getElementById('editRobotModal'));
|
||||
editModal.hide();
|
||||
|
||||
// 刷新机器人列表
|
||||
loadRobots();
|
||||
|
||||
// 显示成功消息
|
||||
alert('机器人角色更新成功!');
|
||||
|
||||
} catch (error) {
|
||||
alert(`更新失败: ${error.message}`);
|
||||
} finally {
|
||||
// 恢复按钮状态
|
||||
saveChangesButton.disabled = false;
|
||||
saveChangesButton.innerHTML = originalButtonText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开编辑机器人模态框
|
||||
function openEditRobotModal(robot) {
|
||||
document.getElementById('editRobotId').value = robot.robot_id;
|
||||
document.getElementById('editRobotName').value = robot.name;
|
||||
document.getElementById('editRobotBackground').value = robot.background;
|
||||
|
||||
// 设置头像预览
|
||||
if (robot.avatar_data) {
|
||||
document.getElementById('editAvatarPreview').src = `data:image/jpeg;base64,${robot.avatar_data}`;
|
||||
} else {
|
||||
document.getElementById('editAvatarPreview').src = '';
|
||||
}
|
||||
|
||||
// 打开模态框
|
||||
const editModal = new bootstrap.Modal(document.getElementById('editRobotModal'));
|
||||
editModal.show();
|
||||
}
|
||||
|
||||
// 初始化删除机器人模态框
|
||||
function initDeleteRobotModal() {
|
||||
const confirmDeleteButton = document.getElementById('confirmDeleteRobot');
|
||||
|
||||
confirmDeleteButton.addEventListener('click', async function() {
|
||||
const robotId = document.getElementById('deleteRobotId').value;
|
||||
|
||||
// 显示删除中状态
|
||||
const originalButtonText = confirmDeleteButton.innerHTML;
|
||||
confirmDeleteButton.disabled = true;
|
||||
confirmDeleteButton.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 删除中...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/imgsearcherApi/api/robots/${robotId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
alert(`删除失败: ${data.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteRobotModal'));
|
||||
deleteModal.hide();
|
||||
|
||||
// 刷新机器人列表
|
||||
loadRobots();
|
||||
|
||||
// 显示成功消息
|
||||
alert('机器人角色删除成功!');
|
||||
|
||||
} catch (error) {
|
||||
alert(`删除失败: ${error.message}`);
|
||||
} finally {
|
||||
// 恢复按钮状态
|
||||
confirmDeleteButton.disabled = false;
|
||||
confirmDeleteButton.innerHTML = originalButtonText;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开删除机器人模态框
|
||||
function openDeleteRobotModal(robotId, robotName) {
|
||||
document.getElementById('deleteRobotId').value = robotId;
|
||||
document.getElementById('deleteRobotModalLabel').textContent = `确认删除 "${robotName}"`;
|
||||
document.querySelector('#deleteRobotModal .modal-body p').textContent = `确定要删除机器人角色 "${robotName}" 吗?此操作不可撤销。`;
|
||||
|
||||
// 打开模态框
|
||||
const deleteModal = new bootstrap.Modal(document.getElementById('deleteRobotModal'));
|
||||
deleteModal.show();
|
||||
}
|
||||
@ -25,12 +25,29 @@
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">上传图片</h5>
|
||||
<form id="uploadImageForm" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="chatImageFile" class="form-label">选择图片</label>
|
||||
<input class="form-control" type="file" id="chatImageFile" name="file" accept="image/*" required>
|
||||
<div class="form-text">支持JPG、PNG、JPEG、GIF格式</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="chatImageFile" class="form-label">选择图片</label>
|
||||
<input class="form-control" type="file" id="chatImageFile" name="file" accept="image/*" required>
|
||||
<div class="form-text">支持JPG、PNG、JPEG、GIF格式</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="robotSelect" class="form-label">选择机器人角色(可选)</label>
|
||||
<select class="form-select" id="robotSelect" name="robot_id">
|
||||
<option value="">默认AI助手</option>
|
||||
<!-- 机器人选项将在这里动态添加 -->
|
||||
</select>
|
||||
<div class="form-text">选择一个机器人角色进行对话</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="submit" class="btn btn-primary">上传并开始对话</button>
|
||||
<a href="/imgsearcherApi/robots" class="btn btn-outline-secondary">管理机器人角色</a>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">上传并开始对话</button>
|
||||
</form>
|
||||
<div id="uploadPreview" class="mt-3 text-center d-none">
|
||||
<div class="preview-container">
|
||||
|
||||
@ -29,6 +29,9 @@
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="/imgsearcherApi/chat-with-image" class="nav-link" style="color: #0d6efd; font-weight: bold;">图片智能对话</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a href="/imgsearcherApi/robots" class="nav-link" style="color: #198754; font-weight: bold;">机器人角色管理</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<!-- 待识别角色录入 -->
|
||||
@ -43,15 +46,17 @@
|
||||
<div class="form-text">支持JPG、PNG、BMP格式,最短边至少50px,最长边最大4096px</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="imageType" class="form-label">类型(这个实际就是宠物名,类似小狗pet、小猫pussy之类的)</label>
|
||||
<label for="imageType" class="form-label">角色名称</label>
|
||||
<input type="text" class="form-control" id="imageType" name="type" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="imageDescription" class="form-label">描述(目前demo只支持250个字符,后面正式版可以扩充,这个实际是角色卡信息,也就是宠物的描述,类似小狗是金毛之类的)</label>
|
||||
<label for="imageDescription" class="form-label">描述(角色卡信息)</label>
|
||||
<textarea class="form-control" id="imageDescription" name="description" rows="2" required></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="imageName" class="form-label">名称(这个实际是图片名称,直接和宠物名重名即可,我会给加个名字)</label>
|
||||
<label for="imageName" class="form-label">图片名称
|
||||
|
||||
</label>
|
||||
<input type="text" class="form-control" id="imageName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
||||
186
app/templates/robots.html
Normal file
186
app/templates/robots.html
Normal file
@ -0,0 +1,186 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>机器人角色管理 - 地瓜机器人</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.8.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
<style>
|
||||
.robot-card {
|
||||
height: 100%;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
.robot-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
.robot-avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.robot-avatar-placeholder {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
background-color: #e9ecef;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
font-size: 2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
.robot-background {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#robotForm .avatar-preview {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#robotForm .avatar-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="text-center my-4">
|
||||
<h1>机器人角色管理</h1>
|
||||
<p class="lead">创建和管理您的机器人角色</p>
|
||||
<nav class="mt-3">
|
||||
<a href="/imgsearcherApi" class="btn btn-outline-secondary me-2">返回首页</a>
|
||||
<a href="/imgsearcherApi/chat-with-image" class="btn btn-outline-primary">图片智能对话</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">添加新机器人角色</h5>
|
||||
<form id="robotForm" enctype="multipart/form-data">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="robotName" class="form-label">机器人名称</label>
|
||||
<input type="text" class="form-control" id="robotName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="robotAvatar" class="form-label">头像(可选)</label>
|
||||
<input class="form-control" type="file" id="robotAvatar" name="avatar" accept="image/*">
|
||||
<div class="avatar-preview d-none mt-2">
|
||||
<img id="avatarPreview" src="" alt="头像预览">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label for="robotBackground" class="form-label">背景故事</label>
|
||||
<textarea class="form-control" id="robotBackground" name="background" rows="5" required></textarea>
|
||||
<div class="form-text">描述机器人的性格、背景和特点,这些信息将用于图片对话中</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> 添加机器人
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" id="robotsList">
|
||||
<div class="col-12 mb-4">
|
||||
<h3>现有机器人角色</h3>
|
||||
<div class="text-center py-5 d-none" id="loadingIndicator">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<p class="mt-2">加载机器人角色...</p>
|
||||
</div>
|
||||
<div class="alert alert-info d-none" id="noRobotsMessage">
|
||||
暂无机器人角色,请添加一个新的机器人角色。
|
||||
</div>
|
||||
</div>
|
||||
<!-- 机器人卡片将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑机器人模态框 -->
|
||||
<div class="modal fade" id="editRobotModal" tabindex="-1" aria-labelledby="editRobotModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editRobotModalLabel">编辑机器人角色</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="editRobotForm" enctype="multipart/form-data">
|
||||
<input type="hidden" id="editRobotId">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="mb-3">
|
||||
<label for="editRobotName" class="form-label">机器人名称</label>
|
||||
<input type="text" class="form-control" id="editRobotName" name="name" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="editRobotAvatar" class="form-label">头像(可选)</label>
|
||||
<input class="form-control" type="file" id="editRobotAvatar" name="avatar" accept="image/*">
|
||||
<div class="avatar-preview mt-2">
|
||||
<img id="editAvatarPreview" src="" alt="头像预览">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<div class="mb-3">
|
||||
<label for="editRobotBackground" class="form-label">背景故事</label>
|
||||
<textarea class="form-control" id="editRobotBackground" name="background" rows="5" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" id="saveRobotChanges">保存更改</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 确认删除模态框 -->
|
||||
<div class="modal fade" id="deleteRobotModal" tabindex="-1" aria-labelledby="deleteRobotModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteRobotModalLabel">确认删除</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>确定要删除这个机器人角色吗?此操作不可撤销。</p>
|
||||
<input type="hidden" id="deleteRobotId">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteRobot">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/robots.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user