改为使用API访问查询

This commit is contained in:
Nanako 2024-12-10 01:12:02 +08:00
parent 7371f2acdb
commit 44bbf7582e
2 changed files with 455 additions and 271 deletions

View File

@ -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(/<div class="result">\s*<table class="resulttable">([\s\S]*?)<\/table>\s*<\/div>/g) || [];
resultBlocks.forEach((block) => {
const result = {
similarity: '',
title: '',
thumbnailUrl: '',
sourceUrl: '',
creator: '',
creatorUrl: '',
source: '',
id: '',
};
const similarityMatch = block.match(/<div class="resultsimilarityinfo">(\d+\.\d+)%<\/div>/);
if (similarityMatch) {
result.similarity = similarityMatch[1];
}
const titleMatch = block.match(/<div class="resulttitle"><strong>(.*?)<\/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(/<strong>(?:pixiv ID|dA ID|Tweet ID):\s*<\/strong><a href="([^"]+)"[^>]*>(\d+)<\/a>/);
if (sourceMatch) {
result.sourceUrl = sourceMatch[1];
result.id = sourceMatch[2];
}
const creatorMatch = block.match(/<strong>(?:Member|Author|Twitter):\s*<\/strong><a href="([^"]+)"[^>]*>(.*?)<\/a>/);
if (creatorMatch) {
result.creatorUrl = creatorMatch[1];
result.creator = creatorMatch[2];
}
const characterSection = block.match(/<strong>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;

View File

@ -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<string>,
title: string,
da_id: string,
author_name: string,
author_url: string,
}
}>;
header: IHeader;
results: Array<IResult>;
}
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<string, IIndexData>;
// 搜索深度
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<string>;
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<string>;
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<void> {
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<void> {
if (setting.id === 'SauceNAOApiKey') {
this.sauceNAOApiKey = setting.value;
}
return super.onSettingUpdated(setting, configurationModify, read, http);
}
protected async extendConfiguration(configuration: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise<void> {
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<Blob> {
return new Blob();
}
async function searchImageOnSauceNAO(image: Blob, suffix: string): Promise<Array<ISearchResult>> {
async function searchImageOnSauceNAO(apiKey: string, image: Blob, suffix: string): Promise<ISauceNAOResponse> {
// 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<Array
// 3. 解析返回的 HTML 数据
if (response.ok) {
const results: Array<ISearchResult> = [];
const html = await response.text();
// 匹配所有结果块,包括隐藏的
const resultBlocks = html.match(/<div class="result">\s*<table class="resulttable">([\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(/<div class="resultsimilarityinfo">(\d+\.\d+)%<\/div>/);
if (similarityMatch) { result.similarity = similarityMatch[1]; }
// Extract title
const titleMatch = block.match(/<div class="resulttitle"><strong>(.*?)<\/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(/<strong>(?:pixiv ID|dA ID|Tweet ID):\s*<\/strong><a href="([^"]+)"[^>]*>(\d+)<\/a>/);
if (sourceMatch) {
result.sourceUrl = sourceMatch[1];
result.id = sourceMatch[2];
}
// Extract creator and creator URL
const creatorMatch = block.match(/<strong>(?:Member|Author|Twitter):\s*<\/strong><a href="([^"]+)"[^>]*>(.*?)<\/a>/);
if (creatorMatch) {
result.creatorUrl = creatorMatch[1];
result.creator = creatorMatch[2];
}
// Extract Characters
const characterSection = block.match(/<strong>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<Array
SauceNAOErrStr = '搜索请求出错: ' + error;
}
return [];
return defaultSauceNAOResponse;
}
/**
@ -310,79 +391,120 @@ function getInterpolatedColor(similarity: number): string {
async function sendSearchResults(
modify: IModify,
room: IRoom,
results: Array<ISearchResult>,
results: ISauceNAOResponse,
appUser: IUser,
) {
const attachments: Array<IMessageAttachment> = results.map((result, index) => {
const attachments: Array<IMessageAttachment> = [];
// 将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<IMessageAction>();
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<Blob> {
// 从 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);
}