完善 SimpleLite 启停控制与服务状态页。

拆分 StopAll/Restart,运维代理透传确认头与用户信息,服务状态页可更稳地重启进程。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-07-26 11:32:30 +08:00
co-authored by Cursor
parent 8299c3dc5c
commit beb6563060
3 changed files with 123 additions and 30 deletions
+4
View File
@@ -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);
+24 -7
View File
@@ -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>