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_POSTGRES_HOST_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
ENV_OVERRIDE_SERVER_HOST_SET=true
@@ -106,6 +110,26 @@ if [ "${DATABASE_URL+x}" = x ]; then
ENV_OVERRIDE_DATABASE_URL="$DATABASE_URL"
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
. "$ROOT_ENV_FILE"
set +a
@@ -122,6 +146,22 @@ if [ "$ENV_OVERRIDE_DATABASE_URL_SET" = true ]; then
DATABASE_URL="$ENV_OVERRIDE_DATABASE_URL"
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_PORT="${SERVER_PORT:-8000}"
DEFAULT_SERVER_RELOAD="false"
@@ -189,7 +229,7 @@ run_bootstrap_python() {
}
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() {

View File

@@ -2,16 +2,16 @@
"version": 1,
"documents": [
{
"id": "fde293670eac4ae2b90a80eeb9f27b5b",
"id": "8af9350f0e02488aaf0df2001286b764",
"folder": "财务知识库",
"original_name": "差旅费季度报销258878.xlsx",
"stored_name": "fde293670eac4ae2b90a80eeb9f27b5b__差旅费季度报销258878.xlsx",
"stored_name": "8af9350f0e02488aaf0df2001286b764__差旅费季度报销258878.xlsx",
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"extension": "xlsx",
"size_bytes": 11123,
"sha256": "ea02e59d3a22a4a02284172acce3fd4c6367a26f1a4fd196dc4f65afed1bd4c5",
"created_at": "2026-05-09T03:33:44.101489+00:00",
"updated_at": "2026-05-09T03:33:44.101489+00:00",
"created_at": "2026-05-09T05:46:24.699125+00:00",
"updated_at": "2026-05-09T05:46:24.699125+00:00",
"uploaded_by": "admin",
"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_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
ENV_OVERRIDE_WEB_HOST_SET=true
@@ -49,6 +53,26 @@ if [ "${SERVER_HOST+x}" = x ]; then
ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST"
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
. "$ENV_FILE"
set +a
@@ -61,6 +85,22 @@ if [ "$ENV_OVERRIDE_SERVER_HOST_SET" = true ]; then
SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST"
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}"
SETUP_COMPLETED="${SETUP_COMPLETED:-false}"
APP_DEBUG="${APP_DEBUG:-true}"

View File

@@ -190,10 +190,10 @@
<div class="preview-viewer">
<div v-if="previewLoading" class="preview-status">正在加载预览...</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>
</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" />
</div>
<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 :id="onlyOfficeHostId" class="onlyoffice-preview-host"></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 工作表页签">
<button
v-for="(page, index) in selectedDocument.previewPages"

View File

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