feat: 增强知识库索引与设置页面模块化拆分
扩展知识库索引任务和 RAG 检索支持增量入库和文档去重,优 化本体检测和规则匹配精度,前端设置页面拆分为 LLM、邮件 和 Hermes 员工同步子面板并重构样式,新增日志详情组件和 知识入库日志模型,补充单元测试覆盖。
This commit is contained in:
@@ -2,8 +2,12 @@ const EXPENSE_SCENE_SELECTION_OPTIONS = [
|
||||
{ key: 'travel', label: '差旅费', description: '出差行程、住宿、跨城交通等费用', icon: 'mdi mdi-bag-suitcase-outline' },
|
||||
{ key: 'transport', label: '交通费', description: '市内交通、打车、停车、通行等费用', icon: 'mdi mdi-car-outline' },
|
||||
{ key: 'hotel', label: '住宿费', description: '单独住宿或酒店发票报销', icon: 'mdi mdi-bed-outline' },
|
||||
{ key: 'entertainment', label: '业务招待费', description: '客户接待、餐饮招待等费用', icon: 'mdi mdi-food-fork-drink' },
|
||||
{ key: 'office', label: '办公费', description: '办公用品、低值易耗品等费用', icon: 'mdi mdi-briefcase-outline' },
|
||||
{ key: 'meal', label: '业务招待费', description: '客户接待、工作餐、加班餐、餐饮票据等费用', icon: 'mdi mdi-food-fork-drink' },
|
||||
{ key: 'meeting', label: '会务费', description: '会议、论坛、会场、参会等费用', icon: 'mdi mdi-account-tie-voice-outline' },
|
||||
{ key: 'office', label: '办公用品费', description: '办公用品、低值易耗品等费用', icon: 'mdi mdi-briefcase-outline' },
|
||||
{ key: 'training', label: '培训费', description: '培训课程、讲师费、教材认证等费用', icon: 'mdi mdi-school-outline' },
|
||||
{ key: 'communication', label: '通讯费', description: '话费、流量、宽带、网络等费用', icon: 'mdi mdi-cellphone-message' },
|
||||
{ key: 'welfare', label: '福利费', description: '团建、体检、慰问、节日福利等费用', icon: 'mdi mdi-gift-outline' },
|
||||
{ key: 'other', label: '其他费用', description: '暂不属于以上类型的费用', icon: 'mdi mdi-dots-horizontal-circle-outline' }
|
||||
]
|
||||
|
||||
|
||||
150
web/src/utils/hermesEmployeeSettingsModel.js
Normal file
150
web/src/utils/hermesEmployeeSettingsModel.js
Normal file
@@ -0,0 +1,150 @@
|
||||
/** 数字员工设置:面向管理员的简明任务列表(频率固定,仅可调执行时间) */
|
||||
export const HERMES_SIMPLE_TASKS = [
|
||||
{
|
||||
id: 'knowledgeAggregation',
|
||||
label: '知识库同步',
|
||||
hint: '同步制度文档与知识索引',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
{
|
||||
id: 'ruleReviewDigest',
|
||||
label: '规则待审提醒',
|
||||
hint: '汇总待审规则并推送管理员',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
{
|
||||
id: 'riskSummary',
|
||||
label: '风险每日巡检',
|
||||
hint: '扫描报销、付款等风险信号',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
{
|
||||
id: 'archiveDigest',
|
||||
label: '归档周报',
|
||||
hint: '汇总已归档报销单',
|
||||
frequency: 'weekly',
|
||||
frequencyLabel: '每周一',
|
||||
weekday: 1
|
||||
},
|
||||
{
|
||||
id: 'dailyStats',
|
||||
label: '日报统计',
|
||||
hint: '生成昨日报销与审批数据',
|
||||
frequency: 'daily',
|
||||
frequencyLabel: '每天'
|
||||
},
|
||||
{
|
||||
id: 'monthlyStats',
|
||||
label: '月报统计',
|
||||
hint: '每月 1 号生成上月汇总',
|
||||
frequency: 'monthly',
|
||||
frequencyLabel: '每月 1 日',
|
||||
monthDay: 1
|
||||
},
|
||||
{
|
||||
id: 'yearlyStats',
|
||||
label: '年报统计',
|
||||
hint: '每年 1 月 1 号生成上年汇总',
|
||||
frequency: 'yearly',
|
||||
frequencyLabel: '每年 1 月 1 日',
|
||||
month: 1,
|
||||
monthDay: 1
|
||||
}
|
||||
]
|
||||
|
||||
function buildDefaultSchedules() {
|
||||
const defaults = {
|
||||
knowledgeAggregation: { enabled: true, frequency: 'daily', time: '00:00', weekday: 1, monthDay: 1, month: 1 },
|
||||
ruleReviewDigest: { enabled: true, frequency: 'daily', time: '18:00', weekday: 5, monthDay: 1, month: 1 },
|
||||
riskSummary: { enabled: true, frequency: 'daily', time: '09:00', weekday: 1, monthDay: 1, month: 1 },
|
||||
archiveDigest: { enabled: false, frequency: 'weekly', time: '10:30', weekday: 1, monthDay: 1, month: 1 },
|
||||
dailyStats: { enabled: true, frequency: 'daily', time: '08:30', weekday: 1, monthDay: 1, month: 1 },
|
||||
monthlyStats: { enabled: true, frequency: 'monthly', time: '09:00', weekday: 1, monthDay: 1, month: 1 },
|
||||
yearlyStats: { enabled: false, frequency: 'yearly', time: '10:00', weekday: 1, monthDay: 1, month: 1 }
|
||||
}
|
||||
|
||||
for (const task of HERMES_SIMPLE_TASKS) {
|
||||
const schedule = defaults[task.id]
|
||||
if (!schedule) {
|
||||
continue
|
||||
}
|
||||
schedule.frequency = task.frequency
|
||||
if (task.weekday != null) {
|
||||
schedule.weekday = task.weekday
|
||||
}
|
||||
if (task.monthDay != null) {
|
||||
schedule.monthDay = task.monthDay
|
||||
}
|
||||
if (task.month != null) {
|
||||
schedule.month = task.month
|
||||
}
|
||||
}
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
export function buildDefaultHermesEmployeeForm() {
|
||||
return {
|
||||
masterEnabled: true,
|
||||
notifyOnFailure: true,
|
||||
capabilities: {
|
||||
knowledgeAggregation: true,
|
||||
ruleReviewDigest: true,
|
||||
riskSummary: true,
|
||||
archiveDigest: false,
|
||||
dailyStats: true,
|
||||
monthlyStats: true,
|
||||
yearlyStats: false
|
||||
},
|
||||
schedules: buildDefaultSchedules()
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeHermesEmployeeForm(override = {}) {
|
||||
const defaults = buildDefaultHermesEmployeeForm()
|
||||
const schedules = { ...defaults.schedules }
|
||||
|
||||
for (const [key, value] of Object.entries(override?.schedules || {})) {
|
||||
schedules[key] = { ...defaults.schedules[key], ...value }
|
||||
}
|
||||
|
||||
for (const task of HERMES_SIMPLE_TASKS) {
|
||||
if (schedules[task.id]) {
|
||||
schedules[task.id].frequency = task.frequency
|
||||
if (task.weekday != null) {
|
||||
schedules[task.id].weekday = task.weekday
|
||||
}
|
||||
if (task.monthDay != null) {
|
||||
schedules[task.id].monthDay = task.monthDay
|
||||
}
|
||||
if (task.month != null) {
|
||||
schedules[task.id].month = task.month
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...override,
|
||||
capabilities: {
|
||||
...defaults.capabilities,
|
||||
...(override?.capabilities || {})
|
||||
},
|
||||
schedules
|
||||
}
|
||||
}
|
||||
|
||||
export function isHermesTaskEnabled(form, taskId) {
|
||||
return Boolean(form?.masterEnabled && form?.capabilities?.[taskId] && form?.schedules?.[taskId]?.enabled)
|
||||
}
|
||||
|
||||
export function countEnabledHermesTasks(form) {
|
||||
return HERMES_SIMPLE_TASKS.filter((task) => isHermesTaskEnabled(form, task.id)).length
|
||||
}
|
||||
|
||||
export function isHermesEmployeeSettingsReady(form) {
|
||||
return Boolean(!form?.masterEnabled || countEnabledHermesTasks(form) > 0)
|
||||
}
|
||||
315
web/src/utils/knowledgeIngestLogModel.js
Normal file
315
web/src/utils/knowledgeIngestLogModel.js
Normal file
@@ -0,0 +1,315 @@
|
||||
const KNOWLEDGE_INGEST_JOB_TYPES = new Set(['knowledge_index_sync', 'llm_wiki_sync'])
|
||||
|
||||
const STATUS_META = {
|
||||
queued: { label: '等待处理', tone: 'muted' },
|
||||
running: { label: '处理中', tone: 'warning' },
|
||||
succeeded: { label: '已完成', tone: 'success' },
|
||||
failed: { label: '失败', tone: 'danger' },
|
||||
skipped: { label: '已跳过', tone: 'muted' }
|
||||
}
|
||||
|
||||
const PHASE_LABELS = {
|
||||
queued: '进入队列',
|
||||
indexing: '解析与索引',
|
||||
indexed: '索引完成',
|
||||
failed: '处理失败',
|
||||
completed: '任务完成'
|
||||
}
|
||||
|
||||
export function isKnowledgeIngestRun(run) {
|
||||
const routeJson = asObject(run?.route_json)
|
||||
return KNOWLEDGE_INGEST_JOB_TYPES.has(String(routeJson.job_type || '').trim())
|
||||
}
|
||||
|
||||
export function buildKnowledgeIngestLogModel(run) {
|
||||
const routeJson = asObject(run?.route_json)
|
||||
const ingest = asObject(routeJson.knowledge_ingest)
|
||||
const toolDocuments = extractToolDocuments(run)
|
||||
const sourceDocuments = normalizeSourceDocuments(
|
||||
ingest.documents,
|
||||
toolDocuments,
|
||||
routeJson.requested_document_ids
|
||||
)
|
||||
const documents = sourceDocuments.map(normalizeDocument)
|
||||
const graph = normalizeGraph(ingest.graph, documents)
|
||||
const progress = normalizeProgress(routeJson.progress, documents)
|
||||
const currentDocumentId = String(ingest.current_document_id || '').trim()
|
||||
|
||||
return {
|
||||
available: isKnowledgeIngestRun(run),
|
||||
folder: String(routeJson.folder || '').trim(),
|
||||
phase: String(ingest.phase || routeJson.phase || '').trim(),
|
||||
phaseLabel: PHASE_LABELS[ingest.phase] || PHASE_LABELS[routeJson.phase] || '运行中',
|
||||
status: String(ingest.status || run?.status || '').trim(),
|
||||
statusLabel: resolveStatusMeta(ingest.status || run?.status).label,
|
||||
statusTone: resolveStatusMeta(ingest.status || run?.status).tone,
|
||||
progress,
|
||||
currentDocumentId,
|
||||
documents,
|
||||
selectedDocumentId: resolveDefaultDocumentId(documents, currentDocumentId),
|
||||
graph,
|
||||
metrics: [
|
||||
{
|
||||
label: '文件',
|
||||
value: `${progress.completedDocuments}/${progress.totalDocuments}`,
|
||||
hint: `失败 ${progress.failedDocuments}`
|
||||
},
|
||||
{
|
||||
label: 'Chunk',
|
||||
value: formatNumber(graph.chunkCount),
|
||||
hint: '已解析块'
|
||||
},
|
||||
{
|
||||
label: '实体',
|
||||
value: formatNumber(graph.entityCount),
|
||||
hint: '图谱节点'
|
||||
},
|
||||
{
|
||||
label: '关系',
|
||||
value: formatNumber(graph.relationCount),
|
||||
hint: '图谱边'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export function formatKnowledgeMetric(value) {
|
||||
return formatNumber(value)
|
||||
}
|
||||
|
||||
function normalizeSourceDocuments(ingestDocuments, toolDocuments, requestedDocumentIds) {
|
||||
if (Array.isArray(ingestDocuments) && ingestDocuments.length) {
|
||||
return ingestDocuments
|
||||
}
|
||||
if (Array.isArray(toolDocuments) && toolDocuments.length) {
|
||||
return toolDocuments
|
||||
}
|
||||
if (Array.isArray(requestedDocumentIds)) {
|
||||
return requestedDocumentIds
|
||||
.map((documentId) => String(documentId || '').trim())
|
||||
.filter(Boolean)
|
||||
.map((documentId) => ({ document_id: documentId, name: documentId, status: 'queued' }))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function extractToolDocuments(run) {
|
||||
const toolCalls = Array.isArray(run?.tool_calls) ? run.tool_calls : []
|
||||
for (const toolCall of [...toolCalls].reverse()) {
|
||||
const responseJson = asObject(toolCall?.response_json)
|
||||
if (Array.isArray(responseJson.documents) && responseJson.documents.length) {
|
||||
return responseJson.documents
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function normalizeDocument(rawDocument) {
|
||||
const document = asObject(rawDocument)
|
||||
const documentId = String(document.document_id || document.id || '').trim()
|
||||
const status = String(document.status || 'queued').trim()
|
||||
const phase = String(document.phase || status).trim()
|
||||
const chunks = normalizeChunks(document.chunks)
|
||||
const sections = normalizeSections(document.sections)
|
||||
const entities = normalizeEntities(document.entities)
|
||||
const relations = normalizeRelations(document.relations)
|
||||
return {
|
||||
documentId,
|
||||
name: String(document.name || document.original_name || documentId || '未命名文件').trim(),
|
||||
folder: String(document.folder || '').trim(),
|
||||
extension: String(document.extension || '').trim(),
|
||||
mimeType: String(document.mime_type || '').trim(),
|
||||
status,
|
||||
statusLabel: resolveStatusMeta(status).label,
|
||||
statusTone: resolveStatusMeta(status).tone,
|
||||
phase,
|
||||
phaseLabel: PHASE_LABELS[phase] || PHASE_LABELS[status] || phase || '未开始',
|
||||
startedAt: String(document.started_at || '').trim(),
|
||||
finishedAt: String(document.finished_at || '').trim(),
|
||||
error: String(document.error || '').trim(),
|
||||
textChars: toNumber(document.text_chars),
|
||||
indexedTextChars: toNumber(document.indexed_text_chars),
|
||||
sectionCount: toNumber(document.section_count || sections.length),
|
||||
chunkCount: toNumber(document.chunk_count || chunks.length),
|
||||
chunkIds: normalizeTextList(document.chunk_ids),
|
||||
chunks,
|
||||
entityCount: toNumber(document.entity_count || entities.length),
|
||||
relationCount: toNumber(document.relation_count || relations.length),
|
||||
entities,
|
||||
relations,
|
||||
events: normalizeEvents(document.events)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProgress(rawProgress, documents) {
|
||||
const progress = asObject(rawProgress)
|
||||
const totalDocuments = toNumber(progress.total_documents || documents.length)
|
||||
const completedDocuments = toNumber(
|
||||
progress.completed_documents || documents.filter((item) => item.status === 'succeeded').length
|
||||
)
|
||||
const failedDocuments = toNumber(
|
||||
progress.failed_documents || documents.filter((item) => item.status === 'failed').length
|
||||
)
|
||||
const skippedDocuments = toNumber(progress.skipped_documents)
|
||||
const percent = clampPercent(
|
||||
progress.percent ?? calculatePercent(totalDocuments, completedDocuments + failedDocuments)
|
||||
)
|
||||
return {
|
||||
totalDocuments,
|
||||
completedDocuments,
|
||||
failedDocuments,
|
||||
skippedDocuments,
|
||||
percent
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGraph(rawGraph, documents) {
|
||||
const graph = asObject(rawGraph)
|
||||
const fallbackEntities = dedupeTextList(documents.flatMap((item) => item.entities))
|
||||
const fallbackRelations = dedupeRelations(documents.flatMap((item) => item.relations))
|
||||
return {
|
||||
chunkCount: toNumber(
|
||||
graph.chunk_count || documents.reduce((total, item) => total + item.chunkCount, 0)
|
||||
),
|
||||
entityCount: toNumber(
|
||||
graph.entity_count || documents.reduce((total, item) => total + item.entityCount, 0)
|
||||
),
|
||||
relationCount: toNumber(
|
||||
graph.relation_count || documents.reduce((total, item) => total + item.relationCount, 0)
|
||||
),
|
||||
entities: normalizeTextList(graph.entities).length
|
||||
? normalizeTextList(graph.entities)
|
||||
: fallbackEntities,
|
||||
relations: normalizeRelations(graph.relations).length
|
||||
? normalizeRelations(graph.relations)
|
||||
: fallbackRelations
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeChunks(rawChunks) {
|
||||
if (!Array.isArray(rawChunks)) return []
|
||||
return rawChunks
|
||||
.map((chunk, index) => {
|
||||
const item = asObject(chunk)
|
||||
return {
|
||||
id: String(item.id || item._id || `chunk-${index + 1}`).trim(),
|
||||
order: toNumber(item.order ?? item.chunk_order_index ?? index),
|
||||
tokens: toNumber(item.tokens),
|
||||
summary: String(item.summary || item.content || '').trim()
|
||||
}
|
||||
})
|
||||
.sort((left, right) => left.order - right.order)
|
||||
}
|
||||
|
||||
function normalizeSections(rawSections) {
|
||||
if (!Array.isArray(rawSections)) return []
|
||||
return rawSections.map((section, index) => {
|
||||
const item = asObject(section)
|
||||
return {
|
||||
title: String(item.title || `章节 ${index + 1}`).trim(),
|
||||
excerpt: String(item.excerpt || '').trim()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeEvents(rawEvents) {
|
||||
if (!Array.isArray(rawEvents)) return []
|
||||
return rawEvents.map((event) => {
|
||||
const item = asObject(event)
|
||||
return {
|
||||
at: String(item.at || '').trim(),
|
||||
level: String(item.level || 'info').trim(),
|
||||
message: String(item.message || '').trim()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeEntities(rawEntities) {
|
||||
return normalizeTextList(rawEntities)
|
||||
}
|
||||
|
||||
function normalizeRelations(rawRelations) {
|
||||
if (!Array.isArray(rawRelations)) return []
|
||||
return rawRelations
|
||||
.map((relation) => {
|
||||
const item = asObject(relation)
|
||||
return {
|
||||
source: String(item.source || item.from || '').trim(),
|
||||
target: String(item.target || item.to || '').trim(),
|
||||
type: String(item.type || '关联').trim()
|
||||
}
|
||||
})
|
||||
.filter((item) => item.source && item.target)
|
||||
}
|
||||
|
||||
function resolveDefaultDocumentId(documents, currentDocumentId) {
|
||||
if (currentDocumentId && documents.some((item) => item.documentId === currentDocumentId)) {
|
||||
return currentDocumentId
|
||||
}
|
||||
return (
|
||||
documents.find((item) => item.status === 'running')?.documentId ||
|
||||
documents.find((item) => item.status === 'failed')?.documentId ||
|
||||
documents[0]?.documentId ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
function resolveStatusMeta(status) {
|
||||
return STATUS_META[String(status || '').trim()] || STATUS_META.queued
|
||||
}
|
||||
|
||||
function asObject(value) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
||||
}
|
||||
|
||||
function normalizeTextList(value) {
|
||||
if (!Array.isArray(value)) return []
|
||||
return dedupeTextList(value)
|
||||
}
|
||||
|
||||
function dedupeTextList(items) {
|
||||
const result = []
|
||||
const seen = new Set()
|
||||
for (const item of items) {
|
||||
const text = String(item || '').trim()
|
||||
if (!text || seen.has(text)) continue
|
||||
seen.add(text)
|
||||
result.push(text)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function dedupeRelations(items) {
|
||||
const result = []
|
||||
const seen = new Set()
|
||||
for (const item of items) {
|
||||
const source = String(item?.source || '').trim()
|
||||
const target = String(item?.target || '').trim()
|
||||
const type = String(item?.type || '关联').trim()
|
||||
const key = `${source}::${target}::${type}`
|
||||
if (!source || !target || seen.has(key)) continue
|
||||
seen.add(key)
|
||||
result.push({ source, target, type })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function calculatePercent(total, done) {
|
||||
if (!total) return 0
|
||||
return Math.round((done / total) * 100)
|
||||
}
|
||||
|
||||
function clampPercent(value) {
|
||||
const numericValue = toNumber(value)
|
||||
return Math.max(0, Math.min(100, numericValue))
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const numericValue = toNumber(value)
|
||||
return Number.isFinite(numericValue) ? numericValue.toLocaleString('zh-CN') : '0'
|
||||
}
|
||||
|
||||
function toNumber(value) {
|
||||
const numericValue = Number(value)
|
||||
return Number.isFinite(numericValue) ? numericValue : 0
|
||||
}
|
||||
@@ -12,8 +12,32 @@ const defaultParagraphOpen = markdown.renderer.rules.paragraph_open
|
||||
const defaultLinkOpen = markdown.renderer.rules.link_open
|
||||
const defaultBlockquoteOpen = markdown.renderer.rules.blockquote_open
|
||||
|
||||
const RISK_TEXT_CLASS_BY_LABEL = {
|
||||
低风险: 'markdown-risk-text-low',
|
||||
中风险: 'markdown-risk-text-medium',
|
||||
高风险: 'markdown-risk-text-high'
|
||||
}
|
||||
|
||||
const ACTION_LINK_CLASS_BY_HREF = {
|
||||
'#confirm-attachment-association': 'markdown-action-link-confirm'
|
||||
'#confirm-attachment-association': 'markdown-action-link-confirm',
|
||||
'#review-next-step': 'markdown-action-link-next',
|
||||
'#review-quick-edit': 'markdown-action-link-edit',
|
||||
'#review-risk-panel': 'markdown-action-link-risk'
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function renderRiskText(text) {
|
||||
return escapeHtml(text).replace(/低风险|中风险|高风险/g, (label) => {
|
||||
const className = RISK_TEXT_CLASS_BY_LABEL[label]
|
||||
return className ? `<span class="${className}">${label}</span>` : label
|
||||
})
|
||||
}
|
||||
|
||||
function resolveActionLinkClass(href) {
|
||||
@@ -70,6 +94,8 @@ markdown.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||
: self.renderToken(tokens, idx, options)
|
||||
}
|
||||
|
||||
markdown.renderer.rules.text = (tokens, idx) => renderRiskText(tokens[idx]?.content)
|
||||
|
||||
markdown.renderer.rules.blockquote_open = (tokens, idx, options, env, self) => {
|
||||
if (blockquoteHasAttachmentHeading(tokens, idx)) {
|
||||
tokens[idx].attrJoin('class', 'markdown-attachment-card')
|
||||
|
||||
@@ -22,17 +22,17 @@ const DEFAULT_EXPENSE_TYPE_LABELS = {
|
||||
travel: '差旅费',
|
||||
hotel: '住宿费',
|
||||
transport: '交通费',
|
||||
meal: '餐费',
|
||||
meal: '业务招待费',
|
||||
meeting: '会务费',
|
||||
entertainment: '业务招待费',
|
||||
office: '办公费',
|
||||
office: '办公用品费',
|
||||
training: '培训费',
|
||||
communication: '通讯费',
|
||||
welfare: '福利费',
|
||||
other: '其他费用'
|
||||
}
|
||||
|
||||
export const TRANSPORT_KEYWORD_PATTERN = /交通|出行|打车|网约车|出租车|滴滴|车费|乘车|用车|叫车|约车|的士|车票|车资|地铁|公交|停车|过路费|通行费/
|
||||
export const TRANSPORT_KEYWORD_PATTERN = /交通|市内交通|出行|打车|网约车|出租车|滴滴|车费|乘车|用车|叫车|约车|的士|车票|车资|地铁|公交|停车|过路费|通行费|高速费|油费/
|
||||
|
||||
const FLOW_INTENT_KEYWORDS = {
|
||||
draft: ['报销', '报账', '草稿', '生成', '提交', '申请', '请走报销'],
|
||||
@@ -104,21 +104,36 @@ export function inferLocalFlowCandidates(rawText) {
|
||||
|
||||
let event = ''
|
||||
let expenseType = ''
|
||||
if (/客户.*吃饭|请客户.*吃饭|招待|宴请|请客/.test(compact)) {
|
||||
event = '请客户吃饭'
|
||||
expenseType = '业务招待费'
|
||||
} else if (/出差|差旅|机票|高铁|火车|行程/.test(compact)) {
|
||||
event = '出差行程'
|
||||
expenseType = '差旅费'
|
||||
} else if (TRANSPORT_KEYWORD_PATTERN.test(compact)) {
|
||||
if (TRANSPORT_KEYWORD_PATTERN.test(compact)) {
|
||||
event = '交通出行'
|
||||
expenseType = '交通费'
|
||||
} else if (/住宿|酒店|宾馆/.test(compact)) {
|
||||
} else if (/客户.*吃饭|请客户.*吃饭|客户用餐|客户接待|商务接待|招待|宴请|请客/.test(compact)) {
|
||||
event = '业务招待'
|
||||
expenseType = '业务招待费'
|
||||
} else if (/出差|差旅|机票|飞机票|航班|高铁票|高铁|火车票|火车|动车|行程单|铁路客票/.test(compact)) {
|
||||
event = '出差行程'
|
||||
expenseType = '差旅费'
|
||||
} else if (/住宿|住宿费|酒店|酒店发票|宾馆|民宿|房费|客房/.test(compact)) {
|
||||
event = '住宿报销'
|
||||
expenseType = '住宿费'
|
||||
} else if (/餐费|用餐|午餐|晚餐|早餐|餐饮/.test(compact)) {
|
||||
event = '餐饮用餐'
|
||||
expenseType = '餐费'
|
||||
} else if (/餐费|工作餐|用餐|午餐|晚餐|早餐|餐饮|伙食|茶歇/.test(compact)) {
|
||||
event = '业务招待'
|
||||
expenseType = '业务招待费'
|
||||
} else if (/会务|会议费|会议|参会|会场|场地费|论坛|展会/.test(compact)) {
|
||||
event = '会务活动'
|
||||
expenseType = '会务费'
|
||||
} else if (/办公用品|办公耗材|办公设备|文具|打印纸|硒鼓|墨盒|键盘|鼠标|白板/.test(compact)) {
|
||||
event = '办公采购'
|
||||
expenseType = '办公用品费'
|
||||
} else if (/培训|讲师费|课程费|教材|认证费|考试费/.test(compact)) {
|
||||
event = '培训学习'
|
||||
expenseType = '培训费'
|
||||
} else if (/通讯费|话费|电话费|手机费|流量费|宽带费|网络费/.test(compact)) {
|
||||
event = '通讯使用'
|
||||
expenseType = '通讯费'
|
||||
} else if (/福利费|团建|慰问|节日福利|体检费|员工关怀/.test(compact)) {
|
||||
event = '员工福利'
|
||||
expenseType = '福利费'
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -232,7 +247,13 @@ export function buildLocalExtractionProgressMessages(rawText, options = {}) {
|
||||
}
|
||||
|
||||
const attachmentHint = Number(options.attachmentCount || 0) > 0 ? '附件完整性' : '票据附件'
|
||||
messages.push(`正在判断待补项:客户名称、参与人员、${attachmentHint}`)
|
||||
const pendingSlots = ['发生时间', '金额', attachmentHint]
|
||||
if (candidates.expenseType === '业务招待费') {
|
||||
pendingSlots.splice(2, 0, '客户名称', '参与人员')
|
||||
} else if (candidates.expenseType === '住宿费') {
|
||||
pendingSlots.splice(2, 0, '酒店/商户')
|
||||
}
|
||||
messages.push(`正在判断待补项:${pendingSlots.join('、')}`)
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
@@ -54,13 +54,13 @@ const REQUEST_TYPE_META = {
|
||||
secondaryStatusLabel: '票据状态'
|
||||
},
|
||||
meal: {
|
||||
label: '餐费',
|
||||
label: '业务招待费',
|
||||
detailVariant: 'general',
|
||||
tone: 'meeting',
|
||||
secondaryStatusLabel: '票据状态'
|
||||
},
|
||||
office: {
|
||||
label: '办公费',
|
||||
label: '办公用品费',
|
||||
detailVariant: 'general',
|
||||
tone: 'office',
|
||||
secondaryStatusLabel: '票据状态'
|
||||
|
||||
461
web/src/utils/settingsModelHelper.js
Normal file
461
web/src/utils/settingsModelHelper.js
Normal file
@@ -0,0 +1,461 @@
|
||||
import {
|
||||
buildDefaultHermesEmployeeForm,
|
||||
isHermesEmployeeSettingsReady,
|
||||
mergeHermesEmployeeForm
|
||||
} from './hermesEmployeeSettingsModel.js'
|
||||
|
||||
export const SETTINGS_STORAGE_KEY = 'x-financial-settings-draft'
|
||||
export const CURRENT_YEAR = new Date().getFullYear()
|
||||
export const CUSTOM_OPENAI_PROVIDER = 'Custom OpenAI Compatible'
|
||||
export const MODEL_SECRET_MASK = '********'
|
||||
export const RENDER_SECRET_MASK = '********'
|
||||
|
||||
export const SECTION_DEFINITIONS = [
|
||||
{
|
||||
id: 'profile',
|
||||
label: '企业信息',
|
||||
title: '系统基本信息',
|
||||
desc: '公司名称、品牌与版权',
|
||||
longDesc: '统一维护企业名称、系统显示名称和版权信息,保存后会同步更新当前系统品牌展示。',
|
||||
actionLabel: '保存企业信息'
|
||||
},
|
||||
{
|
||||
id: 'admin',
|
||||
label: '管理员安全',
|
||||
title: '管理员账号与安全策略',
|
||||
desc: '账号、密码与登录安全',
|
||||
longDesc: '集中管理管理员账号、邮箱和登录安全策略。密码仅在当前输入时可见,不会写入浏览器草稿。',
|
||||
actionLabel: '保存安全设置'
|
||||
},
|
||||
{
|
||||
id: 'session',
|
||||
label: '会话设置',
|
||||
title: '会话留存设置',
|
||||
desc: '会话保留天数',
|
||||
longDesc: '统一配置智能体会话的保留天数。超过保留期的历史会话会在后端清理,避免上下文和管理成本无限增长。',
|
||||
actionLabel: '保存会话设置'
|
||||
},
|
||||
{
|
||||
id: 'hermes',
|
||||
label: '数字员工设置',
|
||||
title: '数字员工设置',
|
||||
desc: 'Hermes 自动任务',
|
||||
longDesc: '选择需要自动执行的任务,并设置每天的执行时间。无需了解 Cron 或复杂调度规则。',
|
||||
actionLabel: '保存数字员工设置'
|
||||
},
|
||||
{
|
||||
id: 'llm',
|
||||
label: '大语言模型',
|
||||
title: '模型接入配置',
|
||||
desc: '主模型、备份模型与检索模型',
|
||||
longDesc: '集中维护主模型、备份模型、Embedding 模型和 Reranker 模型的接入参数,供 AI 助手和检索链路调用。',
|
||||
actionLabel: '保存模型配置'
|
||||
},
|
||||
{
|
||||
id: 'rendering',
|
||||
label: '文件渲染',
|
||||
title: '文件渲染',
|
||||
desc: '文档预览服务与访问密钥',
|
||||
longDesc: '维护文件渲染开关、文档服务对外地址和 JWT 密钥,后端回调地址继续由部署配置管理。',
|
||||
actionLabel: '保存文件渲染配置'
|
||||
},
|
||||
{
|
||||
id: 'logs',
|
||||
label: '日志策略',
|
||||
title: '日志与审计策略',
|
||||
desc: '日志级别、留存与脱敏',
|
||||
longDesc: '定义系统日志级别、留存周期和审计策略,保证问题排查和合规审计可追溯。',
|
||||
actionLabel: '保存日志策略'
|
||||
},
|
||||
{
|
||||
id: 'mail',
|
||||
label: '邮箱设置',
|
||||
title: '邮箱通知配置',
|
||||
desc: 'SMTP 与通知投递策略',
|
||||
longDesc: '维护系统邮件发送配置和通知投递策略,审批、告警和摘要邮件都会依赖这里的设置。',
|
||||
actionLabel: '保存邮箱配置'
|
||||
}
|
||||
]
|
||||
|
||||
export const LOG_LEVELS = ['DEBUG', 'INFO', 'WARN', 'ERROR']
|
||||
|
||||
export const PROVIDER_OPTIONS = [
|
||||
'MiniMax',
|
||||
'GLM',
|
||||
'Kimi',
|
||||
'Ali',
|
||||
'Codex',
|
||||
'Claude',
|
||||
'Gemini',
|
||||
CUSTOM_OPENAI_PROVIDER
|
||||
]
|
||||
|
||||
export const PROVIDER_ENDPOINTS = {
|
||||
MiniMax: 'https://api.minimaxi.com/v1',
|
||||
GLM: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
Kimi: 'https://api.moonshot.ai/v1',
|
||||
Ali: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
Codex: 'https://api.openai.com/v1',
|
||||
Claude: 'https://api.anthropic.com/v1/',
|
||||
Gemini: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
||||
[CUSTOM_OPENAI_PROVIDER]: ''
|
||||
}
|
||||
|
||||
export const RERANKER_PROVIDER_ENDPOINTS = {
|
||||
Ali: 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank',
|
||||
[CUSTOM_OPENAI_PROVIDER]: ''
|
||||
}
|
||||
|
||||
export const LEGACY_PROVIDER_MAP = {
|
||||
'OpenAI Compatible': 'Codex',
|
||||
'Azure OpenAI': CUSTOM_OPENAI_PROVIDER,
|
||||
Ollama: CUSTOM_OPENAI_PROVIDER,
|
||||
'自定义网关': CUSTOM_OPENAI_PROVIDER
|
||||
}
|
||||
|
||||
export const MODEL_TEST_CONFIGS = {
|
||||
main: {
|
||||
label: '主模型',
|
||||
providerKey: 'mainProvider',
|
||||
modelKey: 'mainModel',
|
||||
endpointKey: 'mainEndpoint',
|
||||
apiKeyKey: 'mainApiKey',
|
||||
capability: 'chat'
|
||||
},
|
||||
backup: {
|
||||
label: '备份模型',
|
||||
providerKey: 'backupProvider',
|
||||
modelKey: 'backupModel',
|
||||
endpointKey: 'backupEndpoint',
|
||||
apiKeyKey: 'backupApiKey',
|
||||
capability: 'chat'
|
||||
},
|
||||
embedding: {
|
||||
label: 'Embedding 模型',
|
||||
providerKey: 'embeddingProvider',
|
||||
modelKey: 'embeddingModel',
|
||||
endpointKey: 'embeddingEndpoint',
|
||||
apiKeyKey: 'embeddingApiKey',
|
||||
capability: 'embedding'
|
||||
},
|
||||
reranker: {
|
||||
label: 'Reranker 模型',
|
||||
providerKey: 'rerankerProvider',
|
||||
modelKey: 'rerankerModel',
|
||||
endpointKey: 'rerankerEndpoint',
|
||||
apiKeyKey: 'rerankerApiKey',
|
||||
capability: 'reranker'
|
||||
}
|
||||
}
|
||||
|
||||
export const MODEL_API_KEY_CONFIGS = Object.values(MODEL_TEST_CONFIGS)
|
||||
export const SESSION_RETENTION_OPTIONS = Array.from({ length: 10 }, (_item, index) => ({
|
||||
value: index + 1,
|
||||
label: `${index + 1} 天`
|
||||
}))
|
||||
|
||||
export function normalizeValue(value) {
|
||||
return String(value ?? '').trim()
|
||||
}
|
||||
|
||||
export function normalizeProviderValue(value, fallback = 'Codex') {
|
||||
const normalized = normalizeValue(value)
|
||||
|
||||
if (PROVIDER_OPTIONS.includes(normalized)) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
if (LEGACY_PROVIDER_MAP[normalized]) {
|
||||
return LEGACY_PROVIDER_MAP[normalized]
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
export function getProviderEndpoint(provider) {
|
||||
return PROVIDER_ENDPOINTS[provider] ?? ''
|
||||
}
|
||||
|
||||
export function getRerankerEndpoint(provider) {
|
||||
return RERANKER_PROVIDER_ENDPOINTS[provider] ?? getProviderEndpoint(provider)
|
||||
}
|
||||
|
||||
export function buildDefaultState(companyProfile, currentUser) {
|
||||
const companyName = normalizeValue(companyProfile?.name) || 'X-Financial'
|
||||
const companyCode = normalizeValue(companyProfile?.code) || 'XF-001'
|
||||
const adminEmail =
|
||||
normalizeValue(companyProfile?.adminEmail) ||
|
||||
normalizeValue(currentUser?.email) ||
|
||||
'admin@example.com'
|
||||
const adminAccount = normalizeValue(currentUser?.username) || 'superadmin'
|
||||
|
||||
return {
|
||||
companyForm: {
|
||||
companyName,
|
||||
displayName: companyName,
|
||||
companyCode,
|
||||
logo: normalizeValue(companyProfile?.logo) || '',
|
||||
recordNumber: '',
|
||||
copyright: `Copyright © 2024-${CURRENT_YEAR} ${companyName}. All Rights Reserved.`
|
||||
},
|
||||
adminForm: {
|
||||
adminAccount,
|
||||
adminEmail,
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
sessionTimeout: Number(import.meta.env.VITE_AUTH_IDLE_TIMEOUT_MINUTES || 30),
|
||||
noticeEmail: adminEmail,
|
||||
mfaEnabled: true,
|
||||
strongPassword: true,
|
||||
loginAlertEnabled: true,
|
||||
adminPasswordConfigured: false
|
||||
},
|
||||
sessionForm: {
|
||||
conversationRetentionDays: 3
|
||||
},
|
||||
llmForm: {
|
||||
mainProvider: 'Codex',
|
||||
mainModel: 'codex-mini-latest',
|
||||
mainEndpoint: getProviderEndpoint('Codex'),
|
||||
mainApiKey: '',
|
||||
mainApiKeyConfigured: false,
|
||||
backupProvider: 'GLM',
|
||||
backupModel: 'glm-5.1',
|
||||
backupEndpoint: getProviderEndpoint('GLM'),
|
||||
backupApiKey: '',
|
||||
backupApiKeyConfigured: false,
|
||||
embeddingProvider: 'GLM',
|
||||
embeddingModel: 'Embedding-3',
|
||||
embeddingEndpoint: getProviderEndpoint('GLM'),
|
||||
embeddingApiKey: '',
|
||||
embeddingApiKeyConfigured: false,
|
||||
rerankerProvider: 'Ali',
|
||||
rerankerModel: 'gte-rerank-v2',
|
||||
rerankerEndpoint: getRerankerEndpoint('Ali'),
|
||||
rerankerApiKey: '',
|
||||
rerankerApiKeyConfigured: false
|
||||
},
|
||||
renderForm: {
|
||||
enabled: false,
|
||||
publicUrl: '',
|
||||
jwtSecret: '',
|
||||
jwtSecretConfigured: false
|
||||
},
|
||||
logForm: {
|
||||
level: 'INFO',
|
||||
retentionDays: 180,
|
||||
archiveCycle: 'weekly',
|
||||
logPath: 'server/logs/app.log',
|
||||
alertEmail: adminEmail,
|
||||
operationAudit: true,
|
||||
loginAudit: true,
|
||||
maskSensitive: true
|
||||
},
|
||||
hermesForm: buildDefaultHermesEmployeeForm(),
|
||||
mailForm: {
|
||||
smtpHost: 'smtp.exmail.qq.com',
|
||||
port: 465,
|
||||
encryption: 'SSL/TLS',
|
||||
senderName: companyName,
|
||||
senderAddress: adminEmail,
|
||||
username: adminEmail,
|
||||
password: '',
|
||||
passwordConfigured: false,
|
||||
alertEnabled: true,
|
||||
digestEnabled: false,
|
||||
digestTime: '09:00',
|
||||
defaultReceiver: adminEmail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function readStoredSettings() {
|
||||
if (typeof window === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
const raw = window.sessionStorage.getItem(SETTINGS_STORAGE_KEY)
|
||||
if (!raw) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeState(baseState, overrideState) {
|
||||
const mergedLlmForm = { ...baseState.llmForm, ...(overrideState?.llmForm || {}) }
|
||||
|
||||
mergedLlmForm.mainProvider = normalizeProviderValue(mergedLlmForm.mainProvider, baseState.llmForm.mainProvider)
|
||||
mergedLlmForm.backupProvider = normalizeProviderValue(mergedLlmForm.backupProvider, baseState.llmForm.backupProvider)
|
||||
mergedLlmForm.embeddingProvider = normalizeProviderValue(
|
||||
mergedLlmForm.embeddingProvider,
|
||||
baseState.llmForm.embeddingProvider
|
||||
)
|
||||
mergedLlmForm.rerankerProvider = normalizeProviderValue(
|
||||
mergedLlmForm.rerankerProvider,
|
||||
baseState.llmForm.rerankerProvider
|
||||
)
|
||||
|
||||
return {
|
||||
companyForm: { ...baseState.companyForm, ...(overrideState?.companyForm || {}) },
|
||||
adminForm: { ...baseState.adminForm, ...(overrideState?.adminForm || {}) },
|
||||
sessionForm: { ...baseState.sessionForm, ...(overrideState?.sessionForm || {}) },
|
||||
hermesForm: mergeHermesEmployeeForm({
|
||||
...baseState.hermesForm,
|
||||
...(overrideState?.hermesForm || {})
|
||||
}),
|
||||
llmForm: mergedLlmForm,
|
||||
renderForm: { ...baseState.renderForm, ...(overrideState?.renderForm || {}) },
|
||||
logForm: { ...baseState.logForm, ...(overrideState?.logForm || {}) },
|
||||
mailForm: { ...baseState.mailForm, ...(overrideState?.mailForm || {}) }
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeForStorage(state) {
|
||||
return {
|
||||
companyForm: { ...state.companyForm },
|
||||
adminForm: {
|
||||
...state.adminForm,
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
sessionForm: { ...state.sessionForm },
|
||||
hermesForm: mergeHermesEmployeeForm(state.hermesForm),
|
||||
llmForm: {
|
||||
...state.llmForm,
|
||||
mainApiKey: '',
|
||||
backupApiKey: '',
|
||||
embeddingApiKey: '',
|
||||
rerankerApiKey: ''
|
||||
},
|
||||
renderForm: {
|
||||
...state.renderForm,
|
||||
jwtSecret: ''
|
||||
},
|
||||
logForm: { ...state.logForm },
|
||||
mailForm: {
|
||||
...state.mailForm,
|
||||
password: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getModelConfiguredKey(apiKeyKey) {
|
||||
return `${apiKeyKey}Configured`
|
||||
}
|
||||
|
||||
export function isModelSecretMask(value) {
|
||||
return value === MODEL_SECRET_MASK
|
||||
}
|
||||
|
||||
export function maskConfiguredModelSecrets(state) {
|
||||
for (const config of MODEL_API_KEY_CONFIGS) {
|
||||
const configuredKey = getModelConfiguredKey(config.apiKeyKey)
|
||||
|
||||
if (state.llmForm[configuredKey] && !normalizeValue(state.llmForm[config.apiKeyKey])) {
|
||||
state.llmForm[config.apiKeyKey] = MODEL_SECRET_MASK
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export function buildLlmPayload(llmForm) {
|
||||
const payload = { ...llmForm }
|
||||
|
||||
for (const config of MODEL_API_KEY_CONFIGS) {
|
||||
if (isModelSecretMask(payload[config.apiKeyKey])) {
|
||||
payload[config.apiKeyKey] = ''
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export function isRenderSecretMask(value) {
|
||||
return value === RENDER_SECRET_MASK
|
||||
}
|
||||
|
||||
export function maskConfiguredRenderSecret(state) {
|
||||
if (state.renderForm.jwtSecretConfigured && !normalizeValue(state.renderForm.jwtSecret)) {
|
||||
state.renderForm.jwtSecret = RENDER_SECRET_MASK
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export function buildRenderPayload(renderForm) {
|
||||
const payload = { ...renderForm }
|
||||
|
||||
if (isRenderSecretMask(payload.jwtSecret)) {
|
||||
payload.jwtSecret = ''
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export function persistSettings(state) {
|
||||
if (typeof window === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
window.sessionStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(sanitizeForStorage(state)))
|
||||
}
|
||||
|
||||
export function isModelConfigReady(provider, model, endpoint) {
|
||||
return Boolean(normalizeValue(provider) && normalizeValue(model) && normalizeValue(endpoint))
|
||||
}
|
||||
|
||||
export function computeSectionStatus(state) {
|
||||
return {
|
||||
profile: Boolean(
|
||||
normalizeValue(state.companyForm.companyName) &&
|
||||
normalizeValue(state.companyForm.displayName) &&
|
||||
normalizeValue(state.companyForm.copyright)
|
||||
),
|
||||
admin: Boolean(
|
||||
normalizeValue(state.adminForm.adminAccount) &&
|
||||
normalizeValue(state.adminForm.adminEmail) &&
|
||||
Number(state.adminForm.sessionTimeout) >= 5
|
||||
),
|
||||
session: Boolean(
|
||||
Number(state.sessionForm.conversationRetentionDays) >= 1 &&
|
||||
Number(state.sessionForm.conversationRetentionDays) <= 10
|
||||
),
|
||||
hermes: isHermesEmployeeSettingsReady(state.hermesForm),
|
||||
llm: Boolean(
|
||||
isModelConfigReady(state.llmForm.mainProvider, state.llmForm.mainModel, state.llmForm.mainEndpoint) &&
|
||||
isModelConfigReady(state.llmForm.backupProvider, state.llmForm.backupModel, state.llmForm.backupEndpoint) &&
|
||||
isModelConfigReady(
|
||||
state.llmForm.embeddingProvider,
|
||||
state.llmForm.embeddingModel,
|
||||
state.llmForm.embeddingEndpoint
|
||||
) &&
|
||||
isModelConfigReady(
|
||||
state.llmForm.rerankerProvider,
|
||||
state.llmForm.rerankerModel,
|
||||
state.llmForm.rerankerEndpoint
|
||||
)
|
||||
),
|
||||
rendering: Boolean(
|
||||
!state.renderForm.enabled ||
|
||||
(normalizeValue(state.renderForm.publicUrl) &&
|
||||
(normalizeValue(state.renderForm.jwtSecret) || state.renderForm.jwtSecretConfigured))
|
||||
),
|
||||
logs: Boolean(
|
||||
normalizeValue(state.logForm.level) &&
|
||||
Number(state.logForm.retentionDays) > 0 &&
|
||||
normalizeValue(state.logForm.logPath)
|
||||
),
|
||||
mail: Boolean(
|
||||
normalizeValue(state.mailForm.smtpHost) &&
|
||||
Number(state.mailForm.port) > 0 &&
|
||||
normalizeValue(state.mailForm.senderAddress) &&
|
||||
normalizeValue(state.mailForm.username)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user