🥵🥵🥵 (#496)

* feat: add support for ‘greeting’ and ‘global reply mode’ commands, improve variable naming and remove unnecessary backend output.

* feat: Add support for black and white lists, global reply mode and voice role settings, private chat switch, and active greeting configuration. Refactor some variable names and comment out redundant code for better readability and reduced backend output.

* feat: 为新功能完善了帮助面板

* docs: 完善了‘打招呼’的帮助说明

* Commit Type: feat, bugfix

Add functionality to view plugin command table, fix bug in blacklist/whitelist, and fix bug where chat mode can still be used in private messaging when disabled.

* Commit Type: feat, bugfix

Add functionality to view plugin command table, fix bug in blacklist/whitelist, and fix bug where chat mode can still be used in private messaging when disabled.

* refactor: Remove redundant log output.

* Refactor: optimize code logic

* Fix: 修复绘图指令表被抢指令的bug。

* Refactor:1. Add support for automatically translating replies to Japanese and generating voice messages in VITS voice mode (please monitor remaining quota after enabling). 2. Add translation function. 3. Add emotion configuration for Azure voice mode, allowing the robot to select appropriate emotional styles for replies.

* Refactor:Handle the issue of exceeding character setting limit caused by adding emotion configuration.

* Fix: fix bugs

* Refactor: Added error feedback to translation service

* Refactor: Added support for viewing the list of supported roles for each language mode, and fixed some bugs in the emotion switching feature of the auzre mode.

* Refactor: Optimized some command feedback and added owner restriction to chat record export function.

* Refactor: Optimized feedback when viewing role list to avoid excessive messages.

* Refactor: Optimized feedback when configuring multi-emotion mode.

* Feature: Added help instructions for translation feature.

* chore: Adjust help instructions for mood settings

* Fix: Fixed issue where only first line of multi-line replies were being read and Azure voice was pronouncing punctuation marks.

* Fix: Fixed bug where switching to Azure voice mode prompted for missing key and restricted ability to view voice role list to only when in voice mode.

* Refactor: Add image OCR function and support translation for both quoted text and image.

* fix: Fix issue with error caused by non-image input.

* Refactor: Optimize code to filter emojis that cannot be displayed properly in claude mode.

* Refactor: Optimize some code structures.

* fix: Fix the bug of returning only one result when entering multiple lines of text on Windows system.

* Refactor: Optimize code logic for better user experience

* Refactor: Fix the conflict issue with other plugin translation commands

* Refactor: Replace Baidu Translation with Youdao Translation to eliminate configuration steps; optimize translation experience; add missing dependency prompts instead of causing program errors.Optimize the experience of switching between voice mode and setting global reply mode.

* Refactor: Remove unused files and dependencies in the project.

* Feature: Add Youdao translation service to provide more comprehensive translation support.

* Refactor: Optimize translation experience

* Refactor: Optimize translation experience

* Feature: Add functionality of keyword search command

* Feature: Add functionality of keyword search command.

* Refactor: Remove redundant code

* Add: Add feature to support randomly selecting roles for Azure voice. Refactor the code to support existing voice services for the ‘greeting’ feature. Fix the display issue of Azure voice role selection on the Guoba panel.

* Refactor: Remove redundant code

* Refactor: Improve the function of setting global voice roles and viewing role lists. Now you can set default roles for each voice service separately or view the supported role list.

* Refactor: Remove redundant code

* Feature: Add new function to support random character dialogues in all voice modes, add the ability to view the current user’s reply settings, and improve related functions in the global settings.

* Refactor: Add compatibility directive for viewing reply settings feature

* Feature: support adding QQ number to blacklist/whitelist

* fix: 处理全局设置指令被上下班指令占用的问题

* fix: 处理全局设置指令被上下班指令占用的问题

* Refactor: Preprocess dialogue blacklist/whitelist when filling in the form in Guoba panel

* Fix: Fixed the issue where black and white lists were not effective when filled in the Guoba panel, and the issue where no results were returned when viewing the voice role list without parameters in azure tts mode.

* fix: 2.7 dev start

* feat: 初步支持function call(WIP)

* fix: syntax error

* fix: syntax error

* feat: 群聊上下文

* fix: 暂时阉割掉全员禁言功能

* fix: 修改禁言时间范围

* fix: 修复一些功能易用性

* fix: 只有管理员和群主才能用jinyan和kickout

* fix: 加回来禁言和踢出

* fix: 修复管理员权限判断问题(可能吧)

* fix: 试图优化逻辑

* fix: fuck openai documents

* fix: 删掉认主不然一直禁言我烦死了

* fix: 哔哩哔哩封面损坏问题

* fix: 加个天气小工具

* fix: 天气不存在城市

* fix: website工具用浏览器

* feat: serp tool

* feat: 增加一个google搜索源

* fix: 加一句描述

* feat: 增加搜索来源选项

* feat: 搜图和发图

* fix: groupId format error

* Refactor: Optimized the HTML parsing rules

* fix: Fixed the bug where conversations could not be properly ended in at mode, now it works normally

* refactor: Added EliMovieTool and EliMusicTool. Modified some tool’s prompt to make AI make better choices. Optimized the display of chat history.Remove SendMusicTool and replace it with EliMusicTool.

* chore: trivial changes.

* refactor:Keep SearchMusicTool and SendMusicTool when avocado plugin is not installed, waiting for more testing~~~

* Refactor: Move the parameter processing logic of some tools into each tool internally.🥑Rename ttstool to SendAudioMessageTool, defaulting to the voice role configured by the current user, and support personalized configuration of the existing tts mode in the plugin.🥑Add SendMessageToSpecificGroupOrUserTool, which allows the robot to send messages to specific groups or friends.🥑Encapsulate the function of generating audio messages into an independent function for easy tool invocation.🥑Separate SendPictureTool and SendAvatarTool to avoid unnecessary bugs.🥑

* chore: Remove unnecessary log file🥑

* refactor: Trivial changes and fixed a bug in QueryUserinfoTool.🥑

* Refactor: Rewrite blacklist and whitelist functionality, optimize code calls Description: Rewrote the blacklist and whitelist functionality to support group, QQ number, and a combination of group and QQ number configurations. Removed the command to set the blacklist and whitelist functionality and unified it through the Guoba panel. Also optimized some code calls to EliMusicTool and EliMovieTool.🥑

---------

Co-authored-by: Sean <1519059137@qq.com>
Co-authored-by: ikechan8370 <geyinchibuaa@gmail.com>
Co-authored-by: ikechan8370 <geyinchi@buaa.edu.cn>
This commit is contained in:
Sean Murphy 2023-07-01 15:46:15 +08:00 committed by GitHub
parent 36e592e773
commit 2443ed6f71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 769 additions and 481 deletions

View file

@ -8,9 +8,11 @@ import buffer from 'buffer'
import yaml from 'yaml'
import puppeteer from '../../../lib/puppeteer/puppeteer.js'
import { Config } from './config.js'
import { speakers as vitsRoleList } from './tts.js'
import { supportConfigurations as voxRoleList } from './tts/voicevox.js'
import { supportConfigurations as azureRoleList } from './tts/microsoft-azure.js'
import { convertSpeaker, generateVitsAudio, speakers as vitsRoleList } from './tts.js'
import VoiceVoxTTS, { supportConfigurations as voxRoleList } from './tts/voicevox.js'
import AzureTTS, { supportConfigurations as azureRoleList } from './tts/microsoft-azure.js'
import { translate } from './translate.js'
import uploadRecord from './uploadRecord.js'
// export function markdownToText (markdown) {
// return remark()
// .use(stripMarkdown)
@ -702,11 +704,11 @@ export async function getUserData (user) {
}
export function getVoicevoxRoleList () {
return voxRoleList.map(item => item.name).join('')
return voxRoleList.map(item => item.name).join(',')
}
export function getAzureRoleList () {
return azureRoleList.map(item => item.name).join('、')
return azureRoleList.map(item => item.roleInfo + (item?.emotion ? '-> 支持:' + Object.keys(item.emotion).join('') + ' 情绪。' : '')).join('\n\n')
}
export async function getVitsRoleList (e) {
@ -778,18 +780,6 @@ export async function getImageOcrText (e) {
return false
}
}
// 对原始黑白名单进行去重和去除无效群号处理,并处理通过锅巴面板添加错误配置时可能导致的问题
export function processList (whitelist, blacklist) {
whitelist = Array.isArray(whitelist)
? whitelist
: String(whitelist).split(/[,]/)
blacklist = !Array.isArray(blacklist)
? blacklist
: String(blacklist).split(/[,]/)
whitelist = Array.from(new Set(whitelist)).filter(value => /^\^?[1-9]\d{5,9}$/.test(value))
blacklist = Array.from(new Set(blacklist)).filter(value => /^\^?[1-9]\d{5,9}$/.test(value))
return [whitelist, blacklist]
}
export function getMaxModelTokens (model = 'gpt-3.5-turbo') {
if (model.startsWith('gpt-3.5-turbo')) {
@ -806,3 +796,137 @@ export function getMaxModelTokens (model = 'gpt-3.5-turbo') {
}
}
}
/**
* 生成当前语音模式下可发送的音频信息
* @param e - 上下文对象
* @param pendingText - 待处理文本
* @param speakingEmotion - AzureTTSMode中的发言人情绪
* @param emotionDegree - AzureTTSMode中的发言人情绪强度
* @returns {Promise<{file: string, type: string}|undefined|boolean>}
*/
export async function generateAudio (e, pendingText, speakingEmotion, emotionDegree = 1) {
if (!Config.ttsSpace && !Config.azureTTSKey && !Config.voicevoxSpace) return false
let wav
const speaker = getUserSpeaker(await getUserReplySetting(e))
try {
if (Config.ttsMode === 'vits-uma-genshin-honkai' && Config.ttsSpace) {
if (Config.autoJapanese) {
try {
pendingText = await translate(pendingText, '日')
} catch (err) {
logger.warn(err.message + '\n将使用原始文本合成语音...')
return false
}
}
wav = await generateVitsAudio(pendingText, speaker, '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)')
} else if (Config.ttsMode === 'azure' && Config.azureTTSKey) {
return await generateAzureAudio(pendingText, speaker, speakingEmotion, emotionDegree)
} else if (Config.ttsMode === 'voicevox' && Config.voicevoxSpace) {
pendingText = (await translate(pendingText, '日')).replace('\n', '')
wav = await VoiceVoxTTS.generateAudio(pendingText, {
speaker
})
}
} catch (err) {
logger.error(err)
return false
}
let sendable
try {
try {
sendable = await uploadRecord(wav, Config.ttsMode)
if (sendable) {
await e.reply(sendable)
} else {
// 如果合成失败尝试使用ffmpeg合成
sendable = segment.record(wav)
}
} catch (err) {
logger.error(err)
sendable = segment.record(wav)
}
} catch (err) {
logger.error(err)
return false
}
if (Config.ttsMode === 'azure' && Config.azureTTSKey) {
// 清理文件
try {
fs.unlinkSync(wav)
} catch (err) {
logger.warn(err)
}
}
return sendable
}
/**
* 生成可发送的AzureTTS音频
* @param pendingText - 待转换文本
* @param role - 发言人
* @param speakingEmotion - 发言人情绪
* @param emotionDegree - 发言人情绪强度
* @returns {Promise<{file: string, type: string}|boolean>}
*/
export async function generateAzureAudio (pendingText, role = '随机', speakingEmotion, emotionDegree = 1) {
if (!Config.azureTTSKey) return false
let speaker
try {
if (role !== '随机') {
// 判断传入的是不是code
if (azureRoleList.find(s => s.code === role.trim())) {
speaker = role
} else {
speaker = azureRoleList.find(s => s.roleInfo.includes(role.trim()))
if (!speaker) {
logger.warn('找不到名为' + role + '的发言人,将使用默认发言人 晓晓 发送音频.')
speaker = 'zh-CN-XiaoxiaoNeural'
} else {
speaker = speaker.code
}
}
let languagePrefix = azureRoleList.find(config => config.code === speaker).languageDetail.charAt(0)
languagePrefix = languagePrefix.startsWith('E') ? '英' : languagePrefix
pendingText = (await translate(pendingText, languagePrefix)).replace('\n', '')
} else {
let role, languagePrefix
role = azureRoleList[Math.floor(Math.random() * azureRoleList.length)]
speaker = role.code
languagePrefix = role.languageDetail.charAt(0).startsWith('E') ? '英' : role.languageDetail.charAt(0)
pendingText = (await translate(pendingText, languagePrefix)).replace('\n', '')
if (role?.emotion) {
const keys = Object.keys(role.emotion)
speakingEmotion = keys[Math.floor(Math.random() * keys.length)]
}
emotionDegree = 2
logger.info('using speaker: ' + speaker)
logger.info('using language: ' + languagePrefix)
logger.info('using emotion: ' + speakingEmotion)
}
let ssml = AzureTTS.generateSsml(pendingText, {
speaker,
emotion: speakingEmotion,
pendingText,
emotionDegree
})
return await uploadRecord(
await AzureTTS.generateAudio(pendingText, {
speaker
}, await ssml)
, Config.ttsMode
)
} catch (err) {
logger.error(err)
return false
}
}
export function getUserSpeaker (userSetting) {
if (Config.ttsMode === 'vits-uma-genshin-honkai') {
return convertSpeaker(userSetting.ttsRole || Config.defaultTTSRole)
} else if (Config.ttsMode === 'azure') {
return userSetting.ttsRoleAzure || Config.azureTTSSpeaker
} else if (Config.ttsMode === 'voicevox') {
return userSetting.ttsRoleVoiceVox || Config.voicevoxTTSSpeaker
}
}

View file

@ -1,4 +1,5 @@
import { AbstractTool } from './AbstractTool.js'
import cfg from '../../../../lib/config/config.js'
export class EditCardTool extends AbstractTool {
name = 'editCard'
@ -23,13 +24,20 @@ export class EditCardTool extends AbstractTool {
description = '当你想要修改某个群员的群名片时有用。输入应该是群号、qq号和群名片用空格隔开。'
func = async function (opts) {
let {qq, card, groupId} = opts
groupId = parseInt(groupId.trim())
qq = parseInt(qq.trim())
logger.info('edit card: ', groupId, qq)
func = async function (opts, e) {
let { qq, card, groupId } = opts
qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
groupId = isNaN(groupId) || !groupId ? e.group_id : parseInt(groupId.trim())
let group = await Bot.pickGroup(groupId)
let mm = await group.getMemberMap()
if (!mm.has(qq)) {
return `failed, the user ${qq} is not in group ${groupId}`
}
if (mm.get(Bot.uin).role === 'member') {
return `failed, you, not user, don't have permission to edit card in group ${groupId}`
}
logger.info('edit card: ', groupId, qq)
await group.setCard(qq, card)
return `the user ${qq}'s card has been changed into ${card}`
}
}
}

View file

@ -0,0 +1,45 @@
import { AbstractTool } from './AbstractTool.js'
export class EliMovieTool extends AbstractTool {
name = 'currentHotMovies'
parameters = {
properties: {
yesOrNo: {
type: 'string',
description: 'do you want to check?'
}
},
required: ['yesOrNo']
}
description = 'Useful when you want to check out the current hot movies'
func = async function (opts, e) {
let { yesOrNo } = opts
if (yesOrNo === 'no') {
return 'tell user why you don\'t want to check'
}
if (e.at === Bot.uin) {
e.at = null
}
e.atBot = false
let avocado
try {
// eslint-disable-next-line camelcase
let { AvocadoRuleALL } = await import('../../../avocado-plugin/apps/avocado.js')
avocado = new AvocadoRuleALL(e)
} catch (err1) {
return 'the user didn\'t install avocado-plugin. suggest him to install'
}
try {
// eslint-disable-next-line new-cap
e.senderFromChatGpt = e.sender.user_id
await avocado.avocadoMovie(e)
return 'notify the user that the movie has been sent to them and they can obtain more information by sending commands displayed in the picture. you dont need to search for additional information to reply! just simply inform them that you have completed your task!!!'
} catch (err) {
logger.warn(err)
return 'failed due to unknown error'
}
}
}

View file

@ -0,0 +1,82 @@
import { AbstractTool } from './AbstractTool.js'
export class EliMusicTool extends AbstractTool {
name = 'musicTool'
parameters = {
properties: {
keyword: {
type: 'string',
description: 'Not necessarily a songName, it can be some descriptive words, but does not include the singer\'s name! can be left blank!'
},
singer: {
type: 'string',
description: 'Singer name, multiple singers are separated by \',\'! can be left blank!'
},
isRandom: {
type: 'boolean',
description: 'Whether to randomly select songs, default is false'
},
isRandom2: {
type: 'boolean',
description: 'when isRandom is true and neither singer nor songName is specified!!!, this value is true; otherwise, it is false. The default value is false.'
},
isHot: {
type: 'boolean',
description: 'Whether it\'s related to \'hot\', consider filling in this item when there is no song name, can be left blank'
},
singerRegion: {
type: 'string',
description: 'Whether it\'s related to \'hot\', consider filling in this item when there is no song name, can be left blank'
},
isRelax: {
type: 'boolean',
description: 'Fill in when the user wants to sleep or rest, leave others blank when filling in this item, default is false'
}
},
required: ['keyword', 'singer', 'isRandom', 'singerRegion, isRelax', 'isRandom2']
}
description = 'It is very useful when you want to meet the music needs or of users or when users want to sleep or unwind., so you should use this tool as much as possible, regardless of whether I asked you before!'
func = async function (opts, e) {
let { keyword, singer, isRandom, isHot, singerRegion, isRelax, isRandom2 } = opts
let avocado
try {
let { AvocadoMusic } = await import('../../../avocado-plugin/apps/avocadoMusic.js')
avocado = new AvocadoMusic(e)
} catch (err) {
return 'the user didn\'t install avocado-plugin. suggest him to install'
}
try {
// 不听话的gpt
isRandom2 = !keyword && isRandom && !isRandom2 && !singer
if (isRandom2) {
try {
singer = await redis.get(`AVOCADO:MUSIC_${e.sender.user_id}_FAVSINGER`)
if (!singer) throw new Error('no favorite singer')
singer = JSON.parse(singer).singer
logger.warn(singer)
} catch (err) {
return 'the user didn\'t set a favorite singer. Suggest setting it through the command \'#设置歌手+歌手名称\'!'
}
e.msg = '#鳄梨酱#随机' + singer
} else if (isRelax) {
e.msg = '#鳄梨酱#随机放松'
} else if (singerRegion) {
e.msg = '#鳄梨酱#' + (isRandom ? '随机' : '') + (isHot ? '热门' : '') + singerRegion + '歌手'
} else {
e.msg = '#鳄梨酱#' + (isRandom ? '随机' : '') + (isHot ? '热门' : '') + (singer ? singer + (keyword ? ',' + keyword : '') : keyword)
}
e.senderFromChatGpt = e.sender.user_id
await avocado.pickMusic(e)
if (isRandom2) {
return 'tell the user that a random song by his favorite artist has been sent to him! you don\'t need to find other info!'
} else {
return 'tell user that the response of his request has been sent to the user! you don\'t need to find other info!'
}
} catch (e) {
return `music share failed: ${e}`
}
}
}

View file

@ -24,8 +24,9 @@ export class ImageCaptionTool extends AbstractTool {
description = 'useful when you want to know what is inside a photo, such as user\'s avatar or other pictures'
func = async function (opts) {
func = async function (opts, e) {
let { imgUrl, qq, question } = opts
if (isNaN(qq) || !qq) qq = e.sender.user_id
if (!imgUrl && qq) {
imgUrl = `https://q1.qlogo.cn/g?b=qq&s=160&nk=${qq}`
}

View file

@ -25,9 +25,20 @@ export class JinyanTool extends AbstractTool {
required: ['groupId', 'time']
}
func = async function (opts) {
func = async function (opts, e) {
let { qq, groupId, time = '600', sender, isAdmin, isPunish } = opts
groupId = isNaN(groupId) || !groupId ? e.group_id : parseInt(groupId.trim())
qq = qq !== 'all'
? isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
: 'all'
let group = await Bot.pickGroup(groupId)
let m = await group.getMemberMap()
if (!m.has(qq)) {
return `failed, the user ${qq} is not in group ${groupId}`
}
if (m.get(Bot.uin).role === 'member') {
return `failed, you, not user, don't have permission to edit card in group ${groupId}`
}
time = parseInt(time.trim())
if (time < 60 && time !== 0) {
time = 60
@ -36,17 +47,16 @@ export class JinyanTool extends AbstractTool {
time = 86400 * 30
}
if (isAdmin) {
if (qq.trim() === 'all') {
if (qq === 'all') {
return 'you cannot mute all because the master doesn\'t allow it'
} else {
qq = parseInt(qq.trim())
qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
await group.muteMember(qq, time)
}
} else {
if (qq.trim() === 'all') {
if (qq === 'all') {
return 'the user is not admin, he can\'t mute all. the user should be punished'
} else if (qq == sender) {
qq = parseInt(qq.trim())
await group.muteMember(qq, time)
} else {
return 'the user is not admin, he can\'t let you mute other people.'

View file

@ -22,10 +22,10 @@ export class KickOutTool extends AbstractTool {
required: ['groupId']
}
func = async function (opts) {
func = async function (opts, e) {
let { qq, groupId, sender, isAdmin, isPunish } = opts
groupId = parseInt(groupId.trim())
qq = parseInt(qq.trim())
qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
groupId = isNaN(groupId) || !groupId ? e.group_id : parseInt(groupId.trim())
if (!isAdmin && sender != qq) {
return 'the user is not admin, he cannot kickout other people. he should be punished'
}

View file

@ -9,7 +9,7 @@ export class ProcessPictureTool extends AbstractTool {
type: {
type: 'string',
enum: ['Image2Hed', 'Image2Scribble'],
description: 'how to process it. Image2Hed: useful when you want to detect the soft hed boundary of the image; Image2Scribble: useful when you want to generate a scribble of the image'
description: 'how to process it. Image2Hed: useful when you want to detect the soft hed boundary of the picture; Image2Scribble: useful when you want to generate a scribble of the picture'
},
qq: {
type: 'string',
@ -23,9 +23,9 @@ export class ProcessPictureTool extends AbstractTool {
required: ['type']
}
description = 'useful when you want to know what is inside a photo, such as user\'s avatar or other pictures'
description = 'useful when you want to process a picture or user\'s avatar.'
func = async function (opts) {
func = async function (opts, e) {
let { url, qq, type } = opts
if (qq) {
url = `https://q1.qlogo.cn/g?b=qq&s=160&nk=${qq}`
@ -57,7 +57,7 @@ export class ProcessPictureTool extends AbstractTool {
})
if (captionRes.status === 200) {
let result = await captionRes.text()
return `the processed image url is ${Config.extraUrl}${result}`
return `the processed image url is ${Config.extraUrl}${result}${qq ? ' and ' + url : ''}. you should send it with SendPictureTool.`
} else {
return 'error happened'
}

View file

@ -23,6 +23,7 @@ export class QueryGenshinTool extends AbstractTool {
func = async function (opts, e) {
let { qq, uid = '', character = '' } = opts
qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
if (e.at === Bot.uin) {
e.at = null
}

View file

@ -23,6 +23,7 @@ export class QueryStarRailTool extends AbstractTool {
func = async function (opts, e) {
let { qq, uid, character } = opts
qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
if (e.at === Bot.uin) {
e.at = null
}

View file

@ -2,7 +2,7 @@ import { AbstractTool } from './AbstractTool.js'
import { getMasterQQ } from '../common.js'
export class QueryUserinfoTool extends AbstractTool {
name = 'sendDice'
name = 'queryUserinfo'
parameters = {
properties: {
@ -16,9 +16,10 @@ export class QueryUserinfoTool extends AbstractTool {
func = async function (opts, e) {
let { qq } = opts
if (e.is_group && typeof e.group.getMemberMap === 'function') {
let mm = e.group.getMemberMap()
let user = mm.get(parseInt(qq) || e.sender.user_id)
qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
if (e.isGroup && typeof e.group.getMemberMap === 'function') {
let mm = await e.group.getMemberMap()
let user = mm.get(qq) || e.sender.user_id
let master = (await getMasterQQ())[0]
let prefix = ''
if (qq != master) {

View file

@ -26,5 +26,5 @@ export class SerpImageTool extends AbstractTool {
return `the images search results are here in json format:\n${JSON.stringify(res)}. the murl field is real picture url. You should use sendPicture to send them`
}
description = 'Useful when you want to search images from the internet. '
description = 'Useful when you want to search images from the internet.'
}

View file

@ -8,7 +8,7 @@ export class SearchMusicTool extends AbstractTool {
properties: {
keyword: {
type: 'string',
description: '音乐的标题或关键词'
description: '音乐的标题或关键词, 可以是歌曲名或歌曲名+歌手名的组合'
}
},
required: ['keyword']

View file

@ -0,0 +1,122 @@
import { AbstractTool } from './AbstractTool.js'
import { generateVitsAudio } from '../tts.js'
import { Config } from '../config.js'
import { generateAudio, generateAzureAudio } from '../common.js'
import VoiceVoxTTS from '../tts/voicevox.js'
import uploadRecord from '../uploadRecord.js'
export class SendAudioMessageTool extends AbstractTool {
name = 'sendAudioMessage'
parameters = {
properties: {
pendingText: {
type: 'string',
description: 'Message to be sent and it will be turned into audio message'
},
ttsMode: {
type: 'number',
description: 'default is 1, which indicates that the text will be processed in the current ttsMode.' +
'2 is azureMode.' +
'3 or 4 corresponds to vitsMode or voxMode.'
},
vitsModeRole: {
type: 'string',
description: 'use whose voice',
enum: ['琴', '空',
'丽莎', '荧', '芭芭拉', '凯亚', '迪卢克', '雷泽', '安柏', '温迪',
'香菱', '北斗', '行秋', '魈', '凝光', '可莉', '钟离', '菲谢尔(皇女)',
'班尼特', '达达利亚(公子)', '诺艾尔(女仆)', '七七', '重云', '甘雨(椰羊)',
'阿贝多', '迪奥娜(猫猫)', '莫娜', '刻晴', '砂糖', '辛焱', '罗莎莉亚',
'胡桃', '枫原万叶(万叶)', '烟绯', '宵宫', '托马', '优菈', '雷电将军(雷神)',
'早柚', '珊瑚宫心海', '五郎', '九条裟罗', '荒泷一斗',
'埃洛伊', '申鹤', '八重神子', '神里绫人(绫人)', '夜兰', '久岐忍',
'鹿野苑平藏', '提纳里', '柯莱', '多莉', '云堇', '纳西妲(草神)', '深渊使徒',
'妮露', '赛诺']
},
azureModeRole: {
type: 'string',
description: 'can be \'随机\' or specified by the user. default is currentRole.'
},
voxModeRole: {
type: 'string',
description: 'can be random or currentRole or specified by the user. default is currentRole.'
},
speakingEmotion: {
type: 'string',
description: 'specified by the user. default is blank.'
},
speakingEmotionDegree: {
type: 'number',
description: 'specified by the user. default is blank.'
},
targetGroupIdOrQQNumber: {
type: 'string',
description: 'Fill in the target user\'s qq number or groupId when you need to send audio message to specific user or group, otherwise leave blank'
}
},
required: ['pendingText', 'ttsMode', 'targetGroupIdOrQQNumber']
}
description = 'This tool is used to send voice|audio messages, utilize it only if the user grants you permission to do so.'
func = async function (opts, e) {
if (!Config.ttsSpace && !Config.azureTTSKey && !Config.voicevoxSpace) {
return 'you don\'t have permission to send audio message due to a lack of a valid ttsKey'
}
let { pendingText, ttsMode, vitsModeRole, azureModeRole, voxModeRole, speakingEmotion, speakingEmotionDegree, targetGroupIdOrQQNumber } = opts
let sendable
ttsMode = isNaN(ttsMode) || !ttsMode ? 1 : ttsMode
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? e.isGroup ? e.group_id : e.sender.user_id
: parseInt(targetGroupIdOrQQNumber.trim())
try {
switch (ttsMode) {
case 1:
sendable = await generateAudio(e, pendingText, speakingEmotion)
break
case 2:
if (!Config.azureTTSKey) return 'audio generation failed, due to a lack of a azureTTSKey'
sendable = await generateAzureAudio(pendingText, azureModeRole, speakingEmotion, speakingEmotionDegree)
break
case 3:
if (!Config.ttsSpace) return 'audio generation failed, due to a lack of a ttsSpace'
sendable = await uploadRecord(
await generateVitsAudio(pendingText, vitsModeRole, '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)')
, 'vits-uma-genshin-honkai'
)
break
case 4:
if (!Config.voicevoxSpace) return 'audio generation failed, due to a lack of a voicevoxSpace'
sendable = await uploadRecord(
await VoiceVoxTTS.generateAudio(pendingText, voxModeRole)
, 'voicevox'
)
break
default:
sendable = await generateAzureAudio(pendingText, azureModeRole, speakingEmotion, speakingEmotionDegree)
}
} catch (err) {
logger.error(err)
return `audio generation failed, error: ${JSON.stringify(err)}`
}
if (sendable) {
let groupList = await Bot.getGroupList()
try {
if (groupList.get(target)) {
let group = await Bot.pickGroup(target)
await group.sendMsg(sendable)
return 'audio has been sent to group' + target
} else {
let user = await Bot.pickFriend(target)
await user.sendMsg(sendable)
return 'audio has been sent to user' + target
}
} catch (err) {
return `failed to send audio, error: ${JSON.stringify(err)}`
}
} else {
return 'audio generation failed'
}
}
}

View file

@ -7,27 +7,34 @@ export class SendAvatarTool extends AbstractTool {
properties: {
qq: {
type: 'string',
description: '要发头像的人的qq号'
description: 'if you need to send avatar of a user, input his qq.If there are multiple qq, separate them with a space'
},
groupId: {
targetGroupIdOrQQNumber: {
type: 'string',
description: '群号或qq号发送目标'
description: 'Fill in the target user\'s qq number or groupId when you need to send avatar to specific user or group, otherwise leave blank'
}
},
required: ['qq', 'groupId']
required: ['qq', 'targetGroupIdOrQQNumber']
}
func = async function (opts) {
let { qq, groupId } = opts
let groupList = await Bot.getGroupList()
groupId = parseInt(groupId.trim())
console.log('sendAvatar', groupId, qq)
if (groupList.get(groupId)) {
let group = await Bot.pickGroup(groupId)
await group.sendMsg(segment.image('https://q1.qlogo.cn/g?b=qq&s=0&nk=' + qq))
func = async function (opts, e) {
let { qq, targetGroupIdOrQQNumber } = opts
const pictures = qq.split(/[,\s]/).filter(qq => !isNaN(qq.trim()) && qq.trim()).map(qq => segment.image('https://q1.qlogo.cn/g?b=qq&s=0&nk=' + parseInt(qq.trim())))
if (!pictures.length) {
return 'there is no valid qq'
}
return `the user ${qq}'s avatar has been sent to group ${groupId}`
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? e.isGroup ? e.group_id : e.sender.user_id
: parseInt(targetGroupIdOrQQNumber.trim())
let groupList = await Bot.getGroupList()
console.log('sendAvatar', target, pictures)
if (groupList.get(target)) {
let group = await Bot.pickGroup(target)
await group.sendMsg(pictures)
}
return `the ${pictures.length > 1 ? 'users: ' + qq + '\'s avatar' : 'avatar'} has been sent to group ${target}`
}
description = 'Useful when you want to send the user avatar picture to the group. The input to this tool should be the user\'s qq number and the target group number, and they should be concated with a space. 如果是在群聊中,优先选择群号发送。'
description = 'Useful when you want to send the user avatar to the group. Note that if you want to process user\'s avatar, it is advisable to utilize the ProcessPictureTool and input the qq of target user.'
}

View file

@ -12,21 +12,24 @@ export class SendVideoTool extends AbstractTool {
type: 'string',
description: '要发的视频的id'
},
groupId: {
targetGroupIdOrQQNumber: {
type: 'string',
description: '群号或qq号发送目标为空则发送到当前聊天'
description: 'Fill in the target user\'s qq number or groupId when you need to send video to specific user or group, otherwise leave blank'
}
},
required: ['id']
}
func = async function (opts) {
let { id, groupId } = opts
groupId = parseInt(groupId.trim())
func = async function (opts, e) {
let { id, targetGroupIdOrQQNumber } = opts
// 非法值则发送到当前群聊或私聊
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? e.isGroup ? e.group_id : e.sender.user_id
: parseInt(targetGroupIdOrQQNumber.trim())
let msg = []
try {
let { arcurl, title, pic, description, videoUrl, headers, bvid, author, play, pubdate, like, honor } = await getBilibili(id)
let group = await Bot.pickGroup(groupId)
let group = await Bot.pickGroup(target)
msg.push(title.replace(/(<([^>]+)>)/ig, '') + '\n')
msg.push(`UP主${author} 发布日期:${formatDate(new Date(pubdate * 1000))} 播放量:${play} 点赞:${like}\n`)
msg.push(arcurl + '\n')
@ -47,7 +50,7 @@ export class SendVideoTool extends AbstractTool {
await fs.writeFileSync(fileLoc, buffer)
await group.sendMsg(segment.video(fileLoc))
})
return `the video ${title.replace(/(<([^>]+)>)/ig, '')} was shared to ${groupId}. the video information: ${msg}`
return `the video ${title.replace(/(<([^>]+)>)/ig, '')} was shared to ${target}. the video information: ${msg}`
} catch (err) {
logger.error(err)
if (msg.length > 0) {
@ -58,7 +61,7 @@ export class SendVideoTool extends AbstractTool {
}
}
description = 'Useful when you want to share a video. You must use searchVideo to get search result and choose one video and get its id'
description = 'Useful when you are allowed to send a video. You must use searchVideo to get search result and choose one video and get its id'
}
export async function getBilibili (bvid) {
@ -103,7 +106,7 @@ export async function getBilibili (bvid) {
let pubdate = videoInfo.data.pubdate
let like = videoInfo.data.stat.like
let honor = videoInfo.data.honor_reply?.honor?.map(h => h.desc)?.join('、')
let downloadInfo = await fetch(`https://api.bilibili.com/x/player/playurl?bvid=${bvid}&cid=${cid}`, {headers})
let downloadInfo = await fetch(`https://api.bilibili.com/x/player/playurl?bvid=${bvid}&cid=${cid}`, { headers })
let videoUrl = (await downloadInfo.json()).data.durl[0].url
return {
arcurl, title, pic, description, videoUrl, headers, bvid, author, play, pubdate, like, honor
@ -133,4 +136,4 @@ function randomIndex () {
}
}
console.log('send bilibili')
// console.log('send bilibili')

View file

@ -1,5 +1,4 @@
import {AbstractTool} from "./AbstractTool.js";
import { AbstractTool } from './AbstractTool.js'
export class SendDiceTool extends AbstractTool {
name = 'sendDice'
@ -10,26 +9,38 @@ export class SendDiceTool extends AbstractTool {
type: 'number',
description: '骰子的数量'
},
groupId: {
targetGroupIdOrQQNumber: {
type: 'string',
description: '群号或qq号发送目标'
description: 'Fill in the target qq number or groupId when you need to send Dice to specific user or group, otherwise leave blank'
}
},
required: ['num', 'groupId']
required: ['num', 'targetGroupIdOrQQNumber']
}
func = async function (opts) {
let {num, groupId} = opts
func = async function (opts, e) {
let { num, targetGroupIdOrQQNumber } = opts
// 非法值则发送到当前群聊或私聊
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? e.isGroup ? e.group_id : e.sender.user_id
: parseInt(targetGroupIdOrQQNumber.trim())
let groupList = await Bot.getGroupList()
if (groupList.get(groupId)) {
let group = await Bot.pickGroup(groupId, true)
await group.sendMsg(segment.dice(num))
num = isNaN(num) || !num ? 1 : num > 5 ? 5 : num
if (groupList.get(target)) {
let group = await Bot.pickGroup(target, true)
for (let i = 0; i < num; i++) {
await group.sendMsg(segment.dice())
}
} else {
let friend = await Bot.pickFriend(groupId)
await friend.sendMsg(segment.dice(num))
let friend = await Bot.pickFriend(target)
await friend.sendMsg(segment.dice())
}
if (num === 5) {
logger.warn(1)
return 'tell the user that in order to avoid spamming the chat, only five dice are sent this time, and warn him not to use this tool to spamming the chat, otherwise you will use JinyanTool to punish him'
} else {
return 'the dice has been sent'
}
return `the dice has been sent`
}
description = 'If you want to roll dice, use this tool. If you know the group number, use the group number instead of the qq number first. The input should be the number of dice to be cast (1-6) and the target group number or qq numberand they should be concat with a space'
description = 'If you want to roll dice, use this tool. Be careful to check that the targetGroupIdOrQQNumber is correct. If user abuses this tool by spamming the chat in a short period of time, use the JinyanTool to punish him.'
}

View file

@ -0,0 +1,44 @@
import { AbstractTool } from './AbstractTool.js'
import { convertFaces } from '../face.js'
export class SendMessageToSpecificGroupOrUserTool extends AbstractTool {
name = 'sendMessageToSpecificGroupOrUser'
parameters = {
properties: {
msg: {
type: 'string',
description: 'Message text to be sent'
},
targetGroupIdOrQQNumber: {
type: 'string',
description: 'Fill in the target user\'s qq number or groupId when you need to send specific message to specific user or group, otherwise leave blank'
}
},
required: ['msg', 'msgType', 'targetGroupIdOrQQNumber']
}
func = async function (opt, e) {
let { msg, targetGroupIdOrQQNumber } = opt
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? e.isGroup ? e.group_id : e.sender.user_id
: parseInt(targetGroupIdOrQQNumber.trim())
let groupList = await Bot.getGroupList()
try {
if (groupList.get(target)) {
let group = await Bot.pickGroup(target)
await group.sendMsg(await convertFaces(msg, true, e))
return 'msg has been sent to group' + target
} else {
let user = await Bot.pickFriend(target)
await user.sendMsg(msg)
return 'msg has been sent to user' + target
}
} catch (err) {
return `failed to send msg, error: ${JSON.stringify(err)}`
}
}
description = 'Useful when you want to send a text message to specific user or group'
}

View file

@ -9,21 +9,25 @@ export class SendMusicTool extends AbstractTool {
type: 'string',
description: '音乐的id'
},
groupId: {
targetGroupIdOrQQNumber: {
type: 'string',
description: '群号或qq号发送目标为空则发送到当前聊天'
description: 'Fill in the target user_id or groupId when you need to send music to specific group or user, otherwise leave blank'
}
},
required: ['keyword']
}
func = async function (opts) {
let { id, groupId } = opts
groupId = parseInt(groupId.trim())
func = async function (opts, e) {
let { id, targetGroupIdOrQQNumber } = opts
// 非法值则发送到当前群聊
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? e.group_id
: parseInt(targetGroupIdOrQQNumber.trim())
try {
let group = await Bot.pickGroup(groupId)
let group = await Bot.pickGroup(target)
await group.shareMusic('163', id)
return `the music has been shared to ${groupId}`
return `the music has been shared to ${target}`
} catch (e) {
return `music share failed: ${e}`
}

View file

@ -5,46 +5,47 @@ export class SendPictureTool extends AbstractTool {
parameters = {
properties: {
picture: {
urlOfPicture: {
type: 'string',
description: 'the url of the pictures, split with space if more than one.'
description: 'the url of the pictures, not text, split with space if more than one. can be left blank.'
},
qq: {
targetGroupIdOrQQNumber: {
type: 'string',
description: 'if you want to send avatar of a user, input his qq number.'
},
groupId: {
type: 'string',
description: '群号或qq号发送目标为空则发送到当前聊天'
description: 'Fill in the target user\'s qq number or groupId when you need to send picture to specific user or group, otherwise leave blank'
}
},
required: ['picture']
required: ['urlOfPicture', 'targetGroupIdOrQQNumber']
}
func = async function (opt) {
let { picture, groupId, qq } = opt
if (qq) {
let avatar = `https://q1.qlogo.cn/g?b=qq&s=0&nk=${qq}`
picture += (' ' + avatar)
func = async function (opt, e) {
let { urlOfPicture, targetGroupIdOrQQNumber } = opt
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? e.isGroup ? e.group_id : e.sender.user_id
: parseInt(targetGroupIdOrQQNumber.trim())
// 处理错误url和picture留空的情况
const urlRegex = /(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:((?:(?:[a-z0-9\u00a1-\u4dff\u9fd0-\uffff][a-z0-9\u00a1-\u4dff\u9fd0-\uffff_-]{0,62})?[a-z0-9\u00a1-\u4dff\u9fd0-\uffff]\.)+(?:[a-z\u00a1-\u4dff\u9fd0-\uffff]{2,}\.?))(?::\d{2,5})?)(?:\/[\w\u00a1-\u4dff\u9fd0-\uffff$-_.+!*'(),%]+)*(?:\?(?:[\w\u00a1-\u4dff\u9fd0-\uffff$-_.+!*(),%:@&=]|(?:[\[\]])|(?:[\u00a1-\u4dff\u9fd0-\uffff]))*)?(?:#(?:[\w\u00a1-\u4dff\u9fd0-\uffff$-_.+!*'(),;:@&=]|(?:[\[\]]))*)?\/?/i
if (/https:\/\/example.com/.test(urlOfPicture) || !urlOfPicture || !urlRegex.test(urlOfPicture)) urlOfPicture = ''
if (!urlOfPicture) {
return 'Because there is no correct URL for the picture ,tell user the reason and ask user if he want to use SearchImageTool'
}
let pictures = picture.trim().split(' ')
let pictures = urlOfPicture.trim().split(' ')
logger.mark('pictures to send: ', pictures)
pictures = pictures.map(img => segment.image(img))
let groupList = await Bot.getGroupList()
groupId = parseInt(groupId)
try {
if (groupList.get(groupId)) {
let group = await Bot.pickGroup(groupId)
if (groupList.get(target)) {
let group = await Bot.pickGroup(target)
await group.sendMsg(pictures)
return `picture has been sent to group ${groupId}`
return 'picture has been sent to group' + target
} else {
let user = await Bot.pickFriend(groupId)
let user = await Bot.pickFriend(target)
await user.sendMsg(pictures)
return `picture has been sent to user ${groupId}`
return 'picture has been sent to user' + target
}
} catch (err) {
return `failed to send pictures, error: ${JSON.stringify(err)}`
}
}
description = 'Useful when you want to send one or more pictures. '
description = 'Useful when you want to send one or more pictures.'
}

View file

@ -1,4 +1,4 @@
import {AbstractTool} from "./AbstractTool.js";
import { AbstractTool } from './AbstractTool.js'
export class SendRPSTool extends AbstractTool {
name = 'sendRPS'
@ -8,20 +8,24 @@ export class SendRPSTool extends AbstractTool {
type: 'number',
description: '石头剪刀布的代号'
},
groupId: {
targetGroupIdOrQQNumber: {
type: 'string',
description: '群号或qq号发送目标'
description: 'Fill in the target user_id or groupId when you need to send RPS to specific group or user'
},
required: ['num', 'groupId']
required: ['num', 'targetGroupIdOrUserQQNumber']
}
func = async function (num, groupId) {
func = async function (num, targetGroupIdOrQQNumber, e) {
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? e.isGroup ? e.group_id : e.sender.user_id
: parseInt(targetGroupIdOrQQNumber.trim())
let groupList = await Bot.getGroupList()
if (groupList.get(groupId)) {
let group = await Bot.pickGroup(groupId, true)
if (groupList.get(target)) {
let group = await Bot.pickGroup(target, true)
await group.sendMsg(segment.rps(num))
} else {
let friend = await Bot.pickFriend(groupId)
let friend = await Bot.pickFriend(target)
await friend.sendMsg(segment.rps(num))
}
}

View file

@ -36,5 +36,5 @@ export class SerpTool extends AbstractTool {
return `the search results are here in json format:\n${JSON.stringify(res)}`
}
description = 'Useful when you want to search something from the internet. If you don\'t know much about the user\'s question, just search about it! If you want to know details of a result, you can use website tool'
description = 'Useful when you want to search something from the internet. If you don\'t know much about the user\'s question, just search about it! If you want to know details of a result, you can use website tool! use it as much as you can!'
}

View file

@ -1,57 +0,0 @@
import { AbstractTool } from './AbstractTool.js'
import { convertSpeaker, generateAudio } from '../tts.js'
import uploadRecord from '../uploadRecord.js'
import { Config } from '../config.js'
export class TTSTool extends AbstractTool {
name = 'tts'
parameters = {
properties: {
text: {
type: 'string',
description: 'the text will be turned into audio'
},
role: {
type: 'string',
description: 'use whose voice',
enum: ['琴', '空',
'丽莎', '荧', '芭芭拉', '凯亚', '迪卢克', '雷泽', '安柏', '温迪',
'香菱', '北斗', '行秋', '魈', '凝光', '可莉', '钟离', '菲谢尔(皇女)',
'班尼特', '达达利亚(公子)', '诺艾尔(女仆)', '七七', '重云', '甘雨(椰羊)',
'阿贝多', '迪奥娜(猫猫)', '莫娜', '刻晴', '砂糖', '辛焱', '罗莎莉亚',
'胡桃', '枫原万叶(万叶)', '烟绯', '宵宫', '托马', '优菈', '雷电将军(雷神)',
'早柚', '珊瑚宫心海', '五郎', '九条裟罗', '荒泷一斗',
'埃洛伊', '申鹤', '八重神子', '神里绫人(绫人)', '夜兰', '久岐忍',
'鹿野苑平藏', '提纳里', '柯莱', '多莉', '云堇', '纳西妲(草神)', '深渊使徒',
'妮露', '赛诺']
},
groupId: {
type: 'string',
description: 'groupId'
}
},
required: ['text', 'role', 'groupId']
}
description = 'Useful when you want to turn text into audio and send it'
func = async function (opts) {
let { text, role, groupId } = opts
groupId = parseInt(groupId.trim())
try {
let wav = await generateAudio(text, convertSpeaker(role), '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)')
let sendable = await uploadRecord(wav, Config.ttsMode)
if (sendable) {
let group = await Bot.pickGroup(groupId)
await group.sendMsg(sendable)
return 'audio has been sent successfully'
} else {
return 'audio generation failed'
}
} catch (err) {
logger.error(err)
return 'audio generation failed'
}
}
}

View file

@ -43,14 +43,21 @@ export class WebsiteTool extends AbstractTool {
if (origin) {
Config.headless = false
}
// text = text.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
// .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
// .replace(/<head\b[^<]*(?:(?!<\/head>)<[^<]*)*<\/head>/gi, '')
// .replace(/<!--[\s\S]*?-->/gi, '')
text = text.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '') // 移除<style>标签及其内容
.replace(/<[^>]+style\s*=\s*(["'])(?:(?!\1).)*\1[^>]*>/gi, '') // 移除带有style属性的标签
.replace(/<[^>]+>/g, '')
text = text.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
.replace(/<head\b[^<]*(?:(?!<\/head>)<[^<]*)*<\/head>/gi, '')
.replace(/<figure\b[^<]*(?:(?!<\/figure>)<[^<]*)*<\/figure>/gi, '')
.replace(/<path\b[^<]*(?:(?!<\/path>)<[^<]*)*<\/path>/gi, '')
.replace(/<video\b[^<]*(?:(?!<\/video>)<[^<]*)*<\/video>/gi, '')
.replace(/<audio\b[^<]*(?:(?!<\/audio>)<[^<]*)*<\/audio>/gi, '')
.replace(/<img[^>]*>/gi, '')
.replace(/<!--[\s\S]*?-->/gi, '') // 去除注释
.replace(/<(?!\/?(title|ul|li|td|tr|thead|tbody|blockquote|h[1-6]|H[1-6])[^>]*)\w+\s+[^>]*>/gi, '') // 去除常见语音标签外的含属性标签
.replace(/<(\w+)(\s[^>]*)?>/gi, '<$1>') // 进一步去除剩余标签的属性
.replace(/<\/(?!\/?(title|ul|li|td|tr|thead|tbody|blockquote|h[1-6]|H[1-6])[^>]*)[a-z][a-z0-9]*>/gi, '') // 去除常见语音标签外的含属性结束标签
.replace(/[\n\r]/gi, '') // 去除回车换行
.replace(/\s{2}/g, '') // 多个空格只保留一个空格
.replace('<!DOCTYPE html>', '') // 去除<!DOCTYPE>声明
let maxModelTokens = getMaxModelTokens(Config.model)
text = text.slice(0, Math.min(text.length, maxModelTokens - 1600))
let api = new ChatGPTAPI({
@ -74,7 +81,7 @@ export class WebsiteTool extends AbstractTool {
},
maxModelTokens
})
const htmlContentSummaryRes = await api.sendMessage(`这是一个网页html经过筛选的内容请你进一步去掉其中的标签、样式、script等无用信息并从中提取出其中的主体内容转换成自然语言告诉我不需要主观描述性的语言${text}`)
const htmlContentSummaryRes = await api.sendMessage(`去除与主体内容无关的部分从中整理出主体内容并转换成md格式不需要主观描述性的语言与冗余的空白行${text}`)
let htmlContentSummary = htmlContentSummaryRes.text
return `this is the main content of website:\n ${htmlContentSummary}`
} catch (err) {

View file

@ -36,7 +36,18 @@ function randomNum (minNum, maxNum) {
return 0
}
}
export async function generateAudio (text, speaker = '随机', language = '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)', noiseScale = Config.noiseScale, noiseScaleW = Config.noiseScaleW, lengthScale = Config.lengthScale) {
/**
* 生成VitsTTSMode下的wav音频
* @param text
* @param speaker
* @param language
* @param noiseScale
* @param noiseScaleW
* @param lengthScale
* @returns {Promise<string>}
*/
export async function generateVitsAudio (text, speaker = '随机', language = '中日混合(中文用[ZH][ZH]包裹起来,日文用[JA][JA]包裹起来)', noiseScale = Config.noiseScale, noiseScaleW = Config.noiseScaleW, lengthScale = Config.lengthScale) {
if (!speaker || speaker === '随机') {
logger.info('随机角色!这次哪个角色这么幸运会被选到呢……')
speaker = speakers[randomNum(0, speakers.length)]

View file

@ -9,7 +9,15 @@ try {
} catch (err) {
logger.warn('未安装microsoft-cognitiveservices-speech-sdk无法使用微软Azure语音源')
}
async function generateAudio (text, option = {}, ssml = '') {
/**
* 生成AzureTTSMode下的wav音频
* @param pendingText - 待处理文本
* @param option
* @param ssml
* @returns {Promise<string>}
*/
async function generateAudio (pendingText, option = {}, ssml = '') {
if (!sdk) {
throw new Error('未安装microsoft-cognitiveservices-speech-sdk无法使用微软Azure语音源')
}
@ -22,7 +30,7 @@ async function generateAudio (text, option = {}, ssml = '') {
let audioConfig = sdk.AudioConfig.fromAudioFileOutput(filename)
let synthesizer
let speaker = option?.speaker || '随机'
let context = text
let context = pendingText
// 打招呼用
if (speaker === '随机') {
speaker = supportConfigurations[Math.floor(Math.random() * supportConfigurations.length)].code
@ -47,9 +55,9 @@ async function generateAudio (text, option = {}, ssml = '') {
return filename
}
async function speakTextAsync (synthesizer, text) {
async function speakTextAsync (synthesizer, pendingText) {
return new Promise((resolve, reject) => {
synthesizer.speakTextAsync(text, result => {
synthesizer.speakTextAsync(pendingText, result => {
if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) {
logger.info('speakTextAsync: true')
resolve()
@ -82,7 +90,7 @@ async function speakSsmlAsync (synthesizer, ssml) {
})
})
}
async function generateSsml (text, option = {}) {
async function generateSsml (pendingText, option = {}) {
let speaker = option?.speaker || '随机'
let emotionDegree, role, emotion
// 打招呼用
@ -104,7 +112,7 @@ async function generateSsml (text, option = {}) {
return `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="${speaker}">
${expressAs}${text}${expressAs ? '</mstts:express-as>' : ''}
${expressAs}${pendingText}${expressAs ? '</mstts:express-as>' : ''}
</voice>
</speak>`
}

View file

@ -24,6 +24,12 @@ const newFetch = (url, options = {}) => {
return fetch(url, mergedOptions)
}
/**
* 生成voxTTSMode下的wav音频
* @param text
* @param options
* @returns {Promise<Buffer>}
*/
async function generateAudio (text, options = {}) {
let host = Config.voicevoxSpace
let speaker = options.speaker || '随机'