feat: 完善知识库、策略预览与OnlyOffice集成,增强后端启动依赖检查
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
40
server/tests/test_server_start_dependencies.py
Normal file
40
server/tests/test_server_start_dependencies.py
Normal 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
|
||||
40
start.sh
40
start.sh
@@ -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}"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
21
web/src/views/scripts/knowledgePreviewMode.js
Normal file
21
web/src/views/scripts/knowledgePreviewMode.js
Normal 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)
|
||||
}
|
||||
39
web/tests/knowledge-preview-mode.test.mjs
Normal file
39
web/tests/knowledge-preview-mode.test.mjs
Normal 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()
|
||||
Reference in New Issue
Block a user