feat: 集成 OSS 存储与 AI 助手 PDF 支持
- OSS 对象存储集成 - 文档上传后保存OSS URL 和 Key 到数据库,删除文档时同步清理 OSS 文件 - AI 助手 PDF 支持 - 历史详情页将 PDF base64 转换为 File 对象,供 AI 助手直接使用 - 认证状态同步 - 后端 /health 返回 authDisabled 状态,前端自动适配无认证模式 - 添加TODO: 生产环境必须移除的配置
This commit is contained in:
parent
96f136a9bc
commit
6c78f08769
|
|
@ -1,2 +1,67 @@
|
||||||
{
|
{
|
||||||
|
"window.title": "${activeEditorShort}${separator}${separator}deer-flow/frontend",
|
||||||
|
"todo-tree.regex.regex": "((%|#|//|<!--|\\{/\\*|^\\s*\\*)\\s*($TAGS)|^\\s*- \\[ \\])",
|
||||||
|
"todo-tree.general.tags": [
|
||||||
|
"TODO:",
|
||||||
|
"BUG:",
|
||||||
|
"TAG:",
|
||||||
|
"DONE:",
|
||||||
|
"MARK:",
|
||||||
|
"TEST:",
|
||||||
|
"XXX:"
|
||||||
|
],
|
||||||
|
"todo-tree.regex.regexCaseSensitive": false,
|
||||||
|
"todo-tree.highlights.defaultHighlight": {
|
||||||
|
"foreground": "#000000",
|
||||||
|
"background": "#fff700",
|
||||||
|
"icon": "check",
|
||||||
|
"rulerColour": "#fff700",
|
||||||
|
"type": "tag",
|
||||||
|
"iconColour": "#fff700"
|
||||||
|
},
|
||||||
|
"todo-tree.highlights.customHighlight": {
|
||||||
|
"TODO:": {
|
||||||
|
"icon": "todo",
|
||||||
|
"background": "#fff700",
|
||||||
|
"rulerColour": "#fff700",
|
||||||
|
"iconColour": "#fff700"
|
||||||
|
},
|
||||||
|
"BUG:": {
|
||||||
|
"background": "#eb5c5c",
|
||||||
|
"icon": "bug",
|
||||||
|
"rulerColour": "#eb5c5c",
|
||||||
|
"iconColour": "#eb5c5c"
|
||||||
|
},
|
||||||
|
"TAG:": {
|
||||||
|
"background": "#38b2f4",
|
||||||
|
"icon": "tag",
|
||||||
|
"rulerColour": "#38b2f4",
|
||||||
|
"iconColour": "#38b2f4",
|
||||||
|
"rulerLane": "full"
|
||||||
|
},
|
||||||
|
"DONE:": {
|
||||||
|
"background": "#5eec95",
|
||||||
|
"icon": "check",
|
||||||
|
"rulerColour": "#5eec95",
|
||||||
|
"iconColour": "#5eec95"
|
||||||
|
},
|
||||||
|
"MARK:": {
|
||||||
|
"background": "#f90",
|
||||||
|
"icon": "note",
|
||||||
|
"rulerColour": "#f90",
|
||||||
|
"iconColour": "#f90"
|
||||||
|
},
|
||||||
|
"TEST:": {
|
||||||
|
"background": "#df7be6",
|
||||||
|
"icon": "flame",
|
||||||
|
"rulerColour": "#df7be6",
|
||||||
|
"iconColour": "#df7be6"
|
||||||
|
},
|
||||||
|
"XXX:": {
|
||||||
|
"background": "#d65d8e",
|
||||||
|
"icon": "versions",
|
||||||
|
"rulerColour": "#d65d8e",
|
||||||
|
"iconColour": "#d65d8e"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +90,7 @@ async function uploadToMistral(fileToProcess, mistralKey) {
|
||||||
*
|
*
|
||||||
* @param {File|Blob} fileToProcess - 需要上传的 File 或 Blob 对象。
|
* @param {File|Blob} fileToProcess - 需要上传的 File 或 Blob 对象。
|
||||||
* @param {string} fileName - 文件名。
|
* @param {string} fileName - 文件名。
|
||||||
* @returns {Promise<string>} 上传成功后返回 OSS 的公网访问 URL。
|
* @returns {Promise<{url: string, key: string}>} 上传成功后返回 OSS 的公网访问 URL 和 key。
|
||||||
*/
|
*/
|
||||||
async function uploadFileToOssViaProxy(fileToProcess, fileName) {
|
async function uploadFileToOssViaProxy(fileToProcess, fileName) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
@ -115,7 +115,7 @@ async function uploadFileToOssViaProxy(fileToProcess, fileName) {
|
||||||
if (!resultData || !resultData.url) {
|
if (!resultData || !resultData.url) {
|
||||||
throw new Error('上传成功但未返回有效的URL');
|
throw new Error('上传成功但未返回有效的URL');
|
||||||
}
|
}
|
||||||
return resultData.url;
|
return { url: resultData.url, key: resultData.key };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -110,8 +110,31 @@ window.ChatbotActions = {
|
||||||
|
|
||||||
// 调用上传
|
// 调用上传
|
||||||
console.log('[handleProvidePdf] 开始上传到 OSS...');
|
console.log('[handleProvidePdf] 开始上传到 OSS...');
|
||||||
const ossUrl = await window.uploadFileToOssViaProxy(pdfFileContent, `${currentDocName}`);
|
const { url: ossUrl, key: ossKey } = await window.uploadFileToOssViaProxy(pdfFileContent, `${currentDocName}`);
|
||||||
console.log('[handleProvidePdf] OSS 上传成功, URL:', ossUrl);
|
console.log('[handleProvidePdf] OSS 上传成功, URL:', ossUrl, 'Key:', ossKey);
|
||||||
|
|
||||||
|
// 保存 OSS URL 到数据库
|
||||||
|
if (window.storageAdapter && typeof window.storageAdapter.saveOssUrl === 'function') {
|
||||||
|
try {
|
||||||
|
await window.storageAdapter.saveOssUrl(dbDocId, ossUrl, ossKey);
|
||||||
|
console.log('[handleProvidePdf] OSS URL 已保存到数据库');
|
||||||
|
} catch (saveErr) {
|
||||||
|
console.warn('[handleProvidePdf] 保存 OSS URL 到数据库失败:', saveErr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 直接调用 API
|
||||||
|
try {
|
||||||
|
const apiUrl = (window.PBX_PROXY_BASE_URL || '/api') + `/documents/${dbDocId}/oss`;
|
||||||
|
await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ossUrl, ossKey })
|
||||||
|
});
|
||||||
|
console.log('[handleProvidePdf] OSS URL 已通过 API 保存');
|
||||||
|
} catch (apiErr) {
|
||||||
|
console.warn('[handleProvidePdf] 通过 API 保存 OSS URL 失败:', apiErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 构建包含 file_url 的消息内容(智谱 AI 格式)
|
// 构建包含 file_url 的消息内容(智谱 AI 格式)
|
||||||
const userMessageContent = [
|
const userMessageContent = [
|
||||||
|
|
|
||||||
|
|
@ -273,14 +273,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
if (historyClearMode === 'record') {
|
if (historyClearMode === 'record') {
|
||||||
if (pendingDeleteRecordId) {
|
if (pendingDeleteRecordId) {
|
||||||
removeFolderAssignmentForRecord(pendingDeleteRecordId);
|
removeFolderAssignmentForRecord(pendingDeleteRecordId);
|
||||||
|
// 使用 storageAdapter 删除(支持后端模式)
|
||||||
|
if (window.storageAdapter && typeof window.storageAdapter.deleteResultFromDB === 'function') {
|
||||||
|
await window.storageAdapter.deleteResultFromDB(pendingDeleteRecordId);
|
||||||
|
} else if (typeof deleteResultFromDB === 'function') {
|
||||||
await deleteResultFromDB(pendingDeleteRecordId);
|
await deleteResultFromDB(pendingDeleteRecordId);
|
||||||
|
}
|
||||||
await renderHistoryList();
|
await renderHistoryList();
|
||||||
if (typeof showNotification === 'function') {
|
if (typeof showNotification === 'function') {
|
||||||
showNotification('历史记录已删除。', 'success');
|
showNotification('历史记录已删除。', 'success');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// 使用 storageAdapter 清空(支持后端模式)
|
||||||
|
if (window.storageAdapter && typeof window.storageAdapter.clearAllResultsFromDB === 'function') {
|
||||||
|
await window.storageAdapter.clearAllResultsFromDB();
|
||||||
|
} else if (typeof clearAllResultsFromDB === 'function') {
|
||||||
await clearAllResultsFromDB();
|
await clearAllResultsFromDB();
|
||||||
|
}
|
||||||
clearFolderAssignments();
|
clearFolderAssignments();
|
||||||
historyUIState.activeFolder = 'all';
|
historyUIState.activeFolder = 'all';
|
||||||
historyUIState.searchQuery = '';
|
historyUIState.searchQuery = '';
|
||||||
|
|
@ -2194,7 +2204,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const records = batchCache[batchId];
|
const records = batchCache[batchId];
|
||||||
if (!records || !records.length) return;
|
if (!records || !records.length) return;
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
|
// 使用 storageAdapter 删除(支持后端模式)
|
||||||
|
if (window.storageAdapter && typeof window.storageAdapter.deleteResultFromDB === 'function') {
|
||||||
|
await window.storageAdapter.deleteResultFromDB(record.id);
|
||||||
|
} else if (typeof deleteResultFromDB === 'function') {
|
||||||
await deleteResultFromDB(record.id);
|
await deleteResultFromDB(record.id);
|
||||||
|
}
|
||||||
removeFolderAssignmentForRecord(record.id);
|
removeFolderAssignmentForRecord(record.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,30 @@ async function renderDetail() {
|
||||||
|
|
||||||
const hasOriginalPdf = !!(meta.originalPdfBase64);
|
const hasOriginalPdf = !!(meta.originalPdfBase64);
|
||||||
|
|
||||||
|
// ========== 将 PDF base64 转换为 File 对象,供 AI 助手的 PDF 功能使用 ==========
|
||||||
|
if (hasOriginalPdf && data.name) {
|
||||||
|
try {
|
||||||
|
// 使用 history_detail_scripts.js 中定义的 base64ToFile 函数
|
||||||
|
if (typeof base64ToFile === 'function') {
|
||||||
|
window.currentOriginalFile = base64ToFile(meta.originalPdfBase64, data.name);
|
||||||
|
console.log('[renderDetail] 已设置 window.currentOriginalFile,供 AI 助手使用');
|
||||||
|
} else {
|
||||||
|
// 如果 base64ToFile 不可用,手动转换
|
||||||
|
const base64Data = meta.originalPdfBase64;
|
||||||
|
const byteString = atob(base64Data);
|
||||||
|
const ab = new ArrayBuffer(byteString.length);
|
||||||
|
const ia = new Uint8Array(ab);
|
||||||
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
|
ia[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
window.currentOriginalFile = new File([ab], data.name, { type: 'application/pdf' });
|
||||||
|
console.log('[renderDetail] 已设置 window.currentOriginalFile(手动转换),供 AI 助手使用');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[renderDetail] 转换 PDF base64 为 File 失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[renderDetail] 检测结果:', {
|
console.log('[renderDetail] 检测结果:', {
|
||||||
hasMinerUStructuredData,
|
hasMinerUStructuredData,
|
||||||
hasOriginalPdf,
|
hasOriginalPdf,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ let DEPLOYMENT_MODE = (getQueryModeOverride() || (window.ENV_DEPLOYMENT_MODE &&
|
||||||
|
|
||||||
const API_BASE_URL = window.ENV_API_BASE_URL || '/api';
|
const API_BASE_URL = window.ENV_API_BASE_URL || '/api';
|
||||||
|
|
||||||
|
// 后端认证状态(当 AUTH_DISABLED=true 时,前端也需要绕过认证检查)
|
||||||
|
let backendAuthDisabled = false;
|
||||||
|
|
||||||
async function autoDetectBackendAvailability(timeoutMs = 900) {
|
async function autoDetectBackendAvailability(timeoutMs = 900) {
|
||||||
// file:// 明确无后端
|
// file:// 明确无后端
|
||||||
if (window.location.protocol === 'file:') return false;
|
if (window.location.protocol === 'file:') return false;
|
||||||
|
|
@ -37,7 +40,20 @@ async function autoDetectBackendAvailability(timeoutMs = 900) {
|
||||||
const id = setTimeout(() => controller.abort(), timeoutMs);
|
const id = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
const res = await fetch(`${API_BASE_URL}/health`, { signal: controller.signal, cache: 'no-store' });
|
const res = await fetch(`${API_BASE_URL}/health`, { signal: controller.signal, cache: 'no-store' });
|
||||||
clearTimeout(id);
|
clearTimeout(id);
|
||||||
return res.ok;
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
// 如果后端禁用认证,设置假 token 并标记状态
|
||||||
|
// TODO: 生产环境必须移除此逻辑,强制要求用户登录认证
|
||||||
|
if (data.authDisabled) {
|
||||||
|
backendAuthDisabled = true;
|
||||||
|
// 设置假 token 让 AuthManager.isAuthenticated() 返回 true
|
||||||
|
if (!localStorage.getItem('auth_token')) {
|
||||||
|
localStorage.setItem('auth_token', 'dev-mode-no-auth');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -253,6 +269,36 @@ class BackendStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存 OSS URL 到文档(供 AI 助手使用)
|
||||||
|
* @param {string} id - 文档 ID
|
||||||
|
* @param {string} ossUrl - OSS 公网访问 URL
|
||||||
|
* @param {string} ossKey - OSS 对象 key(用于删除)
|
||||||
|
*/
|
||||||
|
async saveOssUrl(id, ossUrl, ossKey) {
|
||||||
|
try {
|
||||||
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
// 前端模式:保存到 IndexedDB metadata
|
||||||
|
const doc = await this._fallbackTo('getResultFromDB', id);
|
||||||
|
if (doc) {
|
||||||
|
doc.metadata = doc.metadata || {};
|
||||||
|
doc.metadata.ossUrl = ossUrl;
|
||||||
|
doc.metadata.ossKey = ossKey;
|
||||||
|
await this._fallbackTo('saveResultToDB', doc);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.fetchAPI(`/documents/${id}/oss`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ossUrl, ossKey })
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save OSS URL to document:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async clearAllResultsFromDB() {
|
async clearAllResultsFromDB() {
|
||||||
try {
|
try {
|
||||||
if (!AuthManager.isAuthenticated()) {
|
if (!AuthManager.isAuthenticated()) {
|
||||||
|
|
@ -477,6 +523,20 @@ class StorageAdapterFactory {
|
||||||
getResultFromDB: window.getResultFromDB,
|
getResultFromDB: window.getResultFromDB,
|
||||||
deleteResultFromDB: window.deleteResultFromDB,
|
deleteResultFromDB: window.deleteResultFromDB,
|
||||||
clearAllResultsFromDB: window.clearAllResultsFromDB,
|
clearAllResultsFromDB: window.clearAllResultsFromDB,
|
||||||
|
// OSS URL 保存(前端模式:保存到 IndexedDB metadata)
|
||||||
|
saveOssUrl: async function(id, ossUrl, ossKey) {
|
||||||
|
try {
|
||||||
|
const doc = await window.getResultFromDB(id);
|
||||||
|
if (doc) {
|
||||||
|
doc.metadata = doc.metadata || {};
|
||||||
|
doc.metadata.ossUrl = ossUrl;
|
||||||
|
doc.metadata.ossKey = ossKey;
|
||||||
|
await window.saveResultToDB(doc);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[StorageAdapter] saveOssUrl failed in frontend mode:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
loadGlossarySets: window.loadGlossarySets,
|
loadGlossarySets: window.loadGlossarySets,
|
||||||
saveGlossarySets: window.saveGlossarySets,
|
saveGlossarySets: window.saveGlossarySets,
|
||||||
saveAnnotationToDB: window.saveAnnotationToDB,
|
saveAnnotationToDB: window.saveAnnotationToDB,
|
||||||
|
|
|
||||||
|
|
@ -224,12 +224,12 @@
|
||||||
// 检查是否为移动端设备
|
// 检查是否为移动端设备
|
||||||
if (window.innerWidth <= 700) {
|
if (window.innerWidth <= 700) {
|
||||||
console.warn('拒绝在移动端(≤700px)进入沉浸式布局');
|
console.warn('拒绝在移动端(≤700px)进入沉浸式布局');
|
||||||
// 即使不进入沉浸模式,也需要显示页面
|
// 移动端:显示普通布局
|
||||||
document.documentElement.classList.remove('immersive-pending');
|
document.documentElement.classList.remove('immersive-pending');
|
||||||
document.documentElement.classList.add('immersive-ready');
|
document.documentElement.classList.add('immersive-ready');
|
||||||
document.body.classList.remove('immersive-pending');
|
document.body.classList.remove('immersive-pending');
|
||||||
document.body.classList.add('immersive-ready');
|
document.body.classList.add('immersive-ready');
|
||||||
return;
|
return true; // 移动端算作成功
|
||||||
}
|
}
|
||||||
|
|
||||||
reQueryDynamicElements();
|
reQueryDynamicElements();
|
||||||
|
|
@ -256,12 +256,12 @@
|
||||||
immersiveChatbotArea: !!immersiveChatbotArea
|
immersiveChatbotArea: !!immersiveChatbotArea
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 【严格检查】如果元素缺失,返回 false 表示失败,不显示页面
|
||||||
|
// 调用者应该重试直到成功
|
||||||
if (missingElements.length > 0) {
|
if (missingElements.length > 0) {
|
||||||
console.warn('Immersive mode elements not found:', missingElements.join(', '));
|
console.error('[enterImmersiveMode] 严重错误:沉浸模式元素缺失:', missingElements.join(', '));
|
||||||
// 元素缺失时也需要显示页面
|
console.error('[enterImmersiveMode] 拒绝显示普通布局,等待重试...');
|
||||||
document.body.classList.remove('immersive-pending');
|
return false; // 返回失败,不显示页面
|
||||||
document.body.classList.add('immersive-ready');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isImmersiveActive = true;
|
isImmersiveActive = true;
|
||||||
|
|
@ -430,6 +430,8 @@
|
||||||
initializeTocDockResizer();
|
initializeTocDockResizer();
|
||||||
localStorage.setItem(LS_IMMERSIVE_KEY, 'true');
|
localStorage.setItem(LS_IMMERSIVE_KEY, 'true');
|
||||||
document.dispatchEvent(new CustomEvent('immersiveModeEntered'));
|
document.dispatchEvent(new CustomEvent('immersiveModeEntered'));
|
||||||
|
|
||||||
|
return true; // 成功进入沉浸模式
|
||||||
}
|
}
|
||||||
// 禁用退出沉浸模式功能 - 页面始终保持在沉浸式布局
|
// 禁用退出沉浸模式功能 - 页面始终保持在沉浸式布局
|
||||||
function exitImmersiveMode() {
|
function exitImmersiveMode() {
|
||||||
|
|
@ -513,8 +515,8 @@
|
||||||
|
|
||||||
function mainInit() {
|
function mainInit() {
|
||||||
if (!initializeDomElements()) {
|
if (!initializeDomElements()) {
|
||||||
console.warn('Immersive layout core static elements not found on DOMContentLoaded. Retrying shortly...');
|
console.warn('[ImmersiveLayout] 核心静态元素未找到,200ms 后重试...');
|
||||||
setTimeout(mainInit, 500);
|
setTimeout(mainInit, 200);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -534,25 +536,29 @@
|
||||||
// }
|
// }
|
||||||
// window.addEventListener('resize', handleWindowResize);
|
// window.addEventListener('resize', handleWindowResize);
|
||||||
|
|
||||||
// 强制始终进入沉浸模式(忽略 localStorage 状态)
|
// 【严格沉浸模式】必须成功进入沉浸模式才显示页面
|
||||||
// 页面默认沉浸式布局,且无法退出
|
|
||||||
const shouldEnterImmersive = true;
|
|
||||||
|
|
||||||
console.log('[ImmersiveLayout] 强制进入沉浸模式');
|
console.log('[ImmersiveLayout] 强制进入沉浸模式');
|
||||||
|
|
||||||
// 检查是否为移动端,如果是则不进入沉浸模式
|
// 检查是否为移动端,如果是则显示普通布局
|
||||||
if (window.innerWidth <= 700) {
|
if (window.innerWidth <= 700) {
|
||||||
console.log('检测到移动端设备,不进入沉浸式布局');
|
console.log('[ImmersiveLayout] 检测到移动端设备,显示普通布局');
|
||||||
// 显示页面
|
|
||||||
document.documentElement.classList.remove('immersive-pending');
|
document.documentElement.classList.remove('immersive-pending');
|
||||||
document.documentElement.classList.add('immersive-ready');
|
document.documentElement.classList.add('immersive-ready');
|
||||||
document.body.classList.remove('immersive-pending');
|
document.body.classList.remove('immersive-pending');
|
||||||
document.body.classList.add('immersive-ready');
|
document.body.classList.add('immersive-ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试进入沉浸模式(静默模式,无动画)
|
||||||
|
const success = enterImmersiveMode({ silent: true });
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
console.log('[ImmersiveLayout] 成功进入沉浸模式');
|
||||||
} else {
|
} else {
|
||||||
// 立即进入沉浸模式(静默模式,无动画)
|
// 失败时持续重试,不显示页面
|
||||||
console.log('[ImmersiveLayout] 准备进入沉浸模式, isImmersiveActive:', isImmersiveActive);
|
console.error('[ImmersiveLayout] 进入沉浸模式失败,300ms 后重试...');
|
||||||
enterImmersiveMode({ silent: true });
|
setTimeout(mainInit, 300);
|
||||||
console.log('[ImmersiveLayout] enterImmersiveMode 调用完成, isImmersiveActive:', isImmersiveActive);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Immersive layout logic initialized.');
|
console.log('Immersive layout logic initialized.');
|
||||||
|
|
|
||||||
|
|
@ -63,12 +63,14 @@ app.get('/health', async (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
database: 'connected',
|
database: 'connected',
|
||||||
|
authDisabled: process.env.AUTH_DISABLED === 'true',
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
database: 'disconnected',
|
database: 'disconnected',
|
||||||
|
authDisabled: process.env.AUTH_DISABLED === 'true',
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import fetch from 'node-fetch';
|
||||||
import { prisma } from '../db/client.js';
|
import { prisma } from '../db/client.js';
|
||||||
|
|
||||||
// 是否禁用认证(开发/测试模式)
|
// 是否禁用认证(开发/测试模式)
|
||||||
|
// TODO: 生产环境必须移除此配置,强制要求认证
|
||||||
const AUTH_DISABLED = process.env.AUTH_DISABLED === 'true' || process.env.NODE_ENV === 'test';
|
const AUTH_DISABLED = process.env.AUTH_DISABLED === 'true' || process.env.NODE_ENV === 'test';
|
||||||
|
|
||||||
// 默认认证 API URL
|
// 默认认证 API URL
|
||||||
|
|
@ -17,6 +18,7 @@ const tokenCache = new Map();
|
||||||
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
|
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
|
||||||
|
|
||||||
// 测试用户(认证禁用时使用)
|
// 测试用户(认证禁用时使用)
|
||||||
|
// TODO: 生产环境必须移除此测试用户
|
||||||
const TEST_USER = {
|
const TEST_USER = {
|
||||||
id: 'test-user-001',
|
id: 'test-user-001',
|
||||||
username: 'testuser',
|
username: 'testuser',
|
||||||
|
|
@ -114,6 +116,7 @@ export async function ensureUserExists(userId, name) {
|
||||||
*/
|
*/
|
||||||
export async function requireAuth(req, res, next) {
|
export async function requireAuth(req, res, next) {
|
||||||
// 认证禁用模式(开发/测试)
|
// 认证禁用模式(开发/测试)
|
||||||
|
// TODO: 生产环境必须移除此分支,强制要求认证
|
||||||
if (AUTH_DISABLED) {
|
if (AUTH_DISABLED) {
|
||||||
await ensureUserExists(TEST_USER.id, TEST_USER.nickname);
|
await ensureUserExists(TEST_USER.id, TEST_USER.nickname);
|
||||||
req.user = TEST_USER;
|
req.user = TEST_USER;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,10 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"dev": "node --watch server.js"
|
"dev": "node --watch server.js",
|
||||||
|
"prisma:migrate": "npx prisma migrate dev",
|
||||||
|
"prisma:generate": "npx prisma generate",
|
||||||
|
"prisma:studio": "npx prisma studio"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"paper-burner",
|
"paper-burner",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "documents" ADD COLUMN "ossKey" TEXT,
|
||||||
|
ADD COLUMN "ossUrl" TEXT;
|
||||||
|
|
@ -141,6 +141,10 @@ model Document {
|
||||||
fileType String
|
fileType String
|
||||||
filePath String? // 如果启用文件上传存储
|
filePath String? // 如果启用文件上传存储
|
||||||
|
|
||||||
|
// OSS 存储(供 AI 助手使用)
|
||||||
|
ossUrl String? // OSS 公网访问 URL
|
||||||
|
ossKey String? // OSS 对象 key(用于删除)
|
||||||
|
|
||||||
// 处理状态
|
// 处理状态
|
||||||
status DocStatus @default(PENDING)
|
status DocStatus @default(PENDING)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,54 @@
|
||||||
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { prisma } from '../db/client.js';
|
import { prisma } from '../db/client.js';
|
||||||
|
import OSS from 'ali-oss';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// ==================== OSS 客户端初始化 ====================
|
||||||
|
let ossClient = null;
|
||||||
|
|
||||||
|
function initOssClient() {
|
||||||
|
const region = process.env.OSS_REGION;
|
||||||
|
const accessKeyId = process.env.OSS_ACCESS_KEY_ID;
|
||||||
|
const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET;
|
||||||
|
const bucket = process.env.OSS_BUCKET || process.env.OSS_BUCKET_NAME;
|
||||||
|
|
||||||
|
if (region && accessKeyId && accessKeySecret && bucket) {
|
||||||
|
let normalizedRegion = region;
|
||||||
|
if (!region.startsWith('oss-')) {
|
||||||
|
normalizedRegion = `oss-${region}`;
|
||||||
|
}
|
||||||
|
ossClient = new OSS({
|
||||||
|
region: normalizedRegion,
|
||||||
|
accessKeyId,
|
||||||
|
accessKeySecret,
|
||||||
|
bucket,
|
||||||
|
secure: true
|
||||||
|
});
|
||||||
|
console.log(`[Documents] OSS client initialized with bucket: ${bucket}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initOssClient();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 OSS 文件
|
||||||
|
* @param {string} ossKey - OSS 对象 key
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function deleteOssFile(ossKey) {
|
||||||
|
if (!ossClient || !ossKey) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ossClient.delete(ossKey);
|
||||||
|
console.log(`[OSS] Deleted file: ${ossKey}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[OSS] Failed to delete file ${ossKey}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 允许的状态值白名单
|
// 允许的状态值白名单
|
||||||
const ALLOWED_STATUSES = ['PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'];
|
const ALLOWED_STATUSES = ['PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'];
|
||||||
|
|
||||||
|
|
@ -313,6 +358,11 @@ router.delete('/:id', async (req, res, next) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (document) {
|
if (document) {
|
||||||
|
// 删除 OSS 文件(如果有)
|
||||||
|
if (document.ossKey) {
|
||||||
|
await deleteOssFile(document.ossKey);
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.document.delete({
|
await prisma.document.delete({
|
||||||
where: { id }
|
where: { id }
|
||||||
});
|
});
|
||||||
|
|
@ -324,6 +374,40 @@ router.delete('/:id', async (req, res, next) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 保存 OSS URL 到文档(供 AI 助手使用)
|
||||||
|
router.post('/:id/oss', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { ossUrl, ossKey } = req.body;
|
||||||
|
|
||||||
|
if (!ossUrl) {
|
||||||
|
return res.status(400).json({ error: 'ossUrl is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文档所有权
|
||||||
|
const document = await prisma.document.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
userId: req.user.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!document) {
|
||||||
|
return res.status(404).json({ error: 'Document not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新文档的 OSS 信息
|
||||||
|
await prisma.document.update({
|
||||||
|
where: { id },
|
||||||
|
data: { ossUrl, ossKey }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, ossUrl, ossKey });
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== 标注管理 ====================
|
// ==================== 标注管理 ====================
|
||||||
|
|
||||||
// 保存标注
|
// 保存标注
|
||||||
|
|
|
||||||
|
|
@ -820,6 +820,7 @@ async function handleOssUpload(req, res, origin) {
|
||||||
jsonResponse(res, {
|
jsonResponse(res, {
|
||||||
success: true,
|
success: true,
|
||||||
url: urlToReturn,
|
url: urlToReturn,
|
||||||
|
key: objectName, // 返回 ossKey 用于后续删除
|
||||||
file_name: filePart.filename
|
file_name: filePart.filename
|
||||||
}, 200, origin);
|
}, 200, origin);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,37 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 严格沉浸模式:默认隐藏普通布局 */
|
||||||
|
body.immersive-pending .app-shell {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 只有沉浸模式激活后才显示内容 */
|
||||||
|
body:not(.immersive-active) .app-shell {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 沉浸模式容器默认隐藏,由 JS 控制 */
|
||||||
|
#immersive-layout-container {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 沉浸模式激活后显示容器 */
|
||||||
|
body.immersive-active #immersive-layout-container {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移动端例外:显示普通布局 */
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
body.immersive-pending .app-shell,
|
||||||
|
body:not(.immersive-active) .app-shell {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
body.immersive-ready .app-shell {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
// 在 DOM 解析前就添加类,避免闪烁
|
// 在 DOM 解析前就添加类,避免闪烁
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue