| import axios from 'axios'; |
| |
| 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; |
|
|
| |
| if (error.response) { |
| throw error; |
| } |
|
|
| |
| if (attempt < maxRetries) { |
| console.warn(`Jina Fetch: 网络错误,第 ${attempt} 次尝试失败,${retryDelay}ms 后重试...`); |
| await delay(retryDelay); |
| } |
| } |
| } |
|
|
| throw lastError; |
| } |
|
|
| |
| |
| |
| |
| |
| export async function jinaFetch(url: string, maxChars: number = 50000): Promise<string | null> { |
| |
| if (!url || typeof url !== 'string' || url.trim().length === 0) { |
| console.warn("Jina Fetch: Invalid or empty URL provided."); |
| return null; |
| } |
|
|
| try { |
| |
| |
| const requestUrl = `${READER_ENDPOINT}${encodeURIComponent(url)}`; |
|
|
| |
| const response = await fetchWithRetry(async () => { |
| return await axios.get(requestUrl, { |
| headers: { |
| |
| ...(JINA_API_KEY ? { 'Authorization': `Bearer ${JINA_API_KEY}` } : {}), |
| 'Accept': 'application/json', |
| 'X-Return-Format': 'markdown' |
| }, |
| timeout: 30000 |
| }); |
| }, 3, 1000); |
|
|
| |
| if (response.status === 429) { |
| console.debug("Jina Reader rate limited"); |
| return null; |
| } |
|
|
| |
| |
| 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; |
| } |
|
|
| |
| 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) { |
| |
| if (error.response) { |
| |
| 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; |
| } |
| } |
|
|