From 0788fad74a7adc704a34e25691b75fa068e7e3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E5=85=86=E5=B0=89?= <228127304@qq.com> Date: Thu, 27 Aug 2026 17:30:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=B0=E5=9B=BE=E7=9B=91=E6=8E=A7=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=9B=BA=E5=AE=9A=E5=8F=8C=E6=A0=8F=E5=B9=B6=E5=8F=AF?= =?UTF-8?q?=E6=94=B6=E8=B5=B7=E4=BE=A7=E6=A0=8F=EF=BC=8C=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E6=94=B9=E8=B5=B0=E5=86=85=E6=A0=B8=20CAD?= =?UTF-8?q?=EF=BC=8C=E5=B9=B6=E6=8E=A5=E9=80=9A=20428=20=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E7=A5=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .gitignore | 1 + MiGu.Server/Controllers/OpsController.cs | 42 +- .../simple-platform-vue/src/api/reflection.ts | 94 ++- .../src/views/admin/MapEditorView.vue | 90 ++- .../src/views/admin/MapMonitorView.vue | 573 ++++++------------ 5 files changed, 372 insertions(+), 428 deletions(-) diff --git a/.gitignore b/.gitignore index 49947dd..15751a7 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ Desktop.ini .codex-temp/ frontends/apps/simple-platform-vue/imgui.ini /.cursor/plans +/tmp-*.json diff --git a/MiGu.Server/Controllers/OpsController.cs b/MiGu.Server/Controllers/OpsController.cs index a8fdfa6..9ce6b3d 100644 --- a/MiGu.Server/Controllers/OpsController.cs +++ b/MiGu.Server/Controllers/OpsController.cs @@ -385,6 +385,13 @@ public class OpsController : ControllerBase try { var call = await CallLiteAsync(HttpMethod.Post, path, actor); + if (TryReadConfirmTicket(call.Body, out var token)) + { + call = await CallLiteAsync(HttpMethod.Post, path, actor, new Dictionary + { + [ConfirmTokenHeader] = token + }); + } var success = call.Ok && ParseSuccess(call.Body); return success ? new ForwardOutcome(true, "ok", null) @@ -397,7 +404,32 @@ public class OpsController : ControllerBase } } - private async Task<(bool Ok, int Status, string Body)> CallLiteAsync(HttpMethod method, string path, string? actor) + private const string ConfirmTokenHeader = "X-Platform-Confirm-Token"; + + private static bool TryReadConfirmTicket(string body, out string token) + { + token = ""; + try + { + using var doc = JsonDocument.Parse(body); + var root = doc.RootElement; + var code = root.TryGetProperty("code", out var c) && c.TryGetInt32(out var n) ? n : 0; + if (code != 428) return false; + if (!root.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object) + return false; + token = data.TryGetProperty("confirmToken", out var t) && t.ValueKind == JsonValueKind.String + ? t.GetString() ?? "" + : ""; + return !string.IsNullOrWhiteSpace(token); + } + catch + { + return false; + } + } + + private async Task<(bool Ok, int Status, string Body)> CallLiteAsync( + HttpMethod method, string path, string? actor, IReadOnlyDictionary? extraHeaders = null) { var url = $"http://127.0.0.1:{_sl.ProjectionPort}{path}"; using var client = _httpFactory.CreateClient(); @@ -409,6 +441,14 @@ public class OpsController : ControllerBase msg.Headers.TryAddWithoutValidation("X-Platform-Confirmed", "1"); if (!string.IsNullOrWhiteSpace(actor)) msg.Headers.TryAddWithoutValidation("X-Platform-User", actor); + if (extraHeaders != null) + { + foreach (var kv in extraHeaders) + { + if (string.IsNullOrWhiteSpace(kv.Key) || kv.Value is null) continue; + msg.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + } using var resp = await client.SendAsync(msg); var body = await resp.Content.ReadAsStringAsync(); return (resp.IsSuccessStatusCode, (int)resp.StatusCode, body); diff --git a/frontends/apps/simple-platform-vue/src/api/reflection.ts b/frontends/apps/simple-platform-vue/src/api/reflection.ts index c1ae668..7c05df8 100644 --- a/frontends/apps/simple-platform-vue/src/api/reflection.ts +++ b/frontends/apps/simple-platform-vue/src/api/reflection.ts @@ -260,10 +260,16 @@ export function formatReflectionExecuteMessage( } export interface ReflectionExecuteOptions { - /** 已在平台侧完成二次确认时带上,对应后端 X-Platform-Confirmed: 1 */ + /** 已在平台侧完成二次确认时带上;新内核仍需再带 428 下发的 confirmToken。 */ platformConfirmed?: boolean } +interface PlatformConfirmChallenge { + message: string + token: string + header: string +} + async function get(path: string): Promise { const { data } = await http.get>(`${BASE}${path}`) if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data) @@ -275,9 +281,69 @@ async function post( params?: Record, headers?: Record ): Promise { - const { data } = await http.post>(`${BASE}${path}`, null, { params, headers }) - if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data) - return data.data as T + try { + const { data } = await http.post>(`${BASE}${path}`, null, { params, headers }) + if (!data?.success) throw new ReflectionApiError(data?.message ?? `reflection ${path} failed`, data?.code ?? 500, data?.data) + return data.data as T + } catch (e) { + if (e instanceof ReflectionApiError) throw e + throw wrapEnvelopeError(e, `reflection ${path} failed`) + } +} + +function wrapEnvelopeError(e: unknown, fallback: string): unknown { + const resp = e && typeof e === 'object' && 'response' in e + ? (e as { response?: { status?: number; data?: ReflectionEnvelope } }).response + : undefined + const data = resp?.data + if (data && typeof data === 'object' && (data.success === false || typeof data.code === 'number')) { + return new ReflectionApiError( + data.message || fallback, + data.code ?? resp?.status ?? 500, + data.data + ) + } + return e +} + +const CONFIRM_TOKEN_HEADER = 'X-Platform-Confirm-Token' + +function parseConfirmChallenge(e: unknown): PlatformConfirmChallenge | null { + const payload = unwrapConfirmPayload(e) + const token = payload?.confirmToken?.trim() + if (!token) return null + const rawHeader = payload?.confirmTokenHeader?.trim() + return { + message: payload?.confirmMessage?.trim() || '此操作需要确认', + token, + header: rawHeader && rawHeader.toLowerCase() === CONFIRM_TOKEN_HEADER.toLowerCase() + ? rawHeader + : CONFIRM_TOKEN_HEADER + } +} + +function unwrapConfirmPayload(e: unknown): { + confirmMessage?: string | null + confirmToken?: string | null + confirmTokenHeader?: string | null +} | null { + if (e instanceof ReflectionApiError) { + if (e.code !== 428) return null + return (e.data as { + confirmMessage?: string | null + confirmToken?: string | null + confirmTokenHeader?: string | null + } | null) ?? null + } + const resp = e && typeof e === 'object' && 'response' in e + ? (e as { response?: { status?: number; data?: ReflectionEnvelope> } }).response + : undefined + const data = resp?.data + const code = typeof data?.code === 'number' ? data.code : resp?.status + if (code !== 428) return null + const inner = data?.data + if (inner && typeof inner === 'object') return inner + return null } async function del(path: string): Promise { @@ -297,22 +363,24 @@ async function executeWithPlatformConfirm( params?: Record, opts?: ReflectionExecuteOptions ): Promise { - const headers = opts?.platformConfirmed ? { 'X-Platform-Confirmed': '1' } : undefined + const headers: Record = {} + if (opts?.platformConfirmed) headers['X-Platform-Confirmed'] = '1' try { - return await post(path, params, headers) + return await post(path, params, Object.keys(headers).length ? headers : undefined) } catch (e) { - if (e instanceof ReflectionApiError && e.code === 428 && !opts?.platformConfirmed) { - const confirmMessage = - (e.data as { confirmMessage?: string | null } | null)?.confirmMessage?.trim() - || '此操作需要确认' - await ElMessageBox.confirm(confirmMessage, '确认', { + const challenge = parseConfirmChallenge(e) + if (!challenge) throw e + if (!opts?.platformConfirmed) { + await ElMessageBox.confirm(challenge.message, '确认', { type: 'warning', confirmButtonText: '确定', cancelButtonText: '取消' }) - return await post(path, params, { 'X-Platform-Confirmed': '1' }) } - throw e + return await post(path, params, { + 'X-Platform-Confirmed': '1', + [challenge.header]: challenge.token + }) } } diff --git a/frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue b/frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue index 381a1cf..c14c67e 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/MapEditorView.vue @@ -1350,38 +1350,74 @@ function pickCreateKind(originalKind: string, typeName: string): string { return originalKind } +const CAD_ALIGN_MODES = new Set([ + 'left', 'right', 'top', 'bottom', 'centerH', 'centerV', 'center', 'distributeH', 'distributeV' +]) + async function applyAlignment(mode: AlignMode) { - // 选中对象必须是有 x,y 坐标的(site / special? - const targets: AlignTarget[] = [] - for (const it of selection.items.value) { - if (it.kind === 'track') continue - // 拉一?bundle ?x/y - try { - const b = await reflectionApi.getBundle(it.kind, it.id) - const x = Number(b.fields?.x ?? 0) - const y = Number(b.fields?.y ?? 0) - targets.push({ kind: it.kind, id: it.id, x, y }) - } catch (err) { - const msg = err instanceof Error ? err.message : String(err) - throw new Error(`读取对象坐标失败(${it.kind}#${it.id}):${msg}`) - } - } - if (targets.length < 2) { - ElMessage.warning('对齐需要选中至少 2 个对象') + const refs = selection.items.value.filter((it) => it.kind !== 'track' && it.kind !== 'car') + if (refs.length < 2) { + ElMessage.warning('对齐需要选中至少 2 个站点或装饰(文本/模型)') return } - const ops = buildAlignOps(targets, mode) - if (ops.length === 0) { ElMessage.info('对齐无变化'); return } - await history.run({ - label: `对齐 (${mode})`, - apply: async () => { await mapEditApi.batch(ops) }, - revert: async () => { - // 反向恢复每个对象的原坐标 - const revertOps = targets.map((t) => ({ action: 'patch' as const, kind: t.kind, id: t.id, data: { x: t.x, y: t.y } })) - await mapEditApi.batch(revertOps) + try { + if (CAD_ALIGN_MODES.has(mode)) { + let previous: Array<{ kind: string; id: number; x: number; y: number }> = [] + let alignedCount = 0 + await history.run({ + label: `对齐 (${mode})`, + apply: async () => { + const r = await mapEditApi.cadAlign(mode, refs.map((it) => ({ kind: it.kind, id: it.id }))) + previous = r.previous ?? [] + alignedCount = r.count ?? 0 + if (!alignedCount) throw new Error('没有可对齐的站点或装饰') + }, + revert: async () => { + if (!previous.length) return + await mapEditApi.batch(previous.map((t) => ({ + action: 'patch' as const, + kind: t.kind, + id: t.id, + data: { x: t.x, y: t.y } + }))) + } + }) + ElMessage.success(`已对齐 ${alignedCount} 个对象`) + return } - }) + + const targets: AlignTarget[] = [] + for (const it of refs) { + const b = await reflectionApi.getBundle(it.kind, it.id) + targets.push({ + kind: it.kind, + id: it.id, + x: Number(b.fields?.x ?? 0), + y: Number(b.fields?.y ?? 0) + }) + } + const ops = buildAlignOps(targets, mode) + if (ops.length === 0) { + ElMessage.info('对齐无变化') + return + } + await history.run({ + label: `对齐 (${mode})`, + apply: async () => { await mapEditApi.batch(ops) }, + revert: async () => { + await mapEditApi.batch(targets.map((t) => ({ + action: 'patch' as const, + kind: t.kind, + id: t.id, + data: { x: t.x, y: t.y } + }))) + } + }) + ElMessage.success(`已对齐 ${targets.length} 个对象`) + } catch (err) { + ElMessage.error(`对齐失败:${(err as Error).message}`) + } } async function runBatchGenerate(id: EditToolId) { diff --git a/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue b/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue index 8aa7c50..e0f8b30 100644 --- a/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue +++ b/frontends/apps/simple-platform-vue/src/views/admin/MapMonitorView.vue @@ -1,5 +1,5 @@