mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-18 06:17:06 +00:00
Merge branch 'v2' of https://github.com/HalcyonAlcedo/chatgpt-plugin; branch 'v2' of https://github.com/ikechan8370/chatgpt-plugin into v2
This commit is contained in:
commit
ef1423ddb4
19 changed files with 417 additions and 126 deletions
20
README.md
20
README.md
|
|
@ -1,5 +1,5 @@
|
||||||

|

|
||||||
<div align=center> <h1>云崽QQ机器人的ChatGPT插件</h1> </div>
|
<div align=center> <h1>云崽系机器人的智能聊天插件</h1> </div>
|
||||||
<div align=center>
|
<div align=center>
|
||||||
|
|
||||||
<img src ="https://img.shields.io/github/issues/ikechan8370/chatgpt-plugin?logo=github"/>
|
<img src ="https://img.shields.io/github/issues/ikechan8370/chatgpt-plugin?logo=github"/>
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
### 推荐的相关文档和参考资料
|
### 推荐的相关文档和参考资料
|
||||||
本README
|
本README
|
||||||
[手册](https://chatgptplugin.ikechan8370.com/)
|
[手册](https://yunzai.chat)
|
||||||
[文档1(建设中)](https://chatgpt-docs.err0r.top/)
|
[文档1(建设中)](https://chatgpt-docs.err0r.top/)
|
||||||
[插件常见问题(鹤望兰版)](https://chatgptplugin.ikechan8370.com/guide/)
|
[插件常见问题(鹤望兰版)](https://chatgptplugin.ikechan8370.com/guide/)
|
||||||
[Yunzai常见问题(LUCK小运版)](https://www.wolai.com/oA43vuW71aBnv7UsEysn4T)
|
[Yunzai常见问题(LUCK小运版)](https://www.wolai.com/oA43vuW71aBnv7UsEysn4T)
|
||||||
|
|
@ -52,16 +52,6 @@ Node.js >= 18 / Node.js >= 14(with node-fetch)
|
||||||
## 安装与使用方法
|
## 安装与使用方法
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
在安装之前,请先判断自己需要使用哪种模式,本插件支持官方API/第三方API/~~浏览器~~/必应四种模式。也可以选择**我全都要**(通过qq发送命令`#chatgpt切换浏览器/API/API3/Bing`实时切换)
|
|
||||||
|
|
||||||
> #### API模式和浏览器模式如何选择?
|
|
||||||
>
|
|
||||||
> * API模式会调用OpenAI官方提供的gpt-3.5-turbo API,ChatGPT官网同款模型,只需要提供API Key。一般情况下,该种方式响应速度更快,可配置项多,且不会像chatGPT官网一样总出现不可用的现象,但注意API调用是收费的,新用户有18美元试用金可用于支付,价格为`$0.0020/ 1K tokens`。(问题和回答**加起来**算token)
|
|
||||||
> * API3模式会调用第三方提供的官网反代API,他会帮你绕过CF防护,需要提供ChatGPT的Token。效果与官网和浏览器一致,但稳定性不一定。发送#chatgpt设置token来设置token。
|
|
||||||
> * (Deprecated)浏览器模式通过在本地启动Chrome等浏览器模拟用户访问ChatGPT网站,使得获得和官方以及API2模式一模一样的回复质量,同时保证安全性。缺点是本方法对环境要求较高,需要提供桌面环境和一个可用的代理(能够访问ChatGPT的IP地址),且响应速度不如API,而且高峰期容易无法使用。一般作为API3的下位替代。
|
|
||||||
> * 必应(Bing)将调用微软新必应接口进行对话。需要在必应网页能够正常使用新必应且设置有效的Bing登录Cookie方可使用。强烈推荐
|
|
||||||
|
|
||||||
1. 进入 Yunzai根目录
|
1. 进入 Yunzai根目录
|
||||||
|
|
||||||
2. 请将 chatgpt-plugin 放置在 Yunzai-Bot 的 plugins 目录下
|
2. 请将 chatgpt-plugin 放置在 Yunzai-Bot 的 plugins 目录下
|
||||||
|
|
@ -83,12 +73,8 @@ pnpm i
|
||||||
|
|
||||||
如果是手工下载的 zip 压缩包,请将解压后的 chatgpt-plugin 文件夹(请删除压缩自带的-master或版本号后缀)放置在 Yunzai-Bot 目录下的 plugins 文件夹内
|
如果是手工下载的 zip 压缩包,请将解压后的 chatgpt-plugin 文件夹(请删除压缩自带的-master或版本号后缀)放置在 Yunzai-Bot 目录下的 plugins 文件夹内
|
||||||
|
|
||||||
> ~~浏览器模式仅为备选,如您需要使用浏览器模式,您还需要有**桌面环境**,优先级建议:API≈必应>API3>浏览器~~\
|
|
||||||
> ~~2.20更新:必应被大削,变得蠢了,建议还是API/API3优先~~\
|
|
||||||
> 4.2更新:必应站起来了,必应天下第一。建议都用必应,别用API/API3了。浏览器模式除非极其特殊的需求否则强烈建议不使用,已经不维护了。
|
|
||||||
|
|
||||||
3. 修改配置
|
3. 修改配置
|
||||||
**本插件配置项比较多,强烈建议使用后台面板或[锅巴面板](https://github.com/guoba-yunzai/Guoba-Plugin)修改**
|
**本插件配置项比较多,强烈建议使用后台工具箱或[锅巴面板](https://github.com/guoba-yunzai/Guoba-Plugin)修改**
|
||||||
|
|
||||||
或者创建和编辑config/config.json文件。
|
或者创建和编辑config/config.json文件。
|
||||||
|
|
||||||
|
|
|
||||||
69
apps/chat.js
69
apps/chat.js
|
|
@ -77,6 +77,7 @@ import {solveCaptchaOneShot} from '../utils/bingCaptcha.js'
|
||||||
import {ClaudeAIClient} from '../utils/claude.ai/index.js'
|
import {ClaudeAIClient} from '../utils/claude.ai/index.js'
|
||||||
import {getProxy} from '../utils/proxy.js'
|
import {getProxy} from '../utils/proxy.js'
|
||||||
import {QwenApi} from '../utils/alibaba/qwen-api.js'
|
import {QwenApi} from '../utils/alibaba/qwen-api.js'
|
||||||
|
import {getChatHistoryGroup} from '../utils/chat.js'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await import('@azure/openai')
|
await import('@azure/openai')
|
||||||
|
|
@ -565,6 +566,18 @@ export class chatgpt extends plugin {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
case 'qwen': {
|
||||||
|
let qcs = await redis.keys('CHATGPT:CONVERSATIONS_QWEN:*')
|
||||||
|
for (let i = 0; i < qcs.length; i++) {
|
||||||
|
await redis.del(qcs[i])
|
||||||
|
// todo clean last message id
|
||||||
|
if (Config.debug) {
|
||||||
|
logger.info('delete qwen conversation bind: ' + qcs[i])
|
||||||
|
}
|
||||||
|
deleted++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await this.reply(`结束了${deleted}个用户的对话。`, true)
|
await this.reply(`结束了${deleted}个用户的对话。`, true)
|
||||||
}
|
}
|
||||||
|
|
@ -1090,9 +1103,7 @@ export class chatgpt extends plugin {
|
||||||
logger.mark({ conversation })
|
logger.mark({ conversation })
|
||||||
}
|
}
|
||||||
let chatMessage = await this.sendMessage(prompt, conversation, use, e)
|
let chatMessage = await this.sendMessage(prompt, conversation, use, e)
|
||||||
if (chatMessage.image) {
|
if (chatMessage?.noMsg) {
|
||||||
this.setContext('solveBingCaptcha', false, 60)
|
|
||||||
await e.reply([chatMessage.text, segment.image(`base64://${chatMessage.image}`)])
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// 处理星火和bard图片
|
// 处理星火和bard图片
|
||||||
|
|
@ -1650,28 +1661,7 @@ export class chatgpt extends plugin {
|
||||||
if (master && !e.group) {
|
if (master && !e.group) {
|
||||||
opt.masterName = e.bot.getFriendList().get(parseInt(master))?.nickname
|
opt.masterName = e.bot.getFriendList().get(parseInt(master))?.nickname
|
||||||
}
|
}
|
||||||
let latestChats = await e.group.getChatHistory(0, 1)
|
opt.chats = await getChatHistoryGroup(e, Config.groupContextLength)
|
||||||
if (latestChats.length > 0) {
|
|
||||||
let latestChat = latestChats[0]
|
|
||||||
if (latestChat) {
|
|
||||||
let seq = latestChat.seq
|
|
||||||
let chats = []
|
|
||||||
while (chats.length < Config.groupContextLength) {
|
|
||||||
let chatHistory = await e.group.getChatHistory(seq, 20)
|
|
||||||
chats.push(...chatHistory)
|
|
||||||
}
|
|
||||||
chats = chats.slice(0, Config.groupContextLength)
|
|
||||||
let mm = await e.group.getMemberMap()
|
|
||||||
chats.forEach(chat => {
|
|
||||||
let sender = mm.get(chat.sender.user_id)
|
|
||||||
if (sender) {
|
|
||||||
chat.sender = sender
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// console.log(chats)
|
|
||||||
opt.chats = chats
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn('获取群聊聊天记录失败,本次对话不携带聊天记录', err)
|
logger.warn('获取群聊聊天记录失败,本次对话不携带聊天记录', err)
|
||||||
}
|
}
|
||||||
|
|
@ -1778,7 +1768,7 @@ export class chatgpt extends plugin {
|
||||||
const { maxConv } = error
|
const { maxConv } = error
|
||||||
if (message && typeof message === 'string' && message.indexOf('CaptchaChallenge') > -1) {
|
if (message && typeof message === 'string' && message.indexOf('CaptchaChallenge') > -1) {
|
||||||
if (bingToken) {
|
if (bingToken) {
|
||||||
if (maxConv > 20) {
|
if (maxConv >= 20) {
|
||||||
// maxConv为30说明token有效,可以通过解验证码码服务过码
|
// maxConv为30说明token有效,可以通过解验证码码服务过码
|
||||||
await e.reply('出现必应验证码,尝试解决中')
|
await e.reply('出现必应验证码,尝试解决中')
|
||||||
try {
|
try {
|
||||||
|
|
@ -1857,10 +1847,10 @@ export class chatgpt extends plugin {
|
||||||
text: errorMessage,
|
text: errorMessage,
|
||||||
error: true
|
error: true
|
||||||
}
|
}
|
||||||
} else {
|
} else if (response?.response) {
|
||||||
return {
|
return {
|
||||||
text: response?.response,
|
text: response?.response,
|
||||||
quote: response.quote,
|
quote: response?.quote,
|
||||||
suggestedResponses: response.suggestedResponses,
|
suggestedResponses: response.suggestedResponses,
|
||||||
conversationId: response.conversationId,
|
conversationId: response.conversationId,
|
||||||
clientId: response.clientId,
|
clientId: response.clientId,
|
||||||
|
|
@ -1869,6 +1859,11 @@ export class chatgpt extends plugin {
|
||||||
parentMessageId: response.apology ? conversation.parentMessageId : response.messageId,
|
parentMessageId: response.apology ? conversation.parentMessageId : response.messageId,
|
||||||
bingToken
|
bingToken
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug('no message')
|
||||||
|
return {
|
||||||
|
noMsg: true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'api3': {
|
case 'api3': {
|
||||||
|
|
@ -2147,7 +2142,7 @@ export class chatgpt extends plugin {
|
||||||
opt.groupId = e.group_id
|
opt.groupId = e.group_id
|
||||||
opt.qq = e.sender.user_id
|
opt.qq = e.sender.user_id
|
||||||
opt.nickname = e.sender.card
|
opt.nickname = e.sender.card
|
||||||
opt.groupName = e.group.name
|
opt.groupName = e.group.name || e.group_name
|
||||||
opt.botName = e.isGroup ? (e.group.pickMember(getUin(e)).card || e.group.pickMember(getUin(e)).nickname) : e.bot.nickname
|
opt.botName = e.isGroup ? (e.group.pickMember(getUin(e)).card || e.group.pickMember(getUin(e)).nickname) : e.bot.nickname
|
||||||
let master = (await getMasterQQ())[0]
|
let master = (await getMasterQQ())[0]
|
||||||
if (master && e.group) {
|
if (master && e.group) {
|
||||||
|
|
@ -2156,21 +2151,7 @@ export class chatgpt extends plugin {
|
||||||
if (master && !e.group) {
|
if (master && !e.group) {
|
||||||
opt.masterName = e.bot.getFriendList().get(parseInt(master))?.nickname
|
opt.masterName = e.bot.getFriendList().get(parseInt(master))?.nickname
|
||||||
}
|
}
|
||||||
let latestChat = await e.group.getChatHistory(0, 1)
|
let chats = await getChatHistoryGroup(e, Config.groupContextLength)
|
||||||
let seq = latestChat[0].seq
|
|
||||||
let chats = []
|
|
||||||
while (chats.length < Config.groupContextLength) {
|
|
||||||
let chatHistory = await e.group.getChatHistory(seq, 20)
|
|
||||||
chats.push(...chatHistory.reverse())
|
|
||||||
}
|
|
||||||
chats = chats.slice(0, Config.groupContextLength)
|
|
||||||
// 太多可能会干扰AI对自身qq号和用户qq的判断,感觉gpt3.5也处理不了那么多信息
|
|
||||||
chats = chats > 50 ? 50 : chats
|
|
||||||
let mm = await e.group.getMemberMap()
|
|
||||||
chats.forEach(chat => {
|
|
||||||
let sender = mm.get(chat.sender.user_id)
|
|
||||||
chat.sender = sender
|
|
||||||
})
|
|
||||||
opt.chats = chats
|
opt.chats = chats
|
||||||
const namePlaceholder = '[name]'
|
const namePlaceholder = '[name]'
|
||||||
const defaultBotName = 'ChatGPT'
|
const defaultBotName = 'ChatGPT'
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,10 @@ export class Entertainment extends plugin {
|
||||||
reg: '^#(|最新)词云(\\d{1,2}h{0,1}|)$',
|
reg: '^#(|最新)词云(\\d{1,2}h{0,1}|)$',
|
||||||
fnc: 'wordcloud_latest'
|
fnc: 'wordcloud_latest'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
reg: '^#(我的)?(本月|本周|今日)?词云$',
|
||||||
|
fnc: 'wordcloud_new'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
reg: '^#((寄批踢|gpt|GPT)?翻.*|chatgpt翻译帮助)',
|
reg: '^#((寄批踢|gpt|GPT)?翻.*|chatgpt翻译帮助)',
|
||||||
fnc: 'translate'
|
fnc: 'translate'
|
||||||
|
|
@ -215,10 +219,10 @@ ${translateLangLabels}
|
||||||
|
|
||||||
const regExp = /词云(\d{0,2})(|h)/
|
const regExp = /词云(\d{0,2})(|h)/
|
||||||
const match = e.msg.trim().match(regExp)
|
const match = e.msg.trim().match(regExp)
|
||||||
const duration = !match[1] ? 12 : parseInt(match[1]) // default 12h
|
const duration = !match[1] ? 12 : parseInt(match[1]) // default 12h
|
||||||
|
|
||||||
if (duration > 24) {
|
if (duration > 24) {
|
||||||
await e.reply('最多只能统计24小时内的记录哦')
|
await e.reply('最多只能统计24小时内的记录哦,你可以使用#本周词云和#本月词云获取更长时间的统计~')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
await e.reply('在统计啦,请稍等...')
|
await e.reply('在统计啦,请稍等...')
|
||||||
|
|
@ -236,6 +240,56 @@ ${translateLangLabels}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async wordcloud_new (e) {
|
||||||
|
if (e.isGroup) {
|
||||||
|
let groupId = e.group_id
|
||||||
|
let userId
|
||||||
|
if (e.msg.includes('我的')) {
|
||||||
|
userId = e.sender.user_id
|
||||||
|
}
|
||||||
|
let at = e.message.find(m => m.type === 'at')
|
||||||
|
if (at) {
|
||||||
|
userId = at.qq
|
||||||
|
}
|
||||||
|
let lock = await redis.get(`CHATGPT:WORDCLOUD_NEW:${groupId}_${userId}`)
|
||||||
|
if (lock) {
|
||||||
|
await e.reply('别着急,上次统计还没完呢')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
await e.reply('在统计啦,请稍等...')
|
||||||
|
let duration = 24
|
||||||
|
if (e.msg.includes('本周')) {
|
||||||
|
const now = new Date() // Get the current date and time
|
||||||
|
let day = now.getDay()
|
||||||
|
let diff = now.getDate() - day + (day === 0 ? -6 : 1)
|
||||||
|
const startOfWeek = new Date(new Date().setDate(diff))
|
||||||
|
startOfWeek.setHours(0, 0, 0, 0) // Set the time to midnight (start of the day)
|
||||||
|
duration = (now - startOfWeek) / 1000 / 60 / 60
|
||||||
|
} else if (e.msg.includes('本月')) {
|
||||||
|
const now = new Date() // Get the current date and time
|
||||||
|
const startOfMonth = new Date(new Date().setDate(0))
|
||||||
|
startOfMonth.setHours(0, 0, 0, 0) // Set the time to midnight (start of the day)
|
||||||
|
duration = (now - startOfMonth) / 1000 / 60 / 60
|
||||||
|
} else {
|
||||||
|
// 默认今天
|
||||||
|
const now = new Date()
|
||||||
|
const startOfToday = new Date() // Get the current date and time
|
||||||
|
startOfToday.setHours(0, 0, 0, 0) // Set the time to midnight (start of the day)
|
||||||
|
duration = (now - startOfToday) / 1000 / 60 / 60
|
||||||
|
}
|
||||||
|
await redis.set(`CHATGPT:WORDCLOUD_NEW:${groupId}_${userId}`, '1', { EX: 600 })
|
||||||
|
try {
|
||||||
|
await makeWordcloud(e, e.group_id, duration, userId)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err)
|
||||||
|
await e.reply(err)
|
||||||
|
}
|
||||||
|
await redis.del(`CHATGPT:WORDCLOUD_NEW:${groupId}_${userId}`)
|
||||||
|
} else {
|
||||||
|
await e.reply('请在群里发送此命令')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async combineEmoj (e) {
|
async combineEmoj (e) {
|
||||||
let left = e.msg.codePointAt(0).toString(16).toLowerCase()
|
let left = e.msg.codePointAt(0).toString(16).toLowerCase()
|
||||||
let right = e.msg.codePointAt(2).toString(16).toLowerCase()
|
let right = e.msg.codePointAt(2).toString(16).toLowerCase()
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,8 @@ export class help extends plugin {
|
||||||
const keyMap = {
|
const keyMap = {
|
||||||
api: 'promptPrefixOverride',
|
api: 'promptPrefixOverride',
|
||||||
Custom: 'sydney',
|
Custom: 'sydney',
|
||||||
claude: 'slackClaudeGlobalPreset'
|
claude: 'slackClaudeGlobalPreset',
|
||||||
|
qwen: 'promptPrefixOverride'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyMap[use]) {
|
if (keyMap[use]) {
|
||||||
|
|
|
||||||
101
client/BaseClient.js
Normal file
101
client/BaseClient.js
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
/**
|
||||||
|
* Base LLM Chat Client \
|
||||||
|
* All the Chat Models should extend this class
|
||||||
|
*
|
||||||
|
* @since 2023-10-26
|
||||||
|
* @author ikechan8370
|
||||||
|
*/
|
||||||
|
export class BaseClient {
|
||||||
|
/**
|
||||||
|
* create a new client
|
||||||
|
*
|
||||||
|
* @param props required fields: e, getMessageById, upsertMessage
|
||||||
|
*/
|
||||||
|
constructor (props = {}) {
|
||||||
|
this.supportFunction = false
|
||||||
|
this.maxToken = 4096
|
||||||
|
this.tools = []
|
||||||
|
const {
|
||||||
|
e, getMessageById, upsertMessage
|
||||||
|
} = props
|
||||||
|
this.e = e
|
||||||
|
this.getMessageById = getMessageById
|
||||||
|
this.upsertMessage = upsertMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get a message according to the id. note that conversationId is not needed
|
||||||
|
*
|
||||||
|
* @type function
|
||||||
|
* @param {string} id
|
||||||
|
* @return {Promise<object>} message
|
||||||
|
*/
|
||||||
|
getMessageById
|
||||||
|
|
||||||
|
/**
|
||||||
|
* insert or update a message with the id
|
||||||
|
*
|
||||||
|
* @type function
|
||||||
|
* @param {string} id
|
||||||
|
* @param {object} message
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
upsertMessage
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send prompt message with history and return response message \
|
||||||
|
* if function called, handled internally \
|
||||||
|
* override this method to implement logic of sending and receiving message
|
||||||
|
*
|
||||||
|
* @param msg
|
||||||
|
* @param opt other options, optional fields: [conversationId, parentMessageId], if not set, random uuid instead
|
||||||
|
* @returns {Promise<Message>} required fields: [text, conversationId, parentMessageId, id]
|
||||||
|
*/
|
||||||
|
async sendMessage (msg, opt = {}) {
|
||||||
|
throw new Error('not implemented in abstract client')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get chat history between user and assistant
|
||||||
|
* override this method to implement logic of getting history
|
||||||
|
* keyv with local file or redis recommended
|
||||||
|
*
|
||||||
|
* @param userId such as qq number
|
||||||
|
* @param opt other options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async getHistory (userId, opt = {}) {
|
||||||
|
throw new Error('not implemented in abstract client')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy a chat history
|
||||||
|
* @param conversationId conversationId of the chat history
|
||||||
|
* @param opt other options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async destroyHistory (conversationId, opt = {}) {
|
||||||
|
throw new Error('not implemented in abstract client')
|
||||||
|
}
|
||||||
|
|
||||||
|
addTools (...tools) {
|
||||||
|
if (!this.isSupportFunction) {
|
||||||
|
throw new Error('function not supported')
|
||||||
|
}
|
||||||
|
if (!this.tools) {
|
||||||
|
this.tools = []
|
||||||
|
}
|
||||||
|
this.tools.push(tools)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTools () {
|
||||||
|
if (!this.isSupportFunction) {
|
||||||
|
throw new Error('function not supported')
|
||||||
|
}
|
||||||
|
return this.tools || []
|
||||||
|
}
|
||||||
|
|
||||||
|
get isSupportFunction () {
|
||||||
|
return this.supportFunction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
"js-tiktoken": "^1.0.5",
|
"js-tiktoken": "^1.0.5",
|
||||||
"keyv": "^4.5.3",
|
"keyv": "^4.5.3",
|
||||||
"keyv-file": "^0.2.0",
|
"keyv-file": "^0.2.0",
|
||||||
"microsoft-cognitiveservices-speech-sdk": "^1.30.1",
|
"microsoft-cognitiveservices-speech-sdk": "1.32.0",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"openai": "^3.2.1",
|
"openai": "^3.2.1",
|
||||||
"p-timeout": "^6.1.2",
|
"p-timeout": "^6.1.2",
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,11 @@ export default class SydneyAIClient {
|
||||||
agent = proxy(this.opts.proxy)
|
agent = proxy(this.opts.proxy)
|
||||||
}
|
}
|
||||||
if (Config.sydneyWebsocketUseProxy) {
|
if (Config.sydneyWebsocketUseProxy) {
|
||||||
sydneyHost = Config.sydneyReverseProxy.replace('https://', 'wss://').replace('http://', 'ws://')
|
if (!Config.sydneyReverseProxy) {
|
||||||
|
logger.warn('用户开启了对话反代,但是没有配置反代,忽略反代配置')
|
||||||
|
} else {
|
||||||
|
sydneyHost = Config.sydneyReverseProxy.replace('https://', 'wss://').replace('http://', 'ws://')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
logger.mark(`use sydney websocket host: ${sydneyHost}`)
|
logger.mark(`use sydney websocket host: ${sydneyHost}`)
|
||||||
let host = sydneyHost + '/sydney/ChatHub'
|
let host = sydneyHost + '/sydney/ChatHub'
|
||||||
|
|
@ -472,7 +476,7 @@ export default class SydneyAIClient {
|
||||||
context += chats
|
context += chats
|
||||||
.map(chat => {
|
.map(chat => {
|
||||||
let sender = chat.sender || chat || {}
|
let sender = chat.sender || chat || {}
|
||||||
if (chat.raw_message.startsWith('建议的回复')) {
|
if (chat.raw_message?.startsWith('建议的回复')) {
|
||||||
// 建议的回复太容易污染设定导致对话太固定跑偏了
|
// 建议的回复太容易污染设定导致对话太固定跑偏了
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
utils/chat.js
Normal file
33
utils/chat.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
export async function getChatHistoryGroup (e, num) {
|
||||||
|
if (e.adapter === 'shamrock') {
|
||||||
|
return await e.group.getChatHistory(0, num, false)
|
||||||
|
} else {
|
||||||
|
let latestChats = await e.group.getChatHistory(0, 1)
|
||||||
|
if (latestChats.length > 0) {
|
||||||
|
let latestChat = latestChats[0]
|
||||||
|
if (latestChat) {
|
||||||
|
let seq = latestChat.seq
|
||||||
|
let chats = []
|
||||||
|
while (chats.length < num) {
|
||||||
|
let chatHistory = await e.group.getChatHistory(seq, 20)
|
||||||
|
chats.push(...chatHistory)
|
||||||
|
}
|
||||||
|
chats = chats.slice(0, num)
|
||||||
|
try {
|
||||||
|
let mm = await e.group.getMemberMap()
|
||||||
|
chats.forEach(chat => {
|
||||||
|
let sender = mm.get(chat.sender.user_id)
|
||||||
|
if (sender) {
|
||||||
|
chat.sender = sender
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(err)
|
||||||
|
}
|
||||||
|
// console.log(chats)
|
||||||
|
return chats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
@ -832,8 +832,10 @@ export function getMaxModelTokens (model = 'gpt-3.5-turbo') {
|
||||||
if (model.startsWith('gpt-3.5-turbo')) {
|
if (model.startsWith('gpt-3.5-turbo')) {
|
||||||
if (model.includes('16k')) {
|
if (model.includes('16k')) {
|
||||||
return 16000
|
return 16000
|
||||||
} else {
|
} else if (model.includes('0613') || model.includes('0314')) {
|
||||||
return 4000
|
return 4000
|
||||||
|
} else {
|
||||||
|
return 16000
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (model.includes('32k')) {
|
if (model.includes('32k')) {
|
||||||
|
|
@ -847,14 +849,13 @@ export function getMaxModelTokens (model = 'gpt-3.5-turbo') {
|
||||||
export function getUin (e) {
|
export function getUin (e) {
|
||||||
if (e?.bot?.uin) return e.bot.uin
|
if (e?.bot?.uin) return e.bot.uin
|
||||||
if (Array.isArray(Bot.uin)) {
|
if (Array.isArray(Bot.uin)) {
|
||||||
if (Config.trssBotUin && Bot.uin.indexOf(Config.trssBotUin) > -1) {return Config.trssBotUin}
|
if (Config.trssBotUin && Bot.uin.indexOf(Config.trssBotUin) > -1) { return Config.trssBotUin } else {
|
||||||
else {
|
Bot.uin.forEach((u) => {
|
||||||
Bot.uin.forEach((u) => {
|
if (Bot[u].self_id) {
|
||||||
if (Bot[u].self_id) {
|
return Bot[u].self_id
|
||||||
return Bot[u].self_id
|
}
|
||||||
}
|
})
|
||||||
})
|
return Bot.uin[Bot.uin.length - 1]
|
||||||
return Bot.uin[Bot.uin.length - 1]
|
|
||||||
}
|
}
|
||||||
} else return Bot.uin
|
} else return Bot.uin
|
||||||
}
|
}
|
||||||
|
|
@ -871,6 +872,7 @@ export async function generateAudio (e, pendingText, speakingEmotion, emotionDeg
|
||||||
if (!Config.ttsSpace && !Config.azureTTSKey && !Config.voicevoxSpace) return false
|
if (!Config.ttsSpace && !Config.azureTTSKey && !Config.voicevoxSpace) return false
|
||||||
let wav
|
let wav
|
||||||
const speaker = getUserSpeaker(await getUserReplySetting(e))
|
const speaker = getUserSpeaker(await getUserReplySetting(e))
|
||||||
|
let ignoreEncode = e.adapter === 'shamrock'
|
||||||
try {
|
try {
|
||||||
if (Config.ttsMode === 'vits-uma-genshin-honkai' && Config.ttsSpace) {
|
if (Config.ttsMode === 'vits-uma-genshin-honkai' && Config.ttsSpace) {
|
||||||
if (Config.autoJapanese) {
|
if (Config.autoJapanese) {
|
||||||
|
|
@ -883,7 +885,7 @@ export async function generateAudio (e, pendingText, speakingEmotion, emotionDeg
|
||||||
}
|
}
|
||||||
wav = await generateVitsAudio(pendingText, speaker, '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)')
|
wav = await generateVitsAudio(pendingText, speaker, '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)')
|
||||||
} else if (Config.ttsMode === 'azure' && Config.azureTTSKey) {
|
} else if (Config.ttsMode === 'azure' && Config.azureTTSKey) {
|
||||||
return await generateAzureAudio(pendingText, speaker, speakingEmotion, emotionDegree)
|
return await generateAzureAudio(pendingText, speaker, speakingEmotion, emotionDegree, ignoreEncode)
|
||||||
} else if (Config.ttsMode === 'voicevox' && Config.voicevoxSpace) {
|
} else if (Config.ttsMode === 'voicevox' && Config.voicevoxSpace) {
|
||||||
pendingText = (await translate(pendingText, '日')).replace('\n', '')
|
pendingText = (await translate(pendingText, '日')).replace('\n', '')
|
||||||
wav = await VoiceVoxTTS.generateAudio(pendingText, {
|
wav = await VoiceVoxTTS.generateAudio(pendingText, {
|
||||||
|
|
@ -897,7 +899,7 @@ export async function generateAudio (e, pendingText, speakingEmotion, emotionDeg
|
||||||
let sendable
|
let sendable
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
sendable = await uploadRecord(wav, Config.ttsMode)
|
sendable = await uploadRecord(wav, Config.ttsMode, ignoreEncode)
|
||||||
if (!sendable) {
|
if (!sendable) {
|
||||||
// 如果合成失败,尝试使用ffmpeg合成
|
// 如果合成失败,尝试使用ffmpeg合成
|
||||||
sendable = segment.record(wav)
|
sendable = segment.record(wav)
|
||||||
|
|
@ -927,9 +929,10 @@ export async function generateAudio (e, pendingText, speakingEmotion, emotionDeg
|
||||||
* @param role - 发言人
|
* @param role - 发言人
|
||||||
* @param speakingEmotion - 发言人情绪
|
* @param speakingEmotion - 发言人情绪
|
||||||
* @param emotionDegree - 发言人情绪强度
|
* @param emotionDegree - 发言人情绪强度
|
||||||
|
* @param ignoreEncode - 不在客户端处理编码
|
||||||
* @returns {Promise<{file: string, type: string}|boolean>}
|
* @returns {Promise<{file: string, type: string}|boolean>}
|
||||||
*/
|
*/
|
||||||
export async function generateAzureAudio (pendingText, role = '随机', speakingEmotion, emotionDegree = 1) {
|
export async function generateAzureAudio (pendingText, role = '随机', speakingEmotion, emotionDegree = 1, ignoreEncode = false) {
|
||||||
if (!Config.azureTTSKey) return false
|
if (!Config.azureTTSKey) return false
|
||||||
let speaker
|
let speaker
|
||||||
try {
|
try {
|
||||||
|
|
@ -970,11 +973,13 @@ export async function generateAzureAudio (pendingText, role = '随机', speaking
|
||||||
pendingText,
|
pendingText,
|
||||||
emotionDegree
|
emotionDegree
|
||||||
})
|
})
|
||||||
|
let record = await AzureTTS.generateAudio(pendingText, {
|
||||||
|
speaker
|
||||||
|
}, await ssml)
|
||||||
return await uploadRecord(
|
return await uploadRecord(
|
||||||
await AzureTTS.generateAudio(pendingText, {
|
record
|
||||||
speaker
|
, Config.ttsMode,
|
||||||
}, await ssml)
|
ignoreEncode
|
||||||
, Config.ttsMode
|
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ const defaultConfig = {
|
||||||
groupContextTip: '你看看我们群里的聊天记录吧,回答问题的时候要主动参考我们的聊天记录进行回答或提问。但要看清楚哦,不要把我和其他人弄混啦,也不要把自己看晕啦~~',
|
groupContextTip: '你看看我们群里的聊天记录吧,回答问题的时候要主动参考我们的聊天记录进行回答或提问。但要看清楚哦,不要把我和其他人弄混啦,也不要把自己看晕啦~~',
|
||||||
groupContextLength: 50,
|
groupContextLength: 50,
|
||||||
enableRobotAt: true,
|
enableRobotAt: true,
|
||||||
maxNumUserMessagesInConversation: 20,
|
maxNumUserMessagesInConversation: 30,
|
||||||
sydneyApologyIgnored: true,
|
sydneyApologyIgnored: true,
|
||||||
enforceMaster: false,
|
enforceMaster: false,
|
||||||
bingAPDraw: false,
|
bingAPDraw: false,
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,12 @@ export class SendAudioMessageTool extends AbstractTool {
|
||||||
return `audio generation failed, error: ${JSON.stringify(err)}`
|
return `audio generation failed, error: ${JSON.stringify(err)}`
|
||||||
}
|
}
|
||||||
if (sendable) {
|
if (sendable) {
|
||||||
let groupList = await e.bot.getGroupList()
|
let groupList
|
||||||
|
try {
|
||||||
|
groupList = await e.bot.getGroupList()
|
||||||
|
} catch (err) {
|
||||||
|
groupList = e.bot.gl
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (groupList.get(target)) {
|
if (groupList.get(target)) {
|
||||||
let group = await e.bot.pickGroup(target)
|
let group = await e.bot.pickGroup(target)
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,12 @@ export class SendAvatarTool extends AbstractTool {
|
||||||
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
|
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
|
||||||
? defaultTarget
|
? defaultTarget
|
||||||
: parseInt(targetGroupIdOrQQNumber) === e.bot.uin ? defaultTarget : parseInt(targetGroupIdOrQQNumber)
|
: parseInt(targetGroupIdOrQQNumber) === e.bot.uin ? defaultTarget : parseInt(targetGroupIdOrQQNumber)
|
||||||
|
let groupList
|
||||||
let groupList = await e.bot.getGroupList()
|
try {
|
||||||
|
groupList = await e.bot.getGroupList()
|
||||||
|
} catch (err) {
|
||||||
|
groupList = e.bot.gl
|
||||||
|
}
|
||||||
console.log('sendAvatar', target, pictures)
|
console.log('sendAvatar', target, pictures)
|
||||||
if (groupList.get(target)) {
|
if (groupList.get(target)) {
|
||||||
let group = await e.bot.pickGroup(target)
|
let group = await e.bot.pickGroup(target)
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,12 @@ export class SendDiceTool extends AbstractTool {
|
||||||
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
|
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
|
||||||
? defaultTarget
|
? defaultTarget
|
||||||
: parseInt(targetGroupIdOrQQNumber) === e.bot.uin ? defaultTarget : parseInt(targetGroupIdOrQQNumber)
|
: parseInt(targetGroupIdOrQQNumber) === e.bot.uin ? defaultTarget : parseInt(targetGroupIdOrQQNumber)
|
||||||
let groupList = await e.bot.getGroupList()
|
let groupList
|
||||||
|
try {
|
||||||
|
groupList = await e.bot.getGroupList()
|
||||||
|
} catch (err) {
|
||||||
|
groupList = e.bot.gl
|
||||||
|
}
|
||||||
num = isNaN(num) || !num ? 1 : num > 5 ? 5 : num
|
num = isNaN(num) || !num ? 1 : num > 5 ? 5 : num
|
||||||
if (groupList.get(target)) {
|
if (groupList.get(target)) {
|
||||||
let group = await e.bot.pickGroup(target, true)
|
let group = await e.bot.pickGroup(target, true)
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,12 @@ export class SendMessageToSpecificGroupOrUserTool extends AbstractTool {
|
||||||
? defaultTarget
|
? defaultTarget
|
||||||
: parseInt(targetGroupIdOrQQNumber) === e.bot.uin ? defaultTarget : parseInt(targetGroupIdOrQQNumber)
|
: parseInt(targetGroupIdOrQQNumber) === e.bot.uin ? defaultTarget : parseInt(targetGroupIdOrQQNumber)
|
||||||
|
|
||||||
let groupList = await e.bot.getGroupList()
|
let groupList
|
||||||
|
try {
|
||||||
|
groupList = await e.bot.getGroupList()
|
||||||
|
} catch (err) {
|
||||||
|
groupList = e.bot.gl
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (groupList.get(target)) {
|
if (groupList.get(target)) {
|
||||||
let group = await e.bot.pickGroup(target)
|
let group = await e.bot.pickGroup(target)
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,12 @@ export class SendPictureTool extends AbstractTool {
|
||||||
let pictures = urlOfPicture.trim().split(' ')
|
let pictures = urlOfPicture.trim().split(' ')
|
||||||
logger.mark('pictures to send: ', pictures)
|
logger.mark('pictures to send: ', pictures)
|
||||||
pictures = pictures.map(img => segment.image(img))
|
pictures = pictures.map(img => segment.image(img))
|
||||||
let groupList = await e.bot.getGroupList()
|
let groupList
|
||||||
|
try {
|
||||||
|
groupList = await e.bot.getGroupList()
|
||||||
|
} catch (err) {
|
||||||
|
groupList = e.bot.gl
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (groupList.get(target)) {
|
if (groupList.get(target)) {
|
||||||
let group = await e.bot.pickGroup(target)
|
let group = await e.bot.pickGroup(target)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,12 @@ export class SendRPSTool extends AbstractTool {
|
||||||
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
|
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
|
||||||
? defaultTarget
|
? defaultTarget
|
||||||
: parseInt(targetGroupIdOrQQNumber) === e.bot.uin ? defaultTarget : parseInt(targetGroupIdOrQQNumber)
|
: parseInt(targetGroupIdOrQQNumber) === e.bot.uin ? defaultTarget : parseInt(targetGroupIdOrQQNumber)
|
||||||
let groupList = await e.bot.getGroupList()
|
let groupList
|
||||||
|
try {
|
||||||
|
groupList = await e.bot.getGroupList()
|
||||||
|
} catch (err) {
|
||||||
|
groupList = e.bot.gl
|
||||||
|
}
|
||||||
if (groupList.get(target)) {
|
if (groupList.get(target)) {
|
||||||
let group = await e.bot.pickGroup(target, true)
|
let group = await e.bot.pickGroup(target, true)
|
||||||
await group.sendMsg(segment.rps(num))
|
await group.sendMsg(segment.rps(num))
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ if (module) {
|
||||||
// import { pcm2slk } from 'node-silk'
|
// import { pcm2slk } from 'node-silk'
|
||||||
let errors = {}
|
let errors = {}
|
||||||
|
|
||||||
async function uploadRecord (recordUrl, ttsMode = 'vits-uma-genshin-honkai') {
|
async function uploadRecord (recordUrl, ttsMode = 'vits-uma-genshin-honkai', ignoreEncode = false) {
|
||||||
let recordType = 'url'
|
let recordType = 'url'
|
||||||
let tmpFile = ''
|
let tmpFile = ''
|
||||||
if (ttsMode === 'azure') {
|
if (ttsMode === 'azure') {
|
||||||
|
|
@ -48,6 +48,9 @@ async function uploadRecord (recordUrl, ttsMode = 'vits-uma-genshin-honkai') {
|
||||||
recordType = 'buffer'
|
recordType = 'buffer'
|
||||||
tmpFile = `data/chatgpt/tts/tmp/${crypto.randomUUID()}.wav`
|
tmpFile = `data/chatgpt/tts/tmp/${crypto.randomUUID()}.wav`
|
||||||
}
|
}
|
||||||
|
if (ignoreEncode) {
|
||||||
|
return segment.record(recordUrl)
|
||||||
|
}
|
||||||
let result
|
let result
|
||||||
if (pcm2slk) {
|
if (pcm2slk) {
|
||||||
result = await getPttBuffer(recordUrl, Bot.config.ffmpeg_path)
|
result = await getPttBuffer(recordUrl, Bot.config.ffmpeg_path)
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ try {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Tokenizer {
|
export class Tokenizer {
|
||||||
async getHistory (groupId, date = new Date(), duration = 0) {
|
async getHistory (e, groupId, date = new Date(), duration = 0, userId) {
|
||||||
if (!groupId) {
|
if (!groupId) {
|
||||||
throw new Error('no valid group id')
|
throw new Error('no valid group id')
|
||||||
}
|
}
|
||||||
let group = Bot.pickGroup(groupId, true)
|
let group = e.bot.pickGroup(groupId, true)
|
||||||
let latestChat = await group.getChatHistory(0, 1)
|
let latestChat = await group.getChatHistory(0, 1)
|
||||||
let seq = latestChat[0].seq
|
let seq = latestChat[0].seq
|
||||||
let chats = latestChat
|
let chats = latestChat
|
||||||
|
|
@ -41,14 +41,15 @@ export class Tokenizer {
|
||||||
let startOfSpecifiedDate = date.getTime()
|
let startOfSpecifiedDate = date.getTime()
|
||||||
// if duration > 0, go back to the specified number of hours
|
// if duration > 0, go back to the specified number of hours
|
||||||
if (duration > 0) {
|
if (duration > 0) {
|
||||||
// duration should be in range [0, 24]
|
// duration should be in range [0, 24]
|
||||||
duration = Math.min(duration, 24)
|
// duration = Math.min(duration, 24)
|
||||||
startOfSpecifiedDate = currentTime - (duration * 60 * 60 * 1000)
|
startOfSpecifiedDate = currentTime - (duration * 60 * 60 * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Get the end of the specified date by adding 24 hours (in milliseconds)
|
// Step 4: Get the end of the specified date by current time
|
||||||
const endOfSpecifiedDate = startOfSpecifiedDate + (24 * 60 * 60 * 1000)
|
const endOfSpecifiedDate = currentTime
|
||||||
while (isTimestampInDateRange(chats[0]?.time, startOfSpecifiedDate, endOfSpecifiedDate) && isTimestampInDateRange(chats[chats.length - 1]?.time, startOfSpecifiedDate, endOfSpecifiedDate)) {
|
while (isTimestampInDateRange(chats[0]?.time, startOfSpecifiedDate, endOfSpecifiedDate) &&
|
||||||
|
isTimestampInDateRange(chats[chats.length - 1]?.time, startOfSpecifiedDate, endOfSpecifiedDate)) {
|
||||||
let chatHistory = await group.getChatHistory(seq, 20)
|
let chatHistory = await group.getChatHistory(seq, 20)
|
||||||
if (chatHistory.length === 1) {
|
if (chatHistory.length === 1) {
|
||||||
if (chats[0].seq === chatHistory[0].seq) {
|
if (chats[0].seq === chatHistory[0].seq) {
|
||||||
|
|
@ -58,45 +59,51 @@ export class Tokenizer {
|
||||||
}
|
}
|
||||||
chats.push(...chatHistory)
|
chats.push(...chatHistory)
|
||||||
chats.sort(compareByTime)
|
chats.sort(compareByTime)
|
||||||
seq = chatHistory[0].seq
|
seq = chatHistory?.[0]?.seq
|
||||||
|
if (!seq) {
|
||||||
|
break
|
||||||
|
}
|
||||||
if (Config.debug) {
|
if (Config.debug) {
|
||||||
logger.info(`拉取到${chatHistory.length}条聊天记录,当前已累计获取${chats.length}条聊天记录,继续拉...`)
|
logger.info(`拉取到${chatHistory.length}条聊天记录,当前已累计获取${chats.length}条聊天记录,继续拉...`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chats = chats.filter(chat => isTimestampInDateRange(chat.time, startOfSpecifiedDate, endOfSpecifiedDate))
|
chats = chats.filter(chat => isTimestampInDateRange(chat.time, startOfSpecifiedDate, endOfSpecifiedDate))
|
||||||
|
if (userId) {
|
||||||
|
chats = chats.filter(chat => chat.sender.user_id === userId)
|
||||||
|
}
|
||||||
return chats
|
return chats
|
||||||
}
|
}
|
||||||
|
|
||||||
async getKeywordTopK (groupId, topK = 100, duration = 0) {
|
async getKeywordTopK (e, groupId, topK = 100, duration = 0, userId) {
|
||||||
if (!nodejieba) {
|
if (!nodejieba) {
|
||||||
throw new Error('未安装node-rs/jieba,娱乐功能-词云统计不可用')
|
throw new Error('未安装node-rs/jieba,娱乐功能-词云统计不可用')
|
||||||
}
|
}
|
||||||
// duration represents the number of hours to go back, should in range [0, 24]
|
// duration represents the number of hours to go back, should in range [0, 24]
|
||||||
let chats = await this.getHistory(groupId, new Date(), duration)
|
let chats = await this.getHistory(e, groupId, new Date(), duration, userId)
|
||||||
let duration_str = duration > 0 ? `${duration}小时` : '今日'
|
let durationStr = duration > 0 ? `${duration}小时` : '今日'
|
||||||
logger.mark(`聊天记录拉取完成,获取到${duration_str}内${chats.length}条聊天记录,准备分词中`)
|
logger.mark(`聊天记录拉取完成,获取到${durationStr}内${chats.length}条聊天记录,准备分词中`)
|
||||||
|
|
||||||
const _path = process.cwd()
|
const _path = process.cwd()
|
||||||
let stopWordsPath = `${_path}/plugins/chatgpt-plugin/utils/wordcloud/cn_stopwords.txt`
|
let stopWordsPath = `${_path}/plugins/chatgpt-plugin/utils/wordcloud/cn_stopwords.txt`
|
||||||
const data = fs.readFileSync(stopWordsPath)
|
const data = fs.readFileSync(stopWordsPath)
|
||||||
const stopWords = String(data)?.split('\n') || []
|
const stopWords = String(data)?.split('\n') || []
|
||||||
let chatContent = chats
|
let chatContent = chats
|
||||||
.map(c => c.message
|
.map(c => c.message
|
||||||
//只统计文本内容
|
// 只统计文本内容
|
||||||
.filter(item => item.type == 'text')
|
.filter(item => item.type == 'text')
|
||||||
.map(textItem => `${textItem.text}`)
|
.map(textItem => `${textItem.text}`)
|
||||||
.join("").trim()
|
.join('').trim()
|
||||||
)
|
)
|
||||||
.map(c => {
|
.map(c => {
|
||||||
let length = c.length
|
// let length = c.length
|
||||||
let threshold = 10
|
let threshold = 2
|
||||||
if (length < 100 && length > 50) {
|
// if (length < 100 && length > 50) {
|
||||||
threshold = 6
|
// threshold = 6
|
||||||
} else if (length <= 50 && length > 25) {
|
// } else if (length <= 50 && length > 25) {
|
||||||
threshold = 3
|
// threshold = 3
|
||||||
} else if (length <= 25) {
|
// } else if (length <= 25) {
|
||||||
threshold = 2
|
// threshold = 2
|
||||||
}
|
// }
|
||||||
return nodejieba.extract(c, threshold)
|
return nodejieba.extract(c, threshold)
|
||||||
})
|
})
|
||||||
.reduce((acc, curr) => acc.concat(curr), [])
|
.reduce((acc, curr) => acc.concat(curr), [])
|
||||||
|
|
@ -132,6 +139,85 @@ export class Tokenizer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ShamrockTokenizer extends Tokenizer {
|
||||||
|
async getHistory (e, groupId, date = new Date(), duration = 0, userId) {
|
||||||
|
logger.mark('当前使用Shamrock适配器')
|
||||||
|
if (!groupId) {
|
||||||
|
throw new Error('no valid group id')
|
||||||
|
}
|
||||||
|
let group = e.bot.pickGroup(groupId, true)
|
||||||
|
// 直接加大力度
|
||||||
|
let pageSize = 500
|
||||||
|
let chats = (await group.getChatHistory(0, pageSize, false)) || []
|
||||||
|
// Get the current timestamp
|
||||||
|
let currentTime = date.getTime()
|
||||||
|
|
||||||
|
// Step 2: Set the hours, minutes, seconds, and milliseconds to 0
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
// Step 3: Calculate the timestamp representing the start of the specified date
|
||||||
|
// duration represents the number of hours to go back
|
||||||
|
// if duration is 0, keeping the original date (start of today)
|
||||||
|
let startOfSpecifiedDate = date.getTime()
|
||||||
|
// if duration > 0, go back to the specified number of hours
|
||||||
|
if (duration > 0) {
|
||||||
|
// duration should be in range [0, 24]
|
||||||
|
// duration = Math.min(duration, 24)
|
||||||
|
startOfSpecifiedDate = currentTime - (duration * 60 * 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Get the end of the specified date by currentTime
|
||||||
|
const endOfSpecifiedDate = currentTime
|
||||||
|
let cursor = chats.length
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// | | |
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// ^ ^
|
||||||
|
// long ago cursor+pageSize cursor current
|
||||||
|
while (isTimestampInDateRange(chats[0]?.time, startOfSpecifiedDate, endOfSpecifiedDate)) {
|
||||||
|
// 由于Shamrock消息是从最新的开始拉,结束时由于动态更新,一旦有人发送消息就会立刻停止,所以不判断结束时间
|
||||||
|
// 拉到后面会巨卡,所以增大page减少次数
|
||||||
|
pageSize = Math.floor(Math.max(cursor / 2, pageSize))
|
||||||
|
cursor = cursor + pageSize
|
||||||
|
let retries = 3
|
||||||
|
let chatHistory
|
||||||
|
while (retries >= 0) {
|
||||||
|
try {
|
||||||
|
chatHistory = await group.getChatHistory(0, cursor, false)
|
||||||
|
break
|
||||||
|
} catch (err) {
|
||||||
|
if (retries === 0) {
|
||||||
|
logger.error(err)
|
||||||
|
}
|
||||||
|
retries--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (retries < 0) {
|
||||||
|
logger.warn('拉不动了,就这样吧')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (chatHistory.length === 1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (chatHistory.length === chats.length) {
|
||||||
|
// 没有了!再拉也没有了
|
||||||
|
break
|
||||||
|
}
|
||||||
|
let oldLength = chats.length
|
||||||
|
chats = chatHistory
|
||||||
|
// chats.sort(compareByTime)
|
||||||
|
if (Config.debug) {
|
||||||
|
logger.info(`拉取到${chats.length - oldLength}条聊天记录,当前已累计获取${chats.length}条聊天记录,继续拉...`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chats = chats.filter(chat => isTimestampInDateRange(chat.time, startOfSpecifiedDate, endOfSpecifiedDate))
|
||||||
|
if (userId) {
|
||||||
|
chats = chats.filter(chat => chat.sender.user_id === userId)
|
||||||
|
}
|
||||||
|
return chats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isTimestampInDateRange (timestamp, startOfSpecifiedDate, endOfSpecifiedDate) {
|
function isTimestampInDateRange (timestamp, startOfSpecifiedDate, endOfSpecifiedDate) {
|
||||||
if (!timestamp) {
|
if (!timestamp) {
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
import { Tokenizer } from './tokenizer.js'
|
import { ShamrockTokenizer, Tokenizer } from './tokenizer.js'
|
||||||
import { render } from '../common.js'
|
import { render } from '../common.js'
|
||||||
|
|
||||||
export async function makeWordcloud (e, groupId, duration = 0) {
|
export async function makeWordcloud (e, groupId, duration = 0, userId) {
|
||||||
let tokenizer = new Tokenizer()
|
let tokenizer = getTokenizer(e)
|
||||||
let topK = await tokenizer.getKeywordTopK(groupId, 100, duration)
|
let topK = await tokenizer.getKeywordTopK(e, groupId, 100, duration, userId)
|
||||||
let list = JSON.stringify(topK)
|
let list = JSON.stringify(topK)
|
||||||
// let list = topK
|
logger.info(list)
|
||||||
console.log(list)
|
let img = await render(e, 'chatgpt-plugin', 'wordcloud/index', { list }, { retType: 'base64' })
|
||||||
await render(e, 'chatgpt-plugin', 'wordcloud/index', { list })
|
await e.reply(img, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTokenizer (e) {
|
||||||
|
if (e.adapter === 'shamrock') {
|
||||||
|
return new ShamrockTokenizer()
|
||||||
|
} else {
|
||||||
|
return new Tokenizer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue