Merge pull request #8 from ikechan8370/v2

v2
This commit is contained in:
ycxom 2025-02-17 02:01:51 +08:00 committed by GitHub
commit eddb7c7685
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 106 additions and 171 deletions

View file

@ -238,11 +238,11 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
// ANY要笑死人的效果
let mode = opt.toolMode || 'AUTO'
let lastFuncName = opt.functionResponse?.name
let lastFuncName = (/** @type {FunctionResponse[] | undefined}**/ opt.functionResponse)?.map(rsp => rsp.name)
const mustSendNextTurn = [
'searchImage', 'searchMusic', 'searchVideo'
]
if (lastFuncName && mustSendNextTurn.includes(lastFuncName)) {
if (lastFuncName && lastFuncName?.find(name => mustSendNextTurn.includes(name))) {
mode = 'ANY'
}
body.tool_config = {
@ -328,7 +328,7 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
let args = Object.assign(fc.args, {
isAdmin,
isOwner,
sender: this.e.sender,
sender: this.e.sender.user_id,
mode: 'gemini'
})
functionResponse.response.content = await chosenTool.func(args, this.e)

View file

@ -75,12 +75,12 @@ export function supportGuoba () {
bottomHelpMessage: '独立的后台管理面板默认3321端口与锅巴类似。工具箱会有额外占用启动速度稍慢酌情开启。修改后需重启生效',
component: 'Switch'
},
// {
// field: 'enableMd',
// label: 'QQ开启markdown',
// bottomHelpMessage: 'qq的第三方md非QQBot。需要适配器实现segment.markdown和segment.button方可使用否则不建议开启会造成各种错误。默认关闭',
// component: 'Switch'
// },
{
field: 'enableToolPrivateSend',
label: '允许智能模式私聊',
bottomHelpMessage: '是否允许智能模式下发起临时对话骚扰其他群友。默认开启如果怕Bot乱骚扰其他人可以关闭。主人不受影响。',
component: 'Switch'
},
{
field: 'translateSource',
label: '翻译来源',

View file

@ -29,7 +29,7 @@ import { SendAvatarTool } from '../utils/tools/SendAvatarTool.js'
import { SerpImageTool } from '../utils/tools/SearchImageTool.js'
import { SearchMusicTool } from '../utils/tools/SearchMusicTool.js'
import { SendMusicTool } from '../utils/tools/SendMusicTool.js'
import { SendAudioMessageTool } from '../utils/tools/SendAudioMessageTool.js'
// import { SendAudioMessageTool } from '../utils/tools/SendAudioMessageTool.js'
import { SendMessageToSpecificGroupOrUserTool } from '../utils/tools/SendMessageToSpecificGroupOrUserTool.js'
import { QueryGenshinTool } from '../utils/tools/QueryGenshinTool.js'
import { WeatherTool } from '../utils/tools/WeatherTool.js'
@ -42,11 +42,11 @@ import { SerpIkechan8370Tool } from '../utils/tools/SerpIkechan8370Tool.js'
import { SerpTool } from '../utils/tools/SerpTool.js'
import common from '../../../lib/common/common.js'
import { SendDiceTool } from '../utils/tools/SendDiceTool.js'
import { EliMovieTool } from '../utils/tools/EliMovieTool.js'
import { EliMusicTool } from '../utils/tools/EliMusicTool.js'
// import { EliMovieTool } from '../utils/tools/EliMovieTool.js'
// import { EliMusicTool } from '../utils/tools/EliMusicTool.js'
import { HandleMessageMsgTool } from '../utils/tools/HandleMessageMsgTool.js'
import { ProcessPictureTool } from '../utils/tools/ProcessPictureTool.js'
import { ImageCaptionTool } from '../utils/tools/ImageCaptionTool.js'
// import { ImageCaptionTool } from '../utils/tools/ImageCaptionTool.js'
import { ChatGPTAPI } from '../utils/openai/chatgpt-api.js'
import { newFetch } from '../utils/proxy.js'
import { ChatGLM4Client } from '../client/ChatGLM4Client.js'
@ -530,56 +530,10 @@ class Core {
option.image = base64Image.toString('base64')
}
if (opt.enableSmart) {
/**
* @type {AbstractTool[]}
*/
let tools = [
new QueryStarRailTool(),
new WebsiteTool(),
new SendPictureTool(),
new SendVideoTool(),
new SearchVideoTool(),
new SendAvatarTool(),
new SerpImageTool(),
new SearchMusicTool(),
new SendMusicTool(),
new SendAudioMessageTool(),
new APTool(),
new SendMessageToSpecificGroupOrUserTool(),
new QueryGenshinTool()
]
if (Config.amapKey) {
tools.push(new WeatherTool())
}
if (e.isGroup) {
tools.push(new QueryUserinfoTool())
if (e.group.is_admin || e.group.is_owner) {
tools.push(new EditCardTool())
tools.push(new JinyanTool())
tools.push(new KickOutTool())
}
if (e.group.is_owner) {
tools.push(new SetTitleTool())
}
}
switch (Config.serpSource) {
case 'ikechan8370': {
tools.push(new SerpIkechan8370Tool())
break
}
case 'azure': {
if (!Config.azSerpKey) {
logger.warn('未配置bing搜索密钥转为使用ikechan8370搜索源')
tools.push(new SerpIkechan8370Tool())
} else {
tools.push(new SerpTool())
}
break
}
default: {
tools.push(new SerpIkechan8370Tool())
}
}
const {
funcMap
} = await collectTools(e)
let tools = Object.keys(funcMap).map(k => funcMap[k].tool)
client.addTools(tools)
}
let system = opt.system.gemini
@ -821,12 +775,12 @@ async function collectTools (e) {
new SetTitleTool()
]
// todo 3.0再重构tool的插拔和管理
let /** @type{AbstractTool} **/ tools = [
let /** @type{AbstractTool[]} **/ tools = [
new SendAvatarTool(),
new SendDiceTool(),
new SendMessageToSpecificGroupOrUserTool(),
// new EditCardTool(),
// new QueryStarRailTool(),
new QueryStarRailTool(),
new QueryGenshinTool(),
new SendMusicTool(),
new SearchMusicTool(),
@ -873,13 +827,15 @@ async function collectTools (e) {
tools.forEach(tool => {
funcMap[tool.name] = {
exec: tool.func,
function: tool.function()
function: tool.function(),
tool
}
})
fullTools.forEach(tool => {
fullFuncMap[tool.name] = {
exec: tool.func,
function: tool.function()
function: tool.function(),
tool
}
})
return {

View file

@ -227,7 +227,8 @@ const defaultConfig = {
_2captchaKey: '',
bingReasoning: false, // 是否深度思考
apiMaxToken: 4096,
version: 'v2.8.3'
enableToolPrivateSend: true, // 是否允许智能模式下私聊骚扰其他群友。主人不受影响。
version: 'v2.8.4'
}
const _path = process.cwd()
let config = {}

View file

@ -24,7 +24,7 @@ export class EditCardTool extends AbstractTool {
description = 'Useful when you want to edit someone\'s card in the group(群名片)'
func = async function (opts, e) {
let { qq, card, groupId, isAdmin } = opts
let { qq, card, groupId, sender, isAdmin } = opts
qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
groupId = isNaN(groupId) || !groupId ? e.group_id : parseInt(groupId.trim())
@ -41,7 +41,7 @@ export class EditCardTool extends AbstractTool {
logger.error('获取群信息失败,可能使用的底层协议不完善')
}
logger.info('edit card: ', groupId, qq)
if (isAdmin) {
if (isAdmin || sender == qq) {
await group.setCard(qq, card)
} else {
return 'the user is not admin, he can\'t edit card of other people.'

View file

@ -15,7 +15,7 @@ export class JinyanTool extends AbstractTool {
},
time: {
type: 'string',
description: '禁言时长单位为秒默认为600'
description: '禁言时长单位为秒默认为600。如果需要解除禁言则填0.'
},
isPunish: {
type: 'string',

View file

@ -22,65 +22,29 @@ export class QueryStarRailTool extends AbstractTool {
}
func = async function (opts, e) {
let { qq, uid, character } = opts
let { qq, uid = '', character = '' } = opts
qq = isNaN(qq) || !qq ? e.sender.user_id : parseInt(qq.trim())
if (e.at === e.bot.uin) {
e.at = null
}
e.atBot = false
if (!uid) {
try {
let { Panel } = await import('../../../StarRail-plugin/apps/panel.js')
uid = await redis.get(`STAR_RAILWAY:UID:${qq}`)
if (!uid) {
return '用户没有绑定uid无法查询。可以让用户主动提供uid进行查询'
}
} catch (e) {
// todo support miao-plugin and sruid
return '未安装StarRail-Plugin无法查询'
}
}
try {
let { Panel } = await import('../../../StarRail-plugin/apps/panel.js')
e.msg = character ? `*${character}面板${uid}` : '*更新面板' + uid
e.user_id = qq
e.isSr = true
let panel = new Panel(e)
panel.e = e
panel.panel(e).catch(e => logger.warn(e))
let uidRes = await fetch('https://avocado.wiki/v1/info/' + uid)
uidRes = await uidRes.json()
let { assistAvatar, displayAvatars } = uidRes.playerDetailInfo
function dealAvatar (avatar) {
delete avatar.position
delete avatar.vo_tag
delete avatar.desc
delete avatar.promption
delete avatar.relics
delete avatar.behaviorList
delete avatar.images
delete avatar.ranks
if (avatar.equipment) {
avatar.equipment = {
level: avatar.equipment.level,
rank: avatar.equipment.rank,
name: avatar.equipment.name,
skill_desc: avatar.equipment.skill_desc
}
}
if (character) {
let ProfileDetail = (await import('../../../miao-plugin/apps/profile/ProfileDetail.js')).default
// e.msg = `#${character}面板${uid}`
e.original_msg = `*${character}面板${uid}`
e.user_id = parseInt(qq)
e.isSr = true
await ProfileDetail.detail(e)
return 'the character panel of star rail has been sent to group. you don\'t need text version'
} else {
let ProfileList = (await import('../../../miao-plugin/apps/profile/ProfileList.js')).default
e.msg = `*面板${uid}`
e.user_id = qq
e.isSr = true
await ProfileList.render(e)
return 'the player panel of genshin impact has been sent to group. you don\'t need text version'
}
dealAvatar(assistAvatar)
if (displayAvatars) {
displayAvatars.forEach(avatar => {
dealAvatar(avatar)
})
}
uidRes.playerDetailInfo.assistAvatar = assistAvatar
uidRes.playerDetailInfo.displayAvatars = displayAvatars
delete uidRes.repository
delete uidRes.version
return `the player info in json format is: \n${JSON.stringify(uidRes)}`
} catch (err) {
return `failed to query, error: ${err.toString()}`
}

View file

@ -1,5 +1,7 @@
import { AbstractTool } from './AbstractTool.js'
import { convertFaces } from '../face.js'
import {getMasterQQ} from '../common.js'
import {Config} from '../config.js'
export class SendMessageToSpecificGroupOrUserTool extends AbstractTool {
name = 'sendMessage'
@ -19,7 +21,7 @@ export class SendMessageToSpecificGroupOrUserTool extends AbstractTool {
}
func = async function (opt, e) {
let { msg, targetGroupIdOrQQNumber } = opt
let { msg, sender, targetGroupIdOrQQNumber } = opt
const defaultTarget = e.isGroup ? e.group_id : e.sender.user_id
const target = isNaN(targetGroupIdOrQQNumber) || !targetGroupIdOrQQNumber
? defaultTarget
@ -37,6 +39,10 @@ export class SendMessageToSpecificGroupOrUserTool extends AbstractTool {
await group.sendMsg(await convertFaces(msg, true, e))
return 'msg has been sent to group' + target
} else {
let masters = (await getMasterQQ())
if (!Config.enableToolPrivateSend && !masters.includes(sender + '')) {
return 'you are not allowed to pm other group members'
}
let user = e.bot.pickUser(target)
if (e.group_id) {
user = user.asMember(e.group_id)

View file

@ -1,4 +1,6 @@
import { AbstractTool } from './AbstractTool.js'
import {getMasterQQ} from '../common.js'
import {Config} from '../config.js'
export class SendPictureTool extends AbstractTool {
name = 'sendPicture'
@ -18,7 +20,7 @@ export class SendPictureTool extends AbstractTool {
}
func = async function (opt, e) {
let { urlOfPicture, targetGroupIdOrQQNumber } = opt
let { urlOfPicture, targetGroupIdOrQQNumber, sender } = opt
if (typeof urlOfPicture === 'object') {
urlOfPicture = urlOfPicture.join(' ')
}
@ -55,6 +57,10 @@ export class SendPictureTool extends AbstractTool {
// await group.sendMsg(pictures)
return 'picture has been sent to group' + target + (errs.length > 0 ? `, but some pictures failed to send (${errs.join('、')})` : '')
} else {
let masters = (await getMasterQQ())
if (!Config.enableToolPrivateSend && !masters.includes(sender + '')) {
return 'you are not allowed to pm other group members'
}
let user = e.bot.pickUser(target)
if (e.group_id) {
user = user.asMember(e.group_id)

View file

@ -11,7 +11,8 @@ export class SerpIkechan8370Tool extends AbstractTool {
},
source: {
type: 'string',
enum: ['google', 'bing', 'baidu']
enum: ['google', 'bing', 'baidu', 'duckduckgo'],
description: 'search source, default value is bing'
}
},
required: ['q']
@ -19,7 +20,7 @@ export class SerpIkechan8370Tool extends AbstractTool {
func = async function (opts) {
let { q, source } = opts
if (!source || !['google', 'bing', 'baidu'].includes(source)) {
if (!source || !['google', 'bing', 'baidu', 'duckduckgo'].includes(source)) {
source = 'bing'
}
let serpRes = await fetch(`https://serp.ikechan8370.com/${source}?q=${encodeURIComponent(q)}&lang=zh-CN&limit=5`, {
@ -29,11 +30,11 @@ export class SerpIkechan8370Tool extends AbstractTool {
})
serpRes = await serpRes.json()
let res = serpRes.data
let res = serpRes.data || serpRes.results
res?.forEach(r => {
delete r?.rank
})
return `the search results are here in json format:\n${JSON.stringify(res)}`
return `the search results are here in json format:\n${JSON.stringify(res)} \n(Notice that these information are only available for you, the user cannot see them, you next answer should consider about the information)`
}
description = 'Useful when you want to search something from the Internet. If you don\'t know much about the user\'s question, prefer to search about it! If you want to know further details of a result, you can use website tool'

View file

@ -61,48 +61,49 @@ export class WebsiteTool extends AbstractTool {
.replace(/\s{2}/g, '') // 多个空格只保留一个空格
.replace('<!DOCTYPE html>', '') // 去除<!DOCTYPE>声明
if (mode === 'gemini') {
let client = new CustomGoogleGeminiClient({
e,
userId: e?.sender?.user_id,
key: Config.getGeminiKey(),
model: Config.geminiModel,
baseUrl: Config.geminiBaseUrl,
debug: Config.debug
})
const htmlContentSummaryRes = await client.sendMessage(`去除与主体内容无关的部分从中整理出主体内容并转换成md格式不需要主观描述性的语言与冗余的空白行。${text}`)
let htmlContentSummary = htmlContentSummaryRes.text
return `this is the main content of website:\n ${htmlContentSummary}`
} else {
let maxModelTokens = getMaxModelTokens(Config.model)
text = text.slice(0, Math.min(text.length, maxModelTokens - 1600))
let completionParams = {
// model: Config.model
model: 'gpt-3.5-turbo-16k'
}
let api = new ChatGPTAPI({
apiBaseUrl: Config.openAiBaseUrl,
apiKey: Config.apiKey,
debug: false,
completionParams,
fetch: (url, options = {}) => {
const defaultOptions = Config.proxy
? {
agent: proxy(Config.proxy)
}
: {}
const mergedOptions = {
...defaultOptions,
...options
}
return fetch(url, mergedOptions)
},
maxModelTokens
})
const htmlContentSummaryRes = await api.sendMessage(`去除与主体内容无关的部分从中整理出主体内容并转换成md格式不需要主观描述性的语言与冗余的空白行。${text}`, { completionParams })
let htmlContentSummary = htmlContentSummaryRes.text
return `this is the main content of website:\n ${htmlContentSummary}`
}
// if (mode === 'gemini') {
// let client = new CustomGoogleGeminiClient({
// e,
// userId: e?.sender?.user_id,
// key: Config.getGeminiKey(),
// model: Config.geminiModel,
// baseUrl: Config.geminiBaseUrl,
// debug: Config.debug
// })
// const htmlContentSummaryRes = await client.sendMessage(`去除与主体内容无关的部分从中整理出主体内容并转换成md格式不需要主观描述性的语言与冗余的空白行。${text}`)
// let htmlContentSummary = htmlContentSummaryRes.text
// return `this is the main content of website:\n ${htmlContentSummary}`
// } else {
// let maxModelTokens = getMaxModelTokens(Config.model)
// text = text.slice(0, Math.min(text.length, maxModelTokens - 1600))
// let completionParams = {
// // model: Config.model
// model: 'gpt-3.5-turbo-16k'
// }
// let api = new ChatGPTAPI({
// apiBaseUrl: Config.openAiBaseUrl,
// apiKey: Config.apiKey,
// debug: false,
// completionParams,
// fetch: (url, options = {}) => {
// const defaultOptions = Config.proxy
// ? {
// agent: proxy(Config.proxy)
// }
// : {}
// const mergedOptions = {
// ...defaultOptions,
// ...options
// }
// return fetch(url, mergedOptions)
// },
// maxModelTokens
// })
// const htmlContentSummaryRes = await api.sendMessage(`去除与主体内容无关的部分从中整理出主体内容并转换成md格式不需要主观描述性的语言与冗余的空白行。${text}`, { completionParams })
// let htmlContentSummary = htmlContentSummaryRes.text
// return `this is the main content of website:\n ${htmlContentSummary}`
// }
return `the content of the website is:\n${text}`
} catch (err) {
return `failed to visit the website, error: ${err.toString()}`
} finally {