feat(server): 添加浏览器内 OpenAPI 调试页及诊断入口

* 新增 DebugConsoleController,提供 /debug 纯内嵌调试页
  - 零外部依赖,基于 Swagger JSON 自动生成各端点表单
  - 与 Swagger:Enabled 同步开关,避免生产环境误暴露
* 启用 <GenerateDocumentationFile>,将 XML 注释注入 OpenAPI
  - 调试页与 Swagger UI 共用同一份端点标题和说明
* 为 Health/Status/LegacyHttpApi 控制器添加 Tags 分组
* 补充 VS Code launch.json 与 tasks.json,支持现场调试
* 新增 DebugConsoleEndpointTests 覆盖调试页基础响应
* 同步更新 README 进度与待办清单
This commit is contained in:
2026-04-27 10:33:53 +08:00
parent 69fa3edd89
commit 0292e077ff
12 changed files with 1650 additions and 12 deletions

View File

@@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"Bash(git commit -m ':*)",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln -v minimal 2>&1')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj -v minimal 2>&1')",
"Bash(/bin/bash -lc 'DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test FlyshotReplacement.sln --no-build -v minimal 2>&1')",
"Bash(DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet build FlyshotReplacement.sln --no-restore -v minimal)",
"Bash(DOTNET_CLI_HOME=/tmp NUGET_PACKAGES=/tmp/nuget-packages dotnet test tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj --no-build -v minimal)",
"Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json'\\)\\); json.load\\(open\\('.vscode/tasks.json'\\)\\); print\\('JSON valid.'\\)\")",
"Bash(python -c \"import json; json.load\\(open\\('.vscode/launch.json', encoding='utf-8'\\)\\); json.load\\(open\\('.vscode/tasks.json', encoding='utf-8'\\)\\); print\\('JSON valid.'\\)\")"
]
}
}

67
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,67 @@
{
// VS Code 启动与调试配置
// 依赖 C# 扩展OmniSharp 或 C# Dev Kit提供 coreclr 调试器。
// 文档https://code.visualstudio.com/docs/csharp/debugger-settings
"version": "0.2.0",
"configurations": [
{
// 标准调试启动:编译并启动 Host命中断点浏览器自动打开首页
"name": ".NET Core Launch (Host)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"run",
"--project",
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
"--no-launch-profile"
],
"cwd": "${workspaceFolder}",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:5190"
},
"stopAtEntry": false,
"console": "internalConsole",
"preLaunchTask": "build",
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)",
"uriFormat": "%s"
}
},
{
// 热重载调试启动:自动编译、自动重启、断点保留;迭代 Web / 控制器层时首选
"name": ".NET Core Watch (Host)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
"--no-launch-profile"
],
"cwd": "${workspaceFolder}",
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_URLS": "http://localhost:5190"
},
"stopAtEntry": false,
"console": "integratedTerminal",
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)",
"uriFormat": "%s"
}
},
{
// 附加到正在运行的 dotnet 进程(如已手动 `dotnet run` 或 Windows Service 模式)
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

160
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,160 @@
{
// VS Code 任务配置
// 文档https://code.visualstudio.com/docs/editor/tasks
"version": "2.0.0",
"tasks": [
{
// 构建整个解决方案,是 launch.json 启动前的默认 preLaunchTask
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/FlyshotReplacement.sln",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-v",
"minimal"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$msCompile",
"presentation": {
"reveal": "silent",
"clear": true
}
},
{
// 仅构建宿主项目,迭代 Web 层时比整解决方案快
"label": "build-host",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary",
"-v",
"minimal"
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
// 还原 NuGet 包,新增引用或克隆后第一次打开时使用
"label": "restore",
"command": "dotnet",
"type": "process",
"args": [
"restore",
"${workspaceFolder}/FlyshotReplacement.sln"
],
"problemMatcher": []
},
{
// 清理所有项目的 bin/obj
"label": "clean",
"command": "dotnet",
"type": "process",
"args": [
"clean",
"${workspaceFolder}/FlyshotReplacement.sln"
],
"problemMatcher": "$msCompile"
},
{
// 跑全部测试(领域 + 集成)
"label": "test",
"command": "dotnet",
"type": "process",
"args": [
"test",
"${workspaceFolder}/FlyshotReplacement.sln",
"--no-restore",
"-v",
"minimal"
],
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": "$msCompile"
},
{
// 仅跑领域 / 算法层测试,迭代规划逻辑时使用
"label": "test-core",
"command": "dotnet",
"type": "process",
"args": [
"test",
"${workspaceFolder}/tests/Flyshot.Core.Tests/Flyshot.Core.Tests.csproj",
"-v",
"minimal"
],
"group": "test",
"problemMatcher": "$msCompile"
},
{
// 仅跑宿主集成测试,迭代 HTTP / 控制器层时使用
"label": "test-integration",
"command": "dotnet",
"type": "process",
"args": [
"test",
"${workspaceFolder}/tests/Flyshot.Server.IntegrationTests/Flyshot.Server.IntegrationTests.csproj",
"-v",
"minimal"
],
"group": "test",
"problemMatcher": "$msCompile"
},
{
// 启动宿主,供 launch.json 的 watch 配置作为前置任务
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
"--launch-profile",
"http"
],
"isBackground": true,
"problemMatcher": {
"owner": "dotnet-watch",
"pattern": [
{
"regexp": "^.*$",
"file": 1,
"location": 2,
"message": 3
}
],
"background": {
"activeOnStart": true,
"beginsPattern": "^.*Watch run started.*$",
"endsPattern": "^.*Application started.*$"
}
}
},
{
// Release 配置发布到 publish/,用于现场部署包打包
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/src/Flyshot.Server.Host/Flyshot.Server.Host.csproj",
"-c",
"Release",
"-o",
"${workspaceFolder}/publish"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -37,6 +37,7 @@
- [x] 完成 ICSP 轨迹导出结果与旧系统对齐
- [x]`ExecuteTrajectory` / `ExecuteFlyShotTraj` 接入 FANUC 运行时链路
- [x] 落地 Web 状态页
- [x] 落地浏览器内 OpenAPI 自动驱动的接口调试页(`/debug`),与 `Swagger:Enabled` 同步可见
- [x] 固化 `10010 / 10012 / 60015` FANUC 基础协议帧编解码,确认 `10010` 状态帧为 90B
- [x] 使用本地 TCP/UDP 模拟器覆盖命令通道、状态通道和 J519 基础收发
- [x] 补齐 `Get/SetSpeedRatio``Get/SetTCP``Get/SetIO` 真机命令体与响应解析
@@ -47,13 +48,14 @@
1. 配置与测试基线
- [x] 修正 `ConfigCompatibilityTests` 当前样本路径漂移:`Rvbust/EOL10_EAU_0/RobotConfig.json` 不再包含 `001`,应改用稳定样本或更新断言。
- [x]`RobotConfig.json` 中的 `use_do``io_keep_cycles``acc_limit``jerk_limit``adapt_icsp_try_num` 全部贯通到规划和执行链路。
- [ ] 为新 HTTP API 补一份当前现场调用顺序文档,替代旧 `ControllerClient` 工作流。
- [ ] 为新 HTTP API 补一份当前现场调用顺序文档,替代旧 `ControllerClient` 工作流`/debug` 页已提供交互式覆盖,仍需补静态文档说明现场调用顺序)
2. 轨迹规划
- [x] 补齐 ICSP 最终 `global_scale > 1.0` 失败判定,避免未收敛轨迹被当作有效结果执行。
- [x] 将 self-adapt-icsp 的补点次数改为使用配置中的 `adapt_icsp_try_num`
- [ ] 如果现场仍需要 `method="doubles"`,实现 `TrajectoryDoubleS` 等价规划;否则在 HTTP 文档中明确标为不支持。
- [ ] 把已完成对齐的旧系统轨迹样本固化为 golden tests防止后续重构破坏轨迹一致性。
- [ ] 补齐 `save_traj` / `SaveTrajInfo` 的规划结果导出,将稠密关节轨迹、笛卡尔轨迹和 ShotEvents 写入可诊断 artifacts。
3. FANUC TCP 10012 命令通道
- [x] 补齐 `GetSpeedRatio` / `SetSpeedRatio` 真机命令体与响应解析。

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ namespace Flyshot.Server.Host.Controllers;
/// 提供宿主基础探活与诊断接口。
/// </summary>
[ApiController]
[Tags("基础与状态")]
public sealed class HealthController : ControllerBase
{
/// <summary>

View File

@@ -8,6 +8,7 @@ namespace Flyshot.Server.Host.Controllers;
/// 提供对 `flyshot-uaes-interface` 既有 FastAPI HTTP 路由层的一比一 MVC 兼容控制器。
/// </summary>
[ApiController]
[Tags("ControllerClient 兼容")]
public sealed class LegacyHttpApiController : ControllerBase
{
private readonly IControllerClientCompatService _compatService;
@@ -431,6 +432,8 @@ public sealed class LegacyHttpApiController : ControllerBase
/// 兼容旧 `/execute_trajectory/` 路由,并接受两种历史请求体形状。
/// </summary>
/// <param name="waypoints">轨迹请求体。</param>
/// <param name="method">查询字符串中的 method 覆盖值(兼容历史调用方式)。</param>
/// <param name="save_traj">查询字符串中的 save_traj 覆盖值(兼容历史调用方式)。</param>
/// <returns>旧 FastAPI 层风格的状态响应。</returns>
[HttpPost("/execute_trajectory/")]
public IActionResult ExecuteTrajectory(

View File

@@ -7,6 +7,7 @@ namespace Flyshot.Server.Host.Controllers;
/// 提供只读状态监控页面和控制器状态快照 API。
/// </summary>
[ApiController]
[Tags("基础与状态")]
public sealed class StatusController : ControllerBase
{
/// <summary>
@@ -84,6 +85,30 @@ public sealed class StatusController : ControllerBase
cursor: default;
}
/* 顶部操作区按钮和外链按钮共用同一组视觉样式,便于现场顺手跳转。 */
.actions {
display: flex;
align-items: center;
gap: 10px;
}
.link-button {
display: inline-flex;
align-items: center;
min-height: 36px;
padding: 0 14px;
border: 1px solid var(--accent);
border-radius: 6px;
background: transparent;
color: var(--accent);
font: inherit;
text-decoration: none;
}
.link-button:hover {
background: rgba(0, 124, 137, 0.08);
}
main {
width: min(1180px, calc(100% - 32px));
margin: 22px auto;
@@ -216,8 +241,11 @@ public sealed class StatusController : ControllerBase
<header>
<div class="topbar">
<h1>Flyshot Replacement </h1>
<div class="actions">
<a class="link-button" href="/debug" target="_blank" rel="noopener"></a>
<button id="refresh" type="button"></button>
</div>
</div>
</header>
<main>
<div class="summary">

View File

@@ -1,4 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<!-- 生成 XML 文档以便 Swashbuckle 把控制器/DTO 上的 /// summary 注释注入 OpenAPI 文档,
调试页和 Swagger UI 的端点标题都依赖这一份文档。1591 抑制掉 “缺失 XML 注释” 的噪音。 -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

View File

@@ -2,6 +2,7 @@ using Flyshot.ControllerClientCompat;
using Flyshot.Server.Host;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
var builder = WebApplication.CreateBuilder(args);
@@ -19,6 +20,13 @@ builder.Services.AddSwaggerGen(options =>
Title = swaggerOptions.Title,
Version = swaggerOptions.Version
});
// 把控制器与 DTO 上的 /// summary 注释纳入 OpenAPI 文档;调试页据此渲染端点标题。
var xmlDocumentationPath = Path.Combine(AppContext.BaseDirectory, $"{typeof(Program).Assembly.GetName().Name}.xml");
if (File.Exists(xmlDocumentationPath))
{
options.IncludeXmlComments(xmlDocumentationPath, includeControllerXmlComments: true);
}
});
var corsOptions = builder.Configuration.GetSection("Cors").Get<HostCorsOptions>() ?? new HostCorsOptions();

View File

@@ -0,0 +1,141 @@
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
namespace Flyshot.Server.IntegrationTests;
/// <summary>
/// 验证 `/debug` 在线 API 调试页的暴露策略与基础内容契约。
/// </summary>
/// <remarks>
/// 调试页与 Swagger UI 共用 <c>Swagger:Enabled</c> 开关,开关关闭时
/// 调试页与 `/swagger` 一同下线,避免生产环境意外暴露调试入口。
/// </remarks>
public sealed class DebugConsoleEndpointTests(FlyshotServerFactory factory) : IClassFixture<FlyshotServerFactory>
{
private readonly FlyshotServerFactory _factory = factory;
/// <summary>
/// 当 Swagger 启用时,`/debug` 应当返回完整的 HTML 调试页。
/// </summary>
[Fact]
public async Task GetDebug_WhenSwaggerEnabled_ReturnsConsoleHtml()
{
// 默认配置即开启 Swagger调试页应当作为浏览器可直接打开的 HTML 暴露。
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
using var client = configuredFactory.CreateClient();
using var response = await client.GetAsync("/debug");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.StartsWith("text/html", response.Content.Headers.ContentType?.MediaType);
var html = await response.Content.ReadAsStringAsync();
// 页面标题与稳定锚点用于回归保护:调试页骨架一旦丢失,测试立即报警。
Assert.Contains("Flyshot Replacement 接口调试", html, StringComparison.Ordinal);
Assert.Contains("id=\"debug-console-app\"", html, StringComparison.Ordinal);
// 控制器需要在返回 HTML 前把 Swagger JSON URL 注入到页面占位符里,
// 否则前端无法在加载时拉取 OpenAPI 文档。
Assert.Contains("/swagger/v1/swagger.json", html, StringComparison.Ordinal);
Assert.DoesNotContain("__SWAGGER_JSON_URL__", html, StringComparison.Ordinal);
}
/// <summary>
/// 当 Swagger 关闭时,`/debug` 应当与 `/swagger` 同步下线404
/// </summary>
[Fact]
public async Task GetDebug_WhenSwaggerDisabled_ReturnsNotFound()
{
// 显式把 Swagger:Enabled 置为 false此时调试页也不应当被访问到。
using var configuredFactory = CreateFactoryWithSwaggerEnabled(false);
using var client = configuredFactory.CreateClient();
using var response = await client.GetAsync("/debug");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
/// <summary>
/// 调试页需要从 Swagger JSON 中读取所有端点,因此 Swagger 文档必须包含基础和兼容层的代表性路由。
/// </summary>
[Fact]
public async Task SwaggerDocument_ContainsRepresentativeRoutesForDebugConsole()
{
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
using var client = configuredFactory.CreateClient();
using var response = await client.GetAsync("/swagger/v1/swagger.json");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
await using var responseStream = await response.Content.ReadAsStreamAsync();
using var document = await System.Text.Json.JsonDocument.ParseAsync(responseStream);
var paths = document.RootElement.GetProperty("paths");
// 这些路径分别覆盖:基础探活、状态快照、版本查询、上传飞拍轨迹四种典型形态。
AssertPathExists(paths, "/healthz");
AssertPathExists(paths, "/api/status/snapshot");
AssertPathExists(paths, "/get_server_version");
AssertPathExists(paths, "/upload_flyshot");
}
/// <summary>
/// 状态页应当提供跳转到调试页的入口,便于现场顺手跳转。
/// </summary>
[Fact]
public async Task GetStatusPage_LinksToDebugConsole()
{
using var configuredFactory = CreateFactoryWithSwaggerEnabled(true);
using var client = configuredFactory.CreateClient();
using var response = await client.GetAsync("/status");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var html = await response.Content.ReadAsStringAsync();
// 状态页头部需要至少一个指向 `/debug` 的链接,文案不强制以保留排版调整空间。
Assert.Contains("href=\"/debug\"", html, StringComparison.Ordinal);
}
/// <summary>
/// 检查 Swagger 文档中是否存在指定路径,兼容尾斜杠归一化两种形态。
/// </summary>
/// <param name="paths">OpenAPI 文档中的 paths 节点。</param>
/// <param name="route">期望存在的路由字符串。</param>
private static void AssertPathExists(System.Text.Json.JsonElement paths, string route)
{
// OpenAPI 生成器会把部分尾斜杠路径规范化,这里同时接受两种形态。
var withSlash = route.EndsWith('/') ? route : route + "/";
var withoutSlash = route.EndsWith('/') ? route.TrimEnd('/') : route;
Assert.True(
paths.TryGetProperty(withSlash, out _) || paths.TryGetProperty(withoutSlash, out _),
$"Swagger 文档应当包含路径 {route}");
}
/// <summary>
/// 构造覆盖了 <c>Swagger:Enabled</c> 配置项的测试宿主工厂。
/// </summary>
/// <param name="enabled">期望的 Swagger 启用状态。</param>
/// <returns>已应用配置覆盖的测试工厂。</returns>
private WebApplicationFactory<Program> CreateFactoryWithSwaggerEnabled(bool enabled)
{
return _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureAppConfiguration((_, configurationBuilder) =>
{
// 通过 InMemory 配置覆盖 appsettings.json 中的 Swagger 开关,避免修改磁盘文件。
configurationBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
["Swagger:Enabled"] = enabled ? "true" : "false",
["Swagger:DocumentName"] = "v1",
["Swagger:Title"] = "Flyshot Replacement HTTP API",
["Swagger:Version"] = "v1",
["Swagger:JsonRouteTemplate"] = "swagger/{documentName}/swagger.json",
["Swagger:RoutePrefix"] = "swagger"
});
});
});
}
}