Compare commits
10 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
b019917c18 | |
|
|
5c1c66bd8f | |
|
|
51d1a4a1a2 | |
|
|
3028484949 | |
|
|
eb2a09b82f | |
|
|
823d6c15a7 | |
|
|
29e11f8877 | |
|
|
e9b9cc91ef | |
|
|
abebd5d62f | |
|
|
bfe00cc98f |
Binary file not shown.
|
|
@ -494,6 +494,7 @@ body.toc-dock-resizing #toc-vs-dock-resize-handle {
|
||||||
/* ==================== 8. 沉浸模式切换按钮 ==================== */
|
/* ==================== 8. 沉浸模式切换按钮 ==================== */
|
||||||
|
|
||||||
#toggle-immersive-btn {
|
#toggle-immersive-btn {
|
||||||
|
display: none !important; /* 永久隐藏 */
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
top: var(--spacing-md) !important;
|
top: var(--spacing-md) !important;
|
||||||
right: 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;
|
flex-direction: column !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 沉浸模式下隐藏 PDF 对照按钮 */
|
/* 沉浸模式下显示 PDF 对照按钮(用户要求在沉浸模式中也能使用) */
|
||||||
body.immersive-active #tab-pdf-compare {
|
body.immersive-active #tab-pdf-compare {
|
||||||
display: none !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Modal 和右键菜单的 z-index 提升 */
|
/* Modal 和右键菜单的 z-index 提升 */
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@ services:
|
||||||
# 挂载前端与服务端源码为只读,确保容器运行当前工作区代码(避免旧镜像残留)
|
# 挂载前端与服务端源码为只读,确保容器运行当前工作区代码(避免旧镜像残留)
|
||||||
- ./server/src:/app/server/src:ro
|
- ./server/src:/app/server/src:ro
|
||||||
- ./index.html:/app/index.html:ro
|
- ./index.html:/app/index.html:ro
|
||||||
- ./login.html:/app/login.html:ro
|
|
||||||
- ./admin:/app/admin:ro
|
- ./admin:/app/admin:ro
|
||||||
- ./views:/app/views:ro
|
- ./views:/app/views:ro
|
||||||
- ./js:/app/js:ro
|
- ./js:/app/js:ro
|
||||||
|
|
|
||||||
48
index.html
48
index.html
|
|
@ -5,7 +5,6 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Paper Burner X - 您的一站式AI文献阅读与智能分析平台,支持OCR、翻译、深度解析与高级管理。">
|
<meta name="description" content="Paper Burner X - 您的一站式AI文献阅读与智能分析平台,支持OCR、翻译、深度解析与高级管理。">
|
||||||
<title>Paper Burner X - AI文献阅读与智能分析平台</title>
|
<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>
|
<!-- 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 内容 -->
|
<!-- XSS 防护:DOMPurify - 用于清理 AI 生成的 HTML 内容 -->
|
||||||
|
|
@ -238,7 +237,6 @@
|
||||||
bottom: -40px;
|
bottom: -40px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
background-image: url('public/pure.svg');
|
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
|
|
@ -582,7 +580,6 @@
|
||||||
<!-- 移动端 Header
|
<!-- 移动端 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">
|
<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">
|
<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
|
PBX
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
|
|
@ -1197,7 +1194,7 @@
|
||||||
<!-- 卡片 4: 文件上传 -->
|
<!-- 卡片 4: 文件上传 -->
|
||||||
<!-- ===================== -->
|
<!-- ===================== -->
|
||||||
<div class="modern-card p-4 md:p-6">
|
<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">
|
<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);">
|
<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>
|
<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);">
|
<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>
|
<span class="select-none font-medium">批量模式</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<!-- 拖拽区域 -->
|
<!-- 拖拽区域 -->
|
||||||
<div id="dropZone" class="upload-zone-modern p-8 text-center mb-6 group cursor-pointer">
|
<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>
|
<script src="js/storage/storage.js"></script>
|
||||||
<!-- 先初始化存储适配器(提供 isFrontendMode 标记) -->
|
<!-- 先初始化存储适配器(提供 isFrontendMode 标记) -->
|
||||||
<script src="js/storage/storage-adapter.js"></script>
|
<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="js/storage/glossary-storage.js"></script>
|
||||||
<script src="https://gcore.jsdelivr.net/npm/mammoth@1.4.21/mammoth.browser.min.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
|
// Desktop Sidebar Collapse Logic
|
||||||
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
|
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
|
||||||
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
|
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) {
|
function setSidebarState(collapsed) {
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
appSidebar.classList.add('collapsed');
|
appSidebar.classList.add('collapsed');
|
||||||
if (sidebarToggleIcon) sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-open');
|
if (sidebarToggleIcon) sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-open');
|
||||||
if (sidebarLogo) sidebarLogo.src = LOGO_PURE;
|
|
||||||
} else {
|
} else {
|
||||||
appSidebar.classList.remove('collapsed');
|
appSidebar.classList.remove('collapsed');
|
||||||
if (sidebarToggleIcon) sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-close');
|
if (sidebarToggleIcon) sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-close');
|
||||||
if (sidebarLogo) sidebarLogo.src = LOGO_FULL;
|
|
||||||
}
|
}
|
||||||
localStorage.setItem('pbx_sidebar_collapsed', collapsed);
|
localStorage.setItem('pbx_sidebar_collapsed', collapsed);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2126
index.html.backup
2126
index.html.backup
File diff suppressed because it is too large
Load Diff
|
|
@ -66,7 +66,7 @@ async function uploadToMistral(fileToProcess, mistralKey) {
|
||||||
// 使用统一配置获取代理地址
|
// 使用统一配置获取代理地址
|
||||||
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getMistralUrl('/v1/files')
|
? 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, {
|
const response = await fetch(proxyUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -99,9 +99,7 @@ async function uploadFileToOssViaProxy(fileToProcess, fileName) {
|
||||||
// 使用统一配置获取代理地址
|
// 使用统一配置获取代理地址
|
||||||
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getOssUploadUrl()
|
? window.ProxyConfig.getOssUploadUrl()
|
||||||
: (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
|
: (window.PBX_PROXY_BASE_URL || '/api') + '/upload/oss';
|
||||||
? 'http://localhost:3456/api/upload/oss'
|
|
||||||
: '/api/upload/oss');
|
|
||||||
|
|
||||||
const response = await fetch(proxyUrl, {
|
const response = await fetch(proxyUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -133,7 +131,7 @@ async function getMistralSignedUrl(fileId, mistralKey) {
|
||||||
// 使用统一配置获取代理地址
|
// 使用统一配置获取代理地址
|
||||||
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getMistralUrl(`/v1/files/${fileId}/url?expiry=24`)
|
? 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, {
|
const response = await fetch(proxyUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
|
|
@ -162,7 +160,7 @@ async function callMistralOcr(signedUrl, mistralKey) {
|
||||||
// 使用统一配置获取代理地址
|
// 使用统一配置获取代理地址
|
||||||
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getMistralUrl('/v1/ocr')
|
? 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, {
|
const response = await fetch(proxyUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -202,7 +200,7 @@ async function deleteMistralFile(fileId, apiKey) {
|
||||||
// 使用统一配置获取代理地址
|
// 使用统一配置获取代理地址
|
||||||
const deleteUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const deleteUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getMistralUrl(`/v1/files/${fileId}`)
|
? 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 {
|
try {
|
||||||
const response = await fetch(deleteUrl, {
|
const response = await fetch(deleteUrl, {
|
||||||
|
|
|
||||||
21
js/app.js
21
js/app.js
|
|
@ -1758,10 +1758,10 @@ async function handleProcessClick() {
|
||||||
|
|
||||||
// 2. 检查 OCR 配置(如果有 PDF 文件)
|
// 2. 检查 OCR 配置(如果有 PDF 文件)
|
||||||
if (hasPdfFiles) {
|
if (hasPdfFiles) {
|
||||||
let ocrEngine = 'mistral';
|
let ocrEngine = 'mineru';
|
||||||
try {
|
try {
|
||||||
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
|
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();
|
const validation = window.ocrSettingsManager.validateConfig();
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
const engineNames = { mistral: 'Mistral OCR', mineru: 'MinerU', doc2x: 'Doc2X', none: '不需要 OCR' };
|
const engineNames = { mistral: 'Mistral OCR', mineru: 'MinerU', doc2x: 'Doc2X', none: '不需要 OCR' };
|
||||||
|
|
@ -2241,9 +2241,19 @@ async function handleReadClick() {
|
||||||
batchTotal: null,
|
batchTotal: null,
|
||||||
batchTemplate: null,
|
batchTemplate: null,
|
||||||
batchFormats: 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') {
|
if (typeof window.storageAdapter !== 'undefined' && typeof window.storageAdapter.saveResultToDB === 'function') {
|
||||||
await window.storageAdapter.saveResultToDB(record);
|
await window.storageAdapter.saveResultToDB(record);
|
||||||
|
|
@ -2254,6 +2264,11 @@ async function handleReadClick() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加短暂延迟确保 IndexedDB 事务完全提交
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
console.log('[仅阅读] 数据已保存,准备跳转...');
|
||||||
|
|
||||||
// 跳转到历史详情页面
|
// 跳转到历史详情页面
|
||||||
window.location.href = `views/history/history_detail.html?id=${encodeURIComponent(recordId)}`;
|
window.location.href = `views/history/history_detail.html?id=${encodeURIComponent(recordId)}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
|
||||||
// 忽略所有门禁过程中的异常,避免影响前端模式体验
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -162,7 +162,7 @@
|
||||||
const proxyBaseUrl = options.proxyBaseUrl ||
|
const proxyBaseUrl = options.proxyBaseUrl ||
|
||||||
(typeof window !== 'undefined' && window.ProxyConfig ? window.ProxyConfig.getProxyUrl() : null) ||
|
(typeof window !== 'undefined' && window.ProxyConfig ? window.ProxyConfig.getProxyUrl() : null) ||
|
||||||
(typeof window !== 'undefined' && window.PBX_PROXY_BASE_URL ? window.PBX_PROXY_BASE_URL : null) ||
|
(typeof window !== 'undefined' && window.PBX_PROXY_BASE_URL ? window.PBX_PROXY_BASE_URL : null) ||
|
||||||
'http://localhost:3456';
|
'/api';
|
||||||
const provider = options.provider || 'openai';
|
const provider = options.provider || 'openai';
|
||||||
|
|
||||||
// 获取当前选择的模型ID(如果有模型检测模块)
|
// 获取当前选择的模型ID(如果有模型检测模块)
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@
|
||||||
const useProxy = globalProxyMode === 'proxy';
|
const useProxy = globalProxyMode === 'proxy';
|
||||||
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getProxyUrl()
|
? window.ProxyConfig.getProxyUrl()
|
||||||
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
|
: (window.PBX_PROXY_BASE_URL || '/api');
|
||||||
|
|
||||||
// 确定提供商(从模型 ID 或配置推断)
|
// 确定提供商(从模型 ID 或配置推断)
|
||||||
let provider = 'openai';
|
let provider = 'openai';
|
||||||
|
|
|
||||||
|
|
@ -447,7 +447,7 @@ async function sendChatbotMessage(userInput, updateChatbotUI, externalConfig = n
|
||||||
const useProxy = globalProxyMode === 'proxy';
|
const useProxy = globalProxyMode === 'proxy';
|
||||||
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getProxyUrl()
|
? window.ProxyConfig.getProxyUrl()
|
||||||
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
|
: (window.PBX_PROXY_BASE_URL || '/api');
|
||||||
|
|
||||||
// 在使用代理服务器模式时,API Key 在后端配置,前端可以没有 API Key
|
// 在使用代理服务器模式时,API Key 在后端配置,前端可以没有 API Key
|
||||||
// 否则,检查前端是否配置了有效的 API Key
|
// 否则,检查前端是否配置了有效的 API Key
|
||||||
|
|
@ -1740,7 +1740,7 @@ async function singleChunkSummary(sysPrompt, userInput, config, apiKey) {
|
||||||
const useProxy = globalProxyMode === 'proxy';
|
const useProxy = globalProxyMode === 'proxy';
|
||||||
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const proxyBaseUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getProxyUrl()
|
? window.ProxyConfig.getProxyUrl()
|
||||||
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
|
: (window.PBX_PROXY_BASE_URL || '/api');
|
||||||
|
|
||||||
// 只做单轮整理,不带历史
|
// 只做单轮整理,不带历史
|
||||||
let apiConfig;
|
let apiConfig;
|
||||||
|
|
|
||||||
|
|
@ -276,12 +276,9 @@ window.ChatbotMessageRenderer = {
|
||||||
const isPurelyEmpty = (!m.content || String(m.content).trim() === '') && !m.reasoningContent && !m.toolCallHtml;
|
const isPurelyEmpty = (!m.content || String(m.content).trim() === '') && !m.reasoningContent && !m.toolCallHtml;
|
||||||
|
|
||||||
if (m.role === 'assistant' && isPurelyEmpty) {
|
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 = `
|
renderedContent = `
|
||||||
<div class="typing-indicator">
|
<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>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -525,15 +522,11 @@ window.ChatbotMessageRenderer = {
|
||||||
* @returns {string} HTML字符串。
|
* @returns {string} HTML字符串。
|
||||||
*/
|
*/
|
||||||
renderTypingIndicator: function() {
|
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 `
|
return `
|
||||||
<div class="message-container assistant-message-container">
|
<div class="message-container assistant-message-container">
|
||||||
<div class="chat-bubble assistant typing-bubble">
|
<div class="chat-bubble assistant typing-bubble">
|
||||||
<div class="typing-indicator">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ window.showHistoryDetail = function(id) {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[showHistoryDetail] Failed to save doc id to localStorage', 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。
|
* @param {string} id - 要查看详情的历史记录的唯一 ID。
|
||||||
*/
|
*/
|
||||||
|
|
@ -2206,7 +2206,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[showHistoryDetail] Failed to save doc id to localStorage', 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -466,15 +466,11 @@ async function triggerReprocess(includeTranslation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示处理中 Toast
|
|
||||||
showToast('正在准备文档...', 'info');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 将 Base64 转为 File 对象
|
// 将 Base64 转为 File 对象
|
||||||
const file = base64ToFile(pdfBase64, docName);
|
const file = base64ToFile(pdfBase64, docName);
|
||||||
|
|
||||||
// 执行 OCR
|
|
||||||
showToast('正在进行 OCR 识别...', 'info');
|
|
||||||
|
|
||||||
const ocrResult = await performOcr(file, (current, total, msg) => {
|
const ocrResult = await performOcr(file, (current, total, msg) => {
|
||||||
showToast(`${msg || 'OCR 处理中'} (${current}/${total})`, 'info', 5000);
|
showToast(`${msg || 'OCR 处理中'} (${current}/${total})`, 'info', 5000);
|
||||||
|
|
@ -493,7 +489,6 @@ async function triggerReprocess(includeTranslation) {
|
||||||
|
|
||||||
// 如果需要翻译,执行翻译
|
// 如果需要翻译,执行翻译
|
||||||
if (includeTranslation) {
|
if (includeTranslation) {
|
||||||
showToast('正在进行翻译...', 'info');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const translationResult = await performTranslation(ocrResult.markdown, (current, total, msg) => {
|
const translationResult = await performTranslation(ocrResult.markdown, (current, total, msg) => {
|
||||||
|
|
@ -515,7 +510,6 @@ async function triggerReprocess(includeTranslation) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到 IndexedDB
|
// 保存到 IndexedDB
|
||||||
showToast('正在保存...', 'info');
|
|
||||||
await saveResultToDB(window.data);
|
await saveResultToDB(window.data);
|
||||||
|
|
||||||
// 刷新页面显示
|
// 刷新页面显示
|
||||||
|
|
@ -710,7 +704,6 @@ async function triggerReprocessWithMinerU() {
|
||||||
const savedMineruMode = localStorage.getItem('mineruMode');
|
const savedMineruMode = localStorage.getItem('mineruMode');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showToast('正在使用 MinerU 处理文档...', 'info');
|
|
||||||
|
|
||||||
// 临时设置 OCR 配置为 MinerU + 结构化翻译模式
|
// 临时设置 OCR 配置为 MinerU + 结构化翻译模式
|
||||||
localStorage.setItem('ocrEngine', 'mineru');
|
localStorage.setItem('ocrEngine', 'mineru');
|
||||||
|
|
@ -736,7 +729,7 @@ async function triggerReprocessWithMinerU() {
|
||||||
pdfFile,
|
pdfFile,
|
||||||
null, // mistralKeyObject - 使用 MinerU 不需要
|
null, // mistralKeyObject - 使用 MinerU 不需要
|
||||||
null, // translationKeyObject - 使用后端代理不需要
|
null, // translationKeyObject - 使用后端代理不需要
|
||||||
'tongyi', // 使用通义模型,后端代理会处理
|
'aliyun', // 使用阿里云百炼(后端代理会处理)
|
||||||
null, // translationModelConfig
|
null, // translationModelConfig
|
||||||
settings.maxTokensPerChunk || 2000,
|
settings.maxTokensPerChunk || 2000,
|
||||||
settings.targetLanguage || 'Chinese',
|
settings.targetLanguage || 'Chinese',
|
||||||
|
|
@ -773,16 +766,21 @@ async function triggerReprocessWithMinerU() {
|
||||||
await saveResultToDB(window.data);
|
await saveResultToDB(window.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast('处理完成!', 'success');
|
showToast('处理完成!正在加载 PDF 对照视图...', 'success');
|
||||||
|
|
||||||
|
// 重置渲染锁
|
||||||
|
if (typeof renderingTab !== 'undefined') renderingTab = null;
|
||||||
|
|
||||||
// 刷新页面显示
|
// 刷新页面显示
|
||||||
if (typeof renderDetail === 'function') {
|
if (typeof renderDetail === 'function') {
|
||||||
renderDetail();
|
await renderDetail();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动跳转到原始文件标签页
|
// 自动跳转到 PDF 对照视图
|
||||||
if (typeof showTab === 'function') {
|
if (typeof showTabImmediate === 'function') {
|
||||||
showTab('original-file');
|
showTabImmediate('pdf-compare');
|
||||||
|
} else if (typeof showTab === 'function') {
|
||||||
|
showTab('pdf-compare');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -810,7 +808,7 @@ async function executeMinerUStructuredTranslation() {
|
||||||
// 使用统一配置获取代理地址
|
// 使用统一配置获取代理地址
|
||||||
const PROXY_BASE = (typeof window !== 'undefined' && window.ProxyConfig)
|
const PROXY_BASE = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getProxyUrl()
|
? window.ProxyConfig.getProxyUrl()
|
||||||
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
|
: (window.PBX_PROXY_BASE_URL || '/api');
|
||||||
|
|
||||||
// 获取翻译配置
|
// 获取翻译配置
|
||||||
const settings = typeof loadSettings === 'function' ? loadSettings() : {};
|
const settings = typeof loadSettings === 'function' ? loadSettings() : {};
|
||||||
|
|
@ -983,9 +981,17 @@ async function executeMinerUStructuredTranslation() {
|
||||||
addLog(`注意: 有 ${failedItems.length} 个片段翻译失败`);
|
addLog(`注意: 有 ${failedItems.length} 个片段翻译失败`);
|
||||||
}
|
}
|
||||||
|
|
||||||
addLog('正在加载 PDF 对照视图...');
|
addLog('正在刷新界面...');
|
||||||
|
|
||||||
// 短暂延迟后显示 PDF 对照视图
|
// 重置渲染锁,确保后续 showTab 可以执行
|
||||||
|
if (typeof renderingTab !== 'undefined') renderingTab = null;
|
||||||
|
|
||||||
|
// 刷新界面
|
||||||
|
if (typeof renderDetail === 'function') {
|
||||||
|
await renderDetail(); // await async 函数
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟后显示 PDF 对照视图
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (typeof showTabImmediate === 'function') {
|
if (typeof showTabImmediate === 'function') {
|
||||||
showTabImmediate('pdf-compare');
|
showTabImmediate('pdf-compare');
|
||||||
|
|
|
||||||
|
|
@ -150,6 +150,10 @@ function showTabImmediate(tab) {
|
||||||
if (DOM_CACHE.layout.meta) DOM_CACHE.layout.meta.style.display = '';
|
if (DOM_CACHE.layout.meta) DOM_CACHE.layout.meta.style.display = '';
|
||||||
if (DOM_CACHE.layout.tabsContainer) DOM_CACHE.layout.tabsContainer.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 html = '';
|
||||||
let contentContainerId = ''; // 用于 applyAnnotationsToContent
|
let contentContainerId = ''; // 用于 applyAnnotationsToContent
|
||||||
let activeContentElement = null; // 用于 applyAnnotationsToContent
|
let activeContentElement = null; // 用于 applyAnnotationsToContent
|
||||||
|
|
@ -295,11 +299,6 @@ function showTabImmediate(tab) {
|
||||||
// ========== MinerU PDF 对照视图 ==========
|
// ========== MinerU PDF 对照视图 ==========
|
||||||
if (DOM_CACHE.tabs.pdfCompare) DOM_CACHE.tabs.pdfCompare.classList.add('active');
|
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;
|
const hasStructuredData = data.metadata && data.metadata.originalPdfBase64 && data.metadata.contentListJson && data.metadata.translatedContentList;
|
||||||
|
|
||||||
|
|
@ -311,6 +310,13 @@ function showTabImmediate(tab) {
|
||||||
return;
|
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 容器
|
// 设置 HTML 容器
|
||||||
document.getElementById('tabContent').innerHTML = '<div id="pdf-compare-container"></div>';
|
document.getElementById('tabContent').innerHTML = '<div id="pdf-compare-container"></div>';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,13 +137,13 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
|
||||||
let usedOcrEngine = null;
|
let usedOcrEngine = null;
|
||||||
let usedOcrSource = null;
|
let usedOcrSource = null;
|
||||||
// 更合理的开始日志:显示 OCR 引擎而不是固定显示 Mistral Key
|
// 更合理的开始日志:显示 OCR 引擎而不是固定显示 Mistral Key
|
||||||
let ocrEngineForLog = 'mistral';
|
let ocrEngineForLog = 'mineru';
|
||||||
try {
|
try {
|
||||||
if (typeof window !== 'undefined' && window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
|
if (typeof window !== 'undefined' && window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
|
||||||
const cfg = window.ocrSettingsManager.getCurrentConfig();
|
const cfg = window.ocrSettingsManager.getCurrentConfig();
|
||||||
if (cfg && cfg.engine) ocrEngineForLog = cfg.engine;
|
if (cfg && cfg.engine) ocrEngineForLog = cfg.engine;
|
||||||
} else {
|
} else {
|
||||||
ocrEngineForLog = localStorage.getItem('ocrEngine') || 'mistral';
|
ocrEngineForLog = localStorage.getItem('ocrEngine') || 'mineru';
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
if (typeof addProgressLog === "function") {
|
if (typeof addProgressLog === "function") {
|
||||||
|
|
@ -800,7 +800,7 @@ const fileType = fileToProcess.name.split('.').pop().toLowerCase();
|
||||||
provider: selectedTranslationModelName,
|
provider: selectedTranslationModelName,
|
||||||
proxyBase: (typeof window !== 'undefined' && window.ProxyConfig)
|
proxyBase: (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getProxyUrl()
|
? 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),
|
maxRetries: (typeof loadSettings === 'function' ? (loadSettings().structuredMaxRetries || undefined) : undefined),
|
||||||
retryDelay: (typeof loadSettings === 'function' ? (loadSettings().structuredRetryDelayMs || undefined) : undefined)
|
retryDelay: (typeof loadSettings === 'function' ? (loadSettings().structuredRetryDelayMs || undefined) : undefined)
|
||||||
|
|
|
||||||
|
|
@ -717,7 +717,7 @@ ${jsonContent}
|
||||||
const proxyBase = options.proxyBase ||
|
const proxyBase = options.proxyBase ||
|
||||||
(typeof window !== 'undefined' && window.ProxyConfig ? window.ProxyConfig.getProxyUrl() : null) ||
|
(typeof window !== 'undefined' && window.ProxyConfig ? window.ProxyConfig.getProxyUrl() : null) ||
|
||||||
(typeof window !== 'undefined' && window.PBX_PROXY_BASE_URL ? window.PBX_PROXY_BASE_URL : null) ||
|
(typeof window !== 'undefined' && window.PBX_PROXY_BASE_URL ? window.PBX_PROXY_BASE_URL : null) ||
|
||||||
'http://localhost:3456';
|
'/api';
|
||||||
const provider = options.provider || 'aliyun';
|
const provider = options.provider || 'aliyun';
|
||||||
|
|
||||||
// 后端代理端点映射
|
// 后端代理端点映射
|
||||||
|
|
@ -836,7 +836,7 @@ ${jsonContent}
|
||||||
// 前端发出的请求源头
|
// 前端发出的请求源头
|
||||||
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const proxyUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getProxyUrl()
|
? window.ProxyConfig.getProxyUrl()
|
||||||
: (window.PBX_PROXY_BASE_URL || 'http://localhost:3456');
|
: (window.PBX_PROXY_BASE_URL || '/api');
|
||||||
const predefinedConfigs = {
|
const predefinedConfigs = {
|
||||||
'proxy': {
|
'proxy': {
|
||||||
endpoint: `${proxyUrl}/api/llm/aliyun/v1/chat/completions`,
|
endpoint: `${proxyUrl}/api/llm/aliyun/v1/chat/completions`,
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@ class MistralOcrAdapter extends OcrAdapter {
|
||||||
this.currentKeyIndex = 0;
|
this.currentKeyIndex = 0;
|
||||||
|
|
||||||
// 使用统一配置获取代理地址
|
// 使用统一配置获取代理地址
|
||||||
// 优先级:config.baseUrl > window.ProxyConfig > 默认值
|
// 优先级:config.baseUrl > window.ProxyConfig > PBX_PROXY_BASE_URL > 默认相对路径
|
||||||
if (config.baseUrl && config.baseUrl !== 'https://api.mistral.ai') {
|
if (config.baseUrl && config.baseUrl !== 'https://api.mistral.ai') {
|
||||||
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
||||||
} else if (typeof window !== 'undefined' && window.ProxyConfig) {
|
} else if (typeof window !== 'undefined' && window.ProxyConfig) {
|
||||||
this.baseUrl = window.ProxyConfig.getMistralUrl();
|
this.baseUrl = window.ProxyConfig.getMistralUrl();
|
||||||
} else {
|
} else {
|
||||||
this.baseUrl = 'http://localhost:3456/api/mistral';
|
this.baseUrl = (window.PBX_PROXY_BASE_URL || '/api') + '/mistral';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class OcrManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: 直接从 localStorage 读取
|
// Fallback: 直接从 localStorage 读取
|
||||||
const engine = localStorage.getItem('ocrEngine') || 'mistral';
|
const engine = localStorage.getItem('ocrEngine') || 'mineru';
|
||||||
|
|
||||||
switch (engine) {
|
switch (engine) {
|
||||||
case 'local':
|
case 'local':
|
||||||
|
|
@ -97,7 +97,7 @@ class OcrManager {
|
||||||
// 使用统一配置获取默认 workerUrl
|
// 使用统一配置获取默认 workerUrl
|
||||||
const mineruDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const mineruDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getProxyUrl()
|
? window.ProxyConfig.getProxyUrl()
|
||||||
: 'http://localhost:3456';
|
: (window.PBX_PROXY_BASE_URL || '/api');
|
||||||
return {
|
return {
|
||||||
engine: 'mineru',
|
engine: 'mineru',
|
||||||
token: localStorage.getItem('ocrMinerUToken') || '',
|
token: localStorage.getItem('ocrMinerUToken') || '',
|
||||||
|
|
@ -113,7 +113,7 @@ class OcrManager {
|
||||||
// 使用统一配置获取默认 workerUrl
|
// 使用统一配置获取默认 workerUrl
|
||||||
const doc2xDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
const doc2xDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getProxyUrl()
|
? window.ProxyConfig.getProxyUrl()
|
||||||
: 'http://localhost:3456';
|
: (window.PBX_PROXY_BASE_URL || '/api');
|
||||||
return {
|
return {
|
||||||
engine: 'doc2x',
|
engine: 'doc2x',
|
||||||
token: localStorage.getItem('ocrDoc2XToken') || '',
|
token: localStorage.getItem('ocrDoc2XToken') || '',
|
||||||
|
|
|
||||||
|
|
@ -705,7 +705,7 @@ async function translateMarkdown(
|
||||||
// 所有翻译请求都指向后端代理,由后端决定使用哪个模型
|
// 所有翻译请求都指向后端代理,由后端决定使用哪个模型
|
||||||
endpoint: (typeof window !== 'undefined' && window.ProxyConfig)
|
endpoint: (typeof window !== 'undefined' && window.ProxyConfig)
|
||||||
? window.ProxyConfig.getLLMProxyUrl('tongyi', '/v1/chat/completions')
|
? 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: '通义百炼',
|
modelName: '通义百炼',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
bodyBuilder: (sys, user) => {
|
bodyBuilder: (sys, user) => {
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,16 @@ class AuthManager {
|
||||||
|
|
||||||
// ---------------- 后端存储实现 ----------------
|
// ---------------- 后端存储实现 ----------------
|
||||||
class BackendStorage {
|
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 = {}) {
|
async fetchAPI(endpoint, options = {}) {
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
...options,
|
...options,
|
||||||
|
|
@ -82,9 +92,8 @@ class BackendStorage {
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// Token 过期,需要重新登录
|
// Token 过期,清除token
|
||||||
AuthManager.removeToken();
|
AuthManager.removeToken();
|
||||||
window.location.href = '/login.html';
|
|
||||||
}
|
}
|
||||||
throw new Error(`API Error: ${response.status}`);
|
throw new Error(`API Error: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
@ -95,112 +104,148 @@ class BackendStorage {
|
||||||
// 用户设置
|
// 用户设置
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return this._getDefaultSettings();
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('loadSettings');
|
||||||
|
}
|
||||||
const data = await this.fetchAPI('/user/settings');
|
const data = await this.fetchAPI('/user/settings');
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load settings from backend:', error);
|
console.error('Failed to load settings from backend:', error);
|
||||||
return this._getDefaultSettings();
|
return this._fallbackTo('loadSettings');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings(settings) {
|
async saveSettings(settings) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('saveSettings', settings);
|
||||||
|
}
|
||||||
await this.fetchAPI('/user/settings', {
|
await this.fetchAPI('/user/settings', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(settings)
|
body: JSON.stringify(settings)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save settings to backend:', error);
|
console.error('Failed to save settings to backend:', error);
|
||||||
throw error;
|
return this._fallbackTo('saveSettings', settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Keys
|
// API Keys
|
||||||
async loadModelKeys(provider) {
|
async loadModelKeys(provider) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return [];
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('loadModelKeys', provider);
|
||||||
|
}
|
||||||
const keys = await this.fetchAPI(`/user/api-keys?provider=${provider}`);
|
const keys = await this.fetchAPI(`/user/api-keys?provider=${provider}`);
|
||||||
return keys;
|
return keys;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load API keys:', error);
|
console.error('Failed to load API keys:', error);
|
||||||
return [];
|
return this._fallbackTo('loadModelKeys', provider);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveModelKeys(provider, keys) {
|
async saveModelKeys(provider, keys) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('saveModelKeys', provider, keys);
|
||||||
|
}
|
||||||
await this.fetchAPI('/user/api-keys', {
|
await this.fetchAPI('/user/api-keys', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ provider, keys })
|
body: JSON.stringify({ provider, keys })
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save API keys:', error);
|
console.error('Failed to save API keys:', error);
|
||||||
throw error;
|
return this._fallbackTo('saveModelKeys', provider, keys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文档历史
|
// 文档历史
|
||||||
async saveResultToDB(document) {
|
async saveResultToDB(document) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('saveResultToDB', document);
|
||||||
|
}
|
||||||
await this.fetchAPI('/documents', { method: 'POST', body: JSON.stringify(document) });
|
await this.fetchAPI('/documents', { method: 'POST', body: JSON.stringify(document) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save document:', error);
|
console.error('Failed to save document to backend, falling back to local:', error);
|
||||||
throw error;
|
return this._fallbackTo('saveResultToDB', document);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllResultsFromDB() {
|
async getAllResultsFromDB() {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return [];
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('getAllResultsFromDB');
|
||||||
|
}
|
||||||
const data = await this.fetchAPI('/documents');
|
const data = await this.fetchAPI('/documents');
|
||||||
return data.documents || [];
|
return data.documents || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load documents:', error);
|
console.error('Failed to load documents from backend, falling back to local:', error);
|
||||||
return [];
|
return this._fallbackTo('getAllResultsFromDB');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getResultFromDB(id) {
|
async getResultFromDB(id) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return null;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('getResultFromDB', id);
|
||||||
|
}
|
||||||
return await this.fetchAPI(`/documents/${id}`);
|
return await this.fetchAPI(`/documents/${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load document:', error);
|
console.error('Failed to load document from backend, falling back to local:', error);
|
||||||
return null;
|
return this._fallbackTo('getResultFromDB', id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteResultFromDB(id) {
|
async deleteResultFromDB(id) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('deleteResultFromDB', id);
|
||||||
|
}
|
||||||
await this.fetchAPI(`/documents/${id}`, { method: 'DELETE' });
|
await this.fetchAPI(`/documents/${id}`, { method: 'DELETE' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete document:', error);
|
console.error('Failed to delete document from backend, falling back to local:', error);
|
||||||
throw 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() {
|
async loadGlossarySets() {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return {};
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('loadGlossarySets');
|
||||||
|
}
|
||||||
const glossaries = await this.fetchAPI('/user/glossaries');
|
const glossaries = await this.fetchAPI('/user/glossaries');
|
||||||
const sets = {};
|
const sets = {};
|
||||||
glossaries.forEach(g => { sets[g.id] = g; });
|
glossaries.forEach(g => { sets[g.id] = g; });
|
||||||
return sets;
|
return sets;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load glossaries:', error);
|
console.error('Failed to load glossaries:', error);
|
||||||
return {};
|
return this._fallbackTo('loadGlossarySets');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveGlossarySets(sets) {
|
async saveGlossarySets(sets) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('saveGlossarySets', sets);
|
||||||
|
}
|
||||||
// 批量保存(简化实现)
|
// 批量保存(简化实现)
|
||||||
for (const [id, set] of Object.entries(sets)) {
|
for (const [id, set] of Object.entries(sets)) {
|
||||||
if (set._isNew) {
|
if (set._isNew) {
|
||||||
|
|
@ -211,35 +256,42 @@ class BackendStorage {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save glossaries:', error);
|
console.error('Failed to save glossaries:', error);
|
||||||
throw error;
|
return this._fallbackTo('saveGlossarySets', sets);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标注
|
// 标注
|
||||||
async saveAnnotationToDB(annotation) {
|
async saveAnnotationToDB(annotation) {
|
||||||
try {
|
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) });
|
await this.fetchAPI(`/documents/${annotation.documentId}/annotations`, { method: 'POST', body: JSON.stringify(annotation) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save annotation:', error);
|
console.error('Failed to save annotation:', error);
|
||||||
throw error;
|
return this._fallbackTo('saveAnnotationToDB', annotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAnnotationsForDocFromDB(docId) {
|
async getAnnotationsForDocFromDB(docId) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return [];
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('getAnnotationsForDocFromDB', docId);
|
||||||
|
}
|
||||||
return await this.fetchAPI(`/documents/${docId}/annotations`);
|
return await this.fetchAPI(`/documents/${docId}/annotations`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load annotations:', error);
|
console.error('Failed to load annotations:', error);
|
||||||
return [];
|
return this._fallbackTo('getAnnotationsForDocFromDB', docId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 聊天历史
|
// 聊天历史
|
||||||
async loadChatHistory(docId) {
|
async loadChatHistory(docId) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return [];
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
// 聊天历史在本地存储中没有对应方法,返回空数组
|
||||||
|
return [];
|
||||||
|
}
|
||||||
const data = await this.fetchAPI(`/chat/${docId}/history`);
|
const data = await this.fetchAPI(`/chat/${docId}/history`);
|
||||||
return data.messages || [];
|
return data.messages || [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -250,82 +302,95 @@ class BackendStorage {
|
||||||
|
|
||||||
async saveChatMessage(docId, message) {
|
async saveChatMessage(docId, message) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
// 聊天历史在本地存储中没有对应方法
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.fetchAPI(`/chat/${docId}/history`, {
|
await this.fetchAPI(`/chat/${docId}/history`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(message)
|
body: JSON.stringify(message)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save chat message:', error);
|
console.error('Failed to save chat message:', error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearChatHistory(docId) {
|
async clearChatHistory(docId) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.fetchAPI(`/chat/${docId}/history`, { method: 'DELETE' });
|
await this.fetchAPI(`/chat/${docId}/history`, { method: 'DELETE' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to clear chat history:', error);
|
console.error('Failed to clear chat history:', error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文献引用
|
// 文献引用
|
||||||
async loadReferences(docId) {
|
async loadReferences(docId) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return [];
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('loadReferences', docId) || [];
|
||||||
|
}
|
||||||
return await this.fetchAPI(`/references/${docId}/references`);
|
return await this.fetchAPI(`/references/${docId}/references`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load references:', error);
|
console.error('Failed to load references:', error);
|
||||||
return [];
|
return this._fallbackTo('loadReferences', docId) || [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveReference(docId, reference) {
|
async saveReference(docId, reference) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('saveReference', docId, reference);
|
||||||
|
}
|
||||||
await this.fetchAPI(`/references/${docId}/references`, {
|
await this.fetchAPI(`/references/${docId}/references`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(reference)
|
body: JSON.stringify(reference)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save reference:', error);
|
console.error('Failed to save reference:', error);
|
||||||
throw error;
|
return this._fallbackTo('saveReference', docId, reference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteReference(docId, refId) {
|
async deleteReference(docId, refId) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('deleteReference', docId, refId);
|
||||||
|
}
|
||||||
await this.fetchAPI(`/references/${docId}/references/${refId}`, { method: 'DELETE' });
|
await this.fetchAPI(`/references/${docId}/references/${refId}`, { method: 'DELETE' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete reference:', error);
|
console.error('Failed to delete reference:', error);
|
||||||
throw error;
|
return this._fallbackTo('deleteReference', docId, refId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt Pool
|
// Prompt Pool
|
||||||
async loadPromptPool() {
|
async loadPromptPool() {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return { prompts: [], healthConfig: null };
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('loadPromptPool');
|
||||||
|
}
|
||||||
return await this.fetchAPI('/prompt-pool');
|
return await this.fetchAPI('/prompt-pool');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load prompt pool:', error);
|
console.error('Failed to load prompt pool:', error);
|
||||||
return { prompts: [], healthConfig: null };
|
return this._fallbackTo('loadPromptPool');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async savePromptPool(data) {
|
async savePromptPool(data) {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) return;
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
return this._fallbackTo('savePromptPool', data);
|
||||||
|
}
|
||||||
await this.fetchAPI('/prompt-pool', {
|
await this.fetchAPI('/prompt-pool', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save prompt pool:', error);
|
console.error('Failed to save prompt pool:', error);
|
||||||
throw error;
|
return this._fallbackTo('savePromptPool', data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,26 +473,16 @@ class StorageAdapterFactory {
|
||||||
|
|
||||||
// ---------------- 初始化与自动切换 ----------------
|
// ---------------- 初始化与自动切换 ----------------
|
||||||
function printBanner() {
|
function printBanner() {
|
||||||
const logoStyle = 'font-size: 16px; font-weight: bold; color: #3b82f6;';
|
|
||||||
const infoStyle = 'font-size: 14px; color: #10b981;';
|
const infoStyle = 'font-size: 14px; color: #10b981;';
|
||||||
const modeStyle = 'font-size: 14px; font-weight: bold; color: #f59e0b;';
|
const modeStyle = 'font-size: 14px; font-weight: bold; color: #f59e0b;';
|
||||||
const borderStyle = 'color: #6366f1;';
|
const borderStyle = 'color: #6366f1;';
|
||||||
const linkStyle = 'font-size: 13px; color: #06b6d4; text-decoration: underline;';
|
const linkStyle = 'font-size: 13px; color: #06b6d4; text-decoration: underline;';
|
||||||
|
|
||||||
const logo = `
|
|
||||||
____ ____ __ __
|
|
||||||
| _ \\ __ _ _ __ ___ _ __ | __ ) _ _ _ __ _ __ ___ _ __ \\ \\/ /
|
|
||||||
| |_) / _\` | '_ \\ / _ \\ '__| | _ \\| | | | '__| '_ \\ / _ \\ '__| \\ /
|
|
||||||
| __/ (_| | |_) | __/ | | |_) | |_| | | | | | | __/ | / \\
|
|
||||||
|_| \\__,_| .__/ \\___|_| |____/ \\__,_|_| |_| |_|\\___|_| /_/\\_\\
|
|
||||||
|_|
|
|
||||||
`;
|
|
||||||
|
|
||||||
const mode = DEPLOYMENT_MODE === 'backend' ? '后端模式 (Backend Mode)' : '前端模式 (Frontend Mode)';
|
const mode = DEPLOYMENT_MODE === 'backend' ? '后端模式 (Backend Mode)' : '前端模式 (Frontend Mode)';
|
||||||
const storage = DEPLOYMENT_MODE === 'backend' ? 'Backend API + PostgreSQL' : 'localStorage + IndexedDB';
|
const storage = DEPLOYMENT_MODE === 'backend' ? 'Backend API + PostgreSQL' : 'localStorage + IndexedDB';
|
||||||
const auth = DEPLOYMENT_MODE === 'backend' ? 'JWT Authentication' : 'No Authentication';
|
const auth = DEPLOYMENT_MODE === 'backend' ? 'JWT Authentication' : 'No Authentication';
|
||||||
|
|
||||||
console.log('%c' + logo, logoStyle);
|
|
||||||
console.log('%c╔════════════════════════════════════════════════════════════╗', borderStyle);
|
console.log('%c╔════════════════════════════════════════════════════════════╗', borderStyle);
|
||||||
console.log('%c║ 系统信息 / System Info ║', borderStyle);
|
console.log('%c║ 系统信息 / System Info ║', borderStyle);
|
||||||
console.log('%c╠════════════════════════════════════════════════════════════╣', borderStyle);
|
console.log('%c╠════════════════════════════════════════════════════════════╣', borderStyle);
|
||||||
|
|
|
||||||
|
|
@ -711,7 +711,7 @@ KeyManagerUI.exportAllModelData = function() {
|
||||||
|
|
||||||
// 添加 OCR 配置导出
|
// 添加 OCR 配置导出
|
||||||
const ocrConfig = {
|
const ocrConfig = {
|
||||||
engine: localStorage.getItem('ocrEngine') || 'mistral',
|
engine: localStorage.getItem('ocrEngine') || 'mineru', // 默认为 mineru
|
||||||
mistralKeys: localStorage.getItem('ocrMistralKeys') || '',
|
mistralKeys: localStorage.getItem('ocrMistralKeys') || '',
|
||||||
workerAuthKey: localStorage.getItem('ocrWorkerAuthKey') || '',
|
workerAuthKey: localStorage.getItem('ocrWorkerAuthKey') || '',
|
||||||
mineruToken: localStorage.getItem('ocrMinerUToken') || '',
|
mineruToken: localStorage.getItem('ocrMinerUToken') || '',
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ class OcrSettingsManager {
|
||||||
loadSettings() {
|
loadSettings() {
|
||||||
try {
|
try {
|
||||||
// 引擎选择
|
// 引擎选择
|
||||||
const engine = localStorage.getItem(this.keys.engine) || 'mistral';
|
const engine = localStorage.getItem(this.keys.engine) || 'mineru';
|
||||||
if (this.elements.ocrEngine) {
|
if (this.elements.ocrEngine) {
|
||||||
this.elements.ocrEngine.value = engine;
|
this.elements.ocrEngine.value = engine;
|
||||||
this.switchEngine(engine); // 显示对应的配置面板
|
this.switchEngine(engine); // 显示对应的配置面板
|
||||||
|
|
@ -125,7 +125,8 @@ class OcrSettingsManager {
|
||||||
this.elements.mineruToken.value = localStorage.getItem(this.keys.mineruToken) || '';
|
this.elements.mineruToken.value = localStorage.getItem(this.keys.mineruToken) || '';
|
||||||
}
|
}
|
||||||
if (this.elements.mineruWorkerUrl) {
|
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) {
|
if (this.elements.mineruEnableOcr) {
|
||||||
this.elements.mineruEnableOcr.checked = localStorage.getItem(this.keys.mineruEnableOcr) !== 'false';
|
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) || '';
|
this.elements.doc2xToken.value = localStorage.getItem(this.keys.doc2xToken) || '';
|
||||||
}
|
}
|
||||||
if (this.elements.doc2xWorkerUrl) {
|
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) {
|
if (this.elements.doc2xFormulaMode) {
|
||||||
this.elements.doc2xFormulaMode.value = localStorage.getItem(this.keys.doc2xFormulaMode) || 'dollar';
|
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.mineruWorkerUrl, (cfg.workerUrl || '').replace(/\/+$/, ''));
|
||||||
localStorage.setItem(this.keys.mineruToken, cfg.token || '');
|
localStorage.setItem(this.keys.mineruToken, cfg.token || '');
|
||||||
localStorage.setItem(this.keys.workerAuthKey, cfg.authKey || '');
|
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.mineruEnableOcr, cfg.enableOcr !== false);
|
||||||
localStorage.setItem(this.keys.mineruEnableFormula, cfg.enableFormula !== false);
|
localStorage.setItem(this.keys.mineruEnableFormula, cfg.enableFormula !== false);
|
||||||
localStorage.setItem(this.keys.mineruEnableTable, cfg.enableTable !== 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.doc2xWorkerUrl, (cfg.workerUrl || '').replace(/\/+$/, ''));
|
||||||
localStorage.setItem(this.keys.doc2xToken, cfg.token || '');
|
localStorage.setItem(this.keys.doc2xToken, cfg.token || '');
|
||||||
localStorage.setItem(this.keys.workerAuthKey, cfg.authKey || '');
|
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.doc2xFormulaMode, cfg.formulaMode || 'dollar');
|
||||||
localStorage.setItem(this.keys.doc2xExportFormat, cfg.exportFormat || '');
|
localStorage.setItem(this.keys.doc2xExportFormat, cfg.exportFormat || '');
|
||||||
}
|
}
|
||||||
|
|
@ -474,7 +476,7 @@ class OcrSettingsManager {
|
||||||
* @returns {Object} 配置对象
|
* @returns {Object} 配置对象
|
||||||
*/
|
*/
|
||||||
getCurrentConfig() {
|
getCurrentConfig() {
|
||||||
const engine = localStorage.getItem(this.keys.engine) || 'mistral';
|
const engine = localStorage.getItem(this.keys.engine) || 'mineru';
|
||||||
|
|
||||||
switch (engine) {
|
switch (engine) {
|
||||||
case 'none':
|
case 'none':
|
||||||
|
|
@ -514,12 +516,13 @@ class OcrSettingsManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'mineru':
|
case 'mineru':
|
||||||
|
const mineruDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
|
||||||
return {
|
return {
|
||||||
engine: 'mineru',
|
engine: 'mineru',
|
||||||
token: localStorage.getItem(this.keys.mineruToken) || '',
|
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) || '',
|
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',
|
enableOcr: localStorage.getItem(this.keys.mineruEnableOcr) !== 'false',
|
||||||
enableFormula: localStorage.getItem(this.keys.mineruEnableFormula) !== 'false',
|
enableFormula: localStorage.getItem(this.keys.mineruEnableFormula) !== 'false',
|
||||||
enableTable: localStorage.getItem(this.keys.mineruEnableTable) !== 'false',
|
enableTable: localStorage.getItem(this.keys.mineruEnableTable) !== 'false',
|
||||||
|
|
@ -527,12 +530,13 @@ class OcrSettingsManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'doc2x':
|
case 'doc2x':
|
||||||
|
const doc2xDefaultUrl = (typeof window !== 'undefined' && window.ProxyConfig) ? window.ProxyConfig.getProxyUrl() : '/api';
|
||||||
return {
|
return {
|
||||||
engine: 'doc2x',
|
engine: 'doc2x',
|
||||||
token: localStorage.getItem(this.keys.doc2xToken) || '',
|
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) || '',
|
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',
|
formulaMode: localStorage.getItem(this.keys.doc2xFormulaMode) || 'dollar',
|
||||||
exportFormat: localStorage.getItem(this.keys.doc2xExportFormat) || ''
|
exportFormat: localStorage.getItem(this.keys.doc2xExportFormat) || ''
|
||||||
};
|
};
|
||||||
|
|
@ -563,17 +567,17 @@ class OcrSettingsManager {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'mineru':
|
case 'mineru':
|
||||||
// 前端透传模式需要 Token,Worker 配置模式不需要
|
// 后端转发模式(tokenMode 不是 'frontend')不需要前端配置 Token
|
||||||
if (config.tokenMode === 'frontend' && !config.token) {
|
if (config.tokenMode === 'frontend' && !config.token) {
|
||||||
return { valid: false, message: '请配置 MinerU Token(前端透传模式)' };
|
return { valid: false, message: '请配置 MinerU Token(前端透传模式)' };
|
||||||
}
|
}
|
||||||
if (!config.workerUrl) {
|
if (config.tokenMode === 'frontend' && !config.workerUrl) {
|
||||||
return { valid: false, message: '请配置 MinerU Worker URL' };
|
return { valid: false, message: '请配置 MinerU Worker URL' };
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'doc2x':
|
case 'doc2x':
|
||||||
// 前端透传模式需要 Token,Worker 配置模式不需要
|
// 后端转发模式(tokenMode 不是 'frontend')不需要前端配置 Token
|
||||||
if (config.tokenMode === 'frontend' && !config.token) {
|
if (config.tokenMode === 'frontend' && !config.token) {
|
||||||
return { valid: false, message: '请配置 Doc2X Token(前端透传模式)' };
|
return { valid: false, message: '请配置 Doc2X Token(前端透传模式)' };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,6 @@
|
||||||
|
|
||||||
// Settings Link
|
// Settings Link
|
||||||
originalSettingsLink: '#settings-link'
|
originalSettingsLink: '#settings-link'
|
||||||
},
|
|
||||||
logos: {
|
|
||||||
full: '../../public/h_with_name.svg',
|
|
||||||
pure: '../../public/pure.svg'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -146,18 +142,16 @@
|
||||||
* @param {boolean} collapsed - 是否折叠
|
* @param {boolean} collapsed - 是否折叠
|
||||||
*/
|
*/
|
||||||
function setSidebarCollapsed(collapsed) {
|
function setSidebarCollapsed(collapsed) {
|
||||||
if (!elements.appSidebar || !elements.sidebarLogo) return;
|
if (!elements.appSidebar) return;
|
||||||
|
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
elements.appSidebar.classList.add('collapsed');
|
elements.appSidebar.classList.add('collapsed');
|
||||||
elements.sidebarLogo.src = CONFIG.logos.pure;
|
|
||||||
// 切换图标为"打开"图标
|
// 切换图标为"打开"图标
|
||||||
if (elements.sidebarToggleIcon) {
|
if (elements.sidebarToggleIcon) {
|
||||||
elements.sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-open');
|
elements.sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-open');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
elements.appSidebar.classList.remove('collapsed');
|
elements.appSidebar.classList.remove('collapsed');
|
||||||
elements.sidebarLogo.src = CONFIG.logos.full;
|
|
||||||
// 切换图标为"关闭"图标
|
// 切换图标为"关闭"图标
|
||||||
if (elements.sidebarToggleIcon) {
|
if (elements.sidebarToggleIcon) {
|
||||||
elements.sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-close');
|
elements.sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-close');
|
||||||
|
|
|
||||||
|
|
@ -131,18 +131,18 @@
|
||||||
const hasPdfFiles = effectiveFiles.some(file => file.name.toLowerCase().endsWith('.pdf'));
|
const hasPdfFiles = effectiveFiles.some(file => file.name.toLowerCase().endsWith('.pdf'));
|
||||||
|
|
||||||
// 检查 OCR 引擎与所需配置
|
// 检查 OCR 引擎与所需配置
|
||||||
let ocrEngine = 'mistral';
|
let ocrEngine = 'mineru';
|
||||||
let ocrConfigValid = true;
|
let ocrConfigValid = true;
|
||||||
let ocrConfigMessage = '';
|
let ocrConfigMessage = '';
|
||||||
try {
|
try {
|
||||||
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
|
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 检查配置是否完整
|
// 使用 validateConfig 检查配置是否完整
|
||||||
const validation = window.ocrSettingsManager.validateConfig();
|
const validation = window.ocrSettingsManager.validateConfig();
|
||||||
ocrConfigValid = validation.valid;
|
ocrConfigValid = validation.valid;
|
||||||
ocrConfigMessage = validation.message;
|
ocrConfigMessage = validation.message;
|
||||||
} else {
|
} else {
|
||||||
ocrEngine = localStorage.getItem('ocrEngine') || 'mistral';
|
ocrEngine = localStorage.getItem('ocrEngine') || 'mineru';
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -186,13 +186,13 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// 检查当前 OCR 引擎配置
|
// 检查当前 OCR 引擎配置
|
||||||
let currentOcrEngine = 'mistral';
|
let currentOcrEngine = 'mineru';
|
||||||
let currentOcrConfigured = false;
|
let currentOcrConfigured = false;
|
||||||
try {
|
try {
|
||||||
if (window.ocrSettingsManager && typeof window.ocrSettingsManager.getCurrentConfig === 'function') {
|
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 {
|
} else {
|
||||||
currentOcrEngine = localStorage.getItem('ocrEngine') || 'mistral';
|
currentOcrEngine = localStorage.getItem('ocrEngine') || 'mineru';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentOcrEngine === 'none' || currentOcrEngine === 'local') {
|
if (currentOcrEngine === 'none' || currentOcrEngine === 'local') {
|
||||||
|
|
|
||||||
|
|
@ -60,9 +60,10 @@
|
||||||
*/
|
*/
|
||||||
function renderMinerUConfig(container) {
|
function renderMinerUConfig(container) {
|
||||||
// 从 localStorage 加载配置
|
// 从 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 authKey = localStorage.getItem('ocrWorkerAuthKey') || '';
|
||||||
const tokenMode = localStorage.getItem('ocrMinerUTokenMode') || 'frontend';
|
const tokenMode = localStorage.getItem('ocrMinerUTokenMode') || 'backend';
|
||||||
const token = localStorage.getItem('ocrMinerUToken') || '';
|
const token = localStorage.getItem('ocrMinerUToken') || '';
|
||||||
|
|
||||||
const configDiv = document.createElement('div');
|
const configDiv = document.createElement('div');
|
||||||
|
|
@ -336,9 +337,10 @@
|
||||||
*/
|
*/
|
||||||
function renderDoc2XConfig(container) {
|
function renderDoc2XConfig(container) {
|
||||||
// 从 localStorage 加载配置
|
// 从 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 authKey = localStorage.getItem('ocrWorkerAuthKey') || '';
|
||||||
const tokenMode = localStorage.getItem('ocrDoc2XTokenMode') || 'frontend';
|
const tokenMode = localStorage.getItem('ocrDoc2XTokenMode') || 'backend';
|
||||||
const token = localStorage.getItem('ocrDoc2XToken') || '';
|
const token = localStorage.getItem('ocrDoc2XToken') || '';
|
||||||
|
|
||||||
const configDiv = document.createElement('div');
|
const configDiv = document.createElement('div');
|
||||||
|
|
|
||||||
132
login.html
132
login.html
|
|
@ -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 |
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
|
|
@ -30,7 +30,7 @@ See https://github.com/adobe-type-tools/cmap-resources
|
||||||
<!-- This snippet is used in production (included from viewer.html) -->
|
<!-- This snippet is used in production (included from viewer.html) -->
|
||||||
<link rel="resource" type="application/l10n" href="locale/locale.properties">
|
<link rel="resource" type="application/l10n" href="locale/locale.properties">
|
||||||
<!-- 引入tailwindcss -->
|
<!-- 引入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>
|
<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 |
|
|
@ -9,9 +9,6 @@ async function main() {
|
||||||
const admin = await request(app).get('/admin');
|
const admin = await request(app).get('/admin');
|
||||||
console.log('ADMIN', admin.status, admin.headers['content-type']);
|
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) {
|
if (health.status !== 200) {
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -344,11 +344,6 @@ app.use('/api/prompt-pool', promptPoolRoutes);
|
||||||
|
|
||||||
// ==================== 前端路由(SPA) ====================
|
// ==================== 前端路由(SPA) ====================
|
||||||
|
|
||||||
// 登录页需显式返回 login.html,避免被通配符 * 误回退到 index.html
|
|
||||||
app.get('/login.html', (req, res) => {
|
|
||||||
res.sendFile(join(rootPath, 'login.html'));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 管理员面板
|
// 管理员面板
|
||||||
app.get('/admin*', (req, res) => {
|
app.get('/admin*', (req, res) => {
|
||||||
res.sendFile(join(rootPath, 'admin/index.html'));
|
res.sendFile(join(rootPath, 'admin/index.html'));
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>配图编辑器 - draw.io</title>
|
<title>配图编辑器 - draw.io</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg">
|
|
||||||
<style>
|
<style>
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
|
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title>历史详情</title>
|
<title>历史详情</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg" />
|
|
||||||
<!-- CDN 性能优化:DNS 预连接 - 使用淘宝 npmmirror 镜像加速 -->
|
<!-- CDN 性能优化:DNS 预连接 - 使用淘宝 npmmirror 镜像加速 -->
|
||||||
<link rel="dns-prefetch" href="https://registry.npmmirror.com" />
|
<link rel="dns-prefetch" href="https://registry.npmmirror.com" />
|
||||||
<link rel="preconnect" href="https://registry.npmmirror.com" crossorigin />
|
<link rel="preconnect" href="https://registry.npmmirror.com" crossorigin />
|
||||||
|
|
@ -105,8 +104,8 @@
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="immersive-pending">
|
<body class="immersive-pending">
|
||||||
<!-- Standard Immersive Mode Toggle Button -->
|
<!-- Standard Immersive Mode Toggle Button 进入沉浸式布局-->
|
||||||
<button id="toggle-immersive-btn" class="tiny-round-btn hidden" style="display: none;" title="进入沉浸式布局">
|
<button id="toggle-immersive-btn" class="tiny-round-btn disvisible hidden" style="display: none;" title="">
|
||||||
<i class="fas fa-expand-alt"></i>
|
<i class="fas fa-expand-alt"></i>
|
||||||
<!-- Default icon for entering immersive mode -->
|
<!-- Default icon for entering immersive mode -->
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -190,12 +189,6 @@
|
||||||
<aside class="app-sidebar" id="appSidebar">
|
<aside class="app-sidebar" id="appSidebar">
|
||||||
<!-- 侧边栏头部 -->
|
<!-- 侧边栏头部 -->
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<!-- <img
|
|
||||||
src="../../public/h_with_name.svg"
|
|
||||||
alt="Paper Burner X"
|
|
||||||
class="sidebar-logo"
|
|
||||||
id="sidebarLogo"
|
|
||||||
/> -->
|
|
||||||
<!-- 占位元素 -->
|
<!-- 占位元素 -->
|
||||||
<span></span>
|
<span></span>
|
||||||
<!-- 桌面端收起按钮 -->
|
<!-- 桌面端收起按钮 -->
|
||||||
|
|
@ -329,11 +322,6 @@
|
||||||
<!-- 移动端顶部栏 -->
|
<!-- 移动端顶部栏 -->
|
||||||
<div class="mobile-header">
|
<div class="mobile-header">
|
||||||
<div class="mobile-header-title">
|
<div class="mobile-header-title">
|
||||||
<img
|
|
||||||
src="../../public/pure.svg"
|
|
||||||
alt="PBX"
|
|
||||||
class="mobile-header-logo"
|
|
||||||
/>
|
|
||||||
<span>文档详情</span>
|
<span>文档详情</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="mobile-menu-btn" id="mobileMenuBtn">
|
<button class="mobile-menu-btn" id="mobileMenuBtn">
|
||||||
|
|
@ -829,6 +817,8 @@
|
||||||
<script src="../../js/storage/storage.js"></script>
|
<script src="../../js/storage/storage.js"></script>
|
||||||
<!-- 结构化重试需要 API/Translation/Structured 翻译模块 -->
|
<!-- 结构化重试需要 API/Translation/Structured 翻译模块 -->
|
||||||
<script src="../../js/api/api.js"></script>
|
<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/translation.js"></script>
|
||||||
<script src="../../js/process/mineru-structured-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/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>
|
<script src="https://registry.npmmirror.com/jspdf/2.5.1/files/dist/jspdf.umd.min.js"></script>
|
||||||
|
|
||||||
<!-- PDF.js for structured translation PDF comparison -->
|
<!-- 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 src="../../public/pdfjs/build/pdf.js"></script>
|
||||||
<script>
|
<script>
|
||||||
if (typeof pdfjsLib !== "undefined") {
|
if (typeof pdfjsLib !== "undefined") {
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc =
|
// 使用绝对路径确保 Worker 可以正确加载
|
||||||
"https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
|
const baseUrl = window.location.origin + window.location.pathname.split('/views/')[0];
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = baseUrl + "/public/pdfjs/build/pdf.worker.js";
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Phase 3.5: 性能优化配置(必须在所有 chatbot 模块之前加载) -->
|
<!-- Phase 3.5: 性能优化配置(必须在所有 chatbot 模块之前加载) -->
|
||||||
<script src="../../js/chatbot/config/performance-config.js"></script>
|
<script src="../../js/chatbot/config/performance-config.js"></script>
|
||||||
<!-- 历史页 Chatbot 代理配置:使用阿里云百炼平台,保护 API Key -->
|
<!-- 历史页 Chatbot 代理配置:使用阿里云百炼平台,保护 API Key -->
|
||||||
|
<script src="../../js/config/proxy-config.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// 全局配置:启用代理模式,使用本地代理服务器
|
// 全局配置:启用代理模式,使用 ProxyConfig 自动检测环境
|
||||||
window.PBX_PROXY_MODE = "proxy";
|
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";
|
window.PBX_LLM_PROVIDER = "aliyun";
|
||||||
// 默认模型:qwen-plus(推荐),其他可选:qwen-turbo、qwen-max、qwen-long-context
|
// 默认模型:qwen-plus(推荐),其他可选:qwen-turbo、qwen-max、qwen-long-context
|
||||||
|
|
@ -889,10 +883,10 @@
|
||||||
window.PBX_LLM_MODEL,
|
window.PBX_LLM_MODEL,
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
<!-- 直接向 localhost:3456 发送对话消息的函数 -->
|
<!-- 通过代理发送对话消息的函数 -->
|
||||||
<script>
|
<script>
|
||||||
/**
|
/**
|
||||||
* 直接向 http://localhost:3456 发送对话消息
|
* 通过代理服务器发送对话消息
|
||||||
* @param {string} userMessage - 用户消息内容
|
* @param {string} userMessage - 用户消息内容
|
||||||
* @param {Array} [conversationHistory=[]] - 对话历史 [{role, content}, ...]
|
* @param {Array} [conversationHistory=[]] - 对话历史 [{role, content}, ...]
|
||||||
* @param {Object} [options={}] - 可选配置
|
* @param {Object} [options={}] - 可选配置
|
||||||
|
|
@ -903,7 +897,8 @@
|
||||||
conversationHistory = [],
|
conversationHistory = [],
|
||||||
options = {},
|
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 model = options.model || window.PBX_LLM_MODEL || "qwen-plus";
|
||||||
const temperature = options.temperature || 0.7;
|
const temperature = options.temperature || 0.7;
|
||||||
const maxTokens = options.maxTokens || 2048;
|
const maxTokens = options.maxTokens || 2048;
|
||||||
|
|
@ -961,7 +956,8 @@
|
||||||
onChunk,
|
onChunk,
|
||||||
options = {},
|
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 model = options.model || window.PBX_LLM_MODEL || "qwen-plus";
|
||||||
const temperature = options.temperature || 0.7;
|
const temperature = options.temperature || 0.7;
|
||||||
const maxTokens = options.maxTokens || 2048;
|
const maxTokens = options.maxTokens || 2048;
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,11 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Paper Burner X - 开源的、在浏览器中即开即用的 AI 工作站,专为扫除海量的 PDF 文献、复杂的公式和跨语言的障碍。支持前端 Agent 驱动的智能检索、高性能批量处理和完全本地化部署。">
|
<meta name="description" content="Paper Burner X - 开源的、在浏览器中即开即用的 AI 工作站,专为扫除海量的 PDF 文献、复杂的公式和跨语言的障碍。支持前端 Agent 驱动的智能检索、高性能批量处理和完全本地化部署。">
|
||||||
<title>Paper Burner X - 开源 AI 文献工作站</title>
|
<title>Paper Burner X - 开源 AI 文献工作站</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg">
|
|
||||||
<!-- Tailwind CSS & 依赖库 -->
|
<!-- Tailwind CSS & 依赖库 -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<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>
|
<script src="https://gcore.jsdelivr.net/npm/iconify-icon@2.0.0/dist/iconify-icon.min.js"></script>
|
||||||
<!-- GitHub Stars 统一获取模块 -->
|
<!-- GitHub Stars 统一获取模块 -->
|
||||||
<script src="../../js/utils/github-stars.js"></script>
|
<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>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/3.2.1/anime.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
/* 谷歌字体 Inter (更专业、现代) */
|
/* 谷歌字体 Inter (更专业、现代) */
|
||||||
|
|
@ -559,7 +556,7 @@
|
||||||
<header id="mainHeader" class="glass-header">
|
<header id="mainHeader" class="glass-header">
|
||||||
<!-- Left: Logo -->
|
<!-- Left: Logo -->
|
||||||
<div class="header-logo flex-shrink-0">
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Center: Update Pill (Optional, can be hidden if needed) -->
|
<!-- Center: Update Pill (Optional, can be hidden if needed) -->
|
||||||
|
|
@ -594,8 +591,8 @@
|
||||||
|
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<section class="hero-section pt-12 md:pt-16 min-h-[85vh] flex flex-col justify-center">
|
<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">
|
<div class="mb-6 md:mb-8">
|
||||||
<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">
|
<h2 class="text-3xl sm:text-4xl md:text-5xl font-bold text-gray-900 mx-auto">Paper Burner X</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hero Carousel Container -->
|
<!-- Hero Carousel Container -->
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>思维导图 - Markmap</title>
|
<title>思维导图 - Markmap</title>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<link rel="icon" type="image/svg+xml" href="../../public/pure.svg">
|
|
||||||
|
|
||||||
<!-- Markmap 依赖 -->
|
<!-- Markmap 依赖 -->
|
||||||
<script src="https://gcore.jsdelivr.net/npm/d3@7"></script>
|
<script src="https://gcore.jsdelivr.net/npm/d3@7"></script>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue