feat(bym): 好像应该没问题支持了多图片处理?

This commit is contained in:
ycxom 2025-01-01 23:35:50 +08:00
parent e3fe14c6e6
commit 6033d93028
3 changed files with 172 additions and 60 deletions

View file

@ -134,34 +134,50 @@ export class bym extends plugin {
let opt = { let opt = {
maxOutputTokens: 500, maxOutputTokens: 500,
temperature: 1, temperature: 1,
replyPureTextCallback: e.reply replyPureTextCallback: e.reply,
images: []
} }
let imgs = await getImg(e) let imgs = await getImg(e)
// 处理图片
if (!e.msg) { if (!e.msg) {
if (imgs?.length > 0) { if (imgs?.length > 0) {
let image = imgs[0] // 并行处理多张图片
const response = await fetch(image) opt.images = await Promise.all(imgs.map(async image => {
const base64Image = Buffer.from(await response.arrayBuffer()) try {
opt.image = base64Image.toString('base64') const response = await fetch(image)
e.msg = '[图片]' const base64Image = Buffer.from(await response.arrayBuffer())
return base64Image.toString('base64')
} catch (error) {
logger.error(`处理图片失败: ${error}`)
return null
}
})).then(results => results.filter(Boolean))
e.msg = `[${opt.images.length}张图片]`
} else { } else {
return setTimeout(async () => { return setTimeout(async () => {
e.msg = '我单纯只是at了你根据群聊内容回应' e.msg = '我单纯只是at了你根据群聊内容回应'
await bymGo() await bymGo()
}, 3000); }, 3000)
} }
} else if (imgs?.length > 0 && !opt.images.length) {
// 处理有消息且有图片的情况
opt.images = await Promise.all(imgs.map(async image => {
try {
const response = await fetch(image)
const base64Image = Buffer.from(await response.arrayBuffer())
return base64Image.toString('base64')
} catch (error) {
logger.error(`处理图片失败: ${error}`)
return null
}
})).then(results => results.filter(Boolean))
} }
if (!opt.image && imgs?.length > 0) {
let image = imgs[0]
const response = await fetch(image)
const base64Image = Buffer.from(await response.arrayBuffer())
opt.image = base64Image.toString('base64')
}
logger.info('[bymGo] 开始处理回复') logger.info('[bymGo] 开始处理回复')
let previousRole = ALLRole let previousRole = ALLRole
if (opt.image && !context.isAtBot && !NotToImg && !e.at && Config.AutoToDownImg) { if (opt.images?.length > 0 && !context.isAtBot && !NotToImg && !e.at && Config.AutoToDownImg) {
ALLRole = 'downimg' ALLRole = 'downimg'
} }
@ -252,7 +268,7 @@ export class bym extends plugin {
switch (user_role) { switch (user_role) {
case "downimg": case "downimg":
Role = '现在看到的是一张图片,若你觉得是一张表情包,并不是通知,或其他的图片,注意辨别图片文字是否为通知;单纯是表情包,请发送 DOWNIMG: 命名该表情。 不需要发送过多的参数只需要发送格式DOWNIMG: 命名该表情,注意不需要携带后缀,请以你的角度觉得如果要发这个表情包要用什么名字来命名; 若不是表情包等及发送NOTIMG'; Role = `现在看到的是${opt.images.length}张图片从第1张到第${opt.images.length}张),请依次查看各张图片。若觉得是表情包,并不是通知或其他类型的图片,请发送 DOWNIMG: 命名该表情。不需要发送过多的参数只需要发送格式DOWNIMG: 命名该表情,注意不需要携带后缀若不是表情包等请发送NOTIMG并对图片内容进行分析描述。注意请从第1张图片开始依次描述。`;
break; break;
case "default": case "default":
Role = `你的名字是"${Config.assistantLabel}"你在一个qq群里群号是${group},当前和你说话的人群名片是${card}, qq号是${sender}, 请你结合用户的发言和聊天记录作出回应,要求表现得随性一点,最好参与讨论,混入其中。不要过分插科打诨,不知道说什么可以复读群友的话。要求你做搜索、发图、发视频和音乐等操作时要使用工具。不可以直接发[图片]这样蒙混过关。要求优先使用中文进行对话。` + Role = `你的名字是"${Config.assistantLabel}"你在一个qq群里群号是${group},当前和你说话的人群名片是${card}, qq号是${sender}, 请你结合用户的发言和聊天记录作出回应,要求表现得随性一点,最好参与讨论,混入其中。不要过分插科打诨,不知道说什么可以复读群友的话。要求你做搜索、发图、发视频和音乐等操作时要使用工具。不可以直接发[图片]这样蒙混过关。要求优先使用中文进行对话。` +
@ -297,9 +313,6 @@ export class bym extends plugin {
if (Config.azSerpKey) { if (Config.azSerpKey) {
tools.push(new SerpTool()) tools.push(new SerpTool())
} }
if (Config.azSerpKey) {
tools.push(new SerpTool())
}
if (e.group.is_admin || e.group.is_owner) { if (e.group.is_admin || e.group.is_owner) {
tools.push(new EditCardTool()) tools.push(new EditCardTool())
tools.push(new JinyanTool()) tools.push(new JinyanTool())
@ -321,7 +334,7 @@ export class bym extends plugin {
let text = rsp.text let text = rsp.text
let texts = text.split(/(?<!\?)[。?\n](?!\?)/) let texts = text.split(/(?<!\?)[。?\n](?!\?)/)
for (let t of texts) { for (let t of texts) {
if (!t) { if (!t || !t.trim()) {
continue continue
} }
t = t.trim() t = t.trim()
@ -329,11 +342,32 @@ export class bym extends plugin {
t += '' t += ''
} }
const processed = await imageTool.processText(t, { const processed = await imageTool.processText(t, {
image: opt.image images: opt.images // 传入图片数组而不是单个图片
}) })
if (!processed) { // 处理工具返回结果
if (processed && typeof processed === 'object') {
if (processed.switchRole) {
ALLRole = processed.switchRole
}
if (processed.continueProcess) {
// 根据是否是重新处理来设置不同的消息
if (processed.reprocess) {
e.msg = `[重新处理第${processed.currentIndex + 1}张图片的内容]`
} else {
e.msg = `[处理第${processed.currentIndex + 1}张图片(共${opt.images.length}张)]`
}
await bymGo(true)
return false
} else if (processed.needResponse) {
await bymGo(true)
return false
}
} else {
let finalMsg = await convertFaces(t, true, e) let finalMsg = await convertFaces(t, true, e)
if (!finalMsg || (typeof finalMsg === 'string' && !finalMsg.trim())) {
continue
}
logger.info(JSON.stringify(finalMsg)) logger.info(JSON.stringify(finalMsg))
if (Math.floor(Math.random() * 100) < 10) { if (Math.floor(Math.random() * 100) < 10) {
await e.reply(finalMsg, true, { await e.reply(finalMsg, true, {

View file

@ -81,7 +81,7 @@ export const HarmBlockThreshold = {
*/ */
export class CustomGoogleGeminiClient extends GoogleGeminiClient { export class CustomGoogleGeminiClient extends GoogleGeminiClient {
constructor (props) { constructor(props) {
super(props) super(props)
this.model = props.model this.model = props.model
this.baseUrl = props.baseUrl || BASEURL this.baseUrl = props.baseUrl || BASEURL
@ -99,7 +99,8 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
* onProgress: function?, * onProgress: function?,
* functionResponse: FunctionResponse?, * functionResponse: FunctionResponse?,
* system: string?, * system: string?,
* image: string?, * image: string?, // 保留旧版单图片支持
* images: string[], // 新增多图片支持
* maxOutputTokens: number?, * maxOutputTokens: number?,
* temperature: number?, * temperature: number?,
* topP: number?, * topP: number?,
@ -111,7 +112,7 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
* }} opt * }} opt
* @returns {Promise<{conversationId: string?, parentMessageId: string, text: string, id: string}>} * @returns {Promise<{conversationId: string?, parentMessageId: string, text: string, id: string}>}
*/ */
async sendMessage (text, opt = {}) { async sendMessage(text, opt = {}) {
let history = await this.getHistory(opt.parentMessageId) let history = await this.getHistory(opt.parentMessageId)
let systemMessage = opt.system let systemMessage = opt.system
if (systemMessage) { if (systemMessage) {
@ -138,26 +139,40 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
const idModel = crypto.randomUUID() const idModel = crypto.randomUUID()
const thisMessage = opt.functionResponse const thisMessage = opt.functionResponse
? { ? {
role: 'user', role: 'user',
parts: [{ parts: [{
functionResponse: opt.functionResponse functionResponse: opt.functionResponse
}], }],
id: idThis, id: idThis,
parentMessageId: opt.parentMessageId || undefined parentMessageId: opt.parentMessageId || undefined
} }
: { : {
role: 'user', role: 'user',
parts: [{ text }], parts: [{ text }],
id: idThis, id: idThis,
parentMessageId: opt.parentMessageId || undefined parentMessageId: opt.parentMessageId || undefined
}
if (opt.image || opt.images) {
// 兼容旧版单图片
if (opt.image) {
thisMessage.parts.push({
inline_data: {
mime_type: 'image/jpeg',
data: opt.image
}
})
}
// 处理多图片
if (opt.images && Array.isArray(opt.images)) {
for (let imageData of opt.images) {
thisMessage.parts.push({
inline_data: {
mime_type: 'image/jpeg',
data: imageData
}
})
} }
if (opt.image) { }
thisMessage.parts.push({
inline_data: {
mime_type: 'image/jpeg',
data: opt.image
}
})
} }
history.push(_.cloneDeep(thisMessage)) history.push(_.cloneDeep(thisMessage))
let url = `${this.baseUrl}/v1beta/models/${this.model}:generateContent` let url = `${this.baseUrl}/v1beta/models/${this.model}:generateContent`
@ -220,7 +235,7 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
if (opt.codeExecution) { if (opt.codeExecution) {
body.tools.push({ code_execution: {} }) body.tools.push({ code_execution: {} })
} }
if (opt.image) { if (opt.image || (opt.images && opt.images.length > 0)) {
delete body.tools delete body.tools
} }
body.contents.forEach(content => { body.contents.forEach(content => {
@ -365,7 +380,7 @@ export class CustomGoogleGeminiClient extends GoogleGeminiClient {
* @param {Content} responseContent * @param {Content} responseContent
* @returns {{final: string, responseContent}} * @returns {{final: string, responseContent}}
*/ */
function handleSearchResponse (responseContent) { function handleSearchResponse(responseContent) {
let final = '' let final = ''
// 遍历每个 part 并处理 // 遍历每个 part 并处理

View file

@ -5,6 +5,9 @@ export class ImageProcessTool extends AbstractTool {
name = 'imageProcess' name = 'imageProcess'
#availableImages = [] #availableImages = []
#initialized = false #initialized = false
#currentImageIndex = 0
#totalImages = 0
#needReprocess = false
parameters = { parameters = {
properties: { properties: {
@ -63,7 +66,7 @@ export class ImageProcessTool extends AbstractTool {
async initializeImageList() { async initializeImageList() {
try { try {
this.#availableImages = await fileImgList() this.#availableImages = await fileImgList()
this.#initialized = true this.#initialized = true
@ -88,6 +91,19 @@ async initializeImageList() {
} }
async processText(text, options = {}) { async processText(text, options = {}) {
// 初始化或重置图片处理信息
if (options.images?.length > 0) {
this.#totalImages = options.images.length
// 只在新的处理周期时重置索引
if (!this.#needReprocess && this.#currentImageIndex >= this.#totalImages) {
this.#currentImageIndex = 0
}
} else if (options.image) {
this.#totalImages = 1
if (!this.#needReprocess) {
this.#currentImageIndex = 0
}
}
const commands = { const commands = {
get: { get: {
@ -101,21 +117,54 @@ async initializeImageList() {
notImage: { notImage: {
regex: /NOTIMG(.*)/i, regex: /NOTIMG(.*)/i,
handler: async (match) => { handler: async (match) => {
await this.handleNotImage(match[1]?.trim()) const result = await this.handleNotImage()
if (result.success) {
// 标记需要重新处理当前图片
this.#needReprocess = true
return {
success: true,
continueProcess: true,
currentIndex: this.#currentImageIndex, // 保持当前索引不变
switchRole: result.switchRole,
needResponse: result.needResponse,
reprocess: true // 添加重新处理标记
}
}
return true return true
} }
}, },
download: { download: {
regex: /DOWNIMG:\s*(.+)/i, regex: /DOWNIMG:\s*(.+)/i,
handler: async (match) => { handler: async (match) => {
await this.handleDownloadImage(match[1].trim(), options.image) const baseName = match[1].trim()
if (options.images) {
const imageName = this.#totalImages === 1
? baseName
: `${baseName}_${this.#currentImageIndex + 1}`
await this.handleDownloadImage(imageName, options.images[this.#currentImageIndex])
// 处理完当前图片后,重置重新处理标记
this.#needReprocess = false
// 只有在不是重新处理时才增加索引
if (this.#currentImageIndex < this.#totalImages - 1) {
this.#currentImageIndex++
return {
success: true,
continueProcess: true,
currentIndex: this.#currentImageIndex
}
}
} else if (options.image) {
await this.handleDownloadImage(baseName, options.image)
}
return true return true
} }
}, },
list: { list: {
regex: /^(?:表情包列表|查看表情包|列出表情包)$/i, regex: /^(?:表情包列表|查看表情包|列出表情包)$/i,
handler: async () => { handler: async () => {
await this.e.reply(this.getImagesPrompt()) await this.e.reply(await this.getImagesPrompt())
return true return true
} }
} }
@ -125,14 +174,14 @@ async initializeImageList() {
const match = text.match(regex) const match = text.match(regex)
if (match) { if (match) {
try { try {
return await handler(match) const result = await handler(match)
return result
} catch (error) { } catch (error) {
await this.e.reply(`处理失败: ${error.message}`) await this.e.reply(`处理失败: ${error.message}`)
return true return true
} }
} }
} }
return null return null
} }
async handleGetImage(imageName) { async handleGetImage(imageName) {
@ -157,7 +206,6 @@ async initializeImageList() {
if (!imageName || !imageData) { if (!imageName || !imageData) {
throw new Error('需要提供图片名称和数据') throw new Error('需要提供图片名称和数据')
} }
try { try {
const text = `DOWNIMG: ${imageName}` const text = `DOWNIMG: ${imageName}`
await downImg(this.e, imageData, text) await downImg(this.e, imageData, text)
@ -170,14 +218,22 @@ async initializeImageList() {
async handleNotImage() { async handleNotImage() {
try { try {
this.ALLRole = this.previousRole return {
await this.bymGo(true) success: true,
return true switchRole: this.previousRole,
needResponse: true
}
} catch (error) { } catch (error) {
throw error throw error
} }
} }
getCurrentProgress() {
return {
currentIndex: this.#currentImageIndex,
totalImages: this.#totalImages,
isProcessing: this.#currentImageIndex < this.#totalImages
}
}
description = `图片处理工具:支持发送本地表情包、保存新表情包、切换处理模式。 description = `图片处理工具:支持发送本地表情包、保存新表情包、切换处理模式。
@ -185,20 +241,27 @@ async initializeImageList() {
- GETIMG: <表情包名称> - 发送指定表情包 - GETIMG: <表情包名称> - 发送指定表情包
- DOWNIMG: <名称> - 保存当前图片为表情包 - DOWNIMG: <名称> - 保存当前图片为表情包
- NOTIMG - 切换到文本模式 - NOTIMG - 切换到文本模式
- 表情包列表 - 查看所有可用表情包` - 表情包列表 - 查看所有可用表情包
\n\n多图片处理说明
- 当处理多张图片时会自动为每张图片添加序号后缀
- 例如DOWNIMG: happy 命令处理多张图片时会保存为 happy_1, happy_2 \n`
// 添加 getSystemPrompt 方法 // 添加 getSystemPrompt 方法
async getSystemPrompt() { async getSystemPrompt() {
const images = await this.getAvailableImages() const images = await this.getAvailableImages()
const progress = this.getCurrentProgress()
let prompt = `${this.description}\n` let prompt = `${this.description}\n`
if (progress.isProcessing) {
prompt += `\n当前正在处理第 ${progress.currentIndex + 1}/${progress.totalImages} 张图片\n`
}
if (images.length > 0) { if (images.length > 0) {
prompt += `\n当前可用的表情包:\n${images.join('\n')}` prompt += `\n当前可用的表情包:\n${images.join('\n')}`
prompt += `\n使用 GETIMG: <表情包名称> 来发送表情包` prompt += `\n使用 GETIMG: <表情包名称> 来发送表情包`
} else { } else {
logger.warn('[ImageProcessTool] 没有可用的表情包')
prompt += '\n当前没有可用的表情包' prompt += '\n当前没有可用的表情包'
} }
return prompt return prompt
} }
} }