Merge branch 'v2' of https://github.com/ikechan8370/chatgpt-plugin into ikechan8370-v2

This commit is contained in:
ycxom 2025-02-18 11:24:05 +08:00
commit 8f47a3d759
8 changed files with 140 additions and 69 deletions

View file

@ -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) {

View file

@ -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) {

View file

@ -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'
}
],
// 获取配置数据方法(用于前端填充显示数据)

View file

@ -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) {

View file

@ -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
View 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.'
}

View file

@ -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)

View file

@ -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'
}