改为使用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; exports.getFileBlob = getFileBlob;
const App_1 = require("@rocket.chat/apps-engine/definition/App"); const App_1 = require("@rocket.chat/apps-engine/definition/App");
const messages_1 = require("@rocket.chat/apps-engine/definition/messages"); 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 = { const defaultSauceNAOResponse = {
header: { header: {
user_id: '', user_id: '',
@ -14,7 +75,7 @@ const defaultSauceNAOResponse = {
long_remaining: 0, long_remaining: 0,
short_remaining: 0, short_remaining: 0,
status: 0, status: 0,
results_requested: 0, results_requested: '',
index: {}, index: {},
search_depth: '', search_depth: '',
minimum_similarity: 0, minimum_similarity: 0,
@ -30,10 +91,12 @@ class PicSearcherApp extends App_1.App {
constructor(info, logger, accessors) { constructor(info, logger, accessors) {
super(info, logger, accessors); super(info, logger, accessors);
this.rootUrl = ''; this.rootUrl = '';
this.sauceNAOApiKey = '';
myLogger = logger; myLogger = logger;
} }
async initialize(configurationExtend, environmentRead) { async initialize(configurationExtend, environmentRead) {
this.rootUrl = await environmentRead.getEnvironmentVariables().getValueByName('ROOT_URL'); this.rootUrl = await environmentRead.getEnvironmentVariables().getValueByName('ROOT_URL');
this.sauceNAOApiKey = await environmentRead.getSettings().getValueById('SauceNAOApiKey');
myLogger.log('rootUrl:', this.rootUrl); myLogger.log('rootUrl:', this.rootUrl);
return super.initialize(configurationExtend, environmentRead); return super.initialize(configurationExtend, environmentRead);
} }
@ -47,9 +110,26 @@ class PicSearcherApp extends App_1.App {
} }
myLogger.info('message.attachments:', message.attachments); myLogger.info('message.attachments:', message.attachments);
const imageBlobs = await processMessageImages(message, http, read); 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); 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) { async sendMessage(textMessage, room, author, modify) {
const messageBuilder = modify.getCreator().startMessage({ const messageBuilder = modify.getCreator().startMessage({
text: textMessage, text: textMessage,
@ -92,79 +172,21 @@ async function getFileBlob(fileId, read) {
} }
return new Blob(); return new Blob();
} }
async function searchImageOnSauceNAO(image, suffix) { async function searchImageOnSauceNAO(apiKey, image, suffix) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', image, 'image.' + suffix); formData.append('file', image, 'image.' + suffix);
formData.append('api_key', apiKey);
formData.append('output_type', '2');
try { try {
const response = await fetch('https://saucenao.com/search.php', { const response = await fetch('https://saucenao.com/search.php', {
method: 'POST', method: 'POST',
body: formData, body: formData,
}); });
if (response.ok) { if (response.ok) {
const results = []; const json = await response.text();
const html = await response.text(); const result = JSON.parse(json);
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;
}
SauceNAOErrStr = ''; SauceNAOErrStr = '';
return results; return result;
} }
else { else {
console.error('请求失败:', response.status, response.statusText); console.error('请求失败:', response.status, response.statusText);
@ -175,7 +197,7 @@ async function searchImageOnSauceNAO(image, suffix) {
console.error('搜索请求出错:', error); console.error('搜索请求出错:', error);
SauceNAOErrStr = '搜索请求出错: ' + error; SauceNAOErrStr = '搜索请求出错: ' + error;
} }
return []; return defaultSauceNAOResponse;
} }
function getInterpolatedColor(similarity) { function getInterpolatedColor(similarity) {
const clampedSimilarity = Math.max(70, Math.min(90, similarity)); const clampedSimilarity = Math.max(70, Math.min(90, similarity));
@ -187,65 +209,105 @@ function getInterpolatedColor(similarity) {
return `#${redHex}${greenHex}00`; return `#${redHex}${greenHex}00`;
} }
async function sendSearchResults(modify, room, results, appUser) { async function sendSearchResults(modify, room, results, appUser) {
const attachments = results.map((result, index) => { const attachments = [];
var _a, _b; results.results.sort((a, b) => {
const similarityValue = parseFloat(result.similarity.replace('%', '')); 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); 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 = { const attachmentObject = {
title: { title: {
value: `${result.title} (${result.similarity}%)`, value: `${data.title} (${header.similarity}%)`,
}, },
author: { thumbnailUrl: header.thumbnail,
name: result.creator, collapsed: attachments.length !== 0,
},
text: textStr,
thumbnailUrl: result.thumbnailUrl,
collapsed: index !== 0,
color, color,
}; };
if (!result.thumbnailUrl) { if (!header.thumbnail) {
delete attachmentObject.thumbnailUrl; delete attachmentObject.thumbnailUrl;
} }
if (textStr === ': ') { attachmentObject.actions = new Array();
delete attachmentObject.text; if (data.ext_urls) {
delete attachmentObject.collapsed; 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) { if (data.pixiv_id) {
(_a = attachmentObject.actions) === null || _a === void 0 ? void 0 : _a.push({ attachmentObject.actions.push({
type: messages_1.MessageActionType.BUTTON, type: messages_1.MessageActionType.BUTTON,
text: '查看原图', text: '作者Pixiv',
url: result.sourceUrl, url: `https://www.pixiv.net/users/${data.member_id}`,
}); });
} }
if (result.creatorUrl) { if (data.da_id) {
(_b = attachmentObject.actions) === null || _b === void 0 ? void 0 : _b.push({ attachmentObject.actions.push({
type: messages_1.MessageActionType.BUTTON, type: messages_1.MessageActionType.BUTTON,
text: '查看作者', text: '作者DeviantArt',
url: result.creatorUrl, url: data.author_url,
}); });
} }
if (result.characters) { if (data.bcy_id) {
attachmentObject.fields = [{ attachmentObject.actions.push({
short: false, type: messages_1.MessageActionType.BUTTON,
title: 'Characters', text: '作者BCY',
value: result.characters, url: `https://bcy.net/u/${data.member_link_id}`,
}]; });
} }
return attachmentObject; if (data.anidb_aid) {
}); attachmentObject.actions.push({
const text = results.length > 0 ? '以下是查找到的结果:' : SauceNAOErrStr.length > 0 ? SauceNAOErrStr : '没有找到相关结果'; 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() const messageBuilder = modify.getCreator().startMessage()
.setRoom(room) .setRoom(room)
.setUsernameAlias(appUser.username) .setUsernameAlias(appUser.username)
.setText(text) .setText(text)
.setAttachments(attachments); .setAttachments(attachments)
.setGroupable(true);
await modify.getCreator().finish(messageBuilder); await modify.getCreator().finish(messageBuilder);
} }
async function urlToBlob(url) { async function urlToBlob(url) {
@ -262,7 +324,7 @@ async function urlToBlob(url) {
} }
async function test() { async function test() {
const blob = await urlToBlob('https://www.z4a.net/images/2024/12/09/69054578_p0.jpg'); 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); console.log(result);
} }
exports.default = PicSearcherApp; exports.default = PicSearcherApp;

View File

@ -1,6 +1,6 @@
import { import {
IAppAccessors, IAppAccessors,
IConfigurationExtend, IConfigurationExtend, IConfigurationModify,
IEnvironmentRead, IEnvironmentRead,
IHttp, IHttp,
ILogger, ILogger,
@ -20,55 +20,181 @@ import {
import type {IMessageAction} from '@rocket.chat/apps-engine/definition/messages/IMessageAction'; import type {IMessageAction} from '@rocket.chat/apps-engine/definition/messages/IMessageAction';
import {IAppInfo} from '@rocket.chat/apps-engine/definition/metadata'; import {IAppInfo} from '@rocket.chat/apps-engine/definition/metadata';
import {IRoom} from '@rocket.chat/apps-engine/definition/rooms'; 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'; 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 { interface ISauceNAOResponse {
header: { header: IHeader;
user_id: string; results: Array<IResult>;
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,
}
}>;
} }
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; title?: string;
thumbnailUrl?: string;
sourceUrl?: string; // Pixiv 特有字段
creator?: string; pixiv_id?: number;
creatorUrl?: string; member_name?: string;
member_id?: number;
// DeviantArt 特有字段
da_id?: string;
author_name?: string;
author_url?: string;
// E-Hentai 特有字段
source?: string; source?: string;
id?: string; creator?: Array<string>;
characters?: 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 { interface IImageBlob {
blob: Blob; blob: Blob;
suffix: string; suffix: string;
@ -83,7 +209,7 @@ const defaultSauceNAOResponse: ISauceNAOResponse = {
long_remaining: 0, long_remaining: 0,
short_remaining: 0, short_remaining: 0,
status: 0, status: 0,
results_requested: 0, results_requested: '',
index: {}, index: {},
search_depth: '', search_depth: '',
minimum_similarity: 0, minimum_similarity: 0,
@ -99,6 +225,7 @@ let SauceNAOErrStr: string;
export class PicSearcherApp extends App implements IPostMessageSent { export class PicSearcherApp extends App implements IPostMessageSent {
private rootUrl: string = ''; private rootUrl: string = '';
private sauceNAOApiKey: string = '';
constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) {
super(info, logger, accessors); super(info, logger, accessors);
@ -107,6 +234,7 @@ export class PicSearcherApp extends App implements IPostMessageSent {
public async initialize(configurationExtend: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise<void> { public async initialize(configurationExtend: IConfigurationExtend, environmentRead: IEnvironmentRead): Promise<void> {
this.rootUrl = await environmentRead.getEnvironmentVariables().getValueByName('ROOT_URL'); this.rootUrl = await environmentRead.getEnvironmentVariables().getValueByName('ROOT_URL');
this.sauceNAOApiKey = await environmentRead.getSettings().getValueById('SauceNAOApiKey');
myLogger.log('rootUrl:', this.rootUrl); myLogger.log('rootUrl:', this.rootUrl);
return super.initialize(configurationExtend, environmentRead); return super.initialize(configurationExtend, environmentRead);
} }
@ -123,10 +251,29 @@ export class PicSearcherApp extends App implements IPostMessageSent {
} }
myLogger.info('message.attachments:', message.attachments); myLogger.info('message.attachments:', message.attachments);
const imageBlobs = await processMessageImages(message, http, read); 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!); 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) { private async sendMessage(textMessage: string, room: IRoom, author: IUser, modify: IModify) {
const messageBuilder = modify.getCreator().startMessage({ const messageBuilder = modify.getCreator().startMessage({
text: textMessage, text: textMessage,
@ -186,10 +333,12 @@ export async function getFileBlob(fileId: string, read: IRead): Promise<Blob> {
return new 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 并附加文件 // 1. 创建 FormData 并附加文件
const formData = new FormData(); const formData = new FormData();
formData.append('file', image, 'image.' + suffix); // 文件名可以随意 formData.append('file', image, 'image.' + suffix); // 文件名可以随意
formData.append('api_key', apiKey);
formData.append('output_type', '2'); // 详细输出
// 2. 发送 POST 请求到 SauceNAO // 2. 发送 POST 请求到 SauceNAO
try { try {
@ -200,78 +349,10 @@ async function searchImageOnSauceNAO(image: Blob, suffix: string): Promise<Array
// 3. 解析返回的 HTML 数据 // 3. 解析返回的 HTML 数据
if (response.ok) { if (response.ok) {
const results: Array<ISearchResult> = []; const json = await response.text();
const html = await response.text(); const result = JSON.parse(json) as ISauceNAOResponse;
// 匹配所有结果块,包括隐藏的
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;
}
SauceNAOErrStr = ''; SauceNAOErrStr = '';
return results; return result;
} else { } else {
console.error('请求失败:', response.status, response.statusText); console.error('请求失败:', response.status, response.statusText);
SauceNAOErrStr = '请求失败: ' + response.status + ' ' + response.statusText; SauceNAOErrStr = '请求失败: ' + response.status + ' ' + response.statusText;
@ -281,7 +362,7 @@ async function searchImageOnSauceNAO(image: Blob, suffix: string): Promise<Array
SauceNAOErrStr = '搜索请求出错: ' + error; SauceNAOErrStr = '搜索请求出错: ' + error;
} }
return []; return defaultSauceNAOResponse;
} }
/** /**
@ -310,79 +391,120 @@ function getInterpolatedColor(similarity: number): string {
async function sendSearchResults( async function sendSearchResults(
modify: IModify, modify: IModify,
room: IRoom, room: IRoom,
results: Array<ISearchResult>, results: ISauceNAOResponse,
appUser: IUser, 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 // From percentage string to numeric value
const similarityValue = parseFloat(result.similarity.replace('%', '')); const similarityValue = header.similarity;
if (similarityValue < 70) {
continue;
}
// Dynamically generate color // Dynamically generate color
const color = getInterpolatedColor(similarityValue); 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 = { const attachmentObject: IMessageAttachment = {
title: { title: {
value: `${result.title} (${result.similarity}%)`, // Title content value: `${data.title} (${header.similarity}%)`, // Title content
} as IMessageAttachmentTitle, } as IMessageAttachmentTitle,
author: { thumbnailUrl: header.thumbnail, // Optional: Image URL
name: result.creator, // Source name collapsed: attachments.length !== 0, // First one expanded, others collapsed
} as IMessageAttachmentAuthor,
text: textStr, // Image ID
thumbnailUrl: result.thumbnailUrl, // Optional: Image URL
collapsed: index !== 0, // First one expanded, others collapsed
color, // Dynamically set color color, // Dynamically set color
}; };
// if (!result.sourceUrl) { if (!header.thumbnail) {
// delete attachmentObject.title?.link;
// }
// if (!result.creatorUrl) {
// delete attachmentObject.author?.link;
// }
if (!result.thumbnailUrl) {
delete attachmentObject.thumbnailUrl; delete attachmentObject.thumbnailUrl;
} }
if (textStr === ': ') { attachmentObject.actions = new Array<IMessageAction>();
delete attachmentObject.text;
delete attachmentObject.collapsed; 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) { if (data.pixiv_id) {
attachmentObject.actions?.push({ attachmentObject.actions.push({
type: MessageActionType.BUTTON, type: MessageActionType.BUTTON,
text: '查看原图', text: '作者Pixiv',
url: result.sourceUrl, url: `https://www.pixiv.net/users/${data.member_id}`,
} as IMessageAction); } as IMessageAction);
} }
if (result.creatorUrl) { if (data.da_id) {
attachmentObject.actions?.push({ attachmentObject.actions.push({
type: MessageActionType.BUTTON, type: MessageActionType.BUTTON,
text: '查看作者', text: '作者DeviantArt',
url: result.creatorUrl, url: data.author_url,
} as IMessageAction); } as IMessageAction);
} }
if (result.characters) { if (data.bcy_id) {
attachmentObject.fields = [{ attachmentObject.actions.push({
short: false, type: MessageActionType.BUTTON,
title: 'Characters', text: '作者BCY',
value: result.characters, 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() const messageBuilder = modify.getCreator().startMessage()
.setRoom(room) .setRoom(room)
.setUsernameAlias(appUser.username) .setUsernameAlias(appUser.username)
.setText(text) .setText(text)
.setAttachments(attachments); .setAttachments(attachments)
.setGroupable(true);
await modify.getCreator().finish(messageBuilder); await modify.getCreator().finish(messageBuilder);
} }
@ -402,7 +524,7 @@ async function urlToBlob(url: string): Promise<Blob> {
// 从 URL 获取图片 Blob // 从 URL 获取图片 Blob
async function test() { async function test() {
const blob = await urlToBlob('https://www.z4a.net/images/2024/12/09/69054578_p0.jpg'); 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); console.log(result);
} }