ForcePilot/web/src/components/DebugComponent.vue

855 lines
21 KiB
Vue

<template>
<a-modal
v-model:open="showModal"
title="调试面板(请在生产环境中谨慎使用)"
width="90%"
:footer="null"
:maskClosable="true"
:destroyOnClose="true"
class="debug-modal"
>
<div :class="['log-viewer', { fullscreen: state.isFullscreen }]" ref="logViewer">
<div class="control-panel">
<div class="button-group">
<a-button
@click="fetchLogs"
:loading="state.fetching"
:icon="h(ReloadOutlined)"
class="icon-only"
>
</a-button>
<a-button @click="clearLogs" :icon="h(ClearOutlined)" class="icon-only"> </a-button>
<a-button @click="printSystemConfig">
<template #icon><SettingOutlined /></template>
系统配置
</a-button>
<a-button @click="printUserInfo">
<template #icon><UserOutlined /></template>
用户信息
</a-button>
<a-button @click="printDatabaseInfo">
<template #icon><DatabaseOutlined /></template>
知识库信息
</a-button>
<a-button @click="printAgentConfig">
<template #icon><RobotOutlined /></template>
智能体配置
</a-button>
<a-button @click="toggleDebugMode" :type="infoStore.debugMode ? 'primary' : 'default'">
<template #icon><BugOutlined /></template>
Debug 模式: {{ infoStore.debugMode ? '开启' : '关闭' }}
</a-button>
<a-button @click="toggleFullscreen">
<template #icon>
<FullscreenOutlined v-if="!state.isFullscreen" />
<FullscreenExitOutlined v-else />
</template>
{{ state.isFullscreen ? '退出全屏' : '全屏' }}
</a-button>
<a-tooltip :title="state.autoRefresh ? '点击停止自动刷新' : '点击开启自动刷新'">
<a-button
:type="state.autoRefresh ? 'primary' : 'default'"
:class="{ 'auto-refresh-button': state.autoRefresh }"
@click="toggleAutoRefresh(!state.autoRefresh)"
>
<template #icon>
<SyncOutlined :spin="state.autoRefresh" />
</template>
自动刷新
<span v-if="state.autoRefresh" class="refresh-interval">(5s)</span>
</a-button>
</a-tooltip>
<a-button @click="openUserSwitcher">
<template #icon><SwapOutlined /></template>
切换用户
</a-button>
</div>
<div class="filter-group">
<a-input-search
v-model:value="state.searchText"
placeholder="搜索日志..."
style="width: 200px; height: 32px"
@search="onSearch"
/>
<div class="log-level-selector">
<div class="multi-select-cards">
<div
v-for="level in logLevels"
:key="level.value"
class="option-card"
:class="{
selected: isLogLevelSelected(level.value),
unselected: !isLogLevelSelected(level.value)
}"
@click="toggleLogLevel(level.value)"
>
<div class="option-content">
<span class="option-text">{{ level.label }}</span>
<div class="option-indicator">
<CheckCircleOutlined v-if="isLogLevelSelected(level.value)" />
<PlusCircleOutlined v-else />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div ref="logContainer" class="log-container">
<div v-if="processedLogs.length" class="log-lines">
<div
v-for="(log, index) in processedLogs"
:key="index"
:class="['log-line', `level-${log.level.toLowerCase()}`]"
>
<span class="timestamp">{{ formatTimestamp(log.timestamp) }}</span>
<span class="level">{{ log.level }}</span>
<span class="module">{{ log.module }}</span>
<span class="message">{{ log.message }}</span>
</div>
</div>
<div v-else class="empty-logs">暂无日志</div>
</div>
<p v-if="error" class="error">{{ error }}</p>
<!-- 用户切换 Modal -->
<a-modal
v-model:open="state.showUserSwitcher"
title="切换用户"
:confirmLoading="state.switchingUser"
:footer="null"
:bodyStyle="{ padding: '12px' }"
>
<a-list item-layout="horizontal" :data-source="state.users">
<template #renderItem="{ item }">
<a-list-item @click="switchToUser(item)" style="cursor: pointer">
<a-list-item-meta :title="item.username" :description="item.role" />
</a-list-item>
</template>
<template #empty>
<a-empty description="暂无用户" />
</template>
</a-list>
</a-modal>
</div>
</a-modal>
</template>
<script setup>
import {
ref,
reactive,
computed,
defineModel,
onMounted,
onActivated,
onUnmounted,
nextTick,
toRaw,
h,
watch
} from 'vue'
const showModal = defineModel('show')
// 监听 showModal 变化,当打开时获取日志
watch(showModal, (isOpen) => {
if (isOpen) {
// 延迟一下确保 DOM 渲染完成
setTimeout(fetchLogs, 100)
}
})
import { useConfigStore } from '@/stores/config'
import { useUserStore } from '@/stores/user'
import { useDatabaseStore } from '@/stores/database'
import { useAgentStore } from '@/stores/agent'
import { useInfoStore } from '@/stores/info'
import { useThrottleFn } from '@vueuse/core'
import {
message,
Modal,
List as AList,
ListItem as AListItem,
ListItemMeta as AListItemMeta,
Empty as AEmpty
} from 'ant-design-vue'
import {
FullscreenOutlined,
FullscreenExitOutlined,
ReloadOutlined,
ClearOutlined,
SettingOutlined,
SyncOutlined,
CheckCircleOutlined,
PlusCircleOutlined,
UserOutlined,
DatabaseOutlined,
RobotOutlined,
BugOutlined,
SwapOutlined
} from '@ant-design/icons-vue'
import dayjs from '@/utils/time'
import { configApi } from '@/apis/system_api'
import { checkSuperAdminPermission } from '@/stores/user'
const configStore = useConfigStore()
const userStore = useUserStore()
const databaseStore = useDatabaseStore()
const agentStore = useAgentStore()
const infoStore = useInfoStore()
const config = configStore.config
// 定义日志级别
const logLevels = [
{ value: 'INFO', label: 'INFO' },
{ value: 'ERROR', label: 'ERROR' },
{ value: 'DEBUG', label: 'DEBUG' },
{ value: 'WARNING', label: 'WARNING' }
]
const logViewer = ref(null)
// 状态管理
const state = reactive({
fetching: false,
autoRefresh: false,
searchText: '',
selectedLevels: logLevels.map((l) => l.value),
rawLogs: [],
isFullscreen: false,
showUserSwitcher: false,
users: [],
switchingUser: false
})
const error = ref('')
const logContainer = ref(null)
let autoRefreshInterval = null
// 解析日志行
const parseLogLine = (line) => {
// 支持两种时间戳格式:带毫秒和不带毫秒
const match = line.match(
/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:,\d{3})?)\s*-\s*(\w+)\s*-\s*([^-]+?)\s*-\s*(.+)$/
)
if (match) {
return {
timestamp: match[1],
level: match[2],
module: match[3].trim(),
message: match[4].trim(),
raw: line
}
}
return null
}
// 格式化时间戳
const formatTimestamp = (timestamp) => {
try {
// 处理带毫秒的格式:将 "2025-03-10 08:26:37,269" 转换为 "2025-03-10 08:26:37.269"
let normalizedTimestamp = timestamp.replace(',', '.')
// 如果没有毫秒,添加 .000
if (!/\.\d{3}$/.test(normalizedTimestamp)) {
normalizedTimestamp += '.000'
}
const date = dayjs(normalizedTimestamp)
return date.isValid() ? date.format('HH:mm:ss.SSS') : timestamp
} catch (err) {
console.error('时间戳格式化错误:', err)
return timestamp
}
}
// 处理日志显示
const processedLogs = computed(() => {
return state.rawLogs
.map(parseLogLine)
.filter((log) => log !== null)
.filter((log) => {
if (!state.searchText) return true
return log.raw.toLowerCase().includes(state.searchText.toLowerCase())
})
})
// 获取日志数据
const fetchLogs = async () => {
if (!checkSuperAdminPermission()) return
state.fetching = true
try {
error.value = ''
// 将选中的日志级别转换为逗号分隔的字符串传递给后端
const levelsParam = state.selectedLevels.join(',')
const logData = await configApi.getLogs(levelsParam)
state.rawLogs = logData.log.split('\n').filter((line) => line.trim())
await nextTick()
const scrollToBottom = useThrottleFn(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
}
}, 100)
scrollToBottom()
} catch (err) {
error.value = `错误: ${err.message}`
} finally {
state.fetching = false
}
}
// 清空日志
const clearLogs = () => {
if (!checkSuperAdminPermission()) return
state.rawLogs = []
}
// 搜索功能
const onSearch = () => {
// 搜索会通过computed自动触发
}
// 日志级别选择相关方法
const isLogLevelSelected = (level) => {
return state.selectedLevels.includes(level)
}
const toggleLogLevel = (level) => {
const currentLevels = [...state.selectedLevels]
const index = currentLevels.indexOf(level)
if (index > -1) {
// 如果取消选中后没有选中的级别,默认全选
if (currentLevels.length === 1) {
return
}
currentLevels.splice(index, 1)
} else {
currentLevels.push(level)
}
state.selectedLevels = currentLevels
// 切换日志级别后重新获取数据
fetchLogs()
}
// 自动刷新
const toggleAutoRefresh = (value) => {
if (!checkSuperAdminPermission()) return
if (value) {
autoRefreshInterval = setInterval(fetchLogs, 5000)
state.autoRefresh = true
} else {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval)
autoRefreshInterval = null
}
state.autoRefresh = false
}
}
// 全屏切换
const toggleFullscreen = async () => {
if (!checkSuperAdminPermission()) return
try {
if (!state.isFullscreen) {
if (logViewer.value.requestFullscreen) {
await logViewer.value.requestFullscreen()
} else if (logViewer.value.webkitRequestFullscreen) {
await logViewer.value.webkitRequestFullscreen()
} else if (logViewer.value.msRequestFullscreen) {
await logViewer.value.msRequestFullscreen()
}
} else {
if (document.exitFullscreen) {
await document.exitFullscreen()
} else if (document.webkitExitFullscreen) {
await document.webkitExitFullscreen()
} else if (document.msExitFullscreen) {
await document.msExitFullscreen()
}
}
} catch (err) {
console.error('全屏切换失败:', err)
}
}
// 监听全屏变化
const handleFullscreenChange = () => {
state.isFullscreen = Boolean(
document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement
)
}
onMounted(() => {
document.addEventListener('fullscreenchange', handleFullscreenChange)
document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
document.addEventListener('msfullscreenchange', handleFullscreenChange)
})
onActivated(() => {
if (state.autoRefresh) {
toggleAutoRefresh(true)
} else if (showModal.value) {
fetchLogs()
}
})
onUnmounted(() => {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval)
autoRefreshInterval = null
}
document.removeEventListener('fullscreenchange', handleFullscreenChange)
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
document.removeEventListener('msfullscreenchange', handleFullscreenChange)
})
// 打印系统配置
const printSystemConfig = () => {
if (!checkSuperAdminPermission()) return
console.log('=== 系统配置 ===')
console.log(config)
}
// 打印用户信息
const printUserInfo = () => {
if (!checkSuperAdminPermission()) return
console.log('=== 用户信息 ===')
const userInfo = {
token: userStore.token ? '*** (已隐藏)' : null,
userId: userStore.userId,
username: userStore.username,
userIdLogin: userStore.userIdLogin,
phoneNumber: userStore.phoneNumber,
avatar: userStore.avatar,
userRole: userStore.userRole,
isLoggedIn: userStore.isLoggedIn,
isAdmin: userStore.isAdmin,
isSuperAdmin: userStore.isSuperAdmin
}
console.log(JSON.stringify(userInfo, null, 2))
}
// 打印知识库信息
const printDatabaseInfo = async () => {
if (!checkSuperAdminPermission()) return
try {
console.log('=== 知识库信息 ===')
console.log('基本信息:', {
databaseId: databaseStore.databaseId,
databaseName: databaseStore.database.name,
databaseDesc: databaseStore.database.description,
fileCount: Object.keys(databaseStore.database.files || {}).length
})
console.log('状态信息:', {
databaseLoading: databaseStore.state.databaseLoading,
refrashing: databaseStore.state.refrashing,
searchLoading: databaseStore.state.searchLoading,
lock: databaseStore.state.lock,
autoRefresh: databaseStore.state.autoRefresh,
queryParamsLoading: databaseStore.state.queryParamsLoading
})
console.log('查询参数:', {
queryParams: databaseStore.queryParams,
meta: databaseStore.meta,
selectedFileCount: databaseStore.selectedRowKeys.length
})
} catch (error) {
console.error('获取知识库信息失败:', error)
message.error('获取知识库信息失败: ' + error.message)
}
}
// 切换Debug模式
const toggleDebugMode = () => {
if (!checkSuperAdminPermission()) return
infoStore.toggleDebugMode()
}
// 打印智能体配置
const printAgentConfig = async () => {
if (!checkSuperAdminPermission()) return
try {
console.log('=== 智能体配置信息 ===')
// Store状态信息
console.log('Store 状态:', {
isInitialized: agentStore.isInitialized,
selectedAgentId: agentStore.selectedAgentId,
defaultAgentId: agentStore.defaultAgentId,
agentCount: agentStore.agentsList.length,
loadingStates: {
isLoadingAgents: agentStore.isLoadingAgents,
isLoadingConfig: agentStore.isLoadingConfig,
isLoadingTools: agentStore.isLoadingTools
},
error: agentStore.error,
hasConfigChanges: agentStore.hasConfigChanges
})
// 智能体列表信息
console.log('智能体列表:', {
count: agentStore.agentsList.length,
agents: toRaw(agentStore.agentsList)
})
// 当前选中智能体信息
if (agentStore.selectedAgent) {
console.log('当前选中智能体:', {
agent: toRaw(agentStore.selectedAgent),
isDefault: agentStore.isDefaultAgent,
configurableItemsCount: Object.keys(agentStore.configurableItems).length
})
// 当前智能体配置(仅管理员可见)
if (userStore.isAdmin) {
console.log('当前智能体配置:', {
current: toRaw(agentStore.agentConfig),
original: toRaw(agentStore.originalAgentConfig),
hasChanges: agentStore.hasConfigChanges
})
} else {
console.log('智能体配置: 需要管理员权限查看详细配置')
}
}
// 工具信息
const toolsList = agentStore.availableTools ? Object.values(agentStore.availableTools) : []
console.log('可用工具:', {
count: toolsList.length,
tools: toolsList
})
// 配置项信息(管理员可见)
if (userStore.isAdmin && agentStore.selectedAgent) {
console.log('可配置项:', toRaw(agentStore.configurableItems))
}
} catch (error) {
console.error('获取智能体配置失败:', error)
message.error('获取智能体配置失败: ' + error.message)
}
}
// 获取用户列表
const fetchUsers = async () => {
try {
const response = await fetch('/api/auth/users', {
headers: userStore.getAuthHeaders()
})
if (!response.ok) {
throw new Error('获取用户列表失败')
}
state.users = await response.json()
} catch (err) {
message.error(`获取用户列表失败: ${err.message}`)
}
}
// 打开用户选择器
const openUserSwitcher = () => {
if (!checkSuperAdminPermission()) return
state.showUserSwitcher = true
fetchUsers()
}
// 切换用户
const switchToUser = async (user) => {
if (!checkSuperAdminPermission()) return
// 危险操作确认
Modal.confirm({
title: '⚠️ 危险操作确认',
content: `确定要切换为用户 "${user.username}" 吗?此操作将被记录。`,
okText: '确认切换',
cancelText: '取消',
okType: 'danger',
onOk: async () => {
state.switchingUser = true
try {
const response = await fetch(`/api/auth/impersonate/${user.id}`, {
method: 'POST',
headers: userStore.getAuthHeaders()
})
if (!response.ok) {
const error = await response.json()
throw new Error(error.detail || '切换用户失败')
}
const data = await response.json()
// 设置新 token
localStorage.setItem('user_token', data.access_token)
message.success(`已切换用户: ${user.username}`)
state.showUserSwitcher = false
// 刷新页面以重新初始化应用
window.location.reload()
} catch (err) {
message.error(`切换失败: ${err.message}`)
} finally {
state.switchingUser = false
}
}
})
}
</script>
<style scoped>
.log-viewer.fullscreen {
padding: 16px;
}
.control-panel {
margin-bottom: 16px;
}
.button-group {
display: flex;
gap: 8px;
margin-bottom: 10px;
flex-wrap: wrap;
.ant-btn {
min-width: 80px;
height: 32px;
padding: 4px 12px;
font-size: 13px;
border-color: var(--gray-300);
color: var(--gray-700);
&.icon-only {
min-width: 32px;
padding: 0;
}
&:hover {
border-color: var(--main-color);
color: var(--main-color);
}
&.ant-btn-primary {
background-color: var(--main-color);
border-color: var(--main-color);
color: var(--gray-0);
&:hover,
&:focus {
background-color: var(--main-color);
border-color: var(--main-color);
color: var(--gray-0);
}
}
.anticon {
font-size: 14px;
}
}
.refresh-interval {
font-size: 12px;
opacity: 0.8;
margin-left: 2px;
}
.auto-refresh-button {
color: var(--gray-0);
}
}
.filter-group {
display: flex;
gap: 16px;
align-items: flex-start;
flex-wrap: wrap;
height: 32px;
@media (max-width: 768px) {
flex-direction: column;
gap: 12px;
}
}
.error {
color: var(--color-error-500);
}
.log-container {
height: calc(80vh - 200px);
overflow-y: auto;
background: var(--gray-0);
color: var(--gray-1000);
border-radius: 5px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
}
.log-lines {
padding: 8px;
}
.log-line {
padding: 2px 4px;
display: flex;
gap: 8px;
line-height: 1.4;
}
.log-line:hover {
background: rgba(255, 255, 255, 0.05);
}
.timestamp {
color: var(--color-success-500);
min-width: 80px;
}
.level {
min-width: 40px;
font-weight: bold;
}
.module {
color: var(--color-info-500);
min-width: 30px;
}
.message {
flex: 1;
white-space: pre-wrap;
word-break: break-all;
}
.level-info {
.level {
color: var(--color-success-500);
}
}
.level-error {
.level {
color: var(--color-error-500);
}
}
.level-debug {
.level {
color: var(--color-info-500);
}
}
.level-warning {
.level {
color: var(--color-warning-500);
}
}
.empty-logs {
padding: 16px;
text-align: center;
color: var(--gray-500);
}
@media (prefers-color-scheme: dark) {
.log-container {
background: var(--gray-900);
}
}
:fullscreen .log-container {
height: calc(100vh - 160px);
}
:-webkit-full-screen .log-container {
height: calc(100vh - 160px);
}
:-ms-fullscreen .log-container {
height: calc(100vh - 160px);
}
.multi-select-cards {
display: flex;
flex-direction: row;
gap: 10px;
.option-card {
border: 1px solid var(--gray-300);
border-radius: 6px;
padding: 0px 10px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--gray-0);
user-select: none;
height: 32px;
display: flex;
align-items: center;
&:hover {
border-color: var(--main-color);
background: var(--main-5);
}
&.selected {
border-color: var(--main-color);
background: var(--main-10);
.option-indicator {
color: var(--main-color);
}
.option-text {
color: var(--main-color);
font-weight: 500;
}
}
&.unselected {
.option-indicator {
color: var(--gray-400);
}
.option-text {
color: var(--gray-700);
}
}
.option-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
width: 100%;
}
.option-text {
flex: 1;
font-size: 12px;
text-align: center;
}
.option-indicator {
flex-shrink: 0;
font-size: 14px;
transition: color 0.2s ease;
}
}
}
/* 响应式适配 */
@media (max-width: 768px) {
.log-level-selector {
min-width: 280px;
}
.multi-select-cards .options-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>