Merge pull request #17 from xuemian168/main

feat(engine): 添加启动超时功能和强制终止引擎的支持
This commit is contained in:
Chen Janai
2025-08-28 12:25:33 +08:00
committed by GitHub
16 changed files with 170 additions and 18 deletions

View File

@@ -182,6 +182,16 @@
**数据类型:** 无数据 **数据类型:** 无数据
### `control.engine.forceKill`
**介绍:** 强制关闭启动超时的字幕引擎
**发起方:** 前端控制窗口
**接收方:** 后端控制窗口实例
**数据类型:** 无数据
### `caption.windowHeight.change` ### `caption.windowHeight.change`
**介绍:** 字幕窗口宽度发生改变 **介绍:** 字幕窗口宽度发生改变

View File

@@ -1,6 +1,7 @@
import socket import socket
import threading import threading
import json import json
# import time
from utils import thread_data, stdout_cmd, stderr from utils import thread_data, stdout_cmd, stderr
@@ -33,6 +34,7 @@ def start_server(port: int):
stderr(str(e)) stderr(str(e))
stdout_cmd('kill') stdout_cmd('kill')
return return
# time.sleep(20)
stdout_cmd('connect') stdout_cmd('connect')
client, addr = server.accept() client, addr = server.accept()

View File

@@ -159,6 +159,10 @@ class ControlWindow {
captionEngine.stop() captionEngine.stop()
}) })
ipcMain.on('control.engine.forceKill', () => {
captionEngine.forceKill()
})
ipcMain.on('control.captionLog.clear', () => { ipcMain.on('control.captionLog.clear', () => {
allConfig.captionLog.splice(0) allConfig.captionLog.splice(0)
}) })

View File

@@ -4,5 +4,6 @@ export default {
"engine.start.error": "Caption engine failed to start: ", "engine.start.error": "Caption engine failed to start: ",
"engine.output.parse.error": "Unable to parse caption engine output as a JSON object: ", "engine.output.parse.error": "Unable to parse caption engine output as a JSON object: ",
"engine.error": "Caption engine error: ", "engine.error": "Caption engine error: ",
"engine.shutdown.error": "Failed to shut down the caption engine process: " "engine.shutdown.error": "Failed to shut down the caption engine process: ",
"engine.start.timeout": "Caption engine startup timeout, automatically force stopped"
} }

View File

@@ -4,5 +4,6 @@ export default {
"engine.start.error": "字幕エンジンの起動に失敗しました: ", "engine.start.error": "字幕エンジンの起動に失敗しました: ",
"engine.output.parse.error": "字幕エンジンの出力を JSON オブジェクトとして解析できませんでした: ", "engine.output.parse.error": "字幕エンジンの出力を JSON オブジェクトとして解析できませんでした: ",
"engine.error": "字幕エンジンエラー: ", "engine.error": "字幕エンジンエラー: ",
"engine.shutdown.error": "字幕エンジンプロセスの終了に失敗しました: " "engine.shutdown.error": "字幕エンジンプロセスの終了に失敗しました: ",
"engine.start.timeout": "字幕エンジンの起動がタイムアウトしました。自動的に強制停止しました"
} }

View File

@@ -4,5 +4,6 @@ export default {
"engine.start.error": "字幕引擎启动失败:", "engine.start.error": "字幕引擎启动失败:",
"engine.output.parse.error": "字幕引擎输出内容无法解析为 JSON 对象:", "engine.output.parse.error": "字幕引擎输出内容无法解析为 JSON 对象:",
"engine.error": "字幕引擎错误:", "engine.error": "字幕引擎错误:",
"engine.shutdown.error": "字幕引擎进程关闭失败:" "engine.shutdown.error": "字幕引擎进程关闭失败:",
"engine.start.timeout": "字幕引擎启动超时,已自动强制停止"
} }

View File

@@ -13,7 +13,8 @@ export interface Controls {
modelPath: string, modelPath: string,
customized: boolean, customized: boolean,
customizedApp: string, customizedApp: string,
customizedCommand: string customizedCommand: string,
startTimeoutSeconds: number
} }
export interface Styles { export interface Styles {

View File

@@ -39,7 +39,8 @@ const defaultControls: Controls = {
translation: true, translation: true,
customized: false, customized: false,
customizedApp: '', customizedApp: '',
customizedCommand: '' customizedCommand: '',
startTimeoutSeconds: 30
}; };

View File

@@ -14,8 +14,9 @@ export class CaptionEngine {
process: any | undefined process: any | undefined
client: net.Socket | undefined client: net.Socket | undefined
port: number = 8080 port: number = 8080
status: 'running' | 'starting' | 'stopping' | 'stopped' = 'stopped' status: 'running' | 'starting' | 'stopping' | 'stopped' | 'starting-timeout' = 'stopped'
timerID: NodeJS.Timeout | undefined timerID: NodeJS.Timeout | undefined
startTimeoutID: NodeJS.Timeout | undefined
private getApp(): boolean { private getApp(): boolean {
if (allConfig.controls.customized) { if (allConfig.controls.customized) {
@@ -96,6 +97,11 @@ export class CaptionEngine {
public connect() { public connect() {
if(this.client) { Log.warn('Client already exists, ignoring...') } if(this.client) { Log.warn('Client already exists, ignoring...') }
// 清除启动超时计时器
if (this.startTimeoutID) {
clearTimeout(this.startTimeoutID)
this.startTimeoutID = undefined
}
this.client = net.createConnection({ port: this.port }, () => { this.client = net.createConnection({ port: this.port }, () => {
Log.info('Connected to caption engine server'); Log.info('Connected to caption engine server');
}); });
@@ -130,6 +136,17 @@ export class CaptionEngine {
this.process = spawn(this.appPath, this.command) this.process = spawn(this.appPath, this.command)
this.status = 'starting' this.status = 'starting'
Log.info('Caption Engine Starting, PID:', this.process.pid) Log.info('Caption Engine Starting, PID:', this.process.pid)
// 设置启动超时机制
const timeoutMs = allConfig.controls.startTimeoutSeconds * 1000
this.startTimeoutID = setTimeout(() => {
if (this.status === 'starting') {
Log.warn(`Engine start timeout after ${allConfig.controls.startTimeoutSeconds} seconds, forcing kill...`)
this.status = 'starting-timeout'
controlWindow.sendErrorMessage(i18n('engine.start.timeout'))
this.forceKill()
}
}, timeoutMs)
this.process.stdout.on('data', (data: any) => { this.process.stdout.on('data', (data: any) => {
const lines = data.toString().split('\n') const lines = data.toString().split('\n')
@@ -165,6 +182,11 @@ export class CaptionEngine {
} }
this.status = 'stopped' this.status = 'stopped'
clearInterval(this.timerID) clearInterval(this.timerID)
// 清理启动超时计时器
if (this.startTimeoutID) {
clearTimeout(this.startTimeoutID)
this.startTimeoutID = undefined
}
Log.info(`Engine exited with code ${code}`) Log.info(`Engine exited with code ${code}`)
}); });
} }
@@ -188,21 +210,47 @@ export class CaptionEngine {
} }
public kill(){ public kill(){
if(!this.process || !this.process.pid) return
if(this.status !== 'running'){ if(this.status !== 'running'){
Log.warn('Trying to kill engine which is not running, current status:', this.status) Log.warn('Trying to kill engine which is not running, current status:', this.status)
return
}
this.sendCommand('stop')
if(this.client){
this.client.destroy()
this.client = undefined
}
this.status = 'stopping'
this.timerID = setTimeout(() => {
if(this.status !== 'stopping') return
Log.warn('Engine process still not stopped, trying to kill...')
this.forceKill()
}, 4000);
}
public forceKill(){
if(!this.process || !this.process.pid) return
Log.warn('Force killing engine process, PID:', this.process.pid)
// 清理启动超时计时器
if (this.startTimeoutID) {
clearTimeout(this.startTimeoutID)
this.startTimeoutID = undefined
} }
Log.warn('Trying to kill engine process, PID:', this.process.pid)
if(this.client){ if(this.client){
this.client.destroy() this.client.destroy()
this.client = undefined this.client = undefined
} }
if (this.process.pid) { if (this.process.pid) {
let cmd = `kill ${this.process.pid}`; let cmd = `kill -9 ${this.process.pid}`;
if (process.platform === "win32") { if (process.platform === "win32") {
cmd = `taskkill /pid ${this.process.pid} /t /f` cmd = `taskkill /pid ${this.process.pid} /t /f`
} }
exec(cmd) exec(cmd, (error) => {
if (error) {
Log.error('Failed to force kill process:', error)
} else {
Log.info('Process force killed successfully')
}
})
} }
this.status = 'stopping' this.status = 'stopping'
} }

View File

@@ -109,6 +109,25 @@
v-model:value="currentModelPath" v-model:value="currentModelPath"
/> />
</div> </div>
<div class="input-item">
<a-popover>
<template #content>
<p class="label-hover-info">{{ $t('engine.startTimeoutInfo') }}</p>
</template>
<span
class="input-label info-label"
style="vertical-align: middle;"
>{{ $t('engine.startTimeout') }}</span>
</a-popover>
<a-input-number
class="input-area"
v-model:value="currentStartTimeoutSeconds"
:min="10"
:max="120"
:step="5"
:addon-after="$t('engine.seconds')"
/>
</div>
</a-card> </a-card>
</a-card> </a-card>
<div style="height: 20px;"></div> <div style="height: 20px;"></div>
@@ -139,6 +158,7 @@ const currentModelPath = ref<string>('')
const currentCustomized = ref<boolean>(false) const currentCustomized = ref<boolean>(false)
const currentCustomizedApp = ref('') const currentCustomizedApp = ref('')
const currentCustomizedCommand = ref('') const currentCustomizedCommand = ref('')
const currentStartTimeoutSeconds = ref<number>(30)
const langList = computed(() => { const langList = computed(() => {
for(let item of captionEngine.value){ for(let item of captionEngine.value){
@@ -160,6 +180,7 @@ function applyChange(){
engineControl.customized = currentCustomized.value engineControl.customized = currentCustomized.value
engineControl.customizedApp = currentCustomizedApp.value engineControl.customizedApp = currentCustomizedApp.value
engineControl.customizedCommand = currentCustomizedCommand.value engineControl.customizedCommand = currentCustomizedCommand.value
engineControl.startTimeoutSeconds = currentStartTimeoutSeconds.value
engineControl.sendControlsChange() engineControl.sendControlsChange()
@@ -181,6 +202,7 @@ function cancelChange(){
currentCustomized.value = engineControl.customized currentCustomized.value = engineControl.customized
currentCustomizedApp.value = engineControl.customizedApp currentCustomizedApp.value = engineControl.customizedApp
currentCustomizedCommand.value = engineControl.customizedCommand currentCustomizedCommand.value = engineControl.customizedCommand
currentStartTimeoutSeconds.value = engineControl.startTimeoutSeconds
} }
function selectFolderPath() { function selectFolderPath() {

View File

@@ -67,11 +67,26 @@
@click="openCaptionWindow" @click="openCaptionWindow"
>{{ $t('status.openCaption') }}</a-button> >{{ $t('status.openCaption') }}</a-button>
<a-button <a-button
v-if="!isStarting"
class="control-button" class="control-button"
:loading="pending && !engineEnabled" :loading="pending && !engineEnabled"
:disabled="pending || engineEnabled" :disabled="pending || engineEnabled"
@click="startEngine" @click="startEngine"
>{{ $t('status.startEngine') }}</a-button> >{{ $t('status.startEngine') }}</a-button>
<a-popconfirm
v-if="isStarting"
:title="$t('status.forceKillConfirm')"
:ok-text="$t('status.confirm')"
:cancel-text="$t('status.cancel')"
@confirm="forceKillEngine"
>
<a-button
danger
class="control-button"
type="primary"
:icon="h(LoadingOutlined)"
>{{ $t('status.forceKillStarting') }}</a-button>
</a-popconfirm>
<a-button <a-button
danger class="control-button" danger class="control-button"
:loading="pending && engineEnabled" :loading="pending && engineEnabled"
@@ -128,15 +143,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { EngineInfo } from '@renderer/types' import { EngineInfo } from '@renderer/types'
import { ref, watch } from 'vue' import { ref, watch, h } from 'vue'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useCaptionLogStore } from '@renderer/stores/captionLog' import { useCaptionLogStore } from '@renderer/stores/captionLog'
import { useSoftwareLogStore } from '@renderer/stores/softwareLog' import { useSoftwareLogStore } from '@renderer/stores/softwareLog'
import { useEngineControlStore } from '@renderer/stores/engineControl' import { useEngineControlStore } from '@renderer/stores/engineControl'
import { GithubOutlined, InfoCircleOutlined } from '@ant-design/icons-vue' import { GithubOutlined, InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons-vue'
const showAbout = ref(false) const showAbout = ref(false)
const pending = ref(false) const pending = ref(false)
const isStarting = ref(false)
const captionLog = useCaptionLogStore() const captionLog = useCaptionLogStore()
const { captionData } = storeToRefs(captionLog) const { captionData } = storeToRefs(captionLog)
@@ -158,8 +174,11 @@ function openCaptionWindow() {
function startEngine() { function startEngine() {
pending.value = true pending.value = true
isStarting.value = true
if(engineControl.engine === 'vosk' && engineControl.modelPath.trim() === '') { if(engineControl.engine === 'vosk' && engineControl.modelPath.trim() === '') {
engineControl.emptyModelPathErr() engineControl.emptyModelPathErr()
pending.value = false
isStarting.value = false
return return
} }
window.electron.ipcRenderer.send('control.engine.start') window.electron.ipcRenderer.send('control.engine.start')
@@ -170,6 +189,12 @@ function stopEngine() {
window.electron.ipcRenderer.send('control.engine.stop') window.electron.ipcRenderer.send('control.engine.stop')
} }
function forceKillEngine() {
pending.value = true
isStarting.value = false
window.electron.ipcRenderer.send('control.engine.forceKill')
}
function getEngineInfo() { function getEngineInfo() {
window.electron.ipcRenderer.invoke('control.engine.info').then((data: EngineInfo) => { window.electron.ipcRenderer.invoke('control.engine.info').then((data: EngineInfo) => {
pid.value = data.pid pid.value = data.pid
@@ -181,12 +206,16 @@ function getEngineInfo() {
}) })
} }
watch(engineEnabled, () => { watch(engineEnabled, (enabled) => {
pending.value = false pending.value = false
if (enabled) {
isStarting.value = false
}
}) })
watch(errorSignal, () => { watch(errorSignal, () => {
pending.value = false pending.value = false
isStarting.value = false
errorSignal.value = false errorSignal.value = false
}) })
</script> </script>

View File

@@ -27,7 +27,8 @@ export default {
"engineChange": "Cpation Engine Configuration Changed", "engineChange": "Cpation Engine Configuration Changed",
"changeInfo": "If the caption engine is already running, you need to restart it for the changes to take effect.", "changeInfo": "If the caption engine is already running, you need to restart it for the changes to take effect.",
"styleChange": "Caption Style Changed", "styleChange": "Caption Style Changed",
"styleInfo": "Caption style changes have been saved and applied." "styleInfo": "Caption style changes have been saved and applied.",
"engineStartTimeout": "Caption engine startup timeout, automatically force stopped"
}, },
general: { general: {
"title": "General Settings", "title": "General Settings",
@@ -54,8 +55,11 @@ export default {
"showMore": "More Settings", "showMore": "More Settings",
"apikey": "API KEY", "apikey": "API KEY",
"modelPath": "Model Path", "modelPath": "Model Path",
"startTimeout": "Timeout",
"seconds": "seconds",
"apikeyInfo": "API KEY required for the Gummy subtitle engine, which needs to be obtained from the Alibaba Cloud Bailing platform. For more details, see the project user manual.", "apikeyInfo": "API KEY required for the Gummy subtitle engine, which needs to be obtained from the Alibaba Cloud Bailing platform. For more details, see the project user manual.",
"modelPathInfo": "The folder path of the model required by the Vosk subtitle engine. You need to download the required model to your local machine in advance. For more details, see the project user manual.", "modelPathInfo": "The folder path of the model required by the Vosk subtitle engine. You need to download the required model to your local machine in advance. For more details, see the project user manual.",
"startTimeoutInfo": "Caption engine startup timeout duration. Engine will be forcefully stopped if startup exceeds this time. Recommended range: 10-120 seconds.",
"customEngine": "Custom Engine", "customEngine": "Custom Engine",
custom: { custom: {
"title": "Custom Caption Engine", "title": "Custom Caption Engine",
@@ -112,6 +116,11 @@ export default {
"startEngine": "Start Caption Engine", "startEngine": "Start Caption Engine",
"restartEngine": "Restart Caption Engine", "restartEngine": "Restart Caption Engine",
"stopEngine": "Stop Caption Engine", "stopEngine": "Stop Caption Engine",
"forceKill": "Force Stop",
"forceKillStarting": "Starting Engine... (Force Stop)",
"forceKillConfirm": "Are you sure you want to force stop the caption engine? This will terminate the process immediately.",
"confirm": "Confirm",
"cancel": "Cancel",
about: { about: {
"title": "About This Project", "title": "About This Project",
"proj": "Auto Caption Project", "proj": "Auto Caption Project",

View File

@@ -27,7 +27,8 @@ export default {
"engineChange": "字幕エンジンの設定が変更されました", "engineChange": "字幕エンジンの設定が変更されました",
"changeInfo": "字幕エンジンがすでに起動している場合、変更を有効にするには再起動が必要です。", "changeInfo": "字幕エンジンがすでに起動している場合、変更を有効にするには再起動が必要です。",
"styleChange": "字幕のスタイルが変更されました", "styleChange": "字幕のスタイルが変更されました",
"styleInfo": "字幕のスタイル変更が保存され、適用されました" "styleInfo": "字幕のスタイル変更が保存され、適用されました",
"engineStartTimeout": "字幕エンジンの起動がタイムアウトしました。自動的に強制停止しました"
}, },
general: { general: {
"title": "一般設定", "title": "一般設定",
@@ -54,8 +55,11 @@ export default {
"showMore": "詳細設定", "showMore": "詳細設定",
"apikey": "API KEY", "apikey": "API KEY",
"modelPath": "モデルパス", "modelPath": "モデルパス",
"startTimeout": "時間制限",
"seconds": "秒",
"apikeyInfo": "Gummy 字幕エンジンに必要な API KEY は、アリババクラウド百煉プラットフォームから取得する必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。", "apikeyInfo": "Gummy 字幕エンジンに必要な API KEY は、アリババクラウド百煉プラットフォームから取得する必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。",
"modelPathInfo": "Vosk 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。", "modelPathInfo": "Vosk 字幕エンジンに必要なモデルのフォルダパスです。必要なモデルを事前にローカルマシンにダウンロードする必要があります。詳細情報はプロジェクトのユーザーマニュアルをご覧ください。",
"startTimeoutInfo": "字幕エンジンの起動タイムアウト時間です。この時間を超えると自動的に強制停止されます。10-120秒の範囲で設定することを推奨します。",
"customEngine": "カスタムエンジン", "customEngine": "カスタムエンジン",
custom: { custom: {
"title": "カスタムキャプションエンジン", "title": "カスタムキャプションエンジン",
@@ -112,6 +116,11 @@ export default {
"startEngine": "字幕エンジンを開始", "startEngine": "字幕エンジンを開始",
"restartEngine": "字幕エンジンを再起動", "restartEngine": "字幕エンジンを再起動",
"stopEngine": "字幕エンジンを停止", "stopEngine": "字幕エンジンを停止",
"forceKill": "強制停止",
"forceKillStarting": "エンジン起動中... (強制停止)",
"forceKillConfirm": "字幕エンジンを強制停止しますか?プロセスが直ちに終了されます。",
"confirm": "確認",
"cancel": "キャンセル",
about: { about: {
"title": "このプロジェクトについて", "title": "このプロジェクトについて",
"proj": "Auto Caption プロジェクト", "proj": "Auto Caption プロジェクト",

View File

@@ -27,7 +27,8 @@ export default {
"engineChange": "字幕引擎配置已更改", "engineChange": "字幕引擎配置已更改",
"changeInfo": "如果字幕引擎已经启动,需要重启字幕引擎修改才会生效", "changeInfo": "如果字幕引擎已经启动,需要重启字幕引擎修改才会生效",
"styleChange": "字幕样式已修改", "styleChange": "字幕样式已修改",
"styleInfo": "字幕样式修改已经保存并生效" "styleInfo": "字幕样式修改已经保存并生效",
"engineStartTimeout": "字幕引擎启动超时,已自动强制停止"
}, },
general: { general: {
"title": "通用设置", "title": "通用设置",
@@ -54,8 +55,11 @@ export default {
"showMore": "更多设置", "showMore": "更多设置",
"apikey": "API KEY", "apikey": "API KEY",
"modelPath": "模型路径", "modelPath": "模型路径",
"startTimeout": "启动超时",
"seconds": "秒",
"apikeyInfo": "Gummy 字幕引擎需要的 API KEY需要在阿里云百炼平台获取。详细信息见项目用户手册。", "apikeyInfo": "Gummy 字幕引擎需要的 API KEY需要在阿里云百炼平台获取。详细信息见项目用户手册。",
"modelPathInfo": "Vosk 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。", "modelPathInfo": "Vosk 字幕引擎需要的模型的文件夹路径,需要提前下载需要的模型到本地。信息详情见项目用户手册。",
"startTimeoutInfo": "字幕引擎启动超时时间,超过此时间将自动强制停止。建议设置为 10-120 秒之间。",
"customEngine": "自定义引擎", "customEngine": "自定义引擎",
custom: { custom: {
"title": "自定义字幕引擎", "title": "自定义字幕引擎",
@@ -112,6 +116,11 @@ export default {
"startEngine": "启动字幕引擎", "startEngine": "启动字幕引擎",
"restartEngine": "重启字幕引擎", "restartEngine": "重启字幕引擎",
"stopEngine": "关闭字幕引擎", "stopEngine": "关闭字幕引擎",
"forceKill": "强行停止",
"forceKillStarting": "正在启动引擎... (强行停止)",
"forceKillConfirm": "确定要强行停止字幕引擎吗?这将立即终止进程。",
"confirm": "确定",
"cancel": "取消",
about: { about: {
"title": "关于本项目", "title": "关于本项目",
"proj": "Auto Caption 项目", "proj": "Auto Caption 项目",

View File

@@ -27,6 +27,7 @@ export const useEngineControlStore = defineStore('engineControl', () => {
const customized = ref<boolean>(false) const customized = ref<boolean>(false)
const customizedApp = ref<string>('') const customizedApp = ref<string>('')
const customizedCommand = ref<string>('') const customizedCommand = ref<string>('')
const startTimeoutSeconds = ref<number>(30)
const changeSignal = ref<boolean>(false) const changeSignal = ref<boolean>(false)
const errorSignal = ref<boolean>(false) const errorSignal = ref<boolean>(false)
@@ -43,7 +44,8 @@ export const useEngineControlStore = defineStore('engineControl', () => {
modelPath: modelPath.value, modelPath: modelPath.value,
customized: customized.value, customized: customized.value,
customizedApp: customizedApp.value, customizedApp: customizedApp.value,
customizedCommand: customizedCommand.value customizedCommand: customizedCommand.value,
startTimeoutSeconds: startTimeoutSeconds.value
} }
window.electron.ipcRenderer.send('control.controls.change', controls) window.electron.ipcRenderer.send('control.controls.change', controls)
} }
@@ -75,6 +77,7 @@ export const useEngineControlStore = defineStore('engineControl', () => {
customized.value = controls.customized customized.value = controls.customized
customizedApp.value = controls.customizedApp customizedApp.value = controls.customizedApp
customizedCommand.value = controls.customizedCommand customizedCommand.value = controls.customizedCommand
startTimeoutSeconds.value = controls.startTimeoutSeconds
changeSignal.value = true changeSignal.value = true
} }
@@ -137,6 +140,7 @@ export const useEngineControlStore = defineStore('engineControl', () => {
customized, // 是否使用自定义字幕引擎 customized, // 是否使用自定义字幕引擎
customizedApp, // 自定义字幕引擎的应用程序 customizedApp, // 自定义字幕引擎的应用程序
customizedCommand, // 自定义字幕引擎的命令 customizedCommand, // 自定义字幕引擎的命令
startTimeoutSeconds, // 启动超时时间(秒)
setControls, // 设置引擎配置 setControls, // 设置引擎配置
sendControlsChange, // 发送最新控制消息到后端 sendControlsChange, // 发送最新控制消息到后端
emptyModelPathErr, // 模型路径为空时显示警告 emptyModelPathErr, // 模型路径为空时显示警告

View File

@@ -13,7 +13,8 @@ export interface Controls {
modelPath: string, modelPath: string,
customized: boolean, customized: boolean,
customizedApp: string, customizedApp: string,
customizedCommand: string customizedCommand: string,
startTimeoutSeconds: number
} }
export interface Styles { export interface Styles {