feat: 智能模式,添加群管、试图、联网搜索、发图、发音乐和视频等功能 (#488)

* 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

* fix: add a image caption tool

* fix: 修改一些提示。tool太多机器人开始混乱了

* fix: 一些极端的措施

* fix: 增加一些提示和一个暂时的公共接口

* fix: 收拾一下

* fix: 修改命令正则

* fix: 修改一些提示

* fix: move send avatar into send picture tool

* fix: 修复解除禁言的bug
This commit is contained in:
ikechan8370 2023-06-25 01:09:29 +08:00 committed by GitHub
parent 2c5b084b04
commit b7427e74c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 18987 additions and 58 deletions

View file

@ -0,0 +1,20 @@
export class AbstractTool {
name = ''
parameters = {}
description = ''
func = async function () {}
function () {
if (!this.parameters.type) {
this.parameters.type = 'object'
}
return {
name: this.name,
description: this.description,
parameters: this.parameters
}
}
}

View file

@ -0,0 +1,35 @@
import { AbstractTool } from './AbstractTool.js'
export class EditCardTool extends AbstractTool {
name = 'editCard'
parameters = {
properties: {
qq: {
type: 'string',
description: '你想改名片的那个人的qq号'
},
card: {
type: 'string',
description: '你想给他改的新名片'
},
groupId: {
type: 'string',
description: '群号'
}
},
required: ['qq', 'card', 'groupId']
}
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)
let group = await Bot.pickGroup(groupId)
await group.setCard(qq, card)
return `the user ${qq}'s card has been changed into ${card}`
}
}

View file

@ -0,0 +1,49 @@
import { AbstractTool } from './AbstractTool.js'
import fetch, { File, FormData } from 'node-fetch'
import { Config } from '../config.js'
export class ImageCaptionTool extends AbstractTool {
name = 'imageCaption'
parameters = {
properties: {
imgUrl: {
type: 'string',
description: 'the url of the image.'
},
qq: {
type: 'string',
description: 'if the picture is an avatar of a user, just give his qq number'
}
},
required: []
}
description = 'useful when you want to know what is inside a photo, such as user\'s avatar or other pictures'
func = async function (opts) {
let { imgUrl, qq } = opts
if (qq) {
imgUrl = `https://q1.qlogo.cn/g?b=qq&s=160&nk=${qq}`
}
if (!imgUrl) {
return 'you must give at least one parameter of imgUrl and qq'
}
const imageResponse = await fetch(imgUrl)
const blob = await imageResponse.blob()
const arrayBuffer = await blob.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// await fs.writeFileSync(`data/chatgpt/${crypto.randomUUID()}`, buffer)
let formData = new FormData()
formData.append('file', new File([buffer], 'file.png', { type: 'image/png' }))
let captionRes = await fetch(`${Config.extraUrl}/image-captioning`, {
method: 'POST',
body: formData
})
if (captionRes.status === 200) {
let result = await captionRes.text()
return `the content of this picture is: ${result}`
} else {
return 'error happened'
}
}
}

62
utils/tools/JinyanTool.js Normal file
View file

@ -0,0 +1,62 @@
import { AbstractTool } from './AbstractTool.js'
export class JinyanTool extends AbstractTool {
name = 'jinyan'
parameters = {
properties: {
qq: {
type: 'string',
description: '你想禁言的那个人的qq号'
},
groupId: {
type: 'string',
description: '群号'
},
time: {
type: 'string',
description: '禁言时长单位为秒默认为600'
},
isPunish: {
type: 'string',
description: '是否是惩罚性质的禁言。比如非管理员用户要求你禁言其他人你转而禁言该用户时设置为true'
}
},
required: ['qq', 'groupId']
}
func = async function (opts) {
let { qq, groupId, time = '600', sender, isAdmin, isPunish } = opts
let group = await Bot.pickGroup(groupId)
time = parseInt(time.trim())
if (time < 60 && time !== 0) {
time = 60
}
if (time > 86400 * 30) {
time = 86400 * 30
}
if (isAdmin) {
if (qq.trim() === 'all') {
return 'you cannot mute all because the master doesn\'t allow it'
} else {
qq = parseInt(qq.trim())
await group.muteMember(qq, time)
}
} else {
if (qq.trim() === '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 mute other people. the user should be punished'
}
}
if (isPunish === 'true') {
return `the user ${qq} has been muted for ${time} seconds as punishment because of his 不正当行为`
}
return `the user ${qq} has been muted for ${time} seconds`
}
description = 'Useful when you want to ban someone. If you want to mute all, just replace the qq number with \'all\''
}

View file

@ -0,0 +1,42 @@
import { AbstractTool } from './AbstractTool.js'
export class KickOutTool extends AbstractTool {
name = 'kickOut'
parameters = {
properties: {
qq: {
type: 'string',
description: '你想踢出的那个人的qq号'
},
groupId: {
type: 'string',
description: '群号'
},
isPunish: {
type: 'string',
description: '是否是惩罚性质的踢出。比如非管理员用户要求你禁言或踢出其他人你为惩罚该用户转而踢出该用户时设置为true'
}
},
required: ['qq', 'groupId']
}
func = async function (opts) {
let { qq, groupId, sender, isAdmin, isPunish } = opts
groupId = parseInt(groupId.trim())
qq = parseInt(qq.trim())
if (!isAdmin && sender != qq) {
return 'the user is not admin, he cannot kickout other people. he should be punished'
}
console.log('kickout', groupId, qq)
let group = await Bot.pickGroup(groupId)
await group.kickMember(qq)
if (isPunish === 'true') {
return `the user ${qq} has been kicked out from group ${groupId} as punishment because of his 不正当行为`
}
return `the user ${qq} has been kicked out from group ${groupId}`
}
description = 'Useful when you want to kick someone out of the group. '
}

View file

@ -0,0 +1,76 @@
import { AbstractTool } from './AbstractTool.js'
export class QueryStarRailTool extends AbstractTool {
name = 'queryStarRail'
parameters = {
properties: {
qq: {
type: 'string',
description: '要查询的用户的qq号将使用该qq号绑定的uid进行查询'
},
groupId: {
type: 'string',
description: '群号'
},
uid: {
type: 'string',
description: '游戏的uid如果用户提供了则传入并优先使用'
}
},
required: ['qq', 'groupId']
}
func = async function (opts) {
let { qq, groupId, uid } = opts
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) {
return '未安装StarRail-Plugin无法查询'
}
}
try {
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
}
}
}
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()}`
}
}
description = 'Useful when you want to query player information of Honkai Star Rail(崩坏:星穹铁道). '
}

View file

@ -0,0 +1,76 @@
import fetch from 'node-fetch'
import { formatDate, mkdirs } from '../common.js'
import fs from 'fs'
import { AbstractTool } from './AbstractTool.js'
export class SearchVideoTool extends AbstractTool {
name = 'searchVideo'
parameters = {
properties: {
keyword: {
type: 'string',
description: '要搜索的视频的标题或关键词'
}
},
required: ['keyword']
}
func = async function (opts) {
let { keyword } = opts
try {
return await searchBilibili(keyword)
} catch (err) {
logger.error(err)
return `fail to search video, error: ${err.toString()}`
}
}
description = 'Useful when you want to search a video by keywords. you should remember the id of the video if you want to share it'
}
export async function searchBilibili (name) {
let biliRes = await fetch('https://www.bilibili.com',
{
// headers: {
// accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
// Accept: '*/*',
// 'Accept-Encoding': 'gzip, deflate, br',
// 'accept-language': 'en-US,en;q=0.9',
// Connection: 'keep-alive',
// 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
// }
})
const headers = biliRes.headers.raw()
const setCookieHeaders = headers['set-cookie']
if (setCookieHeaders) {
const cookies = []
setCookieHeaders.forEach(header => {
const cookie = header.split(';')[0]
cookies.push(cookie)
})
const cookieHeader = cookies.join('; ')
let headers = {
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'accept-language': 'en-US,en;q=0.9',
Referer: 'https://www.bilibili.com',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
cookie: cookieHeader
}
let response = await fetch(`https://api.bilibili.com/x/web-interface/search/type?keyword=${name}&search_type=video`,
{
headers
})
let json = await response.json()
if (json.data?.numResults > 0) {
let result = json.data.result.map(r => {
return `id: ${r.bvid},标题:${r.title},作者:${r.author},播放量:${r.play},发布日期:${formatDate(new Date(r.pubdate * 1000))}`
}).slice(0, Math.min(json.data?.numResults, 5)).join('\n')
return `这些是关键词“${name}”的搜索结果:\n${result}`
} else {
return `没有找到关键词“${name}”的搜索结果`
}
}
return {}
}

View file

@ -0,0 +1,30 @@
import { AbstractTool } from './AbstractTool.js'
export class SerpImageTool extends AbstractTool {
name = 'searchImage'
parameters = {
properties: {
q: {
type: 'string',
description: 'search keyword'
}
},
required: ['q']
}
func = async function (opts) {
let { q } = opts
let serpRes = await fetch(`https://serp.ikechan8370.com/image/bing?q=${encodeURIComponent(q)}`, {
headers: {
'X-From-Library': 'ikechan8370'
}
})
serpRes = await serpRes.json()
let res = serpRes.data
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. '
}

View file

@ -0,0 +1,39 @@
import fetch from 'node-fetch'
import { AbstractTool } from './AbstractTool.js'
export class SearchMusicTool extends AbstractTool {
name = 'searchMusic'
parameters = {
properties: {
keyword: {
type: 'string',
description: '音乐的标题或关键词'
}
},
required: ['keyword']
}
func = async function (opts) {
let { keyword } = opts
try {
let result = await searchMusic163(keyword)
return `search result: ${result}`
} catch (e) {
return `music search failed: ${e}`
}
}
description = 'Useful when you want to search music by keyword.'
}
export async function searchMusic163 (name) {
let response = await fetch(`http://music.163.com/api/search/get/web?s=${name}&type=1&offset=0&total=true&limit=6`)
let json = await response.json()
if (json.result?.songCount > 0) {
return json.result.songs.map(song => {
return `id: ${song.id}, name: ${song.name}, artists: ${song.artists.map(a => a.name).join('&')}, alias: ${song.alias || 'none'}`
}).join('\n')
}
return null
}

View file

@ -0,0 +1,33 @@
import { AbstractTool } from './AbstractTool.js'
export class SendAvatarTool extends AbstractTool {
name = 'sendAvatar'
parameters = {
properties: {
qq: {
type: 'string',
description: '要发头像的人的qq号'
},
groupId: {
type: 'string',
description: '群号或qq号发送目标'
}
},
required: ['qq', 'groupId']
}
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))
}
return `the user ${qq}'s avatar has been sent to group ${groupId}`
}
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. 如果是在群聊中,优先选择群号发送。'
}

View file

@ -0,0 +1,136 @@
import fetch from 'node-fetch'
import { formatDate, mkdirs } from '../common.js'
import fs from 'fs'
import { AbstractTool } from './AbstractTool.js'
export class SendVideoTool extends AbstractTool {
name = 'sendVideo'
parameters = {
properties: {
id: {
type: 'string',
description: '要发的视频的id'
},
groupId: {
type: 'string',
description: '群号或qq号发送目标'
}
},
required: ['id', 'groupId']
}
func = async function (opts) {
let { id, groupId } = opts
groupId = parseInt(groupId.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)
msg.push(title.replace(/(<([^>]+)>)/ig, '') + '\n')
msg.push(`UP主${author} 发布日期:${formatDate(new Date(pubdate * 1000))} 播放量:${play} 点赞:${like}\n`)
msg.push(arcurl + '\n')
msg.push(segment.image(pic))
msg.push('\n' + description)
if (honor) {
msg.push(`本视频曾获得过${honor}称号`)
}
msg.push('\n视频在路上啦')
await group.sendMsg(msg)
const videoResponse = await fetch(videoUrl, { headers })
const fileType = videoResponse.headers.get('Content-Type').split('/')[1]
let fileLoc = `data/chatgpt/videos/${bvid}.${fileType}`
mkdirs('data/chatgpt/videos')
videoResponse.blob().then(async blob => {
const arrayBuffer = await blob.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
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}`
} catch (err) {
logger.error(err)
if (msg.length > 0) {
return `fail to share video, but the video msg is found: ${msg}, you can just tell the information of this video`
} else {
return `fail to share video, error: ${err.toString()}`
}
}
}
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'
}
export async function getBilibili (bvid) {
let biliRes = await fetch('https://www.bilibili.com',
{
// headers: {
// accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
// Accept: '*/*',
// 'Accept-Encoding': 'gzip, deflate, br',
// 'accept-language': 'en-US,en;q=0.9',
// Connection: 'keep-alive',
// 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36'
// }
})
const headers = biliRes.headers.raw()
const setCookieHeaders = headers['set-cookie']
if (setCookieHeaders) {
const cookies = []
setCookieHeaders.forEach(header => {
const cookie = header.split(';')[0]
cookies.push(cookie)
})
const cookieHeader = cookies.join('; ')
let headers = {
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'accept-language': 'en-US,en;q=0.9',
Referer: 'https://www.bilibili.com',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
cookie: cookieHeader
}
let videoInfo = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, {
headers
})
videoInfo = await videoInfo.json()
let cid = videoInfo.data.cid
let arcurl = `http://www.bilibili.com/video/av${videoInfo.data.aid}`
let title = videoInfo.data.title
let pic = videoInfo.data.pic
let description = videoInfo.data.desc
let author = videoInfo.data.owner.name
let play = videoInfo.data.stat.view
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 videoUrl = (await downloadInfo.json()).data.durl[0].url
return {
arcurl, title, pic, description, videoUrl, headers, bvid, author, play, pubdate, like, honor
}
} else {
return {}
}
}
function randomIndex () {
// Define weights for each index
const weights = [5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1]
// Compute the total weight
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0)
// Generate a random number between 0 and the total weight
const randomNumber = Math.floor(Math.random() * totalWeight)
// Choose the index based on the random number and weights
let weightSum = 0
for (let i = 0; i < weights.length; i++) {
weightSum += weights[i]
if (randomNumber < weightSum) {
return i
}
}
}
console.log('send bilibili')

View file

@ -0,0 +1,35 @@
import {AbstractTool} from "./AbstractTool.js";
export class SendDiceTool extends AbstractTool {
name = 'sendDice'
parameters = {
properties: {
num: {
type: 'number',
description: '骰子的数量'
},
groupId: {
type: 'string',
description: '群号或qq号发送目标'
}
},
required: ['num', 'groupId']
}
func = async function (opts) {
let {num, groupId} = opts
let groupList = await Bot.getGroupList()
if (groupList.get(groupId)) {
let group = await Bot.pickGroup(groupId, true)
await group.sendMsg(segment.dice(num))
} else {
let friend = await Bot.pickFriend(groupId)
await friend.sendMsg(segment.dice(num))
}
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'
}

View file

@ -0,0 +1,33 @@
import { AbstractTool } from './AbstractTool.js'
export class SendMusicTool extends AbstractTool {
name = 'sendMusic'
parameters = {
properties: {
id: {
type: 'string',
description: '音乐的id'
},
groupId: {
type: 'string',
description: '群号或qq号发送目标'
}
},
required: ['keyword', 'groupId']
}
func = async function (opts) {
let { id, groupId } = opts
groupId = parseInt(groupId.trim())
try {
let group = await Bot.pickGroup(groupId)
await group.shareMusic('163', id)
return `the music has been shared to ${groupId}`
} catch (e) {
return `music share failed: ${e}`
}
}
description = 'Useful when you want to share music. You must use searchMusic first to get the music id'
}

View file

@ -0,0 +1,50 @@
import { AbstractTool } from './AbstractTool.js'
export class SendPictureTool extends AbstractTool {
name = 'sendPicture'
parameters = {
properties: {
picture: {
type: 'string',
description: 'the url of the pictures, split with space if more than one.'
},
qq: {
type: 'string',
description: 'if you want to send avatar of a user, input his qq number.'
},
groupId: {
type: 'string',
description: '群号或qq号发送目标'
}
},
required: ['picture', 'groupId']
}
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
}
let pictures = picture.trim().split(' ')
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)
await group.sendMsg(pictures)
return `picture has been sent to group ${groupId}`
} else {
let user = await Bot.pickFriend(groupId)
await user.sendMsg(pictures)
return `picture has been sent to user ${groupId}`
}
} catch (err) {
return `failed to send pictures, error: ${JSON.stringify(err)}`
}
}
description = 'Useful when you want to send one or more pictures. '
}

View file

@ -0,0 +1,30 @@
import {AbstractTool} from "./AbstractTool.js";
export class SendRPSTool extends AbstractTool {
name = 'sendRPS'
parameters = {
num: {
type: 'number',
description: '石头剪刀布的代号'
},
groupId: {
type: 'string',
description: '群号或qq号发送目标'
},
required: ['num', 'groupId']
}
func = async function (num, groupId) {
let groupList = await Bot.getGroupList()
if (groupList.get(groupId)) {
let group = await Bot.pickGroup(groupId, true)
await group.sendMsg(segment.rps(num))
} else {
let friend = await Bot.pickFriend(groupId)
await friend.sendMsg(segment.rps(num))
}
}
description = 'Use this tool if you want to play rock paper scissors. If you know the group number, use the group number instead of the qq number first. The input should be the number 1, 2 or 3 to represent rock-paper-scissors and the target group number or qq numberand they should be concat with a space'
}

View file

@ -0,0 +1,37 @@
import { AbstractTool } from './AbstractTool.js'
export class SerpIkechan8370Tool extends AbstractTool {
name = 'search'
parameters = {
properties: {
q: {
type: 'string',
description: 'search keyword'
},
source: {
type: 'string',
enum: ['google', 'bing', 'baidu']
}
},
required: ['q']
}
func = async function (opts) {
let { q, source } = opts
if (!source) {
source = 'google'
}
let serpRes = await fetch(`https://serp.ikechan8370.com/${source}?q=${encodeURIComponent(q)}&lang=zh-CN&limit=10`, {
headers: {
'X-From-Library': 'ikechan8370'
}
})
serpRes = await serpRes.json()
let res = serpRes.data
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'
}

40
utils/tools/SerpTool.js Normal file
View file

@ -0,0 +1,40 @@
import { AbstractTool } from './AbstractTool.js'
import { Config } from '../config.js'
export class SerpTool extends AbstractTool {
name = 'serp'
parameters = {
properties: {
q: {
type: 'string',
description: 'search keyword'
}
},
required: ['q']
}
func = async function (opts) {
let { q } = opts
let key = Config.azSerpKey
let serpRes = await fetch(`https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent(q)}&mkt=zh-CN`, {
headers: {
'Ocp-Apim-Subscription-Key': key
}
})
serpRes = await serpRes.json()
let res = serpRes.webPages.value
res.forEach(p => {
delete p.displayUrl
delete p.isFamilyFriendly
delete p.thumbnailUrl
delete p.id
delete p.isNavigational
})
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'
}

View file

@ -0,0 +1,35 @@
import { AbstractTool } from './AbstractTool.js'
import {Config} from "../config.js";
export class WeatherTool extends AbstractTool {
name = 'weather'
parameters = {
properties: {
city: {
type: 'string',
description: '要查询的地点,细化到县/区级'
}
},
required: ['city']
}
func = async function (opts) {
let { city } = opts
let key = Config.amapKey
let adcodeRes = await fetch(`https://restapi.amap.com/v3/config/district?keywords=${city}&subdistrict=1&key=${key}`)
adcodeRes = await adcodeRes.json()
let adcode = adcodeRes.districts[0]?.adcode
if (!adcode) {
return `the area ${city} doesn't exist! are you kidding? you should mute him for 1 minute`
}
let cityName = adcodeRes.districts[0].name
let res = await fetch(`https://restapi.amap.com/v3/weather/weatherInfo?city=${adcode}&key=${key}`)
res = await res.json()
let result = res.lives[0]
return `the weather information of area ${cityName} in json format is:\n${JSON.stringify(result)}`
}
description = 'Useful when you want to query weather '
}

View file

@ -0,0 +1,86 @@
import { AbstractTool } from './AbstractTool.js'
import { ChatGPTAPI } from '../openai/chatgpt-api.js'
import { Config } from '../config.js'
import fetch from 'node-fetch'
import proxy from 'https-proxy-agent'
import { getMaxModelTokens } from '../common.js'
import { ChatGPTPuppeteer } from '../browser.js'
export class WebsiteTool extends AbstractTool {
name = 'website'
parameters = {
properties: {
url: {
type: 'string',
description: '要访问的网站网址'
}
},
required: ['url']
}
func = async function (opts) {
let { url } = opts
try {
// let res = await fetch(url, {
// headers: {
// 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
// }
// })
// let text = await res.text()
let origin = false
if (!Config.headless) {
Config.headless = true
origin = true
}
let ppt = new ChatGPTPuppeteer()
let browser = await ppt.getBrowser()
let page = await browser.newPage()
await page.goto(url, {
waitUntil: 'networkidle2'
})
let text = await page.content()
await page.close()
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, '')
let maxModelTokens = getMaxModelTokens(Config.model)
text = text.slice(0, Math.min(text.length, maxModelTokens - 1600))
let api = new ChatGPTAPI({
apiBaseUrl: Config.openAiBaseUrl,
apiKey: Config.apiKey,
debug: false,
completionParams: {
model: Config.model
},
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(`这是一个网页html经过筛选的内容请你进一步去掉其中的标签、样式、script等无用信息并从中提取出其中的主体内容转换成自然语言告诉我不需要主观描述性的语言。${text}`)
let htmlContentSummary = htmlContentSummaryRes.text
return `this is the main content of website:\n ${htmlContentSummary}`
} catch (err) {
return `failed to visit the website, error: ${err.toString()}`
}
}
description = 'Useful when you want to browse a website by url'
}