File size: 4,527 Bytes
1bf9e0c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | import axios from 'axios';
// 根据你的目录结构,向上跳两级到 src,再进入 config
import { getGlobalConfig } from "../../config/config";
const JINA_API_KEY = getGlobalConfig() || process.env.JINA_API_KEY;
const READER_ENDPOINT = "https://r.jina.ai/";
export interface FetchResult {
url: string;
finalUrl: string;
status: number;
extractor: string;
truncated: boolean;
length: number;
untrusted: boolean;
text: string;
}
/**
* 延迟函数
*/
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 带重试的请求函数
*/
async function fetchWithRetry(
requestFn: () => Promise<any>,
maxRetries: number = 3,
retryDelay: number = 1000
): Promise<any> {
let lastError: any;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await requestFn();
} catch (error: any) {
lastError = error;
// 如果是服务器返回的错误(4xx, 5xx),不重试
if (error.response) {
throw error;
}
// 如果是网络错误或超时,进行重试
if (attempt < maxRetries) {
console.warn(`Jina Fetch: 网络错误,第 ${attempt} 次尝试失败,${retryDelay}ms 后重试...`);
await delay(retryDelay);
}
}
}
throw lastError;
}
/**
* 使用 Jina Reader API 抓取网页内容
* @param url 目标网页 URL
* @param maxChars 最大字符截断限制
*/
export async function jinaFetch(url: string, maxChars: number = 50000): Promise<string | null> {
// 1. 基础校验:如果 URL 为空或明显非法,直接返回 null
if (!url || typeof url !== 'string' || url.trim().length === 0) {
console.warn("Jina Fetch: Invalid or empty URL provided.");
return null;
}
try {
// 2. 安全拼接 URL
// 使用 encodeURIComponent 确保目标 URL 中的特殊字符不会破坏请求格式
const requestUrl = `${READER_ENDPOINT}${encodeURIComponent(url)}`;
// 3. 使用重试机制发送请求
const response = await fetchWithRetry(async () => {
return await axios.get(requestUrl, {
headers: {
// 只有当 API_KEY 存在时才添加 Authorization
...(JINA_API_KEY ? { 'Authorization': `Bearer ${JINA_API_KEY}` } : {}),
'Accept': 'application/json',
'X-Return-Format': 'markdown'
},
timeout: 30000 // 增加超时时间到 30 秒
});
}, 3, 1000); // 最多重试 3 次,每次间隔 1 秒
// 4. 处理频率限制
if (response.status === 429) {
console.debug("Jina Reader rate limited");
return null;
}
// 5. 解析数据结构
// Jina API 返回结构: { code, status, data: { title, content, url, ... } }
const apiResponse = response.data || {};
const data = apiResponse.data || {};
const title = data.title || "";
let content = data.content || "";
// 调试日志:记录响应结构
console.debug(`Jina API Response Status: ${response.status}`);
console.debug(`Response data keys: ${Object.keys(data).join(', ')}`);
console.debug(`Title length: ${title.length}, Content length: ${content.length}`);
if (!content) {
console.warn(`Jina Fetch: No content found in response for URL: ${url}`);
console.warn(`Response data:`, JSON.stringify(data, null, 2));
return null;
}
// 6. 格式化内容
let fullText = title ? `# ${title}\n\n${content}` : content;
const isTruncated = fullText.length > maxChars;
if (isTruncated) {
fullText = fullText.slice(0, maxChars);
}
const result: FetchResult = {
url: url,
finalUrl: data.url || url,
status: response.status,
extractor: "jina",
truncated: isTruncated,
length: fullText.length,
untrusted: true,
text: fullText
};
return JSON.stringify(result, null, 2);
} catch (error: any) {
// 7. 增强错误日志输出
if (error.response) {
// 服务器响应了错误(如 400, 403, 404)
const status = error.response.status;
const errorDetail = JSON.stringify(error.response.data);
console.error(`Jina API Error (Status ${status}): ${errorDetail}`);
} else if (error.request) {
// 请求已发出但未收到响应
console.error(`Jina Fetch No Response: ${error.message}`);
} else {
// 设置请求时发生错误
console.error(`Jina Fetch Configuration Error: ${error.message}`);
}
return null;
}
}
|