@ever-works/jina-plugin 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +358 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +353 -0
- package/package.json +87 -74
- package/LICENSE +0 -661
- package/src/__tests__/jina.plugin.spec.ts +0 -318
- package/src/index.ts +0 -2
- package/src/jina.plugin.ts +0 -398
- package/tsconfig.json +0 -22
- package/tsup.config.ts +0 -12
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var net = require('net');
|
|
6
|
+
|
|
7
|
+
// ../../plugin/dist/helpers/ssrf-guard.js
|
|
8
|
+
function isSafeWebhookUrl(rawUrl) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(rawUrl);
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
let host = url.hostname.toLowerCase();
|
|
19
|
+
if (host.startsWith("[") && host.endsWith("]")) {
|
|
20
|
+
host = host.slice(1, -1);
|
|
21
|
+
}
|
|
22
|
+
if (CLOUD_METADATA_HOSTNAMES.has(host)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const ipKind = net.isIP(host);
|
|
26
|
+
if (ipKind === 4 && isPrivateIPv4(host)) return false;
|
|
27
|
+
if (ipKind === 6 && isPrivateIPv6(host)) return false;
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
var CLOUD_METADATA_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
31
|
+
// GCP
|
|
32
|
+
"metadata.google.internal",
|
|
33
|
+
"metadata.goog",
|
|
34
|
+
// AWS
|
|
35
|
+
"instance-data",
|
|
36
|
+
"instance-data.ec2.internal",
|
|
37
|
+
// Azure
|
|
38
|
+
"metadata.azure.internal"
|
|
39
|
+
]);
|
|
40
|
+
function isPrivateIPv4(ip) {
|
|
41
|
+
const parts = ip.split(".").map((p) => Number(p));
|
|
42
|
+
if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return false;
|
|
43
|
+
const [a, b] = parts;
|
|
44
|
+
if (a === 10) return true;
|
|
45
|
+
if (a === 127) return true;
|
|
46
|
+
if (a === 0) return true;
|
|
47
|
+
if (a === 169 && b === 254) return true;
|
|
48
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
49
|
+
if (a === 192 && b === 168) return true;
|
|
50
|
+
if (a === 192 && b === 0 && parts[2] === 0) return true;
|
|
51
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
52
|
+
if (a >= 224) return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function isPrivateIPv6(ip) {
|
|
56
|
+
const lower = ip.toLowerCase();
|
|
57
|
+
if (lower === "::1" || lower === "::") return true;
|
|
58
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
59
|
+
const firstHextet = lower.split(":", 1)[0];
|
|
60
|
+
if (/^[0-9a-f]{1,4}$/.test(firstHextet)) {
|
|
61
|
+
const group0 = parseInt(firstHextet, 16);
|
|
62
|
+
if ((group0 & 65472) === 65152) return true;
|
|
63
|
+
}
|
|
64
|
+
if (lower.startsWith("::ffff:")) {
|
|
65
|
+
const v4 = lower.slice("::ffff:".length);
|
|
66
|
+
if (net.isIP(v4) === 4) return isPrivateIPv4(v4);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/jina.plugin.ts
|
|
72
|
+
var JINA_READER_URL = "https://r.jina.ai/";
|
|
73
|
+
var JINA_SEARCH_URL = "https://s.jina.ai/";
|
|
74
|
+
var JinaReaderPlugin = class {
|
|
75
|
+
id = "jina";
|
|
76
|
+
name = "Jina AI";
|
|
77
|
+
version = "1.0.0";
|
|
78
|
+
category = "content-extractor";
|
|
79
|
+
capabilities = ["search", "content-extractor"];
|
|
80
|
+
providerName = "Jina";
|
|
81
|
+
settingsSchema = {
|
|
82
|
+
type: "object",
|
|
83
|
+
required: ["apiKey"],
|
|
84
|
+
properties: {
|
|
85
|
+
apiKey: {
|
|
86
|
+
type: "string",
|
|
87
|
+
title: "API Key",
|
|
88
|
+
description: "Your Jina API key. Get one at https://jina.ai",
|
|
89
|
+
"x-secret": true,
|
|
90
|
+
"x-envVar": "PLUGIN_JINA_API_KEY",
|
|
91
|
+
"x-scope": "user"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
configurationMode = "hybrid";
|
|
96
|
+
context;
|
|
97
|
+
async search(options) {
|
|
98
|
+
const startTime = Date.now();
|
|
99
|
+
const apiKey = options.settings?.apiKey;
|
|
100
|
+
try {
|
|
101
|
+
const headers = {
|
|
102
|
+
Accept: "application/json",
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
"X-Respond-With": "no-content",
|
|
105
|
+
Authorization: `Bearer ${apiKey}`
|
|
106
|
+
};
|
|
107
|
+
if (options.includeDomains && options.includeDomains.length > 0) {
|
|
108
|
+
headers["X-Site"] = options.includeDomains[0];
|
|
109
|
+
}
|
|
110
|
+
const body = {
|
|
111
|
+
q: options.query
|
|
112
|
+
};
|
|
113
|
+
if (options.limit) {
|
|
114
|
+
body.num = options.limit;
|
|
115
|
+
}
|
|
116
|
+
if (options.region) {
|
|
117
|
+
body.gl = options.region;
|
|
118
|
+
}
|
|
119
|
+
if (options.language) {
|
|
120
|
+
body.hl = options.language;
|
|
121
|
+
}
|
|
122
|
+
const response = await fetch(JINA_SEARCH_URL, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers,
|
|
125
|
+
body: JSON.stringify(body),
|
|
126
|
+
signal: AbortSignal.timeout(3e4)
|
|
127
|
+
});
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`Jina Search API returned ${response.status}: ${response.statusText}`);
|
|
130
|
+
}
|
|
131
|
+
const json = await response.json();
|
|
132
|
+
const results = (json.data || []).map((r, index) => {
|
|
133
|
+
let source;
|
|
134
|
+
try {
|
|
135
|
+
if (r.url) source = new URL(r.url).hostname;
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
title: r.title || "",
|
|
140
|
+
url: r.url || "",
|
|
141
|
+
snippet: r.description || "",
|
|
142
|
+
position: index + 1,
|
|
143
|
+
publishedDate: r.date || void 0,
|
|
144
|
+
source
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
results,
|
|
149
|
+
query: options.query,
|
|
150
|
+
totalResults: results.length,
|
|
151
|
+
hasMore: false,
|
|
152
|
+
duration: Date.now() - startTime
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.context?.logger.error(`Jina search failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async isAvailable() {
|
|
160
|
+
if (!this.context) return false;
|
|
161
|
+
const settings = await this.context.getSettings();
|
|
162
|
+
return Boolean(settings?.apiKey);
|
|
163
|
+
}
|
|
164
|
+
async validateConnection(settings) {
|
|
165
|
+
const apiKey = settings.apiKey;
|
|
166
|
+
if (!apiKey) {
|
|
167
|
+
return { success: false, message: "Jina AI API key is not configured." };
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const response = await fetch(JINA_SEARCH_URL, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: {
|
|
173
|
+
Accept: "application/json",
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
"X-Respond-With": "no-content",
|
|
176
|
+
Authorization: `Bearer ${apiKey}`
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify({ q: "test", num: 1 }),
|
|
179
|
+
signal: AbortSignal.timeout(15e3)
|
|
180
|
+
});
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
message: `Jina AI connection failed (${response.status}): ${response.statusText}`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { success: true, message: "Jina AI connection verified." };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return {
|
|
190
|
+
success: false,
|
|
191
|
+
message: `Jina AI connection failed: ${error instanceof Error ? error.message : String(error)}`
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async getRateLimitInfo() {
|
|
196
|
+
return {
|
|
197
|
+
remaining: -1,
|
|
198
|
+
limit: -1,
|
|
199
|
+
period: "minute"
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async extract(options) {
|
|
203
|
+
const startTime = Date.now();
|
|
204
|
+
const { url, settings } = options;
|
|
205
|
+
const apiKey = settings?.apiKey;
|
|
206
|
+
if (!isSafeWebhookUrl(url)) {
|
|
207
|
+
return {
|
|
208
|
+
success: false,
|
|
209
|
+
url,
|
|
210
|
+
error: `URL host is not safe to fetch (SSRF guard blocked: ${url})`,
|
|
211
|
+
duration: Date.now() - startTime
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const headers = {
|
|
216
|
+
Accept: "application/json",
|
|
217
|
+
"X-Respond-With": "markdown",
|
|
218
|
+
Authorization: `Bearer ${apiKey}`
|
|
219
|
+
};
|
|
220
|
+
const response = await fetch(JINA_READER_URL, {
|
|
221
|
+
method: "POST",
|
|
222
|
+
headers: {
|
|
223
|
+
...headers,
|
|
224
|
+
"Content-Type": "application/json"
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify({ url }),
|
|
227
|
+
signal: AbortSignal.timeout(options.timeout || 3e4)
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
url,
|
|
233
|
+
error: `Jina API returned ${response.status}: ${response.statusText}`,
|
|
234
|
+
duration: Date.now() - startTime
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const json = await response.json();
|
|
238
|
+
const data = json.data;
|
|
239
|
+
if (!data || !data.content) {
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
url,
|
|
243
|
+
error: "No content returned from Jina API",
|
|
244
|
+
duration: Date.now() - startTime
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const wordCount = data.content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
248
|
+
const images = data.images ? Object.entries(data.images).map(([alt, src]) => ({ src, alt })) : void 0;
|
|
249
|
+
const links = data.links ? Object.entries(data.links).map(([text, href]) => ({
|
|
250
|
+
href,
|
|
251
|
+
text,
|
|
252
|
+
isExternal: true
|
|
253
|
+
})) : void 0;
|
|
254
|
+
const metadata = {};
|
|
255
|
+
if (data.description) metadata.description = data.description;
|
|
256
|
+
if (data.publishedTime) metadata.publishedDate = data.publishedTime;
|
|
257
|
+
if (data.metadata?.lang) metadata.language = data.metadata.lang;
|
|
258
|
+
return {
|
|
259
|
+
success: true,
|
|
260
|
+
url,
|
|
261
|
+
finalUrl: data.url !== url ? data.url : void 0,
|
|
262
|
+
title: data.title,
|
|
263
|
+
content: data.content,
|
|
264
|
+
markdown: data.content,
|
|
265
|
+
images: options.includeImages !== false ? images : void 0,
|
|
266
|
+
links: options.includeLinks !== false ? links : void 0,
|
|
267
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : void 0,
|
|
268
|
+
duration: Date.now() - startTime,
|
|
269
|
+
wordCount,
|
|
270
|
+
readingTime: Math.ceil(wordCount / 200)
|
|
271
|
+
};
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
url,
|
|
276
|
+
error: error instanceof Error ? error.message : String(error),
|
|
277
|
+
duration: Date.now() - startTime
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async extractBatch(urls, options) {
|
|
282
|
+
const batchSize = 5;
|
|
283
|
+
const results = [];
|
|
284
|
+
for (let i = 0; i < urls.length; i += batchSize) {
|
|
285
|
+
const batch = urls.slice(i, i + batchSize);
|
|
286
|
+
const batchResults = await Promise.all(batch.map((url) => this.extract({ url, ...options })));
|
|
287
|
+
results.push(...batchResults);
|
|
288
|
+
if (i + batchSize < urls.length) {
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return results;
|
|
293
|
+
}
|
|
294
|
+
async canExtract(url) {
|
|
295
|
+
return isSafeWebhookUrl(url);
|
|
296
|
+
}
|
|
297
|
+
getSupportedFormats() {
|
|
298
|
+
return ["text", "markdown"];
|
|
299
|
+
}
|
|
300
|
+
async onLoad(context) {
|
|
301
|
+
this.context = context;
|
|
302
|
+
context.logger.log("Jina AI Reader Plugin loaded");
|
|
303
|
+
}
|
|
304
|
+
async onUnload() {
|
|
305
|
+
this.context = void 0;
|
|
306
|
+
}
|
|
307
|
+
async healthCheck() {
|
|
308
|
+
return {
|
|
309
|
+
status: "healthy",
|
|
310
|
+
message: "Jina AI Reader plugin is ready",
|
|
311
|
+
checkedAt: Date.now()
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
getManifest() {
|
|
315
|
+
return {
|
|
316
|
+
id: this.id,
|
|
317
|
+
name: this.name,
|
|
318
|
+
version: this.version,
|
|
319
|
+
description: "Web search and content extraction using the Jina AI APIs",
|
|
320
|
+
category: this.category,
|
|
321
|
+
capabilities: [...this.capabilities],
|
|
322
|
+
author: { name: "Ever Works Team" },
|
|
323
|
+
license: "AGPL-3.0",
|
|
324
|
+
builtIn: true,
|
|
325
|
+
systemPlugin: false,
|
|
326
|
+
autoEnable: false,
|
|
327
|
+
homepage: "https://jina.ai",
|
|
328
|
+
icon: {
|
|
329
|
+
type: "url",
|
|
330
|
+
value: "https://jina.ai/icons/favicon-128x128.png"
|
|
331
|
+
},
|
|
332
|
+
readme: [
|
|
333
|
+
"## What does Jina AI do?",
|
|
334
|
+
"",
|
|
335
|
+
"Jina AI provides web search with LLM-optimized results and content extraction that converts any web page into clean markdown.",
|
|
336
|
+
"",
|
|
337
|
+
"## Why use it?",
|
|
338
|
+
"",
|
|
339
|
+
"- **Web search** \u2014 search the web and get results with content already extracted",
|
|
340
|
+
"- **Content extraction** \u2014 converts pages to clean markdown, strips ads and navigation",
|
|
341
|
+
"- **Domain filtering** \u2014 restrict search to specific domains",
|
|
342
|
+
"",
|
|
343
|
+
"## How it works in Ever Works",
|
|
344
|
+
"",
|
|
345
|
+
"During work generation, Jina finds relevant information about each item via search and extracts clean content from web pages for enriching descriptions.",
|
|
346
|
+
"",
|
|
347
|
+
"## Getting started",
|
|
348
|
+
"",
|
|
349
|
+
"1. Get an API key at [jina.ai](https://jina.ai)",
|
|
350
|
+
"2. Enter the key in the **API Key** field below",
|
|
351
|
+
"3. Enable the plugin"
|
|
352
|
+
].join("\n")
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
exports.JinaReaderPlugin = JinaReaderPlugin;
|
|
358
|
+
exports.default = JinaReaderPlugin;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IPlugin, ISearchPlugin, IContentExtractorPlugin, PluginCategory, JsonSchema, SearchOptions, SearchResponse, ConnectionValidationResult, RateLimitInfo, ContentExtractionOptions, ContentExtractionResult, PluginContext, PluginHealthCheck, PluginManifest } from '@ever-works/plugin';
|
|
2
|
+
|
|
3
|
+
declare class JinaReaderPlugin implements IPlugin, ISearchPlugin, IContentExtractorPlugin {
|
|
4
|
+
readonly id = "jina";
|
|
5
|
+
readonly name = "Jina AI";
|
|
6
|
+
readonly version = "1.0.0";
|
|
7
|
+
readonly category: PluginCategory;
|
|
8
|
+
readonly capabilities: readonly string[];
|
|
9
|
+
readonly providerName = "Jina";
|
|
10
|
+
readonly settingsSchema: JsonSchema;
|
|
11
|
+
readonly configurationMode: 'admin-only' | 'user-required' | 'hybrid';
|
|
12
|
+
private context?;
|
|
13
|
+
search(options: SearchOptions): Promise<SearchResponse>;
|
|
14
|
+
isAvailable(): Promise<boolean>;
|
|
15
|
+
validateConnection(settings: Record<string, unknown>): Promise<ConnectionValidationResult>;
|
|
16
|
+
getRateLimitInfo(): Promise<RateLimitInfo>;
|
|
17
|
+
extract(options: ContentExtractionOptions): Promise<ContentExtractionResult>;
|
|
18
|
+
extractBatch(urls: readonly string[], options?: Partial<ContentExtractionOptions>): Promise<readonly ContentExtractionResult[]>;
|
|
19
|
+
canExtract(url: string): Promise<boolean>;
|
|
20
|
+
getSupportedFormats(): readonly ('text' | 'html' | 'markdown')[];
|
|
21
|
+
onLoad(context: PluginContext): Promise<void>;
|
|
22
|
+
onUnload(): Promise<void>;
|
|
23
|
+
healthCheck(): Promise<PluginHealthCheck>;
|
|
24
|
+
getManifest(): PluginManifest;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { JinaReaderPlugin, JinaReaderPlugin as default };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { IPlugin, ISearchPlugin, IContentExtractorPlugin, PluginCategory, JsonSchema, SearchOptions, SearchResponse, ConnectionValidationResult, RateLimitInfo, ContentExtractionOptions, ContentExtractionResult, PluginContext, PluginHealthCheck, PluginManifest } from '@ever-works/plugin';
|
|
2
|
+
|
|
3
|
+
declare class JinaReaderPlugin implements IPlugin, ISearchPlugin, IContentExtractorPlugin {
|
|
4
|
+
readonly id = "jina";
|
|
5
|
+
readonly name = "Jina AI";
|
|
6
|
+
readonly version = "1.0.0";
|
|
7
|
+
readonly category: PluginCategory;
|
|
8
|
+
readonly capabilities: readonly string[];
|
|
9
|
+
readonly providerName = "Jina";
|
|
10
|
+
readonly settingsSchema: JsonSchema;
|
|
11
|
+
readonly configurationMode: 'admin-only' | 'user-required' | 'hybrid';
|
|
12
|
+
private context?;
|
|
13
|
+
search(options: SearchOptions): Promise<SearchResponse>;
|
|
14
|
+
isAvailable(): Promise<boolean>;
|
|
15
|
+
validateConnection(settings: Record<string, unknown>): Promise<ConnectionValidationResult>;
|
|
16
|
+
getRateLimitInfo(): Promise<RateLimitInfo>;
|
|
17
|
+
extract(options: ContentExtractionOptions): Promise<ContentExtractionResult>;
|
|
18
|
+
extractBatch(urls: readonly string[], options?: Partial<ContentExtractionOptions>): Promise<readonly ContentExtractionResult[]>;
|
|
19
|
+
canExtract(url: string): Promise<boolean>;
|
|
20
|
+
getSupportedFormats(): readonly ('text' | 'html' | 'markdown')[];
|
|
21
|
+
onLoad(context: PluginContext): Promise<void>;
|
|
22
|
+
onUnload(): Promise<void>;
|
|
23
|
+
healthCheck(): Promise<PluginHealthCheck>;
|
|
24
|
+
getManifest(): PluginManifest;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { JinaReaderPlugin, JinaReaderPlugin as default };
|