feat: 加载历史记录
This commit is contained in:
@@ -36,12 +36,26 @@
|
||||
<div
|
||||
v-for="(item, index) in historyItems"
|
||||
:key="index"
|
||||
class="flex items-center w-[216px] h-[37px] justify-between rounded-[10px] pl-3 pr-[132px] mx-auto text-gray-500 hover:bg-gray-50 hover:text-gray-700 cursor-pointer transition-all duration-200"
|
||||
class="flex items-center w-[216px] h-[37px] justify-between rounded-[10px] pl-3 pr-3 mx-auto text-gray-500 hover:bg-gray-50 hover:text-gray-700 cursor-pointer transition-all duration-200"
|
||||
@click="onHistoryClick(item)"
|
||||
:title="item.name"
|
||||
>
|
||||
<span class="text-sm font-medium whitespace-nowrap truncate">{{ item.name }}</span>
|
||||
<span class="text-sm font-medium whitespace-nowrap truncate flex-1">{{ item.name }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 服务状态栏 -->
|
||||
<div class="flex items-center justify-between px-3 py-2 border-t border-gray-200">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-2 h-2 rounded-full" :class="isServiceRunning ? 'bg-green-500' : 'bg-gray-400'" />
|
||||
<span class="text-xs text-gray-600">
|
||||
{{ isServiceRunning ? '服务运行中' : '服务未启动' }}
|
||||
</span>
|
||||
</div>
|
||||
<el-button v-if="!isServiceRunning" size="small" type="primary" @click="startService"> 启动 </el-button>
|
||||
<el-button v-else size="small" type="danger" plain @click="stopService"> 停止 </el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户区域 -->
|
||||
<div class="flex items-center justify-between p-3 border-t border-gray-200">
|
||||
<div class="flex items-center">
|
||||
@@ -74,11 +88,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useAppStore } from '@/stores/app';
|
||||
import { House, Monitor, Expand, Fold, ChatDotRound, Search, Collection, Clock } from '@element-plus/icons-vue';
|
||||
import router from '@/router';
|
||||
import axios from 'axios';
|
||||
|
||||
const route = useRoute();
|
||||
const appStore = useAppStore();
|
||||
@@ -90,5 +105,110 @@ const menus = ref([
|
||||
{ name: '发现设备', index: '/bonjour', icon: 'server' },
|
||||
]);
|
||||
|
||||
const historyItems = ref([{ name: '历史记录示例 1' }, { name: '历史记录示例 2' }, { name: '历史记录示例 3' }]);
|
||||
const historyItems = ref([]);
|
||||
const isServiceRunning = ref(false);
|
||||
let checkInterval = null;
|
||||
|
||||
// 查询历史会话列表
|
||||
async function loadHistorySessions() {
|
||||
console.log('[loadHistorySessions] 开始加载历史会话...');
|
||||
try {
|
||||
const baseUrl = window.__opencodeBaseUrl || 'http://127.0.0.1:4096';
|
||||
console.log('[loadHistorySessions] baseUrl:', baseUrl);
|
||||
const response = await axios.get(`${baseUrl}/session`);
|
||||
console.log('[loadHistorySessions] 响应数据:', response.data);
|
||||
const sessions = response.data || [];
|
||||
console.log('[loadHistorySessions] 会话数量:', sessions.length);
|
||||
// 将会话列表转换为历史记录格式
|
||||
historyItems.value = sessions.map((session) => ({
|
||||
id: session.id,
|
||||
name: session.title || `会话 ${session.slug || session.id.slice(0, 8)}...`,
|
||||
slug: session.slug,
|
||||
created: session.time?.created,
|
||||
updated: session.time?.updated,
|
||||
}));
|
||||
console.log('[loadHistorySessions] 转换后的历史记录:', historyItems.value);
|
||||
} catch (err) {
|
||||
console.error('[loadHistorySessions] 加载历史会话失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 点击历史会话,跳转到对话页面并加载该会话
|
||||
function onHistoryClick(item) {
|
||||
router.push({
|
||||
path: '/chat',
|
||||
query: { sessionId: item.id },
|
||||
});
|
||||
}
|
||||
|
||||
// 启动服务
|
||||
async function startService() {
|
||||
try {
|
||||
const info = await window.opencode.start();
|
||||
isServiceRunning.value = info.running;
|
||||
// 更新 baseUrl 供 http 层使用
|
||||
if (info.url) window.__opencodeBaseUrl = info.url;
|
||||
} catch (err) {
|
||||
console.error('启动服务失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 停止服务
|
||||
async function stopService() {
|
||||
try {
|
||||
await window.opencode.stop();
|
||||
isServiceRunning.value = false;
|
||||
historyItems.value = [];
|
||||
} catch (err) {
|
||||
console.error('停止服务失败:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查服务状态
|
||||
async function checkServiceStatus() {
|
||||
try {
|
||||
console.log('[checkServiceStatus] 检查服务状态...');
|
||||
const info = await window.opencode?.info();
|
||||
console.log('[checkServiceStatus] 服务信息:', info);
|
||||
const isRunning = info?.running || false;
|
||||
|
||||
// 更新服务状态
|
||||
isServiceRunning.value = isRunning;
|
||||
|
||||
if (isRunning) {
|
||||
// 服务运行中,更新 baseUrl 并加载历史记录
|
||||
if (info?.url) {
|
||||
window.__opencodeBaseUrl = info.url;
|
||||
console.log('[checkServiceStatus] 更新 baseUrl:', info.url);
|
||||
}
|
||||
// 如果历史记录为空,则加载
|
||||
if (historyItems.value.length === 0) {
|
||||
console.log('[checkServiceStatus] 服务运行中,加载历史记录');
|
||||
loadHistorySessions();
|
||||
}
|
||||
} else {
|
||||
// 服务未运行,清空历史记录
|
||||
console.log('[checkServiceStatus] 服务未运行,清空历史记录');
|
||||
historyItems.value = [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[checkServiceStatus] 获取服务状态失败:', err);
|
||||
// 获取状态失败,视为服务断开
|
||||
isServiceRunning.value = false;
|
||||
historyItems.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始检查
|
||||
checkServiceStatus();
|
||||
// 定期检查服务状态(每2秒)
|
||||
checkInterval = setInterval(checkServiceStatus, 2000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
<template>
|
||||
<div class="chat-page">
|
||||
<!-- 服务状态栏 -->
|
||||
<div class="status-bar">
|
||||
<div class="status-indicator">
|
||||
<span class="dot" :class="serviceStatus" />
|
||||
<span class="status-text">{{ statusText }}</span>
|
||||
</div>
|
||||
<div class="status-actions">
|
||||
<el-button v-if="!isRunning" size="small" type="primary" :loading="isStarting" @click="startService"> 启动服务 </el-button>
|
||||
<el-button v-else size="small" type="danger" plain @click="stopService"> 停止服务 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div ref="messagesRef" class="messages">
|
||||
<div v-if="messages.length === 0" class="empty-hint">
|
||||
@@ -32,23 +20,23 @@
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 5 }"
|
||||
placeholder="输入消息,Ctrl+Enter 发送"
|
||||
:disabled="!isRunning || isSending"
|
||||
:disabled="isSending"
|
||||
resize="none"
|
||||
@keydown.ctrl.enter.prevent="send"
|
||||
/>
|
||||
<el-button type="primary" :disabled="!isRunning || isSending || !inputText.trim()" :loading="isSending" @click="send"> 发送 </el-button>
|
||||
<el-button type="primary" :disabled="isSending || !inputText.trim()" :loading="isSending" @click="send"> 发送 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted, nextTick } from 'vue';
|
||||
import { ref, computed, onUnmounted, nextTick, watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ChatDotRound } from '@element-plus/icons-vue';
|
||||
import { createEventSource } from '@/http/api.js';
|
||||
import { createEventSource, listMessagesAction } from '@/http/api.js';
|
||||
|
||||
const isRunning = ref(false);
|
||||
const isStarting = ref(false);
|
||||
const route = useRoute();
|
||||
const isSending = ref(false);
|
||||
const inputText = ref('');
|
||||
const messages = ref([]);
|
||||
@@ -57,15 +45,61 @@ const currentSessionId = ref(null);
|
||||
const assistantMessageIds = new Set();
|
||||
let eventSource = null;
|
||||
|
||||
const serviceStatus = computed(() => {
|
||||
if (isStarting.value) return 'starting';
|
||||
return isRunning.value ? 'running' : 'stopped';
|
||||
});
|
||||
// 从路由参数中获取 sessionId
|
||||
const routeSessionId = computed(() => route.query.sessionId);
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (isStarting.value) return '正在启动...';
|
||||
return isRunning.value ? '服务运行中' : '服务未启动';
|
||||
});
|
||||
// 加载历史消息
|
||||
async function loadHistoryMessages(sessionId) {
|
||||
if (!sessionId) return;
|
||||
try {
|
||||
const messagesData = await listMessagesAction(sessionId);
|
||||
// 清空当前消息
|
||||
messages.value = [];
|
||||
assistantMessageIds.clear();
|
||||
|
||||
// 处理历史消息
|
||||
messagesData.forEach((item) => {
|
||||
const { info, parts } = item;
|
||||
if (!info || !parts) return;
|
||||
|
||||
// 提取文本内容
|
||||
const text = parts
|
||||
.filter((part) => part.type === 'text')
|
||||
.map((part) => part.text)
|
||||
.join('');
|
||||
|
||||
if (text) {
|
||||
messages.value.push({
|
||||
id: info.id,
|
||||
role: info.role,
|
||||
text: text,
|
||||
});
|
||||
|
||||
// 记录 assistant 消息 ID
|
||||
if (info.role === 'assistant') {
|
||||
assistantMessageIds.add(info.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scrollToBottom();
|
||||
} catch (err) {
|
||||
console.error('加载历史消息失败:', err);
|
||||
ElMessage.error(`加载历史消息失败: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由参数变化,加载对应会话
|
||||
watch(
|
||||
routeSessionId,
|
||||
(newSessionId) => {
|
||||
if (newSessionId) {
|
||||
currentSessionId.value = newSessionId;
|
||||
loadHistoryMessages(newSessionId);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
@@ -128,35 +162,6 @@ function connectSSE() {
|
||||
};
|
||||
}
|
||||
|
||||
async function startService() {
|
||||
isStarting.value = true;
|
||||
try {
|
||||
const info = await window.opencode.start();
|
||||
isRunning.value = info.running;
|
||||
// 更新 baseUrl 供 http 层使用
|
||||
if (info.url) window.__opencodeBaseUrl = info.url;
|
||||
connectSSE();
|
||||
ElMessage.success('服务已启动');
|
||||
} catch (err) {
|
||||
ElMessage.error(`启动失败: ${err.message}`);
|
||||
} finally {
|
||||
isStarting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function stopService() {
|
||||
await window.opencode.stop();
|
||||
isRunning.value = false;
|
||||
currentSessionId.value = null;
|
||||
messages.value = [];
|
||||
assistantMessageIds.clear();
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
eventSource = null;
|
||||
}
|
||||
ElMessage.info('服务已停止');
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const text = inputText.value.trim();
|
||||
if (!text || isSending.value) return;
|
||||
@@ -186,18 +191,6 @@ async function send() {
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时同步服务状态
|
||||
window.opencode
|
||||
?.info()
|
||||
.then((info) => {
|
||||
isRunning.value = info.running;
|
||||
if (info.running) {
|
||||
if (info.url) window.__opencodeBaseUrl = info.url;
|
||||
connectSSE();
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (eventSource) eventSource.close();
|
||||
});
|
||||
@@ -212,52 +205,6 @@ onUnmounted(() => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot.stopped {
|
||||
background: #c0c4cc;
|
||||
}
|
||||
.dot.starting {
|
||||
background: #e6a23c;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
.dot.running {
|
||||
background: #67c23a;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.messages {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
Reference in New Issue
Block a user