完善 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)。
|
// 即使 SimpleLite 默认放行 loopback,也带上 internal token,兼容其严格模式(AllowLoopback=false)。
|
||||||
if (!string.IsNullOrEmpty(_internalToken.Token))
|
if (!string.IsNullOrEmpty(_internalToken.Token))
|
||||||
msg.Headers.TryAddWithoutValidation("X-Platform-Internal-Token", _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);
|
using var resp = await client.SendAsync(msg);
|
||||||
var body = await resp.Content.ReadAsStringAsync();
|
var body = await resp.Content.ReadAsStringAsync();
|
||||||
var success = resp.IsSuccessStatusCode && ParseSuccess(body);
|
var success = resp.IsSuccessStatusCode && ParseSuccess(body);
|
||||||
|
|||||||
@@ -349,11 +349,10 @@ public sealed class SimpleLiteLauncher : IDisposable
|
|||||||
/// <summary>写 active-scenes.json 的结果(供向导保存接口回显)。</summary>
|
/// <summary>写 active-scenes.json 的结果(供向导保存接口回显)。</summary>
|
||||||
public readonly record struct ActiveScenesWriteResult(bool Ok, string? Path, string? Error);
|
public readonly record struct ActiveScenesWriteResult(bool Ok, string? Path, string? Error);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>终止本机全部 SimpleLite 进程并清理 MiGu.Server 侧托管引用。</summary>
|
||||||
/// 关闭所有 SimpleLite 进程、同步 obj→bin 最新 DLL,再按 launchMode 重新拉起。
|
public int StopAll()
|
||||||
/// </summary>
|
|
||||||
public LaunchResult RestartForUpdate(string launchMode = "webonly")
|
|
||||||
{
|
{
|
||||||
|
var killed = 0;
|
||||||
foreach (var proc in Process.GetProcessesByName("SimpleLite"))
|
foreach (var proc in Process.GetProcessesByName("SimpleLite"))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -361,12 +360,13 @@ public sealed class SimpleLiteLauncher : IDisposable
|
|||||||
if (!proc.HasExited)
|
if (!proc.HasExited)
|
||||||
{
|
{
|
||||||
proc.Kill(entireProcessTree: true);
|
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)
|
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
|
finally
|
||||||
{
|
{
|
||||||
@@ -374,7 +374,7 @@ public sealed class SimpleLiteLauncher : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.Sleep(1500);
|
if (killed > 0) Thread.Sleep(1500);
|
||||||
|
|
||||||
lock (_sync)
|
lock (_sync)
|
||||||
{
|
{
|
||||||
@@ -382,6 +382,23 @@ public sealed class SimpleLiteLauncher : IDisposable
|
|||||||
_lastLaunchMode = null;
|
_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 synced = SimpleLiteBuildSync.TrySyncFromObjToBin(_env.ContentRootPath, _log);
|
||||||
var result = MaybeStart(launchMode);
|
var result = MaybeStart(launchMode);
|
||||||
if (!synced && result.Warning == null)
|
if (!synced && result.Warning == null)
|
||||||
|
|||||||
@@ -43,6 +43,28 @@
|
|||||||
|
|
||||||
<el-alert v-if="error" :title="error" type="warning" :closable="false" class="status-block" />
|
<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">
|
<div class="status-actions">
|
||||||
<el-button :loading="loading" @click="refresh">刷新</el-button>
|
<el-button :loading="loading" @click="refresh">刷新</el-button>
|
||||||
<el-button @click="back">返回</el-button>
|
<el-button @click="back">返回</el-button>
|
||||||
@@ -54,34 +76,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
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'
|
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 router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const actionLoading = ref<'stop' | 'restart' | null>(null)
|
||||||
const health = ref<HealthInfo | null>(null)
|
const health = ref<HealthInfo | null>(null)
|
||||||
const sl = ref<SimpleLiteDiagnostics | null>(null)
|
const sl = ref<SimpleLiteDiagnostics | null>(null)
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
|
|
||||||
|
const canControlSimpleLite = computed(() => auth.token && auth.scope === 'Platform')
|
||||||
|
|
||||||
let timer: number | undefined
|
let timer: number | undefined
|
||||||
|
|
||||||
const serverStartTime = computed(() =>
|
const serverStartTime = computed(() =>
|
||||||
@@ -117,11 +134,10 @@ async function refresh() {
|
|||||||
try {
|
try {
|
||||||
// /status 是 public 页:未登录只拉匿名 /health,不调需登录的 simplelite 诊断
|
// /status 是 public 页:未登录只拉匿名 /health,不调需登录的 simplelite 诊断
|
||||||
// (401 会触发全局拦截器强制跳转登录页)。
|
// (401 会触发全局拦截器强制跳转登录页)。
|
||||||
const requests: [Promise<{ data: HealthInfo }>, Promise<{ data: SimpleLiteDiagnostics }> | null] = [
|
const [h, d] = await Promise.allSettled([
|
||||||
http.get<HealthInfo>('/health'),
|
getHealth(),
|
||||||
auth.token ? http.get<SimpleLiteDiagnostics>('/health/simplelite') : null
|
auth.token ? getSimpleLiteDiagnostics() : Promise.reject(new Error('skipped'))
|
||||||
]
|
])
|
||||||
const [h, d] = await Promise.allSettled([requests[0], requests[1] ?? Promise.reject(new Error('skipped'))])
|
|
||||||
health.value = h.status === 'fulfilled' ? h.value.data : null
|
health.value = h.status === 'fulfilled' ? h.value.data : null
|
||||||
sl.value = d.status === 'fulfilled' ? d.value.data : null
|
sl.value = d.status === 'fulfilled' ? d.value.data : null
|
||||||
if (h.status === 'rejected') error.value = '无法连接 MiGu.Server(/api/health)'
|
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(() => {
|
onMounted(() => {
|
||||||
void refresh()
|
void refresh()
|
||||||
// 10s 轮询:服务端对 goto-site 探测有 60s 缓存,此频率不会对 SimpleLite 产生压力。
|
// 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-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
|
||||||
.status-card { width: 720px; }
|
.status-card { width: 720px; }
|
||||||
.status-block { margin-top: 16px; }
|
.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; }
|
.status-actions { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user