ForcePilot/web/src/components/DebugComponent.vue
Wenjie Zhang d6dfa2b5e3 refactor(ui): 优化调试模式实现并改进界面样式
- 将调试模式状态管理移至 infoStore 实现全局统一控制
- 移除各组件中分散的调试模式状态管理代码
- 调整侧边栏和头部组件的边框和背景颜色
- 优化按钮和交互元素的悬停效果
- 更新文档中的 changelog 记录待办事项
2025-09-11 03:09:17 +08:00

685 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<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>
</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>
</div>
</template>
<script setup>
import { ref, onMounted, onActivated, onUnmounted, nextTick, reactive, computed, h, toRaw } from 'vue';
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 } from 'ant-design-vue';
import {
FullscreenOutlined,
FullscreenExitOutlined,
ReloadOutlined,
ClearOutlined,
SettingOutlined,
SyncOutlined,
CheckCircleOutlined,
PlusCircleOutlined,
UserOutlined,
DatabaseOutlined,
RobotOutlined,
BugOutlined
} from '@ant-design/icons-vue';
import dayjs from 'dayjs';
import { configApi } from '@/apis/system_api';
import { checkAdminPermission } 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,
});
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 => state.selectedLevels.includes(log.level))
.filter(log => {
if (!state.searchText) return true;
return log.raw.toLowerCase().includes(state.searchText.toLowerCase());
});
});
// 获取日志数据
const fetchLogs = async () => {
if (!checkAdminPermission()) return;
state.fetching = true;
try {
error.value = '';
const logData = await configApi.getLogs();
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 (!checkAdminPermission()) 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) {
currentLevels.splice(index, 1);
} else {
currentLevels.push(level);
}
state.selectedLevels = currentLevels;
};
// 自动刷新
const toggleAutoRefresh = (value) => {
if (!checkAdminPermission()) return;
if (value) {
autoRefreshInterval = setInterval(fetchLogs, 5000);
state.autoRefresh = true;
} else {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
state.autoRefresh = false;
}
};
// 全屏切换
const toggleFullscreen = async () => {
if (!checkAdminPermission()) 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(() => {
if (checkAdminPermission()) {
fetchLogs();
}
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
});
onActivated(() => {
if (!checkAdminPermission()) return;
if (state.autoRefresh) {
toggleAutoRefresh(true);
} else {
fetchLogs();
}
});
onUnmounted(() => {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
autoRefreshInterval = null;
}
document.removeEventListener('fullscreenchange', handleFullscreenChange);
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);
document.removeEventListener('msfullscreenchange', handleFullscreenChange);
});
// 打印系统配置
const printSystemConfig = () => {
if (!checkAdminPermission()) return;
console.log('=== 系统配置 ===');
console.log(config);
};
// 打印用户信息
const printUserInfo = () => {
if (!checkAdminPermission()) return;
console.log('=== 用户信息 ===');
const userInfo = {
token: userStore.token ? '*** (已隐藏)' : null,
userId: userStore.userId,
username: userStore.username,
userRole: userStore.userRole,
isLoggedIn: userStore.isLoggedIn,
isAdmin: userStore.isAdmin,
isSuperAdmin: userStore.isSuperAdmin
};
console.log(JSON.stringify(userInfo, null, 2));
};
// 打印知识库信息
const printDatabaseInfo = async () => {
if (!checkAdminPermission()) return;
try {
console.log('知识库信息', databaseStore.database);
} catch (error) {
console.error('获取知识库信息失败:', error);
message.error('获取知识库信息失败: ' + error.message);
}
};
// 切换Debug模式
const toggleDebugMode = () => {
if (!checkAdminPermission()) return;
infoStore.toggleDebugMode();
};
// 打印智能体配置
const printAgentConfig = async () => {
if (!checkAdminPermission()) return;
try {
console.log('=== 智能体配置信息 ===');
// 从store 获取状态信息
console.log('Store 状态:', {
isInitialized: agentStore.isInitialized,
selectedAgentId: agentStore.selectedAgentId,
defaultAgentId: agentStore.defaultAgentId,
loadingStates: {
isLoadingAgents: agentStore.isLoadingAgents,
isLoadingConfig: agentStore.isLoadingConfig,
isLoadingTools: agentStore.isLoadingTools
},
error: agentStore.error,
note: '线程相关状态已迁移到组件级别'
});
// 智能体列表信息
console.log('智能体列表 (从store):', {
count: agentStore.agentsList.length,
agents: toRaw(agentStore.agentsList)
});
// 当前选中智能体信息
if (agentStore.selectedAgent) {
console.log('当前选中智能体:', {
agent: toRaw(agentStore.selectedAgent),
isDefault: agentStore.isDefaultAgent,
configurableItems: Object.keys(agentStore.configurableItems).length
});
// 当前智能体配置
console.log('当前智能体配置:', {
current: toRaw(agentStore.agentConfig),
original: toRaw(agentStore.originalAgentConfig),
hasChanges: agentStore.hasConfigChanges
});
}
// 工具信息
console.log('可用工具:', toRaw(agentStore.availableTools));
// 注意线程和对话相关信息已迁移到AgentChatComponent组件级别
console.log('注意: 线程和对话状态已迁移到组件级别请在AgentChatComponent中查看相关信息');
} catch (error) {
console.error('获取智能体配置失败:', error);
message.error('获取智能体配置失败: ' + error.message);
}
};
</script>
<style scoped>
.log-viewer {
background: white;
}
.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);
&:hover {
background-color: var(--main-color);
border-color: var(--main-color);
}
}
.anticon {
font-size: 14px;
}
}
.refresh-interval {
font-size: 12px;
opacity: 0.8;
margin-left: 2px;
}
.auto-refresh-button {
color: white;
}
}
.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(--error-color);
}
.log-container {
height: calc(80vh - 200px);
overflow-y: auto;
background: #0C0C0C;
color: #D1D1D1;
border-radius: 5px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
}
.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: #6A9955;
min-width: 80px;
}
.level {
min-width: 40px;
font-weight: bold;
}
.module {
color: #569CD6;
min-width: 30px;
}
.message {
flex: 1;
white-space: pre-wrap;
word-break: break-all;
}
.level-info {
.level { color: #4EC9B0; }
}
.level-error {
.level { color: #F14C4C; }
}
.level-debug {
.level { color: #9CDCFE; }
}
.level-warning {
.level { color: #DCD900; }
}
.empty-logs {
padding: 16px;
text-align: center;
color: #666;
}
@media (prefers-color-scheme: dark) {
.log-container {
background: #1E1E1E;
}
}
: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>