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 {string} fileName - 文件名。
|
||||
* @returns {Promise<string>} 上传成功后返回 OSS 的公网访问 URL。
|
||||
* @returns {Promise<{url: string, key: string}>} 上传成功后返回 OSS 的公网访问 URL 和 key。
|
||||
*/
|
||||
async function uploadFileToOssViaProxy(fileToProcess, fileName) {
|
||||
const formData = new FormData();
|
||||
|
|
@ -115,7 +115,7 @@ async function uploadFileToOssViaProxy(fileToProcess, fileName) {
|
|||
if (!resultData || !resultData.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...');
|
||||
const ossUrl = await window.uploadFileToOssViaProxy(pdfFileContent, `${currentDocName}`);
|
||||
console.log('[handleProvidePdf] OSS 上传成功, URL:', ossUrl);
|
||||
const { url: ossUrl, key: ossKey } = await window.uploadFileToOssViaProxy(pdfFileContent, `${currentDocName}`);
|
||||
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 格式)
|
||||
const userMessageContent = [
|
||||
|
|
|
|||
|
|
@ -273,14 +273,24 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
if (historyClearMode === 'record') {
|
||||
if (pendingDeleteRecordId) {
|
||||
removeFolderAssignmentForRecord(pendingDeleteRecordId);
|
||||
await deleteResultFromDB(pendingDeleteRecordId);
|
||||
// 使用 storageAdapter 删除(支持后端模式)
|
||||
if (window.storageAdapter && typeof window.storageAdapter.deleteResultFromDB === 'function') {
|
||||
await window.storageAdapter.deleteResultFromDB(pendingDeleteRecordId);
|
||||
} else if (typeof deleteResultFromDB === 'function') {
|
||||
await deleteResultFromDB(pendingDeleteRecordId);
|
||||
}
|
||||
await renderHistoryList();
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('历史记录已删除。', 'success');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await clearAllResultsFromDB();
|
||||
// 使用 storageAdapter 清空(支持后端模式)
|
||||
if (window.storageAdapter && typeof window.storageAdapter.clearAllResultsFromDB === 'function') {
|
||||
await window.storageAdapter.clearAllResultsFromDB();
|
||||
} else if (typeof clearAllResultsFromDB === 'function') {
|
||||
await clearAllResultsFromDB();
|
||||
}
|
||||
clearFolderAssignments();
|
||||
historyUIState.activeFolder = 'all';
|
||||
historyUIState.searchQuery = '';
|
||||
|
|
@ -2194,7 +2204,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const records = batchCache[batchId];
|
||||
if (!records || !records.length) return;
|
||||
for (const record of records) {
|
||||
await deleteResultFromDB(record.id);
|
||||
// 使用 storageAdapter 删除(支持后端模式)
|
||||
if (window.storageAdapter && typeof window.storageAdapter.deleteResultFromDB === 'function') {
|
||||
await window.storageAdapter.deleteResultFromDB(record.id);
|
||||
} else if (typeof deleteResultFromDB === 'function') {
|
||||
await deleteResultFromDB(record.id);
|
||||
}
|
||||
removeFolderAssignmentForRecord(record.id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,6 +245,30 @@ async function renderDetail() {
|
|||
|
||||
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] 检测结果:', {
|
||||
hasMinerUStructuredData,
|
||||
hasOriginalPdf,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ let DEPLOYMENT_MODE = (getQueryModeOverride() || (window.ENV_DEPLOYMENT_MODE &&
|
|||
|
||||
const API_BASE_URL = window.ENV_API_BASE_URL || '/api';
|
||||
|
||||
// 后端认证状态(当 AUTH_DISABLED=true 时,前端也需要绕过认证检查)
|
||||
let backendAuthDisabled = false;
|
||||
|
||||
async function autoDetectBackendAvailability(timeoutMs = 900) {
|
||||
// file:// 明确无后端
|
||||
if (window.location.protocol === 'file:') return false;
|
||||
|
|
@ -37,7 +40,20 @@ async function autoDetectBackendAvailability(timeoutMs = 900) {
|
|||
const id = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const res = await fetch(`${API_BASE_URL}/health`, { signal: controller.signal, cache: 'no-store' });
|
||||
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 {
|
||||
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() {
|
||||
try {
|
||||
if (!AuthManager.isAuthenticated()) {
|
||||
|
|
@ -477,6 +523,20 @@ class StorageAdapterFactory {
|
|||
getResultFromDB: window.getResultFromDB,
|
||||
deleteResultFromDB: window.deleteResultFromDB,
|
||||
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,
|
||||
saveGlossarySets: window.saveGlossarySets,
|
||||
saveAnnotationToDB: window.saveAnnotationToDB,
|
||||
|
|
|
|||
|
|
@ -224,12 +224,12 @@
|
|||
// 检查是否为移动端设备
|
||||
if (window.innerWidth <= 700) {
|
||||
console.warn('拒绝在移动端(≤700px)进入沉浸式布局');
|
||||
// 即使不进入沉浸模式,也需要显示页面
|
||||
// 移动端:显示普通布局
|
||||
document.documentElement.classList.remove('immersive-pending');
|
||||
document.documentElement.classList.add('immersive-ready');
|
||||
document.body.classList.remove('immersive-pending');
|
||||
document.body.classList.add('immersive-ready');
|
||||
return;
|
||||
return true; // 移动端算作成功
|
||||
}
|
||||
|
||||
reQueryDynamicElements();
|
||||
|
|
@ -256,12 +256,12 @@
|
|||
immersiveChatbotArea: !!immersiveChatbotArea
|
||||
});
|
||||
|
||||
// 【严格检查】如果元素缺失,返回 false 表示失败,不显示页面
|
||||
// 调用者应该重试直到成功
|
||||
if (missingElements.length > 0) {
|
||||
console.warn('Immersive mode elements not found:', missingElements.join(', '));
|
||||
// 元素缺失时也需要显示页面
|
||||
document.body.classList.remove('immersive-pending');
|
||||
document.body.classList.add('immersive-ready');
|
||||
return;
|
||||
console.error('[enterImmersiveMode] 严重错误:沉浸模式元素缺失:', missingElements.join(', '));
|
||||
console.error('[enterImmersiveMode] 拒绝显示普通布局,等待重试...');
|
||||
return false; // 返回失败,不显示页面
|
||||
}
|
||||
|
||||
isImmersiveActive = true;
|
||||
|
|
@ -430,6 +430,8 @@
|
|||
initializeTocDockResizer();
|
||||
localStorage.setItem(LS_IMMERSIVE_KEY, 'true');
|
||||
document.dispatchEvent(new CustomEvent('immersiveModeEntered'));
|
||||
|
||||
return true; // 成功进入沉浸模式
|
||||
}
|
||||
// 禁用退出沉浸模式功能 - 页面始终保持在沉浸式布局
|
||||
function exitImmersiveMode() {
|
||||
|
|
@ -513,8 +515,8 @@
|
|||
|
||||
function mainInit() {
|
||||
if (!initializeDomElements()) {
|
||||
console.warn('Immersive layout core static elements not found on DOMContentLoaded. Retrying shortly...');
|
||||
setTimeout(mainInit, 500);
|
||||
console.warn('[ImmersiveLayout] 核心静态元素未找到,200ms 后重试...');
|
||||
setTimeout(mainInit, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -534,25 +536,29 @@
|
|||
// }
|
||||
// window.addEventListener('resize', handleWindowResize);
|
||||
|
||||
// 强制始终进入沉浸模式(忽略 localStorage 状态)
|
||||
// 页面默认沉浸式布局,且无法退出
|
||||
const shouldEnterImmersive = true;
|
||||
|
||||
// 【严格沉浸模式】必须成功进入沉浸模式才显示页面
|
||||
console.log('[ImmersiveLayout] 强制进入沉浸模式');
|
||||
|
||||
// 检查是否为移动端,如果是则不进入沉浸模式
|
||||
// 检查是否为移动端,如果是则显示普通布局
|
||||
if (window.innerWidth <= 700) {
|
||||
console.log('检测到移动端设备,不进入沉浸式布局');
|
||||
// 显示页面
|
||||
console.log('[ImmersiveLayout] 检测到移动端设备,显示普通布局');
|
||||
document.documentElement.classList.remove('immersive-pending');
|
||||
document.documentElement.classList.add('immersive-ready');
|
||||
document.body.classList.remove('immersive-pending');
|
||||
document.body.classList.add('immersive-ready');
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试进入沉浸模式(静默模式,无动画)
|
||||
const success = enterImmersiveMode({ silent: true });
|
||||
|
||||
if (success) {
|
||||
console.log('[ImmersiveLayout] 成功进入沉浸模式');
|
||||
} else {
|
||||
// 立即进入沉浸模式(静默模式,无动画)
|
||||
console.log('[ImmersiveLayout] 准备进入沉浸模式, isImmersiveActive:', isImmersiveActive);
|
||||
enterImmersiveMode({ silent: true });
|
||||
console.log('[ImmersiveLayout] enterImmersiveMode 调用完成, isImmersiveActive:', isImmersiveActive);
|
||||
// 失败时持续重试,不显示页面
|
||||
console.error('[ImmersiveLayout] 进入沉浸模式失败,300ms 后重试...');
|
||||
setTimeout(mainInit, 300);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Immersive layout logic initialized.');
|
||||
|
|
|
|||
|
|
@ -63,12 +63,14 @@ app.get('/health', async (req, res) => {
|
|||
res.json({
|
||||
status: 'ok',
|
||||
database: 'connected',
|
||||
authDisabled: process.env.AUTH_DISABLED === 'true',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
database: 'disconnected',
|
||||
authDisabled: process.env.AUTH_DISABLED === 'true',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import fetch from 'node-fetch';
|
|||
import { prisma } from '../db/client.js';
|
||||
|
||||
// 是否禁用认证(开发/测试模式)
|
||||
// TODO: 生产环境必须移除此配置,强制要求认证
|
||||
const AUTH_DISABLED = process.env.AUTH_DISABLED === 'true' || process.env.NODE_ENV === 'test';
|
||||
|
||||
// 默认认证 API URL
|
||||
|
|
@ -17,6 +18,7 @@ const tokenCache = new Map();
|
|||
const CACHE_TTL = 5 * 60 * 1000; // 5 分钟
|
||||
|
||||
// 测试用户(认证禁用时使用)
|
||||
// TODO: 生产环境必须移除此测试用户
|
||||
const TEST_USER = {
|
||||
id: 'test-user-001',
|
||||
username: 'testuser',
|
||||
|
|
@ -114,6 +116,7 @@ export async function ensureUserExists(userId, name) {
|
|||
*/
|
||||
export async function requireAuth(req, res, next) {
|
||||
// 认证禁用模式(开发/测试)
|
||||
// TODO: 生产环境必须移除此分支,强制要求认证
|
||||
if (AUTH_DISABLED) {
|
||||
await ensureUserExists(TEST_USER.id, TEST_USER.nickname);
|
||||
req.user = TEST_USER;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@
|
|||
"type": "module",
|
||||
"scripts": {
|
||||
"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": [
|
||||
"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
|
||||
filePath String? // 如果启用文件上传存储
|
||||
|
||||
// OSS 存储(供 AI 助手使用)
|
||||
ossUrl String? // OSS 公网访问 URL
|
||||
ossKey String? // OSS 对象 key(用于删除)
|
||||
|
||||
// 处理状态
|
||||
status DocStatus @default(PENDING)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,54 @@
|
|||
|
||||
import express from 'express';
|
||||
import { prisma } from '../db/client.js';
|
||||
import OSS from 'ali-oss';
|
||||
|
||||
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'];
|
||||
|
||||
|
|
@ -313,6 +358,11 @@ router.delete('/:id', async (req, res, next) => {
|
|||
});
|
||||
|
||||
if (document) {
|
||||
// 删除 OSS 文件(如果有)
|
||||
if (document.ossKey) {
|
||||
await deleteOssFile(document.ossKey);
|
||||
}
|
||||
|
||||
await prisma.document.delete({
|
||||
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, {
|
||||
success: true,
|
||||
url: urlToReturn,
|
||||
key: objectName, // 返回 ossKey 用于后续删除
|
||||
file_name: filePart.filename
|
||||
}, 200, origin);
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,37 @@
|
|||
opacity: 1;
|
||||
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>
|
||||
<script>
|
||||
// 在 DOM 解析前就添加类,避免闪烁
|
||||
|
|
|
|||
Loading…
Reference in New Issue