chenpei d0dc94e36d feat: 将弹幕功能替换为AI聊天助手
新功能:
- 创建AI聊天助手组件(AIChatWidget)
- 支持文字+图片多模态对话
- 集成Gemini AI API
- 支持图片编辑和内容生成
- 实时对话历史管理
- 优雅的UI设计,支持黑夜模式

技术实现:
- 后端API代理Gemini API(/api/ai-chat)
- 支持最多5张图片上传
- 流式响应处理
- 对话历史管理
- Base64图片处理

配置:
- 在.env中添加Gemini API配置
- GEMINI_API_KEY
- GEMINI_API_BASE_URL
- GEMINI_MODEL

替换原弹幕功能,提供更强大的AI助手体验!
2025-10-27 17:45:37 +08:00

177 lines
6.6 KiB
Python
Executable File

# coding:utf-8
import json
import os
import threading
from volcengine.const.Const import *
from volcengine.util.Util import *
from volcengine.Policy import *
from volcengine.imagex.v2.const import *
from retry import retry
from urllib.parse import quote
try:
import queue
except ImportError:
import Queue as queue
class Uploader:
def __init__(
self, imagex_service, host, store_infos=None, file_paths_or_bytes=None
):
if store_infos is None:
store_infos = []
if file_paths_or_bytes is None:
file_paths_or_bytes = []
self.imagex_service = imagex_service
self.host = host
self.store_infos = store_infos
self.file_paths_or_bytes = file_paths_or_bytes
self.queue = queue.Queue()
self.queue_lock = threading.Lock()
self.successOids = []
self.results = []
for i in range(len(store_infos)):
self.queue.put(i)
def async_upload(self):
while not self.queue.empty():
self.queue_lock.acquire()
if self.queue.empty():
self.queue_lock.release()
break
idx = self.queue.get()
self.queue_lock.release()
store_info = self.store_infos[idx]
file_paths_or_bytes = self.file_paths_or_bytes[idx]
try:
if isinstance(file_paths_or_bytes, bytes):
self.upload_by_host(store_info, file_paths_or_bytes)
elif isinstance(file_paths_or_bytes, str):
file_path = file_paths_or_bytes
file_size = os.path.getsize(file_path)
data = open(file_path, "rb")
if file_size < MinChunkSize:
self.upload_by_host(store_info, data.read())
elif file_size > LargeFileSize:
self.chunk_upload(store_info, data, file_size, True)
else:
self.chunk_upload(store_info, data, file_size, False)
data.close()
else:
raise Exception("Uploader only accept bytes or path data")
self.successOids.append(store_info["StoreUri"])
self.results.append(
{
"Uri": store_info["StoreUri"],
"UriStatus": 2000,
}
)
except Exception as e:
self.results.append(
{
"Uri": store_info["StoreUri"],
"UriStatus": 2001,
"Error": e,
}
)
@retry(tries=3, delay=1, backoff=2)
def upload_by_host(self, store_info, img_data):
url = "https://{}/{}".format(self.host, quote(store_info["StoreUri"]))
check_sum = crc32(img_data) & 0xFFFFFFFF
check_sum = "%08x" % check_sum
headers = {"Content-CRC32": check_sum, "Authorization": store_info["Auth"]}
upload_status, resp = self.imagex_service.put_data(url, img_data, headers)
if not upload_status:
raise Exception(
"upload by host: upload url %s status false, resp: %s" % (url, resp)
)
def chunk_upload(self, store_info, f, size, is_large_file):
upload_id = self.init_upload_part(store_info, is_large_file)
n = size // MinChunkSize
last_num = n - 1
parts = []
for i in range(0, last_num):
data = f.read(MinChunkSize)
part_number = i
if is_large_file:
part_number = i + 1
part = self.upload_part(
store_info, upload_id, part_number, data, is_large_file
)
parts.append(part)
data = f.read()
if is_large_file:
last_num = last_num + 1
part = self.upload_part(store_info, upload_id, last_num, data, is_large_file)
parts.append(part)
return self.upload_merge_part(store_info, upload_id, parts, is_large_file)
@retry(tries=3, delay=1, backoff=2)
def init_upload_part(self, store_info, is_large_file):
url = "https://{}/{}?uploads".format(self.host, quote(store_info["StoreUri"]))
headers = {"Authorization": store_info["Auth"]}
if is_large_file:
headers["X-Storage-Mode"] = "gateway"
upload_status, resp = self.imagex_service.put_data(url, None, headers)
resp = json.loads(resp)
if not upload_status:
raise Exception("init upload error")
if resp.get("success") is None or resp["success"] != 0:
raise Exception("init upload error")
return resp["payload"]["uploadID"]
@retry(tries=3, delay=1, backoff=2)
def upload_part(self, store_info, upload_id, part_number, data, is_large_file):
url = "https://{}/{}?partNumber={}&uploadID={}".format(
self.host, quote(store_info["StoreUri"]), part_number, upload_id
)
check_sum = crc32(data) & 0xFFFFFFFF
check_sum = "%08x" % check_sum
headers = {"Content-CRC32": check_sum, "Authorization": store_info["Auth"]}
if is_large_file:
headers["X-Storage-Mode"] = "gateway"
upload_status, resp = self.imagex_service.put_data(url, data, headers)
if not upload_status:
raise Exception(url + json.dumps(resp))
resp = json.loads(resp)
if resp.get("success") is None or resp["success"] != 0:
raise Exception("upload part error")
return check_sum
# noinspection DuplicatedCode
@staticmethod
def generate_merge_body(check_sum_list):
if len(check_sum_list) == 0:
raise Exception("crc32 list empty")
s = []
for i in range(len(check_sum_list)):
s.append("{}:{}".format(i, check_sum_list[i]))
comma = ","
return comma.join(s)
@retry(tries=3, delay=1, backoff=2)
def upload_merge_part(self, store_info, upload_id, check_sum_list, is_large_file):
url = "https://{}/{}?uploadID={}".format(
self.host, quote(store_info["StoreUri"]), upload_id
)
data = self.generate_merge_body(check_sum_list)
headers = {"Authorization": store_info["Auth"]}
if is_large_file:
headers["X-Storage-Mode"] = "gateway"
upload_status, resp = self.imagex_service.put_data(url, data, headers)
resp = json.loads(resp)
if not upload_status:
raise Exception("init upload error")
if resp.get("success") is None or resp["success"] != 0:
raise Exception("init upload error")