mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-16 13:27:08 +00:00
Merge branch 'v2' of https://github.com/ikechan8370/chatgpt-plugin into ikechan8370-v2
This commit is contained in:
commit
8f47a3d759
8 changed files with 140 additions and 69 deletions
|
|
@ -81,7 +81,7 @@ export class help extends plugin {
|
|||
prompts.push(...[defaultPrompt, defaultSydneyPrompt])
|
||||
prompts.push(...readPrompts())
|
||||
console.log(prompts)
|
||||
e.reply(await makeForwardMsg(e, prompts.map(p => `《${p.name}》\n${limitString(p.content, 500)}`), '设定列表'))
|
||||
e.reply(await makeForwardMsg(e, prompts.map(p => `《${p.name}》\n${limitString(p.content, 100)}`), '设定列表'))
|
||||
}
|
||||
|
||||
async detailPrompt (e) {
|
||||
|
|
|
|||
|
|
@ -112,9 +112,10 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
|
|||
* search: boolean,
|
||||
* codeExecution: boolean,
|
||||
* }} opt
|
||||
* @param {number} retryTime 重试次数
|
||||
* @returns {Promise<{conversationId: string?, parentMessageId: string, text: string, id: string}>}
|
||||
*/
|
||||
async sendMessage(text, opt = {}) {
|
||||
async sendMessage (text, opt = {}, retryTime = 3) {
|
||||
let history = await this.getHistory(opt.parentMessageId)
|
||||
let systemMessage = opt.system
|
||||
// if (systemMessage) {
|
||||
|
|
@ -216,7 +217,7 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
|
|||
}
|
||||
],
|
||||
generationConfig: {
|
||||
maxOutputTokens: opt.maxOutputTokens || 1000,
|
||||
maxOutputTokens: opt.maxOutputTokens || 4096,
|
||||
temperature: opt.temperature || 0.9,
|
||||
topP: opt.topP || 0.95,
|
||||
topK: opt.tokK || 16
|
||||
|
|
@ -285,7 +286,7 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
|
|||
*/
|
||||
let responseContent
|
||||
/**
|
||||
* @type {{candidates: Array<{content: Content, groundingMetadata: GroundingMetadata}>}}
|
||||
* @type {{candidates: Array<{content: Content, groundingMetadata: GroundingMetadata, finishReason: string}>}}
|
||||
*/
|
||||
let response = await result.json()
|
||||
if (this.debug) {
|
||||
|
|
@ -293,14 +294,19 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
|
|||
}
|
||||
responseContent = response.candidates[0].content
|
||||
let groundingMetadata = response.candidates[0].groundingMetadata
|
||||
if (response.candidates[0].finishReason === 'MALFORMED_FUNCTION_CALL') {
|
||||
logger.warn('遇到MALFORMED_FUNCTION_CALL,进行重试。')
|
||||
return this.sendMessage(text, opt, retryTime--)
|
||||
}
|
||||
// todo 空回复也可以重试
|
||||
if (responseContent.parts.filter(i => i.functionCall).length > 0) {
|
||||
// functionCall
|
||||
const functionCall = responseContent.parts.filter(i => i.functionCall).map(i => i.functionCall)
|
||||
const text = responseContent.parts.find(i => i.text)?.text
|
||||
if (text) {
|
||||
if (text && text.trim()) {
|
||||
// send reply first
|
||||
logger.info('send message: ' + text)
|
||||
opt.replyPureTextCallback && await opt.replyPureTextCallback(text)
|
||||
logger.info('send message: ' + text.trim())
|
||||
opt.replyPureTextCallback && await opt.replyPureTextCallback(text.trim())
|
||||
}
|
||||
let /** @type {FunctionResponse[]} **/ fcResults = []
|
||||
for (let fc of functionCall) {
|
||||
|
|
|
|||
|
|
@ -1198,6 +1198,12 @@ export function supportGuoba () {
|
|||
label: '额外工具url',
|
||||
bottomHelpMessage: '(测试期间提供一个公益接口,一段时间后撤掉)参考搭建:https://github.com/ikechan8370/chatgpt-plugin-extras',
|
||||
component: 'Input'
|
||||
},
|
||||
{
|
||||
field: 'githubAPIKey',
|
||||
label: 'github Access Token',
|
||||
bottomHelpMessage: '去https://github.com/settings/personal-access-tokens生成。用于提高AI调用github工具的Rate Limit',
|
||||
component: 'Input'
|
||||
}
|
||||
],
|
||||
// 获取配置数据方法(用于前端填充显示数据)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ import { QwenApi } from '../utils/alibaba/qwen-api.js'
|
|||
import { BingAIClient } from '../client/CopilotAIClient.js'
|
||||
import Keyv from 'keyv'
|
||||
import crypto from 'crypto'
|
||||
import {GithubAPITool} from '../utils/tools/GithubTool.js'
|
||||
|
||||
export const roleMap = {
|
||||
owner: 'group owner',
|
||||
|
|
@ -774,7 +775,8 @@ async function collectTools (e) {
|
|||
new SendMessageToSpecificGroupOrUserTool(),
|
||||
new SendDiceTool(),
|
||||
new QueryGenshinTool(),
|
||||
new SetTitleTool()
|
||||
new SetTitleTool(),
|
||||
new GithubAPITool()
|
||||
]
|
||||
// todo 3.0再重构tool的插拔和管理
|
||||
let /** @type{AbstractTool[]} **/ tools = [
|
||||
|
|
@ -796,7 +798,8 @@ async function collectTools (e) {
|
|||
new APTool(),
|
||||
// new HandleMessageMsgTool(),
|
||||
serpTool,
|
||||
new QueryUserinfoTool()
|
||||
new QueryUserinfoTool(),
|
||||
new GithubAPITool()
|
||||
]
|
||||
let systemAddition = ''
|
||||
if (e.isGroup) {
|
||||
|
|
|
|||
|
|
@ -229,6 +229,8 @@ const defaultConfig = {
|
|||
apiMaxToken: 4096,
|
||||
enableToolPrivateSend: true, // 是否允许智能模式下私聊骚扰其他群友。主人不受影响。
|
||||
geminiForceToolKeywords: [],
|
||||
githubAPI: 'https://api.github.com',
|
||||
githubAPIKey: '',
|
||||
version: 'v2.8.4'
|
||||
}
|
||||
const _path = process.cwd()
|
||||
|
|
|
|||
59
utils/tools/GithubTool.js
Normal file
59
utils/tools/GithubTool.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { AbstractTool } from './AbstractTool.js'
|
||||
import { Config } from '../config.js'
|
||||
|
||||
export class GithubAPITool extends AbstractTool {
|
||||
name = 'github'
|
||||
|
||||
parameters = {
|
||||
properties: {
|
||||
q: {
|
||||
type: 'string',
|
||||
description: 'search keyword. you should build it. If you want to find from specified repo, please must use repo:ORG/REPO as part of the keyword. For example, if you want to find the oldest unresolved Python bugs on Windows. Your query might look something like this: q=windows+label:bug+language:python+state:open&sort=created&order=asc'
|
||||
},
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['repositories', 'issues', 'users', 'code', 'custom'],
|
||||
description: 'search type. If custom is chosen, you must provide full github api url path.'
|
||||
},
|
||||
num: {
|
||||
type: 'number',
|
||||
description: 'search results limit number, default is 5'
|
||||
},
|
||||
fullUrl: {
|
||||
type: 'string',
|
||||
description: 'if type is custom, you need provide this, such as /repos/OWNER/REPO/actions/artifacts?name=NAME&page=2&per_page=1. if type is not custom, is will be ignored'
|
||||
}
|
||||
},
|
||||
required: ['q', 'type']
|
||||
}
|
||||
|
||||
func = async function (opts) {
|
||||
let { q, type, num = 5, fullUrl = '' } = opts
|
||||
let headers = {
|
||||
'X-From-Library': 'ikechan8370',
|
||||
Accept: 'application/vnd.github+json'
|
||||
}
|
||||
if (Config.githubAPIKey) {
|
||||
headers.Authorization = `Bearer ${Config.githubAPIKey}`
|
||||
}
|
||||
let res
|
||||
if (type !== 'custom') {
|
||||
let serpRes = await fetch(`${Config.githubAPI}/search/${type}?q=${encodeURIComponent(q)}&per_page=${num}`, {
|
||||
headers
|
||||
})
|
||||
serpRes = await serpRes.json()
|
||||
|
||||
res = serpRes
|
||||
} else {
|
||||
let serpRes = await fetch(`${Config.githubAPI}${fullUrl}`, {
|
||||
headers
|
||||
})
|
||||
serpRes = await serpRes.json()
|
||||
res = serpRes
|
||||
}
|
||||
|
||||
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 api.github.com. You can use preset search types or build your own url path with order, per_page, page and other params. Automatically adjust the query and params if any error messages return.'
|
||||
}
|
||||
|
|
@ -51,7 +51,7 @@ export class SendPictureTool extends AbstractTool {
|
|||
try {
|
||||
await group.sendMsg(pic)
|
||||
} catch (err) {
|
||||
errs.push(pic.url)
|
||||
errs.push(pic)
|
||||
}
|
||||
}
|
||||
// await group.sendMsg(pictures)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,58 @@ import proxy from 'https-proxy-agent'
|
|||
import { getMaxModelTokens } from '../common.js'
|
||||
import { ChatGPTPuppeteer } from '../browser.js'
|
||||
import { CustomGoogleGeminiClient } from '../../client/CustomGoogleGeminiClient.js'
|
||||
|
||||
/**
|
||||
* Generated by GPT-4o
|
||||
* @param html
|
||||
* @returns {*}
|
||||
*/
|
||||
function cleanHTML (html) {
|
||||
// 1. 移除 <style>、<script>、<link>、<head> 等无关内容
|
||||
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '') // 移除CSS
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '') // 移除JS
|
||||
.replace(/<link[^>]*>/gi, '') // 移除外部CSS文件
|
||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '') // 移除整个<head>
|
||||
.replace(/<!--[\s\S]*?-->/g, '') // 移除HTML注释
|
||||
.replace(/<figure[^>]*>[\s\S]*?<\/figure>/gi, '') // 移除<figure>
|
||||
|
||||
// 2. 允许的标签列表
|
||||
const allowedTags = ['title', 'meta', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'img', 'video', 'audio', 'source', 'a']
|
||||
|
||||
// 3. 处理HTML标签,移除不在允许列表中的标签
|
||||
html = html.replace(/<\/?([a-zA-Z0-9]+)(\s[^>]*)?>/g, (match, tagName, attrs) => {
|
||||
tagName = tagName.toLowerCase()
|
||||
if (allowedTags.includes(tagName)) {
|
||||
if (tagName === 'meta') {
|
||||
// 允许<meta>标签,仅保留其中的 charset, name, content
|
||||
return match.replace(/<(meta)([^>]*)>/gi, (_, tag, attributes) => {
|
||||
let allowedAttrs = attributes.match(/(charset|name|content)=["'][^"']+["']/gi)
|
||||
return `<${tag} ${allowedAttrs ? allowedAttrs.join(' ') : ''}>`
|
||||
})
|
||||
} else if (tagName === 'img' || tagName === 'video' || tagName === 'audio' || tagName === 'source') {
|
||||
// 仅保留 `src` 属性,并去掉 base64 编码的 `data:` 形式
|
||||
return match.replace(/<(img|video|audio|source)([^>]*)>/gi, (_, tag, attributes) => {
|
||||
let srcMatch = attributes.match(/\bsrc=["'](?!data:)[^"']+["']/i) // 过滤 base64
|
||||
return srcMatch ? `<${tag} ${srcMatch[0]}>` : '' // 没有合法的 src 就移除整个标签
|
||||
})
|
||||
} else if (tagName === 'a') {
|
||||
// 仅保留 `href`,并去掉 base64 `data:` 形式
|
||||
return match.replace(/<a([^>]*)>/gi, (_, attributes) => {
|
||||
let hrefMatch = attributes.match(/\bhref=["'](?!data:)[^"']+["']/i)
|
||||
return hrefMatch ? `<a ${hrefMatch[0]}>` : '' // 没有合法的 href 就移除整个标签
|
||||
})
|
||||
}
|
||||
return match // 其他允许的标签直接保留
|
||||
}
|
||||
return '' // 过滤不在允许列表中的标签
|
||||
})
|
||||
|
||||
// 4. 移除多余的空格和换行符
|
||||
html = html.replace(/\s+/g, ' ').trim()
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
export class WebsiteTool extends AbstractTool {
|
||||
name = 'website'
|
||||
|
||||
|
|
@ -45,64 +97,7 @@ 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(/<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>声明
|
||||
|
||||
// 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}`
|
||||
// }
|
||||
text = cleanHTML(text)
|
||||
return `the content of the website is:\n${text}`
|
||||
} catch (err) {
|
||||
return `failed to visit the website, error: ${err.toString()}`
|
||||
|
|
@ -115,5 +110,5 @@ export class WebsiteTool extends AbstractTool {
|
|||
}
|
||||
}
|
||||
|
||||
description = 'Useful when you want to browse a website by url'
|
||||
description = 'Useful when you want to browse a website by url, it can be a html or api url'
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue