paper-burner/workers/academic-search-proxy/test.html

852 lines
31 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Academic Search Proxy - 测试页面</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
margin-bottom: 10px;
color: #333;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.config-section {
background: #f9f9f9;
padding: 20px;
border-radius: 6px;
margin-bottom: 30px;
border: 1px solid #e0e0e0;
}
.config-section h2 {
font-size: 16px;
margin-bottom: 15px;
color: #333;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #555;
}
input[type="text"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 8px;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.test-section {
margin-bottom: 30px;
}
.test-section h2 {
font-size: 18px;
margin-bottom: 15px;
color: #333;
border-bottom: 2px solid #4CAF50;
padding-bottom: 8px;
}
.test-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.test-btn {
padding: 12px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.test-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.test-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-health {
background: #2196F3;
color: white;
}
.btn-crossref {
background: #FF9800;
color: white;
}
.btn-openalex {
background: #9C27B0;
color: white;
}
.btn-pubmed {
background: #4CAF50;
color: white;
}
.btn-semanticscholar {
background: #E91E63;
color: white;
}
.btn-arxiv {
background: #00BCD4;
color: white;
}
.btn-all {
background: #607D8B;
color: white;
grid-column: 1 / -1;
}
.result-box {
margin-top: 20px;
padding: 20px;
border-radius: 6px;
border: 1px solid #ddd;
background: #fafafa;
max-height: 400px;
overflow-y: auto;
}
.result-box h3 {
font-size: 14px;
margin-bottom: 10px;
color: #666;
}
.result-box pre {
background: white;
padding: 15px;
border-radius: 4px;
border: 1px solid #e0e0e0;
overflow-x: auto;
font-size: 12px;
line-height: 1.5;
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.status-success {
background: #4CAF50;
}
.status-error {
background: #f44336;
}
.status-loading {
background: #FF9800;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.info-box {
background: #E3F2FD;
border-left: 4px solid #2196F3;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.info-box h3 {
font-size: 14px;
margin-bottom: 8px;
color: #1976D2;
}
.info-box p {
font-size: 13px;
color: #555;
line-height: 1.6;
}
.info-box code {
background: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
border: 1px solid #BBDEFB;
}
</style>
</head>
<body>
<div class="container">
<h1>🔬 Academic Search Proxy 测试</h1>
<p class="subtitle">测试 Cloudflare Worker 代理服务</p>
<!-- 配置区域 -->
<div class="config-section">
<h2>⚙️ 代理配置</h2>
<!-- Worker 地址 -->
<div class="form-group">
<label>Worker 地址 (Proxy URL)</label>
<input type="text" id="proxyUrl" placeholder="http://localhost:8787" value="http://localhost:8787">
</div>
<!-- 使用模式 -->
<div class="form-group">
<label>使用模式</label>
<select id="usageMode" onchange="toggleModeConfig()">
<option value="passthrough">方案一:透传模式(推荐)</option>
<option value="shared">方案二:共享密钥模式</option>
</select>
</div>
<!-- 方案一:透传模式配置 -->
<div id="passthroughConfig" style="display: block;">
<div class="form-group">
<label>Semantic Scholar API Key可选</label>
<input type="text" id="s2ApiKey" placeholder="你的 Semantic Scholar API Key">
<small style="color: #666; font-size: 12px;">留空表示不使用 API Key20次/分钟限制)</small>
</div>
<div class="form-group">
<label>PubMed API Key可选</label>
<input type="text" id="pubmedApiKey" placeholder="你的 PubMed API Key">
<small style="color: #666; font-size: 12px;">留空表示不使用 API Key3次/秒限制)</small>
</div>
</div>
<!-- 方案二:共享密钥模式配置 -->
<div id="sharedConfig" style="display: none;">
<div class="form-group">
<label>Auth Key必须</label>
<input type="password" id="authKey" placeholder="Worker 的 AUTH_SECRET">
<small style="color: #666; font-size: 12px;">向 Worker 部署者获取此密钥</small>
</div>
</div>
<!-- 启用代理 -->
<div class="form-group checkbox-group">
<input type="checkbox" id="useProxy" checked>
<label for="useProxy" style="margin: 0;">启用代理(对 PubMed 和 Semantic Scholar</label>
</div>
</div>
<!-- 使用说明 -->
<div class="info-box">
<h3>📖 使用说明</h3>
<p>
<strong>本地测试:</strong><br>
1. 启动 Worker<code>cd workers/academic-search-proxy && npx wrangler dev</code><br>
2. 确认代理地址:<code>http://localhost:8787</code><br><br>
<strong>生产环境:</strong><br>
1. 部署 Worker 后,填入你的 Worker URL<br>
2. 选择使用模式(推荐方案一)<br>
3. 方案一:填入你自己的 API Key可选<br>
4. 方案二:填入 Worker 的 Auth Key必须<br><br>
<strong>测试范围:</strong><br>
- <strong>CrossRef、OpenAlex</strong>:直接访问(支持 CORS<br>
- <strong>PubMed、Semantic Scholar、arXiv</strong>:通过代理访问(解决 CORS
</p>
</div>
<!-- 健康检查 -->
<div class="test-section">
<h2>🏥 健康检查</h2>
<div class="test-grid">
<button class="test-btn btn-health" onclick="testHealth()">
<span class="status-indicator" id="status-health"></span>
测试 Worker 健康状态
</button>
</div>
<div class="result-box" id="result-health" style="display: none;">
<h3>结果:</h3>
<pre id="result-health-content"></pre>
</div>
</div>
<!-- API 测试 -->
<div class="test-section">
<h2>🔍 API 测试</h2>
<div class="test-grid">
<button class="test-btn btn-crossref" onclick="testCrossRef()">
<span class="status-indicator" id="status-crossref"></span>
CrossRef
</button>
<button class="test-btn btn-openalex" onclick="testOpenAlex()">
<span class="status-indicator" id="status-openalex"></span>
OpenAlex
</button>
<button class="test-btn btn-arxiv" onclick="testArXiv()">
<span class="status-indicator" id="status-arxiv"></span>
arXiv
</button>
<button class="test-btn btn-pubmed" onclick="testPubMed()">
<span class="status-indicator" id="status-pubmed"></span>
PubMed (需要代理)
</button>
<button class="test-btn btn-semanticscholar" onclick="testSemanticScholar()">
<span class="status-indicator" id="status-semanticscholar"></span>
Semantic Scholar (需要代理)
</button>
<button class="test-btn btn-all" onclick="testAll()">
🚀 测试全部
</button>
<button class="test-btn btn-all" onclick="testRateLimit()" style="background: #FF5722;">
⚡ 测试速率限制
</button>
</div>
<div class="result-box" id="result-api" style="display: none;">
<h3>结果:</h3>
<pre id="result-api-content"></pre>
</div>
</div>
</div>
<script>
const testQuery = 'machine learning';
// 切换模式配置
function toggleModeConfig() {
const mode = document.getElementById('usageMode').value;
document.getElementById('passthroughConfig').style.display =
mode === 'passthrough' ? 'block' : 'none';
document.getElementById('sharedConfig').style.display =
mode === 'shared' ? 'block' : 'none';
}
function getProxyUrl() {
return document.getElementById('proxyUrl').value.trim();
}
function isProxyEnabled() {
return document.getElementById('useProxy').checked;
}
function getUsageMode() {
return document.getElementById('usageMode').value;
}
function getAuthKey() {
return document.getElementById('authKey').value.trim();
}
function getS2ApiKey() {
return document.getElementById('s2ApiKey').value.trim();
}
function getPubMedApiKey() {
return document.getElementById('pubmedApiKey').value.trim();
}
// 构建请求头
function buildHeaders() {
const headers = {};
const mode = getUsageMode();
if (mode === 'shared') {
// 方案二:共享密钥模式
const authKey = getAuthKey();
if (authKey) {
headers['X-Auth-Key'] = authKey;
}
} else {
// 方案一:透传模式
// API Key 会在具体服务中添加
}
return headers;
}
function setStatus(id, status) {
const el = document.getElementById(`status-${id}`);
if (el) {
el.className = 'status-indicator';
if (status === 'loading') el.classList.add('status-loading');
else if (status === 'success') el.classList.add('status-success');
else if (status === 'error') el.classList.add('status-error');
}
}
function showResult(boxId, content) {
const box = document.getElementById(boxId);
const contentEl = document.getElementById(`${boxId}-content`);
box.style.display = 'block';
contentEl.textContent = JSON.stringify(content, null, 2);
}
async function testHealth() {
const proxyUrl = getProxyUrl();
if (!proxyUrl) {
alert('请输入 Worker 地址');
return;
}
setStatus('health', 'loading');
try {
const headers = buildHeaders();
const response = await fetch(`${proxyUrl}/health`, { headers });
const data = await response.json();
setStatus('health', 'success');
showResult('result-health', {
status: response.status,
data: data
});
} catch (error) {
setStatus('health', 'error');
showResult('result-health', {
error: error.message,
tip: getUsageMode() === 'shared' && !getAuthKey()
? '共享密钥模式需要 Auth Key'
: '请确认 Worker 是否启动npx wrangler dev'
});
}
}
async function testCrossRef() {
setStatus('crossref', 'loading');
try {
// CrossRef 支持 CORS直接访问
const url = `https://api.crossref.org/works?query.title=${encodeURIComponent(testQuery)}&rows=1`;
const response = await fetch(url);
const data = await response.json();
setStatus('crossref', 'success');
showResult('result-api', {
service: 'CrossRef',
method: 'Direct (CORS supported)',
status: response.status,
found: data.message?.items?.length || 0,
sample: data.message?.items?.[0] || null
});
} catch (error) {
setStatus('crossref', 'error');
showResult('result-api', {
service: 'CrossRef',
error: error.message
});
}
}
async function testOpenAlex() {
setStatus('openalex', 'loading');
try {
// OpenAlex 支持 CORS直接访问
const url = `https://api.openalex.org/works?search=${encodeURIComponent(testQuery)}`;
const response = await fetch(url);
const data = await response.json();
setStatus('openalex', 'success');
showResult('result-api', {
service: 'OpenAlex',
method: 'Direct (CORS supported)',
status: response.status,
found: data.results?.length || 0,
sample: data.results?.[0] || null
});
} catch (error) {
setStatus('openalex', 'error');
showResult('result-api', {
service: 'OpenAlex',
error: error.message
});
}
}
async function testArXiv() {
setStatus('arxiv', 'loading');
try {
const useProxy = getProxyUrl();
const mode = getUsageMode();
if (useProxy) {
// 通过代理访问 arXiv
const proxyUrl = `${getProxyUrl()}/api/arxiv/query?search_query=ti:${encodeURIComponent(testQuery)}&max_results=1`;
const headers = buildHeaders();
const response = await fetch(proxyUrl, { headers });
const xmlText = await response.text();
// 解析 XML 提取结果数量
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'application/xml');
const totalResults = xmlDoc.querySelector('totalResults')?.textContent || '0';
const entries = xmlDoc.querySelectorAll('entry');
let sampleData = { totalResults, found: entries.length };
if (entries.length > 0) {
const firstEntry = entries[0];
sampleData.title = firstEntry.querySelector('title')?.textContent?.trim();
sampleData.id = firstEntry.querySelector('id')?.textContent?.trim();
sampleData.published = firstEntry.querySelector('published')?.textContent?.trim();
sampleData.summary = firstEntry.querySelector('summary')?.textContent?.trim().substring(0, 200) + '...';
}
setStatus('arxiv', 'success');
showResult('result-api', {
service: 'arXiv',
method: 'Via Proxy',
mode: mode,
status: response.status,
contentType: response.headers.get('content-type'),
found: totalResults,
sample: sampleData
});
} else {
// 直接访问(会因 CORS 失败)
const url = `http://export.arxiv.org/api/query?search_query=ti:${encodeURIComponent(testQuery)}&max_results=1`;
const response = await fetch(url);
const xmlText = await response.text();
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'application/xml');
const totalResults = xmlDoc.querySelector('totalResults')?.textContent || '0';
const entries = xmlDoc.querySelectorAll('entry');
let sampleData = { totalResults, found: entries.length };
if (entries.length > 0) {
const firstEntry = entries[0];
sampleData.title = firstEntry.querySelector('title')?.textContent?.trim();
sampleData.id = firstEntry.querySelector('id')?.textContent?.trim();
}
setStatus('arxiv', 'success');
showResult('result-api', {
service: 'arXiv',
method: 'Direct (may fail due to CORS)',
status: response.status,
contentType: response.headers.get('content-type'),
found: totalResults,
sample: sampleData
});
}
} catch (error) {
setStatus('arxiv', 'error');
showResult('result-api', {
service: 'arXiv',
error: error.message
});
}
}
async function testPubMed() {
setStatus('pubmed', 'loading');
const useProxy = isProxyEnabled();
const proxyUrl = getProxyUrl();
try {
// Step 1: esearch - 搜索获取 PMID
let searchUrl;
const headers = {};
if (useProxy) {
searchUrl = `${proxyUrl}/api/pubmed/esearch.fcgi?db=pubmed&term=${encodeURIComponent(testQuery)}&retmode=json&retmax=1`;
Object.assign(headers, buildHeaders());
if (getUsageMode() === 'passthrough') {
const apiKey = getPubMedApiKey();
if (apiKey) {
headers['X-Api-Key'] = apiKey;
}
}
} else {
searchUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=${encodeURIComponent(testQuery)}&retmode=json&retmax=1`;
}
const searchResponse = await fetch(searchUrl, { headers });
const searchData = await searchResponse.json();
const pmids = searchData.esearchresult?.idlist || [];
if (pmids.length === 0) {
setStatus('pubmed', 'success');
showResult('result-api', {
service: 'PubMed',
method: useProxy ? 'Via Proxy (2-step)' : 'Direct',
mode: getUsageMode(),
status: searchResponse.status,
found: searchData.esearchresult?.count || 0,
message: 'No results found',
searchResult: searchData
});
return;
}
// Step 2: efetch - 获取详细信息
let fetchUrl;
if (useProxy) {
fetchUrl = `${proxyUrl}/api/pubmed/efetch.fcgi?db=pubmed&id=${pmids[0]}&retmode=xml`;
} else {
fetchUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=${pmids[0]}&retmode=xml`;
}
const fetchResponse = await fetch(fetchUrl, { headers });
const xmlText = await fetchResponse.text();
// 解析 XML
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(xmlText, 'application/xml');
const article = xmlDoc.querySelector('PubmedArticle');
let paperData = { pmid: pmids[0] };
if (article) {
// 提取标题
const titleEl = article.querySelector('ArticleTitle');
paperData.title = titleEl?.textContent?.trim();
// 提取作者
const authorEls = article.querySelectorAll('Author');
paperData.authors = Array.from(authorEls).slice(0, 3).map(auth => {
const lastName = auth.querySelector('LastName')?.textContent;
const foreName = auth.querySelector('ForeName')?.textContent;
return foreName && lastName ? `${foreName} ${lastName}` : (lastName || foreName);
}).filter(Boolean);
// 提取 DOI
const articleIds = article.querySelectorAll('ArticleId');
for (const id of articleIds) {
if (id.getAttribute('IdType') === 'doi') {
paperData.doi = id.textContent.trim();
break;
}
}
// 提取期刊
const journalEl = article.querySelector('Journal Title');
paperData.journal = journalEl?.textContent?.trim();
// 提取年份
const yearEl = article.querySelector('PubDate Year');
paperData.year = yearEl?.textContent?.trim();
// 提取摘要
const abstractEl = article.querySelector('AbstractText');
paperData.abstract = abstractEl?.textContent?.trim().substring(0, 300) + '...';
}
setStatus('pubmed', 'success');
showResult('result-api', {
service: 'PubMed',
method: useProxy ? 'Via Proxy (2-step: esearch + efetch)' : 'Direct',
mode: getUsageMode(),
status: fetchResponse.status,
found: searchData.esearchresult?.count || 0,
steps: {
step1: 'esearch - found PMIDs',
step2: 'efetch - fetched details'
},
sample: paperData
});
} catch (error) {
setStatus('pubmed', 'error');
showResult('result-api', {
service: 'PubMed',
method: useProxy ? 'Via Proxy' : 'Direct',
error: error.message,
tip: useProxy ? '请确认 Worker 已启动' : '请启用代理'
});
}
}
async function testSemanticScholar() {
setStatus('semanticscholar', 'loading');
const useProxy = isProxyEnabled();
const proxyUrl = getProxyUrl();
try {
let url;
const headers = {};
if (useProxy) {
// 通过代理
url = `${proxyUrl}/api/semanticscholar/graph/v1/paper/search?query=${encodeURIComponent(testQuery)}&limit=1`;
Object.assign(headers, buildHeaders());
// 方案一:透传 API Key
if (getUsageMode() === 'passthrough') {
const apiKey = getS2ApiKey();
if (apiKey) {
headers['X-Api-Key'] = apiKey;
}
}
} else {
// 直接访问(会失败)
url = `https://api.semanticscholar.org/graph/v1/paper/search?query=${encodeURIComponent(testQuery)}&limit=1`;
}
const response = await fetch(url, { headers });
const data = await response.json();
setStatus('semanticscholar', 'success');
showResult('result-api', {
service: 'Semantic Scholar',
method: useProxy ? 'Via Proxy' : 'Direct (will fail due to CORS)',
mode: getUsageMode(),
status: response.status,
found: data.data?.length || 0,
sample: data.data?.[0] || null
});
} catch (error) {
setStatus('semanticscholar', 'error');
showResult('result-api', {
service: 'Semantic Scholar',
method: useProxy ? 'Via Proxy' : 'Direct',
error: error.message,
tip: useProxy ? '请确认 Worker 已启动' : '请启用代理'
});
}
}
async function testRateLimit() {
const proxyUrl = getProxyUrl();
if (!proxyUrl) {
alert('请输入 Worker 地址');
return;
}
if (!confirm('将快速发送 20 个请求测试速率限制,是否继续?')) {
return;
}
showResult('result-api', {
message: '速率限制测试中...',
note: '发送 20 个请求,观察是否触发 429 错误'
});
const headers = buildHeaders();
const results = [];
for (let i = 0; i < 20; i++) {
try {
const start = Date.now();
const response = await fetch(`${proxyUrl}/health`, { headers });
const duration = Date.now() - start;
const data = await response.json();
results.push({
request: i + 1,
status: response.status,
duration: `${duration}ms`,
rateLimitRemaining: response.headers.get('X-RateLimit-Remaining'),
success: response.status === 200
});
if (response.status === 429) {
const retryAfter = data.retryAfter || response.headers.get('Retry-After');
results.push({
message: `触发速率限制!需要等待 ${retryAfter}`
});
break;
}
} catch (error) {
results.push({
request: i + 1,
error: error.message
});
}
// 快速发送,不延迟
}
showResult('result-api', {
test: 'Rate Limit',
totalRequests: results.length,
results: results,
summary: results.some(r => r.status === 429)
? '✅ 速率限制正常工作'
: '⚠️ 未触发速率限制(可能限制配置较宽松)'
});
}
async function testAll() {
await testHealth();
await new Promise(resolve => setTimeout(resolve, 500));
await testCrossRef();
await new Promise(resolve => setTimeout(resolve, 500));
await testOpenAlex();
await new Promise(resolve => setTimeout(resolve, 500));
await testArXiv();
await new Promise(resolve => setTimeout(resolve, 500));
await testPubMed();
await new Promise(resolve => setTimeout(resolve, 500));
await testSemanticScholar();
}
</script>
</body>
</html>