feat(工具): 实现秘塔AI搜索和网页读取工具
秘塔搜索 (mita_search): - 支持网页搜索和图片搜索两种模式 - 图片搜索包含智能验证和动态回填机制 - 自动过滤无效图片URL,确保返回有效结果 秘塔阅读 (mita_reader): - 将网页内容转换为结构化Markdown格式 - 支持URL格式验证和错误处理 工具执行器更新: - 添加 metasoApiKey 选项支持 - 集成秘塔搜索和阅读工具到执行流程 - 返回搜索图片数据供前端展示
This commit is contained in:
parent
baf27ceca6
commit
97d89f44ac
@ -24,6 +24,23 @@ import {
|
|||||||
type WebFetchInput,
|
type WebFetchInput,
|
||||||
type WebFetchResponse,
|
type WebFetchResponse,
|
||||||
} from './webFetch';
|
} from './webFetch';
|
||||||
|
import {
|
||||||
|
metasoSearch,
|
||||||
|
formatMetasoSearchResults,
|
||||||
|
formatMetasoSearchResultsShort,
|
||||||
|
getValidImages,
|
||||||
|
ImageValidationConfig,
|
||||||
|
type MetasoSearchInput,
|
||||||
|
type MetasoSearchResponse,
|
||||||
|
type MetasoImageResult,
|
||||||
|
} from './metasoSearch';
|
||||||
|
import {
|
||||||
|
metasoReader,
|
||||||
|
formatMetasoReaderResult,
|
||||||
|
formatMetasoReaderResultShort,
|
||||||
|
type MetasoReaderInput,
|
||||||
|
type MetasoReaderResponse,
|
||||||
|
} from './metasoReader';
|
||||||
import { shouldUsePyodide, analyzeCode, type LoadingCallback } from './codeAnalyzer';
|
import { shouldUsePyodide, analyzeCode, type LoadingCallback } from './codeAnalyzer';
|
||||||
|
|
||||||
// 导出代码分析函数供外部使用
|
// 导出代码分析函数供外部使用
|
||||||
@ -39,6 +56,8 @@ export interface ToolExecutionResult {
|
|||||||
rawData?: unknown;
|
rawData?: unknown;
|
||||||
/** Base64 编码的图片数组(代码执行时可能产生) */
|
/** Base64 编码的图片数组(代码执行时可能产生) */
|
||||||
images?: string[];
|
images?: string[];
|
||||||
|
/** 搜索到的图片数组(图片搜索时产生) */
|
||||||
|
searchImages?: MetasoImageResult[];
|
||||||
/** 是否需要浏览器端 Pyodide 执行 */
|
/** 是否需要浏览器端 Pyodide 执行 */
|
||||||
requiresPyodide?: boolean;
|
requiresPyodide?: boolean;
|
||||||
/** 代码内容(当 requiresPyodide 为 true 时) */
|
/** 代码内容(当 requiresPyodide 为 true 时) */
|
||||||
@ -47,18 +66,26 @@ export interface ToolExecutionResult {
|
|||||||
language?: string;
|
language?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolExecutionOptions {
|
||||||
|
/** Pyodide 加载进度回调 */
|
||||||
|
onProgress?: LoadingCallback;
|
||||||
|
/** 秘塔AI API Key */
|
||||||
|
metasoApiKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行工具
|
* 执行工具
|
||||||
* @param toolName 工具名称
|
* @param toolName 工具名称
|
||||||
* @param input 工具输入参数
|
* @param input 工具输入参数
|
||||||
* @param onProgress Pyodide 加载进度回调(可选)
|
* @param options 执行选项
|
||||||
* @returns 执行结果(包含完整版和简短版)
|
* @returns 执行结果(包含完整版和简短版)
|
||||||
*/
|
*/
|
||||||
export async function executeTool(
|
export async function executeTool(
|
||||||
toolName: string,
|
toolName: string,
|
||||||
input: Record<string, unknown>,
|
input: Record<string, unknown>,
|
||||||
onProgress?: LoadingCallback
|
options?: ToolExecutionOptions
|
||||||
): Promise<ToolExecutionResult> {
|
): Promise<ToolExecutionResult> {
|
||||||
|
const { onProgress, metasoApiKey } = options || {};
|
||||||
try {
|
try {
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case 'web_search': {
|
case 'web_search': {
|
||||||
@ -119,6 +146,63 @@ export async function executeTool(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'mita_search': {
|
||||||
|
const query = String(input.query || '');
|
||||||
|
const scope = (input.scope as 'webpage' | 'image') || 'webpage';
|
||||||
|
|
||||||
|
// 图片搜索时请求更多图片用于验证筛选,网页搜索保持原有逻辑
|
||||||
|
const requestSize = scope === 'image'
|
||||||
|
? ImageValidationConfig.REQUEST_SIZE
|
||||||
|
: (input.size ? Number(input.size) : 10);
|
||||||
|
|
||||||
|
const searchInput: MetasoSearchInput = {
|
||||||
|
query,
|
||||||
|
scope,
|
||||||
|
size: requestSize,
|
||||||
|
includeSummary: Boolean(input.includeSummary),
|
||||||
|
};
|
||||||
|
|
||||||
|
const response: MetasoSearchResponse = await metasoSearch(searchInput, metasoApiKey || '');
|
||||||
|
|
||||||
|
// 如果是图片搜索且成功,验证图片有效性
|
||||||
|
let validatedImages: MetasoImageResult[] | undefined;
|
||||||
|
if (scope === 'image' && response.success && response.images && response.images.length > 0) {
|
||||||
|
// 验证图片,返回指定数量的有效图片
|
||||||
|
validatedImages = await getValidImages(
|
||||||
|
response.images,
|
||||||
|
ImageValidationConfig.TARGET_COUNT,
|
||||||
|
ImageValidationConfig.CONCURRENCY
|
||||||
|
);
|
||||||
|
|
||||||
|
// 更新 response 中的图片为验证后的有效图片
|
||||||
|
response.images = validatedImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: response.success,
|
||||||
|
fullResult: formatMetasoSearchResults(response),
|
||||||
|
displayResult: formatMetasoSearchResultsShort(response, query),
|
||||||
|
rawData: response,
|
||||||
|
// 如果是图片搜索,返回验证后的图片数据
|
||||||
|
searchImages: scope === 'image' ? validatedImages : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'mita_reader': {
|
||||||
|
const url = String(input.url || '');
|
||||||
|
const readerInput: MetasoReaderInput = {
|
||||||
|
url,
|
||||||
|
format: 'Markdown',
|
||||||
|
};
|
||||||
|
const response: MetasoReaderResponse = await metasoReader(readerInput, metasoApiKey || '');
|
||||||
|
return {
|
||||||
|
success: response.success,
|
||||||
|
fullResult: formatMetasoReaderResult(response, url),
|
||||||
|
displayResult: formatMetasoReaderResultShort(response, url),
|
||||||
|
rawData: response,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@ -148,17 +232,23 @@ export function getAvailableTools() {
|
|||||||
description: '搜索互联网获取最新信息',
|
description: '搜索互联网获取最新信息',
|
||||||
icon: '🔍',
|
icon: '🔍',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'code_execution',
|
|
||||||
name: '代码执行',
|
|
||||||
description: '执行代码并返回结果',
|
|
||||||
icon: '💻',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'web_fetch',
|
id: 'web_fetch',
|
||||||
name: '网页获取',
|
name: '网页获取',
|
||||||
description: '获取指定 URL 的网页内容',
|
description: '获取指定 URL 的网页内容',
|
||||||
icon: '🌐',
|
icon: '🌐',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'mita_search',
|
||||||
|
name: 'Metaso Search',
|
||||||
|
description: '秘塔AI智能搜索,需要配置API Key',
|
||||||
|
icon: '🔎',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mita_reader',
|
||||||
|
name: 'Metaso Reader',
|
||||||
|
description: '秘塔AI网页读取,返回Markdown格式',
|
||||||
|
icon: '📄',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
139
src/services/tools/metasoReader.ts
Normal file
139
src/services/tools/metasoReader.ts
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* 秘塔AI网页读取工具服务
|
||||||
|
* 使用秘塔AI API 读取网页内容并返回Markdown格式
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface MetasoReaderResponse {
|
||||||
|
success: boolean;
|
||||||
|
content?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetasoReaderInput {
|
||||||
|
url: string;
|
||||||
|
format?: 'Markdown' | 'Text';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取网页内容
|
||||||
|
*/
|
||||||
|
export async function metasoReader(
|
||||||
|
input: MetasoReaderInput,
|
||||||
|
apiKey: string
|
||||||
|
): Promise<MetasoReaderResponse> {
|
||||||
|
if (!apiKey) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '请先在设置中配置秘塔AI API Key',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证URL格式
|
||||||
|
try {
|
||||||
|
new URL(input.url);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '无效的URL格式',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://metaso.cn/api/v1/reader', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Accept': 'text/plain',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
url: input.url,
|
||||||
|
format: input.format || 'Markdown',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Metaso Reader API error:', errorText);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '秘塔AI API Key 无效,请检查配置',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '无法访问该网页,请检查URL是否正确',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `秘塔读取API错误: ${response.status}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 秘塔Reader返回的是文本内容
|
||||||
|
const content = await response.text();
|
||||||
|
|
||||||
|
if (!content || content.trim().length === 0) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '网页内容为空或无法解析',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
content: content,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Metaso reader error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '秘塔读取服务异常',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化读取结果为文本(完整版 - 发送给 AI)
|
||||||
|
*/
|
||||||
|
export function formatMetasoReaderResult(response: MetasoReaderResponse, url: string): string {
|
||||||
|
if (!response.success) {
|
||||||
|
return `读取网页失败: ${response.error}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = `## 网页内容\n`;
|
||||||
|
result += `**来源**: ${url}\n\n`;
|
||||||
|
result += `---\n\n`;
|
||||||
|
result += response.content || '无内容';
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化读取结果为简短文本(显示给用户)
|
||||||
|
*/
|
||||||
|
export function formatMetasoReaderResultShort(response: MetasoReaderResponse, url: string): string {
|
||||||
|
if (!response.success) {
|
||||||
|
return `读取失败: ${response.error}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取网站域名
|
||||||
|
let hostname = url;
|
||||||
|
try {
|
||||||
|
hostname = new URL(url).hostname.replace('www.', '');
|
||||||
|
} catch {
|
||||||
|
// 保持原始URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算内容长度
|
||||||
|
const contentLength = response.content?.length || 0;
|
||||||
|
const wordCount = response.content?.split(/\s+/).length || 0;
|
||||||
|
|
||||||
|
return `> 📄 已读取 [${hostname}](${url}) 的内容\n> 📊 共 ${contentLength} 字符,约 ${wordCount} 词`;
|
||||||
|
}
|
||||||
369
src/services/tools/metasoSearch.ts
Normal file
369
src/services/tools/metasoSearch.ts
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
/**
|
||||||
|
* 秘塔AI搜索工具服务
|
||||||
|
* 使用秘塔AI API 实现智能搜索(支持网页搜索和图片搜索)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============ 图片验证相关常量 ============
|
||||||
|
const IMAGE_VALIDATION_TIMEOUT = 3000; // 单张图片验证超时时间(毫秒)
|
||||||
|
const IMAGE_VALIDATION_CONCURRENCY = 5; // 并发验证数量
|
||||||
|
const IMAGE_REQUEST_SIZE = 15; // 图片搜索时请求的数量
|
||||||
|
const IMAGE_TARGET_COUNT = 10; // 返回给前端的图片数量(前端会显示5张+5张备用)
|
||||||
|
|
||||||
|
// 网页搜索结果
|
||||||
|
export interface MetasoSearchResult {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
snippet: string;
|
||||||
|
score: string;
|
||||||
|
position: number;
|
||||||
|
date?: string;
|
||||||
|
authors?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片搜索结果
|
||||||
|
export interface MetasoImageResult {
|
||||||
|
title: string;
|
||||||
|
imageUrl: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
score: string;
|
||||||
|
position: number;
|
||||||
|
sourceUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索响应
|
||||||
|
export interface MetasoSearchResponse {
|
||||||
|
success: boolean;
|
||||||
|
credits?: number;
|
||||||
|
total?: number;
|
||||||
|
scope?: 'webpage' | 'image';
|
||||||
|
// 网页搜索结果
|
||||||
|
results?: MetasoSearchResult[];
|
||||||
|
// 图片搜索结果
|
||||||
|
images?: MetasoImageResult[];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索输入参数
|
||||||
|
export interface MetasoSearchInput {
|
||||||
|
query: string;
|
||||||
|
scope?: 'webpage' | 'image';
|
||||||
|
size?: number;
|
||||||
|
/** 页码,用于获取不同页的结果(从1开始) */
|
||||||
|
page?: number;
|
||||||
|
includeSummary?: boolean;
|
||||||
|
includeRawContent?: boolean;
|
||||||
|
conciseSnippet?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行秘塔AI搜索
|
||||||
|
*/
|
||||||
|
export async function metasoSearch(
|
||||||
|
input: MetasoSearchInput,
|
||||||
|
apiKey: string
|
||||||
|
): Promise<MetasoSearchResponse> {
|
||||||
|
if (!apiKey) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '请先在设置中配置秘塔AI API Key',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = input.scope || 'webpage';
|
||||||
|
const size = input.size || (scope === 'image' ? 5 : 10); // 图片默认5张
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://metaso.cn/api/v1/search', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
q: input.query,
|
||||||
|
scope: scope,
|
||||||
|
size: size,
|
||||||
|
includeSummary: input.includeSummary || false,
|
||||||
|
includeRawContent: input.includeRawContent || false,
|
||||||
|
conciseSnippet: input.conciseSnippet || false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Metaso API error:', errorText);
|
||||||
|
|
||||||
|
if (response.status === 401) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: '秘塔AI API Key 无效,请检查配置',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `秘塔搜索API错误: ${response.status}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 根据 scope 解析不同的结果
|
||||||
|
if (scope === 'image') {
|
||||||
|
// 图片搜索结果
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
credits: data.credits,
|
||||||
|
total: data.total,
|
||||||
|
scope: 'image',
|
||||||
|
images: data.images?.map((item: {
|
||||||
|
title: string;
|
||||||
|
imageUrl: string;
|
||||||
|
imageWidth: number;
|
||||||
|
imageHeight: number;
|
||||||
|
score: string;
|
||||||
|
position: number;
|
||||||
|
sourceUrl?: string;
|
||||||
|
}) => ({
|
||||||
|
title: item.title,
|
||||||
|
imageUrl: item.imageUrl,
|
||||||
|
width: item.imageWidth,
|
||||||
|
height: item.imageHeight,
|
||||||
|
score: item.score,
|
||||||
|
position: item.position,
|
||||||
|
sourceUrl: item.sourceUrl,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 网页搜索结果
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
credits: data.credits,
|
||||||
|
total: data.total,
|
||||||
|
scope: 'webpage',
|
||||||
|
results: data.webpages?.map((item: {
|
||||||
|
title: string;
|
||||||
|
link: string;
|
||||||
|
snippet: string;
|
||||||
|
score: string;
|
||||||
|
position: number;
|
||||||
|
date?: string;
|
||||||
|
authors?: string[];
|
||||||
|
}) => ({
|
||||||
|
title: item.title,
|
||||||
|
link: item.link,
|
||||||
|
snippet: item.snippet,
|
||||||
|
score: item.score,
|
||||||
|
position: item.position,
|
||||||
|
date: item.date,
|
||||||
|
authors: item.authors,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Metaso search error:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : '秘塔搜索服务异常',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化网页搜索结果为文本(完整版 - 发送给 AI)
|
||||||
|
*/
|
||||||
|
export function formatMetasoSearchResults(response: MetasoSearchResponse): string {
|
||||||
|
if (!response.success) {
|
||||||
|
return `搜索失败: ${response.error}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片搜索结果
|
||||||
|
if (response.scope === 'image' && response.images) {
|
||||||
|
let result = `## 秘塔图片搜索结果 (共${response.total}张)\n\n`;
|
||||||
|
response.images.forEach((item, index) => {
|
||||||
|
result += `${index + 1}. **${item.title}**\n`;
|
||||||
|
result += ` - 图片URL: ${item.imageUrl}\n`;
|
||||||
|
result += ` - 尺寸: ${item.width}x${item.height}\n`;
|
||||||
|
if (item.sourceUrl) {
|
||||||
|
result += ` - 来源: ${item.sourceUrl}\n`;
|
||||||
|
}
|
||||||
|
result += '\n';
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网页搜索结果
|
||||||
|
let result = '';
|
||||||
|
if (response.results && response.results.length > 0) {
|
||||||
|
result += `## 秘塔搜索结果 (共${response.total}条)\n\n`;
|
||||||
|
response.results.forEach((item, index) => {
|
||||||
|
result += `### ${index + 1}. ${item.title}\n`;
|
||||||
|
result += `**链接**: ${item.link}\n`;
|
||||||
|
if (item.date) {
|
||||||
|
result += `**日期**: ${item.date}\n`;
|
||||||
|
}
|
||||||
|
if (item.authors && item.authors.length > 0) {
|
||||||
|
result += `**作者**: ${item.authors.join(', ')}\n`;
|
||||||
|
}
|
||||||
|
result += `${item.snippet}\n\n`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result || '未找到相关结果';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 图片验证相关函数 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证单张图片 URL 是否可访问
|
||||||
|
* 使用 GET + Range 请求模拟真实浏览器行为,比 HEAD 更可靠
|
||||||
|
* @param url 图片 URL
|
||||||
|
* @param timeout 超时时间(毫秒)
|
||||||
|
* @returns 是否可访问
|
||||||
|
*/
|
||||||
|
export async function validateImageUrl(
|
||||||
|
url: string,
|
||||||
|
timeout: number = IMAGE_VALIDATION_TIMEOUT
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
|
// 使用 GET 请求 + Range 头部,只请求前 1KB 数据
|
||||||
|
// 这比 HEAD 更可靠,因为某些服务器对 HEAD 和 GET 的处理不一致
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
// 只请求前 1024 字节,节省带宽
|
||||||
|
'Range': 'bytes=0-1023',
|
||||||
|
// 模拟浏览器请求,避免被某些服务器拒绝
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||||
|
'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// 检查响应状态:200 OK 或 206 Partial Content 都是有效的
|
||||||
|
if (!response.ok && response.status !== 206) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
// 必须是图片类型
|
||||||
|
if (contentType && !contentType.startsWith('image/')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 额外检查:尝试读取一些数据确保内容存在
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
if (buffer.byteLength === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// 网络错误、超时或其他异常
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量验证图片,返回指定数量的有效图片
|
||||||
|
* @param images 原始图片列表
|
||||||
|
* @param targetCount 目标有效图片数量
|
||||||
|
* @param concurrency 并发验证数量
|
||||||
|
* @returns 有效图片列表
|
||||||
|
*/
|
||||||
|
export async function getValidImages(
|
||||||
|
images: MetasoImageResult[],
|
||||||
|
targetCount: number = IMAGE_TARGET_COUNT,
|
||||||
|
concurrency: number = IMAGE_VALIDATION_CONCURRENCY
|
||||||
|
): Promise<MetasoImageResult[]> {
|
||||||
|
const validImages: MetasoImageResult[] = [];
|
||||||
|
|
||||||
|
// 如果图片数量不足,直接返回所有图片(不验证)
|
||||||
|
if (images.length <= targetCount) {
|
||||||
|
return images;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分批并行验证
|
||||||
|
for (let i = 0; i < images.length && validImages.length < targetCount; i += concurrency) {
|
||||||
|
const batch = images.slice(i, i + concurrency);
|
||||||
|
|
||||||
|
// 并行验证当前批次的图片
|
||||||
|
const validationResults = await Promise.all(
|
||||||
|
batch.map(async (img) => ({
|
||||||
|
img,
|
||||||
|
valid: await validateImageUrl(img.imageUrl),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// 收集有效图片
|
||||||
|
for (const { img, valid } of validationResults) {
|
||||||
|
if (valid && validImages.length < targetCount) {
|
||||||
|
validImages.push(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经收集到足够的有效图片,提前结束
|
||||||
|
if (validImages.length >= targetCount) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return validImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出图片验证相关常量供外部使用
|
||||||
|
*/
|
||||||
|
export const ImageValidationConfig = {
|
||||||
|
TIMEOUT: IMAGE_VALIDATION_TIMEOUT,
|
||||||
|
CONCURRENCY: IMAGE_VALIDATION_CONCURRENCY,
|
||||||
|
REQUEST_SIZE: IMAGE_REQUEST_SIZE,
|
||||||
|
TARGET_COUNT: IMAGE_TARGET_COUNT,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化搜索结果为简短文本(显示给用户)
|
||||||
|
*/
|
||||||
|
export function formatMetasoSearchResultsShort(
|
||||||
|
response: MetasoSearchResponse,
|
||||||
|
query: string
|
||||||
|
): string {
|
||||||
|
if (!response.success) {
|
||||||
|
return `搜索失败: ${response.error}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片搜索结果
|
||||||
|
if (response.scope === 'image' && response.images) {
|
||||||
|
const imageCount = response.images.length;
|
||||||
|
return `> 🖼️ 秘塔搜索「${query}」图片,找到 ${response.total || imageCount} 张相关图片`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网页搜索结果
|
||||||
|
const resultCount = response.results?.length || 0;
|
||||||
|
|
||||||
|
// 提取来源网站并生成 Markdown 链接
|
||||||
|
const sourceLinks = response.results?.slice(0, 3).map((r, index) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(r.link);
|
||||||
|
const hostname = url.hostname.replace('www.', '');
|
||||||
|
return `${index + 1}. [${hostname}](${r.link})`;
|
||||||
|
} catch {
|
||||||
|
return `${index + 1}. ${r.title.slice(0, 20)}`;
|
||||||
|
}
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
let result = `> 🔍 秘塔搜索「${query}」,找到 ${response.total || resultCount} 个相关结果`;
|
||||||
|
|
||||||
|
if (sourceLinks.length > 0) {
|
||||||
|
result += `\n\n**🔗 来源:**\n${sourceLinks.join('\n')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user