Skip to content

Latest commit

 

History

History
821 lines (709 loc) · 21.3 KB

3.md

File metadata and controls

821 lines (709 loc) · 21.3 KB

完美!现在我看到了整个架构。我给你一个完整的前端停止/重启服务的实现方案:

📐 完整的前端服务控制架构

第一步:创建 src/composables/useGoProcess.ts(新建)

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 }

第二步:更新 src/composables/useServerStatus.ts(集成新逻辑)

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,
  }
}

第三步:更新 src/plugins/GoProcess.ts(Capacitor Plugin 定义)

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 }
  }
}

第四步:更新 src/views/ServerDetail.vue(集成新的服务控制)

<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>

第五步:更新 package.json 脚本

{
  "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+ 通知权限
调试模式 双击标题切换调试信息(开发用)

⚠️ 重要注意

  1. 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"
}
  1. Web 平台兼容:所有 native 操作都 wrapped 在 isNativePlatform() 检查中

  2. 错误处理:充分的 try-catch 和用户提示

现在你可以在前端完全控制后端服务的启停和重启!需要我补充什么吗?