Compare commits

...

10 Commits

Author SHA1 Message Date
肖应宇 b019917c18 fix: 默认OCR引擎换成MinerU而不是mistral 2026-03-23 14:51:55 +08:00
肖应宇 5c1c66bd8f feat: 点击历史记录时,不会再打开新建标签页 2026-03-23 13:51:29 +08:00
肖应宇 51d1a4a1a2 fix: 修复引入pdfjs的位置 2026-03-23 13:47:06 +08:00
肖应宇 3028484949 fix: 生产环境使用相对路径由nginx代理到后端 2026-03-23 12:00:31 +08:00
肖应宇 eb2a09b82f feat: PDF对照视图移植进沉浸式布局,交互优化与多项修复
- PDF对照视图:只在有结构化翻译数据时隐藏AI助手,修复确认框弹出时tab栏被隐藏
- 修复翻译模型名称错误(tongyi -> aliyun)
- 仅阅读功能:添加延迟确保IndexedDB事务提交后再跳转
- triggerReprocessWithMinerU完成后自动跳转到PDF对照视图
- 重置renderingTab防抖锁解决界面卡住问题
- 永久隐藏沉浸模式切换按钮
- 删除未使用的logo SVG文件
2026-03-20 14:28:05 +08:00
肖应宇 823d6c15a7 feat: 默认使用IndexDB 2026-03-13 10:08:50 +08:00
肖应宇 29e11f8877 feat: 删除原系统中https的登陆逻辑 2026-03-13 09:51:05 +08:00
肖应宇 e9b9cc91ef build: 添加pdfjs的build文件夹,这是必须的。更换tailwindCDN为国内镜像 2026-03-12 17:10:30 +08:00
肖应宇 abebd5d62f fix: 修复仅阅读没有正确传递base64的问题 2026-03-12 16:57:56 +08:00
SuperManTouX bfe00cc98f feat: 删除了一些多余toast 2026-03-11 14:31:39 +08:00
41 changed files with 77027 additions and 2630 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

@ -494,6 +494,7 @@ 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;
@ -673,9 +674,9 @@ body.immersive-active #immersive-main-content-area .container {
flex-direction: column !important;
}
/* 沉浸模式下隐藏 PDF 对照按钮 */
/* 沉浸模式下显示 PDF 对照按钮(用户要求在沉浸模式中也能使用) */
body.immersive-active #tab-pdf-compare {
display: none !important;
display: flex !important;
}
/* Modal 和右键菜单的 z-index 提升 */

View File

@ -85,7 +85,6 @@ 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,7 +5,6 @@
<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 内容 -->
@ -238,7 +237,6 @@
bottom: -40px;
width: 300px;
height: 300px;
background-image: url('public/pure.svg');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
@ -582,7 +580,6 @@
<!-- 移动端 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">
@ -1197,7 +1194,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>
@ -1208,7 +1205,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">
@ -1440,42 +1437,6 @@
<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>
@ -1973,19 +1934,14 @@
// 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);
}

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')
: 'http://localhost:3456/api/mistral/v1/files';
: (window.PBX_PROXY_BASE_URL || '/api') + '/mistral/v1/files';
const response = await fetch(proxyUrl, {
method: 'POST',
@ -99,9 +99,7 @@ async function uploadFileToOssViaProxy(fileToProcess, fileName) {
// 使用统一配置获取代理地址
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getOssUploadUrl()
: (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? 'http://localhost:3456/api/upload/oss'
: '/api/upload/oss');
: (window.PBX_PROXY_BASE_URL || '/api') + '/upload/oss';
const response = await fetch(proxyUrl, {
method: 'POST',
@ -133,7 +131,7 @@ async function getMistralSignedUrl(fileId, mistralKey) {
// 使用统一配置获取代理地址
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getMistralUrl(`/v1/files/${fileId}/url?expiry=24`)
: `http://localhost:3456/api/mistral/v1/files/${fileId}/url?expiry=24`;
: (window.PBX_PROXY_BASE_URL || '/api') + `/mistral/v1/files/${fileId}/url?expiry=24`;
const response = await fetch(proxyUrl, {
method: 'GET',
@ -162,7 +160,7 @@ async function callMistralOcr(signedUrl, mistralKey) {
// 使用统一配置获取代理地址
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getMistralUrl('/v1/ocr')
: 'http://localhost:3456/api/mistral/v1/ocr';
: (window.PBX_PROXY_BASE_URL || '/api') + '/mistral/v1/ocr';
const response = await fetch(proxyUrl, {
method: 'POST',
@ -202,7 +200,7 @@ async function deleteMistralFile(fileId, apiKey) {
// 使用统一配置获取代理地址
const deleteUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getMistralUrl(`/v1/files/${fileId}`)
: `http://localhost:3456/api/mistral/v1/files/${fileId}`;
: (window.PBX_PROXY_BASE_URL || '/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 = 'mistral';
let ocrEngine = 'mineru';
try {
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
ocrEngine = window.ocrSettingsManager.getCurrentConfig().engine || 'mistral';
ocrEngine = window.ocrSettingsManager.getCurrentConfig().engine || 'mineru';
const validation = window.ocrSettingsManager.validateConfig();
if (!validation.valid) {
const engineNames = { mistral: 'Mistral OCR', mineru: 'MinerU', doc2x: 'Doc2X', none: '不需要 OCR' };
@ -2241,9 +2241,19 @@ async function handleReadClick() {
batchTotal: null,
batchTemplate: null,
batchFormats: null,
batchStartedAt: null
batchStartedAt: null,
// 对于 PDF 文件,将 base64 也存入 metadata.originalPdfBase64
// 以便历史详情页面能够正确显示PDF
metadata: fileType === 'pdf' ? { originalPdfBase64: base64Content } : 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);
@ -2254,6 +2264,11 @@ 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) {

View File

@ -1,133 +0,0 @@
// 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) ||
'http://localhost:3456';
'/api';
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 || 'http://localhost:3456');
: (window.PBX_PROXY_BASE_URL || '/api');
// 确定提供商(从模型 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 || 'http://localhost:3456');
: (window.PBX_PROXY_BASE_URL || '/api');
// 在使用代理服务器模式时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 || 'http://localhost:3456');
: (window.PBX_PROXY_BASE_URL || '/api');
// 只做单轮整理,不带历史
let apiConfig;

View File

@ -276,12 +276,9 @@ 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">
<img src="${logoPath}" class="typing-logo" alt="Thinking..." />
<iconify-icon icon="carbon:ai" class="typing-icon" width="24"></iconify-icon>
</div>
`;
} else {
@ -525,15 +522,11 @@ 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">
<img src="${logoPath}" class="typing-logo" alt="Thinking..." />
<iconify-icon icon="carbon:ai" class="typing-icon" width="24"></iconify-icon>
</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.open('views/history/history_detail.html?id=' + encodeURIComponent(id), '_blank');
window.location.href = 'views/history/history_detail.html?id=' + encodeURIComponent(id);
};
// =====================
@ -2195,8 +2195,8 @@ document.addEventListener('DOMContentLoaded', function() {
};
/**
* (全局可调用) 新的浏览器标签页或窗口中显示指定历史记录的详细信息
* 它通过构建一个指向 `views/history/history_detail.html` URL (包含记录 ID 作为查询参数) 并使用 `window.open` 实现
* (全局可调用) 当前页面显示指定历史记录的详细信息
* 它通过构建一个指向 `views/history/history_detail.html` URL (包含记录 ID 作为查询参数) 实现
*
* @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.open('views/history/history_detail.html?id=' + encodeURIComponent(id), '_blank');
window.location.href = 'views/history/history_detail.html?id=' + encodeURIComponent(id);
};
/**

View File

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

View File

@ -150,6 +150,10 @@ 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
@ -295,11 +299,6 @@ 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;
@ -311,6 +310,13 @@ 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 = 'mistral';
let ocrEngineForLog = 'mineru';
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') || 'mistral';
ocrEngineForLog = localStorage.getItem('ocrEngine') || 'mineru';
}
} 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 || 'http://localhost:3456'),
: (window.PBX_PROXY_BASE_URL || '/api'),
// 允许从设置自定义重试,若无则用默认
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) ||
'http://localhost:3456';
'/api';
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 || 'http://localhost:3456');
: (window.PBX_PROXY_BASE_URL || '/api');
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 > 默认值
// 优先级config.baseUrl > window.ProxyConfig > PBX_PROXY_BASE_URL > 默认相对路径
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 = 'http://localhost:3456/api/mistral';
this.baseUrl = (window.PBX_PROXY_BASE_URL || '/api') + '/mistral';
}
}

View File

@ -44,7 +44,7 @@ class OcrManager {
}
// Fallback: 直接从 localStorage 读取
const engine = localStorage.getItem('ocrEngine') || 'mistral';
const engine = localStorage.getItem('ocrEngine') || 'mineru';
switch (engine) {
case 'local':
@ -97,7 +97,7 @@ class OcrManager {
// 使用统一配置获取默认 workerUrl
const mineruDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: 'http://localhost:3456';
: (window.PBX_PROXY_BASE_URL || '/api');
return {
engine: 'mineru',
token: localStorage.getItem('ocrMinerUToken') || '',
@ -113,7 +113,7 @@ class OcrManager {
// 使用统一配置获取默认 workerUrl
const doc2xDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig)
? window.ProxyConfig.getProxyUrl()
: 'http://localhost:3456';
: (window.PBX_PROXY_BASE_URL || '/api');
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 || 'http://localhost:3456') + '/api/llm/tongyi/v1/chat/completions'),
: ((window.PBX_PROXY_BASE_URL || '/api') + '/llm/tongyi/v1/chat/completions'),
modelName: '通义百炼',
headers: { 'Content-Type': 'application/json' },
bodyBuilder: (sys, user) => {

View File

@ -71,6 +71,16 @@ 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,
@ -82,9 +92,8 @@ 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}`);
}
@ -95,112 +104,148 @@ class BackendStorage {
// 用户设置
async loadSettings() {
try {
if (!AuthManager.isAuthenticated()) return this._getDefaultSettings();
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadSettings');
}
const data = await this.fetchAPI('/user/settings');
return data;
} catch (error) {
console.error('Failed to load settings from backend:', error);
return this._getDefaultSettings();
return this._fallbackTo('loadSettings');
}
}
async saveSettings(settings) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveSettings', settings);
}
await this.fetchAPI('/user/settings', {
method: 'PUT',
body: JSON.stringify(settings)
});
} catch (error) {
console.error('Failed to save settings to backend:', error);
throw error;
return this._fallbackTo('saveSettings', settings);
}
}
// API Keys
async loadModelKeys(provider) {
try {
if (!AuthManager.isAuthenticated()) return [];
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadModelKeys', provider);
}
const keys = await this.fetchAPI(`/user/api-keys?provider=${provider}`);
return keys;
} catch (error) {
console.error('Failed to load API keys:', error);
return [];
return this._fallbackTo('loadModelKeys', provider);
}
}
async saveModelKeys(provider, keys) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveModelKeys', provider, keys);
}
await this.fetchAPI('/user/api-keys', {
method: 'POST',
body: JSON.stringify({ provider, keys })
});
} catch (error) {
console.error('Failed to save API keys:', error);
throw error;
return this._fallbackTo('saveModelKeys', provider, keys);
}
}
// 文档历史
async saveResultToDB(document) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveResultToDB', document);
}
await this.fetchAPI('/documents', { method: 'POST', body: JSON.stringify(document) });
} catch (error) {
console.error('Failed to save document:', error);
throw error;
console.error('Failed to save document to backend, falling back to local:', error);
return this._fallbackTo('saveResultToDB', document);
}
}
async getAllResultsFromDB() {
try {
if (!AuthManager.isAuthenticated()) return [];
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('getAllResultsFromDB');
}
const data = await this.fetchAPI('/documents');
return data.documents || [];
} catch (error) {
console.error('Failed to load documents:', error);
return [];
console.error('Failed to load documents from backend, falling back to local:', error);
return this._fallbackTo('getAllResultsFromDB');
}
}
async getResultFromDB(id) {
try {
if (!AuthManager.isAuthenticated()) return null;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('getResultFromDB', id);
}
return await this.fetchAPI(`/documents/${id}`);
} catch (error) {
console.error('Failed to load document:', error);
return null;
console.error('Failed to load document from backend, falling back to local:', error);
return this._fallbackTo('getResultFromDB', id);
}
}
async deleteResultFromDB(id) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('deleteResultFromDB', id);
}
await this.fetchAPI(`/documents/${id}`, { method: 'DELETE' });
} catch (error) {
console.error('Failed to delete document:', error);
throw 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');
}
}
// 术语库
async loadGlossarySets() {
try {
if (!AuthManager.isAuthenticated()) return {};
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadGlossarySets');
}
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 {};
return this._fallbackTo('loadGlossarySets');
}
}
async saveGlossarySets(sets) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveGlossarySets', sets);
}
// 批量保存(简化实现)
for (const [id, set] of Object.entries(sets)) {
if (set._isNew) {
@ -211,35 +256,42 @@ class BackendStorage {
}
} catch (error) {
console.error('Failed to save glossaries:', error);
throw error;
return this._fallbackTo('saveGlossarySets', sets);
}
}
// 标注
async saveAnnotationToDB(annotation) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveAnnotationToDB', annotation);
}
await this.fetchAPI(`/documents/${annotation.documentId}/annotations`, { method: 'POST', body: JSON.stringify(annotation) });
} catch (error) {
console.error('Failed to save annotation:', error);
throw error;
return this._fallbackTo('saveAnnotationToDB', annotation);
}
}
async getAnnotationsForDocFromDB(docId) {
try {
if (!AuthManager.isAuthenticated()) return [];
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('getAnnotationsForDocFromDB', docId);
}
return await this.fetchAPI(`/documents/${docId}/annotations`);
} catch (error) {
console.error('Failed to load annotations:', error);
return [];
return this._fallbackTo('getAnnotationsForDocFromDB', docId);
}
}
// 聊天历史
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) {
@ -250,82 +302,95 @@ 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 [];
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadReferences', docId) || [];
}
return await this.fetchAPI(`/references/${docId}/references`);
} catch (error) {
console.error('Failed to load references:', error);
return [];
return this._fallbackTo('loadReferences', docId) || [];
}
}
async saveReference(docId, reference) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('saveReference', docId, reference);
}
await this.fetchAPI(`/references/${docId}/references`, {
method: 'POST',
body: JSON.stringify(reference)
});
} catch (error) {
console.error('Failed to save reference:', error);
throw error;
return this._fallbackTo('saveReference', docId, reference);
}
}
async deleteReference(docId, refId) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('deleteReference', docId, refId);
}
await this.fetchAPI(`/references/${docId}/references/${refId}`, { method: 'DELETE' });
} catch (error) {
console.error('Failed to delete reference:', error);
throw error;
return this._fallbackTo('deleteReference', docId, refId);
}
}
// Prompt Pool
async loadPromptPool() {
try {
if (!AuthManager.isAuthenticated()) return { prompts: [], healthConfig: null };
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('loadPromptPool');
}
return await this.fetchAPI('/prompt-pool');
} catch (error) {
console.error('Failed to load prompt pool:', error);
return { prompts: [], healthConfig: null };
return this._fallbackTo('loadPromptPool');
}
}
async savePromptPool(data) {
try {
if (!AuthManager.isAuthenticated()) return;
if (!AuthManager.isAuthenticated()) {
return this._fallbackTo('savePromptPool', data);
}
await this.fetchAPI('/prompt-pool', {
method: 'PUT',
body: JSON.stringify(data)
});
} catch (error) {
console.error('Failed to save prompt pool:', error);
throw error;
return this._fallbackTo('savePromptPool', data);
}
}
@ -408,26 +473,16 @@ 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') || 'mistral',
engine: localStorage.getItem('ocrEngine') || 'mineru', // 默认为 mineru
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) || 'mistral';
const engine = localStorage.getItem(this.keys.engine) || 'mineru';
if (this.elements.ocrEngine) {
this.elements.ocrEngine.value = engine;
this.switchEngine(engine); // 显示对应的配置面板
@ -125,7 +125,8 @@ class OcrSettingsManager {
this.elements.mineruToken.value = localStorage.getItem(this.keys.mineruToken) || '';
}
if (this.elements.mineruWorkerUrl) {
this.elements.mineruWorkerUrl.value = localStorage.getItem(this.keys.mineruWorkerUrl) || '';
const defaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
this.elements.mineruWorkerUrl.value = localStorage.getItem(this.keys.mineruWorkerUrl) || defaultUrl;
}
if (this.elements.mineruEnableOcr) {
this.elements.mineruEnableOcr.checked = localStorage.getItem(this.keys.mineruEnableOcr) !== 'false';
@ -151,7 +152,8 @@ class OcrSettingsManager {
this.elements.doc2xToken.value = localStorage.getItem(this.keys.doc2xToken) || '';
}
if (this.elements.doc2xWorkerUrl) {
this.elements.doc2xWorkerUrl.value = localStorage.getItem(this.keys.doc2xWorkerUrl) || '';
const defaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
this.elements.doc2xWorkerUrl.value = localStorage.getItem(this.keys.doc2xWorkerUrl) || defaultUrl;
}
if (this.elements.doc2xFormulaMode) {
this.elements.doc2xFormulaMode.value = localStorage.getItem(this.keys.doc2xFormulaMode) || 'dollar';
@ -240,7 +242,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 || 'frontend');
localStorage.setItem(this.keys.mineruTokenMode, cfg.tokenMode || 'backend'); // 默认后端转发模式
localStorage.setItem(this.keys.mineruEnableOcr, cfg.enableOcr !== false);
localStorage.setItem(this.keys.mineruEnableFormula, cfg.enableFormula !== false);
localStorage.setItem(this.keys.mineruEnableTable, cfg.enableTable !== false);
@ -249,7 +251,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 || 'frontend');
localStorage.setItem(this.keys.doc2xTokenMode, cfg.tokenMode || 'backend'); // 默认后端转发模式
localStorage.setItem(this.keys.doc2xFormulaMode, cfg.formulaMode || 'dollar');
localStorage.setItem(this.keys.doc2xExportFormat, cfg.exportFormat || '');
}
@ -474,7 +476,7 @@ class OcrSettingsManager {
* @returns {Object} 配置对象
*/
getCurrentConfig() {
const engine = localStorage.getItem(this.keys.engine) || 'mistral';
const engine = localStorage.getItem(this.keys.engine) || 'mineru';
switch (engine) {
case 'none':
@ -514,12 +516,13 @@ 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) || '').replace(/\/+$/, ''), // 去掉末尾斜杠
workerUrl: (localStorage.getItem(this.keys.mineruWorkerUrl) || mineruDefaultUrl).replace(/\/+$/, ''), // 去掉末尾斜杠
authKey: localStorage.getItem(this.keys.workerAuthKey) || '',
tokenMode: localStorage.getItem(this.keys.mineruTokenMode) || 'frontend',
tokenMode: localStorage.getItem(this.keys.mineruTokenMode) || 'backend', // 默认后端转发模式
enableOcr: localStorage.getItem(this.keys.mineruEnableOcr) !== 'false',
enableFormula: localStorage.getItem(this.keys.mineruEnableFormula) !== 'false',
enableTable: localStorage.getItem(this.keys.mineruEnableTable) !== 'false',
@ -527,12 +530,13 @@ 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) || '').replace(/\/+$/, ''), // 去掉末尾斜杠
workerUrl: (localStorage.getItem(this.keys.doc2xWorkerUrl) || doc2xDefaultUrl).replace(/\/+$/, ''), // 去掉末尾斜杠
authKey: localStorage.getItem(this.keys.workerAuthKey) || '',
tokenMode: localStorage.getItem(this.keys.doc2xTokenMode) || 'frontend',
tokenMode: localStorage.getItem(this.keys.doc2xTokenMode) || 'backend', // 默认后端转发模式
formulaMode: localStorage.getItem(this.keys.doc2xFormulaMode) || 'dollar',
exportFormat: localStorage.getItem(this.keys.doc2xExportFormat) || ''
};
@ -563,17 +567,17 @@ class OcrSettingsManager {
break;
case 'mineru':
// 前端透传模式需要 TokenWorker 配置模式不需要
// 后端转发模式tokenMode 不是 'frontend')不需要前端配置 Token
if (config.tokenMode === 'frontend' && !config.token) {
return { valid: false, message: '请配置 MinerU Token前端透传模式' };
}
if (!config.workerUrl) {
if (config.tokenMode === 'frontend' && !config.workerUrl) {
return { valid: false, message: '请配置 MinerU Worker URL' };
}
break;
case 'doc2x':
// 前端透传模式需要 TokenWorker 配置模式不需要
// 后端转发模式tokenMode 不是 'frontend')不需要前端配置 Token
if (config.tokenMode === 'frontend' && !config.token) {
return { valid: false, message: '请配置 Doc2X Token前端透传模式' };
}

View File

@ -66,10 +66,6 @@
// Settings Link
originalSettingsLink: '#settings-link'
},
logos: {
full: '../../public/h_with_name.svg',
pure: '../../public/pure.svg'
}
};
@ -146,18 +142,16 @@
* @param {boolean} collapsed - 是否折叠
*/
function setSidebarCollapsed(collapsed) {
if (!elements.appSidebar || !elements.sidebarLogo) return;
if (!elements.appSidebar) 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 = 'mistral';
let ocrEngine = 'mineru';
let ocrConfigValid = true;
let ocrConfigMessage = '';
try {
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
ocrEngine = window.ocrSettingsManager.getCurrentConfig().engine || (localStorage.getItem('ocrEngine') || 'mistral');
ocrEngine = window.ocrSettingsManager.getCurrentConfig().engine || (localStorage.getItem('ocrEngine') || 'mineru');
// 使用 validateConfig 检查配置是否完整
const validation = window.ocrSettingsManager.validateConfig();
ocrConfigValid = validation.valid;
ocrConfigMessage = validation.message;
} else {
ocrEngine = localStorage.getItem('ocrEngine') || 'mistral';
ocrEngine = localStorage.getItem('ocrEngine') || 'mineru';
}
} catch {}

View File

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

View File

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

View File

@ -1,132 +0,0 @@
<!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>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

18146
public/pdfjs/build/pdf.js Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

58353
public/pdfjs/build/pdf.worker.js vendored Normal file

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://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link href="https://registry.npmmirror.com/tailwindcss/2.2.19/files/dist/tailwind.min.css" rel="stylesheet">
<script src="../build/pdf.js"></script>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -9,9 +9,6 @@ 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,11 +344,6 @@ 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'));

View File

@ -4,7 +4,6 @@
<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,7 +3,6 @@
<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 />
@ -105,8 +104,8 @@
</script>
</head>
<body class="immersive-pending">
<!-- Standard Immersive Mode Toggle Button -->
<button id="toggle-immersive-btn" class="tiny-round-btn hidden" style="display: none;" title="进入沉浸式布局">
<!-- Standard Immersive Mode Toggle Button 进入沉浸式布局-->
<button id="toggle-immersive-btn" class="tiny-round-btn disvisible hidden" style="display: none;" title="">
<i class="fas fa-expand-alt"></i>
<!-- Default icon for entering immersive mode -->
</button>
@ -190,12 +189,6 @@
<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>
<!-- 桌面端收起按钮 -->
@ -329,11 +322,6 @@
<!-- 移动端顶部栏 -->
<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">
@ -829,6 +817,8 @@
<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>
@ -862,22 +852,26 @@
<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="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<!-- PDF.js for structured translation PDF comparison (本地版本) -->
<script src="../../public/pdfjs/build/pdf.js"></script>
<script>
if (typeof pdfjsLib !== "undefined") {
pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
// 使用绝对路径确保 Worker 可以正确加载
const baseUrl = window.location.origin + window.location.pathname.split('/views/')[0];
pdfjsLib.GlobalWorkerOptions.workerSrc = baseUrl + "/public/pdfjs/build/pdf.worker.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";
window.PBX_PROXY_BASE_URL = "http://localhost:3456";
// PBX_PROXY_BASE_URL 由 proxy-config.js 自动设置:
// - 本地开发: http://localhost:3456
// - 生产环境: /api (相对路径)
// 使用阿里云百炼平台
window.PBX_LLM_PROVIDER = "aliyun";
// 默认模型qwen-plus推荐其他可选qwen-turbo、qwen-max、qwen-long-context
@ -889,10 +883,10 @@
window.PBX_LLM_MODEL,
);
</script>
<!-- 直接向 localhost:3456 发送对话消息的函数 -->
<!-- 通过代理发送对话消息的函数 -->
<script>
/**
* 直接向 http://localhost:3456 发送对话消息
* 通过代理服务器发送对话消息
* @param {string} userMessage - 用户消息内容
* @param {Array} [conversationHistory=[]] - 对话历史 [{role, content}, ...]
* @param {Object} [options={}] - 可选配置
@ -903,7 +897,8 @@
conversationHistory = [],
options = {},
) {
const endpoint = "http://localhost:3456/v1/chat/completions";
const baseUrl = window.PBX_PROXY_BASE_URL || '/api';
const endpoint = `${baseUrl}/v1/chat/completions`;
const model = options.model || window.PBX_LLM_MODEL || "qwen-plus";
const temperature = options.temperature || 0.7;
const maxTokens = options.maxTokens || 2048;
@ -961,7 +956,8 @@
onChunk,
options = {},
) {
const endpoint = "http://localhost:3456/v1/chat/completions";
const baseUrl = window.PBX_PROXY_BASE_URL || '/api';
const endpoint = `${baseUrl}/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,14 +5,11 @@
<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 (更专业、现代) */
@ -559,7 +556,7 @@
<header id="mainHeader" class="glass-header">
<!-- Left: Logo -->
<div class="header-logo flex-shrink-0">
<img src="../../public/h_with_name.svg" alt="Paper Burner X">
<span class="text-xl font-bold text-gray-800">Paper Burner X</span>
</div>
<!-- Center: Update Pill (Optional, can be hidden if needed) -->
@ -594,8 +591,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 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 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>
<!-- Hero Carousel Container -->

View File

@ -4,7 +4,6 @@
<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>