完美!现在我看到了整个架构。我给你一个完整的前端停止/重启服务的实现方案:
import { ref, reactive } from 'vue'
import { Capacitor } from '@capacitor/core'
import { App as CapacitorApp } from '@capacitor/app'
const isNativePlatform = () => Capacitor.isNativePlatform()
/**
* 与原生 GoProcessPlugin 通信的 Composable
*/
export function useGoProcess() {
const isRestarting = ref(false)
const isStopping = ref(false)
const serviceStatus = reactive({
running: false,
port: 2025,
lastError: '',
})
/**
* 调用原生 GoProcessPlugin.restart() 方法
* - iOS/Android 通过 Capacitor Plugin 架构调用
* - Web 平台直接返回 true(无需操作)
*/
async function restartService(): Promise<boolean> {
if (!isNativePlatform()) {
console.log('[useGoProcess] Web platform - restart skipped')
return true
}
isRestarting.value = true
try {
// 动态导入 Plugin,避免 Web 平台加载错误
const { GoProcess } = await import('@capacitor/plugins')
console.log('[GoProcess] Calling restart()')
const result = await GoProcess.restart()
console.log('[GoProcess] Restart result:', result)
serviceStatus.running = true
serviceStatus.port = result.port || 2025
serviceStatus.lastError = ''
return true
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error)
console.error('[GoProcess] Restart failed:', errMsg)
serviceStatus.lastError = errMsg
serviceStatus.running = false
return false
} finally {
isRestarting.value = false
}
}
/**
* 调用原生 GoProcessPlugin.stop() 方法
*/
async function stopService(): Promise<boolean> {
if (!isNativePlatform()) {
console.log('[useGoProcess] Web platform - stop skipped')
return true
}
isStopping.value = true
try {
const { GoProcess } = await import('@capacitor/plugins')
console.log('[GoProcess] Calling stop()')
const result = await GoProcess.stop()
console.log('[GoProcess] Stop result:', result)
serviceStatus.running = false
serviceStatus.lastError = ''
return true
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error)
console.error('[GoProcess] Stop failed:', errMsg)
serviceStatus.lastError = errMsg
return false
} finally {
isStopping.value = false
}
}
/**
* 获取后端服务当前状态
*/
async function getStatus(): Promise<{ running: boolean; port: number }> {
if (!isNativePlatform()) {
return { running: true, port: 2025 }
}
try {
const { GoProcess } = await import('@capacitor/plugins')
const result = await GoProcess.getStatus()
serviceStatus.running = result.running
serviceStatus.port = result.port || 2025
return result
} catch (error) {
console.error('[GoProcess] getStatus failed:', error)
return { running: false, port: 0 }
}
}
return {
isRestarting,
isStopping,
serviceStatus,
restartService,
stopService,
getStatus,
}
}
export { isNativePlatform }import { ref, computed, onMounted, onUnmounted } from 'vue'
import { checkServerStatus } from '@/api/encv'
import { eventBus } from './useEventBus'
import { useGoProcess } from './useGoProcess'
let statusCheckInterval: number | null = null
export function useServerStatus() {
const isOnline = ref(false)
const lastError = ref('')
const backendPort = ref(2025)
const isChecking = ref(false)
const { restartService, stopService, isRestarting, isStopping } = useGoProcess()
/**
* 检查服务器状态(包括 HTTP 请求 + 服务状态)
*/
async function checkStatus() {
if (isChecking.value) return
isChecking.value = true
try {
const result = await checkServerStatus()
isOnline.value = result.online
lastError.value = result.error || ''
if (result.online) {
eventBus.emit('server:status', { online: true })
} else {
eventBus.emit('server:connection-error', { error: result.error || 'Connection failed' })
}
} catch (error) {
isOnline.value = false
const msg = error instanceof Error ? error.message : String(error)
lastError.value = msg
eventBus.emit('server:connection-error', { error: msg })
} finally {
isChecking.value = false
}
}
/**
* 重启后端服务
* 1. 调用原生 Plugin 的 restart()
* 2. 等待 2 秒后开始检查状态
* 3. 轮询最多 10 秒直到连接成功
*/
async function restartBackend(): Promise<boolean> {
console.log('[useServerStatus] Restarting backend...')
const success = await restartService()
if (!success) {
console.error('[useServerStatus] Service restart failed')
return false
}
// 等待后端进程启动
await new Promise(resolve => setTimeout(resolve, 2000))
// 轮询检查连接
for (let i = 0; i < 20; i++) {
await checkStatus()
if (isOnline.value) {
console.log('[useServerStatus] Backend is now online')
return true
}
await new Promise(resolve => setTimeout(resolve, 500))
}
console.warn('[useServerStatus] Backend did not come online within 10 seconds')
return false
}
/**
* 停止后端服务
* 1. 调用原生 Plugin 的 stop()
* 2. 等待 500ms 后检查状态
*/
async function stopBackend(): Promise<boolean> {
console.log('[useServerStatus] Stopping backend...')
const success = await stopService()
if (!success) {
console.error('[useServerStatus] Service stop failed')
return false
}
// 等待后端进程停止
await new Promise(resolve => setTimeout(resolve, 500))
await checkStatus()
console.log('[useServerStatus] Backend stopped')
return true
}
/**
* 启动自动检查循环
*/
function startAutoCheck() {
if (statusCheckInterval) return
// 立即检查一次
checkStatus()
// 每 5 秒检查一次
statusCheckInterval = window.setInterval(() => {
checkStatus()
}, 5000)
}
/**
* 停止自动检查
*/
function stopAutoCheck() {
if (statusCheckInterval) {
clearInterval(statusCheckInterval)
statusCheckInterval = null
}
}
onMounted(() => {
startAutoCheck()
})
onUnmounted(() => {
stopAutoCheck()
})
return {
isOnline,
lastError,
backendPort,
isChecking,
isRestarting,
isStopping,
checkStatus,
restartBackend,
stopBackend,
startAutoCheck,
stopAutoCheck,
}
}import { registerPlugin } from '@capacitor/core'
export interface GoProcessPlugin {
/**
* 重启后端服务
*/
restart(options?: any): Promise<{ success: boolean; port: number }>
/**
* 停止后端服务
*/
stop(options?: any): Promise<{ success: boolean }>
/**
* 获取后端服务状态
*/
getStatus(): Promise<{ running: boolean; port: number }>
/**
* 请求通知权限(Android 13+)
*/
requestNotificationPermission(): Promise<{ granted: boolean }>
/**
* 请求存储权限(Android 11+)
*/
requestStoragePermission(): Promise<{ granted: boolean; requiresSettings?: boolean }>
/**
* 检查权限状态
*/
checkPermissions(): Promise<{ notifications: boolean; storage: boolean }>
}
const GoProcess = registerPlugin<GoProcessPlugin>('GoProcess')
export { GoProcess }
export function isNative(): boolean {
return (window as any).Capacitor?.isNativePlatform() ?? false
}
export async function requestNotificationPermission() {
try {
if (!isNative()) return
await GoProcess.requestNotificationPermission()
} catch (e) {
console.error('[GoProcess] requestNotificationPermission failed:', e)
}
}
export async function requestStoragePermission() {
try {
if (!isNative()) return
await GoProcess.requestStoragePermission()
} catch (e) {
console.error('[GoProcess] requestStoragePermission failed:', e)
}
}
export async function checkPermissions() {
try {
if (!isNative()) {
return { notifications: true, storage: true }
}
return await GoProcess.checkPermissions()
} catch (e) {
console.error('[GoProcess] checkPermissions failed:', e)
return { notifications: false, storage: false }
}
}<template>
<ion-page>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button default-href="/tabs/settings"></ion-back-button>
</ion-buttons>
<ion-title>{{ t('settings.serverTitle') }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-list-header>
<ion-label>{{ t('settings.connection') }}</ion-label>
</ion-list-header>
<ion-item>
<ion-icon :icon="serverIcon" slot="start"></ion-icon>
<ion-input
v-model="serverUrl"
:label="t('settings.serverUrl')"
label-placement="stacked"
placeholder="http://127.0.0.1:2025"
@ionBlur="saveServerUrl"
></ion-input>
</ion-item>
<ion-item>
<ion-icon :icon="serverIcon" slot="start"></ion-icon>
<ion-label>
<h3>{{ t('settings.status') }}</h3>
<p>
<ion-badge :color="serverOnline ? 'success' : 'danger'">
{{ serverOnline ? t('settings.online') : t('settings.offline') }}
</ion-badge>
<span v-if="serverOnline && backendPort" class="port-info">:{{ backendPort }}</span>
</p>
<p v-if="!serverOnline && connectionError" class="connection-error">
{{ connectionError }}
</p>
</ion-label>
<!-- 服务控制按钮组 -->
<div slot="end" class="server-controls">
<!-- 刷新按钮 -->
<ion-button
fill="outline"
size="small"
@click="checkServer"
:disabled="isChecking"
>
<ion-icon :icon="refreshIcon" slot="icon-only"></ion-icon>
</ion-button>
<!-- 停止按钮(服务运行中时显示) -->
<ion-button
v-if="serverOnline"
fill="outline"
size="small"
color="danger"
@click="handleStop"
:disabled="isStopping"
>
<ion-spinner
v-if="isStopping"
slot="icon-only"
name="crescent"
></ion-spinner>
<ion-icon v-else :icon="stopIcon" slot="icon-only"></ion-icon>
</ion-button>
<!-- 重启/启动按钮(服务停止时显示) -->
<ion-button
v-if="!serverOnline && !isRestarting"
fill="outline"
size="small"
color="warning"
@click="handleRestart"
>
<ion-icon :icon="playIcon" slot="icon-only"></ion-icon>
</ion-button>
<!-- 重启中的加载动画 -->
<ion-button
v-if="isRestarting"
fill="outline"
size="small"
color="medium"
disabled
>
<ion-spinner slot="icon-only" name="crescent"></ion-spinner>
</ion-button>
</div>
</ion-item>
</ion-list>
<!-- 原生平台的权限管理 -->
<ion-list v-if="isNativePlatform">
<ion-list-header>
<ion-label>{{ t('settings.permissions') }}</ion-label>
</ion-list-header>
<ion-item>
<ion-icon :icon="notificationsIcon" slot="start"></ion-icon>
<ion-label>
<h3>{{ t('settings.notificationPermission') }}</h3>
<p>{{ permNotifications ? t('settings.granted') : t('settings.denied') }}</p>
</ion-label>
<ion-button
v-if="!permNotifications"
fill="outline"
size="small"
@click="handleRequestNotification"
>
{{ t('settings.request') }}
</ion-button>
</ion-item>
<ion-item>
<ion-icon :icon="folderOpen" slot="start"></ion-icon>
<ion-label>
<h3>{{ t('settings.storagePermission') }}</h3>
<p>{{ permStorage ? t('settings.granted') : t('settings.denied') }}</p>
</ion-label>
<ion-button
v-if="!permStorage"
fill="outline"
size="small"
@click="handleRequestStorage"
>
{{ t('settings.request') }}
</ion-button>
</ion-item>
</ion-list>
<!-- 服务日志区域(调试用) -->
<ion-list v-if="showDebugInfo">
<ion-list-header>
<ion-label>{{ t('settings.debug') }} (Debug Info)</ion-label>
</ion-list-header>
<ion-item>
<ion-label>
<p class="debug-info">{{ debugInfo }}</p>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
</ion-page>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import {
IonPage, IonHeader, IonToolbar, IonTitle, IonButtons, IonBackButton,
IonContent, IonList, IonListHeader, IonItem, IonIcon, IonLabel,
IonInput, IonBadge, IonButton, alertController, toastController, IonSpinner,
} from '@ionic/vue'
import {
server as serverIcon, refresh as refreshIcon,
stop as stopIcon, play as playIcon,
notifications as notificationsIcon, folderOpen,
} from 'ionicons/icons'
import { useServerStatus } from '@/composables/useServerStatus'
import { useI18n } from '@/composables/useI18n'
import { setApiBaseUrl, getServerUrl } from '@/api/encv'
import { isNative, requestNotificationPermission, requestStoragePermission, checkPermissions } from '@/plugins/GoProcess'
const {
isOnline: serverOnline,
lastError: connectionError,
checkStatus,
restartBackend,
stopBackend,
backendPort,
isRestarting,
isStopping,
isChecking,
} = useServerStatus()
const { t } = useI18n()
const serverUrl = ref(getServerUrl())
const isNativePlatform = ref(isNative())
const permNotifications = ref(false)
const permStorage = ref(false)
const showDebugInfo = ref(false) // 切换调试信息显示
let permissionCheckTimer: number | null = null
// 生成调试信息字符串
const debugInfo = computed(() => {
return `
Platform: ${isNativePlatform.value ? 'Native' : 'Web'}
Online: ${serverOnline.value}
Port: ${backendPort.value}
Error: ${connectionError.value || 'none'}
Restarting: ${isRestarting.value}
Stopping: ${isStopping.value}
`.trim()
})
/**
* 刷新权限状态
*/
async function refreshPermissions() {
const perms = await checkPermissions()
permNotifications.value = perms.notifications
permStorage.value = perms.storage
}
/**
* 请求通知权限
*/
async function handleRequestNotification() {
await requestNotificationPermission()
if (permissionCheckTimer) clearTimeout(permissionCheckTimer)
permissionCheckTimer = window.setTimeout(() => refreshPermissions(), 1000)
setTimeout(() => refreshPermissions(), 3000)
setTimeout(() => refreshPermissions(), 5000)
}
/**
* 请求存储权限
*/
async function handleRequestStorage() {
await requestStoragePermission()
if (permissionCheckTimer) clearTimeout(permissionCheckTimer)
permissionCheckTimer = window.setTimeout(() => refreshPermissions(), 1000)
setTimeout(() => refreshPermissions(), 3000)
setTimeout(() => refreshPermissions(), 5000)
}
/**
* 保存服务器 URL
*/
function saveServerUrl() {
const url = serverUrl.value.trim()
if (url) {
setApiBaseUrl(url)
checkStatus()
}
}
/**
* 检查服务器连接
*/
async function checkServer() {
await checkStatus()
const toast = await toastController.create({
message: serverOnline.value ? t('settings.serverOnline') : t('settings.serverOffline'),
duration: 1500,
color: serverOnline.value ? 'success' : 'danger',
})
await toast.present()
}
/**
* 处理重启后端
*/
async function handleRestart() {
const loadingToast = await toastController.create({
message: t('settings.restarting'),
duration: 30000,
})
await loadingToast.present()
try {
const success = await restartBackend()
await loadingToast.dismiss()
const resultToast = await toastController.create({
message: success ? t('settings.restartSuccess') : t('settings.restartFailed'),
duration: 2000,
color: success ? 'success' : 'danger',
})
await resultToast.present()
} catch (error) {
await loadingToast.dismiss()
const errorToast = await toastController.create({
message: `${t('settings.restartFailed')}: ${error}`,
duration: 3000,
color: 'danger',
})
await errorToast.present()
}
}
/**
* 处理停止后端(带确认弹窗)
*/
async function handleStop() {
const alert = await alertController.create({
header: t('settings.stopConfirm'),
message: t('settings.stopMessage') || '确定要停止后端服务吗?',
buttons: [
{
text: t('settings.cancel'),
role: 'cancel'
},
{
text: t('settings.stop'),
role: 'destructive',
handler: async () => {
try {
const success = await stopBackend()
const toast = await toastController.create({
message: success ? t('settings.stopped') : t('settings.stopFailed'),
duration: 2000,
color: success ? 'success' : 'danger',
})
await toast.present()
} catch (error) {
const errorToast = await toastController.create({
message: `${t('settings.stopFailed')}: ${error}`,
duration: 3000,
color: 'danger',
})
await errorToast.present()
}
},
},
],
})
await alert.present()
}
/**
* 切换调试信息显示
*/
function toggleDebugInfo() {
showDebugInfo.value = !showDebugInfo.value
}
onMounted(async () => {
if (isNativePlatform.value) {
await refreshPermissions()
}
// 双击标题切换调试信息(开发调试用)
const titleEl = document.querySelector('ion-title')
if (titleEl) {
titleEl.addEventListener('dblclick', toggleDebugInfo)
}
})
onUnmounted(() => {
if (permissionCheckTimer) clearTimeout(permissionCheckTimer)
const titleEl = document.querySelector('ion-title')
if (titleEl) {
titleEl.removeEventListener('dblclick', toggleDebugInfo)
}
})
</script>
<style scoped>
.connection-error {
color: var(--ion-color-danger);
font-size: 12px;
margin-top: 4px;
}
.port-info {
font-size: 12px;
opacity: 0.7;
margin-left: 4px;
}
.server-controls {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.debug-info {
font-family: monospace;
font-size: 12px;
color: var(--ion-color-medium);
white-space: pre-wrap;
word-break: break-all;
}
</style>{
"scripts": {
"build": "vue-tsc --noEmit && vite build",
"capacitor:sync:after": "node scripts/post-cap-sync.mjs"
}
}Vue Component (ServerDetail.vue)
↓
handleRestart() / handleStop()
↓
useServerStatus() composable
↓
restartBackend() / stopBackend()
↓
useGoProcess() composable
↓
@capacitor/plugins GoProcess.restart/stop()
↓
Kotlin: GoProcessPlugin.kt
↓
Intent → EncvGoService.kt
↓
SERVICE.ACTION_RESTART / ACTION_STOP
↓
Kill existing process + startEncvGo()
↓
Broadcast "com.encvgo.BACKEND_READY"
↓
MainActivity 接收 Broadcast
↓
notifyFrontendReady(port)
↓
Vue Component 感知状态变化
| 特性 | 实现 |
|---|---|
| 停止服务 | 调用 GoProcess.stop() → SERVICE.ACTION_STOP → destroyForcibly() |
| 重启服务 | 调用 GoProcess.restart() → SERVICE.ACTION_RESTART → 先 stop 再 start |
| 状态检查 | 定时轮询 HTTP /health,同时检查 Service 状态 |
| UI 反馈 | 加载动画、Toast 提示、错误消息 |
| 权限管理 | Android 12+ FOREGROUND_SERVICE、13+ 通知权限 |
| 调试模式 | 双击标题切换调试信息(开发用) |
- ACTION_START/RESTART/STOP 需要在
EncvGoService.kt的 companion object 中定义:
companion object {
const val ACTION_START = "com.encvgo.action.START"
const val ACTION_STOP = "com.encvgo.action.STOP"
const val ACTION_RESTART = "com.encvgo.action.RESTART"
}-
Web 平台兼容:所有 native 操作都 wrapped 在
isNativePlatform()检查中 -
错误处理:充分的 try-catch 和用户提示
现在你可以在前端完全控制后端服务的启停和重启!需要我补充什么吗?