@seacolour/openalex-mcp-server-tool
Version:
MCP server for querying OpenAlex papers
494 lines (435 loc) • 17.2 kB
JavaScript
import fetch from 'node-fetch';
/**
* 将 abstract_inverted_index 转换为摘要文本。
* @param {Object} abstractInvertedIndex - 摘要的倒排索引对象
* @returns {string} - 转换后的摘要文本
*/
function convertAbstract(abstractInvertedIndex) {
if (!abstractInvertedIndex || typeof abstractInvertedIndex !== 'object') {
return '无摘要';
}
const posMap = [];
for (const [word, positions] of Object.entries(abstractInvertedIndex)) {
for (const pos of positions) {
posMap.push({pos, word});
}
}
posMap.sort((a, b) => a.pos - b.pos);
return posMap.map(item => item.word).join(' ');
}
/**
* 查询 OpenAlex API 获取论文信息。
* @param {Object} params - 查询参数
* @param {string} params.keyword - 关键词
* @param {number} params.maxResults - 最大结果数
* @param {number} [params.year] - 发表年份
* @param {boolean} [params.openAccess] - 是否开放获取
* @param {string} [params.authorId] - 作者 ID
* @param {string} [params.institutionId] - 机构 ID
* @param {string} [params.type] - 论文类型
* @returns {Promise<Array>} - 包含论文信息的数组
*/
export async function searchPapers({
keyword,
maxResults,
year,
openAccess,
authorId,
institutionId,
type
}) {
const baseUrl = 'https://api.openalex.org/works';
const params = new URLSearchParams();
params.append('search', keyword);
params.append('per-page', maxResults);
const filters = [];
if (year) filters.push(`publication_year:${year}`);
if (openAccess !== undefined) filters.push(`is_oa:${openAccess}`);
if (authorId) filters.push(`authorships.author.id:${authorId}`);
if (institutionId) filters.push(`institutions.id:${institutionId}`);
if (type) filters.push(`type:${type}`);
if (filters.length > 0) {
params.append('filter', filters.join(','));
}
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
const papers = [];
const printedTitles = new Set();
for (const item of data.results) {
const title = item.title || '无标题';
if (printedTitles.has(title)) continue;
printedTitles.add(title);
const authors = item.authorships.map(auth => auth.author.display_name || '未知作者');
const publicationYear = item.publication_year || 0;
const abstractText = item.abstract_inverted_index
? convertAbstract(item.abstract_inverted_index)
: '无摘要';
const link = item.id || '无链接';
const citedByCount = item.cited_by_count || 0;
const pdfUrl = item.primary_location?.pdf_url || null;
papers.push({
title,
authors,
year: publicationYear,
abstractText,
link,
citedByCount,
pdfUrl
});
if (papers.length >= maxResults) break;
}
return papers;
}
/**
* 根据作者名称搜索作者信息。
* @param {string} authorName - 作者名称
* @param {number} [maxResults=5] - 返回的最大结果数
* @returns {Promise<Array>} - 包含作者信息的数组
*/
export async function searchAuthorsByName(authorName, maxResults = 5) {
const baseUrl = 'https://api.openalex.org/authors';
const params = new URLSearchParams();
params.append('filter', `display_name.search:${authorName}`);
params.append('per-page', maxResults);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
return data.results.map(author => ({
id: author.id,
name: author.display_name,
orcid: author.orcid || '无 ORCID',
worksCount: author.works_count || 0,
citationCount: author.cited_by_count || 0,
institutions: author.last_known_institutions ?
author.last_known_institutions.map(inst => ({
name: inst.display_name,
country: inst.country_code,
type: inst.type
})) : [],
hIndex: author.summary_stats?.h_index || 0,
i10Index: author.summary_stats?.i10_index || 0
}));
}
/**
* 根据机构名称获取 OpenAlex ID。
* @param {string} institutionName - 机构名称
* @returns {Promise<string|null>} - 返回机构的 OpenAlex ID 或 null
*/
export async function getInstitutionIdByName(institutionName) {
const baseUrl = 'https://api.openalex.org/institutions';
const params = new URLSearchParams();
params.append('filter', `display_name.search:${institutionName}`);
params.append('per-page', 1);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
if (data.results && data.results.length > 0) {
return data.results[0].id;
} else {
return null;
}
}
/**
* 根据机构 ID 查询作品。
* @param {string} institutionId - 机构的 OpenAlex ID
* @param {number} maxResults - 最大返回结果数
* @returns {Promise<Array>} - 返回作品列表
*/
export async function searchWorksByInstitutionId(institutionId, maxResults = 5) {
const baseUrl = 'https://api.openalex.org/works';
const params = new URLSearchParams();
params.append('filter', `authorships.institutions.id:${institutionId}`);
params.append('per-page', maxResults);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
return data.results.map(work => ({
id: work.id,
title: work.display_name,
publicationYear: work.publication_year,
citedByCount: work.cited_by_count,
doi: work.doi,
openAccess: work.open_access?.is_oa,
authors: work.authorships.map(auth => auth.author.display_name)
}));
}
/**
* 根据期刊名称获取 OpenAlex ID。
* @param {string} journalName - 期刊名称
* @returns {Promise<string|null>} - 返回期刊的 OpenAlex ID 或 null
*/
export async function getJournalIdByName(journalName) {
const baseUrl = 'https://api.openalex.org/sources';
const params = new URLSearchParams();
params.append('filter', `display_name.search:${journalName}`);
params.append('per-page', 1);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
if (data.results && data.results.length > 0) {
return data.results[0].id;
} else {
return null;
}
}
/**
* 根据期刊 ID 查询作品。
* @param {string} journalId - 期刊的 OpenAlex ID
* @param {number} maxResults - 最大返回结果数
* @returns {Promise<Array>} - 返回作品列表
*/
export async function searchWorksByJournalId(journalId, maxResults = 5) {
const baseUrl = 'https://api.openalex.org/works';
const params = new URLSearchParams();
params.append('filter', `primary_location.source.id:${journalId}`);
params.append('per-page', maxResults);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
return data.results.map(work => ({
id: work.id,
title: work.display_name,
publicationYear: work.publication_year,
citedByCount: work.cited_by_count,
doi: work.doi,
openAccess: work.open_access?.is_oa,
authors: work.authorships.map(auth => auth.author.display_name)
}));
}
/**
* 通过论文标题搜索论文。
* @param {string} title - 论文标题
* @param {number} [maxResults=5] - 返回的最大结果数
* @returns {Promise<Array>} - 包含论文信息的数组
*/
export async function searchPapersByTitle(title, maxResults = 5) {
const baseUrl = 'https://api.openalex.org/works';
const params = new URLSearchParams();
params.append('filter', `title.search:${title}`);
params.append('per-page', maxResults);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
const papers = [];
const printedTitles = new Set();
for (const item of data.results) {
const paperTitle = item.title || '无标题';
if (printedTitles.has(paperTitle)) continue;
printedTitles.add(paperTitle);
const authors = item.authorships.map(auth => auth.author.display_name || '未知作者');
const publicationYear = item.publication_year || 0;
const abstractText = item.abstract_inverted_index
? convertAbstract(item.abstract_inverted_index)
: '无摘要';
const link = item.id || '无链接';
const citedByCount = item.cited_by_count || 0;
const pdfUrl = item.primary_location?.pdf_url || null;
const doi = item.doi || null;
const journal = item.primary_location?.source?.display_name || '未知期刊';
papers.push({
title: paperTitle,
authors,
year: publicationYear,
abstractText,
link,
citedByCount,
pdfUrl,
doi,
journal
});
if (papers.length >= maxResults) break;
}
return papers;
}
/**
* 通过论文DOI精确查找论文。
* @param {string} doi - 论文的DOI标识符
* @returns {Promise<Object|null>} - 论文信息或null(如果未找到)
*/
export async function getPaperByDoi(doi) {
const baseUrl = 'https://api.openalex.org/works';
const params = new URLSearchParams();
params.append('filter', `doi:${doi}`);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
if (!data.results || data.results.length === 0) {
return null;
}
const item = data.results[0];
const title = item.title || '无标题';
const authors = item.authorships.map(auth => auth.author.display_name || '未知作者');
const publicationYear = item.publication_year || 0;
const abstractText = item.abstract_inverted_index
? convertAbstract(item.abstract_inverted_index)
: '无摘要';
const link = item.id || '无链接';
const citedByCount = item.cited_by_count || 0;
const pdfUrl = item.primary_location?.pdf_url || null;
const journal = item.primary_location?.source?.display_name || '未知期刊';
const isOpenAccess = item.open_access?.is_oa || false;
const concepts = item.concepts?.map(concept => ({
name: concept.display_name,
score: concept.score
})) || [];
return {
title,
authors,
year: publicationYear,
abstractText,
link,
citedByCount,
pdfUrl,
doi: doi,
journal,
isOpenAccess,
concepts
};
}
/**
* 进行高级论文搜索,支持多个过滤条件。
* @param {Object} params - 搜索参数
* @param {string} [params.keyword] - 关键词(可选)
* @param {number} [params.minCitations] - 最小引用次数(可选)
* @param {number} [params.maxCitations] - 最大引用次数(可选)
* @param {string} [params.concept] - 学科概念(可选)
* @param {boolean} [params.isOpenAccess] - 是否仅搜索开放获取论文(可选)
* @param {number} [params.fromYear] - 起始年份(可选)
* @param {number} [params.toYear] - 结束年份(可选)
* @param {number} [params.maxResults=10] - 返回的最大结果数
* @returns {Promise<Array>} - 包含论文信息的数组
*/
export async function advancedPaperSearch({
keyword,
minCitations,
maxCitations,
concept,
isOpenAccess,
fromYear,
toYear,
maxResults = 10
}) {
const baseUrl = 'https://api.openalex.org/works';
const params = new URLSearchParams();
// 添加关键词搜索
if (keyword) {
params.append('search', keyword);
}
// 构建过滤条件
const filters = [];
// 引用范围
if (minCitations !== undefined) {
filters.push(`cited_by_count>=${minCitations}`);
}
if (maxCitations !== undefined) {
filters.push(`cited_by_count<=${maxCitations}`);
}
// 学科概念
if (concept) {
filters.push(`concepts.name.search:${concept}`);
}
// 开放获取状态
if (isOpenAccess !== undefined) {
filters.push(`is_oa:${isOpenAccess}`);
}
// 发表年份范围
if (fromYear) {
filters.push(`publication_year>=${fromYear}`);
}
if (toYear) {
filters.push(`publication_year<=${toYear}`);
}
// 添加过滤条件
if (filters.length > 0) {
params.append('filter', filters.join(','));
}
// 设置结果数量
params.append('per-page', maxResults);
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
const data = await response.json();
return data.results.map(item => {
const title = item.title || '无标题';
const authors = item.authorships.map(auth => auth.author.display_name || '未知作者');
const publicationYear = item.publication_year || 0;
const abstractText = item.abstract_inverted_index
? convertAbstract(item.abstract_inverted_index)
: '无摘要';
const link = item.id || '无链接';
const citedByCount = item.cited_by_count || 0;
const pdfUrl = item.primary_location?.pdf_url || null;
const doi = item.doi || null;
const journal = item.primary_location?.source?.display_name || '未知期刊';
const isOpenAccess = item.open_access?.is_oa || false;
return {
title,
authors,
year: publicationYear,
abstractText,
link,
citedByCount,
pdfUrl,
doi,
journal,
isOpenAccess
};
});
}
/**
* 使用有道翻译API将中文查询参数翻译成英文
* @param {string} text - 要翻译的文本
* @param {string} appKey - 有道智云应用ID
* @param {string} appSecret - 有道智云应用密钥
* @returns {Promise<string>} - 翻译后的英文文本
*/
export async function translateQueryToEnglish(text, appKey, appSecret) {
// 如果文本是空的,直接返回
if (!text || text.trim() === '') {
return text;
}
// 检测文本是否包含中文字符
const hasChinese = /[\u4e00-\u9fa5]/.test(text);
if (!hasChinese) {
return text; // 如果不包含中文,直接返回原文
}
try {
// 导入Node.js内置的crypto模块
const crypto = await import('crypto');
// 生成签名
const salt = new Date().getTime();
const curtime = Math.round(new Date().getTime() / 1000);
const input = text.length <= 20 ? text : text.substring(0, 10) + text.length + text.substring(text.length - 10);
// 使用Node.js内置crypto模块的createHash方法
const sign = crypto.createHash('sha256')
.update(appKey + input + salt + curtime + appSecret)
.digest('hex');
const url = 'https://openapi.youdao.com/api';
const params = new URLSearchParams();
params.append('q', text);
params.append('from', 'zh-CHS'); // 源语言:中文
params.append('to', 'en'); // 目标语言:英文
params.append('appKey', appKey);
params.append('salt', salt);
params.append('sign', sign);
params.append('signType', 'v3');
params.append('curtime', curtime);
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: params
});
const data = await response.json();
if (data.errorCode === '0') {
return data.translation[0];
} else {
console.error('参数翻译错误:', data.errorCode);
return text; // 翻译失败时返回原文
}
} catch (error) {
console.error('参数翻译请求失败:', error);
return text; // 请求失败时返回原文
}
}