完善 SimpleLite 启停控制与服务状态页。
拆分 StopAll/Restart,运维代理透传确认头与用户信息,服务状态页可更稳地重启进程。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -133,6 +133,10 @@ public class OpsController : ControllerBase
|
||||
// 即使 SimpleLite 默认放行 loopback,也带上 internal token,兼容其严格模式(AllowLoopback=false)。
|
||||
if (!string.IsNullOrEmpty(_internalToken.Token))
|
||||
msg.Headers.TryAddWithoutValidation("X-Platform-Internal-Token", _internalToken.Token);
|
||||
// 运维面板已对 needConfirm 动作做过二次确认;内核 RequiresPlatformConfirm 方法需此头。
|
||||
msg.Headers.TryAddWithoutValidation("X-Platform-Confirmed", "1");
|
||||
if (!string.IsNullOrWhiteSpace(user))
|
||||
msg.Headers.TryAddWithoutValidation("X-Platform-User", user);
|
||||
using var resp = await client.SendAsync(msg);
|
||||
var body = await resp.Content.ReadAsStringAsync();
|
||||
var success = resp.IsSuccessStatusCode && ParseSuccess(body);
|
||||
|
||||
@@ -349,11 +349,10 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
/// <summary>写 active-scenes.json 的结果(供向导保存接口回显)。</summary>
|
||||
public readonly record struct ActiveScenesWriteResult(bool Ok, string? Path, string? Error);
|
||||
|
||||
/// <summary>
|
||||
/// 关闭所有 SimpleLite 进程、同步 obj→bin 最新 DLL,再按 launchMode 重新拉起。
|
||||
/// </summary>
|
||||
public LaunchResult RestartForUpdate(string launchMode = "webonly")
|
||||
/// <summary>终止本机全部 SimpleLite 进程并清理 MiGu.Server 侧托管引用。</summary>
|
||||
public int StopAll()
|
||||
{
|
||||
var killed = 0;
|
||||
foreach (var proc in Process.GetProcessesByName("SimpleLite"))
|
||||
{
|
||||
try
|
||||
@@ -361,12 +360,13 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
if (!proc.HasExited)
|
||||
{
|
||||
proc.Kill(entireProcessTree: true);
|
||||
_log.LogInformation("[SimpleLite] restart-for-update: killed pid={Pid}", proc.Id);
|
||||
killed++;
|
||||
_log.LogInformation("[SimpleLite] stop: killed pid={Pid}", proc.Id);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.LogWarning("[SimpleLite] restart-for-update: kill pid={Pid} failed: {Msg}", proc.Id, ex.Message);
|
||||
_log.LogWarning("[SimpleLite] stop: kill pid={Pid} failed: {Msg}", proc.Id, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -374,7 +374,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
Thread.Sleep(1500);
|
||||
if (killed > 0) Thread.Sleep(1500);
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
@@ -382,6 +382,23 @@ public sealed class SimpleLiteLauncher : IDisposable
|
||||
_lastLaunchMode = null;
|
||||
}
|
||||
|
||||
return killed;
|
||||
}
|
||||
|
||||
/// <summary>关闭全部 SimpleLite 后按 launchMode 重新拉起(不同步 DLL)。</summary>
|
||||
public LaunchResult Restart(string launchMode = "webonly")
|
||||
{
|
||||
StopAll();
|
||||
return MaybeStart(launchMode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关闭所有 SimpleLite 进程、同步 obj→bin 最新 DLL,再按 launchMode 重新拉起。
|
||||
/// </summary>
|
||||
public LaunchResult RestartForUpdate(string launchMode = "webonly")
|
||||
{
|
||||
StopAll();
|
||||
|
||||
var synced = SimpleLiteBuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
|
||||
var result = MaybeStart(launchMode);
|
||||
if (!synced && result.Warning == null)
|
||||
|
||||
@@ -43,6 +43,28 @@
|
||||
|
||||
<el-alert v-if="error" :title="error" type="warning" :closable="false" class="status-block" />
|
||||
|
||||
<div v-if="canControlSimpleLite" class="status-block sl-control">
|
||||
<div class="sl-control-title">SimpleLite 操作</div>
|
||||
<div class="sl-control-actions">
|
||||
<el-button
|
||||
type="warning"
|
||||
:loading="actionLoading === 'restart'"
|
||||
:disabled="!!actionLoading"
|
||||
@click="onRestart">
|
||||
重启 SimpleLite
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:loading="actionLoading === 'stop'"
|
||||
:disabled="!!actionLoading"
|
||||
@click="onStop">
|
||||
关闭 SimpleLite
|
||||
</el-button>
|
||||
</div>
|
||||
<p class="sl-control-hint">将终止本机全部 SimpleLite 进程;重启后按当前启动模式重新拉起。</p>
|
||||
</div>
|
||||
|
||||
<div class="status-actions">
|
||||
<el-button :loading="loading" @click="refresh">刷新</el-button>
|
||||
<el-button @click="back">返回</el-button>
|
||||
@@ -54,34 +76,29 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import http from '@/api/http'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
getHealth,
|
||||
getSimpleLiteDiagnostics,
|
||||
restartSimpleLite,
|
||||
resolveRestartLaunchMode,
|
||||
stopSimpleLite,
|
||||
type HealthInfo,
|
||||
type SimpleLiteDiagnostics
|
||||
} from '@/api/health'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
interface HealthInfo {
|
||||
status: string
|
||||
startTime: string
|
||||
uptimeSec: number
|
||||
}
|
||||
|
||||
interface SimpleLiteDiagnostics {
|
||||
enabled: boolean
|
||||
isRunning: boolean
|
||||
lastLaunchMode?: string | null
|
||||
projectionPort: number
|
||||
projectionPortReachable: boolean
|
||||
gotoSiteApiAvailable?: boolean | null
|
||||
executableExists: boolean
|
||||
deployHint?: string | null
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const actionLoading = ref<'stop' | 'restart' | null>(null)
|
||||
const health = ref<HealthInfo | null>(null)
|
||||
const sl = ref<SimpleLiteDiagnostics | null>(null)
|
||||
const error = ref('')
|
||||
|
||||
const canControlSimpleLite = computed(() => auth.token && auth.scope === 'Platform')
|
||||
|
||||
let timer: number | undefined
|
||||
|
||||
const serverStartTime = computed(() =>
|
||||
@@ -117,11 +134,10 @@ async function refresh() {
|
||||
try {
|
||||
// /status 是 public 页:未登录只拉匿名 /health,不调需登录的 simplelite 诊断
|
||||
// (401 会触发全局拦截器强制跳转登录页)。
|
||||
const requests: [Promise<{ data: HealthInfo }>, Promise<{ data: SimpleLiteDiagnostics }> | null] = [
|
||||
http.get<HealthInfo>('/health'),
|
||||
auth.token ? http.get<SimpleLiteDiagnostics>('/health/simplelite') : null
|
||||
]
|
||||
const [h, d] = await Promise.allSettled([requests[0], requests[1] ?? Promise.reject(new Error('skipped'))])
|
||||
const [h, d] = await Promise.allSettled([
|
||||
getHealth(),
|
||||
auth.token ? getSimpleLiteDiagnostics() : Promise.reject(new Error('skipped'))
|
||||
])
|
||||
health.value = h.status === 'fulfilled' ? h.value.data : null
|
||||
sl.value = d.status === 'fulfilled' ? d.value.data : null
|
||||
if (h.status === 'rejected') error.value = '无法连接 MiGu.Server(/api/health)'
|
||||
@@ -131,6 +147,59 @@ async function refresh() {
|
||||
}
|
||||
}
|
||||
|
||||
async function onStop() {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'将终止本机全部 SimpleLite 进程,地图监控与 /api/sl/* 功能将不可用,直到重新登录或手动重启。',
|
||||
'关闭 SimpleLite',
|
||||
{ type: 'warning', confirmButtonText: '关闭', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
actionLoading.value = 'stop'
|
||||
try {
|
||||
const { data } = await stopSimpleLite()
|
||||
sl.value = data.diagnostics
|
||||
ElMessage.success(data.killed > 0 ? `已关闭 ${data.killed} 个 SimpleLite 进程` : '当前没有运行中的 SimpleLite 进程')
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '关闭 SimpleLite 失败')
|
||||
} finally {
|
||||
actionLoading.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function onRestart() {
|
||||
const launchMode = resolveRestartLaunchMode(sl.value?.lastLaunchMode, auth.runMode)
|
||||
const modeLabel = launchMode === 'WebOnly' ? '仅 Web' : '本地 + Web'
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`将关闭本机全部 SimpleLite 并按「${modeLabel}」模式重新拉起,期间 /api/sl/* 可能短暂不可用。`,
|
||||
'重启 SimpleLite',
|
||||
{ type: 'warning', confirmButtonText: '重启', cancelButtonText: '取消' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
actionLoading.value = 'restart'
|
||||
try {
|
||||
const { data } = await restartSimpleLite(launchMode)
|
||||
sl.value = data.diagnostics
|
||||
const r = data.restart
|
||||
if (r.warning) ElMessage.warning(r.warning)
|
||||
else if (r.started) ElMessage.success('SimpleLite 已重新拉起')
|
||||
else ElMessage.error(`重启未完成:${r.status}`)
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '重启 SimpleLite 失败')
|
||||
} finally {
|
||||
actionLoading.value = null
|
||||
void refresh()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refresh()
|
||||
// 10s 轮询:服务端对 goto-site 探测有 60s 缓存,此频率不会对 SimpleLite 产生压力。
|
||||
@@ -148,5 +217,8 @@ function back() { router.back() }
|
||||
.status-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
||||
.status-card { width: 720px; }
|
||||
.status-block { margin-top: 16px; }
|
||||
.sl-control-title { font-weight: 600; margin-bottom: 8px; }
|
||||
.sl-control-actions { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.sl-control-hint { margin: 8px 0 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||
.status-actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user