mirror of
https://github.com/ikechan8370/chatgpt-plugin.git
synced 2025-12-17 22:07:10 +00:00
feat(image-tool): 被说写答辩了(
This commit is contained in:
parent
541ad12023
commit
e3fe14c6e6
3 changed files with 412 additions and 197 deletions
200
utils/ToDoimg.js
200
utils/ToDoimg.js
|
|
@ -2,127 +2,143 @@ import fs from "fs";
|
|||
import pathModule from 'path';
|
||||
import { fileTypeFromBuffer } from 'file-type';
|
||||
import moment from 'moment';
|
||||
const _path = process.cwd();
|
||||
const path = _path + "/temp/tp-bq";
|
||||
|
||||
// 没文件夹就创建一个
|
||||
if (!fs.existsSync(path)) {
|
||||
fs.mkdirSync(path, { recursive: true })
|
||||
}
|
||||
if (!fs.existsSync(pathModule.join(path, 'pictures'))) {
|
||||
fs.mkdirSync(pathModule.join(path, 'pictures'), { recursive: true })
|
||||
}
|
||||
// 配置
|
||||
const ROOT_PATH = process.cwd();
|
||||
const PICTURES_DIR = pathModule.join(ROOT_PATH, "temp/tp-bq", "pictures");
|
||||
|
||||
// 工具函数
|
||||
const createDirIfNotExists = (dir) => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeFilename = (name) => {
|
||||
return name
|
||||
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||||
.replace(/[\[\]]/g, '')
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9\u4e00-\u9fa5-_.]/g, '-');
|
||||
};
|
||||
|
||||
const findMatchingFiles = (fileList, tag) => {
|
||||
const sanitizedTag = sanitizeFilename(tag);
|
||||
let matches = fileList.filter(file => file === sanitizedTag);
|
||||
if (matches.length === 0) {
|
||||
matches = fileList.filter(file => file.startsWith(sanitizedTag));
|
||||
}
|
||||
if (matches.length === 0) {
|
||||
matches = fileList.filter(file => file.includes(sanitizedTag));
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
// 初始化目录
|
||||
createDirIfNotExists(PICTURES_DIR);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} e - 输入的消息
|
||||
* @param {*} tag - 表情包标签
|
||||
* @returns
|
||||
* 获取并发送表情包
|
||||
* @param {Object} e - 消息对象
|
||||
* @param {string} tag - 表情包标签
|
||||
* @returns {Promise<boolean|undefined>}
|
||||
*/
|
||||
export async function getToimg(e, tag) {
|
||||
const picturesPath = pathModule.join(path, 'pictures');
|
||||
const fileImgList = await fs.promises.readdir(picturesPath);
|
||||
|
||||
try {
|
||||
const sanitizedTag = tag
|
||||
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
||||
.replace(/[\[\]]/g, '')
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9\u4e00-\u9fa5-_.]/g, '-');
|
||||
let matchedFiles = fileImgList.filter(file => file === sanitizedTag);
|
||||
// 读取文件列表
|
||||
const fileList = await fs.promises.readdir(PICTURES_DIR);
|
||||
const matchedFiles = findMatchingFiles(fileList, tag);
|
||||
|
||||
if (matchedFiles.length === 0) {
|
||||
matchedFiles = fileImgList.filter(file => file.startsWith(sanitizedTag));
|
||||
}
|
||||
if (matchedFiles.length === 0) {
|
||||
matchedFiles = fileImgList.filter(file => file.includes(sanitizedTag));
|
||||
}
|
||||
if (matchedFiles.length === 0) {
|
||||
logger.warn(`未找到匹配的表情包: ${sanitizedTag}`);
|
||||
logger.warn(`未找到匹配的表情包: ${tag}`);
|
||||
return;
|
||||
}
|
||||
// 随机选择一个文件
|
||||
|
||||
// 随机选择文件
|
||||
const selectedFile = matchedFiles[Math.floor(Math.random() * matchedFiles.length)];
|
||||
const picPath = pathModule.join(picturesPath, selectedFile);
|
||||
const picPath = pathModule.join(PICTURES_DIR, selectedFile);
|
||||
|
||||
try {
|
||||
await fs.promises.access(picPath);
|
||||
await e.reply(segment.image('file:///' + picPath));
|
||||
logger.info(`发送表情包: ${picPath}`);
|
||||
return false;
|
||||
} catch {
|
||||
logger.warn(`找不到指定的表情包文件: ${picPath}`);
|
||||
return;
|
||||
}
|
||||
e.reply(segment.image('file:///' + picPath));
|
||||
|
||||
logger.info(`发送表情包: ${picPath}`);
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error('Error in getToimg:', error);
|
||||
logger.error('获取表情包失败:', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} e - 输入的消息
|
||||
* @param {*} image - 图片Base64
|
||||
* @returns
|
||||
* 保存表情包
|
||||
* @param {Object} e - 消息对象
|
||||
* @param {string} image - Base64图片数据
|
||||
* @param {string} text - 命令文本
|
||||
* @returns {Promise<boolean|undefined>}
|
||||
*/
|
||||
export async function downImg(e, image, t) {
|
||||
try {
|
||||
let reply;
|
||||
if (e.source) {
|
||||
if (e.isGroup) {
|
||||
reply = (await e.group.getChatHistory(e.source.seq, 1)).pop()?.message;
|
||||
} else {
|
||||
reply = (await e.friend.getChatHistory(e.source.time, 1)).pop()?.message;
|
||||
if (!e.img && !image) {
|
||||
return false;
|
||||
}
|
||||
if (reply) {
|
||||
for (let val of reply) {
|
||||
if (val.type === "image") {
|
||||
e.img = [val.url];
|
||||
break;
|
||||
let kWordReg = /^#?(DOWNIMG:)\s*(.*)/i;
|
||||
t = t.replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
|
||||
const match = kWordReg.exec(t);
|
||||
if (!match) {
|
||||
logger.error('DOWNIMG command format invalid:', t);
|
||||
return;
|
||||
}
|
||||
let rawmsg = match[2] || "defaultTag";
|
||||
let kWord = rawmsg.replace(/,|,|、| |。/g, "-")
|
||||
.replace(/--+/g, "-")
|
||||
.replace(/^-|-$|--/g, "")
|
||||
.trim() || "defaultTag";
|
||||
|
||||
if (image) {
|
||||
const imageBuffer = Buffer.from(image, 'base64');
|
||||
const type = await fileTypeFromBuffer(imageBuffer);
|
||||
let picType = 'png';
|
||||
if (type && type.ext) {
|
||||
picType = type.ext;
|
||||
}
|
||||
}
|
||||
const currentTime = moment().format("YYMMDDHHmmss");
|
||||
const safeTag = kWord.replace(/[^a-zA-Z0-9\u4e00-\u9fa5-_]/g, '-');
|
||||
const picPath = pathModule.join(PICTURES_DIR, 'pictures', `${currentTime}-${safeTag.substring(0, 200)}.${picType}`);
|
||||
logger.mark("DOWNIMG:", picPath);
|
||||
|
||||
if (!fs.existsSync(pathModule.join(PICTURES_DIR, 'pictures'))) {
|
||||
fs.mkdirSync(pathModule.join(PICTURES_DIR, 'pictures'), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(picPath, imageBuffer);
|
||||
logger.info(`图片已保存,标签为:${kWord}`);
|
||||
return true; // 返回成功标志
|
||||
}
|
||||
}
|
||||
if (!e.img && !image) {
|
||||
return false;
|
||||
}
|
||||
let kWordReg = /^#?(DOWNIMG:)\s*(.*)/i;
|
||||
t = t.replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
|
||||
const match = kWordReg.exec(t);
|
||||
if (!match) {
|
||||
logger.error('DOWNIMG command format invalid:', t);
|
||||
return;
|
||||
}
|
||||
let rawmsg = match[2] || "defaultTag";
|
||||
let kWord = rawmsg.replace(/,|,|、| |。/g, "-").replace(/--+/g, "-").replace(/^-|-$|--/g, "").trim() || "defaultTag";
|
||||
if (image) {
|
||||
const imageBuffer = Buffer.from(image, 'base64');
|
||||
const type = await fileTypeFromBuffer(imageBuffer);
|
||||
let picType = 'png';
|
||||
if (type && type.ext) {
|
||||
picType = type.ext;
|
||||
}
|
||||
const currentTime = moment().format("YYMMDDHHmmss");
|
||||
const safeTag = kWord.replace(/[^a-zA-Z0-9\u4e00-\u9fa5-_]/g, '-');
|
||||
const picPath = pathModule.join(path, 'pictures', `${currentTime}-${safeTag.substring(0, 200)}.${picType}`);
|
||||
logger.mark("DOWNIMG:", picPath);
|
||||
if (!fs.existsSync(pathModule.join(path, 'pictures'))) {
|
||||
fs.mkdirSync(pathModule.join(path, 'pictures'), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(picPath, imageBuffer);
|
||||
logger.info(`图片已保存,标签为:${kWord}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Error in downImg:', error);
|
||||
logger.error("保存图片时发生错误");
|
||||
logger.error('Error in downImg:', error);
|
||||
logger.error("保存图片时发生错误");
|
||||
return false; // 返回失败标志
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表情包列表
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export async function fileImgList() {
|
||||
const picturesPath = pathModule.join(path, 'pictures');
|
||||
const ImgList = await fs.promises.readdir(picturesPath);
|
||||
const fileImgList = ImgList.map(filename => {
|
||||
const match = filename.match(/\d{12}-(.+)$/);
|
||||
return match ? match[1] : filename;
|
||||
});
|
||||
return fileImgList;
|
||||
}
|
||||
try {
|
||||
const files = await fs.promises.readdir(PICTURES_DIR);
|
||||
return files
|
||||
.map(filename => {
|
||||
const match = filename.match(/\d{12}-(.+)$/);
|
||||
return match ? match[1] : filename;
|
||||
})
|
||||
.filter(Boolean);
|
||||
} catch (error) {
|
||||
logger.error('读取表情包列表失败:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
210
utils/tools/ImageTool.js
Normal file
210
utils/tools/ImageTool.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
import { AbstractTool } from './AbstractTool.js'
|
||||
import { getToimg, downImg, fileImgList } from '../../utils/ToDoimg.js'
|
||||
|
||||
export class ImageProcessTool extends AbstractTool {
|
||||
name = 'imageProcess'
|
||||
#availableImages = []
|
||||
#initialized = false
|
||||
|
||||
parameters = {
|
||||
properties: {
|
||||
action: {
|
||||
type: 'string',
|
||||
enum: ['get', 'download', 'notImage', 'listImages'],
|
||||
description: 'Action to perform: get (local image), download (save image), notImage (switch mode), or listImages'
|
||||
},
|
||||
imageName: {
|
||||
type: 'string',
|
||||
description: 'Name or tag of the image'
|
||||
},
|
||||
imageData: {
|
||||
type: 'string',
|
||||
description: 'Base64 image data for download action'
|
||||
}
|
||||
},
|
||||
required: ['action']
|
||||
}
|
||||
|
||||
constructor(e, previousRole, bymGo) {
|
||||
super()
|
||||
this.e = e
|
||||
this.previousRole = previousRole
|
||||
this.bymGo = bymGo
|
||||
this.initializeImageList()
|
||||
}
|
||||
|
||||
async func(opts) {
|
||||
const { action, imageName, imageData } = opts
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'get':
|
||||
const getResult = await this.handleGetImage(imageName)
|
||||
return getResult
|
||||
|
||||
case 'download':
|
||||
const downloadResult = await this.handleDownloadImage(imageName, imageData)
|
||||
return downloadResult
|
||||
|
||||
case 'notImage':
|
||||
const notImageResult = await this.handleNotImage()
|
||||
return notImageResult
|
||||
|
||||
case 'listImages':
|
||||
return this.getImagesPrompt() // 直接返回列表文本,让模型知道有哪些表情包
|
||||
|
||||
default:
|
||||
throw new Error(`未知操作: ${action}`)
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async initializeImageList() {
|
||||
try {
|
||||
this.#availableImages = await fileImgList()
|
||||
this.#initialized = true
|
||||
} catch (error) {
|
||||
this.#availableImages = []
|
||||
}
|
||||
}
|
||||
|
||||
async getAvailableImages() {
|
||||
if (!this.#initialized) {
|
||||
await this.initializeImageList()
|
||||
}
|
||||
return this.#availableImages
|
||||
}
|
||||
|
||||
async getImagesPrompt() {
|
||||
const images = await this.getAvailableImages()
|
||||
if (images.length === 0) {
|
||||
return '当前没有可用的表情包'
|
||||
}
|
||||
return `可用的表情包列表(共 ${images.length} 个):\n${images.join('\n')}\n\n使用方法:发送 GETIMG: <表情包名称> 来使用表情包`
|
||||
}
|
||||
|
||||
async processText(text, options = {}) {
|
||||
|
||||
const commands = {
|
||||
get: {
|
||||
regex: /GETIMG:\s*([\s\S]+?)\s*$/i,
|
||||
handler: async (match) => {
|
||||
const imageName = match[1].trim()
|
||||
await this.handleGetImage(imageName)
|
||||
return true
|
||||
}
|
||||
},
|
||||
notImage: {
|
||||
regex: /NOTIMG(.*)/i,
|
||||
handler: async (match) => {
|
||||
await this.handleNotImage(match[1]?.trim())
|
||||
return true
|
||||
}
|
||||
},
|
||||
download: {
|
||||
regex: /DOWNIMG:\s*(.+)/i,
|
||||
handler: async (match) => {
|
||||
await this.handleDownloadImage(match[1].trim(), options.image)
|
||||
return true
|
||||
}
|
||||
},
|
||||
list: {
|
||||
regex: /^(?:表情包列表|查看表情包|列出表情包)$/i,
|
||||
handler: async () => {
|
||||
await this.e.reply(this.getImagesPrompt())
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [type, { regex, handler }] of Object.entries(commands)) {
|
||||
const match = text.match(regex)
|
||||
if (match) {
|
||||
try {
|
||||
return await handler(match)
|
||||
} catch (error) {
|
||||
await this.e.reply(`处理失败: ${error.message}`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
async handleGetImage(imageName) {
|
||||
if (!imageName) {
|
||||
throw new Error('需要指定表情包名称')
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getToimg(this.e, imageName)
|
||||
|
||||
if (result === undefined) {
|
||||
await this.e.reply(`未找到匹配的表情包: ${imageName}`)
|
||||
}
|
||||
|
||||
return true // 表示已处理
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async handleDownloadImage(imageName, imageData) {
|
||||
if (!imageName || !imageData) {
|
||||
throw new Error('需要提供图片名称和数据')
|
||||
}
|
||||
|
||||
try {
|
||||
const text = `DOWNIMG: ${imageName}`
|
||||
await downImg(this.e, imageData, text)
|
||||
await this.initializeImageList()
|
||||
return true
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async handleNotImage() {
|
||||
try {
|
||||
this.ALLRole = this.previousRole
|
||||
await this.bymGo(true)
|
||||
return true
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
description = `图片处理工具:支持发送本地表情包、保存新表情包、切换处理模式。
|
||||
可用命令:
|
||||
- GETIMG: <表情包名称> - 发送指定表情包
|
||||
- DOWNIMG: <名称> - 保存当前图片为表情包
|
||||
- NOTIMG - 切换到文本模式
|
||||
- 表情包列表 - 查看所有可用表情包`
|
||||
// 添加 getSystemPrompt 方法
|
||||
async getSystemPrompt() {
|
||||
const images = await this.getAvailableImages()
|
||||
let prompt = `${this.description}\n`
|
||||
|
||||
if (images.length > 0) {
|
||||
prompt += `\n当前可用的表情包:\n${images.join('\n')}`
|
||||
prompt += `\n使用 GETIMG: <表情包名称> 来发送表情包`
|
||||
} else {
|
||||
logger.warn('[ImageProcessTool] 没有可用的表情包')
|
||||
prompt += '\n当前没有可用的表情包'
|
||||
}
|
||||
|
||||
return prompt
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeImageTool(e, previousRole, bymGo) {
|
||||
const tool = new ImageProcessTool(e, previousRole, bymGo)
|
||||
await tool.initializeImageList() // 确保初始化完成
|
||||
return tool
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue