feat: 重构知识库系统,移除Hermes集成,增强RAG和同步功能
主要变更: - 移除Hermes智能体及相关回调服务 - 新增知识库RAG、同步、调度、规范化和索引任务服务 - 重构orchestrator服务,增强运行时聊天功能 - 更新前端聊天、政策制度、设置等页面样式和逻辑 - 更新expense_claims和document_intelligence服务 - 删除llm_wiki相关服务和测试文件 - 更新docker-compose配置和启动脚本
This commit is contained in:
@@ -1,287 +1,225 @@
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { fetchOntologyParse } from '../../services/ontology.js'
|
||||
|
||||
function isMarkdownTableDivider(line = '') {
|
||||
const value = String(line || '').trim()
|
||||
if (!value.includes('|')) return false
|
||||
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(value)
|
||||
}
|
||||
|
||||
function splitMarkdownTableRow(line = '') {
|
||||
return String(line || '')
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim())
|
||||
}
|
||||
|
||||
function buildAnswerBlocks(text = '') {
|
||||
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n')
|
||||
const blocks = []
|
||||
let index = 0
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = lines[index].trim()
|
||||
if (!line) {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
line.includes('|') &&
|
||||
index + 1 < lines.length &&
|
||||
isMarkdownTableDivider(lines[index + 1])
|
||||
) {
|
||||
const headers = splitMarkdownTableRow(line)
|
||||
const rows = []
|
||||
index += 2
|
||||
while (index < lines.length && lines[index].includes('|') && lines[index].trim()) {
|
||||
rows.push(splitMarkdownTableRow(lines[index]))
|
||||
index += 1
|
||||
}
|
||||
blocks.push({ type: 'table', headers, rows })
|
||||
continue
|
||||
}
|
||||
|
||||
const paragraphLines = [line]
|
||||
index += 1
|
||||
while (
|
||||
index < lines.length &&
|
||||
lines[index].trim() &&
|
||||
!(
|
||||
lines[index].includes('|') &&
|
||||
index + 1 < lines.length &&
|
||||
isMarkdownTableDivider(lines[index + 1])
|
||||
)
|
||||
) {
|
||||
paragraphLines.push(lines[index].trim())
|
||||
index += 1
|
||||
}
|
||||
blocks.push({ type: 'paragraph', text: paragraphLines.join(' ') })
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ChatView',
|
||||
props: {
|
||||
documents: { type: Array, required: true },
|
||||
docSearch: { type: String, default: '' },
|
||||
messages: { type: Array, required: true },
|
||||
uploadedFiles: { type: Array, required: true },
|
||||
activeCase: { type: Object, default: null },
|
||||
quickPrompts: { type: Array, required: true },
|
||||
draft: { type: String, default: '' },
|
||||
sending: { type: Boolean, default: false },
|
||||
messageList: { type: Object, default: null }
|
||||
},
|
||||
emits: ['send', 'upload', 'draft', 'approveCase', 'rejectCase', 'selectCase'],
|
||||
setup(props, { emit }) {
|
||||
const { currentUser } = useSystemState()
|
||||
const localMessageList = ref(null)
|
||||
const promptPage = ref(0)
|
||||
const semanticDraft = ref('查一下本周报销超标风险')
|
||||
const semanticLoading = ref(false)
|
||||
const semanticError = ref('')
|
||||
const semanticResult = ref(null)
|
||||
|
||||
const sessions = [
|
||||
{ title: '北京出差,酒店超标报销怎么处理?', time: '10:32', active: true },
|
||||
{ title: '发票抬头不一致怎么办', time: '09:48' },
|
||||
{ title: '借款冲销流程', time: '昨天' },
|
||||
{ title: '预算占用失败处理', time: '昨天' },
|
||||
{ title: '招待费报销范围', time: '05-11' },
|
||||
{ title: '差旅住宿标准如何匹配城市级别?', time: '05-10' },
|
||||
{ title: '电子发票验真失败如何处理?', time: '05-09' },
|
||||
{ title: '跨部门项目费用怎么归集?', time: '05-08' },
|
||||
{ title: '会议费和招待费如何区分?', time: '05-07' },
|
||||
{ title: '超预算申请需要哪些审批节点?', time: '05-06' },
|
||||
{ title: '海外差旅汇率按哪天计算?', time: '05-05' },
|
||||
{ title: '员工退票手续费是否可报销?', time: '05-04' }
|
||||
]
|
||||
|
||||
const prompts = [
|
||||
{ icon: 'mdi mdi-bed-outline', short: '差旅标准', text: '差旅报销特殊标准是什么?' },
|
||||
{ icon: 'mdi mdi-receipt-text-outline', short: '发票规范', text: '发票丢失如何处理?' },
|
||||
{ icon: 'mdi mdi-cash-refund', short: '借款冲销', text: '借款多久内需要冲销?' },
|
||||
{ icon: 'mdi mdi-file-chart-outline', short: '预算冲突', text: '预算不足如何申请?' },
|
||||
{ icon: 'mdi mdi-shield-check-outline', short: '审批要求', text: '酒店超标后如何申请例外报销?' },
|
||||
{ icon: 'mdi mdi-office-building-marker', short: '住宿标准', text: '差旅住宿标准按什么规则执行?' },
|
||||
{ icon: 'mdi mdi-file-question-outline', short: '材料补齐', text: '报销附件缺失怎么补交?' },
|
||||
{ icon: 'mdi mdi-alert-circle-outline', short: '风险等级', text: '哪些情况会触发中风险?' }
|
||||
]
|
||||
|
||||
const visiblePrompts = computed(() => prompts.slice((promptPage.value % 2) * 4, (promptPage.value % 2) * 4 + 4))
|
||||
|
||||
const hotQuestions = [
|
||||
'差旅报销标准是什么?',
|
||||
'酒店超标后如何申请例外报销?',
|
||||
'发票丢失如何处理?',
|
||||
'借款多久内需要冲销?',
|
||||
'预算超支如何申请?',
|
||||
'招待费报销需要哪些凭证?',
|
||||
'发票抬头不一致是否允许报销?',
|
||||
'报销附件缺失怎么补交?',
|
||||
'差旅住宿标准按什么规则执行?',
|
||||
'电子发票验真失败如何处理?'
|
||||
]
|
||||
|
||||
const similarQuestions = [
|
||||
{ text: '酒店超标后如何申请例外报销?', score: '96%' },
|
||||
{ text: '发票抬头不一致是否允许报销?', score: '92%' },
|
||||
{ text: '差旅住宿标准按什么规则执行?', score: '89%' },
|
||||
{ text: '预算不足时能否先提交报销?', score: '86%' },
|
||||
{ text: '电子发票验真失败是否可以先报销?', score: '84%' },
|
||||
{ text: '跨部门项目费用如何归集?', score: '81%' },
|
||||
{ text: '招待费报销需要哪些凭证?', score: '78%' },
|
||||
{ text: '借款冲销逾期会影响报销吗?', score: '76%' }
|
||||
]
|
||||
|
||||
const semanticExamples = [
|
||||
'查一下本周报销超标风险',
|
||||
'客户 A 这个月还有多少应收',
|
||||
'帮我直接付款给供应商B'
|
||||
]
|
||||
|
||||
const semanticConfidenceLabel = computed(() =>
|
||||
semanticResult.value ? `${Math.round((semanticResult.value.confidence || 0) * 100)}%` : '未解析'
|
||||
)
|
||||
const semanticEntitiesText = computed(() => {
|
||||
const items = semanticResult.value?.entities || []
|
||||
return items.length
|
||||
? items.map((item) => `${item.type}:${item.normalized_value}`).join(' / ')
|
||||
: '未识别'
|
||||
})
|
||||
const semanticTimeRangeText = computed(() => {
|
||||
const value = semanticResult.value?.time_range
|
||||
if (!value?.start_date || !value?.end_date) {
|
||||
return '未识别'
|
||||
}
|
||||
return `${value.start_date} ~ ${value.end_date}${value.granularity ? ` · ${value.granularity}` : ''}`
|
||||
})
|
||||
const semanticMetricsText = computed(() => {
|
||||
const items = semanticResult.value?.metrics || []
|
||||
return items.length
|
||||
? items
|
||||
.map((item) => {
|
||||
const suffix = item.top_n ? ` top_${item.top_n}` : ''
|
||||
return `${item.name}${suffix}`
|
||||
})
|
||||
.join(' / ')
|
||||
: '未识别'
|
||||
})
|
||||
const semanticConstraintsText = computed(() => {
|
||||
const items = semanticResult.value?.constraints || []
|
||||
return items.length
|
||||
? items.map((item) => `${item.field}${item.operator}${item.value}`).join(' / ')
|
||||
: '未识别'
|
||||
})
|
||||
const semanticRiskFlagsText = computed(() => {
|
||||
const items = semanticResult.value?.risk_flags || []
|
||||
return items.length ? items.join(' / ') : '未识别'
|
||||
})
|
||||
const semanticClarificationText = computed(() => {
|
||||
if (!semanticResult.value) {
|
||||
return '未解析'
|
||||
}
|
||||
if (!semanticResult.value.clarification_required) {
|
||||
return '无需澄清'
|
||||
}
|
||||
return semanticResult.value.clarification_question || '需要补充更多上下文'
|
||||
})
|
||||
const semanticResultJson = computed(() =>
|
||||
semanticResult.value ? JSON.stringify(semanticResult.value, null, 2) : ''
|
||||
)
|
||||
|
||||
function rotatePrompts() {
|
||||
promptPage.value += 1
|
||||
}
|
||||
|
||||
function applyPrompt(text) {
|
||||
emit('draft', text)
|
||||
semanticDraft.value = text
|
||||
}
|
||||
|
||||
function applySemanticExample(text) {
|
||||
semanticDraft.value = text
|
||||
}
|
||||
|
||||
function useDraftAsSemanticInput() {
|
||||
semanticDraft.value = props.draft || semanticDraft.value
|
||||
}
|
||||
|
||||
async function parseSemanticQuery() {
|
||||
const query = String(semanticDraft.value || '').trim()
|
||||
if (!query) {
|
||||
semanticError.value = '请输入要解析的问题。'
|
||||
semanticResult.value = null
|
||||
return
|
||||
}
|
||||
|
||||
semanticLoading.value = true
|
||||
semanticError.value = ''
|
||||
|
||||
try {
|
||||
semanticResult.value = await fetchOntologyParse({
|
||||
query,
|
||||
user_id: currentUser.value?.username || currentUser.value?.name || 'anonymous',
|
||||
context_json: {
|
||||
role_codes: currentUser.value?.roleCodes || [],
|
||||
is_admin: Boolean(currentUser.value?.isAdmin),
|
||||
name: currentUser.value?.name || '',
|
||||
role: currentUser.value?.role || '',
|
||||
position: currentUser.value?.position || '',
|
||||
grade: currentUser.value?.grade || ''
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
semanticResult.value = null
|
||||
semanticError.value = error.message || '语义解析失败,请稍后重试。'
|
||||
} finally {
|
||||
semanticLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.messages.length,
|
||||
() => {
|
||||
nextTick(() => localMessageList.value?.scrollTo({ top: localMessageList.value.scrollHeight, behavior: 'smooth' }))
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
emit,
|
||||
localMessageList,
|
||||
promptPage,
|
||||
sessions,
|
||||
prompts,
|
||||
visiblePrompts,
|
||||
hotQuestions,
|
||||
similarQuestions,
|
||||
rotatePrompts,
|
||||
applyPrompt,
|
||||
semanticDraft,
|
||||
semanticLoading,
|
||||
semanticError,
|
||||
semanticResult,
|
||||
semanticExamples,
|
||||
semanticConfidenceLabel,
|
||||
semanticEntitiesText,
|
||||
semanticTimeRangeText,
|
||||
semanticMetricsText,
|
||||
semanticConstraintsText,
|
||||
semanticRiskFlagsText,
|
||||
semanticClarificationText,
|
||||
semanticResultJson,
|
||||
buildAnswerBlocks,
|
||||
applySemanticExample,
|
||||
useDraftAsSemanticInput,
|
||||
parseSemanticQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { fetchOntologyParse } from '../../services/ontology.js'
|
||||
import { renderMarkdown } from '../../utils/markdown.js'
|
||||
|
||||
export default {
|
||||
name: 'ChatView',
|
||||
props: {
|
||||
documents: { type: Array, required: true },
|
||||
docSearch: { type: String, default: '' },
|
||||
messages: { type: Array, required: true },
|
||||
uploadedFiles: { type: Array, required: true },
|
||||
activeCase: { type: Object, default: null },
|
||||
quickPrompts: { type: Array, required: true },
|
||||
draft: { type: String, default: '' },
|
||||
sending: { type: Boolean, default: false },
|
||||
messageList: { type: Object, default: null }
|
||||
},
|
||||
emits: ['send', 'upload', 'draft', 'approveCase', 'rejectCase', 'selectCase'],
|
||||
setup(props, { emit }) {
|
||||
const { currentUser } = useSystemState()
|
||||
const localMessageList = ref(null)
|
||||
const promptPage = ref(0)
|
||||
const semanticDraft = ref('查一下本周报销超标风险')
|
||||
const semanticLoading = ref(false)
|
||||
const semanticError = ref('')
|
||||
const semanticResult = ref(null)
|
||||
|
||||
const sessions = [
|
||||
{ title: '北京出差,酒店超标报销怎么处理?', time: '10:32', active: true },
|
||||
{ title: '发票抬头不一致怎么办', time: '09:48' },
|
||||
{ title: '借款冲销流程', time: '昨天' },
|
||||
{ title: '预算占用失败处理', time: '昨天' },
|
||||
{ title: '招待费报销范围', time: '05-11' },
|
||||
{ title: '差旅住宿标准如何匹配城市级别?', time: '05-10' },
|
||||
{ title: '电子发票验真失败如何处理?', time: '05-09' },
|
||||
{ title: '跨部门项目费用怎么归集?', time: '05-08' },
|
||||
{ title: '会议费和招待费如何区分?', time: '05-07' },
|
||||
{ title: '超预算申请需要哪些审批节点?', time: '05-06' },
|
||||
{ title: '海外差旅汇率按哪天计算?', time: '05-05' },
|
||||
{ title: '员工退票手续费是否可报销?', time: '05-04' }
|
||||
]
|
||||
|
||||
const prompts = [
|
||||
{ icon: 'mdi mdi-bed-outline', short: '差旅标准', text: '差旅报销特殊标准是什么?' },
|
||||
{ icon: 'mdi mdi-receipt-text-outline', short: '发票规范', text: '发票丢失如何处理?' },
|
||||
{ icon: 'mdi mdi-cash-refund', short: '借款冲销', text: '借款多久内需要冲销?' },
|
||||
{ icon: 'mdi mdi-file-chart-outline', short: '预算冲突', text: '预算不足如何申请?' },
|
||||
{ icon: 'mdi mdi-shield-check-outline', short: '审批要求', text: '酒店超标后如何申请例外报销?' },
|
||||
{ icon: 'mdi mdi-office-building-marker', short: '住宿标准', text: '差旅住宿标准按什么规则执行?' },
|
||||
{ icon: 'mdi mdi-file-question-outline', short: '材料补齐', text: '报销附件缺失怎么补交?' },
|
||||
{ icon: 'mdi mdi-alert-circle-outline', short: '风险等级', text: '哪些情况会触发中风险?' }
|
||||
]
|
||||
|
||||
const visiblePrompts = computed(() => prompts.slice((promptPage.value % 2) * 4, (promptPage.value % 2) * 4 + 4))
|
||||
|
||||
const hotQuestions = [
|
||||
'差旅报销标准是什么?',
|
||||
'酒店超标后如何申请例外报销?',
|
||||
'发票丢失如何处理?',
|
||||
'借款多久内需要冲销?',
|
||||
'预算超支如何申请?',
|
||||
'招待费报销需要哪些凭证?',
|
||||
'发票抬头不一致是否允许报销?',
|
||||
'报销附件缺失怎么补交?',
|
||||
'差旅住宿标准按什么规则执行?',
|
||||
'电子发票验真失败如何处理?'
|
||||
]
|
||||
|
||||
const similarQuestions = [
|
||||
{ text: '酒店超标后如何申请例外报销?', score: '96%' },
|
||||
{ text: '发票抬头不一致是否允许报销?', score: '92%' },
|
||||
{ text: '差旅住宿标准按什么规则执行?', score: '89%' },
|
||||
{ text: '预算不足时能否先提交报销?', score: '86%' },
|
||||
{ text: '电子发票验真失败是否可以先报销?', score: '84%' },
|
||||
{ text: '跨部门项目费用如何归集?', score: '81%' },
|
||||
{ text: '招待费报销需要哪些凭证?', score: '78%' },
|
||||
{ text: '借款冲销逾期会影响报销吗?', score: '76%' }
|
||||
]
|
||||
|
||||
const semanticExamples = [
|
||||
'查一下本周报销超标风险',
|
||||
'客户 A 这个月还有多少应收',
|
||||
'帮我直接付款给供应商B'
|
||||
]
|
||||
|
||||
const semanticConfidenceLabel = computed(() =>
|
||||
semanticResult.value ? `${Math.round((semanticResult.value.confidence || 0) * 100)}%` : '未解析'
|
||||
)
|
||||
const semanticEntitiesText = computed(() => {
|
||||
const items = semanticResult.value?.entities || []
|
||||
return items.length
|
||||
? items.map((item) => `${item.type}:${item.normalized_value}`).join(' / ')
|
||||
: '未识别'
|
||||
})
|
||||
const semanticTimeRangeText = computed(() => {
|
||||
const value = semanticResult.value?.time_range
|
||||
if (!value?.start_date || !value?.end_date) {
|
||||
return '未识别'
|
||||
}
|
||||
return `${value.start_date} ~ ${value.end_date}${value.granularity ? ` · ${value.granularity}` : ''}`
|
||||
})
|
||||
const semanticMetricsText = computed(() => {
|
||||
const items = semanticResult.value?.metrics || []
|
||||
return items.length
|
||||
? items
|
||||
.map((item) => {
|
||||
const suffix = item.top_n ? ` top_${item.top_n}` : ''
|
||||
return `${item.name}${suffix}`
|
||||
})
|
||||
.join(' / ')
|
||||
: '未识别'
|
||||
})
|
||||
const semanticConstraintsText = computed(() => {
|
||||
const items = semanticResult.value?.constraints || []
|
||||
return items.length
|
||||
? items.map((item) => `${item.field}${item.operator}${item.value}`).join(' / ')
|
||||
: '未识别'
|
||||
})
|
||||
const semanticRiskFlagsText = computed(() => {
|
||||
const items = semanticResult.value?.risk_flags || []
|
||||
return items.length ? items.join(' / ') : '未识别'
|
||||
})
|
||||
const semanticClarificationText = computed(() => {
|
||||
if (!semanticResult.value) {
|
||||
return '未解析'
|
||||
}
|
||||
if (!semanticResult.value.clarification_required) {
|
||||
return '无需澄清'
|
||||
}
|
||||
return semanticResult.value.clarification_question || '需要补充更多上下文'
|
||||
})
|
||||
const semanticResultJson = computed(() =>
|
||||
semanticResult.value ? JSON.stringify(semanticResult.value, null, 2) : ''
|
||||
)
|
||||
|
||||
function rotatePrompts() {
|
||||
promptPage.value += 1
|
||||
}
|
||||
|
||||
function applyPrompt(text) {
|
||||
emit('draft', text)
|
||||
semanticDraft.value = text
|
||||
}
|
||||
|
||||
function applySemanticExample(text) {
|
||||
semanticDraft.value = text
|
||||
}
|
||||
|
||||
function useDraftAsSemanticInput() {
|
||||
semanticDraft.value = props.draft || semanticDraft.value
|
||||
}
|
||||
|
||||
async function parseSemanticQuery() {
|
||||
const query = String(semanticDraft.value || '').trim()
|
||||
if (!query) {
|
||||
semanticError.value = '请输入要解析的问题。'
|
||||
semanticResult.value = null
|
||||
return
|
||||
}
|
||||
|
||||
semanticLoading.value = true
|
||||
semanticError.value = ''
|
||||
|
||||
try {
|
||||
semanticResult.value = await fetchOntologyParse({
|
||||
query,
|
||||
user_id: currentUser.value?.username || currentUser.value?.name || 'anonymous',
|
||||
context_json: {
|
||||
role_codes: currentUser.value?.roleCodes || [],
|
||||
is_admin: Boolean(currentUser.value?.isAdmin),
|
||||
name: currentUser.value?.name || '',
|
||||
role: currentUser.value?.role || '',
|
||||
position: currentUser.value?.position || '',
|
||||
grade: currentUser.value?.grade || ''
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
semanticResult.value = null
|
||||
semanticError.value = error.message || '语义解析失败,请稍后重试。'
|
||||
} finally {
|
||||
semanticLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.messages.length,
|
||||
() => {
|
||||
nextTick(() => localMessageList.value?.scrollTo({ top: localMessageList.value.scrollHeight, behavior: 'smooth' }))
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
emit,
|
||||
localMessageList,
|
||||
promptPage,
|
||||
sessions,
|
||||
prompts,
|
||||
visiblePrompts,
|
||||
hotQuestions,
|
||||
similarQuestions,
|
||||
rotatePrompts,
|
||||
applyPrompt,
|
||||
semanticDraft,
|
||||
semanticLoading,
|
||||
semanticError,
|
||||
semanticResult,
|
||||
semanticExamples,
|
||||
semanticConfidenceLabel,
|
||||
semanticEntitiesText,
|
||||
semanticTimeRangeText,
|
||||
semanticMetricsText,
|
||||
semanticConstraintsText,
|
||||
semanticRiskFlagsText,
|
||||
semanticClarificationText,
|
||||
semanticResultJson,
|
||||
renderMarkdown,
|
||||
applySemanticExample,
|
||||
useDraftAsSemanticInput,
|
||||
parseSemanticQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,470 +1,470 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import LogTrendChart from '../../components/charts/LogTrendChart.vue'
|
||||
import DonutChart from '../../components/charts/DonutChart.vue'
|
||||
import { fetchAgentRuns } from '../../services/agentAssets.js'
|
||||
import { fetchSystemLogEntries } from '../../services/systemLogs.js'
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import { isManagerUser } from '../../utils/accessControl.js'
|
||||
|
||||
const POLL_INTERVAL_MS = 5000
|
||||
|
||||
const SOURCE_LABELS = {
|
||||
schedule: '定时任务',
|
||||
system_event: '系统事件',
|
||||
user_message: '用户触发'
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) {
|
||||
return '未结束'
|
||||
}
|
||||
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
return date.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
|
||||
function resolveStatusLabel(status) {
|
||||
if (status === 'running') {
|
||||
return '运行中'
|
||||
}
|
||||
if (status === 'succeeded') {
|
||||
return '已完成'
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return '失败'
|
||||
}
|
||||
if (status === 'blocked') {
|
||||
return '待确认'
|
||||
}
|
||||
return status || '未知'
|
||||
}
|
||||
|
||||
function resolveStatusTone(status) {
|
||||
if (status === 'running') {
|
||||
return 'warning'
|
||||
}
|
||||
if (status === 'succeeded') {
|
||||
return 'success'
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return 'danger'
|
||||
}
|
||||
if (status === 'blocked') {
|
||||
return 'muted'
|
||||
}
|
||||
return 'muted'
|
||||
}
|
||||
|
||||
function resolveRunSourceLabel(source) {
|
||||
return SOURCE_LABELS[source] || source || '未标记'
|
||||
}
|
||||
|
||||
function resolveRunModuleLabel(run) {
|
||||
const routeJson = run?.route_json || {}
|
||||
if (routeJson.job_type === 'llm_wiki_sync') {
|
||||
return '知识归纳'
|
||||
}
|
||||
if (routeJson.selected_agent) {
|
||||
return String(routeJson.selected_agent)
|
||||
}
|
||||
if (routeJson.folder) {
|
||||
return String(routeJson.folder)
|
||||
}
|
||||
return resolveRunSourceLabel(run?.source)
|
||||
}
|
||||
|
||||
function resolveRunTitle(run) {
|
||||
const routeJson = run?.route_json || {}
|
||||
if (routeJson.job_type === 'llm_wiki_sync') {
|
||||
return `LLM Wiki 归纳 · ${routeJson.folder || '未指定目录'}`
|
||||
}
|
||||
return `Hermes 调用 · ${resolveRunModuleLabel(run)}`
|
||||
}
|
||||
|
||||
function resolveRunLevel(run) {
|
||||
const progress = run?.route_json?.progress || {}
|
||||
if (run?.status === 'failed' || run?.error_message) {
|
||||
return 'ERROR'
|
||||
}
|
||||
if (run?.status === 'blocked' || Number(progress.failed_documents || 0) > 0) {
|
||||
return 'WARN'
|
||||
}
|
||||
if (run?.status === 'running') {
|
||||
return 'INFO'
|
||||
}
|
||||
return 'INFO'
|
||||
}
|
||||
|
||||
function resolveLevelTone(level) {
|
||||
if (level === 'ERROR') {
|
||||
return 'danger'
|
||||
}
|
||||
if (level === 'WARN') {
|
||||
return 'warning'
|
||||
}
|
||||
if (level === 'INFO') {
|
||||
return 'info'
|
||||
}
|
||||
return 'muted'
|
||||
}
|
||||
|
||||
function formatSummary(summary) {
|
||||
const text = String(summary || '').trim()
|
||||
if (!text) {
|
||||
return '暂无摘要。'
|
||||
}
|
||||
if (text.length <= 64) {
|
||||
return text
|
||||
}
|
||||
return `${text.slice(0, 64)}...`
|
||||
}
|
||||
|
||||
function resolveSystemLevelTone(level) {
|
||||
if (level === 'ERROR' || level === 'CRITICAL') {
|
||||
return 'danger'
|
||||
}
|
||||
if (level === 'WARNING' || level === 'WARN') {
|
||||
return 'warning'
|
||||
}
|
||||
if (level === 'INFO') {
|
||||
return 'info'
|
||||
}
|
||||
return 'muted'
|
||||
}
|
||||
|
||||
function resolveSystemOutcomeTone(outcome) {
|
||||
if (outcome === '失败') {
|
||||
return 'danger'
|
||||
}
|
||||
if (outcome === '异常' || outcome === '告警') {
|
||||
return 'warning'
|
||||
}
|
||||
if (outcome === '成功') {
|
||||
return 'success'
|
||||
}
|
||||
return 'muted'
|
||||
}
|
||||
|
||||
function formatHourBucketLabel(date) {
|
||||
return `${String(date.getHours()).padStart(2, '0')}:00`
|
||||
}
|
||||
|
||||
function buildTrendSeries(runs) {
|
||||
const parsedTimes = runs
|
||||
.map((run) => new Date(run?.started_at))
|
||||
.filter((date) => !Number.isNaN(date.getTime()))
|
||||
const latest = parsedTimes.length ? new Date(Math.max(...parsedTimes.map((date) => date.getTime()))) : new Date()
|
||||
latest.setMinutes(0, 0, 0)
|
||||
|
||||
const buckets = Array.from({ length: 8 }, (_, index) => {
|
||||
const date = new Date(latest)
|
||||
date.setHours(latest.getHours() - (7 - index))
|
||||
return {
|
||||
key: date.toISOString(),
|
||||
label: formatHourBucketLabel(date),
|
||||
total: 0,
|
||||
failed: 0
|
||||
}
|
||||
})
|
||||
const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket]))
|
||||
|
||||
for (const run of runs) {
|
||||
const date = new Date(run?.started_at)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
continue
|
||||
}
|
||||
date.setMinutes(0, 0, 0)
|
||||
const bucket = bucketMap.get(date.toISOString())
|
||||
if (!bucket) {
|
||||
continue
|
||||
}
|
||||
bucket.total += 1
|
||||
if (run.status === 'failed') {
|
||||
bucket.failed += 1
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
buckets,
|
||||
labels: buckets.map((bucket) => bucket.label),
|
||||
totals: buckets.map((bucket) => bucket.total),
|
||||
failures: buckets.map((bucket) => bucket.failed)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LogsView',
|
||||
components: {
|
||||
LogTrendChart,
|
||||
DonutChart
|
||||
},
|
||||
emits: ['summary-change'],
|
||||
setup(_, { emit }) {
|
||||
const router = useRouter()
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
|
||||
const activeTab = ref('hermes')
|
||||
const hermesLoading = ref(false)
|
||||
const systemLogLoading = ref(false)
|
||||
const hermesRuns = ref([])
|
||||
const systemSearchKeyword = ref('')
|
||||
const systemLevelFilter = ref('')
|
||||
const systemEventTypeFilter = ref('')
|
||||
const systemLogEntries = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const pageSizes = [10, 20, 50]
|
||||
const pageSizeOpen = ref(false)
|
||||
let pollTimer = 0
|
||||
|
||||
const isAdmin = computed(() => isManagerUser(currentUser.value))
|
||||
const filteredHermesRuns = computed(() => hermesRuns.value)
|
||||
const systemLevelOptions = computed(() =>
|
||||
Array.from(new Set(systemLogEntries.value.map((entry) => entry.level).filter(Boolean)))
|
||||
)
|
||||
const systemEventTypeOptions = computed(() =>
|
||||
Array.from(new Set(systemLogEntries.value.map((entry) => entry.event_type).filter(Boolean)))
|
||||
)
|
||||
const filteredSystemLogEntries = computed(() => {
|
||||
const keyword = systemSearchKeyword.value.trim().toLowerCase()
|
||||
return systemLogEntries.value.filter((entry) => {
|
||||
if (systemLevelFilter.value && entry.level !== systemLevelFilter.value) {
|
||||
return false
|
||||
}
|
||||
if (systemEventTypeFilter.value && entry.event_type !== systemEventTypeFilter.value) {
|
||||
return false
|
||||
}
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
const haystack = [
|
||||
entry.summary,
|
||||
entry.message,
|
||||
entry.logger,
|
||||
entry.request_id,
|
||||
entry.path,
|
||||
entry.event_type,
|
||||
entry.outcome,
|
||||
entry.source_file
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
return haystack.includes(keyword)
|
||||
})
|
||||
})
|
||||
const hermesRunCount = computed(() => hermesRuns.value.length)
|
||||
const runningRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'running').length)
|
||||
const completedRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'succeeded').length)
|
||||
const failedRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'failed').length)
|
||||
const trendSeries = computed(() => buildTrendSeries(filteredHermesRuns.value))
|
||||
const levelDistribution = computed(() => {
|
||||
const items = [
|
||||
{ level: 'INFO', count: 0, color: '#3b82f6' },
|
||||
{ level: 'WARN', count: 0, color: '#f59e0b' },
|
||||
{ level: 'ERROR', count: 0, color: '#ef4444' }
|
||||
]
|
||||
|
||||
for (const run of filteredHermesRuns.value) {
|
||||
const item = items.find((candidate) => candidate.level === resolveRunLevel(run))
|
||||
if (item) {
|
||||
item.count += 1
|
||||
}
|
||||
}
|
||||
|
||||
const total = items.reduce((sum, item) => sum + item.count, 0)
|
||||
return {
|
||||
items: items.map((item) => ({
|
||||
name: item.level,
|
||||
value: item.count,
|
||||
color: item.color,
|
||||
display: total ? `${item.count} (${Math.round((item.count / total) * 100)}%)` : '0'
|
||||
})),
|
||||
total
|
||||
}
|
||||
})
|
||||
const activeRows = computed(() =>
|
||||
activeTab.value === 'hermes' ? filteredHermesRuns.value : filteredSystemLogEntries.value
|
||||
)
|
||||
const totalCount = computed(() => activeRows.value.length)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
|
||||
const visiblePageItems = computed(() => {
|
||||
if (totalPages.value <= 6) {
|
||||
return Array.from({ length: totalPages.value }, (_, index) => index + 1)
|
||||
}
|
||||
return [1, 2, 3, 4, 5, 'ellipsis', totalPages.value]
|
||||
})
|
||||
const visibleHermesRuns = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredHermesRuns.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
const visibleSystemLogEntries = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredSystemLogEntries.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
function changePageSize(size) {
|
||||
pageSize.value = size
|
||||
pageSizeOpen.value = false
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
async function loadHermesRuns() {
|
||||
if (!isAdmin.value) {
|
||||
return
|
||||
}
|
||||
|
||||
hermesLoading.value = true
|
||||
try {
|
||||
const payload = await fetchAgentRuns({ agent: 'hermes', limit: 100 })
|
||||
hermesRuns.value = Array.isArray(payload) ? payload : []
|
||||
} catch (error) {
|
||||
toast(error.message || 'Hermes 日志加载失败。')
|
||||
} finally {
|
||||
hermesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectRun(runId) {
|
||||
router.push({
|
||||
name: 'app-log-detail',
|
||||
params: { logKind: 'hermes', logId: runId }
|
||||
})
|
||||
}
|
||||
|
||||
async function loadSystemLogs() {
|
||||
if (!isAdmin.value) {
|
||||
return
|
||||
}
|
||||
|
||||
systemLogLoading.value = true
|
||||
try {
|
||||
const payload = await fetchSystemLogEntries(300)
|
||||
systemLogEntries.value = Array.isArray(payload) ? payload : []
|
||||
} catch (error) {
|
||||
toast(error.message || '系统日志加载失败。')
|
||||
} finally {
|
||||
systemLogLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectSystemLog(entryId) {
|
||||
router.push({
|
||||
name: 'app-log-detail',
|
||||
params: { logKind: 'system', logId: entryId }
|
||||
})
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling()
|
||||
pollTimer = window.setInterval(() => {
|
||||
loadHermesRuns()
|
||||
loadSystemLogs()
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) {
|
||||
window.clearInterval(pollTimer)
|
||||
pollTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
activeTab,
|
||||
systemSearchKeyword,
|
||||
systemLevelFilter,
|
||||
systemEventTypeFilter
|
||||
],
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(totalPages, (value) => {
|
||||
if (currentPage.value > value) {
|
||||
currentPage.value = value
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [
|
||||
hermesRunCount.value,
|
||||
runningRunCount.value,
|
||||
completedRunCount.value,
|
||||
failedRunCount.value
|
||||
],
|
||||
([total, running, completed, failed]) => {
|
||||
emit('summary-change', { total, running, completed, failed })
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadHermesRuns()
|
||||
await loadSystemLogs()
|
||||
startPolling()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
changePageSize,
|
||||
completedRunCount,
|
||||
failedRunCount,
|
||||
filteredHermesRuns,
|
||||
filteredSystemLogEntries,
|
||||
formatDateTime,
|
||||
formatSummary,
|
||||
hermesLoading,
|
||||
hermesRunCount,
|
||||
hermesRuns,
|
||||
isAdmin,
|
||||
levelDistribution,
|
||||
loadHermesRuns,
|
||||
loadSystemLogs,
|
||||
currentPage,
|
||||
pageSize,
|
||||
pageSizeOpen,
|
||||
pageSizes,
|
||||
resolveLevelTone,
|
||||
resolveRunLevel,
|
||||
resolveRunModuleLabel,
|
||||
resolveRunSourceLabel,
|
||||
resolveRunTitle,
|
||||
resolveStatusLabel,
|
||||
resolveStatusTone,
|
||||
resolveSystemLevelTone,
|
||||
resolveSystemOutcomeTone,
|
||||
runningRunCount,
|
||||
selectRun,
|
||||
selectSystemLog,
|
||||
systemEventTypeFilter,
|
||||
systemEventTypeOptions,
|
||||
systemLevelFilter,
|
||||
systemLevelOptions,
|
||||
systemLogEntries,
|
||||
systemLogLoading,
|
||||
systemSearchKeyword,
|
||||
totalCount,
|
||||
totalPages,
|
||||
trendSeries,
|
||||
visiblePageItems,
|
||||
visibleHermesRuns,
|
||||
visibleSystemLogEntries
|
||||
}
|
||||
}
|
||||
}
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import LogTrendChart from '../../components/charts/LogTrendChart.vue'
|
||||
import DonutChart from '../../components/charts/DonutChart.vue'
|
||||
import { fetchAgentRuns } from '../../services/agentAssets.js'
|
||||
import { fetchSystemLogEntries } from '../../services/systemLogs.js'
|
||||
import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import { isManagerUser } from '../../utils/accessControl.js'
|
||||
|
||||
const POLL_INTERVAL_MS = 5000
|
||||
|
||||
const SOURCE_LABELS = {
|
||||
schedule: '定时任务',
|
||||
system_event: '系统事件',
|
||||
user_message: '用户触发'
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
if (!value) {
|
||||
return '未结束'
|
||||
}
|
||||
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
return date.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
|
||||
function resolveStatusLabel(status) {
|
||||
if (status === 'running') {
|
||||
return '运行中'
|
||||
}
|
||||
if (status === 'succeeded') {
|
||||
return '已完成'
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return '失败'
|
||||
}
|
||||
if (status === 'blocked') {
|
||||
return '待确认'
|
||||
}
|
||||
return status || '未知'
|
||||
}
|
||||
|
||||
function resolveStatusTone(status) {
|
||||
if (status === 'running') {
|
||||
return 'warning'
|
||||
}
|
||||
if (status === 'succeeded') {
|
||||
return 'success'
|
||||
}
|
||||
if (status === 'failed') {
|
||||
return 'danger'
|
||||
}
|
||||
if (status === 'blocked') {
|
||||
return 'muted'
|
||||
}
|
||||
return 'muted'
|
||||
}
|
||||
|
||||
function resolveRunSourceLabel(source) {
|
||||
return SOURCE_LABELS[source] || source || '未标记'
|
||||
}
|
||||
|
||||
function resolveRunModuleLabel(run) {
|
||||
const routeJson = run?.route_json || {}
|
||||
if (routeJson.job_type === 'knowledge_index_sync' || routeJson.job_type === 'llm_wiki_sync') {
|
||||
return '\u77e5\u8bc6\u5f52\u7eb3'
|
||||
}
|
||||
if (routeJson.selected_agent) {
|
||||
return String(routeJson.selected_agent)
|
||||
}
|
||||
if (routeJson.folder) {
|
||||
return String(routeJson.folder)
|
||||
}
|
||||
return resolveRunSourceLabel(run?.source)
|
||||
}
|
||||
|
||||
function resolveRunTitle(run) {
|
||||
const routeJson = run?.route_json || {}
|
||||
if (routeJson.job_type === 'knowledge_index_sync' || routeJson.job_type === 'llm_wiki_sync') {
|
||||
return `\u77e5\u8bc6\u5f52\u7eb3 \u00b7 ${routeJson.folder || '\u672a\u6307\u5b9a\u76ee\u5f55'}`
|
||||
}
|
||||
return `Hermes 调用 · ${resolveRunModuleLabel(run)}`
|
||||
}
|
||||
|
||||
function resolveRunLevel(run) {
|
||||
const progress = run?.route_json?.progress || {}
|
||||
if (run?.status === 'failed' || run?.error_message) {
|
||||
return 'ERROR'
|
||||
}
|
||||
if (run?.status === 'blocked' || Number(progress.failed_documents || 0) > 0) {
|
||||
return 'WARN'
|
||||
}
|
||||
if (run?.status === 'running') {
|
||||
return 'INFO'
|
||||
}
|
||||
return 'INFO'
|
||||
}
|
||||
|
||||
function resolveLevelTone(level) {
|
||||
if (level === 'ERROR') {
|
||||
return 'danger'
|
||||
}
|
||||
if (level === 'WARN') {
|
||||
return 'warning'
|
||||
}
|
||||
if (level === 'INFO') {
|
||||
return 'info'
|
||||
}
|
||||
return 'muted'
|
||||
}
|
||||
|
||||
function formatSummary(summary) {
|
||||
const text = String(summary || '').trim()
|
||||
if (!text) {
|
||||
return '暂无摘要。'
|
||||
}
|
||||
if (text.length <= 64) {
|
||||
return text
|
||||
}
|
||||
return `${text.slice(0, 64)}...`
|
||||
}
|
||||
|
||||
function resolveSystemLevelTone(level) {
|
||||
if (level === 'ERROR' || level === 'CRITICAL') {
|
||||
return 'danger'
|
||||
}
|
||||
if (level === 'WARNING' || level === 'WARN') {
|
||||
return 'warning'
|
||||
}
|
||||
if (level === 'INFO') {
|
||||
return 'info'
|
||||
}
|
||||
return 'muted'
|
||||
}
|
||||
|
||||
function resolveSystemOutcomeTone(outcome) {
|
||||
if (outcome === '失败') {
|
||||
return 'danger'
|
||||
}
|
||||
if (outcome === '异常' || outcome === '告警') {
|
||||
return 'warning'
|
||||
}
|
||||
if (outcome === '成功') {
|
||||
return 'success'
|
||||
}
|
||||
return 'muted'
|
||||
}
|
||||
|
||||
function formatHourBucketLabel(date) {
|
||||
return `${String(date.getHours()).padStart(2, '0')}:00`
|
||||
}
|
||||
|
||||
function buildTrendSeries(runs) {
|
||||
const parsedTimes = runs
|
||||
.map((run) => new Date(run?.started_at))
|
||||
.filter((date) => !Number.isNaN(date.getTime()))
|
||||
const latest = parsedTimes.length ? new Date(Math.max(...parsedTimes.map((date) => date.getTime()))) : new Date()
|
||||
latest.setMinutes(0, 0, 0)
|
||||
|
||||
const buckets = Array.from({ length: 8 }, (_, index) => {
|
||||
const date = new Date(latest)
|
||||
date.setHours(latest.getHours() - (7 - index))
|
||||
return {
|
||||
key: date.toISOString(),
|
||||
label: formatHourBucketLabel(date),
|
||||
total: 0,
|
||||
failed: 0
|
||||
}
|
||||
})
|
||||
const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket]))
|
||||
|
||||
for (const run of runs) {
|
||||
const date = new Date(run?.started_at)
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
continue
|
||||
}
|
||||
date.setMinutes(0, 0, 0)
|
||||
const bucket = bucketMap.get(date.toISOString())
|
||||
if (!bucket) {
|
||||
continue
|
||||
}
|
||||
bucket.total += 1
|
||||
if (run.status === 'failed') {
|
||||
bucket.failed += 1
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
buckets,
|
||||
labels: buckets.map((bucket) => bucket.label),
|
||||
totals: buckets.map((bucket) => bucket.total),
|
||||
failures: buckets.map((bucket) => bucket.failed)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LogsView',
|
||||
components: {
|
||||
LogTrendChart,
|
||||
DonutChart
|
||||
},
|
||||
emits: ['summary-change'],
|
||||
setup(_, { emit }) {
|
||||
const router = useRouter()
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
|
||||
const activeTab = ref('hermes')
|
||||
const hermesLoading = ref(false)
|
||||
const systemLogLoading = ref(false)
|
||||
const hermesRuns = ref([])
|
||||
const systemSearchKeyword = ref('')
|
||||
const systemLevelFilter = ref('')
|
||||
const systemEventTypeFilter = ref('')
|
||||
const systemLogEntries = ref([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const pageSizes = [10, 20, 50]
|
||||
const pageSizeOpen = ref(false)
|
||||
let pollTimer = 0
|
||||
|
||||
const isAdmin = computed(() => isManagerUser(currentUser.value))
|
||||
const filteredHermesRuns = computed(() => hermesRuns.value)
|
||||
const systemLevelOptions = computed(() =>
|
||||
Array.from(new Set(systemLogEntries.value.map((entry) => entry.level).filter(Boolean)))
|
||||
)
|
||||
const systemEventTypeOptions = computed(() =>
|
||||
Array.from(new Set(systemLogEntries.value.map((entry) => entry.event_type).filter(Boolean)))
|
||||
)
|
||||
const filteredSystemLogEntries = computed(() => {
|
||||
const keyword = systemSearchKeyword.value.trim().toLowerCase()
|
||||
return systemLogEntries.value.filter((entry) => {
|
||||
if (systemLevelFilter.value && entry.level !== systemLevelFilter.value) {
|
||||
return false
|
||||
}
|
||||
if (systemEventTypeFilter.value && entry.event_type !== systemEventTypeFilter.value) {
|
||||
return false
|
||||
}
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
const haystack = [
|
||||
entry.summary,
|
||||
entry.message,
|
||||
entry.logger,
|
||||
entry.request_id,
|
||||
entry.path,
|
||||
entry.event_type,
|
||||
entry.outcome,
|
||||
entry.source_file
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
return haystack.includes(keyword)
|
||||
})
|
||||
})
|
||||
const hermesRunCount = computed(() => hermesRuns.value.length)
|
||||
const runningRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'running').length)
|
||||
const completedRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'succeeded').length)
|
||||
const failedRunCount = computed(() => hermesRuns.value.filter((run) => run.status === 'failed').length)
|
||||
const trendSeries = computed(() => buildTrendSeries(filteredHermesRuns.value))
|
||||
const levelDistribution = computed(() => {
|
||||
const items = [
|
||||
{ level: 'INFO', count: 0, color: '#3b82f6' },
|
||||
{ level: 'WARN', count: 0, color: '#f59e0b' },
|
||||
{ level: 'ERROR', count: 0, color: '#ef4444' }
|
||||
]
|
||||
|
||||
for (const run of filteredHermesRuns.value) {
|
||||
const item = items.find((candidate) => candidate.level === resolveRunLevel(run))
|
||||
if (item) {
|
||||
item.count += 1
|
||||
}
|
||||
}
|
||||
|
||||
const total = items.reduce((sum, item) => sum + item.count, 0)
|
||||
return {
|
||||
items: items.map((item) => ({
|
||||
name: item.level,
|
||||
value: item.count,
|
||||
color: item.color,
|
||||
display: total ? `${item.count} (${Math.round((item.count / total) * 100)}%)` : '0'
|
||||
})),
|
||||
total
|
||||
}
|
||||
})
|
||||
const activeRows = computed(() =>
|
||||
activeTab.value === 'hermes' ? filteredHermesRuns.value : filteredSystemLogEntries.value
|
||||
)
|
||||
const totalCount = computed(() => activeRows.value.length)
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(totalCount.value / pageSize.value)))
|
||||
const visiblePageItems = computed(() => {
|
||||
if (totalPages.value <= 6) {
|
||||
return Array.from({ length: totalPages.value }, (_, index) => index + 1)
|
||||
}
|
||||
return [1, 2, 3, 4, 5, 'ellipsis', totalPages.value]
|
||||
})
|
||||
const visibleHermesRuns = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredHermesRuns.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
const visibleSystemLogEntries = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredSystemLogEntries.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
function changePageSize(size) {
|
||||
pageSize.value = size
|
||||
pageSizeOpen.value = false
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
async function loadHermesRuns() {
|
||||
if (!isAdmin.value) {
|
||||
return
|
||||
}
|
||||
|
||||
hermesLoading.value = true
|
||||
try {
|
||||
const payload = await fetchAgentRuns({ agent: 'hermes', limit: 100 })
|
||||
hermesRuns.value = Array.isArray(payload) ? payload : []
|
||||
} catch (error) {
|
||||
toast(error.message || 'Hermes 日志加载失败。')
|
||||
} finally {
|
||||
hermesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectRun(runId) {
|
||||
router.push({
|
||||
name: 'app-log-detail',
|
||||
params: { logKind: 'hermes', logId: runId }
|
||||
})
|
||||
}
|
||||
|
||||
async function loadSystemLogs() {
|
||||
if (!isAdmin.value) {
|
||||
return
|
||||
}
|
||||
|
||||
systemLogLoading.value = true
|
||||
try {
|
||||
const payload = await fetchSystemLogEntries(300)
|
||||
systemLogEntries.value = Array.isArray(payload) ? payload : []
|
||||
} catch (error) {
|
||||
toast(error.message || '系统日志加载失败。')
|
||||
} finally {
|
||||
systemLogLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectSystemLog(entryId) {
|
||||
router.push({
|
||||
name: 'app-log-detail',
|
||||
params: { logKind: 'system', logId: entryId }
|
||||
})
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling()
|
||||
pollTimer = window.setInterval(() => {
|
||||
loadHermesRuns()
|
||||
loadSystemLogs()
|
||||
}, POLL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) {
|
||||
window.clearInterval(pollTimer)
|
||||
pollTimer = 0
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
[
|
||||
activeTab,
|
||||
systemSearchKeyword,
|
||||
systemLevelFilter,
|
||||
systemEventTypeFilter
|
||||
],
|
||||
() => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(totalPages, (value) => {
|
||||
if (currentPage.value > value) {
|
||||
currentPage.value = value
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [
|
||||
hermesRunCount.value,
|
||||
runningRunCount.value,
|
||||
completedRunCount.value,
|
||||
failedRunCount.value
|
||||
],
|
||||
([total, running, completed, failed]) => {
|
||||
emit('summary-change', { total, running, completed, failed })
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadHermesRuns()
|
||||
await loadSystemLogs()
|
||||
startPolling()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling()
|
||||
})
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
changePageSize,
|
||||
completedRunCount,
|
||||
failedRunCount,
|
||||
filteredHermesRuns,
|
||||
filteredSystemLogEntries,
|
||||
formatDateTime,
|
||||
formatSummary,
|
||||
hermesLoading,
|
||||
hermesRunCount,
|
||||
hermesRuns,
|
||||
isAdmin,
|
||||
levelDistribution,
|
||||
loadHermesRuns,
|
||||
loadSystemLogs,
|
||||
currentPage,
|
||||
pageSize,
|
||||
pageSizeOpen,
|
||||
pageSizes,
|
||||
resolveLevelTone,
|
||||
resolveRunLevel,
|
||||
resolveRunModuleLabel,
|
||||
resolveRunSourceLabel,
|
||||
resolveRunTitle,
|
||||
resolveStatusLabel,
|
||||
resolveStatusTone,
|
||||
resolveSystemLevelTone,
|
||||
resolveSystemOutcomeTone,
|
||||
runningRunCount,
|
||||
selectRun,
|
||||
selectSystemLog,
|
||||
systemEventTypeFilter,
|
||||
systemEventTypeOptions,
|
||||
systemLevelFilter,
|
||||
systemLevelOptions,
|
||||
systemLogEntries,
|
||||
systemLogLoading,
|
||||
systemSearchKeyword,
|
||||
totalCount,
|
||||
totalPages,
|
||||
trendSeries,
|
||||
visiblePageItems,
|
||||
visibleHermesRuns,
|
||||
visibleSystemLogEntries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -39,8 +39,8 @@ const SECTION_DEFINITIONS = [
|
||||
id: 'llm',
|
||||
label: '大语言模型',
|
||||
title: '模型接入配置',
|
||||
desc: '主模型、备份模型与多模态模型',
|
||||
longDesc: '集中维护主模型、备份模型、VLM 模型和 Embedding 模型的接入参数,供 AI 助手和识别链路调用。',
|
||||
desc: '主模型、备份模型与检索模型',
|
||||
longDesc: '集中维护主模型、备份模型、Embedding 模型和 Reranker 模型的接入参数,供 AI 助手和检索链路调用。',
|
||||
actionLabel: '保存模型配置'
|
||||
},
|
||||
{
|
||||
@@ -82,7 +82,7 @@ const PROVIDER_OPTIONS = [
|
||||
CUSTOM_OPENAI_PROVIDER
|
||||
]
|
||||
|
||||
const PROVIDER_ENDPOINTS = {
|
||||
const PROVIDER_ENDPOINTS = {
|
||||
MiniMax: 'https://api.minimaxi.com/v1',
|
||||
GLM: 'https://open.bigmodel.cn/api/paas/v4/',
|
||||
Kimi: 'https://api.moonshot.ai/v1',
|
||||
@@ -90,8 +90,13 @@ const PROVIDER_ENDPOINTS = {
|
||||
Codex: 'https://api.openai.com/v1',
|
||||
Claude: 'https://api.anthropic.com/v1/',
|
||||
Gemini: 'https://generativelanguage.googleapis.com/v1beta/openai/',
|
||||
[CUSTOM_OPENAI_PROVIDER]: ''
|
||||
}
|
||||
[CUSTOM_OPENAI_PROVIDER]: ''
|
||||
}
|
||||
|
||||
const RERANKER_PROVIDER_ENDPOINTS = {
|
||||
Ali: 'https://dashscope.aliyuncs.com/api/v1/services/rerank/text-rerank/text-rerank',
|
||||
[CUSTOM_OPENAI_PROVIDER]: ''
|
||||
}
|
||||
|
||||
const LEGACY_PROVIDER_MAP = {
|
||||
'OpenAI Compatible': 'Codex',
|
||||
@@ -117,23 +122,23 @@ const MODEL_TEST_CONFIGS = {
|
||||
apiKeyKey: 'backupApiKey',
|
||||
capability: 'chat'
|
||||
},
|
||||
vlm: {
|
||||
label: 'VLM 模型',
|
||||
providerKey: 'vlmProvider',
|
||||
modelKey: 'vlmModel',
|
||||
endpointKey: 'vlmEndpoint',
|
||||
apiKeyKey: 'vlmApiKey',
|
||||
capability: 'chat'
|
||||
},
|
||||
embedding: {
|
||||
label: 'Embedding 模型',
|
||||
providerKey: 'embeddingProvider',
|
||||
modelKey: 'embeddingModel',
|
||||
endpointKey: 'embeddingEndpoint',
|
||||
apiKeyKey: 'embeddingApiKey',
|
||||
capability: 'embedding'
|
||||
}
|
||||
}
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
const MODEL_API_KEY_CONFIGS = Object.values(MODEL_TEST_CONFIGS)
|
||||
const SESSION_RETENTION_OPTIONS = Array.from({ length: 10 }, (_item, index) => ({
|
||||
@@ -159,9 +164,13 @@ function normalizeProviderValue(value, fallback = 'Codex') {
|
||||
return fallback
|
||||
}
|
||||
|
||||
function getProviderEndpoint(provider) {
|
||||
return PROVIDER_ENDPOINTS[provider] ?? ''
|
||||
}
|
||||
function getProviderEndpoint(provider) {
|
||||
return PROVIDER_ENDPOINTS[provider] ?? ''
|
||||
}
|
||||
|
||||
function getRerankerEndpoint(provider) {
|
||||
return RERANKER_PROVIDER_ENDPOINTS[provider] ?? getProviderEndpoint(provider)
|
||||
}
|
||||
|
||||
function buildDefaultState(companyProfile, currentUser) {
|
||||
const companyName = normalizeValue(companyProfile?.name) || 'X-Financial'
|
||||
@@ -201,21 +210,21 @@ function buildDefaultState(companyProfile, currentUser) {
|
||||
mainEndpoint: getProviderEndpoint('Codex'),
|
||||
mainApiKey: '',
|
||||
mainApiKeyConfigured: false,
|
||||
backupProvider: 'GLM',
|
||||
backupModel: 'glm-5.1',
|
||||
backupEndpoint: getProviderEndpoint('GLM'),
|
||||
backupApiKey: '',
|
||||
backupApiKeyConfigured: false,
|
||||
vlmProvider: 'Gemini',
|
||||
vlmModel: 'gemini-2.5-flash',
|
||||
vlmEndpoint: getProviderEndpoint('Gemini'),
|
||||
vlmApiKey: '',
|
||||
vlmApiKeyConfigured: false,
|
||||
embeddingProvider: 'GLM',
|
||||
embeddingModel: 'Embedding-3',
|
||||
backupProvider: 'GLM',
|
||||
backupModel: 'glm-5.1',
|
||||
backupEndpoint: getProviderEndpoint('GLM'),
|
||||
backupApiKey: '',
|
||||
backupApiKeyConfigured: false,
|
||||
embeddingProvider: 'GLM',
|
||||
embeddingModel: 'Embedding-3',
|
||||
embeddingEndpoint: getProviderEndpoint('GLM'),
|
||||
embeddingApiKey: '',
|
||||
embeddingApiKeyConfigured: false
|
||||
embeddingApiKeyConfigured: false,
|
||||
rerankerProvider: 'Ali',
|
||||
rerankerModel: 'gte-rerank-v2',
|
||||
rerankerEndpoint: getRerankerEndpoint('Ali'),
|
||||
rerankerApiKey: '',
|
||||
rerankerApiKeyConfigured: false
|
||||
},
|
||||
renderForm: {
|
||||
enabled: false,
|
||||
@@ -268,15 +277,18 @@ function readStoredSettings() {
|
||||
}
|
||||
|
||||
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.vlmProvider = normalizeProviderValue(mergedLlmForm.vlmProvider, baseState.llmForm.vlmProvider)
|
||||
mergedLlmForm.embeddingProvider = normalizeProviderValue(
|
||||
mergedLlmForm.embeddingProvider,
|
||||
baseState.llmForm.embeddingProvider
|
||||
)
|
||||
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 || {}) },
|
||||
@@ -302,8 +314,8 @@ function sanitizeForStorage(state) {
|
||||
...state.llmForm,
|
||||
mainApiKey: '',
|
||||
backupApiKey: '',
|
||||
vlmApiKey: '',
|
||||
embeddingApiKey: ''
|
||||
embeddingApiKey: '',
|
||||
rerankerApiKey: ''
|
||||
},
|
||||
renderForm: {
|
||||
...state.renderForm,
|
||||
@@ -402,11 +414,15 @@ function computeSectionStatus(state) {
|
||||
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.vlmProvider, state.llmForm.vlmModel, state.llmForm.vlmEndpoint) &&
|
||||
isModelConfigReady(
|
||||
state.llmForm.embeddingProvider,
|
||||
state.llmForm.embeddingModel,
|
||||
state.llmForm.embeddingEndpoint
|
||||
) &&
|
||||
isModelConfigReady(
|
||||
state.llmForm.rerankerProvider,
|
||||
state.llmForm.rerankerModel,
|
||||
state.llmForm.rerankerEndpoint
|
||||
)
|
||||
),
|
||||
rendering: Boolean(
|
||||
@@ -440,11 +456,11 @@ export default {
|
||||
const sessionRetentionPickerOpen = ref(false)
|
||||
const sessionRetentionPickerRef = ref(null)
|
||||
const modelTestState = ref({
|
||||
main: { status: 'idle', message: '' },
|
||||
backup: { status: 'idle', message: '' },
|
||||
vlm: { status: 'idle', message: '' },
|
||||
embedding: { status: 'idle', message: '' }
|
||||
})
|
||||
main: { status: 'idle', message: '' },
|
||||
backup: { status: 'idle', message: '' },
|
||||
embedding: { status: 'idle', message: '' },
|
||||
reranker: { status: 'idle', message: '' }
|
||||
})
|
||||
|
||||
const sections = SECTION_DEFINITIONS
|
||||
const logLevels = LOG_LEVELS
|
||||
@@ -484,8 +500,8 @@ export default {
|
||||
if (preserveModelApiKeys) {
|
||||
nextState.llmForm.mainApiKey = currentState.llmForm.mainApiKey
|
||||
nextState.llmForm.backupApiKey = currentState.llmForm.backupApiKey
|
||||
nextState.llmForm.vlmApiKey = currentState.llmForm.vlmApiKey
|
||||
nextState.llmForm.embeddingApiKey = currentState.llmForm.embeddingApiKey
|
||||
nextState.llmForm.rerankerApiKey = currentState.llmForm.rerankerApiKey
|
||||
}
|
||||
|
||||
if (preserveAdminPasswords) {
|
||||
@@ -582,7 +598,8 @@ export default {
|
||||
const provider = normalizeProviderValue(llmForm[config.providerKey], CUSTOM_OPENAI_PROVIDER)
|
||||
|
||||
llmForm[config.providerKey] = provider
|
||||
llmForm[config.endpointKey] = getProviderEndpoint(provider)
|
||||
llmForm[config.endpointKey] =
|
||||
slot === 'reranker' ? getRerankerEndpoint(provider) : getProviderEndpoint(provider)
|
||||
}
|
||||
|
||||
function getModelTestState(testKey) {
|
||||
@@ -735,12 +752,12 @@ export default {
|
||||
|
||||
async function saveLlmSection() {
|
||||
const llmForm = pageState.value.llmForm
|
||||
const modelConfigs = [
|
||||
['主模型', llmForm.mainProvider, llmForm.mainModel, llmForm.mainEndpoint],
|
||||
['备份模型', llmForm.backupProvider, llmForm.backupModel, llmForm.backupEndpoint],
|
||||
['VLM 模型', llmForm.vlmProvider, llmForm.vlmModel, llmForm.vlmEndpoint],
|
||||
['Embedding 模型', llmForm.embeddingProvider, llmForm.embeddingModel, llmForm.embeddingEndpoint]
|
||||
]
|
||||
const modelConfigs = [
|
||||
['主模型', llmForm.mainProvider, llmForm.mainModel, llmForm.mainEndpoint],
|
||||
['备份模型', llmForm.backupProvider, llmForm.backupModel, llmForm.backupEndpoint],
|
||||
['Embedding 模型', llmForm.embeddingProvider, llmForm.embeddingModel, llmForm.embeddingEndpoint],
|
||||
['Reranker 模型', llmForm.rerankerProvider, llmForm.rerankerModel, llmForm.rerankerEndpoint]
|
||||
]
|
||||
|
||||
for (const [label, provider, model, endpoint] of modelConfigs) {
|
||||
if (!isModelConfigReady(provider, model, endpoint)) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSystemState } from '../../composables/useSystemState.js'
|
||||
import { useToast } from '../../composables/useToast.js'
|
||||
import { recognizeOcrFiles } from '../../services/ocr.js'
|
||||
import { clearUserConversations, deleteConversation, fetchLatestConversation, runOrchestrator } from '../../services/orchestrator.js'
|
||||
import { renderMarkdown } from '../../utils/markdown.js'
|
||||
import {
|
||||
fetchExpenseClaimAttachmentAsset,
|
||||
fetchExpenseClaimDetail,
|
||||
@@ -228,69 +229,6 @@ function createMessage(role, text, attachments = [], extras = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
function isMarkdownTableDivider(line = '') {
|
||||
const value = String(line || '').trim()
|
||||
if (!value.includes('|')) return false
|
||||
return /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(value)
|
||||
}
|
||||
|
||||
function splitMarkdownTableRow(line = '') {
|
||||
return String(line || '')
|
||||
.trim()
|
||||
.replace(/^\|/, '')
|
||||
.replace(/\|$/, '')
|
||||
.split('|')
|
||||
.map((cell) => cell.trim())
|
||||
}
|
||||
|
||||
function buildAnswerBlocks(text = '') {
|
||||
const lines = String(text || '').replace(/\r\n/g, '\n').split('\n')
|
||||
const blocks = []
|
||||
let index = 0
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = lines[index].trim()
|
||||
if (!line) {
|
||||
index += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
line.includes('|') &&
|
||||
index + 1 < lines.length &&
|
||||
isMarkdownTableDivider(lines[index + 1])
|
||||
) {
|
||||
const headers = splitMarkdownTableRow(line)
|
||||
const rows = []
|
||||
index += 2
|
||||
while (index < lines.length && lines[index].includes('|') && lines[index].trim()) {
|
||||
rows.push(splitMarkdownTableRow(lines[index]))
|
||||
index += 1
|
||||
}
|
||||
blocks.push({ type: 'table', headers, rows })
|
||||
continue
|
||||
}
|
||||
|
||||
const paragraphLines = [line]
|
||||
index += 1
|
||||
while (
|
||||
index < lines.length &&
|
||||
lines[index].trim() &&
|
||||
!(
|
||||
lines[index].includes('|') &&
|
||||
index + 1 < lines.length &&
|
||||
isMarkdownTableDivider(lines[index + 1])
|
||||
)
|
||||
) {
|
||||
paragraphLines.push(lines[index].trim())
|
||||
index += 1
|
||||
}
|
||||
blocks.push({ type: 'paragraph', text: paragraphLines.join(' ') })
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
function formatMessageTime(value) {
|
||||
if (!value) {
|
||||
return nowTime()
|
||||
@@ -3424,31 +3362,39 @@ export default {
|
||||
}
|
||||
|
||||
const backendMessage = buildBackendMessage(rawText, effectiveFileNames, effectiveOcrSummary)
|
||||
const payload = await runOrchestrator({
|
||||
source: 'user_message',
|
||||
user_id: user.username || user.name || 'anonymous',
|
||||
conversation_id: conversationId.value || null,
|
||||
message: backendMessage,
|
||||
context_json: {
|
||||
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
|
||||
is_admin: Boolean(user.isAdmin),
|
||||
name: user.name || '',
|
||||
role: user.role || '',
|
||||
position: user.position || '',
|
||||
grade: user.grade || '',
|
||||
...buildClientTimeContext(),
|
||||
session_type: activeSessionType.value,
|
||||
entry_source: props.entrySource,
|
||||
user_input_text: systemGenerated ? '' : rawText,
|
||||
attachment_names: effectiveFileNames,
|
||||
attachment_count: effectiveFileNames.length,
|
||||
draft_claim_id: isKnowledgeSession.value ? undefined : draftClaimId.value || undefined,
|
||||
ocr_summary: effectiveOcrSummary,
|
||||
ocr_documents: effectiveOcrDocuments,
|
||||
...(linkedRequest.value && !isKnowledgeSession.value ? { request_context: linkedRequest.value } : {}),
|
||||
...extraContext
|
||||
}
|
||||
})
|
||||
const payload = await runOrchestrator(
|
||||
{
|
||||
source: 'user_message',
|
||||
user_id: user.username || user.name || 'anonymous',
|
||||
conversation_id: conversationId.value || null,
|
||||
message: backendMessage,
|
||||
context_json: {
|
||||
role_codes: Array.isArray(user.roleCodes) ? user.roleCodes : [],
|
||||
is_admin: Boolean(user.isAdmin),
|
||||
name: user.name || '',
|
||||
role: user.role || '',
|
||||
position: user.position || '',
|
||||
grade: user.grade || '',
|
||||
...buildClientTimeContext(),
|
||||
session_type: activeSessionType.value,
|
||||
entry_source: props.entrySource,
|
||||
user_input_text: systemGenerated ? '' : rawText,
|
||||
attachment_names: effectiveFileNames,
|
||||
attachment_count: effectiveFileNames.length,
|
||||
draft_claim_id: isKnowledgeSession.value ? undefined : draftClaimId.value || undefined,
|
||||
ocr_summary: effectiveOcrSummary,
|
||||
ocr_documents: effectiveOcrDocuments,
|
||||
...(linkedRequest.value && !isKnowledgeSession.value ? { request_context: linkedRequest.value } : {}),
|
||||
...extraContext
|
||||
}
|
||||
},
|
||||
isKnowledgeSession.value
|
||||
? {
|
||||
timeoutMs: 18000,
|
||||
timeoutMessage: '知识问答整理超时,已停止等待。建议缩小问题范围或稍后重试。'
|
||||
}
|
||||
: {}
|
||||
)
|
||||
responsePayload = payload
|
||||
|
||||
conversationId.value = String(payload?.conversation_id || '').trim() || conversationId.value
|
||||
@@ -3852,7 +3798,7 @@ export default {
|
||||
buildReviewRiskHint,
|
||||
buildReviewActionHint,
|
||||
buildReviewStatusTag,
|
||||
buildAnswerBlocks,
|
||||
renderMarkdown,
|
||||
buildExpenseQueryWindowLabel,
|
||||
buildExpenseQueryHint,
|
||||
getExpenseQueryActivePage,
|
||||
|
||||
Reference in New Issue
Block a user