feat: 完善知识库、策略预览与OnlyOffice集成

## 配置与环境
- .env.example: 更新环境变量配置
- docker-compose.yml: 完善Docker编排配置
- docker/README.md: 更新Docker文档

## 后端知识库模块
- endpoints/knowledge.py: 增强知识库API端点
- schemas/knowledge.py: 扩展知识库数据模型
- services/knowledge.py: 完善知识库业务逻辑
- config.py: 优化配置管理
- storage/knowledge/.index.json: 更新知识库索引

## 前端功能
- api.js: 完善API服务层
- knowledge.js: 优化知识库服务
- onlyoffice.js: 新增OnlyOffice文档服务集成
- TopBar.vue: 优化顶部导航栏
- PoliciesView.vue: 完善策略视图
- AppShellRouteView.vue: 新增应用外壳路由视图
- views/scripts/PoliciesView.js: 优化策略脚本
- policiesPreviewFormatters.js: 新增策略预览格式化工具

## 样式
- policies-view.css: 完善策略页样式

## 测试
- api-request.test.mjs: API请求测试
- onlyoffice-service.test.mjs: OnlyOffice服务测试
- policies-preview-formatters.test.mjs: 策略预览格式化测试
This commit is contained in:
caoxiaozhu
2026-05-09 04:25:30 +00:00
parent 619281afc3
commit d9ffa9ce2c
21 changed files with 1469 additions and 508 deletions

View File

@@ -1,4 +1,37 @@
const API_BASE_STORAGE_KEY = 'x-financial-api-base-url'
const AUTH_USER_STORAGE_KEY = 'x-financial-auth-user'
function readCurrentUserHeaders() {
if (typeof window === 'undefined') {
return {}
}
const raw = window.sessionStorage.getItem(AUTH_USER_STORAGE_KEY)
if (!raw) {
return {}
}
try {
const payload = JSON.parse(raw)
const username = String(payload?.username || '').trim()
const name = String(payload?.name || username).trim()
const roleCodes = Array.isArray(payload?.roleCodes) ? payload.roleCodes.filter(Boolean) : []
const isAdmin = Boolean(payload?.isAdmin)
if (!username && !name) {
return {}
}
return {
'x-auth-username': username,
'x-auth-name': name,
'x-auth-role-codes': roleCodes.join(','),
'x-auth-is-admin': String(isAdmin)
}
} catch {
return {}
}
}
function normalizeApiBaseUrl(value) {
return String(value || '/api/v1').replace(/\/$/, '')
@@ -66,22 +99,50 @@ function buildUrl(path) {
}
export async function apiRequest(path, options = {}) {
const {
contentType = 'application/json',
responseType = 'json',
headers: customHeaders,
...fetchOptions
} = options
const headers = {
...readCurrentUserHeaders(),
...(customHeaders || {})
}
if (contentType !== null && typeof headers['Content-Type'] === 'undefined') {
headers['Content-Type'] = contentType
}
let response
try {
response = await fetch(buildUrl(path), {
headers: {
'Content-Type': 'application/json',
...(options.headers || {})
},
...options
...fetchOptions,
headers
})
} catch {
throw new Error('无法连接 FastAPI 后端服务,请确认后端已启动且浏览器可访问后端端口。')
}
let payload = null
if (responseType === 'blob') {
if (!response.ok) {
let payload = null
try {
payload = await response.json()
} catch {
payload = null
}
throw new Error(payload?.detail || '接口请求失败,请稍后重试。')
}
return response.blob()
}
let payload = null
try {
payload = await response.json()
} catch {

View File

@@ -8,6 +8,10 @@ export function fetchKnowledgeDocument(documentId) {
return apiRequest(`/knowledge/documents/${documentId}`)
}
export function fetchKnowledgeOnlyOfficeConfig(documentId) {
return apiRequest(`/knowledge/documents/${documentId}/onlyoffice-config`)
}
export function uploadKnowledgeDocument({ folder, file }) {
return apiRequest(
`/knowledge/documents?folder=${encodeURIComponent(folder)}&filename=${encodeURIComponent(file.name)}`,

View File

@@ -0,0 +1,43 @@
const scriptPromises = new Map()
function normalizeBaseUrl(value) {
return String(value || '').replace(/\/$/, '')
}
export function buildOnlyOfficeScriptUrl(documentServerUrl) {
return `${normalizeBaseUrl(documentServerUrl)}/web-apps/apps/api/documents/api.js`
}
export function loadOnlyOfficeApi(documentServerUrl) {
const scriptUrl = buildOnlyOfficeScriptUrl(documentServerUrl)
if (typeof window === 'undefined') {
return Promise.reject(new Error('ONLYOFFICE 只能在浏览器环境中加载。'))
}
if (window.DocsAPI?.DocEditor) {
return Promise.resolve(window.DocsAPI)
}
if (scriptPromises.has(scriptUrl)) {
return scriptPromises.get(scriptUrl)
}
const promise = new Promise((resolve, reject) => {
const existing = document.querySelector(`script[src="${scriptUrl}"]`)
if (existing) {
existing.addEventListener('load', () => resolve(window.DocsAPI), { once: true })
existing.addEventListener('error', () => reject(new Error('ONLYOFFICE 脚本加载失败。')), { once: true })
return
}
const script = document.createElement('script')
script.src = scriptUrl
script.async = true
script.onload = () => resolve(window.DocsAPI)
script.onerror = () => reject(new Error('ONLYOFFICE 脚本加载失败。'))
document.head.appendChild(script)
})
scriptPromises.set(scriptUrl, promise)
return promise
}