Merge branch 'fix-error' of https://gitea.cirry.cn/cirry/electron-opencode into fix-error

This commit is contained in:
houakang
2026-04-12 12:59:09 +08:00
4 changed files with 155 additions and 17 deletions

View File

@@ -47,12 +47,28 @@
<!-- 服务状态栏 --> <!-- 服务状态栏 -->
<div class="flex items-center justify-between px-3 py-2 border-t border-gray-200"> <div class="flex items-center justify-between px-3 py-2 border-t border-gray-200">
<div class="flex items-center gap-2"> <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="w-2 h-2 rounded-full"
:class="{
'bg-green-500': appStore.serviceStatus === appStore.SERVICE_STATUS.RUNNING,
'bg-yellow-500': appStore.serviceStatus === appStore.SERVICE_STATUS.CONNECTING,
'bg-red-500': appStore.serviceStatus === appStore.SERVICE_STATUS.FAILED,
'bg-gray-400': appStore.serviceStatus === appStore.SERVICE_STATUS.IDLE,
}"
/>
<span class="text-xs text-gray-600"> <span class="text-xs text-gray-600">
{{ isServiceRunning ? '服务运行中' : '服务未启动' }} {{ statusLabel }}
</span> </span>
</div> </div>
<el-button v-if="!isServiceRunning" size="small" type="primary" @click="startService"> 启动 </el-button> <el-button
v-if="appStore.serviceStatus !== appStore.SERVICE_STATUS.RUNNING"
size="small"
type="primary"
:loading="appStore.serviceStatus === appStore.SERVICE_STATUS.CONNECTING"
@click="startService"
>
{{ appStore.serviceStatus === appStore.SERVICE_STATUS.FAILED ? '重新启动' : '启动' }}
</el-button>
<el-button v-else size="small" type="danger" plain @click="stopService"> 停止 </el-button> <el-button v-else size="small" type="danger" plain @click="stopService"> 停止 </el-button>
</div> </div>
@@ -88,7 +104,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue'; import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useAppStore } from '@/stores/app'; import { useAppStore } from '@/stores/app';
import { useHistoryStore } from '@/stores/history'; import { useHistoryStore } from '@/stores/history';
@@ -100,6 +116,20 @@ const route = useRoute();
const appStore = useAppStore(); const appStore = useAppStore();
const historyStore = useHistoryStore(); const historyStore = useHistoryStore();
const statusLabel = computed(() => {
switch (appStore.serviceStatus) {
case appStore.SERVICE_STATUS.CONNECTING:
return '连接中';
case appStore.SERVICE_STATUS.RUNNING:
return '服务运行中';
case appStore.SERVICE_STATUS.FAILED:
return '连接失败';
case appStore.SERVICE_STATUS.IDLE:
default:
return '服务未启动';
}
});
const menus = ref([ const menus = ref([
{ name: '新对话', index: '/', icon: 'plus' }, { name: '新对话', index: '/', icon: 'plus' },
{ name: '知识空间', index: '/knowledge', icon: 'book' }, { name: '知识空间', index: '/knowledge', icon: 'book' },
@@ -117,6 +147,9 @@ function handleMenuClick(item) {
const isServiceRunning = ref(false); const isServiceRunning = ref(false);
let checkInterval = null; let checkInterval = null;
let retryCount = 0;
const MAX_RETRY = 3;
const RETRY_DELAY = 3000;
// 查询历史会话列表 // 查询历史会话列表
async function loadHistorySessions() { async function loadHistorySessions() {
@@ -142,17 +175,42 @@ async function onHistoryClick(item) {
// 启动服务 // 启动服务
async function startService() { async function startService() {
if (appStore.serviceStatus === appStore.SERVICE_STATUS.CONNECTING) return;
appStore.serviceStatus = appStore.SERVICE_STATUS.CONNECTING;
retryCount = 0;
doStartService();
}
async function doStartService() {
try { try {
const info = await window.opencode.start(); const info = await window.opencode.start();
isServiceRunning.value = info.running;
// 更新 baseUrl 供 http 层使用
if (info.url) window.__opencodeBaseUrl = info.url;
// 服务启动成功后,初始化 SSE 连接
if (info.running) { if (info.running) {
isServiceRunning.value = true;
appStore.serviceStatus = appStore.SERVICE_STATUS.RUNNING;
// 更新 baseUrl 供 http 层使用
if (info.url) window.__opencodeBaseUrl = info.url;
// 服务启动成功后,初始化 SSE 连接
appStore.initSSE(); appStore.initSSE();
retryCount = 0;
} else {
handleRetry();
} }
} catch (err) { } catch (err) {
console.error('启动服务失败:', err); console.error('启动服务失败:', err);
handleRetry();
}
}
function handleRetry() {
if (retryCount < MAX_RETRY) {
retryCount++;
console.log(`[App.vue] 启动失败,第 ${retryCount} 次重连中...`);
setTimeout(doStartService, RETRY_DELAY);
} else {
appStore.serviceStatus = appStore.SERVICE_STATUS.FAILED;
isServiceRunning.value = false;
console.error(`[App.vue] 重连 ${MAX_RETRY} 次后依然失败,停止重连。`);
} }
} }
@@ -161,7 +219,9 @@ async function stopService() {
try { try {
await window.opencode.stop(); await window.opencode.stop();
isServiceRunning.value = false; isServiceRunning.value = false;
appStore.serviceStatus = appStore.SERVICE_STATUS.IDLE;
historyStore.clearHistory(); historyStore.clearHistory();
appStore.closeSSE();
} catch (err) { } catch (err) {
console.error('停止服务失败:', err); console.error('停止服务失败:', err);
} }
@@ -169,14 +229,17 @@ async function stopService() {
// 检查服务状态 // 检查服务状态
async function checkServiceStatus() { async function checkServiceStatus() {
// 如果正在重连中,则不进行状态更新,避免干扰
if (appStore.serviceStatus === appStore.SERVICE_STATUS.CONNECTING) return;
try { try {
const info = await window.opencode?.info(); const info = await window.opencode?.info();
const isRunning = info?.running || false; const isRunning = info?.running || false;
// 更新服务状态 // 更新服务状态
isServiceRunning.value = isRunning; isServiceRunning.value = isRunning;
if (isRunning) { if (isRunning) {
appStore.serviceStatus = appStore.SERVICE_STATUS.RUNNING;
// 服务运行中,更新 baseUrl 并加载历史记录 // 服务运行中,更新 baseUrl 并加载历史记录
if (info?.url) { if (info?.url) {
window.__opencodeBaseUrl = info.url; window.__opencodeBaseUrl = info.url;
@@ -185,13 +248,24 @@ async function checkServiceStatus() {
if (historyStore.historyItems.length === 0) { if (historyStore.historyItems.length === 0) {
loadHistorySessions(); loadHistorySessions();
} }
// 确保 SSE 连接已建立
if (!appStore.sseConnected) {
appStore.initSSE();
}
} else { } else {
// 只有当前是运行中才变更为未启动,防止覆盖 FAILED 状态
if (appStore.serviceStatus === appStore.SERVICE_STATUS.RUNNING) {
appStore.serviceStatus = appStore.SERVICE_STATUS.IDLE;
}
// 服务未运行,清空历史记录 // 服务未运行,清空历史记录
historyStore.clearHistory(); historyStore.clearHistory();
} }
} catch (err) { } catch (err) {
// 获取状态失败,视为服务断开 // 获取状态失败,视为服务断开
isServiceRunning.value = false; isServiceRunning.value = false;
if (appStore.serviceStatus === appStore.SERVICE_STATUS.RUNNING) {
appStore.serviceStatus = appStore.SERVICE_STATUS.IDLE;
}
historyStore.clearHistory(); historyStore.clearHistory();
} }
} }
@@ -201,6 +275,16 @@ onMounted(() => {
checkServiceStatus(); checkServiceStatus();
// 定期检查服务状态每2秒 // 定期检查服务状态每2秒
checkInterval = setInterval(checkServiceStatus, 2000); checkInterval = setInterval(checkServiceStatus, 2000);
// 监听启动服务的指令
watch(
() => appStore.startServiceFlag,
(newVal) => {
if (newVal > 0) {
startService();
}
}
);
}); });
onUnmounted(() => { onUnmounted(() => {

View File

@@ -3,8 +3,22 @@ import { ref, computed } from 'vue';
import { sseManager } from '@/http/sse.js'; import { sseManager } from '@/http/sse.js';
export const useAppStore = defineStore('app', () => { export const useAppStore = defineStore('app', () => {
// 服务运行状态常量
const SERVICE_STATUS = {
IDLE: 'idle', // 未启动
CONNECTING: 'connecting', // 连接中
RUNNING: 'running', // 服务运行中
FAILED: 'failed', // 连接失败
};
const title = ref('智聚超脑'); const title = ref('智聚超脑');
const collapsed = ref(false); const collapsed = ref(false);
const serviceStatus = ref(SERVICE_STATUS.IDLE);
const startServiceFlag = ref(0); // 用于触发外部启动服务的标记
function triggerStartService() {
startServiceFlag.value++;
}
// SSE 相关状态 // SSE 相关状态
const sseConnected = ref(false); const sseConnected = ref(false);
@@ -134,5 +148,10 @@ export const useAppStore = defineStore('app', () => {
isAssistantMessage, isAssistantMessage,
addAssistantMessageId, addAssistantMessageId,
clearAssistantMessageIds, clearAssistantMessageIds,
// 服务运行状态
serviceStatus,
SERVICE_STATUS,
startServiceFlag,
triggerStartService,
}; };
}); });

View File

@@ -44,7 +44,7 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { ChatDotRound, ArrowRight, ArrowDown } from '@element-plus/icons-vue'; import { ChatDotRound, ArrowRight, ArrowDown } from '@element-plus/icons-vue';
import { useAppStore } from '@/stores/app.js'; import { useAppStore } from '@/stores/app.js';
@@ -52,6 +52,7 @@ import { sseManager } from '@/http/sse.js';
import axios from 'axios'; import axios from 'axios';
const route = useRoute(); const route = useRoute();
const router = useRouter();
const appStore = useAppStore(); const appStore = useAppStore();
const isSending = ref(false); const isSending = ref(false);
const inputText = ref(''); const inputText = ref('');
@@ -69,9 +70,17 @@ async function loadHistoryMessages(sessionId) {
if (!sessionId) return; if (!sessionId) return;
try { try {
const baseUrl = window.__opencodeBaseUrl || 'http://127.0.0.1:4096'; const baseUrl = window.__opencodeBaseUrl || 'http://127.0.0.1:4096';
const response = await axios.get(`${baseUrl}/session/${sessionId}/message`); const requestUrl = `${baseUrl}/session/${sessionId}/message`;
console.log('[loadHistoryMessages] ========================================');
console.log('[loadHistoryMessages] 开始获取消息sessionId:', sessionId);
console.log('[loadHistoryMessages] 请求URL:', requestUrl);
const response = await axios.get(requestUrl);
const messagesData = response.data || []; const messagesData = response.data || [];
console.log('[loadHistoryMessages] 原始消息数据:', messagesData);
console.log('[loadHistoryMessages] 获取成功!消息数量:', messagesData.length);
console.log('[loadHistoryMessages] 原始消息数据 (JSON):', JSON.stringify(messagesData, null, 2));
console.log('[loadHistoryMessages] ========================================');
// 清空当前消息 // 清空当前消息
messages.value = []; messages.value = [];
@@ -136,10 +145,25 @@ async function loadHistoryMessages(sessionId) {
// 监听路由参数变化,加载对应会话 // 监听路由参数变化,加载对应会话
watch( watch(
routeSessionId, routeSessionId,
(newSessionId) => { async (newSessionId) => {
if (newSessionId) { if (newSessionId) {
currentSessionId.value = newSessionId; currentSessionId.value = newSessionId;
loadHistoryMessages(newSessionId); // 等待历史消息加载完毕,避免在 send 时 messages 被清空
await loadHistoryMessages(newSessionId);
// 处理从首页带过来的初始消息
const text = route.query.text;
if (text) {
inputText.value = text;
// 清除 query 中的 text防止刷新页面时重复发送
const query = { ...route.query };
delete query.text;
router.replace({ query });
// 触发发送逻辑
send();
}
// 确保 SSE 连接已建立 // 确保 SSE 连接已建立
if (!appStore.sseConnected) { if (!appStore.sseConnected) {
appStore.initSSE(); appStore.initSSE();
@@ -241,8 +265,14 @@ async function send() {
try { try {
await window.opencode.promptAsync(currentSessionId.value, text); await window.opencode.promptAsync(currentSessionId.value, text);
// 发送成功后等待 SSE 事件流推送 AI 响应isSending 由 message.completed 事件重置 // 发送成功后等待 SSE 事件流推送 AI 响应isSending 由 session.idle 事件重置
} catch (err) { } catch (err) {
console.error('发送指令失败:', err);
// 如果是服务未运行,尝试启动服务
if (appStore.serviceStatus !== appStore.SERVICE_STATUS.RUNNING) {
ElMessage.info('服务未运行,正在尝试启动...');
appStore.triggerStartService();
}
ElMessage.error(`发送失败: ${err.message}`); ElMessage.error(`发送失败: ${err.message}`);
isSending.value = false; isSending.value = false;
} }

View File

@@ -72,10 +72,12 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useAppStore } from '@/stores/app';
import { useHistoryStore } from '@/stores/history'; import { useHistoryStore } from '@/stores/history';
import { Document, Plus, Promotion } from '@element-plus/icons-vue'; import { Document, Plus, Promotion } from '@element-plus/icons-vue';
const router = useRouter(); const router = useRouter();
const appStore = useAppStore();
const historyStore = useHistoryStore(); const historyStore = useHistoryStore();
const inputText = ref(''); const inputText = ref('');
const isCreating = ref(false); const isCreating = ref(false);
@@ -94,10 +96,13 @@ async function handleSend() {
// 清空输入框 // 清空输入框
inputText.value = ''; inputText.value = '';
// 跳转到对话页面 // 跳转到对话页面,并将消息文本带入 query
router.push({ router.push({
path: '/chat', path: '/chat',
query: { sessionId: session.id }, query: {
sessionId: session.id,
text: text,
},
}); });
} catch (err) { } catch (err) {
console.error('创建会话失败:', err); console.error('创建会话失败:', err);