Compare commits

..

No commits in common. "master" and "feat/v0.6UI" have entirely different histories.

42 changed files with 2740 additions and 77137 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

@ -494,7 +494,6 @@ body.toc-dock-resizing #toc-vs-dock-resize-handle {
/* ==================== 8. 沉浸模式切换按钮 ==================== */
#toggle-immersive-btn {
display: none !important; /* 永久隐藏 */
position: fixed !important;
top: var(--spacing-md) !important;
right: var(--spacing-md) !important;
@ -674,9 +673,9 @@ body.immersive-active #immersive-main-content-area .container {
flex-direction: column !important;
}
/* 沉浸模式下显示 PDF 对照按钮(用户要求在沉浸模式中也能使用) */
/* 沉浸模式下隐藏 PDF 对照按钮 */
body.immersive-active #tab-pdf-compare {
display: flex !important;
display: none !important;
}
/* Modal 和右键菜单的 z-index 提升 */

View File

@ -85,6 +85,7 @@ services:
# 挂载前端与服务端源码为只读,确保容器运行当前工作区代码(避免旧镜像残留)
- ./server/src:/app/server/src:ro
- ./index.html:/app/index.html:ro
- ./login.html:/app/login.html:ro
- ./admin:/app/admin:ro
- ./views:/app/views:ro
- ./js:/app/js:ro

View File

@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Paper Burner X - 您的一站式AI文献阅读与智能分析平台支持OCR、翻译、深度解析与高级管理。">
<title>Paper Burner X - AI文献阅读与智能分析平台</title>
<link rel="icon" type="image/svg+xml" href="public/pure.svg">
<!-- CDN 性能优化DNS 预连接 - 提前建立连接,节省 100-300ms --> <link rel="dns-prefetch" href="https://gcore.jsdelivr.net"> <link rel="preconnect" href="https://gcore.jsdelivr.net" crossorigin> <link rel="dns-prefetch" href="https://cdnjs.cloudflare.com"> <link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin> <link rel="dns-prefetch" href="https://cdn.tailwindcss.com"> <link rel="preconnect" href="https://cdn.tailwindcss.com" crossorigin>
<!-- XSS 防护DOMPurify - 用于清理 AI 生成的 HTML 内容 -->
@ -237,11 +238,12 @@
bottom: -40px;
width: 300px;
height: 300px;
background-image: url('public/pure.svg');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
极低透明度,仅作纹理
opacity: 0.07;
opacity: 0.07;
transform: rotate(-10deg);
pointer-events: none;
} */
@ -580,6 +582,7 @@
<!-- 移动端 Header
<header class="md:hidden bg-white/80 backdrop-blur-md border-b border-slate-200 px-4 py-3 flex items-center justify-between sticky top-0 z-10">
<a href="views/landing/landing-page.html" class="text-lg font-bold flex items-center gap-2 text-slate-800">
<img src="public/pure.svg" class="w-7 h-7" alt="PBX Logo">
PBX
</a>
<div class="flex items-center gap-3">
@ -1194,7 +1197,7 @@
<!-- 卡片 4: 文件上传 -->
<!-- ===================== -->
<div class="modern-card p-4 md:p-6">
<!-- <div class="section-header justify-between">
<div class="section-header justify-between">
<h2 class="section-title">
<div class="w-9 h-9 rounded-xl text-white flex items-center justify-center mr-3 shadow-md" style="background: var(--color-primary); box-shadow: var(--shadow-sm);">
<iconify-icon icon="mdi:upload" width="20"></iconify-icon>
@ -1205,7 +1208,7 @@
<input type="checkbox" id="batchModeToggle" class="rounded border-slate-300 focus:ring-indigo-500 h-4 w-4" style="color: var(--color-primary); accent-color: var(--color-primary);">
<span class="select-none font-medium">批量模式</span>
</label>
</div> -->
</div>
<!-- 拖拽区域 -->
<div id="dropZone" class="upload-zone-modern p-8 text-center mb-6 group cursor-pointer">
@ -1437,6 +1440,42 @@
<script src="js/storage/storage.js"></script>
<!-- 先初始化存储适配器(提供 isFrontendMode 标记) -->
<script src="js/storage/storage-adapter.js"></script>
<script src="js/boot/backend-gate.js"></script>
<!-- 后端模式访问信任门禁:后端模式且未登录时,跳转到登录页 -->
<script>
(function() {
function gateIfBackend() {
try {
if (window.DEPLOYMENT_MODE === 'backend') {
var token = localStorage.getItem('auth_token');
if (!token) {
// 允许 ?mode=frontend 显式绕过(用于演示/调试)
var m = (new URLSearchParams(window.location.search).get('mode') || '').toLowerCase();
if (m === 'frontend') return;
var redirect = encodeURIComponent(window.location.href);
window.location.replace('/login.html?redirect=' + redirect);
}
}
} catch(e) { /* ignore */ }
}
// 若显式强制 backend则立即校验否则等待自动探测事件
try {
var forced = (new URLSearchParams(window.location.search).get('mode') || '').toLowerCase();
var envMode = (window.ENV_DEPLOYMENT_MODE || '').toLowerCase();
if (forced === 'backend' || envMode === 'backend') {
gateIfBackend();
}
} catch(_) {}
// 监听存储模式自动切换事件
window.addEventListener('pb:storage-mode-changed', function(evt) {
if (evt && evt.detail && evt.detail.mode === 'backend') {
gateIfBackend();
}
});
})();
</script>
<!-- 再加载术语库存储(会依据适配器模式决定是否探测后端) -->
<script src="js/storage/glossary-storage.js"></script>
<script src="https://gcore.jsdelivr.net/npm/mammoth@1.4.21/mammoth.browser.min.js"></script>
@ -1934,14 +1973,19 @@
// Desktop Sidebar Collapse Logic
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
const sidebarLogo = document.getElementById('sidebarLogo');
const LOGO_FULL = 'public/h_with_name.svg';
const LOGO_PURE = 'public/pure.svg';
function setSidebarState(collapsed) {
if (collapsed) {
appSidebar.classList.add('collapsed');
if (sidebarToggleIcon) sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-open');
if (sidebarLogo) sidebarLogo.src = LOGO_PURE;
} else {
appSidebar.classList.remove('collapsed');
if (sidebarToggleIcon) sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-close');
if (sidebarLogo) sidebarLogo.src = LOGO_FULL;
}
localStorage.setItem('pbx_sidebar_collapsed', collapsed);
}

2126
index.html.backup Normal file

File diff suppressed because it is too large Load Diff

View File

@ -66,7 +66,7 @@ async function uploadToMistral(fileToProcess, mistralKey) {
// 使用统一配置获取代理地址
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getMistralUrl('/v1/files')
: (window.PBX_PROXY_BASE_URL || '/api') + '/mistral/v1/files';
: 'http://localhost:3456/api/mistral/v1/files';
const response = await fetch(proxyUrl, {
method: 'POST',
@ -99,7 +99,9 @@ async function uploadFileToOssViaProxy(fileToProcess, fileName) {
// 使用统一配置获取代理地址
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getOssUploadUrl()
: (window.PBX_PROXY_BASE_URL || '/api') + '/upload/oss';
: (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:3456/api/upload/oss'
: '/api/upload/oss');
const response = await fetch(proxyUrl, {
method: 'POST',
@ -131,7 +133,7 @@ async function getMistralSignedUrl(fileId, mistralKey) {
// 使用统一配置获取代理地址
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getMistralUrl(`/v1/files/${fileId}/url?expiry=24`)
: (window.PBX_PROXY_BASE_URL || '/api') + `/mistral/v1/files/${fileId}/url?expiry=24`;
: `http://localhost:3456/api/mistral/v1/files/${fileId}/url?expiry=24`;
const response = await fetch(proxyUrl, {
method: 'GET',
@ -160,7 +162,7 @@ async function callMistralOcr(signedUrl, mistralKey) {
// 使用统一配置获取代理地址
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getMistralUrl('/v1/ocr')
: (window.PBX_PROXY_BASE_URL || '/api') + '/mistral/v1/ocr';
: 'http://localhost:3456/api/mistral/v1/ocr';
const response = await fetch(proxyUrl, {
method: 'POST',
@ -200,7 +202,7 @@ async function deleteMistralFile(fileId, apiKey) {
// 使用统一配置获取代理地址
const deleteUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getMistralUrl(`/v1/files/${fileId}`)
: (window.PBX_PROXY_BASE_URL || '/api') + `/mistral/v1/files/${fileId}`;
: `http://localhost:3456/api/mistral/v1/files/${fileId}`;
try {
const response = await fetch(deleteUrl, {

View File

@ -1758,10 +1758,10 @@ async function handleProcessClick() {
// 2. 检查 OCR 配置(如果有 PDF 文件)
if (hasPdfFiles) {
let ocrEngine = 'mineru';
let ocrEngine = 'mistral';
try {
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
ocrEngine = window.ocrSettingsManager.getCurrentConfig().engine || 'mineru';
ocrEngine = window.ocrSettingsManager.getCurrentConfig().engine || 'mistral';
const validation = window.ocrSettingsManager.validateConfig();
if (!validation.valid) {
const engineNames = { mistral: 'Mistral OCR', mineru: 'MinerU', doc2x: 'Doc2X', none: '不需要 OCR' };
@ -2241,19 +2241,9 @@ async function handleReadClick() {
batchTotal: null,
batchTemplate: null,
batchFormats: null,
batchStartedAt: null,
// 对于 PDF 文件,将 base64 也存入 metadata.originalPdfBase64
// 以便历史详情页面能够正确显示PDF
metadata: fileType === 'pdf' ? { originalPdfBase64: base64Content } : null
batchStartedAt: null
};
console.log('[仅阅读] 保存记录:', {
id: recordId,
fileType,
hasMetadata: !!record.metadata,
hasOriginalPdfBase64: !!(record.metadata && record.metadata.originalPdfBase64)
});
// 保存到数据库
if (typeof window.storageAdapter !== 'undefined' && typeof window.storageAdapter.saveResultToDB === 'function') {
await window.storageAdapter.saveResultToDB(record);
@ -2264,11 +2254,6 @@ async function handleReadClick() {
return;
}
// 添加短暂延迟确保 IndexedDB 事务完全提交
await new Promise(resolve => setTimeout(resolve, 100));
console.log('[仅阅读] 数据已保存,准备跳转...');
// 跳转到历史详情页面
window.location.href = `views/history/history_detail.html?id=${encodeURIComponent(recordId)}`;
} catch (error) {

133
js/boot/backend-gate.js Normal file
View File

@ -0,0 +1,133 @@
// backend-gate.js — 在“后端模式”未登录时,拦截并跳转到登录页
// 适用范围:主站/落地页/其他静态页面(不影响 /admin 管理台)
(function () {
try {
var loc = window.location;
var path = loc.pathname || '';
// 纯本地文件访问file://)时,强制视为前端模式:不做任何后端探测或跳转
if (loc.protocol === 'file:') return;
// 排除管理台与登录页自身
if (path.startsWith('/admin') || path.endsWith('/login.html')) return;
function q(key) {
try { return new URLSearchParams(loc.search).get(key); } catch { return null; }
}
function normalizeRedirectParam(urlObj) {
try {
var red = urlObj.searchParams.get('redirect');
if (!red) return;
function deepDecode(s, limit) {
var i = 0, prev = s;
while (i++ < (limit || 8)) {
try {
var next = decodeURIComponent(prev);
if (next === prev) break;
prev = next;
} catch { break; }
}
return prev;
}
var decoded = deepDecode(red, 8);
// 连续嵌套或超长,直接归一为首页
if (red.length > 512 || decoded.length > 512 || decoded.indexOf('/login.html?redirect=') !== -1) {
urlObj.searchParams.set('redirect', '/');
return;
}
var target = null;
try { target = new URL(decoded, urlObj.origin); } catch {}
if (!target || target.origin !== urlObj.origin || target.pathname.endsWith('/login.html')) {
urlObj.searchParams.set('redirect', '/');
}
} catch {}
}
function modeForced() {
var m = (q('mode') || '').toLowerCase();
if (m === 'backend') return true;
var env = (window.ENV_DEPLOYMENT_MODE || '').toLowerCase();
return env === 'backend';
}
function modeFrontendForced() {
var m = (q('mode') || '').toLowerCase();
return m === 'frontend';
}
function hasToken() {
try { return !!localStorage.getItem('auth_token'); } catch { return false; }
}
function buildSafeRedirectTarget() {
try {
var u = new URL(window.location.href);
// 避免递归嵌套:去除已有的 redirect 参数
u.searchParams.delete('redirect');
// 若当前已是登录页,则回首页
if (u.pathname.endsWith('/login.html')) {
u.pathname = '/';
u.search = '';
}
// 限制同源
if (u.origin !== window.location.origin) return '/';
return u.toString();
} catch { return '/'; }
}
function redirectToLogin() {
// 如果当前 URL 已包含 redirect 指向 login.html异常嵌套则强制回首页
try {
var current = new URL(window.location.href);
normalizeRedirectParam(current);
// 将标准化后的当前 URL 写回,避免下次读取到异常 redirect
history.replaceState(null, '', current.toString());
} catch {}
var safe = encodeURIComponent(buildSafeRedirectTarget());
window.location.replace('/login.html?redirect=' + safe);
}
function gateIfBackendKnown() {
// 若明确为后端模式,且未登录且未显式前端绕过 → 跳转登录
if (modeFrontendForced()) return; // 允许 ?mode=frontend 绕过
if (hasToken()) return;
redirectToLogin();
}
async function healthCheck(timeoutMs) {
try {
var ctrl = new AbortController();
var id = setTimeout(function(){ try{ctrl.abort();}catch{} }, timeoutMs);
var base = window.ENV_API_BASE_URL || '/api';
var res = await fetch(base + '/health', { signal: ctrl.signal, cache: 'no-store' });
clearTimeout(id);
return !!(res && res.ok);
} catch { return false; }
}
// 情况 1已强制后端 → 立即门禁
if (modeForced()) {
gateIfBackendKnown();
return;
}
// 情况 2依赖 storage-adapter 的自动切换事件(若其已加载)
if (typeof window !== 'undefined') {
window.addEventListener('pb:storage-mode-changed', function (evt) {
if (evt && evt.detail && evt.detail.mode === 'backend') gateIfBackendKnown();
});
}
// 情况 3页面未加载 storage-adapter例如落地页→ 自行做一次短健康检查
// 仅当未显式前端绕过时执行
if (!modeFrontendForced()) {
healthCheck(700).then(function (hasBackend) {
if (hasBackend) gateIfBackendKnown();
});
}
} catch (e) {
// 忽略所有门禁过程中的异常,避免影响前端模式体验
}
})();

View File

@ -162,7 +162,7 @@
const proxyBaseUrl = options.proxyBaseUrl ||
(typeof window !== 'undefined' && window.ProxyConfig ? window.ProxyConfig.getProxyUrl() : null) ||
(typeof window !== 'undefined' && window.PBX_PROXY_BASE_URL ? window.PBX_PROXY_BASE_URL : null) ||
'/api';
'http://localhost:3456';
const provider = options.provider || 'openai';
// 获取当前选择的模型ID如果有模型检测模块

View File

@ -140,7 +140,7 @@
const useProxy = globalProxyMode === 'proxy';
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: (window.PBX_PROXY_BASE_URL || '/api');
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
// 确定提供商(从模型 ID 或配置推断)
let provider = 'openai';

View File

@ -447,7 +447,7 @@ async function sendChatbotMessage(userInput, updateChatbotUI, externalConfig = n
const useProxy = globalProxyMode === 'proxy';
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: (window.PBX_PROXY_BASE_URL || '/api');
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
// 在使用代理服务器模式时API Key 在后端配置,前端可以没有 API Key
// 否则,检查前端是否配置了有效的 API Key
@ -1740,7 +1740,7 @@ async function singleChunkSummary(sysPrompt, userInput, config, apiKey) {
const useProxy = globalProxyMode === 'proxy';
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: (window.PBX_PROXY_BASE_URL || '/api');
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
// 只做单轮整理,不带历史
let apiConfig;

View File

@ -276,9 +276,12 @@ window.ChatbotMessageRenderer = {
const isPurelyEmpty = (!m.content || String(m.content).trim() === '') && !m.reasoningContent && !m.toolCallHtml;
if (m.role === 'assistant' && isPurelyEmpty) {
// Determine the correct path for the logo based on the current page
const isHistoryDetail = window.location.pathname.includes('/history_detail.html');
const logoPath = isHistoryDetail ? '../../public/pure.svg' : 'public/pure.svg';
renderedContent = `
<div class="typing-indicator">
<iconify-icon icon="carbon:ai" class="typing-icon" width="24"></iconify-icon>
<img src="${logoPath}" class="typing-logo" alt="Thinking..." />
</div>
`;
} else {
@ -522,11 +525,15 @@ window.ChatbotMessageRenderer = {
* @returns {string} HTML字符串
*/
renderTypingIndicator: function() {
// Determine the correct path for the logo based on the current page
const isHistoryDetail = window.location.pathname.includes('/history_detail.html');
const logoPath = isHistoryDetail ? '../../public/pure.svg' : 'public/pure.svg';
return `
<div class="message-container assistant-message-container">
<div class="chat-bubble assistant typing-bubble">
<div class="typing-indicator">
<iconify-icon icon="carbon:ai" class="typing-icon" width="24"></iconify-icon>
<img src="${logoPath}" class="typing-logo" alt="Thinking..." />
</div>
</div>
</div>

View File

@ -20,7 +20,7 @@ window.showHistoryDetail = function(id) {
} catch (e) {
console.warn('[showHistoryDetail] Failed to save doc id to localStorage', e);
}
window.location.href = 'views/history/history_detail.html?id=' + encodeURIComponent(id);
window.open('views/history/history_detail.html?id=' + encodeURIComponent(id), '_blank');
};
// =====================
@ -2195,8 +2195,8 @@ document.addEventListener('DOMContentLoaded', function() {
};
/**
* (全局可调用) 当前页面显示指定历史记录的详细信息
* 它通过构建一个指向 `views/history/history_detail.html` URL (包含记录 ID 作为查询参数) 实现
* (全局可调用) 新的浏览器标签页或窗口中显示指定历史记录的详细信息
* 它通过构建一个指向 `views/history/history_detail.html` URL (包含记录 ID 作为查询参数) 并使用 `window.open` 实现
*
* @param {string} id - 要查看详情的历史记录的唯一 ID
*/
@ -2206,7 +2206,7 @@ document.addEventListener('DOMContentLoaded', function() {
} catch (e) {
console.warn('[showHistoryDetail] Failed to save doc id to localStorage', e);
}
window.location.href = 'views/history/history_detail.html?id=' + encodeURIComponent(id);
window.open('views/history/history_detail.html?id=' + encodeURIComponent(id), '_blank');
};
/**

View File

@ -466,11 +466,15 @@ async function triggerReprocess(includeTranslation) {
return;
}
// 显示处理中 Toast
showToast('正在准备文档...', 'info');
try {
// 将 Base64 转为 File 对象
const file = base64ToFile(pdfBase64, docName);
// 执行 OCR
showToast('正在进行 OCR 识别...', 'info');
const ocrResult = await performOcr(file, (current, total, msg) => {
showToast(`${msg || 'OCR 处理中'} (${current}/${total})`, 'info', 5000);
@ -489,6 +493,7 @@ async function triggerReprocess(includeTranslation) {
// 如果需要翻译,执行翻译
if (includeTranslation) {
showToast('正在进行翻译...', 'info');
try {
const translationResult = await performTranslation(ocrResult.markdown, (current, total, msg) => {
@ -510,6 +515,7 @@ async function triggerReprocess(includeTranslation) {
}
// 保存到 IndexedDB
showToast('正在保存...', 'info');
await saveResultToDB(window.data);
// 刷新页面显示
@ -704,6 +710,7 @@ async function triggerReprocessWithMinerU() {
const savedMineruMode = localStorage.getItem('mineruMode');
try {
showToast('正在使用 MinerU 处理文档...', 'info');
// 临时设置 OCR 配置为 MinerU + 结构化翻译模式
localStorage.setItem('ocrEngine', 'mineru');
@ -729,7 +736,7 @@ async function triggerReprocessWithMinerU() {
pdfFile,
null, // mistralKeyObject - 使用 MinerU 不需要
null, // translationKeyObject - 使用后端代理不需要
'aliyun', // 使用阿里云百炼(后端代理会处理)
'tongyi', // 使用通义模型,后端代理会处理
null, // translationModelConfig
settings.maxTokensPerChunk || 2000,
settings.targetLanguage || 'Chinese',
@ -766,21 +773,16 @@ async function triggerReprocessWithMinerU() {
await saveResultToDB(window.data);
}
showToast('处理完成!正在加载 PDF 对照视图...', 'success');
// 重置渲染锁
if (typeof renderingTab !== 'undefined') renderingTab = null;
showToast('处理完成!', 'success');
// 刷新页面显示
if (typeof renderDetail === 'function') {
await renderDetail();
renderDetail();
}
// 自动跳转到 PDF 对照视图
if (typeof showTabImmediate === 'function') {
showTabImmediate('pdf-compare');
} else if (typeof showTab === 'function') {
showTab('pdf-compare');
// 自动跳转到原始文件标签页
if (typeof showTab === 'function') {
showTab('original-file');
}
} catch (error) {
@ -808,7 +810,7 @@ async function executeMinerUStructuredTranslation() {
// 使用统一配置获取代理地址
const PROXY_BASE = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: (window.PBX_PROXY_BASE_URL || '/api');
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
// 获取翻译配置
const settings = typeof loadSettings === 'function' ? loadSettings() : {};
@ -981,17 +983,9 @@ async function executeMinerUStructuredTranslation() {
addLog(`注意: 有 ${failedItems.length} 个片段翻译失败`);
}
addLog('正在刷新界面...');
addLog('正在加载 PDF 对照视图...');
// 重置渲染锁,确保后续 showTab 可以执行
if (typeof renderingTab !== 'undefined') renderingTab = null;
// 刷新界面
if (typeof renderDetail === 'function') {
await renderDetail(); // await async 函数
}
// 延迟后显示 PDF 对照视图
// 短暂延迟后显示 PDF 对照视图
setTimeout(() => {
if (typeof showTabImmediate === 'function') {
showTabImmediate('pdf-compare');

View File

@ -150,10 +150,6 @@ function showTabImmediate(tab) {
if (DOM_CACHE.layout.meta) DOM_CACHE.layout.meta.style.display = '';
if (DOM_CACHE.layout.tabsContainer) DOM_CACHE.layout.tabsContainer.style.display = '';
// 恢复 AI 智能助手显示(退出 PDF 对照模式时)
const chatbotArea = document.getElementById('immersive-chatbot-area');
if (chatbotArea) chatbotArea.style.display = '';
let html = '';
let contentContainerId = ''; // 用于 applyAnnotationsToContent
let activeContentElement = null; // 用于 applyAnnotationsToContent
@ -299,6 +295,11 @@ function showTabImmediate(tab) {
// ========== MinerU PDF 对照视图 ==========
if (DOM_CACHE.tabs.pdfCompare) DOM_CACHE.tabs.pdfCompare.classList.add('active');
// 隐藏顶部区域以获得更大空间 - 使用缓存
if (DOM_CACHE.layout.title) DOM_CACHE.layout.title.style.display = 'none';
if (DOM_CACHE.layout.meta) DOM_CACHE.layout.meta.style.display = 'none';
if (DOM_CACHE.layout.tabsContainer) DOM_CACHE.layout.tabsContainer.style.display = 'none';
// 检查是否有必要的结构化翻译数据
const hasStructuredData = data.metadata && data.metadata.originalPdfBase64 && data.metadata.contentListJson && data.metadata.translatedContentList;
@ -310,13 +311,6 @@ function showTabImmediate(tab) {
return;
}
// 有结构化数据时,隐藏顶部区域和 AI 智能助手以获得更大空间
if (DOM_CACHE.layout.title) DOM_CACHE.layout.title.style.display = 'none';
if (DOM_CACHE.layout.meta) DOM_CACHE.layout.meta.style.display = 'none';
if (DOM_CACHE.layout.tabsContainer) DOM_CACHE.layout.tabsContainer.style.display = 'none';
const chatbotArea = document.getElementById('immersive-chatbot-area');
if (chatbotArea) chatbotArea.style.display = 'none';
// 设置 HTML 容器
document.getElementById('tabContent').innerHTML = '<div id="pdf-compare-container"></div>';

View File

@ -137,13 +137,13 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
let usedOcrEngine = null;
let usedOcrSource = null;
// 更合理的开始日志:显示 OCR 引擎而不是固定显示 Mistral Key
let ocrEngineForLog = 'mineru';
let ocrEngineForLog = 'mistral';
try {
if (typeof window !== 'undefined' && window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
const cfg = window.ocrSettingsManager.getCurrentConfig();
if (cfg && cfg.engine) ocrEngineForLog = cfg.engine;
} else {
ocrEngineForLog = localStorage.getItem('ocrEngine') || 'mineru';
ocrEngineForLog = localStorage.getItem('ocrEngine') || 'mistral';
}
} catch {}
if (typeof addProgressLog === "function") {
@ -800,7 +800,7 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
provider: selectedTranslationModelName,
proxyBase: (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: (window.PBX_PROXY_BASE_URL || '/api'),
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456'),
// 允许从设置自定义重试,若无则用默认
maxRetries: (typeof loadSettings === 'function' ? (loadSettings().structuredMaxRetries || undefined) : undefined),
retryDelay: (typeof loadSettings === 'function' ? (loadSettings().structuredRetryDelayMs || undefined) : undefined)

View File

@ -717,7 +717,7 @@ ${jsonContent}
const proxyBase = options.proxyBase ||
(typeof window !== 'undefined' && window.ProxyConfig ? window.ProxyConfig.getProxyUrl() : null) ||
(typeof window !== 'undefined' && window.PBX_PROXY_BASE_URL ? window.PBX_PROXY_BASE_URL : null) ||
'/api';
'http://localhost:3456';
const provider = options.provider || 'aliyun';
// 后端代理端点映射
@ -836,7 +836,7 @@ ${jsonContent}
// 前端发出的请求源头
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: (window.PBX_PROXY_BASE_URL || '/api');
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
const predefinedConfigs = {
'proxy': {
endpoint: `${proxyUrl}/api/llm/aliyun/v1/chat/completions`,

View File

@ -12,13 +12,13 @@ class MistralOcrAdapter extends OcrAdapter {
this.currentKeyIndex = 0;
// 使用统一配置获取代理地址
// 优先级config.baseUrl > window.ProxyConfig > PBX_PROXY_BASE_URL > 默认相对路径
// 优先级config.baseUrl > window.ProxyConfig > 默认值
if (config.baseUrl && config.baseUrl !== 'https://api.mistral.ai') {
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
} else if (typeof window !== 'undefined' && window.ProxyConfig) {
this.baseUrl = window.ProxyConfig.getMistralUrl();
} else {
this.baseUrl = (window.PBX_PROXY_BASE_URL || '/api') + '/mistral';
this.baseUrl = 'http://localhost:3456/api/mistral';
}
}

View File

@ -44,7 +44,7 @@ class OcrManager {
}
// Fallback: 直接从 localStorage 读取
const engine = localStorage.getItem('ocrEngine') || 'mineru';
const engine = localStorage.getItem('ocrEngine') || 'mistral';
switch (engine) {
case 'local':
@ -97,7 +97,7 @@ class OcrManager {
// 使用统一配置获取默认 workerUrl
const mineruDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: (window.PBX_PROXY_BASE_URL || '/api');
: 'http://localhost:3456';
return {
engine: 'mineru',
token: localStorage.getItem('ocrMinerUToken') || '',
@ -113,7 +113,7 @@ class OcrManager {
// 使用统一配置获取默认 workerUrl
const doc2xDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: (window.PBX_PROXY_BASE_URL || '/api');
: 'http://localhost:3456';
return {
engine: 'doc2x',
token: localStorage.getItem('ocrDoc2XToken') || '',

View File

@ -705,7 +705,7 @@ async function translateMarkdown(
// 所有翻译请求都指向后端代理,由后端决定使用哪个模型
endpoint: (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getLLMProxyUrl('tongyi', '/v1/chat/completions')
: ((window.PBX_PROXY_BASE_URL || '/api') + '/llm/tongyi/v1/chat/completions'),
: ((window.PBX_PROXY_BASE_URL || 'http://localhost:3456') + '/api/llm/tongyi/v1/chat/completions'),
modelName: '通义百炼',
headers: { 'Content-Type': 'application/json' },
bodyBuilder: (sys, user) => {

View File

@ -71,16 +71,6 @@ class AuthManager {
// ---------------- 后端存储实现 ----------------
class BackendStorage {
// 回退到本地存储的方法
_fallbackTo(method, ...args) {
const localMethod = window[method];
if (typeof localMethod === 'function') {
return localMethod.apply(null, args);
}
console.warn(`[BackendStorage] No local fallback for ${method}`);
return Promise.resolve();
}
async fetchAPI(endpoint, options = {}) {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
@ -92,8 +82,9 @@ class BackendStorage {
if (!response.ok) {
if (response.status === 401) {
// Token 过期,清除token
// Token 过期,需要重新登录
AuthManager.removeToken();
window.location.href = '/login.html';
}
throw new Error(`API Error: ${response.status}`);
}
@ -104,148 +95,112 @@ class BackendStorage {
// 用户设置
async loadSettings() {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadSettings');
}
if (!AuthManager.isAuthenticated()) return this._getDefaultSettings();
const data = await this.fetchAPI('/user/settings');
return data;
} catch (error) {
console.error('Failed to load settings from backend:', error);
return this._fallbackTo('loadSettings');
return this._getDefaultSettings();
}
}
async saveSettings(settings) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveSettings', settings);
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI('/user/settings', {
method: 'PUT',
body: JSON.stringify(settings)
});
} catch (error) {
console.error('Failed to save settings to backend:', error);
return this._fallbackTo('saveSettings', settings);
throw error;
}
}
// API Keys
async loadModelKeys(provider) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadModelKeys', provider);
}
if (!AuthManager.isAuthenticated()) return [];
const keys = await this.fetchAPI(`/user/api-keys?provider=${provider}`);
return keys;
} catch (error) {
console.error('Failed to load API keys:', error);
return this._fallbackTo('loadModelKeys', provider);
return [];
}
}
async saveModelKeys(provider, keys) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveModelKeys', provider, keys);
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI('/user/api-keys', {
method: 'POST',
body: JSON.stringify({ provider, keys })
});
} catch (error) {
console.error('Failed to save API keys:', error);
return this._fallbackTo('saveModelKeys', provider, keys);
throw error;
}
}
// 文档历史
async saveResultToDB(document) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveResultToDB', document);
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI('/documents', { method: 'POST', body: JSON.stringify(document) });
} catch (error) {
console.error('Failed to save document to backend, falling back to local:', error);
return this._fallbackTo('saveResultToDB', document);
console.error('Failed to save document:', error);
throw error;
}
}
async getAllResultsFromDB() {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('getAllResultsFromDB');
}
if (!AuthManager.isAuthenticated()) return [];
const data = await this.fetchAPI('/documents');
return data.documents || [];
} catch (error) {
console.error('Failed to load documents from backend, falling back to local:', error);
return this._fallbackTo('getAllResultsFromDB');
console.error('Failed to load documents:', error);
return [];
}
}
async getResultFromDB(id) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('getResultFromDB', id);
}
if (!AuthManager.isAuthenticated()) return null;
return await this.fetchAPI(`/documents/${id}`);
} catch (error) {
console.error('Failed to load document from backend, falling back to local:', error);
return this._fallbackTo('getResultFromDB', id);
console.error('Failed to load document:', error);
return null;
}
}
async deleteResultFromDB(id) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('deleteResultFromDB', id);
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI(`/documents/${id}`, { method: 'DELETE' });
} catch (error) {
console.error('Failed to delete document from backend, falling back to local:', error);
return this._fallbackTo('deleteResultFromDB', id);
}
}
async clearAllResultsFromDB() {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('clearAllResultsFromDB');
}
// 后端没有批量删除接口,逐个删除
const docs = await this.getAllResultsFromDB();
for (const doc of docs) {
await this.fetchAPI(`/documents/${doc.id}`, { method: 'DELETE' });
}
} catch (error) {
console.error('Failed to clear all documents from backend, falling back to local:', error);
return this._fallbackTo('clearAllResultsFromDB');
console.error('Failed to delete document:', error);
throw error;
}
}
// 术语库
async loadGlossarySets() {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadGlossarySets');
}
if (!AuthManager.isAuthenticated()) return {};
const glossaries = await this.fetchAPI('/user/glossaries');
const sets = {};
glossaries.forEach(g => { sets[g.id] = g; });
return sets;
} catch (error) {
console.error('Failed to load glossaries:', error);
return this._fallbackTo('loadGlossarySets');
return {};
}
}
async saveGlossarySets(sets) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveGlossarySets', sets);
}
if (!AuthManager.isAuthenticated()) return;
// 批量保存(简化实现)
for (const [id, set] of Object.entries(sets)) {
if (set._isNew) {
@ -256,42 +211,35 @@ class BackendStorage {
}
} catch (error) {
console.error('Failed to save glossaries:', error);
return this._fallbackTo('saveGlossarySets', sets);
throw error;
}
}
// 标注
async saveAnnotationToDB(annotation) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveAnnotationToDB', annotation);
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI(`/documents/${annotation.documentId}/annotations`, { method: 'POST', body: JSON.stringify(annotation) });
} catch (error) {
console.error('Failed to save annotation:', error);
return this._fallbackTo('saveAnnotationToDB', annotation);
throw error;
}
}
async getAnnotationsForDocFromDB(docId) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('getAnnotationsForDocFromDB', docId);
}
if (!AuthManager.isAuthenticated()) return [];
return await this.fetchAPI(`/documents/${docId}/annotations`);
} catch (error) {
console.error('Failed to load annotations:', error);
return this._fallbackTo('getAnnotationsForDocFromDB', docId);
return [];
}
}
// 聊天历史
async loadChatHistory(docId) {
try {
if (!AuthManager.isAuthenticated()) {
// 聊天历史在本地存储中没有对应方法,返回空数组
return [];
}
if (!AuthManager.isAuthenticated()) return [];
const data = await this.fetchAPI(`/chat/${docId}/history`);
return data.messages || [];
} catch (error) {
@ -302,95 +250,82 @@ class BackendStorage {
async saveChatMessage(docId, message) {
try {
if (!AuthManager.isAuthenticated()) {
// 聊天历史在本地存储中没有对应方法
return;
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI(`/chat/${docId}/history`, {
method: 'POST',
body: JSON.stringify(message)
});
} catch (error) {
console.error('Failed to save chat message:', error);
throw error;
}
}
async clearChatHistory(docId) {
try {
if (!AuthManager.isAuthenticated()) {
return;
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI(`/chat/${docId}/history`, { method: 'DELETE' });
} catch (error) {
console.error('Failed to clear chat history:', error);
throw error;
}
}
// 文献引用
async loadReferences(docId) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadReferences', docId) || [];
}
if (!AuthManager.isAuthenticated()) return [];
return await this.fetchAPI(`/references/${docId}/references`);
} catch (error) {
console.error('Failed to load references:', error);
return this._fallbackTo('loadReferences', docId) || [];
return [];
}
}
async saveReference(docId, reference) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveReference', docId, reference);
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI(`/references/${docId}/references`, {
method: 'POST',
body: JSON.stringify(reference)
});
} catch (error) {
console.error('Failed to save reference:', error);
return this._fallbackTo('saveReference', docId, reference);
throw error;
}
}
async deleteReference(docId, refId) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('deleteReference', docId, refId);
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI(`/references/${docId}/references/${refId}`, { method: 'DELETE' });
} catch (error) {
console.error('Failed to delete reference:', error);
return this._fallbackTo('deleteReference', docId, refId);
throw error;
}
}
// Prompt Pool
async loadPromptPool() {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadPromptPool');
}
if (!AuthManager.isAuthenticated()) return { prompts: [], healthConfig: null };
return await this.fetchAPI('/prompt-pool');
} catch (error) {
console.error('Failed to load prompt pool:', error);
return this._fallbackTo('loadPromptPool');
return { prompts: [], healthConfig: null };
}
}
async savePromptPool(data) {
try {
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('savePromptPool', data);
}
if (!AuthManager.isAuthenticated()) return;
await this.fetchAPI('/prompt-pool', {
method: 'PUT',
body: JSON.stringify(data)
});
} catch (error) {
console.error('Failed to save prompt pool:', error);
return this._fallbackTo('savePromptPool', data);
throw error;
}
}
@ -473,16 +408,26 @@ class StorageAdapterFactory {
// ---------------- 初始化与自动切换 ----------------
function printBanner() {
const logoStyle = 'font-size: 16px; font-weight: bold; color: #3b82f6;';
const infoStyle = 'font-size: 14px; color: #10b981;';
const modeStyle = 'font-size: 14px; font-weight: bold; color: #f59e0b;';
const borderStyle = 'color: #6366f1;';
const linkStyle = 'font-size: 13px; color: #06b6d4; text-decoration: underline;';
const logo = `
____ ____ __ __
| _ \\ __ _ _ __ ___ _ __ | __ ) _ _ _ __ _ __ ___ _ __ \\ \\/ /
| |_) / _\` | '_ \\ / _ \\ '__| | _ \\| | | | '__| '_ \\ / _ \\ '__| \\ /
| __/ (_| | |_) | __/ | | |_) | |_| | | | | | | __/ | / \\
|_| \\__,_| .__/ \\___|_| |____/ \\__,_|_| |_| |_|\\___|_| /_/\\_\\
|_|
`;
const mode = DEPLOYMENT_MODE === 'backend' ? '后端模式 (Backend Mode)' : '前端模式 (Frontend Mode)';
const storage = DEPLOYMENT_MODE === 'backend' ? 'Backend API + PostgreSQL' : 'localStorage + IndexedDB';
const auth = DEPLOYMENT_MODE === 'backend' ? 'JWT Authentication' : 'No Authentication';
console.log('%c' + logo, logoStyle);
console.log('%c╔════════════════════════════════════════════════════════════╗', borderStyle);
console.log('%c║ 系统信息 / System Info ║', borderStyle);
console.log('%c╠════════════════════════════════════════════════════════════╣', borderStyle);

View File

@ -711,7 +711,7 @@ KeyManagerUI.exportAllModelData = function() {
// 添加 OCR 配置导出
const ocrConfig = {
engine: localStorage.getItem('ocrEngine') || 'mineru', // 默认为 mineru
engine: localStorage.getItem('ocrEngine') || 'mistral',
mistralKeys: localStorage.getItem('ocrMistralKeys') || '',
workerAuthKey: localStorage.getItem('ocrWorkerAuthKey') || '',
mineruToken: localStorage.getItem('ocrMinerUToken') || '',

View File

@ -106,7 +106,7 @@ class OcrSettingsManager {
loadSettings() {
try {
// 引擎选择
const engine = localStorage.getItem(this.keys.engine) || 'mineru';
const engine = localStorage.getItem(this.keys.engine) || 'mistral';
if (this.elements.ocrEngine) {
this.elements.ocrEngine.value = engine;
this.switchEngine(engine); // 显示对应的配置面板
@ -125,8 +125,7 @@ class OcrSettingsManager {
this.elements.mineruToken.value = localStorage.getItem(this.keys.mineruToken) || '';
}
if (this.elements.mineruWorkerUrl) {
const defaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
this.elements.mineruWorkerUrl.value = localStorage.getItem(this.keys.mineruWorkerUrl) || defaultUrl;
this.elements.mineruWorkerUrl.value = localStorage.getItem(this.keys.mineruWorkerUrl) || '';
}
if (this.elements.mineruEnableOcr) {
this.elements.mineruEnableOcr.checked = localStorage.getItem(this.keys.mineruEnableOcr) !== 'false';
@ -152,8 +151,7 @@ class OcrSettingsManager {
this.elements.doc2xToken.value = localStorage.getItem(this.keys.doc2xToken) || '';
}
if (this.elements.doc2xWorkerUrl) {
const defaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
this.elements.doc2xWorkerUrl.value = localStorage.getItem(this.keys.doc2xWorkerUrl) || defaultUrl;
this.elements.doc2xWorkerUrl.value = localStorage.getItem(this.keys.doc2xWorkerUrl) || '';
}
if (this.elements.doc2xFormulaMode) {
this.elements.doc2xFormulaMode.value = localStorage.getItem(this.keys.doc2xFormulaMode) || 'dollar';
@ -242,7 +240,7 @@ class OcrSettingsManager {
localStorage.setItem(this.keys.mineruWorkerUrl, (cfg.workerUrl || '').replace(/\/+$/, ''));
localStorage.setItem(this.keys.mineruToken, cfg.token || '');
localStorage.setItem(this.keys.workerAuthKey, cfg.authKey || '');
localStorage.setItem(this.keys.mineruTokenMode, cfg.tokenMode || 'backend'); // 默认后端转发模式
localStorage.setItem(this.keys.mineruTokenMode, cfg.tokenMode || 'frontend');
localStorage.setItem(this.keys.mineruEnableOcr, cfg.enableOcr !== false);
localStorage.setItem(this.keys.mineruEnableFormula, cfg.enableFormula !== false);
localStorage.setItem(this.keys.mineruEnableTable, cfg.enableTable !== false);
@ -251,7 +249,7 @@ class OcrSettingsManager {
localStorage.setItem(this.keys.doc2xWorkerUrl, (cfg.workerUrl || '').replace(/\/+$/, ''));
localStorage.setItem(this.keys.doc2xToken, cfg.token || '');
localStorage.setItem(this.keys.workerAuthKey, cfg.authKey || '');
localStorage.setItem(this.keys.doc2xTokenMode, cfg.tokenMode || 'backend'); // 默认后端转发模式
localStorage.setItem(this.keys.doc2xTokenMode, cfg.tokenMode || 'frontend');
localStorage.setItem(this.keys.doc2xFormulaMode, cfg.formulaMode || 'dollar');
localStorage.setItem(this.keys.doc2xExportFormat, cfg.exportFormat || '');
}
@ -476,7 +474,7 @@ class OcrSettingsManager {
* @returns {Object} 配置对象
*/
getCurrentConfig() {
const engine = localStorage.getItem(this.keys.engine) || 'mineru';
const engine = localStorage.getItem(this.keys.engine) || 'mistral';
switch (engine) {
case 'none':
@ -516,13 +514,12 @@ class OcrSettingsManager {
}
case 'mineru':
const mineruDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
return {
engine: 'mineru',
token: localStorage.getItem(this.keys.mineruToken) || '',
workerUrl: (localStorage.getItem(this.keys.mineruWorkerUrl) || mineruDefaultUrl).replace(/\/+$/, ''), // 去掉末尾斜杠
workerUrl: (localStorage.getItem(this.keys.mineruWorkerUrl) || '').replace(/\/+$/, ''), // 去掉末尾斜杠
authKey: localStorage.getItem(this.keys.workerAuthKey) || '',
tokenMode: localStorage.getItem(this.keys.mineruTokenMode) || 'backend', // 默认后端转发模式
tokenMode: localStorage.getItem(this.keys.mineruTokenMode) || 'frontend',
enableOcr: localStorage.getItem(this.keys.mineruEnableOcr) !== 'false',
enableFormula: localStorage.getItem(this.keys.mineruEnableFormula) !== 'false',
enableTable: localStorage.getItem(this.keys.mineruEnableTable) !== 'false',
@ -530,13 +527,12 @@ class OcrSettingsManager {
};
case 'doc2x':
const doc2xDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
return {
engine: 'doc2x',
token: localStorage.getItem(this.keys.doc2xToken) || '',
workerUrl: (localStorage.getItem(this.keys.doc2xWorkerUrl) || doc2xDefaultUrl).replace(/\/+$/, ''), // 去掉末尾斜杠
workerUrl: (localStorage.getItem(this.keys.doc2xWorkerUrl) || '').replace(/\/+$/, ''), // 去掉末尾斜杠
authKey: localStorage.getItem(this.keys.workerAuthKey) || '',
tokenMode: localStorage.getItem(this.keys.doc2xTokenMode) || 'backend', // 默认后端转发模式
tokenMode: localStorage.getItem(this.keys.doc2xTokenMode) || 'frontend',
formulaMode: localStorage.getItem(this.keys.doc2xFormulaMode) || 'dollar',
exportFormat: localStorage.getItem(this.keys.doc2xExportFormat) || ''
};
@ -567,17 +563,17 @@ class OcrSettingsManager {
break;
case 'mineru':
// 后端转发模式tokenMode 不是 'frontend')不需要前端配置 Token
// 前端透传模式需要 TokenWorker 配置模式不需要
if (config.tokenMode === 'frontend' && !config.token) {
return { valid: false, message: '请配置 MinerU Token前端透传模式' };
}
if (config.tokenMode === 'frontend' && !config.workerUrl) {
if (!config.workerUrl) {
return { valid: false, message: '请配置 MinerU Worker URL' };
}
break;
case 'doc2x':
// 后端转发模式tokenMode 不是 'frontend')不需要前端配置 Token
// 前端透传模式需要 TokenWorker 配置模式不需要
if (config.tokenMode === 'frontend' && !config.token) {
return { valid: false, message: '请配置 Doc2X Token前端透传模式' };
}

View File

@ -66,6 +66,10 @@
// Settings Link
originalSettingsLink: '#settings-link'
},
logos: {
full: '../../public/h_with_name.svg',
pure: '../../public/pure.svg'
}
};
@ -142,16 +146,18 @@
* @param {boolean} collapsed - 是否折叠
*/
function setSidebarCollapsed(collapsed) {
if (!elements.appSidebar) return;
if (!elements.appSidebar || !elements.sidebarLogo) return;
if (collapsed) {
elements.appSidebar.classList.add('collapsed');
elements.sidebarLogo.src = CONFIG.logos.pure;
// 切换图标为"打开"图标
if (elements.sidebarToggleIcon) {
elements.sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-open');
}
} else {
elements.appSidebar.classList.remove('collapsed');
elements.sidebarLogo.src = CONFIG.logos.full;
// 切换图标为"关闭"图标
if (elements.sidebarToggleIcon) {
elements.sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-close');

View File

@ -131,18 +131,18 @@
const hasPdfFiles = effectiveFiles.some(file => file.name.toLowerCase().endsWith('.pdf'));
// 检查 OCR 引擎与所需配置
let ocrEngine = 'mineru';
let ocrEngine = 'mistral';
let ocrConfigValid = true;
let ocrConfigMessage = '';
try {
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
ocrEngine = window.ocrSettingsManager.getCurrentConfig().engine || (localStorage.getItem('ocrEngine') || 'mineru');
ocrEngine = window.ocrSettingsManager.getCurrentConfig().engine || (localStorage.getItem('ocrEngine') || 'mistral');
// 使用 validateConfig 检查配置是否完整
const validation = window.ocrSettingsManager.validateConfig();
ocrConfigValid = validation.valid;
ocrConfigMessage = validation.message;
} else {
ocrEngine = localStorage.getItem('ocrEngine') || 'mineru';
ocrEngine = localStorage.getItem('ocrEngine') || 'mistral';
}
} catch {}

View File

@ -186,13 +186,13 @@
});
// 检查当前 OCR 引擎配置
let currentOcrEngine = 'mineru';
let currentOcrEngine = 'mistral';
let currentOcrConfigured = false;
try {
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
currentOcrEngine = window.ocrSettingsManager.getCurrentConfig().engine || (localStorage.getItem('ocrEngine') || 'mineru');
currentOcrEngine = window.ocrSettingsManager.getCurrentConfig().engine || (localStorage.getItem('ocrEngine') || 'mistral');
} else {
currentOcrEngine = localStorage.getItem('ocrEngine') || 'mineru';
currentOcrEngine = localStorage.getItem('ocrEngine') || 'mistral';
}
if (currentOcrEngine === 'none' || currentOcrEngine === 'local') {

View File

@ -60,10 +60,9 @@
*/
function renderMinerUConfig(container) {
// 从 localStorage 加载配置
const defaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
const workerUrl = localStorage.getItem('ocrMinerUWorkerUrl') || defaultUrl;
const workerUrl = localStorage.getItem('ocrMinerUWorkerUrl') || '';
const authKey = localStorage.getItem('ocrWorkerAuthKey') || '';
const tokenMode = localStorage.getItem('ocrMinerUTokenMode') || 'backend';
const tokenMode = localStorage.getItem('ocrMinerUTokenMode') || 'frontend';
const token = localStorage.getItem('ocrMinerUToken') || '';
const configDiv = document.createElement('div');
@ -337,10 +336,9 @@
*/
function renderDoc2XConfig(container) {
// 从 localStorage 加载配置
const defaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
const workerUrl = localStorage.getItem('ocrDoc2XWorkerUrl') || defaultUrl;
const workerUrl = localStorage.getItem('ocrDoc2XWorkerUrl') || '';
const authKey = localStorage.getItem('ocrWorkerAuthKey') || '';
const tokenMode = localStorage.getItem('ocrDoc2XTokenMode') || 'backend';
const tokenMode = localStorage.getItem('ocrDoc2XTokenMode') || 'frontend';
const token = localStorage.getItem('ocrDoc2XToken') || '';
const configDiv = document.createElement('div');

132
login.html Normal file
View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - Paper Burner X</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://gcore.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<script>
// 防止登录页 redirect 参数递归叠加/循环
(function () {
try {
var url = new URL(window.location.href);
var red = url.searchParams.get('redirect');
if (!red) return;
// 多层解码,防止 %25 级联编码导致的嵌套绕过
function deepDecode(s, limit) {
var i = 0, prev = s;
while (i++ < (limit || 8)) {
try {
var next = decodeURIComponent(prev);
if (next === prev) break;
prev = next;
} catch { break; }
}
return prev;
}
var decoded = deepDecode(red, 8);
// 规则:
// 1) 任意层包含 login.html?redirect= 视为递归 → 归一 '/'
// 2) 过长(>512视为异常 → 归一 '/'
var shouldClamp = (red.length > 512) || (decoded.length > 512) || (decoded.indexOf('/login.html?redirect=') !== -1);
if (shouldClamp) {
url.searchParams.set('redirect', '/');
history.replaceState(null, '', url.toString());
return;
}
// 仅允许同源,且目标不能是登录页自身
var target;
try { target = new URL(decoded, window.location.origin); } catch { target = null; }
if (!target || target.origin !== window.location.origin || target.pathname.endsWith('/login.html')) {
url.searchParams.set('redirect', '/');
history.replaceState(null, '', url.toString());
}
} catch {}
})();
</script>
<div class="bg-white p-8 rounded-lg shadow w-full max-w-md">
<h1 class="text-2xl font-bold mb-6 text-center">登录到 Paper Burner X</h1>
<form id="loginForm" class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">邮箱</label>
<input id="email" type="email" required class="mt-1 block w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="you@example.com" />
</div>
<div>
<label class="block text-sm font-medium text-gray-700">密码</label>
<input id="password" type="password" required class="mt-1 block w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500" placeholder="••••••••" />
</div>
<button type="submit" class="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700">登录</button>
<p id="error" class="text-red-600 text-sm hidden"></p>
</form>
<div class="text-sm text-gray-500 mt-4">
<p>默认管理员:<code>admin@paperburner.local / admin123456</code></p>
<p class="mt-1">管理员入口:<a href="/admin" class="text-blue-600 hover:underline">/admin</a></p>
</div>
</div>
<script>
const API_BASE = window.location.origin + '/api';
function getSafeRedirect() {
try {
const p = new URLSearchParams(window.location.search);
const raw = p.get('redirect');
if (!raw) return '/';
// 多层解码,长度限制
function deepDecode(s, limit) {
let i = 0, prev = s;
while (i++ < (limit || 8)) {
try {
const next = decodeURIComponent(prev);
if (next === prev) break;
prev = next;
} catch { break; }
}
return prev;
}
const decoded = deepDecode(raw, 8);
if (decoded.length > 512 || decoded.indexOf('/login.html?redirect=') !== -1) return '/';
const u = new URL(decoded, window.location.origin);
// 仅允许同源回跳,且不指向 login.html 防止循环
if (u.origin !== window.location.origin) return '/';
if (u.pathname.endsWith('/login.html')) return '/';
u.searchParams.delete('redirect');
return u.toString();
} catch { return '/'; }
}
function getRedirect() {
try {
const p = new URLSearchParams(window.location.search);
return p.get('redirect') || '/';
} catch { return '/'; }
}
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const email = document.getElementById('email').value.trim();
const password = document.getElementById('password').value;
const err = document.getElementById('error');
err.classList.add('hidden');
err.textContent = '';
try {
const res = await axios.post(`${API_BASE}/auth/login`, { email, password });
if (res.data && res.data.token) {
// 与前端适配器一致的存储键
localStorage.setItem('auth_token', res.data.token);
window.location.href = getSafeRedirect();
} else {
throw new Error('登录响应异常');
}
} catch (e2) {
err.textContent = e2?.response?.data?.error || e2.message || '登录失败';
err.classList.remove('hidden');
}
});
</script>
</body>
</html>

1
public/h_with_name.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -30,7 +30,7 @@ See https://github.com/adobe-type-tools/cmap-resources
<!-- This snippet is used in production (included from viewer.html) -->
<link rel="resource" type="application/l10n" href="locale/locale.properties">
<!-- 引入tailwindcss -->
<link href="https://registry.npmmirror.com/tailwindcss/2.2.19/files/dist/tailwind.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<script src="../build/pdf.js"></script>

1
public/pure.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

1
public/with_name.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -9,6 +9,9 @@ async function main() {
const admin = await request(app).get('/admin');
console.log('ADMIN', admin.status, admin.headers['content-type']);
const login = await request(app).get('/login.html');
console.log('LOGIN', login.status, login.headers['content-type']);
if (health.status !== 200) {
process.exitCode = 1;
}

View File

@ -344,6 +344,11 @@ app.use('/api/prompt-pool', promptPoolRoutes);
// ==================== 前端路由SPA ====================
// 登录页需显式返回 login.html避免被通配符 * 误回退到 index.html
app.get('/login.html', (req, res) => {
res.sendFile(join(rootPath, 'login.html'));
});
// 管理员面板
app.get('/admin*', (req, res) => {
res.sendFile(join(rootPath, 'admin/index.html'));

220
stop.sh
View File

@ -1,111 +1,111 @@
#!/bin/bash
# Paper Burner X - 停止脚本
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
BACKEND_PORT=3456
FRONTEND_PORT=8080
PID_DIR="$PROJECT_DIR/.pids"
echo -e "${BLUE}"
echo "======================================"
echo " Paper Burner X - 停止脚本"
echo "======================================"
echo -e "${NC}"
# 函数:根据端口查找并杀死进程
kill_port() {
local port=$1
local pids=$(lsof -t -i:$port 2>/dev/null)
if [ -n "$pids" ]; then
echo -e "${YELLOW}→ 终止端口 $port 上的进程...${NC}"
for pid in $pids; do
kill -15 $pid 2>/dev/null
if kill -0 $pid 2>/dev/null; then
sleep 1
kill -9 $pid 2>/dev/null
fi
echo -e " ${GREEN}已终止进程 $pid${NC}"
done
return 0
else
return 1
fi
}
# 函数:根据 PID 文件杀死进程
kill_pid_file() {
local pid_file=$1
local service_name=$2
if [ -f "$pid_file" ]; then
local pid=$(cat "$pid_file")
if [ -n "$pid" ] && kill -0 $pid 2>/dev/null; then
echo -e "${YELLOW}→ 终止 $service_name (PID: $pid)...${NC}"
kill -15 $pid 2>/dev/null
# 等待进程退出
local count=0
while kill -0 $pid 2>/dev/null && [ $count -lt 5 ]; do
sleep 0.5
count=$((count + 1))
done
# 如果进程还在,强制终止
if kill -0 $pid 2>/dev/null; then
kill -9 $pid 2>/dev/null
fi
echo -e " ${GREEN}已终止${NC}"
fi
rm -f "$pid_file"
fi
}
STOPPED_SOMETHING=false
# 1. 通过 PID 文件停止
echo -e "${YELLOW}[1/2] 通过 PID 文件停止服务...${NC}"
if [ -f "$PID_DIR/backend.pid" ]; then
kill_pid_file "$PID_DIR/backend.pid" "后端服务"
STOPPED_SOMETHING=true
fi
if [ -f "$PID_DIR/frontend.pid" ]; then
kill_pid_file "$PID_DIR/frontend.pid" "前端服务"
STOPPED_SOMETHING=true
fi
# 2. 通过端口确保彻底清理
echo -e "${YELLOW}[2/2] 确保端口已释放...${NC}"
if kill_port $BACKEND_PORT; then
STOPPED_SOMETHING=true
else
echo -e " ${GREEN}端口 $BACKEND_PORT 已空闲${NC}"
fi
if kill_port $FRONTEND_PORT; then
STOPPED_SOMETHING=true
else
echo -e " ${GREEN}端口 $FRONTEND_PORT 已空闲${NC}"
fi
echo ""
if [ "$STOPPED_SOMETHING" = true ]; then
echo -e "${GREEN}"
echo "======================================"
echo " 服务已停止"
echo "======================================"
echo -e "${NC}"
else
echo -e "${YELLOW}"
echo "======================================"
echo " 没有运行中的服务"
echo "======================================"
echo -e "${NC}"
#!/bin/bash
# Paper Burner X - 停止脚本
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
BACKEND_PORT=3456
FRONTEND_PORT=8080
PID_DIR="$PROJECT_DIR/.pids"
echo -e "${BLUE}"
echo "======================================"
echo " Paper Burner X - 停止脚本"
echo "======================================"
echo -e "${NC}"
# 函数:根据端口查找并杀死进程
kill_port() {
local port=$1
local pids=$(lsof -t -i:$port 2>/dev/null)
if [ -n "$pids" ]; then
echo -e "${YELLOW}→ 终止端口 $port 上的进程...${NC}"
for pid in $pids; do
kill -15 $pid 2>/dev/null
if kill -0 $pid 2>/dev/null; then
sleep 1
kill -9 $pid 2>/dev/null
fi
echo -e " ${GREEN}已终止进程 $pid${NC}"
done
return 0
else
return 1
fi
}
# 函数:根据 PID 文件杀死进程
kill_pid_file() {
local pid_file=$1
local service_name=$2
if [ -f "$pid_file" ]; then
local pid=$(cat "$pid_file")
if [ -n "$pid" ] && kill -0 $pid 2>/dev/null; then
echo -e "${YELLOW}→ 终止 $service_name (PID: $pid)...${NC}"
kill -15 $pid 2>/dev/null
# 等待进程退出
local count=0
while kill -0 $pid 2>/dev/null && [ $count -lt 5 ]; do
sleep 0.5
count=$((count + 1))
done
# 如果进程还在,强制终止
if kill -0 $pid 2>/dev/null; then
kill -9 $pid 2>/dev/null
fi
echo -e " ${GREEN}已终止${NC}"
fi
rm -f "$pid_file"
fi
}
STOPPED_SOMETHING=false
# 1. 通过 PID 文件停止
echo -e "${YELLOW}[1/2] 通过 PID 文件停止服务...${NC}"
if [ -f "$PID_DIR/backend.pid" ]; then
kill_pid_file "$PID_DIR/backend.pid" "后端服务"
STOPPED_SOMETHING=true
fi
if [ -f "$PID_DIR/frontend.pid" ]; then
kill_pid_file "$PID_DIR/frontend.pid" "前端服务"
STOPPED_SOMETHING=true
fi
# 2. 通过端口确保彻底清理
echo -e "${YELLOW}[2/2] 确保端口已释放...${NC}"
if kill_port $BACKEND_PORT; then
STOPPED_SOMETHING=true
else
echo -e " ${GREEN}端口 $BACKEND_PORT 已空闲${NC}"
fi
if kill_port $FRONTEND_PORT; then
STOPPED_SOMETHING=true
else
echo -e " ${GREEN}端口 $FRONTEND_PORT 已空闲${NC}"
fi
echo ""
if [ "$STOPPED_SOMETHING" = true ]; then
echo -e "${GREEN}"
echo "======================================"
echo " 服务已停止"
echo "======================================"
echo -e "${NC}"
else
echo -e "${YELLOW}"
echo "======================================"
echo " 没有运行中的服务"
echo "======================================"
echo -e "${NC}"
fi

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<title>配图编辑器 - draw.io</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg">
<style>
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');

View File

@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<title>历史详情</title>
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg" />
<!-- CDN 性能优化DNS 预连接 - 使用淘宝 npmmirror 镜像加速 -->
<link rel="dns-prefetch" href="https://registry.npmmirror.com" />
<link rel="preconnect" href="https://registry.npmmirror.com" crossorigin />
@ -104,8 +105,8 @@
</script>
</head>
<body class="immersive-pending">
<!-- Standard Immersive Mode Toggle Button 进入沉浸式布局-->
<button id="toggle-immersive-btn" class="tiny-round-btn disvisible hidden" style="display: none;" title="">
<!-- Standard Immersive Mode Toggle Button -->
<button id="toggle-immersive-btn" class="tiny-round-btn hidden" style="display: none;" title="进入沉浸式布局">
<i class="fas fa-expand-alt"></i>
<!-- Default icon for entering immersive mode -->
</button>
@ -189,6 +190,12 @@
<aside class="app-sidebar" id="appSidebar">
<!-- 侧边栏头部 -->
<div class="sidebar-header">
<!-- <img
src="../../public/h_with_name.svg"
alt="Paper Burner X"
class="sidebar-logo"
id="sidebarLogo"
/> -->
<!-- 占位元素 -->
<span></span>
<!-- 桌面端收起按钮 -->
@ -322,6 +329,11 @@
<!-- 移动端顶部栏 -->
<div class="mobile-header">
<div class="mobile-header-title">
<img
src="../../public/pure.svg"
alt="PBX"
class="mobile-header-logo"
/>
<span>文档详情</span>
</div>
<button class="mobile-menu-btn" id="mobileMenuBtn">
@ -817,8 +829,6 @@
<script src="../../js/storage/storage.js"></script>
<!-- 结构化重试需要 API/Translation/Structured 翻译模块 -->
<script src="../../js/api/api.js"></script>
<script src="../../js/process/utils.js"></script>
<script src="../../js/process/document.js"></script>
<script src="../../js/process/translation.js"></script>
<script src="../../js/process/mineru-structured-translation.js"></script>
@ -852,26 +862,22 @@
<script src="https://registry.npmmirror.com/html2canvas/1.4.1/files/dist/html2canvas.min.js"></script>
<script src="https://registry.npmmirror.com/jspdf/2.5.1/files/dist/jspdf.umd.min.js"></script>
<!-- PDF.js for structured translation PDF comparison (本地版本) -->
<script src="../../public/pdfjs/build/pdf.js"></script>
<!-- PDF.js for structured translation PDF comparison -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
if (typeof pdfjsLib !== "undefined") {
// 使用绝对路径确保 Worker 可以正确加载
const baseUrl = window.location.origin + window.location.pathname.split('/views/')[0];
pdfjsLib.GlobalWorkerOptions.workerSrc = baseUrl + "/public/pdfjs/build/pdf.worker.js";
pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
}
</script>
<!-- Phase 3.5: 性能优化配置(必须在所有 chatbot 模块之前加载) -->
<script src="../../js/chatbot/config/performance-config.js"></script>
<!-- 历史页 Chatbot 代理配置:使用阿里云百炼平台,保护 API Key -->
<script src="../../js/config/proxy-config.js"></script>
<script>
// 全局配置:启用代理模式,使用 ProxyConfig 自动检测环境
// 全局配置:启用代理模式,使用本地代理服务器
window.PBX_PROXY_MODE = "proxy";
// PBX_PROXY_BASE_URL 由 proxy-config.js 自动设置:
// - 本地开发: http://localhost:3456
// - 生产环境: /api (相对路径)
window.PBX_PROXY_BASE_URL = "http://localhost:3456";
// 使用阿里云百炼平台
window.PBX_LLM_PROVIDER = "aliyun";
// 默认模型qwen-plus推荐其他可选qwen-turbo、qwen-max、qwen-long-context
@ -883,10 +889,10 @@
window.PBX_LLM_MODEL,
);
</script>
<!-- 通过代理发送对话消息的函数 -->
<!-- 直接向 localhost:3456 发送对话消息的函数 -->
<script>
/**
* 通过代理服务器发送对话消息
* 直接向 http://localhost:3456 发送对话消息
* @param {string} userMessage - 用户消息内容
* @param {Array} [conversationHistory=[]] - 对话历史 [{role, content}, ...]
* @param {Object} [options={}] - 可选配置
@ -897,8 +903,7 @@
conversationHistory = [],
options = {},
) {
const baseUrl = window.PBX_PROXY_BASE_URL || '/api';
const endpoint = `${baseUrl}/v1/chat/completions`;
const endpoint = "http://localhost:3456/v1/chat/completions";
const model = options.model || window.PBX_LLM_MODEL || "qwen-plus";
const temperature = options.temperature || 0.7;
const maxTokens = options.maxTokens || 2048;
@ -956,8 +961,7 @@
onChunk,
options = {},
) {
const baseUrl = window.PBX_PROXY_BASE_URL || '/api';
const endpoint = `${baseUrl}/v1/chat/completions`;
const endpoint = "http://localhost:3456/v1/chat/completions";
const model = options.model || window.PBX_LLM_MODEL || "qwen-plus";
const temperature = options.temperature || 0.7;
const maxTokens = options.maxTokens || 2048;

View File

@ -5,11 +5,14 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Paper Burner X - 开源的、在浏览器中即开即用的 AI 工作站,专为扫除海量的 PDF 文献、复杂的公式和跨语言的障碍。支持前端 Agent 驱动的智能检索、高性能批量处理和完全本地化部署。">
<title>Paper Burner X - 开源 AI 文献工作站</title>
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg">
<!-- Tailwind CSS & 依赖库 -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://gcore.jsdelivr.net/npm/iconify-icon@2.0.0/dist/iconify-icon.min.js"></script>
<!-- GitHub Stars 统一获取模块 -->
<script src="../../js/utils/github-stars.js"></script>
<!-- 后端模式未登录门禁(在有后端部署时,直接要求登录) -->
<script src="/js/boot/backend-gate.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
<style>
/* 谷歌字体 Inter (更专业、现代) */
@ -556,7 +559,7 @@
<header id="mainHeader" class="glass-header">
<!-- Left: Logo -->
<div class="header-logo flex-shrink-0">
<span class="text-xl font-bold text-gray-800">Paper Burner X</span>
<img src="../../public/h_with_name.svg" alt="Paper Burner X">
</div>
<!-- Center: Update Pill (Optional, can be hidden if needed) -->
@ -591,8 +594,8 @@
<!-- Hero Section -->
<section class="hero-section pt-12 md:pt-16 min-h-[85vh] flex flex-col justify-center">
<div class="mb-6 md:mb-8">
<h2 class="text-3xl sm:text-4xl md:text-5xl font-bold text-gray-900 mx-auto">Paper Burner X</h2>
<div class="mb-6 md:mb-8 animate-float">
<img src="../../public/pure.svg" alt="Paper Burner X Logo" class="w-28 h-28 sm:w-36 sm:h-36 md:w-44 md:h-44 mx-auto drop-shadow-lg">
</div>
<!-- Hero Carousel Container -->

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<title>思维导图 - Markmap</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg">
<!-- Markmap 依赖 -->
<script src="https://gcore.jsdelivr.net/npm/d3@7"></script>