feat: 完善知识库、策略预览与OnlyOffice集成,增强后端启动依赖检查
This commit is contained in:
@@ -1,157 +1,157 @@
|
||||
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(/\/$/, '')
|
||||
}
|
||||
|
||||
function isLoopbackHost(hostname) {
|
||||
const normalized = String(hostname || '').trim().toLowerCase()
|
||||
return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '0.0.0.0' || normalized === '::1'
|
||||
}
|
||||
|
||||
function resolveBrowserReachableApiBaseUrl(value) {
|
||||
const normalized = normalizeApiBaseUrl(value)
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return normalized
|
||||
}
|
||||
|
||||
try {
|
||||
const apiUrl = new URL(normalized)
|
||||
const browserHost = window.location.hostname
|
||||
|
||||
if (isLoopbackHost(apiUrl.hostname) && browserHost && !isLoopbackHost(browserHost)) {
|
||||
apiUrl.hostname = browserHost
|
||||
return normalizeApiBaseUrl(apiUrl.toString())
|
||||
}
|
||||
} catch {
|
||||
return normalized
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function readStoredApiBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
|
||||
return resolveBrowserReachableApiBaseUrl(window.localStorage.getItem(API_BASE_STORAGE_KEY) || '')
|
||||
}
|
||||
|
||||
let runtimeApiBaseUrl = normalizeApiBaseUrl('/api/v1')
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.removeItem(API_BASE_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export function setRuntimeApiBaseUrl(value) {
|
||||
runtimeApiBaseUrl = resolveBrowserReachableApiBaseUrl(value)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(API_BASE_STORAGE_KEY, runtimeApiBaseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
export function getRuntimeApiBaseUrl() {
|
||||
return runtimeApiBaseUrl
|
||||
}
|
||||
|
||||
function buildUrl(path) {
|
||||
if (!path.startsWith('/')) {
|
||||
return `${runtimeApiBaseUrl}/${path}`
|
||||
}
|
||||
|
||||
return `${runtimeApiBaseUrl}${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), {
|
||||
...fetchOptions,
|
||||
headers
|
||||
})
|
||||
} catch {
|
||||
throw new Error('无法连接 FastAPI 后端服务,请确认后端已启动且浏览器可访问后端端口。')
|
||||
}
|
||||
|
||||
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 {
|
||||
payload = null
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.detail || '接口请求失败,请稍后重试。')
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
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(/\/$/, '')
|
||||
}
|
||||
|
||||
function isLoopbackHost(hostname) {
|
||||
const normalized = String(hostname || '').trim().toLowerCase()
|
||||
return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '0.0.0.0' || normalized === '::1'
|
||||
}
|
||||
|
||||
function resolveBrowserReachableApiBaseUrl(value) {
|
||||
const normalized = normalizeApiBaseUrl(value)
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
return normalized
|
||||
}
|
||||
|
||||
try {
|
||||
const apiUrl = new URL(normalized)
|
||||
const browserHost = window.location.hostname
|
||||
|
||||
if (isLoopbackHost(apiUrl.hostname) && browserHost && !isLoopbackHost(browserHost)) {
|
||||
apiUrl.hostname = browserHost
|
||||
return normalizeApiBaseUrl(apiUrl.toString())
|
||||
}
|
||||
} catch {
|
||||
return normalized
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
function readStoredApiBaseUrl() {
|
||||
if (typeof window === 'undefined') {
|
||||
return ''
|
||||
}
|
||||
|
||||
return resolveBrowserReachableApiBaseUrl(window.localStorage.getItem(API_BASE_STORAGE_KEY) || '')
|
||||
}
|
||||
|
||||
let runtimeApiBaseUrl = normalizeApiBaseUrl('/api/v1')
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.removeItem(API_BASE_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export function setRuntimeApiBaseUrl(value) {
|
||||
runtimeApiBaseUrl = resolveBrowserReachableApiBaseUrl(value)
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(API_BASE_STORAGE_KEY, runtimeApiBaseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
export function getRuntimeApiBaseUrl() {
|
||||
return runtimeApiBaseUrl
|
||||
}
|
||||
|
||||
function buildUrl(path) {
|
||||
if (!path.startsWith('/')) {
|
||||
return `${runtimeApiBaseUrl}/${path}`
|
||||
}
|
||||
|
||||
return `${runtimeApiBaseUrl}${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), {
|
||||
...fetchOptions,
|
||||
headers
|
||||
})
|
||||
} catch {
|
||||
throw new Error('无法连接 FastAPI 后端服务,请确认后端已启动且浏览器可访问后端端口。')
|
||||
}
|
||||
|
||||
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 {
|
||||
payload = null
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(payload?.detail || '接口请求失败,请稍后重试。')
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import { apiRequest } from './api.js'
|
||||
|
||||
export function fetchKnowledgeLibrary() {
|
||||
return apiRequest('/knowledge/library')
|
||||
}
|
||||
|
||||
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)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: file,
|
||||
contentType: file.type || 'application/octet-stream'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteKnowledgeDocument(documentId) {
|
||||
return apiRequest(`/knowledge/documents/${documentId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchKnowledgeDocumentBlob(documentId, disposition = 'inline') {
|
||||
return apiRequest(`/knowledge/documents/${documentId}/content?disposition=${disposition}`, {
|
||||
responseType: 'blob',
|
||||
contentType: null
|
||||
})
|
||||
}
|
||||
import { apiRequest } from './api.js'
|
||||
|
||||
export function fetchKnowledgeLibrary() {
|
||||
return apiRequest('/knowledge/library')
|
||||
}
|
||||
|
||||
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)}`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: file,
|
||||
contentType: file.type || 'application/octet-stream'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteKnowledgeDocument(documentId) {
|
||||
return apiRequest(`/knowledge/documents/${documentId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchKnowledgeDocumentBlob(documentId, disposition = 'inline') {
|
||||
return apiRequest(`/knowledge/documents/${documentId}/content?disposition=${disposition}`, {
|
||||
responseType: 'blob',
|
||||
contentType: null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,43 +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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user