feat: 完善知识库、策略预览与OnlyOffice集成,增强后端启动依赖检查

This commit is contained in:
caoxiaozhu
2026-05-09 05:59:46 +00:00
parent 1d3ac5c2e0
commit d9133193e8
31 changed files with 5534 additions and 5343 deletions

View File

@@ -90,6 +90,10 @@ fi
ENV_OVERRIDE_SERVER_HOST_SET=false ENV_OVERRIDE_SERVER_HOST_SET=false
ENV_OVERRIDE_POSTGRES_HOST_SET=false ENV_OVERRIDE_POSTGRES_HOST_SET=false
ENV_OVERRIDE_DATABASE_URL_SET=false ENV_OVERRIDE_DATABASE_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=false
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=false
if [ "${SERVER_HOST+x}" = x ]; then if [ "${SERVER_HOST+x}" = x ]; then
ENV_OVERRIDE_SERVER_HOST_SET=true ENV_OVERRIDE_SERVER_HOST_SET=true
@@ -106,6 +110,26 @@ if [ "${DATABASE_URL+x}" = x ]; then
ENV_OVERRIDE_DATABASE_URL="$DATABASE_URL" ENV_OVERRIDE_DATABASE_URL="$DATABASE_URL"
fi fi
if [ "${ONLYOFFICE_ENABLED+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=true
ENV_OVERRIDE_ONLYOFFICE_ENABLED="$ONLYOFFICE_ENABLED"
fi
if [ "${ONLYOFFICE_PUBLIC_URL+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=true
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL="$ONLYOFFICE_PUBLIC_URL"
fi
if [ "${ONLYOFFICE_BACKEND_URL+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=true
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL="$ONLYOFFICE_BACKEND_URL"
fi
if [ "${ONLYOFFICE_JWT_SECRET+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=true
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET="$ONLYOFFICE_JWT_SECRET"
fi
set -a set -a
. "$ROOT_ENV_FILE" . "$ROOT_ENV_FILE"
set +a set +a
@@ -122,6 +146,22 @@ if [ "$ENV_OVERRIDE_DATABASE_URL_SET" = true ]; then
DATABASE_URL="$ENV_OVERRIDE_DATABASE_URL" DATABASE_URL="$ENV_OVERRIDE_DATABASE_URL"
fi fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET" = true ]; then
ONLYOFFICE_ENABLED="$ENV_OVERRIDE_ONLYOFFICE_ENABLED"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET" = true ]; then
ONLYOFFICE_PUBLIC_URL="$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET" = true ]; then
ONLYOFFICE_BACKEND_URL="$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET" = true ]; then
ONLYOFFICE_JWT_SECRET="$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET"
fi
SERVER_HOST="${SERVER_HOST:-0.0.0.0}" SERVER_HOST="${SERVER_HOST:-0.0.0.0}"
SERVER_PORT="${SERVER_PORT:-8000}" SERVER_PORT="${SERVER_PORT:-8000}"
DEFAULT_SERVER_RELOAD="false" DEFAULT_SERVER_RELOAD="false"
@@ -189,7 +229,7 @@ run_bootstrap_python() {
} }
dependencies_ready() { dependencies_ready() {
"$PYTHON_BIN" -c "import fastapi, uvicorn, sqlalchemy, alembic, pydantic_settings" >/dev/null 2>&1 "$PYTHON_BIN" -c "import alembic, dotenv, email_validator, fastapi, jwt, psycopg, pydantic_settings, sqlalchemy, uvicorn" >/dev/null 2>&1
} }
pip_ready() { pip_ready() {

View File

@@ -2,16 +2,16 @@
"version": 1, "version": 1,
"documents": [ "documents": [
{ {
"id": "fde293670eac4ae2b90a80eeb9f27b5b", "id": "8af9350f0e02488aaf0df2001286b764",
"folder": "财务知识库", "folder": "财务知识库",
"original_name": "差旅费季度报销258878.xlsx", "original_name": "差旅费季度报销258878.xlsx",
"stored_name": "fde293670eac4ae2b90a80eeb9f27b5b__差旅费季度报销258878.xlsx", "stored_name": "8af9350f0e02488aaf0df2001286b764__差旅费季度报销258878.xlsx",
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"extension": "xlsx", "extension": "xlsx",
"size_bytes": 11123, "size_bytes": 11123,
"sha256": "ea02e59d3a22a4a02284172acce3fd4c6367a26f1a4fd196dc4f65afed1bd4c5", "sha256": "ea02e59d3a22a4a02284172acce3fd4c6367a26f1a4fd196dc4f65afed1bd4c5",
"created_at": "2026-05-09T03:33:44.101489+00:00", "created_at": "2026-05-09T05:46:24.699125+00:00",
"updated_at": "2026-05-09T03:33:44.101489+00:00", "updated_at": "2026-05-09T05:46:24.699125+00:00",
"uploaded_by": "admin", "uploaded_by": "admin",
"version_number": 1 "version_number": 1
} }

View File

@@ -0,0 +1,40 @@
from __future__ import annotations
from pathlib import Path
import os
import stat
import subprocess
def test_dependencies_ready_fails_when_jwt_is_missing(tmp_path: Path) -> None:
fake_python = tmp_path / "fake-python.sh"
fake_python.write_text(
"""#!/usr/bin/env bash
if [ "$1" = "-c" ]; then
case "$2" in
*jwt*) exit 1 ;;
*) exit 0 ;;
esac
fi
exit 0
""",
encoding="utf-8",
)
fake_python.chmod(fake_python.stat().st_mode | stat.S_IEXEC)
script_path = Path(__file__).resolve().parents[1] / "server_start.sh"
script_prefix = script_path.read_text(encoding="utf-8").split('case "$MODE" in', 1)[0]
command = f"""{script_prefix}
PYTHON_BIN="{fake_python}"
dependencies_ready
"""
result = subprocess.run(
["bash", "-c", command],
capture_output=True,
text=True,
env={**os.environ, "MODE": "test"},
cwd=script_path.parent,
check=False,
)
assert result.returncode != 0

View File

@@ -38,6 +38,10 @@ fi
ENV_OVERRIDE_WEB_HOST_SET=false ENV_OVERRIDE_WEB_HOST_SET=false
ENV_OVERRIDE_SERVER_HOST_SET=false ENV_OVERRIDE_SERVER_HOST_SET=false
ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=false
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=false
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=false
if [ "${WEB_HOST+x}" = x ]; then if [ "${WEB_HOST+x}" = x ]; then
ENV_OVERRIDE_WEB_HOST_SET=true ENV_OVERRIDE_WEB_HOST_SET=true
@@ -49,6 +53,26 @@ if [ "${SERVER_HOST+x}" = x ]; then
ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST" ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST"
fi fi
if [ "${ONLYOFFICE_ENABLED+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=true
ENV_OVERRIDE_ONLYOFFICE_ENABLED="$ONLYOFFICE_ENABLED"
fi
if [ "${ONLYOFFICE_PUBLIC_URL+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=true
ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL="$ONLYOFFICE_PUBLIC_URL"
fi
if [ "${ONLYOFFICE_BACKEND_URL+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=true
ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL="$ONLYOFFICE_BACKEND_URL"
fi
if [ "${ONLYOFFICE_JWT_SECRET+x}" = x ]; then
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=true
ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET="$ONLYOFFICE_JWT_SECRET"
fi
set -a set -a
. "$ENV_FILE" . "$ENV_FILE"
set +a set +a
@@ -61,6 +85,22 @@ if [ "$ENV_OVERRIDE_SERVER_HOST_SET" = true ]; then
SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST" SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST"
fi fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET" = true ]; then
ONLYOFFICE_ENABLED="$ENV_OVERRIDE_ONLYOFFICE_ENABLED"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET" = true ]; then
ONLYOFFICE_PUBLIC_URL="$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET" = true ]; then
ONLYOFFICE_BACKEND_URL="$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL"
fi
if [ "$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET" = true ]; then
ONLYOFFICE_JWT_SECRET="$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET"
fi
SERVER_STARTUP_TIMEOUT="${SERVER_STARTUP_TIMEOUT:-300}" SERVER_STARTUP_TIMEOUT="${SERVER_STARTUP_TIMEOUT:-300}"
SETUP_COMPLETED="${SETUP_COMPLETED:-false}" SETUP_COMPLETED="${SETUP_COMPLETED:-false}"
APP_DEBUG="${APP_DEBUG:-true}" APP_DEBUG="${APP_DEBUG:-true}"

View File

@@ -190,10 +190,10 @@
<div class="preview-viewer"> <div class="preview-viewer">
<div v-if="previewLoading" class="preview-status">正在加载预览...</div> <div v-if="previewLoading" class="preview-status">正在加载预览...</div>
<div v-else-if="previewError" class="preview-status error">{{ previewError }}</div> <div v-else-if="previewError" class="preview-status error">{{ previewError }}</div>
<div v-else-if="selectedDocument.previewKind === 'pdf' && previewBlobUrl" class="preview-embed-wrap"> <div v-else-if="previewMode === 'pdf' && previewBlobUrl" class="preview-embed-wrap">
<iframe :src="previewBlobUrl" class="preview-embed" title="PDF 预览"></iframe> <iframe :src="previewBlobUrl" class="preview-embed" title="PDF 预览"></iframe>
</div> </div>
<div v-else-if="selectedDocument.previewKind === 'image' && previewBlobUrl" class="preview-image-wrap"> <div v-else-if="previewMode === 'image' && previewBlobUrl" class="preview-image-wrap">
<img :src="previewBlobUrl" :alt="selectedDocument.name" class="preview-image" /> <img :src="previewBlobUrl" :alt="selectedDocument.name" class="preview-image" />
</div> </div>
<div v-else-if="shouldUseOnlyOffice" class="onlyoffice-preview-wrap"> <div v-else-if="shouldUseOnlyOffice" class="onlyoffice-preview-wrap">
@@ -201,7 +201,7 @@
<div v-else-if="onlyOfficeError" class="preview-status error">{{ onlyOfficeError }}</div> <div v-else-if="onlyOfficeError" class="preview-status error">{{ onlyOfficeError }}</div>
<div v-else :id="onlyOfficeHostId" class="onlyoffice-preview-host"></div> <div v-else :id="onlyOfficeHostId" class="onlyoffice-preview-host"></div>
</div> </div>
<div v-else-if="selectedDocument.previewKind === 'table'" class="excel-preview-wrap"> <div v-else-if="previewMode === 'table'" class="excel-preview-wrap">
<div v-if="selectedDocument.previewPages.length > 1" class="excel-sheet-tabs" role="tablist" aria-label="Excel 工作表页签"> <div v-if="selectedDocument.previewPages.length > 1" class="excel-sheet-tabs" role="tablist" aria-label="Excel 工作表页签">
<button <button
v-for="(page, index) in selectedDocument.previewPages" v-for="(page, index) in selectedDocument.previewPages"

View File

@@ -17,6 +17,7 @@ import {
buildPreviewMetaLine, buildPreviewMetaLine,
buildPreviewSecondaryMetaLine buildPreviewSecondaryMetaLine
} from './policiesPreviewFormatters.js' } from './policiesPreviewFormatters.js'
import { canUseOnlyOfficePreview, resolveKnowledgePreviewMode } from './knowledgePreviewMode.js'
function triggerFileDownload(blob, filename) { function triggerFileDownload(blob, filename) {
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
@@ -27,12 +28,6 @@ const anchor = document.createElement('a')
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
} }
const ONLYOFFICE_EXTENSIONS = new Set(['docx', 'xlsx', 'pptx'])
function supportsOnlyOfficePreview(document) {
return ONLYOFFICE_EXTENSIONS.has(String(document?.extension || '').toLowerCase())
}
export default { export default {
name: 'PoliciesView', name: 'PoliciesView',
emits: ['summary-change'], emits: ['summary-change'],
@@ -58,6 +53,7 @@ export default {
const previewError = ref('') const previewError = ref('')
const onlyOfficeLoading = ref(false) const onlyOfficeLoading = ref(false)
const onlyOfficeError = ref('') const onlyOfficeError = ref('')
const onlyOfficeAvailable = ref(false)
const onlyOfficeEditor = ref(null) const onlyOfficeEditor = ref(null)
const onlyOfficeHostId = ref('knowledge-onlyoffice-preview') const onlyOfficeHostId = ref('knowledge-onlyoffice-preview')
const currentPreviewPageIndex = ref(0) const currentPreviewPageIndex = ref(0)
@@ -95,7 +91,12 @@ export default {
const previewSecondaryMetaLine = computed(() => const previewSecondaryMetaLine = computed(() =>
buildPreviewSecondaryMetaLine(selectedDocument.value, activePreviewPage.value) buildPreviewSecondaryMetaLine(selectedDocument.value, activePreviewPage.value)
) )
const shouldUseOnlyOffice = computed(() => supportsOnlyOfficePreview(selectedDocument.value)) const previewMode = computed(() =>
resolveKnowledgePreviewMode(selectedDocument.value, {
onlyOfficeAvailable: onlyOfficeAvailable.value
})
)
const shouldUseOnlyOffice = computed(() => previewMode.value === 'onlyoffice')
const excelPreviewTable = computed(() => const excelPreviewTable = computed(() =>
selectedDocument.value?.previewKind === 'table' selectedDocument.value?.previewKind === 'table'
? buildExcelPreviewTable(activePreviewPage.value) ? buildExcelPreviewTable(activePreviewPage.value)
@@ -119,6 +120,7 @@ export default {
async function mountOnlyOfficeEditor(documentId) { async function mountOnlyOfficeEditor(documentId) {
onlyOfficeLoading.value = true onlyOfficeLoading.value = true
onlyOfficeError.value = '' onlyOfficeError.value = ''
onlyOfficeAvailable.value = false
destroyOnlyOfficeEditor() destroyOnlyOfficeEditor()
try { try {
@@ -133,8 +135,11 @@ export default {
onlyOfficeHostId.value = `knowledge-onlyoffice-preview-${documentId}` onlyOfficeHostId.value = `knowledge-onlyoffice-preview-${documentId}`
await nextTick() await nextTick()
onlyOfficeEditor.value = new window.DocsAPI.DocEditor(onlyOfficeHostId.value, payload.config) onlyOfficeEditor.value = new window.DocsAPI.DocEditor(onlyOfficeHostId.value, payload.config)
onlyOfficeAvailable.value = true
return true
} catch (error) { } catch (error) {
onlyOfficeError.value = error.message || 'ONLYOFFICE 预览加载失败。' onlyOfficeError.value = error.message || 'ONLYOFFICE 预览加载失败。'
return false
} finally { } finally {
onlyOfficeLoading.value = false onlyOfficeLoading.value = false
} }
@@ -172,6 +177,7 @@ export default {
previewLoading.value = true previewLoading.value = true
previewError.value = '' previewError.value = ''
onlyOfficeError.value = '' onlyOfficeError.value = ''
onlyOfficeAvailable.value = false
revokePreviewBlob() revokePreviewBlob()
destroyOnlyOfficeEditor() destroyOnlyOfficeEditor()
@@ -180,9 +186,11 @@ export default {
selectedDocument.value = payload selectedDocument.value = payload
currentPreviewPageIndex.value = 0 currentPreviewPageIndex.value = 0
if (supportsOnlyOfficePreview(payload)) { if (canUseOnlyOfficePreview(payload)) {
await mountOnlyOfficeEditor(documentId) await mountOnlyOfficeEditor(documentId)
} else if (payload.previewKind === 'pdf' || payload.previewKind === 'image') { }
if (payload.previewKind === 'pdf' || payload.previewKind === 'image') {
const blob = await fetchKnowledgeDocumentBlob(documentId, 'inline') const blob = await fetchKnowledgeDocumentBlob(documentId, 'inline')
previewBlobUrl.value = URL.createObjectURL(blob) previewBlobUrl.value = URL.createObjectURL(blob)
} }
@@ -290,6 +298,7 @@ export default {
revokePreviewBlob() revokePreviewBlob()
destroyOnlyOfficeEditor() destroyOnlyOfficeEditor()
onlyOfficeError.value = '' onlyOfficeError.value = ''
onlyOfficeAvailable.value = false
} }
function selectPreviewPage(index) { function selectPreviewPage(index) {
@@ -315,6 +324,7 @@ export default {
onBeforeUnmount(() => { onBeforeUnmount(() => {
revokePreviewBlob() revokePreviewBlob()
destroyOnlyOfficeEditor()
}) })
return { return {
@@ -340,6 +350,7 @@ export default {
onlyOfficeError, onlyOfficeError,
onlyOfficeHostId, onlyOfficeHostId,
onlyOfficeLoading, onlyOfficeLoading,
previewMode,
previewMetaLine, previewMetaLine,
previewSecondaryMetaLine, previewSecondaryMetaLine,
previewBlobUrl, previewBlobUrl,

View File

@@ -0,0 +1,21 @@
const ONLYOFFICE_EXTENSIONS = new Set(['docx', 'xlsx', 'pptx'])
function supportsOnlyOfficePreview(document) {
return ONLYOFFICE_EXTENSIONS.has(String(document?.extension || '').toLowerCase())
}
export function resolveKnowledgePreviewMode(document, options = {}) {
if (!document) {
return 'none'
}
if (supportsOnlyOfficePreview(document) && options.onlyOfficeAvailable) {
return 'onlyoffice'
}
return document.previewKind || 'unsupported'
}
export function canUseOnlyOfficePreview(document) {
return supportsOnlyOfficePreview(document)
}

View File

@@ -0,0 +1,39 @@
import assert from 'node:assert/strict'
import { resolveKnowledgePreviewMode } from '../src/views/scripts/knowledgePreviewMode.js'
function testPrefersOnlyOfficeForSupportedOfficeFileWhenAvailable() {
const document = {
extension: 'xlsx',
previewKind: 'table'
}
assert.equal(resolveKnowledgePreviewMode(document, { onlyOfficeAvailable: true }), 'onlyoffice')
}
function testFallsBackToStructuredPreviewForOfficeFileWhenOnlyOfficeUnavailable() {
const document = {
extension: 'xlsx',
previewKind: 'table'
}
assert.equal(resolveKnowledgePreviewMode(document, { onlyOfficeAvailable: false }), 'table')
}
function testUsesPreviewKindForNonOnlyOfficeFile() {
const document = {
extension: 'pdf',
previewKind: 'pdf'
}
assert.equal(resolveKnowledgePreviewMode(document, { onlyOfficeAvailable: false }), 'pdf')
}
function run() {
testPrefersOnlyOfficeForSupportedOfficeFileWhenAvailable()
testFallsBackToStructuredPreviewForOfficeFileWhenOnlyOfficeUnavailable()
testUsesPreviewKindForNonOnlyOfficeFile()
console.log('knowledge preview mode tests passed')
}
run()