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

198 lines
6.7 KiB
Python
Executable File

# coding:utf-8
import json
import copy
import time
from volcengine.ApiInfo import ApiInfo
from volcengine.Credentials import Credentials
from volcengine.base.Service import Service
from volcengine.ServiceInfo import ServiceInfo
from volcengine.auth.SignerV4 import SignerV4
from volcengine.maas.sse_decoder import SSEDecoder
from volcengine.maas.exception import MaasException, new_client_sdk_request_error
from volcengine.maas.utils import dict_to_object, json_to_object
from .utils import gen_req_id
from volcengine.maas.v2.audio.audio import Audio
from volcengine.maas.v2.images.images import Images
class MaasService(Service):
def __init__(self, host, region, connection_timeout=60, socket_timeout=60):
service_info = MaasService.get_service_info(
host, region, connection_timeout, socket_timeout
)
api_info = MaasService.get_api_info()
self._setted_apikey = None
api_info = self.get_api_info()
super(MaasService, self).__init__(service_info, api_info)
self.audio = Audio(self)
self.images = Images(self)
def set_apikey(self, apikey):
self._setted_apikey = apikey
@staticmethod
def get_service_info(host, region, connection_timeout, socket_timeout):
service_info = ServiceInfo(
host,
{"Accept": "application/json"},
Credentials("", "", "ml_maas", region),
connection_timeout,
socket_timeout,
"https",
)
return service_info
@staticmethod
def get_api_info():
api_info = {
"chat": ApiInfo("POST", "/api/v2/endpoint/{endpoint_id}/chat", {}, {}, {}),
"tokenization": ApiInfo(
"POST", "/api/v2/endpoint/{endpoint_id}/tokenization", {}, {}, {}
),
"classification": ApiInfo(
"POST", "/api/v2/endpoint/{endpoint_id}/classification", {}, {}, {}
),
"embeddings": ApiInfo(
"POST", "/api/v2/endpoint/{endpoint_id}/embeddings", {}, {}, {}
),
"audio.speech": ApiInfo(
"POST", "/api/v2/endpoint/{endpoint_id}/audio/speech", {}, {}, {}
),
"images.quick_gen": ApiInfo(
"POST", "/api/v2/endpoint/{endpoint_id}/images/quick-gen", {}, {}, {}
),
"images.flex_gen": ApiInfo(
"POST", "/api/v2/endpoint/{endpoint_id}/images/flex-gen", {}, {}, {}
),
}
return api_info
def chat(self, endpoint_id, req):
req["stream"] = False
return self._request(endpoint_id, "chat", req)
def stream_chat(self, endpoint_id, req):
req_id = gen_req_id()
self._validate("chat", req_id)
apikey = self._setted_apikey
try:
req["stream"] = True
res = self._call(
endpoint_id, "chat", req_id, {}, json.dumps(req).encode("utf-8"), apikey, stream=True
)
decoder = SSEDecoder(res)
def iter():
for data in decoder.next():
if data == b"[DONE]":
return
try:
res = json_to_object(str(data, encoding="utf-8"), req_id=req_id)
except:
raise
else:
if res.error is not None and res.error.code_n != 0:
raise MaasException(
res.error.code_n,
res.error.code,
res.error.message,
req_id,
)
yield res
return iter()
except MaasException:
raise
except Exception as e:
raise new_client_sdk_request_error(str(e))
def tokenize(self, endpoint_id, req):
return self._request(endpoint_id, "tokenization", req)
def classification(self, endpoint_id, req):
return self._request(endpoint_id, "classification", req)
def embeddings(self, endpoint_id, req):
return self._request(endpoint_id, "embeddings", req)
def _request(self, endpoint_id, api, req, params={}):
req_id = gen_req_id()
self._validate(api, req_id)
apikey = self._setted_apikey
try:
res = self._call(endpoint_id, api, req_id, params, json.dumps(req).encode("utf-8"), apikey)
resp = dict_to_object(res.json())
if resp and isinstance(resp, dict):
resp["req_id"] = req_id
return resp
except MaasException as e:
raise e
except Exception as e:
raise new_client_sdk_request_error(str(e), req_id)
def _validate(self, api, req_id):
credentials_exist = (
self.service_info.credentials is not None and
self.service_info.credentials.sk is not None and
self.service_info.credentials.ak is not None
)
if not self._setted_apikey and not credentials_exist:
raise new_client_sdk_request_error("no valid credential", req_id)
if not (api in self.api_info):
raise new_client_sdk_request_error("no such api", req_id)
def _call(self, endpoint_id, api, req_id, params, body, apikey=None, stream=False):
api_info = copy.deepcopy(self.api_info[api])
api_info.path = api_info.path.format(endpoint_id=endpoint_id)
r = self.prepare_request(api_info, params)
r.headers["x-tt-logid"] = req_id
r.headers["Content-Type"] = "application/json"
r.body = body
if apikey is None:
SignerV4.sign(r, self.service_info.credentials)
elif apikey is not None:
r.headers["Authorization"] = "Bearer " + apikey
url = r.build()
res = self.session.post(
url,
headers=r.headers,
data=r.body,
timeout=(
self.service_info.connection_timeout,
self.service_info.socket_timeout,
),
stream=stream,
)
if res.status_code != 200:
raw = res.text.encode()
res.close()
try:
resp = json_to_object(str(raw, encoding="utf-8"), req_id=req_id)
except Exception:
raise new_client_sdk_request_error(raw, req_id)
else:
if resp.error:
raise MaasException(
resp.error.code_n, resp.error.code, resp.error.message, req_id
)
else:
raise new_client_sdk_request_error(resp, req_id)
return res