feat: add arxiv and wikipedia adapters (#132)
Add arXiv (search, paper) and Wikipedia (search, summary) public API adapters. - arxiv/search: search papers by keyword - arxiv/paper: get paper details by ID - wikipedia/search: search articles with lang support - wikipedia/summary: get article summary Type safety fixes applied: wikiFetch returns unknown, typed search results. Co-authored-by: BruceLoveDecimal <39156883+BruceLoveDecimal@users.noreply.github.com>
This commit is contained in:
1 parent
4f74b45963
commit
3cda14a2ab
8 files changed
+190
No files matched your search
@@ -137,6 +137,8 @@ Run `opencli list` for the live registry.
|
||||
| **bbc** | `news` | Public |
|
||||
| **ctrip** | `search` | Browser |
|
||||
| **github** | `search` | Public |
|
||||
| **arxiv** | `search` `paper` | Public |
|
||||
| **wikipedia** | `search` `summary` | Public |
|
||||
| **hackernews** | `top` | Public |
|
||||
| **linkedin** | `search` | Browser |
|
||||
| **reuters** | `search` | Browser |
|
||||
|
||||
@@ -138,6 +138,8 @@ npm install -g @jackwener/opencli@latest
|
||||
| **bbc** | `news` | 公共 API |
|
||||
| **ctrip** | `search` | 浏览器 |
|
||||
| **github** | `search` | 公共 API |
|
||||
| **arxiv** | `search` `paper` | 公开 |
|
||||
| **wikipedia** | `search` `summary` | 公开 |
|
||||
| **hackernews** | `top` | 公共 API |
|
||||
| **linkedin** | `search` | 浏览器 |
|
||||
| **reuters** | `search` | 浏览器 |
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { CliError } from '../../errors.js';
|
||||
import { arxivFetch, parseEntries } from './utils.js';
|
||||
|
||||
cli({
|
||||
site: 'arxiv',
|
||||
name: 'paper',
|
||||
description: 'Get arXiv paper details by ID',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'id', positional: true, required: true, help: 'arXiv paper ID (e.g. 1706.03762)' },
|
||||
],
|
||||
columns: ['id', 'title', 'authors', 'published', 'abstract', 'url'],
|
||||
func: async (_page, args) => {
|
||||
const xml = await arxivFetch(`id_list=${encodeURIComponent(args.id)}`);
|
||||
const entries = parseEntries(xml);
|
||||
if (!entries.length) throw new CliError('NOT_FOUND', `Paper ${args.id} not found`, 'Check the arXiv ID format, e.g. 1706.03762');
|
||||
return entries;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { CliError } from '../../errors.js';
|
||||
import { arxivFetch, parseEntries } from './utils.js';
|
||||
|
||||
cli({
|
||||
site: 'arxiv',
|
||||
name: 'search',
|
||||
description: 'Search arXiv papers',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'keyword', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
|
||||
{ name: 'limit', type: 'int', default: 10, help: 'Max results (max 25)' },
|
||||
],
|
||||
columns: ['id', 'title', 'authors', 'published'],
|
||||
func: async (_page, args) => {
|
||||
const limit = Math.max(1, Math.min(Number(args.limit), 25));
|
||||
const query = encodeURIComponent(`all:${args.keyword}`);
|
||||
const xml = await arxivFetch(`search_query=${query}&max_results=${limit}&sortBy=relevance`);
|
||||
const entries = parseEntries(xml);
|
||||
if (!entries.length) throw new CliError('NOT_FOUND', 'No papers found', 'Try a different keyword');
|
||||
return entries.map(e => ({ id: e.id, title: e.title, authors: e.authors, published: e.published }));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* arXiv adapter utilities.
|
||||
*
|
||||
* arXiv exposes a public Atom/XML API — no key required.
|
||||
* https://info.arxiv.org/help/api/index.html
|
||||
*/
|
||||
|
||||
import { CliError } from '../../errors.js';
|
||||
|
||||
export const ARXIV_BASE = 'https://export.arxiv.org/api/query';
|
||||
|
||||
export async function arxivFetch(params: string): Promise<string> {
|
||||
const resp = await fetch(`${ARXIV_BASE}?${params}`);
|
||||
if (!resp.ok) {
|
||||
throw new CliError('FETCH_ERROR', `arXiv API HTTP ${resp.status}`, 'Check your search term or paper ID');
|
||||
}
|
||||
return resp.text();
|
||||
}
|
||||
|
||||
/** Extract the text content of the first matching XML tag. */
|
||||
function extract(xml: string, tag: string): string {
|
||||
const m = xml.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`));
|
||||
return m ? m[1].trim() : '';
|
||||
}
|
||||
|
||||
/** Extract all text contents of a repeated XML tag. */
|
||||
function extractAll(xml: string, tag: string): string[] {
|
||||
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'g');
|
||||
const results: string[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(xml)) !== null) results.push(m[1].trim());
|
||||
return results;
|
||||
}
|
||||
|
||||
export interface ArxivEntry {
|
||||
id: string;
|
||||
title: string;
|
||||
authors: string;
|
||||
abstract: string;
|
||||
published: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/** Parse Atom XML feed into structured entries. */
|
||||
export function parseEntries(xml: string): ArxivEntry[] {
|
||||
const entryRe = /<entry>([\s\S]*?)<\/entry>/g;
|
||||
const entries: ArxivEntry[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = entryRe.exec(xml)) !== null) {
|
||||
const e = m[1];
|
||||
const rawId = extract(e, 'id');
|
||||
const arxivId = rawId.replace(/^https?:\/\/arxiv\.org\/abs\//, '').replace(/v\d+$/, '');
|
||||
entries.push({
|
||||
id: arxivId,
|
||||
title: extract(e, 'title').replace(/\s+/g, ' '),
|
||||
authors: extractAll(e, 'name').slice(0, 3).join(', '),
|
||||
abstract: extract(e, 'summary').replace(/\s+/g, ' ').slice(0, 200) + '...',
|
||||
published: extract(e, 'published').slice(0, 10),
|
||||
url: `https://arxiv.org/abs/${arxivId}`,
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { CliError } from '../../errors.js';
|
||||
import { wikiFetch } from './utils.js';
|
||||
|
||||
cli({
|
||||
site: 'wikipedia',
|
||||
name: 'search',
|
||||
description: 'Search Wikipedia articles',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'keyword', positional: true, required: true, help: 'Search keyword' },
|
||||
{ name: 'limit', type: 'int', default: 10, help: 'Max results' },
|
||||
{ name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' },
|
||||
],
|
||||
columns: ['title', 'snippet', 'url'],
|
||||
func: async (_page, args) => {
|
||||
const limit = Math.max(1, Math.min(Number(args.limit), 50));
|
||||
const lang = args.lang || 'en';
|
||||
const q = encodeURIComponent(args.keyword);
|
||||
const data = await wikiFetch(lang, `/w/api.php?action=query&list=search&srsearch=${q}&srlimit=${limit}&format=json&utf8=1`);
|
||||
const results = data?.query?.search;
|
||||
if (!results?.length) throw new CliError('NOT_FOUND', 'No articles found', 'Try a different keyword');
|
||||
return results.map((r: any) => ({
|
||||
title: r.title,
|
||||
snippet: r.snippet.replace(/<[^>]+>/g, '').slice(0, 120),
|
||||
url: `https://${lang}.wikipedia.org/wiki/${encodeURIComponent(r.title.replace(/ /g, '_'))}`,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { cli, Strategy } from '../../registry.js';
|
||||
import { CliError } from '../../errors.js';
|
||||
import { wikiFetch } from './utils.js';
|
||||
|
||||
cli({
|
||||
site: 'wikipedia',
|
||||
name: 'summary',
|
||||
description: 'Get Wikipedia article summary',
|
||||
strategy: Strategy.PUBLIC,
|
||||
browser: false,
|
||||
args: [
|
||||
{ name: 'title', positional: true, required: true, help: 'Article title (e.g. "Transformer (machine learning model)")' },
|
||||
{ name: 'lang', default: 'en', help: 'Language code (e.g. en, zh, ja)' },
|
||||
],
|
||||
columns: ['title', 'description', 'extract', 'url'],
|
||||
func: async (_page, args) => {
|
||||
const lang = args.lang || 'en';
|
||||
const title = encodeURIComponent(args.title.replace(/ /g, '_'));
|
||||
const data = await wikiFetch(lang, `/api/rest_v1/page/summary/${title}`);
|
||||
if (!data?.title) throw new CliError('NOT_FOUND', `Article "${args.title}" not found`, 'Try searching first: opencli wikipedia search <keyword>');
|
||||
return [{
|
||||
title: data.title,
|
||||
description: data.description ?? '-',
|
||||
extract: (data.extract ?? '').slice(0, 300),
|
||||
url: data.content_urls?.desktop?.page ?? `https://${lang}.wikipedia.org/wiki/${title}`,
|
||||
}];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Wikipedia adapter utilities.
|
||||
*
|
||||
* Uses the public MediaWiki REST API and Action API — no key required.
|
||||
* REST API: https://en.wikipedia.org/api/rest_v1/
|
||||
* Action API: https://en.wikipedia.org/w/api.php
|
||||
*/
|
||||
|
||||
import { CliError } from '../../errors.js';
|
||||
|
||||
export async function wikiFetch(lang: string, path: string): Promise<any> {
|
||||
const url = `https://${lang}.wikipedia.org${path}`;
|
||||
const resp = await fetch(url, {
|
||||
headers: { 'User-Agent': 'opencli/1.0 (https://github.com/jackwener/opencli)' },
|
||||
});
|
||||
if (!resp.ok) {
|
||||
throw new CliError('FETCH_ERROR', `Wikipedia API HTTP ${resp.status}`, `Check your title or search term`);
|
||||
}
|
||||
return resp.json();
|
||||
}
|
||||
Reference in new issue
Block a user