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

165 lines
6.1 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const needle = require('needle')
const moment = require('moment')
const {Class} = require("./Class");
const {RequestType} = require('./RequestType')
const {SignUtil} = require('./SignUtil')
const {BaseRequest} = require('../request/BaseRequest')
const HEADERS = {'Accept-Encoding': 'identity'}
const getHeaders = function (headers) {
if (!headers) {
return HEADERS
}
for (const key in HEADERS) {
headers[key] = HEADERS[key];
}
return headers
}
const OpenClient = Class.create({
/**
* 初始化客户端
* @param appId 应用ID
* @param privateKey 应用私钥,2048位,PKCS8
* @param url 请求url
*/
init: function (appId, privateKey, url) {
this.appId = appId;
this.privateKey = privateKey;
this.url = url;
},
/**
* 发送请求
* @param request 请求类
* @param callback 回调函数,参数json
*/
execute: function (request, callback) {
this.executeToken(request,null, null, callback)
},
executeUserToken: function (request, userToken, callback) {
this.executeToken(request, userToken, null, callback)
},
executeAppToken: function (request, appToken, callback) {
this.executeToken(request, null, appToken, callback)
},
/**
* 发送请求
* @param request 请求类
* @param userToken 用户token
* @param appToken 应用token
* @param callback 回调函数,参数json
*/
executeToken: function (request, userToken, appToken, callback) {
if (!(request instanceof BaseRequest)) {
throw 'request类未继承BaseRequest'
}
const requestType = request.getRequestType();
if (request.files && request.files.length > 0) {
this._postFile(request, userToken, appToken, callback);
} else {
switch (requestType) {
case RequestType.GET:
this._get(request, userToken, appToken, callback);
break
case RequestType.POST_FORM:
this._form(request, 'POST', userToken, appToken, callback);
break
case RequestType.POST_JSON:
this._json(request, 'POST', userToken, appToken, callback);
break
case RequestType.POST_FILE:
this._postFile(request, userToken, appToken, callback);
break
case RequestType.PUT:
this._json(request, 'PUT', userToken, appToken, callback);
break
case RequestType.DELETE:
this._json(request, 'DELETE', userToken, appToken, callback);
break
default :{
throw 'request.getRequestType()类型不正确'
}
}
}
},
_get: function(request, userToken, appToken, callback) {
const allParams = this._buildParams(request, userToken, appToken)
const that = this
// needle.request(method, url, data[, options][, callback])
needle.request('GET',this.url, allParams, {
headers: getHeaders()
}, function(error, response) {
callback(that._parseResponse(error, response, request))
});
},
_form: function (request, method, userToken, appToken, callback) {
const allParams = this._buildParams(request, userToken, appToken)
const that = this
needle.request(method, this.url, allParams, {
headers: getHeaders({
'Content-Type': 'application/x-www-form-urlencoded'
})
}, function(error, response) {
callback(that._parseResponse(error, response, request))
});
},
_json: function (request, method, userToken, appToken, callback) {
const allParams = this._buildParams(request, userToken, appToken)
const that = this
needle.request(method, this.url, allParams, {
headers: getHeaders(), json: true
}, function(error, response) {
callback(that._parseResponse(error, response, request))
});
},
_postFile: function (request, userToken, appToken, callback) {
const allParams = this._buildParams(request, userToken, appToken)
const files = request.files;
files.forEach(row => {
// 设置成{ file: row.path, content_type: 'application/octet-stream' }格式
// needle会认为是上传文件
allParams[row.name] = { file: row.path, content_type: 'application/octet-stream' }
})
const that = this
needle.request('POST',this.url, allParams, {
headers: getHeaders(), multipart: true
}, function(error, response) {
callback(that._parseResponse(error, response, request))
});
},
_parseResponse: function (error, response, request) {
if (!error) {
return request.parseResponse(response.body)
} else {
throw '请求异常:' + error
}
},
_buildParams: function (request, userToken, appToken) {
const allParams = {
'app_id': this.appId,
'method': request.getMethod(),
'charset': 'UTF-8',
'sign_type': 'RSA2',
'timestamp': moment().valueOf(),
'version': request.getVersion(),
'sdk_version': request.getSdkVersion()
}
allParams['biz_content'] = JSON.stringify(request.bizModel)
allParams['params_content'] = JSON.stringify(request.paramsModel)
if (userToken) {
allParams['user_access_token'] = userToken
}
if (appToken) {
allParams['app_auth_token'] = appToken
}
// 创建签名
const sign = SignUtil.createSign(allParams, this.privateKey, 'RSA2')
allParams.sign = sign
return allParams;
}
})
module.exports = OpenClient