改为使用API访问查询
This commit is contained in:
parent
7371f2acdb
commit
44bbf7582e
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user