From 44bbf7582e9273f4c27f0fdfee462dc9015a903a Mon Sep 17 00:00:00 2001 From: Nanako <469449812@qq.com> Date: Tue, 10 Dec 2024 01:12:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E4=B8=BA=E4=BD=BF=E7=94=A8API?= =?UTF-8?q?=E8=AE=BF=E9=97=AE=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PicSearcherApp.js | 276 +++++++++++++++++----------- PicSearcherApp.ts | 450 +++++++++++++++++++++++++++++----------------- 2 files changed, 455 insertions(+), 271 deletions(-) diff --git a/PicSearcherApp.js b/PicSearcherApp.js index 12bcd33..97ebe7a 100644 --- a/PicSearcherApp.js +++ b/PicSearcherApp.js @@ -5,6 +5,67 @@ exports.processMessageImages = processMessageImages; exports.getFileBlob = getFileBlob; const App_1 = require("@rocket.chat/apps-engine/definition/App"); const messages_1 = require("@rocket.chat/apps-engine/definition/messages"); +const settings_1 = require("@rocket.chat/apps-engine/definition/settings"); +var SourceType; +(function (SourceType) { + SourceType["Pixiv"] = "Pixiv"; + SourceType["DeviantArt"] = "DeviantArt"; + SourceType["AniDB"] = "AniDB"; + SourceType["MyAnimeList"] = "MyAnimeList"; + SourceType["AniList"] = "AniList"; + SourceType["BCY"] = "BCY"; + SourceType["EHentai"] = "E-Hentai"; + SourceType["IMDB"] = "IMDB"; + SourceType["Twitter"] = "Twitter"; + SourceType["Danbooru"] = "Danbooru"; + SourceType["YandeRe"] = "Yande.re"; + SourceType["Gelbooru"] = "Gelbooru"; + SourceType["AnimePictures"] = "AnimePictures"; + SourceType["Unknown"] = "Unknown"; +})(SourceType || (SourceType = {})); +function getUrlType(url) { + const lowerUrl = url.toLowerCase(); + if (lowerUrl.includes('pixiv.net')) { + return SourceType.Pixiv; + } + if (lowerUrl.includes('twitter.com') || lowerUrl.includes('x.com')) { + return SourceType.Twitter; + } + if (lowerUrl.includes('danbooru.donmai.us')) { + return SourceType.Danbooru; + } + if (lowerUrl.includes('yande.re')) { + return SourceType.YandeRe; + } + if (lowerUrl.includes('gelbooru.com')) { + return SourceType.Gelbooru; + } + if (lowerUrl.includes('anime-pictures.net')) { + return SourceType.AnimePictures; + } + if (lowerUrl.includes('deviantart.com')) { + return SourceType.DeviantArt; + } + if (lowerUrl.includes('anidb.net')) { + return SourceType.AniDB; + } + if (lowerUrl.includes('myanimelist.net')) { + return SourceType.MyAnimeList; + } + if (lowerUrl.includes('anilist.co')) { + return SourceType.AniList; + } + if (lowerUrl.includes('bcy.net')) { + return SourceType.BCY; + } + if (lowerUrl.includes('e-hentai.org')) { + return SourceType.EHentai; + } + if (lowerUrl.includes('imdb.com')) { + return SourceType.IMDB; + } + return SourceType.Unknown; +} const defaultSauceNAOResponse = { header: { user_id: '', @@ -14,7 +75,7 @@ const defaultSauceNAOResponse = { long_remaining: 0, short_remaining: 0, status: 0, - results_requested: 0, + results_requested: '', index: {}, search_depth: '', minimum_similarity: 0, @@ -30,10 +91,12 @@ class PicSearcherApp extends App_1.App { constructor(info, logger, accessors) { super(info, logger, accessors); this.rootUrl = ''; + this.sauceNAOApiKey = ''; myLogger = logger; } async initialize(configurationExtend, environmentRead) { this.rootUrl = await environmentRead.getEnvironmentVariables().getValueByName('ROOT_URL'); + this.sauceNAOApiKey = await environmentRead.getSettings().getValueById('SauceNAOApiKey'); myLogger.log('rootUrl:', this.rootUrl); return super.initialize(configurationExtend, environmentRead); } @@ -47,9 +110,26 @@ class PicSearcherApp extends App_1.App { } myLogger.info('message.attachments:', message.attachments); const imageBlobs = await processMessageImages(message, http, read); - const result = await searchImageOnSauceNAO(imageBlobs[0].blob, imageBlobs[0].suffix); + const result = await searchImageOnSauceNAO(this.sauceNAOApiKey, imageBlobs[0].blob, imageBlobs[0].suffix); await sendSearchResults(modify, message.room, result, author); } + async onSettingUpdated(setting, configurationModify, read, http) { + if (setting.id === 'SauceNAOApiKey') { + this.sauceNAOApiKey = setting.value; + } + return super.onSettingUpdated(setting, configurationModify, read, http); + } + async extendConfiguration(configuration, environmentRead) { + await configuration.settings.provideSetting({ + id: 'SauceNAOApiKey', + type: settings_1.SettingType.STRING, + packageValue: '', + required: true, + public: false, + i18nLabel: 'searcher_api_key_label', + i18nDescription: 'searcher_api_key_description', + }); + } async sendMessage(textMessage, room, author, modify) { const messageBuilder = modify.getCreator().startMessage({ text: textMessage, @@ -92,79 +172,21 @@ async function getFileBlob(fileId, read) { } return new Blob(); } -async function searchImageOnSauceNAO(image, suffix) { +async function searchImageOnSauceNAO(apiKey, image, suffix) { const formData = new FormData(); formData.append('file', image, 'image.' + suffix); + formData.append('api_key', apiKey); + formData.append('output_type', '2'); try { const response = await fetch('https://saucenao.com/search.php', { method: 'POST', body: formData, }); if (response.ok) { - const results = []; - const html = await response.text(); - const resultBlocks = html.match(/
\s*([\s\S]*?)<\/table>\s*<\/div>/g) || []; - resultBlocks.forEach((block) => { - const result = { - similarity: '', - title: '', - thumbnailUrl: '', - sourceUrl: '', - creator: '', - creatorUrl: '', - source: '', - id: '', - }; - const similarityMatch = block.match(/
(\d+\.\d+)%<\/div>/); - if (similarityMatch) { - result.similarity = similarityMatch[1]; - } - const titleMatch = block.match(/
(.*?)<\/strong>/); - if (titleMatch) { - result.title = titleMatch[1]; - } - const thumbnailMatch = block.match(/src="(https:\/\/img\d\.saucenao\.com\/[^"]+)"/); - if (thumbnailMatch) { - result.thumbnailUrl = thumbnailMatch[1]; - } - const sourceMatch = block.match(/(?:pixiv ID|dA ID|Tweet ID):\s*<\/strong>]*>(\d+)<\/a>/); - if (sourceMatch) { - result.sourceUrl = sourceMatch[1]; - result.id = sourceMatch[2]; - } - const creatorMatch = block.match(/(?:Member|Author|Twitter):\s*<\/strong>]*>(.*?)<\/a>/); - if (creatorMatch) { - result.creatorUrl = creatorMatch[1]; - result.creator = creatorMatch[2]; - } - const characterSection = block.match(/Characters:\s*<\/strong>(.*?)<(?:br|\/div)/); - if (characterSection && characterSection[1]) { - const chars = characterSection[1].match(/"([^"]+)"/g); - if (chars) { - result.characters = chars - .map((char) => char.replace(/"/g, '')) - .join(', '); - } - } - if (block.includes('pixiv ID:')) { - result.source = 'Pixiv'; - } - else if (block.includes('dA ID:')) { - result.source = 'DeviantArt'; - } - else if (block.includes('E-Hentai')) { - result.source = 'E-Hentai'; - } - else if (block.includes('Twitter')) { - result.source = 'Twitter'; - } - results.push(result); - }); - if (results.length === 0) { - SauceNAOErrStr = html; - } + const json = await response.text(); + const result = JSON.parse(json); SauceNAOErrStr = ''; - return results; + return result; } else { console.error('请求失败:', response.status, response.statusText); @@ -175,7 +197,7 @@ async function searchImageOnSauceNAO(image, suffix) { console.error('搜索请求出错:', error); SauceNAOErrStr = '搜索请求出错: ' + error; } - return []; + return defaultSauceNAOResponse; } function getInterpolatedColor(similarity) { const clampedSimilarity = Math.max(70, Math.min(90, similarity)); @@ -187,65 +209,105 @@ function getInterpolatedColor(similarity) { return `#${redHex}${greenHex}00`; } async function sendSearchResults(modify, room, results, appUser) { - const attachments = results.map((result, index) => { - var _a, _b; - const similarityValue = parseFloat(result.similarity.replace('%', '')); + const attachments = []; + results.results.sort((a, b) => { + if (a.data.ext_urls && !b.data.ext_urls) { + return -1; + } + if (!a.data.ext_urls && b.data.ext_urls) { + return 1; + } + return b.header.similarity - a.header.similarity; + }); + for (const result of results.results) { + const header = result.header; + const data = result.data; + const similarityValue = header.similarity; + if (similarityValue < 70) { + continue; + } const color = getInterpolatedColor(similarityValue); - let textStr = result.source + ': ' + result.id; - if (result.sourceUrl) { - textStr += '\n' + result.sourceUrl; - } - if (result.characters) { - textStr += '\n' + 'Characters: ' + result.characters; - } const attachmentObject = { title: { - value: `${result.title} (${result.similarity}%)`, + value: `${data.title} (${header.similarity}%)`, }, - author: { - name: result.creator, - }, - text: textStr, - thumbnailUrl: result.thumbnailUrl, - collapsed: index !== 0, + thumbnailUrl: header.thumbnail, + collapsed: attachments.length !== 0, color, }; - if (!result.thumbnailUrl) { + if (!header.thumbnail) { delete attachmentObject.thumbnailUrl; } - if (textStr === ': ') { - delete attachmentObject.text; - delete attachmentObject.collapsed; + attachmentObject.actions = new Array(); + if (data.ext_urls) { + for (const extUrl of data.ext_urls) { + const urlType = getUrlType(extUrl); + attachmentObject.actions.push({ + type: messages_1.MessageActionType.BUTTON, + text: urlType.toString(), + url: extUrl, + }); + } } - if (result.sourceUrl) { - (_a = attachmentObject.actions) === null || _a === void 0 ? void 0 : _a.push({ + if (data.pixiv_id) { + attachmentObject.actions.push({ type: messages_1.MessageActionType.BUTTON, - text: '查看原图', - url: result.sourceUrl, + text: '作者Pixiv', + url: `https://www.pixiv.net/users/${data.member_id}`, }); } - if (result.creatorUrl) { - (_b = attachmentObject.actions) === null || _b === void 0 ? void 0 : _b.push({ + if (data.da_id) { + attachmentObject.actions.push({ type: messages_1.MessageActionType.BUTTON, - text: '查看作者', - url: result.creatorUrl, + text: '作者DeviantArt', + url: data.author_url, }); } - if (result.characters) { - attachmentObject.fields = [{ - short: false, - title: 'Characters', - value: result.characters, - }]; + if (data.bcy_id) { + attachmentObject.actions.push({ + type: messages_1.MessageActionType.BUTTON, + text: '作者BCY', + url: `https://bcy.net/u/${data.member_link_id}`, + }); } - return attachmentObject; - }); - const text = results.length > 0 ? '以下是查找到的结果:' : SauceNAOErrStr.length > 0 ? SauceNAOErrStr : '没有找到相关结果'; + if (data.anidb_aid) { + attachmentObject.actions.push({ + type: messages_1.MessageActionType.BUTTON, + text: 'AniDB', + url: `https://anidb.net/anime/${data.anidb_aid}`, + }); + } + if (data.mal_id) { + attachmentObject.actions.push({ + type: messages_1.MessageActionType.BUTTON, + text: 'MyAnimeList', + url: `https://myanimelist.net/anime/${data.mal_id}`, + }); + } + if (data.anilist_id) { + attachmentObject.actions.push({ + type: messages_1.MessageActionType.BUTTON, + text: 'AniList', + url: `https://anilist.co/anime/${data.anilist_id}`, + }); + } + if (data.imdb_id) { + attachmentObject.actions.push({ + type: messages_1.MessageActionType.BUTTON, + text: 'IMDb', + url: `https://www.imdb.com/title/${data.imdb_id}`, + }); + } + attachments.push(attachmentObject); + } + const successStr = `搜索成功,短时间内剩余查询次数:${results.header.short_remaining}/${results.header.short_limit},长时间内剩余查询次数:${results.header.long_remaining}/${results.header.long_limit}`; + const text = attachments.length > 0 ? successStr : SauceNAOErrStr.length > 0 ? SauceNAOErrStr : '没有找到相关结果'; const messageBuilder = modify.getCreator().startMessage() .setRoom(room) .setUsernameAlias(appUser.username) .setText(text) - .setAttachments(attachments); + .setAttachments(attachments) + .setGroupable(true); await modify.getCreator().finish(messageBuilder); } async function urlToBlob(url) { @@ -262,7 +324,7 @@ async function urlToBlob(url) { } async function test() { const blob = await urlToBlob('https://www.z4a.net/images/2024/12/09/69054578_p0.jpg'); - const result = await searchImageOnSauceNAO(blob, 'jpg'); + const result = await searchImageOnSauceNAO('ac635e5f5011234871260aac1d37ac8360ed979c', blob, 'jpg'); console.log(result); } exports.default = PicSearcherApp; diff --git a/PicSearcherApp.ts b/PicSearcherApp.ts index 91d23be..e72d186 100644 --- a/PicSearcherApp.ts +++ b/PicSearcherApp.ts @@ -1,6 +1,6 @@ import { IAppAccessors, - IConfigurationExtend, + IConfigurationExtend, IConfigurationModify, IEnvironmentRead, IHttp, ILogger, @@ -20,55 +20,181 @@ import { import type {IMessageAction} from '@rocket.chat/apps-engine/definition/messages/IMessageAction'; import {IAppInfo} from '@rocket.chat/apps-engine/definition/metadata'; import {IRoom} from '@rocket.chat/apps-engine/definition/rooms'; +import {ISetting, SettingType} from '@rocket.chat/apps-engine/definition/settings'; import {IUser} from '@rocket.chat/apps-engine/definition/users'; -// 定义 API 响应的接口(根据 API 文档自行调整) +enum SourceType { + Pixiv = 'Pixiv', + DeviantArt = 'DeviantArt', + AniDB = 'AniDB', + MyAnimeList = 'MyAnimeList', + AniList = 'AniList', + BCY = 'BCY', + EHentai = 'E-Hentai', + IMDB = 'IMDB', + Twitter = 'Twitter', + Danbooru = 'Danbooru', + YandeRe = 'Yande.re', + Gelbooru = 'Gelbooru', + AnimePictures = 'AnimePictures', + Unknown = 'Unknown', +} + +function getUrlType(url: string): SourceType { + const lowerUrl = url.toLowerCase(); + + if (lowerUrl.includes('pixiv.net')) { + return SourceType.Pixiv; + } + if (lowerUrl.includes('twitter.com') || lowerUrl.includes('x.com')) { + return SourceType.Twitter; + } + if (lowerUrl.includes('danbooru.donmai.us')) { + return SourceType.Danbooru; + } + if (lowerUrl.includes('yande.re')) { + return SourceType.YandeRe; + } + if (lowerUrl.includes('gelbooru.com')) { + return SourceType.Gelbooru; + } + if (lowerUrl.includes('anime-pictures.net')) { + return SourceType.AnimePictures; + } + if (lowerUrl.includes('deviantart.com')) { + return SourceType.DeviantArt; + } + if (lowerUrl.includes('anidb.net')) { + return SourceType.AniDB; + } + if (lowerUrl.includes('myanimelist.net')) { + return SourceType.MyAnimeList; + } + if (lowerUrl.includes('anilist.co')) { + return SourceType.AniList; + } + if (lowerUrl.includes('bcy.net')) { + return SourceType.BCY; + } + if (lowerUrl.includes('e-hentai.org')) { + return SourceType.EHentai; + } + if (lowerUrl.includes('imdb.com')) { + return SourceType.IMDB; + } + + return SourceType.Unknown; +} + +// 主要响应接口 interface ISauceNAOResponse { - header: { - user_id: string; - account_type: string; - short_limit: string; - long_limit: string; - long_remaining: number; - short_remaining: number; - status: number; - results_requested: number; - index: any; - search_depth: string; - minimum_similarity: number; - query_image_display: string; - query_image: string; - results_returned: number; - }; - results: Array<{ - header: { - similarity: string; - thumbnail: string; - index_id: number; - index_name: string; - dupes: number; - hidden: number; - }; - data: { - ext_urls: Array, - title: string, - da_id: string, - author_name: string, - author_url: string, - } - }>; + header: IHeader; + results: Array; } -interface ISearchResult { - similarity: string; + +// 头部信息接口 +interface IHeader { + // 用户ID + user_id: string; + // 账户类型 + account_type: string; + // 短时限 + short_limit: string; + // 长时限 + long_limit: string; + // 剩余长时限 + long_remaining: number; + // 剩余短时限 + short_remaining: number; + // 状态 + status: number; + // 请求结果 + results_requested: string; + // 索引 + index: Record; + // 搜索深度 + search_depth: string; + // 最小相似度 + minimum_similarity: number; + // 查询图片显示 + query_image_display: string; + // 查询图片 + query_image: string; + // 返回结果数 + results_returned: number; +} + +// 索引数据接口 +interface IIndexData { + // 状态 + status: number; + // 父ID + parent_id: number; + // ID + id: number; + // 结果 + results: number; +} + +// 搜索结果接口 +interface IResult { + header: IResultHeader; + data: IResultData; +} + +// 结果头部信息接口 +interface IResultHeader { + // 相似度 + similarity: number; + // 缩略图 + thumbnail: string; + // 索引ID + index_id: number; + // 索引名称 + index_name: string; + dupes: number; + // 隐藏 + hidden: number; +} + +// 结果数据接口 +interface IResultData { + ext_urls?: Array; title?: string; - thumbnailUrl?: string; - sourceUrl?: string; - creator?: string; - creatorUrl?: string; + + // Pixiv 特有字段 + pixiv_id?: number; + member_name?: string; + member_id?: number; + + // DeviantArt 特有字段 + da_id?: string; + author_name?: string; + author_url?: string; + + // E-Hentai 特有字段 source?: string; - id?: string; - characters?: string; + creator?: Array; + eng_name?: string; + jp_name?: string; + + // BCY 特有字段 + bcy_id?: number; + member_link_id?: number; + bcy_type?: string; + + // 动画特有字段 + anidb_aid?: number; + mal_id?: number; + anilist_id?: number; + part?: string; + year?: string; + est_time?: string; + + // 电影特有字段 + imdb_id?: string; } + interface IImageBlob { blob: Blob; suffix: string; @@ -83,7 +209,7 @@ const defaultSauceNAOResponse: ISauceNAOResponse = { long_remaining: 0, short_remaining: 0, status: 0, - results_requested: 0, + results_requested: '', index: {}, search_depth: '', minimum_similarity: 0, @@ -99,6 +225,7 @@ let SauceNAOErrStr: string; export class PicSearcherApp extends App implements IPostMessageSent { private rootUrl: string = ''; + private sauceNAOApiKey: string = ''; constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); @@ -107,6 +234,7 @@ export class PicSearcherApp extends App implements IPostMessageSent { public async initialize(configurationExtend: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise { this.rootUrl = await environmentRead.getEnvironmentVariables().getValueByName('ROOT_URL'); + this.sauceNAOApiKey = await environmentRead.getSettings().getValueById('SauceNAOApiKey'); myLogger.log('rootUrl:', this.rootUrl); return super.initialize(configurationExtend, environmentRead); } @@ -123,10 +251,29 @@ export class PicSearcherApp extends App implements IPostMessageSent { } myLogger.info('message.attachments:', message.attachments); const imageBlobs = await processMessageImages(message, http, read); - const result = await searchImageOnSauceNAO(imageBlobs[0].blob, imageBlobs[0].suffix); + const result = await searchImageOnSauceNAO(this.sauceNAOApiKey, imageBlobs[0].blob, imageBlobs[0].suffix); await sendSearchResults(modify, message.room, result, author!); } + public async onSettingUpdated(setting: ISetting, configurationModify: IConfigurationModify, read: IRead, http: IHttp): Promise { + if (setting.id === 'SauceNAOApiKey') { + this.sauceNAOApiKey = setting.value; + } + return super.onSettingUpdated(setting, configurationModify, read, http); + } + + protected async extendConfiguration(configuration: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise { + await configuration.settings.provideSetting({ + id: 'SauceNAOApiKey', + type: SettingType.STRING, + packageValue: '', + required: true, + public: false, + i18nLabel: 'searcher_api_key_label', + i18nDescription: 'searcher_api_key_description', + }); + } + private async sendMessage(textMessage: string, room: IRoom, author: IUser, modify: IModify) { const messageBuilder = modify.getCreator().startMessage({ text: textMessage, @@ -186,10 +333,12 @@ export async function getFileBlob(fileId: string, read: IRead): Promise { return new Blob(); } -async function searchImageOnSauceNAO(image: Blob, suffix: string): Promise> { +async function searchImageOnSauceNAO(apiKey: string, image: Blob, suffix: string): Promise { // 1. 创建 FormData 并附加文件 const formData = new FormData(); formData.append('file', image, 'image.' + suffix); // 文件名可以随意 + formData.append('api_key', apiKey); + formData.append('output_type', '2'); // 详细输出 // 2. 发送 POST 请求到 SauceNAO try { @@ -200,78 +349,10 @@ async function searchImageOnSauceNAO(image: Blob, suffix: string): Promise = []; - const html = await response.text(); - // 匹配所有结果块,包括隐藏的 - const resultBlocks = html.match(/
\s*
([\s\S]*?)<\/table>\s*<\/div>/g) || []; - - resultBlocks.forEach((block) => { - const result: ISearchResult = { - similarity: '', - title: '', - thumbnailUrl: '', - sourceUrl: '', - creator: '', - creatorUrl: '', - source: '', - id: '', - }; - - // Extract similarity - const similarityMatch = block.match(/
(\d+\.\d+)%<\/div>/); - if (similarityMatch) { result.similarity = similarityMatch[1]; } - - // Extract title - const titleMatch = block.match(/
(.*?)<\/strong>/); - if (titleMatch) { result.title = titleMatch[1]; } - - // Extract thumbnail URL - const thumbnailMatch = block.match(/src="(https:\/\/img\d\.saucenao\.com\/[^"]+)"/); - if (thumbnailMatch) { result.thumbnailUrl = thumbnailMatch[1]; } - - // Extract source URL and ID - const sourceMatch = block.match(/(?:pixiv ID|dA ID|Tweet ID):\s*<\/strong>]*>(\d+)<\/a>/); - if (sourceMatch) { - result.sourceUrl = sourceMatch[1]; - result.id = sourceMatch[2]; - } - - // Extract creator and creator URL - const creatorMatch = block.match(/(?:Member|Author|Twitter):\s*<\/strong>]*>(.*?)<\/a>/); - if (creatorMatch) { - result.creatorUrl = creatorMatch[1]; - result.creator = creatorMatch[2]; - } - - // Extract Characters - const characterSection = block.match(/Characters:\s*<\/strong>(.*?)<(?:br|\/div)/); - if (characterSection && characterSection[1]) { - const chars = characterSection[1].match(/"([^"]+)"/g); - if (chars) { - result.characters = chars - .map((char) => char.replace(/"/g, '')) - .join(', '); - } - } - - // Determine source - if (block.includes('pixiv ID:')) { - result.source = 'Pixiv'; - } else if (block.includes('dA ID:')) { - result.source = 'DeviantArt'; - } else if (block.includes('E-Hentai')) { - result.source = 'E-Hentai'; - } else if (block.includes('Twitter')) { - result.source = 'Twitter'; - } - - results.push(result); - }); - if (results.length === 0) { - SauceNAOErrStr = html; - } + const json = await response.text(); + const result = JSON.parse(json) as ISauceNAOResponse; SauceNAOErrStr = ''; - return results; + return result; } else { console.error('请求失败:', response.status, response.statusText); SauceNAOErrStr = '请求失败: ' + response.status + ' ' + response.statusText; @@ -281,7 +362,7 @@ async function searchImageOnSauceNAO(image: Blob, suffix: string): Promise, + results: ISauceNAOResponse, appUser: IUser, ) { - const attachments: Array = results.map((result, index) => { + const attachments: Array = []; + + // 将results.results按照相似度排序,但如果ext_urls没有的话排在最后面 + results.results.sort((a, b) => { + if (a.data.ext_urls && !b.data.ext_urls) { + return -1; + } + if (!a.data.ext_urls && b.data.ext_urls) { + return 1; + } + return b.header.similarity - a.header.similarity; + }); + + for (const result of results.results) { + const header = result.header; + const data = result.data; + // From percentage string to numeric value - const similarityValue = parseFloat(result.similarity.replace('%', '')); + const similarityValue = header.similarity; + if (similarityValue < 70) { + continue; + } // Dynamically generate color const color = getInterpolatedColor(similarityValue); - let textStr = result.source + ': ' + result.id; - if (result.sourceUrl) { - textStr += '\n' + result.sourceUrl; - } - if (result.characters) { - textStr += '\n' + 'Characters: ' + result.characters; - } + const attachmentObject: IMessageAttachment = { title: { - value: `${result.title} (${result.similarity}%)`, // Title content + value: `${data.title} (${header.similarity}%)`, // Title content } as IMessageAttachmentTitle, - author: { - name: result.creator, // Source name - } as IMessageAttachmentAuthor, - text: textStr, // Image ID - thumbnailUrl: result.thumbnailUrl, // Optional: Image URL - collapsed: index !== 0, // First one expanded, others collapsed + thumbnailUrl: header.thumbnail, // Optional: Image URL + collapsed: attachments.length !== 0, // First one expanded, others collapsed color, // Dynamically set color }; - // if (!result.sourceUrl) { - // delete attachmentObject.title?.link; - // } - // if (!result.creatorUrl) { - // delete attachmentObject.author?.link; - // } - if (!result.thumbnailUrl) { + if (!header.thumbnail) { delete attachmentObject.thumbnailUrl; } - if (textStr === ': ') { - delete attachmentObject.text; - delete attachmentObject.collapsed; + attachmentObject.actions = new Array(); + + if (data.ext_urls) { + for (const extUrl of data.ext_urls) { + const urlType = getUrlType(extUrl); + attachmentObject.actions.push({ + type: MessageActionType.BUTTON, + text: urlType.toString(), + url: extUrl, + } as IMessageAction); + } } - if (result.sourceUrl) { - attachmentObject.actions?.push({ + if (data.pixiv_id) { + attachmentObject.actions.push({ type: MessageActionType.BUTTON, - text: '查看原图', - url: result.sourceUrl, + text: '作者Pixiv', + url: `https://www.pixiv.net/users/${data.member_id}`, } as IMessageAction); } - if (result.creatorUrl) { - attachmentObject.actions?.push({ + if (data.da_id) { + attachmentObject.actions.push({ type: MessageActionType.BUTTON, - text: '查看作者', - url: result.creatorUrl, + text: '作者DeviantArt', + url: data.author_url, } as IMessageAction); } - if (result.characters) { - attachmentObject.fields = [{ - short: false, - title: 'Characters', - value: result.characters, - }]; + if (data.bcy_id) { + attachmentObject.actions.push({ + type: MessageActionType.BUTTON, + text: '作者BCY', + url: `https://bcy.net/u/${data.member_link_id}`, + } as IMessageAction); + } + if (data.anidb_aid) { + attachmentObject.actions.push({ + type: MessageActionType.BUTTON, + text: 'AniDB', + url: `https://anidb.net/anime/${data.anidb_aid}`, + } as IMessageAction); + } + if (data.mal_id) { + attachmentObject.actions.push({ + type: MessageActionType.BUTTON, + text: 'MyAnimeList', + url: `https://myanimelist.net/anime/${data.mal_id}`, + } as IMessageAction); + } + if (data.anilist_id) { + attachmentObject.actions.push({ + type: MessageActionType.BUTTON, + text: 'AniList', + url: `https://anilist.co/anime/${data.anilist_id}`, + } as IMessageAction); + } + if (data.imdb_id) { + attachmentObject.actions.push({ + type: MessageActionType.BUTTON, + text: 'IMDb', + url: `https://www.imdb.com/title/${data.imdb_id}`, + } as IMessageAction); } - return attachmentObject; - }); + attachments.push(attachmentObject); + } - const text = results.length > 0 ? '以下是查找到的结果:' : SauceNAOErrStr.length > 0 ? SauceNAOErrStr : '没有找到相关结果'; + const successStr = `搜索成功,短时间内剩余查询次数:${results.header.short_remaining}/${results.header.short_limit},长时间内剩余查询次数:${results.header.long_remaining}/${results.header.long_limit}`; + const text = attachments.length > 0 ? successStr : SauceNAOErrStr.length > 0 ? SauceNAOErrStr : '没有找到相关结果'; const messageBuilder = modify.getCreator().startMessage() .setRoom(room) .setUsernameAlias(appUser.username) .setText(text) - .setAttachments(attachments); + .setAttachments(attachments) + .setGroupable(true); await modify.getCreator().finish(messageBuilder); } @@ -402,7 +524,7 @@ async function urlToBlob(url: string): Promise { // 从 URL 获取图片 Blob async function test() { const blob = await urlToBlob('https://www.z4a.net/images/2024/12/09/69054578_p0.jpg'); - const result = await searchImageOnSauceNAO(blob, 'jpg'); + const result = await searchImageOnSauceNAO('ac635e5f5011234871260aac1d37ac8360ed979c', blob, 'jpg'); console.log(result); }